惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

小松鼠的博客

记录一次线上k8s工作节点无法创建容器的问题排查思路与解决办法 记一次线上GoLang项目OOM排查过程 从LastPass转向拥抱开源KeePass的心路历程 故障定位与 AI 结合前后端编码实践 FileBeat收集nginx-ingress-controller日志 2024年最新关闭火绒安全工具的开机自启方法 Kubernetes任务调度实践-Go语言实现Job和CronJob对比分析 离线更新k8s环境下的trivy漏洞库方法 使用Go语言接入Choerodon实现基于OAuth2的统一身份认证登录 在Vue2中自定义Switch组件并实现父子组件双向数据绑定 关于docker jdk1.8镜像中的GB18030-2022标准支持及验证 Go框架gin中的session存储gin-contrib-sessions和go-session 关于修改node_module中的源码问题记录 docker-compose网络和内网服务IP冲突问题 慎用存储过程:一条语句引发的数据库存储100%占用 Spring Boot中4种文件下载方法的实现 避坑-不能将specific类型的gitlab-runner改变为share类型 Docker compose中的MySQL主从复制模式和percona-toolkit工具使用 在minio中开启https访问以及使用rclone备份minio桶 在多机Docker环境下部署Choerodon的解决方案 Prometheus中Monitor添加对SpringBoot Actuator的Basic认证 在Nginx的容器镜像中隐藏Nginx的Server响应头 K8s中的两种nginx-ingress-controller及其区别 两个docker工具:runlike和whaler Grafana中的邮件报警和截图插件grafana-image-enderer K8s中externalName-service和services-without-selectors maven配置文件settings.xml中的一些概念总结 K8s中flexvolume插件驱动的安装 K8s中的coredns无法解析svc问题排查 K8s中使用Ingress访问请求体过大问题解决 关于k8s中对于SpringBoot应用TCP类型的就绪探针不准确的问题发现 K8s中的环境变量与应用程序的对应关系与操作 SpringMVC4升级为SpringBoot2实战 在Vmware中Ubuntu22.04的vm-tools和网络问题 修改k8s节点主机名并重新加入集群 离线安装Grafana插件 Spring Data Jpa 中使用CriteriaBuilder动态拼接SQL 在SpringBoot项目配置Liquibase数据库版本管理 记录Vue中父子组件传值的实战应用 实现单例模式的8种方法 三种常用的生产者消费者模式实现 使用两个线程交替打印0-100的奇偶数 关于部署于JBoss5中的Spring应用获取项目真实部署路径的问题 获取下一个完全对称日 通过短信验证码验证修改密码的解决方案 在Win10中使用Win+R快速启动软件 使用RSA加解密时注意Cipher.getInstance(String var0,Provider var1)提供的Provider是否正确 在RestEasy2.x中解决接口重复提交问题 几道简单的CTF题目思路 重温Spring---Spring事务控制与基于XML和注解的配置方法 重温Spring---Spring AOP基于XML和注解的配置 重温Spring---AOP动态代理和Spring AOP及其基本原理 重温Spring---Spring IOC基于XML和注解的配置和比较 在Windows10中安装MySQL5.7 Zip版本及常用配置 重温Spring---使用Spring IOC解决程序耦合 策略模式与责任链模式实战应用 Linux上直接打开war包修改文件 在Windows上运行两个微信的简单脚本 ThreadPoolExecutor的使用方法与分页查询数据实例 IDEA中Shelve Changes 和 Git Stash 通过resteasy发布RESTful接口 解决前端请求后台接口,后台报错Can not deserialize instance of java.util.ArrayList out of START_OBJECT token 使用VBA脚本汇总Excel文档 使用Jenkins+GitLab实现自动部署vue项目 Kubernetes:使用hostPath挂载nginx集群的配置文件和html 彻底搞定VirtualBox虚拟机的网络设定 在Docker中安装MySQL5.7并开启远程访问(附授权和修改密码方式) 利用git命令和java文件流 获取自己改动过的文件 浅谈Spring定时任务的使用(Scheduled注解) 在Spring项目简单配置Flyway(V4.2版本)数据库版本管理 解决Spring单元测试中因外键关联导致的失败integrity constraint violation:foreign key no action Redis安装与哨兵模式配置入门 关于Vue中使用Element-UI样式row-class-name失效的问题 Element-UI中实现可动态增加行列和可编辑单元格的表格 Windows系统查看端口占用、结束进程方法和命令 层次分析法(AHP)分析步骤与计算方法 源码分析之解决layui框架重载表格时额外参数不清空的问题 Spring Data Jpa 返回自定义对象(实体部分属性、多表联查) 如何将一个jar放到本地maven仓库中 关于SSM项目停止Tomcat时Log4j出现java.lang.NoClassDefFoundError: 获取el-table单元格值并根据该值对元素自定义样式渲染 解决Git每次push都要重新输入账号密码和HttpRequestException encountered的问题 解决前后端分离项目中Vue不带cookies的问题 SSM集成Shiro自定义权限过滤器不执行解决方案 SSM集成Shiro不进入自定义Realm的doGetAuthorizationInfo的解决方案 Vue+SSM中使用Token验证登录 Git拉代码推送代码提示密码错误如何修改 Git配置SSH Key(Git配置多个账户) 安装Tomcat服务器以及错误汇总(tomcat8.0、jdk8) 关于我
K8s云原生环境下文件描述符占用过高查询思路
2024-06-14 · via 小松鼠的博客

