是一个臭名昭著的问题。尽管基于JVM的应用程序具有出色的性能,但它们需要一个预热过程,在此期间性能不是最佳的。它可以归因于诸如即时(JIT)编译之类的事情,它通过收集使用配置文件信息来优化常用代码。最终的负面影响是,与平均时间相比,预热期间收到的请求将具有非常高的响应时间。在容器化、高吞吐量、频繁部署和自动缩放环境中,这个问题可能会加剧。在这篇文章中,我将讨论我们在Kubernetes集群中使用Java服务解决JVM预热问题的经验和方法。Genesis几年前,我们逐渐从单体架构转向微服务架构,并部署在Kubernetes中。大多数新服务都是用Java开发的。当我们启用Java服务时,我们首先遇到了这个问题。通过负载测试执行正常的容量规划过程,确定N个容器足以处理高于预期的峰值流量。尽管该服务可以毫无困难地处理高峰流量,但我们在部署期间开始发现问题。我们的每个pod在高峰时间处理超过10kRPM,并且我们正在使用Kubernetes滚动更新机制。在部署期间,该服务的响应时间会出现几分钟的峰值,然后才稳定到其通常的稳定状态。在我们的NewRelic仪表板中,我们会看到类似下图的图表:与此同时,依赖于我们部署的其他服务也在相关时间段内出现高响应时间和超时错误。采取1:增加应用程序的数量我们很快意识到问题与JVM预热阶段有关,但是因为其他重要的事情正在进行,我们没有太多时间来解决问题。所以我们尝试了最简单的解决方案——增加容器的数量来降低每个容器的吞吐量。我们几乎将pod的数量增加了两倍,以便每个pod在峰值时处理大约4kRPM的吞吐量。我们还调整了部署策略以确保一次最多部署25%(使用maxSurge和maxUnavailable参数)。这解决了问题,虽然我们运行的是稳定状态所需容量的3倍,但我们能够在我们的服务或任何相关服务中毫无问题地进行部署。在接下来的几个月里,随着我们迁移更多服务,我们也开始在其他服务中经常注意到这个问题。然后我们决定花一些时间解决问题并找到更好的解决方案。Take2:热身脚本在阅读了各种文章之后,我们决定尝试一下热身脚本。这个想法是运行一个预热脚本,该脚本向服务发送合成请求几分钟,以尝试在允许实际流量通过之前预热JVM。为了创建预热脚本,我们从生产流量中抓取了实际的URL。然后我们创建了一个Python脚本,它使用这些URL发送并行请求。我们相应地配置了就绪探针的initialDelaySeconds,以确保预热脚本在Pod准备就绪并开始接受流量之前完成。令我们惊讶的是,虽然我们看到了一些改进,但这并不重要。我们仍然观察响应时间和错误。此外,预热脚本引入了新问题。以前,我们的pod可以在40-50秒内准备就绪,但使用脚本,它们大约需要3分钟,这在部署期间成为一个问题,但在自动缩放期间更重要。我们对预热机制做了一些调整,例如预热脚本和实际流量之间的短暂重叠,并对脚本本身进行了更改,但没有看到明显的改进。最后,我们认为热身策略的小小收获不值得,彻底放弃了。Take3:ExploringHeuristics既然我们的热身脚本想法被吹了,我们决定尝试一些启发式方法:GC(G1、CMS和Parallel)和各种GC参数HeapmemoryCPUallocated经过几轮实验,我们终于取得了突破。我们正在测试的服务配置了Kubernetes资源限制:resources:requests:cpu:1000mmemory:2000Milimits:cpu:1000mmemory:2000Mi我们增加了CPU请求并将其限制为2000M,然后部署服务以查看影响。与预热脚本相比,我们在响应时间和错误方面看到了巨大的改进。为了进一步测试,我们将配置升级到3000mCPU,令人惊讶的是,问题完全消失了。正如您在下面看到的,响应时间没有峰值。很快就发现问题出在CPU节流上。显然,在预热阶段,JVM需要比平均稳定状态更多的CPU时间,但Kubernetes资源处理机制(CGroup)正在根据配置的限制限制CPU。有一种直接的方法可以验证这一点。Kubernetes公开了每个容器的指标container_cpu_cfs_throttled_seconds_total,它指示自启动以来此容器已限制CPU的秒数。如果我们在1000m配置上遵守这个指标,我们应该会在开始时看到很多节流,然后在几分钟后稳定下来。我们使用此配置进行部署,这里是Prometheus中所有Pod的container_cpu_cfs_throttled_seconds_total图表:正如预期的那样,在容器启动的前5到7分钟内有很多节流-通常在500到1000秒之间,但随后稳定下来,确认我们的假设。当我们使用3000mCPU配置部署时,我们观察到下图:CPU节流几乎可以忽略不计(几乎所有pod都不到4秒),这就是部署顺利的原因。Take4:配置BurstableQos尽管我们找到了导致此问题的瓶颈,但从成本角度来看,解决方案(三倍的CPU请求/限制)并不可行。这种解决方案实际上可能比运行更多pod更糟糕,因为Kubernetes根据请求调度pod,这可能会导致集群自动缩放器频繁触发,从而向集群添加更多节点。再想一想:在初始预热阶段(持续几分钟),JVM需要比配置限制(1000m)更多的CPU(~3000m)。预热后,即使CPU限制为1000m,JVM也可以充分发挥其潜力。Kubernetes使用“请求”而不是“限制”来调度pod。一旦我们以清晰、冷静的头脑阅读问题陈述,答案就会出现:KubernetesBurstableQoS。Kubernetes根据配置的资源请求和限制将QoS类分配给Pod。到目前为止,我们一直在通过将请求和限制指定为相等的值(最初都是1000m,然后都是3000m)来使用有保证的QoS类。虽然QoS保证有其好处,但我们不需要3个CPU在整个pod的生命周期内发挥全部功能,我们只需要在最初的几分钟内使用它。BurstableQoS类就是这样做的。它允许我们指定小于限制的请求,例如resources:requests:cpu:1000mmemory:2000Milimits:cpu:3000mmemory:2000Mi由于Kubernetes使用请求中指定的值来调度Pod,它会找到一个节点1000m的空闲CPU容量来调度ThisPod。但是,由于这个限制在3000M时要高很多,如果应用程序在任何时候需要超过1000M的CPU,并且节点上有可用的CPU空闲容量,则应用程序不会在CPU上受到限制。如果可用,它可以使用高达3000米。最后,是时候检验假设了。我们更改了资源配置并部署了应用程序。它有效!我们做了更多的部署来测试我们是否可以重复结果,并且它们是一致的。此外,我们还监控了container_cpu_cfs_throttled_seconds_total指标,这是其中一个部署的图表:如我们所见,该图表与3000mCPU的“保证QoS”设置非常相似。节流几乎可以忽略不计,这证实了具有突发QoS的解决方案是有效的。结论Kubernetes资源限制是一个重要的概念,我们已经在所有基于Java的服务和部署中实施了这个解决方案,自动缩放工作正常,没有任何问题。以下三个关键点需要你注意:container_cpu_cfs_throttled_seconds_total
