SpringCloud 服务的平滑上下线

吐槽
以前都是手撸 RPC,最近接触 SpringCloud,深感痛心。主要有以下几点:
1)代码量巨大,找 BUG 时间长,超级复杂的设计
2)版本管理混乱,经常出现莫名其妙的配置错误(所以 2.0 是打死不敢上生产啊)
3)Netflix 公司的有些代码,实在是让人费解,根本就不考虑扩展性
4)生态链庞大,学习成本大

建议准备上微服务的同学,固定下一个版本,不要随意更新或降级。拿 tomcat 的 basedir来说, 1.5.8到 1.5.13到 1.5.16版本是换来换去,不小心点会出事故的。

server:
 port: 21004
 context-path: /
 tomcat:
   basedir: file:.

如上, basedir先是从 .换到 file:.,又从 file:.换成 .,连兼容代码都木有。有木有想打死工程师?

前言
今天主要谈的话题,是 平滑的上下线功能。所谓平滑,指的是发版无感知,不至于等到夜深人静的时候偷偷去搞。某些请求时间可以长点,但不能失败,尤其是对支付来说,想花钱花不出去是很让人苦恼的;花了钱买不到东西是很让人恼火的。整体来说,SpringCloud 功能齐全,经过一段时间的踩坑后使用起来还是非常舒服的。

我们的微服务,大体集成了以下内容。
SpringCloud 服务的平滑上下线

嗯,一个庞大的生态

问题
那么问题来了,SpringCloud 到注册中心的注册是通过 Rest接口调用的。它不能像 ZooKeeper那样,有问题节点反馈及时生效。也不能像 Redis那么快的去轮训,太娇贵怕轮坏了。如下图:

SpringCloud 服务的平滑上下线

有三个要求:

1)ServiceA 下线一台实例后,Zuul 网关的调用不能失败
2)ServiceB 下线一台实例后,ServiceA 的 Feign 调用不能失败
3)服务上线下线,Eureka 服务能够快速感知

说白了就一件事,怎样尽量缩短服务下线后 Zuul 和其他被依赖服务的发现时间,并在这段时间内保证请求不失败。

解决时间问题
影响因子
1) Eureka 的两层缓存问题 (这是什么鬼
EurekaServer 默认有两个缓存,一个是 ReadWriteMap,另一个是 ReadOnlyMap。有服务提供者注册服务或者维持心跳时时,会修改 ReadWriteMap。当有服务调用者查询服务实例列表时,默认会从 ReadOnlyMap 读取(这个在原生 Eureka 可以配置,SpringCloud Eureka 中不能配置,一定会启用 ReadOnlyMap 读取),这样可以减少 ReadWriteMap 读写锁的争用,增大吞吐量。EurekaServer 定时把数据从 ReadWriteMap 更新到 ReadOnlyMap 中
2) 心跳时间
服务提供者注册服务后,会定时心跳。这个根据服务提供者的 Eureka 配置中的服务刷新时间决定。还有个配置是服务过期时间,这个配置在服务提供者配置但是在 EurekaServer 使用了,但是默认配置 EurekaServer 不会启用这个字段。需要配置好 EurekaServer 的扫描失效时间,才会启用 EurekaServer 的主动失效机制。在这个机制启用下:每个服务提供者会发送自己服务过期时间上去,EurekaServer 会定时检查每个服务过期时间和上次心跳时间,如果在过期时间内没有收到过任何一次心跳,同时没有处于保护模式下,则会将这个实例从 ReadWriteMap 中去掉
3)调用者服务从 Eureka 拉列表的轮训间隔
4) Ribbon 缓存
解决方式
1) 禁用 Eureka 的 ReadOnlyMap 缓存 (Eureka 端)


eureka.server.use-read-only-response-cache: false

2) 启用主动失效,并且每次主动失效检测间隔为 3s (Eureka 端)


eureka.server.eviction-interval-timer-in-ms: 3000

