☰
PostgreSQL启停命令全解析:pg_ctl、systemctl与停机模式避坑指南
2026/11/2 12:44:13 网站建设 项目流程

数据库启停这件事,看起来是整个运维工作里最不需要动脑子的操作,两条命令,最多加个重启,谁会写错?但我这些年见过的生产事故里,因为一次"手滑重启"而导致的故障占比相当高:有人用kill -9收工,结果第二天早高峰数据库在跑崩溃恢复;有人改完shared_buffers直接pg_ctl reload,然后纳闷为什么内存没变化;有人远程 SSH 敲下pg_ctl start就断了连接,进程跟着 shell 一起消失。PostgreSQL 的启动、停止、重启这三组命令,真正的难点从来不是记住语法,而是搞清楚"这条命令背后到底发生了什么"。这篇文章就把 PostgreSQL 启动停止命令以及重启这件事从里到外拆一遍,包括pg_ctl、systemctl、pg_ctlcluster三套入口怎么选,停机模式smart/fast/immediate的信号语义差别,postmaster.pid到底能不能删,以及重启之后怎么验证"真的生效了"。刚接手数据库的新手可以照着抄步骤,有一定经验的同学可以直接看第 4 章的排查路径和第 5 章的形态差异,那里才是真正会踩坑的地方。

1. 动手之前先想清楚:启停到底在操作什么

1.1 同一个 PostgreSQL 为什么会有三套启停入口

刚接触 PostgreSQL 的人很容易被搞晕:教程里写的是pg_ctl start,服务器上运维同事用的是systemctl restart postgresql-16,而另一台机器上又能用pg_ctlcluster 16 main restart。三个命令都能把数据库拉起来,那到底该用哪个?要回答这个问题,得先明白 PostgreSQL 的进程模型——它不是一个单体服务,而是由一个叫postgres(历史上叫postmaster)的主进程,加上一堆后台辅助进程(checkpointer、background writer、walwriter、autovacuum launcher、若干 walsender)和每个连接对应的 backend 进程共同组成的进程树。主进程负责监听端口、接受连接、fork 出 backend,一旦主进程没了,整个实例就没了。所以"启动"的本质是拉起这个主进程,"停止"的本质是让主进程体面地通知所有子进程退出并做收尾。

理解了这一点,三套入口的定位就清楚了。postgres是最原始的二进制,直接执行它就是启动主进程,前台运行,日志打在标准错误上,终端一关进程就收到 SIGHUP 跟着走,只适合调试。pg_ctl是 PostgreSQL 自带的控制工具,它做的事情是:以守护方式启动postgres、重定向日志、写 PID 文件、等待启动完成、把结果反馈给你,同时提供 stop/restart/reload/status 等子命令。而systemctl和pg_ctlcluster都是"上层封装"——systemctl是操作系统级的服务管理器,它按照 unit 文件里写死的ExecStart/ExecStop去调用postgres或pg_ctl,额外提供了开机自启、失败重启、日志归集、依赖顺序管理;pg_ctlcluster则是 Debian/Ubuntu 系postgresql-common包提供的多实例管理脚本,它把配置按版本/集群名分目录组织,底层依然是调pg_ctl。

注意:不管你用哪套入口,最终落到数据目录上的只有一个 postmaster 进程。同一个数据目录被两种方式同时拉起,第二次一定会报lock file "postmaster.pid" already exists,或者更隐蔽地报端口占用。

1.2 三套入口的横向对比与选型判断

选哪套入口,本质上取决于你的环境是谁装的、谁在管。源码编译安装的实例,系统里根本没有对应的 service 文件,只能走pg_ctl;包管理器安装的实例,发行版已经帮你写好了 unit,硬要用pg_ctl反而会和 systemd 的状态产生分歧——systemd 认为服务是 stopped,实际上你手工拉起来了,下次systemctl start就会因为端口占用而失败。这是个非常经典的"状态漂移"问题,我在好几台机器上见过。

入口典型命令依赖适用场景主要风险点
postgres直启postgres -D $PGDATA无本地调试、单次排障、容器内前台运行前台阻塞,终端断开即停
pg_ctlpg_ctl start -D $PGDATA -l log二进制路径源码安装、脚本自动化、精细化控制与 systemd 状态不同步
systemctlsystemctl restart postgresql-16unit 文件包管理器安装、需要开机自启unit 文件写错会导致超时被强杀
pg_ctlclusterpg_ctlcluster 16 main restartpostgresql-commonDebian 系多版本多集群只认/etc/postgresql下的集群

