那一刻的窒息感
凌晨三点十七分,手机开始疯狂震动。
Sentry 的告警通知像鞭炮一样涌进来。短短两分钟内,七个平台的报错量爆了。错误日志里反复出现同一句话:
Error: connect ECONNREFUSED 127.0.0.1:5432
数据库挂了。
我光着脚跑进书房,屏幕上 PostgreSQL 的进程状态是 crashed。所有平台全部不可用——登录、支付、内容发布、消息推送,全部停摆。
第一反应:重启
先重启 PostgreSQL 服务:
sudo systemctl restart postgresql
# Failed! Error: disk full
磁盘满了。df -h 一看,/var/lib/postgresql 挂载的磁盘 100% 满了。
追根溯源:WAL 日志爆炸
PostgreSQL 的 WAL(Write-Ahead Log)日志目录占了 180GB。原因是三天前我开启了 log_statement = 'all' 来做性能分析,但忘了关掉,也忘了设置日志轮转。
-- 我三天前执行的命令
ALTER SYSTEM SET log_statement = 'all';
SELECT pg_reload_conf();
-- 但我忘了这些!
-- 没有设置日志轮转
-- 没有设置日志大小限制
-- 没有设置自动清理
三天内,150 万条 SQL 日志塞满了磁盘。PostgreSQL 写入到一半发现磁盘满了,直接崩溃退出。数据库的文件系统可能已经损坏了。
恢复过程(一次次绝望)
尝试 1:清理日志
rm -rf /var/lib/postgresql/14/main/pg_log/*
# 释放了 180GB
重启 PostgreSQL——还是起不来。磁盘满了导致最后的 WAL 文件残缺,PostgreSQL 拒绝启动。
尝试 2:pg_resetwal
pg_resetwal -f /var/lib/postgresql/14/main
# Transaction log reset
这相当于给数据库做心脏起搏器——能用,但可能丢失最后几条事务。好在是凌晨三点,基本没用户操作。
重启数据库,起来了!
但是…… SELECT count(*) FROM users 执行了 8 秒。索引全坏了。WAL 不完整导致很多索引状态脏了。
尝试 3:重建索引
REINDEX DATABASE water_db;
# 等了 12 分钟
索引重建完成。查询恢复正常。最后检查一遍数据完整性:
-- 检查有没有脏数据
SELECT count(*) FROM orders WHERE created_at IS NULL;
-- 0 条,谢天谢地
SELECT pg_last_wal_receive_lsn() - pg_last_wal_replay_lsn() AS lag;
-- 0,主从同步正常
凌晨四点半,全部平台恢复。七个平台一共宕机了 78 分钟。
事后的补救措施
第二天睡了三小时,下午开始重建灾备体系:
1. 日志自动轮转
# postgresql.conf
log_rotation_age = 1d # 每天轮转
log_rotation_size = 100MB # 超过100MB就轮转
log_truncate_on_rotation = on
2. 磁盘监控 + 告警
# crontab:每5分钟检查磁盘使用率
*/5 * * * * [ $(df /var/lib/postgresql | tail -1 | awk '{print $5}' | tr -d '%') -gt 85 ] && /usr/local/bin/alert-disk-full
3. 慢日志只记录关键语句
-- 生产环境只用这个级别
ALTER SYSTEM SET log_min_duration_statement = 1000; -- 只记录 1 秒以上的慢查询
ALTER SYSTEM SET log_statement = 'none'; -- 关掉全量查询日志
4. 自动化备份验证
之前我每周手动备份,但从不验证备份有效性。现在加了自动恢复测试:
#!/bin/bash
# 每周日凌晨自动恢复备份到测试库,验证数据完整性
pg_restore --dbname=water_db_test --schema-only latest.dump
if [ $? -ne 0 ]; then
curl -X POST "https://api.xxx.com/alert" -d '{"msg":"Database backup validation FAILED"}'
fi
5. 运维手册
把这次事故的排查步骤和恢复命令写成了文档,保存在 Git 仓库里。下次再出事——虽然我希望不会有下次——至少不用在凌晨三点从零开始排查。
刻在骨子里的教训
数据库运维铁律
□ 开启任何全局日志前,先确认轮转策略
□ 磁盘使用率不能超过 80%
□ 备份恢复必须定期自动化验证
□ 凌晨三点是所有事故的高发期
□ 每个运维操作,做到一半要反问自己:如果现在拔电源,会怎样?
心态上的变化
这件事之前,我对运维的态度是"能用就行"。觉得"又不是大厂,没必要搞得那么复杂"。
但用户不会因为你是个人开发者就原谅你宕机。 一个用户在凌晨三点打开你的平台发现打不开,他不会想"哦这个开发者只有一个人",他只会想"这个平台真烂"然后关掉。
可靠性不是看你是不是大厂,是看你对用户的态度。
写在最后
这次事故的成本:
- 直接损失:78 分钟全平台宕机
- 数据损失:三条订单记录由于 WAL 残缺无法恢复(幸好凌晨三点下单的本来就很少)
- 心理损失:至今每次凌晨收到手机震动都会心跳加速
但换来的是一套可靠的运维体系,和一颗敬畏生产环境的心。
做独立开发,你是一个人扛所有。代码你要写,产品你要定,运维你也要兜底。但正是这些"半夜爬起来救火"的时刻,让你从一个写代码的人,变成了一个真正负责的人。