CRUSH算法实现原理详细分析
目录
CRUSH算法概述
1.1 什么是CRUSH
CRUSH (Controlled Replication Under Scalable Hashing) 是Ceph中用于数据分布的核心算法。它是一个伪随机数据分布算法,能够高效地将输入值(通常是数据对象)分布到异构的、结构化的存储集群中。
1.2 核心特点
- 确定性:给定相同的输入,总是产生相同的输出
- 伪随机性:分布看起来是随机的,但实际上是确定性的
- 可扩展性:支持大规模集群
- 容错性:能够处理节点故障和恢复
- 权重支持:根据设备权重进行负载均衡
1.3 算法原理
CRUSH算法通过以下步骤将对象映射到OSD:
1 | 对象名 (object_name) |
1.4 关键概念
- Item:CRUSH层次结构中的节点,可以是设备(OSD)或桶(Bucket)
- Bucket:包含其他Item的容器,形成层次结构
- Rule:定义如何从层次结构中选择Item的规则序列
- Weight:Item的权重,用于负载均衡
- Type:Item的类型,用于故障域隔离
核心数据结构
2.1 crush_map
1 | struct crush_map { |
说明:
buckets:存储所有桶的指针数组,桶ID为负数(-1, -2, …)rules:存储所有规则的指针数组max_devices:最大设备ID + 1- 可调参数用于控制映射行为和稳定性
2.2 crush_bucket
1 | struct crush_bucket { |
说明:
id:桶的唯一标识符,必须为负数type:桶的类型,用于规则匹配(如rack、host等)alg:选择算法(uniform、list、straw2等)weight:所有子项的累积权重items:子项数组,负数表示桶,非负数表示设备
2.3 桶类型结构
2.3.1 Uniform Bucket
1 | struct crush_bucket_uniform { |
特点:
- 所有项目权重相同
- 选择速度最快 O(1)
- 添加/删除项目时数据移动较大
2.3.2 List Bucket
1 | struct crush_bucket_list { |
特点:
- 支持不同权重
- 选择速度 O(n)
- 添加项目时数据移动最优
- 删除项目时数据移动较大
2.3.3 Straw2 Bucket
1 | struct crush_bucket_straw2 { |
特点:
- 支持不同权重
- 选择速度 O(n)
- 添加/删除/重权重时数据移动最优
- 推荐使用的算法
2.4 crush_rule
1 | struct crush_rule { |
操作码类型:
CRUSH_RULE_TAKE:选择起始桶CRUSH_RULE_CHOOSE_FIRSTN:选择N个项目(深度优先)CRUSH_RULE_CHOOSE_INDEP:选择N个项目(广度优先)CRUSH_RULE_CHOOSELEAF_FIRSTN:选择N个叶子(深度优先)CRUSH_RULE_CHOOSELEAF_INDEP:选择N个叶子(广度优先)CRUSH_RULE_EMIT:输出结果
哈希函数
3.1 哈希函数实现
CRUSH使用Robert Jenkins的哈希函数(rjenkins1),位于hash.c:
1 | // 哈希混合函数 |
特点:
- 使用混合函数确保良好的分布
- 支持多个输入参数
- 确定性:相同输入产生相同输出
3.2 哈希函数使用
在CRUSH映射中,哈希函数用于:
- 桶内项目选择:
hash(x, bucket_id, r)选择桶内项目 - 排列生成:
hash(x, bucket_id, position)生成排列 - 权重检查:
hash(x, item)检查项目是否”out”
Bucket算法
4.1 Uniform Bucket算法
实现位置:mapper.c:115-119
1 | static int bucket_uniform_choose( |
算法原理:
- 所有项目权重相同
- 使用排列选择算法
- 时间复杂度:O(1)(优化后)
排列选择算法(bucket_perm_choose):
1 | static int bucket_perm_choose(const struct crush_bucket *bucket, |
特点:
- 快速:O(1)平均情况
- 均匀分布
- 添加/删除项目时数据移动大
4.2 List Bucket算法
实现位置:mapper.c:122-145
1 | static int bucket_list_choose(const struct crush_bucket_list *bucket, |
算法原理:
- 从列表尾部(最新添加的项目)开始
- 对每个项目计算哈希值
- 根据累积权重决定是否选择该项目
- 如果选择,返回该项目;否则继续
特点:
- 时间复杂度:O(n)
- 添加项目时数据移动最优
- 删除项目时数据移动较大
4.3 Straw2 Bucket算法
实现位置:mapper.c:342-365
1 | static int bucket_straw2_choose( |
指数分布生成(generate_exponential_distribution):
1 | static inline __s64 generate_exponential_distribution( |
算法原理:
- 为每个项目生成一个指数分布的随机变量(”straw”长度)
- 权重越大,straw长度期望越大
- 选择straw长度最大的项目
数学原理:
- 使用指数分布的逆变换采样
- 如果每个OSD的请求间隔服从指数分布,则PG分布与权重成正比
- 参考:指数分布最小值的分布
特点:
- 时间复杂度:O(n)
- 添加/删除/重权重时数据移动最优
- 推荐使用
映射规则(Rule)
5.1 规则结构
规则由一系列步骤组成,每个步骤执行一个操作:
1 | // 示例规则:三副本规则 |
5.2 规则步骤类型
5.2.1 TAKE
1 | case CRUSH_RULE_TAKE: |
功能:选择规则执行的起始桶
5.2.2 CHOOSE_FIRSTN / CHOOSELEAF_FIRSTN
深度优先选择:
CHOOSE_FIRSTN:选择N个桶CHOOSELEAF_FIRSTN:选择N个叶子(OSD)
特点:
- 深度优先:先选择一个桶,再递归选择
- 副本间有依赖关系
- 适合副本存储
5.2.3 CHOOSE_INDEP / CHOOSELEAF_INDEP
广度优先选择:
CHOOSE_INDEP:选择N个桶CHOOSELEAF_INDEP:选择N个叶子(OSD)
特点:
- 广度优先:同时选择所有副本
- 副本间独立
- 适合纠删码
5.3 规则执行流程
规则执行在crush_do_rule_no_retry中实现:
1 | static int crush_do_rule_no_retry(...) |
映射执行流程
6.1 整体流程
1 | crush_do_rule() |
6.2 crush_choose_firstn(深度优先)
实现位置:mapper.c:441-629
核心逻辑:
1 | static int crush_choose_firstn(...) |
关键点:
- r值计算:
r = rep + parent_r + ftotalrep:副本位置parent_r:父级r值ftotal:总失败次数
- 冲突检测:检查是否与已选择的项目冲突
- 重试机制:
retry_bucket:桶内重试retry_descent:下降重试
- out检测:检查项目是否可用(基于权重)
6.3 crush_choose_indep(广度优先)
实现位置:mapper.c:636-824
核心逻辑:
1 | static void crush_choose_indep(...) |
关键点:
- 独立选择:每个副本位置独立选择
- r值计算:考虑副本数量和失败次数
- 广度优先:同时处理所有副本位置
6.4 is_out函数
实现位置:mapper.c:405-419
1 | static int is_out(const struct crush_map *map, |
功能:检查项目是否”out”(不可用)
原理:
- 权重=0:总是out
- 权重>=1.0:总是in
- 0<权重<1.0:基于哈希的概率检查
代码实现分析
7.1 文件结构
1 | crush/ |
7.2 关键函数调用链
7.2.1 映射调用链
1 | crush_do_rule() |
7.2.2 桶选择函数
1 | // mapper.c:368-399 |
7.3 工作空间管理
工作空间结构:
1 | struct crush_work { |
初始化:
1 | size_t crush_work_size(const struct crush_map *map, int result_max) |
7.4 重试机制
重试层次:
- 本地重试(
local_retries):桶内重试,避免冲突 - 本地回退重试(
local_fallback_retries):桶内穷举搜索 - 下降重试(
retry_descent):重新开始下降过程 - 总重试(
choose_total_tries):总重试次数限制
重试逻辑(crush_choose_firstn):
1 | if (collide && flocal <= local_retries) |
性能优化
8.1 Uniform Bucket优化
优化点:
- r=0优化:直接计算第一个元素,避免完整排列
- 延迟排列:只在需要时计算排列元素
1 | // 优化:r=0的情况 |
8.2 工作空间重用
优化:
- 工作空间可以在多次调用间重用
- 只要CRUSH map不变,工作空间就有效
- 减少内存分配开销
8.3 哈希函数优化
优化:
- 使用查找表加速自然对数计算(
crush_ln) - 内联函数减少函数调用开销
- 位运算优化
8.4 选择算法选择
性能对比:
| 算法 | 选择速度 | 添加项目 | 删除项目 | 重权重 |
|---|---|---|---|---|
| Uniform | O(1) | 差 | 差 | 差 |
| List | O(n) | 优 | 差 | 差 |
| Straw2 | O(n) | 优 | 优 | 优 |
建议:
- 小规模、权重相同的桶:使用Uniform
- 大规模、频繁变化的桶:使用Straw2
- 只添加、不删除的场景:使用List
8.5 规则优化
优化建议:
- 减少规则步骤:步骤越少,执行越快
- 合理使用类型:利用类型快速过滤
- 避免过深层次:层次越深,递归开销越大
总结
核心要点
- CRUSH是确定性伪随机算法:相同输入总是产生相同输出
- 支持多种桶算法:Uniform、List、Straw2,各有优缺点
- 规则驱动:通过规则定义映射行为
- 权重支持:根据权重进行负载均衡
- 容错机制:通过重试处理冲突和故障
算法优势
- 可扩展性:支持大规模集群
- 灵活性:通过规则和权重灵活控制
- 稳定性:Straw2算法在变化时数据移动最小
- 性能:Uniform算法选择速度最快
应用场景
- 副本存储:使用FIRSTN模式
- 纠删码:使用INDEP模式
- 故障域隔离:通过类型和规则实现
- 负载均衡:通过权重实现
参考资料
- CRUSH论文:http://www.ssrc.ucsc.edu/Papers/weil-sc06.pdf
- Ceph源码:
cephMain/src/crush/ - CRUSH算法文档:Ceph官方文档
正在加载留言…