緩衝機制是對數據持久化的延遲,減少不必要的IO,提高數據落盤的效率。本文將會詳細探討擁有雙Buffer的緩衝池(下文統稱TwinsBufferPool)是如何實現的,讀者可以依此推廣,得到N-Buffer的實現原理。

在此篇文章中,緩衝區(Buffer)和緩衝池(BufferPool)是兩個重要的概念,很明顯,兩者構成了一個包含與被包含的關係,一個緩衝池內可以有一個或者多個緩衝區協同工作,緩衝池中的所有緩衝區被組織成了一個環形隊列,一前一後的兩個緩衝區可以互相替換角色。

當然,在整個過程中,還會有其他輔助工具的出現,在下文都會逐一闡述。

一、設計要點

1、可擴展性。毫無疑問,可擴展性是對一個設計良好的軟體的一項基本要求,而一個軟體的可擴展的地方通常是有很多處的,這在某種程度上會依賴於編程者的經驗,如果僅僅侷限於產品需求,可能會嚴重限制了軟體的可擴展性。緩衝池是一種相對通用的中間件,擴展點相對比較多,比如:緩衝區數量可指定,線程安全與否,緩衝區閾值調配等等。

2、易用性。設計出來的中間件應該是對用戶友好的,使用過程中不會有繁瑣的配置,奇形怪狀的API,更不能有諸多不必要的Dependencies,如果能做到代碼無侵入性,那就非常完美了。基於這個要求,TwinsBufferPool做成了一個Spring Boot Starter的形式,加入到項目裏的dependencies中即可開啟使用。

3、穩定性。這就是衡量一個中間件好壞的重要KPI之一,從外觀上看,同樣是一艘船,破了一個洞和完好無缺將會是一個致命的區別,用戶期望自己搭上了一艘完整的船,以便能航行萬裏而無憂。

4、高效性。說到穩定性,那就不得不說高效了,如果能幫助用戶又好又快的解決問題,無疑是最完美的結果。關於TwinsBufferPool的穩定性和高效性兩個指標,會在文中附上jemeter的壓測結果,並加以說明。

二、設計方案

這一小節將會給出TwinsBufferPool完整的設計方案,我們先從配置說起。

每個參數都會提供默認值,所以不做任何配置也是允許的。如下是目前TwinsBufferPool能提供的配置參數(yml):

buffer:
capacity: 2000
threshold: 0.5
allow-duplicate: true
pool:
enable-temporary-storage: true
buffer-time-in-seconds: 120

下面附上參數說明表:

TwinsBufferPool參數表

以上參數比較淺顯易懂,這裡重點解釋enable-temporary-storage和buffer-time-in-seconds這兩個參數。

根據參數說明,很明顯可以感受到,這兩個參數是為了預防突發情況,導致數據丟失。因為緩衝區都是基於內存的設計的,這就意味著緩衝的數據隨時處於一種服務重啟,或者服務宕機的高風險環境中,因此,才會有這兩個參數的誕生。

因為TwinsBufferPool良好的介面設計,對於以上兩個參數的實現機制也是高度可擴展的。TwinsBufferPool默認的是基於Redis的實現,用戶也可以用MongoDB,MySQL,FileSystem等方式實現。由此又會衍生出另外一個問題,由於各種異常情況,導致臨時存儲層遺留了一定量的數據,需要在下次啟動的時候,恢復這一部分的數據。

總而言之,數據都是通過flush動作最終持久化到磁碟上。

緩衝池主要結構

因為大多數實際業務場景對於緩衝池的並發量是有一定要求的,所以默認就採用了線程安全的實現策略,受到JDK中ThreadPool的啟發,緩衝池也具備了自身狀態管理的機制。如下列出了緩衝池所有可能存在的狀態,以及各個狀態的流轉。

/**
* 緩衝池暫未就緒
*/
private static final int ST_NOT_READY = 1;

/**
* 緩衝池初始化完畢,處於啟動狀態
*/
private static final int ST_STARTED = 2;

/**
* 如果安全關閉緩衝池,會立即進入此狀態
*/
private static final int ST_SHUTTING_DOWN = 3;

/**
* 緩衝池已關閉
*/
private static final int ST_SHUTDOWN = 4;

/**
* 正在進行數據恢復
*/
private static final int ST_RECOVERING = 5;

緩衝池狀態機

通過上述的一番分析,設計的方案也呼之欲出了,下面給出主要的介面設計與實現。

BufferPool介面定義

