不少人在面試時,會被問到這樣的問題:遇到過什麼系統故障?怎麼解決的?下面是筆者根據本身15年互聯網研發經歷總結的多個線上故障真實案例。相信能夠幫你從容應對面試官的提問!java
本文圖很少,但內容很乾!理解爲主,面試爲輔,學以至用!程序員
故障一:JVM頻繁FULL GC快速排查
在分享此案例前,先聊聊哪些場景會致使頻繁Full GC:面試
- 內存泄漏(代碼有問題,對象引用沒及時釋放,致使對象不能及時回收)
- 死循環
- 大對象
尤爲是大對象,80%以上的狀況就是他。sql
那麼大對象從哪裏來的呢?shell
- 數據庫(包括Mysql和Mongodb等NOSql數據庫),結果集太大
- 第三方接口傳輸的大對象
- 消息隊列,消息太大
根據多年一線互聯網經驗,絕大部分狀況是數據庫大結果集致使。數據庫
好,如今咱們開始介紹此次線上故障:緩存
在沒有任何發佈的狀況下,POP服務(接入第三方商家的服務)忽然開始瘋狂Full GC,觀察堆內存監控沒內存泄漏,回滾到前一版本,問題仍然存在,尷尬了!!!安全
按照常規作法,通常先用jmap導出堆內存快照(jmap -dump:format=b,file=文件名 [pid]),而後用mat等工具分析出什麼對象佔用了大量空間,再查看相關引用找到問題代碼。這種方式定位問題週期會比較長,若是是關鍵服務,長時間不能定位解決問題,影響太大。bash
下面來看看咱們的作法。先按照常規作法分析堆內存快照,與此同時另外的同窗去查看數據庫服務器網絡IO監控,若是數據庫服務器網絡IO有明顯上升,而且時間點吻合,基本能夠肯定是數據庫大結果集致使了Full GC,趕忙找DBA快速定位大SQL(對DBA來講很簡單,分分鐘搞定,若是DBA不知道怎麼定位,那他要被開除了,哈哈),定位到SQL後再定位代碼就很是簡單了。按照這種辦法,咱們很快定位了問題。原來是一個接口必傳的參數沒傳進來,也沒加校驗,致使SQL語句where後面少了兩個條件,一次查幾萬條記錄出來,真坑啊!這種方法是否是要快不少,哈哈,5分鐘搞定。服務器
當時的DAO層是基於Mybatis實現的,出問題的SQL語句以下:
<select id="selectOrders" resultType="com.***.Order" >
select * from user where 1=1
<if test=" orderID != null ">
and order_id = #{orderID}
</if >
<if test="userID !=null">
and user_id=#{userID}
</if >
<if test="startTime !=null">
and create_time >= #{createTime}
</if >
<if test="endTime !=null">
and create_time <= #{userID}
</if >
</select>
複製代碼
上面SQL語句意思是根據orderID查一個訂單,或者根據userID查一個用戶全部的訂單,兩個參數至少要傳一個。可是兩個參數都沒傳,只傳了startTime和endTime。因此一次Select就查出了幾萬條記錄。
因此咱們在使用Mybatis的時候必定要慎用if test,一不當心就會帶來災難。後來咱們將上面的SQL拆成了兩個:
根據訂單ID查詢訂單:
<select id="selectOrderByID" resultType="com.***.Order" >
select * from user where
order_id = #{orderID}
</select>
複製代碼
根據userID查詢訂單:
<select id="selectOrdersByUserID" resultType="com.***.Order" >
select * from user where
user_id=#{userID}
<if test="startTime !=null">
and create_time >= #{createTime}
</if >
<if test="endTime !=null">
and create_time <= #{userID}
</if >
</select>
複製代碼
故障二:內存泄漏
介紹案例前,先了解一下內存泄漏和內存溢出的區別。
內存溢出:程序沒有足夠的內存使用時,就會發生內存溢出。內存溢出後程序基本上就沒法正常運行了。
內存泄漏:當程序不能及時釋放內存,致使佔用內存逐漸增長,就是內存泄漏。內存泄漏通常不會致使程序沒法運行。不過持續的內存泄漏,累積到內存上限時,就會發生內存溢出。在Java中,若是發生內存泄漏,會致使GC回收不完全,每次GC後,堆內存使用率逐漸增高。下圖是JVM發生內存泄漏的監控圖,咱們能夠看到每次GC後堆內存使用率都比之前提升了。
當時內存泄漏的場景是,用本地緩存(公司基礎架構組本身研發的框架)存放了商品數據,商品數量不算太多,幾十萬的樣子。若是隻存熱點商品,內存佔用不會太大,可是若是存放全量商品,內存就不夠了。初期咱們給每一個緩存記錄都加了7天的過時時間,這樣就能夠保證緩存中絕大部分都是熱點商品。不事後來本地緩存框架通過一次重構,過時時間被去掉了。沒有了過時時間,日積月累本地緩存愈來愈大,不少冷數據也被加載到了緩存。直到有一天接到告警短信,提示堆內存太高。趕忙經過jmap(jmap -dump:format=b,file=文件名 [pid] )下載了堆內存快照,而後用eclipse的mat工具分析快照,發現了本地緩存中有大量的商品記錄。定位問題後趕忙讓架構組加上了過時時間,而後逐個節點重啓了服務。
虧了咱們加了服務器內存和JVM堆內存監控,及時發現了內存泄漏的問題。不然隨着泄漏問題日積月累,若是哪天真的OOM就慘了。因此技術團隊除了作好CPU,內存等運維監控,JVM監控也很是重要。
故障三:冪等問題
不少年前,筆者在一家大型電商公司作Java程序員,當時開發了積分服務。當時的業務邏輯是,用戶訂單完結後,訂單系統發送消息到消息隊列,積分服務接到消息後給用戶積分,在用戶現有的積分上加上新產生的積分。
因爲網絡等緣由會有消息重複發送的狀況,這樣也就致使了消息的重複消費。當時筆者仍是個初入職場的小菜鳥,並無考慮到這種狀況。因此上線後偶爾會出現重複積分的狀況,也就是一個訂單完結後會給用戶加兩次或屢次積分。
後來咱們加了一個積分記錄表,每次消費消息給用戶增長積分前,先根據訂單號查一遍積分記錄表,若是沒有積分記錄纔給用戶增長積分。這也就是所謂的「冪等性」,即屢次重複操做不影響最終的結果。實際開發中不少須要重試或重複消費的場景都要實現冪等,以保證結果的正確性。例如,爲了不重複支付,支付接口也要實現冪等。
故障四:緩存雪崩
咱們常常會遇到須要初始化緩存的狀況。好比,咱們曾經經歷過用戶系統重構,用戶系統表結構發生了變化,緩存信息也要變。重構完成後上線前,須要初始化緩存,將用戶信息批量存入Reids。每條用戶信息緩存記錄過時時間是1天,記錄過時後再從數據庫查詢最新的數據並拉取到Redis中。灰度上線時一切正常,因此很快就全量發佈了。整個上線過程很是順利,碼農們也很開心。
不過,次日,災難發生了!到某一個時間點,各類報警紛至沓來。用戶系統響應忽然變得很是慢,甚至一度沒有任何響應。查看監控,用戶服務CPU忽然飆高(IO wait很高),Mysql訪問量激增,Mysql服務器壓力也隨之暴增,Reids緩存命中率也跌到了極點。依賴於咱們強大的監控系統(運維監控,數據庫監控,APM全鏈路性能監控),很快定位了問題。緣由就是Reids中大量用戶記錄集中失效,獲取用戶信息的請求在Redis中查不到用戶記錄,致使大量的請求穿透到數據庫,瞬間給數據庫帶來巨大壓力。同時用戶服務和相關聯的其餘服務也都受到了影響。
這種緩存集中失效,致使大量請求同時穿透到數據庫的狀況,就是所謂的「緩存雪崩」。若是沒到緩存失效時間點,性能測試也測不出問題。因此必定要引發你們注意。
因此,須要初始化緩存數據時,必定要保證每一個緩存記錄過時時間的離散性。例如,咱們給這些用戶信息設置過時時間,能夠採用一個較大的固定值加上一個較小的隨機值。好比過時時間能夠是:24小時 + 0到3600秒的隨機值。
故障五:磁盤IO致使線程阻塞
問題發生在2017年下半年,有一段時間地理網格服務時不常的會響應變慢,每次持續幾秒鐘到幾十秒鐘就自動恢復。
若是響應變慢是持續的還好辦,直接用jstack抓線程堆棧,基本能夠很快定位問題。關鍵持續時間只有最多幾十秒鐘,並且是偶發的,一天只發生一兩次,有時幾天才發生一次,發生時間點也不肯定,人盯着而後用jstack手工抓線程堆棧顯然不現實。
好吧,既然手工的辦法不現實,我們就來自動的,寫一個shell腳本自動定時執行jstack,5秒執行一次jstack,每次執行結果放到不一樣日誌文件中,只保存20000個日誌文件。
Shell腳本以下:
#!/bin/bash
num=0
log="/tmp/jstack_thread_log/thread_info"
cd /tmp
if [ ! -d "jstack_thread_log" ]; then
mkdir jstack_thread_log
fi
while ((num <= 10000));
do
ID=`ps -ef | grep java | grep gaea | grep -v "grep" | awk '{print $2}'`
if [ -n "$ID" ]; then
jstack $ID >> ${log}
fi
num=$(( $num + 1 ))
mod=$(( $num%100 ))
if [ $mod -eq 0 ]; then
back=$log$num
mv $log $back
fi
sleep 5
done
複製代碼
下一次響應變慢的時候,咱們找到對應時間點的jstack日誌文件,發現裏面有不少線程阻塞在logback輸出日誌的過程,後來咱們精簡了log,而且把log輸出改爲異步,問題解決了,這個腳本果然好用!建議你們保留,之後遇到相似問題時,能夠拿來用!
故障六:數據庫死鎖問題
在分析案例以前,咱們先了解一下MySQL INNODB。在MySQL INNODB引擎中主鍵是採用聚簇索引的形式,即在B樹的葉子節點中既存儲了索引值也存儲了數據記錄,即數據記錄和主鍵索引是存在一塊兒的。而普通索引的葉子節點存儲的只是主鍵索引的值,一次查詢找到普通索引的葉子節點後,還要根據葉子節點中的主鍵索引去找到聚簇索引葉子節點並拿到其中的具體數據記錄,這個過程也叫「回表」。
故障發生的場景是關於咱們商城的訂單系統。有一個定時任務,每一小時跑一次,每次把全部一小時前未支付訂單取消掉。而客服後臺也能夠批量取消訂單。
訂單表t_order結構大至以下:
id | 訂單id,主鍵 |
---|---|
status | 訂單狀態 |
created_time | 訂單建立時間 |
id是表的主鍵,created_time字段上是普通索引。
聚簇索引(主鍵id)
id(索引) | status | created_time |
---|---|---|
1 | UNPAID | 2020-01-01 07:30:00 |
2 | UNPAID | 2020-01-01 08:33:00 |
3 | UNPAID | 2020-01-01 09:30:00 |
4 | UNPAID | 2020-01-01 09:39:00 |
5 | UNPAID | 2020-01-01 09:50:00 |
普通索引(created_time字段)
created_time(索引) | id(主鍵) |
---|---|
2020-01-01 09:50:00 | 5 |
2020-01-01 09:39:00 | 4 |
2020-01-01 09:30:00 | 3 |
2020-01-01 08:33:00 | 2 |
2020-01-01 07:30:00 | 1 |
定時任務每一小時跑一次,每次把全部一小時前兩小時內的未支付訂單取消掉,好比上午11點會取消8點到10點的未支付訂單。SQL語句以下:
update t_order set status = 'CANCELLED' where created_time > '2020-01-01 08:00:00' and created_time < '2020-01-01 10:00:00' and status = 'UNPAID'
複製代碼
客服批量取消訂單SQL以下:
update t_order set status = 'CANCELLED' where id in (2, 3, 5) and status = 'UNPAID'
複製代碼
上面的兩條語句同時執行就可能發生死鎖。咱們來分析一下緣由。
第一條定時任務的SQL,會先找到created_time普通索引並加鎖,而後再在找到主鍵索引並加鎖。
第一步,created_time普通索引加鎖
第二步,主鍵索引加鎖
第二條客服批量取消訂單SQL,直接走主鍵索引,直接在主鍵索引上加鎖。
咱們能夠看到,定時任務SQL對主鍵加鎖順序是5,4,3,2。客服批量取消訂單SQL對主鍵加鎖順序是2,3,5。當第一個SQL對3加鎖後,正準備對2加鎖時,發現2已經被第二個SQL加鎖了,因此第一個SQL要等待2的鎖釋放。而此時第二個SQL準備對3加鎖,卻發現3已經被第一個SQL加鎖了,就要等待3的鎖釋放。兩個SQL互相等待對方的鎖,也就發生了「死鎖」。
解決辦法就是從SQL語句上保證加鎖順序一致。或者把客服批量取消訂單SQL改爲每次SQL操做只能取消一個訂單,而後在程序裏屢次循環執行SQL,若是批量操做的訂單數量很少,這種笨辦法也是可行的。
故障七:域名劫持
先看看DNS解析是怎麼回事,當咱們訪問www.baidu.com時,首先會根據www.baidu.com到DNS域名解析服務器去查詢百度服務器對應的IP地址,而後再經過http協議訪問該IP地址對應的網站。而DNS劫持是互聯網攻擊的一種方式,經過攻擊域名解析服務器(DNS)或者僞造域名解析服務器,把目標網站域名解析到其餘的IP。從而致使請求沒法訪問目標網站或者跳轉到其餘網站。以下圖:
下面這張圖是咱們曾經經歷過的DNS劫持的案例。
看圖中的紅框部分,原本上方的圖片應該是商品圖片,可是卻顯示成了廣告圖片。是否是圖片配錯了?不是,是域名(DNS)被劫持了。本來應該顯示存儲在CDN上的商品圖片,可是被劫持以後卻顯示了其餘網站的廣告連接圖片。因爲當時的CDN圖片連接採用了不安全的http協議,因此很容易被劫持。後來改爲了https,問題就解決了。
固然域名劫持有不少方式,https也不能規避全部問題。因此,除了一些安全防禦措施,不少公司都有本身的備用域名,一旦發生域名劫持能夠隨時切換到備用域名。
故障八:帶寬資源耗盡
帶寬資源耗盡致使系統沒法訪問的狀況,雖然很少見,可是也應該引發你們的注意。來看看,以前遇到的一塊兒事故。
場景是這樣的。社交電商每一個分享出去的商品圖片都有一個惟一的二維碼,用來區分商品和分享者。因此二維碼要用程序生成,最初咱們在服務端用Java生成二維碼。前期因爲系統訪問量不大,系統一直沒什麼問題。可是有一天運營忽然搞了一次優惠力度空前的大促,系統瞬時訪問量翻了幾十倍。問題也就隨之而來了,網絡帶寬直接被打滿,因爲帶寬資源被耗盡,致使不少頁面請求響應很慢甚至沒任何響應。緣由就是二維碼生成數量瞬間也翻了幾十倍,每一個二維碼都是一張圖片,對帶寬帶來了巨大壓力。
怎麼解決呢?若是服務端處理不了,就考慮一下客戶端。把生成二維碼放到客戶端APP處理,充分利用用戶終端手機,目前Andriod,IOS或者React都有相關生成二維碼的SDK。這樣不但解決了帶寬問題,並且也釋放了服務端生成二維碼時消耗的CPU資源(生成二維碼過程須要必定的計算量,CPU消耗比較明顯)。
外網帶寬很是昂貴,咱們仍是要省着點用啊!
看完三件事❤️
若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:
-
點贊,轉發,有大家的 『點贊和評論』,纔是我創造的動力。
-
關注公衆號 『 java爛豬皮 』,不按期分享原創知識。
-
同時能夠期待後續文章ing🚀
做者:馮濤 出處:https://club.perfma.com/article/1678492