写点什么

详解 openGauss 多线程架构启动过程

  • 2022 年 6 月 22 日
  • 本文字数:4913 字

    阅读完需:约 16 分钟

本文分享自华为云社区《openGauss内核分析(一):openGauss 多线程架构启动过程详解》,作者:Gauss 松鼠会。


openGauss 数据库自 2020 年 6 月 30 日开源以来,吸引了众多内核开发者的关注。那么 openGauss 的多线程是如何启动的,一条 SQL 语句在 SQL 引擎,执行引擎和存储引擎的执行过程是怎样的,酷哥做了一些总结,第一期内容主要分析 openGauss 多线程架构启动过程。


openGauss 数据库是一个单进程多线程的数据库,客户端可以使用 JDBC/ODBC/Libpq/Psycopg 等驱动程序,向 openGauss 的主线程(Postmaster)发起连接请求。


openGauss 为什么要使用多线程架构


随着计算机领域多核技术的发展,如何充分有效的利用多核的并行处理能力,是每个服务器端应用程序都必须考虑的问题。由于数据库服务器的服务进程或线程间存在着大量数据共享和同步,而多线程可以充分利用多 CPU 来并行执行多个强相关任务,例如执行引擎可以充分的利用线程的并发执行以提供性能。在多线程的架构下,数据共享的效率更高,能提高服务器访问的效率和性能,同时维护开销和复杂度更低,这对于提高数据库系统的并行处理能力非常重要。

多线程的三大主要优势:


优势一:线程启动开销远小于进程启动开销。与进程相比,它是一种非常“节俭”的多任务操作方式。在 Linux 系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种“昂贵”的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间。


优势二:线程间方便的通信机制:对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。


优势三:线程切换开销小于进程切换开销,对于 Linux 系统来讲,进程切换分两步:1.切换页目录以使用新的地址空间;2.切换内核栈和硬件上下文。对线程切换,第 1 步是不需要做的,第 2 步是进程和线程都要做的,所以明显线程切换开销小。

openGauss 主要线程有哪些



数据库启动后,可以通过操作系统命令 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 函数完成线程的启动。



1、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);}
复制代码


2、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>,......};
复制代码


3、在 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 循环,进行查询执行                  


  • 调用 pgstat_report_activity()、pgstat_report_waitstatus(),告诉统计系统后台线程正处于 idle 状态

  • 设置全局变量 DoingCommandRead = true

  • 调用 ReadCommand(),读取客户端 SQL 语句

  • 设置全局变量 DoingCommandRead=false

  • 若在上述过程中收到 SIGHUP 信号,表示线程需要重新加载修改过的 postgresql.conf 配置文件

  • 进入 switch (firstchar),根据接收到的信息进行分支判断

思考如何新增一个辅助线程


参考其他线程完成



点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 3
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
详解openGauss多线程架构启动过程_数据库_华为云开发者联盟_InfoQ写作社区