k8s最佳實踐:業務丟包問題排查
一.問題描述
有用戶反饋大量圖片加載不出來。
圖片下載走的 k8s ingress,這個 ingress 路徑對應後端 service 是一個代理靜態圖片文件的 nginx deployment,這個 deployment 只有一個副本,靜態文件存儲在 nfs 上,nginx 通過掛載 nfs 來讀取靜態文件來提供圖片下載服務
所以調用鏈是:client –> k8s ingress –> nginx –> nfs。
二.原因猜測
猜測: ingress 圖片下載路徑對應的後端服務出問題了。
驗證:在 k8s 集羣直接 curl nginx 的 pod ip,發現不通,果然是後端服務的問題!
三.問題排查
1.抓包:確定存在丟包
登上 nginx pod 所在節點,進入容器的 netns (網絡命名空間 )中
# 拿到 pod 中 nginx 的容器 id
$ kubectl describe pod tcpbench-6484d4b457-847gl | grep -A10 "^Containers:" | grep -Eo 'docker://.*$' | head -n 1 | sed 's/docker:\/\/\(.*\)$/\1/'
49b4135534dae77ce5151c6c7db4d528f05b69b0c6f8b9dd037ec4e7043c113e
# 通過容器 id 拿到 nginx 進程 pid
$ docker inspect -f {{.State.Pid}} 49b4135534dae77ce5151c6c7db4d528f05b69b0c6f8b9dd037ec4e7043c113e
3985
# 進入 nginx 進程所在的 netns
$ nsenter -n -t 3985
# 查看容器 netns 中的網卡信息,確認下
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
3: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 56:04:c7:28:b0:3c brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.26.0.8/26 scope global eth0
valid_lft forever preferred_lft forever
使用 tcpdump 指定端口 24568 抓容器 netns 中 eth0 網卡的包:
tcpdump -i eth0 -nnnn -ttt port 24568
在其它節點準備使用 nc 指定源端口爲 24568 向容器發包:
nc -u 24568 172.16.1.21 80
觀察抓包結果:
00:00:00.000000 IP 10.0.0.3.24568 > 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000206334 ecr 0,nop,wscale 9], length 0
00:00:01.032218 IP 10.0.0.3.24568 > 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000207366 ecr 0,nop,wscale 9], length 0
00:00:02.011962 IP 10.0.0.3.24568 > 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000209378 ecr 0,nop,wscale 9], length 0
00:00:04.127943 IP 10.0.0.3.24568 > 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000213506 ecr 0,nop,wscale 9], length 0
00:00:08.192056 IP 10.0.0.3.24568 > 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000221698 ecr 0,nop,wscale 9], length 0
00:00:16.127983 IP 10.0.0.3.24568 > 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000237826 ecr 0,nop,wscale 9], length 0
00:00:33.791988 IP 10.0.0.3.24568 > 172.16.1.21.80: Flags [S], seq 416500297, win 29200, options [mss 1424,sackOK,TS val 3000271618 ecr 0,nop,wscale 9], length 0
SYN 包到容器內網卡了,但容器沒回 ACK,像是報文到達容器內的網卡後就被丟了。
看樣子跟防火牆應該也沒什麼關係,也檢查了容器 netns 內的 iptables 規則,是空的,沒問題。
排除是 iptables 規則問題,在容器 netns 中使用 netstat -s
檢查下是否有丟包統計:
$ netstat -s | grep -E 'overflow|drop'
12178939 times the listen queue of a socket overflowed
12247395 SYNs to LISTEN sockets dropped
果然有丟包
全連接隊列滿了:xxx times the listen queue of a socket overflowed
半連接隊列滿了:xxx SYNs to LISTEN sockets dropped
2.尋找爲什麼丟包?全連接隊列 滿了
背景知識:
Linux 進程監聽端口時,內核會給它對應的 socket 分配兩個隊列:
- syn queue: 半連接隊列。server 收到 SYN 後,連接會先進入
SYN_RCVD
狀態,並放入 syn queue,此隊列的包對應還沒有完全建立好的連接(TCP 三次握手還沒完成)。 - accept queue: 全連接隊列。當 TCP 三次握手完成之後,連接會進入
ESTABELISHED
狀態並從 syn queue 移到 accept queue,等待被進程調用accept()
系統調用 “拿走”。
注意:這兩個隊列的連接都還沒有真正被應用層接收到,當進程調用
accept()
後,連接纔會被應用層處理,具體到我們這個問題的場景就是 nginx 處理 HTTP 請求。
根據之前的分析,我們可以推測是 syn queue 或 accept queue 滿了。
先檢查下 syncookies 配置:
$ cat /proc/sys/net/ipv4/tcp_syncookies
1
確認啓用了 syncookies
,所以 syn queue 大小沒有限制,不會因爲 syn queue 滿而丟包,並且即便沒開啓 syncookies
,syn queue 有大小限制,隊列滿了也不會使 ListenOverflows
計數器 +1。
從計數器結果來看,ListenOverflows
和 ListenDrops
的值差別不大
所以推測很有可能是 accept queue 滿了
因爲當 accept queue 滿了會丟 SYN 包,並且同時將 ListenOverflows
與 ListenDrops
計數器分別 +1。
所以,得出結論,猜測是accept queue滿了
如何驗證 accept queue 滿了呢?
可以在容器的 netns 中執行 ss -lnt
看下:
$ ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 129 128 *:80 *:*
通過這條命令我們可以看到當前 netns 中監聽 tcp 80 端口的 socket,Send-Q
爲 128,Recv-Q
爲 129。
什麼意思呢?通過調研得知:
- 對於
LISTEN
狀態,Send-Q
表示 accept queue 的最大限制大小,Recv-Q
表示其實際大小。 - 對於
ESTABELISHED
狀態,Send-Q
和Recv-Q
分別表示發送和接收數據包的 buffer。
所以,看這裏輸出結果可以得知 accept queue 滿了,當 Recv-Q
的值比 Send-Q
大 1 時表明 accept queue 溢出了,如果再收到 SYN 包就會丟棄掉。
問題1:爲什麼不用考慮syn queue滿了?
因爲默認都是啓動了syn_cookies,所以一般不用擔心 syn queue 滿了導致丟包
- syncookies 是爲了防止 SYN Flood 攻擊 (一種常見的 DDoS 方式),攻擊原理就是 client 不斷髮 SYN 包但不回最後的 ACK,填滿 server 的 syn queue 從而無法建立新連接,導致 server 拒絕服務。
-
如果啓用了 syncookies (net.ipv4.tcp_syncookies=1),當 syn queue 滿了,server 還是可以繼續接收
SYN
包並回復SYN+ACK
給 client,只是不會存入 syn queue 了。因爲會利用一套巧妙的 syncookies 算法機制生成隱藏信息寫入響應的SYN+ACK
包中,等 client 回ACK
時,server 再利用 syncookies 算法校驗報文,校驗通過後三次握手就順利完成了。所以如果啓用了 syncookies,syn queue 的邏輯大小是沒有限制的, -
如果 syncookies 沒有啓用,syn queue 的大小就有限制,除了跟 accept queue 一樣受
net.core.somaxconn
大小限制之外,還會受到net.ipv4.tcp_max_syn_backlog
的限制,即:max syn queue size = min(backlog, net.core.somaxconn, net.ipv4.tcp_max_syn_backlog)
3.尋找 全連接隊列 滿的原因
原因1:accept()調用很慢
導致 accept queue 滿的原因一般都是因爲進程調用 accept()
太慢了,導致大量連接不能被及時 “拿走”。
那麼什麼情況下進程調用 accept()
會很慢呢?猜測可能是進程連接負載高,處理不過來。
而負載高不僅可能是 CPU 繁忙導致,還可能是 IO 慢導致,當文件 IO 慢時就會有很多 IO WAIT,在 IO WAIT 時雖然 CPU 不怎麼幹活,但也會佔據 CPU 時間片,影響 CPU 幹其它活。
最終進一步定位發現是 nginx pod 掛載的 nfs 服務對應的 nfs server 負載較高,導致 IO 延時較大,從而使 nginx 調用 accept()
變慢,accept queue 溢出,使得大量代理靜態圖片文件的請求被丟棄,也就導致很多圖片加載不出來。
原因2:全連接隊列很小
內核既然給監聽端口的 socket 分配了 syn queue 與 accept queue 兩個隊列,那它們有大小限制嗎?可以無限往裏面塞數據嗎?當然不行! 資源是有限的,尤其是在內核態,所以需要限制一下這兩個隊列的大小。那麼它們的大小是如何確定的呢?我們先來看下 listen 這個系統調用:
int listen(int sockfd, int backlog)
可以看到,能夠傳入一個整數類型的 backlog
參數,我們再通過 man listen
看下解釋:
-
listen 的 backlog 參數同時指定了 socket 的 syn queue 與 accept queue 大小。
-
accept queue 最大不能超過
net.core.somaxconn
的值,即:-
max accept queue size = min(backlog, net.core.somaxconn)
-
解決方案:調大全連接隊列大小,需要調節的參數有兩個:backlog, net.core.somaxconn
somaxconn 的默認值很小
我們再看下之前 ss -lnt
的輸出:
$ ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 129 128 *:80 *:*
仔細一看,Send-Q
表示 accept queue 最大的大小,才 128 ?也太小了吧!
根據前面的介紹我們知道,accept queue 的最大大小會受 net.core.somaxconn
內核參數的限制,我們看下 pod 所在節點上這個內核參數的大小:
$ cat /proc/sys/net/core/somaxconn
32768
是 32768,挺大的,爲什麼這裏 accept queue 最大大小就只有 128 了呢?
net.core.somaxconn
這個內核參數是 namespace 隔離了的,我們在容器 netns 中再確認了下:
$ cat /proc/sys/net/core/somaxconn
128
爲什麼只有 128?看下 stackoverflow 這裏 的討論:
The "net/core" subsys is registered per network namespace. And the initial value for somaxconn is set to 128.
原來新建的 netns 中 somaxconn 默認就爲 128,在 include/linux/socket.h
中可以看到這個常量的定義:
/* Maximum queue length specifiable by listen. */
#define SOMAXCONN 128
很多人在使用 k8s 時都沒太在意這個參數,爲什麼大家平常在較高併發下也沒發現有問題呢?
因爲通常進程 accept()
都是很快的,所以一般 accept queue 基本都沒什麼積壓的數據,也就不會溢出導致丟包了。
對於併發量很高的應用,還是建議將 somaxconn 調高。雖然可以進入容器 netns 後使用 sysctl -w net.core.somaxconn=1024
或 echo 1024 > /proc/sys/net/core/somaxconn
臨時調整,但調整的意義不大,因爲容器內的進程一般在啓動的時候纔會調用 listen()
,然後 accept queue 的大小就被決定了,並且不再改變。
四.解決問題
第一步:調節內核參數somaxconn的默認值?
方式一: 使用 k8s sysctls 特性直接給 pod 指定內核參數
示例 yaml:
apiVersion: v1
kind: Pod
metadata:
name: sysctl-example
spec:
securityContext:
sysctls:
- name: net.core.somaxconn
value: "8096"
有些參數是 unsafe
類型的,不同環境不一樣,我的環境裏是可以直接設置 pod 的 net.core.somaxconn
這個 sysctl 的。如果你的環境不行,請參考官方文檔 Using sysctls in a Kubernetes Cluster 啓用 unsafe
類型的 sysctl。
方式二: 使用 initContainers 設置內核參數
示例 yaml:
apiVersion: v1
kind: Pod
metadata:
name: sysctl-example-init
spec:
initContainers:
- image: busybox
command:
- sh
- -c
- echo 1024 > /proc/sys/net/core/somaxconn
imagePullPolicy: Always
name: setsysctl
securityContext:
privileged: true
Containers:
...
方式三: 安裝 tuning CNI 插件統一設置 sysctl
tuning plugin 地址: https://github.com/containernetworking/plugins/tree/master/plugins/meta/tuning
CNI 配置示例:
{
"name": "mytuning",
"type": "tuning",
"sysctl": {
"net.core.somaxconn": "1024"
}
}
第二步:調節nginx backlog 參數的默認值
支持在配置裏改。通過 ngx_http_core_module 的官方文檔我們可以看到它在 linux 下的默認值就是 511:
配置示例:
listen 80 default backlog=1024;
五.總結
大致總結一下排除過程:
第一步:猜測是後端服務問題
第二步:抓包,得出結論:確定存在丟包現象
第三步:尋找爲什麼丟包,得出結論:確定是accept queue滿了
第四步:尋找如何accept queue大小由什麼決定,是如何計算的?得出結論:全連接隊列大小由backlog和內核參數somaxconn決定
第五步:調整nginx的backlog和內核參數somaxconn的大小
所以,在容器中使用 nginx 來支撐高併發的業務時,記得要同時調整下 net.core.somaxconn
內核參數和 nginx.conf
中的 backlog 配置。