摘要:本文介绍openGauss数据库的启动过程,包括主线程,辅助线程及业务处理线程的启动过程。

本文分享自华为云社区《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函数。下面对这个函数进行更详细的介绍。

  1. MemoryContextInit:内存上下文系统初始化,主要完成对ThreadTopMemoryContext,ErrorContext,AlignContext和ProfileLogging等全局变量的初始化
  2. pg_perm_setlocale:设置程序语言环境相关的全局变量
  3. check_root: 确认程序运行者无操作系统的root权限,防止的意外文件覆盖等问题
  4. 如果gaussdb后的第一个参数是—boot,则进行数据库初始化,如果gaussdb后的第一个参数是--single,则调用PostgresMain(),进入(本地)单用户版服务端程序。之后,与普通服务器端线程类似,循环等待用户输入SQL语句,直至用户输入EOF(Ctrl+D),退出程序。如果没有指定额外启动选项,程序进入PostmasterMain函数,开始一系列服务器端的正常初始化工作。

PostmasterMain函数

下面具体介绍PostmasterMain。

  1. 设置线程号相关的全局变量MyProcPid、PostmasterPid、MyProgName和程序运行环境相关的全局变量IsPostmasterEnvironment
  2. 调用postmaster_mem_cxt = AllocSetContextCreate(t_thrd.top_mem_cxt,...),在目前线程的top_mem_cxt下创建postmaster_mem_cxt全局变量和相应的内存上下文
  3. MemoryContextSwitchTo(postmaster_mem_cxt)切换到postmaster_mem_cxt内存上下文
  4. 调用getInstallationPaths(),设置my_exec_path(一般即为gaussdb可执行文件所在路径)
  5. 调用InitializeGUCOptions(),根据代码中各个GUC参数的默认值生成ConfigureNamesBool、ConfigureNamesInt、ConfigureNamesReal、ConfigureNamesString、ConfigureNamesEnum等 GUC参数的全局变量数组,以及统一管理GUC参数的guc_variables、num_guc_variables、size_guc_variables全局变量,并设置与具体操作系统环境相关的GUC参数
  6. while (opt = ...) SetConfigOption, 若在启动gaussdb时用指定了非默认的GUC参数,则在此时加载至上一步中创建的全局变量中
  7. 调用checkDataDir(),确认数据库安装成功以及PGDATA目录的有效性
  8. 调用CreateDataDirLockFile(),创建数据目录的锁文件
  9. 调用process_shared_preload_libraries(),处理预加载库
  10. 为每个ListenSocket创建**
  11. reset_shared,设置共享内存和信号,主要包括页面缓存池、各种锁缓存池、WAL日志缓存池、事务日志缓存池、事务(号)概况缓存池、各后台线程(锁使用)概况缓存池、各后台线程等待和运行状态缓存池、两阶段状态缓存池、检查点缓存池、WAL日志复制和接收缓存池、数据页复制和接收缓存池等。在后续阶段创建出的客户端后台线程以及各个辅助线程均使用该共享内存空间,不再单独开辟
  12. 将启动时手动设置的GUC参数以文件形式保存下来,以供后续后台服务端线程启动时使用
  13. 为不同信号设置handler
  14. 调用pgstat_init(),初始化状态收集子系统;
  15. 调用load_hba(),加载pg_hba.conf文件,该文件记录了允许连接(指定或全部)数据库的客户端物理机的地址和端口;调用load_ident(),加载pg_ident.conf文件,该文件记录了操作系统用户名与数据库系统用户名的对应关系,以便后续处理客户端连接时的身份认证
  16. 调用 StartupPID = initialize_util_thread(STARTUP),进行数据一致性校验。对于服务端主机来说,查看pg_control文件,若上次关闭状态为DB_SHUTDOWNED且recovery.conf文件没有指定进行恢复,则认为数据一致性成立;否则,根据pg_control中检查点的redo位置或者recovery.conf文件中指定的位置,读取WAL日志或归档日志进行replay(回放),直至数据达到预期的一致性状,主要函数StartupXLOG
  17. 最后进入ServerLoop()函数,循环响应客户端连接请求

ServerLoop函数

下面来讲ServerLoop函数主流程

  1. 调用gs_signal_setmask(&UnBlockSig, NULL)和gs_signal_unblock_sigusr2(),使得线程可以响应用户或其它线程的、指定的信号集
  2. 每隔PM_POLL_TIMEOUT_MINUTE时间修改一次socket文件和socket锁文件的访问和修改时间,以免**作系统淘汰
  3. 判断线程状态(pmState),若为PM_WAIT_DEAD_END,则休眠100毫秒,并且不接收任何连接;否则,通过系统调用poll()或select()来阻塞地读取**端口上传入的数据,最长阻塞时间PM_POLL_TIMEOUT_SECOND
  4. 调用gs_signal_setmask(&BlockSig, NULL)和gs_signal_block_sigusr2()不再接收外源信号
  5. 判断poll()或select()函数的返回值,若小于零,**出错,服务端进程退出;若大于零,则创建连接ConnCreate(),并进入后台服务线程启动流程BackendStartup()。对于父线程,即postmaster线程,在结束BackendStartup()的调用以后,会调用ConnFree(),清除连接信息;若poll()或select()的返回值为零,即没有信息传入,则不进行任何操作
  6. 调用ADIO_RUN()、ADIO_END() ,若AioCompleters没有启动,则启动之
  7. 检查各个辅助线程的线程号是否为零,若为零,则调用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函数

  1. process_postgres_switches(),加载传入的启动选项和GUC参数
  2. 为不同信号设置handler
  3. 调用sigdelset(&BlockSig, SIGQUIT),允许响应SIGQUIT信号
  4. 调用BaseInit(),初始化存储管理系统和页面缓存池计数
  5. 调用on_shmem_exit(),设置线程退出前需要进行的内存清理动作。这些清理动作构成一个链表(on_shmem_exit_list全局变量),每次调用该函数都向链表尾端添加一个节点,链表长度由on_shmem_exit_index记录,且不可超过MAX_ON_EXITS宏。在线程退出时,从后往前调用各个节点中的动作(函数指针),完成清理工作
  6. 调用gs_signal_setmask (&UnBlockSig),设置屏蔽的信号类型
  7. 调用InitBackendWorker进行统计系统初始化、syscache初始化工作
  8. BeginReportingGUCOptions如有需要则打印GUC参数
  9. 调用on_proc_exit(),设置线程退出前需要进行的线程清理动作。设置和调用机制与on_shmem_exit()类似
  10. 调用process_local_preload_libraries(),处理GUC参数设定后的预加载库
  11. AllocSetContextCreate创建MessageContext、RowDescriptionContext、MaskPasswordCtx上下文
  12. 调用sigsetjmp(),设置longjump点,若后续查询执行中出错,在某些情况下可以返回此处重新开始
  13. 调用gs_signal_unblock_sigusr2(),允许线程响应指定的信号集
  14. 然后进入for循环,进行查询执行

  1. 调用pgstat_report_activity()、pgstat_report_waitstatus(),告诉统计系统后台线程正处于idle状态
  2. 设置全局变量DoingCommandRead = true
  3. 调用ReadCommand(),读取客户端SQL语句
  4. 设置全局变量DoingCommandRead=false
  5. 若在上述过程中收到SIGHUP信号,表示线程需要重新加载修改过的postgresql.conf配置文件
  6. 进入switch (firstchar),根据接收到的信息进行分支判断

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

参考其他线程完成

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