判断方法其实很简单:先执行systemctl status postgresql-*,如果能看到 active 状态,说明这套实例归 systemd 管,后续一律用systemctl;如果提示Unit postgresql-16.service could not be found,再去看pg_lsclusters,有输出就是 Debian 集群模式;两者都没有,那就是源码或自定义安装,老老实实用pg_ctl。最忌讳的是三套混用,出了事连"刚才那次重启是谁干的"都查不清。

1.3 停机模式不是"快慢"之分,是信号语义之别

pg_ctl stop有一个-m参数,可选smart、fast、immediate。很多人把它理解成"温柔地停 / 正常停 / 粗暴地停",这个理解只对了一半,真正决定行为的是它背后发送的 Unix 信号。

smart对应 SIGTERM,主进程收到后立刻停止接受新连接,但会等所有已存在的会话自己断开,然后才做 checkpoint 并退出。问题在于"等所有会话断开"这件事没有上限——只要有一个客户端挂着连接不动(比如某个 idle in transaction 的连接池),这条命令就会一直卡着,直到-t超时报错退出,而服务实际上还在跑。

fast对应 SIGINT,这是绝大多数场景应该用的模式。主进程停止接受新连接,主动断开所有已有连接,未提交的事务回滚,然后执行一次 checkpoint,把脏页刷到磁盘,写一个干净的关闭记录到 WAL,最后退出。因为是干净关闭,下次启动直接可用,不需要恢复。

immediate对应 SIGQUIT,主进程直接让所有子进程退出,不做 checkpoint,不刷脏页,不等任何人。这相当于给数据库拔电源,下次启动时 PostgreSQL 会检测到"上次没有正常关闭",自动进入崩溃恢复流程,从最后一个检查点开始重放 WAL。数据不会丢(WAL 保证了持久性),但如果当时有大量脏页,恢复时间可能很长。

模式信号是否等连接断开是否做 checkpoint下次启动推荐场景
smartSIGTERM是,无限等是干净几乎不用,除非确知没有活跃连接
fastSIGINT否,主动断开是干净默认选择,日常重启、升级、改参数
immediateSIGQUIT否否需要崩溃恢复仅当 fast 停不下来时的最后手段

实际使用中我基本只用fast,immediate只在一种情况下用:主进程卡死、pg_ctl stop -m fast超时无响应、且确认短时间内没有恢复窗口。用完immediate之后,重启的第一件事一定是盯日志,确认database system was not properly shut down; automatic recovery in progress这条出现并且恢复顺利完成,而不是直接看端口通了就放心。

2. pg_ctl 命令拆解:每个参数都在解决一个具体问题

2.1 -D 数据目录:所有操作的锚点

-D是整个pg_ctl命令里最不能出错的参数,它告诉工具"你要操作哪个数据目录"。数据目录里放着PG_VERSION、postgresql.conf、pg_hba.conf、pg_wal/、base/、postmaster.pid这些关键内容,选错了目录,轻则报错,重则把一个完全无关的实例启动起来。

-D的取值来源优先级是:命令行显式指定的-D> 环境变量PGDATA> 编译时写死的默认路径。这个优先级顺序很重要,因为线上经常出现"配置文件里写的是 A,环境变量是 B"的情况。比如某个运维脚本在/etc/profile里导出了PGDATA=/var/lib/pgsql/data,而实际数据已经迁到了/data/pgdata,这时候执行pg_ctl start不带-D,就会把老目录里的实例拉起来,然后一堆人对着"数据怎么是旧的"发懵。

我的建议是:任何时候都在命令里显式写-D,不要依赖环境变量。同时把数据目录路径固化成一个脚本变量,避免每条命令手敲。另外有个细节值得提醒,-D指定的路径必须是绝对路径或者相对于当前工作目录的有效路径,而且路径末尾不要加斜杠,/data/pgdata/虽然多数情况能识别,但在某些版本的pg_ctl里会导致 PID 文件路径拼接出双斜杠,虽然不影响功能,但排查时会让人多花几分钟确认。

确认数据目录是否正确,最快的办法是看两个文件:PG_VERSION(里面是一个大版本号,比如16)和postgresql.conf。如果PG_VERSION里的数字和你要启动的二进制版本对不上,PostgreSQL 会直接拒绝启动并提示database files are incompatible with server。这种情况下不要试图"强行启动",跨大版本必须走pg_upgrade或逻辑导出导入。

