openGauss数据库有以下几个主要应用场景。
大并发、大数据量、以联机事务处理为主的交易型应用,如电商、金融、O2O、电信CRM/计费等,可按需选择不同的主备部署模式。
物联网场景如工业监控、远程控制、智慧城市及其延展领域、智能家居和车联网等。物联网场景的特点是传感监控设备的种类和数量多、数据采样频率高、数据存储为追加模型、对数据的操作和分析并重。
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 |
服务器的本地存储资源,持久化存储数据 |
本节从数据库系统通信管理、SQL引擎和存储引擎3个方面对openGauss的代码结构进行介绍。
openGauss查询响应使用简单的“单一用户对应一个服务器线程”的客户端/服务器模型实现。由于无法提前知道需要建立多少个连接,因此必须使用主进程(gaussmaster)。主进程在指定的TCP/IP(transmission control protocol/internet protocol,传输控制协议/互联网协议)端口上侦听传入的连接,只要检测到连接请求,主进程就会生成一个新的服务器线程。服务器线程之间使用信号量和共享内存相互通信,以确保整个并发数据访问期间的数据完整性。
客户端进程可以被理解为满足openGauss协议的任何程序。许多客户端都基于C语言库libpq进行通信,但是该协议有几种独立的实现,例如Java JDBC驱动程序。
建立连接后,客户端进程可以将查询发送到后端服务器。查询使用纯文本传输,即在前端(客户端)中没有进行解析。服务器解析查询语句、创建执行计划、执行并通过在已建立连接上传输检索到的结果集,将其返回给客户端。
openGauss数据库中处理客户端连接请求的模块叫作作postmaster。前端程序发送启动信息给postmaster,postmaster根据信息内容建立后端响应线程。postmaster也管理系统级的操作,比如调用启动和关闭程序。postmaster在启动时创建共享内存和信号量池,但它自身不管理内存、信号量和锁操作。
当客户端发来一个请求信息,postmaster立刻启动一个新会话,新会话对请求进行验证,验证成功后为它匹配后端工作线程。这种模式架构上处理简单,但是高并发下由于线程过多,切换和轻量级锁区域的冲突过大导致性能急剧下降。因此openGauss通过线程资源池化复用的技术来解决该问题。线程池技术的整体设计思想是线程资源池化,并且在不同连接直接复用。
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 |
预写式日志写入 |
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主业务循环*/
...
}
数据库的SQL引擎是数据库重要的子系统之一,它对上负责承接应用程序发送过来的SQL语句,对下则负责指挥执行器运行执行计划。其中优化器作为SQL引擎中最重要、最复杂的模块,被称为数据库的“大脑”,优化器产生的执行计划的优劣直接决定数据库的性能。
本节从SQL语句发送到数据库服务器开始,对SQL引擎的各个模块进行全面的介绍与源码解析,以实现对SQL语句执行的逻辑与源码更深入的理解。其响应流程如图1-2所示。
图1-2 openGauss数据库SQL查询响应流程
SQL解析对输入的SQL语句进行词法分析、语法分析、语义分析,获得查询解析树或者逻辑计划。SQL查询语句解析的解析器(parser)阶段包括如下。
检查关系的使用:FROM子句中出现的关系必须是该查询对应模式中的关系或视图。
‚ 检查与解析属性的使用:在SELECT语句中或者WHERE子句中出现的各个属性必须是FROM子句中某个关系或视图的属性。
ƒ 检查数据类型:所有属性的数据类型必须是匹配的。
词法和语法分析代码基于gram.y和scan.l中定义的规则,使用UNIX工具bison和flex构建产生。其中,词法分析器在文件scan.l中定义,它负责识别标识符、SQL关键字等。对于找到的每个关键字或标识符,都会生成一个标记并将其传递给解析器。语法解析器在文件gram.y中定义,由一组语法规则和每当触发规则时执行的动作组成,基于这些动作代码架构并输出语法树。在解析过程中,如果语法正确,则进入语义分析阶段并建立查询树返回,否则将返回错误,终止解析过程。
解析器在词法和语法分析阶段仅使用有关SQL语法结构的固定规则来创建语法树。它不会在系统目录中进行任何查找,因此无法理解所请求操作的详细语义。
语法解析完成后,语义分析过程将解析器返回的语法树作为输入,并进行语义分析以了解查询所引用的表、函数和运算符。用来表示此信息的数据结构称为查询树。解析器解析过程分为原始解析与语义分析,分开的原因是,系统目录查找只能在事务内完成,并且不希望在收到查询字符串后立即启动事务。原始解析阶段足以识别事务控制命令(BEGIN,ROLLBACK等),然后可以正确执行这些命令而无须任何进一步分析。一旦知道正在处理的实际查询(例如SELECT或UPDATE),就可以开始事务,这时才调用语义分析过程。
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 |
处理实用命令的解析分析 |
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;
}
traffic cop模块负责查询的分流,它负责区分简单和复杂的查询指令。事务控制命令(例如BEGIN和ROLLBACK)非常简单,因此不需要其他处理,而其他命令(例如SELECT和JOIN)则传递给重写器(参考第6章)。这种区分通过对简单命令执行最少的优化,并将更多的时间投入复杂的命令上,从而减少了处理时间。简单和复杂查询指令也对应如下两类解析。
可以查询gs_prepared_statements查看缓存了什么,以区分软/硬解析(它仅对当前会话可见)。此外,gs_buffercache模块提供了一种实时检查共享缓冲区高速缓存内容的方法,它甚至可以分辨出有多少数据块来自磁盘,有多少数据来自共享缓冲区。
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语句失败时,分析返回的错误码,决定是否进行重试 |
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': ... /* 执行 */
}
...
}
...
}