刨根問底兒,看我如何處理 Too Many Open Files 錯誤!
本文轉(zhuǎn)載自微信公眾號「開發(fā)內(nèi)功修煉」,作者張彥飛allen 。轉(zhuǎn)載本文請聯(lián)系開發(fā)內(nèi)功修煉公眾號。
如果你的項目中支持高并發(fā),或者是測試過比較多的并發(fā)連接。那么相信你一定遇到過“Too many open files”這個錯誤。
這個錯誤的出現(xiàn)其實是正常的,因為每打開一個文件(包括socket),都需要消耗一定的內(nèi)存資源。為了避免個別進程不受控制地打開了過多的文件而讓整個服務(wù)器崩潰,Linux 對打開的文件描述符數(shù)量有限制。
但是解決這個錯誤“奇葩”的地方在于,竟然需要修改三個參數(shù):fs.nr_open、nofile(其實 nofile 還分 soft 和 hard) 和 fs.file-max。這幾個參數(shù)里有的是進程級的、有的是系統(tǒng)級的、有的是用戶進程級的,說一遍都覺得好亂。而且另外這幾個參數(shù)還有依賴關(guān)系,著實比較復雜。
不知道你,反正飛哥我是根本記不住哪個是哪個。每次遇到這種問題,還是都得再繼續(xù) Google 一遍。但由于復雜性,所以其實網(wǎng)上的很多帖子里也都并沒有真正搞清楚。如果照搜索出來的文章改,稍有不慎就會踩雷,把機器搞出問題。
我在測試最大TCP連接數(shù)的時候就踩過兩次坑。
第一次是當時開了二十個子進程,每個子進程開啟了五萬個并發(fā)連接興高采烈準備測試百萬并發(fā)。結(jié)果倒霉催地忘了改 file-max 了。實驗剛開始沒多大一會兒就開始報錯“Too many open files”。但問題是這個時候更悲催的是發(fā)現(xiàn)所有的命令包括 ps、kill也同時無法使用了。因為它們也都需要打開文件才能工作。后來沒辦法,重啟系統(tǒng)解決的。
另外一次是重啟機器完了之后發(fā)現(xiàn)無法 ssh 登錄了。后來找運維工程部的同學報障以后才算是修復。最終發(fā)現(xiàn)是因為 hard nofile 比 fs.nr_open 高了,直接導致無法登陸。(其實我把 fs.nr_open 加大過,但是用的是 echo 命令 修改的。系統(tǒng)一重啟,還原了)。
一、找到源代碼
對于這三個家伙,我真的是無法言語更多了。所以我下定了決心,一定要把它們徹底搞清楚。怎么搞?那沒有比把它的源碼扒出來能看的更準確了。我們就拿創(chuàng)建 socket 來舉例,首先找到 socket 系統(tǒng)調(diào)用的入口
- //file: net/socket.c
- SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
- {
- retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
- if (retval < 0)
- goto out_release;
- }
我們看到 socket 調(diào)用 sock_map_fd 來創(chuàng)建相關(guān)內(nèi)核對象。接著我們再進入 sock_map_fd 瞧瞧。
- //file: net/socket.c
- static int sock_map_fd(struct socket *sock, int flags)
- {
- struct file *newfile;
- //在這里會判斷打開文件數(shù)是否超過 soft nofile 和 fs.nr_open
- //獲取 fd 句柄號
- int fd = get_unused_fd_flags(flags);
- if (unlikely(fd < 0))
- return fd;
- //在這里會判斷打開文件數(shù)是否超過 fs.file-max
- //創(chuàng)建 sock_alloc_file對象
- newfile = sock_alloc_file(sock, flags, NULL);
- if (likely(!IS_ERR(newfile))) {
- fd_install(fd, newfile);
- return fd;
- }
- put_unused_fd(fd);
- return PTR_ERR(newfile);
- }
為什么創(chuàng)建一個socket又要申請 fd,又要申請 sock_alloc_file 呢?我們看一個進程打開文件時的內(nèi)核數(shù)據(jù)結(jié)構(gòu)圖就明白了
結(jié)合上圖,就能輕松理解這兩個函數(shù)的作用
- get_unused_fd_flags:申請 fd,這只是一個在找一個可用的數(shù)組下標而已
- sock_alloc_file:申請真正的 file 內(nèi)核對象
二、找到進程級限制 nofile 和 fs.nr_open
接下來,我們再回到最大文件數(shù)量的判斷上。這里我直接把結(jié)論拋出來。get_unused_fd_flags 中判斷了 nofile、和 fs.nr_open。如果超過了這兩個參數(shù),就會報錯。請看!
- //file: fs/file.c
- int get_unused_fd_flags(unsigned flags)
- {
- // RLIMIT_NOFILE 是 limits.conf 中配置的 nofile
- return __alloc_fd(
- current->files,
- 0,
- rlimit(RLIMIT_NOFILE),
- flags
- );
- }
在get_unused_fd_flags 中,調(diào)用了 rlimit(RLIMIT_NOFILE)。這個是讀取的 limits.conf 中配置的 soft nofile,代碼如下:
- //file: include/linux/sched.h
- static inline unsigned long task_rlimit(const struct task_struct *tsk,
- unsigned int limit)
- {
- return ACCESS_ONCE(tsk->signal->rlim[limit].rlim_cur);
- }
通過當前進程描述訪問到 rlim[RLIMIT_NOFILE],這個對象的 rlim_cur 是 soft nofile(rlim_max 對應(yīng) hard nofile )。
緊接著讓我們進入 __alloc_fd() 中來
- //file: include/uapi/asm-generic/errno-base.h
- #define EMFILE 24 /* Too many open files */
- int __alloc_fd(struct files_struct *files,
- unsigned start, unsigned end, unsigned flags)
- {
- ...
- error = -EMFILE;
- //看要分配的文件號是否超過 end(limits.conf 中的 nofile)
- if (fd >= end)
- goto out;
- error = expand_files(files, fd);
- if (error < 0)
- goto out;
- ...
- }
在__alloc_fd() 中會判斷要分配的句柄號是不是超過了 limits.conf 中 nofile 的限制。fd 是當前進程相關(guān)的,是一個從 0 開始的整數(shù)。如果超限,就報錯 EMFILE (Too many open files)。
這里注意個小細節(jié),那就是進程里的 fd 是一個從 0 開始的整數(shù)。只要確保分配出去的 fd 編號不超過 limits.conf 中 nofile,就能保證該進程打開的文件總數(shù)不會超過這個數(shù)。
接著我們看到調(diào)用又會進入 expand_files:
- static int expand_files(struct files_struct *files, int nr)
- {
- //2. 判斷打開文件數(shù)是否超過 fs.nr_open
- if (nr >= sysctl_nr_open)
- return -EMFILE;
- }
在 expand_files 我們看到,又到 nr (就是 fd 編號) 和 fs.nr_open 相比較了。超過這個限制,返回錯誤 EMFILE (Too many open files)。
由上可見,無論是和 fs.nr_open,還是和 soft nofile 比較,都用的是當前進程的文件描述符序號在比較的,所以這兩個參數(shù)都是進程級別的。
有意思的是和這兩個參數(shù)的比較幾乎是前后腳進行的,所以它兩的作用也基本一樣。Linux之所以分兩個參數(shù)來控制,那是因為 fs.nr_open 是系統(tǒng)全局的,而 nofile 則可以分用戶來分別控制。
所以,現(xiàn)在我們可以得出第一個結(jié)論。
結(jié)論1:soft nofile 和 fs.nr_open的作用一樣,它兩都是限制的單個進程的最大文件數(shù)量。區(qū)別是 soft nofile 可以按用戶來配置,而 fs.nr_open 所有用戶只能配一個。
三、找到系統(tǒng)級限制 fs.nr_open
我們在回過頭來看 sock_map_fd 中調(diào)用的另外一個函數(shù) sock_alloc_file,在這個函數(shù)里我們發(fā)現(xiàn)它會和 fs.file-max 這個系統(tǒng)參數(shù)來比較。用啥比的呢?
- //file: fs/file_table.c
- struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
- {
- file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
- &socket_file_ops);
- }
- struct file *alloc_file(struct path *path, fmode_t mode,
- const struct file_operations *fop)
- {
- file = get_empty_filp();
- ...
- }
- struct file *get_empty_filp(void)
- {
- //files_stat.max_files就是 fs.file-max參數(shù)
- if (get_nr_files() >= files_stat.max_files
- && !capable(CAP_SYS_ADMIN) //注意這里root賬號并不受限制
- ) {
- }
- }
可見是用 get_nr_files() 來和 fs.file-max來比較的。根據(jù)該函數(shù)的注釋我們能看到它是當前系統(tǒng)打開的文件描述符總量。如下:
- /*
- * Return the total number of open files in the system
- */
- static long get_nr_files(void)
- {
- ...
另外注意下 !capable(CAP_SYS_ADMIN) 這行??赐赀@句,我才恍然大悟,原來 file-max 這個參數(shù)只限制非 root 用戶。開篇中我提到的文件打開過多時無法使用 ps,kill 等命令,是因為我用的非 root 賬號操作的。哎,下次再遇到這種文件直接用 root 去 kill 就行了。之前竟然丟臉地采用了重啟機器大法。。
所以現(xiàn)在我們可以得出另一個結(jié)論了。
結(jié)論2:fs.file-max: 整個系統(tǒng)上可打開的最大文件數(shù),但不限制 root 用戶
總結(jié)一下
我們總結(jié)一下,其實在 Linux 上能打開多少個文件,限制有兩種:
- 第一種,進程級別的,限制的是單個進程上可打開的文件數(shù)。具體參數(shù)是 soft nofile 和 fs.nr_open。它們兩個的區(qū)別是 soft nofile 可以不同用戶配置不同的值。而 fs.nr_open 在一臺 Linux 上只能配一次。
- 第二種,系統(tǒng)級別的,整個系統(tǒng)上可打開的最大文件數(shù),具體參數(shù)是fs.file-max。但是這個參數(shù)不限制 root 用戶。
另外這幾個參數(shù)之間還有耦合關(guān)系,因此還要注意以下三點:
- 1、如果你想加大 soft nofile, 那么 hard nofile 也需要一起調(diào)整。因為如果 hard nofile 設(shè)置的低, 你的 soft nofile 設(shè)置的再高都沒用,實際生效的值會按二者里最低的來。
- 2、如果你加大了 hard nofile,那么 fs.nr_open 也都需要跟著一起調(diào)整。如果不小心把 hard nofile 設(shè)置的比 fs.nr_open 大了,后果比較嚴重。會導致該用戶無法登陸。如果設(shè)置的是 * 的話,那么所有的用戶都無法登陸。
- 3、還要注意如果你加大了 fs.nr_open,但是用的是 echo "xx" > ../fs/nr_open 的方式,剛改完你可能覺得沒問題。只要機器一重啟你的 fs.nr_open 設(shè)置就會失效,還是會無法登陸。
假如你想讓你的進程可以打開 100 萬個文件描述符,我覺得比較穩(wěn)妥點的修改方法是干脆都直接用 conf 文件的方式來改。這樣比較統(tǒng)一,也比較安全。
- # vi /etc/sysctl.conf
- fs.nr_open=1100000 //要比 hard nofile 大一點
- fs.file-max=1100000 //多留點buffer
- # sysctl -p
- # vi /etc/security/limits.conf
- * soft nofile 1000000
- * hard nofile 1000000
通過這種方式修改,你就可以繞過飛哥踩過的坑了。