2.2 -l 与 -w:把"启动成功"变成可判断的事实

-l用于把服务端的 stdout/stderr 重定向到一个日志文件。不带-l启动,所有输出会打在pg_ctl的标准输出上,你看到的就是一堆形如2024-05-11 10:20:33.123 CST [1234] LOG: starting PostgreSQL 16.2的行。这在交互终端里没什么问题,但一旦写进 crontab 或者远程 SSH 执行,输出丢失就麻烦了——启动失败了,你连报错长什么样都看不到。

-l指向的文件路径必须所在目录可写,否则pg_ctl会先报could not open log file。这里有个常见误区:很多人把日志路径写成/data/pgdata/log/pg.log,但log/目录本身不存在,于是启动直接失败,而此时pg_ctl的报错信息又因为日志重定向已经生效而被吞掉了,看起来就是"启动没反应"。稳妥的做法是在执行前先mkdir -p并把属主改成数据库用户。

提示:-l指定的日志文件不会自动切割,跑上几个月就是几个 GB。要么配logrotate并在切割后发 SIGHUP,要么干脆用 PostgreSQL 内置的logging_collector = on,让它按天/按大小自己分文件。

-w是等待启动完成。这个参数在 PostgreSQL 10 之前默认是关闭的,10 之后改成了默认开启,但老脚本里仍然能看到显式写-w的。它的作用是:pg_ctl start不立即返回,而是轮询pg_isready或读取 postmaster 状态,直到数据库真正接受连接才返回 0;如果超时(由-t控制,默认 60 秒)还没起来,就返回非 0 并附带错误信息。

这个差别在实际使用中非常关键。想象一个升级脚本:pg_ctl start之后立刻执行下一行psql -c "ALTER SYSTEM ..."。如果没开-w,start 命令几乎瞬间返回,而数据库还在做崩溃恢复,下一行的 psql 直接连不上,脚本失败。开了-w就会老老实实等到可以连上为止。所以哪怕新版默认开启,我也建议在自动化脚本里显式写出来,让人一眼看出这里需要等待。

2.3 -m 与 -t:停机动作的两道保险

-m已经在 1.3 讲过语义,这里补充它的默认值——pg_ctl stop的默认模式是fast,pg_ctl restart同样。这个默认值选得很合理,但不少人误以为是smart,于是在需要主动断开连接的场景下(比如要立刻释放连接数做维护),发现命令"卡住了",其实是模式判断反了。

-t控制等待超时秒数,默认 60。它同时作用于 stop 和 start 两个阶段。这个值需不需要调?取决于你的实例规模。一个共享缓冲区配到几百 GB、脏页很多的大实例,fast停机时的 checkpoint 可能要几分钟才能刷完,60 秒根本不够,pg_ctl会报server does not shut down然后退出,但服务端其实还在继续刷盘。这时候如果排错的人以为"停不下来",接着补一个-m immediate,那就等于白等前面那几分钟,还额外引入了一次崩溃恢复。

我的经验值是:停机超时按shared_buffers大小粗估,每 10 GB 共享缓冲留 30 秒,再叠加 60 秒余量;启动超时按数据量估,普通实例 120 秒足够,开了崩溃恢复或者有大量 WAL 要重放的要给到 300 秒甚至更多。这个估算不精确,但比默认值靠谱得多。

# 一个相对稳妥的大实例重启写法 /usr/local/pgsql/bin/pg_ctl restart \ -D /data/pgdata \ -l /data/pgdata/log/pgctl.log \ -m fast \ -t 300 \ -w

2.4 -o 与 -c:临时参数的正确传法

-o用于把额外的参数透传给底层的postgres进程,-c用于传递配置项,两者经常一起出现。典型用法是在不改配置文件的前提下临时改端口启动一个影子实例,用来做数据校验或者只读查询。

pg_ctl start -D /data/pgdata -o "-p 5544 -c listen_addresses='127.0.0.1'" -l /tmp/shadow.log

这里有两个坑要提醒。第一,-o后面的内容会被当作一个整体字符串拆分成多个参数,所以里面的引号要写对,-o "-p 5544 -c shared_buffers=1GB"是合法的,-o "-c search_path='a b'"这种带空格的配置值就必须用嵌套引号,很容易出错。第二,-o传入的参数优先级高于postgresql.conf,但低于ALTER SYSTEM写进postgresql.auto.conf的值——因为 auto.conf 是最后加载的。所以你在pg_ctl start -o "-c max_connections=500"之后,SHOW max_connections可能还是旧的,因为 auto.conf 里有个更高的值覆盖了它。

真正需要长期生效的参数,正确做法是改postgresql.conf或者用ALTER SYSTEM SET写进 auto.conf,然后重启;-o只适合临时验证。如果只是想验证某个参数改完的效果再决定要不要持久化,用-o起一个临时实例反而是最安全的方式,验证完直接停掉,主实例完全不受影响。

3. 一次干净的完整重启:从检查到验证

3.1 重启前的五项前置检查

重启前的检查清单,是我认为比重启命令本身更有价值的部分。因为绝大多数"重启事故"不是重启动作做错了,而是重启前压根没确认状态。

第一项,确认当前实例真的在运行,以及跑了多久。pg_ctl status -D $PGDATA会输出pg_ctl: server is running (PID: 12345),或者no server running。它的退出码很有用:0 表示运行中,3 表示没运行,4 表示数据目录不可访问。脚本里判断状态就应该用退出码而不是解析文本。同时用SELECT pg_postmaster_start_time();看一眼启动时间,这个时间在重启前后必须变化,否则说明你的重启命令根本没打到这个实例上。

第二项,确认没有长事务和长查询在跑。SELECT pid, now() - xact_start AS duration, query FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY duration DESC LIMIT 10;一个跑了两小时的CREATE INDEX或者一个idle in transaction的会话,都会让fast停机的价值打折——虽然fast会主动断开它们,但大事务回滚本身要花时间,而且业务侧会看到一次"连接被重置"。理想做法是提前通知、主动取消,而不是等停机时被动断开。

第三项,确认复制状态。如果这个实例是主库,重启期间所有备库都会断连,重启后需要重新建立复制。SELECT application_name, state, sync_state, replay_lag FROM pg_stat_replication;看清楚有没有同步备库——如果有同步备库且synchronous_commit = on,主库重启会导致写入短暂不可用,因为同步提交必须等到备库确认。这个影响必须提前告知业务方。

第四项,确认备份在位。重启本身不会丢数据,但重启过程中如果发现需要立刻回滚、或者启动失败需要重建,没有一份可用的基础备份就会非常被动。至少要确认最近一次全备的时间和 WAL 归档是否正常。

第五项,确认变更窗口和回滚方案。如果是配合参数修改的重启,把旧值记下来;如果是版本升级,确认二进制路径切换方式和降级路径。这一条看起来是流程废话,但我确实见过改完max_connections重启失败、想改回去却忘了原值是多少的情况。

3.2 单机 pg_ctl 方式逐步实操

以源码安装、数据目录/data/pgdata、数据库用户postgres、端口 5432 为例,走一遍完整流程。

第一步,切换到数据库用户。PostgreSQL 拒绝以 root 身份运行 postmaster,这不是"建议"而是硬性检查,用 root 直接执行会报cannot be run as root。切换方式推荐su - postgres或者runuser -u postgres --,后者更适合非交互脚本。注意su不带短横线时不会加载目标用户的环境变量,PATH里可能找不到pg_ctl,这一点在写脚本时特别容易翻车。

runuser -u postgres -- /usr/local/pgsql/bin/pg_ctl status -D /data/pgdata echo "exit code: $?"

第二步,执行重启。这里有个细节值得展开:PostgreSQL没有原生的restart操作,pg_ctl restart本质上就是先stop再start的组合动作。这意味着如果 stop 阶段失败(比如超时),start 阶段根本不会执行,实例会停留在"已停止"状态。所以不要以为写了 restart 就一定有服务,必须检查返回码。

runuser -u postgres -- /usr/local/pgsql/bin/pg_ctl restart \ -D /data/pgdata \ -l /data/pgdata/log/pgctl.log \ -m fast -t 300 -w

第三步,判断结果。pg_ctl的退出码 0 表示成功,非 0 表示失败。失败时不要急着再敲一遍,先看日志。日志位置有三个可能:-l指定的文件、logging_collector = on时log_directory下的文件、以及 systemd 托管下的journalctl。先看-l指定的那个,因为最靠近失败点。

第四步,验证。这一步在 3.4 展开,先说结论:端口通了不等于启动成功,pg_ctl返回 0 也不完全等于服务可用,一定要跑一条真实查询。

3.3 systemd 托管方式的实操与自建 unit

包管理器安装的实例,systemctl已经把细节封装好了:

systemctl status postgresql-16 systemctl restart postgresql-16 systemctl reload postgresql-16 # 只发 SIGHUP,不重启 systemctl is-active postgresql-16 # 脚本里判断状态更方便

systemctl restart的执行逻辑完全由 unit 文件决定。想知道它到底做了什么,直接systemctl cat postgresql-16看ExecStart和ExecStop。多数发行版的写法是ExecStart=.../postgres -D ${PGDATA}配合Type=notify,ExecStop走pg_ctl stop -m fast。Type=notify意味着 PostgreSQL 启动了 sd_notify 支持,systemd 会等到数据库真正就绪才认为服务启动完成,这比Type=forking靠 PID 文件猜测要准确得多。

源码安装的实例建议自己写一个 unit,收益很直接:开机自启、崩溃自动拉起、日志统一进 journald、systemctl status能看到最近几次退出状态。下面这份是我在用的模板,改一改路径就能用:

[Unit] Description=PostgreSQL 16 database server Documentation=man:postgres(1) After=network.target [Service] Type=notify User=postgres Group=postgres Environment=PGDATA=/data/pgdata Environment=PGPORT=5432 ExecStart=/usr/local/pgsql/bin/postgres -D ${PGDATA} ExecReload=/bin/kill -HUP $MAINPID ExecStop=/usr/local/pgsql/bin/pg_ctl stop -D ${PGDATA} -m fast -w -t 300 TimeoutStartSec=300 TimeoutStopSec=300 KillMode=mixed KillSignal=SIGINT Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target

几个参数值得单独说明。TimeoutStopSec=300是为了给大实例的 checkpoint 留时间,默认 90 秒在很多实例上不够,超时后 systemd 会升级为 SIGKILL,等于人为制造一次崩溃恢复,非常得不偿失。KillMode=mixed让停止信号只发给主进程,由主进程自己通知子进程退出,而不是 systemd 把整个进程组一起砍掉,后者会打乱 PostgreSQL 的关闭顺序。KillSignal=SIGINT对应 fast shutdown 语义。Restart=on-failure配合RestartSec=5用于偶发崩溃后的自动拉起,但要注意如果是因为配置错误导致的启动失败,它会一直循环重启,日志会被刷爆,所以观察期最好把Restart先设成no。

改完 unit 文件必须systemctl daemon-reload,否则新配置不生效——这是新手最常漏的一步。

3.4 重启之后必须做的三项验证

第一项,确认启动时间变了。SELECT pg_postmaster_start_time();这个值如果不早于你执行重启的时间,说明操作根本没生效,可能打到了另一个实例,也可能只是执行了一次 reload。

第二项,确认配置真正加载。SELECT pg_conf_load_time();给出配置文件最后一次被读取的时间。如果改的是需要重启的参数,这个时间应该和启动时间接近;如果改的是 reload 就能生效的参数,它应该等于你执行 reload 的时间。更进一步,直接SHOW出你改的那个参数确认值:

SELECT name, setting, unit, source, pending_restart FROM pg_settings WHERE name IN ('shared_buffers','max_connections','work_mem','wal_level');

pending_restart这一列特别有用,它就是 PostgreSQL 自己告诉你"这个参数改了但还没重启"的官方答案。改完配置先查这个,比翻文档确认哪个参数需要重启靠谱得多。

第三项,跑一条真实业务查询。SELECT 1只能证明连接建立成功,证明不了 buffer、索引、扩展都正常。我的习惯是跑一条带索引扫描的查询,比如SELECT count(*) FROM <某张有索引的表> WHERE <有索引的字段> = <某个值>;,再看一眼pg_stat_database里的blks_hit有没有增长。如果是升级重启,还要额外验证扩展(\dx)和自定义函数是否存在。

3.5 哪些参数改了必须重启,哪些 reload 就够

这是"重启"话题里最高频的疑问,也是误操作最集中的地方。PostgreSQL 的参数按生效时机分三类:postmaster 级(必须重启)、sighup 级(reload 即可)、user/session 级(改会话内生效)。

参数生效方式说明
shared_buffers必须重启共享内存段在启动时分配,改大小只能重启
max_connections必须重启决定后台进程槽位与共享内存布局
wal_level必须重启影响 WAL 记录格式
max_worker_processes必须重启后台工作进程池大小
listen_addresses/port必须重启socket 监听在启动时绑定
shared_preload_libraries必须重启库在启动阶段加载
work_memreload 即可每个排序/哈希操作独立分配
maintenance_work_memreload 即可VACUUM、建索引使用
effective_cache_sizereload 即可优化器估算用,不占真实内存
checkpoint_timeout/max_wal_sizereload 即可影响检查点频率
log_min_duration_statementreload 即可慢日志阈值
random_page_costreload 即可优化器成本估算

