爱奇艺基于 SpringCloud 的韧性能力建设
国际站后端业务不断扩展,支撑的服务实例规模也越来越大。并且在此过程中,支持了双云及多地部署。
这也给服务治理带来了挑战,如何应对同城多机房路由、多地容灾等场景,并解决微服务优雅上下线等问题,是国际站业务拓展亟需解决的课题。
本文将从设计、开发、实践各个维度,叙述爱奇艺解决上述问题的思路和实践。
一、SpringCloud 客户端路由
客户端就近路由
什么是就近路由?在业务多机房部署场景中,内部服务如果存在大量的跨机房、甚至跨地域的网络调用,则请求时延会显著加大,会直接影响到服务质量,甚至是用户体验。
但是在一般的微服务架构中,路由策略并不支持这种机房或者地区的多级别判断。所以实现自定义的客户端负载均衡路由变得迫在眉睫。
在常规单机房部署的时候,如下图中的 dc1 中,consumer 只会请求到 provider1,整个链路并没有什么问题。但是当需要多机房多区域部署的时候,瓶颈就出现了。假设有下图的简单部署场景:
provider 分别部署在同 zone 的 dc1、同 region 的 dc2 和不同 region 的 dc3
各自注册到所在 dc 的注册中心
可以预想到会有以下几个问题:题:
如果 consumer 能从注册中心获取到所有 provider 列表,那么它会轮询请求,这样正常情况下就会跨机房访问
如果 consumer 不能从注册中心获取到 provider2 和 provider3,那么在容灾情况下,provider1 挂了,不能故障转移到 provider1 和 provider2
这里就有了智能路由的概念,也就是就近路由,如何满足需求呢?需要做到以下几点:
各 consumer 能获取到其他 dc 的实例列表,也就是注册中心需要支持多 dc
正常情况下,consumer 的流量只会请求到同 dc 的 provider1(通道 1),而不会跨机房访问
当同 dc 的 provider 出现不可用情况下,会首先降级到不同 dc 但是同 region 的 provider2(通道 2),如果 provider2 也不可用,才会降级到不同 region 的 provider3(通道 3)
在讨论实现之前,先同步一下我们后面要用到的 idc、zone 及 region 的概念。
idc、zone 及 region
这里先给出 AWS 的 Region 和 AZ 示意图,如下:
AZ – Availability Zone 内部保证<1ms,一个机房或多个机房组成
Region 内部之间保证 2-5ms 时延,多个 AZ 组成
Region 之间通常 20-100ms,取决于物理距离
SpringCloud 现有能力
Netflix Ribbon
Ribbon 是一个为客户端提供负载均衡功能的服务,它内部提供了一个叫做 ILoadBalance 的接口代表负载均衡器的操作,比如有添加服务器操作、选择服务器操作、获取所有的服务器列表、获取可用的服务器列表等等。
以下就是 Ribbon 提供的负载均衡规则列表:
可以看到,Ribbon 是提供了基于 zone 的 ZoneAvoidanceRule,它可以根据 zone 进行服务选择。但是如果有 region 等概念,它就没办法处理了。
Spring Cloud Loadbalancer
Spring Cloud 在新版本中,逐渐抛弃 Netflix 的内容,比如 ribbon。也确实是 ribbon 已经停止更新很久了。重新推出的 Spring Cloud LoadBalancer 只有简单的轮询和随机路由策略。在新的版本中,也在新增按照时间权重等等策略。
它主要是支持了响应式的服务选择,像 ribbon 的服务选择还是同步的,这与 Spring Cloud 在倡导的响应式趋势不符。
可以看到新版本的 Load Balancer 支持的路由功能还很初级,也并不支持微服务高可用方方案下的智能就近路由。
自定义扩展能力
经过对 Spring Cloud 现有能力的调研和期望需求的评估,决定对 Spring Cloud 进行自定义扩展,支持就近路由的功能,并期望在短期内服务公司内部开源,未来贡献给开源社区。
经过和架构组同学的讨论和之前 dubbo 就近路由的扩展经验,设计了以下功能改动:
可以看到,大体分为以下几步:
provider 注册时,调用服务获取自身所在的 zone 和 region,并且向注册中心注册时携带 zone 和 region 信息
consumer 启动时也调用服务获取自身所在的 zone 和 region
consumer 在拉取实例时,首先筛选同 zone 的实例
如果同 zone 实例中健康比例大于 50%,则进行负载均衡策略选择一台实例
如果同 zone 中健康比例小于 50%,则降级到同 region 中进行判断,逻辑同上
最后如果还未筛选出足够数量的实例,则降级到返回所有实例。然后进行负载均衡策略选择
经过路由策略改造后,客户端负载均衡具备了就近路由的功能,基本具备容灾降级的能力。
国际站落地案例
国际站在爱奇艺海外机房接入自定义扩展的 spring-cloud-iqiyi 后。把服务进行部署演练大致情况如下:
期望结果
正常三个 dc 都健康时,流量是通道 1
当 dc1 的服务被摘除,流量是通道 2
当 dc1 和 dc2 的服务都被摘除,流量是通道 3
演练步骤
使用 Hoxton.SR11-iqiyi-0.1.1 版本的 spring-cloud-iqiyi
针对 JAVA 类型应用,对容器进行杀死演练
结果展示
从演练结果看,符合预期。进行了常规情况下就近路由,异常情况下的智能路由
二、SpringCloud 优雅上下线
在部署和实践 SpringCloud 服务过程中,发现在服务部署过程中,总有接口超时或者接口 5xx 的情况。分析后发现原因有以下两点:
新启动的实例没有进行预热或者预热没有执行完,流量就进入,导致接口请求超时
kill 的实例,在退出后,还有 consumer 的流量进入,导致出现接口 5xx
其实这是微服务架构中的常见问题,即如何进行预热以及优雅上下线。具体应该如何处理?
假如有这样的简单的微服务架构:
在微服务架构体系中,理想的优雅上下线过程应该是像下面这样:
provider1 就是对应的优雅下线,provider2 就是对应的优雅上线。而且顺序不能颠倒。
在 Spring Cloud 的体系中,使用 consul、ribbon 等组件下,总结下优雅上下线就是:
SpringCloud 优雅上线
自定义扩展能力
针对 SpringCloud 现有架构,我们在 SpringBoot 启动过程中,改变之前服务注册的时机,延迟注册并保证服务预热。
通过禁用 SpringBoot 原生在 WebServerIntializedEvent 事件监听器中实现的自动注册功能,改为在 ApplicationReadyEvent 事件监听器中实现自定义的自动注册,实现了延迟注册和执行自定义预热逻辑的能力。
当然自定义预热逻辑可以由业务代码控制,可以根据实际项目中的需求,进行本地缓存预热、长连接预热、连接池预热等。
同步执行完预热后,再进行服务注册,注册完成后才会收到 consumer 请求,避免由于冷启动造成的慢请求。
SpringCloud 优雅下线
自定义扩展能力
针对 SpringCloud 现有架构,我们在 SpringBoot 退出过程中,增加自定义逻辑,保证服务下线过程中严格按照上面的流程。
具体如上图所示,在 ContextClosedEvent 事件中,拦截处理。首先执行解注册,这个时候注册中心已经没有当前 provider。
然后等待一段时间(可配置),直到 consumer 的 serverList 更新(ribbon 默认是 30s),再继续执行退出流程。
Spring Boot 优雅退出
上面我们介绍了微服务架构中的优雅上下线,但是在服务本身,也存在优雅停机的问题。
什么叫优雅停机?简单说就是,在对应用进程发送停止指令之后,能保证正在执行的业务操作不受影响。应用接收到停止指令之后的步骤应该是,停止接收访问请求,等待已经接收到的请求处理完成,并能成功返回,这时才真正停止应用。
这种完美的应用停止方式如何实现呢?Java 语言本身是支持优雅停机的,当我们使用 kill PID 的方式结束一个 Java 应用的时候,JVM 会收到一个停止信号,然后执行 shutdownHook 的线程。
Spring Boot 现有能力
SpringBoot 2.3.0 开始提供了官方的优雅停机方案,那我们首先来看下需要怎么使用呢?首先需要在配置文件中配置优雅停机,如下:
而在 Spring Boot 2.3 以前,是没有官方方案的,需要自己实现 shutdownhook,具体参考官方的 issue。大体步骤就是判断是否为 tomcat 的线程,如果是则等待线程状态完成再关闭。
所以,一般建议直接升级到 Spring Boot 2.3,使用新特性。
三、成果
经过一系列的自定义扩展,SpringCloud 已完善大多比较重要的功能,基于现有扩展功能,国际站完成部署两地三中心架构:
后端服务整体稳定性得到大大提升,并且具备很强的容灾能力。
还有一些比如标签路由、灰度部署等扩展功能,也亟待开发解决。未来,我们也计划将这些扩展开源贡献给 SpringCloud 社区,共同进步!
评论