Chris Richardson 微服务系列翻译全7篇链接:
- 微服务介绍
- 构建微服务之使用API网关
- 构建微服务之微服务架构的进程通讯
- 微服务架构中的服务发现
- 微服务之事件驱动的数据管理(本文)
- 微服务部署
- 重构单体应用为微服务
原文链接:Event-Driven Data Management for Microservices
微服务与分布式数据管理问题
单体应用一般只有一个关系型数据库,这样的好处是可以实现 ACID 保证:
- 原子性(Atomicity):原子粒度的更改
- 一致性(Consistency)数据库的状态始终保持一致
- 隔离性(Isolation):并发的事务表现的像是串行执行,事务之间不会互相影响
- 持久性(Durable):一旦事务提交就不会撤销
因此,应用可以简单的开始事务,更改(增删改)多行数据,然后提交事务。
使用关系型数据库另一好处是支持 SQL。SQL 是一种丰富的、声明式的标准查询语言,用户能简易的关联查询多个表中的数据,然后RDBMS 查询调度器会执行最优的查询方式,用户不必关系底层的细节。所有的数据在一个数据库中也方便查询。
然而微服务架构中数据访问变的复杂,因为每个微服务都拥有独立的数据库,仅能通过 API 来访问。数据封装保证了微服务的松耦合,各个服务可以独立其他服务演进。而如果多个服务访问同样的数据,架构更新会更耗费时间,也需要更多的服务协调。
不同服务可能使用不同类型的数据库,现代应用存储和处理各种各样的数据,关系数据库并不总是最好的选择。一些场景下,一种特殊的 NoSQL 能提供更方便的数据模型、更好的性能和可扩展性。例如:使用 ElasticSearch 这样的搜索引擎来进行文本的存储和查询;使用 Neo4j 这样的图谱数据库来存储社交图谱数据。因此,微服务应用会混合使用 SQL 和 NoSQL 数据库,即多态型数据持久方案。这也带来了一些挑战:
1)如何跨多个服务实现事务,维护数据的一致性。我们以 B2B 商店为例:客户服务维护用户信用额度等相关的信息,订单服务管理订单并确保新订单没有超过用户的信用额度。单体应用中,订单服务可以使用 ACID 事务来核对用户信用额度并新建订单。而在微服务架构中, order 和 customer 表是各个服务私有的:
订单服务无法直接访问 customer 表,只能通过客户服务的 API。订单服务可能使用分布式事务,也被称为两阶段提交(2PC),然而 2PC 在现代应用通常不是很好的选择。CAP 定理需要用户在可用性和 ACID 的一致性中二选一,通常可用性是更好的选择。此外,很多NoSQL 并不支持 2PC,维护服务和数据库中数据的一致性是很重要的,因此我们可以选择另一种方案。
2)另一个挑战是如何检索多个服务中的数据,例如应用需要显示一位客户和他最近的订单,如果订单服务提供了用户订单的查询 API,那么可以在应用端获取该数据,应用端通过客户服务检索客户,再通过订单服务检索该客户的订单。假设订单服务只支持通过主键来查询订单,此时就没有合适的方法来检索所需数据了。
事件驱动的架构
对于许多应用,解决方案就是事件驱动的架构:服务在业务发生时(例如更新一条记录信息)会发布一个事件,其他微服务订阅该事件,当某一微服务接收到事件就会更新自己的业务记录,然后其他更多的事件会被发布。下图展示了如何使用事件驱动的方式在创建订单时检查可用信用,微服务间通过 MQ 来交换事件:
1)订单服务创建状态为 NEW 的订单,然后发布『订单创建』的事件
2)客户服务获取『订单创建』事件,为此订单保留信用,发布『信用保留』事件
3)订单服务获取『信用保留』事件,将订单状态修改为 OPEN
更为复杂的场景会涉及更多的步骤,比如核对用户信用时预留库存。
基于(a)每个服务原子性的更新数据库并发布事件;(b)MQ 确保事件至少交付一次,那么就可以实现跨服务的业务逻辑了。这并非 ACID 事务,他提供更弱一点的最终一致性。这种事务模型可称为 BASE model。
也可以使用事件维护关联多个微服务的物化视图。维护此视图的服务订阅相关事件并更新视图,例如:用户订单视图通过订阅订单事件和用户事件来进行更新:
当用户订单服务收到用户服务或订单服务的事件时,会更新视图。可以使用类似 MongoDB 的文档数据库为每个用户存储一份用户订单的文档。
事件驱动架构的优点:
- 他使得事务能跨多个服务并提供最终一致性;
- 使得应用可以维护物化视图。
不足之处:
- 编程模型比 ACID 事务更加复杂,为了从应用级别的错误中恢复,需要完成补偿事务,例如:信用检查不成功则必须取消订单;
- 临时事务会导致不一致的数据。另外应用从物化视图中读取的数据未能及时更新,也会产生不一致的问题;
- 必须检测并忽略重复事件
实现原子化
事件驱动架构还存在一个问题:以原子粒度更新 DB 与发布事件。例如:订单服务在订单表中 insert 一行记录,然后发布『订单创建』的事件,这两个操作需要是原子性的,否则,更新 DB 后,发布事件前服务崩溃了,系统将存在不一致。确保原子性的方法是使用 分布式事务 调用 DB 和 MQ。然而由于 CAP 理论,我们是想避免这么做。
使用本地事务发布事件
应用发布事件并保证原子性的方法之一就是:多步骤本地事务方法。技巧是 DB 中有一张 EVENT 表(模拟消息队列),存储业务数据的状态。首先启动一个本地数据库事务,更新业务数据记录并往 EVENT 表中插入一条数据,最后提交事务。一个单独的线程会轮询 EVENT 表,将查询结果往 MQ 中发送事件消息,然后使用本地事务标注事件状态为已发布。如下图所示:
订单服务首先往 ORDER 表中 insert 一行记录,然后在 EVENT 表插入类型为 Order Created 的事件(状态为 NEW )。事件发布线程或进程轮询 EVENT 表中未发布的事件,发布事件然后更新 EVENT 表事件状态为已发布。
这种方法的优点:
- 保证每次更新都有对应的事件发布,不使用两阶段提交(2PC);
- 应用发布了业务级别的事件,消除推断事件类型的麻烦。
不足:
- 易出错,因为要求开发者必须记得更新后去发布事件;
- 当使用 NoSQL 时,因为 NoSQL 的事务和查询能力有限,实现起来较麻烦。
挖掘数据库事务日志
另一种不需要两阶段提交就能实现原子性的方法是:分析数据库事务日志或提交日志来发布事件。应用更新 DB时,DB的事务日志会记录这些变更,事务日志挖掘线程或进程读取这些日志,并将事件发布到 MQ,如下图所示:
范例之一就是开源的 LinkedIn Databus 项目,Databus 挖掘 Oracle等数据库的事务日志并发布相应的事件,LinkedIn 使用 Databus 来保持各种派生数据存储和记录系统的一致。
另一范例就是 streams mechanism in AWS DynamoDB,AWS DynamoDB 流包括 DynamoDB 表在过去 24 小时内的时序变化,包括新建、更新和删除操作。应用能读取这些变更,将其作为事件发布。
事务日志挖掘的优点:
- 能保证无需使用两阶段提交就能对每个更新发布事件;
- 简化应用,将事件发布与主业务逻辑分离。
不足:
- 每个 DB 或 同一 DB 的不同版本的事务日志格式不同;
- 很难从低级别事务日志的更新记录中反推高级别的业务事件。
使用事件源
事件源通过采用一种截然不同的、以事件为中心的方法来保存业务实体,而不需要 2PC 来实现原子性。这种方法存储一系列状态变动的事件,而不是实体的当前状态。应用通过重放事件来构建实体的当前状态,每当业务实体的状态改变,就往事件列表中添加新的事件。由于保存事件是唯一操作,本质上就是原子性的。
以订单为例:传统方案中,每个订单为 ORDER 表中的一行记录。使用事件源时,订单服务存储导致订单状态变化的事件,包括创建、批准、配送、取消。每个事件由充足的信息来重新构建订单:
事件被存储 DB 中,可使用 API 添加或查找实体的事件。事件存储类似上文提及的 MQ,提供 API 让其他服务订阅事件,将事件传达至感兴趣的订阅者。事件存储是事件驱动的微服务架构的支柱。
事件源的优点:
- 解决了事件驱动的微服务架构的关键问题,能够可靠的发布事件;
- 解决了数据一致性问题,由于存储事件而不是领域对象,也避免了面向对象到关系数据库的不匹配问题;
- 为实体提供了100%可靠的审计日志,使得获取任何时间点的实体状态成为可能;
- 业务逻辑与事件交互的业务实体是松耦合的,这使得单体服务迁移到微服务更容易。
不足:
- 采用了陌生的编程风格,学习曲线陡峭;
- 事件数据库只支持通过主键查询业务实体,必须使用 CQRS(Command Query Responsibility Segregation)来完成查询,因此应用程序必须采用最终一致性。
总结
微服务架构中,每个微服务都有自己的数据存储,不同的微服务可能使用不同的 SQL 和 NoSQL 数据库。这些数据库架构有很多优势,也带来了分布式数据管理的挑战。第一个挑战就是如何实现跨服务的业务事务,并保证一致性;第二个挑战就是如何从多个服务中查询数据。
对于许多应用,解决方案就是使用事件驱动的架构。事件驱动的架构带来的挑战是如何原子化地更新状态和发布事件。有几种方案可以考虑,包括把数据库用作消息队列、事务日志挖掘和事件源。