前言

在云原生架构中,Tracing、Metric、Logs三类遥测数据默认是独立没有任何关联字段的;

OpenTelemetry的核心价值是:用一套统一的标准,解决可观测性数据(指标、日志、链路)的采集和导出问题,让你能高效地关联分析系统状态,快速定位未知故障。

OpenTelemetry通过TraceaID实现3类可观测性的统一关联,进行关联分析。

一次性地从宏观指标下钻到微观的错误日志和具体的问题代码行,真正实现了快速定位根因。这就是 OpenTelemetry统一可观测性数据的强大威力。

故障分析

Pod因内存不足OOM,会出现以下两种情况:

  • 单个Pod内存飙升 → 内核杀它 → Kubelet只记录,不驱逐别的 Pod

  • 单个Node内存整体紧张 → Kubelet 触发 Eviction → 驱逐其他 Pod

下文所探讨的是第1种情况。

情况1:Kubelet正常上报

容器进程因为内存超限 → Linux 内核 cgroup OOM-killer 把进程干掉。

容器运行时(containerd/cri-o)感知到进程退出 → 返回退出码给 Kubelet。

Kubelet 更新 Pod 状态到 apiserver:reason=OOMKilled

Prometheus/用户通过 kube-state-metrics 能看到。

apiVersion: v1
kind: Pod
metadata:
  name: memory-demo
  namespace: mem-example
spec:
  containers:
  - name: memory-demo-ctr
    image: polinux/stress
    resources:
      limits:
        memory: "200Mi"
      requests:
        memory: "100Mi"
    command: ["stress"]
    args: ["--vm", "1", "--vm-bytes", "250M", "--vm-hang", "1"]
test-oom.yaml

现象如下

[root@master /]# kubectl delete -f test-oom.yaml 
pod "memory-demo" deleted
[root@master /]# kubectl apply -f test-oom.yaml 
pod/memory-demo created
[root@master /]# kubectl get pod -n mem-example -o wide -w
NAME          READY   STATUS              RESTARTS   AGE   IP       NODE    NOMINATED NODE   READINESS GATES
memory-demo   0/1     ContainerCreating   0          4s    <none>   node1   <none>           <none>
memory-demo   1/1     Running             0          18s   10.244.166.150   node1   <none>           <none>
memory-demo   0/1     OOMKilled           0          19s   10.244.166.150   node1   <none>           <none>

情况2:Linux内核killer干掉了pod但kubelet没有上报

Pod中进程分配内存过快 → 内核先杀进程 → Kubelet还没检查到 → 没有 OOMKilled 状态 → Prometheus 监控不到

如果1个Pod分配内存的速度太快,以至于Kubelet没有在默认的检查窗口(默认10s)中发现它;

Pod内存使用试图超过可分配内存,加上硬驱逐阈值的总和,那么Linux内核的OOM killer将介入并强行终止Pod容器中的1个或N个进程。

apiVersion: v1
kind: Pod
metadata:
  name: memory-demo
  namespace: mem-example
spec:
  containers:
  - name: memory-demo-ctr
    image: polinux/stress
    resources:
      limits:
        memory: "3Gi"
      requests:
        memory: "3Gi"
    command: ["stress"]
    args: ["--vm", "1", "--vm-bytes", "500M", "--vm-hang", "1"]
test-oom.yaml

 此时Pod会突然完全消失,Prometheus也发现不了;

只能采用监控系统+日志系统对该现象进行监控报警,需要通过日志系统收集Node节点的日志进行佐证;

stress --vm 4 --vm-bytes 730M --vm-keep

查看K8s node的/var/log/messages日志可发现Pod中运行进程被该node宿主机的Linux内核Kill掉的日志;

1.监控结合日志系统

监控+日志结合起来监控Pod进程被Linux系统内核Kill导致的OOM事件;

2.missing-container-metrics 

也可以在每个Node上运行missing-container-metrics

Kubernetes 默认情况下使用 cAdvisor 来收集容器的各项指标,足以满足大多数人的需求,但还是有所欠缺,比如缺少对以下几个指标的收集:

OOM kill

