微服务设计-读书笔记5
分解单块系统
一、分解的关键概念——接缝
接缝的概念,是指从接缝处可以抽象出相对独立的一部分代码,对这部分代码进行修改不会影响系统的其他部分。识别出接缝不仅仅能够清理代码库,更重要的是,这些被识别出的接缝可以成为服务的边界。
二、分解单块系统的原因
分解单块系统的方法是增量修改,增量方式的修改可以降低修改风险,快速响应。
如何增量修改?从哪里下手呢?优先修改什么地方得到的收益最大呢?可以参考以下指导因素:
(1)、改变的速度
抽取某个接缝出来作为服务,使其成为一个自治单元,能够带来后期开发速度的大幅提升。那么就可以优先考虑分解这块内容。
(2)、团队结构
抽取某个接缝出来后,能够使不同的团队更好的全权负责该接缝,那么可以优先考虑。
(3)、安全
系统中某一部分需要涉及对敏感信息的保护,如果把这个服务分离出去,可以对它做独立的监控、传输数据保护、静态数据保护等,那么可以优先考虑。
(4)、技术
系统的某一部分与业务关联不大,分离出来能大大改善系统服务,比如算法等,可以考虑优先分解。
三、整理杂乱的依赖源头——数据库
数据库通常是接缝之间所有杂乱依赖的源头。
(1)、找到问题的关键
首先查看代码中对数据库进行读写的部分,这些代码通常使用一些框架如Hibernate等来把代码和数据库进行绑定,从而简化对象和数据库之间的读写操作。需要做的是将这些代码按照限界上下文分到不同的包中,例如:
注意:分离时需要把数据库映射相关的代码和功能代码放在同一个上下文中。
(2)、分离不同限界上下文中的表结构
通常存在不同限界上下文中的表结构存在相互依赖。可以使用工具SchemaSpy(http://schemaspy.sourceforge.net)来可视化这些依赖。将其中只在某个限界上下文中用到的表分离到对应的上下文中。对于不同上下文之间耦合的表,要消除这些耦合因素。
<1>、比如,打破外键关系
总账表和行条目表之间存在外键约束,打破外键约束的步骤是:先去除财务部分的代码对行条目标的访问,让财务部分通过产品目录暴露的API访问,然后消除外键关联(放弃外键条件,后面需要实现跨服务的一致性检查,或者周期性触发清理数据的任务,来补偿)。
<2>、比如,分离共享的静态数据
对于一些不变得数据,比如国家代码,可以将这些数据共享,共享的方法有:@1,为每个服务保存一个静态数据表的副本。(可能会导致潜在的一致性问题,比如漏掉修改某个副本);@2,把这些共享的静态数据放入代码,比如放入属性文件中,或者枚举值中。(数据一致性问题依然存在,但修改配置文件比修改数据库简单);@3,把这些静态数据放入一个单独的服务中。(这样做的前提是数据量和复杂性及相关的规则值得这样做)
<3>、比如,分离共享的可变数据
比如,财务代码会追踪客户产生的订单信息,同时也会追踪退货和退款。仓库代码也会在客户订单被分发或者接受之后更新订单信息。这些信息需要在网站某个地方统一显示出来,这样客户就可以看到他们的账户活动记录。
分离的话,需要区分出来领域概念:客户。需要把抽象的客户概念具体化,通过API来访问客户服务。
<4>、比如,分离共享的表
比如,产品目录需要存储记录的名字和价格,而仓库需要保存仓储的电子记录。最初可能把这两个东西放在一个行条目表,这些在单体系统中是分辨不出来关注点的不同。在分解单体系统时就要考虑将它们存储在不同的表中。
四、重构数据库
Scott J.Ambler 和 Pramod J.Sadalage编写的《数据库重构》,详细介绍了重构技术。
实施分离的步骤
一般对于单体系统,推荐先分离数据库结构,暂时不对服务进行分离,这样做的好处是,可以随时选择回退修改还是继续做下去,而不会影响服务的任何消费者。
五、事务边界
使用单块表结构是,所有的创建或者更新操作都在一个事务边界内完成。分离数据库之后,这种好处就没有了。可能需要跨越两个事务边界,可能在一个事务中成功,另一个事务中失败。例如在创建订单这个场景中,更新订单表的同时,也应该在仓库团队的一张表中插入一条记录来通知他们去派发订单,在单事务和跨事务中的过程如下:
针对跨事务逻辑可能存在的部分失败,导致的不一致问题。可以通过如下方式处理:
(1)、重试
我们可以把失败的操作放在一个队列或者日志文件中,之后再尝试对其进行触发。(对于某些场景这么做是合理的,比如保证重试是可以解决问题的)。这种形式叫做最终一致性,相对于使用事务来保证系统处于一直的状态,最终一致性可以接受系统在未来的某个时间达到一致。这种方式对于长时间的操作来说尤其管用。
(2)、终止整个操作
这种情况下的另一个选择是拒绝整个操作。如果跨事务的操作,一个事务中成功。另个事务失败,可以采用补偿事务来抵消前一个成功事务的操作,但要注意补偿事务也可能失败,这时可以重试补偿事务或者使用后台任务来清除状态的不一致,甚至可以为后台维护人员提供界面操作或者实现自动化。
(3)、分布式事务
分布式事务会横跨多个事务,然后使用一个叫做事务管理器的工具来统一编配其他底层系统中运行的事务。就像一个普通事务一样,一个分布式的事务会保证整个系统处于抑制的状态。
分布式事务(尤其是短事务),常用的算法是两阶段提交。在这种方式中,首先是投票阶段,在这个阶段,每个参与者会告诉事务管理器它是否应该继续,如果事务管理器收到的所有投票都是成功,则会告知它们进行提交操作。只要收到一个否定的投票,事务管理器就会让所有的参与者回退。
分布式事务会使所有的参与者暂停并等待*协调进程的指令,从而容易导致系统的中断。协调进程也会用到锁(进行中的事务对某些资源持有一个锁)。对资源加锁会使系统很难扩展,尤其是在分布式系统的上下文中。
分布式事务在某些技术栈上已有现成的实现,比如Java的事务API,如果要使用分布式事务,尽量使用现有的实现而不是自己实现。
(4)、如何取舍
跨系统事务操作的上述解决方法都增加了复杂性,实际场景如果有一个业务操作发生在跨系统的单个事务中,需要再次确定是否真的需要这么做,是否可以简单地把它们放在不同的本地事务中,然后依赖于最终一致性?如果确实需要跨系统的事务,再考虑能否避免把它们放在不同的地方,如果实在不行,避免从纯技术的角度考虑,而是显式地创建一个概念来表示这个事务,再进行类似补偿事务这样的操作。
六、报表
在对服务进行分离的同时,可能需要对数据存储进行分离,但是这样做有一种需求的操作就会遇到问题,这就是报表。报表通常需要来自组织内各个部分的数据来生成有用的输出。
(1)、报表数据库
在标准的单块服务架构中,所有的数据都存储在一个大数据库中,所以很容易获取到所有的信息,通过SQL查询对几张表做一个连接即可。通常为了防止对主系统性能产生影响,报表系统会从副本数据库中读取数据。
这样做的好处是,实现非常简单。缺点是单块服务和报表系统之间共享数据库,对数据库的修改需要格外小心,此外这种方式的优化手段有限。
(2)、通过服务调用获取数据
这种方式依赖API调用来获取想要的数据。从两个或者多个系统中获取数据,然后进行组装。对于数据量很小的报表系统来说是可行的。但是当数据量很大时,这种API方式就不适用了。如果是标准的HTTP来进行大量调用,会带来很大的额外开销。
批量API方式,可以一次传递一组客户ID,请求的数据被服务写入某个CSV文件,通过共享位置读取到文件,这种方式可以工作的很好。
(3)、数据导出
相比于报表系统拉取数据的方式,把数据推送给报表系统也是一种可行的。这种方式使用一个独立的程序直接访问其他服务使用的那些数据库,把这些数据导出到单独的报表数据库。
虽然共享数据库不是好的方式,但这个场景是个例外,因为这样做足够简单来抵消它的缺点。
报表数据库包含了各个服务的数据,存在表结构的耦合,但是可以通过视图之类的技术来创建一个聚合,这样导出数据的服务只需要知道自己的报表视图结构即可。
(4)、事件数据导出
每个微服务可以在其管理的实体发生状态改变时发送一些事件,可以在客户端编写自己的事件订阅器把数据导出到报表数据库中,如下:
使用这种方式,与源微服务底层数据库之间的耦合就被消除了。我们只需要绑定到服务所发送的事件即可,由于事件的时效性,在事件发送时给报表系统发送数据,而不是靠原有的周期性数据导出,所以数据就能更快地流入报表系统。
因为事件数据导出的方式与服务的内部实现耦合很小,所以可以把这部分工作交给另一个独立的团队(而非持有数据的团队)来维护。只要事件流设计没有造成订阅者和服务之间的耦合,则这个事件映射器就可以独立于它所订阅的服务来进行演化。
这种方法的缺点是,所有需要的信息都必须以事件的形式广播出去,所以在数据量比较大时,不容易像数据导出方式那样直接在数据库级别进行扩展。
(5)、数据导出的备份
Netflix采用这种方式解决与扩展性相关的问题,他们将数据备份到Amazon的S3对象存储服务中,存储的文件叫做SSTables,在进行报表时,Netflix需要对所有数据进行处理,但考虑到扩展性问题,它使用了Hadoop,将SSTable的备份数据作为任务的数据源,实现了一个能够处理大量数据的流水线,并且开源出
来,这就是Aegisthus项目(https://github.com、Netflix/aegisthus)。
(6)、走向实时
上述各种方式都能够把数据从不同的地方汇聚到同一个地方,但所有的报表也可能需要从不同的地方产生,比如仪表盘、告警、财务报表、用户分析等应用。这些使用场景对于时效性的要求不同,所以需要使用不同的技术。但终极需求,就是实现将数据按需路由到多个不同地方的通用事件系统。
七、修改的代价
小的、增量的修改,可能理解这些改变会造成的影响,会帮助我们更好地消除错误的代价,在设计面向对象系统时可以考虑的技术:CRC(class-reponsibility-collaboration,类-职责-交互)卡片。可以在一张卡片写上类的名字、它的职责及谁与之交互。设计微服务时,可以把每个服务的职责列出来,写清楚它提供了什么能力,和那些服务之间有协作关系。这样遍历了解到的用例越多,就越能明白这些组件之间应该如何一起工作。
八、理解服务变大的根本原因
服务会逐渐变大,直到大到需要拆分。关键是要在拆分这件事情变得太过昂贵之前去完成拆分。