当前位置: 首页 > news >正文

ClickHouse核心架构设计

列式存储原理与数据压缩

列式存储原理
  • 列式存储 vs 行式存储
    特性行式存储(如MySQL)列式存储(ClickHouse)
    数据排列按行连续存储(所有字段相邻)按列连续存储(单列数据紧密排列)
    适用场景OLTP:频繁单行读写、事务操作OLAP:批量读取、聚合计算、复杂分析
    I/O效率读取整行,即使只需少量字段(I/O浪费)仅读取查询涉及的列(减少I/O,提升吞吐)
    压缩潜力低(不同数据类型相邻,压缩率低)高(同列数据类型一致,重复模式多,压缩率高)
  • 列式存储的核心优势

    • 减少I/O开销:仅加载必要列数据,降低磁盘和网络带宽压力。
    • 向量化处理:同列数据连续存储,可利用SIMD指令并行处理(如批量计算SUM(price))。
    • 高效压缩:同列数据重复性高,可采用针对性的压缩算法(如字典编码、Delta编码)。
    • 延迟物化:在压缩数据上直接计算,减少内存占用(如聚合前不解压字符串)。
数据压缩机制
  • 压缩算法选择
    算法压缩速度解压速度压缩率适用场景
    LZ4极快极快中等默认选择,通用场景
    ZSTD极快存储成本敏感,CPU资源充足
    ZLIB中等历史兼容,不推荐新项目使用
  • 压缩优化策略

    • 自适应压缩级别:ZSTD允许动态调整压缩级别(1-22),平衡速度与压缩率。
    • 列级压缩策略:不同列可独立配置压缩算法(如文本列用ZSTD,数值列用LZ4)。
    • 字典编码:对低基数列(如country枚举值)自动构建字典,用整数代替原始值存储。
    • Delta压缩:对有序数值列(如时间戳、自增ID),存储差值而非原始值,大幅减少存储空间。

向量化执行引擎

传统行式执行 vs 向量化执行
特性行式执行(Row-based)向量化执行(Vectorized)
处理单位单行数据(逐行处理)数据块(Block,包含多行数据的列式集合)
CPU指令优化频繁分支跳转,无法利用SIMD指令按列连续处理,触发SIMD指令并行计算
缓存利用率低(随机访问不同列数据)高(连续访问单列数据,缓存命中率高)
适用场景OLTP(短事务、单行操作)OLAP(批量计算、聚合分析)
  • 行式执行:逐行计算price * quantity,循环次数多,无法并行。
  • 向量化执行:将pricequantity两列数据分别加载到寄存器,通过SIMD指令批量计算。
ClickHouse的向量化实现
  • 数据块(Block)结构
    • 处理单元:每个Block包含多行数据(如默认8192行),按列存储(与内存中的列式布局一致)。
    • 内存连续:单列数据在内存中连续排列,便于SIMD指令加载(如一次读取256位寄存器数据)。
    • 类型感知:Block内数据类型明确(如ColumnUInt32),避免运行时类型判断开销。
  • SIMD指令加速

    • SIMD(Single Instruction Multiple Data):单条指令处理多个数据,例如:

      AVX-512指令集:单指令处理16个Int32或8个Double

      适用操作:过滤(WHERE)、聚合(SUM)、数学运算(sqrt)、哈希计算等。

    • 自动向量化:ClickHouse编译器(CLang)自动将循环展开为SIMD指令,或手动内联汇编优化关键路径。
  • 零拷贝与惰性物化
    • 零拷贝(Zero-Copy) :在Block之间传递数据时,仅传递指针而非复制数据,减少内存开销。
    • 惰性物化(Lazy Materialization) :延迟生成中间结果,仅在必要时解压或转换数据。
向量化执行的优势
  • 性能提升

    • 吞吐量:相比行式引擎,向量化执行在聚合查询中可提升 5-50倍 性能。
    • 资源效率:减少CPU指令分支预测失败和缓存失效,提高计算密度。
  • 典型优化场景

    • 聚合函数sumavgcount等统计计算。
    • 过滤操作WHERE条件中的批量判断(如WHERE value > 100)。
    • 排序与JOIN:向量化哈希表加速JOIN,SIMD加速排序比较。
