追踪nginx ingress最大打开文件数问题
by 伊布
问题现象
我们的kubernetes ingress controller使用的是kubernetes出品的ingress-nginx,最近遇到了一个“Too many open files”的问题。
2019/09/19 09:47:56 [warn] 26281#26281: *97238945 a client request body is buffered to a temporary file /var/lib/nginx/body/0000269456, client: 1.1.1.1, server: xxx.ieevee.com, request: "POST /api/v1/xxx HTTP/1.1", host: "xxx.ieevee.com"
2019/09/19 09:47:56 [crit] 26281#26281: accept4() failed (24: Too many open files)
2019/09/19 09:47:56 [crit] 26281#26281: *97238948 open() "/var/lib/nginx/body/0000269457" failed (24: Too many open files), client: 1.1.1.1, server: xxx.ieevee.com, request: "POST /api/v1/xxx HTTP/1.1", host: "xxx.ieevee.com"
初步分析
看提示信息,是nginx打开文件太多了(nginx缓存用户请求)。nginx建立新连接,是通过nginx worker来处理的,所以我们看看nginx worker打开了多少文件。
# ps aux|grep worker
...
nobody 26281 0.0 0.0 422196 71596 ? Sl 13:05 0:00 nginx: worker process
nobody 26282 0.0 0.0 422196 71664 ? Sl 13:05 0:00 nginx: worker process
...
# cat /proc/26281/limits |grep open
Max open files 1024 1024 files
# lsof -p 26281|wc -l
910
# ulimit -n
65535
可以看到,nginx worker最大可以打开1024个文件,但是当前抓到的进程已经打开了910了,在流量较大时,可能会出现打开文件数比较大的问题。另外注意ulimit值,后面会提到。
linux基础:fs.file-max vs ulimit
fs.file-max
首先,linux上有个全局范围的可以打开文件的个数。
# cat /proc/sys/fs/file-max
13179954
注意:
- 这个值是跟操作系统、硬件资源相关的,不同系统可能不同。比如上面是一个物理服务器上的值,但在虚拟机上就只有808539;同一硬件、不同os,值也可能不同
- 这个值表达的是系统层面打开文件的max值,跟某个用户、某个会话没有关系
- 这个值是可以改的!如果运行一些数据库或者web服务器,由于需要打开大量文件,默认的file-max很可能不够用,这时可以通过sysctl来修改。
# sysctl -w fs.file-max=500000
或者修改sysctl.conf,增加 fs.file-max=500000
,并sysctl -p
生效。
fs.file-nr
sys fs还有一个兄弟值file-nr
,表示已经打开的文件数。如下,表示已经打开了55296个文件,只要这个值低于file-max,系统就还可以打开新文件。
# cat /proc/sys/fs/file-nr
55296 0 13179954
ulimit
那么ulimit呢,是系统层面的吗?不是的,这是一个经典的误解,其实ulimit限制的是一个进程所能使用的资源。
如果是在宿主机上,可以通过修改/etc/security/limits.conf
来修改ulimit。
//未设置时,可打开文件soft限制是1024。
$ ulimit -Sn
1024
$ ulimit -Hn
1048576
//修改limits.conf
$ cat /etc/security/limits.conf
bottle soft nofile 10000
bottle hard nofile 100000
//ssh重新登录
$ ulimit -Sn
10000
$ ulimit -Hn
100000
深入分析
回到最初的问题。
我们的nginx ingress controller是在容器中运行的,因此nginx worker能打开的文件数,取决于容器中的ulimit。
从前面日志可以看到,容器中的ulimit最大可打开文件数为65535,nginx worker为多线程模型,线程数取决于cpu核数,因此,每个nginx worker能打开的文件数为 65535/(cpu核数),由于ingress运行的物理机为56核,56核全部分配给了nginx,因此每个nginx worker能最大能打开的文件数为 65535/56 = 1170。
那么前面看到的每个nginx worker最大打开文件数为1024是怎么来的呢?
我们来看下ingress-nginx里是怎么计算的。
// the limit of open files is per worker process
// and we leave some room to avoid consuming all the FDs available
wp, err := strconv.Atoi(cfg.WorkerProcesses)
glog.V(3).Infof("number of worker processes: %v", wp)
if err != nil {
wp = 1
}
maxOpenFiles := (sysctlFSFileMax() / wp) - 1024
glog.V(2).Infof("maximum number of open file descriptors : %v", maxOpenFiles)
if maxOpenFiles < 1024 {
// this means the value of RLIMIT_NOFILE is too low.
maxOpenFiles = 1024
}
也就是说会扣除1024作为线程本身使用(毕竟要打开一些.so .a 等等文件),剩下的用来处理web请求;如果不到1024,则向上取整1024,因此我们前面看到每个nginx worker最大打开文件数为1024。
解决办法
现在问题明确了,是nginx worker能打开的文件数达到上限了,怎么解决呢?
ulimit可以吗?
首先想到的办法是修改ulimit。
但是,由于ingress controller的nginx是运行在容器中的,我们需要修改容器里的limit。
docker本质上也是一个进程,如果要设置ulimit,以docker run为例,可以设置--ulimit
参数,格式为
$ docker run -it --ulimit nofile=1024:65535 ubuntu bash
root@917bd8850581:/# ulimit -Sn
1024
root@917bd8850581:/# ulimit -Hn
65535
如前所述,ulimit表示的是“进程”的限制,那么该进程的子进程的ulimit呢?还是前面的容器,我们再进入bash查看。
root@917bd8850581:/# bash
root@917bd8850581:/# ulimit -Sn
1024
root@917bd8850581:/# ulimit -Hn
65535
可以看到,子进程(新bash)的ulimit和父进程(即docker run起来的bash)的ulimit,是一致的。
因此,我们可以想办法把这个参数传递给nginx worker,这样,就可以控制nginx worker的最大打开文件数了。
然而,kubernetes并不支持用户自定义ulimit。issue 3595是thockin在docker刚推出ulimit设置时就提出来的,但是多年过去了,并没有close,社区对这个选项有不同的声音。
此路不通。
修改docker daemon的ulimit默认值
容器中进程的ulimit值,是继承自docker daemon的,因此我们可以修改docker daemon的配置,设置默认的ulimit,这样就可以控制ingress 机器上运行的容器(包括ingress容器本身)的ulimit值了。修改 /etc/docker/daemon.json
:
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
}
},
这条路可以通,但是略微有点tricky,暂时先不用。
/etc/security/limits.conf
既然宿主机上可以通过/etc/security/limits.conf
来修改ulimit,容器里是不是也可以呢?
我们做一个新镜像,覆盖原始镜像里的/etc/security/limits.conf
。
FROM ubuntu
COPY limits.conf /etc/security/limits.conf
limits.conf内容如下:
root soft nofile 10000
root hard nofile 100000
制作新镜像,名为xxx,不设置--ulimit
启动后,查看:
$ docker run -it --rm ubuntu bash
root@fee1ea85ca56:/# ulimit -Sn
1048576
root@fee1ea85ca56:/# ulimit -Hn
1048576
root@fee1ea85ca56:/# exit
$ docker run -it --rm xxx bash
root@d535db9287b0:/# ulimit -Sn
1048576
root@d535db9287b0:/# ulimit -Hn
1048576
很遗憾,docker并不使用/etc/security/limits.conf
文件的设置,此路亦不通。
修改计算方式
这个问题其实在2018年的时候也有人遇到,并提交了PR 2050。
作者的想法是,既然我设置不了ulimit,那我可以通过在容器里设置fs.file-max
(例如增加init Container),并修改ingress nginx的计算方式,这样问题不就解决了吗?
init Container中设置:
sysctl -w fs.file-max=xxx
ingress nginx中的修改也很简单,前面的计算方式并不修改,只是改了sysctlFSFileMax
的实现,从获取ulimit改为获取fs/file-max
(显然,原来的注释是错误的,因为取的其实是ulimit,进程级的,并不是fs.file-max)。
// sysctlFSFileMax returns the value of fs.file-max, i.e.
// maximum number of open file descriptors
func sysctlFSFileMax() int {
fileMax, err := sysctl.New().GetSysctl("fs/file-max")
if err != nil {
glog.Errorf("unexpected error reading system maximum number of open file descriptors (fs.file-max): %v", err)
// returning 0 means don't render the value
return 0
}
glog.V(3).Infof("system fs.file-max=%v", fileMax)
return fileMax
}
尽管ingress是在容器里,但因为我们一整台物理机器都是用作为ingress,因此我们也不需要init Container,按上面的计算就可以正常工作了。
路通了!
修改回退
但是,这个PR早于我用的ingress nginx版本,在我的版本里,使用的还是ulimit(当然注释仍然是错的)。
// sysctlFSFileMax returns the value of fs.file-max, i.e.
// maximum number of open file descriptors
func sysctlFSFileMax() int {
var rLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
glog.Errorf("unexpected error reading system maximum number of open file descriptors (RLIMIT_NOFILE): %v", err)
// returning 0 means don't render the value
return 0
}
glog.V(2).Infof("rlimit.max=%v", rLimit.Max)
return int(rLimit.Max)
}
发生了什么?
原来,三个月后,有用户提交了一个新的issue,抱怨PR 2050是一个特殊案例,不能假设ingress nginx能够使用容器宿主机的所有资源,并提交了一个新的PR,回退了代码。
应该说,使用ulimit是合理的,因为的确nginx是在容器中的,分配所有宿主机资源给容器是不合理的。
然而,docker的隔离性做的并不好,默认nginx ingress controller计算nginx worker的方式,就是取的runtime.NumCPU()
,这样,即使给nginx容器分配了4核CPU,获取出来的worker数仍然是宿主机的!
但是你也不能说现在的计算方式不对,不应该除CPU个数,因为ulimit的确是“进程”级的,而nginx worker的确是线程。
ok,按照上面的理解,只要能设置nginx容器的ulimit就万事大吉了,然而,kubernetes又不支持。
此路不通。
不跟你们玩:配置项
从前面的分析可以发现,ingress nginx对nginx worker最大打开文件数的计算是有点乱的,所以后来有用户提交了一个新的PR,跳过guess的过程,直接作为一个配置项。
终于清静了。
Ref:
Subscribe via RSS