写点什么

爱奇艺基于 SpringCloud 的韧性能力建设

  • 2021 年 11 月 12 日
  • 本文字数:3189 字

    阅读完需:约 10 分钟

国际站后端业务不断扩展,支撑的服务实例规模也越来越大。并且在此过程中,支持了双云及多地部署。

这也给服务治理带来了挑战,如何应对同城多机房路由、多地容灾等场景,并解决微服务优雅上下线等问题,是国际站业务拓展亟需解决的课题。

本文将从设计、开发、实践各个维度,叙述爱奇艺解决上述问题的思路和实践。

一、SpringCloud 客户端路由


客户端就近路由


什么是就近路由?在业务多机房部署场景中,内部服务如果存在大量的跨机房、甚至跨地域的网络调用,则请求时延会显著加大,会直接影响到服务质量,甚至是用户体验。


但是在一般的微服务架构中,路由策略并不支持这种机房或者地区的多级别判断。所以实现自定义的客户端负载均衡路由变得迫在眉睫。


在常规单机房部署的时候,如下图中的 dc1 中,consumer 只会请求到 provider1,整个链路并没有什么问题。但是当需要多机房多区域部署的时候,瓶颈就出现了。假设有下图的简单部署场景:


  1. provider 分别部署在同 zone 的 dc1、同 region 的 dc2 和不同 region 的 dc3

  2. 各自注册到所在 dc 的注册中心


可以预想到会有以下几个问题:题:


  1. 如果 consumer 能从注册中心获取到所有 provider 列表,那么它会轮询请求,这样正常情况下就会跨机房访问

  2. 如果 consumer 不能从注册中心获取到 provider2 和 provider3,那么在容灾情况下,provider1 挂了,不能故障转移到 provider1 和 provider2


这里就有了智能路由的概念,也就是就近路由,如何满足需求呢?需要做到以下几点:


  1. 各 consumer 能获取到其他 dc 的实例列表,也就是注册中心需要支持多 dc

  2. 正常情况下,consumer 的流量只会请求到同 dc 的 provider1(通道 1),而不会跨机房访问

  3. 当同 dc 的 provider 出现不可用情况下,会首先降级到不同 dc 但是同 region 的 provider2(通道 2),如果 provider2 也不可用,才会降级到不同 region 的 provider3(通道 3)

在讨论实现之前,先同步一下我们后面要用到的 idc、zone 及 region 的概念。


  • idc、zone 及 region


这里先给出 AWS 的 Region 和 AZ 示意图,如下:



  1. AZ – Availability Zone 内部保证<1ms,一个机房或多个机房组成

  2. Region 内部之间保证 2-5ms 时延,多个 AZ 组成

  3. 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 就近路由的扩展经验,设计了以下功能改动:


可以看到,大体分为以下几步:


  1. provider 注册时,调用服务获取自身所在的 zone 和 region,并且向注册中心注册时携带 zone 和 region 信息

  2. consumer 启动时也调用服务获取自身所在的 zone 和 region

  3. consumer 在拉取实例时,首先筛选同 zone 的实例

  4. 如果同 zone 实例中健康比例大于 50%,则进行负载均衡策略选择一台实例

  5. 如果同 zone 中健康比例小于 50%,则降级到同 region 中进行判断,逻辑同上

  6. 最后如果还未筛选出足够数量的实例,则降级到返回所有实例。然后进行负载均衡策略选择

经过路由策略改造后,客户端负载均衡具备了就近路由的功能,基本具备容灾降级的能力。


  • 国际站落地案例


国际站在爱奇艺海外机房接入自定义扩展的 spring-cloud-iqiyi 后。把服务进行部署演练大致情况如下:



  • 期望结果


  1. 正常三个 dc 都健康时,流量是通道 1

  2. 当 dc1 的服务被摘除,流量是通道 2

  3. 当 dc1 和 dc2 的服务都被摘除,流量是通道 3


  • 演练步骤


  1. 使用 Hoxton.SR11-iqiyi-0.1.1 版本的 spring-cloud-iqiyi

  2. 针对 JAVA 类型应用,对容器进行杀死演练


  • 结果展示


从演练结果看,符合预期。进行了常规情况下就近路由,异常情况下的智能路由


二、SpringCloud 优雅上下线


在部署和实践 SpringCloud 服务过程中,发现在服务部署过程中,总有接口超时或者接口 5xx 的情况。分析后发现原因有以下两点:


  1. 新启动的实例没有进行预热或者预热没有执行完,流量就进入,导致接口请求超时

  2. 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 开始提供了官方的优雅停机方案,那我们首先来看下需要怎么使用呢?首先需要在配置文件中配置优雅停机,如下:


server:  shutdown: graceful   ## 开启优雅停机spring:  lifecycle:    timeout-per-shutdown-phase: 5s    ## 优雅停机等待时间,默认30s
复制代码


而在 Spring Boot 2.3 以前,是没有官方方案的,需要自己实现 shutdownhook,具体参考官方的 issue。大体步骤就是判断是否为 tomcat 的线程,如果是则等待线程状态完成再关闭。


所以,一般建议直接升级到 Spring Boot 2.3,使用新特性。

三、成果


经过一系列的自定义扩展,SpringCloud 已完善大多比较重要的功能,基于现有扩展功能,国际站完成部署两地三中心架构:


后端服务整体稳定性得到大大提升,并且具备很强的容灾能力。


还有一些比如标签路由、灰度部署等扩展功能,也亟待开发解决。未来,我们也计划将这些扩展开源贡献给 SpringCloud 社区,共同进步!

用户头像

科技赋能娱乐,“码”出快乐生活 2020.02.13 加入

爱奇艺技术产品团队秉持高效、开放、创新的理念,分享前沿技术,传达爱奇艺生态理念及技术进展。

评论

发布
暂无评论
爱奇艺基于SpringCloud的韧性能力建设