读懂信号,决定先动哪里
目标
数据库故障中代价最高的错误,是把受害者误认为根因。 即使杀掉四个被阻塞的会话,它们还会再次阻塞,而真正阻塞其他会话的那个仍然存在。
这里将读取真实形态的快照,并决定应先处理什么。
材料
/opt/lab/dbsignals/activity.tsv pg_stat_activity 스냅숏 30줄
/opt/lab/dbsignals/locks.tsv 누가 누구를 막고 있는가
/opt/lab/dbsignals/statements.tsv pg_stat_statements 상위 질의
mkdir -p /root/dbsignals && cp /opt/lab/dbsignals/* /root/dbsignals/ && cd /root/dbsignals
column -t -s $'\t' activity.tsv | head -5
要留下的文件
01-states.txt 상태별 개수
02-waits.txt 무엇을 기다리는가
03-root.txt 사슬의 뿌리
04-statements.txt 총 시간과 평균 시간
05-decide.md 지금 할 것과 나중에 할 것
06-prevent.md 다시 안 생기게
07-notes.md 왜 그런지
当前正在运行什么
统计 activity.tsv 中各状态的数量,并把 idle in transaction 中最老会话的秒数写入 01-states.txt。再用一行说明该状态为何危险。
awk -F'\t' 'NR>1 {print $3}' activity.tsv | sort | uniq -c
idle 通常只是空闲连接,一般不是问题。idle in transaction 则不同——它在保持事务开启的同时无所事事,持有的锁不会释放,清理(vacuum)也无法越过该位置。
正在等待什么
只筛选active 状态的会话,按 wait_event_type 统计并写入 02-waits.txt,同时说明 Lock 等待与 IO 等待的处理方法有何不同。
awk -F'\t' 'NR>1 && $3=="active" {print ($4=="" ? "(없음)" : $4)}' activity.tsv | sort | uniq -c
等待类型为空,表示会话正在实际占用 CPU 运行。
Lock 必须等其他会话释放,因此应查看那个其他会话。IO 表示存储较慢或数据未在缓存中,应检查查询或硬件。同样表现为“慢”,排查位置却完全不同。
寻找阻塞链的根
查看 locks.tsv,把谁被阻塞、谁在阻塞别人写入 03-root.txt,并说明根会话正在做什么。
被阻塞的会话是受害者。即使杀掉它们,也会再次被阻塞。
找到根后,在 activity.tsv 中查看该 pid 的状态和最后执行的查询,并与前一步的观察联系起来。
总耗时与平均耗时是不同问题
在 statements.tsv 中分别找出总耗时最大的查询和因调用次数极多而累计耗时很大的查询,写入 04-statements.txt,并说明两者的处理方法有何不同。
平均 3 秒的查询与平均 0.2ms 的查询相比,后者若调用 480 万次,累计耗时也会相近。
平均耗时高时,应优化该查询本身(索引、预计算聚合)。调用次数高时,应优化调用方(N+1、缓存、批处理)。
决定现在做什么、以后做什么
把此刻应采取的行动写入 05-decide.md。分为立即处理、很快检查、以后修复三类,并分别说明原因。
现在只需决定一件事:对该会话是取消还是终止。
pg_cancel_backend(pid)— 只取消当前查询,连接仍保留pg_terminate_backend(pid)— 断开连接,打开的事务也会回滚
idle in transaction 没有正在运行的查询,因此无法通过取消解除。
防止再次发生
把防止同类事故再次发生的配置和告警写入 06-prevent.md,并为配置给出具体数值。
idle_in_transaction_session_timeout 可以整体阻止该事故。还应同时设定 lock_timeout 和 statement_timeout。
只说“开启”无法落地。 必须给出具体数值。
仅靠配置看不到下一次事故,还应写明要监控什么,例如最老事务的年龄。
写给下一个阅读者
从本实验的观察中选择至少四项,整理到 07-notes.md。不要只写做了什么,要写明为什么。
假设凌晨被叫醒的自己会读这份文件。“查看了 pg_stat_activity”没有帮助;“被阻塞者是受害者——杀掉后仍会再次阻塞”才有帮助。