- 什么是numa
随着计算机硬件技术的发展,CPU的架构发展从之前的单核发展到多核,而且核数越来越多,
而CPU和内存之间的交互从之前的通过主板的北桥实现通讯,发展到现在的numa架构
Numa架构中CPU之间的通讯是通过QPI(quick path interconnected) 而MCH(memory control hub)集成到了CPU模块中,这样单个CPU访问自己的内存速度最快,而跨CPU访问就需要通过QPI来实现,架构图如下:
这就是一个典型的基于SMP的Numa架构。
CPU NUMA化给服务器带来澎湃算力的同时,也给软件开发带来了很大挑战。从整个IT软件栈来看,首先需要对NUMA化进行支持的是操作系统,现在通用的企业操作系统是Linux操作系统。在NUMA出现后,Linux也提供了针对性的优化方案,优先尝试在请求线程当前所处的CPU的Local内存上分配空间。如果local内存不足,优先淘汰local内存中无用的Page。但Linux提供的NUMA内存使用方式并不适合数据库,因为数据库是一个数据密集型高并发的应用,内部有很多的内核数据结构,这些数据结构既会被本核的CPU访问,也会被远程的CPU核访问。为了提高数据访问性能,数据库还有自己的共享数据缓冲区,这些共享缓冲区是随机的被各个CPU核上的业务线程访问。从IT软件栈来看,数据库是处于企业应用的核心位置,很多应用后台都有一个数据库,数据库的性能决定了很多应用的整体吞吐量。因此如果数据库无法在NUMA下发挥最大性能,实现随着核数的增加,性能呈现一定的线性比,那么CPU NUMA虽然算力很丰富,但可能没有企业愿意买单。
反过来,NUMA作为CPU发展的一种必然趋势,一款企业级的数据库如果不能适应硬件的发展,在企业的数据库选型中,这款数据库也将被淘汰出局。
openGauss作为一款开源关系型数据库管理系统,针对CPU NUMA 化的硬件发展趋势,从并发控制算法,内核数据结构,数据访问等全方位进行了优化,释放处理器多核算力,实现两路鲲鹏128核场景150万tpmC性能。本文深度解读openGauss在鲲鹏服务器上的NUMA多核优化技术,同时也为其他数据库在鲲鹏上进行性能优化提供借鉴和参考。
- Openguass如何实现内核优化
既然numa在访问本地内存性能最后,所以我们尽可能的让cpu只访问本地内存是不是就可以发挥CPU的最大性能
但是原有PG的进程结构首先无法实现该要求,因为一个进程下可能存在多个线程,一个线程才对应cpu的一个处理线程。如果单独绑定一个进程很难实现该进程只在单个CPU上处理相关作业,
2.1 进程改线程
针对这个问题opengauss首先讲PG的进程结构修改为线程结构,同时引入线程访问池的概念,即在数据库初始化的同时相关服务进程就已经预分配好,同时服务进程对应的处理内核也通过参数绑定,这样单个会话连接上数据库后,它对应的服务线程和用户计算的cpu就已经绑定杜绝了CPU的跨核访问。如下图
为了实现CPU核的就近访问,首先需要把线程固定到具体的核上。openGauss有一个GUC配置参数numa_distribute_mode控制CPU绑核分配。通过这个参数,可以把业务处理线程绑定到具体NUMA NODE上。openGauss是一个客户端服务器结构,客户端和服务器通过网络通信来进行交互,网络是一个频繁的操作,为了避免网络中断和业务处理相互干扰,也需要进行网络中断绑核,同时需要考虑网络中断绑核和后台业务线程绑核区分开。
数据库为了实现并发,内部使用了很多锁,比如openGauss中的Clog、WALInsert、WALWrite、ProcArray、XidGen等。这些锁是性能瓶颈点,而锁的本质是保护内核数据结构。所以openGauss需要对这些数据结构进行调整和优化,来应对鲲鹏NUMA架构下多核的并发问题。主要目的有三个:实现CPU的就近访问,去除单点瓶颈,共享数据的内存均匀分配和访问。
2.2WAL写入优化
WALInsertLock用来对WAL Insert操作进行并发保护,可以配置多个,如16。访问行为主要有两种:1)Xlog Insert 时分配一个Insert Lock;2)遍历访问所有WALInsertLock,比如查找是否存在空洞信息,用于XLogFlush等。
在原来的实现方案中,所有的WALInsertLock都在同一个全局数组,存放在共享内存中。WALInsertLock竞争激烈,大概率会涉及到远端访存,即多个线程会进行跨Node、跨P竞争。而实际上Insert Lock有多个实例,大部分操作每次仅需申请一个Insert Lock,可以考虑将申请操作限制在相同的NUMA Node内部。
优化后的方案将全局WALInsertLock数组按照NUMA Node的数目分为多份,分别在对应NUMA Node上申请内存。每个事务线程根据自己所归属的NUMA Node,选择WALInsertLocks子数组。WALInsertLock引用了共享内存中的LWLock,为了最大化减少跨Node竞争,将LWLock直接嵌入到WALInsertLock内部,这样就可以一起进行NUMA分布,同时还减少了一次Cache Line访问。
如下图
2.3 CLOG日志和CSN机制
CLOG日志即事务提交日志,用来记录事务的最终状态,是XLOG日志的辅助,用来加速通过日志判断事务状态的过程,存在四种事务状态,即:IN_PROGRESS、COMMITED、ABORTED、SUB_COMMITED,每条日志占2 bit,CLOG需要存储在磁盘上,一个页面(8k)可包含215条,每个日志文件(段=2048x8k)226条,而日志ID位32位,因此可能存在256个clog文件,CLOG文件在 PGDATA/pg_clog 目录下,为了加速对磁盘文件的访问,对CLOG的访问是通过缓冲池实现的,代码中使用统一的SLRU缓冲池。
优化前CLOG的日志缓冲池在共享内存中且全局唯一,名称为“CLOG Ctl”,各工作线程使用由线程局部变量ClogCtl来指向该资源,在高并发的场景下,该资源的竞争成为性能瓶颈。优化后按照PageNo将日志均分到多个共享内存的缓冲池中,由线程局部对象的数组 ClogCtlData来记录,名称为“CLOG Ctl i”,同步增加共享内存中的缓冲池对象及对应的全局锁。
在将CLOG调整后,opengauss 引入了CSN来提高事务快照的生成效率,提高事务并发。
优化前事务启动获取事务快照,需要获取ProcArrayLock。事务结束清理事务状态快照时,需要获取ProcArrayLock。并发连接增大时导致全局事务管理器上获取的快照变大。
优化后使用事务提交快照,每个非只读事务在运行过程中会取得一个XID,在事务提交时会推进CSN,同时会将当前CSN与事务的XID映射关系保存起来。棕色竖线表示取snapshot时刻,如果不使用CSN方案,那么棕色竖线对应snapshot的集合应该是{2,4,6}。如果采用CSN方案,会获取当前的CSN值,也就是3,事务TX2、TX4、TX6、TX7、TX8的CSN分别为4、6、5、7、8,对于该snapshot而言,这几个事务的修改都不可见。如下图
2.4借助ARM原子指令,减少计算开销
传统编译器的原子操作默认采用ll/sc原子指令,任何核要获取共享变量的写权限,必须先 “排他性”的获取共享全部变量的ownership,即把“最新数据”先Load到本核所在的L1 Cache中,才能进行修改操作,在多CPU的情况下,会由于激烈的竞争导致系统性能下降。
在ARMv8.1引入了一种新的原子操作LSE,将计算操作放到存储端去做,从而提升计算性能。理论上在多核系统下,LSE的性能优于ll/sc, 实测在6.4.0中使用LSE的性能在高并发下是ll/sc的3~5倍。