队长: 严炜琦
队员:许张贤、付哲
项目地址: https://github.com/ywqdev/tiflash/tree/fiberpunkhackthon
Fiber is all you need for TiFlash. 使用有栈协程 Boost::Fiber 构建协程池改造 TiFlash 线程执行模型,减少线程滥用,提升稳定性,增强查询性能。
TiFlash 是一个实现了 MPP 查询的计算加速器。 TiFlash 执行器当前的多线程模型继承了早期版本的 Clickhouse,实现较为粗犷。 其线程模型如下: 针对于 MPP 查询中划分的每一个任务(MPPTask) 都会独立的创建一组线程来进行查询执行。利用 DynamicThreadPool 让 MPPTask 之间复用线程来降低线程创建和销毁的开销参考 深入解析 TiFlash丨多并发下线程创建、释放的阻塞问题)。 然而 DynamicThreadPool 不同于固定大小的线程池,在其线程资源不足时会额外分配线程以避免死锁问题。因此在高并发场景下 DynamicThreadPool 无法控制住系统计算所用的线程上限,线程将会毫无限制地进行分配。 在 TiFlash 现有的线程模型下,存在如下问题:
- 高并发场景存在严重问题。当线程数过多时将构造过多线程,导致查询失败。
- 在复杂查询下,存在阻塞算子(比如 join build),导致线程资源利用率不够高。
- TiFlash 使用 Volcano(pull based) 执行模型,在执行过程中存在大量阻塞,此时阻塞的任务将会占据线程池中线程资源,导致其他任务无法复用线程池内资源,线程资源利用率无法打满。
- 使用线程池可能导致 Task 的所有 job 同时运行,存在死锁风险。
作为 TiDB 的查询加速器,TiFlash 需要更高更快更强更稳定,其中查询执行器需要有效利用 OS 线程资源,榨干硬件所有性能,同时避免线程的过量使用以至于超越 OS 上限。
使用协程
对 TiFlash 现有线程模型进行改造可以有效解决上述问题,仅需最小限度地修改 TiFlash 线程模型即可控制住 TiFlash 在高并发场景下的线程数。
协程的优势:
- 线程间的切换需要从用户态到内核态再回到用户态,这个过程会产生大量额外的开销,例如上下文切换,和内核的调度系统。我们可以通过选择使用用户态的线程来避免这部分的开销。
- 线程的频繁创建和销毁也会带来大量的开销,如果只是绑定固定数量的线程,并且通过协程合理使用它们,可以减小这部分开销。
- 使用协程能够将阻塞操作异步化,执行算子中的阻塞协程将被自动挂起,进入等待唤醒状态。而物理线程则开始调度下一个可执行协程。
我们将得到一个:
控制线程滥用
,稳定性更强性能更好
的 TiFlash 分支。
有栈协程or无栈协程?
有栈协程优势:
兼容性高,不需要给所有同步代码加上 async/await,改进现有线程模型时改动量小。
无栈协程优势:
相比于有栈协程,不需要把局部变量开辟在新开的空间上,直接使用系统栈,内存开销小,切换成本小。
考虑实现难度,且 AP 场景下线程切换较少,选择有栈协程库 Boost::fiber。
利用 Boost
携带的协程库 Fiber 实现一个协程池 FiberPool
。该协程池将申请固定个数的线程资源,确保运行时线程资源个数不变。
TiFlash 计算层中所有申请计算资源的任务均从 FiberPool 中申请资源。
TiFlash 在算子间插入了一些算子来控制 Volcano 模型执行期不同阶段的并发,这些算子包括 UnionBlockInputStream
, SharedQueryBlockInputStream
, CreatingSetBlockInputStream
, ParallelAggregatingBlockInputStream
。
由于这些算子均需要申请线程进行执行,故而需要对其申请线程的流程进行修改,上述算子的功能分别如下:
UnionBlockInputStream
: 将多个 Streams 的输出结果汇聚到一个 stream 中,利用了 ParallelInputsProcessor 将结果放入 output_queue 中。SharedQueryBlockInputStream
: 将一个 stream 输出到多个 streams 中。原有实现利用 ThreadManager 申请线程资源。CreatingSetBlockInputStream
: 接受一个数据 InputStream 和代表 JoinBuild 的若干个 Subquery,并发启动这些 Subquery(利用 ThreadManager 申请线程资源),并等待它们执行结束之后再开始启动数据 InputStream。ParallelAggregatingBlockInputStream
: 聚合,将多个 stream 并发进行聚合。 其中 partial 阶段将数据划分为 N 份并发进行 HashTable 的构建。Merge 阶段将 N 个 HashTable 合并,输出单个 stream。其中并发执行利用了 ParallelInputsProcessor。
其中 ParallelInputsProcessor
提供并发执行多个 BlockInputStream 的能力。
需要修改上述算子中的线程相关代码,包括线程池和并发元语,替换为 Fiber 相关代码,最终完成 Fiber 对原有线程模型的替换。
牛啊!