跳过正文
数据库运维实践:MySQL 高可用与 PostgreSQL 调优经验

数据库运维实践:MySQL 高可用与 PostgreSQL 调优经验

·1117 字·6 分钟·
目录

数据库这层出事代价最大,也是最经不起折腾的地方。这几年维护 MySQL 和 PostgreSQL 踩过的坑和攒下的 SOP 一并整理出来,只讲实操,不重复基础原理。

MySQL 主从复制延迟
#

复制延迟(Seconds_Behind_Master)是 MySQL 主从架构最常见的问题,严重时会导致读从库的业务读到旧数据,更严重时如果主库宕机、从库延迟过大,切换主库会有数据丢失风险。

监控延迟
#

-- 在从库执行,查看详细复制状态
SHOW REPLICA STATUS\G

-- 关键字段:
-- Seconds_Behind_Master: 从库落后主库的秒数
-- Relay_Log_Space: relay log 大小,快速增长说明 SQL thread 处理慢
-- Exec_Master_Log_Pos vs Read_Master_Log_Pos: 两者差距大说明 SQL thread 跟不上 IO thread

Seconds_Behind_Master 有一个陷阱:它是用「当前正在执行的 binlog 事件的时间戳」和「当前时间」计算的。如果从库有一个大事务执行了很久,这个值会持续增大,但事务提交后会瞬间跳回 0。不能只看快照值,要看趋势。

延迟原因分类
#

1. 大事务

主库的 DDL(ALTER TABLE)或大批量 DML 会产生一个巨大的 binlog event,从库必须串行执行完这个事务才能继续。

-- 找出正在执行的大事务(从库执行)
SELECT * FROM information_schema.INNODB_TRX 
ORDER BY trx_started ASC LIMIT 10;

-- 查看 relay log 当前执行到哪
SHOW PROCESSLIST;

处理:大表 DDL 一定用 pt-online-schema-changegh-ost,分批次执行,避免长时间锁表和大 binlog。

2. 从库单线程回放

MySQL 5.7 之前从库 SQL thread 是单线程的,主库并发写入高时,从库跟不上。MySQL 5.7+ 支持并行复制:

# my.cnf 从库配置
[mysqld]
slave_parallel_workers = 8        # 并行线程数,建议 CPU 核数的 2-4 倍
slave_parallel_type = LOGICAL_CLOCK  # 比 DATABASE 模式并发度更高
slave_preserve_commit_order = ON  # 保证从库事务提交顺序与主库一致(数据一致性)

3. 从库 IO 瓶颈

从库写 relay log 和读 relay log 都在磁盘,IO 慢会是瓶颈。检查:

# 查看磁盘 IO 使用率
iostat -x 1 5

# 查看 MySQL 的 IO wait
SELECT * FROM performance_schema.file_summary_by_event_name 
WHERE event_name LIKE 'wait/io/file/innodb%'
ORDER BY total_latency DESC LIMIT 10;

复制监控告警
#

# Prometheus 告警规则
- alert: MySQLReplicationLag
  expr: mysql_slave_status_seconds_behind_master > 30
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "MySQL 从库复制延迟超过 30 秒"
    description: "实例 {{ $labels.instance }} 延迟 {{ $value }} 秒"

- alert: MySQLReplicationStopped
  expr: mysql_slave_status_slave_sql_running == 0
    or mysql_slave_status_slave_io_running == 0
  for: 1m
  labels:
    severity: critical

慢查询分析
#

开启慢查询日志
#

# my.cnf
[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1        # 超过 1 秒记录(生产建议 0.5-1)
log_queries_not_using_indexes = ON
log_throttle_queries_not_using_indexes = 10  # 每分钟最多记录 10 条未用索引的查询,避免日志爆炸
min_examined_row_limit = 100  # 扫描行数小于 100 的不记录

pt-query-digest 分析
#

pt-query-digest 是分析慢查询日志的最佳工具,按查询模式聚合,找出最耗时的 SQL:

# 分析慢查询日志,按总耗时倒序
pt-query-digest /var/log/mysql/slow.log \
  --order-by Query_time:sum \
  --limit 20 \
  > slow_report.txt

# 只看最近 1 小时的
pt-query-digest /var/log/mysql/slow.log \
  --since "1h" \
  --limit 10

# 输出关键字段含义:
# Response time: 总耗时(占比)
# Calls: 执行次数
# R/Call: 平均每次耗时
# Rows sent/examined: 发送行数/扫描行数(比值低说明全表扫描)

EXPLAIN 解读关键点
#

EXPLAIN SELECT o.id, u.name 
FROM orders o 
JOIN users u ON o.user_id = u.id 
WHERE o.status = 'pending' AND o.created_at > '2026-01-01'
ORDER BY o.created_at DESC 
LIMIT 100\G

看 EXPLAIN 结果时,重点关注:

字段好的值需要优化
typeref, range, constALL(全表扫), index(全索引扫)
key有值(用了索引)NULL(没用索引)
rows尽量小超过表总行数的 10% 要注意
ExtraUsing index(覆盖索引)Using filesort(内存/磁盘排序), Using temporary

Using filesort 不一定慢(数据量小时可以),但配合 rows 很大时就是问题。

覆盖索引是常见优化手段:

-- 原始查询需要回表(先查索引,再读数据行)
SELECT id, name, email FROM users WHERE status = 'active';

-- 创建覆盖索引,查询只需读索引页
ALTER TABLE users ADD INDEX idx_status_covering (status, id, name, email);
-- EXPLAIN 的 Extra 列会显示 Using index

PostgreSQL 连接池:PgBouncer
#

PostgreSQL 的连接模型与 MySQL 不同:每个连接对应一个后端进程(fork),连接数高时内存消耗大,且连接建立本身(TCP + auth + catalog lookup)开销不小。Web 应用动辄几百个并发连接,不用连接池会把 Postgres 压垮。

PgBouncer 配置
#

# pgbouncer.ini

[databases]
# 格式: 逻辑库名 = host=... dbname=...
mydb = host=postgres-host port=5432 dbname=mydb
mydb_readonly = host=postgres-replica port=5432 dbname=mydb

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 5432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt

# 连接池模式(核心配置)
# session: 客户端连接期间独占一个服务端连接(兼容性最好,省连接效果差)
# transaction: 事务结束后释放服务端连接(推荐,对应用透明)
# statement: 每条 SQL 后释放(不支持事务,少用)
pool_mode = transaction

# 每个 database/user 组合的最大服务端连接数
default_pool_size = 20

# 总服务端连接上限
max_client_conn = 1000

# 队列中等待连接的最大时间(超时返回错误,避免雪崩)
query_wait_timeout = 30

# 空闲连接保留时间
server_idle_timeout = 600

# 统计信息更新间隔
stats_period = 60

# 日志
log_connections = 0  # 生产关掉,否则日志量巨大
log_disconnections = 0

max_connections 规划
#

PostgreSQL 的 max_connections 建议值是 (CPU核数 × 4)(CPU核数 × 10)。太高会导致上下文切换开销大,反而降低吞吐。

-- 查看当前连接使用情况
SELECT 
  state,
  count(*) as count,
  max(now() - state_change) as max_duration
FROM pg_stat_activity 
WHERE datname = 'mydb'
GROUP BY state;

-- 找出长时间 idle 的连接(可能是连接池泄漏)
SELECT pid, usename, application_name, state, 
       now() - state_change as idle_duration
FROM pg_stat_activity 
WHERE state = 'idle' 
  AND now() - state_change > interval '10 minutes'
ORDER BY idle_duration DESC;

-- 强制关闭问题连接
SELECT pg_terminate_backend(pid) 
FROM pg_stat_activity 
WHERE pid <> pg_backend_pid()
  AND state = 'idle'
  AND now() - state_change > interval '30 minutes';

PgBouncer 使用 transaction 模式的限制:

不支持以下特性,应用要注意:

  • SET 命令(会话级参数设置)
  • LISTEN/NOTIFY
  • pg_advisory_lock
  • 预处理语句(PREPARE/EXECUTE)— 在 pgbouncer.ini 里可以开启 server_reset_query 解决部分场景

索引管理
#

找出可以删除的冗余索引
#

-- PostgreSQL:找出从未被使用的索引(重启后才能准确,RDS 实例重启较少,数据比较可信)
SELECT 
  schemaname,
  tablename,
  indexname,
  pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
  idx_scan as times_used
FROM pg_stat_user_indexes
WHERE idx_scan = 0
  AND indexname NOT LIKE '%pkey%'  -- 主键不算
ORDER BY pg_relation_size(indexrelid) DESC;

-- MySQL:找出未使用的索引
SELECT 
  object_schema,
  object_name,
  index_name,
  count_read,
  count_write
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema NOT IN ('mysql', 'performance_schema', 'information_schema')
  AND index_name IS NOT NULL
  AND count_read = 0
ORDER BY count_write DESC;

删索引前的注意事项:

  1. idx_scan = 0 的索引不一定能删,要确认监控窗口足够长(至少覆盖一个完整业务周期,包括月末跑批等低频场景)
  2. 外键约束会自动创建索引,要先检查是否被外键使用
  3. 删除前在 staging 环境验证,观察慢查询日志

统计信息维护
#

-- PostgreSQL:查看统计信息是否过期
SELECT 
  relname,
  n_live_tup,
  n_dead_tup,
  last_vacuum,
  last_autovacuum,
  last_analyze,
  last_autoanalyze
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;

-- 手动触发 vacuum + analyze(不锁表)
VACUUM ANALYZE orders;

-- 查看 autovacuum 是否在正常工作
SELECT pid, query, state, now() - state_change as duration
FROM pg_stat_activity
WHERE query LIKE '%autovacuum%';

表膨胀(dead tuple 积累)会导致查询变慢、索引效率下降。如果 autovacuum 跟不上(大表高频写入场景),要调整 autovacuum 参数或手动 vacuum:

-- 针对特定高频写入表调整 autovacuum 频率
ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor = 0.01,  -- 默认 0.2,即 1% 变更就触发
  autovacuum_analyze_scale_factor = 0.005
);

