Yang's Kernel

如果你不能解决一个问题,那么有一个更小的问题你也不能解决,找出那个问题!

0%

linux文件描述符调优

背景

互联网业务中很多服务需要支撑千万级TCP长连接,这种场景往往Linux默认设置的可用文件描述符数量是不够的, 我们需要修改一些参数来优化, 下面列举一些今天讨论要涉及到的文件和命令:

1
2
3
4
5
6
# 命令 
ulimit systemd
# 文件
/proc/sys/fs/file-max
/proc/sys/fs/nr_open
/etc/security/limits.*

术语定义

FD (file descriptor) 文件描述符
RLIMIT_NOFILE 进程可打开的最大文件描述符数量限制

参考实验

这里使用Go分别编写server和client, client发起1000个tcp连接, server接收连接并打印连接序列号, 因为我们实验目标是验证进程可打开的FD数量是否符合预期, 所以并不需要在连接上收发消息.

代码见底部

分别启动server和client, 可以看到server端有如下报错信息

1
accept tcp 127.0.0.1:12345: accept4: too many open files

server进程每accept一个连接, 就需要一个创建一个socket对象进而打开一个FD去操作这个维护和操作这个新连接, 产生这个错误说明server进程已经无法打开新的FD.

资源限制

Linux作为一个多用户系统, 允许多人同时在线共享机器有限的硬件资源, 资源限制能力则必不可少

八股文 “进程线程的区别”经典答案: “进程是资源分配的最小单位,线程是CPU调度的最小单位”, 可见资源限制主要是针对进程的, 被限制的资源类型有很多种, 例如RLIMIT_CPU(cpu时间数), RLIMIT_CORE(core文件大小), 还有我们此篇的主题RLIMIT_NOFILE.

Linux在资源限制方面的顶层设计有以下几点:

  1. 资源限制主要针对进程, 少数是系统级别
  2. 进程资源限制分为软限制和硬限制
  3. 进程资源限制可通过fork系统调用由子进程继承

ulimit

ulimit应该是大家比较常用的shell内建命令

1
2
[viim@VM-8-5-centos ~]# type ulimit
ulimit is a shell builtin

ulimit主要针对当前shell进程及其fork的进程做资源控制, 可控制的资源种类很多, 这里我们只聚焦于RLIMIT_NOFILE, 资源限制分为hard和soft两种, soft值是当前资源的控制值, hard值是非root用户可调整的最大上限.

The soft limit is the value that the kernel enforces for the corresponding resource. The hard limit acts as a ceiling for the soft limit: an unprivileged process may set only its soft limit to a value in the range from 0 up to the hard limit, and (irreversibly) lower its hard limit. A privileged process (under Linux: one with the CAP_SYS_RESOURCE capability) may make arbitrary changes to either limit value.

man 2 getrlimit

下面实验一下, 使用普通用户viim可查看当前可打开文件描述符的软硬限制分别是500和501, 接着可以提升至硬限制501, 但为了server可以接收1000个tcp连接, 需要把限制提升到一个略大于1000的值, 比如1024, 但执行命令提示Operation not permitted, 因为使用使用普通用户viim无法突破硬限制501.

1
2
3
4
5
6
7
8
9
[viim@VM-8-5-centos ~]$ ulimit -Sn && ulimit -Hn
500
501
# 提升到最大值
[viim@VM-8-5-centos ~]$ ulimit -n 501
[viim@VM-8-5-centos ~]$ ulimit -Sn
501
[viim@VM-8-5-centos ~]$ ulimit -n 1024
-bash: ulimit: open files: cannot modify limit: Operation not permitted

如果想突破这个限制, 可以切换root用户来重复刚才的操作, 可以看到软硬限制均被提升至了1024, 这样server就不会报错了.

1
2
3
4
[root@VM-8-5-centos ~]# ulimit -n 1024
[root@VM-8-5-centos ~]# ulimit -Sn && ulimit -Hn
1024
1024

难道root用户使用ulimit -n 就没有限制了吗? 当然不是, 尝试一个比较夸张的输入, 可以看到如下报错

1
2
[root@VM-8-5-centos ~]# ulimit -n 100000000000
-bash: ulimit: open files: cannot modify limit: Operation not permitted

这里就要修改 /proc/sys/fs/nr_open, 官方文档的描述如下

This denotes the maximum number of file-handles a process can allocate. Default value is 1024*1024 (1048576) which should be enough for most machines. Actual limit depends on RLIMIT_NOFILE resource limit.

nr_open决定了RLIMIT_NOFILE的硬限制上限, 在我实验的机器上其默认值是1048576, 这个值对于普通程序已经足够大, 但对于并发几百万连接的服务器程序来说还是不够, 可以直接修改来临时增大.

1
2
3
4
[root@VM-8-5-centos ~]# sysctl -w fs.nr_open=3000000
[root@VM-8-5-centos ~]# ulimit -n 3000000
[root@VM-8-5-centos ~]# ulimit -n
3000000

