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

image

上一篇把 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本身就支持插件的方式来增加指标采集接口。回顾一下这条链路:

image-20260710120021031

拆下来一共要动五个地方:

  1. ocm-agent 新增 pod-monitor 插件:通过 CRI 采集 Pod/容器指标,走 SDK 上报。
  2. ClickHouse 新增 pod_monitor:存容器指标,结构与 os_monitor 一致,多几个 Pod 维度。
  3. 后端新增查询 APImsg-etl.ListPodMetricsSeries(图表数据)+ footstone.ListPodMeta(Pod 下拉列表)。
  4. 指标元数据:往 metrics_description / metrics_tag_info 表插入 pod_monitor 的指标说明。
  5. 前端整机监控页扩展:加"主机视图 / 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 层加两个 APIportrait.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。

image-20260710114112185

生成 proto 代码

后端引用的 ListPodMetricsSeriesRequestPodConditionListPodMetaRequest 这些类型,都定义在 .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_idkeyword——而 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_loadcpu_usagemem_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-nodeaws-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展示效果如下

image-20260710113555670

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

image-20260710114424477

结语

我们从面板看不到容器这个朴素痛点出发,回顾整条链路,加一个新监控维度需要且只需要动这几处:

  1. 写一个 ocm-agent 插件:独立进程,采集你要的数据,用 SDK 推给 agent。采集逻辑随便你怎么写——调 API、读文件、跑命令都行,agent 那套 mTLS 连接、数据通道、批量上报全部复用,不用碰。
  2. 建一张同名 ClickHouse 表:msg-etl 用插件名当表名自动落库,表结构照抄 os_monitor 即可。
  3. 补指标元数据:往 metrics_description / metrics_tag_info 塞几行,前端就能列出可选指标。
  4. 后端做二次加工:像 CPU 那样需要差分/聚合的,在 msg-etl 查询层加一段;纯瞬时量则直接透传。
  5. 前端加视图:需要新维度的选择器就仿 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 给建议"的运维闭环才算真正成型。这是这次把容器监控补上之后,最值得期待的方向。

参考资料

posted @ 2026-07-10 12:01  zhaojie10  阅读(12)  评论(0)    收藏  举报