代码实战:从单体式应用到微服务的低风险演变

8个月前

大量干货和工具:服务测试、部署和UI调整,用ISITO来管理路由以及API设计。

本文获得blog.christianposta授权翻译发表,转载需要注明来自公众号EAWorld。


作者:Christian Posta 

译者:海松 

原题:Low-risk Monolith to Microservice Evolution Part II


继续来深入探讨!在之前的文章(第一部分)中,我们为本篇文章建立了一个上下文环境(以便于讨论)。一个基本原则是,当微服务被引入到现有架构中时,不能也不应该破坏当前的请求流程(request flows)。“单体应用(monolish)”程序依然能带来很多商业价值(因此仍将在新的时代被使用,编者注),我们只能在迭代和扩展时,尽可能地减少其负面影响,这过程中就有一个经常被忽略的事实:当我们开始探索如何从单体应用过渡到微服务时,会遇到一些我们不愿意碰到的难题,但显然我们不能视而不见。如果你还没读过这段内容,我建议你再回去看看第一部分。同时也可以参考什么时候不要做微服务[0]


关注推特上的(@christianposta)或访问http://blog.christianposta.com,以获取最新更新和讨论。


在此前的第一部分,想解决的问题有: 

  • 如何可以有效可靠地生成微服务。以及如何建立一个持续交付的系统。 

  • 如何能够对服务和单体应用等对象进行测试。 

  • 如何在新的微服务中能安全地引入任何变更,包含灰度上线、金丝雀测试等等 

  • 如何将流量路由到新的服务中去,以保证启用/终止任何新的特性或更改都不会出现问题 

  • 如何面对许多棘手的数据集成挑战


一、技术层面


以下这些技术在我们的实践过程中将具备一定的指导作用:

• 开发人员服务框架(Spring Boot [1]WildFly [2]WildFly Swarm [3]) 

