<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          這幾招,讓服務(wù)的可用性提升到5個9

          共 6283字,需瀏覽 13分鐘

           ·

          2021-10-30 16:21

          對每一個程序員而言,故障都是懸在頭上的達(dá)摩克利斯之劍,都唯恐避之不及,如何避免故障是每一個程序員都在苦苦追尋希望解決的問題。對于這一問題,大家都可以從需求分析、架構(gòu)設(shè)計、代碼編寫、測試、code review、上線、線上服務(wù)運(yùn)維等各個視角給出自己的答案。

          我們大部分服務(wù)都是如下的結(jié)構(gòu),既要給使用方使用,又依賴于他人提供的第三方服務(wù),中間又穿插了各種業(yè)務(wù)、算法、數(shù)據(jù)等邏輯,這里面每一塊都可能是故障的來源。如何避免故障?我用一句話概括 : 懷疑第三方,防備使用方,做好自己

          1. 懷疑第三方

          堅持一條信念:“所有第三方服務(wù)都不可靠”,不管第三方什么天花亂墜的承諾?;谶@樣的信念,我們需要有以下行動。

          1.1 有兜底,制定好業(yè)務(wù)降級方案

          如果第三方服務(wù)掛掉怎么辦?我們業(yè)務(wù)也跟著掛掉?顯然這不是我們希望看到的結(jié)果,如果能制定好降級方案,那將大大提高服務(wù)的可靠性。舉幾個例子以便大家更好的理解。比如我們做個性化推薦服務(wù)時,需要從用戶中心獲取用戶的個性化數(shù)據(jù),以便代入到模型里進(jìn)行打分排序,但如果用戶中心服務(wù)掛掉,我們獲取不到數(shù)據(jù)了,那么就不推薦了?顯然不行,我們可以在 cache 里放置一份熱門商品以便兜底。又比如做一個數(shù)據(jù)同步的服務(wù),這個服務(wù)需要從第三方獲取最新的數(shù)據(jù)并更新到 mysql 中,恰好第三方提供了兩種方式:1)一種是消息通知服務(wù),只發(fā)送變更后的數(shù)據(jù);2)一種是 HTTP 服務(wù),需要我們自己主動調(diào)用獲取數(shù)據(jù)。我們一開始選擇消息同步的方式,因?yàn)閷?shí)時性更高,但是之后就遭遇到消息遲遲發(fā)送不過來的問題,而且也沒什么異常,等我們發(fā)現(xiàn)一天時間已過去,問題已然升級為故障。合理的方式應(yīng)該兩個同步方案都使用,消息方式用于實(shí)時更新,HTTP 主動同步方式定時觸發(fā)(比如1小時)用于兜底,即使消息出了問題,通過主動同步也能保證一小時一更新。

          有些時候第三方服務(wù)表面看起來正常,但是返回的數(shù)據(jù)是被污染的,這時還有什么方法兜底嗎?有人說這個時候除了通知第三方快速恢復(fù)數(shù)據(jù),基本只能干等了。舉個例子,我們做移動端的檢索服務(wù),其中需要調(diào)用第三方接口獲取數(shù)據(jù)來構(gòu)建倒排索引,如果第三方數(shù)據(jù)出錯,我們的索引也將出錯,繼而導(dǎo)致我們的檢索服務(wù)篩選出錯誤的內(nèi)容。第三方服務(wù)恢復(fù)數(shù)據(jù)最快要半小時,我們構(gòu)建索引也需要半小時,即可能有超過 1 個多小時的時間檢索服務(wù)將不能正常使用,這是不可接受的。如何兜底呢?我們采取的方法是每隔一段時間保存全量索引文件快照,一旦第三方數(shù)據(jù)源出現(xiàn)數(shù)據(jù)污染問題,我們先按下停止索引構(gòu)建的開關(guān),并快速回滾到早期正常的索引文件快照,這樣盡管數(shù)據(jù)不是很新(可能1小時之前),但是至少能保證檢索有結(jié)果,不至于對交易產(chǎn)生特別大的影響。

          1.2 遵循快速失敗原則,一定要設(shè)置超時時間

          某服務(wù)調(diào)用的一個第三方接口正常響應(yīng)時間是 50 ms,某天該第三方接口出現(xiàn)問題,大約有 15% 的請求響應(yīng)時間超過 2s,沒過多久服務(wù) load 飆高到 10 以上,響應(yīng)時間也非常緩慢,即第三方服務(wù)將我們服務(wù)拖垮了。為什么會被拖垮?沒設(shè)置超時!我們采用的是同步調(diào)用方式,使用了一個線程池,該線程池里最大線程數(shù)設(shè)置了 50,如果所有線程都在忙,多余的請求就放置在隊列里中。如果第三方接口響應(yīng)時間都是 50 ms 左右,那么線程都能很快處理完自己手中的活,并接著處理下一個請求,但是不幸的是如果有一定比例的第三方接口響應(yīng)時間為 2 s,那么最后這 50 個線程都將被拖住,隊列將會堆積大量的請求,從而導(dǎo)致整體服務(wù)能力極大下降。

          正確的做法是和第三方商量確定個較短的超時時間比如 200 ms,這樣即使他們服務(wù)出現(xiàn)問題也不會對我們服務(wù)產(chǎn)生很大影響。

          1.3 適當(dāng)保護(hù)第三方,慎重選擇重試機(jī)制

          需要結(jié)合自己的業(yè)務(wù)以及異常來仔細(xì)斟酌是否使用重試機(jī)制。比如調(diào)用某第三方服務(wù),報了個異常,有些同學(xué)就不管三七二十一就直接重試,這樣是不對的,比如有些業(yè)務(wù)返回的異常表示業(yè)務(wù)邏輯出錯,那么你怎么重試結(jié)果都是異常;又如有些異常是接口處理超時異常,這個時候就需要結(jié)合業(yè)務(wù)來判斷了,有些時候重試往往會給后方服務(wù)造成更大壓力,啟到雪上加霜的效果。

          2. 防備使用方

          這里又要堅持一條信念:“所有的使用方都不靠譜”,不管使用方什么天花亂墜的保證?;谶@樣的信念,我們需要有以下行動。

          2.1 設(shè)計一個好的 API,避免誤用

          過去兩年間看過不少故障,直接或間接原因來自于糟糕的接口。如果你的接口讓很多人誤用,那要好好反思自己的接口設(shè)計了,接口設(shè)計雖然看著簡單,但是學(xué)問很深,建議大家好好看看 Joshua Bloch 的演講《How to Design a Good API & Why it Matters(如何設(shè)計一個好的 API 及為什么這很重要)》以及《Java API 設(shè)計清單》。

          下面簡單談?wù)勎业慕?jīng)驗(yàn):

          • 遵循接口最少暴露原則。使用方用多少接口我們就提供多少,因?yàn)樘峁┑慕涌谠蕉嘣饺菀壮霈F(xiàn)亂用現(xiàn)象,言多必失嘛。此外接口暴露越多自己維護(hù)成本就越高。

          • 不要讓使用方做接口可以做的事情。如果使用方需要調(diào)用我們接口多次才能進(jìn)行一個完整的操作,那么這個接口設(shè)計就可能有問題。比如獲取數(shù)據(jù)的接口,如果僅僅提供 getData(int id) 接口,那么使用方如果要一次性獲取 20 個數(shù)據(jù),它就需要循環(huán)遍歷調(diào)用我們接口 20 次,不僅使用方性能很差,也無端增加了我們服務(wù)的壓力,這時提供 getDataList(List?idList) 接口顯然是必要的。

          • 避免長時間執(zhí)行的接口。還是以獲取數(shù)據(jù)方法為例:getDataList(List?idList)。假設(shè)一個用戶一次傳 1w 個 id 進(jìn)來,我們的服務(wù)估計沒個幾秒出不來結(jié)果,而且往往是超時的結(jié)果,用戶怎么調(diào)用結(jié)果都是超時異常,那怎么辦?限制長度,比如限制長度為 100,即每次最多只能傳 100 個 id,這樣就能避免長時間執(zhí)行,如果用戶傳的 id 列表長度超過100就報異常。加了這樣限制后,必須要讓使用方清晰地知道這個方法有此限制。之前就遇到誤用的情況,某用戶一個訂單買了超過 100 個商品,該訂單服務(wù)需要調(diào)用商品中心接口獲取該訂單下所有商品的信息,但是怎么調(diào)用都失敗,而且異常也沒打出什么有價值的信息,后來排查好久才得知是商品中心接口做了長度限制。怎么才能做到加了限制,又不讓用戶誤用呢?兩種思路:1)接口幫用戶做了分割調(diào)用操作,比如用戶傳了 1w 個 id,接口內(nèi)部分割成 100 個 id 列表(每個長度 100),然后循環(huán)調(diào)用,這樣對使用方屏蔽了內(nèi)部機(jī)制,對使用方透明;2)讓用戶自己做分割,自己寫循環(huán)顯示調(diào)用,這樣需要讓用戶知道我們方法做了限制,具體方法有:1)改變方法名,比如getDataListWithLimitLength(List?idList); 2)增加注釋;3)如果長度超過 100,很明確地拋出異常,很直白地進(jìn)行告知。

          • 參數(shù)易用原則。避免參數(shù)長度太長,一般超過 3 個后就較難使用,那有人說了我參數(shù)就是這么多,那怎么辦?寫個參數(shù)類嘛!此外,避免連續(xù)的同類型的參數(shù),不然很容易誤用。能用其它類型如 int 等的盡量不要用 String 類型,這也是避免誤用的方法。

          • 異常。接口應(yīng)當(dāng)最真實(shí)的反應(yīng)出執(zhí)行中的問題,更不能用聰明的代碼做某些特別處理。經(jīng)??吹揭恍┩瑢W(xué)接口代碼里一個 try catch,不管內(nèi)部拋了什么異常,捕獲后返回空集合。這讓使用方很無奈,很多時候不知道是自己參數(shù)傳的問題,還是服務(wù)方內(nèi)部的問題,而一旦未知就可能誤用了。

          1. public List<Integer> test() {

          2. ? ? ? ?try {

          3. ? ? ? ? ? ?...

          4. ? ? ? ?} catch (Exception e) {

          5. ? ? ? ? ? ?return Collections.emptyList();

          6. ? ? ? ?}

          7. }

          2.2 流量控制,按服務(wù)分配流量,避免濫用

          相信很多做過高并發(fā)服務(wù)的同學(xué)都碰到類似事件:某天 A 君突然發(fā)現(xiàn)自己的接口請求量突然漲到之前的 10 倍,沒多久該接口幾乎不可使用,并引發(fā)連鎖反應(yīng)導(dǎo)致整個系統(tǒng)崩潰。為什么會漲 10 倍,難道是接口被外人攻擊了,以我的經(jīng)驗(yàn)看一般內(nèi)部人“作案”可能性更大。之前還見過有同學(xué) mapreduce job 調(diào)用線上服務(wù),分分鐘把服務(wù)搞死。如何應(yīng)對這種情況?生活給了我們答案:比如老式電閘都安裝了保險絲,一旦有人使用超大功率的設(shè)備,保險絲就會燒斷以保護(hù)各個電器不被強(qiáng)電流給燒壞。同理我們的接口也需要安裝上“保險絲”,以防止非預(yù)期的請求對系統(tǒng)壓力過大而引起的系統(tǒng)癱瘓,當(dāng)流量過大時,可以采取拒絕或者引流等機(jī)制。具體限流算法參見《接口限流實(shí)踐》一文。

          2.3 做好自己

          做好自己是個非常大的話題,從需求分析、架構(gòu)設(shè)計 、代碼編寫、測試、code review、上線、線上服務(wù)運(yùn)維等階段都可以重點(diǎn)展開介紹,這次簡單分享下架構(gòu)設(shè)計、代碼編寫上的幾條經(jīng)驗(yàn)原則。

          2.3.1 單一職責(zé)原則

          對于工作了兩年以上的同學(xué)來說,設(shè)計模式應(yīng)該好好看看,我覺得各種具體的設(shè)計模式其實(shí)并不重要,重要的是背后體現(xiàn)的原則。比如單一職責(zé)原則,在我們的需求分析、架構(gòu)設(shè)計、編碼等各個階段都非常有指導(dǎo)意義。在需求分析階段,單一職責(zé)原則可以界定我們服務(wù)的邊界,如果服務(wù)邊界如果沒界定清楚,各種合理的不合理的需求都接,最后導(dǎo)致服務(wù)出現(xiàn)不可維護(hù)、不可擴(kuò)展、故障不斷的悲哀結(jié)局。對于架構(gòu)來講,單一職責(zé)也非常重要。比如讀寫模塊放置在一起,導(dǎo)致讀服務(wù)抖動非常厲害,如果讀寫分離那將大大提高讀服務(wù)的穩(wěn)定性(讀寫分離);比如一個服務(wù)上同時包含了訂單、搜索、推薦的接口,那么如果推薦出了問題可能影響訂單的功能,那這個時候就可以將不同接口拆分為獨(dú)立服務(wù),并獨(dú)立部署,這樣一個出問題也不會影響其他服務(wù)(資源隔離);又比如我們的圖片服務(wù)使用獨(dú)立域名、并放置到cdn上,與其它服務(wù)獨(dú)立(動靜分離)。從代碼角度上講,一個類只干一件事情,如果你的類干了多個事情,就要考慮將他分開。這樣做的好處是非常清晰,以后修改起來非常方便,對其它代碼的影響就很小。再細(xì)粒度看類里的方法,一個方法也只干一個事情,即只有一個功能,如果干兩件事情,那就把它分開,因?yàn)樾薷囊粋€功能可能會影響到另一個功能。

          2.3.2 控制資源的使用

          寫代碼腦子一定要繃緊一根弦,認(rèn)知到我們所在的機(jī)器資源是有限的。機(jī)器資源有哪些?CPU、內(nèi)存、網(wǎng)絡(luò)、磁盤等,如果不做好保護(hù)控制工作,一旦某一資源滿負(fù)荷,很容易導(dǎo)致出現(xiàn)線上問題。

          2.3.2.1 CPU 資源怎么限制
          • 計算算法優(yōu)化。如果服務(wù)需要進(jìn)行大量的計算,比如推薦排序服務(wù),那么務(wù)必對你的計算算法進(jìn)行優(yōu)化,比如筆者曾經(jīng)對地理空間距離計算這一重度使用的算法進(jìn)行了優(yōu)化,取得了較好的效果,詳見《地理空間距離計算優(yōu)化》一文。

          • 鎖。對于很多服務(wù)而言,沒有那么多耗費(fèi)計算資源的算法,但 CPU 使用率也很高,這個時候需要看看鎖的使用情況,我的建議是如無必要,盡量不用顯式使用鎖。

          • 習(xí)慣問題。比如寫循環(huán)的時候,千萬要檢查看看是否能正確退出,有些時候一不小心,在某些條件下就成為死循環(huán),很著名的案例就是《多線程下HashMap的死循環(huán)問題》。比如集合遍歷時候使用性能較差的遍歷方式、String + 檢查,如果有超過多個 String 相加,是否使用 StringBuffer.append?

          • 盡量使用線程池。通過線程池來限制線程的數(shù)目,避免線程過多造成的線程上下文切換的開銷。

          • JVM 參數(shù)調(diào)優(yōu)。JVM 參數(shù)也會影響 CPU 的使用,如《發(fā)布或重啟線上服務(wù)時抖動問題解決方案》。

          2.3.2.2 內(nèi)存資源怎么限制
          • JVM 參數(shù)設(shè)置。通過 JVM 參數(shù)的設(shè)置來限制內(nèi)存使用,JVM 參數(shù)調(diào)優(yōu)比較靠經(jīng)驗(yàn),有一篇朋友寫的好文可以參考《Linux 與 JVM 的內(nèi)存關(guān)系分析》。

          • 初始化 Java 集合類大小。使用 Java 集合類的時候盡量初始化大小,在長連接服務(wù)等耗費(fèi)內(nèi)存資源的服務(wù)中這種優(yōu)化非常重要。

          • 使用內(nèi)存池/對象池

          • 使用線程池的時候一定要設(shè)置隊列的最大長度。之前看過好多起故障都是由于隊列最大長度沒有限制最后導(dǎo)致內(nèi)存溢出。

          • 如果數(shù)據(jù)較大避免使用本地緩存。如果數(shù)據(jù)量較大,可以考慮放置到分布式緩存如 Redis、Tair 等,不然 gc 都可能把自己服務(wù)卡死。

          • 對緩存數(shù)據(jù)進(jìn)行壓縮。比如之前做推薦相關(guān)服務(wù)時,需要保存用戶偏好數(shù)據(jù),如果直接保存可能有 12G,后來采用短文本壓縮算法直接壓縮到 6G,不過這時一定要考慮好壓縮解壓縮算法的 cpu 使用率、效率與壓縮率的平衡,一些壓縮率很高但是性能很差的算法,也不適合線上實(shí)時調(diào)用。有些時候直接使用 probuf 來序列化之后保存,這樣也能節(jié)省內(nèi)存空間。

          • 清楚第三方軟件實(shí)現(xiàn)細(xì)節(jié),精確調(diào)優(yōu)。在使用第三方軟件時,只有清楚細(xì)節(jié)后才知道怎么節(jié)約內(nèi)存,這點(diǎn)我在實(shí)際工作中深有體會,比如之前在閱讀過lucene的源碼后發(fā)現(xiàn)我們的索引文件原來是可以壓縮的,而這在說明文檔中都找不到,具體參考《lucene索引文件大小優(yōu)化小結(jié)》一文。

          2.3.2.3 網(wǎng)絡(luò)資源怎么限制
          • 減少調(diào)用的次數(shù)。經(jīng)常看到有同學(xué)在循環(huán)里用 redis/tair 的 get,如果意識到這里面的網(wǎng)絡(luò)開銷的話就應(yīng)該使用批量處理;又如在推薦服務(wù)中經(jīng)常遇到要去多個地方去取數(shù)據(jù),一般采用多線程并行去取數(shù)據(jù),這個時候不僅耗費(fèi)cpu資源,也耗費(fèi)網(wǎng)絡(luò)資源,一種在實(shí)際中常常采用的方法就是先將很多數(shù)據(jù)離線存儲到一塊 ,這時候線上服務(wù)只要一個請求就能將所有數(shù)據(jù)獲取。

          • 減少傳輸?shù)臄?shù)據(jù)量。一種方法是壓縮后傳輸,還有一種就是按需傳輸,比如經(jīng)常遇到的 getData(int id),如果我們返回該 id 對應(yīng)的 Data 所有信息,一來人家不需要,二來數(shù)據(jù)量傳輸太大,這個時候可以改為 getData(int id, Listfields),使用方傳輸相應(yīng)的字段過來,服務(wù)端只返回使用方需要的字段即可。

          2.3.2.4 磁盤資源怎么限制

          打日志要控制量,并定期清理。1)只打印關(guān)鍵的異常日志;2)對日志大小進(jìn)行監(jiān)控報警。我有一次就遇到了第三方服務(wù)掛了,然后我這邊就不斷打印調(diào)用該第三方服務(wù)異常的日志,本來我的服務(wù)有降級方案,如果第三方服務(wù)掛了會自動使用其它服務(wù),但是突然收到報警說我服務(wù)掛了,登上機(jī)器一看才知道是磁盤不夠?qū)е碌谋罎ⅲ?)定期對日志進(jìn)行清理,比如用 crontab,每隔幾天對日志進(jìn)行清理;4)打印日志到遠(yuǎn)端,對于一些比較重要的日志可以直接將日志打印到遠(yuǎn)端HDFS文件系統(tǒng)里;

          2.3.3 避免單點(diǎn)

          不要把雞蛋放在一個籃子上!從大層次上講服務(wù)可以多機(jī)房部署、異地多活;從自己設(shè)計角度上講,服務(wù)應(yīng)該能做到水平擴(kuò)展。對于很多無狀態(tài)的服務(wù),通過 nginx、zookeeper 能輕松實(shí)現(xiàn)水平擴(kuò)展;對一些 job 類型的服務(wù),怎么避免單點(diǎn)呢,畢竟只能在一個節(jié)點(diǎn)上運(yùn)行,可以參考《Quartz應(yīng)用與集群原理分析》一文;對數(shù)據(jù)服務(wù)來說,怎么避免單點(diǎn)呢?簡而言之、可以通過分片、分層等方式來實(shí)現(xiàn),后面會有個博文總結(jié)。

          小結(jié)

          如何避免故障,我的經(jīng)驗(yàn)濃縮為一句:“懷疑第三方,防備使用方,做好自己”,大家也可以思考、總結(jié)并分享下自己的經(jīng)驗(yàn)。



          瀏覽 56
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  www.毛片网站 | 久久精品国产亚洲A | 国产无码福利在线视频 | 婷婷五月天丁香成人社区 | 欧美一级婬片A片免费老牛 |