但遗憾的是只修改这里还会碰到另外一个坑: /proc/sys/fs/file-max, 这个值决定了整个系统能打开的FD数量, 官方描述如下:

The value in file-max denotes the maximum number of file- handles that the Linux kernel will allocate. When you get lots of error messages about running out of file handles, you might want to increase this limit.

Attempts to allocate more file descriptors than file-max are reported with printk, look for “VFS: file-max limit reached”.

查看机器上file-max当前值

1
2
[viim@VM-8-5-centos ~]$ cat /proc/sys/fs/file-max
97822

97822远小于nr_open的值, 而且刚才使用sysctl设置nr_open也并没有报错, 重复实验server端会报如下错误:

1
open 96393: too many open files in system

修复此问题就需要增大file-max的值, 或者使用root启动进程, 内核代码中会对root用户忽略此限制, 可见root的威力! 因此也不推荐使用root启动进程, 资源不可控

由以上实验我们可得出如下结论:

1
ulimit -Sn <= ulimit -Hn <= fs.nr_open <= fs.file-max

注意: 上面提到过ulimit是针对当前shell 及其子进程所做的资源调整, 如果重新登录则一切都会恢复如初, 想要持久化修改就需要调整/etc/security/limits.* , 这里容易踩坑把 nofile hard限制直接修改超过nr_open, 导致无法登录linux, 此时只能通过单用户模式恢复

PAM

为什么修改/etc/security/limits.* 相关文件就可以达到ulimit限制持久化的效果呢? 这里就要简单提下PAM了.
PAM全称 Pluggable Authentication Modules, 可插入式认证模块. 提供一套共享库可以用于接入PAM的应用程序做身份认证相关的工作. 具体认证模块和应用程序解耦可以无需修改应用代码就灵活切换认证方式.

当我们使用ssh登录到主机时, sshd会调用login, login又会调用PAM提供的模块来设置当前会话进程的资源限制, /etc/security/limit.* 就是标准模块pam_limits.so读取的配置文件. pam历史悠久, 可以追溯到unix时代, 有兴趣的小伙伴可以自行google.

使用ulimit调整RLIMIT_NOFILE如此繁琐, 涉及 hard/soft limit, /proc/sys/fs/nr_open, /proc/sys/fs/file-max, /etc/security/limits.*, 其实背后还牵扯到复杂的Linux PAM, 一不小心就容易踩坑. 我们大多数在Linux下都是开发后台服务, 下面推荐一种更简单现代的做法

Systemd

systemd是一组软件, 用于linux进程管理, 目前已成为众多发行版标准组件, systemd非常复杂, 这里我们只关注对进程RLIMIT_NOFILE的资源控制

以tlinux2.2为例, 1号进程就是systemd

1
2
3
4
5
[root@VM-centos ~]# cat /etc/issue
Tencent tlinux release 2.2 (Final)

UID PID PPID C STIME TTY TIME CMD
root 1 0 0 6月29 ? 00:01:29 /usr/lib/systemd/systemd --switched-root --system --

我们都知道1号进程是所有用户态进程的父进程, 再结合上面提到的Linux进程资源限制机制, 它可以限制其子进程的资源分配, 只需要在进程配置文件中写入下面这行, 免去了使用ulimit的繁琐步骤, 不过nr_open和file-max这种系统级限制依然需要配置

1
LimitNOFILE = 1000000

使用systemd不仅免去了使用ulimit的繁琐步骤, 还可以进行进程监控, 日志收集等工作, 虽然systemd很复杂, 争议也很多, 但作者认为这种大而全的工具箱让程序员可以更聚焦一个工具使用, 不用为学习各种平台工具而分心了.

实验代码

Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
"log"
"net"
)

func handleConnection(num int, conn net.Conn) {
defer func() {
if err := conn.Close(); err != nil {
log.Println(err)
}
}()
log.Println(num)
buf := make([]byte, 0, 1024)
for {
if _, err := conn.Read(buf); err != nil {
log.Println(err)
return
}
}
}

func main() {
ln, err := net.Listen("tcp", "127.0.0.1:12345")
if err != nil {
log.Fatal(err)
}
fmt.Println("server start")

num := 0
for {
conn, err := ln.Accept()
if err != nil {
log.Println(err)
}
go handleConnection(num, conn)
num++
}

}

client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"log"
"net"
"sync"
)

func newConn(group *sync.WaitGroup) {
c, err := net.Dial("tcp", "127.0.0.1:12345")
if err != nil {
log.Println(err)
return
}
defer func() {
_ = c.Close()
group.Done()
}()

for {
buf := make([]byte, 0)
if _, err := c.Read(buf); err != nil {
log.Println(err)
return
}
}
}

func main() {
wg := &sync.WaitGroup{}
for i := 1; i <= 1000; i++ {
wg.Add(1)
go newConn(wg)
}
wg.Wait()
}

参考

  1. https://www.kernel.org/doc/Documentation/sysctl/fs.txt