背景
某客户在执行应用程序的高可用切换测试时,收到了来自数据库的报错,提示不能创建自治事务。
ERROR: autonomous transaction failed to create autonomous session
DETAIL: wait /data/gaussdb506/tmp:7456 timeout expired
数据库环境是GaussDB 506.0 SPC0100.0.0 集中式单机,应用软件部署在两个节点上。同一时刻只有一个应用节点承载业务,当主应用节点故障时,另一个应用节点会接管业务。
测试时先让两个应用都开启,主应用节点正常开始业务,然后断掉主应用节点的网卡,由备应用节点接管。切换后数据库顿时出现整体卡顿:新建连接要等待很长时间,已经建立的连接执行任何 SQL 都非常慢。大约 25 秒后,日志中出现了前面提到的自治事务创建报错。
GaussDB 的自治事务是服务端自行使用 libpq 建立的一个本地连接。
分析
该应用执行业务时,会根据配置的并发任务数创建对应数量的数据库连接,本次测试并发数配置为 200。测试人员也反馈,当并发为 100 时不会出现这个问题。
检查数据库最大连接数和最大自治事务连接数,都远超测试并发度:
gaussdb=# show max_connections;
max_connections
-----------------
3000
(1 row)
gaussdb=# show max_concurrent_autonomous_transactions;
max_concurrent_autonomous_transactions
----------------------------------------
1000
(1 row)
该应用执行的每个业务,都会调用一个自治事务的存储过程用于记录日志。因此当业务并发数为 200 时,也会同时创建 200 个自治事务连接,最大连接数也只有 400 个。
尝试编写简单用例进行复现:写一个 shell 文件,循环后台使用 gsql 调用自治事务匿名块,模拟应用行为。
#!/bin/bash
rm -rf lll.log
for i in {1..200}
do
gsql -t <<'SQL_CMD' >>lll.log 2>&1 &
select 1;
declare
PRAGMA AUTONOMOUS_TRANSACTION;
begin
pg_sleep(30);
end;
/
select pg_sleep(200);
SQL_CMD
echo "Launched $i at $(date)" >> lll.log
done
由于是后台异步执行,这个脚本很快就能跑完。top 观察此时 CPU 和内存占用都不高,ps -ef |grep gsql 也能看到还有 200 个 gsql 进程。
随后另开一个 gsql 连接,发现连接时间比之前长了很多。连接上之后执行以下 SQL 观察,此时任意 SQL 执行都会非常耗时:
gaussdb=# select application_name,count(1) from pg_stat_activity where application_name in ('gsql','autonomoustransaction') and pid<>pg_backend_pid() group by application_name;
application_name | count
-----------------------+-------
autonomoustransaction | 54
gsql | 200
(2 rows)
可以看到 gsql 主事务已经有 200 个,但自治事务只有 54 个,说明有大量自治事务尚未创建。也就是说,并发创建自治事务确实会因某种原因产生阻塞。检查 lll.log 文件,也出现了创建自治事务超时的报错。
继续查看官方文档《自治事务-规格约束》:https://doc.hcs.huawei.com/db/zh-cn/gaussdbqlh/25.1.30/devg-cent/gaussdb-42-0970.html
自治事务执行时,将会在后台启动自治事务 session,可以通过 max_concurrent_autonomous_transactions 设置自治事务执行的最大并行数量,该参数取值范围为 0~10000,默认值为 10。
当 max_concurrent_autonomous_transactions 参数设置为 0 时,自治事务将无法执行。
自治事务新启 session 后,将使用默认 session 参数,不共享主 session 下对象(包括 session 级别变量、本地临时变量、全局临时表的数据等)。
自治事务理论上限为 10000,实际上限为动态值,参考 GUC 参数 max_concurrent_autonomous_transactions 描述。
自治事务受通信缓冲区影响,返回给客户端的信息大小受限于通信缓冲区长度,超过通信缓冲区长度时报错。
自治事务的锁不受 lock timeout 影响,锁超时时间为 2147483s,自治事务执行超过此时间会报错锁超时。
自治事务设置建立连接超时时间 5s,建立连接尝试 5 次。建立连接期间不立即响应信号,每次建立连接前检查信号。高并发、高 CPU、高内存,以及线程池扩容场景下可能存在超时报错现象。
在 PACKAGE SPECIFICATION 或 PACKAGE BODY SPECIFICATION 中声明自治事务 PRAGMA AUTONOMOUS_TRANSACTION 语法,可成功创建 PACKAGE,但自治事务不生效。
其中有一条注意事项和当前现象很像:超时 5 秒、尝试 5 次,一共就是 25 秒。
于是观察故障时间点的 CPU、内存、磁盘 IO 情况,却发现这些指标反而比平时更低,服务器几乎没有在干活。那么有可能就是文档里提到的“线程池扩容场景”了。
为了验证是否与线程池特性有关,尝试关闭线程池后再执行上面的 shell 测试。这次自治事务创建非常顺利,也没有出现报错:
gaussdb=# select application_name,count(1) from pg_stat_activity where application_name in ('gsql','autonomoustransaction') and pid<>pg_backend_pid() group by application_name;
application_name | count
-----------------------+-------
autonomoustransaction | 200
gsql | 200
(2 rows)
看起来和线程池功能有关。不过开启线程池后,改成 400 个并发且不使用自治事务,并没有出现阻塞,400 个 gsql 连接瞬间就连接成功。
这里的设计大概率存在问题:从线程池里获取线程用于普通连接和用于自治事务连接,似乎走了两种不同逻辑,自治事务连接需要阻塞度更大的锁。不过没有源码,暂时无法继续分析。
大概看了一眼 openGauss 的自治事务超时:从 3.1 版本开始改成了固定的 10min,之前版本没有额外超时设置,与 libpq 共用。也就是说,自治事务代码在 openGauss 和 GaussDB 中已经有所区别。实测 openGauss 在线程池模式下,并发创建自治事务也会导致数据库整体卡顿,只是由于超时需要 10min,所以没有立即报错,自治事务连接数会慢慢增加。但如果并发度更高,仍可能出现自治事务连接超过 10min 而报错。
测试已经充分说明:在 GaussDB 里开启线程池时,并发创建自治事务会导致数据库整体卡顿、新连接建立缓慢、旧连接执行 SQL 缓慢,以及自治事务执行超时报错。但应用需求就在眼前,怎么解决?
回到 GaussDB 文档,“线程池扩容场景下可能存在超时报错现象”。既然扩容会超时,那么不让它扩容,一开始就把线程数配够,是不是就不会超时了?
GaussDB 中有这样一个参数:
thread_pool_attr
参数说明:用于控制线程池功能的详细属性,该参数仅在 enable_thread_pool 打开后生效,仅 sysadmin 用户可以访问。
参数类型:字符串
参数单位:无
取值范围:
该参数分为三个部分,'thread_num, group_num, cpubind_info',这三个部分的具体含义如下:
• thread_num:线程池中的初始线程总数,可以动态扩充,取值范围是 0~4096。其中 0 的含义是数据库根据系统 CPU core 的数量来自动配置线程池的线程数,如果参数值大于 0,线程池中的线程数等于 thread_num。线程池大小建议根据硬件配置进行设置,计算公式如下:thread_num = CPU核数*(3~5),thread_num 最大值为 4096。
• group_num:线程池中的线程分组个数,取值范围是 0~64。其中 0 的含义是数据库根据系统 NUMA 组的个数来自动配置线程池的线程分组个数,如果参数值大于 0,线程池中的线程组个数等于 group_num。
• cpubind_info:线程池是否绑核的配置参数。可选择的配置方式有:1. '(nobind)',线程不做绑核;2. '(allbind)',利用当前系统所有能查询到的 CPU core 做线程绑核;3. '(nodebind: 1, 2)',利用 NUMA 组 1、2 中的 CPU core 进行绑核;4. '(cpubind: 0-30)',利用 0-30 号 CPU core 进行绑核;5. '(numabind: 0-30)',在 NUMA 组内利用 0-30 号 CPU core 进行绑核。该参数不区分大小写。当开启资源多租模式时,该参数不生效。
默认值:'4096,2,(nobind)'(196核CPU/1536G内存,128核CPU/1024G内存,104核CPU/1024G内存,96核CPU/1024G内存);'2048,2,(nobind)'(96核CPU/768G内存,80核CPU/640G内存);'1024,2,(nobind)'(64核CPU/512G内存,60核CPU/480G内存,32核CPU/256G内存);'512,2,(nobind)'(16核CPU/128G内存);'256,2,(nobind)'(8核CPU/64G内存)
设置方式:该参数属于 POSTMASTER 类型参数,请参考表 1 中对应设置方法进行设置。
设置建议:内存充足且 CPU 性能好的情况下,当业务需要更多连接时可以增加该参数值。
设置不当的风险与影响:若 thread_pool_attr 设置值大于等于 max_connections,会导致 proc 资源全部被线程池占用,出现 proc 资源不足的问题。默认值即推荐值,不建议修改。修改必须详细确认参数的规格限制,并考虑硬件资源是否足够,否则可能导致数据库异常。
当前环境配置的是 256,2,(nobind)。也就是说,数据库启动时会同时启动 256 个 worker 线程;动态扩容意味着并不只有 256 个 worker 能同时干活,理论上会根据实际需要自动增加。但自治事务这里似乎有些差异:结合上面查询的会话数,主事务 200 个,自治事务 54 个,总数已经接近 256。所以理论上把 256 调大,改成超过 400,并发 200 个连接套自治事务就不会报错了。
实测也的确如此。我们调整安装初始化参数配置,直接把 thread_pool_attr 改成 4096,2,(nobind),这样启动时就会有四千多个空闲 worker 线程,在当前应用场景下绝对够用。
这种“主事务加自治事务会话数需要低于启动时线程池个数”的表现,在 openGauss 中似乎并不存在,因此继续看 openGauss 源码分析意义也不大。
不过这也意味着,在当前 GaussDB 数据库版本中,由于默认开启了线程池,如果业务代码经常使用自治事务,无论机器硬件配置多高,最大支持的并发业务数其实只有 2048 个,超过后可能导致数据库整体变慢,甚至报错。
结论
在 GaussDB 中,如果频繁使用自治事务,当 thread_pool_attr 的第一个值小于应用并发连接数的两倍时,数据库可能会出现严重卡顿以及报错。因此,在硬件资源足够的情况下,建议把 thread_pool_attr 的第一个值配置为实际最大客户端连接数的两倍。
参考链接
[1] https://doc.hcs.huawei.com/db/zh-cn/gaussdbqlh/25.1.30/devg-cent/gaussdb-42-0970.html