给 OCManager 加上容器级监控:从 CRI 采集到前端展示的全链路开发

上一篇把 OCManager 从控制平面部署、Agent 通过 SSM 批量下发到 EKS 节点、再到接入 OCAI 大模型跑通了。跑起来之后盯着监控面板看了几天,一个问题越来越明显:面板上只有主机级指标——某个 EKS 节点 CPU 80%,我知道是这台机器忙,但到底是哪个 Pod、哪个容器在吃 CPU,面板答不上来。
这在裸机时代无所谓,但被管目标是 EKS 节点,节点上跑着几十个容器,"节点 CPU 高"这个信息的诊断价值几乎为零。于是我们设想能否给 OCManager 补上容器级(Pod / Container)监控能力。
动手前有两个绕不开的问题。
问题一:K8s 生态已经有 Prometheus 了,为什么还要往 OCManager 里塞容器指标?
Prometheus + cAdvisor 的容器监控体系非常成熟,但OCManager 的定位是运维——它的核心价值是"节点 → 诊断命令下发 → AI 辅助判断"这条闭环,尤其在 EKS 托管节点不能直接 SSH的场景下,agent 是唯一的运维通道。
我们可以填补 OCManager 在容器场景的上下文缺失:让面板能从"节点 CPU 高"下钻到"哪个 Pod 的哪个容器",进而让 OCAI 能回答"192.168.x.x 上是哪个容器在吃 CPU"。
问题二:数据从哪采?
既然每台 EKS 节点上已经装了 ocm-agent,那就不需要单独部署一个 DaemonSet——直接作为 ocm-agent 的一个插件来采集,复用现成的 mTLS 连接、数据通道、上报链路。
至于容器指标的来源,Linux 上有三条路:
| 方案 | 数据来源 | |
|---|---|---|
| CRI API | 容器运行时(containerd)通过 gRPC 暴露 | K8s 原生、自带 Pod 元数据 |
| cAdvisor | kubelet 内嵌,直接读 cgroup | 成熟但正被 K8s 逐步弃用 |
| 直接读 /sys/fs/cgroup | 内核 cgroup 文件 | 最底层,但要自己处理 v1/v2 差异和 Pod→cgroup 映射 |
这里我们选择CRI,至于为什么选 CRI 而不是 cAdvisor?Kubernetes 社区在 KEP-2371 里明确要把容器/Pod stats 从 cAdvisor 迁移到 CRI,核心原因有几个:
- cAdvisor 和容器运行时都在读同一份 cgroup,双份开销、数据还可能不一致。
- Kata、gVisor 这类跑在轻量 VM 里的容器,cAdvisor 只能看到外层 shim,看不到 VM 内部真实指标;而运行时知道真实数据。
- cAdvisor 要自己理解 Docker/containerd/CRI-O 各种运行时,维护面巨大。
先验证节点到底支不支持 CRI——SSH 到 EKS worker 节点看一眼:containerd 2.2.4、CRI socket 存在、cgroup v2,环境支持CRI
$ sudo /usr/bin/containerd --version
containerd github.com/containerd/containerd/v2 2.2.4+unknown
$ ls -la /run/containerd/containerd.sock
srw-rw----. 1 root root 0 /run/containerd/containerd.sock
$ cat /sys/fs/cgroup/cgroup.controllers # cgroup v2
cpuset cpu io memory hugetlb pids misc
整体设计
新功能没有另起炉灶,而是完全嵌进 OCManager 现有的数据管道,ocm-agent本身就支持插件的方式来增加指标采集接口。回顾一下这条链路:

拆下来一共要动五个地方:
- ocm-agent 新增
pod-monitor插件:通过 CRI 采集 Pod/容器指标,走 SDK 上报。 - ClickHouse 新增
pod_monitor表:存容器指标,结构与os_monitor一致,多几个 Pod 维度。 - 后端新增查询 API:
msg-etl.ListPodMetricsSeries(图表数据)+footstone.ListPodMeta(Pod 下拉列表)。 - 指标元数据:往
metrics_description/metrics_tag_info表插入 pod_monitor 的指标说明。 - 前端整机监控页扩展:加"主机视图 / Pod 视图"切换 + 命名空间/Pod 选择器。
先搞清楚 ocm-agent 的插件机制
在写 pod-monitor 之前,得先理解 ocm-agent 的插件是怎么跑的。它用的是外部进程插件模型:每个插件是一个独立的可执行文件,由 agent 主进程按配置管理生命周期,插件采集到数据后通过 Unix 域套接字回传给 agent,再由 agent 统一走 mTLS 上报。
一个插件由两部分组成:
- 注册配置(
pod_monitor.yml):告诉 agent 这个插件叫什么、二进制在哪、多久跑一次。agent 启动时扫描plugin-conf/目录加载:
name: pod_monitor
cron: "*/30 * * * * *" # 每 30 秒
exec_path: /opt/ocm-agent/plugins/pod_monitor/pod_monitor
timeout: -1 # -1 = 常驻进程
enable: true
run_on_boot: true # 启动即拉起
timeout: -1 表示常驻进程——插件自己管采集节奏(内部用 ticker),agent 不重复拉起;这和 sysinfo_collector 那种"跑一次就退出"的 one-shot 插件不同。
- 插件二进制:独立的 Go 程序,用 agent 提供的 SDK 把指标推给主进程。
现成的 os-monitor、sysinfo-collector 就是这个套路,pod-monitor 照抄即可。
pod-monitor 的实现
插件目录按职责分层,和 os-monitor 结构一致:
plugins/pod-monitor/
├── main.go # 入口:加载配置、起 worker、处理信号
├── worker.go # 采集循环:collect / push 双 ticker
├── collector/
│ ├── manager.go # 任务调度:按配置启用采集任务
│ ├── task/base.go # ITask 接口 + 定时触发基类
│ └── cri/
│ ├── cri.go # CRI gRPC 客户端(连 containerd)
│ └── metrics.go # 把 CRI stats 转成 agent.Metric
├── config/cfg.go # app.conf.yml 解析
├── pod_monitor.yml # 插件注册配置
└── build.sh
入口 main.go:加载配置、初始化日志、起 worker,然后阻塞等 SIGINT/SIGTERM 优雅退出。常驻进程的标准骨架:
func main() {
setWorkingDir()
cfg, _ := config.NewAppConfig(cfgPath)
logger.InitLogger(cfg.App.LogLevel)
logger.Info("Pod Monitor started")
worker := NewWorker(cfg)
go worker.Start()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
worker.Stop()
}
采集循环 worker.go:用两个 ticker 把"采集"和"上报"解耦——collectTicker 定期采集堆到内存缓冲,pushTicker 定期把缓冲一次性推给 agent。这样既能高频采样,又能批量上报、减少 socket 往返:
func (w *Worker) Start() {
collectTicker := time.NewTicker(time.Duration(w.cfg.App.TickInterval) * time.Second)
pushTicker := time.NewTicker(time.Duration(w.cfg.App.PushInterval) * time.Second)
for w.running {
select {
case <-collectTicker.C:
go w.collectData() // 采集 → 追加到 w.metrics
case <-pushTicker.C:
w.pushMetrics(w.metrics) // 批量上报
w.metrics = []*agent.Metric{} // 清空缓冲
}
}
}
任务调度 manager.go:Collect() 遍历已启用的采集任务(这里只有一个 CRI 任务),到点就执行。这套 ITask 接口是从 os-monitor 抄来的,将来加别的采集源直接往 Tasks 里塞:
func NewManager(cfg *config.AppConfig) *Manager {
tasks := make([]task.ITask, 0)
if cfg.Pod.Metrics.Interval > 0 || cfg.Container.Metrics.Interval > 0 {
tasks = append(tasks, cri.NewMetricsTask(cfg))
}
return &Manager{cfg: cfg, Tasks: tasks}
}
上报 SDK:worker 把采集到的 []*agent.Metric 包成 MetricsData,调 sdk.PushMetricsData 推给 agent。SDK 内部会 protobuf 序列化 + zlib 压缩 + 写 Unix socket(/opt/ocm-agent/var/chan.sock),这些 agent 都封装好了,插件不用管:
metricData := &agent.MetricsData{
TsNs: uint64(time.Now().UnixNano()),
Groups: []*agent.MetricGroup{{Metrics: metrics}},
}
sdk.PushMetricsData("pod_monitor", "v1.0.0", map[string]string{}, metricData)
到这里,"采集 → 缓冲 → 上报"的骨架就搭好了。核心的"采集"落在 collector/cri/ 里,即连 containerd、调 CRI、把 stats 转成 Metric。
指标为空问题
骨架搭完,插件"跑起来"了:agent 日志 Loaded plugin: pod_monitor、进程也在。但 ClickHouse 里一条数据都没有。在节点上手动跑一下插件看输出:
[INFO] Pod Monitor started
[INFO] Worker started successfully
[INFO] Collected 0 pod/container metrics # 问题出现
[INFO] No metrics collected
节点上明明有 77 个容器,却采集到 0 个。翻 collector/cri/cri.go,发现 CRI 客户端是个桩实现(stub)——结构定义得挺全,但方法体直接返回空数组,压根没连 containerd:
func (c *Client) ListContainerStats() ([]*ContainerStats, error) {
return []*ContainerStats{}, nil // 假的!从没调过 CRI
}
func (c *Client) ListPodSandboxStats() ([]*PodSandboxStats, error) {
return []*PodSandboxStats{}, nil
}
难怪采集到 0 个。换成真正的 CRI gRPC 调用——连 /run/containerd/containerd.sock,调 ListContainerStats,遍历 resp.Stats 把 CPU/内存/网络提取出来:
import runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
func NewClient(socketPath string, timeout int) (*Client, error) {
conn, err := grpc.DialContext(ctx, "unix://"+socketPath,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock())
if err != nil {
return nil, err
}
return &Client{conn: conn, runtime: runtimeapi.NewRuntimeServiceClient(conn)}, nil
}
func (c *Client) ListContainerStats() ([]*ContainerStats, error) {
resp, err := c.runtime.ListContainerStats(ctx, &runtimeapi.ListContainerStatsRequest{})
if err != nil {
return nil, err
}
out := make([]*ContainerStats, 0, len(resp.Stats))
for _, s := range resp.Stats {
cs := &ContainerStats{}
if s.Attributes != nil {
// Pod 名 / 命名空间藏在 Labels 里
cs.Attributes = &ContainerAttributes{Id: s.Attributes.Id, Labels: s.Attributes.Labels}
}
if s.Cpu != nil {
cs.Cpu = &CpuUsage{UsageCoreNanoSeconds: toU64(s.Cpu.UsageCoreNanoSeconds)}
}
if s.Memory != nil {
cs.Memory = &MemoryUsage{
WorkingSetBytes: toU64(s.Memory.WorkingSetBytes),
UsageBytes: toU64(s.Memory.UsageBytes),
RssBytes: toU64(s.Memory.RssBytes),
// ...
}
}
out = append(out, cs)
}
return out, nil
}
metrics.go 再把这些 ContainerStats 转成 agent.Metric——每个指标带上 pod_name / namespace / container_name 标签(这些从 CRI 返回的 Labels 里取:io.kubernetes.pod.name 等)。
注意:
k8s.io/cri-api@latest要求go >= 1.26,而后端主工程用的是 1.24/1.25。这个依赖只在 ocm-agent 插件里用,插件独立 go.mod,所以升级插件的 Go 版本即可,不影响后端。另外 CRI 的NetworkInterfaceUsage结构里没有RxPackets/TxPackets字段(只有 bytes 和 errors),照着 kubelet 的字段名写会编译不过。
换成真实调用后再测,从 0 到 597:
[INFO] Collected 597 pod/container metrics
数据写错表
接下来,发现插件采集到数据、agent 也上报了(日志 Received message plugin: pod_monitor data_len: 22386),但 ClickHouse 的 pod_monitor 表还是空的。查 msg-etl 日志,一堆报错:
INSERT INTO `pod_monitor` ... Table msg_etl.pod_monitor does not exist. Maybe you meant os_monitor?
原来 msg-etl 消费数据时,直接拿插件名当表名写入。我一开始建的表叫 pod_metrics,但插件名是 pod_monitor,对不上。考虑到 msg-etl 用插件名作表名是它既定的设计(os_monitor 插件→os_monitor 表),最省事的做法就是建一张叫 pod_monitor 的表:
CREATE TABLE IF NOT EXISTS msg_etl.pod_monitor
(
group_name LowCardinality(String),
metrics_name LowCardinality(String),
ip String,
pod_name String,
namespace String,
container_name String,
server_time DateTime CODEC(DoubleDelta, LZ4),
local_time DateTime64(9) CODEC(DoubleDelta, LZ4),
tags Map(String, String),
data String,
num_data Float64 DEFAULT 0,
uuid String,
...
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(server_time)
ORDER BY (ip, server_time, metrics_name)
TTL updated_at + toIntervalDay(15);
建表后数据立刻流进来了。跑一次端到端验证:1634 条记录、37 个 Pod、17 种指标,两个节点都在上报。采集链路真正打通了。
$ docker exec oc-clickhouse clickhouse-client -q "
SELECT count(*), uniq(tags['pod_name']), uniq(metrics_name)
FROM msg_etl.pod_monitor WHERE server_time > now() - INTERVAL 5 MINUTE"
1634 37 17
构建和编译
后端数据有了,接下来是前端适配。整机监控页原本只服务主机视图(选 IP → 看指标),要支持 Pod 视图,改动集中在四处。
前端适配:整机监控页加 Pod 视图
视图切换开关:搜索栏顶部加一个「主机视图 / Pod 视图」单选,用 viewMode 控制。切到哪个模式,下面的选择器和查询逻辑就跟着走哪条分支:
<t-radio-group v-model="viewMode" @change="onViewModeChange">
<t-radio-button value="host">主机视图</t-radio-button>
<t-radio-button value="pod">Pod 视图</t-radio-button>
</t-radio-group>
命名空间 / Pod 选择器:主机视图用的是 IP 输入框;Pod 视图则换成两个联动的下拉——先选命名空间,再选 Pod。
<!-- 仅 Pod 视图显示 -->
<t-select v-model="searchData.namespace" @change="onNamespaceChange"
placeholder="请选择命名空间">
<t-option v-for="ns in namespaceOptions" :value="ns" :label="ns" :key="ns" />
</t-select>
<t-select-input v-model:value="searchData.podName" @focus="fetchPodList('')"
placeholder="请输入Pod名称" allow-input clearable />
命名空间和 Pod 列表通过新加的 fetchListPodMeta 拉取——它查的是 footstone 的 ListPodMeta(底层扫 ClickHouse 的 pod_monitor 表取 distinct pods),前端再从返回的 data 数组里提取去重的 namespace / pod_name 填进下拉框。
service 层加两个 API:portrait.ts 里新增 Pod 相关接口,注意两个 API 挂在不同服务上——指标数据走 msg-etl,Pod 元数据走 footstone:
const MSG_ETL_PREFIX = '/trpc.ocmanager.msg_etl.Metrics';
const METRICS_META_PREFIX = '/trpc.ocmanager.footstone.MetricsMeta';
// Pod 图表数据(msg-etl)
fetchListPodMetricsData(params) {
return fetchApi({ url: `${MSG_ETL_PREFIX}/ListPodMetricsSeries`, method: 'post', params });
},
// Pod / 命名空间下拉列表(footstone)
fetchListPodMeta(params) {
return fetchApi({ url: `${METRICS_META_PREFIX}/ListPodMeta`, method: 'post', params });
},
查询按模式分流:点"查询"时,根据 viewMode 决定调哪个 API、构造哪种 condition。主机模式发 ip + plugin_name,Pod 模式发 pod_name + namespace:
const isPodMode = viewMode.value === 'pod';
const apiCall = isPodMode
? portraitApi.fetchListPodMetricsData // → ListPodMetricsSeries
: portraitApi.fetchListMetricsData; // → ListMetricsSeries(原主机逻辑)
图表组件(lineChartBox)、KPI 卡片、时间选择器这些全部复用主机视图现成的,Pod 视图只是把数据源换掉,所以真正的前端改动量并不大,主要是加视图开关 + Pod 选择器 + 两个 API。
生成 proto 代码
后端引用的 ListPodMetricsSeriesRequest、PodCondition、ListPodMetaRequest 这些类型,都定义在 .proto 里但没生成 Go 代码。OCManager 的 proto 用 trpc-cmdline 生成(.pb.go + .trpc.go)。
proto 生成后编译,又冒出一串错误,逐一让AI修复。
用 deploy.sh 重建镜像
项目自带的 scripts/deploy.sh build 就是干这个的——make build-bin-static 静态编译后端二进制,再 docker-compose build 把二进制打进镜像、顺便 npm run build 前端。proto 生成好之后,这一步就顺了:
# 后端静态编译
make -C manager/backend build-bin-static
# 重建三个镜像
docker-compose -f .../docker-compose.yml --env-file config/.env build msg-etl footstone frontend
# 用新镜像重启
docker-compose ... up -d --force-recreate msg-etl footstone frontend
Pod 视图报 unknown field
镜像重建、登录进 UI,切到Pod 视图,页面直接弹错:
service codec Unmarshal: proto: (line 1:2): unknown field "project_id"
这是 tRPC 的严格反序列化报错:前端发的 JSON 里带了 project_id,但后端 proto 的 message 里没这个字段。翻前端代码,发现生成的 Pod 视图逻辑塞了好几个 proto 不认识的字段:
fetchListPodMeta请求带了project_id、keyword——而ListPodMetaRequest只有ip/pod_name/namespace/start_time/end_time。- Pod 的 condition 里塞了
plugin_name: 'pod_monitor'——而PodCondition根本没有plugin_name字段(那是主机视图 os_monitor 的字段)。
一共 5 处 condition 构造 + 2 个下拉查询都犯了同样的毛病。逐个改成只发 proto 支持的字段:
// 改前:Pod 模式也塞 plugin_name(PodCondition 不认)
base.plugin_name = isPodMode ? 'pod_monitor' : 'os_monitor';
// 改后:plugin_name 只在主机模式加
if (isPodMode) {
base.pod_name = target;
if (searchData.value.namespace) base.namespace = searchData.value.namespace;
} else {
base.ip = target;
base.plugin_name = 'os_monitor';
}
指标下拉框是空的
project_id 报错修完,Pod 视图能选 Pod 了,但指标下拉框空空如也。
原因是切到 Pod 视图后,前端拿默认勾选的指标名还是硬编码的主机指标(cpu_load、cpu_usage、mem_used……),而这些在 pod_monitor 里根本不存在。更底层的问题是:metrics_description / metrics_tag_info 两张元数据表里压根没有 pod_monitor 的指标定义——fetchMetrics 用 INNER JOIN 查元数据,pod_monitor 一条都 JOIN 不出来。
首先,插入 pod_monitor 的指标元数据(17 个指标,对应插件实际采集的那些):
INSERT INTO metrics_description
(plugin_name, group_name, group_ch_name, metrics_name, metrics_ch_name, unit, description)
VALUES
('pod_monitor','CPU','CPU','container_cpu_usage_nano_cores','容器CPU使用','nanocores','容器CPU使用量'),
('pod_monitor','Mem','内存','container_memory_working_set_bytes','容器工作集内存','bytes','容器工作集内存'),
... -- 共 17 条
然后,前端切 Pod 视图时默认勾选 Pod 指标,而不是主机指标:
if (isPodMode) {
searchData.value.metricLabels = [
'container_cpu_usage_nano_cores', 'container_memory_working_set_bytes',
'pod_cpu_usage_nano_cores', 'pod_memory_working_set_bytes',
'pod_network_rx_bytes', 'pod_network_tx_bytes', ...
];
}
CPU 值只升不降问题
前面的坑都算"接线没接对",这个坑是指标语义本身错了。
图表能出数据了,但 Pod CPU 显示成这样:
Pod CPU使用 当前 12715303000 nanocores
最大 12715303000 @10:32
最小 12426539000 @09:33
一百多亿"nanocores",而且只升不降。查原始数据,果然是单调递增。这是累计计数器(counter)的典型特征。翻插件代码,CPU 采的是 CRI 的 UsageCoreNanoSeconds——容器从启动到现在累计消耗的 CPU 纳秒数,但指标名却叫 ..._nano_cores(暗示瞬时使用率),等于把 counter 当 gauge 展示了。
CRI 的 CpuUsage 其实有两个字段:
UsageCoreNanoSeconds // 累计 CPU 纳秒(我们错用了这个,单调递增)
UsageNanoCores // 瞬时使用率(但 containerd 不一定填充)
performance_platform.allocstall 就是在 msg-etl 里做一阶差分将指标转增量的,因此我们直接抄过来后端做一阶差分。于是在 msg-etl 查询层对这两个 CPU 指标做差分转速率:
// rate = (v[i+1] - v[i]) / (ts[i+1] - ts[i]) 单位 nanocores/秒
for i := 0; i < n-1; i++ {
dt := ts[i+1] - ts[i]
if dt <= 0 { continue }
rate := (vals[i+1] - vals[i]) / float64(dt)
if rate < 0 { rate = 0 } // 容器重启计数器归零 → 负值置 0
outVals = append(outVals, rate)
outTS = append(outTS, ts[i+1])
}
改完再看,值从 127 亿变成了合理的 100 万~350 万 nanocores(约 0.001~0.003 core),而且有升有降了。
CPU 曲线时不时掉到 0
差分修完,又发现 CPU 曲线偶尔掉 0。查了一下,container_cpu_usage_nano_cores 原始数据里 0 值占比是 0%——所以这个 0 是差分/降采样过程冒出来的,不是原始数据。
深挖发现根因:一个 Pod 里有多个容器。比如 aws-node-nqtx5 这个 Pod,里面有 aws-node 和 aws-eks-nodeagent 两个容器,各自有独立的累计 CPU 计数器,量级还差 100 倍:
aws-node 累计值 ≈ 388,000,000,000
aws-eks-nodeagent 累计值 ≈ 3,600,000,000
而查询只按 pod_name 过滤、用 max() 降采样,把两个容器的累计值混在一个序列里——差分时从大容器值跳到小容器值,得到负数,被置 0。这就是 CPU 掉 0 的真相:多容器数据混合后差分错乱。
我们需要按容器拆成多条线(每个容器一条,各自独立差分),跟主机视图的 Top6 多线是同一个套路。实现上,在 ListPodMetricsSeries 里对 container_* 指标且未指定容器时,先查出该 Pod 有哪些容器,再为每个容器生成一条独立 series:
func (s *metricsQueryService) expandPodConditions(cond *pb.PodCondition) []*pb.PodCondition {
if !strings.HasPrefix(cond.MetricName, "container_") || cond.ContainerName != "" {
return []*pb.PodCondition{cond} // pod_* 指标本身是 Pod 级,不拆
}
containers, _ := s.metricsRepo.ListPodContainers(...) // 查该 Pod 的容器列表
// 每个容器一个独立 condition
for _, c := range containers {
nc := *cond; nc.ContainerName = c
out = append(out, &nc)
}
return out
}
改完验证,同一个 Pod 返回两条独立的线:
container: aws-node points:59 zeros:0 sample:[1106817, 3199700, 2844783]
container: aws-eks-nodeagent points:59 zeros:10 sample:[0, 44233, 44000]
aws-node(主容器、有负载)零个 0 值,曲线正常。aws-eks-nodeagent(sidecar、几乎不干活)还有 10 个 0——但这次的 0 是真实且正确的:查原始累计值,这个容器某些 60 秒周期内累计值一模一样(3653304000 → 3653304000),说明那段时间它就是没消耗 CPU。这和 Prometheus 里 rate() 对一个几乎不动的 counter 返回 0 是完全一样的语义。
最终的UI展示效果如下

对一个pod进行请求测试,发现各项指标按照预期波动

结语
我们从面板看不到容器这个朴素痛点出发,回顾整条链路,加一个新监控维度需要且只需要动这几处:
- 写一个 ocm-agent 插件:独立进程,采集你要的数据,用 SDK 推给 agent。采集逻辑随便你怎么写——调 API、读文件、跑命令都行,agent 那套 mTLS 连接、数据通道、批量上报全部复用,不用碰。
- 建一张同名 ClickHouse 表:msg-etl 用插件名当表名自动落库,表结构照抄
os_monitor即可。 - 补指标元数据:往
metrics_description/metrics_tag_info塞几行,前端就能列出可选指标。 - 后端做二次加工:像 CPU 那样需要差分/聚合的,在 msg-etl 查询层加一段;纯瞬时量则直接透传。
- 前端加视图:需要新维度的选择器就仿 Pod 视图加,纯指标的话主机视图直接就能看。
也就是说,顺着它,能补的东西还很多:
- 节点文件系统 / inode 指标:读
/proc/mounts+statfs,补上磁盘按挂载点的用量与 inode 耗尽预警——这是线上最常见的"磁盘写满"类故障的前置信号。 - GPU 指标:GPU 节点上跑
nvidia-smi --query-gpu=...或对接 DCGM,采显存占用、GPU 利用率、温度,AI 训练/推理集群刚需。 - 进程级指标:读
/proc/<pid>/采 Top CPU/内存进程、僵尸进程、D 状态进程,配合诊断命令定位"到底哪个进程在作妖"。 - 日志转指标:把节点上关键日志(OOM、内核报错、应用 ERROR)按规则计数,转成可告警的时序指标。
- CRI 里还没采的:这次只用了 CPU/内存/网络/可写层,Pod 的重启次数、OOM 事件、探针状态等也能顺着 CRI/kubelet 补上。
再往上一层,这些容器/Pod 指标进了库,OCAI 也就能读到——当它能回答"192.168.x.x 上是 grafana 这个容器在吃 CPU、已经持续 10 分钟",再配合受控命令下发,"节点异常 → 定位到容器/进程 → 下发诊断 → AI 给建议"的运维闭环才算真正成型。这是这次把容器监控补上之后,最值得期待的方向。
参考资料
- Kubernetes CRI Pod & Container Metrics:https://kubernetes.io/docs/reference/instrumentation/cri-pod-container-metrics/
- KEP-2371 cAdvisor-less, CRI-full Container and Pod Stats:https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/2371-cri-pod-container-stats/README.md

浙公网安备 33010602011771号