ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

第8章:RabbitMQ 生产者可靠性——Confirm、Mandatory 与 Return

第8章:RabbitMQ 生产者可靠性——Confirm、Mandatory 与 Return 1. 项目背景支付组把第 7 章的持久队列用上了仍然丢单客户端日志写着basic_publish okBroker 重启窗口里有一批订单没有进q.order.pay。复盘发现发布代码是channel.basic_publish(...) # 没有 confirm.select return 200 给收银台TCP 还在写缓冲区、Broker 还在路由、磁盘还没 fsyncHTTP 已经对用户说成功。对账只能靠「用户投诉」。另一类事故Routing Key 写错第 6 章已知会静默丢弃。开发认为 Confirm 能挡住——不能。Confirm 说的是「Broker 受理了这次发布」mandatory/Return 才说「有没有进任何队列」。两者缺一中台发布器都不合格。Fire-and-forget → 只保证帧离开进程 Confirm → Broker 按队列类型完成了「收妥」经典队列≈入队并非全球副本 mandatory Return → 至少路由到一个队列否则退回 delivery_mode2 → 要求持久化仍要结合 Confirm 才有意义immediate标志早已移除文档里再抄会 406。本章在经典队列上把发布器做对仲裁队列的多数派语义第 19 章再加码。还有一种假成功更隐蔽发布到存在的交换机、key 正确、Confirm 也回来了但没有任何消费者消息在队列里躺三天直到磁盘报警。Confirm 不负责「有人处理」。测试契约要拆成两层发布契约本章与消费契约第 9 章。收银台 200 必须绑的是订单库状态而不是 MQ Confirm——Confirm 只允许 outbox 从 NEW 改为 SENT。2. 项目设计小胖把外卖 App「下单成功」截图和骑手「未收到订单」的聊天一拼。小胖这不就是食堂窗口喊「下一号」吗我把盘子递出去了还要等人盖章才算卖出去太墨迹了大促 3000 单还等盖章黄花菜都凉了。大师你递盘子有三种终点倒进洗碗池没路由、放上出餐台进队列、进保险柜持久化确认。盖章就是 Confirm。不盖章也能喊下一号出事只能翻监控。延迟用异步 Confirm 本地 outbox摊平不是取消盖章。技术映射publish 返回 本地调用返回basic.ackconfirm Broker 收妥basic.return 没格口退货。小白Confirm 是连接级还是通道级序号从 1 涨重连后怎么办批量 waitForConfirms 失败哪几条没成功mandatory 和 Confirm 同时开未路由是 nack 还是 Return事务 tx 和 Confirm 能一起开吗持久化消息 Confirm 了是不是一定落盘大师Confirm 是Channel上confirm.select序号按通道从 1 递增重连必须当新通道、序号作废所以 outbox 只能用订单号/message_id。批量 API 失败往往只能「这批重发」所以批次要小、且全部幂等。未路由在开了 Confirm 的客户端上pika 常合成UnroutableError协议上是 Return可能仍伴随 confirm ack「我处理了这次发布结果是没人收」——以库文档为准测试要写死断言。tx 与 Confirm互斥源码cannot switch from tx to confirm mode。经典队列 Confirm 不保证掉电级 fsync 完成只保证进入队列实现的收妥路径要多数派换 quorum。小胖outbox 是不是又搞一张表我最烦双写。大师本地 outbox 与订单库同一事务写下单 「待发消息」另线程扫表 publishConfirm 再标已发送。这是把「先 200 再发 MQ」改成「先落库再发 MQ」。双写是代价换的是可对账。禁止只在内存字典当 outbox 当生产方案实验可以用 SQLite。技术映射Outbox 业务 DB 为真相Broker 为传播Confirm 传播回执。小白超时怎么定P99 多少算健康nack 要不要立刻重试mandatory是否每条都开confirm 的 multiple ack 客户端怎么对上 outbox 多行连接被 block内存告警时 wait confirm 会不会一直挂到超时大师超时从 P99 的 35 倍起预发先打 1000 条测。nack/Unroutable 立刻重试会放大错误 key。mandatory 在订单 Direct上默认开Fanout 营销若允许「零消费者」则不要 mandatory否则没人订阅就全 Return。策略按交换机类型写进发布器配置不要全局一把梭。multiple confirm 把「到某序号为止都成功」一次回来outbox 扫表应按序号区间批量标 SENT实验阶段用单条 wait 更简单。内存告警时连接进入 blockedpublish 会卡住直到解除或超时——超时必须标 FAILED 并告警禁止当成功。第 14 章会点亮 Alarm 做对照。小胖今天发布器三件套Confirm、mandatory、outbox 状态机。测 P99 只求数量级不求实验室。3. 项目实战3.1 环境准备q.order.payex.order.direct第 6 章。pip install pika。可选sqlite3标准库。3.2 步骤一无 Confirm 的假成功反面教材步骤目标展示「库返回了」不等于「队列里有」。# promo-mq/ch08/fire_forget.pyimportpika connpika.BlockingConnection(pika.ConnectionParameters(127.0.0.1,5672,promo,pika.PlainCredentials(promo,promo_dev_2026)))chconn.channel()# 立即关连接制造「写到一半」的窗口不一定每次都丢但是错误模式ch.basic_publish(ex.order.direct,pay.ok,bUNSAFE,propertiespika.BasicProperties(delivery_mode2))print(library returned, closing NOW)conn.close()运行结果多数时候消息仍在所以这个反例不能当稳定复现只能当代码评审红线热路径禁止无 Confirm。稳定验收靠步骤二。坑用「偶发成功」证明 fire-and-forget 安全是统计陷阱。3.3 步骤二Confirm mandatory 发布器步骤目标正确 key 确认成功错误 key 明确失败记录耗时。# promo-mq/ch08/confirm_publisher.pyimporttimeimportuuidimportpikafrompika.exceptionsimportUnroutableError,NackErrordefpublish(rk,body,timeout5.0):connpika.BlockingConnection(pika.ConnectionParameters(127.0.0.1,5672,promo,pika.PlainCredentials(promo,promo_dev_2026),heartbeat30,blocked_connection_timeout10,client_properties{connection_name:ch08-confirm}))chconn.channel()ch.confirm_delivery()propspika.BasicProperties(delivery_mode2,content_typeapplication/json,message_idstr(uuid.uuid4()),timestampint(time.time()),)t0time.perf_counter()try:ch.basic_publish(ex.order.direct,rk,body,propertiesprops,mandatoryTrue)dt(time.perf_counter()-t0)*1000conn.close()returnconfirmed,dt,props.message_idexceptUnroutableError:conn.close()returnunroutable,(time.perf_counter()-t0)*1000,props.message_idexceptNackError:conn.close()returnnack,(time.perf_counter()-t0)*1000,props.message_idif__name____main__:print(publish(pay.ok,b{orderId:P1}))print(publish(pay.typo,b{orderId:P2}))运行结果示例(confirmed, 2.1, c0a1...) (unroutable, 1.8, bb32...)源码通道进入 confirm 模式。handle_method(#confirm.select{}, _, #ch{tx {_, _}}) - rabbit_misc:precondition_failed(cannot switch from tx to confirm mode); handle_method(#confirm.select{nowait NoWait}, _, State) - return_ok(State#ch{confirm_enabled true}, NoWait, #confirm.select_ok{});mandatory 无队列时走process_routing_mandatory。坑每个请求 new Connection 会毁 P99生产必须连接池第 4 章。本脚本为清晰才短连接。坑confirm_delivery()必须在 publish 前。坑Fanout 零绑定也会 unroutable。3.4 步骤三本地 outboxSQLite 演示步骤目标同一「业务提交」先写 outbox再 Confirm失败可重扫。# promo-mq/ch08/outbox.pyimportsqlite3,time,json,pathlibfromconfirm_publisherimportpublish DBpathlib.Path(promo-outbox.db)definit():connsqlite3.connect(DB)conn.execute(CREATE TABLE IF NOT EXISTS outbox( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id TEXT UNIQUE, rk TEXT, payload TEXT, status TEXT, last_error TEXT))conn.commit();conn.close()defsubmit_order(order_id,payload):connsqlite3.connect(DB)conn.execute(INSERT INTO outbox(order_id,rk,payload,status) VALUES(?,?,?,?),(order_id,pay.ok,json.dumps(payload),NEW))conn.commit();conn.close()defdrain(limit20):connsqlite3.connect(DB)rowsconn.execute(SELECT id,order_id,rk,payload FROM outbox WHERE status IN (NEW,FAILED) LIMIT ?,(limit,)).fetchall()forid_,oid,rk,payloadinrows:st,ms,midpublish(rk,payload.encode())ifstconfirmed:conn.execute(UPDATE outbox SET statusSENT WHERE id?,(id_,))print(sent,oid,f{ms:.1f}ms,mid)else:conn.execute(UPDATE outbox SET statusFAILED, last_error? WHERE id?,(st,id_))print(fail,oid,st)conn.commit();conn.close()if__name____main__:init()submit_order(P20260828088,{orderId:P20260828088})drain()运行结果outbox中该行SENT。把 rk 改成 typo 再 submit 另一单状态停在FAILED可人工修 key 后重扫。坑生产 outbox 必须与订单表同库事务SQLite 只是实验。坑SENT 之后 Broker 丢失仍要靠消费者幂等 对账不是 outbox 万能。坑无限 FAILED 重试会打满 Broker次数上限见第 10 章。3.5 步骤四100 条确认延迟步骤目标打出 P50/P99 数量级写入容量笔记。# promo-mq/ch08/bench_confirm.pyimportstatisticsfromconfirm_publisherimportpublish ms[]foriinrange(100):st,dt,_publish(pay.ok,f{{i:{i}}}.encode())assertstconfirmed,st ms.append(dt)ms.sort()p50,p99ms[49],ms[98]print(fn100 p50{p50:.1f}ms p99{p99:.1f}ms max{ms[-1]:.1f}ms)运行结果文字本机 Docker 常见 P99 在数毫秒到几十毫秒若几百毫秒查磁盘、内存告警、是否每次新建连接。把数字贴 Wiki第 30 章再对比 quorum。坑短连接基准会偏坏注明「含握手」。连接池版才是应用真实 P99。把 100 次结果写入bench.csvorder_id, ms, status测试可以做回归同一机器上 P99 恶化 3 倍则发工单查磁盘与告警。不要和 quorum 的 P99 直接比那是第 30 章的事。源码里未路由且 mandatory 时会走 Return 路径而mandatoryfalse且零队列则直接丢弃——这就是第 6 章「静默丢弃」在发布器里的最后一公里。发布器规范写死订单通道mandatorytrue。3.6 完整代码清单column/samples/ch08/ fire_forget.py confirm_publisher.py outbox.py bench_confirm.py3.7 测试验证编号操作期望TC-CH08-01正确 key confirm mandatoryconfirmed队列 1TC-CH08-02错误 keyunroutable队列不变TC-CH08-03outbox drainSENTTC-CH08-04tx 后再 confirm.selectChannel 异常# 快速断言队列增量可用 HTTP GET messagescurl-s-upromo:promo_dev_2026\http://127.0.0.1:15672/api/queues/promo/q.order.pay|rgmessages值班检查单订单发布路径代码评审只问四个问题。第一通道有没有confirm.select。第二订单 Direct 有没有mandatory。第三HTTP 对用户成功是否发生在 outbox SENT 之后严格说应发生在订单库提交之后Confirm 只驱动 SENT。第四失败是按 unroutable / nack / timeout 分桶还是揉成一句「MQ 异常」。四个都「是」才允许发车。预发每天跑 100 条 Confirm 延迟P99 翻倍先查磁盘告警和 blocked 连接再查业务。事务 tx 看起来能把多条 publish 捆在一起但吞吐差、与 Confirm 互斥且回滚语义救不了已经给用户的 HTTP 200。推广中台明确禁用 tx 发布。需要原子时原子落在订单库不在 AMQP 通道。补充发布侧容量直觉单条 Confirm 在本机 Docker 上常常只要几毫秒但一到「每次新建连接 TLS 磁盘抖动」就会上百毫秒。所以第 4 章的连接池不是性能彩头而是 Confirm 能否上生产的前提。没有池化的 Confirm 发布器大促会自己打满握手然后把超时当成 Broker 故障。outbox 扫表线程还要限速避免 FAILED 风暴把 Direct 打满——这和第 10 章重试风暴是同一类问题只是发生在发布侧。从测试角度看Confirm 用例必须包含「正确路由」「错误路由」「Broker 短暂不可达」三种。只测第一种会让错误 key 在生产用日志去发现。不可达时 outbox 应停在 NEW/FAILED 而不是 SENT恢复后扫表补发消费者靠订单号幂等。这三句话写进契约发布列车才算有消费侧之前的防护网。Java 客户端的waitForConfirmsOrDie(timeout)在超时会抛异常务必捕获后把 outbox 标 FAILED而不是让异常冒泡成 HTTP 500 同时又把行标 SENT。pika 的confirm_delivery加basic_publish在不可路由时走 UnroutableError同样不要写成笼统 Exception 后默认成功。语言不同分桶必须相同confirmed / unroutable / nack / timeout / blocked。五个桶进同一个仪表盘值班才知道是绑定错了还是磁盘满了。把发布器做成团队共用的小库而不是每个微服务复制一份 pika 代码。重复实现会在某一天漏掉 mandatory。库的单元测试用 Mock 不够至少在 CI 对真实 Broker 跑 TC-CH08-01 与 TC-CH08-02。谁改发布器谁负责这两组用例继续绿。文档里写清Confirm 成功只表示「进了匹配到的队列」不表示短信发出、不表示磁盘永存、不表示有消费者在。这三句否定句比任何架构图更能挡住错误的 200。把它们贴在发布器类的文件头注释里比 Wiki 更靠近代码评审的眼睛。大促前的发布器演练建议断开 Broker 十秒观察 outbox 停留在 NEW恢复后 drain 补发人为改错 Routing Key观察 UNROUTABLE 进缺陷而不是重试死循环。三步都过发布侧才算和消费侧重试第 10 章接上。演练记录进发版检查单口头说「应该没问题」不算过。检查单没有附件日志同样不算过。把 Confirm 失败分桶截图一并附上方便对照第 14 章告警。没有分桶截图就当作演练失败第二天重跑。重跑仍不分桶发布器不允许合入主干。4. 项目总结优点与缺点策略优点缺点Fire-and-forget最快无法对账Confirm收妥可测延迟、要幂等重发mandatory发现无路由Fanout 空订阅会全失败Outbox与 DB 同命运要扫表、要去重AMQP tx通道内多步原子吞吐差且与 Confirm 互斥大促不用优点1失败可分类。2message_id 贯穿对账。3P99 可回归。缺点1Confirm ≠ 消费者成功。2经典队列 Confirm ≠ 掉电安全。3客户端把 Return 和 Nack 搅在一起。发布器状态机建议只允许NEW → SENT / FAILED / UNROUTABLE。SENT 之后不再因「用户再点一次」而自动重发除非业务状态机明确允许补偿。FAILED 进值班UNROUTABLE 进开发缺陷key 或绑定错误不要混成一种「MQ 不行」。适用场景订单、支付通知的发布路径。需要「发到了队列」的契约测试。与订单库一体的 outbox。预发测量 Confirm P99 作为容量基线。不适用日志 firehose用 Stream/批量无确认或不同 SLA用 tx 代替 Confirm把 Confirm 当「短信已送达」。注意事项序号不能跨连接。4.x 无 immediate。内存告警时发布阻塞超时要当故障而非成功。安全message_id 不要用自增可猜订单号当唯一机密。常见踩坑生产HTTP 200 在 Confirm 之前。收银台成功、队列没有。根因把库调用当收妥。Confirm 当路由成功。key 写错仍「成功」。根因没 mandatory。重连后用旧 delivery-tag 对账。根因通道序号重置。思考题经典队列上 Confirm 已返回节点立刻 kill -9消息是否一定还在与 quorum 对比一句。outbox 已 SENT消费者尚未 Ack用户再次点击支付应靠哪一层防双发短信附录 C第 7 章思考题参考答案题 1Confirm 后立刻掉电。经典队列不提供跨节点的多数派 fsync 承诺kill -9 仍可能丢最后一批。要更高安全换 quorumRa 日志多数派。Confirm 只说明 Broker 当时认为入队完成。题 2未 Ack 重启。消息会重新投递redeliveredtrue。消费者必须幂等。详见第 9 章。延伸阅读与资源SQLAlchemy 2.0从入门到进阶的实战之旅Dify 从入门到进阶LLM 应用平台实战修炼Java 工程师进阶从 JVM 生产排障到OpenJDK原理NumPy 从入门到生产落地全链路实战指南科学计算/向量化Redis 8 实战精讲从 CRUD 到源码构建高可用缓存系统Redis 实战修炼与原理进阶Python 3实战精进从脚本到高并发订单引擎python入门Rquests从菜鸟脚本到企业级SDK的网络实战圣经Milvus向量数据库实战修炼从 0 到 1精通向量检索与生产落地MongoDB 实战进阶与内核修炼后端工程师的 AI 转型第一课Ollama 与私有化大模型实战10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析
返回列表