容器重启的次数

容器的退出码

missing-container-metrics 这个项目弥补了 cAdvisor 的缺陷,新增了以上几个指标,集群管理员可以利用这些指标迅速定位某些故障。例如,假设某个容器有多个子进程,其中某个子进程被 OOM kill,但容器还在运行,如果不对 OOM kill 进行监控,管理员很难对故障进行定位。

3.OpenTelemetry

通过可观测平台定位PodOom故障发生原因;

使用Top命令

我们平时会部署一些应用到Linux服务器,所以经常需要了解服务器的运行状态;

Top命令是帮助我们了解服务器当前的CPU、内存、进程状态的实用工具;

在node宿主机使用top命令查看被Pod管理的容器进程的内存占用情况

1.top快速入门

(base) [root@docker /]# top
top - 09:57:23 up 137 days, 25 min,  2 users,  load average: 0.05, 0.03, 0.05
Tasks: 148 total,   1 running, 147 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 32779568 total, 31119584 free,   636676 used,  1023308 buff/cache
KiB Swap:        0 total,        0 free,        0 used. 31707740 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                 
 8753 root      20   0  822868  47888   8040 S   0.7  0.1  11:52.10 python3.8                                                               
    1 root      20   0  191028   4040   2604 S   0.0  0.0   0:46.16 systemd                                                                 
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.26 kthreadd                                                                
    4 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 kworker/0:0H                                                            
    5 root      20   0       0      0      0 S   0.0  0.0   0:01.76 kworker/u16:0                                                           
    6 root      20   0       0      0      0 S   0.0  0.0   0:00.01 ksoftirqd/0                                                             
    7 root      rt   0       0      0      0 S   0.0  0.0   0:00.06 migration/0                                                             
    8 root      20   0       0      0      0 S   0.0  0.0   0:00.00 rcu_bh                                                                  
    9 root      20   0       0      0      0 S   0.0  0.0   3:13.03 rcu_sched                                                               
   10 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 lru-add-drain                                                           
   11 root      rt   0       0      0      0 S   0.0  0.0   0:46.57 watchdog/0                                                              
   12 root      rt   0       0      0      0 S   0.0  0.0   0:41.06 watchdog/1                                                              
   13 root      rt   0       0      0      0 S   0.0  0.0   0:00.05 migration/1                                                             
   14 root      20   0       0      0      0 S   0.0  0.0   0:00.02 ksoftirqd/1                                                             
   16 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 kworker/1:0H                                                            
   17 root      rt   0       0      0      0 S   0.0  0.0   0:39.13 watchdog/2                                                              
   18 root      rt   0       0      0      0 S   0.0  0.0   0:00.07 migration/2                                                             
   19 root      20   0       0      0      0 S   0.0  0.0   0:00.01 ksoftirqd/2                                                             
   21 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 kworker/2:0H                                                            
   22 root      rt   0       0      0      0 S   0.0  0.0   0:40.14 watchdog/3       

2.系统状态概览

第一行是对当前Linux系统情况的整体概况;

top - 09:57:23 up 137 days, 25 min,  2 users,  load average: 0.05, 0.03, 0.05

top:当前处在Top命令模式,系统时间

up:Linux操作系统运行了多久

users:当前活跃用户

load average:5分钟、10分钟、15分钟之内的CPU负载

2.1.EWMA时序平滑统计方法

EWMA和中位数都能平滑瞬时数据的毛刺(异常值),

中位数:像体检1次性测心跳,只取当下中间数值,偶尔心跳突快突慢直接忽略,不记你之前心跳咋样。

EWMA:像持续监测实时心跳,保留你过往心跳92%的状态,只纳入当下8%的新波动,有历史记忆,慢慢跟着真实心率变化,不会被瞬间早搏毛刺带偏。

EWMA是一种经典的、一直在用的时序数据平滑统计方法,全称Exponentially Weighted Moving Average(指数加权移动平均)。

Linux Load Average 只是它最知名的应用场景之一。

2.2.LoadAverage

