性能文章>docker容器高性能之文件句柄>

docker容器高性能之文件句柄原创

2977010

一、前言

前段时间同事遇到了一个容器内文件句柄泄漏的问题,容器内部偶发性出现Too many open files报错。当时的文件句柄配置如下:

问题是不是来了,主机的文件句柄和容器的文件句柄数有关系吗?是什么关系呢?这就是以下要探究讨论的问题。

中间记录了实验的过程,导致篇幅有点长,心急的小伙伴们可以直接跳转至四章节《结论分析》。

二、docker容器启动流程

docker是典型的client-server类型的应用,可以使用docker version命令查看服务端和客户端的信息:

docker启动容器的大致流程如下:

注: 左边黄色文字框中对应的是在Linux下的常驻进程名称;

重点在runc创建容器子进程的时候,会传入相关的namespace参数进行资源隔离(用户隔离是基于Linux的CLONE_NEWUSER这个namespace);

三、本地实验过程

正好本地有个现成的war包,就用了tomcat容器来测试,用Dockerfile将war拷贝到webapps下就行了。
Dockerfile如下:

FROM tomcat:8.5.72-jdk8

RUN useradd dockertester -m -d /home/dockertester \
    && gpasswd -a dockertester dockertester \
    && chown dockertester:dockertester -R /usr/local/tomcat

ADD --chown=dockertester pftest.war /usr/local/tomcat/webapps

USER dockertester

CMD ["catalina.sh", "run"]
  • CentOS Linux release 7.4.1708 (Core)
  • docker-19.03.9
  • tomcat:8.5.72-jdk8

(一)使用Linux默认配置,2000并发测试

环境准备1:

  1. 使用Dockerfile构建应用镜像;
  2. 使用locust【比较省资源】进行并发请求,需要自己写压测脚本;
  3. ulimit -n查看当前文件句柄数限制(默认普通用户都是1024):
 [root@localhost ~]# ulimit -a
 core file size          (blocks, -c) 0
 data seg size           (kbytes, -d) unlimited
 scheduling priority             (-e) 0
 file size               (blocks, -f) unlimited
 pending signals                 (-i) 7216
 max locked memory       (kbytes, -l) 64
 max memory size         (kbytes, -m) unlimited
 open files                      (-n) 1024
 pipe size            (512 bytes, -p) 8
 POSIX message queues     (bytes, -q) 819200
 real-time priority              (-r) 0
 stack size              (kbytes, -s) 8192
 cpu time               (seconds, -t) unlimited
 max user processes              (-u) 7216
 virtual memory          (kbytes, -v) unlimited
 file locks                      (-x) unlimited
  1. cat /proc/sys/fs/file-max查看系统最大句柄数限制(默认181953);
[root@localhost ~]# cat /proc/sys/fs/file-max
181953
  1. 在容器内查看java进程当前文件句柄数限制情况(当前容器内java进程文件句柄数最大为1048576,这数值哪来的后面解释):
dockertester@9798be4fb19c:/usr/local/tomcat$ cat /proc/$(ps -A | grep java | awk '{print $1}')/limits | grep "files"
Max open files            1048576              1048576              files     
  • 测试执行2分钟,没有任何报错。

  • 在容器内查看当前使用句柄数,发现已经句柄数使用已经达到3488,远超默认的1024
[root@localhost ~]# docker exec -it tomcat-test bash
dockertester@9798be4fb19c:/usr/local/tomcat$ cat /proc/sys/fs/file-nr
3488    0       181953

测试小结

  docker容器内的文件句柄数限制不受Linux宿主机用户文件句柄数限制。使用普通用户启动容器内应用也是一样,本次试验就是新建了dockertester用户。

(二)修改容器的limit参数,2000并发测试

  1. 环境准备1的配置都不用修改;
  2. 容器启动命令添加限制参数:--ulimit nofile=1024:1024,容器启动完成后,在容器内查看java进程句柄数限制(已经成功修改为1024):
dockertester@123da4ac778d:/usr/local/tomcat$ cat /proc/$(ps -A | grep java | awk '{print $1}')/limits | grep "files"
Max open files            1024                 1024                 files
  • 压测不到1分钟开始出现大量报错:

  • 查看当前系统句柄数使用情况,保持在2432(超出1024的那些是Linux宿主机其他进程占用的):
