最近上线了一个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\": \"[email protected]\"}, \"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\": \"[email protected]\"}, \"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': '[email protected]'},
'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进程,一切就正常了。
由此我总结了以下几点: