我们的消息服务早期发版用的是最原始的方式:直接重启进程。某次发版后客服反映有客户两小时前问的话没人回——重启那一瞬间正在处理的消息和还没应答的推送,全随着进程一起消失了。

消息服务不是普通 Web 服务:连接层随时可能在推消息,处理是异步的,粗暴重启丢的不是请求,是业务往来记录。整改后我们建立了一套发版纪律,零丢失跑了半年。

原理:三阶段切换

优雅停机的核心是让进程"善终":先停止接收新工作,再处理完手头的,最后退出。

  • 阶段一摘流量:进程从负载均衡摘除,回调推送不再路由到它

  • 阶段二排存量:等队列和正在处理的消息全部落库,设置超时上限

  • 阶段三才退出:确认无在途工作后结束进程,日志记录退出原因

排空的对象要具体:按 Eyun 文档的使用建议,同一 wId 的发送要进入队列避免并发直发,所以进程里真正要排空的就是这些按 wId 分队列的待处理工作,而不是笼统的"等一下"。

核心实现:信号驱动的停机流程

def on_sigterm(signum, frame):
    drain(stop_accepting=True)      # 摘流量,按 wId 队列排空
    close_workers(timeout=30)       # 限时收尾,超时强落库
    log.info("shutdown clean")
signal.signal(signal.SIGTERM, on_sigterm)

drain 负责排空各 wId 队列,close_workers 的超时是安全网:收尾不能无限等,超时就强制落库再退,宁可留"已接收未处理"的待续状态,也不丢记录。

滚动升级的幂等防线

滚动升级时新旧副本会短暂并存,同一条推送可能被投递到两边——这不是异常,是发布的正常代价,幂等必须扛住。幂等键选推送体里自带的 newMsgId(文档响应字段,十九位大数,业务侧按字符串处理):

防线

做法

拦住的问题

唯一索引

newMsgId 建唯一约束

双副本重复落库

条件更新

流水状态更新带前置条件

重复触发业务动作

摘除确认

摘流量后核对推送停止

切换窗口的错位

我们的踩坑发生在防线一没布的时期:滚动升级后同一条客户消息进了两次工单系统,客服按两条单处理,客户收到两次回复。newMsgId 唯一索引上线后,这类问题在数据库层面绝迹。

与发版窗口的配合

滚动升级解决技术问题,发布纪律解决业务问题:

  • 避开业务高峰,选凌晨或会话低谷时段

  • 升级前确认所有实例在线、队列无积压,带病不发布

  • 升级后发一条测试消息到文件传输助手走完整链路,确认无恙再放量

文档的快速开始也推荐用 filehelper 做首次验证——发版冒烟沿用同一个习惯,手机看到消息才算链路正常。

权衡:停机排空的超时怎么定

排空超时给太长,发布周期被拖长;给太短,收尾不彻底。我们的经验值是按各 wId 队列正常处理时长的两倍设置,配合监控观察实际排空耗时,每季度校准一次。没有普适数字,只有按自己链路的数据校准出来的数字。

总结

发版不丢消息靠三件事:三阶段停机流程、newMsgId 幂等防线兜住双副本窗口、发布纪律选对时机。消息服务的发版应该是无聊的——所有惊险都说明设计还有缺口。发送队列与限速要求见 Eyun 的 API 使用与安全,接入细节可通过 Eyun 平台咨询。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