在高並行的場景下,非同步是一個極其重要的優化方向。
前段時間,生產環境發生一次事故,筆者認為事故的場景非常具備典型性 。
寫這篇文章,筆者想和大家深入探討該場景的架構優化方案。希望大家讀完之後,可以對非同步有更深刻的理解。
老師登入教研平臺,會看到課程列表,點選課程後,課程會以視訊的形式展現出來。
存取課程詳情頁面,包含兩個核心動作:
讀取課程視訊資訊 :
從快取伺服器 Redis 獲取課程的視訊資訊 ,返回給前端,前端通過視訊元件渲染。
寫入課程觀看行為記錄 :
當教師觀看視訊的過程中,瀏覽器每隔3秒發起請求,教研服務將觀看行為記錄插入到資料庫表中。而且隨著使用者線上人數越多,寫操作的頻率也會指數級增長。
上線初期,這種設計執行還算良好,但隨著線上使用者的增多,系統響應越來越慢,大量執行緒阻塞在寫入視訊觀看進度表上的 Dao 方法。上。
首先我們會想到一個非常直觀的方案,提升寫入資料庫的能力。
這種方案其實也可以滿足我們的需求,但是通過擴容硬體並不便宜,另外寫操作可以允許適當延遲和丟失少量資料,那這種方案更顯得價效比不足。
那麼架構優化的方向應該是:「減少寫動作的耗時,提升寫動作的並行度」, 只有這樣才能讓系統更順暢的執行。
於是,我們想到了第二種方案:寫請求非同步化。
2014年,筆者在藝龍旅行網負責紅包系統相關工作。運營系統會呼叫紅包系統給特定使用者傳送紅包,當這些使用者登入 app 後,app 端會呼叫紅包系統的啟用紅包介面 。
啟用紅包介面是一個寫操作,速度也比較快(20毫秒左右),介面的日請求量在2000萬左右。
應用存取高峰期,紅包系統會變得不穩定,啟用介面經常超時,筆者為了快速解決問題,採取了一個非常粗糙的方案:
"控制器收到請求後,將寫操作放入到獨立的執行緒池中後,立即返回給前端,而執行緒池會非同步執行啟用紅包方法"。
坦率的講,這是一個非常有效的方案,優化後,紅包系統非常穩定。
回到教研的場景,見下圖,我們也可以設計類似執行緒池模型的方案:
使用執行緒池模式,需要注意如下幾點:
開源中國統計瀏覽數的方案非常經典。
使用者存取過一次文章、新聞、程式碼詳情頁面,存取次數位段加 1 , 在 oschina 上這個操作是非同步的,存取的時候只是將資料在記憶體中儲存,每隔固定時間將這些資料寫入資料庫。
範例程式碼如下:
我們可以借鑑開源中國的方案 :
控制器接收請求後,觀看進度資訊儲存到本地記憶體 LinkedBlockingQueue 物件裡;
非同步執行緒每隔1分鐘從佇列裡獲取資料 ,組裝成 List 物件,最後呼叫 Jdbc batchUpdate 方法批次寫入資料庫;
批次寫入主要是為了提升系統的整體吞吐量,每次批次寫入的 List 大小也不宜過大 。
這種方案優點是:不改動原有業務架構,簡單易用,效能也高。該方案同樣需要考慮記憶體溢位的風險。
很多同學們會想到 MQ 模式 ,訊息佇列最核心的功能是非同步和解耦,MQ 模式架構清晰,易於擴充套件。
核心流程如下:
這種方案優點是:
不過 MQ 模式需要引入新的元件,增加額外的複雜度。
網際網路大廠還有一種常見的非同步的方案:Agent 服務 + MQ 模式。
教研伺服器上部署 Agent 服務(獨立的程序) , 教研服務接收寫請求後,將請求按照固定的格式(比如 JSON )寫入到本次磁碟中,然後給前端返回成功資訊。
Agent 服務會監聽檔案變動,將檔案內容傳送到訊息佇列 , 消費者服務獲取觀看行為記錄,將其儲存到 MySQL 資料庫中。
還有一種演進,假設我們不想在應用中依賴訊息佇列,不生成本地檔案,可以採用如下的方式:
這種方案最大的優點是:架構分層清晰,業務服務不需要引入 MQ 元件。
筆者原來接觸過的效能監控平臺,或者紀錄檔分析平臺都使用這種模式。
學習需要一層一層遞進的思考。
第一層:什麼場景下需要非同步
第二層:非同步的外功心法
本文提到了四種非同步方式:
執行緒池模式
本地記憶體 + 定時任務
MQ 模式
Agent 服務 + MQ 模式
它們的共同特點是:將寫操作命令儲存在一個池子後,立刻響應給前端,減少寫動作的耗時。任務服務非同步從池子裡獲取任務後執行。
第三層:非同步的本質
在筆者看來,非同步是更細粒度的使用系統資源的一種方式。
在教研課程詳情場景裡,資料庫的資源是固定的,但寫操作佔據大量資料庫資源,導致整個系統的阻塞,但寫操作並不是最核心的業務流程,它不應該佔用那麼多的系統資源。
不能為了非同步而非同步,無論是使用執行緒池,還是本地記憶體 + 定時任務 ,亦或是 MQ ,對資料庫資源的使用都需要在合理的範圍內,否則非同步就達不到我們想要的效果。
如果我的文章對你有所幫助,還請幫忙點贊、在看、轉發一下,你的支援會激勵我輸出更高質量的文章,非常感謝!