ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从单体到微服务:后端架构演进的实战笔记

2026/8/24 13:00:02 拓冰建站 浏览量
从单体到微服务:后端架构演进的实战笔记 凌晨两点十七分报警电话把我从梦里拽出来。订单服务超时紧接着支付回调堆积整个系统像多米诺骨牌一样倒下。那一刻单体架构的所有丑陋都暴露无遗一次遍历库存计算导致Full GC所有请求排队杀进程又引发脏数据。我们一边回滚版本一边翻着几百个Controller的代码没人说得清哪条链路是安全的。后来我们花了整整一年拆掉这个庞然大物又花了三年为拆它的决策买单。这不是一个励志故事而是一份沾染着生产事故血泪的实战笔记。单体不是罪罪的是从不思考边界单体架构最可怕的不是代码体积而是“一切皆有可能”的幻觉。任何业务逻辑都能直接修改共享内存任何一个开发都能顺手new一个数据库连接。在用户量几千时这种幻觉是舒适的但当性能瓶颈出现你会发现所有请求都在争抢同一个进程同一个连接池同一份缓存。最常见的处理手段就是加机器可单体的水平扩展只能做到“复制一个完整的炸弹”它的容量上限由最慢的那个模块决定。我们曾在一台8核16G的服务器上跑着用户、订单、支付、营销、消息五个模块。双十一活动一上来营销模块的Redis热点击穿直接把订单数据库拖死。运营说“你们服务器不够”运维说“加机器也没用”开发说“都别吵了我先扩容Redis”。真正的问题在于五个不同生命周期的业务被强行绑定在同一生命周期里。营销一周上线三次订单一个月才发一版但单体发布必须一起走。为了一个字段全链路回归测试为了一个Bug凌晨全部停服。最终让我们下定决心的不是技术而是组织第五个业务团队入驻后代码冲突比会议纪要还频繁。那一刻我们明白服务边界不是技术人员的自嗨而是对抗熵增的必要暴力。拆分的分水岭按业务域切而不是按技术层切很多人一听到微服务第一反应是把Controller、Service、DAO各拆一层。这纯粹是给自己挖坑。按技术层拆分出来的服务本质上仍然是单体只是把进程从Java换成了远程调用。我们见过最蠢的案例把“订单Controller”拆成“订单接口服务”再把“订单Service”拆成“订单逻辑服务”每个都要独立部署、独立运维结果一个查询要经过三次网络跳转延迟从3毫秒变成30毫秒。正确切入点是业务域。借助领域驱动设计的限界上下文来画边界用户有用户的生命周期订单有订单的状态机支付有支付的账务规则库存有库存的余量约束。如果两个业务需要同时修改同一张数据库表它们就是一个不可分割的域。我们当时先拆的是“用户”和“订单”因为它们的调用关系清晰数据表边界明确。而碰都没碰的是“支付”因为它和账务之间有强一致性的需求那个复杂度远超当时的基础设施支撑能力。整个拆分过程要遵循一条铁律每次只拆一个服务并且拆完至少要能独立运行两周修复所有边界问题后再拆下一个。最怕的是同时拆五个结果线上跑着一堆调用不匹配的半成品连回滚都不知道回滚到哪个commit。数据库是最后一道防线也是最痛的一刀代码拆分其实很快把类复制到新工程改改依赖几天就能完成。真正让人崩溃的是数据。单体时代一张订单表里既能查用户信息又能查商品快照还能统计促销效果。一旦拆库这些跨域的join瞬间变成了一场灾难。我们曾经天真地想把订单库和库存库直接拆开结果订单完成时扣减库存的本地事务变成分布式事务一致性问题直接引爆了超卖和超买。后来才学到数据拆分必须与业务拆分同步进行并且要接受“最终一致性”这个现实。我们的做法是先分逻辑再分物理。短期内让两个服务共享同一个数据库但约定只准访问自己的表。再通过数据库层面的双向同步逐步把读流量迁移到新库最后才切断旧依赖。这个过程中最痛苦的是处理“历史数据”。订单表里存了商品名称、价格、图片这些其实应该属于商品服务。但我们没有勇气做全量迁移因为任何一次迁移都是一场赌博。最终我们选择保留“冗余字段”的合法性让订单服务自带商品快照而不是强行join商品服务。这一决定让很多刚入行的架构师嗤之以鼻但它实实在在救了我们的命——查询性能没有因为分布式而退化业务方也不用等待商品服务响应。分布式事务是绕不过的坎但别迷信全局事务拆分后最常遇到的坑就是“一次操作跨多个服务”。用户下单时订单服务创建订单库存服务扣库存优惠券服务标记已使用。单体里一个Transactional就能搞定微服务里却要面对三套数据库三套事务。许多团队第一反应是上Seata AT模式或者尝试两阶段提交。在两阶段提交的锁时长面前任何高并发都是笑话而且它会让微服务的可用性等于所有参与者可用性的乘积。我们试用一周后就把全局事务关掉了。后来我们用了Saga模式把一个大事务拆成一系列本地事务每个本地事务完成后发布事件下一个服务监听事件继续执行。如果某一环失败就执行反向补偿操作。比如订单创建成功但库存扣减失败则触发“取消订单”流程。听起来简单但补偿动作本身也是分布式调用也会失败。Saga的可怕之处在于你以为你写的是业务代码实际上你写的是状态机。每一步都要记录状态每个状态都要能重放补偿逻辑要和主流程一样经过完整测试。更实用的是Outbox模式。在同一数据库事务里把业务操作和“待发送事件”写入同一张outbox表然后另起一个异步进程扫描这张表把事件投递到消息队列。这样既保证了原子性又不用引入分布式事务。我们在订单创建时坚持使用Outbox系统的数据一致性从未因消息丢失而被破坏。但要注意重复投递是常态消费端必须实现幂等。服务治理不是堆组件而是对人性的对抗拆完服务注册中心、配置中心、网关、熔断限流组件一股脑全上这是很多团队的必经之路。我们用过Eureka、Consul、Nacos最后发现这些工具本身也会成为故障源。最讽刺的是我们为了消除单点又引入了新的单点——注册中心一旦雪崩所有服务集体失联。后来我们干脆弱化注册中心的作用让服务间通过DNS或静态配置做fallback。配置中心的坑更隐蔽。把配置放到Nacos之后开发偷懒把业务开关也塞进去每次变更都推一次。有一次运维不小心把生产环境的某个值填成了测试环境的值结果线上支付故障持续了四十分钟就是因为配置推不了重启也无济于事。请记住配置中心是给基础设施用的不是给业务逻辑当垃圾桶的。业务开关应该放数据库或专门的控制台每次变更都要能审计。网关是另一个膨胀点。一开始只是做路由转发后来有人往里面加了鉴权、限流、日志、灰度、熔断最后网关成了新的单体出了问题整个系统瘫痪。我见过最严重的案例网关自身的慢SQL阻塞了线程池所有服务全部超时。后来我们规定网关只做协议转换和路由一切需要业务上下文的逻辑下沉到各服务自己处理。流量治理的真相熔断不是避风港限流不是永远有效微服务最诱人的承诺是“故障隔离”但实际操作中最容易被忽略的是“故障传播”。服务A依赖服务BB的RT从50ms涨到5秒A的线程池很快被打满A的请求排队接着A的健康检查失败负载均衡把A摘掉但摘掉后更多流量打到BB彻底崩溃。没有熔断器这个雪崩只需要90秒。Hystrix退出历史舞台后我们折腾过Resilience4j和Sentinel最终选择的是Sentinel因为它的控制台能实时看到调用树。但熔断本身不是万能的。很多团队把熔断阈值设置成“错误率50%”结果每次熔断都发生在故障已经持续十分钟之后该挂的已经挂了。我们的经验是熔断必须基于延迟的滑动窗口而不是简单的错误次数。另外降级策略要前置设计——当依赖不可用时是返回缓存、返回默认值还是直接失败这些要在业务方评审时确认而不是在故障演练中现编。限流的难点在于“限谁”。如果对所有请求一视同仁普通用户和重试请求互相踩踏。我们的做法是在网关层按来源应用和API路径分别限流同时在服务层次用信号量控制最大并发。限流不是要限制用户而是要限制系统自身对压力的吸收能力。这需要容量预估和压测数据拍脑袋设的阈值只会引发误杀。没有可观测性微服务就是盲人摸象单体时代一条日志就包含了从收到请求到返回响应的全部过程。微服务里一个用户下单要经过四个服务、三次消息队列每次调用都有自己的traceId但没有串联起来的时候你看到的是一个破碎的故事。我们不得不承认微服务让故障定位的难度提升了至少一个数量级。所以拆服务之前必须先把链路追踪和日志链路打通否则生产环境就会成为推理小说现场。我们采用了OpenTelemetry规范统一在框架层埋点把traceId放到请求头和日志上下文里。开发人员打印日志时无需手动携带而是由日志框架自动注入。这样按traceId搜索就能拉出整条调用链。但链路追踪只能告诉你“哪一步慢了”不能告诉你“为什么慢”你还需要结构化日志和metrics做交叉验证。有一次订单超时链路追踪显示支付服务响应了3秒但支付服务的日志里没有异常metrics显示CPU正常最后发现是SDK里一个HttpClient连接池被别的业务耗尽。这个结论是靠四种数据源对照才找到的。监控告警的粒度也要跟着微服务调整。不能只盯机器CPU和内存必须盯上下游依赖的健康度、线程池活跃数、消息队列积压量、缓存命中率。我们建立了一个“故障演练日历”每月故意杀掉一个服务倒逼告警链路和应急手册保持新鲜。这比任何架构评审都有效。部署这件事才是微服务最大的隐性成本拆了三十个服务很多人以为完成了一大半其实没有。每个服务都要求独立的构建、测试、发布、回滚这意味着CI/CD流水线至少三十条每个服务还要维护不同环境的namespace。我们最初用Jenkins做单体发布一条流水线搞定拆服务后Jenkins master动不动就卡死构建排队排到天荒地老。后来切到GitLab CI Argo CD才算松了一口气。微服务的基础设施复杂度不是线性的而是指数级的服务数量每翻一倍发布编排、配置管理、权限管控的复杂度要翻四倍。我们为此专门组建了一个只有三人的平台组负责统一封装部署模板、日志采集、监控仪表盘。这本质上是在用平台化的方式吸收复杂度而不是让每个业务团队自己造轮子。环境管理是最容易被低估的坑。开发环境、测试环境、预发环境、生产环境每个环境都有不同版本的配置和依赖。没有Kubernetes之前我们在虚拟机里靠脚本硬撑经常出现测试环境微服务启动顺序不对导致整个环境不可用。上了K8s后通过Pod亲和性和就绪探针解决了部分问题但K8s本身的运维门槛又成为新的痛点升级集群比升级单体应用还要让人失眠。如果你只有二十人以下的团队上不上容器编排真的要三思。最隐蔽的敌人分布式单体与分布式泥球很多团队拆了一年多最后代码分层和模块划分倒是清晰了但服务之间仍然互相调用、你中有我。订单服务要调用户服务用户服务要调营销服务营销服务又反过来调订单服务查订单总量调用图画出来像一团意大利面。这就是典型的“分布式单体”比单体更难维护比微服务更没有性能优势。它兼具了分布式的最坏部分和单体的最坏部分。为什么会变成这样根源在于团队没有真正理解“服务自治”的含义。一个服务如果不能独立完成一个业务闭环必须依赖其他服务返回数据才能实现自己的功能那么它就不是一个服务而是一个远程函数。自治的边界意味着你为完成自己的业务所需的数据要么由自己存储要么通过领域事件获取而不是同步请求别人实时查询。比如订单服务需要展示用户昵称它应该在用户注册时把昵称冗余下来或者在订单生成时快照一份而不是每次查询都调用用户服务。分布式泥球则是另一个极端为了追求纯净架构强行把内聚的功能拆散。比如用户的账户余额和交易记录本来强一致却因为“高性能”被拆成两个服务最终引入了复杂的对账逻辑还丢过数据。微服务拆分的第一原则是“宁可粗一点不要碎一地”因为合并服务比拆分服务容易得多。我们后来把三个服务合并回一个只用了两周而拆分时花了四个月。组织架构才是真正的架构康威定律说得很直白设计系统的架构者其沟通结构应当与系统架构一致。如果你的团队有四个开发组分工是按“前端、后端、测试、运维”划分的那你绝对拆不出按业务域微服务。团队职责边界不调整你再画一百张架构图也是白画。我们当时为了拆除单体先把开发团队按业务域重组用户组、订单组、商品组。每个组里既有后端也有前端有测试也有运维。然后让他们各自负责自己服务的全生命周期。这个调整引发了巨大的阵痛。原来大家是“互助式”的一起做需求现在变成了“自扫门前雪”的独立交付。产品经理抱怨同一个需求要拆给多个组沟通成本翻了倍。但真正的收益在半年后出现订单组能自主上线、自主优化、自主决策技术选型不再被全公司的最低研发水平拖累。所以微服务不只是一个技术架构更是一套组织鞭策机制。没有魄力触碰组织架构就别谈微服务。该止损时就要止损微服务不是终点是工具讲完这么多套路和血泪最后想说一点反熵的东西并不是所有系统都适合微服务。如果你的业务规模用一个单体加两三个缓存就能扛住如果你的团队不超过十个人如果你的部署频率一周低于一次那么微服务带来的收益几乎为零而它的成本却是实实在在的。我们见过太多人听说微服务好就盲目拆最后连“订单服务”和“用户服务”之间该不该走HTTP都争论不休。“微服务”这个词汇被抬高成了一种信仰好像不拆就是技术落后。但实际上架构的核心永远是权衡而不是站队。你可以从模块化单体起步用清晰的后端模块边界和严格的数据库权限来规避单体的缺点。也可以采用“绞杀者模式”在原有单体旁边新写一个微服务逐步替换而不是一刀切重写。我们最终留下了一个“大而重”的报表服务和三十个轻量业务服务共存这个混合架构很难看但它最符合团队当前的能力和业务的实际节奏。衡量一个架构好坏的标准只有一个当业务需要变化时你能否在十分钟内定位到需要修改的代码并在三十分钟内安全上线。如果你的单体做得到就继续用单体如果做不到再考虑微服务也不迟。我们用一年的血泪拆掉了单体又用了三年填坑那些坑教会我们最大的道理是架构演进不是追逐名词而是对抗现实。