像 eureka.server.responseCacheUpdateInvervalMs和 eureka.server.responseCacheAutoExpirationInSeconds在启用了主动失效后其实没什么用了。默认的 180s 真够把人给急疯的。

3) 服务过期时间 (服务提供方)


ureka.instance.lease-expiration-duration-in-seconds: 15

超过这个时间没有接收到心跳 EurekaServer 就会将这个实例剔除。EurekaServer 一定要设置 eureka.server.eviction-interval-timer-in-ms 否则这个配置无效,这个配置一般为服务刷新时间配置的三倍。默认 90s!
4) 服务刷新时间配置,每隔这个时间会主动心跳一次 (服务提供方)


reka.instance.lease-renewal-interval-in-seconds: 5

5) 拉服务列表时间间隔 (客户端)


eureka.client.registryFetchIntervalSeconds: 5

默认 30s

6) ribbon 刷新时间 (客户端)


ribbon.ServerListRefreshInterval: 5000

ribbon 竟然也有缓存,默认 30s

这些超时时间相互影响,竟然三个地方都需要配置,一不小心就会出现服务不下线,服务不上线的囧境。不得不说 SpringCloud 的这套默认参数简直就是在搞笑。

重试
那么一台服务器下线,最长的不可用时间是多少呢?(即请求会落到下线的服务器上,请求失败)。赶的巧的话,这个基本时间就是 eureka.client.registryFetchIntervalSeconds+ribbon.ServerListRefreshInterval, 大约是 8秒的时间。如果算上服务端主动失效的时间,这个时间会增加到 11秒。

如果你只有两个实例,极端情况下服务上线的发现时间也需要 11 秒,那就是 22 秒的时间。

理想情况下,在这 11 秒之间,请求是失败的。假如你的 QPS 是 1000,部署了四个节点,那么在 11 秒中失败的请求数量会是 1000 / 4 * 11 = 2750,这是不可接受的。所以我们要引入重试机制。

SpringCloud 引入重试还是比较简单的。但不是配置一下就可以的,既然用了重试,那么就还需要控制超时。可以按照以下的步骤:
1) 引入 pom (千万别忘了哦)


<dependency>

<groupId>
org.springframework.retry
</groupId>

<artifactId>
spring-retry
</artifactId>

</dependency>

2) 加入配置

ribbon.OkToRetryOnAllOperations:true 
#(是否所有操作都重试,若false则仅get请求重试)
ribbon.MaxAutoRetriesNextServer:3 
#(重试负载均衡其他实例最大重试次数,不含首次实例)
ribbon.MaxAutoRetries:1
#(同一实例最大重试次数,不含首次调用)
ribbon.ReadTimeout:30000
ribbon.ConnectTimeout:3000
ribbon.retryableStatusCodes:404,500,503
#(那些状态进行重试)
spring.cloud.loadbalancer.retry.enable:true
# (重试开关)

发布系统
OK, 机制已经解释清楚,但是实践起来还是很繁杂的,让人焦躁。比如有一个服务有两个实例,我要一台一台的去发布,在发布第二台之前,起码要等上 11 秒。如果手速太快,那就是灾难。所以一个配套的发布系统是必要的。

首先可以通过 rest 请求去请求 Eureka,主动去隔离一台实例,多了这一步,可以减少至少 3 秒服务不可用的时间(还是比较划算的)。

然后通过打包工具打包,推包。依次上线替换。

市面上没有这样的持续集成工具,那么发布系统就需要定制,这也是一部分工作量。

到此,仅仅是解决了 SpringCloud 微服务平滑上下线的功能,至于灰度,又是另外一个话题了。有条件的公司选择自研还是很明智的,不至于将功能拉低到如此的水平。

不过大体不用担心,你的公司能不能活下去,还是一个未知数。Netflix 都忍了,在座的各位能比它强大么?

SpringCloud的存在是视觉污染,而你我的存在是星光点点。

可我的钱包,还是空空的。

要不要帮帮忙?

上一篇:springcloud04 Ribbbon


下一篇:SpringCloud(2)---负载均衡 Ribbon(Greenwich版本)