Manjusaka

Manjusaka

簡單聊聊在 Linux 核心中的網絡質量監控

這可能是 2021 年最後一篇文章(農曆年),也可能是 2022 年第一篇文章,不過這完全取決於我什麼時候寫完。這次來簡單聊聊 Linux 中的網路監控

開篇#

這篇文章,既是一篇水文,又不是一篇水文。不過還是新手向的一個文章。這篇文章實際上在我的草稿箱裡呆了一年多的時間了,靈感最初源自我在阿里的一些工作(某種意義上算是國內領先的(但也是比較小眾的工作(XD

隨著技術的發展,大家對於服務的穩定性要求越來越高,而保證服務品質的前提就是有著合格的監控的覆蓋面(阿里對於服務穩定性的要求叫做 "1-5-10" 即,一分鐘發現,五分鐘處理,十分鐘自癒,而這樣一個對於穩定性的要求沒有足夠的覆蓋面的監控的話,那麼一切等於圈圈)。而在這其中,網路品質的監控是重中之重

在討論網路品質的監控之前,我們需要來明確網路品質這個定義的覆蓋範圍。

  1. 網路鏈路上的異常情況
  2. 服務端網路的處理能力

在明確這樣的覆蓋範圍後,我們可以來思考什麼樣的指標代表著網路品質的降低。(注:本文主要分析 TCP 及 over TCP 協議的監控,後續不再贅述)

  1. 毫無疑問,如果我們存在丟包的情況
  2. 發送 / 接收隊列阻塞
  3. 超時

那麼我們可以再來看下具體細節

  1. 如 RFC7931 提出的 RTO,RFC62982 提出的 Retransmission Timer 等指標,可以衡量包傳送時間。一個粗略的概括是,這兩個指標越大代表著網路品質越低
  2. 如 RFC20183 提出的 SACK,一個不精確的概括是 SACK 越多,代表著丟包越多
  3. 如果我們的鏈接頻繁的被 RST,那麼也代表著我們的網路品質存在問題

當然在實際的生產過程中,我們還可以從很多其餘的指標來輔助衡量網路品質,不過因為本文主要是介紹思路以 prototype 為主,所以不做過多贅述

在明確我們這篇文章中要獲取什麼指標後,我們再來分析一下我們怎麼樣去獲取這些指標

核心網路品質監控#

暴力版#

從核心中獲取網路的 metric ,本質上來說是從核心獲取運行狀態。說到這點,對 Linux 有所了解的同學第一反應肯定是說從 The Proc Filesystem4 看一下能不能拿到具體的指標。Yep,不錯的思路,實際上確實可以拿到一部分的指標(這也是 netstat 等一些網路工具的原理)

/proc/net/tcp 中,我們可以獲取到核心吐出的 Metric,現在包括這樣一些

  1. 連接狀態
  2. 本地端口,地址
  3. 遠程端口,地址
  4. 接收隊列長度
  5. 發送隊列長度
  6. 慢啟動閾值
  7. RTO 值
  8. 連接所屬的 socket 的 inode id
  9. uid
  10. delay ack 軟時鐘

完整的解釋可以參考 proc_net_tcp.txt5

這樣的做法針對 prototype 可能說是可以的,不過其固有的幾個弊端限制了在生產上大規模使用

  1. 核心已經明確不推薦使用 proc_net_tcp.txt5,換句話說,並不保證未來的相容性與維護
  2. 核心直接提供的 metric 信息還是太少,一些關於 RTT,SRTT 這樣的指標還是沒法獲取,也沒法獲取 SACK 等一些特定事件。
  3. 根據核心輸出的 metric。存在的問題是即時性和精度的問題,換句話說,我們在不考慮精度的情況下可以去做這方面的嘗試
  4. proc_net_tcp.txt5 是和 network namespace 進行綁定的,換句話說,在容器的場景下,我們需要遍歷可能存在的多個 network namespace,不斷的走 nsenter 去獲取對應的 Metric

所以在這樣的背景下,proc_net_tcp.txt5 並不太適合比較大規模的使用場景。所以我們需要對其做更進一步的優化

優化 1.0 版#

在上文裡,我們提到了關於直接從 The Proc Filesystem4 中獲取數據的弊端。其中一條很重要的是提到了

核心已經明確不推薦使用 proc_net_tcp.txt5,換句話說,並不保證未來的相容性與維護

那麼推薦的做法是什麼呢?答案是 netlink+sock_diag

簡單介紹下 netlink6 是 Linux 2.2 引入的一種 Kernel Space 與 User Space 進行通信的機制,最早由 RFC35497 提出。官方對於 netlink6 的描述大概是這樣

Netlink is used to transfer information between the kernel and user-space processes. It consists of a standard sockets-based interface for user space processes and an internal kernel API for kernel modules.
The internal kernel interface is not documented in this manual page. There is also an obsolete netlink interface via netlink character devices; this interface is not documented here and is provided only for backward compatibility.

簡而言之大概是用戶可以利用 netlink6 很方便的與核心中的不同的 Kernel Module 進行數據交互

而在我們這樣的場景下,我們就需要利用到 sock_diag8,官方對此的描述是

The sock_diag netlink subsystem provides a mechanism for obtaining information about sockets of various address families from the kernel. This subsystem can be used to obtain information about individual sockets or request a list of sockets.

這裡簡而言之是說我們可以利用 sock_diag7 來獲取不同 socket 的連接狀態及相應的指標。(我們能獲取到上文提到的所有指標,也能獲得更細的 RTT 等指標)啊對了,這裡要注意,netlink6 可以通過設置參數來從所有的 Network Namespace 獲取指標。

在使用 netlink6 時,可能直接用 Pure C 來寫比較繁瑣。所幸,社區已經有不少封裝成熟的 Lib,比如這裡我選用 vishvananda 所封裝的 netlink 庫8,這裡我給一個 Demo

package main

import (
    "fmt"

	"github.com/vishvananda/netlink"
	"syscall"
)

func main() {
	results, err := netlink.SocketDiagTCPInfo(syscall.AF_INET)
	if err != nil {
		return
	}
	for _, item := range results {
		if item.TCPInfo != nil {
			fmt.Printf("Source:%s, Dest:%s, RTT:%d\n", item.InetDiagMsg.ID.Source.String(), item.InetDiagMsg.ID.Destination.String(), item.TCPInfo.Rtt)
		}
	}
}

運行示例大概是這樣

netlink

OK,現在我們能用官方推薦的 Best Practice 來獲取到更全更細的指標,也無需操心 Network namespace 的問題,但是我們最開始的幾個問題還有一個比較棘手,就是即時性的问题。

因為如果我們選擇周期性的輪詢,那麼如果在我們的輪詢間隔中發生了網路波動,我們將丟失掉對應的現場。所以我們怎麼樣去解決即時性的问题呢?

優化 2.0 版#

如果要在具體的比如重傳,connection reset 等事件發生的時候,直接觸發我們的調用。看過我之前博客的同學,可能第一時間考慮使用 eBPF + kprobe 的組合,在一些諸如 tcp_resettcp_retransmit_skb 之類的關鍵調用上打點來獲取即時的數據。Sounds good!

不過實際上還是有一些小小的問題

  1. kprobe 的開銷在高頻的情況下,相對來說會比較大一些
  2. 如果我們僅僅需要一些諸如 source_address, dest_address, source_port, dest_port 之類的信息,我們直接走 kprobe 拿完整地 skb 再來 cast 實屬有點浪費

所以我們有什麼更好的方法嗎?有的!

在 Linux 中,對於一系列的類似我們需求這樣的特殊事件的觸發與回調的場景,有一套基礎設施叫做 Tracepoint9。這套設施,能夠很好地幫我們處理監聽事件並回調的需求。而在 Linux 4.15 以及 4.16 之後,Linux 新增了 6 個 tcp 相關的 Tracepoint9

分別是

  1. tcp
  2. tcp
  3. tcp
  4. tcp
  5. tcp
  6. tcp

這些 Tracepoint9 的含義,大家看名字可能就能明白了

而在這些 Tracepoint9 觸發的時候,他們會給註冊回調函數傳入若干參數,這裡我也給大家列一下

tcp:tcp_retransmit_skb
    const void * skbaddr;
    const void * skaddr;
    __u16 sport;
    __u16 dport;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
tcp:tcp_send_reset
    const void * skbaddr;
    const void * skaddr;
    __u16 sport;
    __u16 dport;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
tcp:tcp_receive_reset
    const void * skaddr;
    __u16 sport;
    __u16 dport;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
tcp:tcp_destroy_sock
    const void * skaddr;
    __u16 sport;
    __u16 dport;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
tcp:tcp_retransmit_synack
    const void * skaddr;
    const void * req;
    __u16 sport;
    __u16 dport;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
tcp:tcp_probe
    __u8 saddr[sizeof(struct sockaddr_in6)];
    __u8 daddr[sizeof(struct sockaddr_in6)];
    __u16 sport;
    __u16 dport;
    __u32 mark;
    __u16 length;
    __u32 snd_nxt;
    __u32 snd_una;
    __u32 snd_cwnd;
    __u32 ssthresh;
    __u32 snd_wnd;
    __u32 srtt;
    __u32 rcv_wnd;

嗯,看到這裡,大家可能心裡應該有個數了,那麼我們還是來寫一下示例代碼

from bcc import BPF

bpf_text = """
BPF_RINGBUF_OUTPUT(tcp_event, 65536);

enum tcp_event_type {
    retrans_event,
    recv_rst_event,
};

struct event_data_t {
    enum tcp_event_type type;
    u16 sport;
    u16 dport;
    u8 saddr[4];
    u8 daddr[4];
    u32 pid;
};

TRACEPOINT_PROBE(tcp, tcp_retransmit_skb)
{
    struct event_data_t event_data={};
    event_data.type = retrans_event;
    event_data.sport = args->sport;
    event_data.dport = args->dport;
    event_data.pid=bpf_get_current_pid_tgid()>>32;
    bpf_probe_read_kernel(&event_data.saddr,sizeof(event_data.saddr), args->saddr);
    bpf_probe_read_kernel(&event_data.daddr,sizeof(event_data.daddr), args->daddr);
    tcp_event.ringbuf_output(&event_data, sizeof(struct event_data_t), 0);
    return 0;
}

TRACEPOINT_PROBE(tcp, tcp_receive_reset)
{
    struct event_data_t event_data={};
    event_data.type = recv_rst_event;
    event_data.sport = args->sport;
    event_data.dport = args->dport;
    event_data.pid=bpf_get_current_pid_tgid()>>32;
    bpf_probe_read_kernel(&event_data.saddr,sizeof(event_data.saddr), args->saddr);
    bpf_probe_read_kernel(&event_data.daddr,sizeof(event_data.daddr), args->daddr);
    tcp_event.ringbuf_output(&event_data, sizeof(struct event_data_t), 0);
    return 0;
}

"""

bpf = BPF(text=bpf_text)


def process_event_data(cpu, data, size):
    event = bpf["tcp_event"].event(data)
    event_type = "retransmit" if event.type == 0 else "recv_rst"
    print(
        "%s %d %d %s %s %d"
        % (
            event_type,
            event.sport,
            event.dport,
            ".".join([str(i) for i in event.saddr]),
            ".".join([str(i) for i in event.daddr]),
            event.pid,
        )
    )


bpf["tcp_event"].open_ring_buffer(process_event_data)


while True:
    bpf.ring_buffer_consume()

我這裡使用了 tcp_receive_resettcp_retransmit_skb 來監控我們機器上的程序。為了演示具體的效果,我先用 Go 寫了一個訪問 Google 的程序,然後通過 sudo iptables -I OUTPUT -p tcp -m string --algo kmp --hex-string "|c02bc02fc02cc030cca9cca8c009c013c00ac014009c009d002f0035c012000a130113021303|" -j REJECT --reject-with tcp-reset 來給這個 Go 程序注入 Connection Reset (這裡的注入原理是 Go 默認庫的發起 HTTPS 鏈接的 Client Hello 特徵是固定的,我用 iptables 識別出方向流量,然後重置鏈接)

效果如下

Tracepoint

嗯,寫到這裡,你可能想明白了,我們可以將 Tracepoint9 和 netlink6 結合使用來滿足我們即時性的需求

優化 3.0 版#

實際上寫到現在,也更多的是講一些 Prototype 和思路上的介紹。而為了能滿足生產上的需要,還有很多的工作要做(這也是我之前所做的工作的一部分),包括不僅限於:

  1. 工程上的性能優化,避免影響服務
  2. Kubernetes 等容器平台的相容
  3. 對接 Prometheus 等數據監控平台
  4. 可能需要嵌入 CNI 來獲取更簡便的監控路徑等等

實際上社區在這一塊也有很多很有意思的工作,比如 Cilium 等,大家有興趣也可以關注下。而我後續拾掇拾掇代碼,也會在合適的時候將我之前的一些實現路徑給開源出來。

總結#

這篇文章差不多就寫到這裡,核心的網路監控終歸是比較小眾的領域。希望我這裡面的一些經驗能夠幫助上大家。嗯,祝大家新年快樂!虎年大吉!(下一篇文章就是寫去年的年終總結了)

Reference#

  1. RFC793: https://datatracker.ietf.org/doc/html/rfc793
  2. RFC6298:https://datatracker.ietf.org/doc/html/rfc6298
  3. RFC2018:https://datatracker.ietf.org/doc/html/rfc2018
  4. The /proc Filesystem:https://www.kernel.org/doc/html/latest/filesystems/proc.html
  5. proc_net_tcp.txt:https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt
  6. netlink:https://man7.org/linux/man-pages/man7/netlink.7.html
  7. sock_diag:https://man7.org/linux/man-pages/man7/sock_diag.7.html
  8. vishvananda/netlink:https://github.com/vishvananda/netlink
    9: Linux Tracepoint:https://www.kernel.org/doc/html/latest/trace/tracepoints.html
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。