2026/1/10 2:35:16
网站建设
项目流程
长沙岳麓区广告公司,百度seo查询收录查询,上海二手房,门户网站建设公开情况自查前言
去年公司要搞一个数据分析平台#xff0c;日志量大概一天3亿条#xff0c;保留90天#xff0c;总共约300亿条数据。之前用MySQL分库分表勉强扛着#xff0c;但复杂查询动不动就几十秒#xff0c;业务那边已经骂了好几次。
调研了一圈#xff0c;Druid、Doris、Click…前言去年公司要搞一个数据分析平台日志量大概一天3亿条保留90天总共约300亿条数据。之前用MySQL分库分表勉强扛着但复杂查询动不动就几十秒业务那边已经骂了好几次。调研了一圈Druid、Doris、ClickHouse最后选了ClickHouse。原因很简单单机性能强悍运维相对简单社区活跃。用了一年多踩了不少坑也总结了一些优化经验。这篇文章分享出来希望能帮到同样在做数据分析的朋友。ClickHouse是什么一句话列式存储的OLAP数据库专门用来做数据分析写进去快查得更快。和MySQL这类行存数据库的核心区别行存一行数据存在一起适合CRUD单条记录列存同一列的数据存在一起适合聚合统计举个例子有张100列的表查询只涉及3列MySQL要把整行读出来再挑3列ClickHouse直接只读3列的数据IO少一个数量级。环境搭建单机版很简单# Ubuntu/Debianapt-getinstall-y apt-transport-https ca-certificates apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 8919F6BD2B48D754echodeb https://packages.clickhouse.com/deb stable main/etc/apt/sources.list.d/clickhouse.listapt-getupdateapt-getinstall-y clickhouse-server clickhouse-client# 启动systemctl start clickhouse-server# 连接clickhouse-client默认配置文件在/etc/clickhouse-server/主要改这几个!-- config.xml --max_memory_usage10000000000/max_memory_usage!-- 单查询内存限制10G --max_threads8/max_threads!-- 查询并行度 --建表选对引擎很重要ClickHouse有几十种引擎生产环境90%用MergeTree家族。基础MergeTreeCREATETABLElogs(event_dateDate,event_timeDateTime,user_id UInt64,actionString,duration UInt32,city String,device String)ENGINEMergeTree()PARTITIONBYtoYYYYMM(event_date)ORDERBY(user_id,event_time)SETTINGS index_granularity8192;几个关键点PARTITION BY分区键按月分区是常见做法。查询时指定月份可以跳过无关分区。ORDER BY排序键决定数据物理存储顺序。查询最常用的筛选条件要放进去。index_granularity索引粒度默认8192行一个索引项。选错ORDER BY的代价第一版我把ORDER BY写成了(event_time, user_id)结果按user_id查询特别慢。原因ORDER BY决定数据排列顺序也决定稀疏索引的结构。user_id放第二位按它查需要扫描大量数据块。改成(user_id, event_time)后按user_id查询速度提升10倍以上。原则把等值查询最常用的列放前面范围查询的列放后面。写入优化批量写入是基本ClickHouse的写入不是针对小事务设计的。每次INSERT都会生成一个数据part小批量频繁写入会产生大量parts影响查询性能。# 错误方式一条条插forrowindata: insert(row)# 正确方式批量插# 至少1000条起批建议1万~10万条一批insert_batch(data[:10000])官方建议每批次不少于1000行每秒写入不超过1次。用Buffer引擎缓冲如果必须高频小批量写入用Buffer引擎CREATETABLElogs_bufferASlogsENGINEBuffer(currentDatabase(),logs,16,-- num_layers10,100,-- min_time, max_time10000,1000000,-- min_rows, max_rows10000000,100000000-- min_bytes, max_bytes);数据先写Buffer表积攒到一定量或时间后自动flush到主表。实际写入架构我们的架构是 Kafka - Flink - ClickHouseKafka (原始日志) | v Flink (聚合、清洗) | v ClickHouse (每10秒一批每批约50万条)Flink里做时间窗口聚合积攒10秒的数据一次写入避免小批量问题。查询优化看执行计划EXPLAINSELECTuser_id,count()FROMlogsWHEREevent_date2024-12-25ANDcityBeijingGROUPBYuser_id;关注点有没有用到分区裁剪有没有用到主键索引扫描了多少行更详细的EXPLAINPIPELINESELECT...能看到每个处理阶段的执行计划。分区裁剪-- 好指定分区SELECT*FROMlogsWHEREevent_date2024-12-01ANDevent_date2024-12-31-- 差分区列加函数SELECT*FROMlogsWHEREtoYYYYMM(event_date)202412第二种写法分区裁剪失效会扫全表。利用主键索引ORDER BY的列才有稀疏索引-- 表定义 ORDER BY (user_id, event_time)-- 快user_id等值查询精准定位SELECT*FROMlogsWHEREuser_id12345-- 较快user_id event_time范围SELECT*FROMlogsWHEREuser_id12345ANDevent_time2024-12-01-- 慢跳过user_id直接查event_timeSELECT*FROMlogsWHEREevent_time2024-12-01避免SELECT *列存数据库的优势就是只读需要的列-- 差读所有列SELECT*FROMlogsWHEREuser_id12345-- 好只读需要的列SELECTuser_id,action,durationFROMlogsWHEREuser_id12345100列的表读3列和读100列IO差距巨大。prewhere代替whereSELECT*FROMlogs PREWHERE event_date2024-12-25-- 先过滤WHEREcityBeijing-- 再过滤prewhere在读取其他列之前先过滤减少需要读取的数据量。过滤性强的条件用prewhere。物化视图预聚合高频查询的聚合结果提前算好-- 创建物化视图按天按城市预聚合CREATEMATERIALIZEDVIEWlogs_daily_cityENGINESummingMergeTree()PARTITIONBYtoYYYYMM(event_date)ORDERBY(event_date,city)ASSELECTevent_date,city,count()ascnt,sum(duration)astotal_durationFROMlogsGROUPBYevent_date,city;新数据写入logs时自动聚合到物化视图。查询日/城市维度的汇总直接查物化视图速度快几十倍。跳数索引对于不在ORDER BY里的列可以加跳数索引-- 给city列加布隆过滤器索引ALTERTABLElogsADDINDEXidx_city cityTYPEbloom_filter GRANULARITY4;-- 给duration加minmax索引ALTERTABLElogsADDINDEXidx_duration durationTYPEminmax GRANULARITY4;bloom_filter适合等值查询minmax适合范围查询。几个踩过的坑坑1String类型太慢最开始action、city这些列都用String后来发现GROUP BY特别慢。改用LowCardinalityALTERTABLElogsMODIFYCOLUMNcity LowCardinality(String);ALTERTABLElogsMODIFYCOLUMNactionLowCardinality(String);LowCardinality内部是字典编码对于基数低不同值少的列存储和计算都更高效。我们改完后相关查询快了3倍。坑2不小心全表扫描线上出过一次事故有人写了个没有where条件的count查询直接把CPU打满了。解决设置查询限制-- 用户级别限制CREATEUSERanalyst SETTINGS max_rows_to_read100000000;-- 查询级别限制SETmax_rows_to_read100000000;SELECT...坑3JOIN性能ClickHouse的JOIN不是强项大表JOIN特别慢。优化方向小表放右边会被广播到所有节点用字典代替维度表JOIN数据预先宽表化避免JOIN-- 创建字典CREATEDICTIONARY city_dict(city_id UInt32,city_name String,province String)PRIMARYKEYcity_id SOURCE(CLICKHOUSE(HOSTlocalhostPORT9000USERdefaultTABLEcity_dimDBdefault))LIFETIME(MIN300MAX600)LAYOUT(HASHED());-- 用字典代替JOINSELECTdictGet(city_dict,city_name,city_id)ascity_name,count()FROMlogsGROUPBYcity_id;坑4磁盘打满日志数据量太大磁盘很快就满了。配置TTL自动清理ALTERTABLElogsMODIFYTTL event_dateINTERVAL90DAY;90天前的数据自动删除。也可以配置冷热分层旧数据迁移到便宜的存储storage_configurationdiskshotpath/data/clickhouse/hot//path/hotcoldpath/data/clickhouse/cold//path/cold/diskspoliciestieredvolumeshotdiskhot/disk/hotcolddiskcold/disk/cold/volumesmove_factor0.1/move_factor/tiered/policies/storage_configuration监控与运维系统表查性能-- 查询日志SELECTquery,read_rows,read_bytes,result_rows,memory_usage,query_duration_msFROMsystem.query_logWHEREtypeQueryFinishORDERBYquery_start_timeDESCLIMIT20;-- 慢查询SELECT*FROMsystem.query_logWHEREquery_duration_ms10000ORDERBYquery_start_timeDESC;-- 各表大小SELECTtable,formatReadableSize(sum(bytes_on_disk))assize,sum(rows)asrowsFROMsystem.partsWHEREactiveGROUPBYtableORDERBYsum(bytes_on_disk)DESC;常规运维操作-- 手动合并parts一般不需要后台自动做OPTIMIZETABLElogs FINAL;-- 查看partsSELECTtable,partition,name,rows,bytes_on_diskFROMsystem.partsWHEREtablelogs;-- 删除分区ALTERTABLElogsDROPPARTITION202401;多节点管理多个ClickHouse节点时我会用星空组网把它们串到一个虚拟网络里维护的时候不用一台台跳。特别是跨机房部署的时候组到一起后用Ansible批量执行命令很方便。性能数据参考分享一下我们的实际数据单表300亿行约8TB存储压缩后8核32G的机器简单聚合查询单分区100ms以内复杂聚合跨月、多维度3-10秒物化视图查询50ms以内相比之前MySQL分库分表的方案复杂查询从几十秒降到几秒业务那边再也没骂过。总结ClickHouse的使用核心就几点建表ORDER BY要根据查询模式设计写入批量写别小批量频繁写查询利用好分区裁剪和主键索引只读需要的列预计算高频查询用物化视图它不是万能的OLTP场景别用JOIN重的场景谨慎用。但在OLAP场景特别是日志分析、用户行为分析、实时报表这些方向确实是目前性价比最高的选择之一。刚开始用的时候可能觉得概念多用熟了会发现套路就那些。希望这篇文章能帮你少踩点坑。