1促销夜的两类客诉:重复扣款与支付网关被打满
某电商订单服务在促销峰值出现两类客诉:一是用户收到重复扣款短信,财务对账发现同一订单号被下游库存服务处理了两次;
二是支付网关 CPU 陡增,账户服务线程池打满并级联超时。链路追踪把根因钉在 Dubbo 消费端:
retries=2 叠加默认 failover,在库存 Provider 个别节点 GC 停顿约 800ms 时,
Consumer 判定超时并重发;库存扣减未做幂等,第二次重试成功落库,造成重复下单。
与此同时,订单对支付的超时设为 3000ms,中间还串了会员、优惠券两次 Dubbo 调用,各自占满预算,
到达支付层剩余时间不足 500ms,大面积 TimeoutException 后 Consumer 继续重试,
流量被放大到平时约三倍。值班同学第一反应是「加机器、加大 timeout」,却让排队更长。
这类事故发生在「会用 @DubboReference 注入」与「能在生产上安全调参」之间。
代理怎么建、序列化怎么选、负载均衡怎么挑实例、超时怎么传递、重试在什么条件下会反噬——
必须放到同一条调用链坐标系里讨论。本文按图解调用链(约 40%)、
问题现场(约 35%)、架构约束(约 25%)组织,
目标读者是已做过微服务开发、需要在故障时快速定位 RPC 层的工程师。
若你已经能从 Metrics 里读出消费端 RT 与失败率,但还说不清「这次超时之后框架有没有替你重试」,
那么第五章与第八章的 SOP 会比再读一遍注解文档更有用。
全文不涉及具体源码行号走读——Dubbo 3.x 仍在演进,稳定可依赖的是公开 API
(注解与 application.yml)与机制共识
(Invoker 链、Cluster 语义、注册中心 metadata)。
文中「复合场景」为教学合成,覆盖常见失败模式,不对应单一真实事故。
配置项与官方文档一致处视为已核验事实;性能对比若无标注则为作者经验或机制共识,不作为跨版本基准。
「Consumer 超时」不等于「Provider 未执行」。超时 Filter 在消费端结算等待预算时,
Provider 侧业务线程可能仍在跑写库逻辑;随后 failover 重试会让同一写操作再执行一次。
写接口必须显式 retries=0 + 业务幂等,而不能依赖「抛了异常就不会重试」。
值班同学常见的错误处置顺序是:先加机器、再加大 timeout、最后才去看 retries。 正确顺序应当反过来:先确认是否写接口在重试、链路深度与超时预算是否叠加、序列化与 version 是否在发布窗口漂移, 再决定是否扩容。加机器只能推迟线程池打满的时间点,不能消除「超时触发更多请求」的正反馈。 本文后续章节分别把序列化、负载均衡、超时重试拆开讲透,最后用清单、SOP 与决策矩阵收束成可落地动作。
分享时长建议四十分钟讲解加十分钟讨论,重点留给超时重试与踩坑复盘。 若读者已熟悉 Nacos 注册与 Sentinel 限流,可把注意力放在「Cluster 与 LoadBalance 正交」以及「deadline 倒扣」两处—— 这是大多数重复下单与雪崩事故的分水岭。
2Dubbo 3.x 一次 RPC 的完整链路
从业务线程执行 orderService.create(...) 到 Provider 方法返回,
数据与控制信息经过多层代理与过滤器。理解这条链,是后续讨论序列化、负载均衡、超时的共同坐标系。
以下对应 Dubbo 3.x 默认的 Triple(基于 HTTP/2,兼容 gRPC)或经典 dubbo 协议:
传输层不同,但 Invoker、Cluster、Directory 等上层模型一致(机制共识)。
把上表当作排障坐标系:超时与重复提交优先看 Cluster 与 Filter;流量倾斜优先看 LoadBalance 与 Router; 发布后突然 Serialization / No provider 优先看 Protocol 与 Registry metadata。 不要在未分层之前同时改 retries、timeout、weight——一次改多个旋钮会让复盘无法归因。
| 组件 | 公开入口 / 配置 | 职责 |
|---|---|---|
| Registry | dubbo.registry.address |
维护 Provider 地址与 metadata;Directory 订阅变更并刷新 Invoker 列表。 |
| ProxyFactory | @DubboReference |
为接口生成 Stub,业务代码无感远程化。 |
| Cluster | cluster="failover" |
多 Provider 时的容错:失败是否重选、重试几次、是否快速失败。 |
| LoadBalance | loadbalance="leastactive" |
在健康实例中挑选一个 Invoker,不决定重试次数。 |
| Filter | filter="-default,custom" |
横切:上下文、限流、日志、超时倒计时、泛化调用。 |
| Protocol | protocol="tri" / dubbo |
编解码与连接管理;Triple 更利于网关穿透与 gRPC 互通。 |
Dubbo 3.x 推荐新服务使用 Triple 协议(配置 protocol name="tri"),可与经典 dubbo 协议并存。
Triple 基于 HTTP/2,与 gRPC 互通;Java 接口仍可声明 Hessian2 等序列化,但 Consumer / Provider 必须一致。
来源:Dubbo Triple 协议文档。
Attachment、协议分工与 Filter 顺序
隐式参数通过 RpcContext.getClientAttachment() / getServerAttachment() 在 Filter 间传递,
典型用途包括 traceId、租户 ID、灰度标签。Dubbo 3 起应避免业务线程与 IO 线程间错误读取上下文;
异步 CompletableFuture 返回时,Provider 拿不到 traceId 往往是上下文未还原,而非「链路坏了」。
经典 dubbo 协议基于 Netty 长连接 + 自定义帧,默认 Hessian2,端口常见 20880,存量 Java 迁移成本最低。
Triple 更适合穿透七层负载与 Service Mesh。二者可在同一进程多 Protocol Export:对外走 Triple,对内 legacy 仍走 dubbo。
混合协议集群务必在 Nacos metadata 显式标注 protocol,否则 Consumer 可能按错误协议建连,
表现为连接超时而非序列化错误,容易误判为网络问题(作者经验总结)。
超时 Filter 在 invoke 前记录开始时间,收到响应或异常时结算;若 Provider 处理已超过 Consumer 剩余 budget,
Consumer 中断等待,Provider 侧可能仍在执行——这正是写操作必须幂等的机制原因。
自定义 Filter 注意 @Activate 的 group 与 order,避免在上下文尚未注入时清空 Attachment。
公开扩展点包括 org.apache.dubbo.rpc.Filter SPI。常见内置 Filter 负责上下文、限流统计、泛化调用、超时计数。
【复合场景】自定义 Filter 在 provider 端因 order 过大覆盖默认传递 Filter,导致审计字段丢失——
对比官方默认 Filter 列表与本地 SPI 配置即可定位。排障时若业务侧「明明传了 Attachment 却拿不到」,
优先查 Filter 顺序与异步边界,而不是先怀疑注册中心。
从架构层看,Dubbo 3.x 把「应用层 RPC 语义」与「传输层协议」解耦。Protocol 只影响 Client/Server 编解码与连接池实现, 不改变 Cluster、LoadBalance、Filter 的顺序。这意味着可以把问题稳定拆成三层: 注册发现有没有实例、选实例与重试是否合理、编解码是否一致——再进入具体协议细节。 混合协议集群若未在 metadata 标注 protocol,连接超时会被误判为「网络抖动」,值班同学往往会先加 timeout,反而掩盖根因。
3序列化选型:Hessian2、Kryo 与 Protobuf
序列化发生在 Protocol Client 将 Java 对象转为字节流、Provider 再还原的环节。
选错序列化器,轻则性能差一截,重则发版后 SerializationException 全站不可用。
| 序列化 | 配置值 | 格式特点 | 性能(机制共识) | 兼容与演进 | 适用场景 |
|---|---|---|---|---|---|
| Hessian2 | hessian2 |
二进制、支持 Java 对象图 | 中等;对象图深时开销上升 | 字段增删注意兼容;枚举勿依赖 ordinal | 存量 Java 微服务 |
| Kryo | kryo |
注册式,字节更紧凑 | 高;适合大对象、高 QPS | 类结构变更敏感;需维护注册表 | 纯 Java 内部性能路径 |
| Protobuf | protobuf / Triple 方向 |
IDL 驱动、强 schema | 高;字段编号演进规范 | 最佳跨语言与前后向兼容 | 新接口、网关 gRPC 互通 |
# application.yml — Dubbo 3.2.x
dubbo:
protocol:
name: tri
port: 50052
serialization: hessian2
provider:
serialization: hessian2
consumer:
check: false
@DubboService(version = "1.0.0", serialization = "hessian2")
public class OrderServiceImpl implements OrderService { ... }
@DubboReference(version = "1.0.0", timeout = 2000, retries = 0)
private InventoryService inventoryService;
【复合场景】订单服务发版给 DTO 新增字段却改了枚举 ordinal,或灰度 Pod 把 serialization 改为 kryo
且未用 version 隔离:部分 Consumer 仍用 hessian2 连上新 Pod,出现
HessianProtocolException / Serialization error。
处置:DTO 只增不改;枚举用 name;灰度用 version 隔离;禁止无约定的全局默认切换。
序列化耗时还取决于对象图形状:深嵌套 List/Map、循环引用、把 JPA 懒加载代理直接传出,都会放大 CPU 与异常面。 机制共识:Hessian2 对循环引用有处理,但深嵌套会显著放大字节与 CPU;Kryo 对未注册类可能 fallback 到较慢路径; Protobuf 要求字段有编号,改字段语义而不改编号是兼容演进的基本功。工程上建议:RPC 接口 DTO 与领域实体分离, 避免把持久化代理对象直接传出(可能触发序列化异常或超大对象图)。
对大字段(如报表 JSON、图片 Base64),优先考虑异步消息或对象存储直传,RPC 只传引用 ID。 作者经验总结:一次将约 200KB JSON 嵌在 Hessian2 DTO 内,在约 2k QPS 下 CPU 较 Protobuf 定长字段方案高约 25% (内部压测数量级,仅供选型参考,非跨版本基准)。若必须传大 payload,应单独压测序列化与网络带宽,而非沿用默认 Hessian2。
网关或运维工具可能通过 GenericService 泛化调用 Provider,参数以 Map/POJO 通用形式序列化,对类型信息更敏感。
Dubbo 3 文档建议泛化路径与常规定义接口使用相同 serialization。排障时若仅泛化调用失败、正常 Stub 调用成功,
应检查 generic=true 的 Reference 配置与参数是否符合 GenericObject 约定格式。
此外 fastjson2 等选项可读性好但 payload 偏大,一般用于调试或管理面,不建议作为核心交易链路默认项。
选型决策要点(可直接贴进评审清单)
- 新服务 + 可能跨语言:Triple + Protobuf IDL,一次定义多处生成。
- 存量 dubbo 协议 + 纯 Java:保持 Hessian2,优化 DTO(减少嵌套、避免巨型 Map)。
- 单集群极致性能:评估 Kryo,但必须配套类注册规范与发版检查。
- 禁止:Consumer 与 Provider 的 serialization 不一致;未约定却在全局切换默认值。
4负载均衡:随机、轮询、最少活跃与一致性哈希
LoadBalance 只在单次 invoke 的实例选择阶段生效;与 Cluster 的「失败重试」正交:
前者选谁,后者失败后是否换实例再调。策略均可通过 loadbalance 声明。
| 策略 | 配置值 | 行为摘要 | 适用 | 不适用 / 注意 |
|---|---|---|---|---|
| 随机 | random |
按权重随机,量大时趋近比例 | 无状态通用服务 | 实例少且 QPS 低时可能不均 |
| 轮询 | roundrobin |
平滑加权轮询 | 希望严格均摊、实例同质 | 慢节点仍分到等量请求 |
| 最少活跃 | leastactive |
选 in-flight 最少的节点 | 耗时差异大、需避让慢节点 | 假慢节点可导致健康节点过载 |
| 一致性哈希 | consistenthash |
同一 key 固定到同一 Provider | 本地缓存、分片状态 | 扩缩容迁移 key;需虚拟节点与失效策略 |
@DubboReference(loadbalance = "consistenthash",
parameters = {"hash.arguments", "0"})
private UserSessionService sessionService; // 第 0 个参数参与 hash
【复合场景】查询服务 4 个 Pod 中 1 个因磁盘 IO 抖动 RT 升高,leastactive 持续把新请求打向其余 3 个,
最终 healthy 节点也过载。应结合权重下调、Sentinel / 熔断摘除异常实例,而不是单纯依赖 LB 自愈。
注册 weight 与 warmup 预热能避免新 Pod JIT 未热就被打满。
权重、预热、路由与观测
注册时可携带 weight,random / roundrobin 会按权重分配。Dubbo 3 支持服务预热
(warmup 毫秒数内权重从低渐增至设定值),避免新扩容 Pod JIT 未热就被打满。
同源优先与本地 injvm 暴露时,排障需区分「本地调用接近 0ms」与「远程 RPC」日志,避免误判网络。
LoadBalance 之前,Directory 把注册中心地址转为 Invoker 列表,并应用路由规则(Router)。 公开能力包括标签路由、条件路由、脚本路由等。机制共识:路由先过滤再负载——例如灰度 tag=canary 只留金丝雀实例, 然后 LoadBalance 在子集中挑选。问题现场常见误判:以为 leastactive 失效,实际是路由把流量限制到 1 个 subset, 表现为「怎么调都打到同一台」。与 Spring Cloud 灰度配合时,可在网关写入 tag Attachment,Provider 注册不同 tag,Router 实现泳道。
# 条件路由示意(以官方语法为准)
# force: false 表示无匹配时 fallback 全量
dubbo:
consumer:
parameters:
router: tag
排障第一步确认当前请求的 Invoker 列表长度是否为预期实例数(QOS 或 metrics 中 directory size,具体 metric 名随版本略有差异)。 dubbo 协议默认长连接,leastactive 能持续感知各连接上 in-flight 请求数。 待验证推断:Triple / HTTP2 多路复用下 leastactive 与 random 差异可能缩小,应以 RT 与错误率为准,不必强行换策略。
5超时传递、Cluster 容错与雪崩式放大
超时与重试是稳定性里最容易被误配的两个旋钮。Dubbo 语义是:Consumer 在剩余预算内未收到完整响应则中断并抛
TimeoutException(或包装为 RpcException)。
优先级(机制共识,高到低):方法级 @Method > 接口级 reference > consumer 全局 > provider 端声明。
dubbo:
consumer:
timeout: 3000
retries: 0 # 写操作强烈建议 0
@DubboReference(methods = {
@Method(name = "createOrder", timeout = 5000, retries = 0),
@Method(name = "queryOrder", timeout = 1000, retries = 1)
})
private OrderService orderService;
默认 failover 下,网络超时、连接失败等临时故障可能触发重试;业务异常通常不重试。
但若 Provider 已执行完写库才超时返回,Consumer 仍可能重试——幂等性不能建立在「抛了异常就不会重试」上。
写操作应显式 retries=0 + cluster=failfast + 业务幂等键。
设 Consumer 数为 C、每端 retries=2,最坏时单次用户请求可产生 1+retries 次 Provider 调用;
若链路有 N 层且每层都重试,理论放大近似 (1+retries)^N。
Provider 已因 GC 或线程池满变慢时,重试进一步占用线程,形成正反馈——这与第一章支付网关被打满同构。
促销峰值下,即使单层重试「看起来只有两次」,叠加多层与多 Consumer 后,下游瞬时 QPS 仍可能翻数倍。
超时传递实践公式
设网关到订单服务 SLA 为 2000ms,订单内部串行调用库存(预算 600ms)、优惠券(400ms)、支付(剩余)。 推荐在订单服务入口 Filter 或框架层记录 deadline,下游 Reference 的 timeout 设为不大于剩余预算, 并预留 50~100ms 给序列化与网络。全链路超时不应简单相加各层配置值,而应以入口 deadline 倒扣。 若中间某跳必须同步且预算不够,应砍链路深度、合并接口,或把非关键路径改为异步消息,而不是把每层都调到 3000ms「看起来更安全」。
经验上,读接口可以给一次有限重试(retries 不超过 1),但必须确认读路径无副作用(例如「查询」接口内部不会顺带写审计表且无唯一约束冲突)。 写接口则应默认 failfast:失败交给上层业务补偿或用户重试,而不是让 RPC 框架替你「再试一次」。 这一区分写进接口评审模板,比事后在故障复盘里争论「当时为什么重试」便宜得多。
异步调用、Sentinel 衔接与 Provider 执行上限
Dubbo 3 支持方法返回 CompletableFuture<T> 的异步 RPC,Consumer 线程在发起 invoke 后可释放,
结果在回调中完成。异步模式下超时仍由同一 invoke 上下文计时,但业务若在回调中再次发起嵌套 RPC,
必须手动传递上下文或使用框架提供的上下文包装。复合场景中曾出现:异步回调里读取 RpcContext 为空导致租户 ID 丢失,
引发越权查询风险;修复需按官方异步文档推荐的上下文包装模式传递 Attachment。
Dubbo 可与 Sentinel 整合,在 Consumer 侧对 interface:method 做 QPS 限流与慢调用熔断。
机制上 Sentinel 规则通常在 Filter 链靠近业务的一侧生效:限流直接拒绝 invoke,熔断打开后快速失败,
不会增加 Dubbo 自带 retries。这与 failover 重试是两层防护:Sentinel 保护本服务线程与下游总量,
retries 控制单次调用的重复次数。大促前建议:写操作不依赖「熔断后换实例重试」,而应与 retries=0 双保险。
# 与 T12 联动 — 需引入 dubbo-sentinel 适配包
spring:
cloud:
sentinel:
eager: true
Provider 可配置 executes(最大并发)与线程池类型(fixed/cached/limited)。
当 Provider 线程池队列满时,Consumer 可能收到快速失败或超时,表现与网络超时相似。
排障时需看 Provider 侧线程池耗尽类日志(文案随版本略有差异),区分「下游慢」与「自身线程池满」。
处置应是扩容、调大 threads、优化慢 SQL,而非单纯加大 Consumer timeout——加大 timeout 只会更长占用 Consumer 线程,加剧排队。
不同小版本在超时递减与 Attachment 异步传递细节上可能有差异;上线前应用本服务 workload 复测 「入口 2s SLA + 三层串行调用」的实际剩余预算,不要照搬本文示例毫秒数。 全链路超时不应简单相加各层配置值,而应以入口 deadline 倒扣,并预留 50~100ms 给序列化与网络。
6什么时候继续用 Dubbo,什么时候该换路
Dubbo 擅长 JVM 微服务间的高性能、可治理 RPC:注册发现、多协议、集群容错、路由灰度都在同一套模型里。 它不擅长替代消息队列做最终一致,也不适合把大文件、长事务塞进同步调用。
适合继续用 Dubbo
- 多服务同步协作,需要明确超时、重试与负载策略
- 已有 Nacos / ZK 注册体系,团队熟悉 Java 契约
- 需要 version / tag 灰度、条件路由与权重预热
- 新接口可走 Triple,与 gRPC / 网关互通
- 读多写少且写接口已具备幂等键
不适合硬扛的场景
- 跨语言为主且团队无 IDL 治理能力——优先 gRPC 原生生态
- 需要削峰填谷、可延迟处理——用消息队列,而非加大 retries
- 超大 payload / 长耗时批处理——改异步或对象存储直传
- 强依赖「超时即未执行」假设的金融写——必须幂等,不能只靠 RPC
- 用 forking 换 RT 却下游容量不足——会把读流量翻倍
与 Spring Cloud OpenFeign 对比:Feign 偏 HTTP 资源语义与生态集成,Dubbo 偏高性能二进制与细粒度集群策略。 同仓库混用可以,但不要在同一业务写路径上叠两套默认重试。与 Mesh(如 Istio)并存时,超时预算应在入口统一设定, 避免 Sidecar 超时与 Dubbo timeout 双重截断却互不知情——否则会出现「Mesh 已断开、业务侧仍在重试」的幽灵流量。
从组织能力看,Dubbo 适合已经建立接口契约治理的团队:version / group / serialization / 幂等键有明确负责人。 若团队连「写接口默认 retries」都无法在 CI 拦截,先补治理比先上 Kryo 更有价值。 反过来,若契约、观测、限流已经齐备,Dubbo 3.x 的 Triple 与路由能力可以把灰度与多协议共存成本压到可控范围。
快速自检三问
- 这条调用失败后重试,业务上是否允许执行两次?不允许则 retries 必须为 0。
- 入口 SLA 倒扣后,最慢的一跳还剩多少毫秒?不够就砍链路或改异步,而不是每层都写 3000。
- 发版是否可能让 Consumer 与 Provider 的 serialization / version 短暂不一致?若会,灰度必须隔离。
7线上踩坑复盘与失败模式速查
案例一:重试 + 非幂等写 = 重复下单
【复合场景】促销期间库存扣减出现同一 orderId 两条成功记录。
Order→Inventory 使用默认 retries=2、cluster=failover;
某 Pod Full GC 导致首次超时,Consumer 重试成功,首次在 GC 结束后也完成处理,库无唯一约束。
处置:写接口改 failfast + retries=0;加幂等键;冲正重复数据。预防:评审与 CI 扫描 mutation 方法名上的 retries。
案例二:超时预算耗尽导致「越慢越调越多」
【复合场景】A→B→C 每层 timeout=3000ms,无 deadline 传递。C 变慢 2.5s 后,A 对 B 接近超时并重试, B 对 C 请求翻倍。处置:临时 retries=0;C 扩容;B→C timeout 收紧并开启超时递减;入口限流。
案例三:序列化切换引发局部不可用
【复合场景】灰度发布后约 5% 流量出现 RpcException: Serialization error。
根因是灰度 Pod 将 serialization 改为 kryo,注册中心未按 version 隔离,部分仍用 hessian2 的 Consumer 连到新 Pod。
处置:回滚 serialization;改用 provider version 做灰度分组。预防:任何序列化变更按接口维度灰度,禁止改全局默认后直接放量。
案例四:consistenthash 扩缩容缓存击穿
【复合场景】扩容后特定 userId 请求 RT 突增约十倍,DB 压力上升。会话服务用 consistenthash,本地缓存按 userId 键控; 节点数变化导致约四成 key 重新映射,新节点缓存冷启动。预防:扩缩容前后预热;或改用外部 Redis 统一缓存,LB 回到 random。
案例五:version / group 错配导致「幽灵」调用
【复合场景】测试环境正常,生产偶发 No provider,重启 Consumer 后恢复。 部分 Provider 注册 version=2.0.0,Consumer 仍引用 1.0.0;滚动发布期间两版并存,Consumer 缓存刷新滞后,短暂 list 为空。 处置:对齐 version;发布顺序改为先 Provider 后 Consumer;开启注册中心推送日志确认订阅更新。 预防:接口契约集中管理;禁止在 yml 与注解中重复定义不同 version。
案例六:Forking 集群误用于支付查询
【复合场景】为降低 RT 配置 cluster=forking、forks=2,每次查询并行打两个 Provider,
先返回者胜,另一请求白做,DB 读压力告警。Forking 仅用于极低延迟、只读、且下游资源充裕的特例;默认 failover 即可。
| 失败模式 | 典型异常 / 指标 | 首要怀疑点 |
|---|---|---|
| 重复提交 | 幂等键冲突、双份日志 | retries > 0 的写操作 |
| 链路整体变慢 | P99 上升、无单点错误 | leastactive 热点、下游线程池满 |
| 发布后 RpcException 激增 | Serialization / No provider | serialization、version、group |
| 间歇性 Timeout | 与 GC、抖动同步 | timeout 过紧或重试放大 |
| 流量不均 | 单 Pod QPS 2~3 倍 | 权重未生效、hash 热点 key |
8踩坑规避清单与 RPC 排障 SOP
上线前与季度巡检对照下表。建议将「写接口重试」「序列化一致性」纳入 CI:
扫描 @DubboReference 默认 retries,对 create/update/deduct/pay 等 mutation 方法名白名单告警。
| 检查项 | 标准 | 责任人 |
|---|---|---|
| 写接口重试 | 创建/更新/扣减 retries=0、cluster=failfast;业务幂等或唯一约束 |
服务负责人 |
| 读接口重试 | retries 不超过 1;timeout 明确;确认无副作用 | 服务负责人 |
| 超时与链路深度 | 入口 SLA 倒扣各层;嵌套启用超时递减;无「每层 3s」叠加 | 架构师 |
| 序列化一致性 | C/P serialization 一致;DTO 只增不改;灰度用 version 隔离 | 发布负责人 |
| LoadBalance 选型 | 有状态用 consistenthash 并评估扩缩容;无状态用 random / leastactive | 服务负责人 |
| 协议与端口 | 新服务优先 Triple;metadata protocol/port 与防火墙一致 | 运维 |
| 线程池与连接 | Provider threads/queues 与 CPU/RT 匹配;Consumer 连接无泄漏 | 运维 |
| 可观测性 | traceId 贯穿 Attachment;按 method 看次数、RT、失败率 | SRE |
| 大促预案 | 写接口 retries 复核;入口限流与重试不叠加放大;容量评估 | 架构师 |
发版前五分钟快检
- 对比本次变更接口的 Consumer/Provider
version、group、serialization是否成对一致。 - grep 写方法是否仍存在
retries缺省或大于 0。 - 看 Nacos 上新旧实例 metadata 是否混杂未隔离。
- 确认 Provider
threads与 Pod CPU 匹配(经验起点:核数 × 2~4,需压测修正)。 - 金丝雀 Pod 权重是否从低到高渐进。
QOS 生产默认关闭,仅在排障窗口临时开启;DEBUG 日志短时打开并控制采样,避免日志打爆磁盘。 Nacos 实例列表与 metadata 是「No provider / 协议错配」的第一证据源。 打开 DEBUG 前先确认磁盘与日志采集容量;排障结束后立即关闭,并把当时的 Invoker 列表长度、weight、serialization 截图归档到事故单。
排障决策树可再压缩为四步:① 单 Consumer 还是全 Consumer → 单点查配置与依赖版本; ② 单 Provider 还是全 Provider → 单点查 GC、线程池、网络; ③ 错误类型落在 Timeout / No provider / Serialization → 进入本表对应行; ④ 是否处于发布窗口 → 优先对比 metadata 与 version。 避免一上来调整全局 timeout 或 retries,那往往会掩盖根因而引入流量放大。
| 故障现象 | 排查工具链 | 根因定位要点 | 处置 | 预防 |
|---|---|---|---|---|
| 大面积 TimeoutException | dubbo_consumer_rt;链路追踪;Provider jstack / 线程池;注册列表 | 单 Provider 慢还是全量慢;retries 是否放大;GC 日志 | 临时 retries=0;扩容;收紧下游 timeout;入口限流 | deadline 传递;写禁重试;P99 告警 |
| No provider available | Nacos 服务列表;QOS;Consumer 启动日志 | version/group/interface;健康检查;注册连通 | 修复 Provider;对齐 version;恢复网络 | 健康检查;多 AZ;启动顺序文档化 |
| Serialization / Hessian 错误 | 对比依赖版本;日志中的 payload 类型 | serialization 不一致;DTO 不兼容;类加载隔离 | 回滚;统一 serialization;对齐包版本 | 发版检查表;灰度 version;DTO 规范 |
| 重复数据 / 重复扣款 | 幂等表;访问日志 request 次数;按 orderId 查重 | 同一 trace 多次成功;写接口 retries > 0 | 停重试;冲正;加唯一约束 | 写接口 failfast;幂等键;对账 |
| 流量倾斜单 Pod | 各 Pod QPS;weight 元数据;loadbalance 配置 | hash 热点;weight 未同步;leastactive 误判 | 调权重;扩容;换 random;缓存外置 | 扩缩容预案;缓存预热;热点监控 |
| 发布后错误率突增 | 灰度比例;堆栈聚合;注册 metadata | 仅灰度报错查 serialization/version;全量查依赖冲突 | 回滚;缩小灰度;修复 metadata | 金丝雀;自动化对比注册 metadata |
# Dubbo 3.x QOS(生产默认关闭,排障窗口临时开启)
# dubbo.application.qos-enable=true
# telnet localhost 22222
# ls -l
# invoke com.example.OrderService.queryStatus("ORD123")
# Nacos 查看实例 metadata
curl -s "http://nacos:8848/nacos/v1/ns/instance/list?serviceName=providers:com.example.OrderService:1.0.0:"
# 短时 DEBUG(控制日志量)
# logging.level.org.apache.dubbo.rpc.cluster=DEBUG
9决策矩阵、相邻知识地图与带走清单
9.1 决策矩阵
| 场景信号 | 继续用当前 Dubbo 配置 | 调整选型 / 迁移 | 关键旋钮 |
|---|---|---|---|
| 写接口偶发重复落库 | 否——默认 failover 不安全 | failfast + retries=0 + 幂等键 | cluster / retries / DB 唯一约束 |
| 存量 Java、无跨语言 | 继续 Hessian2 + dubbo/tri | 性能瓶颈再评估 Kryo(带注册表) | serialization / version 灰度 |
| 新接口、网关 gRPC 互通 | 可短期 Hessian2 过渡 | Triple + Protobuf IDL | protocol=tri / IDL 字段编号 |
| 本地会话缓存粘滞 | consistenthash + 预热 | 缓存外置 Redis 后改 random | hash.arguments / 扩缩容预案 |
| 下游慢导致全链路超时 | 否——勿先加大全局 timeout | deadline 倒扣 + 限流 + 临时禁重试 | timeout 分层 / Sentinel / retries |
| 需要削峰与最终一致 | 否——RPC 同步不适合 | 消息队列 + 补偿(非加大 retries) | 异步边界 / 幂等消费 |
9.2 相邻知识地图
- T11 Nacos:注册 metadata、多环境隔离与配置推送,决定 Directory 看到什么实例。
- T12 Sentinel:出站限流与熔断,和 Dubbo retries 是两层防护,不可互相替代。
- T14 SkyWalking:把 Attachment 中的 traceId 与 Span 判读结合,完成「慢在哪一层、是否发生过重试」闭环。
9.3 带走清单
- 一次调用经 Proxy → Cluster → LoadBalance → Filter → Protocol;排障先定层,再动参数。
- LoadBalance 解决「选谁」,Cluster 解决「失败后怎么办」;写操作默认 failfast + retries=0。
- 序列化在 Hessian2 / Kryo / Protobuf 间按生命周期决策,禁止 Consumer/Provider 不一致。
- 超时按入口 deadline 倒扣;重试在慢节点场景会放大流量,必须与限流协同。
- 重复下单、序列化崩溃、流量热点都有检查表与 SOP——关键是发版前扫描,而非事后救火。
- 调用链各层职责不同:不要把 LoadBalance 当成容错,也不要把 timeout 当成 Provider 性能指标——二者分别解决选择与等待。
- 注册 metadata(protocol、serialization、weight、tag)与 Consumer 引用必须成对审计,发布窗口优先查 metadata 漂移。
若只带走三句话:第一,写操作禁重试并补幂等;第二,超时按入口倒扣而不是层层写满三秒; 第三,任何序列化与协议变更用 version 隔离灰度。做到这三点,第一章两类客诉的大多数路径会被提前切断。
测试环境部署三个 Provider,分别调整 weight 与 loadbalance,用压测统计命中分布; 再对写接口故意设置 retries=2 并模拟慢节点,观察是否出现重复落库——比只读文档更能固化「写操作禁重试」。
本文交付的是调用链坐标系 + 序列化/LB/超时机制 + 失败模式 + 清单/SOP + 决策矩阵。 机制描述属机制共识,场景数字属数量级经验,上线前必须用本服务 workload 复测。 把检查表里的「写接口重试」和「序列化一致性」两项真正挂进流水线之后,第一章的复合场景才会从「每年大促必演一次」变成「偶发可定位的个案」。
最后提醒:Dubbo 参数表很长,但真正决定线上生死的往往只有少数几个——retries、timeout、serialization、version、cluster、loadbalance。 先把这六个旋钮的职责边界讲清楚,再去讨论线程池细调与协议迁移,路径会短很多。 参数默认值会随小版本变化,任何写进 runbook 的数字都应以当前版本官方文档与本集群压测为准,而不是以本文示例毫秒数为准。 把这份决策矩阵贴进架构评审模板,下一次大促前逐行打勾,比临时加机器更能降低重复下单与雪崩式重试的概率。 做得好的团队,会把这六个旋钮的变更当作和生产变更同等级别的评审项来对待,而不是发版当天临时拍板。 评审记录本身也是事故复盘时最便宜的证据。