配置支援高併發TCP連線的Linux伺服器全攻略

工程師WWW發表於2015-01-13

隨著internet的發展,更高效率、更多併發連線數的服務已經成為當前迫切的需要。我們雖然可以採用UDP這樣的無連線協議來解除連線限制(事實上這個限制也同樣是存在的,尤其是當一個UDP處理需要佔用不少CPU處理時間的情況下其上限也就越小),但是這樣的方案並不能解決所有問題(例如從服務端發出給連線客戶端的及時訊息)。  

    解決這個問題的一種途徑是執行緒池(Thread Pool),並在各個執行緒中執行單獨的select()以實現對網路事件的多路分離。select其實的內部機制依賴於fd_set,說白了就是一個陣列,select通過遍歷這個陣列中的所有socket handle,通過ioctl判斷該handle是否被啟用的socket事件。因此,我們可以對select得出兩個結論: 
      1)select可以在多執行緒中分離使用,但是我們需要實現一個機制,以便在多個執行緒之間平均的劃分socket handles,從而在各個執行緒之間平均分攤負載。 
      2)由於需要不停的(迴圈的)遍歷一個fd_set陣列,當這個陣列變得很大的時候select就會顯得效率低下(簡直難以忍受,這就是為什麼在windows中FD_SETSIZE的預設大小為64的原因)  
      從以上的結論我們看到,採用select和執行緒池的方式解決以上問題應該是一種方法。但是我想這個問題還沒完,因為這還不是我看來最有高效的解決方案。為什麼這麼說呢,首先select的選擇能力有限,假設我們使用最大的FD_SETSIZE=1024,這樣,我們需要10個左右不停迴圈的執行緒,由於每個執行緒每次遍歷fd_set所花費的時間較長,10個不停迴圈的執行緒將很有可能使服務程式阻塞,佔用大量的CPU資源,由於我們不是單純的保持這些TCP連線就萬事大吉了,我們還得對這些連線的請求進行處理,而伺服器恐怕此時已經難以承擔(當然,採用提高硬體配置、多CPU的方式可以有效解決這一問題,但這不是我們做軟體的應該帶給系統的其他成員的。如果採用FD_SETSIZE=64的方案呢?這樣我們將線上程池中執行大約16*10=160個執行緒,考慮執行緒間切換和同步開銷,該方案仍然是比較難忍受的。  
      如何解決這個問題?從目前的情況看,
問題主要還是存在於select這一古老socket api 函式的瓶頸上。(畢竟執行緒間切換和同步開銷問題是無法解決的)。於是我們考慮是否可以用poll或epoll多路分離來解決這個瓶頸問題。poll和select其實是採用的同種機制,因此其效率不會比select高多少。epoll提供Edge Triggered ( ET ) 和Level Triggered ( LT )兩種方式的多路事件分離方法,使用LT方式時,其效果與select和poll相似,頂多可以看成是一個更快的poll;但在使用ET方式時其效率在處理具有大量idle connection的時候明顯高於select和poll,在處理always busy connection等情況時,效率與select和poll沒有多大區別。(關於這些效能的比較可以參考[1]中的Fig5和Fig6)當然,epoll比起poll和select來說預設的最大FD大小更大,不過這其實不是問題,因為我們都可以修改其水平邊界的大小的。補充一點,由於epoll、poll和select都使用檔案描述符fd作為陣列的索引,而在普通系統中一個FD的定義是integer,也就是說它是存在上限的,在C中一個int通常是16位的,也就是說最大檔案控制程式碼數32768(不是65536哦;-)構成了單個fd_set的上限。見[2] 
      實際情況下,很少有服務的連線是always busy connection的,也就是說客戶端一但建立和與服務端的TCP連線後,該連線在很多時侯是處於idle connection狀態的,它只是在有資料需要傳送的時候才處於busy connection狀態的。換句話說,在實際服務情況下,使用ET模式的epoll(以下的epoll都是指在ET模式下的)比使用poll和select具有更高的效率。這樣看來,使用epoll比使用select或poll在單執行緒中具有更高的處理效率和更大的處理連線數。它僅使用單個執行緒就已經能處理前面問題中所提到的併發10K連線數問題。          
      至此,前面提出的10K連線數問題已經基本解決。但是我認為這還只是個開始,我們常見一些服務能夠支援1M以上的同時連線數。為達到這個目的,我們需要將執行緒池和epoll結合起來,即,在多個執行緒中使用epoll_wait[3]等待多個socket events的到達。這帶來了另外一些深層次的問題: 
      1)socket descript的限制,在大多數系統中SOCKET對應的是integer,其連線數是有上限的;      

      2)執行緒間同步及流量分配問題,需要為每個執行緒合理的分配連線以達到負載平衡;       

      3)服務質量問題,防止出現餓死現象。  

1、修改使用者程式可開啟檔案數限制

在Linux平臺上,無論編寫客戶端程式還是服務端程式,在進行高併發TCP連線處理時,最高的併發數量都要受到系統對使用者單一程式同時可開啟檔案數量的限制(這是因為系統為每個TCP連線都要建立一個socket控制程式碼,每個socket控制程式碼同時也是一個檔案控制程式碼)。可使用ulimit命令檢視系統允許 當前使用者程式開啟的檔案數限制:
[speng@as4 ~]$ ulimit -n
1024
這表示當前使用者的每個程式最多允許同時開啟1024個檔案,這1024個檔案中還得除去每個程式必然開啟的標準輸入,標準輸出,標準錯誤,伺服器監聽 socket,程式間通訊的unix域socket等檔案,那麼剩下的可用於客戶端socket連線的檔案數就只有大概1024-10=1014個左右。 也就是說預設情況下,基於Linux的通訊程式最多允許同時1014個TCP併發連線。

對於想支援更高數量的TCP併發連線的通訊處理程式,就必須修改Linux對當前使用者的程式同時開啟的檔案數量的軟限制(soft limit)和硬限制(hardlimit)。其中軟限制是指Linux在當前系統能夠承受的範圍內進一步限制使用者同時開啟的檔案數;硬限制則是根據系統 硬體資源狀況(主要是系統記憶體)計算出來的系統最多可同時開啟的檔案數量。通常軟限制小於或等於硬限制。

修改上述限制的最簡單的辦法就是使用ulimit命令:
[speng@as4 ~]$ ulimit -n <file_num>
上述命令中,在<file_num>中指定要設定的單一程式允許開啟的最大檔案數。如果系統回顯類似於“Operation notpermitted”之類的話,說明上述限制修改失敗,實際上是因為在<file_num>中指定的數值超過了Linux系統對該使用者 開啟檔案數的軟限制或硬限制。因此,就需要修改Linux系統對使用者的關於開啟檔案數的軟限制和硬限制。

第一步,修改/etc/security/limits.conf檔案,在檔案中新增如下行:
speng soft nofile 10240
speng hard nofile 10240
其中speng指定了要修改哪個使用者的開啟檔案數限制,可用'*'號表示修改所有使用者的限制;soft或hard指定要修改軟限制還是硬限制;10240則指定了想要修改的新的限制值,即最大開啟檔案數(請注意軟限制值要小於或等於硬限制)。修改完後儲存檔案。

第二步,修改/etc/pam.d/login檔案,在檔案中新增如下行:
session required /lib/security/pam_limits.so
這是告訴Linux在使用者完成系統登入後,應該呼叫pam_limits.so模組來設定系統對該使用者可使用的各種資源數量的最大限制(包括使用者可開啟 的最大檔案數限制),而pam_limits.so模組就會從/etc/security/limits.conf檔案中讀取配置來設定這些限制值。修改 完後儲存此檔案。

