大云海山数据库(He3DB)进程管理:Postmaster 架构与实现深度剖析

18 阅读12分钟

海山数据库进程管理:Postmaster 架构与实现深度剖析

注:本文涉及的源码都以 PostgreSQL 15 版本为例说明。

海山数据库采用多进程架构,每个客户端连接、后台任务都是独立的操作系统进程。这种设计带来了进程隔离、故障隔离的优势,但也引入了进程管理的复杂性:如何统一监控所有子进程?如何检测子进程崩溃?如何协调系统启动和关闭?这一切的核心是 Postmaster —— 海山数据库的进程管理器和系统协调者。本文将深入剖析 Postmaster 的架构设计、实现细节和关键技术决策。

进程管理的必要性

在深入实现之前,让我们先理解为什么海山数据库需要一个专门的进程管理器。如果没有 Postmaster,每个进程都独立管理自己,会带来一系列问题:

  1. 资源泄漏问题:子进程死亡后,如果没有父进程及时调用 waitpid() 回收,会变成僵尸进程,占用进程号。长期运行会导致 PID 耗尽,系统无法创建新进程。

  2. 崩溃感知问题:某个 Backend 进程崩溃了(如段错误),其他进程如何知道?如果没有统一的监控机制,崩溃可能被忽略,导致数据损坏持续扩散。

  3. 关闭协调问题:关闭数据库时,如何确保所有进程都优雅退出?如果进程正在处理查询,直接杀死会导致数据不一致。

海山数据库的解决方案是 Postmaster —— 海山数据库进程树的根进程,其负责:

  1. 启动时初始化系统:创建监听 socket、初始化进程间通信机制

  2. 为客户端连接分配 Backend 进程:每个客户端连接对应一个独立的 Backend

  3. 监控所有子进程的生命周期:通过 SIGCHLD 信号检测子进程死亡

  4. 协调系统关闭:确保所有进程优雅退出,数据落盘

进程管理架构

海山数据库的进程架构是"一个 Postmaster + N 个子进程",但这些子进程并不是一视同仁的。根据职责不同,Postmaster 用三种不同的方式管理它们:

Postmaster 进程管理的三层架构:

第一层:全局变量管理 (辅助进程)

├─ StartupPID → Startup Process

│ └─ 崩溃恢复进程,读取 WAL 日志将数据库恢复到一致状态

├─ CheckpointerPID → Checkpointer

│ └─ 执行检查点,将共享缓冲区的脏页写入磁盘,确保数据持久化

├─ BgWriterPID → Background Writer

│ └─ 后台定期将脏页写入磁盘,减少检查点时的 I/O 峰值

├─ WalWriterPID → WAL Writer

│ └─ 将 WAL 日志从内存刷写到磁盘,确保持久性

├─ WalReceiverPID → WAL Receiver

│ └─ WAL 接收进程(备用库),从主库接收 WAL 流并应用到本地

├─ AutoVacPID → AutoVacuum Launcher

│ └─ 自动清理启动器,负责调度 autovacuum worker 回收死元组

├─ PgArchPID → Archiver

│ └─ WAL 归档进程,将已填充的 WAL 日志文件复制到归档存储

├─ SysLoggerPID → System Logger

│ └─ 系统日志进程,收集所有子进程的日志输出并写入日志文件

├─ CleanfilecachePID → Cleanfilecache Leader (海山数据库特有)

│ └─ 清理本地缓存数据进程,控制本地缓存数据量并调整共享表模式

├─ ParallelFlushPID → Parallel Flush Worker (海山数据库特有)

│ └─ 并行回放数据页进程,多进程推进与回放 WAL 日志

└─ CleanLogindexPID → Clean Logindex To LSN (海山数据库特有)

└─ 清理并行回放链表进程,回收 Page 与 WAL 日志的链表关系

第二层:BackendList (双向链表)

└─ dlist_head 双向链表

├─ Backend 1 (客户端连接 A)

├─ Backend 2 (客户端连接 B)

└─ Backend 3 (WAL Sender)

└─ 流复制发送进程(主库),将 WAL 日志发送到备库

第三层:BackgroundWorkerList (单向链表)

└─ slist_head 单向链表

├─ RegisteredBgWorker 1 (Parallel Query Worker)

│ └─ 并行查询执行,利用多核 CPU 加速查询

├─ RegisteredBgWorker 2 (Logical Replication Worker)

│ └─ 逻辑复制 worker,捕获逻辑变更并应用到订阅端

└─ RegisteredBgWorker 3 (用户自定义扩展)

└─ 通过 bgworker APIs 注册的自定义后台任务

海山数据库在标准 PostgreSQL 基础上,针对共享存储架构引入了三个专用辅助进程,解决多节点共享存储的特殊问题:

  1. Cleanfilecache Leader:本地缓存管理。

• 当本地缓存超过预设大小时,清理可回收的缓存数据。

• 根据用户需求调整实例的共享模式(共享表 vs 非共享表)。

  1. Parallel Flush Worker:并行 WAL 回放。

• 传统单进程回放 → 改为多进程并行推进与回放。

• 充分利用多核 CPU,提升备机回放速度。

  1. Clean Logindex To LSN:回放链表回收。

• 回收并行回放后 Page 与 WAL 日志的链表关系。

• 与 Parallel Flush Worker 配套使用,确保资源及时释放。

这三个进程协同工作,使海山数据库在共享存储架构下实现高性能的主备同步。

为什么不统一用一种数据结构管理所有进程?因为每类进程有不同的特征:

辅助进程(Checkpointer、BgWriter 等):系统级服务,每个类型通常只有一个实例。用全局变量管理,O(1) 访问时间。

Backend:动态数量,随客户端连接变化。需要频繁增删,双向链表支持高效插入/删除。

Background Worker:用户可扩展的后台任务(如并行查询、逻辑复制)。需要存储额外元数据(注册信息、崩溃时间)。

这种"按需管理"的设计体现了海山数据库的工程哲学:不同场景使用不同数据结构,权衡访问时间、内存开销和代码复杂度。理解了三层架构, 接下来详解核心的数据结构。

核心数据结构

Backend 结构体

每个 Backend 进程在 Postmaster 中都有一个对应的 Backend 结构体:

// src/backend/postmaster/postmaster.c

typedef struct Backend

{

pid_t pid; // 进程 PID

int32 cancel_key; // 取消密钥

int child_slot; // PMChildSlot 索引

int bkend_type; // Backend 类型

bool dead_end; // 死端进程标志

bool bgworker_notify; // Background Worker 通知

dlist_node elem; // 双向链表节点

} Backend;

static dlist_head BackendList; // 双向链表头

关键字段解析

• pid:进程的 PID。Postmaster 通过它发送信号(如 SIGTERM、SIGQUIT)来控制进程。

• cancel_key:取消密钥。客户端用这个密钥可以取消正在执行的查询。Postmaster 验证密钥后代为发送 SIGINT。

• child_slot:Postmaster 内部的槽位索引(1 到 N),用于追踪子进程是否正确清理了共享内存资源。

• bkend_type:Backend 类型(NORMAL/AUTOVAC/WALSND/BGWORKER)。

NORMAL:普通客户端连接,执行 SQL 查询。

AUTOVAC:自动清理进程,回收死元组防止事务 ID 回卷。

WALSND:WAL Sender(流复制发送进程),将主库 WAL 发送到备库。

BGWORKER:后台工作进程(如并行查询 worker、逻辑复制 worker)。

• dead_end:是否为死端进程(只发送错误消息后退出)。

• elem:双向链表节点,用于链入 BackendList。

BackendList:双向链表管理

Postmaster 使用双向链表 BackendList 管理所有 Backend 进程:

  1. 动态进程追踪:管理所有客户端连接、autovacuum worker、WalSender 等动态创建的进程。

  2. 快速查找和删除:双向链表支持 O(1) 的插入和删除操作。

  3. 遍历信号发送:崩溃时遍历所有 Backend 发送 SIGQUIT。

BackednList的典型操作如下(src/backend/postmaster/postmaster.c):

// 添加 Backend(fork 成功后)

static bool assign_backendlist_entry(RegisteredBgWorker *rw)

{

Backend *bn;

bn = malloc(sizeof(Backend));

bn->pid = worker_pid;

bn->child_slot = MyPMChildSlot;

bn->bkend_type = BACKEND_TYPE_BGWORKER;

// 加入双向链表头部 - O(1)

dlist_push_head(&BackendList, &bn->elem);

rw->rw_backend = bn;

return true;

}

// 删除 Backend(进程退出时)

dlist_foreach_modify(iter, &BackendList)

{

Backend *bp = dlist_container(Backend, elem, iter.cur);

if (bp->pid == dead_pid)

{

// 从双向链表删除 - O(1)

dlist_delete(iter.cur);

free(bp);

break;

}

}

// 遍历发送信号(崩溃处理时)

dlist_foreach(iter, &BackendList)

{

Backend *bp = dlist_container(Backend, elem, iter.cur);

if (!bp->dead_end)

{

signal_child(bp->pid, SIGQUIT); // 发送信号

}

}

与辅助进程不同(每个类型只有一个实例,如 CheckpointerPID),Backend 的数量是动态的(随客户端连接变化),无法用固定数量的全局变量管理。双向链表提供了:

动态扩容:可以处理任意数量的连接(受 max_connections 限制)。

高效管理:插入删除都是 O(1) 操作。

简洁代码:一个循环即可遍历所有 Backend。

服务端 Postmaster 启动流程

Postmaster 的生命周期始于 PostmasterMain() 函数。让我们先看整体流程:

调用栈(谁调用了 PostmasterMain):

操作系统内核

└─ main() (src/backend/main/main.c)

└─ PostmasterMain (src/backend/postmaster/postmaster.c:574)

├─ ProcessConfigFile(PGC_POSTMASTER)

├─ pqsignal_pm(SIGCHLD, reaper)

├─ StreamServerPort() // 创建监听 socket

├─ StartChildProcess(StartupProcess)

└─ ServerLoop()

ServerLoop:I/O 多路复用

ServerLoop 使用 select() 实现 I/O 多路复用,监听多个监听 socket:

// src/backend/postmaster/postmaster.c:1727

static void ServerLoop(void)

{

for (;;)

{

// 1. select() 监听所有 ListenSocket

selres = select(ListenSocketCount, ListenSocket,

NULL, NULL, &timeout);

if (selres < 0 && errno == EINTR)

{

// 被信号中断,重新计算状态后继续

goto restart;

}

else if (selres > 0)

{

// 2. 有 socket 可读

for (i = 0; i < ListenSocketCount; i++)

{

if (FD_ISSET(ListenSocket[i], &readfds))

{

Port *port = ConnCreate(ListenSocket[i]);

BackendStartup(port);

}

}

}

// 3. 处理信号、状态机转换

PostmasterStateMachine();

}

}

ListenSocket[] 数组:支持多网卡和 Unix socket。

// ListenSocket[0] = 192.168.1.10:5432

// ListenSocket[1] = 192.168.1.11:5432

// ListenSocket[2] = Unix socket /tmp/.s.PGSQL.5432

理解了启动流程,我们来看 Postmaster 如何处理客户端连接。

客户端 Postgres 连接处理

当客户端连接进来时,Postmaster 会 fork 一个 Backend 进程处理该连接。

Backend 初始化序列

Backend 进程启动后执行一系列初始化:

调用栈(Postmaster 如何启动 Backend):

ServerLoop (src/backend/postmaster/postmaster.c:1727)

└─ BackendStartup (src/backend/postmaster/postmaster.c:4167)

└─ fork_process()

└─ [子进程]

├─ InitPostmasterChild() (src/backend/utils/init/miscinit.c:96)

├─ ClosePostmasterPorts() (src/backend/postmaster/postmaster.c:4230)

├─ BackendInitialize(port) (src/backend/postmaster/postmaster.c:4237)

├─ InitProcess() (src/backend/storage/lmgr/proc.c:301)

└─ BackendRun(port) (src/backend/postmaster/postmaster.c:4243)

└─ PostgresMain(port->database_name, port->user_name)

Backend的初始化的关键点是:

InitPostmasterChild:设置 IsUnderPostmaster = true,初始化 Latch(进程间通信机制)。

ClosePostmasterPorts:关闭所有 ListenSocket(子进程不需要监听新连接)。

ProcessStartupPacket:读取客户端发送的 startup packet(包含 database_name、user_name)。

InitProcess:从共享内存分配 PGPROC slot。

BackendRun:调用 PostgresMain,进入查询处理循环。

BackendRun 的 port 参数**。**port 结构体传递客户端连接信息(socket、database_name、user_name),PostgresMain 用这些信息进行认证和查询处理。

理解了正常流程,我们来看异常情况:进程崩溃时如何处理?

进程崩溃检测

当子进程死亡时,内核向父进程发送 SIGCHLD 信号。Postmaster 注册 reaper() 作为 SIGCHLD 处理函数。

waitpid() 如何知道哪个进程死了

答案在于 waitpid() 系统调用的返回值:

// src/backend/postmaster/postmaster.c:2986

static void reaper(SIGNAL_ARGS)

{

int pid;

int exitstatus;

// 循环回收所有僵尸进程

while ((pid = waitpid(-1, &exitstatus, WNOHANG)) > 0)

{

// pid 就是死掉的进程的 PID!

if (pid == StartupPID)

{

// ... 拉起其他辅助进程

}

else if (pid == BgWriterPID)

{

BgWriterPID = 0;

if (!FatalError) // 非 FatalError 状态,重启辅助进程

BgWriterPID = StartBackgroundWriter();

}

// ... 其他辅助进程

else

{

// 不是辅助进程,就是 Backend

HandleChildCrash(pid, exitstatus);

}

}

}

waitpid() 的函数签名:pid_t waitpid(pid_t pid, int *status, int options)。详细参数如下表:

参数

含义

pid

-1

等待任意子进程

status

&exitstatus

输出参数,返回子进程退出状态

options

WNOHANG

非阻塞:如果没有子进程死亡,立即返回 0

返回值语义:(1) > 0:死掉的子进程的 PID;(2) = 0:没有子进程死亡(WNOHANG 模式);(3)-1:出错(如没有子进程)。

循环必要性:如果多个子进程同时死亡(如批量 kill),SIGCHLD 只发送一次(Unix 信号不排队),但 waitpid() 的循环可以回收所有僵尸进程。

进程死亡处理

当进程死亡时,Postmaster 的处理策略截然不同:

(1)辅助进程(如 BgWriter)

if (pid == BgWriterPID)

{

BgWriterPID = 0;

if (!FatalError)

BgWriterPID = StartBackgroundWriter();

}

如果 FatalError == false(系统健康),Postmaster 会立即重启辅助进程。这是因为辅助进程是"基础设施",必须存在。但前提是系统没有致命错误。

(2)Backend 进程

else

{

HandleChildCrash(pid, exitstatus);

}

Backend 进程死亡不会自动重启,而是调用 HandleChildCrash()。为什么?因为 Backend 处理客户端连接,如果它崩溃可能是因为 SQL 逻辑错误或内存 bug,重启没有意义,客户端应该重新连接。更重要的是,Backend 崩溃可能意味着共享内存已损坏,继续运行会破坏数据一致性。因此 HandleChildCrash 会设置 FatalError = true,阻止所有自动重启,并让系统进入关闭流程。

这个"Fail-Fast"策略是海山数据库高可靠性的关键:宁可整个系统停止,也不允许在可能损坏的状态下继续运行

HandleChildCrash:崩溃处理的完整流程

当子进程异常退出时,Postmaster 需要快速响应并采取行动。HandleChildCrash() 就是这个紧急响应机制的核心。

整体流程:从崩溃到系统保护的连锁反应

当某个 Backend 崩溃时,HandleChildCrash() 执行一系列精心设计的操作,目标是最大限度保护数据一致性。整体流程可以分为四个阶段:

  1. 确认崩溃严重性 - 检查退出状态,判断是否需要触发全系统关闭

  2. 清理死亡进程 - 释放 PMChildSlot,从进程列表中移除

  3. 通知其他进程 - 向所有存活的进程发送 SIGQUIT,让它们优雅退出

  4. 进入保护模式 - 设置 FatalError 标志,转换状态机到 PM_WAIT_BACKENDS

这个"连锁反应"设计确保:一旦检测到任何 Backend 崩溃,整个系统迅速进入安全状态,而不是继续运行可能损坏的数据。

调用栈(谁调用了 HandleChildCrash):

内核发送 SIGCHLD

└─ reaper() (src/backend/postmaster/postmaster.c:2986)

└─ HandleChildCrash() (src/backend/postmaster/postmaster.c:3489)

├─ LogChildExit(LOG, procname, pid, exitstatus)

├─ SetQuitSignalReason(PMQUIT_FOR_CRASH)

├─ slist_foreach(&BackgroundWorkerList)

│ └─ ReleasePostmasterChildSlot() + signal_child(..., SIGQUIT)

├─ dlist_foreach_modify(&BackendList)

│ └─ ReleasePostmasterChildSlot() + signal_child(..., SIGQUIT)

├─ signal_child(StartupPID, SIGQUIT)

├─ signal_child(BgWriterPID, SIGQUIT)

├─ signal_child(CheckpointerPID, SIGQUIT)

├─ FatalError = true

├─ pmState = PM_WAIT_BACKENDS

└─ AbortStartTime = time(NULL)

FatalError 防止在损坏状态下继续运行

一旦检测到 Backend 崩溃,Postmaster 会立即设置 FatalError = true,这个标志就像一个"紧急刹车",防止系统在可能损坏的状态下继续运行。

if (Shutdown != ImmediateShutdown)

FatalError = true; // 设置致命错误标志

FatalError 标志控制辅助进程是否自动重启:

// reaper() 中

if (pid == BgWriterPID)

{

BgWriterPID = 0;

if (!FatalError) // 只有 FatalError == false 时才重启

BgWriterPID = StartBackgroundWriter();

}

(1)设置 FatalError 的时机

  1. Backend 异常退出(exit status != 0)- 检测到用户进程崩溃。

  2. Checkpointer fork 失败(无法创建 checkpoint 进程)- 系统资源耗尽

(2)清除 FatalError 的时机

  1. Startup 进程成功完成恢复(exit status == 0)- 系统恢复到一致状态

这个标志的设计体现了"Fail-Fast"原则:一旦发现异常,立即停止所有操作,避免错误扩散。

结语

Postmaster 本身不处理 SQL 查询,但它是整个海山数据库进程架构的基石。理解了 Postmaster,就理解了海山数据库如何通过精心设计的进程管理、信号处理和状态机,实现高可用性、高可靠性的数据库系统。