8

文件分发系统

 3 years ago
source link: https://www.zenlife.tk/delivery.md
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

文件分发系统

2015-09-18

这个是业务上对一些基础设施的需求。我们的竞价进程启动后需要加载大量算法或其他依赖数据,目前部分数据是落地成文件,竞价进程运行时也需要动态更新加载,需要设计一个统一的文件推送和通知加载中心。

一些基本情况是:

  • 部分文件内容比较大,可能会达到200MB,单个文件多达1M行数。
  • 需要周期性更新,可能会按照10min一次的频率。
  • 客户端分布在大概50台左右服务器,不同数据需要推送到不同机器上,要求比较灵活。

之前同事的做法是完全用脚本写了一套系统,简单快速搭建起来。用的psync把文件依次同步到各机器。主要存在的问题是:

  1. 在同一个机器上很多psync,这个机器网卡打满,磁盘IO也非常高,成为传输瓶颈(效率)。
  2. 完成后的事件通知,传输过程中挂掉了重传,处理各种异常等等很多细节不合适用脚本(可靠性)。
  3. 整体的进度完成了多少,哪些传输挂掉了,错误原因是什么,用脚本都不好监控(监控)。

我思考了一下这个问题觉得,核心的是我们应该在底层做一套分布式的文件分发的项目,这个项目要做的工作是:把一些文件,推送到一批机器上面。外部只需要告诉它要干什么,剩下的事情交给它去做。这个基础设施做好之后,外围就可以围绕着做一些其它的功能,像监控,加载和通知之类的。

之前想着很高大上的做法,做P2P的实现,自己写底层的协议,为此还去研究了一下bittorrent的做法。结果投了半个月进去,东西遥遥无期,被叫停了。公司的文化决定了,这里不会允许在技术上做一些比较深远的投资。比如同事会说,他用脚本把这些东西弄起来,花了不过半天时间,言语中不无自豪。

有时候会感到很无奈,大家都知道存在问题,可能需要一些hard and dirty的方式才能解决,但没人站出来,因为有些东西的投资与回报根本不会被认可。反思之后,这次决定用更简单,更投机取巧的方式做事,而不是要做出来有多么牛B。

核心思想:中心化调度,分布式的数据传输。总体设计是有一个中心化的server服务器,负责任务的调度。每个机器上开一个daemon,负责与中心服务器通信,以及daemon之间的P2P数据传输。

每次传输任务叫一个Job,Job中包含传哪些文件,要传到哪些机器的信息。文件会被切片,方便做daemon之间的相互传输(P2P),不依赖于推送的那个daemon。中心server收到Job后,开始调度。它告诉各个daemon从哪里去下载哪个分片。各个daemon下载完毕后给中心server发ack。中心server收到ack就知道哪个daemon有哪些分片了。再对调度进行反馈。

为了简化系统的设计,完全是中心化的调度。中心化server只负责调度。daemon只负责执行。全局的状态全部在中心的调度服务器上面。还有进一步的简化是,组件交互全部走http协议,不要自己做底层了。比如说daemon通知中心服务器是访问http接口,分片下载完成ack也是http接口。通知daemon去执行下载操作http接口。甚至最极端的,数据传输直接是通过请求某个url完成下载。这样明确各个组件职责之后,围绕url的handler进行开发就非常方便了。Go语言做http太轻松。

下面把各个http接口的职责列一下。

  • /center/job 由daemon向中心调度发送任务信息,描述要将哪些文件传到哪些机器。
  • /daemon/pull center收到job请求以后,会开始执行调度。它会向daemon发送pull请求,告诉哪个daemon去哪个daemon下载某个块
  • /daemon/download daemon收到pull请求后,会去另一个daemon那里下载分片。下载其实就是GET方法调这个接口而已。
  • /center/ack daemon下载完一块chunk以后,会向center发送ack。这样center就知道哪台机器把哪一块下载好了,继续触发调度。
  • /daemon/pack center不断地收到ack。当它知道某一个daemon已经下载完毕所有分片,会用这个接口通知daemon去合并分片恢复文件。
  • /center/fin 某个daemon合并完文件后,它的job就完成了, 通过这个接口给center发送一个finish消息。

对外的接口是/daemon/api,方法是POST,内容是json格式,来描述一次推送的任务。

{
    files: [
        "/data/resource/warden/ctr/10026.txt",
        "/data/resource/warden/ctr/10028.txt",
        "/data/resource/warden/ctr/10062.txt",
    ],
    machines: [
        "192.168.10.60",
        "192.168.10.41"
    ],
    callback: ""
}

callback是一个URL地址。如果不提供,表示是同步接口。调用会阻塞,直到这次任务完成才返回。如果callback提供了,表示是异步接口。请求会立即返回。任务完成后,系统会回调callback接口。

再次重复下设计的核心思想:中心化的调度,分布式的数据传输。由于状态全部保存在中心化的调度中,像进度的监控,结果是成功还是失败,任务花费多长时间等这些东西都不难做。文件被分片了,数据传输是分布式的,不会依赖原始的推送的那台机器,于是不会成为瓶颈。

唯一的问题是,调度服务器现在是一个单点,挂了整个系统就挂了。为了增加可靠性,还要保证实现足够简单,我决定做REDO日志,挂了重启。

记录REDO日志。操作前要先写日志,再响应请求。传输的数据是持久化了的,操作的日志也是持久化了的,整个系统的状态不会丢。

日志要记录下三种类型的消息:

  • JOB 每次中心节点收到/center/job,记录下收到任务
  • ACK 每次中心节点收到/center/ack时,说明daemon已完成了某一块
  • FIN 每次中心节点收到/center/finish,说明daemon已经完成了某一个文件

只要记录下以上信息,是足够恢复整个系统的状态的。如果是daemon那边挂掉,直接返回一个任务失败就够了。如果调度节点挂了,它重启时会扫描日志,恢复到挂掉之前的状态,然后重新开始调度。

记录一个日志文件的offset。如果中间到了某个安全的点,可以记录一个check point,方便更快速的重启。

恢复过程从offset开始扫描log。如果遇到JOB日志,在内存中重建一个JOB。如果遇到ACK,对相应的JOB标记相应的块完成。如果遇到FIN,对相应的JOB标记文件完成。等log扫描完成时,进程就恢复到了一个挂掉之前的状态。

大概用了三天多时间弄了一个原型出来。要不要开源再看情况吧,公司的东西离开了使用场景的需求就变得毫无价值。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK