一次共享内存引起的线上事故分析
一、前言
? ? ipquery是一個用于根據(jù)ip查詢對應信息(地址、天氣等)的php模塊,基于共享內(nèi)存實現(xiàn),為了做到更新數(shù)據(jù)時不重啟php,我們引入了數(shù)據(jù)動態(tài)加載概念。如下圖1設計:
(圖1)
? ? 在調(diào)用查詢接口時,php進程會首先訪問共享內(nèi)存D,取出存儲在D中shmkey,然后再去訪問shmkey表示的內(nèi)容,熱加載的過程就是當數(shù)據(jù)有更新時,重新申請一塊共享內(nèi)存,把數(shù)據(jù)加載到這塊內(nèi)存中,然后把D中的內(nèi)容改成New data的shmkey,當IPQuery接口被調(diào)用時,如果取出的shmkey跟舊的shmkey不同,php進程就會dattach Old data, attach New data, 之后就可以訪問到新的數(shù)據(jù)了。
二、問題
? ??上線一段時間后出現(xiàn)了致命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ā)現(xiàn)此錯誤出自源代碼 :“_shmid = ?result::not_val<int>(shmget(_s_shm_key,_len,IPC_CREAT|0666),-1,"shmget failed!")”,錯誤碼errno 為22,Invalid argument(非法參數(shù)),可以確定是attach 共享內(nèi)存時報錯。
man shmget :
shmget函數(shù)返回錯誤碼22有兩種原因:
a、創(chuàng)建size<SHMMIN or size>SHMMAX的共享內(nèi)容;
b、指定key的共享內(nèi)存存在,但是size大于已存在共享內(nèi)存的大小。SHMMIN默認值為1,_len肯定是大于1的;
執(zhí)行命令:
cat /proc/sys/kernel/shmmax
可以看到SHMMAX值遠大于所申請的共享內(nèi)存大小,所以錯誤只可能是最后一種:共享內(nèi)存存在,但_len大于存在的共享內(nèi)存的size。
三、調(diào)試分析
? ?經(jīng)過配合測試,客戶端用siege一直打壓,執(zhí)行數(shù)據(jù)熱加載數(shù)分鐘后,問題重現(xiàn)了:
圖中的nattch是共享內(nèi)存當前被引用的次數(shù)。以下圖2、3、4是連續(xù)幾次執(zhí)行ipcs的結(jié)果。
(圖2)
(圖3)
(圖4)
? ?圖中key為0x00924660是每個httpd進程都要attach的共享內(nèi)存,對應圖1中的D,key為0x7c000237的是httpd子進程第一次處理請求時需要attach的共享內(nèi)存。從圖2和圖3可以看出,key為0x00924660和key為0x7c000237的nattch在減少,但都不為0,圖4中key為0x00924660的nattch值回升。但是key為0x7c000237的nattch值為0.
? ? 整個過程中,數(shù)據(jù)熱加載執(zhí)行的時間是[Fri Feb 01 20:06:38 2013],但error_log總最早出現(xiàn)錯誤時間為[Fri Feb 01 20:11:28 2013],結(jié)合圖2-4也可以說明,數(shù)據(jù)熱加載之前已存在的httpd子進程可以正常服務,也就是說數(shù)據(jù)熱加載之前已存在的httpd子進程的數(shù)據(jù)源已成功切換到新的共享內(nèi)存區(qū),可以排除crash由數(shù)據(jù)源切換導致的疑慮,確定是由動態(tài)創(chuàng)建的httpd子進程造成的。但是不能確定是在子進程的創(chuàng)建過程中還是創(chuàng)建完之后處理請求過程中。
? ? 圖4中key為0x7c000237的nattch值為0,而key為0x00924660的nattch值回升到522,結(jié)合apache錯誤日志可以知道:在出錯過程中,動態(tài)創(chuàng)建httpd子進程一直在crash,httpd父進程也在不停地創(chuàng)建子進程,但趕不上crash的速度,直到全crash掉,客戶端連不上服務器,siege退出,httpd子進程數(shù)量才回升至穩(wěn)定,如果繼續(xù)siege發(fā)請求,又會crash。由此確定crash發(fā)生在接口attach新共享內(nèi)存時。
? ? 以上確定crash發(fā)生在httpd動態(tài)創(chuàng)建的子進程處理第一次請求過程,希望觀察在處理請求過程中_len的變化,找出真正的真兇!于是用gdb在線上調(diào)試httpd,觀察_len的變化。
# : sudo gdb httpd
# : (gdb) attach pid
# : (gdb) b _Z16space_ptr_updatev(attach和切換數(shù)據(jù)源的函數(shù))
# : (gdb) c
客戶端啟動siege,當此進程運行到斷點處時,會停在端點上
# : (gdb) p idx->shmkey
# : (gdb) $3 = 3472884279(0xcf000237) 可以看到idx->shmkey是正確的。
# ::(gdb) p_len
# : (gdb) $5 = 927305456? ? ? ??
927305456是舊的共享內(nèi)存的大小,找到crash的真正原因了:請求的數(shù)據(jù)大小比存在的共享內(nèi)存大。
? ? 因為crash是由數(shù)據(jù)熱加載引起的,所以在apache啟動之后,多次執(zhí)行熱加載命令,加載不同大小的ip數(shù)據(jù)文件,然后gdb attach到未處理過請求的httpd子進程中觀察_len值。多次測試后發(fā)現(xiàn)未處理過請求的httpd子進程中_len始終為apache啟動時加載數(shù)據(jù)的大小。驗證了apache動態(tài)創(chuàng)建子進程機制為: apache啟動時,由父進程加載模塊,以后動態(tài)創(chuàng)建子進程時fork自己,復制地址空間到子進程空間。
四、解決方案
1、在attach共享內(nèi)存時把_len設置問題0:
_len = 0;
_shmid = result::not_val<int>(shmget(_s_shm_key_len, _len, IPC_CREAT|0666), -1, "shmget failed!");
2、在圖1中D上記錄最新的共享內(nèi)存的大小,attach之前把_len設成此值。
說明:
_len = 0? 獲取已存在的共享內(nèi)存,不存在則失敗
_len > 0 不存在則創(chuàng)建,存在則返回共享內(nèi)存
?
?
?
?
總結(jié)
以上是生活随笔為你收集整理的一次共享内存引起的线上事故分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: APR分析-共享内存篇
- 下一篇: 清理apache共享内存引起的oracl