導語

本文講述了 Nginx 作爲負載均衡組件,在應對超高併發的情況下所遇到性能問題,以及針對這些具體問題的分析、定位、並最終通過整體調優 kernel、網卡、Nginx 參數,達到 QPS 從20000+ 提高到 40000+ 的完整過程。。

背景

58 集團房產 HBG 有一些內網 API ,通訊使用的是 HTTP協議,並且使用 Nginx 做負載均衡。隨着業務增長,發現高峯期有很多 HTTP 錯誤,其中最多的錯誤反映在 PHP curl 中就是 “Connection timed out after xx ”,但此時 Nginx 服務器本身負載卻非常低,因此肯定不是服務器出現瓶頸導致超時的問題,基於此背景需要對 Nginx 做網絡調優,來解決超時的問題。

分析問題

前面提到了業務出現最多的錯誤是 “Connection timed out after xx”,這個錯誤其實就是 TCP 握手失敗,握手失敗其實就是丟包,丟包最常見原因如下

A、網卡丟包(包含syn 包和數據包)

B、內核 syn 包丟失

C、中間鏈路丟包,比如交換機、路由器

本文章重點說明原因A和B。

解決問題

1、 解決網卡丟包的問題 我們能從 ifconfig 命令中看到網卡的相關統計信息,其中和錯誤相關的有 3 個分別是 overruns、dropped 、errors。

A、errors : 收到的數據包錯誤,比如 CRC 校驗錯誤,不常見

B、dropped : 從網卡fifo 隊列複製到 內核空間錯誤,比如內核無內存可用,不常見

C、overruns:數據進入網卡 fifo 隊列時被丟棄(就是沒有進入網卡)最常見的錯誤,常見的原因就是中斷不均衡,默認內核將所有的網卡中斷都綁定在 CPU core 0 ,高併發下 CPU core 0 有太多中斷,當處理不過來時,fifo 隊列沒有被及時消費,後續數據沒有進入網卡直接被丟棄。

1.1 網卡 overruns 如何解決

前分析網卡 overruns 是因爲中斷都默認綁定在了 CPU core 0 ,比如下圖 CPU core 0 全部時間都在處理軟中斷,肯定有很多中斷來不及響應。

所以只要將網卡中斷和 CPU core num 進行一一綁定就行,舉例如下

$ cat /proc/interrupts | grep eth | awk '{print $1,$NF}'

77: eth0-0

78: eth0-1

79: eth0-2

80: eth0-3

81: eth0-4

82: eth0-5

83: eth0-6

84: eth0-7


$ echo 0 > /proc/irq/77/smp_affinity_list

$ echo 1 > /proc/irq/78/smp_affinity_list

.....

2、解決內核 syn 包丟失的問題 通過抓包發現,基本上都是 TCP 握手的第一個 syn 包發了兩次,並且間隔 1s ,這種現象很明顯就是包丟棄,因爲抓包能抓到,所以第一個被明確丟棄,具體如下圖

內核針對 syn 包丟棄有 3 個統計參數 TcpExtListenOverflows、TcpExtListenDrops、TcpExtTCPBacklogDrop,如何查看如下

$ nstat -za | grep -Ei '(TcpExtListenOverflows|TcpExtListenDrops|TcpExtTCPBacklogDrop)'

TcpExtListenOverflows 0 0.0

TcpExtListenDrops 0 0.0

TcpExtTCPBacklogDrop 0 0.0

可以看到 syn 包丟失有多種原因,下面逐步分析其中典型的 3 個原因。

2.1 syn 包丟失 - TcpExtListenOverflows & TcpExtListenDrops 同時增大

此案例比較簡單,從 TcpExtListenOverflows 明顯可以看出是溢出導致,具體就是 TCP 三次握手成功的 accept-queue 滿了,nginx 還沒有來得及 accept ,此時新進來的握手請求只能丟棄,具體如下圖

很明顯這種 case 只需要適當增加 accept-queue 大小就行
acccept-queue = min(nginx backlog, net.core.somaxconn)
nginx backlog 默認 511
net.core.somaxconn 默認 128
如果系統未修改參數,那麼默認 accept-queue= 128 ,這個值太小了,一般 2048 比較合適 具體調整如下,網絡上介紹的文章比較多
注:nginx backlog 是全局配置 ,一個 port 只需要在一個地方添加即可
案例 2.1 解決方案

# linux kernel

sysctl -w net.core.somaxconn=2048

sysctl -w net.ipv4.tcp_max_syn_backlog=4096


# nginx

server {

listen 80 backlog=2048

.....

}

2.2 syn 包丟失 - TcpExtListenOverflows 不變 & TcpExtListenDrops增大
相比 3.1.1 案例,這個案例 TcpExtListenOverflows 不變,原因更加複雜。當時解決問題的時候也是偶然發現內核有錯誤堆棧,才分析出具體原因。
具體原因是 TCP 的內存分配採用的是 slab + buddy算法,slab 塊不夠時,通過buddy 內存分配算法申請內存,如果高併發情況下 buddy 分配的內存剛好用完(此時系統仍然有可用的 free 內存),此時會因爲申請不到 buddy 內存而直接丟棄TCP 請求,具體堆棧如下,這個堆棧網上能搜到很多,有其他程序比如 MySQL 也會出現

這個堆棧有幾個重要的信息
A、nginx tcp_v4_syn_recv_sock (第一個syn 請求過來) 時申請內存失敗
B、TCP 申請內存用的是 slab 算法 (kmem_cache_alloc)
C、kmem_cache_alloc申請失敗時,通過 buddy 內存算法來補充 slab 空間
D、申請內存的 buddy order = 1 表示需要申請的內存大小是 2 * 4k = 8k
簡單介紹下 buddy 內存算法
buddy 內存情況可以通過 /proc/buddyinfo 來查看

簡單說明 第 3 列 556 表示 numa node 0 Normal 區域 order = 2 有 556 塊,佔用大小是 556 * 2 * 2 * 4K,也就是有556 個連續的16K內存
buddy 內存分配原則是高級別可以給低級別用 ,但是低級別不能給高級別用
比如 order = 1 可用,order = 0 不可用,此時如果申請 order = 0,會從 order = 1 借用1個,拆成2個,1個給申請方,另外一個劃歸 order = 0
buddy 內存是預分配的,可能在高併發瞬間用完,這個時候就會有內存分配失敗的情況。
下面的圖是出問題時 nginx 的 buddyinfo 監控

可以發現 Normal 區域 order = 0 有 174851 塊,但是 order > 0 都沒有可用內存,nginx 剛好要申請的 order = 1,所以申請內存失敗,syn 包丟棄
針對這種情況阿里的工程師給了一個解決方案

# numa 在當前node回收內存

sysctl -w vm.zone_reclaim_mode=1

# 調大系統最小內存閾值

sysctl -w vm.min_free_kbytes=512000

真實測試下來並沒有起到做大作用,只能再進行深入分析。首先發現 Nginx 機器物理內存非常大有 128G,進一步分析發現大部分內存都處於 cache 模式,此時可以聯想到大部分內存都因爲寫巨大的 nginx 日誌而處於文件 cache 模式,解決問題的關鍵變成爲儘快釋放 cache 緩存。
通過 echo 1 > /proc/sys/vm/drop_caches 釋放 cache 會瞬間影響機器負載,還好 linux 有個 vm.extra_free_kbytes 參數,可以控制回收內存的時機
案例 2.2 解決方案

# 設置更大的水位線,讓 kswapd 提前啓動,

# 比如我們nginx 服務器內存是128G,但是由於寫入的日誌非常大,大量的日誌

# 使得內存都被劃分到 cache 區域,只有 kswapd 啓動後纔會回收

sysctl -w vm.extra_free_kbytes= 1048576


# 調大系統最小內存閾值

sysctl -w vm.min_free_kbytes=1048576

2.3 syn 包丟失 - TcpExtTCPBacklogDrop 增大
相比前面2個 案例 3.2.1、3.2.2, 這個更加複雜也難易理解,主要是因爲是在多核多進程 lock 時發生,先看下面流程圖,相比 3.2.1 的圖更加複雜,當某一個核 比如 CPU core 0 處於 accept ->lock 狀態, 這個時候,其他 CPU core 收到的 syn 數據包不能直接進入 syn-queue ,而是進入到了 backlog-queue ,backlog-queue 的大小是sk_rcvbuf + sk_sndbuf

假如默認內核參數如下
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_wmem=4096 65536 16777216
那麼默認 backlog-queue = 87380 + 65536 = 152916
看起來還挺大,但是高併發下面其實還是比較小的,backlog 每次進去的數據大小是 800 ~ 1800,不同網卡驅動有些區別,也就是lock 期間最多進入的請求爲 152916/{800,1800} = 190 ~ 80,確實有些小。
2種方式增大 backlog-queue
A、調整內核參數 ,修改中間的值 net.ipv4.tcp_rmem & net.ipv4.tcp_wmem
B、nginx 提供了修改的方式,比如如下 server backlog=2048 rcvbuf=131072 sndbuf=131072
修改後的效果,可以看出增大 rcvbuf & sndbuf 還是很有效果 但是有一個機器有些問題,效果不明顯

繼續分析
上面看到,之所以會產生 TcpExtTCPBacklogDrop 是因爲多 CPU core 同時 accept,如果有 1 個 cpu core lock 了 sock,會導致其他 CPU core 收到請求後只能放到 backlog-queue,如果降低lock ,那麼就從根本上解決, Nginx 提供了 reuseport 的功能(需要內核支持 SO_REUSEPORT)
無 reuseport 的情況如下圖,多個 cpu core 競爭 一個 sock->accept-queue,

使用 reuseport 後如下圖,每個cpu core 都分配 1 個一套資源,避免了競爭

開啓 reuseport 後,nginx 同一個監聽端口會出現多條記錄,也就對應多個內核 sock,並且多個 accept-queue,syn-queue,backlog-queue。

效果如下圖

2.3 解決方案總結
增加 reuseport 選項

# nginx

server {

listen 80 backlog=2048 rcvbuf=131072 sndbuf=131072 reuseport;

.....

}

3、優化總結

1. 通過綁定網卡中斷號到 CPU core ,解決網卡 overruns

2. 調整 backlog 來解決 TcpExtListenOverflows++ & TcpExtListenDrops++

3. 調整 vm.extra_free_kbytes && vm.min_free_kbytes 解決 buddy memory 分配失敗的問題導致的 TcpExtListenDrops++

4. nginx 增大 rcvbuf & sndbuf ,並且開啓 reuseport 來解決 TcpExtTCPBacklogDrop++

4、內存說明

4.1 backlog 每次進去的數據大小是 800 ~ 1800
backlog 進入的是一個數據包,也就是 1 個 sk_buff,1個 sk_buff會包含 (sizeof sk_buff)+ data (比如 1500 以太包最大值),此時並沒有進入 TCP 棧處理。
4.2 申請內存的 buddy order = 1 表示需要申請的內存大小是 2 * 4k = 8k
內核內存的特點就是大小固定(因爲結構體都是固定大小),針對這種場景內核設計了 slab 內存算法,將大塊內存分割成固定的小塊,大塊內存採用 buddy 算法,小塊內存採用 slab 算法,比如固定申請大小爲 2600 大小的內存,就可以採用 2個連續的4K內存 2700 * 3 = 8100 ,這樣可以分成3個小塊,實際其實更加複雜,可以參考後面的參考文獻,slab 的情況可以查看 /proc/slabinfo
slab 情況查看如下

總結

因爲篇幅原因每個案例只能簡單介紹,繼續深挖還可以寫很多內容,比如 backlog 爲啥 511 有點小。Nginx 調優的地方有很多,本文章只介紹了網絡的部分,其中部分是網絡上已經很成熟的方案,其他的都是自己通過實踐總結的,並且起到作用。調優本身是一個實踐的過程,本文講述的內容都是實踐過並且有用的,但是不一定在其他場景100%適用,不過分析過程可以作爲參考。

參考文獻

Linux Used內存到底哪裏去了?:http://blog.yufeng.info/archives/2456

Linux內核分析:頁回收導致的cpu load瞬間飆高的問題分析與思考 :http://www.mamicode.com/info-detail-1420432.html

夥伴系統之避免碎片--Linux內存管理(十六) :https://blog.csdn.net/gatieme/article/details/52694362

Linux page allocation failure 的問題處理 - zonereclaimmode :https://yq.aliyun.com/articles/228285/

Linux服務器Cache佔用過多內存導致系統內存不足問題的排查解決(續):https://www.cnblogs.com/panfeng412/p/drop-caches-under-linux-system-2.html

如何釋放Linux的cache :https://blog.csdn.net/william_m999/article/details/94898565

tcp的半連接與完全連接隊列:https://www.jianshu.com/p/ff26312e67a9

tcp sk_backlog(後備隊列分析): https://www.cnblogs.com/alreadyskb/p/4386565.html

TCP套接口的skbacklog接收隊列: https://blog.csdn.net/sinat20184565/article/details/88861077

kernel 3.10內核源碼分析--slab原理及相關代碼 : http://blog.chinaunix.net/uid-20671208-id-4655765.html

作者簡介

宋武斌,HBG二手房經紀人技術部架構師,目前負責三網經紀人APP以及三網後端技術架構的優化和建設工作。

閱讀推薦

相關文章