記憶體資料庫如何發揮記憶體優勢?

hfhsdgzsdgsdg發表於2023-02-21

與以磁碟儲存為主的普通資料庫相比,記憶體資料庫的資料訪問速度可以高出幾個數量級,能大幅提高運算效能,更適合高併發、低延時的業務場景。


不過,當前大部分記憶體資料庫仍然採用 SQL 模型,而 SQL 缺乏一些必要的資料型別和運算,不能充分利用記憶體的特徵實現某些高效能演演算法。僅僅是把外存的資料和運算簡單地搬進記憶體,固然也能獲得比外存好得多的效能,但還沒有充分利用記憶體特徵,也就不能獲得極致的效能。


下面我們來看看,有哪些適合記憶體特徵的演演算法和儲存機制,可以進一步提升記憶體資料庫計算速度。


指標式複用

我們知道,記憶體可以透過地址(指標)來訪問。但 SQL 沒有用記憶體指標表示的資料物件,在返回結果集時,通常要把資料複製一份,形成一個新的資料表。這樣不僅多消耗 CPU 時間(用於複製資料)而且還會佔用更多昂貴的記憶體空間(用於儲存複製的資料),降低記憶體使用率。


除了 SQL 型的記憶體資料庫外,Spark 中的 RDD 也有這個問題,而且情況更嚴重。為了保持 RDD 的 immutable 特性,Spark 在每個計算步驟後都會複製出新的 RDD,造成記憶體和 CPU 的大量浪費。所以,即使耗用了巨大資源,Spark 也仍然做不到高效能。相比之下,SQL 型的記憶體資料庫通常還會最佳化,在 SQL 語句中的計算會盡量使用記憶體地址,通常要比 Spark 的效能更好。


但是,受到理論限制,實現 SQL 的邏輯時,返回的結果集就必須複製了。如果涉及多步驟的過程運算,要多次在上一步的結果集(臨時表)基礎上進一步計算,SQL 的劣勢就會很明顯了。


事實上,如果沒有改變資料結構,我們可以直接用原資料的地址形成結果集,不需要複製資料本身,僅僅多儲存一個地址(指標),同時減少 CPU 和記憶體的消耗。


SPL 擴充套件了 SQL 的資料型別,支援這種指標式複用機制。比如,對訂單表按照訂單日期(odate)範圍過濾後,分別求出訂單金額(amount1)大於 1000 和運貨費(amount2)大於 1000 的訂單,再計算出兩者的交集、並集和差集,最後將差集按照客戶號(cid)排序。SPL 程式碼大致是這樣:


A B

1 =orders.select(odate>=date(2000,1,1) && odate<=date(2022,1,1))

2 =A1.select(amount1>1000) =A1.select(amount2>1000)

3 =A2^B2 =A2&B2

4 =A2\B2 =B2\A2

5 =A4.sort(cid) =B4.sort(cid)

以上程式碼中有好幾個步驟,有的中間結果也被用了多次,但由於使用的都是訂單表記錄的指標,所以記憶體佔用增加的很少,也避免了記錄複製的耗時。

外來鍵預關聯

外來鍵關聯是指用一個表(事實表)的非主鍵欄位,去關聯另一個表(維表)的主鍵。比如訂單表中的客戶號和產品號分別關聯客戶表、產品表的主鍵。現實運算中這種關聯可能多達七八個甚至十幾個表,還可能出現多層的關聯。SQL 資料庫通常使用 HASH JOIN 演演算法來做記憶體連線,需要計算和比對 HASH 值,過程中還會佔用記憶體來儲存中間結果,關聯表很多時計算效能就會急劇下降。


其實,我們也可以利用記憶體指標引用機制事先做好關聯。在系統初始化階段,把事實表中的關聯欄位值轉換為對應維表記錄的指標。因為維表的關聯欄位是主鍵,所以關聯記錄唯一,將外來鍵值轉換成記錄指標不會引起錯誤。在後續計算中,需要引用維表欄位時,可以用指標直接引用,無需計算和比對 HASH 值,也不需要再儲存中間結果,從而獲得更優的效能。SQL 沒有記錄指標這種資料型別,也就無法實現預關聯了。


SPL 則從原理上支援並實現了這種預關聯機制。例如,完成訂單表和客戶表、產品表預關聯的程式碼大致是這樣:


A

1 =file(“customer.btx”).import@b().keys@i(cid)

2 =file(“product.btx”).import@b().keys@i(pid)

3 =file(“orders.btx”).import@b().switch(cid,A1;pid,A2)

4 >env(orders,A3)

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))

A1:計算出 2000 年到 2022 年總天數是 8401 天。


A2:用原始的 T 表記錄計算出距離起始日期的天數,再和 to(A1)這個自然數集合 [1,2,3,…,8401] 對齊,空缺的日期會用 null 補齊。align 的 @b 選項表示對齊時將使用二分法來查詢位置,這樣完成對齊動作也會更快一點。


A3:計算好的結果,放到全域性變數 T 中。


A4:要查詢 2021 年 4 月 20 日記錄,求出這個日期和起始日期距離 7781 天,直接取出 T 表中第 7781 條記錄就可以了。


A1 到 A3 是對齊計算,用於處理空缺的日期,可以放在系統初始化階段。在查詢計算時,用 A4 中的序號定位程式碼就能得到查詢結果,實際查詢的日期可以作為引數傳入。


叢集維表

當資料量太大,超出單機記憶體時,就要使用叢集來載入這些資料。許多記憶體資料庫也支援分散式計算,通常是將資料分成多段,分別載入到叢集不同分機的記憶體中。


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 還提供了排號鍵、序號索引、資料型別壓縮等等其它方法。程式設計師可以根據具體的場景,有針對性的採用這些方法,就能充分發揮記憶體的優勢,從而有效提升記憶體資料計算的效能。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70026759/viewspace-2936124/,如需轉載,請註明出處,否則將追究法律責任。

相關文章