1. 从狂热到理性的必然转身
前两年你要是说自己在做微服务架构,那眼神都带着光;这两年的技术分享会上,越来越多的人开始坦诚地说,其实我们不该一开始就拆那么碎。热度退潮之后,反而能看清微服务真正适合什么场景,不适合什么场景。这篇文章就围绕微服务架构选型这件事,聊聊我自己一路从“拆了再说”到“能不拆就不拆、拆了要有拆的理由”的转变过程,给正在纠结架构方案的团队一点参考。
这个选题适合谁?适合刚准备做架构升级的团队负责人,适合被运维压得喘不过气的后端开发者,也适合那些已经开始微服务化但是总觉得哪里不对的技术管理者。我的结论可能会让一些人大跌眼镜,但我觉得这是踩过坑之后最真实的声音:微服务不该是默认选项,它应该是在特定条件下才值得选择的方案。而且即便决定上了微服务,怎么拆、拆多大、数据怎么处理,每一步都藏着学问。
说完这些,我想先把当年那股“拆服务热”是怎么升温的捋清楚。因为理解了狂热从哪里来,才能真正理解为什么现在大家不谈微服务了,反而更会做微服务了。
2. 为什么人人都想冲进微服务的坑
2.1 那时我们把微服务当成万能解药
回忆一下大概五六年前,微服务的概念在技术社区里迅速升温。当时的信息传播方式和一系列成功的公开案例,把微服务的优势放大了好几倍。很多团队看到的是:独立部署、故障隔离、技术栈灵活、团队自治,这些词每一个都打在痛点上。那时候我们团队也面临一个典型的增长问题:某个系统的代码已经膨胀到几个人的脑子装不下的程度,发布要挑深夜,一个小改动要拉着全局回归测试,线上出问题定位靠翻日志百度和运气。
于是很自然,大家就把目光投向了微服务。负责架构的同事做的方案里画满了方框和箭头,每次评审会都像在描述一种理想化的技术社会,每个小方框各自为政,中间用漂亮的协议连接起来。当时的我虽然有一些疑虑,但说不清楚问题出在哪,只能跟着大流往前走。事后反省,这种心态就是一种典型的“锤子效应”,手里拿着微服务这把锤子,看什么系统都像钉子。
现在回头看,推动那波热潮的另外一股力量是社区讨论的圈层效应。技术群里聊的是服务拆分成多少份、用哪种服务网格、怎么搞灰度发布。你要说我们在用单体应用,瞬间觉得自己落伍了一个时代。这种舆论压力很容易让技术判断变形,不是从业务实际出发,而是从“别人都在做”出发。
2.2 热度过后的冷静信号
从去年到今年,业内风向开始明显变化。不是微服务本身被判了死刑,而是大家开始比较“拆”和“不拆”的账。很多人意识到,微服务不是免费的,它把复杂性转移了,而不是消灭了。原来你只有一个大程序的复杂度,现在你有几十个小的复杂度,外加一套让它们协作的复杂度。
信号之一是很多技术团队开始讨论模块化单体,就是把代码按业务模块分好目录、定好边界、用编译期或进程内隔离的方式来管理复杂度,但先不拆成独立服务。信号之二是像亚马逊那样以极大规模微服务著称的公司,内部其实也在反思服务拆分的粒度到底该多大。连最激进的实践者都在往回找平衡点,这本身就说明问题。
不过我个人特别反感那种“微服务已死”的非黑即白论调。理性的状态应该是:在正确的时间、正确的地点、正确的团队条件下使用微服务,其他地方老老实实待着。接下来就说说,当你真正把微服务放到生产环境里跑一年之后,真实的账单长什么样。
3. 微服务真实成本拆解
3.1 运维复杂度并不是翻倍而是指数级
我认识很多团队,包括我们自己,刚上微服务的前半年都在一种“觉得自己很现代”的兴奋中度过。真正反应过来的时候,是在一次故障处理现场。以前单体时代,一个请求链路是清晰的,从网关到控制器到服务到数据库,最多两三个节点。拆了之后,一次用户操作要穿越六七个服务,每个服务都有独立的日志文件、独立的指标数据、独立的依赖版本。
那一次线上告警,我们排查了快三个小时。先确定是哪个入口服务报错,再顺着链路一个个服务翻日志,中间发现有个服务的日志级别没调好,丢了一部分关键信息,又回头去补日志重新复现。整个过程最让人崩溃的不是故障本身,而是你连故障发生在哪个服务里都无法第一时间确定。单体时代的定位思路完全失效了,你需要一套完整的链路追踪体系、集中的日志平台、统一的告警规则。
说到运维成本,有一个容易被忽略的细节:每个服务都有自己独立的构建、部署、配置管理、依赖升级、安全补丁流程。一百个服务的升级难度不是一百倍,而是一百个需要各自处理的小项目。单纯把配置中心、CI/CD、监控告警这些基础设施搭好,一个五六人的团队至少得投入两个月的全职工作量。这笔账很多人拆服务之前根本没有算过。
3.2 数据一致性是迟早要还的债
业务系统里最难处理的其实是数据。单体的优势在于一个事务可以跨多张表,保证强一致,出问题可以直接回滚。拆成微服务后,数据被按服务边界拆开了,原来一个事务里搞定的事情,现在要跨服务跨数据库去处理。
最常见的订单场景就是典型例子。用户下单,订单服务和库存服务分开部署,库存不能超卖,订单不能丢单,还要保证最终一致。你得引入分布式事务框架、消息队列做补偿或者事件驱动的Saga模式。这些方案的实现难度打过交道的都懂,光是处理消息重复消费和事务回滚补偿就能让一个团队忙活相当长时间。
我们自己实践的教训是:能用一张表解决的数据,就不要拆到两个服务里;能用同一个数据库解决的,就不要引入分布式事务。数据边界必须紧紧跟着业务边界走,但前提是这条业务边界本身是真的需要独立成服务。很多团队在领域划分上偷懒,按数据表来拆服务,结果服务是拆开了,数据耦合还在,事务问题成了甩不掉的包袱。
3.3 团队沟通成本被系统性低估
微服务的一大卖点是小团队自治,但多数中小团队拆了服务之后,人还是那几个人,沟通成本反而高了。以前改一个接口,内部调用改了就行,现在改一个接口,要协调消费方一起发版。接口版本升级,必须预留兼容期,这在快速迭代的项目里是一件很磨人的事。
我见过一个项目因为接口契约沟通不畅,A服务改了自己的返回结构,B服务解析失败,线上出了整整一个下午的错。本来两个人在同一个代码库里可以直接通过代码review发现这种问题,拆开之后,反而要靠文档、靠约定、靠反复确认。团队人数不够、协作成熟度不高的时候,微服务的组织优势根本发挥不出来。反而是单体里那种高效协作模式被打散了。
数据清晰之后,我们来认真回答一个核心问题:到底什么样的系统、什么样的团队才真正适合微服务?我把自己这几年的判断标准整理了一下,不一定都对,但是能帮助团队少走一些弯路。
4. 理性选型:什么时候该拆、什么时候绝不拆
4.1 做架构决策前必须先回答的三个问题
第一问:你的业务复杂度是真的高,还是团队自己觉得高?我见过不少系统,功能看起来挺多,但认真梳理起来,核心业务链路就那么两三条。数据关系也不复杂,用户、订单、商品、支付,没有复杂的审批流,没有跨领域的异构集成。这种系统强行拆成微服务,只会把业务复杂度变成架构复杂度和运维复杂度。
第二问:你的系统需要独立伸缩的部分是什么?微服务的好处之一是不同模块可以分别扩容。有些模块CPU密集,有些模块IO密集,有些模块访问量是另一些的几十倍。如果你确实存在这种明显的资源特征差异,微服务是有价值的。反之,如果所有模块的负载曲线长得很像,拆分之后对资源利用率的帮助非常有限。
第三问:团队有没有建立配套能力的精力和预算?我自己把微服务相关的配套能力列了一个清单:链路追踪系统、集中日志平台、统一配置中心、服务注册发现、网关与鉴权、CI/CD自动化、监控告警、容器编排或云平台。这个清单里的每一项都需要投入人力去搭、去维护。如果团队只有两三个人,连需求都做不完,那真的不建议走这条路。
4.2 一张适合保存的架构选型决策表
我把这些判断标准做成了一张表,每次做技术评审的时候可以直接拿出来对照打分。它的判断逻辑不是定量到精确计算,而是通过几个维度的体检式检查,帮团队把思路理顺。
| 评估维度 | 适合微服务的信号 | 不适合微服务的信号 |
|---|---|---|
| 业务复杂度 | 多个边界清晰的业务域,各自有不同的演化节奏 | 单一业务域,流程线性,改动集中在同一批代码 |
| 团队规模 | 有独立团队长期维护各自服务,团队间协作成熟 | 人数少,角色重叠,靠非正式沟通就能解决需求 |
| 资源伸缩需求 | 不同模块负载差异大,需要独立伸缩与故障隔离 | 全系统负载均衡,整体水平扩展即可满足要求 |
| 数据特征 | 数据天然按域划分,跨域事务少 | 大量跨表事务,数据强一致要求高 |
| 交付频率 | 各模块发布节奏差异大,按各自节奏迭代 | 所有模块一起发布,发布周期统一 |
| 基础设施能力 | 已有或愿意投入建设运维自动化,有独立运维人力 | 只有基础的服务器或云主机,无人专职做平台建设 |
这个表不是想让大家逐项打分求个总分,而是希望把平时模糊的争论变成一个可以逐条对齐的讨论。我们当时做决策的时候,就是把团队成员拉到一起,对着表逐行讨论。很多之前说不清楚的感觉,放到这些维度里之后就比较明确了。
4.3 回头说说我们最终的决定
现在可以回到开头的那个项目了。我们当时经过这轮审视之后做了什么决定?答案是:我们没有把整系统全部拆成微服务,而是选择了一条中间路线。核心业务模块(比如交易链路)保留在模块化单体阶段,只是把代码结构按业务域做清楚,用接口隔离模块之间的调用关系。真正拆出去的,是几个独立性强、资源消耗特殊、发布频率和其他模块差异巨大的部分,比如短信通知服务、文件处理服务、报表生成服务。
这个决定看起来很保守,但实际运行效果让我意识到:架构选型的目标不是追求最先进的方案,而是追求最合适的方案。后来的迭代里,每次要改动核心交易逻辑,我们依然在一个仓库里完成,没有跨服务的协调成本。同时短信服务出问题不会拖垮主流程,文件处理服务的高CPU消耗也不影响其他模块的性能。该灵活的地方灵活了,该稳定紧密的地方保持了单体,这就是我理解的理性。
选择好路线了,接下来是很多团队都会遇到的实操问题:如果确实要落地微服务,应该怎么控制风险、怎么避免走上一条不归路?下面这部分是我从多次实践和几次失败中攒出来的经验。
5. 如果要拆,怎么拆才踩坑最少
5.1 从代码边界而不是数据表开始
我们做服务划分踩过的第一个坑,就是按照数据库表来定服务边界。订单相关的表放一起做一个服务,用户相关的表放一起做一个用户服务,听起来言之成理,但做起来极其痛苦。因为业务操作天然是跨表的,下单要读用户信息、商品信息、库存信息、优惠信息,如果这些分属不同服务,每个简单的操作就变成一次分布式事务编排了。
更好的做法是,服务边界应该跟着业务能力走,先把业务用例列出来,按高内聚低耦合的原则把职责归堆。这里其实可以参考领域驱动设计里的限界上下文概念,不用把整个DDD体系都学完,核心是理解:服务和数据库表不是一对一的关系,一个服务可以包含多张表,也可以和别的服务共享同库里的不同表(当然数据物理分离的问题后面会提)。
举个生活化的类比:以前的老式百货商场,所有商品都摆在一层楼,顾客逛一圈啥都买到。后来有的商品(比如需要冷链的生鲜、耐仓储的家电)单独开了专卖店,既方便管理,也避免影响主商场的客流。这就是按业务特征而不是按商品名归类的思路。
5.2 数据库与事务的边界策略
这里有着很多架构师不愿意直说的坑。一旦服务拆了,数据库是物理隔离还是逻辑隔离,会直接影响后续的演进空间。物理分离是你每个服务独立一套数据库实例;逻辑分离是大家共用一个大数据库,但各自服务只管自己的表。
我们的经验是不建议一上来就物理分离,因为数据库物理隔离之后,跨服务的查询就变成了跨数据库的查询,连表要走接口层聚合,性能开销和实现复杂度一下子上升很多。逻辑分离会好一些:数据库还是同一个,但通过表命名和权限管理确定归属权,哪个服务只能读写自己的表。这种方式在一段时间内能保持一定的数据共享查询能力,业务对账、报表统计还走得通。
不管是哪种方式,跨服务的数据一致性都绕不开。如果你已经决定跨服务的事务必须存在,那么没有必须强制要求强一致的,尽量用事件通知加异步补偿的模式。比如下单扣库存,订单服务创建订单成功后发一个领域事件,库存服务监听事件扣减库存,失败了通过重试或人工介入补偿。禁止在代码里同步调用一堆服务然后期待一个分布式事务把所有都搞定,那种设计在低流量低并发时看不出问题,一旦流量上来,就等着一个节点超时整条链路重试风暴吧。
5.3 服务拆分后的可观测性必须先行
很多团队是在拆了服务之后,才开始想到监控怎么做。这个顺序非常危险。你可以在没有完整可观测性能力的情况下运行一个单体,但绝对没有办法运行好微服务。因为单体出问题,你可以用线程dump、慢查询、访问日志来做排查;微服务出问题,如果没有链路追踪,你连请求到底去哪了都看不到。
所以我的建议是:拆服务之前,先把下面这三样东西建设好。第一,集中日志平台,所有服务的日志统一收集到一个地方,支持按traceId关联查询。第二,链路追踪系统,每个请求从入口生成唯一ID,透传全程,能够展示各服务调用耗时和状态。第三,监控告警体系,每个服务至少要有请求量、错误率、P99延迟这三个指标,告警规则要针对异常而不是等到用户投诉。
这三样东西搭建起来大概需要什么样的工作量?如果你们有云原生成熟产品可以直接用,基础建设时间会大幅缩短。如果是自建,日志平台加链路追踪加监控告警,一个熟练的工程师大概需要两到四周。别嫌这个投入大,如果没有可观测性撑腰,微服务就像蒙着眼睛在高速公路上开车,只能靠后续的故障倒逼你补课。
5.4 演进思维比最终架构更重要
还有一点想单独说一说,就是架构不应该追求一次到位。微服务已经成为一种事实上的演进式设计,言下之意,你完全可以从模块化单体开始,随着业务复杂度真实增长,再把部分模块逐步抽出来变成独立服务。这个过程比一次性拆分要平滑得多,因为每个抽取动作都是针对一个已经被生产流量验证过的边界。
具体落地时,可以先把代码模块的边界做清楚,保持进程内调用;后续如果某个模块的伸缩需求、故障隔离需求、独立发布需求变得强烈,再把它抽取成独立服务。抽取的时候,数据库可以暂时继续共享同一个库,先用服务接口隔离访问,再把数据逐渐抽离。这种渐进式路线给了团队充分的学习时间和调整空间,比起一步到位的大手术要稳太多。我们现在的做法就是随时保留发展成更加彻底的微服务架构的可能,但不会在当前阶段把不需要的能力全部提前支付。
说完方法论与边界策略,我想把常见问题集中整理一下,特别是那些在实际项目里反复出现的问题。这些问题有些是技术实现层面的,有些是组织协作层面的,但它们的共性是:不提前处理,后期一定会爆发。
6. 微服务落地的常见问题与排查思路
6.1 问题一:一个“简单的”查询变得特别慢
拆服务最常见的现象是,本来一条SQL就能把数据查出来,拆完之后要一个服务接一个服务地调用,然后再把数据聚合起来。这种问题的根源往往是划分服务的时候,把原本高内聚的查询逻辑撕碎了。解决方案有两个方向,其一是对查询场景单独做读模型,专门建立一套可以满足聚合查询的数据表或视图,通过最终一致的方式同步数据,这就是CQRS里读模型的思想,也是比较彻底的解法。其二是在不那么追求独立伸缩的服务边界上保持一定的跨库查询能力,这个前面提到过,前提是数据库物理隔离没有做死。
我们实际项目里就用过第一种方式。报表系统本来要跨订单服务、商品服务、用户服务三个服务拉数据,每个请求耗时在几百毫秒。后来我们把这三类数据通过订阅消息同步到了一张专门的报表宽表里,查询一次性走数据库索引,性能回到个位数毫秒。代价是多了一个异步同步链路,但只要同步失败有补偿机制,问题不大。
6.2 问题二:发布总是互相踩脚
服务之间接口不兼容,是很常见的协作难题。A服务改了接口响应结构,B服务没跟上,线上直接报错。这个问题我们在踩过几次之后强制了三条规则。第一,接口版本必须显式管理,URL或Header里带版本号,旧版本至少保留一个发布周期。第二,每个接口的变更必须有契约单记录,改动之前先发变更知会给所有消费方。第三,消费方必须做兼容性测试,服务提供方要提供契约测试用例,自动化跑一遍。
这三条规则看起来是为微服务定制的,但其实模块化单体也能受益。哪怕你不拆服务,只是在代码里用接口做模块隔离,那接口的变更同样存在。很多团队在单体时代写代码很随意,觉得反正内部调用,改了不通知,拆成微服务之后还带着这个习惯,那就只能线上事故相见了。
6.3 问题三:测试环境怎么部署都不对
测试环境的治理也是微服务绕不开的痛点。服务一多,测试环境怎么部署就成了一道复杂的管理题。全部一起部署,资源开销太大,启动时间太长;只部署被测服务,其他服务用什么方式依赖?我们尝试过几种方案,最终比较有效的是按需构建的轻量测试环境。也就是说,一个完整的服务集群作为基座常驻运行,被测的服务单独部署新版本,然后通过网关路由把测试流量打到新版本上,其他请求继续走基座。
这套方案配合容器化之后,测试环境的问题基本解决,同时整个研发团队的迭代效率提高了很多。如果你所在团队的规模已经需要用一套标准办法来治理测试环境,可以往这个方向想,它比本地起全套依赖有效得多。
6.4 团队协作方面最容易被低估的问题
最后想提一个不是技术但比技术更容易出问题的层面:微服务会让组织沟通结构直接映射到系统架构上。如果你组织内部信息传递本身就拐弯抹角,那么微服务那套去中心化的好处就会被内耗抵消。做架构选型时,我建议先把组织的沟通成本估进去。我们后来坚持了“两披萨团队”的调整思路,核心是把服务归属权明确到具体小团队,避免两个部门共用一个服务却互相踢皮球。
如果你们团队还很依赖集中式管理,凡事要层层审批,那微服务带来的自治红利基本吃不到,反而会因为需要多方协作让决策更慢。架构选型真的不只是技术决策,它同时是一场组织设计的调整。这一点,很多文章里很少提到,但实际影响非常大。
我的体会是,微服务本身没有过时,过时的是不加思考、一拥而上的态度。热度过去了反而特别好,因为终于可以安安静静地讨论架构本身,而不是讨论如何在社交平台上站队。架构选型的核心永远是匹配:匹配业务复杂度,匹配团队规模,匹配组织协作方式。希望这篇文章能把我们从狂热到理性的这条路上看到的风景、踩过的坑、得出的经验都表达清楚,对正在做架构决策的你有帮助。