本文介绍如何在RDS PostgreSQL中通过设置锁等待超时和自动化重试DDL操作,有效解决在执行添加字段的DDL操作时所遇到的锁阻塞问题,从而减少对业务的影响。
背景
大部分情况下,RDS PostgreSQL在添加不带默认值的字段时能够在秒级内完成,带有非计算默认值的字段同样从RDS PostgreSQL 11版本开始具备了秒级添加的能力。详情请参见PostgreSQL官网。
然而,在实际业务中,尽管添加字段的DDL操作通常为秒级,但仍可能面临锁阻塞问题。这主要是由于autovacuum或业务长事务造成的表锁争用。在RDS PostgreSQL中,添加字段的DDL操作所需的表锁为排它锁,这个锁会阻止其他读写操作,导致相关请求进入表锁的等待队列,直至该DDL操作完成。
解决方案
方案一
设置事务级别的锁等待,超时后将自动取消DDL操作。例如:
BEGIN;
SET LOCAL lock_timeout = 500; -- 设置锁定超时时间为500毫秒
ALTER TABLE <表名> ADD COLUMN <列名> VARCHAR; -- 添加字段的DDL操作
COMMIT; -- 提交事务
事务级别的锁等待时间(lock_timeout)请根据实际情况设置,锁等待时间越长,对目标表的读写TPS影响则越显著。详情请参见下文的压测效果。
方案二
在设置事务级别锁等待的基础上,自动化周期性执行DDL,直至操作成功。例如:
DO $$
DECLARE
msg text;
BEGIN
LOOP
BEGIN
PERFORM pg_sleep(1);
SET LOCAL lock_timeout = 500;
ALTER TABLE <表名> ADD COLUMN <列名> VARCHAR; -- 添加字段的DDL操作
EXIT;
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS msg = MESSAGE_TEXT;
RAISE NOTICE 'failed, error: %s', msg;
END;
END LOOP;
RAISE NOTICE 'success!';
END;
$$;
(可选)终止阻塞DDL的连接
执行此操作会导致相关连接断开,请谨慎操作。
查询当前连接的进程ID(PID)。
SELECT pg_backend_pid();
执行添加字段的DDL。
ALTER TABLE <表名> ADD COLUMN <列名> VARCHAR;
查询阻塞DDL操作的进程。
SELECT pg_blocking_pids(<步骤1查询到的当前连接的pid>);
手动终止阻塞DDL的进程。
取消一个正在执行的进程。
SELECT pg_cancel_backend(<步骤3查询到的阻塞DDL操作的pid>);
强制终止目标进程。
SELECT pg_terminate_backend(<步骤3查询到的阻塞DDL操作的pid>);
说明终止autovacuum进程可能会失败。
压测效果
使用sysbench工具对测试表进行只读压力测试(oltp_read_only),测试时长为300s,并在测试表上执行长事务。在此过程中,当执行添加字段的DDL语句时,设置锁等待超时与未设置锁等待超时的结果如下:
未设置锁等待超时(lock_timeout),执行添加字段的DDL操作在遇到锁阻塞问题时,测试实例的读写TPS为0。
设置了锁等待超时(lock_timeout),执行添加字段的DDL操作在遇到锁阻塞问题时,对测试实例的读写TPS影响有限。然而,锁等待超时(lock_timeout)的时间越长,对测试实例的读写TPS影响则越显著。
测试期间的TPS趋势图及各个阶段的操作如下所示:
图中序号 | 执行的操作 | 说明 |
1 | 使用sysbench工具对测试表进行只读压力测试,并在测试表上执行长事务。 | 无 |
2 | 执行持续约30秒的DDL操作(添加字段操作)。 | 执行添加字段的DDL期间,实例的TPS降至0。 |
3 | 依照方案二,将lock_timeout设置为100ms后,执行持续约30秒的DDL操作(添加字段操作)。 | 执行添加字段的DDL期间,实例的TPS存在轻微波动。 |
4 | 依照方案二,将lock_timeout设置为500ms后,执行持续约30秒的DDL操作(添加字段操作)。 | 执行添加字段的DDL期间,实例的TPS存在显著波动。 |
5 | 结束长事务,详细操作请参见(可选)终止阻塞DDL的连接。 | 添加字段的DDL迅速成功执行。 |
相关文档
使用sysbench工具对RDS PostgreSQL进行性能测试,请参见PostgreSQL版性能测试。