每个工程师都应该了解的:系统拆分。
公司从
1
到N
发展过程中的系统拆分问题。
原因
当一个公司规模很小的时候,基本复杂度相对较小,单一代码库(Monolith
)的效率也高。
随着公司业务的扩展,访问量的增加,其基本复杂度就会逐步升高,达到某一个临界点后,微服务(Microservice
)的效率就远远高于单一代码库。
业务拆分,旨在解决效率和复杂度的问题。
举例
有一个功能模块,大概可以分成四部分。其中模块 A
连接一个外部模块 D
,A
输出的结果,会被模块 B
和 模块 C
分别调用。
针对这样的模块,我们可以做一个集成测试(Integration Test
),在模拟 D
的情况下,测试 A
、B
、C
是不是可以正确运行。
如果修改了模块 A
的返回值,但忘了修改模块 B
和 C
的接口,测试就会立刻失败。一旦通过了集成测试,所有的改动会在一次部署中同时展现(Rollout
)或者回滚(Rollback
),非常容易控制。
随着业务的发展,A
、B
、C
三个功能被拆分成三个独立的服务(Service
),各自保存在不同的代码库,或者是同一个代码库不同的服务容器(Service Container
)里。现在 A
、B
、C
都是独立的服务,可以独立地部署。
注意事项
-
测试会变得异常复杂。
模块被独立出来之后,并没有办法很方便地写出集成测试用例。
- 一个做法是模拟出所有接口的请求和响应,但实际上大部分时候根本没法测试跨服务的改动。
- 另一个方法是在本地配置好所有的服务,用真实的服务响应来测试。但是本地设置多服务很复杂,保证本地服务一直是最新代码也是一件麻烦的事。
可以每个服务都对应设置在线的测试服务,方便写集成测试,或者把服务做成开箱即用,工程师可以一次性地建立所有的本地服务进行联调和测试。但是,对于大部分创业公司来说,很难达到这个水准。
-
与接口相关的改动需要大量协调。
假如有一天,
A
修改了自己的接口,远程调用(RPC
)中请求的一个字段(Field
)从integer
变成string
类型,此时需要为接口做个向后兼容(Backward Compatibility
):- 先改
A
的接口,让它接受integer
也接受string
,如果请求是integer
,先做一下转换,然后发布这个改动; - 修改
B
和C
的接口,响应从integer
变成string
类型,发布这个改动; - 等到
A
、B
、C
的新代码都稳定了,再修改A
的接口,只接受string
类型的参数,发布这个变化,从而完成所有接口的改动。
期间有可能需要通过延长第一步的兼容时间来避免因为
A
的代码问题触发的代码回滚导致线上请求报错的问题。任何改动,都需要所有不同服务的维护者在代码里增加向前或者向后的兼容来对代码进行保护。
同时,代码的上线顺序和修改顺序也息息相关。我们需要做一张检查列表,考虑各种可能性,精确地按照顺序执行。一旦发生代码回滚,可能又要重来一遍。
- 先改
-
报错的处理。
好的程序员在写服务的时候知道要把异常信息封装后层层传播出去,并最终暴露到接口的
4XX
响应里,方便调用方在堆栈信息里看到具体的出错信息。但是,如果有的程序员没有这么做,那么
Debug
将变得很难:因为程序不在一起了,当异常发生的时候,由于得不到完整的异常堆栈信息(Exception Stack
),只能追踪到某个服务的接口处。可能还需要去另一个服务的日志里进一步定位问题。 -
日志的完整性。
系统拆分了,日志系统也会分离。想要真正从日志里获取完整有用的信息,就需要将不同服务的日志一起取出来进行分析和处理。
值得高兴的是,现在有了比较标准的解决方案:一个共享的消息总线(
Message Bus
)。如Kafka
,有了日志就分门别类的扔到消息总线里处理,然后再进行分析。 -
超时设置。
为了保证用户体验,我们常常在系统里做一些超时设置(
Timeout
),比如一个请求从终端设备发过来,我们希望用户最多等待 5 秒,超过 5 秒就会放弃请求并返回相应的结果通知用户。系统拆分之后,我们可以做一个全局的超时设置,让所有的服务都使用这个全局变量。
但是,由于服务是独立开发的,如果某一个服务的实现没有使用 5 秒的全局变量,我们就不知道这个服务到底超时多久才会返回结果,或者是否有超时的设置。
另外,根据某些服务的性质不同,我们希望尽可能地给出最合理的延时设置。还有些请求会经历多次跨服务的调用,一旦同时出现超时,就会进行叠加,超时设置就完全不可控了。
此时,需要增加流程和规范,并且在进行系统拆分的时候进行宏观的设计和考虑。
-
关于代码自由。
拆分之后每个服务的实现都可以自主选择自己的语言,自己的数据存储方式,自己的代码风格。
短期来说,这种做法可以让程序员的效率极大地提高。但是在同一个公司里,当各种各样的服务变成一场技术秀的时候,不论是维护还是稳定性都会受到极大的挑战。
另外,独立服务的开发周期相对较短,往往一两个工程师几周时间就可以写出一个新的服务,这样系统里会出现数不清的服务,有的服务由于人员离职等原因没人维护了,有的服务被重写了,有的服务要退休了,为了管理这些服务,我们还需要一个服务编排和管理系统。
临界点判断
在系统拆分和服务化之前,我们需要综合考虑各种因素来找到平衡点:
- 业务量是否足够大,逻辑是否足够复杂以至于必须进行系统拆分。水平扩展是不是已经不起作用了?代码的相互影响、部署时间过长真的是系统的切肤之痛吗?如果答案事肯定的,此时就应该进行系统拆分。
- 对于服务化的架构,开发人员多少经验,能否正确驾驭?
- 系统拆分是一个“从一到多容易,从多到一困难”的过程,这个过程几乎事不可逆的。
在做拆分计划的时候,一定要慎之又慎。