跳到主要内容
跳到主要内容

并行副本

Beta feature. Learn more.

简介

ClickHouse 处理查询的速度极快,但这些查询是如何在多台服务器之间被分发并并行执行的呢?

在本指南中,我们将首先介绍 ClickHouse 如何通过分布式表在多个分片之间分发查询,然后说明查询如何利用多个副本来完成执行。

分片架构

在无共享(shared-nothing)架构中,集群通常被拆分为多个分片(shard),每个分片包含整体数据的一个子集。一个分布式表位于这些分片之上,为完整数据提供统一视图。

读取请求可以发送到本地表,此时查询只会在指定分片上执行;也可以发送到分布式表,此时每个分片都会执行该查询。发起对分布式表查询的服务器会聚合数据并将结果返回给客户端:

分片架构

上图展示了客户端查询分布式表时发生的过程:

  1. SELECT 查询被任意发送到某个节点上的分布式表 (通过轮询策略,或在经过负载均衡器路由到特定服务器之后)。 该节点随后将作为协调器。

  2. 节点会根据分布式表中指定的信息,定位需要执行查询的每个分片, 并将查询发送给各个分片。

  3. 每个分片在本地读取、过滤并聚合数据,然后将可合并的中间状态 返回给协调器。

  4. 协调器节点对数据进行合并,然后将响应返回给客户端。

当我们引入副本时,流程基本相同,唯一的区别是每个分片中只有一个副本会执行查询, 从而能够并行处理更多查询请求。

非分片架构

ClickHouse Cloud 的架构与上文所介绍的架构有很大差异。 (参见 "ClickHouse Cloud Architecture" 了解更多详情。)由于计算与存储的分离,以及几乎无限的存储容量,对分片的需求就不那么重要了。

下图展示了 ClickHouse Cloud 的架构:

非分片架构

这种架构使我们几乎可以瞬时添加和移除副本,从而确保集群具有极高的可扩展性。右侧所示的 ClickHouse Keeper 集群确保我们拥有元数据的唯一可信来源。各个副本可以从 ClickHouse Keeper 集群获取元数据,并都维护相同的数据。数据本身存储在对象存储中,而 SSD 缓存则可以加速查询。

但是,现在我们如何将查询执行分布到多台服务器上呢?在分片架构中,这一点相对显而易见,因为每个分片实际上都可以在其数据子集上执行查询。那么,在没有分片的情况下这是如何实现的呢?

并行副本简介

为了在多台服务器上实现查询的并行执行,我们首先需要能够将其中一台服务器指定为协调节点。协调节点负责创建需要执行的任务列表,确保这些任务全部执行、聚合,并将结果返回给客户端。与大多数分布式系统类似,这个角色由接收初始查询的节点承担。我们还需要定义工作单元。在分片架构中,工作单元是分片,即数据的一个子集。使用并行副本时,我们将使用表中的一小部分数据,称为粒度,作为工作单元。

现在,让我们借助下图来看看它在实践中是如何工作的:

Parallel replicas

使用并行副本时:

  1. 来自客户端的查询在经过负载均衡器后被发送到某个节点。该节点成为此查询的协调节点。

  2. 该节点分析每个 part 的索引,并选择需要处理的合适 part 和粒度。

  3. 协调节点将工作负载拆分成一组可以分配给不同副本的粒度。

  4. 每组粒度由相应的副本进行处理,完成后将可合并的中间状态发送给协调节点。

  5. 最后,协调节点合并来自各副本的所有结果,然后将响应返回给客户端。

上述步骤描述了并行副本在理论上的工作方式。 然而在实践中,存在许多因素会阻碍这套逻辑完美运行:

  1. 某些副本可能不可用。

  2. ClickHouse 中的复制是异步的,在某些时间点上,一些副本可能不具有相同的 part。

  3. 副本之间的尾延迟需要以某种方式进行处理。

  4. 文件系统缓存会根据各副本上的活动在副本之间有所差异,这意味着随机分配任务在缓存局部性方面可能导致性能不够理想。

