1.1 需求拆解:调用多、发现难、排障慢
拆完服务的第一周,线上就出了三起故障:订单服务调用户服务,对方改名部署后调用方还是旧地址,直接 502;用户服务一次扩容,新机器上线没人通知,流量全打在老机器上;一次支付超时,我们花了四十分钟才定位到是第三方网关的问题。这三件事把「必须做服务治理」从口号变成了 KPI,也决定了这一篇的写法:不谈理念,只把痛点翻成数字。
场景:把痛点量化
我们先把三类痛点翻成数字,避免后面选型拍脑袋。
| 痛点 | 当时的状态 | 量化指标 |
|---|---|---|
| 调用关系多 | 17 个服务互调 50 多条链路,写在 yaml 里 | 地址变更平均 3 天才全量同步 |
| 服务发现难 | 手动维护 IP 列表,扩缩容靠喊 | 扩容后流量不均,最大节点比最小高 47% |
| 排障慢 | 日志散落在 9 台机器上 grep | 一次 P2 故障平均定位 35 分钟 |
| 无熔断 | 用户服务抖动时订单服务跟着雪崩 | 故障放大率约 8 倍 |
| 无灰度 | 全量发布,回滚靠重发旧包 | 回滚平均耗时 12 分钟 |
雪崩那次印象最深:用户服务的缓存层抖了 20 秒,订单服务没有超时控制也没有熔断,线程池被拖满,连网关的 /health 都开始超时,故障从 1 个服务扩散到 5 个,影响面拉大到 20 分钟。这就是需求拆解的核心结论:调用链越长,缺治理的代价是指数级的,不是线性的。
还有一个容易被忽略的痛点:发布节奏被绑死。17 个服务共用一套 CI,一个团队改用户服务要等整个流水线排到才发版,跨团队协作全靠拉群。拆服务本来是想要独立性,结果治理缺位把独立性又丢了。这类「隐性成本」量化起来难,但按人力算,每周约 0.5 人日耗在协调上。
量化方法也很朴素:拉三周 oncall 记录,按故障类型打标(依赖故障、配置错误、容量不足、发布问题),统计每类的发生次数、影响时长、参与人数。35 分钟的平均定位时间就是 22 起 P2 以上的中位数;故障放大率 8 倍来自那次缓存抖动的监控曲线,入口 QPS 从 1200 跌到 150,而下游故障服务的错误率只有 12%。把这类原始数据存档,后面每个 P0 组件上线后都能回头对照,验证「治理是否真把数字改好了」。
方案:按调用链分层治理
需求排出来后,方案不用一步到位,按「先保命、再提效」分三层落地。
| 层次 | 解决什么 | 落地组件 | 优先级 |
|---|---|---|---|
| 保命层 | 故障不再放大 | 熔断器、超时控制、降级兜底 | P0 |
| 基础层 | 服务互相找得到 | etcd 注册发现、客户端负载均衡 | P0 |
| 提效层 | 发布与排障提速 | 按版本灰度、TraceID 全链路、日志归集 CLS | P1 |
判断顺序的依据很简单:没有熔断,扩再多机器也是给雪崩续命;没有注册发现,熔断的实例列表都拿不到;这两层稳了,灰度和追踪才有意义。资源分配也是这个顺序:两个全职加一个兼职先做保命加基础层共六周,提效层排到下一个迭代,这样第一个月线上就能摸到效果,团队信心比进度表重要。
分层的另一个好处是回退粒度:保命层上线后即使提效层延期,系统也远比拆分前稳;反过来如果先做追踪再做熔断,故障放大期间产出的只是「更清晰地看到雪崩」,价值打折。
需求清单:从痛点到验收项
三层方案落下去之前,我们把需求写成一份可验收的清单,每条对应一个可测的行为。这份清单后来直接变成治理框架的测试验收表。
| 编号 | 需求描述 | 验收方式 | 对应章节 |
|---|---|---|---|
| R1 | 任意服务实例下线后 10s 内调用方不再发流量 | kill 一台后观察流量分布 | 2.3 |
| R2 | 下游错误率超阈值时自动熔断,恢复后半开探测 | 注入故障脚本回放 | 3.1 |
| R3 | 超时请求统一在调用侧截止,不依赖服务端 | 压测时断网模拟 | 3.3 |
| R4 | 新版本服务可按 5%、20%、50% 三档放量 | 灰度控制器 API | 4.1 |
| R5 | 一条 traceId 可串起跨 5 个服务的日志 | CLS 检索实测 | 4.2、4.4 |
| R6 | 单机故障不导致整体不可用 | 逐台停机演练 | 5.2 |
R1 的 10s 是刻意选的:15s 租约过期加 300ms watch 传播,理论上限 15.3s,但调用方本地缓存策略会把实际感知压到 10s 内,这条数字在 2.3 会反复用到。清单的好处是逼着每个需求都有测试脚本,框架合入主干前必须全绿,避免了「治理能力」停留在演示阶段。
每条需求还标注了责任人:R1、R2 归基础服务组,R3 归网关组(因为调用侧统一在网关执行),R4、R5 归 SRE 组。责任到人之后,跨组扯皮的时间从每天 1 小时降到 10 分钟,这比任何技术细节都更影响落地速度。
配置与代码:从「发现难」开始拆
先把最疼的服务发现摸清楚。当时每个服务启动参数里硬编码下游地址,改一次要发 17 份配置。我们的拆法是用 etcd 的 lease 机制:服务启动时注册 gov/services/{name}/inst/{id},挂 15 秒租约并每 5 秒续约,进程挂了租约自动过期,调用方用 watch 订阅前缀变更,感知延迟实测在 300ms 以内。
// 注册示例:租约 15s,续约 goroutine 每 5s 刷新一次
lease, _ := cli.Grant(context.Background(), 15)
cli.Put(ctx, "gov/services/orders/inst/"+instID, "10.0.1.11:9001")
go renewLoop(cli, lease.ID) // 续约失败 3 次即判定失联,主动退出
watch 侧把实例列表缓存到内存 map,收到事件就 diff 更新;gRPC 的 resolver 从 map 里取地址,负载均衡切换零延迟。关键参数我们定的是:租约 TTL 15s(不能太短,网络抖动容易误下线;不能太长,故障恢复慢)、续约周期 5s(TTL 的三分之一,留两次失败余量)。
etcd 集群本身也按最小可用配置:3 节点跨可用区部署,磁盘用 SSD,单节点内存不超过 2GB。我们实测 17 个服务、每服务最多 8 个实例的规模下,watch 事件吞吐峰值不到 200 条/秒,etcd 压力很小,真正要注意的是网络分区时多数派投票会卡住,所以跨可用区是硬要求,不是可选项。另外把 etcd 的健康检查也纳入监控,3 个节点任一不可写就告警,避免注册中心静默失效两周才发现。
排障慢的拆解对应到 TraceID:网关入口生成 32 位 hex,随 gRPC metadata 透传,每个服务打出 traceId=xxx spanId=yyy 的日志行,最后在 CLS 里一条 traceId 串出整条链,日志归集的方法我们在 rust-logpipe-guide 里也写过一篇对照。这条线在 4.2、4.4 两篇展开,这里只把它写进需求清单。
熔断的需求拆解同样从数字出发:下游 P99 延迟超过 800ms 持续 10s,或错误率(5s 窗口)超过 50%,就切断流量 30s,半开放行 3 个探测请求。参数不拍脑袋,每个数字都对应验收表里一条注入故障的回放脚本,4.1 篇会用压测数据反推合理性。
踩坑:量化本身也会骗人
第一次做痛点量化时我犯两个错。第一,用峰值 QPS 估算扩容机器数,结果按 P99 算完才发现白天低谷机子全闲着,成本多花一倍;正确做法是压测(5.1)拿到单机天花板再定机器数,压测工具与手法可以参考 go-micro-cps-guide 里的 ab 与自研压测客户端对照。第二,「定位耗时 35 分钟」是平均值,拉出明细后 80% 的故障其实 5 分钟内能定位,真正拖久的是 3 种:跨服务依赖、数据不一致、发布窗口。需求拆解要盯分布不看均值,这直接决定了追踪系统的采样策略(4.3 会讲)。
第三,量化表里最容易掺水的是「最大节点比最小高 47%」这种数字:第一次统计时漏了网关的长连接,重算后实际偏差 61%。凡是要写进验收清单的数字,都必须有原始数据截图存档,不然复盘时没人信。
还有一个组织层面的坑:拆解需求时我们差点把「统一日志格式」也塞进 P0。后来砍掉是因为它不阻止故障,只影响排查效率,归到提效层更合适。需求拆解最难的其实不是写出来,而是砍掉那些「看起来很重要」的条目,保住 P0 只解决会不会放大故障这一件事。
小结
这一篇把模糊的「需要治理」拆成五类可量化的需求,并按保命、基础、提效三层排出优先级。P0 两件:熔断加超时、etcd 注册发现,它们决定故障会不会放大;P1 两件:灰度与全链路,决定发布和排障的速度。拆解方法论就三条:先量化再排层,验收项写成可测行为,砍掉不阻止故障的条目。下一篇我们拿自研框架和现成开源方案做对照,定下技术路线。
延伸阅读
- 服务拆分后的调用链治理思路,见 go-micro-cps-guide。
- 腾讯云网络与部署的产品选择,见 tencent-cloud-practice。
- 从服务调用到链路观测的完整实践,见 rust-logpipe-guide。
