應用場景
聊天室或即時消息推送系統等,因爲很多消息需要到產生時才推送給客戶端,所以當沒有消息產生時,就需要hold住客戶端的連接,這樣,當有大量的客戶端時,要hold住大量的長連接。
服務器配置
此處我們按照10M併發連接爲目標進行配置。
一般服務器默認限制1024個文件句柄,也就是最多支持1024個併發長連接,在root用戶下編輯/etc/security/limits.conf文件,修改:
* soft nofile 1048576
* hard nofile 1048576
·soft是一個警告值,而hard則是一個真正意義的閾值,超過就會報錯。
·soft 指的是當前系統生效的設置值。hard 表明系統中所能設定的最大值
·nofile – 單進程打開文件的最大數目
·星號表示針對所有用戶,若僅針對某個用戶登錄ID,請替換星號
這樣理論上10個進程可以達到10m個併發長連接,但是在測試時會發現,併發數最多隻能到達28200左右,此時,需要修改默認的本地端口範圍。
linux系統端口範圍爲0-65536,系統提供了默認的端口範圍:
cat /proc/sys/net/ipv4/ip_local_port_range
32768 61000
故當前端口使用範圍爲61000-32768約爲28200個,將端口範圍擴大,修改/etc/sysctl.conf,增加一行:
net.ipv4.ip_local_port_range= 1024 65535
保存,使之生效
sysctl –p
接着在測試端程序發出的連接數量大於某個值(大概爲40萬時)是,通過dmesg命令查看會得到大量警告信息:[warn]socket: Too many openfiles in system
此時需要修改file-max,表示系統所有進程最多允許同時打開所有的文件句柄數,系統級硬限制。添加fs.file-max = 1048576到/etc/sysctl.conf中,sysctl -p保存並使之生效。
在服務端連接達到一定數量時,通過查看dmesg命令查看,發現大量TCP: toomany of orphaned sockets錯誤,此時需要調整tcp socket參數,添加:
net.ipv4.tcp_mem= 786432 2097152 3145728
net.ipv4.tcp_rmem= 4096 4096 16777216
net.ipv4.tcp_wmem= 4096 4096 16777216
net.ipv4.tcp_max_orphans= 131072
net.ipv4.tcp_rmem用來配置讀緩衝的大小,三個值,第一個是這個讀緩衝的最小值,第三個是最大值,中間的是默認值。我們可以在程序中修改讀緩衝的大小,但是不能超過最小與最大。爲了使每個socket所使用的內存數最小,我這裏設置默認值爲4096。
net.ipv4.tcp_wmem用來配置寫緩衝的大小。
讀緩衝與寫緩衝的大小,直接影響到socket在內核中內存的佔用。
而net.ipv4.tcp_mem則是配置tcp的內存大小,其單位是頁,1頁等於4096字節。三個值分別爲low, pressure, high
·low:當TCP使用了低於該值的內存頁面數時,TCP不會考慮釋放內存。
·pressure:當TCP使用了超過該值的內存頁面數量時,TCP試圖穩定其內存使用,進入pressure模式,當內存消耗低於low值時則退出pressure狀態。
·high:允許所有tcp sockets用於排隊緩衝數據報的頁面量,當內存佔用超過此值,系統拒絕分配socket,後臺日誌輸出“TCP: too many of orphaned sockets”。
一般情況下這些值是在系統啓動時根據系統內存數量計算得到的。 根據當前tcp_mem最大內存頁面數是1864896,當內存爲(1864896*4)/1024K=7284.75M時,系統將無法爲新的socket連接分配內存,即TCP連接將被拒絕。實際測試環境中,據觀察大概在99萬個連接左右的時候(零頭不算),進程被殺死,觸發outof socket memory錯誤(dmesg命令查看獲得)。每一個連接大致佔用7.5K內存(下面給出計算方式),大致可算的此時內存佔用情況(990000 * 7.5/ 1024K = 7251M)。這樣和tcp_mem最大頁面值數量比較吻合,因此此值也需要修改。
另外net.ipv4.tcp_max_orphans這個值也要設置一下,這個值表示系統所能處理不屬於任何進程的 socket數量,當我們需要快速建立大量連接時,就需要關注下這個值了。當不屬於任何進程的socket的數量大於這個值時,dmesg就會看 到”too many of orphaned sockets”。
綜上,服務端需要配置的內容做個彙總:
/etc/sysctl.conf 添加:
fs.file-max= 10485760
net.ipv4.ip_local_port_range= 1024 65535
net.ipv4.tcp_mem= 786432 2097152 3145728
net.ipv4.tcp_rmem= 4096 4096 16777216
net.ipv4.tcp_wmem= 4096 4096 16777216
net.ipv4.tcp_max_orphans= 131072
/etc/security/limits.conf 修改:
* soft nofile 1048576
* hardnofile 1048576
線上測試
使用ucloud雲主機進行測試,服務器配置:
啓動壓測前系統資源情況:
使用https://github.com/yedf/handy 庫自帶的測試程序,進行單機併發長連接測試,該庫在linux系統上使用epoll,在MacOSX上使用kqueue。
選取一臺主機S作爲服務器,運行服務器
10m/10m-svr100 300 10 301 #啓動10個子進程,每個進程分別監聽100-300的端口。
選取另一臺主機C作爲客戶端,運行客戶端,(S爲服務器的內網ip)
#啓動10個客戶端子進程,連接到S的100-300端口,發起10m個連接,在500秒內創建所有的連接,每600秒發送一個心跳,心跳數據爲64字節,多進程的管理端口爲301。
10m/10m-cli<S> 100 300 10000000 500 10 600 64 301
在服務器端使用watch ss –s 發現tcp連接數持續上升,最終穩定在4m左右
消耗資源:
系統佔用了20G左右的內存,但cpu佔用極少
客戶端報錯:
0m-cli:handy/logging.cc:164: void handy::Logger::logv(int, const char*, int, constchar*, const char*, ...): Assertion `0' failed.
2017/05/03-15:54:58.4048281a46 FATAL handy/poller.cc:35 epoll_ctl add failed 28 No space left on device
2017/05/03-15:54:58.4048121a40 FATAL handy/poller.cc:35 epoll_ctl add failed 28 No space left on device
2017/05/03-15:54:58.4048251a45 FATAL handy/poller.cc:35 epoll_ctl add failed 28 No space left on device
此錯誤一般是磁盤滿導致,但是在這裏是客戶端在進行epoll_ctl時,內存已滿導致註冊epoll事件失敗,所以客戶端此時停止繼續創建連接,可見此時的瓶頸出現在壓測的客戶端,如果客戶端內存夠用,理論上服務端10m個併發長連接應該可以實現。
說明:
單進程最大文件數量限制:ulimit -n 最多能把這個數字修改到1048575,因此單個進程最多能夠打開百萬個文件,千萬併發連接需要千萬個文件描述符,於是我們使用多進程來做到千萬文件的支持。
多進程之間的負載均衡:nginx使用多進程來增加自己的吞吐量,原先採用共享鎖的方式來平衡負載,對核數較多的服務器,較多的進程並沒有達到性能的線性提升。最新的linux內核引入了SO_REUSEPORT選項,該選項可以自動平衡監聽同一端口的多進程,是內核級的解決方案。handy採用該方案,優於nginx的舊有方式(最新的nginx也支持SO_REUSEPORT)。
測試中客戶端本地端口不夠:讓服務器監聽了200個端口,這樣客戶端連接服務器的每個端口只有50k個連接,然後加大默認的本地端口範圍就可以滿足要求(見前面的服務器系統參數)。
測試中如果一次性創建千萬個連接,則絕大部分的連接創建都會失敗,因此讓客戶端每100ms創建2000個連接,提高連接創建的成功率。