OceanBase 原始碼解讀(七):一文讀懂資料庫索引實現原理

OceanBase資料庫發表於2021-12-10

此前,帶你讀原始碼第六篇 《戳這裡回顧: OceanBase 原始碼解讀(六):儲存引擎詳解 為大家詳細講解了  OceanBase  儲存引擎,併為大家 回答了關於 OceanBase 資料庫的相關提問。

本期“原始碼解讀”將嘗試從程式碼導讀的角度,簡要介紹 OceanBase 的索引構建流程,帶領大家讀懂索引構建的相關程式碼,必須速速收藏!

 
1. 什麼是索引


首先請思考,在一個一般的資料庫中,索引表的語義是什麼?

索引表,其實是在主表(也稱資料表)之外,再建立一份冗餘且有序的資料,用於加速某些查詢流程。為什麼索引表可以加速查詢,是因為索引表是按照索引鍵排序的,如果某個查詢語句的查詢條件中指定了索引字首,那麼透過二分查詢即可快速找到對應的行。索引表的行中包括了主鍵資訊,所以可以透過主鍵快速找到主表的完整行,這個過程也叫索引回表。

知道了索引表的語義,那建立索引要做哪些事呢?

其實索引表也跟主表一樣,有自己的 schema、記憶體結構和持久化的資料(通常存放於本地磁碟上)。在分散式場景下,還可能有自己的位置資訊。那建立索引也就是要建立索引表的 schema,然後在某個位置建立索引表的記憶體結構,建立索引表的持久化資料。

在建立索引的過程中,我們不希望影響主表的正常讀寫,即建索引過程中業務是線上(online)的。為了做到線上建索引,不同廠商對線上的有不同的實現機制,本文會介紹 OceanBase 是如何實現線上建索引的,對其他方案感興趣的同學可以自行參考相關文件。


2. 索引建立過程總覽


2.1 使用者視角
我們先從使用者的視角看一下索引構建的過程是怎樣的。例如:使用者在一個 session 上傳送了建索引的語句 create index i1 on t1(c2),然後使用者就在當前 session 上不斷等待,直到索引構建成功或者失敗。

2.2 中控 observer 的視角 

那在 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 狀態來獲取建索引的結果,如果構建成功,則向客戶端返回構建成功,如果構建失敗,則向客戶端返回失敗的錯誤碼。


3. RS的同步處理流程


上文說到 rs 在向中控 obs 回包前做了一些同步處理,本節我們先看下這部分的具體流程。呼叫路徑:

ObRootService::create_index -> ObIndexBuilder::create_index -> ObIndexBuilder::create_index::do_create_index -> ObIndexBuilder::do_create_local_index
                                                                                                            -> ObIndexBuilder::do_create_global_index


上述的呼叫過程會做一些防禦性的檢查,例如系統表、回收站中的表,OceanBase 是不支援建索引的,如果一個表的索引數量超過上限,也是不允許建索引的。透過檢查後,根據索引型別選擇走 local 還是 global 的構建索引流程。

3.1 global 和 local 的概念 

全域性(global)索引和區域性(local)索引的區別是什麼?其實最主要的區別是區域性索引是分割槽級的,即索引表的分割槽一一對應主表的分割槽,全域性索引是表級的,即全域性索引表的分割槽與主表的分割槽沒有對應關係。

換句話說,local 指的是分割槽級的 local,global 指的是表級的 global。例如,t1 表有兩個 hash 分割槽,如果建區域性索引 i1,則 i1 一定有兩個分割槽,並且 i1 的第一個分割槽是對 t1 的第一個分割槽的索引,i1 的第二個分割槽是對 t1 的第二個分割槽的索引。如果對 t1 建全域性索引 i2,則 i2 可以有一個分割槽,也可以有多個分割槽,且分割槽不與主表一一對應。

因為區域性索引跟主表的分割槽是一一對應的,所以在 OceanBase 中,我們將區域性索引的分割槽與主表的分割槽緊密繫結在一起,這樣主表分割槽和索引表分割槽的 location 資訊是一致的(一定在一臺機器上),從而避免跨機的分散式事務。也因此,上述選擇索引構建路徑時,對全域性索引有個最佳化,如果全域性索引的主表和索引表都是非分割槽表,那這種全域性索引可以走區域性索引的構建流程。


