本文档是设计、构建和部署微服务的四篇系列文章中的第三篇。本系列文章介绍微服务架构的各种元素。该系列介绍了微服务架构模式的优缺点及其应用方式。
- 微服务简介
- 将单体式应用重构为微服务
- 微服务设置中的服务间通信(本文档)
- 微服务应用中的分布式跟踪
本系列文章面向设计和实施迁移以将单体式应用重构为微服务应用的应用开发者和架构师。
本文档介绍了异步消息传递与微服务中的同步 API 之间的权衡。本文档将逐步介绍单体式应用的构建,并介绍如何在基于微服务的新设置中将原始应用中的同步请求转换为异步流。这种转换包括在服务之间实现分布式事务。
示例应用
在本文档中,您将使用名为 Online Boutique 的预构建电子商务应用。应用会实现基本的电子商务流程,例如浏览商品、将商品添加到购物车和结算。该应用还根据用户选择提供建议和广告。
逻辑分离服务
在本文档中,您将付款服务与应用的其余部分隔离开来。原始 Online Boutique 应用中的所有流都是同步的。在重构的应用中,付款流程将转换为异步流程。因此,当您收到购买请求时,您无需立即处理请求,而是向用户提供“收到请求”确认。在后台,系统会向付款服务触发异步请求来处理付款。
在将付款数据和逻辑移至新服务之前,您应先在单体式应用中将付款数据和逻辑与其余部分隔离开来。通过在单体式应用中隔离付款数据和逻辑,如果付款服务边界有误(业务逻辑或数据),您可以更轻松地在同一代码库中重构代码。
本文档中单体式应用的组件已经模块化,因此它们彼此隔离。如果您的应用具有更严格的相互依赖性,您需要隔离业务逻辑并创建单独的类和模块。您还需要将任何数据库依赖项分离到其各自的表中,并创建单独的仓库类。分离数据库依赖项时,拆分表之间可能存在外键关系。但是,在将服务与单体式应用完全分离后,这些依赖项将不再存在,并且该服务只能通过预定义的 API 或 RPC 合同进行互动。
分布式事务和部分失败
将服务与单体式应用隔离开来后,原始单体式系统中的本地事务会分布在多项服务中。在单体式实现中,结账流程遵循下图所示的顺序。
图 1. 单体式实现中的结账流程序列。
在图 1 中,当应用收到采购订单时,结账控制器会调用付款服务和订单服务来处理付款并分别保存订单。如果任何步骤失败,则可以回滚数据库事务。请设想一个场景:订单请求成功存储在订单表中,但付款失败。在这种情况下,系统会回滚整个事务,并从订单表中移除该条目。
将付款分离到其自己的服务中后,修改后的结算流程如下图所示:
图 2:付款流程中的结账流程分离成自己的服务。
在图 2 中,事务现在跨多个服务及其相应的数据库,因此是一个分布式事务。收到订单请求时,结账控制器会将订单详细信息保存在本地数据库中,并调用其他服务以完成订单。这些服务(例如付款服务)可以使用自己的本地数据库来存储订单的详细信息。
在单体式应用中,数据库系统确保本地事务是原子性的。但是,默认情况下,基于微服务的每项服务都有一个单独的数据库,因此没有跨不同数据库的全局事务协调器。由于交易未进行集中协调,因此处理付款失败不会回滚在订单服务中提交的更改。因此,系统处于不一致状态。
以下模式通常用于处理分布式事务:
- 两阶段提交协议 (2PC):作为共识协议系列的一部分,2PC 会协调分布式事务的提交并维护原子性、一致性、隔离性、持久性 (ACID) 保证。该协议分为两个阶段:准备和提交。仅当所有参与者都已经为个事务投票时,才会提交事务。如果参与者未能达成共识,则会回滚整个事务。
- Saga:Saga 模式包括在构成分布式事务的每个微服务中运行本地事务。每个成功或失败的操作结束时都会触发事件。分布式事务所涉及的所有微服务都会订阅这些事件。如果以下微服务收到成功事件,则会执行其操作。如果失败,则上述微服务会完成补偿性操作以撤消更改。Saga 通过保证所有步骤都完成(所有操作都成功或补偿性操作撤消所有工作),提供一致的系统视图。
我们建议使用 Saga 来处理长期事务。在基于微服务的应用中,您期望服务间调用以及与第三方系统的通信。因此,最好针对最终一致性进行设计:重试可恢复的错误,并公开最终修改不可恢复错误的补偿事件。
您可以通过各种方式实现 Saga,例如,您可以使用 Apache Airflow 或 Apache Camel 等任务和工作流引擎。您还可以使用基于 Kafka、RabbitMQ 或 ActiveMQ 的系统编写您自己的事件处理程序。
Online Boutique 应用使用结账服务来编排付款、配送和电子邮件通知服务。结账服务还会处理业务和订单工作流。除了构建您自己的工作流引擎外,您还可以使用 Zeebe 等第三方组件。Zeebe 提供了一个基于界面的 modeler。我们建议您根据应用的要求仔细评估微服务编排器的选择。此选择是运行和扩缩微服务的关键部分。
重构应用
为了在重构应用中启用分布式事务,结账服务会处理付款、配送和电子邮件服务之间的通信。通用的业务流程模型和标记法 (BPMN) 工作流采用以下流程:
图 3.有助于确保典型微服务中的分布式事务的订单工作流。
上图显示了以下工作流:
- 前端服务收到订单请求,然后执行以下操作:
- 将订单商品发送到购物车服务。然后,购物车服务会保存订单详情 (Redis)。
- 重定向至结账页。结账服务会从购物车服务中拉取订单,将订单状态设置为
Pending
,然后要求客户付款。 - 确认用户已付款。确认后,结账服务会告知电子邮件服务生成确认电子邮件并将其发送给客户。
- 付款服务随后会处理请求。
- 如果付款请求成功,付款服务会将订单状态更新为
Complete
。 - 如果付款请求失败,付款服务会发起补偿性事务。
- 付款请求已取消。
- 结账服务会将订单状态更改为
Failed
。
- 如果付款服务不可用,请求会在 N 秒后超时,并且结账服务会发起补偿性事务。
- 结账服务会将订单状态更改为
Failed
。
- 如果付款请求成功,付款服务会将订单状态更新为
目标
- 在 Google Kubernetes Engine (GKE) 上部署大型 Online Boutique 应用。
- 验证单体式结账流程。
- 部署重构单体式应用的微服务版本
- 验证新的结账流程是否正常工作。
- 如果发生故障,请验证分布式事务和补偿操作是否生效。
费用
在本文档中,您将使用 Google Cloud 的以下收费组件:
您可使用价格计算器根据您的预计使用情况来估算费用。
完成本文档后,您可以删除所创建的资源以避免继续计费。如需了解详情,请参阅清理。
准备工作
-
In the Google Cloud console, on the project selector page, select or create a Google Cloud project.
-
Make sure that billing is enabled for your Google Cloud project.
-
In the Google Cloud console, activate Cloud Shell.
At the bottom of the Google Cloud console, a Cloud Shell session starts and displays a command-line prompt. Cloud Shell is a shell environment with the Google Cloud CLI already installed and with values already set for your current project. It can take a few seconds for the session to initialize.
启用适用于 Compute Engine、Google Kubernetes Engine、Cloud SQL、Artifact Analysis 和 Container Registry 的 API:
gcloud services enable \ compute.googleapis.com \ sql-component.googleapis.com \ servicenetworking.googleapis.com\ container.googleapis.com \ containeranalysis.googleapis.com \ containerregistry.googleapis.com \ sqladmin.googleapis.com
导出以下环境变量:
export PROJECT=$(gcloud config get-value project) export CLUSTER=$PROJECT-gke export REGION="us-central1"
部署电子商务单体式应用
在本部分中,您将在 GKE 集群中部署单体式 Online Boutique 应用。该应用使用 Cloud SQL 作为其关系型数据库。下图演示了单体式应用架构:
图 4.客户端连接到 GKE 集群中的应用,应用连接到 Cloud SQL 数据库。
如需部署该应用,请完成以下步骤:
克隆 GitHub 代码库:
git clone https://github.com/GoogleCloudPlatform/monolith-to-microservices-example
替换 Terraform 变量清单文件中的
PROJECT_ID
占位符:cd monolith-to-microservices-example/setup && \ sed -i -e "s/\[PROJECT_ID\]/$PROJECT/g" terraform.tfvars
运行 Terraform 脚本以完成基础架构设置并部署基础架构。如需详细了解 Terraform,请参阅 Terraform on Google Cloud 使用入门:
terraform init && terraform apply -auto-approve
Terraform 脚本会创建以下内容:
- 名为
PROJECT_ID-vpc
的 VPC 网络 - 名为
PROJECT_ID-gke
的 GKE 集群 - 名为
PROJECT_ID-mysql
的 Cloud SQL 实例- 应用使用的名为
ecommerce
的数据库 - 用户
root
,密码设为password
- 应用使用的名为
您可以修改 Terraform 脚本以自动生成密码。此设置使用简化示例,不应在生产环境中使用。
基础架构预配最多可能需要 10 分钟。脚本成功完成后,输出如下所示:
... Apply complete! Resources: 8 added, 0 changed, 0 destroyed. Outputs: kubernetes_cluster_name = PROJECT_ID-gke sql_database_name = PROJECT_ID-mysql vpc_name = PROJECT_ID-vpc
- 名为
连接到集群并创建名为
monolith
的命名空间。您可在 GKE 集群自己的命名空间中部署应用:gcloud container clusters get-credentials $CLUSTER \ --region $REGION \ --project $PROJECT && \ kubectl create ns monolith
在 GKE 上运行的应用使用 Kubernetes Secret 访问 Cloud SQL 数据库。创建一个使用数据库用户凭据的 Secret:
kubectl create secret generic dbsecret \ --from-literal=username=root \ --from-literal=password=password -n monolith
构建单体式应用映像,并将其上传到 Container Registry:
cd ~/monolith gcloud builds submit --tag gcr.io/$PROJECT_ID/ecomm
将
deploy.yaml
文件中的引用更新为新创建的 Docker 映像:cd ~/monolith sed -i -e "s/\[PROJECT_ID\]/$PROJECT/g" deploy.yaml
替换部署清单文件中的占位符,然后部署应用:
cd .. && \ DB_IP=$(gcloud sql instances describe $PROJECT-mysql | grep "ipAddress:" | tail -1 | awk -F ":" '{print $NF}') sed -i -e "s/\[DB_IP\]/$DB_IP/g" monolith/deploy.yaml kubectl apply -f monolith/deploy.yaml
检查部署的状态:
kubectl rollout status deployment/ecomm -n monolith
输出如下所示。
Waiting for deployment "ecomm" rollout to finish: 0 of 1 updated replicas are available... deployment "ecomm" successfully rolled out
获取已部署应用的 IP 地址:
kubectl get svc ecomm -n monolith \ -o jsonpath="{.status.loadBalancer.ingress[*].ip}" -w
等待负载均衡器 IP 地址发布。如需退出命令,请按
Ctrl+C
。请记下负载均衡器 IP 地址,然后通过网址http://IP_ADDRESS
访问应用。负载均衡器可能需要一段时间才能恢复正常并开始传输流量。
验证单体式结账流程
在本部分中,您将创建一个测试订单来验证结账流程。
- 转到您在上一部分记下的网址
http://IP_ADDRESS
。 - 在显示的应用首页上,选择任何产品,然后点击添加到购物车。
- 如需创建测试购买,请点击下单:
- 结算成功后,系统会显示订单确认窗口并显示订单确认 ID。
如需查看订单详情,请连接到数据库:
gcloud sql connect $PROJECT-mysql --user=root
您还可以使用任何其他受支持的方法来连接到数据库。出现提示时,请输入
password
作为密码。如需查看已保存的订单详细信息,请运行以下命令:
select cart_id from ecommerce.cart;
输出如下所示:
+--------------------------------------+ | cart_id | +--------------------------------------+ | 7cb9ab11-d268-477f-bf4d-4913d64c5b27 | +--------------------------------------+
部署基于微服务的电子商务应用
在本部分中,您将部署重构的应用。本文档仅重点介绍分离前端和付款服务。本系列的下一个文档(即微服务应用中的分布式跟踪)介绍了其他服务(例如推荐和广告服务),您可以将其与单体式应用。结账服务会处理前端服务和付款服务之间的分布式事务,并将其部署为 GKE 集群中的 Kubernetes 服务,如下图所示:
图 5. 结账服务编排购物车、付款和电子邮件服务之间的事务。
部署微服务
在本部分中,您将使用之前预配的基础架构在其自身的命名空间 microservice
中部署微服务:
确保您满足以下要求:
- Google Cloud 项目
- 包含
gcloud
、git
和kubectl
的 Shell 环境
在 Cloud Shell 中,克隆微服务代码库:
git clone https://github.com/GoogleCloudPlatform/microservices-demo cd microservices-demo/
设置 Google Cloud 项目和区域,并确保已启用 GKE API:
export PROJECT_ID=PROJECT_ID export REGION=us-central1 gcloud services enable container.googleapis.com \ --project=${PROJECT_ID}
将
PROJECT_ID
替换为您的 Google Cloud 项目的 ID。创建 GKE 集群并获取其凭据:
gcloud container clusters create-auto online-boutique \ --project=${PROJECT_ID} --region=${REGION}
创建集群可能需要几分钟的时间。
将微服务部署到集群:
kubectl apply -f ./release/kubernetes-manifests.yaml
等待 Pod 准备就绪:
kubectl get pods
几分钟后,您会看到 Pod 处于
Running
状态。使用前端的外部 IP 地址在浏览器中访问 Web 前端:
kubectl get service frontend-external | awk '{print $4}'
在网络浏览器中访问
http://EXTERNAL_IP
以访问 Online Boutique 实例。
验证新的结账流程
- 如需验证结账流程,请按照前面的验证单体式结账流程部分所述,选择一件商品并下单。
- 完成订单结算后,确认窗口不会显示确认 ID。相反,确认窗口会引导您查看电子邮件,获取确认详细信息。
如需验证订单已收到、付款服务已处理付款以及订单详细信息已更新,请运行以下命令:
kubectl logs -f deploy/checkoutservice --tail=100
输出如下所示:
[...] {"message":"[PlaceOrder] user_id=\"98828e7a-b2b3-47ce-a663-c2b1019774a3\" user_currency=\"CAD\"","severity":"info","timestamp":"2023-08-10T04:19:20.498893921Z"} {"message":"payment went through (transaction_id: f0b4a592-026f-4b4a-9892-ce86d2711aed)","severity":"info","timestamp":"2023-08-10T04:19:20.528338189Z"} {"message":"order confirmation email sent to \"someone@example.com\"","severity":"info","timestamp":"2023-08-10T04:19:20.540275988Z"}
如需退出日志,请按
Ctrl+C
。验证付款是否成功:
kubectl logs -f deploy/paymentservice -n --tail=100
输出如下所示:
[...] {"severity":"info","time":1691641282208,"pid":1,"hostname":"paymentservice-65cc7795f6-r5m8r","name":"paymentservice-charge","message":"Transaction processed: visa ending 0454 Amount: CAD119.30128260"} {"severity":"info","time":1691641300051,"pid":1,"hostname":"paymentservice-65cc7795f6-r5m8r","name":"paymentservice-server","message":"PaymentService#Charge invoked with request {\"amount\":{\"currency_code\":\"USD\",\"units\":\"137\",\"nanos\":850000000},\"credit_card\":{\"credit_card_number\":\"4432-8015-6152-0454\",\"credit_card_cvv\":672,\"credit_card_expiration_year\":2039,\"credit_card_expiration_month\":1}}"}
如需退出日志,请按
Ctrl+C
。验证订单确认电子邮件是否已发送:
kubectl logs -f deploy/emailservice -n --tail=100
输出如下所示:
[...] {"timestamp": 1691642217.5026057, "severity": "INFO", "name": "emailservice-server", "message": "A request to send order confirmation email to kalani@examplepetstore.com has been received."}
每个微服务的日志消息表明结账、付款和电子邮件服务之间分布式事务已成功完成。
验证分布式事务中的薪酬操作
本节模拟客户下单并且付款服务关闭的情况。
如需模拟服务不可用,请删除付款部署和服务:
kubectl delete deploy paymentservice && \ kubectl delete svc paymentservice
再次访问应用并完成结算流程。在此示例中,如果付款服务未响应,则请求会超时并触发补偿操作。
在界面前端,点击下单按钮。输出类似以下内容:
HTTP Status: 500 Internal Server Error rpc error: code = Internal desc = failed to charge card: could not charge the card: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp: lookup paymentservice on 34.118.224.10:53: no such host" failed to complete the order main.(*frontendServer).placeOrderHandler /src/handlers.go:360
查看前端服务日志:
kubectl logs -f deploy/frontend --tail=100
输出类似以下内容:
[...] {"error":"failed to complete the order: rpc error: code = Internal desc = failed to charge card: could not charge the card: rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp: lookup paymentservice on 34.118.224.10:53: no such host\"","http.req.id":"0a4cb058-ee9b-470a-9bb1-3a965636022e","http.req.method":"POST","http.req.path":"/cart/checkout","message":"request error","session":"96c94881-a435-4490-9801-c788dc400cc1","severity":"error","timestamp":"2023-08-11T18:25:47.127294259Z"}
查看结账服务日志:
kubectl logs -f deploy/frontend --tail=100
输出类似以下内容:
[...] {"message":"[PlaceOrder] user_id=\"96c94881-a435-4490-9801-c788dc400cc1\" user_currency=\"USD\"","severity":"info","timestamp":"2023-08-11T18:25:46.947901041Z"} {"message":"[PlaceOrder] user_id=\"96c94881-a435-4490-9801-c788dc400cc1\" user_currency=\"USD\"","severity":"info","timestamp":"2023-08-11T19:54:21.796343643Z"}
请注意,后续不会调用电子邮件服务来发送通知。不存在
payment went through (transaction_id: 06f0083f-fa47-4d91-8258-6d61edfab1ca)
等事务日志。查看电子邮件服务日志:
kubectl logs -f deploy/emailservice --tail=100
请注意,系统不会为电子邮件服务上的失败事务创建任何日志条目。
作为编排器,如果服务调用失败,结账服务会返回错误状态并退出结账流程。
清理
如果您计划完成本系列文章下一个文档(微服务应用中的分布式跟踪)中的步骤,则可以重复使用项目和资源,而无需将其删除。
删除项目
- In the Google Cloud console, go to the Manage resources page.
- In the project list, select the project that you want to delete, and then click Delete.
- In the dialog, type the project ID, and then click Shut down to delete the project.
删除资源
如果您希望保留本文档中使用的 Google Cloud 项目,请删除各个资源:
在 Cloud Shell 中,运行以下命令:
cd setup && terraform destroy -auto-approve
如需使用 Google Cloud CLI 删除微服务集群,请运行以下命令:
gcloud container clusters delete online-boutique \ --location $REGION
后续步骤
- 详细了解微服务架构。
- 阅读本系列文章的第一篇,了解微服务、其优势、挑战和使用场景。
- 阅读本系列文章的第二篇,了解用于分解微服务的应用重构策略。
- 阅读本系列文章的最后一篇,了解微服务之间的分布式跟踪。