无状态java服务在k8s下流量无缝切换

背景

为了服务愉快的上线(其实就是不想每次发布都通知一遍相关人员,社恐瑟瑟发抖),所以我们需要服务能够无感知替换(没有流量遇到因为服务替换导致的失败)。
而通常的java服务,因为需要准备大量资源,导致启动时间通常比较久(普遍1分钟,慢的3,5分钟也是常见),而且有时候需要预热,避免短时间流量冲击造成服务down,等等。
由此引出待解决的问题清单。

问题清单

  1. 留给服务足够的启动和准备时间
  2. 流量无缝切换
  3. 预热

方案

  1. 利用k8s提供的功能实现服务的流量切换
  2. 类似golang的流量无缝切换(与netty的处理IO的方式很像)
  3. 热更新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服务的热更新,及流量无缝切换是经常遇到的问题,对于中,后台服务开发可能不是很重要,但对于前台开发,因为用户能直接感知,处理则需要十分小心了。
其他方案还有很多,根据实际情况应对。

猜你喜欢

转载自blog.csdn.net/weixin_46080554/article/details/127406991