Flask集成MQTT在gunicorn eventlet模式下CPU占用过高问题
问题根因
这是flask-mqtt与eventlet协程模型适配的典型问题,和业务消息处理逻辑无关:
flask-mqtt默认封装paho-mqtt客户端时,内置消息循环的MQTT_CLIENT_LOOP_DELAY配置默认值为0,即每次消息轮询结束后不做等待直接进入下一轮- 代码开头调用
eventlet.monkey_patch()将原生socket操作替换为eventlet非阻塞实现后,paho客户端默认的阻塞等待网络事件逻辑失效,退化为无等待的忙轮询 - MQTT消息推送频率越高,轮询触发越频繁,单个eventlet worker会直接占满1个CPU核心的全部资源,和
on_message回调中是否存在业务逻辑没有关联。
在on_message回调中添加eventlet.sleep(0.1)触发断连的原因:
- 没有消息到达时回调不会触发,忙轮询问题依然存在
- 0.1s的延迟会在消息高频场景下持续累计,直接阻塞MQTT keepalive心跳包发送,超过broker超时阈值后就会被强制断开连接。
单独运行原生paho客户端脚本无CPU占用过高问题,是因为未做monkey patch时,paho默认的loop_forever()会在无消息时阻塞在socket等待状态,不会空跑消耗CPU。
修复方案
按落地成本从低到高排序:
方案1:补全flask-mqtt配置项(最简单)
在create_app的MQTT配置段添加MQTT_CLIENT_LOOP_DELAY配置,给消息循环增加10ms的轮询间隔,既不会产生可感知的消息延迟,也能彻底消除忙轮询:
app.config['MQTT_BROKER_URL'] = 'localhost' app.config['MQTT_BROKER_PORT'] = 1883 app.config['MQTT_KEEPALIVE'] = 20 app.config['MQTT_TLS_ENABLED'] = False # 新增以下配置 app.config['MQTT_CLIENT_LOOP_DELAY'] = 0.01 mqtt_client.init_app(app)
方案2:手动接管paho事件循环(兼容性最好)
如果配置项不生效(部分旧版本flask-mqtt没有暴露该配置),可以在mqtt初始化完成后手动关闭默认循环,将其交给eventlet调度:
mqtt_client.init_app(app) # 拿到flask-mqtt封装的底层paho客户端实例 paho_client = mqtt_client.client # 停止flask-mqtt默认启动的内置忙轮询循环 paho_client.loop_stop() def mqtt_eventlet_loop(): while True: # 执行一次非阻塞消息轮询,设置10ms网络等待超时 paho_client.loop(timeout=0.01) # 主动让出1ms执行权给其他协程(flask请求、socketio推送、心跳发送) eventlet.sleep(0.001) # 将MQTT循环放入eventlet独立协程调度 eventlet.spawn(mqtt_eventlet_loop)
方案3:修正gunicorn启动参数
原启动命令缺少eventlet worker的必要配置,补充后可避免协程调度异常,注意多进程模式下flask-mqtt、flask-socketio的状态无法跨进程同步,保持1个worker即可:
gunicorn -b 0.0.0.0:8000 --worker-class eventlet -w 1 --worker-connections 1000 --timeout 30 'app:create_app()'
避坑说明
- 不要在
on_message回调中添加大于0.01s的sleep操作,回调运行在MQTT循环协程内,过长等待会直接阻塞心跳和其他消息接收 - 无需更换gevent、asyncio类型的worker,flask-mqtt对这两类异步模式的适配bug更多,eventlet是当前flask-socketio+flask-mqtt技术栈兼容性最好的worker类型
- 若需要更高的消息吞吐,可以在消息回调内将业务逻辑丢到独立的eventlet协程池处理,避免阻塞MQTT核心循环
内容的提问来源于stack exchange,提问作者h2e
相关产品推荐
相关产品推荐

