kdb+ 以討厭迴圈而聞名(不要討厭的迴圈)。身為 kdb+ 開發者,我們在架構設計上有很大的彈性,而今天我想說服你,我們的系統裡還有另一個東西應該移除:rdb。不要討厭的 rdb!

在 kdb+ 的世界中,基本上有兩種儲存資料的方式:記憶體內與磁碟上。資料本身是相同的。這是一個關鍵的洞見,如今在欄位式資料領域已相當常見(特別可參考 Apache Arrow),因為這消除了在資料移動時進行序列化的需求,並支援記憶體對應與從磁碟極快速讀取資料。kdb+ 支援多種不同的架構,但標準或預設模型仍然是將兩種主要程序類型一起使用:即時資料庫(rdb),資料完全位於程序自己的記憶體空間中,以及歷史資料庫(hdb),資料序列化在磁碟上並記憶體對應到程序的記憶體空間中。

我的開場白是有意挑釁且帶有點戲謔。我並不真的認為我們完全不應該使用 rdb。kdb+ 作為一項技術的一大優勢在於其彈性,而 kdb-x 承諾會帶來更多彈性。我在此要提出的論點是,rdb 不應被視為所有 kdb 系統的預設選項。它們一直是 kdb 核心程序類型之一,但我認為對於許多問題來說,它們可能並非必要,你至少應該考慮不使用它們。這種做法並非新創,已在多個安裝環境以及 KX 自身 Insights 產品的某些元件中部署,但關於何時何地適用,其細節並不總是清楚。

為什麼要有 rdb?

更快的查詢

對記憶體中的資料進行查詢當然更快,不是嗎?我用相當標準的硬體與相當標準的資料進行了一個簡單的基準測試:

  • 一台 i7i.xlarge ec2 執行個體,具備 32GB 記憶體與快速 NVMe SSD 儲存
  • 單一典型(相對較小)的 NYSE TAQ 交易日資料。大約 5000 萬筆交易,磁碟上約 10GB
  • 比較對 記憶體內資料(rdb)磁碟上資料(hdb) 以及 快取冷卻時的磁碟上資料(hdb cold cache) 的查詢效能
  • 使用一組涵蓋典型工作負載的小型查詢

bench1

因此,在我們進一步討論這些結果之前,關鍵的背景是 hdb 程序通常不會真的到磁碟去取得資料。它們會記憶體對應磁碟上的檔案,當存取時,作業系統會先檢查頁面快取,如果特定檔案存在於該快取中,就不會真的到磁碟去取得。因此在上述所有 hdb 結果 中,查詢都是命中頁面快取而非磁碟,也就是快取是溫的。所以當快取是溫的時候,hdb 與 rdb 並沒有太大差異:資料都在記憶體中,只是位於作業系統層級的頁面快取,而不是程序自己的記憶體空間。我也加入了 第三組快取「冷卻」時的基準測試,這樣我們就能看到當作業系統必須真的到磁碟去取得資料時的差異。

那麼,這些基準測試的主要結論是:

  • 先說重點:記憶體內 在所有基準測試中都是最快的
  • 如果查詢可以使用屬性且結果集很小,記憶體內 資料會大幅勝過 磁碟。最後一個查詢尤其如此,當資料在記憶體中且有屬性時,速度快非常多。這些查詢實際上是在雜湊映射上進行查找,因此是 O(1) 且非常快。我們也可以在磁碟上的資料加入屬性,但磁碟上的屬性在 kdb+ 中無法附加,因此我們無法維護它們。稍後我會再回來討論這一點。
  • 如果查詢不使用屬性,或是使用了但結果集較大,rdbhdb 之間的效能差距很小。視查詢而定,大約在 0-20% 之間。這個小差距很可能來自存取頁面快取時的輕微分頁錯誤、TLB 壓力以及其他技術因素,不值得過度擔心。作業系統頁面快取中的記憶體資料,並不完全 和 rdb 堆積中的資料一樣快,但已非常接近。上述的聚合與大型掃描(帶有篩選屬性)比較也許最有趣:這兩個查詢都受益於屬性,但沒有屬性的 hdb 仍然不會落後太多,因為查詢仍然需要存取大量資料,所以屬性的效益較小。因此,hdb 其實可以被視為另一種記憶體內資料庫,只是記憶體位於共享空間:作業系統頁面快取
  • 即使查詢必須一路到 磁碟,速度差異仍然不算太大。最差的情況大約是 2 倍。這是現代 NVMe SSD 帶來巨大差異的地方。在傳統硬碟上,這個差異會大非常多。

