☁️ 第四章 构建云服务的技术
📄 一句话概括

微服务是把一个系统拆成若干能独立部署的小服务——拆的真正目的不是【显得先进】,而是把一次改动的影响面从 100% 缩到几个百分点;而拆开之后必须付的另一半代价,是把原来的函数调用换成网络调用,于是 API(接口约定)从一件好事变成一件必需品。

📝 本节和前面几节的关系

上一节(4-9)讲了交付的四个数字,其中【变更失败率】和【故障恢复时间】都指向同一件事:一次改动的范围有多大。这一节讲的就是从结构上把这件事做小的办法。

它和容器(4-6)、编排(4-7)是一套的:微服务是【怎么拆】,容器是【怎么装】,编排是【怎么跑】。 三件事缺一不可——拆成一堆服务但没有容器和编排,运维成本会高到没人受得了。

一、先看清它要解决的问题

一个传统的单体系统是这样的:所有功能在一个程序里,一起编译、一起打包、一起部署。

这个做法在小规模时非常好用——改起来简单、调试方便、不用担心服务之间怎么通信。问题出在规模上:

规模 单体的表现
几万行、两三个人 很好用
几十万行、十几个人 开始难受:改一处要全量测、发布要排期
上百万行、几十人 很难受:一次改动的风险面是全站,发布周期以月计

痛苦的来源可以概括成一句话:任何一次改动,都要承担整个系统的风险。

二、拆开之后,变化落在哪几处

好处与代价,都能从几个具体数字上直接看出来——一边是收益,一边是必须付的成本。

三、动手 Demo:一次改动影响多大一块

看到什么算成功:

==================================================================================================
【Demo 10】微服务:拆开之后,一次改动影响多大一块
==================================================================================================
  场景:一个 120 万行的单体系统,交给 30 人的团队维护。

  项目                  单体                      拆成 30 个微服务
  --------------------------------------------------------------------------------------------
  一次改动的代码范围    全量 120 万行             约 4 万行(一个服务)
  发布影响面            100%(整包替换)          3.3%(1 / 30)
  要回归测试的范围      全部功能                  一个服务加它的调用方
  扩容粒度              整包一起扩                只扩压力大的那个
  技术栈                一套定终身                各服务可各选各的
  出故障的影响          整站不可用                一个功能受影响
  --------------------------------------------------------------------------------------------
  但拆开不是白来的,代价落在三处:
    ① 原来一次函数调用,现在变成一次网络调用(要处理超时、重试)
    ② 数据不再能靠一个事务保证一致,要额外做补偿
    ③ 30 个服务 = 30 套部署、日志、监控,运维复杂度上升

  把这三条代价兜起来的,就是 API:
    · 每个服务只对外暴露一组定义好的接口,内部怎么改外面不管
    · 接口用版本号管理,老版本继续跑一段,调用方可以慢慢迁
    · 一个接口的响应时间变成可测量的一等公民,谁慢一目了然

  判断要不要拆,只问一句:这个模块的改动频率和发布节奏,
  是不是明显跟别的模块不一样?是就值得单独成服务,不是就先别拆。

读结果:

四、API 为什么是拆开之后必须付的另一半

拆开之后,服务之间只剩一条路可走:通过网络互相调用。而网络调用和函数调用有三处本质差别:

函数调用 网络调用
会不会失败 不会(除非崩溃) 会(超时、丢包、对端挂了)
快慢 纳秒级 毫秒级,且会波动
参数传递 直接给对象 要序列化、要有格式约定

所以 API 要承担的不是【让人调用】,而是三件更硬的事:

  1. 约定格式:我发给你什么、你回给我什么,必须有明确的定义——否则双方各写各的,一改就对不上
  2. 管理版本:接口一旦对外,就不能随便改。做法是用版本号,老版本继续可用一段时间,让调用方慢慢迁
  3. 给出观测点:既然是网络调用,响应时间、成功率、错误码就成了可以被测量和告警的东西——这是拆开之后一个额外的好处:性能问题变得可见了

