- 存储
- 分布式文件系统
- 对象存储
- 后端
- 高可用
【免费下载链接】ceph
Ceph is a distributed object, block, and file storage platform
导读
本文基于 Ceph 开源仓库中的 doc/dev/radosgw/bucket_index.rst,系统解析 RGW(RADOS Gateway)的 Rados Bucket Index 机制。Bucket Index 是每个存储桶(Bucket)内对象列表及其元数据(size、etag、mtime 等)的索引,是支撑 S3ListObjectsV2/ListObjectVersions与 SwiftGET Container等列目录 API 的底层数据结构。读完本文,你将掌握 bucket index 的存储模型、S3 版本化下的 olh 间接寻址、读写一致性保证、索引事务协议、分片与动态重分片机制,以及基于radosgw-admin的手动运维手段。
什么是 Bucket Index
在 RGW 中,每个存储桶都维护一份bucket index(桶索引),用于存放该桶内对象列表及每个对象的相关元数据(size、etag、mtime 等),从而支撑对象列举类 API 请求,并承担部分内部记账(bookkeeping)职责。这些 API 对应 S3 的ListObjectsV2、ListObjectVersions以及 Swift 的GET Container。
存储载体:RADOS omap
索引条目并非存放在独立文件中,而是存放在索引对象(或按分片拆分的多个索引对象)的RADOS omap条目中。omap 是 RADOS 对象上以键值对形式存储的扩展属性集合,天然适合存放“对象名 → 元数据”这种字典结构。
Indexless 桶
存储桶也可以被创建为'indexless'(无索引)模式。此类桶没有索引,因此无法进行列目录操作。在源码中对应 src/rgw/rgw_bucket_layout.h 的BucketIndexType枚举:
enum class BucketIndexType : uint8_t { Normal, // normal hash-based sharded index layout Indexless, // no bucket index, so listing is unsupported };可以看到Indexless的注释明确指出 "no bucket index, so listing is unsupported"。而is_layout_reshardable()等辅助函数(rgw_bucket_layout.h)也以BucketIndexType::Normal作为可重分片的前提。
索引条目的粒度
对于非版本化存储桶,bucket index 中每个对象对应一个条目。对于 S3版本化存储桶则更为复杂(详见下一节):每个对象版本和删除标记(delete marker)都对应一个索引条目。
S3 对象版本化与 olh 间接寻址
版本化存储桶的 bucket index 中,除了按对象名排序的条目外,还为同名对象的各个版本维护了一组从新到旧排序的条目,供版本化列目录使用。这解释了为何版本化桶在RGWRados::calculate_preferred_shards()(src/rgw/driver/rados/rgw_rados.cc)中会提前触发重分片——源码注释说明,每个版本化桶的第一个对象需要 4 个索引条目,之后每新增一个对象需要 2 个条目(max_objs_per_shard /= 3),因此索引增长远快于普通桶。
head 对象与版本 oid
RGW 在rgw.buckets.data池中为每个对象版本存放一个 head 对象。该 RADOS 对象的 oid 由**对象名 + 版本 id(version id)**组合而成,从而保证不同版本对应不同的 RADOS 对象。
Object Logical Head(olh)
S3 的 GET/HEAD 请求按对象名访问时,应返回该对象的“当前(current)”版本。为了支持这一点,RGW 额外保存一个'object logical head'(olh)对象,其 oid 只包含对象名(不含版本 id),作为指向当前版本 head 对象的间接层(indirection)。这段间接寻址逻辑实现在RGWRados::follow_olh()(src/rgw/driver/rados/rgw_rados.cc):
int RGWRados::follow_olh(const DoutPrefixProvider *dpp, RGWBucketInfo& bucket_info, RGWObjectCtx& obj_ctx, RGWObjState *state, const rgw_obj& olh_obj, rgw_obj *target, optional_yield y)从实现可见其核心流程:收集 olh 对象属性中以RGW_ATTR_OLH_PENDING_PREFIX为前缀的 pending 条目(即尚未收敛的版本写入/删除记录)→ 检查并移除已过期的 pending 条目(check_pending_olh_entries/remove_olh_pending_entries)→ 若仍有 pending 条目则调用update_olh()将 olh 推进到最新版本 → 最后从RGW_ATTR_OLH_INFO中解码RGWOLHInfo,将olh.target作为当前版本目标返回。若该 olh 标记为removed(对象当前为删除标记),则设置state->is_dm = true并返回-ENOENT。
保证 olh 与 bucket index 的一致性
为了维持 olh 对象与 bucket index 之间的一致性,索引为每个对象名保留一个独立的 'olh' 条目,记录对其所有版本的全部写入/删除日志。当 RGW 需要依据索引判定“当前版本”时,RGWRados::apply_olh_log()(同样位于 src/rgw/driver/rados/rgw_rados.cc)会重放(replay)这份日志,保证 olh 对象收敛到与 bucket index 一致的“当前版本”。
一致性保证(Consistency Guarantee)
RGW 在对象操作上保证读后写一致性(read-after-write consistency):一旦客户端收到某个写请求的成功响应,该写入的效果必须对后续读请求可见。
例如,S3 客户端先发PutObject覆盖一个已存在对象、紧接着发GetObject读回时,RGW不得返回旧对象内容,必须返回新对象内容,或返回其后某次写入/删除的结果。
该保证适用于所有对象写请求(PutObject、DeleteObject、PutObjectAcl等)与所有对象读请求(HeadObject、GetObject、ListObjectsV2等)。这一强一致性约束正是后续“三步索引事务”设计(见后文)的根本动因。
Rados 对象模型
S3/Swift 对象(即 'API objects')存放在rgw.buckets.data池中,每个 API 对象由一个head 对象和零个或多个 tail 对象组成。而bucket index 对象则存放在独立的rgw.buckets.index池中。
写入对象时,head 对象最后写入,作为使其对读请求可见的原子“提交(commit)”动作。将索引与数据分池、head 后写,为下一步通过事务协议保证两者一致提供了物理基础。
分片(Sharding)与重分片(Resharding)
为什么需要分片
一个桶的索引通常会被拆分为多个 RADOS 对象,称为bucket index shards(索引分片)。在 RADOS 中,对同一对象的多次写入无法并行;把索引分散到更多 RADOS 对象上,即可提高写入并行度。
对于一次对象上传,其对应的索引分片通过对象名的哈希选择:
enum class BucketHashType : uint8_t { Mod, // rjenkins hash of object name, modulo num_shards };(见 src/rgw/rgw_bucket_layout.h)
Mod模式即“对象名的 rjenkins 哈希值对分片数取模”。这一设计保证:同一对象的所有条目(及其在 S3 版本化桶中的所有版本)必定落在同一分片上,同时一般也能让对象均匀分布到各分片——但当某些对象拥有大量版本时,均匀性会被破坏(热点分片)。
分片数量配置
新桶的默认分片数为 11,可通过以下两个途径覆盖:
| 配置位置 | 配置项 | 说明 |
|---|---|---|
| zonegroup | bucket_index_max_shards | 区域组级默认分片数 |
| ceph.conf | rgw_override_bucket_index_max_shards | 全局覆盖,优先级更高 |
源码佐证见 src/rgw/driver/rados/rgw_bucket.cc:
} else if (cct->_conf->rgw_override_bucket_index_max_shards > 0) { ... cct->_conf->rgw_override_bucket_index_max_shards;以及 src/rgw/driver/rados/rgw_rados.cc 中bucket_index_max_shards对rgw_override_bucket_index_max_shards的优先读取逻辑。
分片布局结构定义在 src/rgw/rgw_bucket_layout.h 的bucket_index_normal_layout:
struct bucket_index_normal_layout { uint32_t num_shards = 1; // 当前分片数 uint32_t min_num_shards = 1; // 该桶布局允许的最少分片数 BucketHashType hash_type = BucketHashType::Mod; };注意:旧桶曾用num_shards = 0表示 1,rgw::num_shards()辅助函数做了兼容归一化(rgw_bucket_layout.h)。
重分片的必要性与触发条件
任何 RADOS 对象的 omap 都有约 100,000 条目的实际上限,因此必须保证分片数量足够、使每片条目数不超过该上限。随着桶内对象数增长,可能需要reshard(重分片)以增加分片数。
重分片存在两种方式:
- 手动重分片:通过
radosgw-admin管理命令主动触发; - 动态重分片(dynamic resharding):无需管理员干预自动发生。
动态重分片的判定逻辑位于RGWRados::check_bucket_shards()(src/rgw/driver/rados/rgw_rados.cc),关键流程为:
- 首先检查配置开关
rgw_dynamic_resharding(关闭则直接返回,不触发); - 校验布局可重分片(
is_layout_reshardable); - 调用
calculate_preferred_shards()结合rgw_max_dynamic_shards(动态分片上限)与rgw_max_objs_per_shard(每分片目标条目数)计算期望分片数;版本化桶按 1/3 折算条目上限以提前触发; - 若涉及缩减分片,还受
rgw_dynamic_resharding_may_reduce开关约束; - 判定需要重分片时,通过
add_bucket_to_reshard()(rgw_rados.cc)写入重分片队列(reshard log),记录old_num_shards、new_num_shards、initiator = Dynamic等信息,交由后台进程执行。
动态缩减分片与延迟等待
动态重分片也可以减少分片数量。由于某些桶的对象数会快速增减,且不希望频繁重分片去“追逐”这些波动,缩减分片操作在“判定需要缩减”与“实际执行”之间引入了延迟:到执行时刻会再次检查桶,仅当桶仍确实需要缩减分片时才真正执行。
该延迟默认 5 天,可用 ceph.conf 中的rgw_dynamic_resharding_reduction_wait配置。源码实现见 src/rgw/driver/rados/rgw_reshard.cc,读取该配置后换算为等待时间窗口。
布局信息的载体
桶的索引对象布局信息存放在RGWBucketInfo的struct rgw::BucketLayout中(src/rgw/rgw_bucket_layout.h)。该结构除记录当前索引布局current_index外,还包含:
resharding:BucketReshardState枚举(None/InProgress/InLogrecord),标记重分片是否进行中;target_index:重分片操作的目标布局(可选,重分片期间存在);logs:未裁剪的桶日志(bilog)布局代际历史;judge_reshard_lock_time:用于判断桶是否正在重分片的锁时间戳。
真正的重分片执行逻辑位于 src/rgw/driver/rados/rgw_reshard.cc。
手动重分片:radosgw-admin 命令
radosgw-admin提供了一组完整的重分片运维命令(src/rgw/radosgw-admin/radosgw-admin.cc 的命令清单):
| 命令 | 用途 |
|---|---|
radosgw-admin bucket reshard --bucket=<name> --num-shards=<N> | 手动对指定桶重分片 |
radosgw-admin bucket set-min-shards | 设置动态重分片会为桶考虑的最少分片数 |
radosgw-admin reshard add | 将桶加入(调度)重分片队列 |
radosgw-admin reshard list | 列出所有正在重分片或已调度的桶 |
radosgw-admin reshard status | 读取桶的重分片状态 |
radosgw-admin reshard process | 处理已调度的重分片任务 |
radosgw-admin reshard cancel | 取消桶的重分片 |
radosgw-admin reshard stale-instances list | 列出重分片遗留的 stale-instances |
radosgw-admin reshard stale-instances delete | 清理重分片遗留的 stale-instances |
radosgw-admin reshardlog list | 列出桶重分片日志 |
radosgw-admin reshardlog purge | 裁剪桶重分片日志 |
示例:将mybucket强制重分片为 32 个索引分片:
radosgw-admin bucket reshard --bucket=mybucket --num-shards=32查看所有待重分片/进行中的桶:
radosgw-admin reshard list radosgw-admin reshard status --bucket=mybucket注:上述命令的具体行为与输出格式以当前仓库 radosgw-admin.cc 中的命令注册为准。
索引事务(Index Transaction)
问题:两个对象无法原子更新
所有对象写入或删除都必须同步更新 bucket index 以保持一致性。但 head 对象与 bucket index 存放在不同的 RADOS 对象(分属rgw.buckets.data与rgw.buckets.index池)中,无法用单个 RADOS 操作原子地同时更新两者。为了满足列目录操作的一致性保证,RGW 用三步桶索引事务协调这两次对象写入:
- 在 bucket index 对象上**预备(Prepare)**一个事务;
- 写入或删除 head 对象;
- 在 bucket index 对象上**提交(Commit)事务;若第 2 步失败则取消(Cancel)**事务。
pending 与 completed 状态
对象写入与删除可能彼此竞争,因此同一对象在某一时刻可能同时存在多个已预备但未提交的事务。RGW 将对象条目区分为两种状态:
- pending(挂起):存在未完成事务;
- completed(完成):不存在未完成事务。
源码实现位置
该事务协议在 src/rgw/driver/rados/rgw_rados.cc 中实现为:
RGWRados::Object::Write::write_meta():对象写入路径;RGWRados::Object::Delete::delete_obj():对象删除路径。
而 bucket index 侧的底层操作由 OSD 侧的 class 方法实现,位于 src/cls/rgw/cls_rgw.cc:
rgw_bucket_prepare_op()(cls_rgw.cc):预备事务,将条目标记为 pending;rgw_bucket_complete_op()(cls_rgw.cc):提交事务(或在上游失败时回滚),将条目推进到 completed。
这些方法通过cls注册接口暴露给 radosgw 调用(cls_rgw.cc 中的cls.register_cxx_method(...)注册片段),形成 "RGW 侧编排 + OSD class 侧执行" 的完整事务链路。
列目录(Listing)
列出条目与盘查 head
列目录时,RGW 会读取 bucket index 中的全部条目(包括 pending 与 completed)。对任何 pending 条目,必须先检查对应 head 对象是否存在,再决定是否将条目纳入最终列表——这保证了即使事务中断,列表也不会出现幽灵条目。
dir suggest:从列目录到索引的反馈
如果 RGW 在索引事务中途崩溃,索引条目可能卡在 pending 状态。当后续列目录遇到这些 pending 条目时,RGW 会把从 head 对象读到的信息回写给 bucket index,用于更新条目、解决陈旧事务。这条回写消息称为'dir suggest'(目录建议)——因为 bucket index 把它当作一条提示/建议来采纳。
源码实现位置
列目录功能在 src/rgw/driver/rados/rgw_rados.cc 中实现为:
RGWRados::Bucket::List::list_objects_ordered():有序列目录;RGWRados::Bucket::List::list_objects_unordered():无序列目录;RGWRados::check_disk_state():读取 head 对象并编码建议的索引变更(即生成 dir suggest 载荷)。
bucket index 侧的对应操作在 src/cls/rgw/cls_rgw.cc 中实现为:
rgw_bucket_list()(cls_rgw.cc):按前缀/游标等条件遍历 omap 条目;rgw_dir_suggest_changes()(cls_rgw.cc):应用列目录回传的建议,解决 stale pending 事务。
有序列目录的高昂代价
由于 RGW 对象依据哈希分布到桶的各索引分片上,分片之间不存在词典序(lexical ordering),只有单个分片内部保持有序。因此有序列目录(list_objects_ordered)是一种复杂且 I/O 密集的操作:
- 从每个分片并行拉取一批条目;
- 使用**选择排序(selection sort)**在批次间归并,产出一段有序结果;
- 当任一分的批次耗尽时,从所有分片再次批量读取,如此循环直至完成。
这也意味着:分片数量越多,有序列目录的并行批次管理开销越大;运维时需要在写入并行度(分片多)与有序列目录成本(分片少)之间权衡,并借助动态重分片在桶规模变化时自动保持平衡。
小结
Rados Bucket Index 是 RGW 对象存储语义的基石:它以 RADOS omap 承载对象元数据索引,通过 olh 间接层支撑 S3 版本化语义,以三步索引事务在数据与索引分池的约束下满足读后写一致性,并以分片 + 动态重分片应对 omap 容量上限与写入并发需求。理解这些机制,有助于在部署、排障与容量规划时做出正确的配置决策——例如合理设置rgw_override_bucket_index_max_shards、关注rgw_max_objs_per_shard与rgw_dynamic_resharding系列参数,以及熟练使用radosgw-admin的重分片运维命令。
- 存储
- 分布式文件系统
- 对象存储
- 后端
- 高可用
【免费下载链接】ceph
Ceph is a distributed object, block, and file storage platform
相关推荐
Ceph RGW 数据布局深度解析:元数据、Bucket 索引与对象寻址机制
Ceph RGW 数据布局深度解析:元数据、Bucket 索引与对象寻址机制 本指南以 Ceph 官方文档 doc/radosgw/layout.rst htt
存储分布式文件系统对象存储后端高可用TiDB 整型分片索引(Integer Shard Index)设计与实现深度解析
TiDB 整型分片索引(Integer Shard Index)设计与实现深度解析 本篇文章围绕 TiDB 设计文档 Integer shard index h
数据库分布式数据库后端OLAPLance 碎片复用索引(Fragment Reuse Index)深度解析:用延迟索引重映射化解 compaction 与索引优化冲突
Lance 碎片复用索引(Fragment Reuse Index)深度解析:用延迟索引重映射化解 compaction 与索引优化冲突 Lance 采用碎片(
数据库向量数据库数据湖全文检索
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考