
作者:李紅建
責編:宇亭
在第一期研發分享中,我們解釋了,為什么Tinamu作為一款列式存盤引擎在初期不支持 Delete 功能的原因,然后對一些友商列式存盤引擎的 Delete 方案進行了一些調研和總結,感興趣的同學可以查看我們上一期的分享:關于列式資料庫實作 Delete 功能的調研之旅,
本期文章,我將向社區小伙伴們詳細地介紹一下給 StoneDB 的 Tianmu 存盤引擎添加 Delete 功能的開發思路,希望對感興趣的同學提供幫助,
Tianmu 引擎的存盤結構
首先我們需要知道 Tianmu 引擎的資料是怎么樣存盤的,這樣才知道應該怎么洗掉資料,所以我們先研究下 Tianmu 引擎的存盤結構,Tianmu 為每個表單獨建立了一個檔案夾,以表名+tianmu 命名,每個表的檔案夾下面又為各個列分別建立了對應列的檔案夾,以列編號為名從 0 開始依次遞增,列檔案夾下面存盤元資料 DN 檔案和實際列資料的 DATA 檔案,


如上圖所示,可以看到每個列檔案夾下面有這么幾個檔案夾,其中:DATA 檔案夾存盤對應列 pack 的檔案;DN 檔案夾存盤元資料 DPN 的檔案;filters 檔案夾下存放著直方圖、映射表、布隆過濾器(Bloom Filter)等中間資料的檔案;META 檔案夾存盤了列的一些固有屬性,如資料型別、版本、壓縮型別等;v檔案下存盤了列資料的版本,
當然,可能一些同學乍一看上面的什么 DN、DPN 都不知道什么意思,其實是因為我們的 Tianmu 引擎使用了非常重要的知識網格(Knowledge Grid)技術,后面我們有時間會單獨出更詳細的文章來分享知識網格相關的最新研究,

如上圖所示,DPN(Data Pack Node) 是知識網格的資料元資訊節點在代碼中的資料結構,資料持久化在各個列檔案夾下面的 DN 檔案中,初始化資料元資訊節點時從利用 mmap 機制把 DN 檔案映射到記憶體中,pack 是物理的資料塊,每個pack存盤著對應列中多個列資料,pack 物件跟 DPN 物件 1:1 對應,負責從 各個列檔案夾下面的 DATA 檔案寫入資料 和讀取資料,pack 中的資料經過高度壓縮后存盤到 DATA 檔案中,
Tianmu 的資料都是根據列按照行資料緊密排序進行存盤的,從檔案中讀取和寫入的單位是 pack,其中:
行號與 DPN & pack 的關系:
DPN id 由 row_id 進行位移右移運行得出, pss 的值一般為 16 ,也就是說每 65536 行的資料組成一個 pack,資料包結構的資訊比如行與資料包的偏移量 pss, 資料化結構體為 COL_META 持久化在 META 檔案中,
行號與 pack 中資料 id 的關系:
資料 ID 由 row_id 對 1 左移 pss 為后的值 取余后得出,基本上資料ID也都是在 0~65536 之間,
可以看以下這幅圖:

好了,以上就是我們對 Tianmu 引擎存盤結構的一個簡單介紹,
MySQL 的多引擎架構和執行介面
了解完 Tianmu 的存盤結構后,我們就要去想如何進行洗掉的操作了,這個時候就需要用到 MySQL 的多引擎架構和執行介面了,因為我們要讓用戶使用 MySQL 客戶端來進行 Tianmu 引擎里的洗掉操作,下圖是 MySQL 的多引擎架構圖:

可以看到,MySQL 架構的最大特點之一,就是支持可插拔存盤引擎,再來看一下MySQL 的執行介面邏輯圖:

這個部分,網路上的一些基礎知識分享很多了,大家可以學習了解一下,我們這邊特別要去講解的是代碼部分的邏輯,下面是我在 GDB 中除錯的幾個重要代碼邏輯:
insert 呼叫堆疊:
#0 Tianmu::handler::ha_tianmu::write_row (this=0x7fdcec0107b0, buf=0x7fdcec09e710 "\374\002")
at /home/Code/GitHub/stonedb/storage/tianmu/handler/ha_tianmu.cpp:455
#1 0x0000000001d6e5a1 in handler::ha_write_row (this=0x7fdcec0107b0, buf=0x7fdcec09e710 "\374\002")
at /home/Code/GitHub/stonedb/sql/handler.cc:8189
#2 0x00000000025ebf12 in write_record (thd=0x7fdcec000bc0,table=0x7fdcec00fdf0,info=0x7fe0c81c9b00, update=0x7fe0c81c9a80)
at /home/Code/GitHub/stonedb/sql/sql_insert.cc:1904
#3 0x00000000025e8fdd in Sql_cmd_insert::mysql_insert (this=0x7fdcec006ab0,thd=0x7fdcec000bc0, table_list=0x7fdcec006518)
at /home/Code/GitHub/stonedb/sql/sql_insert.cc:778
#4 0x00000000025ef9b3 in Sql_cmd_insert::execute (this=0x7fdcec006ab0, thd=0x7fdcec000bc0)
at /home/Code/GitHub/stonedb/sql/sql_insert.cc:3151
#5 0x00000000023cb967 in mysql_execute_command (thd=0x7fdcec000bc0,first_level=true)
at /home/Code/GitHub/stonedb/sql/sql_parse.cc:3645
#6 0x00000000023d175d in mysql_parse (thd=0x7fdcec000bc0,parser_state=0x7fe0c81cae70)
at /home/Code/GitHub/stonedb/sql/sql_parse.cc:5655
#7 0x00000000023c68b8 in dispatch_command (thd=0x7fdcec000bc0,com_data=https://www.cnblogs.com/stonedb/archive/2022/12/09/0x7fe0c81cb610, command=COM_QUERY)
at /home/Code/GitHub/stonedb/sql/sql_parse.cc:1495
#8 0x00000000023c57e5 in do_command (thd=0x7fdcec000bc0)
at /home/Code/GitHub/stonedb/sql/sql_parse.cc:1034
#9 0x00000000024f6beb in handle_connection (arg=0x91fc3a0)
at /home/Code/GitHub/stonedb/sql/conn_handler/connection_handler_per_thread.cc:313
#10 0x0000000002bc3d2a in pfs_spawn_thread (arg=0x91ce010)
at /home/Code/GitHub/stonedb/storage/perfschema/pfs.cc:2197
#11 0x00007fe141fa9ea5 in start_thread () from /lib64/libpthread.so.0
#12 0x00007fe13f246b0d in clone () from /lib64/libc.so.6
update 呼叫堆疊:
#0 Tianmu::handler::ha_tianmu::update_row (this=0x7fdcec0107b0,
old_data=https://www.cnblogs.com/stonedb/archive/2022/12/09/0x7fdcec09eb18"\374\002", new_data=https://www.cnblogs.com/stonedb/archive/2022/12/09/0x7fdcec09e710"\374\002")
at /home/Code/GitHub/stonedb/storage/tianmu/handler/ha_tianmu.cpp:508
#1 0x0000000001d6ea41 in handler::ha_update_row (this=0x7fdcec0107b0,
old_data=https://www.cnblogs.com/stonedb/archive/2022/12/09/0x7fdcec09eb18"\374\002", new_data=https://www.cnblogs.com/stonedb/archive/2022/12/09/0x7fdcec09e710"\374\002")
at /home/Code/GitHub/stonedb/sql/handler.cc:8230
#2 0x000000000247ed8c in mysql_update (thd=0x7fdcec000bc0, fields=...,
values=..., limit=18446744073709551615, handle_duplicates=DUP_ERROR,
found_return=0x7fe0c81c9c58, updated_return=0x7fe0c81c9c50)
at /home/Code/GitHub/stonedb/sql/sql_update.cc:894
#3 0x0000000002484ead in Sql_cmd_update::try_single_table_update (
this=0x7fdcec006808, thd=0x7fdcec000bc0,
switch_to_multitable=0x7fe0c81c9cff)
at /home/Code/GitHub/stonedb/sql/sql_update.cc:2927
#4 0x00000000024853d7 in Sql_cmd_update::execute (this=0x7fdcec006808,
thd=0x7fdcec000bc0) at /home/Code/GitHub/stonedb/sql/sql_update.cc:3058
#5 0x00000000023cba0c in mysql_execute_command (thd=0x7fdcec000bc0,
first_level=true) at /home/Code/GitHub/stonedb/sql/sql_parse.cc:3655
#6 0x00000000023d175d in mysql_parse (thd=0x7fdcec000bc0,
parser_state=0x7fe0c81cae70)
at /home/Code/GitHub/stonedb/sql/sql_parse.cc:5655
#7 0x00000000023c68b8 in dispatch_command (thd=0x7fdcec000bc0,
com_data=https://www.cnblogs.com/stonedb/archive/2022/12/09/0x7fe0c81cb610, command=COM_QUERY)
at /home/Code/GitHub/stonedb/sql/sql_parse.cc:1495
#8 0x00000000023c57e5 in do_command (thd=0x7fdcec000bc0)
at /home/Code/GitHub/stonedb/sql/sql_parse.cc:1034
#9 0x00000000024f6beb in handle_connection (arg=0x91fc3a0)
at /home/Code/GitHub/stonedb/sql/conn_handler/connection_handler_per_thread.cc:313
#10 0x0000000002bc3d2a in pfs_spawn_thread (arg=0x91ce010)
at /home/Code/GitHub/stonedb/storage/perfschema/pfs.cc:2197
#11 0x00007fe141fa9ea5 in start_thread () from /lib64/libpthread.so.0
#12 0x00007fe13f246b0d in clone () from /lib64/libc.so.6
delete呼叫堆疊:
#0 Tianmu::handler::ha_tianmu::delete_row (this=0x7fdcec0107b0,
buf=0x7fdcec09e710 "\374\002")
at /home/Code/GitHub/stonedb/storage/tianmu/handler/ha_tianmu.cpp:581
#1 0x0000000001d6ee3f in handler::ha_delete_row (this=0x7fdcec0107b0,
buf=0x7fdcec09e710 "\374\002")
at /home/Code/GitHub/stonedb/sql/handler.cc:8263
#2 0x00000000025e053f in Sql_cmd_delete::mysql_delete (this=0x7fdcec006e28,
thd=0x7fdcec000bc0, limit=18446744073709551615)
at /home/Code/GitHub/stonedb/sql/sql_delete.cc:497
#3 0x00000000025e3268 in Sql_cmd_delete::execute (this=0x7fdcec006e28,
thd=0x7fdcec000bc0) at /home/Code/GitHub/stonedb/sql/sql_delete.cc:1411
#4 0x00000000023cba0c in mysql_execute_command (thd=0x7fdcec000bc0,
first_level=true) at /home/Code/GitHub/stonedb/sql/sql_parse.cc:3655
#5 0x00000000023d175d in mysql_parse (thd=0x7fdcec000bc0,
parser_state=0x7fe0c81cae70)
at /home/Code/GitHub/stonedb/sql/sql_parse.cc:5655
#6 0x00000000023c68b8 in dispatch_command (thd=0x7fdcec000bc0,
com_data=https://www.cnblogs.com/stonedb/archive/2022/12/09/0x7fe0c81cb610, command=COM_QUERY)
at /home/Code/GitHub/stonedb/sql/sql_parse.cc:1495
#7 0x00000000023c57e5 in do_command (thd=0x7fdcec000bc0)
at /home/Code/GitHub/stonedb/sql/sql_parse.cc:1034
#8 0x00000000024f6beb in handle_connection (arg=0x91fc3a0)
at /home/Code/GitHub/stonedb/sql/conn_handler/connection_handler_per_thread.cc:313
#9 0x0000000002bc3d2a in pfs_spawn_thread (arg=0x91ce010)
at /home/Code/GitHub/stonedb/storage/perfschema/pfs.cc:2197
#10 0x00007fe141fa9ea5 in start_thread () from /lib64/libpthread.so.0
#11 0x00007fe13f246b0d in clone () from /lib64/libc.so.6
由呼叫堆疊可知,insert、update和delete的相關代碼指令都會呼叫到Tianmu::dbhandler::TianmuHandler 類中各自功能的函式,而 TianmuHandler 繼承自 handler,MySQL 以 handler 為基類,各個引擎的 handler 類為子類,利用多型的原理實作對不同引擎的呼叫,
如果要實作Tianmu的單表 delete 功能,就需要在 TianmuHandler :: delete_row() 中進行實作,同時 handler 類還提供了洗掉所有行的虛函式 delete_all_rows() 如需支持洗掉所有行的資料,可在TianmuHandler :: delete_all_rows() 中進行實作,
Tianmu 引擎洗掉資料的程序
由此,我們便可以對 Tianmu 的delete功能進行設計和研發了,下面是我調研實作 delete 功能的流程圖:

單表 delete all 功能:
目前我們是支持 truncate 功能的,單表 delete all 的功能就直接復用 truncate 的邏輯,
條件 delete 功能:
條件 delete 這里我們采用標記洗掉的策略,列式資料庫的存盤結構決定了對真實的資料進行洗掉時必須要對整個表的資料進行重新移動整理,因為除了洗掉無用的行,還需要合并資料塊,這樣的話,在資料量非常多的情況下,對真實的資料進行洗掉將會是非常大的動作,不僅會消耗機器大量的IO資源和CPU資源,同時洗掉的速度也會比較慢,這也是目前主流支持列式資料庫的廠商都使用標記洗掉的原因,
注意看上面的執行流程圖,我們會發現一個很重要的節點——Delete bitmap(Delete位圖),這個 Delete 位圖是什么呢?這里要重點講解一下,
位圖(bitmap)的實際存盤形式是個 int32 型別的陣列,原理是使用 int32 型別的值占用的 32 位空間使用 0 或 1 存盤并記錄這 32 個值的狀態,位圖中的位元總數等于包中的行的總數,資料在 pack 中的位置和位圖中的位置是一一對應的,這樣可以有效地節省空間,
那么 Delete 位圖應該存放在哪個位置呢?一般有這么四種方案:
方案1.存放在pack里:
優點:進行標記洗掉的時候同時可對資料置空,可有效的釋放字串型別的空間,同時可優化 select ,insert ,update 帶where子句的資料過濾場景,不需要修改上層邏輯,整體邏輯簡單,修改面主要集中在pack層,
缺點:每次洗掉都需要對 涉及的pack進行讀取 解壓縮/壓縮,(其他方案在修改元資料時也需要對pack進行讀取解壓縮)
方案2.存放DPN里:
優點:洗掉不需要對 pack 進行讀取保存,只需要修改元資料即可,且 delete 位圖大小是固定的,
缺點:delete 位圖過大,一般是 8192 個位元組,遠遠超過原本元資料的大小,會極大的影響原 DPN 的讀寫效率,
方案3. RCAttr::hdr 中:
優點:一個列中只需要維護一個delete位圖即可,節省存盤空間,
缺點:因為列的資料數量是會隨時變化的,不像 pack 和DPN 維護的單獨一個包資料的數量是固定的,這就造成了 ,delete位圖的大小也需要隨時變化,
方案4. 為每列新增 deleteBitMap 檔案:
在 DPN中增加 deletBitMap 索引,與 deleteBitMap 檔案中的 deleteBitMap 對應,如下圖:

優點:可以與 DPN 一 一對應,且 delete 位圖大小固定,
缺點:需要新增一個檔案專門維護 delete bitmap ,讀取 DPN 檔案的同時也需要讀取 delelte bitmap 檔案,會增加一次 IO,
我們最后的選擇
最后,經過綜合考量,我們這里使用了方案 1 進行了把 delete 位圖放到 pack 里進行標記 delete 功能的開發,
資料過濾的流程和涉及邏輯的改造
經過上述的思路梳理,我們應該大致能清晰地了解到增加 Delete 功能的流程,因為涉及的東西比較多,我這里做了一個腦圖,具體的代碼,大家可以訪問我們的Github 代碼倉庫進行了解:https://github.com/stoneatom/stonedb

其中Tianmu引擎存盤的元資料和pack資料是支持多版本的,這樣可以保障資料的原子性,而且可以支持并發的讀取資料,也就是說,在執行delete時并不會堵塞select,用戶訪問的資料是最終確定的版本,關于多版本和并發訪問控制會在以后單獨出文章進行詳細的講解,
好了,以上就是目前 StoneDB 自研列式引擎 Tianmu 對 Delete的實作思路,希望這兩期分享能給大家帶來幫助,當然,由于是文章,里面很多圖片的細節,我們沒有展開描述,之前我們有開展過技術分享公開課,大家也可以前往B站觀看這兩期視頻:
【StoneDB每日講】Tianmu 引擎 Delete 方案的調研-第一講
https://www.bilibili.com/video/BV1Q14y1t7ZC
【StoneDB每日講】Tianmu 引擎 Delete 功能的誕生-第二講
https://www.bilibili.com/video/BV1Cg411S7tt
StoneDB 2.0 云原生分布式實時 HTAP 架構詳細設計以 RFC 形式持續進行,歡迎大家關注我們最新進展,更歡迎給我們開源協作的模式和方法提出改進意見,一起通過開源的方式共建 StoneDB ~
https://github.com/stoneatom/stonedb/issues/436
- StoneDB 代碼已完全在 Github 開源:
https://github.com/stoneatom/stonedb
- StoneDB 官網:
https://stonedb.io/
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/539690.html
標籤:其他
