背景:這個是在centos7 + lxcfs 和jdk11 的環境上復現的。
下面列一下我們是怎麼排查並解這個問題。
一、故障現象OPPO內核團隊接到jvm的兄弟甘兄發來的一個案例,現象是java的熱點一直是如下函數,佔比很高。
at sun.management.OperatingSystemImpl.getProcessCpuLoad(Native Method)我們授權後登陸oppo雲宿主,發現top顯示如下:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 617248 service 20 0 11.9g 8.6g 9300 R 93.8 2.3 3272:09 java 179526 service 20 0 11.9g 8.6g 9300 R 93.8 2.3 77462:17 java 二、故障現象分析java的線程,能把cpu使用率打這麼高,常見一般是gc或者跑jni的死循環。從jvm團隊反饋的熱點看,這個線程應該在一直獲取cpu利用率,而不是以前常見的問題。從perf的角度看,該線程一直在觸發read調用。
然後strace查看read的返回值,列印如下:
read(360, 0x7f6f9000b000, 4096) = -1 ENOTCONN (Transport endpoint is not connected)read(360, 0x7f6f9000b000, 4096) = -1 ENOTCONN (Transport endpoint is not connected)read(360, 0x7f6f9000b000, 4096) = -1 ENOTCONN (Transport endpoint is not connected)read(360, 0x7f6f9000b000, 4096) = -1 ENOTCONN (Transport endpoint is not connected)read(360, 0x7f6f9000b000, 4096) = -1 ENOTCONN (Transport endpoint is not connected)read(360, 0x7f6f9000b000, 4096) = -1 ENOTCONN (Transport endpoint is not connected)接著查看360這個fd到底是什麼類型:
lr-x- 1 service service 64 Feb 4 11:24 /proc/22345/fd/360 -> /proc/stat發現是在讀取/proc/stat,和上面的java熱點讀取cpu利用率是能吻合的。
根據/proc/stat在容器內的掛載點。
tmpfs /proc/timer_stats tmpfs rw,nosuid,size=65536k,mode=755 0 0lxcfs /proc/stat fuse.lxcfs rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other 0 0lxcfs /proc/stat fuse.lxcfs rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other 0 0lxcfs /proc/diskstats fuse.lxcfs rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other 0 0很奇怪的是,發現有兩個掛載點都是/proc/stat。
嘗試手工讀取一下這個文件:
cpu 12236554 0 0 9808893 0 0 0 0 0 0cpu0 10915814 0 0 20934 0 0 0 0 0 0cpu1 1320740 0 0 9787959 0 0 0 0 0 0說明bash訪問這個掛載點沒有問題,難道bash訪問的掛載點和出問題的java線程訪問的掛載點不是同一個?
查看對應java打開的fd的super_block和mount。
crash> files 22345 |grep -w 360360 ffff914f93efb400 ffff91541a16d440 ffff9154c7e29500 REG /rootfs/proc/stat/proc/statcrash> inode.i_sb ffff9154c7e29500 i_sb = 0xffff9158797bf000然後查看對應進程歸屬mount的namespace中的掛載點信息:
crash> mount -n 22345 |grep lxcfsffff91518f28b000 ffff9158797bf000 fuse lxcfs /rootfs/proc/statffff911fb0209800 ffff914b3668e800 fuse lxcfs /rootfs/var/lib/baymax/var/lib/baymax/lxcfsffff915535becc00 ffff914b3668e800 fuse lxcfs /rootfs/proc/cpuinfoffff915876b45380 ffff914b3668e800 fuse lxcfs /rootfs/proc/meminfoffff912415b56e80 ffff914b3668e800 fuse lxcfs /rootfs/proc/uptimeffff91558260f300 ffff914b3668e800 fuse lxcfs /rootfs/proc/stat/proc/statffff911f52a6bc00 ffff914b3668e800 fuse lxcfs /rootfs/proc/diskstatsffff914d24617180 ffff914b3668e800 fuse lxcfs /rootfs/proc/swapsffff911de87d0180 ffff914b3668e800 fuse lxcfs /rootfs/sys/devices/system/cpu/online由於cat操作較短,寫一個簡單的demo代碼,open 之後不關閉並且read 對應的/proc/stat路徑,發現它訪問的掛載點的super_blcok是 ffff914b3668e800,由於代碼極為簡單,在此不列出。
也就是進一步地確認了,出問題的java進程訪問的是一個出問題的掛載點。那為什麼訪問這個掛載點會返回ENOTCONN呢?
crash> struct file.private_data ffff914f93efb400 private_data = 0xffff911e57b49b80---根據fuse模塊代碼,這個是一個fuse_file
crash> fuse_file.fc 0xffff911e57b49b80 fc = 0xffff9158797be000crash> fuse_conn.connected 0xffff9158797be000 connected = 0果然對應的fuse_conn的連接狀態是0,從打點看,是在fuse_get_req返回的。調用鏈如下:
sys_read-->vfs_read-->fuse_direct_read-->__fuse_direct_read-->fuse_direct_io --->fuse_get_req--->__fuse_get_req
static struct fuse_req *__fuse_get_req(struct fuse_conn *fc, unsigned npages, bool for_background){... err = -ENOTCONN; if (!fc->connected) goto out;...}下面要定位的就是,為什麼出問題的java線程會沒有判斷read的返回值,而不停重試呢?
gdb跟蹤一下:
(gdb) bt#0 0x00007fec3a56910d in read () at ../sysdeps/unix/syscall-template.S:81#1 0x00007fec3a4f5c04 in _IO_new_file_underflow (fp=0x7fec0c01ae00) at fileops.c:593#2 0x00007fec3a4f6dd2 in __GI__IO_default_uflow (fp=0x7fec0c01ae00) at genops.c:414#3 0x00007fec3a4f163e in _IO_getc (fp=0x7fec0c01ae00) at getc.c:39#4 0x00007febeacc6738 in get_totalticks.constprop.4 () from /usr/local/paas-agent/HeyTap-Linux-x86_64-11.0.7/lib/libmanagement_ext.so#5 0x00007febeacc6e47 in Java_com_sun_management_internal_OperatingSystemImpl_getSystemCpuLoad0 () from /usr/local/paas-agent/HeyTap-Linux-x86_64-11.0.7/lib/libmanagement_ext.so確認了用戶態在循環進入 _IO_getc的流程,然後jvm的甘兄開始走查代碼,發現有一個代碼非常可疑:
UnixOperatingSystem.c:get_totalticks 函數,會有一個循環如下:
static void next_line(FILE *f) { while (fgetc(f) != '\n');}從fuse模塊的代碼看,fc->connected = 0的地方並不是很多,基本都是abort或者fuse_put_super調用導致,通過對照java線程升高的時間點以及查看journal的日誌,進一步fuse的掛載點失效有用戶態文件系統lxcfs退出,而掛載點還有inode在訪問導致,導致又無法及時地回收這個super_block,新的掛載使用的是:
remount_container() { export f=/proc/stat && test -f /var/lib/baymax/lxcfs/$f && (umount $f; mount --bind /var/lib/baymax/lxcfs/$f $f)使用了bind模式,具體bind的mount的特性可以參照網上的資料。
並且在journal日誌中,查看到了關鍵的:
: umount: /proc/stat: target is busy這行日誌表明,當時卸載時出現失敗,不是所有的訪問全部關閉。
三、故障復現針對以上的懷疑點,寫了一個很簡單的demo如下:
#include <stdio.h>#include <unistd.h>#include <stdlib.h>#define SCNd64 "I64d"#define DEC_64 "%"SCNd64
static void next_line(FILE *f) { while (fgetc(f) != '\n');}
#define SET_VALUE_GDB 10int main(int argc,char* argv[]){ unsigned long i =1; unsigned long j=0; FILE * f=NULL; FILE *fh; unsigned long userTicks, niceTicks, systemTicks, idleTicks; unsigned long iowTicks = 0, irqTicks = 0, sirqTicks= 0; int n;
if((fh = fopen("/proc/stat", "r")) == NULL) { return -1; }
n = fscanf(fh, "cpu " DEC_64 " " DEC_64 " " DEC_64 " " DEC_64 " " DEC_64 " " DEC_64 " " DEC_64, &userTicks, &niceTicks, &systemTicks, &idleTicks, &iowTicks, &irqTicks, &sirqTicks);
while(i!=SET_VALUE_GDB)如果掛載點不變,則走這個大循環,單核cpu接近40% { next_line(fh); j++; } fclose(fh); return 0;}執行以上代碼,獲得結果如下:
一開始單獨運行./caq.o,會看到cpu佔用如下:628957 root 20 0 8468 612 484 R 32.5 0.0 18:40.92 caq.o 發現cpu佔用率時32.5左右,此時掛載點信息如下:crash> mount -n 628957 |grep lxcfsffff88a5a2619800 ffff88a1ab25f800 fuse lxcfs /rootfs/proc/statffff88cf53417000 ffff88a4dd622800 fuse lxcfs /rootfs/var/lib/baymax/var/lib/baymax/lxcfsffff88a272f8c600 ffff88a4dd622800 fuse lxcfs /rootfs/proc/cpuinfoffff88a257b28900 ffff88a4dd622800 fuse lxcfs /rootfs/proc/meminfoffff88a5aff40300 ffff88a4dd622800 fuse lxcfs /rootfs/proc/uptimeffff88a3db2bd680 ffff88a4dd622800 fuse lxcfs /rootfs/proc/stat/proc/statffff88a2836ba400 ffff88a4dd622800 fuse lxcfs /rootfs/proc/diskstatsffff88bcb361b600 ffff88a4dd622800 fuse lxcfs /rootfs/proc/swapsffff88776e623480 ffff88a4dd622800 fuse lxcfs /rootfs/sys/devices/system/cpu/online
由於沒有關閉/proc/stat的fd,也就是進行大循環,然後這個時候重啟lxcfs掛載:重啟之後,發現掛載點信息如下:crash> mount -n 628957 |grep lxcfsffff88a5a2619800 ffff88a1ab25f800 fuse lxcfs /rootfs/proc/statffff88a3db2bd680 ffff88a4dd622800 fuse lxcfs /rootfs/proc/stat/proc/stat--這個掛載點,由於fd未關閉,所以卸載肯定失敗,可以看到super_block是重啟前的ffff887795a8f600 ffff88a53b6c6800 fuse lxcfs /rootfs/var/lib/baymax/var/lib/baymax/lxcfsffff88a25472ae80 ffff88a53b6c6800 fuse lxcfs /rootfs/proc/cpuinfoffff88cf75ff1e00 ffff88a53b6c6800 fuse lxcfs /rootfs/proc/meminfoffff88a257b2ad00 ffff88a53b6c6800 fuse lxcfs /rootfs/proc/uptimeffff88cf798f0d80 ffff88a53b6c6800 fuse lxcfs /rootfs/proc/stat/proc/stat/proc/stat---bind模式掛載,會新生成一個/proc/statffff88cf36ff2880 ffff88a53b6c6800 fuse lxcfs /rootfs/proc/diskstatsffff88cf798f1f80 ffff88a53b6c6800 fuse lxcfs /rootfs/proc/swapsffff88a53f295980 ffff88a53b6c6800 fuse lxcfs /rootfs/sys/devices/system/cpu/onlinecpu立刻打到接近100%628957 root 20 0 8468 612 484 R 98.8 0.0 18:40.92 caq.o四、故障規避或解決我們的解決方案是:
1. 在fgetc 中增加返回值的判斷,不能無腦死循環。
2. 由於lxcfs在3.1.2 的版本會有段錯誤導致退出的bug,儘量升級到4.0.0以上版本,不過這個涉及到libfuse動態庫的版本適配。
3. 在lxcfs等使用fuse的用戶態文件系統進行remount的時候,如果遇到umount失敗,則延遲加重試,超過次數通過日誌告警系統拋出告警。
關於作者:
Anqing,OPPO混合雲內核&虛擬化技術負責人