ClickHouse 分布式表与本地表:查询路由机制与数据一致性
ClickHouse 分布式表与本地表查询路由机制与数据一致性一、分布式查询的隐藏陷阱你以为的分布式可能只是转发ClickHouse 的分布式架构中分布式表Distributed Engine和本地表ReplicatedMergeTree 等的关系是理解查询行为的关键。分布式表本身不存储数据它只是一个查询路由层——接收查询请求将查询分发到各个分片的本地表上执行然后汇总结果。这个查询路由看似简单实则暗藏多个工程陷阱。第一查询路由的延迟开销分布式表需要与每个分片建立连接、发送查询、等待响应网络延迟会叠加到查询时间中。第二数据一致性问题本地表的副本之间可能存在复制延迟不同副本返回的数据可能不一致。第三路由策略的选择ClickHouse 支持随机路由、轮询路由和优先级路由不同策略对查询性能和负载均衡的影响差异显著。在存储部的 ClickHouse 集群中我们遇到过这样的案例一个分布式查询在 3 个分片上执行其中一个分片的副本正在合并 Part导致该分片响应超时整个查询被取消。这不是 ClickHouse 的 Bug而是分布式查询路由机制的设计取舍。二、分布式表查询路由的底层机制2.1 分布式表的查询执行流程当客户端向分布式表发送查询时ClickHouse 的执行流程如下flowchart TD A[客户端发送查询到分布式表] -- B[解析查询 SQL] B -- C[确定分片列表] C -- D[为每个分片选择副本] D -- E{路由策略} E --|随机| F[随机选择一个副本] E --|轮询| G[按顺序选择副本] E --|优先级| H[选择延迟最低的副本] F -- I[向各分片发送子查询] G -- I H -- I I -- J[各分片本地执行查询] J -- K[汇总结果到发起节点] K -- L[返回结果给客户端] subgraph 单分片内部 M[本地表 Replica 1] N[本地表 Replica 2] O[复制同步通道] M --|数据复制| N end关键细节分布式表将原始查询的 SQL 文本原样发送到各分片各分片独立解析和执行。这意味着各分片的 ClickHouse 版本和配置必须兼容否则可能出现语法错误。2.2 副本选择与负载均衡ClickHouse 的副本选择策略由load_balancing参数控制random默认随机选择一个副本。简单但可能导致负载不均。round_robin轮询选择副本。负载更均衡但不考虑副本的实际负载。first_or_random优先选择配置中的第一个副本如果不可达则随机选择。适合有主从区分的场景。in_order按配置顺序尝试副本第一个不可达则尝试第二个。适合有明确优先级的场景。副本的健康检查基于 TCP 连接探测不检查副本的数据完整性。如果一个副本的复制队列积压了 100 万条记录但 TCP 连接正常它仍然会被选为查询目标。2.3 数据一致性的三个层次分布式查询的数据一致性取决于三个因素分片内一致性同一分片的不同副本之间通过 ReplicatedMergeTree 的复制机制保证最终一致性。但存在复制延迟——写入主副本后从副本可能需要数秒才能同步。分片间一致性不同分片之间没有事务保证。一个 INSERT 可能成功写入部分分片而失败于其他分片导致数据不完整。查询一致性分布式查询在汇总时各分片返回的是查询时刻本地表的数据快照。如果查询过程中有数据写入不同分片可能看到不同版本的数据。三、生产级配置与最佳实践3.1 分布式表与本地表的规范定义-- 本地表每个分片的每个副本上创建 CREATE TABLE orders_local ON CLUSTER {cluster} ( order_id UInt64, user_id UInt64, amount Decimal(12, 2), product_category LowCardinality(String), city LowCardinality(String), created_at DateTime ) ENGINE ReplicatedMergeTree( /clickhouse/tables/{cluster}/{database}/orders_local/{shard}, {replica} ) PARTITION BY toYYYYMM(created_at) ORDER BY (city, product_category, created_at) SETTINGS index_granularity 8192; -- 分布式表只在查询入口节点创建 CREATE TABLE orders_all ON CLUSTER {cluster} ( order_id UInt64, user_id UInt64, amount Decimal(12, 2), product_category LowCardinality(String), city LowCardinality(String), created_at DateTime ) ENGINE Distributed( {cluster}, currentDatabase(), orders_local, -- 分片键按 user_id 哈希分片 xxHash64(user_id) ) SETTINGS -- 路由策略优先低延迟副本 load_balancing in_order, -- 发送查询的超时时间 distributed_ddl_task_timeout 180;3.2 写入策略直接写本地表 vs 写分布式表-- 方式 1直接写本地表推荐 -- 客户端根据分片键计算目标分片直接写入对应分片的本地表 -- 优点写入延迟低无分布式事务开销 -- 缺点客户端需要实现分片路由逻辑 -- 方式 2写分布式表 -- ClickHouse 自动按分片键路由写入 -- 优点客户端无需关心分片逻辑 -- 缺点写入需要经过分布式表的转发延迟更高 INSERT INTO orders_all VALUES (1, 1001, 99.90, 电子产品, 北京, 2026-06-12 10:00:00); -- 内部流程分布式表接收 → 计算分片 → 转发到目标分片的本地表生产环境的推荐策略是写入走本地表查询走分布式表。写入时由客户端或中间件层如 CHProxy负责分片路由避免分布式表的写入转发开销。3.3 复制延迟监控与查询降级-- 监控复制延迟查看每个副本的复制队列深度 SELECT database, table, replica_name, absolute_delay, -- 主副本与当前副本的延迟秒数 queue_size, -- 复制队列中待处理的条目数 parts_to_check -- 需要校验的 Part 数量 FROM system.replicas WHERE absolute_delay 60 -- 延迟超过 60 秒的副本 ORDER BY absolute_delay DESC; -- 查询降级当副本延迟过高时排除该副本 SET load_balancing in_order; -- 配置优先级将健康副本排在前面延迟副本排在后面 -- 在 config.xml 中配置 remote_servers 的 replica 顺序3.4 分布式查询的超时与重试-- 设置分布式查询的超时时间 SET max_execution_time 300; -- 单分片查询最大执行时间 300s SET connect_timeout 10; -- 连接超时 10s SET receive_timeout 300; -- 接收数据超时 300s SET send_timeout 300; -- 发送数据超时 300s -- 设置连接池参数减少连接建立开销 SET distributed_connections_pool_size 16; -- 每个分片的连接池大小 SET connections_with_failover_max_tries 3; -- 连接失败时的重试次数四、Trade-offs分布式表的设计取舍4.1 查询灵活性与性能的权衡分布式表提供了透明的查询路由但每次查询都需要与所有分片通信。在分片数量较多20的场景下连接建立和结果汇总的开销不可忽略。对于只涉及单个分片的查询如按分片键过滤应该直接查询本地表避免分布式表的转发开销。4.2 数据一致性与可用性的权衡ReplicatedMergeTree 保证最终一致性而非强一致性。在复制延迟较高时查询可能读到旧数据。如果业务要求强一致性需要在查询前强制等待复制完成SYSTEM FLUSH DISTRIBUTED 等待复制队列为空但这会显著增加查询延迟。4.3 适用边界分布式表查询路由适用于以下场景查询需要跨分片聚合、分片数量在 3-20 之间、对数据一致性要求为最终一致。不适用于单分片查询为主直接查本地表更高效、分片数量过多连接开销大、要求强一致性的业务。五、总结理解 ClickHouse 分布式表的查询路由机制是优化分布式查询性能的基础。核心落地步骤如下规范建表本地表使用 ReplicatedMergeTree分布式表只做查询路由层。写入走本地表避免分布式表的写入转发开销由客户端或中间件负责分片路由。选择路由策略根据业务特点选择load_balancing参数优先in_order或first_or_random。监控复制延迟定期检查system.replicas延迟过高的副本应从查询路由中排除。合理设置超时根据查询复杂度和网络延迟设置超时参数避免单分片超时导致整个查询失败。分布式表不是万能路由器它有自己的性能边界和一致性限制。理解这些边界才能在架构设计时做出正确的取舍。