第三步,檢視Linux系統級的最大開啟檔案數限制,使用如下命令:
[speng@as4 ~]$ cat /proc/sys/fs/file-max
12158
這表明這臺Linux系統最多允許同時開啟(即包含所有使用者開啟檔案數總和)12158個檔案,是Linux系統級硬限制,所有使用者級的開啟檔案數限制 都不應超過這個數值。通常這個系統級硬限制是Linux系統在啟動時根據系統硬體資源狀況計算出來的最佳的最大同時開啟檔案數限制,如果沒有特殊需要,不 應該修改此限制,除非想為使用者級開啟檔案數限制設定超過此限制的值。修改此硬限制的方法是修改/etc/rc.local指令碼,在指令碼中新增如下行:
echo 22158 > /proc/sys/fs/file-max
這是讓Linux在啟動完成後強行將系統級開啟檔案數硬限制設定為22158。修改完後儲存此檔案。

完成上述步驟後重啟系統,一般情況下就可以將Linux系統對指定使用者的單一程式允許同時開啟的最大檔案數限制設為指定的數值。如果重啟後用 ulimit-n命令檢視使用者可開啟檔案數限制仍然低於上述步驟中設定的最大值,這可能是因為在使用者登入指令碼/etc/profile中使用 ulimit-n命令已經將使用者可同時開啟的檔案數做了限制。由於通過ulimit-n修改系統對使用者可同時開啟檔案的最大數限制時,新修改的值只能小於 或等於上次ulimit-n設定的值,因此想用此命令增大這個限制值是不可能的。所以,如果有上述問題存在,就只能去開啟/etc/profile指令碼文 件,在檔案中查詢是否使用了ulimit-n限制了使用者可同時開啟的最大檔案數量,如果找到,則刪除這行命令,或者將其設定的值改為合適的值,然後儲存文 件,使用者退出並重新登入系統即可。
通過上述步驟,就為支援高併發TCP連線處理的通訊處理程式解除關於開啟檔案數量方面的系統限制。

2、修改網路核心對TCP連線的有關限制

在Linux上編寫支援高併發TCP連線的客戶端通訊處理程式時,有時會發現儘管已經解除了系統對使用者同時開啟檔案數的限制,但仍會出現併發TCP連線數增加到一定數量時,再也無法成功建立新的TCP連線的現象。出現這種現在的原因有多種。

第一種原因可能是因為Linux網路核心對本地埠號範圍有限制。此時,進一步分析為什麼無法建立TCP連線,會發現問題出在connect()呼叫返 回失敗,檢視系統錯誤提示訊息是“Can't assign requestedaddress”。同時,如果在此時用tcpdump工具監視網路,會發現根本沒有TCP連線時客戶端發SYN包的網路流量。這些情況 說明問題在於本地Linux系統核心中有限制。其實,問題的根本原因在於Linux核心的TCP/IP協議實現模組對系統中所有的客戶端TCP連線對應的本地埠號的範圍進行了限制(例如,核心限制本地埠號的範圍為1024~32768之間)。當系統中某一時刻同時存在太多的TCP客戶端連線時,由於每 個TCP客戶端連線都要佔用一個唯一的本地埠號(此埠號在系統的本地埠號範圍限制中),如果現有的TCP客戶端連線已將所有的本地埠號佔滿,則此 時就無法為新的TCP客戶端連線分配一個本地埠號了,因此係統會在這種情況下在connect()呼叫中返回失敗,並將錯誤提示訊息設為“Can't assignrequested address”。有關這些控制邏輯可以檢視Linux核心原始碼,以linux2.6核心為例,可以檢視tcp_ipv4.c檔案中如下函式:
static int tcp_v4_hash_connect(struct sock *sk)
請注意上述函式中對變數sysctl_local_port_range的訪問控制。變數sysctl_local_port_range的初始化則是在tcp.c檔案中的如下函式中設定:
void __init tcp_init(void)
核心編譯時預設設定的本地埠號範圍可能太小,因此需要修改此本地埠範圍限制。
第一步,修改/etc/sysctl.conf檔案,在檔案中新增如下行:
net.ipv4.ip_local_port_range = 1024 65000
這表明將系統對本地埠範圍限制設定為1024~65000之間。請注意,本地埠範圍的最小值必須大於或等於1024;而埠範圍的最大值則應小於或等於65535。修改完後儲存此檔案。
第二步,執行sysctl命令:
[speng@as4 ~]$ sysctl -p
如果系統沒有錯誤提示,就表明新的本地埠範圍設定成功。如果按上述埠範圍進行設定,則理論上單獨一個程式最多可以同時建立60000多個TCP客戶端連線。

