PostgreSQL postmaster代码深入探究, 从启动到关闭的逻辑
PostgreSQL postmaster代码深入探究, 从启动到关闭的逻辑
您是否好奇过 PostgreSQL 从启动到关闭过程中内部发生了什么,以及各个进程是如何协同工作的?本篇博文将带您了解整个过程。
以下内容翻译自: https://www.enterprisedb.com/blog/postgres-internals-deep-dive-process-architecture
也可使用 deepwiki 帮助阅读Postgres postmaster代码:
https://deepwiki.com/postgres/postgres
PostgreSQL postmaster代码深入探究, 从启动到关闭的逻辑
流程架构
当我们使用 pg_ctl 启动 postgres 时,实际上启动的是“postmaster”,它是大多数后续进程的父进程。Postmaster 运行 main() 函数,该函数调用 PostmasterMain 函数,并在那里执行一些重要的工作:
1、初始化,例如 GUC 选项
2、解析 postgres 二进制命令行参数并相应地设置与其相关的 GUC。
3、对 datadir、锁文件(记录ipc信息的文件, 防止重复启动pg实例)、控制文件进行一些检查;并创建具有postmaster status 和 pid 的 datadir 锁文件,用于确保对于 1 个集群,只有 1 个server或一个postmaster运行。
4、如果有的话,加载共享库;同时计算核心server和库/扩展需要多少共享内存。
5、分配共享内存段。
6、接下来是主循环,即 ServerLoop,所有子进程都从这里启动。这个循环永不结束。如果结束,server就会宕机。
ServerLoop(主循环)
这是一个无限事件循环,等待 信号处理程序设置的闩锁,或任何套接字上待处理的新连接。
在这个循环中,子进程启动,并且当同一个进程终止时,也会进行清理。来自backends的请求/信号在这里处理。
在 ServerLoop 中,postmaster 会持续检查是否有任何所需进程由于某些原因未运行,或者可能需要启动一些 IO 工作进程(后台进程)。LaunchMissingBackgroundProcesses 会启动所需的子进程。
启动子进程
postmaster 的子进程有:
io workers :它们有助于异步读取block,从而减少普通backends的负载, checkpointer :用于不时删除/回收旧的 WAL 并从共享缓冲区中刷出脏页, bgwriter :仅从共享缓冲区中刷出脏页,通过维护尽可能多的空闲共享缓冲区来减少用户backends的负载, startup process :在主节点上, 用于异常停库后启动数据库时重播 WAL,直到恢复完成. 但在副本(replica)节点上,它会连续重播复制到此节点的 WAL, wal writer :此过程将 WAL 从 WAL 缓冲区刷到磁盘, Autovacuum launcher :它会定期启动 autovacuum-worker 进程,用于清理dead tuple/dead index item, 可能还有更多独立功能的process,我认为我们可以针对这些过程撰写单独的博客,因此这里不再详细介绍每个过程。
在 PostgreSQL 中,新的子进程会根据需要以不同的方式fork。
server启动时会立即创建一些子进程。之后,可以根据需要创建其他子进程,例如:
backends进程(处理客户端连接) 后台工作者(用于运行用户提供的代码,例如来自扩展extension的代码)
大多数子进程都是使用 postmaster_child_launch 函数启动的。该函数从BackendType枚举中获取一个值 ,该值决定了该特定类型子进程需要调用的正确主函数。
backends进程
默认情况下,Postmaster 将监听 listen_addresses 上的“localhost”以查找来自 psql 客户端的请求。psql 尝试使用 libpq 库连接到 Postmaster 地址,libpq 库是 postgres 套接字之上的框架。
然后,postmaster接受连接请求并启动一个新的backends进程来处理从现在开始来自 psql 客户端的查询。由于新的backends是postmaster的子进程,它继承了其中的共享内存知识,这意味着该进程也可以使用共享缓冲区,因为指针(例如 BufferBlocks)是由postmaster启动的。
每个 psql 客户端进程都会在server上生成一个新的子进程。当查询需要某个block时,如果该block尚未存在于共享内存缓冲区中,它会从磁盘读取该block。如果由于所有缓冲区都正在使用中而没有空缓冲区可以读取新block,则backends会尝试寻找一个牺牲缓冲区(根据LRU算法选择dirty page或不常被访问的block),强制刷新该block,然后将block从磁盘读取到该缓冲区中。
如果发生更多读取,则缓冲区将很容易被填满,因此要将新block读入共享缓冲区,处理查询的backends将必须自行刷新block,因此查询执行会变慢。
为了减少backends将脏缓冲区刷新到磁盘的额外工作,另外两个进程 —— 检查点进程(checkpointer)和后台写入进程(bgwriter) —— 可以提供帮助。这两个进程会以不同的时间间隔将脏页从共享缓冲区刷新到磁盘,从而减轻backends的负载。
伪装的backends进程 (wal sender)
在复制期间,当副本节点启动时,启动进程会启动 walreceiver 进程,并尝试读取 WAL。然后,walreceiver 进程使用 libpq 向主server的 postmaster 发送一个带有“复制”消息的连接请求。
Postmaster 为该请求启动一个正常的backends进程,但是当新fork的backends进程使用来自 walreceiver 的ProcessStartupPacket解析启动包时,它会查找“replication”,如果存在,则backends将变为 walsender。
Walsender 会从磁盘(也可能是os page cache中)读取 WAL,并在物理复制中将其发送给 Walreceiver。Walsender 会解码 WAL 中的语句,并在逻辑复制中发送给 Walreceiver。因此 Walsender 作为一个普通的backends进程启动。
background process
这些进程用于运行用户提供的代码,例如来自扩展程序的代码。如果扩展程序想要运行此类进程,则应在 扩展程序的 PG_init() 中使用RegisterBackgroundWorker注册后台工作线程。
因此,当 postmaster 获悉 shared_preload_libraries 中提到的扩展时,会调用相应的 _PG_init() 函数,然后将 bg worker 添加到 postmaster 的 BackgroundWorkerList 列表中。当 postmaster 在 ServerLoop 循环中调用 perhaps_start_bgworkers 时,它们就会被启动。
即使在server启动并运行后,也可以通过使用 RegisterDynamicBackgroundWorker来启动这些background进程。
还有一些子进程,例如“logical replication launcher”,它们是在server启动期间与postmaster的其他子进程一起启动的后台工作进程。
通过信号请求
Postmaster 有信号处理程序来处理以下任何类型的请求:
我们可以在postmaster.c中找到以下处理程序、信号处理器的所有定义 。
当postmaster收到特定信号时,它将调用相应的处理程序设置相关变量(ServerLoop进程循环过程中会检查这些变量?),从而强制 ServerLoop 调用相应信号处理函数来执行请求的工作。
如果有人想重新读取 postgresql.conf,他们可以向 postmaster 进程发送 SIGHUP。SIGUSR1 用于处理代表来自backends的请求的 pmsignal 信号,并检查来自 pg_ctl 的promote或logrotate请求。
下面我们将更详细地讨论shutdown信号和 SIGCHLD。
shutdown 模式
在 Postgres 中,根据关闭方式,我们有三种关闭server的模式。基本上,我们可以直接向 Postmaster 发送与关闭模式相关的信号,或者让 pg_ctl 使用“-m”选项为我们执行同样的操作。
在smart模式下,禁止新建连接, 同时要等所有客户端断开连接后, server退出。
在fast模式下,未完成的事务被回滚, server直接退出,并正确关闭(默认)。
在immediate模式下,server退出而不完全关闭(非正常关闭数据库);这将导致重新启动时需要恢复数据库实例。
现在来看看内部的东西!当 postmaster 收到上述任何信号时,都会调用相应的处理程序,将 pending_pm_shutdown_request 变量设置为 true,并发送特定信号变量,从而使 ServerLoop 调用 process_pm_shutdown_request() 并设置特定模式。根据模式的不同,关闭方式也会有所不同。
我们可以在这里的代码中看到下面提到的所有 pmStates 。
在smart模式下,如果 postmaster 正在运行,则意味着它将处于 PM_RUN 或 PM_HOT_STANDBY pmState,因此它将等待客户端完成其工作,但从现在开始将不允许建立新连接。当我说“wait”时,基本上是在循环进入其正常工作状态时,当客户端完成其工作并退出时,这是 postmaster 的子进程退出,因此它会触发 process_pm_child_exit 来清理backends进程,并且最初由于smart关闭信号 connsAllowed 设置为 false,这有助于我们在 PostmasterStateMachine 中将 pmState 设置为 PM_STOP_BACKENDS,从而确保所有子进程退出并且 postmaster 关闭。
但是如果我们处于 PM_STARTUP 或 PM_RECOVERY 状态,其中不允许任何连接,我们不需要等待任何人并直接停止其他子进程,它会将 pmState 更新为 PM_STOP_BACKENDS。
在fast模式下,在上述 4 个 pmState 中的任何一个中,子进程都会直接停止,这意味着 pmState 更改为 PM_STOP_BACKENDS,postmaster不会等待客户端完成其工作。
在immediate模式下,postmaster 会向所有子进程(如果已启用 syslogger 除外)发送 SIGQUIT,对于此信号,在每个子进程启动期间 都会调用 InitPostmasterChild ,其中会为 SIGQUIT 设置 SignalHandlerForCrashExit 处理程序,这会使进程返回值exit(2)。
如果启用了 syslogger,那么当所有进程(甚至包括 postmaster)都退出时,该进程就会退出,因为所有进程都退出时,它会在 syslog 管道上看到 EOF。
在smart和fast模式下,关闭期间会进行一次检查点。由于某种原因,如果检查点未运行,则postmaster会启动它并发出 SIGINT 信号,这会使检查点调用 ReqShutdownXLOG ,从而设置 ShutdownXLOGPending。
现在,检查点会在ShutdownXLOG中创建一个 带有 CHECKPOINT_IS_SHUTDOWN 和 CHECKPOINT_IMMEDIATE 标志的检查点。然后,检查点会向 postmaster 发出信号,告知它已完成检查点操作,postmaster 随后会使用 SIGUSR2 信号通知 walsender 关闭进程。等待 IO workers 退出后,postmaster 最终会向检查点发出 SIGUSR2 信号,从而设置 ShutdownRequestPending,这会使 checkpointerMain 循环中断并正常退出。
Reaper
现在我们来看看 Postmaster 进程如何处理子进程的死亡/退出。在 PG v15 之前,负责子进程死亡清理的函数被称作 “reaper”。这名字挺酷的。
每当子进程退出时,postmaster(父进程)都会收到一个 SIGCHLD 信号。当postmaster收到此信号时,将调用 handle_pm_child_exit_signal,其中 pending_pm_child_exit 设置为true。
当postmaster在 ServerLoop 中循环时,它会检查 pending_pm_child_exit 是否为true,它会调用 process_pm_child_exit() 来清理子进程。
如果子进程成功退出 exit(0),它会通过正常关闭释放子进程使用的所有资源。如果子进程异常退出, HandleChildCrash API 会记录并处理退出事件,方法是向除 syslogger 之外的所有子进程发送 SIGABRT/SIGQUIT 信号(如上所述)。
当postmaster意识到由于使用在 HandleChildCrash 期间设置的 FatalError 导致进程异常退出而导致所有子进程都消失时,它会重新启动server,启动过程将再次启动,崩溃恢复开始。
如果启动过程正常退出,则意味着server现在一致并准备好接受新连接,但在副本节点启动中将始终运行以重放从主节点接收的 WAL。