CRUSH怎么分配数据?底层原理拆解

从PG映射到OSD选择,拆解CRUSH算法在分布式存储中的数据分配流程、故障域隔离机制及实际工程注意事项。

CRUSH算法怎么分配数据?分布式存储底层原理拆解

你往分布式存储里写一个文件,这个文件最终落在哪台服务器的哪块磁盘上,是谁决定的?

不是管理员手动指定,也不是随机扔,而是一个叫CRUSH的算法算出来的。这篇文章把CRUSH从输入到输出的完整流程拆开讲清楚,包括它为什么不用传统哈希、怎么通过Rule控制副本分布、以及实际部署中容易踩的坑。

一、CRUSH是什么

CRUSH,全称 Controlled Replication Under Scalable Hashing(可控副本的可扩展哈希),是Ceph分布式存储系统的核心数据分布算法。它的职责很明确:给定一个数据对象,计算出它应该存放在哪些OSD(Object Storage Device,对象存储设备)上。

"可控"是关键词。传统哈希算法是"撒"数据,撒到哪算哪,你管不了。CRUSH的"Controlled"体现在:你可以通过Rule(规则)精确控制副本放在哪个机架、哪个节点、哪块盘上,而不是交给随机数。

深信服EDS的EDSFS存储引擎基于改进的CRUSH算法实现数据分布,在原版CRUSH基础上增加了权重感知和负载均衡能力,后面会展开讲。

二、为什么不用传统一致性哈希

分布式存储领域,数据分布算法不止CRUSH一个。最常被拿来对比的是一致性哈希(Consistent Hashing)。很多人会问:既然一致性哈希已经解决了"节点增减时尽量少搬数据"的问题,为什么还要搞一个CRUSH?

三个核心原因:

1. 一致性哈希的迁移量仍然不够小

一致性哈希在节点增减时,只有相邻环段的数据需要迁移,看起来已经很优了。但在大规模集群中,每次扩容仍然会有相当比例的数据发生迁移。CRUSH的做法不同——它基于集群拓扑的层级结构计算,扩容时只有涉及新节点的PG需要重新映射,已有数据不需要全量重分布。迁移粒度从"对象级"变成了"PG级",数据搬迁量大幅减少。

2. 一致性哈希无法控制副本落在不同故障域

这是最致命的问题。一致性哈希只管"把数据均匀分布",不管"副本之间是否隔离"。假设你有3个副本,一致性哈希可能把其中2个副本落在同一个机架上。一旦这个机架断电,2个副本同时丢失,数据就没了。

CRUSH通过Rule可以显式指定:副本1选host A,副本2必须选host B,副本3必须选host C——三个副本强制分布在不同host上。这种故障域隔离能力,是一致性哈希做不到的。

3. CRUSH的Rule机制让放置策略可定义

一致性哈希的分布策略是固定的——哈希函数决定一切,你想改策略只能换哈希函数。CRUSH的Rule是一段可配置的逻辑,你可以定义:

  • 副本数是几个
  • 每个副本要在哪个层级做隔离(host级、rack级、row级)
  • 是否要走纠删码模式
  • 是否要区分SSD和HDD的存储池

改Rule不用改算法,改配置就行。这种灵活性是工程上非常需要的。

三、CRUSH怎么工作:从对象到OSD的完整链路

CRUSH不是一步到位的。一个数据对象从写入到最终落到磁盘上,经过两次映射。下面按顺序拆解。

第一步:对象 → PG(Placement Group)

数据对象先经过一次哈希映射,落到一个PG(Placement Group,归置组)里。计算公式:

```

pg_id = hash(对象名) % PG数量

```

PG是什么?你可以把它理解为一个"数据桶"。一个存储集群有几千到几万个PG,每个PG负责一批对象。引入PG这层中间映射,是为了减少CRUSH的计算频率——对象直接映射到OSD的话,每次集群拓扑变化都要对所有对象重新计算,对象数量动辄上亿,计算量爆炸。有了PG,只需要对PG数量级别的数据做CRUSH计算,对象到PG的映射不变。

举个例子:集群有1024个PG,你写一个叫"report.pdf"的文件,系统先算 `hash("report.pdf") % 1024`,假设结果是768,这个文件就归PG 768管。

第二步:PG → OSD(CRUSH核心计算)

PG拿到ID后,交给CRUSH算法计算它应该落在哪些OSD上。CRUSH的输入有三个:

输入说明
PG ID上一步算出来的归置组编号
Rule放置规则,定义副本数和故障域隔离策略
Cluster Map集群拓扑图,描述所有OSD的层级关系

输出是一组OSD列表,比如 `[OSD.12, OSD.35, OSD.78]`,表示这个PG的主副本在OSD.12,另外两个副本在OSD.35和OSD.78。

第三步:Cluster Map——集群拓扑的树状结构

CRUSH能做故障域隔离,靠的是Cluster Map。这是一棵树,层级从上到下:

```

root(根)

└── row(行/区域)

└── rack(机架)

└── host(主机/节点)

└── osd(存储设备)

└── disk(磁盘)

```

每个OSD是叶子节点,挂在某个host下面;每个host挂在某个rack下面,以此类推。CRUSH在选择OSD时,会沿着这棵树从上往下走,每经过一个层级就做一次选择和隔离判断。

第四步:Rule——怎么选OSD的

Rule是一段步骤化的逻辑,定义了CRUSH遍历Cluster Map的方式。以一个典型的3副本Rule为例:

```

rule replicated_rule {

ruleset 0

type replicated

min_size 1

max_size 10

step take root # 从root开始

step chooseleaf firstn 0 type host # 选3个不同host下的OSD

step emit # 输出结果

}

```

这段Rule的意思是:

1. take root:从Cluster Map的root节点开始

