容器内核优化..

首先我们抛出3个问题:

  1. docker容器的内核与宿主机内核是怎样的关系?
  2. 容器在运行时如何调用系统资源?
  3. docker的性能参数有没有作用范围?

容器与宿主机的内核关系 —— 共享内核

docker镜像是一个“应用程序和它运行依赖环境”的封装。当镜像运行起来后,即是docker容器。运行时的容器本质是操作系统下的一个进程,这些进程共享同一个宿主机OS的内核。

与传统VM相比,docker是一种操作系统虚拟化技术,并不需要在镜像内安装GuestOS。

docker在共享宿主机内核的基础上包装内核提供的一系列API,这些API中最重要的就是namespace和cgroup。通过namespace实现隔离,通过cgroup实现资源限制。

Namespace–命名空间

  • PIDnamespace:每当在此空间中启动一个程序,内核就给它分配唯一的ID。与宿主机所见的不同,容器内的进程都有自己的进程ID空间。
  • MNTnamespace:每个容器都有自己目录挂载的命名空间。
  • NETnamespace:每个容器都有自己单独的网络栈,其中的socket和网卡设备都是其他容器不能访问的。
  • UTSnamespace:在此命名空间中的进程拥有自己的主机名和NIS域名。
  • IPCnamespace:拥有相同IPC命名空间的进程才可以利用“共享内存、信号量、消息队列”方式进行通信。
  • User namespace:用于隔离容器中的UID,GID和根目录

Cgroup–控制组

顾名思义,控制、分组,用于对进程进行层次化分组,并以组为粒度实现资源限制和策略控制。每一个容器内的进程都收到组策略的管控。

Cgroup包含了很多控制器。

Cgroup对进程分组的控制是通过一个虚拟文件系统下的目录节点和节点文件实现的,控制器是目录节点中的文件,文件内容规定了系统资源的配额。

以全局cgroup为例 # ls /sys/fs/cgroup

img

默认的情况下,docker会在这些目录下面创建自己的节点子目录,而每个容器都会创建自己容器ID的节点子目录;这些目录都包含了对应层次的控制器文件,用于控制每一级的资源配额。下文简述一些控制器的功能:

  • cpu:限制组中进程的cpu使用量
  • cpuacct:对进程的CPU使用程度进行计量
  • memory:限制组中进程的内存使用量,并计量
  • blkio:限制组中进程对块设备的IO操作
  • net_prio:用于设置组相关网络包的优先级
  • hugetlb:用于限制和报告组内进程对大页面的使用量
  • freezer:用于组内进程的挂起和解冻

docker内核与调优

理解了上面的几部分,我们现在应该有个印象:

  • docker容器共享宿主机OS的内核
  • docker容器视为一个进程
  • docker容器的资源配额受docker引擎和宿主机OS限制

所以在进行docker内核调优时,可以推论:

  • docker的内核参数大多继承自宿主机内核
  • docker容器可以在某些情况、某些范围内调整自己的内核参数

那么某些情况和某些范围指的是什么呢?让我们来分享一些特殊情况

docker的 –sysctl 参数的白名单

语法:docker run –sysctl key=value IMAGE:TAG CMD

该参数允许docker容器启动时设置某些内核参数,这些参数是有限制的,可以在docker源码中查看到

