• 无状态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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    解决问题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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    启动探针是为解决我们背景中的问题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";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    解决问题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";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    为何要使用3种探针的组合?

    这主要是考虑到探针的检查粒度做出的考量,如果不用StartupProbe启动探针处理开始那段服务非正常状态的检测,那就需要非常弹性的失败阈值以及粗粒度的检查间隔,比如 检查间隔5秒,允许失败70次, 很明显,服务挂了6分钟才能被重启,这简直是灾难(更何况坏情况通常是一起出现或者在高峰时出现).

    总结

    java服务的热更新,及流量无缝切换是经常遇到的问题,对于中,后台服务开发可能不是很重要,但对于前台开发,因为用户能直接感知,处理则需要十分小心了。
    其他方案还有很多,根据实际情况应对。

  • 相关阅读:
    Javaweb05-Ajax
    python中图片处理库Image,transforms,cv2,matplotlib的图片矩阵尺寸变化
    C#使用winform做一个开关小游戏
    ArcMap中之提取影像数据边界
    home assistant弹出卡片
    Java方法案例
    基于java web的学生考勤带请假管理系统——计算机毕业设计
    unresolved external symbol w32_fcntl
    图解网络(三)——TCP篇01
    广告学概论笔记
  • 原文地址:https://blog.csdn.net/weixin_46080554/article/details/127406991