Featured image of post Kubernetes Pod扩容预热陷阱:如何避免5xx错误和CPU飙升?

Kubernetes Pod扩容预热陷阱:如何避免5xx错误和CPU飙升?

等到第二次再去启动的时候,JVM就可以去读取刚刚所记录的这些方法编译的信息,同时会主动的触发即时编译器编译刚刚记录的热点方法,使得在用户请求到来之前,就把热点方法编译成为性能较高的Native Code,避免了在用户请求大量进入的时候做编译,这样就能够进一步提高应用程序的性能,节约CPU使用率。因此,每次扩展服务时,都会丢失数据或者会导致这部分请求的响应时间增加。在慢启动模式下,添加新的目标服务Pod时,避免新增Pod被大量请求击垮,这些新目标服务可以根据指定的加速期在接受其均衡策略的请求之前进行预热。

问题背景

在Kubernetes(k8s)中,Pod的自动扩容(Horizontal Pod Autoscaler, HPA)是一个常见的功能,用于根据负载动态调整Pod的数量。然而,当新Pod加入时,如果没有预热机制,流量会立即涌入新Pod,导致新Pod在启动初期无法处理大量请求,进而引发5xx错误、CPU飙升等问题。特别是在基于JVM的应用程序中,JVM需要一定的时间进行预热(如JIT编译、类加载等),才能达到最佳性能。

阿里云介绍: 在未启用慢启动预热功能时,每当新目标Pod加入时,请求方都会向该Pod发送一定比例的流量,不支持新Pod的渐进式流量增加。这对于需要一些预热时间来提供全部负载的服务可能是不可取的,并且可能会导致请求超时、数据丢失和用户体验恶化。例如在基于JVM的Web应用程序中,这些应用程序使用水平Pod自动缩放。当服务刚启动时,它会被大量请求淹没,这会导致应用程序预热时持续超时。因此,每次扩展服务时,都会丢失数据或者会导致这部分请求的响应时间增加。预热的基本思想就是让刚启动的机器逐步接入流量。 地址:https://help.aliyun.com/zh/asm/user-guide/use-the-warm-up-feature

问题分析

  1. 是什么
    • Pod扩容时的流量分配问题:当新Pod加入时,Kubernetes默认会将流量均匀分配到所有Pod(包括新Pod),导致新Pod在启动初期无法处理大量请求。
    • 预热陷阱:新Pod在启动初期需要时间进行预热(如JVM的JIT编译、缓存预热等),如果此时涌入大量请求,会导致请求超时、5xx错误、CPU飙升等问题。
  2. 为什么
    • 流量分配机制:Kubernetes默认的流量分配机制是均匀分配,不支持渐进式流量增加。
    • 预热需求:某些应用程序(如基于JVM的应用)在启动初期需要时间进行预热,才能达到最佳性能。
  3. 怎么做
    • 解决方案:通过引入预热机制,让新Pod在启动初期逐步接入流量,避免一次性涌入大量请求

解决方案

方案一:使用Istio的LoadBalancerSettings进行流量预热

慢启动模式又称渐进式流量增加,用户可以为服务配置一个时间段,每当一个服务实例启动时,请求方会向该实例发送一部分请求负载,并在配置的时间段内逐步增加请求量。当慢启动窗口持续时间到达,就会退出慢启动模式。

在慢启动模式下,添加新的目标服务Pod时,避免新增Pod被大量请求击垮,这些新目标服务可以根据指定的加速期在接受其均衡策略的请求之前进行预热。

慢启动对于依赖缓存并且需要预热期才能以最佳性能响应请求的应用程序非常有用。只需在服务对应的DestinationRule下的配置trafficPolicy/loadBalancer即可,需要注意的是:

loadBalancer:表示负载均衡器信息。类型限定于ROUND_ROBIN和LEAST_REQUEST负载均衡器。

warmupDurationSecs:表示Service的预热持续时间。如果设置,则新创建的服务端点在此窗口期间从其创建时间开始保持预热模式,并且Istio逐渐增加该端点的流量,而不是发送成比例的流量。

慢启动要求应用在当前可用区的副本数不为0。例如:

数据面集群只有一个可用区A,可用区A中当前有一个副本,启动第二个时,慢启动会生效。

数据面集群有两个可用区A、B,当前应用只有一个副本,且位于可用区A,如果启动的第二个副本位于可用区B(一些调度器会默认采用跨可用区分布的策略),则不会触发慢启动。此时,如果再启动第三个副本,慢启动会生效。

Istio 提供了LoadBalancerSettings配置,可以通过warmupDurationSecs参数实现流量预热。该参数允许新Pod在启动后的一段时间内逐步增加流量。 地址:https://istio.io/latest/docs/reference/config/networking/destination-rule/#LoadBalancerSettings 步骤

  1. 在Istio的DestinationRule中配置LoadBalancerSettings
  2. 设置warmupDurationSecs参数,定义预热时间。

