ClickHouse中的查询性能分析。 Yandex报告

如果您的数据库查询运行速度不够快怎么办? 您如何知道查询是最佳使用计算资源还是可以加速查询? 在莫斯科举行的上次HighLoad ++会议上,我谈到了查询性能的内省-ClickHouse DBMS提供的功能以及每个人都应该知道的OS功能。



每当我提出请求时,我不仅会担心结果,还会担心此请求的作用。 例如,它可以工作一秒钟。 是很多还是一点? 我一直在想:为什么不半秒钟? 然后,我优化某些东西,加快速度,它可以工作10毫秒。 我通常很满意。 但是,在这种情况下,我还是尝试做出令人不快的面部表情,并问:“为什么不设置5毫秒?” 我如何找出处理请求的时间? 原则上可以加速吗?

通常,请求处理速度是简单的算法。 我们编写了代码(可能是最优的),并且系统中有一些设备。 设备有规格。 例如,从L1缓存读取的速度。 或SSD可以执行的随机读取次数。 我们都知道。 我们需要考虑这些特征,加,减,乘,除并检查答案。 但这是理想情况,几乎永远不会发生。 差不多了 实际上,这有时会在ClickHouse中发生。

考虑一下有关服务器中哪些设备和哪些资源的琐碎事实。



处理器,内存,磁盘,网络。 我以这种方式特别安排了这些资源,从最简单,最方便的审查和优化开始,到最不方便,最复杂的结尾。 例如,我执行一个请求,发现我的程序似乎驻留在CPU上。 这是什么意思? 我会发现存在某种内部循环,该函数最常执行,重写代码,重新编译,一次执行-我的程序运行速度更快。

如果您花费过多的RAM,那么一切都会更加复杂。 您需要重新考虑数据结构,压缩一些位。 无论如何,我都重新启动程序,并且它花费更少的RAM。 没错,这通常会损害处理器。

如果所有内容都取决于磁盘,那么这也将更加困难,因为我可以更改磁盘上的数据结构,但是稍后必须转换此数据。 如果发布新版本,人们将不得不进行某种数据迁移。 事实证明,磁盘已经复杂得多了,最好事先考虑一下。

还有网络……我真的不喜欢网络,因为它经常完全不清楚其中发生了什么,特别是如果是大陆之间,数据中心之间的网络。 那里的速度变慢了,甚至连您的网络,服务器都没有,您也无能为力。 您唯一可以事先想到的就是如何传输数据以及如何最大程度地减少网络上的交互。

碰巧没有使用系统中的单个资源,并且程序只是在等待某些东西。 实际上,这是一个非常常见的情况,因为我们的系统是分布式的,并且可能有许多不同的流程和流程,并且某些流程正在等待另一个流程和流程,并且所有这些都需要以某种方式彼此关联才能正确考虑。



最简单的方法是以某个数值看资源的利用率。 例如,您从顶部开始,他写道:处理器是100%。 或运行iostat,他写道:磁盘是100%。 是的,这通常是不够的。 一个人会看到该程序驻留在磁盘上。 该怎么办? 您可以简单地注意到这一点并休息一下,然后决定一切都无法优化。 但是实际上,内部的每个设备都很复杂。 处理器具有一堆用于不同类型操作的计算设备。 磁盘可能具有RAID阵列。 如果有SSD,则内部有自己的处理器和控制器,这使得不清楚。 一个值-50%或100%-是不够的。 基本规则:如果您发现某些资源被100%利用,请不要放弃。 通常,您仍然可以改进某些东西。 但是它发生了,反之亦然。 假设您看到回收率为50%,但无能为力。

让我们仔细看看。



最简单,最方便的资源是处理器。 您看顶部,它表示处理器为100%。 但是应该记住,这不是100%的处理器。 顶层程序不知道处理器在那里做什么。 她从OS计划者的角度看。 也就是说,现在某种程序线程正在处理器上运行。 处理器会执行某项操作,然后将其平均显示为100%。 同时,处理器正在做某事,尚不清楚其效果如何。 每个周期可以执行不同数量的指令。 如果指令很少,则处理器本身可以等待某些事情,例如,从内存中加载数据。 同时,同一件事将显示在顶部-100%。 我们正在等待处理器遵循我们的指示。 他内部的工作还不清楚。

最后,当您认为程序驻留在处理器上时,情况简直是小巫见大巫。 的确如此,但是由于某种原因,处理器的频率较低。 原因可能很多:过热,功率限制。 出于某种原因,在数据中心中,电源存在功率限制,或者可以简单地打开节能功能。 然后,处理器将不断地从较高的频率切换到较低的频率,但是如果您的负载不稳定,则这将是不够的,平均而言,代码将执行得更慢。 有关当前处理器频率,请参见涡轮增压器。 检查dmesg中是否过热。 如果发生这样的事情,它会说:“过热。 降低频率。”

