如何设计实现真正的响应式微服务系统?

这是一篇讲解微服务系统在扩展性伸缩性方面的演进文章,Jonas Boner认为目前普通的微服务最终将演进为事件驱动的响应式微系统架构(Reactive Microsystem), 英文PPT文档见:这里

今天系统架构大概有三种:单体Monolith、微单体Micoliths和微服务Microsystems,所谓微单体就是介于单体和微服务之间的一种,有很多服务,数据库也进行了分库分表,不像单体那样只有一个大的WAR应用和一个数据库,但是又不像微服务那样,每个服务都有自己独立的数据库,互不干涉。

大部分微单体系统是切分了单体系统而形成的,每个Microlith由一个台服务器运行,有自己的REST/Servlet,有自己的服务和JPA数据库,数据库访问是一种同步堵塞式的数据库线程连接池方式访问,微单之间式通过同步的堵塞的RPC访问(比如Dubbo和gRPC),这样的系统没有弹性,不具有伸缩。


Carl Hewitt说: 一个Actor模型不是真正的Actor,一个系统中应该有多个Actors。

迈向可扩展的微服务系统是引入Actor模型,有三种有用的方式:
1. 事件第一的DDD Events-first DDD
2. 响应式设计 Reactive Design
3. 基于事件的持久 Event-Based Persistence

实践的事件第一的领域驱动设计
这种方式有别于传统的DDD,传统DDD最后可能形成一个臃肿的肥胖的聚合根,大家都把东西塞在里面,这种方式的问题在于我们过于关注聚焦名词:领域对象,就像我们过于关注数据表一样。

相反,我们应该首先聚焦关注发生了什么事情,也就是动词,是事件。

Greg Young说:当你开始对事件建模时,将会强迫你思考系统的行为,而不是思考系统的结构。

具体怎么做呢?在传统DDD中,分析建模第一步是找出情景场景,也就是有界的上下文,但是如何定义有界的上下文却很难定义,有可能来自于用例需求的分解等等,但是这些边界划分不一定是面向领域的,可能是面向功能的,所以,事件建模就是通过事件来定义有界上下文。

比如订单发生的地方是一个上下文,而进出库事件发生的地方又是一个上下文。这些上下文都是因为发生不同事件而自然形成边界。

事件重要特征是它代表发生的事实,这在很多跟踪系统中有非常重要的应用,比如货物跟踪,财务和人事跟踪记账系统等等。

寻找发生的事实,如同大侦探波罗探案一样,他在东方快车谋杀案中自诩和上帝一样能看清真相,当然这也可能来自于他的强迫症,当他发现犯罪事实竟然是来自每个人的人性之恶时,他再也不敢自诩上帝了。

我们使用DDD分析的系统真相,当然不会涉及到道德和人性幽暗,所以,在这样的逻辑系统我们是可以
扮演上帝的,那么如何发现需求中的事实真相呢?通过事件风暴,Event Storming。

事实是遵循因果一致性的,理解事实是如何进行因果相关,Peter Alvaro说:因果性通往时空的途径。哲学大师康德认为时空是我们观察世界的方式,而时空蕴含的因果性正是我们科学追寻发现的目标。

因果关系代表了一种因果一致性,有因必有果,这是一致的,不会前后矛盾,是前后顺序的,如同老子说:道生一,一生二,二生三,三生万物,这也是表达了一种因果一致性。因果一致性对立面就是矛盾,如果发现矛盾,实际就是否定了因果一致性,有时我们会从矛盾方面去证伪因果一致性。因果一致性是形式逻辑的重要特征。

既然存在因果一致性,就会存在一致性自然形成的边界,比如从第一因一直到最后一个果,这就自然形成了一条因果链,这条因果链自然也就形成自己的边界,如果两条因果链放在一起,我们能够区分它们也是因为边界不同。但是在佛教或现代艺术中,经常把最后一个果和第一个因再链接起来,形成了因果循环,也就是轮回,这实际破坏了一致性,是非时空理性思维方式。

在软件需求中,我们如何发现因果链形成的边界?一般这样边界中需要包含可变状态和已经发生的事实,这些事实其实是造成状态可变的原因,只不过表现形式不同而已。

所以,DDD中的寻找聚合,实际就是寻找因果一致性单元,也是寻找会发生failure的单元。

