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