天天看點

分析單機最大長連接配接數

linux檔案描述符限制和單機最大長連接配接數

相關參數

​​linux系統​​中與檔案描述符相關的參數有以下幾個:

  1. ​soft/hard nofile​

  2. ​file-max(/proc/sys/fs/file-max)​

  3. ​nr_open(/proc/sys/fs/nr_open)​

這三個參數的作用都是限制一個程序可以打開的最大檔案數,它們之間有什麼差別和聯系呢?本文會從核心代碼出發,分析這些參數是怎麼影響檔案打開的,以及它們之間什麼差別,并在最後讨論一個于此有關的流行話題:單機最大長連接配接數。

如果你沒有興趣看核心代碼,請直接跳到“檔案描述符總結”處。

核心剖析

(本文中引用的核心是linux-2.6.39的代碼,用于測試的作業系統為centos7 Linux 3.10.0-229.el7.x86_64)

當使用者調用open時,産生中斷進入核心态,核心接着調用這個函數​

​do_sys_open​

​:

long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
    struct open_flags op;
    int lookup = build_open_flags(flags, mode, &op); // 根據傳入的參數構造open_flags結構
    char *tmp = getname(filename); // 對檔案名做長度檢測
    int fd = PTR_ERR(tmp);

    if (!IS_ERR(tmp)) {
        fd = get_unused_fd_flags(flags); // 根據flags擷取一個空閑的fd,此處為關鍵之一
        if (fd >= 0) {
            struct file *f = do_filp_open(dfd, tmp, &op, lookup); // 打開指定檔案,并和之前擷取的fd關聯,此處為關鍵之二
            if (IS_ERR(f)) {
                put_unused_fd(fd);
                fd = PTR_ERR(f);
            } else {
                fsnotify_open(f);
                fd_install(fd, f);
            }
        }
        putname(tmp);
    }
    return fd;
}


get_unused_fd_flags
#define get_unused_fd_flags(flags) alloc_fd(0, (flags))
alloc_fd
int alloc_fd(unsigned start, unsigned flags)
{
    struct files_struct *files = current->files;// current指向目前程序的結構,而current->files管理着本程序所有打開的檔案
    unsigned int fd;
    int error;
    struct fdtable *fdt;

    spin_lock(&files->file_lock);
repeat:
    fdt = files_fdtable(files); // 擷取檔案描述表,由此可知每個程序都維護一張檔案描述符表,這張表的大小限制着本程序可以打開的檔案數
    fd = start;
    if (fd < files->next_fd)
        fd = files->next_fd;

    if (fd < fdt->max_fds) // 先嘗試在現有表範圍内尋找未使用的fd,fdt->max_fds是檔案描述符表裡最大的fd
        fd = find_next_zero_bit(fdt->open_fds->fds_bits,
                       fdt->max_fds, fd);

    // 擴充檔案描述表, 僅當現有表沒有未使用的fd時才擴充
    error = expand_files(files, fd);
    if (error < 0)
        goto out;

    ...

}



int expand_files(struct files_struct *files, int nr)
{
    struct fdtable *fdt;

    fdt = files_fdtable(files);

    /*
     * N.B. For clone tasks sharing a files structure, this test
     * will limit the total number of files that can be opened.
     */
    // rlimit(RLIMIT_NOFILE)的作用是擷取soft nofile,當新的fd大于soft nofile時傳回-EMFILE,EMFILE對應我們通常看到的錯誤資訊“Too many open files”
    if (nr >= rlimit(RLIMIT_NOFILE)) 
        return -EMFILE;

    /* Do we need to expand? */
    if (nr < fdt->max_fds) // 此處判斷是否需要擴充檔案描述符表
        return 0;

    /* Can we expand? */
    if (nr >= sysctl_nr_open) // sysctl_nr_open即/proc/sys/fs/nr_open。當fd大于nr_open時傳回EMFILE
        return -EMFILE;

    /* All good, so we try */
    return expand_fdtable(files, nr);
}      

從上面代碼可以看到,當fd大于​

​soft nofile​

​​或者大于​