向量化执行的实现细节
  • 列式内存布局

    • 列数据连续存储:每列数据在内存中连续分配,便于SIMD指令批量加载。
    • 数据类型优化:使用紧凑数据类型(如UInt8代替String枚举)提升寄存器利用率。
  • 查询计划优化

    • Pipeline执行模式:将查询分解为多个Stage,每个Stage处理一个Block流,减少中间结果内存占用。
    • 并行流水线:多个Pipeline并行执行,充分利用多核CPU(如max_threads参数控制并发度)。
  • JIT编译优化(实验性)

    • 动态代码生成:对热点循环(如聚合函数)生成机器码,绕过虚函数调用开销。
    • LLVM优化:使用LLVM编译器进一步优化生成的代码(需启用compile_expressions配置)。

分布式计算模型(分片与副本机制)

分片(Sharding)机制
  • 目标:将数据水平拆分到多个物理节点,突破单机存储与计算瓶颈。

  • 分片键:通过分片键(如用户ID、时间戳)决定数据分布策略,支持以下两种模式:

    • 显式分片:用户手动指定数据分布规则。
    • 隐式分片:通过哈希函数自动分配数据(如rand()随机分布)。
  • 分片策略
    分片类型实现方式适用场景
    哈希分片对分片键(如user_id)计算哈希值,按模运算分配节点。数据分布均匀,避免热点。
    范围分片按分片键的范围(如时间范围2023-01-01 ~ 2023-06-30)分配节点。时序数据,按时间分区查询。
    自定义分片通过用户定义的规则(如user_id % 10 < 5)将数据映射到指定分片。复杂业务逻辑(如多租户隔离)。
  • 分片写入与查询

    • 写入流程

      客户端向任意节点发送写入请求。

      分布式表根据分片键计算目标分片节点。

      数据路由到对应节点的本地表(如local_table)存储。

    • 查询流程

      查询发送到协调节点(任意节点)。

      协调节点将查询分发到所有相关分片。

      各分片并行执行查询,返回中间结果。

      协调节点汇总结果并返回最终数据。

副本(Replication)机制
  • 副本的核心思想

    • 目标:在多个节点保存相同数据副本,实现高可用故障恢复
    • 实现方式:基于异步复制,通过ReplicatedMergeTree引擎实现数据同步。
  • 副本工作机制

    • 依赖组件:ZooKeeper(存储元数据与复制日志,协调副本间状态)。
    • 数据同步流程

      写入请求发送到主副本节点。

      主副本将操作日志(如插入、合并)写入ZooKeeper。

      从副本监听ZooKeeper日志,拉取并应用变更。

      副本间周期性校验数据一致性。

MergeTree引擎的底层逻辑

数据写入与存储结构
  • 数据写入流程

    • 内存缓冲:数据先写入内存缓冲区(Memory引擎的临时结构)。
    • 生成数据块(Part) :缓冲区满或手动触发时,数据按列压缩并写入磁盘,形成独立的数据块(Part)。
    • 数据块特征

      每个Part包含数据文件(.bin)、索引文件(.idx)和元数据(checksums.txt)。

      Part按写入顺序命名(如20231001_1_2_0),命名规则为{partition_id}_{min_block}_{max_block}_level

  • 存储目录结构

    # 示例表目录结构
    /user_behavior/
    └── 20231001_1_1_0/         # 数据Part├── event_time.bin      # 列数据文件(压缩)├── event_time.mrk2     # 列标记文件(定位数据位置)├── primary.idx         # 主键索引文件└── checksums.txt       # 校验和元数据
    