在接下来的小节中,我们将讨论如何克服这些因素。

公告

为了解决上面列表中的 (1) 和 (2),我们引入了“公告”(announcement)的概念。其工作方式如下图所示:

Announcements
  1. 来自客户端的查询在经过负载均衡器后被发送到某个节点,该节点成为此查询的协调节点。

  2. 协调节点发送请求,从集群中所有副本获取公告。副本对于某个表当前分区(parts)集合的视图可能略有不同。因此,我们需要收集这些信息以避免做出错误的调度决策。

  3. 随后,协调节点使用这些公告来确定一组可分配给不同副本的粒度单元。例如,在这里我们可以看到,没有来自 part 3 的粒度被分配给副本 2,因为该副本在其公告中未声明该分区。还要注意,没有任务被分配给副本 3,因为该副本没有提供公告。

  4. 在每个副本都处理完其粒度子集上的查询,并将可合并状态发送回协调节点之后,协调节点会合并结果,并将响应返回给客户端。

动态协调

为了解决尾延迟问题,我们引入了动态协调机制。这意味着,所有 granule 不再在一次请求中全部发送到同一个副本,而是由每个副本向协调节点请求新的任务(要处理的一组 granule)。协调节点会根据收到的公告,为副本分配相应的一组 granule。

假设当前处于这样一个阶段:所有副本都已经发送了包含所有 part 的公告。

下图展示了动态协调的工作方式:

动态协调 - 第 1 部分
  1. 各副本向协调节点表明它们可以处理任务,同时还可以指定各自能够处理的工作量。

  2. 协调节点将任务分配给这些副本。

动态协调 - 第 2 部分
  1. 副本 1 和 2 能够非常快速地完成各自的任务,它们会向协调节点请求新的任务。

  2. 协调节点为副本 1 和 2 分配新的任务。

动态协调 - 第 3 部分
  1. 所有副本现在都已完成各自任务的处理,它们会请求更多任务。

  2. 协调节点利用公告检查还有哪些任务尚未被处理,但已经没有剩余任务了。

  3. 协调节点会告知副本所有内容都已处理完成。随后它会合并所有可合并的状态,并返回查询结果。

管理缓存局部性

最后一个潜在的问题是如何处理缓存局部性。如果多次执行同一个查询,如何确保相同的任务被路由到同一副本?在前面的示例中,我们有如下任务分配:

副本 1副本 2副本 3
Part 1g1, g6, g7g2, g4, g5g3
Part 2g1g2, g4, g5g3
Part 3g1, g6g2, g4, g5g3

为了确保相同的任务被分配到同一副本并能从缓存中获益,会进行两步操作:首先计算分片 + 一组 granule(即一个任务)的哈希值;然后对副本数量取模以进行任务分配。

理论上这听起来没问题,但在实际中,如果某个副本突然负载升高或网络质量下降,而该副本又持续用于执行某些任务,就会引入尾延迟。如果 max_parallel_replicas 小于副本数量,那么会随机选择副本来执行查询。

任务窃取

如果某个副本处理任务的速度比其他副本慢,其他副本会尝试从该副本那里“窃取”按哈希分配本应由它处理的任务,以降低尾部延迟。

限制

此功能存在一些已知限制,主要限制记录在本节中。

注意

如果您发现的问题不在下面列出的限制之中,并且怀疑是由并行副本(parallel replica)导致的,请在 GitHub 上使用 comp-parallel-replicas 标签提交 issue。

限制说明
复杂查询目前,并行副本在处理简单查询时表现良好。增加复杂度的层级(例如 CTE、子查询、JOIN、非扁平查询等)可能会对查询性能产生负面影响。
小型查询如果您执行的查询处理的行数不多,将其在多个副本上执行可能并不能带来更好的性能,因为副本之间协调所需的网络时间会在查询执行中引入额外开销。您可以通过使用以下设置来减少这些问题:parallel_replicas_min_number_of_rows_per_replica
使用 FINAL 时禁用并行副本
投影与并行副本不会同时使用
高基数数据与复杂聚合需要发送大量数据的高基数聚合会显著拖慢查询。
与新分析器的兼容性在特定场景下,新分析器可能会显著减慢或加速查询执行。
SettingDescription
enable_parallel_replicas0: 禁用
1: 启用
2: 强制启用并行副本,如果无法使用并行副本则抛出异常。
cluster_for_parallel_replicas用于并行副本的集群名称;如果你使用的是 ClickHouse Cloud,请使用 default
max_parallel_replicas在多个副本上执行查询时可使用的最大副本数;如果指定的值小于集群中的副本数,则会随机选择节点。此值也可以设置得高于实际副本数,以预留水平扩展空间。
parallel_replicas_min_number_of_rows_per_replica用于根据需要处理的行数来限制所使用的副本数量,实际使用的副本数定义为:
estimated rows to read / min_number_of_rows_per_replica
allow_experimental_analyzer0: 使用旧分析器
1: 使用新分析器。

并行副本的行为可能会因所使用的分析器不同而发生变化。

排查并行副本相关问题

你可以在 system.query_log 表中检查每个查询实际使用的设置。你也可以查看 system.events 表,了解服务器上发生的所有事件,并使用 clusterAllReplicas 表函数查看所有副本上的表(如果你是云用户,请将集群名设为 default)。

SELECT
   hostname(),
   *
