ElIauk

架构演进复盘:单体到微服务

演进不是重写 架构演进最常见的失败方式是“推倒重写”。新系统从零开始,需求在开发过程中还在变,旧系统也不能停,两套代码并行维护半年,最后新系统上线时功能比旧系统少了三成,只能继续双跑,团队被拖垮。 可控的演进一定是增量替换:旧系统继续提供服务,新实现从一条接口、一个模块开始接管流量,随时可以切回去。

评论
架构演进复盘:单体到微服务

演进不是重写

架构演进最常见的失败方式是“推倒重写”。新系统从零开始,需求在开发过程中还在变,旧系统也不能停,两套代码并行维护半年,最后新系统上线时功能比旧系统少了三成,只能继续双跑,团队被拖垮。

可控的演进一定是增量替换:旧系统继续提供服务,新实现从一条接口、一个模块开始接管流量,随时可以切回去。判断标准是“任何一步都能在一分钟内回退”,做不到这一点,就不是演进,是赌博。

绞杀者模式与防腐层

绞杀者模式的做法是在调用方与旧系统之间加一层路由,按路径、用户或比例把流量逐步引到新服务。旧系统的功能一块块被“绞杀”,直到只剩空壳。

这层路由最好放在已有的入口上,不要新建。如果已有网关,就在网关加路由规则;如果只有 Nginx,就先在 Nginx 上做。新增一层专用代理,等于给未来埋一个没人维护的组件。

新服务与旧系统的数据交互,要通过防腐层。旧系统的表结构往往很糟糕(大宽表、状态字段语义模糊),新服务的领域模型不应该照抄。防腐层负责在两边翻译,旧系统改字段时,改动止步于防腐层。

数据拆分的顺序

代码先拆、数据后拆是最稳的顺序。代码拆分只需要改调用关系,数据拆分要面对“同一个事务里改两张表”的历史包袱。

数据拆分的标准路径:

  1. 新建独立表或独立库,结构与新模型对齐;
  2. 双写。新库写失败不阻塞主流程,只记录待补偿;
  3. 存量迁移。按主键区间分批搬,每批可重入;
  4. 校验。总量、分桶、抽样三层比对,差集能定位到主键;
  5. 切读。先按小比例读新库,比对两边结果一致再放大;
  6. 切写。写路径切过去之后,旧表保留一个业务周期再下线。

第 4 步最容易被跳过,也最容易被反噬。没有校验的迁移,问题往往在几周后才以“某用户订单查不到”的形式暴露,补救成本高一个数量级。

用 split_clients 做流量切换

如果入口是 Nginx,split_clients 是最小改动的灰度方案。它按一个稳定的 key 做哈希,把用户固定分到某一侧:

# 以用户 cookie 为主,缺失时退化为 IP,保证同一用户始终落在同一侧
split_clients "${cookie_user_id}${remote_addr}" $order_backend {
    5%      order_service_new;
    *       monolith;
}

upstream order_service_new {
    server 10.0.5.11:8080 max_fails=2 fail_timeout=10s;
}

upstream monolith {
    server 10.0.4.10:8080 max_fails=2 fail_timeout=10s;
}

server {
    listen 80;

    location /api/orders/ {
        proxy_pass http://$order_backend;
        proxy_connect_timeout 300ms;
        proxy_read_timeout 2s;
        proxy_next_upstream error timeout http_502 http_503;
    }
}

比例从 5% 到 50% 再全量,每次观察一个业务周期。切换的前提是新旧两侧的响应结构一致、幂等逻辑一致、错误码一致,否则前端会出现难以复现的兼容问题。

如果链路已经上了 Istio,这一步换成 VirtualService 的权重和 header 匹配,效果相同且不需要改 Nginx 配置。

什么时候不该拆

不是所有单体都该拆。以下几种情况,留在单体里更划算:

  • 团队不到十个人,一个发布就能覆盖全站,拆开只会增加沟通成本;
  • 业务强一致要求高,跨模块操作都在同一个事务里,拆开后补偿逻辑比业务代码还多;
  • 没有可观测性和自动化发布能力,拆成十个服务后,排障时间会从十分钟变成一天;
  • 没有独立的伸缩需求,所有模块的负载曲线基本一致。

判断可以开始拆的信号通常是:某模块的发布频率明显高于其他模块、某模块的流量是其他模块的十倍、或者某模块的故障会拖垮整个应用。

常见坑

按技术分层拆。 拆出“前端服务”“业务服务”“数据服务”,每一层都要跨服务调用,延迟翻倍,边界比单体还模糊。拆分必须按业务能力。

分布式单体。 服务拆开了,但一次下单要同步调用八个服务,任何一个抖动都失败。这种架构的可用性比单体低,因为故障点变多了。

共享库版本地狱。 每个服务都依赖同一个内部 SDK,SDK 一升级就要全量发布。公共代码应该收敛成稳定的基础库,且升级必须向后兼容。

组织没跟上。 一个服务被三个团队同时改,边界形同虚设。服务边界和团队边界要一起划,否则康威定律会以最痛的方式显灵。

只拆不删。 旧逻辑为了“保险”一直留着,双写、双读、开关越积越多,两年后没人能说清哪条路径是主路径。每次切流完成都要有计划地下线旧代码。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者