跟上插入作業

除了讀取資料,我們還必須能夠加入新資料。新的交易在交易日中不斷流入,我們需要能夠加入它們。這裡是另一個比較兩種設定的基準測試:

bench2

記憶體內 程序在此有更明確的優勢。但這個優勢可能不如你預期的那麼大。它肯定比使用慢速傳統硬碟時小得多。另外一個關鍵點是:插入記憶體是一個序列化程序。當 rdb 正在插入新資料時,無法同時提供查詢服務。後面的情況則不同,我們可以同時插入到磁碟上的 splayed 資料表,並讓程序對資料提供查詢服務。運算與記憶體是分離的。

為什麼不要有 rdb?

更快的查詢

我知道上面我展示了 rdb(稍微)更快,但那只是單一查詢。如果有 2 個或 10 個查詢呢?在 rdb 上,你只能序列化執行查詢。它無法並行執行查詢。然而,如果你的資料共享在磁碟上(並透過作業系統頁面快取在記憶體中),你就可以平行執行。每個單一查詢可能會稍慢一點,但你可以同時在不同的程序上執行全部 10 個查詢。一個在 hdb 上慢 10% 的查詢,如果你需要執行兩次,就會變快 45%,因為你可以平行執行,例如兩個各 110ms 的查詢平行執行總共 110ms,與兩個各 100ms 的查詢序列執行需要 200ms 相比。這也是如果你想要在同一系統上支援不同類型使用案例的一大架構優勢。例如研究查詢與延遲敏感的交易查詢,而不會互相干擾。

所有記憶體預設共享

我們基本上可以免費進行分片與擴展。rdb 有固定的記憶體成本,如果我們想要更多程序,就需要更多記憶體,而且新程序啟動很慢,因為它們需要將所有目前資料讀入記憶體。當你的資料只儲存在一個地方(磁碟與作業系統頁面快取)時,你可以在其上擁有 10 個程序,或是 100 個程序呢?這些程序可以幾乎立即啟動。這從根本上分離了記憶體與運算

不需要閘道程序

當然,如果你想要的話,你仍然可以擁有閘道程序,但你並不需要它們來將 rdb 與 hdb 資料結合在一起。讓所有資料在一個程序中都可以存取是可能的。使用者可以啟動自己的單一程序,存取所有資料,包括即時與歷史資料。

更簡單的架構

在即時附加到磁碟資料時,重新載入與對應資料有一些細節需要考慮,但整體來說,你會有更少的程序與更少的程序類型。不存在 rdb、hdb 與閘道程序之間的協調問題,因此你的系統很可能會更簡單。

那麼,讓我們再多談一點……

替代的預設架構

標準 kdb+ 架構 之所以如此是有原因的。如果你處於記憶體昂貴許多,且所有非揮發性儲存都是傳統硬碟的世界,將資料庫分成兩個部分會合理得多。對磁碟上的 splayed 資料表進行插入會比記憶體慢非常多,可能會有數個數量級,而任何未命中作業系統頁面快取(冷快取)的查詢也會慢非常多,同樣可能是數個數量級。但這已經不是我們所處的世界了。

我提出一個替代的預設架構,看起來更像這樣:

arch

tickerplant 程序與之前完全相同,但所有資料都會透過「writer」程序(基本上是 TorQ wdb 程序)直接寫到磁碟,而所有資料都會透過掛載磁碟資料的資料庫程序(TorQ hdb)讀取。

我在此要清楚說明,這絕不是什麼革命性的想法。我相信今天運作中的 kdb+ 系統中,有些已經有類似的架構。我在此的論點並非這是新的想法,而是我們應該更認真且更廣泛地考慮這個替代方案。

