日志文件管理
数据库出问题时,第一现场往往只剩下日志。本节讲解 PostgreSQL 的日志收集器(Logging Collector)如何把日志落到文件、如何按时间或大小做日志轮转(Log Rotation),以及怎样控制日志内容——让它既能在排障时给出足够信息,又不至于把磁盘写满。
前置知识
阅读本节前,建议先了解:配置文件位置与加载
基础概念
- 日志收集器(Logging Collector):由
logging_collector开启的后台进程,负责把服务端进程写到stderr的输出重定向到文件 - 日志目的地(Log Destination):
log_destination决定日志格式,取值为stderr、csvlog、jsonlog的组合 - 日志目录(Log Directory):
log_directory,默认log,相对数据目录(Data Directory)解析 - 日志文件名(Log Filename):
log_filename,支持strftime风格的占位符 - 日志轮转(Log Rotation):日志写满一段时间或达到一定大小后切换新文件
- 日志级别(Message Level):从
DEBUG5到PANIC共 17 级,log_min_messages控制写入日志的最低级别 - 日志行前缀(Log Line Prefix):
log_line_prefix,定义每条日志开头的附加字段,如时间、用户、数据库 - 慢查询日志(Slow Query Log):通过
log_min_duration_statement记录执行超过指定毫秒数的 SQL - 启动日志(Startup Log):
log_startup_progress_interval控制启动慢时每隔多久输出一次进度
| 配置项 | 作用 | 生效方式 | 默认值 |
|---|---|---|---|
logging_collector | 是否启用日志收集器 | 重启(postmaster) | off |
log_destination | 日志格式 | 重启 | stderr |
log_directory | 日志目录 | 重启 | log |
log_filename | 日志文件名模板 | 重启 | postgresql-%Y-%m-%d_%H%M%S.log |
log_rotation_age | 按时间轮转 | 重启 | 1d |
log_rotation_size | 按大小轮转 | 重启 | 10MB |
log_min_messages | 写入日志的最低级别 | 只加载配置(Reload) | warning |
log_min_duration_statement | 慢查询阈值(毫秒) | Reload | -1(不记录) |
log_line_prefix | 日志行前缀 | Reload | %m [%p] |
log_timezone | 日志时间戳时区 | Reload | Etc/UTC |
版本说明
jsonlog 格式自 PostgreSQL 15 引入,18 中同样可用,便于对接 Loki / Elasticsearch 等日志系统。log_connections 自 17 起接受 receipt、authentication、authorization 等字符串取值,比早期的 on / off 更精细。
语法与用法
开启日志收集器
# postgresql.conf
# 开启日志收集器,将 stderr 重定向到文件(需重启生效)
logging_collector = on
# 日志格式:stderr 为默认文本格式
log_destination = 'stderr'
# 日志目录,相对路径基于数据目录,也可写绝对路径
log_directory = 'log'
# 文件名模板,这里按天切分(每天一个文件)
log_filename = 'postgresql-%Y-%m-%d.log'logging_collector 属于 postmaster 级参数,修改后必须重启服务:
# Debian / Ubuntu
sudo systemctl restart postgresql
# RHEL / Rocky(带版本号的系统服务名)
sudo systemctl restart postgresql-18
# 直接用 pg_ctl(需在 postgres 用户下执行)
sudo -u postgres pg_ctl -D /var/lib/postgresql/18/main restart# 确认参数已生效
sudo -u postgres psql -c "SHOW logging_collector;" -c "SHOW log_directory;" -c "SHOW log_filename;" logging_collector
-------------------
on
(1 row)
log_directory
---------------
log
(1 row)
log_filename
----------------------
postgresql-%Y-%m-%d.log
(1 row)日志轮转配置
# postgresql.conf
# 每隔多久切换一个新的日志文件,0 表示禁用按时间轮转
log_rotation_age = 1d
# 单个日志文件达到多大后切换,0 表示禁用按大小轮转
log_rotation_size = 100MB
# 启动时是否截断已存在的同名日志文件
log_truncate_on_rotation = on控制日志内容
# postgresql.conf
# 写入日志的最低消息级别:debug5 ~ panic
log_min_messages = warning
# 客户端发来的错误消息记录到日志的最低级别
log_min_error_statement = error
# 记录执行时间超过 500ms 的语句(-1 为禁用,0 表示记录所有语句)
log_min_duration_statement = 500ms
# 记录连接建立与断开
log_connections = on
log_disconnections = on
# 输出执行计划(谨慎开启,日志量极大)
# auto_explain.log_min_duration = 1000日志行前缀
# postgresql.conf
# %m 带毫秒的时间戳;%p 进程号;%u 用户名;%d 数据库名;%a 应用名;%h 客户端地址;%l 会话日志行号
log_line_prefix = '%m [%p] %q%u@%d/%a '详细说明
日志收集器与输出位置
PostgreSQL 的日志有两条完全不同的路径:
- 不启用收集器(默认):所有消息写到
stderr,由启动它的父进程负责处置。用 systemd 启动时,stderr被 journald 接管,日志进入journalctl;用 Docker 启动时,stderr就是容器标准输出,日志进入docker logs。 - 启用收集器:
postmaster额外派生一个logger进程,把stderr上的内容按配置写入磁盘文件,同时仍保留一份到stderr。
# 查看是否运行着 logger 进程
ps -ef | grep '[l]ogger'postgres 1198 1147 0 09:12 ? 00:00:00 /usr/lib/postgresql/18/bin/postgres -D /var/lib/postgresql/18/main
postgres 1201 1147 0 09:12 ? 00:00:00 postgres: logger两种路径各有适用场景:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 物理机 / 虚拟机 + systemd | 关闭收集器,交给 journald | 无需自己管理轮转,journalctl -u postgresql 直接查 |
| 需要长期归档、按天切分 | 开启收集器 | 文件形式便于打包、投递给日志平台 |
| Docker / Kubernetes | 关闭收集器,走 stdout | 容器日志由运行时采集,写文件反而不好捞 |
| 需要结构化日志 | 开启收集器 + csvlog / jsonlog | 便于程序解析 |
注意:容器里不要开收集器
官方 Docker 镜像默认把日志输出到标准输出(stderr),这样 docker logs 才能看到内容。若在容器里设置 logging_collector = on,日志会写进容器内文件系统,容器一删日志就没了,还会绕过运行时的日志采集。
日志轮转
轮转有两种触发条件,二者同时生效、谁先满足谁触发:
- 时间维度:
log_rotation_age,默认1d - 大小维度:
log_rotation_size,默认10MB
轮转后的文件名由 log_filename 模板决定。模板使用 strftime 占位符:
| 占位符 | 含义 | 示例输出 |
|---|---|---|
%Y | 四位年份 | 2026 |
%m | 两位月份 | 09 |
%d | 两位日期 | 21 |
%H / %M / %S | 时 / 分 / 秒 | 09 / 12 / 33 |
%a | 星期缩写 | Mon |
%u | 星期数字(1~7) | 1 |
%% | 字面量百分号 | % |
关键规则:只有模板中包含时间占位符时,轮转才会产生不同的文件名。如果写成固定的 postgresql.log,需要配合 log_truncate_on_rotation = on 才能复用同一文件,否则日志会一直追加到同一文件、无限增长。
# postgresql.conf
# 场景 A:按天一个文件,保留全部历史(配合外部清理脚本)
log_filename = 'postgresql-%Y-%m-%d.log'
log_rotation_age = 1d
log_rotation_size = 0
# 场景 B:固定 7 个文件循环覆盖(%a 为星期,一周一循环)
# log_filename = 'postgresql-%a.log'
# log_rotation_age = 1d
# log_truncate_on_rotation = on
# 场景 C:按大小切分,不按时间
# log_filename = 'postgresql-%Y%m%d_%H%M%S.log'
# log_rotation_age = 0
# log_rotation_size = 100MB手工触发一次轮转
不重启服务也能强制轮转——向 postmaster 发送 SIGHUP 时,如果距离上次轮转已超过 log_rotation_age,就会切分新文件;另外 pg_ctl logrotate -D <数据目录> 会直接指示收集器立即轮转一次。Debian 系还会安装 /etc/logrotate.d/postgresql-common 做外部轮转。
日志级别与慢查询
日志级别共 17 级,从高到低:PANIC、FATAL、LOG、ERROR、WARNING、NOTICE、INFO、DEBUG1 ~ DEBUG5。log_min_messages 表示「该级别及以上才写入服务器日志」。
| 取值 | 含义 | 典型用途 |
|---|---|---|
error | 只记录错误 | 生产环境、日志量要求极低 |
warning | 错误 + 警告(默认) | 常规生产环境 |
notice | 加上提示信息 | 排查应用行为异常 |
log | 加上检查点、连接等运行事件 | 需要审计周期性动作 |
info | 加上 VACUUM、ANALYZE 等语句 | 排查自动清理问题 |
debug1 ~ debug5 | 详细内部信息 | 仅限短时间排障,日志量暴涨 |
log_min_messages 只影响服务器日志。客户端连接上报错的级别另由 client_min_messages 控制,两者互不影响——这解释了「为什么 psql 里看到报错,日志文件里却没有」。
排障时最常用的其实是慢查询日志:
# postgresql.conf
# 记录超过 500ms 的语句
log_min_duration_statement = 500ms
# 记录 DDL 语句(CREATE / ALTER / DROP)
log_statement = 'ddl'
# 记录所有语句(压力测试用,生产勿开)
# log_statement = 'all'
# 对慢语句采样记录(PG 13+),避免高峰期日志爆炸
log_min_duration_sample = 100ms
log_statement_sample_rate = 0.1# 用 ALTER SYSTEM 临时调整,改完 reload 即生效,无需重启
sudo -u postgres psql -c "ALTER SYSTEM SET log_min_duration_statement = '1s';"
sudo -u postgres psql -c "SELECT pg_reload_conf();"LOG: duration: 2341.882 ms statement: SELECT count(*) FROM orders WHERE created_at > now() - interval '1 year';日志行前缀
log_line_prefix 决定每条日志的公共前缀,是日志可读性的关键。默认值 %m [%p] 只有时间和进程号,定位问题常常不够。
| 占位符 | 含义 | 说明 |
|---|---|---|
%m | 时间戳(含毫秒) | 最常用的时间字段,UTC 时区由 log_timezone 决定 |
%t | 时间戳(不含毫秒) | 体积更小 |
%p | 进程 ID | 用于串起同一后端进程的多行日志 |
%u | 数据库用户名 | 谁触发的 |
%d | 数据库名 | 在哪个库里发生的 |
%a | 应用名 | 对应 application_name,便于区分应用 |
%h | 客户端主机 | 连接来源 IP |
%l | 会话日志行号 | 同一会话内递增,用于排序 |
%e | SQLSTATE 错误码 | 程序化分类错误 |
%q | 不输出任何内容 | 仅用于非会话进程(如检查点)时截断后续字段 |
生产环境常用的组合:
# postgresql.conf
# 时间 + 进程号 + 用户@数据库/应用名 + 客户端地址 + 会话行号
log_line_prefix = '%m [%p] %q%u@%d/%a %h '2026-09-21 09:12:33.421 CST [1201] app_user@shop/psql 10.0.0.21 LOG: duration: 1522.311 ms statement: SELECT * FROM orders LIMIT 10;%q 的妙用
%q 只在会话相关进程中输出(相当于把后面的字段「激活」),对后台进程则截断其后所有内容。写法 [%p] %q%u@%d 能让后台进程输出 [1201] 后直接接消息,避免出现 [1201] @ / 这种空字段噪音。
实战示例
场景一:生产环境日志配置
需求:日志按天切分、保留在数据目录下的 pg_log、记录慢查询、包含足够的上下文信息。
# postgresql.conf
# ---- 采集与格式 ----
logging_collector = on
log_destination = 'stderr'
log_directory = '/var/log/postgresql/18/main' # 使用绝对路径,便于独立挂载分区
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_timezone = 'Asia/Shanghai'
# ---- 轮转 ----
log_rotation_age = 1d
log_rotation_size = 100MB
log_truncate_on_rotation = off
# ---- 内容 ----
log_min_messages = warning
log_min_error_statement = error
log_min_duration_statement = 500ms
log_connections = on
log_disconnections = on
log_checkpoints = on
log_lock_waits = on
log_line_prefix = '%m [%p] %q%u@%d/%a %h '
# ---- 性能相关 ----
log_temp_files = 10MB
log_autovacuum_min_duration = 0配置完成后重启并验证:
# 1. 创建日志目录并确认属主
sudo mkdir -p /var/log/postgresql/18/main
sudo chown postgres:postgres /var/log/postgresql/18/main
# 2. 重启服务
sudo systemctl restart postgresql
# 3. 制造一条慢查询
sudo -u postgres psql -c "SELECT pg_sleep(1);" >/dev/null
# 4. 查看最新日志文件
ls -lt /var/log/postgresql/18/main | head -n 3
sudo tail -n 20 /var/log/postgresql/18/main/postgresql-*.log2026-09-21 09:15:02.113 CST [1240] postgres@postgres/psql [local] LOG: duration: 1001.557 ms statement: SELECT pg_sleep(1);场景二:用 jsonlog 对接日志平台
结构化日志省去正则解析,直接投递给采集器即可:
# postgresql.conf
logging_collector = on
log_destination = 'jsonlog'
log_directory = 'log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.json'
log_rotation_age = 1h
log_rotation_size = 50MB# 用 jq 直接筛选慢查询
sudo cat /var/lib/postgresql/18/main/log/postgresql-*.json | jq -r 'select(.duration != null) | "\(.timestamp) \(.duration) \(.statement)"'2026-09-21 09:20:11.402 CST 1522.311 SELECT * FROM orders LIMIT 10;场景三:日志清理与磁盘保护
日志写满磁盘会导致数据库不可写(甚至无法启动),必须做保留策略。
# 方案 A:find 按修改时间清理,保留 14 天(放进 crontab 每日执行)
find /var/log/postgresql/18/main -name 'postgresql-*.log' -type f -mtime +14 -delete
# 方案 B:用 logrotate 管理(Debian 系已内置 /etc/logrotate.d/postgresql-common)
sudo logrotate -d /etc/logrotate.d/postgresql-common # -d 为演练,不实际执行
# 方案 C:压缩后归档到对象存储
find /var/log/postgresql/18/main -name 'postgresql-*.log' -type f -mtime +1 \
-exec gzip {} \; -exec aws s3 cp {}.gz s3://db-logs/ \;# /etc/logrotate.d/postgresql-common 的关键内容
/var/log/postgresql/*.log {
weekly
rotate 6
compress
delaycompress
missingok
notifempty
create 0640 postgres postgres
}清理前先确认文件没被占用
若开启了 logging_collector,收集器进程始终持有当前日志文件的句柄。用 rm 删除正在写入的文件,磁盘空间不会立即释放(df 仍显示占用)。正确做法是走轮转(pg_ctl logrotate)之后再删除旧文件,或让 logrotate 使用 copytruncate。
场景四:排障时临时提高日志级别
需要短时间抓取详细日志时,用 ALTER SYSTEM 调整,事后务必回滚:
# 1. 提高级别并重新加载
sudo -u postgres psql <<'SQL'
ALTER SYSTEM SET log_min_messages = 'info';
ALTER SYSTEM SET log_min_duration_statement = 0;
SELECT pg_reload_conf();
SQL
# 2. 复现问题并抓取日志
sudo tail -f /var/log/postgresql/18/main/postgresql-*.log
# 3. 排障结束后回滚(RESET 会移除 postgresql.auto.conf 中的对应项)
sudo -u postgres psql <<'SQL'
ALTER SYSTEM RESET log_min_messages;
ALTER SYSTEM RESET log_min_duration_statement;
SELECT pg_reload_conf();
SQL注意事项
logging_collector是postmaster级参数,修改必须重启,不能用 reload 生效。log_rotation_age与log_rotation_size建议至少保留一个非 0;两个都为 0 时日志永不轮转。log_filename若不含时间占位符且log_truncate_on_rotation = off,日志会持续追加到同一文件,直到磁盘写满。log_min_duration_statement = 0会记录所有语句,仅在排障时短时开启,否则日志量和 I/O 开销都不可接受。log_statement = 'all'会把明文 SQL 全量落盘,其中可能包含密码等敏感字面量,生产环境禁止长期开启。log_min_messages与client_min_messages是两套独立配置,调了前者不代表客户端能看到更多信息。log_directory的写权限必须属于数据库运行用户(通常是postgres),否则启动时收集器会静默失败。- 日志目录不要放在数据目录所在的同一分区上,日志暴涨会连带撑爆数据盘。
- 开启
debug1~debug5会产生极大的日志量,务必限定时长并提前确认磁盘余量。 log_connections/log_disconnections在高并发短连接场景会显著增加日志量,需评估后开启。csvlog/jsonlog与stderr可以同时开启,但会增加一份写出开销。- 时间戳时区由
log_timezone决定,默认Etc/UTC,与应用时区不一致时排查时间线容易出错,建议显式设为本地时区。 - 用
ALTER SYSTEM修改的参数会写入postgresql.auto.conf,其优先级高于postgresql.conf;后续手工编辑postgresql.conf不会覆盖它。
最佳实践
- 管理方式二选一:systemd 环境交给 journald +
journalctl,容器环境交给运行时采集 stdout,需要按天归档再开启logging_collector,避免两套机制打架。 - 显式设置
log_timezone = 'Asia/Shanghai',让日志时间与应用、监控系统对齐。 - 统一使用
log_line_prefix = '%m [%p] %q%u@%d/%a %h ',时间、进程、身份、来源四要素齐全,便于跨系统关联。 - 生产环境
log_min_duration_statement从500ms或1s起步,观察一周后再收紧或放宽;高峰期可用采样参数控制量级。 - 至少开启
log_checkpoints、log_lock_waits、log_temp_files,这三项是排查性能问题的「低成本高回报」配置。 - 制定明确的保留策略(常见为 7~30 天),并用 logrotate 或定时任务落地,配好磁盘告警。
- 日志目录独立挂载分区,并设置配额,防止日志写满导致数据库不可用。
- 需要投递到日志平台(Loki、Elasticsearch)时优先用
jsonlog,免去正则解析的脆弱性。 - 排障用
ALTER SYSTEM临时调整参数,问题定位后立即ALTER SYSTEM RESET回滚,避免遗留在postgresql.auto.conf中。 - 定期抽查日志中
FATAL、PANIC以及重复出现的WARNING,很多隐患(如连接数打满、复制延迟、临时文件过多)都先在这里露头。
下一节
继续学习:psql 命令行详解