​nr_open​

​​時,傳回EMFILE。​

​soft nofile​

​​和​

​nr_open​

​​都限制了fd的申請,差別在于,​

​nr_open​

​​的判斷是在需要擴充fdtable的時候,是以可以了解為​

​soft nofile​

​​直接限制打開fd的數目,而​

​nr_open​

​限制了fdtable的擴充(間接限制了fd的打開數目)。

另一點差別是​

​soft nofile​

​​是程序内部的參數,修改它不影響其他程序,而​

​nr_open​

​​是作業系統參數,修改它影響到系統的所有程序。上述代碼裡面的rlimit(RLIMIT_NOFILE)實際上是調用的​

​current->signal->rlim[limit].rlim_cur​

​​,current是目前程序的結構指針,是以soft nofile是程序級别的,而​

​sysctl_nr_open​

​​是核心全局變量,定義在file.c裡面:​

​int sysctl_nr_open __read_mostly = 1024*1024​

​,這個值可以通過sysctl來修改。

核心在限制fd時沒有用打開的fd數目來比較(核心也沒有專門的變量來記錄打開的fd數目)而是用fd直接比較,因為fd的申請是按從小到大順序的,是以用fd來做數目的比較可以達到相同的效果。

至此,我們已經看到了兩個參數​

​soft nofile​

​​和​

​nr_open​

​它們是在fd的申請過程中起作用,再來看下檔案的打開過程:

struct file *do_filp_open(int dfd, const char *pathname,
        const struct open_flags *op, int flags)
{
    struct nameidata nd;
    struct file *filp;

    // 實際的打開函數path_openat
    filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
    if (unlikely(filp == ERR_PTR(-ECHILD)))
        filp = path_openat(dfd, pathname, &nd, op, flags);
    if (unlikely(filp == ERR_PTR(-ESTALE)))
        filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
    return filp;
}      

​do_filp_open​

​​做了一層封裝,真正起作用的是​

​path_openat​

​,根據路徑來打開檔案,并傳回file指針:

static struct file *path_openat(int dfd, const char *pathname,
        struct nameidata *nd, const struct open_flags *op, int flags)
{
    struct file *base = NULL;
    struct file *filp;
    struct path path;
    int error;

    filp = get_empty_filp(); // 擷取空閑file指針,此處為關鍵
    if (!filp)
        return ERR_PTR(-ENFILE);

    filp->f_flags = op->open_flag;
    nd->intent.open.file = filp;
    nd->intent.open.flags = open_to_namei_flags(op->open_flag);
    nd->intent.open.create_mode = op->mode;

    error = path_init(dfd, pathname, flags | LOOKUP_PARENT, nd, &base);

    ...
}      

類似于fd的申請,檔案打開過程也有相似的函數​

​get_empty_filp()​

​,擷取空閑指針,進入這個函數看看:

struct file *get_empty_filp(void)
{
    const struct cred *cred = current_cred();
    static long old_max;
    struct file * f;

    /*
     * Privileged users can go above max_files
     */
    // get_nr_files擷取目前打開的檔案數,而files_stat.max_files對應file-max參數,可以在sysctl的代碼裡面找到
    // 下面這段代碼的意思是當目前程序沒有CAP_SYS_ADMIN權限的時候,才會比較file-max,
    // 而root使用者預設是有CAP_SYS_ADMIN權限的,這意味着當程式以root使用者啟動時,可打開的檔案數不受file-max限制。
    if (get_nr_files() >= files_stat.max_files && !capable(CAP_SYS_ADMIN)) {
        /*
         * percpu_counters are inaccurate.  Do an expensive check before
         * we go and fail.
         */
        // 更精确的計算打開的檔案數:所有cpu計數累加,這種操作有資料同步問題,更耗時,是以先用本cpu計數做判斷。
        if (percpu_counter_sum_positive(&nr_files) >= files_stat.max_files)
            goto over;
    }

    f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);
    if (f == NULL)
        goto fail;
    // 檔案打開,計數+1
    // 核心percpu counter機制,優化性能
    percpu_counter_inc(&nr_files);

    ...

    return f;