如果您对内部有多少个高速缓存未命中,每个周期执行了多少指令感兴趣,请使用perf记录。 记录该程序的一些示例。 此外,可以使用性能统计或性能报告来查看它。



反之亦然。 假设您看到的是顶部,并且处理器的回收利用率不到50%。 假设您的系统中有32个虚拟处理器核心和16个物理核心,这是因为在Intel处理器上,超线程是双倍的。 但这并不意味着附加的内核是无用的。 这完全取决于负载。 假设您有一些优化的线性代数运算,或者您有用于挖掘比特币的哈希。 然后,代码将被清除,每个周期将执行许多指令,也不会发生高速缓存未命中,分支预测错误。 而且超线程无济于事。 当您有一个内核在等待某些东西,而另一个内核可以同时从另一个线程执行指令时,它会有所帮助。

ClickHouse有两种情况。 例如,当我们进行数据聚合(GROUP BY)或按集合过滤(IN子查询)时,我们将有一个哈希表。 如果哈希表不适合处理器高速缓存,则会发生高速缓存未命中。 这是很难避免的。 在这种情况下,超线程将为我们提供帮助。

默认情况下,ClickHouse仅使用物理处理器内核,超线程除外。 如果您知道您的请求可以从超线程中受益,则只需将线程数加倍即可:SET max thread = 32,这样您的请求就会更快。

碰巧处理器已被完美使用,但是您查看图表并看到例如10%。 例如,在最坏的情况下,您的日程表是五分钟。 即使是一秒钟,仍然会有某种平均值。 实际上,您经常有请求,它们以每秒100毫秒的速度快速执行,这很正常。 因为ClickHouse会尝试尽快执行请求。 他根本不尝试完全持续地使用和过热您的处理器。



让我们仔细看看,这是一个有点复杂的选项。 在子查询中有一个带有表达式的查询。 在子查询中,我们有1亿个随机数。 我们只是过滤此结果。

我们看到这样的照片。 顺便说一句,谁会说我可以用什么工具看到这张精美的图画? 绝对正确-性能。 我很高兴您知道这一点。

我打开perf,以为现在我明白了一切。 我打开汇编列表。 我在那儿写了在一条特定指令上执行程序的频率,即一条指令指针的频率。 此处的数字以百分比表示,并写成几乎90%的时间都在执行test%edx,%edx指令,即检查四个字节是否为零。

问题是:为什么处理器要花这么长时间才能简单地将四个字节与零进行比较? (听众的回答...)该部分没有剩余内容。 有位移,然后是crc32q指令,但好像指令指针从未在其上发生。 并且随机数生成不在此列表中。 有一个单独的功能,并且已经非常优化,并且不会降低速度。 其他事情在这里放慢了。 代码执行在此指令处停止,并花费大量时间。 空闲循环? 不行 为什么要插入空循环? 另外,如果我插入了Idle循环,那么在perf中也将可见。 没有被零除,只有与零的比较。

处理器具有流水线,它可以并行执行多个指令。 而且,当指令指针位于某个位置时,这并不意味着它正在执行该指令。 也许他在等待其他指示。

我们有一个哈希表来验证某个数字是否出现在某个集合中。 为此,我们在内存中进行查找。 当我们在内存中查找时,我们会遇到高速缓存未命中的情况,因为哈希表包含1亿个数字,因此不能保证它适合任何高速缓存。 因此,要执行调零检查指令,该数据应已从存储器中加载。 我们等到它们加载完毕。



现在,下一个资源(稍微复杂一点)是驱动器。 SSD有时也称为驱动器,尽管这并不完全正确。 SSD也将包含在此示例中。

我们以iostat为例,它显示利用率为100%。

在会议上,演讲者经常登台演讲并大声疾呼:“数据库总是紧靠磁盘。 因此,我们创建了一个内存数据库。 她不会放慢脚步。” 如果有人接近您并说了话,您可以放心地发送他。 会有一些问题-您说,我解决了。 :)

假设程序驻留在磁盘上,利用率为100。但是,这当然并不意味着我们可以最佳地使用磁盘。

一个典型的例子是您只有很多随机访问权限时。 即使访问是顺序的,也可以按顺序读取文件,但是它仍然可能是最佳的。

例如,您有一个RAID阵列,多个设备-例如8个磁盘。 而且,您只是顺序读取而不需要提前读取,缓冲区大小为1 MB,RAID中条带中的块大小也为1 MB。 然后,您将从一台设备获得的每个读数。 或者,如果未对齐,则来自两个设备。 半个兆字节会放在某个地方,另一个半兆字节会在某个地方,依此类推-依次使用磁盘:一个磁盘,然后另一个磁盘,然后三分之一磁盘。

