He3DB多NUMA架构中同步复制性能优化

0 阅读4分钟

He3DB多NUMA架构中同步复制性能优化

在现代企业级数据库部署场景中,服务器配置逐渐走向高核数、大内存、多 NUMA(Non-Uniform Memory Access)架构。对于 大云海山He3DB数据库来说,同步复制(Synchronous Replication)(本文关注的是同步复制的提交路径,即主节点提交事务时,必须等待 WAL 成功写入到从库的磁盘(flush/fsync),并收到从库确认(ACK)后才返回成功。与从库的replay性能无关。)是保障数据可靠性的关键机制,但它也极易成为性能瓶颈。因为,同步复制是一条极依赖共享内存访问时延的链路,而 NUMA 架构放大了这条链路的热点。

因此,如何在多 NUMA 节点的硬件环境下优化同步复制性能,已经成为高并发写入场景中最重要的调优方向之一。本文将从 NUMA 架构的硬件特性出发,深入分析同步复制在多 NUMA 场景下的瓶颈来源,并结合实践经验给出可落地的优化方案与内核级改进思路。

多NUMA的最大特点就是,同NUMA节点访问延迟低、跨NUMA节点访问延迟显著较高。

NUMA 的本质问题

多 NUMA 的关键特征是:跨节点访问内存延迟变高。同一块数据若不在当前 CPU 所属的内存节点,会导致:

  • 远端节点内存访问延迟明显增加
  • 多核争抢导致的 Cache Line 来回漂移

而数据库内核对共享内存区域(Shmem)和 WAL buffer 的访问极其频繁,因此 NUMA 会被无限放大。

同步复制的关键路径

同步复制在主库的关键路径为:

  1. 处理SQL请求,事务提交(commit)
  2. 写 WAL → 刷盘(fsync)
  3. WAL Sender 发送
  4. Standby 接收 WAL → 写磁盘
  5. Standby 回 ACK
  6. 主节点释放事务

其中,在第1、2、3步中,具有高度共享内存交互(获取LWLock,如 WALInsertLock, ProcArrayLock, SyncRepLock),最容易被NUMA放大。

本文仅针对主从同步复制带来的影响,进行深入分析。当处理简单SQL时,同步复制带来的影响最大,最容易观察。因此,我们通过jmeter压测工具,高并发(800并发)压测简单SQL(insert一条记录,并update该记录的内容),来观察数据库的状态。

在压测期间,我们使用perf工具,统计CPU热点,并生成热力图:

从上图中,我们可以发现,SyncRepWaitForLSN函数调用较高,约21%。

接下来,针对主从同步逻辑进行分析,并尝试对瓶颈进行优化。

主从同步复制流程

/Users/shipx/Library/Containers/com.kingsoft.wpsoffice.mac/Data/tmp/wpsoffice.lqkoWjwpsoffice

  1. 主库生成commit日志,并持久化到磁盘;
  2. 日志持久化到磁盘后,唤醒WAL Sender进程发送WAL日志;
  3. 获取SyncRepLock的排他锁,将当前进程插入到SyncRepQueue链表中(按照等待的lsn进行排序),并等待WAL Sender进程唤醒;
  4. WAL Sender进程从本地读取待发送的WAL数据;
  5. WAL Sender进程把待发送的WAL日志发送给从库,并更新sendPtr值;
  6. 从库的WAL Receiver进程收到WAL日志后,持久化到磁盘中。
  7. 从库的WAL Receiver进程将从库的flushLSN、applyLSN等信息返回给主库;
  8. 主库的WAL Sender进程收到从库的flushLSN后,获取SyncRepLock的排他锁,从SyncRepQueue链表中移除小于该LSN的backend信息,并唤醒该backend。
  9. 主库的backend被唤醒后,则反馈客户端事务已提交成功。

【注】在该流程中,仅介绍一主一从同步的情况,对于其他更复杂的同步复制方式(基于优先级),原理也大体相似,本文不再赘述。

主从同步复制优化

基于上述流程,可以发现在每次事务提交时,都需要对SyncRepLock加排他锁。在高并发压测,尤其是在多NUMA架构中,backend对该锁的争抢极为严重。基于该锁,可以实现backend被动唤醒,避免backend等待期间对资源的消耗。而该锁的作用主要包括:

  1. backend插入SyncRepQueue中可以保证链表的有序;
  2. WAL Sender更新lsn时,可以一次性唤醒所有满足条件的后台进程。

因此,为了避免对SyncRepLock锁的争抢,考虑该锁去掉,改为backend主动去判断,定期与从库发送的LSN进行比较,当大于等于该LSN值时即可返回给客户端事务已提交。

通过这种方式去判断主从是否同步,可以很好的避免对该锁的争抢,而带来的代价就是在backend等待期间,需要定期去检查从库发送的flushLSN,一是会额外带来资源消耗,二是不能实时感知到当前backend已满足同步条件。而这两者本身就会有冲突,如何很好的平衡这两者,是一个需要持续优化的点。

优化完主备同步逻辑后,再次进行压测:

可以看到,SyncRepWaitForLsn函数的占比已经很低了,并且整体TPS值提升了1倍多。