Docker环境undertow线程数不足问题探究

背景

上篇Docker环境Spring Boot应用大量http请求超时,我们找到大量http请求超时原因:undertow的工作线程不足
留下一些疑问:undertow默认配置是怎样的?为什么其他微服务也使用默认参数,却有256个工作线程?

结论

k8s调度启动容器默认分配的cpu资源很小OpenJDK 1.8.0_181会感知容器资源限制
两个因素共同导致undertow获取可用cpu数少,从而undertow工作线程数少。

过程

undertow的默认线程配置是什么

Google没有搜到。我通过查看启动日志和源码加断点,发现默认配置是由io.undertow.Undertow.Builder#Builder类定义:

ioThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2);
workerThreads = ioThreads * 8;

这2行代码,我们可以看出undertow工作线程数量计算公式:“可用cpu数”与2取大值,乘以8

验证获取可用cpu数

异常微服务只有16=2*8个线程,可能原因是Runtime.getRuntime().availableProcessors()获取的“可用cpu数”小于或等于2。

是不是这样?我们写一个类,输出获取的可用cpu数:

public class AvailCpu {
    public static void main(String[] args) {
        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

编译成class,手动copy到只有16个工作线程的微服务容器内和有256个工作线程的微服务容器内,
执行结果分别是1和32。

这就可以解释为什么那个微服务只有16个工作线程,而其他微服务使用默认参数却有256个工作线程:因为undertow获取的“可用cpu数”不一样。

可用cpu数为什么不一样

接下来我对比两个微服务的环境有什么差异。
为了方便描述,后续有16个工作线程的微服务简称微服务16,有256个线程的微服务简称微服务256

它们的JDK版本不一致

微服务16的JDK版本是OpenJDK 1.8.0_181,微服务256是OpenJDK 1.8.0_111,JDK版本微服务16 > 微服务256 。会不会是JDK版本不同导致的问题呢?(其实是这个原因,具体细节后面再描述。接下来,我走了不少弯路)

第一步,我基于微服务16的JDK镜像openjdk:8-jdk-alpine,加上AvailCpu.class,构造一个镜像进行测试。发现JDK的版本并不是OpenJDK 1.8.0_181,原因是openjdk:8-jdk-alpine这个tag已经指向最新的8-jdk-alpine版本。OpenJDK 1.8.0_181对应的镜像tag是openjdk:8u181-jdk-alpine。

第二步,使用openjdk:8u181-jdk-alpine镜像,加上AvailCpu.class,重新构造一个镜像进行测试。手动启动一个容器,发现输出的可用cpu数是正确的。难道不是JDK版本的问题?

服务器内核版本不一致

测试过程中,我发现测试服务器内核版本和部署微服务16服务器内核版本不一致。

是不是内核版本问题?以下是验证过程,

  1. 找到一个阿里云服务器,确认内核版本和生产环境一致。
  2. 折腾测试镜像构建,上传阿里云私服,下载到做测试的阿里云服务器。
  3. 手动启动一个镜像,发现输出的可用cpu数也正确。

结论是和内核版本没关系

接下来,又回到JDK版本不一致上。

我尝试网上下载OpenJDK 1.8.0_181的源码,源码不好搜,尝试好多链接也没下载到,遂放弃。

k8s启动容器和手动启动容器有差异

最后,我即将放弃之时,突然灵光一现,会不会是k8s启动容器和手动启动容器有差异?
接下来是验证过程,

  1. 重新构造一个测试镜像,上传到私服。
  2. 在k8s上基于这个测试镜像启动一个容器,输出可用cpu数是1。
  3. 手动基于这个测试镜像启动一个容器,输出可用cpu数是4。

果然是k8s启动容器和手动启动容器有差异。

我内心欣喜。

紧接着,我顺着2个方向查找原因:

  1. k8s调度启动容器默认参数是怎样的?
  2. 为什么OpenJDK 1.8.0_181在Docker环境获取可用cpu数是1?

方向1,我没有找到k8s启动容器的默认参数,但发现Docker启动容器有一些设置cpu资源的参数:–cpu-shares、–cpuset-cpus、–cpus、–cpu-period、–cpu-quota。

方向2,我发现JavaSE8u131+和JDK9 支持了对容器资源限制的自动感知能力。在微服务16容器内执行

java -XX:+PrintFlagsFinal 2>/dev/null|grep Container

输出

 bool PreferContainerQuotaForCPUCount           = true                                {product}
 bool UseContainerSupport                       = true                                {product}

说明OpenJDK 1.8.0_181会感知容器资源限制

那么k8s启动容器和手动启动容器资源限制到底有什么不同呢?
通过学习Docker利用的Linux底层技术cgroup,我了解到每个容器会产生/sys/fs/cgroup文件描述资源,cpu相关的文件夹是cpu和cpuacct。

  • cpu描述cpu资源
  • cpuacct描述cpu资源的使用统计情况。

通过对比k8s启动容器和手动启动容器/sys/fs/cgroup/cpu里面的文件,发现cpu.shares存在差异,

  • k8s启动容器,cpu.shares=2
  • 手动启动容器,cpu.shares=1024

手动启动容器docker run --cpu-shares=2 … ,验证发现获取到的可用cpu数=1。

至此,我们找到了undertow工作线程数不足的真实原因:k8s调度启动容器默认分配的cpu资源很小OpenJDK 1.8.0_181会感知容器资源限制,两个因素共同导致获取undertow获取的可用cpu数少,从而导致undertow工作线程数少

拓展

  1. 研究K8S启动容器的默认参数。
  2. 学习Docker的核心基础知识。
  3. 研究K8S与JVM怎么一起做资源限制。

参考资料

  1. 《Kubernetes之路 1 - Java应用资源限制的迷思》https://yq.aliyun.com/articles/562440
  2. 《Docker run参数参考》https://docs.docker.com/engine/reference/run/?spm=a2c4e.11153940.blogcont562440.8.35c7627aBRGAn3#cpu-period-constraint
  3. 《容器(docker)中运行java需关注的几个小问题》http://www.concurrent.work/docker/java/jvm/gc/pitfalls-about-running-java-inside-container/
  4. 《Docker 背后的内核知识——cgroups 资源限制》https://www.infoq.cn/article/docker-kernel-knowledge-cgroups-resource-isolation
  5. 《聊聊新版JDK对docker容器的支持》https://segmentfault.com/a/1190000014142950
  6. 《DOCKER基础技术:LINUX CGROUP》https://coolshell.cn/articles/17049.html
Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