问题现象

我们的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参数,格式为=[:],我们要设置的最大打开文件数,type对应nofile。

$ 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: