Java進程與父子進程的標準輸出流關聯導致線程卡死的故障

故障現象

    Java調度系統創建PHP數據腳本後,並且獲取其標準輸出流,然後循環讀取其標準輸出流內容。此時PHP數據腳本執行時間過長,Java調度系統Process.destory()殺掉進程後,ps也無法找到對應PHP數據腳本,但Java的線程卻無法退出,依然卡死在讀取標準輸出流。曾經懷疑是kill無法清理乾淨進程,於是變爲強制執行kill -9殺掉超時的PHP數據腳本,但情況依舊,Java線程依然卡死。

    Java創建PHP數據腳本的代碼大致如下:

childProcess = Runtime.getRuntime().exec(cmd);
    stream = childProcess.getInputStream();
    while ((size = stream.read(bytes)) != -1) {
    //處理輸出流數據
    }

    //子進程退出
    //...


故障分析

    首先利用jstack找出對應線程堆棧,堆棧如下所示:

"TaskThread-CRON556" prio=10 tid=0x00007f4e38005800 nid=0x3563 runnable [0x00007f4f4167c000]
   java.lang.Thread.State: RUNNABLE
        at java.io.FileInputStream.readBytes(Native Method)
        at java.io.FileInputStream.read(FileInputStream.java:272)
        ......

    可以發現線程卡死在java.io.FileInputStream.readBytes(Native Method)這個native方法中,查找對應的native代碼,發現最終是linux的系統調用read方法。於是通過jstack上該線程對應的nid,0x3563轉換10進製爲13667 ,使用命令

strace -p 13667 -e read

    打印出當前的read調用的參數,具體如下:

read(79, 0x7f4f4167a4a0, 8192)          = -1 EBADF (Bad file descriptor)    

    然而此時奇蹟出現了,Java線程突然恢復了,之前卡死的native方法也退出了。雖然這樣突然恢復了,但是根本的原因沒有找到,因此仍然需要繼續探究。好在這個故障比較容易重現。重現調度任務後,讓線程再次阻塞讀取PHP數據腳本的標準輸出流,不同的是,這次不等待超時殺死PHP子進程,先查看正常狀態下read對應的FD究竟是什麼。同樣找出了阻塞的線程nid,然後再次使用命令strace查看read,打印如下:

read(79

    此時可以看到依然是fd爲79的pipe,然後使用命令

  lsof -p 11896

    打印出對應Java進程的所有fd,可以發現read參數對應的fd爲79的pipe,打印如下:

java      11896      root   79r     FIFO                0,8         0t0 2250040940 pipe

    可以看到對應fd爲79的管道序號爲2250040940,然後使用命令

lsof | grep 2250040940

    打印出對應pipe的所有關聯進程:

java      11896      root   79r     FIFO                0,8         0t0 2250040940 pipe
php       13668      root    1w     FIFO                0,8         0t0 2250040940 pipe
php       15007      root    1w     FIFO                0,8         0t0 2250040940 pipe

    可以發現,Java調度系統的79的pipe和其它兩個PHP進程有關聯。其中,13668是PHP數據腳本的進程號,同時發現了另外一個PHP子進程15007,而這個PHP子進程是負責連接InfoBright數據庫,然後導入相關數據。

    在超時之後,Java調度系統kill掉13668的PHP數據腳本,此時使用ps已無法找到其進程,但利用lsof | grep 2250040940查找原先的管道時,卻發現了

php       15007      root    1w     FIFO                0,8         0t0 2250040940 pipe

    15007的PHP子進程依然存在,這時由於InfoBright數據仍然沒有全部導入。此時再次調用strace查看Java阻塞的線程時,和之前一樣,提示Bad file descriptor後線程退出。


代碼分析

    在Java調度系統啓動的PHP數據腳本里,爲了能夠並行地往InfoBright導入數據,使用了proc_open()創建了多個PHP子進程去連接InfoBright數據庫,代碼大致如下:

$descriptorspec = array(2=>array('pipe', 'w'));
$process = proc_open($cmd, $descriptorspec, $pipes);
$error = fread($v['pipes'][2], 2048);
fclose($v['pipes'][2]);

    在這個proc_open的調用裏,$descriptorspec的參數表示爲子進程的標準錯誤流創建新的pipe,PHP數據腳本利用這個新的pipe和子進程進行通信。但是,從之前打印pipe關聯的進程可以看到,Java調度系統關聯的pipe是另外兩個進程的fd爲1的pipe,也就是標準輸出流。
    Java進程和PHP數據腳本的標準輸出流關聯,顯然這是因爲Java調用Process.getInputStream()獲取了PHP子進程的標準輸出流所導致的,但另外一個子進程的標準輸出流也關聯了起來,估計是因爲PHP數據腳本創建子進程時,調用proc_open()並沒有指明標準輸出流,因此子進程繼承了父進程的標準輸出流。
    而這時當PHP數據腳本超時之後,Java調度系統把PHP數據腳本kill掉,但連接數據庫的子進程依然存在,因此,Java線程阻塞等待,造成卡死。

    事實上,如果利用lsof查看PHP數據腳本和連接數據庫的子進程,可以發現它們的標準輸入流和標準輸出流如下:

lsof -p 13668
php     13668 root    0r  FIFO        0,8      0t0 2250040939 pipe
php     13668 root    1w  FIFO        0,8      0t0 2250040940 pipe
php     13668 root    2w  FIFO        0,8      0t0 2250040941 pipe

lsof -p 15007
php     15007 root    0r  FIFO        0,8      0t0 2250040939 pipe
php     15007 root    1w  FIFO        0,8      0t0 2250040940 pipe
php     15007 root    2w  FIFO        0,8      0t0 2250090133 pipe

    可以看到兩個進程的標準輸入/輸出流的pipe是一樣的,只有錯誤輸出流是不同的,顯然這時因爲PHP數據腳本創建了新的pipe給子進程進行通信導致的。至此,根本原因應該理清:
    主要是由於PHP數據腳本創建了子進程連接數據庫,子進程繼承了PHP數據腳本的標準輸出流,Java線程監聽這個標準輸出流的pipe。當PHP數據腳本被kill時,子進程依然存在,Java線程只能一直等待連接數據庫的子進程退出,才能真正的退出read的阻塞。
    但無法理解的是,爲何調用strace的時候,Java線程會退出,而且此時子進程依然存在?這個可能和Linux內核的read調用的實現有關。。具體原因卻很難理解。


解決辦法

    最後,由於考慮到每次執行都需要PHP數據腳本把數據完全導入,因此允許其繼續超時等待,讓數據完全導入後才退出腳本的執行。另外,如果真的要考慮kill掉子進程,也可以有如下辦法:
    1.可以考慮強制kill掉整個進程樹,包括數據腳本創建的PHP子進程
    2.也可以在proc_open()參數指定標準輸入流和標準輸出流,這樣這些子進程就不會繼承父進程的標準輸入流和標準輸出流管道

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章