☰
PostgreSQL启停重启:systemctl与pg_ctl调用链
2026/11/2 12:44:13 网站建设 项目流程

改完几个参数,随手敲了一条systemctl restart postgresql,然后去泡了杯咖啡,回来发现监控在报警——数据库没起来。这种场景我见过不止一次,出问题的往往不是 PostgreSQL 本身,而是操作者对"重启"这条命令背后到底发生了什么没有概念。很多人把 PostgreSQL 的启停理解成一个按钮,按下去就停,再按一下就走,实际上这条命令至少要穿过三层:操作系统的服务管理器、pg_ctl这个封装工具、以及postmaster主进程自身的信号处理逻辑。这三层里任何一层的语义理解错了,维护窗口里就会翻车。

这篇内容围绕 PostgreSQL 的启动、停止与重启命令展开,重点不是把手册上的参数罗列一遍,而是把每条命令"为什么这么写""什么时候不能用""出问题往哪看"讲透。适合已经能连上数据库、但还没真正管过实例的开发者,也适合那些被postmaster.pid already exists或the database system is shutting down卡过半小时的运维同学。下面所有例子都基于常见的 Linux 环境,命令路径会随安装方式不同而变化,我会在对应位置标出来。

1. 重启之前先搞清三层调用关系:systemctl 不是 PostgreSQL 本身

1.1 一条 restart 命令到底经过了谁

在大多数发行版里,你敲下的systemctl restart postgresql并不会直接和数据库进程对话。systemd 拿到这条指令后,会去读对应的 unit 文件,找到ExecStop和ExecStart两行,然后按顺序执行。以常见的软件包安装为例,unit 文件里写的通常是ExecStart=/usr/pgsql-15/bin/pg_ctl start -D ${PGDATA} -s -w -t 300和ExecStop=/usr/pgsql-15/bin/pg_ctl stop -D ${PGDATA} -s -m fast,也就是说 systemd 只是个调度者,真正干活的是pg_ctl。

pg_ctl再往下走一层,它会 fork 出postgres进程(也就是postmaster),然后根据你给的-m参数,给这个进程发送不同的 Unix 信号。postmaster收到信号后,才开始拒绝新连接、等待或终止会话、做检查点、刷脏页、最后退出。这条链路是有先后依赖的,任何一环超时或失败,整个 restart 就会卡住。

理解这层关系最直接的好处是:当systemctl restart报出 "Job for postgresql.service failed" 时,你知道该去看/var/lib/pgsql/15/data/log/下的数据库日志,而不是只盯着journalctl。systemd 只负责告诉你"我启动失败了",失败原因九成写在数据库自己的日志里。

1.2 三种安装形态对应三条完全不同的命令路径

同样是 PostgreSQL,安装方式不同,启停命令长得完全不一样,这是新手最容易混淆的地方。我把常见的三种情况列成表,你可以对照自己环境确认:

安装方式启动停止重启数据目录典型位置
发行版软件包(systemd 管理)systemctl start postgresql-15systemctl stop postgresql-15systemctl restart postgresql-15/var/lib/pgsql/15/data或/var/lib/postgresql/15/main
源码编译或二进制包pg_ctl start -D $PGDATA -l logfilepg_ctl stop -D $PGDATA -m fastpg_ctl restart -D $PGDATA -m fast自己指定的目录,通常在/usr/local/pgsql/data
容器化部署docker start <容器名>docker stop <容器名>docker restart <容器名>镜像约定的/var/lib/postgresql/data

有个细节值得注意:使用 systemd 管理时,不要直接再用pg_ctl stop去停实例。因为 systemd 并不知道你在外面手动停过一次,它记录的仍是在运行状态,下一次systemctl start会直接失败,因为它认为服务已经在跑了。反过来说,如果你手动pg_ctl start起了实例,再执行systemctl stop,systemd 会因为找不到自己启动时记录的 PID 而报错。两种管理方式混用,是生产事故的高发点。

1.3 启动前必须确认的四件事

很多人习惯直接敲命令,失败了再回头查。其实在start之前花三十秒确认四件事,能省掉后面半小时的排查。

