vlambda博客
学习文章列表

如何简单地从源码分析问题

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,注意行级锁不在内存中,不会影响这个;

永远不要手动去动系统表,关系太过于复杂,除非你知道自己在干什么。尤其是生产环境,此例是在开发环境,同时有备份的存在,加之要大概摸清楚可能的原因才如此操作。

备份永远是最简单粗暴的解决方式。