檢視日誌之後,發現大量操作在對玩家程序進行gen_server:call()時就發生了超時,顯然玩家程序在忙於做其他事情,而且也並未產生太高的reduction。對阻塞的玩家程序呼叫process_info()後,能獲得的資訊非常有限。而無法登陸的錯誤,根本就沒有明顯的報錯。
我們意識到,使用者操作超時就是由於有的使用者程序啟動失敗造成的。我們的玩家有一些操作需要喚醒其他未登入玩家的程序,顯然是需要被喚醒的玩家程序也出現了啟動失敗的情況。而發生一次超時之後,該程序一直在等待目標程序的返回,所以一直未處理新來的訊息。然而,對其他玩家程序進行操作的時,我們使用的gen_server:call()在預設情況下會在5秒後超時,照理說不可能會長時間阻塞。
我們迅速review**,發現沒有在呼叫gen_server:call()時使用infinite超時的地方。同時,我們認為db會是乙個可能的瓶頸,但是在檢視慢查詢日誌之後一無所獲。然後我們嘗試讓客戶端在登陸時一直等待伺服器的返回,發現過了接近10-15分鐘之後,登陸請求居然成功返回了。這種現象在我們的認識(gen_server:call()五秒超時)下看來時極其詭異的。
獲得這個線索之後,我們在登陸的流程上新增了大量記錄時間的日誌,並且仔細閱讀了**和原始碼。最終,我們注意到跨節點的rpc:call()和啟動程序的supervisor:new_process()是不會超時的。看來罪魁禍首就是玩家程序上面的supervisor了。我們立即檢視了該supervisor的訊息佇列,發現已經積壓上千條。
我們的玩家程序在初始化時是同步等待的。單個玩家程序的啟動時間並不慢,不足以引起我們的注意。然而,在玩家數量增多之後,supervisor需要等待大量玩家程序的初始化完成,極大地降低處理請求的速度。
最終我們增加了supervisor的速度,並且將玩家程序的初始化修改為非同步,解決了問題。
需要記住的教訓有以下幾點:
1. 在關注worker效能的同時,也需要關注supervisor。尤其是會大量產生新程序的supervisor,其下的程序絕不可以使用同步初始化。
2. 對可能喚醒乙個甚至多個其他程序的操作應當重點注意,在測試時應當專門關照。
3. 不能輕視複雜的線上環境,效能測試時需要覆蓋盡可能多的複雜操作。能同時處理大量來自機械人的指令並不能說明系統能應對線上的壓力。
在解決前乙個問題之後,伺服器一直狀態良好,記憶體也未見異常增長。
晚上下班後邀朋友吃烤串慶祝一下,誰知道拍黃瓜剛端上來,同事就告訴我出大事兒了:邏輯伺服器例項消失了,所有玩家自然也登陸不上去了。丟下風中獨自凌亂的朋友回到公司,姑且先重啟了邏輯伺服器。
檢查日誌之後,沒有任何報錯和異常。然後開始分析伺服器留下的crashdump。
顯然,伺服器是在試圖擴張堆尺寸時發現作業系統的記憶體已經耗盡,才發生崩潰的。此時,ets和atom的情況也沒有異常,不是它們造成的記憶體暴漲。我們檢視了所有執行緒的快照,發現只有乙個玩家程序的新生代和老生代堆非常巨大,達到了gb級別。不幸的是,該程序在崩潰時出於gc狀態,其stack traceback都已經丟失,只能看見其pc正指向string:tokens()。
我們認為,可能是我們的某些**邏輯存在問題,使得內存在一段不短的時間後逐漸增長到如此之大。而且當時時間已經很晚,所以大家先行回家休息,計畫第二天再進行排查。
回家之後屁股還沒有坐熱,同事那邊又傳來噩耗,伺服器再一次崩潰了。看來記憶體暴漲是在短時間之內發生的。這樣的崩潰在當天晚上一共來了3次,發生異常的玩家全是同乙個,而且crashdump幾乎如出一轍。而該玩家的資料和行為未見明顯異常。
同事的第一反應是邏輯錯誤造成的無限遞迴呼叫,不過在嘗試之後,我們的**和協議應該不會允許這樣的錯誤。
string:tokens()這個方法代價不小,一次呼叫new的字串物件連同棧幀本身的結構,即便是最保守的估計都將需要數十位元組堆、棧空間。顯然記憶體的暴漲多半是某處**反覆呼叫該方法造成的。即便不是無限,也足以使得伺服器崩潰。由於資訊非常有限,我們不得不排查**中可能會反覆呼叫string:tokens()的地方。花費不少時間後,我們定位到了某商店的購買功能。該功能中商品一次可以購買複數個,且每次**都是動態的,需要解析表來獲得。而且,商品在需求中沒有限制購買次數!
該請求的數量引數是uint32,於是我們立即嘗試在請求時傳入0xffffffff,果不其然erts在十分鐘之內崩潰,crashdump也和事故時基本一致。的確,42億次函式呼叫,耗盡伺服器的記憶體綽綽有餘。這處**也寫的相當有問題,它選擇在遞迴中每次解析字串而非解析為列表後再進行遞迴,比較隨意和浪費。
顯然,這是一位不懷好意的玩家,修改了客戶端的**,向伺服器發出了極端資料型別的請求。
我們迅速在該處功能和其他系統中的類似功能中都對傳入的引數新增了限制,堵住了漏洞。日後應該會在code review和開發中加倍注意類似情況。
需要記住的教訓有以下幾點:
1. 需求不需要限制不代表程式不進行限制。
2. 哪怕是不那麼核心的功能,在開發時也不能過於隨意。應避免對cpu時間和記憶體的無謂浪費。
3. 不要盲目信任erlang的軟實時和搶占式排程。在測試中我們發現,該異常的玩家程序憑藉一己之力便大大降低整個伺服器的響應。所以對於個別熱點功能對其他功能的影響,我們應提高警惕並重點關注。
謹以此文為我在這家公司的工作畫上句號。雖然將要離開這個團隊,但也算是陪著她扛住了最後一波考驗。祝願同事們碼力日進,專案一切順利。
伺服器程序fork兩次原理
首先,要了解什麼叫殭屍程序,什麼叫孤兒程序,以及伺服器程序執行所需要的一些條件。兩次fork 就是為了解決這些相關的問題而出現的一種程式設計方法。孤兒程序 孤兒程序是指父程序在子程序結束之前死亡 return 或exit 如下圖1所示 圖1 孤兒程序 但是孤兒程序並不會像上面畫的那樣持續很長時間,當...
C 伺服器 關於兩次fork
我覺得這裡還有些重要的東西沒講,比如setsid 參見apne 8 11.兩次fork 的作用 首先,要了解什麼叫殭屍程序,什麼叫孤兒程序,以及伺服器程序執行所需要的一些條件。兩次fork 就是為了解決這些相關的問題而出現的一種程式設計方法。孤兒程序 孤兒程序是指父程序在子程序結束之前死亡 retu...
linux伺服器程序為何通常fork 兩次
首先,要了解什麼叫殭屍程序,什麼叫孤兒程序,以及伺服器程序執行所需要的一些條件。兩次fork 就是為了解決這些相關的問題而出現的一種程式設計方法。孤兒程序 孤兒程序是指父程序在子程序結束之前死亡 return 或exit 如下圖所示 但是孤兒程序並不會像上面畫的那樣持續很長時間,當系統發現孤兒程序時...