微服务是把一个系统拆成若干能独立部署的小服务——拆的真正目的不是【显得先进】,而是把一次改动的影响面从 100% 缩到几个百分点;而拆开之后必须付的另一半代价,是把原来的函数调用换成网络调用,于是 API(接口约定)从一件好事变成一件必需品。
上一节(4-9)讲了交付的四个数字,其中【变更失败率】和【故障恢复时间】都指向同一件事:一次改动的范围有多大。这一节讲的就是从结构上把这件事做小的办法。
它和容器(4-6)、编排(4-7)是一套的:微服务是【怎么拆】,容器是【怎么装】,编排是【怎么跑】。 三件事缺一不可——拆成一堆服务但没有容器和编排,运维成本会高到没人受得了。
一、先看清它要解决的问题
一个传统的单体系统是这样的:所有功能在一个程序里,一起编译、一起打包、一起部署。
这个做法在小规模时非常好用——改起来简单、调试方便、不用担心服务之间怎么通信。问题出在规模上:
| 规模 | 单体的表现 |
|---|---|
| 几万行、两三个人 | 很好用 |
| 几十万行、十几个人 | 开始难受:改一处要全量测、发布要排期 |
| 上百万行、几十人 | 很难受:一次改动的风险面是全站,发布周期以月计 |
痛苦的来源可以概括成一句话:任何一次改动,都要承担整个系统的风险。
- 改一个报表的排序方式,理论上不会影响支付——但因为它们在同一个包里,所以必须一起回归测试、一起发布、一起承担发布失败的风险
- 发布失败时,回滚要回滚整个包——那些没问题的改动也跟着回去了
二、拆开之后,变化落在哪几处
好处与代价,都能从几个具体数字上直接看出来——一边是收益,一边是必须付的成本。
三、动手 Demo:一次改动影响多大一块
看到什么算成功:
==================================================================================================
【Demo 10】微服务:拆开之后,一次改动影响多大一块
==================================================================================================
场景:一个 120 万行的单体系统,交给 30 人的团队维护。
项目 单体 拆成 30 个微服务
--------------------------------------------------------------------------------------------
一次改动的代码范围 全量 120 万行 约 4 万行(一个服务)
发布影响面 100%(整包替换) 3.3%(1 / 30)
要回归测试的范围 全部功能 一个服务加它的调用方
扩容粒度 整包一起扩 只扩压力大的那个
技术栈 一套定终身 各服务可各选各的
出故障的影响 整站不可用 一个功能受影响
--------------------------------------------------------------------------------------------
但拆开不是白来的,代价落在三处:
① 原来一次函数调用,现在变成一次网络调用(要处理超时、重试)
② 数据不再能靠一个事务保证一致,要额外做补偿
③ 30 个服务 = 30 套部署、日志、监控,运维复杂度上升
把这三条代价兜起来的,就是 API:
· 每个服务只对外暴露一组定义好的接口,内部怎么改外面不管
· 接口用版本号管理,老版本继续跑一段,调用方可以慢慢迁
· 一个接口的响应时间变成可测量的一等公民,谁慢一目了然
判断要不要拆,只问一句:这个模块的改动频率和发布节奏,
是不是明显跟别的模块不一样?是就值得单独成服务,不是就先别拆。
读结果:
- 第一行和第二行是全部价值所在:4 万行对 120 万行、3.3% 对 100%。这不是效率提升,是风险模型换了。 前面 4-9 里【变更失败率从 30% 降到 5%】,很大一部分就来自这里——不是测试做得多好,是改动本身变小了。
- 看那三条代价:三条都不是【优化一下就没了的】,而是结构带来的固有成本。所以拆微服务是一件【用架构复杂度换发布自由度】的交易——只有当发布自由度对你真的值钱时,这笔交易才划算。
四、API 为什么是拆开之后必须付的另一半
拆开之后,服务之间只剩一条路可走:通过网络互相调用。而网络调用和函数调用有三处本质差别:
| 函数调用 | 网络调用 | |
|---|---|---|
| 会不会失败 | 不会(除非崩溃) | 会(超时、丢包、对端挂了) |
| 快慢 | 纳秒级 | 毫秒级,且会波动 |
| 参数传递 | 直接给对象 | 要序列化、要有格式约定 |
所以 API 要承担的不是【让人调用】,而是三件更硬的事:
- 约定格式:我发给你什么、你回给我什么,必须有明确的定义——否则双方各写各的,一改就对不上
- 管理版本:接口一旦对外,就不能随便改。做法是用版本号,老版本继续可用一段时间,让调用方慢慢迁
- 给出观测点:既然是网络调用,响应时间、成功率、错误码就成了可以被测量和告警的东西——这是拆开之后一个额外的好处:性能问题变得可见了
这也是为什么教材把【微服务】和【API】写在同一条里:前面是【怎么拆】,括号里是【拆开靠什么连】。
30 秒实验
打开任意一个网站的开发者页面(比如随便找一个提供开放接口的网站,搜【开放平台 API 文档】),做两件事:
- 看它的接口路径里有没有版本号(形如
/v1/、/v2/)。 - 找一个接口,看它的返回里有没有错误码列表。
做完这个你会看到两件事:版本号说明这个接口已经承诺对外了,改不得;错误码列表说明调用方必须处理这些失败情况。这两样东西在单体系统内部是不需要的——它们的存在本身,就是【拆开】这个决定留下的痕迹。
常见误区
-
错误:微服务就是【把一个项目拆成好几个项目】。——纠正:拆的依据是【变更的边界】,不是【代码量的多少】。按代码量平均切成 30 份,切出来的往往是一堆【必须一起改、一起发】的东西——那就是把单体拆成了分布式单体,复杂度涨了,自由没涨。正确的依据是那个判断句:改动频率和发布节奏明显不一样的部分,才值得单独成服务。
-
错误:微服务一定比单体好,新项目就该直接上微服务。——纠正:在规模还没到的时候,单体更好用。理由很简单:拆分要付出【网络调用、数据一致性、运维复杂度】三笔成本,而这几笔成本是固定的;收益(发布自由)却和规模成正比。所以规模小的时候,收益盖不住成本。多数团队的做法是先单体、到痛点明显时再拆,而不是一开始就拆。
-
错误:拆开之后每个服务可以随便选技术栈,这是微服务最大的好处。——纠正:这句是理论正确、实践上要非常谨慎。多技术栈意味着多套构建、多套依赖、多套排障知识、多套安全补丁——人力成本的上升是按技术栈数量算的。所以现实中绝大多数团队仍然统一技术栈,只有极少数场景(比如某个服务确实需要特殊语言库)才会破例。把【可以随便选】当卖点是常见误解。
-
错误:服务拆得越细越好。——纠正:过细会把网络调用和运维成本推到失控。一个请求要穿过十几个服务,每一跳都有延迟和失败可能——整体可用性是各服务可用率的乘积(每个 99.9% 的十个服务串起来只有约 99%)。同时【一个功能要改三个服务才能上线】也就失去了拆分的本意。判断粒度是否合适的标准还是那一条:能不能让【一次改动】只落在一个服务里。
自测三问(点开对答案)
1. 为什么说【拆开之后运维复杂度上升】这条代价是一定要付的? —— 因为**它和收益同源**。收益来自【每个服务可以独立部署】,而【独立部署】就意味着**每个服务都有自己的构建、发布、日志、监控、告警、容量规划**——30 个服务就是 30 套。**想要独立,就必须各自完整。** 这也解释了为什么微服务和容器、编排是绑在一起出现的:**用人工去维护 30 套流程是不可能的,必须靠自动化把它压回来。** 2. API 的版本号为什么不能省? —— 因为**接口一旦被外部依赖,就成了承诺**。在没有版本号的情况下改接口,等于**要求所有调用方在同一个时刻一起改**——而现实中做不到(有的调用方是别的团队、有的是第三方、有的已经上线半年)。**版本号的作用是允许两个版本同时存在一段时间**,让升级变成一个可以排期的过程,而不是一次强行停机。**这是工程上【不让所有人同时动】的通用办法。** 3. 判断【要不要拆】为什么用【改动频率和发布节奏】而不是【功能模块】? —— 因为**拆分的目的是缩短发布链路,而发布链路的长度是由【必须一起改的东西】决定的**。如果两个模块虽然功能不同,但每次改动都要一起动,那把它们拆开并没有让发布变快——**反而多了一次跨服务的协调**。反过来,如果某个模块改动特别频繁,而它和别的东西耦合很少,那就值得单独拆出去,**让它独立发布、不再拖累别人。** 所以判断依据是变更的耦合度,不是功能的分类。- ☐ 下一节(4-11 网络的虚拟化①:VLAN)
相关笔记
- 开发与运用间的协调 —— 上一篇:一次改动的影响面为什么要变小
- 云原生的真正含义 —— 微服务在云原生四件事里的位置
- 最新技术的运用 —— 技术换代时运维负担怎么变化
- 实现多云计算的容器 —— 拆开之后靠什么跑起来
- ☁️ 云计算 · 索引 —— 全课路线图