1.2 选型对比:自研框架与现成方案

需求清单定下来后,第二个问题立刻冒出来:治理组件是直接用现成的开源方案,还是自己写一个薄的框架?我们两种都试过,把账算完才做决定。这一篇把对照表完整摊开,包括当时没有算进去、后来才补上的那部分成本。

场景:两条路摆在面前

一条是集成成熟组件:服务注册发现用 etcd 官方 client,熔断降级引 Sentinel 的 Go 版或 Hystrix 思路,负载均衡直接用 gRPC 内置 round_robin,追踪接 OpenTelemetry。另一条是围绕 gRPC 写一个内部框架 govkit:把注册发现、负载均衡、熔断、降级、灰度路由、TraceID 注入收拢到一个包里,服务侧只接一次初始化。

先看纯技术账。

维度 直接用现成组件 自研 govkit 薄框架
引入成本 4 个依赖、4 套配置、50 多条调用链逐个适配 一个 init 函数、一份全局配置
行为一致性 各组件参数口径不一,超时和熔断要自己对齐 熔断、重试、超时共用同一套配置
学习成本 每个组件的坑要单独踩一遍 只需要懂一个框架的坑
长期维护 跟随上游版本升级 完全可控,但升级自己负责
故障排查 问题可能出在四个组件的任意交叉点 边界清晰,一个框架内闭环

两张表都要读全:左边表说服我们自研,右边「长期维护」一行提醒我们自研是终身责任制。评审时把每一行都问一遍「谁来负责」,答不出人名的行直接删掉,表里只剩有人背的东西才敢合入。

自研还有一个当时没写进表、事后才意识到的代价:组织绑定。框架代码只有 2 个人读得懂,其中一人休假一周时框架冻结,排队的 6 个需求全停。之后我们把框架代码拆成 4 个模块交给 4 人轮值,单模块改动不超过 200 行,把「框架 = 两个专家」改成「框架 = 一个团队」。技术选型的账永远要加上人这一项。

实测数字:直接集成方案在测试环境跑了两周,跨组件的参数冲突出了 11 次,典型的是 gRPC 超时 800ms 但熔断器统计窗口 5s,慢请求被反复计入窗口导致误熔断。这类参数口径问题在 go-micro-cps-guide 里也有记录,我们自研方案把超时、熔断、重试统一挂在同一个 interceptor 链上,冲突归零。

方案:自研薄壳,组件不重写

关键决策是「自研」的边界:我们只自研胶水层,不重写底层组件。etcd 的 client、gRPC 的 dial 选项、CLS 的 SDK 全部用官方的,govkit 只做四件事:

层 做什么 不做什么
注册发现 封装 etcd lease 注册与 watch,暴露本地实例表 不自己实现 KV 存储
负载均衡 在 gRPC resolver 里做加权轮询与灰度路由 不写内核级调度
熔断降级 统一状态机(关闭、打开、半开),共享配置 不实现多种算法全家桶
观测 TraceID 注入、指标埋点、日志钩子 不自建存储与检索

胶水层的代码量最终控制在 4000 行以内,这是刻意的:超过 5000 行自研代码,团队维护成本就开始超过收益。判断标准是「删掉胶水层后官方组件能不能单独工作」,能,说明我们只是缝合,没有造轮子。

这个边界在项目启动时就被质疑过:「为什么不直接全自研?」答案是一句话:全自研意味着自己实现一个 KV 注册中心、一个熔断算法库、一个追踪后端,三个团队的工作量,而我们真正的差异化需求只有一个——把 17 个服务的调用行为对齐到同一套配置。差多少做多少,是薄壳的精髓。

配置与代码:框架初始化长什么样

服务侧接入 govkit 只需要一段初始化,这也是选型时最看重的指标之一。

gov, _ := govkit.Init(govkit.Config{
    Name:      "orders",
    Etcd:      []string{"10.0.1.2:2379", "10.0.1.3:2379", "10.0.1.4:2379"},
    Timeout:   800 * time.Millisecond,
    Breaker:   govkit.BreakerPolicy{ErrRatio: 0.5, Window: 5 * time.Second},
    GrayRatio: 0, // 灰度比例由控制器下发,本地默认 0
})
defer gov.Close()
server := gov.NewServer(":9001")

配置全部集中在这一个结构体里,熔断窗口、超时、灰度比例都从这里出,运维改一处全链路生效。现成组件方案里同样的事情要改三个配置文件加两个注解,跨组件口径对齐靠人肉,这是 11 次参数冲突的根源。

