基于 ES|QL 的 Kubernetes 监控工具箱:使用 ES|QL 进行 Kubernetes 监控,从内存压力到错误日志

本文提供的 Elasticsearch ES|QL 查询,快速定位 Kubernetes 集群中的崩溃循环、OOM 前夕的内存压力、节点资源饱和以及命名空间级别的错误尖峰。所有查询均基于 Elastic 发行的 OpenTelemetry Collector(EDOT)采集的数据,可 Discover 中运行,也可保存为仪表板面板或告警规则。


前置条件

  • Elasticsearch 9.2 或更高版本(ES|QL 在 8.11 中引入,但 9.2 后的 TS 命令更完善)。
  • EDOT Collector 已在集群中运行 ,并启用了以下接收器(receivers):
    • kubeletstats:采集 Pod、容器、节点的资源指标(CPU、内存、网络等)。
    • k8s_cluster:采集 Kubernetes 对象状态(如 Pod 相位、容器重启次数)。
    • filelog(配合 k8sattributes 处理器):采集容器日志,并添加上下文属性(命名空间、Pod 名等)。

EDOT 会将这些数据通过 OpenTelemetry 协议(OTLP)发送至 Elasticsearch,指标存储在 metrics-* 索引,日志存储在 logs-* 索引。


ES|QL 之 Kubernetes 监控

ES|QL 是一种管道式查询语言,它的核心思想是:从一个数据源开始,每一行追加一个操作------过滤、计算、聚合、排序------逐步精炼你的查询。这种结构天然适合故障排查,因为你可以像剥洋葱一样,层层缩小问题范围。

例如,一个典型的 ES|QL 查询流程如下:

esql 复制代码
FROM metrics-*                     // 数据源
| WHERE @timestamp > NOW() - 1h    // 时间过滤
| STATS count BY namespace         // 聚合统计
| SORT count DESC                  // 排序

每行只做一件事,逻辑清晰,易于调试和复用。


TS 命令与时间序列

Kubernetes 指标绝大多数是计数器(Counter)仪表盘(Gauge) ,它们随时间变化采样而来。使用 TS(Time Series)命令可以正确处理时间序列的语义:

  • 对于 Gauge(如当前内存使用率),你关心的是最新值或峰值,而非平均值。
  • 对于 Counter(如累计重启次数),你关心的是增量或最大值,而非对时间窗口内的所有样本求和(那样会得到一个无意义的巨大数字)。

TS 命令为每个时间序列(由维度如 podcontainer 唯一标识)单独处理聚合函数,并支持 TBUCKET 对时间进行分桶。普通 FROM 则适用于计数去重、简单取最大值等不涉及时间窗口内序列间计算的场景。


EDOT 采集器与依赖字段

EDOT 采集的数据遵循 OpenTelemetry 语义约定,但具体的指标名称由接收器定义,而非语义规范本身。因此,不同接收器输出的字段名可能不同。

接收器 典型指标/字段 说明
kubeletstats k8s.container.memory_limit_utilization k8s.pod.cpu.node.utilization k8s.node.cpu.usage k8s.container.restarts 资源利用率指标,来自 kubelet 的 API
k8s_cluster k8s.pod.phase Pod 相位(编码为数字)
filelog + k8sattributes body.text(日志内容) severity_text(日志级别) k8s.pod.namek8s.namespace.name 日志消息与上下文

注意 :如果你启用的接收器不同,或自定义了采集配置,某些字段可能缺失。请将下面的查询视为模板,根据实际索引映射调整字段名称。


集群概览:从全局开始

在深入具体问题之前,先把握集群的整体面貌。以下查询统计过去一小时内每个命名空间有多少个不同的 Pod 上报了指标

esql 复制代码
FROM metrics-*
| WHERE @timestamp >= NOW() - 1 hour
| STATS pod_count = COUNT_DISTINCT(k8s.pod.name) BY k8s.namespace.name
| SORT pod_count DESC