操作系统的CPU调度的最小单位是线程,系统Load Average 本质就是统计系统中处于

  • R(Running/Runnable):正在运行 或 就绪排队等 CPU 的线程
  • D(Uninterruptible Sleep):不可中断 IO 阻塞(如磁盘、网络 IO)的线程

两种态两种线程数量,经EWMA平滑统计算法计算后,得的时序均值。

  • R 多:开启的线程太多,排队等CPU → Load 高、CPU 低
  • D 多:磁盘或网络的I/O瓶颈(etcd 最常见)→ Load 高、CPU 低

2.3.Linux系统load过载原因分析

线程 = 汽车, CPU = 马路车道数(比如 8 核 = 8 车道), Load = 路口排队总车辆数

R 状态(Runnable)= 车多路窄

车都能跑、没故障、没堵车,就是车太多,远大于车道数,在路口排队等放行。→ 结果:Load 高(排长队)、CPU 低(车道空着,一次只能过 8 辆)

D 状态(Uninterruptible Sleep)= 路上有故障车故导致堵车

车不多,但车道被堵死(磁盘慢 / 网络卡 /etcd 挂了),车卡在半路动不了,既不能走也不能退。→ 结果:Load 高(堵在路上算排队)、CPU 低(车不动,车道闲置)

一句话总结

  • R 高:车太多,路不够宽(线程爆炸,CPU 核数少)
  • D 高:路堵死,车动不了(I/O 瓶颈,磁盘 / 网络慢)

3.进程状态概览

Tasks: 148 total,   1 running, 147 sleeping,   0 stopped,   0 zombie

Tasks:当前系统中运行的进程总数

Running:当前系统中处在running  状态的进程个数

Sleeping:当前系统中处在sleeping状态的进程个数

Stopped:当前系统中处在Stopped状态的进程个数

Zombie:当前系统中处在Zombie状态的进程个数,子进程比父进程先结束,父进程无法获取到子进程的Exit状态,该进程称为僵尸进程。

4.CPU状态概览

%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

CPU分为用户态和内核态

us:CPU在用户态花费的时间 ,应该<60%

sy:CPU在内核态花费的时间,sy+us<80%

id:CPU在空闲状态花费的时间

wa: 进程执行IO操作占用CPU的时间,<30%,不同功能的服务器,阈值不一,邮件服务器的wa很高;

5.内存使用概览

以CentOS-7.6为例

[root@node1 ~]# cat /etc/centos-release
CentOS Linux release 7.6.1810 (Core) 
[root@node1 ~]# cat /proc/version 
Linux version 3.10.0-957.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC) ) #1 SMP Thu Nov 8 23:39:32 UTC 2018
[root@node1 ~]# 

讲解Linux的内存

KiB Mem : 32779568 total, 31119584 free,   636676 used,  1023308 buff/cache
KiB Swap:        0 total,        0 free,        0 used. 31707740 avail Mem 

5.1.buff/cache占用内存的意义

Linux系统设计哲学之一是一切皆文件,所以该系统对IO性能要求比较高;

buffer:用于存放从内存即将输出到磁盘的数据

cache:用于存放从磁盘读取到内存中,待今后使用的数据

Linux借内存空间,造buff/cache,是为了提升Linux系统的IO性能,使Linux用起来更加流畅;

当进程可使用的内存空间严重不足时,Linux会把借用的buff/cache内存空间让进程占用,虽然内存空间还回来了,但此时Linux会变得卡顿起来。

5.2.如何查看进程使用的内存空间大小

[root@node1 ~]# free -h
              total        used        free      shared  buff/cache   available