通過以上的講解,也不難理解BufferPool定義的介面。緩衝池的整個生命週期,以及內部的一些運作機制都得以體現。值得注意的是,在設計上,將緩衝池和存儲層做了邏輯分離,使得擴展性進一步得到增強。

存儲相關的介面包含了一些簡單的CURD,目前默認是用Redis作為臨時存儲層,MongoDB作為永久存儲層,用戶可以根據需要實現其他的存儲方式。

下圖展現的是TwinsBufferPool的實現方式,DataBuffer是緩衝區,必須依賴的基礎元素。因為設計的是環形隊列,所以依賴了CycleQueue,這個環形隊列的interface也是自定義的,在JDK中沒有找到比較合適的實現。

BufferPool介面實現

值得注意的是,BufferPool介面定義是靈活可擴展的,TwinsBufferPool只是提供了一種基於環形隊列的實現方式,用戶也可以自行設計,使用另外一種數據結構來支撐緩衝池的運作。

三、壓測報告

使用的是個人的PC電腦,機器的配置如下:

處理器:i5-7400 CPU 3.00GHZ 四核

內存:8.00GB

操作系統:Windows10 64位 基於x64的處理器

運行環境如下:

jdk 1.8.0_144

SpringBoot_2.1.0,內置Tomcat9.0

Redis_v4.0.1

MongoDB_v3.4.7

測試工具:

jemeter_v5.1

總共測試了四組參數,每組參數主要是針對最大容量,閾值和最大緩衝時間三個參數來做調整。

第一組:

buffer:
capacity: 1000
threshold: 0.8
pool:
buffer-time-in-seconds: 60

第二組:

buffer:
capacity: 5000
threshold: 0.8
pool:
buffer-time-in-seconds: 60

第三組:

buffer:
capacity: 5000
threshold: 0.8
pool:
buffer-time-in-seconds: 300

第四組:

buffer:
capacity: 10000
threshold: 0.8
pool:
buffer-time-in-seconds: 300

總共採集了9個指標:CPU佔用率,堆內存/M,線程數,錯誤率,吞吐量/sec,最長響應時間/ms,最短響應時間/ms,平均響應時間/ms,數據丟失量。

限於篇幅,只展示4個指標:堆內存,數據丟失量,平均響應時間,吞吐量。

堆內存/mb
數據丟失量

平均響應時間/ms
吞吐量/sec

總體來看,隨著每秒並發量的增加,各項指標呈現了不太樂觀的趨勢,其中最不穩定的是第四組參數,波動較為明顯,綜合表現最佳的是第二組參數,其次是第三組。

數據丟失量是一個比較讓人關心的指標,從圖中可以得知,在並發量達到4000的時候,開始有數據丟失的現象,而造成這一現象的原因並非是TwinsBufferPool實現代碼的Bug,而是請求超時導致的「Connection refused」,因為每個Servlet運行容器都會有超時機制,如果排隊請求時間過長,就是直接被拒絕了。因此,看數據丟失量和錯誤率曲線,這兩者是一致的。如果設置成不超時,那麼將是零丟失量,零錯誤率,所帶來的代價就是平均響應時間會拉長。

因為受限於個人的測試環境,整個測試過程顯得不是很嚴謹,得出來的數據也並不是很完美,不過,我這裡提供了一些優化調整的建議:

1、硬體環境。正所謂「巧婦難為無米之炊」,如果提供的硬體性能本身就是有限的話,那麼,在上面運行的軟體也難以得到正常的發揮。

2、軟體架構。這個想像的空間很大,其中有一種方案我認為未來可以納入到RoadMap中:多緩衝池的負載均衡。我們可以嘗試在一個應用中啟用多個緩衝池,通過調度演算法,將緩衝數據均勻的分配給各個緩衝池,不至於出現只有一個緩衝池「疲於奔命」的狀況,最起碼系統的吞吐量會有所提升。

3、其他中間件或者工具的輔助,比如加上消息中間件可以起到削峯的作用,各項指標也將會有所改善。

4、參數調優。這裡的參數指代的不僅僅是緩衝池的參數,還有包括最大連接數,最大線程數,超時時間等諸多外部參數。

四、總結

本文詳細闡述了雙Buffer緩衝池的設計原理,以及實現方式,並對TwinsBufferPool實施了壓測,也對測試結果進行了一番分析。

歡迎關注我的微信訂閱號:技術匯

如果想查看完整的測試報告,可在訂閱號內回復關鍵詞:測試報告,即可獲取到下載鏈接。

如果想深入研究TwinsBufferPool源碼的讀者,可在訂閱號內回復關鍵詞:緩衝池


推薦閱讀:
相關文章