用ClickHouse做数据分析有一年多了,它的查询速度确实惊艳,但也踩了不少坑。从表结构设计到数据导入,从查询优化到集群部署,每个环节都有需要注意的地方。今天把这些踩坑经验整理出来,希望对正在用或者打算用ClickHouse的同学有帮助。
先说说背景。我们公司有一个数据分析平台,每天需要处理几千万条用户行为日志,做各种维度的统计和分析。一开始用的是MySQL,数据量小的时候还凑合,数据量到了几千万之后,查询速度就完全不能忍了,一个简单的聚合查询要跑几十秒甚至几分钟。后来调研了几个OLAP引擎,最终选择了ClickHouse。
选择ClickHouse的原因很简单:它太快了。同样的数据量和查询,MySQL要跑几十秒,ClickHouse只要几百毫秒。而且它的安装部署很简单,不需要复杂的集群配置就能跑起来。我们用ClickHouse重构了数据分析平台之后,查询体验有了质的飞跃,用户满意度大幅提升。
但在使用过程中,我们也踩了不少坑。有些坑是因为对ClickHouse的原理不了解,有些是因为它和传统关系型数据库的差异,有些是因为文档不够详细或者有坑。今天就把这些经验分享出来,帮大家少走弯路。
一、表结构设计的坑
第一个大坑是表结构设计。ClickHouse和MySQL等关系型数据库的表结构设计思路完全不同,如果按照MySQL的思路来设计ClickHouse的表,一定会出问题。
第一个坑是主键不是唯一约束。在MySQL中,主键是唯一的,不允许重复。但在ClickHouse中,主键(ORDER BY)不是唯一约束,它只是决定了数据的物理排序方式。你可以插入完全相同的两行数据,ClickHouse不会报错。这一点很多刚接触ClickHouse的人都会踩坑,以为主键能保证数据不重复,结果导入数据的时候重复了都不知道。
我们一开始就踩了这个坑。因为数据导入程序有重试机制,网络抖动的时候会重复导入数据。我们以为主键会去重,结果数据重复了,统计结果偏大。后来才知道,ClickHouse中要实现数据去重,需要用ReplacingMergeTree引擎,或者在查询的时候用GROUP BY去重。ReplacingMergeTree会在合并的时候去重,但不是实时的,在合并之前重复数据仍然存在。所以对于需要精确去重的场景,最好在查询的时候加GROUP BY,或者在导入的时候做幂等控制。
第二个坑是分区键的选择。ClickHouse支持分区(PARTITION BY),分区可以加速数据的删除和部分查询。但分区不是越多越好,分区太多会导致文件数量过多,影响性能。我们一开始按照天分区,后来发现对于我们的数据量来说,按月分区就够了,按天分区导致每个分区的数据量太小,文件数量太多,查询的时候需要打开很多文件,反而影响了性能。
分区键的选择原则是:分区的粒度要合适,每个分区的数据量不要太小也不要太大。一般来说,每个分区的数据量在几千万到几亿条比较合适。如果数据量小,可以按月或者按年分区;如果数据量大,可以按天甚至按小时分区。另外,分区键最好是经常用来做查询过滤条件的字段,这样查询的时候可以跳过不需要的分区,提升查询速度。
第三个坑是字段类型的选择。ClickHouse有丰富的字段类型,选择合适的类型对性能影响很大。我们一开始图省事,把所有数值字段都设成了Float64,所有字符串字段都设成了String。后来发现,这样做浪费了很多存储空间,也影响了查询性能。
比如,年龄、数量这些字段,用UInt8或者UInt16就够了,不需要用Float64。状态、类型这些取值有限的字段,可以用Enum类型,比String更省空间,查询也更快。对于基数比较低的字符串字段(比如省份、城市),可以用LowCardinality类型,它会在内部用字典编码,大幅减少存储空间。我们把字段类型优化之后,存储空间减少了将近一半,查询速度也提升了不少。
第四个坑是不要用太多的Materialized View。物化视图是ClickHouse的一个强大功能,可以预计算一些常用的聚合查询,大幅提升查询速度。但物化视图不是越多越好,每个物化视图都会在数据导入的时候额外计算,影响导入速度,而且会占用额外的存储空间。我们一开始建了十几个物化视图,结果数据导入速度从每秒几万条降到了每秒几千条,根本扛不住实时导入的压力。后来精简到了三个最常用的物化视图,导入速度恢复了,大部分查询也能满足。
二、数据导入的坑
第二个大坑是数据导入。ClickHouse的数据导入方式很多,但每种方式都有需要注意的地方。
第一个坑是不要单条插入。ClickHouse是列式存储,它的写入是批量的,单条插入的性能非常差。我们一开始用INSERT语句一条一条地插入数据,结果每秒只能插几百条,完全满足不了需求。后来改成了批量插入,每次插入一万到十万条,导入速度提升到了每秒十几万条。
所以,用ClickHouse的时候,一定要批量导入。如果是实时数据,可以先在内存中攒一批,然后批量写入,或者用Kafka等消息队列做缓冲。ClickHouse有专门的Kafka引擎,可以直接从Kafka消费数据,非常方便。
第二个坑是分区写入的顺序问题。ClickHouse的MergeTree引擎在写入的时候,会为每个分区创建一个数据片段(part)。如果写入的时候数据的分区是乱序的,会产生很多小的part,导致合并压力大,甚至出现"Too many parts"的错误。
我们一开始从MySQL同步历史数据的时候,是按照主键顺序导出的,而主键顺序和分区时间顺序不一致,导致写入的时候每个批次都包含多个分区的数据,产生了大量的小part。后来我们改成了按照分区顺序导出和导入,也就是先导入一个月的数据,再导入下一个月的数据,这样每个批次的数据都属于同一个分区,part数量大大减少,导入速度和后续的查询性能都有提升。
第三个坑是数据导入的时候不要开太多并发。ClickHouse的写入并发不需要太高,因为它是批量写入的,几个并发的写入线程就足够了。我们一开始开了20个并发写入,结果不仅速度没有提升,反而因为锁竞争和part过多导致性能下降。后来降到了4个并发,写入速度反而更快了。
第四个坑是TTL(数据生命周期)的使用。ClickHouse支持TTL,可以自动删除过期的数据。这个功能很实用,但使用的时候要注意。我们一开始给表设置了TTL,保留三个月的数据。但发现TTL删除数据的时候会占用大量的IO和CPU,影响正常的查询。后来我们把TTL的删除操作配置在业务低峰期执行,并且控制了删除的速度,就没有这个问题了。另外,TTL是在合并的时候执行的,如果合并不及时,过期数据可能不会被立即删除,需要手动触发合并。
三、查询优化的坑
第三个大坑是查询优化。ClickHouse的查询速度很快,但如果写得不好,也会很慢。
第一个坑是不要用SELECT 。ClickHouse是列式存储,查询的时候只会读取需要的列。如果你用SELECT ,就会读取所有列,浪费大量的IO和内存。我们一开始有个查询用了SELECT ,跑了十几秒才出结果,后来改成只查需要的五列,只要几百毫秒。所以,在ClickHouse中,一定要明确指定需要的列,不要图省事用SELECT 。
第二个坑是注意WHERE条件的顺序。ClickHouse的查询优化器不如MySQL那么智能,WHERE条件的顺序会影响查询性能。应该把过滤性最强的条件(也就是能过滤掉最多数据的条件)放在最前面,把分区键和主键相关的条件放在前面。这样ClickHouse可以利用主键索引和分区裁剪,快速跳过不需要的数据。我们有一个查询,一开始WHERE条件写得比较随意,跑了五秒,调整了条件顺序之后,只要三百毫秒。
第三个坑是不要在大表上做JOIN。ClickHouse的JOIN性能不如MySQL,尤其是大表之间的JOIN,会非常慢。因为ClickHouse是分布式的列式存储,JOIN需要把数据拉到内存中做哈希匹配,内存消耗大,速度也慢。我们的经验是,尽量把需要JOIN的数据在导入的时候就做宽表,把关联字段冗余到主表中,查询的时候就不需要JOIN了。如果确实需要JOIN,尽量用小表JOIN大表,并且用GLOBAL JOIN来避免分布式场景下的重复计算。
第四个坑是合理使用LIMIT。ClickHouse的LIMIT和MySQL不一样,它不是在查询结果中取前N条,而是在读取数据的时候就限制读取的数量。如果你的查询有ORDER BY,ClickHouse需要先排序再取LIMIT,这时候LIMIT不会减少数据读取量。但如果没有ORDER BY,或者ORDER BY的字段是主键,LIMIT可以大幅减少数据读取量。我们有一个查询,只需要最新的100条数据,加了LIMIT 100之后,查询速度从几秒降到了几十毫秒。
第五个坑是注意数据类型的隐式转换。在查询条件中,如果字段类型和值的类型不匹配,ClickHouse会做隐式转换,这可能导致索引失效,查询变慢。比如,你的字段是String类型,但查询的时候用了数字来比较,ClickHouse会把字符串转换成数字,这时候就无法利用主键索引了。我们有一个查询,因为类型不匹配,本来应该走索引的,结果做了全表扫描,从几百毫秒变成了十几秒。所以,查询的时候一定要注意类型匹配,必要的时候用CAST函数做显式转换。
四、集群部署的坑
第四个大坑是集群部署。ClickHouse支持分布式集群,但部署和运维比单机复杂很多,坑也更多。
第一个坑是分片和副本的配置。ClickHouse的集群由多个分片(shard)组成,每个分片可以有多个副本(replica)。分片负责数据的水平拆分,副本负责数据的冗余备份。配置集群的时候,要根据数据量和查询需求来决定分片数和副本数。分片太多会导致查询的时候需要在多个节点上聚合,增加网络开销;副本太多会导致写入的时候需要同步到多个节点,影响写入速度。
我们一开始配置了4个分片2个副本,结果发现对于我们的数据量来说,2个分片就够了,4个分片导致小查询的网络开销偏大。后来调整成了2个分片2个副本,性能反而更好。所以,分片不是越多越好,要根据实际情况选择。
第二个坑是ZooKeeper的配置。ClickHouse的副本同步依赖ZooKeeper,ZooKeeper的性能和稳定性直接影响整个集群的表现。我们一开始把ZooKeeper和ClickHouse部署在同一台机器上,结果在高负载的时候,ZooKeeper因为资源不足而响应缓慢,导致副本同步延迟,甚至出现数据不一致。后来我们把ZooKeeper独立部署在专门的机器上,并且用了SSD磁盘,就没有这个问题了。
另外,ZooKeeper的节点数也很重要。一般来说,ZooKeeper集群用3个或者5个节点就够了,不要用太多,因为ZooKeeper是一致性协议,节点越多,写入的延迟越高。我们用的是3个节点的ZooKeeper集群,足够稳定和高效。
第三个坑是分布式表和本地表的区别。在ClickHouse集群中,你需要创建本地表(在每个节点上存储实际数据)和分布式表(作为查询入口,自动路由到各个节点)。查询的时候应该查分布式表,写入的时候也应该写分布式表,ClickHouse会自动把数据路由到对应的分片。但有些新手会直接查本地表,导致只能看到一个节点的数据,或者直接写本地表,导致数据分布不均匀。
我们一开始就犯了这个错误,数据直接写到了本地表,结果所有数据都写到了一个节点上,其他节点是空的。后来改成了写分布式表,数据就均匀分布了。另外,写入分布式表的时候,要注意分片键的选择,分片键应该是基数比较高的字段,这样数据才能均匀分布到各个分片。
第四个坑是集群的扩容。ClickHouse的集群扩容比较麻烦,因为它不像有些分布式数据库那样支持自动rebalance。增加分片之后,旧的数据不会自动迁移到新的分片上,只有新写入的数据会分布到所有分片。所以扩容之后,旧数据仍然集中在旧的分片上,导致负载不均匀。
我们扩容的时候就遇到了这个问题,加了两个新分片之后,旧数据还在原来的两个分片上,查询的时候旧分片的负载很高,新分片很闲。后来我们手动做了数据迁移,把旧数据重新导入到新的集群中,才解决了这个问题。所以,在一开始规划集群的时候,就要考虑好未来的扩容需求,尽量避免频繁扩容。如果确实需要扩容,要做好手动迁移数据的准备。
五、一些实用的建议
最后,分享一些实用的建议,都是我们在实际使用中总结出来的。
第一,做好监控。ClickHouse有丰富的系统表,可以监控查询性能、写入性能、合并状态、副本同步状态等。一定要建立完善的监控体系,及时发现问题。我们用Prometheus + Grafana监控ClickHouse的各种指标,包括查询延迟、写入速度、part数量、ZooKeeper状态等,出现异常的时候会自动告警。有了监控之后,很多问题都能在影响用户之前发现和解决。
第二,定期做数据备份。虽然ClickHouse有副本机制,但副本不能代替备份。因为副本只能防止硬件故障,不能防止人为误操作(比如误删数据)和软件bug。我们每天都会做一次全量备份,保留最近30天的备份。备份的方式可以用ClickHouse的ALTER TABLE ... FREEZE命令,或者直接复制数据目录。有了备份,出了问题心里就有底了。
第三,合理设置merge参数。ClickHouse的MergeTree引擎会在后台自动合并小的part,合并的速度和频率由参数控制。如果合并太慢,会导致part数量过多,影响查询性能;如果合并太快,会占用大量的IO和CPU,影响写入和查询。我们根据自己的负载情况,调整了merge的并发数和速度限制,找到了一个平衡点。一般来说,写入量大的时候要适当限制merge速度,避免影响写入;写入量小的时候可以加快merge,及时合并小part。
第四,善用system表排查问题。ClickHouse有很多system表,比如system.querylog(查询日志)、system.parts(数据片段)、system.replicas(副本状态)、system.merges(合并状态)等。出了问题的时候,先查这些system表,往往能快速定位问题。比如查询慢了,查system.querylog看看是哪个查询慢、慢在哪里;part太多了,查system.parts看看哪些分区的part多、为什么多;副本不同步了,查system.replicas看看同步延迟和队列长度。
第五,关注社区和版本更新。ClickHouse是一个非常活跃的开源项目,版本更新很快,每个版本都会修复很多bug,增加很多新功能。我们的经验是,尽量用比较新的稳定版本,因为旧版本的bug比较多,新版本的性能和稳定性都更好。当然,升级版本之前要做好测试,确保新版本和你的业务兼容。另外,ClickHouse的社区非常活跃,遇到问题可以在GitHub上提issue,或者在社区群里提问,通常能得到很快的响应。
六、写在最后
ClickHouse是一个非常优秀的OLAP引擎,它的查询速度确实令人惊艳,对于大数据量的分析场景来说,是一个非常好的选择。但它也不是银弹,有自己的适用场景和局限性。它不适合做事务性的操作(比如频繁的单行更新和删除),不适合做小数据量的查询(列式存储的开销反而更大),也不适合做复杂的多表JOIN。
在选择ClickHouse之前,一定要先搞清楚自己的业务场景是不是适合。如果你的场景是大数据量的追加写入、批量的聚合分析查询、不需要事务和频繁更新,那么ClickHouse非常适合。如果你的场景需要频繁的单行读写、复杂的事务、多表关联,那可能还是用传统的关系型数据库更合适。
使用ClickHouse的过程中,一定要深入理解它的原理和特性,不要用MySQL的思维来用它。表结构怎么设计、数据怎么导入、查询怎么写、集群怎么部署,每个环节都有ClickHouse自己的最佳实践。按照它的方式来用,你就能享受到它带来的极致性能;如果用错了方式,就会踩很多坑,甚至觉得它不好用。
希望我的这些踩坑经验能对大家有帮助。如果有什么问题或者不同的看法,欢迎在评论区交流。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录