最近在将老服务上云,在k8s测试集群发现一个奇怪的问题,一个业务进程会自动启动二个,这里先说一下业务背景:
服务器本身的技术栈是c++&lua,业务使用了hive-framwork框架,服务管理使用了tcm,这里将某个服务上云是将相关的进程打包到一个Image,以富容器的方式运行
服务部署后,我发现Pod中同时运行了两个一样的进程:
1 | root 141682 0.6 ./hive modelsvr.lua --tbus=2020 --daemon --id=1.1.59.1 ... |
两个进程命令行参数完全相同,其中一个是我自己启动的,另一个来路不明,问题开始了!
1. 是否和tagent有关
一开始怀疑和tagent(服务管理,提供异常退出自动拉起功能)有关,排除过程如下:
- 查看tagent的日志并没有拉起的记录;
- 把tcm相关的所有管理进程全部kill后,手动执行进程的启动,结果还是一样会启动两个进程;
- 手动启动其他进程,例如
./hive dbagent.lua ...,并不会和./hive modelsvr.lua ...一样,启动两个进程;
这个时候就有点懵逼了,这个modelsvr有什么特殊的,不应该呀,云下的老架构线网都是正常运行的。其实这里就应该想到是modelsvr本身的业务特殊性导致,但是一开始没往这方面想。因为我们的框架是有防多开的保护,所以拉起重复的进程自动失败的,所以我后面优先的定位问题思路就是:是谁在拉起这个进程?
2. “谁在访问文件”
接下来我的方向就转移到了,是谁在拉起这个”幽灵进程”,也就是谁在读取./hive。基于AI给出了几个解决方案
2.1 inotify 的天然局限
第一个是 inotifywait ,这个也是用的比较多的,之前业务会通过inotify监听文件的变化进行配置相关的reload,这里监听hive被执行了,可以通过如下
1 | inotifywait -m -e access hive |
但是inotify 只能告诉你”文件被访问了”,无法告诉你”是谁(哪个 PID/UID)访问的”。这是 inotify 机制的天然限制,要补上”是谁”,AI给出了一个方案:只能在事件触发瞬间用 fuser/lsof 抓当前打开该文件的进程,但读取往往是毫秒级的 open→read→close,等通知到达再去查,进程可能已经关闭文件了,会漏抓。结果也当然是没有收获,因为也不是这个问题。
2.2 auditd 审计
接下来就是auditd审计了,平常定位问题的时候也会使用。关于它的介绍:
内核自带的安全审计框架,用于记录系统中与安全相关的事件——哪个用户、在什么时候、对哪个文件或系统调用做了什么。它不阻止行为(那是 SELinux/AppArmor 的活),只负责忠实记录,是合规审计、入侵溯源、故障排查的底层基础设施。
使用方式如下:
1 | # 添加监控规则:监听对 hive 文件的读(r)、写(w)、属性(a)、执行(x) |
容器内auditd
容器内没有auditd,安装后发现启动报错了:
1 | # auditctl -s |
报错的原因其实很简单:auditd 依赖内核 audit 子系统,而 容器默认共享宿主机内核且没有 audit 权限(容器只是通过Namespace + Cgroup实现和宿主机的资源隔离+限制)。所以在容器内通过auditd监控文件的读取这条路也行不通。
基于overlay的文件结构进行宿主机文件audit
不过AI给了一个思路:在Node宿主机上用auditd进行文件的访问抓取。
那么如何在Node上抓取容器中的文件访问呢?如果你知道Linux如何通过Namespace进行资源隔离,你就会知道,容器的文件系统是通过overlayFS+Mount Namespace与主机文件系统进行隔离的。k8s中容器镜像的Layers数据在主机上的位置可以通过如下方式定位到:
1 | # containerd 的 CRI 工具是 `crictl` |
找到Node真是的文件路径后:
1 | auditctl -w /data/bcs/service/docker/io.containerd.snapshotter.v1.overlayfs/snapshots/10463/fs/data/home/user00/pangusvr/bin/hive -p rwxa -k hive |
结果在容器内手动启动,还是抓取不到,AI提到一个原因:overlay 会”错位”,具体原因如下:
容器进程通过 overlay 合并视图访问文件时,内核为其构建的是 overlay 自身的 inode 对象,这个对象和 lowerdir 底层文件系统的原始 inode 不是同一个;更重要的是:这个 overlay inode 本身生命周期不稳定,可能被内核动态回收重建,导致基于固定 inode 的
-w监控规则悄悄失效,而不会有任何报错。
宿主机系统调用的audit
没办法了,AI给出了另外一个思路:对二进制来说,监控 execve 系统调用比 watch 文件更靠谱,不受 overlay inode 错位影响。毕竟容器只是利用虚拟化在母机上运行,还是母机的内核,万变不离其中。
下面是通过audit进行系统调用的监控
1 | # 监控所有 execve,然后按路径名过滤 |
结果还是没有,此时的我一脸懵逼,我只能把audit的现有规则贴给AI看:
1 | $ auditctl -l |
这里AI分析了半天:从auditd状态没有enable,到audit 事件被送去别处了(本地日志被关闭、或被安全 agent 接管消费),再到内核编译时关掉了 CONFIG_AUDITSYSCALL,逐个排除后,最终怀疑到了最开始的那条规则:
1 | -a never,task |
这条规则的含义:专门对应 TASK 事件(进程 fork/clone 创建新任务时触发的内核审计钩子),不生成审计记录,原因是进程创建非常频繁(fork 炸弹式的高频),会产生大量低价值日志。
Audit 规则匹配是分 list 独立执行的,内核维护的是多条不同用途的过滤链(task、exit、entry、user、exclude 等),task list 只在**进程创建(fork/clone)时被触发检查,exit list 只在系统调用返回(syscall exit)**时被触发检查。这是两套完全独立的钩子点。
为什么task list的规则会影响execve的系统调用事件呢?因为task 规则更早决定了这个任务(**内核里的执行单元,基本对应 task_struct**,对应一个线程)是否参与审计。命中该规则后,该任务就已经被标记为不审计,后续的规则即使命中也不会生效。
删除task的规则后,容器内执行命令启动,就可以看到如下的事件:
1 | type=PROCTITLE msg=audit(07/10/2026 01:40:38.369:437201) : proctitle=./hive modelsvr.lua --tbus=2020 --daemon --id=1.1.59.1 --hash=1 --tlogconf=./config/recomodelsvr.log.xml --rundata=./config/reco |
这里只看到一次hive进程的调用,没有第二次,所以问题到这里进入了死胡同,突然想到是不是他自己拉起自己的?
3. 真相大白:Daemon流程的锅
上面最终通过audit发现并没有其他进程多次拉起hive,我只能怀疑是该进程自己的问题了,这个时候我才想起来去看进程的日志,发现了:
1 | [start_model_process] success, pid:142236, cost_time:2, dir:/data/home/user00/recomlsvr/models/bin, bin:/data/home/user00/recomlsvr/models/bin/recomlsvr, instance_id:144232076925992978 start_nohup_path:/data/home/user00/recomlsvr/models/bin/../log/144232076925992978_start.nohup, is_reload:false |
到这里我才想起来recomodelsvr会启动后拉起一个后端的recomlsvr,但是按道理通过fork+exec的方式拉起子进程的后会变成新的进程名呀,为啥会一直显示./hive recomodelsvr呢?
下面贴一下拉起子进程的逻辑,删除了一些不重要的逻辑:
1 | int my_shell::exec(lua_State* L) |
按道理execv() 成功会用新程序完全替换当前进程映像,comm、/proc/pid/exe、cmdline 都会变成新程序。那是哪里出问题了呢?这段代码在多少个业务跑了多少年了,也没出过问题呀!,接下来就是AI最擅长的地方了,把这个代码丢给AI,他直接给出了问题的根源:
关键嫌疑:
for (int f = 3; f < limits.rlim_cur; f++) close(f);这段是”关闭所有继承的文件描述符”。问题在于
limits.rlim_cur可能非常大。
我分别去了线上CVM集群上和K8s的Node上查看这个内核参数配置:
1 | # 线上CVM集群:tlinux2.6版本,linux内核5.4.241 |
然后进程的默认limit限制,如下:我们线上的CVM集群初始化的设置了额外限制进程的limit openfiles最大值为204800。
1 | # 线上CVM集群:tlinux2.6版本,linux内核5.4.241 |
所以果然真相大白了!,业务不严谨的代码遇到不确定的环境参数触发了意想不到的BUG。修复方式就很简单,最通用的方式就是遍历打开的fd目录,然后逐一关闭:
1 | ... |
针对Linux 5.9+ 高版本的内核,可以通过:
1 | // 一次系统调用关闭整个区间 |
4. 不同机型的性能测试
让AI写了一个benchmark的性能测试对比,分别在外网不同的机型进行了测试,在不同max limit openfiles的参数前提下,遍历rlimi_cur进行关闭的代码所消耗的时间的性能对比测试:
这里测试完后,发现一个奇怪的问题,为什么S5的机型为啥close的性能那么差?,一脸懵逼,这个时候我只能把所有的数据丢给AI,包括各个机器的CPU型号,果然发现了一个大瓜S5系列的CPU有缺陷😂。
大家应该都知道(「计算机组成原理课程」)CPU 为了加速指令的执行,会预测执行 (大意就是还没确定该不该执行的指令,先猜着跑),猜错了就回滚。
问题在于:回滚只撤销了”架构状态”(寄存器、内存值),但微架构状态(Cache、TLB、分支预测器里留下的痕迹)没清干净。攻击者通过测量”哪块 Cache 变快了”就能反推出被推测访问过的秘密数据——这就是幽灵攻击(Spectre)
芯片改不了,只能靠操作系统内核在软件层面打补丁来堵——代价就是每次 syscall / 上下文切换要多做一堆额外动作。Cascade Lake是受这批漏洞影响最全的一代,几乎全中招。下面是一次系统调用的过程示意:
下面是S5和S6机型关于CPU 硬件漏洞和当前系统对该漏洞的状态和缓解情况,内核参数配置如下:grep -r . /sys/devices/system/cpu/vulnerabilities/
| vulnerabilities 项 | S5 (Cascade Lake) | S6 (Ice Lake) | syscall 路径影响 |
|---|---|---|---|
| meltdown | 🔴 Mitigation: PTI |
🟢 Not affected |
这就是决定性差异 |
| mds | ⚠️ Vulnerable(尝试 VERW) | 🟢 Not affected |
S5 多一条 VERW |
| l1tf | Mitigation: PTE Inversion |
🟢 Not affected |
都可忽略 |
| tsx_async_abort | ⚠️ Vulnerable(尝试 VERW) | 🟢 Not affected |
S5 多一条 VERW |
| spectre_v2 | Vulnerable(未缓解) | Vulnerable: eIBRS | 都不压 syscall |
| spec_store_bypass | Vulnerable | prctl/seccomp 按需 | 默认不压 syscall |
所以结论是:S5 上每次 syscall 确实都多付一笔 PTI 固定开销,虽然只有”极轻量 + 高频”的 syscall 才会表现成明显的性能问题,但是这也是一个坑呀。
5. 写在最后
这个问题排查花了一下午的时间,其实一开始ps如果看到另外一个进程的CPU跑满的话,可能会更快的定位到问题,另外这个问题一开始表现的就是和此进程强相关的特性,应该第一时间考虑从业务的日志上出发,但是自己当时先入为主,脑海中形成了一定固定的假设:
- 首先,是有其他地方拉起了这个幽灵进程;
- 然后,由于进程的防多开,导致启动失败;
- 最后,返回循环;
基于上面的假设才进行了“谁在访问hive文件”的流程定位,最终没有发现有其他进程在访问,才回归到进程自己的业务日志上,最终发现了daemon进程创建的流程的问题,还是分析问题的顺序问题。
另外感叹的是,业务代码还是有很多潜在的坑,只是没有发现而已,即使我们已经用AI扫描了很多轮,可能是知识库的建设还是不完善,但是话说模型应该很容易理解这种基础代码才对。
最后,在AI的加持下,如果我们的IDC的机器能够提供MCP进行问题定位分析(安全性问题需要考虑,但是基本的命令组合应该还是可以的),虽然现在可以通过各个组件平台侧提供的MCP/SKILL进行分析问题,例如蓝鲸监控,BKLog,但是都没那么直接,如果能够直接拿到数据一手数据,应该能够更快的定位分析问题。
6. 测试数据
性能测试的原始数据下面
SA3 AMD EPYC™ Milan(2.55GHz/3.5GHz):30.49.40.64
SA5 AMD EPYC Bergamo(-/3.1GHz):21.245.122.28
S5 Intel Xeon Cascade Lake 8255C(2.5GHz/3.1GHz):11.177.159.161
S6 Intel Ice Lake(2.7GHz/3.3GHz):30.49.239.225
SA9 AMD EPYC Turin-D(-/3.4GHz):11.152.253.72
SA5 AMD EPYC Bergamo(-/3.1GHz):30.49.42.109 tlinux4.4的内核