over:
    /* Ran out of filps - report that */
    // 當系統打開的fd超過file-max時,在log裡面看到的錯誤資訊就是從這裡輸出的。
    // old_max的作用是僅當fd超标且一直在增加的情況下才會輸出日志,減少的情況則不報日志,畢竟情況在變好嘛。
    if (get_nr_files() > old_max) {
        pr_info("VFS: file-max limit %lu reached\n", get_max_files());
        old_max = get_nr_files();
    }
    goto fail;

fail_sec:
    file_free(f);
fail:
    return NULL;
}      

從上面的代碼和注釋可以看出file-max是用來限制file結構的建立的,但當程序擁有​

​CAP_SYS_ADMIN​

​​權限的時候可以突破file-max限制(man 7

capabilities),在實際應用中表現為root啟動的程序不受file-max限制,也就是說nofile和nr_open有可能超過file-max,這種情況下,程序可打開的檔案數就會超越file-max,這将導緻其他非root啟動的程序資源耗盡。

到這裡一直沒有看到hard nofile,hard nofile沒有參與檔案的打開過程,它的作用僅僅是限制soft nofile的大小,看下setrlimit的代碼:

int do_prlimit(struct task_struct *tsk, unsigned int resource,
        struct rlimit *new_rlim, struct rlimit *old_rlim)
{
    struct rlimit *rlim;
    int retval = 0;

    if (resource >= RLIM_NLIMITS)
        return -EINVAL;
    if (new_rlim) {
        // rlim_cur是soft nofile,rlim_max是hard nofile,設定的soft nofile必須小于hard nofile,否則傳回失敗
        if (new_rlim->rlim_cur > new_rlim->rlim_max)
            return -EINVAL;
        // 下面這裡是個意外收獲,nr_open竟然和hard nofile有關系,而且hard nofile不能超過nr_open,否則傳回“Operation not permitted”
        if (resource == RLIMIT_NOFILE &&
                new_rlim->rlim_max > sysctl_nr_open)
            return -EPERM;
    }

    ...

    // 擷取目前程序的rlimit
    rlim = tsk->signal->rlim + resource;
    task_lock(tsk->group_leader);
    if (new_rlim) {
        // 如果目前程序沒有CAP_SYS_RESOURCE權限,則禁止設定的hard nofile超過現在的值
        // 在實際應用中表現為非root使用者無權提升hard nofile的值
        if (new_rlim->rlim_max > rlim->rlim_max &&
                !capable(CAP_SYS_RESOURCE))
            retval = -EPERM;
        if (!retval)
            retval = security_task_setrlimit(tsk->group_leader,
                    resource, new_rlim);
        ...
    }
    ... 
}      

通過上面的代碼,可以知道hard nofile的作用僅僅是限制soft nofile的大小,當我們調用setrlimit設定nofile的時候,如果傳入的soft nofile大于hard nofile則會傳回失敗。這樣聽起來似乎hard nofile沒什麼用因為很多時候我們都習慣于一起把soft nofile和hard nofile設定成一個值,但是比較安全的做法是隻修改soft nofile,非必須不要修改hard nofile,改小一般不是我們想要的,但是改大hard nofile甚至超過nr_open很容易造成系統資源耗盡,導緻其他程序和系統故障。最好先用getrlimit擷取現在的hard nofile,然後在setrlimit時傳入這個值,可以保證不修改hard nofile。

nofile、nr_open、file-max分别可以改多大?

上面提到nofile受限于​

​nr_open​

​​,那麼就先來看下​

​nr_open​

​,它的資料類型是int,理論上應該能達到2147483647,這個是int類型的最大值。

在設定nr_open之前先看下系統預設值:

[xuwei@localhost ~]$ cat /proc/sys/fs/nr_open
1048576      

1048576正好是1024*1024,跟核心代碼裡面的預設值一樣,現在把這個值改成2147483647:

root@localhost proc]# echo 2147483647 > /proc/sys/fs/nr_open
bash: echo: write error: Invalid argument      

