|
| 1 | +# 为啥要学习ClickHouse |
| 2 | + |
| 3 | +## 1 前言 |
| 4 | + |
| 5 | +技术选型时,价格也是重要影响因素。ES虽用着方便,却有大量硬件资源损耗,再富有公司,看到每月服务器账单也心疼。 |
| 6 | + |
| 7 | +而ck新生代OLAP,尝试很多有趣实现,虽很多不足,如: |
| 8 | + |
| 9 | +- 不支持数据更新 |
| 10 | +- 动态索引较差 |
| 11 | +- 查询优化难度高 |
| 12 | +- 分布式需手动设计 |
| 13 | + |
| 14 | +但架构简单,相对廉价,逐渐得到很多团队认同,很多互联网企业加入社区,不断优化ck。 |
| 15 | + |
| 16 | +ck属列式存储DB,多用于写多读少,灵活的分布式存储引擎,还有分片、集群等多种模式,可按需选择。 |
| 17 | + |
| 18 | +本文从写入、分片、索引、查询等方面认识ck,注意对比ES、MySQL、RocksDB优缺点和适用场景。 |
| 19 | + |
| 20 | +## 2 并行能力CPU吞吐和性能 |
| 21 | + |
| 22 | +MySQL处理一个SQL请求只能利用一个CPU。但ck会充分利用多核,对本地大量数据做快速计算,因此有更高数据处理能力(2~30G/s,未压缩数据),但这也导致并发不高,因为一个请求就能用尽系统资源。 |
| 23 | + |
| 24 | +刚使用ck,经常碰到查几年的用户行为时,一个SQL就将整个ck卡住,几min都无响应。官方默认ck查询QPS限制在100,若查询索引设置得好,几十上百亿数据可在1s内数据统计返回。而MySQL至少需1min以上;但若ck查询设计不好,可能等0.5h还没计算完,甚至卡死。 |
| 25 | + |
| 26 | +所以,使用ck的场景若是对用户服务,最好对这种查询做缓存。且界面在加载时要设置30s以上等待时间,因为我们的请求可能在排队等待别的查询。 |
| 27 | + |
| 28 | +若用户量大,建议多放些节点用分区、副本、相同数据子集群来分担查询计算的压力。不过,考虑到若想提供1w QPS查询,极端情况下需100台ck存储同样数据,所以推荐还是尽量用脚本推送数据结果到缓存中对外服务。 |
| 29 | + |
| 30 | +但若集群都是小数据,且能保证每次查询可控,ck能支持上万QPS,这取决于投入多少时间做优化分析。 |
| 31 | + |
| 32 | +## 3 优化思路 |
| 33 | + |
| 34 | +基于排序字段做范围查询过滤后,再聚合查询。 |
| 35 | + |
| 36 | +- 需要【高并发查询数据的服务】 |
| 37 | +- 和【缓慢查询的服务】 |
| 38 | + |
| 39 | +需隔离,才能提供更好性能。 |
| 40 | + |
| 41 | +## 4 批量写入优化 |
| 42 | + |
| 43 | +ck客户端驱动很有趣,客户端会有多个写入数据缓存,批量插入数据时,客户端会将要insert数据先在本地缓存,直到积累足够配置的block_size,才把数据批量提交服务端,以提高写性能。若实时性要求高,block\_size可设置小点,代价就是性能变差些。 |
| 44 | + |
| 45 | +为优化高并发写服务,除了客户端做的合并,ck的引擎也做类似工作 |
| 46 | + |
| 47 | +### 4.1 MergeTree |
| 48 | + |
| 49 | +单ck批量写性能达280M/s(受硬件性能及输入数据量影响)。 |
| 50 | + |
| 51 | +MergeTree采用批量写盘、定期合并(batch write-merge),让人想起写性能强的RocksDB。ck刚出来时,没用内存进行缓存,而是直接写盘。 |
| 52 | + |
| 53 | +最近两年ck更新,才实现类似内存缓存及WAL日志。所以,若你用ck,建议搭配高性能SSD作写入磁盘存储。 |
| 54 | + |
| 55 | +### 4.2 OLAP数据来源 |
| 56 | + |
| 57 | +- 业务系统 |
| 58 | + |
| 59 | + 属性字段多,但平时更新量不大。使用ck常是为做历史数据筛选和属性共性的计算 |
| 60 | + |
| 61 | +- 大数据 |
| 62 | + |
| 63 | + 通常有很多列,每列代表不同用户行为,数据量很大 |
| 64 | + |
| 65 | +两种情况的数据量不同,优化方式自然不同,ck咋优化这两种? |
| 66 | + |
| 67 | + |
| 68 | + |
| 69 | +批量输入的数据量<min_bytes_for_wide_part,按compact part落盘。这会将落盘的数据放到一个data.bin文件,merge时有很好的写效率,适合小量业务数据筛选。 |
| 70 | + |
| 71 | +批量输入的数据量超过配置规定的大小时,按wide part落盘,落盘数据会按字段,生成不同文件。适用字段较多的数据,merge相对慢,但对指定参与计算列的统计计算,并行吞吐写入和计算能力更强,适合分析指定小范围的列计算。 |
| 72 | + |
| 73 | +两种方式对数据存储、查询很有针对性,可见字段多少、每次的更新数据量、统计查询时参与的列个数,都影响服务效率: |
| 74 | + |
| 75 | +- 大部分数据都是小数据,一条数据拆分成多列有些浪费磁盘IO,因为是小量数据,也不会给他太多机器,推荐compact parts |
| 76 | +- 数据列很大,需对某几列数据统计分析时,wide part列存储更优 |
| 77 | + |
| 78 | +## 5 咋提高查询效率 |
| 79 | + |
| 80 | +可见, DB的存储和数据如何使用、如何查询息息相关。不过,这种定期落盘虽有很好写性能,却产生大量data part文件,对查询效率很影响。那ck咋提高查询效率? |
| 81 | + |
| 82 | +再分析,新写入的parts数据保存在data parts文件夹内,数据一旦写入数据内容,就不会再更改。 |
| 83 | + |
| 84 | +一般data part的文件夹名格式为 partition(分区)_min_block_max_block_level,为提高查询效率,ck会对data part定期merge合并。 |
| 85 | + |
| 86 | + |
| 87 | + |
| 88 | +merge分层进行,期间会减少要扫描的文件夹个数,对数据进行整理、删除、合并。不同分区无法合并,所以若想提高一个表的写性能,多划几个分区。 |
| 89 | + |
| 90 | +若写数据量太大&&数据写速度太快,导致:产生文件夹的速度会>后台合并速度,ck就报Too many part,毕竟data parts文件夹个数不能无限增加。 |
| 91 | + |
| 92 | +此时,调整min\_bytes\_for\_wide\_part或增加分区都可改善。若写数据量不大,可考虑多生成compact parts数据,可加快合并速度。 |
| 93 | + |
| 94 | +因为分布式的ck表基于zk做分布式调度,所以表数据一旦写并发过高,zk就成瓶颈。此时,建议升级ck,新版本支持多组zk,不过这也意味着要投更多资源。 |
| 95 | + |
| 96 | +## 6 稀疏索引与跳数索引 |
| 97 | + |
| 98 | +查询功能离不开索引支持。ck有两种索引方式: |
| 99 | + |
| 100 | +- 主键索引,建表时需指定 |
| 101 | +- 跳表索引,以跳过一些数据 |
| 102 | + |
| 103 | +查询更推荐主键索引。 |
| 104 | + |
| 105 | +### 6.1 主键索引 |
| 106 | + |
| 107 | +ck表使用主键索引,才能让数据查询有更好性能,因为数据和索引会按主键排序存储,用主键索引查询可很快处理数据并返回结果。ck属左前缀查询: |
| 108 | + |
| 109 | +- 通过索引和分区,先快速缩小数据范围 |
| 110 | +- 再遍历计算,只不过遍历计算是多节点、多CPU并行处理 |
| 111 | + |
| 112 | +ck咋数据检索的?先了解 |
| 113 | + |
| 114 | +#### data parts文件夹内的主要数据组成 |
| 115 | + |
| 116 | + |
| 117 | + |
| 118 | +按从大到小看data part目录结构。 |
| 119 | + |
| 120 | +data parts文件夹中,bin文件里保存一或多个字段的数据。继续拆分bin文件,它里面是多个block数据块,block是磁盘交互读取的最小单元,其大小取决于min\_compress\_block\_size设置。 |
| 121 | + |
| 122 | +#### block内的结构 |
| 123 | + |
| 124 | +它保存多个granule(颗粒),这是数据扫描的最小单位。每个granule默认会保存8192行数据,其中第一条数据就是主键索引数据。data part文件夹内的主键索引,保存了排序后的所有主键索引数据,而排序顺序是创建表时就指定好的。 |
| 125 | + |
| 126 | +- 为加快查询速度,data parts内的主键索引(即稀疏索引)会被加载在内存 |
| 127 | +- 为配合快速查找数据在磁盘位置,ck在data part文件夹中,会保存多个按字段名命名的mark文件,这文件保存的是bin文件中压缩后的block的offset及granularity在解压后block中的offset |
| 128 | + |
| 129 | +整体查询效果: |
| 130 | + |
| 131 | + |
| 132 | + |
| 133 | +#### 查询过程 |
| 134 | + |
| 135 | +- 先二分查找内存里的主键索引,定位到特定mark文件 |
| 136 | +- 再根据mark查找到对应block,将其加载到内存 |
| 137 | +- 在block里找到指定granule开始遍历加工,直到查到所需数据 |
| 138 | + |
| 139 | +由于ck允许同一个主键多次Insert,查出数据可能发生同一个主键数据出现多次,需人工对查询结果去重。 |
| 140 | + |
| 141 | +### 6.2 跳数索引 |
| 142 | + |
| 143 | +ck除了主键,没有其他索引,导致无法用主键索引的查询统计场景,都要扫全表,但DB通常每天保存几十~几百亿数据,这么做性能很差。 |
| 144 | + |
| 145 | +因此,性能抉择,ck反向思维,设计跳数索引减少遍历granule的资源浪费。 |
| 146 | + |
| 147 | +#### 常见方式 |
| 148 | + |
| 149 | +- min\_max:辅助数字字段范围查询,保存当前矩阵内最大最小数 |
| 150 | +- set:列出字段内所有出现的枚举值,可设置取多少条 |
| 151 | +- Bloom Filter:确认数据有没有可能在当前块 |
| 152 | +- func:支持很多where条件内的函数,详参[官网](https://ck.com/docs/en/engines/table-engines/mergetree-family/mergetree/#table_engine-mergetree-data_skipping-indexes)。 |
| 153 | + |
| 154 | +跳数索引按上面提到类型和对应字段,保存在data parts文件夹,跳数索引不是减少数据搜索范围,而是排除不符筛选条件的granule,以加快查询速度。 |
| 155 | + |
| 156 | +## 7 整体看ck的查询工作流程 |
| 157 | + |
| 158 | +1. 根据查询条件,查询过滤出要查询需要读取的data part 文件夹范围 |
| 159 | +2. 根据data part 内数据的主键索引、过滤出要查询的granule |
| 160 | +3. 使用skip index 跳过不符合的granule |
| 161 | +4. 范围内数据进行计算、汇总、统计、筛选、排序 |
| 162 | +5. 返回结果 |
| 163 | + |
| 164 | +只有step 4的几个操作是并行,其他流程都串行。 |
| 165 | + |
| 166 | +用ck后,会发现很难对它做索引查询优化,动不动就扫全表,why?主要是大部分数据特征不明显、建立的索引区分度不够。导致写入的数据,在每个颗粒内区分度不大,通过稀疏索引的索引无法排除大多数颗粒,最终ck只能扫描全表计算。 |
| 167 | + |
| 168 | +因为目录过多,有多份数据同时散落在多个data parts文件夹内,ck需加载所有date part的索引挨个查询,这也消耗很多资源。这两个原因导致ck很难做查询优化,若输入数据很有特征,且特征数据插入时,能按特征排序顺序插入,性能可能更好。 |
| 169 | + |
| 170 | +### 7.1 实时统计 |
| 171 | + |
| 172 | +ck往往要扫全表才做统计,导致其指标分析功能不太友好,为此官方提供另一个引擎。 |
| 173 | + |
| 174 | +内存计算,ck能将自己的表作为数据源,再创建一个Materialized View的表,View表会将数据源的数据通过聚合函数实时统计计算,每次我们查询这个表,就能获得表规定的统计结果。 |
| 175 | + |
| 176 | +### 7.2 案例 |
| 177 | + |
| 178 | +```sql |
| 179 | +-- 创建数据源表 |
| 180 | + |
| 181 | +CREATE TABLE products_orders |
| 182 | + |
| 183 | +( |
| 184 | + |
| 185 | + prod_id UInt32 COMMENT '商品', |
| 186 | + |
| 187 | + type UInt16 COMMENT '商品类型', |
| 188 | + |
| 189 | + name String COMMENT '商品名称', |
| 190 | + |
| 191 | + price Decimal32(2) COMMENT '价格' |
| 192 | + |
| 193 | +) ENGINE = MergeTree() |
| 194 | +ORDER BY (prod_id, type, name) |
| 195 | +PARTITION BY prod_id; |
| 196 | +--创建 物化视图表 |
| 197 | +CREATE MATERIALIZED VIEW product_total |
| 198 | +ENGINE = AggregatingMergeTree() |
| 199 | +PARTITION BY prod_id |
| 200 | +ORDER BY (prod_id, type, name) |
| 201 | +AS |
| 202 | +SELECT prod_id, type, name, sumState(price) AS price |
| 203 | +FROM products_orders |
| 204 | +GROUP BY prod_id, type, name; |
| 205 | +-- 插入数据 |
| 206 | +INSERT INTO products_orders VALUES |
| 207 | +(1,1,'过山车玩具', 20000), |
| 208 | +(2,2,'火箭',10000); |
| 209 | +-- 查询结果 |
| 210 | +SELECT prod_id,type,name,sumMerge(price) |
| 211 | +FROM product_total |
| 212 | +GROUP BY prod_id, type, name; |
| 213 | +``` |
| 214 | + |
| 215 | +当数据源插入ck数据源表,生成data parts数据时,就会触发View表。View表会按我们创建时设置的聚合函数,对插入的数据做批量的聚合。每批数据都会生成一条具体的聚合统计结果并写入磁盘。 |
| 216 | + |
| 217 | +当我们查询统计数据时,ck会对这些数据再次聚合汇总,才能拿到最终结果对外做展示。这样就实现了指标统计,这个实现方式很符合ck的引擎思路,这很有特色。 |
| 218 | + |
| 219 | +## 8 分布式表 |
| 220 | + |
| 221 | +这部分实现还不成熟,所以我们把重点放在这个特性支持什么功能。 |
| 222 | + |
| 223 | +ck的分布式表,不像Elasticsearch那样全智能地帮我们分片调度,而是需要研发手动设置创建,虽然官方也提供了分布式自动创建表和分布式表的语法,但我不是很推荐,因为资源的调配目前还是偏向于人工规划,ck并不会自动规划,使用类似的命令会导致100台服务器创建100个分片,这有些浪费。 |
| 224 | + |
| 225 | +使用分布式表,就得先在不同服务器手动创建相同结构的分片表,同时在每个服务器创建分布式表映射,这样在每个服务上都能访问这个分布式表。 |
| 226 | + |
| 227 | +通常分片是同一个服务器可以存储多个分片,而ck并不一样,它规定一个表在一个服务器里只能存在一个分片。 |
| 228 | + |
| 229 | +ck的分布式表的数据插入,一般有两种方式: |
| 230 | + |
| 231 | +- 对分布式表插入数据,这样数据会先在本地保存,然后异步转发到对应分片,通过这个方式实现数据的分发存储 |
| 232 | +- 由客户端根据不同规则(如随机、hash),将分片数据推送到对应的服务器上。这样相对来说性能更好,但是这么做,客户端需要知道所有分片节点的IP。这不利于失败恢复。 |
| 233 | + |
| 234 | +为更好平衡高可用和性能,还是推荐你选择前一种方式。但是由于各个分片为了保证高可用,会先在本地存储一份,然后再同步推送,这很浪费资源。面对这种情况,我们比较推荐的方式是通过类似proxy服务转发一层,用这种方式解决节点变更及直连分发问题。 |
| 235 | + |
| 236 | +### 主从分片 |
| 237 | + |
| 238 | +ck的表是按表设置副本(主从同步),副本之间支持同步更新或异步同步。 |
| 239 | + |
| 240 | +主从分片通过分布式表设置在ZooKeeper内的相同路径来实现同步,这种设置方式导致ck的分片和复制有很多种组合方式,比如:一个集群内多个子集群、一个集群整体多个分片、客户端自行分片写入数据、分布式表代理转发写入数据等多种方式组合。 |
| 241 | + |
| 242 | +就是ck支持人为做资源共享的多租户数据服务。当我们扩容服务器时,需要手动修改新加入集群分片,创建分布式表及本地表,这样的配置才可以实现数据扩容,但是这种扩容数据不会自动迁移。 |
| 243 | + |
| 244 | +## 9 总结 |
| 245 | + |
| 246 | +Ck作为OLAP新秀代表,有很多独特设计,引起OLAP数据库的革命,也引发很多云厂商做出更多思考,参考它的思路实现HTAP服务。 |
| 247 | + |
| 248 | +- Ck通过分片及内存周期顺序落盘,提高写并发 |
| 249 | +- 通过后台定期合并data parts文件,提高了查询效率 |
| 250 | +- 索引方面,通过稀疏索引缩小了检索数据的颗粒范围,对于不在主键的查询,则是通过跳数索引来减少遍历数据的数据量 |
| 251 | +- ck还有多线程并行读取筛选的设计 |
| 252 | + |
| 253 | +这些特性,共同实现ck大吞吐的数据查找功能。 |
| 254 | + |
| 255 | +而最近选择 ES还是CK讨论火热,目前来看还没有彻底分出高下。 |
| 256 | + |
| 257 | +- 硬件资源丰富,研发人员少,就选ES |
| 258 | +- 硬件资源少,研发人员多,考虑试用ck |
| 259 | +- 硬件和人员都少,建议买云服务的云分布式数据库 |
| 260 | + |
| 261 | +### 评估表格 |
| 262 | + |
| 263 | + |
| 264 | + |
| 265 | +## FAQ |
| 266 | + |
| 267 | +ck不能轻易修改删除数据的,如何做历史数据的清理? |
| 268 | + |
| 269 | +ck不建议直接修改和删除数据,因其存储结构和查询引擎都是在只读数据的前提下进行优化。所以,要清理ck中的历史数据,有以下几种方法: |
| 270 | + |
| 271 | +1. 分区管理:在创建表时就按时间字段分区,定期删除过期分区的数据。这是最推荐的方法,可以最大限度利用ck的优化进行数据管理 |
| 272 | +2. TTL:在创建表时为表设置TTL。过期的数据会被自动清理 |
| 273 | +3. 定期重建表:可以定期创建一个新表,仅迁移需要保留的数据,重复利用表架构和分区方案。然后删除旧表,实现历史数据的清理 |
| 274 | +4. DETACH PARTITION:可用DETACH PARTITION语句将不需要的数据分区"切断",实现分区数据的清理。但是,需要主动调用这个语句,不是自动过期 |
| 275 | +5. 过滤器:在查询时加入时间范围的过滤条件,筛选出需要的历史数据进行查询分析,忽略过期的数据。这种方法不会真正清理数据,只是在查询使用上过滤数据。以上几种方法都可以在不同程度上实现ck历史数据的清理。但总的来说,通过表分区、TTL等主动过期历史数据的方式会更加彻底和自动化一些。过滤器的方式更加适用于少量指定时间范围内的历史数据查询和分析 |
| 276 | +6. ck不直接支持UPDATE和DELETE操作,是为最大化存储和查询性能。但它提供其他更适合其存储结构的数据管理方法,可实现比较彻底的历史数据清理 |
0 commit comments