對每個程序員而言,故障都是懸在頭上的達摩克利斯之劍,都惟恐避之不及,如何避免故障是每個程序員都在苦苦追尋但願解決的問題。對於這一問題,你們均可以從需求分析、架構設計 、代碼編寫、測試、code review、上線、線上服務運維等各個視角給出本身的答案。本人結合本身兩年有限的互聯網後端工做經驗,從某幾個視角談談本身對這一問題的理解,不足之處,望你們多多指出。
咱們大部分服務都是以下的結構,既要給使用方使用,又依賴於他人提供的第三方服務,中間又穿插了各類業務、算法、數據等邏輯,這裏面每一塊均可能是故障的來源。如何避免故障?我用一句話歸納,「懷疑第三方,防備使用方,作好本身」。
1 懷疑第三方
堅持一條信念:「全部第三方服務都不可靠」,無論第三方什麼天花亂墜的承諾。基於這樣的信念,咱們須要有如下行動。
1.1 有兜底,制定好業務降級方案
若是第三方服務掛掉怎麼辦?咱們業務也跟着掛掉?顯然這不是咱們但願看到的結果,若是能制定好降級方案,那將大大提升服務的可靠性。舉幾個例子以便你們更好的理解。
好比咱們作個性化推薦服務時,須要從用戶中心獲取用戶的個性化數據,以便代入到模型裏進行打分排序,但若是用戶中心服務掛掉,咱們獲取不到數據了,那麼就不推薦了?顯然不行,咱們能夠在cache裏放置一份熱門商品以便兜底;
又好比作一個數據同步的服務,這個服務須要從第三方獲取最新的數據並更新到mysql中,剛好第三方提供了兩種方式:1)一種是消息通知服務,只發送變動後的數據;2)一種是http服務,須要咱們本身主動調用獲取數據。咱們一開始選擇消息同步的方式,由於實時性更高,可是以後就遭遇到消息遲遲發送不過來的問題,並且也沒什麼異常,等咱們發現一天時間已過去,問題已然升級爲故障。合理的方式應該兩個同步方案都使用,消息方式用於實時更新,http主動同步方式定時觸發(好比1小時)用於兜底,即便消息出了問題,經過主動同步也能保證一小時一更新。
有些時候第三方服務表面看起來正常,可是返回的數據是被污染的,這時還有什麼方法兜底嗎?有人說這個時候除了通知第三方快速恢復數據,基本只能乾等了。舉個例子,咱們作移動端的檢索服務,其中須要調用第三方接口獲取數據來構建倒排索引,若是第三方數據出錯,咱們的索引也將出錯,繼而致使咱們的檢索服務篩選出錯誤的內容。第三方服務恢復數據最快要半小時,咱們構建索引也須要半小時,便可能有超過1個多小時的時間檢索服務將不能正常使用,這是不可接受的。如何兜底呢?咱們採起的方法是每隔一段時間保存全量索引文件快照,一旦第三方數據源出現數據污染問題,咱們先按下中止索引構建的開關,並快速回滾到早期正常的索引文件快照,這樣儘管數據不是很新(可能1小時以前),可是至少能保證檢索有結果,不至於對交易產生特別大的影響。
1.2 遵循快速失敗原則,必定要設置超時時間
某服務調用的一個第三方接口正常響應時間是50ms,某天該第三方接口出現問題,大約有15%的請求響應時間超過2s,沒過多久服務load飆高到10以上,響應時間也很是緩慢,即第三方服務將咱們服務拖垮了。
爲何會被拖垮?沒設置超時!咱們採用的是同步調用方式,使用了一個線程池,該線程池裏最大線程數設置了50,若是全部線程都在忙,多餘的請求就放置在隊列裏中。若是第三方接口響應時間都是50ms左右,那麼線程都能很快處理完本身手中的活,並接着處理下一個請求,可是不幸的是若是有必定比例的第三方接口響應時間爲2s,那麼最後這50個線程都將被拖住,隊列將會堆積大量的請求,從而致使總體服務能力極大降低。
正確的作法是和第三方商量肯定個較短的超時時間好比200ms,這樣即便他們服務出現問題也不會對咱們服務產生很大影響。
1.3 適當保護第三方,慎重選擇重試機制
須要結合本身的業務以及異常來仔細斟酌是否使用重試機制。好比調用某第三方服務,報了個異常,有些同窗就無論三七二十一就直接重試,這樣是不對的,好比有些業務返回的異常表示業務邏輯出錯,那麼你怎麼重試結果都是異常;又若有些異常是接口處理超時異常,這個時候就須要結合業務來判斷了,有些時候重試每每會給後方服務形成更大壓力,啓到雪上加霜的效果。
2 防備使用方
這裏又要堅持一條信念:「全部的使用方都不靠譜」,無論使用方什麼天花亂墜的保證。基於這樣的信念,咱們須要有如下行動。
2.1 設計一個好的api(RPC、Restful),避免誤用
過去兩年間看過很多故障,直接或間接緣由來自於糟糕的接口。若是你的接口讓不少人誤用,那要好好反思本身的接口設計了,接口設計雖然看着簡單,可是學問很深,建議你們好好看看Joshua Bloch的演講《How to Design a Good API & Why it Matters(如何設計一個好的API及爲何這很重要)》以及《
Java API 設計清單》。
下面簡單談談個人經驗。
a) 遵循接口最少暴露原則
使用方用多少接口咱們就提供多少,由於提供的接口越多越容易出現亂用現象,言多必失嘛。此外接口暴露越多本身維護成本就越高。
b) 不要讓使用方作接口能夠作的事情
若是使用方須要調用咱們接口屢次才能進行一個完整的操做,那麼這個接口設計就可能有問題。好比獲取數據的接口,若是僅僅提供getData(int id);接口,那麼使用方若是要一次性獲取20個數據,它就須要循環遍歷調用咱們接口20次,不只使用方性能不好,也無故增長了咱們服務的壓力,這時提供getDataList(List<Integer> idList);接口顯然是必要的。
c)避免長時間執行的接口
仍是以獲取數據方法爲例:getDataList(List<Integer> idList); 假設一個用戶一次傳1w個id進來,咱們的服務估計沒個幾秒出不來結果,並且每每是超時的結果,用戶怎麼調用結果都是超時異常,那怎麼辦?限制長度,好比限制長度爲100,即每次最多隻能傳100個id,這樣就能避免長時間執行,若是用戶傳的id列表長度超過100就報異常。
加了這樣限制後,必需要讓使用方清晰地知道這個方法有此限制。以前就遇到誤用的狀況,某用戶一個訂單買了超過100個商品,該訂單服務須要調用商品中心接口獲取該訂單下全部商品的信息,可是怎麼調用都失敗,並且異常也沒打出什麼有價值的信息,後來排查很久才得知是商品中心接口作了長度限制。
怎麼才能作到加了限制,又不讓用戶誤用呢?
兩種思路:1)接口幫用戶作了分割調用操做,好比用戶傳了1w個id,接口內部分割成100個id列表(每一個長度100),而後循環調用,這樣對使用方屏蔽了內部機制,對使用方透明;2)讓用戶本身作分割,本身寫循環顯示調用,這樣須要讓用戶知道咱們方法作了限制,具體方法有:1)改變方法名,好比getDataListWithLimitLength(List<Integer> idList); ;2)增長註釋;3)若是長度超過 100,很明確地拋出異常,很直白地進行告知。
d)參數易用原則
避免參數長度太長,通常超過3個後就較難使用,那有人說了我參數就是這麼多,那怎麼辦?寫個參數類嘛!
此外避免連續的同類型的參數,否則很容易誤用。
能用其它類型如int等的儘可能不要用String類型,這也是避免誤用的方法。
e)異常
接口應當最真實的反應出執行中的問題,更不能用聰明的代碼作某些特別處理。常常看到一些同窗接口代碼裏一個try catch,無論內部拋了什麼異常,捕獲後返回空集合。
1
2
3
4
5
6
7
|
public List<Integer> test() {
try {
...
} catch (Exception e) {
return Collections.emptyList();
}
}
|
這讓使用方很無奈,不少時候不知道是本身參數傳的問題,仍是服務方內部的問題,而一旦未知就可能誤用了。
2.2 流量控制,按服務分配流量,避免濫用
相信不少作太高併發服務的同窗都碰到相似事件:某天A君忽然發現本身的接口請求量忽然漲到以前的10倍,沒多久該接口幾乎不可以使用,並引起連鎖反應致使整個系統崩潰。
爲何會漲10倍,難道是接口被外人攻擊了,以個人經驗看通常內部人「做案」可能性更大。以前還見過有同窗mapreduce job調用線上服務,分分鐘把服務搞死。
如何應對這種狀況?生活給了咱們答案:好比老式電閘都安裝了保險絲,一旦有人使用超大功率的設備,保險絲就會燒斷以保護各個電器不被強電流給燒壞。同理咱們的接口也須要安裝上「保險絲」,以防止非預期的請求對系統壓力過大而引發的系統癱瘓,當流量過大時,能夠採起拒絕或者引流等機制。具體限流算法參見《
接口限流實踐》一文。
3 作好本身
作好本身是個很是大的話題,從需求分析、架構設計 、代碼編寫、測試、code review、上線、線上服務運維等階段均可以重點展開介紹,此次簡單分享下架構設計、代碼編寫上的幾條經驗原則。
3.1 單一職責原則
對於工做了兩年以上的同窗來講,設計模式應該好好看看,我以爲各類具體的設計模式其實並不重要,重要的是背後體現的原則。好比單一職責原則,在咱們的需求分析、架構設計、編碼等各個階段都很是有指導意義。
在需求分析階段,單一職責原則能夠界定咱們服務的邊界,若是服務邊界若是沒界定清楚,各類合理的不合理的需求都接,最後致使服務出現不可維護、不可擴展、故障不斷的悲哀結局。
對於架構來說,單一職責也很是重要。好比讀寫模塊放置在一塊兒,致使讀服務抖動很是厲害,若是讀寫分離那將大大提升讀服務的穩定性(讀寫分離);好比一個服務上同時包含了訂單、搜索、推薦的接口,那麼若是推薦出了問題可能影響訂單的功能,那這個時候就能夠將不一樣接口拆分爲獨立服務,並獨立部署,這樣一個出問題也不會影響其餘服務(資源隔離);又好比咱們的圖片服務使用獨立域名、並放置到cdn上,與其它服務獨立(動靜分離)。
從代碼角度上講,一個類只幹一件事情,若是你的類幹了多個事情,就要考慮將他分開。這樣作的好處是很是清晰,之後修改起來很是方便,對其它代碼的影響就很小。再細粒度看類裏的方法,一個方法也只幹一個事情,即只有一個功能,若是幹兩件事情,那就把它分開,由於修改一個功能可能會影響到另外一個功能。
3.2 控制資源的使用
寫代碼腦子必定要繃緊一根弦,認知到咱們所在的機器資源是有限的。機器資源有哪些?cpu、內存、網絡、磁盤等,若是不作好保護控制工做,一旦某一資源滿負荷,很容易致使出現線上問題。
3.2.1 CPU資源怎麼限制?
a)計算算法優化
若是服務須要進行大量的計算,好比推薦排序服務,那麼務必對你的計算算法進行優化,好比筆者曾經對地理空間距離計算這一重度使用的算法進行了優化,取得了較好的效果,詳見《
地理空間距離計算優化》一文。
b)鎖
對於不少服務而言,沒有那麼多耗費計算資源的算法,但cpu使用率也很高,這個時候須要看看鎖的使用狀況,個人建議是如無必要,儘可能不用顯式使用鎖。
c) 習慣問題
好比寫循環的時候,千萬要檢查看看是否能正確退出,有些時候一不當心,在某些條件下就成爲死循環,很著名的案例就是《
多線程下HashMap的死循環問題》。好比集合遍歷時候使用性能較差的遍歷方式、String +檢查,若是有超過多個String相加,是否使用StringBuffer.append?
d)儘可能使用線程池
經過線程池來限制線程的數目,避免線程過多形成的線程上下文切換的開銷。
e)jvm參數調優
3.2.2 內存資源怎麼限制?
a)Jvm參數設置
b)初始化java集合類大小
使用java集合類的時候儘可能初始化大小,在長鏈接服務等耗費內存資源的服務中這種優化很是重要;
c)使用內存池/對象池
d)使用線程池的時候必定要設置隊列的最大長度
以前看過好多起故障都是因爲隊列最大長度沒有限制最後致使內存溢出。
e)若是數據較大避免使用本地緩存
若是數據量較大,能夠考慮放置到分佈式緩存如redis、tair等,否則gc均可能把本身服務卡死;
f)對緩存數據進行壓縮
好比以前作推薦相關服務時,須要保存用戶偏好數據,若是直接保存可能有12G,後來採用短文本壓縮算法直接壓縮到6G,不過這時必定要考慮好壓縮解壓縮算法的cpu使用率、效率與壓縮率的平衡,一些壓縮率很高可是性能不好的算法,也不適合線上實時調用。
有些時候直接使用probuf來序列化以後保存,這樣也能節省內存空間。
g)清楚第三方軟件實現細節,精確調優
在使用第三方軟件時,只有清楚細節後才知道怎麼節約內存,這點我在實際工做中深有體會,好比以前在閱讀過lucene的源碼後發現咱們的索引文件原來是能夠壓縮的,而這在說明文檔中都找不到,具體參考《
lucene索引文件大小優化小結》一文。
3.2.3 網絡資源怎麼限制?
a)減小調用的次數
減小調用的次數?常常看到有同窗在循環裏用redis/tair的get,若是意識到這裏面的網絡開銷的話就應該使用批量處理;又如在推薦服務中常常遇到要去多個地方去取數據,通常採用多線程並行去取數據,這個時候不只耗費cpu資源,也耗費網絡資源,一種在實際中經常採用的方法就是先將不少數據離線存儲到一塊 ,這時候線上服務只要一個請求就能將全部數據獲取。
b)減小傳輸的數據量
一種方法是壓縮後傳輸,還有一種就是按需傳輸,好比常常遇到的getData(int id),若是咱們返回該id對應的Data全部信息,一來人家不須要,二來數據量傳輸太大,這個時候能夠改成getData(int id, List<String> fields),使用方傳輸相應的字段過來,服務端只返回使用方須要的字段便可。
3.2.4 磁盤資源怎麼限制?
打日誌要控制量,並按期清理。1)只打印關鍵的異常日誌;2)對日誌大小進行監控報警。我有一次就遇到了第三方服務掛了,而後我這邊就不斷打印調用該第三方服務異常的日誌,原本個人服務有降級方案,若是第三方服務掛了會自動使用其它服務,可是忽然收到報警說我服務掛了,登上機器一看才知道是磁盤不夠致使的崩潰;3)按期對日誌進行清理,好比用crontab,每隔幾天對日誌進行清理;4)打印日誌到遠端,對於一些比較重要的日誌能夠直接將日誌打印到遠端HDFS文件系統裏;
3.3 避免單點
不要把雞蛋放在一個籃子上!從大層次上講服務能夠多機房部署、異地多活;從本身設計角度上講,服務應該能作到水平擴展。
對於不少無狀態的服務,經過nginx、zookeeper能輕鬆實現水平擴展;
對數據服務來講,怎麼避免單點呢?簡而言之、能夠經過分片、分層等方式來實現,後面會有個博文總結。
4 小結
如何避免故障?個人經驗濃縮爲一句:「懷疑第三方,防備使用方,作好本身」,你們也能夠思考、總結並分享下本身的經驗。