在K8s云环境下,如何查询某一Linux系统进程ID与Pod对应关系需要一定的技巧。本文利用在K8s集群中某一机器的文件描述符占用过高的问题,排查定位到对应Pod的一次经验记录Linux系统进程ID与Pod对应关系查询思路。

使用的Docker版本:19.03.5,部署机器:Red Hat Enterprise Linux Server 7.9(maipo),K8s版本:v1.17.2

使用Promethues监控机器(node_filefd_allocated{instance=~"$node"}),发现使用的文件描述符不断升高。
可以使用PromSQL直接查询机器所有Pod占用的文件描述符。

sort_desc(sum by (pod, namespace) (container_file_descriptors{container!="POD", id=~"/kubepods.slice/.*",instance="10.xx.xx.xx:10250"}))

查询结果如下所示:

{namespace="xxx", pod="api-67abc-7cb496dcd8-7p8rx"}6919
{namespace="xxx", pod="api-mock-7449d9649-z7llj"}3943
{namespace="yyy", pod="admin-a95c3-6965b98dbd-sr988"}1147
{namespace="zzz", pod="sale-57fd89fcbd-qxzkc"}1082
{namespace="yyy", pod="demo-test-8899b9c4c-f5955"}1080

本文提供另一种思路,从宿主机器的进程ID一路定位到具体的Pod。

确定问题

首先确定 Kubernetes 节点上的 Linux 系统最大可打开文件数量:

$ cat /proc/sys/fs/file-max
100000

再多次查看当前已打开的文件数量:

$ cat /proc/sys/fs/file-nr
76335 0 100000
$ cat /proc/sys/fs/file-nr
79220 0 100000

发现不断上涨中,已逼近系统的最大可打开文件数量极限。

初步判定

由于是K8s节点,首先想到的就是docker或kubelet是不是占用了过多文件描述符。通过以下命令查看Docker占用的文件描述符,kubelet查看方式也类似。

$ ps -ef | grep docker
root         456       1  0 09:52 ?        00:00:56 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
$ ls /proc/456/fd | wc -l
2110

这里说一下 Docker 默认使用 unix domain socket (IPC socket) 进行本地通讯,而 kubelet 通过 dockershim 将 CRI 请求转换成相应的 Docker API 请求发给 dockerd (Docker Daemon) 进程,所以 /var/run/docker.sock 是由 kubelet 使用。

要查看某个进程到底打开了多少文件,正常情况下 lsof当然可以做到,但是在极端情况下,lsof 都是无法正常使用的,我们这时就要通过 /proc 虚拟文件系统来查看进程的数据,/proc/${pid}/fd 这个路径下的文件与进程所打开的文件是一对一的关系,所以我们统计  /proc/${pid}/fd 路径下文件的数量就能够得到进程打开文件的数量。[1]

查看是否由某个进程疯狂打开 /var/run/docker.sock 导致。

$ ss -a | grep "docker.sock" | wc -l
1054
$ ss -a | grep "docker.sock" | wc -l
1099

使用脚本持续打印出 kubelet 与全局已打开文件数量,查看打开文件数的增量

$ cat << 'EOF' > kubelet.sh
cat << 'EOF' > kubelet.sh
while true
do
    sock=$(ss -a | grep "docker.sock" | wc -l)
    fd=$(ls /proc/767196/fd | wc -l)
    file_nr=$(cat /proc/sys/fs/file-nr)
    echo "docker.sock: $sock; file opened: $fd; file-nr: $file_nr"
    sleep 10
done
EOF

$ sh kubelet.sh
docker.sock: 1099; file opened: 1690; file-nr: 92224 0 100000
docker.sock: 1099; file opened: 1690; file-nr: 92192 0 100000
docker.sock: 1099; file opened: 1690; file-nr: 92224 0 100000

