
1. 为什么ClickHouse也需要负载均衡从集群架构说起做大数据的人对ClickHouse应该都不陌生。它让人又爱又恨——单机查询快得离谱几亿行数据秒级响应但一到集群化部署各种问题就来了某台节点CPU被打满其他节点在摸鱼某个分片磁盘炸了整个查询报错后端节点挂了连接还在往那里发查询超时一片红。这个时候你就知道光有ClickHouse本身是不够的还得在它前面加一层流量调度把请求合理地分配到各个节点上。说白了这就是为ClickHouse配置负载均衡要解决的问题。先说清楚一个基础概念ClickHouse的集群架构里有两个核心角色分片和副本。分片解决的是数据水平扩展的问题比如你有1亿条数据单机存不下也查不动拆成4个分片每个分片存2500万条副本解决的是高可用和读吞吐的问题每个分片的数据复制成2份或3份分散在不同机器上。副本之间做数据同步主副本挂了从副本能顶上。负载均衡做的事情就是在这堆分片和副本之间做流量调度让每个请求都落到“合适”的节点上——注意这个“合适”不只是“随便一台能用的”。这里要纠正一个常见的误区。很多人觉得ClickHouse自带的分布式表已经天然做了负载均衡不需要再额外配置。实际上分布式表只是把写入和查询的请求按照分片键路由到了对应的分片它并没有真正解决两个问题。第一个是连接层面的故障转移当你通过分布式表查询时如果某个分片所在节点正好宕机查询还是会失败除非你配置了副本并且开启internal_replication。第二个是连接统一入口的问题你的服务端程序不能直连一堆IP你得有一个统一的入口地址让后端的节点对客户端透明这样节点扩缩容才不会影响上游。这两个需求恰恰是负载均衡层要干的核心活。另外还有一个非常实际的痛点读写流量的隔离。大批量写入和即时查询混在同一个节点上会互相拖垮。你跑一个INSERT INTO ... SELECT的大任务CPU核都被占满了线上报表查询跟着遭殃。理想的做法是把写入流量和查询流量分别路由到不同的副本上或者至少做到写入只打到某些特定节点查询分摊到其他节点。这个功能ClickHouse本身不提供要靠负载均衡层来做流量调度策略。所以为ClickHouse配置负载均衡本质上是在做三件事一是提供统一的访问入口屏蔽后端节点变化二是在多个可用节点之间做请求分发避免单点热点三是根据业务需要做流量的精细调度比如读写分离、副本优先、故障转移。理解了这三件事你就知道后面所有配置方案都是围绕它们展开的。2. 部署前的基础准备集群规划与关键参数配置负载均衡之前先把ClickHouse集群本身的规划做好。集群没规划好负载均衡配得再花哨也是白搭。很多人在这一步就踩了坑——分片键选得随意副本数拍脑袋定的结果后期数据倾斜、同步延迟各种问题接踵而至。2.1 Linux环境下的ClickHouse部署要点以Linux部署ClickHouse为例我建议你直接使用官方预编译的RPM或DEB包而不是自己从源码编译。ClickHouse编译非常耗时而且对硬件要求高预编译包性能已经调优过了。部署方式有三种单机、单分片多副本、多分片多副本。负载均衡场景下至少要保证有两个节点才有意义所以我建议最少采用多副本架构。安装完成后立刻要做几件事修改listen_host配置默认只监听127.0.0.1不改的话其他机器根本连不上配置好max_connections、max_thread_pool_size这些资源参数避免连接数耗尽规划数据目录ClickHouse的存储目录建议放在SSD上机械盘的IO延迟会直接拖垮查询性能。还有一点很多人忽略ClickHouse的Linux系统参数需要调整。比如vm.max_map_count建议设置大于262144否则节点运行过程中容易出现Too many open files或者内存映射相关的错误。2.2 分片、副本与负载均衡的关系这里重点讲一下分片副本配置和负载均衡的关系。你需要在config.xml里配置remote_servers节点定义一个集群名称。一个典型的双副本集群配置大概是这样的remote_servers clickhouse_cluster shard replica hostclickhouse-01/host port9000/port /replica replica hostclickhouse-02/host port9000/port /replica /shard shard replica hostclickhouse-03/host port9000/port /replica replica hostclickhouse-04/host port9000/port /replica /shard /clickhouse_cluster /remote_servers这个配置定义了一个两分片两副本的集群。有两个关键点要理解。第一一个shard内的多个replica之间的数据是相同的互为备份。第二shard之间是水平拆分数据各不相同。负载均衡层需要知道这个拓扑结构才能做好路由决策。比如写入请求应该均匀分布到两个分片上去而查询请求可以优先路由到本分片副本来减少网络开销。在规划阶段分片键的选择是最重要也最容易被忽视的。分片键决定了数据怎么被拆到不同分片。如果选得不好比如选了性别这类取值类型很少的字段就会导致数据严重倾斜——某个分片的数据占了80%以上查询性能被最慢的那个分片拖死。实际项目里我建议优先选择高基数且分布均匀的字段比如用户ID、订单ID这类或者直接用rand()做均匀哈希。但要注意如果后续需要做GROUP BY等聚合查询尽量让相同分组的数据落到同一个分片上否则查询会被迫做跨分片的数据重分布性能下降非常明显。2.3 Zookeeper/Keeper集群配置ClickHouse的副本同步依赖ZooKeeper完成元数据协调。从21.8版本之后官方也推出了内置的ClickHouse Keeper部署更简单。我在实际项目里更推荐用ClickHouse Keeper少维护一套ZooKeeper集群而且性能不差。这里要注意一点Keeper或者ZooKeeper集群本身也要做高可用挂了三台里的两台写入就是不可用状态整个集群也就名存实亡了。Keeper节点的故障恢复速度直接影响ClickHouse的数据写入延迟所以这块不能掉以轻心。3. 用Nginx给ClickHouse做负载均衡最通用的方案聊完基础进入正题。ClickHouse的负载均衡方案有好几种从简单到复杂依次是Nginx、HAProxy、chproxy、ClickHouse自带的分布式表路由以及自研网关。其中Nginx是最通用、上手最快、资料最多的方案。我先把Nginx方案讲透因为即便是后面用chproxy或自研方案核心思路依然相通。3.1 为什么Nginx能胜任ClickHouse负载均衡Nginx做负载均衡本质上就是一个反向代理。它接收客户端的请求按照配置好的策略转发给后端的一组服务器再把后端返回的数据传回客户端。对ClickHouse来说客户端访问有两种方式一种是HTTP接口默认端口8123另一种是原生TCP协议默认端口9000。Nginx对这两种协议支持都有方案——HTTP层用http模块TCP层用stream模块。为什么要用两层来处理因为HTTP接口走的是同步请求响应模式适合查询类场景原生TCP协议是长连接模式性能更高适合高频写入和查询。如果你的业务主要通过clickhouse-client、JDBC或各种官方驱动访问这些驱动默认优先走TCP协议9000端口。如果你的业务走HTTP接口比如对接一些BI工具、前端数据可视化平台那就走8123端口。两边都要配负载均衡否则单点压力一样存在。3.2 完整的Nginx配置示例与解析下面是一份我在生产环境用过的stream模块配置。这个配置处理TCP层的负载均衡。# /etc/nginx/nginx.conf 中 stream 块 stream { upstream clickhouse_tcp_backend { least_conn; # 最少连接数策略适合长连接场景 server clickhouse-01:9000 max_fails3 fail_timeout30s; server clickhouse-02:9000 max_fails3 fail_timeout30s; server clickhouse-03:9000 max_fails3 fail_timeout30s; server clickhouse-04:9000 max_fails3 fail_timeout30s; } server { listen 9000; proxy_connect_timeout 5s; proxy_timeout 600s; # 长连接超时时间 proxy_pass clickhouse_tcp_backend; } # 配置一个健康检查用的端口 upstream clickhouse_health_check { server clickhouse-01:8123; server clickhouse-02:8123; server clickhouse-03:8123; server clickhouse-04:8123; } }几个参数需要拆开讲。least_conn策略对于ClickHouse这种长连接场景非常合适。为什么因为HTTP请求是短连接轮询就能保证均匀但ClickHouse原生协议下客户端通常会保持长连接一个客户端可能连着同一个后端节点不撒手。如果单纯用轮询可能会出现某些节点连接数堆积、其他节点空闲的情况。least_conn会动态选择当前连接数最少的节点长连接场景下更均匀。proxy_timeout设置成600s也很关键。ClickHouse的查询时长差异巨大慢查询可能跑几分钟。如果proxy_timeout设得太小比如默认60s一个超过60秒的查询会被Nginx直接掐断连接客户端报错。但设得太大也有问题TCP层无法感知HTTP层的请求完成只能等连接关闭所以这个值要根据业务情况合理设置。max_fails和fail_timeout是故障转移的核心。Nginx在这段时间内连续3次连接失败就会把这个节点标记为不可用30秒后才会重新试探。注意这里有坑如果ClickHouse节点只是负载高、响应慢而不是连接失败Nginx的TCP探活是感知不到的。TCP只要能建立连接Nginx就认为节点活着。解决办法是结合HTTP层的健康检查定期请求8123端口的/ping接口这样能更准确地反映节点健康状况。3.3 读写分离与HTTP接口的负载均衡配置对于HTTP接口可以在http块里再配置一组upstream。关键技巧是同一个Nginx可以同时监听两个端口——9000走TCP转发8123走HTTP反向代理。HTTP层还能做更精细的路由通过location匹配判断是查询接口还是写入接口分别转发到不同的节点组。比如# /etc/nginx/conf.d/clickhouse_http.conf upstream clickhouse_write_backend { # 写入走主分片副本 server clickhouse-01:8123; server clickhouse-03:8123; } upstream clickhouse_read_backend { # 查询走全部分片副本 server clickhouse-01:8123; server clickhouse-02:8123; server clickhouse-03:8123; server clickhouse-04:8123; } server { listen 8123; location /ping { proxy_pass http://clickhouse_read_backend; } # 通过URL区分读写TODO if ($request_method POST) { # 这里可以按请求体区分但Nginx层不太好做精细判断 # 更推荐的方式是——将写入和查询的请求打到不同的端口 proxy_pass http://clickhouse_write_backend; } location / { proxy_pass http://clickhouse_read_backend; } }实际上用Nginx的if判断POST请求来区分读写并不可靠因为ClickHouse的查询也可能是POST。我见过比较靠谱的做法是开两个HTTP端口一个用于写入比如8124一个用于查询还是8123。上游服务按照业务类型连不同的端口Nginx在端口层面做分流。这样做的好处是配置清晰、逻辑简单不会有请求被错误路由的风险。如果你的业务读写比例固定这种端口级分流的方式非常实用。另外如果用到OpenResty可以用Lua脚本做更动态的路由策略。比如根据请求参数中的表名把特定表的查询路由到特定节点或者根据请求的耗时统计把慢查询自动切换到备用节点。这些高级玩法在生产环境非常有用但需要你对Lua和Nginx内部机制有足够的了解建议有一定经验后再尝试。4. 更专业的方案chproxy与自研网关Nginx方案虽然通用但有一些天生的短板。比如Nginx层看不到ClickHouse的查询内容无法做细粒度的用户权限控制没有缓存能力同样的查询结果每次都要后端重新计算对慢查询、大查询等异常请求也没有限流熔断能力。在纯大数据分析场景下这些短板会逐渐放大。这个时候就该上chproxy或者自研网关了。4.1 chproxy专为ClickHouse设计的负载均衡与访问代理chproxy是一个专门为ClickHouse设计的高性能HTTP代理由社区开发者维护。可以把它理解为ClickHouse的“专属网关”。它解决了几个Nginx解决不了的问题支持ClickHouse的session会话保持同一个用户的请求尽量路由到同一台后端避免分布式查询的跨节点开销、支持缓存查询结果重复查询直接返回缓存结果大幅降低后端压力、支持基于用户的权限管理可以配置每个用户允许访问的表、最大内存、最大执行时间等、支持限流和配额控制。chproxy的配置比Nginx更贴近ClickHouse的使用场景。下面是一个典型的配置片段# /etc/chproxy/config.yml server: http: listen_addr: :8080 allowed_networks: [10.0.0.0/8] users: - name: analyst password: analyst_password # 该用户只能执行SELECT allowed_networks: [10.0.0.0/8] limits: max_execution_time: 60s max_memory_bytes: 1073741824 clusters: - name: clickhouse_cluster nodes: - clickhouse-01:8123 - clickhouse-02:8123 - clickhouse-03:8123 - clickhouse-04:8123你可以在chproxy层定义不同的用户角色每个角色绑定不同的集群节点、权限和资源限制。这样数据分析师、数据工程师、报表系统分别用不同的账号访问彼此隔离互不干扰。运维人员则可以用admin账号跳过限制做全量查询。这一层用户权限管理在纯Nginx方案里要自己用Lua脚本实现工作量不小。再说缓存。chproxy的查询缓存是基于URL精确匹配的。同样一条SQL带同样的参数会命中缓存直接返回。实际使用中这个缓存很适合报表场景——同一张报表每小时生成一次查询SQL完全相同后端结果也一样缓存能极大降低ClickHouse的负载。但要注意缓存默认关闭需要显式在配置里开启同时要设置合理的TTL否则数据更新后缓存不及时失效报表数据就“脏”了。4.2 数据同步场景下的负载均衡Flink写入ClickHouse热词里反复出现一个话题使用Flink实现MySQL同步到ClickHouse。这是目前很常见的数据管道架构。增量数据从MySQL的binlog同步到KafkaFlink消费Kafka的数据后写入ClickHouse。这个链路里的负载均衡有和纯查询不一样的地方。首先是写入端的均衡。Flink的ClickHouse连接器比如官方的clickhouse-flink-connector本身有shuffle机制可以按分片键将数据写入ClickHouse的分布式表。注意这里的关键参数是sink.bulk-flush.size和sink.bulk-flush.interval控制批量写入的触发条件。批量大小设置不当会导致写入压力波动很大——一次flush的数据量太大ClickHouse的merge任务就跟不上太小则写入次数过多网络开销大整体吞吐上不去。其次要关注并行度和ClickHouse的负载匹配。Flink的写入并行度如果高于后端ClickHouse节点能承受的并发量会出现too many simultaneous queries的错误。我一般建议先把Flink写入的并行度控制在4-8之间再根据ClickHouse节点的CPU和内存占用情况逐步上调不要一上来就开10几个并发。特别是Flink端做了多表合并写入的时候并发控制更加重要。还有一个经常被忽视的配置点ClickHouse分布式表写入时可以选择直接写本地表再由ClickHouse的分布式表自动路由也可以由Flink端先做分片再写入具体分片的本地表。前一种方案简单写入延迟略高而且分布式表有时候会二次分发性能有损耗后一种方案需要你在Flink端实现分片逻辑复杂一些但写入路径更直接。生产环境如果对写入延迟敏感推荐用后一种方式。负载均衡层在这个架构里的角色就变成了保证Flink连接的节点本身是高可用的避免某个节点故障导致整个写入链路中断。4.3 Doris与ClickHouse选型中的负载均衡差异热词里提到doris和clickhouse的选型这里顺带讲几句因为它直接影响负载均衡的配置思路。DorisApache Doris在架构上采用了FEFrontend和BEBackend分离的架构FE节点天然就是一个路由层负责SQL解析和查询规划客户端只需要连接任意一个FE节点即可FE内部会自动把查询分发到对应的BE节点。也就是说Doris自身的架构已经把负载均衡的一部分工作内置了你只需要在FE节点前面再加一层简单的负载均衡入口防止多个FE节点之间连接不均。ClickHouse则不一样它的分布式表是客户端视角的逻辑概念。你连接任意一个节点如果这个节点配置了分布式表它就会担任协调节点把查询分发到其他分片。但问题是如果你只连接某一个节点这个节点就要承担所有查询的入口压力容易成为瓶颈。所以我们才需要在ClickHouse前面加负载均衡层让查询流量分散到不同的节点让多个节点轮流充当协调者。简单说Doris自带了一层路由ClickHouse需要额外配一层路由这就是两者在负载均衡层面最大的区别。4.4 自研网关当现成方案不够用时的最后一招当业务足够复杂Nginx和chproxy都无法满足需求时就只能自研网关。比如你需要在网关层做精细的查询成本控制、字段级权限过滤、审计日志、动态熔断或者需要和公司的统一认证体系打通。自研方案的核心架构是用Netty或其他高性能异步框架做反向代理解析ClickHouse的HTTP或TCP协议内置路由规则和连接池管理。自研网关的工作量不小但也不是非要从头写。可以基于开源的网关框架做二次开发比如ShenYu、Spring Cloud Gateway这类现成的API网关在此基础上增加ClickHouse协议的处理逻辑。我见过一个生产项目基于Netty自研了一个简易网关核心功能就是连接池管理、路由分发、权限校验。整个实现也就几千行代码但极大简化了上游服务的接入逻辑——上游只需要连接固定的IP端口网关自动做节点健康检查、故障转移和流量分配。如果你所在团队后端能力较强自研是一个很值得考虑的长期方案特别是当有多个ClickHouse集群需要统一管理的时候。5. 常见问题与排障实录把那些坑提前踩平配置负载均衡不难难的是配置完之后集群能稳定运行。我把自己在真实项目中踩过的一些典型坑列出来整理成速查表希望你能绕过这些弯路。5.1 查询结果不一致副本之间数据同步延迟现象负载均衡把查询请求分发到了多个副本但不同副本返回的数据不一样。原因很简单ClickHouse的副本同步是异步的。当主副本接收并确认了写入从副本可能还没有完全应用这些变更。如果查询刚好被路由到了滞后的从副本就会读到旧数据。解决办法有两个层面。第一在ClickHouse层开启select_sequential_consistency参数这样查询会等待副本同步完成后再返回结果保证一致性。代价是查询延迟会略有上升。第二在负载均衡层做一个简单的策略——写入后短时间内的查询路由到主副本其他查询走从副本。这个策略在chproxy里可以通过user配置实现在Nginx里需要额外的路由规则。5.2 连接耗尽Too many simultaneous queries这是ClickHouse集群最经典的问题。当你通过负载均衡把大量并发请求分发到后端节点每个节点能处理的并发查询是有限的。ClickHouse默认的max_concurrent_queries是100具体看版本超过这个数新的查询会直接报错。排查思路先看后端节点的system.query表确认并发查询数量再看Nginx的连接数确认是否长连接未释放导致连接堆积最后检查客户端代码确认连接池设置是否合理。很多客户端框架默认用完不归还连接会把ClickHouse的连接池撑爆。建议在客户端连接池设置合理的maxTotal和maxIdle参数避免连接浪费。5.3 数据倾斜某个分片负载明显高于其他分片负载均衡只是把请求分发到节点但如果数据本身分布不均匀再均衡的请求分发也救不回来。典型场景按某个低基数字段做分片比如按省份分片某个大省的数据量占了三分之一这个分片所在节点就成了热门节点CPU、磁盘IO、查询负载都比其他节点高。解决思路还是要回到分片键的选择上。用高基数、分布均匀的字段或者用rand()做纯随机分片。如果业务对数据分布有确定性要求可以考虑复合分片键比如用省份加上随机数组合。重新分片的代价很大所以一定要在集群建设初期就做好规划。5.4 故障转移不生效健康检查形同虚设Nginx默认的健康检查只检查TCP连接是否建立。ClickHouse节点即使进程假死、磁盘打满、或查询线程池耗尽TCP连接依然能建立Nginx不会把它从upstream列表里摘掉。于是请求继续转发给“假死”节点客户端拿到的就是超时或者错误。解决方法是做两层健康检查。TCP层保留基础的connect检查同时在HTTP层定期请求ClickHouse的/ping接口。这个接口会真实执行一个轻量查询能反映节点的真实健康度。如果/ping连续几次超时就把节点标记为不可用。chproxy天然支持健康检查机制Nginx需要借助第三方模块或者用OpenResty的Lua脚本实现。生产环境里这一步不要省。5.5 大查询拖垮小查询资源隔离没做到位负载均衡层可以把流量分散到不同节点但无法控制同一个节点上大查询和小查询的资源争抢。一个大查询把CPU和内存吃满了其他小查询的响应时间会显著上升。这在小节点集群上尤其致命。我用的方案是在集群里单独规划出一批“重型节点”专跑大查询剩下的节点跑常规查询。然后通过负载均衡层的路由规则根据查询的大小、来源用户等特征分发到不同节点组。chproxy的users配置里的max_execution_time、max_memory_bytes参数也可以起到一定的隔离作用超出限制的查询直接拒绝执行。6. 结尾一个经验分享最后分享一个我在多次实践中摸索出来的小经验。负载均衡不是配完就一劳永逸的它是一个需要持续观察和调整的部分。我建议无论你用Nginx、chproxy还是自研网关都配上基本的访问日志和监控指标——请求量、延迟、后端节点健康状态、查询失败率。我见过太多人配好负载均衡后什么都不管直到某天某个节点默默宕机了一整天服务整体才爆发问题。另一个心得是关于方案选择的不要一上来就上最复杂的方案。如果你的集群只有两三个节点业务查询量也不大Nginx加一条upstream配置就能解决90%的问题。等业务量真的起来了节点扩展到十几台、需要精细权限控制和缓存的时候再平滑迁移到chproxy或者自研网关。技术方案的演进应该跟着业务走而不是一步到位堆满所有能力。毕竟负载均衡的目标是让ClickHouse集群更稳定高效地提供服务而不是让运维工作更复杂。