SlackHQ 使用 Kafka 和 Redis 处理数十亿任务

查看原文

本文是 Slack 工程博客关于如何扩展他们的任务队列的分享,他们的任务队列具体来说运行任何不在 web request 中执行的任务,例如推送提醒,unfurl,账单计算等,高峰时期队列 qps 可达到 33k/per second,任务时长从毫秒到数分钟不等。初版设计在公司上古时代就在用了,还用了很多年。Scaling 的契机是遇上了一次生产环境的大故障,就出在这个任务队列系统上,简单来说数据库资源争抢导致任务执行时间变长直到 Redis 内存超限,Boom。恢复时还屋漏偏逢连夜雨的碰上了任务没法 dequeue。所以,事故后,他们的重新设计的目标是:在核心系统最小故障时间的前提下,做到不停机的升级。

  • 初版设计:[www1 www2 www3] => [redis1 redis2 redis3 redis4] => [worker1 worker2 worker3 worker4], www 可以任意发布任务到 redis,每个 worker 监听一到两个 redis。www 会做任务哈希分配到一个 redis 去,如果必要做一次 job deduplication, 忽略已在队列中的任务。worker 把任务从 pending queue 中挪走创建出异步任务执行之,执行成功就弹走任务,否则重试直到失败挪到failure queue去。听上去吧,就是非常常规的万金油式的队列设计。
  • 问题分析:如果 enqueue / dequeue 速率不匹配,就会导致 redis 内存超限,尤其是出在 slack 这样 qps 很高的应用, 一旦失衡一会会儿就崩溃了。worker 要去连很多 redis instance。加新的 worker 会给 redis 加负载,有潜在的拖垮系统的风险。
  • 潜在方案:1) 换成 Kafka,换成 durable storage,防止内存耗尽任务丢失 2) 提供更好地队列支持例如 rate-limiting, prioritization 3) 就干脆不用 Redis 好了。
  • 实际方案:在 Redis 前面加装一层 Kafka,处于时间人力的考量,算是 1) 的折衷: [www1 www2 www3] => [kafkagate] => [kafka] ===(relay by topic)===> [redis1] [redis2] [redis3] => [worker1 worker2 worker3].
    • kafkagate 是自撸的 Go HTTP stateless 服务,将任务 enqueue 到 Kafka。处于性能考量,他们 Kafka 选择了 leader ack request 后就认为写入好了,而不是等到任务 replicate 到 brokers 后才认为写好。这引入了丢失任务的风险,但也减少了拖垮系统的风险。这个层也使 job 优先路由到当前 AZ,且 failover 时路由到其它 AZ 变得简单。
    • relay 这层还是 Go service,使用的是叫做 JQRelay 的工具。关注的核心问题是 data encoding(很不巧地还碰上了 PHP/GO 的 JSON encoder 实现不一样)。Relay 还要考虑一点就是起实例的时候要获取一个 Consul 锁,锁住 Topic 的读。这个锁保证了实例死掉的时候其它实例可以快速接管任务。
    • kafka 有 16 个 brokers 运行在 i3.2xlarge EC2 instances (财大气粗?),每个 topic 32 partitions,replication factor=3,暂存两天。同时做压测,故障测试。
  • 上线:往两个系统写,新系统行为正确后,花几周时间慢慢切到新系统。