响应式设计

响应式设计分为两个阶段:初期的响应式编程和分布式的响应式系统。

响应式编程能够帮助我们让独立的实例系统(一个VM 一个JVM或一台服务器)内部运行更高效。通向响应式第一步是引入异步,也就是引入非堵塞,能够更有效地利用资源,防止数据库连接池的暴障拖垮数据库,或一个很轻的小动作就能让巍峨的Oracle当机趴下,这些都是同步堵塞造成的罪行。

异步和非堵塞能够降低共享资源比如数据库表等的竞争,避免大量使用事务机制造成的死锁和数据库连接池耗尽。

使用异步和非堵塞,会降低快速访问冲击缓慢的后端系统,比如大量的并行峰值访问会立即打趴巍峨的ORacle数据库。在一个大型系统在,存在各种不同处理效率的子系统,如何在快与慢之间协调,异步系统往往扮演重要的协调身份。

引入了响应式编程可以帮助处理好一台机器内多线程问题,而响应式系统则是带给我们多台机器之间的分布式协调高效处理,实现弹性和伸缩性。

响应式系统是基于异步消息传递,能够在空间和时间两个维度上解耦。能够实现从CPU核到Socket 到容器到服务器到数据中心的透明化处理方式。

真正微服务系统应该是响应式编程+响应式系统,每个微服务都需要被设计为一个分布式的微系统,

首先,将无状态行为从有状态的实体中分离出来,形成命令或事件,事件代表行为,实体代表结构,这样行为和结构才能分别各自扩展伸缩。因为扩展无状态行为比扩展有状态的实体要容易得多。

基于事件的持久化

JIM Gray说:覆盖式更新(update-in-place)是设计师是原罪,它违反了传统几百年来的会计记账实践。

Pat Helland说:真相是日志Log,数据库只是日志子集的缓存。

对于建立一种可伸缩的持久存储系统来说,事件日志才是正道。

CRUD增删改查已经死了,只能留下增读两个操作CR,更新和删除UD应该放入坟墓。

事件溯源Event Sourcing是一种CR,通过事件建模,能够让你注重系统中的来龙去脉,时间成为系统关键因素。

通过事件日志记录事件按时间发生的顺序,一旦发生事件就不断新增追加到事件日志,其他使用者只要根据时间顺序不断读取这些事件,再执行这些事件代表的动作,事件代表因,事件执行后自然会产生结果。

同时,事件日志避免了面向对象和关系数据库的不匹配阻抗问题。让对象归对象,让数据库归数据库,
对象用于命令执行写操作,而数据库用于查询读操作,通过CQRS分离读写操作。使用事件日志同步读写两个系统的数据。

如何协调不同聚合之间的交互呢?通过事件驱动的工作流,比如有三个聚合:订单、支付和库存,下订单时,需要支付系统支付钞票,然后从库存出货, 其中交互流程是这样:

1. 开始订单处理
2. 订单系统发出保留订购商品的命令给库存系统
3. 库存系统接受命令后,检查是否有库存,如果有执行保留该商品命令,发出商品被保留的事件
4. 订单系统因为事先订阅了商品被保留的事件,因为通过Kafka等事件流总线会接受到商品被保留的事件,
5. 订单系统然后向支付系统发出扣款支付的命令
6. 支付系统接受到扣款命令后,如果能够扣款,执行扣款,发出已经支付的事件。
7. 订单系统因为也事先订阅了支付系统的事件,将接受到已经支付的事件。
8. 订单系统向库存系统发出发货命令。
9. 库存系统接受发货命令,将之前预先保留的商品进行真正出库,发出商品装运事件。
10. 订单系统因为也事先订阅了库存系统的事件,将接受到商品已经出库装货的事件。
11. 订单系统确认整个订单完成。

分布式事务怎么处理?

Pat Helland说,两段提交的分布式事务其实是一种反高可用性的协议。

使用最终一致性的事务协议是一种猜测、抱歉试错和补偿的协议。比如Saga模式,Saga协调器能够知道长事务运行中所有事务步骤,如果任何一个步骤,比如上面库存系统出错,Saga协调器将会采取补偿回退之前所有步骤。

总结:不要建立微单系统,微服务其实是分布式微系统,积极拥抱响应式设计原则,拥抱事件第一的DDD和事件持久化方案。

原文