原理解释

  • COUNT_DISTINCT 会去重同一个 Pod 在时间窗口内产生的多个采样点,因此每个 Pod 只被计数一次。
  • 结果是一个"命名空间 --- Pod 数量"的排行榜,快速告诉你哪些命名空间负载最重,也能帮你发现预期运行的 Pod 是否"消失"了(如果某个命名空间计数为 0,说明它可能已缩容或没有被采集到)。

定位 Pod 重启与崩溃循环

重启是问题的第一信号k8s.container.restarts 是一个 Gauge 类型,报告每个容器的当前累计重启次数(该值会持续增长,直到容器被重新创建)。

以下查询列出过去 24 小时内重启次数最多的容器

esql 复制代码
FROM metrics-*
| WHERE @timestamp >= NOW() - 24 hours AND k8s.container.restarts IS NOT NULL
| STATS restarts = MAX(k8s.container.restarts)
    BY k8s.namespace.name, k8s.pod.name, k8s.container.name
| WHERE restarts > 0
| SORT restarts DESC
| LIMIT 20

原理解释

  • MAX(k8s.container.restarts) 取得该时间窗口内每个容器重启计数的最新值(因为 Gauge 的值会随时间更新,最大值即当前最新值)。
  • 如果一个容器的重启次数很高且在持续上升,这几乎是 CrashLoopBackOff 的教科书式信号。
  • 拿到 Pod 名称后,你可以直接跳转到后面的日志查询,查看它崩溃前输出了什么。

发现非 Running 状态的 Pod

重启次数告诉你"过去发生过故障",而 Pod 相位(Phase) 告诉你"现在是否健康"。k8s.pod.phase 是一个编码为数字的 Gauge:

数值 相位
1 Pending
2 Running
3 Succeeded
4 Failed
5 Unknown

以下查询使用 TS 读取每个 Pod 的最新相位,并筛选出非 Running 的 Pod:

esql 复制代码
TS metrics-*
| WHERE TRANGE(15m)
| STATS phase = MAX(LAST_OVER_TIME(k8s.pod.phase))
    BY k8s.namespace.name, k8s.pod.name
| WHERE phase != 2
| EVAL phase_name = CASE(
    phase == 1, "Pending",
    phase == 3, "Succeeded",
    phase == 4, "Failed",
    phase == 5, "Unknown",
    "Other")
| SORT phase_name

原理解释

  • TRANGE(15m) 限定时间范围为最近 15 分钟。
  • LAST_OVER_TIME 从每个 Pod 的时间序列中取出最新样本,确保我们比较的是当前状态而非平均值。
  • MAX 在这里只是辅助(因为每个 Pod 在最后一个时间点只有一个值),语法要求必须有聚合函数。
  • 结果中,Pending 的 Pod 通常表明调度问题(资源不足),FailedUnknown 则需要立即关注。

在 OOM 发生前捕捉内存压力

OOM(Out-Of-Memory)Kill 是 Kubernetes 中最常见的故障之一,而且预防远胜于事后调试 。当你在 kubeletstats 接收器中启用了 limit 元数据(默认开启),EDOT 会上报 k8s.container.memory_limit_utilization,它表示容器内存使用量占其 limit 的比例(取值范围 0~1)。

以下查询找出过去一小时内容器内存使用峰值超过 85% limit 的容器:

esql 复制代码
TS metrics-*
| WHERE TRANGE(1h)
| STATS peak_mem_pct = MAX(MAX_OVER_TIME(k8s.container.memory_limit_utilization))
    BY k8s.namespace.name, k8s.pod.name, k8s.container.name
| EVAL peak_mem_pct = ROUND(peak_mem_pct * 100, 1)
| WHERE peak_mem_pct > 85
| SORT peak_mem_pct DESC

