文中提及了两个现象:
Almost all the successful micro service stories have started with a monolith that got too big and was broken up
Almost all the cases where I’ve heard of a system that was built as a micro service system from scratch, it has ended up in serious trouble.
其实文章中也大致归结了原因,有两个:
- 首先不确定你的产品是否会被用户喜欢,这个时候其实你需要更快速的将项目完成最小功能,然后投入市场检验,而微服务本身就是有成本的,对于短期内的快速交付没有帮助
- 其次,产品形态不确定的情况下,就没有办法有明确的边界,也就没有办法设计出比较好的服务拓扑,然而微服务拓扑一旦形成,后续重构的成本是超出想象的
最近负责了一个相对复杂的产品的设计,从一开始的设计原则就是:「面向未来服务拆分构建单体应用」,所以在最一开始结合产品功能进行和模块划分以及拓扑设计,按照如下规则:
- 每个模块管理自己的 DB 和 Cache,禁止跨模块访问 DB 和 Cache
- 上层对于下层,或者同层模块之间的调用只能通过模块的 API
- 禁止下层调用上层,通信依赖于队列或者消息
- UI 层面的拼接通过最上层的 Handler 完成
- 鉴权逻辑通过中间件实现
当时考虑的点如下:
- 禁止了跨模块存储访问,避免了 Schema 变更或者存储选型变化引起不可用
- 通过 API 调用,方便后续进行服务的拆分
- UI 层面单独抽离,方便后面进行 UI 和具体功能逻辑的拆分,也能在 UI 层实现 Module 的复用,避免大量填充 Module 的代码出现,同时拼接逻辑放到 最上层,避免请求次数放大
鉴权通过中间件实现,后续拆分成微服务之后,需要进一步前置到 Gateway,减少 Auth 服务的请求次数,因为该服务是整个系统最关键且不可降级的部分,所以需要尽量减少压力
除了上述限制以外,还引入了 Protobuf,吐出数据给客户端以及日志都会采用 pb 进行序列化,好处是方便字段管理,同时 pb 兼容生成 json,顺便也能降低书写成本,不用写一堆 json:"xxxx"
的 struct tag
。
文章随后又说到了从头构建系统的四种套路:
1、仔细设计一个单体应用,留意软件模块划分,包括API边界和数据存储方式。做好这些工作后,向微服务切换就相对简单了。
2、开始时采用单体架构,然后逐步从系统边缘剥离出微服务。
3、首先构建一个单体架构作为“牺牲架构(SacrificialArchitecture)”,然后整个的替换掉。
4、以几个粒度较大的服务开始,待功能边界稳定后再分解成细粒度的服务。
提到的第一种就是我这个项目提到的方法,但是随后 martin 说了一句:
However I’d feel much more comfortable with this approach if I’d heard a decent number of stories where it worked out that way.
嗯,其实就是他是对这种方案没有信心的,注释中提到因为他觉得不是任何一个系统都能拆分成微服务,这种上来就假设拆分成微服务的思路是有问题的。
当然啦,为什么我却用了这种方法呢,其实是因为我明确的知道这个系统可以拆分成微服务,基于经验。
当我们接触到一个自己并不是非常熟悉的系统或者领域的时候,确实不能在假设可以拆分成微服务的前提下,去设计系统,万一未来不能很好的划定边界,提早的拆分其实会给以后的灵活性大打折扣。
综上,新建立一个系统的时候,最好的方式不是从微服务开始,原因如下:
- 无法确定较好的边界划分方式,而一旦仓促决定,后续重构成本远高于单体应用
- 伴随系统建立过程中,需要花费大量工作在协议编写,接口定义上,影响开发进度
- 分布式系统联调、Debug 的难度都高于单体应用,需要配套监控、部署、最终一致性等相关的设施
微服务架构本身是一件奢侈品,本身会带来的在研发,管理的成本就很高,应该把他作为团队逐渐变得非常复杂后,不得不考虑的一种架构方案,而不应该作为 start up 项目的推荐方案。
为什么说是「团队逐渐变复杂」而不是「系统逐渐变复杂」呢?因为如果是系统逐渐变复杂的情况下,无论是单体还是微服务,都需要你对于边界有个相对清晰的划分和理解,此时单纯的引入微服务架构,并不能解决根本的问题。只有当团队逐渐变复杂之后,微服务架构更多是通过服务之间物理上的隔离,提高了团队将整个项目变成大杂烩的门槛。
首发于 Medium:engineer-alex