场景

有一个需求是用户可以自己设定时间, 定时推送给用户通知, 例如邮件订阅每日新闻.

Golang 定时任务Golang cron

有的是一个系统:

有的是一个库:

其中不乏Star上千的优秀项目, 却发现并不适合我的业务需求:

  • 上百万量级的定时任务支持. 而上面的项目没有说性能 也没有指出如何扩展性能.
  • 任务入库, 可以增删改查. 这点除了robfig/cron的框架, 上面所述的系统都是支持的, 但他们要么不支持API, 要么还没写文档.....
  • 简单. 至少在我看来以上的系统都没有提供充足的文档让它看起来简单清晰, 特别是故障恢复和如何提升性能部分.

那么是时候自个儿思考下如何做一个适合项目的定时任务系统了. 正好, 这也是我去了解分布式与高可用的入门题.

目的

分布式

这个程序应当很好扩展出分布式架构, 最好是去中心化的, 因为我能力有限, 不想引入像etcd这样繁琐的依赖.

简单

不重复造轮子, 尽量使用开源项目. 一是更快完成需求, 二是这样方便维护

重新思考

研究上述优秀项目后启发也很大, 抱着少重复造轮子的想法, 所以对于已有的东西能用则用. 故

  • cron库能够被很好的使用, 用于计算下一次运行时间.
  • 用消息队列来保证任务执行的可靠性与性能

一个任务执行一次的流程如下:

  1. 计算任务的下一次执行时间并存放在DB
  2. 调度器每1s取出需要被执行的任务(run_at < now)发送给消息队列, 并更新下一次执行时间
  3. 消息队列将任务消息分发到执行任务的机器
  4. 消费者执行任务.

由于我们引入了消息队列, 所以我们不再需要设计整个系统中最复杂的部分: 任务消息分发. 这交给值得信赖的NSQ来做十分合适, 当然如果你想使用其他消息队列如kafka也可以.

接下来是详细设计

数据库我们可以设计如下

id cron next_run_at topic body
1 * * * 10 10 1111111 send_email {"to":"zbysir@qq.com","content":"hello world"}

其中

  • cron: cron表达式, 更多请看
  • next_run_at: 使用这个cron库能方便的计算出下次运行时间.
  • topic: 也就是Nsq消息队列中的topic, 可以用于当做要执行的任务类型, 如这里是用来发送邮件.
  • body: 任何消息体.

我们可以给next_run_at添加索引已获得更好的性能, 因为我们需要使用这个字段排序.

现在只需要写一个简单的程序, 每一秒去读取一下需要执行(next_run_at < now)的任务, 并Publish到NSQ就好了.

就是这样简单, 你不需要找一个专门为定时任务而设计的框架, 使用这个方案 就算加上增删改查功能 代码也不超过几百行.

如何升级性能

不用太担心性能, 整个项目的性能瓶颈只取决于MySql的读取速度, 而对于MySql的优化建议网上一搜一大把, 比如分库: 我们可以将发送邮件 和 发送短信这两个任务分为两个库, 或者将用户id<10000的定时任务分一个表, id>10000分一个表, 这和写你的业务代码一样.

而NSQ的性能我们也无需担心, 反正都是可以无限水平扩展的.

我建议的架构拓扑图如下:

如果你了解NSQ的话, 你会发现这其实就是NSQ建议的分布式部署方式. 整个系统非常简单, 没有任何需要额外理解的逻辑.