原理解释

  • MAX_OVER_TIME 在每个容器的时间序列内部找出峰值(因为 Utilization 是 Gauge,随时间波动)。
  • 外层 MAX 确保每个容器只输出一个最终峰值(由于 TS 可能产生多个时间分片,此处用 MAX 取最大值)。
  • 如果一个容器的峰值反复超过 95%,那它极有可能成为下一个 OOM 牺牲品。
  • 结合重启查询:如果一个容器同时满足"重启次数高"和"内存峰值高",几乎可以断定它因 OOM 被杀后重启了。

追踪 CPU 使用率与节点压力

CPU 问题通常表现为节流(Throttling)和响应变慢 ,而非崩溃。k8s.pod.cpu.node.utilization 指标报告每个 Pod 的 CPU 使用量占整个节点容量的比例(因此所有 Pod 的该值之和不应超过 1)。

5 分钟粒度下最繁忙的 Pod

esql 复制代码
TS metrics-*
| WHERE TRANGE(1h)
| STATS avg_cpu = AVG(AVG_OVER_TIME(k8s.pod.cpu.node.utilization))
    BY k8s.pod.name, TBUCKET(5m)
| SORT avg_cpu DESC
  • TBUCKET(5m) 将时间划分为 5 分钟桶,每个桶内 AVG_OVER_TIME 对每个 Pod 的时间序列求平均,外层 AVG 再合并同名 Pod(因为一个 Pod 可能包含多个容器,但其 Pod 级指标只有一个时间序列)。
  • 结果可渲染为时间序列图,直观展示 CPU 使用高峰。

节点级别的资源饱和检查

若要判断节点本身是否过载,直接查询节点指标:

esql 复制代码
TS metrics-*
| WHERE TRANGE(1h)
| STATS cpu = AVG(AVG_OVER_TIME(k8s.node.cpu.usage)),
        mem = AVG(LAST_OVER_TIME(k8s.node.memory.usage))
    BY k8s.node.name, TBUCKET(5m)
| SORT cpu DESC
  • k8s.node.cpu.usage 是节点 CPU 使用率(百分比,通常 0~1),k8s.node.memory.usage 是内存使用率。
  • 如果某个节点的 CPU 长期接近 100%,那它上面所有 Pod 都会受到节流影响,这很容易被误认为是应用程序自身的问题。

深入容器日志排查

当指标将你指向某个 Pod 后,日志 会告诉你它当时在做什么。EDOT 将日志消息存储在 body.text 字段,日志级别存储在 severity_text,同时附带了 k8s.* 上下文字段(与指标一致)。

按错误量对 Pod 排名

esql 复制代码
FROM logs-*
| WHERE @timestamp >= NOW() - 1 hour
  AND severity_text IN ("ERROR", "FATAL")
| STATS errors = COUNT(*)
    BY k8s.namespace.name, k8s.pod.name
| SORT errors DESC
| LIMIT 20
  • 这个查询帮你快速识别错误集中的 Pod。如果错误分布均匀,可能是集群级问题;如果集中在某个 Pod,则是该工作负载的个例。

查看特定 Pod 的日志(带关键词过滤)

esql 复制代码
FROM logs-*
| WHERE @timestamp >= NOW() - 1 hour
  AND k8s.pod.name == "checkout-<your-hash>"
  AND body.text LIKE "*timeout*"
| KEEP @timestamp, severity_text, body.text
| SORT @timestamp DESC
| LIMIT 50
  • LIKE "*timeout*" 执行通配符匹配。若想进行全文相关性搜索,可替换为 MATCH(body.text, "timeout")
  • KEEP 只保留你关心的字段,使结果更清晰。

组合查询:一次 Kubernetes 事故的完整调查链