这也是为什么教材把【微服务】和【API】写在同一条里:前面是【怎么拆】,括号里是【拆开靠什么连】。

30 秒实验

打开任意一个网站的开发者页面(比如随便找一个提供开放接口的网站,搜【开放平台 API 文档】),做两件事:

  1. 看它的接口路径里有没有版本号(形如 /v1/、/v2/)。
  2. 找一个接口,看它的返回里有没有错误码列表。

做完这个你会看到两件事:版本号说明这个接口已经承诺对外了,改不得;错误码列表说明调用方必须处理这些失败情况。这两样东西在单体系统内部是不需要的——它们的存在本身,就是【拆开】这个决定留下的痕迹。

常见误区

  1. 错误:微服务就是【把一个项目拆成好几个项目】。——纠正:拆的依据是【变更的边界】,不是【代码量的多少】。按代码量平均切成 30 份,切出来的往往是一堆【必须一起改、一起发】的东西——那就是把单体拆成了分布式单体,复杂度涨了,自由没涨。正确的依据是那个判断句:改动频率和发布节奏明显不一样的部分,才值得单独成服务。

  2. 错误:微服务一定比单体好,新项目就该直接上微服务。——纠正:在规模还没到的时候,单体更好用。理由很简单:拆分要付出【网络调用、数据一致性、运维复杂度】三笔成本,而这几笔成本是固定的;收益(发布自由)却和规模成正比。所以规模小的时候,收益盖不住成本。多数团队的做法是先单体、到痛点明显时再拆,而不是一开始就拆。

  3. 错误:拆开之后每个服务可以随便选技术栈,这是微服务最大的好处。——纠正:这句是理论正确、实践上要非常谨慎。多技术栈意味着多套构建、多套依赖、多套排障知识、多套安全补丁——人力成本的上升是按技术栈数量算的。所以现实中绝大多数团队仍然统一技术栈,只有极少数场景(比如某个服务确实需要特殊语言库)才会破例。把【可以随便选】当卖点是常见误解。

  4. 错误:服务拆得越细越好。——纠正:过细会把网络调用和运维成本推到失控。一个请求要穿过十几个服务,每一跳都有延迟和失败可能——整体可用性是各服务可用率的乘积(每个 99.9% 的十个服务串起来只有约 99%)。同时【一个功能要改三个服务才能上线】也就失去了拆分的本意。判断粒度是否合适的标准还是那一条:能不能让【一次改动】只落在一个服务里。

自测三问(点开对答案) 1. 为什么说【拆开之后运维复杂度上升】这条代价是一定要付的? —— 因为**它和收益同源**。收益来自【每个服务可以独立部署】,而【独立部署】就意味着**每个服务都有自己的构建、发布、日志、监控、告警、容量规划**——30 个服务就是 30 套。**想要独立,就必须各自完整。** 这也解释了为什么微服务和容器、编排是绑在一起出现的:**用人工去维护 30 套流程是不可能的,必须靠自动化把它压回来。** 2. API 的版本号为什么不能省? —— 因为**接口一旦被外部依赖,就成了承诺**。在没有版本号的情况下改接口,等于**要求所有调用方在同一个时刻一起改**——而现实中做不到(有的调用方是别的团队、有的是第三方、有的已经上线半年)。**版本号的作用是允许两个版本同时存在一段时间**,让升级变成一个可以排期的过程,而不是一次强行停机。**这是工程上【不让所有人同时动】的通用办法。** 3. 判断【要不要拆】为什么用【改动频率和发布节奏】而不是【功能模块】? —— 因为**拆分的目的是缩短发布链路,而发布链路的长度是由【必须一起改的东西】决定的**。如果两个模块虽然功能不同,但每次改动都要一起动,那把它们拆开并没有让发布变快——**反而多了一次跨服务的协调**。反过来,如果某个模块改动特别频繁,而它和别的东西耦合很少,那就值得单独拆出去,**让它独立发布、不再拖累别人。** 所以判断依据是变更的耦合度,不是功能的分类。

相关笔记