k8s最佳實踐:業務丟包問題排查

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。

從計數器結果來看,ListenOverflowsListenDrops 的值差別不大

所以推測很有可能是 accept queue 滿了

因爲當 accept queue 滿了會丟 SYN 包,並且同時將 ListenOverflowsListenDrops 計數器分別 +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-QRecv-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=1024echo 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 配置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章