一次共享内存引起的线上事故分析
一、前言
? ? ipquery是一個用于根據ip查詢對應信息(地址、天氣等)的php模塊,基于共享內存實現,為了做到更新數據時不重啟php,我們引入了數據動態(tài)加載概念。如下圖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)
分析代碼發(fā)現此錯誤出自源代碼 :“_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、創(chuàng)建size<SHMMIN or size>SHMMAX的共享內容;
b、指定key的共享內存存在,但是size大于已存在共享內存的大小。SHMMIN默認值為1,_len肯定是大于1的;
執(zhí)行命令:
cat /proc/sys/kernel/shmmax
可以看到SHMMAX值遠大于所申請的共享內存大小,所以錯誤只可能是最后一種:共享內存存在,但_len大于存在的共享內存的size。
三、調試分析
? ?經過配合測試,客戶端用siege一直打壓,執(zhí)行數據熱加載數分鐘后,問題重現了:
圖中的nattch是共享內存當前被引用的次數。以下圖2、3、4是連續(xù)幾次執(zhí)行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.
? ? 整個過程中,數據熱加載執(zhí)行的時間是[Fri Feb 01 20:06:38 2013],但error_log總最早出現錯誤時間為[Fri Feb 01 20:11:28 2013],結合圖2-4也可以說明,數據熱加載之前已存在的httpd子進程可以正常服務,也就是說數據熱加載之前已存在的httpd子進程的數據源已成功切換到新的共享內存區(qū),可以排除crash由數據源切換導致的疑慮,確定是由動態(tài)創(chuàng)建的httpd子進程造成的。但是不能確定是在子進程的創(chuàng)建過程中還是創(chuàng)建完之后處理請求過程中。
? ? 圖4中key為0x7c000237的nattch值為0,而key為0x00924660的nattch值回升到522,結合apache錯誤日志可以知道:在出錯過程中,動態(tài)創(chuàng)建httpd子進程一直在crash,httpd父進程也在不停地創(chuàng)建子進程,但趕不上crash的速度,直到全crash掉,客戶端連不上服務器,siege退出,httpd子進程數量才回升至穩(wěn)定,如果繼續(xù)siege發(fā)請求,又會crash。由此確定crash發(fā)生在接口attach新共享內存時。
? ? 以上確定crash發(fā)生在httpd動態(tài)創(chuàng)建的子進程處理第一次請求過程,希望觀察在處理請求過程中_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啟動之后,多次執(zhí)行熱加載命令,加載不同大小的ip數據文件,然后gdb attach到未處理過請求的httpd子進程中觀察_len值。多次測試后發(fā)現未處理過請求的httpd子進程中_len始終為apache啟動時加載數據的大小。驗證了apache動態(tài)創(chuàng)建子進程機制為: apache啟動時,由父進程加載模塊,以后動態(tài)創(chuàng)建子進程時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 不存在則創(chuàng)建,存在則返回共享內存
?
?
?
?
總結
以上是生活随笔為你收集整理的一次共享内存引起的线上事故分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: APR分析-共享内存篇
- 下一篇: 清理apache共享内存引起的oracl