现象
后端现象是,某个配置文件 “changedir/某配置文件.json” 不定期会被删除。但是不知道是什么进程删除的。
inotify和audit监控
使用inotify监控,的确看到有删除动作,但是同样没法看到是什么进程删除了。
于是改用 audit 监控,监控出来,如下信息:
可以看到,进程号pid=23637,进程名为python,删除了文件。但是,我们执行:
并没有发现进程。
ppid=10311为supervisor监控进程。
此外,我们发现,刚才的删除记录里面,每个十分钟就会删除一次文件:
上面结束,明确了一点。
结论
有一个由supervisor拉起来的python进程,每十分钟会删除一次文件,查不到进程号的原因,目前怀疑是进程一启动便结束了。疑问
上面”grep DELETE -B1”没有显示出来,其实我们看到,这里每10分钟执行一次DELETE操作的进程,其PID都是23637,为什么会这样?
难道每次启动都是同样的PID?还是auditd本来监控就不准确?
python代码分析
supervisor下面会拉起很多进程,逐一排查不太容易,通过搜索文件名”某配置文件.json”,我们定位到了一个python脚本:
如果verify_plat_serial和verify_access_serial都判断错误,会周期性进入sleep 600s逻辑,刚好是10分钟。而且此进程也是由supervisor拉起来的,但是这个进程一直在运行,其PID并不是23637,而是23629。(注意,这两个ID之前的关联性,其PID值差别不大,应该想到是差不多时刻fork出来的。)
检测这段代码里面的判断逻辑,没什么问题,只有文件不存在的时候,才可能走到下面来,但是文件的确是存在的,单独用python调试,也是不会进入这个逻辑的。
这是最为矛盾的点:
代码是正确的,逻辑是正确的,单独调试调用函数也是正确的,pid不是它,但是从inotify删除的痕迹来看,的确是这个函数捣的鬼。
结论
这段代码嫌疑最大。疑问
仍然是PID问题,PID不是它。
进程strace监控
我决定看看,这个 23629 进程,到底在搞什么鬼,用strace监控:
抓到一段在10分钟周期点上的系统调用痕迹:
结论
问题就是出在进程 23629,而23637,其实是进程的线程ID。
在LINUX下,线程ID也用PID表示,而真正的进程ID,其实是TGID,这里属于一个表达上的误导。auditd的审计结果展示的是PID,是用的gettid获取的结果,本质上是TID。
查看进程的线程,的确存在23637线程,前面的两个疑问也能很好诠释。
LINUX线程问题
Linux的NPTL(Native POSIX Threading Library)多线程,每个用户线程对应一个内核线程,是1:1的映射,相比原本实现的单线程进程,也就是在tast_struct结构体里面多加了一个tgid字段,这个字段表示线程组ID,而原本的pid含义改为线程ID的意思。
使用getpid系统调用返回的也是tast_struct中的tgid, 而tast_struct中的pid则由gettid系统调用返回。
tgid等于pid的线程,就是线程组长,也就是我们平时看到的进程ID。
查看多线程:
通过上面的strace日志,还可以看出一个重要结论:
结论
打开文件返回的错误是 -1 EMFILE (Too many open files) 。
再明确不过的提示,表示进程句柄超限了,确认:
123 ~ # lh /proc/23629/fd | wc -l65537
而且,此通过句柄超限,可以印证为什么会走到上面删除文件的逻辑:
|
|
代码移除法
通过proc可以看到,泄漏的句柄属于socket:
而netstat里面看不到是谁连接的socket,这是比较不解的地方。代码里面有两个地方使用了socket,一是使用了mongo连接,检查逻辑,没什么问题。二是使用了UDPServer监听端口:
现在怀疑UDPServer用法是否有误,撸了一遍UDPServer源码,没有发现问题。
又写了一个客户端,来发送UDP包,发现,每发送一次UDP包,UDPServer这端的进程,会增加两个socket句柄。
又将官方的UDPServer示例代码拷贝下来,执行,同样的客户端发给UDPServer,没有句柄泄漏。
现在怀疑是UDPServer和多线程嵌套后,有BUG,于是在官方示例代码基础上,加了多线程,也没有句柄泄漏。
目前看,只能用笨方法,代码移除法,一点一点验证,好在是Python代码,移除代码执行非常方便。没几下,便定位到了一段序列号验证函数。
有它的时候,便会句柄泄漏,没它的时候,正常。
单独对这段代码做strace,发现:
这段序列号验证函数是C代码书写,外层调用没什么问题,后来定位到一公共库,公共库是公共技术部提供的动态库,其中一个函数,获取网卡信息没有关闭句柄: