一次共享內存引起的線上事故分析

一、前言

    ipquery是一個用於根據ip查詢對應信息(地址、天氣等)的php模塊,基於共享內存實現,爲了做到更新數據時不重啓php,我們引入了數據動態加載概念。如下圖1設計:

(圖1)

    在調用查詢接口時,php進程會首先訪問共享內存D,取出存儲在D中shmkey,然後再去訪問shmkey表示的內容,熱加載的過程就是當數據有更新時,重新申請一塊共享內存,把數據加載到這塊內存中,然後把D中的內容改成New data的shmkey,當IPQuery接口被調用時,如果取出的shmkey跟舊的shmkey不同,php進程就會dattach Old data, attach New data, 之後就可以訪問到新的數據了。

二、問題

    上線一段時間後出現了致命bug(期間應該使用了熱加載程序),apache錯誤日誌分析,報如下錯誤爲:

terminate called after throwing an instance of 'std::runtime_error'
  what():  appinfo: shmget failed!,errno:22 errmsg:Invalid argument
[Fri Feb 01 20:11:30 2013] [notice] child pid 10507 exit signal Aborted (6)
分析代碼發現此錯誤出自源代碼 :“_shmid =  result::not_val<int>(shmget(_s_shm_key,_len,IPC_CREAT|0666),-1,"shmget failed!")”,錯誤碼errno 爲22,Invalid argument(非法參數),可以確定是attach 共享內存時報錯。

man shmget :

shmget函數返回錯誤碼22有兩種原因:

a、創建size<SHMMIN or size>SHMMAX的共享內容;

b、指定key的共享內存存在,但是size大於已存在共享內存的大小。SHMMIN默認值爲1,_len肯定是大於1的;

執行命令:

cat /proc/sys/kernel/shmmax

可以看到SHMMAX值遠大於所申請的共享內存大小,所以錯誤只可能是最後一種:共享內存存在,但_len大於存在的共享內存的size。

三、調試分析

   經過配合測試,客戶端用siege一直打壓,執行數據熱加載數分鐘後,問題重現了:

圖中的nattch是共享內存當前被引用的次數。以下圖2、3、4是連續幾次執行ipcs的結果。

(圖2)

(圖3)

(圖4)

   圖中key爲0x00924660是每個httpd進程都要attach的共享內存,對應圖1中的D,key爲0x7c000237的是httpd子進程第一次處理請求時需要attach的共享內存。從圖2和圖3可以看出,key爲0x00924660和key爲0x7c000237的nattch在減少,但都不爲0,圖4中key爲0x00924660的nattch值回升。但是key爲0x7c000237的nattch值爲0.

    整個過程中,數據熱加載執行的時間是[Fri Feb 01 20:06:38 2013],但error_log總最早出現錯誤時間爲[Fri Feb 01 20:11:28 2013],結合圖2-4也可以說明,數據熱加載之前已存在的httpd子進程可以正常服務,也就是說數據熱加載之前已存在的httpd子進程的數據源已成功切換到新的共享內存區,可以排除crash由數據源切換導致的疑慮,確定是由動態創建的httpd子進程造成的。但是不能確定是在子進程的創建過程中還是創建完之後處理請求過程中。

    圖4中key爲0x7c000237的nattch值爲0,而key爲0x00924660的nattch值回升到522,結合apache錯誤日誌可以知道:在出錯過程中,動態創建httpd子進程一直在crash,httpd父進程也在不停地創建子進程,但趕不上crash的速度,直到全crash掉,客戶端連不上服務器,siege退出,httpd子進程數量纔回升至穩定,如果繼續siege發請求,又會crash。由此確定crash發生在接口attach新共享內存時。

    以上確定crash發生在httpd動態創建的子進程處理第一次請求過程,希望觀察在處理請求過程中_len的變化,找出真正的真兇!於是用gdb在線上調試httpd,觀察_len的變化。

# : sudo gdb httpd

# : (gdb) attach pid

# : (gdb) b _Z16space_ptr_updatev(attach和切換數據源的函數)

# : (gdb) c

客戶端啓動siege,當此進程運行到斷點處時,會停在端點上

# : (gdb) p idx->shmkey

# : (gdb) $3 = 3472884279(0xcf000237) 可以看到idx->shmkey是正確的。

# ::(gdb) p_len

# : (gdb) $5 = 927305456        

927305456是舊的共享內存的大小,找到crash的真正原因了:請求的數據大小比存在的共享內存大。

    因爲crash是由數據熱加載引起的,所以在apache啓動之後,多次執行熱加載命令,加載不同大小的ip數據文件,然後gdb attach到未處理過請求的httpd子進程中觀察_len值。多次測試後發現未處理過請求的httpd子進程中_len始終爲apache啓動時加載數據的大小。驗證了apache動態創建子進程機制爲: apache啓動時,由父進程加載模塊,以後動態創建子進程時fork自己,複製地址空間到子進程空間。

四、解決方案

1、在attach共享內存時把_len設置問題0:

_len = 0;

_shmid = result::not_val<int>(shmget(_s_shm_key_len, _len, IPC_CREAT|0666), -1, "shmget failed!");

2、在圖1中D上記錄最新的共享內存的大小,attach之前把_len設成此值。

說明:

_len = 0  獲取已存在的共享內存,不存在則失敗

_len > 0 不存在則創建,存在則返回共享內存

 

 

 

 

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