init 失败时的行为也是当初争论过的设计点:etcd 连不上时 govkit 选择本地缓存兜底加阻塞重试,而不是直接退出。理由是从 2.3 篇的注册机制看,etcd 短暂失联是常态(网络抖动、选主),进程退了流量才真的丢;缓存在,旧实例表还能用 30 秒。代价是故障期间新实例要等恢复后补注册,这个取舍写进了框架 README 的第一条注意事项。

interceptor 的执行顺序也是选型时争论最久的一点,最终定死为:观测(TraceID 与指标)→ 限流 → 熔断 → 超时 → 业务。原则是观测必须最先执行,否则被熔断拒绝的请求没有埋点,监控曲线会出现「无数据」的空洞,告警反而失灵。这个顺序写进了框架单测,谁想调整顺序必须先改单测,把隐式约定变成显式代码。

灰度比例走 etcd 下发的逻辑:控制器写 gov/gray/orders 节点为 0.2,govkit 的 watch 收到后把 20% 流量路由到 v=2 标签的实例。这个设计把「发布」和「流量」解耦,4.1 篇展开。

踩坑:算漏的账

选型评审时我们都只算了显性成本,后来补了三笔被漏掉的账。

漏算项 当时假设 实际发生
团队切换成本 会 gRPC 就会 govkit 前 3 周 2 人专职答疑,约 6 人日
演示与文档 代码即文档 补了 4 份接入文档加 1 份故障手册
与 CI 的耦合 框架升级不碰流水线 一次依赖升级卡了 3 天流水线

但同样有三笔账是现成方案会漏的:跨组件故障的根因分析时间(每次 P2 平均多 20 分钟)、新人上手时 4 套组件的认知负担、以及「上游升级破坏兼容」时的被动等待。把 6 笔账全摆上桌面后,结论没有变:调用链 50 条以上的团队,薄胶水层的收益能盖过维护成本;调用链 10 条以内的小服务组,直接用现成组件更划算,别学我们自研。

还有一笔账要单独拎出来:上游风险。选现成组件时我们默认了「跟上游升级即可」,但实际两次上游破坏性升级都在业务高峰期撞上,一次是 etcd client 的 watch 语义变更导致全量重连,一次是熔断库默认参数收紧导致误熔断。自研胶水层把这层风险挡在了框架内,升级窗口可以挑凌晨。这类风险很难写进评审表,只能在出事之后补,评审时记得留一行「上游升级的响应时间预算」。

另一个隐性收益当初没人提:govkit 把 etcd、CLS、Redis 都挡在框架后面,后来把 Redis 从自建切到云数据库 Redis,改的只是框架里一个连接串,17 个服务零改动。

复盘:半年后的账本

框架上线半年,把账本重新拉了一遍,验证当初的决策有没有偏。

指标 选型前预估 半年实际
govkit 代码行 ≤ 5000 4310
跨组件参数冲突 归零 归零
框架引发的故障 未评估 2 起(watch 重连风暴、灰度参数误写)
新服务接入耗时 未知 首次 2 天,之后 2 小时
上游升级被动等待 现成方案 4 次 自研 0 次

2 起框架故障都在半开探测与灰度参数上,正是胶水层最难写对的两段,这也反过来验证了 1.1 篇「需求驱动」的做法:先验收后实现,故障模式在需求阶段就被枚举过。半年净结论:薄壳方案在 17 个服务这个规模上跑通,再往上堆到 40 个服务时,建议把 etcd 换成云托管的注册中心,把框架再削薄一层。

选型文档最后沉淀成了一份评审模板,团队后来所有自研决策都套这个表:第一列写组件边界(做什么不做什么),第二列写漏算账(显性成本之外的人力、文档、CI 耦合),第三列写上游风险预算,第四列写半年复盘指标。模板本身不产生结论,但它保证每个人算的是同一套账,比争论「自研好还是开源好」有用得多。

小结

选型结论:自研薄壳 govkit 管胶水,组件全部用官方实现,代码量封顶 5000 行。决策依据不是「自研一定强」,而是 50 条调用链下跨组件口径冲突的成本被反复验证过高。对照表里最该抄走的是「删掉胶水层后组件能否单独工作」这条判断线,它决定了你写的是缝合还是轮子。规模到了 40 个服务再考虑托管化注册中心,别提前过度设计。下一篇 1.3 讲机器准备,把 CVM 规格与服务拓扑一次定死。

延伸阅读