摘要:用線程池執行異步任務為了減少阻塞時間,加快響應速度,把無需返回結果的操作變成異步任務,用線程池來執行,這是提高性能的一種手段。疲于奔命的模式,做不好大型服務端開發,也難以做好各種領域的開發。
1. 用線程池執行異步任務
為了減少阻塞時間,加快響應速度,把無需返回結果的操作變成異步任務,用線程池來執行,這是提高性能的一種手段。
你可能要驚訝了,這么做不對嗎?
首先,我們把異步任務分為兩種:
務必成功執行的
不成功就放棄
顯然大多數時候都是第一種。那么當你把任務丟給線程池,你知道它完成了沒有嗎?
如果服務器宕機、升級或重啟,那些尚未完成或還在排隊的任務就丟了。后果是,用戶在促銷活動中搶到的優惠券,沒有發給用戶。更嚴重的后果是,一個訂單在送往倉庫系統的途中消失了。
我推薦的做法是把任務投遞到消息中間件,讓它分發給消息消費者來執行(消費者可能是發送者自身)。
消息中間件可以要求消費者在完成任務后通知中間件,否則就重新分發消息,直到收到任務已完成的通知。
如果中間件沒這種功能,可以讓應用要求消費者在完成任務后回發一個"任務已完成"的消息,但應用不能同步等待這一消息,否則異步就退化為同步了。
更重要的是,消息中間件有持久化功能,即使宕機也不丟消息,而且消息中間件可以長期不升級、不重啟。消息中間件的缺點是,對失敗情況的處理難以定制化——你可能想定制重試間隔、重試次數等細節。
換個角度來看,要解決丟任務的問題,你不一定要用消息中間件。你可以在應用代碼中把任務和完成狀態保存到數據庫中,用線程池執行,在完成后更新狀態。這是不是很像作業調度(例如Quartz)呢?是的。
然而,有些任務確實是可有可無的,例如『刷新一個不重要的緩存』的任務,那么就隨便丟到線程池吧。
2. 日志采用同步模式我們知道,性能瓶頸通常都是I/O,尤其是數據庫的I/O。因此我們用了緩存,速度蹬的一下竄上來了——不一定哦。
用緩存把I/O變成了內存計算,最大瓶頸消除,速度上升一個數量級。在這個數量級,一些原本不重要的因素開始產生影響了。
日志是一種I/O啊,雖然順序寫磁盤很快,但還是比內存計算要慢啊。更糟的是,一個線程寫日志時,另一個線程必須等它寫完才能接著寫,否則日志會亂,當日志量較大時,就stop the world了。
所以當你的系統性能高到一定程度,就要對日志做性能優化了(有過提高3倍QPS的案例),兩個常見辦法:
少打日志
異步模式
今天少打日志,明天排查bug就想哭。所以主要靠異步模式。
Logback可以通過配置(網上搜一搜),在RollingFileAppender上套一個AsyncAppender,實際上就是加了個緩沖隊列,變成了批量寫磁盤。缺點是無法看到最新日志(還沒寫磁盤)。queueSize默認是256,即使設為16,也有明顯的性能提升,還緩和了不能及時看到最新日志的問題。
Log4j 2的異步模式有更深入的優化,是否選用,以測試數據為準。
3. 沒有超時設置網絡忘記設超時,系統隨時可能掛。
每一個網絡操作,都記得設置超時時間,超過這個時間就放棄。訪問分布式緩存也如此。
如果不設超時,可能會等到天荒地老。網絡,就是這么不確定。
4. 沒有統計緩存命中率一個命中率低下的緩存,不如沒有。雖然LRU算法很好用,但未必沒有例外情況。頻繁作廢的數據、大體積數據都可能是負擔。
統計緩存命中率的實現辦法可以是內存里計數,定期寫到數據庫或文件;也可以是把命中情況打到日志里,日后匯總統計。也可能有更精巧的實現。
當你的系統進入精耕細作時代,這個問題必然擺上案頭。
5. 沒有統計緩存響應時間緩存一定快嗎?我真的見過不快的。分布式緩存要經由網絡,網絡抖一抖,緩存抖三抖;還依賴運維,運維抖一抖,緩存抖三抖。此事之微妙,不可不察也。
留個心,設個超時,記個響應時間。一個簡單辦法是,當響應時間過長時,打一行日志,正常情況不打日志。這樣既留了記錄,又不產生過多日志。
6. 單一的緩存模式中央分布式緩存是由緩存中間件組成的集群,一致性較好,緩存的很快會同步到所有副本。但是畢竟要經由網絡,延遲為亞毫秒級,而且負載聚集到中間件,可能因網絡擁塞或機器負載高而出現性能波動。
為了進一步提高部分業務的性能和穩定性,可能要輔以本地緩存。
緩存要有過期策略,如果使用了Guava Cache,可以選擇expireAfterAccess(不常用)、expireAfterWrite或refreshAfterWrite策略:
expire是先丟棄舊數據,再從數據庫加載新數據,期間讓數據庫承擔壓力,適用于一般情況。
refresh是在異步加載新數據完成前,一直保留舊數據,能始終為數據庫擋住壓力,適用于高壓情況。
各個應用實例的本地緩存是獨立的,舊數據的作廢依賴于過期策略。作為改進,可以利用消息隊列,一個實例廣播消息說某數據作廢了,其他實例紛紛自檢。這是準實時同步。
緩存的更新方式也影響著性能,@左耳朵耗子 的緩存更新的套路很好地介紹了三種套路,現在我來對比一下:
Cache Aside Pattern: 應用程序在數據庫和緩存之間周旋,以數據庫為準。適合一般情況,因此最常用。
Read/Write Through Pattern: 應用只讀寫緩存,緩存同步寫回數據庫(同步是指應用等待著寫回完成)。理論性能略高一些。
Write Behind Caching Pattern: 應用只讀寫緩存,緩存異步寫回數據庫(應用不等待寫回完成,緩存若宕機將丟數據)。理論性能最高,如果有Write Ahead Logging(Redis AOF或Apache Ignite都可以)特性,可避免丟數據,但略為降低性能。
7. 分布式緩存加鎖有的系統步入精耕細作時代后,想避免一種情況——緩存作廢時,很多應用實例同時訪問數據庫,加重負載,而且浪費資源。于是有了給緩存加鎖的方案。
簡單版是每個實例內部設鎖,某條數據只許一個線程去數據庫取。復雜版是把鎖設在分布式緩存中,某條數據只許一個實例去數據庫取,然后放入緩存讓其他實例用。
復雜版的想法是好的,但注意,鎖要設置超時(還記得我上文說的嗎),否則萬一持有鎖的實例發生問題,就全體耽誤了。即使設了超時,也可能全體實例一直等待超時,浪費時間。所以這種方案不一定好,需以測試數據為準。
8. 疲于奔命很多公司經常加班,實際上效率低下、也不持久,只能復制既有經驗,靠不停換人來維持,其后果就是:需求混亂、bug巨多、創新乏力。
要把技術搞好,需要有條不紊,遇變不亂,持久輸出。疲于奔命的模式,做不好大型服務端開發,也難以做好各種領域的開發。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/11742.html
摘要:用線程池執行異步任務為了減少阻塞時間,加快響應速度,把無需返回結果的操作變成異步任務,用線程池來執行,這是提高性能的一種手段。疲于奔命的模式,做不好大型服務端開發,也難以做好各種領域的開發。 1. 用線程池執行異步任務 為了減少阻塞時間,加快響應速度,把無需返回結果的操作變成異步任務,用線程池來執行,這是提高性能的一種手段。 你可能要驚訝了,這么做不對嗎? 首先,我們把異步任務分...
摘要:關于緩存熱點重建原文說到在緩存失效的瞬間,有大量線程來重建緩存,造成后端負載加大,甚至可能會讓應用崩潰,并給出互斥鎖和永遠不過期兩種候選方案。即使繞過互斥鎖,也不會產生什么不好的后果,因為更新緩存是一個冪等操作。 向大家推薦這篇文章——Redis架構之防雪崩設計:網站不宕機背后的兵法 (另外推薦我去年的短文作為餐前點心——略談服務端緩存設計) 《Redis架構之防雪崩設計》這篇文章(下...
摘要:我們整個監控的部分,沒有采用社區流行的,而是自己實現了一套。但是對于前端來說,只暴露一個入口,引入一個反代即可。簡介是一個為了讓部署微服務更加便捷而誕生的現代反向代理負載均衡工具。配置熱更新,支持多種后端。將請求轉發到統一認證服務。 前言 對于監控這塊,我們基于prometheus實現,當然做了大量的優化,包括前面所講到的配置接口化。我們整個監控的UI部分,沒有采用社區流行的grafa...
摘要:我們整個監控的部分,沒有采用社區流行的,而是自己實現了一套。但是對于前端來說,只暴露一個入口,引入一個反代即可。簡介是一個為了讓部署微服務更加便捷而誕生的現代反向代理負載均衡工具。配置熱更新,支持多種后端。將請求轉發到統一認證服務。 前言 對于監控這塊,我們基于prometheus實現,當然做了大量的優化,包括前面所講到的配置接口化。我們整個監控的UI部分,沒有采用社區流行的grafa...
閱讀 3537·2021-09-10 10:51
閱讀 2507·2021-09-07 10:26
閱讀 2482·2021-09-03 10:41
閱讀 810·2019-08-30 15:56
閱讀 2896·2019-08-30 14:16
閱讀 3488·2019-08-30 13:53
閱讀 2103·2019-08-26 13:48
閱讀 1912·2019-08-26 13:37