FROM clusterAllReplicas('default', system.events)
WHERE event ILIKE '%ParallelReplicas%'
响应
┌─hostname()───────────────────────┬─event──────────────────────────────────────────┬─value─┬─description──────────────────────────────────────────────────────────────────────────────────────────┐
│ c-crimson-vd-86-server-rdhnsx3-0 │ ParallelReplicasHandleRequestMicroseconds      │   438 │ 处理来自副本的标记请求所用时间                                               │
│ c-crimson-vd-86-server-rdhnsx3-0 │ ParallelReplicasHandleAnnouncementMicroseconds │   558 │ 处理副本通告所用时间                                                         │
│ c-crimson-vd-86-server-rdhnsx3-0 │ ParallelReplicasReadUnassignedMarks            │   240 │ 所有副本中已调度未分配标记的总数                                  │
│ c-crimson-vd-86-server-rdhnsx3-0 │ ParallelReplicasReadAssignedForStealingMarks   │     4 │ 所有副本中通过一致性哈希分配用于窃取的已调度标记总数 │
│ c-crimson-vd-86-server-rdhnsx3-0 │ ParallelReplicasStealingByHashMicroseconds     │     5 │ 收集用于哈希窃取的段所用时间                                            │
│ c-crimson-vd-86-server-rdhnsx3-0 │ ParallelReplicasProcessingPartsMicroseconds    │     5 │ 处理数据部分所用时间                                                                     │
│ c-crimson-vd-86-server-rdhnsx3-0 │ ParallelReplicasStealingLeftoversMicroseconds  │     3 │ 收集孤立段所用时间                                                              │
│ c-crimson-vd-86-server-rdhnsx3-0 │ ParallelReplicasUsedCount                      │     2 │ 执行基于任务的并行副本查询所用的副本数                         │
│ c-crimson-vd-86-server-rdhnsx3-0 │ ParallelReplicasAvailableCount                 │     6 │ 可用于执行基于任务的并行副本查询的副本数                    │
└──────────────────────────────────┴────────────────────────────────────────────────┴───────┴──────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─hostname()───────────────────────┬─event──────────────────────────────────────────┬─value─┬─description──────────────────────────────────────────────────────────────────────────────────────────┐
│ c-crimson-vd-86-server-e9kp5f0-0 │ ParallelReplicasHandleRequestMicroseconds      │   698 │ 处理来自副本的标记请求所用时间                                               │
│ c-crimson-vd-86-server-e9kp5f0-0 │ ParallelReplicasHandleAnnouncementMicroseconds │   644 │ 处理副本通告所用时间                                                         │
│ c-crimson-vd-86-server-e9kp5f0-0 │ ParallelReplicasReadUnassignedMarks            │   190 │ 所有副本中已调度未分配标记的总数                                  │
│ c-crimson-vd-86-server-e9kp5f0-0 │ ParallelReplicasReadAssignedForStealingMarks   │    54 │ 所有副本中通过一致性哈希分配用于窃取的已调度标记总数 │
│ c-crimson-vd-86-server-e9kp5f0-0 │ ParallelReplicasStealingByHashMicroseconds     │     8 │ 收集用于哈希窃取的段所用时间                                            │
│ c-crimson-vd-86-server-e9kp5f0-0 │ ParallelReplicasProcessingPartsMicroseconds    │     4 │ 处理数据部分所用时间                                                                     │
│ c-crimson-vd-86-server-e9kp5f0-0 │ ParallelReplicasStealingLeftoversMicroseconds  │     2 │ 收集孤立段所用时间                                                              │
│ c-crimson-vd-86-server-e9kp5f0-0 │ ParallelReplicasUsedCount                      │     2 │ 执行基于任务的并行副本查询所用的副本数                         │
│ c-crimson-vd-86-server-e9kp5f0-0 │ ParallelReplicasAvailableCount                 │     6 │ 可用于执行基于任务的并行副本查询的副本数                    │
└──────────────────────────────────┴────────────────────────────────────────────────┴───────┴──────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─hostname()───────────────────────┬─event──────────────────────────────────────────┬─value─┬─description──────────────────────────────────────────────────────────────────────────────────────────┐
│ c-crimson-vd-86-server-ybtm18n-0 │ ParallelReplicasHandleRequestMicroseconds      │   620 │ 处理来自副本的标记请求所用时间                                               │
│ c-crimson-vd-86-server-ybtm18n-0 │ ParallelReplicasHandleAnnouncementMicroseconds │   656 │ 处理副本通告所用时间                                                         │
│ c-crimson-vd-86-server-ybtm18n-0 │ ParallelReplicasReadUnassignedMarks            │     1 │ 所有副本中已调度未分配标记的总数                                  │
│ c-crimson-vd-86-server-ybtm18n-0 │ ParallelReplicasReadAssignedForStealingMarks   │     1 │ 所有副本中通过一致性哈希分配用于窃取的已调度标记总数 │
│ c-crimson-vd-86-server-ybtm18n-0 │ ParallelReplicasStealingByHashMicroseconds     │     4 │ 收集用于哈希窃取的段所用时间                                            │
│ c-crimson-vd-86-server-ybtm18n-0 │ ParallelReplicasProcessingPartsMicroseconds    │     3 │ 处理数据部分所用时间                                                                     │
│ c-crimson-vd-86-server-ybtm18n-0 │ ParallelReplicasStealingLeftoversMicroseconds  │     1 │ 收集孤立段所用时间                                                              │
│ c-crimson-vd-86-server-ybtm18n-0 │ ParallelReplicasUsedCount                      │     2 │ 执行基于任务的并行副本查询所用的副本数                         │
│ c-crimson-vd-86-server-ybtm18n-0 │ ParallelReplicasAvailableCount                 │    12 │ 可用于执行基于任务的并行副本查询的副本数                    │
└──────────────────────────────────┴────────────────────────────────────────────────┴───────┴──────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─hostname()───────────────────────┬─event──────────────────────────────────────────┬─value─┬─description──────────────────────────────────────────────────────────────────────────────────────────┐
│ c-crimson-vd-86-server-16j1ncj-0 │ ParallelReplicasHandleRequestMicroseconds      │   696 │ 处理来自副本的标记请求所用时间                                               │
│ c-crimson-vd-86-server-16j1ncj-0 │ ParallelReplicasHandleAnnouncementMicroseconds │   717 │ 处理副本通告所用时间                                                         │
│ c-crimson-vd-86-server-16j1ncj-0 │ ParallelReplicasReadUnassignedMarks            │     2 │ 所有副本中已调度未分配标记的总数                                  │
│ c-crimson-vd-86-server-16j1ncj-0 │ ParallelReplicasReadAssignedForStealingMarks   │     2 │ 所有副本中通过一致性哈希分配用于窃取的已调度标记总数 │
│ c-crimson-vd-86-server-16j1ncj-0 │ ParallelReplicasStealingByHashMicroseconds     │    10 │ 收集用于哈希窃取的段所用时间                                            │
│ c-crimson-vd-86-server-16j1ncj-0 │ ParallelReplicasProcessingPartsMicroseconds    │     6 │ 处理数据部分所用时间                                                                     │
│ c-crimson-vd-86-server-16j1ncj-0 │ ParallelReplicasStealingLeftoversMicroseconds  │     2 │ 收集孤立段所用时间                                                              │
│ c-crimson-vd-86-server-16j1ncj-0 │ ParallelReplicasUsedCount                      │     2 │ 执行基于任务的并行副本查询所用的副本数                         │
│ c-crimson-vd-86-server-16j1ncj-0 │ ParallelReplicasAvailableCount                 │    12 │ 可用于执行基于任务的并行副本查询的副本数                    │
└──────────────────────────────────┴────────────────────────────────────────────────┴───────┴──────────────────────────────────────────────────────────────────────────────────────────────────────┘

system.text_log 表中还包含有关使用并行副本执行查询的信息:

SELECT message
FROM clusterAllReplicas('default', system.text_log)
WHERE query_id = 'ad40c712-d25d-45c4-b1a1-a28ba8d4019c'
ORDER BY event_time_microseconds ASC
响应
┌─message────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ (来自 54.218.178.249:59198) SELECT * FROM session_events WHERE type='type2' LIMIT 10 SETTINGS allow_experimental_parallel_reading_from_replicas=2; (阶段: Complete)                                                                                       │
│ 查询 SELECT __table1.clientId AS clientId, __table1.sessionId AS sessionId, __table1.pageId AS pageId, __table1.timestamp AS timestamp, __table1.type AS type FROM default.session_events AS __table1 WHERE __table1.type = 'type2' LIMIT _CAST(10, 'UInt64') SETTINGS allow_experimental_parallel_reading_from_replicas = 2 到阶段 Complete │
│ 已授予访问权限: SELECT(clientId, sessionId, pageId, timestamp, type) ON default.session_events                                                                                                                                                             │
│ 查询 SELECT __table1.clientId AS clientId, __table1.sessionId AS sessionId, __table1.pageId AS pageId, __table1.timestamp AS timestamp, __table1.type AS type FROM default.session_events AS __table1 WHERE __table1.type = 'type2' LIMIT _CAST(10, 'UInt64') 到阶段 WithMergeableState 仅分析 │
│ 已授予访问权限: SELECT(clientId, sessionId, pageId, timestamp, type) ON default.session_events                                                                                                                                                             │
│ 查询 SELECT __table1.clientId AS clientId, __table1.sessionId AS sessionId, __table1.pageId AS pageId, __table1.timestamp AS timestamp, __table1.type AS type FROM default.session_events AS __table1 WHERE __table1.type = 'type2' LIMIT _CAST(10, 'UInt64') 从阶段 FetchColumns 到阶段 WithMergeableState 仅分析 │
│ 查询 SELECT __table1.clientId AS clientId, __table1.sessionId AS sessionId, __table1.pageId AS pageId, __table1.timestamp AS timestamp, __table1.type AS type FROM default.session_events AS __table1 WHERE __table1.type = 'type2' LIMIT _CAST(10, 'UInt64') SETTINGS allow_experimental_parallel_reading_from_replicas = 2 到阶段 WithMergeableState 仅分析 │
│ 已授予访问权限: SELECT(clientId, sessionId, pageId, timestamp, type) ON default.session_events                                                                                                                                                             │
│ 查询 SELECT __table1.clientId AS clientId, __table1.sessionId AS sessionId, __table1.pageId AS pageId, __table1.timestamp AS timestamp, __table1.type AS type FROM default.session_events AS __table1 WHERE __table1.type = 'type2' LIMIT _CAST(10, 'UInt64') SETTINGS allow_experimental_parallel_reading_from_replicas = 2 从阶段 FetchColumns 到阶段 WithMergeableState 仅分析 │
│ 查询 SELECT __table1.clientId AS clientId, __table1.sessionId AS sessionId, __table1.pageId AS pageId, __table1.timestamp AS timestamp, __table1.type AS type FROM default.session_events AS __table1 WHERE __table1.type = 'type2' LIMIT _CAST(10, 'UInt64') SETTINGS allow_experimental_parallel_reading_from_replicas = 2 从阶段 WithMergeableState 到阶段 Complete │
│ 请求的副本数量 (100) 大于集群中实际可用的副本数量 (6)。将使用后者执行查询。                                                                                                       │
│ 来自副本 4 的初始请求: 2 个分区: [分区 all_0_2_1 范围 [(0, 182)], 分区 all_3_3_0 范围 [(0, 62)]]----------
已从副本 4 接收
                                                                                                   │
