01
客户端就近路由
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类型应用,对容器进行杀死演练
结果展示
从演练结果看,符合预期。进行了常规情况下就近路由,异常情况下的智能路由
02
在部署和实践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开始提供了官方的优雅停机方案,那我们首先来看下需要怎么使用呢?首先需要在配置文件中配置优雅停机,如下:
server:
shutdown: graceful ## 开启优雅停机
spring:
lifecycle:
timeout-per-shutdown-phase: 5s ## 优雅停机等待时间,默认30s
而在Spring Boot 2.3以前,是没有官方方案的,需要自己实现shutdownhook,具体参考官方的issue。大体步骤就是判断是否为tomcat的线程,如果是则等待线程状态完成再关闭。
所以,一般建议直接升级到Spring Boot 2.3,使用新特性。
03
经过一系列的自定义扩展,SpringCloud已完善大多比较重要的功能,基于现有扩展功能,国际站完成部署两地三中心架构:
后端服务整体稳定性得到大大提升,并且具备很强的容灾能力。
还有一些比如标签路由、灰度部署等扩展功能,也亟待开发解决。未来,我们也计划将这些扩展开源贡献给SpringCloud社区,共同进步!
以上文章来源于爱奇艺技术产品团队 ,作者海外后端团队