需要提前阅读。 或者,如果您有O_DIRECT,请增加缓冲区的大小。 也就是说,规则是:8个磁盘,块大小为1 MB,将缓冲区大小设置为至少8 MB。 但这只有在读数对齐的情况下才能达到最佳效果。 如果未对齐,则首先会有多余的部分,您需要放置更多部分,再乘以更多。

或者,例如,您具有RAID10。您可以从RAID 10中以什么速度读取数据(例如从8个磁盘中读取)? 有什么好处? 四倍,因为有镜子,还是八倍? 实际上,这取决于如何创建RAID,以及条带中的块的排列方式。

如果在Linux上使用mdadm,则可以在其中指定Near布局和far布局,其中Near更好地用于编写,而far则用于阅读。

我总是建议使用远距布局,因为当您写入分析数据库时,时间通常并不那么重要-即使写入的内容比读取的更多。 这是通过一些后台过程完成的。 但是,阅读时,您需要尽快完成。 因此,最好通过设置远距布局来优化RAID以进行读取。

幸运的是,在Linux中,mdadm默认会将您设置为接近布局,而您将只能获得一半的性能。 有很多这样的耙子。

另一个可怕的原因是RAID 5或RAID6。通过顺序读写,一切都可以很好地扩展。 在RAID 5中,多重性是“设备数减一”。 即使使用随机读数,它也可以很好地缩放,但使用随机读数时,它不能很好地缩放。 在任何一个地方进行记录,您需要从所有其他磁盘读取数据,然后对它们进行poksorit(XOR-近似编辑),然后再写入另一位置。 为此,使用了某些高速缓存条,这是一个糟糕的耙子。 在Linux中,默认情况下会创建RAID 5,它将为您减慢速度。 您会认为RAI​​D 5总是变慢,因为这是可以理解的。 但实际上,原因是安装错误。

另一个例子。 您正在从SSD读取数据,并且购买了一个不错的SSD,它表示规范中每秒30万次随机读取。 由于某些原因,您无法执行此操作。 而且您认为-是的,它们全都位于其规范中,没有这样的事情。 但是所有这些读数必须并行进行,并具有最大的并行度。 达到最佳效果的唯一方法是使用异步I / O,它是通过系统调用io_submit,io_getevents,io_setup等实现的。

顺便说一句,磁盘上的数据(如果存储它们)总是需要压缩。 我将通过实践给出一个例子。 有人在ClickHouse 支持聊天中与我们联系,并说:

-ClickHouse压缩数据。 我看到它放在处理器上。 我有非常快的NVMe SSD,它们的读取速度为每秒几GB。 是否可以以某种方式禁用ClickHouse中的压缩?
“不,没有办法,”我说。 -您需要保持数据压缩。
-停止吧,将有另一种不起作用的压缩算法。
-容易 在此代码行中输入这些字母。
“的确,一切都很简单。”一天后,他回答。 -是的
-性能有多大变化?
“测试失败,”第二天他写道。 -数据太多。 它们不再适合SSD。

现在,让我们看一下从磁盘读取的内容。 我们启动dstat,它显示读取速度。

dstat和iostat的第一个示例


这是读取的列-300 MB / s。 我们从光盘中读取。 很多或一点-我不知道。

现在,我运行iostat进行检查。 在这里,您可以按设备查看细分。 我有RAID,md2和八个硬盘。 他们每个人都显示回收,甚至没有达到100%(50-60%)。 但是最重​​要的是,我只能以20-30 MB / s的速度读取每个磁盘。 从小我就记得一个规则,即您可以从硬盘读取100 MB / s的数据。 由于某些原因,这仍然没有太大变化。

dstat和iostat的第二个示例


这是另一个例子。 阅读更理想。 我运行dstat,从八个驱动器中的RAID 5中读取数据的速度为1 GB / s。 iostat显示什么? 是的,接近1 GB /秒。

现在,驱动器最终已100%加载。 是的,出于某种原因,两个是100%,其余的是95%。 可能它们还是有些不同。 但是,与他们每个人我读150 MB / s,甚至比它可以。 有什么区别? 在第一种情况下,我读取的缓冲区大小不足,碎片不足。 很简单,我告诉你一些常识。

顺便说一句,如果您认为仍不需要为分析数据库压缩数据,即HighLoad ++ Siberia会议的报告基于该报告的habrastaty-近似 。)。 组织者决定在新西伯利亚做出最严格的报告。



