文件分发系统

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扫描完成时,进程就恢复到了一个挂掉之前的状态。

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

deliveryp2p