Celery 导致 Redis 服务器内存不断增长问题的排查

最近上线了一个Django + Celery的项目,使用Redis做broker,但发现Redis所在的服务器内存使用量会缓慢增长,大概2个星期左右内存耗尽,Redis进程挂掉,所有的Worker也都停止工作。
我的服务器内存是8GB,正常情况 Redis 服务器的内存只使用1GB左右。
查了下内存监控,历史数据如下:
服务器内存使用
最一开始怀疑是 Django settings 中的 DEBUG 设置成了 True 导致的内存泄漏,因为 Celery Worker 启动时候会提示:

UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!

但查了一遍后发现并不是。

后来发现是因为我的定时任务设置的是每秒执行一次, Celery Worker消费任务的速度赶不上 Beat 产生任务的速度,导致了任务积压

以下是发现事件真相的详细过程:

登录 Redis:

127.0.0.1:6379> KEYS * 
1) "_kombu.binding.celery"
2) "unacked_index"
3) "_kombu.binding.celery.pidbox"
4) "unacked"
5) "unacked_mutex"
6) "celery"
7) "_kombu.binding.celeryev"

127.0.0.1:6379> TYPE celery
list
# 查看 “celery” 长度
127.0.0.1:6379> LLEN celery
(integer) 2017633

在 Redis 中,“celery” 这个key记录的是由任务队列中还未被消费的任务,居然有200多万!

顺便查了下 “celery” 里面存了些什么:

127.0.0.1:6379> LRANGE celery 0 0 
"{\"body\": \"W1sxMDcwXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d\", \"content-encoding\": \"utf-8\", \"content-type\": \"application/json\", \"headers\": {\"lang\": \"py\", \"task\": \"screen.tasks.flush_cluster_status\", \"id\": \"0c5b8e80-9d8d-4a27-aa2c-98c20a43dbbf\", \"shadow\": null, \"eta\": null, \"expires\": null, \"group\": null, \"retries\": 0, \"timelimit\": [null, null], \"root_id\": \"2b52932d-2ad6-4c30-bd0d-61dcb1066a45\", \"parent_id\": \"2b52932d-2ad6-4c30-bd0d-61dcb1066a45\", \"argsrepr\": \"(1070,)\", \"kwargsrepr\": \"{}\", \"origin\": \"gen15149@HC-24-93-72.h.chinabank.com.cn\"}, \"properties\": {\"correlation_id\": \"0c5b8e80-9d8d-4a27-aa2c-98c20a43dbbf\", \"reply_to\": \"8d7eb4dc-bcdc-3442-be4d-03a32f078c36\", \"delivery_mode\": 2, \"delivery_info\": {\"exchange\": \"\", \"routing_key\": \"celery\"}, \"priority\": 0, \"body_encoding\": \"base64\", \"delivery_tag\": \"3407cf25-862a-45cc-8898-679b0197b09b\"}}"
# 进入 Python
In [1]: import json

In [2]: s = "{\"body\": \"W1sxMDcwXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d\", \"content-encoding\": \"utf-8\", \"content-type\": \"application/json\", \"headers\": {\"lang\": \"py\", \"task\": \"screen.tas
   ...: ks.flush_cluster_status\", \"id\": \"0c5b8e80-9d8d-4a27-aa2c-98c20a43dbbf\", \"shadow\": null, \"eta\": null, \"expires\": null, \"group\": null, \"retries\": 0, \"timelimit\": [null, null], \"root_id\": \"2b52932d-2ad6-4c30-bd0d-61dcb1066a45\", \"parent_id
   ...: \": \"2b52932d-2ad6-4c30-bd0d-61dcb1066a45\", \"argsrepr\": \"(1070,)\", \"kwargsrepr\": \"{}\", \"origin\": \"gen15149@HC-24-93-72.h.chinabank.com.cn\"}, \"properties\": {\"correlation_id\": \"0c5b8e80-9d8d-4a27-aa2c-98c20a43dbbf\", \"reply_to\": \"8d7eb4d
   ...: c-bcdc-3442-be4d-03a32f078c36\", \"delivery_mode\": 2, \"delivery_info\": {\"exchange\": \"\", \"routing_key\": \"celery\"}, \"priority\": 0, \"body_encoding\": \"base64\", \"delivery_tag\": \"3407cf25-862a-45cc-8898-679b0197b09b\"}}"

In [3]: json.loads(s)
Out[3]:
{'body': 'W1sxMDcwXSwge30sIHsiY2FsbGJhY2tzIjogbnVsbCwgImVycmJhY2tzIjogbnVsbCwgImNoYWluIjogbnVsbCwgImNob3JkIjogbnVsbH1d',
 'content-encoding': 'utf-8',
 'content-type': 'application/json',
 'headers': {'lang': 'py',
  'task': 'screen.tasks.flush_cluster_status',
  'id': '0c5b8e80-9d8d-4a27-aa2c-98c20a43dbbf',
  'shadow': None,
  'eta': None,
  'expires': None,
  'group': None,
  'retries': 0,
  'timelimit': [None, None],
  'root_id': '2b52932d-2ad6-4c30-bd0d-61dcb1066a45',
  'parent_id': '2b52932d-2ad6-4c30-bd0d-61dcb1066a45',
  'argsrepr': '(1070,)',
  'kwargsrepr': '{}',
  'origin': 'gen15149@HC-24-93-72.h.chinabank.com.cn'},
 'properties': {'correlation_id': '0c5b8e80-9d8d-4a27-aa2c-98c20a43dbbf',
  'reply_to': '8d7eb4dc-bcdc-3442-be4d-03a32f078c36',
  'delivery_mode': 2,
  'delivery_info': {'exchange': '', 'routing_key': 'celery'},
  'priority': 0,
  'body_encoding': 'base64',
  'delivery_tag': '3407cf25-862a-45cc-8898-679b0197b09b'}}

可以看出,这里面的每一条是一个任务的id、函数名、参数等详细信息。

因为我的程序逻辑是每秒刷新一次缓存,所以过期的任务就不再有意义,所以我简单粗暴地使用 DEL celery把 “celery”的值清空,然后把程序里面周期任务的间隔改成了1.2 秒一次,重启了 Celery Beat进程,一切就正常了。

由此我总结了以下几点:

  1. 周期性任务的时间间隔要综合考虑时效性和Worker消费任务的速度,不能单方面为了提高时效性而改小时间间隔
  2. 项目上线后要关注 Redis 中 “celery” 值的长度,一旦发现这个长度在增加就说明 Worker 的消费能力不足,需要增加 Worker 或增大周期任务的周期
  3. 在周期任务需要考虑时效性的情况下,我们要通过缩小时间间隔尽量压榨服务器集群的资源,此时周期任务的时间间隔有一个“最小安全值”(我自己起的名字 ? ),这个值需要通过衡量 Worker 的消费能力来确定,比如上面说的1.2秒,如果小于这个值就会出现“压爆内存的最后0.1秒”的情况。另外,如果一台Worker所在的服务器挂掉导致消费能力不足就需要考虑任务积压的问题了。
  4. 可以通过设置 Redis 将内存中的数据持久化到磁盘来避免内存爆掉的情况,但这只是一个安全策略,并没有从根本上解决问题,实际上在正常的情况下 Celery 是不需要占用很多 Redis 空间的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值