reload 有三种触发方式:pg_ctl reload -D $PGDATA、systemctl reload postgresql-16、SQL 层的SELECT pg_reload_conf();。三者发出去的都是 SIGHUP,效果一致。pg_reload_conf()需要超级用户权限,远程操作时最方便,但要注意它会返回t表示信号已发送,不代表配置解析成功——如果postgresql.conf有语法错误,PostgreSQL 会在日志里记一条configuration file ... contains errors,然后继续用旧配置运行。所以 reload 之后一定要看日志,或者用SHOW确认值真的变了。

顺带说一个我自己踩过的坑:有人把ALTER SYSTEM SET shared_buffers = '4GB'执行完,看到pg_settings.pending_restart = t,以为重启后就会生效,结果重启后发现值还是 2GB。原因是启动脚本里用-o "-c shared_buffers=2GB"传了参数,而命令行参数会覆盖 auto.conf。排查这类"配置不生效"的问题,第一步永远是看pg_settings.source和sourcefile,它会明确告诉你这个值来自哪里。

4. 故障现场:启动停止异常的排查路径

4.1 启动失败的高频报错与处理

报错关键字常见原因处理办法
lock file "postmaster.pid" already exists已有实例占用同一数据目录pg_ctl status确认;进程存在就别删文件
could not bind ... Address already in use端口被占用ss -lntp | grep 5432找占用进程
could not create any TCP/IP socketslisten_addresses配了本机不存在的地址改成localhost或*后重启
data directory has invalid permissions目录权限过宽或属主错误chown -R postgres:postgres,权限 700 或 750
database files are incompatible with server二进制版本与数据目录不匹配确认PG_VERSION,走pg_upgrade
could not open file "pg_wal/0000..."WAL 缺失或被误删从归档恢复;不要试图伪造 WAL
the database system is starting up还在崩溃恢复中等它跑完,看日志进度
sorry, too many clients already连接数打满不是启动失败,但会让"起来了"的错觉变成业务不可用
configuration file ... contains errors配置文件语法错误按日志行号逐条改,改完先reload验证

这张表我建议贴在运维手册第一页,因为 80% 的启动失败都落在里面。重点说两个容易判断错的。

一个是postmaster.pid已存在。这个文件里其实存了七行信息:PID、数据目录、启动时间、端口、socket 目录、监听地址、共享内存键。拿到里面的 PID 之后,用ps -p <PID> -o pid,cmd看进程是否还存在。进程存在,说明真的有一个实例在跑,要么是别人先启动了,要么是你的-D指错了目录。进程不存在,说明是上次异常退出留下的残骸,这时候删掉 pid 文件是安全的。问题在于"进程存在但已经僵死"的情况——进程还在,但完全不响应,这时删文件会导致两个 postmaster 抢同一个目录,后果比不删严重得多。

另一个是 WAL 缺失导致启动失败。这种故障几乎都是人为的:有人清理磁盘时删了pg_wal下的旧文件,有人用pg_resetwal想"修一下"。正确做法是从基础备份加归档 WAL 做时间点恢复,或者用pg_resetwal但必须清楚这意味着数据可能不一致、后续需要重建。这不是"命令用法"能解决的问题,得走恢复流程。

4.2 命令卡住不返回的三种情形

pg_ctl stop敲下去之后光标一直闪,什么也不输出,这是很多人第一次遇到会慌的场景。实际上有三种完全不同的原因,处理方式也不同。

第一种,-m smart在等连接退出。前面说过,只要有一个连接挂着,smart 就会一直等。判断方法是另开一个窗口SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';,如果数字不为零且不下降,就是它。处理办法只有两个:等,或者换-m fast(需要另起一个pg_ctl进程,因为原来的还在阻塞)。

第二种,-m fast在做 checkpoint。大实例刷脏页可能要几分钟,这段时间主进程还在,日志里能看到waiting for checkpointer to finish。这是正常的,不要打断。判断方式看日志或ps里的活跃进程状态,checkpointer 在跑 IO 就说明它在干活。这时候如果误判成卡死、补一个kill -9,下次启动就要做崩溃恢复,恢复时间往往比等 checkpoint 更长。

第三种,-t超时后pg_ctl自己退出了,但服务端还在停。这时候pg_ctl会打印server does not shut down。注意,超时退出不等于服务还在正常运行,它可能正在关闭过程中。所以超时之后第一件事不是发新命令,而是pg_ctl status确认状态,再看日志。

提示:远程 SSH 执行pg_ctl stop有个隐蔽问题——如果 SSH 会话意外断开,pg_ctl收到 SIGHUP 退出,但它的子进程可能被连带影响。用nohup或者systemd-run --scope包一层会更稳,或者干脆用 systemctl 代替。

4.3 停不下来时的处理顺序

真的遇到了停不下来的情况,处理顺序必须是递进的,不能跳步。

第一层,pg_ctl stop -m fast -t 600,把超时放大到足够长。这一步能解决 90% 的问题,因为绝大多数"停不下来"只是 checkpoint 慢。

第二层,看日志确认卡在哪。如果日志显示在等某个进程退出,ps -ef | grep postgres看进程树,找出是哪个子进程不响应。如果是一个 backend 卡在 IO 上(比如某个查询在读一个已经挂掉的 NFS 挂载点),那可能要处理底层存储问题。

第三层,对主进程发 SIGINT(等同 fast 指令),看它是否有反应。这一步和第一层本质相同,只是绕过了pg_ctl。

第四层,才是-m immediate。执行前一定要在群里通报,因为它意味着下次启动要走崩溃恢复,恢复时长取决于 WAL 量和检查点间隔。

第五层,kill -9主进程。这是最后手段,和 immediate 的区别是它连"让子进程有序退出"都跳过了,会留下一堆孤儿进程,可能需要手工清理。清理完之后启动,一定会走崩溃恢复,且恢复可能因为共享内存没释放而失败,必要时需要清理/dev/shm下的残留段(前提是确认没有其他实例在使用)。

4.4 postmaster.pid 残留到底能不能删

这个问题我被问过很多次,答案是可以删,但删之前必须完成三个确认。

确认一,pid 文件里的 PID 对应的进程不存在。cat postmaster.pid拿第一行,ps -p <PID>看结果。注意进程可能因为长时间运行被系统复用 PID,所以只看 PID 不够,还要看进程名是不是postgres。

确认二,端口没有被占用。ss -lntp | grep 5432,如果有输出,说明有进程在监听,那不管 pid 文件怎么说,都不能删。

确认三,共享内存段没有残留。ipcs -m看一下,如果有属主是 postgres 的大段共享内存,说明可能有进程还在(或者上次异常退出没清)。现代 PostgreSQL 一般能自己处理这个,但极端情况下残留会导致新实例分配共享内存失败。

三个确认都过了,rm postmaster.pid是安全的。删完立刻启动,看日志里有没有performing crash recovery——如果有,说明上次确实不是正常关闭,恢复是预期行为。

反过来,还有一个陷阱:数据目录的属主和权限。postmaster.pid是 postmaster 创建的,属主一定是运行数据库的用户。如果你用 root 删了文件,然后用 root 启动(会被拒绝),接着切到 postgres 用户启动,发现could not write to lock file,那是因为目录权限被 root 操作影响了。稳妥做法是全程用同一个用户操作,删文件也用runuser -u postgres -- rm。

5. 不同运行形态下的启停差异

5.1 容器里为什么不能用 systemctl

容器里跑 PostgreSQL,最常见的错误是照着宿主机教程执行systemctl restart postgresql,然后得到System has not been booted with systemd as init system。容器里没有 init 系统,这个命令根本不存在。

容器场景下的启停由容器运行时接管。以官方镜像为例,entrypoint 脚本最后会exec到postgres进程,让它成为 PID 1。所以docker stop发出的 SIGTERM 直接打到 postmaster 上,而 SIGTERM 对应的是 smart shutdown——它会等待所有连接断开。Docker 默认只等 10 秒,超时就发 SIGKILL,结果就是一次强制崩溃。

官方镜像对此做了处理,Dockerfile 里设置了STOPSIGNAL SIGINT,让停止信号变成 fast shutdown 语义。如果你用的是自己打的镜像,记得把这个补上。另外把停止超时显式调大也有必要:

docker stop -t 120 pg-prod

-t 120表示给容器 120 秒优雅退出时间。这个值和前面pg_ctl的-t是同一个道理:给 checkpoint 留够时间。