2. chooseleaf firstn 0 type host:选择3个(firstn 0表示选num_replicas个)不同的host,然后从每个被选中的host下面再选一个OSD

3. emit:输出最终的OSD列表

关键在 `type host` 这个参数——它指定了故障域隔离级别。设成host,意味着3个副本必须落在3个不同的host上。如果设成 `type rack`,则3个副本必须落在3个不同的机架上,隔离级别更高,能容忍整个机架故障。

故障域隔离的实际效果

假设集群有3个机架,每个机架4台主机,每台主机6块OSD:

  • Rule设 `type host`:3副本分布在3个不同host上,单host宕机不丢数据,但同一机架断电可能导致部分PG的2个副本同时不可用(如果这2个副本恰好在这个机架的不同host上)
  • Rule设 `type rack`:3副本分布在3个不同机架上,单机架整体故障不丢数据,隔离级别最高

选择哪个级别取决于你的故障场景预期。一般中小型集群用host级隔离就够了,大型集群或对可用性要求极高的场景建议rack级。

四、CRUSH在实际运维中的影响

理解了CRUSH的原理,再看运维中的几个典型场景就清晰了。

场景1:集群扩容

新增3台节点接入集群后,Cluster Map更新。CRUSH重新计算所有PG的OSD映射。由于新节点在Cluster Map中新增了权重,部分PG会被重新映射到新节点上的OSD。但关键在于:只有被重新映射的PG需要迁移数据,其余PG的OSD列表不变,已有数据原地不动。

这和一致性哈希的区别在于:一致性哈希扩容时,新节点相邻环段的所有对象都要迁移;CRUSH扩容时,迁移以PG为单位,粒度更粗但总量更可控。实际工程中,往一个已有100TB数据的集群加3台节点,数据迁移量通常在几个TB级别,而不是全量重分布。

场景2:节点故障

某台host宕机,Cluster Map中该host下的OSD被标记为down。CRUSH重新计算涉及这些OSD的PG,将它们重新映射到其他存活的OSD上。这个过程是自动的——存储集群检测到OSD离线后,触发PG的rebackfill(回填),从剩余副本复制数据到新选出的OSD,恢复副本数。

场景3:深信服EDS的改进

原版Ceph的CRUSH算法存在一个已知问题:权重计算不够精细,容易导致数据分布不均衡,尤其在节点异构(不同容量磁盘混布)的场景下。深信服EDS的EDSFS引擎在CRUSH基础上做了改进:

  • 权重感知:不只是按OSD数量分配,而是按实际容量权重动态调整,大容量盘多分数据,小容量盘少分数据
  • 负载均衡:在CRUSH计算结果基础上做二次均衡,避免某些OSD成为热点
  • 纠删码支持:CRUSH算法不只支持副本模式,也支持纠删码模式,两者的选择我们之前详细对比过(EDS副本数和纠删码选哪个),副本模式适合小文件和高随机读写场景,纠删码适合大文件和容量优先的归档场景

五、实际部署中要注意什么

CRUSH算法本身设计精巧,但用不好会出大问题。以下三个是工程中最常见的坑。

1. PG数量设置不当导致数据不均衡

PG数量在创建存储池时设定,后续修改成本高(要触发PG分裂和数据重分布)。PG太少,每个PG承载的数据量大,单PG迁移耗时长;PG太多,CRUSH计算开销和内存占用上升。

官方建议:每个OSD承载50-100个PG。计算公式:

```

总PG数 = OSD数量 × 每OSD期望PG数 / 副本数

```

例如:30个OSD,3副本,每OSD目标100个PG:

```

总PG数 = 30 × 100 / 3 = 1000

```

取最近的2的幂次,设为1024。如果设成128,每OSD只有约13个PG,数据分布容易出现倾斜;如果设成8192,每OSD约273个PG,CRUSH计算和内存开销明显增大。

2. CRUSH Rule写错导致副本落在同一故障域

这是最危险的错误。如果Rule里 `type` 设成了 `osd` 而不是 `host`,CRUSH只保证3个副本落在3个不同的OSD上,但不保证它们在不同的host上。结果可能是:3个副本全在同一个host的3块盘上,这台host一宕机,数据全丢。

排查方法:用 `crushtool --test --show-mappings` 检查PG到OSD的实际映射,确认同一PG的多个副本确实分布在不同故障域。生产环境上线前务必验证。

3. 大规模集群的CRUSH计算开销

集群规模到100+节点、数千个OSD时,CRUSH每次计算都要遍历Cluster Map的层级树,计算量不可忽视。虽然单次计算是毫秒级,但全集群PG重新映射时(比如大面积OSD故障恢复),数千个PG同时触发CRUSH重算,CPU会出现明显峰值。

应对方式:

  • 控制单次拓扑变更的规模,分批次接入新节点
  • 合理设置PG数量,避免PG过多导致计算放大
  • 关注CRUSH Tree的层级深度,层级过深会增加遍历开销

写在最后

CRUSH算法的核心价值在于:把数据分布从一个纯哈希问题,变成了一个可配置的拓扑感知问题。它让分布式存储在扩容、故障恢复、副本隔离这三个关键场景上都有了确定性的控制能力。

理解CRUSH的原理,对于做分布式存储的选型和运维都有实际意义。如果你在评估超融合和分布式存储的区别,或者想知道超融合方案怎么选,CRUSH的数据分布机制是一个重要的技术判断维度——超融合通常把计算和存储融合部署,分布式存储则专注存储层独立扩展,两者在CRUSH的利用方式和故障域设计上有明显差异。

实际部署中,PG数量规划、Rule配置验证、大规模集群的计算开销管理,是把CRUSH用好的三个关键点。配置正确,CRUSH是分布式存储数据可靠性的基石;配置错误,它也可能成为数据丢失的隐患。