日志轮转与清理
日志一旦开始产生,就必然面对两个问题:单个文件不能无限变大,磁盘不能被历史日志塞满。前者靠「轮转」解决——把日志按时间或大小切分成一个个文件;后者靠「保留策略」解决——按规则删除或压缩过期文件。PostgreSQL 内置了轮转能力(log_rotation_age / log_rotation_size),而清理需要自己动手(脚本、cron 或外部 logrotate 工具)。
轮转配置的核心是一门「文件名与参数配合」的艺术:log_filename 里的时间格式化符决定新文件叫什么,log_truncate_on_rotation 决定同名文件是追加还是覆盖,pg_rotate_logfile() 与 pg_ctl logrotate 提供手动触发手段。参数都很简单,组合错了却会出现「日志全写进一个文件」「旧文件被悄悄覆盖」「半夜磁盘打满」等经典事故。本页把这些组合关系一次讲透。
基础概念
先分清两类轮转方案:
| 方案 | 触发者 | 优点 | 缺点 |
|---|---|---|---|
| 内置轮转 | 收集进程按 log_rotation_age / log_rotation_size 主动切换 | 零外部依赖、参数少 | 清理仍需自己做 |
| 外部 logrotate | 系统 logrotate 守护进程按计划切割、压缩、删除 | 生态成熟、自带压缩与保留策略 | 需要让 PostgreSQL 释放旧文件句柄 |
两种方案二选一即可,混用(内置轮转 + logrotate 又切割一遍)会造成文件名混乱与句柄问题。选择建议:已有统一 logrotate 体系的团队用外部方案,其余场景用内置轮转 + 自写清理脚本。
轮转发生的时机
- 按大小:当前文件超过
log_rotation_size(默认 10MB)后,写入下一条消息时切换新文件。 - 按时间:距离上次文件打开超过
log_rotation_age(默认 24 小时)后,写入下一条消息时切换新文件。时间轮转是惰性检查的——没有消息就没有轮转,长期空闲的实例不会凭空产生空文件。 - 手动:执行
SELECT pg_rotate_logfile();或pg_ctl logrotate,收集进程立即打开新文件。
「切换新文件」的实际含义由 log_filename 决定:新名字与旧名字不同 → 落成新文件;新名字与旧名字相同 → 继续追加原文件(或按 log_truncate_on_rotation 覆盖)。
语法与用法
三个内置参数 + 一个文件名模板:
# 单文件最长存活时间,单位分钟;0 表示不按时间轮转(默认 1440,即 24h)
log_rotation_age = 1440
# 单文件最大尺寸,单位 kB;0 表示不按大小轮转(默认 10240,即 10MB)
log_rotation_size = 10240
# 时间轮转打开同名文件时是否截断(默认 off,追加)
log_truncate_on_rotation = off
# 文件名模板(strftime 格式化符)
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'-- 在线查看
SHOW log_rotation_age;
SHOW log_rotation_size;
SHOW log_truncate_on_rotation;常用文件名模板与轮转参数的搭配:
| log_filename | 搭配的轮转参数 | 效果 |
|---|---|---|
postgresql-%Y-%m-%d_%H%M%S.log | 默认参数 | 每次轮转(含重启)都产生唯一新文件 |
postgresql-%Y-%m-%d.log | log_rotation_age = 1440 | 每天一个文件 |
postgresql-%Y-%m-%d_%H.log | log_rotation_age = 60 | 每小时一个文件 |
postgresql-%a.log | log_rotation_age = 1440 + log_truncate_on_rotation = on | 按星期几循环,七天覆盖一轮 |
详细说明
log_rotation_age 与 log_rotation_size 的语义细节
- 两个参数是「或」的关系:任一条件满足,下一条消息写入时就轮转。
log_rotation_age的单位是分钟,写1d、1h这类带单位字符串也可以(配置系统会换算)。设 0 禁用时间轮转。log_rotation_size的单位是 kB,默认 10240(10MB)。设 0 禁用大小轮转。- 大小轮转只看「当前文件是否超过阈值」,与内容无关;一条超长消息可能把文件推过阈值,轮转发生在下一条消息。
- 时间轮转的计时起点是当前文件打开的时刻,不是整点。想要「整点切割」的直觉要靠文件名配合(如每小时文件名
_HH变化)来实现。 - 轮转是收集进程的轻量操作(关旧文件、开新文件),不会阻塞会话;但轮转瞬间旧文件停止写入、新文件开始接收,采集端要按「文件变化」而非「单文件尾随」设计。
log_truncate_on_rotation 的精确语义
这是最容易误解的参数,规则原文可以概括为三条:
- 只在「时间轮转」打开同名文件时截断。大小轮转、手动
pg_rotate_logfile()、进程重启打开文件时都不截断——这些场景一律追加。 - 默认
off:同名文件追加写入,历史内容保留。 - 典型用途是「循环覆盖」型命名:
# 星期一覆盖上周一的文件,天然保留七天
log_filename = 'postgresql-%a.log'
log_rotation_age = 1440
log_truncate_on_rotation = on覆盖即丢失
log_truncate_on_rotation = on + 无时间变化的文件名 = 历史日志被周期性清空。合规或审计场景(如连接审计)严禁使用循环覆盖命名,必须用带完整日期的文件名 + 外部归档。
log_filename 格式化与命名策略
log_filename 支持 strftime 风格的格式化符,常用的有 %Y(年)、%m(月)、%d(日)、%H(时)、%M(分)、%S(秒)、%a(星期缩写)。命名策略的三条经验:
- 文件名里的时间精度 ≥ 轮转周期。 每小时轮转但文件名只到「天」,则同一天的文件一直追加——这不是错误,但要清楚这是「单文件按天追加」而不是「每小时一个文件」。
- 按天命名是最常用的生产基线。 文件数量适中、清理规则简单(
-mtime即可)、与外部归档系统天然对齐。 - 高流量实例用「天 + 小时」两级命名。 单天日志动辄几个 GB 时,按小时切分方便压缩与搬运,也避免单个文件过大导致采集端处理困难。
# 基线:按天
log_filename = 'postgresql-%Y-%m-%d.log'
log_rotation_age = 1440
log_rotation_size = 0 # 不按大小切,保证一天一文件
# 高流量:按小时
log_filename = 'postgresql-%Y-%m-%d_%H.log'
log_rotation_age = 60
log_rotation_size = 2GB # 兜底:小时粒度也超限时按大小切注意「按大小轮转 + 按天命名」的组合:大小触发的新文件与原文件同名,继续追加——等于大小轮转被命名策略「吸收」了。想让大小轮转真正切出文件,文件名里必须有秒级或毫秒级格式化符。
手动轮转:pg_rotate_logfile() 与 pg_ctl logrotate
-- SQL 方式:让收集进程立即打开新文件
SELECT pg_rotate_logfile(); pg_rotate_logfile
-------------------
t
(1 row)# 命令行方式(PG12+ 提供 logrotate 子命令)
pg_ctl logrotate -D /var/lib/postgresql/18/main
# 返回值示意
pg_ctl: server process signaled (PID 18241)使用场景:
- 归档前切割:把「昨天的日志」完整地封进一个确定的文件,方便校验与上传。
- 低流量实例:时间轮转靠「下一条消息」触发,长期无消息的实例文件迟迟不切换,手动强制即可。
- 外部 logrotate 的配套动作:见下一节。
外部 logrotate 工具方案
logrotate 方案的要点是:切割后必须让 PostgreSQL 释放旧文件的句柄,否则写入继续指向已被改名的旧文件(infd 现象),新文件一直是空的。现代 PostgreSQL 提供 pg_ctl logrotate 专门配合这个动作:
# /etc/logrotate.d/postgresql-log
/var/lib/postgresql/18/main/log/postgresql-*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
dateext
postrotate
/usr/bin/su - postgres -c "/usr/pgsql-18/bin/pg_ctl logrotate -D /var/lib/postgresql/18/main" || true
endscript
}配置逐条解读:
daily+rotate 14:每天切割、保留 14 份,超过自动删除——保留策略由 logrotate 托管。compress+delaycompress:切割后压缩,但最近一份延迟一轮再压(避免句柄尚未释放就压缩)。dateext:切割文件名带日期,避免默认的.1、.2序号名。postrotate里的pg_ctl logrotate:通知收集进程切换到新文件。这是 PG12+ 的标准做法;更老版本只能对收集进程发信号或重启,copytruncate(复制后截断原文件,无需通知)是另一种兜底——但截断瞬间可能丢少量正在写入的日志,有损方案要知情使用。
syslog 通道的轮转
选择 log_destination = 'syslog' 时,PostgreSQL 完全不管理日志文件——写入由 syslog 守护进程完成,轮转也自然由 syslog 生态接管:
- rsyslog/journald 有自己的轮转与转发配置,PostgreSQL 侧无需
log_rotation_*。 - logrotate 同样可以切 syslog 产出的文件,
postrotate里通知的对象是 rsyslog(systemctl reload rsyslog)而不是 PostgreSQL。 - 缺点回顾:syslog 会按行拆分消息(默认 900 字节内拆分)、丢失 CSV/JSON 的结构化列。对需要结构化的场景,坚持用收集进程 + 文件方案。
保留策略与清理脚本
内置轮转只切文件不删文件,清理要自己做。按天命名时一个 find 脚本即可:
#!/usr/bin/env bash
# pg_log_cleanup.sh —— 保留 30 天,删除更早的日志
LOGDIR=${1:-/var/lib/postgresql/18/main/log}
KEEP_DAYS=${2:-30}
find "$LOGDIR" -maxdepth 1 -name 'postgresql-*.log' -mtime +"$KEEP_DAYS" -print -delete# crontab:每天凌晨 2:30 清理
30 2 * * * postgres /usr/local/bin/pg_log_cleanup.sh /var/lib/postgresql/18/main/log 30容量估算公式:日均日志量 × 保留天数 × 安全系数(1.3)= 日志分区容量。日均日志量可以从「按天命名的文件大小」直接读出——这也是按天命名的隐藏好处:容量趋势一目了然。归档类需求(合规留存 180 天)不要靠磁盘硬扛,切割后压缩再转对象存储,热盘只留近期文件。
与日志消费端的协同
轮转对采集端(filebeat、promtail、pgBadger 等)的影响要主动验证:
- 采集端应按「目录 + 通配符」监听而不是固定文件名,轮转后自动跟随新文件。
- 删除窗口必须大于采集端的读取延迟窗口:文件还在被读取时删除会丢日志。logrotate 的
delaycompress正是为此留出的缓冲。 - pgBadger 等离线分析工具按天消费文件,按天命名 + 手动轮转封口是最顺滑的配合。
压缩与传输:把历史日志的成本降下来
文本日志的压缩比通常在 5~10 倍,长保留期的成本主要靠它化解。分层策略:
| 层 | 内容 | 保留 | 载体 |
|---|---|---|---|
| 热层 | 最近 7~30 天原始文件 | 随时可查 | 本地磁盘(独立分区) |
| 温层 | 已切割、已压缩的归档 | 数月 | 本地或 NAS,按需解压 |
| 冷层 | 合规留存 | 按合规要求 | 对象存储 |
# 每日把「前天」的日志压缩归档(留一天缓冲给采集端)
0 4 * * * gzip /var/lib/postgresql/18/main/log/postgresql-$(date -d '2 days ago' +%F).log
# 归档到对象存储(以 aws cli 为例)
30 4 * * * aws s3 sync /var/lib/postgresql/18/main/log/ s3://pg-logs/main-01/ --exclude "*" --include "*.gz"成本账的三个数字:日均日志量(按天文件大小直接可读)、压缩比(gzip 对文本通常 6~8 倍)、热盘与对象存储的单价差。把「热层 14 天 + 温层压缩 90 天」作为默认起点,再按审计要求调整。
容器与 systemd 场景的轮转
collector 文件化不适用时,轮转交给了生态:
Docker(json-file 驱动)——stdout 日志默认无限增长,必须显式限容:
# 单容器级别
docker run --log-driver json-file \
--log-opt max-size=50m --log-opt max-file=5 postgres:18// 全局默认(/etc/docker/daemon.json)
{ "log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "5" } }systemd(journald)——配额由 journald 管理:
# /etc/systemd/journald.conf
SystemMaxUse=2G
MaxRetentionSec=30day要点:max-size × max-file 就是单容器的日志上限,超限后最旧文件被覆盖(丢弃式轮转);改 daemon.json 只对新建容器生效,存量容器要重建。容器形态下 PostgreSQL 侧的 log_rotation_* 不再参与,轮转参数写进部署模板而不是 postgresql.conf。
多实例部署的目录与命名规划
一机多实例(不同端口、不同数据目录)时,日志规划多两条规则:
- 目录隔离:每个实例独立
log_directory(如inst1/log与inst2/log),混在一个目录里,清理脚本与采集端都无法区分归属。 - 文件名带实例标识:若必须共享目录,把实例名嵌进
log_filename(如pg-inst1-%Y-%m-%d.log)。 - 容量独立核算:每个实例的日志量差异可能很大(主库 vs 归档库),配额脚本按目录跑而不是按机器跑。
# 实例 1
log_directory = '/var/lib/postgresql/18/inst1/log'
log_filename = 'pg-inst1-%Y-%m-%d.log'
# 实例 2
log_directory = '/var/lib/postgresql/18/inst2/log'
log_filename = 'pg-inst2-%Y-%m-%d.log'按容量清理:配额脚本
按天数清理假设日志量均匀,磁盘紧张的环境更稳妥的是「按配额清理最旧」:
#!/usr/bin/env bash
# pg_log_quota.sh —— 日志目录超过 20GB 时,从最旧开始删到 15GB 以下
LOGDIR=${1:?need log dir}
QUOTA=${2:-20971520} # KB
TARGET=${3:-15360000} # KB
used=$(du -sk "$LOGDIR" | cut -f1)
if [ "$used" -lt "$QUOTA" ]; then exit 0; fi
find "$LOGDIR" -maxdepth 1 \( -name '*.log' -o -name '*.csv' -o -name '*.json' \) | sort | while read -r f; do
sz=$(du -sk "$f" | cut -f1)
rm -f "$f"
used=$((used - sz))
[ "$used" -le "$TARGET" ] && break
done# crontab:每小时检查一次
0 * * * * /usr/local/bin/pg_log_quota.sh /var/lib/postgresql/18/main/log 20971520 15360000两个防御细节:find 的模式要和实际产出文件的后缀对齐(csvlog 是 .csv,jsonlog 是 .json——「清理脚本没删掉任何文件」的事故多半是这个原因);配额触顶后的删除不可恢复,热层配额要留出采集延迟与人工处置的余量。
三则轮转事故复盘
真实世界的轮转事故高度雷同,三则典型案例:
事故一:同名追加,单文件 200GB。 配置按天命名却保留了默认的大小轮转阈值——大小轮转触发的新文件与原文件同名,继续追加。文件名没有更细的时间格式化符时,大小轮转形同虚设。教训:命名精度必须匹配轮转意图。
事故二:logrotate 切了文件,新文件永远 0 字节。 运维给日志目录配了 logrotate 却没在 postrotate 里通知 PostgreSQL,进程继续写旧句柄,新文件恒空,采集端「丢」了一周日志。教训:外部轮转必须配套 pg_ctl logrotate(PG12+)或 copytruncate。
事故三:清理脚本通配符与 csvlog 后缀不符。 脚本写的是 postgresql-*.log,开启 csvlog 后产出 .csv 文件,清理半年没生效直到磁盘告警。教训:文件名模式与 log_destination 联动管理,改 destination 时同步 review 清理与采集脚本。
logrotate 方案的完整落地
坚持用系统 logrotate 而非 collector 轮转时,配置必须把「通知」写对:
# /etc/logrotate.d/postgresql-log
/var/lib/postgresql/18/main/log/postgresql-*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 postgres postgres
postrotate
/usr/bin/pg_ctl logrotate -D /var/lib/postgresql/18/main >/dev/null 2>&1 || true
endscript
}要点逐条拆解:
postrotate里的pg_ctl logrotate(PG12+)是灵魂:它让 PostgreSQL 立即关闭旧文件、打开新文件。没有这一步,新文件永远 0 字节(事故二的教训)。PG11 及以前用copytruncate(复制后截断,有丢行窗口)替代。delaycompress延迟一轮压缩:留出采集端读取上一份文件的窗口,避免「边读边压」的竞态。create ... postgres postgres:新文件归属数据库运行账号,否则 collector 打不开新文件。missingok/notifempty:日志目录可能没有当日文件(低流量),不要报错。
systemd timer 也可以替代 cron/logrotate(OnCalendar=daily + 执行 pg_ctl logrotate 的 unit),原理相同:轮转动作必须通知到 PostgreSQL 本体。
备库与归档实例的日志差异
同一套轮转策略不要无脑复制到备库——它们日志的「品类」不同:
- 备库没有 walwriter 写入峰值与检查点(recovery 阶段不同),但会持续出现「恢复冲突」类日志(
recovery is waiting for WAL redo等),长查询与回放冲突的排障都靠它们,max_standby_streaming_delay相关日志建议保留更长。 log_min_duration_statement在备库同样生效:只读负载的慢查询一样要监控,阈值可放宽到主库的 2~3 倍。- 归档实例(archive_command 所在实例):归档失败日志(
archive command failed)是pg_stat_archiver之外的重要信号源,出现即告警。 - 备库的日志量通常只有主库的几分之一,磁盘配额与保留天数可以收紧;但主备切换后角色互换,轮转与保留策略必须按「未来主库」设计而不是按当前角色。
与监控系统的采集衔接
轮转方案定稿前,与采集端对齐三件事:
- 监听方式:采集器按「目录 + 通配符」跟随新文件(
*.csv、*.json),不固定文件名;游标文件(fluent bit 的DB、vector 的 checkpoint)持久化位移,重启不重读。 - 删除窗口 > 读取延迟:文件被删除时采集器必须已经读完。采集高峰积压时的删除就是丢日志,配额脚本与压缩调度要给采集留缓冲。
- 坏行兜底:stderr 多行消息、轮转瞬间的半行,采集端要有 JSON/CSV 解析失败的容错计数,解析失败率突增本身就是监控信号。
轮转参数速查与组合建议
把三组核心参数的语义浓缩成一张速查表,配置前对一遍:
| 参数 | 语义 | 常见误配 |
|---|---|---|
log_rotation_age | 分钟;到期触发时间轮转 | 单位当秒写(60 = 1 小时) |
log_rotation_size | kB;单文件达到触发大小轮转 | 0 是禁用,不是「无限制保留」 |
log_truncate_on_rotation | 时间轮转打开同名文件时截断 | 以为它能阻止追加(它只作用于时间轮转 + 同名场景) |
log_filename | strftime 格式命名 | 精度低于轮转周期 → 同名追加 |
log_file_mode | 文件权限 | 默认 0600,采集器用非 postgres 账号时要放行 |
三种常用组合:
- 按天 + 大小兜底:
%Y-%m-%d+log_rotation_size = 500MB——日常按天,突发流量不会撑爆单文件。 - 按小时(高流量):
%Y-%m-%d_%H+log_rotation_age = 60——配合小时级压缩调度。 - 固定分片:
pg-%a.log一周七个文件循环——配log_truncate_on_rotation = on实现滚动覆盖,适合空间极度受限且无需长期留存的场景。
命名精度必须 ≥ 轮转周期:按天轮转就别用不带日期的文件名,否则大小轮转触发的新文件与旧文件同名继续追加(事故一的根源)。
日志盘与 WAL 归档盘的联动监控
日志盘打满的前兆往往能在 WAL 侧找到镜像信号,巡检时把两块盘放在一起看:
# 两块盘的使用率一起观察(日志盘 + WAL 盘)
df -h /var/lib/postgresql/18/main/log /var/lib/postgresql/18/main/pg_wal-- 归档停滞会反向推高 pg_wal(与日志清理是同一类「保留失控」问题)
SELECT archived_count, failed_count,
last_archived_time, last_failed_time,
coalesce(last_failed_wal, '-') AS last_failed_wal
FROM pg_stat_archiver;联动规则:归档 failed_count 增长或 last_failed_time 逼近当前 → WAL 开始堆积 → 同一块盘(若共盘)的日志清理配额也要收紧。磁盘容量的通用原则:日志、WAL、数据三者分盘或至少分目录配额,任何一块打满都会拖垮整个实例。
上线前自检清单
轮转方案上线(或改配置)后,按这张清单验证:
| 检查项 | 命令 / 观察点 | 通过标准 |
|---|---|---|
| 配置已生效 | SHOW log_destination; SHOW log_filename; | 与目标一致 |
| 新文件按预期产出 | 整点/零点后 ls -lt 日志目录 | 新文件出现,命名正确 |
| 无同名追加 | 新旧文件大小各自增长 | 不存在「一个文件永续变大」 |
| 采集端跟随 | 采集器面板看最近一条日志的时间 | 延迟 < 采集周期 |
| 清理脚本命中 | 手动跑一次清理(dry-run) | 删除/压缩的文件名与预期一致 |
| 权限正确 | ls -l 新文件 | 运行账号可读,采集账号可读 |
| 磁盘水位 | df -h 日志分区 | 低于 80% |
七项全过,才算真正「轮转无感」;任何一项不过都按对应小节回查。
实战示例
示例一:按天命名 + cron 清理的最小组合
# postgresql.conf
logging_collector = on
log_destination = 'csvlog'
log_directory = 'log'
log_filename = 'postgresql-%Y-%m-%d.log'
log_rotation_age = 1440
log_rotation_size = 0
log_truncate_on_rotation = off# 清理脚本已入 cron
crontab -l -u postgres | grep cleanup30 2 * * * /usr/local/bin/pg_log_cleanup.sh /var/lib/postgresql/18/main/log 30验证轮转行为:
SELECT pg_rotate_logfile();ls -l /var/lib/postgresql/18/main/log/-rw------- 1 postgres postgres 52377600 Sep 29 23:59 postgresql-2026-09-29.log
-rw------- 1 postgres postgres 31204 Sep 30 00:00 postgresql-2026-09-30.log示例二:七日循环覆盖(非审计场景)
开发或测试环境,磁盘紧张、无合规要求,七天循环最省事:
log_filename = 'postgresql-%a.log'
log_rotation_age = 1440
log_rotation_size = 0
log_truncate_on_rotation = on效果:每周一到周日各一个文件,第八天覆盖第一天的内容。优点是零清理成本;缺点是历史丢失,任何审计需求都不适用。
示例三:logrotate 全托管(含压缩与保留)
# /etc/logrotate.d/postgresql-log
/var/lib/postgresql/18/main/log/postgresql-*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
dateext
sharedscripts
postrotate
su - postgres -c "pg_ctl logrotate -D /var/lib/postgresql/18/main" || true
endscript
}# 手动验证一次完整流程(不真正执行删除)
logrotate -d /etc/logrotate.d/postgresql-log-d 是 dry-run 模式,先看计划再实跑;确认切割、通知、压缩、保留四步都符合预期后再上线。
示例四:小时级切分的高流量配置
日均日志 20GB 以上的实例,按小时切分方便压缩、搬运与 pgBadger 分时段分析:
logging_collector = on
log_destination = 'csvlog'
log_filename = 'postgresql-%Y-%m-%d_%H.log' # 天 + 小时两级
log_rotation_age = 60 # 每小时
log_rotation_size = 2GB # 兜底:极端流量下按大小再切
log_truncate_on_rotation = off# 配套:小时文件的压缩与清理
# 48 小时内的原始文件保留,超过的压缩,压缩件保留 30 天
0 5 * * * find /var/lib/postgresql/18/main/log -name '*.csv' -mmin +2880 -exec gzip {} \;
30 5 * * * find /var/lib/postgresql/18/main/log -name '*.csv.gz' -mtime +30 -delete验证要点:整点后 1~2 分钟内应出现新小时的文件(惰性轮转依赖下一条日志消息,低峰时段可能延迟);pgBadger 分析时按 postgresql-*-HH.csv 分段喂入,内存友好。
示例五:Docker stdout 容量上限
容器化 PostgreSQL 的日志限额配置与验证:
# 启动时显式限额
docker run -d --name pg-main \
--log-driver json-file --log-opt max-size=50m --log-opt max-file=10 \
postgres:18
# 验证生效
docker inspect pg-main --format '{{.HostConfig.LogConfig.Config}}'map[max-file:10 max-size:50m]# 观察实际占用:单容器上限 = 50m × 10 = 500MB
du -sh $(docker inspect pg-main --format '{{.LogPath}}' | xargs dirname)要点回顾:限额只对新建容器生效;max-file 覆盖即丢历史,重要日志必须同时接入采集管道;PostgreSQL 侧的 logging_collector 在此模式下应保持 off,避免出现「文件写进容器内部、随容器销毁而丢失」的隐性路径。
示例六:轮转故障快速定位
「文件没切、新文件空、文件没删」三类症状各对应一条定位路径:
-- 症状 A:手动触发一次轮转,验证内核侧功能正常
SELECT pg_rotate_logfile();# 症状 B:pg_ctl logrotate 后新文件仍空——看打开的句柄
ls -l /proc/$(head -1 /var/lib/postgresql/18/main/postmaster.pid)/fd \
| grep -c log # 应指向新文件;仍指向旧文件说明通知未生效
# 症状 C:清理脚本没删任何文件——先看真实文件名再对通配符
ls /var/lib/postgresql/18/main/log/ | head -5
echo "glob:"; ls /var/lib/postgresql/18/main/log/postgresql-*.log 2>/dev/null | head -3三步定位的经验:先 pg_rotate_logfile() 区分「内核轮转是否正常」(正常 → 问题在脚本/通知链路;异常 → 问题在配置或权限),再按句柄与通配符两条线收网。定位完成后回到《上线前自检清单》复核一遍。
注意事项
轮转触发是惰性的。 时间轮转要等「下一条消息」才发生,低流量库的文件可能远超 log_rotation_age 仍未切换。需要精确封口用 pg_rotate_logfile()。
同名文件默认追加。 log_truncate_on_rotation = off(默认)时,文件名相同就继续追加。想要「每次轮转新文件」,文件名必须有足够细的时间格式化符。
截断只发生在时间轮转。 重启实例、大小轮转、手动轮转打开同名文件时都不截断(追加)。基于「重启会清空日志」的假设做依赖必然出错。
外部 logrotate 必须通知 PostgreSQL。 只改名不通知,写入继续落在旧句柄上,新文件恒为空。PG12+ 用 pg_ctl logrotate,更老版本考虑 copytruncate 并接受少量丢失。
删除窗口要大于消费延迟。 采集端、离线分析还没读完的文件不能删。日志在被删除前至少要经历「切割 → 采集完成 →(可选压缩)→ 删除」的完整生命周期。
日志量没有免费午餐。 log_min_messages 降低、log_error_verbosity = verbose、连接审计全开,都会线性放大日志量。每次调整记录级别后,观察三天文件大小再下结论。
按大小兜底要有精细文件名配合。 「按天命名 + 小的 rotation_size」不会切出新文件(同名追加);想让大小轮转生效,文件名要有 %H%M%S 级别的变化。
权限与敏感内容照旧。 轮转后的历史文件权限继承 log_file_mode;压缩包、归档副本的存放位置与权限要纳入同一套安全策略,日志里的语句原文(见上一页)不会因为归档而脱敏。
最佳实践
- 生产基线:按天命名 +
log_rotation_age = 1440+log_rotation_size = 0。 一天一文件,清理规则最简单,与 pgBadger 等分析工具天然对齐;超大流量再升级为按小时。 - 清理必须自动化并纳入监控。 cron + find 是最低配;同时把「日志目录使用率」接入告警,防止清理脚本本身失效。
- 保留天数与合规要求对齐。 审计类日志(连接、DDL)的留存期按合规标准定,不要与普通运行日志混用同一保留策略。
- 热盘只留近期、压缩归档历史。 30 天热 + 压缩转存是常见分层;压缩比通常 5~10 倍,长留存期的成本压力主要靠压缩化解。
- 外部 logrotate 与内置轮转二选一。 混用会让文件名与句柄状态失控;选定方案后把配置纳入版本管理。
- 每次轮转配置变更后做一次实机验证。
pg_rotate_logfile()触发、确认新文件出现、旧文件完整、采集端跟随——五分钟演练避免半夜事故。 - 采集端按目录监听并验证轮转跟随。 新文件出现后若干秒内应被采集;把「采集延迟」也做成监控项。
- 低流量实例不要依赖惰性轮转。 用每日定时
pg_rotate_logfile()(cron 里 psql 调用)保证文件边界可预测。 - 容量按「日均值 × 保留天数 × 1.3」估算并留告警。 日志增速随业务波动,80% 使用率告警是最后防线,容量规划才是第一防线。
- 把轮转参数写进部署文档与初始化清单。 新实例交付时日志配置一次性到位(collector、命名、轮转、清理 cron 四件套),避免「先跑起来再说」的技术债。
下一节
继续学习:CSV 日志分析