7532 lines
352 KiB
Markdown
7532 lines
352 KiB
Markdown
> 基于 Ceph 官方文档整理 | 文档版本:**Tentacle v20.2.4** | 抓取日期:2026-09-17 | 源文:ceph/ceph@v20.2.4 `doc/dev/`
|
||
> 官网入口:https://docs.ceph.com/en/tentacle/dev/
|
||
|
||
# 第 16 册 · 内部实现篇(一):核心机制、数据结构与协议
|
||
|
||
---
|
||
|
||
## 本册导读
|
||
|
||
### 内部实现四册的分工
|
||
|
||
本册是「内部实现」四册中的**第一册**,四册的分工大致如下:
|
||
|
||
| 册 | 主题 | 关注点 |
|
||
|---|---|---|
|
||
| **第 16 册(本册)** | 核心机制、数据结构与协议 | 存储引擎抽象(ObjectStore / BlueStore / SeaStore)、PG 与 peering 状态机、Messenger 与 msgr2 线协议、编码序列化、cephx/Kerberos 认证、集群映射(mon)元数据、数据路径特性(条带化 / EC / 重删 / CRUSH MSR)、配置与可观测性内部实现 |
|
||
| 第 17 册 | OSD 内部与数据服务 | `osd_internals/` 下的 async_recovery、backfill_reservation、erasure_coding(ECBackend / jerasure / developer_notes / enhancements / proposals)等 |
|
||
| 第 18 册 | Crimson 与 MDS 内部 | `crimson/`(crimson.rst、osd.rst、pipeline.rst、error-handling.rst、backfillmachine.rst、poseidonstore.rst)与 `mds_internals/`(data-structures、locking、exports、quiesce) |
|
||
| 第 19 册 | 开发者工具链与工程实践 | `developer_guide/`、测试体系、构建与打包、文档工具链等 |
|
||
|
||
> 💡 补充:上表四册的分工是编者为便于阅读而做的划分,官方 `doc/dev/` 并未明确给出「四册」的说法。官方的入口索引是 `dev/internals.rst`,它只是把内部实现页面按字母/主题平铺列出。
|
||
|
||
### 谁该读本册
|
||
|
||
**本册面向想读懂 Ceph 源码、或要做深度调优 / 二次开发的人。**
|
||
|
||
- 如果你是**运维人员**,日常做集群部署、扩缩容、故障处理:本册**可以跳过**。你需要的是 RADOS 运维手册(`rados/` 目录下)而不是 `dev/` 目录。`dev/` 是 Ceph 的**开发者文档**,官方明确把它定位为「给想理解 Ceph 内部实现的人」的材料,其中的内容**不构成任何稳定性承诺**——页面里大量出现 `TODO`、`.. todo::`、`Issues:`、`This is very incomplete` 等字样,很多结构描述甚至落后于代码(例如 `cephx_protocol.rst` 开头就声明「代码可能已经变了,本文档不会同步更新」)。
|
||
- 如果你是**研发 / 性能工程师 / 存储方向的学习者**:本册会带你读完 Ceph 最核心的几条内部主线:**对象是怎么落到磁盘的(一)→ 副本之间怎么达成一致(二)→ 节点之间怎么通信(三)→ 客户端和服务端怎么互相证明身份(四)→ 集群元数据怎么组织(五)→ 上层特性怎么切分数据(六)→ 怎么观测和调参(七)**。
|
||
|
||
### 阅读本册需要注意的三件事
|
||
|
||
1. **类名、函数名、配置项、默认值、协议字段名、状态机状态名一律保留英文原文。** 这不是偷懒:Ceph 里 `peering`、`acting set`、`up_thru`、`onode`、`lba_tree` 这类词翻译成中文后几乎无法与代码对应,反而会增加阅读成本。
|
||
2. **本册严格遵循「不编造」原则。** `doc/dev/` 的实现类文档普遍不完整,`object-store.rst` 只有 62 行(其中一半是 graphviz 图,末尾一句 `.. todo:: write more here`)、`messenger.rst` 只有 33 行且只讲了一个 benchmark 工具、`delayed-delete.rst` 只有 13 行。凡是官方没写的,本册会**明写「文档未明确」**,而不是用想象去填充。
|
||
3. **源码路径是本册的重要信息。** 官方在这些页面里大量直接指向 `src/...` 路径(如 `src/crimson/os/seastore/journal.h`、`src/osd/PeeringState.h`)。本册会原样保留这些路径,因为它们是「文档 → 代码」之间唯一的桥梁。
|
||
|
||
---
|
||
|
||
## 一、对象存储抽象与实现
|
||
|
||
本章覆盖 `object-store.rst`、`bluestore.rst`、`seastore.rst`、`zoned-storage.rst` 四页。
|
||
|
||
> ⚠️ 关于本章的预期管理:官方 `doc/dev/` 的这四页**并不是 BlueStore 的完整设计文档**。真正的 BlueStore 设计细节(RocksDB / BlueFS / onode / Extent / allocator 的完整语义)在 `doc/rados/configuration/bluestore-config-ref.rst` 和源码里,而不在 `doc/dev/bluestore.rst`。本章会如实说明每一页到底覆盖了什么。
|
||
|
||
### 1.1 ObjectStore 抽象层(object-store.rst)
|
||
|
||
#### 官方页面的实际内容
|
||
|
||
必须首先说明:**`object-store.rst` 全文只有 62 行**,正文部分**几乎为零**。整页由两部分组成:
|
||
|
||
1. 一段 graphviz 有向图(第 5–58 行),描述各组件之间的依赖/调用关系;
|
||
2. 第 61 行一句 `.. todo:: write more here`。
|
||
|
||
也就是说,**官方这一页没有描述 ObjectStore 的接口签名、事务模型(`ObjectStore::Transaction`)、`mkfs/mount/umount` 生命周期、collection 语义等任何内容**。这些在 `dev/` 里是缺失的。
|
||
|
||
#### 依赖关系图(graphviz 原文还原)
|
||
|
||
官方图的边(edge)如下,方向表示「上层依赖下层 / 调用下层」:
|
||
|
||
```text
|
||
testrados -> librados
|
||
testradospp -> librados
|
||
rbd -> librados
|
||
radostool -> librados
|
||
radosgw-admin -> radosgw
|
||
radosgw -> librados
|
||
radosacl -> librados
|
||
librados -> objecter
|
||
ObjectCacher -> Filer
|
||
dumpjournal -> Journaler
|
||
Journaler -> Filer
|
||
SyntheticClient -> Filer
|
||
SyntheticClient -> objecter
|
||
Filer -> objecter
|
||
objecter -> OSDMap
|
||
ceph-osd -> PG
|
||
ceph-osd -> ObjectStore
|
||
crushtool -> CrushWrapper
|
||
OSDMap -> CrushWrapper
|
||
OSDMapTool -> OSDMap
|
||
PG -> PrimaryLogPG
|
||
PG -> ObjectStore
|
||
PG -> OSDMap
|
||
PrimaryLogPG -> ObjectStore
|
||
PrimaryLogPG -> OSDMap
|
||
ObjectStore -> BlueStore
|
||
BlueStore -> rocksdb
|
||
```
|
||
|
||
#### 从这张图能读出的分层结构
|
||
|
||
虽然官方没有写正文,但这张图本身信息量不小,可以整理成完整的分层视图:
|
||
|
||
```text
|
||
┌───────────────────────────────────────────────┐
|
||
│ 测试 / 工具层 │
|
||
│ testrados testradospp radostool crushtool │
|
||
│ dumpjournal SyntheticClient OSDMapTool │
|
||
└───────────────────────────────────────────────┘
|
||
│
|
||
┌───────────────────────────────────────────────┐
|
||
│ 高层客户端层 │
|
||
│ rbd radosgw (+radosgw-admin) radosacl │
|
||
│ ObjectCacher → Filer ← Journaler │
|
||
└───────────────────────────────────────────────┘
|
||
│
|
||
┌───────────────────────────────────────────────┐
|
||
│ librados → objecter → OSDMap → CrushWrapper │
|
||
└───────────────────────────────────────────────┘
|
||
│ (网络另一侧)
|
||
┌───────────────────────────────────────────────┐
|
||
│ ceph-osd → PG → PrimaryLogPG │
|
||
│ ↘ ObjectStore ↘ │
|
||
│ ceph-osd ───────────────────↗ BlueStore │
|
||
│ → rocksdb │
|
||
└───────────────────────────────────────────────┘
|
||
```
|
||
|
||
关键点解读:
|
||
|
||
| 观察 | 含义 |
|
||
|---|---|
|
||
| `ObjectStore -> BlueStore` 是**唯一**一条出边 | 在这份文档的语境里,BlueStore 是 ObjectStore 抽象的唯一实现(图中未画出 SeaStore / FileStore / MemStore)。注意 SeaStore 在 Crimson 分支下也实现了 ObjectStore 接口,但此图未体现 |
|
||
| `BlueStore -> rocksdb` | BlueStore 的元数据(onode、extent map、omap 等)由 RocksDB 承载 |
|
||
| `PG -> PrimaryLogPG` | 副本型池的 PG 逻辑在 `PrimaryLogPG` 中;EC 池另有 `ECBackend`/`ECPG`,此图未画 |
|
||
| `PG -> ObjectStore` 与 `PrimaryLogPG -> ObjectStore` **两条边并存** | PG 基类与其副本型子类都会直接使用 ObjectStore |
|
||
| `ceph-osd -> PG` 且 `ceph-osd -> ObjectStore` | OSD 守护进程同时持有 PG 集合与 ObjectStore 实例 |
|
||
| `objecter -> OSDMap` | 客户端侧通过 OSDMap + CRUSH 自行计算对象该发给哪个 OSD(这正是 Ceph「客户端可自路由」的体现) |
|
||
| `Filer` / `ObjectCacher` / `Journaler` | CephFS/MDS 侧的对象读写与日志抽象,位于 librados 之上 |
|
||
|
||
#### 文档未明确的 ObjectStore 内容
|
||
|
||
以下内容**官方 `object-store.rst` 完全没有提及**,本册不做推测:
|
||
|
||
- `ObjectStore` 的虚函数接口清单(`mkfs`、`mount`、`umount`、`mkcoll`、`stat`、`read`、`write`、`touch`、`clone`、`omap_*` 等);
|
||
- `ObjectStore::Transaction` 的操作类型与 `queue_transaction` 语义;
|
||
- `coll_t` / `ghobject_t` 的结构定义;
|
||
- `ObjectStore` 与 `PGBackend` 之间的边界。
|
||
|
||
> 💡 补充:这些内容的权威来源是源码头文件 `src/os/ObjectStore.h`(类 `ObjectStore`、`ObjectStore::Transaction`)与 `src/osd/PGBackend.h`。本册不转述源码,只标注路径。
|
||
|
||
> 引自:[Object Store Architecture Overview](https://docs.ceph.com/en/tentacle/dev/object-store/)
|
||
|
||
---
|
||
|
||
### 1.2 BlueStore 内部:小写入策略(bluestore.rst)
|
||
|
||
#### 官方页面的实际内容
|
||
|
||
`bluestore.rst` 全文 85 行,**只讲了一件事:小写入(small write)策略的分类与选型矩阵**。它**没有**描述:
|
||
|
||
- RocksDB 与 BlueFS 的关系与布局;
|
||
- `onode` 的字段结构;
|
||
- `Extent` / `Blob` / `SharedBlob` / `pextent` 的完整语义;
|
||
- `Allocator`(`BitmapAllocator` / `AvlAllocator` / `HybridAllocator` / `StupidAllocator`)的实现;
|
||
- 校验和(csum)计算与校验流程;
|
||
- 压缩、`bluefs_db` 分层、WAL/DB 设备等配置语义。
|
||
|
||
> ⚠️ 这一点需要特别提醒:很多人以为 `dev/bluestore.rst` 是 BlueStore 的设计总纲,实际上它只是「小写入策略速查表」。本册严格按官方内容展开,不补充官方未写的实现细节。
|
||
|
||
#### 设计动机
|
||
|
||
BlueStore 直接管理裸块设备,绕过本地文件系统。这带来一个问题:**在裸设备上做小写(small write / partial overwrite)开销极大**——因为分配粒度(`min_alloc_size`,HDD 上通常 64 KiB、SSD 上通常 4 KiB)远大于一次小写的数据量,且校验和是以 `csum_block_size` 为块计算的,改一个字节就要重算整块。
|
||
|
||
因此 BlueStore 需要一套策略矩阵:**对给定的「写入模式 × blob 类型」,选择一种代价最小的落盘方式**。这就是本页全部内容。
|
||
|
||
#### 六种当前已实现的小写入策略
|
||
|
||
| 代号 | 名称 | 步骤 | 说明 |
|
||
|---|---|---|---|
|
||
| **U** | Uncompressed write of a complete, new blob | ① write to new blob ② kv commit | 写一个**全新的、完整的** blob。最干净的路径 |
|
||
| **P** | Uncompressed partial write to unused region of an existing blob | ① write to unused chunk(s) of existing blob ② kv commit | 写已有 blob 的**未使用区域**,不覆盖已有数据,因此可以避开 RMW |
|
||
| **W** | WAL overwrite | ① kv commit ② wal overwrite (chunk-aligned) of existing blob | 先把「意图」以 WAL 形式提交到 RocksDB,再异步覆盖写。**必须 chunk_size = MAX(block_size, csum_block_size) 对齐** |
|
||
| **N** | Uncompressed partial write to a new blob | ① write into a new (sparse) blob ② kv commit | 写到一个新的稀疏 blob;未来对该区域的写会退化成 **P** 或 **W** |
|
||
| **R+W** | Read partial chunk, then WAL overwrite | ① read (out to chunk boundaries) ② kv commit ③ wal overwrite (chunk-aligned) of existing blob | 最贵:必须先读补齐到 chunk 边界,再走 WAL 覆盖写 |
|
||
| **C** | Compress data, write to new blob | ① compress and write to new blob ② kv commit | 压缩后写入新 blob,用于开启了压缩且数据量够大的场景 |
|
||
|
||
关键区分点:
|
||
|
||
- **U / P / N 是「先写数据、后提交元数据」**(write → kv commit)。
|
||
- **W / R+W 是「先提交意图、后写数据」**(kv commit → wal overwrite)。这两者依赖 BlueStore 的 deferred write(小写先落 RocksDB WAL 再异步落盘)机制。
|
||
- **W 要求 chunk 对齐**,因为 WAL 覆盖写必须能独立校验与重放。
|
||
- **R+W 必须先读**,代价最高,通常只在「100 字节级别的覆盖写 + 无缓存 + 有校验和」时出现。
|
||
|
||
#### 两种「未来可能实现」的模式
|
||
|
||
| 代号 | 名称 | 步骤 | 说明 |
|
||
|---|---|---|---|
|
||
| **F** | Fragment lextent space by writing small piece of data into a piecemeal blob | ① write to a piecemeal blob (min_alloc_size 或更大,但只用其中 1 个 block) ② kv commit | 用一个「碎片化收集 blob」来承载零散小写数据(`piecemeal blob` 收集随机的、不连续的小块写)。注意:写 `min_alloc_size` 或更大的 blob,但**只使用其中一个 block** |
|
||
| **X** | WAL read/modify/write on a single block (like legacy bluestore) | ① kv commit ② wal read/modify/write | 单块上的 WAL 读改写,类似 legacy bluestore 行为。**无校验和(No checksum)** |
|
||
|
||
#### 策略选型矩阵(官方原表)
|
||
|
||
官方说明:这张表是「写入类型 → 遇到某种 blob 时的处理方式」的**粗略映射**。实际实现更复杂,因为可能同时有多个候选 blob 可供考虑(例如既可以对 blob A 做 **W**,也可以对 blob B 做 **P**),这张表传达的是大致策略。
|
||
|
||
行 = 写入类型;列 = blob 类型(raw / raw cached / 带 4KB 校验和 / 带 16KB 校验和 / 128KB 压缩):
|
||
|
||
| 写入类型 | raw | raw (cached) | csum (4 KB) | csum (16 KB) | comp (128 KB) |
|
||
|---|---|---|---|---|---|
|
||
| 128+ KB (over)write | U | U | U | U | C |
|
||
| 64 KB (over)write | U | U | U | U | U 或 C |
|
||
| 4 KB overwrite | W | P \| W | P \| W | P \| R+W | P \| N (F?) |
|
||
| 100 byte overwrite | R+W | P \| W | P \| R+W | P \| R+W | P \| N (F?) |
|
||
| 100 byte append | R+W | P \| W | P \| R+W | P \| R+W | P \| N (F?) |
|
||
| 4 KB clone overwrite | P \| N | P \| N | P \| N | P \| N | N (F?) |
|
||
| 100 byte clone overwrite | P \| N | P \| N | P \| N | P \| N | N (F?) |
|
||
|
||
#### 矩阵解读(几条可读出来的规律)
|
||
|
||
1. **大写入(≥64 KB)几乎总是 U(或压缩场景的 C)**:因为写入量已经覆盖整个分配单元,不需要 RMW,直接写新 blob 最省。
|
||
2. **一旦引入校验和(csum 列),小写的代价显著上升**:同样的 4 KB 覆盖写,在 raw 上是 **W**,在 csum(4K) 上是 **P|W**,在 csum(16K) 上退化到 **P|R+W**——因为 `csum_block_size` 越大,一次小写需要读回重算的字节就越多。
|
||
3. **开启压缩(comp 128 KB)后,小覆盖写只能走 N**:因为压缩 blob 无法原地覆盖,只能写新 blob(**N**),或在未来版本中用 **F**(piecemeal blob)来聚集碎片。
|
||
4. **raw (cached) 一列比 raw 便宜**:因为缓存里已有数据,可以省掉 R+W 的读,从 `R+W` 降为 `P|W`。
|
||
5. **clone overwrite 一律是 P|N**:克隆出的对象需要做 copy-on-write,因此要么写未使用区域(P),要么写新 blob(N)。
|
||
|
||
> 💡 补充:矩阵中的 `|` 表示「备选」,不是固定的优先级;官方未给出在 P 与 W 同时可行时的选择判据,这部分逻辑在源码 `src/os/bluestore/BlueStore.cc` 的 `_do_write` 相关路径中,本册不展开。
|
||
|
||
> 引自:[BlueStore Internals](https://docs.ceph.com/en/tentacle/dev/bluestore/)
|
||
|
||
---
|
||
|
||
### 1.3 SeaStore:Crimson 的新存储引擎(seastore.rst)
|
||
|
||
SeaStore 是 **Crimson**(Ceph 的 Seastar 化 OSD)配套的新一代存储引擎。官方这一页有 325 行,是本章内容最完整的一页。
|
||
|
||
#### 1.3.1 目标与基础(Goals and Basics)
|
||
|
||
| 目标 | 说明 |
|
||
|---|---|
|
||
| **面向 NVMe 设备** | 不主要关注 pmem 或 HDD |
|
||
| **使用 SPDK** | 做用户态驱动的 IO |
|
||
| **使用 Seastar futures 编程模型** | 以支撑 run-to-completion 与分片式(sharded)内存/处理模型 |
|
||
| **零拷贝 / 最少拷贝** | 当与基于 Seastar + DPDK 的 messenger 结合时,读写路径上做到零(或极小)数据拷贝 |
|
||
|
||
#### 1.3.2 设计动机与背景(Motivation and background)
|
||
|
||
核心矛盾:
|
||
|
||
```text
|
||
全闪存设备内部以 segment 为单位组织:
|
||
- segment 可以被高效写入
|
||
- 但必须整体擦除(erase)
|
||
NVMe 设备本身对「segment 内哪些数据是 live(未被逻辑丢弃)」所知有限
|
||
→ 设备内部的垃圾回收(GC)因此效率低下
|
||
```
|
||
|
||
解决思路:**设计一种对下层 GC 友好的 on-disk 布局,并把垃圾回收上移到更高层来做**。
|
||
|
||
具体做法:
|
||
|
||
1. **所有数据顺序地流式写入设备上的大 segment**。SSD 硬件内部的 segment 大小通常在数百 MB 到数十 GB 量级。
|
||
2. **SeaStore 的逻辑 segment 理想情况下应与硬件 segment 完美对齐**。但实践中很难确定设备几何信息,也很难向设备提示「正在写的 LBA 应对齐到底层硬件」。
|
||
3. **最坏情况下的兜底**:把逻辑 segment 设为物理 segment 的 **5 倍**,这样大约只有 **~20%** 的数据是错位的。
|
||
4. **达到某个利用率阈值后,把清理工作混入正在进行的写工作负载中**,从先前写过的 segment 里疏散(evacuate)还活着的数据。
|
||
5. **一旦一个 segment 完全空闲,就可以 discard 整个 segment**,让设备擦除并回收它。
|
||
6. **关键:把一小部分清理工作混入每一个写事务**,以避免写延迟出现尖峰(spike)和剧烈波动(variance)。
|
||
|
||
为什么不用 fine-grained discard(细粒度 discard)向设备传达意图?官方给出的理由是:**实践中 discard 在设备固件和中间软件层里实现得很差**。
|
||
|
||
#### 1.3.3 数据布局基础(Data layout basics)
|
||
|
||
- 一个或多个 core/shard 会同时对设备进行读写。
|
||
- **每个 shard 有自己独立的数据,并流式写入自己打开的 segment。**
|
||
- 支持 **streams** 的设备可以被相应地提示(hint),使得来自不同 shard 的数据不会在底层介质上混杂。
|
||
|
||
**持久内存(Persistent Memory)**:待上述的「顺序写」设计成熟后,会引入持久内存用于承载元数据和缓存结构。(官方只是一句展望,未给出方案。)
|
||
|
||
#### 1.3.4 整体设计(Design)
|
||
|
||
设计** heavily based on f2fs 和 btrfs**。
|
||
|
||
- **每个 reactor 管理自己的 root。**
|
||
- 在重用一个 segment 之前,先把其中所有 live block 重写到某个 open segment。
|
||
- 因为只向 open segment 顺序写,所以**稳态下每写入 1 字节,就必须清理 1 字节的已有 segment**。
|
||
- 因此**必须预留一部分可用容量**,以保证写放大(write amplification)维持在可接受水平。官方给出的数字是「20% for 2x?」,并标注 **TODO: find prior work**(即这个数字没有被严谨论证)。
|
||
- **明确的设计选择:避免后台 GC 机制**。理由有二:
|
||
1. 后台 GC 会使操作成本估算复杂化;
|
||
2. 后台 GC 容易引入不确定的延迟行为(non-deterministic latency)。
|
||
- 因此需要一套**能够把 block 重定位工作内联(inline)进正常客户端 IO** 的结构。
|
||
|
||
**两棵基础元数据树**:
|
||
|
||
| 树 | 作用 | 备注 |
|
||
|---|---|---|
|
||
| `onode_by_hobject` | 映射 `ghobject_t` → `onode_t` | 后文 Logical Layout 小节写作「onode tree: maps hobject_t to onode_t」 |
|
||
| `lba_tree` | ① 找到一个 segment 内的 live block;② 把内部引用与物理位置解耦 | 后文写作「lba_tree: maps lba_t to lba_range_t」 |
|
||
|
||
> ⚠️ 注意:官方在 Design 小节说第一棵树是 `ghobject_t -> onode_t`,在 Logical Layout 小节说是 `hobject_t -> onode_t`。**文档自身存在表述不一致**,本册原样记录。
|
||
|
||
每个 `onode` **直接包含 xattrs**,以及 **omap 树 与 extent 树的树根**。
|
||
(优化方向:足够小的对象应该能整个塞进 onode 里。)
|
||
|
||
#### 1.3.5 Segment 布局(Segment Layout)
|
||
|
||
**Segment 三态**:`empty`、`open`、`closed`。
|
||
|
||
**Segment 的字节内容 = 一串 record(记录)序列**。
|
||
|
||
**Record 结构**:
|
||
|
||
```text
|
||
record: [ header | delta | delta... | block | block ... ]
|
||
segment: [ record ... ]
|
||
```
|
||
|
||
- `header`:包含长度(length)与校验和(checksums)。
|
||
- `delta`:描述对某个 block 的一次**逻辑变更**(logical mutation)。
|
||
- `block`:一个**对齐的 extent**,由 `<segment_id_t, segment_offset_t>` 寻址。
|
||
- **一个事务 = 构造一个 record(组合若干 delta 与更新后的 block),并把它写到一个 open segment。**
|
||
|
||
**Segment 会很大**(大约 ≥256MB),因此通常不会有太多个。
|
||
|
||
**每个 shard 保持 N 个 segment 处于 open 状态用于写入**:
|
||
|
||
| 设备类型 | N 的取值 |
|
||
|---|---|
|
||
| HDD | 大概是 1(在单个 shard 上) |
|
||
| NVME/SSD | 大概是 2/shard:一个用作 "journal",一个用作已完成的 data record,因为它们的生命周期不同 |
|
||
|
||
官方明确指出:**open segment 的确切数量、以及如何在它们之间划分写入,是一个调优问题** —— gc/layout 应当保持灵活。实践中的目标大概是**按预期生命周期对 block 分区**,使得一个 segment 要么全是长生命周期 block,要么全是短生命周期 block。
|
||
|
||
**相关源码**:
|
||
|
||
- Journal 实现:`src/crimson/os/seastore/journal.h`
|
||
- 大部分 seastore 结构定义:`src/crimson/os/seastore/seastore_types.h`
|
||
- 后端物理层以 segment 为单位抽象出来:`src/crimson/os/seastore/segment_manager.h`
|
||
|
||
#### 1.3.6 Journal 与原子性(Journal and Atomicity)
|
||
|
||
- **某一个 open segment 被指定为 journal。**
|
||
- **一个事务由一个原子写入的 record 表示。**
|
||
- 一个 record 包含:① 作为该事务一部分写入的 block;② 对已有物理 extent 的逻辑变更 delta。
|
||
- **事务的 delta 总是写入 journal。**
|
||
- **如果事务还关联着写到其他 segment 的 block,那么「包含这些 delta 的最终 record」必须等到那些 block 都持久化之后才能写入。**
|
||
|
||
**崩溃恢复流程(Crash recovery)**:
|
||
|
||
1. 找到包含当前 journal 起点的 segment;
|
||
2. 加载 root node;
|
||
3. 重放(replay)delta;
|
||
4. 按需把 block 加载进 cache。
|
||
|
||
源码:`src/crimson/os/seastore/journal.h`
|
||
|
||
#### 1.3.7 Block Cache
|
||
|
||
**每个 block 处于两种状态之一**:
|
||
|
||
| 状态 | 含义 |
|
||
|---|---|
|
||
| `clean` | 可能在 cache 中,也可能不在;读操作可能导致其进入 cache,也可能不会 |
|
||
| `dirty` | 该 record 的当前版本需要叠加(overlay)来自 journal 的 delta;**必须完整存在于 cache 中** |
|
||
|
||
**Journal trim(裁剪)**:
|
||
|
||
- 必须周期性地 trim journal,否则就得从「时间起点」开始重放 journal delta。
|
||
- Trim 的方法是**创建一个 checkpoint**:重写 root block 与所有当前 dirty block。
|
||
- **Journal checkpoint 可以做得相对不频繁,且不必阻塞写流。**
|
||
|
||
**Delta 不一定是字节范围修改(byte range modification)** —— 这一点很重要:
|
||
|
||
> 考虑一个 btree 节点,其结构是「key 在左、value 在右」(提升点查 / key scan 性能的常见技巧)。在这个节点的最小位置插入一个 key/value,需要移动大量字节——如果纯粹用一串字节操作来表达,代价会非常昂贵(或者非常冗长)。
|
||
>
|
||
> 因此,**每个 delta 都会指明它所对应的 extent 的「类型」以及「位置」,每种 block 类型可以自行实现 `CachedExtent::apply_delta`**。
|
||
|
||
源码:`src/os/crimson/seastore/cached_extent.h`、`src/os/crimson/seastore/cache.h`
|
||
|
||
#### 1.3.8 GC(垃圾回收)
|
||
|
||
**基本约束**:在重用一个 segment 之前,必须先重定位其中所有 live block。因为只向 empty segment 顺序写,所以**每向当前 open segment 写 1 字节,就需要清理 1 字节的已有 closed segment**。
|
||
|
||
与 Design 小节一致:**避免后台工作**,因此**每个 seastore reactor 会内联插入足够的清理工作,使清理 segment 的速率与到达的操作速率相同**。
|
||
|
||
**如何廉价地识别死块(dead block)**:
|
||
|
||
- 为了让稀疏 segment 的清理变得便宜,需要一种能**正向识别死块**的方法。
|
||
- 做法:**每写一个 block,就往 lba tree 里加一个条目,其中包含指向该 segment 中前一个 lba 的指针**(即形成链表)。
|
||
- **任何移动 block、或修改已有 block 引用集的事务,都会包含更新 lba tree 所需的 delta/block**,以更新或删除前一个 block 分配。
|
||
- 因此 **GC 状态只需维护**:
|
||
- 一个指向「当前正在清理的 segment」在 lba tree segment 链表中的迭代器(某种意义上的);
|
||
- 一个指向「下一个待检查 record」的指针。
|
||
- **注意**:不在分配树(allocation tree)里的 record **仍可能包含 root**(例如 allocation tree block 本身),所以必须检查 record 元数据中**指示 root block 的标志位**。
|
||
|
||
**清理判据**:对每个事务,评估一个关于「当前可用空间」与「当前 live 空间」的启发式函数,以决定是否需要做清理工作(可以简单到就是一个 live/used 空间比例区间)。
|
||
|
||
> ⚠️ 官方明确写道:**`TODO: there is not yet a GC implementation`** —— 到 Tentacle v20.2.4 为止,SeaStore 的 GC 尚未实现。
|
||
|
||
#### 1.3.9 逻辑布局(Logical Layout)
|
||
|
||
在上述 block + delta 语义之上,构建两棵 root 级树:
|
||
|
||
| 树 | 映射 |
|
||
|---|---|
|
||
| onode tree | `hobject_t` → `onode_t` |
|
||
| lba_tree | `lba_t` → `lba_range_t` |
|
||
|
||
**寻址方式**:
|
||
|
||
- 上述结构都由「以 delta 编码变更」的 block 组成;树的每个节点对应一个 block。
|
||
- **每个 block 要么是物理寻址(physically addressed),要么是逻辑寻址(logically addressed)**:
|
||
|
||
| 类型 | 适用对象 | 寻址方式 |
|
||
|---|---|---|
|
||
| 物理寻址 | root block、lba_tree 节点 | 由 `paddr_t`(`<segment_id_t, segment_off_t>` 二元组)定位,并在 record 中被标记为 physically addressed |
|
||
| 逻辑寻址 | 其余一切 | 由 `laddr_t` 寻址,需要查 `lba_tree` 才能定位 |
|
||
|
||
**原子性的来源**:因为 **cache/transaction 机制位于 lba tree 之下**,所以「对 lba tree 与其他结构的原子变更」可以通过**简单地把两者放进同一个事务**来表示。
|
||
|
||
#### 1.3.10 LBAManager / BtreeLBAManager
|
||
|
||
- `LBAManager` 接口的实现负责管理**逻辑 → 物理**的映射。源码:`crimson/os/seastore/lba_manager.h`。
|
||
- `BtreeLBAManager` 在 `Journal` 与 `SegmentManager` 之上,用 **wandering btree** 方法直接实现该接口。
|
||
|
||
**相对地址问题(关键设计点)**:
|
||
|
||
> 因为 `SegmentManager` 不允许我们预测一个已提交 record 的位置(这是 **SMR 与 Zone 设备**的共同属性),所以**同一事务内创建的 block 之间的引用必然是「相对地址」**。
|
||
|
||
`BtreeLBAManager` 维护的不变量(invariant):
|
||
|
||
| 条件 | 内存中 block 副本包含的地址类型 |
|
||
|---|---|
|
||
| `!is_pending()` | **只包含绝对地址**;`on_commit` 与 `complete_load` 基于实际 block 地址填充,`on_delta_write` 基于刚提交的 record 填充 |
|
||
| `is_pending() && is_initial_pending` | **block_relative**(因为它们将被写到原始 block 位置) |
|
||
| `is_pending()` 且非 initial_pending | **record_relative**(值将被写入 delta) |
|
||
|
||
#### 1.3.11 TransactionManager
|
||
|
||
`TransactionManager` 负责在 `Journal`、`SegmentManager`、`Cache`、`LBAManager` 之上提供一个统一接口。使用者可以基于逻辑地址分配和变更 extent,**segment cleaning 在后台处理**。
|
||
|
||
源码:`crimson/os/seastore/transaction_manager.h`
|
||
|
||
#### 1.3.12 Next Steps(官方列出的未完成项)
|
||
|
||
| 模块 | 待办 |
|
||
|---|---|
|
||
| Journal | ① 支持扫描一个 segment 以找到物理寻址的 block;② 支持 trim journal 并释放 segment |
|
||
| Cache | ① 支持重写 dirty block —— 需要给 `CachedExtent` 增加查找/更新依赖 block 的能力;② 需要支持把 dirty block 写出加到 `try_construct_record` 里 |
|
||
| LBAManager | ① 支持 pinning;② 支持 `segment -> laddr` 供 GC 使用;③ 支持定位 segment 中剩余的已用 block |
|
||
| GC | ① 初始实现;② `BtreeLBAManager` 支持跟踪 segment 中的已用 block;③ 识别待清理 segment 的启发式 |
|
||
| Other | ① 支持周期性生成 journal checkpoint;② Onode tree;③ Extent tree;④ 剩余的 ObjectStore 集成工作 |
|
||
|
||
#### 1.3.13 ObjectStore 的适配问题:split、merge 与 sharding
|
||
|
||
**当前的 ObjectStore 需求之一**:能够**在 O(1) 时间内拆分(split)一个 collection(PG)**。从 **mimic** 开始,还需要能够**把两个 collection 合并成一个**(即 split 的严格逆操作)。
|
||
|
||
**冲突点**:
|
||
|
||
1. 我们 split 出来的那些 PG,在当前的 sharding 方案下会 hash 到 OSD 的不同 shard 上。
|
||
2. 可以想象用一种「临时映射」替代该 sharding 方案,把较小的子 PG 引导到正确的 shard(因为我们通常随后就会把该 PG 迁移到另一个 OSD)。
|
||
3. **但这对 merge 场景没帮助**:merge 时,组成部分可能一开始就在不同 shard 上,最终却需要被放进同一个 collection(并且要能通过单个事务来操作)。
|
||
|
||
**结论**:我们很可能需要一种机制,让**通过某个 shard 写入的数据能够「切换所有权」(switch ownership),之后被另一个 shard 读取和管理**。
|
||
|
||
> 引自:[SeaStore](https://docs.ceph.com/en/tentacle/dev/seastore/)
|
||
|
||
---
|
||
|
||
### 1.4 Zoned Storage(zoned-storage.rst)
|
||
|
||
#### 背景
|
||
|
||
Zoned Storage 是一类存储设备,通过让主机与存储设备协作来实现更高容量、更高吞吐与更低延迟。分区存储接口目前通过以下标准提供:
|
||
|
||
| 标准 | 适用设备 |
|
||
|---|---|
|
||
| SCSI **ZBC**(Zoned Block Commands) | 叠瓦式磁记录(SMR)硬盘 |
|
||
| ATA **ZAC**(Zoned Device ATA Command Set) | 同上 |
|
||
| NVMe **ZNS**(Zoned Namespaces) | NVMe SSD(当时尚未普及) |
|
||
|
||
项目主页:http://zonedstorage.io
|
||
|
||
#### 项目目标
|
||
|
||
1. 使 Ceph 能够运行在 zoned storage 设备上;
|
||
2. 同时探索采用这种新接口所涉及的研究问题。
|
||
|
||
**第一阶段目标**:在 **host-managed SMR(HM-SMR)** 驱动器上支持**非覆盖写(non-overwrite)负载**(例如 RGW),并探索**清理(garbage collection)策略**。HM-SMR 是带 ZBC/ZAC 接口的大容量硬盘。
|
||
**长期目标**:支持 **ZNS SSD**(随其可用性推进),以及**覆盖写(overwrite)负载**。
|
||
|
||
#### 引入的新组件:`ZonedFreelistManger`
|
||
|
||
官方说明的补丁序列:
|
||
|
||
1. **第一个 patch**:使得可以向 HM-SMR 驱动器写数据。
|
||
2. **本 patch**:引入 `ZonedFreelistManger` —— 一个 `FreelistManager` 的实现,它向 `ZonedAllocator` 传递足够的信息,通过**跟踪每个 zone 的写指针(write pointer)与死字节数(number of dead bytes per zone)**来正确初始化 zone 状态。
|
||
3. **第三个(计划中的)patch**:引入一个初步的 cleaner,作为后续研究的基线。
|
||
|
||
**为什么必须引入新的 FreelistManager?**
|
||
|
||
| | 状态数 | 能表达的状态 |
|
||
|---|---|---|
|
||
| 现有 `BitmapFreelistManager` | 2 | `empty`、`used` |
|
||
| 分区设备上的一块磁盘区域 | 3 | `empty`、`used`、`dead` |
|
||
|
||
**只跟踪两种状态无法准确初始化 `ZonedAllocator` 中的 zone 状态。**
|
||
|
||
#### 当前能力状态
|
||
|
||
官方声称可以做到:
|
||
|
||
- 在运行于 HM-SMR 驱动器的 OSD 上执行基本的 RADOS benchmark;
|
||
- 重启 OSD;
|
||
- 读取已写入的数据,并写入新数据。
|
||
|
||
#### 示例:在 HM-SMR 上跑 vstart 集群
|
||
|
||
```bash
|
||
$ sudo zbd report -i -n /dev/sdc
|
||
Device /dev/sdc:
|
||
Vendor ID: ATA HGST HSH721414AL T240
|
||
Zone model: host-managed
|
||
Capacity: 14000.520 GB (27344764928 512-bytes sectors)
|
||
Logical blocks: 3418095616 blocks of 4096 B
|
||
Physical blocks: 3418095616 blocks of 4096 B
|
||
Zones: 52156 zones of 256.0 MB
|
||
Maximum number of open zones: no limit
|
||
Maximum number of active zones: no limit
|
||
52156 / 52156 zones
|
||
|
||
$ MON=1 OSD=1 MDS=0 sudo ../src/vstart.sh --new --localhost --bluestore \
|
||
--bluestore-devs /dev/sdc --bluestore-zoned
|
||
|
||
$ sudo ./bin/ceph osd pool create bench 32 32
|
||
$ sudo ./bin/rados bench -p bench 10 write --no-cleanup
|
||
```
|
||
|
||
重启(**注意第二次不带 `--new`**):
|
||
|
||
```bash
|
||
$ sudo ../src/stop.sh
|
||
$ MON=1 OSD=1 MDS=0 sudo ../src/vstart.sh --localhost --bluestore \
|
||
--bluestore-devs /dev/sdc --bluestore-zoned
|
||
$ sudo ./bin/rados bench -p bench 10 rand
|
||
```
|
||
|
||
官方给出的实测数据(使用 `HGST HSH721414AL`,52156 个 256MB zone):
|
||
|
||
| 负载 | 带宽 | 平均 IOPS | 平均延迟 |
|
||
|---|---|---|---|
|
||
| 4 MB 顺序写(`write --no-cleanup`,首次) | 153.333 MB/s | 38 | 0.411226 s |
|
||
| 4 MB 随机读(`rand`,重启后) | 251.558 MB/s | 62 | 0.249385 s |
|
||
| 4 MB 顺序写(重启后再次写) | 约 143 MB/s | — | 约 0.436 s |
|
||
|
||
联系人:Abutalib Aghayev <aghayev@psu.edu>
|
||
|
||
> 引自:[Zoned Storage Support](https://docs.ceph.com/en/tentacle/dev/zoned-storage/)
|
||
|
||
---
|
||
|
||
## 二、PG 与 peering
|
||
|
||
本章覆盖 `placement-group.rst` 与 `peering.rst`。**peering 是 Ceph 中最难理解的部分**,2.2 节会尽可能讲透。
|
||
|
||
### 2.1 PG 的内部结构与状态(placement-group.rst)
|
||
|
||
#### 官方页面的定位
|
||
|
||
页面开头第一句就说明了它的性质:
|
||
|
||
> `Miscellaneous copy-pastes from emails, when this gets cleaned up it should move out of /dev.`
|
||
|
||
即:**这是一堆邮件片段的拼贴,整理好之后应该从 `dev/` 移走。** 因此这页的风格是问答式的,而不是规范式的。
|
||
|
||
#### 2.1.1 概述:为什么需要 PG
|
||
|
||
| 概念 | 说明 |
|
||
|---|---|
|
||
| **PG = placement group** | 放置数据时,对象被映射到 PG,PG 再被映射到 OSD |
|
||
| **为什么要这层间接?** | 为了**把对象分组**,从而减少需要维护的 per-object 元数据量、以及需要运行的进程数量。如果不分组,按对象跟踪(例如)placement history 会**昂贵到不可行** |
|
||
| **PG 数量的权衡** | 增加 PG 数可以降低集群中 per-OSD 负载的方差,但**每个 PG 都会在存放它的 OSD 上多消耗一点 CPU 和内存** |
|
||
| **经验值** | 大致按 **100 PGs/OSD** 估算;官方说「根据你的集群不同,可以有很大偏差而不会产生不良影响」 |
|
||
| **PG 类别** | 存在几种不同类别的 PG。(原文提到:在原始邮件作者的 `ceph -s` 输出里有 6 个 "local" PG,它们绑定在特定 OSD 上,但**在标准 Ceph 配置中并不使用**) |
|
||
|
||
#### 2.1.2 映射算法(简化版)
|
||
|
||
官方以问答形式给出:
|
||
|
||
| 问题 | 回答 |
|
||
|---|---|
|
||
| Object→PG 是怎么映射的?一个 PG 上会映射多个对象吗?一个对象会映射到多个 PG 吗? | **多个对象映射到一个 PG。每个对象恰好映射到一个 PG。** |
|
||
| PG→OSD 呢?一个 PG 只属于一个 OSD 吗? | **一个 PG 映射到一个 OSD 列表**,列表中第一个是 primary,其余是 replica。**多个 PG 可以映射到同一个 OSD。** |
|
||
| 一个 PG 代表固定大小的存储空间吗? | **不代表。** PG 只是对象的一个分组;你配置你想要的 PG 数量(OSD 数 × 100 是个不错的起点),所有存储的对象伪随机、均匀地分布到这些 PG 上。所以一个 PG **明确地不代表固定的存储量,它代表你 OSD 上存储量的 1/pg_num**。 |
|
||
|
||
**伪代码(忽略 CRUSH 与自定义放置的细节)**:
|
||
|
||
```python
|
||
locator = object_name
|
||
obj_hash = hash(locator)
|
||
pg = obj_hash % num_pg
|
||
OSDs_for_pg = crush(pg) # 返回一个 OSD 列表
|
||
primary = osds_for_pg[0]
|
||
replicas = osds_for_pg[1:]
|
||
```
|
||
|
||
**`crush()` 的极简心智模型**(官方原文:想象一个「真空中的完美球形数据中心」——即所有 OSD 权重都是 1.0、数据中心没有拓扑结构、所有 OSD 都在顶层、使用默认值等,此时它退化为一致性哈希):
|
||
|
||
```python
|
||
def crush(pg):
|
||
all_osds = ['osd.0', 'osd.1', 'osd.2', ...]
|
||
result = []
|
||
# size 是副本数;primary + replicas
|
||
while len(result) < size:
|
||
r = hash(pg)
|
||
chosen = all_osds[r % len(all_osds)]
|
||
if chosen in result:
|
||
# 同一个 OSD 只能被选中一次
|
||
continue
|
||
result.append(chosen)
|
||
return result
|
||
```
|
||
|
||
#### 2.1.3 用户可见的 PG 状态(完整清单)
|
||
|
||
官方在此处有一个 `.. todo:: diagram of states and how they can overlap`(状态图及它们如何重叠的示意图尚未绘制)。
|
||
|
||
本册把全部 28 个状态整理成表(顺序与官方一致,释义逐条对应原文):
|
||
|
||
| 状态 | 官方释义 |
|
||
|---|---|
|
||
| `creating` | PG 正在被创建 |
|
||
| `active` | 发往该 PG 的请求会被处理 |
|
||
| `clean` | PG 中所有对象都已按正确的副本数复制 |
|
||
| `down` | 一个持有必要数据的副本挂了,因此该 PG 离线 |
|
||
| `recovery_unfound` | recovery 无法完成,因为有对象处于 unfound 状态 |
|
||
| `backfill_unfound` | backfill 无法完成,因为有对象处于 unfound 状态 |
|
||
| `premerge` | 由于即将发生 PG merge,该 PG 处于 IO 静默(quiesced-IO)状态。发生在 `pg_num_pending < pg_num` 时,适用于满足 `pg_num_pending <= ps < pg_num` 的 PG,以及它要合并进去的那个对应 peer PG |
|
||
| `scrubbing` | PG 正在被检查不一致性 |
|
||
| `degraded` | PG 中部分对象尚未达到足够的副本数 |
|
||
| `inconsistent` | PG 的各副本不一致(例如对象大小错误、recovery **结束后**某副本仍缺对象等) |
|
||
| `peering` | PG 正在经历 peering 过程 |
|
||
| `repair` | PG 正在被检查,发现的任何不一致都会被修复(如果可能) |
|
||
| `recovering` | 对象正在被迁移 / 与副本同步 |
|
||
| `backfill_wait` | PG 正在排队等待开始 backfill |
|
||
| `incomplete` | PG 缺少一段必要的历史记录(log)。**如果看到这个状态,请报 bug,并尝试启动任何可能含有所需信息的、已失败的 OSD** |
|
||
| `stale` | PG 处于未知状态 —— monitor 自 PG 映射变更后就没有收到过它的更新 |
|
||
| `remapped` | PG 被临时映射到一组与 CRUSH 指定不同的 OSD |
|
||
| `deep` | 与 `scrubbing` 同时出现时,表示这是一次 deep scrub |
|
||
| `backfilling` | recovery 的一种特殊情形:**扫描并同步 PG 的全部内容**,而不是根据近期操作的 PG log 推断需要传输什么 |
|
||
| `backfill_toofull` | backfill 预留被拒绝,OSD 太满 |
|
||
| `recovery_wait` | PG 正在等待本地 / 远程 recovery 预留 |
|
||
| `undersized` | 给定其 size,PG 无法选出足够的 OSD |
|
||
| `activating` | PG 已经 peered,但尚未 active |
|
||
| `peered` | PG 已经 peered,但无法进入 active |
|
||
| `snaptrim` | PG 正在 trim 快照 |
|
||
| `snaptrim_wait` | PG 已排队等待 trim 快照 |
|
||
| `recovery_toofull` | recovery 预留被拒绝,OSD 太满 |
|
||
| `snaptrim_error` | PG 由于错误无法完成 snap trimming |
|
||
| `forced_recovery` | PG 已被标记为最高优先级 recovery |
|
||
| `forced_backfill` | PG 已被标记为最高优先级 backfill |
|
||
| `failed_repair` | 一次修复该 PG 的尝试失败了,需要人工介入 |
|
||
|
||
> 💡 补充:这组状态在代码中对应 `PG_STATE_*` 位标志(见 `src/include/object.h` / `src/osd/osd_types.h`),因此多个状态可以同时出现(典型如 `active+clean`、`active+degraded+remapped+backfilling`)。官方在这里的 TODO 正是「画一张状态图说明它们如何重叠」。
|
||
|
||
#### 2.1.4 OMAP 统计(OMAP STATISTICS)
|
||
|
||
Omap 统计在 **deep scrub** 期间收集,并显示在以下命令的输出中:
|
||
|
||
```bash
|
||
ceph pg dump
|
||
ceph pg dump all
|
||
ceph pg dump summary
|
||
ceph pg dump pgs
|
||
ceph pg dump pools
|
||
ceph pg ls
|
||
```
|
||
|
||
**重要限制**:由于这些统计**不是持续更新的**,在「deep scrub 运行得不频繁」或「omap 活动很频繁」的环境中,它们**可能相当不准确**。因此不应依赖它们来获得精确数值,而应**只作为参考(guide)**。
|
||
|
||
**想获得当前 omap 用量的较好指示**:跑一次 deep scrub,并**紧接着**检查这些统计。
|
||
|
||
> 引自:[PG (Placement Group) notes](https://docs.ceph.com/en/tentacle/dev/placement-group/)
|
||
|
||
---
|
||
|
||
### 2.2 peering(peering.rst)—— Ceph 最难的部分
|
||
|
||
> 本节是全册的重点。peering 决定了 Ceph 在故障、重启、网络分区之后,如何**在不丢失已确认写入的前提下**重新建立一致性。理解它需要同时理解:**acting set / up set、interval、PG log、missing set、past intervals、up_thru、authoritative history**。
|
||
|
||
#### 2.2.1 概念表(Concepts)
|
||
|
||
官方定义逐条整理(保留英文术语):
|
||
|
||
| 术语 | 定义 |
|
||
|---|---|
|
||
| **Peering** | 让存放某个 PG 的所有 OSD **就该 PG 中所有对象的状态、以及这些对象关联的所有元数据达成一致**的过程。注意:两个 OSD 可以就 PG 中对象的状态达成一致,却**仍未必持有最新的内容** |
|
||
| **Acting set** | 负责某个 PG 的**有序 OSD 列表**(是「某个 epoch 时」或「曾经是」的负责者) |
|
||
| **Up set** | 根据 CRUSH,某个特定 epoch 下负责某个 PG 的**有序 OSD 列表**。它与 acting set 相同,**除非** acting set 被通过 OSDMap 中的 **PG temp** 显式覆盖 |
|
||
| **PG temp** | 在回填(backfill)primary OSD 期间使用的**临时** PG acting set |
|
||
| **current interval / past interval** | 一段 OSD map epoch 序列,在此期间某 PG 的 **acting set 与 up set 都不发生变化** |
|
||
| **primary** | acting set 中负责**协调 peering** 的成员。也是**唯一**接受客户端发起的、写往该 PG 中对象的写操作的 OSD。**按惯例,primary 是 acting set 的第一个成员** |
|
||
| **replica** | PG 的 acting set 中的**非 primary** OSD。一个 replica 已被确认为非 primary OSD,且**已被 primary activate** |
|
||
| **stray** | 一个**不是**当前 acting set 成员、且**尚未被通知删除**其某 PG 副本的 OSD |
|
||
| **recovery** | 确保 PG 中所有对象的副本都存在于 acting set 的所有 OSD 上的过程。**peering 完成后**,primary 就可以开始接受写操作,**recovery 则在后台进行** |
|
||
| **PG info** | 关于 PG 的基本元数据:创建 epoch、该 PG 最近一次写入的 version、**last epoch started**、**last epoch clean**、以及 **current interval 的起点**。任何 OSD 间关于 PG 的通信都包含 PG info,这样任何知道某 PG 存在(或曾经存在)的 OSD 也就有了 **last epoch clean 或 last epoch started 的一个下界** |
|
||
| **PG log** | 对 PG 中对象所做的最近更新的列表。在 acting set 中所有 OSD 都确认了这些变更之后,这些 log 可以被截断 |
|
||
| **missing set** | 所有「内容尚未更新到与 log 条目一致」的对象的集合。missing set 由**每个 OSD 各自**整理。missing set 是按 `<OSD,PG>` 粒度跟踪的 |
|
||
| **Authoritative History** | 一个**完整的、全序的**操作集合,能够把某个 OSD 的 PG 副本更新到最新状态 |
|
||
| **epoch** | (单调递增的)OSD map 版本号 |
|
||
| **last epoch start** | acting set 中所有节点就一份 **authoritative history** 达成一致的**最后一个 epoch**。在 last epoch 开始时,peering 被认为已经成功 |
|
||
| **up_thru** | 在 primary 能够成功完成 peering 之前,它必须通过让 monitor 在 osd map 中设置其 **up_thru**,来告知 monitor 它「活到了当前 OSD map epoch」。这有助于 peering **忽略那些 peering 从未完成的先前 acting set** |
|
||
| **last epoch clean** | acting set 中所有节点**完全最新**(既包括 PG 的 log,也包括 PG 的对象内容)的最后一个 epoch。此时 recovery 被认为已经完成 |
|
||
|
||
#### 2.2.2 PG temp 的工作方式(官方原文示例)
|
||
|
||
```text
|
||
假设 acting set = [0,1,2],且我们处于 active+clean。
|
||
现在发生了一些事,acting set 变成 [3,1,2]。
|
||
在这种情况下,OSD 3 是空的,即使它是 primary 也无法服务读。
|
||
```
|
||
|
||
处理流程:
|
||
|
||
1. `osd.3` 会响应:使用一条 **`MOSDPGTemp`** 消息,向 monitor 请求一个 **PG temp = [1,2,3]**;
|
||
2. `osd.1` 临时成为 primary;
|
||
3. `osd.1` 会选择 `osd.3` 作为 backfill peer,并在 `osd.3` 被 backfill 期间继续服务读写;
|
||
4. **backfill 完成后,PG temp 被丢弃**;acting set 变回 `[3,1,2]`,`osd.3` 成为 primary。
|
||
|
||
#### 2.2.3 Golden Rule(黄金规则)—— 整个 peering 的基石
|
||
|
||
> **对任何 PG 的任何写操作,在该 PG acting set 的所有成员都将其持久化之前,不会向客户端确认。**
|
||
|
||
由此推出的两个关键结论:
|
||
|
||
1. **可恢复性**:如果自上次成功 peering 以来,我们能与**每个 acting set 中的至少一个成员**通信,那么一定有人持有自上次成功 peering 以来每一次(已确认的)操作的记录。
|
||
2. **可构造性**:因此,当前的 primary 应当**总是有可能**构造并分发一份新的 **authoritative history**。
|
||
|
||
#### 2.2.4 OSD map 在 peering 中的角色
|
||
|
||
官方强调三点:
|
||
|
||
1. 当 OSD 上线或下线(或被添加/移除)时,这**可能影响很多 PG 的 active set**。
|
||
2. **在 primary 成功完成 peering 之前,OSD map 必须反映出「该 OSD 在 current interval 的第一个 epoch 时是活的、健康的」**。
|
||
3. **只有在 peering 成功之后,才能进行变更。**
|
||
|
||
综合起来:**一个新的 primary 可以利用最新的 OSD map,外加近期 map 的历史,生成一组 past intervals,从而确定在能够成功 peer 之前必须咨询哪些 OSD。**
|
||
|
||
- **past intervals 的集合由 last epoch started 界定** —— last epoch started 是「我们知道 peering 已完成」的**最近的**那个 past interval。
|
||
- **OSD 最初发现某个 PG 存在的途径,是通过交换 PG info 消息**,因此 OSD 总是对 last epoch started 有某个下界。
|
||
|
||
#### 2.2.5 up_thru 到底解决什么问题(官方给出的失败序列)
|
||
|
||
`up_thru` 的定义里给出了一个具体的失败序列,这是理解 up_thru 的关键:
|
||
|
||
```text
|
||
* acting set = [A,B]
|
||
* acting set = [A]
|
||
* acting set = [] (紧随其后,例如同时发生故障但检测有先后)
|
||
* acting set = [B] (B 重启了,A 没有)
|
||
```
|
||
|
||
**问题**:如果没有 up_thru,peering 会去咨询那些「peering 从未真正完成」的旧 acting set,从而可能基于不完整的历史做出判断。
|
||
|
||
**解决**:primary 必须让 monitor 把它的 `up_thru` 设到当前 OSD map epoch,以此证明「我确实活到了当前 epoch」。这样 peering 就可以**忽略掉那些 peering 从未完成的先前 acting set**。
|
||
|
||
> 💡 补充:up_thru 与 `last epoch started` 是配合使用的——后者界定「要回溯到哪个 past interval 为止」,前者用来**筛掉**那些「虽然存在过、但 peering 从未完成」的 interval。判据见下面步骤 2。
|
||
|
||
#### 2.2.6 peering 主流程(官方的 10 大步骤)
|
||
|
||
以下是官方 `Description of the Peering Process` 的完整流程,本册按原文逐步展开。
|
||
|
||
**前提**:当前 PG primary 执行下述过程。
|
||
|
||
---
|
||
|
||
**步骤 1:获取最近的 OSD map**
|
||
|
||
- 目的:① 识别出所有**有意义的 acting set** 的成员;② 确认我们自己仍然是 primary。
|
||
|
||
---
|
||
|
||
**步骤 2:生成自 last epoch started 以来的 past interval 列表,并筛选**
|
||
|
||
- 生成自 **last epoch started** 以来的所有 past interval。
|
||
- **筛选条件**:考虑那些「**在最后一个 interval epoch 的 OSD map 中,up_thru 大于该 interval 的第一个 epoch**」的 interval 子集。
|
||
- 用人话说:**只保留那些「在 acting set 变成另一组 OSD 之前,peering 有可能已经完成」的 interval**。
|
||
- **成功的 peering 要求我们能够联系到每个 past interval 的 acting set 中的至少一个 OSD。**
|
||
|
||
---
|
||
|
||
**步骤 3:向列表中的每个节点索要 PG info**
|
||
|
||
- PG info 包含:① 该 PG 最近的写入;② 一个 **last epoch started** 的值。
|
||
- **优化**:如果我们得知某个 **last epoch started 比我们自己的更新**,就可以**剪掉(prune)更老的 past interval**,从而减少需要联系的 peer OSD。
|
||
|
||
---
|
||
|
||
**步骤 4:补齐自己的 PG log**
|
||
|
||
- 如果**任何其他节点**的 PG log 里有**我们没有的操作**,就指示它们把这些缺失的 log 条目发给我,使得 **primary 的 PG log 是最新的(包含最新的写入)**。
|
||
|
||
---
|
||
|
||
**步骤 5:对当前 acting set 的每个成员,做两件事**
|
||
|
||
**5.a 索要自 last epoch start 以来的所有 PG log 条目副本**
|
||
|
||
- 目的:验证它们与我的 log 一致(或者,弄明白我将要指示它删除哪些对象)。
|
||
|
||
**分歧(divergence)场景 —— 这是 peering 最反直觉的地方**:
|
||
|
||
```text
|
||
如果集群在某个操作被 acting set 所有成员持久化之前发生故障,
|
||
且随后的 peering 没有记住那个操作,
|
||
而后一个「确实记住了那个操作」的节点重新加入,
|
||
那么它的 log 就会记录一段与「故障后 peering 重建出来的 authoritative history」
|
||
不同的(divergent)历史。
|
||
```
|
||
|
||
**处理方式**:
|
||
|
||
- 由于这些 **divergent 事件没有被该 acting set 的其他 log 记录**,因此**它们没有被向客户端确认过**;
|
||
- 所以**丢弃它们并无害处**(这样所有 OSD 就都对 authoritative history 达成一致了);
|
||
- **但是**,我们必须指示任何存有「来自 divergent 更新的数据」的 OSD,**删除受影响的(现在被认定为伪造的 / apocryphal)对象**。
|
||
|
||
**5.b 索要它的 missing set**
|
||
|
||
- missing set = 在它的 PG log 中记录了、但**它还没有新数据**的那些对象更新。
|
||
- 这份清单就是**在我们能接受写操作之前,必须被完整复制的对象列表**。
|
||
|
||
---
|
||
|
||
**步骤 6:authoritative history 成型**
|
||
|
||
- 此时,**primary 的 PG log 包含了该 PG 的一份 authoritative history**,且该 OSD 已有充足信息可以把 acting set 中的任何其他 OSD 更新到最新。
|
||
|
||
---
|
||
|
||
**步骤 7:确保 up_thru(需要时向 monitor 申请)**
|
||
|
||
- 如果 **primary 在当前 OSD map 中的 up_thru 值没有大于或等于 current interval 的第一个 epoch**:
|
||
- 向 monitor 发送请求以更新它;
|
||
- **等待**直到收到反映了该变更的更新后的 OSD map。
|
||
|
||
---
|
||
|
||
**步骤 8:向当前 acting set 的每个成员分发日志更新**
|
||
|
||
**8.a** 给它们发送 log 更新,使它们的 PG log 与我自己的(authoritative history)一致……**这可能涉及决定删除 divergent 对象**。
|
||
|
||
**8.b** **等待**它们确认已持久化这些 PG log 条目。
|
||
|
||
---
|
||
|
||
**步骤 9:元数据完全一致,开始接受写与数据恢复**
|
||
|
||
此时,acting set 中的所有 OSD 就所有元数据达成一致,并且(在任何未来的 peering 中)都会返回完全相同的更新记录。
|
||
|
||
**9.a 开始接受客户端写操作**
|
||
|
||
- 因为我们对「这些更新将被写入其中的对象的状态」取得了一致同意。
|
||
- **注意**:如果客户端尝试写一个对象,**该对象会被提升到 recovery 队列的最前面**,并且该写操作会在它被完整复制到当前 acting set 之后才被应用。
|
||
|
||
**9.b 更新本地 PG info 中的 last epoch started 值**
|
||
|
||
- 并指示其他 active set OSD 做同样的事。
|
||
|
||
**9.c 开始拉取(pull)其他 OSD 有、而我没有的对象数据更新**
|
||
|
||
- 我们可能需要查询**额外的 past interval** 中的 OSD:
|
||
- **早于 last epoch started**(上一次 peering 完成的时间)的;
|
||
- 以及**晚于 last epoch clean**(上一次 recovery 完成的时间)的;
|
||
- 以便找到所有对象的副本。
|
||
|
||
**9.d 开始推送(push)对象数据更新给还没有这些更新的其他 OSD**
|
||
|
||
**为什么是 primary 推送,而不是让 replica 自己拉取?** 官方给出两条理由:
|
||
|
||
1. 这使得 primary 可以**确保一个 replica 在收到更新写之前已经拥有当前内容**;
|
||
2. 这使得**一次读(从 primary)就可以用于把数据写到多个 replica**。如果每个 replica 各自拉取,数据可能必须被读多次。
|
||
|
||
---
|
||
|
||
**步骤 10:更新 last epoch clean 并解散 stray**
|
||
|
||
- 一旦所有 replica 都存有了所有对象(在本 epoch 开始之前就存在的那些对象)的所有副本,我们就可以:
|
||
- 更新 PG info 中的 **last epoch clean**;
|
||
- **解散(dismiss)所有 stray 副本**,允许它们删除那些「它们已不再属于其 acting set」的对象的副本。
|
||
|
||
**为什么在此之前不能解散 stray?** 官方解释:
|
||
|
||
> 因为有可能这些 stray 中的某一个,持有某个**旧对象的唯一存活副本**(该对象的所有副本在当前 acting set 成员完成复制之前就都消失了)。
|
||
|
||
#### 2.2.7 全流程时序图(本册整理)
|
||
|
||
```text
|
||
primary replicas / past interval OSDs monitor
|
||
| | |
|
||
|--(1) 获取最近 OSDMap --------------------------------------------->|
|
||
| | |
|
||
|--(2) 计算 past intervals(用 up_thru 过滤) |
|
||
| | |
|
||
|--(3) PG info 请求 ----------------->| |
|
||
|<---- PG info(含 last_epoch_started)-| |
|
||
| (若别人的 les 更新 → prune 更老的 interval) |
|
||
| | |
|
||
|--(4) 请求缺失 log 条目 ------------->| |
|
||
|<---- log entries -------------------| |
|
||
| | |
|
||
|--(5a) 请求自 last epoch start 的 log->| |
|
||
|<---- log(发现 divergent → 标记待删)| |
|
||
|--(5b) 请求 missing set ------------->| |
|
||
|<---- missing set -------------------| |
|
||
| | |
|
||
|==(6) primary 的 PG log = authoritative history == |
|
||
| | |
|
||
|--(7) up_thru 不足? 请求更新 -------------------------------------->|
|
||
|<------------------------- 更新后的 OSDMap(up_thru 已反映)---------|
|
||
| | |
|
||
|--(8a) 分发 log 更新(含删除 divergent 对象)------------------------>|
|
||
|--(8b) 等待持久化 ack --------------------------------------------->|
|
||
|<------------------------------- ack --------------------------------|
|
||
| | |
|
||
|==(9) 元数据完全一致 == |
|
||
| 9a 开始接受客户端写(未恢复的对象会被提到 recovery 队列最前) |
|
||
| 9b 更新 last epoch started(并通知 replicas) |
|
||
| 9c pull 别人有我没有的对象(可能查更早的 past interval OSD) |
|
||
| 9d push 我有没有的对象给 replicas |
|
||
| | |
|
||
|==(10) 所有对象齐备 == |
|
||
| 更新 last epoch clean |
|
||
| 解散 stray(允许其删除过期副本) |
|
||
```
|
||
|
||
#### 2.2.8 如何生成 peering 状态机图
|
||
|
||
官方给出了从源码自动生成最新 peering 状态模型的方法:
|
||
|
||
```bash
|
||
$ git clone https://github.com/ceph/ceph.git
|
||
$ cd ceph
|
||
$ cat src/osd/PeeringState.h src/osd/PeeringState.cc | doc/scripts/gen_state_diagram.py > doc/dev/peering_graph.generated.dot
|
||
$ sed -i 's/7,7/1080,1080/' doc/dev/peering_graph.generated.dot
|
||
$ dot -Tsvg doc/dev/peering_graph.generated.dot > doc/dev/peering_graph.generated.svg
|
||
```
|
||
|
||
关键源码文件:**`src/osd/PeeringState.h` 与 `src/osd/PeeringState.cc`**(类 `PeeringState` 及其状态机)。
|
||
生成脚本:**`doc/scripts/gen_state_diagram.py`**。
|
||
|
||
> ⚠️ 官方页面上嵌入的示例状态图是 `peering_graph.generated.svg`(一个图片文件),**不是 rst 文本**,因此本册无法从源文中还原出具体的状态节点与转移边。想看完整状态机请按上面的命令自行生成,或直接读 `src/osd/PeeringState.h` 中的 `PeeringState` 状态定义(如 `Initial`、`Reset`、`Started`、`Primary`、`WaitActingChange`、`Stray` 等)。
|
||
|
||
> 引自:[Peering](https://docs.ceph.com/en/tentacle/dev/peering/)
|
||
|
||
---
|
||
|
||
## 三、Messenger 与网络
|
||
|
||
本章覆盖 `messenger.rst`、`msgr2.rst`、`network-protocol.rst`、`network-encoding.rst`、`encoding.rst`、`dpdk.rst` 六页。
|
||
|
||
### 3.1 Messenger 笔记(messenger.rst)
|
||
|
||
#### 官方页面的实际内容
|
||
|
||
**`messenger.rst` 全文只有 33 行**,内容**只有一句话的概述 + 一个 benchmark 工具的使用说明**。它**没有**描述:
|
||
|
||
- `Messenger` 抽象基类的接口;
|
||
- `Connection` / `Dispatcher` 的语义;
|
||
- `Policy`(连接策略,如 `lossy` / `stateless_server` / `server` / `client`);
|
||
- `AsyncMessenger` 的线程模型(Worker / EventCenter / EventCallback);
|
||
- 消息 throttle / 优先级队列。
|
||
|
||
官方原文全部信息:
|
||
|
||
1. **Messenger 是 Ceph 的网络层实现。当前 Ceph 支持一种 messenger 类型:`"async"`。**
|
||
2. 其余内容全部是关于 **`ceph_perf_msgr`** 这个 benchmark 工具的。
|
||
|
||
#### `ceph_perf_msgr`:messenger 模块的专用基准测试
|
||
|
||
**用途**:只对 messenger 模块做 benchmark,帮助定位 messenger 模块内部的瓶颈或耗时点。官方比喻:它就像 "iperf"。
|
||
|
||
**服务端启动**:
|
||
|
||
```bash
|
||
# ./ceph_perf_msgr_server 172.16.30.181:10001 1 0
|
||
```
|
||
|
||
| 参数位置 | 含义 |
|
||
|---|---|
|
||
| 第 1 个参数 | `ip:port` 对,指明客户端需要指定的目标地址 |
|
||
| 第 2 个参数 | 配置服务端线程数 |
|
||
| 第 3 个参数 | 指定 dispatch 消息时的 "think time"(微秒) |
|
||
|
||
**关于 "think time" 的官方解释**:
|
||
|
||
> 在 Giant 之后,`CEPH_OSD_OP` 消息(即真正的客户端读/写 io 请求)会**被快速派发(fast dispatched)**,不排队到 Dispatcher,以获得更好的性能。所以 `CEPH_OSD_OP` 消息会被**内联处理**,"think time" 就是用来**模拟这个 "inline process" 过程**的。
|
||
|
||
**客户端启动**:
|
||
|
||
```bash
|
||
# ./ceph_perf_msgr_client 172.16.30.181:10001 1 32 10000 10 4096
|
||
```
|
||
|
||
| 参数位置 | 含义 |
|
||
|---|---|
|
||
| 第 1 个 | 服务端 `ip:port` |
|
||
| 第 2 个 | 客户端线程数 |
|
||
| 第 3 个 | 并发度(每个客户端线程的最大 inflight 消息数) |
|
||
| 第 4 个 | 每个客户端线程要向服务端发出的 io 数量 |
|
||
| 第 5 个 | 客户端线程在接收消息时的 "think time",也是用来模拟客户端的 fast dispatch 过程 |
|
||
| 第 6 个 | 要发出的消息的数据长度 |
|
||
|
||
> 引自:[Messenger notes](https://docs.ceph.com/en/tentacle/dev/messenger/)
|
||
|
||
---
|
||
|
||
### 3.2 msgr2 协议(msgr2.0 与 msgr2.1)(msgr2.rst)
|
||
|
||
msgr2 是对 **SimpleMessenger 实现的 legacy Ceph on-wire 协议**的一次修订,用于解决**性能**与**安全**问题。官方这一页有 956 行,是本章最详尽的一页。
|
||
|
||
#### 3.2.1 修订目标(Goals)
|
||
|
||
| 目标 | 说明 |
|
||
|---|---|
|
||
| **Flexible handshaking** | 原协议没有足够灵活的协议协商机制,无法支持那些「非必需」的特性 |
|
||
| **Encryption** | 将引入线上加密(encryption over the wire) |
|
||
| **Performance** | 提供协议层面的特性(例如 padding),以尽可能把计算与内存拷贝排除在 fast path 之外 |
|
||
| **Signing** | 允许流量被签名(但未必加密)。**官方标注:This is not implemented(尚未实现)** |
|
||
|
||
#### 3.2.2 定义(Definitions)
|
||
|
||
| 术语 | 定义 |
|
||
|---|---|
|
||
| **client (C)** | 发起(TCP)连接的一方 |
|
||
| **server (S)** | 接受(TCP)连接的一方 |
|
||
| **connection** | 两个进程之间一个(TCP)连接的实例 |
|
||
| **entity** | 一个 ceph entity 实例,例如 `osd.0`。每个 entity 凭借 `nonce` 字段(通常是一个 pid 或随机值)拥有一个或多个唯一的 `entity_addr_t` |
|
||
| **session** | 两个 entity 之间的**有状态会话**,其中消息交换是**有序且无损的**。如果发生中断(TCP 连接断开),**一个 session 可以跨越多个 connection** |
|
||
| **frame** | peer 之间发送的一个离散消息。每个 frame 由一个 tag(类型码)、payload、以及(如果启用了签名或加密)一些其他字段组成 |
|
||
| **tag** | 与 frame 关联的类型码,tag 决定 payload 的结构 |
|
||
|
||
#### 3.2.3 连接的四个阶段(Phases)
|
||
|
||
|
||
```text
|
||
1. banner
|
||
2. authentication frame exchange
|
||
3. message flow handshake frame exchange
|
||
4. message frame exchange
|
||
```
|
||
|
||
#### 3.2.4 Banner
|
||
|
||
client 与 server 在连接建立后**双方都发送**一个 banner:
|
||
|
||
```text
|
||
"ceph v2\n"
|
||
__le16 banner payload length
|
||
banner payload
|
||
```
|
||
|
||
banner payload 的形式:
|
||
|
||
```text
|
||
__le64 peer_supported_features
|
||
__le64 peer_required_features
|
||
```
|
||
|
||
- 这是一个**全新的、独立的 feature bit 命名空间(`CEPH_MSGR2_*`)**。
|
||
- 目前只定义了 **`CEPH_MSGR2_FEATURE_REVISION_1`**。它是 **supported 但 not required**,因此 **msgr2.0 与 msgr2.1 的 peer 可以互相通信**。
|
||
- 如果远端宣告了我们不支持的 required feature,我们可以断开连接。
|
||
|
||
```text
|
||
+---------+ +--------+
|
||
| Client | | Server |
|
||
+---------+ +--------+
|
||
| send banner |
|
||
|----+ +----|
|
||
| | | |
|
||
| +-------+--->|
|
||
| send banner| |
|
||
|<-----------+ |
|
||
| |
|
||
```
|
||
|
||
#### 3.2.5 Frame 格式
|
||
|
||
**前置条件**:banner 交换之后,**所有后续通信都以 frame 形式进行**。frame 的确切格式取决于连接模式。
|
||
|
||
**四种连接模式**:`msgr2.0-crc`、`msgr2.0-secure`、`msgr2.1-crc`、`msgr2.1-secure`。
|
||
**所有连接都从 crc 模式开始**(是 `msgr2.0-crc` 还是 `msgr2.1-crc`,取决于 banner 中的 `peer_supported_features`)。
|
||
|
||
##### 通用 preamble(32 字节)
|
||
|
||
```text
|
||
__u8 tag
|
||
__u8 number of segments
|
||
{
|
||
__le32 segment length
|
||
__le16 segment alignment
|
||
} * 4
|
||
__u8 flags
|
||
reserved (1 byte)
|
||
__le32 preamble crc
|
||
```
|
||
|
||
规则:
|
||
|
||
- **一个空 frame 有一个(空的)segment。**
|
||
- **一个非空 frame 可以有 1 到 4 个 segment**,除最后一个外,其余 segment 都可以是空的。
|
||
- 如果 segment 少于 4 个,**未使用的(末尾的)segment length 与 segment alignment 字段被置零**。
|
||
- **当前支持的 flag**:`FRAME_EARLY_DATA_COMPRESSED`(见「压缩」一节)。
|
||
- **Reserved 字节被置零。**
|
||
- **preamble 校验和是 CRC32-C**,覆盖它之前的全部内容(**28 字节**),且**无论连接模式如何**(即使 frame 被加密)都会计算并校验。
|
||
|
||
##### msgr2.0-crc 模式
|
||
|
||
```text
|
||
preamble (32 bytes)
|
||
{
|
||
segment payload
|
||
} * number of segments
|
||
epilogue (17 bytes)
|
||
```
|
||
|
||
epilogue:
|
||
|
||
```text
|
||
__u8 late_flags
|
||
{
|
||
__le32 segment crc
|
||
} * 4
|
||
```
|
||
|
||
**`late_flags` 用于 frame 中止(frame abortion)**:
|
||
|
||
> 在传输完 preamble 和第一个 segment 之后,发送方可以用**零**填满剩余 segment,并**设置一个标志**指示接收方必须丢弃该 frame。这使得发送方在一个正在上线(put on the wire)的 frame **被撤销(revoked,即从 messenger 中被抽走)**时,**可以避免额外的缓冲**:payload buffer 可以被立即 unpin 并交还给用户,**无需做拷贝,也无需阻塞到整个 frame 传输完成**。
|
||
>
|
||
> 目前只有 kernel client 使用这个机制,见 `ceph_msg_revoke()`。
|
||
|
||
**segment 校验和规则**:
|
||
|
||
- 校验和是 **CRC32-C**。
|
||
- 对于「被使用的」空 segment,它被设为 **`(__le32)-1`**。
|
||
- 对于「未使用的」(末尾的)segment,它被**置零**。
|
||
- **CRC 只是为了防护比特错误(bit errors)**。**不提供任何真实性保证** —— 这与 msgr1 不同,msgr1 尝试通过「用 session key 对 segment 长度和 crc 做可选签名」来提供某种程度的真实性保证。
|
||
|
||
**官方列出的两个已知问题(Issues)**:
|
||
|
||
1. **ceph_msg_header2 的 crc 被移到了 epilogue**
|
||
- 背景:为了引入一个「segment 数量可变、同时适用于控制帧与消息帧」的通用 frame 结构,msgr2.0 把消息帧第一个 segment(`ceph_msg_header2`)的 crc 移到了 epilogue。
|
||
- **后果**:`ceph_msg_header2` **再也无法在整个 frame 被从线上读完之前被安全地解释**。
|
||
- **这是相对 msgr1 的退步(regression)**:因为要想「把 payload 直接 scatter 到用户提供的 buffer 中」,以避免接收消息帧时的额外缓冲与拷贝,就**必须提前拿到 `ceph_msg_header2`**——它存放着用户 buffer 赖以索引的 **transaction id**。
|
||
- **实现必须二选一**:要么放弃这个优化,要么在**未校验的 segment** 上行动。
|
||
2. **`late_flags` 没有被任何 crc 覆盖**
|
||
- 因为它存放 abort 标志,所以**单比特翻转**可能导致:① 一个已完成的 frame 被丢弃(导致发送方挂起等待回复);② 更糟,一个被中止的、segment payload 是垃圾数据的 frame 被派发(dispatch)。
|
||
- **这个问题在 msgr1 中就存在,并被带到了 msgr2.0。**
|
||
|
||
##### msgr2.1-crc 模式(相对 msgr2.0-crc 的差异)
|
||
|
||
| # | 差异 |
|
||
|---|---|
|
||
| 1 | **第一个 segment 的 crc 存放在第一个 segment 的末尾,而不是 epilogue 里。** epilogue 最多存 **3 个** crc,而不是 4 个。如果第一个 segment 为空,**不生成 `(__le32)-1` crc** |
|
||
| 2 | **只有当 frame 有超过一个 segment 时才生成 epilogue**(即第 2 到第 4 个 segment 中至少有一个非空)。理由:如果 frame 只有一个 segment,它就无法被 abort,而且 epilogue 里也没有 crc 可存 |
|
||
| 3 | **未校验的 `late_flags` 被替换为 `late_status`**,后者通过「每个标志使用一个 4-bit nibble,以及两个 Hamming Distance = 4 的码字(且不是全 0 或全 1)」内建了比特错误检测。**代价是只剩一个保留标志位** |
|
||
|
||
msgr2.1-crc 的 epilogue:
|
||
|
||
```text
|
||
__u8 late_status
|
||
{
|
||
__le32 segment crc
|
||
} * 3
|
||
```
|
||
|
||
**示例帧(官方原文)**:
|
||
|
||
```text
|
||
* 0+0+0+0 帧(空,无 epilogue):
|
||
preamble (32 bytes)
|
||
|
||
* 20+0+0+0 帧(无 epilogue):
|
||
preamble (32 bytes)
|
||
segment1 payload (20 bytes)
|
||
__le32 segment1 crc
|
||
|
||
* 0+70+0+0 帧:
|
||
preamble (32 bytes)
|
||
segment2 payload (70 bytes)
|
||
epilogue (13 bytes)
|
||
|
||
* 20+70+0+350 帧:
|
||
preamble (32 bytes)
|
||
segment1 payload (20 bytes)
|
||
__le32 segment1 crc
|
||
segment2 payload (70 bytes)
|
||
segment4 payload (350 bytes)
|
||
epilogue (13 bytes)
|
||
```
|
||
|
||
#### 3.2.6 Hello
|
||
|
||
**`TAG_HELLO`:client→server 且 server→client**
|
||
|
||
```text
|
||
__u8 entity_type
|
||
entity_addr_t peer_socket_address
|
||
```
|
||
|
||
作用:我们立即共享自己的 entity type 以及 **peer 的地址**(后者可用于检测我们的有效 IP 地址,**尤其在存在 NAT 的情况下**)。
|
||
|
||
#### 3.2.7 认证阶段(Authentication)
|
||
|
||
| Tag | 方向 | 载荷 |
|
||
|---|---|---|
|
||
| `TAG_AUTH_REQUEST` | client→server | `__le32 method;`(`CEPH_AUTH_{NONE, CEPHX, ...}`)<br>`__le32 num_preferred_modes;`<br>`list<__le32> mode`(`CEPH_CON_MODE_*`)<br>`method specific payload` |
|
||
| `TAG_AUTH_BAD_METHOD` | server→client | `__le32 method`<br>`__le32 negative error result code`<br>`__le32 num_methods`<br>`list<__le32> allowed_methods`(`CEPH_AUTH_{NONE, CEPHX, ...}`)<br>`__le32 num_modes`<br>`list<__le32> allowed_modes`(`CEPH_CON_MODE_*`) |
|
||
| `TAG_AUTH_REPLY_MORE` | server→client | `__le32 len;`<br>`method specific payload` |
|
||
| `TAG_AUTH_REQUEST_MORE` | client→server | `__le32 len;`<br>`method specific payload` |
|
||
| `TAG_AUTH_DONE` | server→client | `__le64 global_id`<br>`__le32 connection mode`(`CEPH_CON_MODE_*`)<br>`method specific payload` |
|
||
|
||
要点:
|
||
|
||
- `TAG_AUTH_BAD_METHOD` 返回:被尝试的 auth method、错误码(**若该 method 不被支持,则为 `-EOPNOTSUPP`**)、以及允许的认证方法列表。
|
||
- **`TAG_AUTH_DONE`:由 server 决定认证是否完成,以及最终的连接模式是什么。**
|
||
|
||
**正常流程(客户端使用了被允许的认证方法)**:
|
||
|
||
```text
|
||
+---------+ +--------+
|
||
| Client | | Server |
|
||
+---------+ +--------+
|
||
| auth request |
|
||
|---------------->|
|
||
|<----------------|
|
||
| auth more|
|
||
| |
|
||
|auth more |
|
||
|---------------->|
|
||
|<----------------|
|
||
| auth done|
|
||
```
|
||
|
||
**客户端首次尝试了一个被禁止的认证方法**:
|
||
|
||
```text
|
||
| auth request |
|
||
|---------------->|
|
||
|<----------------|
|
||
| bad method |
|
||
| |
|
||
| auth request |
|
||
|---------------->|
|
||
|<----------------|
|
||
| auth more|
|
||
| |
|
||
| auth more |
|
||
|---------------->|
|
||
|<----------------|
|
||
| auth done|
|
||
```
|
||
|
||
#### 3.2.8 认证后的 frame 格式(Post-auth frame format)
|
||
|
||
根据 `TAG_AUTH_DONE` 协商出的连接模式,连接要么**停留在 crc 模式**,要么**切换到对应的 secure 模式**(`msgr2.0-secure` 或 `msgr2.1-secure`)。
|
||
|
||
##### msgr2.0-secure 模式
|
||
|
||
```text
|
||
{
|
||
preamble (32 bytes)
|
||
{
|
||
segment payload
|
||
zero padding (out to 16 bytes)
|
||
} * number of segments
|
||
epilogue (16 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
```
|
||
|
||
epilogue:
|
||
|
||
```text
|
||
__u8 late_flags
|
||
zero padding (15 bytes)
|
||
```
|
||
|
||
- `late_flags` 的含义与 `msgr2.0-crc` 模式相同。
|
||
- **每个 segment 与 epilogue 都被零填充到 16 字节。**
|
||
- **为什么需要 padding?** 官方解释:严格来说 **GCM 不需要任何 padding**,因为 Counter mode(GCM 中的 C)本质上把块密码变成了流密码。**但是**,如果整体输入长度不是 16 字节的倍数,GCM 用于生成 auth tag 的 **GHASH 函数只能作用于 16 字节块**,因此内部还是会发生隐式的零填充。
|
||
|
||
**官方列出的两个已知问题**:
|
||
|
||
1. **解密预言机(decryption oracle)问题**
|
||
- 发送方用**单个 nonce** 加密整个 frame,并生成**单个 auth tag**。
|
||
- 由于 segment 长度存放在 preamble 里,**接收方别无选择,只能在不校验 auth tag 的情况下解密并解释 preamble** —— 否则它连「要从线上读多少字节才能拿到 auth tag」都无从得知!
|
||
- 这**创造了一个 decryption oracle**,它与 **Counter mode 的可延展性(malleability)**结合,可能导致敏感信息被恢复。
|
||
- **这个问题同样延伸到消息帧的第一个 segment**:与 `msgr2.0-crc` 模式一样,`ceph_msg_header2` 无法在整个 frame 读完之前被安全解释。
|
||
2. **nonce 复用风险(CVE-2020-1759)**
|
||
- 使用**确定性 nonce 构造**:一个 **4 字节 counter 字段**后面跟一个 **8 字节固定字段**。初始值取自 connection secret(认证阶段生成的随机字节串)。
|
||
- 由于 counter 字段只有 4 字节,它**可能在不到一天的时间内回绕并重复**,导致 **GCM nonce 重用**,进而**可能完全丧失该连接的真实性与机密性**。
|
||
- **缓解措施:在 counter 重复之前断开连接(CVE-2020-1759)。**
|
||
|
||
##### msgr2.1-secure 模式(相对 msgr2.0-secure 的差异)
|
||
|
||
| # | 差异 |
|
||
|---|---|
|
||
| 1 | **preamble、第一个 segment、以及 frame 的其余部分被分别加密**,使用**各自的 nonce** 并生成**各自的 auth tag**。这消除了「使用未验证的明文」的问题,并使 `msgr2.1-secure` 模式与 `msgr2.1-crc` 模式保持接近,使实现可以用类似的方式接收消息帧(几乎不需要缓冲、同样的 scatter/gather 逻辑等)。**为了减少每帧的加/解密操作次数,preamble 被扩展了一个固定大小的内联缓冲区(48 字节)**,第一个 segment 会被(全部或部分)内联进去。preamble 的 auth tag 同时覆盖 preamble 与内联缓冲区,所以**如果第一个 segment 小到可以完整内联,它在单次解密操作后就可用了** |
|
||
| 2 | 与 `msgr2.1-crc` 一样,**只有当 frame 有超过一个 segment 时才生成 epilogue**。理由更充分:因为那需要一次额外的加/解密操作 |
|
||
| 3 | 为与 `msgr2.1-crc` 保持一致,`late_flags` 被替换为 `late_status`(内建的比特错误检测在 secure 模式下其实并不真的需要) |
|
||
| 4 | 遵循 **NIST Recommendation for GCM**:使用**确定性 nonce 构造**,一个 **4 字节固定字段**后面跟一个 **8 字节 counter 字段**。8 字节 counter 字段**永远不应重复**,但为 `msgr2.0-secure` 设置的 nonce 重用保护仍然存在。初始值与 `msgr2.0-secure` 相同 |
|
||
|
||
**padding 规则**:
|
||
|
||
- 与 `msgr2.0-secure` 一样,每个 segment 被零填充到 16 字节。
|
||
- **如果第一个 segment 被完整内联,它的 padding 进入内联缓冲区;否则 padding 在剩余部分上。**
|
||
- 推论:**内联缓冲区以 16 字节为单位被消耗。**
|
||
- **内联缓冲区中未使用的部分被置零。**
|
||
|
||
**示例帧(官方原文,全部 6 种)**:
|
||
|
||
```text
|
||
* 0+0+0+0 帧(空,无可内联内容,无 epilogue):
|
||
{
|
||
preamble (32 bytes)
|
||
zero padding (48 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
|
||
* 20+0+0+0 帧(第一个 segment 完整内联,无 epilogue):
|
||
{
|
||
preamble (32 bytes)
|
||
segment1 payload (20 bytes)
|
||
zero padding (28 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
|
||
* 0+70+0+0 帧(无可内联内容):
|
||
{
|
||
preamble (32 bytes)
|
||
zero padding (48 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
{
|
||
segment2 payload (70 bytes)
|
||
zero padding (10 bytes)
|
||
epilogue (16 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
|
||
* 20+70+0+350 帧(第一个 segment 完整内联):
|
||
{
|
||
preamble (32 bytes)
|
||
segment1 payload (20 bytes)
|
||
zero padding (28 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
{
|
||
segment2 payload (70 bytes)
|
||
zero padding (10 bytes)
|
||
segment4 payload (350 bytes)
|
||
zero padding (2 bytes)
|
||
epilogue (16 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
|
||
* 105+0+0+0 帧(第一个 segment 部分内联,无 epilogue):
|
||
{
|
||
preamble (32 bytes)
|
||
segment1 payload (48 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
{
|
||
segment1 payload remainder (57 bytes)
|
||
zero padding (7 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
|
||
* 105+70+0+350 帧(第一个 segment 部分内联):
|
||
{
|
||
preamble (32 bytes)
|
||
segment1 payload (48 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
{
|
||
segment1 payload remainder (57 bytes)
|
||
zero padding (7 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
{
|
||
segment2 payload (70 bytes)
|
||
zero padding (10 bytes)
|
||
segment4 payload (350 bytes)
|
||
zero padding (2 bytes)
|
||
epilogue (16 bytes)
|
||
} ^ AES-128-GCM cipher
|
||
auth tag (16 bytes)
|
||
```
|
||
|
||
epilogue:
|
||
|
||
```text
|
||
__u8 late_status
|
||
zero padding (15 bytes)
|
||
```
|
||
|
||
#### 3.2.9 压缩(Compression)
|
||
|
||
压缩握手是**通过 msgr2 基于 feature 的握手机制**实现的。在此阶段,客户端会向服务端表明:是否可以对消息传输使用 on-wire-compression,并附带支持的压缩方法列表。如果客户端与服务端**都启用了** on-wire-compression,服务端会基于客户端的请求与自身偏好选择一个压缩方法。握手完成后,两端都设置好各自的 compression handler(如果需要的话)。
|
||
|
||
**`TAG_COMPRESSION_REQUEST`(client→server)**:声明压缩能力与要求
|
||
|
||
```text
|
||
bool is_compress
|
||
std::vector<uint32_t> preferred_methods
|
||
```
|
||
|
||
- 如果客户端识别出**双方都支持压缩 feature**,它就发起这个握手。
|
||
- `is_compress` 标志表示客户端的配置是否要使用压缩。
|
||
- `preferred_methods` 是客户端支持的压缩算法列表。
|
||
|
||
**`TAG_COMPRESSION_DONE`(server→client)**:确定压缩设置
|
||
|
||
```text
|
||
bool is_compress
|
||
uint32_t method
|
||
```
|
||
|
||
- 服务端根据自己的配置判断压缩是否可行。
|
||
- 如果可行,它会挑选**优先级最高的、同时又被客户端支持**的压缩方法。
|
||
- 如果不存在这样的方法,它就判定这两个 peer 之间的会话**不使用压缩**。
|
||
|
||
```text
|
||
+---------+ +--------+
|
||
| Client | | Server |
|
||
+---------+ +--------+
|
||
| compression request |
|
||
|----------------------->|
|
||
|<-----------------------|
|
||
| compression done |
|
||
```
|
||
|
||
**与安全模式的关系(msgr2.x-secure mode)**:
|
||
|
||
> **压缩与加密结合会引入安全隐患。使用 secure 模式时,除非管理员专门配置,否则无法使用压缩。**
|
||
|
||
**压缩后的 frame 格式(Post-compression frame format)**:
|
||
|
||
根据 `TAG_COMPRESSION_DONE` 协商的连接模式,连接可以接受/发送压缩帧,或把所有帧都按未压缩处理。
|
||
|
||
| 模式 | 行为 |
|
||
|---|---|
|
||
| **msgr2.x-force mode** | 后续通过该连接发送的所有帧**都会**被压缩(如果满足压缩条件,例如帧大小)。对于压缩帧,发送 peer 会启用 **`FRAME_EARLY_DATA_COMPRESSED`** 标志,使接收 peer 能够检测到并解压该帧 |
|
||
| **msgr2.x-none mode** | preamble 中的 `FRAME_EARLY_DATA_COMPRESSED` 标志**被禁用** |
|
||
|
||
#### 3.2.10 Message flow handshake(消息流握手)
|
||
|
||
此阶段中,两端互相标识身份,并且(如果需要)**重连到一个已建立的 session**。
|
||
|
||
**`TAG_CLIENT_IDENT`(client→server)**:标识我们自己
|
||
|
||
```text
|
||
__le32 num_addrs
|
||
entity_addrvec_t*num_addrs entity addrs
|
||
entity_addr_t target entity addr
|
||
__le64 gid (numeric part of osd.0, client.123456, ...)
|
||
__le64 global_seq
|
||
__le64 features supported (CEPH_FEATURE_* bitmask)
|
||
__le64 features required (CEPH_FEATURE_* bitmask)
|
||
__le64 flags (CEPH_MSG_CONNECT_* bitmask)
|
||
__le64 cookie
|
||
```
|
||
|
||
官方注解(逐条):
|
||
|
||
- **客户端先发,服务端用同样的帧回复。** 如果这是一个新 session,客户端和服务端可以继续进行消息交换。
|
||
- **target addr 是客户端试图连接的目标**,这样服务端一侧可以在「客户端找错了守护进程」时关闭连接。
|
||
- **`type.gid`(`entity_name_t`)在此处设置** —— 通过把 hello 帧中共享的 type 与此处的 gid 组合而成。这意味着:① 我们不需要在每个消息的 header 里都带上它;② **也意味着我们不能发送「来自」其他 `entity_name_t` 的消息**。当前实现在 `_send_message` 等的顶部设置它,因此这不应破坏任何现有功能。**实现很可能想要把它与「已认证的凭证所允许的范围」做一次掩码(mask)校验。**
|
||
- **cookie 是客户端用于标识一个 session 的 cookie**,可用于**重连到已存在的 session**。
|
||
- **我们去掉了 msgr1 中的 `protocol_version` 字段。**
|
||
|
||
**`TAG_IDENT_MISSING_FEATURES`(server→client)**:抱怨 `TAG_IDENT` 的 feature 太少
|
||
|
||
```text
|
||
__le64 features we require that the peer didn't advertise
|
||
```
|
||
|
||
**`TAG_SERVER_IDENT`(server→client)**:接受 client ident 并标识服务端
|
||
|
||
```text
|
||
__le32 num_addrs
|
||
entity_addrvec_t*num_addrs entity addrs
|
||
__le64 gid (numeric part of osd.0, client.123456, ...)
|
||
__le64 global_seq
|
||
__le64 features supported (CEPH_FEATURE_* bitmask)
|
||
__le64 features required (CEPH_FEATURE_* bitmask)
|
||
__le64 flags (CEPH_MSG_CONNECT_* bitmask)
|
||
__le64 cookie
|
||
```
|
||
|
||
- **服务端 cookie 可以被客户端在之后断开时使用**,用于重连并恢复 session。
|
||
|
||
**`TAG_RECONNECT`(client→server)**:重连到已建立的 session
|
||
|
||
```text
|
||
__le32 num_addrs
|
||
entity_addr_t * num_addrs
|
||
__le64 client_cookie
|
||
__le64 server_cookie
|
||
__le64 global_seq
|
||
__le64 connect_seq
|
||
__le64 msg_seq (the last msg seq received)
|
||
```
|
||
|
||
**`TAG_RECONNECT_OK`(server→client)**:确认一次重连尝试
|
||
|
||
```text
|
||
__le64 msg_seq (last msg seq received)
|
||
```
|
||
|
||
- **客户端一收到这个,就可以开始消息交换。**
|
||
- **服务端一发出这个,就可以开始消息交换。**
|
||
|
||
**失败类 tag**:
|
||
|
||
| Tag | 方向 | 含义 |
|
||
|---|---|---|
|
||
| `TAG_RECONNECT_RETRY_SESSION` | server only | 由于 **stale connect_seq** 导致重连失败 |
|
||
| `TAG_RECONNECT_RETRY_GLOBAL` | server only | 由于 **stale global_seq** 导致重连失败 |
|
||
| `TAG_RECONNECT_WAIT` | server only | 由于 **connect race(连接竞争)** 导致重连失败。表示服务端已经在连接客户端了,**那个方向应当赢得竞争**。客户端应当等待那个连接完成 |
|
||
| `TAG_RESET_SESSION` | server only | 要求客户端重置 session。`__u8 full` —— `full` 标志表示 peer 是否应做**完全重置**(即丢弃消息队列) |
|
||
|
||
**六种失败场景(官方原文时序图)**:
|
||
|
||
1. **客户端的第一个 `client_ident` 消息丢失,然后客户端重连**
|
||
|
||
```text
|
||
c_cookie(a) | client_ident(a) |
|
||
|-------------X |
|
||
| |
|
||
| client_ident(a) |
|
||
|-------------------->|
|
||
|<--------------------|
|
||
| server_ident(b) | s_cookie(b)
|
||
| |
|
||
| session established |
|
||
```
|
||
|
||
2. **服务端的 `server_ident` 消息丢失,然后客户端重连**
|
||
|
||
```text
|
||
c_cookie(a) | client_ident(a) |
|
||
|-------------------->|
|
||
| X------------|
|
||
| server_ident(b) | s_cookie(b)
|
||
| |
|
||
| client_ident(a) |
|
||
|-------------------->|
|
||
|<--------------------|
|
||
| server_ident(c) | s_cookie(c)
|
||
| |
|
||
| session established |
|
||
```
|
||
|
||
3. **服务端的 `server_ident` 消息丢失,然后服务端重连**
|
||
|
||
```text
|
||
c_cookie(a) | client_ident(a) |
|
||
|-------------------->|
|
||
| X------------|
|
||
| server_ident(b) | s_cookie(b)
|
||
| |
|
||
| reconnect(a, b) |
|
||
|<--------------------|
|
||
|-------------------->|
|
||
| reset_session(F) |
|
||
| |
|
||
| client_ident(a) | c_cookie(a)
|
||
|<--------------------|
|
||
|-------------------->|
|
||
s_cookie(c) | server_ident(c) |
|
||
```
|
||
|
||
4. **session 建立后连接失败,然后客户端重连**
|
||
|
||
```text
|
||
c_cookie(a) | session established | s_cookie(b)
|
||
|<------------------->|
|
||
| X------------|
|
||
| |
|
||
| reconnect(a, b) |
|
||
|-------------------->|
|
||
|<--------------------|
|
||
| reconnect_ok |
|
||
```
|
||
|
||
5. **session 建立后因为服务端 reset 导致连接失败,然后客户端重连**
|
||
|
||
```text
|
||
c_cookie(a) | session established | s_cookie(b)
|
||
|<------------------->|
|
||
| X------------| reset
|
||
| |
|
||
| reconnect(a, b) |
|
||
|-------------------->|
|
||
|<--------------------|
|
||
| reset_session(RC*) |
|
||
| |
|
||
c_cookie(c) | client_ident(c) |
|
||
|-------------------->|
|
||
|<--------------------|
|
||
| server_ident(d) | s_cookie(d)
|
||
```
|
||
|
||
> `RC*` 表示 reset session 的 full 标志取决于该连接的 `policy.resetcheck`。
|
||
|
||
6. **session 建立后因为客户端 reset 导致连接失败,然后客户端重连**
|
||
|
||
```text
|
||
c_cookie(a) | session established | s_cookie(b)
|
||
|<------------------->|
|
||
reset | X------------|
|
||
| |
|
||
c_cookie(c) | client_ident(c) |
|
||
|-------------------->|
|
||
|<--------------------| reset if policy.resetcheck
|
||
| server_ident(d) | s_cookie(d)
|
||
```
|
||
|
||
#### 3.2.11 消息交换(Message exchange)
|
||
|
||
| Tag | 载荷 | 说明 |
|
||
|---|---|---|
|
||
| `TAG_MSG` | `ceph_msg_header2` + `front` + `middle` + `data_pre_padding` + `data` | 一条消息 |
|
||
| `TAG_ACK` | `__le64 seq` | 确认收到消息。**只用于有状态 session** |
|
||
| `TAG_KEEPALIVE2` | `ceph_timespec stamp` | 检查连接存活。**时间戳是发送方本地的** |
|
||
| `TAG_KEEPALIVE2_ACK` | `ceph_timestamp stamp` | 对 keepalive2 的回复。**时间戳取自所响应的那个 `TAG_KEEPALIVE2`** |
|
||
| `TAG_CLOSE` | — | 终止一个连接。表示连接应当被终止。**等价于一次 hangup 或 reset(即应当触发 `ms_handle_reset`)**。它并非严格必要或有用,因为我们本可以直接断开 TCP 连接 |
|
||
|
||
**`ceph_msg_header2` 相对 `ceph_msg_header` 的修改**(官方列出 3 点):
|
||
|
||
1. **包含一个 `ack_seq`**。这避免了大多数情况下对 `TAG_ACK` 消息的需要。
|
||
2. **去掉了 `src` 字段**,我们现在从消息流握手(`TAG_IDENT`)中获得它。
|
||
3. **指定了 `data_pre_padding` 长度**,可用于调整 data payload 的对齐。
|
||
- 官方在此处带了一个疑问:`(NOTE: is this is useful?)` —— **官方自己也不确定这个字段是否有用**。
|
||
|
||
#### 3.2.12 状态机(官方 graphviz)
|
||
|
||
**客户端侧状态机**:
|
||
|
||
```text
|
||
start_connect --<connected>--> banner_connecting
|
||
|
||
subgraph "hello banner exchange":
|
||
banner_connecting --"banner exchange"--> hello_connecting
|
||
hello_connecting --"hello exchange"--> banner_connecting
|
||
|
||
banner_connecting --<exchange done>--> auth_connecting
|
||
auth_connecting --"auth reply more"--> auth_connecting
|
||
auth_connecting --"auth bad method"--> auth_connecting
|
||
auth_connecting --"auth done"--> auth_connecting_sign
|
||
auth_connecting_sign --"auth signature"--> session_connecting
|
||
session_connecting --"wait"--> wait
|
||
wait --<backoff>--> start_connect
|
||
session_connecting --"ident missing features"--> closed
|
||
session_connecting --"server ident"--> ready
|
||
ready --"keep alive"--> ready
|
||
```
|
||
|
||
> 官方对 `session_connecting --"server ident"--> ready` 这条边的 tooltip:`set peer_name, peer_addr and connection features`。
|
||
|
||
**服务端侧状态机**:
|
||
|
||
```text
|
||
start_accept --<accepted>--> banner_accepting
|
||
|
||
subgraph "hello banner exchange":
|
||
banner_accepting --"banner exchange"--> hello_accepting
|
||
hello_accepting --"hello exchange"--> banner_accepting
|
||
|
||
banner_accepting --<exchange done>--> auth_accepting
|
||
auth_accepting --"auth_request => 0"--> auth_accepting_more
|
||
auth_accepting --"auth_request => 1"--> auth_accepting_sign
|
||
auth_accepting_more --"auth_request => 0"--> auth_accepting_more
|
||
auth_accepting_more --"auth_request => 1"--> auth_accepting_sign
|
||
auth_accepting_more --"auth_request => EBUSY"--> standby
|
||
auth_accepting_more --"auth_request => *"--> auth_accepting_more
|
||
auth_accepting --"auth_request => EBUSY"--> standby
|
||
auth_accepting --"send <auth bad method>"--> auth_accepting
|
||
auth_accepting_sign --"auth signature"--> session_accepting
|
||
session_accepting --"reconnect"--> session_accepting
|
||
session_accepting --"ident missing features"--> closed
|
||
session_accepting --"client ident"--> ready
|
||
ready --"keep alive"--> ready
|
||
```
|
||
|
||
> 官方对 `session_accepting --"client ident"--> ready` 这条边的 tooltip:`set connection features`。
|
||
|
||
> 引自:[msgr2 protocol (msgr2.0 and msgr2.1)](https://docs.ceph.com/en/tentacle/dev/msgr2/)
|
||
|
||
---
|
||
|
||
### 3.3 网络协议(network-protocol.rst)
|
||
|
||
> 官方在此页开头明确建议:**为了理解这些结构是如何定义的,建议先阅读 `network-encoding` 的介绍。**
|
||
|
||
#### 3.3.1 Hello 阶段
|
||
|
||
协议以一个握手开始,用于确认**双方都在说 ceph**,并共享一些基本信息。
|
||
|
||
##### Banner
|
||
|
||
- **第一个动作是 server 向 client 发送 banner。** banner 定义在 `src/include/msgr.h` 的 **`CEPH_BANNER`** 中。
|
||
- banner 之后是 **server 的地址**,然后是 **client 的地址**,各自编码为一个 **`entity_addr_t`**。
|
||
- **一旦 client 验证 server 的 banner 与自己的匹配,它就回复自己的 banner 与自己的地址。**
|
||
|
||
##### Connect
|
||
|
||
banner 验证完毕、地址交换完成后,开始连接协商。**首先客户端发送一个 `ceph_msg_connect` 结构**,携带它的信息:
|
||
|
||
```cpp
|
||
// From src/include/msgr.h
|
||
struct ceph_msg_connect {
|
||
u64le features; // Supported features (CEPH_FEATURE_*)
|
||
u32le host_type; // CEPH_ENTITY_TYPE_*
|
||
u32le global_seq; // Number of connections initiated by this host.
|
||
u32le connect_seq; // Number of connections initiated in this session.
|
||
u32le protocol_version;
|
||
u32le authorizer_protocol;
|
||
u32le authorizer_len;
|
||
u8 flags; // CEPH_MSG_CONNECT_*
|
||
u8 authorizer[authorizer_len];
|
||
}
|
||
```
|
||
|
||
| 字段 | 含义 |
|
||
|---|---|
|
||
| `features` | 支持的特性(`CEPH_FEATURE_*`) |
|
||
| `host_type` | `CEPH_ENTITY_TYPE_*` |
|
||
| `global_seq` | 该主机发起过的连接数 |
|
||
| `connect_seq` | 该 session 中发起过的连接数 |
|
||
| `protocol_version` | 协议版本 |
|
||
| `authorizer_protocol` | 认证器协议 |
|
||
| `authorizer_len` | 认证器长度 |
|
||
| `flags` | `CEPH_MSG_CONNECT_*` |
|
||
| `authorizer` | 变长认证器数据 |
|
||
|
||
##### Connect Reply
|
||
|
||
一旦 connect 被发送,连接实际上就已经打开了,**但服务端发送的第一个消息必须是一个 connect reply 消息**:
|
||
|
||
```cpp
|
||
struct ceph_msg_connect_reply {
|
||
u8 tag; // Tag indicating response code.
|
||
u64le features;
|
||
u32le global_seq;
|
||
u32le connect_seq;
|
||
u32le protocol_version;
|
||
u32le authorizer_len;
|
||
u8 flags;
|
||
u8 authorizer[authorizer_len];
|
||
}
|
||
```
|
||
|
||
#### 3.3.2 MSGR 协议
|
||
|
||
这是一个**承载消息传递的底层协议**。这一层的消息由**一个 tag 字节**(标识消息类型)**后跟消息数据**组成。
|
||
|
||
```cpp
|
||
// Virtual structure.
|
||
struct {
|
||
u8 tag; // CEPH_MSGR_TAG_*
|
||
u8 data[]; // Length depends on tag and data.
|
||
}
|
||
```
|
||
|
||
- `data` 的长度由 tag 字节决定,并且根据消息类型的不同,还可能由 `data` 数组自身中的信息决定。
|
||
- ⚠️ **官方 Note**:**如果你不理解消息的类型,就没有办法确定消息的长度。**
|
||
|
||
消息 tag 定义在 `src/include/msgr.h` 中。官方同时说明:**下面列出的这些结构在源码中并不存在,它们只是为了表示协议而给出的。**
|
||
|
||
##### `CEPH_MSGR_TAG_CLOSE` (0x06)
|
||
|
||
```cpp
|
||
struct ceph_msgr_close {
|
||
u8 tag = 0x06;
|
||
u8 data[0]; // No data.
|
||
}
|
||
```
|
||
|
||
表示连接正在被关闭。
|
||
|
||
##### `CEPH_MSGR_TAG_MSG` (0x07)
|
||
|
||
```cpp
|
||
struct ceph_msgr_msg {
|
||
u8 tag = 0x07;
|
||
ceph_msg_header header;
|
||
u8 front [header.front_len ];
|
||
u8 middle[header.middle_len];
|
||
u8 data [header.data_len ];
|
||
ceph_msg_footer footer;
|
||
}
|
||
```
|
||
|
||
**`ceph_msg_header`(from `src/include/msgr.h`)**:
|
||
|
||
```cpp
|
||
struct ceph_msg_header {
|
||
u64le seq; // Sequence number.
|
||
u64le tid; // Transaction ID.
|
||
u16le type; // Message type (CEPH_MSG_* or MSG_*).
|
||
u16le priority; // Priority (higher is more important).
|
||
u16le version; // Version of message encoding.
|
||
|
||
u32le front_len; // The size of the front section.
|
||
u32le middle_len; // The size of the middle section.
|
||
u32le data_len; // The size of the data section.
|
||
u16le data_off; // The way data should be aligned by the receiver.
|
||
|
||
ceph_entity_name src; // Information about the sender.
|
||
|
||
u16le compat_version; // Oldest compatible encoding version.
|
||
u16le reserved; // Unused.
|
||
u32le crc; // CRC of header.
|
||
}
|
||
```
|
||
|
||
**`ceph_msg_footer`(from `src/include/msgr.h`)**:
|
||
|
||
```cpp
|
||
struct ceph_msg_footer {
|
||
u32le front_crc; // Checksums of the various sections.
|
||
u32le middle_crc; //
|
||
u32le data_crc; //
|
||
u64le sig; // Cryptographic signature.
|
||
u8 flags;
|
||
}
|
||
```
|
||
|
||
**要点**:
|
||
|
||
- 消息是 Ceph 的业务逻辑,是用于在节点间发送请求与数据的东西。
|
||
- **消息 header 包含消息的长度,因此未知的消息可以被优雅地处理(unknown messages can be handled gracefully)。**
|
||
- **消息类型常量有两个命名:`CEPH_MSG_*` 与 `MSG_*`。两者唯一的区别是前者被视为 "public",后者仅供内部使用。在协议层面没有任何区别。**
|
||
|
||
##### `CEPH_MSGR_TAG_ACK` (0x08)
|
||
|
||
```cpp
|
||
struct ceph_msgr_ack {
|
||
u8 tag = 0x08;
|
||
u64le seq; // The sequence number of the message being acknowledged.
|
||
}
|
||
```
|
||
|
||
##### `CEPH_MSGR_TAG_KEEPALIVE` (0x09)
|
||
|
||
```cpp
|
||
struct ceph_msgr_keepalive {
|
||
u8 tag = 0x09;
|
||
u8 data[0]; // No data.
|
||
}
|
||
```
|
||
|
||
##### `CEPH_MSGR_TAG_KEEPALIVE2` (0x0E)
|
||
|
||
```cpp
|
||
struct ceph_msgr_keepalive2 {
|
||
u8 tag = 0x0E;
|
||
utime_t timestamp;
|
||
}
|
||
```
|
||
|
||
##### `CEPH_MSGR_TAG_KEEPALIVE2_ACK` (0x0F)
|
||
|
||
```cpp
|
||
struct ceph_msgr_keepalive2_ack {
|
||
u8 tag = 0x0F;
|
||
utime_t timestamp;
|
||
}
|
||
```
|
||
|
||
> 引自:[Network Protocol](https://docs.ceph.com/en/tentacle/dev/network-protocol/)
|
||
|
||
---
|
||
|
||
### 3.4 网络编码(network-encoding.rst)
|
||
|
||
本页描述用于**序列化数据**的编码方式。官方说明:**它不覆盖具体的 object/message,而聚焦于基础类型(base types)**。
|
||
|
||
**核心警告**:**这些类型不以任何方式自我描述(self documenting)。除非你知道它们是什么,否则无法解码。**
|
||
|
||
#### 3.4.1 约定(Conventions)
|
||
|
||
##### 整数
|
||
|
||
整数类型的命名规则是 `{signed}{size}{endian}`。
|
||
|
||
| 示例 | 含义 |
|
||
|---|---|
|
||
| `u16le` | 无符号 16 位整数,**小端**字节序 |
|
||
| `s64be` | 有符号 64 位整数,**大端**字节序 |
|
||
| `u8` / `s8` | 无符号 / 有符号字节 |
|
||
|
||
**有符号整数使用二进制补码(two's complement)编码。**
|
||
|
||
##### 复杂类型
|
||
|
||
本页使用类 C 语法描述结构。结构代表**将在线上传输的数据**。**元素之间没有 padding,元素按出现的顺序发送。**
|
||
|
||
```cpp
|
||
struct foo {
|
||
u8 tag;
|
||
u32le data;
|
||
}
|
||
```
|
||
|
||
当编码值 `0x05` 与 `0x12345678` 时,线上出现的字节序列是:
|
||
|
||
```text
|
||
05 78 56 34 12
|
||
```
|
||
|
||
##### 变长数组(Variable Arrays)
|
||
|
||
与 C 不同,**长度数组可以用在结构中的任何位置,并且在协议中是内联的**。此外,**长度可以用结构中更早出现的某个元素来描述**。
|
||
|
||
```cpp
|
||
struct blob {
|
||
u32le size;
|
||
u8 data[size];
|
||
u32le checksum;
|
||
}
|
||
```
|
||
|
||
该结构被编码为:32 位 size + `size` 个数据字节 + 32 位 checksum。
|
||
|
||
##### 基本类型别名(Primitive Aliases)
|
||
|
||
```cpp
|
||
// From /src/include/types.h
|
||
|
||
typedef u32le epoch_t;
|
||
typedef u32le ceph_seq_t;
|
||
typedef u64le ceph_tid_t;
|
||
typedef u64le version_t;
|
||
```
|
||
|
||
#### 3.4.2 结构(Structures)
|
||
|
||
> 官方说明:这些结构**在源码中并不真实存在**,它们是不同类型被编码的方式。
|
||
|
||
| 类型 | 编码方式 | 用于编码 |
|
||
|---|---|---|
|
||
| **Optional** | 一个 presence 字节,后跟(如果存在的话)该元素 | `boost::optional`,以及引入 C++17 之后的 `std::optional` |
|
||
| **Pair** | 第一个元素后跟第二个元素 | `std::pair` |
|
||
| **Triple** | 三个元素依次排列 | `ceph::triple` |
|
||
| **List** | 一个元素计数,后跟那么多个元素 | `std::list`、`std::vector`、`std::deque`、`std::set`、`std::unordered_set` |
|
||
| **Blob** | 一个字节列表(`u32le size` + `u8 data[size]`) | `std::string`、`const char *`、`bufferlist` |
|
||
| **Map** | 一个 pair 列表(`u32le length` + `ceph_pair<K,V> entries[length]`) | `std::map`、`std::multimap`、`std::unordered_map`、`std::unordered_multimap` |
|
||
|
||
**Optional**:
|
||
|
||
```cpp
|
||
struct ceph_optional<T> {
|
||
u8 present;
|
||
T element[present? 1 : 0]; // Only if present is non-zero.
|
||
}
|
||
```
|
||
|
||
**Pair**:
|
||
|
||
```cpp
|
||
struct ceph_pair<A,B> {
|
||
A a;
|
||
B b;
|
||
}
|
||
```
|
||
|
||
**Triple**:
|
||
|
||
```cpp
|
||
struct ceph_triple<A,B,C> {
|
||
A a;
|
||
B b;
|
||
C c;
|
||
}
|
||
```
|
||
|
||
**List**:
|
||
|
||
```cpp
|
||
struct ceph_list<T> {
|
||
u32le length;
|
||
T elements[length];
|
||
}
|
||
```
|
||
|
||
> ⚠️ **官方 Note:列表中元素的大小不一定是一致的。**
|
||
|
||
**Blob**:
|
||
|
||
```cpp
|
||
struct ceph_string {
|
||
ceph_list<u8>;
|
||
}
|
||
|
||
// AKA
|
||
|
||
struct ceph_string {
|
||
u32le size;
|
||
u8 data[size];
|
||
}
|
||
```
|
||
|
||
> ⚠️ **官方 Note:Blob 的内容是任意二进制数据。**
|
||
|
||
**Map**:
|
||
|
||
```cpp
|
||
struct ceph_map<K,V> {
|
||
ceph_list<ceph_pair<K,V>>;
|
||
}
|
||
|
||
// AKA
|
||
|
||
struct ceph_map<K,V> {
|
||
u32le length;
|
||
ceph_pair<K,V> entries[length];
|
||
}
|
||
```
|
||
|
||
#### 3.4.3 复杂类型(Complex Types)
|
||
|
||
**`utime_t`**:
|
||
|
||
```cpp
|
||
// From /src/include/utime.h
|
||
struct utime_t {
|
||
u32le tv_sec; // Seconds since epoch.
|
||
u32le tv_nsec; // Nanoseconds since the last second.
|
||
}
|
||
```
|
||
|
||
**`ceph_entity_name`**:
|
||
|
||
```cpp
|
||
// From /src/include/msgr.h
|
||
struct ceph_entity_name {
|
||
u8 type; // CEPH_ENTITY_TYPE_*
|
||
u64le num;
|
||
}
|
||
```
|
||
|
||
> 引自:[Network Encoding](https://docs.ceph.com/en/tentacle/dev/network-encoding/)
|
||
|
||
---
|
||
|
||
### 3.5 序列化(encode/decode)(encoding.rst)
|
||
|
||
本页讲的是**内存中结构与字节串之间的转换机制**,与 3.4 的线上编码是互补关系:3.4 是「字节怎么排布」,3.5 是「这套排布如何做到版本兼容」。
|
||
|
||
#### 3.5.1 基本模型
|
||
|
||
当一个结构被通过网络发送或写入磁盘时,它被**编码成一串字节**。通常(**但不总是** —— Ceph 中并存着多套序列化设施)可序列化结构会有 `encode` 和 `decode` 方法,它们从代表字节串的 **`bufferlist`** 对象写入/读取。
|
||
|
||
#### 3.5.2 术语(Terminology)
|
||
|
||
官方建议的思维方式:**不要从 daemon 和 client 的角度思考,而要从 encoder(编码器)和 decoder(解码器)的角度思考。**
|
||
|
||
- encoder 把一个结构序列化进一个 bufferlist;decoder 做相反的事。
|
||
- encoder 与 decoder 合称 **dencoders**。
|
||
- **Dencoders 同时存在于 daemon 和 client 中。**
|
||
|
||
**角色可以互换 —— 官方给的例子**:
|
||
|
||
```text
|
||
当一个 RBD 客户端发起一个 IO 操作时:
|
||
它准备一个 MOSDOp 结构的实例,并把它编码进一个 bufferlist,放到线上。
|
||
一个 OSD 读取这些字节,并把它们解码回一个 MOSDOp 实例。
|
||
→ 这里 encoder 被客户端使用,decoder 被 OSD 使用。
|
||
|
||
但处理响应时角色就反转了:
|
||
OSD 编码 MOSDOpReply,而 RBD 客户端解码。
|
||
```
|
||
|
||
**Encoder 和 decoder 按照一种「由程序员通过实现 `encode` 和 `decode` 方法定义的格式」运作。**
|
||
|
||
#### 3.5.3 格式变更的原则(Principles for format change)
|
||
|
||
**一般规则:一个 decoder 必须能够理解一个 encoder 所编码的东西。**
|
||
|
||
大多数困难出现在两个过程中:① 确保**旧 decoder 与新 encoder** 的兼容性;② 确保**新 decoder 与旧 encoder** 的兼容性。**除非另有说明,否则应当假定集群中「新旧混合」的任何组合都是可能的。**
|
||
|
||
**两个主要关切**:
|
||
|
||
1. **升级(Upgrades)**。虽然有关于实体类型顺序(mons/OSDs/clients)的建议,但**它不是强制性的,不应做任何假设**。
|
||
2. **客户端版本的巨大差异性**。内核升级(因而内核客户端)与 Ceph 升级是**解耦的**;容器化甚至给 `librbd` 带来了可变性 —— 现在用户态库就在容器自身里。
|
||
|
||
**限制 dencoder 之间互操作程度的三条规则**:
|
||
|
||
| 规则 | 内容 |
|
||
|---|---|
|
||
| daemon 之间的 dencoding | **`n-2`** |
|
||
| 客户端场景的硬性要求 | **`n-3`** |
|
||
| 客户端场景的软性要求 | **`n-3..`**。理想情况下每个客户端都应该能与任何版本的 daemon 通信 |
|
||
|
||
官方指出:由于底层原因相同,**dencoders 遵循的规则与我们的 feature bit 弃用规则几乎相同**,参见 `src/include/ceph_features.h` 中的 `Notes on deprecation`。
|
||
|
||
#### 3.5.4 框架(Frameworks)
|
||
|
||
当前并存着多类 dencoding helper:
|
||
|
||
| 框架 | 特点 |
|
||
|---|---|
|
||
| **`encoding.h`** | 最普及的一个(the most proliferated one) |
|
||
| **`denc.h`** | 性能优化过的,主要见于 **`BlueStore`** |
|
||
| **`Message` 继承体系** | — |
|
||
|
||
**虽然细节各异,但互操作性规则保持不变。**
|
||
|
||
#### 3.5.5 向结构中增加一个字段
|
||
|
||
**初始版本**:
|
||
|
||
```cpp
|
||
class AcmeClass
|
||
{
|
||
int member1;
|
||
std::string member2;
|
||
|
||
void encode(bufferlist &bl)
|
||
{
|
||
ENCODE_START(1, 1, bl);
|
||
::encode(member1, bl);
|
||
::encode(member2, bl);
|
||
ENCODE_FINISH(bl);
|
||
}
|
||
|
||
void decode(bufferlist::iterator &bl)
|
||
{
|
||
DECODE_START(1, bl);
|
||
::decode(member1, bl);
|
||
::decode(member2, bl);
|
||
DECODE_FINISH(bl);
|
||
}
|
||
};
|
||
```
|
||
|
||
**`ENCODE_START` 宏**写入一个 header,指定了 **version** 与 **compat_version**(初始都是 1)。
|
||
|
||
- **message version 在编码发生任何变更时被递增。**
|
||
- **compat_version 只有在该变更会破坏已有 decoder 时才被递增** —— 因为 decoder **容忍尾随字节**,所以在结构末尾追加字段的变更**不需要**递增 compat_version。
|
||
|
||
**`DECODE_START` 宏**接受一个参数,指定**这段代码能处理的最新 message version**。它会与消息中编码的 compat_version 比较,**如果消息太新,就会抛出异常**。由于 compat_version 的变更很罕见,在追加字段时通常不必担心这一点。
|
||
|
||
**增加第三个字段后的版本**:
|
||
|
||
```cpp
|
||
class AcmeClass
|
||
{
|
||
int member1;
|
||
std::string member2;
|
||
std::vector<std::string> member3;
|
||
|
||
void encode(bufferlist &bl)
|
||
{
|
||
ENCODE_START(2, 1, bl);
|
||
::encode(member1, bl);
|
||
::encode(member2, bl);
|
||
::encode(member3, bl);
|
||
ENCODE_FINISH(bl);
|
||
}
|
||
|
||
void decode(bufferlist::iterator &bl)
|
||
{
|
||
DECODE_START(2, bl);
|
||
::decode(member1, bl);
|
||
::decode(member2, bl);
|
||
if (struct_v >= 2) {
|
||
::decode(member3, bl);
|
||
}
|
||
DECODE_FINISH(bl);
|
||
}
|
||
};
|
||
```
|
||
|
||
**关键点**:
|
||
|
||
1. **compat_version 没有变**,因为编码出的消息**仍然可以被只理解 version 1 的代码解码** —— 它们只是**忽略**我们编码 `member3` 的那些尾随字节。
|
||
2. **在 `decode` 函数中,解码新字段是条件性的**,因为我们可能仍会收到**没有该字段的旧版本消息**。
|
||
3. **`struct_v` 变量是由 `DECODE_START` 宏设置的一个局部变量。**
|
||
|
||
#### 3.5.6 深入机制(# Into the weeeds)
|
||
|
||
**append-extendability(追加可扩展性)的来源**:是 `ENCODE_START` 与 `DECODE_FINISH` 宏带来的**前向兼容性(forward compatibility)**。
|
||
|
||
它们实现的是一套**可扩展性设施(extensibility facilities)**。一个 encoder 在填充 bufferlist 时,**前置三个字段**:
|
||
|
||
1. 当前格式的版本(version of the current format);
|
||
2. 与之兼容的 decoder 的最小版本(minimal version of a decoder compatible with it);
|
||
3. 所有已编码字段的总大小(the total size of all encoded fields)。
|
||
|
||
**`ENCODE_START` 宏(官方原文)**:
|
||
|
||
```cpp
|
||
/**
|
||
* start encoding block
|
||
*
|
||
* @param v current (code) version of the encoding
|
||
* @param compat oldest code version that can decode it
|
||
* @param bl bufferlist to encode to
|
||
*
|
||
*/
|
||
#define ENCODE_START(v, compat, bl) \
|
||
__u8 struct_v = v; \
|
||
__u8 struct_compat = compat; \
|
||
ceph_le32 struct_len; \
|
||
auto filler = (bl).append_hole(sizeof(struct_v) + \
|
||
sizeof(struct_compat) + sizeof(struct_len)); \
|
||
const auto starting_bl_len = (bl).length(); \
|
||
using ::ceph::encode; \
|
||
do {
|
||
```
|
||
|
||
**`struct_len` 字段的作用**:允许 decoder **吃掉(eat)在用户提供的 `decode` 实现中未被解码的所有字节。**
|
||
|
||
**对应地,decoder 会跟踪在用户提供的 `decode` 方法中已经解码了多少输入**:
|
||
|
||
```cpp
|
||
#define DECODE_START(bl) \
|
||
unsigned struct_end = 0; \
|
||
__u32 struct_len; \
|
||
decode(struct_len, bl); \
|
||
... \
|
||
struct_end = bl.get_off() + struct_len; \
|
||
} \
|
||
do {
|
||
```
|
||
|
||
**decoder 使用这个信息来丢弃它不理解的额外字节**:
|
||
|
||
```cpp
|
||
#define DECODE_FINISH(bl) \
|
||
} while (false); \
|
||
if (struct_end) { \
|
||
... \
|
||
if (bl.get_off() < struct_end) \
|
||
bl += struct_end - bl.get_off(); \
|
||
}
|
||
```
|
||
|
||
**为什么「推进 bufferlist」是关键?** 官方解释:
|
||
|
||
> 推进 bufferlist 是**关键**的,因为 **dencoders 往往是嵌套的**;如果不动它,**只有嵌套结构中最后一次 `decode` 调用**才能正常工作。
|
||
|
||
**整套协作机制的效果**:
|
||
|
||
> 这一整套协作机制允许 encoder(及其后续修订)生成更多的字节流(例如由于在末尾增加了一个新字段),而**不必担心这些残留字节会让较旧的 decoder 修订版本崩溃**。
|
||
|
||
> 引自:[Serialization (encode/decode)](https://docs.ceph.com/en/tentacle/dev/encoding/)
|
||
|
||
---
|
||
|
||
### 3.6 DPDK(dpdk.rst)
|
||
|
||
#### 3.6.1 编译 DPDKStack
|
||
|
||
**`ceph dpdkstack` 默认不编译**,需要重新编译并启用 DPDKstack 组件。可以先在有预编译 DPDK 包的发行版上安装 `dpdk-devel` 或 `dpdk-dev`,然后编译:
|
||
|
||
```bash
|
||
$ do_cmake.sh -DWITH_DPDK=ON
|
||
```
|
||
|
||
#### 3.6.2 设置 DPDK 网卡
|
||
|
||
- 大多数主流网卡支持 **SR-IOV**,可以虚拟化为多个 **VF** 网卡。
|
||
- **每个 OSD 通过 DPDK 使用若干专用网卡**;**mon、mgr 和 client 通过 POSIX 协议栈使用 PF 网卡**。
|
||
|
||
加载 DPDK 依赖的驱动:
|
||
|
||
```bash
|
||
# modprobe vfio
|
||
# modprobe vfio_pci
|
||
```
|
||
|
||
通过编辑 `/etc/sysctl.conf` 配置 Hugepage:
|
||
|
||
```text
|
||
vm.nr_hugepages = xxx
|
||
```
|
||
|
||
根据 OSD 数量配置 VF 数量:
|
||
|
||
```bash
|
||
# echo $numvfs > /sys/class/net/$port/device/sriov_numvfs
|
||
```
|
||
|
||
把网卡绑定到 DPDK 应用:
|
||
|
||
```bash
|
||
# dpdk-devbind.py -b vfio-pci 0000:xx:yy.z
|
||
```
|
||
|
||
#### 3.6.3 配置 OSD DPDKStack
|
||
|
||
默认情况下,**DPDK RTE 初始化过程需要 root 权限**以访问系统中的各种资源。授予 `ceph` 用户 root 访问权限:
|
||
|
||
```bash
|
||
# usermod -G root ceph
|
||
```
|
||
|
||
**OSD 通过 `ms_dpdk_devs_allowlist` 选择网卡**:
|
||
|
||
1. **配置单个网卡**:
|
||
|
||
```ini
|
||
ms_dpdk_devs_allowlist=-a 0000:7d:010
|
||
```
|
||
|
||
或
|
||
|
||
```ini
|
||
ms_dpdk_devs_allowlist=--allow=0000:7d:010
|
||
```
|
||
|
||
2. **配置 Bond 网卡**:
|
||
|
||
```ini
|
||
ms_dpdk_devs_allowlist=--allow=0000:7d:01.0 --allow=0000:7d:02.6 --vdev=net_bonding0,mode=2,slave=0000:7d:01.0,slave=0000:7d:02.6
|
||
```
|
||
|
||
**DPDK 相关配置项(官方原文)**:
|
||
|
||
```ini
|
||
[osd]
|
||
ms_type=async+dpdk
|
||
ms_async_op_threads=1
|
||
|
||
ms_dpdk_port_id=0
|
||
ms_dpdk_gateway_ipv4_addr=172.19.36.1
|
||
ms_dpdk_netmask_ipv4_addr=255.255.255.0
|
||
ms_dpdk_hugepages=/dev/hugepages
|
||
ms_dpdk_hw_flow_control=false
|
||
ms_dpdk_lro=false
|
||
ms_dpdk_enable_tso=false
|
||
ms_dpdk_hw_queue_weight=1
|
||
ms_dpdk_memory_channel=2
|
||
ms_dpdk_debug_allow_loopback = true
|
||
|
||
[osd.x]
|
||
ms_dpdk_coremask=0xf0
|
||
ms_dpdk_host_ipv4_addr=172.19.36.51
|
||
public_addr=172.19.36.51
|
||
cluster_addr=172.19.36.51
|
||
ms_dpdk_devs_allowlist=--allow=0000:7d:01.1
|
||
```
|
||
|
||
#### 3.6.4 调试与优化
|
||
|
||
**根据日志定位故障,并把日志调到合适的级别**:
|
||
|
||
```ini
|
||
debug_dpdk=xx
|
||
debug_ms=xx
|
||
```
|
||
|
||
- **如果日志中包含大量重传消息,减小 `ms_dpdk_tcp_wmem` 的值。**
|
||
|
||
**查看 DPDKStack 统计**:
|
||
|
||
```bash
|
||
$ ceph daemon osd.$i perf dump | grep dpdk
|
||
```
|
||
|
||
- **如果 `dpdk_device_receive_nombuf_errors` 持续增长**,检查是否超出限流:
|
||
|
||
```bash
|
||
$ ceph daemon osd.$i perf dump | grep throttle-osd_client -A 7 | grep "get_or_fail_fail"
|
||
$ ceph daemon osd.$i perf dump | grep throttle-msgr_dispatch_throttler -A 7 | grep "get_or_fail_fail"
|
||
```
|
||
|
||
- 如果超过阈值,**提高限流阈值或禁掉限流**。
|
||
|
||
**检查网卡是否故障或异常**:
|
||
|
||
```bash
|
||
$ ceph daemon osd.$i show_pmd_stats
|
||
$ ceph daemon osd.$i show_pmd_xstats
|
||
```
|
||
|
||
**某些 DPDK 版本(如 `dpdk-20.11-3.e18.aarch64`)或网卡 TSO 异常,尝试关闭 tso**:
|
||
|
||
```ini
|
||
ms_dpdk_enable_tso=false
|
||
```
|
||
|
||
**如果 VF 网卡支持多队列,可以给单核分配更多网卡队列以提升性能**:
|
||
|
||
```ini
|
||
ms_dpdk_hw_queues_per_qp=4
|
||
```
|
||
|
||
#### 3.6.5 现状与未来工作
|
||
|
||
**与 POSIX Stack 的对比(官方给出的实测结论)**:
|
||
|
||
| 测试项 | 结论 |
|
||
|---|---|
|
||
| 多并发测试:4K 随机写 | 性能**相同** |
|
||
| 多并发测试:8K 随机写 | 性能**提升 28%** |
|
||
| 多并发测试:1 MB 包 | **不稳定** |
|
||
| 单延迟测试:4K 与 8K 随机写 | 延迟**降低 15%**(延迟越低越好) |
|
||
|
||
**未来工作(高层计划)**:
|
||
|
||
- **OSD 多网络支持(public network 与 cluster network)**:public 与 cluster 网卡可以被配置;在连接或监听时,可以根据 IP 地址选择 public 或 cluster 网卡;在 msgr-work 初始化期间,同时初始化 public 与 cluster 网卡,并创建两个 `DPDKQueuePair`。
|
||
|
||
> 引自:[Ceph messenger DPDKStack](https://docs.ceph.com/en/tentacle/dev/dpdk/)
|
||
|
||
---
|
||
|
||
## 四、协议与认证
|
||
|
||
本章覆盖 `rados-client-protocol.rst`、`cephx.rst`、`cephx_protocol.rst`、`ceph_krb_auth.rst`、`session_authentication.rst` 五页。
|
||
|
||
### 4.1 RADOS 客户端协议(rados-client-protocol.rst)
|
||
|
||
> 官方在页面第一句就给出了免责声明:**"This is very incomplete, but one must start somewhere."**(这非常不完整,但总得有个开始。)本页只有 117 行,且只覆盖了 Basics、Resends、Backoff 三节。
|
||
|
||
#### 4.1.1 基础(Basics)
|
||
|
||
| 要素 | 说明 |
|
||
|---|---|
|
||
| 请求消息 | **`MOSDOp`** |
|
||
| 回复消息 | **`MOSDOpReply`** |
|
||
| 寻址目标 | 一个对象请求的目标是 **`hobject_t`**,它包含:pool、**hash 值**、object name、placement key(通常为空)、以及 **snapid** |
|
||
| hash 值 | 一个 32 位 hash 值,**通常由对 object name 做 hash 生成**。但 `hobject_t` **可以被任意构造**,用任何 hash 值和名字 |
|
||
| ⚠️ 注意 | 在 `MOSDOp` 中,上述这些组成部分**分散在若干个字段里,并没有被逻辑地组装成一个真正的 `hobject_t` 成员**(主要是历史原因) |
|
||
| 针对 PG 的请求 | 请求也可以直接针对一个 PG。此时 **ps 值匹配一个特定的 PG**,object name 为空,且(希望如此)请求中的 ops 是 PG ops |
|
||
| 最终目标 | 无论哪种方式,请求**最终都指向一个 PG** —— 要么使用显式的 pgid,要么把 hash 值折叠(fold)到该 pool 当前的 pg 数量上。**客户端把请求发送给该关联 PG 的 primary** |
|
||
| tid | **每个请求被分配一个唯一的 tid** |
|
||
|
||
#### 4.1.2 重发(Resends)
|
||
|
||
**触发重发的两种情况**:
|
||
|
||
1. **连接断开** —— 客户端会重发所有未完成的请求。
|
||
2. **PG 映射发生变更导致 primary 改变** —— 客户端负责重发请求。
|
||
- **注意**:虽然从 OSD 的角度看,可能发生了一次 interval change(触发 PG peering),但**如果 primary 没有变,客户端就不需要重发**。
|
||
|
||
**这条规则的三条例外**:
|
||
|
||
| 例外 | 说明 |
|
||
|---|---|
|
||
| `last_force_op_resend` | OSDMap 的 `pg_pool_t` 中有一个 `last_force_op_resend` 字段。**如果它发生变化,客户端就被强制重发所有未完成的请求**。(例如在 tiering 被调整时会发生) |
|
||
| 某些请求在**任何** PG interval 变更时重发 | 判据由 **`pg_interval_t::is_new_interval()`** 定义(**与 OSD 中 peering 使用的判据相同**) |
|
||
| `PAUSE` OSDMap 标志 | 如果该标志被设置然后被清除 |
|
||
|
||
**attempt 字段**:
|
||
|
||
- **每次请求被发往 OSD 时,`attempt` 字段被递增。**
|
||
- **第一次是 0,下一次是 1,依此类推。**
|
||
|
||
#### 4.1.3 退避(Backoff)—— 客户端侧的流量控制
|
||
|
||
**设计动机**:
|
||
|
||
```text
|
||
通常,OSD 会简单地把它无法立即处理的请求在内存中排队,直到能处理为止。
|
||
这可能变得有问题,因为 OSD 会限制入站消息所消耗的总内存:
|
||
如果「消息条数」或「字节数」中任一阈值被达到,
|
||
新的消息就不会被从网络 socket 上读走,从而通过网络产生背压(backpressure)。
|
||
```
|
||
|
||
但在某些情况下,**OSD 知道或预期某个 PG 或对象将在一段时间内不可用,并且不希望通过排队请求来消耗内存**。此时它可以向客户端发送一条 **`MOSDBackoff`** 消息。
|
||
|
||
##### Backoff 请求的四个属性
|
||
|
||
| # | 属性 | 说明 |
|
||
|---|---|---|
|
||
| 1 | op code | `block`、`unblock` 或 `ack-block` |
|
||
| 2 | **id** | 在该 session 内分配的一个唯一 id |
|
||
| 3 | `hobject_t begin` | 范围起点 |
|
||
| 4 | `hobject_t end` | 范围终点 |
|
||
|
||
##### 两种 Backoff 类型
|
||
|
||
| 类型 | 作用范围 | 表示方式 |
|
||
|---|---|---|
|
||
| **PG backoff** | 在客户端侧**堵住(plug)针对整个 PG 的所有请求** | 用 hash/`hobject_t` 空间的一个区间 `[begin, end)` 描述 |
|
||
| **object backoff** | 堵住针对**单个对象**的所有请求 | `begin == end` |
|
||
|
||
##### 客户端侧的行为
|
||
|
||
1. 当客户端收到一条 **block** backoff 消息时,它就**负责不再发送**由该 backoff 所描述的那些 `hobject_t` 的请求。
|
||
2. **backoff 一直有效,直到**:① 该 backoff 被清除(通过一条 `unblock` 消息);② 或 **OSD session 被关闭**。
|
||
3. 客户端会**立即**回送一条 **`ack_block`** 消息给 OSD,以确认收到了该 backoff。
|
||
|
||
**unblock 的处理(重要细节)**:
|
||
|
||
- 当收到 unblock 时,它会引用**一个客户端先前已阻塞的特定 id**。
|
||
- **但是,unblock 所描述的范围可能比原始范围更小**,因为 **PG 可能在 OSD 上发生了 split**。
|
||
- **`unblock` 应当只解除 unblock 消息中所指定的那个范围。**
|
||
- **任何落在 unblock 请求范围内的请求会被重新检查**,并且**如果没有其他已安装的 backoff 适用,就被重发**。
|
||
|
||
##### OSD 侧的行为:Backoff 的三个状态
|
||
|
||
**Backoff 在 OSD 上也是按 hash 空间的区间来跟踪的**,存在三种状态:
|
||
|
||
| 状态 | 说明 |
|
||
|---|---|
|
||
| `new` | 新安装的 backoff 被设为 `new`,并向客户端发送一条消息 |
|
||
| `acked` | 当收到 `ack-block` 消息时,它被变更为 `acked` 状态 |
|
||
| `deleting` | 待删除状态 |
|
||
|
||
**规则**:
|
||
|
||
- OSD **可以**处理来自客户端的、**被处于 `new` 状态的 backoff 覆盖的**其他消息。
|
||
- **但一旦 backoff 变成 `acked`,除非有 bug,否则 OSD 就不应再看到被阻塞的请求。**
|
||
- 如果 OSD 想移除一个处于 `acked` 状态的 backoff,**它可以简单地移除它并通知客户端**。
|
||
- **如果 backoff 处于 `new` 状态,OSD 必须先把它移到 `deleting` 状态**,并继续用它来丢弃客户端请求,**直到收到 `ack-block` 消息,此时它才能最终被移除**。
|
||
- **为什么必须这样?** 官方解释:**这是为了保持 OSD 所处理操作的顺序(preserve the order of operations processed by the OSD)。**
|
||
|
||
> 引自:[RADOS client protocol](https://docs.ceph.com/en/tentacle/dev/rados-client-protocol/)
|
||
|
||
---
|
||
|
||
### 4.2 cephx(cephx.rst)—— 认证协议(权威版)
|
||
|
||
> 本节是 cephx 的**现代权威描述**;4.3 节是 2012 年的历史文档(Peter Reiher 写的代码级走查),两者对照阅读效果最佳。
|
||
|
||
#### 4.2.1 概述(Intro)
|
||
|
||
- 协议设计**看起来很像 Kerberos**。
|
||
- **「KDC」角色由 monitor 承担**,monitor 持有每个 entity 的共享密钥数据库。
|
||
- **客户端与非 monitor 守护进程都从与 monitor 认证开始,以获取 ticket**(在代码中多被称为 **authorizer**)。
|
||
- 这些 ticket **同时提供 authentication(认证)与 authorization(授权)**,因为它们包含了对该 entity 的 **capabilities(能力)** 的描述 —— 这是一种**简洁的、结构化的**「哪些动作被允许」的描述,**可以被服务守护进程解释并强制执行**。
|
||
|
||
#### 4.2.2 术语(Terms)
|
||
|
||
| 术语 | 定义 |
|
||
|---|---|
|
||
| **monitor(s)** | 中心授权机构(central authorization authority) |
|
||
| **service** | 某一特定类型的**所有**守护进程构成的集合(例如所有 OSD、所有 MDS) |
|
||
| **client** | 正在访问该 service 的一个 entity 或 principal |
|
||
| **entity name** | principal 的字符串标识符(例如 `client.admin`、`osd.123`) |
|
||
| **ticket** | 一段以密码学方式断言身份与授权的数据 |
|
||
| **principal** | 一个 client 或 daemon,由唯一的 `entity_name` 标识,**与 monitor 共享一个 secret** |
|
||
| **principal_secret** | principal 的密钥,一个**由该 principal 与 monitor 共享的密钥(16 字节)** |
|
||
| **mon_secret** | monitor 密钥,**被所有 monitor 共享** |
|
||
| **service_secret** | 一个**轮转的(rotating)**密钥,**被某一 service class 的所有成员共享**(例如所有 OSD) |
|
||
| **auth ticket** | 向 monitor 证明身份的 ticket |
|
||
| **service ticket** | 向某个 service 证明身份与授权的 ticket |
|
||
|
||
#### 4.2.3 记法(Terminology)
|
||
|
||
> `{X, Y, ...}^secret` 表示**用 `secret` 对 `X + Y + ...` 加密**。
|
||
|
||
#### 4.2.4 上下文(Context)
|
||
|
||
- 这里描述的认证消息**特定于 cephx auth 实现**。
|
||
- 这些消息**由 Messenger 协议传输,或由 `MAuth` 消息传输**,取决于 messenger 协议的版本。参见 msgr2 协议。
|
||
- **一次初始的(messenger)握手**会协商:① 要使用的认证方法(cephx vs none vs krb 等);② 客户端或守护进程**试图以哪个 entity 的身份进行认证**的声明。
|
||
|
||
#### 4.2.5 Phase I:获取 auth ticket
|
||
|
||
**交换由 monitor 发起** —— monitor 已经知道客户端声称自己是谁,并发出第一条 cephx 消息:
|
||
|
||
```text
|
||
a->p :
|
||
CephxServerChallenge {
|
||
u64 server_challenge # random (by server)
|
||
}
|
||
```
|
||
|
||
**客户端响应**:加上自己的 challenge,并基于**两个 challenge 与自己的共享密钥 `principal_secret`** 计算一个值:
|
||
|
||
```text
|
||
p->a :
|
||
CephxRequestHeader {
|
||
u16 CEPHX_GET_AUTH_SESSION_KEY
|
||
}
|
||
CephXAuthenticate {
|
||
u8 2 # 2 means nautilus+
|
||
u64 client_challenge # random (by client)
|
||
u64 key = digest({client_challenge, server_challenge}^principal_secret) # (roughly)
|
||
blob old_ticket # old ticket, if we are reconnecting or renewing
|
||
u32 other_keys # bit mask of service keys we want
|
||
}
|
||
```
|
||
|
||
**Nautilus 之前**:
|
||
|
||
```text
|
||
CephXAuthenticate {
|
||
u8 1 # 2 means nautilus+
|
||
u64 client_challenge # random (by client)
|
||
u64 key = digest({client_challenge, server_challenge}^principal_secret) # (roughly)
|
||
blob old_ticket # old ticket, if we are reconnecting or renewing
|
||
}
|
||
```
|
||
|
||
> ⚠️ **官方 Note**:**这在本协议中并不是一个认证步骤。** `server_challenge` **容易受到 MitM 攻击**。**这最多可以被看作一个 DoS 过滤器,别的什么都不是。**
|
||
|
||
**monitor 的处理**:
|
||
|
||
1. 在数据库中查找 `principal_secret`,**并验证 key 是否正确**。
|
||
2. **如果 `old_ticket` 存在**,验证它有效,并**复用同一个 `global_id`**。
|
||
3. **否则**,monitor 分配一个新的 `global_id`。
|
||
|
||
**monitor 的响应**:
|
||
|
||
```text
|
||
a->p :
|
||
CephxReplyHeader {
|
||
u16 CEPHX_GET_AUTH_SESSION_KEY
|
||
s32 result (0)
|
||
}
|
||
u8 encoding_version = 1
|
||
u32 num_tickets ( = 1)
|
||
ticket_info # (N = 1) for AUTH ticket
|
||
```
|
||
|
||
**Nautilus 及以后,还要加上**:
|
||
|
||
```text
|
||
u32 connection_secret_len # in bytes
|
||
connection_secret^session_key # using CEPH_ENTITY_TYPE_AUTH session key
|
||
u32 other_keys_len # bytes of other keys (encoded)
|
||
other_keys {
|
||
u8 encoding_version = 1
|
||
u32 num_tickets
|
||
service_ticket_info * N # for each service ticket
|
||
}
|
||
```
|
||
|
||
**各个子结构**:
|
||
|
||
```text
|
||
ticket_info {
|
||
u32 service_id # CEPH_ENTITY_TYPE_AUTH
|
||
u8 msg_version (1)
|
||
{CephXServiceTicket service_ticket}^principal_secret # principal_secret ONLY for _AUTH
|
||
{CephxTicketBlob ticket_blob}^existing session_key # if we are renewing a ticket,
|
||
CephxTicketBlob ticket_blob # otherwise
|
||
}
|
||
|
||
service_ticket_info {
|
||
u32 service_id # CEPH_ENTITY_TYPE_{MON,MGR,OSD,MDS}
|
||
u8 msg_version (1)
|
||
{CephxServiceTicket service_ticket}^auth_session_key # session_key from _AUTH CephxServiceTicket
|
||
CephxTicketBlob ticket_blob
|
||
}
|
||
|
||
CephxServiceTicket {
|
||
CryptoKey session_key # freshly generated (even if old_ticket is present)
|
||
utime_t expiration # now + auth_mon_ticket_ttl
|
||
}
|
||
|
||
CephxTicketBlob {
|
||
u64 secret_id # which service ticket encrypted this; -1 == monsecret,
|
||
# otherwise service's rotating key id
|
||
{CephxServiceTicketInfo ticket}^rotating_service_secret
|
||
}
|
||
|
||
CephxServiceTicketInfo {
|
||
CryptoKey session_key # same session_key as above
|
||
AuthTicket ticket
|
||
}
|
||
|
||
AuthTicket {
|
||
EntityName name # client's identity, as proven by its possession of principal_secret
|
||
u64 global_id # newly assigned, or from old_ticket
|
||
utime_t created, renew_after, expires
|
||
AuthCapsInfo # what client is allowed to do
|
||
u32 flags = 0 # unused
|
||
}
|
||
```
|
||
|
||
**关于 `rotating_service_secret`(官方 Note)**:
|
||
|
||
> 它是给定类型的 service 所共享的**三个(默认)密钥之一**。Monitor 有两个用于此目的的 service type:
|
||
> - **`CEPH_ENTITY_TYPE_MON`** —— 用于保护 mon 授权;
|
||
> - **`CEPH_ENTITY_TYPE_AUTH`** —— 用于保护 "auth" ticket。
|
||
>
|
||
> **"auth" ticket 允许客户端重新获取一个已分配给它的 `global_id`。**
|
||
|
||
**Phase I 小结**:
|
||
|
||
- 对**每一个** ticket,principal 得到:
|
||
- **一个它能用自己的 secret 解密、从而得到 `session_key` 的部分**(`CephxServiceTicket`);
|
||
- **一个不透明的 `CephxTicketBlob`**(由 mon secret 保护),它可以在稍后用于证明「我是谁、我能做什么」。
|
||
- **Nautilus+ 我们还顺便一并返回 service tickets。**
|
||
- **客户端可以推断 monitor 是可信的**,因为它能够用**自己的 secret** 解密 `service_ticket`(即服务端确实持有它的密钥)。
|
||
|
||
#### 4.2.6 Phase II:获取 service tickets(pre-nautilus)
|
||
|
||
现在客户端需要用于与非 monitor(osd、mds、mgr)通信的密钥:
|
||
|
||
```text
|
||
p->a :
|
||
CephxRequestHeader {
|
||
u16 CEPHX_GET_PRINCIPAL_SESSION_KEY
|
||
}
|
||
CephxAuthorizer authorizer
|
||
CephxServiceTicketRequest {
|
||
u32 keys # bitmask of CEPH_ENTITY_TYPE_NAME (MGR, OSD, MDS, etc)
|
||
}
|
||
```
|
||
|
||
```text
|
||
CephxAuthorizer {
|
||
u8 AUTH_MODE_AUTHORIZER (1)
|
||
u64 global_id
|
||
u32 service_id # CEPH_ENTITY_TYPE_*
|
||
CephxTicketBlob auth_ticket
|
||
{CephxAuthorize msg}^session_key # using CEPH_ENTITY_TYPE_AUTH session key
|
||
}
|
||
|
||
CephxAuthorize msg {
|
||
u8 2
|
||
u64 nonce # random from client
|
||
bool have_challenge = false # not used here
|
||
u64 server_challenge_plus_one = 0 # not used here
|
||
}
|
||
```
|
||
|
||
**monitor 的处理**:
|
||
|
||
1. 通过**用 `rotating_service_secret` 解密 `auth_ticket`** 来验证该 authorizer;
|
||
2. 确认它所说的这个 principal 与 `CephxAuthorizer` 各字段中的一致。
|
||
3. ⚠️ **注意:nonce 随机字节在这里并没有被使用**(该字段是为下面的 Phase III 存在的)。
|
||
|
||
假设一切正常,authorizer 就可以基于 `keys` bitmask 中的 `CEPH_ENTITY_TYPE_*` 位生成 service ticket。
|
||
|
||
**响应**:
|
||
|
||
```text
|
||
CephxResponseHeader {
|
||
u16 CEPHX_GET_PRINCIPAL_SESSION_KEY
|
||
s32 result (= 0)
|
||
}
|
||
u8 encoding_version = 1
|
||
u32 num_tickets
|
||
ticket_info * N
|
||
```
|
||
|
||
其中(与上面一样):
|
||
|
||
```text
|
||
ticket_info {
|
||
u32 service_id # CEPH_ENTITY_TYPE_{OSD,MGR,MDS}
|
||
u8 msg_version (1)
|
||
{CephXServiceTicket service_ticket}^principal_secret
|
||
{CephxTicketBlob ticket_blob}^session_key # using **old** CEPH_ENTITY_TYPE_AUTH session key
|
||
}
|
||
|
||
CephxServiceTicket {
|
||
CryptoKey session_key
|
||
utime_t expiration
|
||
}
|
||
|
||
CephxTicketBlob {
|
||
u64 secret_id # which version of the (rotating) service ticket encrypted this
|
||
{CephXServiceTicketInfo ticket}^rotating_service_secret
|
||
}
|
||
|
||
CephxServiceTicketInfo {
|
||
CryptoKey session_key
|
||
AuthTicket ticket
|
||
}
|
||
|
||
AuthTicket {
|
||
EntityName name
|
||
u64 global_id
|
||
utime_t created, renew_after, expires
|
||
AuthCapsInfo # what you are allowed to do
|
||
u32 flags = 0 # unused
|
||
}
|
||
```
|
||
|
||
**这结束了与 monitor 的认证交换。** 客户端或守护进程现在拥有了用于与 mon 以及所有其他相关守护进程通信的 ticket。
|
||
|
||
#### 4.2.7 Phase III:打开到某个 service 的连接
|
||
|
||
当一个连接被打开时,会发送一个 "authorizer" 载荷:
|
||
|
||
```text
|
||
p->s :
|
||
CephxAuthorizer {
|
||
u8 AUTH_MODE_AUTHORIZER (1)
|
||
u64 global_id
|
||
u32 service_id # CEPH_ENTITY_TYPE_*
|
||
CephxTicketBlob auth_ticket
|
||
{CephxAuthorize msg}^session_key
|
||
}
|
||
|
||
CephxAuthorize msg {
|
||
u8 2
|
||
u64 nonce # random from client
|
||
bool have_challenge = false
|
||
u64 server_challenge_plus_one = 0
|
||
}
|
||
```
|
||
|
||
> **历史差异**:在 **Luminous v12.2.6** 或 **Mimic v13.2.2** 之前,`CephxAuthorize msg` **不包含 challenge**,只由以下内容组成:
|
||
|
||
```text
|
||
CephxAuthorize msg {
|
||
u8 1
|
||
u64 nonce # random from client
|
||
}
|
||
```
|
||
|
||
**服务端(S)的处理分支**:
|
||
|
||
服务端会检查 `auth_ticket` 的 `CephxTicketBlob`(**用它当前的 rotating service key 解密**)。
|
||
|
||
- **如果是 v12.2.6 / v13.2.2 之前的客户端**,服务端**立即回复**:
|
||
|
||
```text
|
||
s->p :
|
||
{CephxAuthorizeReply reply}^session_key
|
||
|
||
CephxAuthorizeReply {
|
||
u64 nonce_plus_one
|
||
}
|
||
```
|
||
|
||
- **否则(现代客户端)**,服务端会**用一个 challenge 来响应(以防止重放攻击)**:
|
||
|
||
```text
|
||
s->p :
|
||
{CephxAuthorizeChallenge challenge}^session_key
|
||
|
||
CephxAuthorizeChallenge {
|
||
u64 server_challenge # random from server
|
||
}
|
||
```
|
||
|
||
**客户端**解密并相应地更新它的 `CephxAuthorize msg`,**重新发送大部分与之前相同的信息**:
|
||
|
||
```text
|
||
p->s :
|
||
CephxAuthorizer {
|
||
u8 AUTH_MODE_AUTHORIZER (1)
|
||
u64 global_id
|
||
u32 service_id # CEPH_ENTITY_TYPE_*
|
||
CephxTicketBlob auth_ticket
|
||
{CephxAuthorize msg}^session_key
|
||
}
|
||
|
||
CephxAuthorize msg {
|
||
u8 2
|
||
u64 nonce # (new) random from client
|
||
bool have_challenge = true
|
||
u64 server_challenge_plus_one # server_challenge + 1
|
||
}
|
||
```
|
||
|
||
**服务端**像之前一样验证 ticket,**然后还验证 msg 的 nonce 是否等于它的 challenge + 1**,从而**确认这是一次实时的认证尝试(而不是重放)**。
|
||
|
||
**最后**,服务端回复一个**向客户端证明其真实性**的消息。它还**包含一些熵(entropy),用于在需要时加密该 session**:
|
||
|
||
```text
|
||
s->p :
|
||
{CephxAuthorizeReply reply}^session_key
|
||
|
||
CephxAuthorizeReply {
|
||
u64 nonce_plus_one
|
||
u32 connection_secret_length
|
||
connection secret
|
||
}
|
||
```
|
||
|
||
> **Nautilus 之前没有 connection secret**:
|
||
|
||
```text
|
||
CephxAuthorizeReply {
|
||
u64 nonce_plus_one
|
||
}
|
||
```
|
||
|
||
**客户端**解密,并**确认服务端正确地递增了 nonce**,从而确认这是一次实时的认证请求而不是重放。
|
||
|
||
#### 4.2.8 轮转 service secret(Rotating service secrets)
|
||
|
||
**设计动机**:
|
||
|
||
```text
|
||
守护进程对它们的 ticket 使用一个「轮转密钥」而不是固定密钥,
|
||
目的是限制「某个守护进程被攻破」的严重程度。
|
||
|
||
如果一个守护进程的密钥被攻击者获取:
|
||
- 该守护进程及其密钥可以从 monitor 的数据库中移除;
|
||
- 但攻击者可能也已经获得了一份「被所有守护进程共享的 service secret」的副本。
|
||
|
||
为缓解这一点,service key 会周期性地轮转,
|
||
这样在一段时间之后(auth_service_ticket_ttl),
|
||
攻击者获得的那个密钥就不再有效了。
|
||
```
|
||
|
||
**获取轮转密钥的交换**:
|
||
|
||
```text
|
||
p->a :
|
||
CephxRequestHeader {
|
||
u16 CEPHX_GET_ROTATING_KEY
|
||
}
|
||
|
||
a->p :
|
||
CephxReplyHeader {
|
||
u16 CEPHX_GET_ROTATING_KEY
|
||
s32 result = 0
|
||
}
|
||
{CryptoKey service_key}^principal_secret
|
||
```
|
||
|
||
即:**新的轮转密钥就是被该守护进程自己的 secret 保护着。**
|
||
|
||
**实现细节(官方 Note)**:**service 会同时保留当前密钥和上一个密钥**,以便在密钥轮转期间继续验证请求。
|
||
|
||
> 引自:[Cephx](https://docs.ceph.com/en/tentacle/dev/cephx/)
|
||
|
||
---
|
||
|
||
### 4.3 cephx 认证协议的详细代码级描述(cephx_protocol.rst)
|
||
|
||
> **这是 Peter Reiher 于 2012-07-13 撰写的一份历史文档**,官方在 `cephx.rst` 中把它作为"Other references"引用。
|
||
>
|
||
> ⚠️ **文档开头的自我声明(必须注意)**:
|
||
> - 因为本备忘录讨论了**被调用的例程与使用的变量**的细节,它**代表的是一个快照**;
|
||
> - **代码可能在本文档创建之后被改变,而本文档不太可能同步更新**;
|
||
> - 运气好的话,**代码注释会指出协议实现方式上的重大变更**。
|
||
>
|
||
> 因此本节的**代码路径、函数名、变量名可能与 Tentacle v20.2.4 的实际代码不一致**。它的价值在于把「为什么要这样做」与「每个字段是谁填的」讲清楚了。
|
||
|
||
#### 4.3.1 引言:协议的基本思想
|
||
|
||
- 基本思想**基于 Kerberos**。客户端 C 想从服务端 S 获取某个东西,S 只会把所请求的服务提供给已授权的客户端。
|
||
- 与其要求每个服务端都处理认证与授权问题,系统使用一个**授权服务器(authorization server)A**。
|
||
- 因此客户端必须先与 A 通信:**认证自己**,并**获取能授予它访问所想要服务的凭证(credentials)**。
|
||
|
||
**认证 ≠ 授权**:
|
||
|
||
| 概念 | 说明 |
|
||
|---|---|
|
||
| **Authentication(认证)** | 提供「某一方确实是它所声称的那个」的证据 |
|
||
| **Authorization(授权)** | 提供「某个特定方被允许做某事」的证据 |
|
||
|
||
一般而言,**安全的授权蕴含安全的认证**(因为没有认证,你可能把东西授权给了一个冒名者),**但反过来不成立** —— 可以只认证不授权。**本协议的目的是授权(authorize)。**
|
||
|
||
**基本方法:全程使用对称密码学**。
|
||
|
||
- 每个客户端 C 有自己的密钥,**只有它自己和 A 知道**。
|
||
- 每个服务端 S 有自己的密钥,**只有它自己和 A 知道**。
|
||
- 授权信息通过 **ticket** 传递,用**提供该服务的一方的密钥**加密。
|
||
- 会有一个 **A 给 C 的 ticket,它允许 C 向 A 请求其他 ticket**。这个 ticket **用 A 的密钥加密**(因为 A 是那个需要检查它的一方)。
|
||
- 之后会有 **A 签发的、允许 C 与 S 通信以请求服务的 ticket**。这些 ticket **用 S 的密钥加密**(因为 S 需要检查它们)。
|
||
- 由于我们还希望提供通信的安全性,**会随 ticket 一起建立 session key**。
|
||
- **目前,这些 session key 只在本协议期间、以及客户端 C 提供服务 ticket 时与服务端 S 之间的握手期间用于认证目的。**
|
||
- 通过对系统做一些改动,它们可以被用于全程的认证或保密。
|
||
|
||
#### 4.3.2 协议必须达成的六个证明目标
|
||
|
||
| # | 目标 |
|
||
|---|---|
|
||
| 1 | **C 必须向 A 证明它确实是 C**。由于一切都是通过消息完成的,客户端还必须证明「这条证明真实性的消息是新鲜的(fresh)」,而不是被攻击者重放的 |
|
||
| 2 | **A 必须向 C 证明它确实是那个 authenticator**。同样,也需要证明没有发生重放 |
|
||
| 3 | **A 与 C 必须安全地共享一个 session key**,用于之后在它们之间分发授权材料。同样不允许重放,且该密钥必须只有 A 和 C 知道 |
|
||
| 4 | **A 必须从 C 那里收到证据**,使 A 能够查找出 C 对服务端 S 的被授权操作 |
|
||
| 5 | **C 必须从 A 收到一个 ticket**,该 ticket 能向 S 证明 C 可以执行其被授权的操作。**这个 ticket 必须只能被 C 使用** |
|
||
| 6 | **C 必须从 A 收到一个 session key**,用于保护 C 与 S 之间的通信。该 session key 必须是新鲜的,而不是重放的结果 |
|
||
|
||
#### 4.3.3 起步(Getting Started With Authorization)
|
||
|
||
- 当客户端第一次需要获取服务时,它联系 monitor。此时它**没有任何 ticket**,因此它使用 "unknown" 协议与 monitor 通信,该协议被指定为 **`CEPH_AUTH_UNKNOWN`**。
|
||
- **monitor 同时承担认证服务器 A 的角色。**
|
||
- 其余的通信将使用 **cephx 协议**(其大部分代码位于 `auth/cephx` 下的文件中)。该协议负责创建并传递上面所说的那些 ticket。
|
||
- **本文档不跟踪 pre-cephx 的协议流程**,它从「客户端已联系上服务端、并准备好开始 cephx 协议本身」这一点开始。
|
||
|
||
**两阶段划分**:
|
||
|
||
- **Phase I**:C 需要一个 ticket 来与 A 安全通信。这个 ticket 随后可用于获取其他 ticket。**由 C→A 的一次发送和 A→C 的一次响应组成。**
|
||
- **Phase II**:C 需要一个 ticket 来与 S 通信以获取服务。**同样由 C→A 的一次发送和 A→C 的一次响应组成。**
|
||
|
||
#### 4.3.4 Phase I(代码路径逐段)
|
||
|
||
##### 客户端侧
|
||
|
||
1. 客户端通过 **`AuthClientHandler`** 类中的一个名为 **`need`** 的变量来知道自己需要什么(`CephxClientHandler` 继承自 `AuthClientHandler`)。
|
||
- 此时,`need` 中编码的一件事是 **`CEPH_ENTITY_TYPE_AUTH`**,表示我们需要**从头开始**启动认证协议。
|
||
- 由于我们**总是与同一个授权服务器通信**,如果我们**之前**已经走过这一步(且得到的 ticket/session 尚未超时),我们可以**跳过**这一步,直接请求客户端 ticket。但**最初**必须做这一步,本文档假定我们正处于那个状态。
|
||
|
||
2. Phase I 中 C 发给 A 的消息在 **`CephxClientHandler::build_request()`**(位于 **`auth/cephx/CephxClientHandler.cc`**)中构造。该例程**被用于多个目的**。
|
||
|
||
3. 在本例中,我们首先调用 **`validate_tickets()`**(来自 **`CephXTicketManager::validate_tickets()`**,位于 **`auth/cephx/CephxProtocol.h`**)。
|
||
- 这段代码遍历可能的 ticket 列表以确定我们需要什么,**按需设置 `need` 标志中的值**。
|
||
|
||
4. 然后调用 **`ticket.get_handler()`**(在 `CephxProtocol.h` 中)。
|
||
- 该例程在 ticket map 中**找到指定类型的 ticket**(一个用于执行授权的 ticket),
|
||
- **为它创建一个 ticket handler 对象**,
|
||
- 并**把该 handler 放进 map 中的正确位置**。
|
||
|
||
5. 然后进入处理各种具体情况的专有代码。这里的分支是 **`if (need & CEPH_ENTITY_TYPE_AUTH)`**(我们仍需要向 A 认证)。
|
||
|
||
6. 我们现在创建一条类型为 **`CEPHX_GET_AUTH_SESSION_KEY`** 的消息。
|
||
- 我们需要用 C 的 secret key 认证这条消息,因此**从本地密钥仓库(key repository)中取出它**。
|
||
- 我们**创建一个随机 challenge**,其**目的是防止重放**。
|
||
- 我们使用 **`cephx_calc_client_server_challenge()`** 加密该 challenge。
|
||
- 我们已经从 pre-cephx 阶段获得了一个 **server challenge**(一组类似的随机字节,但由服务端创建并发给客户端)。
|
||
- 我们**取这两个 challenge 和我们的 secret key,生成一个组合的加密 challenge 值,放进 `req.key`**。
|
||
|
||
7. **如果我们有一个旧 ticket,我们把它存放在 `req.old_ticket` 中。**(我们马上要拿一个新的。)
|
||
|
||
8. 整个 `req` 结构 —— **包括旧 ticket 与两个 challenge 的加密哈希** —— 被放进消息中。然后我们从该函数返回,消息被发送。
|
||
|
||
##### 服务端(A)侧
|
||
|
||
1. 服务端收到类型为 **`CEPH_GET_AUTH_SESSION_KEY`** 的消息。该消息在 **`prep_auth()`**(位于 **`mon/AuthMonitor.cc`**)中被处理,后者调用 **`CephxServiceHandler.cc`** 中的 **`handle_request()`** 来完成大部分工作。这个例程同样处理多种情况。
|
||
|
||
2. 控制流由**与该消息关联的 `cephx_header` 中的 `request_type`** 决定。我们这里的分支是 `CEPH_GET_AUTH_SESSION_KEY`。
|
||
|
||
3. 我们需要 A 与 C 共享的 secret key,因此调用 **`get_secret()`** 从本地密钥仓库获取它。
|
||
- 官方注解:它在代码中被叫做 **`key_server`**,但它**并不是一个真正的独立机器或处理实体,更像是「存放本地使用的密钥的地方」**。
|
||
|
||
4. 我们应该已经与该客户端建立了一个 server challenge,因此**确认我们确实有一个**。
|
||
- 该变量**特定于一个 `CephxServiceHandler`**,因此我们创建的每个这样的结构都有一个不同的值(**大概是 A 正在处理的每个客户端一个**)。
|
||
- **如果没有 challenge,我们就需要重新开始**,因为我们需要检查客户端的加密哈希,而它部分地依赖于一个 server challenge。
|
||
|
||
5. **我们现在调用客户端使用的同一个例程,基于同样的值计算哈希**:
|
||
- 客户端 challenge(在收到的消息中);
|
||
- 服务端 challenge(我们保存的那个);
|
||
- 客户端的 key(我们刚获取的)。
|
||
- **我们检查客户端发来的东西是否与我们期望的相同。**
|
||
- 如果相同,我们就知道「我们正在与正确的客户端通信」。我们知道该 session 是**新鲜的**,因为它用了我们发给它的 challenge 来计算它的加密哈希。所以我们可以给它一张认证 ticket。
|
||
|
||
6. 我们获取 C 的 **`eauth` 结构**。它包含**一个 ID、一个 key、以及一组 caps(capabilities)**。
|
||
|
||
7. 客户端在消息中把它**旧 ticket**(如果有的话)发给了我们。
|
||
- 如果有,我们就**设置一个标志 `should_enc_ticket` 为 true**,并**把 global ID 设为那个旧 ticket 中的 global ID**。
|
||
- 如果解码它的旧 ticket 失败(**很可能是因为它根本没有**),**`should_enc_ticket` 保持为 false**。
|
||
|
||
8. 现在我们**设置新 ticket**:填时间戳、C 的名字、以及方法调用中提供的 global ID(**除非存在旧 ticket**)。
|
||
- 我们需要**一个新的 session key**,以帮助客户端安全地与我们通信(**而不是使用它的永久密钥**)。
|
||
- 我们**把 service ID 设为 `CEPH_ENTITY_TYPE_AUTH`**,这将告诉客户端 C 如何处理我们发给它的消息。
|
||
- 我们**构建一个 cephx 响应 header**,并调用 **`cephx_build_service_ticket_reply()`**。
|
||
|
||
9. **`cephx_build_service_ticket_reply()`**(位于 **`auth/cephx/CephxProtocol.cc`**)构建响应消息。
|
||
- 它的大部分工作是把数据从参数拷贝到消息结构中。
|
||
- **其中一部分信息(session key 与有效期)会用 C 的永久密钥加密。**
|
||
- **如果 `should_encrypt_ticket` 标志被设置,就用旧 ticket 的密钥对它加密。否则(没有旧 ticket 密钥),新 ticket 就不加密。**(当然,它**已经**用 A 的永久密钥加密过了。)
|
||
- 官方对这第二层加密的推测:**大概是想减少「用永久密钥加密的材料」的暴露量。**
|
||
|
||
10. 然后我们用 entity name、一个标志 **`CEPH_ENTITY_TYPE_MON`**、以及 capabilities(将由该例程填充)调用 key server 的 **`get_service_caps()`** 例程。
|
||
- 使用该常量标志**意味着我们要获取的是客户端对 A 的 caps,而不是对某个其他数据服务端的 caps**。这里的 ticket 是用于访问**授权者 A**,而不是服务 S。
|
||
- 该调用的结果是:**caps 变量(我们所处例程的一个参数)被填入了 monitor capabilities**,这些 caps 将允许 C 访问 A 的授权服务。
|
||
|
||
11. **`handle_request()` 本身并不发送响应消息。** 它构建出 **`result_bl`**(基本上持有该消息的内容)与 capabilities 结构,但**不发送消息**。
|
||
|
||
12. 我们回到 **`mon/AuthMonitor.cc`** 中的 **`prep_auth()`** 来发送。
|
||
- 该例程对刚填好的 caps 结构做一些处理;
|
||
- 有一个作为该处理结果出现的 global ID 被放进回复消息中;
|
||
- 回复消息**在这里**被构建(**主要来自 `response_bl` buffer**)并被发送。
|
||
|
||
**Phase I 结束时的状态**:**C 已经向 A 认证了自己,且 A 已经生成了一个新的 session key 与 ticket,允许 C 从 A 获取服务端 ticket。**
|
||
|
||
#### 4.3.5 Phase II(代码路径逐段)
|
||
|
||
**Phase II 在 C 收到来自 A 的、包含新 ticket 与 session key 的消息时开始。**
|
||
**目标**:为 C 提供用于与 S 通信的 session key 与 ticket。
|
||
|
||
##### 客户端侧
|
||
|
||
1. A 发给 C 的消息被派发到 **`CephxClientHandler.cc`** 中的 **`build_request()`** —— **与 Phase I 早期用来构造协议第一条消息的是同一个例程**。
|
||
|
||
2. 这一次,当调用 `validate_tickets()` 时,**`need` 变量将不再包含 `CEPH_ENTITY_TYPE_AUTH`**,因此会走主干例程中的另一个分支 —— 即由 **`if (need)`** 指示的分支。
|
||
- **我们有一个用于授权者的 ticket,但我们仍需要 service ticket。**
|
||
|
||
3. 我们必须向 A 发送另一条消息,以获取用于服务端 S 的 ticket(与 session key)。
|
||
- 我们**把消息的 `request_type` 设为 `CEPHX_GET_PRINCIPAL_SESSION_KEY`**,
|
||
- 并**调用 `ticket_handler.build_authorizer()` 来获取一个 authorizer**。该例程在 **`CephxProtocol.cc`** 中。
|
||
- 我们**把这个 authorizer 的 key 设为我们刚从 A 得到的 session key,并创建一个新的 nonce**;
|
||
- 我们**把 global ID、service ID 和 ticket 放进属于该 authorizer 的一个消息 buffer 中**;
|
||
- 然后我们**创建一个新的 `CephXAuthorize` 结构**。刚创建的 nonce 放进去;
|
||
- 我们**用当前 session key 加密这个 `CephXAuthorize` 结构,并塞进 authorizer 的 buffer**;
|
||
- 我们返回该 authorizer。
|
||
|
||
4. 回到 `build_request()`:我们**取出刚构建的 authorizer 的那一部分(它的 buffer,不包括 session key 或其他东西)**,并**塞进我们正在为要发给 A 的消息创建的 buffer 中**。然后**删除该 authorizer**。
|
||
- 我们**把我们想要的东西的需求放进 `req.keys`**,并**把 `req` 放进 buffer**。然后返回,消息被发送。
|
||
|
||
##### 服务端(A)侧
|
||
|
||
1. A 收到类型为 **`CEPHX_GET_PRINCIPAL_SESSION_KEY`** 的消息。该消息在 **`mon/AuthMonitor.cc`** 的 `prep_auth()` 中被处理,后者再次调用 **`CephxServiceHandler.cc`** 中的 `handle_request()` 来完成大部分工作。
|
||
|
||
2. 本例中,`handle_request()` 走 `CEPHX_GET_PRINCIPAL_SESSION_KEY` 分支。它会调用 **`CephxProtocol.cc`** 中的 **`cephx_verify_authorizer()`**。
|
||
|
||
3. 在 `cephx_verify_authorizer()` 中,我们从输入 buffer 中抓取一堆数据,**包括 global ID、service ID、以及给 A 的 ticket**。
|
||
|
||
4. **该 ticket 包含一个 `secret_id`,指示正在使用哪个 key。**
|
||
- **如果从 ticket 中取出的 secret ID 是 `-1`**,则该 ticket **没有指明 A 应当使用哪个 secret key**。
|
||
- 在这种情况下,**A 应当使用「C 想要联系的那个特定 entity」的 key,而不是「同一类型的所有服务端 entity 共享的轮转 key」**。
|
||
- 要得到那个 key,**A 必须查询密钥仓库以找到正确的 key**。
|
||
- **否则**,已经有了一个从密钥仓库获得的、用于持有必要 secret 的结构。
|
||
- **服务端 secret 基于时间过期来轮转(key rotation 不在本文档覆盖范围内)**,因此**遍历该结构以找到它当前的 secret**。
|
||
- **无论哪种方式,A 现在知道了用于创建这个 ticket 的 secret key。** 现在**用这个 key 解密该 ticket 的加密部分。它应当是给 A 的 ticket。**
|
||
|
||
5. **该 ticket 还包含一个 session key,C 本应使用它来加密本消息的其他部分。用那个 session key 解密消息的其余部分。**
|
||
|
||
6. **创建一个 `CephXAuthorizeReply` 来存放我们的回复。**
|
||
- **取出 nonce**(在我们刚解密的那些东西里),**加 1**,把结果放进回复中;
|
||
- **加密该回复**,放进调用 `cephx_verify_authorizer()` 时提供的 buffer 中,并返回 `handle_request()`。
|
||
- **这将用于向 C 证明「是 A(而不是攻击者)创建了这个响应」。**
|
||
|
||
7. 验证了消息有效且来自 C 之后,现在我们需要**为它构建一张用于 S 的 ticket**。
|
||
- 我们需要知道**它想与哪个 S 通信**、以及**它想要什么服务**。
|
||
- **从它的消息中取出描述这些事情的 ticket request**;
|
||
- 然后**遍历该 ticket request 以查看它想要什么**。(**它可能在同一条请求中请求多个不同的服务,但为便于讨论,我们假定只有一个**。)
|
||
- 一旦我们知道它在找哪个 service ID,就调用 **`build_session_auth_info()`**。
|
||
|
||
8. **`build_session_auth_info()`** 位于 **`CephxKeyServer.cc`**。
|
||
- 它**检查服务 S 的 `service_ID` 的 secret 是否可用**,并把它放进某个参数的子字段中;
|
||
- 然后调用位于同一文件中的、名字相似的 **`_build_session_auth_info()`**。
|
||
- 该例程**用 S 的 ID、一个 ticket、以及该 ticket 的一些时间戳来装载新的 `auth_info` 结构**;
|
||
- **它生成一个新的 session key 并放进该结构**;
|
||
- 然后**调用 `get_caps()` 来填充 `info.ticket` 的 caps 字段**。`get_caps()` 也在 `CephxKeyServer.cc` 中,**它用「C 对 S 被允许的 caps」填充所提供的 `caps_info` 结构**。
|
||
|
||
9. `build_session_auth_info()` 返回后,**A 就有了允许 C 对 S 执行的能力列表**。
|
||
- 我们**把一个基于该上下文当前 TTL 的有效期放进 info 结构**,
|
||
- 并把**它放进我们正在准备的、作为对该消息响应的 `info_vec` 结构中**。
|
||
|
||
10. 现在调用 **`build_cephx_response_header()`**(也在 `CephxServiceHandler.cc` 中)。填入 `request_type`(为 `CEPHX_GET_PRINCIPAL_SESSION_KEY`)、状态 0、以及 result buffer。
|
||
|
||
11. 现在调用 **`cephx_build_service_ticket_reply()`**(在 `CephxProtocol.cc` 中)。
|
||
- **与 Phase I 中 A 处理其响应时用的是同一个例程。**
|
||
- 这里,**session key(现在是一个用于与 S 通信的 session key,而不是与 A 通信的)以及该 key 的有效期,会用 C 与 A 之间已有的 session key 加密。**
|
||
- **`should_encrypt_ticket` 参数在这里是 false,且没有为该加密提供 key。** 相关 ticket(一旦 C 把它发到那里,就是发给 S 的)**已经用 S 的 secret 加密过了**。
|
||
- 所以,**本质上,这个例程会把 ID 信息、加密的 session key、以及允许 C 与 S 通信的 ticket 放进要发给 C 的 buffer 中。**
|
||
|
||
12. 该例程返回后,我们退出 `handle_request()`,回到 `prep_auth()`,最终到底层的消息发送代码。
|
||
|
||
##### 客户端侧(收尾)
|
||
|
||
1. 客户端收到这条消息。**nonce 在消息通过 `Pipe::connect()`(位于 `msg/SimpleMessager.cc`)时被检查。**
|
||
- 在该例程中部一个冗长的 `while(1)` 循环中,它获取一个 authorizer。**如果获取成功,最终它会调用 `verify_reply()`,后者检查 nonce。**
|
||
- ⚠️ 官方指出一个值得注意的点:**`connect()` 从不显式检查是否拿到了 authorizer**,这暗示「不提供 authorizer 可能使攻击者跳过 nonce 检查」。**但是**,在很多地方,**如果没有 authorizer,重要的连接字段会被置零,这最终会导致连接无法提供数据**。官方的结论是:**这值得测试一下,但看起来「不提供包含 nonce 的 authorizer」对攻击者没什么帮助。**
|
||
|
||
2. 该消息最终进入 **`CephxClientHandler.cc`** 中的 **`handle_response()`**。
|
||
- 在该例程中,我们**调用 `get_handler()` 来获取一个 ticket handler 以持有我们刚收到的 ticket**。
|
||
- 该例程嵌入在 **`CephXTicketManager`** 结构的定义中。它**接受一个 type**(本例中是 `CEPH_ENTITY_TYPE_AUTH`)并**遍历 `tickets_map` 以找到该 type**。
|
||
- **应当能找到一个,且它的条目中应当有 C 与 A 之间 session 的 session key。** 该 key 将**被用于解密 A 提供的信息,特别是那个允许 C 与 S 通信的新 session key**。
|
||
|
||
3. 然后我们调用 **`CephxProtocol.cc`** 中的 **`verify_service_ticket_reply()`**。
|
||
- 该例程需要**判断该 ticket 是否 OK,并获取与该 ticket 关联的 session key**;
|
||
- 它**用与 A 共享的 session key 解密消息 buffer 的加密部分**;
|
||
- **这个 ticket 没有被加密**(准确说是**没有被双重加密** —— ticket 总是被加密的,只是有时被双重加密,而这个没有),所以**它可以直接存入一个 service ticket buffer**;
|
||
- 我们**从那个 buffer 中取出该 ticket**;
|
||
- 我们用「C 与 A 共享的 session key」解密出来的东西**包括了那个新的 session key**。那就是我们当前用于这个 ticket 的 session key,所以我们设置它;
|
||
- **检查有效性并设置过期时间**。如果走到这一步,返回 true。
|
||
|
||
4. 回到 `handle_response()`,我们**现在调用 `validate_tickets()` 来调整我们认为自己还需要什么**,因为我们已经有了一个之前没有的 ticket。**如果我们已经把需要的东西都搞定了,就返回 0。**
|
||
|
||
**Phase II 结束时的状态(官方总结)**:
|
||
|
||
> 我们现在已经成功地为客户端 C 建立了用于与服务端 S 通信的 ticket 与 session key。
|
||
> - **S 会知道 C 就是它所声称的那个**,因为 A 会验证它。
|
||
> - **C 会知道它在与 S 通信**,同样是因为 A 验证过。
|
||
> - 用于 C 与 S 通信的 session key 的**唯一副本是分别用 C 和 S 的永久密钥加密后发送的**,因此**除 A(被所有方信任)之外没有其他任何一方知道该 session key**。
|
||
> - **该 ticket 会安全地、且由 A 背书地向 S 表明 C 被允许做什么。**
|
||
> - **A 与 C 之间来回传递的 nonce 确保了它们没有遭受重放攻击。**
|
||
> - C **还没有真正与 S 对话**,但它已经准备好了。
|
||
|
||
#### 4.3.6 密钥被攻破时会发生什么(官方分析)
|
||
|
||
**如果 C 的永久密钥被攻破**:
|
||
|
||
- 攻击者**可以冒充 C**,并获得 C 的全部权限;
|
||
- 攻击者**可以窃听 C 的合法会话**;
|
||
- 攻击者**也可以冒充 A**,但**只能在与 C 的对话中**冒充;
|
||
- 由于(按假设)它**没有任何服务的密钥**,它**无法为任何服务生成新的 ticket**;
|
||
- **不过它可以重放旧的 ticket 和 session key**,直到 S 的永久密钥被更换、或旧 ticket 超时。
|
||
|
||
**如果 S 的密钥被攻破**:
|
||
|
||
- 攻击者**可以向任何一方冒充 S**;
|
||
- 攻击者**可以窃听任何用户与 S 的会话**;
|
||
- **除非某个客户端的密钥也被攻破,否则攻击者无法为 S 生成新的伪造客户端 ticket**,因为那需要它用「它并不知道的那个客户端密钥」把自己认证为 A。
|
||
|
||
> 引自:[A Detailed Description of the Cephx Authentication Protocol](https://docs.ceph.com/en/tentacle/dev/cephx_protocol/)
|
||
|
||
---
|
||
|
||
### 4.4 Kerberos 认证(ceph_krb_auth.rst)
|
||
|
||
> 官方这一页有 1096 行,是本章最长的一页。**但它绝大部分内容是 Kerberos / LDAP / SASL / GSSAPI 的通用背景知识与 KDC 服务器的搭建步骤**,而非 Ceph 内部实现。本页最后更新时间标注为 **Dec 3, 2018**,作者 Daniel Oliveira (doliveira@suse.com)。
|
||
|
||
#### 4.4.1 背景概念(官方给出的定义)
|
||
|
||
| 概念 | 官方定义要点 |
|
||
|---|---|
|
||
| **Directory Services(目录服务)** | 一个可定制的信息存储,作为用户定位分布在网络中的资源与服务的**单一入口**,也为管理员提供管理其对象及属性的单一入口。虽然对用户表现为单一入口,实际通常以**分布式形式**存储。**至少由一个 Directory Server 和一个 Directory Client 组成,基于 X.500 标准实现。** 例子:OpenLDAP、389 Directory Server、MS Active Directory、NetIQ eDirectory。典型特征是 **write-once-read-many-times**。**其数据库不是为事务型数据设计的** |
|
||
| **LDAP (v3)** | 一组 **LDAP 协议交换**(**不是**服务器的实现),定义数据被访问的方法。**由 IETF 在 RFC 2251 中标准化**,描述数据在目录服务中如何表示(Data Model / DIT),以及数据如何被导入/导出(LDIF)。**LDAP 不定义数据如何存储或操作。** **没有任何目录服务实现实现了 LDAP v3 的全部特性** |
|
||
| **Authentication(认证)** | 校验凭证(如用户名/ID 与密码)以**验证身份** |
|
||
| **Authorization(授权)** | 在身份被成功认证之后发生,最终授予访问资源的完整权限。**决定访问系统的能力以及到什么程度** |
|
||
| **Auditing(审计)** | 取 authentication 与 authorization 的结果并记录到 audit log。认证与授权是**预防性**系统,审计是**反应性**系统 |
|
||
| **Kerberos (KRB v5)** | 一种网络**认证协议**,用**对称密钥(secret-key)密码学**为 client/server 应用提供强认证。**使用 ticket 来认证用户或服务应用,且从不以明文在网络上传输口令**,因此双方都能证明身份而无需发送任何未加密的秘密。可以用于 **SSO(单点登录)** |
|
||
| **SASL** | **RFC 4422**,一个帮助开发者实现不同认证机制的**框架**(实现一系列 challenge 与 response),允许客户端与服务端为每条连接**协商**一个双方都可接受的机制,而不是硬编码 |
|
||
| **GSSAPI** | **RFCs 2078, 2743, 2744, 4121, 4752**。**它不定义协议、认证或安全机制本身**,而是为多种认证机制提供**统一、通用的安全服务 API**。特点:mechanism independence、protocol independence、platform independence、QOP independence。**大多数支持 GSSAPI 的应用也支持 Kerberos v5。** Windows 用 SSPI 而非 GSSAPI,但 SSPI 客户端可以与 GSSAPI 服务端通信 |
|
||
| **SPNEGO** | **RFC 2478, 4178**。GSSAPI 解决了「给不同机制提供单一 API」的问题,但**没有解决「协商用哪个机制」的问题**。SPNEGO 提供一个框架,让参与认证的两方从一组可能的机制中**安全地协商**一个双方都支持的机制。它是一个 **meta protocol**,完全搭载在其他应用协议中传输,从不直接使用 |
|
||
|
||
**为什么关心这些?(官方的总结)**
|
||
|
||
1. **Ceph Kerberos 认证完全基于 MIT Kerberos 实现,并使用 GSSAPI。**
|
||
2. **目前仍在使用 Kerberos 的默认后端数据库,但计划加入 LDAP 作为后端**,这将提供:**用 GSSAPI (KRB5) 做认证 + 用 LDAP (LDAPv3) 做授权,经由 SASL 机制。**
|
||
|
||
#### 4.4.2 SASL 机制一览(官方列表)
|
||
|
||
| 机制 | RFC | 说明 |
|
||
|---|---|---|
|
||
| ANONYMOUS | RFC 4505 | 访客访问,即**未认证** |
|
||
| CRAM-MD5 | RFC 2195 | 基于 HMAC-MD5 的简单 challenge-response 方案,**不建立任何安全层**,比 DIGEST-MD5 与 GSSAPI 更不安全 |
|
||
| DIGEST-MD5 | RFC 2831 | 与 HTTP Digest(部分)兼容的、基于 MD5 的 challenge-response 方案,**提供数据安全层**。LDAPv3 服务器的强制认证方法 |
|
||
| EXTERNAL | RFCs 4422, 5246, 4301, 2119 | 认证隐含在上下文中(例如使用 IPsec 或 TLS)。使用公钥做强认证 |
|
||
| GS2 | RFC 5801 | 支持在 SASL 中使用任意 GSS-API 机制的机制族 |
|
||
| NTLM | MS 私有 | MS Windows NT LAN Manager 认证机制 |
|
||
| OAuth 1.0/2.0 | RFCs 5849, 6749, 7628 | 用于委托资源访问的认证协议 |
|
||
| OTP | RFC 2444 | 一次性口令机制(废弃了 SKEY) |
|
||
| PLAIN | RFC 4616 | 简单明文口令机制,大多数应用中不推荐 |
|
||
| SCRAM | RFCs 5802, 7677 | 带 channel binding 支持的现代 challenge-response 机制 |
|
||
|
||
#### 4.4.3 前置条件(Before We Start)
|
||
|
||
**环境假设**:Kerberos 已被正确配置,对**每个服务器和 KDC**都意味着:
|
||
|
||
1. **时间同步**(用 NTP 或 chrony)。
|
||
- 官方强调:**不只 Kerberos,Ceph 本身也依赖并仰仗时间同步。**
|
||
2. **DNS 解析**:
|
||
- **正向和反向**区域都要有,使用 **fqdn**(`hostname + domain.name`);
|
||
- KDC 发现可配置为使用 DNS `srv resources` 作为服务定位协议(RFCs 2052, 2782),以及用 host 或 domain 映射到合适的 realm(`txt record`);
|
||
- 虽然这些 DNS 条目/设置**不是运行一个 Kerberos realm 所必需的**,但它们**确实有助于消除在所有客户端上手工配置的需要**;
|
||
- **这一点极为重要,因为大多数 Kerberos 问题通常与名称解析有关。Kerberos 在检查系统名与主机查询时非常挑剔。**
|
||
3. **尽可能为基础设施中的每一部分(ntp、dns、kdc 服务器)配置备份/备用/从节点**,以避免单点故障。
|
||
|
||
#### 4.4.4 Kerberos 术语
|
||
|
||
| 术语 | 说明 |
|
||
|---|---|
|
||
| **Ticket** | 一组可用于验证客户端身份的信息。Kerberos ticket 可以存放在文件里,也可以只存在于内存中。**获得的第一张 ticket 是 ticket-granting ticket (TGT)**,它允许客户端获取额外的 ticket,而这些额外的 ticket 给予客户端访问特定服务的许可。请求与授予这些额外 ticket 的过程是**透明的** |
|
||
| **KDC** | KDC 为客户端创建 TGT,**用客户端的口令作为密钥加密它**,并把加密后的 TGT 发回客户端。客户端随后**尝试用自己的口令解密 TGT**。如果解密成功(即口令正确),它就保留解密后的 TGT,这就是**客户端身份的证明**。**KDC 由三部分组成:Kerberos database、Authentication service (AS)、Ticket-granting service (TGS)** |
|
||
| **Client** | 发送请求以获取 ticket 的**用户、主机或服务** |
|
||
| **Principal** | Kerberos 可以为其分配 ticket 的**唯一身份**。Principal 可以有任意数量的组成部分,各部分之间通常用 `/` 分隔。**最后一部分是 realm,通常用 `@` 分隔。** 如果没有 realm 部分,则假定该 principal 处于当前上下文的默认 realm。典型格式:**`primary/instance@REALM`**。**对 Ceph,我们将使用 `ceph` 作为 primary 名字**,这样更容易组织和识别与 Ceph 相关的 principal。**realm 通常是大写字母的域名** |
|
||
| **Keytab** | 一个 keytab 文件存放**可用于代替口令挑战的实际加密密钥**。为**非交互式 principal**(如 Service Principal Names,通常关联 Ceph 守护进程这类长期运行的进程)创建 keytab 文件很有用。**一个 keytab 文件不必须与单个 principal 一一对应,多个不同 principal 的 key 可以存放在同一个 keytab 文件中。** **keytab 应受到适当保护。** 默认客户端 keytab 文件是 **`/etc/krb5.keytab`** |
|
||
|
||
#### 4.4.5 Ceph 侧的配置步骤(官方 8 步)
|
||
|
||
**步骤 1**:登录到 Kerberos 客户端(Ceph 服务器节点),通过检查和正确编辑 `/etc/krb5.conf` 确认其配置正确:
|
||
|
||
```ini
|
||
/etc/krb5.conf
|
||
[libdefaults]
|
||
dns_canonicalize_hostname = false
|
||
rdns = false
|
||
forwardable = true
|
||
dns_lookup_realm = true
|
||
dns_lookup_kdc = true
|
||
allow_weak_crypto = false
|
||
default_realm = MYDOMAIN.COM
|
||
default_ccache_name = KEYRING:persistent:%{uid}
|
||
[realms]
|
||
MYDOMAIN.COM = {
|
||
kdc = kerberos.mydomain.com
|
||
admin_server = kerberos.mydomain.com
|
||
...
|
||
}
|
||
...
|
||
```
|
||
|
||
**步骤 2**:登录 KDC 服务器,确认它已正确配置为可以对该 Kerberos realm 进行认证。
|
||
|
||
a) Kerberos 相关的 DNS RRs:
|
||
|
||
```text
|
||
/var/lib/named/master/mydomain.com
|
||
kerberos IN A 192.168.10.21
|
||
kerberos-slave IN A 192.168.10.22
|
||
_kerberos IN TXT "MYDOMAIN.COM"
|
||
_kerberos._udp IN SRV 1 0 88 kerberos
|
||
_kerberos._tcp IN SRV 1 0 88 kerberos
|
||
_kerberos._udp IN SRV 20 0 88 kerberos-slave
|
||
_kerberos-master._udp IN SRV 0 0 88 kerberos
|
||
_kerberos-adm._tcp IN SRV 0 0 749 kerberos
|
||
_kpasswd._udp IN SRV 0 0 464 kerberos
|
||
...
|
||
```
|
||
|
||
b) KDC 配置文件:
|
||
|
||
```ini
|
||
/var/lib/kerberos/krb5kdc/kdc.conf
|
||
[kdcdefaults]
|
||
kdc_ports = 750,88
|
||
[realms]
|
||
MYDOMAIN.COM = {
|
||
acl_file = /var/lib/kerberos/krb5kdc/kadm5.acl
|
||
admin_keytab = FILE:/var/lib/kerberos/krb5kdc/kadm5.keytab
|
||
default_principal_flags = +postdateable +forwardable +renewable +proxiable
|
||
+dup-skey -preauth -hwauth +service
|
||
+tgt-based +allow-tickets -pwchange
|
||
-pwservice
|
||
dict_file = /var/lib/kerberos/krb5kdc/kadm5.dict
|
||
key_stash_file = /var/lib/kerberos/krb5kdc/.k5.MYDOMAIN.COM
|
||
kdc_ports = 750,88
|
||
max_life = 0d 10h 0m 0s
|
||
max_renewable_life = 7d 0h 0m 0s
|
||
}
|
||
...
|
||
```
|
||
|
||
**步骤 3**:在 KDC 服务器上运行 Kerberos 管理工具 `kadmin.local`,列出现有 principal:
|
||
|
||
```text
|
||
kadmin.local: listprincs
|
||
K/M@MYDOMAIN.COM
|
||
krbtgt/MYDOMAIN.COM@MYDOMAIN.COM
|
||
kadmin/admin@MYDOMAIN.COM
|
||
kadmin/changepw@MYDOMAIN.COM
|
||
kadmin/history@MYDOMAIN.COM
|
||
kadmin/kerberos.mydomain.com@MYDOMAIN.COM
|
||
root/admin@MYDOMAIN.COM
|
||
...
|
||
```
|
||
|
||
**步骤 4**:为**每一个**希望通过 Kerberos 认证的 Ceph 集群节点添加一个 principal:
|
||
|
||
```text
|
||
kadmin.local: addprinc -randkey ceph/ceph-mon1
|
||
Principal "ceph/ceph-mon1@MYDOMAIN.COM" created.
|
||
kadmin.local: addprinc -randkey ceph/ceph-osd1
|
||
Principal "ceph/ceph-osd1@MYDOMAIN.COM" created.
|
||
...
|
||
```
|
||
|
||
用户 principal 同理:
|
||
|
||
```text
|
||
kadmin.local: addprinc johndoe
|
||
WARNING: no policy specified for johndoe@MYDOMAIN.COM; defaulting to no policy
|
||
Enter password for principal "johndoe@MYDOMAIN.COM":
|
||
Re-enter password for principal "johndoe@MYDOMAIN.COM":
|
||
Principal "johndoe@MYDOMAIN.COM" created.
|
||
```
|
||
|
||
**步骤 5**:为**每个** Ceph 集群节点创建 keytab 文件:
|
||
|
||
```text
|
||
kadmin.local: ktadd -k /etc/gss_client_mon1.ktab ceph/ceph-mon1
|
||
Entry for principal ceph/ceph-mon1 with kvno 2, encryption type aes256-cts-hmac-sha1-96 added to keytab WRFILE:/etc/gss_client_mon1.ktab.
|
||
Entry for principal ceph/ceph-mon1 with kvno 2, encryption type aes128-cts-hmac-sha1-96 added to keytab WRFILE:/etc/gss_client_mon1.ktab.
|
||
Entry for principal ceph/ceph-mon1 with kvno 2, encryption type des3-cbc-sha1 added to keytab WRFILE:/etc/gss_client_mon1.ktab.
|
||
Entry for principal ceph/ceph-mon1 with kvno 2, encryption type arcfour-hmac added to keytab WRFILE:/etc/gss_client_mon1.ktab.
|
||
kadmin.local: ktadd -k /etc/gss_client_osd1.ktab ceph/ceph-osd1
|
||
Entry for principal ceph/ceph-osd1 with kvno 2, encryption type aes256-cts-hmac-sha1-96 added to keytab WRFILE:/etc/gss_client_osd1.ktab.
|
||
Entry for principal ceph/ceph-osd1 with kvno 2, encryption type aes128-cts-hmac-sha1-96 added to keytab WRFILE:/etc/gss_client_osd1.ktab.
|
||
Entry for principal ceph/ceph-osd1 with kvno 2, encryption type des3-cbc-sha1 added to keytab WRFILE:/etc/gss_client_osd1.ktab.
|
||
Entry for principal ceph/ceph-osd1 with kvno 2, encryption type arcfour-hmac added to keytab WRFILE:/etc/gss_client_osd1.ktab.
|
||
kadmin.local: ktadd -k /etc/gss_client_osd3.ktab ceph/ceph-osd3
|
||
Entry for principal ceph/ceph-osd3 with kvno 3, encryption type aes256-cts-hmac-sha1-96 added to keytab WRFILE:/etc/gss_client_osd3.ktab.
|
||
kadmin.local: ktadd -k /etc/gss_client_osd4.ktab ceph/ceph-osd4
|
||
Entry for principal ceph/ceph-osd4 with kvno 4, encryption type aes256-cts-hmac-sha1-96 added to keytab WRFILE:/etc/gss_client_osd4.ktab.
|
||
...
|
||
```
|
||
|
||
官方提示:**每个 `ktadd` 都会为同一 principal 写入 4 种 encryption type 的 key**:`aes256-cts-hmac-sha1-96`、`aes128-cts-hmac-sha1-96`、`des3-cbc-sha1`、`arcfour-hmac`;不同 principal 的 `kvno` 会各自递增(示例中 osd1/osd2 是 2,osd3 是 3,osd4 是 4)。
|
||
|
||
列出生成的所有 keytab 文件,并用 `klist -kte` 逐个校验:
|
||
|
||
```bash
|
||
# ls -1 /etc/gss_client_*
|
||
/etc/gss_client_mon1.ktab
|
||
/etc/gss_client_osd1.ktab
|
||
/etc/gss_client_osd2.ktab
|
||
/etc/gss_client_osd3.ktab
|
||
/etc/gss_client_osd4.ktab
|
||
|
||
# klist -kte /etc/gss_client_mon1.ktab
|
||
Keytab name: FILE:/etc/gss_client_mon1.ktab
|
||
KVNO Timestamp Principal
|
||
---- ------------------- ------------------------------------------------------
|
||
2 10/8/2018 14:35:30 ceph/ceph-mon1@MYDOMAIN.COM (aes256-cts-hmac-sha1-96)
|
||
2 10/8/2018 14:35:31 ceph/ceph-mon1@MYDOMAIN.COM (aes128-cts-hmac-sha1-96)
|
||
2 10/8/2018 14:35:31 ceph/ceph-mon1@MYDOMAIN.COM (des3-cbc-sha1)
|
||
2 10/8/2018 14:35:31 ceph/ceph-mon1@MYDOMAIN.COM (arcfour-hmac)
|
||
```
|
||
|
||
**步骤 6**:Ceph 新增了一个参数 **`gss_ktab_client_file`**,它指向与该 Ceph 节点(或 principal)相关的 keytab 文件。
|
||
|
||
- **默认值**:`/var/lib/ceph/$name/gss_client_$name.ktab`
|
||
- 例如对于 Ceph 服务器 `osd1.mydomain.com`,keytab 文件的位置与名字应为:`/var/lib/ceph/osd1/gss_client_osd1.ktab`
|
||
|
||
分发 keytab:
|
||
|
||
```bash
|
||
# for node in mon1 osd1 osd2 osd3 osd4; do scp /etc/gss_client_$node*.ktab root@ceph-$node:/var/lib/ceph/$node/; done
|
||
```
|
||
|
||
此时,**即使不使用任何 keytab 客户端文件**,我们也应当已经能够认证一个用户 principal:
|
||
|
||
```bash
|
||
# kdestroy -A && kinit -f johndoe && klist -f
|
||
Password for johndoe@MYDOMAIN.COM:
|
||
Ticket cache: KEYRING:persistent:0:0
|
||
Default principal: johndoe@MYDOMAIN.COM
|
||
|
||
Valid starting Expires Service principal
|
||
10/10/2018 15:32:01 10/11/2018 07:32:01 krbtgt/MYDOMAIN.COM@MYDOMAIN.COM
|
||
renew until 10/11/2018 15:32:01, Flags: FRI
|
||
```
|
||
|
||
使用 keytab 认证:
|
||
|
||
```bash
|
||
# kdestroy -A && kinit -k -t /etc/gss_client_mon1.ktab -f 'ceph/ceph-mon1@MYDOMAIN.COM' && klist -f
|
||
Ticket cache: KEYRING:persistent:0:0
|
||
Default principal: ceph/ceph-mon1@MYDOMAIN.COM
|
||
|
||
Valid starting Expires Service principal
|
||
10/10/2018 15:54:25 10/11/2018 07:54:25 krbtgt/MYDOMAIN.COM@MYDOMAIN.COM
|
||
renew until 10/11/2018 15:54:25, Flags: FRI
|
||
```
|
||
|
||
**步骤 7**:默认客户端 keytab(若存在且可读)被用于自动获取 GSSAPI 客户端应用的初始凭证。获取初始凭证时默认使用该客户端 keytab 中**第一条记录的 principal 名字**。查找顺序是:
|
||
|
||
1. 环境变量 **`KRB5_CLIENT_KTNAME`**;
|
||
2. `[libdefaults]` 中的 profile 变量 **`default_client_keytab_name`**;
|
||
3. 硬编码默认值 **`DEFCKTNAME`**。
|
||
|
||
**Ceph 的做法是**:**在内部把环境变量 `KRB5_CLIENT_KTNAME` 设为与 `gss_ktab_client_file` 相同的位置**,并修改 `ceph.conf` 以加入新的认证方法:
|
||
|
||
```ini
|
||
/etc/ceph/ceph.conf
|
||
[global]
|
||
...
|
||
auth_cluster_required = gss
|
||
auth_service_required = gss
|
||
auth_client_required = gss
|
||
gss_ktab_client_file = /{$my_new_location}/{$my_new_ktab_client_file.keytab}
|
||
...
|
||
```
|
||
|
||
**步骤 8**:GSSAPI 随后就能读取 keytab 文件,并借助(由 DNS 提供的)名称与服务解析,按如下流程请求 TGT:
|
||
|
||
1. 用户/客户端把 principal 身份与凭证发送给 KDC 服务器(TGT 请求);
|
||
2. KDC 在其内部数据库中检查该 principal;
|
||
3. KDC 用该 principal 的 key 创建并包装一个 TGT(TGT + Key);
|
||
4. 新创建的 TGT 被解密并存入凭证缓存;
|
||
5. 此时,Kerberos/GSSAPI 感知的应用(和/或服务)就能够检查 keytab 文件中活跃的 TGT 列表。
|
||
|
||
#### 4.4.6 给 Ceph 开发者:为什么用 GSSAPI 而不是原生 KRB5 API
|
||
|
||
> 官方原文:**我们当然可以直接使用原生 `KRB5 APIs`(而不是 `GSSAPIs`),但我们想要一个在网络安全方面更可移植的选项,而这正是 `GSS`(Generic Security Standard)`-API` 的标志。它本身并不真正提供安全服务,而是一个「以通用方式向调用者提供安全服务」的框架。**
|
||
|
||
分层:
|
||
|
||
```text
|
||
+---------------------------------+
|
||
| Application |
|
||
+---------------------------------+
|
||
| Protocol (RPC, Etc. [Optional]) |
|
||
+---------------------------------+
|
||
| GSS-API |
|
||
+---------------------------------+
|
||
| Security Mechs (Krb v5, Etc) |
|
||
+---------------------------------+
|
||
```
|
||
|
||
**GSS-API 做的两件主要的事**:
|
||
|
||
1. **创建一个安全上下文(security context)**,数据可以在应用之间在其中传递。上下文可以被看作应用之间的一种「信任状态(state of trust)」。共享一个上下文的应用互相知道对方是谁,因此只要上下文持续,就可以允许数据传输。
|
||
2. **对要传输的数据施加一种或多种保护**,即所谓的「安全服务(security services)」。
|
||
|
||
**GSS-API 提供的四种可移植性**:
|
||
|
||
| 类型 | 说明 |
|
||
|---|---|
|
||
| **Mechanism independence** | 为已实现的机制提供通用接口。指定默认安全机制后,应用**不需要知道**它在用哪个机制 |
|
||
| **Protocol independence** | 独立于任何通信协议或协议族,可用于 sockets、RPC、TCP/IP 等。`RPCSEC_GSS` 是把 GSS-API 与 RPC 平滑集成的附加层 |
|
||
| **Platform independence** | 完全不在意应用运行在什么操作系统上 |
|
||
| **Quality of Protection (QOP) independence** | QOP 是用于加密数据或生成密码学标签的算法类型;GSS-API 允许程序员忽略 QOP,使用其提供的默认值(也可以指定) |
|
||
|
||
**GSS-API 提供的三种安全服务**:
|
||
|
||
| 服务 | 说明 |
|
||
|---|---|
|
||
| **Authentication(基本服务)** | 验证身份:如果你被认证了,就意味着你被认可是你所声称的那个人 |
|
||
| **Integrity** | 数据可以附带一个密码学标签,即 **`Message Integrity Code (MIC)`**,以证明到达的数据与发送方发出的数据相同 |
|
||
| **Confidentiality** | 允许数据被加密(如果底层机制支持) |
|
||
|
||
**当前 GSS-API 可用的机制(官方表格)**:
|
||
|
||
| Mechanism Name | Object Identifier | Shared Library | Kernel Module |
|
||
|---|---|---|---|
|
||
| diffie_hellman_640_0 | 1.3.6.4.1.42.2.26.2.4 | dh640-0.so.1 | |
|
||
| diffie_hellman_1024_0 | 1.3.6.4.1.42.2.26.2.5 | dh1024-0.so.1 | |
|
||
| SPNEGO | 1.3.6.1.5.5.2 | | |
|
||
| iakerb | 1.3.6.1.5.2.5 | | |
|
||
| SCRAM-SHA-1 | 1.3.6.1.5.5.14 | | |
|
||
| SCRAM-SHA-256 | 1.3.6.1.5.5.18 | | |
|
||
| GSS-EAP (arc) | 1.3.6.1.5.5.15.1.1.* | | |
|
||
| **kerberos_v5** | **1.2.840.113554.1.2.2** | gl/mech_krb5.so | gl_kmech_krb5 |
|
||
|
||
**OID 的两种格式**:
|
||
|
||
1. `{ 1 2 3 4 }` —— GSS-API 规范正式规定的格式,`gss_str_to_oid()` 期望这一种;
|
||
2. `1.2.3.4` —— 更广泛使用,但**不是官方标准格式**。
|
||
|
||
**GSS-API 不做的事**(为了最大化其通用性):
|
||
|
||
| # | 不做的事 |
|
||
|---|---|
|
||
| a | 为用户或应用提供安全凭证(必须由底层安全机制提供;GSS-API 允许应用自动或显式地获取凭证) |
|
||
| b | 在应用之间传输数据(传输所有数据是应用自己的责任) |
|
||
| c | 区分不同类型的被传输数据(例如判断一个数据包是普通数据还是 GSS-API 相关数据) |
|
||
| d | 指示由于远端(异步)错误导致的状态 |
|
||
| e | 自动保护多进程程序的进程之间发送的信息 |
|
||
| f | 分配要传给 GSS-API 函数的字符串缓冲区 |
|
||
| g | 释放 GSS-API 数据空间(必须用 `gss_release_buffer()` 和 `gss_delete_name()` 等函数显式释放) |
|
||
|
||
**使用 GSS-API 的基本步骤(6 步)**:
|
||
|
||
1. 每个应用(发送方与接收方)**获取凭证**(如果凭证尚未被自动获取,则显式获取);
|
||
2. **发送方发起**一个安全上下文,**接收方接受**它;
|
||
3. 发送方对要传输的消息(数据)**施加安全保护** —— 即加密消息,或给它打上识别标签,然后传输。(发送方也可以选择不施加任何安全保护,此时消息只关联 GSS-API 的默认安全服务,即**认证** —— 接收方知道发送方就是它所声称的那个人);
|
||
4. 接收方**解密**消息(如需要)并**验证**它(如适用);
|
||
5. (可选)接收方向发送方返回一个识别标签以作确认;
|
||
6. 双方**销毁**共享的安全上下文;如有必要,也可以释放任何"残留"的 GSS-API 数据。
|
||
|
||
使用 GSS-API 的应用应当包含头文件 **`gssapi.h`**。
|
||
|
||
**官方给出的参考**:RFC 1964、RFC 2743、RFC 2744、RFC 4178、RFC 6649,以及 MIT Kerberos Documentation。
|
||
|
||
#### 4.4.7 Kerberos 服务器搭建(官方步骤摘要)
|
||
|
||
> ⚠️ 官方明确声明:**"this is not a recommendation for a production environment"**(这不是对生产环境的建议)。不覆盖 Master/Slave 复制集群,也不覆盖生产环境相关的任何东西(ntp/chrony、dns、pam/nss、sssd 等)。
|
||
|
||
官方另外补充的几点前提(源文 830–866 行):
|
||
|
||
- **服务端**:不同的**后端数据库**可能需要不同的依赖和配置步骤,**`LDAP as a backend database`** 就是一个典型例子。
|
||
- **客户端**:不同的**客户端后端配置**步骤不同,例如 `PAM/NSS` 或 `SSSD`(配合 LDAP 做 identity service、Kerberos 做 authentication service)——**SSSD 是加入 MS Active Directory 域、做 User Logon Management 时最合适的选项**。
|
||
- 官方**无意覆盖所有可能的场景/组合**,这里只给"把一个(MIT)Kerberos Server 跑起来"的最简步骤。
|
||
- **rpm 包名、二进制与配置文件路径可能因发行版而略有不同。**
|
||
|
||
> 部分发行版自带 `wizard`,可以完成基础配置(官方以 SUSE 为例),但我们下面的内容走的是**手工配置(manual configuration)**:
|
||
|
||
```text
|
||
SUSE:
|
||
Kerberos server:
|
||
yast2 auth-server
|
||
|
||
Kerberos client:
|
||
pam/nss: yast2 ldapkrb
|
||
sssd: yast2 auth-client
|
||
```
|
||
|
||
**步骤 1**:安装 KDC 服务器
|
||
|
||
a) 安装所需软件包:
|
||
|
||
```bash
|
||
SUSE: zypper install krb5 krb5-server krb5-client
|
||
开发额外需要: krb5-devel
|
||
使用 'sssd' 额外需要: sssd-krb5 sssd-krb5-common
|
||
|
||
REDHAT: yum install krb5-server krb5-libs krb5-workstation
|
||
Additionally: 'Needs to be checked'
|
||
```
|
||
|
||
> 💡 补充:官方对 REDHAT 的额外依赖只写了 `'Needs to be checked'`(待确认),未给出具体包名。
|
||
|
||
b) 编辑 KDC 服务器配置文件 `/var/lib/kerberos/krb5kdc/kdc.conf`(与 4.4.5 步骤 2b 为同一份配置):
|
||
|
||
```ini
|
||
/var/lib/kerberos/krb5kdc/kdc.conf
|
||
[kdcdefaults]
|
||
kdc_ports = 750,88
|
||
[realms]
|
||
MYDOMAIN.COM = {
|
||
acl_file = /var/lib/kerberos/krb5kdc/kadm5.acl
|
||
admin_keytab = FILE:/var/lib/kerberos/krb5kdc/kadm5.keytab
|
||
default_principal_flags = +postdateable +forwardable +renewable +proxiable
|
||
+dup-skey -preauth -hwauth +service
|
||
+tgt-based +allow-tickets -pwchange
|
||
-pwservice
|
||
dict_file = /var/lib/kerberos/krb5kdc/kadm5.dict
|
||
key_stash_file = /var/lib/kerberos/krb5kdc/.k5.MYDOMAIN.COM
|
||
kdc_ports = 750,88
|
||
max_life = 0d 10h 0m 0s
|
||
max_renewable_life = 7d 0h 0m 0s
|
||
}
|
||
...
|
||
```
|
||
|
||
c) 编辑 Kerberos 客户端配置文件 `/etc/krb5.conf`(**注意这里比 4.4.5 步骤 1 多了两行 `//-->` 注释**):
|
||
|
||
```ini
|
||
/etc/krb5.conf
|
||
[libdefaults]
|
||
dns_canonicalize_hostname = false
|
||
rdns = false
|
||
forwardable = true
|
||
dns_lookup_realm = true //--> if using DNS/DNSMasq
|
||
dns_lookup_kdc = true //--> if using DNS/DNSMasq
|
||
allow_weak_crypto = false
|
||
default_realm = MYDOMAIN.COM
|
||
default_ccache_name = KEYRING:persistent:%{uid}
|
||
|
||
[realms]
|
||
MYDOMAIN.COM = {
|
||
kdc = kerberos.mydomain.com
|
||
admin_server = kerberos.mydomain.com
|
||
...
|
||
}
|
||
...
|
||
```
|
||
|
||
**步骤 2**:创建 Kerberos 数据库
|
||
|
||
```bash
|
||
SUSE: kdb5_util create -s
|
||
REDHAT: kdb5_util create -s
|
||
```
|
||
|
||
**步骤 3**:启用并启动 KDC 与 KDC admin 服务器
|
||
|
||
```bash
|
||
SUSE: systemctl enable/start krb5kdc
|
||
systemctl enable/start kadmind
|
||
REDHAT: systemctl enable/start krb5kdc
|
||
systemctl enable/start kadmin
|
||
```
|
||
|
||
**步骤 4**:创建 Kerberos 管理员
|
||
|
||
> Kerberos principal **既可以在 KDC 服务器本地创建,也可以经由网络使用 'admin principal' 创建**。下面在 KDC 服务器上使用 `kadmin.local`。
|
||
|
||
a) 列出现有 principal:
|
||
|
||
```text
|
||
kadmin.local: listprincs
|
||
K/M@MYDOMAIN.COM
|
||
krbtgt/MYDOMAIN.COM@MYDOMAIN.COM
|
||
kadmin/admin@MYDOMAIN.COM
|
||
kadmin/changepw@MYDOMAIN.COM
|
||
kadmin/history@MYDOMAIN.COM
|
||
kadmin/kerberos.mydomain.com@MYDOMAIN.COM
|
||
root/admin@MYDOMAIN.COM
|
||
...
|
||
```
|
||
|
||
b) 如果没有内置的 'admin principal',就创建一个。官方用 **`root`** 作为 principal 名,因为**默认情况下 `kinit` 会尝试用与系统登录用户名相同的名字去认证,除非显式传入一个 principal 参数(`kinit principal`)**:
|
||
|
||
```bash
|
||
# kadmin.local -q "addprinc root/admin"
|
||
Authenticating as principal root/admin@MYDOMAIN.COM with password.
|
||
WARNING: no policy specified for root/admin@MYDOMAIN.COM; defaulting to no policy
|
||
Enter password for principal "root/admin@MYDOMAIN.COM":
|
||
```
|
||
|
||
c) 确认新建的 'admin principal' 在 KDC ACL 中有所需权限(**ACL 变更后需要重启 `kadmind`**):
|
||
|
||
```text
|
||
SUSE: /var/lib/kerberos/krb5kdc/kadm5.acl
|
||
REDHAT: /var/kerberos/krb5kdc/kadm5.acl
|
||
|
||
###############################################################################
|
||
#Kerberos_principal permissions [target_principal] [restrictions]
|
||
###############################################################################
|
||
#
|
||
*/admin@MYDOMAIN.COM *
|
||
```
|
||
|
||
d) 创建一个简单的 'user principal'(与 *The 'Ceph side' of the things* 的 4a 步骤相同):
|
||
|
||
```text
|
||
kadmin.local: addprinc johndoe
|
||
WARNING: no policy specified for johndoe@MYDOMAIN.COM; defaulting to no policy
|
||
Enter password for principal "johndoe@MYDOMAIN.COM":
|
||
Re-enter password for principal "johndoe@MYDOMAIN.COM":
|
||
Principal "johndoe@MYDOMAIN.COM" created.
|
||
```
|
||
|
||
e) 确认新建的 'user principal' 能够认证(与 *The 'Ceph side' of the things* 的第 6 步相同):
|
||
|
||
```bash
|
||
# kdestroy -A && kinit -f johndoe && klist -f
|
||
Password for johndoe@MYDOMAIN.COM:
|
||
Ticket cache: KEYRING:persistent:0:0
|
||
Default principal: johndoe@MYDOMAIN.COM
|
||
|
||
Valid starting Expires Service principal
|
||
11/16/2018 13:11:16 11/16/2018 23:11:16 krbtgt/MYDOMAIN.COM@MYDOMAIN.COM
|
||
renew until 11/17/2018 13:11:16, Flags: FRI
|
||
...
|
||
```
|
||
|
||
**步骤 5**:到此我们应当已经有一个**简单可用的(MIT)Kerberos Server** 了
|
||
|
||
a) 考虑到我们会想同时为 'user' 和 'service' principal 使用 keytab 文件,请回到 *The 'Ceph side' of the things* 的**第 4 步**开始做起。
|
||
|
||
b) 官方建议**熟悉下面这些工具及其 man page**(列表见本节末)。
|
||
|
||
**步骤 6**:名称解析(**官方强调:Kerberos 严重依赖名称解析,大多数 Kerberos 问题都与它有关**)
|
||
|
||
a) DNS RRs(见 4.4.5 步骤 2a,源文此处为 6a,指向 *The 'Ceph side' of the things*; step 2a 与 *Before We Start* 的 DNS resolution 一节)。使用 `srv resources` 做服务定位、用 `txt record` 做 host/domain 到 realm 的映射;
|
||
b) 对于**没有 DNS 服务器可用的小网络或开发环境**,可以选择用 **`DNSMasq`** —— 一个易于配置的轻量级 DNS 服务器(还带有一些其他能力)。除所需的 'host records' 外,可以把下面这些记录加进 `/etc/dnsmasq.conf`:
|
||
|
||
```text
|
||
/etc/dnsmasq.conf
|
||
...
|
||
txt-record=_kerberos.mydomain.com,"MYDOMAIN.COM"
|
||
srv-host=_kerberos._udp.mydomain.com,kerberos.mydomain.com,88,1
|
||
srv-host=_kerberos._udp.mydomain.com,kerberos-2.mydomain.com,88,20
|
||
srv-host=_kerberos-master._udp.mydomain.com,kerberos.mydomain.com,88,0
|
||
srv-host=_kerberos-adm._tcp.mydomain.com,kerberos.mydomain.com,749,0
|
||
srv-host=_kpasswd._udp.mydomain.com,kerberos.mydomain.com,464,0
|
||
srv-host=_kerberos._tcp.mydomain.com,kerberos.mydomain.com,88,1
|
||
...
|
||
```
|
||
|
||
c) b) 全部配置好、且 `dnsmasq` 服务起来之后,可以用下面的方式测试:
|
||
|
||
```bash
|
||
# nslookup kerberos
|
||
Server: 192.168.10.1
|
||
Address: 192.168.10.1#53
|
||
|
||
Name: kerberos.mydomain.com
|
||
Address: 192.168.10.21
|
||
|
||
# host -t SRV _kerberos._tcp.mydomain.com
|
||
_kerberos._tcp.mydomain.com has SRV record 1 0 88 kerberos.mydomain.com.
|
||
|
||
# host -t SRV {each srv-host record}
|
||
# host -t TXT _kerberos.mydomain.com
|
||
_kerberos.mydomain.com descriptive text "MYDOMAIN.COM"
|
||
```
|
||
|
||
f) 只要 `name resolution` 正常工作(无论用的是 `dnsmasq` 还是 `named`),Kerberos 就应当能够找到它所需要的服务。
|
||
|
||
> 💡 补充:源文这一节的编号是 **a、b、c、f**,**跳过了 d 和 e**(原文即如此,未见对应内容)。
|
||
|
||
**常用的 Kerberos 工具(官方建议熟悉它们的 man page)**:
|
||
|
||
| 名称 | 用途 |
|
||
|---|---|
|
||
| `krb5.conf` | Krb 客户端配置文件 |
|
||
| `kdc.conf` | KDC 服务器配置文件 |
|
||
| `krb5kdc` | KDC 服务器守护进程 |
|
||
| `kadmind` | KDC 管理守护进程 |
|
||
| `kadmin` | Krb 管理工具 |
|
||
| `kdb5_util` | Krb 底层数据库管理工具 |
|
||
| `kinit` | 获取并缓存 Kerberos TGT 的工具 |
|
||
| `klist` | 列出已缓存 Kerberos ticket 的工具 |
|
||
| `kdestroy` | 销毁 Kerberos ticket 的工具 |
|
||
|
||
#### 4.4.8 相关配置项速查(含 v20.2.4 真实默认值)
|
||
|
||
> 默认值取自 src/common/options/*.yaml.in @ v20.2.4
|
||
|
||
| 配置项 | type / level | 默认值 | 官方 desc 要点 |
|
||
|---|---|---|---|
|
||
| `gss_ktab_client_file` | str / advanced | `/var/lib/ceph/$name/gss_client_$name.ktab` | GSS/KRB5 Keytab file for client authentication;`long_desc`:设置 GSS/Kerberos 客户端 keytab 文件的**完整路径**。`services: mon, osd` |
|
||
| `gss_target_name` | str / advanced | `ceph` | `long_desc`:设置 gss target service name。`services: mon, osd`(**无 `desc`,只有 `long_desc`**) |
|
||
| `auth_cluster_required` | str / advanced | `cephx`(`flags: runtime`) | authentication methods required by the cluster;`fmt_desc`:**Valid settings are `cephx` or `none`** |
|
||
| `auth_service_required` | str / advanced | `cephx`(`flags: runtime`) | authentication methods required by service daemons;**Valid settings are `cephx` or `none`** |
|
||
| `auth_client_required` | str / advanced | `cephx, none`(`flags: runtime`) | authentication methods allowed by clients;**Valid settings are `cephx` or `none`** |
|
||
|
||
`gss_ktab_client_file` 的默认值与源文步骤 6 的描述**完全一致**:源文说它默认指向 `/var/lib/ceph/$name/gss_client_$name.ktab`,例如 `osd1.mydomain.com` 上就是 `/var/lib/ceph/osd1/gss_client_osd1.ktab`。
|
||
|
||
Ceph 侧启用 gss 的最小配置(源文步骤 7 给出):
|
||
|
||
```ini
|
||
/etc/ceph/ceph.conf
|
||
[global]
|
||
...
|
||
auth_cluster_required = gss
|
||
auth_service_required = gss
|
||
auth_client_required = gss
|
||
gss_ktab_client_file = /{$my_new_location}/{$my_new_ktab_client_file.keytab}
|
||
...
|
||
```
|
||
|
||
> 💡 补充:源文**没有**提到 `gss_target_name`,该配置项来自 `src/common/options/global.yaml.in`,其默认值 `ceph` 正好对应源文里"Ceph 用 `ceph` 作为 principal 的 primary 名字"的约定。
|
||
|
||
#### 4.4.9 与 cephx 的关系、限制与已知问题
|
||
|
||
**源文实际写明的、与 cephx 的对照关系**(只有两条,见 4.4.1 末尾官方总结):
|
||
|
||
1. **Ceph Kerberos 认证完全基于 MIT Kerberos 实现,并使用 GSSAPI**——而 cephx 是 Ceph 自带的认证协议(其协议设计"看起来很像 Kerberos",见 4.2 节);
|
||
2. **当前仍使用 Kerberos 的默认后端数据库**,官方**计划**增加 LDAP 作为后端,从而实现 **GSSAPI (KRB5) 做认证 + LDAP (LDAPv3) 做授权,经由 SASL 机制**。
|
||
|
||
**源文未写明的内容(据实标注)**:
|
||
|
||
| 问题 | 结论 |
|
||
|---|---|
|
||
| `gss` 认证是否覆盖 mon / osd / mds / mgr 全部守护进程与全部客户端 | **文档未明确**(仅 `gss_ktab_client_file` / `gss_target_name` 的 `services` 标注为 `mon`、`osd`) |
|
||
| 启用 gss 后是否仍可同时使用 cephx、两者如何共存或切换 | **文档未明确** |
|
||
| Kerberos ticket 与 Ceph 的 auth ticket / capability(caps)如何映射 | **文档未明确** |
|
||
| 已知问题(known issues)列表 | **文档未明确**(源文没有专门的限制/已知问题章节) |
|
||
|
||
> 💡 补充(基于本机 options 源文件的观察,非官方结论):在 v20.2.4 的 `src/common/options/global.yaml.in` 中,`auth_cluster_required` / `auth_service_required` / `auth_client_required` 三者 `fmt_desc` 里声明的 **Valid settings 只有 `cephx` 或 `none`,并未列出 `gss`**;而 `gss_ktab_client_file` 与 `gss_target_name` 仍然存在且 `services` 仅为 `mon`、`osd`。结合源文最后更新时间为 **Dec 3, 2018**,这一页属于**开发文档(dev/)中的实验性/历史性说明**,落地使用前应自行核对当前代码与版本支持情况。
|
||
|
||
> 💡 补充:源文正文在第 1096 行(即 6f 那句"只要名称解析正常工作,无论 dnsmasq 还是 named,Kerberos 就应能找到所需服务")结束,其后没有更多内容。
|
||
|
||
> 引自:[Kerberos Authentication](https://docs.ceph.com/en/tentacle/dev/ceph_krb_auth/)
|
||
|
||
---
|
||
|
||
### 4.5 会话认证(session_authentication.rst)
|
||
|
||
> 这也是 **Peter Reiher 于 2012-07-30** 撰写的历史文档,描述的是**当时新加入的**会话认证功能。⚠️ 注意文档中的文件路径(`msg/Pipe.cc`、`msg/SimpleMessager.cc` 等)属于 **SimpleMessenger 时代**,Tentacle 中的 async messenger 路径已不同。
|
||
|
||
#### 4.5.1 设计动机
|
||
|
||
```text
|
||
原始的 Cephx 协议把客户端认证给认证服务器,并建立一个 session key,
|
||
该 key 用于把客户端认证给它需要通信的服务端。
|
||
|
||
但是,它并没有认证客户端与服务端之间「持续的(ongoing)」消息。
|
||
|
||
基于它们共享一个秘密密钥这一事实,
|
||
这些持续的会话消息可以很容易地通过「用该 key 对消息签名」来认证。
|
||
```
|
||
|
||
本文档描述的就是**允许这种持续会话认证的代码改动**。
|
||
|
||
- 这些改动**允许未来让其他认证协议**(以及现有的 null `NONE` 与 `UNKNOWN` 协议)**处理签名**,但在撰写本文时,**唯一真正做签名的协议是 Cephx 协议**。
|
||
- 代码**也准备好处理会话消息的加密与解密**(这将在签名提供的完整性之上增加保密性)。**不过目前没有任何已实现的协议会加密持续的会话消息。**
|
||
|
||
#### 4.5.2 基本模型
|
||
|
||
- 本代码在 **Cephx 协议完成之后**才发挥作用。此时客户端与服务端共享一个秘密密钥,该 key 将用于认证。
|
||
- **这里的 "session" 由一个已建立的 pipe(管道)表示。**
|
||
- 对于这样的 pipe,**应当有一个 `session_security` 结构附在其上**。
|
||
- **每当要在该 pipe 上发送一条消息时,处理此类会话安全性的签名的代码就会被调用。**
|
||
- **在 pipe 的另一端,检查此类会话安全性的消息签名的代码会被调用。**
|
||
- **签名检查失败的消息将不会被继续处理。**
|
||
- 这意味着:**发送方最好与接收方就「正在使用的会话安全性」达成一致**,否则它们之间的消息会被一律丢弃。
|
||
|
||
**要让这个功能工作,需要几个步骤**:
|
||
|
||
1. 发送方与接收方必须**有一次成功的 cephx 协议运行**,以建立一个共享 key;
|
||
2. 它们必须**把该 key 存放在某个 pipe 稍后能取到的地方**,以便用它签名消息;
|
||
3. **发出的消息必须被签名,收到的消息必须被检查签名。**
|
||
|
||
**签名的计算方式可以有多种,但目前其大小被限制为 64 位。**
|
||
**一条消息的签名被放在它的 footer 中,字段名为 `sig`。**
|
||
|
||
**开关**:签名代码可以在运行时通过一个名为 **`cephx_sign_messages`** 的 Ceph boolean 选项打开或关闭。
|
||
**它当前默认为 `false`,因此不会有消息被签名。必须把它改为 `true` 才会计算并检查签名。**
|
||
|
||
#### 4.5.3 存储密钥(Storing the Key)
|
||
|
||
- 该 key 在**发送端用于创建签名**、在**接收端用于检查签名**。
|
||
- 未来如果使用非对称加密,**可能需要存两个 key**(本端一个私钥、对端一个公钥)。**目前两个方向的消息都用同一个 key 签名,所以只需要保存那一个 key。**
|
||
|
||
**密钥在 pipe 建立时被保存**:
|
||
|
||
| 端 | 时机 | 位置 |
|
||
|---|---|---|
|
||
| **客户端侧** | 在 **`connect()`** 中(位于 **`msg/Pipe.cc`**) | 该 key 从一次 Cephx 协议运行中取得,该运行产生了一个**已被成功检查的 authorizer 结构**。如果有这样一个 authorizer 可用,代码就**调用 `get_auth_session_handler()` 创建一个新的认证会话 handler,并把它存放在 pipe 数据结构中** |
|
||
| **服务端侧** | 在 **`accept()`** 中,**在客户端提供的 authorizer 被验证之后** | 做类似的事情 |
|
||
|
||
**一旦连接两端都完成了这些事情,会话认证就可以开始了。**
|
||
|
||
**特殊情况**:`connect()` 与 `accept()` 也被用于处理「正在建立一个新 session」的情形。此时还没有 authorizer 被创建,因此没有 key。
|
||
**调用签名代码的那些代码中的特例会跳过这些调用 —— 当 `CEPH_AUTH_UNKNOWN` 协议正在使用时。**
|
||
该协议标签出现在 session 中 pre-authorizer 的那些消息上,**表示「正在协商认证协议,因此无法签名」**。
|
||
官方认为**这不是安全问题**:**在本 session 稍后会进行一次可靠的认证操作,之后才会传递任何敏感信息。**
|
||
|
||
#### 4.5.4 签名消息(Signing Messages)
|
||
|
||
- 消息在位于 **`msg/Pipe.cc`** 的 **`write_message`** 调用中被签名。
|
||
- **实际的签名过程是:用共享密钥加密消息的 CRC。** 因此,**我们必须把签名推迟到所有 CRC 都计算完毕之后**。
|
||
- **header CRC 是最后计算的,所以我们一计算出那个 CRC,就调用 `sign_message()`。**
|
||
|
||
**`sign_message()` 是定义在 `auth/AuthSessionHandler.h` 中的一个虚函数**。因此,**必须为每种被支持的认证协议编写一个特定版本**。当前只支持 `UNKNOWN`、`NONE` 和 `CEPHX`,所以在以下三处各有一个 `sign_message()` 版本:
|
||
|
||
| 协议 | 位置 | 行为 |
|
||
|---|---|---|
|
||
| UNKNOWN | `auth/unknown/AuthUnknownSessionHandler.h` | **简单地返回 0,表示成功** |
|
||
| NONE | `auth/none/AuthNoneSessionHandler.h` | **简单地返回 0,表示成功** |
|
||
| CEPHX | `auth/cephx/CephxSessionHandler.cc` | 完整实现 |
|
||
|
||
**CEPHX 版本的 `sign_message()` 流程**:
|
||
|
||
1. **首先判断是否开启了处理签名的运行时选项(见上)。**
|
||
- 如果没有开启,**Cephx 版本只是返回成功,而不实际计算签名或把它插入消息**。
|
||
2. 如果开启了该选项:
|
||
- **把消息的所有 CRC(一个来自 header,三个来自 footer)拷贝到一个 buffer 中**;
|
||
- **对该 buffer 调用 `encode_encrypt()`**,使用从 pipe 的 `session_security` 结构中取得的 key;
|
||
- **把加密结果的 64 位放进消息 footer 的签名字段**,并**设置一个 footer 标志以表明该消息已被签名**;
|
||
- ⚠️ **官方说明:这个标志只是一个完整性检查(sanity check),它不被视为「该消息已被签名」的决定性证据。接收端存在 `session_security` 结构这一事实本身就要求必须有签名,无论该标志的值是什么。**;
|
||
- 如果一切顺利,**`sign_message()` 返回 0**;
|
||
- **如果过程中任何地方出问题、没有计算出签名,它返回 `SESSION_SIGNATURE_FAILURE`。**
|
||
|
||
#### 4.5.5 检查签名(Checking Signatures)
|
||
|
||
- 签名由一个名为 **`check_message_signature()`** 的例程检查。**它也是定义在 `auth/AuthSessionHandler.h` 中的一个虚函数**,因此同样有针对各被支持协议(UNKNOWN、NONE、CEPHX)的特定版本。UNKNOWN 与 NONE 版本同样只是返回 0。
|
||
|
||
**CEPHX 版本的 `check_message_signature()`**(位于 `auth/cephx/CephxSessionHandler.cc`)流程:
|
||
|
||
1. **如果运行时选项禁用了签名,则以成功退出。**
|
||
2. 否则,**取出 header 与 footer 中的 CRC,对结果加密,并与 footer 中存放的签名比较。**
|
||
- **由于更早的一个例程已经检查过 CRC 与消息内容确实匹配,因此没有必要在消息的原始数据上重新计算 CRC。**
|
||
3. 加密是用**与发送端相同的 `encode_encrypt()` 例程**执行的,**使用存放在本地 `session_security` 数据结构中的 key**。
|
||
4. **如果一切正常,返回 0(成功);如果有问题,返回 `SESSION_SIGNATURE_FAILURE`。**
|
||
|
||
#### 4.5.6 如何新增会话认证方法
|
||
|
||
**仅就会话认证而言**(不是指当前由 Cephx 协议执行的客户端与服务端的基本认证),**除了新增一个协议之外,该协议还必须有一个 `sign_message()` 例程和一个 `check_message_signature` 例程**。
|
||
|
||
- 这些例程**接受一个 message 指针作为参数,成功时返回 0**;
|
||
- 用于签名和检查的过程将特定于新方法,但**很可能会有一个附在 pipe 上、包含密码学密钥的 `session_security` 结构**;
|
||
- 该结构将是 **`AuthSessionHandler`**(位于 `auth/AuthSessionHandler.h`)**或从该类型派生的结构**。
|
||
|
||
#### 4.5.7 如何给会话增加加密
|
||
|
||
- 现有代码**部分(但未完全)**为会话数据包加密做好了准备。
|
||
- 增加加密的工作**一部分与增加新的认证方法类似**,但**还需要在 `write_message()` 和 `read_message()` 中增加对加密与解密例程的调用**。
|
||
- 这些调用的位置**大概会靠近当前认证调用所在的位置**。
|
||
- 官方建议:**应当考虑是否要「用某种更通用的东西」替换现有的调用,该通用东西做「所选会话安全形式所要求的任何事情」,而不是显式地说 `sign` 或 `encrypt`。**
|
||
|
||
#### 4.5.8 会话安全统计(Session Security Statistics)
|
||
|
||
- 现有的 Cephx 认证代码**保存统计信息**:① 多少消息被签名;② 多少消息签名被检查;③ 多少次检查成功与失败。
|
||
- 它也**准备好保存关于加密与解密的类似统计**。
|
||
- **这些统计可以通过调用 `auth/AuthSessionHandler.cc` 中的 `printAuthSessionHandlerStats` 来访问。**
|
||
- **如果新增了认证或加密方法,它们应当包含保存这些统计的代码。**
|
||
|
||
> 引自:[Session Authentication for the Cephx Protocol](https://docs.ceph.com/en/tentacle/dev/session_authentication/)
|
||
|
||
---
|
||
|
||
## 五、集群映射与元数据(mon 内部)
|
||
|
||
本章覆盖 `mon-on-disk-formats.rst`、`mon-bootstrap.rst`、`mon-elections.rst`、`mon-osdmap-prune.rst` 四页。
|
||
|
||
> 📌 说明:这四页**不在本册的指定输入清单中**,但它们确实存在于 `handbook-src/raw/dev/` 目录下。按本册大纲「若文件存在则讲」的要求,本章一并覆盖。
|
||
|
||
### 5.1 Monitor 磁盘格式(mon-on-disk-formats.rst)
|
||
|
||
#### 5.1.1 升级路径(UPGRADE PATH)
|
||
|
||
- **磁盘格式,甚至数据结构格式,都可能在一次升级期间被改变。**
|
||
- 希望这样做的服务,可以通过 **`PaxosService::upgrade_format()`** 调用路径来实现。
|
||
- **没有形式化的、统一的格式版本管理**:`PaxosService` 类通过一个 store 中的 key 来跟踪它的 **`format_version`**,**被假定为一个 `unsigned int`**,但**赋予这些版本以含义是该服务自己的责任**。
|
||
|
||
#### 5.1.2 AUTH MONITOR 的格式版本
|
||
|
||
版本用**单个 `unsigned int`** 表示。**默认情况下,值 `0` 表示「不存在正式的已升级格式」。** 第一个格式版本是在 Dumpling 中引入的;升级到 Dumpling 的集群会看到其格式版本从零升到一:
|
||
|
||
| 版本迁移 | 引入版本 |
|
||
|---|---|
|
||
| `0` → `1` | 引入于 **v0.65**(v0.67 dumpling 的开发版本) |
|
||
| `1` → `2` | 引入于 **v12.0.2**(luminous 的开发版本) |
|
||
| `2` → `3` | 引入于 **mimic** |
|
||
|
||
| 版本值 | 适用集群 |
|
||
|---|---|
|
||
| `0` | 所有 pre-dumpling 集群 |
|
||
| `1` | 所有 dumpling+ 且 pre-luminous 集群 |
|
||
| `2` | 所有 luminous+ 且 pre-mimic 集群 |
|
||
| `3` | 所有 mimic+ 集群 |
|
||
|
||
**各版本的内容**:
|
||
|
||
| 版本 | 引入的内容 |
|
||
|---|---|
|
||
| version 1 | 引入 new-style monitor caps(即 **profiles**) |
|
||
| version 2 | 引入 **mgr caps** 与 **bootstrap-mgr key** |
|
||
| version 3 | 如果所有 bootstrap 与 admin key 尚不存在,就**创建它们** |
|
||
|
||
#### 5.1.3 调用栈(callstack)
|
||
|
||
**`format_version` 在 `PaxosService::refresh()` 中被设置**:
|
||
|
||
```text
|
||
- 最初从 Monitor::refresh_from_paxos 调用
|
||
- 最初从 Monitor::init_paxos() 调用
|
||
- 最初从 Monitor::preinit() 调用
|
||
```
|
||
|
||
**`AuthMonitor::upgrade_format()` 由 `PaxosService::_active()` 调用**:
|
||
|
||
```text
|
||
- 从 C_Committed callback 调用,来自 PaxosService::propose_pending()
|
||
- 从 C_Active callback 调用,来自 PaxosService::_active()
|
||
- 从 PaxosService::election_finished() 调用
|
||
|
||
- 在一个全新部署的集群上,upgrade_format() 将**在 create_initial() 之后**首次被调用
|
||
- 在一个已存在的集群上,upgrade_format() 将在**第一次选举之后**被调用
|
||
|
||
- 在一个全新部署的集群上,upgrade_format() 是**无关紧要的**,
|
||
因为此时没有需要升级的格式
|
||
```
|
||
|
||
#### 5.1.4 升级逻辑(boil down)
|
||
|
||
**版本判定**:
|
||
|
||
```text
|
||
* 如果 format_version >= current_version,那么格式是最新的,返回
|
||
* 如果 features 不包含 LUMINOUS,则 current_version = 1
|
||
* 否则如果 features 不包含 MIMIC,则 current_version = 2
|
||
* 否则 current_version = 3
|
||
```
|
||
|
||
**如果 `format_version == 0`**:
|
||
|
||
```text
|
||
- 升级到 format version 1
|
||
- 迁移到 new-style monitor caps(即 profiles):
|
||
- 为已有的 entity 设置 daemon profiles
|
||
- 为已有的 bootstrap key 设置 profile
|
||
```
|
||
|
||
**如果 `format_version == 1`**:
|
||
|
||
```text
|
||
- 升级到 format version 2
|
||
- 对已有的 entity:
|
||
- 为 mgr 增加新的 cap
|
||
- 对已有的 'mgr' entity,修复因 kraken 的一个 bug 而被设为 'allow *' 的 'mon' caps,
|
||
改为设置 'allow profile mgr'
|
||
- 增加 bootstrap-mgr key
|
||
```
|
||
|
||
**如果 `format_version == 2`**:
|
||
|
||
```text
|
||
- 升级到 format version 3
|
||
- 如果所有 bootstrap key 当前不存在,就创建它们
|
||
```
|
||
|
||
> 💡 补充:官方这一页**只覆盖了 AUTH MONITOR 一个服务的磁盘格式**。官方在标题下只写了 `AUTH MONITOR` 一节,**`OSDMonitor`、`MDSMonitor`、`MonmapMonitor` 等其他 Paxos 服务的 on-disk 格式文档未明确**。
|
||
|
||
> 引自:[ON-DISK FORMAT](https://docs.ceph.com/en/tentacle/dev/mon-on-disk-formats/)
|
||
|
||
---
|
||
|
||
### 5.2 Monitor bootstrap(mon-bootstrap.rst)
|
||
|
||
#### 5.2.1 术语
|
||
|
||
| 术语 | 定义 |
|
||
|---|---|
|
||
| **cluster** | 一组 monitor |
|
||
| **quorum** | 一个活跃的 monitor 集合,**由该 cluster 的多数派构成** |
|
||
|
||
#### 5.2.2 初始化一个新 monitor 的必备输入
|
||
|
||
**必须始终被提供**:
|
||
|
||
1. 一个**逻辑名字(logical name)**;
|
||
2. **`mon.` 的 secret key**。注意它**不带 id 限定,因为所有 Monitor 共享同一个 secret**;
|
||
3. 一个 **cluster fsid (uuid)**。
|
||
|
||
**此外还需要知道两件事**:
|
||
|
||
1. 要绑定(bind)到什么地址;
|
||
2. 它的 peer 是谁(如果有的话)。
|
||
|
||
#### 5.2.3 逻辑 id
|
||
|
||
- 逻辑 id **应当在集群内唯一**。
|
||
- 它会被**附加到 `mon.` 之后**,以在 Ceph 集群中从逻辑上描述该 monitor。例如,如果逻辑 id 是 `foo`,该 monitor 的名字就是 **`mon.foo`**。
|
||
- **对大多数用户而言,每台主机不超过一个 monitor,因此短主机名(short hostname)是一个合乎逻辑的选择。**
|
||
|
||
#### 5.2.4 Secret keys
|
||
|
||
`mon.` 的 secret key 存放在 **`mon data` 目录**中的一个 **`keyring`** 文件里。可以用类似下面的命令生成:
|
||
|
||
```bash
|
||
ceph-authtool --create-keyring /path/to/keyring --gen-key -n mon.
|
||
```
|
||
|
||
**创建一个新的 monitor 集群时,该 keyring 还应当包含一个 `client.admin` key**,用于管理系统:
|
||
|
||
```bash
|
||
ceph-authtool /path/to/keyring --gen-key -n client.admin --cap mon 'allow *' --cap osd 'allow *' --cap mds 'allow'
|
||
```
|
||
|
||
生成的 keyring 通过命令行参数 **`--keyring <keyring>`** 喂给 **`ceph-mon --mkfs`**。
|
||
|
||
> ⚠️ **官方 Note**:Monitor keyring 文件是 **KeyServer 的一个 fallback store**,用于存放通过常规 `ceph auth` 命令套件管理的 key。因此,**`client.admin` key 可以被放在这个 keyring 里,但未来对 `client.admin` 凭证的一次轮转或更新可能会使它失效(obviate it)。**
|
||
|
||
#### 5.2.5 Cluster fsid
|
||
|
||
cluster fsid 是一个普通的 uuid,类似于 `uuidgen` 命令生成的那种。它有两种方式提供给 monitor:
|
||
|
||
1. 通过命令行参数 **`--fsid <uuid>`**(或配置文件选项);
|
||
2. 通过一个 monmap,经 **`--monmap <path>`** 命令行参数提供给新 monitor。
|
||
|
||
#### 5.2.6 Monitor 地址(4 种提供方式)
|
||
|
||
| # | 方式 |
|
||
|---|---|
|
||
| 1 | 通过命令行选项 **`--public-addr <ip[:port]>`**(或配置文件选项) |
|
||
| 2 | 通过命令行选项 **`--public-network <cidr>`**(或配置文件选项) |
|
||
| 3 | 通过 `--monmap <path>` 提供的 monmap(**如果它包含一个与我们的名字相同的 monitor**) |
|
||
| 4 | 通过 bootstrap monmap(经 **`--inject-monmap <path>`** 提供,或由 **`--mon-host <list>`** 生成)—— **如果它包含一个没有名字的 monitor(`noname-<something>`)以及一个配置在本地主机上的地址** |
|
||
|
||
#### 5.2.7 Peers(4 种提供方式)
|
||
|
||
| # | 方式 |
|
||
|---|---|
|
||
| 1 | 通过 initial monmap,经 **`--monmap <filename>`** 提供 |
|
||
| 2 | 通过由 **`--mon-host <list>`** 生成的 bootstrap monmap |
|
||
| 3 | 通过配置文件中 **`[mon.*]`** 节里**已弃用的 `mon addr` 选项**生成的 bootstrap monmap(⚠️ 官方明确说:**该方法不推荐使用,且不支持同时绑定 v1 与 v2 协议地址**) |
|
||
| 4 | **通过 admin socket 动态提供** |
|
||
|
||
> ⚠️ **官方强调:这些方法不能完全互换**,因为「在没有竞争(race)危险的情况下创建一个新的 monitor 集群」这件事本身很复杂。
|
||
|
||
#### 5.2.8 集群创建的三种基本方式
|
||
|
||
官方归纳为三种:
|
||
|
||
1. **提前指定 monitor 的名字与地址**来创建新集群;
|
||
2. **提前指定 monitor 的名字**,然后在 `ceph-mon` 守护进程自行配置时**动态设置地址**;
|
||
3. **提前指定 monitor 的地址**。
|
||
|
||
##### 方式 A:名字 + 地址(Names and addresses)
|
||
|
||
用 `monmaptool` 生成一个包含初始 monitor 名字与地址的 monmap。**生成的 monmap 也会包含一个 cluster fsid。** 把这个 monmap 喂给每个 monitor 守护进程:
|
||
|
||
```bash
|
||
ceph-mon --mkfs -i <name> --monmap <initial_monmap> --keyring <initial_keyring>
|
||
```
|
||
|
||
当守护进程启动时,它们会**确切地知道自己是谁、peer 是谁**。
|
||
|
||
##### 方式 B:只有地址(Addresses only)
|
||
|
||
初始 monitor 地址可以用 **`mon host`** 配置值指定,通过配置文件或命令行参数。这种方法的优点是:**集群的一个全局配置文件里可以有这样一行**:
|
||
|
||
```ini
|
||
mon host = a.foo.com, b.foo.com, c.foo.com
|
||
```
|
||
|
||
**并且它同时也用于告知任何 ceph 客户端或守护进程「monitor 是谁」。**
|
||
|
||
`ceph-mon` 守护进程还需要被喂给初始 keyring 与 cluster fsid 来初始化自己:
|
||
|
||
```bash
|
||
ceph-mon --mkfs -i <name> --fsid <uuid> --keyring <initial_keyring>
|
||
```
|
||
|
||
**当守护进程首次启动时,它们会互相分享自己的名字,并形成一个新的集群。**
|
||
|
||
##### 方式 C:只有名字(Names only)
|
||
|
||
**适用场景**:在动态的"云"环境中,集群创建者可能(还)不知道 monitor 的地址会是什么。相反,他们可能希望机器**并行地**自行配置和启动,并随着它们上线而**自行组成一个新集群**。
|
||
|
||
**问题在于**:monitor 集群依赖**严格多数派**来保持自身一致,而为了"创建"一个新集群,它需要知道**初始的 monitor 集合**是什么。
|
||
|
||
**解决办法**:使用 **`mon initial members`** 配置项,它应当列出**被允许创建该集群的初始 monitor 的 id**:
|
||
|
||
```ini
|
||
mon initial members = foo, bar, baz
|
||
```
|
||
|
||
这些 monitor 随后可以通过提供其余信息(keyring、cluster fsid、以及一种确定自己地址的方式)来初始化:
|
||
|
||
```bash
|
||
ceph-mon --mkfs -i <name> --mon-initial-hosts 'foo,bar,baz' --keyring <initial_keyring> --public-addr <ip>
|
||
```
|
||
|
||
**当这些守护进程启动时,它们会知道自己的地址,但不知道 peer 的地址。它们可以通过 admin socket 学到那些地址**:
|
||
|
||
```bash
|
||
ceph daemon mon.<id> add_bootstrap_peer_hint <peer ip>
|
||
```
|
||
|
||
**一旦它们从初始成员集合中学到了足够多的 peer,它们就能够创建该集群。**
|
||
|
||
#### 5.2.9 集群扩容(Cluster expansion)
|
||
|
||
**集群扩容的要求比集群创建略低**,因为「初始 quorum 的创建」不是问题,也无需担心创建出多个相互独立的集群。
|
||
|
||
**让新节点加入一个已存在集群的两种方式**:
|
||
|
||
1. **不提供任何初始 monitor peer 地址,然后动态喂给它们**;
|
||
2. **指定 `mon initial members` 配置项**以防止新节点形成一个新的、独立的集群,并**通过任何可用的方法喂给它们一些已有的 monitor**。
|
||
|
||
##### 最初无 peer 的扩容(Initially peerless expansion)
|
||
|
||
创建一个新的 monitor,**除了它自己的地址外不给它任何 peer 地址**:
|
||
|
||
```bash
|
||
ceph-mon --mkfs -i <myid> --fsid <fsid> --keyring <mon secret key> --public-addr <ip>
|
||
```
|
||
|
||
守护进程启动后,你可以给它一个或多个 peer 地址以加入集群(**最好是不带端口的裸 IP 地址;mon 会为你设置 addr types 与端口**):
|
||
|
||
```bash
|
||
ceph daemon mon.<id> add_bootstrap_peer_hint <peer ip>
|
||
```
|
||
|
||
或者,你可以显式地指定 `addrvec_t`:
|
||
|
||
```bash
|
||
ceph daemon mon.<id> add_bootstrap_peer_hintv <peer addrvec>
|
||
```
|
||
|
||
例如:
|
||
|
||
```bash
|
||
ceph daemon mon.new add_bootstrap_peer_hintv v2:1.2.3.4:3300,v1:1.2.3.4:6789
|
||
```
|
||
|
||
**这个 monitor 永远不会参与集群创建;它只能加入一个已存在的集群。**
|
||
|
||
> ⚠️ **注意**:指定的地址**应当与新 monitor 所绑定的地址完全匹配**。例如,如果新 mon 只绑定到 v2 地址,但提供了 v2 与 v1 两个地址,mon 之间就有可能产生某种混淆。
|
||
|
||
##### 带 initial members 的扩容(Expanding with initial members)
|
||
|
||
你可以一开始就喂给新 monitor 一些 peer 地址,并通过**同时设置 `mon initial members`** 来避免出问题:
|
||
|
||
```bash
|
||
ceph-mon --mkfs -i <myid> --fsid <fsid> --keyring <mon secret key> --public-addr <ip> --mon-host foo,bar,baz
|
||
```
|
||
|
||
守护进程启动时,**`mon initial members` 必须通过命令行或配置文件设置**:
|
||
|
||
```bash
|
||
ceph-mon -i <myid> --mon-initial-members foo,bar,baz
|
||
```
|
||
|
||
**以防止任何 split-brain(脑裂)的风险。**
|
||
|
||
> 引自:[Monitor bootstrap](https://docs.ceph.com/en/tentacle/dev/mon-bootstrap/)
|
||
|
||
---
|
||
|
||
### 5.3 Monitor 选举(mon-elections.rst)
|
||
|
||
#### 5.3.1 原始算法(The Original Algorithm)
|
||
|
||
**历史上,monitor leader 选举非常简单:排名最低(lowest-ranked)的 monitor 获胜!**
|
||
|
||
- 这是通过一个**低状态的 "Elector" 模块**完成的(**尽管它现在已被拆分成一个处理消息传递的 `Elector`,与一个做投票选择的 `ElectionLogic`**)。
|
||
- **它跟踪选举 epoch,别的几乎不跟踪。**
|
||
- **奇数 epoch 是选举;偶数 epoch 有一个 leader,并让 monitor 做它的日常工作。**
|
||
- **当发生超时,或 monitor 要求一次新选举时,我们递增 epoch,并向所有已知 monitor 发送 `Propose` 消息。**
|
||
|
||
**消息处理规则**:
|
||
|
||
| 情况 | 处理 |
|
||
|---|---|
|
||
| 收到一条**旧**消息 | 要么丢弃它,要么触发一次新选举(**如果我们认为发送方是新启动的、需要加入 quorum**) |
|
||
| 收到一条来自**更新 epoch** 的消息 | 把我们的 epoch 提升以匹配,然后要么 **Defer** 给该 Proposer,要么**再次递增 epoch 并 Propose 我们自己**(如果我们预期能赢过他们) |
|
||
| 在**当前 epoch** 内收到一条 `Propose` | 要么 **Defer** 给发送方,要么**忽略他们**(**如果他们的 rank 比我们高,或比我们已经 defer 给的那个 rank 更高,我们就忽略他们**) |
|
||
|
||
> ⚠️ **官方 Note**:**如果我们拥有最高的 rank,那么我们有可能在同一个选举 epoch 内依次 defer 给每一个其他 monitor!**
|
||
|
||
**收敛性**:在正常情况下这会收敛,因为**所有 monitor 对「优先级投票顺序」是一致的**,且**只有在一个 monitor 没有参与、或看到与已知 proposer 的可能冲突时,epoch 才会被递增**。
|
||
|
||
#### 5.3.2 原有算法的问题(The Problems)
|
||
|
||
**原始算法在多种网络分区(netsplit)条件下完全不工作。**
|
||
|
||
- 实践中这并不经常显现;
|
||
- 但随着社区与商业厂商**把 Ceph 带入需要使用 "stretch clusters"(延展集群)的场景**,它变得重要了。
|
||
|
||
#### 5.3.3 新算法(The New Algorithms)
|
||
|
||
- **我们仍然默认使用原始的("classic")选举算法**,但**支持让用户通过 CLI 切换到新的算法**。
|
||
- 这些算法**作为不同的函数与 switch 语句实现在 `ElectionLogic` 类中**。
|
||
|
||
| 算法 | 说明 |
|
||
|---|---|
|
||
| **disallow** | 非常简单:允许你把若干 monitor 加入一个「不允许成为 leader」的列表 |
|
||
| **connectivity** | 纳入**连接评分(connection score ratings)**,并**选出评分最高的 monitor** |
|
||
|
||
##### 算法:disallow
|
||
|
||
- **如果一个 monitor 在 disallowed 列表中,它总是 defer 给另一个 monitor,无论 rank 如何。**
|
||
- **其他方面与 classic 算法相同。**
|
||
- **由于修改 disallowed 列表需要一次 paxos 更新,一起参与选举的 monitor 应当总是拥有相同的集合。**
|
||
- 这意味着**选举顺序在整个 monitor 集合中是恒定且静态的,选举可以平凡地(trivially)收敛**(假定网络连通)。
|
||
- 官方说:**这个算法实际上只是作为一个演示、以及通往更高级的 connectivity 模式的垫脚石而存在的**,但**它在非对称网络与集群中可能有用**。
|
||
|
||
##### 算法:connectivity
|
||
|
||
- 该算法以**每条连接的评分(双向,见下一节)为输入**,并**尝试选出总评分最高的 monitor**。
|
||
- **我们保持与 classic 算法相同的基本消息传递流程**(选举由对 `Propose` 消息的反应驱动)。
|
||
- **但这带来了若干挑战,因为与 rank 不同,score 不是静态的(而且可能在一次选举期间发生变化!)**。
|
||
- **为了保证一个选举 epoch 不会产生多个 leader,我们必须维护两个关键不变量**:
|
||
|
||
| # | 不变量 |
|
||
|---|---|
|
||
| 1 | **Monitor 必须在一次选举 epoch 期间保持静态的 score** |
|
||
| 2 | **任何 deferral 必须是传递的** —— 如果 A defer 给 B,然后又 defer 给 C,**那么 B 最好也 defer 给 C**! |
|
||
|
||
**如何显式地处理这两点**:
|
||
|
||
1. **每当开始一次选举(或递增 epoch)时,就分支出(branch)我们 `peer_tracker` 评分对象的一个副本 `stable_peer_tracker`**;
|
||
2. **如果某个 monitor 不会被我们当前的 leader 选择所 defer 给,我们就拒绝对它 defer。**(**所有 `Propose` 消息都包含一份「该 leader 正在使用的 score」的副本,因此 peer 可以评估它们。**)
|
||
|
||
**为了保证前向进展(forward progress),还做了几处进一步调整**:
|
||
|
||
1. **如果我们想 defer 给一个新的 peer,但已经 defer 给了一个「其 score 不允许那样做」的 peer,我们就递增选举 epoch 并重新 `start()` 选举。**
|
||
2. **所有选举消息都包含发送方所知道的 score。**
|
||
|
||
**收敛性论证(官方原文)**:
|
||
|
||
> 这保证了只要网络 reasonably stable(即使断开了),我们就能解决选举:**只要所有 score "views" 产生相同的 deferral 顺序,一次选举就会正常完成。** 并且**通过在全部 monitor 之间广泛共享 score,monitor 会迅速收敛到全局最新的状态。**
|
||
|
||
**相比 classic 与 disallow handler 的一个更进一步的重要特性**:
|
||
|
||
> **它可以忽略 out-of-quorum 的 peer。**
|
||
>
|
||
> 通常,当 monitor B 收到来自一个 out-of-quorum peer C 的 `Propose` 时,B 会**自己触发一次新选举**,以给 C 一个加入的机会。
|
||
> **但由于最高评分的 monitor A 可能与 C 发生了 netsplit,这并不令人满意。**
|
||
> 因此**在 connectivity 选举算法中,B 只在其 score 表明「集群会选择一个 A 以外的 leader」时才"转发"(forward)`Propose` 消息。**
|
||
|
||
#### 5.3.4 连接评分(Connection Scoring)
|
||
|
||
- **评分实现在 `ConnectionTracker` 类中**,它由 `Elector` 驱动,并**作为一个资源提供给 `ElectionLogic`**。
|
||
- **`Elector` 负责发出 `MMonPing` 消息**,并负责把结果以「用相关的 peer 和该次调用所计的时间单位调用 `report_[live|dead]_connection`」的形式**报告进 `ConnectionTracker`**。
|
||
- **这些时间单位在 monitor 里是秒,但 `ConnectionTracker` 是不可知(agnostic)的,我们的单元测试只计简单的时间步数。**
|
||
|
||
**评分公式(官方原文)**:
|
||
|
||
```text
|
||
我们配置一个 "half life"(半衰期),每次报告都会更新该 peer 的当前状态
|
||
(alive 或 dead)及其总评分。
|
||
|
||
新的 score 是:
|
||
|
||
current_score * (1 - units_alive / (2 * half_life)) + (units_alive / (2 * half_life))
|
||
|
||
(对于一次 dead report,我们当然是**减去**新的 delta,而不是加上它。)
|
||
```
|
||
|
||
**其他能力**:
|
||
|
||
- 我们可以进一步**编码与解码 `ConnectionTracker`** 以进行线上传输;
|
||
- 可以 **`receive_peer_report()`** 一个完整的 `ConnectionTracker`(包含所有已知 score)或一个 `ConnectionReport`(代表单个 peer 的 score),以吸收来自 peer 的 score;
|
||
- **这些 score 当然都是带版本的,因此我们没有「意外地回到过去」的危险**;
|
||
- 我们可以查询:
|
||
- **单条连接的 score**(**如果该连接是 down 的,它就是 0**);
|
||
- **某个特定 monitor 的总 score** —— 即**从所有其他 monitor 进入该 monitor 的连接 score**。
|
||
|
||
**默认参数**:
|
||
|
||
| 参数 | 默认值 | 含义 |
|
||
|---|---|---|
|
||
| `mon_elector_ping_timeout` | 2 秒 | 超过该时间我们认为 ping 失败 |
|
||
| `mon_elector_ping_divisor` | 1 秒 | 每 1 秒 ping 一次存活的连接 |
|
||
| `mon_con_tracker_score_halflife` | 12 小时 | 半衰期 |
|
||
|
||
> 引自:[Monitor Elections](https://docs.ceph.com/en/tentacle/dev/mon-elections/)
|
||
|
||
---
|
||
|
||
### 5.4 全量 OSDMap 版本裁剪(mon-osdmap-prune.rst)
|
||
|
||
#### 5.4.1 问题背景
|
||
|
||
**对于每一个增量 osdmap epoch,monitor 都会在 store 中保存一个全量(full)osdmap epoch。**
|
||
|
||
| 优点 | 缺点 |
|
||
|---|---|
|
||
| 在响应客户端的 osdmap 请求时非常好 —— 使我们**无需从大量增量中重新计算**就能满足请求 | 一旦我们开始保存**无界数量**的 osdmap,它就会变成负担 |
|
||
|
||
**基本回收机制**:monitor 会尝试在 store 中**保存有界数量的 osdmap**。该数量由 **`mon_min_osdmap_epochs`** 定义并配置,**默认为 500 个 epoch**。一般而言,一旦超过这个限制,我们就会移除较老的 osdmap epoch。
|
||
|
||
**但移除 osdmap 有若干约束**(全部定义在 **`OSDMonitor::get_trim_to()`** 中):
|
||
|
||
- 如果其中一个条件不满足,我们**可能超过 `mon_min_osdmap_epochs` 所定义的边界**;
|
||
- 如果集群有一段时间不满足 trim 条件(例如存在 unclean pg),monitor 可能开始**保存大量 osdmap**;
|
||
- 这**会给底层 key/value store 以及可用磁盘空间带来压力**。
|
||
|
||
#### 5.4.2 被否决的替代方案与其问题
|
||
|
||
**方案:干脆不在磁盘上保存全量 osdmap epoch。**
|
||
|
||
- 那样我们就必须**按需重建 osdmap**,或者**如果它们最近被服务过就从缓存中抓取**;
|
||
- 我们**仍然必须至少保存一个 osdmap**,并在这个「保存在 store 中的最老 map epoch」或「从缓存中抓取的较新 map」之上应用所有增量;
|
||
- 官方评价:**虽然这是可行的,但似乎会有大量 CPU(以及潜在的 IO)被用于重建 osdmap**;
|
||
- 而且:**这只能防止未来出现上述问题,对当前已经处于「确实需要不保存 osdmap」状态的 store 毫无帮助。**
|
||
|
||
**这就引出了「全量 osdmap 裁剪(full osdmap pruning)」。**
|
||
|
||
#### 5.4.3 裁剪思想:打洞而不是全删
|
||
|
||
**我们不是不保存全量 osdmap epoch,而是在「数量过多」时裁剪掉其中一部分。**
|
||
|
||
- **判定「是否过多」由一个可配置选项 `mon_osdmap_full_prune_min`(默认:`10000`)决定。一旦超过这个阈值,裁剪算法就会被启动。**
|
||
- **但我们不会移除全部 `mon_osdmap_full_prune_min` 个全量 osdmap epoch。相反,我们要在全量 map 的序列中「打一些洞」。**
|
||
- **默认情况下,我们会「自上一个保留的 map 起,每 10 个 map 保留 1 个全量 map」** —— 即,如果我们保留 epoch 1,我们也会保留 epoch 10,并**移除全量 map epoch 2 到 9**。
|
||
- **这个间隔的大小可以通过 `mon_osdmap_full_prune_interval` 配置。**
|
||
|
||
**本质上,我们提议保留约 10% 的全量 map,但我们总是会遵守由 `mon_min_osdmap_epochs` 定义的最小 osdmap epoch 数量,且这些不会被计入「待裁剪的最小版本数」。**
|
||
|
||
**官方给出的两个具体例子**:
|
||
|
||
```text
|
||
例 1:磁盘上有版本 [1..50000]
|
||
→ 裁剪算法只被允许在 osdmap epoch [1..49500) 上操作
|
||
|
||
例 2:磁盘上有版本 [1..10200]
|
||
→ 我们不会裁剪,因为算法只会在版本 [1..9700) 上操作,
|
||
而这个区间包含的版本数少于 mon_osdmap_full_prune_min 所要求的最小数量
|
||
```
|
||
|
||
#### 5.4.4 裁剪算法(ALGORITHM)
|
||
|
||
**设定**:store 中有 50,000 个 osdmap epoch,且所有可配置选项均使用默认值。
|
||
|
||
```text
|
||
-----------------------------------------------------------
|
||
|1|2|..|10|11|..|100|..|1000|..|10000|10001|..|49999|50000|
|
||
-----------------------------------------------------------
|
||
^ first last ^
|
||
```
|
||
|
||
**裁剪的两个前置条件(必须全部满足)**:
|
||
|
||
| # | 条件 |
|
||
|---|---|
|
||
| 1 | 版本数量**大于** `mon_min_osdmap_epochs` |
|
||
| 2 | `first` 与 `prune_to` 之间的版本数量**大于或等于** `mon_osdmap_full_prune_min`,其中 **`prune_to` = `last` - `mon_min_osdmap_epochs`** |
|
||
|
||
**如果其中任何一个条件不满足,我们不会裁剪任何 map。**
|
||
|
||
**此外**:如果已知我们一直在裁剪,但此后我们**不再满足**上述至少一个条件,我们就**不会继续裁剪**。
|
||
**本质上:只有当 store 中的 epoch 数量确实需要时,我们才裁剪全量 osdmap。**
|
||
|
||
##### pinned maps 与 manifest
|
||
|
||
**由于裁剪会在全量 map 序列中制造空洞,我们需要跟踪「缺失 map 的区间」。我们的做法是:维护一份「被 pin 的 map」的 manifest —— 即,一份「因为被 pin 所以不应被裁剪」的 map 列表。**
|
||
|
||
- **被 pin 的 map 不会从 store 中移除,而两个相邻 pinned map 之间的 map 会被移除**;
|
||
- **被移除的 map 数量由可配置选项 `mon_osdmap_full_prune_interval` 决定**;
|
||
- **算法会尽力让 pinned map 之间相隔该选项所定义的那么多 map,但在极端情况下它可能允许更小的间隔**;
|
||
- **此外,由于这是一个「在每次裁剪迭代时都会读取」的可配置选项,如果用户改了这个配置项,这个间隔有可能发生变化。**
|
||
|
||
**Pin map 是惰性执行的:我们会在移除 map 的同时 pin map。** 这带来两个好处:
|
||
|
||
1. 在裁剪进行中给了我们**更大的灵活性去改变 prune interval**;
|
||
2. **大大简化了算法,以及我们需要在 manifest 中保存的信息。**
|
||
|
||
##### 简化版算法(官方原文)
|
||
|
||
```python
|
||
manifest.pin(first)
|
||
last_to_prune = last - mon_min_osdmap_epochs
|
||
|
||
while manifest.get_last_pinned() + prune_interval < last_to_prune AND \
|
||
last_to_prune - first > mon_min_osdmap_epochs AND \
|
||
last_to_prune - first > mon_osdmap_full_prune_min AND \
|
||
num_pruned < mon_osdmap_full_prune_txsize:
|
||
|
||
last_pinned = manifest.get_last_pinned()
|
||
new_pinned = last_pinned + prune_interval
|
||
manifest.pin(new_pinned)
|
||
for e in (last_pinned .. new_pinned):
|
||
store.erase(e)
|
||
++num_pruned
|
||
```
|
||
|
||
**算法要点(官方解释)**:
|
||
|
||
- **该算法本质上确保「store 中的第一个版本」总是被 pin。** 毕竟,**我们在重建 map 时需要一个起点,且我们不能简单地移除我们所拥有的最早的那个 map**;否则我们将无法为**第一个**被裁剪的区间重建 map。
|
||
- **一旦我们至少有一个 pinned map,算法的每一次迭代都可以简单地基于 manifest 的「最后一个被 pin 的 map」来进行**(我们可以通过读取 manifest 的 pinned maps 列表的尾元素来获得它)。
|
||
- 接下来我们需要确定**待移除 map 的区间**:从 `last_pinned` 到 `new_pinned` 的所有 map,而 `new_pinned` 又不过是 `last_pinned` 加上 `mon_osdmap_full_prune_interval`。我们知道这两个值之间的所有 map(即 `last_pinned` 与 `new_pinned` 之间)都可以被移除,因为 `new_pinned` 已经被 pin 了。
|
||
|
||
##### 算法的停止条件
|
||
|
||
**一旦两个初始前置条件之一不满足,算法就停止执行;或者,如果我们不满足另外两个「对算法正确性没有影响」的条件,也停止**:
|
||
|
||
| # | 停止条件 | 理由 |
|
||
|---|---|---|
|
||
| 1 | 我们**无法创建一个与 `mon_osdmap_full_prune_interval` 正确对齐、且低于 `last_pruned` 的新裁剪区间**时就停止 | **并没有特别的技术原因**要求这一点,除了:① 让我们保持区间具有预期的大小;② 防止小的、不规则的区间(它们最终必定会出现,例如裁剪在多次迭代中持续进行,每次只移除一两个或三个 map) |
|
||
| 2 | 一旦我们知道**我们已经裁剪了超过一定数量的 map** 就停止。该值由 **`mon_osdmap_full_prune_txsize`** 定义,确保我们**不会花无界的时间周期去裁剪 map** | 我们**并不严格执行这个值**(删除操作开销不大),但**会尽力遵守它** |
|
||
|
||
> **为什么分批?** 官方解释:**我们本可以一次性完成移除,但我们不知道那要花多长时间。因此我们会执行多次迭代,每次最多移除 `mon_osdmap_full_prune_txsize` 个 osdmap。**
|
||
|
||
##### 裁剪后的磁盘形态
|
||
|
||
```text
|
||
------------------------------------------
|
||
|1|10|20|30|..|49500|49501|..|49999|50000|
|
||
------------------------------------------
|
||
^ first last ^
|
||
```
|
||
|
||
#### 5.4.5 `osdmap_manifest_t` 数据结构
|
||
|
||
由于我们不是一次性裁剪完所有版本,**我们需要保存「我们裁剪到什么程度了」的状态**。为此创建了一个数据结构 **`osdmap_manifest_t`**,它保存被 pin 的 map 集合:
|
||
|
||
```cpp
|
||
struct osdmap_manifest_t:
|
||
set<version_t> pinned;
|
||
```
|
||
|
||
**关键推论**:**由于我们只在裁剪时 pin map,我们不需要保存关于「最后被裁剪的版本」的额外状态。** 我们**事实上**知道:**任意两个相邻 pinned map 之间的所有中间 map 都已被裁剪。**
|
||
|
||
##### 崩溃安全性
|
||
|
||
> **问题:如果 monitor 崩溃了,我们如何能确定已经裁剪了所有中间 map?**
|
||
|
||
**解决**:**我们总是在「删除 map 的同一个事务」中把 osdmap manifest 写入磁盘。** 这样我们就有了保证:**如果 monitor 崩溃,我们会读到 manifest 的最新版本** —— 要么包含新 pin 的 map(意味着我们也裁剪了其间的 map);要么我们会找到 manifest 的先前版本,它**不包含**我们在崩溃时正在 pin 的那些 map,因为「我们本会写入更新后的 osdmap manifest」的那个事务没有连同 map 移除一起被应用。
|
||
|
||
**其他规则**:
|
||
|
||
- **osdmap manifest 在每次我们裁剪时被写入 store**,带有更新后的 pinned map 列表;
|
||
- **它被写在「实际裁剪 map」的那个事务中**,因此我们保证 manifest 总是最新的;
|
||
- 作为该标准的**一个推论**:**我们第一次写入 osdmap manifest 就是我们第一次裁剪的时候。如果一个 osdmap manifest 不存在,我们可以确定我们没有持有已裁剪的 map 区间。**
|
||
- 我们将**依赖该 manifest 来确定我们是否有已裁剪的 map 区间**。理论上这总是磁盘上的 osdmap manifest,但**我们确保每次从 paxos 更新时都读取磁盘上的 osdmap manifest**,这样我们总能确保拥有一个最新的内存中的 osdmap manifest。
|
||
- **一旦我们完成裁剪 map,我们会把 manifest 保留在 store 中**,以便我们轻松找到哪些 map 被 pin 了(而不是去检查 store 直到找到一个 map)。**这还有个额外的好处:让我们能快速算出下一个需要裁剪的区间(即 last pinned 加上 prune interval)。**
|
||
- **但这并不意味着我们会永远保留 osdmap manifest**:**一旦 monitor trim 了 osdmap,且 store 中最早可用的 epoch 大于我们最后裁剪的那个 map,osdmap manifest 就不再需要了。**
|
||
|
||
#### 5.4.6 Map trimming(与裁剪的交互)
|
||
|
||
**如果 monitor trim 了 map,我们就必须调整 osdmap manifest 以反映我们的裁剪状态,或者在该 manifest 不再有意义时把它整个移除。**
|
||
|
||
##### 场景 1:trim 越过了最后一个 pruned epoch
|
||
|
||
假设我们没有完成全部裁剪,磁盘形态是:
|
||
|
||
```text
|
||
-------------------------------------------------------------
|
||
|1|10|20|30|..|490|500|501|502|..|49500|49501|..|49999|50000|
|
||
-------------------------------------------------------------
|
||
^ first ^ pinned.last() last ^
|
||
|
||
pinned = {1, 10, 20, ..., 490, 500}
|
||
```
|
||
|
||
现在假设 monitor 要 **trim 到 epoch 501**。这意味着**移除 epoch 501 之前的所有 map**,并把 `first_committed` 指针更新为 `501`。
|
||
|
||
**由于移除所有这些 map 会使我们现有的裁剪成果失效,我们可以认为裁剪已经完成,并丢弃我们的 osdmap manifest。** 这样做**也简化了「如果所有起始条件在我们从 store 刷新状态后仍被满足时,重新开始一次新的裁剪」**。
|
||
|
||
结果:
|
||
|
||
```text
|
||
---------------------------------------
|
||
|501|502|..|49500|49501|..|49999|50000|
|
||
---------------------------------------
|
||
^ first last ^
|
||
```
|
||
|
||
##### 场景 2:trim 落在 pruned 区间内部
|
||
|
||
现在设想一个稍微更复杂的场景:**monitor 要 trim 到 epoch 491,而 epoch 491 先前已从 store 中被裁剪掉了。**
|
||
|
||
**由于我们总是需要在 store 中拥有最老的已知 map,在 trim 之前我们必须检查「那个 map 是否处于 prune 区间内」**(即,该 map epoch 是否属于 `[ pinned.first()..pinned.last() )`)。
|
||
|
||
| 情况 | 处理 |
|
||
|---|---|
|
||
| **它是一个 pinned map** | 除了**从 manifest 的 pinned 列表中移除更低的 epoch** 之外,我们没什么好担心的 |
|
||
| **被 trim 到的那个 map 不是 pinned map** | 我们需要**重建该 map 并 pin 它**,然后**才移除早于该 map epoch 的 pinned map** |
|
||
|
||
结果:
|
||
|
||
```text
|
||
-----------------------------------------------
|
||
|491|500|501|502|..|49500|49501|..|49999|50000|
|
||
-----------------------------------------------
|
||
^ ^- pinned.last() last ^
|
||
`- first
|
||
```
|
||
|
||
##### 场景 3:trim 到「最后一个被裁剪的 epoch」(边界情况)
|
||
|
||
考虑我们要 **trim 到 epoch 499,而它正是最后那个被裁剪的 epoch**。
|
||
|
||
- 与上面的场景非常像,我们最终会把 osdmap epoch 499 写入 store;
|
||
- **但我们应该如何处理 pinned map 与裁剪?**
|
||
|
||
**最简单的解决方案是:丢弃 osdmap manifest。** 官方理由:
|
||
|
||
> 既然我们正在 trim 到最后一个被裁剪的 map,且我们正在重建这个 map,我们就可以**保证所有大于 499 的 map 都是连续的**(因为我们没有裁剪它们中的任何一个)。
|
||
> **本质上,在这种情况下丢弃 osdmap manifest,与「trim 越过最后一个被裁剪的 epoch」是完全相同的**:如果满足了所需条件,我们稍后可以再裁剪。
|
||
|
||
#### 5.4.7 配置项的健全性检查(CONFIGURATION OPTIONS SANITY CHECKS)
|
||
|
||
**我们在裁剪之前执行额外检查,以确保所有相关配置选项都是合理的**:
|
||
|
||
| # | 检查 | 理由 |
|
||
|---|---|---|
|
||
| 1 | **如果 `mon_osdmap_full_prune_interval` 为零,我们不裁剪** | 我们需要一个**实际的、大于 1 的正数**才能裁剪 map。**如果 interval 是 1,我们实际上不会裁剪任何 map**,因为 pinned map 之间的间隔本质上就是一个 epoch,这意味着 pinned map 之间有**零个** map,因此永远不会有 map 被裁剪 |
|
||
| 2 | **如果 `mon_osdmap_full_prune_min` 为零,我们不裁剪** | 我们需要一个**大于零的正值**,以便知道「超过哪个阈值我们就应当裁剪」。**我们不想去猜** |
|
||
| 3 | **如果 `mon_osdmap_full_prune_interval` 大于 `mon_osdmap_full_prune_min`,我们不裁剪** | 因为**无法确定一个合适的裁剪区间** |
|
||
| 4 | **如果 `mon_osdmap_full_prune_txsize` 小于 `mon_osdmap_full_prune_interval`,我们不裁剪** | 我们需要一个 **txsize**,其值**至少等于 interval**,且(取决于后者的值)理想上更高 |
|
||
|
||
#### 5.4.8 需求、条件与不变量(REQUIREMENTS, CONDITIONS & INVARIANTS)
|
||
|
||
**REQUIREMENTS(需求)**:
|
||
|
||
- **quorum 中的所有 monitor 都需要支持裁剪。**
|
||
- **一旦裁剪被启用,不支持裁剪的 monitor 将不被允许进入 quorum,也不被允许同步。**
|
||
- **移除 osdmap manifest 会导致「裁剪特性」的 quorum 要求被禁用。** 这意味着不支持裁剪的 monitor **将被允许同步并加入 quorum**,前提是它们支持所需的任何其他特性。
|
||
|
||
**CONDITIONS & INVARIANTS**:
|
||
|
||
| 场景 | 条件 / 不变量 |
|
||
|---|---|
|
||
| **从未发生过裁剪,或我们已 trim 越过其先前的区间** | invariant: `first_committed > 1`<br>condition: `pinned.empty() AND !store.exists(manifest)` |
|
||
| **至少发生过一次裁剪** | invariant: `first_committed > 0`<br>invariant: `!pinned.empty()`<br>invariant: `pinned.first() == first_committed`<br>invariant: `pinned.last() < last_committed`<br><br>precond: `pinned.last() < prune_to AND pinned.last() + prune_interval < prune_to`<br><br>postcond: `pinned.size() > old_pinned.size() AND`<br>`(for each v in [pinned.first()..pinned.last()]:`<br>` if pinned.count(v) > 0: store.exists_full(v)`<br>` else: !store.exists_full(v))` |
|
||
| **裁剪已完成** | invariant: `first_committed > 0`<br>invariant: `!pinned.empty()`<br>invariant: `pinned.first() == first_committed`<br>invariant: `pinned.last() < last_committed`<br><br>condition: `pinned.last() == prune_to OR pinned.last() + prune_interval < prune_to` |
|
||
| **裁剪区间可以被 trim** | precond: `OSDMonitor::get_trim_to() > 0`<br><br>condition: `!pinned.empty()`<br><br>invariant: `pinned.first() == first_committed`<br>invariant: `pinned.last() < last_committed`<br>invariant: `pinned.first() <= OSDMonitor::get_trim_to()`<br>invariant: `pinned.last() >= OSDMonitor::get_trim_to()` |
|
||
| **Trim 已裁剪的区间** | invariant: `!pinned.empty()`<br>invariant: `pinned.first() == first_committed`<br>invariant: `pinned.last() < last_committed`<br>invariant: `pinned.first() <= OSDMonitor::get_trim_to()`<br>invariant: `pinned.last() >= OSDMonitor::get_trim_to()`<br><br>postcond: `pinned.empty() OR`<br>`(pinned.first() == OSDMonitor::get_trim_to() AND`<br>` pinned.last() > pinned.first() AND`<br>` (for each v in [0..pinned.first()]: !store.exists(v) AND !store.exists_full(v)) AND`<br>` (for each m in [pinned.first()..pinned.last()]:`<br>` if pinned.count(m) > 0: store.exists_full(m)`<br>` else: !store.exists_full(m) AND store.exists(m)))`<br><br>postcond: `!pinned.empty() OR`<br>`(!store.exists(manifest) AND`<br>` (for each v in [pinned.first()..pinned.last()]:`<br>` !store.exists(v) AND !store.exists_full(v)))` |
|
||
|
||
> 引自:[FULL OSDMAP VERSION PRUNING](https://docs.ceph.com/en/tentacle/dev/mon-osdmap-prune/)
|
||
|
||
---
|
||
|
||
## 六、数据路径与特性
|
||
|
||
本章覆盖 5 个页面:`file-striping.rst`、`erasure-coded-pool.rst`、`deduplication.rst`、`delayed-delete.rst`、`crush-msr.rst`。它们分别回答「文件怎么切成对象」「纠删码池怎么用」「重删怎么做」「删除为什么是延迟的」「CRUSH 怎么支持多步重试」五个问题。
|
||
|
||
### 6.1 `file-striping.rst`:文件条带化布局
|
||
|
||
> 官方该页共 161 行,主题单一——只讲 CephFS 客户端的文件数据如何映射到 RADOS 对象,不涉及 RBD、不涉及 OSD 侧实现。
|
||
|
||
#### 6.1.1 设计动机
|
||
|
||
CephFS 客户端的一个「文件」在 RADOS 里并不是一个对象。文件数据会被**条带化(striping)**分布到若干底层对象上,这样做有两个目的:
|
||
|
||
1. 突破单对象的吞吐与并行度上限(一个文件可以并行写多个对象、落在多个 OSD)。
|
||
2. 让超大文件不至于产生超大对象(对象大小有上限 `fl_object_size`)。
|
||
|
||
官方把这种分布定义为**改良版 RAID 0**:数据在一组对象上按条带轮转,写满每个对象的固定大小后,换下一组对象继续。
|
||
|
||
#### 6.1.2 关键术语(官方文档原文定义)
|
||
|
||
| 术语 | 官方定义 | 说明 |
|
||
|---|---|---|
|
||
| file | 从 Ceph 客户端视角命名的连续数据集合(即 Linux 上使用 Ceph 存储的一个文件) | 文件数据被切分为固定大小的 stripe unit,存进 Ceph object |
|
||
| stripe unit | 用于 RAID 0 分布的数据块大小(字节)。同一文件所有 stripe unit 大小相等 | **最后一个 stripe unit 通常是不完整的**——它包含文件末尾数据,以及到 stripe unit 边界为止的未使用「空间」 |
|
||
| stripe count | 构成一个 RAID 0「stripe」的**连续 stripe unit 数量** | 也就是一个 stripe 横跨多少个对象 |
|
||
| stripe | 一段连续的文件数据,以固定大小 stripe unit 为单位,RAID 0 条带化分布在 stripe count 个对象上 | — |
|
||
| object | Ceph 存储维护的数据集合 | 用来存放 Ceph 客户端文件的一部分 |
|
||
| object set | 一组对象,合起来表示文件的一个连续片段 | 每个 object set 里的对象数量 = stripe count |
|
||
|
||
#### 6.1.3 核心数据结构:`ceph_file_layout`
|
||
|
||
由三个字段定义整个映射关系(**磁盘上的实际格式是 `__le32`**):
|
||
|
||
```cpp
|
||
u32 fl_stripe_unit; // 条带单元大小(字节)
|
||
u32 fl_stripe_count; // 每个 stripe 的 stripe unit 数(= 每个 object set 的对象数)
|
||
u32 fl_object_size; // 承载文件数据的对象的最大大小(字节)
|
||
```
|
||
|
||
字段约束:
|
||
|
||
- `fl_object_size` 是 `fl_stripe_unit` 的**整数倍**。
|
||
- 文件数据先切成 stripe unit,连续的 stripe unit 依次存进 object set 中的对象。
|
||
- 存文件数据的对象**不会超过 `fl_object_size`**,所以写满若干个完整 stripe 后,会启用新的 object set。
|
||
|
||
#### 6.1.4 布局算法(按官方叙述整理)
|
||
|
||
> 💡 补充:官方该页**没有**给出伪代码或公式,只给了文字描述和一个具体数值例子。下面第 1~5 步是按官方文字与例子反推出的读法,供理解用,官方未以公式形式给出。
|
||
|
||
1. 计算 `stripe size = fl_stripe_unit × fl_stripe_count`(一个 stripe 覆盖的字节数)。
|
||
2. 计算每个对象可容纳的 stripe unit 数:`fl_object_size / fl_stripe_unit`。
|
||
3. 计算一个 object set 的容量:`fl_object_size × fl_stripe_count`(也等于 step2 的结果 × stripe size)。
|
||
4. 对给定文件偏移,依次分解为:第几个 object set → 该 set 内第几个 stripe → 该 stripe 内第几个 stripe unit → unit 内字节偏移。
|
||
5. 落在最后一个 stripe unit 的数据可能不足一个 unit,剩余部分是空洞(不占存储)。
|
||
|
||
**默认策略**:官方明确指出「by default, Ceph uses a simple striping strategy in which object_size equals stripe_unit and stripe_count is 1」——即每个对象只放一个 stripe unit,等价于不条带化(一个大文件顺序落在 N 个对象上,每个对象 4MB)。
|
||
|
||
#### 6.1.5 官方给出的完整数值例子
|
||
|
||
输入:
|
||
|
||
```text
|
||
file size = 1 trillion = 1000000000000 bytes
|
||
|
||
fl_stripe_unit = 64KB = 65536 bytes
|
||
fl_stripe_count = 5 (stripe units per stripe)
|
||
fl_object_size = 64GB = 68719476736 bytes
|
||
```
|
||
|
||
推导:
|
||
|
||
```text
|
||
file stripe size = 64KB * 5 = 320KB = 327680 bytes
|
||
each object holds = 64GB / 64KB = 1048576 stripe units
|
||
file object set size = 64GB * 5 = 320GB = 343597383680 bytes
|
||
(also 1048576 stripe units * 327680 bytes per stripe unit)
|
||
```
|
||
|
||
把 1 万亿字节逐级分解:
|
||
|
||
```text
|
||
- 1 trillion bytes / 320GB per object set = 2 complete object sets
|
||
(with 312805232640 bytes remaining)
|
||
- 312805232640 bytes / 320KB per stripe = 954605 complete stripes
|
||
(with 266240 bytes remaining)
|
||
- 266240 bytes / 64KB per stripe unit = 4 complete stripe units
|
||
(with 4096 bytes remaining)
|
||
- and the final incomplete stripe unit holds those 4096 bytes.
|
||
```
|
||
|
||
即:2 个完整 object set(= 10 个对象)→ 954605 个完整 stripe → 4 个完整 stripe unit → 最后 1 个只装了 4096 字节的不完整 stripe unit。
|
||
|
||
#### 6.1.6 官方 ASCII 结构图(摘录)
|
||
|
||
官方原文用 ASCII 画出三个 object set(object 0~4 / object 5~9 / object 10~14),每个对象纵向堆叠 1048576 个 stripe unit。下面摘录最后一个 object set 的尾部,用来说明「不完整 stripe unit」:
|
||
|
||
```text
|
||
_________ _________ _________ _________ _________
|
||
/object 10\ /object 11\ /object 12\ /object 13\ /object 14\
|
||
+=========+ +=========+ +=========+ +=========+ +=========+
|
||
| stripe | | stripe | | stripe | | stripe | | stripe | stripe
|
||
o | unit | | unit | | unit | | unit | | unit | 2097152
|
||
b | 10485760| | 10485761| | 10485762| | 10485763| | 10485764|
|
||
j |---------| |---------| |---------| |---------| |---------|
|
||
e | stripe | | stripe | | stripe | | stripe | | stripe | stripe
|
||
c | unit | | unit | | unit | | unit | | unit | 2097153
|
||
t | 10485765| | 10485766| | 10485767| | 10485768| | 10485769|
|
||
|---------| |---------| |---------| |---------| |---------|
|
||
s | . | | . | | . | | . | | . |
|
||
e . . . . .
|
||
t | . | | . | | . | | . | | . |
|
||
|---------| |---------| |---------| |---------| |---------|
|
||
2 | stripe | | stripe | | stripe | | stripe | | stripe | stripe
|
||
| unit | | unit | | unit | | unit | | unit | 3051756
|
||
| 15258780| | 15258781| | 15258782| | 15258783| | 15258784|
|
||
|---------| |---------| |---------| |---------| |---------|
|
||
| stripe | | stripe | | stripe | | stripe | | (partial| (partial
|
||
| unit | | unit | | unit | | unit | | stripe | stripe
|
||
| 15258785| | 15258786| | 15258787| | 15258788| | unit) | 3051757)
|
||
\=========/ \=========/ \=========/ \=========/ \=========/
|
||
```
|
||
|
||
图中 object 10~14 构成 object set 2;最后一个 stripe(编号 3051757)只有前 4 个对象有完整 stripe unit,object 14 上是一个 `(partial stripe unit)`。
|
||
|
||
#### 6.1.7 注意事项
|
||
|
||
- 该页**只**描述 CephFS 客户端文件的存储方式,官方原文第一句即限定:「how files from Ceph file system clients are stored across objects stored in RADOS」。RBD 的条带化是另一套(在 librbd 层),本页未涉及。
|
||
- 条带参数(stripe unit / count / object size)是 **per-file** 的,写在文件的 layout 里,不是全局配置。
|
||
- 修改已有文件的 layout 不会重排已有数据(**文档未明确**,本页未讨论 layout 变更语义)。
|
||
|
||
> 引自:[File striping](https://docs.ceph.com/en/tentacle/dev/file-striping/)
|
||
|
||
---
|
||
|
||
### 6.2 `erasure-coded-pool.rst`:纠删码池
|
||
|
||
> 官方该页共 135 行,且定位是「使用说明」而非「实现说明」——它讲 EC 池的用途、场景和 `ceph osd erasure-code-profile` / `ceph osd pool create` 的用法,**几乎不讲内部实现**(没有编解码流程、没有 `ErasureCodeInterface`、没有 `ECBackend`)。想要实现细节需看 `osd_internals/` 下的页面(不在本册范围)。
|
||
|
||
#### 6.2.1 设计动机(官方 Purpose 段)
|
||
|
||
官方用两句话概括 EC 池的取舍:
|
||
|
||
| 维度 | 与副本池对比 |
|
||
|---|---|
|
||
| 存储空间 | EC 池**需要更少的存储空间** |
|
||
| 计算开销 | EC 的**计算要求更高**(编解码要 CPU) |
|
||
| 支持的操作 | **只支持对象操作的一个子集**——官方举例:不支持部分写(partial write) |
|
||
|
||
这三点决定了 EC 池的适用面:省空间,但别用在需要随机小写、需要 omap、需要 RBD 的热数据上。
|
||
|
||
#### 6.2.2 两个官方用例
|
||
|
||
**用例一:冷存储(Cold storage)**
|
||
|
||
官方描述的一套完整架构:
|
||
|
||
1. 建一个 EC 池存放大量 1GB 对象(影像、基因组等),每月约 10% 被读取;每天新增对象,**写入后不再修改**;平均 1 次写对 10000 次读。
|
||
2. 建一个**副本池**作为该 EC 池的 **cache tier**。
|
||
3. 一个 agent 负责 **demote**(降级/下沉):把一周未被访问的对象从副本池移到 EC 池。
|
||
4. EC 池的 CRUSH rule 指向为冷存储设计的硬件——高延迟、慢访问;副本池的 CRUSH rule 指向更快的硬件以提供更好的响应时间。
|
||
|
||
**用例二:廉价多数据中心存储(Cheap multidatacenter storage)**
|
||
|
||
- 10 个数据中心通过专线互联,每个数据中心存储容量相同,**无 UPS、无空调**。
|
||
- 建一个 EC 池,CRUSH rule 保证**最多 3 个数据中心同时故障时数据不丢**。
|
||
- 用 `k=6, m=3`(数据分 6 份 + 3 个校验块),**开销 50%**。
|
||
- 对比:用副本达到同样容错需要 4 副本,**开销 400%**。
|
||
|
||
> 💡 补充:4 副本对应「容忍 3 个 DC 同时故障」的开销是 (4-1)/1 = 300% 额外,官方写 400% 应是按总量/原始量的口径(4 倍)表述,原文为 "With replication the overhead would be 400% (four replicas)"。此处按原文直译,未做换算。
|
||
|
||
#### 6.2.3 关键操作与命令(官方 Interface 段全量)
|
||
|
||
**1. 创建默认 EC 池:**
|
||
|
||
```bash
|
||
$ ceph osd pool create ecpool erasure
|
||
```
|
||
|
||
**2. 创建 EC 池并指定 CRUSH rule `ecrule`:**
|
||
|
||
```bash
|
||
$ ceph osd crush rule create-erasure ecrule
|
||
$ ceph osd pool create ecpool erasure default ecrule
|
||
```
|
||
|
||
**3. 把 CRUSH failure domain 改成 osd(默认是 host):**
|
||
|
||
```bash
|
||
$ ceph osd erasure-code-profile set myprofile \
|
||
crush-failure-domain=osd
|
||
$ ceph osd erasure-code-profile get myprofile
|
||
k=2
|
||
m=2
|
||
plugin=isa
|
||
technique=reed_sol_van
|
||
crush-failure-domain=osd
|
||
$ ceph osd pool create ecpool erasure myprofile
|
||
```
|
||
|
||
**4. 控制 EC 插件参数(k / m):**
|
||
|
||
```bash
|
||
$ ceph osd erasure-code-profile set myprofile \
|
||
k=3 m=2
|
||
$ ceph osd erasure-code-profile get myprofile
|
||
k=3
|
||
m=2
|
||
plugin=isa
|
||
technique=reed_sol_van
|
||
$ ceph osd pool create ecpool erasure myprofile
|
||
```
|
||
|
||
**5. 选择其他 EC 插件:**
|
||
|
||
```bash
|
||
$ ceph osd erasure-code-profile set myprofile \
|
||
plugin=example technique=xor
|
||
$ ceph osd erasure-code-profile get myprofile
|
||
k=2
|
||
m=2
|
||
plugin=example
|
||
technique=xor
|
||
$ ceph osd pool create ecpool 12 12 erasure \
|
||
myprofile
|
||
```
|
||
|
||
**6. 查看默认 profile:**
|
||
|
||
```bash
|
||
$ ceph osd erasure-code-profile ls
|
||
default
|
||
$ ceph osd erasure-code-profile get default
|
||
k=2
|
||
m=2
|
||
plugin=isa
|
||
technique=reed_sol_van
|
||
```
|
||
|
||
即 Tentacle 默认 EC profile 为 `k=2, m=2, plugin=isa, technique=reed_sol_van`。
|
||
|
||
**7. 自定义 k/m(k+m=6,容忍 3 个 OSD 失效):**
|
||
|
||
```bash
|
||
$ ceph osd erasure-code-profile set myprofile k=3 m=3
|
||
$ ceph osd erasure-code-profile get myprofile
|
||
k=3
|
||
m=3
|
||
plugin=isa
|
||
technique=reed_sol_van
|
||
$ ceph osd erasure-code-profile ls
|
||
default
|
||
myprofile
|
||
```
|
||
|
||
**8. 删除不再使用的 profile**(若仍被池引用会失败,报 `EBUSY`):
|
||
|
||
```bash
|
||
$ ceph osd erasure-code-profile ls
|
||
default
|
||
myprofile
|
||
$ ceph osd erasure-code-profile rm myprofile
|
||
$ ceph osd erasure-code-profile ls
|
||
default
|
||
```
|
||
|
||
**9. 把 rule 指向 ssd(默认是 default root):**
|
||
|
||
```bash
|
||
$ ceph osd erasure-code-profile set myprofile \
|
||
crush-root=ssd
|
||
$ ceph osd erasure-code-profile get myprofile
|
||
k=2
|
||
m=2
|
||
plugin=isa
|
||
technique=reed_sol_van
|
||
crush-root=ssd
|
||
```
|
||
|
||
#### 6.2.4 profile 参数一览(从官方示例归纳)
|
||
|
||
| 参数 | 官方示例中出现的值 | 含义(官方说明) |
|
||
|---|---|---|
|
||
| `k` | 2(默认)/ 3 / 6 | 数据块数量 |
|
||
| `m` | 2(默认)/ 3 | 编码(校验)块数量 |
|
||
| `plugin` | `isa`(默认)/ `example` | 纠删码插件 |
|
||
| `technique` | `reed_sol_van`(默认)/ `xor` | 插件内的具体算法 |
|
||
| `crush-failure-domain` | `osd`(默认 `host`) | CRUSH 故障域 |
|
||
| `crush-root` | `ssd`(默认 `default`) | CRUSH root |
|
||
|
||
#### 6.2.5 注意事项
|
||
|
||
- **不支持部分写**(partial write):官方在 Purpose 段明确点出。因此 EC 池不能用于 RBD(RBD 依赖部分写),也不能用于 CephFS 的 data pool(除非用 `–allow-ec-overwrites`,**该页未提及**)。
|
||
- profile 的 `rm` 受引用保护,正在被池使用的 profile 删除会 `EBUSY`。
|
||
- 该页**没有**解释 `plugin=isa` / `technique=reed_sol_van` 的算法细节,也没有讲 `jerasure`、`clay`、`shec` 等插件——**文档未明确**本册范围内的插件全列表。
|
||
|
||
> 引自:[Erasure Coded pool](https://docs.ceph.com/en/tentacle/dev/erasure-coded-pool/)
|
||
|
||
---
|
||
|
||
### 6.3 `deduplication.rst`:重复数据删除
|
||
|
||
> 官方该页共 426 行,是本章信息量最大的一页。它同时包含设计文档与操作手册两部分。官方在「How to use deduplication」一节开头就用斜体强调:**「This feature is highly experimental and is subject to change or removal.」**(高度实验性,可能变更或移除。)
|
||
|
||
#### 6.3.1 设计动机与两个核心挑战(Introduction 段)
|
||
|
||
在既有软件栈上做重删并不容易,原因是**额外的元数据管理**和**原始数据处理流程**。
|
||
|
||
典型的重删系统工作方式:
|
||
|
||
1. 输入源(一个数据对象)被**分块算法(chunking algorithm)**切成多个 chunk。
|
||
2. 重删系统把每个 chunk 与已存储的 chunk 比较。
|
||
3. 为了高效比较,系统维护一个**指纹索引(fingerprint index)**,存每个 chunk 的 hash 值——通过比较 hash 而不是搜索全部内容来定位已有 chunk。
|
||
|
||
在 Ceph 之上实现重删,官方点出**两个本质难题**:
|
||
|
||
| 挑战 | 官方表述 |
|
||
|---|---|
|
||
| 指纹索引的可扩展性 | managing scalability of fingerprint index |
|
||
| 与既有元数据的兼容性 | complex to ensure compatibility between newly applied deduplication metadata and existing metadata |
|
||
|
||
#### 6.3.2 Key Idea:两个关键设计
|
||
|
||
**关键思想一:内容哈希(Double hashing,双重哈希)**
|
||
|
||
官方给出的链条:
|
||
|
||
```text
|
||
Client 1 -> OID=1 -> HASH(1's content)=K -> OID=K ->
|
||
CRUSH(K) -> chunk's location
|
||
```
|
||
|
||
解读:
|
||
|
||
1. 每个客户端可以用 **CRUSH** 根据 object ID 找到对象数据在 **Base tier** 的位置。
|
||
2. 在 Base tier 上**对对象内容做哈希**,得到一个**新的 OID(chunk ID)**。
|
||
3. **Chunk tier** 就以这个新 OID 存储原始对象的一部分内容。
|
||
|
||
也就是说:第一次哈希(CRUSH)定位原对象,第二次哈希(内容哈希)定位 chunk——这就是「double hashing」的含义。
|
||
|
||
**关键思想二:自包含对象(Self-contained object)**
|
||
|
||
官方论证:**外部元数据设计**难以与既有存储特性集成,因为现有存储特性无法识别这些额外的外部数据结构。如果我们能设计出一个**不带任何外部组件**的重删系统,原有存储特性就可以被复用。
|
||
|
||
> 💡 补充:官方在此引用了一篇论文 https://ieeexplore.ieee.org/document/8416369 作为更多细节来源(本册不联网展开)。
|
||
|
||
#### 6.3.3 架构设计(Design 段)
|
||
|
||
官方用 ditaa 图描述,下面按原图结构重绘为文本:
|
||
|
||
```text
|
||
+-------------+
|
||
| Ceph Client |
|
||
+------+------+
|
||
^
|
||
Tiering is |
|
||
Transparent | Metadata
|
||
to Ceph | +---------------+
|
||
Client Ops | | |
|
||
| +----->+ Base Pool |
|
||
| | | |
|
||
| | +-----+---+-----+
|
||
| | | ^
|
||
v v | | Dedup metadata in Base Pool
|
||
+------+----+--+ | | (Dedup metadata contains chunk offsets
|
||
| Objecter | | | and fingerprints)
|
||
+-----------+--+ | |
|
||
^ | | Data in Chunk Pool
|
||
| v |
|
||
| +-----+---+-----+
|
||
| | |
|
||
+----->| Chunk Pool |
|
||
| |
|
||
+---------------+
|
||
Data
|
||
```
|
||
|
||
关键点:
|
||
|
||
- **Tiering 对 Ceph Client 的操作是透明的**(图左侧文字:Tiering is Transparent to Ceph Client Ops)。
|
||
- 客户端 → `Objecter` → 分别访问 **Base Pool** 与 **Chunk Pool**。
|
||
- **Dedup metadata 放在 Base Pool**,其中包含 **chunk offsets(块偏移)** 和 **fingerprints(指纹)**。
|
||
- **实际数据(chunk 对象)放在 Chunk Pool**。
|
||
|
||
**Pool-based object management(基于池的对象管理)**
|
||
|
||
定义两个池:
|
||
|
||
| 池 | 存储内容 | 官方说明 |
|
||
|---|---|---|
|
||
| metadata pool(即 Base Pool) | 元数据对象 | — |
|
||
| chunk pool | chunk 对象 | — |
|
||
|
||
因为两个池按**用途和使用方式**划分,所以每个池可以根据自身特性**更高效**地被管理:
|
||
|
||
- Base pool 和 chunk pool 可以**各自独立选择**冗余方案(replication 或 erasure coding),依用途而定。
|
||
- 每个池可以**放在不同的存储位置**,依所需性能而定。
|
||
|
||
> 关于如何使用,官方指向 `osd_internals/manifest.rst`(本册范围外)。
|
||
|
||
#### 6.3.4 各接口层的机会与代价(Usage Patterns 段)
|
||
|
||
| 接口层 | 官方判断 | 理由 |
|
||
|---|---|---|
|
||
| **RadosGW** | S3 大数据负载是重删的好机会 | 这类对象倾向于**一次写入、主要读取**,**不会部分覆盖**;因此可以**提前(up front)**做指纹计算与重删 |
|
||
| RadosGW(额外优势) | 可**直接使用 refcounting 机制**,不需要 RADOS 直接支持 manifest | 与 cephfs / rbd 不同,radosgw 有一套把显式元数据存在 **head object** 里、用于定位剩余分片的机制(见 `osd_internals/refcount.rst`) |
|
||
| **RBD / CephFS** | 重删元数据需要作为 RADOS 的一部分( presumably transparently,透明地) | 两者都用**确定性命名方案**把块设备/文件数据划分到 rados 对象上 |
|
||
| RBD / CephFS(写入特性) | **不希望**对这些对象做在线重删,也不想在热路径上付出写延迟代价 | 与 radosgw 不同,rbd/cephfs 的 rados 对象**会被覆盖写**;因此**后台**对冷对象做 tiering + dedup 更合适 |
|
||
| RBD / CephFS(快照) | manifest 支持需要**健壮的快照支持** | 两者负载中快照使用很常见 |
|
||
|
||
#### 6.3.5 RADOS 机制(RADOS Machinery 段)
|
||
|
||
官方该段只有两句指引,全部指向 `osd_internals/` 下不在本册的页面:
|
||
|
||
- RADOS 的 redirect / chunk / dedup 支持 → 见 `osd_internals/manifest.rst`
|
||
- RADOS 的 refcount 支持 → 见 `osd_internals/refcount.rst`
|
||
|
||
#### 6.3.6 状态与后续工作(Status and Future Work 段)
|
||
|
||
官方自述的当前状态:
|
||
|
||
- OSD 内部已有一些**初步的 manifest 对象支持**,以及一个 **dedup tool**。
|
||
- **RadosGW 数据仓库类负载**可能代表该特性最大的机会,因此**第一优先**是给 radosgw 增加直接的指纹计算与 redirect 支持(接入 refcount pool)。
|
||
- 除 radosgw 之外,下一步是**完成 OSD 中 manifest 对象支持**,特别是**与快照相关**的部分,以服务 rbd 与 cephfs 负载。
|
||
|
||
#### 6.3.7 实操:如何使用重删(How to use deduplication)
|
||
|
||
**前置条件(Prerequisite)**
|
||
|
||
若集群是从 Ceph mainline 启动的,用户需要确认安装了包含 `ceph-dedup-tool` 的 **`ceph-test` 包**。
|
||
|
||
**可用操作总览**
|
||
|
||
官方说明用户可以在 `ceph-dedup-tool` 上使用 `estimate`、`sample-dedup`、`chunk-scrub`、`chunk-repair` 操作,并鼓励用脚本自由组合这些操作。
|
||
|
||
---
|
||
|
||
**步骤 1:用 `estimate` 估算目标池的空间节省率**
|
||
|
||
```bash
|
||
ceph-dedup-tool --op estimate
|
||
--pool [BASE_POOL]
|
||
--chunk-size [CHUNK_SIZE]
|
||
--chunk-algorithm [fixed|fastcdc]
|
||
--fingerprint-algorithm [sha1|sha256|sha512]
|
||
--max-thread [THREAD_COUNT]
|
||
```
|
||
|
||
参数语义(官方说明):
|
||
|
||
| 参数 | 说明 |
|
||
|---|---|
|
||
| `--pool` / `BASE_POOL` | 被重删的目标对象所在的池 |
|
||
| `--chunk-size` | 官方提醒:需要**用不同的 chunk_size 多次运行** `ceph-dedup-tool` 来寻找最优块大小;在 `fastcdc`(非 fixed)算法下,最优值可能因每个对象的内容而异 |
|
||
| `--chunk-algorithm` | `fixed` 或 `fastcdc` |
|
||
| `--fingerprint-algorithm` | `sha1` / `sha256` / `sha512` |
|
||
| `--max-thread` | 线程数 |
|
||
|
||
输出示例:
|
||
|
||
```bash
|
||
{
|
||
"chunk_algo": "fastcdc",
|
||
"chunk_sizes": [
|
||
{
|
||
"target_chunk_size": 8192,
|
||
"dedup_bytes_ratio": 0.4897049
|
||
"dedup_object_ratio": 34.567315
|
||
"chunk_size_average": 64439,
|
||
"chunk_size_stddev": 33620
|
||
}
|
||
],
|
||
"summary": {
|
||
"examined_objects": 95,
|
||
"examined_bytes": 214968649
|
||
}
|
||
}
|
||
```
|
||
|
||
输出字段释义(官方原文解释):
|
||
|
||
| 字段 | 含义 |
|
||
|---|---|
|
||
| `target_chunk_size` | 与用户传入的 `chunk_size` 相同 |
|
||
| `dedup_bytes_ratio` | 被检视字节中有多少是冗余的。**`1 - dedup_bytes_ratio` 即为可节省的存储百分比** |
|
||
| `dedup_object_ratio` | 生成的 chunk 对象数 / `examined_objects` |
|
||
| `chunk_size_average` | 执行 CDC 时平均切出的块大小。**可能不同于 `target_chunk_size`**,因为 CDC 会依内容生成不同的块边界 |
|
||
| `chunk_size_stddev` | 块大小的标准差 |
|
||
| `examined_objects` / `examined_bytes` | 被检查的对象数 / 字节数 |
|
||
|
||
官方决策建议:如果节省空间高于用户预期,这个池大概就值得做重删。
|
||
|
||
---
|
||
|
||
**步骤 2:创建 chunk pool**
|
||
|
||
```bash
|
||
ceph osd pool create [CHUNK_POOL]
|
||
```
|
||
|
||
---
|
||
|
||
**步骤 3:执行重删(两种方式)**
|
||
|
||
**方式 A:`sample-dedup`**
|
||
|
||
```bash
|
||
ceph-dedup-tool --op sample-dedup
|
||
--pool [BASE_POOL]
|
||
--chunk-pool [CHUNK_POOL]
|
||
--chunk-size [CHUNK_SIZE]
|
||
--chunk-algorithm [fastcdc]
|
||
--fingerprint-algorithm [sha1|sha256|sha512]
|
||
--chunk-dedup-threshold [THRESHOLD]
|
||
--max-thread [THREAD_COUNT]
|
||
--sampling-ratio [SAMPLE_RATIO]
|
||
--wakeup-period [WAKEUP_PERIOD]
|
||
--loop
|
||
--snap
|
||
```
|
||
|
||
官方行为说明:
|
||
|
||
- `sample-dedup` 按 `THREAD_COUNT` 派生线程,对 `BASE_POOL` 上的对象做重删。
|
||
- 依 `sampling-ratio` 决定行为:**`SAMPLE_RATIO` 为 100 时做全量搜索**;否则在迭代中选择性地对「冗余次数超过 `THRESHOLD`」的块执行重删。
|
||
- 若设置 `--loop`,线程会在 `WAKEUP_PERIOD` 后唤醒;否则**一轮迭代后线程退出**。
|
||
|
||
官方给出的完整效果对照:
|
||
|
||
```bash
|
||
$ bin/ceph df
|
||
--- RAW STORAGE ---
|
||
CLASS SIZE AVAIL USED RAW USED %RAW USED
|
||
ssd 303 GiB 294 GiB 9.0 GiB 9.0 GiB 2.99
|
||
TOTAL 303 GiB 294 GiB 9.0 GiB 9.0 GiB 2.99
|
||
|
||
--- POOLS ---
|
||
POOL ID PGS STORED OBJECTS USED %USED MAX AVAIL
|
||
.mgr 1 1 577 KiB 2 1.7 MiB 0 97 GiB
|
||
base 2 32 2.0 GiB 517 6.0 GiB 2.02 97 GiB
|
||
chunk 3 32 0 B 0 0 B 0 97 GiB
|
||
|
||
$ bin/ceph-dedup-tool --op sample-dedup --pool base --chunk-pool chunk
|
||
--fingerprint-algorithm sha1 --chunk-algorithm fastcdc --loop --sampling-ratio 100
|
||
--chunk-dedup-threshold 2 --chunk-size 8192 --max-thread 4 --wakeup-period 60
|
||
|
||
$ bin/ceph df
|
||
--- RAW STORAGE ---
|
||
CLASS SIZE AVAIL USED RAW USED %RAW USED
|
||
ssd 303 GiB 298 GiB 5.4 GiB 5.4 GiB 1.80
|
||
TOTAL 303 GiB 298 GiB 5.4 GiB 5.4 GiB 1.80
|
||
|
||
--- POOLS ---
|
||
POOL ID PGS STORED OBJECTS USED %USED MAX AVAIL
|
||
.mgr 1 1 577 KiB 2 1.7 MiB 0 98 GiB
|
||
base 2 32 452 MiB 262 1.3 GiB 0.50 98 GiB
|
||
chunk 3 32 258 MiB 25.91k 938 MiB 0.31 98 GiB
|
||
```
|
||
|
||
观察:`base` 池 STORED 从 2.0 GiB 降到 452 MiB,`chunk` 池新增 258 MiB / 25.91k 个对象;整体 RAW USED 从 9.0 GiB 降到 5.4 GiB。
|
||
|
||
**方式 B:`object dedup`**
|
||
|
||
```bash
|
||
ceph-dedup-tool --op object-dedup
|
||
--pool [BASE_POOL]
|
||
--object [OID]
|
||
--chunk-pool [CHUNK_POOL]
|
||
--fingerprint-algorithm [sha1|sha256|sha512]
|
||
--dedup-cdc-chunk-size [CHUNK_SIZE]
|
||
```
|
||
|
||
官方说明:
|
||
|
||
- `object-dedup` 针对 `--object [OID]` 指定的那一个 RADOS 对象触发重删。
|
||
- **上述所有参数都必须指定**。
|
||
- `CHUNK_SIZE` 应当取自**步骤 1 的结果**。
|
||
- 执行该命令时,**`fastcdc` 会被设为默认**,其它诸如 `fingerprint-algorithm` 和 `CHUNK_SIZE` 的参数会被设为该池的默认值。
|
||
- 重删后的对象出现在 chunk pool。
|
||
- **若对象内容随时间被修改,用户需要重新运行 `object-dedup`**,因为块边界需要基于更新后的内容重新计算。
|
||
- 若目标对象有快照,用户需要指定 `snap`。
|
||
- 重删完成后,**目标对象在 `BASE_POOL` 中的大小变为 0(被 evicted)**,同时生成 chunk 对象——它们出现在 `CHUNK_POOL`。
|
||
|
||
---
|
||
|
||
**步骤 4:读写 I/O**
|
||
|
||
官方明确:步骤 3 之后,**用户不需要为 I/O 操心任何事**——重删后的对象与既有 RADOS 操作**完全兼容**。
|
||
|
||
---
|
||
|
||
**步骤 5:运行 scrub 修复引用计数**
|
||
|
||
官方说明:在为重删后的 RADOS 对象处理引用计数时,**极少数情况下会因误报(false positives)产生引用不匹配**。这些不匹配由周期性 scrub 修复:
|
||
|
||
```bash
|
||
ceph-dedup-tool --op chunk-scrub
|
||
--chunk-pool [CHUNK_POOL]
|
||
--pool [POOL]
|
||
--max-thread [THREAD_COUNT]
|
||
```
|
||
|
||
`chunk-scrub` 命令识别**元数据对象与 chunk 对象之间的引用不匹配**;`chunk-pool` 参数告诉 `ceph-dedup-tool` 目标 chunk 对象在哪里。
|
||
|
||
官方示例(先用 `chunk-get-ref` 往 chunk 对象 `2ac67f70...` 里插了一个假引用 `dummy-obj`):
|
||
|
||
```bash
|
||
$ bin/ceph-dedup-tool --op dump-chunk-refs --chunk-pool chunk --object 2ac67f70d3dd187f8f332bb1391f61d4e5c9baae
|
||
{
|
||
"type": "by_object",
|
||
"count": 2,
|
||
"refs": [
|
||
{
|
||
"oid": "testfile2",
|
||
"key": "",
|
||
"snapid": -2,
|
||
"hash": 2905889452,
|
||
"max": 0,
|
||
"pool": 2,
|
||
"namespace": ""
|
||
},
|
||
{
|
||
"oid": "dummy-obj",
|
||
"key": "",
|
||
"snapid": -2,
|
||
"hash": 1203585162,
|
||
"max": 0,
|
||
"pool": 2,
|
||
"namespace": ""
|
||
}
|
||
]
|
||
}
|
||
|
||
$ bin/ceph-dedup-tool --op chunk-scrub --chunk-pool chunk --max-thread 10
|
||
10 seconds is set as report period by default
|
||
join
|
||
join
|
||
2ac67f70d3dd187f8f332bb1391f61d4e5c9baae
|
||
--done--
|
||
2ac67f70d3dd187f8f332bb1391f61d4e5c9baae ref 10:5102bde2:::dummy-obj:head: referencing pool does not exist
|
||
--done--
|
||
Total object : 1
|
||
Examined object : 1
|
||
Damaged object : 1
|
||
```
|
||
|
||
引用条目(ref)的字段结构(从示例归纳):
|
||
|
||
| 字段 | 示例值 | 说明 |
|
||
|---|---|---|
|
||
| `oid` | `testfile2` / `dummy-obj` | 引用方对象名 |
|
||
| `key` | `""` | 键(示例中为空) |
|
||
| `snapid` | `-2` | 快照 id(`-2` 表示 head) |
|
||
| `hash` | `2905889452` | 对象名哈希 |
|
||
| `max` | `0` | —(**文档未明确**其语义) |
|
||
| `pool` | `2` | 引用方所在池 id |
|
||
| `namespace` | `""` | 命名空间 |
|
||
|
||
---
|
||
|
||
**步骤 6:修复不匹配的 chunk 引用**
|
||
|
||
若 `chunk-scrub` 之后出现引用不匹配,官方建议执行 `chunk-repair` 修复:
|
||
|
||
```bash
|
||
ceph-dedup-tool --op chunk-repair
|
||
--chunk-pool [CHUNK_POOL_NAME]
|
||
--object [CHUNK_OID]
|
||
--target-ref [TARGET_OID]
|
||
--target-ref-pool-id [TARGET_POOL_ID]
|
||
```
|
||
|
||
`chunk-repair` 修复的是 `--target-ref`(即某个 `object` 的错误引用)。为正确修复,**用户必须输入正确的 `TARGET_OID` 和 `TARGET_POOL_ID`**。
|
||
|
||
示例:
|
||
|
||
```bash
|
||
$ bin/ceph-dedup-tool --op chunk-repair --chunk-pool chunk --object 2ac67f70d3dd187f8f332bb1391f61d4e5c9baae --target-ref dummy-obj --target-ref-pool-id 10
|
||
2ac67f70d3dd187f8f332bb1391f61d4e5c9baae has 1 references for dummy-obj
|
||
dummy-obj has 0 references for 2ac67f70d3dd187f8f332bb1391f61d4e5c9baae
|
||
fix dangling reference from 1 to 0
|
||
|
||
$ bin/ceph-dedup-tool --op dump-chunk-refs --chunk-pool chunk --object 2ac67f70d3dd187f8f332bb1391f61d4e5c9baae
|
||
{
|
||
"type": "by_object",
|
||
"count": 1,
|
||
"refs": [
|
||
{
|
||
"oid": "testfile2",
|
||
"key": "",
|
||
"snapid": -2,
|
||
"hash": 2905889452,
|
||
"max": 0,
|
||
"pool": 2,
|
||
"namespace": ""
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 6.3.8 注意事项汇总
|
||
|
||
- **高度实验性**:官方原文 "This feature is highly experimental and is subject to change or removal."
|
||
- 需要 `ceph-test` 包提供 `ceph-dedup-tool`。
|
||
- `fastcdc` 是变长分块,最优 chunk size 需实测;`fixed` 是定长分块。
|
||
- 重删后目标对象**不再持有数据**(size = 0),数据全在 chunk pool;一旦 chunk pool 出问题,base pool 的对象无法恢复(**文档未明确**此风险的具体处理,但架构上显然如此)。
|
||
- 引用计数可能误报,必须周期性 `chunk-scrub` + `chunk-repair`。
|
||
- 对象被修改后必须重跑 `object-dedup`。
|
||
- 本页未说明 `sample-dedup` 的 `--snap` 参数细节,也未说明 `estimate` 里 `max` 字段含义——**文档未明确**。
|
||
|
||
> 引自:[Deduplication](https://docs.ceph.com/en/tentacle/dev/deduplication/)
|
||
|
||
---
|
||
|
||
### 6.4 `delayed-delete.rst`:CephFS 延迟删除
|
||
|
||
> 官方该页**只有 13 行**(含标题与空行),是全套 `dev/` 文档中最短的一页之一。内容极简,仅解释动机,不涉及任何数据结构或代码路径。如实说明如下。
|
||
|
||
#### 6.4.1 设计动机
|
||
|
||
删除一个文件**并不会立即移除它的数据**——该文件的**每个底层对象都必须被独立地移除**。
|
||
|
||
官方给出「立即删」的代价:
|
||
|
||
| 问题 | 官方说明 |
|
||
|---|---|
|
||
| 网络/消息开销 | 客户端将不得不发送 `size_of_file / stripe_size * replication_count` 条消息 |
|
||
| 带宽与延迟 | 这会消耗可观的带宽(significant bandwith),并**把客户端拖慢到不可接受**(slow the client unacceptably) |
|
||
| 快照阻塞 | 若存在快照,**快照的存在会阻止**与之关联的对象的删除 |
|
||
|
||
> 💡 补充:原文写作 `significant bandwith`(原文即如此拼写,疑为 bandwidth 笔误)。
|
||
|
||
#### 6.4.2 执行流程(官方两句)
|
||
|
||
在这类情况下,这样的文件会:
|
||
|
||
1. **在 MDS 上被标记为已删除**(marked as deleted on the MDS);
|
||
2. **被懒惰地/延迟地删除**(deleted lazily)。
|
||
|
||
#### 6.4.3 注意事项
|
||
|
||
- 该页**未**说明延迟删除的具体实现:没有 `PurgeQueue`、没有 `purge_queue` 配置项、没有 MDS 侧的异步删除线程、也没有 `stray` 目录机制 —— 这些均由**文档未明确**。
|
||
- 该页**未**说明「lazy delete」的触发时机与吞吐控制。
|
||
- 快照阻止删除的具体判定逻辑,本页只说「their existence can prevent the deletion」,未展开。
|
||
|
||
> 引自:[CephFS delayed deletion](https://docs.ceph.com/en/tentacle/dev/delayed-delete/)
|
||
|
||
---
|
||
|
||
### 6.5 `crush-msr.rst`:CRUSH 多步重试(Multi-step Retry)
|
||
|
||
> 官方该页共 140 行,是一篇**设计与实现说明**,核心在 `src/crush/mapper.c` / `mapper.h`。理解它需要先把「传统 CRUSH 的局限」讲清楚,官方正是按这个顺序组织的。
|
||
|
||
#### 6.5.1 设计动机:传统 CRUSH 无法回退重试(Motivation 段)
|
||
|
||
官方开宗明义:**传统 CRUSH 有一个重要限制——带多个 `choose` 步骤的规则,在遇到 `out` OSD 时无法重试前面的步骤。**
|
||
|
||
**对比场景一:`chooseleaf`(行为良好)**
|
||
|
||
```text
|
||
rule replicated_rule_1 {
|
||
...
|
||
step take default class hdd
|
||
step chooseleaf firstn 3 type host
|
||
step emit
|
||
}
|
||
```
|
||
|
||
官方描述其表现:
|
||
|
||
- 若某台 host 上的**所有 OSD 都被标记 out**,人们会期望包含这些 OSD 的映射最终落到另一台 host 上(前提是 host 数量足够)。**事实上正是如此**。
|
||
- 若某 host 上 **1/8 的 OSD 被标记 out**,大致 **1/8 映射到该 host 的 PG 会被重映射到其它 host**,从而**保持每个 OSD 的总体利用率均衡**。
|
||
|
||
**对比场景二:`choose` 两次(行为糟糕)**
|
||
|
||
```text
|
||
rule replicated_rule_1 {
|
||
...
|
||
step take default class hdd
|
||
step choose firstn 3 type host
|
||
step choose firstn 1 type osd
|
||
step emit
|
||
}
|
||
```
|
||
|
||
官方描述:
|
||
|
||
- 只要没有 OSD 被标记 out,行为与场景一**非常相似**。
|
||
- 但**一旦某个 OSD 被标记 out**,映射到该 OSD 的 PG 会被重映射到**同一台 host 上的其它 OSD**,导致**这些 OSD 相对其它 host 上的 OSD 过度使用(over-utilized)**。
|
||
- 更糟:若**一台 host 上的所有 OSD 都被标记 out**,碰巧命中该 host 的映射会**失败**,导致 **undersized PG**。
|
||
|
||
**结论**:只要目标是「把 N 个 OSD 分到 N 个故障域」,用 `chooseleaf` 就够了。
|
||
|
||
**但 `chooseleaf` 解决不了的场景(官方用例)**
|
||
|
||
想把 **8+6 EC 编码分布到 4 台 host** 上,以容忍「1 台 host 失效 + 另一台 host 上 1 个 OSD 失效」,存储开销 **1.75x**。规则必须写成:
|
||
|
||
```text
|
||
rule ecpool-86 {
|
||
...
|
||
step take default class hdd
|
||
step choose indep 4 type host
|
||
step choose indep 4 type osd
|
||
step emit
|
||
}
|
||
```
|
||
|
||
- 这条规则确实能把最多 **16 个 OSD 分到 4 台 host**(8+6 编码下:前 3 台各放 4 个,最后一台放 2 个),并**满足故障要求**。
|
||
- 但基于前述原因,当 OSD 被标记 out 而**又有其它 host 可以重平衡过去**时,它**表现很差**。
|
||
- **`chooseleaf` 在这里不是解法**,因为它**不支持在指定 type 之下映射多于一个 leaf**。
|
||
|
||
#### 6.5.2 MSR 的解法(MSR 段)
|
||
|
||
**CRUSH MSR(Multi-step Retry)规则**通过一种**不同的下降算法**解决上述问题:**在遇到 out OSD 时重试所有步骤**。
|
||
|
||
| 对比项 | 传统 CRUSH | MSR 规则 |
|
||
|---|---|---|
|
||
| 下降顺序 | **广度优先(breadth first)**:对每一步,先把向量填满,再进入下一步 | **深度优先(depth first)**:对每一个 choice,递归地下降穿过所有步骤,再继续下一个 choice |
|
||
|
||
上述用例可写成:
|
||
|
||
```text
|
||
rule ecpool-86 {
|
||
type msr_indep
|
||
...
|
||
step take default class hdd
|
||
step choosemsr 4 type host
|
||
step choosemsr 4 type osd
|
||
step emit
|
||
}
|
||
```
|
||
|
||
官方说明其效果:与顶部 `chooseleaf` 的例子一样,**当 OSD 被标记 out 时,这些 OSD 会被按比例地重映射到其它 host**(只要有富余的 host 可用)。
|
||
|
||
> 关于「如何在仍然保持故障域隔离的前提下做到这一点」,官方指向源码注释:`mapper.c:crush_msr_choose`。
|
||
|
||
#### 6.5.3 规则结构(Rule Structure 段)
|
||
|
||
**规则类型**:CRUSH MSR 规则是 type 为以下之一的 crush rule:
|
||
|
||
```c
|
||
CRUSH_RULE_TYPE_MSR_FIRSTN
|
||
CRUSH_RULE_TYPE_MSR_INDEP
|
||
```
|
||
|
||
(见 `mapper.c: rule_type_is_msr`)
|
||
|
||
**与传统规则的结构差异**(官方列出的三条):
|
||
|
||
1. **规则类型决定映射是 FIRSTN 还是 INDEP**。因为下降过程可以重试步骤,让每个 step 各自指定输出顺序**没有意义**,而且官方作者「并不清楚有什么用例能从中受益」。因此**与传统 crush 规则不同,单个 step 不指定 firstn 或 indep**。
|
||
2. MSR 规则**必须**被组织为:
|
||
- 一段(**可能为空**的)**config 步骤前缀**(`CRUSH_RULE_SET_CHOOSE_MSR*`),
|
||
- 后面跟着**若干 EMIT block**,每个 EMIT block 由:
|
||
- 一个 **TAKE** 步骤,
|
||
- 一串 **CHOOSE_MSR** 步骤,
|
||
- 以一个 **EMIT** 步骤结束。
|
||
3. MSR 的步骤**必须是 `choosemsr`**。**不允许**使用 `choose` 和 `chooseleaf`。
|
||
|
||
> 上表第 2 条中的「config 步骤前缀」在官方原句中写作 `CRUSH_RULE_SET_CHOOSE_MSR*`,含义是这一类 set 型步骤;**文档未明确**给出允许的具体 set 步骤完整清单。
|
||
|
||
#### 6.5.4 工作空间(Working Space 段)
|
||
|
||
MSR 规则对**工作空间**的要求也不同:
|
||
|
||
| 规则类型 | 工作空间需求 |
|
||
|---|---|
|
||
| 传统 CRUSH | **3 个**大小为 `result_max` 的向量:两个在处理每条规则时交替使用,另外一个额外供 `chooseleaf` 使用 |
|
||
| MSR 规则 | **N 个**向量,N = **最长 EMIT block 中 `choosemsr` 步骤的数量**。因为 MSR 需要保留**每次下降过程中做出的所有选择** |
|
||
|
||
详见 `mapper.h/c:crush_work_size`、`crush_msr_scan_rule`。
|
||
|
||
#### 6.5.5 实现入口(Implementation 段)
|
||
|
||
- `mapper.h/c` 中的 `crush_do_rule` 内部会**分支**到 `mapper.c:crush_msr_do_rule`,条件是规则类型为 `CRUSH_RULE_TYPE_MSR_*`(判定见 `mapper.c:rule_type_is_msr`)。
|
||
- `mapper.c` 中与 MSR 相关的函数**都带有关于该算法的更详细注释**。
|
||
|
||
#### 6.5.6 关键代码路径小结
|
||
|
||
| 关注点 | 位置 |
|
||
|---|---|
|
||
| 是否 MSR 规则判定 | `mapper.c: rule_type_is_msr` |
|
||
| 主入口分派 | `mapper.h/c: crush_do_rule` → `mapper.c: crush_msr_do_rule` |
|
||
| 核心下降算法(含故障域隔离说明的注释) | `mapper.c: crush_msr_choose` |
|
||
| 工作空间计算 | `mapper.h/c: crush_work_size` |
|
||
| 规则扫描 | `mapper.c: crush_msr_scan_rule` |
|
||
|
||
#### 6.5.7 注意事项
|
||
|
||
- MSR 规则里**不能用** `choose` / `chooseleaf`,只能用 `choosemsr`;反过来,传统规则里也没有 `choosemsr`。
|
||
- MSR 规则的输出顺序(FIRSTN / INDEP)是**整条规则级**的,写在 `type` 上(如 `type msr_indep`)。
|
||
- 工作空间随 EMIT block 中 `choosemsr` 步骤数线性增长,这是 MSR 相对传统 CRUSH 的额外代价。
|
||
- 官方**未**给出 `msr_firstn` / `msr_indep` 两类型在失败重试语义上的完整差异,也**未**给出 `crush_msr_choose` 的伪代码——**文档未明确**,需读源码注释。
|
||
|
||
> 引自:[CRUSH MSR (Multi-step Retry)](https://docs.ceph.com/en/tentacle/dev/crush-msr/)
|
||
|
||
---
|
||
|
||
## 七、配置与可观测性内部
|
||
|
||
本章覆盖 12 个页面,是全套 `dev/` 文档中**页数最多但深度参差**的一章:既有 `config.rst` 这种讲配置系统内部实现的,也有 `perf.rst`、`cpu-profiler.rst` 这种纯操作手册(几行命令),还有 `corpus.rst` 这种面向开发者的测试基础设施说明。
|
||
|
||
### 7.1 `config.rst`:配置管理系统(Configuration Management System)
|
||
|
||
> 官方该页共 341 行,标题为 Configuration Management System。它讲的是**配置系统自身的内部实现**:配置从哪来、怎么定义、代码怎么读、怎么改、怎么在文档里生成。
|
||
|
||
#### 7.1.1 设计动机
|
||
|
||
配置管理系统存在的目的:**给每个守护进程提供正确的配置信息**。官方把配置抽象为**一组 key-value 对**。
|
||
|
||
#### 7.1.2 配置来源(三个)
|
||
|
||
| 来源 | 官方说明 | 示例 |
|
||
|---|---|---|
|
||
| Ceph 配置文件 | 通常命名为 `ceph.conf` | — |
|
||
| 命令行参数 | — | `--debug-ms=1`、`--debug-monc=10` |
|
||
| 运行时注入的参数 | 通过 `injectargs` 或 `config set` | — |
|
||
|
||
#### 7.1.3 配置文件的查找顺序
|
||
|
||
官方给出的顺序(**依次检查**):
|
||
|
||
1. **默认位置**(default locations)
|
||
2. 环境变量 **`CEPH_CONF`**
|
||
3. 命令行参数 **`-c`**
|
||
|
||
#### 7.1.4 配置文件结构与 stanza 语义
|
||
|
||
配置文件中每个 stanza 描述「对某一子集守护进程生效的 key-value 对」:
|
||
|
||
| stanza 形式 | 作用范围 |
|
||
|---|---|
|
||
| `global` | 应用于**所有**(everything) |
|
||
| `mon` | 所有 monitor |
|
||
| `osd` | 所有 OSD |
|
||
| `mds` | 所有 mds server |
|
||
| `mon.$name` | 指定名字的那个 monitor |
|
||
| `osd.$name` | 指定名字的那个 OSD |
|
||
| `mds.$name` | 指定名字的那个 MDS |
|
||
|
||
**覆盖规则**:**配置文件中出现较晚的值胜过较早的值**(Configuration values that appear later in the file win over earlier ones)。
|
||
|
||
官方提示:示例配置文件位于 `src/sample.ceph.conf`。
|
||
|
||
#### 7.1.5 Metavariables(元变量)
|
||
|
||
配置系统允许**把任意配置值替换进另一个值**,语法是 `$varname`,「similar to how bash shell expansion works」。
|
||
|
||
额外的特殊元变量:
|
||
|
||
| 元变量 | 展开为 | 官方示例 |
|
||
|---|---|---|
|
||
| `$host` | 当前主机名 | — |
|
||
| `$type` | `mds` / `osd` / `mon` / `client` 之一 | — |
|
||
| `$id` | 守护进程标识符 | `osd.0` → `0`;`mds.a` → `a`;`client.admin` → `admin` |
|
||
| `$num` | 同 `$id` | — |
|
||
| `$name` | `$type.$id` | — |
|
||
|
||
#### 7.1.6 读取配置值的两种方式(Reading configuration values)
|
||
|
||
| 方式 | 做法 | 官方评价 |
|
||
|---|---|---|
|
||
| **直接读变量** | 从 `g_conf`(等价于 `g_ceph_ctx->_conf`)读 | **应被视为 deprecated,新代码不要这样做** |
|
||
| **注册 observer** | 实现 `common/config_obs.h` 里的接口;每次相关配置值变化时被调用 | **新代码应优先使用** |
|
||
|
||
observer 的行为特征:
|
||
|
||
- 在**初始配置被读取后不久**会被调用一次;
|
||
- 此后**每当相关值之一变化时**被调用;
|
||
- 每个 observer **追踪一组 key**,**只有**当相关 key 变化时才被触发。
|
||
|
||
官方给出偏好 observer 的**三个理由**:
|
||
|
||
1. **更灵活**:允许代码执行「实现新配置值所需的任何重新初始化动作」。
|
||
2. **唯一途径**:这是创建「可以被 `injectargs` 修改的 `std::string` 配置变量」的**唯一方式**。
|
||
3. **线程安全**:即便是整型配置项,在一个线程里改值、另一个线程同时读,也会导致**微妙且无法诊断的 bug**。
|
||
|
||
官方强调:**不要去修改 `g_conf`**(Do not ever alter `g_conf`)。
|
||
|
||
#### 7.1.7 修改配置值(Changing configuration values)
|
||
|
||
| 函数 | 作用 | 后续动作 |
|
||
|---|---|---|
|
||
| `g_conf()->set_val` | 修改配置 | 需调用 `g_conf()->apply_changes` 重跑受影响的 observer |
|
||
| `g_conf()->apply_changes` | 重跑受影响的配置 observer | — |
|
||
| `g_conf()->set_val_or_die` | 便捷形式:做一次「你认为绝不该失败」的配置变更 | 内含 apply |
|
||
| `injectargs` | 另一种修改配置的途径 | 与 `set_val` 一样,之后要调 `apply_changes` |
|
||
| `parse_argv` | 解析命令行参数并修改配置 | 同上 |
|
||
| `parse_env` | 解析环境变量并修改配置 | 同上 |
|
||
|
||
#### 7.1.8 定义配置项(Defining config options)
|
||
|
||
**存放位置**:配置项定义在 **`common/options/*.yaml.in`**,按**消费者(consumers)**分类。若某选项只被 `ceph-osd` 使用,就放到 `osd.yaml.in`。
|
||
|
||
**构建流程**:所有 `.yaml.in` 文件在构建时由 **`y2c.py`** 翻译成 `.cc` 和 `.h` 文件。
|
||
|
||
> **注意(官方 note)**:`ceph-mgr` 模块与其它 Ceph 组件**使用同一套配置系统**,但它们的配置项在**各模块自身的 Python 实现里**定义。
|
||
|
||
**一个典型选项(官方示例)**:
|
||
|
||
```yaml
|
||
- name: public_addr
|
||
type: addr
|
||
level: basic
|
||
desc: public-facing address to bind to
|
||
long_desc: The IP address for the public (front-side) network.
|
||
Set for each daemon.
|
||
services:
|
||
- mon
|
||
- mds
|
||
- osd
|
||
- mgr
|
||
flags:
|
||
- startup
|
||
with_legacy: true
|
||
```
|
||
|
||
#### 7.1.9 选项定义的字段语义(官方逐条说明)
|
||
|
||
**`level`** —— 表示该选项「被运维或开发者调整的概率」:
|
||
|
||
| 取值 | 官方定义 |
|
||
|---|---|
|
||
| `basic` | 普通运维**很可能**会调整的基础配置项 |
|
||
| `advanced` | 运维**可以**调整,但**除非明白自己在做什么否则不要碰**;不当调整会导致问题(性能问题甚至数据丢失) |
|
||
| `dev` | 仅供开发者使用:要么用于测试目的,要么描述「不应被用户调整但我们不想硬编码进代码」的常量 |
|
||
|
||
**`desc` / `long_desc` / `fmt_desc`**
|
||
|
||
| 字段 | 官方定义 | 官方示例 |
|
||
|---|---|---|
|
||
| `desc` | 简短描述,句子片段 | `desc: Default checksum algorithm to use` |
|
||
| `long_desc` | 完整句子,可能是多段,可包含其它详细信息或注意事项 | `long_desc: crc32c, xxhash32, and xxhash64 are available. The _16 and _8 variants use only a subset of the bits for more compact (but less reliable) checksumming.` |
|
||
| `fmt_desc` | 用 reStructuredText 格式化的描述。**仅**被 `confval` 指令用于在文档中渲染选项 | `fmt_desc: The interval for "deep" scrubbing (fully reading all data). The ``osd_scrub_load_threshold`` does not affect this setting.` |
|
||
|
||
**默认值(Default values)**
|
||
|
||
- **每个配置项都有默认值**。
|
||
- 某些情况下还存在 **daemon default**——**只对「声明自己是守护进程」的代码生效**;此时常规默认值只作用于非守护进程。
|
||
|
||
```yaml
|
||
default: crc32c
|
||
```
|
||
|
||
- 对 `float` / `size` / `secs` 类型的选项,允许使用**字面量后缀**:
|
||
|
||
```yaml
|
||
- name: mon_scrub_interval
|
||
type: secs
|
||
default: 1_day
|
||
- name: osd_journal_size
|
||
type: size
|
||
default: 5_K
|
||
```
|
||
|
||
官方鼓励在新增或更新默认值时使用这些字面量后缀以提升可读性。
|
||
|
||
**`services`(Service)**
|
||
|
||
- Service 是组件名,如 `common`、`osd`、`rgw`、`mds` 等,也可以是**列表**。
|
||
- 官方例子:rocksdb 选项同时影响 osd 和 mon。
|
||
- **若某选项被放在某个 service 专属的 `.yaml.in` 文件中,对应 service 会被自动加入其 `services` 属性**。例如 `osd_scrub_begin_hour` 位于 `osd.yaml.in`,即便该文件里没有显式写 `services`,其 `services` 属性仍包含 `osd`。
|
||
|
||
**`tags`(标签)**
|
||
|
||
标识跨 service 之间**有某种关联**的选项。官方给出的两个例子:
|
||
|
||
| tag | 含义 |
|
||
|---|---|
|
||
| `network` | 影响网络配置的选项 |
|
||
| `mkfs` | 只在 mkfs 时才有意义的选项 |
|
||
|
||
**`enum_values`(枚举)**
|
||
|
||
用于「有确定允许取值集合」的选项:
|
||
|
||
```yaml
|
||
enum_values:
|
||
- none
|
||
- crc32c
|
||
- crc32c_16
|
||
- crc32c_8
|
||
- xxhash32
|
||
- xxhash64
|
||
```
|
||
|
||
**`flags`(标志位)**
|
||
|
||
| flag | 官方定义 |
|
||
|---|---|
|
||
| `runtime` | 该值**可以在运行时更新** |
|
||
| `no_mon_update` | 守护进程/客户端**不从 monitor 配置数据库拉取**这个值;我们**禁止**通过 `ceph config set ...` 设置它。该选项应通过 `ceph.conf` 或命令行配置 |
|
||
| `startup` | 选项**只在守护进程启动期间生效** |
|
||
| `cluster_create` | 选项**只影响集群创建** |
|
||
| `create` | 选项**只影响守护进程创建** |
|
||
|
||
#### 7.1.10 配置项在文档中的呈现(Documentation of Configuration Values)
|
||
|
||
官方说明:Ceph 配置项是**按需(on-demand)**用 `:confval:` 指令文档化的,**而不是集中在一个地方**。
|
||
|
||
** documenting 用指令**:
|
||
|
||
```rst
|
||
The check interval can be customized by the ``check_interval`` option:
|
||
|
||
.. confval:: mgr/inbox/check_interval
|
||
```
|
||
|
||
> **官方 note**:`ceph-mgr` 模块选项**必须**包含 `mgr/<module>/` 命名空间前缀。上例中 `check_interval` 属于 `inbox` 模块,所以文档化时写 `mgr/inbox/check_interval`。
|
||
|
||
**引用(Referencing)用角色**:
|
||
|
||
```rst
|
||
With the :confval:`mgr/inbox/check_interval` setting, you can customize the
|
||
check interval.
|
||
```
|
||
|
||
对普通 Ceph 选项(非 mgr 模块),流程相同,只是**没有模块前缀**:
|
||
|
||
```rst
|
||
You can set the initial monitor members with :confval:`mon_initial_members`:
|
||
|
||
.. confval:: mon_initial_members
|
||
```
|
||
|
||
**命名约定(Naming Conventions)**
|
||
|
||
| 类别 | 格式 | 示例 |
|
||
|---|---|---|
|
||
| Mgr 模块选项 | `mgr/<module>/<option_name>` | `mgr/inbox/check_interval` |
|
||
| 普通选项 | 直接用选项名 | `mon_initial_members` |
|
||
|
||
官方称这种方式「确保整个文档中交叉引用的一致性,同时为不同配置上下文保持恰当的命名空间」。
|
||
|
||
#### 7.1.11 注意事项
|
||
|
||
- **不要用 `g_conf` 直接读**,新代码一律用 observer。
|
||
- 改完配置必须 `apply_changes`,否则 observer 不会被触发。
|
||
- `no_mon_update` 标志的选项**不能**通过 `ceph config set` 设置——这是排查「配置设了但没生效」时的高频原因。
|
||
- 该页**未**涉及 `config-key`、`ceph config` 命令族、配置掩码(mask)匹配算法的实现细节 —— 其中 mask 的形式出现在 `config-key.rst`(见 7.2)。
|
||
|
||
> 引自:[Configuration Management System](https://docs.ceph.com/en/tentacle/dev/config/)
|
||
|
||
---
|
||
|
||
### 7.2 `config-key.rst`:config-key 的键布局
|
||
|
||
> 官方该页共 68 行,是一份**键命名规范清单**。它记录 mon 提供的通用 K/V 存储服务 `config-key` 中,**树内(in-tree)用户目前使用的键布局 schema**。
|
||
|
||
#### 7.2.1 设计动机
|
||
|
||
`config-key` 是 **mon 提供的一个通用 key/value 存储服务**。官方原话:「Generally speaking, you can put whatever you want there.」——原则上什么都能放。因此本页的作用是:**把当前树内用户的键布局 schema 记录下来**(Current in-tree users should be captured here with their key layout schema)。
|
||
|
||
#### 7.2.2 OSD dm-crypt 密钥
|
||
|
||
**键格式**:
|
||
|
||
```text
|
||
dm-crypt/osd/$OSD_UUID/luks = <json string>
|
||
```
|
||
|
||
**JSON payload 形式**:
|
||
|
||
```text
|
||
{ "dm-crypt": <secret> }
|
||
```
|
||
|
||
其中 secret 是 **base64 编码的 LUKS key**。
|
||
|
||
| 环节 | 说明 |
|
||
|---|---|
|
||
| 创建者 | `osd new` 命令(见 `OSDMonitor.cc`) |
|
||
| 消费者 | `ceph-volume` 及类似工具 |
|
||
| 访问控制 | 通常由一个 `client.osd-lockbox.$OSD_UUID` 的 **cephx key** 授权访问 `dm-crypt/osd/$OSD_UUID` 前缀,**使得只有对应的主机**能取回 LUKS key |
|
||
|
||
**两级加密关系**(官方描述):LUKS key(存在 config-key 中)**又去解密真正的 raw key**,而 raw key **也存放在设备自身上**。
|
||
|
||
#### 7.2.3 ceph-mgr 模块的键约定
|
||
|
||
约定格式(两种):
|
||
|
||
```text
|
||
mgr/$MODULE/$option = $value
|
||
mgr/$MODULE/$MGRID/$option = $value
|
||
```
|
||
|
||
官方示例:
|
||
|
||
```text
|
||
mgr/dashboard/server_port = 80
|
||
mgr/dashboard/foo/server_addr = 1.2.3.4
|
||
mgr/dashboard/bar/server_addr = 1.2.3.5
|
||
```
|
||
|
||
第二种形式用于**区分不同 mgr 实例**(`$MGRID`)。
|
||
|
||
#### 7.2.4 客户端与守护进程的配置项(Configuration)
|
||
|
||
**客户端与守护进程的配置选项也存放在 config-key 中。**
|
||
|
||
**键的四种形式**:
|
||
|
||
```text
|
||
config/$option = $value
|
||
config/$type/$option = $value
|
||
config/$type.$id/$option = $value
|
||
config/$type.$id/$mask[/$mask2...]/$option = $value
|
||
```
|
||
|
||
**字段语义**:
|
||
|
||
| 字段 | 说明 | 官方示例 |
|
||
|---|---|---|
|
||
| `type` | 守护进程类型 | `osd`、`mon`、`mds`、`mgr`、`client` |
|
||
| `id` | 守护进程 id,使得 `$type.$id` 形如 `osd.123` 或 `mds.foo` | `0`、`foo` |
|
||
| `mask` | **限制该选项作用于谁**;有两种形式 | 见下 |
|
||
| mask 形式 1 | `$crush_type:$crush_value` | `rack:foorack` |
|
||
| mask 形式 2 | `class:$classname`,引用 **CRUSH device classes** | `class:ssd` |
|
||
|
||
mask 可以**串联多个**(`/$mask2...`)。
|
||
|
||
#### 7.2.5 注意事项
|
||
|
||
- 该页只记录 **in-tree** 用户;第三方放进去的键不在规范内。
|
||
- 官方**未**说明 `config-key` 的读写 API、事务语义、大小限制或权限模型(除 dm-crypt 那段提到的 cephx lockbox 授权外)——**文档未明确**。
|
||
- 从 7.1 可知,带 `no_mon_update` 标志的选项**不会**走这条 config-key 路径。
|
||
|
||
> 引自:[config-key layout](https://docs.ceph.com/en/tentacle/dev/config-key/)
|
||
|
||
---
|
||
|
||
### 7.3 `logging.rst`:集群日志的使用规范
|
||
|
||
> 官方该页共 106 行,标题是 "Use of the cluster log"。**必须注意**:官方开篇即声明——「none of this applies to the local "dout" logging. This is about the cluster log that we send through the mon daemons」,即**本页规则只适用于经由 mon 守护进程发送的集群日志,不适用于本地 dout 日志**(后者见 7.4)。
|
||
|
||
本页是**规范/风格指南**,不是实现说明。分三段:Severity、Frequency、Language and formatting。
|
||
|
||
#### 7.3.1 Severity(严重级别使用规范)
|
||
|
||
| 级别 | 官方定义 | 官方举例 |
|
||
|---|---|---|
|
||
| **ERR** | 用于**集群因某种原因无法完成其工作**的情况 | 我们尝试写入但返回了错误;尝试读取但因数据损坏读不出来;我们 scrub 了一个 PG 但数据不一致因而无法恢复 |
|
||
| **WRN** | 用于**集群能够处理**、但带有某种异常/负面因素的事件 | 服务的**临时降级**;**意外的内部值**;**可自动修复的**元数据错误;**慢操作(slow operation)** |
|
||
| **INFO** | 用于**不表示 Ceph 存在故障**的常规集群操作 | 官方特别强调:INFO 级消息**措辞清晰、不引起混淆或恐慌**尤为重要 |
|
||
|
||
#### 7.3.2 Frequency(频率规范)
|
||
|
||
核心要求:**所有级别的集群日志消息都不得过于频繁**。
|
||
|
||
官方给出的理由与规则:
|
||
|
||
| 规则 | 官方说明 |
|
||
|---|---|
|
||
| 消费者视角 | 消费者可能在使用一个**包含所有级别消息的滚动日志缓冲区**,因此**即便 DEBUG 消息过于频繁,也会干扰最新 INFO 消息的正常显示** |
|
||
| 坏状态 vs 坏事件 | 如果是「**坏状态**(a bad state)」而非「坏事件(event)」,**那是 health check 的职责**——**不要靠刷集群日志来表示一个持续的不健康状态** |
|
||
| 禁止随规模增长的消息 | **不要**为「随客户端数量或系统活动水平而扩展」的事件、或「正常操作中规律发生」的事件发送集群日志 |
|
||
|
||
官方举的两个**反例**:
|
||
|
||
- 每个新客户端连接都发一条 INFO(**随客户端数扩展**)
|
||
- 每次 CephFS 子树迁移都发一条 INFO(**正常操作中规律发生**)
|
||
|
||
#### 7.3.3 Language and formatting(语言与格式规范)
|
||
|
||
> 官方前置说明:这些准则对 **DEBUG 级消息的重要性远低于 INFO 及以上**;应把精力集中在让 INFO/WRN/ERR 消息尽可能易读上。
|
||
|
||
| 准则 | 官方要求 | 官方示例 |
|
||
|---|---|---|
|
||
| **语态** | 使用**被动语态** | 用 "Object xyz could not be read",而不是 "I could not read the object xyz" |
|
||
| **长/大标识符** | 如 inode 号,**以十六进制打印,加 `0x` 前缀**,让用户能看出是十六进制 | 理由:`0x` 使其无歧义(十进制没有等价写法),且十六进制形式更可能在一屏内放得下 |
|
||
| **大小量** | 以人类可读的 MB/GB 等打印,**数字后带单位**。**例外**:若指定的是**偏移量(offset)**,精度对含义至关重要,则可以按字节指定(但要按十六进制打印) | — |
|
||
| **单行** | 尽最大努力让消息**单行放得下**。不要求必然如此,但至少通常应如此。意味着:**除非列表中只有少数几项,否则不要打印列表** | — |
|
||
| **名词选择** | 使用**对用户有意义、且在文档中有定义**的名词。**常见缩写可以**——不要浪费屏幕空间打 "Rados Object Gateway" 而不是 RGW。**不要使用内部类名称**,如 "MDCache" 或 "Objecter" | 用 "OSD client requests" 而不是 "Objecter requests" |
|
||
| 内部结构的例外 | 如果内部结构体**是消息的直接主体**,可以提及——例如在**损坏(corruption)**的场景 | "Corrupt session table" 可以;但不要说 "Corrupt SessionTable" |
|
||
| **描述后果** | 在可能的情况下,**描述对系统可用性的后果**,而不只是描述底层状态 | 说 "myfs is degraded, waiting for myfs.0 to finish starting",而不是 "MDS myfs.0 is replaying" |
|
||
| **缩写 vs 截断** | 常见缩写可以,但**不要随意截断单词** | 是 "directory inode",不是 "dir ino" |
|
||
| **"不该发生" 的情况** | 如果记录的是一个「本应 assert 但我们没有崩溃」的情况,**要在措辞里说清楚**——这大概不是用户能自行修复的情况 | — |
|
||
| **避免行话** | 避免 UNIX/程序员黑话。用 "error" 而不是 "errno"(最好给出比数字更具描述性的东西) | — |
|
||
| **cluster map epoch** | **不要提及 cluster map epoch,除非它对消息含义至关重要** | "OSDMap epoch 123 is corrupt" 可以(epoch 就是消息的重点);"OSD 123 is down in OSDMap epoch 456" 不可以(osdmap 和 epoch 概念是实现细节,OSD 的 down 状态才是真正的消息)。**可以把额外细节送到守护进程的本地日志**(通过 `dout`/`derr`) |
|
||
| **问题的消失** | 如果你记录了一个**未来可能消失**的问题,**要确保当它消失时也记录一条**。**无论原消息用什么优先级记录,"消失" 消息一律记在 INFO** | — |
|
||
|
||
#### 7.3.4 注意事项
|
||
|
||
- 本页**不适用**于本地 dout 日志 —— 这是最常见的误解点。
|
||
- 「持续的不健康状态」应走 **health check**,不要刷集群日志。
|
||
- 需要给开发者看的细节,应走守护进程本地的 `dout`/`derr`,而不是集群日志。
|
||
|
||
> 引自:[Use of the cluster log](https://docs.ceph.com/en/tentacle/dev/logging/)
|
||
|
||
---
|
||
|
||
### 7.4 `logs.rst`:本地调试日志(dout)
|
||
|
||
> 官方该页共 55 行,标题 "Debug logs"。它讲两件事:**dout/derr 调试日志**,以及**性能计数器**。
|
||
|
||
#### 7.4.1 dout 日志机制
|
||
|
||
官方定义:**Ceph 的主要调试工具是 `dout` 和 `derr` 日志函数**,合称 **"dout logging"**。
|
||
|
||
**faculty(子系统)与级别**:
|
||
|
||
- dout 有若干 **log faculties**(日志子系统);
|
||
- 可以通过**配置管理系统**为不同 faculty 设置不同 log level;
|
||
- 官方例子:**只**想为 messenger 开启调试,就设 `debug_ms = 10`。
|
||
|
||
#### 7.4.2 短路优化:dout 宏的 `if` 包裹
|
||
|
||
官方强调一个重要的性能特性:**dout 宏会把「不会被使用的日志消息」包在一个 `if` 语句里,从而连生成都避免**。
|
||
|
||
含义(官方举例):
|
||
|
||
```cpp
|
||
dout(20) << "myfoo() = " << myfoo() << dendl;
|
||
```
|
||
|
||
如果 debug level 设为 0,那么 **`myfoo()` 在这里根本不会被调用**。
|
||
|
||
> 这是一个非常关键的实践点:`dout(N)` 的参数**不总是被求值**,因此**绝不能**把有副作用的代码写进 `dout(...)` 的参数里。
|
||
|
||
#### 7.4.3 dout 的性能代价(官方说明)
|
||
|
||
官方坦承:**debug logging 的性能相对较低**。原因:
|
||
|
||
| 原因 | 官方说明 |
|
||
|---|---|
|
||
| 全局锁 | 存在**一个进程级(process-wide)的 mutex**,**每条** debug 输出语句都要获取 |
|
||
| 系统调用 | **每条** debug 输出语句都会导致一次 **`write()` 系统调用**或一次 **`syslog()` 调用** |
|
||
| C++ 流开销 | 使用 C++ streams 也有计算开销 |
|
||
|
||
结论:官方建议 —— **你需要在日志上保持节俭(parsimonious)以获得最佳性能**。
|
||
|
||
#### 7.4.4 调试时的陷阱
|
||
|
||
官方提醒:**有时候开启日志会通过改变事件的时序而掩盖住竞态条件及其它 bug**(enabling logging can hide race conditions and other bugs by changing the timing of events)。调试时要把这点记在心里。
|
||
|
||
#### 7.4.5 Performance counters(性能计数器,本页版本)
|
||
|
||
官方该段描述:
|
||
|
||
- Ceph 守护进程用**性能计数器**跟踪关键统计信息,例如 **pinned inode 数量**。
|
||
- 性能计数器本质上是**一组整数和浮点数**,可通过 **PerfCounters API** 进行 set / increment / read。
|
||
|
||
**PerfCounters 对象特征**:
|
||
|
||
| 特征 | 说明 |
|
||
|---|---|
|
||
| 关联范围 | 通常**关联到单个子系统** |
|
||
| 内容 | 包含**多个计数器** |
|
||
| 线程安全 | 由**内部 mutex** 保护 |
|
||
| 数量 | 可以创建**多个** PerfCounters 对象 |
|
||
|
||
**当前支持的三种计数器类型**:
|
||
|
||
| 类型 | 创建函数 | 语义 |
|
||
|---|---|---|
|
||
| u64 counter | `PerfCountersBuilder::add_u64` | 单个 u64 值,可原子地更新、递增、读取 |
|
||
| float counter | `PerfCountersBuilder::add_fl` | 单个 float 值,同上 |
|
||
| long-run floating-point average counter | `PerfCountersBuilder::add_fl_avg` | 提供**两个值**:**当前总和(current total)** 与 **总和被改变的次数**;目的是提供**长期平均值** |
|
||
|
||
**读取方式**:性能计数器信息可从**管理套接字(admin_sock)** 以 **JSON 格式**读取。admin_sock 实现为 **UNIX domain socket**。
|
||
|
||
官方给出的两个参考实现:
|
||
|
||
- **Ceph performance counter plugin for collectd**:展示了如何访问这些信息;
|
||
- **administrative sockets 的单元测试**:另一个例子。
|
||
|
||
#### 7.4.6 注意事项
|
||
|
||
- `dout` 参数可能不被求值,勿写副作用。
|
||
- 日志开销来自「全局锁 + 每条一次 write()/syslog()」,高频热路径上写日志代价很高。
|
||
- 本页对 PerfCounters 的描述是**简化版**,更完整的 schema / dump / labeled counters 在 `perf_counters.rst`(见 7.7)。
|
||
|
||
> 引自:[Debug logs](https://docs.ceph.com/en/tentacle/dev/logs/)
|
||
|
||
---
|
||
|
||
### 7.5 `health-reports.rst`:健康报告
|
||
|
||
> 官方该页共 69 行。**注意**:官方在 "How to Get Reports" 一节写了 "The following diagrams outline the involved parties and how the interact when the clients query for the reports:",但**该页在源文件中并没有实际可见的图内容**(图表是外链图片,不在 rst 文本里)。因此**本册无法复现该图,只能按文字描述说明**——如实标注。
|
||
|
||
#### 7.5.1 获取报告的两种渠道(How to Get Reports)
|
||
|
||
| 渠道 | 具体方式 | 返回 |
|
||
|---|---|---|
|
||
| **ceph CLI** | 发送 **`health` mon 命令** | 集群健康状态 |
|
||
| **mgr module** | 调用 **`mgr.get('health')`** | **同样的报告**,但形式是 **JSON 编码的字符串** |
|
||
|
||
> 关于「客户端查询报告时各方如何交互」的示意图,**文档中的图未能从 rst 源文中提取**(源文仅有引导语,图以图片形式外链)。**文档未明确**其具体内容。
|
||
|
||
#### 7.5.2 报告在哪里生成:聚合者的聚合者(Aggregator of Aggregators)
|
||
|
||
**健康报告由多个 Paxos service 聚合而来**,官方列出的 7 个:
|
||
|
||
```text
|
||
- AuthMonitor
|
||
- HealthMonitor
|
||
- MDSMonitor
|
||
- MgrMonitor
|
||
- MgrStatMonitor
|
||
- MonmapMonitor
|
||
- OSDMonitor
|
||
```
|
||
|
||
**生成与持久化机制**(官方描述):
|
||
|
||
1. 上述每个 service 在**持久化自己域内的 pending changes** 时,会**识别健康相关问题**;
|
||
2. 然后**用同一个事务(the same transaction)**把它们以 **`health` 前缀**存入 **monstore**;
|
||
3. 这些报告**之后被加载和解码**,以便按需收集。
|
||
|
||
**官方举的 `OSDMonitor` 例子**:`OSDMonitor` 检查一个 pending 的新 osdmap 是否存在可能的问题,例如 **down OSDs**、**某个 pool 缺少 scrub flag**,然后把**编码后的健康报告**连同**新的 osdmap** 一起存储。
|
||
|
||
**官方举的 `MDSMonitor` 例子**:`MDSMonitor` **把健康指标持久化在 MDS 守护进程发来的 beacon 中**,并在存储 pending changes 时准备健康报告。
|
||
|
||
**给开发者的指引**:若想新增一个与 cephfs 相关的 warning,**最好的起点大概是 `MDSMonitor::encode_pending()`**——健康报告在那里从最新的 **`FSMap`** 以及 **MDS 守护进程上报的健康指标**中收集而来。
|
||
|
||
**重要例外(官方特别指出)**:**`MgrStatMonitor` 不自己准备报告,它只是存储从 mgr 收到的健康报告!**
|
||
|
||
#### 7.5.3 ceph-mgr:一个被委托的聚合者(A Delegate Aggregator)
|
||
|
||
**动机(官方叙述)**:
|
||
|
||
- Ceph 中创建 mgr 是为了**分担 monitor 的负担**——monitor 用于就「**对维持集群功能至关重要的信息**」建立共识。
|
||
- 显然 **osdmap、mdsmap、monmap** 属于这一类。
|
||
- 但**集群的聚合统计信息**呢?它们对管理员了解集群状态很关键,但对**维持集群运行**可能没那么重要。
|
||
- 为应对这个**可扩展性问题**,Ceph 把**收集和聚合指标的工作卸载(offloaded)给 mgr**。
|
||
|
||
**mgr 的职责(官方描述)**:
|
||
|
||
| 职责 | 说明 |
|
||
|---|---|
|
||
| 接收并处理 **MPGStats** 消息 | 来自 OSD |
|
||
| 通过 **MMgrReport** 收集指标 | 官方开发了一套协议,允许守护进程**周期性**向 mgr 上报其指标与状态 |
|
||
| 向 mon 发送聚合报告 | 在 mgr 侧,它**周期性**地向 mon 上的 **`MgrStatMonitor`** service 发送一份**聚合报告** |
|
||
|
||
如前所述,`MgrStatMonitor` **只是**把聚合报告中的健康报告持久化到 monstore。
|
||
|
||
#### 7.5.4 完整数据流(按官方叙述整理)
|
||
|
||
> 💡 补充:官方未给出一张完整的端到端流程图(图缺失),下面按文字叙述串联,属整理而非官方原图。
|
||
|
||
```text
|
||
OSD ──MPGStats────────────────────────┐
|
||
▼
|
||
守护进程 ──MMgrReport──────────────► mgr
|
||
│ 周期性聚合报告
|
||
▼
|
||
MgrStatMonitor (mon)
|
||
│ 仅持久化(不生成)
|
||
▼
|
||
monstore
|
||
|
||
AuthMonitor / HealthMonitor / MDSMonitor / MgrMonitor /
|
||
MonmapMonitor / OSDMonitor
|
||
│ 在 encode_pending() 时识别健康问题
|
||
│ 以 health 前缀,与自身数据同一事务写入
|
||
▼
|
||
monstore
|
||
|
||
monstore ──► (加载并解码) ──► 按需收集 ──► health mon 命令 ──► ceph CLI
|
||
└─► mgr.get('health') ──► mgr module (JSON)
|
||
```
|
||
|
||
#### 7.5.5 注意事项
|
||
|
||
- **报告分散在多个 Paxos service 的 `encode_pending()` 里**,排查「某条 health warning 从哪来」时,要先定位是哪个 Monitor。
|
||
- `MgrStatMonitor` 是**透传**,不是生成者。
|
||
- 该页**未**说明 health check 的注册机制(`health_check_t`、健康检查的编码格式、detail 的合并规则)——**文档未明确**。
|
||
- 该页**未**说明 `ceph health detail` 与 `health` mon 命令的差异。
|
||
|
||
> 引自:[Health Reports](https://docs.ceph.com/en/tentacle/dev/health-reports/)
|
||
|
||
---
|
||
|
||
### 7.6 `perf.rst`:使用 perf 做 CPU 采样
|
||
|
||
> 官方该页共 55 行,是**纯操作手册**,由 4 组命令 + 2 个注意事项构成。没有任何数据结构或内部实现说明。
|
||
|
||
#### 7.6.1 常用命令
|
||
|
||
**1. Top(实时热点)**:
|
||
|
||
```bash
|
||
sudo perf top -p `pidof ceph-osd`
|
||
```
|
||
|
||
**2. 带调用图采集数据**:
|
||
|
||
```bash
|
||
sudo perf record -p `pidof ceph-osd` -F 99 --call-graph dwarf -- sleep 60
|
||
```
|
||
|
||
参数含义(从命令本身读出,`-F 99` 为 99Hz 采样率,`--call-graph dwarf` 用 DWARF 调试信息展开调用栈,`-- sleep 60` 采集 60 秒)。
|
||
|
||
**3. 按 caller 查看**(可以看到每个 top 函数调用了什么):
|
||
|
||
```bash
|
||
sudo perf report --call-graph caller
|
||
```
|
||
|
||
**4. 按 callee 查看**(可以看到谁调用了每个 top 函数):
|
||
|
||
```bash
|
||
sudo perf report --call-graph callee
|
||
```
|
||
|
||
> **官方 note**:如果 caller/callee 视图**看起来一样**,你可能是**撞上了内核 bug**——**升级到 4.8 或更高版本**。
|
||
|
||
#### 7.6.2 Common Issues:帧指针被优化掉
|
||
|
||
**问题**:Ceph 默认使用 **`RelWithDebInfo`** 作为 `CMAKE_BUILD_TYPE`,因此用 **`-O2 -g`** 编译整棵树。而 **`-O2` 优化级别默认启用 `-fomit-frame-pointer`**。这会**阻止栈 profiler 访问完整的栈信息**。
|
||
|
||
**解法**(两种,官方给出):
|
||
|
||
1. 在启动 `cmake` 时禁用该选项:
|
||
|
||
```bash
|
||
cmake -DCMAKE_CXX_FLAGS="-fno-omit-frame-pointer"
|
||
```
|
||
|
||
2. 或者在构建整棵树时:
|
||
|
||
```bash
|
||
make CMAKE_CXX_FLAGS="-fno-omit-frame-pointer"
|
||
```
|
||
|
||
#### 7.6.3 Flamegraphs(火焰图)
|
||
|
||
**1. 准备工作**:
|
||
|
||
```bash
|
||
cd ~/src
|
||
git clone https://github.com/brendangregg/FlameGraph
|
||
```
|
||
|
||
**2. 运行 ceph,然后采集 perf 数据**:
|
||
|
||
```bash
|
||
sudo perf record -p `pidof ceph-osd` -F 99 --call-graph dwarf -- sleep 60
|
||
```
|
||
|
||
**3. 生成火焰图**:
|
||
|
||
```bash
|
||
sudo perf script | ~/src/FlameGraph/stackcollapse-perf.pl > /tmp/folded
|
||
~/src/FlameGraph/flamegraph.pl /tmp/folded > /tmp/perf.svg
|
||
firefox /tmp/perf.svg
|
||
```
|
||
|
||
#### 7.6.4 注意事项
|
||
|
||
- 默认 `RelWithDebInfo` 构建**必须**加 `-fno-omit-frame-pointer` 才能得到完整调用栈(或改用 `--call-graph dwarf`)。
|
||
- caller/callee 视图相同 → 内核 < 4.8 的问题。
|
||
- 该页**未**涉及 OProfile / LTTng / Blkin(分别在 7.11 与 7.12)。
|
||
|
||
> 引自:[Using perf](https://docs.ceph.com/en/tentacle/dev/perf/)
|
||
|
||
---
|
||
|
||
### 7.7 `perf_counters.rst`:性能计数器
|
||
|
||
> 官方该页共 253 行。相比 `logs.rst` 里的简述,本页给出完整的 **Access / Collections / Schema / Dump / Labeled Perf Counters** 说明。
|
||
|
||
#### 7.7.1 设计动机
|
||
|
||
perf counters 提供**通用的内部基础设施**,用于 **gauges(计量器)**和 **counters(计数器)**。被计数的值**既可以是整型也可以是浮点型**。
|
||
|
||
还有一种 **"average" 类型**(通常是 float),它**把一个 sum 计数器与一个 num 计数器结合起来**,二者相除即可得出平均值。
|
||
|
||
**数据的去向(官方意图)**:
|
||
|
||
| 环节 | 官方说明 |
|
||
|---|---|
|
||
| 采集 | 由 `collectd` 或 `statsd` 之类的工具采集并聚合 |
|
||
| 分析 | 送入 `graphite` 之类的工具做绘图和分析 |
|
||
| 官方也指出 | 参见 **mgr/prometheus** 与 **mgr/telemetry** |
|
||
|
||
**本地用途(官方列举)**:用户和开发者也可以**在本地**访问 perf counter 数据,用于:
|
||
|
||
- 检查集群整体健康状况
|
||
- 识别工作负载模式(workload patterns)
|
||
- 按守护进程类型监控集群性能
|
||
- 排查延迟(latency)、限流(throttling)、内存管理等问题
|
||
|
||
#### 7.7.2 Access(访问方式)
|
||
|
||
通过 **admin socket** 访问:
|
||
|
||
```bash
|
||
ceph daemon osd.0 perf schema
|
||
ceph daemon osd.0 perf dump
|
||
```
|
||
|
||
#### 7.7.3 Collections(集合)
|
||
|
||
- 值被分组进**命名的 collection**,通常代表**一个子系统**或**一个子系统的一个实例**。
|
||
- 官方例子:内部 **`throttle`** 机制报告其限流情况的统计信息,每个实例命名形如:
|
||
|
||
```text
|
||
throttle-msgr_dispatch_throttler-hbserver
|
||
throttle-msgr_dispatch_throttler-client
|
||
throttle-filestore_bytes
|
||
...
|
||
```
|
||
|
||
#### 7.7.4 Schema:`type` 位域(bitfield)
|
||
|
||
`perf schema` 命令导出一份 JSON 描述,说明**有哪些值可用、它们的类型是什么**。每个命名值都有一个 **`type` 位域**,官方定义的位如下:
|
||
|
||
| bit | 含义 |
|
||
|---|---|
|
||
| 1 | floating point value(浮点值) |
|
||
| 2 | unsigned 64-bit integer value(无符号 64 位整型值) |
|
||
| 4 | average (sum + count pair)(平均值:sum + count 对) |
|
||
| 8 | counter (vs gauge)(计数器,相对于计量器) |
|
||
|
||
**解读规则(官方说明)**:
|
||
|
||
| 规则 | 说明 |
|
||
|---|---|
|
||
| 类型 | **每个值都会有 bit 1 或 bit 2 之一被置位**,用以指示类型(float 或 integer) |
|
||
| bit 8(counter) | 若置位,则该值**单调递增**,读取方**可能想要减去上一次读到的值**,以得到上一interval 的增量(delta) |
|
||
| bit 4(average) | 若置位,则有**两个值**要读:一个 **sum** 和一个 **count**。若它同时是 counter,则**上一 interval 的平均值 = sum 的 delta ÷ count 的 delta**;**另一种做法是直接相除,得到的是生命周期平均值(lifetime average)**。官方补充:这些通常用于**测量延迟**(请求数 + 请求延迟之和),而**上一 interval 的平均值才是有意思的那个** |
|
||
|
||
**比位域更友好的替代属性**(官方说明):与其自己解释位域,不如看:
|
||
|
||
| 属性 | 取值 |
|
||
|---|---|
|
||
| `metric type` | `gauge` 或 `counter` |
|
||
| `value type` | `real`、`integer`、`real-integer-pair`(sum + real count 对)、`integer-integer-pair`(sum + integer count 对) |
|
||
|
||
#### 7.7.5 Schema 输出示例(官方完整示例)
|
||
|
||
```json
|
||
{
|
||
"throttle-bluestore_throttle_bytes": {
|
||
"val": {
|
||
"type": 2,
|
||
"metric_type": "gauge",
|
||
"value_type": "integer",
|
||
"description": "Currently available throttle",
|
||
"nick": ""
|
||
},
|
||
"max": {
|
||
"type": 2,
|
||
"metric_type": "gauge",
|
||
"value_type": "integer",
|
||
"description": "Max value for throttle",
|
||
"nick": ""
|
||
},
|
||
"get_started": {
|
||
"type": 10,
|
||
"metric_type": "counter",
|
||
"value_type": "integer",
|
||
"description": "Number of get calls, increased before wait",
|
||
"nick": ""
|
||
},
|
||
"get": {
|
||
"type": 10,
|
||
"metric_type": "counter",
|
||
"value_type": "integer",
|
||
"description": "Gets",
|
||
"nick": ""
|
||
},
|
||
"get_sum": {
|
||
"type": 10,
|
||
"metric_type": "counter",
|
||
"value_type": "integer",
|
||
"description": "Got data",
|
||
"nick": ""
|
||
},
|
||
"get_or_fail_fail": {
|
||
"type": 10,
|
||
"metric_type": "counter",
|
||
"value_type": "integer",
|
||
"description": "Get blocked during get_or_fail",
|
||
"nick": ""
|
||
},
|
||
"get_or_fail_success": {
|
||
"type": 10,
|
||
"metric_type": "counter",
|
||
"value_type": "integer",
|
||
"description": "Successful get during get_or_fail",
|
||
"nick": ""
|
||
},
|
||
"take": {
|
||
"type": 10,
|
||
"metric_type": "counter",
|
||
"value_type": "integer",
|
||
"description": "Takes",
|
||
"nick": ""
|
||
},
|
||
"take_sum": {
|
||
"type": 10,
|
||
"metric_type": "counter",
|
||
"value_type": "integer",
|
||
"description": "Taken data",
|
||
"nick": ""
|
||
},
|
||
"put": {
|
||
"type": 10,
|
||
"metric_type": "counter",
|
||
"value_type": "integer",
|
||
"description": "Puts",
|
||
"nick": ""
|
||
},
|
||
"put_sum": {
|
||
"type": 10,
|
||
"metric_type": "counter",
|
||
"value_type": "integer",
|
||
"description": "Put data",
|
||
"nick": ""
|
||
},
|
||
"wait": {
|
||
"type": 5,
|
||
"metric_type": "gauge",
|
||
"value_type": "real-integer-pair",
|
||
"description": "Waiting latency",
|
||
"nick": ""
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**对照位域表读数**:
|
||
|
||
| type 值 | 二进制 | 解读 |
|
||
|---|---|---|
|
||
| 2 | 0010 | bit 2 → unsigned 64-bit integer,gauge |
|
||
| 10 | 1010 | bit 8 + bit 2 → unsigned 64-bit integer,counter |
|
||
| 5 | 0101 | bit 4 + bit 1 → average (sum+count),value_type 为 `real-integer-pair` |
|
||
|
||
#### 7.7.6 Dump 输出示例
|
||
|
||
官方说明:**实际 dump 与 schema 类似,区别是 average 值会被分组**。示例:
|
||
|
||
```json
|
||
{
|
||
"throttle-msgr_dispatch_throttler-hbserver" : {
|
||
"get_or_fail_fail" : 0,
|
||
"get_sum" : 0,
|
||
"max" : 104857600,
|
||
"put" : 0,
|
||
"val" : 0,
|
||
"take" : 0,
|
||
"get_or_fail_success" : 0,
|
||
"wait" : {
|
||
"avgcount" : 0,
|
||
"sum" : 0
|
||
},
|
||
"get" : 0,
|
||
"take_sum" : 0,
|
||
"put_sum" : 0
|
||
},
|
||
"throttle-msgr_dispatch_throttler-client" : {
|
||
"get_or_fail_fail" : 0,
|
||
"get_sum" : 82760,
|
||
"max" : 104857600,
|
||
"put" : 2637,
|
||
"val" : 0,
|
||
"take" : 0,
|
||
"get_or_fail_success" : 0,
|
||
"wait" : {
|
||
"avgcount" : 0,
|
||
"sum" : 0
|
||
},
|
||
"get" : 2637,
|
||
"take_sum" : 0,
|
||
"put_sum" : 82760
|
||
}
|
||
}
|
||
```
|
||
|
||
注意 `wait` 展开为 `{"avgcount": ..., "sum": ...}` 两个字段,正是 bit 4(average)的 sum + count 对。
|
||
|
||
#### 7.7.7 Labeled Perf Counters(带标签的性能计数器)
|
||
|
||
> **官方 note**:**Labeled perf counters 在 Ceph 的 Reef 版本中引入。**
|
||
|
||
**动机**:一个 Ceph 守护进程有能力**导出一组带不同 label 的 perf counter 实例**。这些计数器旨在用于**在 Prometheus 和 Grafana 等第三方工具中可视化特定指标**。
|
||
|
||
官方示例:下面这些计数器展示了**不同用户在不同 bucket 上**的 put 请求数:
|
||
|
||
```json
|
||
{
|
||
"rgw": [
|
||
{
|
||
"labels": {
|
||
"Bucket: "bkt1",
|
||
"User: "user1",
|
||
},
|
||
"counters": {
|
||
"put": 1,
|
||
},
|
||
},
|
||
{
|
||
"labels": {},
|
||
"counters": {
|
||
"put": 4,
|
||
},
|
||
},
|
||
{
|
||
"labels": {
|
||
"Bucket: "bkt1",
|
||
"User: "user2",
|
||
},
|
||
"counters": {
|
||
"put": 3,
|
||
},
|
||
},
|
||
]
|
||
}
|
||
```
|
||
|
||
**访问命令(官方列出)**:
|
||
|
||
| 命令 | 作用 |
|
||
|---|---|
|
||
| `ceph daemon {daemon id} counter dump` | 查看**所有** labeled **和** unlabeled perf counters |
|
||
| `ceph daemon {daemon id} counter schema` | 查看**所有** labeled **和** unlabeled perf counter 的 schema |
|
||
| `ceph daemon {daemon id} perf dump` | **仅** non-labeled(上例中不带 labels 的那个计数器会出现在这里) |
|
||
| `ceph daemon {daemon id} perf schema` | 同上,仅 non-labeled |
|
||
|
||
> 上例中的第二个计数器(`"labels": {}`)就是那种**同时也会出现在 `perf dump` 中**的计数器。
|
||
|
||
**官方的弃用建议(重要)**:
|
||
|
||
- 由于 `counter dump` 和 `counter schema` **既能看带标签的也能看不带标签的**,因此**不推荐**再使用 `perf dump` 和 `perf schema` 命令——它们**仅为向后兼容而保留**,且**继续只输出 non-labeled 计数器**。
|
||
- **某些**目前通过 `perf dump` / `perf schema` 导出的 perf counter,**未来版本中可能变成 labeled**,届时它们将**不再**被 `perf dump` / `perf schema` 输出。
|
||
|
||
#### 7.7.8 注意事项
|
||
|
||
- 计算「速率 / 区间平均」时,对 counter 类型要**做 delta**,不能直接用原值。
|
||
- 新代码/新脚本应优先用 `counter dump` / `counter schema`。
|
||
- 该页**未**给出 `PerfCountersBuilder` 的完整 API 列表(仅 `logs.rst` 提到 `add_u64` / `add_fl` / `add_fl_avg`),也**未**说明 labeled counter 的 label 基数爆炸风险处理——**文档未明确**。
|
||
|
||
> 引自:[Perf counters](https://docs.ceph.com/en/tentacle/dev/perf_counters/)
|
||
|
||
---
|
||
|
||
### 7.8 `perf_histograms.rst`:性能直方图
|
||
|
||
> 官方该页共 677 行,但**其中约 500 行是 dump 输出示例**(大量重复的 0 值数组)。实质内容集中在开头的 Access / Collections / Schema 与结尾的说明段。
|
||
|
||
#### 7.8.1 设计动机
|
||
|
||
- perf histograms **构建在 perf counters 基础设施之上**。
|
||
- 直方图是为若干计数器构建的,用于**简化「哪些计数器值分组随时间出现得最频繁」**的数据收集。
|
||
- perf histograms **目前是无符号 64 位整型计数器**,因此**主要用于时间和大小**(time and sizes)。
|
||
- 由 perf histogram 导出的数据可以**再喂给其它分析工具/脚本**。
|
||
|
||
#### 7.8.2 Access(访问方式)
|
||
|
||
```bash
|
||
ceph daemon osd.0 perf histogram schema
|
||
ceph daemon osd.0 perf histogram dump
|
||
```
|
||
|
||
#### 7.8.3 Collections(集合)
|
||
|
||
与 perf counters 一样,直方图按**命名的 collection** 分组,通常代表一个子系统或子系统的一个实例。官方例子:
|
||
|
||
```text
|
||
op_r_latency_out_bytes_histogram
|
||
op_rw_latency_in_bytes_histogram
|
||
op_rw_latency_out_bytes_histogram
|
||
...
|
||
```
|
||
|
||
#### 7.8.4 Schema:`type` 位域
|
||
|
||
`perf histogram schema` 导出 JSON 描述。每个命名值有一个 `type` 位域,**第 5 位(bit 16,值为 16)总是被置位**,其余位定义如下:
|
||
|
||
| bit | 含义 |
|
||
|---|---|
|
||
| 1 | floating point value |
|
||
| 2 | unsigned 64-bit integer value |
|
||
| 4 | average (sum + count pair) |
|
||
| 8 | counter (vs gauge) |
|
||
|
||
官方给出的解读例子:**type "18" 就是一个 unsigned 64-bit integer 值的直方图(16 + 2)**。
|
||
|
||
Schema 输出示例(官方完整给出):
|
||
|
||
```json
|
||
{
|
||
"AsyncMessenger::Worker-0": {},
|
||
"AsyncMessenger::Worker-1": {},
|
||
"AsyncMessenger::Worker-2": {},
|
||
"mutex-WBThrottle::lock": {},
|
||
"objecter": {},
|
||
"osd": {
|
||
"op_r_latency_out_bytes_histogram": {
|
||
"type": 18,
|
||
"description": "Histogram of operation latency (including queue time) + data read",
|
||
"nick": ""
|
||
},
|
||
"op_w_latency_in_bytes_histogram": {
|
||
"type": 18,
|
||
"description": "Histogram of operation latency (including queue time) + data written",
|
||
"nick": ""
|
||
},
|
||
"op_rw_latency_in_bytes_histogram": {
|
||
"type": 18,
|
||
"description": "Histogram of rw operation latency (including queue time) + data written",
|
||
"nick": ""
|
||
},
|
||
"op_rw_latency_out_bytes_histogram": {
|
||
"type": 18,
|
||
"description": "Histogram of rw operation latency (including queue time) + data read",
|
||
"nick": ""
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
> 观察:`AsyncMessenger::Worker-0/1/2`、`mutex-WBThrottle::lock`、`objecter` 这几个 collection 在示例中是**空对象 `{}`**(无直方图),只有 `osd` collection 下有 4 个直方图。
|
||
|
||
**四个直方图的语义(按 description)**:
|
||
|
||
| 名称 | 描述 |
|
||
|---|---|
|
||
| `op_r_latency_out_bytes_histogram` | 读操作延迟(含排队时间)+ 读出数据量 |
|
||
| `op_w_latency_in_bytes_histogram` | 写操作延迟(含排队时间)+ 写入数据量 |
|
||
| `op_rw_latency_in_bytes_histogram` | 读写操作延迟(含排队时间)+ 写入数据量 |
|
||
| `op_rw_latency_out_bytes_histogram` | 读写操作延迟(含排队时间)+ 读出数据量 |
|
||
|
||
#### 7.8.5 Dump:二维直方图结构
|
||
|
||
官方说明:**实际 dump 与 schema 类似,区别在于有实际的值分组**。
|
||
|
||
**Axis(轴)的字段语义**(从官方示例与结尾说明归纳):
|
||
|
||
| 字段 | 示例值 | 官方说明 |
|
||
|---|---|---|
|
||
| `name` | `Latency (usec)` / `Request size (bytes)` | 轴名 |
|
||
| `min` | `0` | **最小可接受值**("Min" is a minimum accepted value) |
|
||
| `quant_size` | `100000` / `512` | **量化单位**(quantization unit) |
|
||
| `buckets` | `32` | **值分组(buckets)的数量**("Buckets" denote amount of value groups) |
|
||
| `scale_type` | `log2` / `linear` | **`log2`(对数刻度)或 `linear`(线性刻度)** |
|
||
| `ranges` | 数组 | **每个值分组的取值边界**("Ranges" element denote value bounds for each of value groups) |
|
||
|
||
**Latency 轴的 ranges 结构(官方示例,32 buckets)**:
|
||
|
||
```text
|
||
{ "max": -1 } ← 下溢桶(underflow)
|
||
{ "min": 0, "max": 99999 } ← bucket 1
|
||
{ "min": 100000, "max": 199999 } ← bucket 2
|
||
{ "min": 200000, "max": 399999 } ← bucket 3(宽度开始翻倍)
|
||
{ "min": 400000, "max": 799999 }
|
||
{ "min": 800000, "max": 1599999 }
|
||
...(log2 递增)
|
||
{ "min": 26843545600000, "max": 53687091199999 }
|
||
{ "min": 53687091200000 } ← 上溢桶(overflow,只有 min,无 max)
|
||
```
|
||
|
||
**Request size 轴的 ranges(同样 32 buckets,`quant_size: 512`)**:
|
||
|
||
```text
|
||
{ "max": -1 }
|
||
{ "min": 0, "max": 511 }
|
||
{ "min": 512, "max": 1023 }
|
||
{ "min": 1024, "max": 2047 }
|
||
...(log2 递增)
|
||
{ "min": 137438953472, "max": 274877906943 }
|
||
{ "min": 274877906944 } ← 上溢桶
|
||
```
|
||
|
||
**values 结构**:`values` 是一个**二维数组**,官方在说明段给出结论:
|
||
|
||
> 「This represents the **2D histogram**, consisting of **9 history entries and 32 value groups per each history entry**.」
|
||
|
||
即:
|
||
|
||
| 维度 | 数量 | 说明 |
|
||
|---|---|---|
|
||
| 第一维 | **9** | history entries(历史条目,即按时间保留的 9 份快照) |
|
||
| 第二维 | **32** | value groups(与 `buckets` 对应的值分组) |
|
||
|
||
> 💡 补充:官方示例 JSON 中 `values` 数组实际只展示了 8 个子数组(每个 32 个 0),但该页结尾文字明确写的是 "9 history entries"。**以官方文字说明为准,示例 JSON 疑为截断或省略**。
|
||
|
||
#### 7.8.6 可视化工具
|
||
|
||
官方指出:可以使用 **`histogram_dump.py`** 工具(见 **`src/tools/histogram_dump.py`**)**快速可视化**已有的直方图数据。
|
||
|
||
#### 7.8.7 注意事项
|
||
|
||
- 直方图**只能**是 u64(当前),不适合负值或浮点指标。
|
||
- 横向是 **32 个 log2 bucket + 2 个溢出桶**,纵向保留 **9 个历史条目**,因此是「随时间演进的二维分布」。
|
||
- 该页**未**说明「9 个 history entry」的滚动周期/触发条件 —— **文档未明确**。
|
||
- 该页**未**说明如何在代码中**新增**一个 perf histogram(无 API 说明)——**文档未明确**。
|
||
|
||
> 引自:[Perf histograms](https://docs.ceph.com/en/tentacle/dev/perf_histograms/)
|
||
|
||
---
|
||
|
||
### 7.9 `mempool_accounting.rst`:内存池记账
|
||
|
||
> 官方该页共 49 行,是一份**清单式说明**:什么是 mempool、能追踪哪些常见 mempool、怎么查看。
|
||
|
||
#### 7.9.1 什么是 mempool(官方定义)
|
||
|
||
一个 **memory pool (mempool)** 是一种**追踪内存消耗的方法**。
|
||
|
||
| 特征 | 官方说明 |
|
||
|---|---|
|
||
| 代表对象 | mempool 代表 **C++ 类和容器的内存消耗** |
|
||
| 用途 | 用于**评估内存泄漏**以及其它关于内存使用的洞察,且**开销低(low overhead)** |
|
||
| 追踪内容 | 每个 memory pool 追踪它包含的 **字节数(bytes)** 和 **条目数(items)** |
|
||
| 声明方式 | mempool 是**静态声明**的(statically declared) |
|
||
| 特殊用途 | 它们服务于「**在 BlueStore 中识别与内存相关的检查项**」这一目的 |
|
||
|
||
#### 7.9.2 常见可追踪的 mempool(官方完整清单)
|
||
|
||
| mempool | 官方说明 |
|
||
|---|---|
|
||
| `bloom_filter` | 追踪**已在 cache pool 中的对象**,以判断池中哪些对象正被访问 |
|
||
| `bluestore_alloc` | 统计 allocator **实际完成的分配** |
|
||
| `bluestore_inline_bl` | 度量**内联 buffer 的编码长度** |
|
||
| `bluestore_fsck` | **BlueStore 元数据的文件系统一致性检查**;在 OSD 修复期间帮助追踪和调试 |
|
||
| `bluestore_txc` | 统计**已提交事务(committed transactions)计数器** |
|
||
| `bluestore_writing_deferred` | 度量**被延迟的小写**——即先写入 RocksDB WAL、之后再 flush 到磁盘 |
|
||
| `bluestore_writing` | 统计**在途(in-flight)写 buffer** |
|
||
| `bluefs` | **类文件系统接口**;提供刚好够用的功能,使 RocksDB 能存储它的「文件」并与 BlueStore **共享同一批裸设备** |
|
||
| `buffer_anon` | 存放**任意 buffer 数据** |
|
||
| `buffer_meta` | 与 buffer anon buffer 关联的**所有元数据** |
|
||
| `bluestore_cache_data` | 用于 writing 和 writing deferred 的 mempool |
|
||
| `bluestore_cache_onode` | BlueStore cache 中的**对象节点(onode)元数据** |
|
||
| `bluestore_cache_meta` | `PREFIX_OBJ` 下我们存放的 key |
|
||
| `bluestore_cache_other` | 目前统计:见下三项 |
|
||
| ↳ `map_t` | 用于追踪 **SharedBlob 的磁盘裸 extent**,以及**带 blob namespace 的内存 Blob** |
|
||
| ↳ `coll_map` | `collections_map` |
|
||
| ↳ `csum_data` | **校验和数据** |
|
||
| `bluestore_cache_buffer` | 统计 **buffer cache shards** |
|
||
| `bluestore_extent` | 一个**逻辑(同时也是物理)extent**,指向某个 blob 的一部分 |
|
||
| `bluestore_blob` | 与 cached buffer 关联的**内存 blob 元数据** |
|
||
| `bluestore_shared_blob` | **内存中的 shared blob 状态**;存放**它所归属的 collection 集合的引用**(包含 cached buffers) |
|
||
| `bluefs_file_reader` | 统计 **bluefs file reader buffer** |
|
||
| `bluefs_file_writer` | 统计 **bluefs file writer buffer** |
|
||
|
||
#### 7.9.3 查看方式
|
||
|
||
```bash
|
||
$ ceph daemon osd.NNN dump_mempools
|
||
```
|
||
|
||
> 官方 note 指向更详细的源码:https://github.com/ceph/ceph/blob/main/src/include/mempool.h
|
||
|
||
#### 7.9.4 注意事项
|
||
|
||
- mempool 是**静态声明**的——不能运行时新增;新增 mempool 需要改代码(**文档未明确**具体改法,指向 `src/include/mempool.h`)。
|
||
- 清单**以 BlueStore 为主**(这是官方口径:「serve the purpose of identifying memory-related checks in BlueStore」),非 BlueStore 子系统的 mempool 覆盖情况**文档未明确**。
|
||
- 该页**未**说明 `dump_mempools` 的输出格式细节。
|
||
|
||
> 引自:[What is a mempool?](https://docs.ceph.com/en/tentacle/dev/mempool_accounting/)
|
||
|
||
---
|
||
|
||
### 7.10 `cpu-profiler.rst`:OProfile 安装与使用
|
||
|
||
> 官方该页共 46 行,标题 "Installing Oprofile",是**纯操作手册**。官方开篇即说:**「分析 Ceph CPU 消耗最简单的方法是使用 `oprofile` 这个系统级 profiler。」**
|
||
|
||
> 💡 补充:该页内容较旧(引用 `do-cmake.sh`、`git clean -dfx` 等老式构建流程),与 `perf.rst` 的现代做法并存。按原文如实记录。
|
||
|
||
#### 7.10.1 安装
|
||
|
||
Debian/Ubuntu 发行版:
|
||
|
||
```bash
|
||
sudo apt-get install oprofile oprofile-gui
|
||
```
|
||
|
||
#### 7.10.2 为 Profiling 编译 Ceph
|
||
|
||
**第一步:先清理一切。**
|
||
|
||
```bash
|
||
git clean -dfx
|
||
```
|
||
|
||
**第二步:编译 Ceph。**
|
||
|
||
```bash
|
||
./do-cmake.sh -DCMAKE_CXX_FLAGS="-fno-omit-frame-pointer -O2 -g"
|
||
cd build
|
||
cmake --build .
|
||
```
|
||
|
||
官方说明:命令中指定了 `CMAKE_CXX_FLAGS`,**这提供了 callgraph 输出(This provides callgraph output)**。
|
||
|
||
> 注意与 7.6 的呼应:核心都是为了**关掉 `-fomit-frame-pointer`**(`-O2` 默认开启)以获得完整调用栈。
|
||
|
||
#### 7.10.3 Ceph 配置
|
||
|
||
- 确保**禁用 `lockdep`**。
|
||
- 考虑**把日志级别设为适合生产集群的水平**(官方指向 `Ceph Logging and Debugging` 文档)。
|
||
- 关于**如何使用 OProfile**,官方指向 RADOS Troubleshooting 文档的 **`CPU Profiling`** 一节。
|
||
|
||
#### 7.10.4 注意事项
|
||
|
||
- 该页**未**给出 OProfile 的实际采集与分析命令(如 `operf`、`opreport`),一律指向 troubleshooting 文档 —— **本页范围内文档未明确**。
|
||
- 该页使用的 `do-cmake.sh` 是较老的构建入口;Tentacle 时代通常用 `do_cmake.sh` / `./do_cmake.sh`(**文档未明确**此差异)。
|
||
|
||
> 引自:[Installing Oprofile](https://docs.ceph.com/en/tentacle/dev/cpu-profiler/)
|
||
|
||
---
|
||
|
||
### 7.11 `corpus.rst`:编码语料库(对象 corpus)
|
||
|
||
> 官方该页共 100 行,是**面向 Ceph 开发者**的测试基础设施说明。它与第 3.5 节的 `encoding.rst`(序列化兼容性)**直接配套**:corpus 就是「历史版本的编码样本库」,用来验证新代码还能不能解码旧对象。
|
||
|
||
#### 7.11.1 设计动机
|
||
|
||
为了保证**跨版本的编码兼容性**,需要一个**存档(archive)**:保存来自特定版本的**已编码对象样本**。测试时拿新代码去解码这些旧样本,就能发现兼容性倒退。
|
||
|
||
#### 7.11.2 Corpus 结构
|
||
|
||
`ceph.git/ceph-object-corpus` 是一个 **submodule**。其目录布局:
|
||
|
||
```text
|
||
bin/ # misc scripts
|
||
archive/$version/objects/$type/$hash # 来自特定版本的一批已编码对象样本
|
||
```
|
||
|
||
#### 7.11.3 标记已知/故意的不兼容
|
||
|
||
```text
|
||
archive/$version/forward_incompat/$type
|
||
```
|
||
|
||
官方说明:**该文件的存在表示:新版本的代码无法跨过该 `$version` 解码旧对象**(this is normally the case,通常正是如此)。
|
||
|
||
#### 7.11.4 生成对象 corpus 的完整流程(官方 10 步)
|
||
|
||
官方说明:可以用 **`script/gen-corpus.sh`** 脚本生成,也可以按下面的步骤手工完成。
|
||
|
||
**第 1 步:checkout 一个干净的仓库**(最好别在你日常工作的目录里做)
|
||
|
||
```bash
|
||
git clone ceph.git
|
||
cd ceph
|
||
git submodule update --init --recursive --progress
|
||
```
|
||
|
||
**第 2 步:带 dump 对象的 flag 构建**(dump 到 `/tmp/foo`)
|
||
|
||
```bash
|
||
rm -rf /tmp/foo ; mkdir /tmp/foo
|
||
do_cmake.sh -DCMAKE_CXX_FLAGS="-DENCODE_DUMP_PATH=/tmp/foo"
|
||
cd build
|
||
make
|
||
```
|
||
|
||
**第 3 步:通过 vstart 启动集群**
|
||
|
||
```bash
|
||
cd build
|
||
MON=3 MGR=2 OSD=3 MDS=3 RGW=1 ../src/vstart.sh -n -x
|
||
```
|
||
|
||
**第 4 步:尽可能多地使用集群功能,以触发尽可能多的对象 encoder 方法**
|
||
|
||
```bash
|
||
bin/ceph osd pool create mypool
|
||
bin/rados -p mypool bench 10 write -b 123
|
||
bin/ceph osd out 0
|
||
bin/ceph osd in 0
|
||
bin/init-ceph restart osd.1
|
||
for f in ../qa/workunits/cls/*.sh ; do PATH="bin:$PATH" $f ; done
|
||
PATH="bin:$PATH" ../qa/workunits/rados/test.sh
|
||
bin/ceph_test_librbd
|
||
bin/ceph_test_libcephfs
|
||
bin/init-ceph restart mds.a
|
||
../qa/workunits/rgw/run-s3tests.sh
|
||
```
|
||
|
||
**第 5 步:停止集群**
|
||
|
||
```bash
|
||
../src/stop.sh
|
||
```
|
||
|
||
**第 6 步:导入 corpus(这会花几分钟)**
|
||
|
||
```bash
|
||
../src/test/encoding/import.sh /tmp/foo `bin/ceph-dencoder version` ../ceph-object-corpus/archive
|
||
../src/test/encoding/import-generated.sh ../ceph-object-corpus/archive
|
||
```
|
||
|
||
**第 7 步:剪枝(Prune)**
|
||
|
||
官方说明:**会有 bazillion 份各种对象的拷贝,我们只想要一个有代表性的样本。**
|
||
|
||
```bash
|
||
pushd ../ceph-object-corpus
|
||
bin/prune-archive.sh
|
||
popd
|
||
```
|
||
|
||
**第 8 步:验证测试通过**
|
||
|
||
```bash
|
||
ctest -R readable.sh
|
||
```
|
||
|
||
**第 9 步:提交到 corpus 仓库并推送**
|
||
|
||
```bash
|
||
pushd ../ceph-object-corpus
|
||
git checkout -b wip-new
|
||
git add archive/`../build/bin/ceph-dencoder version`
|
||
git commit -m `../build/bin/ceph-dencoder version`
|
||
git remote add cc git@github.com:ceph/ceph-object-corpus.git
|
||
git push cc wip-new
|
||
popd
|
||
```
|
||
|
||
**第 10 步:去测试它**
|
||
|
||
```bash
|
||
cd my/regular/tree
|
||
cd ceph-object-corpus
|
||
git fetch origin
|
||
git checkout wip-new
|
||
cd ../build
|
||
ctest -R readable.sh
|
||
```
|
||
|
||
**收尾**:如果一切正常,**更新 submodule 的 master 分支,并在 ceph.git 中提交该 submodule**。
|
||
|
||
#### 7.11.5 关键路径小结
|
||
|
||
| 环节 | 路径 / 命令 |
|
||
|---|---|
|
||
| corpus submodule | `ceph.git/ceph-object-corpus` |
|
||
| 一键生成脚本 | `script/gen-corpus.sh` |
|
||
| 编码 dump 宏 | `-DENCODE_DUMP_PATH=/tmp/foo` |
|
||
| 导入脚本 | `src/test/encoding/import.sh`、`src/test/encoding/import-generated.sh` |
|
||
| 剪枝脚本 | `ceph-object-corpus/bin/prune-archive.sh` |
|
||
| 验证测试 | `ctest -R readable.sh` |
|
||
| 版本串来源 | `bin/ceph-dencoder version` |
|
||
|
||
#### 7.11.6 注意事项
|
||
|
||
- corpus 是 **submodule**,改动要**两个仓库分别提交**。
|
||
- 第 4 步「尽可能多地使用功能」**直接决定了兼容性测试的覆盖面**——用得少,样本就少。
|
||
- 官方明确「通常新代码无法解码旧对象」(forward_incompat 是常态),这与 `encoding.rst` 里 n-2 / n-3 的兼容窗口是两回事:**corpus 存档是长期保留,向前不兼容是被显式标记的**。
|
||
- 该页**未**说明 `readable.sh` 测试的具体判定逻辑 —— **文档未明确**。
|
||
|
||
> 引自:[Corpus structure](https://docs.ceph.com/en/tentacle/dev/corpus/)
|
||
|
||
---
|
||
|
||
### 7.12 `blkin.rst`:用 LTTng / Blkin 追踪 Ceph
|
||
|
||
> 官方该页共 225 行,实际包含**两个主题**:上半部是 **LTTng 追踪**(现代、推荐),下半部是 **Blkin**(已废弃)。
|
||
|
||
> ⚠️ **官方废弃声明(原文)**:`.. deprecated:: This feature was deprecated in the Squid release and will be removed in a later release.` —— **Blkin 在 Squid 版本被废弃,将在后续版本移除**。本页保留 LTTng 部分仍有价值。
|
||
|
||
#### 7.12.1 第一部分:用 LTTng 追踪 Ceph
|
||
|
||
**编译配置**
|
||
|
||
若自己编译代码,请使用 `-DWITH_LTTNG` 选项(**默认 ON**):
|
||
|
||
```bash
|
||
./do_cmake -DWITH_LTTNG=ON
|
||
```
|
||
|
||
**包依赖(重要)**
|
||
|
||
如果你的 Ceph 部署是**基于包(YUM / DNF / APT)而非容器化**,需要根据要追踪的模块安装相应软件包,否则**可能因为缺少 `*.tp.so` library 文件而导致 coredump**:
|
||
|
||
```text
|
||
librbd-devel
|
||
librgw-devel
|
||
librados-devel
|
||
```
|
||
|
||
**配置项(必须在 ceph.conf 中设为 true)**
|
||
|
||
官方列出的当前可用选项:
|
||
|
||
| 配置项 | 备注 |
|
||
|---|---|
|
||
| `bluestore_tracing` | — |
|
||
| `event_tracing` | 需要 `-DWITH_EVENTTRACE` |
|
||
| `osd_function_tracing` | 需要 `-DWITH_OSD_INSTRUMENT_FUNCTIONS` |
|
||
| `osd_objectstore_tracing` | 官方注明:**actually filestore tracing**(实为 filestore 追踪) |
|
||
| `rbd_tracing` | — |
|
||
| `osd_tracing` | — |
|
||
| `rados_tracing` | — |
|
||
| `rgw_op_tracing` | — |
|
||
| `rgw_rados_tracing` | — |
|
||
|
||
**测试追踪的完整流程(官方 7 步)**
|
||
|
||
1. 启动 LTTng 守护进程:
|
||
|
||
```bash
|
||
lttng-sessiond --daemonize
|
||
```
|
||
|
||
2. 带 trace 选项运行 vstart 集群:
|
||
|
||
```bash
|
||
../src/vstart.sh -d -n -l -e -o "osd_tracing = true"
|
||
```
|
||
|
||
3. 列出可用 tracepoint:
|
||
|
||
```bash
|
||
lttng list --userspace
|
||
```
|
||
|
||
输出形如:
|
||
|
||
```text
|
||
UST events:
|
||
-------------
|
||
PID: 100859 - Name: /path/to/ceph-osd
|
||
pg:queue_op (loglevel: TRACE_DEBUG_LINE (13)) (type: tracepoint)
|
||
osd:do_osd_op_post (loglevel: TRACE_DEBUG_LINE (13)) (type: tracepoint)
|
||
osd:do_osd_op_pre_unknown (loglevel: TRACE_DEBUG_LINE (13)) (type: tracepoint)
|
||
osd:do_osd_op_pre_copy_from (loglevel: TRACE_DEBUG_LINE (13)) (type: tracepoint)
|
||
osd:do_osd_op_pre_copy_get (loglevel: TRACE_DEBUG_LINE (13)) (type: tracepoint)
|
||
...
|
||
```
|
||
|
||
4. 创建 tracing session、启用 tracepoint、开始追踪:
|
||
|
||
```bash
|
||
lttng create trace-test
|
||
lttng enable-event --userspace osd:*
|
||
lttng start
|
||
```
|
||
|
||
5. 执行一些 Ceph 操作:
|
||
|
||
```bash
|
||
rados bench -p ec 5 write
|
||
```
|
||
|
||
6. 停止追踪并查看结果:
|
||
|
||
```bash
|
||
lttng stop
|
||
lttng view
|
||
```
|
||
|
||
7. 销毁 tracing session:
|
||
|
||
```bash
|
||
lttng destroy
|
||
```
|
||
|
||
#### 7.12.2 第二部分:Blkin(已废弃)
|
||
|
||
**Blkin 是什么(官方叙述)**
|
||
|
||
- Ceph 可以使用 **Blkin** —— 一个由 **Marios Kogias** 等人创建的库,它能够**追踪一个特定请求**:从它**在较高层级进入系统**开始,一直到**最终被 RADOS 服务完成**为止。
|
||
- 总体而言,**Blkin 实现了 Dapper 追踪语义**,用以展示一个 IO 请求可能触发的**不同处理阶段之间的因果关系(causal relationships)**。
|
||
- 目标是**端到端可视化**该请求在系统中的路径,并附带**每个处理阶段的延迟信息**。
|
||
- **得益于 LTTng,这可以以极小的开销、实时地(in realtime)完成**。
|
||
- LTTng traces 随后可以用 Twitter 的 **Zipkin** 进行可视化。
|
||
|
||
(官方外链:Dapper 论文、Zipkin 官网)
|
||
|
||
**编译配置**
|
||
|
||
使用 `-DWITH_BLKIN` 选项(**需要 `-DWITH_LTTNG`**):
|
||
|
||
```bash
|
||
./do_cmake -DWITH_LTTNG=ON -DWITH_BLKIN=ON
|
||
```
|
||
|
||
**blkin 的配置项必须在 ceph.conf 中设为 true**,当前可用选项:
|
||
|
||
```text
|
||
rbd_blkin_trace_all
|
||
osd_blkin_trace_all
|
||
osdc_blkin_trace_all
|
||
```
|
||
|
||
**测试 Blkin(官方完整流程)**
|
||
|
||
官方假设:你还没有运行 Ceph,且你编译了带 Blkin 支持的 Ceph 但**没有安装**它。那么用 Ceph src 目录下的 `vstart.sh` 脚本启动 Ceph,以便看到可用的 tracepoint:
|
||
|
||
```bash
|
||
OSD=3 MON=3 RGW=1 ../src/vstart.sh -n -o "rbd_blkin_trace_all"
|
||
lttng list --userspace
|
||
```
|
||
|
||
你会看到类似:
|
||
|
||
```text
|
||
UST events:
|
||
-------------
|
||
PID: 8987 - Name: ./ceph-osd
|
||
zipkin:timestamp (loglevel: TRACE_WARNING (4)) (type: tracepoint)
|
||
zipkin:keyval_integer (loglevel: TRACE_WARNING (4)) (type: tracepoint)
|
||
zipkin:keyval_string (loglevel: TRACE_WARNING (4)) (type: tracepoint)
|
||
lttng_ust_tracelog:TRACE_DEBUG (loglevel: TRACE_DEBUG (14)) (type: tracepoint)
|
||
|
||
PID: 8407 - Name: ./ceph-mon
|
||
zipkin:timestamp (loglevel: TRACE_WARNING (4)) (type: tracepoint)
|
||
zipkin:keyval_integer (loglevel: TRACE_WARNING (4)) (type: tracepoint)
|
||
zipkin:keyval_string (loglevel: TRACE_WARNING (4)) (type: tracepoint)
|
||
lttng_ust_tracelog:TRACE_DEBUG (loglevel: TRACE_DEBUG (14)) (type: tracepoint)
|
||
|
||
...
|
||
```
|
||
|
||
接着,**停止 Ceph,以便启用 tracepoint**:
|
||
|
||
```bash
|
||
../src/stop.sh
|
||
```
|
||
|
||
启动 LTTng session 并启用 tracepoint:
|
||
|
||
```bash
|
||
lttng create blkin-test
|
||
lttng enable-event --userspace zipkin:timestamp
|
||
lttng enable-event --userspace zipkin:keyval_integer
|
||
lttng enable-event --userspace zipkin:keyval_string
|
||
lttng start
|
||
```
|
||
|
||
然后**再次启动 Ceph**:
|
||
|
||
```bash
|
||
OSD=3 MON=3 RGW=1 ../src/vstart.sh -n -o "rbd_blkin_trace_all"
|
||
```
|
||
|
||
可以检查一下 ceph 是否起来了:
|
||
|
||
```bash
|
||
ceph status
|
||
```
|
||
|
||
现在用 rados 放点东西进去、确认它在、再取回来、再删掉:
|
||
|
||
```bash
|
||
ceph osd pool create test-blkin
|
||
rados put test-object-1 ../src/vstart.sh --pool=test-blkin
|
||
rados -p test-blkin ls
|
||
ceph osd map test-blkin test-object-1
|
||
rados get test-object-1 ./vstart-copy.sh --pool=test-blkin
|
||
md5sum vstart*
|
||
rados rm test-object-1 --pool=test-blkin
|
||
```
|
||
|
||
> 也可以使用 `examples/librados/` 下的例子,或 `rados bench`。
|
||
|
||
然后停止 LTTng session 看看收集到了什么:
|
||
|
||
```bash
|
||
lttng stop
|
||
lttng view
|
||
```
|
||
|
||
输出形如:
|
||
|
||
```text
|
||
[15:33:08.884275486] (+0.000225472) ubuntu zipkin:timestamp: { cpu_id = 53 }, { trace_name = "op", service_name = "Objecter", port_no = 0, ip = "0.0.0.0", trace_id = 5485970765435202833, span_id = 5485970765435202833, parent_span_id = 0, event = "osd op reply" }
|
||
[15:33:08.884614135] (+0.000002839) ubuntu zipkin:keyval_integer: { cpu_id = 10 }, { trace_name = "", service_name = "Messenger", port_no = 6805, ip = "0.0.0.0", trace_id = 7381732770245808782, span_id = 7387710183742669839, parent_span_id = 1205040135881905799, key = "tid", val = 2 }
|
||
[15:33:08.884616431] (+0.000002296) ubuntu zipkin:keyval_string: { cpu_id = 10 }, { trace_name = "", service_name = "Messenger", port_no = 6805, ip = "0.0.0.0", trace_id = 7381732770245808782, span_id = 7387710183742669839, parent_span_id = 1205040135881905799, key = "entity type", val = "client" }
|
||
```
|
||
|
||
**Blkin trace 事件的字段(从示例归纳)**
|
||
|
||
| 字段 | 说明 |
|
||
|---|---|
|
||
| `trace_name` | 追踪名,如 `"op"` |
|
||
| `service_name` | 服务名,如 `Objecter`、`Messenger` |
|
||
| `port_no` / `ip` | 该服务实例的端口与 IP |
|
||
| `trace_id` | 追踪 id(同一次请求的所有 span 共享) |
|
||
| `span_id` | 当前 span id |
|
||
| `parent_span_id` | 父 span id(`0` 表示根 span) |
|
||
| `event` | 事件名(`zipkin:timestamp` 中),如 `"osd op reply"` |
|
||
| `key` / `val` | 键值注解(`zipkin:keyval_integer` / `keyval_string` 中),如 `tid=2`、`entity type=client` |
|
||
|
||
#### 7.12.3 安装 Zipkin
|
||
|
||
官方说明:使用 Blkin 的目的之一就是**用 Zipkin 查看 traces**。用户应当**把 Zipkin 作为 tracepoint collector 兼 web service 运行**。可执行的 jar **在 9410 端口跑 collector,在 9411 端口跑 web 界面**。
|
||
|
||
**下载 Zipkin 包**:
|
||
|
||
```bash
|
||
git clone https://github.com/openzipkin/zipkin && cd zipkin
|
||
wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec'
|
||
java -jar zipkin.jar
|
||
```
|
||
|
||
**或者启动 docker 镜像**:
|
||
|
||
```bash
|
||
docker run -d -p 9411:9411 openzipkin/Zipkin
|
||
```
|
||
|
||
#### 7.12.4 在 Zipkin-web 中展示 Ceph 的 Blkin Traces
|
||
|
||
**下载 babeltrace-zipkin 项目**(该项目把 blkin 生成的 traces 通过 scribe 发送到 Zipkin collector):
|
||
|
||
```bash
|
||
git clone https://github.com/vears91/babeltrace-zipkin
|
||
cd babeltrace-zipkin
|
||
```
|
||
|
||
**把 lttng 数据发给 Zipkin**:
|
||
|
||
```bash
|
||
python3 babeltrace_zipkin.py ${lttng-traces-dir}/${blkin-test}/ust/uid/0/64-bit/ -p ${zipkin-collector-port(9410 by default)} -s ${zipkin-collector-ip}
|
||
```
|
||
|
||
示例:
|
||
|
||
```bash
|
||
python3 babeltrace_zipkin.py ~/lttng-traces-dir/blkin-test-20150225-160222/ust/uid/0/64-bit/ -p 9410 -s 127.0.0.1
|
||
```
|
||
|
||
**在网页上查看 Ceph traces**:
|
||
|
||
```bash
|
||
Browse http://${zipkin-collector-ip}:9411
|
||
Click "Find traces"
|
||
```
|
||
|
||
#### 7.12.5 注意事项
|
||
|
||
- **Blkin 已在 Squid 版本废弃**,将移除;新工作应直接用 **LTTng tracepoint**。
|
||
- 包化部署必须装 `-devel` 包,否则**缺 `*.tp.so` 会导致 coredump** —— 这是最坑的点。
|
||
- `osd_objectstore_tracing` 官方注明**实际上是 filestore 追踪**(filestore 早已移除),该选项很可能是历史遗留。
|
||
- 追踪要**先停 Ceph 再 enable tracepoint 再启动**,否则新启动的守护进程不会被 session 捕获。
|
||
|
||
> 引自:[Tracing Ceph With LTTng / Tracing Ceph With Blkin](https://docs.ceph.com/en/tentacle/dev/blkin/)
|
||
|
||
---
|
||
|
||
## 八、其他
|
||
|
||
本章收录 3 个不属于前面任何主题、但属于「内部实现」范畴的页面:`versions.rst`(版本编码规则)、`osd-class-path.rst`(对象类扩展机制与一个典型故障)、`context.rst`(CephContext)。
|
||
|
||
### 8.1 `versions.rst`:Public OSD Version(两个版本号的语义)
|
||
|
||
> 官方该页共 42 行,标题 "Public OSD Version"。它解释 OSD 在磁盘上维护的**两个版本号**——`pg_log.head`(PG 版本)与 `info.user_version`(用户版本)——以及它们的更新规则。这是理解「对象版本 / 重放 / EC 与缓存池」的关键一页。
|
||
|
||
#### 8.1.1 设计动机:为什么要两个版本号
|
||
|
||
官方开篇:**我们在磁盘上维护两个版本**:
|
||
|
||
| 版本 | 类型/位置 | 官方说明 |
|
||
|---|---|---|
|
||
| PG 版本 | `eversion_t pg_log.head` | PG 层面的版本 |
|
||
| 用户版本 | `version_t info.user_version` | 用户可见版本 |
|
||
|
||
**每个对象都被打上「它最后一次被修改时的 pg version 和 user_version」两个标记。**
|
||
|
||
拆成两个的动机,官方在后面给出:这套更新语义使得
|
||
|
||
- 对**传统池**:`user_version` 将**等于过去的 `reassert_version`**;
|
||
- 对**缓存池**:对象和 PG 的 user-version **能够跨池而不把事情搞得一团糟(without making a total mess of things)**。
|
||
|
||
> 也就是说:PG 版本服务于内部一致性(日志、peering、重放),user_version 服务于对外可见的语义(可跨池、可跨 tier)。
|
||
|
||
#### 8.1.2 PG version 的维护方式
|
||
|
||
官方原文:
|
||
|
||
> The PG version is modified by manipulating `OpContext::at_version` and then persisting it to the pg log as transactions, and is incremented in all the places it used to be.
|
||
|
||
要点:
|
||
|
||
| 项 | 说明 |
|
||
|---|---|
|
||
| 操作入口 | 操纵 **`OpContext::at_version`** |
|
||
| 持久化方式 | 通过 **pg log 的事务(transactions)** 持久化 |
|
||
| 递增时机 | 在**它过去被递增的所有地方**继续递增 |
|
||
|
||
#### 8.1.3 user_version 的维护方式(官方三条更新规则)
|
||
|
||
**修改入口**:操纵新的 **`OpContext::user_at_version`**,同样**通过 pg log 的事务持久化**。
|
||
|
||
**触发条件(关键)**:`user_at_version` **只在 `PrimaryLogPG::prepare_transaction` 中被修改**,且**仅当该 op 是一次 "user modify"** 时 —— 官方定义为**一个非 watch 的写(a non-watch write)**。
|
||
|
||
**更新规则(官方原文编号 1/2/3)**:
|
||
|
||
```text
|
||
1) set user_at_version to the maximum of
|
||
ctx->new_obs.oi.user_version + 1
|
||
and
|
||
info.last_user_version + 1.
|
||
|
||
2) set user_at_version to the maximum of
|
||
itself
|
||
and
|
||
ctx->at_version.version.
|
||
|
||
3) ctx->new_obs.oi.user_version = ctx->user_at_version
|
||
(to change the object's user_version)
|
||
```
|
||
|
||
逐条解读:
|
||
|
||
| 规则 | 作用 |
|
||
|---|---|
|
||
| 规则 1 | 取「对象自身 user_version + 1」与「PG 记录的 `info.last_user_version + 1」的**较大者** —— 保证新版本严格大于两者 |
|
||
| 规则 2 | 再取「规则 1 的结果」与「当前 `ctx->at_version.version`(PG 版本)」的**较大者** —— 把 user_version 顶到不低于 PG 版本 |
|
||
| 规则 3 | 把结果写回 `ctx->new_obs.oi.user_version`,即**真正改变对象的 user_version** |
|
||
|
||
#### 8.1.4 兼容旧客户端:`bad_replay_version`
|
||
|
||
为了支持老客户端,官方的做法:
|
||
|
||
- **保留**旧的 `reassert_version`,但**重命名为 `bad_replay_version`**;
|
||
- **填充方式照旧(we fill it in as before)**:
|
||
|
||
| 场景 | `bad_replay_version` 的取值 |
|
||
|---|---|
|
||
| **写操作(writes)** | 设为 `at_version`(**这就是正确的 replay version**) |
|
||
| **watch** | 设为**我们的 user version** |
|
||
| **ENOENT 回复** | 设为 **replay version 的 epoch**,但用 **user_version 的 version** |
|
||
|
||
- 另外一个改动:现在**在读操作上也填充 `bad_replay_version` 的 `version_t` 部分**(此前只在写操作上填),官方认为**对所有老客户端这都没问题**。
|
||
|
||
#### 8.1.5 面向新客户端:`user_version` 与 `replay_version`
|
||
|
||
对新客户端,官方**阻止它们读取 `bad_replay_version`**,并**新增两个正式的成员**:
|
||
|
||
| 成员 | 填充时机 |
|
||
|---|---|
|
||
| `user_version` | **在每一个操作上都填充(读操作也包括)** |
|
||
| `replay_version` | **只在写操作上填充** |
|
||
|
||
#### 8.1.6 objclass 接口的变化
|
||
|
||
官方说明:**objclass 函数 `get_current_version()` 现在总是返回 `pg->info.last_user_version`**。
|
||
|
||
其含义(官方解释):这意味着它**保证包含该 PG 中最后一次用户更新的版本(**包括在读操作时!**)**。
|
||
|
||
> 这一点很关键:以前 `get_current_version()` 的语义与读/写上下文有关,现在统一为 PG 级的 `last_user_version`,读操作也能拿到「最后一次用户更新」的版本。
|
||
|
||
#### 8.1.7 注意事项
|
||
|
||
- `user_at_version` **只对非 watch 的写**更新 —— watch/unwatch 操作**不推进** user_version。
|
||
- `bad_replay_version` 是**兼容字段**,新客户端不应读取它。
|
||
- 该页描述的是 **PrimaryLogPG** 路径(`PrimaryLogPG::prepare_transaction`);EC 池走的是 `ECBackend` / `ECPGMeta` 之类路径,**该页未覆盖 EC 侧的 user_version 维护 —— 文档未明确**。
|
||
- 该页**未**给出 `eversion_t` / `version_t` 的位域结构定义(epoch + version 的拼接方式)——**文档未明确**。
|
||
|
||
> 引自:[Public OSD Version](https://docs.ceph.com/en/tentacle/dev/versions/)
|
||
|
||
---
|
||
|
||
### 8.2 `osd-class-path.rst`:OSD class 加载路径问题
|
||
|
||
> 官方该页共 28 行,结构是「一段报错 → 定位方法 → 原因 → 解法 → 指向 objclass SDK」。它既是一个**排障手册**,也是**RADOS 对象类(objclass)扩展机制**的入门。
|
||
|
||
#### 8.2.1 现象:`rbd create` 报 `(95) Operation not supported`
|
||
|
||
官方给出的完整报错:
|
||
|
||
```bash
|
||
$ rbd create rbd/test --size 100M
|
||
2021-03-16 01:26:59.012 7fe41426f080 -1 librbd::PoolMetadata: list: failed listing metadata: (95) Operation not supported
|
||
2021-03-16 01:26:59.012 7fe41426f080 -1 librbd::Config: apply_pool_overrides: failed to read pool config overrides: (95) Operation not supported
|
||
2021-03-16 01:26:59.012 7fe400ff9640 -1 librbd::image::CreateRequest: 0x55d62341bb30 handle_add_image_to_directory: error adding image to directory: (95) Operation not supported
|
||
rbd: create error: (95) Operation not supported
|
||
```
|
||
|
||
三条 `-1` 级错误分别来自:
|
||
|
||
| 组件 | 失败动作 |
|
||
|---|---|
|
||
| `librbd::PoolMetadata` | `list`:列举元数据失败 |
|
||
| `librbd::Config` | `apply_pool_overrides`:读取池级配置覆盖失败 |
|
||
| `librbd::image::CreateRequest` | `handle_add_image_to_directory`:把 image 加入目录失败 |
|
||
|
||
#### 8.2.2 定位方法
|
||
|
||
官方给出两步定位法:
|
||
|
||
1. **加上 `--debug-ms=1`** → 就能看出**联系的是哪个 OSD**。
|
||
2. 在**被联系的那个 OSD 的日志**里,你会找到**为什么会发生这件事**的原因;而原因通常就是:**"rados classes" 无法在运行时用 `dlopen` 加载**。
|
||
|
||
#### 8.2.3 根因(OSD 侧日志)
|
||
|
||
```text
|
||
2021-03-16 01:26:59.013 7f6c6dff3640 10 _load_class rbd from /usr/local/lib64/rados-classes/libcls_rbd.so
|
||
2021-03-16 01:26:59.013 7f6c6dff3640 0 _load_class could not stat class /usr/local/lib64/rados-classes/libcls_rbd.so: (2) No such file or directory
|
||
2021-03-16 01:26:59.013 7f6c6dff3640 -1 osd.3 112 class rbd open got (2) No such file or directory
|
||
2021-03-16 01:26:59.013 7f6c6dff3640 1 -- [...] --> [...] -- osd_op_reply(5 rbd_directory [call rbd.dir_add_image] v0'0 uv0 ondisk = -95 ((95) Operation not supported)) v8 -- 0x7f6c6800fed0 con 0x7f6cb80100c0
|
||
```
|
||
|
||
从日志可以读出完整的因果链:
|
||
|
||
| 行 | 信息 |
|
||
|---|---|
|
||
| 第 1 行 | `_load_class rbd from /usr/local/lib64/rados-classes/libcls_rbd.so` —— OSD 尝试从该路径加载名为 `rbd` 的 class |
|
||
| 第 2 行 | `could not stat class ...: (2) No such file or directory` —— 文件不存在 |
|
||
| 第 3 行 | `osd.3 112 class rbd open got (2) No such file or directory` —— 打开失败 |
|
||
| 第 4 行 | `osd_op_reply(... [call rbd.dir_add_image] ... ondisk = -95 ((95) Operation not supported))` —— **失败被转换成 `-95` 返回给客户端** |
|
||
|
||
即:**OSD 找不到 `libcls_rbd.so`**,于是 `[call rbd.dir_add_image]` 这个 objclass 方法调用无法执行,最终以 `ENOTSUP (95)` 回给客户端。
|
||
|
||
#### 8.2.4 解法
|
||
|
||
可以在 **`ceph.conf`** 中用 **`osd_class_dir`** 自定义这些模块的加载路径。
|
||
|
||
- **默认值**:**`$libdir/rados-classes`**
|
||
- 官方提示:**因此在开发时,你很可能不得不调整这个路径**(因为开发树的 `.so` 通常在 build 目录里,而不是安装到 `$libdir`)。
|
||
|
||
#### 8.2.5 机制说明与延伸
|
||
|
||
官方最后一句:**这些 class 库用于扩展 RADOS**(These class libraries are used for extending RADOS),并指向 `:ref:`rados-objclass-api-sdk``。
|
||
|
||
> 💡 补充:本页对 objclass 机制本身**只有这一句**——它没有讲 `cls_method_call_t`、没有讲 `CLS_MODULE` 注册宏、没有讲 class 的版本管理或 `cls_hello` 之类的接口。完整的 SDK 说明在官方 `rados/api/objclass-sdk` 页面,**不在本册输入文件范围内**。
|
||
|
||
#### 8.2.6 注意事项
|
||
|
||
- 报错发生在**客户端**但**根因在 OSD 侧日志**,必须 `--debug-ms=1` 定位到具体 OSD 才能看到。
|
||
- 这是**开发环境**(源码编译未安装)的典型问题;生产包安装一般不会有。
|
||
- 只要任何 `libcls_*.so` 缺失,依赖它的高层功能(rbd、rgw、cephfs 的部分元数据操作)都会以 `(95) Operation not supported` 的形式失败——**错误信息本身极具误导性**,不会提示「找不到 .so」。
|
||
|
||
> 引自:[OSD class path issues](https://docs.ceph.com/en/tentacle/dev/osd-class-path/)
|
||
|
||
---
|
||
|
||
### 8.3 `context.rst`:CephContext
|
||
|
||
> 官方该页共 20 行,是全套 `dev/` 文档中最短的页面之一(与 `delayed-delete.rst` 13 行同量级)。它只讲一件事:**CephContext 是什么、有几个、以及它对日志宏的约束**。
|
||
|
||
#### 8.3.1 CephContext 是什么(官方定义)
|
||
|
||
官方定义:**一个 `CephContext` 代表对 Ceph 集群的单一视图(a single view of the Ceph cluster)**。
|
||
|
||
它完整地带有:
|
||
|
||
| 组成部分 | 官方列举 |
|
||
|---|---|
|
||
| 配置 | a configuration |
|
||
| 性能计数器 | a set of performance counters(`PerfCounters`) |
|
||
| 心跳图 | a heartbeat map |
|
||
|
||
更多信息见 **`src/common/ceph_context.h`**。
|
||
|
||
#### 8.3.2 通常有几个 CephContext
|
||
|
||
| 场景 | 数量 | 官方说明 |
|
||
|---|---|---|
|
||
| 一般应用 | **1 个** | 名为 **`g_ceph_context`** |
|
||
| 库代码 | **可能多个** | 如果库的使用者**调用了 `rados_create` 多于一次**,就会初始化多个 `CephContext` |
|
||
|
||
#### 8.3.3 为什么发日志必须要 CephContext
|
||
|
||
官方自问自答:
|
||
|
||
> 「**为什么需要 ceph context 才能发出日志消息?**」——「因为**没有 CephContext,我们就不知道哪些日志消息被禁用、哪些被启用**。」
|
||
|
||
由此推出的**编码约束(重要)**:
|
||
|
||
| 场景 | 可用的日志宏 | 原因 |
|
||
|---|---|---|
|
||
| **守护进程(daemons)** | 可以用 **`dout`** 和 **`derr`** | — |
|
||
| **库代码(library code)** | **必须**用 **`ldout`** 和 **`lderr`**,并**传入你自己的 `CephContext` 对象** | **`dout()` 宏隐式引用 `g_ceph_context`,因此它不能用在库代码里** |
|
||
|
||
官方强调:**编译器会强制这个限制**(The compiler will enforce this restriction)。
|
||
|
||
#### 8.3.4 注意事项
|
||
|
||
- 在 librados / librbd / libcephfs 等库代码中**写 `dout` 会编译不过**——这是有意设计,不是 bug。
|
||
- 一个进程里**多个 `CephContext`** 意味着**多套配置、多套 PerfCounters、多套 heartbeat map**,它们的日志开关各自独立。
|
||
- 该页**未**说明 `CephContext` 的生命周期管理(引用计数 / 何时析构),也**未**说明 `g_conf` 与 `CephContext->_conf` 的关系细节(后者在 `config.rst` 中提到:`g_conf` 等价于 `g_ceph_ctx->_conf`)—— 本页范围内**文档未明确**。
|
||
|
||
> 引自:[CephContext](https://docs.ceph.com/en/tentacle/dev/context/)
|
||
|
||
---
|
||
|
||
## 文档未明确项
|
||
|
||
本节汇总本册在撰写过程中遇到的、官方源文**没有给出明确说明**的点。按章节列出,便于后续补档或对照源码核实。凡属本人补充的内容,正文中均已用 `> 💡 补充:` 标出。
|
||
|
||
### 全局 / 结构性
|
||
|
||
| # | 未明确项 | 所在页 | 说明 |
|
||
|---|---|---|---|
|
||
| 1 | 「内部实现篇」拆成 4 册的划分依据 | — | 官方 `dev/` 目录并无此 4 分册结构(官方入口是 `dev/internals.rst`)。此为撰稿/编辑侧的划分,已在「本册导读」中标注 |
|
||
| 2 | `object-store.rst` 只 62 行且含 `.. todo:: write more here` | object-store | 官方自认该页未完成,`ObjectStore` 的方法语义、事务模型、各实现差异均**未写** |
|
||
| 3 | `messenger.rst` 只 33 行 | messenger | 除 `ceph_perf_msgr` 的 server/client 参数外,Messenger 的架构(Policy、Connection、DispatchQueue 等)该页**完全未覆盖** |
|
||
| 4 | `delayed-delete.rst` 只 13 行 | delayed-delete | 延迟删除的**具体实现**(PurgeQueue、stray 目录、触发时机、速率控制)**全部未写** |
|
||
| 5 | `context.rst` 只 20 行 | context | `CephContext` 的生命周期、引用计数、与 `g_conf` 的关系细节未写 |
|
||
|
||
### 一、对象存储抽象与实现
|
||
|
||
| # | 未明确项 | 说明 |
|
||
|---|---|---|
|
||
| 6 | BlueStore 的 7 种小写策略对应的**具体代码选择逻辑** | `bluestore.rst` 给了策略表(U/P/W/N/R+W/C + 未来的 F/X)与 7×5 映射矩阵,但**未说明**OSD 运行时依据什么条件(大小阈值、配置项)落到哪个策略 |
|
||
| 7 | SeaStore 的 GC 算法细节 | `seastore.rst` 的 GC 小节带 TODO,官方自认未写完 |
|
||
| 8 | Zoned Storage 场景下 BlueStore 与 SeaStore 的**选择建议** | `zoned-storage.rst` 给了 vstart 与 benchmark,但**未给**生产选型建议 |
|
||
| 9 | `ZonedFreelistManager` 与 `BitmapFreelistManager` 的完整状态机 | 官方只对比了 3-state 与 2-state 的差异 |
|
||
|
||
### 二、PG 与 peering
|
||
|
||
| # | 未明确项 | 说明 |
|
||
|---|---|---|
|
||
| 10 | peering 状态机的**完整状态转移图** | `peering.rst` 中该图是 **SVG 图片**,无法从 rst 源文提取;官方只给出 `gen_state_diagram.py` 的再生成方法 |
|
||
| 11 | 28 个 PG 状态之间的**互斥/共存关系** | `placement-group.rst` 列表格给出各状态含义,但**未给**状态间的转移与组合规则 |
|
||
| 12 | omap 统计项的计算口径 | 官方只说明有 omap 统计,未给公式与更新时机 |
|
||
|
||
### 三、Messenger 与网络
|
||
|
||
| # | 未明确项 | 说明 |
|
||
|---|---|---|
|
||
| 13 | msgr2 各 TAG 的**完整取值表** | `msgr2.rst` 覆盖了大量 `TAG_*`,但**未给**一份完整的 tag → 值 → 方向对照表 |
|
||
| 14 | 压缩(compression)**在 secure 模式下的确切限制** | 官方只给了 caveat,未展开 |
|
||
| 15 | DPDK 的**性能收益量化数据** | `dpdk.rst` 给了配置方法,未给基准测试结果 |
|
||
| 16 | `encoding.rst` 中 n-2 / n-3 兼容规则的**具体判定代码** | 官方给了规则与宏,未给判定函数实现 |
|
||
|
||
### 四、协议与认证
|
||
|
||
| # | 未明确项 | 说明 |
|
||
|---|---|---|
|
||
| 17 | `cephx_protocol.rst`(2012 年快照)中引用的**代码路径是否已漂移** | 该页是历史文档,其中的文件名/函数名可能与 Tentacle 现状不一致,官方**未做同步标注** |
|
||
| 18 | `ceph_krb_auth.rst`(1096 行)中大量 LDAP/Kerberos **运维配置细节** | 该页以背景知识与部署步骤为主,与 Ceph 内部实现的**对照关系**未系统说明 |
|
||
| 19 | `cephx_sign_messages` 默认 false 的**安全影响评估** | `session_authentication.rst` 只说明默认关闭,未给开启后的性能代价或兼容性影响 |
|
||
| 20 | backoff 机制中 OSD 侧三状态(new/acked/deleting)的**超时与清理策略** | `rados-client-protocol.rst` 只描述状态,未给超时值 |
|
||
|
||
### 五、集群映射与元数据(mon 内部)
|
||
|
||
| # | 未明确项 | 说明 |
|
||
|---|---|---|
|
||
| 21 | 各 PaxosService 的 `format_version` **完整演进表** | `mon-on-disk-formats.rst` 只以 AuthMonitor(0~3)为例,其它 service 未列 |
|
||
| 22 | mon 选举 connectivity 算法中**连接评分的具体衰减公式常数来源** | 官方给了公式与默认值,未给调优建议 |
|
||
| 23 | `mon-osdmap-prune.rst` 中 pinned maps 的**pin/unpin 触发点全集** | 官方给了算法与不变量,未给所有调用点 |
|
||
|
||
### 六、数据路径与特性
|
||
|
||
| # | 未明确项 | 说明 |
|
||
|---|---|---|
|
||
| 24 | `file-striping.rst` 的**布局换算公式** | 官方只有文字描述 + 一个数值例子,**没有给伪代码或公式** |
|
||
| 25 | 已有文件 **layout 变更后的数据重排语义** | 官方未讨论 |
|
||
| 26 | `erasure-coded-pool.rst` 的**编解码实现** | 该页是纯使用说明,无 `ErasureCodeInterface` / `ECBackend` 内容;插件全列表(jerasure/clay/shec 等)也未给 |
|
||
| 27 | `deduplication.rst` 中 ref 条目 **`max` 字段的语义** | 示例中恒为 0,官方未解释 |
|
||
| 28 | `deduplication.rst` 中 `sample-dedup --snap` 的**具体行为** | 官方只说「需要指定」,未展开 |
|
||
| 29 | chunk pool 故障对 base pool 对象的**影响与恢复手段** | 官方未讨论(架构上显然存在强依赖) |
|
||
| 30 | CRUSH MSR 中 `CRUSH_RULE_SET_CHOOSE_MSR*` **允许的具体 set 步骤清单** | `crush-msr.rst` 只给了通配写法 |
|
||
| 31 | `crush_msr_choose` 的**伪代码 / 完整算法** | 官方指向源码注释,页面本身未给 |
|
||
| 32 | `msr_firstn` 与 `msr_indep` 在失败重试语义上的**完整差异** | 官方未展开 |
|
||
|
||
### 七、配置与可观测性内部
|
||
|
||
| # | 未明确项 | 说明 |
|
||
|---|---|---|
|
||
| 33 | `health-reports.rst` 中的**交互示意图** | 源文只有引导语 "The following diagrams outline...",图为外链图片,rst 文本中无内容,**无法复现** |
|
||
| 34 | health check 的**注册机制**(`health_check_t`、编码格式、detail 合并规则) | 官方未写 |
|
||
| 35 | `ceph health detail` 与 `health` mon 命令的**差异** | 官方未写 |
|
||
| 36 | `config-key` 的**读写 API、事务语义、大小限制、权限模型** | `config-key.rst` 只记录键布局 schema |
|
||
| 37 | `PerfCountersBuilder` 的**完整 API 列表** | `logs.rst` 提到 `add_u64`/`add_fl`/`add_fl_avg`,`perf_counters.rst` 未补全 |
|
||
| 38 | labeled perf counter 的 **label 基数爆炸风险处理** | 官方未讨论 |
|
||
| 39 | perf histogram 中「**9 个 history entry**」的**滚动周期 / 触发条件** | 官方只说有 9 个,未说何时滚动。且示例 JSON 中 `values` 只展示了 8 个子数组,**与文字说明不一致**(疑为示例截断) |
|
||
| 40 | 如何在代码中**新增**一个 perf histogram | 无 API 说明 |
|
||
| 41 | mempool 的**新增方法**与**非 BlueStore 子系统的覆盖情况** | `mempool_accounting.rst` 只给清单 + 指向 `src/include/mempool.h` |
|
||
| 42 | `dump_mempools` 的**输出格式细节** | 官方只给命令 |
|
||
| 43 | `cpu-profiler.rst` 中 OProfile 的**实际采集/分析命令** | 一律指向 troubleshooting 文档;且该页用 `do-cmake.sh`,与 Tentacle 常用 `do_cmake.sh` 的差异未说明 |
|
||
| 44 | `corpus.rst` 中 `readable.sh` 测试的**判定逻辑** | 官方只说 `ctest -R readable.sh` |
|
||
| 45 | `osd_objectstore_tracing` 的现状 | 官方注明 "actually filestore tracing",而 filestore 已移除,该选项**是否为历史遗留**未说明 |
|
||
| 46 | `blkin.rst` 中 Blkin 的**替代方案**(除直接用 LTTng 外) | 官方只声明废弃,未给迁移路径 |
|
||
|
||
### 八、其他
|
||
|
||
| # | 未明确项 | 说明 |
|
||
|---|---|---|
|
||
| 47 | `eversion_t` / `version_t` 的**位域结构定义** | `versions.rst` 未给(epoch 与 version 的拼接方式) |
|
||
| 48 | **EC 池路径**下 `user_version` 的维护方式 | `versions.rst` 只覆盖 `PrimaryLogPG::prepare_transaction` |
|
||
| 49 | objclass 的**完整 SDK**(注册宏、方法签名、版本管理) | `osd-class-path.rst` 只一句带过,指向 `rados-objclass-api-sdk`(不在本册输入范围) |
|
||
| 50 | `osd-class-path.rst` 中 `.so` 缺失时**为何统一报 95 而非 ENOENT** | 官方未解释该错误码转换的动因 |
|
||
|
||
---
|
||
|
||
## 附:本册覆盖的源文清单
|
||
|
||
| 章节 | 源文文件 | 官方页标题 |
|
||
|---|---|---|
|
||
| 一 | `object-store.rst` | Object Store |
|
||
| 一 | `bluestore.rst` | BlueStore |
|
||
| 一 | `seastore.rst` | SeaStore |
|
||
| 一 | `zoned-storage.rst` | Zoned Storage |
|
||
| 二 | `placement-group.rst` | Placement Group |
|
||
| 二 | `peering.rst` | Peering |
|
||
| 三 | `messenger.rst` | Messenger |
|
||
| 三 | `msgr2.rst` | msgr2 protocol |
|
||
| 三 | `network-protocol.rst` | Network Protocol |
|
||
| 三 | `network-encoding.rst` | Network Encoding |
|
||
| 三 | `encoding.rst` | Encoding |
|
||
| 三 | `dpdk.rst` | DPDK |
|
||
| 四 | `rados-client-protocol.rst` | RADOS client protocol |
|
||
| 四 | `cephx.rst` | Cephx |
|
||
| 四 | `cephx_protocol.rst` | Cephx protocol (2012) |
|
||
| 四 | `ceph_krb_auth.rst` | Kerberos auth |
|
||
| 四 | `session_authentication.rst` | Session authentication |
|
||
| 五 | `mon-on-disk-formats.rst` | Monitor on-disk formats |
|
||
| 五 | `mon-bootstrap.rst` | Monitor bootstrap |
|
||
| 五 | `mon-elections.rst` | Monitor elections |
|
||
| 五 | `mon-osdmap-prune.rst` | Full osdmap version pruning |
|
||
| 六 | `file-striping.rst` | File striping |
|
||
| 六 | `erasure-coded-pool.rst` | Erasure Coded pool |
|
||
| 六 | `deduplication.rst` | Deduplication |
|
||
| 六 | `delayed-delete.rst` | CephFS delayed deletion |
|
||
| 六 | `crush-msr.rst` | CRUSH MSR |
|
||
| 七 | `config.rst` | Configuration Management System |
|
||
| 七 | `config-key.rst` | config-key layout |
|
||
| 七 | `logging.rst` | Use of the cluster log |
|
||
| 七 | `logs.rst` | Debug logs |
|
||
| 七 | `health-reports.rst` | Health Reports |
|
||
| 七 | `perf.rst` | Using perf |
|
||
| 七 | `perf_counters.rst` | Perf counters |
|
||
| 七 | `perf_histograms.rst` | Perf histograms |
|
||
| 七 | `mempool_accounting.rst` | What is a mempool? |
|
||
| 七 | `cpu-profiler.rst` | Installing Oprofile |
|
||
| 七 | `corpus.rst` | Corpus structure |
|
||
| 七 | `blkin.rst` | Tracing Ceph With LTTng / Blkin |
|
||
| 八 | `versions.rst` | Public OSD Version |
|
||
| 八 | `osd-class-path.rst` | OSD class path issues |
|
||
| 八 | `context.rst` | CephContext |
|
||
|
||
> 说明:上表 41 项中,`mon-on-disk-formats.rst`、`mon-bootstrap.rst`、`mon-elections.rst`、`mon-osdmap-prune.rst` 这 4 个文件**不在本册指定的 37 个输入文件列表内**,但存在于 `raw/dev/` 目录中。依据撰稿要求「若文件存在则讲」,已作为第五章写入本册,此处一并登记。
|