如何简单地从源码分析问题
1.问题现象
昨天一位同事联系我,说给一张表添加字段卡住,alter table xxx add column xxx夯住了,当即第一反应就是被DML阻塞住了,后面一看,没有那么简单。
2.问题分析
Alter table xxx add column是AccessExclusiveLock,是最重的锁,和其他锁包括自身都互相冲突,所以第一反应是通过pg_locks和pg_stat_activity查看被什么阻塞:
大致是这样,被0号pid阻塞了,然后查看pg_locks
大致是这样,部分做了脱敏处理。
可以看到,pid为null,其他的page、tuple、transactionid都为null,以及virtualtransactionid为-1/4717522,表示持有锁或等待锁的虚拟事务ID。virtual transaction id 由两部分组成,backend process id和local transaction id组成。backend process id不是操作系统的进程ID,而是PG中用来标识进程序列号的ID,local transansaction id和transaction id是不同的,一个是local的,每个backend 进程独有,一个是cluster级的。
看一个正常的例子
所以看到大量null和不知所以然的虚拟事务ID,应该是哪里出问题了,常规的运维手段已经不太能派上用场了。
用pstack看看函数堆栈,如下:
#0 in __epoll_wait_nocancel () from lib64/libc.so.6 #1 in WaitEventSetWaitBlock #2 in WaitEventSetWait #3 in WaitLatchOrSocket #4 in WaitLatch #5 in ProcSleep #6 in WaitOnLock #7 in LockAcquireExtended #8 in LockRelationOid #9 in RangeVarGetRelidExtended #10 in AlterTableLookupRelation #11 ... |
顶层在epoll_wait_nocancel观察并sleep等待
AlterTableLookupRelation对应上面的alter table的请求,需要获取表的模式信息,然后加锁
RangeVarGetRelidExtended获取合适的namespace和表模式信息等
LockRelationOid,Lock a relation given only its OID,通过oid 来lock一个表
LockAcquireExtended则在获取锁类型、锁请求和以及申请锁,同时pstack看到该函数的传参里面有一项十分值得注意,在lock.c里面,reportMemoryError=true,顺着代码找了一下,out of shared memory,感觉有点...
函数堆栈显示的信息都和表的模式信息有关,那就顺着这条路捋一捋。
我们知道,当数据库要访问表的时候,需要表的模式信息,比如表的oid,统计信息等,PostgreSQL将表的模式信息放在系统表中,因此要访问表的时候,需要先从系统表中获取表的这些信息,比如pg_class,pg_attribute等。在一个PostgreSQL系统中,访问系统表和普通表会很频繁,为了提高效率,开辟了一块内存区域Cache,Cache包括一个系统表元组SysCache和一个表模式信息RelCache,SysCache中存放的是最近使用过的系统表的元组,而RelCache中包含所有最新访问过的表的模式信息(包含系统表的信息),注意每一个PostgreSQL进程都维护着自己的SysCache和RelCache,而非进程间共享。
一般涉及到的主要的系统表就是和pg_class,pg_attribute这两个有关,一个记录了表和其他对象如索引、视图等的元信息,pg_attribute则主要是列的相关信息,那么就有迹可寻了:
先获取一下表的pg_class相关信息,这个可以正常获取。
再获取一下表的pg_attribute关于列的信息,如下,发现一些端倪,SELECT attname, attnum, attisdropped
FROM pg_attribute
WHERE attrelid = 'testtab'::regclass
AND attnum > 0
ORDER BY attnum;
可以看到,有一列已经被删了,理应不被其他表看到,会被解析器忽略,但是该字段物理上仍然存在表中。
然后手动通过create table t_bak as select * from t的想备份该表的时候,神奇的一幕发生了,在新的表里面,比第一个表多了一列,也就是说原本被删掉不可见的列又出现了,看样子是哪里判断列的可见性的时候判断出错了,复制表的时候又复制了出来。
于是对应到上面,alter table去加列申请AccessExclusiveLock,获取表的模式信息的时候,到pg_attribute的时候,关于上面这一列的可见性相关判断出了点问题,获取不到锁。另外如前面所说reportMemoryError,还和out of shared memory有关,PostgreSQL数据库使用固定大小的共享内存区域来保存这些锁,在共享内存中保留的锁的数量为max_connections x max_locks_per_transaction,此例shared memory溢出了,内存耗尽,可能导致部分数据读写异常,因为数据库中用到了大量pg_pathman分区表。
自己也尝试去手动更改pg_attribute中attisdropped、attname、atttypid等字段,pg_class的relnatts等字段,后面发现想的太多了,系统表相互之间的依赖关系太复杂了,表结构里面还有默认值,约束,还会涉及到pg_constraint、pg_type等等
所以最后放弃了这个表的修复,并且还有备份存在,加之之前手动create 了一个备份表,让开发继续使用bak表。
3.总结
这次事件结合了传统的运维手段再加上一点代码的简单分析,主要还是得益于PostgreSQL的高质量代码和详细清晰的注释,看看注释基本上就能大概知道这个函数在做什么,再加上pstack + strace的组合拳。
对于使用到大量分区表的场景,如pg_pathman、pg_partman等,考虑增大max_locks_per_transaction该值,不然容易out of shared memroy,注意行级锁不在内存中,不会影响这个;
永远不要手动去动系统表,关系太过于复杂,除非你知道自己在干什么。尤其是生产环境,此例是在开发环境,同时有备份的存在,加之要大概摸清楚可能的原因才如此操作。
备份永远是最简单粗暴的解决方式。