1.3 团队与发布节奏:微服务的隐性成本

1.2 节定了边界,团队的气氛甚至有点兴奋。真正让大家冷静下来的,是负责人在白板上画的一张成本表。这一节算的是人的账:微服务落地之后,工作方式会怎么变、哪些成本在拆之前根本看不见、以及我们是怎么把生活拉回正轨的。

先说结论:康威定律会兑现

有种说法是系统架构会自动向组织结构对齐,拆成三个服务后五个人怎么分工,这才是最先要回答的问题。我们的分工是用户域 1 人、商品域 2 人、订单域 2 人,网关兼职。听起来挺整齐,但第一个月就出了事:用户域那个同事请了三天婚假,登录系统线上出个紧急 bug,没人能改——不是不愿意,是这三个月只有他一个人碰过那部分代码。

对策是轮值和代码权共享:每个服务至少保证两个人有独立改过的经验,每月轮换一次责任域。代价是每月一周的效率损失,换来的是任何单一服务不会因为一个人掉线而停摆。

制度落地前我们先做了三个月的影子期:每个域 still 由主负责人写代码,但每个 PR 强制由另一域的人 review。三个月后大家都摸过别的域的代码,轮值切换时几乎无感。这个过渡期看似浪费,实际上是后面所有轮值制度能跑起来的地基。

看不见的成本一:联调环境

单体时联调只需要本地起一个进程。拆成三个服务后,本地完整跑起来需要三个进程加 MySQL 加 Redis,新同事入职第一天光搭环境就耗掉半天。我们后来用一份 docker-compose 固定本地依赖:

services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: devpass
    ports: ["3306:3306"]
  redis:
    image: redis:7
    ports: ["6379:6379"]
  etcd:                      # 服务注册用,3.1 节细讲
    image: bitnami/etcd:3.5
    ports: ["2379:2379"]

三个业务服务仍然用 go run 本地起,compose 只管中间件,这样改代码不需要重启容器,调试体验接近单体。这个取舍很重要:让 compose 尽量轻,开发循环才快。

新员工入职我们给一份清单:clone 仓库、make up、make seed(灌入测试数据)、跑 healthz,十五分钟内必须看到三个服务的健康检查通过。这条硬指标的目的是倒逼环境脚本保持可用——脚本一旦年久失修,第一个受害者就是新人,而新人往往不会开口说。

轮值表之外还要排发布日历。三个域的发布窗口错开:用户域周二、商品域周三、订单域周四,每次发布后留 24 小时观察期。大促前一周进入冻结期,任何人要破窗必须拉群 vote。这条秩序听起来死板,却在第一次大促前救了我们一命:商品域一次发布带出一个慢查询,恰好落在冻结期前两小时,回想一下如果赶上大促当天性能曲线,另一个故事我们现在大概不愿意讲。

联调的另一半痛苦是跨服务特性开发。比如下单要同时改订单和库存,两边各开一个分支,本地就得同时跑两个版本的服务。我们的土办法是特性开关:改动隐藏在配置后面,默认关闭,联调时本地打开,互不影响主干分支。上线的顺序也固定下来——先发提供方,再发消费方,两步之间留出观察窗口。

看不见的成本二:发布节奏的错位

单体时一次发布就是一个二进制,所有人天然同步。拆开后,商品域想一天发三次,订单域一周发一次还得拉上财务 review,节奏根本对不齐。我们磨合出一个发布节奏协议:

维度 单体时期 微服务时期 变化
发布单位 全站一个包 按服务独立发 解耦
发布窗口 每周三晚 各域自选,错峰 灵活但需协调
回滚粒度 整站回滚 单服务回滚 精确
跨服务联调 编译期发现 需要契约测试 变重
紧急修复排队 3.2 小时 单域 40 分钟 明显改善
平均缺陷定位 一次堆栈 需查链路 变慢

表格最后两行是关键:微服务确实把发布和回滚变快了,但代价是联调和定位变重。想要保住“定位变快”的收益,必须配套链路追踪和统一日志,这也是为什么后面第五篇要花整整一篇讲 CLS 和云监控——不是选型偏好,是被逼的。