• API设计(APICur.io [4]

• 数据框架(Spring Boot Teiid [5]Debezium.io [6]) 

• 集成工具(Apache Camel [7]) 

• Service Mesh(Istio Service Mesh [8]) 

• 数据库迁移工具(Liquibase [9]) 

• 灰度上线/特性标记框架(FF4J [10]) 

• 部署/CI-CD平台(Kubernetes [11]/OpenShift [12]) 

• Kubernetes开发工具(Fabric8.io [13]) 

• 测试工具(Arquillian [14]Pact [15]/Arquillian Algeron [16]Hoverfly [17]Spring-Boot Test [18]RestAssured [19]Arquillian Cube [20]


我使用的是http://developers.redhat.com上的TicketMonster教程,显示从单体应用到微服务的演变,如果感兴趣的话可以关注,你还可以在github上找到相关的代码和文档(文档还在编写中):https://github.com/ticket-monster-msa/monolith


让我们一步步地读完第一部分 [21],具体来看看每一步应该怎么实施。中间还会引入上一部分中出现的一些注意事项,并在当前背景下再讨论一遍。


二、了解单体式应用


回顾下注意事项:

  • 单体式应用(代码和数据库模型)很难变更 

  • 变更需要整体重新部署和团队间高度的协调 

  • 需要进行大量测试来做回归分析 

  • 需要一个全自动的部署方式


可以的话,尽可能为单体应用安排大量的测试,哪怕不是一直有效。随着演变的开始,无论是添加新功能还是替换现有功能,我们都需要清楚了解任何更改可能产生的影响。Michael Feathers 在他《重构遗留代码》[22]的书中,将“遗留代码(legacy code)”定义为没有被测试所覆盖的代码。像JUnit和Arquillian这样的工具就很能帮到大忙。使用Arquillian,可以任意选择远程方法调用的接口的颗粒大小(fine grain or coarse grain),然后打包应用程序,不过仍需要用适当的模拟等方式,来运行打算被测试的一部分程序。例如,在单体应用(TicketMonster)中,我们可以定义一个微部署(micro-deployment),用来将原有的数据库替换为内存数据库,并预加载一些样例数据。Arquillian适用于Spring Boot应用、Java EE等。在本例中,我们将测试一个Java EE的单体架构:

public static WebArchive deployment() {  return ShrinkWrap    .create(WebArchive.class, "test.war")    .addPackage(Resources.class.getPackage())    .addAsResource("META-INF/test-persistence.xml", "META-INF/persistence.xml")    .addAsResource("import.sql")    .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")    // Deploy our test datasource    .addAsWebInfResource("test-ds.xml"); }

更有意思的是,嵌入在运行环境中的测试可以用来验证内部工作的所有组件。例如,在上面的一个测试中,我们可以将BookingService注入到测试中,并直接运行:

@RunWith(Arquillian.class) public class BookingServiceTest {    @Deployment    public static WebArchive deployment() {        return RESTDeployment.deployment();    }    @Inject    private BookingService bookingService;    @Inject    private ShowService showService;    @Test    @InSequence(1)    public void testCreateBookings() {        BookingRequest br = createBookingRequest(1l, 0, new int[]{4, 1}, new int[]{1,1}, new int[]{3,1});        bookingService.createBooking(br);        BookingRequest br2 = createBookingRequest(2l, 1, new int[]{6,1}, new int[]{8,2}, new int[]{10,2});        bookingService.createBooking(br2);        BookingRequest br3 = createBookingRequest(3l, 0, new int[]{4,1}, new int[]{2,1});        bookingService.createBooking(br3);    }

完整的示例请参阅TicketMonster单体应用模块[23]中的BookingServiceTest。


测试的问题解决了,那么部署呢?


Kubernetes已成为容器化服务或应用程序的实际部署平台。Kubernetes处理诸如健康度检查、扩展、重启、负载平衡等事项。对于Java开发人员来说,像fabric8-maven-plugin[24]这样的工具甚至都可以用来自动构建容器或docker镜像,并生成任意部署资源文件。OpenShift[25]是Red Hat的Kubernetes的产品化版本,其中增加了开发人员的功能,包括CI/CD pipelines等。



无论是微服务、单体应用还是其他平台(比如能够处理持续的工作负载,即数据库等),Kubernetes/OpenShift都是一个适用于应用程序/服务的部署平台。通过Arquillian,容器和OpenShift pipelines,可以持续地将变更引入生产环境。顺便来看一下openshift.io[26],它将开发经验与自动CI/CD pipelines、SCM集成、Eclipse Che[27]开发人员工作区、库扫描等结合在一起。


目前,生产负载指向单体应用。如果我们翻到它的主页,我们会看到这样的内容:



接下来,让我们开始做一些改变…

三、提取用户界面UI


回顾下注意事项:

  • 一开始,先不要变更单体式应用;只需将UI复制粘贴到单独的组件即可 

  • 在UI和单体式应用间需要有一个合适的远程API—但并非所有情况下都需要 

  • 增加一个安全层 

  • 需要用某种方法以受控的方式将流量路由或分离到新的UI或单体式应用,以支持灰度上线(dark launch)/金丝雀测试(canary)/滚动发布(rolling release[28]


如果我们看下TicketMonster UI v1 [29]代码,就会发现它非常简单。静态HTML/JS/CSS组件已经被移到它自己的Web服务器,还被打包到一个容器中。通过这种方式,我们可以在单体应用之外对它进行单独部署,并独立更改或更新版本。这个UI项目仍然需要与单体应用对话来执行它的功能,所以应该是公开一个REST接口,让UI可以与之交互。对于一些单体应用来说,这说起来容易做起来难。如果你想从遗留代码中打包出来一个不错的REST API,又遇到了挑战,我强烈推荐你看看Apache Camel,尤其是它的REST DSL。


比较有意思的是,实际上单体应用并没有被改变。它的代码没有变动,同时新UI也部署完成。如果查看Kubernetes,我们会看到两个单独的部署对象和两个单独的pod:一个用于单体架构,另一个用于UI。



即使tm-ui-v1用户界面部署完了,也没有任何流量进入这个新的TicketMonster UI组件。为了简单起见,即使这个部署并没有承载生产流量,而是ticket-monster这个单体应用在承担所有流量,我们仍然可以把它当作一个简单的灰度上线。相关的UI端口仍旧可以访问:


接下来,用kubectl cli 工具从本地端口转发到特定的pod(端口80上的tm-ui-v1-3105082891-gh31x),并将其映射到本地端口8080。现在,如果导航到http://localhost:8080,应该得到一个新版本UI(注意突出显示的文本部分,表明这是一个不同的UI,但它直接指向单体应用)



如果我们这个新版本还算满意,就可以开始将流量引入进来。为此,我们将使用Istio service mesh [30]。Istio是用于管理由入口点和服务代理组成的网格控制层(control plane)。我已经写了一些关于像Envoy这样的数据层[31]以及service mesh[32]的文章。我个人强烈建议看看Istio的全部功能。接下来的几段内容,我们会围绕整个项目的全过程来依次展开讨论Istio的各项功能。如果控制层和数据层之间的区分让你困惑,请查看Matt Klein[33]撰写的博客。


我们将从使用Istio Ingress Controller[34]开始。该组件允许使用Kubernetes Ingress规范来控制流量进入Kubernetes集群。一旦安装了Istio,我们可以这样创建一个入口资源,将流量指向Ticket Monster UI的Kubernetes服务,tm-ui:


apiVersion: extensions/v1beta1 kind: Ingress metadata:  name: tm-gateway  annotations:    kubernetes.io/ingress.class: "istio" spec:  backend:    serviceName: tm-ui    servicePort: 80

一旦有了入口,就可以开始应用Istio路由规则[35]。例如,有一个规则,“任何时候有人试图与在Kubernetes中运行的tm-ui服务对话,将它们指向服务的第一版本v1”:


apiVersion: config.istio.io/v1alpha2 kind: RouteRule metadata:  name: tm-ui-default spec:  destination:    name: tm-ui  precedence: 1  route:  - labels:      version: v1

如此,我们能够更好地控制进入集群甚至深入集群内部的流量。在这个步骤的最后,我们会将所有的流量都转到tm-ui-v1部署。


四、从单体架构移除UI


回顾下注意事项

  • 从单体式应用中移除UI组件 

  • 需要对单体式应用进行最小的变更(弃用/删除/禁用UI) 

  • 不停机的前提下,再次使用受控的路由/整流方法来引入这种变更


这一步相当直接,通过删除静态UI组件来更新单体应用(删除的部分已经转移到了tm-ui-v1部署)。既然应用程序已经被释放成为一个单体应用的服务,以供UI,API或者其他一些程序调用,那么也可以对这个部署进行一些API层级的更改。而如果想对API进行一些更改,就需要部署一个新版本的UI。此处我们部署了backend-v1服务以及一个新的UI tm-ui-v2,可以利用后端服务中的这个新API。


来看看在Kubernetes集群中的部署情况:


此时,ticket-monster和tm-ui-v1正接收实时流量。backend-v1和指向它的UI--tm-ui-v2则没有流量负载。需要注意的一点是,backend-v1部署与ticket-monster部署共享数据库,但各自有略微不同的外向API(outward facing API)。


现在,新的backend-v1和tm-ui-v2组件已经部署到生产环境中。现在是时候把注意力放在一个简单而又重要的事实上:生产环境部署发生了改变,但是它们还没有发布。在turblabs.io [36]一些优秀的博客更详细地阐述了这一点[37]。现在,我们有机会部署一个非正式的灰度发布。也许我们希望这个部署慢慢来,首先面向内部用户,或者先对某个特定区域内,特定设备的部分用户进行部署等等。



既然已经有了Istio,接下来看看它能做些什么。我们只想为内部用户做一个灰度发布。我们可以用各种方式来识别内部用户,诸如headers、IP等等,在本例中,如果HTTP header带有 x-dark-launch: v2 这样的文本内容,则该请求将会被路由到新的backend-v1和tm -ui-v2服务中。以下是istio路由规则的样子:


apiVersion: config.istio.io/v1alpha2 kind: RouteRule metadata:  name: tm-ui-v2-dark-launch spec:  destination:    name: tm-ui  precedence: 10  match:    request:      headers:        x-dark-launch:          exact: "v2"  route:  - labels:      version: v2

任意用户身份登录主页时,应该可以看到当前的部署(即指向ticket-monster单体应用的tm-ui-v1):

现在,如果改变浏览器中的消息头(例如使用Firefox的修改消息头工具或其他类似工具),我们应该被路由到已灰度上线的服务(指向backend-v1的tm-ui-v2):



然后点击“开始”开始修改消息头并刷新页面:



现在,我们已经被重定向到服务的灰度发布版本。由此,可以通过做一个金丝雀发布(这里也许引1%的实时流量到新部署),来向客户群发布,同时,如果没有负面效果的话,那么就缓慢增加流量负载(5%、10%、50%等)。以下是Istio路由规则的一个例子,其将v2流量以1%进行金丝雀发布:


apiVersion: config.istio.io/v1alpha2 kind: RouteRule metadata:  name: tm-ui-v2-1pct-canary spec:  destination:    name: tm-ui  precedence: 20  route:  - labels:      version: v1    weight: 99  - labels:      version: v2    weight: 1 

能“看到”或“观察”这个版本的影响是至关重要的,稍后我们会进一步讨论。另外请注意,这种金丝雀发布方式目前正在架构外围完成,但是也可以通过istio控制内部服务间通讯/交互时采用金丝雀的方式。在接下来的几个步骤中,我们将开始看到。


五、引入新服务


回顾下注意事项

  • 我们要关注被抽取的服务的API设计或边界 

  • 可能需要重写单体式应用中的某些内容 

  • 在确定API后,将为该服务实施一个简单的脚手架或者place holder 

  • 新的Orders服务将拥有自己的数据库 

  • 新Orders服务目前不会承担任何流量


在这一步中,我们开始设计我们所设想的新订单服务的API,在做一些领域驱动设计练习时,我们常常需要确定一些边界(boundaries),新的API应该更多的与这种边界相一致。这里可以使用API建模工具来设计API,部署一个虚拟化的实施,并且随服务消费者的需求变化 一起迭代,而不是一开始花费大量的精力去构建,最后又发现需要不断修改。


在TicketMonster重构时,需要在单体应用中保留一个上文所说的API,以便在最初的服务拆分时尽可能轻松并且降低风险。无论是哪种情况,有两个给力的工具可以帮到我们:一个是网页式的API设计器,apicur.io[38],一个是测试/ API虚拟化工具,Hoverfly[39]。Hoverlfy是模拟API或捕获现有API流量的好工具,可以用来模拟mock端点。


如果我们正在构建一个新的API,或在使用领域驱动设计方法后,想看看API什么样,可以使用apicur.io工具建立一个Swagger/Open API的规范。



在TicketMonster这个例子中,我们通过在代理模式下启动hoverfly,并使用hoverfly捕获从应用程序到后端服务的流量。我们可以在浏览器设置中设置HTTP代理,从而通过hoverfly发送所有流量。这将把每个请求/响应对(request/response pair)的仿真存储在JSON文件中。这样我们就可以在Mock里使用这些请求/响应对,或者更进一步,用它们开始编写测试,以规范具体的实现代码中的一些行为。


对于所关注的请求或响应对(response pairs),我们可以生成一个JSON架构并用于测试中,参见https://jsonschema.net/#/editor


例如,结合使用Rest Assured和Hoverfly,可以调用hoverfly模拟,并确定该响应符合我们预期的JSON架构:


@Test public void testRestEventsSimulation(){    get("/rest/events").then().assertThat().body(matchesJsonSchemaInClasspath("json-schema/rest-events.json")); } 

在新的订单服务中,可以查看HoverflyTest.java [40]测试。有关测试Java微服务的更多信息,请查阅Manning这本给力的书,《测试Java微服务》[41],我的一些同事Alex Soto Bueno[42]Jason Porter[43]Andy Gumbrecht[44]也参与了这本书的撰写。


由于这篇博文已经很长了,我决定将最后的部分单独写成本主题的第三部分,其中将涉及在单体应用和微服务之间管理数据、服务消费的契约测试(consumer contract testing), 功能发布控制( feature flagging),甚至更复杂的istio路由等内容。本系列的第四部分将展示一个包含上述内容的实操Demo,使用负载仿真测试(load simulation tests)和故障注入(fault injections)。欢迎访问我的网站 [45]和关注我的Twitter [46]。


原文链接:http://blog.christianposta.com/microservices/low-risk-monolith-to-microservice-evolution-part-ii/


参考地址:

[0] http://blog.christianposta.com/microservices/when-not-to-do-microservices/

[1] https://projects.spring.io/spring-boot/

[2] http://wildfly.org/

[3] http://wildfly-swarm.io/

[4] http://www.apicur.io/

[5] https://github.com/teiid/teiid-spring-boot

[6] http://debezium.io/

[7] http://camel.apache.org/

[8] https://istio.io/

[9] http://www.liquibase.org/

[10] https://ff4j.org/

[11] https://kubernetes.io/

[12] https://www.openshift.org/

[13] https://fabric8.io/

[14] http://arquillian.org/

[15] https://github.com/pact-foundation/pact-specification

[16] http://arquillian.org/arquillian-algeron/

[17] https://hoverfly.io/

[18] https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html

[19] http://rest-assured.io/

[20] http://arquillian.org/arquillian-cube/

[21] http://blog.christianposta.com/microservices/low-risk-monolith-to-microservice-evolution/

[22] https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

[23] https://github.com/ticket-monster-msa/monolith/blob/master/monolith/src/test/java/org/jboss/examples/ticketmonster/test/rest/BookingServiceTest.java

[24] https://maven.fabric8.io/

[25] https://www.openshift.com/

[26] https://openshift.io/

[27] https://www.eclipse.org/che/

[28] http://blog.christianposta.com/deploy/blue-green-deployments-a-b-testing-and-canary-releases/

[29] https://github.com/ticket-monster-msa/monolith/tree/master/tm-ui-v1

[30] https://istio.io/

[31] http://blog.christianposta.com/microservices/00-microservices-patterns-with-envoy-proxy-series/

[32] http://blog.christianposta.com/microservices/application-network-functions-with-esbs-api-management-and-now-service-mesh/

[33] https://medium.com/@mattklein123/service-mesh-data-plane-vs-control-plane-2774e720f7fc

[34] https://istio.io/docs/tasks/traffic-management/ingress.html

[35] https://istio.io/docs/reference/config/traffic-rules/routing-rules.html

[36] https://www.turbinelabs.io/

[37] https://blog.turbinelabs.io/deploy-not-equal-release-part-one-4724bc1e726b

[38] http://www.apicur.io/

[39] https://hoverfly.io/

[40] https://github.com/ticket-monster-msa/monolith/blob/master/orders-service/src/test/java/org/ticketmonster/orders/HoverflyTest.java

[41] https://www.manning.com/books/testing-java-microservices

[42] https://twitter.com/alexsotob

[43] https://twitter.com/lightguardjp

[44] https://twitter.com/andygeede?lang=en

[45] http://blog.christianposta.com/

[46] https://twitter.com/christianposta


关于EAWorld微服务,DevOps,数据治理,移动架构原创技术分享,长按二维码关注

COMMENTS

需要 后方可回复
如果没有账号可以 一个帐号。