https://mp.weixin.qq.com/s/sGS-Kw18sDnGEMfQrbPbVwjava
内核futex的BUG致使程序hang死问题排查
近日,Hadoop的同窗反映,新上的几台机器上的java程序出现hang死的现象,查看系统的message记录,发现一些内存方面的错误输出,怀疑是内存不足致使java程序hang死在gc的过程当中。经排查发现即便是在内存充足的状况下也会出现程序hang死的现象。mysql
咱们又发现只有这批新上的机器才出现hang死的问题,以前老机器上一直很正常。排查后发如今老机器上有一个监控脚本,每隔一段时间就会用jstack查看一下java程序的状态。关了监控脚本后,老机器也出现了hang死的问题。最后咱们发现使用jstack、pstack均可以将原来hang死的程序刷活。linux
后来DBA的同窗也反映,他们的xtrabackup程序也出现了hang死的问题,最后咱们使用GDB对这个备份程序分析后发现,问题的缘由出如今内核的一个BUG上。git
结论github
出现问题的机器上的Linux内核都是linux 2.6.32-504版本,这个版本存在一个futex的BUG。sql
参见:数据库
https://github.com/torvalds/linux/commit/76835b0ebf8a7fe85beb03c75121419a7dec52f0缓存
这个BUG会致使非共享锁的程序体会陷入无人唤醒的等待状态,形成程序hang死。服务器
触发这个BUG须要具有如下几个条件:多线程
• 内核是2.6.32-504.23.4如下的版本
• 程序体须要使用非共享锁的锁竞争
• CPU须要有多核,且须要有CPU缓存
知足以上条件就有几率触发这个程序hang死的BUG。
解决方案就是升级到 2.6.32-504.23.4或更高版原本修复此BUG。
下面咱们来看一下,是如何判定问题是由这个BUG引发的。
原理分析
1. 首先要拿到进程ID
咱们要分析的xtrabackup程序的PID是715765
2. 看一下内核调用栈
cat /proc/715765/*/task/stack
发现大多数线程在停在 futex_wait_queue_me 这个内核函数中。
这个函数使当前线程主动释放CPU进入等待状态,若没有被唤醒,就一直停在这个函数中。
也就是说,如今大多数线程都在等其余资源释放锁,下面咱们就须要到用户态下分析,他们到底在等待什么锁。
3. 分析用户态代码
gdb attach 715765
对于这种程序hang死的问题,最好的工具仍是gdb,附加到程序上,来获取的实时状态信息。
3.1 查看线程信息
首先先看一下在用户态中线程的状态。
能够看到线程大致有两类等待, pthread_cond_wait 和 __lll_lock_wait。
pthread_cond_wait是线程在等待一个条件成立,这个条件通常由另外一个线程设置;
__lll_lock_wait是线程在等另外一个线程释放锁,通常是抢占锁失败,在等其余线程释放这个锁。
3.2 查看每一个线程信息
看到大致有三类线程:
• 拷贝线程:data_copy_thread_func
• 压缩线程:compress_wokrer_thread_func
• IO线程:io_handler_thread
为了弄明白这些线程的做用,咱们能够先了解下xtrabackup的工做原理。
3.3 工做原理说明
mysql数据库备份中的一个工做就是将数据库文件拷贝,为节省空间,能够经过参数来设置开启压缩。
在作实际分析前,咱们先梳理一遍启用压缩后,拷贝线程的业务逻辑:
• 拷贝线程会把文件分红多个小块,喂给压缩线程
• 在喂以前,须要经过一个控制锁来获取这个压缩线程的控制权
• 喂完后,会发送一个条件信号来通知压缩线程干活
• 而后就依次等每一个压缩线程将活干完
• 每等到一个压缩线程干完活,就将数据写到文件中,而后释放这个压缩线程控制锁
下面咱们看一个具体的拷贝线程,咱们从第1个拷贝线程开始,也就是2#线程。
3.4 拷贝线程2# 锁分析
拷贝线程2# hang死的位置 是 在给第1个压缩线程发送数据前,加ctrl_mutex锁的地方
它在等其owner 715800 释放,而715800 对应的是7#线程
3.5 拷贝线程 7# 锁分析
咱们看到7# 线程hang死的位置与2号线程是相同,不一样的是 它是卡在第3个压缩线程上,且其ctrl_mutex的owner为空。也就是说没有与其竞争的线程,它本身就一直在这等。
虽然这个现象很奇怪,但能够肯定这不是死锁问题致使的。通常来说只能是内核在释放锁时出现问题才会出现这种空等的状况。
为了更完整的还原出当时的场景,咱们须要分析一下到底都有谁有可能释放压缩线程的控制锁。
3.6 拷贝线程控制锁怎么释放
ctrl_mutex对应的是压缩线程一个控制锁,拥有这个锁才能对压缩线程作相应的操做
在xtrabackup中,大致有四个地方释放这个锁:
1. 建立压缩线程时,会初始化这个锁,并经过这个锁启动线程进入主循环
2. 压缩线程在运行时, 会使用这个锁设置启动状态(与上面的建立线程对应)
3. 拷贝线程会在往压缩线程放原始数据时,把持这个锁,在从压缩线程拿完数据后,释放对应锁
4. 销毁压缩线程后,会释放上面相关的锁
查看日志咱们看到,日志是停在一个压缩文件的过程当中,且上面完成了屡次文件的压缩操做;
因此,能够排除上面的一、二、4这三种状况;
那么咱们能够再作出下面的假设:
前面有一个拷贝线程,取完了几个压缩线程的压缩结果,释放了这几个压缩线程;
这时,7#拷贝线程正好拿到了一、2两个压缩线程的控制锁,往里放完数据后,开始要拿第3个压缩线程的控制锁;
这时前一个拷贝线程并无释放,因而7#只好在加锁处等待;
但当前一个拷贝线程释放第3个压缩线程锁的时候,内核并无通知到7#线程,形成其一直在等待。
而7#线程等待的过程当中,也不会释放其余已把持的压缩线程的锁,形成其余拷贝线程一直等待其释放,最后致使整个进程夯死。
到此咱们大概还原了程序hang死的场景,目前来说嫌疑最大的就是内核出现了问题,而当前内核版本正好有一个futex的BUG,咱们来具体看一下这个BUG是不是致使程序hang死的元凶。
4. 内核的futex的BUG分析
先看一下内核futex中的这个BUG,其实很简单,就是少加了两行代码;严格点说是在非共享锁分支上少加了一个mb。
mb又是什么呢?mb的做用将上下两部分代码作一个严格的分离,通常叫屏障,主要有两种屏障:
• 优化屏障:当gcc编译器从O2级别的优化开始就会对指令进行重排,而mb会在其宏上加一个volatile关键字来告诉编译器禁止与其余指令重排。
• 内存屏障:如今CPU一般是并行的执行若干条指令,具可能从新安排内存访问的次序,这种重排或乱序能够极大地加速程序运行,但也会致使一些须要数据同步的场景致使读到脏数据。而mb会使用mfence汇编指令告诉CPU,必需要把前面的指令执行完,才能执行其下面的指令,保证操做同步。
那不加这个mb 会形成什么实际影响呢?咱们来看futex_wake函数的代码:
futex_wake函数中会查看hb变量里有没有须要被唤醒的锁,若是没有就不作唤醒操做。
若没加 mb,将致使其获取的数据不一致,有机率将实际有锁在等待而误当成没锁在等待,形成该唤醒的锁,错失惟一一次被唤醒的机会,致使其一直处在等待状态,最终致使程序夯死!
下面咱们要肯定一件事情就是,当前程序是否命中了这个BUG,也就是说当前锁是不是非共享锁。咱们查看pthread的代码,能够看到__kind的值决定了其传给内核的锁是共享仍是非共享的。
经过gdb能够看到,__kind的值为0,必定是非共享锁;
经过上面的分析,咱们基本能够得出是内核BUG致使xtracbackup程序的hang死。
最后咱们将内核升到了2.6.32-504.23.4,发现xtrabackup程序能正常运行,而后咱们对hadoop服务器内核也作了升级,发现hang死问题也解决了。
结语
经过上面的分析过程,咱们能够发现gdb对这种须要实时分析的问题场景特别契合,但通常用来调度用户态的代码,内核态的相关信息可能用systemtap等工具更方便一些。
此外,还有一个问题使人困惑,就是为何使用pstack、jstack、gdb或SIGSTP+SIGCON信号能唤醒hang死的程序?
这里要说明的一点就是jstack、gdb、pstack的原理都是经过内核的SIGTRAP等一系列调试信号来,抓取信息或调试程序的;
因此,这个问题的本质是为何信号能唤醒程序?
咱们看到出现这种夯死现象的程序,大多线程都会停在 内核的 futex_wait_queue_me 这个函数处;而这个函数,使用 TASK_INTERRUPTIBLE 来设置本身的状态,表示本身主动想放弃CPU,但能够被中断、信号或其余程序唤醒;并在下面调用 schedule 内核调度方法,主动通知内核放弃本身的CPU。
因此,咱们从外界最简单的就是经过向其发送信号,来唤醒那些可能永远等待的线程,让程序跑起来。