│ 读取状态已完全初始化: 分区 all_0_2_1 范围 [(0, 182)] 位于副本 [4]; 分区 all_3_3_0 范围 [(0, 62)] 位于副本 [4]                                                                                                            │
│ 已发送初始请求: 1 副本数量: 6                                                                                                                                                                                                                 │
│ 来自副本 2 的初始请求: 2 个分区: [分区 all_0_2_1 范围 [(0, 182)], 分区 all_3_3_0 范围 [(0, 62)]]----------
已从副本 2 接收
                                                                                                   │
│ 已发送初始请求: 2 副本数量: 6                                                                                                                                                                                                                 │
│ 正在处理来自副本 4 的请求,最小标记大小为 240                                                                                                                                                                                                                 │
│ 将向副本 4 响应 1 个分区: [分区 all_0_2_1 范围 [(128, 182)]]。完成: false; mine_marks=0, stolen_by_hash=54, stolen_rest=0                                                                                                       │
│ 来自副本 1 的初始请求: 2 个分区: [分区 all_0_2_1 范围 [(0, 182)], 分区 all_3_3_0 范围 [(0, 62)]]----------
已从副本 1 接收
                                                                                                   │
│ 已发送初始请求: 3 副本数量: 6                                                                                                                                                                                                                 │
│ 正在处理来自副本 4 的请求,最小标记大小为 240                                                                                                                                                                                                                 │
│ 将向副本 4 响应 2 个分区: [分区 all_0_2_1 范围 [(0, 128)], 分区 all_3_3_0 范围 [(0, 62)]]。完成: false; mine_marks=0, stolen_by_hash=0, stolen_rest=190                                                                  │
│ 来自副本 0 的初始请求: 2 个分区: [分区 all_0_2_1 范围 [(0, 182)], 分区 all_3_3_0 范围 [(0, 62)]]----------
已从副本 0 接收
                                                                                                   │
│ 已发送初始请求: 4 副本数量: 6                                                                                                                                                                                                                 │
│ 来自副本 5 的初始请求: 2 个分区: [分区 all_0_2_1 范围 [(0, 182)], 分区 all_3_3_0 范围 [(0, 62)]]----------
已从副本 5 接收
                                                                                                   │
│ 已发送初始请求: 5 副本数量: 6                                                                                                                                                                                                                 │
│ 正在处理来自副本 2 的请求,最小标记大小为 240                                                                                                                                                                                                                 │
│ 将向副本 2 响应 0 个分区: []。完成: true; mine_marks=0, stolen_by_hash=0, stolen_rest=0                                                                                                                                                │
│ 来自副本 3 的初始请求: 2 个分区: [分区 all_0_2_1 范围 [(0, 182)], 分区 all_3_3_0 范围 [(0, 62)]]----------
已从副本 3 接收
                                                                                                   │
