TiDB 6.5 LTS 版本已经发布了。这是 TiDB V6 的第二个长期支持版,携带了诸多备受期待的新特性:产品易用性进一步提升、内核不断打磨,更加成熟、多样化的灾备能力、加强应用开发者生态构建……
TiDB 6.5 新特性解析系列文章由 PingCAP 产研团队重磅打造,从原理分析、技术实现、和产品体验几个层面展示了 6.5 版本的多项功能优化,旨在帮助读者更简单、更全面的体验 6.5 版本。
本文为系列文章的第一篇,介绍了 TiFlash 在高并发场景下的稳定性和资源利用率的优化原理。
最近的某天,我们测试 TiFlash 在高并发查询场景下的稳定性时,发现 TiFlash 终于可以长时间稳定将 CPU 完全打满,这意味着我们能充分的利用 CPU 资源。回想一年多前,我们还在为高并发查询下的 OOM(out-of memory)、OOT(out-of thread)、CPU 使用率不高等各种问题而绞尽脑汁。是时候来回顾一下我们做了哪些事情,让量变引起质变。
我们都知道,对于分析型的查询来说,有时候一个请求就能将机器的 CPU 资源打满。所以,大部分 OLAP 系统在设计和实现的时候,很少考虑系统在高查询并发下的表现——早期的 TiFlash 也没在这方面考虑太多。
早期的 TiFlash 的资源管理比较初级——没有高效的线程管理机制、缺少健壮的查询任务调度器、每个查询的内存使用没有任何限制、最新写入的数据存储和管理也有较大优化空间。这些优化措施的缺位,导致早期的 TiFlash 在高并发查询场景下表现不佳,经常无法将 CPU 使用率打满,稍有不慎还可能出现 OOM 或 OOT。
过去一年里,针对 TiFlash 在高并发场景下的稳定性和资源利用率这个问题,我们在存储和计算上都做了不少尝试和努力。如今回头看,有了上面的结果,也算是达到了一个小里程碑。
TiFlash 最开始的线程管理非常简单粗暴:请求到来时,按需创建新线程;请求结束之后,自动销毁线程。在这种模式下,我们发现:对于一些逻辑比较复杂,但是数据量不大的查询,无论怎么增加查询的并发,TiFlash 的整机 CPU 使用率都远远不能打满。
经过一系列的研究之后,我们终于定位到问题的根本原因:高并发下,TiFlash 会频繁地创建线程和释放线程。在 Linux 内核中,线程在创建和释放的时候,都会抢同一把全局互斥锁,从而在高并发线程创建和释放时, 这些线程会发生排队、阻塞的现象,进而导致应用的计算工作也被阻塞,而且并发越多,这个问题越严重,所以 CPU 使用率不会随着并发增加而增加。具体分析可以参考文章:深入解析 TiFlash丨多并发下线程创建、释放的阻塞问题。
解决这个问题的直接思路是使用线程池,减少线程创建和释放的频率。但是,我们目前的查询任务使用线程的模式是非抢占的,对于固定大小的线程池,由于系统中没有全局的调度器,会有死锁的风险。为此,我们引入了 DynamicThreadPool 这一特性。
在 DynamicThreadPool 中,线程分为两类:
每当有新任务需要执行时,DynamicThreadPool 会按以下顺序查找可用线程:
所有空闲的动态线程会组成一个 LIFO 的链表,每个动态线程在处理完一个任务后都会将自身插入到链表头部,这样下次调度时会被优先使用,从而达到尽可能复用最近使用过的动态线程的目的。链表尾部的动态线程会在超过一个时间阈值没有收到新任务之后判断自身已冷却,自行退出。
由于 DynamicThreadPool 没有限制线程的数量,在遇到高并发查询时,TiFlash 仍然有可能会遇到无法分配出线程(OOT)的问题。为了解决此问题,我们必须控制 TiFlash 中同时使用的线程数量。
为了控制同时使用的计算线程数量,同时避免死锁,我们为 TiFlash 引入了名为 MinTSOScheduler 的查询任务调度器——一个完全分布式的调度器,它仅依赖 TiFlash 节点自身的信息。
MinTSOScheduler 的基本原理是:保证 TiFlash 节点上最小的 start_ts 对应的所有 MPPTask 能正常运行。因为全局最小的 start_ts 在各个节点上必然也是最小的 start_ts,所以 MinTSOScheduer 能够保证全局至少有一个查询能顺利运行从而保证整个系统不会有死锁。而对于非最小 start_ts 的 MPPTask,则根据当前系统的线程使用情况来决定是否可以运行,所以也能达到控制系统线程使用量的目的。
DynamicThreadPool 和 MinTSOScheduler 基本上解决了线程频繁创建和销毁、线程使用数量不受控制两大问题。对于一个运行高并发查询的环境,还有一个重要的问题要解决——减少查询之间的相互干扰。
实践中,我们发现最重要的一点就是要避免其中某一个查询忽然消耗掉大量内存,导致整个节点 OOM。为了避免某个大查询导致的 OOM,我们显著增强了 MemoryTracker 跟踪和记录每个 MPPTask 使用的内存的精确度。当内存使用超过限制时,可以强行中止请求,避免 OOM 影响其它请求。
PageStorage 是 TiFlash 中的一个存储的抽象层,类似对象存储。它主要是为了存储一些较小的数据块,如最新数据和 TiFlash 存储引擎的元数据。所以,PageStorage 主要面向新写入数据的高频读写设计。v6.1 及之前 TiFlash 使用的是 PageStorage 的 v2 版本(简称 PSv2)。
经过一系列的迭代和业务打磨,我们发现 PSv2 存在一些问题亟需改进:
这些问题在大部分写入和查询并发较低的 OLAP 场景下,对系统的影响有限。但是,TiFlash 作为 TiDB 的 HTAP 架构中重要的一环,经常需要面对高并发的写入和查询。为此,我们重新设计并实现了全新的 PageStorage (简称 PSv3)以应对更严苛的 HTAP 负载需求。
上图是 PSv3 的整体架构。其中,橙色块代表接口,绿色块代表在硬盘上存储的组件,蓝色块代表在内存中的组件。
DynamicThreadPool | MinTSOScheduler | PageStorageV3 | CPU 最大使用率 |
---|---|---|---|
enable | enable | enable | 100% |
disable | disable | enbale | 75% |
enable | disable | enable | 90% |
enable | enable | disable | 75% |
disable | enable | enable | 85% |
上面这个表格总结了本文介绍的这几个提升 TiFlash 稳定性和 CPU 使用率的关键特性的组合情况,可以看出:
除此之外,过去一年,TiFlash 在性能和功能方面也做了不少优化,感兴趣的朋友可以关注我们的 github 代码和官方文档。以上全部改动可以在 TiDB v6.5 LTS 版本中体验到,欢迎尝试。