openGauss資料庫自2020年6月30日開源以來,吸引了衆多核心開發者的關注。那麼openGauss的多線程是如何啟動的,一條SQL語句在 SQL引擎,執行引擎和存儲引擎的執行過程是怎樣的,酷哥做了一些總結,第一期内容主要分析openGauss 多線程架構啟動過程。
openGauss資料庫是一個單程序多線程的資料庫,用戶端可以使用JDBC/ODBC/Libpq/Psycopg等驅動程式,向openGauss的主線程(Postmaster)發起連接配接請求。
openGauss為什麼要使用多線程架構
随着計算機領域多核技術的發展,如何充分有效的利用多核的并行處理能力,是每個伺服器端應用程式都必須考慮的問題。由于資料庫伺服器的服務程序或線程間存在着大量資料共享和同步,而多線程可以充分利用多CPU來并行執行多個強相關任務,例如執行引擎可以充分的利用線程的并發執行以提供性能。在多線程的架構下,資料共享的效率更高,能提高伺服器通路的效率和性能,同時維護開銷和複雜度更低,這對于提高資料庫系統的并行處理能力非常重要。
多線程的三大主要優勢:
優勢一:線程啟動開銷遠小于程序啟動開銷。與程序相比,它是一種非常“節儉”的多任務操作方式。在Linux系統下,啟動一個新的程序必須配置設定給它獨立的位址空間,建立衆多的資料表來維護它的代碼段、堆棧段和資料段,這是一種“昂貴”的多任務工作方式。而運作于一個程序中的多個線程,它們彼此之間使用相同的位址空間,共享大部分資料,啟動一個線程所花費的空間遠遠小于啟動一個程序所花費的空間。
優勢二:線程間友善的通信機制:對不同程序來說,它們具有獨立的資料空間,要進行資料的傳遞隻能通過通信的方式進行,這種方式不僅費時,而且很不友善。線程則不然,由于同一程序下的線程之間共享資料空間,是以一個線程的資料可以直接為其他線程所用,這不僅快捷,而且友善。
優勢三:線程切換開銷小于程序切換開銷,對于Linux系統來講,程序切換分兩步:1.切換頁目錄以使用新的位址空間;2.切換核心棧和硬體上下文。對線程切換,第1步是不需要做的,第2步是程序和線程都要做的,是以明顯線程切換開銷小。
openGauss主要線程有哪些
背景線程 | 功能介紹 |
Postmaster 主線程 | 入口函數PostmasterMain,主要負責記憶體、全局資訊、信号、線程池等的初始化,啟動輔助線程并監控線程狀态,循環**接收新的連接配接 |
Walwriter 日志寫線程 | 入口函數WalWriterMain,将記憶體的預寫日志頁資料重新整理到預寫日志檔案中,保證已送出的事物永久記錄,不會丢失 |
Startup 資料庫啟動線程 | 入口函數StartupProcessMain,資料庫啟動時Postmaster主線程拉起的第一個子線程,主要完成資料庫的日志REDO(重做)操作,進行資料庫的恢複。日志REDO操作結束,資料庫完成恢複後,如果不是備機,Startup線程就退出了。如果是備機,那麼Startup線程一直在運作,REDO備機接收到新的日志 |
Bgwriter 背景資料寫線程 | 入口函數BackgroundWriterMain,對共享緩沖區的髒頁資料進行下盤 |
PageWriter | 入口函數ckpt_pagewriter_main,将髒頁資料拷貝至雙寫區域并落盤 |
Checkpointer 檢查點線程 | 入口函數CheckpointerMain,周期性檢查點,所有資料檔案被更新,将資料髒頁重新整理到磁盤,確定資料庫一緻;崩潰回複後,做過checkpointer更改不需要從預寫日志中恢複 |
StatCollector 統計線程 | 入口函數PgstatCollectorMain,統計資訊,包括對象、sql、會話、鎖等,儲存到pgstat.stat檔案中,用于性能、故障、狀态分析 |
WalSender 日志發送線程 | 入口函數WalSenderMain,主機發送預寫日志 |
WalReceiver 日志接收線程 | 入口函數WalReceiverMain,備機接收預寫日志 |
Postgres 業務處理線程 | 入口函數PostgresMain:處理用戶端連接配接請求,執行相關SQL業務 |
資料庫啟動後,可以通過作業系統指令ps檢視線程資訊(程序号為17012)
openGauss啟動過程
下面主要介紹openGauss資料庫的啟動過程,包括主線程,輔助線程及業務處理線程的啟動過程
gs_ctl啟動資料庫
gs_ctl是openGauss提供的資料庫服務控制工具,可以用來啟停資料庫服務和查詢資料庫狀态。主要供資料庫管理子產品調用,啟動資料庫使用如下指令
gs_ctl start -D /opt/software/data -Z single_node
gs_ctl的入口函數在“src/bin/pg_ctl/pg_ctl.cpp”,gs_ctl程序fork一個程序來運作 gaussdb程序,通過shell指令啟動。
上圖中的cmd為“/opt/software/openGauss/bin/gaussdb -D /opt/software/openGauss/data”,進入到資料庫運作調用的第一個函數是main函數,在“src/gausskernel/process/main/main.cpp”檔案中,在main.cpp檔案中,主要完成執行個體Context(上下文)的初始化、本地化設定,根據main.cpp檔案的入口參數調用BootStrapProcessMain函數、GucInfoMain函數、PostgresMain函數和PostmasterMain函數。BootStrapProcessMain函數和PostgresMain函數是在initdb場景下初始化資料庫使用的。GucInfoMain函數作用是顯示GUC(grand unified configuration,配置參數,在資料庫中指的是運作參數)參數資訊。正常的資料庫啟動會進入PostmasterMain函數。下面對這個函數進行更詳細的介紹。
- MemoryContextInit:記憶體上下文系統初始化,主要完成對ThreadTopMemoryContext,ErrorContext,AlignContext和ProfileLogging等全局變量的初始化
- pg_perm_setlocale:設定程式語言環境相關的全局變量
- check_root: 确認程式運作者無作業系統的root權限,防止的意外檔案覆寫等問題
- 如果gaussdb後的第一個參數是—boot,則進行資料庫初始化,如果gaussdb後的第一個參數是--single,則調用PostgresMain(),進入(本地)單使用者版服務端程式。之後,與普通伺服器端線程類似,循環等待使用者輸入SQL語句,直至使用者輸入EOF(Ctrl+D),退出程式。如果沒有指定額外啟動選項,程式進入PostmasterMain函數,開始一系列伺服器端的正常初始化工作。
PostmasterMain函數
下面具體介紹PostmasterMain。
- 設定線程号相關的全局變量MyProcPid、PostmasterPid、MyProgName和程式運作環境相關的全局變量IsPostmasterEnvironment
- 調用postmaster_mem_cxt = AllocSetContextCreate(t_thrd.top_mem_cxt,...),在目前線程的top_mem_cxt下建立postmaster_mem_cxt全局變量和相應的記憶體上下文
- MemoryContextSwitchTo(postmaster_mem_cxt)切換到postmaster_mem_cxt記憶體上下文
- 調用getInstallationPaths(),設定my_exec_path(一般即為gaussdb可執行檔案所在路徑)
- 調用InitializeGUCOptions(),根據代碼中各個GUC參數的預設值生成ConfigureNamesBool、ConfigureNamesInt、ConfigureNamesReal、ConfigureNamesString、ConfigureNamesEnum等 GUC參數的全局變量數組,以及統一管理GUC參數的guc_variables、num_guc_variables、size_guc_variables全局變量,并設定與具體作業系統環境相關的GUC參數
- while (opt = ...) SetConfigOption, 若在啟動gaussdb時用指定了非預設的GUC參數,則在此時加載至上一步中建立的全局變量中
- 調用checkDataDir(),确認資料庫安裝成功以及PGDATA目錄的有效性
- 調用CreateDataDirLockFile(),建立資料目錄的鎖檔案
- 調用process_shared_preload_libraries(),處理預加載庫
- 為每個ListenSocket建立**
- reset_shared,設定共享記憶體和信号,主要包括頁面緩存池、各種鎖緩存池、WAL日志緩存池、事務日志緩存池、事務(号)概況緩存池、各背景線程(鎖使用)概況緩存池、各背景線程等待和運作狀态緩存池、兩階段狀态緩存池、檢查點緩存池、WAL日志複制和接收緩存池、資料頁複制和接收緩存池等。在後續階段建立出的用戶端背景線程以及各個輔助線程均使用該共享記憶體空間,不再單獨開辟
- 将啟動時手動設定的GUC參數以檔案形式儲存下來,以供後續背景服務端線程啟動時使用
- 為不同信号設定handler
- 調用pgstat_init(),初始化狀态收集子系統;
- 調用load_hba(),加載pg_hba.conf檔案,該檔案記錄了允許連接配接(指定或全部)資料庫的用戶端實體機的位址和端口;調用load_ident(),加載pg_ident.conf檔案,該檔案記錄了作業系統使用者名與資料庫系統使用者名的對應關系,以便後續處理用戶端連接配接時的身份認證
- 調用 StartupPID = initialize_util_thread(STARTUP),進行資料一緻性校驗。對于服務端主機來說,檢視pg_control檔案,若上次關閉狀态為DB_SHUTDOWNED且recovery.conf檔案沒有指定進行恢複,則認為資料一緻性成立;否則,根據pg_control中檢查點的redo位置或者recovery.conf檔案中指定的位置,讀取WAL日志或歸檔日志進行replay(回放),直至資料達到預期的一緻性狀,主要函數StartupXLOG
- 最後進入ServerLoop()函數,循環響應用戶端連接配接請求
ServerLoop函數
下面來講ServerLoop函數主流程
- 調用gs_signal_setmask(&UnBlockSig, NULL)和gs_signal_unblock_sigusr2(),使得線程可以響應使用者或其它線程的、指定的信号集
- 每隔PM_POLL_TIMEOUT_MINUTE時間修改一次socket檔案和socket鎖檔案的通路和修改時間,以免**作系統淘汰
- 判斷線程狀态(pmState),若為PM_WAIT_DEAD_END,則休眠100毫秒,并且不接收任何連接配接;否則,通過系統調用poll()或select()來阻塞地讀取**端口上傳入的資料,最長阻塞時間PM_POLL_TIMEOUT_SECOND
- 調用gs_signal_setmask(&BlockSig, NULL)和gs_signal_block_sigusr2()不再接收外源信号
- 判斷poll()或select()函數的傳回值,若小于零,**出錯,服務端程序退出;若大于零,則建立連接配接ConnCreate(),并進入背景服務線程啟動流程BackendStartup()。對于父線程,即postmaster線程,在結束BackendStartup()的調用以後,會調用ConnFree(),清除連接配接資訊;若poll()或select()的傳回值為零,即沒有資訊傳入,則不進行任何操作
- 調用ADIO_RUN()、ADIO_END() ,若AioCompleters沒有啟動,則啟動之
- 檢查各個輔助線程的線程号是否為零,若為零,則調用initialize_util_thread啟動
以非線程池模式為例,介紹線程的啟動邏輯。BackendStartup函數是通過調用initialize_worker_thread(WORKE,port)建立一個背景線程處理客戶請求。背景線程的啟動函數initialize_util_thread和工作線程的啟動函數initialize_worker_thread,最後都是調用initialize_thread函數完成線程的啟動。
- initialize_thread函數調用gs_thread_create函數建立線程,調用InternalThreadFunc函數處理線程
ThreadId initialize_thread(ThreadArg* thr_argv)
{
gs_thread_t thread;
int error_code = gs_thread_create(&thread, InternalThreadFunc, 1, (void*)thr_argv);
if (error_code != 0) {
ereport(LOG,
(errmsg("can not fork thread[%s], errcode:%d, %m",
GetThreadName(thr_argv->m_thd_arg.role), error_code)));
gs_thread_release_args_slot(thr_argv);
return InvalidTid;
}
return gs_thread_id(thread);
}
- InternalThreadFunc函數根據角色調用GetThreadEntry函數,GetThreadEntry函數直接以角色為下标,傳回對應GaussdbThreadEntryGate數組對應的元素。數組的元素是處理具體任務的回調函數指針,指針指向的函數為GaussDbThreadMain
static void* InternalThreadFunc(void* args)
{
knl_thread_arg* thr_argv = (knl_thread_arg*)args;
gs_thread_exit((GetThreadEntry(thr_argv->role))(thr_argv));
return (void*)NULL;
}
GaussdbThreadEntry GetThreadEntry(knl_thread_role role)
{
Assert(role > MASTER && role < THREAD_ENTRY_BOUND);
return GaussdbThreadEntryGate[role];
}
static GaussdbThreadEntry GaussdbThreadEntryGate[] = {GaussDbThreadMain<MASTER>,
GaussDbThreadMain<WORKER>,
GaussDbThreadMain<THREADPOOL_WORKER>,
GaussDbThreadMain<THREADPOOL_LISTENER>,
......};
- 在GaussDbThreadMain函數中,首先初始化線程基本資訊,Context和信号處理函數,接着就是根據thread_role角色的不同調用不同角色的處理函數,進入各個線程的main函數,角色為WORKER會進入PostgresMain函數,下面具體介紹PostgresMain函數
PostgresMain函數
- process_postgres_switches(),加載傳入的啟動選項和GUC參數
- 為不同信号設定handler
- 調用sigdelset(&BlockSig, SIGQUIT),允許響應SIGQUIT信号
- 調用BaseInit(),初始化存儲管理系統和頁面緩存池計數
- 調用on_shmem_exit(),設定線程退出前需要進行的記憶體清理動作。這些清理動作構成一個連結清單(on_shmem_exit_list全局變量),每次調用該函數都向連結清單尾端添加一個節點,連結清單長度由on_shmem_exit_index記錄,且不可超過MAX_ON_EXITS宏。線上程退出時,從後往前調用各個節點中的動作(函數指針),完成清理工作
- 調用gs_signal_setmask (&UnBlockSig),設定屏蔽的信号類型
- 調用InitBackendWorker進行統計系統初始化、syscache初始化工作
- BeginReportingGUCOptions如有需要則列印GUC參數
- 調用on_proc_exit(),設定線程退出前需要進行的線程清理動作。設定和調用機制與on_shmem_exit()類似
- 調用process_local_preload_libraries(),處理GUC參數設定後的預加載庫
- AllocSetContextCreate建立MessageContext、RowDescriptionContext、MaskPasswordCtx上下文
- 調用sigsetjmp(),設定longjump點,若後續查詢執行中出錯,在某些情況下可以傳回此處重新開始
- 調用gs_signal_unblock_sigusr2(),允許線程響應指定的信号集
- 然後進入for循環,進行查詢執行
- 設定全局變量DoingCommandRead = true
- 調用ReadCommand(),讀取用戶端SQL語句
- 設定全局變量DoingCommandRead=false
- 若在上述過程中收到SIGHUP信号,表示線程需要重新加載修改過的postgresql.conf配置檔案
- 進入switch (firstchar),根據接收到的資訊進行分支判斷
- 調用pgstat_report_activity()、pgstat_report_waitstatus(),告訴統計系統背景線程正處于idle狀态
思考如何新增一個輔助線程
參考其他線程完成
涉及修改檔案 | Postmaster.cpp |
涉及修改函數 | GaussdbThreadGate – 定義 |