type
status
date
slug
summary
tags
category
icon
password
本文是Manning的《微服务架构设计模式》一书的整理笔记,并加入了一些自己的想法,给尚未读过这本书的人探探路。这本书的代码部分是基于Java编写的,我个人希望脱离Java体系的捆绑,所以文中不会照搬书中的Java内容,只整理架构性、思路性的东西。
本文是笔记的第三部分,描述如何开发业务逻辑,具体涉及到Saga分布式事务管理、领域驱动设计、事件溯源以及CQRS等话题,这些都是十分重量级的设计方案,本文更倾向于分析使用它们的代价权衡,而不是抄书描绘实现细节。
第4章:使用Saga管理事务
由于服务被分散到多个进程中,而每个进程各自拥有自己的可能是异构的数据库,还使用中间件通信,这导致无法执行传统开发中数据库的分布式事务,在这种情况下需要使用一种叫Saga的模式模拟分布式事务,Saga的一个挑战在于牺牲了ACID(原子性、一致性、隔离性、持久性)中的I(隔离性),只要保证最终一致性,那么就需要使用额外的手段尽量提高隔离性。Saga 实质上从强隔离走向语义隔离,它依赖业务语义(状态机、幂等、版本控制)来‘补偿’数据库的隔离缺口。
Saga的思路是,把一个事务中的一连串调用分成两类,一类是可补偿性事务,比如创建订单后,后续服务如果失败,则执行一个补偿事务对订单创建执行反向操作,另一类是关键性事务,这种事务通常被放在最后,一定成功,必须幂等,不会回滚。
Saga存在两种协调模式,一种是协作式Saga,请求由一个Server发出,并且通过事件在不同Server之间传递,这种方式简单,但难以理解,可能导致服务调用时的循环依赖,并且导致了两个服务要同时修改代码的紧耦合,另一种是编排式Saga,由一个中央类(或者AWS StepFunctions这类中间件)维护一个状态机,状态机启动服务,之后和每个服务通信,这种做法不会引入循环依赖(因为都依赖状态机),每个服务都不需要知道其他Saga参与方服务,改善了关注点隔离,但一个弊端在于把事务的业务逻辑都推进了状态机里,需要防止这里的架构腐化。通常境况下,推荐使用编排式Saga。
Saga模式缺乏对事务隔离性的支持,每个Saga事务做的更新都能被其他Saga看到,这可能会在并发情况下导致数据库异常,比如更新被其他事务覆盖,或者脏读等。有几个对策可以在编码层面缓解隔离性缺失的问题。比如采用语义锁,举例来说,在Saga事务开始时改变Order.state为Pendding,这样其他事务看到这个标记后就可以返回失败或者暂时阻塞;比如在更新之前重新读取数据验证一致性;比如添加版本号;比如把更新操作设计得可以交换顺序;比如通过调整调用时序减缓脏读等。
第5章:微服务架构中的业务逻辑设计
在这一章中讨论了DDD的设计方式如何运用到微服务当中。业务逻辑的组织方式有两种——事务脚本和领域驱动设计(DDD)。
事务脚本就是典型的贫血模型,举个例子来讲,订单类Order里只定义字段,也就是数据结构,不承载方法,方法都由OrderService和OrderDAO承载,这是一种以数据结构和数据流程驱动的设计方法,使用这种模式的后果就是在服务层会形成很长的面条代码,事实上大多数商业代码也都是用这种方式构架的,不能彻底否认它的价值。
但是对于以业务流程驱动设计,并且让数据库严格拆分的微服务架构,使用领域驱动设计则更为合适,因为它是纯粹面向对象的,所以容易测试和使用各种设计模式扩展。关于领域驱动设计是一个很大的话题,在这里只稍微提及一下战术层次的几个要素,在DDD中,有实体、值对象、工厂、存储库、服务、聚合、领域事件等概念,具体如何拆分服务,请参考DDD相关的书籍。
DDD模型承载在微服务六边形架构的内层,而外层则是各种入口和出口适配器,例如,入口有REST API,有来自其他服务领域事件队列的消费器,有针对本服务的CQRS命令接收通道,出口有出栈命令适配器,有存储库适配器,有领域事件发布适配器等。这种设计让领域层保持纯净,可测试,只与抽象接口而非基础设施交互,把与基础设施交互的逻辑都放在外层。
在使用DDD设计微服务时有几个原则要掌握好,第一是外部API调用以及CQRS的C只引用聚合根,第二是聚合之间的引用一定使用主键,第三是在一个事务中只能创建或更新一个聚合,且如果聚合涉及多种实体,则需要使用Saga,这些原则保证了一个微服务的独立性,即服务内部的ACID特性,也保证了服务之间的ACD特性。
领域事件则是当一个聚合发生某种变化时对外发出的通知,本质就是个发布订阅模式的异步调用,它用一套编程语法对外部消息队列发送特定格式的消息,由关注这个事件的下游订阅。以AWS举个例子,可以是当S3上上传一个对象时,通过S3 Event发出领域事件,由Lambda函数订阅进行下游处理。
但是我认为,熟练运用DDD恰恰是推行微服务架构最大的阻碍,因为掌握的人很少难以招聘,且运用DDD还要求这人是个懂infra和运维的全栈,一旦DDD使用不当,带来的从业务层到infra层的级联修改成本极高。
第6章:使用事件溯源开发业务逻辑
事件溯源是一种数据持久化技术,在谈论它之前,先举一个传统数据持久化的例子,比如有订单表Order和订单项目表OrderLineItem,通常会使用ORM映射直接存储结果值,但是这种做法存在几个问题,第一是老生常谈的ORM阻抗失调,第二是这两个表作为一个整体聚合,它们的历史无法追溯,第三是如果想追溯历史实现审计需要付出额外的努力,第四是持久化的同时如果想发送领域事件,则必须手动实现这些逻辑,事件发布凌驾于业务之上。
而事件溯源是以另一种思路存储数据,通过记录行为而不是数据来避免这些问题。它不直接让DB存储聚合的信息,而是强制命令操作发送领域事件,每个事件代表一次状态改变,将事件本身写入DB,在读取DB时,DB会将跟某个ID相关的事件全都读取到进程中进行折叠或者叫reduce,形成最新状态的实体。这个模式下有一些技巧,第一可以使用版本号和乐观锁保证事件写入的原子性,第二可以通过加入事件ID或时间戳等版本号要素应对并发情况下先来的后处理问题,第三可以折叠某个时间点之前大量的记录来节约存储空间。
事件溯源的好处在于,由于强制发布并持久化了领域事件,这很有利于审计和溯源,它完全保留了聚合的历史演进,给开发者提供了时光机,也最大程度地避免了阻抗失调。但是它的坏处也十分明显,首先有学习曲线,然后必须保证消息的传递一定成功,增加了架构复杂性,第三是随着业务演化,事件的数据结构的向后兼容性是个难题,第四是删除数据和查询数据都有一定难度。
书籍的后几节提供了一个Java框架来实现这一套模式,但是我想从架构角度补充一些这个架构的其他特点。首先存储事件而不是实体会导致DB压力和存储空间激增,也会导致Java进程聚合数据时的CPU和内存压力激增,归根结底,溯源的需求偏向OLAP范畴,但是事件溯源架构模糊了OLTP和OLAP两种需求的边界,既要有要,那就让数据库设计要在两边不断权衡撕扯,放在AWS上,如果想减轻数据库的压力,就要做冷热分离,事件存储走Kafka、快照进RDS、长期分析走Redshift Spectrum,这其中还要涉及到一系列数据流水线和ETL逻辑,技术成本一下就上去了。
ps.这和IoT领域的InfluxDB还不一样,虽然看上去有点像,但这个DB强调实时数据获取,接口偏向聚合,不是拿来溯源用的。
事件溯源我认为只适合金融等特定领域,保留历史快照是刚需的项目、只适合需求相对稳定、愿意大力投入技术的成熟团队。
第7章:在微服务中实现查询
微服务一旦割裂了数据库,就必须为读的复杂性支付代价。这一章要解决的问题是,多个微服务把DB隔开了,现在要跨DB查询,如何实现?
第一种模式是使用API组合,也就是对每个微服务都调用一个查询API,把他们的结果在程序里做组合。那么在哪里做调用和组合呢?有三个地方,第一是客户端,第二是API Gateway的实现里,第三个是开发专门的用来查询的微服务,但不论如何,依赖多个服务查询不但会降低系统的可用性,且难保证数据一致性,增加网络开销和多个DB的负载,通过编程手动组装多个服务返回的数据结构也是个苦差事。
而且这种模式,很难解决查询多个历史订单、分页排序等复杂问题,且随着查询需求的扩展,查询逻辑也要不断扩展,使代码很难维护。
这一章提倡的模式叫CQRS(命令查询责任分离),简言之,让对领域模型做增删改的操作走API服务,压一个“写库“,写库把数据变化实时同步到一个读库里,读库里有查询需求所需的视图,让读请求通过另外的API压这个”读库“,甚至可以专门开发用于查询的读取用微服务。至于数据的同步,可以物理传递,也可以通过领域事件触发一个对读库视图更改的API。
这个模式的好处在于,在微服务架构中能高效地实现各种复杂的查询,甚至可以选用不同的数据库承载写入和读取的任务,且对事件溯源模式支持友好,彻底实现了读写分离。但坏处在于,技术架构非常复杂,且数据复制有延迟,可能面临无法在写入后立即读取到最新数据的问题,并且,视图需要不断地更改维护。
接下来是我对这个模式的个人理解,读写分离是好的,但CQRS用在微服务里,纯粹是因为微服务强制拆分了DB,这种洁癖式强迫症带来了一系列的紧身衣,理念反噬了工程,在高昂的工程代价的背后(写库,发事件,同步,甚至ELT),要面临的还是数据同步和一致性的问题,是微服务的弊端,导致它不得不CQRS。我并不是说CQRS的想法不好,甚至在某些场景下是必须的,即OLDP和OLAP既要又要的时候,比如IoT设备要把数据写入InfluxDB,但是冷数据要丢该S3或者其他存储工具保存,在数据工程里,CQRS是数据分层的必然结果,源自需求驱动的架构演化,而在微服务语境里,则是自我设限的苦果。
写在最后
这本书还有两章谈如何做测试和重构,我想已经没有读下去的必要了,因为实现一个真正的微服务架构对公司和团队要求极高,不但全员要用DDD建模和开发,还要学习上面提到的这些紧身衣,还要懂各种Infra的使用,以及需要成熟的DevOps文化,关键的是,需要一群全才在一起工作,这十分不现实。并且在整个微服务的理念里,我看到它带来的技术架构弊端甚至大于了服务拆分带来的好处,为了拆服务带来巨大的技术维护成本究竟值得吗?试问什么样的系统一定非要走微服务的路线不可呢?
书中提出的每个技术模式在某些场景下都是合适的可用的,但是硬要拼凑在一起就太折磨人了,我认为一个比较理性的策略,是在K8S上稳定低跑单体应用,即便到了不得不拆服务的地步,也是拆成两三个大单体,而不是真的微服务,这样既能享受平台的红利,又不必付出过高的技术成本,做恰当的架构设计,不应该被理想主义所吞噬。