第二種無法建立TCP連線的原因可能是因為Linux網路核心的IP_TABLE防火牆對最大跟蹤的TCP連線數有限制。此時程式會表現為在 connect()呼叫中阻塞,如同當機,如果用tcpdump工具監視網路,也會發現根本沒有TCP連線時客戶端發SYN包的網路流量。由於 IP_TABLE防火牆在核心中會對每個TCP連線的狀態進行跟蹤,跟蹤資訊將會放在位於核心記憶體中的conntrackdatabase中,這個資料庫 的大小有限,當系統中存在過多的TCP連線時,資料庫容量不足,IP_TABLE無法為新的TCP連線建立跟蹤資訊,於是表現為在connect()呼叫 中阻塞。此時就必須修改核心對最大跟蹤的TCP連線數的限制,方法同修改核心對本地埠號範圍的限制是類似的:
第一步,修改/etc/sysctl.conf檔案,在檔案中新增如下行:
net.ipv4.ip_conntrack_max = 10240
這表明將系統對最大跟蹤的TCP連線數限制設定為10240。請注意,此限制值要儘量小,以節省對核心記憶體的佔用。
第二步,執行sysctl命令:
[speng@as4 ~]$ sysctl -p
如果系統沒有錯誤提示,就表明系統對新的最大跟蹤的TCP連線數限制修改成功。如果按上述引數進行設定,則理論上單獨一個程式最多可以同時建立10000多個TCP客戶端連線。

3、使用支援高併發網路I/O的程式設計技術

在Linux上編寫高併發TCP連線應用程式時,必須使用合適的網路I/O技術和I/O事件分派機制。

可用的I/O技術有同步I/O,非阻塞式同步I/O(也稱反應式I/O),以及非同步I/O。在高TCP併發的情形下,如果使用同步I/O,這會嚴重阻塞 程式的運轉,除非為每個TCP連線的I/O建立一個執行緒。但是,過多的執行緒又會因系統對執行緒的排程造成巨大開銷。因此,在高TCP併發的情形下使用同步I /O是不可取的,這時可以考慮使用非阻塞式同步I/O或非同步I/O。非阻塞式同步I/O的技術包括使用select(),poll(),epoll等機 制。非同步I/O的技術就是使用AIO。

從I/O事件分派機制來看,使用select()是不合適的,因為它所支援的併發連線數有限(通常在1024個以內)。如果考慮效能,poll()也是 不合適的,儘管它可以支援的較高的TCP併發數,但是由於其採用“輪詢”機制,當併發數較高時,其執行效率相當低,並可能存在I/O事件分派不均,導致部 分TCP連線上的I/O出現“飢餓”現象。而如果使用epoll或AIO,則沒有上述問題(早期Linux核心的AIO技術實現是通過在核心中為每個I /O請求建立一個執行緒來實現的,這種實現機制在高併發TCP連線的情形下使用其實也有嚴重的效能問題。但在最新的Linux核心中,AIO的實現已經得到 改進)。

綜上所述,在開發支援高併發TCP連線的Linux應用程式時,應儘量使用epoll或AIO技術來實現併發的TCP連線上的I/O控制,這將為提升程式對高併發TCP連線的支援提供有效的I/O保證。

相關文章