出錯了,通過不斷減小這個值,測得的最大值為2147483584,即7FFFFFC0,也就是在MAXINT(2147483647)基礎上按64位元組對齊:

[root@localhost proc]# echo 2147483584 > /proc/sys/fs/nr_open
[root@localhost proc]# cat /proc/sys/fs/nr_open
2147483584      

是以​

​nr_open​

​最大值受int類型限制成立。

再看下nofile的最大值,先改hard nofile:

[root@localhost proc]# ulimit -n 2147483584 -H
[root@localhost proc]# ulimit -n 2147483585 -H
bash: ulimit: open files: cannot modify limit: Operation not permitted
[root@localhost proc]# ulimit -n -H
2147483584      

hard nofile的最大值正好等于剛才設定nr_open,符合預期,接着看下soft nofile:

[root@localhost proc]# ulimit -n 2147483584 -S
[root@localhost proc]# ulimit -n 2147483585 -S
bash: ulimit: open files: cannot modify limit: Invalid argument
[root@localhost proc]# ulimit -n -S
2147483584      

soft nofile的最大值等于hard nofile。

最後是file-max,它在核心裡面是unsigned long類型的整數,理論上可以達到​​18446744073709551615​​, 下面來測試下:

[root@localhost proc]# echo 18446744073709551615 > /proc/sys/fs/file-max
[root@localhost proc]# cat /proc/sys/fs/file-max
18446744073709551615
[root@localhost proc]# echo 18446744073709551616 > /proc/sys/fs/file-max
[root@localhost proc]# cat /proc/sys/fs/file-max
0      

可以修改成功,再大就回到0了,可見在設定file-max時核心沒有對邊界值做檢測。不過盡管file-max可以改設定成這高,但已經意義不大了,因為打開檔案時先比較nr_open,再比較file-max,是以實際可打開的檔案數永遠不會超過2147483584。

nofile、nr_open、file-max的大小關系

通過以上分析已經确定的是:

soft nofile <= hard nofile <= nr_open      

這個是核心代碼明确限制的,尚有疑惑的是nofile和file-max以及nr_open和file-max之間的大小關系。

當nofile > file-max時:

[root@localhost ~]# cat /proc/sys/fs/file-max
1000
[root@localhost ~]# ulimit -n
2147483584
[root@localhost ~]# echo a > a.txt      

雖然nofile的值大于file-max,但是隻要實際打開的檔案沒有超過file-max,即資源沒有耗盡時,是沒有任何關系的。雖然如此,nofile大于file-max是非常危險的,如果某個程序​​毫無節制​​的打開檔案就會導緻系統資源耗盡,進而導緻其他程序無資源可用,系統故障、無法登陸等嚴重問題。

當nr_open > file-max時:

[root@localhost xuwei]# cat /proc/sys/fs/file-max
97790
[root@localhost xuwei]# cat /proc/sys/fs/nr_open
1048576
[root@localhost xuwei]# echo 80000 > /proc/sys/fs/nr_open
[root@localhost xuwei]# cat /proc/sys/fs/nr_open
80000      

理論和實踐證明​

​nr_open​

​​和​

​file-max​

​​沒有直接的大小關系。雖然​

​nr_open​

​​和​

​file-max​

​​都是系統級參數,但是​

​file-max​

​​限制的是系統所有程序所打開的檔案的總數,它的額度是所有程序共用的,而​

​nr_open​

​​隻限制單個程序可打開的檔案數,每個程序有自己獨立的​

​nr_open​

​​額度。類似于​

​nofile​

​​,當​

​nr_open​

​​大于​

​file-max​

​時,很容易造成系統資源耗盡,導緻其他程序無法發檔案,系統無法登陸等嚴重問題。

檔案描述符總結

根據以上分析,檔案打開流程以及三個參數對其的影響如下圖所示:

分析單機最大長連接配接數

檔案的打開主要分兩步,即申請fd和建立檔案結構兩個過程,​

​nofile​

​​和​

​nr_open​

​​在第一個過程起作用,​

​file-max​

​​在第二個過程起作用。​

​nofile​

