引言
ClickHouse 内核剖析系列文章,本文将为大家深度解读 ClickHouse 以后的 MPP 计算模型、用户资源隔离、查问限流机制,在此基础上为大家介绍阿里巴巴云数据库 ClickHouse 在八月份行将推出的自研弹性资源队列性能。ClickHouse 开源版本以后还没有资源队列相干的布局,自研弹性资源队列的初衷是更好地解决隔离和资源利用率的问题。下文将从 ClickHouse 的 MPP 计算模型、现有的资源隔离计划开展来看 ClickHouse 以后在资源隔离上的痛点,最初为大家介绍咱们的自研弹性资源队列性能。
MPP 计算模型
在深刻到资源隔离之前,这里有必要简略介绍一下 ClickHouse 社区纯自研的 MPP 计算模型,因为 ClickHouse 的 MPP 计算模型和成熟的开源 MPP 计算引擎(例如:Presto、HAWQ、Impala)存在着较大的差别(que xian),这使得 ClickHouse 的资源隔离也有一些独特的要求,同时心愿这部分内容能领导用户更好地对 ClickHouse 查问进行调优。
ClickHouse 的 MPP 计算模型最大的特点是:它压根没有分布式执行打算,只能通过递归子查问和播送表来解决多表关联查问,这给分布式多表关联查问带来的问题是数据 shuffle 爆炸。另外 ClickHouse 的执行打算生成过程中,仅有一些简略的 filter push down,column prune 规定,齐全没有 join reorder 能力。对用户来说就是 ” 所写即所得 ” 的模式,要求人人都是 DBA,上面将联合简略的查问例子来介绍一下 ClickHouse 计算模型最大的几个准则。
递归子查问
在浏览源码的过程中,我能够感触到 ClickHouse 后期是一个齐全受母公司 Yandex 搜寻剖析业务驱动成长起来的数据库。而搜寻业务场景下的 Metric 剖析(uv / pv …),对分布式多表关联剖析的并没有很高的需要,绝大部分业务场景都能够通过简略的数据分表剖析而后聚合后果(数据建模比较简单),所以从一开始 ClickHouse 就注定不善于解决简单的分布式多表关联查问,ClickHouse 的内核只是把单机(单表)剖析做到了性能极致。然而任何一个业务场景下都不能完全避免分布式关联剖析的需要,ClickHouse 采纳了一套简略的 Rule 来解决多表关联剖析的查问。
对 ClickHouse 有所理解的同学应该晓得 ClickHouse 采纳的是简略的节点对等架构,同时不提供任何分布式的语义保障,ClickHouse 的节点中存在着两种类型的表:本地表(实在存放数据的表引擎),分布式表(代理了多个节点上的本地表,相当于 ” 分库分表 ” 的 Proxy)。当 ClickHouse 的节点收到两表的 Join 关联剖析时,问题比拟收敛,无非是以下几种状况:本地表 Join 分布式表、本地表 Join 本地表、分布式表 Join 分布式表、分布式表 Join 本地表,这四种状况会如何执行这里先放一下,等下一大节再介绍。
接下来问题复杂化,如何解决多个 Join 的关联查问?ClickHouse 采纳递归子查问来解决这个问题,如上面的简略例子所示 ClickHouse 会主动把多个 Join 的关联查问改写成子查问进行嵌套, 规定非常简单:1)Join 的左右表必须是本地表、分布式表或者子查问;2)偏向把 Join 的左侧变成子查问;3)从最初一个 Join 开始递归改写成子查问;4)对 Join order 不做任何改变;5)能够主动依据 where 条件改写 Cross Join 到 Inner Join。上面是两个具体的例子帮忙大家了解:
例 1
select from local_tabA join (select from dist_tabB join local_tabC on dist_tabB.key2 = local_tabC.key2) as sub_Q1 on local_tabA.key1 = sub_Q1.key1 join dist_tabD on local_tabA.key1 = dist_tabD.key1; =============> select from (select from local_tabA join (select * from dist_tabB join local_tabC on dist_tabB.key2 = local_tabC.key2) as sub_Q1 on local_tabA.key1 = sub_Q1.key1) as sub_Q2 join dist_tabD on sub_Q2.key1 = dist_tabD.key1;
例 2
select from local_tabA join (select from dist_tabB join local_tabC on dist_tabB.key2 = local_tabC.key2) as sub_Q1 on local_tabA.key1 = sub_Q1.key1 join dist_tabD on local_tabA.key1 = dist_tabD.key1; =============> select from (select from local_tabA join (select * from dist_tabB join local_tabC on dist_tabB.key2 = local_tabC.key2) as sub_Q1 on local_tabA.key1 = sub_Q1.key1) as sub_Q2 join dist_tabD on sub_Q2.key1 = dist_tabD.key1;
Join 关联中的子查问在计算引擎里就相干于是一个本地的 ” 长期表 ”,只不过这个长期表的 Input Stream 对接的是一个子查问的 Output Stream。所以在解决多个 Join 的关联查问时,ClickHouse 会把查问拆成递归的子查问,每一次递归只解决一个 Join 关联,单个 Join 关联中,左右表输出有可能是本地表、分布式表、子查问,这样问题就简化了。
这种简略的递归子查问解决方案纯在最致命的缺点是:
(1)零碎没有主动优化能力,Join reorder 是优化器的重要课题,然而 ClickHouse 齐全不提供这个能力,对内核不够理解的用户根本无奈写出性能最佳的关联查问,然而对教训老道的工程师来说这是另一种体验:能够齐全掌控 SQL 的执行打算。
(2)无奈齐全施展分布式计算的能力,ClickHouse 在两表的 Join 关联中是否利用分布式算力进行 join 计算取决于左表是否是分布式表,只有当左表是分布式表时才有可能利用上 Cluster 的计算能力,也就是左表是本地表或者子查问时 Join 计算过程只在一个节点进行。
(3)多个大表的 Join 关联容易引起节点的 OOM,ClickHouse 中的 Hash Join 算子目前不反对 spill(落盘),递归子查问须要节点在内存中同时保护多个残缺的 Hash Table 来实现最初的 Join 关联。
两表 Join 规定
上一节介绍了 ClickHouse 如何利用递归子查问来解决多个 Join 的关联剖析,最终零碎只会 focus 在单个 Join 的关联剖析上。除了惯例的 Join 形式修饰词以外,ClickHouse 还引入了另外一个 Join 流程修饰词 ”Global”,它会影响整个 Join 的执行打算。节点真正采纳 Global Join 进行关联的前提条件是左表必须是分布式表,Global Join 会构建一个内存长期表来保留 Join 右测的数据,而后把左表的 Join 计算工作分发给所有代理的存储节点,收到 Join 计算工作的存储节点会跨节点拷贝内存长期表的数据,用以构建 Hash Table。
上面顺次介绍所有可能呈现的单个 Join 关联剖析场景:
(1)(本地表 / 子查问)Join(本地表 / 子查问):惯例本地 Join,Global Join 不失效
(2)(本地表 / 子查问)Join(分布式表):分布式表数据全副读到以后节点进行 Hash Table 构建,Global Join 不失效
(3)(分布式表)Join(本地表 / 子查问):Join 计算工作散发到分布式表的所有存储节点上,存储节点上收到的 Join 右表取决于是否采纳 Global Join 策略,如果不是 Global Join 则把右测的(本地表名 / 子查问)间接转给所有存储节点。如果是 Global Join 则以后节点会构建 Join 右测数据的内存表,收到 Join 计算工作的节点会来拉取这个内存表数据。
(4)(分布式表)Join(分布式表):Join 计算工作散发到分布式表的所有存储节点上,存储节点上收到的 Join 右表取决于是否采纳 Global Join 策略,如果不是 Global Join 则把右测的分布式表名间接转给所有存储节点。如果是 Global Join 则以后节点会把右测分布式表的数据全副收集起来构建内存表,收到 Join 计算工作的节点会来拉取这个内存表数据。
从下面能够看出只有分布式表的 Join 关联是能够进行分布式计算的,Global Join 能够提前计算 Join 右测的后果数据构建内存表,当 Join 右测是带过滤条件的分布式表或者子查问时,升高了 Join 右测数据反复计算的次数,还有一种场景是 Join 右表只在以后节点存在则此时必须应用 Global Join 把它替换成内存长期表,因为间接把右表名转给其余节点肯定会报错。
ClickHouse 中还有一个开关和 Join 关联剖析的行为无关:distributed_product_mode,它只是一个简略的查问改写 Rule 用来改写两个分布式表的 Join 行为。当 set distributed_product_mode = ‘LOCAL’ 时,它会把右表改写成代理的存储表名,这要求左右表的数据分区对齐,否则 Join 后果就出错了,当 set distributed_product_mode = ‘GLOBAL’ 时,它会把主动改写 Join 到 Global Join。然而这个改写 Rule 只针对左右表都是分布式表的 case,简单的多表关联剖析场景下对 SQL 的优化作用比拟小,还是不要去依赖这个主动改写的能力。
ClickHouse 的分布式 Join 关联剖析中还有另外一个特点是它并不会对左表的数据进行 re-sharding,每一个收到 Join 工作的节点都会要全量的右表数据来构建 Hash Table。在一些场景下,如果用户确定 Join 左右表的数据是都是依照某个 Join key 分区的,则能够应用(分布式表)Join(本地表)的形式来缓解一下这个问题。然而 ClickHouse 的分布式表 Sharding 设计并不保障 Cluster 在调整节点后数据能齐全分区对齐,这是用户须要留神的。
小结
总结一下下面两节的剖析,ClickHouse 以后的 MPP 计算模型并不善于做多表关联剖析,次要存在的问题:1)节点间数据 shuffle 收缩,Join 关联时没有数据 re-sharding 能力,每个计算节点都须要 shuffle 全量右表数据;2)Join 内存收缩,起因同上;3)非 Global Join 下可能引起计算风暴,计算节点反复执行子查问;4)没有 Join reorder 优化。其中的 1 和 3 还会随着节点数量增长变得更加显著。在多表关联剖析的场景下,用户应该尽可能为小表构建 Dictionary,并应用 dictGet 内置函数来代替 Join,针对无奈防止的多表关联剖析应该间接写成嵌套子查问的形式,并依据实在的查问执行状况尝试调整 Join order 寻找最优的执行打算。以后 ClickHouse 的 MPP 计算模型下,依然存在不少查问优化的小 ”bug” 可能导致性能不如预期,例如列裁剪没有下推,过滤条件没有下推,partial agg 没有下推等等,不过这些小问题都是能够修复。
资源隔离现状
以后的 ClickHouse 开源版本在零碎的资源管理方面曾经做了很多的 feature,我把它们总结为三个方面:全链路(线程 -》查问 -》用户)的资源应用追踪、查问 & 用户级别资源隔离、资源应用限流。对于 ClickHouse 的资深 DBA 来说,这些资源追踪、隔离、限流性能曾经能够解决十分多的问题。接下来我将开展介绍一下 ClickHouse 在这三个方面的功能设计实现。
trace & profile
ClickHouse 的资源应用都是从查问 thread 级别就开始进行追踪,次要的相干代码在 ThreadStatus 类中。每个查问线程都会有一个 thread local 的 ThreadStatus 对象,ThreadStatus 对象中蕴含了对内存应用追踪的 MemoryTracker、profile cpu time 的埋点对象 ProfileEvents、以及监控 thread 热点线程栈的 QueryProfiler。
1.MemoryTracker
ClickHouse 中有很多不同 level 的 MemoryTracker,包含线程级别、查问级别、用户级别、server 级别,这些 MemoryTracker 会通过 parent 指针组织成一个树形构造,把内存申请开释信息层层反馈下来。
MemoryTrack 中还有额定的峰值信息(peak)统计,内存下限查看,一旦某个查问线程的申请内存申请在下层(查问级别、用户级别、server 级别)MemoryTracker 遇到超过限度谬误,查问线程就会抛出 OOM 异样导致查问退出。同时查问线程的 MemoryTracker 每申请一定量的内存都会统计出以后的工作栈,十分不便排查内存 OOM 的起因。
ClickHouse 的 MPP 计算引擎中每个查问的主线程都会有一个 ThreadGroup 对象,每个 MPP 引擎 worker 线程在启动时必须要 attach 到 ThreadGroup 上,在线程退出时 detach,这保障了整个资源追踪链路的残缺传递。最初一个问题是如何把 CurrentThread::MemoryTracker hook 到零碎的内存申请开释下来?ClickHouse 首先是重载了 c ++ 的 new_delete operator,其次针对须要应用 malloc 的一些场景封装了非凡的 Allocator 同步内存申请开释。为了解决内存追踪的性能问题,每个线程的内存申请开释会在 thread local 变量上进行积攒,最初以大块内存的模式同步给 MemoryTracker。
class MemoryTracker {std::atomic<Int64> amount {0}; std::atomic<Int64> peak {0}; std::atomic<Int64> hard_limit {0}; std::atomic<Int64> profiler_limit {0}; Int64 profiler_step = 0; /// Singly-linked list. All information will be passed to subsequent memory trackers also (it allows to implement trackers hierarchy). /// In terms of tree nodes it is the list of parents. Lifetime of these trackers should “include” lifetime of current tracker. std::atomic<MemoryTracker *> parent {}; /// You could specify custom metric to track memory usage. CurrentMetrics::Metric metric = CurrentMetrics::end(); … }
2.ProfileEvents:
ProfileEvents 顾名思义,是监控零碎的 profile 信息,笼罩的信息十分广,所有信息都是通过代码埋点进行收集统计。它的追踪链路和 MemoryTracker 一样,也是通过树状构造组织层层追踪。其中和 cpu time 相干的外围指标包含以下:
///Total (wall clock) time spent in processing thread. RealTimeMicroseconds; ///Total time spent in processing thread executing CPU instructions in user space. ///This include time CPU pipeline was stalled due to cache misses, branch mispredictions, hyper-threading, etc. UserTimeMicroseconds; ///Total time spent in processing thread executing CPU instructions in OS kernel space. ///This include time CPU pipeline was stalled due to cache misses, branch mispredictions, hyper-threading, etc. SystemTimeMicroseconds; SoftPageFaults; HardPageFaults; ///Total time a thread spent waiting for a result of IO operation, from the OS point of view. ///This is real IO that doesn’t include page cache. OSIOWaitMicroseconds; ///Total time a thread was ready for execution but waiting to be scheduled by OS, from the OS point of view. OSCPUWaitMicroseconds; ///CPU time spent seen by OS. Does not include involuntary waits due to virtualization. OSCPUVirtualTimeMicroseconds; ///Number of bytes read from disks or block devices. ///Doesn’t include bytes read from page cache. May include excessive data due to block size, readahead, etc. OSReadBytes; ///Number of bytes written to disks or block devices. ///Doesn’t include bytes that are in page cache dirty pages. May not include data that was written by OS asynchronously OSWriteBytes; ///Number of bytes read from filesystem, including page cache OSReadChars; ///Number of bytes written to filesystem, including page cache OSWriteChars;
以上这些信息都是从 linux 零碎中间接采集,参考 sys/resource.h 和 linux/taskstats.h。采集没有固定的频率,零碎在查问计算的过程中每解决完一个 Block 的数据就会根据间隔上次采集的工夫距离决定是否采集最新数据。
3.QueryProfiler:
QueryProfiler 的外围性能是抓取查问线程的热点栈,ClickHouse 通过对线程设置 timer_create 和自定义的 signal_handler 让 worker 线程定时收到 SIGUSR 信号量记录本人以后所处的栈,这种办法是能够抓到所有被 lock block 或者 sleep 的线程栈的。
除了以上三种线程级别的 trace&profile 机制,ClickHouse 还有一套 server 级别的 Metrics 统计,也是通过代码埋点记录零碎中所有 Metrics 的瞬时值。ClickHouse 底层的这套 trace&profile 伎俩保障了用户能够很不便地从零碎硬件层面去定位查问的性能瓶颈点或者 OOM 起因,所有的 metrics, trace, profile 信息都有对象的 system_log 零碎表能够追溯历史。
资源隔离
资源隔离须要关注的点包含内存、CPU、IO,目前 ClickHouse 在这三个方面都做了不同水平性能:
1. 内存隔离
以后用户能够通过 max_memory_usage(查问内存限度),max_memory_usage_for_user(用户的内存限度),max_memory_usage_for_all_queries(server 的内存限度),max_concurrent_queries_for_user(用户并发限度),max_concurrent_queries(server 并发限度)这一套参数去规划系统的内存资源应用做到用户级别的隔离。然而当用户进行多表关联剖析时,零碎派发的子查问会冲破用户的资源布局,所有的子查问都属于 default
用户,可能引起用户查问的内存超用。
2.CPU 隔离
ClickHouse 提供了 Query 级别的 CPU 优先级设置,当然也能够为不同用户的查问设置不同的优先级,有以下两种优先级参数:
///Priority of the query. ///1 – higher value – lower priority; 0 – do not use priorities. ///Allows to freeze query execution if at least one query of higher priority is executed. priority; ///If non zero – set corresponding ‘nice’ value for query processing threads. ///Can be used to adjust query priority for OS scheduler. os_thread_priority;
3.IO 隔离
ClickHouse 目前在 IO 上没有做任何隔离限度,然而针对异步 merge 和查问都做了各自的 IO 限度,尽量避免 IO 打满。随着异步 merge task 数量增多,零碎会开始限度后续单个 merge task 波及到的 Data Parts 的 disk size。在查问并行读取 MergeTree data 的时候,零碎也会统计每个线程以后的 IO 吞吐,如果吞吐不达标则会反压读取线程,升高读取线程数缓解零碎的 IO 压力,以上这些限度措施都是从部分来缓解问题的一个伎俩。
Quota 限流
除了动态的资源隔离限度,ClickHouse 外部还有一套时序资源应用限流机制 –Quota。用户能够依据查问的用户或者 Client IP 对查问进行分组限流。限流和资源隔离不同,它是束缚查问执行的 ” 速率 ”,以后次要包含以下几种 ” 速率 ”:
QUERIES; /// Number of queries. ERRORS; /// Number of queries with exceptions. RESULT_ROWS; /// Number of rows returned as result. RESULT_BYTES; /// Number of bytes returned as result. READ_ROWS; /// Number of rows read from tables. READ_BYTES; /// Number of bytes read from tables. EXECUTION_TIME; /// Total amount of query execution time in nanoseconds.
用户能够自定义布局本人的限流策略,避免零碎的负载(IO、网络、CPU)被打爆,Quota 限流能够认为是零碎自我爱护的伎俩。零碎会依据查问的用户名、IP 地址或者 Quota Key Hint 来为查问绑定对应的限流策略。计算引擎在算子之间传递 Block 时会查看以后 Quota 组内的流速是否过载,进而通过 sleep 查问线程来升高零碎负载。
小结
总结一下 ClickHouse 在资源隔离 /trace 层面的优缺点:ClickHouse 为用户提供了十分多的工具组件,然而欠缺整体性的解决方案。以 trace & profile 为例,ClickHouse 在本身零碎里集成了十分欠缺的 trace / profile / metrics 日志和刹时状态零碎表,在排查性能问题的过程中它的链路是齐备的。但问题是这个链路太简单了,对个别用户来说排查十分艰难,尤其是碰上递归子查问的多表关联剖析时,须要从用户查问到一层子查问到二层子查问步步深入分析。以后的资源隔离计划出现给用户的更加是一堆配置,基本不是一个残缺的性能。Quota 限流尽管是一个残缺的性能,然而却不容易应用,因为用户不晓得如何量化正当的 ” 速率 ”。
弹性资源队列
第一章为大家介绍了 ClickHouse 的 MPP 计算模型,外围想论述的点是 ClickHouse 这种简略的递归子查问计算模型在资源利用上是十分粗犷的,如果没有很好的资源隔离和零碎过载爱护,节点很容易就会因为 bad sql 变得不稳固。第二章介绍 ClickHouse 以后的资源应用 trace profile 性能、资源隔离性能、Quota 过载爱护。然而 ClickHouse 目前在这三个方面做得都不够完满,还须要深度打磨来晋升零碎的稳定性和资源利用率。我认为次要从三个方面进行增强:性能诊断链路自动化使用户能够一键诊断,资源队列性能增强,Quota(负载限流)做成自动化并拉通来看查问、写入、异步 merge 工作对系统的负载,避免过载。
阿里云数据库 ClickHouse 在 ClickHouse 开源版本上行将推出用户自定义的弹性资源队列性能,资源队列 DDL 定义如下:
CREATE RESOURCE QUEUE [IF NOT EXISTS | OR REPLACE] test_queue [ON CLUSTER cluster] memory=10240000000, /// 资源队列的总内存限度 concurrency=8, /// 资源队列的查问并发管制 isolate=0, /// 资源队列的内存抢占隔离级别 priority=high /// 资源队列的 cpu 优先级和内存抢占优先级 TO {role [,…] | ALL | ALL EXCEPT role [,…]};
我认为资源队列的外围问题是要在保障用户查问稳定性的根底上最大化零碎的资源利用率和查问吞吐。传统的 MPP 数据库相似 GreenPlum 的资源队列设计思维是队列之间的内存资源齐全隔离,通过优化器去评估每一个查问的复杂度加上队列的默人并发度来决定查问在队列中可占用的内存大小,在查问实在开始执行之前曾经限定了它可应用的内存,加上 GreenPlum 弱小的计算引擎所有算子都能够落盘,使得资源队列能够保障系统内的查问稳固运行,然而吞吐并不一定是最大化的。因为 GreenPlum 资源队列之间的内存不是弹性的,有队列闲暇下来它的内存资源也不能给其余队列应用。抛开资源队列间的弹性问题,其要想做到单个资源队列内的查问稳固高效运行离不开 Greenplum 的两个外围能力:CBO 优化器智能评估出查问须要占用的内存,全算子可落盘的计算引擎。
ClickHouse 目前的现状是:1)没有优化器帮忙评估查问的复杂度,2)整个计算引擎的落盘能力比拟弱,在限定内存的状况下无奈保障 query 顺利执行。因而咱们联合 ClickHouse 计算引擎的特色,设计了一套弹性资源队列模型,其中外围的弹性内存抢占准则包含以下几个:
- 对资源队列内的查问不设内存限度
- 队列中的查问在申请内存时如果遇到内存不足,则尝试从优先级更低的队列中抢占式申请内存
- 在 2)中内存抢占过程中,如果抢占申请失败,则查看本人所属的资源队列是否被其余查问抢占内存,尝试 kill 抢占内存最多的查问回收内存资源
- 如果在 3)中尝试回收被抢占内存资源失败,则以后查问报 OOM Exception
- 每个资源队列预留肯定比例的内存不可抢占,当资源队列中的查问负载达到肯定水位时,内存就变成齐全不可被抢占。同时用户在定义资源队列时,isolate= 0 的队列是容许被抢占的,isolate= 1 的队列不容许被抢占,isolate= 2 的队列不容许被抢占也不容许抢占其余队列
- 当资源队列中有查问 OOM 失败,或者因为抢占内存被 kill,则把以后资源队列的并发数长期下调,等零碎复原后再逐渐上调。
ClickHouse 弹性资源队列的设计准则就是容许内存资源抢占来达到资源利用率的最大化,同时动静调整资源队列的并发限度来避免 bad query 呈现时导致用户的查问大面积失败。因为计算引擎的束缚限度,目前无奈保障查问齐全没有 OOM,然而用户端能够通过错误信息来判断查问是否属于 bad sql,同时对误杀的查问进行 retry。
原文链接 本文为阿里云原创内容,未经容许不得转载。