1 月 11 日,腾讯云 TDSQL PG 开源版(开源代号 TBase)再升级:分布区表关联查询性能(join)提升超 10 倍,同时提升了产品在分布式场景下的易用性,增加灵活可用的功能组件。
该升级版本在第十一届 PostgreSQL 中国技术大会上正式公布,同时更新文档已同步在 GitHub 上。依托社区和内部业务系统的实践检验,TDSQL PG 开源版基本保持每月一次小升级、每半年一次重大升级的节奏,助力众多开发者应用前沿数据库技术。
本期将为大家深度解读 TDSQL PG 开源版升级特性,具体包括:分区表功能增强、异地多活易用性增强、分布式死锁自动检测并解锁功能、2PC 残留自动检测并清理功能等硬核干货。
TDSQL PG 版开源发展历程
TDSQL PG 版(原名 TBase)是腾讯自主研发的新一代分布式国产数据库,其具备业界领先的 HTAP 能力,属于 MPP 无共享架构,在企业级安全方面采用三权分立安全体系。
图片
2020 年,腾讯云正式宣布数据库品牌 TDSQL 的全新战略升级计划。原有的 TDSQL、TBase、CynosDB 三大产品线统一升级为“腾讯云企业级分布式数据库 TDSQL”。全新升级后的腾讯云 TDSQL 涵盖分布式、分析型、云原生等多引擎融合的完整数据库产品体系。TDSQL PG 版则是 TDSQL 系列产品之一,主要针对 HTAP 场景。
下图是 TDSQL PG 版的总体框架。左上角为事务管理器 GTM,负责整个集群的事务管理和全局事务的协调。右上角为协调节点 CN,它是业务应用访问的入口,每个节点对等,业务连接任意节点,最终返回的数据都相同。右下角为数据节点 DN,每个 DN 只存储部分用户数据,所有 DN 一起组成完整的数据集。最左边和最下边的部分属于管控系统,负责系统的资源管理和告警监控。
图片
TDSQL PG 版最早可追溯到 2008 年,2012 年其发布第一个分布式版本,2015 年其发布第二个分布式版本。截至目前,TDSQL PG 版已经在众多市场客户中得到应用,包括数字广东、云南公安、微信广告、微信支付等。值得一提的是,TDSQL PG 版在 2019 年中标 PICC,成为行业内第一个成功落地保险核心系统的分布式国产数据库。
图片
TDSQL PG 版在 2019 年 11 月 7 日正式宣布开源。开源地址在 Github 平台上,下图是供广大开发人员使用参考文档的 wiki 地址。
图片
TDSQL PG 版开源对用户有重要价值,分别体现在:
版本经过大量业务验证,成熟稳定,开源版本和腾讯自用版本共基线,能够帮助用户快速构建核心业务。同时支持 OLTP/OLAP 能力,提供一站式数据库解决方案。安全可控的企业级分布式数据库能力,腾讯专业研发运营团队持续投入开发和维护,推动社区进步。团队积极回馈开源社区,即时响应用户需求,努力创造开源社会价值。
TDSQL PG 版开源特性回顾
2021 年 7 月,我们进行了 TDSQL PG 版 2.2.0 版本的升级。2.2.0 升级主要包括四个方面:内存管理优化,提供会话内存视图,进行优化管理;优化分布式执行器、优化器;分布式调优性能增强;分布式执行可视化能力增强。图片
2.1 执行器/优化器优化
我们将子查询在内部优化成一个关联查询,提高查询效率。以视图中的表格为例,需要根据表 A 中的 ID 找到表 B 中每一条对应 ID 的数据,再用表 A 的值与表 B 中值的 MIN 进行比较。在优化前,因为这是一个子查询,假如表 A 有 1000 条记录,则 A 中每一条记录都要在表 B 进行全量扫描。做完这个查询至少要将表 B 扫描一千次,这样做的成本非常高。在优化后,因为表 B 需要取 B 的 MIN 最小值,且我们需要利用 id 做 join,所以可以先扫描一遍 B 表,根据 id 先进行聚集,把每一个 id 最小的 MIN 值取出来,生成聚集表,最后聚集表与表 A 进行 hash join,因此我们只需将 B 表进行一次扫描即可。图片
我们还对 distinct 进行性能优化。在优化前,需要将所有数据从 DN 节点拉到 CN 节点,由 CN 节点进行 distinct 的去重操作。因为分布式系统数据量特别大,如果全部拉到 CN,会导致数据量大,网络负载高,CN 也会成为单点瓶颈,难以发挥分布式特性。我们针对此类情况进行优化。先在 DN 本地进行部分的 distinct 去重,使得每个 DN 可以并行计算,并行后将结果全部发到 CN 上,由 CN 进行最终的去重操作,减少了大量的网络交互,充分发挥出分布式数据库的特性。图片
在对优化器进行优化后,我们取部分 TPCDS 标准测试集进行测试,发现有些 SQL 的性能可以提升上千倍。图片
2.2 内存管理优化
我们提供 Memory 的内存视图详情,能够详细看到每一个进程占用的内存情况。除了对视图进行增强外,我们还对内存使用进行详细优化。图片
下图展示的是假设有一个用户连接,这个连接在访问一万张表后的内存占用情况(访问完该连接还没有退出、仍处于空闲状态时的内存占用情况)。从下表中可以看到,TopMemoryContext 上出现数量级增长,实现 10 倍以上提升,CacheMemoryContext 也实现 20 倍的提升。图片
针对明显空闲的连接却占用大量内存的不合理情况,我们进行了优化。我们将内存的使用分为两部分:第一部分是 RelCache,即用户数据表的 Cache;第二部分是 Cat Cache,即元数据的 Cache。如果需要对某个 Rel 进行 Cache,我们会将新来的项放在 Relation LRU List 的最前面,此后每次来新的项我们都往前面加。当 LRU 项目超过预定意义的参数时,我们会从后往前去遍历 LRU,将引用计数为 0 的 Relation 的内存项释放掉。Cat LRU 元数据缓存也同理,我们将新来的某条元数据缓存,将其放在 Cat LRU List 的最前面,再从后往前进行遍历,将引用计数为 0 的项进行删除,从而释放内存。由此可以将部分占用内存大且低频使用的空闲连接,通过 LRU 将其替换出去,使会话内存减少 55.7%。图片
对于分布式系统,除了单点外还要考虑分布式内存管理。当一个应用程序连接到 CN 后,CN 需要跟 DN 进行数据通信。但它与 DN 的通信的 DN 进程需要向本地的 Pooler 申请连接,Pooler 会将本地缓存的部分到 DN 的空闲连接分配给 CN,此后 CN 才能与 DN 进行通信。Pooler 本地缓存了很多到 DN 的空闲连接,实时准备为 CN 提供服务,以至于存在许多空闲连接。对此,我们在 Pooler 内部上线实时检测空闲连接的功能,将大于配置参数的连接释放掉,使得空闲连接占用减少 89.3%。图片
经过优化后,在 2CN、2DN 的情况下,依旧是一个连接在访问一万张表后的内存占用情况,优化前总内存占 51.4M,经优化后只占 5.4M。节点内部 DN 自动释放空闲连接,空闲连接直接减少 89%,经过 RelCache LRU 替换后,CN 连接内存减少 55%。图片
2.3 分布式执行可视化
以下图为例,表 A 和表 B 进行 join,A 表作为分布键,B 表作为非分布键,我们进行 join 时要将 join 的过程下推到 DN 节点执行,但因为表 B 不是分布键 join,所以需要对表 B 按 f2 字段进行重分布。在重分布过程中,DN1 上有一个 backend 进程,需要扫描本地的表 B,把扫描到的部分数据留给本地的 DN 节点,部分数据则重分布到 DN2 数据节点。所以一次重分布中,本地会新创建一个 DN1 的 backend 进程进行数据扫描,还要创建另一个 backend 进程负责发送数据给 DN2。图片
利用分布式可视化的能力,可以查到任意一个用户的 session id,查到 session id 后,可以根据 session id 查到对应的在不同 CN、DN 上的各个进程目前在做什么。比如这个节点我们需要做 hash join,因为需要根据 f2 字段做数据重分布,现在在等待重分布的数据,另外一个进程是扫描表 B,扫描完后会提供给前面的进程,才能进行 hash join,CN1 则是在执行查询事务。通过分布式执行过程的可视化视图,我们可以详细知道一个 SQL 下去各个节点在做什么。图片
下图展示的是分布式可视化功能的使用方法,详情可参考 Github 上的 wiki 地址:
图片
TDSQL PG 版开源重磅升级
TDSQL PG 开源版此次升级 V2.3.0 版本包括四个方面:分区表能力增强、异地多活易用性增强、分布式死锁检测、2PC 残留自动清理。图片
3.1 分区表能力增强
针对分区表能力,本次 TDSQL PG 版升级,整体响应速度更快,有效应对此前业界仍然面临数据量大时读写性能慢等问题。新版本通过分区表功能增强,包括增加 hash 分区类型、支持 default 分区子表创建、分区父表索引操作自动同步子表等,实现分区表便捷管理;同时,新版本实现了分区剪枝性能提升 30%,分布区表关联查询性能(join)提升超 10 倍,完美解决查询效率问题。
具体来说,在原有支持 range 和 list 分区表的基础上,TDSQL PG 版新增 hash 分区表功能。用户在创建时可以指定该分区表为 hash 分区表,再指定分区键,同时需要指定 hash 模数和余数。
TDSQL PG 版还新增了 default 分区。在上一版本中,在创建分区表且创建子分区时,如果没有创建 default 默认分区,用户插入数据时,如果插入不属于指定分区的其他数据,会出现报错。在本次升级后,如果插入的数据不属于其他指定子分区,所有数据会落到 default 分区。比如下图右边的例子,2019 年 12 月和 2020 年 3 月的数据,都不属于指定的其他子分区,因此会落入 default 分区。其优点是不需要报错,同时必要时可以查询不属于指定分区的其他零散值。该功能目前仅支持 range 和 list 分区,不支持 hash 分区。因为在指定 hash 的模数和余数后,数据就必然会落在指定分区,不存在不属于指定分区的情况。图片
TDSQL PG 版也支持分区键更新。在上一版本中,如果要更新创建表时指定的分区键,就会出现报错,不允许更新。新版本对此进行升级。在下图左边的例子中,一个分区子表存储 0 到 30 的数据,另一个分区子表存储 30 到 60 的数据,用户可以对分区键即 id 键进行更新,将 id 从 25 改为 50,数据会自动从原来的分区转移到新的分区。
此外,TDSQL PG 版还支持分区父表建索引自动同步子表。在上一版本中,如果要为分区表建索引,只能给某个指定子表建分区,不能给父表建分区。在新版本中,我们对此进行优化,支持在父表上指定建分区。在下图右边的例子中,假设要给 tbl 表的 id 字段建索引,在创建后它会自动将索引同步到所有子分区上,比如 2020 年 1 月分区、2020 年 2 月分区和 default 分区。用户只需要在父表上执行一条 SQL,它就会自动在所有子分区上全部创建该索引,其优点是方便用户对分区里的索引进行维护。我们也支持创建唯一分区,但需要包含分区键。图片
除了分区能力的增强,TDSQL PG 版还引入 partition-wise join,对原有性能进行提升。假设有两个分区表,分别为 A 表和 B 表,它们的分区数量完全相同,且分区的字段类型一致。如果没有 partition-wise join,在进行 join 时,需要先将 A 表中所有子分区的数据全部捞取,再将 B 表中所有子分区的数据全部捞取,再用 A 表全量数据与 B 表全量数据进行 join,这种操作方式的数据量非常大。在引入 partition-wise join 后,因为两个分区键为同一类型,且分区数量相同,我们可以将 A 表的分区 1 与 B 表的分区 1 进行 join,A 表的分区 2 与 B 表的分区 2 进行 join。我们并不需要将 A 表的全量数据与 B 表的全量数据进行 join,只需要将对等的分区进行 join 即可,从而使性能得到大幅提升。
下图是该功能实际效果的测试对比。在 partition-wise join 打开的情况下,与上一版本相比,最新版本的性能有 10 倍的提升。图片
3.2 异地多活的易用性增强
另一重要升级,是异地多活的易用性增强。原先单活时跨区接入延时大,一旦发生故障服务和数据库都需要切换,流程复杂;而异地多中心接入时延小,业务在一个中心内能完成闭环,秒级即可完成切换。本次升级新增多活插件化功能,方便用户安装配置,提升了产品易用性。
异地多活主要区别于传统两地三中心的单活模式。在单活模式中,假设主库在南方,北方的服务要写入主库,因为地域相隔太远,延时会非常大。如果出现南北网络故障,北方用户就无法访问主库,出现不可用情况。如果采用异地多活模式则不存在上述问题。我们可以在北方多设立一个主库,北方的服务写入北方主库,南方的服务写入南方主库,南北主库之间进行双向复制。其优点在于南北主库各自都有全量数据集,且业务可以就近接入,在一个中心内部实现闭环,即使南北出现网络故障也不会影响服务。图片
异地多活采用双向复制功能,下图是异地多活架构示意图。区域 1 是深圳,区域 2 是上海,我们将区域 1 的数据节点作为发布端,将区域 2 的 CN 节点作为订阅端(CN 可以订阅作为发布端的数据节点上的数据),在完成数据订阅后,CN 需要经过一个路由才能将订阅到的数据写入本地的 DN 节点上,这样我们支持了异构集群之间的同步,一个 CN 可以订阅多个数据节点。图片
在新版本中,我们对易用性进行增强。通过 tbase_subscription 工具,用户可以利用 SQL 直接创建整个同步的异地多活过程。创建时还可以指定订阅的并行度,从而提高订阅效率。此外,因为一个 CN 可以订阅多个 DN,用户可以将订阅任务下发到不同 CN 上,使 CN 进行负载均衡。图片
3.3 分布式死锁自动检测
以下图为例。假设有两个 CN,分别为 CN1 和 CN2。有两个用户,用户 1 连到 CN1,产生一个 session,用户 2 连到 CN2,也产生一个 session。用户 1 使用显示事务查询 A 表数据,用户 2 也采用显示事务查询 B 表数据。用户 1 查询完 A 表数据时,想将 B 表进行删除。因为是分布式系统,CN2 上也存在 B 表,需要同步进行删除。如果要删除 B 表则需要 B 表的排他锁。但这时在 CN2 上,用户 2 刚启动显示事务查询 B 表,获得 B 表的共享锁,这就产生了锁冲突,反之亦然。
在这种情况下,CN1 有 A 表的共享锁,需要等 B 表的排他锁,CN2 有 B 表的共享锁,又要等 A 表的排他锁,两个事务之间就形成了分布式死锁。图片
针对这种情况,我们引入了 pg_unlock 工具。该工具可通过 extension 安装,快速便捷。用户安装后可以查到分布式死锁之间的依赖关系。比如在上面的例子中,CN1 是一个依赖于 CN2 的事务,CN2 是一个依赖于 CN1 的事务。用户还可以查到集群中一个死锁的详细信息。比如 CN1 上的 drop b 需要 B 表的排他锁,CN2 上的 drop a 也需要 A 的排他锁,它们之间都是求而不得存在死锁。我们可以通过执行 pg_unlock execute 来解开死锁,解开的方法是回滚掉一个事务。在回滚事务时,我们会在内部进行优化,计算出回滚代价最小的事务并进行回滚。图片
3.4 2PC 残留自动清理
以下图为例,假设有两个 CN,分别为 CN1、CN2,有两个 DN,分别为 DN1、DN2。用户连进来创建 A 表,再进行 prepare transaction,该事务命名为 create a。我们通过另一个 CN 连接进来,可以查询处于 prepare 状态的事务有哪些,可以看到 CN1 上有一个 prepare 事务叫 create a,CN2、DN1、DN2 也有。
用户继续执行在 DN2 上将 create a 事务进行回滚的操作,此后该操作会话直接退出。退出后我们再去查处于 prepare 状态的事务,发现 CN1、CN2、DN1 各有一个 create a 事务,但 DN2 上没有。因为上述会话在退出前,对 DN2 上的 create a 事务进行回滚,相当于该事务不存在 prepare 状态。由于会话已经退出,该事务永远不会终止,就出现了 2PC 残留的情况。图片
针对这种情况,我们引入了 pg-clean 工具。通过 pg_clean check transaction,用户可以直接查到目前有哪些 2PC 残留事务,以及这些事务的名称、参与的节点等。通过 pg_clean execute,我们可以对 2PC 残留事务进行自动清理。这实际上是将残留事务进行 abort 操作。因为 CN1、CN2、DN1 上的事务都处于 prepare 状态,但 DN2 上执行了 abort,它只能将该事务 abort,不能将其提交,否则会出现部分提交数据不一致的问题。我们能够根据事务状态自动选择回滚或提交,从而将 2PC 残留事务进行清理。图片
在下个版本中,我们计划在以下四个方面对 TDSQL PG 开源版进行加强:存储能力加强。支持透明压缩,减少磁盘存储成本;提供 Direct IO 能力,提高某些情况下的写效率。索引优化。支持全局索引,提升非分布键查询性能。优化器能力提升。提高执行效率,提升业务查询及写的性能。分布式状态可视化。优化全局 session 可视化、query 可视化、分布式锁可视化、分布式事务状态可视化,帮助用户更好地掌控系统。
开源的本质是以技术开放促进技术创新。腾讯云对 TDSQL PG 版不断开发和投入,版本经过大量业务验证,成熟稳定,开源版本和腾讯自用版本共基线,能够帮助用户快速构建核心业务,将持续给客户带来价值,与广大开发者共同打造可持续的国产数据库开源生态。
同时欢迎各位感兴趣的小伙伴添加下方微信进入技术讨论群,与我们一起共同助力国产数据库技术创新与开源生态发展!