GaussDB(DWS)stream线程池设计(一)
Stream算子作为SQL join操作时频繁发生的执行算子,负责CN节点GATHER数据,DN节点REDISTRIBUTE和BROACAST数据。大集群高并发场景下,Stream算子过多可能会导致通信的性能瓶颈,引起性能劣化(2000个stream同时启动,进程初始化耗时从ms级劣化到s级),因此需要尽可能减少Stream算子。但是在某些现场环境下,存在数据倾斜、join查询不包含必要分布键等客观情况,Stream算子无法有效减少,为多表join场景下的查询时延保障带来挑战。
stream线程是临时线程,随query启动和退出,负责stream算子的执行,stream线程初始化和退出都会争抢锁等进程级资源,在stream线程个数无法进一步优化的场景下,需要设计有效方案以减少stream线程初始化和退出的时间代价,将进程初始化耗时稳定在ms级,保障数据库的确定性时延查询。
Stream线程未池化时的执行过程可以描述为:线程初始化—>线程任务执行—>线程退出。Stream线程池的核心思想是等stream线程执行完计划任务,保留必要且可复用的线程信息,将线程放入线程池中。线程池化后线程执行过程如下图一所示,可以描述为:步骤一线程信息初始化—>步骤二线程待唤醒后轻量级初始化(query级初始化)—>步骤三线程任务执行—>步骤四线程清理,返回步骤二。当线程等待超时、超出线程池容量(最大stream线程个数)、异常时线程退出。
上图中,池化后线程初始化(thread initialize)包含线程创建、创建相关内存上下文、信号处理函数注册、内存追踪信息初始化、初始化GUC选项等操作;池化后线程轻量级/查询级初始化(light initialize)包含恢复GUC参数、初始化BackendParams、重置GUC参数等操作。
stream线程池采用无锁队列实现,其目的是为了高效管理线程的出/入池操作。定义结构体ThreadSlot保存线程池中每一个线程的信息,包含:线程状态、线程号、线程对应的database oid、线程执行所需的信息StreamProducer、线程唤醒所需的锁和条件变量。
当线程还未被创建时,初始化一定数量的ThreadSlot数量以预留stream线程,这些ThreadSlot被保存在数组threadSlots中。当stream线程执行完毕,需要将stream线程放置到表征可复用线程的无锁队列,称之为idleRing;当线程因为超时、异常等原因不再复用,需要退出时,将stream线程对应的ThreadSlot放置到表征未创建线程的无锁队列,称之为emptyRing。
idleRing的作用是为了快速获取并复用线程池中的线程,emptyRing的作用是快速获取一个未被使用的ThreadSlot结构,以创建一个新的stream线程。由于stream线程的初始化信息和database是强相关的,如果不保留database相关的信息,那么线程初始化的时间代价仍然较高,所以线程池中的线程复用时,需要满足database信息匹配。对于设计线程池而言,每一个database都应该对应一个idleRing。
综上所述,基于无锁队列的stream线程池设计如下所示:
从上图可以看出,一个线程池包含预留stream线程结构的threadSlots、一个表征未创建线程的无锁队列emptyRing和表征可复用线程的无锁队列idleRing,由于每个database对应一个idleRing,因此多个idleRing被组织为链表结构。
本文介绍了GaussDB(DWS)线程池总体设计思想和实现方案,更细节的状态转移和接口将在后续介绍。
想了解GuassDB(DWS)更多信息,欢迎微信搜索“GaussDB DWS”关注微信公众号,和您分享最新最全的PB级数仓黑科技,后台还可获取众多学习资料~
- 点赞
- 收藏
- 关注作者
评论(0)