46 | 案例篇:为什么应用容器化后,启动慢了很多?
下载APP
关闭
渠道合作
推荐作者
46 | 案例篇:为什么应用容器化后,启动慢了很多?
2019-03-11 倪朋飞 来自北京
《Linux性能优化实战》
课程介绍
讲述:冯永吉
时长14:13大小12.98M
你好,我是倪朋飞。
不知不觉,我们已经学完了整个专栏的四大基础模块,即 CPU、内存、文件系统和磁盘 I/O、以及网络的性能分析和优化。相信你已经掌握了这些基础模块的基本分析、定位思路,并熟悉了相关的优化方法。
接下来,我们将进入最后一个重要模块—— 综合实战篇。这部分实战内容,也将是我们对前面所学知识的复习和深化。
我们都知道,随着 Kubernetes、Docker 等技术的普及,越来越多的企业,都已经走上了应用程序容器化的道路。我相信,你在了解学习这些技术的同时,一定也听说过不少,基于 Docker 的微服务架构带来的各种优势,比如:
使用 Docker ,把应用程序以及相关依赖打包到镜像中后,部署和升级更快捷;
把传统的单体应用拆分成多个更小的微服务应用后,每个微服务的功能都更简单,并且可以单独管理和维护;
每个微服务都可以根据需求横向扩展。即使发生故障,也只是局部服务不可用,而不像以前那样,导致整个服务不可用。
不过,任何技术都不是银弹。这些新技术,在带来诸多便捷功能之外,也带来了更高的复杂性,比如性能降低、架构复杂、排错困难等等。
今天,我就通过一个 Tomcat 案例,带你一起学习,如何分析应用程序容器化后的性能问题。
案例准备
今天的案例,我们只需要一台虚拟机。还是基于 Ubuntu 18.04,同样适用于其他的 Linux 系统。我使用的案例环境如下所示:
机器配置:2 CPU,8GB 内存。
预先安装 docker、curl、jq、pidstat 等工具,如 apt install docker.io curl jq sysstat。
其中,jq 工具专门用来在命令行中处理 json。为了更好的展示 json 数据,我们用这个工具,来格式化 json 输出。
你需要打开两个终端,登录到同一台虚拟机中,并安装上述工具。
注意,以下所有命令都默认以 root 用户运行,如果你用普通用户身份登陆系统,请运行 sudo su root 命令切换到 root 用户。
如果安装过程有问题,你可以先上网搜索解决,实在解决不了的,记得在留言区向我提问。
到这里,准备工作就完成了。接下来,我们正式进入操作环节。
案例分析
我们今天要分析的案例,是一个 Tomcat 应用。Tomcat 是 Apache 基金会旗下,Jakarta 项目开发的轻量级应用服务器,它基于 Java 语言开发。Docker 社区也维护着 Tomcat 的官方镜像,你可以直接使用这个镜像,来启动一个 Tomcat 应用。
我们的案例,也基于 Tomcat 的官方镜像构建,其核心逻辑很简单,就是分配一点儿内存,并输出 “Hello, world!”。
在终端一中,执行下面的命令,启动 Tomcat 应用,并监听 8080 端口。如果一切正常,你应该可以看到如下的输出:
从输出中,你可以看到,docker run 命令,会自动拉取镜像并启动容器。
这里顺便提一下,之前很多同学留言问,到底要怎么下载 Docker 镜像。其实,上面的 docker run,就是自动下载镜像到本地后,才开始运行的。
由于 Docker 镜像分多层管理,所以在下载时,你会看到每层的下载进度。除了像 docker run 这样自动下载镜像外,你也可以分两步走,先下载镜像,然后再运行容器。
比如,你可以先运行下面的 docker pull 命令,下载镜像:
显然,在我的机器中,镜像已存在,所以就不需要再次下载,直接返回成功就可以了。
接着,在终端二中使用 curl,访问 Tomcat 监听的 8080 端口,确认案例已经正常启动:
不过,很不幸,curl 返回了 “Connection reset by peer” 的错误,说明 Tomcat 服务,并不能正常响应客户端请求。
是不是 Tomcat 启动出问题了呢?我们切换到终端一中,执行 docker logs 命令,查看容器的日志。这里注意,需要加上 -f 参数,表示跟踪容器的最新日志输出:
从这儿你可以看到,Tomcat 容器只打印了环境变量,还没有应用程序初始化的日志。也就是说,Tomcat 还在启动过程中,这时候去访问它,当然没有响应。
为了观察 Tomcat 的启动过程,我们在终端一中,继续保留 docker logs -f 命令,并在终端二中执行下面的命令,多次尝试访问 Tomcat:
观察一会儿,可以看到,一段时间后,curl 终于给出了我们想要的结果 “Hello, wolrd!”。但是,随后又出现了 “Empty reply from server” ,和一直持续的 “Connection refused” 错误。换句话说,Tomcat 响应一次请求后,就再也不响应了。
这是怎么回事呢?我们回到终端一中,观察 Tomcat 的日志,看看能不能找到什么线索。
从终端一中,你应该可以看到下面的输出:
从内容上可以看到,Tomcat 在启动 24s 后完成初始化,并且正常启动。从日志上来看,没有什么问题。
不过,细心的你肯定注意到了最后一行,明显是回到了 Linux 的 SHELL 终端中,而没有继续等待 Docker 输出的容器日志。
输出重新回到 SHELL 终端,通常表示上一个命令已经结束。而我们的上一个命令,是 docker logs -f 命令。那么,它的退出就只有两种可能了,要么是容器退出了,要么就是 dockerd 进程退出了。
究竟是哪种情况呢?这就需要我们进一步确认了。我们可以在终端一中,执行下面的命令,查看容器的状态:
你会看到,容器处于 Exited 状态,说明是第一种情况,容器已经退出。不过为什么会这样呢?显然,在前面容器的日志里,我们并没有发现线索,那就只能从 Docker 本身入手了。
我们可以调用 Docker 的 API,查询容器的状态、退出码以及错误信息,然后确定容器退出的原因。这些可以通过 docker inspect 命令来完成,比如,你可以继续执行下面的命令,通过 -f 选项设置只输出容器的状态:
这次你可以看到,容器已经处于 exited 状态,OOMKilled 是 true,ExitCode 是 137。这其中,OOMKilled 表示容器被 OOM 杀死了。
我们前面提到过,OOM 表示内存不足时,某些应用会被系统杀死。可是,为什么内存会不足呢?我们的应用分配了 256 MB 的内存,而容器启动时,明明通过 -m 选项,设置了 512 MB 的内存,按说应该是足够的。
到这里,我估计你应该还记得,当 OOM 发生时,系统会把相关的 OOM 信息,记录到日志中。所以,接下来,我们可以在终端中执行 dmesg 命令,查看系统日志,并定位 OOM 相关的日志:
从 dmesg 的输出,你就可以看到很详细的 OOM 记录了。你应该可以看到下面几个关键点。
第一,被杀死的是一个 java 进程。从内核调用栈上的 mem_cgroup_out_of_memory 可以看出,它是因为超过 cgroup 的内存限制,而被 OOM 杀死的。
第二,java 进程是在容器内运行的,而容器内存的使用量和限制都是 512M(524288kB)。目前使用量已经达到了限制,所以会导致 OOM。
第三,被杀死的进程,PID 为 27281,虚拟内存为 4.3G(total-vm:4613208kB),匿名内存为 505M(anon-rss:517316kB),页内存为 19M(20168kB)。换句话说,匿名内存是主要的内存占用。而且,匿名内存加上页内存,总共是 524M,已经超过了 512M 的限制。
综合这几点,可以看出,Tomcat 容器的内存主要用在了匿名内存中,而匿名内存,其实就是主动申请分配的堆内存。
不过,为什么 Tomcat 会申请这么多的堆内存呢?要知道,Tomcat 是基于 Java 开发的,所以应该不难想到,这很可能是 JVM 堆内存配置的问题。
我们知道,JVM 根据系统的内存总量,来自动管理堆内存,不明确配置的话,堆内存的默认限制是物理内存的四分之一。不过,前面我们已经限制了容器内存为 512 M,java 的堆内存到底是多少呢?
我们继续在终端中,执行下面的命令,重新启动 tomcat 容器,并调用 java 命令行来查看堆内存大小:
你可以看到,初始堆内存的大小(InitialHeapSize)是 126MB,而最大堆内存则是 1.95GB,这可比容器限制的 512 MB 大多了。
之所以会这么大,其实是因为,容器内部看不到 Docker 为它设置的内存限制。虽然在启动容器时,我们通过 -m 512M 选项,给容器设置了 512M 的内存限制。但实际上,从容器内部看到的限制,却并不是 512M。
我们在终端中,继续执行下面的命令:
果然,容器内部看到的内存,仍是主机内存。
知道了问题根源,解决方法就很简单了,给 JVM 正确配置内存限制为 512M 就可以了。
比如,你可以执行下面的命令,通过环境变量 JAVA_OPTS=’-Xmx512m -Xms512m’ ,把 JVM 的初始内存和最大内存都设为 512MB:
接着,再切换到终端二中,重新在循环中执行 curl 命令,查看 Tomcat 的响应:
可以看到,刚开始时,显示的还是 “Connection reset by peer” 错误。不过,稍等一会儿后,就是连续的 “Hello, wolrd!” 输出了。这说明, Tomcat 已经正常启动。
这时,我们切换回终端一,执行 docker logs 命令,查看 Tomcat 容器的日志:
这次,Tomcat 也正常启动了。不过,最后一行的启动时间,似乎比较刺眼。启动过程,居然需要 22 秒,这也太慢了吧。
由于这个时间是花在容器启动上的,要排查这个问题,我们就要重启容器,并借助性能分析工具来分析容器进程。至于工具的选用,回顾一下我们前面的案例,我觉得可以先用 top 看看。
我们切换到终端二中,运行 top 命令;然后再切换到终端一,执行下面的命令,重启容器:
接着,再切换到终端二,观察 top 的输出:
从 top 的输出,我们可以发现,
从系统整体来看,两个 CPU 的使用率分别是 3% 和 5.7% ,都不算高,大部分还是空闲的;可用内存还有 7GB(7353652 avail Mem),也非常充足。
具体到进程上,java 进程的 CPU 使用率为 10%,内存使用 0.9%,其他进程就都很低了。
这些指标都不算高,看起来都没啥问题。不过,事实究竟如何呢?我们还得继续找下去。由于 java 进程的 CPU 使用率最高,所以要把它当成重点,继续分析其性能情况。
说到进程的性能分析工具,你一定也想起了 pidstat。接下来,我们就用 pidstat 再来分析一下。我们回到终端一中,执行 pidstat 命令:
结果中,各种 CPU 使用率全是 0,看起来不对呀。再想想,我们有没有漏掉什么线索呢?对了,这时候容器启动已经结束了,在没有客户端请求的情况下,Tomcat 本身啥也不用做,CPU 使用率当然是 0。
为了分析启动过程中的问题,我们需要再次重启容器。继续在终端一,按下 Ctrl+C 停止 pidstat 命令;然后执行下面的命令,重启容器。成功重启后,拿到新的 PID,再重新运行 pidstat 命令:
仔细观察这次的输出,你会发现,虽然 CPU 使用率(%CPU)很低,但等待运行的使用率(%wait)却非常高,最高甚至已经达到了 97%。这说明,这些线程大部分时间都在等待调度,而不是真正的运行。
注:如果你看不到 %wait 指标,请先升级 sysstat 后再试试。
为什么 CPU 使用率这么低,线程的大部分时间还要等待 CPU 呢?由于这个现象因 Docker 而起,自然的,你应该想到,这可能是因为 Docker 为容器设置了限制。
再回顾一下,案例开始时容器的启动命令。我们用 --cpus 0.1 ,为容器设置了 0.1 个 CPU 的限制,也就是 10% 的 CPU。这里也就可以解释,为什么 java 进程只有 10% 的 CPU 使用率,也会大部分时间都在等待了。
找出原因,最后的优化也就简单了,把 CPU 限制增大就可以了。比如,你可以执行下面的命令,将 CPU 限制增大到 1 ;然后再重启,并观察启动日志:
现在可以看到,Tomcat 的启动过程,只需要 2 秒就完成了,果然比前面的 22 秒快多了。
虽然我们通过增大 CPU 的限制,解决了这个问题。不过再碰到类似问题,你可能会觉得这种方法太麻烦了。因为要设置容器的资源限制,还需要我们预先评估应用程序的性能。显然还有更简单的方法,比如说直接去掉限制,让容器跑就是了。
不过,这种简单方法,却很可能带来更严重的问题。没有资源限制,就意味着容器可以占用整个系统的资源。这样,一旦任何应用程序发生异常,都有可能拖垮整台机器。
实际上,这也是在各大容器平台上最常见的一个问题。一开始图省事不设限,但当容器数量增长上来的时候,就会经常出现各种异常问题。最终查下来,可能就是因为某个应用资源使用过高,导致整台机器短期内无法响应。只有设置了资源限制,才能确保杜绝类似问题。
小结
今天,我带你学习了,如何分析容器化后应用程序性能下降的问题。
如果你在 Docker 容器中运行 Java 应用,一定要确保,在设置容器资源限制的同时,配置好 JVM 的资源选项(比如堆内存等)。当然,如果你可以升级 Java 版本,那么升级到 Java 10 ,就可以自动解决类似问题了。
当碰到容器化的应用程序性能时,你依然可以使用,我们前面讲过的各种方法来分析和定位。只不过要记得,容器化后的性能分析,跟前面内容稍微有些区别,比如下面这几点。
容器本身通过 cgroups 进行资源隔离,所以,在分析时要考虑 cgroups 对应用程序的影响。
容器的文件系统、网络协议栈等跟主机隔离。虽然在容器外面,我们也可以分析容器的行为,不过有时候,进入容器的命名空间内部,可能更为方便。
容器的运行可能还会依赖于其他组件,比如各种网络插件(比如 CNI)、存储插件(比如 CSI)、设备插件(比如 GPU)等,让容器的性能分析更加复杂。如果你需要分析容器性能,别忘了考虑它们对性能的影响。
思考
最后,我想邀请你一起来聊聊,你碰到过的容器性能问题。你是怎么分析它们的?又是怎么解决根源问题的?你可以结合我的讲解,总结自己的思路。
欢迎在留言区和我讨论,也欢迎把这篇文章分享给你的同事、朋友。我们一起在实战中演练,在交流中进步。
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 17
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
45 | 答疑(五):网络收发过程中,缓冲区位置在哪里?
下一篇
47 | 案例篇:服务器总是时不时丢包,我该怎么办?(上)
精选留言(25)
- Adam2019-03-11这个问题应该是/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了限制导致的。
作者回复: 嗯嗯,正解!
共 2 条评论24 - 饭粒2019-09-22有个疑问,增加的 JVM 堆内存限制也是 512M 和 容器内存限制 512M 的一样,那还有非堆内存和其他的运行内存呢,这个可能还会有 OOM 吧?共 4 条评论12
- Goal2019-06-09打卡,学习linux性能调优,顺带学习docker的基础知识,这案例太赞了6
- 腾达2019-03-12这里看java进程cpu的时候,使用了之前学到的perf record方法,但看docker内部函数名称的时候,还是遇到了问题,我的步骤如下: $ mkdir dockermap $ PID=$(docker inspect --format {{.State.Pid}} tomcat) $ sudo bindfs /proc/$PID/root dockermap $ sudo perf record -g -p $PID $ sudo perf report --symfs dockermap - 99.95% 0.00% java libjvm.so [.] 0x00000000008bf292 - 0x8bf292 - 67.44% 0xa79ff1 0xa79af6 0xa78677 0xa7afc7 0xa75d62 + 0x8e6853展开
作者回复: Java的问题请参考https://github.com/jvm-profiling-tools/perf-map-agent
5 - Tony2019-03-12这个例子对稍微有点docker和jvm的基础的人还是太简单了。应该上点serverless冷启动的分析。cold start是FaaS一个很大的问题,不知道你怎么用前面的知识分析一下?谢谢
作者回复: cold start话题太大了,涉及的不止是单机内的问题。你如果有具体的问题,可以分享出来,大家一起讨论
4 - 且听风吟2019-03-11启动容器几分钟后,直接把宿主机跑死了,没法继续进行,这是怎么回事呢?
作者回复: 主机内存多大?重启后可以看看系统日志,一般会有上次为啥死机的线索
3 - 885912019-11-19vm 开启了swap 导致 docker 容器的内存使用超过512m后还继续执行,没有被oom-kill。2
- xfan2019-03-12和虚拟机内存设置也有关系,老师的是8G 30%就很大,我的2G 所以不大,也不会引起OOM,当我调成8G 的时候就出现了和老师一样的现象了OOM
作者回复: 嗯嗯 是这样的
2 - ninuxer2019-03-11打卡day49 前两天在我们线下环境一台docker宿主机上,一直无法create容器,后来看日志,发现有两个可疑之处: 第一:docker日志显示socket文件损坏,但是当时运行其他docker管理命令能正常返回结果 第二:宿主机上有个kworker/u80进程cpu利用率一直100%,最终是通过重启宿主机解决的~展开
作者回复: 👍 谢谢分享
共 2 条评论2 - Li~2021-08-12请教老师,通过环境变量 JAVA_OPTS=’-Xmx512m -Xms512m’ ,把 JVM 的初始内存和最大内存都设为 512MB后,为什么执行docker exec tomcat free -m还是看到的主机内存,不是512MB 呢?共 1 条评论1
- Geek_8c27312020-09-04在这章我就有东西可讲了,我研究docker至今,开始设置的资源限制太小,导致nodejs应用容器OOM,生成大量文件,占满了整个硬盘,导致其他系统应用不可用。再者就是Java异步行为过程不做限制会消耗大量内存,做完限制后就算OOM也只是它自身1
- 小老鼠2019-03-28测试应用软件的性能(比如用LoadRunner 或JMter)可以在容器中进行吗?
作者回复: 可以的
1 - 腾达2019-03-12我的tomcat为什么不能实现oom?dmesg显示没有oom,进程也没有被杀死。我看有其他网友留言说要虚拟机内存设为8G,我的是设置为8G了。
作者回复: 执行 docker exec tomcat java -XX:+PrintFlagsFinal -version | grep HeapSize 确认一下容器中java的堆内存?
共 2 条评论1 - 我来也2019-03-11[D46打卡] 看来真是有必要对容器做资源限制. 刚开始分配的资源可以不要计算得那么精准,只要别把机器拖垮即可. 实战篇还是很有意思,根据症状去找各种程序及系统日志和各项配置参数. 如果不做特殊处理,容器内部看到的系统cpu/memory配置确实都是主机的.程序默认根据这个来做策略调整确实会受到误导. 目前工作中还没使用容器,暂时还没法在容器中实战.😁展开1
- 阿甘2022-11-06 来自广东如果想要限制容器的CPU比较小,但是又想提升启动性能,怎么破呢?
- 大大林2021-01-15我这不行啊 按照老师的镜像直接启动 没oom CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 9438a69a9630 tomcat 0.63% 343.7MiB / 512MiB 67.14% 3.96kB / 6.53kB 0B / 0B共 1 条评论
- Geek_cef97c2020-11-09你好,我们做docker的性能验证测试,通过unixbench跑数据,发现默认的seccomp设置对性能有较大影响,通过设置--security-opt seccomp:unconfined参数能和宿主机性能相当,而默认seccomp大约有16%左右损耗。 这种情况的性能损耗怎么定位呢?
- 注意力$2020-06-09老师,请问 PID=$(docker inspect tomcat -f '{{.State.Pid}}') 这个pid 是怎么取出来的,前面的课程里好像也有这个吧?不熟悉shell共 2 条评论
- 杉松壁2020-05-06有2个问题: 1. 容器和jvm都设置了内存,但是不知道为什么容器有的时候还是会OOM 2. 既要限制容器的资源,又不好设置JVM资源的时候,有更方便的方法吗
- hhhh2020-04-27之前遇到过k8s里面跑的容器,cgroup限制的内存过小,开发人员错写为1MB,restartPolicy又设置成了always, 结果这个container一直被oom kill,dmesg全是oom信息,cenos7上还造成了memory allocate dead lock 物理机直接挂了。。。