Skip to content

日志文件管理 ​

数据库出问题时,第一现场往往只剩下日志。本节讲解 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日志时间戳时区ReloadEtc/UTC

版本说明

jsonlog 格式自 PostgreSQL 15 引入,18 中同样可用,便于对接 Loki / Elasticsearch 等日志系统。log_connections 自 17 起接受 receipt、authentication、authorization 等字符串取值,比早期的 on / off 更精细。

语法与用法 ​

开启日志收集器 ​

ini
# postgresql.conf

# 开启日志收集器,将 stderr 重定向到文件(需重启生效)
logging_collector = on

# 日志格式:stderr 为默认文本格式
log_destination = 'stderr'

# 日志目录,相对路径基于数据目录,也可写绝对路径
log_directory = 'log'

# 文件名模板,这里按天切分(每天一个文件)
log_filename = 'postgresql-%Y-%m-%d.log'

logging_collector 属于 postmaster 级参数,修改后必须重启服务:

bash
# 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
bash
# 确认参数已生效
sudo -u postgres psql -c "SHOW logging_collector;" -c "SHOW log_directory;" -c "SHOW log_filename;"
text
 logging_collector
-------------------
 on
(1 row)

 log_directory
---------------
 log
(1 row)

     log_filename
----------------------
 postgresql-%Y-%m-%d.log
(1 row)

日志轮转配置 ​

ini
# postgresql.conf

# 每隔多久切换一个新的日志文件,0 表示禁用按时间轮转
log_rotation_age = 1d

# 单个日志文件达到多大后切换,0 表示禁用按大小轮转
log_rotation_size = 100MB

# 启动时是否截断已存在的同名日志文件
log_truncate_on_rotation = on

控制日志内容 ​

ini
# 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

日志行前缀 ​

ini
# postgresql.conf

# %m 带毫秒的时间戳;%p 进程号;%u 用户名;%d 数据库名;%a 应用名;%h 客户端地址;%l 会话日志行号
log_line_prefix = '%m [%p] %q%u@%d/%a '

详细说明 ​

日志收集器与输出位置 ​

PostgreSQL 的日志有两条完全不同的路径:

  1. 不启用收集器(默认):所有消息写到 stderr,由启动它的父进程负责处置。用 systemd 启动时,stderr 被 journald 接管,日志进入 journalctl;用 Docker 启动时,stderr 就是容器标准输出,日志进入 docker logs。
  2. 启用收集器:postmaster 额外派生一个 logger 进程,把 stderr 上的内容按配置写入磁盘文件,同时仍保留一份到 stderr。
bash
# 查看是否运行着 logger 进程
ps -ef | grep '[l]ogger'
text
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 才能复用同一文件,否则日志会一直追加到同一文件、无限增长。

ini
# 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 里看到报错,日志文件里却没有」。

排障时最常用的其实是慢查询日志:

ini
# 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
bash
# 用 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();"
text
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会话日志行号同一会话内递增,用于排序
%eSQLSTATE 错误码程序化分类错误
%q不输出任何内容仅用于非会话进程(如检查点)时截断后续字段

生产环境常用的组合:

ini
# postgresql.conf

# 时间 + 进程号 + 用户@数据库/应用名 + 客户端地址 + 会话行号
log_line_prefix = '%m [%p] %q%u@%d/%a %h '
text
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、记录慢查询、包含足够的上下文信息。

ini
# 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

配置完成后重启并验证:

bash
# 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-*.log
text
2026-09-21 09:15:02.113 CST [1240] postgres@postgres/psql [local] LOG:  duration: 1001.557 ms  statement: SELECT pg_sleep(1);

场景二:用 jsonlog 对接日志平台 ​

结构化日志省去正则解析,直接投递给采集器即可:

ini
# 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
bash
# 用 jq 直接筛选慢查询
sudo cat /var/lib/postgresql/18/main/log/postgresql-*.json | jq -r 'select(.duration != null) | "\(.timestamp) \(.duration) \(.statement)"'
text
2026-09-21 09:20:11.402 CST 1522.311 SELECT * FROM orders LIMIT 10;

场景三:日志清理与磁盘保护 ​

日志写满磁盘会导致数据库不可写(甚至无法启动),必须做保留策略。

bash
# 方案 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/ \;
text
# /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 调整,事后务必回滚:

bash
# 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

注意事项 ​

  1. logging_collector 是 postmaster 级参数,修改必须重启,不能用 reload 生效。
  2. log_rotation_age 与 log_rotation_size 建议至少保留一个非 0;两个都为 0 时日志永不轮转。
  3. log_filename 若不含时间占位符且 log_truncate_on_rotation = off,日志会持续追加到同一文件,直到磁盘写满。
  4. log_min_duration_statement = 0 会记录所有语句,仅在排障时短时开启,否则日志量和 I/O 开销都不可接受。
  5. log_statement = 'all' 会把明文 SQL 全量落盘,其中可能包含密码等敏感字面量,生产环境禁止长期开启。
  6. log_min_messages 与 client_min_messages 是两套独立配置,调了前者不代表客户端能看到更多信息。
  7. log_directory 的写权限必须属于数据库运行用户(通常是 postgres),否则启动时收集器会静默失败。
  8. 日志目录不要放在数据目录所在的同一分区上,日志暴涨会连带撑爆数据盘。
  9. 开启 debug1 ~ debug5 会产生极大的日志量,务必限定时长并提前确认磁盘余量。
  10. log_connections / log_disconnections 在高并发短连接场景会显著增加日志量,需评估后开启。
  11. csvlog / jsonlog 与 stderr 可以同时开启,但会增加一份写出开销。
  12. 时间戳时区由 log_timezone 决定,默认 Etc/UTC,与应用时区不一致时排查时间线容易出错,建议显式设为本地时区。
  13. 用 ALTER SYSTEM 修改的参数会写入 postgresql.auto.conf,其优先级高于 postgresql.conf;后续手工编辑 postgresql.conf 不会覆盖它。

最佳实践 ​

  1. 管理方式二选一:systemd 环境交给 journald + journalctl,容器环境交给运行时采集 stdout,需要按天归档再开启 logging_collector,避免两套机制打架。
  2. 显式设置 log_timezone = 'Asia/Shanghai',让日志时间与应用、监控系统对齐。
  3. 统一使用 log_line_prefix = '%m [%p] %q%u@%d/%a %h ',时间、进程、身份、来源四要素齐全,便于跨系统关联。
  4. 生产环境 log_min_duration_statement 从 500ms 或 1s 起步,观察一周后再收紧或放宽;高峰期可用采样参数控制量级。
  5. 至少开启 log_checkpoints、log_lock_waits、log_temp_files,这三项是排查性能问题的「低成本高回报」配置。
  6. 制定明确的保留策略(常见为 7~30 天),并用 logrotate 或定时任务落地,配好磁盘告警。
  7. 日志目录独立挂载分区,并设置配额,防止日志写满导致数据库不可用。
  8. 需要投递到日志平台(Loki、Elasticsearch)时优先用 jsonlog,免去正则解析的脆弱性。
  9. 排障用 ALTER SYSTEM 临时调整参数,问题定位后立即 ALTER SYSTEM RESET 回滚,避免遗留在 postgresql.auto.conf 中。
  10. 定期抽查日志中 FATAL、PANIC 以及重复出现的 WARNING,很多隐患(如连接数打满、复制延迟、临时文件过多)都先在这里露头。

下一节

继续学习:psql 命令行详解

参考链接 ​