第一件事是数据目录在哪。pg_ctl的-D参数是必填的,如果没有设置PGDATA环境变量,就必须显式指定。数据目录里需要有PG_VERSION文件和postgresql.conf,如果目录为空,initdb就不会被自动触发,pg_ctl会直接告诉你目录无效。

第二件事是用哪个用户运行。PostgreSQL 拒绝以超级用户身份启动,你以root身份执行pg_ctl start会立刻收到 "cannot be run as root" 的错误。正确做法是切换到数据库专用账户,比如su - postgres之后再执行。

第三件事是配置能不能通过语法检查。postgresql.conf里一个漏掉的分号、一个拼错的参数名、一个超出范围的值,都会让启动直接失败。可以在启动前用postgres -C shared_buffers -D $PGDATA这样的方式做一次参数读取,能提前暴露大部分配置问题。

第四件事是端口有没有被占。同一台机器上跑多个实例时尤其容易撞车。用ss -lntp | grep 5432看一眼,比等启动失败再看日志快得多。

2. pg_ctl 的三种停止模式:smart、fast、immediate 的信号真相

2.1 三种模式背后的三个信号

pg_ctl stop的-m参数有三个取值,它们最终对应三个不同的 Unix 信号,行为差异极大。这个对应关系值得记住,因为当你用kill命令直接操作 PID 时,发的信号本质上是同一套语义。

模式对应信号行为
smartSIGTERM等待所有客户端主动断开、等待在线备份结束,再关闭。期间拒绝新连接
fastSIGINT立即断开所有客户端,回滚进行中的事务,做一次检查点,然后关闭
immediateSIGQUIT不等待任何东西,不写检查点,进程直接退出,下次启动走崩溃恢复

fast是默认值,也是最常用的。smart的名字听起来最"温柔",实际上它在生产环境里风险最大,因为只要有连接池保持着一批长连接,或者有人忘了关一个psql会话,它就会无限期地等下去。我在一个项目里见过smart模式等了四十多分钟,最后发现是一个监控 agent 每五秒建一次连接,恰好卡在中间。

immediate则走向另一个极端。它不做检查点,数据文件可能处于不一致状态,下次启动必须重放 WAL 到一致点。数据库小的时候还好,如果实例有几百 GB 数据再加上一个较长的检查点间隔,崩溃恢复可能要跑十几分钟,而且期间是完全不可用的。所以它适合的场景很窄:进程卡死到信号都处理不了,或者磁盘即将写满必须立刻停机。

2.2 为什么生产环境几乎只该用 fast

fast模式的设计目标很明确:在保证数据一致性的前提下,用最短时间完成关闭。它会主动断开所有客户端连接,把每个活动事务回滚掉,然后强制执行一次检查点,把所有脏页刷到磁盘。完成之后,pg_control里的状态被标记为 "shut down",下次启动不需要任何恢复过程,直接就能对外服务。

这和immediate的差别在启动成本上体现得最明显。用fast停掉的实例,重启后几秒钟就能接受连接;用immediate停掉的实例,重启要先做崩溃恢复,日志里能看到 "database system was not properly shut down; automatic recovery in progress" 这行字,恢复期间连接是被拒绝的。

不过fast也有代价。如果关闭时正好有一个执行了两小时的大事务在跑,回滚它可能比正向执行还慢,因为回滚要遍历所有已写入的页面。这时候重启就会表现成"卡住",实际上postmaster一直在干活,只是没有输出。判断方法看日志或者pg_stat_activity里状态为idle in transaction (aborted)的会话。

2.3 停止命令里那些容易被忽略的参数

pg_ctl stop除了-m,还有几个参数在实际运维里很有用。-w表示等待操作完成再返回,-t指定等待的秒数,默认是 60 秒。当你写自动化脚本时,这两个参数必须显式给出来,否则脚本可能在实例还没完全关闭时就继续往下执行,导致后续操作全乱。

一个比较稳妥的写法是这样:

pg_ctl stop -D /var/lib/pgsql/15/data -m fast -w -t 120