检查后发现占用并不高,一定是有其它的服务在占用,从业务容器上下手。
使用脚本来观察所有进程使用文件的增量情况:

$ cat << 'EOF' > all.sh
while true
do
    total_files=0
    for proc in $(find /proc/ -maxdepth 1 -type d -name "[0-9]*")
    do
        fd=$(ls $proc/fd | wc -l)
        if [[ $fd -gt 500 ]]; then
            pid=$(echo $proc | awk -F/ '{print $3}')
            echo "Process $pid opened $fd files"
            total_files=$((total_files + fd))
        fi
    done
    echo "Total files opened: $total_files"
    echo "==========================="
    sleep 10
done
EOF 

$ sh all.sh
process 767196 opened 2772 files
process 2481311 opened 3940 files
ls: cannot access /proc/2525888/fd: No such file or directory 
ls: cannot access /proc/2525889/fd: No such file or directory 
process 3663251 opened 4201 files 
process 3663411 opened 1380 files
process 4007274 opened 5637 files

根据进程ID找出Pod

前面通过脚本找出了打开文件数较多的进程ID,通过以下几步可以确认对应的Pod

pstree指令打印进程树

通过pstree指令,逐个打印出进程树,确认进程名

$ pstree -s 767196
systemd───dockerd───206_[{dockerd}]
$ pstree -s 2481311
systemd───kubelet───83*[{kubelet}]
$ pstree -s 3663251 
systemd───containerd───containerd-shim───tini───java───99_[{java}]
$ pstree -s 4007274 
systemd───containerd───containerd-shim───tini───java───64*[{java}]

由于我们大多数应用都是Java应用,确认Java进程就是我们要找的进程

找父进程

通过ls /proc/<pid>/fd找到的进程可能是一个java进程的子进程,可以通过ps指令查找父进程。
比如我这里3663251的父进程是3663411,才是真正的Java进程。

$ ps -o ppid= -p 3663251 
3663411

或者使用pstree -p也可以打印进程树时把进程ID同时显示出来

$ pstree -sp 3663251
systemd(1)───containerd(1666)───containerd-shim(2711841)───copy_plugins.sh(2711896)───java(3663411)───java(3663251)─┬─{java}(2712617)                                                                                ├─{java}(2712619)
.....

找进程对应的容器

依靠docker inspect命令,找出进程对应的容器ID,如果没查到,可能Pid是子进程ID,用父进程ID试试,如下:

$ docker ps -q | xargs -I {} docker inspect {} | grep -B 20 '"Pid": 3663411'