实际运维中,我们通常串联多个查询,而非孤立运行。典型的调查循环如下:
#mermaid-svg-IUIBKrW7SDfXk7v7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IUIBKrW7SDfXk7v7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IUIBKrW7SDfXk7v7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IUIBKrW7SDfXk7v7 .error-icon{fill:#552222;}#mermaid-svg-IUIBKrW7SDfXk7v7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IUIBKrW7SDfXk7v7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IUIBKrW7SDfXk7v7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IUIBKrW7SDfXk7v7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IUIBKrW7SDfXk7v7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IUIBKrW7SDfXk7v7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IUIBKrW7SDfXk7v7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IUIBKrW7SDfXk7v7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IUIBKrW7SDfXk7v7 .marker.cross{stroke:#333333;}#mermaid-svg-IUIBKrW7SDfXk7v7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IUIBKrW7SDfXk7v7 p{margin:0;}#mermaid-svg-IUIBKrW7SDfXk7v7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IUIBKrW7SDfXk7v7 .cluster-label text{fill:#333;}#mermaid-svg-IUIBKrW7SDfXk7v7 .cluster-label span{color:#333;}#mermaid-svg-IUIBKrW7SDfXk7v7 .cluster-label span p{background-color:transparent;}#mermaid-svg-IUIBKrW7SDfXk7v7 .label text,#mermaid-svg-IUIBKrW7SDfXk7v7 span{fill:#333;color:#333;}#mermaid-svg-IUIBKrW7SDfXk7v7 .node rect,#mermaid-svg-IUIBKrW7SDfXk7v7 .node circle,#mermaid-svg-IUIBKrW7SDfXk7v7 .node ellipse,#mermaid-svg-IUIBKrW7SDfXk7v7 .node polygon,#mermaid-svg-IUIBKrW7SDfXk7v7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IUIBKrW7SDfXk7v7 .rough-node .label text,#mermaid-svg-IUIBKrW7SDfXk7v7 .node .label text,#mermaid-svg-IUIBKrW7SDfXk7v7 .image-shape .label,#mermaid-svg-IUIBKrW7SDfXk7v7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-IUIBKrW7SDfXk7v7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IUIBKrW7SDfXk7v7 .rough-node .label,#mermaid-svg-IUIBKrW7SDfXk7v7 .node .label,#mermaid-svg-IUIBKrW7SDfXk7v7 .image-shape .label,#mermaid-svg-IUIBKrW7SDfXk7v7 .icon-shape .label{text-align:center;}#mermaid-svg-IUIBKrW7SDfXk7v7 .node.clickable{cursor:pointer;}#mermaid-svg-IUIBKrW7SDfXk7v7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IUIBKrW7SDfXk7v7 .arrowheadPath{fill:#333333;}#mermaid-svg-IUIBKrW7SDfXk7v7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IUIBKrW7SDfXk7v7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IUIBKrW7SDfXk7v7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IUIBKrW7SDfXk7v7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IUIBKrW7SDfXk7v7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IUIBKrW7SDfXk7v7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IUIBKrW7SDfXk7v7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IUIBKrW7SDfXk7v7 .cluster text{fill:#333;}#mermaid-svg-IUIBKrW7SDfXk7v7 .cluster span{color:#333;}#mermaid-svg-IUIBKrW7SDfXk7v7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IUIBKrW7SDfXk7v7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IUIBKrW7SDfXk7v7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-IUIBKrW7SDfXk7v7 .icon-shape,#mermaid-svg-IUIBKrW7SDfXk7v7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IUIBKrW7SDfXk7v7 .icon-shape p,#mermaid-svg-IUIBKrW7SDfXk7v7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IUIBKrW7SDfXk7v7 .icon-shape .label rect,#mermaid-svg-IUIBKrW7SDfXk7v7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IUIBKrW7SDfXk7v7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IUIBKrW7SDfXk7v7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IUIBKrW7SDfXk7v7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

按错误量排名 Pod
检查目标 Pod 的重启次数和内存峰值
是否因 OOM 重启?
查看该 Pod 的日志,搜索 OOM 相关关键词
查看日志,搜索异常堆栈或超时
确认问题根因并采取措施

