架构实战营 1 期模块 6 作业——微服务架构
作业
作业内容
拆分电商系统为微服务。
背景:假设你现在是一个创业公司的 CTO,开发团队大约 30 人左右,包括 5 个前端和 25 个后端,后端开发人员 全部都是 Java,现在你们准备从 0 开始做一个小程序电商业务,请你设计微服务拆分的架构以及微服务 基础设施选型。
要求:
需要明确服务拆分思路,并且将拆分后的系统架构图画出来;
需要明确微服务基础设施选型思路,并选择一个微服务框架;
用 1~2 页 PPT 即可。
解答
拆分方式——按业务拆分
因为是从 0 开始做,所以选择按照业务拆分。
划分业务边界
不论有没有业务专家,电商系统是一个有着比较明确模式的系统,所以直接参考业界实现如下
服务拆分
按照三个火枪手原则,平均三个开发人员负责一个微服务。现在有 25 个后端,那么最多可以有 25/3 = 8 个微服务。
那 5 个前端怎么划分?因为微服务主要位于端到端分层服务的业务层,与用户层和展示层的关系不大,所以此处不考虑前端开发的人数。
目前划分了十个业务域,需要合并其中的两个,因为总体上是按照业务拆分,所以按照业务的相似度将同属一个大业务域的子域进行合并:
将商品和购物车合并到一个服务中;
将订单和支付合并到一个服务中,因为订单和支付通常情况下需要位于一个事务中,这样可以减少系统的复杂度。同时由于合并后内部复杂度较高,指派四个开发负责此服务。
最后的服务划分及系统架构如下
落地方式
因为是从 0 开始做,所以选择一步到位进行实现。
微服务基础设施选型
后端开发人员全部使用 Java 语言;
由于是创业公司,规模不大,且为了避免不必要的维护成本,选择 Spring Cloud 或者 Dubbo;
Spring Cloud 的适用面更广,组件更全面,所以选择 Spring Cloud 作为微服务基础设施。
笔记部分
微服务架构详解
微服务与 SOA
SOA 目的是将企业内部已有的异构 IT 系统连接起来,减少重复建设。手段是通过 ESB 将各个系统松耦合的联结到一起。结果导致 ESB 逻辑臃肿、性能低下、扩展困难。Smart pipes and dumb endpoints。
对于微服务,用 Martine Fowler 的话说,其目的是将一个大的系统拆分成一个个较小的服务,服务之间通过轻量级机制通讯,且强调服务可以自动化部署。Smart endpoints and dumb pipes。
对于二者的联系,有如下几种观点,但最后一种才是准确的,即它们之间有非常多的不同,只是在面向服务这一点上是相同的。
按照秦金卫老师的讲解,SOA 与微服务的整体架构是不同的
SOA/ESB:代理调用,直接增强
分布式服务化:直连调用,侧边增强
随着微服务的发展,微服务基础设施变强大也变复杂了。
单体架构、SOA、微服务架构是服务端架构演进的几个不同阶段。
微服务与其它可扩展架构的关系
华仔和《DDD 实战课》一样,都是将整洁架构作为实现微服务的一种架构形式,
但《DDD 实战课》的角度略有不同,将 DDD 四层架构也作为实现微服务的一种具体形式,而这里的分层架构是更普遍意义上的。
与分层架构的关系
分层架构是端到端的架构或者单个系统的内部架构,按照某种规则划分为不同的层级。J2EE 也是分层架构的一种。
微服务是端到端分层架构中的业务层的架构:
在《DDD 实战课》中,直接给出了 DDD 的分层架构
并将单体后端服务的三层架构与上面的四层架构做对比
与整洁架构的关系
单个微服务的架构可以是整洁架构。
与微内核架构的关系
单个微服务的架构可以是微内核架构,例如风控、营销、工作流这几类微服务。
《DDD 实战课》还给出了一个微服务可用的架构模型——六边形架构(又称“端口适配器架构”)
思考题
微服务相比 SOA 架构,其代价是什么,或者说其缺点是什么?
我觉得微服务的代价就是系统节点之间的关系变得非常复杂,一个业务可能会经过非常多的服务节点,节点之间形成了一个网状连接。而 SOA 不同,通过引入 ESB 这个中间节点,将网状连接变为了“中央辐射”型的链接。
这就导致了架构上连接增多,组织上,沟通成本变多。
微服务架构陷阱与挑战
六大陷阱
个人觉得这些陷阱都是由于微服务整体是个网状连接导致的系统复杂度增高导致的。
粒度太细
1、服务关系复杂
降低了单个小服务的内部复杂度,但是增加了系统之间连接的外部复杂度。需求分析、方案设计、测试、部署......难度都会增加。
2、团队效率下降
需求分析、方案设计、测试、部署......工作量都会增加。这是带给组织上的冲击。
3、问题定位困难
节点之间的连接度太高,单个节点的故障会让很多节点产生问题。
4、系统性能下降
调用链越长,单次请求耗时会更长。拆分后单个服务的处理性能会提升,不可以弥补调用链带来的消耗。因为决定耗时的主要因素是存储系统的调用,拆分成单个服务后,单个服务的提升是有限的;而且调用链上消耗的性能会超过单个服务的性能提升。
基础设施缺乏
5、无法快速交付
没有自动化测试支撑,每次测试时需要测试大量接口;
没有自动化部署支撑,人肉运维,手都敲麻了;
没有自动化监控,每次故障定位都需要人工查几十台机器几百个微服务的各种状态和各种日志文件。
6、服务管理混乱
需要服务路由
需要故障隔离
需要服务注册和发现
针对上述陷阱,需要
合适的粒度
完善的基础服务
微服务架构四大挑战
数据分布带来的挑战
1、数据分布在不同的服务节点上,会产生分布式事物
微服务中聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化;
如果把实体和值对象理解成库表或库表的一个对象,那聚合就是数据库或者 Schema 了;
如果把服务理解为规则加数据,那因为聚合有对应的仓储,所以聚合理论上就对应一个服务。
微服务与聚合并不是一对一的形式,而是一对多的关系。聚合可以根据需要在微服务之间做迁移,比如按照业务的需要迁移,也可以按照质量属性做迁移(把访问量很高的聚合迁移到独立的微服务中),还可以按照变更频繁的程度做分离(把变更频繁的和变更不频繁的拆分到不同的服务中去)。
聚合内数据强一致性,而聚合之间数据最终一致性;
一次事务中,最多只能更改一个聚合的状态。
2、需要保证全局幂等。因为分布式的调用,会有失败重试的情况,所以需要保证幂等。
服务分布带来的挑战
3、接口之间需要兼容
4、要解决接口循环调用带来的问题
应对四大挑战
BASE、CAP 以及可线性化
上面说到,聚合内数去强一致性,聚合之间数据采用最终一致性模型。关于最终一致性有一个 BASE 理论。
BASE:Basically Available(基本可用)、Soft State(软状态)和 Eventually Consistent(最终一致性)。其中的最终一致性是由于分布式系统的 CAP 理论描述的。
CAP 由三个性质组成:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。其中,分区容错性指的是网络出现了问题,把原本通过网络连接在一起的机器分成了几个独立的部分,也叫作脑裂。
如果放弃分区容错性,那么就是单机数据库,分布式系统不可能放弃 P,所有在会发生分区的前提下,需要在一致性和可用性中二选一,而不是三个性质排列组合在 AP、CP 和 CA 中三选一(CA 就相当于单机数据库)。
此外,CAP 中的 C 即一致性指的是可线性化(Linearizability),分布式环境不但有一致和不一致,中间还有其他选择,比如是客观一致的,还是看起来是一致的
状态一致性是指,数据所处的客观、实际状态所体现的一致性;
操作一致性是指,外部用户通过协议约定的操作,能够读取到的数据一致性。比如各种会话一致性,即从客户端看起来是一致的。
线性一致性/Linearizability
来自《分布式金融架构课》。
线性一致性的英文名是 Linearizability。线性一致性是分布式系统里最重要的一致性。可以理解为线性一致性是分布式环境下的可串行化(Serializability)。
可串行化
数据库中多个事务是可以并发执行的,可串行化规定了这些同时在运行的事务的结果,它要求这些并发执行的事务的最终结果永远等同于它们某个顺序执行的结果。
如上,将事务 T1 和 T2 的执行顺序调整后,这两个事务在时间上没有任何的重合。这时候这两个事务就是顺序执行。
同时调整了事务的执行顺序之后,最后的读写结果和调整前完全一样。
最后,这个调整结果并不是唯一的。
冲突可串行化(Conflict Serializability)
“冲突”就是读写、写读和写写这 3 种冲突。冲突可串行化依然要求等价于某个事务串行化的结果。但是它和可串行化不一样,可串行化只需要你找到一个等价的串行结果就行,而冲突可串行化要求你通过一系列无冲突的互换过程将原来的执行序列变为等价的串行执行。
如果两个操作之间没有冲突,可以互换他们的顺序,也叫无冲突互换过程,无冲突有两种情况
两个操作的对象不一样
两个操作都是读操作
一个例子如下:
数据库的实现过程中,一般会通过锁的方式调整事务执行顺序,而用锁的方式一般能实现冲突可串行化。通过锁来实现可序列化的方式叫作 2PL(Two Phase Lock)。
2PL(Two Phase Lock)
2PL 的过程很简单,它要求对于任何一个事务,这个事务会先对所有访问的资源加锁,然后再访问所有资源,最后再释放所有的锁,加锁和解锁的过程不能有交替。
2PL 过程中锁数量的变化过程(下面是最一般的过程,还有其余的一些变体,比如见下面,MySQL 使用的就是一种变体),表达的意思是不断的按需获取锁,直到持有所有的锁后才可以进行操作。
用 2PL 实现冲突可串行化
核心原理就是先加锁的事务拥有锁,后面的事务在没有持有锁的情况下,是不能对同一个资源做写操作的,即这两个事务之间没有冲突,那自然是可以互换执行顺序,从而实现冲突可串行化。
因为持有的锁可能不止一把,更具体的说法是:在第一个事务释放第一个锁之前,它和其他所有的事务的所有操作都没有冲突,因此可以通过无冲突操作互换的过程,将第一个事务的所有操作提前到其他事务之前。这样,我们就可以把第一个事务和剩下的事务独立开来。
2PL 要求我们提前知道所有访问的数据都有哪些,这样才能在解锁前锁住所有内容。我们一旦释放了任何一个锁,就不能再新增其他锁住的资源,这就是 2PL 的局限性。
但是 MySQL 似乎对必须提前知道这件事做了改进,是按需加锁的。
MySQL 中的两阶段锁(内容汇总自《MySQL 实战 45 讲》、《分布式数据库 30 讲》)
比如下面的例子
上图中,事务 B 的 update 语句会被阻塞,因为在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。也是两阶段锁协议,还是一个变体——严格两阶段封锁协议(Strict 2PL,S2PL,即事务一直持有已经获得的所有写锁,直到事务终止),如下
所以,如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。比如买电影票的业务场景:
1. 从顾客 A 账户余额中扣除电影票价;
2. 给影院 B 的账户余额增加这张电影票价;
3. 记录一条交易日志。
试想如果同时有另外一个顾客 C 要在影院 B 买票,那么这两个事务冲突的部分就是语句 2 了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。
根据两阶段锁协议,不论你怎样安排语句顺序,所有的操作需要的行锁都是在事务提交的时候才释放的。所以,如果你把语句 2 安排在最后,比如按照 3、1、2 这样的顺序,那么影院账户余额这一行的锁时间就最少。这就最大程度地减少了事务之间的锁等待,提升了并发度。
线性一致性
可串行化是单机程序调整多个事务的执行顺讯,使得找到一种合理的顺序,使得并发事务顺序执行的结果和并发指定的结果一致。
而线性一致性是多个程序的环境下,调整不同程序操作的开始和结束时间,使得这些操作之间没有任何时间上的重叠。
线性一致性对时间的调整也有一个要求,那就是如果两个操作之间没有时间上的重叠,那么这两个操作之间的时间先后顺序不能发生改变。同时,还要求对调整以后结果进行正确性验证
严格可串行化
单机情况下最强的一致性是可串行化, 分布式情况下最重要的一致性是可线性化。那么把这两者结合起来,就得到了分布式情况下最强的一致性,叫作严格可串行化(Strict Serializability)。
可串行化表示两个事务里所有操作的执行结果等价于这两个事务的某一个顺序执行结果。这里对“某一个”并没有做任何限定。
而严格可串行化则对这个“某一个”做出了规定,它要求两个事务的运行结果等价于唯一一个顺序执行结果。在这个结果里,原来谁的事务先结束,那么在顺序执行的情况下谁的所有操作先结束。
严格可串行化虽然有着极强的正确性保障,但是它的运行效率特别低,所以一般很少用到。
级分布式事务 - 本地事务消息
业务级分布式事务 - 消息队列事务消息
业务级分布式事务 - TCC
全局幂等
全局范围内的幂等,保证每个幂等操作都是全局唯一的。
设计关键
全局唯一 ID
状态机
接口兼容
某个微服务的某些接口升级,依赖这些接口的微服务不一定能够全部同时升级。
解决方法:
接口多版本,直接拷贝一份旧接口代码,在旧接口代码上修改,接口 URL 加上 v1/v2 这种标识;
这个和组织架构也有关系吧,现在所在的公司,核心接口升级是,强制要求依赖系统全部升级。
华仔特别强调了,此时拷贝代码并不为被 DRY 原则。因为这样:
不影响现有功能;
旧接口下线时更方便,可以避免不必要的测试。
接口逻辑兼容,同一份接口代码,兼容新旧逻辑,容易互相影响,且旧接口下线时又要修改代码(不推荐)。
接口循环调用
比如某次业务处理过程中,A 调用 B,B 又过来调用 A,A 的处理又进入了之前的处理逻辑,导致循环调用,整个业务进入死循环。
解决方法:几乎没有好的解决方案,运气好靠测试发现,运气不好靠上线发现。
思考题
回忆一下你在工作中遇到过课程中涉及的哪些陷阱和技术挑战,思考一下主要的原因是什么?
太不行了,公司的微服务化才刚刚开始……
微服务基础设施选型
微服务基础设施及其优先级
共有二十个模块,分类图如下。绿框中的是微服务框架核心。
微服务框架(核心)模式
嵌入 SDK 式
比如 Dubbo 和 Spring Cloud。
反向代理式
比如 APISIX。
网络代理式(Service Mesh)
比如 Istio。
可以看出服务网格与 Spring Cloud 或者 Dubbo 的共同点也就是在服务注册、服务发现和服务路由上重叠比较大了。所以他们才会被放在一起比较。
每个模式就不单独记录了,直接记录对比图
如何选择开源微服务框架
遇事不决 Spring,选择太多 Apache!
思考题
既然微服务的基础设施比 ESB 还要复杂了,微服务还有什么存在的必要?
因为微服务已经发展起来,互联网技术也已经渗透到传统行业,所以不可能再走回头路,重新搞 SOA 那一套了。
微服务拆分技巧
技巧相关三要素
拆分方式
基础设施
落地方式
实施建议
上表可以总结出微服务拆分也是遵循架构三原则的——合适原则、演化原则、简单原则,同时也是先解决架构的复杂度,再提升架构的质量:
即微服务拆分也是先要追求正确,再追求优化
如果是新建微服务(包括从 0 开始构建业务系统和单体微服务化改造),都应该按照业务拆分,因为服务本身是一个业务概念,是根据 DDD 的指导下进行的,而 DDD 正是从业务入手。(合适原则)
如果是优化微服务,可以按照质量进行拆分(演化原则)
基础设施是必须的
新建微服务时要新搭建基础设施,但基础设施也是有优先级的,要按照优先级逐步落地(简单原则,演化原则)
优化微服务时,重用已有的基础设施(合适原则)
服务落地方式有两种(合适原则、演化原则)
一步到位
新建时采用一步到位的方式。
逐步落地
特别是单体架构改造时,先从非核心业务模块开始
拆分方式详解
按业务拆分微服务
DDD 的难产
理论上应该使用 DDD 方法论进行指导。
但是 DDD 难以落地,因为:
界限上下文的划分的确定,业务专家的影响举足轻重。
界限上下文其实也是“团队的边界”
界限上下文还收到团队情况(比如团队大小)的影响
总结起来,就是受组织形态的影响更大。
实际业务边界划分过程
实际上的拆分过程不是 DDD 化的,而是如下:
注意,一定是先粗分然后演进,而不是先细分然后合并。因为后者:
首先工作量会很大:为了保证微服务的业务正确、高效的运行,需要大量的基础设施。
其次合并本身就比逐步拆分的难度要高。
实际业务域与服务的对应关系——服务拆分
DDD 的概念中,有如下几个从下往上、从小到大的概念:
实体(充血模型的实体类,一个实体可能对应 0、1 或多个数据库持久化对象,实现个体业务的能力)、值对象(对应贫血对象的类)
聚合。实体和值对象组成了聚合。跨多个实体的业务逻辑通过领域服务来实现,领域服务坐在聚合上。
聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化:如果把实体和值对象理解成库表或库表的一个对象,那聚合就是数据库或者 Schema 了。
如果把服务理解为规则加数据,那因为聚合有对应的仓储,所以聚合理论上就对应一个服务,**但是微服务与聚合并不是一对一的形式,而是存在多种关系,即一个微服务内可以包括多个聚合。**所以,业务域与服务的对应关系有如下几种,它们实际描述的是微服务拆分的粒度,而粒度的大小要遵循合适原则——要适合于团队的现状,参见“三个火枪手案例”。
一对一:每个业务域对应一个微服务
多对一:多个业务域由一个微服务处理
一对多:业务流程的拆分, 列出核心业务功能的处理流程,将流程中的处理步骤作为拆分的维度。
此外,聚合也是有上下文边界的,而领域模型利用限界上下文向上可以指导微服务设计,通过聚合向下可以指导聚合根、实体和值对象的设计。
最后,聚合内数据强一致性,而聚合之间数据最终一致性。
上面的描述可以用华仔的图来总结:
三个火枪手原则
定义:平均 3 个开发人员负责一个微服务。
为什么不是一个人?
因为人员没有备份,且一个人思维有局限。
为什么不是 2 个?
2 个人负责维护一个微服务,微服务复杂度偏低。不利于个人职业成长!
为什么不是 4 个或者 5 个?
如果 4 个或 4 个以上,每个人不一定能掌握单个服务所有细节。
微服务数量=服务端开发人数/3;
处于维护期的服务,维护人员可以为 2 人。
三个火枪手案例
按质量拆分微服务
即根据需要将聚合在微服务之间迁移、合并。
按性能拆分
将流量最大的业务以及强关联的业务拆分出来,以降低业务互相影响程度,拆分后优化流量大的业务,提升性能降低成本。
按业务重要程度拆分
将重要程度高(重要程度高不一定是流量大的)的业务拆分出来,以降低业务互相影响程度,拆分后提升重要程度高的业务的可用性。
按可用性拆分
将经常出问题的业务拆分出来,以降低业务互相影响程度,拆分后有针对性的提升问题多的业务。
按稳定性拆分
将稳定的业务拆分出来,降低业务互相影响程度,拆分后有利于不断变化的业务快速迭代。
思考题
按照质量属性拆分微服务的时候,是否需要遵循三个火枪手原则?
任然需要遵守。因为此时拆分的对象仍然是各种业务服务,而业务服务的构建就应该遵守三个火枪手。
中台深入剖析和实现技巧
中台的出现时为了应对业务复杂度。
共享架构的发展过程
无共享架构,“烟囱林立”,各自重复开发;
共享资源——Iaas 架构;
共享 Runtime、中间件和 DB 等。
SaaS,整个应用进行共享。
中台架构。与 IaaS,PaaS 和 SaaS 都不同,它是将各个业务中相同的业务和数据共享出来。
理解中台概念
华仔提出,中台有两种——业务中台和数据中台。
业务中台
业务中台,是将企业内多个相似业务的通用业务能力沉淀到平台,以减少重复建设,提升业务开发效率的一种架构模式。三个关键点:
业务相关。这一点将中台和 IaaS,PaaS 和 SaaS 区分开来。
跨多个业务。
多个业务具有相似性。所以不可以将差异太大的业务强行塞入中台。
数据中台
数据中台,是将企业所有业务的数据沉淀到同一平台,支持业务间数据打通以及数据复用,提升企业运营效率的一种架构模式。三个关键点如下
所有业务。数据中台应该是支持所有业务的。
数据打通。业务间的数据需要打通。
数据复用。不同业务间的数据可以复用,提升整体的运营效率。三者中最难的部分,因为数据分布在不同的业务中,差异比较大,很难跨业务复用。
中台的价值
业务中台。其价值在于相似的业务之间可以互相借力,进行能力共享,从而避免了大量重复开发、提高开发效率。所以业务相似度越高,业务中台价值越大。华仔建议相似度达到 60%以上的多个业务共建中台。
数据中台。其价值在于数据打通与复用,避免数据孤岛,提升运营效率。
使用数据中台的业务越多,数据中台价值越大。
数据中台的价值体现在:统一数据平台、跨业务的数据打通、跨业务的数据复用(挖掘)。
但是跨业务的数据复用很难。数据分析就是一个发现规律、提取规律的过程,而业务之间的差异较大,如何设定数据复用的目标,本身就是一个很难的目标。比如两个业务部门,有不同的业务语言、思维模式,如何提取数据的相关性?沟通就是一个大问题。
中台带来的问题
小业务抱中台大腿,中台抱大业务大腿,中台建设最后就是一个组织结构问题;对于这一点,目前没有看到很好的应对方法,中台建设最后就是一个组织结构问题(康威定律)。
中台与业务的边界难以明确。即确定哪些业务由中台负责实现,哪些业务自己实现,界限不清。中台设计最难的不是领域划分,而是中台和业务的边界划分!中台适合做“组合式创新”,没法做“颠覆式创新”。可以用 Pipeline 封装不同业务流程的办法进行一定程度上的缓解。
中台的全流程效率并不高。本质原因是因为组织架构上,业务对应不同的部门,而跨部门沟通的成本可想而知是比较大的。
每个业务功能都要讨论边界;
每个业务功能都要考虑对所有业务的影响。
可以用 Pipeline 封装不同业务流程的办法进行一定程度上的缓解。
中台落地技巧
使用微服务来搭建中台。正所谓“微服务不一定是中台,中台一定是微服务!”。
用 Pipeline 封装不同业务流程,借此缓解中台与业务的边界划分难、全流程效率不高的问题。
类似于 Netty 的 Pipeline 使用 Handler 来封装不同处理流程中不同的功能点。相当于设计模式中的职责链模式,对应于面向对象中的组合。
用 SPI 分装不同的业务。相当于是设计模式中的模板模式,对应于面向对象中的继承。
Pipeline 和 SPI 方案对比
根据设计模式中组合优先于继承的观点,似乎应该优先使用 Pipiline。事实上华仔也说应该优先使用
Pipiline,主要原因是因为其开发难度较低,而 SPI 需要明确中台和业务的边界,开发难度高
思考题
假如公司已经有一个正在线上运行的业务了,现在准备做一个类似的业务,此时需要上中台么?
根据情况吧,因为业务中台相似业务越多,价值越高,而此时还只有两个同类业务,根据合适原则,如果这样的情况不多,就没有必要上中台了;如果业务发展很快,可以考虑做一个简单的中台,遵循先粗粒度然后拆分的原则。
实战 - 手游电商平台微服务
如果现有架构是单体,那解决现有架构的问题,微服务改造不一定是最佳的、自然而然的答案,还是要具体问题具体分析。
思考题
业务中台高度抽象的业务模型有什么优点,又有什么缺点?
优点:可以复用。对设计的要求比较高,如果设计的不好,反而复用性不好。
缺点:不能直接用于具体的业务,需要进一步的开发,沟通成本比较高。
评论