​​直接限制fd的申請,​

​nr_open​

​​限制檔案描述符表的擴充,間接限制了fd的申請,​

​file-max​

​限制檔案的實際建立過程。

​nofile​

​​,​

​nr_open​

​​,​

​file-max​

​這三個參數的差別如下:

分析單機最大長連接配接數

注:

  1. 系統參數的意思是,​

    ​nofile​

    ​​儲存在各個程序的結構裡,​

    ​nr_open​

    ​​和​

    ​file-max​

    ​儲存在系統變量裡。
  2. 額度共享,​

    ​nofile​

    ​​和​

    ​nr_open​

    ​​從程序級别限制,随着檔案的打開,它們額度的減少不影響其他程序,​

    ​file-max​

    ​​是系統級别的限制,一個程序多打開了一個檔案,其他程序可用的​

    ​file-max​

    ​額度就少了一個。

單機最大長連接配接數?

linux系統單機支援的tcp連接配接數主要受三個方面的限制:

  1. 檔案描述符的限制
  2. tcp本身的限制
  3. 系統記憶體限制

因為每個tcp連接配接都對應一個socket對象,而每個socket對象本身就占用一個檔案描述符,檔案描述符的限制在前文已經分析過,單機可以達到20+億,如果不考慮其他限制,單機支援的tcp長連接配接數就是20+億,這個值是非常可觀的,它絕對可以滿足世界上任何一個系統對長連接配接的需求,隻要一台機器就可以哦。

談到tcp本身的限制,就涉及到tcp四元組(遠端IP,遠端端口号,本地IP,本地端口号),它辨別一個tcp連接配接。根據常識了解,IP位址限定了一台主機(準确的說是網卡),端口号則限定了這個IP上的tcp連接配接。對于兩個tcp連接配接,四個參數中必然是有一個不同的,是以四元組的數目決定了tcp連接配接的個數。對于服務端程式來,一般來說,本地ip和本地端口号固定,是以它上面可接受的的連接配接數=2^32*65536=2^48(不考慮少量的特殊ip和特殊端口号),這也是個海量數字,基本可以支援世界上任何系統。對于用戶端程式來說,一般本地ip、遠端ip、遠端口号都是固定的,是以可以支援的長連接配接數最多隻有65536個,是以作為用戶端的tcp代理比較容易出現端口号耗盡問題。

linux系統對ip沒有限制,對端口号有限制,相關參數為​

​ip_local_port_range​

​:

[root@localhost xuwei]# cat /proc/sys/net/ipv4/ip_local_port_range
1024    65535      

這兩個值分别代表最小值和最大值,小于1024的端口号一般是預留給系統使用的,這不是強制的,你一定要把最小值改成小于1024也是可以的。

這個端口号範圍參數​

​ip_local_port_range​

​對于服務端程式沒太大意義,服務端監聽端口号一般也就幾個,對于用戶端來說,比如一些tcp代理程式,或壓測用戶端,這些程式通常會建立很多連接配接,這個參數就顯得很重要。

關于系統記憶體限制,主要是兩方面,一是tcp中繼資料的大小,包含sock、inode、file等結構;二是tcp緩存占用空間,這又包含系統緩存和使用者緩存,系統緩存是系統調用read/write使用的緩存,使用者的緩存是碼農在寫代碼時設計的緩沖區,在異步服務端程式裡面用于把讀寫和資料解析處理分離。

我做過測試,寫兩個程式,服務端隻接收連接配接,用戶端隻發起連接配接,不讀寫資料,用戶端和服務端分别部署在兩個虛拟機上,當建立50w個連接配接時,服務端消耗2g記憶體,大概每個socket占用4kb,這個4kb是核心申請的空間,并不增加使用者程序的記憶體。至于這個4kb是由哪個部分占用的,我還麼找到答案,sock、inode、file這些中繼資料結構加起來也就一兩百位元組,由于沒有收發資料是以跟tcp讀寫緩存關系不大,而且系統預設的讀寫緩沖區大小均為80k+。

linux檔案描述符限制和單機最大長連接配接數