回滚的速度是实打实的红利。单体回滚要整体发版,微服务下单域回滚就是切回上一个镜像,我们的回滚脚本只有十几行:

# 快速回滚:拉镜像历史列表,切回上一版本并健康检查
SERVER=$1
LAST=$(curl -s http://registry.internal/tags/$SERVER | tail -2 | head -1)
ssh deploy@$SERVER "docker pull $LAST && systemctl restart $SERVER"
# 等 30 秒做健康探测,失败就再回退一层

看不见的成本三:on-call 与告警膨胀

拆分第一个月,告警数量直接涨了四倍。不是因为故障变多,是因为每个服务都在各自告警,同一个 MySQL 抖动会触发三个服务的连锁告警。大半夜被电话吵醒,爬起来发现是另一个域的问题,这种事发生两次就足以下决心治理。

补充一个反直觉的观察:告警变多的初期,团队第一反应往往是加更多监控来“覆盖更全”,结果是告警进一步膨胀。正确方向恰恰相反,是先减后增——先消除噪音,再逐条评估新增监控是否值得一条告警通道。每个告警都应当对应一个明确的动作:半夜被叫醒之后,你爬起来能做什么?如果答案是重启服务,那告警应当触发自动重启而不是叫醒人。

我们做了两件事:一是告警归并,同一类告警五分钟内只通知责任域;二是制定 on-call 轮值表,每周一个域的主负责人。告警通道最后收敛到腾讯云告警平台,维度统一之后才真正能睡好觉。这部分在云监控与自定义指标一节展开。

告警治理还有一条经验:先砍再建。我们把历史告警按触发频次排序,凡是每周误报超过三次的规则直接停掉,停的时候定下复核日期。两周后告警量从每天四十多条降到十条以内,而且没有一条真正的故障被漏掉——被漏掉的感觉会在第一周就显现。

一个容易低估的账:机器成本

三个服务各自要至少两台实例做高可用,网关再两台,瞬间就是八台机器的常态开销。拆分初期服务都很瘦,为了控制成本我们先用低配机型顶着,赶上腾讯云 CVM 新用户特惠囤了一批时长,等流量上来再逐台升降配。这一阶段的经验后来沉淀成第五篇的成本治理复盘:闲置实例和过度预留是微服务时代最容易被忽视的浪费。

我们也认真算过机器账的临界点。按当时的业务量,单体两台机能扛住,拆成八个实例后每台平均利用率只有两三成。为什么还愿意付这个钱?因为业务连续性的价值远高于机器费:大促期间任何一个服务单独出问题,影响面被限制在单域,而不是像单体那样全站陪葬。这笔钱本质上是买故障隔离,而不是买性能。

踩坑:联调地狱

最狼狈的一次是订单服务升级接口,按约定商品和用户都同步更新,结果用户域的同学请病假没来得及发,订单先上了。当天下午商品服务的调用方开始 500,排查半小时才定位到是接口契约不一致。教训是:任何跨服务的接口变更,必须在契约层先让消费方测试通过再上线,后来我们所有跨域接口都强制走 protobuf 定义加自动化回归。

那段时间我们整理出一个经验法则:拆分的头三个月,团队的有效产出会下降三成左右,因为联调、契约、环境这些额外开销集中在这个阶段爆发。负责人提前把这段 buffer 给老板打过招呼,所以季度 review 时大家没有被怀疑在摸鱼,这一点很重要——隐性成本不被看见的时候,人会背锅。

小结

微服务的隐性成本集中在三块:联调环境与契约联调、发布节奏与回滚粒度的再协调、告警与 on-call 的膨胀。对应的解法是固定轻量的本地编排、发布节奏协议化、告警归并加轮值。还有一个不能省的动作:把头三个月的产能下降提前同步给决策方,让隐性成本被看见。把这些账算清楚再动手,团队才不会在拆分三个月后开始怀疑人生。

最后给一个可操作的时间线:第一个月只做目录整理与契约定义,第二个月单_service 灰度搬迁(先拆订单域,它与外部交互边界最清晰),第三个月补齐监控与回滚机制。我们实际走完用了三个月零一周,比计划多出的一周花在联调环境脚本上——这恰好印证了前面说的,环境类债务最容易被低估。

延伸阅读