详解openGauss多线程架构启动过程的更多相关文章

  1. 详解linux系统的启动过程及系统初始化

    一.linux系统的启动流程 关于linux系统的启动流程我们可以按步进行划分为如下: POST加电自检 -->BIOS(Boot Sequence)-->加载对应引导上的MBR(boot ...

  2. MySQL 5.7主从复制从零开始设置及全面详解——实现多线程并行同步,解决主从复制延迟问题!

    MySQL 5.7主从复制从零开始设置及全面详解——实现多线程并行同步,解决主从复制延迟问题!2017年06月15日 19:59:44 蓝色-鸢尾 阅读数:2062版权声明:本文为博主原创文章,如需转 ...

  3. RocketMQ详解(三)启动运行原理

    专题目录 RocketMQ详解(一)原理概览 RocketMQ详解(二)安装使用详解 RocketMQ详解(三)启动运行原理 RocketMQ详解(四)核心设计原理 RocketMQ详解(五)总结提高 ...

  4. 异常处理器详解 Java多线程异常处理机制 多线程中篇(四)

    在Thread中有异常处理器相关的方法 在ThreadGroup中也有相关的异常处理方法 示例 未检查异常 对于未检查异常,将会直接宕掉,主线程则继续运行,程序会继续运行 在主线程中能不能捕获呢? 我 ...

  5. 【嵌入式开发】 Bootloader 详解 ( 代码环境 | ARM 启动流程 | uboot 工作流程 | 架构设计)

    作者 : 韩曙亮 博客地址 : http://blog.csdn.net/shulianghan/article/details/42462795 转载请著名出处 相关资源下载 :  -- u-boo ...

  6. HTTP协议详解以及URL具体访问过程

    1.简介 1.1.HTTP协议是什么? 即超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准.从 ...

  7. Mybatis详解(二) sqlsession的创建过程

    我们处于的位置 我们要清楚现在的情况. 现在我们已经调用了SqlSessionFactoryBuilder的build方法生成了SqlSessionFactory 对象. 但是如标题所说,要想生成sq ...

  8. java多线程详解(1)-多线程入门

    一.多线程的概念 线程概念 线程就是程序中单独顺序的流控制. 线程本身不能运行,它只能用于程序中. 说明:线程是程序内的顺序控制流,只能使用分配给程序的资源和环境. 进程:操作系统中执行的程序 程序是 ...

  9. 【Java学习笔记之三十四】超详解Java多线程基础

    前言 多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程并发编程对我们来说极其重要,下面跟我一起开启本次的学习之旅吧. 正文 线程与进程 1 线程:进程中负责程序执行的 ...

  10. 【java】详解java多线程

    目录结构: contents structure [+] 线程的创建与启动 继承Thread类创建线程类 实现Runnable接口创建线程类 使用Callable和Future创建线程 线程的生命周期 ...

随机推荐

  1. k8s-1.23.6 安装部署文档(超详细)

    一.文档简介 作者:lanjiaxuan 邮箱:lanheader@163.com 博客地址:https://www.cnblogs.com/lanheader/ 更新时间:2022-09-09 二. ...

  2. mybatis 操作 mysql 动态创建数据表

    Map 数据一般是根据需求生成的,例如 map.put("ticketId",176),map.put("ticketName","测试工单" ...

  3. [WPF]浅析资源引用(pack URI)

    WPF中我们引用资源时常常提到一个概念:pack URI,这是WPF标识和引用资源最常见的方式,但不是唯一的方式.本文将介绍WPF中引用资源的几种方式,并回顾一下pack URI标识引用在不同位置的资 ...

  4. (int argc, char *argv[])在MCU中的调试使用

    这里主要讲了基于RTT的 finsh->MSH_CMD_EXPORT 方法,在串口终端中调用自定义函数,并传入参数的方法. 在传统的MCU开发中 当我们需要测试一个函数在传入不同参数时的运算结果 ...

  5. 颠覆了!eShop跟随.Net 8迎来重磅升级,微服务架构与GPT的完美结合!

    .Net 8正式发布了,发布了诸多重大的新功能.新特性! .Net 8新增的功能带来诸多惊喜,还未一一体验完毕呢,我又发现了跟随.Net 8的发布,eShop也迎来重磅升级! eShop一直以来都是微 ...

  6. 在路上---学习篇(一)Python 数据结构和算法 (4) --希尔排序、归并排序

    独白: 希尔排序是经过优化的插入排序算法,之前所学的排序在空间上都是使用列表本身.而归并排序是利用增加新的空间,来换取时间复杂度的减少.这俩者理念完全不一样,注定造成的所消耗的时间不同以及空间上的不同 ...

  7. Python 潮流周刊#28:两种线程池、四种优化程序的方法

    你好,我是猫哥.这里每周分享优质的 Python.AI 及通用技术内容,大部分为英文.本周刊开源,欢迎投稿.另有电报频道作为副刊,补充发布更加丰富的资讯. 产品推荐 Walles.AI 是一款适用于所 ...

  8. 多维度分析数据的软件,BI软件不就是吗

    BI软件(Business Intelligence Software)是一种用于多维度分析数据的工具,可以帮助企业从海量数据中提取有价值的洞察力,并为决策者和业务用户提供可视化.交互式的报表和仪表盘 ...

  9. Java实现相似结构表算法

    [产品需求] 对所有元数据进行分析,匹配出表字段相似度达到阈值的向相似结构表关系数据. 网上没有搜到相关算法实现,只能自己动手了. [算法实现] 简单点实现的话,可以轮询所有表,每张表都和其它表进行匹 ...

  10. [ARC160F] Count Sorted Arrays

    Problem Statement There are an integer $N$ and $M$ pairs of integers: $(a_1, b_1), (a_2, b_2), \dots ...