代码编织梦想

引言(Introduction)

经过前面initdb初始化数据集簇后,生成了三个模板数据库。其中template1是最先创建的模板数据库(oid为1),template0用作template1的备份,postgres用于客户连接。但是PostgreSQL服务端的进程不是只有简单地监听客户的请求,还需要有专门负责日志书写,数据落盘,内存回收等功能的进程。这节主要介绍PostgreSQL的进程结构。

进程结构

PostgreSQL使用一种专用的服务器进程体系结构,其中最重要的两个进程是守护进程Postmaster与服务进程Postgres。其中服务进程Postgres可以接收并执行客户端发送的命令,并调用底层存储、事务管理、索引等功能模块完成客户端的各种数据库操作,并返回执行结果。

PostgreSQL采用C/S模式,系统为每个连接的客户端分配一个服务进程Postgres。除此之外,Postmaster还负责启动系统的辅助进程:SysLogger(系统日志进程),PgStat(统计数据收集进程),AutoVacuum(系统自动清理进程)。并且在Postmaster进入循环监听端口时启动以下进程:BgWriter(后台写进程),WalWriter(预写式日志写进程),PgArch(预写式日志归档进程)。

当运行pg_ctl命令进入Postgres程序时,其进程创建流程如下:

在这里插入图片描述

Postmaster守护进程

从上面可以知道,Postmaster负责整个系统的启动和关闭,并且在服务进程出现错误时完成系统的恢复,还要在系统奔溃的时候重启系统。。它是运行在服务器上的总控进程,同时也负责整个系统范围内的操作,例如中断操作与信号处理。但是Postmaster本身并不执行这些操作,而是指派一个子进程在适当的时间处理它们。Postmaster进程在起始时会创建共享内存与信号库,用于与子进程的通信,同时也能在某个子进程奔溃的时候重置共享内存即可恢复。

Postmaster守护进程的执行流程如下:

在这里插入图片描述

初始化内存上下文

内存的功能如前面介绍具有进程通信与隔离进程奔溃影响的功能。为了实现后者的功能,需要给每个内存区域(进程)维护一个内存上下文,用于不仅用于记录进程运行的上下文信息,还能整块回收。

程序首先调用MemoryContextInit创建TopMemoryContext和ErrorContext。然后调用AllocSetContextCreate以TopMemoryContext为根节点创建PostmasterContext,并将全局变量CurrentMemoryContext指向PostmasterContext。这些内存上下文的内容如下:

  • TopMemoryContext:在TopMemoryContext中分配的内存直到系统退出的时候才会释放;
  • ErrorContext:这是错误恢复处理的永久性内存空间,恢复完毕即重设;
  • PostmasterContext:这是Postmaster正常运行的内存环境。

注册信号处理函数

信号是操作系统响应某些错误状况而产生的事件,可以明确的由一个进程发送给另外一个进程,用这种方法传递信息或协调操作行为。进程可以自定义信号处理函数来处理信号,并且进程有权选择响应或屏蔽信号(SIGKILL和SIGSTOP不能被屏蔽,并且SIGKILL会直接终止进程,不给进程任何清理现场的机会)。Postmaster定义了三个信号集:1. BlockSig(屏蔽信号集);2. UnBlockSig(不希望屏蔽的信号集);3. AuthBlockSig(在进行用户连接认证时需要屏蔽的信号集)。

在设置响应信号的处理函数之前,需要通过PG_SETMASK函数将这些信号全部屏蔽,然后使用pqsignal函数为感兴趣的信号设置处理函数。

配置参数

在初始化内存环境之后,需要配置Postmaster在运行时需要的各种参数。Postgresql实现了多种配置变量,并且这些参数可能会由不同的进程在不同的时机进行配置,系统会根据既定的优先权来确定什么情况下的配置可以生效。Postmaster配置参数的基本过程如下:
初始化GUC参数:将参数设置为默认值 → 配置GUC参数:根据命令行参数配置参数 → 读取配置文件:读配置文件重新设置参数 \text{初始化GUC参数:将参数设置为默认值} \rightarrow \text{配置GUC参数:根据命令行参数配置参数} \rightarrow \text{读取配置文件:读配置文件重新设置参数} 初始化GUC参数:将参数设置为默认值配置GUC参数:根据命令行参数配置参数读取配置文件:读配置文件重新设置参数
注:按照优先等级,后面读取配置文件不能修改根据命令行配置的参数。

创建监听套接字

PostgreSQL允许通过网络或本地访问数据库。PostgreSQL网络连接使用到的数据结构如下:

  • ListenAddress:因为PostgreSQL部署的服务器可能是多接口的主机,所以其IP地址通常也会有多个;
  • ListenSocket[MAXLISTEN]:该数组用于保存监听IP地址上绑定的套接字描述符,MAXLISTEN=64,初始时全为-1;
  • addrinfo:该结构体用于保存网络通信的信息,如监听的IP地址,端口号以及协议。

创建监听套接字的流程是经典的流程:1. 创建socket对象;2. 绑定socket对象与addrinfo对象;3. listen监听绑定套接字;4. 返回一个监听套接字。

辅助进程启动

辅助进程用于帮助系统能够正常的运行,一般用于处理主进程Postmaster不会处理的事情(因为Postmaster都不会自己去干这些事,只会通过创建相应的进程去干活)。辅助进程的详细内容会在后面章节进行介绍,这里只是简单带过。

装载客户端认证文件

循环等待客户连接请求

当客户端应用与数据库进行连接时,PostgreSQL会为该用户(假设已经验证完成)创建一个Postgres的服务进程。这里面的函数调用关系如下:
main->PostmasterMain->ServerLoop->BackendStartup->BackendRun->PostgresMain \text{main->PostmasterMain->ServerLoop->BackendStartup->BackendRun->PostgresMain} main->PostmasterMain->ServerLoop->BackendStartup->BackendRun->PostgresMain

辅助进程

PostgreSQL的各个辅助进程完成各种细节的任务,并且没有辅助进程都有一个全局变量形式的进程号。辅助进程分别有:SysLogger,BgWriter,WalWriter,AutoVacu,PgArch,PgStat。本节将简要介绍各个辅助进程的功能与执行流程。

SysLogger系统日志进程

日志是数据库管理系统的重要组成部分,不管是获取数据库运行状态还是维护数据库系统。SysLogger进程的参数信息存放在文件"postgresql.conf"中,这些参数定义了包括日志输出端口,日志输出文件夹、日志文件的容量大小以及输出日志文件名的格式定义。

系统日志辅助进程的入口位置为SysLogger_Start()函数。在SysLogger_Start()函数第一次运行时,首先调用系统函数pipe创建管道syslogPipe用于接收stderr的输出,并创建日志目录和第一个日志文件,然后调用fork_process()函数fork一个子进程用于运行syslogger。在创建完子进程后,父进程将stdout,stderr重定向到日志管道的输入端。然后关闭日志管道的写入端和日志文件(因为父进程是Postmaster进程,不需要再来写日志文件,stdout和stderr的输出将直接通过管道写入到日志文件)。而在子进程中,则关闭已创建的监听套接字,并丢弃与Postmaster的共享内存的联系,最后调用SysLoggerMain()函数真正进入日志进程。

在SysLoggerMain函数中,首先对系统日志进程相关的全局变量进行赋值初始化如记录自己的进程号、获取当前启动时间、进程状态标志等,若要重新启动SysLogger进程则要对stderr和管道进行重新设置。在进入系统日志进程处理流程主循环之前,需要注册信号处理函数。完成准备工作后,程序进入一个无限循环。

每次循环将首先判断是否收到了SIGHUP信号,如收到则调用ProcessConfigFile()函数重新处理配置文件,并设置相应的参数的值。然后判断配置参数Log_directory,Log_filename和当前日志文件信息是否相同,即日志文件目录和名称是否发生修改,若修改则设置全局变量rotation_requested为ture,表示进入新的日志周期。若rotation_requested为真,则调用logfile_rotate()函数创建一个新日志文件,并关闭旧的日志文件。

接下来调用epoll_wait()监听日志管道的读取端,当监听到发生了数据读取事件时,调用read()函数从syslogPipe管道的读取段读取一定长度的数据到临时缓冲区logbuffer中。如果读取数据的长度大于0,则调用process_pipe_input()函数对读取的数据进行处理;如果长度==0,则表明已经到达了日志管道末端,当前已没有任何进程维持着管道写入端,那么退出SysLogger进程。

BgWriter后台写进程

BgWriter是PostgreSQL中在后台将脏页写出到磁盘的辅助进程,引入该进程主要为到达如下两个目的:首先,数据库在进行查询处理时若发现要读取的数据不在缓冲区中时要先从磁盘中读入要读取的数据所在的页面,此时若缓冲区已满,则需要现选择部分缓冲区的页面替换出去。如果被替换的页面没有被修改,那么可以直接丢弃;但如果要替换的页已被修改,则必需先将这页写出到磁盘中后才能替换,这样数据库的查询处理就会被阻塞。

通过使用BgWriter定期写出缓冲区中部分脏页到磁盘中,为缓冲区腾出空间,就可以降低查询处理被阻塞的可能性。其次,PostgreSQL在定期作检查点时需要把所有脏页写出到磁盘,通过BgWriter预先写出一些脏页,可以减少设置检查点时要进行的IO操作,使IO负载趋向平稳。通过BgWriter对共享缓冲区写操作的统一管理,避免了其它服务进程在需要读入新的页面到贡献缓冲区时,不得不将之前修改过的页面写出到磁盘的操作。

不过,当BgWriter无法维护足够的干净共享缓冲区时,不得不将之前修改过的页面写出到磁盘的操作。不过,当BgWriter无法维护足够的干净共享缓冲区时,其他服务进程仍然可以自行完成将脏页写回磁盘的操作。BgWriter同时页负责处理所有的检查点,也会定期地发出一个检查点请求,当然也可以由其他进程通过信号要求BgWriter执行一个检查点。

BgWriter的配置选项有3个:bgwriter_delay,bgwriter_lru_maxpages以及bgwriter_lru_multiplier。系统每隔bgwriter_delay事件启动BgWriter。BgWriter扫描缓冲区的LRU链表,写出至多bgwriter_lru_multiplier * N,并且不超过bgwriter_lru_maxpages的脏页。其中N是最近一段事件在两次BgWriter运行期间系统新申请的缓冲页数。bgwriter_delay不能太小也不能太大,太小时频繁将脏页写出,增加了数据库的IO次数。而周期太长时,又不能起到优化数据库写IO操作的作用。

该辅助进程的入口是StartBackgroundWriter()函数,该函数调用统一的子进程启动函数StartChildProcess()。创建的子进程与SysLogger进程一样,关闭已创建的监听套接字,并丢弃与Postmaster的共享内存的联系,最后调用AuxiliaryProcessMain()辅助函数进入进程主体。

在这里插入图片描述

BgWriter实际的工作函数是GackgroundWriterMain(),其处理流程如上图所示。其中注册异常处理中结合系统调用setjmp和longjmp完成,实现进程的错误处理过程。在异常处理结构中,会进行向系统日志中写错误信息、清理正在运行的缓冲区I/O等操作。(此处源码好像有所出入,所以暂时搁浅一下)

WalWriter预写式日志写进程

预写式日志WAL(Write Ahead Log,也称为Xlog)的中心思想是对数据文件进行修改之前必须要先将这些修改记录到日志中,即先写日志后写数据。(从这里可以看出,前面SysLogger是对系统的状态进行日志输出,而Wal则是为了保护数据)。

如果遵循Wal的工作原理,那么不必在每次事务提交的时候都将数据块落盘(Group Commit),因为可以在出现崩溃的情况下使用日志来恢复数据库。除此之外,使用Wal还能显著地减少写磁盘的次数,因为事务提交的时候只需要把日志文件刷新到磁盘,而不是事务修改的所有数据文件。并且,因为日志文件是顺序写的,因此同步日志的开销远比同步数据块的开销要小。

它避免了其他服务进程在事务提交时需要同步地写入预写式日志到磁盘,也使得事务提交记录不是在提交时同步地写入磁盘,而是在一个已知地预先设置地事件异步地写入。同BgWriter一样,其他服务进程在WalWriter出错时也允许直接进行预写日志写操作。

在PostgreSQL14.0中,WAL日志文件存放在数据集簇中的pg_wal目录中。该日志文件是一个段文件,每个段16MB,并分割成若干页,每页8KB。其名字由24个十六进制字符组成,分为三个部分,每个部分由8个十六进制字符组成。第一部分表示时间线,第二部分表示日志文件标号,第三部分表示日志文件的段标号。

#define XLogFilePath(path, tli, logSegNo, wal_segsz_bytes) \
	snprintf(path, MAXPGPATH, XLOGDIR "/%08X%08X%08X", til, \
	(uint32)((logSegNo) / XLogSegmentsPerXlogId(wal_segsz_bytes)), \
	(uint32)((logSegNo) % XLogSegmentsPerXlogId(wal_segsz_bytes)))

在这里插入图片描述

与BgWriter相同,WalWrtier一样是通过StartChildrenProcess()函数创建的。其主函数为WalWrtierMain(),其处理流程如上图所示。与BgWriter不同,WalWriter使用的是专属的WAL缓冲区,而BgWriter使用的缓冲区是与普通数据共享的缓冲区。

PgArch预写式日志归档进程

PostgreSQL从8.0版本开始提出了PITR(Point-In-Time-Recovery)技术,支持将数据库恢复到其运行历史中任意一个有记录的时间点。除了WalWriter外,PITR的另一个重要基础是WAL文件的归档功能。PgArch辅助进程的目标是对WAL日志在磁盘上存储形式进行归档备份。

PostgreSQL在数据集簇的pg_wal目录中始终只使用一个WAL日志文件,这个日志文件记录数据库中数据文件的每个改变。从逻辑上看WAL含有无限长的WAL,但实际上WAL会被分成多个WAL段文件存储,但是只会使用其中一个来记录WAL日志。如果当前使用的WAL段文件超过了大小限制,则会关闭当前段文件,然后新建一个段文件或重用另外一个可重复使用的段文件。为实现PITR,需要在WAL段文件被重用时进行归档备份操作,把将被重用的WAL段中的日志记录保存到其他位置。这样归档日志加上当前日志就可以形成连续的WAL日志记录。为了给数据库管理员提供最大的灵活性,PostgreSQL不对如何归档作任何假设,而是让管理员提供一个shell命令来拷贝一个完整的WAL段文件到备份存储位置。该命令可以是一个cp命令,或者是一个复杂的shell脚本,所有的操作都是由管理员决定的。

PgArch进程也是由StartChildrenProcess()函数创建,其工作函数为PgArchiverMain(),其处理流程如下图:

在这里插入图片描述

PgArch进程在工作循环中检测需要进行归档(按照文件后缀)的WAL段文件,并对其进行归档处理。然后休眠等待下一次归档。

对WAL日志的归档提供了一种新的数据库备份策略,这种策略组合了文件系统备份与WAL文件备份。在恢复时,首先恢复数据文件,然后重放WAL日志到指定的时间点。另外,分段的WAL备份可以减缓备份压力,如果某次备份失败了可以从终端备份文件开始。

AutoVacuum系统自动清理进程

在PostgreSQL数据库中,对表元组的UPDATE或DELETE操作并不会立即删除旧版本的数据,表中的旧元组只是被标记为删除状态,并未立即释放空间。这种处理对于获取多版本并发控制(Multi Version Concurrency Control)是必要的,如果一个元组的版本仍有可能被其他事务看到,那么就不能删除元组的该版本。但是当涉及的事务提交后,过期版本的元组对事务不再有效,因此其占据的空间必须回收以供其他新元组使用,以避免在磁盘空间上的增长。

AutoVacuum是PostgreSQL用于回收被标识为删除状态记录空间的进程。AutoVacuum系统包含两种不同的处理进程:AutoVacuum Launcher和AutoVacuum Worker。

AutoVacuum Launcher进程为监控进程,用于收集数据库运行信息,根据数据库状态调度一个AutoVacuum Worker进程执行清理操作。

AutoVacuum Worker进程执行实际的清理任务,在Launcher进程中,维护有一个Worker进程列表。Worker进程列表由三种不同状态的进程列表组成:空闲Worker、正在启动的Worker与运行中的Worker。Worker进程的状态变化为启动->空闲->运行。Worker的启动并不是由Launcher负责的,因为Launcher无法处理Worker异常退出时的处理。当需要启动一个Worker进程时,Launcher向Postmaster进程发送启动消息,Postmaster会fork一个进程作为Worker。

worker在启动成功并连接数据库后,将遍历该数据库中的表,根据对表的清理规则选择表与要执行的操作。对表的操作。对表格的操作分为VACUUM与ANALYZE两种。

  1. AutoVacuum Launcher进程
    Launcher进程由Postmaster进程调用StartAutoVacLauncher()创建,其主函数是AutoVacLauncherMain(),其处理流程如下图:

    在这里插入图片描述

  2. AutoVacuum Worker进程
    Launcher进程启动Worker需要进入launch_worker()函数,并在该入口处调用do_start_worker()函数向Postmaster进程发送开启一个Worker进程的信号。Postmaster在接收到该信号后,进入信号处理函数StartAutovacuumWorker(),该函数通过调用StartAutoVacWorker()真正创建Worker进程,同时进入AutoVacWorkerMain()主函数。

此处与内核分析那本书还是有一些出入,但现在只做大致的了解,后续再深入展开。

PgStat统计数据收集进程

PgStat进程是PostgreSQL数据库系统用于统计信息收集的,如在一个表和索引上进行了多少次插入与更新操作、磁盘块的数量和元组的数量、每个表上最近一次执行清理和分析操作的时间,以及统计每个用户自定义函数调用执行的时间等。系统表pg_statistic中存储了PgStat收集的各类统计信息,另外在数据库集簇中pg_stat与pg_stat_tmp子目录中存放了当前的统计信息。

PgStat收集的统计信息主要用于查询优化时的代价估算。在PostgreSQL的查询优化过程中,查询请求的不同执行方案时通过建立不同的路径来表达的。查询优化时会根据查询要求生成许多符合条件的路径,并从中选择代价最小的一条转化为执行计划,并交给执行器。造成同一个查询请求有不同路径的主要原因是Relation具有不同的访问方式,如顺序访问(Sequential Access)、索引访问(Index Access);Relation之间不同的连接方式,如嵌套循环连接(Nest-loop join)、归并连接(Merge Join)、Hash连接(Hash Join);Relation之间不同的连接顺序,如左连接(Left-join)、右连接(Right-join)、布希连接(Bushy-join)。评价不同路径优劣的方法是使用系统表pg_statistic中的系统统计信息估计出不同路径的代价。

这里需要了解的是Relation之间的连接方式与索引相关,比如当只有一个表具有索引时,往往使用Nest-loop join,并且小表,没有索引的表是外部表,而具有大表和有索引的表则充当内部表;而如果两个表都没有索引时,往往使用Hash连接;如果两个表都具有索引,并且是顺序索引时,则可以使用归并连接。

在Postmaster进程启动数据库过程完成后,将调用pgstat_init()函数对PgStat进程进行初始化操作。包括创建用于和后台进程之间通信的UDP端口(PgStat进程与后台通信的方式是通过UDP端口实现的)。PgStat进程由Postmaster调用函数pgstat_start()创建,其主函数是PgstatCollectorMain(),下图是该函数的主要流程。

在这里插入图片描述

Postgres服务进程

Postgres进程是实际的接受查询请求并调用相应模块处理查询的进程。其直接接受用户的命令进行编译执行,并将结果返回给用户。用户的命令分为DML(Data Manipulation Language)与DDL(Data Define Language)。前者主要包含对Relation的增删查改,后者则主要是对表格或者视图的结构的操作修改。Postgres进程的主要源码位于"src/backend/tcop"目录下,包括:

  • postgres.c:Postgres的入口文件,负责管理查询的整体流程;
  • pquery.c:含有DML的执行源码;
  • utility.c:含有DDL的执行源码;
  • dest.c:含有Postgres和远端客户的消息通信操作的源码,包括返回命令执行结果。