下一个示例是内存。 持续的真理。 首先,在Linux上,永远都看不到免费节目。 对于正在观看的人,他们特别创建了站点linuxatemyram.com。 进来,会有一个解释。 您也不需要查看虚拟内存量,因为有什么区别,程序分配了多少地址空间? 查看使用了多少物理内存。

而且还不清楚如何与之战斗。 请记住:分配器通常不喜欢向系统分配内存这一事实是正常的。 他们制作了mmap,但是munmap不再这样做了。 内存不会返回系统。 该程序认为-我更好地知道如何使用内存。 我留给我自己。 因为mmap和munmap系统调用非常慢。 更改地址空间,重置处理器的TLB缓存-最好不要这样做。 但是,操作系统仍然可以使用madvise系统调用来正确释放内存。 地址空间将保留,但实际上可以卸载内存。

永远不要在具有数据库的生产服务器上启用交换。 您认为-没有足够的内存,我将包括swap。 之后,该请求将停止工作。 它将破解无尽的时间。



网络太典型了。 如果每次都创建一个TCP连接,则选择正确的窗口大小会花费一些时间,因为TCP协议不知道传输数据的速度有多快。 他适应了这一点。

或想象一下-您正在传输文件,并且网络上存在较大的延迟,并且丢包率很高。 然后,使用TCP传输文件是否正确并不明显。 我认为这是错误的,因为TCP保证了一致性。 另一方面,您可以同时传输文件的一半和另一半。 TCP- TCP . , , , TCP . .

100- , . 10 -, , , . . .



? — . , , , 10 . , .



: « - » — . iotop, , , iops.

, . .

: top


top -, , clickHouse-server - , - . , , Shift+H, . , ClickHouse . ParalInputsProc, . BackgrProcPool — merges . , .

? ClickHouse, , . BackgroundProcessingPool. 15 . 16 1, 1 — . 16? , Linux — , : «16 . ». :)

clickhouse-benchmark. clickhouse-client. , clickhouse-client, . - . .

: clickhouse-benchmark + perf top


. clickhouse-benchmark, , , , , . peft top. peft top, . , - -, uniq: UniquesHashSet. . , . , .

, , . — -. , , XOR - . -. - -. , -.

, , crc32q. , , - , - .

, ClickHouse. , , . ClickHouse.



. , — , SHOW PROCESSLIST. . , SELECT * FROM system processes. : , , . ClickHouse top.

ClickHouse ? background-. Background- — merges. , merges , SELECT * FROM system.merges.

c


, . -. . — ClickHouse. . , , . , . - traf_testing. ? , , . ClickHouse .



. , . , , , , . query_log. — , - , SELECT , - . query_log , . - . — , . : .

, , — merge, inserts, . part_log. , .



query_log clickhouse-benchmark. select , , stdin clickhouse-benchmark.

query_log - , .



, , . . SET send_logs_level = 'trace', , .

:


, . , 98%. , . 这很简单。 SET send_logs_level = 'trace', , . - : merging aggregated data, . 1% . , .

, , query_log.

. SELECT * FROM system.query_log . . , , , query_log. . — , , , . .



ClickHouse . — system.events, system.metrics system.asynchronous_metrics. Events — , , . 100 . — 10 . system.metrics — . , 10 , 10 .

system.asynchronous_metrics , . . — . , system.asynchronous_metrics — , - . , .

, . SHOW PROCESSLIST . query_log, .



, . , . , . , , . , Linux, . Linux . , . , . .

, OSReadChars OSReadBytes. ? , , , . , . , , , . , - , .

page cache


, . - . , 40 , 6,7 . . , , . , , .

, 1,3 , 5 . 怎么了 , — page cache. ?

c


. . , , . . : 3,2 , — 2,5 . , , , . 怎么了 -, : read ahead. , — ? -, — 4 , , 512 KB. . , . , - read ahead.



. . , . , , ReadBytes — , . 3 , 3 . , , .

— IOWait. 87 . 7 , IOWait — 87. ? — . . , , 87 . , - .

— CPUWait. , , , . - — , . CPU. CPU. - , . — , , user space. , - . 好吧,好

— , Linux. - , . , , .

: query_thread_log


现在,我们有了最高级的东西:query_thread_log。有了它,您可以了解每个查询执行线程所花费的时间。

我寻找我的请求,通过query_id选择并指定指标“用户空间中花费的处理器时间量”。这是我们的信息流。为了并行处理请求,分配了16个线程。他们每个人都花了800毫秒。然后再分配16个线程以合并聚合函数的状态,每个线程花费0.25 s。现在,我可以确切地了解每个请求所花费的时间。

有关HighLoad ++的视频报告:

Source: https://habr.com/ru/post/zh-CN459198/


All Articles