背景
项目中一直使用 Redis 作为 celery 的 broker,会出现偶发的任务重复执行的情况,再加上一直没有队列的监控工具,任务没有持久化,存在一定的风险。故开启了 broker 由 Redis 迁移至 RabbitMq 的路程。
版本
- Flask==0.10.1
- celery==3.1.18
如何无缝迁移 ?
由于不能停服迁移,最大的问题就变成了如何保证原有 Redis 中的任务都能正常执行完成,且新的任务在 rabbitmq 中正常运转。
话说 celery 提供了 celery migrate 消息迁移工具,主要通过 task_id 或 task_name 匹配 queue_name 来迁移,但是由于之前项目未做持久化,这样比较麻烦风险比较大。
解决方案
- 在服务器上临时目录下再拉一套原代码
- 新启一个celery服务,记为 celery_old_service,并指定broker地址为原 Redis 的地址,指定PYTHONPATH为临时目录下的原代码路径。待这个迁移前celery服务正常运转,就可以关闭原服务的celery服务了。
- 发版,上迁移后的代码。此时需要观察两个事情:
(1)新的任务已经进入 RabbitMq 中正常执行 ;
(2) 若原来 Redis 中还有没执行完的任务,可以发现 celery_old_service 还在正常执行剩余的任务,待任务全部消耗掉,即可关闭 celery_old_service。
坑
- 任务大量堆积,占用大量内存,拖垮同一集群中的其他服务:
解决方案:
(1)调研阶段要做好风险预估,了解集群配置,多方面考虑,上线后,持续观察 mq 生产消费速率,;
(2)代码优化。为了降低业务风险,本次迁移不做业务的单独优化,所以这种老业务中的坑,可能不会注意到,在线上爆出来才发现,这时候只能紧急回滚,通过代码优化/重构解决任务堆积的问题之后再重新上线。 - celery 每个任务的执行结果都会产生一个临时队列,虽然这个临时队列生存期只有24h,但是大量的无效队列会污染整个集群。
解决方案:不产生结果,通过配置解决
CELERY_RESULT_PERSISTENT = False
CELERY_IGNORE_RESULT = True
- celeryenv 没有consumer
每个服务器都会产生一个 celeryenv 的队列,由于此类队列都是通过routing_key和exchange路由的,一旦有的队列失去了消费者,就会造成消息大量堆积
解决方案:通过配置解决,一分钟之后没有消费者的celeryenv队列自动删除
CELERY_EVENT_QUEUE_EXPIRES = 60
- 由于之前使用 Redis,项目启动用 supervisor 管理,同一个队列会配置启多个进程(numprocs大于1),会造成 celery 的 node 命名冲突, 告警信息如下:
UserWarning: A node named [email protected] is already using this process mailbox!
解决方案:启用一个进程(numprocs=1 ),配置多个并发进程(–concurrency)
- task 任务数据序列化
由于之前使用 Redis,设置为[‘pickle’, ‘json’, ‘msgpack’, ‘yaml’], 改为 rabbitmq 之后,序列化改为了 [‘json’],此时就要去梳理项目中每个异步任务了,检查参数是否符合 json 格式,容易出错的参数类型有 byte, datetime, 以及key是数字的dict 等。