内核futex的BUG致使程序hang死问题排查

 

https://mp.weixin.qq.com/s/sGS-Kw18sDnGEMfQrbPbVwjava

内核futex的BUG致使程序hang死问题排查

原创: 王领先 58架构师 今天
 

近日,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。 

 

因此,咱们从外界最简单的就是经过向其发送信号,来唤醒那些可能永远等待的线程,让程序跑起来。 

相关文章
相关标签/搜索