原创 泊明 2025-07-07 18:30 上海
鉴于多主机间共享的便利和高性能,NAS 在得物在算法训练、应用构建等场景中成为了基础支撑,得物通过建设整体能力成功构建了 NAS 实例级别的IOPS、吞吐和读写延时数据监控大盘,有效提升异常问题定位与解决效率,为后续带宽错峰复用提供数据支持
💡 得物基于NAS在算法训练和应用构建等场景中的重要性,为避免因NAS异常导致业务中断,构建了服务级流量监控能力。
🔍 通过深入研究NFS工作原理,得物团队利用eBPF技术,在内核中采集关键数据,实现对NAS流量的精准监控。
⚙️ 方案的核心在于将进程PID与容器上下文信息关联,通过获取cgroup信息,进而获取容器ID、Pod信息等,实现服务级别的流量追踪。
📊 得物设计了整体架构,包括内核eBPF程序和用户空间程序,用户空间程序负责数据处理、元数据缓存和指标导出,最终生成Prometheus标准的Metrics指标。
📈 最终,得物实现了NAS实例级别的IOPS、吞吐和读写延时数据监控大盘,支持任务和Pod实例级别的流量溯源,显著提升了问题定位效率。
原创 泊明 2025-07-07 18:30 上海
鉴于多主机间共享的便利和高性能,NAS 在得物在算法训练、应用构建等场景中成为了基础支撑,得物通过建设整体能力成功构建了 NAS 实例级别的IOPS、吞吐和读写延时数据监控大盘,有效提升异常问题定位与解决效率,为后续带宽错峰复用提供数据支持
在多业务共享的场景中,单个业务流量异常容易引发全局故障。目前,异常发生后需依赖云服务厂商 NAS 的溯源能力,但只能定位到主机级别,无法识别具体异常服务。要定位到服务级别,仍需依赖所有使用方协同排查,并由 SRE 多轮统计分析,效率低下(若服务实例发生迁移或重建,排查难度进一步增加)。
针对 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 端的统计信息只能定位到主机维度,无法进一步区分主机内具体的容器或进程。内核空间实现示意//NFS 文件系统的 VFS 层实现的函数如下所示:
const struct file_operations nfs_file_operations = {
.llseek = nfs_file_llseek,
.read_iter = nfs_file_read,
.write_iter = nfs_file_write,
// ...
};
以某容器进程为例,该进程在 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 命令进行验证,结果如下: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
}
NAS 上下文信息关联NAS 产品的访问通过挂载命令完成本地文件路径的挂载。我们可以通过 mount 命令将 NAS 手工挂载到本地文件系统中。docker ps -a|grep 2b3b0ba
2b3b0ba12e92 registry-cn-hangzhou-vpc.ack.aliyuncs.com/acs/pause:3.5
执行上述挂载命令成功后,通过 mount 命令则可查询到类似的挂载记录: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
核心信息分析如下: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
挂载记录中的 0:660 为本地设备编号,格式为 major:minor , 0 为 major 编号, 660 为 minor 编号,系统主要以 minor 为主。在系统的 NFS 跟踪点 nfs_initiate_read 的信息中的 dev 字段则为在挂载记录中的 minor 编号。# 挂载点 父挂载点 挂载设备号 目录 挂载到本机目录 协议 NAS地址
5368 47 0:660 / /mnt/nas nfs 3f0f3489aa-xxxx.cn-shanghai.nas.aliyuncs.com:/test
maror:minor
通过用户空间 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 实现信息的关联与补全。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;
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)
{
//... 省略
}
※ 容器元信息缓存通过 Docker 或 Containerd 客户端,从本地读取单机的容器实例信息,并将容器的上下文数据保存到本地缓存中,以便后续查询使用。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)
数据处置流程用户空间程序的主要任务是持续读取内核 eBPF 程序生成的指标数据,并对读取到的原始数据进行处理,提取访问设备的 dev_id 和 container_id 。随后,通过查询已建立的元数据缓存,分别获取 NAS 信息和容器 Pod 的上下文数据。最终,经过数据合并与处理,生成指标数据缓存供后续使用。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,
}
启动 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() :用于搜集指标数据,该函数支持并发拉取,因此需要通过加锁机制确保线程安全。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)
}
我们在指标管理器中实现 Collector 接口, 部分实现代码,如下所示:type Collector interface {
// 指标的定义描述符
Describe(chan<- *Desc)
// 并将收集的数据传递到Channel中返回
Collect(chan<- Metric)
}
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...)
}
AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。
鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