这里的-t 120是根据实例规模估出来的。经验值是:数据量每 100 GB 大概给 30 秒余量,如果你有 400 GB 的数据,120 秒是个合理的上限。超时之后pg_ctl会返回非零退出码,脚本就能据此判断"关不干净",而不是傻等。

还有一个-s参数表示静默模式,只输出错误。在 cron 或自动化脚本里加上它,日志会干净很多。但排查问题时千万别加,否则你连"关闭成功"这句话都看不到。

3. 启动命令的细节:参数、日志与那几个绕不开的坑

3.1 start 的常用参数组合

pg_ctl start最基础的用法是pg_ctl start -D $PGDATA,但这个写法有个问题:默认情况下服务端的输出会打到当前终端上,一旦你退出登录,实例可能跟着挂掉。所以生产环境基本都配合-l把日志重定向到文件:

pg_ctl start -D /var/lib/pgsql/15/data \ -l /var/lib/pgsql/15/data/log/pgstart.log \ -w -t 60

-l指定的文件是pg_ctl自己捕获的 stdout/stderr,不是数据库的logging_collector日志。这两个是不同层次的东西,排查启动阶段的错误(比如配置解析失败)要看前者,运行期的慢查询、死锁要看后者。

-o参数用来给postgres进程传递额外的命令行选项,这个在临时改端口或改监听地址时特别好用:

pg_ctl start -D $PGDATA -o "-p 5433 -c listen_addresses='127.0.0.1'"

注意这里的引号层级,-o后面的内容会被原样传给postgres,所以内部的引号要处理好。这种临时覆盖不会写入配置文件,重启后失效,适合做故障隔离或者临时起一个测试实例。

3.2 postmaster.pid 残留:最常见的启动拦路虎

启动时报出这一串:

FATAL: lock file "postmaster.pid" already exists HINT: Is another postmaster (PID 12345) running in data directory "/var/lib/pgsql/15/data"?

这是 PostgreSQL 启动阶段最高频的错误。postmaster.pid文件里记录了七个字段:进程 PID、数据目录路径、实例启动时间、端口号、socket 目录、监听地址、共享内存键值。它的作用是防止同一个数据目录被两个实例同时打开。

出现这个错误,只有两种可能:要么真的还有实例在跑,要么上一次实例被强制杀掉(比如kill -9、OOM killer、宿主机断电)导致文件没被清理。先别急着删文件,用ps -p <PID>确认一下那个 PID 是否还活着。如果进程确实不存在了,再删。直接删文件而进程还在,会造成两个实例同时写同一份数据,后果不用多说。

确认进程已死的情况下,处理步骤是:

cat /var/lib/pgsql/15/data/postmaster.pid # 记下第一行的 PID ps -p <PID> # 确认进程不存在 rm /var/lib/pgsql/15/data/postmaster.pid # 才能删 pg_ctl start -D /var/lib/pgsql/15/data -l log/pgstart.log -w

还有一种隐蔽情况:PID 被系统复用给了别的进程。这时候ps -p会显示一个存在的进程,但它显然不是 postgres。判断依据是看进程名,或者用fuser检查数据目录是否被占用:fuser /var/lib/pgsql/15/data/postmaster.pid。这个坑我踩过一次,删完文件才发现是一个无关的后台任务恰好拿到了同一个 PID,白紧张一场。

3.3 数据目录权限与运行用户:那条绕不过去的 0700

PostgreSQL 对数据目录权限的要求非常严格,目录必须是0700,属主必须是运行实例的用户。这个限制不是洁癖,是因为数据目录里有表空间文件、WAL、配置文件,其中某些文件包含敏感信息,权限放开就等于把数据暴露给同机的其他用户。

启动时如果权限不对,日志里会出现 "data directory has invalid permissions" 或 "could not access directory ... Permission denied"。修复很简单:

chown -R postgres:postgres /var/lib/pgsql/15/data chmod 700 /var/lib/pgsql/15/data