這裡有更清楚的圖片,顯示我們如何將磁碟上的資料分為「即時分割區」與所有標準歷史資料:

arch1.5

我相信這種做法會帶來幾個顯著的優勢:

  • 更簡單。 程序越少,問題越少
  • 更具彈性與可擴展性。 運算與記憶體是分離的。如果我們需要更多資料庫程序,我們可以直接加入,它們沒有直接的記憶體開銷,而且啟動速度非常快(不像傳統的 rdb)。我們實際上是在使用作業系統頁面快取作為「免費」的記憶體管理,而這是經過實戰考驗的核心作業系統程式碼,因此非常穩固。
  • 資料位於一個地方。 如果你需要今天與前幾天的資料,無需對兩個不同的程序執行兩個查詢並處理聯結等問題。所有資料都在一個地方

當然,軟體工程中的一切都是權衡,而這裡我們做的權衡是:

  • 與 rdb 相比,查詢效能會稍慢一些。如上所述,差異遠小於你想像的,但確實存在差異。
  • 在即時附加的分割區中,我們無法擁有屬性。 這意味著對於某些「小型查找」類型的查詢,效能差異會很明顯。然而,這些查詢可能不是你工作流程的重要部分,或者如果你想要上述優勢同時保持這些類型的查詢快速,你可以為你需要的項目加入專用快取程序,也就是根據需求加入專用快取,而不是大型昂貴的通用快取程序(也就是 rdb)

對於「即時附加」資料庫(db,不再需要稱為 hdb 與 rdb)還有幾個關鍵的技術考量:

  • 我們想要將「即時分割區」鎖定在作業系統頁面快取中,以確保查詢很少/從不需要到磁碟去。這在資料被大量使用時會自然發生,因為作業系統會嘗試將經常存取的資料保留在快取中。然而,我們也可以使用 vmtouchtmpfs(RAMdisk)來直接控制這一點。因此我們可以保證始終獲得上述熱快取的基準測試結果,而永遠不需要到磁碟去。這相對容易設定。
  • 資料庫程序與 symfile(列舉)以及即時附加中的欄位之間存在協調問題。這並不像看起來那麼糟。我認為解決這問題的最佳方式是使用 .Q.MAP 來避免每次查詢的 mmap 開銷,並使用 \l 定期重新載入 symfile 與重新對應最新資料。原則上這可以非常快(在我的上述測試設定中,執行 \l 需要 0.018ms),而我們有幾個選項可以根據系統中可用性與速度之間的權衡來決定如何執行:
    • 如果成本足夠低,你可以在任何查詢之前直接執行
    • 或者你可以讓 writer 在每次插入時觸發所有 db 的重新載入,這樣我們只會在有新資料時重新載入
    • 或者可以基於計時器
  • symfile 必須保持很小,因為我們會經常重新載入它。雖然我們可以對此變得聰明,並樂觀地重新載入,也就是使用 chksum 快速檢查它是否已變更,只在需要時才重新載入。
  • 我們無法在「即時」分割區中擁有屬性。傳統上 hdb 分割區會在最常篩選的欄位上放置 p 屬性,作為查找的索引。kdb+ 中磁碟上的屬性基本上無法附加,因此我們必須放棄它們。然而,當資料移到歷史並離開「即時分割區」時,我們仍然可以在 rollover 時正常排序並加入它們

根據你特定系統的需求,上述架構可以透過即時引擎與/或快取程序來擴展,以計算分析(例如 VWAP、bars 等)。這些值可以發布回 tickerplant 並從資料庫程序讀取,或直接存取

arch2

我不想過度討論這些技術細節,所以我打算後續提供如何使用 TorQ 設定這種架構的細節:

  • 如何盡可能快速地重新載入與重新對應資料
  • Rollover
  • 盡可能輕鬆地設定與開始使用

我不是一個喜歡長篇結論的人,但希望我至少讓你考慮 kdb+ 的替代預設架構。「無 rdb」架構可以更簡單、更具可擴展性,且速度幾乎一樣快。我並不是說這是「正確」的方式,只是我們應該更強烈地考慮這件事。如果有其他人有想法或想進一步討論這些想法,我們很樂意聽到你的意見!