大型服務(wù)端開(kāi)發(fā)的反模式技巧
1. 用線程池執(zhí)行異步任務(wù)
為了減少阻塞時(shí)間,加快響應(yīng)速度,把無(wú)需返回結(jié)果的操作變成異步任務(wù),用線程池來(lái)執(zhí)行,這是提高性能的一種手段。
你可能要驚訝了,這么做不對(duì)嗎?
首先,我們把異步任務(wù)分為兩種:
- 務(wù)必成功執(zhí)行的
- 不成功就放棄
顯然大多數(shù)時(shí)候都是***種。那么當(dāng)你把任務(wù)丟給線程池,你知道它完成了沒(méi)有嗎?
如果服務(wù)器宕機(jī)、升級(jí)或重啟,那些尚未完成或還在排隊(duì)的任務(wù)就丟了。后果是,用戶在促銷(xiāo)活動(dòng)中搶到的優(yōu)惠券,沒(méi)有發(fā)給用戶。更嚴(yán)重的后果是,一個(gè)訂單在送往倉(cāng)庫(kù)系統(tǒng)的途中消失了。
我推薦的做法是把任務(wù)投遞到消息中間件,讓它分發(fā)給消息消費(fèi)者來(lái)執(zhí)行(消費(fèi)者可能是發(fā)送者自身)。
- 消息中間件可以要求消費(fèi)者在完成任務(wù)后通知中間件,否則就重新分發(fā)消息,直到收到任務(wù)已完成的通知。
- 如果中間件沒(méi)這種功能,可以讓?xiě)?yīng)用要求消費(fèi)者在完成任務(wù)后回發(fā)一個(gè)"任務(wù)已完成"的消息,但應(yīng)用不能同步等待這一消息,否則異步就退化為同步了。
更重要的是,消息中間件有持久化功能,即使宕機(jī)也不丟消息,而且可以長(zhǎng)期不升級(jí)、不重啟。消息中間件的缺點(diǎn)是,對(duì)失敗情況的處理難以定制化——你可能想定制重試間隔、重試次數(shù)等細(xì)節(jié)。
換個(gè)角度來(lái)看,要解決丟任務(wù)的問(wèn)題,你不一定要用消息中間件。你可以在應(yīng)用代碼中把任務(wù)和完成狀態(tài)保存到數(shù)據(jù)庫(kù)中,用線程池執(zhí)行,在完成后更新?tīng)顟B(tài)。這是不是很像作業(yè)調(diào)度(例如Quartz)呢?是的。
然而,有些任務(wù)確實(shí)是可有可無(wú)的,例如『刷新一個(gè)不重要的緩存』的任務(wù),那么就隨便丟到線程池吧。
2. 日志采用同步模式
我們知道,性能瓶頸通常都是I/O,尤其是數(shù)據(jù)庫(kù)的I/O。因此我們用了緩存,速度蹬的一下竄上來(lái)了——不一定哦。
用緩存把I/O變成了內(nèi)存計(jì)算,***瓶頸消除,速度上升一個(gè)數(shù)量級(jí)。在這個(gè)數(shù)量級(jí),一些原本不重要的因素開(kāi)始產(chǎn)生影響了。
日志是一種I/O啊,雖然順序?qū)懘疟P(pán)很快,但還是比內(nèi)存計(jì)算要慢啊。更糟的是,一個(gè)線程寫(xiě)日志時(shí),另一個(gè)線程必須等它寫(xiě)完才能接著寫(xiě),否則日志會(huì)亂,當(dāng)日志量較大時(shí),就stop the world了。
所以當(dāng)你的系統(tǒng)性能高到一定程度,就要對(duì)日志做性能優(yōu)化了(有過(guò)提高3倍QPS的案例),兩個(gè)常見(jiàn)辦法:
- 少打日志
- 異步模式
今天少打日志,明天排查bug就想哭。所以主要靠異步模式。
Logback可以通過(guò)配置(網(wǎng)上搜一搜),在RollingFileAppender上套一個(gè)AsyncAppender,實(shí)際上就是加了個(gè)緩沖隊(duì)列,變成了批量寫(xiě)磁盤(pán)。缺點(diǎn)是無(wú)法看到***日志(還沒(méi)寫(xiě)磁盤(pán))。queueSize默認(rèn)是256,即使設(shè)為16,也有明顯的性能提升,還緩和了不能及時(shí)看到***日志的問(wèn)題。
Log4j 2的異步模式有更深入的優(yōu)化,是否選用,以測(cè)試數(shù)據(jù)為準(zhǔn)。
3. 沒(méi)有超時(shí)設(shè)置
網(wǎng)絡(luò)忘記設(shè)超時(shí),系統(tǒng)隨時(shí)可能掛。
每一個(gè)網(wǎng)絡(luò)操作,都記得設(shè)置超時(shí)時(shí)間,超過(guò)這個(gè)時(shí)間就放棄。訪問(wèn)分布式緩存也如此。
如果不設(shè)超時(shí),可能會(huì)等到天荒地老。網(wǎng)絡(luò),就是這么不確定。
4. 沒(méi)有統(tǒng)計(jì)緩存***率
一個(gè)***率低下的緩存,不如沒(méi)有。雖然LRU算法很好用,但未必沒(méi)有例外情況。頻繁作廢的數(shù)據(jù)、大體積數(shù)據(jù)都可能是負(fù)擔(dān)。
統(tǒng)計(jì)緩存***率的實(shí)現(xiàn)辦法可以是內(nèi)存里計(jì)數(shù),定期寫(xiě)到數(shù)據(jù)庫(kù)或文件;也可以是把***情況打到日志里,日后匯總統(tǒng)計(jì)。也可能有更精巧的實(shí)現(xiàn)。
當(dāng)你的系統(tǒng)進(jìn)入精耕細(xì)作時(shí)代,這個(gè)問(wèn)題必然擺上案頭。
5. 沒(méi)有統(tǒng)計(jì)緩存響應(yīng)時(shí)間
緩存一定快嗎?我真的見(jiàn)過(guò)不快的。分布式緩存要經(jīng)由網(wǎng)絡(luò),網(wǎng)絡(luò)抖一抖,緩存抖三抖;還依賴(lài)運(yùn)維,運(yùn)維抖一抖,緩存抖三抖。此事之微妙,不可不察也。
留個(gè)心,設(shè)個(gè)超時(shí),記個(gè)響應(yīng)時(shí)間。一個(gè)簡(jiǎn)單辦法是,當(dāng)響應(yīng)時(shí)間過(guò)長(zhǎng)時(shí),打一行日志,正常情況不打日志。這樣既留了記錄,又不產(chǎn)生過(guò)多日志。
6. 單一的緩存模式
分布式緩存是由緩存中間件組成的集群,一致性較好,緩存的很快會(huì)同步到所有副本。但是畢竟要經(jīng)由網(wǎng)絡(luò),延遲為亞毫秒級(jí),而且負(fù)載聚集到中間件,可能因網(wǎng)絡(luò)擁塞或機(jī)器負(fù)載高而出現(xiàn)性能波動(dòng)。
為了進(jìn)一步提高部分業(yè)務(wù)的性能和穩(wěn)定性,可能要輔以本地緩存。
- 緩存要有過(guò)期策略,如果使用了Guava Cache,可以選擇expireAfterAccess(不常用)、expireAfterWrite或refreshAfterWrite策略:
expire是先丟棄舊數(shù)據(jù),再?gòu)臄?shù)據(jù)庫(kù)加載新數(shù)據(jù),期間讓數(shù)據(jù)庫(kù)承擔(dān)壓力,適用于一般情況。
refresh是在異步加載新數(shù)據(jù)完成前,一直保留舊數(shù)據(jù),能始終為數(shù)據(jù)庫(kù)擋住壓力,適用于高壓情況。 - 各個(gè)應(yīng)用實(shí)例的本地緩存是獨(dú)立的,舊數(shù)據(jù)的作廢依賴(lài)于過(guò)期策略。作為改進(jìn),可以利用消息隊(duì)列,一個(gè)實(shí)例廣播消息說(shuō)某數(shù)據(jù)作廢了,其他實(shí)例紛紛自檢。這是準(zhǔn)實(shí)時(shí)同步。
緩存的更新方式也影響著性能,@左耳朵耗子 的緩存更新的套路很好地介紹了三種套路,現(xiàn)在我來(lái)對(duì)比一下:
- Cache Aside Pattern: 應(yīng)用程序在數(shù)據(jù)庫(kù)和緩存之間周旋,以數(shù)據(jù)庫(kù)為準(zhǔn)。適合一般情況,因此最常用。
- Read/Write Through Pattern: 應(yīng)用只讀寫(xiě)緩存,緩存同步寫(xiě)回?cái)?shù)據(jù)庫(kù)(同步是指應(yīng)用等待著寫(xiě)回完成)。理論性能略高一些。
- Write Behind Caching Pattern: 應(yīng)用只讀寫(xiě)緩存,緩存異步寫(xiě)回?cái)?shù)據(jù)庫(kù)(應(yīng)用不等待寫(xiě)回完成,緩存若宕機(jī)將丟數(shù)據(jù))。理論性能***,如果有Write Ahead Logging特性,可避免丟數(shù)據(jù),但略為降低性能。
7. 分布式緩存加鎖
有的系統(tǒng)步入精耕細(xì)作時(shí)代后,想避免一種情況——緩存作廢時(shí),很多應(yīng)用實(shí)例同時(shí)訪問(wèn)數(shù)據(jù)庫(kù),加重負(fù)載,而且浪費(fèi)資源。于是有了給緩存加鎖的方案。
簡(jiǎn)單版是每個(gè)實(shí)例內(nèi)部設(shè)鎖,某條數(shù)據(jù)只許一個(gè)線程去數(shù)據(jù)庫(kù)取。復(fù)雜版是把鎖設(shè)在分布式緩存中,某條數(shù)據(jù)只許一個(gè)實(shí)例去數(shù)據(jù)庫(kù)取,然后放入緩存讓其他實(shí)例用。
復(fù)雜版的想法是好的,但注意,鎖要設(shè)置超時(shí)(還記得我上文說(shuō)的嗎),否則萬(wàn)一持有鎖的實(shí)例發(fā)生問(wèn)題,就全體耽誤了。即使設(shè)了超時(shí),也可能全體實(shí)例一直等待超時(shí),浪費(fèi)時(shí)間。所以這種方案不一定好,需以測(cè)試數(shù)據(jù)為準(zhǔn)。
8. 疲于奔命
很多公司經(jīng)常加班,實(shí)際上效率低下、也不持久,只能復(fù)制既有經(jīng)驗(yàn),靠不停換人來(lái)維持,其后果就是:需求混亂、bug巨多、創(chuàng)新乏力。
要把技術(shù)搞好,需要有條不紊,遇變不亂,持久輸出。疲于奔命的模式,做不好大型服務(wù)端開(kāi)發(fā),也難以做好各種領(lǐng)域的開(kāi)發(fā)。