但有个陷阱:不要对整个数据目录做chmod -R 700。数据目录下的普通文件权限一般是0600,如果全刷成0700,目录还能用,但某些场景下(比如 pg_basebackup 的备份目录处理)会出现奇怪的问题。正确的做法是只对目录本身设0700,文件保持0600。

另一个高频错误是could not create lock file "/tmp/.s.PGSQL.5432.lock",这通常是因为 Unix socket 目录/tmp或/var/run/postgresql的权限不归 postgres 用户所有。软件包安装的环境里,这个目录一般由 tmpfiles.d 机制自动维护,源码编译的环境就需要手动处理。可以用unix_socket_directories参数把它改到一个 postgres 有写权限的位置。

4. 重启卡住时的排查链路:先分清是停不下去还是起不来

4.1 卡在哪个阶段,看日志时间戳就能判断

执行pg_ctl restart之后没有任何输出,命令一直挂着,这是最让人焦虑的情况。这时候先别乱按 Ctrl+C,先看日志文件最后几行的时间和内容。

如果日志停在 "waiting for server to shut down...",说明卡在停止阶段;如果日志显示 "database system is shut down" 之后就没有新内容了,说明停止成功了但启动失败,问题在启动侧。

停止阶段的日志通常长这样:

LOG: received fast shutdown request LOG: aborting any active transactions LOG: waiting for ... LOG: database system is shut down

从 "received fast shutdown request" 到 "database system is shut down" 之间的时间,就是实际关闭耗时。如果这个间隔超过预期,往下走。

4.2 停不下去的三种典型原因

第一种是长事务回滚。前面提过,fast模式要回滚所有活动事务。如果有个事务已经跑了很久、写入了大量页面,回滚耗时会非常可观。这时候在另一个能连上的会话里查:

SELECT pid, now() - xact_start AS duration, state, query FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY duration DESC LIMIT 5;

注意关闭过程中新连接是进不来的,这个查询要在发起 stop 之前就跑好,或者从已经建立的其他连接里执行。

第二种是在线备份未结束。如果有pg_basebackup正在跑,或者有人手动执行了pg_start_backup()却没调用pg_stop_backup(),fast模式会等备份结束。这类问题在日志里能看到 "waiting for backup to end" 的提示。补救办法是找到备份会话直接终止,或者在极端情况下改用immediate。

第三种是复制槽阻塞。逻辑复制槽如果对应的订阅端已经下线,WAL 会一直堆积不被清理。停止时如果涉及检查点刷新,可能被拖慢。用SELECT * FROM pg_replication_slots WHERE active = false;确认有没有残留的槽,不用的及时pg_drop_replication_slot。

4.3 起不来的四类错误,看日志首行就能分流

启动失败的错误信息其实很规整,按日志里第一条 FATAL 或 PANIC 分四类处理。

日志关键字大概率原因处理方向
syntax error in file配置文件语法错误定位行号,检查引号、分号、参数名
could not create shared memory共享内存或信号量不足调整内核参数,或降低shared_buffers
could not write to file/No space left磁盘满清理 WAL 归档或临时文件
address already in use端口被占ss -lntp查占用进程

配置文件错误最容易被忽略的是postgresql.auto.conf。这个文件由ALTER SYSTEM命令生成,优先级高于postgresql.conf,而且它不会做太多校验。有人用ALTER SYSTEM SET设了一个非法值,之后每次启动都失败,却一直去改主配置文件,怎么改都没用。遇到莫名其妙的启动失败,先看这个文件。

共享内存问题在容器环境里更常见。容器默认的/dev/shm只有 64 MB,而 PostgreSQL 需要按shared_buffers的大小申请共享内存。解决办法是启动容器时加--shm-size=1g之类的参数,或者在容器内调整dynamic_shared_memory_type。

5. 哪些参数改了必须重启,哪些 reload 一下就够

5.1 pg_settings 的 context 字段是最好的说明书

很多人改完参数就重启,其实大部分参数根本不需要。判断依据就在pg_settings视图的context列里,它有五个取值,含义如下:

SELECT name, setting, context FROM pg_settings WHERE name IN ('shared_buffers', 'work_mem', 'log_min_duration_statement');
  • internal:编译期固定,改不了
  • postmaster:必须重启才能生效
  • sighup:发 SIGHUP 信号(即 reload)即可生效
  • superuser:普通超级用户用SET就能改,只影响当前会话
  • user:任何用户都能SET

这个字段比任何文档都靠谱,因为它是当前实例的真实状态。与其背参数表,不如在改之前查一下:

SELECT name, context FROM pg_settings WHERE name = 'shared_buffers';

返回postmaster就老老实实安排重启窗口,返回sighup就直接 reload。

5.2 常见参数的重启需求对照

下面这张表是我在维护窗口里最常翻的,列出的都是改配置时最容易被问到的几个:

参数context说明
shared_bufferspostmaster共享内存大小,进程启动时分配,必须重启
max_connectionspostmaster连接槽位在启动时确定,必须重启
wal_levelpostmaster影响 WAL 格式,必须重启
shared_preload_librariespostmaster预加载库在启动时加载,必须重启
portpostmaster监听端口,必须重启
listen_addressespostmaster监听地址,必须重启
work_memuser会话级,SET即可
maintenance_work_memuser会话级,可动态调整
log_min_duration_statementsuperuserSET或 reload 均可
max_wal_sizesighupreload 生效
checkpoint_timeoutsighupreload 生效

有个容易搞混的地方:archive_command的 context 是sighup,但archive_mode是postmaster。也就是说,修改归档命令不用重启,但打开或关闭归档功能必须重启。这个差别在配置归档时经常被忽略。

5.3 reload 的正确姿势与验证方法

reload 有三条路径,效果完全一样,按环境选一条:

# 路径一:pg_ctl pg_ctl reload -D /var/lib/pgsql/15/data # 路径二:systemd systemctl reload postgresql-15 # 路径三:SQL psql -c "SELECT pg_reload_conf();"

reload 的本质是给postmaster发 SIGHUP 信号,让它重新读配置文件。它不会中断现有连接,也不会回滚事务,所以可以在业务高峰期执行。

但 reload 有两个重要的局限。第一,pg_hba.conf的修改需要 reload 才能生效,而postgresql.conf中标记为postmaster的参数即使 reload 了也不会变,只是不会报错,容易让人误以为生效了。第二,reload 失败不会让实例崩溃,错误只会写进日志,所以必须验证。

验证方式是查pg_settings的pending_restart列:

SELECT name, setting, pending_restart FROM pg_settings WHERE pending_restart = true;

如果这个查询返回了行,说明有参数改了但需要重启才能生效。这是我每次改完配置必做的一步,比翻日志可靠得多。

6. 容器、主备与维护窗口里的启停变数

6.1 容器里的 PID 1 信号陷阱

在容器里跑 PostgreSQL,docker stop的行为和裸机上完全不同。Docker 默认会给容器内的 PID 1 进程发送 SIGTERM,然后等 10 秒,如果还没退出就发 SIGKILL。

问题在于,SIGTERM 对 PostgreSQL 来说对应的是smart模式——也就是说,容器引擎发的是一个"温柔等待"的信号。如果此时有连接池保持着连接,postmaster会一直等,10 秒后容器引擎失去耐心,直接 SIGKILL。而 SIGKILL 的效果等于immediate,下次启动就要走崩溃恢复。表现出来就是"每次重启容器都要多等十几秒才能连上"。

这个问题的解法有两个。一是调整容器的停止宽限期:

docker stop --time=120 <容器名>

二是如果需要更可控,可以在容器里用一个轻量的 init 进程(比如 tini)作为 PID 1,它会把 SIGTERM 正确转发给postgres,并且按fast模式处理。或者直接在 entrypoint 脚本里加信号转发逻辑。

还有一个更彻底的方案:让postgres直接作为 PID 1 运行,并且在docker stop时通过STOPSIGNAL SIGINT指定发送 SIGINT,这样对应的就是fast模式,语义正确。这个细节在自建镜像时值得写进 Dockerfile。

6.2 主备环境下重启主库会连带什么

在有流复制备库的环境里,重启主库的影响远不止"主库短暂不可用"这么简单。首先要清楚,主库关闭期间,备库会持续尝试重连,日志里会刷出大量 "could not receive data from WAL stream" 的记录,这是正常的,不需要处理。

真正需要注意的是重启时长和wal_keep_size的关系。主库关闭时,备库无法接收新 WAL。如果关闭时间过长,而主库在关闭前刚做过大量的写入,WAL 文件可能被回收,导致备库落后太多接不上,需要重建。判断依据是备库的pg_stat_wal_receiver状态,或者在主库上看pg_stat_replication的replay_lag。

如果重启窗口很长,建议在关闭前先确认备库的接收位置:

SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes FROM pg_stat_replication;

lag_bytes就是重启期间可能丢失的追赶量。如果这个值很大,又叠加了长时间停机,备库可能需要重新做基础备份,那工程量就完全不一样了。

另外,如果有同步复制配置(synchronous_standby_names),主库关闭时同步备库也会跟着进入不可用状态。如果此时发生主备切换,需要格外小心数据一致性。

7. 把启停写成可复用的脚本与验证清单

7.1 一个带前置检查和超时控制的重启脚本

手工敲命令容易漏步骤,我的做法是把它固化成脚本,每次维护窗口直接跑。核心逻辑是三段:前置检查、执行重启、结果验证。

#!/usr/bin/env bash set -euo pipefail PGDATA="/var/lib/pgsql/15/data" PGUSER="postgres" TIMEOUT=180 # 前置检查:确认当前确实在运行 if ! su - "$PGUSER" -c "pg_ctl status -D $PGDATA" >/dev/null 2>&1; then echo "实例当前未运行,请改用 start 而非 restart" exit 1 fi # 记录重启前的状态,便于事后对比 su - "$PGUSER" -c "psql -Atc \"SELECT pg_postmaster_start_time();\"" \ > /tmp/pg_start_time.before # 执行重启,显式指定模式和超时 su - "$PGUSER" -c \ "pg_ctl restart -D $PGDATA -m fast -w -t $TIMEOUT -l $PGDATA/log/pgstart.log" # 结果验证 if su - "$PGUSER" -c "pg_isready -q"; then echo "重启完成,实例已就绪" else echo "重启后实例未就绪,请检查 $PGDATA/log 下的日志" exit 2 fi

这个脚本里几个点值得说明。set -euo pipefail保证任何一步失败就立刻停下,不会带着错误继续往下跑。pg_ctl status的退出码有明确含义:0 表示运行中,3 表示未运行,4 表示数据目录无效,用这个判断比自己去解析输出可靠。pg_isready是官方提供的一个轻量探测工具,它只检查实例是否能接受连接,不会真正建立会话,适合在脚本里做健康检查。

7.2 重启之后必须做的五项验证

命令返回成功不等于万事大吉。我习惯按下面五项逐一确认,缺一不可。

第一项,启动时间是否真的更新了。查SELECT pg_postmaster_start_time();,如果这个时间还是旧值,说明重启根本没成功,可能是脚本里的pg_ctl restart因为某种原因跳过了。

第二项,参数是否生效。前面提到的pending_restart查询是必做的,返回空结果才说明所有配置都落地了。

第三项,连接是否正常。用pg_isready -h <地址> -p <端口>从应用侧发起探测,而不是只在数据库本机测。这一项能顺带验证监听地址和防火墙配置。

第四项,复制状态。有备库的环境里查pg_stat_replication,确认state是streaming,replay_lag在合理范围内。

第五项,日志有没有异常。看一眼重启后的日志头部,确认没有PANIC,没有反复出现的FATAL重试记录。如果有连接被拒绝的告警,通常是pg_hba.conf没配好或者端口没监听上。

这套清单看起来繁琐,但真正跑起来不到一分钟。相比之下,重启后直接通知业务方"可以了",然后十分钟后收到连接超时告警,代价高得多。我个人在实际操作中的体会是,维护窗口里最省时间的做法恰恰是把验证做在前面,而不是把问题留给监控去发现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询