数据合并机制(Merge)
  • 合并触发条件

    • 后台线程周期性触发:默认每10分钟检查一次可合并的Part。
    • 合并规则:合并相邻的、时间范围重叠的Part;合并后生成更大的有序Part,减少碎片化。
  • 合并过程

    • 选择候选Part:选取多个小Part(如20231001_1_1_020231001_2_2_0)。
    • 重新排序:按主键(ORDER BY字段)重新排序数据,生成全局有序的新Part。
    • 更新元数据:旧Part标记为过期,新Part生效(原子性操作)。
  • 合并策略优化

    • 层级合并(Leveled Merge)level值表示合并次数(如20231001_1_2_1中的1);高层级Part优先合并,减少合并次数。
    • TTL(Time to Live) :自动删除过期数据(如保留最近30天数据)。
主键索引与数据查询加速
  • 主键(PRIMARY KEY)设计

    • 主键定义:通过ORDER BY字段隐式定义主键(默认与ORDER BY相同,可独立指定)。
    • 主键作用:确定数据在磁盘上的物理排序顺序;生成稀疏索引(.idx文件),加速范围查询。
  • 稀疏索引原理

    • 索引粒度(index_granularity) :默认每8192行生成一个索引条目。
    • 索引结构:存储每个索引粒度的主键最小值(如event_time的最小值)。
    • 查询流程:根据查询条件定位主键索引的起始和结束位置;跳过不满足条件的数据块(Mark文件定位具体列数据位置)。
跳数索引(Skipping Index)
  • 跳数索引的作用

    • 辅助过滤:在主键索引基础上,进一步加速非主键字段的查询(如event_type)。
    • 索引类型:支持minmaxsetbloom_filter等算法。
  • 索引工作原理

    • 生成阶段:在数据合并时生成跳数索引。
    • 查询阶段:根据索引快速判断数据块是否包含目标值(如event_type = 'click')。
分区管理(PARTITION BY)
  • 数据裁剪:按分区字段(如时间)快速过滤无关数据块。
  • 生命周期管理:按分区删除过期数据(如ALTER TABLE ... DROP PARTITION)。
最佳实践
  • 主键设计:选择高频查询字段,确保主键前缀能覆盖大多数查询条件;避免主键过多字段(1-3列),减少索引冗余。
  • 分区策略:按时间分区(如PARTITION BY toYYYYMM(event_time));单个分区数据量控制在10GB-100GB,避免分区过多。
  • 跳数索引优化:高基数字段(如user_id)使用bloom_filter索引;低基数字段(如status)使用setminmax索引。
  • 监控与维护:定期检查system.parts表,合并小Part(OPTIMIZE TABLE FINAL);定期清理过期数据。

相关文章:

  • day47——平方数之和(LeetCode-633)
  • STM32(M4)入门:GPIO与位带操作(价值 3w + 的嵌入式开发指南)
  • FFmpeg:M3U8的AES加密
  • 《Android 应用开发基础教程》——第三章:布局管理与 UI 组件详解
  • 多模态大语言模型arxiv论文略读(三十一)
  • 机器学习 Day12 集成学习简单介绍
  • [Windows]_[VS2017]_[如何进行远程调试程序]
  • POSIX标准系统调用详解:从概念到实践
  • 破解吞咽困境!进行性核上性麻痹患者的科学饮食方案
  • 62页华为IPD-MM流程:市场调研理论与实践方案精读【附全文阅读】
  • Linux 网络基础(二) (传输协议层:UDP、TCP)
  • 【算法提高】单源最短路的建图方式
  • Linux系统编程---孤儿进程与僵尸进程
  • UML统一建模
  • Vue常用指令入门
  • 【项目实训个人博客】数据集搜集
  • 【python】尾部多写个逗号会把表达式变成 tuple
  • 使用virtualbox的HostOnly建立共享网络-实现虚拟机上网
  • 面向对象编程的四大特性详解:封装、继承、多态与抽象
  • React 自定义Hook之usePrevious
  • 民政部:从未设立或批准设立“一脉养老”“惠民工程”项目,有关App涉嫌诈骗
  • 宁波一季度GDP为4420.5亿元,同比增长5.6%
  • 2025年上海车展后天开幕,所有进境展品已完成通关手续
  • 全球在役最大火电厂被通报
  • 商务部24日下午将举行发布会,介绍近期商务领域重点工作情况
  • 特写|为何这么多人喜欢上海半马,答案藏在他们的笑容里