與基于磁盤存儲的普通數據庫相比,內存數據庫的數據訪問速度可以高出幾個數量級,可以大幅提升計算性能,更適用于高并發、低延遲的業務場景。但目前大部分內存數據庫仍然采用SQL模型,SQL缺乏一些必要的數據類型和操作,無法充分利用內存的特性來實現一些高性能的算法。簡單地把數據和操作從外存移到內存中,可以取得比外存好得多的性能,但如果不充分利用內存的特性,是無法達到極致性能的。我們來看看有哪些適合內存特性的算法和存儲機制,可以進一步提高內存數據庫的計算速度。指針多路復用
我們知道,內存是可以通過地址(指針)來訪問的。然而,SQL沒有由內存指針表示的數據對象。當返回結果集時,通常會制作數據的副本以形成新的數據表。這不僅會消耗更多的CPU時間(用于復制數據),還會占用更昂貴的內存空間(用于存儲復制的數據),從而降低內存利用率。除了基于SQL的內存數據庫,Spark中的RDD也有這個問題,而且情況更嚴重。為了保持RDD的不可變特性,Spark在每一個計算步驟后都會復制新的RDD,造成內存和CPU的極大浪費。所以即使消耗了巨大的資源,Spark依然無法實現高性能。相比之下,基于SQL的內存數據庫通常會進行優化,SQL語句中的計算會嘗試使用內存地址,這通常比Spark的要好。但是受限于理論,在實現SQL的邏輯時,必須復制返回的結果集。如果涉及到多步流程操作,就需要在上一步結果集(臨時表)的基礎上多次進行進一步的計算,SQL的劣勢就會很明顯。事實上,如果不改變數據結構,我們可以直接使用原始數據的地址來形成結果集,而不需要復制數據本身,只需多保存一個地址(指針),同時減少CPU和內存的消耗。擴展了SQL的數據類型,支持這種指針式復用機制。例如,根據訂單日期(odate)的范圍過濾訂單表后,分別計算訂單金額(amount1)大于1000和運費(amount2)大于1000的訂單,然后計算兩者的交、并、差,最后根據客戶編號(cid)對差進行排序。SPL電碼大致是這樣的:
A
B
一個
=orders . select(otate=date(2000,1,1)otate=date(2022,1,1))
2
=A1.select(金額11000)
=A1.select(金額21000)
三
=A2^B2
=A2B2
四
=A2\B2
=B2\A2
五
=A4 .排序(cid)
=B4.sort(cid)
上面的代碼中有幾個步驟,一些中間結果被多次使用。但由于使用了訂單中記錄的所有指針,所以內存占用增加很少,也避免了記錄復制的耗時。
外鍵預關聯
外鍵關聯是指使用一個表(事實表)的非主鍵字段來關聯另一個表(維度表)的主鍵。例如,訂單表中的客戶號和產品號分別與客戶表和產品表的主鍵相關聯?,F實中可能有七張、八張甚至十幾張表之多,可能有多層關聯。SQL數據庫通常使用哈希連接算法進行內存連接,需要計算和比較哈希值。在這個過程中還要占用內存存儲中間結果,關聯表多的時候計算性能會急劇下降。其實我們也可以利用內存指針引用機制,提前做好關聯。在系統的初始階段,事實表中關聯字段的值被轉換為相應維度表記錄的指針。因為維度表的關聯字段是主鍵,所以關聯記錄是唯一的,將外鍵值轉換為記錄指針不會導致錯誤。在后續的計算中,當需要引用維度表字段時,可以通過指針直接引用,不需要計算比較哈希值和存儲中間結果,從而獲得更好的性能。沒有SQL記錄指針的數據類型,就無法實現預關聯。原則上,SPL支持并實施這一預關聯機制。例如,完成訂單表與客戶表和產品表的預關聯的代碼大致如下:
A
一個
=file('customer.btx ')。導入@b()。按鍵@i(c
id)A1、A2 加載客戶表和產品表。
A3:加載訂單表,將其中的客戶號 cid、產品號 pid 轉換為對應維表記錄的指針。
A4:將完成預關聯的訂單表存入全局變量,供后續計算使用。系統運行時,按照產品供應商過濾訂單,再按客戶所在城市分組匯總的代碼大致是下面這樣:A | |
1 | =orders.select(pid.supplier=="raqsoft.com").groups(cid.city;sum(pid.price*quantity)) |
訂單表中的 pid 已經轉換為產品表記錄的指針,所以可以直接用“.”操作符引用產品表記錄。不僅書寫更簡單,而且運算性能也快得多。
只是兩、三個表關聯時,預關聯和 HASH JOIN 的差別還不是非常明顯。這是因為關聯并不是最終目的,之后還會有其它很多運算,關聯本身運算消耗時間的占比相對不大。但如果關聯情況比較復雜,涉及的表很多,以及有多層的時候(比如訂單關聯產品,產品關聯供應商,供應商關聯城市,城市關聯國家等等),預關聯的性能優勢會更明顯。序號定位
與外存相比,內存的另一個重要特征是支持高速的隨機訪問,可以快速從內存表中按指定序號(也就是位置)取出數據。在做查找計算時,如果被查找的值正好是目標值在內存表中的序號,或者很容易通過被查找值計算出目標值的序號,我們就可以用序號直接取目標記錄。這種方法不需要進行任何比對就能直接取出查找結果,性能不僅遠遠好于遍歷查找,也好于使用索引的查找算法。但是,SQL 以無序集合為基礎,不能按序號取成員,只能用序號去查找。如果沒有索引就只能遍歷查找,會非常慢。即使有索引也要計算 HASH 值或用二分法查找,速度也比不上直接定位。而且,建立索引也會占用昂貴的內存。如果數據表中沒有序號還要先排序再硬造個序號時,性能就會更差。SPL 以有序集合為基礎,提供序號定位功能。比如訂單表中的訂單號是從 1 開始的自然數。在查找訂單號 i 時,直接取訂單表中的第 i 條記錄就行了。再比如數據表 T 從 2000 年到 2022 年每天存儲一條數據,現在需要查詢指定日期的記錄。日期雖然不是目標值的序號,但是我們可以先算出指定日期距離起始日期的天數。這就是目標值的序號,然后再用序號取 T 表記錄就可以了。對表 T 用序號定位查找 2022 年 4 月 20 日記錄的代碼,大致是下面這樣:A | |
1 | =date(2022,12,31)-date(1999,12,31) |
2 | =T_orginal.align@b(to(A1),dt-date(1999,12,31)) |
3 | =env(T,A5) |
4 | =T(date(2021,4,20)-date(1999,12,31)) |
集群維表
當數據量太大,超出單機內存時,就要使用集群來加載這些數據。許多內存數據庫也支持分布式計算,通常是將數據分成多段,分別加載到集群不同分機的內存中。JOIN 是分布式計算的一個麻煩任務,會涉及多個分機之間的數據傳輸。嚴重的時候,傳輸造成的延遲會抵消集群分攤計算量得到的好處,會出現集群變大反而性能并不能提升的現象。SQL 體系下的分布式數據庫,通常是將單機 HASH JOIN 方法擴展到集群上。每個分機根據 HASH 值將本機數據分發到其他分機,確保相關聯的數據在同一分機上。然后再在各個分機上做單機連接。但是,HASH 方法在運氣不好的時候,可能會造成數據分配的嚴重不均衡,需要借助外存來緩存這些分發到的數據,否則可能因為內存溢出而導致系統崩潰。但是,內存數據庫的主要特征就是將數據加載到內存中計算,出現外存緩存會嚴重拖慢計算性能。實際上,外鍵關聯的事實表和維表有很大區別。事實表一般都比較大,要用各個分機內存分段加載才能裝的下。正好事實表也比較適合分段,每個分段的數據都相互獨立,分機之間不需要相互訪問。而維表記錄則會被隨機訪問,事實表的任何一個分段都可能關聯全部維表記錄。我們可以利用事實表和維表的區別,對集群的外鍵關聯提速。如果維表比較小,則將維表全量數據復制到所有分機內存中。這樣,每個分機中的事實表分段和全量維表就可以繼續完成預關聯,完全避免了關聯過程中的網絡傳輸。如果維表也很大,單機內存放不下,只能在各分機內存中分段加載。這時,沒有一個分機上有全量的維表,外鍵關聯計算就無法避免網絡傳輸了。不過傳輸內容并不算很大,只涉及事實表的外鍵和維表關聯記錄的字段,事實表其它字段不需要傳輸,計算可以直接完成,過程中也不會產生緩存數據。SPL 從原理上區分維表和事實表,針對維表較小和維表較大兩種情況,分別提供了維表復制機制和分段維表機制,實現了上述算法,能顯著提高集群情況下外鍵關聯的計算性能。備胎式容錯
集群系統必須要考慮容錯,內存數據的容錯和外存是不同的。外存一般使用副本的方法,即同一份數據有多個副本,某個分機失效后仍然能在其它分機找到數據。這種機制的存儲利用率很低,只有 1/k(k 是副本數量)。但是,對于內存中的數據,卻不能使用這種副本容錯方法。這是因為硬盤足夠便宜且幾乎可以無限擴容,但是內存要昂貴的多而且擴容有上限。只有 1/k 的內存利用率是無法容忍的。內存容錯需要不同于外存的專門手段。SPL 提供了備胎式容錯機制,將數據分成 n 段后分別加載到 n 個分機的內存中。然后準備 k 個空閑的分機作為備用機。當正在運行的某個分機失效時,則立即啟用某個備用機,臨時加載失效分機的數據,和其它分機重新組成擁有完整數據的集群繼續提供服務。失效的分機排除故障后恢復使用,可以再充當備用機。整個過程和汽車更換備胎的模式很像。備胎式容錯機制的內存利用率可以高達 n/(n+k),遠遠高于副本式容錯的 1/k。能加載進內存的數據量通常不會非常大,分機失效后臨時加載的時間并不多,集群服務就可以較快地恢復。回顧與總結
內存數據庫的計算體系,必須充分利用內存的特征才能獲得極致性能。從數據計算的角度來看,內存主要優點有:支持指針引用、支持高速隨機訪問、并發讀取能力強。內存的缺點是:成本高昂、擴容有上限。而 SQL 計算體系中缺乏一些必要的數據類型和運算,比如:缺少記錄指針類型,不支持有序運算,JOIN 定義過于籠統,不區分 JOIN 類型等,從原理上就不能充分利用內存的上述特征實現某些高速算法?;?SQL 的內存數據庫,通常只是簡單的照搬外存數據結構和運算,會出現各種問題。比如:記錄式復制過多消耗 CPU 和內存;查找和 JOIN 性能沒有達到極致。再比如集群方面:內存利用率過低;大量網絡傳輸導致分機數量增加但性能反而下降;多機 JOIN 出現外存緩存等等。開源數據計算引擎 SPL 擴展了數據類型和運算定義,可以充分利用內存的特征,從而實現多種高性能算法,讓性能達到極致。其中,指針式復用利用內存特有的指針引用機制,節省了內存空間,而且速度更快。預關聯同樣利用指針引用機制,在初始化階段完成很耗時的外鍵關聯,后續計算中直接使用關聯好的結果,計算速度顯著提高。序號定位利用有序性,充分發揮內存高速隨機訪問的優勢,不用做任何計算和比對,直接用序號讀取記錄,性能好于 HASH 索引等查找算法。集群維表有效避免或減少了網絡傳輸、避免了外存緩存,備胎式容錯在保證高可用性的前提下,有效提高了集群內存利用率。除此之外,SPL 還提供了排號鍵、序號索引、數據類型壓縮等等其它方法。程序員可以根據具體的場景,有針對性的采用這些方法,就能充分發揮內存的優勢,從而有效提升內存數據計算的性能。原文