Postgres进程的主函数是PostgresMain()函数。因为Postgres既可以通过Postmaster守护进程创建,也可以通过pg_ctl命令启动,后者的启动流程如下:

在这里插入图片描述

配置参数,注册信号处理函数,初始化内存环境这些与前面Postmaster进程相似。需要注意的是为了查询客户的命令,Postgres需要创建一个名为MessageContext的内存上下文,用于存储从前端发送过来的查询命令,以及在查询过程中产生的中间数据,例如在DML中产生的分析树与计划树。每当PostgresMain进入一次主循环时,该内存的上下文将被重设,即所有经过MessageContext分配的内存块将被释放。

在主循环中,Postgres会首先释放上次查询时使用的内存,并创建新的查询输入缓冲区。(1)接着判断当前是否是空闲状态,若是则告诉前端它已经准备好进行下一次查询。(2)如果服务进程正在等待客户端输入(空闲),即允许处理异步信号。(3)调用ReadCommand()函数读取客户端命令,此处会阻塞。这里读取命令有两个地方,一个是从前端读取(客户通过网络连接发送命令),另外一个是从标准输入中读取。(4)关闭空闲的会话以及事务。(5)禁止异步信号处理。(6)检查在阻塞或休眠期间发生的事件。(7)处理客户命令

DML执行流程

Postgres通过调用exec_simple_query()函数进行处理DML命令,该函数的执行流程主要如下:

->exec_simple_query
    ->pg_parse_query()
    ->pg_analyze_and_rewrite()
    ->pg_plan_queries()
    ->CreatePortal()
    ->PortalDefineQuery()
    ->PortalStart()
    ->PortalRun()
    ->PortalDrop()

该函数中涉及了编译器、分析器、优化器与执行器的功能。这些功能会在后续过程中进行介绍与讲解。

参考资料(References)

《PostgreSQL源码分析》

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_40837929/article/details/129817180

MySQL学习笔记 01 - MySQL及Navicat Premium的简介及安装-爱代码爱编程

一、MySQL 1、MySQL简介 MySQL是一个功能齐全的关系数据库管理系统(RDBMS),可以与Oracle DB和Microsoft的SQLServer竞争。MySQL由瑞典公司MySQL AB赞助,该公司由Oracle公司拥有。 MySQL 是一个功能齐全的关系数据库管理系统(RDBMS),可以与 Oracle DB 和 Micr

Postgresql学习笔记之——流复制搭建(主从结构异步、同步流复制)-爱代码爱编程

PostgreSQL 可以通过流复制技术,从实例级复制出一个与主库一模一样的从库(也称之为备库) 举个简单的例子,在主机 local128 上创建了一个 PostgreSQL 实例,并在实例上创建多个数据库,通过流复制技术可以在另外一台主机如 local29 上创建 个热备只读 PostgreSQL实例,我们通常将 local128 上的数据库称为主库(

04_MySQL笔记-介绍-rpm安装/源码编译安装MySQL-远程连接-爱代码爱编程

文章目录 介绍rpm方式安装MySQL源码编译安装MySQL一键安装脚本远程连接 个人博客 https://blog.csdn.net/cPen_web 介绍 MySQL MySQL是一个数据库软件 数据库 database 关系型数据库 oracle(甲骨文)、MSSQL(SQL server)、MySQL、postgreSQL(P

postgresql源码学习笔记(1)-文件结构-爱代码爱编程

文章目录 引言(Introduction)文件结构(File System)参考资料(References) 引言(Introduction) Postgresql是一个跨平台开源且性能良好的关系型数据库,支

postgresql源码学习笔记(8)-事务管理-爱代码爱编程

文章目录 引言(Introduction)概述事务块接口事务块状态 事务的底层事务块操作函数 事务保存点与子事务事务保存点SAVEPOINT子事务 LocksRelation-level locksRow