因为所有查询都使用相同的 k8s.namespace.namek8s.pod.name 字段,你可以将 Pod 名称从一个查询直接复制到下一个。这种一致性也方便构建联动仪表板------点击某个命名空间或 Pod,所有面板同步过滤。


将查询升级为告警与仪表板

本文中任何生成聚合值 的查询都可以转化为告警规则,而不仅仅是临时排查工具。

以重启查询为例,若要监控"15 分钟内重启超过 5 次"的情况,只需稍作调整:

esql 复制代码
FROM metrics-*
| WHERE @timestamp >= NOW() - 15 minutes AND k8s.container.restarts IS NOT NULL
| STATS restarts = MAX(k8s.container.restarts)
    BY k8s.namespace.name, k8s.pod.name, k8s.container.name
| WHERE restarts >= 5

将此查询配置为 Elasticsearch 查询规则(Elastic 的告警框架),当结果非空时触发告警。你就能在用户发现之前,第一时间收到通知。

同样的模式可应用于:

  • 内存利用率 > 90%
  • 节点 CPU > 95%
  • 错误日志数 > 阈值
  • Pod 相位非 Running

总结:构建你的 ES|QL 监控工具包

ES|QL 用一种统一的语法,覆盖了 Kubernetes 所有的可观测信号:

  • 对象状态(Pod 相位、重启次数)
  • 资源指标(CPU、内存、网络)
  • 容器日志(错误、堆栈、事件)
查询类型 关键函数 适用场景
集群概览 COUNT_DISTINCT 查看命名空间负载
重启排行 MAX(Gauge) 发现崩溃循环
非运行 Pod LAST_OVER_TIME 检查当前健康状态
内存压力 MAX_OVER_TIME 预测 OOM 风险
CPU 使用 AVG_OVER_TIME + TBUCKET 追踪资源热点
错误日志排名 COUNT(*) 定位故障 Pod
日志详情 LIKE / MATCH 深入排查根因

开始你的监控之旅吧:先用概览查询熟悉集群面貌,然后将重启、相位、内存、CPU 和日志查询保存为常用工具。当事故发生时,按调查链串联使用。再进一步,将高频查询制作成仪表板,将关键阈值升级为告警。

小贴士:根据你实际启用的接收器,某些指标字段名可能有所不同。请查阅 EDOT 文档,调整查询中的字段名称。但核心逻辑和聚合函数是通用的,足以应对大多数场景。

现在,你已经拥有了一套。祝排查愉快!🚀

相关推荐
想你依然心痛2 小时前
电子商务行业中的大数据应用案例
大数据·电子商务·用户画像·个性化推荐·库存管理·用户行为分析·供应链优化
延凡科技3 小时前
告别粗放灌溉!让灌区管理智能化、节水化、精细化
大数据·人工智能·科技·物联网·信息可视化
沉迷学习 日益消瘦3 小时前
09-Helm 包管理
kubernetes
Summer-Bright3 小时前
AI 芯片简报 07.21-07.24:NVIDIA Vera 亮剑、AMD 2nm GPU、Google 叛逃 CoWoS
大数据·人工智能·ai·自然语言处理·芯片·agi
带娃的IT创业者3 小时前
从印度私营火箭首飞成功看新兴航天架构的技术突围
大数据·架构·架构设计·系统工程·控制系统·火箭发射·商业航天
科技大视界4 小时前
企业 Agent 2026 选型指南:智能体应用进入深水区
大数据·人工智能
zandy10114 小时前
体验家 XMPlus 跨系统数据同步与一致性保障机制:CEM 与 CRM/ERP/BI 的双向集成架构
大数据·架构
蓝速科技4 小时前
蓝速larxu 21.5 寸会议预约屏:状态指示灯重塑大型会议室管理效能
大数据
暖和_白开水5 小时前
数据分析agent(十):loguru日志输出优化 和es 启动流程
java·elasticsearch·数据分析