$ docker ps -q | xargs -I {} docker inspect {} | grep -B 20 '"Pid": 2711896'
    {
        "Id": "82414f376d1f157d67c526cd8209b6d55f986e90ee8ea453ff59cb1857856a32",
        "Created": "2023-12-13T08:31:00.001807558Z",
        "Path": "/tmp/scripts/copy_plugins.sh",
        "Args": [],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 2711896,
$ docker ps -q | xargs -I {} docker inspect {} | grep -B 20 '"Pid": 2711896' | grep -oP '"Id": "\K[^"]+'
82414f376d1f157d67c526cd8209b6d55f986e90ee8ea453ff59cb1857856a32

或者使用ll -tr打印出进程打开的文件

$ ll -tr /proc/2711896/fd | tail -n 50
...
lr-x------ 1 root root 64 June 13 22:36 4560 -> /run/docker/containerd/82414f376d1f157d67c526cd8209b6d55f986e90ee8ea453ff59cb1857856a32/4sdf897632141of34265lkh32134j53n0991234jl2k3jj41092443354365tdf87-stderr
l-wx------ 1 root root 64 June 13 22:36 4558 -> /run/docker/containerd/82414f376d1f157d67c526cd8209b6d55f986e90ee8ea453ff59cb1857856a32/4sdf897632141of34265lkh32134j53n0991234jl2k3jj41092443354365tdf87-stderr
lr-x------ 1 root root 64 June 13 22:36 4556 -> /run/docker/containerd/82414f376d1f157d67c526cd8209b6d55f986e90ee8ea453ff59cb1857856a32/4sdf897632141of34265lkh32134j53n0991234jl2k3jj41092443354365tdf87-stdout
l-wx------ 1 root root 64 June 13 22:36 4554 -> /run/docker/containerd/82414f376d1f157d67c526cd8209b6d55f986e90ee8ea453ff59cb1857856a32/4sdf897632141of34265lkh32134j53n0991234jl2k3jj41092443354365tdf87-stdout
...

找到类似上面的输出,其中82414f376d1f157d67c526cd8209b6d55f986e90ee8ea453ff59cb1857856a32就是容器ID。

找容器对应的Pod

使用如下命令可以将对应容器ID的NS和Pod名打印出来。

$ kubectl get pods --all-namespaces -o=jsonpath='{range .items[_]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{range .status.containerStatuses[_]}{.containerID}{"\n"}{end}{end}' | grep 82414f376d1f157d67c526cd8209b6d55f986e90ee8ea453ff59cb1857856a32 | awk '{print $1" " $2}'
NamespaceName1 PodName

出现了 error array index 错误,可能是因为 JSONPath 表达式中的数组索引 _使用不当。您可以尝试以下修改:

修正 JSONPath 表达式: 使用 [*] 来遍历数组,而不是 [_]

kubectl get pods --all-namespaces -o=jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{range .status.containerStatuses[*]}{.containerID}{"\n"}{end}{end}' | grep 82414f376d1f157d67c526cd8209b6d55f986e90ee8ea453ff59cb1857856a32 | awk '{print $1" " $2}'

此时可以去找对应的业务系统排查问题了。

进一步找出原因

前面使用ll -tr /proc/2711896/fd | tail -n 50发现很多类似lr-x------ 1 root root 64 June 13 22:36 4560 -> /run/docker/containerd/82414f376d1f157d67c526cd8209b6d55f986e90ee8ea453ff59cb1857856a32/4sdf897632141of34265lkh32134j53n0991234jl2k3jj41092443354365tdf87-stdout这样的软链接指向输出。
这些都是命名管道文件,而我们常用的 | 竖线符号是匿名管道,管道经常被用于父子进程间通讯

这些管道文件的命名都是以 /run/docker/containerd/82414f376d1f1 为前缀, 82414f376d1f1 就是某个容器的 ID

这些文件都是用来做啥的?

先看看 Docker 源码 https://github.com/moby/moby,创建命名管道也就是 Linux FIFO 的源码在 https://github.com/moby/moby/blob/v19.03.12/libcontainerd/remote/client_linux.go#L97-L122

func newFIFOSet(bundleDir, processID string, withStdin, withTerminal bool) *cio.FIFOSet {
    config := cio.Config{
        Terminal: withTerminal,
        Stdout:   filepath.Join(bundleDir, processID+"-stdout"),
    }
    paths := []string{config.Stdout}
 
    if withStdin {
        config.Stdin = filepath.Join(bundleDir, processID+"-stdin")
        paths = append(paths, config.Stdin)
    }
    if !withTerminal {
        config.Stderr = filepath.Join(bundleDir, processID+"-stderr")
        paths = append(paths, config.Stderr)
    }
    closer := func() error {
        for _, path := range paths {
            if err := os.RemoveAll(path); err != nil {
                logrus.Warnf("libcontainerd: failed to remove fifo %v: %v", path, err)
            }
        }
        return nil
    }
 
    return cio.NewFIFOSet(config, closer)
}

再找到 newFIFOSet 调用处 https://github.com/moby/moby/blob/v19.03.12/libcontainerd/remote/client.go#L194

// Exec creates exec process.
//
// The containerd client calls Exec to register the exec config in the shim side.
// When the client calls Start, the shim will create stdin fifo if needs. But
// for the container main process, the stdin fifo will be created in Create not
// the Start call. stdinCloseSync channel should be closed after Start exec
// process.
func (c *client) Exec(ctx context.Context, containerID, processID string, spec *specs.Process, withStdin bool, attachStdio libcontainerdtypes.StdioCallback) (int, error) {
 
    // a lot of code here
 
    fifos := newFIFOSet(labels[DockerContainerBundlePath], processID, withStdin, spec.Terminal)
 
 
    p, err = t.Exec(ctx, processID, spec, func(id string) (cio.IO, error) {
        rio, err = c.createIO(fifos, containerID, processID, stdinCloseSync, attachStdio)
        return rio, err
    })
 
    // a lot of code here
}

根据源码分析,每当 exec 新进程,都会创建 Linux FIFO 也就是命名管道,从而占用系统文件描述符。

最终发现,是该业务Pod调用第三方服务做了一个心跳检测,检测周期是1秒,当三方服务挂掉,导致频繁创建的心跳检测连接不能及时关闭从而长时间占用文件描述符过多。

本文参考:


  1. https://blog.csdn.net/alex_yangchuansheng/article/details/122613102 ↩︎