1.2 应用场景
openGauss数据库有以下几个主要应用场景。
(1) 交易型应用。
大并发、大数据量、以联机事务处理为主的交易型应用,如电商、金融、O2O、电信CRM/计费等,可按需选择不同的主备部署模式。
(2) 物联网数据。
物联网场景如工业监控、远程控制、智慧城市及其延展领域、智能家居和车联网等。物联网场景的特点是传感监控设备的种类和数量多、数据采样频率高、数据存储为追加模型、对数据的操作和分析并重。
1.3 系统架构
openGauss主要包含了openGauss服务器、客户端驱动、OM(operations manager,运维管理模块)等模块,它的架构如图1-1所示,模块说明如表1-1所示。
图1-1 openGauss软件架构
表1-1 openGauss模块说明
名称 | 描述 |
OM | 运维管理模块,提供openGauss日常运维、配置管理的管理接口、工具 |
客户端驱动 | 客户端驱动(client driver),负责接收来自应用的访问请求,并向应用返回执行结果;负责与openGauss实例的通信,下发SQL在openGauss实例上执行,并接收命令执行结果 |
openGauss主(备)设备 | openGauss主(备)设备,负责存储业务数据(支持行存储、列存储、内存表存储)、执行数据查询任务以及向客户端驱动返回执行结果 |
storage | 服务器的本地存储资源,持久化存储数据 |
1.4 代码结构
本节从数据库系统通信管理、SQL引擎和存储引擎3个方面对openGauss的代码结构进行介绍。
1.4.1 通信管理
openGauss查询响应使用简单的“单一用户对应一个服务器线程”的客户端/服务器模型实现。由于无法提前知道需要建立多少个连接,因此必须使用主进程(gaussmaster)。主进程在指定的TCP/IP(transmission control protocol/internet protocol,传输控制协议/互联网协议)端口上侦听传入的连接,只要检测到连接请求,主进程就会生成一个新的服务器线程。服务器线程之间使用信号量和共享内存相互通信,以确保整个并发数据访问期间的数据完整性。
客户端进程可以被理解为满足openGauss协议的任何程序。许多客户端都基于C语言库libpq进行通信,但是该协议有几种独立的实现,例如Java JDBC驱动程序。
建立连接后,客户端进程可以将查询发送到后端服务器。查询使用纯文本传输,即在前端(客户端)中没有进行解析。服务器解析查询语句、创建执行计划、执行并通过在已建立连接上传输检索到的结果集,将其返回给客户端。
openGauss数据库中处理客户端连接请求的模块叫作作postmaster。前端程序发送启动信息给postmaster,postmaster根据信息内容建立后端响应线程。postmaster也管理系统级的操作,比如调用启动和关闭程序。postmaster在启动时创建共享内存和信号量池,但它自身不管理内存、信号量和锁操作。
当客户端发来一个请求信息,postmaster立刻启动一个新会话,新会话对请求进行验证,验证成功后为它匹配后端工作线程。这种模式架构上处理简单,但是高并发下由于线程过多,切换和轻量级锁区域的冲突过大导致性能急剧下降。因此openGauss通过线程资源池化复用的技术来解决该问题。线程池技术的整体设计思想是线程资源池化,并且在不同连接直接复用。
1. postmaster源码组织
postmaster源码目录为:/src/gausskernel/process/postmaster。postmaster源码文件如表1-2所示。
表1-2 postmaster源码文件
模块 | 源码文件 | 功能 |
postmaster | postmaster.cpp | 用户响应主程序 |
aiocompleter.cpp | 完成预取(prefetch)和后端写(backWrite)I/O操作 | |
alarmchecker.cpp | 闹钟检查线程 | |
lwlockmonitor.cpp | 轻量锁的死锁检测 | |
pagewriter.cpp | 写页面 | |
pgarch.cpp | 日志存档 | |
pgaudit.cpp | 审计线程 | |
pgstat.cpp | 统计信息收集 | |
startup.cpp | 服务初始化和恢复 | |
syslogger.cpp | 捕捉并写所有错误日志 | |
autovacuum.cpp | 垃圾清理线程 | |
bgworker.cpp | 后台工作线程(服务共享内存) | |
bgwriter.cpp | 后台写线程(写共享缓存) | |
cbmwriter.cpp | 修改数据块跟踪记录线程 | |
remoteservice.cpp | 远程服务线程,用于双机损坏页修复时的远程服务 | |
checkpointer.cpp | 检查点处理 | |
fencedudf.cpp | 保护模式下运行用户定义函数 | |
gaussdb_version.cpp | 版本特性控制 | |
twophasecleaner.cpp | 清理两阶段事务线程 | |
walwriter.cpp | 预写式日志写入 |
2. postmaster主流程
postmaster主流程代码如下:
/* postmaster.cpp */
...
int PostmasterMain(int argc, char* argv[])
{
InitializePostmasterGUC(); /* 初始化postmaster模块配置参数*/
...
pgaudit_agent_init(); /* 初始化审计模块*/
...
for (i = 0; i < MAXLISTEN; i++) /* 建立输入socket监听*/
t_thrd.postmaster_cxt.ListenSocket[i] = PGINVALID_SOCKET;
...
/* 建立共享内存和信号池*/
reset_shared(g_instance.attr.attr_network.PostPortNumber);
...
/* 初始化postmaster信号管理*/
gs_signal_slots_init(GLOBAL_ALL_PROCS + EXTERN_SLOTS_NUM);
...
InitPostmasterDeathWatchHandle(); /* 初始化宕机监听*/
...
pgstat_init(); /* 初始化统计数据收集子系统*/
InitializeWorkloadManager(); /* 初始化工作负载管理器*/
...
InitUniqueSQL(); /* 初始化unique SQL资源*/
...
autovac_init(); /* 初始化垃圾清理线程子系统*/
...
status = ServerLoop(); /* 启动postmaster主业务循环*/
...
}
1.4.2 SQL引擎
数据库的SQL引擎是数据库重要的子系统之一,它对上负责承接应用程序发送过来的SQL语句,对下则负责指挥执行器运行执行计划。其中优化器作为SQL引擎中最重要、最复杂的模块,被称为数据库的“大脑”,优化器产生的执行计划的优劣直接决定数据库的性能。
本节从SQL语句发送到数据库服务器开始,对SQL引擎的各个模块进行全面的介绍与源码解析,以实现对SQL语句执行的逻辑与源码更深入的理解。其响应流程如图1-2所示。
图1-2 openGauss数据库SQL查询响应流程
1. 查询解析——parser
SQL解析对输入的SQL语句进行词法分析、语法分析、语义分析,获得查询解析树或者逻辑计划。SQL查询语句解析的解析器(parser)阶段包括如下。
(1) 词法分析:从查询语句中识别出系统支持的关键字、标识符、操作符、终结符等,每个词确定自己固有的词性。
(2) 语法分析:根据SQL语言的标准定义语法规则,使用词法分析中产生的词去匹配语法规则,如果一个SQL语句能够匹配一个语法规则,则生成对应的语法树(abstract synatax tree,AST)。
(3) 语义分析:对语法树进行检查与分析,检查语法树中对应的表、列、函数、表达式是否有对应的元数据(指数据库中定义有关数据特征的数据,用来检索数据库信息)描述,基于分析结果对语法树进行扩充,输出查询树(Query)。主要检查的内容包括:
检查关系的使用:FROM子句中出现的关系必须是该查询对应模式中的关系或视图。
‚ 检查与解析属性的使用:在SELECT语句中或者WHERE子句中出现的各个属性必须是FROM子句中某个关系或视图的属性。
ƒ 检查数据类型:所有属性的数据类型必须是匹配的。
词法和语法分析代码基于gram.y和scan.l中定义的规则,使用UNIX工具bison和flex构建产生。其中,词法分析器在文件scan.l中定义,它负责识别标识符、SQL关键字等。对于找到的每个关键字或标识符,都会生成一个标记并将其传递给解析器。语法解析器在文件gram.y中定义,由一组语法规则和每当触发规则时执行的动作组成,基于这些动作代码架构并输出语法树。在解析过程中,如果语法正确,则进入语义分析阶段并建立查询树返回,否则将返回错误,终止解析过程。
解析器在词法和语法分析阶段仅使用有关SQL语法结构的固定规则来创建语法树。它不会在系统目录中进行任何查找,因此无法理解所请求操作的详细语义。
语法解析完成后,语义分析过程将解析器返回的语法树作为输入,并进行语义分析以了解查询所引用的表、函数和运算符。用来表示此信息的数据结构称为查询树。解析器解析过程分为原始解析与语义分析,分开的原因是,系统目录查找只能在事务内完成,并且不希望在收到查询字符串后立即启动事务。原始解析阶段足以识别事务控制命令(BEGIN,ROLLBACK等),然后可以正确执行这些命令而无须任何进一步分析。一旦知道正在处理的实际查询(例如SELECT或UPDATE),就可以开始事务,这时才调用语义分析过程。
1) parser源码组织
parser源码目录为:/src/common/backend/parser。parser源码文件如表1-3所示。
表1-3 parser源码文件
模块 | 源码文件 | 功能 |
parser | parser.cpp | 解析主程序 |
scan.l | 词法分析,分解查询成token(令牌) | |
scansup.cpp | 处理查询语句转义符 | |
kwlookup.cpp | 将关键词转换为具体的token | |
keywords.cpp | 标准关键词列表 | |
analyze.cpp | 语义分析 | |
gram.y | 语法分析,解析查询token并产生原始解析树 | |
parse_agg.cpp | 处理聚集操作,比如SUM(col1)、AVG(col2) | |
parse_clause.cpp | 处理子句,比如WHERE、ORDER BY | |
parse_compatibility.cpp | 处理数据库兼容语法和特性支持 | |
parse_coerce.cpp | 处理表达式数据类型强制转换 | |
parse_collate.cpp | 对完成表达式添加校对信息 | |
parse_cte.cpp | 处理公共表格表达式(WITH 子句) | |
parse_expr.cpp | 处理表达式,比如col、col+3、x=3 | |
parse_func.cpp | 处理函数,table.column和列标识符 | |
parse_node.cpp | 对各种结构创建解析节点 | |
parse_oper.cpp | 处理表达式中的操作符 | |
parse_param.cpp | 处理参数 | |
parse_relation.cpp | 支持表和列的关系处理程序 | |
parse_target.cpp | 处理查询解析的结果列表 | |
parse_type.cpp | 处理数据类型 | |
parse_utilcmd.cpp | 处理实用命令的解析分析 |
2) parser主流程
parser主流程代码如下:
/* parser.cpp */
...
/* 原始解析器,输入查询字符串,做词法和语法分析,返回原始语法解析树列表*/
List* raw_parser(const char* str, List** query_string_locationlist)
{
...
/* 初始化 flex scanner */
yyscanner = scanner_init(str, &yyextra.core_yy_extra, ScanKeywords, NumScanKeywords);
...
/* 初始化 bison parser */
parser_init(&yyextra);
/* 解析! */
yyresult = base_yyparse(yyscanner);
/* 清理释放内存*/
scanner_finish(yyscanner);
...
return yyextra.parsetree;
}
/* analyze.cpp */
...
/* 分析原始语法解析树,做语义分析并输出查询树 */
Query* parse_analyze(
Node* parseTree, const char* sourceText, Oid* paramTypes, int numParams, bool isFirstNode, bool isCreateView)
{
/* 初始化解析状态和查询树 */
ParseState* pstate = make_parsestate(NULL);
Query* query = NULL;
...
/* 将解析树转换为查询树 */
query = transformTopLevelStmt(pstate, parseTree, isFirstNode, isCreateView);
...
/* 释放解析状态 */
free_parsestate(pstate);
...
return query;
}
2. SQL查询分流——traffic cop
traffic cop模块负责查询的分流,它负责区分简单和复杂的查询指令。事务控制命令(例如BEGIN和ROLLBACK)非常简单,因此不需要其他处理,而其他命令(例如SELECT和JOIN)则传递给重写器(参考第6章)。这种区分通过对简单命令执行最少的优化,并将更多的时间投入复杂的命令上,从而减少了处理时间。简单和复杂查询指令也对应如下两类解析。
(1) 软解析(简单,旧查询):当openGauss共享缓冲区中存在已提交SQL语句的已解析表示形式时,可以重复利用缓存内容执行语法和语义检查,避免查询优化相对昂贵的操作。
(2) 硬解析(复杂,新查询):如果无缓存语句可重用,或者第一次将SQL语句加载到openGauss共享缓冲区中,则会导致硬解析。同样,当一条语句在共享缓冲区中老化时,再次重新加载该语句时,还会导致另一次硬解析。因此,共享缓冲区的大小也会影响解析调用的数量。
可以查询gs_prepared_statements查看缓存了什么,以区分软/硬解析(它仅对当前会话可见)。此外,gs_buffercache模块提供了一种实时检查共享缓冲区高速缓存内容的方法,它甚至可以分辨出有多少数据块来自磁盘,有多少数据来自共享缓冲区。
1) traffic cop(tcop)源码组织
traffic cop(tcop)源码目录为:/src/common/backend/tcop。traffic cop(tcop)源码文件如表1-4所示。
表1-4 traffic cop(tcop)源码文件
模块 | 源码文件 | 功能 |
tcop | auditfuncs.cpp | 记录数据库操作审计信息 |
autonomous.cpp | 创建可被用来执行SQL查询的自动会话 | |
dest.cpp | 与查询结果被发往的终点通信 | |
utility.cpp | 数据库通用指令控制函数 | |
fastpath.cpp | 在事务期间缓存操作函数和类型等信息 | |
postgres.cpp | 后端服务器主程序 | |
pquery.cpp | 查询处理指令 | |
stmt_retry.cpp | 执行SQL语句失败时,分析返回的错误码,决定是否进行重试 |
2) traffic cop主流程
traffic cop主流程代码如下:
...
/*原始解析器,输入查询字符串,做词法和语法分析,返回原始解析树列表*/
int PostgresMain(int argc, char* argv[], const char* dbname, const char* username)
{
...
/* 整体初始化*/
t_thrd.proc_cxt.PostInit->SetDatabaseAndUser(dbname, InvalidOid, username);
...
/* 事务的自动错误处理 */
if (sigsetjmp(local_sigjmp_buf, 1) != 0) { ... }
...
/* 错误语句的重新尝试阶段 */
if (IsStmtRetryEnabled() && StmtRetryController->IsQueryRetrying())
{ ... }
/* 无错误查询指令循环处理*/
for (;;) {
...
/* 按命令类型执行处理流程*/
switch(firstchar){
...
case: 'Q': ... /* 简单查询 */
case: 'P': ... /* 解析 */
case: 'E': ... /* 执行 */
}
...
}
...
}