天天看點

面向祖傳代碼 Debug,我挽回了一位準備跑路的程式員現場複現總結

交流群的風格突然驟變,沒有了往日的灌水扯淡,居然聊起了技術。

面向祖傳代碼 Debug,我挽回了一位準備跑路的程式員現場複現總結
面向祖傳代碼 Debug,我挽回了一位準備跑路的程式員現場複現總結

看了大家的全部的聊天記錄,發現問題并沒解決。群裡難得這麼多人聊技術,抱着問答不斷,必有回響的原則,主動勾搭一起看看是什麼問題。

大概了解其問題是這樣,apache 子程序一直異常退出

$ sudo tail -f /var/log/httpd/error_log
[Sun Jun 13 19:32:25.660349 2021] [core:notice] [pid 23340] AH00052: child pid 20605 exit signal Bus error (7)
[Sun Jun 13 19:32:25.660413 2021] [core:notice] [pid 23340] AH00052: child pid 20606 exit signal Bus error (7)
[Sun Jun 13 19:35:39.862368 2021] [core:notice] [pid 23340] AH00052: child pid 21332 exit signal Bus error (7)
[Sun Jun 13 19:35:50.872363 2021] [core:notice] [pid 23340] AH00052: child pid 21369 exit signal Bus error (7)
[Sun Jun 13 19:39:43.079650 2021] [core:notice] [pid 23340] AH00052: child pid 18595 exit signal Bus error (7)
[Sun Jun 13 19:42:08.210353 2021] [core:notice] [pid 23340] AH00052: child pid 21348 exit signal Bus error (7)
[Sun Jun 13 19:42:16.221076 2021] [core:notice] [pid 23340] AH00052: child pid 21331 exit signal Bus error (7)
[Sun Jun 13 19:42:16.221177 2021] [core:notice] [pid 23340] AH00052: child pid 23156 exit signal Bus error (7)
[Sun Jun 13 19:44:11.329344 2021] [core:notice] [pid 23340] AH00052: child pid 27824 exit signal Bus error (7)           

現場複現

了解到其架構是

apache + php

,群友也是臨時接受的祖傳代碼,項目也不是很清楚。可以使用

strace

來看看,背後到底是什麼問題

$ ps -ef|grep apache           

首先看到主程序 id 是 23340,這裡需要跟着其子程序,是以我執行了如下指令

$ sudo strace -o strace.log -s 1024 $(pidof "/usr/sbin/httpd" -o 23340|sed 's/\([0-9]*\)/-p \1/g')
strace: Process 26212 attached
strace: Process 26211 attached
strace: Process 26191 attached
strace: Process 25940 attached
...           

參數大概解釋下

code 釋義
-o strace.log 輸出 strace 日志到 strace.log
-s 1024 當系統調用的某個參數是字元串時,最多輸出指定長度的内容,預設是32個位元組
pidof "/usr/sbin/httpd" -o 23340 排除主程序 23340 之外的 httpd 子程序

然後新開一個視窗監控日志