备份策略与恢复演练
#

备份的核心价值在于「能恢复」,而不是「有备份」。很多团队有备份,但从来没做过恢复演练,真出事才发现备份文件损坏或恢复流程有问题。

标准备份策略:

  • 全量备份:每天一次(AWS RDS 自动执行),保留 7-30 天
  • binlog/WAL 备份:持续上传到 S3,支持 point-in-time recovery(PITR)

恢复演练 SOP:

# MySQL(基于 RDS 快照的 PITR 演练)
# 1. 在 AWS 控制台或 CLI 将快照恢复到测试实例
aws rds restore-db-instance-to-point-in-time \
  --source-db-instance-identifier prod-mysql \
  --target-db-instance-identifier recovery-test \
  --restore-time "2026-04-01T10:00:00Z"

# 2. 连接测试实例,验证数据完整性
mysql -h recovery-test.xxx.rds.amazonaws.com -u admin -p << 'EOF'
SELECT COUNT(*) FROM orders WHERE created_at < '2026-04-01 10:00:00';
SELECT MAX(created_at) FROM orders;
EOF

# 3. 记录恢复时间(RTO)
# 一般 RDS PITR 恢复需要 10-30 分钟,取决于数据库大小

建议每季度做一次完整的恢复演练,并记录 RTO(恢复时间目标)和 RPO(恢复点目标)的实际值。

RDS 运维注意事项
#

AWS RDS vs 自建 MySQL/PostgreSQL
#

方面RDS自建
主从切换自动(Multi-AZ),约 60-120 秒需要手动或脚本,更快但需要维护
参数修改通过 Parameter Group,部分需重启直接改 my.cnf/postgresql.conf
超级权限受限,无法 SUPER,改用 RDS 特有存储过程完全控制
操作系统访问
维护窗口要规划,避免业务高峰自己控制

RDS 常踩的坑:

  1. 参数组修改生效时机dynamic 参数立即生效,static 参数需要重启实例。在变更参数组前确认参数类型,避免计划外重启。

  2. 存储自动扩展:RDS 支持存储自动扩展,但扩展后无法缩小。建议开启自动扩展 + 设置上限,同时监控存储使用率告警(80%时告警,90%时紧急处理)。

  3. Enhanced Monitoring vs CloudWatch:Enhanced Monitoring 是 OS 级别的监控(1 秒粒度),CloudWatch 是数据库层面(1 分钟粒度)。排查 IO/CPU 问题时要用 Enhanced Monitoring。

常用运维 SQL 备忘
#

-- ====== MySQL ======

-- 查看表大小(按数据+索引排序)
SELECT 
  table_schema,
  table_name,
  ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb,
  table_rows
FROM information_schema.TABLES
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema')
ORDER BY (data_length + index_length) DESC
LIMIT 20;

-- 查看当前正在执行的查询(排除 Sleep)
SELECT id, user, host, db, command, time, state, LEFT(info, 100) as query
FROM information_schema.PROCESSLIST
WHERE command != 'Sleep'
ORDER BY time DESC;

-- 查看 InnoDB 状态(锁等待分析)
SHOW ENGINE INNODB STATUS\G

-- 查看锁等待关系
SELECT 
  r.trx_id waiting_trx_id,
  r.trx_mysql_thread_id waiting_thread,
  r.trx_query waiting_query,
  b.trx_id blocking_trx_id,
  b.trx_mysql_thread_id blocking_thread,
  b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

-- ====== PostgreSQL ======

-- 查看表大小(含 TOAST)
SELECT 
  relname as table_name,
  pg_size_pretty(pg_total_relation_size(relid)) as total_size,
  pg_size_pretty(pg_relation_size(relid)) as table_size,
  pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) as index_size
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 20;

-- 查看当前活跃查询及等待事件
SELECT 
  pid,
  usename,
  application_name,
  state,
  wait_event_type,
  wait_event,
  now() - query_start as duration,
  LEFT(query, 100) as query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC NULLS LAST;

-- 查看锁冲突
SELECT 
  blocked.pid AS blocked_pid,
  blocked.query AS blocked_query,
  blocking.pid AS blocking_pid,
  blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking 
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.cardinality(pg_blocking_pids(blocked.pid)) > 0;

-- 查看索引使用率(命中率低于 99% 考虑优化)
SELECT 
  sum(idx_blks_hit) / nullif(sum(idx_blks_hit + idx_blks_read), 0) as index_hit_rate,
  sum(heap_blks_hit) / nullif(sum(heap_blks_hit + heap_blks_read), 0) as table_hit_rate
FROM pg_statio_user_tables;

小结
#

MySQL 和 PostgreSQL 的日常运维有很多共通之处:监控延迟/慢查询、定期清理无用索引、保证备份可恢复。区别主要在连接模型(PostgreSQL 必须用连接池)和统计信息维护(PostgreSQL 的 autovacuum 需要更多关注)。RDS 降低了运维门槛,但不意味着可以不关注数据库内部状态,定期查 slow log 和监控连接数是最基本的习惯。

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。

相关文章

PostgreSQL 运维实战:配置调优、连接池、慢查询与高可用

·1918 字·10 分钟
系统梳理 PostgreSQL 运维核心技能:从 shared_buffers、WAL 参数调优,到 PgBouncer 事务模式配置;从 pg_stat_statements 慢查询分析到 PITR 时间点恢复;以及主从流复制、膨胀表清理和 Prometheus 监控指标的完整实践。