背景介绍

如图1中在基于springcloud的微服务架构中,我们使用eureka作为注册中心,zuul作为网关,用户请求会先进入网关,网关中会通过ribbon组件缓存eureka中注册的服务列表,在

ruoyi 微服务版使用 微服务版本发布_服务列表

         图1: 基于springcloud(eureka作为注册中心,zuul作为网关)的微服务架构图

服务列表中进行选择分发,分发到相应的服务.服务间的调用的逻辑也是类似于zuul网管的分发逻辑.当有服务有新版本或修复服务bug后,需要对原有服务进行重新发布部署,在以往我们使用传统的强制发布去发布服务,传统的发布过程如下图2

ruoyi 微服务版使用 微服务版本发布_spring cloud_02

图2: 传统的服务发布过程

我们这里假设正在发布的是A服务,由于A服务在发布时kill -9 pid没有通知到eureka,euraka服务列表中暂时还会存在这个已经被杀死的服务A,当前的A服务是不可用的,那么当用户请求时如果分发到此服务将不可用,导致用户请求失败,如图3,并且在kill -9 pid结束服务进程时可能存在正在处理的业务或者功能,强制的结束会导致异常中断,脏数据等问题,如图3.

ruoyi 微服务版使用 微服务版本发布_spring cloud_03

图3: A服务使用传统发布过程发布

传统的强制发布过程严重降低了服务的可用性,在生产环境中服务发布过程中对于用户的体验是极差的不能接受的!进而我们设计了一套优雅的发布过程.

优雅发布设计细节

预备知识

Eureka的 RestFul接口:

请求名称

请求方式

HTTP地址

请求描述

查询所有服务

GET

/eureka/apps

HTTP code为200时表示成功,返回XML/JSON数据内容

变更服务状态

PUT

/eureka/apps/{appID}/{instanceID}/status?value=DOWN

服务上线、服务下线等状态变动,HTTP code为200时表示成功

引用: Eureka REST operations · Netflix/eureka Wiki · GitHub

Eureka中服务的状态说明:

状态值

说明

UP

上线服务

DOWN

删除服务

OUT_OF_SERVICE

将某个实例设置为暂停服务,这个和删除(DOWN)不同,如果你手动调用删除,但如果客户端还活着,定时任务还是会将实例注册上去。但是改成这个状态,定时任务更新不了这个状态 

Kill -9 和 kill -15的区别:

9) SIGKILL

用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

15) SIGTERM

程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

优雅发包过程介绍

整个优雅发布的过程,如图4

ruoyi 微服务版使用 微服务版本发布_java_04

图4: 优雅发布的过程

 

 

  1. 调用发布脚本传入参数:服务名(appID),jar包名,java服务占用的端口($port)等
     
  2. 通过jar包名,使用脚本(ps)查询当前服务进程号
     
  3. 判断是否存在服务进程,存在服务进程继续往下走,不存在的话直接跳转到发布新版服务(第10步)
     
  4. 通过脚本查询当前服务器的ip(ip addr)
     
  5. 通过服务名,服务实例id=$ip:$port(这里要求注册到eureka的实例id为$ip+:+$port), 调用eureka服务状态变更接口设置服务实例状态为暂停(OUT_OF_SERVICE)
    说明: 这一步主要是告知eureka当前服务实例需要暂停提供服务,但是由于ribbon的缓存机制,服务间的服务列表缓存是不会及时刷新的,并且这个服务实例内可能还存在未处理完的请求,所以不能立即执行关闭服务(kill -15)操作
     
  6. 调用eureka服务列表接口循环判断服务实例状态是否为暂停(OUT_OF_SERVICE),如果到达暂停服务最大尝试时间(420s),本次发布中断
    说明: 这里是一个check的过程,保证调用的服务状态变更为暂停接口一定成功,这里需要设置一个最大的check时间420s,避免无限循环
     
  7. 确定服务实例状态变更为暂停成功后,等待指定时间(70s),保证剩余web请求处理完
    说明: 这里的70s考虑到ribbon的服务列表缓存刷新时间和最后请求到达的最大处理时间,所以这里的时间大于等于ribbon的服务列表缓存刷新时间(ribbon.ServerListRefreshInterval默认为30秒)+请求最大处理时间(ribbon.ReadTimeout默认值为5秒,我们的服务设置的为30秒).一般来说还需要考虑在ribbon从eureka获取列表所花的时间,所以再在上调10秒.
     
  8. 使用kill -15(SIGTERM) pid关闭服务进程
    说明: 使用kill -15 pid可以使程序进入正常关闭流程,保证java中的关闭钩子正常运行,程序执行收尾操作,避免了强制关闭导致的程序强制中断关闭
     
  9. 循环检查服务进程是否正常退出,如果到达服务正常关闭最大的超时间(120秒)仍然未关闭,使用kill -9 pid强制关闭服务
    说明: 由于kill -15 pid可以被阻塞和处理,要求程序自己正常退出,存在程序退出逻辑堵塞的可能,所以需要增加一个check的机制,设置一个程序正常处理完剩余任务的最大等待时间,这里设置120秒的超时时间,最终通过kill -9 pid保证程序一定被关闭.
  10. 发布新版服务
  11. 服务启动后,还需要循环调用eureka服务列表接口判断服务实例是否已经注册到服务列表中,只有真正注册到eureka服务列表后才能证明服务是真正的启动,才可以被各个服务的ribbon获取到.由于存在服务内部出错导致注册失败的情况,这里也需要设置一个最大的超时时间(我们这里设置的为420秒),避免无限循环检测的问题
    说明: 这里主要是考虑之前设置了服务实例状态为暂停(OUT_OF_SERVICE),有时eureka会遗留这状态,为免这个问题,可以主动调用一次状态变更为UP即可
  12. 保证当前服务发布成功,再发布其他服务器(同时发布)的当前服务

优雅发布过程是如何保证接口可用的

如图5,发布A服务的过程,优先发布’A服务-服务器1’,在优雅发布的第1-7步中服务实例都是可用的,当第7步完成后,所有的服务中(包括zuul网关)的ribbon服务列表缓存中,当前正在发布的实例的状态将会是暂停的,此时新进来的请求将不会再进入当前实例,并且当前实例的请求都应该处理完成并返回了

ruoyi 微服务版使用 微服务版本发布_服务列表_05

图5: A服务-服务器1正在使用优雅发布发布中(完成第7步后)

直到’A服务-服务器1’发布成功,’A服务-服务器1’当前服务变为可用状态,紧接着可以发布服务器2-n的A服务, 服务器2-n发布A服务时在第1-7步中服务实例都是可用的,当第7步完成后,所有的服务中(包括zuul网关)的ribbon服务列表缓存中, 服务器2-n中正在发布的实例的状态将会是暂停的,此时新进来的请求将不会再进入正在发布的实例中,并且正在发布的实例的请求都应该处理完成并返回了,如图6

ruoyi 微服务版使用 微服务版本发布_eureka_06

图6: A服务-服务器2-n正在使用优雅发布发布中(完成第7步后)

直到所有的A服务的服务器都发布完成,新版A服务发布结束,整个过程没有请求的丢失和中断

优雅发布带来的优点和解决的痛点

  1. 保证了在发包过程,接口依然是可用的,避免了发包过程的接口不可用问题
  2. 发布过程先使用kill -15 pid再使用kill -9 pid进行关闭进程保证了java应用的正常关闭,关闭时可以完成收尾操作,避免强制中断执行流程