1
2
3
4
5
6
7
8
9
10
11
12
// docker/opts/opts.go
func ValidateSysctl(val string) (string, error) { validSysctlMap := map[string]bool{
"kernel.msgmax": true, "kernel.msgmnb": true, "kernel.msgmni": true,
"kernel.sem": true,
"kernel.shmall": true,
"kernel.shmmax": true, "kernel.shmmni": true, "kernel.shm_rmid_forced": true,
}
validSysctlPrefixes := []string{
"net.",
"fs.mqueue.",
}
...

如上所示,只有被标识为 True 的 kernel., net. 以及fs.mqueue.* 是可以传入参数并修改的

如果传入其他参数,会提示:for –sysctl: sysctl’kernel.acpi_video_flags=0’ is not whitelisted

即白名单未通过

容器中看不到的参数

通常容器内可以修改的参数,都是可以通过sysctl -a查看到的,但有些则不行:

例如 net.core.rmem_max参数(定义内核用于所有类型的连接的最大接收缓冲大小)

1
2
3
4
root@host01:~/tmp# sysctl -a | grep rmem_max
net.core.rmem_max = 212992
root@host01:~/tmp# docker run hub.c.163.com/public/debian:7.9 sysctl -a | grep rmem_max
root@host01:~/tmp#

原因是这些参数是隶属于kernel的namespace

不能被namespace化的参数

当一些内核参数在容器中修改后,会反过来作用于宿主机,这一类内核参数的特点是无法被namespace化;因为docker使用共享内核的机制,大多数和kernel相关的参数都有这样的特点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# step1.检查当前进程数最大值
root@host01:~/tmp# sysctl -a | grep pid_max
kernel.pid_max =32768

# step2.启动一个测试用的容器,--privileged 表示获得root权限
root@host01:~/tmp# docker run -d --privileged --name test hub.c.163.com/public/debian:7.9
a43e89ee85d36e250e0886331e9d6213094f31260eb9e1539b83f0e9cfc91848

# step3.在容器内修改进程数最大值
root@host01:~/tmp# docker exec test sysctl -w kernel.pid_max=86723
kernel.pid_max = 86723

# step4.再次检查进程数最大值,发现变更成了上一步docker容器内设定的值
root@host01:~/tmp# sysctl -a | grep pid_max
kernel.pid_max = 86723

引入3中特殊情况对应的图形:

img

1 doker 表示在白名单内; 2 visible 表示sysctl -a 容器内可见;3 namespace 表示不影响全局

综上,我们可以得出结论:

  • 程序白名单以外的参数不可调整;想调整请修改docker源码
  • 容器内systcl看不到的继承自宿主机
  • 容器内能影响宿主机的不要修改

针对docker容器的内存调优策略:

  • 请尽量在宿主机OS进行内核优化操作;
  • 需要特化&满足条件的参数请通过 –sysctl参数传入或写入dockerfile

五、操作系统参数调优

限于篇幅,本文对于操作系统的参数调优不再阐述,给与几篇相关扩展阅读:

  1. 文件句柄数 ulimit -n & /etc/security/limits.conf
  2. Docker Container继承自Docker Daemon的ulimit设置,也可以修改/etc/sysconfig/docker,指定docker-deamon启动参数
  3. TCP/IP协议栈优化、内核参数默认值和建议值

优化

tcp_max_syn_backlog和somaxconn

tcp_max_syn_backlog介绍:

socket接收的所有连接都是存放在队列类型的数据结构中,关键问题是这种队列有两个,而且其长度都是可以设置的。
分别是下面两个内核参数:
/proc/sys/net/ipv4/tcp_max_syn_backlog
/proc/sys/net/core/somaxconn
其中:
tcp_max_syn_backlog是指定所能接受SYN同步包的最大客户端数量,即半连接上限;

somaxconn参数介绍:

somaxconn是指服务端所能accept即处理数据的最大客户端数量,即完成连接上限。
对于没有调优的新装的centOS6.5系统,这两个参数的值都是128。
这么描述虽然精确,但是没有一定基础,不熟练网络编程的人理解起来很费劲。

syn queue 与 accept queue

Linux 进程监听端口时,内核会给它对应的 socket 分配两个队列:

  • syn queue: 半连接队列。server 收到 SYN 后,连接会先进入 SYN_RCVD 状态,并放入 syn queue,此队列的包对应还没有完全建立好的连接(TCP 三次握手还没完成)。
  • accept queue: 全连接队列。当 TCP 三次握手完成之后,连接会进入 ESTABELISHED 状态并从 syn queue 移到 accept queue,等待被进程调用 accept() 系统调用 “拿走”。

注意:这两个队列的连接都还没有真正被应用层接收到,当进程调用 accept() 后,连接才会被应用层处理,具体到我们这个问题的场景就是 nginx 处理 HTTP 请求。

为了更好理解,可以看下这张 TCP 连接建立过程的示意图:

image-20210820155828635