時序資料庫QuestDB是如何實現每秒140萬行的寫入速度?

banq發表於2021-05-15

QuestDB是一個快速開源時間序列資料庫,QuestDB是一個用於時間序列,事件和分析工作負載的開源資料庫,主要關注效能(https://github.com/questdb/questdb)。
 

誕生之路
它始於2012年,當時一家能源貿易公司僱用我來重建他們的實時船隻跟蹤系統。管理層希望我使用一個他們剛購買許可證的著名XML資料庫。僅此選項就需要將生產中斷大約一個星期,以獲取資料。一週的停機時間是不可行的。由於沒有更多的錢可以花在軟體上,我轉向了OpenTSDB之類的替代產品,但它們不適合我們的資料模型。目前還沒有解決方案可以交付該專案。
然後,我偶然發現了Peter Lawrey的Java Chronicle庫。它使用記憶體對映檔案在2分鐘內而不是一週內載入了相同的資料。除了效能方面,我還很著迷於這樣一個簡單的方法可以同時解決多個問題:即使在將資料提交到磁碟之前,程式碼與記憶體(而不是IO功能)互動,沒有複製緩衝區的情況下,也可以進行快速寫入,讀取。順便說一句,這是我第一次接觸零GC Java。
但是有幾個問題:

  • 首先,當時看來該庫似乎無法維護。
  • 其次,它使用Java NIO而不是直接使用OS API。這增加了開銷,因為它建立單個物件的唯一目的是為每個記憶體頁面保留一個記憶體地址。
  • 第三,儘管NIO分配API有充分的文件記錄,但發行API卻沒有。

因為很容易用完記憶體,而很難管理記憶體頁面的釋放。我決定放棄XML DB,然後開始用Java編寫自定義儲存引擎,類似於Java Chronicle所做的。
該引擎使用了記憶體對映檔案,堆外記憶體以及用於地理空間時間序列的自定義查詢系統。實施它是一種令人耳目一新的體驗。在工作上,我花了幾個星期而不是幾年。
我離開了工作,開始獨自從事QuestDB的工作。我使用Java和小的C層直接與OS API進行互動,而無需透過選擇器API。儘管現有的OS API包裝器可能更容易上手,但開銷卻增加了複雜性並損害了效能。我還希望該系統完全不使用GC。為此,我必須自己構建堆外記憶體管理,並且無法使用現成的庫。這些年來,我不得不重寫許多標準的標準,以避免產生任何垃圾。
一年後,我意識到我的最初設計確實有缺陷,必須將其丟棄。它沒有讀取和寫入分開的概念,因此會發生髒讀。無法保證儲存是連續的,並且頁面可以具有各種不可64位整除的大小。它也非常不適合快取,迫使使用慢行讀取而不是快速列讀取和向量化讀取。提交速度很慢,而且由於單獨的列檔案可以獨立提交,因此資料容易遭到破壞。
我重新開始工作:我編寫了新引擎,以允許原子和持久的多列提交,提供可重複的讀取隔離,並使提交是瞬時的。為此,我將事務檔案與資料檔案分開。這使得可以同時提交多個列,作為對最後提交的行ID的簡單更新。我還透過刪除重疊的記憶體頁面並在頁面邊緣逐位元組寫入資料來提高儲存密度。
這種新方法提高了查詢效能。透過預取,可以輕鬆地在工作執行緒之間拆分資料並最佳化CPU管道。藉助Agner Fog的向量類庫,它使用SIMD指令集[2]解鎖了基於列的執行和附加的虛擬並行性。它使實施更多新的創新成為可能,例如我們自己的Google SwissTable版本。幾周前,我們在ShowHN [5]上釋出了演示伺服器時,我釋出了更多詳細資訊。該演示仍可透過線上預載入的16億行資料集進行線上嘗試。
QuestDB已部署到生產環境中,包括一家大型金融科技公司。我們一直致力於建立一個社群來吸引我們的第一批使用者並收集儘可能多的反饋。詳細點選駭客新聞
 

每秒140萬行的寫入
在專案的早期階段,我們受益於基於向量的僅僅附加系統,因為該模型具有速度優勢和簡單的程式碼路徑的優點。我們還要求行時間戳以升序儲存,從而導致快速的時間序列查詢而沒有昂貴的索引。
我們發現該模型並不適合所有資料採集用例,例如亂序資料。儘管有幾種解決方法,但我們希望在不損失我們多年構建的效能的情況下提供此功能。
我們研究了現有的方法,大多數都是以我們不滿意的效能成本為代價的。像我們的整個程式碼庫一樣,我們今天提供的解決方案是從頭開始構建的。花費了9個月的時間才能實現,並向該專案新增了65k行程式碼。
這是我們重新構建的原因,在此過程中學到的知識以及將QuestDB與InfluxDB,ClickHouse和TimescaleDB進行比較的基準。
我們的資料模型有一個致命的缺陷-如果記錄與現有資料相比在時間戳上出現亂序,則記錄將被丟棄。在實際的應用程式中,由於網路抖動,延遲或時鐘同步問題,有效載荷資料不會像這樣執行。
有可能的解決方法,例如每個資料來源使用一個表或定期對錶進行排序,但是對於大多數使用者而言,這是不方便且不可持續的。
時間序列資料庫中常用的LSM樹或B樹。新增樹將帶來以下優勢:能夠即時排序資料,而無需從頭開始發明替代儲存模型。但是,這種方法最讓我們困擾的是,與將資料儲存在陣列中相比,每個後續讀取操作都將面臨效能損失。我們還將透過具有用於有序資料的儲存模型和用於O3資料的儲存模型來引入複雜性。
更有希望的選擇是在資料到達時引入排序和合並階段。這樣,我們可以保持儲存模型不變,同時動態合併資料,而有序向量作為輸出落在磁碟上。
 

解決方案
我們關於如何處理O3 ingestion的想法是新增三階段的方法:

  • 保持追加模型,直到記錄無序到達
  • 在暫存區的記憶體中對未提交的記錄進行排序
  • 在提交時對已排序的O3資料和持久資料進行協調和合並

前兩個步驟簡單明瞭且易於實現,處理僅追加資料的操作保持不變。僅當登臺區域中有資料時,才會執行繁重的O3提交。這種設計的好處是輸出是向量,這意味著我們基於向量的讀取仍然相容。
這種預先提交的排序和合並功能為攝取增加了一個額外的處理階段,並伴有效能損失。儘管如此,我們還是決定探索這種方法,看看透過最佳化O3提交可以減少多大的損失。
為了增加對O3的支援,我們尋求了一種新穎的解決方案,該解決方案與諸如B樹或基於LSM的攝取框架之類的傳統方法相比,具有出乎意料的良好效能。
  • 快速複製資料

第二步的批次處理暫存區為我們提供了一個整體分析資料的獨特機會。這種分析的目的是在可能的情況下完全避免物理合併,並可能避免使用快速、direct memcpy或類似的資料移動方法。我們透過最佳化版本的基數排序對暫存區域中的timestamp列進行排序,然後使用所得的索引並行地重新排序暫存區域中的其餘列。
  • 推遲提交

雖然能夠快速複製資料是一個不錯的選擇,但我們認為在大多數時間序列提取方案中都可以避免繁重的資料複製。假設大多數實時亂序情況是由傳遞機制和硬體抖動引起的,我們可以推斷出時間戳分佈將區域性地包含在某個邊界中。
例如,如果任何新的時間戳值很可能落在先前接收到的值的10秒以內,則邊界為10秒,我們將此邊界稱為O3滯後。當時間戳值遵循此模式時,推遲提交可以有效地將O3呈現為追加操作。我們並不是將QuestDB的O3設計為僅處理 O3滯後模式,但是如果您的資料符合該模式,則會為熱路徑識別並確定優先順序。
 

TSBS基準
我們看到時間序列基準套件 (TSBS)經常出現在有關資料庫效能的討論中,並決定我們應該提供對QuestDB和其他系統進行基準測試的功能。
TSBS是Go程式的集合,以生成資料集,然後對讀寫效能進行基準測試。該套件是可擴充套件的,因此可以包含不同的用例和查詢型別,並在系統之間進行比較。
在4個執行緒上執行時,QuestDB比ClickHouse快1.7倍,比InfluxDB快6.4倍,比TimescaleDB快6.5倍。
有關更多詳細資訊,版本6.0的 GitHub發行版 包含此發行版中的新增內容和修復程式的變更日誌。

 

相關文章