│ 已发送初始请求: 6 副本数量: 6                                                                                                                                                                                                                 │
│ 总读取行数: 2000000                                                                                                                                                                                                                                │
│ 正在处理来自副本 5 的请求,最小标记大小为 240                                                                                                                                                                                                                 │
│ 将向副本 5 响应 0 个分区: []。完成: true; mine_marks=0, stolen_by_hash=0, stolen_rest=0                                                                                                                                                │
│ 正在处理来自副本 0 的请求,最小标记大小为 240                                                                                                                                                                                                                 │
│ 将向副本 0 响应 0 个分区: []。完成: true; mine_marks=0, stolen_by_hash=0, stolen_rest=0                                                                                                                                                │
│ 正在处理来自副本 1 的请求,最小标记大小为 240                                                                                                                                                                                                                 │
│ 将向副本 1 响应 0 个分区: []。完成: true; mine_marks=0, stolen_by_hash=0, stolen_rest=0                                                                                                                                                │
│ 正在处理来自副本 3 的请求,最小标记大小为 240                                                                                                                                                                                                                 │
│ 将向副本 3 响应 0 个分区: []。完成: true; mine_marks=0, stolen_by_hash=0, stolen_rest=0                                                                                                                                                │
│ (c-crimson-vd-86-server-rdhnsx3-0.c-crimson-vd-86-server-headless.ns-crimson-vd-86.svc.cluster.local:9000) 正在取消查询,因为已读取足够的数据                                                                                              │
│ 已读取 81920 行,5.16 MiB,耗时 0.013166 秒,6222087.194288318 行/秒,391.63 MiB/秒。                                                                                                                                                                   │
│ 协调完成: 统计信息: 副本 0 - {请求数: 2 标记数: 0 分配给我的: 0 通过哈希窃取: 0 窃取未分配的: 0}; 副本 1 - {请求数: 2 标记数: 0 分配给我的: 0 通过哈希窃取: 0 窃取未分配的: 0}; 副本 2 - {请求数: 2 标记数: 0 分配给我的: 0 通过哈希窃取: 0 窃取未分配的: 0}; 副本 3 - {请求数: 2 标记数: 0 分配给我的: 0 通过哈希窃取: 0 窃取未分配的: 0}; 副本 4 - {请求数: 3 标记数: 244 分配给我的: 0 通过哈希窃取: 54 窃取未分配的: 190}; 副本 5 - {请求数: 2 标记数: 0 分配给我的: 0 通过哈希窃取: 0 窃取未分配的: 0} │
│ 峰值内存使用量 (查询): 1.81 MiB。                                                                                                                                                                                                   │
│ 处理耗时 0.024095586 秒。                                                                                                                                                                                                              │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

最后,你还可以使用 EXPLAIN PIPELINE。它会直观展示 ClickHouse 将如何执行查询,以及在执行该查询时将使用哪些资源。比如下面这个查询:

SELECT count(), uniq(pageId) , min(timestamp), max(timestamp) 
FROM session_events 
WHERE type='type3' 
GROUP BY toYear(timestamp) LIMIT 10

我们先来看一下在没有并行副本时的查询流水线:

EXPLAIN PIPELINE graph = 1, compact = 0 
SELECT count(), uniq(pageId) , min(timestamp), max(timestamp) 
FROM session_events 
WHERE type='type3' 
GROUP BY toYear(timestamp) 
LIMIT 10 
SETTINGS allow_experimental_parallel_reading_from_replicas=0 
FORMAT TSV;
未使用 parallel_replica 的 EXPLAIN

现在开启 parallel replica:

EXPLAIN PIPELINE graph = 1, compact = 0 
SELECT count(), uniq(pageId) , min(timestamp), max(timestamp) 
FROM session_events 
WHERE type='type3' 
GROUP BY toYear(timestamp) 
LIMIT 10 
SETTINGS allow_experimental_parallel_reading_from_replicas=2 
FORMAT TSV;
带有 parallel_replica 的 EXPLAIN