巨人的肩膀
PostgreSQL是数据库管理系统中理论与实践结合的集大成者,通过学习PostgreSQL的源码可以加深我们对数据库管理系统的理解。近年兴起的NewSQL原生分布式数据库OceanBase、TiDB、MemFireDB都是站在巨人的肩膀上逐渐发展而来的。正如oceanbase的CTO所言:经典数据库包含存储、事务、SQL 这几个核心引擎,以及基于这些核心引擎之上的数据库功能和性能、成本、安全、可靠等企业级特性。分布式数据库在经典数据库的基础之上引入了分布式,实现了高可用和可扩展。虽然分布式数据库在原生分布式架构上产生了很多创新技术,但在数据库的功能、性能、精细化程度上还有一段很长的路要走。
前面梳理过一个数据库管理系统的知识Roadmapjuejin.cn/post/698500…,通过该roadmap我们对数据库管理系统能够有一个宏观上的了解,然后分解任务、制定计划循序渐进的进行学习。为了学习PostgreSQL我们同样需要梳理一个大纲出来
PostgreSQL
Postgres系统结构
PostgreSQL采用C/S(客户机/服务器)模式结构。应用层通过INET或者Unix Socket利用既定的协议与数据库服务器进行通信。
另外,还有一种‘Standalone Backend’使用的方式, 虽然通过这种方式也可以启动服务器,但是一般只在数据库的初始化(PostgreSQL的cluster的初始化,相当于其他数据库的instance的初始化)、紧急维护的时候使用,所以简单来说可以认为PostgreSQL是使用C/S的形式进行访问的。
PostgreSQL把客户端称为前端(Frontend),把服务器端成为后端(Backend), 后端有复数个进程构成,这个在后面会进行说明。
前端和后端通信的协议在PostgreSQL的官方文档中的《前端和后端的通信协议》一章中有详细的说明。简单来说,大体的工作模式是:前端向后端发送查询的SQL文,然后后端通过复数个报文把结果返回给前端。
由于需要进行连接的初始化、错误等各种各样处理,PostgreSQL的协议的处理也是相当复杂,如果要自己从头实现这些协议的处理的话,还是相当麻烦的,所以PostgreSQL本身提供了C语言写的libpq这样一个协议处理库,利用这个库可以比较轻松地和后端进行通信。PostgreSQL的话除了C以外,还支持Perl和PHP等其他语言,这些语言在内部也调用了libpq.
也有不使用libpq而直接与PostgreSQL通信的库。比较具有代表性的是Java, PostgreSQL的JDBC驱动是不依赖于libpq直接与PostgreSQL通信的.
另外后端的话,比较核心的是进行数据库处理的数据库引擎(Database Engine)。 数据库引擎可以对用户所编写的函数进行解析和处理,用户如果能够利用好这个功能的话,可以柔软地扩展PostgreSQL的功能。 比较经常使用的是存储过程(PostgreSQL中称为用户自定义函数),PostgreSQL支持的用户定义函数的语言如下:
语言 | 对应的自定义函数 |
---|---|
C | C函数 |
SQL | SQL 函数 |
类似Oracle的PL/SQL的语言 | PL/pgSQL |
Perl | PL/Perl |
Python | PL/Python |
PostgreSQL进程结构
再详细看看PostgreSQL的进程结构。
PostgreSQL是一种多进程体系结构,其中,最主要的两个进程就是守护进程Post-master和服务进程Postgres。Postmaster和Postgres都是通过载入Postgres程序而形成的进程,只是在运行时所处的分支不同而已。守护进程Postmaster负责整个系统的启动和关闭。它监听并接受客户端的连接请求,为其分配服务进程Postgres。服务进程Postgres接受并执行客户端发送的命令。它在底层模块(存储、事故管理、索引等)之上调用各个主要的功能模块(如编译器、优化器、执行器等),完成客户端的各种数据库操作,并返回执行结果。
Postmaster除为用户连接请求分配postgres服务进程外,还将启动相关的后台辅助进程:Writer、WalWriter、Archive、Autovacuum等等。
Potgres(常驻进程)
管理后端的常驻进程,也称为’postmaster’。其默认监听UNIX Domain Socket和TCP/IP(Windows等,一部分的平台只监听tcp/ip)的5432端口,等待来自前端的的连接处理。监听的端口号可以在PostgreSQL的设置文件postgresql.conf里面可以改。
一旦有前端连接过来,postgres会通过fork(2)生成子进程。没有Fork(2)的windows平台的话,则利用createProcess()生成新的进程。这种情形的话,和fork(2)不同的是,父进程的数据不会被继承过来,所以需要利用共享内存把父进程的数据继承过来。
Postgres(子进程)
子进程根据pg_hba.conf定义的安全策略来判断是否允许进行连接,根据策略,会拒绝某些特定的IP及网络,或者也可以只允许某些特定的用户或者对某些数据库进行连接。
Postgres会接受前端过来的查询,然后对数据库进行检索,最后把结果返回,有时也会对数据库进行更新。更新的数据同时还会记录在事务日志里面(PostgreSQL称为WAL日志),这个主要是当停电的时候,服务器当机,重新启动的时候进行恢复处理的时候使用的。另外,把日志归档保存起来,可在需要进行恢复的时候使用。在PostgreSQL 9.0以后,通过把WAL日志传送其他的postgreSQL,可以实时得进行数据库复制,这就是所谓的‘数据库复制’功能。
Postgres之外还有一些辅助的进程。这些进程都是由常驻postgres启动的进程。
Writer process
Writer process在适当的时间点把共享内存上的缓存写往磁盘。通过这个进程,可以防止在检查点的时候(checkpoint),大量的往磁盘写而导致性能恶化,使得服务器可以保持比较稳定的性能。Background writer起来以后就一直常驻内存,但是并非一直在工作,它会在工作一段时间后进行休眠,休眠的时间间隔通过postgresql.conf里面的参数bgwriter_delay设置,默认是200微秒。
这个进程的另外一个重要的功能是定期执行检查点(checkpoint)。
检查点的时候,会把共享内存上的缓存内容往数据库文件写,使得内存和文件的状态一致。通过这样,可以在系统崩溃的时候可以缩短从WAL恢复的时间,另外也可以防止WAL无限的增长。 可以通过postgresql.conf的checkpoint_segments、checkpoint_timeout指定执行检查点的时间间隔。
WAL writer process
WAL writer process把共享内存上的WAL缓存在适当的时间点往磁盘写,通过这样,可以减轻后端进程在写自己的WAL缓存时的压力,提高性能。另外,非同步提交设为true的时候,可以保证在一定的时间间隔内,把WAL缓存上的内容写入WAL日志文件。
Archive process
Archive process把WAL日志转移到归档日志里。如果保存了基础备份以及归档日志,即使实在磁盘完全损坏的时候,也可以回复数据库到最新的状态。
stats collector process
统计信息的收集进程。收集好统计表的访问次数,磁盘的访问次数等信息。收集到的信息除了能被autovaccum利用,还可以给其他数据库管理员作为数据库管理的参考信息。
Logger process
把postgresql的活动状态写到日志信息文件(并非事务日志),在指定的时间间隔里面,对日志文件进行rotate.
Autovacuum启动进程
autovacuum launcher process是依赖于postmaster间接启动vacuum进程。而其自身是不直接启动自动vacuum进程的。通过这样可以提高系统的可靠性。
自动vacuum进程
autovacuum worker process进程实际执行vacuum的任务。有时候会同时启动多个vacuum进程。
wal sender / wal receiver
wal sender 进程和wal receiver进程是实现postgresql复制(streaming replication)的进程。Wal sender进程通过网络传送WAL日志,而其他PostgreSQL实例的wal receiver进程则接收相应的日志。Wal receiver进程的宿主PostgreSQL(也称为Standby)接受到WAL日志后,在自身的数据库上还原,生成一个和发送端的PostgreSQL(也称为Master)完全一样的数据库。
SQL请求处理流程简述
理论结构
理论上一个数据库管理系统的查询处理流程应该如下(来源于《数据库系统概念》)
基本步骤包括:
- 语法分析与翻译
- 优化
- 执行
语法分析器检查用户查询的语法,验证查询中出现的关系名是数据库中的关系名等。系统构造该查询语法的语法分析树表示,然后将之翻译成关系代数表达式。如果查询是用视图形式表示的,翻译阶段还要用定义该视图关系代数表达式来替换所有对该视图的引用。
每个查询都会有多重计算结果的方法。每个SQL查询可以用其中的一种方式翻译成关系代数表达式。
查询的关系代数表达式形式仅仅部分指定了如何执行查询;通常有许多方式来计算关系代数表达式。
要全面说明如何执行一个查询,不仅要提供关系代数表达式,还要对表达式加上注释来说明如何执行每个操作。注释可以声明某个具体操作所采用的算法,或将要使用的一个或多个特定的索引。加了“如何执行”注释的关系代数运算称为计算原语(evaluation primitive) 。
用于执行一个查询的原语操作序列称为查询执行计划(query-execution plan) 或查询计算计划(query-evaluation plan)
查询执行引擎(query-execution engine) 接受一个查询执行计划,执行该计划并把结果返回给查询。
给定查询的不同执行计划会有不同的代价。不可能用户寄希望于用户写出具有最高效率执行计划的查询语句。相反,构造具有最小查询执行代价的查询执行计划应当是系统的责任。这叫做查询优化
许多数据库并不用关系代数表达式来表示查询,而是采用基于给定SQl查询结构的带注释的语法分析树来表示。
为了优化查询,查询优化器必须知道每个操作的代价。虽然精确计算代价是困难的,因为这依赖于许多参数,但对每个操作的执行代价得出一个粗略的估计是可能的。
PostgreSQL的实现
下面看看数据库引擎postgres子进程的处理概要。为了简单起见下面的说明中,把backend process简称为backend。Backend的main函数是PostgresMain (tcop/postgres.c)。
后台服务进程postgres接收到查询语句后,首先将其传递到查询分析模块,进行此法、语法和语义分析。若是简单的命令(例如建表、创建用户、备份等)则将其分配到功能性命令处理模块;对于复杂的命令(SELECT/INSERT/DELETE/UPDATE)则要为其构建查询树(Query结构体),然后交给查询重写模块。查询重写模块接收到查询树后,按照该查询所设计的规则和视图对查询树进行重写,生成新的查询树。生成路径模块依据重写过程的查询树,考虑关系的访问方式、连接方式和连接顺序等问题,采用动态规划算法或遗传算法,生成最优的表连接路径。最后,由最优路径生成可执行的计划,并将其传递到查询执行模块执行。查询处理各模块说明见下表:
源码目录
src目录
只列出部分重要的,makefile和config不做说明
文件目录 | 说明 |
---|---|
backend/ | 后端的源码目录 |
bin/ | psql等 UNIX命令的代码 |
include/ | 头文件 |
interfaces/ | 前端相关的库的代码 |
pl/ | 存储过程语言的代码 |
port/ | 平台移植相关的代码 |
template/ | 平台相关的设置值 |
test/ | 各种测试脚本 |
timezone/ | 时区相关代码 |
tools/ | 各自开发工具和文档 |
tutorial/ | 教程 |
src/backend 目录
目录文件 | 说明 |
---|---|
access/ | 各种存储访问方法(在各个子目录下) common(共同函数)、gin (Generalized Inverted Index通用逆向索引)、gist (Generalized Search Tree通用索引)、 hash (哈希索引)、heap (heap的访问方法)、index (通用索引函数)、 nbtree (Btree函数)、transam (事务处理) |
bootstrap/ | 数据库的初始化处理(initdb的时候) |
catalog/ | 系统目录 |
commands/ | 执行非计划查询的SQL命令,如创建表命令等。 |
executor/ | 执行器,执行来自Optimizer的查询计划。 |
foreign/ | FDW(Foreign Data Wrapper)处理 |
lib/ | 共同函数 |
libpq/ | 前端/后端通信处理 |
main/ | 主程序模块,它负责将控制权转到Postmaster进程或Postgres进程。 |
nodes/ | 表达式树节点相关的处理函数 |
optimizer/ | 优化器,根据查询树创建最优的查询路径和查询计划。 |
parser/ | 编译器,将SQL查询转化为内部查询树。 |
port/ | 平台兼容性处理相关的函数。 |
postmaster/ | 监听用户请求的守护进程,并控制Postgres进程的启动和终止。 |
replication/ | streaming replication |
regex/ | 正规表达式库及相关函数,用于支持正规表达式处理。 |
rewrite/ | 规则及视图相关的重写处理 |
snowball/ | 全文检索相关(语干处理) |
storage/ | 共享内存、磁盘上的存储、缓存等全部一次/二次记录管理(以下的目录)buffer/(缓存管理)、 file/(文件)、freespace/(Fee Space Map管理) ipc/(进程间通信)、large_object /(大对象的访问函数)、lmgr/(锁管理)、page/(页面访问相关函数)、 smgr/(存储管理器) |
tcop/ | Postgres服务进程的主要处理部分,它调用Parser、Optimizer、Executor和Commands中的函数来执行客户端提交的查询 |
tsearch/ | 全文检索 |
utils/ | 各种模块(以下目录) adt/(嵌入的数据类型)、cache/(缓存管理)、 error/(错误处理)、fmgr/(函数管理)、hash/(hash函数)、 init/(数据库初始化、postgres的初期处理)、 mb/(多字节文字处理)、misc/(其他)、mmgr/(内存的管理函数)、 resowner/(查询处理中的数据(buffer pin及表锁)的管理)、sort/(排序处理)、time/(事务的 MVCC 管理) |
模块间调用关系
参考
《数据库系统概念》
《PostgreSQL数据库内核分析》