此次天池中間件性能大賽初賽和複賽的成績都正好是第五名
,本次整理了複賽《單機百萬消息隊列的存儲設計》的思路方案分享給你們,實現方案上也是決賽隊伍中相對比較特別的。linux
單機可支持100萬隊列以上
。數據發送、索引校檢、數據消費
三個階段評測。對於單機幾百的大隊列來講業務已有成熟的方案,Kafka和RocketMQ。算法
方案 | 幾百個大隊列 |
---|---|
Kafka | 每一個隊列一個文件(獨立存儲) |
RocketMQ | 全部隊列共用一個文件(混合存儲) |
若直接採用現有的方案,在百萬量級的小隊列場景都有極大的弊端。數組
方案 | 百萬隊列場景弊端 |
---|---|
Kafka獨立存儲 | 單個小隊列數據量少,批量化程度徹底取決於內存大小,落盤時間長,寫數據容易觸發IOPS瓶頸 |
RocketMQ混合存儲 | 隨機讀嚴重,一個塊中連續數據很低,讀速度很慢,消費速度徹底受限於IOPS |
爲了兼顧讀寫速度,咱們最終採用了折中的設計方案:多個隊列merge,共享一個塊存儲。
緩存
設計上要支持邊寫邊讀
多個隊列須要合併處理
單個隊列的數據存儲部分連續
索引稀疏,儘量常駐內存
架構圖中Bucket Manager和Group Manager分別對百萬隊列進行分桶以及合併管理,而後左右兩邊是分別是寫模塊和讀模塊,數據寫入包括隊列merge處理,消息塊落盤。讀模塊包括索引管理和讀緩存。(見左圖)bash
bucket、group、queue的關係:對消息隊列進行bucket處理,每一個bucket包含多個group,group是咱們進行隊列merge的最小單元,每一個group管理固定數量的隊列。(見右圖)數據結構
接下來對整個存儲每一個階段的細節進行展開分析,包括隊列合併、索引管理和數據落盤。架構
當Group達到M個後便造成一個固定分組。相同隊列會在Group內進行合併,新的隊列數據將繼續分配Group接收。
當Block達到16k(可配置)時以隊列爲單位進行數據排序,保證單個隊列數據連續。
L2二級索引與數據存儲的位置息息相關,見下圖。爲每一個排序後的Block塊創建一個L2索引,L2索引的結構分爲文件偏移(file offset),數據壓縮大小(size),原始大小(raw size),由於咱們是多個隊列merge,而後接下來是每一個隊列相對於起始位置的delta offset以及消息數量。
併發
爲了加快查詢速度,在L2基礎上創建L1一級索引,每16個L2創建一個L1,L1按照時間前後順序存放。
L1和L2的組織關係以下:異步
L1索引的結構很是簡單,file id對應消息存儲的文件id,以及16個Block塊中每一個隊列消息的起始序列號seq num。例如MQ1從序列號1000開始,MQ2從序列號2000開始等等。高併發
如何根據索引定位須要查找的數據?
對L1先進行二分查找,定位到上下界範圍,而後對範圍內的全部L2進行順序遍歷。
當blcok超過指定大小後,根據桶的hashcode再進行一次mask操做將group中的隊列數據同步寫入到m個文件中。
同步刷盤主要嘗試了兩種方案:Nio和Dio。Dio大約性能比Nio提高約5%
。CPP使用DIO是很是方便的,然而做爲Java Coder你也許是第一次據說DIO,在Java中並無提供直接使用DIO的接口,能夠經過JNA的方式調用。
DIO(DIRECT IO,直接IO),出於對系統cache和調度策略的不滿,用戶本身在應用層定製本身的文件讀寫。DIO最大的優勢就是可以減小OS內核緩衝區和應用程序地址空間的數據拷貝次數,下降文件讀寫時的CPU開銷以及內存的佔用。然而DIO的缺陷也很明顯,DIO在數據讀取時會形成磁盤大量的IO,它並無緩衝IO從PageCache獲取數據的優點。
這裏就遇到一個問題,一樣配置的阿里雲機器測試隨機數據同步寫入性能是很是高的,可是線上的評測數據都是58字節,數據過於規整致使同一時間落盤的機率很大,出現了大量的鎖競爭。因此這裏作了一個小的改進:按機率隨機4K、8K、16K進行落盤,寫性能雖有必定提高,可是效果也是不太理想,因而採用了第二種思路異步刷盤。
採用RingBuffer接收block塊,使用AIO對多個block塊進行Batch刷盤,減小IO Copy的次數。異步刷盤寫性能有了顯著的提高。
如下是異步Flush的核心代碼:
while (gWriterThread) {
if (taskQueue->pop(task)) {
writer->mWriting.store(true);
do {
// 使用異步IO
aiocb *pAiocb = aiocb_list[aio_size++];
memset(pAiocb, 0, sizeof(aiocb));
pAiocb->aio_lio_opcode = LIO_WRITE;
pAiocb->aio_buf = task.mWriteCache.mCache;
pAiocb->aio_nbytes = task.mWriteCache.mSize;
pAiocb->aio_offset = task.mWriteCache.mStartOffset;
pAiocb->aio_fildes = task.mBlockFile->mFd;
pAiocb->aio_sigevent.sigev_value.sival_ptr = task.mBlockFile;
task.mBlockFile->mWriting = true;
if (aio_size >= MAX_AIO_TASK_COUNT) {
break;
}
} while (taskQueue->pop(task));
if (aio_size > 0) {
if (0 != lio_listio(LIO_WAIT, aiocb_list, aio_size, NULL)) {
aos_fatal_log("aio error %d %s.", errno, strerror(errno));
}
for (int i = 0; i < aio_size; ++i) {
((BlockFile *) aiocb_list[i]->aio_sigevent.sigev_value.sival_ptr)->mWriting = false;
free((void *) aiocb_list[i]->aio_buf);
}
aio_size = 0;
}
} else {
++waitCount;
sched_yield();
if (waitCount > 100000) {
usleep(10000);
}
}
}
複製代碼
整個流程主要有兩個優化點:預讀取和讀緩存。
主要有兩個做用:
順序消費且已經消費到當前block尾,則進行預讀取操做。如何判斷順序消費?判斷上次消費的結束位置是否與此次消費的起始位置相等。
if (msgCount >= destCount) {
if (mLastGetSequeneNum == offsetCount &&
beginIndex + 1 < mL2IndexCount &&
beginOffsetCount + blockIndex.mMsgDeltaIndexCount <= offsetCount + msgCount + msgCount) {
MessageBlockIndex &nextIndex = mL2IndexArray[beginIndex + 1];
// 預讀取
#ifdef __linux__
readahead(pManager->GetFd(hash), nextIndex.mFileOffset, PER_BLOCK_SIZE);
#endif
}
mLastGetSequeneNum = offsetCount + msgCount;
return msgCount;
}
複製代碼
關於read cache作了一些精巧的小設計,保證足夠簡單高效。
分桶
(部分隔離),必定程度緩解緩存餓死現象。數組 + 自旋鎖 + 原子變量
實現了一個循環分配緩存塊的方案。雙向指針綁定
高效定位緩存節點。Read Cache一共分爲N=64(可配)個Bucket,每一個Bucket中包含M=3200(可配)個緩存塊,大概總計20w左右的緩存塊,每一個是4k,大約佔用800M的內存空間。
關於緩存的核心數據結構,咱們並無從隊列的角度出發,而是針對L2索引和緩存塊進行了綁定,這裏設計了一個雙向指針。判斷緩存是否有效的核心思路:check雙向指針是否相等。
CacheItem cachedItem = (CacheItem *) index->mCache;
cachedItem->mIndexPtr == (void *) index;
複製代碼
3.1 Bucket分桶
3.2 Alloc Cache Block
count.fetch_add(1) % M = index
3.3 Cache Hit
3.4 Cache Page Replace
count.fetch_add(1) % M = M-1
,找到新的緩存塊進行從新綁定。說明:整個分配的邏輯是一個循環使用的過程,當全部的緩存桶都被使用,那麼會從數組首地址開始從新分配、替換。
開始咱們嘗試了兩種讀緩存方案:最簡單的LRU緩存和直接使用PageCache讀取。PageCache所實現的實際上是高級版的LRU緩存。在順序讀的場景下,咱們本身實現的讀緩存(Cycle Cache Allocate,暫簡稱爲CCA)與LRU、PageCache的優劣分析對好比下:
多隊列Merge,保證隊列局部連續的存儲方式。
資源利用率低
,約300~400MB索引便可支撐百萬隊列、100GB數據量高併發讀寫。轉載請註明出處,歡迎關注個人公衆號:亞普的技術輪子