容器中的JVM错误地计算处理器?

Gra*_*ath 7 java jvm cgroups docker kubernetes

我最近再次进行了一些研究,并偶然发现了这一点。在向 OpenJDK 团队抱怨之前,我想看看其他人是否观察到了这一点,或者不同意我的结论。

因此,众所周知,JVM 长期以来一直忽略应用于 cgroup 的内存限制。众所周知,它现在将它们考虑在内,从 Java 8 开始更新某些内容,以及 9 和更高版本。不幸的是,基于 cgroup 限制所做的计算是如此无用,以至于您仍然必须手动完成所有工作。请参阅谷歌和数百篇关于此的文章。

我几天前才发现的,并且没有在这些文章中阅读任何一篇文章,是 JVM 如何检查 cgroup 中的处理器数量。处理器计数用于决定用于各种任务的线程数,包括垃圾收集。所以正确理解很重要。

在 cgroup 中(据我所知,我不是专家)您可以设置可用 cpu 时间的限制(--cpusDocker 参数)。这仅限制时间,而不限制并行性。还有 cpu 份额(--cpu-sharesDocker 参数),这是在负载下分配 cpu 时间的相对权重。Docker 将默认值设置为 1024,但这纯粹是一个相对比例。

最后,还有 cpu 集(--cpuset-cpus用于 Docker)将 cgroup 和 Docker 容器显式分配给处理器的子集。这与其他参数无关,实际上会影响并行性。

因此,在检查我的容器可以并行运行多少线程时,据我所知,只有 cpu 集是相关的。JVM 虽然忽略了这一点,而是使用 cpu 限制(如果设置),否则 cpu 共享(假设 1024 默认为绝对比例)。恕我直言,这已经很错误了。它计算可用的 CPU 时间来调整线程池的大小。

在 Kubernetes 中情况变得更糟。AFAIK 最佳实践是不设置 cpu 限制,以便集群节点具有高利用率。此外,您应该为大多数应用程序设置一个低 CPU 请求,因为它们大部分时间都处于空闲状态,并且您希望在一个节点上安排多个应用程序。Kubernetes 将请求以毫 cpu 为单位设置为 cpu 份额,最有可能在 1000m 以下。JVM 始终假设一个处理器,即使您的节点运行在某个 64 核 CPU 怪物上。

有没有人也观察过这一点?我在这里错过了什么吗?还是 JVM 开发人员在为 cpu 实施 cgroup 限制时实际上使事情变得更糟?

以供参考:

apa*_*gin 9

作为大规模服务(> 15K 容器在自己的云中运行分布式 Java 应用程序)的开发人员,我也承认所谓的“Java 容器支持”远非完美。同时,我也能理解JVM开发者实现当前资源检测算法的推理。

问题是,运行容器化应用程序的云环境和用例如此之多,几乎不可能解决所有的配置问题。您声称对于 Kubernetes 中的大多数应用程序来说是“最佳实践”,但对于其他部署来说不一定是典型的。例如,这绝对不是我们服务的常见情况,大多数容器都需要一定的最低保证 CPU 资源量,因此也有一个不能超过的配额,以保证其他容器的 CPU 资源。此策略非常适合低延迟任务。OTOH,您所描述的策略更适合高吞吐量或批处理任务。

HotSpot JVM 当前实现的目标是开箱即用地支持流行的云环境,并提供覆盖默认值的机制。

Bob Vandette在一封电子邮件中解释了当前的选择。源码中还有一条注释cpu.shares,描述了为什么JVM会查看并除以1024。

/*
 * PER_CPU_SHARES has been set to 1024 because CPU shares' quota
 * is commonly used in cloud frameworks like Kubernetes[1],
 * AWS[2] and Mesos[3] in a similar way. They spawn containers with
 * --cpu-shares option values scaled by PER_CPU_SHARES. Thus, we do
 * the inverse for determining the number of possible available
 * CPUs to the JVM inside a container. See JDK-8216366.
 *
 * [1] https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu
 *     In particular:
 *        When using Docker:
 *          The spec.containers[].resources.requests.cpu is converted to its core value, which is potentially
 *          fractional, and multiplied by 1024. The greater of this number or 2 is used as the value of the
 *          --cpu-shares flag in the docker run command.
 * [2] https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html
 * [3] https://github.com/apache/mesos/blob/3478e344fb77d931f6122980c6e94cd3913c441d/src/docker/docker.cpp#L648
 *     https://github.com/apache/mesos/blob/3478e344fb77d931f6122980c6e94cd3913c441d/src/slave/containerizer/mesos/isolators/cgroups/constants.hpp#L30
 */
Run Code Online (Sandbox Code Playgroud)

至于并行性,我还建议 HotSpot 开发人员在估计可用 CPU 数量时应该考虑cpu.quotaJVM的情况。cpu.shares当容器分配有一定数量的 vcore(无论哪种方式)时,它只能依赖此数量的资源,因为无法保证有更多资源可供进程使用。考虑一个在 64 核计算机上运行的具有 4 个 vcore 的容器。任何在 64 个并行线程中运行的 CPU 密集型任务(GC 就是此类任务的一个示例)将很快耗尽配额,并且操作系统将长时间限制容器。例如,每 100 毫秒中的 94 毫秒,应用程序将处于停止状态暂停,因为记帐配额 ( cpu.cfs_period_us) 的默认周期是 100 毫秒。

无论如何,如果该算法在您的特定情况下不能很好地工作,那么总是可以使用-XX:ActiveProcessorCount选项覆盖可用处理器的数量,或者使用 完全禁用容器感知-XX:-UseContainerSupport