dockertester@123da4ac778d:/usr/local/tomcat$ cat /proc/sys/fs/file-nr
2432    0       181953
  • 查看tomcat的日志,可以发现大量Too many open files报错:

测试小结

  docker容器内的句柄数受启动参数--ulimit nofile=1024:1024的限制。

(三)修改Linux系统最大文件句柄数限制,2000并发测试

环境准备2:

  1. 去掉容器启动命令添加限制参数:--ulimit nofile=1024:1024
  2. 修改系统最大文件句柄数限制:
[root@localhost ~]# cat /proc/sys/fs/file-max
181953
[root@localhost ~]# echo 2000 > /proc/sys/fs/file-max
[root@localhost ~]# cat /proc/sys/fs/file-max
2000
  1. 容器内查看当前java进程文件句柄数限制(还是之前默认的1048576):
dockertester@c8e0796a224a:/usr/local/tomcat$ cat /proc/$(ps -A | grep java | awk '{print $1}')/limits | grep "files"
Max open files            1048576              1048576              files
  • 压测执行不到1分钟,开始大量报错:

  • 在容器内查看当前文件句柄数使用情况,文件句柄数已经用满:
bash: start_pipeline: pgrp pipe: Too many open files in system
bash: /bin/cat: Too many open files in system
  • tomcat日志中存在大量Too many open files报错:

测试小结

  docker容器内的句柄数受宿主机系统文件句柄数限制(/proc/sys/fs/file-max/etc/sysctl.conf文件配置的参数)

四、结论分析

  • 注: 本文不适用于Rootless方式安装的docker应用,针对这种方式安装的docker还未研究过。

(一)namespace进程隔离【隔离不完全】

  1. docker启动容器会基于namespace做进程隔离,且拥有CAP_SYS_RESOURCE的能力,像文件句柄这样的参数docker可以进行重设,但是重设之后还是受宿主机系统参数的限制,即/proc/sys/fs/file-max/etc/sysctl.conf文件配置的参数;
     
  2. 默认docker内进程的启动用户为root,容器内的root权限要比宿主机的权限稍低。正常情况下,容器内无法修改类似/proc/sys/fs/file-max这些系统配置,会出现只读文件报错bash: /proc/sys/fs/file-max: Read-only file system。但如果在docker run的时候添加--privileged参数,就可以修改这些内核参数,而且宿主机的配置也会被同步修改,这是十分危险⚠️的事;
     
  3. docker和宿主机共享内核,因此cat /proc/sys/fs/file-max这类查看系统级别的命令在容器内和容器外执行都是一样的。还有top命令看到的CPU核数并不是docker启动参数限制的核数,还是宿主机的核数。

(二)支持namespace的内核配置项

参见官方文档:Configure namespaced kernel parameters (sysctls) at runtime

IPC(用于隔离进程间通讯所需的资源)
  • kernel.msgmax, kernel.msgmnb, kernel.msgmni, kernel.sem, kernel.shmall, kernel.shmmax, kernel.shmmni, kernel.shm_rmid_forced
  • fs.mqueue开头的参数,fs.mqueue.*
Network相关:
  • 类似net.*的配置参数;
  • 如果使用了--network=host选项, 则不允许以docker run --sysctl net.ipv4.ip_forward=1 someimage方式启动;

(三)容器内进程默认文件句柄数的来源

  联系第二章的容器启动过程,容器进程runc的子进程(创建完结束),runccontainerd的子进程。在fork子进程的时候,子进程会继承父进程的所有信息,所以容器内进程默认文件句柄数是继承的containerd进程的句柄数限制。可以命令查看,其实就是之前默认的1048576;

cat /proc/$(ps -A | grep containerd-PID | awk '{print $1}')/limits | grep "files"

containerd进程的句柄数限制在containerd.service文件中默认设置了1048576

  • 新版本的不是在docker.service中设置的了
  • 从Docker 1.11开始,Docker Daemon不再包含任何运行时的代码,这部分逻辑迁移单独的OCI兼容层,而containerd就在daemonrunc所在的OCI层之间,负责容器的创建,而销毁工作交给shim

(四)一个有意思的用户命名空间映射

  在Dockerfile构建应用镜像的时候,如果指定非root启动容器,那么docker默认会映射uid1000的用户,如果本地宿主机没有设置uid为1000的用户,容器启动后,ps -ef会看到用户那一列直接就显示1000。

请先登录,再评论

暂无回复,快来写下第一个回复吧~

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr