无状态java服务在k8s下流量无缝切换
java在k8s下流量无缝切换
背景
为了服务愉快的上线(其实就是不想每次发布都通知一遍相关人员,社恐瑟瑟发抖),所以我们需要服务能够无感知替换(没有流量遇到因为服务替换导致的失败)。
而通常的java服务,因为需要准备大量资源,导致启动时间通常比较久(普遍1分钟,慢的3,5分钟也是常见),而且有时候需要预热,避免短时间流量冲击造成服务down,等等。
由此引出待解决的问题清单。
问题清单
- 留给服务足够的启动和准备时间
- 流量无缝切换
- 预热
方案
- 利用k8s提供的功能实现服务的流量切换
- 类似golang的流量无缝切换(与netty的处理IO的方式很像)
- 热更新nginx配置,实现代理的流量切换
这里主要以k8s背景下的方案作为讲解,因为其通用性好,实现简单。而剩下的方案或多或少需要维护额外的代码,或者另起服务。
k8s的流量切换
k8s提供了健康检查
机制,用来检查pod的状况,以便在出现问题时进行pod的重启,替换等能力。
而健康检查的实现则是利用探针机制,目前k8s提供了3种探针StartupProbe
,ReadinessProbe
,LivenessProbe
,分别应对pod首次启动检查,流量能否切换检查,pod是否存活场景。
而我们就需要利用3种探针来达到对java服务的流量切换。
下面是一个k8s的service的yaml配置文件:
spec:
containers:
name: podName
image: imageUrl
imagePullPolicy: Always
livenessProbe:
httpGet:
path: /health/ping
port: 80
scheme: HTTP
failureThreshold: 3
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 1
readinessProbe:
httpGet:
path: /health/ping
port: 80
scheme: HTTP
failureThreshold: 3
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 1
startupProbe:
httpGet:
path: /health/ping
port: 80
scheme: HTTP
failureThreshold: 30
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
解决问题1-留给服务足够的启动和准备时间
startupProbe:
# 以http get方式去请求特定地址,此处就是 http://domain:80/health/ping
httpGet:
path: /health/ping
port: 80
scheme: HTTP
# 首次调用延迟 60秒
initialDelaySeconds: 60
# 失败阈值 30次
failureThreshold: 30
# 检查周期 10s
periodSeconds: 10
# 成功阈值 1次,也就一旦上面的接口调通,该存活探针就不再执行
successThreshold: 1
# 超时 1s,一旦超过1秒没有收到上面的接口返回,就失败
timeoutSeconds: 1
启动探针是为解决我们背景中的问题1,会在成功前阻止另外的探针执行。
在我们的配置中,服务只要在60 + (10 * 30) = 360
秒以内成功,就可进入到剩余步骤。
在此期间,服务会留有足够的时间进行资源(初始化,编译替换,动态代理生成,动态配置,连接池,定时任务的首次调用,缓存的触发)准备。
解决问题2-流量无缝切换
当StartupProbe
完成其使命之后,剩下2种探针将接管后续健康检查的行为。
因为配置参数一样,就不再描述。
livenessProbe
负责告诉k8s pod是否存活,当超过设置阈值之后,根据restartPolicy
配置的策略来决定是否由新pod来接替。
readinessProbe
负责告诉k8s pod是否能够接管流量。
当readinessProbe
准备探针首次成功后,k8s就会将同组service的其他pod的流量按照更新策略,逐步转发到新就绪的pod上,之后当旧pod处理完剩余流量,进行收尾工作,释放资源.
在此处,我们3种探针用的同一个地址作为检查目标,而实际上应该根据服务的具体情况,添加新的路由各自处理,作更细致的判断。
比如服务虽然已经启动且存活了,但还不能接管流量,则准备探针的检查地址就得分开。
如果遇到服务的重大异常,还可以修改健康检查返回的http code,让服务不在处理后续流量,此时,我们可以登录该pod,进行维护工作,包括dump,arthas排查等等。
代码
事实上,我们并不需要添加这样额外的路由接收点,任何项目中路径都可以,不过为了避免干扰,才单独拿出来(这也是我说不需要额外代码的原因).
/**
* @Description k8s健康检查
* @Date 2022/10/17
* @Version 1.0
*/
@RestController
@RequestMapping("/health")
public class HealthController {
@GetMapping("/ping")
public String ping() {
return "pong";
}
}
解决问题3-预热
预热问题可以通过编写代码实现,当服务启动,健康检查接口首次收到请求时,设置标记,执行预热代码,当预热代码执行完毕再修改标记,让健康检查接口返回正确的httpCode。
/**
* @Description k8s健康检查
* @Date 2022/10/17
* @Version 1.0
*/
@RestController
@RequestMapping("/health")
public class HealthController {
// 预热标识
private volatile boolean isReceive = false;
@GetMapping("/ping")
public String ping() {
if (!isReceive) {
// 简单演示如何触发预热
isReceive = true;
// 调用预热代码
callWarmup();
}
return "pong";
}
}
为何要使用3种探针的组合?
这主要是考虑到探针的检查粒度做出的考量,如果不用StartupProbe
启动探针处理开始那段服务非正常状态的检测,那就需要非常弹性的失败阈值以及粗粒度的检查间隔,比如 检查间隔5秒,允许失败70次, 很明显,服务挂了6分钟才能被重启,这简直是灾难(更何况坏情况通常是一起出现或者在高峰时出现
).
总结
java服务的热更新,及流量无缝切换是经常遇到的问题,对于中,后台服务开发可能不是很重要,但对于前台开发,因为用户能直接感知,处理则需要十分小心了。
其他方案还有很多,根据实际情况应对。
更多推荐
所有评论(0)