3.2 生成全域性索引的控制任務 
關鍵函式的呼叫路徑:

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。


3.3 生成區域性索引的控制任務 
關鍵函式的呼叫路徑:

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。


4. 全域性索引的構建流程


全域性索引的控制任務 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


4.1 單副本構建

單副本構建是指在一個副本上完成索引表基線資料的補全,根據 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 校驗來檢查主表和索引表資料的一致性。


4.2 基線補全

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


多副本複製是把單副本構建流程中構建好的基線資料複製到其他副本的過程,實際資料複製是透過 ObCopySSTableTask 完成的,該任務被rs的 ObRebalanceTaskMgr 排程執行,入口在 ObCopySSTableTask::execute,實際上是傳送 copy_sstable_batch 的rpc,收到該rpc的obs的執行入口是ObService::copy_sstable_batch。完成基線資料複製任務後,obs 向 rs 彙報結果,rs 執行回撥 ObGlobalIndexBuilder::on_copy_multi_replica_reply 更新多副本複製任務的狀態。

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。

拿到快照點後,傳送 rpc 請主表和索引表的 leader 計算改快照點的列校驗和,收到該 rpc 的 obs 的處理入口是 ObService::calc_column_checksum_request。計算完成後,將列校驗和記錄在內部表 __all_index_checksum中,並透過 rpc 通知 rs,rs 執行回撥 ObGlobalIndexBuilder::on_col_checksum_calculation_reply,更新 checksum 計算任務的狀態。drive_this_unique_index_calc_checksum 不斷檢查checksum 計算任務的狀態,如果 checksum 全部計算完成,則透過ObGlobalIndexBuilder::try_unique_index_check -> ObIndexChecksumOperator::check_column_checksum執行校驗和的比對。

4.5 索引狀態變更

如果上述步驟全部成功完成,則透過 ObGlobalIndexBuilder::try_handle_index_build_take_effect 函式使索引生效,實際是修改索引表的 schema 狀態為 INDEX_STATUS_AVAILABLE,中控 obs 看到該狀態後,向客戶端 session 返回成功。

如果上述任一步驟失敗,則透過函式將索引表狀態改為 INDEX_STATUS_INDEX_ERROR,中控 obs 看到該狀態後,向客戶端 session 返回索引構建失敗。


4.6 中間結果清理
索引構建結束後,無論結果是成功或是失敗,都要執行中間狀態清理,包括清理 sql 執行的中間結果,釋放快照,清理內部表等。程式碼路徑:

ObGlobalIndexBuilder::try_handle_index_build_finish -> clear_intermediate_result -> ObIndexSSTableBuilder::clear_interm_result
                                                                                -> release_snapshot


5. 區域性索引的構建流程


區域性索引的 rs 端控制流程比較簡單,這是因為 rs 端不是主要戰場。程式碼路徑:

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。

那這個監測任務是如何產生的呢?在一臺 obs 的核心服務 partition_service 啟動時,會啟動子服務 ObBuildIndexScheduler,ObBuildIndexScheduler中有個定時任務 ObCheckTenantSchemaTask,不斷生成各租戶的 ObTenantDDLCheckSchemaTask,也放到 ObBuildIndexScheduler 的 ObDDLTaskExecutor 中執行,詳見 ObCheckTenantSchemaTask::runTimerTask。

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


區域性索引構建的整體流程跟全域性索引類似,也是先等事務結束,拿到快照點,然後選擇一個副本做單副本構建,等單副本構建完成後,複製基線資料到其他副本,然後(對唯一索引)做唯一性檢查,之後索引生效。其中基線資料的構建透過ObBuildIndexDag 完成,唯一性的檢查透過 ObUniqueCheckingDag 完成。

如果大家有任何疑問,歡迎留言討論:

釘釘群:33254054


往期推薦:

如何更快上手使用 OceanBase 社群版?

第一屆 OceanBase 技術徵文大賽來啦!

新成就!OceanBase 入選 Forrester 首份分散式資料庫報告

50強誕生!2021 OceanBase 資料庫大賽百所高校爭霸!

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

相關文章