清明假日前一天,我們進行了服務器升級,然后第二天,同事給我來了一個電話,網站訪問異常卡頓,在MySQL里面發現大量的slow log。
于是我立刻放棄了休假的打算,連上了服務器,首先查看slow log,發現即使是單條記錄的主鍵查詢,也耗時將近1s多,這是完全不可能的事情。然后show processlist,發現大量的語句狀態為statistics,根本不能快速的執行完成。
然后查看了機器的CPU,發現MySQL的CPU很高,但IO的負載異常的低,表明MySQL真的在處理很大量的請求。我們不停地show processlist,發現有些select語句查詢,row examined竟然有幾十萬行之多,然后看語句,發現了很蛋疼的問題。類似如下:
SELECT * FROM tbl WHERE group = 123 and parent = 2
我們使用的是鄰接表模型在MySQL里面存儲樹狀結構,也就是每條記錄需要存儲父節點的ID,上面那條語句就是在一個group里面查詢某個節點下面的子節點。
然后這個表里面的索引竟然只有group,至于原因,貌似是最開始設計的同學認為一個group里面的數據不可能很多,即使全examined一次也無所謂。可是偏偏,一個用戶蛋疼的就往一個group里面放了幾十萬的數據,然后,任何一次查詢都會極大地拉低整個MySQL的性能。經過這一次,再一次讓我意識到,組里面的小盆友的MySQL知識還需要提升,如何用好索引,寫過高效的查詢語句真的不是一件簡單的事情。
于是我們立刻更新了索引,然后慢查詢馬上就沒有了。但沒有了超時,我們仍然發現,MySQL的CPU異常的高,show processlist的時候出現了很多system lock的情況,表明寫入并發量大,一直在爭鎖。通過日志發現,一個用戶不停地往自己的group里面增加文件,而從升級之前到第二天下午,這個用戶已經累計上傳了50w的文件。
最開始我們懷疑是被攻擊了,但通過日志,發現這個用戶的文件都是很正常的文件,并且完全像是在正常使用的。但仍不能排除嫌疑,于是我們立刻升級了一臺服務器,將LVS的流量全部切過去(幸好放假了,不然一臺機器鐵定頂不住),但令我們吃驚的是,記錄的訪問請求壓根沒有這個用戶任何的信息,但是文件仍然在不停地新增。然后通過show processlist查看,MySQL的請求全部是另外幾臺機器發過來的,但另外幾臺機器現在已經完全沒有流量了。
于是我們立刻想到了異步任務,會不會有某個異步任務死循環導致不停地插入,但監控rabbitmq,卻發現仍然沒有該用戶的任何信息。這時候,一個同事看代碼,突然發現,一個導致死循環的操作根本沒扔到異步任務里面,而是直接在服務里面go了一個coroutine跑了。于是我們立刻將其他幾臺機器重啟,然后MySQL正常了。
從晚上升級,到第二天真正發現問題并解決,因為我們的代碼bug,導致了我們整個服務幾乎不可用,不可不說是一個嚴重的教訓。
- MySQL索引設計不合理,導致極端情況下面拖垮了整個性能。
- 代碼健壯性不足,對于可能引起死循環的應用沒有做更多的檢查處理。
- 日志缺失,很多偷懶不寫日志,覺得沒啥用,但偏偏遇到問題了才會有用。
- 統計缺失,沒有詳細的統計信息,用來監控整個服務。
- 沒有更健壯的限頻限容策略,雖然我們開啟了,但因為程序內部bug,導致完全沒用。
有些時候,只有做好了完全的準備,才能更好的應對對突發情況,畢竟我們是在做產品,要對用戶負責,也要對自己的職業負責。
最后,這次的教訓再一次說明,節假日前一天晚上升級,后果很嚴重。