$ sudo tail -f strace.log |grep SIGBUS -B 500
28741 open("/xxxx/www/data/cache/xxx.php", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 15
28741 write(15, "<?php defined('DYMall') or exit('Access Invalid!'); return array ( ) ?>", 71) = 71
28741 close(15)
...
28741 open("/xxxx/www/data/cache/xxx.php", O_RDONLY) = 15
28741 fstat(15, {st_mode=S_IFREG|0644, st_size=71, ...}) = 0
28741 fstat(15, {st_mode=S_IFREG|0644, st_size=71, ...}) = 0
28741 fstat(15, {st_mode=S_IFREG|0644, st_size=71, ...}) = 0
28741 mmap(NULL, 71, PROT_READ, MAP_SHARED, 15, 0) = 0x7f584aee8000
28741 --- SIGBUS {si_signo=SIGBUS, si_code=BUS_ADRERR, si_addr=0x7f584aee8000} ---
28741 chdir("/etc/httpd")               = 0
28741 rt_sigaction(SIGBUS, {SIG_DFL, [], SA_RESTORER|SA_INTERRUPT, 0x7f58499bc5e0}, {SIG_DFL, [], SA_RESTORER|SA_RESETHAND, 0x7f58499bc5e0}, 8) = 0
28741 kill(28741, SIGBUS)               = 0
28741 rt_sigreturn({mask=[]})           = 140017190993928
28741 --- SIGBUS {si_signo=SIGBUS, si_code=SI_USER, si_pid=28741, si_uid=48} ---
28741 +++ killed by SIGBUS +++           
SIGBUS 在使用者态最為常見的場景,也最容易觸發,通常來說根本原因都是程序 mmap 了一個檔案後,另外的程序把這個檔案截斷了,導緻 mmap 出來的某些記憶體頁超出檔案的實際大小,通路那些超出的記憶體頁就會觸發 SIGBUS

驗證檔案大小變化

監控檔案的大小來驗證檔案是否一直在變化

$ while :; do cat /xxxx/www/data/cache/xxx.php|wc -L ;done
$ while :; do cat /xxxx/www/data/cache/xxx.php|wc -L ;done|grep -v 71           

發現檔案确實在少數情況下會變為 0 個位元組,根據檔案的路徑,初步估計應該是緩存檔案被重寫了

确認檔案寫操作來源

$ sudo yum -y install audit auditd-libs           

監控

topic_goodsclass.php

檔案的寫入操作

$ sudo auditctl -w /xxxx/www/data/cache/xxx.php -p w           

結果類似于

time->Mon Jun 14 21:21:39 2021
type=PROCTITLE msg=audit(1623676899.778:1883303): proctitle=2F7573722F7362696E2F6874747064002D44464F524547524F554E44
type=PATH msg=audit(1623676899.778:1883303): item=1 name="/xxxx/www/data/cache/xxx.php" inode=30151674 dev=fd:11 mode=0100644 ouid=48 ogid=48 rdev=00:00 objtype=NORMAL
type=PATH msg=audit(1623676899.778:1883303): item=0 name="/xxxx/www/data/cache/" inode=30151667 dev=fd:11 mode=040755 ouid=48 ogid=48 rdev=00:00 objtype=PARENT
type=CWD msg=audit(1623676899.778:1883303):  cwd="/xxxx/www"
type=SYSCALL msg=audit(1623676899.778:1883303): arch=c000003e syscall=2 success=yes exit=16 a0=55b452b93078 a1=241 a2=1b6 a3=2a items=2 ppid=31318 pid=18269 auid=4294967295 uid=48 gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=4294967295 comm="httpd" exe="/usr/sbin/httpd" key=(null)           

收集一段時間,做下統計

$ sudo ausearch -f /xxxx/www/data/cache/xxx.php |grep "exe="|awk '{print $12,$26}'|sort|uniq -c
  44200 ppid=31318 exe="/usr/sbin/httpd"           

發現都是來自于

/usr/sbin/httpd

,并沒有

CLI

模式的寫入(擔心有定時任務來生成緩存),清理掉監控

sudo auditctl -D           

多程序檔案讀寫沖突的解決方案

需要注意,在對檔案進行

fopen($filename, "w+")

的時候,就已經将檔案清空了,是以加鎖需要在

fopen

目标檔案之前,是以我的方式是增加一個檔案一一對應的鎖檔案。如下:

aa.php

function addFileLock($filename, $lock)
{
    $fp = fopen($filename . ".lock", "w+");
    flock($fp, $lock);
    return $fp;
}

function releaseLock($fp)
{
    flock($fp, LOCK_UN);
}

$filename = "cc.log";


// mock 資料
file_put_contents($filename, "100");

$fp = fopen($filename, "r");

if ($lockfp = addFileLock($filename, LOCK_SH)) {

    $n = 10;
    while ($n > 0) {
        sleep(1);
        printf("%d %d\n", time(), --$n);
    }

    echo fgets($fp);

    releaseLock($lockfp);
}

fclose($fp);           

bb.php

function setFileLock($filename, $lock)
{
    $fp = fopen($filename . ".lock", "w+");
    flock($fp, $lock);
    return $fp;
}

function releaseFileLock($fp)
{
    flock($fp, LOCK_UN);
    fclose($fp);
}

$filename = "cc.log";

if ($lockfp = setFileLock($filename, LOCK_EX)) {
    $fp = fopen($filename, "w+");
    echo time();
    fwrite($fp, 200);
    releaseFileLock($lockfp);
}

fclose($fp);           

是以先執行

php aa.php

再執行

php bb.php

,就能看到效果。

實際代碼定位

根據系統調用裡面的字元串關鍵字,搜到了相關的緩存操作代碼

28741 write(15, "<?php defined('DYMall') or exit('Access Invalid!'); return array ( ) ?>", 71) = 71           

改完之後發現線上還是有問題,本來我已經懷疑人生了,還是通過系統調用發現讀取的時候并沒有走我增加的共享鎖

面向祖傳代碼 Debug,我挽回了一位準備跑路的程式員現場複現總結

原來緩存的讀取是走的别的邏輯,從上面的截圖可以看到緩存的寫入已經走了修改之後的代碼(加鎖邏輯),然後根據個人經驗加搜尋該緩存寫入方法,按圖索骥找到了另外一個緩存檔案的讀取邏輯,然後再次修改,線上問題沒有再複現。

問題解決,跑路哥給我發來紅包,我心滿意足的接受了。

面向祖傳代碼 Debug,我挽回了一位準備跑路的程式員現場複現總結

總結

  1. 首先通過

    strace

    複現

    SIGBUS

    的場景
  2. 預計是檔案在多程序模型下,并發讀寫導緻的問題
  3. 通過

    auditctl

    監控統計檔案的寫操作來源均為

    httpd

    沒有其他程序來修改
  4. 通過讀加共享鎖、寫加排它鎖,線上還是有問題
  5. 再次根據

    strace

    發現讀的地方另有他處,根據經驗,補上,問題修複

不足的地方:最後定位過于依靠個人經驗,不夠系統化。

小想法:可以弄一個擴充使用

zend_set_user_opcode_handler

來監控自定義函數和方法和系統方法和函數調用鍊,然後在每個請求初始化階段寫入日志,比如

write(fd, "{unique code}", xxx);
write(fd, "request uri", xxx);           

然後的方法調用在

zend_set_user_opcode_handler

裡面列印,這樣就能友善排查祖傳代碼的一些業務邏輯問題了。

如果有其他更快更簡單的方案,大家留言告訴我。

“程式員跑路筆記” 我的公衆号,期待你的關注,一起避免跑路

面向祖傳代碼 Debug,我挽回了一位準備跑路的程式員現場複現總結