很多用户量大并发度高的应用系统为了避免发布过程中的流量有损,一般选择在流量较小的半夜发布,虽然这样做有效果,但不可控导致背后的研发运维成本对企业来说是一笔不小的负担。基于此,阿里云微服务引擎 MSE 在应用发布过程中,通过应用下线时进行自适应等待+主动通知,应用上线时就绪检查与微服务生命周期对齐+服务预热等技术手段所提供的微服务应用无损上下线功能,能有效帮助企业规避线上发布所出现的流量资损。
无损上下线功能设计
常见的流量有损现象出现的原因包括但不限于以下几种:
- 服务无法及时下线:服务消费者感知注册中心服务列表存在延时,导致应用下线后在一段时间内服务消费者仍然调用已下线应用造成请求报错。
- 初始化慢:应用刚启动接收线上流量进行资源初始化加载,由于流量太大,初始化过程慢,出现大量请求响应超时、阻塞、资源耗尽从而造成刚启动应用宕机。
- 注册太早:服务存在异步资源加载问题,当服务还未初始化完全就被注册到注册中心,导致调用时资源未加载完毕出现请求响应慢、调用超时报错等现象。
- 发布态与运行态未对齐:使用 Kubernetes 的滚动发布功能进行应用发布,由于Kubernetes 的滚动发布一般关联的就绪检查机制,是通过检查应用特定端口是否启动作为应用就绪的标志来触发下一批次的实例发布,但在微服务应用中只有当应用完成了服务注册才可对外提供服务调用。因此某些情况下会出现新应用还未注册到注册中心,老应用实例就被下线,导致无服务可用。
无损下线
其中的服务无法及时下线问题,如下图 1 所示:
图1. Spring Cloud 应用消费者无法及时感知提供者服务下线
对于 Spring Cloud 应用,当应用的两个实例 A’ 和 A 中的 A 下线时,由于 Spring Cloud 框架为了在可用性和性能方面做平衡,消费者默认是 30s 去注册中心拉取最新的服务列表,因此 A 实例的下线不能被实时感知,此时消费者继续通过本地缓存继续调用 A 就会出现调用已下线实例出现流量有损。
针对该问题,阿里云微服务引擎 MSE 基于 Java Agent 字节码技术设计实现的无损下线功能如下图 2 所示:
图2. 无损下线方案
在该套无损下线方案中,服务提供者应用仅需接入 MSE,相比一般的有损下线。应用在下线前会有一段自适应等待时期,该时期待下线应用会通过主动通知的方式,向其在自适应等待阶段发送了请求的服务消费者发送下线事件,消费者接收到下线事件后会主动拉取注册中心服务实例列表以便实时感知应用下线事件避免调用已下线实例造成应用下线流量有损。
无损上线
延迟加载是软件框架设计中最常见的一种策略,例如在 Spring Cloud 框架中 Ribbon 组件的拉取服务列表初始化时机默认是要等到服务的第 1 次调用时刻,例如下图 3 是 Spring Cloud 应用中第 1 次和第 2 次通过 RestTemplate 调用远程服务的请求耗时情况:
图3. 应用启动资源初始化与正常运行过程中耗时情况对比
由测试结果可见,第一次调用由于进行了一些资源初始化,耗时是正常情况的数倍之多。因此把新应用发布到线上直接处理大流量极易出现大量请求响应慢,资源阻塞,应用实例宕机的现象。针对该类大流量下应用资源初始化慢问题,MSE 提供的小流量预热功能通过调节刚上线应用所分配的流量帮助其在进行充分预热后再处理正常流量从而对新实例进行保护。小流量预热过程如下图 4 所示:
图4. 小流量服务预热过程 QPS 与启动时间关系图
除了针对上述应用第一次调用初始化慢所造成的有损上线问题,MSE 还提供了资源预建连接、延迟注册、确保 Kubernetes 就绪检查通过前完成服务注册和确保 Kubernetes 就绪检查通过前完成服务预热等一整套无损上线手段来满足各类不同应用的无损上线需求,整套方案如图 5 所示:
图5. MSE 无损上线方案
如何使用 MSE 的无损上下线
接下来将演示阿里云微服务引擎 MSE 在应用发布时提供的无损上下线和服务预热能力最佳实践。假设应用的架构由 Zuul 网关以及后端的微服务应用实例(Spring Cloud)构成。具体的后端调用链路有购物车应用 A,交易中心应用 B,库存中心应用 C,这些应用中的服务之间通过 Nacos 注册中心实现服务注册与发现。
前提条件
开启 MSE 微服务治理
- 已创建 Kubernetes 集群,请参见创建 Kubernetes 托管版集群[1]。
- 已开通 MSE 微服务治理专业版,请参见开通 MSE 微服务治理[2]。
准备工作
注意,本实践所使用的 Agent 目前还在灰度中,需要对应用 Agent 进行灰度升级,升级文档:
https://help.aliyun.com/document_detail/392373.html
应用部署在不同的 Region(暂时仅支持国内 Region)请使用对应的 Agent 下载地址:
http://arms-apm-cn-[regionId].oss-cn-[regionId].aliyuncs.com/2.7.1.3-mse-beta/
注意替换地址中的[RegionId],RegionId 是阿里云 RegionId。
例如 Region 北京 Agent 地址为:
http://arms-apm-cn-beijing.oss-cn-beijing.aliyuncs.com/2.7.1.3-mse-beta/
应用部署流量架构图
图6. 演示应用部署架构
流量压力来源
在 spring-cloud-zuul 应用中,如图 6 所示,其分别向 spring-cloud-a 的灰度版本和正常版本以 QPS 为 100 的速率同时进行服务调用。
部署 Demo 应用程序
将下面的内容保存到一个文件中,假设取名为 mse-demo.yaml,并执行 kubectl apply -f mse-demo.yaml 以部署应用到提前创建好的 Kubernetes 集群中(注意因为 demo 中有 CronHPA 任务,所以请先在集群中安装 ack-kubernetes-cronhpa-controller 组件,具体在容器服务-Kubernetes->市场->应用目录中搜索组件在测试集群中进行安装),这里我们将要部署 Zuul,A, B 和 C 三个应用,其中 A、B 两个应用分别部署一个基线版本和一个灰度版本,B 应用的基线版本关闭了无损下线能力,灰度版本开启了无损下线能力。C 应用开启了服务预热能力,其中预热时长为 120 秒。
# Nacos Server --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: nacos-server name: nacos-server spec: replicas: 1 selector: matchLabels: app: nacos-server template: metadata: labels: app: nacos-server spec: containers: - env: - name: MODE value: standalone image: registry.cn-shanghai.aliyuncs.com/yizhan/nacos-server:latest imagePullPolicy: Always name: nacos-server resources: requests: cpu: 250m memory: 512Mi dnsPolicy: ClusterFirst restartPolicy: Always # Nacos Server Service 配置 --- apiVersion: v1 kind: Service metadata: name: nacos-server spec: ports: - port: 8848 protocol: TCP targetPort: 8848 selector: app: nacos-server type: ClusterIP #入口 zuul 应用 --- apiVersion: apps/v1 kind: Deployment metadata: name: spring-cloud-zuul spec: replicas: 1 selector: matchLabels: app: spring-cloud-zuul template: metadata: annotations: msePilotAutoEnable: "on" msePilotCreateAppName: spring-cloud-zuul labels: app: spring-cloud-zuul spec: containers: - env: - name: JAVA_HOME value: /usr/lib/jvm/java-1.8-openjdk/jre - name: LANG value: C.UTF-8 image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-zuul:1.0.1 imagePullPolicy: Always name: spring-cloud-zuul ports: - containerPort: 20000 # A 应用 base 版本,开启按照机器纬度全链路透传 --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: spring-cloud-a name: spring-cloud-a spec: replicas: 2 selector: matchLabels: app: spring-cloud-a template: metadata: annotations: msePilotCreateAppName: spring-cloud-a msePilotAutoEnable: "on" labels: app: spring-cloud-a spec: containers: - env: - name: LANG value: C.UTF-8 - name: JAVA_HOME value: /usr/lib/jvm/java-1.8-openjdk/jre - name: profiler.micro.service.tag.trace.enable value: "true" image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-a:0.1-SNAPSHOT imagePullPolicy: Always name: spring-cloud-a ports: - containerPort: 20001 protocol: TCP resources: requests: cpu: 250m memory: 512Mi livenessProbe: tcpSocket: port: 20001 initialDelaySeconds: 10 periodSeconds: 30 # A 应用 gray 版本,开启按照机器纬度全链路透传 --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: spring-cloud-a-gray name: spring-cloud-a-gray spec: replicas: 2 selector: matchLabels: app: spring-cloud-a-gray strategy: template: metadata: annotations: alicloud.service.tag: gray msePilotCreateAppName: spring-cloud -a msePilotAutoEnable: "on" labels: app: spring-cloud-a-gray spec: containers: - env: - name: LANG value: C.UTF-8 - name: JAVA_HOME value: /usr/lib/jvm/java-1.8-openjdk/jre - name: profiler.micro.service.tag.trace.enable value: "true" image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-a:0.1-SNAPSHOT imagePullPolicy: Always name: spring-cloud-a-gray ports: - containerPort: 20001 protocol: TCP resources: requests: cpu: 250m memory: 512Mi livenessProbe: tcpSocket: port: 20001 initialDelaySeconds: 10 periodSeconds: 30 # B 应用 base 版本,关闭无损下线能力 --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: spring-cloud-b name: spring-cloud-b spec: replicas: 2 selector: matchLabels: app: spring-cloud-b strategy: template: metadata: annotations: msePilotCreateAppName: spring-cloud-b msePilotAutoEnable: "on" labels: app: spring-cloud-b spec: containers: - env: - name: LANG value: C.UTF-8 - name: JAVA_HOME value: /usr/lib/jvm/java-1.8-openjdk/jre - name: micro.service.shutdown.server.enable value: "false" - name: profiler.micro.service.http.server.enable value: "false" image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-b:0.1-SNAPSHOT imagePullPolicy: Always name: spring-cloud-b ports: - containerPort: 8080 protocol: TCP resources: requests: cpu: 250m memory: 512Mi livenessProbe: tcpSocket: port: 20002 initialDelaySeconds: 10 periodSeconds: 30 # B 应用 gray 版本,默认开启无损下线功能 --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: spring-cloud-b-gray name: spring-cloud-b-gray spec: replicas: 2 selector: matchLabels: app: spring-cloud-b-gray template: metadata: annotations: alicloud.service.tag: gray msePilotCreateAppName: spring-cloud-b msePilotAutoEnable: "on" labels: app: spring-cloud-b-gray spec: containers: - env: - name: LANG value: C.UTF-8 - name: JAVA_HOME value: /usr/lib/jvm/java-1.8-openjdk/jre image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-b:0.1-SNAPSHOT imagePullPolicy: Always name: spring-cloud-b-gray ports: - containerPort: 8080 protocol: TCP resources: requests: cpu: 250m memory: 512Mi lifecycle: preStop: exec: command: - /bin/sh - '-c' - >- wget http://127.0.0.1:54199/offline 2>/tmp/null;sleep 30;exit 0 livenessProbe: tcpSocket: port: 20002 initialDelaySeconds: 10 periodSeconds: 30 # C 应用 base 版本 --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: spring-cloud-c name: spring-cloud-c spec: replicas: 2 selector: matchLabels: app: spring-cloud-c template: metadata: annotations: msePilotCreateAppName: spring-cloud-c msePilotAutoEnable: "on" labels: app: spring-cloud-c spec: containers: - env: - name: LANG value: C.UTF-8 - name: JAVA_HOME value: /usr/lib/jvm/java-1.8-openjdk/jre image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-c:0.1-SNAPSHOT imagePullPolicy: Always name: spring-cloud-c ports: - containerPort: 8080 protocol: TCP resources: requests: cpu: 250m memory: 512Mi livenessProbe: tcpSocket: port: 20003 initialDelaySeconds: 10 periodSeconds: 30 #HPA 配置 --- apiVersion: autoscaling.alibabacloud.com/v1beta1 kind: CronHorizontalPodAutoscaler metadata: labels: controller-tools.k8s.io: "1.0" name: spring-cloud-b spec: scaleTargetRef: apiVersion: apps/v1beta2 kind: Deployment name: spring-cloud-b jobs: - name: "scale-down" schedule: "0 0/5 * * * *" targetSize: 1 - name: "scale-up" schedule: "10 0/5 * * * *" targetSize: 2 --- apiVersion: autoscaling.alibabacloud.com/v1beta1 kind: CronHorizontalPodAutoscaler metadata: labels: controller-tools.k8s.io: "1.0" name: spring-cloud-b-gray spec: scaleTargetRef: apiVersion: apps/v1beta2 kind: Deployment name: spring-cloud-b-gray jobs: - name: "scale-down" schedule: "0 0/5 * * * *" targetSize: 1 - name: "scale-up" schedule: "10 0/5 * * * *" targetSize: 2 --- apiVersion: autoscaling.alibabacloud.com/v1beta1 kind: CronHorizontalPodAutoscaler metadata: labels: controller-tools.k8s.io: "1.0" name: spring-cloud-c spec: scaleTargetRef: apiVersion: apps/v1beta2 kind: Deployment name: spring-cloud-c jobs: - name: "scale-down" schedule: "0 2/5 * * * *" targetSize: 1 - name: "scale-up" schedule: "10 2/5 * * * *" targetSize: 2 # zuul 网关开启 SLB 暴露展示页面 --- apiVersion: v1 kind: Service metadata: name: zuul-slb spec: ports: - port: 80 protocol: TCP targetPort: 20000 selector: app: spring-cloud-zuul type: ClusterIP # a 应用暴露 k8s service --- apiVersion: v1 kind: Service metadata: name: spring-cloud-a-base spec: ports: - name: http port: 20001 protocol: TCP targetPort: 20001 selector: app: spring-cloud-a --- apiVersion: v1 kind: Service metadata: name: spring-cloud-a-gray spec: ports: - name: http port: 20001 protocol: TCP targetPort: 20001 selector: app: spring-cloud-a-gray # Nacos Server SLB Service 配置 --- apiVersion: v1 kind: Service metadata: name: nacos-slb spec: ports: - port: 8848 protocol: TCP targetPort: 8848 selector: app: nacos-server type: LoadBalancer
结果验证一:无损下线功能
由于我们对 spring-cloud-b 跟 spring-cloud-b-gray 应用均开启了定时 HPA,模拟每 5 分钟进行一次定时的扩缩容。
登录 MSE 控制台,进入微服务治理中心->应用列表->spring-cloud-a->应用详情,从应用监控曲线,我们可以看到 spring-cloud-a 应用的流量数据:
gray 版本的流量在 pod 扩缩容的过程中请求错误数为 0,无流量损失。未打标的版本由于关闭了无损下线功能,在 pod 扩缩容的过程中有 20 个从 spring-cloud-a 发到 spring-cloud-b 的请求出现报错,发生了请求流量损耗。
结果验证二:服务预热功能
我们在 spring-cloud-c 应用开启了定时 HPA 模拟应用上线过程,每隔 5 分钟做一次伸缩,在扩缩容周期内第 2 分钟第 0 秒缩容到 1 个节点,第 2 分钟第 10 秒扩容到 2 个节点。
在预热应用的消费端 spring-cloud-b 开启服务预热功能。
在预热应用的服务提供端 spring-cloud-c 开启服务预热功能。预热时长配置为 120 秒。
观察节点的流量,发现节点流量缓慢上升。并且能看到节点的预热开始和结束时间,以及相关的事件。
从上图可以看到开启预热功能的应用重启后的流量会随时间缓慢增加,在一些应用启动过程中需要预建连接池和缓存等资源的慢启动场景,开启服务预热能有效保护应用启动过程中缓存资源有序创建保障应用安全启动从而实现应用上线的流量无损。
方案介绍 & 实操
更多方案设计细节,请点击下方链接观看微服务应用如何实现无损上下线主题直播视频回放:
https://yqh.aliyun.com/live/detail/27936
相关链接
[1]创建 Kubernetes 托管版集群https://help.aliyun.com/document_detail/95108.htm#task-skz-qwk-qfb
[2]开通 MSE 微服务治理
https://help.aliyun.com/document_detail/347625.htm#task-2140253