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