容器里另一个坑是数据目录的持久化。容器重启(docker restart)本身不会丢数据,但如果数据目录挂的是匿名卷,容器被删除重建后就找不回来了。用命名卷或者宿主机目录挂载是必须的。还有docker restart到底做了什么?它是 stop + start,和pg_ctl restart同构,如果 stop 阶段超时被 SIGKILL,start 阶段就要做崩溃恢复。

5.2 主备环境重启的顺序与风险

单机重启无非是短暂不可用,主备环境要考虑的东西多一层:重启顺序会影响业务读写切换的时机。

基本原则是先重启备库,再重启主库。备库重启期间,主库仍然可以读写,只是复制中断;等备库起来、pg_stat_replication里state = streaming恢复,再动主库。如果反过来先重启主库,会让所有备库同时断连,而且主库重启期间业务完全不可写。

主库重启时有两个风险点要盯。第一是同步复制。如果配了synchronous_standby_names且synchronous_commit = on,主库重启后备库还没重连上,主库的写入会一直等待,业务侧表现为"写入卡住"。这时候要么临时把synchronous_commit调成local(reload 生效,不用重启),要么重启前先摘掉同步配置。第二是复制槽。如果备库重启时间较长,主库上对应的复制槽会积压 WAL,pg_replication_slots里的pg_wal_status和积压大小要盯着,磁盘写满就是大事故。

还有一个容易被忽略的点:pg_ctl promote和重启完全是两回事。promote是把备库提升为主库,退出恢复模式,此后再也回不到备库身份(除非重建)。这个词和restart长得不像,但在脚本里写错位置的案例我见过——有人想"重启一下备库",结果写成了pg_ctl promote,一台备库直接变成第二个主库,双主写入,数据分叉。

5.3 写进自动化脚本时的几个硬约束

真正把启停写成脚本,会遇到一堆交互式终端里碰不到的问题。

第一个约束是set -e。脚本开头写了set -e,然后pg_ctl status返回 3(未运行),整个脚本直接退出。这是pg_ctl status退出码设计带来的副作用,处理办法是显式捕获:

if runuser -u postgres -- "$PG_BIN/pg_ctl" status -D "$PGDATA" >/dev/null 2>&1; then echo "already running" exit 0 fi

第二个约束是用户切换。su - postgres -c "..."会加载完整登录环境,比较慢但变量齐全;runuser -u postgres -- cmd不加载 profile,速度快但不带环境变量,PATH里找不到pg_ctl就会被报command not found。我的习惯是脚本里用绝对路径,这样两种方式都能跑。

第三个约束是判断"真的起来了"。只判断pg_ctl退出码不够,更可靠的是pg_isready:

"$PG_BIN/pg_isready" -h /var/run/postgresql -p 5432 -d postgres -t 5

它的退出码含义:0 表示接受连接,1 表示拒绝连接(服务在跑但没准备好,比如还在恢复),2 表示无响应(服务没跑)。这个分级比pg_ctl status更细,适合做健康检查。注意-h参数,如果配了unix_socket_directories,走 socket 比走 TCP 更贴近真实连接路径。

第四个约束是日志。脚本执行完没人看输出,所以一定要把关键动作、返回码、时间戳写进日志文件,出问题时才有据可查。我一般会记录三行:重启前状态、命令返回码、重启后的pg_postmaster_start_time。第三行是灵魂,没有它就无法证明重启真的发生了。

第五个约束是并发保护。两个自动化任务同时执行重启,一个在 stop 阶段,一个在 start 阶段,交错起来能搞出花。加一个基于文件锁或者flock的互斥是必要的:

exec 9>/var/lock/pg_restart.lock flock -n 9 || { echo "another restart in progress"; exit 1; }

这套东西铺下来,脚本会比手动操作复杂不少,但线上环境就是这样,能自动化的部分必须自动化,剩下的才靠人守着。

我个人在这些年的操作里体会最深的一点是:PostgreSQL 的启停命令本身没有难度,难的是每次执行前对状态的确认和执行后对结果的验证。同样一条pg_ctl restart,在确认了复制状态、长事务、备份、超时之后执行,和在什么都不知道的情况下执行,风险差着一个数量级。我现在已经养成习惯,不管多急,重启前一定跑一遍pg_isready、看一眼pg_stat_activity、确认一下备份时间;重启后一定查一次pg_postmaster_start_time和pending_restart。这三步加起来不到一分钟,但它帮我在好几次变更窗口里提前发现了"参数根本没加载"和"重启打错了实例"这类问题,避免了对着监控面板猜半天的局面。

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

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

立即咨询