引言
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。
原文链接 本文为阿里云原创内容,未经容许不得转载。