Mem:           7.1G        2.0G        2.8G        397M        2.3G        4.3G
Swap:            0B          0B          0B
[root@node1 ~]# 
  • total:total=used(进程使用的内存)+buff/cache(提升Linux系统的IO性能花销的内存)+free(空闲的)
  • used:正在运行的进程使用的内存(used= total – free – buff/cache
  • free: 未使用的内存 (free= total – used – buff/cache)
  • shared:多个进程共享的内存
  • buffers:内存保留用于内核操作一个进程队列请求
  • cache:在 RAM 中保存最近使用的文件的页面缓存的大小
  • buff/cache:Buffers + Cache
  • available:在不使用Swap分区的前提下,预计有多少内存可用于将程序启动为进程,available = free + buffer/cache (注:只是大概的计算方法)

6.进程详细状态

 PID   USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM  TIME+      COMMAND                                                                 
 8753  root      20   0  822868  47888   8040 S   0.7  0.1  11:52.10   python3.8    

PID:进程编号

USER:执行进程的用户

PR(Priority):进程优先级,由OS决定的;(PR越低越优先被CPU调度),越低越优先

NI(Nice):进程的Nice值,用户可以设置进程的执行优先权,root(-20到19),普通user 0-19

VIRT(Virtual memory usage ):进程需要的虚拟内存空间大小

RES(Resident Memory Usage):进程当前实际占用的物理内存大小,但不包括swap out 

SHR(Shared Memory):除了自身进程的共享内存,也包括其他进程的共享内存  

S(Status):进程状态D(Dead)R(运行) T(跟踪/停止)Z(僵尸)

%CPU:当前进程占用CPU时间的百分比

%MEM:当前进程占用内存空间的百分比

TIME+:当前进程使用CPU时间的总和

COMMAND:命令行

6.1.虚拟内存和物理内存的关系

虚拟内存和物理内存通过Page Table关联起来。

虚拟内存空间中着色的部分,分别被映射到物理内存空间对应相同着色的部分。

然而虚拟内存空间中的灰色部分,在物理内存空间中没有与之对应的部分,也就是说灰色部分没有被映射到物理内存空间中。

如此设计是本着“按需映射”的指导思想。

因为虚拟内存空间很大,虚拟内存中很多部分,在1次程序运行过程中根本不需要访问;

所以也就没有必要,将虚拟内存空间中的这些不需要被访问的部分映射到物理内存空间上。

6.2.虚拟内存(Virtual Memory)

VIRT=Swap分区+Resident Memory

虚拟内存是1个假象的内存空间,在程序运行过程中虚拟内存空间中需要被访问的部分会被映射到物理内存空间中

虚拟内存空间大只能表示1个程序运行过程中可访问的内存空间比较大,不代表占用实际物理内存的空间

6.3.驻留内存(Resident Memory)

RES=Code+Data

驻留内存,是指那些被映射到虚拟内存空间中的物理内存。

上图中,在系统物理内存空间中被着色的部分都是驻留内存。

比如,A1、A2、A3和A4是进程A的驻留内存,B1、B2和B3是进程B的驻留内存。

驻留内存就是进程实实在在占用的物理内存,一般我们所讲的进程占用了多少内存,其实就是说的占用了多少驻留内存而不是多少虚拟内存。

因为进程的虚拟内存大,并不意味着进程运行过程成所占用的物理内存大。

6.4.共享内存(Shared Memory)

多个进程之间通过共享内存的方式相互通信也会出现了共享内存。

7.常用快捷键

  • shift+e:切换内存单位显示格式 (可重复按键切换)
  • z:换是否彩色显示(可复按键换
  • m: 切换内存使用率显示格式(可重复按键切换)
  • e:切换底部各个进程详细中单位的显示模式 (可重复按键切换)
  • b:切换高亮选中 (可重复按键切换)
  • W:把当前配置保存到文件中,下次启动top会使用当前的配置
  • h:进入帮助菜单(进入菜单后,可按ESC或q退出帮助菜单)
  • q:退出 top命令

8.字段排序

top底部的进程列表信息是可以选择指定列进行排序的

  • 按f进入字段选择界面
  • 按上下键选择要进行排序的字段/ space键选择该字段是否显示
  • 按下s键激活这个选择
  • 按q键退出排序字段选择界面

Node宿主机内存不足监控方案

Pod是K8s中的逻辑单位,当Pod被调度在Node宿主机之后,Pod本质就是运行在Node宿主机的1-n个容器进程

ps -ef | grep "app-workspace-373-1675214627414" | grep -v grep

我们可以通过容器进程的名称来识别这些容器进程,之前属于哪1个Pod;

[root@byg612sv036 ~]# docker ps | grep "app-workspace-373-1675214627414" | grep -v grep
040582751cbc   registry.docker.aimaster.lenovo.com:20443/library/373/workspace                     "jupyter lab --port …"   8 minutes ago    Up 8 minutes              k8s_app-workspace-373-1675214627414-podgroup-pfhh-pod_app-workspace-373-1675214627414-podgroup-pfhh-d42q6_aimaster-user-namespace-373_4881e655-9c3e-41a0-81d7-187ce8bcbafc_0
1d331fcc018b   registry.access.redhat.com/rhel7/pod-infrastructure:latest                          "/usr/bin/pod"           8 minutes ago    Up 8 minutes              k8s_POD_app-workspace-373-1675214627414-podgroup-pfhh-d42q6_aimaster-user-namespace-373_4881e655-9c3e-41a0-81d7-187ce8bcbafc_0
[root@byg612sv036 ~]# ps -ef | grep "app-workspace-373-1675214627414" | grep -v grep
root     43434 43404  0 09:58 pts/0    00:00:04 /usr/bin/python /usr/local/bin/jupyter-lab --port 8888 --allow-root --config='/dfs/share-read-only/jupyter_notebook_config.py' --LabApp.base_url='/app-workspace-373-1675214627414' --NotebookApp.token='6745c8z8'

1./proc/pid

Linux一切皆文件, 每1个进程的详细信息,都记录在/proc/pid目录下;

  • /proc/pid/cmdline 进程启动命令
  • /proc/pid/cwd 链接到进程当前工作目录
  • /proc/pid/environ 进程环境变量列表
  • /proc/pid/exe 链接到进程的执行命令文件
  • /proc/pid/fd 包含进程相关的所有的文件描述符
  • /proc/pid/maps 与进程相关的内存映射信息
  • /proc/pid/mem 指代进程持有的内存,不可读
  • /proc/pid/root 链接到进程的根目录
  • /proc/pid/stat 进程的状态
  • /proc/pid/statm 进程使用的内存的状态
  • /proc/pid/status 进程状态信息,比stat/statm更具可读性
  • /proc/self 链接到当前正在运行的进程
[centos@docker 8751]$ ps -ef | grep python
root       867     1  0  2022 ?        00:20:30 /usr/bin/python2 -Es /usr/sbin/tuned -l -P
root      8751     1  0 Feb22 ?        00:00:00 python3.8 manage.py runserver 0.0.0.0:8001
root      8753  8751  0 Feb22 ?        00:30:33 /usr/local/miniconda3/bin/python3.8 manage.py runserver 0.0.0.0:8001
centos   12048 12007  0 09:50 pts/0    00:00:00 grep --color=auto python
[centos@docker 8751]$ pwd
/proc/8751
[centos@docker 8751]$ ls
ls: cannot read symbolic link cwd: Permission denied
ls: cannot read symbolic link root: Permission denied
ls: cannot read symbolic link exe: Permission denied
attr        cmdline          environ  io         mem         ns             pagemap      sched      stack    task
autogroup   comm             exe      limits     mountinfo   numa_maps      patch_state  schedstat  stat     timers
auxv        coredump_filter  fd       loginuid   mounts      oom_adj        personality  sessionid  statm    uid_map
cgroup      cpuset           fdinfo   map_files  mountstats  oom_score      projid_map   setgroups  status   wchan
clear_refs  cwd              gid_map  maps       net         oom_score_adj  root         smaps      syscall
[centos@docker 8751]$ '
View Code

2.cgroups

cgroups,其名称源自控制组群(control groups)的简写,是Linux内核的一个功能;

用来限制、控制与分离一个进程组资源(如CPU内存磁盘输入输出等)。

[root@node1 43359]# ps -ef | grep stress
root      43359  43338  0 16:09 ?        00:00:00 stress --vm 1 --vm-bytes 150M --vm-hang 1
root      43372  43359  5 16:09 ?        00:01:10 stress --vm 1 --vm-bytes 150M --vm-hang 1
root      62876  26220  0 16:28 pts/2    00:00:00 grep --color=auto stress
[root@node1 43359]# docker inspect --format '{{.Config.Hostname}}' $(cat /proc/43359/cgroup|awk -F 'docker-' '{print $2}' |cut -c1-12| head -n 1)
memory-demo
[root@node1 43359]# 

3.为什么要监控容器进程但要输出Pod信息

在MLOps平台生产环境,Pod内部代码不可控,极易出现 Node或Pod内存瞬间耗尽的情况。

此类极端OOM或Pod短生命周期场景下,Kubernetes控制平面可能无法及时收到Pod被系统内核杀掉的事件,导致事件丢失、监控盲区和告警失效。

为了确保业务容器异常死亡能够被及时发现,必须在 Pod 启动时注册自身身份或心跳信息,由 Node 层或监控系统结合进程状态、内核 OOM 日志等判断Pod是否异常退出,并立即触发告警,从而防止 unnoticed failure。

3.1.Prometheus自动监控的局限性

process-exporter和node-problem-detector能直接监控Linux系统进程和发现内核事件,但有一个致命问题,PID无法把容器、Pod、Node信息关联绑定启动,对动态业务容器不可靠。

3.2.Kubenetes自带监控的局限性

在Kubernetes自带的监控体系中:

kubelet

负责Node节点上Pod与容器的生命周期管理,并定期将容器状态上报至APIServer;

kube-state-metrics

负责监听APIServer,获取Pod 状态、副本数、重启次数等对象状态信息。

cAdvisor

cAdvisor全称ContainerAdvisor,是Google开发的一个开源工具,用于实时监控容器的资源使用和性能指标,特别适合Docker或Kubernetes 环境。

cAdvisor可以收集CPU、内存、磁盘 I/O、网络等指标,并提供Web UI可视化界面,同时可以输出 Prometheus格式指标供监控系统采集。

kubelet内置的cAdvisor组件负责采集容器的CPU、内存等资源使用指标,并通过http指标接口对外暴露给Prometheus等监控系统抓取。

三者协同构成了Kubernetes资源与状态监控的完整基础体系。

 3.3.结合Prometheus和K8s自研OOM监控方案

在MLOps平台场景下,Pod OOM故障具有明显特殊性:

用户算法代码不可控、内存使用波动极大、峰值突高突增,极易在瞬间把节点资源打满,此时K8s原生监控体系缺陷会被进一步放大:

kubelet出现状态上报滞后甚至失败,cAdvisor无法感知内核级强杀事件,且kube-state-metrics高度依赖APIServer状态同步,最终导致严重的监控盲区与告警失效

为此,本机制用于弥补Kubelet、cAdvisor、kube-state-metrics三者在极端OOM 场景下的监控不足。

实现脱离Kubernetes控制平面的内核级Pod OOM精准溯源。

Downward API

K8s分为控制平面和工作平面

  • Control Plane(上层) APIServer、etcd、调度器~控制器管理器
  • 工作平面(下层):Kubeproxy、kubelet、Pod/Container、CNI等

把上层才能拿到的元信息,往下传给容器里的应用,就叫Downward,Downward API就是往下传递信息的API,把Pod/Node信息注入到容器里的机制。

步骤1:Pod/主容器中注入身份信息

在CD时给容器中注入该容器自身相关的身份信息,包含所在的集群名称/Node名称/Pod名称/业务容器名称/主进程PID信息。

步骤2:相关身份自动注册到本地文件

Pod启动时,业务容器写入自己的

  • PID
  • Pod 名称
  • Namespace
  • Node
  • 启动时间

信息到Node的固定目录(如 /var/lib/pod_status/<pod>.prom

步骤3:扩展NodeExporter监控指标

在每个Node上运行OOM监控脚本,定时读取Pod身份注册文件中记录的容器进程PID,检查PID进程的状态,将进程状态输出Textfile Collector,定时更新生成Prometheus指标,被Prometheus抓取到时序数据库,并根据告警规则Push到AlerManager发出告警到用户。

 

 

 

参考

posted on 2023-02-23 19:59  运维体系建设之路  阅读(906)  评论(0)    收藏  举报