eBPF 助力 NAS 分鐘級別 Pod 實例溯源
一、背景
二、流量溯源方案調研和驗證
1. NAS 工作原理
2. 方案調研和驗證
三、架構設計和實現
1. 整體架構設計
2. 內核 eBPF 程序流程
3. 用戶空間程序架構
四、總結
一、 背景
云存儲 NAS 產品是一個可共享訪問、彈性擴展、高可靠、高性能的分布式文件系統。 NAS 兼容了 POSIX 文件接口,可支持數千臺計算節點共享訪問,可掛載到彈性計算 ECS、容器實例等計算業務上,提供高性能的共享存儲服務。
鑒于多主機間共享的便利性和高性能, NAS 在得物的算法訓練、應用構建等場景中均成為了基礎支撐。
圖片
在多業務共享的場景中,單個業務流量異常容易引發全局故障。目前,異常發生后需依賴云服務廠商 NAS 的溯源能力,但只能定位到主機級別,無法識別具體異常服務。要定位到服務級別,仍需依賴所有使用方協同排查,并由 SRE 多輪統計分析,效率低下(若服務實例發生遷移或重建,排查難度進一步增加)。
為避免因 NAS 異常或帶寬占滿導致模型訓練任務受阻,因此需構建支持服務級流量監控、快速溯源及 NAS 異常實時感知的能力,以提升問題定位效率并減少業務中斷。
二、 流量溯源方案調研和驗證
NAS工作原理
NAS 本地掛載原理
在 Linux 平臺上,NAS 的產品底層是基于標準網絡文件系統 NFS(Network File System),通過將遠端文件系統掛載到本地,實現用戶對遠端文件的透明訪問。
NFS 協議(主要支持 NFS v3 和 v4,通常以 v3 為主)允許將遠端服務掛載到本地,使用戶能夠像訪問本地文件目錄一樣操作遠端文件。文件訪問請求通過 RPC 協議發送到遠端進行處理,其整體流程如下:
文件系統訪問時的數據流向示意
Linux 內核中 NFS 文件系統
在 Linux NFS 文件系統的實現中,文件操作接口由 nfs_file_operations 結構體定義,其讀取操作對應的函數為:
//NFS 文件系統的 VFS 層實現的函數如下所示:
const struct file_operations nfs_file_operations = {
.llseek = nfs_file_llseek,
.read_iter = nfs_file_read,
.write_iter = nfs_file_write,
// ...
};針對 NFS 文件系統的讀操作涉及到 2 個階段(寫流程類似,只是函數名字有所差異,本文僅以讀取為例介紹)。由于文件讀取涉及到網絡操作因此這兩個階段涉及為異步操作:
※ 兩個階段
- 讀取請求階段:當應用程序針對 NFS 文件系統發起 read() 讀操作時,內核會在VFS層調用 nfs_file_read 函數,然后調用 NFS 層的 nfs_initiate_read 函數,通過 RPC 的 rpc_task_begin 函數將讀請求發送到 NFS Server,至此向 NFS Server 發起的請求工作完成。
- 讀響應階段:在 NFS Server 返回消息后,會調用 rpc_task_end 和 nfs_page_read_done 等函數,將數據返回到用戶空間的應用程序。
圖片
在了解 NFS 文件系統的讀流程后,我們回顧一下 NFS Server 為什么無法區分單機訪問的容器實例或進程實例。
這是因為 NFS 文件系統的讀寫操作是在內核空間實現的。當容器 A/B 和主機上的進程 C 發起讀請求時,這些請求在進入內核空間后,統一使用主機 IP(如 192.168.1.2)作為客戶端 IP 地址。因此,NFS Server 端的統計信息只能定位到主機維度,無法進一步區分主機內具體的容器或進程。
內核空間實現示意
方案調研和驗證
進程對應容器上下文信息關聯
內核中進程以 PID 作為唯一編號,與此同時,內核會建立一個 struct task_struct 對象與之關聯,在 struct task_struct 結構會保存進程對應的上下文信息。如實現 PID 信息與用戶空間容器上下文的對應(進程 PID 1000 的進程屬于哪個 Pod 哪個 Container 容器實例),我們需基于內核 task_struct 結構獲取到容器相關的信息。
通過分析內核代碼和資料確認,發現可以通過 task_struct 結構中對應的 cgroup 信息獲取到進程對應的 cgroup_name 的信息,而該信息中包含了容器 ID 信息,例如 docker-2b3b0ba12e92...983.scope ,完整路徑較長,使用 .... 省略。基于容器 ID 信息,我們可進一步管理到進程所歸屬的 Pod 信息,如 Pod NameSpace 、 Pod Name 、 Container Name 等元信息,最終完成進程 PID 與容器上下文信息元數據關聯。
struct task_struct {
struct css_set __rcu *cgroups;
}
struct css_set {
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
}
struct cgroup_subsys_state {
struct cgroup *cgroup;
}
struct cgroup {
struct kernfs_node *kn; /* cgroup kernfs entry */
}
struct kernfs_node {
const char *name; // docker-2b3b0ba12e92...983.scope
}以某容器進程為例,該進程在 Docker 容器環境中的 cgroup 路徑完整為 /sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podefeb3229_4ecb_413a_8715_5300a427db26.slice/docker-2b3b0ba12e925820ac8545f67c8cadee864e5b4033b3d5004d8a3aa742cde2ca.scope 。
經驗證,我們在內核中讀取 task->cgroups->subsys[0]->kn->name 的值為 docker-2b3b0ba12e925820ac8545f67c8cadee864e5b4033b3d5004d8a3aa742cde2ca.scope 。
圖片
其中容器 ID 字段為 docker- 與 .scope 間的字段信息,在 Docker 環境中一般取前 12 個字符作為短 ID,如 2b3b0ba12e92 ,可通過 docker 命令進行驗證,結果如下:
docker ps -a|grep 2b3b0ba
2b3b0ba12e92 registry-cn-hangzhou-vpc.ack.aliyuncs.com/acs/pause:3.5NAS 上下文信息關聯
NAS 產品的訪問通過掛載命令完成本地文件路徑的掛載。我們可以通過 mount 命令將 NAS 手工掛載到本地文件系統中。
mount -t nfs -o vers=3,nolock,proto=tcp,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport \
3f0f3489aa-xxxx.cn-shanghai.nas.aliyuncs.com:/test /mnt/nas執行上述掛載命令成功后,通過 mount 命令則可查詢到類似的掛載記錄:
5368 47 0:660 / /mnt/nas rw,relatime shared:1175 \
- nfs 3f0f3489aa-xxxx.cn-shanghai.nas.aliyuncs.com:/test \
rw,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,nolock,\
noresvport,proto=tcp,timeo=600,retrans=2,sec=sys, \
mountaddr=192.168.0.91,mountvers=3,mountport=2049,mountproto=tcp,\
local_lock=all,addr=192.168.0.92核心信息分析如下:
# 掛載點 父掛載點 掛載設備號 目錄 掛載到本機目錄 協議 NAS地址
5368 47 0:660 / /mnt/nas nfs 3f0f3489aa-xxxx.cn-shanghai.nas.aliyuncs.com:/test
maror:minor掛載記錄中的 0:660 為本地設備編號,格式為 major:minor , 0 為 major 編號, 660 為 minor 編號,系統主要以 minor 為主。在系統的 NFS 跟蹤點 nfs_initiate_read 的信息中的 dev 字段則為在掛載記錄中的 minor 編號。
cat /sys/kernel/debug/tracing/events/nfs/nfs_initiate_read/format
format:
field:dev_t dev; offset:8; size:4; signed:0;
...
field:u32 count; offset:32; size:4; signed:0;通過用戶空間 mount 信息和跟蹤點中 dev_id 信息,則可實現內核空間設備編號與 NAS 詳情的關聯。
內核空間信息獲取
如容器中進程針對掛載到本地的目錄 /mnt/nas 下的文件讀取時,會調用到 nfs_file_read() 和 nfs_initiate_read 函數。通過 nfs_initiate_read 跟蹤點我們可以實現進程容器信息和訪問 NFS 服務器的信息關聯。
通過編寫 eBPF 程序針對跟蹤點 tracepoint/nfs/nfs_initiate_read 觸發事件進行數據獲取,我們可獲取到訪問進程所對應的 cgroup_name 信息和訪問 NFS Server 在本機的設備 dev_id 編號。
圖片
獲取cgroup_name信息
- 進程容器上下文獲取: 通過 cgroup_name 信息,如樣例中的 docker-2b3b0ba12e92...983.scope ,后續可以基于 container_id 查詢到容器對應的 Pod NameSpace 、 Pod Name 和 Container Name 等信息,從而定位到訪問進程關聯的 Pod 信息。
- NAS 上下文信息獲取: 通過 dev 信息,樣例中的 660 ,通過掛載到本地的記錄,可以通過 660 查詢到對應的 NAS 產品的地址,比如3f0f3489aa-xxxx.cn-shanghai.nas.aliyuncs.com 。
用戶空間元信息緩存
圖片
在用戶空間中,可以通過解析掛載記錄來獲取 DEV 信息,并將其與 NAS 信息關聯,從而建立以 DevID 為索引的查詢緩存。如此,后續便可以基于內核獲取到 dev_id 進行關聯,進一步補全 NAS 地址及相關詳細信息。
對于本地容器上下文的信息獲取,最直接的方式是通過 K8s kube-apiserver 通過 list-watch 方法進行訪問。然而,這種方式會在每個節點上啟動一個客戶端與 kube-apiserver 通信,顯著增加 K8s 管控面的負擔。因此,我們選擇通過本地容器引擎進行訪問,直接在本地獲取主機的容器詳情。通過解析容器注解中的 Pod 信息,可以建立容器實例緩存。后續在處理指標數據時,則可以通過 container-id 實現信息的關聯與補全。
三、架構設計和實現
整體架構設計
內核空間的信息采集采用 Linux eBPF 技術實現,這是一種安全且高效的內核數據采集方式。簡單來說,eBPF 的原理是在內核中基于事件運行用戶自定義程序,并通過內置的 map 和 perf 等機制實現用戶空間與內核空間之間的雙向數據交換。
在 NFS 和 RPC 調用事件觸發的基礎上,可以通過編寫內核空間的 eBPF 程序來獲取必要的原始信息。當用戶空間程序搜集到內核指標數據后,會對這些原始信息進行二次處理,并在用戶空間的采集程序中補充容器進程信息(如 NameSpace、Pod 和 Container 名稱)以及 NFS 地址信息(包括 NFS 遠端地址)。
圖片
內核eBPF程序流程
以 NFS 文件讀為例,通過編寫 eBPF 程序跟蹤 nfs_initiate_read / rpc_task_begin / rpc_task_end / nfs_page_read_done 等關鍵鏈路上的函數,用于獲取到 NFS 讀取的數據量和延時數據,并將訪問鏈路中的進程上下文等信息保存到內核中的指標緩存中。
圖片
如上圖所示, nfs_initate_read 和 rpc_task_begin 發生在同一進程上下文中,而 rpc_task_begin 與 rpc_task_end 是異步操作,盡管兩者不處于同一進程上下文,但可以通過 task_id 進行關聯。同時, page_read_done 和 rpc_task_end 則發生在同一進程上下文中。
圖片
nfs_initiate_read 函數調用觸發的 eBPF 代碼示例如下所示:
SEC("tracepoint/nfs/nfs_initiate_read")
int tp_nfs_init_read(struct trace_event_raw_nfs_initiate_read *ctx)
// 步驟1 獲取到 nfs 訪問的設備號信息,比如 3f0f3489aa-xxxx.cn-shanghai.nas.aliyuncs.com
// dev_id 則為: 660
dev_t dev_id = BPF_CORE_READ(ctx, dev);
u64 file_id = BPF_CORE_READ(ctx, fileid);
u32 count = BPF_CORE_READ(ctx, count);
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
// 步驟2 獲取進程上下文所在的容器 cgroup_name 信息
// docker-2b3b0ba12e925820ac8545f67c8cadee864e5b4033b3d5004d8a3aa742cde2ca.scope
const char *cname = BPF_CORE_READ(task, cgroups, subsys[0], cgroup, kn, name);
if (cname)
{
bpf_core_read_str(&info.container, MAX_PATH_LEN, cname);
}
bpf_map_update_elem(&link_begin, &tid, &info, BPF_ANY);
}
SEC("tracepoint/nfs/nfs_readpage_done")
int tp_nfs_read_done(struct trace_event_raw_nfs_readpage_done *ctx)
{
//... 省略
}
SEC("tracepoint/sunrpc/rpc_task_begin")
int tp_rpc_task_begin(struct trace_event_raw_rpc_task_running *ctx)
{
//... 省略
}
SEC("tracepoint/sunrpc/rpc_task_end")
int tp_rpc_task_done(struct trace_event_raw_rpc_task_running *ctx)
{
//... 省略
}用戶空間程序架構
圖片
元數據緩存
※ NAS 掛載信息緩存
通過解析掛載記錄,可以獲取 DEV 信息與 NAS 信息的關聯關系。以下是實現該功能的關鍵代碼詳情:
scanner := bufio.NewScanner(mountInfoFile)
count := 0
for scanner.Scan() {
line := scanner.Text()
devID,remoteDir, localDir, NASAddr = parseMountInfo(line)
mountInfo := MountInfo{
DevID: devID,
RemoteDir: remoteDir,
LocalMountDir: localDir,
NASAddr: NASAddr,
}
mountInfos = append(mountInfos, mountInfo)※ 容器元信息緩存
通過 Docker 或 Containerd 客戶端,從本地讀取單機的容器實例信息,并將容器的上下文數據保存到本地緩存中,以便后續查詢使用。
podInfo := PodInfo{
NameSpace: labels["io.kubernetes.pod.namespace"],
PodName: labels["io.kubernetes.pod.name"],
ContainerName: labels["io.kubernetes.container.name"],
UID: labels["io.kubernetes.pod.uid"],
ContainerID: conShortID,
}數據處置流程
用戶空間程序的主要任務是持續讀取內核 eBPF 程序生成的指標數據,并對讀取到的原始數據進行處理,提取訪問設備的 dev_id 和 container_id 。隨后,通過查詢已建立的元數據緩存,分別獲取 NAS 信息和容器 Pod 的上下文數據。最終,經過數據合并與處理,生成指標數據緩存供后續使用。
func (m *BPFEventMgr) ProcessIOMetric() {
// ...
events := m.ioMetricMap
iter := events.Iterate()
for iter.Next(&nextKey, &event) {
// ① 讀取到的 dev_id 轉化為對應的完整 NAS 信息
devId := nextKey.DevId
mountInfo, ok := m.mountMgr.Find(int(devId))
// ② 讀取 containerID 格式化并查詢對應的 Pod 上下文信息
containerId := getContainerID(nextKey.Container)
podInfo, ok = m.criMgr.Find(containerId)
// ③ 基于事件信息、NAS 掛載信息和 Pod 上下文信息,生成指標數據緩存
metricKey, metricValue := formatMetricData(nextKey, mountInfo, podInfo)
value, loaded := metricCache.LoadOrStore(metricKey, metricValue)
}
// ④ 指標數據緩存,生成最終的 Metrics 指標并更新
var ioMetrics []metric.Counter
metricCache.Range(func(key, value interface{}) bool {
k := key.(metric.IOKey)
v := value.(metric.IOValue)
ioMetrics = append(ioMetrics, metric.Counter{"read_count", float64(v.ReadCount),
[]string{k.NfsServer, v.NameSpace, v.Pod, v.Container})
// ...
}
return true
})
m.metricMgr.UpdateIOStat(ioMetrics)
}啟動 Goroutine 處理指標數據:通過啟動一個 Goroutine,循環讀取內核存儲的指標數據,并對數據進行處理和信息補齊,最終生成符合導出格式的 Metrics 指標。
※ 具體步驟
- 獲取 NAS 信息:從讀取的原始數據中提取 dev_id ,并通過 dev_id 查詢掛載的 NAS 信息,例如遠端訪問地址等相關數據。
- 查詢 Pod 上下文:對 containerID 進行格式化處理,并查詢對應的容器 Pod 上下文信息。
- 生成指標數據緩存:基于事件數據、NAS 掛載信息和 Pod 上下文信息,生成指標數據緩存。此過程主要包括對相同容器上下文的數據進行合并和累加。
- 導出 Metrics 指標:根據指標數據緩存,生成最終的 Metrics 指標,并更新到指標管理器。隨后,通過自定義的 Collector 接口對外導出數據。當 Prometheus 拉取數據時,指標會被轉換為最終的 Metrics 格式。
通過上述步驟,用戶空間能夠高效地處理內核 eBPF 程序生成的原始數據,并結合 NAS 掛載信息和容器上下文信息,生成符合 Prometheus 標準的 Metrics 指標,為后續的監控和分析提供了可靠的數據基礎。
自定義指標導出器
在導出指標的場景中,我們需要基于保存在 Go 語言中的 map 結構中的動態數據實時生成,因此需要實現自定義的 Collector 接口。自定義 Collector 接口需要實現元數據描述函數 Describe() 和指標搜集的函數 Collect() ,其中 Collect() 函數可以并發拉取,因此需要通過加鎖實現線程安全。該接口需要實現以下兩個核心函數:
- Describe() :用于定義指標的元數據描述,向 Prometheus 注冊指標的基本信息。
- Collect() :用于搜集指標數據,該函數支持并發拉取,因此需要通過加鎖機制確保線程安全。
type Collector interface {
// 指標的定義描述符
Describe(chan<- *Desc)
// 并將收集的數據傳遞到Channel中返回
Collect(chan<- Metric)
}我們在指標管理器中實現 Collector 接口, 部分實現代碼,如下所示:
nfsIOMetric := prometheus.NewDesc(
prometheus.BuildFQName(prometheusNamespace, "", "io_metric"),
"nfs io metrics by cgroup",
[]string{"nfs_server", "ns", "pod", "container", "op", "type"},
nil,
)
// Describe and Collect implement prometheus collect interface
func (m *MetricMgr) Describe(ch chan<- *prometheus.Desc) {
ch <- m.nfsIOMetric
}
func (m *MetricMgr) Collect(ch chan<- prometheus.Metric) {
// Note:加鎖保障線程并發安全
m.activeMutex.Lock()
defer m.activeMutex.Unlock()
for _, v := range m.ioMetricCounters {
ch <- prometheus.MustNewConstMetric(m.nfsIOMetric, prometheus.GaugeValue, v.Count, v.Labels...)
}四、總結
當前 NAS 溯源能力已正式上線,以下是主要功能和視圖介紹:
※ 單 NAS 實例整體趨勢
支持基于環境和 NAS 訪問地址過濾,展示 NAS 產品的讀寫 IOPS 和吞吐趨勢圖。同時,基于內核空間統計的延時數據,提供 P95 讀寫延時指標,用于判斷讀寫延時情況,輔助問題分析和定位。
圖片
圖片
在 NAS 流量溯源方面,我們結合業務場景設計了基于任務和 Pod 實例維度的流量分析視圖:
※ 任務維度流量溯源
通過聚合具有共同屬性的一組 Pod 實例,展示任務級別的整體流量情況。該視圖支持快速定位任務級別的流量分布,幫助用戶進行流量溯源和多任務錯峰使用的依據。
圖片
※ Pod 實例維度流量溯源
以 Pod 為單位進行流量分析和匯總,提供 Pod NameSpace 和 Name 信息,支持快速定位和分析實例級別的流量趨勢,幫助細粒度監控和異常流量的精準定位。
圖片
在整體能力建設完成后,我們成功構建了 NAS 實例級別的 IOPS、吞吐和讀寫延時數據監控大盤。通過該能力,進一步實現了 NAS 實例的 IOPS 和吞吐可以快速溯源到任務級別和 Pod 實例級別,流量溯源時效從小時級別縮短至分鐘級別,有效提升了異常問題定位與解決的效率。同時,基于任務流量視圖,我們為后續帶寬錯峰復用提供了直觀的數據支持。
































