六、引入新的服务
接下来的这步,跳过了耦合、领域驱动设计等细节,我们引入了一项新的服务:Orders服务。在这项关键服务里,业务部分希望比其它应用程序变更的频度更高,但同时它的编写模式相当复杂。我们也可用这个模型来探索CQRS之类的架构模式(跑题了)。
我们要根据现有Backend内的实现来关注Orders服务的边界和API。实际上,这个实现更可能是个重写而不是利用现有代码的端口,但是想法或方法都是相同的。注意在这个架构中,Orders服务有自己的数据库。这点很好,尽管还差那么几步,但离达成一个完整的解耦也已经不远了。接下来还需要考虑以下几个步骤。
同时,这也是考虑该服务在整个服务架构中所处角色的好时机,需要做的是关注于它可能发布或消耗的事件。现在是时候进行事件冲突(Event Storming)这类活动了,并思考在开始处理事务性工作负载时我们该发布的事件。这些事件在集成其它系统甚至在演变单体式应用时,都会派上用场。
七、将API与实现进行对接
在这里,我们应该继续推演该服务的API和领域模型,以及如何在代码中实现模型。该服务会将新的事务性工作负载存储到其数据库中,并将数据库与其它服务分开。服务访问这些数据时必须经过API。
不能忽视的是:新服务及其数据与单体式应用中的数据关系紧密(虽然在某些地方不完全相同)。实际上这非常不方便。开始构建新服务时,需要来自Backend服务数据库的现有数据的支持。由于数据模型中的标准化、FK约束、关系,这可能会非常棘手。在单体式应用/backend上重用现有API的话,粒度可能过于粗糙,这就需要重新发明一些技巧来获取特定形式的数据。
我们要做的是通过底层API以只读模式从Backend获取数据,并重塑数据以适应新服务的领域模型。在此架构中,我们将连接到后端数据库,并且直接查询数据。这一步需要一个能反映直接访问数据库的一致性模型。
一开始,可能有些人会不敢采用这种方法。但事实是,这方法绝对可行,而且已经有在关键系统中应用成功的案例了。更重要的是,它不是最终架构(不要认为它可能成为最终架构)。可能你会认为连接到后端数据库、查询数据和将数据制作成新服务领域模型所需的正确形式,会牵涉到许多不成熟,堆砌而成的代码。但我认为这只是暂时的,所以在单体式应用的演化过程中,这可能是没问题的,也就是说,首先利用技术债,然后再迅速偿还它们。不过,还有个更好的办法。我会在本主题的第二部分讨论。
又或者,大家还会说:“好吧,只需要在后台数据库前立个REST API,然后就可以提供更低级的数据访问,再用新的服务调用它”。这也是个可行的方法,但它不是没有缺点。同样的,我也会在第二部分更详细地讨论这点。
注意事项
八、发送shadow traffic到
新的微服务(dark launch)
接下来,需要将流量引入到新的微服务。注意,这不是一场重量级的发布。简单地把它扔到生产流量中显然是不行的(特别是考虑到本例中使用了接受订单的“订购(order)”服务!这个过程中我们当然不想产生任何问题!)。虽然更改底层的单体式应用数据库不是件容易的事,但如果可能,您可以小心地去尝试更改单体式应用应用程序,使其调用新的订单服务。如果你不知道哪种方式最好,我强烈推荐你看看Michael Feather的《有效利用遗留代码》③。Sprout Method/Class或Wrap Method/Class这样的模式也能帮到你。
当变更单体式应用/后台时,我们希望保留旧的代码路径。这就需要加入足够的代码,让新旧代码路径都能运行,甚至并行运行。理想情况下,变更后的新版单体式应用应该允许我们在运行时,能选择是将流量发送给新的订单服务、还是使用旧的代码路径,或是两者兼顾。无论采用什么调用路径组合,我们应当了解新旧执行路径之间存在哪些潜在偏差。
另外要注意的是,若允许单体式应用将执行命令发送给旧代码路径以及用于调用新服务,我们需要某种方法来将该新服务的事务或调用标记为“合成(synthetic)”调用。如果你的新服务没有本例那么重要,且可以处理重复内容,那么识别这个合成请求可能就不那么重要。如果你的新服务倾向于更多的为服务于只读流量,可能就不用再识别哪些是合成的事务。然而,在综合交易的前提下,你会希望能够端到端地运行整个服务,包括存储和数据库。此时您可以选择使用“合成(synthetic)”标志来标记数据并存储,或者在数据存储支持的前提下,回滚该事务。
最后需要注意的是,当我们变更单体式应用/Backend时,我们希望再次使用灰度上线(dark launch)/金丝雀测试(canary)/滚动发布(rolling release)。但基础设施必须支持它才行。在第二部分我们会详细讨论。
在这里,流量被迫回到单体式应用。我们试图不扰乱主要的调用流程,以便当canary无效时能够快速回滚。另一方面,部署网关或控制组件可能会发挥一些作用,它们能以更细的粒度控制对新服务的调用,而不是将调用强加给单体式应用。这种情况下,网关将具备控制逻辑,即能选择是否将事务发送给单体式应用、新服务还是两者都发。
注意事项
九、金丝雀测试或滚动发布新的微服务
若前面的步骤不会对事务路径产生不良影响,同时,我们有很大信心能够通过背景流量相关的测试及初期的生产实验,那么现在我们就可以将单体式应用设置为“NOT shadow”,并将流量发送到新的微服务上了。这时,要指定特定的群组或用户,让其始终转入微服务。同时,我们正在慢慢导出那些从旧代码路径通过的真实生产流量。我们可以增加Backend服务的滚动发布频率,直到所有用户都转到新的订单微服务上。
需要提醒一下,这里存在风险:当我们开始将实时流量(非影子或合成流量)滚动到微服务时,期望与群组匹配的用户总是去调用这个微服务。因为我们已经不能在新旧代码路径之间来回切换了。此时,如果我们想要实现回滚,就会牵涉到很多协调,才能使新事务从新业务移回到旧业务单元时也能使用。希望这种情况不会发生,但我们必须有所警惕并事先做好计划,有相应的测试。
注意事项
十、离线数据ETL/迁移
至此,订单微服务开始承载实时的生产流量了。单体式应用或Backend仍然在处理其它需求,但我们已成功地将服务功能迁出了单体式应用。接下来需要迫切关注的是,需要还清新的微服务和Backend服务之间建立直接数据库连接时产生的技术债。这很可能牵涉到从单一数据库到新服务的一次性ETL(提取转换加载)。单体式应用可能仍需要只读式地保存那些数据(比如出于合规的考虑等)。如果它们是共享的引用数据(比如只读的),这么做应该没问题。必须确保单体式应用和新的微服务中,各自的数据不共享。如果它们是的话,那么最终会出现数据或数据所有权的相关问题。
注意事项
十一、解耦数据存储
完成了上一步,新的订单微服务准备就绪,可以加入到服务架构中去了。本文介绍的步骤都有各自的注意事项和优缺点。我们的目标应该是完成所有步骤,避免技术债产生利息。当然,这种模式与实际操作可能会有差异,但方法没有问题。
在接下来的后续博文中,我将展示如何使用之前提到的示例服务来完成以上步骤,并深入探讨对哪些是有帮助的工具、框架和基础设施。我们会看看Kubernetes、Istio④、特性标志框架、数据视图工具和测试框架等内容。请保持关注!
原文链接:
http://blog.christianposta.com/microservices/low-risk-monolith-to-microservice-evolution/