代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: mocka
spec:
  host: mocka
  trafficPolicy:
    loadBalancer:
      simple: ROUND_ROBIN
      warmupDurationSecs: 300  # 预热时间为300秒(5分钟)

优点

  • 简单易用,直接通过Istio配置实现流量预热。
  • 无需修改应用程序代码。

缺点

  • 需要部署Istio,增加了系统复杂性。

方案二:使用阿里云的JVM预热方案(jwarmup)

参考文档:https://baijiahao.baidu.com/s?id=1752631259548476891&wfr=spider&for=pc

阿里云提供了jwarmup工具,可以将JVM的JIT编译信息保存到文件中,下次发布时自动加载,从而加速JVM预热过程。

JwarmUp的基本原理:根据前一次程序运行的情况,记录热点代码以及类加载顺序等信息。在应用下一次启动的时候积极主动地对相关类进行加载,并积极编译相关代码,进而使得应用尽快使用上C2编译优化的指令。从而在流量进来之前,提前完成类的加载、初始化和方法编译, 跳过解释阶段, 直接执行编译好的native code, 避免一面解释执行一面后台编译带来的CPU与load飙高, rt超时等问题。

在这里插入图片描述

在这里插入图片描述 上方为JWarmup的流程图,它将应用程序的发布分成了两个阶段,分别是Recording和Replaying。在Recording阶段,JVM会接受线上的请求,同时记录JVM即时编译器它所编译方法的信息,并且将这些信息都输出到一个文件之中。 等到第二次再去启动的时候,JVM就可以去读取刚刚所记录的这些方法编译的信息,同时会主动的触发即时编译器编译刚刚记录的热点方法,使得在用户请求到来之前,就把热点方法编译成为性能较高的Native Code,避免了在用户请求大量进入的时候做编译,这样就能够进一步提高应用程序的性能,节约CPU使用率。

步骤

  1. 在应用程序中集成jwarmup工具。
  2. 在发布前运行jwarmup,生成JIT编译信息文件。
  3. 在下次发布时,自动加载JIT编译信息文件。

代码示例

  • 记录编译信息阶段
1
-XX:+CompilationWarmUpRecording  -XX:CompilationWarmUpLogfile=jwarmup.log  -XX:CompilationWarmUpRecordTime=300

记录模式、记录存储的jwarmup.log,在5分钟后生成profiling data

  • 使用编译信息阶段
1
-XX:+CompilationWarmUp  -XX:CompilationWarmUpLogfile=jwarmup.log  -XX:CompilationWarmUpDeoptTime=0

JWarmUp会在指定时间退优化warmup编译的方法,设置CompilationWarmUpDeoptTime为0可以取消这个定时。

1、recording记录下来的日志,是怎么分发到其他线上机的?

答:在应用启动的脚本文件进行控制:

  • 预热节点,会将记录下来的编译信息上传到远程服务器oss上,
  • 发布节点,在启动时从远处机器主动pull下来预热节点上传的编译信息。

2、是怎么制定一台机器做recording的呢?是访问某个url还是判断beta机器?

答:是通过访问oss做了一个类似于“文件锁”的东西,先拿到锁的beta机器做为预热节点,其余机器为发布节点。想要达到预热的效果请确保:

  • 发布的机器的参数中有 -XX:+CompilationWarmUp
  • 每次beta发布后,记得检查下编译信息文件是否已经上传
  • beta发布的那台机器必须是有流量的,Recording时间不要太短,尽量多编译一些方法。

如果不保证上述两点的话,便无法完成预热发布,即没有充分利用beta的编译信息,仍然走正常发布的流程


方案三:手动预热接口

在Pod启动后,手动调用所有接口进行预热,然后再接入流量。

步骤

  1. 在Pod启动后,编写脚本调用所有接口。
  2. 确保所有接口都被调用后,再接入流量。

代码示例

1
2
3
4
# 预热脚本示例
curl http://my-service/api/endpoint1
curl http://my-service/api/endpoint2
curl http://my-service/api/endpoint3

优点

  • 简单直接,适用于所有类型的应用程序。
  • 无需额外工具或配置。

缺点

  • 需要手动编写和维护预热脚本。
  • 无法动态调整预热时间。

方案四:使用Kubernetes的Readiness Probe

通过配置Kubernetes的Readiness Probe,确保Pod在完全预热后再接入流量。

步骤

  1. 在Pod的Readiness Probe中配置预热检查。
  2. 确保Pod在预热完成后再标记为Ready

代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: my-image
    readinessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 60  # 延迟60秒开始检查
      periodSeconds: 10        # 每10秒检查一次

优点

  • 简单易用,直接通过Kubernetes配置实现。
  • 无需修改应用程序代码。

缺点

  • 无法精确控制预热时间。
  • 需要应用程序提供健康检查接口。