OceanBase 原始碼解讀(七):一文讀懂資料庫索引實現原理
此前,帶你讀原始碼第六篇 《戳這裡回顧: OceanBase 原始碼解讀(六):儲存引擎詳解 》 為大家詳細講解了 OceanBase 儲存引擎,併為大家 回答了關於 OceanBase 資料庫的相關提問。
本期“原始碼解讀”將嘗試從程式碼導讀的角度,簡要介紹 OceanBase 的索引構建流程,帶領大家讀懂索引構建的相關程式碼,必須速速收藏!
首先請思考,在一個一般的資料庫中,索引表的語義是什麼?
索引表,其實是在主表(也稱資料表)之外,再建立一份冗餘且有序的資料,用於加速某些查詢流程。為什麼索引表可以加速查詢,是因為索引表是按照索引鍵排序的,如果某個查詢語句的查詢條件中指定了索引字首,那麼透過二分查詢即可快速找到對應的行。索引表的行中包括了主鍵資訊,所以可以透過主鍵快速找到主表的完整行,這個過程也叫索引回表。
知道了索引表的語義,那建立索引要做哪些事呢?
其實索引表也跟主表一樣,有自己的 schema、記憶體結構和持久化的資料(通常存放於本地磁碟上)。在分散式場景下,還可能有自己的位置資訊。那建立索引也就是要建立索引表的 schema,然後在某個位置建立索引表的記憶體結構,建立索引表的持久化資料。
在建立索引的過程中,我們不希望影響主表的正常讀寫,即建索引過程中業務是線上(online)的。為了做到線上建索引,不同廠商對線上的有不同的實現機制,本文會介紹 OceanBase 是如何實現線上建索引的,對其他方案感興趣的同學可以自行參考相關文件。
那在 observer 的視角,這其中包含哪些流程呢?首先這條語句的文字被隨機傳送到一個 observer(observer下文簡稱obs),收到這條語句的 obs 叫做中控 obs。跟其他語句一樣,這條建索引的 sql 語句首先經過 parser 和 resolver,發現這是一條 ddl 語句,並且將這條語句解析成了 OceanBaseCreateIndexArg 這樣一個結構體。對於ddl 語句,OceanBase 是傳送到 rootservice(rootservice下文簡稱 rs)來處理,所以中控 obs 向 rs 傳送一個建索引的 rpc 請求,這個 rpc 請求中攜帶了OceanBaseCreateIndexArg。
rs 收到該請求後,透過
ObRootService::create_index
函式來處理這個請求,這個介面做完必要的同步處理後,就向中控 obs 傳送 rpc 的回包,但注意,此時索引構建還未完成,實際 rs 會透過非同步任務來真正推進索引構建。中控 obs 收到 rs 的回包後,會不斷查詢索引表的 schema 狀態來獲取建索引的結果,如果構建成功,則向客戶端返回構建成功,如果構建失敗,則向客戶端返回失敗的錯誤碼。
上文說到 rs 在向中控 obs 回包前做了一些同步處理,本節我們先看下這部分的具體流程。呼叫路徑:
ObRootService::create_index -> ObIndexBuilder::create_index -> ObIndexBuilder::create_index::do_create_index -> ObIndexBuilder::do_create_local_index
-> ObIndexBuilder::do_create_global_index
全域性(global)索引和區域性(local)索引的區別是什麼?其實最主要的區別是區域性索引是分割槽級的,即索引表的分割槽一一對應主表的分割槽,全域性索引是表級的,即全域性索引表的分割槽與主表的分割槽沒有對應關係。
換句話說,local 指的是分割槽級的 local,global 指的是表級的 global。例如,t1 表有兩個 hash 分割槽,如果建區域性索引 i1,則 i1 一定有兩個分割槽,並且 i1 的第一個分割槽是對 t1 的第一個分割槽的索引,i1 的第二個分割槽是對 t1 的第二個分割槽的索引。如果對 t1 建全域性索引 i2,則 i2 可以有一個分割槽,也可以有多個分割槽,且分割槽不與主表一一對應。
因為區域性索引跟主表的分割槽是一一對應的,所以在 OceanBase 中,我們將區域性索引的分割槽與主表的分割槽緊密繫結在一起,這樣主表分割槽和索引表分割槽的 location 資訊是一致的(一定在一臺機器上),從而避免跨機的分散式事務。也因此,上述選擇索引構建路徑時,對全域性索引有個最佳化,如果全域性索引的主表和索引表都是非分割槽表,那這種全域性索引可以走區域性索引的構建流程。
ObIndexBuilder::do_create_global_index -> ObIndexBuilder::generate_schema
-> ObDDLService::create_global_index -> ObDDLService::generate_global_index_locality_and_primary_zone
-> ObDDLService::create_user_table -> ObDDLService::create_table_in_trans -> ObDDLOperator::create_table
-> ObDDLService::create_table_partitions
-> ObDDLService::publish_schema
-> ObIndexBuilder::submit_build_global_index_task -> ObGlobalIndexBuilder::submit_build_global_index_task
ObIndexBuilder::generate_schema 負責生成索引表 schema 的基礎資訊,表級資訊主要繼承自主表,索引表主要考慮列資訊,正常索引只要帶索引列和主鍵列,重複的主鍵列省掉。唯一索引需要帶索引列,隱藏主鍵列和主鍵列。什麼是隱藏主鍵,隱藏主鍵是為了解索引列值為 null 時的比較問題。
因為 sql 語義中,null 與null 是不相等的,但在程式碼比較中 null 與 null 的數值是相等的,所以如果索引列存在 null 時,隱藏主鍵列會填上主鍵的具體值,如果索引列不為 null,則隱藏主鍵列填null 值,索引鍵比較時帶上隱藏主鍵列就可以實現 null 與 null 不相等的語義。generate_schema 只是生成列索引表 schema 的記憶體物件,此時索引表還不可用,所以在 schema 中將索引表的狀態置為 INDEX_STATUS_UNAVAILABLE。
生成好索引表的 schema 後,需要將schema寫入內部表,這一步是透過
ObDDLOperator::create_table
完成的。
然後還需要在相關機器建立索引表的的記憶體結構,因此透過
ObDDLService::generate_global_index_locality_and_primary_zone
生成了索引表的位置資訊,透過 ObDDLService::create_table_partitions 向目標機器傳送 rpc,通知他們建立索引表的各個分割槽的記憶體結構,包括 memtable,table_store,以及 partition_key 到 table_store 的對映等。然後,透過ObDDLService::publish_schema 通知其他機器重新整理 schema。
完成上述索引表的 schema 和記憶體結構的建立後,透過ObGlobalIndexBuilder::submit_build_global_index_task 提交全域性索引的資料補全的控制任務到佇列中,後續透過該控制任務,推進全域性索引的資料補全流程。
在提交該控制任務同時,submit_build_global_index_task 會在__all_index_build_stat 中建立一條任務記錄,之後該控制任務的狀態推進也會更新到__all_index_build_stat 表中。
全域性索引控制任務由 ObGlobalIndexBuilder 負責執行,這個執行緒池只有1個執行緒,佇列長度受限於記憶體(未設定記憶體上限),任務執行的入口是ObGlobalIndexBuilder::run3 -> ObGlobalIndexBuilder::try_drive。
ObIndexBuilder::do_create_local_index -> ObIndexBuilder::generate_schema
-> ObDDLService::create_user_table -> ObDDLService::create_table_in_trans -> ObDDLOperator::create_table
-> ObDDLService::create_table_partitions
-> ObDDLService::publish_schema
-> ObIndexBuilder::submit_build_local_index_task -> ObRSBuildIndexScheduler::push_task
區域性索引生成 schema 以及建立記憶體物件的的邏輯跟全域性索引幾乎相同,唯一的區別是區域性索引不需要生成索引表的位置資訊,其他邏輯這裡不再贅述。
完成上述索引表的 schema 和記憶體結構的建立後,透過ObRSBuildIndexScheduler::push_task將區域性索引的控制任務ObRSBuildIndexTask放入佇列,同時更新內部表__all_index_build_stat。
區域性索引的控制任務由ObDDLTaskExecutor負責執行,這個 executor 只有1個執行緒,佇列長度受限於記憶體(記憶體上限為1G),任務執行的入口是ObDDLTaskExecutor::run1 -> ObRSBuildIndexTask::process。
全域性索引的控制任務 ObGlobalIndexTask 設計了一個簡單的狀態推進,對每種任務狀態,執行相應的函式。整體思路是,先在索引表的一個副本上構建基線資料,然後將基線資料複製到其他副本,再執行必要的一致性和唯一性檢查,最後讓索引生效。
程式碼路徑:
process _function task _status
----------------------------------------------------------------------------
ObGlobalIndexBuilder:: try _drive -> try _build _single _replica GIBS _BUILD _SINGLE _REPLICA
-> try _copy _multi _replica GIBS _MULTI _REPLICA _COPY
-> try _unique _index _calc _checksum GIBS _UNIQUE _INDEX _CALC _CHECKSUM
-> try _unique _index _check GIBS _UNIQUE _INDEX _CHECK
-> try _handle _index _build _take _effect GIBS _INDEX _BUILD _TAKE _EFFECT
-> try _handle _index _build _failed GIBS _INDEX _BUILD _FAILED
-> try _handle _index _build _finish GIBS _INDEX _BUILD _FINISH
單副本構建是指在一個副本上完成索引表基線資料的補全,根據 OceanBase 的 lsm-tree 的結構,這裡的基線資料是指建索引表的 major sstable。
程式碼路徑:
ObGlobalIndexBuilder::try_build_single_replica -> launch_new_build_single_replica -> get_global_index_build_snapshot -> do_get_associated_snapshot
-> hold_snapshot
-> update_task_global_index_build_snapshot
-> do_build_single_replica -> ObRootService::submit_index_sstable_build_task
-> drive_this_build_single_replica -> ObIndexChecksumOperator::check_column_checksum
單副本構建首先要選一個快照點,保證該快照點之後,對主表的 dml 操作(增量資料)都可以看到該索引表,這意味著快照點之後的 DML 操作都會同步修改索引表,但對查詢操作來說,該索引表不可用,這種 write-only 的行為其實是 OceanBase 實現線上建索引的關鍵。
基於該快照點構建基線資料(存量資料)後,又因為 lsm-tree 的查詢會 fuse 多層的資料,所以可以透過冪等性保證索引表資料的完整性。為了拿到該快照點,假設建立索引表時 schema_version 是v1,那麼需要等待所有依賴 schema_version <= v1 的事務都結束。do_get_associated_snapshot 函式就是傳送 rpc 給主表分割槽的leader,詢問這些事務是否都已經結束,收到該請求的 obs 透過ObService::check_schema_version_elapsed介面處理,之後do_get_associated_snapshot透過 wait_all 等待所有 rpc 的返回,注意這裡其實是批次的同步 rpc,所以分割槽數特別多時,可能會阻塞索引任務推動執行緒。
為了保證單副本構建過程中,選中的快照點不被釋放,需要 hold 住該快照點,注意,如果這裡 hold 住快照的時間過長的話,可能會導致 table_store 的數量爆掉。然後將選好的構建快照點更新到內部表__all_index_build_stat中。最後提交一個索引表基線資料的構建任務 ObIndexSSTableBuildTask。
基線補全任務提交後,透過 drive_this_build_single_replica 不斷檢查基線補全的任務狀態,如果基線構建完成,則透過 checksum 校驗來檢查主表和索引表資料的一致性。
ObIndexSSTableBuildTask 任務由 IdxBuild 執行緒池負責執行,任務佇列4096,執行緒數16。
看下 ObIndexSSTableBuildTask 的執行過程, 程式碼路徑:
ObIndexSSTableBuildTask::process -> ObIndexSSTableBuilder::init
-> ObIndexSSTableBuilder::build -> ObCommonSqlProxy::execute -> ObInnerSQLConnection::execute -> ObInnerSQLConnection::query -> ObInnerSQLConnection::do_query -> ObIndexSSTableBuilder::ObBuildExecutor::execute -> ObIndexSSTableBuilder::build
-> ObIndexSSTableBuilder::ObBuildExecutor::process_result -> ObResultSet::get_next_row
-> ObGlobalIndexBuilder::on_build_single_replica_reply
ObIndexSSTableBuilder::build 函式是同步執行,也就是說,系統中最多有16個基線補全的任務同時執行,執行結束後,透過on_build_single_replica_reply更改基線補全的任務狀態。
上述程式碼路徑看似複雜,其實最終是透過 ObIndexSSTableBuilder::build 構建了一個物理執行計劃,透過 ObResultSet::get_next_row 來執行該計劃,下面的程式碼路徑給出了物理執行計劃的生成過程,PHY 開頭的常量是指物理運算元的型別。
ObIndexSSTableBuilder::build -> generate_build_param -> split_ranges
-> store_build_param
-> gen_data_scan PHY_TABLE_SCAN_WITH_CHECKSUM
PHY_UK_ROW_TRANSFORM
-> gen_data_exchange PHY_DETERMINATE_TASK_TRANSMIT
PHY_TASK_ORDER_RECEIVE
-> gen_build_macro PHY_SORT
PHY_APPEND_LOCAL_SORT_DATA
-> gen_macro_exchange PHY_DETERMINATE_TASK_TRANSMIT
PHY_TASK_ORDER_RECEIVE
-> gen_build_sstable PHY_APPEND_SSTABLE
-> gen_sstable_exchange PHY_DETERMINATE_TASK_TRANSMIT
PHY_TASK_ORDER_RECEIVE
coordinator | ObTaskOrderReceive
transmit | ObDeterminateTaskTransmit
append_sstable | ObTableAppendSSTable
receive | ObTaskOrderReceive
transmit_macro_block | ObDeterminateTaskTransmit
append_local_sort_data | ObTableAppendLocalSortData
sort | ObSort
receive | ObTaskOrderReceive
transmit_by_range | ObDeterminateTaskTransmit
table_scan_with_checksum | ObTableScanWithChecksum
4.3 多副本複製
ObGlobalIndexBuilder::try_copy_multi_replica -> launch_new_copy_multi_replica -> build_task_partition_sstable_stat -> generate_task_partition_sstable_array
-> drive_this_copy_multi_replica -> check_partition_copy_replica_stat
-> build_replica_sstable_copy_task -> ObCopySSTableTask::build
-> ObRebalanceTaskMgr::add_task
4.4 唯一性校驗
對於唯一索引,需要校驗索引列資料的唯一性,非唯一索引不需要執行該校驗。程式碼路徑:
O bGlobalIndexBuilder::try_unique_index_calc_checksum -> launch_new_unique_index_calc_checksum -> get_checksum_calculation_snapshot -> do_get_associated_snapshot
-> do_checksum_calculation -> build_task_partition_col_checksum_stat
-> send_checksum_calculation_request -> send_col_checksum_calc_rpc
-> drive_this_unique_index_calc_checksum
為了校驗唯一性,需要選一個快照點,在此快照點之後,對主表的 dml 操作(增量資料)都可以看到該索引表的基線,因此可以在 dml 過程中校驗唯一性,在此快照點之前的資料(存量資料),計算該快照點的主表和索引表列 checksum,透過 checksum 比對,校驗其唯一性。該快照點,需要所有副本的新事務都可以看到基線資料,假設各副本看到基線資料的時間戳的最大值是 sstable_ts,則需要等待所有事務的事務上下文建立時間戳推過 sstable_ts。get_checksum_calculation_snapshot 函式完成上述操作,檢查事務上下文建立時間戳是否推過 sstable_ts 的函式入口是 ObPartitionService::check_ctx_create_timestamp_elapsed。
如果上述步驟全部成功完成,則透過 ObGlobalIndexBuilder::try_handle_index_build_take_effect 函式使索引生效,實際是修改索引表的 schema 狀態為 INDEX_STATUS_AVAILABLE,中控 obs 看到該狀態後,向客戶端 session 返回成功。
如果上述任一步驟失敗,則透過函式將索引表狀態改為 INDEX_STATUS_INDEX_ERROR,中控 obs 看到該狀態後,向客戶端 session 返回索引構建失敗。
ObGlobalIndexBuilder::try_handle_index_build_finish -> clear_intermediate_result -> ObIndexSSTableBuilder::clear_interm_result
-> release_snapshot
ObRSBuildIndexTask::process -> wait_trans_end -> ObIndexWaitTransStatus::get_wait_trans_status
-> calc_snapshot_version
-> acquire_snapshot
-> wait_build_index_end -> report_index_status
-> report_index_status
-> release_snapshot
5.1 任務觸發
因為區域性索引的分割槽與主表的分割槽一一繫結,所以區域性索引構建流程的主戰場就在主表分割槽所在的 obs 上。obs 透過監測每個租戶的 ddl 變更,來觸發構建區域性索引的任務:在索引表的 schema 釋出後,主表所在的 obs 會重新整理到該 schema,然後發起區域性索引構建任務。
程式碼路徑:
ObTenantDDLCheckSchemaTask::process -> process_schedule_build_index_task -> get_candidate_tables
-> find_build_index_partitions
-> generate_schedule_index_task -> ObBuildIndexScheduler::push_task(ObBuildIndexScheduleTask)
ObTenantDDLCheckSchemaTask 會找到要構建索引的 partition_key ,然後生成一個 ObBuildIndexScheduleTask,放入 ObBuildIndexScheduler 的ObDDLTaskExecutor 中執行。這個 executor 有4個執行緒,佇列長度受限於記憶體,任務佇列的記憶體上限為1GB。
5.2 區域性索引構建
ObBuildIndexScheduleTask::process -> check_partition_need_build_index
-> wait_trans_end -> check_trans_end -> ObPartitionService::check_schema_version_elapsed
-> report_trans_status
-> wait_snapshot_ready -> get_snapshot_version
-> check_rs_snapshot_elapsed -> ObTsMgr::wait_gts_elapse
-> ObPartitionService::check_ctx_create_timestamp_elapsed
-> choose_build_index_replica -> get_candidate_source_replica
-> check_need_choose_replica
-> ObIndexTaskTableOperator::generate_new_build_index_record
-> wait_choose_or_build_index_end -> get_candidate_source_replica
-> check_need_schedule_dag
-> schedule_dag -> ObPartitionStorage::get_build_index_param
-> ObPartitionStorage::get_build_index_context
-> ObBuildIndexDag::init
-> alloc_index_prepare_task -> ObIndexPrepareTask::init
-> ObIDag::add_task
-> ObDagScheduler::add_dag
-> copy_build_index_data -> send_copy_replica_rpc
-> ObPartitionService::check_single_replica_major_sstable_exist
-> unique_index_checking -> ObUniqueCheckingDag::init
-> ObUniqueCheckingDag::alloc_local_index_task_callback
-> ObUniqueCheckingDag::alloc_unique_checking_prepare_task -> ObUniqueCheckingPrepareTask::init
-> ObIDag::add_task
-> ObDagScheduler::add_dag
-> wait_report_status -> check_all_replica_report_build_index_end
如果大家有任何疑問,歡迎留言討論:
釘釘群:33254054
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69909943/viewspace-2846950/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 一文讀懂圖資料庫 Nebula Graph 訪問控制實現原理資料庫
- 一文讀懂 OceanBase 資料庫的SLog日誌資料庫
- 從原始碼解讀Category實現原理原始碼Go
- 一文讀懂資料庫發展史資料庫
- 一文讀懂如何實施資料治理?
- 開源資料庫OceanBase原始碼解讀(九):tableAPI和OB多模型資料庫原始碼API模型
- 一文讀懂大資料實時計算大資料
- 一文讀懂資料庫70年發展史資料庫
- 一文讀懂git核心工作原理Git
- 一文讀懂Go Http Server原理GoHTTPServer
- 從程式碼層讀懂 Java HashMap 的實現原理JavaHashMap
- 一文徹底讀懂 hystrix-go 原始碼Go原始碼
- 一文讀懂「Attention is All You Need」| 附程式碼實現
- 一文讀懂隨機森林的解釋和實現隨機森林
- 一文讀懂 Kubernetes APIServer 原理APIServer
- Axios 原始碼解讀 —— 原始碼實現篇iOS原始碼
- Laravel7——一文讀懂中介軟體原始碼Laravel原始碼
- 一文讀懂支援向量機SVM(附實現程式碼、公式)公式
- HTML5(十二)——一文讀懂 WebSocket 原理HTMLWeb
- 一文讀懂DHCP的工作原理和作用
- 【React原始碼解讀】- 元件的實現React原始碼元件
- vuex 2.*原始碼解析—— 花半小時就可以讀懂vuex實現原理Vue原始碼
- 一文讀懂mavenMaven
- OceanBase 原始碼解讀(九):儲存層程式碼解讀之「巨集塊儲存格式」原始碼
- 一文讀懂機器學習大殺器XGBoost原理機器學習
- 一文讀懂微搭低程式碼
- 一文讀懂知識圖譜與向量資料庫的異同資料庫
- HashMap 基礎原理以及原始碼解讀HashMap原始碼
- 一文讀懂遊戲資料分析指標的應用遊戲指標
- 一文讀懂SpringMVC中的資料繫結SpringMVC
- 一文讀懂資料鑽取-騰訊雲開發者社群
- 一文讀懂微核心
- 一文讀懂特徵工程特徵工程
- 國產資料庫oceanBbase,達夢,金倉與mysql資料庫的效能對比 七、python讀oceanBase資料庫資料庫MySqlPython
- OceanBase 原始碼解讀(五):租戶的一生原始碼
- OceanBase 原始碼解讀(三):分割槽的一生原始碼
- 一文讀懂選擇資料湖還是資料倉儲
- PostgreSQL 原始碼解讀(1)- 插入資料#1SQL原始碼