> 基于 Ceph 官方文档整理 | 文档版本:**Tentacle v20.2.4** | 抓取日期:2026-09-17 | 源文:ceph/ceph@v20.2.4 `doc/dev/` > 官网入口:https://docs.ceph.com/en/tentacle/dev/ # 第 18 册 · 内部实现篇(三):组件实现、Crimson 与开发工具 --- ## 本册导读 内部实现分为四册,本册是**第三册**,主题是「**组件实现、Crimson 与开发工具**」。 - 第 16 册:核心机制(OSD/MDS 内部机制、peering、PG、编解码等,对应 `doc/dev/osd_internals/`、`doc/dev/mds_internals/`、`peering.rst` 等) - 第 17 册:协议与基础设施(msgr2、cephx、编码、日志、性能计数器等) - **第 18 册(本册)**:开发者上手环境与工具链、Crimson 下一代 OSD、CephFS / RBD / RGW / cephadm / dashboard / ceph-volume 的组件级开发文档、测试与 CI、文档与发布工程、调试观测工具 - 第 19 册:`doc/dev/developer_guide/`(**本册不包含该目录**,属于另一册) 本册覆盖的源文件范围(`doc/dev/` 下): | 分组 | 文件 | |---|---| | A 顶层 | `internals.rst`、`quick_guide.rst`、`dev_cluster_deployment.rst`、`development-workflow.rst`、`balancer-design.rst`、`cephfs-mirroring.rst`、`cephfs-reclaim.rst`、`cephfs-snapshots.rst`、`kclient.rst`、`kubernetes.rst`、`libcephfs_proxy.rst`、`libs.rst`、`rbd-diff.rst`、`rbd-export.rst`、`rbd-layering.rst`、`testing.rst`、`continuous-integration.rst`、`cxx.rst`、`documenting.rst`、`generatedocs.rst`、`release-process.rst`、`release-checklists.rst`、`sepia.rst`、`vstart-ganesha.rst`、`wireshark.rst`、`macos.rst`、`freebsd.rst`、`iana.rst`、`dpdk.rst` | | B crimson/ | `index.rst`、`crimson.rst`、`osd.rst`、`pipeline.rst`、`error-handling.rst`、`backfillmachine.rst`、`poseidonstore.rst` | | C cephadm/ | `index.rst`、`developing-cephadm.rst`、`host-maintenance.rst`、`compliance-check.rst`、`design/storage_devices_and_osds.rst`、`scalability-notes.rst` | | D radosgw/ | `index.rst`、`usage.rst`、`bucket_index.rst`、`s3_compliance.rst`、`admin/adminops_nonimplemented.rst` | | E dashboard/ | `ui_goals.rst` | | F ceph-volume/ | `index.rst`、`plugins.rst`、`lvm.rst`、`zfs.rst`、`systemd.rst` | **阅读须知**:`doc/dev/` 下的页面质量差异极大——有些是近一两个版本刚写的(如 `crimson/crimson.rst`、`cephadm/host-maintenance.rst`),有些明显**多年未更新**(如 `wireshark.rst` 仍在说 Wireshark 1.12.1、`macos.rst` 仍在说 Xcode 9、`development-workflow.rst` 仍以 hammer/infernalis 为例)。本册对每个页面会如实标注其时代特征,**不把过时内容当成现状陈述**。凡官方文档没写的,一律写「文档未明确」,补充说明一律以 `> 💡 补充:` 标注。 --- # 一、开发者环境与工具 ## 1.1 Ceph Internals 索引页(`internals.rst`) ### 是什么 `doc/dev/internals.rst` 是开发者文档区的**索引与入口页**,实际内容只有三段:如何启动一个开发模式集群、开发者邮件列表、以及被注释掉的 toctree(HTML 注释 `` 包裹,说明目录树由 Sphinx 的 `:glob:` 自动展开,而非手工维护)。 ### 启动开发模式集群 ```bash cd build OSD=3 MON=3 MGR=3 ../src/vstart.sh -n -x # 检查集群是否起来了 bin/ceph health ``` 注意这条命令与 `quick_guide.rst` 中的示例略有不同(那里用 `-d -n` 且默认 1 个 MON/MDS)。两者都可用,区别见 1.3 节的参数说明。 ### 邮件列表(社区沟通渠道) | 列表 | 用途 | 订阅方式 | |---|---|---| | `dev@ceph.io` | Ceph 本身的开发讨论、与其他技术的互操作、项目运作 | 发信到 `dev-join@ceph.io`,主题写 `subscribe`;或在 https://lists.ceph.io 注册 | | `ceph-devel@vger.kernel.org` | **Linux 内核 Ceph 客户端**组件的讨论与 patch review | 发信到 `majordomo@vger.kernel.org`,正文写 ` subscribe ceph-devel` | `internals.rst` 开头还有一个 note:如果你想**把 Ceph 当库**在自己的软件里用,应看 `/api/index` 而不是本页。 > 引自:[Ceph Internals](https://docs.ceph.com/en/tentacle/dev/internals/) --- ## 1.2 开发者快速指南(`quick_guide.rst`) ### 是什么 一份「从源码到能跑测试」的最小路径说明,覆盖:依赖安装 → 编译 → 起 vstart 集群 → 建池跑基准 → 跑单元测试。 ### 三种构建姿势 **姿势一:全自动(推荐首次)** ```bash ./run-make-check.sh ``` 该脚本会:安装 Ceph 依赖 → 以 debug 模式编译全部 → 跑一批测试验证结果符合预期。 **姿势二:只装依赖 + 手动 cmake(推荐日常开发)** ```bash ./install-deps.sh ./do_cmake.sh -DWITH_MANPAGE=OFF -DWITH_BABELTRACE=OFF -DWITH_MGR_DASHBOARD_FRONTEND=OFF ``` **姿势三:关掉无关组件,加速编译** ```bash ./do_cmake.sh ... -DWITH_RBD=OFF -DWITH_KRBD=OFF -DWITH_RADOSGW=OFF ``` 最后编译: ```bash cmake --build build [--target ...] ``` 省略 `--target...` 即全量构建。 ### 启动开发集群 ```bash cd build ../src/vstart.sh -d -n ``` 或者只起 1 个 mon + 1 个 mds: ```bash env MON=1 MDS=1 ../src/vstart.sh -d -n -x ``` 绝大多数集群日志在 `build/out`。 ### 集群起来后能立刻做什么 vstart 启动时会自动创建两个池:`cephfs_data_a` 和 `cephfs_metadata_a`。 ```bash $ bin/ceph osd pool stats *** DEVELOPER MODE: setting PATH, PYTHONPATH and LD_LIBRARY_PATH *** pool cephfs_data_a id 1 nothing is going on pool cephfs_metadata_a id 2 nothing is going on $ bin/rados df POOL_NAME USED OBJECTS CLONES COPIES MISSING_ON_PRIMARY UNFOUND DEGRADED RD_OPS RD WR_OPS WR cephfs_data_a 0 0 0 0 0 0 0 0 0 0 0 cephfs_metadata_a 2246 21 0 63 0 0 0 0 0 42 8192 total_objects 21 total_used 244G total_space 1180G ``` 建池并跑基准、放对象、列对象: ```bash bin/ceph osd pool create mypool bin/rados -p mypool bench 10 write -b 123 bin/rados -p mypool put objectone bin/rados -p mypool put objecttwo bin/rados -p mypool ls ``` 停止集群: ```bash ../src/stop.sh ``` ### 重置 vstart 环境 vstart 会创建 `out/` 和 `dev/` 两个目录保存集群状态。想彻底重置: ```bash ../src/stop.sh rm -rf out dev env MDS=1 MON=1 OSD=3 ../src/vstart.sh -n -d ``` ### 起 RGW 开发环境 用环境变量 `RGW` 打开: ```bash cd build RGW=1 ../src/vstart.sh -d -n -x ``` 然后可以用 swift python client 通信: ```bash swift -A http://localhost:8000/auth -U test:tester -K testing list swift -A http://localhost:8000/auth -U test:tester -K testing upload mycontainer ceph swift -A http://localhost:8000/auth -U test:tester -K testing list ``` ### 跑单元测试 测试源码在 `src/tests`: ```bash (cd build && ninja check) ``` > 引自:[Developer Guide (Quick)](https://docs.ceph.com/en/tentacle/dev/quick_guide/) --- ## 1.3 vstart.sh 开发集群详解(`dev_cluster_deployment.rst`) ### 是什么 / 为什么需要 `vstart.sh` 是 Ceph 自带的一个实用脚本,让开发者在**本机**起一个「假集群」(fake local cluster)用于开发调试,无需真实硬件、无需多机。它会启动 rgw / mon / osd / mds,不指定则全部启动。 ### 基本用法 ```bash vstart.sh [OPTIONS]... # 停止 ./stop.sh ``` ### 完整选项表 | 选项 | 含义 | |---|---| | `-b, --bluestore` | 用 bluestore 作为 osd 的 objectstore 后端 | | `--cache ` | 给指定 pool 设置 cache-tier | | `-d, --debug` | 以 debug 模式启动 | | `-e` | 创建一个纠删码池 | | `--hitset ` | 启用 hitset 追踪 | | `-i ip_address` | 绑定到指定 IP,而不是从 hostname 猜测/解析 | | `-k` | 保留旧配置文件,不覆盖 | | `-K, --kstore` | 用 kstore 作为 osd objectstore 后端 | | `-l, --localhost` | 用 localhost 而非 hostname | | `-m ip[:port]` | 指定 monitor 的 IP 与端口 | | `--memstore` | 用 memstore 作为 osd 的 objectstore 后端 | | `--multimds ` | 允许 multimds,指定最大 active 数量 | | `-n, --new` | 创建一个新集群 | | `-N, --not-new` | 复用已有集群配置(**默认**) | | `--nodaemon` | 用 `ceph-run` 作为 mon/osd/mds 的 wrapper | | `--nolockdep` | 禁用 lockdep | | `-o ` | 把 *config* 加到 ceph 配置的所有 section | | `--rgw_port ` | 指定 rgw 的 http 监听端口 | | `--rgw_frontend ` | 指定 rgw frontend 配置(默认 `civetweb`) | | `--rgw_compression ` | 指定 rgw 压缩插件(默认关闭) | | `--smallmds` | 给 mds 配置较小的 cache 上限 | | `--short` | 只用短对象名;ext4 开发环境下必需 | | `--valgrind[_{osd,mds,mon}] 'valgrind_toolname [args...]'` | 用 valgrind 跑 osd/mds/mon/全部 ceph 二进制,可指定 tool 与参数 | | `--without-dashboard` | 不跑 mgr dashboard | | `-x` | 启用 cephx(**默认开启**) | | `-X` | 禁用 cephx | ### 环境变量 `{OSD,MDS,MON,RGW}` 这四个环境变量分别表示要启动的对应进程**实例数量**。 ```bash OSD=3 MON=3 RGW=1 vstart.sh ``` > 💡 补充:`MGR`、`NFS` 这两个环境变量在其他页面也被使用(见 `internals.rst` 的 `MGR=3` 与 `vstart-ganesha.rst` 的 `NFS=1`),但 `dev_cluster_deployment.rst` 的选项表中未列出它们。 ### 在同一台机器上跑多个开发集群 `mstart.sh` 是 vstart 的一层小包装,可为每个集群在不同端口上启动 mon、rgw,实现多集群共存: ```bash mstart.sh # 例 ./mstart.sh cluster1 -n # 停止 ./mstop.sh ``` > 💡 补充:文档没有说明端口分配规则,也没有说明 `mstart.sh` 是否隔离 `out/` 目录——实践上需要自行确认。 > 引自:[Deploying a development cluster](https://docs.ceph.com/en/tentacle/dev/dev_cluster_deployment/) --- ## 1.4 vstart + NFS Ganesha(`vstart-ganesha.rst`) ### 是什么 让开发者用 vstart 集群快速拉起 NFS Ganesha,导出 CephFS 或 RGW。**CephFS 导出自 Octopus 起支持,RGW 导出自 Quincy 起支持**。 ### 导出 CephFS 方式一:用 `cephadm` ```bash $ MDS=1 MON=1 OSD=3 NFS=1 ../src/vstart.sh -n -d --cephadm ``` 会部署单个 NFS Ganesha 守护进程,监听**默认 NFS Ganesha 端口**,同时创建 cephfs export。 方式二:用 test orchestrator ```bash $ MDS=1 MON=1 OSD=3 NFS=1 ../src/vstart.sh -n -d ``` `NFS` 环境变量 = 要部署的 NFS Ganesha 守护进程数量,每个监听**随机端口**。 > ⚠️ 注意:使用 test orchestrator 方式时,**NFS Ganesha 软件包必须预先安装**。 ### 导出 RGW ```bash $ MON=1 OSD=3 RGW=1 NFS=1 ../src/vstart.sh -n -d --cephadm ``` > ⚠️ 注意:RGW 导出方式要求预先安装 **boto python module**。 > 💡 补充:该页只给了 `cephadm` 方式的 RGW 示例,未给出 test orchestrator 方式的 RGW 示例。 > 引自:[NFS CephFS-RGW Developer Guide](https://docs.ceph.com/en/tentacle/dev/vstart-ganesha/) --- ## 1.5 在 macOS 上构建(`macos.rst`) > ⚠️ **该页内容明显过时**:文中提到「Xcode 9 自带的 clang 不支持全部 C++17 特性」「建议用 brew 安装 llvm」,并给出 `nss/3.48`、`openssl/1.0.2t`、`snappy/1.1.7_1` 这类具体版本号,还建议使用 boost v1.69 规避 https://github.com/boostorg/atomic/issues/15 。这些版本对应的是 2019 年前后的环境,请在 Tentacle 时代谨慎参考。 ### 步骤 ```bash brew install llvm brew install snappy ccache cmake pkg-config pip install cython # 如果要构建 FUSE 支持 brew cask install osxfuse ``` 然后在 Ceph 源码目录下: ```bash mkdir build cd build export PKG_CONFIG_PATH=/usr/local/Cellar/nss/3.48/lib/pkgconfig:/usr/local/Cellar/openssl/1.0.2t/lib/pkgconfig cmake .. -DBOOST_J=4 \ -DCMAKE_C_COMPILER=/usr/local/opt/llvm/bin/clang \ -DCMAKE_CXX_COMPILER=/usr/local/opt/llvm/bin/clang++ \ -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/opt/llvm/lib" \ -DENABLE_GIT_VERSION=OFF \ -DSNAPPY_ROOT_DIR=/usr/local/Cellar/snappy/1.1.7_1 \ -DWITH_BABELTRACE=OFF \ -DWITH_BLUESTORE=OFF \ -DWITH_CCACHE=OFF \ -DWITH_CEPHFS=OFF \ -DWITH_KRBD=OFF \ -DWITH_LIBCEPHFS=OFF \ -DWITH_LTTNG=OFF \ -DWITH_LZ4=OFF \ -DWITH_MANPAGE=ON \ -DWITH_MGR=OFF \ -DWITH_MGR_DASHBOARD_FRONTEND=OFF \ -DWITH_RADOSGW=OFF \ -DWITH_RDMA=OFF \ -DWITH_SPDK=OFF \ -DWITH_SYSTEMD=OFF \ -DWITH_TESTS=OFF \ -DWITH_XFS=OFF ``` ### 注意事项 - `nss` 与 `snappy` 的路径会随安装版本变化,需要自行调整。 - 文档明确说:**目前在 macOS 上最实用的 Ceph 用途是 FUSE 和一些基于 librados 的应用**。换言之上表中的 `-DWITH_BLUESTORE=OFF -DWITH_CEPHFS=OFF -DWITH_MGR=OFF -DWITH_RADOSGW=OFF ...` 是刻意的「最小可用」配置,macOS 不是一个完整的 Ceph 服务端开发平台。 > 引自:[build on MacOS](https://docs.ceph.com/en/tentacle/dev/macos/) --- ## 1.6 FreeBSD 实现细节(`freebsd.rst`) > ⚠️ 该页是一份很老的移植笔记,文中指引到 `docs.ceph.com/en/latest/install/manual-freebsd-deployment/`,且通篇围绕 ZFS + 手工部署。 ### 磁盘布局 当前实现工作在 **ZFS pool** 上: - pool 创建在 `/var/lib/ceph` - **每个 OSD 一个 ZFS pool**: ```bash gpart create -s GPT ada1 gpart add -t freebsd-zfs -l osd1 ada1 zpool create -o mountpoint=/var/lib/ceph/osd/osd.1 osd ``` - 可选:加 cache 和 log(ZIL),假设 `ada2` 是 SSD: ```bash gpart create -s GPT ada2 gpart add -t freebsd-zfs -l osd1-log -s 1G ada2 zpool add osd1 log gpt/osd1-log gpart add -t freebsd-zfs -l osd1-cache -s 10G ada2 zpool add osd1 log gpt/osd1-cache ``` - ⚠️ **UFS2 不允许大的 xattribs**(即 UFS2 不可用作 OSD 后端) > 💡 补充:文档示例中第二条 `zpool add osd1 log gpt/osd1-cache` 疑似笔误(cache 设备通常应用 `zpool add osd1 cache ...`)。此处按原文照录,未做修正。 ### 配置 FreeBSD 默认把第三方软件装在 `/usr/local/`,所以 `ceph.conf` 的默认位置是 `/usr/local/etc/ceph/ceph.conf`。最省事的做法是建软链: ```bash ln -s /usr/local/etc/ceph /etc/ceph ``` 示例配置文件位于 `/usr/local/share/doc/ceph/sample.ceph.conf`。 ### MON 与 OSD 创建 - MON:按 https://docs.ceph.com/en/latest/install/manual-freebsd-deployment/ 的手工步骤创建。 - OSD:**只能手工创建**(文档直接指向 `freebsd_adding_osds` 引用,未在本页展开)。 > 引自:[FreeBSD Implementation details](https://docs.ceph.com/en/tentacle/dev/freebsd/) --- # 二、Crimson(`doc/dev/crimson/`) Crimson 是本册最核心、也是当前 Ceph 社区投入最大的重写项目。官方文档目录如下: | 页面 | 内容 | |---|---| | `crimson/crimson.rst` | 总纲:是什么、构建、部署、CPU 分配、后端、vstart 选项、观测、剖析、调试 | | `crimson/osd.rst` | OSDState 状态机 | | `crimson/pipeline.rst` | `ClientRequest` 流水线 | | `crimson/error-handling.rst` | `errorator` 错误处理机制 | | `crimson/backfillmachine.rst` | BackfillMachine 状态机 | | `crimson/poseidonstore.rst` | PoseidonStore 后端设计 | ## 2.1 什么是 Crimson(`crimson/crimson.rst`) **Crimson 是 `crimson-osd` 的代号,即下一代 `ceph-osd`。** 它通过 DPDK、SPDK 等现代技术,在**高速网络与存储设备**上提供增强性能。 关键定位: - Crimson 旨在成为经典 OSD 的 **drop-in replacement(直接替换件)**,允许从现有 `ceph-osd` 部署**通过 backfill 无缝迁移**。 - 项目的**第二阶段**引入了 **SeaStore**(`seastore`)——一个围绕 Crimson 原生架构完全重新设计的对象存储后端,针对 NVMe 这类高性能设备优化,**可能不适合传统 HDD**。 - Crimson 会**继续支持 BlueStore**,以保证对 HDD 和较慢 SSD 的兼容性。 > 引自:[crimson](https://docs.ceph.com/en/tentacle/dev/crimson/crimson/) ## 2.2 设计动机:为什么需要 Crimson 官方文档没有单独写一个「动机」章节,但动机散落在 `crimson.rst` 与 `poseidonstore.rst` 中,可以归纳为: 1. **CPU 成为瓶颈。** 现代分布式存储服务器节点可以挂十几块甚至更多 NVMe SSD。「由于服务器节点上可用的 CPU 资源有限,很难发挥 NVMe SSD 的全部性能。在这样的环境中,CPU 往往成为性能瓶颈。」(`poseidonstore.rst`) 2. **经典 OSD 的线程模型存在锁与上下文切换开销。** Crimson 采用 Seastar 的 share-nothing + run-to-completion 模型,每个线程绑定一个 CPU core,数据按 shard 划分,从而消除大部分锁竞争。 3. **存储栈里的「黑盒」组件带来额外开销。** BlueStore 依赖 RocksDB(一个黑盒 KV 存储)以及为运行 RocksDB 而存在的文件抽象层,这引入了不必要的数据拷贝和序列化/反序列化开销。PoseidonStore 的设计目标之一就是**去掉 RocksDB 这一黑盒组件**。(`poseidonstore.rst`) 4. **轮询(polling)不等于高效。** 「像 SPDK 这样的用户态驱动 I/O 方案通过避免系统调用和零拷贝访问提供高 I/O 性能,但它不支持中断驱动 I/O……轮询有利于低延迟但不利于 CPU 效率。另一方面,中断有利于 CPU 效率而不利于低延迟。」PoseidonStore 因此选择 **io_uring**,并主张:**轮询只应用于网络处理,因为网络通常比存储更快、优先级更高**。(`poseidonstore.rst`) ### Crimson 与经典 OSD 对比 下表由本册依据 `crimson/crimson.rst`、`crimson/pipeline.rst`、`crimson/error-handling.rst`、`crimson/osd.rst`、`crimson/poseidonstore.rst` 的事实整理;凡文档未直接给出对比的条目,均在备注中说明。 | 维度 | 经典 `ceph-osd` | `crimson-osd`(Crimson) | 文档依据 | |---|---|---|---| | 进程/二进制名 | `ceph-osd` | `crimson-osd` | `crimson.rst` | | 线程模型 | 文档未在本册详述(属 `osd_internals/`,另一册) | **Seastar reactor**:`crimson_seastar_num_threads` 个线程服务 reactor,每线程跑在一个专用 CPU core 上;share-nothing / run-to-completion | `crimson.rst`「Crimson CPU allocation」 | | CPU 亲和 | 文档未明确 | 默认**不绑核**;可用 `crimson_seastar_cpu_cores` 显式绑核(**可能**提升性能,但需为每个 OSD 手工设置 CPU set,复杂度高,一般不推荐) | `crimson.rst` | | 存储引擎 | BlueStore(默认) | 原生:**seastore**、**cyanstore**;非原生(经线程池代理 + alien thread):**bluestore**、**memstore** | `crimson.rst`「Object Store Backends」 | | 与后端交互方式 | 直接调用 | 原生后端用 Seastar reactor 做 I/O;非原生后端通过 **thread pool proxy** 与运行在 **alien thread**(不受 Seastar 管理的工作线程)中的 object store 交互 | `crimson.rst` | | 请求排序机制 | 文档未在本册详述 | **Pipeline + Stage**(`crimson/osd/osd_operation.h`):`ConnectionPipeline` → `PerShardPipeline` → `CommonPGPipeline` → `CommonOBCPipeline` | `crimson/pipeline.rst` | | 错误处理 | C++ 异常 | **errorator**:把「预期错误类型」编码进 future 的类型里,编译期检查;非预期错误导致 `static_assert` 编译失败 | `crimson/error-handling.rst` | | 崩溃 backtrace | 会打印人类可读 backtrace | **不会**(处理 SIGSEGV/SIGABRT 时不打印可读 backtrace),需借助 `seastar-addr2line` / `ceph-debug-docker.sh` | `crimson.rst`「Debugging Crimson」 | | daemonize 行为 | 自己 daemonize | **不自行 daemonize**,即使 `daemonize` 选项为真;依赖 systemd,sysvinit 用户可用 `start-stop-daemon` | `crimson.rst`「daemonize」 | | 日志 | Ceph 自有 dout 体系写 `log_file` | 使用 **Seastar logger**;写 stdout 和/或 syslog;`--log-to-stdout`(默认开)、`--log-to-syslog`(默认关) | `crimson.rst`「logging」 | | 指标导出 | perf counters / mgr | 三种:PG stats → mgr(`MPGStats`);admin socket `dump_metrics`;**Prometheus text protocol**(`--prometheus_port`) | `crimson.rst`「Metrics and Tracing」 | | 性能目标 | 通用 | 针对**快速网络与存储设备**提升性能;低 CPU 消耗 | `crimson.rst` 开头 + `poseidonstore.rst` | | 成熟度 | 生产可用 | **tech preview 阶段,不适合生产使用** | `crimson.rst`「Running Crimson」 | | 部署方式 | cephadm 默认 | `cephadm --image quay.ceph.io/ceph-ci/ceph:-crimson-release --allow-mismatched-release bootstrap ...`;cephadm 的 SeaStore 支持处于 **early stages**(tracker issue 71946) | `crimson.rst` | | 开启方式 | 默认 | 需开启实验特性 + OSDMap flag(见 2.4) | `crimson.rst` | ### 成熟度与"当前状态" - **Crimson 处于 tech preview 阶段,不适合生产使用**(原文加粗强调)。 - **Cephadm 的 SeaStore 支持处于 early stages**(文档直接链到 https://tracker.ceph.com/issues/71946)。 - Ceph CI/CD 流水线会构建用 `crimson-osd` 替换标准 `ceph-osd` 的容器。 - `main` 分支**每天构建**一次(https://shaman.ceph.com/builds/ceph/main),镜像在 quay(https://quay.ceph.io/repository/ceph-ci/ceph?tab=tags ,按 `crimson-release` 过滤)。 - 文档原话:「We recommend using one of the latest available builds, as **Crimson evolves rapidly**」(Crimson 演进很快,建议用最新构建)。 - Crimson 构建有两个变体:`crimson-debug` 和 `crimson-release`。**测试应使用 `release` 变体**,`debug` 变体主要面向开发。 > 💡 补充:官方文档**没有**给出 Crimson 的正式路线图/GA 时间表页面。「当前状态」只能从上述零散陈述中归纳,`cephfs`/`rgw`/`mds` 等其他守护进程的 Crimson 化计划文档未明确。 ## 2.3 Crimson 架构 ### 2.3.1 Seastar reactor 与 sharded services - `crimson_seastar_num_threads` 定义用于服务 Seastar reactor 的线程数,**每个线程预期跑在一个专用 CPU core 上**。 - 该参数**没有默认值**。用户必须**在部署 OSD 之前**,在 OSD 级别根据系统资源和集群需求配置。 - 推荐值:**小于** 该主机的 CPU 核数(`nproc`)**除以**该主机上的 OSD 数量。 ```bash # 例:每个 OSD 8 核 ceph config set osd crimson_seastar_num_threads 8 ``` ⚠️ 两个不可不知的约束: 1. **分配选项在部署后不可更改**(原文:Allocation options **cannot** be changed after deployment)。 2. `vstart.sh` 用 `--crimson-smp` 参数设置这些选项。 `crimson_seastar_num_threads` **不会**把线程绑到具体 CPU core。要显式指定,用 `crimson_seastar_cpu_cores`。 「sharded service」这一概念在 `crimson.rst` 的 daemonize 小节被直接提及:为了读取 `daemonize` 选项,`crimson-osd` 需要准备好它的 **config sharded service**,而这个 sharded service 生存在 Seastar reactor 中。 ### 2.3.2 alien thread(外来线程) 非原生后端(bluestore / memstore)**不直接**在 Seastar reactor 上跑 I/O,而是: > Non-native backends operate through a **thread pool proxy**, which interfaces with object stores running in **alien threads**—worker threads not managed by Seastar. 即:**线程池代理 → alien thread(不受 Seastar 管理的工作线程)→ 传统 ObjectStore**。 相关配置项: | 配置项 | 作用 | |---|---| | `crimson_alien_op_num_threads` | 每个 OSD 上专用于服务 BlueStore ObjectStore 的线程数,需按可用 CPU set 设置 | | `crimson_alien_thread_cpu_cores` | 若使用了 `crimson_seastar_num_threads`,应相应设置此项,使**两组 CPU 互斥** | ### 2.3.3 `ClientRequest` 流水线(`pipeline.rst`) **为什么需要它**:RADOS 要求对**每个对象**的写是有序的。如果客户端并发提交一串写(不等前一个完成就提交下一个),客户端依赖这些写**按提交顺序完成**。因此 client→osd 的通信与两侧的排队机制必须保证:对 (connection, object) 这个 pair 而言,写在整个流程中保持有序。 **Crimson 如何保证**:通过 `crimson/osd/osd_operation.h` 中的 **Pipelines 和 Stages**。消息到达 OSD 后进入 `ConnectionPipeline::AwaitActive` stage,然后依次流经: | 阶段 | 含义 | |---|---| | `ConnectionPipeline` | per-connection 的 stage,表示消息在交付给目标 PG 之前的处理路径 | | `PerShardPipeline` | 中间 Pipeline,表示从**接收 shard** 到**持有目标 PG 的 shard** 的交接 | | `CommonPGPipeline` | 在目标 PG 上、获取操作目标对象的 `ObjectContext` **之前**的处理 | | `CommonOBCPipeline` | 对目标对象实际执行 IO 的处理 | **并发性与顺序的折中**(这是本页的精髓): - 因为 `CommonOBCPipeline` 是 **per-object**(而非 per-connection 或 per-pg)的,**同一 PG 内不同对象上的多个请求可以同时处于同一个 `CommonOBCPipeline` stage**。 - 这让我们可以: - **并发服务同一 PG 内的多个读**; - **并发处理多个对象上的写**,直到「真正提交写」的那一点为止。 详见 `crimson/osd/osd_operations/client_request.(h|cc)`。 > 引自:[The ClientRequest pipeline](https://docs.ceph.com/en/tentacle/dev/crimson/pipeline/) ### 2.3.4 OSDState 状态机(`osd.rst`) Crimson OSD 的生命周期状态机(原文档用 graphviz 表达): ```text tick +---------------------------------+ | v +-----------------+ i am healthy! +---------+ | waiting_for_ | -----------------> | preboot | | healthy | <----------------- +---------+ +-----------------+ recv(osdmap) ^ | | send(MOSDBoot) | | v | | +-----------+ | | | booting | | | +-----------+ | | | recv(osdmap) | | v | | +-----------+ kill(SIGINT) | | | active | --------------> (end) | | +-----------+ | | | | | +-----------------------------+ | stop() | i am unhealthy! v | +-----------+ +-----------------------------------| prestop | (via active -> preboot) +-----------+ | recv(osdmap) v (end) ``` 各状态含义: | 状态 | 说明 | |---|---| | `waiting_for_healthy` | 如果 OSD 守护进程**无法**连接到它的心跳 peer,或它自己的**内部心跳**失败,就认为自己不健康,进入 `waiting_for_healthy`,并周期性检查自身可达性与内部心跳。 | | `preboot` | OSD 向已连接的 monitor 发送 `MOSDBoot` 消息,告知集群它已准备好服务,以便 quorum 在 osdmap 中把它标记为 `up`。 | | `booting` | 在被标记为 `up` 之前,OSD 必须停留在 `booting` 状态。 | | `active` | 收到将其标记为 `up` 的 osdmap 后转入 `active`,此后才有权开展业务。但 OSD 服务可能因各种原因被完全停止或挂起:管理员手工停止、在 osdmap 中被标记 `stop`、或它的任一 IP 地址与 osdmap 中配置的不匹配(此时若它自认为健康则转回 `preboot`)。 | | `prestop` | 收到 `stop` 请求后**无条件**转入 `prestop`。但在告别之前,它会尝试获得 monitor 的确认:发送 `MOSDMarkMeDown`,并等待更新后的 osdmap 响应或另一个 `MOSDMarkMeDown` 消息。 | > 引自:[OSDState](https://docs.ceph.com/en/tentacle/dev/crimson/osd/) ### 2.3.5 BackfillMachine 状态机(`backfillmachine.rst`) **实现方式**:Crimson 中的 backfill 使用 **Boost State Chart** 实现(文档链接指向 Boost 1.86 的 statechart 文档)。 **可复现的状态图**:与 `peering.rst` 类似,最新 BackfillMachine 状态模型可用 `gen_state_diagram.py`(https://github.com/ceph/ceph/blob/master/doc/scripts/gen_state_diagram.py)生成。 **状态模型的两条重要规则**(原文档 note): - **任何**状态都可以转换到 `Crushed`。 - **任何**状态(除了 `Initial` 和 `Waiting`)都可以转换到 `Cancelled`。 (为了让图更易读,示例状态图中未画出 `Cancelled` 与 `Crushed` 两个状态。示例图本体是 `crimson_backfillmachine.svg` 图片文件,本册无法展示其像素内容。) **该页的 TODO**(官方自己标注未完成): ``` //TODO: Once the implementation is settled: * Explain exceptional states once we finish working on this code * Explain example happy path flow (code walkthrough?) * https://tracker.ceph.com/issues/68728 ``` 也就是说:异常状态、正常路径的代码走读,官方文档**尚未写完**。 > 引自:[BackfillMachine](https://docs.ceph.com/en/tentacle/dev/crimson/backfillmachine/) ### 2.3.6 错误处理:`errorator`(`error-handling.rst`) **背景:Seastar 的 `future` 与它的两个问题** 在 Seastar 中,`future` 表示一个「尚不可用但之后可用」的值,有三种状态:`unavailable`、`value`、`failed`(计算值时抛异常,异常被捕获并以 `std::exception_ptr` 存进 future)。失败可用 `future::handle_exception()` / `handle_exception_type()` 处理,Seastar 甚至提供 `future::or_terminate()` 在失败时直接终止程序。 但在 Crimson 中,**很多错误严重性不足以终止整个程序**。例如按 object id 查找对象,可能因为对象不存在或已损坏而失败——此时我们应该**去恢复该对象以完成请求**,而不是终止进程。换句话说: 1. 这些错误是**预期的**; 2. 非正常路径(unhappy path)的性能应与正常路径**相当**; 3. 我们希望有一种方式**确保所有预期错误都被处理**——类似编译器对 `switch-case` 未处理枚举值发出警告那样的静态分析。 文档指出 `seastar::future` 无法满足后两条: - Seastar 通过**重新抛异常**来分派不同类型的异常,这既不高效也不可扩展(语言 runtime 中可能发生加锁); - Seastar **不把预期的异常类型编码进返回的 `seastar::future` 类型**,只编码值类型。要确保所有预期错误都被处理,只能靠人工代码审计,心智负担巨大。 **errorator 的诞生** 「errorator」是对原生 `seastar::future` 的包装,解决性能与可扩展性问题,同时**把所有预期错误类型的信息嵌入 future 的类型**。 ```cpp using ertr = crimson::errorator; ``` 上例定义了一个允许两种错误类型的 errorator:`crimson::ct_error::enoent` 和 `crimson::ct_error::einval`。 `crimson::ct_error` 命名空间下的这些类型本质上是 **`std::error_code` 的不可抛(unthrowable)包装**,以排除意外的抛出,并使错误信号化从而支持编译期检查。 **用法 1:作为返回类型** ```cpp static ertr::future foo(int bar) { if (bar == 42) { return crimson::ct_error::einval::make(); } else { return ertr::make_ready_future(bar); } } ``` **关键性质:返回不在 errorator 错误集合内的错误 → 编译期错误** ```cpp static ertr::future foo(int bar) { // Oops, input_output_error is not allowed in `ertr`. static_assert() will // terminate the compilation. return crimson::ct_error::input_output_error::make(); } ``` 文档强调这个行为「absolutely fundamental」:调用者只需**看函数签名**就知道需要担心哪些错误,不再需要通读实现。 **用法 2:`safe_then()` 与错误集合的「加减法」** `then()` 只在 errorator 的错误集合**为空**(即 `errorator<>::future`)时可用;否则调用者必须用 `safe_then()`: ```cpp seastar::future<> baz() { return foo(42).safe_then( [] (const int bar) { std::cout << "the optimistic path! got bar=" << bar << std::endl return ertr::now(); }, ertr::all_same_way(const std::error_code& err) { // handling errors removes them from errorator's error set std::cout << "the error path! got err=" << err << std::endl; return ertr::now(); }).then([] { // as all errors have been handled, errorator's error set became // empty and the future instance returned from `safe_then()` has // `then()` available! return seastar::now(); }); } ``` `ertr::all_same_way` 用于「用同一种方式处理所有错误」。这不是必须的——调用者可以**分别处理**每种错误,也可以**只处理一部分**,代价是 `then()` 的可用性: ```cpp using einval_ertr = crimson::errorator; // we can't return seastar::future<> as this level deals only with enoent // leaving einval without a handler. Handling it becomes a responsibility // of a caller of `baz()`. einval_ertr::future<> baz() { return foo(42).safe_then( [] (const int bar) { std::cout << "the optimistic path! got bar=" << bar << std::endl return ertr::now(); }, // provide a handler only for crimson::ct_error::enoent. // crimson::ct_error::einval stays unhandled! crimson::ct_error::enoent::handle([] { std::cout << "the enoent error path!" << std::endl; return ertr::now(); })); // .safe_then() above returned `errorator::future<>` // which lacks `then()`. } ``` **反向也成立**:在 `safe_then()` 中返回新的错误会**追加**到错误集合,当然该集合必须与 `baz()` 签名中的错误集合兼容: ```cpp using broader_ertr = crimson::errorator; broader_ertr::future<> baz() { return foo(42).safe_then( [] (const int bar) { std::cout << "oops, the optimistic path generates a new error!"; return crimson::ct_error::input_output_error::make(); }, // we have a special handler to delegate the handling up. For convenience, // the same behaviour is available as single argument-taking variant of // `safe_then()`. ertr::pass_further{}); } ``` **一句话总结**:在 `safe_then()` 中处理和发出错误,本质上是一个**在编译期检查的错误集合运算**。 更多细节见 Seastar Summit 2019 的演讲幻灯片:`ceph::errorator<> throw/catch-free, compile time-checked exceptions for seastar::future<>`。 > 引自:[error handling](https://docs.ceph.com/en/tentacle/dev/crimson/error-handling/) ## 2.4 构建、部署与启用 ### 构建 Crimson **默认不启用**,需在构建时打开: ```bash $ WITH_CRIMSON=true ./install-deps.sh $ ./do_cmake.sh -DWITH_CRIMSON=ON ``` > ⚠️ 注意:**如果 Crimson 是从 git clone 的源码构建的,`ASan`(AddressSanitizer)默认开启。** ### 用 cephadm 部署 ```bash cephadm --image quay.ceph.io/ceph-ci/ceph:-crimson-release --allow-mismatched-release bootstrap ... ``` - 使用非 release 分支时,很可能需要加 `--allow-mismatched-release`。 - 变体选择:`crimson-release`(测试用)/ `crimson-debug`(开发用)。 ### 启用 Crimson(三条命令,缺一不可) 在集群起来之后、部署 OSD 之前执行: ```bash ceph config set global 'enable_experimental_unrecoverable_data_corrupting_features' crimson ceph osd set-allow-crimson --yes-i-really-mean-it ceph config set mon osd_pool_default_crimson true ``` 逐条解释: | 命令 | 作用 | |---|---| | 第 1 条 | 启用 `crimson` 实验特性 | | 第 2 条 | 启用 `allow_crimson` OSDMap flag。**monitor 不会允许 `crimson-osd` 在没有该 flag 的情况下启动。** | | 第 3 条 | 让池在创建时默认带上 `crimson` flag。Crimson 池**只能**执行 Crimson 支持的操作;`crimson-osd` **不会**为非 Crimson 池实例化 PG。 | > 💡 补充:`vstart.sh` 在使用 `--crimson` 时**自动**启用 crimson(见下节)。 ### vstart.sh 中 Crimson 相关选项 | 选项 | 含义 | |---|---| | `--crimson` | 启动 `crimson-osd` 而不是 `ceph-osd` | | `--nodaemon` | 不 daemonize 服务 | | `--redirect-output` | 把 `stdout`/`stderr` 重定向到 `out/$type.$num.stdout` | | `--osd-args` | 向 `crimson-osd` / `ceph-osd` 传额外命令行选项。对传 Seastar 选项很有用,例如 `--osd-args "--memory 2G"` 设置内存用量。更多 Seastar 选项见 `crimson-osd --help-seastar` 的输出 | | `--crimson-smp` | 每个 OSD 使用的 core 数。若用 BlueStore,剩余可用核(按 `nproc` 计算)分配给 object store | | `--bluestore` | 使用 alienized BlueStore 作为 object store 后端(**默认**) | | `--cyanstore` | 使用 CyanStore 作为后端 | | `--memstore` | 使用 alienized MemStore 作为后端 | | `--seastore` | 使用 SeaStore 作为后端 | | `--seastore-devs` | 指定 SeaStore 使用的块设备 | | `--seastore-secondary-devs` | 可选。SeaStore 支持多设备,把块设备传给此选项即可启用 | | `--seastore-secondary-devs-type` | 可选。指定 secondary device 类型。当 secondary 设备比 `--seastore-devs` 指定的主设备慢时,快设备中的**冷数据会逐渐被驱逐到慢设备**。合法值:`HDD`、`SSD`(默认)、`ZNS`、`RANDOM_BLOCK_SSD`。注意:secondary 设备**不应**比主设备更快 | **示例 1:单节点 Crimson + BlueStore** ```bash $ MGR=1 MON=1 OSD=1 MDS=0 RGW=0 ../src/vstart.sh \ --without-dashboard --bluestore --crimson \ --redirect-output ``` **示例 2:SeaStore + 主/从设备** ```bash $ MGR=1 MON=1 OSD=1 MDS=0 RGW=0 ../src/vstart.sh -n -x \ --without-dashboard --seastore \ --crimson --redirect-output \ --seastore-devs /dev/sda \ --seastore-secondary-devs /dev/sdb \ --seastore-secondary-devs-type HDD ``` **停止该 vstart 集群:** ```bash $ ../src/stop.sh --crimson ``` ### `daemonize` 行为的内部原因(值得单独记住) 与 `ceph-osd` 不同,**`crimson-osd` 即使 `daemonize` 选项为真也不会自己 daemonize**。原因: > 为了读取这个选项,`crimson-osd` 需要准备好它的 config sharded service,但这个 sharded service 生存在 Seastar reactor 中。如果我们 fork 一个子进程并在启动 Seastar 引擎后退出父进程,那将只剩下一个线程,它是调用 `fork()` 的那个线程的副本。在 Crimson 中解决这个问题会让代码不必要地复杂化。 由于受支持的 GNU/Linux 发行版都使用 `systemd`(能够 daemonize 进程),没有必要自己 daemonize。使用 sysvinit 的用户可以用 `start-stop-daemon` 来 daemonize `crimson-osd`。如果这也不行,可以设计一个辅助工具。 ### 日志级别映射(`dout.h` → Seastar) `Crimson-osd` 目前使用 Seastar 提供的日志工具。`src/common/dout.h` 中有 Ceph 日志级别到 Seastar 严重级别的映射。例如发往 `derr` 的消息用 `logger::error()` 发出,debug level 大于 `20` 的消息用 `logger::trace()` 发出。 | ceph | seastar | |---|---| | `< 0` | `error` | | `0` | `warn` | | `[1, 6)` | `info` | | `[6, 20]` | `debug` | | `> 20` | `trace` | 注意:`crimson-osd` **不会**把日志消息直接发送到指定的 `log_file`,而是写到 stdout 和/或 syslog。可用 `--log-to-stdout` 和 `--log-to-syslog` 命令行选项改变。默认 `--log-to-stdout` 启用、`--log-to-syslog` 禁用。 ## 2.5 对象存储后端(Object Store Backends) `crimson-osd` 支持两大类后端:**原生(native)** 与 **非原生(non-native)**。 ### 原生后端 用 **Seastar reactor** 执行 I/O 操作,与 Seastar 框架紧耦合,遵循其设计原则。 | 后端 | 说明 | |---|---| | `seastore` | Crimson OSD 的**主要原生对象存储**。用 Seastar 框架构建,遵循其异步、基于 shard 的架构。 | | `cyanstore` | 灵感来自经典 OSD 的 `memstore`,提供轻量级**内存**对象存储模型。**CyanStore 不存储数据**,只应用于测量 OSD 自身开销,而不必承担真正存数据的代价。 | ### 非原生后端 通过 **thread pool proxy** 操作,该代理与运行在 **alien thread**(不受 Seastar 管理的工作线程)中的 object store 交互,使 Crimson 能与遗留或外部 object store 实现互操作。 | 后端 | 说明 | |---|---| | `bluestore` | 经典 `ceph-osd` 使用的默认对象存储,提供健壮的生产级存储能力。需要按可用 CPU set 设置 `crimson_alien_op_num_threads`;若使用 `crimson_seastar_num_threads`,应相应使用 `crimson_alien_thread_cpu_cores` 使两组 CPU 互斥。 | | `memstore` | 内存对象存储后端,主要用于测试与开发。 | ## 2.6 指标、追踪与性能剖析 ### 三种上报方式 **1)PG stats 上报给 mgr** Crimson 把 per-pg、per-pool、per-osd 的统计收集到 `MPGStats` 消息中发给 Ceph Manager。Manager 模块可通过 `MgrModule.get()` 方法查询。 **2)Admin socket 命令** ```bash $ ceph tell osd.0 dump_metrics $ ceph tell osd.0 dump_metrics reactor_utilization ``` `reactor_utilization` 是一个可选字符串,用于按前缀过滤要 dump 的指标。 **3)Prometheus text protocol** 监听端口和地址可用命令行选项 `--prometheus_port` 配置(详见 Seastar 的 prometheus 文档)。 ### 用 `fio` 剖析(`crimson-store-nbd`) `crimson-store-nbd` 把可配置的 `FuturizedStore` 内部机制暴露为一个 **NBD server**,供 `fio` 使用。 **第 1 步:安装 `libnbd` 并把它编译进 `fio`** ```bash apt-get install libnbd-dev git clone git://git.kernel.dk/fio.git cd fio ./configure --enable-libnbd make ``` **第 2 步:构建 `crimson-store-nbd`** ```bash cd build ninja crimson-store-nbd ``` **第 3 步:用块设备启动 `crimson-store-nbd` server** ```bash export disk_img=/tmp/disk.img export unix_socket=/tmp/store_nbd_socket.sock rm -f $disk_img $unix_socket truncate -s 512M $disk_img ./bin/crimson-store-nbd \ --device-path $disk_img \ --smp 1 \ --mkfs true \ --type transaction_manager \ --uds-path ${unix_socket} & ``` (要用真实块设备测试时,把 `$disk_img` 换成裸设备路径,例如 `/dev/nvme1n1`) 命令行参数说明: | 参数 | 含义 | |---|---| | `--smp` | 使用的 CPU 核数(Symmetric MultiProcessor) | | `--mkfs` | 先初始化设备 | | `--type` | 使用的后端。若指定 `transaction_manager`,则用 SeaStore 的 `TransactionManager` 和 `BlockSegmentManager` 模拟块设备;否则该选项用于选择 `FuturizedStore` 的某个后端,此时整个「设备」被划分为多个固定大小的对象,大小由 `--object-size` 指定。**如果你只想测试 SeaStore 的底层实现(如逻辑地址转换层、垃圾回收)而不需要对象存储语义,`transaction_manager` 是更好的选择。** | **第 4 步:创建 `fio` job 文件 `nbd.fio`** ```ini [global] ioengine=nbd uri=nbd+unix:///?socket=${unix_socket} rw=randrw time_based runtime=120 group_reporting iodepth=1 size=512M [job0] offset=0 ``` **第 5 步:用刚编译的 `fio` 测试** ```bash ./fio nbd.fio ``` ### 用 `cbt` 做性能对比测试 ```bash $ git checkout main $ make crimson-osd $ ../src/script/run-cbt.sh --cbt ~/dev/cbt -a /tmp/baseline ../src/test/crimson/cbt/radosbench_4K_read.yaml $ git checkout yet-another-pr $ make crimson-osd $ ../src/script/run-cbt.sh --cbt ~/dev/cbt -a /tmp/yap ../src/test/crimson/cbt/radosbench_4K_read.yaml $ ~/dev/cbt/compare.py -b /tmp/baseline -a /tmp/yap -v 19:48:23 - INFO - cbt - prefill/gen8/0: bandwidth: (or (greater) (near 0.05)):: 0.183165/0.186155 => accepted 19:48:23 - INFO - cbt - prefill/gen8/0: iops_avg: (or (greater) (near 0.05)):: 46.0/47.0 => accepted 19:48:23 - WARNING - cbt - prefill/gen8/0: iops_stddev: (or (less) (near 0.05)):: 10.4403/6.65833 => rejected ... 19:48:23 - WARNING - cbt - 1 tests failed out of 16 ``` 方法:对 `main` 和 `yet-another-pr` 两个分支编译并跑同一测试,然后比较结果。每个测试用例都定义了一组规则用于检查性能回归;若发现可能的回归,规则与对应测试结果会被高亮。 `cbt` 项目地址:https://github.com/ceph/cbt ## 2.7 调试 Crimson ### GDB 调试 Scylla 的 tips 同样适用于 Crimson(文档链到 Scylla 的 `docs/dev/debugging.md#tips-and-tricks`)。 ### 用 `addr2line` 得到人类可读 backtrace Seastar 应用崩溃时,留下的只有一串地址的 backtrace: ```text Segmentation fault. Backtrace: 0x00000000108254aa 0x00000000107f74b9 0x00000000105366cc ... /lib/x86_64-linux-gnu/libc.so.6+0x000000000002409a 0x000000000d833ac9 Segmentation fault ``` Seastar 提供的 `seastar-addr2line` 工具可把这些地址映射为函数。它从 `stdin` 读取输入,因此需要粘贴上述地址,然后用 `control-D` 发送 EOF;也可以用 `echo` 或 `cat`: ```bash $ ../src/seastar/scripts/seastar-addr2line -e bin/crimson-osd 0x00000000108254aa 0x00000000107f74b9 ... [Backtrace #0] seastar::backtrace_buffer::append_backtrace() at /home/kefu/dev/ceph/build/../src/seastar/src/core/reactor.cc:1136 seastar::print_with_backtrace(seastar::backtrace_buffer&) at /home/kefu/dev/ceph/build/../src/seastar/src/core/reactor.cc:1157 seastar::print_with_backtrace(char const*) at /home/kefu/dev/ceph/build/../src/seastar/src/core/reactor.cc:1164 seastar::sigsegv_action() at /home/kefu/dev/ceph/build/../src/seastar/src/core/reactor.cc:5119 seastar::install_oneshot_signal_handler<11, &seastar::sigsegv_action>()::{lambda(int, siginfo_t*, void*)#1}::operator()(int, siginfo_t*, void*) const at .../reactor.cc:5105 ... main at /home/kefu/dev/ceph/build/../src/crimson/osd/main.cc:131 (discriminator 1) ``` 注意 `seastar-addr2line` 能**从输入中提取地址**,所以也可以直接粘贴日志消息: ```text 2020-07-22T11:37:04.500 INFO:teuthology.orchestra.run.smithi061.stderr:Backtrace: 2020-07-22T11:37:04.500 INFO:teuthology.orchestra.run.smithi061.stderr: 0x0000000000e78dbc 2020-07-22T11:37:04.501 INFO:teuthology.orchestra.run.smithi061.stderr: 0x0000000000e3e7f0 ... 2020-07-22T11:37:04.501 INFO:teuthology.orchestra.run.smithi061.stderr: /lib64/libpthread.so.0+0x0000000000012dbf ``` **与经典 `ceph-osd` 的一个关键区别**:Crimson 在处理 `SIGSEGV` / `SIGABRT` 等致命信号时**不打印人类可读 backtrace**;对于 stripped binary 更复杂。因此与其在 Crimson 里植入信号处理程序,不如用 `script/ceph-debug-docker.sh` 来映射 backtrace 中的地址: ```bash # 假设你在 ceph 源码树下 $ ./src/script/ceph-debug-docker.sh --flavor crimson master:27e237c137c330ebb82627166927b7681b20d0aa centos:8 .... [root@3deb50a8ad51 ~]# wget -q https://raw.githubusercontent.com/scylladb/seastar/master/scripts/seastar-addr2line [root@3deb50a8ad51 ~]# dnf install -q -y file [root@3deb50a8ad51 ~]# python3 seastar-addr2line -e /usr/bin/crimson-osd # 在此粘贴 backtrace ``` ### 其他资源 - Seastar 文档:https://github.com/scylladb/seastar/blob/master/doc/tutorial.md 或本地构建可浏览版本并起 HTTP server: ```bash $ cd seastar $ ./configure.py --mode debug $ ninja -C build/debug docs $ python3 -m http.server -d build/debug/doc/html ``` (可能需要预先安装 `pandoc` 等依赖) - 视频走读: - Ceph Code Walkthroughs: Crimson — https://www.youtube.com/watch?v=rtkrHk6grsg - Ceph Code Walkthroughs: SeaStore — https://www.youtube.com/watch?v=0rr5oWDE2Ck > 引自:[crimson](https://docs.ceph.com/en/tentacle/dev/crimson/crimson/) ## 2.8 PoseidonStore(Crimson 的可插拔后端之一) > 说明:`poseidonstore.rst` 是一份**设计文档**(含大量 TODO 式「Plans」),描述的是一个**未完成**的后端。阅读时应把它当作设计提案而非已落地实现。 ### 关键概念与目标 - PoseidonStore 作为 Crimson 的可插拔后端存储之一,**只针对高端 NVMe SSD**(不关心 ZNS 设备)。 - **完全为低 CPU 消耗而设计**: - 对不同数据类型采用**混合更新策略**(in-place / out-of-place),通过减少 host-side GC 来最小化 CPU 消耗; - **去掉像 RocksDB 这样的黑盒组件**以及 BlueStore 中的文件抽象层,避免不必要的开销(数据拷贝、序列化/反序列化); - 利用 NVMe 特性(原子大写命令、Atomic Write Unit Normal);利用 **io_uring**(新的内核异步 I/O 接口),有选择地使用**中断驱动模式**以节省 CPU(或对低延迟场景用轮询模式)。 - **Sharded data/processing 模型**。 ### 背景:两种更新策略的权衡 | | Log-structured store(out-of-place) | In-place update store | |---|---|---| | 做法 | 从不修改已写入的数据,写总是追加到日志尾部,实现 I/O 顺序化 | 块一旦放在某个磁盘位置就不再移动,写直接落到对应位置(ext4/xfs 即如此) | | Pros | 一次顺序写即可存下数据;天然支持事务(无覆盖,可回滚到上一个稳定状态);闪存友好(减轻 SSD GC 负担) | host CPU 消耗更少(无需 host-side GC);查找快;无需 log-structured 的额外空间(但有内部碎片) | | Cons | 存在 host-side GC,带来 I/O 放大与更多 host CPU 消耗;元数据查找慢;空间开销(有效数据与无用数据共存) | 需要更多次写来记录数据(元数据与数据区分离);无法支持事务,一般需要某种 WAL 保证更新原子性;闪存不友好(把 GC 负担甩给 SSD) | ### 动机与三个核心设计选择 **背景观察**:现代分布式存储系统中,一个服务器节点可以配多块 NVMe。事实上十块以上 NVMe SSD 也很常见。结果是,由于服务器节点 CPU 资源有限,很难发挥 NVMe SSD 的全部性能,CPU 往往成为瓶颈。因此现在应当聚焦于**最小化 host-CPU 消耗**——这与 Crimson 的目标一致。 三个设计选择: 1. **PoseidonStore 没有像 BlueStore 中的 RocksDB 那样的黑盒组件。** 因此可避免不必要的数据拷贝与序列化/反序列化开销;而且可以去掉为运行 RocksDB 而必需的文件抽象层。对象数据与元数据现在**直接映射到磁盘块**。消除这些开销会降低 CPU 消耗(例如预分配、NVMe 原子特性)。 2. **PoseidonStore 对不同数据大小采用混合更新策略,类似 BlueStore。** 由于 CPU 只在**小 I/O** 负载下成为瓶颈,我们对小 I/O 选用 **update-in-place** 以最小化 CPU 消耗;对大 I/O 选用 **update-out-of-place** 以避免双写。就长期 CPU 消耗而言,小数据双写可能优于 host-GC 开销(尽管它把 GC 完全交给 SSD)。 3. **PoseidonStore 使用 io_uring 来利用中断驱动 I/O。** 像 SPDK 这样的用户态方案不支持中断驱动 I/O(只有内核态驱动 I/O 才行)。轮询利于低延迟但不利于 CPU 效率;中断利于 CPU 效率而不利于低延迟(但随着 I/O size 增大没那么糟)。注意像 DPDK 这样的网络加速方案也会为轮询过度消耗 CPU。**由于网络通常比存储更快且优先级更高,轮询应只用于网络处理。** 补充论点:高端 NVMe SSD 有足够能力处理更多工作;SSD 寿命如今也不是实际顾虑(P/E cycle 限制足够,引用 FAST 2020 的 SSD 可靠性研究)。另一方面,对于大 I/O 负载,host 负担得起 host-GC;而且当无效对象较大时,host 垃圾回收更高效。 ### 观察:Ceph 中的两类数据 | 数据类型 | 特点 | 最佳存储方式 | |---|---|---| | **Data**(对象数据) | 双写代价高 | **in-place update**。虽然至少需要两次操作(1 数据、2 数据位置),但即使会加剧 SSD 的 WAF,**常数次操作**仍优于 out-of-place | | **Metadata 或 small data**(如 `object_info_t`、snapset、pg_log、collection) | 一个对象对应多个小元数据条目 | **WAL + 使用 cache**。高效做法是把与数据相关的所有元数据合并起来,通过**单次写操作**存储,即使这要求后台 flush 来更新 data partition | ### 磁盘布局 ```text +-WAL partition-|----------------------Data partition-------------------------------+ | Sharded partition | +-----------------------------------------------------------------------------------+ | WAL -> | | Super block | Freelist info | Onode radix tree info| Data blocks | +-----------------------------------------------------------------------------------+ | Sharded partition 2 +-----------------------------------------------------------------------------------+ | WAL -> | | Super block | Freelist info | Onode radix tree info| Data blocks | +-----------------------------------------------------------------------------------+ | Sharded partition N +-----------------------------------------------------------------------------------+ | WAL -> | | Super block | Freelist info | Onode radix tree info| Data blocks | +-----------------------------------------------------------------------------------+ | Global information (in reverse order) +-----------------------------------------------------------------------------------+ | Global WAL -> | | SB | Freelist | | +-----------------------------------------------------------------------------------+ ``` - **WAL**:日志、元数据和小数据存在 WAL 分区中;WAL 分区内的空间以**循环方式持续复用**;必要时 flush 数据以 trim WAL。 - **Disk layout**:数据块要么是元数据块要么是数据块;**Freelist** 管理空闲空间 B+tree 的根;**Super block** 含一个 data partition 的管理信息;**Onode radix tree info** 含 onode radix tree 的根。 ### I/O 流程 **写**(按请求大小分流): 1. **若 Request Size ≤ Threshold**(类似 BlueStore 的 minimum allocation size): 把数据和元数据写到 `[WAL]` —flush→ 分别写到 `[Data section (in-place)]` 和 `[Metadata section]`。 因为 CPU 在小 I/O 负载下成为瓶颈,所以用 in-place 更新。 2. **若 Request Size > Threshold**: 把数据追加到 `[Data section (log-structure)]` → 把相应元数据写到 `[WAL]` —flush→ 把元数据写到 `[Metadata section]`。 大 I/O 负载下 host 负担得起 host-GC。 > 注意:**Threshold 可以配置成一个非常大的数,使得只有场景 (1) 发生。** 通过这个设计,我们可以结合上述针对 crimson 的优化来控制整体 I/O 流程。 **详细流程**(利用 NVMe 提供原子性保证的写命令 Atomic Write Unit Power Fail,例如 512 KB 数据可原子一次写入而无需 `fsync()`): - **stage 1** - 数据小:WAL (written) --> `| TxBegin A | Log Entry | TxEnd A |` 用 NVMe 原子写命令向 WAL 追加一条日志条目,包含 pg_log、snapset、`object_info_t` 和块分配信息。 - 数据大:Data partition (written) --> `| Data blocks |` - **stage 2** - 数据小:**不需要**。 - 数据大:然后把元数据追加到 WAL:`| TxBegin A | Log Entry | TxEnd A |` **读**: - 用**缓存的对象元数据**找到数据位置; - 若未缓存,需要在 checkpoint 之后搜索 WAL,以及搜索 Object meta partition,以找到最新的元数据。 **Flush(WAL → Data partition)**: - flush 已提交的 WAL 条目。两个触发条件:1)WAL 接近满;2)收到 flush 信号。 - 可以通过批处理来缓解频繁 flush 的开销,但那会导致**完成延迟**。 ### 崩溃一致性 **Large case:** | 场景 | 结论 | |---|---| | 写完 Data blocks 后立刻崩溃 | 无需关心。数据尚未分配,这些块会被复用。 | | 写完 WAL 后崩溃 | 写流程已完成,没有数据丢失或不一致状态。 | **Small case:** | 场景 | 结论 | |---|---| | 写完 WAL 后崩溃 | 所有数据都已写入(`TxBegin A / Log Entry / TxEnd A` 完整) | ### 与 LSM-tree 的对比 - **Best case(预分配)**:只需在 WAL 和 Data partition 上各写一次,**无需**更新对象元数据(位置信息)。 - **Worst case**:至少需要在 WAL、对象元数据、数据块上**额外**三次写。如果从 WAL 到 data partition 的 flush 频繁发生,radix tree onode 结构需要多次更新。为最小化该开销,可利用批处理来减少对树的更新(与某对象相关的数据具有局部性,因为它们有相同的父节点,所以更新可被最小化)。 - **WAL 需要 flush 的条件**:WAL 接近满,或收到 flush 信号。这一设计背后的前提是 **OSD 可以把最新元数据作为单一副本管理**,所以追加的条目不需要被读取。 - 无论最好还是最坏情况,**都不会产生严重的 I/O 放大**(会产生 I/O,但 I/O 速率是恒定的),不像 LSM-tree DB。(文档自评:proposed design 类似只有 level-0 的 LSM-tree。) ### 详细设计要点 **Onode 查找** - **Radix tree**:设计完全基于前缀树。Ceph 已经利用 OID 前缀的特性来切分或搜索 OID(例如 `pool id + hash + oid`),所以前缀树很适合存储或搜索对象。 - **Sharded partition**:OID 的 hash 的**最左边若干 bit** 决定对象位于哪个 sharded partition。例如如果分区数配置为 4,`hobject_t` 中整个 hash 空间被分为四段:`0x0xxx~0x3xxx`、`0x4xxx~0x7xxx`、`0x8xxx~0xBxxx`、`0xCxxx~0xFxxx`。 - **On-disk onode**: ```c struct onode { extent_tree block_maps; b+_tree omaps; map xattrs; } ``` onode 包含用于查找的 radix tree 节点,意味着可以用 onode 中的树节点信息搜索对象。另外,如果数据很小,onode 可以**内嵌**数据和 xattrs。**onode 是固定大小(256 或 512 字节)**;而 omaps 和 block_maps 通过 onode 中的指针实现为**变长**。 ```text +----------------+------------+--------+ | on-disk onode | block_maps | omaps | +----------+-----+------------+--------+ | ^ ^ | | | +-----------+---------+ ``` - **Lookup**:onode 树的根位置在 「Onode radix tree info」中指定,因此可用前缀树的根找出对象位置。先用 OID 确定 sharded partition,再用 OID 剩余 bit 和 radix tree 找出 onode 的位置。extent tree(`block_maps`)包含数据块的位置,于是最终确定数据位置。 **分配** - **Sharded partitions (SP)**:整个磁盘空间被划分为若干称为 sharded partition 的数据块,每个 SP 有自己的管理数据结构。 - **Data allocation**:管理信息(super block、freelist info、onode radix tree info)在每个 shared partition 中**预先分配**。给定 OID,可以把 Data block section 中的任何数据映射到 onode 里的 extent tree。块可通过搜索空闲空间跟踪数据结构来分配。 onode radix tree root node block(Per-SP Meta)结构: ```text +-----------------------------------+ | onode radix tree root node block | | (Per-SP Meta) | | | | # of records | | left_sibling / right_sibling | | +--------------------------------+| | | keys[# of records] || | | +-----------------------------+|| | | | start onode ID ||| | | | ... ||| | | +-----------------------------+|| | +--------------------------------+| | +--------------------------------+| | | ptrs[# of records] || | | +-----------------------------+|| | | | SP block number ||| | | | ... ||| | | +-----------------------------+|| | +--------------------------------+| +-----------------------------------+ ``` - **Free space tracking**:空闲空间**按 SP** 跟踪。可以像 XFS 那样用 extent-based B+tree 跟踪空闲空间。freelist info 含空闲空间 B+tree 的根。**粒度是 Data blocks partition 中的 data block,data block 是最小且固定大小的数据单位。** ```text +-----------------------------------+ | Free space B+tree root node block | | (Per-SP Meta) | | | | # of records | | left_sibling / right_sibling | | +--------------------------------+| | | keys[# of records] || | | +-----------------------------+|| | | | startblock / blockcount ||| | | | ... ||| | | +-----------------------------+|| | +--------------------------------+| | +--------------------------------+| | | ptrs[# of records] || | | +-----------------------------+|| | | | SP block number ||| | | | ... ||| | | +-----------------------------+|| | +--------------------------------+| +-----------------------------------+ ``` **Omap 与 xattr**:在此设计中,omap 和 xattr 数据由 onode 中的 b+tree 跟踪。onode 只有 b+tree 的根节点,根节点包含指示 key onode 所在位置的条目。因此只要知道 onode,就能通过 omap b+tree 找到 omap。 **碎片** - **内部碎片**:在一个块中尽可能多地打包不同类型的数据/元数据以减少内部碎片。Extent-based B+tree 可通过分配最适合对象的连续块进一步减少碎片。 - **外部碎片**:频繁的对象创建/删除可能导致外部碎片,此时需要清理工作(类 GC)。为此参考 NetApp 的 **Continuous Segment Cleaning**(*Countering Fragmentation in an Enterprise Storage System*, NetApp, ACM TOS, 2020),文档认为这与 SeaStore 的做法类似。 **WAL**:每个 SP 有一个 WAL。写入 WAL 的是**元数据更新、空闲空间更新和小数据**。注意只有**小于预定义 threshold** 的数据需要写 WAL;更大的数据写到**未分配的空闲空间**,并相应更新其 onode 的 extent_tree(以及 on-disk extent tree)。WAL 分区是**静态分配**的,与 data partition 分开,需预配置。 **Partition 与 Reactor thread**:早期开发阶段,PoseidonStore 采用**静态分区分配**——sharded partition 的数量固定,每个分区的大小也需在运行集群前配置。但分区数量可以按如下方式增长,这部分留作未来工作。**每个 reactor thread 拥有一组静态的 SP。** ```text +------+------+-------------+------------------+ | SP 1 | SP N | --> <-- | global partition | +------+------+-------------+------------------+ ``` **Cache**:主要有两种缓存数据结构——onode cache 和 block cache: 1. **Onode cache**: `lru_map ;` 2. **Block cache (data and omap)**: Data cache --> `lru_map ` 要填充 onode 数据结构,需要用前缀树检索目标 onode。Block cache 用于缓存块内容。对一个事务而言,所有对块(包括 object meta block、data block)的更新**先在内存 block cache 中执行**;把事务写入 WAL 之后,脏块才被 flush 到各自分区中的相应位置。PoseidonStore 可以为每种类型配置缓存大小,两者都可用简单的 LRU 淘汰策略。 **Sharded partitions 与跨 SP 事务**:整个磁盘空间被划分为若干称为 sharded partitions (SP) 的块。**父 collection ID(collection 切分之前的原始 collection ID,即 `hobject.hash`)的前缀**用于将任意 collection 映射到 SP。可以沿用 BlueStore 的 collection 切分方法,即改变 collection 前缀的有效 bit 数。因为父 collection ID 的前缀即使在 collection 切分后也不变,collection 与 SP 的映射得以保持。**SP 的数量可以配置为与每个磁盘分配的 CPU 数相匹配**,使每个 SP 容纳足够多的对象,从而让跨 SP 事务不发生。 如果需要跨 SP 事务,可以使用 **global WAL**。协调线程(主要管理 global partition)通过**在处理跨 SP 事务前获取源 SP 和目标 SP 的锁**来处理跨 SP 事务——源和目标大概率会被阻塞。对于负载不均衡的情况,PoseidonStore 可以创建分区以高效利用整个空间并提供负载均衡。 **CoW/Clone**:对于 CoW/Clone,**一个 clone 像其他普通对象一样拥有自己的 onode**。虽然每个 clone 有自己的 onode,但**如果数据块没有变化,应该在原始对象和 clone 之间共享**,以最小化空间开销。为此需要数据块的**引用计数**来管理这些共享数据块。为处理带引用计数的数据块,PoseidonStore 使用了 **`shared_blob`**,它维护被引用的数据块;`shared_blob` 由 **superblock 中的 `shared_blob_list`** 管理。 ```text /----------\ /----------\ | Object A | | Object B | +----------+ +----------+ | Extent | | Extent | +---+--+---+ +--+----+--+ | | | | | | +----------+ | | | | | | +---------------+ | | | | | v v v v +---------------+---------------+ | Data block 1 | Data block 2 | +-------+-------+------+--------+ | | v v /---------------+---------------\ | shared_blob 1 | shared_blob 2 | +---------------+---------------+ shared_blob_list | refcount | refcount | +---------------+---------------+ ``` ### Plans(官方列出的开发计划,说明这些都是 To-Do) 所有 PR 都应包含单元测试以验证其最小功能。 | 计划项 | 内容 | |---|---| | WAL 和 block cache 实现 | 第一步构建 WAL,包括读写 WAL 的 I/O 流程。随着 WAL 开发,block cache 需要一起开发。此外要添加一个 I/O 库以从/向 NVMe 存储读写,从而利用 NVMe 特性和异步接口 | | Radix tree 和 onode | 先提一个 PR 给出更详细的 on-disk layout 和 onode radix tree 的查找策略;设计 PR 合并后再做实现。第二个 PR 是 radix tree 的实现,它是查找对象的关键结构 | | Extent tree | 管理 onode 中数据块的 extent tree,并演示查找对象时它如何工作 | | B+tree for omap | 为 omap 提供一个简单的 key/value 接口,可能是一个单独的 PR | | CoW/Clone | 添加 `shared_blob` 和 `shared_blob_list` | | 与 Crimson 的 I/O 接口集成 | 使 `queue_transaction()`、`read()`、`clone_range()` 等与 Crimson 交互的接口能正常工作 | | Configuration | 详细定义 Poseidon store 的配置 | | 压力测试环境与 teuthology 集成 | 添加压力测试和 teuthology suites | > 引自:[PoseidonStore](https://docs.ceph.com/en/tentacle/dev/crimson/poseidonstore/) --- # 三、CephFS 相关开发文档 ## 3.1 CephFS Mirroring(`cephfs-mirroring.rst`) ### 是什么 / 为什么需要 CephFS 支持通过 `cephfs-mirror` 工具把快照**异步复制**到远端的 CephFS 文件系统。快照的同步方式是:先同步快照数据,然后在远端文件系统的对应目录上创建一个**同名快照**。 **版本要求**:主(本地)与备(远端)Ceph 集群版本都应为 **Pacific 或更高**。 ### 核心思想(Key Idea) 对给定目录中的一对快照,`cephfs-mirror` 守护进程依赖 **CephFS Snapdiff Feature** 来识别目录树中的变化。diff 被应用到远端文件系统的目录中,从而**只同步两个快照之间发生变化的文件**。 > ⚠️ 当前(文档原文):快照数据是通过**批量拷贝(bulk copying)**到远端文件系统来同步的。 > ⚠️ **不支持同步硬链接**——硬链接文件会被当作独立文件分别同步。 Snapdiff 参考:https://croit.io/blog/cephfs-snapdiff-feature ### 创建用户 在主/本地集群上为 mirror daemon 创建用户。该用户需要**元数据池的写能力**(用于创建 RADOS 对象 / index objects 以支持 watch/notify 操作)和**数据池的读能力**: ```bash ceph auth get-or-create client.mirror mon 'profile cephfs-mirror' mds 'allow r' osd 'allow rw tag cephfs metadata=*, allow r tag cephfs data=*' mgr 'allow r' ``` 为每个文件系统 peer(在备/远端集群上)创建用户。该用户需要对 MDS(以创建快照)和 OSD 的**完整能力**: ```bash ceph fs authorize client.mirror_remote / rwps ``` 该用户应在添加 peer 时(作为 peer 规格的一部分)使用。 ### 启动 Mirror Daemon 用 `systemctl` unit 文件: ```bash systemctl enable cephfs-mirror@mirror systemctl start cephfs-mirror@mirror ``` 前台运行(`-f`): ```bash cephfs-mirror --id mirror --cluster site-a -f ``` ### Mirroring 设计 - **快照同步顺序**:虽然选择哪些快照进行同步的顺序本身不重要,但快照是**按创建顺序(使用 snap-id)**挑选的。 - **快照化身(Snapshot Incarnation)**:一个快照可能被删除后以相同名字重建,但内容不同。一个「旧」快照可能早先已被同步过,而重建动作发生在 mirroring 被禁用期间。**如果只用快照名来推断续传点,会导致「新」快照(新的化身)永远不会被同步。** 因此:备端文件系统上的快照会**存储它所同步自的快照的 snap-id**,该元数据存放在 MDS 的 `SnapInfo` 结构中。 ### 接口(Interfaces) `mirroring` 模块(一个 **Manager 插件**)提供管理目录快照镜像的接口。Manager 接口(大多数)是管理文件系统镜像的 monitor 命令的包装,是**推荐的控制接口**。 **关键分工**:mirroring 模块**不管理** mirror daemon 的启动与终止——`systemctl` 是启停 mirror daemon 的首选方式。未来 mirror daemon 会由 `cephadm` 部署和管理(Tracker: http://tracker.ceph.com/issues/47261)。 Manager 模块负责**把目录分配给 mirror daemon** 进行同步。可以启动多个 mirror daemon 以实现目录快照同步的并发。当 mirror daemon 启动(或终止)时,mirroring 模块会发现 mirror daemon 集合的变化,并在新集合间**重新平衡目录分配**,从而提供高可用。 > ⚠️ 注意:**多 mirror daemon 的配置目前未经测试。只推荐单个 mirror daemon。** ### 常用命令 | 目的 | 命令 | |---|---| | 启用 mirroring 模块(**默认禁用**) | `ceph mgr module enable mirroring` | | 为某文件系统启用镜像 | `ceph fs snapshot mirror enable ` | | 为某文件系统禁用镜像 | `ceph fs snapshot mirror disable ` | | 添加 peer | `ceph fs snapshot mirror peer_add [] [] []` | | 移除 peer | `ceph fs snapshot mirror peer_remove ` | | 列出 peer | `ceph fs snapshot mirror peer_list ` | | 配置目录参与镜像 | `ceph fs snapshot mirror add ` | | 停止某目录镜像 | `ceph fs snapshot mirror remove ` | > ⚠️ **命令前缀陷阱**:mirroring **模块**命令使用 `fs snapshot mirror` 前缀,而 **monitor** 命令使用 `fs mirror` 前缀。**请确保使用模块命令(即 `fs snapshot mirror`)。** 其他要点: - `` 是可选的,默认为远端的 ``。 - `peer_add` 也可直接传远端集群 monitor 地址与用户 key,但**推荐用 bootstrap peer 的方式**添加。 - **目前只支持单个 peer。** ### 路径规则 只允许**绝对目录路径**,且路径会被 mirroring 模块**归一化**。这意味着 `/a/b/../b` 等价于 `/a/b`: ```bash mkdir -p /d0/d1/d2 ceph fs snapshot mirror add cephfs /d0/d1/d2 {} ceph fs snapshot mirror add cephfs /d0/d1/../d1/d2 ``` ```text Error EEXIST: directory /d0/d1/d2 is already tracked ``` 一个目录被加入镜像后,它的**子目录或祖先目录**都不允许再被添加: ```bash ceph fs snapshot mirror add cephfs /d0/d1 ``` ```text Error EINVAL: /d0/d1 is a ancestor of tracked path /d0/d1/d2 ``` ```bash ceph fs snapshot mirror add cephfs /d0/d1/d2/d3 ``` ```text Error EINVAL: /d0/d1/d2/d3 is a subtree of tracked path /d0/d1/d2 ``` ### Bootstrap Peers 通过 `peer_add` 添加 peer 要求 peer 集群的配置和用户 keyring 在主集群(Manager 主机和运行 mirror daemon 的主机)上可用。可以通过 **bootstrap 并导入 peer token** 来规避这一要求。 在 peer 集群上创建 bootstrap token: ```bash ceph fs snapshot mirror peer_bootstrap create # 例 ceph fs snapshot mirror peer_bootstrap create backup_fs client.mirror_remote site-remote ``` 返回的 token(base64 编码的 JSON)形如: ```json {"token": "eyJmc2lkIjogIjBkZjE3MjE3LWRmY2QtNDAzMC05MDc5LTM2Nzk4NTVkNDJlZiIsICJmaWxlc3lzdGVtIjogImJhY2t1cF9mcyIsICJ1c2VyIjogImNsaWVudC5taXJyb3JfcGVlcl9ib290c3RyYXAiLCAic2l0ZV9uYW1lIjogInNpdGUtcmVtb3RlIiwgImtleSI6ICJBUUFhcDBCZ0xtRmpOeEFBVnNyZXozai9YYUV0T2UrbUJEZlJDZz09IiwgIm1vbl9ob3N0IjogIlt2MjoxOTIuMTY4LjAuNTo0MDkxOCx2MToxOTIuMTY4LjAuNTo0MDkxOV0ifQ=="} ``` `site-name` 是用户定义的用于标识远端文件系统的字符串。在 `peer_add` 接口的语境下,`site-name` 就是从 `remote_cluster_spec` 中传入的 `cluster` 名。 在主集群导入 token: ```bash ceph fs snapshot mirror peer_bootstrap import # 例 ceph fs snapshot mirror peer_bootstrap import cephfs eyJmc2lkIjogIjBkZjE3MjE3LWRmY2QtNDAzMC05MDc5LTM2Nzk4NTVkNDJlZiIsICJmaWxlc3lzdGVtIjogImJhY2t1cF9mcyIsICJ1c2VyIjogImNsaWVudC5taXJyb3JfcGVlcl9ib290c3RyYXAiLCAic2l0ZV9uYW1lIjogInNpdGUtcmVtb3RlIiwgImtleSI6ICJBUUFhcDBCZ0xtRmpOeEFBVnNyZXozai9YYUV0T2UrbUJEZlJDZz09IiwgIm1vbl9ob3N0IjogIlt2MjoxOTIuMTY4LjAuNTo0MDkxOCx2MToxOTIuMTY4LjAuNTo0MDkxOV0ifQ== ``` ### Mirror Daemon 状态 Mirror daemon 会被**异步通知**文件系统镜像状态与 peer 的变化。 ```bash ceph fs snapshot mirror daemon status # 例(配合 jq) ceph fs snapshot mirror daemon status | jq ``` ```json [ { "daemon_id": 284167, "filesystems": [ { "filesystem_id": 1, "name": "a", "directory_count": 1, "peers": [ { "uuid": "02117353-8cd1-44db-976b-eb20609aa160", "remote": { "client_name": "client.mirror_remote", "cluster_name": "ceph", "fs_name": "backup_fs" }, "stats": { "failure_count": 1, "recovery_count": 0 } } ] } ] } ] ``` 每个 mirror daemon 实例显示一条记录,包含已配置的 peer 与基本统计。更详细的统计请用 admin socket 接口。 列出可用的 `mirror status` 类命令: ```bash ceph --admin-daemon /path/to/mirror/daemon/admin/socket help ``` ```json { .... "fs mirror status cephfs@360": "get filesystem mirror status", .... } ``` 带 `fs mirror status` 前缀的命令提供已启用镜像的文件系统的状态。注意 `cephfs@360` 的格式是 `filesystem-name@filesystem-id`。**之所以需要这个格式,是因为 mirror daemon 是异步获知文件系统镜像状态的**(一个文件系统可能被删除后又以相同名字重建)。 > 📌 **文档明确标注的时效性**:「**Currently (May 2025)**, the command provides minimal information regarding mirror status」(截至 2025 年 5 月,该命令只提供极少的镜像状态信息)。 ```bash ceph --admin-daemon /var/run/ceph/cephfs-mirror.asok fs mirror status cephfs@360 ``` ```json { "rados_inst": "192.168.0.5:0/1476644347", "peers": { "a2dc7784-e7a1-4723-b103-03ee8d8768f8": { "remote": { "client_name": "client.mirror_remote", "cluster_name": "site-a", "fs_name": "backup_fs" } } }, "snap_dirs": { "dir_count": 1 } } ``` `Peers` 段显示 peer 信息,如唯一 peer-id(UUID)与规格。**peer-id 是移除已有 peer 时必需的**(见 `Mirror Module and Interface` 一节)。 带 `fs mirror peer status` 前缀的命令返回 peer 的同步状态,形式为 `filesystem-name@filesystem-id peer-uuid`: ```bash ceph --admin-daemon /var/run/ceph/cephfs-mirror.asok fs mirror peer status cephfs@360 a2dc7784-e7a1-4723-b103-03ee8d8768f8 ``` ```json { "/d0": { "state": "idle", "last_synced_snap": { "id": 120, "name": "snap1", "sync_duration": 0.079997898999999997, "sync_time_stamp": "274900.558797s" }, "snaps_synced": 2, "snaps_deleted": 0, "snaps_renamed": 0 } } ``` > ⚠️ 同步统计(`snaps_synced`、`snaps_deleted`、`snaps_renamed`)在 daemon 重启时会被重置;部署多个 mirror daemon 时,当目录被重新分配给另一个 mirror daemon 时也会重置。 **目录的三种状态**: | 状态 | 含义 | |---|---| | `idle` | 该目录当前**未**被同步 | | `syncing` | 该目录当前**正在**被同步 | | `failed` | 该目录已达到连续失败次数上限 | 当一个目录的连续同步失败次数达到配置值,mirror daemon 就把它标记为 `failed`,并会**重试**这些目录的同步。默认阈值由 `cephfs_mirror_max_consecutive_failures_per_directory` 控制(默认 **`10`**);失败目录的重试间隔由 `cephfs_mirror_retry_failed_directories_interval` 控制(默认 **`60s`**)。 例如,把一个**普通文件**(而非目录)加进来同步,会得到 `failed` 状态: ```bash ceph fs snapshot mirror add cephfs /f0 ceph --admin-daemon /var/run/ceph/cephfs-mirror.asok fs mirror peer status cephfs@360 a2dc7784-e7a1-4723-b103-03ee8d8768f8 ``` ```json { "/d0": { "state": "idle", "last_synced_snap": {...}, "snaps_synced": 2, "snaps_deleted": 0, "snaps_renamed": 0 }, "/f0": { "state": "failed", "snaps_synced": 0, "snaps_deleted": 0, "snaps_renamed": 0 } } ``` **这个行为其实是一个特性**:它允许用户为一个**尚不存在**的目录添加同步。mirror daemon 会把该目录标记为 failed 并以较低频率重试;当该目录出现后,mirror daemon 会察觉到同步成功并**解除** failed 标记。 当 mirroring 被禁用时,该文件系统的 `fs mirror status` 命令不会出现在命令帮助中。 **查看目录映射与分布:** ```bash ceph fs snapshot mirror dirmap cephfs /d0/d1/d2 ``` ```json { "instance_id": "404148", "last_shuffled": 1601284516.10986, "state": "mapped" } ``` > `instance_id` 是与某个 mirror daemon 关联的 RADOS instance-id。其他信息如 `state` 和 `last_shuffled` 在运行多个 mirror daemon 时比较有意义。 如果没有 mirror daemon 在运行: ```json { "reason": "no mirror daemons running", "state": "stalled" } ``` 这表示没有 mirror daemon 在运行,镜像已停滞。 ### 重新添加 Peers(Re-adding Peers) 当把一个 peer 重新添加(重新分配)到另一个集群中的文件系统时,必须**确保所有 mirror daemon 都已停止与该 peer 的同步**。可以通过 `fs mirror status` admin socket 命令检查(`Peer UUID` **不应**出现在命令输出中)。 文档建议:在把目录重新添加到另一个文件系统之前,**清除 peer 上已同步的目录**(尤其是那些可能存在于新主文件系统中的目录)。如果是重新添加到**之前同步自的同一个主文件系统**,则不需要这样做。 ### 特性状态 `cephfs-mirror` daemon **默认构建**,遵循 `WITH_CEPHFS` CMake 规则。 > 引自:[CephFS Mirroring](https://docs.ceph.com/en/tentacle/dev/cephfs-mirroring/) ## 3.2 CephFS Reclaim Interface(`cephfs-reclaim.rst`) ### 是什么 / 为什么需要 NFS 服务器通常**不在稳定存储上跟踪临时状态(ephemeral state)**。如果 NFS 服务器重启,它会以「无任何临时状态」的方式复活,NFS 客户端则被期望在一个 **grace period(宽限期)**内发送请求来**回收(reclaim)**它们之前持有的状态。 为了支持这个用例,**libcephfs 增加了若干函数**,允许一个被停止又重启的客户端去销毁或回收「它自己上一次化身(incarnation)」所持有的状态。这让客户端能重新获取之前持有的状态,并**避免等待旧会话超时**才能释放状态的长等待。 **为什么紧迫**:一旦跑在 cephfs 之上的 NFS 服务器宕机,它就在与自己的 MDS session timeout 赛跑。**如果 Ceph session 在 NFS grace period 开始之前就超时了,冲突状态可能被另一个客户端获取。** 这个机制也允许我们为这类客户端**增大 timeout**,确保服务器有较长的时间窗口来完成重启。 ### 设置 UUID 为了正确地对旧会话做 reset 或 reclaim,需要一种识别旧会话的方式。这通过设置会话上一个**唯一的不透明值**完成——使用 **`ceph_set_uuid()`**。uuid 值可以是任意字符串,客户端把它当作不透明数据处理。 > ⚠️ **直接设置 uuid 只能在新会话上、mount 之前完成。** 当执行 reclaim 时,当前会话将**继承**旧会话的 uuid。 ### 启动 Reclaim 在得到的 `struct ceph_mount_info` 上调用 `ceph_create` 和 `ceph_init` 之后,客户端应发出 `ceph_start_reclaim`,传入客户端上一次化身的 uuid 和任意 flags。 **Flag:** | Flag | 含义 | |---|---| | `CEPH_RECLAIM_RESET` | 表示我们不打算针对给定 uuid 指示的旧会话做任何 reclaim,它应被直接丢弃。先前客户端持有的任何状态应立即释放。 | ### 完成 Reclaim Ceph 客户端完成所有 reclaim 操作后,应发出 `ceph_finish_reclaim` 表示 reclaim 已完成。 ### 设置 Session Timeout(可选) 当一个客户端死掉又重启、而我们需要保留它的状态时,我们实际上在和会话过期时钟赛跑。这种情况下通常希望有**更长的 timeout**,因为我们预期最终会手工杀掉旧会话。 ### 完整示例:重置旧会话 这个例子直接干掉由自身上一个实例持有的 MDS session。NFS 服务器可以启动一个 grace period,然后要求 MDS 拆除旧会话,这让客户端可以立即开始 reclaim。 (注:为清晰起见省略了错误处理) ```c struct ceph_mount_info *cmount; const char *uuid = "foobarbaz"; /* Set up a new cephfs session, but don't mount it yet. */ rc = ceph_create(&cmount); rc = ceph_init(&cmount); /* * Set the timeout to 5 minutes to lengthen the window of time for * the server to restart, should it crash. */ ceph_set_session_timeout(cmount, 300); /* * Start reclaim vs. session with old uuid. Before calling this, * all NFS servers that could acquire conflicting state _must_ be * enforcing their grace period locally. */ rc = ceph_start_reclaim(cmount, uuid, CEPH_RECLAIM_RESET); /* Declare reclaim complete */ rc = ceph_finish_reclaim(cmount); /* Set uuid held by new session */ ceph_set_uuid(cmount, nodeid); /* * Now mount up the file system and do normal open/lock operations to * satisfy reclaim requests. */ ceph_mount(cmount, rootpath); ... ``` **顺序要点**:`ceph_create` → `ceph_init` → `ceph_set_session_timeout` → `ceph_start_reclaim` → `ceph_finish_reclaim` → `ceph_set_uuid` → `ceph_mount`。 > 💡 补充:文档标题写的是 "Setting Session Timeout (Optional)",示例中用的是 `ceph_set_session_timeout(cmount, 300)`,但文档未给出该函数的完整签名与参数单位说明(示例值 300 与注释 "5 minutes" 对应,可推断单位为秒)。 > 引自:[CephFS Reclaim Interface](https://docs.ceph.com/en/tentacle/dev/cephfs-reclaim/) ## 3.3 CephFS 快照内部实现(`cephfs-snapshots.rst`) ### 概述 CephFS 支持快照,通常通过在 `.snap` 目录中执行 `mkdir` 来创建。注意这是一个**隐藏的特殊目录**,在目录列表中不可见。 两个「头条特性」使 CephFS 快照与常见预期不同: 1. **任意子树(Arbitrary subtrees)**:快照可以在你选择的**任何目录**中创建,并覆盖该文件系统中该目录下的**全部数据**。 2. **异步(Asynchronous)**:创建快照时,缓冲数据(包括来自其他客户端的)是**惰性刷出**的。因此「创建」快照非常快。 ### 重要数据结构 | 结构 | 说明 | |---|---| | `SnapRealm` | 每当你在层次结构中一个新的点创建快照时(或当一个已被快照的 inode 被移出其父快照时)就会创建一个 `SnapRealm`。SnapRealm 包含一个 `sr_t srnode`,以及属于该快照的 `inodes_with_caps`。**客户端也有 SnapRealm 概念**,它维护的数据较少,但用于把 `SnapContext` 与每个为写而打开的文件关联起来。 | | `sr_t` | **on-disk 快照元数据**。它是所在目录的一部分,包含 sequence counters、timestamps、关联的快照 ID 列表,以及 `past_parent_snaps`。 | | `SnapServer` | 管理快照 ID 分配、快照删除,并跟踪文件系统中有效快照的列表。**一个文件系统只有一个 snapserver 实例。** | | `SnapClient` | 用于与 snapserver 通信,**每个 MDS rank 有自己的 snapclient 实例**。SnapClient 也在本地缓存有效快照。 | ### 创建快照 新文件系统上 CephFS 快照特性**默认启用**。要在已有文件系统上启用: ```bash $ ceph fs set allow_new_snaps true ``` 启用后,CephFS 中所有目录都会有一个特殊的 `.snap` 目录。(可用 `client snapdir` 设置项配置成其他名字。) 创建快照就是在 `.snap` 下建一个你命名的子目录。例如在 `/1/2/3/` 上创建快照: ```bash mkdir /1/2/3/.snap/my-snapshot-name ``` > ⚠️ **两条命名约束**: > 1. 快照名**不能**以下划线(`_`)开头,这些名字保留给内部使用。 > 2. 快照名**不能超过 240 个字符**。原因是 MDS 内部会使用长快照名,格式为 `__`;而文件名通常不能超过 255 字符,`` 占 13 个字符,所以长快照名最多 `255 - 1 - 1 - 13 = 240`。 **内部流程**: 1. 该操作作为带 `CEPH_MDS_OP_MKSNAP` 标记的 `MClientRequest` 传给 MDS Server,最初在 `Server::handle_client_mksnap()` 中处理。 2. 它从 `SnapServer` 分配一个 `snapid`,投影(project)一个带新 SnapRealm 的新 inode,并照常把它提交到 MDLog。 3. 提交后,它调用 `MDCache::do_realm_invalidate_and_update_notify()`,通知所有对 `/1/2/3/` 下文件持有 caps 的客户端关于新 SnapRealm 的事。 4. 客户端收到通知后,更新客户端侧的 SnapRealm 层次结构,把 `/1/2/3/` 下的文件链接到新的 SnapRealm,并为新 SnapRealm 生成 `SnapContext`。 > ⚠️ **注意:这不是快照创建的同步部分!**(这是「异步」特性的体现。) ### 更新快照 删除快照遵循类似流程。如果你把一个 inode 移出其父 SnapRealm,**rename 代码会为被 rename 的 inode 创建一个新的 SnapRealm**(如果该 SnapRealm 尚不存在),把在**原父 SnapRealm** 上有效的快照 ID 保存到新 SnapRealm 的 `past_parent_snaps` 中,然后遵循类似创建快照的流程。 ### 生成 SnapContext RADOS `SnapContext` 由一个快照 sequence ID(`snapid`)以及该对象**已经属于的**所有快照 ID 组成。为了生成这个列表,我们把与该 SnapRealm 关联的 `snapids` 和 `past_parent_snaps` 中所有有效的 `snapids` 合并起来。**过期的 `snapids` 由 SnapClient 缓存的有效快照过滤掉。** ### 存储快照数据 文件数据存储在 RADOS 的 **"self-managed" 快照**中。客户端在把文件数据写到 OSD 时会小心使用正确的 `SnapContext`。 ### 存储快照元数据 被快照的 dentry(及其 inode)**内嵌(in-line)**存储在它们被快照时所在的目录中。***所有* dentry** 都包含一个 `first` 和 `last` snapid,表示其有效范围。(未被快照的 dentry 的 `last` 会被设为 `CEPH_NOSNAP`。) ### 快照回写(Snapshot writeback) 这里有大量代码用于高效处理回写: - 当客户端收到 `MClientSnap` 消息时,它更新本地 `SnapRealm` 表示及其到具体 `Inodes` 的链接,并为该 `Inode` 生成一个 `CapSnap`。 - `CapSnap` 作为 capability writeback 的一部分被刷出;**如果有脏数据,`CapSnap` 会被用来阻塞新的数据写入,直到快照完全刷到 OSD。** - 在 MDS 中,我们生成表示快照的 dentry 作为常规 flush 流程的一部分。带有未决 `CapSnap` 数据的 dentry 会被保持 pinned 并留在 journal 中。 ### 删除快照 快照通过对它们所根植的 `.snap` 目录执行 `rmdir` 来删除。(**尝试删除一个 root 了快照的目录会失败**,你必须先删除这些快照。)删除后,它们被加入 `OSDMap` 的已删除快照列表中,文件数据由 OSD 移除。元数据则在目录对象被读入和写回时逐步清理。 ### 硬链接 拥有多个硬链接的 inode 会被移动到一个 **dummy global SnapRealm**。这个 dummy SnapRealm 覆盖文件系统中的所有快照。该 inode 的数据会为任何新快照保留,这些保留的数据覆盖该 inode **任何链接**上的快照。 ### Multi-FS(多文件系统) **快照与多文件系统配合不好。** 具体而言,每个 MDS 集群**独立分配 `snapids`**;如果你有多个文件系统共享同一个池(通过 namespace),它们的快照**会冲突**,删除其中一个会导致其他文件系统丢失文件数据。(这甚至可能是**不可见的**,不会向用户报错。)如果每个 FS 有自己的池,可能可以工作,但**这未经测试,可能并非事实**。 > ⚠️ 另一条重要约束:为避免 mon 管理的快照与文件系统快照之间的 snap id 冲突,**带有 mon-managed 快照的池不允许挂载到文件系统上**;同样,**已经挂载到文件系统的池也不能创建 mon-managed 快照**。 > 引自:[CephFS Snapshots](https://docs.ceph.com/en/tentacle/dev/cephfs-snapshots/) ## 3.4 测试 Linux 内核 CephFS 驱动(`kclient.rst`) > 📌 这是一份**作者个人环境的操作指南**(原文即声明 "one (opinionated) way"、"This guide is a walkthrough of the author's own environment"),并非官方唯一标准流程。但它是 `doc/dev/` 中唯一详细讲解内核客户端开发测试的页面,价值很高。 ### Step One:构建内核 克隆内核: ```bash git init linux && cd linux git remote add torvalds git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git git remote add ceph https://github.com/ceph/ceph-client.git git fetch && git checkout torvalds/master ``` 配置内核: ```bash make defconfig ``` > 也可以直接使用 `Ceph Kernel QA Config`(https://github.com/ceph/ceph-build/tree/899d0848a0f487f7e4cee773556aaf9529b8db26/kernel/build)来构建内核。 `make defconfig` 给你一个针对当前架构的合理默认配置。接下来启用 Ceph 相关及测试所需的功能: ```bash cat > ~/.ceph.config < ⚠️ 建议:**不要把任何东西构建为内核模块**。否则你需要在 VM 的根盘上执行 `make modules_install`。 合并配置并构建: ```bash scripts/kconfig/merge_config.sh .config ~/.ceph.config make -j ``` > 文档不涉及如何获取构建内核所需的发行版工具(如 gcc),请自行搜索。 ### Step Two:创建 VM **为什么用 VM**:更容易监控和配置网络;可以极快地测试内核改动(构建 → 挂载 < 10 秒);内核故障不会搞崩你的机器;有一整套工具可用于分析运行中的内核。 文档作者用 Arch Linux + LVM 创建卷(你也可以用分区或任何机制创建块设备)。一般建议用快照,避免 VM 破坏根盘导致重来。 ```bash # create a volume VOLUME_GROUP=foo sudo lvcreate -L 256G "$VOLUME_GROUP" -n $(whoami)-vm-0 DEV="/dev/${VOLUME_GROUP}/$(whoami)-vm-0" sudo mkfs.xfs "$DEV" sudo mount "$DEV" /mnt sudo pacstrap /mnt base base-devel vim less jq sudo arch-chroot /mnt # # delete root's password for ease of login # passwd -d root # mkdir -p /root/.ssh && echo "$YOUR_SSH_KEY_PUBKEY" >> /root/.ssh/authorized_keys # exit sudo umount /mnt ``` 运行 VM: ```bash qemu-system-x86_64 -enable-kvm -kernel $(pwd)/arch/x86/boot/bzImage -drive file="$DEV",if=virtio,format=raw -append 'root=/dev/vda rw' ``` 你会看到类似输出: ```text VNC server running on ::1:5900 ``` 可以用下面命令查看控制台: ```bash vncviewer 127.0.0.1:5900 ``` ### Step Three:VM 网络("the hard part") 作者的开发环境拓扑: ```text sepian netns ______________ | | | kernel VM | sepia-bounce VM vossi04.front.sepia.ceph.com | ------- | | ------ ------- | | | | | 192.168.20.1 | | | | | | |--|--|- <- wireguard -> | | <-- sepia vpn -> | | | |_____| | | 192.168.20.2 |____| |_____| | br0 | |______________| ``` 要点: - `sepia-bounce` VM 用作到 sepia lab 的跳板机,可代理 ssh 连接、路由 sepia 流量、或作为 DNS 代理。它是**可选的**,但如果你想创建大量内核 VM 会很有用。 - 作者喜欢用 `vossi04` **developer playground**(https://wiki.sepia.ceph.com/doku.php?id=devplayground#developer_playgrounds)来构建 Ceph 和搭建 vstart 集群,它资源充足(冷构建约 5 分钟)。 - 主要配置:在宿主机与 sepia-bounce VM 之间建立 **wireguard 隧道**;用 `systemd-resolved` 作为 DNS 解析器并监听 `192.168.20.2`(而非仅 localhost);连接 sepia **VPN** 并用脚本配置 `systemd-resolved` 使用从 sepia VPN DHCP 获取的 DNS 服务器;配置 `firewalld` 允许 wireguard 流量并做 masquerade/转发到 sepia vpn。 **用 network namespace 隔离 VM 流量**(作者的 systemd one-shot unit): ```bash # create the net namespace ExecStart=/usr/bin/ip netns add sepian # bring lo up ExecStart=/usr/bin/ip netns exec sepian ip link set dev lo up # setup wireguard to sepia-bounce ExecStart=/usr/bin/ip link add wg-sepian type wireguard ExecStart=/usr/bin/wg setconf wg-sepian /etc/wireguard/wg-sepian.conf # move the wireguard interface to the sepian nents ExecStart=/usr/bin/ip link set wg-sepian netns sepian # configure the static ip and bring it up ExecStart=/usr/bin/ip netns exec sepian ip addr add 192.168.20.1/24 dev wg-sepian ExecStart=/usr/bin/ip netns exec sepian ip link set wg-sepian up # logging info ExecStart=/usr/bin/ip netns exec sepian ip addr ExecStart=/usr/bin/ip netns exec sepian ip route # make wireguard the default route ExecStart=/usr/bin/ip netns exec sepian ip route add default via 192.168.20.2 dev wg-sepian # more logging ExecStart=/usr/bin/ip netns exec sepian ip route # add a bridge interface for VMs ExecStart=/usr/bin/ip netns exec sepian ip link add name br0 type bridge # configure the addresses and bring it up ExecStart=/usr/bin/ip netns exec sepian ip addr add 192.168.0.1/24 dev br0 ExecStart=/usr/bin/ip netns exec sepian ip link set br0 up # masquerade/forward traffic to sepia-bounce ExecStart=/usr/bin/ip netns exec sepian iptables -t nat -A POSTROUTING -o wg-sepian -j MASQUERADE ``` `ip netns exec` 有个便利特性:会自动把 `/etc/netns//` 下的文件 bind mount 到 `/etc`: ```bash # cat /etc/netns/sepian/resolv.conf nameserver 192.168.20.2 ``` 于是该 netns 里的任何应用都能解析 sepia 主机名: ```bash $ sudo ip netns exec sepian host vossi04.front.sepia.ceph.com vossi04.front.sepia.ceph.com has address 172.21.10.4 ``` **把 VM 接到网桥上**(tap 设备): ```bash sudo ip netns exec sepian qemu-system-x86_64 \ -enable-kvm \ -kernel $(pwd)/arch/x86/boot/bzImage \ -drive file="$DEV",if=virtio,format=raw \ -netdev tap,id=net0,ifname=tap0,script="$HOME/bin/qemu-br0",downscript=no \ -device virtio-net-pci,netdev=net0 \ -append 'root=/dev/vda rw' ``` 新增的关键点:(a) 在我们构建的 netns 中执行 VM;(b) 用 `-netdev` 配置 tap 设备;(c) 给 VM 一块虚拟网卡。`$HOME/bin/qemu-br0` 是 qemu 用来配置 tap 设备的脚本: ```bash #!/bin/bash tap=$1 ip link set "$tap" master br0 ip link set dev "$tap" up ``` **还差最后一步:VM 的 IP 从哪来?** 两个选项: 1. 配置静态 IP——但必须修改 VM 根设备的网络栈配置; 2. **用 DHCP**,并把 VM 根设备配置为总是对其以太网设备使用 dhcp。 选项 2 搭建更复杂(需要跑 DHCP server),但对后续增加 VM 最灵活。 「改造过的」dhcpd systemd service: ```ini # cat sepian-dhcpd.service [Unit] Description=IPv4 DHCP server After=network.target network-online.target sepian-netns.service Wants=network-online.target Requires=sepian-netns.service [Service] ExecStartPre=/usr/bin/touch /tmp/dhcpd.leases ExecStartPre=/usr/bin/cat /etc/netns/sepian/dhcpd.conf ExecStart=/usr/bin/dhcpd -f -4 -q -cf /etc/netns/sepian/dhcpd.conf -lf /tmp/dhcpd.leases NetworkNamespacePath=/var/run/netns/sepian RuntimeDirectory=dhcpd4 User=dhcp AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW ProtectSystem=full ProtectHome=on KillSignal=SIGINT RestartSec=2s Restart=on-failure StartLimitInterval=12s [Install] WantedBy=multi-user.target ``` 对应的 `dhcpd.conf`: ```bash # cat /etc/netns/sepian/dhcpd.conf option domain-name-servers 192.168.20.2; option subnet-mask 255.255.255.0; option routers 192.168.0.1; subnet 192.168.0.0 netmask 255.255.255.0 { range 192.168.0.100 192.168.0.199; } ``` 关键点:这告诉 VM 把流量路由到 `192.168.0.1`(netns 中网桥的 IP),DNS 由 `192.168.20.2` 提供(经由 sepia-bounce VM 上的 `systemd-resolved`)。 VM 内的网络看起来像(节选): ```text 2: enp0s3: ... inet 192.168.0.100/24 metric 1024 brd 192.168.0.255 scope global dynamic enp0s3 ... Current DNS Server: 192.168.20.2 DNS Servers: 192.168.20.2 ``` **其他可选网络方案:** - 在你的机器上直接跑 VM,让它完全访问宿主机网络栈。如果你有 sepia vpn,这可能无需太多配置就能工作。 - 在 netns 中跑 VM,**并在同一个 netns 里配置 sepia vpn**(可避免使用 sepia-bounce VM,但仍需配置网桥与 sepia VPN 之间的路由)。 - 在 netns 中跑 VM,但只用本地 vstart 集群(可能在另一个 VM 里),且都在同一个 netns 中。 ### Step Four:在 VM 中挂载 CephFS 由于 vstart 集群的 mon 地址每次新建集群都会变,任何静态配置都会失效。因此应写个脚本,**在挂载前先拉取 vstart 集群的配置**: ```bash #!/bin/bash # kmount.sh -- mount a vstart Ceph cluster on a remote machine # the cephx client credential, vstart creates "client.fs" by default NAME=fs # static fs name, vstart creates an "a" file system by default FS=a # where to mount on the VM MOUNTPOINT=/mnt # cephfs mount point (root by default) CEPHFS_MOUNTPOINT=/ function run { printf '%s\n' "$*" >&2 "$@" } function mssh { run ssh vossi04.front.sepia.ceph.com "cd ceph/build && (source vstart_environment.sh; $1)" } # create the minimum config (including mon addresses) and store it in the VM's ceph.conf. This is not used for mounting; we're storing it for potential use with `ceph` commands. mssh "ceph config generate-minimal-conf" > /etc/ceph/ceph.conf # get the vstart cluster's fsid FSID=$(mssh "ceph fsid") # get the auth key associated with client.fs KEY=$(mssh "ceph auth get-key client.$NAME") # dump the v2 mon addresses and format for the -o mon_addr mount option MONS=$(mssh "ceph mon dump --format=json" | jq -r '.mons[] | .public_addrs.addrvec[] | select(.type == "v2") | .addr' | paste -s -d/) # turn on kernel debugging (and any other debugging you'd like) echo "module ceph +p" | tee /sys/kernel/debug/dynamic_debug/control # do the mount! we use the new device syntax for this mount run mount -t ceph "${NAME}@${FSID}.${FS}=${CEPHFS_MOUNTPOINT}" -o "mon_addr=${MONS},ms_mode=crc,name=${NAME},secret=${KEY},norequire_active_mds,noshare" "$MOUNTPOINT" ``` 运行方式: ```bash $ sudo ip netns exec sepian ssh root@192.168.0.100 ./kmount.sh ... mount -t ceph fs@c9653bca-110b-4f70-9f84-5a195b205e9a.a=/ -o mon_addr=172.21.10.4:40762/172.21.10.4:40764/172.21.10.4:40766,ms_mode=crc,name=fs,secret=AQD0jgln43pBCxAA7cJlZ4Px7J0UmiK4A4j3rA==,norequire_active_mds,noshare /mnt $ sudo ip netns exec sepian ssh root@192.168.0.100 df -h /mnt Filesystem Size Used Avail Use% Mounted on fs@c9653bca-110b-4f70-9f84-5a195b205e9a.a=/ 169G 0 169G 0% /mnt ``` **遇到问题时的排查方向**: - 运行 vstart 集群的节点上的防火墙阻断了你的连接; - 你的网络栈有某些配置错误; - 挂载配置不正确。 ### Step Five:在 teuthology 中测试内核改动 `ceph kernel git repository`(https://github.com/ceph/ceph-client)中有 3 个由 Ceph 团队管理的**静态分支**: | 分支 | 用途 | |---|---| | `for-linus` | 由 Ceph 主要维护者管理,用于向上游(Linus Torvalds)提交变更。**不要推送到这个分支。** | | `master` | 计划发给 Linus 的补丁的暂存区。**不要推送到这个分支。** | | `testing` | 需要更广泛 QA 测试(通过 nightlies 或常规 Ceph QA 测试)的各种补丁的暂存区。**推送你认为已接近可被上游接受的补丁。** | 你也可以向 `ceph-client.git` 推送一个 `wip-$feature` 分支,它会由 Jenkins 构建,然后在 **Shaman**(https://shaman.ceph.com/builds/kernel/)查看构建结果。 内核分支构建完成后,可通过 `fs` CephFS QA suite 测试: ```bash $ teuthology-suite ... --suite fs --kernel wip-$feature --filter k-testing ``` `k-testing` filter 寻找的是那个通常会为常规 QA 设置 kernel 的 `testing` 分支的 fragment。也就是说,`fs` suite 会定期针对 kernel 的 `testing` 分支跑测试,而我们通过 `--kernel wip-$feature` 覆盖这个选择。 > ⚠️ 如果不加 `k-testing` 过滤,`fs` suite 也会跑使用 ceph-fuse 或 stock kernel 的任务、libcephfs 测试,以及其他在评估内核改动时你可能不关心的测试。 实际的覆盖行为由 `k-testing.yaml` fragment 中的 Lua merge scripts 控制,详见该文件。 > 引自:[Testing changes to the Linux Kernel CephFS driver](https://docs.ceph.com/en/tentacle/dev/kclient/) ## 3.5 libcephfs proxy 设计(`libcephfs_proxy.rst`) ### 问题描述(为什么需要它) 当应用通过 `libcephfs.so` 库连接 Ceph volume 时,**进程内部会创建一个本地 cache**。`libcephfs.so` 的实现已经会处理 cache 的内存占用并调整它,使其不会耗尽所有可用内存。 **但是**:如果**多个进程**通过库的不同实例连接 CephFS,**每个进程都会保留一份私有 cache**。这种情况下内存管理就不有效了——因为即使配置了内存上限,**可创建的 libcephfs 实例数量是无界的**,它们无法协调地正确控制资源使用。因此,当所有进程都在密集使用 data cache 时,**很容易耗尽所有内存,导致 OOM killer 杀掉这些进程**。 ### 提出的方案:高层思路 - 创建一个 **`libcephfs_proxy.so`** 库,提供与原始 `libcephfs.so` **相同的 API**,但**不缓存任何数据**。任何当前使用 `libcephfs.so` 的应用都可以**透明地**使用它——无需修改代码,只需改为链接 `libcephfs_proxy.so`,或者干脆用 **`LD_PRELOAD`**。 - 同时创建一个新的 **`libcephfsd` 守护进程**。该守护进程链接真正的 `libcephfs.so`,并在一个 **UNIX socket** 上监听 incoming connections。 - 当应用启动并通过 `libcephfs_proxy.so` 发起 CephFS 请求时,它会通过 UNIX socket 连接到 `libcephfsd`,把**所有 CephFS 请求转发**给它。守护进程用真正的 `libcephfs.so` 执行这些请求,并把应答返回给应用,**可能在 `libcephfsd` 进程自身中缓存数据**。这一切**对应用完全透明**。 - 守护进程会在不同应用之间**共享底层的 `libcephfs.so` mount**,以避免为每个应用创建一个实例(那样在内存上与每个应用直接链接 `libcephfs.so` 效果一样)。**但只有在各应用定义的配置完全相同时才会共享**,否则仍会创建新的独立实例。 - 某些 `libcephfs.so` 函数需要在 `libcephfsd` 内部**特殊实现**,以隐藏「与多个客户端共享同一个 mount 实例」带来的差异(例如 `chdir`/`getcwd` 不能直接依赖 `libcephfs.so` 的 `ceph_chdir()`/`ceph_getcwd()`)。 - **初始阶段只提供 Samba 的 VFS CephFS 模块用到的那部分 `libcephfs.so` 底层接口函数。** ### 通用组件设计:网络协议 由于 UNIX socket 连接的是**同一台机器上的另一个进程**,且要传的数据相当简单,因此**不用通用的 XDR 编解码和 RPC 传输**(避免全部开销),而是使用**直接在代码中实现的非常简单的序列化**。未来可能考虑使用 **cap'n proto**(https://capnproto.org),它号称编解码零开销,并且在将来需要修改网络协议时能方便地支持向后兼容。 **特性协商(Feature negotiation)**: 在通过 UNIX socket 的初始连接期间,客户端发起它想启用的特性的协商。目前支持一个 supported/required/desired 的**特性位图**,但允许通过增加字段来扩展包含协商数据的结构,**不破坏向后兼容**。 结构本身包含**结构的 version 和 size**,以及**支持的最低版本**,使得对端即使不完全理解内容也能读取传输的数据,并调整到双方都已知的最高版本。 协商流程: 1. 客户端发送:**supported 位图**、**required 位图**(必须有,否则连接无法继续)、**desired 位图**(希望启用)。 2. 服务端收到后:创建**双方都支持的**特性位图,并**校验服务端 required 的所有特性都被客户端支持**。它还会**把自己的 desired 特性加到客户端的 desired 特性上**。 3. 这些数据被发回客户端,客户端做最终检查并决定最终启用的特性集。 4. 一旦这个特性集被发给服务端,双方就可以开始使用已启用的特性。 ### `libcephfs_proxy.so` 库的设计 这个库基本上就是:连接到 `libcephfsd` 监听的 UNIX socket,等待来自应用的请求,**序列化所有函数参数**并发送给守护进程;守护进程响应后**反序列化应答**并把结果返回给应用。 **本地缓存(Local caching)**:虽然这个库的主要目的是避免每个进程上的独立 cache,但初步测试表明,当所有请求都通过 proxy daemon 时,**基于元数据操作和/或小文件的工作负载性能下降很大**。为缓解这一点,**应实现元数据缓存**。元数据缓存比数据缓存小得多,能在内存占用与性能之间取得良好折中。 为了安全地实现缓存,需要在数据变陈旧之前正确失效它。目前 `libcephfs.so` 提供可用于实现此目的的**失效通知(invalidation notifications)**,但**其语义尚未被完全理解**,所以 `libcephfs_proxy.so` 中的缓存将**在未来的版本中设计和实现**。 ### `libcephfsd` 守护进程的设计 该守护进程是一个常规进程,集中处理来自同一机器上其他进程的 libcephfs 请求。 **进程维护**:由于该进程作为独立守护进程工作,会提供一个简单的 **systemd unit file** 将其作为常规系统服务管理。**最可能的情况是未来集成进 cephadm。** 如果 `libcephfsd` 崩溃,我们依赖 systemd 重启它。 ### 特殊函数 某些函数需要在 `libcephfsd` 内部特殊处理,因为由于**共享底层 mount**,直接转发给 `libcephfs.so` 可能返回不正确的结果。 **A. 共享底层 `struct ceph_mount_info`** 代理的主要目的是避免**为每个访问相同数据的进程创建新 mount**。为此我们需要**「虚拟化」挂载点**,让应用以为自己在使用自己的 mount,而实际上它可能在使用一个共享的 mount。 守护进程会跟踪:用于连接 volume 的 Ceph account、配置文件、以及挂载 volume 之前所做的任何特定配置变更。**只有当所有设置与另一个已挂载实例完全相同时,该 mount 才会被共享。** 守护进程**不理解** CephFS 设置,也不理解设置之间任何潜在的依赖关系。因此会做一个**非常严格的比较**:配置文件必须完全相同,之后所做的任何其他变更必须以完全相同的顺序设置为完全相同的值,两个配置才被视为相同。 判断是否相同的检查会在**挂载 volume 之前**(即 `ceph_mount()` 时)进行。这意味着在配置阶段,我们可能同时分配了多个 mount 但尚未挂载——然而**只有一个会成为真正的 mount**,其余保持未挂载状态,并在用户 umount 并释放它们时最终销毁。 受影响的函数及其处理: | 函数 | 处理方式 | |---|---| | `ceph_create` | 分配一个新的 `ceph_mount_info` 结构,并记录提供的 id,用于将来的可匹配 mount 比较 | | `ceph_release` | 释放一个**未挂载**的 `ceph_mount_info` 结构。未挂载的结构不会与任何其他结构共享 | | `ceph_conf_read_file` | 读取配置文件、**计算校验和**并**制作一份拷贝**。拷贝用于确保自校验和计算以来配置文件没有变化;校验和被记录用于将来的比较 | | `ceph_conf_get` | 获取请求的设置,并**记录**它以备将来比较。虽然看起来多余(因为守护进程把配置当黑盒),但可能存在某些**动态设置**会因外部因素返回不同值,所以守护进程也要求任何被请求的设置返回相同值才认为两个配置相同 | | `ceph_conf_set` | 记录修改后的值以备将来比较。正常情况下某些设置甚至在挂载之后仍可设置——**代理不允许这样做**,以避免与共享同一 mount 的其他客户端产生潜在干扰 | | `ceph_init` | **no-op**。调用此函数会触发若干资源分配并启动一些线程;如果这个 `ceph_mount_info` 结构最终因为与已有 mount 匹配而没有被挂载,那就是纯粹的浪费。**只有在 `ceph_mount()` 时没有匹配到已有 mount,才会同时初始化并挂载** | | `ceph_select_filesystem` | 记录所选文件系统以备将来比较 | | `ceph_mount` | 尝试寻找一个与这个 `ceph_mount_info` 结构所定义的所有配置匹配的**活动 mount**。若没找到,就挂载它;否则,**把这个已存在的 mount 共享给该客户端**。未挂载的 `ceph_mount_info` 结构会被保留并与已挂载的那个关联。**所有「真实」mount 都针对 volume 的绝对根(即 `/`)进行**,以确保它们以后可以与其他客户端共享,无论它们是否使用相同挂载点。这意味着挂载后守护进程需要解析并存储「虚拟」挂载点的根 inode。**CWD 也会被初始化为同一个 inode** | | `ceph_unmount` | 把客户端从已挂载的 `ceph_mount_info` 结构上**分离**,并重新关联到某个关联的未挂载结构上。**如果这是该 mount 的最后一个使用者,则最终卸载它。** 调用此函数后,客户端继续使用一个**私有的、自己独占的** `ceph_mount_info` 结构,因此可以安全地进行其他配置变更和操作 | **B. 把访问限制在预期的挂载点内** 由于有效挂载点可能与实际挂载点不一致,如果不小心处理,某些函数可能返回有效挂载点之外的 inode。为此需要在 `libcephfsd` 内部**模拟**其中一些函数。 三种特殊情况: 1. 以 `/` 开头的路径的处理 2. 包含 `..`(父目录)的路径的处理 3. 包含符号链接的路径的处理 当发现这些特殊路径时,需要特殊处理,以确保返回的 inode 是客户端期望的那个。 受影响的函数: | 函数 | 处理方式 | |---|---| | `ceph_ll_lookup` | lookup 接受 `..` 作为要解析的名字。如果父目录是「虚拟」挂载点的根(可能与实际挂载点不同),我们需要返回**挂载时存储的「虚拟」挂载点对应的 inode**,而不是真正的父目录 | | `ceph_ll_lookup_root` | 需要返回**挂载时存储的根 inode** | | `ceph_ll_walk` | 将在守护进程内部**完全重新实现**,以便正确解析每个路径分量和符号链接,并正确处理 `/` 和 `..` | | `ceph_chdir` | 解析传入的路径,并把该路径及对应 inode 存储在当前「虚拟」mount 中。**不会**调用真正的 `ceph_chdir()` | | `ceph_getcwd` | 直接返回之前 `ceph_chdir()` 调用存储在「虚拟」mount 中的路径 | **C. 处理 `AT_FDCWD`** 任何接收文件描述符的函数都可能收到特殊的 **`AT_FDCWD`** 值。这些函数需要检查该值并改用「虚拟」CWD。 ### 测试 代理对任何已经使用 `libcephfs.so` 的应用应该是**透明的**,这也适用于测试脚本和应用。因此**任何针对常规 `libcephfs.so` 库的现有测试也可以用来测试代理**。 > 引自:[Design of the libcephfs proxy](https://docs.ceph.com/en/tentacle/dev/libcephfs_proxy/) ## 3.6 Ceph 库架构(`libs.rst`) Ceph 被组织成若干**库**,这些库先被构建,然后组合成可执行文件和其他库。 | 库 | 说明 | |---|---| | **libcommon** | 一组对几乎所有 ceph 库和可执行文件都可用的实用工具。**一般来说 libcommon 不应包含全局变量**,因为它被设计成会被链接进像 `libcephfs.so` 这样的库中。 | | **libglobal** | 一组专注于 **Ceph 守护进程程序**需求的实用工具。在这里你会找到 pidfile 管理函数、信号处理器等等。 | > ⚠️ 该页有一行 `.. todo:: document other libraries` —— **官方承认其他库尚未文档化**。因此除 libcommon / libglobal 外的库说明,本文档「未明确」。 > 引自:[Library architecture](https://docs.ceph.com/en/tentacle/dev/libs/) --- # 四、RBD 相关开发文档 ## 4.1 RBD Layering(`rbd-layering.rst`) ### 是什么 / 为什么需要 RBD layering 指创建块设备的 **copy-on-write(写时复制)克隆**。这使得**快速创建镜像**成为可能,例如把一个虚机的 golden master 镜像克隆出一个新实例。为了简化语义,**你只能克隆一个快照**——快照永远是只读的,所以镜像的其余部分不受影响,也不存在误写的可能。 从用户角度看,**克隆就是和其他 rbd 镜像一样**:可以对它们做快照、读写、resize 等,从用户视角没有任何限制。 术语:下文中的 **`child`(子)** 指通过克隆创建的 rbd image,**`parent`(父)** 指子镜像所克隆自的那个 rbd image snapshot。 ### 命令行用法 克隆前必须把快照标记为 **protected**,防止在还有子镜像引用它时被删除: ```bash $ rbd snap protect pool/image@snap ``` 然后执行克隆: ```bash $ rbd clone [--parent] pool/parent@snap [--image] pool2/child1 ``` 可以创建与父镜像对象大小不同的克隆: ```bash $ rbd clone --order 25 pool/parent@snap pool2/child2 ``` 要删除父镜像,必须先取消保护,`unprotect` 会检查是否还有子镜像存在: ```bash $ rbd snap unprotect pool/image@snap Cannot unprotect: Still in use by pool2/image2 $ rbd children pool/image@snap pool2/child1 pool2/child2 $ rbd flatten pool2/child1 $ rbd rm pool2/child2 $ rbd snap rm pool/image@snap Cannot remove a protected snapshot: pool/image@snap $ rbd snap unprotect pool/image@snap ``` 然后就可以像平常一样删除快照了: ```bash $ rbd snap rm pool/image@snap ``` ### 实现:数据流(Data Flow) 初始实现称为 **"trivial layering"(平凡分层)**,其中**不跟踪克隆中存在哪些对象**。 - **读**:一次读如果命中一个不存在的对象,会尝试从**父快照**读,并**递归地**继续,直到找到对象存在,或找到一个没有父的镜像。这是通过**从父读取的正常读路径**完成的,所以父子之间对象大小不同也没关系。 - **写**:在对一个对象执行写之前,会检查该对象是否存在。如果不存在,就执行一次 **copy-up(上拷)** 操作——从父快照读取相关范围的数据,并把它(加上原始的写)写入子镜像。 - 为防止多个写同时尝试 copy-up 同一对象,这个 copy-up 操作会包含一个 **atomic create(原子创建)**。如果原子创建失败,就改为执行原始的写。 - copy-up 操作**实现为一个类的方法**,以便将来可以由它存储额外的元数据。 - 在 trivial layering 中,copy-up 会把**所需的整个范围**(即子对象的完整大小)拷贝到子对象。未来的优化可以让这个 copy-up 更细粒度。 - **未来优化 1**:更细粒度的 copy-up。 - **未来优化 2**:存储一个**位图**,记录子镜像中实际存在哪些对象。这将**免除每次写之前的存在性检查**,并让读在需要时直接转向父。 - 这些优化在 http://marc.info/?l=ceph-devel&m=129867273303846 讨论。 ### 实现:父子关系(Parent/Child relationships) - 子镜像在其 header 中以一个 **tuple `(pool id, image id, snapshot id)`** 存储对其父的引用。这足以打开父并从中读取。 - 除了知道某个 image 的父是谁,我们还想知道**一个被保护的快照是否还有子镜像**。这通过一个**新的 per-pool 对象 `rbd_children`** 完成,它把 `(parent pool id, parent image id, parent snapshot id)` 映射到**子 image id 列表**。 - 它存储在**与子镜像相同的池**中,因为创建克隆的客户端已经拥有该池中所有内容的读/写权限,但**可能没有父池的写权限**。 - 这让一个只对某个池有**只读**访问权的客户端,可以把该池中的快照克隆到一个它有完全访问权限的池。 - 代价是**增加了 unprotect 一个 image 的成本**,因为这需要检查**每个池**中的子镜像——但这是个罕见操作,通常只在删除旧镜像之前做,而那已经昂贵得多(涉及删除镜像中每个数据对象)。 ### 实现:保护(Protection) 在内部,`protection_state` 是 header 对象中的一个字段,可处于三种状态: | 状态 | 何时设置 | |---|---| | `protected` | `rbd protect` 的结果 | | `unprotected` | `rbd unprotect` 的结果 | | `unprotecting` | 在 `rbd unprotect` 命令**检查是否存在任何子镜像**期间设置 | **只有处于 `protected` 状态的快照可以被克隆**,因此 `unprotected` 状态防止了如下竞态: ```text 1. A: 遍历所有池,寻找克隆,没有找到 2. B: 创建一个克隆 3. A: unprotect 父 4. A: rbd snap rm pool/parent@snap ``` (`unprotecting` 这个中间状态的存在,正是为了覆盖步骤 1→3 之间这段窗口。) ### 实现:Resize - Resize 一个 rbd image 类似于 **truncate 一个稀疏文件**。新空间被视为零;缩小 rbd image 会删除超出新边界的内容。这意味着如果你有一个装满数据的 10G 镜像,先缩到 5G 再放大回 10G,**最后 5G 会被当作零**(而持有那部分数据的对象在缩容时已被删除)。 - **Layering 使这变复杂了**,因为**对象不存在不再意味着应被当作零**——如果该对象是克隆的一部分,它可能意味着需要从父读取一些数据。 - 为了对克隆保持 resize 的行为,我们需要**跟踪哪些对象可能存储在父中**。我们可以把它跟踪为**子与父的重叠量(overlap)**,因为 resize 只改变镜像的末端。 - 当子被创建时,它的 overlap = 父快照的大小。 - 此后每次 resize,overlap = `min(overlap, new_size)`。即:**缩小镜像可能缩小 overlap,放大镜像的 size 不改变 overlap。** - **overlap 之外的、不存在的对象被视为零**;**该点之前的、不存在的对象回退到从父读取**。 - 由于 overlap 会随时间变化,我们把它作为**快照元数据的一部分**存储。 ### 实现:Rename 当前 rbd header 对象(存储镜像所有元数据的那个)**以镜像名字命名**。这使得 rename 会扰乱已打开该镜像的客户端(例如从父读取的子)。为避免这一点,我们可以用**镜像的 id** 为 header 对象命名,id 不会改变——即 header 对象的名字可以是 `rbd_header.$id`,其中 `$id` 是该镜像在池中的唯一 id。 当客户端打开一个镜像时,它只知道名字。已经有一个 per-pool 的 `rbd_directory` 对象把 image name 映射到 id,但如果我们依赖它来获取 id,那么一旦这单个对象不可用,我们就**无法打开该池中的任何镜像**。为避免这种依赖,我们可以把 image 的 id 存储在一个名为 **`rbd_id.$image_name`** 的对象中。per-pool 的 `rbd_directory` 对象仍然对**列出池中所有镜像**有用。 ### Header 变更 header 需要几个新字段: ```cpp * int64_t parent_pool_id * string parent_image_id * uint64_t parent_snap_id * uint64_t overlap (how much of the image may be referring to the parent) ``` 这些存储在一个 **"parent" key** 中,该 key **仅当镜像有父时存在**。 ### `cls_rbd` 需要的新方法 ```cpp /***************** methods on the rbd header *********************/ /** * Sets the parent and overlap keys. * Fails if any of these keys exist, since the image already * had a parent. */ set_parent(uint64_t pool_id, string image_id, uint64_t snap_id) /** * returns the parent pool id, image id, snap id, and overlap, or -ENOENT * if parent_pool_id does not exist or is -1 */ get_parent(uint64_t snapid) /** * Removes the parent key */ remove_parent() // after all parent data is copied to the child /*************** methods on the rbd_children object *****************/ add_child(uint64_t parent_pool_id, string parent_image_id, uint64_t parent_snap_id, string image_id); remove_child(uint64_t parent_pool_id, string parent_image_id, uint64_t parent_snap_id, string image_id); /** * List ids of a given parent */ get_children(uint64_t parent_pool_id, string parent_image_id, uint64_t parent_snap_id, uint64_t max_return, string start); /** * list parent */ get_parents(uint64_t max_return, uint64_t start_pool_id, string start_image_id, string start_snap_id); /************ methods on the rbd_id.$image_name object **************/ set_id(string id) get_id() /************** methods on the rbd_directory object *****************/ dir_get_id(string name); dir_get_name(string id); dir_list(string start_after, uint64_t max_return); dir_add_image(string name, string id); dir_remove_image(string name, string id); dir_rename_image(string src, string dest, string id); ``` 如果镜像支持 layering,两个已有方法会改变: ```cpp snapshot_add - stores current overlap and has_parent with other snapshot metadata (images that don't have layering enabled aren't affected) set_size - will adjust the parent overlap down as needed. ``` ### `librbd` 层面 - 打开一个子镜像会打开它的父(并且会按需**递归**继续)。这意味着一个 `ImageCtx` 会包含一个指向父 image context 的指针。**对象大小不同无关紧要**,因为从父读取会通过父的 image context 进行。 - **Discard 需要对 layered image 改变**,使其**只 truncate 对象,而不移除对象**。如果我们移除了对象,就无法判断是否需要从父读取它们。 - 会新增一个 **clone 方法**,参数与 `create` 相同,除了 size(使用父镜像的 size)。 - 与其扩展 `rbd_info` 结构,我们会把元数据检索**拆分成若干 API 调用**。目前除了 `rbd info` 之外,`rbd_stat()` 的唯一使用者只是用它来获取 image size。 > 引自:[RBD Layering](https://docs.ceph.com/en/tentacle/dev/rbd-layering/) ## 4.2 RBD Incremental Backup 格式(`rbd-diff.rst`) ### 是什么 这是一种简单的**流式文件格式**,用于表示 RBD image 的**两个快照之间**(或一个快照与 head 之间)的 diff。 ### v1 格式 **Header**:`"rbd diff v1\n"` **Metadata records**:每条记录有一个标识记录类型的**一字节「tag」**,后跟一些其他数据。Metadata records 位于 image 的**第一部分**。顺序不重要,只要所有 metadata records 都排在 data records 之前。 | 记录 | 结构 | |---|---| | From snap | `u8: 'f'` + `le32: snap name length` + snap name | | To snap | `u8: 't'` + `le32: snap name length` + snap name | | Size | `u8: 's'` + `le64: (ending) image size` | **Data Records**:位于序列的**第二部分**。 | 记录 | 结构 | |---|---| | Updated data | `u8: 'w'` + `le64: offset` + `le64: length` + length 字节的实际数据 | | Zero data | `u8: 'z'` + `le64: offset` + `le64: length` | **Final Record(End)**:`u8: 'e'` ### v2 格式 **Header**:`"rbd diff v2\n"` **Metadata records**:每条记录有一个一字节 tag,**后跟数据长度**,然后是数据。metadata records 位于 image 的第一部分,顺序不重要,只要都排在 data records 之前。 在 v2 中,每个 section 的元数据为:**(1 Byte) tag + (8 Bytes) length + (n Bytes) data**。**这样我们就可以跳过不认识的 tag。** | 记录 | 结构 | |---|---| | From snap | `u8: 'f'` + `le64: length of appending data (4 + length)` + `le32: snap name length` + snap name | | To snap | `u8: 't'` + `le64: length of appending data (4 + length)` + `le32: snap name length` + snap name | | Size | `u8: 's'` + `le64: length of appending data (8)` + `le64: (ending) image size` | | Updated data | `u8: 'w'` + `le64: length of appending data (8 + 8 + length)` + `le64: offset` + `le64: length` + length 字节的实际数据 | | Zero data | `u8: 'z'` + `le64: length of appending data (8 + 8)` + `le64: offset` + `le64: length` | | End | `u8: 'e'` | > 💡 补充:v1 与 v2 的关键差异就是 v2 在每个记录中加入了 8 字节长度字段,从而支持**向前兼容地跳过未知 tag**。文档未说明当前 `rbd export-diff` 默认产生哪个版本。 > 引自:[RBD Incremental Backup](https://docs.ceph.com/en/tentacle/dev/rbd-diff/) ## 4.3 RBD Export & Import 格式(`rbd-export.rst`) ### 是什么 这是**一个 RBD image 或 snapshot 的文件格式**。它是针对完整镜像的**稀疏格式**。文件中**有三个记录段**: 1. Header 2. Metadata 3. Diffs ### Header & Metadata records **Header**:`"rbd image v2\n"` Metadata records 与 rbd-diff v2 采用同样的方式:每条记录有一个一字节 tag,后跟数据长度,然后是数据。**(1 Byte)tag + (8 Bytes) length + (n Bytes) data**,这样可以跳过不认识的 tag。顺序不重要,只要 metadata records 都排在 data records 之前。 | 记录 | 结构 | |---|---| | Image order | `u8: 'O'` + `le64: length of appending data (8)` + `le64: image order` | | Image format | `u8: 'F'` + `le64: length of appending data (8)` + `le64: image format` | | Image Features | `u8: 'T'` + `le64: length of appending data (8)` + `le64: image features` | | Image Stripe unit | `u8: 'U'` + `le64: length of appending data (8)` + `le64: image striping unit` | | Image Stripe count | `u8: 'C'` + `le64: length of appending data (8)` + `le64: image striping count` | | ImageMeta Key and Value | `u8: 'M'` + `le64: length of appending data (length of key + length of value + 4 * 2)` + `string: image-meta key` + `string: image-meta value` | **Final Record(End)**:`u8: 'E'`(注意是**大写 E**,与 rbd-diff 的小写 `e` 不同) ### Diffs records 这一段记录**所有快照以及 HEAD**。 | 记录 | 结构 | |---|---| | Snap Protection status | 当 `--export-format=2` 时记录快照的保护状态:`u8: 'p'` + `le64: length of appending data (8)` + `u8: snap protection status (0 for false, 1 for true)` | | Others | `le64: number of diffs` + Diffs ... | 细节参见 `rbd-diff.rst`(即本册 4.2 节)。 > 引自:[RBD Export & Import](https://docs.ceph.com/en/tentacle/dev/rbd-export/) --- # 五、组件 / 模块开发文档 ## 5.1 Balancer 设计(`balancer-design.rst`) ### 引言:为什么需要平衡 在像 Ceph 这样的分布式存储系统中,平衡写和读请求对获得最优性能很重要: - **写平衡(Write balancing)** 确保数据在集群中快速存储和复制; - **读平衡(Read balancing)** 确保数据在集群中快速访问和检索。 两者重要性来源不同。 ### Upmap Balancing(容量平衡) **为什么重要(功能性需求)**:容量平衡是一个**功能性需求**。像 Ceph 这样的系统,**其容量取决于最满的那个设备**:当一个设备满了,系统就无法再服务写请求,Ceph 也就失去了功能。为避免设备被填满,我们希望以公平的方式在各设备间平衡容量——**每个设备应按其大小成比例地获得容量**,使所有设备处于相同的满载水平。从性能角度看,容量平衡为写请求在 OSD 上创造了公平的工作负载分担。 **代价**:容量平衡**昂贵**。该操作(改变 pgs 的映射)**按定义就需要数据移动**,这需要时间。在此期间系统性能会下降。 **前提**:在 Ceph 中,**如果所有设备是同构的(相同大小和性能)**,我们就能平衡写性能。 具体做法见官方文档的 `upmap` 章节(本册未包含该页内容)。 ### Read Balancing(读平衡) **与容量平衡不同,读平衡对 Ceph 的功能性不是严格要求**,而是一个**性能需求**——它帮助系统「更好地工作」。总体目标是**确保每个设备获得其公平份额的 primary OSD**,使读请求在集群 OSD 间均匀分布。读请求不均衡会导致整体集群带宽下降,从而性能变差。 **代价:便宜。** 与容量平衡不同,**不涉及数据移动**。它只是一个**元数据操作**——更新 osdmap 以改变 pg 中哪个参与 OSD 是 primary。这个操作很快,对集群性能没有影响(除了操作完成后——几乎是立即——带来的性能提升)。 **前提**:在 Ceph 中,**如果所有设备是同构的**,我们就能平衡读性能。然而**在未来的版本中,read balancer 可以被改进以在异构系统中实现整体集群性能**。 具体做法见官方文档的 `read_balancer` 章节。另见 Cephalocon 2023 的演讲 *New Read Balancer in Ceph*(https://www.youtube.com/watch?v=AT_cKYaQzcU/),演示了 read balancer 的**离线版本**。 ### 下一版本计划(Plans for the Next Version) 1. **改善池中异构 OSD 的行为。** > 💡 补充:该页非常简短,只给出了「为什么」和高层次方向,没有算法细节(评分函数、迭代策略等均未在本文档中)。 > 引自:[Balancing in Ceph](https://docs.ceph.com/en/tentacle/dev/balancer-design/) ## 5.2 cephadm 开发文档(`doc/dev/cephadm/`) 目录(来自 `cephadm/index.rst`):`developing-cephadm`、`host-maintenance`、`compliance-check`、`Storage devices and OSDs management (design/storage_devices_and_osds)`、`scalability-notes`。 ### 5.2.1 Developing with cephadm(`developing-cephadm.rst`) #### 方式一:`vstart --cephadm` 适用于**开发 cephadm 本身**。 ```bash MON=1 MGR=1 OSD=0 MDS=0 ../src/vstart.sh -d -n -x --cephadm ``` - mon 和 manager 至少是以通常的 vstart 方式运行的,**不由 cephadm 管理**;但 cephadm 已启用且本地 host 已加入,因此你可以部署额外的守护进程或添加额外的主机。 - 这对开发 cephadm 本身很有效,因为任何 mgr/cephadm 或 cephadm/cephadm 的代码改动都可以通过 **kick ceph-mgr** 来应用:`ceph mgr fail x`。(当 mgr (重新)启动时,它会把 cephadm/cephadm 脚本加载到内存。) - `~/.ssh/id_dsa[.pub]` 被用作 cluster key。假定该 key 被授权可以无密码 ssh 到 root@`hostname`。 - cephadm **不会**尝试管理任何由 vstart.sh 启动的守护进程(环境变量中的任何非零数字)。没有为 mon 或 mgr 定义 service spec。 - 你会看到来自 cephadm 的关于 **stray daemons** 的健康告警——因为 vstart 启动的守护进程不受 cephadm 控制。 - 默认镜像是 `quay.io/ceph-ci/ceph:main`,可通过 `-o container_image=...` 或 `ceph config set global container_image ...` 修改。 #### 方式二:`cstart` 与 `cpatch` `cstart.sh` 会用 cephadm 启动一个集群,并把 conf 和 keyring 放在你的 build 目录里,使得 `bin/ceph ...` CLI 可用(就像 vstart 一样)。`ckill.sh` 负责拆除。 - 一个唯一但稳定的 fsid 存储在 build 目录的 `fsid` 文件中。 - mon 端口是随机的,就像 vstart 一样。 - 容器镜像是 `quay.io/ceph-ci/ceph:$tag`,其中 `$tag` 是 fsid 的**前 8 个字符**。 - 如果首次运行 cstart 时容器镜像还不存在,会用 cpatch 构建它。 **优点**:这个集群是一个「正常」的 cephadm 集群,外观和行为**与用户的集群一样**。相比之下,vstart 和 teuthology 集群往往在某些(微妙的和不那么微妙的)方面很特殊(例如打开了 `lockdep`)。 ```bash sudo ../src/cstart.sh ``` 输出的最后一行是你可以复制粘贴用来更新容器镜像的命令,例如: ```bash sudo ../src/script/cpatch -t quay.io/ceph-ci/ceph:8f509f4e ``` 默认情况下 cpatch 会把本地 build 目录中它能想到的一切都打进容器镜像。如果你只在系统某个特定部分上工作,可以做更小的改动让 cpatch 跑得更快: ```bash # 更新 mgr modules(不含 dashboard) sudo ../src/script/cpatch -t quay.io/ceph-ci/ceph:8f509f4e --py # 更新大多数二进制和库 sudo ../src/script/cpatch -t quay.io/ceph-ci/ceph:8f509f4e --core # 查看所有选项 cpatch -h ``` 容器更新后,可以通过重启来刷新/重启守护进程: ```bash sudo systemctl restart ceph-`cat fsid`.target ``` 拆除集群: ```bash sudo ../src/ckill.sh # 或, sudo ../src/cephadm/cephadm rm-cluster --force --fsid `cat fsid` ``` #### 方式三:`cephadm bootstrap --shared_ceph_folder` **无需编译 ceph 二进制**即可直接使用 cephadm。 ```bash sudo ./cephadm bootstrap --mon-ip 127.0.0.1 \ --ssh-private-key /home//.ssh/id_rsa \ --skip-mon-network \ --skip-monitoring-stack --single-host-defaults \ --skip-dashboard \ --shared_ceph_folder /home//path/to/ceph/ ``` - `~/.ssh/id_rsa` 被用作 cluster key,假定该 key 被授权可以无密码 ssh 到 root@`hostname`。 - 在 `pybind/mgr/` 目录中做的源码修改需要**重启守护进程**才能生效。 #### 方式四:Kcli(虚拟化管理工具) `Kcli`(https://github.com/karmab/kcli)用于与现有虚拟化提供者(libvirt、KubeVirt、oVirt、OpenStack、VMware vSphere、GCP、AWS)交互,从 cloud images 轻松部署和定制 VM。 **主要优势**: - **快**。通常你可以在 5 分钟内得到一个全新的 Ceph 集群,随时可调试和开发 orchestrator 特性。 - **「接近生产」的实验室**。得到的环境接近 QE 实验室甚至生产中的「真实」集群,便于在近乎真实的环境中测试「真实的东西」。 - **安全且隔离**。不依赖你机器上安装的东西,VM 与你的环境隔离。 - **易于工作的「dev」环境**。对于「未编译」的软件部分(例如任何 mgr 模块),允许你交互式地测试改动。 **安装**(建议用容器镜像方式): ```bash # podman pull quay.io/karmab/kcli # alias kcli='podman run --net host -it --rm --security-opt label=disable -v $HOME/.ssh:/root/.ssh -v $HOME/.kcli:/root/.kcli -v /var/lib/libvirt/images:/var/lib/libvirt/images -v /var/run/libvirt:/var/run/libvirt -v $PWD:/workdir -v /var/tmp:/ignitiondir quay.io/karmab/kcli' ``` > ⚠️ 假设 `/var/lib/libvirt/images` 是你的默认 libvirt pool,若不同请调整。 > ⚠️ 一旦你用 kcli 工具创建并使用了不同的 lab,**建议固定使用某个容器 tag 并更新你的 kcli alias**。因为 kcli 采用滚动发布模型,固定 tag 会提升整体稳定性。 **创建 Ceph 集群**:使用 "plan"(一个文件,可定义一组不同设置的 VM,包括硬件参数、操作系统,还能自动化安装配置任何你想要的软件)。 ```bash # kcli create plan -u https://github.com/karmab/kcli-plans/blob/master/ceph/ceph_cluster.yml ``` 这会创建三个 VM。几分钟后检查: ```bash # kcli list vms # kcli ssh ceph-node-00 [centos@ceph-node-00 ~]$ sudo -i [root@ceph-node-00 ~]# cephadm version [root@ceph-node-00 ~]# cephadm shell [ceph: root@ceph-node-00 /]# ceph orch host ls ``` **让开发 mgr 模块变简单**:原理是把各 ceph 守护进程中的若干 python mgr 目录替换为你宿主机上的源码目录。这样你可以修改任何 orchestrator 或 dashboard 模块并**立即**测试(只需 disable/enable 该 mgr 模块)。 ```bash # kcli create plan -u https://github.com/karmab/kcli-plans/blob/master/ceph/ceph_cluster.yml -P ceph_dev_folder=/home/mycodefolder/ceph ``` **Ceph Dashboard 开发**:如果没有事先生成 frontend bundle,dashboard 模块不会被加载。目前需要在你的笔记本上跑 `ng build`: ```bash # Start local frontend build with watcher (in background): sudo dnf install -y nodejs cd cd src/pybind/mgr/dashboard/frontend sudo chown -R :root dist node_modules NG_CLI_ANALYTICS=false npm ci npm run build -- --deleteOutputPath=false --watch & ``` 保存改动后 frontend bundle 会重新构建,完成后你会看到 `"Localized bundle generation complete."`,然后刷新 Dashboard 浏览器标签页即可。 #### 方式五:Cephadm box(Podman inside Podman) 由于 kcli 启动时间长,社区创建了这个更快的替代方案(**用 Podman 套 Podman**)。代价是必须**用 loopback 设备模拟 OSD 创建和设备添加**。 概念:**box** 是第一层 Podman 容器,可以是 **seed** 或 **host**。seed 是主 box,持有 Cephadm 并在其中 bootstrap 集群;host 则配置有 SSH server 以便加入集群。第二层由 Cephadm 在 seed box 内部管理,需要 Ceph 镜像。 > ⚠️ **该开发环境仍处于实验阶段**,可能有意外行为。 **要求**:`podman-compose`、lvm **Setup**: ```bash cd src/cephadm/box ./box.py -v cluster setup ``` > 建议用 verbose(`-v`)运行 box,它会显示正在执行的 shell 命令输出。 启动一个没有 OSD 和 host 的简单集群: ```bash ./box.py -v cluster start ``` 带更多 OSD 和 host 部署: ```bash # 默认 3 osds 和 3 hosts sudo box -v cluster start --extended # 显式指定 host 和 osd 数量 sudo box -v cluster start --extended --osds 5 --hosts 5 ``` > ⚠️ **OSD 在基于 Podman 的 box 实现中尚不支持,仍在开发中。** > ⚠️ 不带 `--extended` 时,显式添加更多 host 或 OSD 都不会改变集群状态。 > ⚠️ OSD 是用 loopback 设备创建的,因此需要 sudo 来创建能容纳 OSD 的 loopback 设备。 > ⚠️ **每个 OSD 需要 5GiB 空间。** 进入 seed box: ```bash ./box.py -v cluster bash [root@8d52a7860245] cephadm --help [root@8d52a7860245] cephadm shell ... ``` Dashboard:浏览器访问 https://localhost:8443 查看每个 box 容器的 hostname 和 ip: ```bash ./box.py cluster list ``` ```text IP Name Hostname 172.30.0.2 box_hosts_1 6283b7b51d91 172.30.0.3 box_hosts_3 3dcf7f1b25a4 172.30.0.4 box_seed_1 8d52a7860245 172.30.0.5 box_hosts_2 c3c7b3273bf1 ``` 拆除/清理/帮助: ```bash ./box.py cluster down ./box.py cluster cleanup ./box.py --help ``` 用 Docker 作为引擎: ```bash ./box.py -v --engine docker cluster list sudo ./box.py -v --engine docker cluster start --expanded ``` > ⚠️ **用 Docker 作为 box 引擎是危险的**,曾有实例导致 Xorg session 被杀掉。 > ⚠️ Docker 容器以 `--privileged` 运行,已观察到会让一些计算机 logout。 **已知问题**: - 如果因为无法推断 keyring 和配置而遇到 cephadm 权限问题,可以这样运行: `cephadm shell --config /etc/ceph/ceph.conf --keyring /etc/ceph/ceph.kerying` - **如果 SELinux 未禁用,你可能会看到意外行为**。例如,如果 Ceph repo 文件的权限没有全部设为你的用户,用 podman-compose 启动可能会失败。 - 如果运行某个命令时因为找不到容器而无法执行 podman 命令,可以用 `-v` 显示的同一条 `podman-compose .. up` 命令来调试。 **Road map**: - 用 `ceph-volume raw` 创建 osd - 让 ceph-volume 能把 loopback 设备标记为 inventory 中的有效块设备 - 让 box 具备运行 dashboard CI 测试(包括集群扩容)的能力 #### 关键设计约束:CLI handler 中的网络调用 执行任何 cephadm CLI 命令(如 `ceph orch ls`)会**阻塞 MGR 内的 mon 命令处理线程**,从而**阻止任何并发的 CLI 调用**。注意按 `^C` **不能**解决这个情况,因为**只有客户端会被中止,而 orchestrator manager module 内部命令的执行不会被中止**。这意味着 **cephadm 将完全无响应,直到该 CLI handler 执行完毕**。注意即使是 `ceph orch ps` 在另一个 handler 执行期间也不会响应。 因此:**我们应该对远程主机做非常少的同步调用。作为准则,cephadm 在 CLI handler 中最多做 `O(1)` 次网络调用。其他一切都应在其他线程(如 `serve()`)中异步完成。** #### 代码中变量的命名约定(很容易混淆,值得背下来) | 变量 | 含义 | |---|---| | `service_type` | 诸如 mon、mgr、alertmanager 之类,定义在 `ServiceSpec` 中 | | `service_id` | 服务的名字。有些服务没有名字 | | `service_name` | `.` | | `daemon_type` | 与 `service_type` 相同,**除了 ingress**——ingress 有 haproxy 和 keepalived 两种 daemon type | | `daemon_id` | 通常是 `..`。(OSD 不是这样,OSD 总是叫 OSD.N) | | `daemon_name` | `.` | #### 编译 cephadm 较新版本的 cephadm 基于 **Python Zip Application**(PEP 441)支持,是从 ceph 树中的 Python 源码文件「编译」出来的。用 `src/cephadm/build.py` 创建你自己的 cephadm「二进制」: ```bash ./src/cephadm/build.py [output] ``` 可用 `--set-version-var` / `-S` 传入有限的一组版本元数据,值为 `KEY=VALUE`,合法 key 包括: - `CEPH_GIT_VER` - `CEPH_GIT_NICE_VER` - `CEPH_RELEASE` - `CEPH_RELEASE_NAME` - `CEPH_RELEASE_TYPE` 示例: ```bash ./src/cephadm/build.py -SCEPH_GIT_VER=$(git rev-parse HEAD) -SCEPH_GIT_NICE_VER=$(git describe) /tmp/cephadm ``` 通常这些值会由更高层的构建工具(如 cmake)传给 build.py。 用 `--bundled-dependencies` / `-B` 选择捆绑依赖的模式,值为 `pip`、`rpm` 或 `none`(分别使用 Python 的 `pip`、本地安装的 RPM,或禁用捆绑依赖)。 编译出的 cephadm zipapp 保留了构建方式的元数据,可用 `cephadm version --verbose` 显示: ```bash $ ./cephadm version --verbose { "name": "cephadm", "ceph_git_nice_ver": "18.0.0-6867-g6a1df2d0b01", "ceph_git_ver": "6a1df2d0b01da581bfef3357940e1e88d5ce70ce", "ceph_release_name": "reef", "ceph_release_type": "dev", "bundled_packages": [ { "name": "Jinja2", "version": "3.1.2", "package_source": "pip", "requirements_entry": "Jinja2 == 3.1.2" }, { "name": "MarkupSafe", "version": "2.1.3", "package_source": "pip", "requirements_entry": "MarkupSafe == 2.1.3" } ], "zip_root_entries": [ "Jinja2-3.1.2-py3.9.egg-info", "MarkupSafe-2.1.3-py3.9.egg-info", "__main__.py", "__main__.pyc", "_cephadmmeta", "cephadmlib", "jinja2", "markupsafe" ] } ``` > 引自:[Developing with cephadm](https://docs.ceph.com/en/tentacle/dev/cephadm/developing-cephadm/) ### 5.2.2 Host Maintenance(`host-maintenance.rst`) > 这是一份**设计文档**("This document defines the maintenance strategy that **could be** implemented in cephadm and mgr/cephadm"),描述的是方案而非既有实现。 #### 高层设计 把一台主机置入维护状态,采用如下工作流: 1. **确认移除该主机不会影响数据可用性**(后续步骤假定可以继续)。此处使用 `orch host ok-to-stop `。 2. 如果该主机上有 osd 守护进程,对该主机的 **subtree 施加 noout**,以防止在计划的维护窗口期间触发数据迁移。 3. **停止 ceph target**(所有守护进程停止)。 4. **在该主机上禁用 ceph target**,防止重启后自动再次启动 ceph 服务。 **退出维护(Exiting Maintenance)** 基本上就是上述序列的逆过程。 #### 管理员交互 `ceph orch` 命令将扩展以支持维护: ```bash ceph orch host maintenance enter [ --force ] ceph orch host maintenance exit ``` > 此外,主机的 status 应被更新以反映它是否处于维护状态。 **`--check` 选项的作用**:`orch host ok-to-stop` 聚焦于 ceph 守护进程(mon、osd、mds),提供了第一层检查。然而 Ceph 集群还使用**其他类型的守护进程**来做监控、管理和非原生协议支持,这意味着逻辑还需要**考虑服务影响(service impact)**。`--check` 选项提供了这个额外层次,用于提醒用户**次要(secondary)守护进程**的服务影响。 受影响的次要守护进程包括: - `mgr`(**不在** ok-to-stop 检查范围内) - prometheus、grafana、alertmanager - rgw - haproxy - iscsi gateways - ganesha gateways 通过**先**使用 `--check`,管理员可以决定是否继续。这个工作流对 CLI 用户显然是可选的,但可以**集成到 UI 工作流**中,帮助经验不足的管理员管理集群。 采用这种两阶段方法,基于 UI 的工作流大致是: 1. 用户选择一台主机置入维护 - orchestrator 检查**数据**和**服务**影响 2. 如果显示存在潜在影响,后续步骤取决于影响类型: - **数据可用性(data availability)**:**拒绝**维护,并告知用户问题 - **服务可用性(service availability)**:向用户提供受影响服务的列表,并要求确认 #### 受影响的组件 | 组件 | 需要的改动 | |---|---| | `cephadm` | 添加 maintenance 子命令,含 `enter`、`exit`、`check` 三个「动词」 | | `mgr/cephadm` | 给 `CephadmOrchestrator` 添加 enter/exit 和 check 方法;**处于维护状态的主机将跳过数据采集** | | `mgr/orchestrator` | 给 `OrchestratorCli` 添加暴露 enter/exit 与 check 交互的 CLI 命令 | #### 未来工作想法 1. 当主机被置入维护时,**可以持久化该事件的时间**。这将允许 orchestrator 层为该任务建立一个**维护窗口**,并在超出维护窗口时告警。 2. 维护过程**可以支持插件**,允许在进出维护状态的过程中触发其他集成任务。该插件能力可支持如下动作: - 向第三方监控框架**抑制告警** - **服务级别报告**,记录停机窗口 > 引自:[Host Maintenance](https://docs.ceph.com/en/tentacle/dev/cephadm/host-maintenance/) ### 5.2.3 Compliance Check(`compliance-check.rst`) > 这也是一份**设计讨论文档**(原文:"This document is intended to promote a design discussion")。 #### 是什么 / 为什么需要 Ceph 集群的稳定性和可靠性不仅取决于 Ceph 守护进程,也取决于 Ceph 所安装的 **OS 和硬件**。本文档旨在推动在 mgr/cephadm 中提供一个 **"compliance"(合规)特性**,负责识别可能影响 Ceph 稳定性和运行的常见平台相关问题。 **最终目标**:尽早发现问题,并**raise 一个 healthcheck WARN 事件**,提醒管理员注意该问题。 #### 前提条件 为有效分析 Ceph 所部署到的主机,该特性需要一份**主机相关元数据的缓存**。这些元数据已经可以从 **cephadm 的 `HostFacts` 类**和 `gather-facts` cephadm 命令获得。就本文档而言,我们假定这些数据在 mgr/cephadm 的 "cache" 结构中可用。 某些检查还需要 host status 也被填充,例如 `ONLINE`、`OFFLINE`、`MAINTENANCE`。 #### 管理员交互 并非所有用户都需要这个特性,**必须能够 'opt out'**。因此 mgr/cephadm 必须提供如下控制: ```bash ceph cephadm compliance enable | disable | status [--format json] ceph cephadm compliance ls [--format json] ceph cephadm compliance enable-check ceph cephadm compliance disable-check ceph cephadm compliance set-check-interval ceph cephadm compliance get-check-interval ``` - `status` 选项会显示该特性的启用/禁用状态,以及 check-interval。 - `ls` 子命令以 `check-name status description` 的格式显示所有检查。 #### 集成方式 - 合规检查**不需要一直运行**,而应在**离散的时间间隔**上运行。间隔可通过 `set-check-interval` 子命令配置(**默认每 12 小时**)。 - mgr/cephadm 当前执行一个**事件驱动(基于时间)的 serve loop** 来处理 deploy/remove 和 reconcile 活动。为了执行合规检查,**当 `set-check-interval` 满足时**,合规检查代码将从这个主 serve loop 中被调用。 #### 提议的检查项 所有检查会把任何错误 push 到一个列表中,因此**多个问题可以同时升级给管理员**。下表括号/下划线后的**短名**是启用或禁用某个检查时命令交互所使用的引用。 | 检查项(短名) | 内容 | 数据来源 | |---|---|---| | **OS Consistency (OS)** | 所有主机必须使用**同一厂商**;所有主机必须处于**同一 major release**(该检查只适用于提供长期支持策略的发行版,如 RHEL、CentOS、SLES、Ubuntu 等) | `gather-facts` output | | **Linux Kernel Security Mode (LSM)** | 所有主机应具有一致的 **SELINUX/AppArmor** 配置 | `gather-facts` output | | **Services Check (SERVICES)** | 处于 `ONLINE` 状态的主机应遵循:所有守护进程(systemd units)应为 **enabled**;所有守护进程应在**运行**(不是 dead) | `list_daemons` output | | **Support Status (SUPPORT)** | 如果检测到支持状态,它应在所有主机间保持一致。目前**只有 Red Hat 机器**能获得支持状态 | `gather-facts` output | | **Network : MTU (MTU)** | 同一 Ceph 网络(public/cluster)上的所有网络接口应具有**相同的 MTU** | `gather-facts` output | | **Network : LinkSpeed (LINKSPEED)** | 同一 Ceph 网络(public/cluster)上的所有网络接口应具有**相同的 Linkspeed** | `gather-facts` output | | **Network : Consistency (INTERFACE)** | 所有带 OSD 的主机应具有**一致的网络配置**——例如,如果有些主机不分离 cluster/public 流量而另一些分离,这就是一个会产生合规检查告警的异常 | `gather-facts` output | #### 通知策略 如果任何检查失败,mgr/cephadm 会 raise 一个 **WARN 级别告警**。 #### 未来(Futures) 这里列出的检查只是一个起点,我们应预期会随时间扩展检查项。 > 引自:[Compliance Check](https://docs.ceph.com/en/tentacle/dev/cephadm/compliance-check/) ### 5.2.4 Storage Devices and OSDs Management Workflows(`design/storage_devices_and_osds.rst`) > 这是一份**设计/UX 提案文档**,对现有实现提出了相当尖锐的批评。它同时覆盖了 CLI 与 GUI 两端的现状与提议。 #### 四个基本用例 1. 获取设备信息(Inventory) 2. 添加 OSD 3. 移除 OSD 4. 替换 OSD #### 用例 1:Inventory(获取设备信息) 需要能审视集群存储设备的当前状态与状况:需要标识与特性详情(**包括 ident/fault led 是否支持开关**),以及该设备是否被用作 OSD/DB/WAL 设备。 每个设备至少需要的信息: ```text Hostname Path Type Serial Size Health Ident Fault Available ``` 为了知道设备的当前状况,需要知道:已用存储量、空闲空间百分比、平均 IOPS、fault led 状态。这些信息大概应由 **Ceph orchestrator** 提供(因为它是我们能访问这类信息的组件)。 **「效率」的四条要求**: 1. 以最快的方式获取每个设备的完整信息; 2. 一个主机上所有设备的全部信息总是能**立即**访问; 3. 信息在每台主机上**持续更新**。设备故障或新设备添加必须在尽可能短的时间窗内被检测到; 4. **可扩展性**——在数百台主机上管理数千个设备不应成为问题。 **A. 现状:** CLI: ```bash ceph orch device ls ceph orch device ls json ( to get all the fields for each device ) ``` 现状问题:**Does not scale(不能扩展)。** GUI:`cluster.Inventory` 部分提供集群设备的基本列表,是一个**只有少数字段的固定列表**。只有 "ident light on" 操作可用,而且要等真正发起操作后才知道是否可行。 现状问题:不能扩展(依赖 orchestrator);用户体验僵化。 **B. 提议:** - CLI:当前 API 已经足够好,只需确保:拥有每个设备的全部 attribute/health/operative state 字段;响应快;可扩展。 - GUI:inventory 应能**自定义**,以显示每个设备所需的字段信息,并**可自定义每个字段(列)的位置和排序顺序**;应能使用设备列表中出现的任何字段进行过滤;自定义的 inventory 列表连同过滤和排序顺序应能被**保存**以便复用,从而提供一组有趣的预定义 inventory 列表(例如:可用设备;使用更多的设备(更高平均 iops,应作为 alert/trigger);大于 n Gb 的设备)。 - inventory 还应提供直接对物理设备执行操作的方式:**Identify**(开始/停止闪烁标识灯)、**Create OSD**(如果设备可用就用它创建 OSD)、**Remove OSD**(使用该磁盘设备删除 OSD)。 #### 用例 2:添加 OSD **A. 现状:** CLI:可以指定具体设备,或使用 "drive group" 规范在不同主机上创建 OSD。默认情况下,要创建的 OSD 的定义是 **"declarative"(声明式)**,除非你使用 `unmanaged` 参数。 ```bash ceph orch daemon add osd :device1,device2 [--unmanaged=true] (manual approach) ceph orch apply osd -i [--dry-run] [--unmanaged=true]* (Service Spec based approach) ``` GUI:实现在 dashboard 的 `cluster.OSDs` 部分。有一个按钮用于创建 OSD,会弹出一个页面对话框选择将作为 OSD 的 main、WAL 或 Db 设备的物理设备。**选择非常困难(或者说很难理解该如何选择)**。如果你的集群有同类设备,情况更糟,会导致一个怪现象:**不可能只用一块存储设备创建 OSD(因为你无法选中它)**。问题在于 UI 是围绕 **"drive groups"** 设计的,而不是为用户设计的。"drive group" 是一个抽象概念,**只应在后台使用,用户不应意识到这个概念**。 **B. 提议(CLI 与 GUI 共同):** **核心结论:避免 "declarative" 地使用 drive groups。** 理由:声明式 drive group 的使用让人很难理解如何配置 OSD 及其影响,也让实现变得困难,因为生产系统中可能性的多样性和大量不同条件使得正确评估和使用存储设备的声明式描述非常复杂。这会导致意外情况,例如: - 一块被清空的磁盘会**自动且无任何警告地**被复用来创建新 OSD; - 新安装的磁盘会被**自动**用于 OSD(无任何警告); - 尝试在已从系统移除的磁盘上重建 OSD 时出错。 **GUI 设计前提**: - 我们只使用 **bluestore OSD**,这意味着创建 OSD 时可以在不同策略间选择:只消耗单个设备、用额外设备做 WAL、和/或再用另一个设备做 DB。 - 只有当 WAL/DB 设备**比主存储设备更快**时,把 bluestore OSD 的数据组件拆分到不同设备才有意义。 - 设备的拆分**总是在同一主机内**,尽管该配置可应用于具有相同存储设备方案的其他主机。 - 在生产系统中**大规模创建 OSD 可能酿成真实灾难**,因为 rebalance 会对正常系统性能产生负面影响;而在一个首次安装的集群中做同样的大规模创建,可能正是期望行为。因此应提供机制让用户选择 OSD 的创建方式: - **Fast creation**:快但对性能有害——直接创建 OSD。 - **Conservative creation**:慢但尊重性能——以 **0 weight** 创建所有 OSD;一旦所有 OSD 安装完成,逐个为每个 OSD 赋予正确的 weight。 基于这些前提,提议了两种模式的界面: **Device mode(设备模式)**: - 呈现一个包含所有可用设备、带过滤/列表能力的 inventory 列表,用户可以「把玩」这个列表,得到一组偏好的集群物理存储设备。 - 用户可以从「偏好设备列表」中选择一个、多个或全部设备。这些被选中的设备将用于创建 OSD(**每个物理设备 1 个**)。 - 来自先前被删除 OSD 的 **OSD id 可能是可用的**。用户应指明这些 id 是否要在新 OSD 中使用。 **Host mode(主机模式)**: - 基本上是使用一台主机中的存储设备做一次 OSD 配置。这个配置将作为基础模板,用于在其他主机上应用相同的方案。 - 用户必须先选择一个基础主机。选定后,应提供三个列表(或选择方式): - **"slow devices"**:`hdd` 设备 - **"fast devices (WAL)"**:可用于 Bluestore WAL 数据的 `sdd/nvme` 设备 - **"fast devices(DB)"**:可用于 Bluestore DB 数据的 `sdd/nvme` 设备 - 用户通过对 "slow devices" 列表进行过滤,选择其中一个、多个或全部设备用于创建 OSD。如果用户想把 Bluestore 数据组件拆分到多个设备,需要在另外两个 "fast devices" 列表中执行相同操作。 - OSD id 复用问题同上。 - 设备选定后,可以提供将创建的 OSD 的 **"preview"**(快设备可能承载多个 OSD 的 WAL/DB)。 - 例:用户选择 8 块慢存储设备用于 OSD,2 块 nvme 用于 WAL,1 块 ssd 用于 DB。则每块 nvme 承载 4 个 OSD 的 WAL,ssd 承载 8 个 OSD 的 DB。 - **OSD 创建应先 inventory 并分析,判断 OSD 创建可以是 hybrid 还是 dedicated,并把这些作为选项呈现给用户(他们永远看不到 device group!),然后用户点击创建。** - 当用户对主机上的 OSD 配置满意后,应提供一份可应用相同 OSD 配置的主机列表供其选择。用户从中选择要在其上创建 OSD 的主机。 - 必须提供在所有主机上创建 OSD 的 preview/summary,如果用户想要这个配置,就应用它,从而在多个主机上批量创建 OSD。 - 应提供所有主机上 OSD 创建进度的信息。 **需考虑的关键点**: **1. Context is everything(上下文就是一切)**: 当前的 OSD 创建流程**不提供任何可用设备或主机的指示**。这导致用户点了 add 按钮却什么也看不到(如果没有可用设备)——此时用户会假定没有可用设备。host-mode 和 device-mode 两种 UI 流程都说明了若干**至少应实现**的可用性特性: - a. **如果没有可用设备,add 按钮应被禁用**; - b. OSD 创建的 UI 应包含**已发现主机(带磁盘)的摘要,以及可用于创建 OSD 的可用磁盘总数**。还应显示 total raw,例如:`5 hosts, 50 HDDs (80TB), 10 NVME (5TB)`。发现的配置还可以: - 利用主机的 **rack ID annotations** 从故障域角度审视容量,确保均衡,若不均衡则告警; - **确认主机配置是否相同(homogeneous)**。异构配置应在 UI 中伴随 INFO/WARN 消息,提示异构集群潜在的均衡问题。 - c. 一旦做出部署决定,**显示选择的摘要,由用户 CONFIRM**: - 将使用的按类型划分的设备总数 - 将创建的 OSD 总数 - 创建请求的总体 raw 容量,以及 OSD 添加完成后的潜在 raw 集群容量 - 用经验法则估算大致部署时间——设定预期 **2. Enabling new capacity(启用新容量)**:新磁盘加入集群的策略选项(host-mode 和 device-mode 设计中都有): | 策略 | 行为 | |---|---| | **Phased by OSD** | 所有新增 OSD 的 weight 为 0。orchestrator 然后逐个 reweight 每个 OSD 以驱动 rebalance | | **Phased by host** | 某台主机上的所有 OSD 同时被 reweight | | **Immediate** | 不使用 reweight,直接把 OSD 拉起/置入(在空集群上,这应是默认) | **3. UI redesign**:发现设备、基于这些设备结合最佳实践建议一种布局、在 flash 不足以支撑 HDD 数量时告知、在没有空闲设备时告知,同时也提供我们今天看到的高级用例(即呼应 drive group 方式)。 **4. Imperative not Declarative(命令式而非声明式)**: 使用声明式 "drive groups" 在几个方面是个问题: 对最终用户而言,那位第一次安装集群的「admin persona」知道当前硬件构成,并可能使用集群中所有计划承载 OSD 的主机上的全部存储设备来创建 OSD。**但我们没有告诉这位 admin persona:这个初始决定在未来是不可变的,且会被自动应用而没有任何警告。** 这会导致几种不期望的情形: 1. 带 OSD 的存储设备**不能用于其他用途**,因为它们一被清空就会立刻被重装为 OSD。很难解释:如果你不想这样,就得把设备加入黑名单,或者用 `unmanaged` 参数创建 OSD(而 UI 里没提供这个)。 另一个糟糕的情形:你为主机买了一块新设备用来放 minecraft 服务器,不巧这块设备与你用于 OSD 的设备差不多……那你就装不了 minecraft 服务器了,因为设备被自动用于 OSD。 另一个让人抓狂的情形:你的实验室团队在集群里装了 10 块新盘,而他们恰好装在集群网络流量最大的位置。数据 rebalance 会给「admin persona」带来一个「有趣」的局面。 (原文自评:这是一个关于「我们如何把用户的 OSD 管理搞得更困难」的好例子。) 2. 大概几年后需求会增长,会添加新的不同存储设备,admin persona 需要指定这些设备要承载 OSD。于是我们必须保存最初用于创建初始 OSD 的 "drive group",以及新设备的新 "drive group" 定义。这样我们就有了**多于一个** drive group,意味着两种可能:加一个 "drive groups" 管理工具,或者把两个定义合并成一个。无论哪种,都是「如何把用户和开发者的生活搞得更困难」的好例子。 **所有这些都可以通过使用 imperative drive groups 避免**——我们提供相同的功能,但没有那些不期望的副作用。从开发角度看,这也会简化事情,因此**从 "declarative" drive groups 转向 "imperative" drive groups 似乎是一个非常好的想法**。 > 注:当前 dashboard 中创建 OSD 的实现试图处理 "drive groups",这就是它对最终用户如此不友好的原因。**"drive group" 概念应完全对 dashboard 用户隐藏。** #### 用例 3:移除 OSD **A. 现状:** CLI: ```bash # 逐个删除 OSD ceph orch osd rm [--replace] [--force] # 查看删除操作状态 ceph orch osd rm status # 彻底「清洗」OSD 用过的设备 ceph orch device zap my_hostname /dev/sdx ``` GUI:在 cluster OSD 部分有一个按钮,可对选中的 OSD 执行不同的原语操作,其中之一就是 delete。选中 "delete" 原语并按下 action 按钮后,会弹出一个确认对话框,并有一个复选框询问是否保留 "osd id"。接受之后**似乎什么也没发生……无法知道删除操作的进度。** > 文档自问:我们倾向于在 UI 中展示所有 osd 管理原语——问题是,这会让环境更复杂吗?UI 是否应专注于快速轻松地覆盖 90% 工作的关键 osd 管理流程,而把剩下 10% 留给 CLI? **B. 提议:** - CLI:需要一种**提前知道删除一个 OSD 需要多长时间**(如果我们 rebalance 数据)的方式;当前的命令集能满足主要需求。 - GUI: - 用户应从一个带过滤能力的列表中选择要移除的 OSD(或 OSD 集合)。 - OSD 移除应提供**保留 OSD id 供创建新 OSD 时使用**的选项。对操作所需时间的评估是决定如何以及何时执行该操作的另一个重要元素。 - 当用户决定执行移除操作时,系统应遵循一个**具有一定智能的安全流程**。根据 OSD 状态(in/out、up/down)和当前情形(我们处于低/高 cpu/网络利用率的时间区间),可能需要做不同的事情: - **Direct removal of the OSD**:不等待,直接执行 OSD 删除操作。 - **Safe OSD Removal**:以最安全的方式移除 OSD。这意味着**等到我们确知该 OSD 不再存储信息**。**当可以安全移除 OSD 时,用户必须收到通知。** - **Scheduled OSD removal**:在将来执行移除。此外,很可能我们只希望在**系统利用率低于某个限制时**才执行移除。 #### 用例 4:替换 OSD **A. 现状:** 与移除 OSD 使用相同的工作流,只是在删除时使用 `"replace"` 参数以保留 OSD id 供将来使用。在 GUI 中 replace 参数表现为一个复选框。 **B. 提议:** 遵循 OSD 移除提议工作流中的指示。 > 引自:[Storage Devices and OSDs Management Workflows](https://docs.ceph.com/en/tentacle/dev/cephadm/design/storage_devices_and_osds/) ### 5.2.5 Cephadm 可扩展性思考(`scalability-notes.rst`) > 原文第一句即声明:**This document does NOT define a specific proposal or some future work. Instead it merely lists a few thoughts that MIGHT be relevant for future cephadm enhancements.**(这不是提案,只是一些可能对未来 cephadm 增强有关的想法。) **现状**: - Cephadm 管理所有已注册的主机。这意味着它**周期性地从每台主机 scrape 数据**,以识别主机上的变化,例如:磁盘添加/移除、守护进程添加/移除、主机网络/防火墙等发生变化。 - 当前,cephadm **每 6 分钟** scrape 每台主机(**最多 10 台并行**),除非手动强制 refresh。 - 对磁盘(ceph-volume)、守护进程(podman/docker)等的 refresh 是**串行**发生的。 - 有了 **cephadm exporter** 后,我们已大幅缩短扫描主机的时间,但问题依然存在:**cephadm-exporter 是否足以解决所有未来的可扩展性问题?** **关于 cephadm-exporter REST API 的考虑**(可能需要缓解的问题): - 有了 cephadm-exporter,我们使用 **SSH 和 HTTP** 两种方式连接每台主机。**有两个截然不同的传输层感觉很奇怪**,我们可能想考虑将其**减少到只有一个协议**。 - 当前把 `bin/cephadm` 投递到主机的方式**不允许使用外部依赖**。这意味着我们被**内置的 HTTP server lib** 卡住了,这对提供良好的开发者体验不利。`bin/cephadm` 需要被打包和分发(无论以何种方式),我们才能使用更好的 http server 库。 **MON 的 config-key store**: 在 `mgr/cephadm` 从每台主机查询元数据之后,cephadm 把数据存储在 **mon 的 k-v store** 中。 **如果允许每台主机把自己的元数据写入该 store,`mgr/cephadm` 就不再需要去收集这些数据了。** 由此产生一些问题: - `mgr/cephadm` 现在需要从 config-key store **查询**数据,而不是依赖缓存的数据; - cephadm 知道三种不同类型的数据:**(1)** 关键数据,需要存在 config-key store 中;**(2)** 可以只保留在内存中的数据;**(3)** 可以存在 RADOS pool 中的数据。**我们如何把这个想法应用到这些不同类型的数据上?** **增大 worker pool size**: - `mgr/cephadm` 当前能**同时 scrape 10 个节点**。 - scrape 单个主机所需的时间不变,我们只是缩短整体执行时间。 - 最好情况下能达到 `O(hosts) + O(daemons)`。 **向后兼容性**: 任何改动都需要**向后兼容**,或**与任何现有功能完全隔离**。外面有正在运行的 cephadm 集群,它们需要升级路径。 > 引自:[Notes and Thoughts on Cephadm's scalability](https://docs.ceph.com/en/tentacle/dev/cephadm/scalability-notes/) > 引自(目录):[CEPHADM Developer Documentation](https://docs.ceph.com/en/tentacle/dev/cephadm/) ## 5.3 RADOS Gateway 开发文档(`doc/dev/radosgw/`) 目录(来自 `radosgw/index.rst`):`usage`、`Admin Ops Nonimplemented (admin/adminops_nonimplemented)`、`s3_compliance`、`bucket_index`。 ### 5.3.1 Rados Bucket Index(`bucket_index.rst`) #### 是什么 RGW 中的 bucket 把它们的**对象列表**存储在一个 **bucket index** 中。每个 index entry 只存储足够的元数据(size、etag、mtime 等),以服务列对象的 API 请求。这些 API 在 S3 中是 `ListObjectsV2` 和 `ListObjectVersions`,在 Swift 中是 `GET Container`。 > 注意:bucket 可以被创建为 **'indexless'**。这样的 bucket 没有 index,**不能被列出**。 #### 一致性保证(Consistency Guarantee) **RGW 保证对象操作上的 read-after-write 一致性。** 这意味着:一旦客户端收到写请求的成功响应,那么该写的效果必须对后续读请求可见。 例如:如果一个 S3 客户端发送 `PutObject` 请求覆盖一个已有对象,随后发送 `GetObject` 请求读回它,RGW **不能**返回前一个对象的内容。它必须以新对象的内容响应,或以某个更晚的对象写/删除的结果响应。 该一致性保证适用于**所有对象写请求**(`PutObject`、`DeleteObject`、`PutObjectAcl` 等)和**所有对象读请求**(`HeadObject`、`GetObject`、`ListObjectsV2` 等)。 #### Rados 对象模型 - S3/Swift 对象(或称「API objects」)作为 rados 对象存储在 **`rgw.buckets.data` 池**中。每个 API object 由一个 **head object** 和**零个或多个 tail objects** 组成。 - **Bucket index 对象存储在 `rgw.buckets.index` 池**中。 - **写对象时,其 head object 最后被写。** 这起到一个原子「**commit**」的作用,使该对象对读请求可见。 #### 分片与重分片(Sharding and Resharding) - 对给定 bucket,index 可以被拆分到若干 rados 对象,称为 **bucket index shards**。 - **为什么需要分片**:在 RADOS 中,**对同一个对象的多次写不能并行执行**。通过把 index 分散到更多 rados 对象上,我们**提高了其写并行度**。 - 对于一次对象上传,对应的 bucket index shard 是基于**对象名的 hash** 选出的。 - **默认 shard 数是 11**,但可在 zonegroup 的 **`bucket_index_max_shards`** 或 ceph.conf 的 **`rgw_override_bucket_index_max_shards`** 中覆盖。 - 随着 bucket 中对象数量增长,其 index shard 数量也会因**动态重分片(dynamic resharding)**而增加。 - 关于 bucket index 对象布局的信息存储在 `RGWBucketInfo` 中,形式是从 `src/rgw/rgw_bucket_layout.h` 来的 `struct rgw::BucketLayout`。重分片逻辑在 `src/rgw/driver/rados/rgw_reshard.cc`。 #### Index Transaction(索引事务) **问题**:为保持 bucket index 一致,所有对象写或删除也必须相应地更新 index。由于 **head objects 与 bucket indices 存储在不同的 rados 对象中**,我们无法用单个 rados 操作原子地更新两者。 **解决方案**:为了满足列操作的 `Consistency Guarantee`,我们必须用一个**三步 bucket index 事务**来协调这两个对象写: 1. **Prepare** 一个事务,写在它的 bucket index 对象上; 2. **写或删除** head object; 3. **Commit** 该事务(写在 bucket index 对象上)(或者如果第 2 步失败则 **cancel** 事务)。 对象写与删除可能彼此竞争,所以**一个给定对象可能同时有多个 prepared transaction**。RGW 认为:如果存在任何未决(outstanding)事务,则该对象条目是 **'pending'**;否则是 **'completed'**。 **代码实现位置**: | 功能 | 位置 | |---|---| | 对象写的事务 | `src/rgw/driver/rados/rgw_rados.cc` 中 `RGWRados::Object::Write::write_meta()` | | 对象删除的事务 | `src/rgw/driver/rados/rgw_rados.cc` 中 `RGWRados::Object::Delete::delete_obj()` | | bucket index 操作 | `src/cls/rgw/cls_rgw.cc` 中 `rgw_bucket_prepare_op()` 和 `rgw_bucket_complete_op()` | #### Listing(列对象) **流程**:列对象时,RGW 会从 bucket index 读取**所有条目**(pending 和 completed)。**对于任何 pending 条目,它必须先检查 head object 是否存在,然后才能把该条目包含在最终列表中。** **崩溃恢复与 'dir suggest'**:如果一个 RGW 在 `Index Transaction` 中间崩溃,一个 index 条目可能卡在这个 'pending' 状态。当 bucket listing 遇到这些 pending 条目时,它**还会把来自 head object 的信息发回 bucket index,以便 bucket index 能更新该条目并解决其过期事务**。这个消息被称为 **'dir suggest'**,因为 bucket index 把它当作一个提示或建议。 **代码实现位置**: | 功能 | 位置 | |---|---| | Bucket listing | `src/rgw/driver/rados/rgw_rados.cc` 中 `RGWRados::Bucket::List::list_objects_ordered()` 和 `list_objects_unordered()` | | 读取 head object 并编码建议的变更 | `RGWRados::check_disk_state()` | | 对应的 bucket index 操作 | `src/cls/rgw/cls_rgw.cc` 中 `rgw_bucket_list()` 和 `rgw_dir_suggest_changes()` | #### S3 对象版本控制(Object Versioning) - 对于**版本化的 bucket**,bucket index 为每个**对象版本**和**删除标记(delete marker)**各包含一个条目。除了按对象名排序 index 条目外,它还必须把同一名字的对象版本**从新到旧**排序。 - RGW 在 `rgw.buckets.data` 池中为每个对象版本存储一个 head object。这个 rados object 的 oid 是**对象名与其 version id 的组合**。 - 在 S3 中,对一个对象名的 GET/HEAD 请求会返回该对象的 **"current" 版本**。为支持这一点,RGW 存储一个额外的 **'object logical head'(olh)对象**,其 oid **只包含对象名**,充当指向其当前版本的 head object 的**间接层**。这个间接逻辑在 `src/rgw/driver/rados/rgw_rados.cc` 中实现为 `RGWRados::follow_olh()`。 - 为维护这个 olh 对象与 bucket index 之间的一致性,index 为每个对象名保留一个单独的 **'olh' 条目**。该条目存储对其所有版本的**写/删除日志**。在 `src/rgw/driver/rados/rgw_rados.cc` 中,`RGWRados::apply_olh_log()` **重放**这个日志,以保证该 olh 对象收敛到与 bucket index 相同的 "current" 版本。 > 引自:[Rados Bucket Index](https://docs.ceph.com/en/tentacle/dev/radosgw/bucket_index/) ### 5.3.2 RGW S3 API 合规性(`s3_compliance.rst`) > ⚠️ **该页顶部自带警告**:`This document is a draft, it might not be accurate`(本文是草案,可能不准确)。而且表中的 Code Links 指向的是**具体历史 commit**(如 `8a2eb18494005aa968b71f18121da8ebab48e950`、`b139a7cd34b4e203ab164ada7a8fa590b50d8b13`),行号早已失效。请把它当作**一份历史遗留的对照清单**,而非权威的当前兼容性矩阵。 #### 代码引用命名规范(BNF) 为便于在代码中引用某个特性,给出如下 BNF 命名定义: ```text name ::= request_type "_" ( header | operation ) ( "_" header_option )? request_type ::= "req" | "res" header ::= string operation ::= method resource method ::= "GET" | "PUT" | "POST" | "DELETE" | "OPTIONS" | "HEAD" resource ::= string header_option ::= string ``` #### 通用请求头(Common Request Headers)支持情况 | Header | Supported? | |---|---| | `Authorization` | Yes | | `Content-Length` | Yes | | `Content-Type` | Yes | | `Content-MD5` | Yes | | `Date` | Yes | | `Expect` | Yes | | `Host` | **?** | | `x-amz-date` | Yes(应优先于 `DATE`) | | `x-amz-security-token` | **No** | #### 通用响应头(Common Response Headers)支持情况 | Header | Supported? | |---|---| | `Content-Length` | Yes | | `Connection` | **?** | | `Date` | **?** | | `ETag` | Yes | | `Server` | **No** | | `x-amz-delete-marker` | **No** | | `x-amz-id-2` | **No** | | `x-amz-request-id` | Yes | | `x-amz-version-id` | **No** | > 💡 补充:表中 `?` 的含义文档未明确定义,推测为「未确认/未验证」。 #### 对 Service 的操作 | Type | Operation | Supported? | |---|---|---| | `GET` | Service | Yes | #### 对 Bucket 的操作 | Type | Operation | Supported? | |---|---|---| | `DELETE` | Bucket | Yes | | `DELETE` | Bucket cors | ? | | `DELETE` | Bucket lifecycle | No | | `DELETE` | Bucket policy | ? | | `DELETE` | Bucket tagging | ? | | `DELETE` | Bucket website | No | | `GET` | Bucket | Yes | | `GET` | Bucket acl | Yes | | `GET` | Bucket cors | ? | | `GET` | Bucket lifecycle | No | | `GET` | Bucket location | No | | `GET` | Bucket policy | ? | | `GET` | Bucket logging | ? | | `GET` | Bucket notification | No | | `GET` | Bucket tagging | No | | `GET` | Bucket Object versions | No | | `GET` | Bucket requestPayment | No | | `GET` | Bucket versioning | No | | `GET` | Bucket website | No | | `GET` | List Multipart uploads | Yes | | `HEAD` | Bucket | Yes | | `PUT` | Bucket | Yes | | `PUT` | Bucket acl | Yes | | `PUT` | Bucket cors | ? | | `PUT` | Bucket lifecycle | No | | `PUT` | Bucket policy | ? | | `PUT` | Bucket logging | ? | | `PUT` | Bucket notification | No | | `PUT` | Bucket tagging | ? | | `PUT` | Bucket requestPayment | No | | `PUT` | Bucket versioning | No | | `PUT` | Bucket website | No | #### 对 Object 的操作 | Type | Operation | Supported? | |---|---|---| | `DELETE` | Object | Yes | | `DELETE` | Multiple objects | Yes | | `GET` | Object | Yes | | `GET` | Object acl | Yes | | `GET` | Object torrent | No | | `HEAD` | Object | Yes | | `OPTIONS` | Object | Yes | | `POST` | Object | Yes | | `POST` | Object restore | ? | | `PUT` | Object | Yes | | `PUT` | Object acl | Yes | | `PUT` | Object copy | Yes | | `PUT` | Initiate multipart upload | Yes | | `PUT` | Upload Part | Yes | | `PUT` | Upload Part copy | ? | | `PUT` | Complete multipart upload | Yes | | `PUT` | Abort multipart upload | Yes | | `PUT` | List parts | Yes | > ⚠️ **重要提示**:上表是文档**草案**中的历史快照。RGW 在后续版本中已大量补全(例如 bucket lifecycle、versioning、tagging、object lock 等早已支持),**请勿以本表判断 Tentacle 的实际 S3 兼容性**。 > 引自:[Rados Gateway S3 API Compliance](https://docs.ceph.com/en/tentacle/dev/radosgw/s3_compliance/) ### 5.3.3 Admin Ops(未实现的 Admin 操作)(`admin/adminops_nonimplemented.rst`) #### 是什么 一份描述 **RGW Admin API** 的文档。关键点: - 一个 admin API 请求发往一个以可配置的 **'admin' resource entry point** 开头的 URI。 - Admin API 的**授权沿用 S3 授权机制**。 - 某些操作要求用户持有**特殊的管理能力(administrative capabilities)**。 - 响应实体类型(XML 或 JSON)可在请求中通过 `'format'` 选项指定,**未指定则默认为 JSON**。 > ⚠️ 文档标题含 "Nonimplemented",但页面正文标题只是 "Admin Operations",且描述的多数操作都给出了完整语法。请谨慎判断哪些是「未实现」的。 #### 各操作一览 | 操作 | 语法 | 请求参数 | 响应实体 | |---|---|---|---| | **Get Object** | `GET /{admin}/bucket?object&format=json HTTP/1.1` | `bucket`(必填)、`object`(必填) | `object`(期望的对象)。注:**不要求 owner 为非 suspended** | | **Head Object** | `HEAD /{admin}/bucket?object HTTP/1.1` | `bucket`(必填)、`object`(必填) | None | | **Get Zone Info** | `GET /{admin}/zone&format=json HTTP/1.1` | 无 | `zone` 容器,含当前集群池配置 | | **Add Placement Pool** | `PUT /{admin}/pool?format=json HTTP/1.1` | `pool`(必填)、`create`(可选,Boolean,默认 `False`,若池不存在则创建) | TBD | | **Remove Placement Pool** | `DELETE /{admin}/pool?format=json HTTP/1.1` | `pool`(必填)、`destroy`(可选,Boolean,默认 `False`,从 active set 移除后销毁该池) | TBD | | **List Available Data Placement Pools** | `GET /{admin}/pool?format=json HTTP/1.1` | 无 | `pools`(当前可用于数据放置的池列表) | | **List Expired Garbage Collection Items** | `GET /{admin}/garbage?format=json HTTP/1.1` | 无 | `garbage` → `object` → `name`、`expired` | | **Manually Processes Garbage Collection Items** | `DELETE /{admin}/garbage?format=json HTTP/1.1` | 无 | `garbage` → `object` → `name`、`expired`(已移除的对象列表) | | **Show Log Objects** | `GET /{admin}/log?format=json HTTP/1.1` | `object`(可选,如 `2012-10-11-09-4165.2-foo_bucket`) | 若未指定 object,返回完整 log objects 列表:`log-objects` → `object`、`log` | #### Get Zone Info 的响应字段 | 字段 | 说明 | Parent | |---|---|---| | `zone` | 包含当前集群池配置 | — | | `domain_root` | root of all buckets | `cluster` | | `control_pool` | (文档未给出描述) | `cluster` | | `gc_pool` | Garbage collection pool | `cluster` | | `log_pool` | Log pool | `cluster` | | `intent_log_pool` | Intent log pool | `cluster` | | `usage_log_pool` | Usage log pool | `cluster` | | `user_keys_pool` | User key pool | `cluster` | | `user_email_pool` | User email pool | `cluster` | | `user_swift_pool` | Pool of swift users | `cluster` | 示例响应: ```json { "domain_root": ".rgw", "control_pool": ".rgw.control", "gc_pool": ".rgw.gc", "log_pool": ".log", "intent_log_pool": ".intent-log", "usage_log_pool": ".usage", "user_keys_pool": ".users", "user_email_pool": ".users.email", "user_swift_pool": ".users.swift", "user_uid_pool ": ".users.uid" } ``` #### 特殊错误响应 - `Get Object` / `Head Object`:`NoSuchObject`(指定的对象不存在,**404 Not Found**) #### 标准错误响应 | 错误 | 说明 | HTTP Code | |---|---|---| | `AccessDenied` | Access denied | 403 Forbidden | | `InternalError` | Internal server error | 500 Internal Server Error | | `NoSuchUser` | User does not exist | 404 Not Found | | `NoSuchBucket` | Bucket does not exist | 404 Not Found | | `NoSuchKey` | No such access key | 404 Not Found | > 引自:[Admin Operations](https://docs.ceph.com/en/tentacle/dev/radosgw/admin/adminops_nonimplemented/) ### 5.3.4 Usage Design Overview(`usage.rst`) 这是一份**关于 RGW usage 统计功能该测什么的清单**,页面正文只有「Testing」一节(标题 "Usage Design Overview" 下没有实际的设计描述)。 #### 当前 usage 测试做的事 先执行这些操作: - 创建几个 bucket - 移除 bucket - 创建一个 bucket - Put object - Remove object 然后测试: 1. 验证带 `delete_obj` 类别的 `usage show` 在**不超过 45 秒**(等待 flush)后**不为空**; 2. 检查以下各项: - **`usage show`** - 不报错 - 条目数 > 0 - summary 条目数 > 0 - 对 categories 中的每个条目,检查 `successful_ops > 0` - 检查 user summary 中的 uid 正确 - **带指定 uid 的 `usage show`(`--uid=`)** - 条目数 > 0 - summary 条目数 > 0 - 对每个 category 条目检查 `successful_ops > 0` - 检查 user summary 中的 uid 正确 - **带指定 uid 和指定 categories 的 `usage show`(`create_bucket`、`put_obj`、`delete_obj`、`delete_bucket`)** - 对每个 category: - 不报错 - 条目数 > 0 - user summary 中的 user 是正确的 user - user summary 下 categories 条目的长度**恰好为 1** - user summary 下 category 的名称正确 - 该 category 的 successful ops > 0 - **带指定 uid 的 `usage trim`** - 不报错 - 检查随后的 `usage show` 显示该用户的 usage info 已被**完全清除** #### 还需要补充的测试 | 测试项 | 内容 | |---|---| | **测试多用户** | 设置多个用户,执行与上面 (2) 相同的检查 | | **测试多 bucket(> 1000 * factor,例如 2000)** | 创建多个 bucket,每个里放对象。统计写入的数据量,并验证 usage 报告显示期望的数值(允许一定的 delta) | | **验证带日期/时间范围的 `usage show`** | 取测试开始和结束的时间戳,把时间戳向下/向上取整到最近的小时。以 end-time 开始列数据,确保**没有**数据显示;以 start-time 结束列数据,确保没有数据显示;以 start-time 开始列数据,确保显示**正确**数据;以 end-time 结束列数据,确保显示正确数据;以 begin-time 开始、end-time 结束列数据,确保显示正确数据 | | **验证带日期/时间范围的 `usage trim`** | 同样取整时间戳。从 end-time 开始 trim,确保**没有**数据被 trim;到 start-time 结束 trim,确保没有数据被 trim;从 begin-time 开始到 end-time 结束 trim,确保**所有**数据都被 trim | > 引自:[Usage Design Overview](https://docs.ceph.com/en/tentacle/dev/radosgw/usage/) > 引自(目录):[RADOS Gateway developer documentation](https://docs.ceph.com/en/tentacle/dev/radosgw/) ## 5.4 Ceph Dashboard 设计目标(`dashboard/ui_goals.rst`) > 文档自带 note:「本文档旨在为讨论 mgr/dashboard 的总体设计原则提供一个焦点」。 ### 引言:为什么需要设计原则 大多数分布式存储架构本质上很复杂,会给通常横跨多个产品和平台学科的运维团队带来管理挑战。一般而言,任何解决方案的复杂度都会**直接影响管理它所产生的运维成本**。答案很简单……**让它变简单**。 本文档旨在强调 Ceph Dashboard 的设计目标,这些目标可能有助于: - 降低复杂度 - 提高生产力 - 改善 time-to-value - 增强可观测性 ### 理解目标用户的画像(Persona) - Ceph 历来是从 CLI 管理的。**CLI 永远提供最丰富、最灵活的安装和管理 Ceph 集群的方式。** 需要并要求这种控制级别的管理员,不太可能为了技术好奇之外的理由去采用 UI。 - 因此,**UI 的相关性对一个「新的 SysAdmin」更为关键**——它可以帮助技术采纳,并减少在实施新解决方案时通常遇到的运维摩擦。 - **理解目标用户画像是设计的基本第一步。** 试图设计一个同时满足「资深 Ceph 管理员/开发者」和「相对新手的 SysAdmin」的 UI,**不太可能让任何一方满意**。 ### 设计原则 **关键原则:** 1. **Clarity and consistency(清晰与一致)**:UI 应确保显示的数据无歧义,且在不同视图间保持一致。 2. **Data timeliness(数据时效性)**:UI 中显示的数据必须是**及时**的。状态信息**必须**相当新,才具有相关性并让人有信心地据此行动。此外,**数据的「年龄」应以 age 形式展示(例如 "20s ago")而不是 UTC 时间戳**,以便管理员能立即消费。 3. **Automate through workflows(通过工作流实现自动化)**:如果管理员必须遵循一个「配方(recipe)」来完成某项任务,那么 dashboard UI 的目标就应该是**把这个流程实现出来**。 4. **Provide a natural next step(提供自然的下一步)**:**UI *就是* 那个「专家系统」**,所以与其期待用户知道下一步去哪,**UI 应该引导他们**。这意味着把组件链接起来以建立流程,并实现 alertmanager 与 dashboard 元素之间更深入的集成,使管理员能高效地从告警走到受影响的组件。 5. **Platform visibility(平台可见性)**:平台(OS 和硬件配置)是解决方案的基础组成部分,因此提供平台级洞察有助于呈现更整体的 Ceph 集群视图。 6. **Jargon Busting(破除行话)**:行话在大多数系统中不可避免。然而,一个好的系统会包含**内联帮助**,以支持 UI 的新用户和不频繁使用者。 **常见陷阱(Common Pitfalls):** - **不要在 UI 中重新实现 CLI 命令。** 系统管理员很可能会在脚本中使用 CLI 原语来自动化任务,所以仅仅「加一个 CLI 功能」会让我们**错失工作流**,并增加复杂度,可能会让 UI「膨胀」。 - **不要像开发者那样思考……试着采用管理员的心态**——他们只是**兼职**与 Ceph 集群打交道,这是当今运维团队的现实。 ### 聚焦用户体验(Focus On User Experience) 最终目标必须是:**不再通过多步工作流(如 iSCSI 配置,或按特定顺序设置特定集群 flag)把复杂性推给 GUI 用户**。简单应该是 UI 的目标……**把复杂性留给 CLI**。 > 引自:[Ceph Dashboard Design Goals](https://docs.ceph.com/en/tentacle/dev/dashboard/ui_goals/) ## 5.5 ceph-volume 开发文档(`doc/dev/ceph-volume/`) 目录(来自 `ceph-volume/index.rst`):`plugins`、`lvm`、`zfs`、`systemd`。 ### 5.5.1 Plugins(插件机制) `ceph-volume` 最初是为支持用 **`lvm`** 作为 OSD 的底层系统而创建的。它作为工具的一部分被包含进来,但**被当作插件对待**。 这种模块化允许其他设备或类设备技术能够消费和复用 `ceph-volume` 提供的实用工具和工作流。 **添加插件**:作为一个 Python 工具,插件使用 **`setuptools` entry points**。要让一个新插件可用,它的 `setup.py` 中应有类似如下的条目: ```python setup( ... entry_points = dict( ceph_volume_handlers = [ 'my_command = my_package.my_module:MyClass', ], ), ``` `MyClass` 应该是一个**接受 `sys.argv` 作为其参数**的类;`ceph-volume` 会在实例化时把它传入,并调用它们的 **`main`** 方法。 一个 `ZFS` 插件的例子长这样: ```python class ZFS(object): help_menu = 'Deploy OSDs with ZFS' _help = """ Use ZFS as the underlying technology for OSDs --verbose Increase the verbosity level """ def __init__(self, argv): self.argv = argv def main(self): parser = argparse.ArgumentParser() args = parser.parse_args(self.argv) ... ``` 其在 `setup.py` 中的 entry point: ```python entry_points = { 'ceph_volume_handlers': [ 'zfs = ceph_volume_zfs.zfs:ZFS', ], }, ``` 安装后,`zfs` 子命令就会被列出并可如下使用: ```bash ceph-volume zfs ``` > 引自:[Plugins](https://docs.ceph.com/en/tentacle/dev/ceph-volume/plugins/) ### 5.5.2 LVM 后端:Tag API 与元数据 `ceph-volume lvm` 的后端是 **LVM**,它**重度依赖 tag**(LVM 允许扩展其卷元数据的一种方式)。这些值之后可以针对设备查询,也是它们之后被发现的途径。 > ⚠️ **这些 API 本意不是公开的**,文档化只是为了说明工具在幕后做了什么。**不要修改这些值中的任何一个。** **Tag API**:把 logical volume 识别为 Ceph 的一部分依赖于在所有卷上**打 tag**。它遵循如下命名空间命名约定: ```text ceph.= ``` 所有 tag 都以 `ceph` 关键字为前缀,以声明对该命名空间的所有权并使其易于识别。下面是 OSD ID 在 lvm tag 语境中的用法: ```text ceph.osd_id=0 ``` **存储在 LVM 卷上的 Ceph OSD 元数据一览:** | Tag | 说明 | 示例 | |---|---|---| | `type` | 描述该设备是 OSD 还是 Journal,并能在支持时扩展到其他类型(例如 lockbox) | `ceph.type=osd` | | `cluster_fsid` | 集群 FSID | `ceph.cluster_fsid=7146B649-AE00-4157-9F5D-1DBFF1D52C26` | | `data_device` | 数据设备路径 | `ceph.data_device=/dev/ceph/data-0` | | `data_uuid` | 数据设备 UUID | `ceph.data_uuid=B76418EB-0024-401C-8955-AE6919D45CC3` | | `journal_device` | journal 设备路径 | `ceph.journal_device=/dev/ceph/journal-0` | | `journal_uuid` | journal 设备 UUID | `ceph.journal_uuid=2070E121-C544-4F40-9571-0B7F35C6CB2B` | | `encrypted` | 是否加密。启用 `luks` 加密时为 `1`;不支持或仅禁用加密时为 `0` | `ceph.encrypted=1` / `ceph.encrypted=0` | | `osd_fsid` | OSD FSID | `ceph.osd_fsid=88ab9018-f84b-4d62-90b4-ce7c076728ff` | | `osd_id` | OSD ID | `ceph.osd_id=1` | | `block_device` | 仅用于 **bluestore** 后端。记录 logical volume 路径 | `ceph.block_device=/dev/mapper/vg-block-0` | | `block_uuid` | 仅用于 **bluestore** 后端。记录 logical volume UUID 或分区 UUID | `ceph.block_uuid=E5F041BB-AAD4-48A8-B3BF-31F7AFD7D73E` | | `db_device` | 仅用于 **bluestore** 后端。记录 logical volume 路径 | `ceph.db_device=/dev/mapper/vg-db-0` | | `db_uuid` | 仅用于 **bluestore** 后端。记录 logical volume UUID 或分区 UUID | `ceph.db_uuid=F9D02CF1-31AB-4910-90A3-6A6302375525` | | `wal_device` | 仅用于 **bluestore** 后端。记录 logical volume 路径 | `ceph.wal_device=/dev/mapper/vg-wal-0` | | `wal_uuid` | 仅用于 **bluestore** 后端。记录 logical volume UUID 或分区 UUID | `ceph.wal_uuid=A58D1C68-0D6E-4CB3-8E99-B261AD47CC39` | | `vdo` | 在 prepare 设备时检测到 VDO-enabled 设备会被记录下来,供 activate 时后续检查。它会通过追加 `discard` mount flag 影响挂载选项,**无论使用何种 mount flags** | `ceph.vdo=1` | > 引自:[LVM](https://docs.ceph.com/en/tentacle/dev/ceph-volume/lvm/) ### 5.5.3 ZFS 后端 `ceph-volume zfs` 的后端是 **ZFS**,同样**重度依赖 tag**。 > ⚠️ **当前该接口只能在 FreeBSD 上使用。** > ⚠️ 同样,这些 API 本意不是公开的,**不要修改这些值**。 **Tag API**:与 LVM 相同的命名约定 `ceph.=`,例如 `ceph.osd_id=0`。 **存储差异**: - filesystem 上的 tag 作为 **property** 存储; - zpool 上的 tag 存储在 **comment property** 中,以 **`;` 分隔的拼接列表**形式。 **元数据字段**(与 LVM 基本一致,仅路径前缀不同,且无 `encrypted` / `vdo`,多一个 `compression`): | Tag | 说明 | 示例 | |---|---|---| | `type` | 设备是 OSD 还是 Journal | `ceph.type=osd` | | `cluster_fsid` | 集群 FSID | `ceph.cluster_fsid=7146B649-AE00-4157-9F5D-1DBFF1D52C26` | | `data_device` | 数据设备路径 | `ceph.data_device=/dev/ceph/data-0` | | `data_uuid` | 数据设备 UUID | `ceph.data_uuid=B76418EB-0024-401C-8955-AE6919D45CC3` | | `journal_device` | journal 设备路径 | `ceph.journal_device=/dev/ceph/journal-0` | | `journal_uuid` | journal 设备 UUID | `ceph.journal_uuid=2070E121-C544-4F40-9571-0B7F35C6CB2B` | | `osd_fsid` | OSD FSID | `ceph.osd_fsid=88ab9018-f84b-4d62-90b4-ce7c076728ff` | | `osd_id` | OSD ID | `ceph.osd_id=1` | | `block_device` | 仅 bluestore。记录 logical volume 路径 | `ceph.block_device=/dev/gpt/block-0` | | `block_uuid` | 仅 bluestore | `ceph.block_uuid=E5F041BB-AAD4-48A8-B3BF-31F7AFD7D73E` | | `db_device` | 仅 bluestore | `ceph.db_device=/dev/gpt/db-0` | | `db_uuid` | 仅 bluestore | `ceph.db_uuid=F9D02CF1-31AB-4910-90A3-6A6302375525` | | `wal_device` | 仅 bluestore | `ceph.wal_device=/dev/gpt/wal-0` | | `wal_uuid` | 仅 bluestore | `ceph.wal_uuid=A58D1C68-0D6E-4CB3-8E99-B261AD47CC39` | | `compression` | 启用压缩的设备。压缩总是可以用 zfs 原生设置在 volume 或 filesystem 上开启,并可在创建 volume/filesystem 时激活。**当由 `ceph-volume zfs` 激活时,这个 tag 会被创建。在 `ceph-volume` 之后手工设置的压缩不会被察觉,除非这个 tag 也被手工设置。** | `ceph.vdo=1` | > 💡 补充:`compression` 一节的说明文字讲的是 compression,但给出的示例却是 `ceph.vdo=1`——**疑为文档复制粘贴 LVM 小节时遗留的错误**,正确示例应为类似 `ceph.compression=1` 的形式。此处按原文照录。 > 引自:[ZFS](https://docs.ceph.com/en/tentacle/dev/ceph-volume/zfs/) ### 5.5.4 systemd 集成 **activate(激活)一个 OSD 的工作流**依赖于 **systemd unit files** 以及它把信息作为 instance name 的**后缀持久化的能力**。 `ceph-volume` 为 unit file 暴露如下约定: ```text ceph-volume@- ``` 例如,为 `:ref:ceph-volume-lvm` 子命令启用一个 OSD 看起来是这样: ```bash systemctl enable ceph-volume@lvm-0-8715BEB4-15C5-49DE-BA6F-401086EC7B41 ``` 这**三部分被持久化的信息**(子命令 + OSD id + OSD FSID)是子命令理解「它需要激活哪个 OSD」所必需的。 由于 `lvm` 不是唯一会被支持的子命令,这也正是未来允许定义其他设备类型的方式。例如未来对于普通磁盘,可能是: ```bash systemctl enable ceph-volume@disk-0-8715BEB4-15C5-49DE-BA6F-401086EC7B41 ``` **启动时**,systemd unit 会执行一个 helper script,它解析后缀并最终**回调 `ceph-volume`**。用前面 lvm 的例子,那个调用看起来是: ```bash ceph-volume lvm activate 0 8715BEB4-15C5-49DE-BA6F-401086EC7B41 ``` > ⚠️ 这些工作流本意不是公开的,文档化只是为说明工具在幕后做了什么。**不要修改这些值。** > 引自:[systemd](https://docs.ceph.com/en/tentacle/dev/ceph-volume/systemd/) > 引自(目录):[ceph-volume developer documentation](https://docs.ceph.com/en/tentacle/dev/ceph-volume/) --- # 六、测试与 CI ## 6.1 Testing notes(`testing.rst`) ### `build-integration-branch` **Setup:** 1. 在 https://github.com/settings/tokens 创建一个 github token,并把它放到 `~/.github_token`。注意只有 `repo` section 下的 **`public_repo`** 需要勾选。 2. 如果你还没有,在 https://github.com/ceph/ceph/labels 创建一个 ceph repo label `wip-yourname-testing`。 3. 创建 `ci` remote: ```bash git remote add ci git@github.com:ceph/ceph-ci ``` **Usage:** 1. 用你的 label(通常是 `wip-yourname-testing`)标记一部分 `needs-qa` commits。 2. 创建 integration branch: ```bash git checkout master git pull ../src/script/build-integration-branch wip-yourname-testing ``` 3. 冒烟测试: ```bash ./run-make-check.sh ``` 4. 推送到 ceph-ci: ```bash git push ci $(git rev-parse --abbrev-ref HEAD) ``` > 引自:[Testing notes](https://docs.ceph.com/en/tentacle/dev/testing/) ## 6.2 持续集成架构(`continuous-integration.rst`) ### 总览 在 Ceph 中,开发依赖**多条 CI 流水线**。这些流水线**大多围绕 Jenkins**,其配置由 **Jenkins Job Builder**(https://docs.openstack.org/infra/jenkins-job-builder/)生成。 ### 以 `make check` 为例:`ceph-pull-requests` `ceph-pull-requests` 是一个 jenkins job,由 GitHub pull request 或一句触发短语触发,例如: ```text jenkins test make check ``` **涉及的各方**(原文档用 graphviz 图表达,此处转为文字与表): ``` github (git_repo | webhooks | api) | | webhooks: notify v jenkins_controller ---- schedule jobs ----> jenkins_agents ^ | | | git pull (from github:git) | | query for chacra repo URL --> shaman | | pull build dependencies --> chacra | | pull build dependencies --> package_repos | | pull Python packages --> pypi | | pull JavaScript packages --> npm | | pull build dependencies --> apt_mirror | | +-------------------- update ----------------+ (github:api) Sepia Lab 集群内包含:apt_mirror、shaman、chacra、jenkins(controller + agents) ``` | 角色 | 说明 | |---|---| | **Sepia Lab** | Ceph 项目使用的测试实验室,提供 CI 基础设施所需的存储与计算资源(https://wiki.sepia.ceph.com/doku.php) | | **Jenkins agents** | 执行 CI 作业的一组机器。在本例中它们:1)从 GitHub pull git repo;2)把 pull request **rebase 到最新 master**;3)设置必要的环境变量;4)运行 `run-make-check.sh` | | **Chacra** | 提供 RESTful API 的服务器,允许客户端**存储和检索二进制包**,并自动为上传的包创建 repo。一旦在 chacra 上创建了某个 repo,配置好的 shaman 服务器也会被更新,然后我们就可以向 shaman 查询对应的 repo 地址。Chacra 不仅托管 Ceph 包,还托管相当多其他包,如各种构建依赖(https://github.com/ceph/chacra) | | **Shaman** | 提供 RESTful API 的服务器,允许客户端查询由 chacra 节点托管的 repo 信息。Shaman 也以其 Web UI(https://shaman.ceph.com)闻名。但请注意,**shaman 不构建包,它只提供构建的信息** | **Chacra / Shaman / chacractl 的关系:** ``` shaman -> postgresql (database) chacra -> postgresql chacractl -> chacra chacra -> libboost, libzbd, ... (各项目 repo) ``` ### 构建依赖(build dependencies) Ceph 有 build-time 和 run-time 依赖。多数时候我们倾向于使用发行版预构建的包。但有些情况下: - 发行版中缺少必需的依赖; - 它们的版本太老; - 它们打包时没有启用某些重要特性; - 我们希望确保某个运行时依赖的版本**与我们在实验室中测试过的完全一致**。 无论原因为何,我们要么需要从源码构建它们,要么把它们打包成二进制包而不使用发行版提供的版本。**相当多的 build-time 依赖以 git submodule 形式包含**,但为了避免反复重建这些依赖,我们**预构建了其中一部分并上传到我们自己的 repos**。因此在执行 `make check` 时,我们 CI 中的构建主机只需从托管这些包的内部 repos 拉取,而不用构建它们。 目前已为 **ubuntu focal** 预构建并上传到 chacra 的包: | 包 | 说明 | |---|---| | **libboost** | 打包 `boost`。包名从 `libboost-*` 改为 **`ceph-libboost-*`**,并且被安装到 **`/opt/ceph`**,以免与发行版自带的官方 `libboost` 包冲突。构建脚本在 https://github.com/ceph/ceph-boost (版本号 bump 示例见其 commit `2a8ae02932b2a1fd6a68072da8ca0df2b99b805c`)。在 vanilla Ubuntu Focal 上构建 1.79 的命令见下 | | **libzbd** | 打包 `libzbd`(https://github.com/westerndigitalcorporation/libzbd)。上游 libzbd 已包含 debian packaging | | **libpmem** | 打包 `pmdk`(https://github.com/pmem/pmdk)。注意 `ndctl` 是 pmdk 的构建依赖之一,更新的 debian packaging 见 https://github.com/ceph/ceph-ndctl | 构建 libboost 1.79 的命令: ```bash sudo apt install debhelper dctrl-tools chrpath libbz2-dev libicu-dev bison \ flex docbook-to-man help2man xsltproc doxygen dh-python python3-all-dev graphviz wget http://download.ceph.com/qa/boost_1_79_0.tar.bz2 git clone https://github.com/ceph/ceph-boost tar xjf boost_1_79_0.tar.bz2 cp -ra ceph-boost/debian boost_1_79_0/ pushd boost_1_79_0 export DEB_BUILD_OPTIONS='parallel=6 nodoc' dpkg-buildpackage -us -uc -b popd BOOST_SHA=$(git ls-remote https://github.com/ceph/ceph-boost main | awk '{ print $1 }') ls *.deb | chacractl binary create \ libboost/master/$BOOST_SHA/ubuntu/focal/amd64/flavors/default ``` > ⚠️ **重要**:在更新/升级 packaging 时,请确保**包的版本号和 packaging 的 release number 被正确更新**,否则将难以分辨安装了哪个版本的包。我们在 `install-deps.sh` 中会在尝试升级之前检查包的版本。 **JavaScript 依赖的处理**:此外,`ceph-mgr-dashboard` 的 frontend 使用大量 JavaScript 包,其中很多发行版没有打包,更不用说测试这些包不同版本组合的麻烦。因此我们决定**用 `make-dist` 把这些 JavaScript 包包含进我们的 dist tarball**。 **下游再分发的考虑**:由于我们的下游在再分发预编译的 Ceph 包时可能不想使用预打包的二进制,我们还需要把这些库包含进 dist tarball。它们是: - boost - liburing - pmdk `make-dist` 是我们的 CI 流水线用来创建 dist tarball 的脚本,以便该 tarball 能在干净室环境(clean room environment)中用于构建 Ceph 包。当我们需要升级这些第三方库时,应: 1. 更新 CMake 脚本; 2. 重建预构建包; 3. 更新这个脚本以反映变更。 ### 上传依赖(Uploading Dependencies) 为确保预构建包对 jenkins agents 可用,我们需要把它们上传到 `apt-mirror.front.sepia.ceph.com` 或 chacra。上传到前者需要实验室管理员的帮助,所以如果我们想定期维护包仓库,更好的选择是用 **`chacractl`** 管理它们。 Chacra 用如下资源层次表示包仓库: ```text ///// ``` | 层次 | 说明 | |---|---| | `project` | 通常用于表示一组相关的包。例如 `libboost` | | `branch` | 项目的分支。这映射了 git repo 的概念 | | `ref` | 给定版本的一组包的唯一 id,用于在 `/` 下引用这组包。**最佳实践是对打包配方(如构建 DEB 的 `debian` 目录、构建 RPM 的 `spec`)做版本化,并用打包配方的 SHA1 作为 `ref`**。但你也可以使用随机字符串,比如所构建源码树的 tag name | | `distro` | 为哪个发行版构建。当前支持:`centos`、`debian`、`fedora`、`rhel`、`ubuntu` | | `distro-version` | 发行版版本。例如 ubuntu focal 上是 `20.04` | | `arch` | 包的架构:`arm64`、`amd64`、`noarch` | 示例: ```bash ls *.deb | chacractl binary create \ libboost/master/099c0fd56b4a54457e288a2eff8fffdc0d416f7a/ubuntu/focal/amd64/flavors/default ``` (`chacra`: https://github.com/ceph/chacra ;`chacractl`: https://github.com/ceph/chacractl) ### 更新 `install-deps.sh` 我们还需要更新 `install-deps.sh`,把构建脚本指向新的 repo。详见 https://github.com/ceph/ceph/blob/master/install-deps.sh 。 > 引自:[Continuous Integration Architecture](https://docs.ceph.com/en/tentacle/dev/continuous-integration/) ## 6.3 Sepia 社区测试实验室(`sepia.rst`) Ceph 社区维护了一个**对 Ceph 项目活跃贡献者开放**的测试实验室。详见 Sepia wiki:https://wiki.sepia.ceph.com/doku.php > ⚠️ 该页极短(只有两句话),所有实质内容都在 wiki 上(需访问权限)。本册无法展开。 > 引自:[Sepia community test lab](https://docs.ceph.com/en/tentacle/dev/sepia/) ## 6.4 关于 `corpus.rst` 本册**未读取** `doc/dev/corpus.rst`,因此不展开。仅在 `release-checklists.rst` 的「First stable release」检查表中出现过对它的引用:「generate new object corpus for encoding/decoding tests - see corpus」。 --- # 七、文档与发布工程 ## 7.1 开发工作流(`development-workflow.rst`) > ⚠️ **该页严重过时**:文中的发布周期图仍以 `hammer` / `infernalis` 和 **2015** 为例;角色清单仍写着「Sage Weil 是 Ceph lead」「Loic Dachary 是 stable point release 的 release master」「Yuri Weinstein 是 quality engineer」「Alfredo Deza 是 publisher」。请**只把本页当作工作流思想的参考,不要当作现状**。 ### 本页的定位 本页解释开发者为实现 Ceph 发布周期中的各项目标而应遵循的工作流。**它不涉及技术细节,旨在提供一个高层视图。** 每一章讲一个既定目标,如「合并 bug 修复或特性」或「发布 point release 与 backport」。 ### 一个关键特性:工作流之间不互相阻塞 所有工作流的一个关键方面是:**它们之间没有任何一个是阻塞另一个的**。例如,在下一个 point release 正在发布的同时,一个 bug 修复可以被 backport 并合并到 stable 分支。要让这个具体例子成立,本应创建一个分支以避免任何干扰。**但实践中对 Ceph 来说并不必要**,因为: - 参与的人很少; - backport 的频率不算太高; - 知道某个 release 正在发布的 reviewer,不太可能合并任何可能引发问题的东西。 这种**临时(ad-hoc)方式意味着工作流会被定期调整以适应**。例如,`quality engineers` 并未参与发布 `dumpling` point release 的工作流;而 backport 到 `firefly` 的 commit 数量之多,使得让负责写代码或修 bug 的开发者同时去跑并验证完整的集成测试套件变得不切实际。**引入 `quality engineers` 使得有人可以通过分析测试结果参与工作流。** 工作流在**施加无意义的开销时不会被执行**。例如,如果某个 point release 的 release notes 在检查完所有集成测试之前还没写好,它们可以被提交到 stable 分支,并直接送去发布,而不必再跑一轮集成测试。 ### 发布周期(Release Cycle) ``` Ceph hammer infernalis Developer CDS CDS Summit | | | | development | | release | v0.88 v0.89 v0.90 ... | v9.0.0 --v--^----^--v---^------^--v- ---v----^----^--- 2015 | | | | stable giant | | hammer release v0.87 | | v0.94 | | point firefly dumpling release v0.80.8 v0.67.12 ``` - 每年**四次**,开发路线图在 **Ceph Developer Summit**(http://tracker.ceph.com/projects/ceph/wiki/Planning#Ceph-Developer-Summit)上在线讨论。 - 一个新的 stable release(hammer、infernalis、jewel ...)以**相同的频率**发布。 - **每隔一个 release**(firefly、hammer、jewel...)是一个 **Long Term Stable (LTS)** 版本。 ### 合并 bug 修复或特性 开发分支是 `master`,所有开发者遵循的工作流可概括为: 1. 开发者准备一系列 commit; 2. 开发者通过 pull request 提交这一系列 commit; 3. 一个 reviewer 被分配该 pull request; 4. 当 reviewer 认为该 pull request 没问题,它由 tester 合并到一个 integration branch; 5. 在集成测试成功运行后,该 pull request 由 tester 合并。 **角色与节奏约定**: | 角色 | 职责 | 超时约定 | |---|---|---| | `developer` | 一系列 commit 的作者 | — | | `reviewer` | 负责定期向开发者提供反馈 | **一周**后若无动静,开发者被邀请 ping reviewer | | `tester` | 负责在该 pull request 上运行 teuthology 集成测试 | **一个月**内若无动静,reviewer 被邀请 ping tester | ### 解决 bug 报告与实现特性 所有 bug 报告和特性请求都在 issue tracker(http://tracker.ceph.com)中,工作流可概括为: 1. reporter 创建 issue,优先级 `Normal`; 2. 开发者可以立即认领该 issue; 3. 在**每两周一次的 bug scrub** 中,团队过一遍所有新 issue 并赋予优先级; 4. 高优先级的 bug 先处理。 每个 `team` 负责一个项目,由 `leads` 管理。 被分配到 issue 的 `developer` 对它负责。一个 open issue 的状态可以是: | 状态 | 含义 | |---|---| | `New` | 不清楚该 issue 是否需要工作 | | `Verified` | 该 bug 可被复现,或已多次出现 | | `In Progress` | 开发者本周正在处理它 | | `Pending Backport` | 该修复需要 backport 到 backport 字段中列出的 stable release | 对于每个 `Pending Backport` issue,在 `Backport` tracker 中**至少存在一个 issue**,用于记录从 master 分支 cherry-pick 必要 commit 到目标 stable 分支所做的工作。详见 backporter 手册:https://github.com/ceph/ceph/blob/main/SubmittingPatches-backports.rst ### 运行与解读 teuthology 集成测试 Sepia 实验室**定期运行 teuthology 集成测试**,结果发布在 pulpito(http://pulpito.ceph.com/)和 ceph-qa 邮件列表(https://ceph.com/irc/)。 - job 失败由 **quality engineers 和 developers 分析**; - 如果原因是**环境性的**(例如网络连接问题),在 sepia lab 项目中创建一个 issue; - 如果该 bug **已知**,把失败 job 的 pulpito URL 加到该 issue; - 如果该 bug **是新的**,创建一个 issue。 `quality engineer` 要么是开发者,要么是 QE 团队成员。**每个项目至少有一个集成测试套件**: - `rgw` suite - `CephFS` suite - `rados` suite - `rbd` suite 以及许多其他,如: - `upgrade` suites - `power-cycle` suite - ... ### 准备一个新 release 一个 release 在一个**专用分支**中准备,该分支不同于 `master`: - 对于 stable release,是与 release code name 匹配的分支(dumpling、firefly 等); - 对于 development release,是 `next` 分支。 为稳定 release candidate,所有开发者应遵循的工作流与正常开发工作流相同,区别在于: - pull request 必须**指向 stable 分支或 next**,而不是 master; - reviewer **拒绝**非 bug 修复的 pull request; - 与 teuthology 测试失败相匹配、且优先级设为 **`Urgent`** 的 `Backport` issue **必须在发布前修复**。 ### Cutting a new stable release(发布新的 stable release) **可以发布的条件**: - 所有优先级为 `Urgent` 的 `Backport` issue 都已修复; - 集成测试和升级测试运行成功。 **风险权衡**(这段是全页最有价值的部分之一):发布一个新的 stable release 意味着**存在回归或在升级过程中发现新 bug 的风险**,无论测试多么仔细。决定是否发布必须考虑到这一点——**发布一个只修复了几个小 bug 的 stable release 可能并不明智**。例如,如果只有一个 commit 被 backport 到一个非 LTS 的 stable release,最好等到有更多了再发。 当一个 stable release 即将退役时,**推荐用户升级到下一个 LTS release,可能比提议一个新的 point release 来修复问题更安全**。举例:`dumpling` v0.67.11 有与 backfilling 相关的 bug,这些 bug 已在 `firefly` v0.80.x 中修复。修复这些 backfilling bug 的 backport 已在 draft point release `dumpling` v0.67.12 中测试,但它们足够大,会带来回归风险。由于 `dumpling` 即将退役,遭受该 bug 的用户可以升级到 `firefly` 来修复它。**除非用户明确要求 `dumpling` v0.67.12,否则这个 draft release 可能永远不会发布。** **发布流程与角色**: 1. `Ceph lead` 决定必须发布一个新的 stable release; 2. `release master` 获得所有 leads 的批准; 3. `release master` 编写并提交 release notes; 4. `release master` 通知 `quality engineer` 该分支已准备好测试; 5. `quality engineer` 运行额外的集成测试; 6. 如果 `quality engineer` 发现需要 `Urgent Backport` 的新 bug,该 release **回到准备状态**——它终究还没准备好; 7. `quality engineer` 通知 `publisher` 该分支已可发布; 8. `publisher` **创建包并设置 release tag**。 ### Cutting a new development release development release 的发布工作流与「准备新 release 并发布」相同,区别在于: - `next` 分支在发布后**被重置到 `master` 的 tip**; - **`quality engineer` 不需要运行额外测试**,`release master` 直接通知 `publisher` 该 release 已可发布。 ### 发布 point release 与 backporting point release 的发布工作流与「准备新 release 并发布」相同,区别在于: - 每个 issue 的 `backport` 字段包含 stable release 的 code name; - 对于该 issue 被 backport 到的每个 stable release,在 `Backport` tracker 中**恰好有一个** issue; - 所有 commit 用 **`git cherry-pick -x`** cherry-pick,以引用原始 commit。 > 注意:**如果 backport 是合适的,提交者负责确定必须 backport 到的目标 stable 分支。** > 引自:[Development workflows](https://docs.ceph.com/en/tentacle/dev/development-workflow/) ## 7.2 Ceph Release Process(`release-process.rst`) > 📌 该页**较新**,示例使用 **Squid 19.2.2**(即 Tentacle 的前一个版本),可视为当前流程。 ### 前提条件 **Signing Machine(签名机)**: - 是 **Sepia lab** 中的一台虚拟机(https://wiki.sepia.ceph.com/doku.php?id=start)。 - 对签名机的 SSH 访问**仅限于**通常的基础设施管理员以及少数几个组件 lead(如 nfs-ganesha、ceph-iscsi)。 - 机器上的 `ubuntu` 用户有一些用于 pull、push 和签名包的构建脚本(https://github.com/ceph/ceph-build/tree/main/scripts)。 - **GPG 签名密钥永久存放在一个 `Nitrokey Pro` 上,并通过 RHV 透传给 VM。这有助于确保密钥无法被导出或以任何方式离开数据中心。** **New Major Releases(每个新的 major/字母序 release)**: - 必须为每个 RPM repo 创建一个 `ceph-release` RPM(例如 el8 一个、el9 一个)。 - `chacra`(https://github.com/ceph/chacra)是我们用来存储 DEB 和 RPM repo 的 python 服务。chacra repos 被配置为包含这个 `ceph-release` RPM,但它**必须单独构建**。你必须确保 chacra 被正确配置为为每个特定 release 包含这个 RPM。 1. 更新 chacra 使其知晓新的 Ceph release(示例 PR:https://github.com/ceph/chacra/pull/219); 2. 重新部署 chacra(例如 `ansible-playbook chacra.ceph.com.yml`); 3. 运行 https://jenkins.ceph.com/view/all/job/ceph-release-rpm/ 。 ### 构建流程总览(10 步) 1. QE 完成测试并找到一个停止点。该 commit 被推送到 ceph.git 中的 **`$release-release` 分支**(例如 `squid-release`)。**这使得工作可以在 `$release` 分支中继续,而不必在发布过程中冻结它。** 2. Ceph Council 批准并通知「**Build Lead**」。 3. Build Lead 启动 **Jenkins multijob**(https://jenkins.ceph.com/view/all/job/ceph),触发所有构建。 4. 包被推送到 **chacra.ceph.com**。 5. 包从 chacra.ceph.com 拉到 **Signer VM**。 6. 包被**签名**。 7. 包被推送到 **download.ceph.com 上的 prerelease 区域**。 8. **Prerelease 容器**被构建并推送到 **quay.ceph.io**。 9. 对 prerelease 包和容器进行**最终测试与验证**。 10. Prerelease 包和容器被**提升**为 download.ceph.com 和 quay.io 上的正式 release。 ### Hotfix Release Process Deviation(热修复流程的偏离) 1. Check out 最近的 tag。例如如果我们要在 19.2.1 之上发一个 hotfix:`git checkout -f -B squid-release tags/v19.2.1` 2. `git cherry-pick -x` 必要的 hotfix commits(**注意:只能使用 "cherry-pick"**) 3. `git push -f origin squid-release` 4. 验证 `$release-release` 分支中的 commits: 1. 要与**上一个 point release**(如果我们在做 19.2.2,那就是 19.2.1)对比,运行: `git log --pretty=oneline --no-merges tags/v19.2.1..origin/squid-release` 验证产生的 commits 恰好就是我们想要进入下一个 point release 的内容。 2. 要与 "ceph-ci" repo 中的 RC(本例中是 `ceph-ci`)对比,运行: `git log --pretty=oneline --no-merges origin/squid-release...ceph-ci/squid-release` **如果 ceph repo 中的 `$release-release` 分支与 `ceph-ci` 中的 RC 完全相同,则不应有输出。** 注意这里使用的是 git **triple dot notation**,它显示两个引用之间的任何 commit 差异。 5. 通知 Build Lead 开始构建。 6. Build Lead 应设置 **`RELEASE_TYPE=HOTFIX`** 而不是 `STABLE`。 ### Security Release Process Deviation(安全发布流程的偏离) 安全/CVE 发布与 hotfix 类似,但有**两点不同**: 1. 修复应推送到 **`ceph-private`** repo(https://github.com/ceph/ceph-private)而不是 ceph.git(**需要 GitHub Admin Role**)。 2. tag(例如 `v19.2.3`)必须由 **Build Lead 手工推送**到 ceph.git。 具体步骤: 1. Check out 最近的 tag。例如如果我们要在 19.2.2 之上发安全修复:`git checkout -f -B squid-release origin/v19.2.2` 2. `git cherry-pick -x` 必要的安全修复 commits 3. `git remote add security git@github.com:ceph/ceph-private.git` 4. `git push -f security squid-release` 5. 通知 Build Lead 开始构建。 6. Build Lead 应设置 **`RELEASE_TYPE=SECURITY`** 而不是 `STABLE`。 7. 最后,**`ceph-tag` 步骤需要由 Build Lead 在尽可能接近公告时间手工运行**: ```bash # 示例:squid,假设 19.2.3 是安全发布版本 # 添加 ceph-releases repo(也需要 GitHub Admin Role)。`ceph-setup` job 已经创建并推送了 tag 到 ceph-releases.git git remote add releases git@github.com:ceph/ceph-releases.git git fetch --all # Check out the version commit git checkout -f -B squid-release releases/squid-release git push -f origin squid-release git push origin v19.2.3 # 现在创建一个 squid-release 针对 squid 的 Pull Request,把 version commit 和安全修复合并回 squid 分支 ``` ### 1. 准备 release 分支 一旦 QE 在工作分支(例如 `squid`)中确定了停止点,该 commit 应被推送到对应的 `squid-release` 分支。然后通知 Build Lead 该 release 分支已就绪。 ### 2. 启动构建 (以 Squid 19.2.2 这个 stable/regular release 为例) 1. 浏览 https://jenkins.ceph.com/view/all/job/ceph/build?delay=0sec 2. 用 GitHub OAuth 登录 3. 设置必要参数: ```text BRANCH=squid TAG=checked VERSION=19.2.2 RELEASE_TYPE=STABLE ARCHS=x86_64 arm64 ``` > ⚠️ **注意**:如果由于某种原因构建必须重启(例如某个 distro 失败了),那么 **`TAG` 选项必须取消勾选**。 4. 使用 https://docs.ceph.com/en/latest/start/os-recommendations/?highlight=debian#platforms 来确定 `DISTROS` 参数。例如: | Release | Distro Codemap | |---|---| | pacific (16.X.X) | `focal bionic buster bullseye` | | quincy (17.X.X) | `jammy focal centos9 bullseye` | | reef (18.X.X) | `jammy focal centos9 windows bookworm` | | squid (19.X.X) | `jammy centos9 windows bookworm` | 5. 点击 `Build`。 ### 3. Release Notes 构建包需要数小时。**利用这几个小时来创建 Release Notes 和公告**: 1. ceph.git Release Notes(例如 v19.2.2 的 ceph.git / docs.ceph.com PR) 2. ceph.io Release Notes(例如 v19.2.2 的 ceph.io.git / www.ceph.io PR) 3. 邮件公告 参见 Ceph Tracker wiki 中关于如何撰写 release notes 的说明。 ### 4. 签名与发布构建 **步骤:** 1. 从 build job 或由 `ceph-setup` job 创建的 `sha1` 文件中获取 version commit 的 sha1。 2. 从 chacra.ceph.com 下载包到签名虚拟机。这些包被下载到 `/opt/repos`,那里挂载着 Sepia Lab Long Running (Ceph) Cluster。 > 注意:这一步还会通过 ssh 到 download.ceph.com 并运行 `/home/signer/bin/get-tarballs.sh`,把源码 tarball 从 chacra.ceph.com 直接传送到 download.ceph.com。 ```bash ssh ubuntu@signer.front.sepia.ceph.com sync-pull ceph [pacific|quincy|etc] ``` 示例输出(节选): ```text $ sync-pull ceph squid 0eceb0defba60152a8182f7bd87d164b639885b8 sync for: ceph squid **************************************** + : 0eceb0defba60152a8182f7bd87d164b639885b8 + project=ceph + release=squid + sha1=0eceb0defba60152a8182f7bd87d164b639885b8 ... + for combo in debian/bookworm debian/bullseye ubuntu/bionic ubuntu/focal ubuntu/jammy ++ curl -fs https://chacra.ceph.com/r/ceph/squid/0eceb0defba60152a8182f7bd87d164b639885b8/debian/bookworm/flavors/default/pool/main/c/ceph/ + combo_count=161 ... ``` 3. 签名 DEBs: ```bash merfi gpg /opt/repos/ceph/squid-19.2.2/debian/ ``` 示例输出(节选): ```text --> Starting path collection, looking for files to sign --> 1 repos found --> signing: /opt/repos/ceph/squid-19.2.2/debian/jessie/dists/bookworm/Release --> Running command: gpg --batch --yes --armor --detach-sig --output Release.gpg Release --> Running command: gpg --batch --yes --clearsign --output InRelease Release ... ``` 4. 签名 RPMs: ```bash sign-rpms ceph squid ``` 示例输出(节选): ```text $ sign-rpms ceph squid + project=ceph + releases=("$@") + distros=(centos rhel) + distro_versions=(7 8 9) + read -s -p 'Key Passphrase: ' GPG_PASSPHRASE Key Passphrase: + for release in "${releases[@]}" + for distro in "${distros[@]}" + for distro_version in "${distro_versions[@]}" ... + echo 'Checking packages in: /opt/repos/ceph/squid-19.1.0/centos/9' Checking packages in: /opt/repos/ceph/squid-19.1.0/centos/9 ... ``` 5. 发布包到 download.ceph.com: ```bash sync-push ceph squid-19.2.2 2 ``` 这会把包和 tarball 留在 https://download.ceph.com/prerelease/ceph 这个**受密码保护**的 prerelease 区域。从那里验证它们。**当完成并准备好发布时**,登录 download.ceph.com,把目录和 tarball 从 prerelease 家目录(`/data/download.ceph.com/www/prerelease/ceph`)**移动**到 release 目录(`/data/download.ceph.com/www`)。 ### 5. 构建容器 **为什么不能像 CI 构建那样直接构建容器**:与 CI 构建不同——CI 构建能访问正确形式的包——**release 构建不能,因为构建过程不对包签名。因此 release 构建不构建容器。** 这必须在「签名与发布构建」**之后**完成。 有一个名为 **`ceph-release-containers`** 的 Jenkins job,让我们能在发布前测试镜像。这个 job 的存在既是为了方便,也是因为它需要同时访问 x86_64 和 arm64 builder。在 Jenkins 服务器上以 "Build with Parameters" 启动该 job,设置 `BRANCH`、`SHA1` 和 `VERSION` 字段,其他字段保留默认。该 job: - 构建**架构特定的**容器镜像,并推送到 `quay.ceph.io/ceph/prerelease-amd64` 和 `quay.ceph.io/ceph/prerelease-arm64`; - 把架构特定的镜像**融合(fuse)**成一个 "manifest-list" 或 "fat" 容器镜像,并推送到 `quay.ceph.io/ceph/prerelease`。 最后,当对容器镜像完成了所有适当的测试与验证后,从 Ceph 源码树(`container/make-manifest-list.py`)运行: ```bash cd /src/container ./make-manifest-list.py --promote ``` 把它提升到 `quay.io/ceph/ceph` 上的最终 release 位置(**你必须确保已用适当权限登录了 `quay.io/ceph` 和 `quay.ceph.io/ceph`**)。 > ⚠️ `--promote` 步骤**只应作为发布容器的最后一步**执行,且必须在容器镜像已被测试并确认良好之后。 ### 6. 宣布 Release **Version Commit PR**:`ceph-tag` Jenkins job 会在 ceph.git 中创建一个针对 release 分支的 Pull Request。**如果这是一个常规 release(不是 hotfix 或 security release),该 PR 中唯一的 commit 应该就是 version commit。** 请求 review 然后合并该 PR。 **Announcing(宣布)**:**在通过邮件宣布 release 之前,先在 ceph.io 上发布 Release Notes**,因为邮件公告会引用 ceph.io 的博客文章。 > 引自:[Ceph Release Process](https://docs.ceph.com/en/tentacle/dev/release-process/) ## 7.3 Release checklists(`release-checklists.rst`) 这是一份**可勾选的检查清单**(原文中 `[x]` 表示已完成的范式项,`[ ]` 表示需要人工执行的具体动作)。下面 X = 我们刚开始开发的那个 release,X-1 = 刚发布的那个,X-2、X-3 依此类推。 > 📌 该页是**当前版本**的:其中出现了 `update qa/ upgrade suites require-osd-release calls to **tentacle**`,说明它已更新到 Tentacle 周期。 ### Dev Kickoff(新 major release 开始时) 这些步骤应在**开始一个新的 major release 时**执行,也就是在上一个 release 刚被 tag(vX.2.0)且该 tag 已合并回 master 之后。 #### Versions and tags - [x] 更新 `CMakeLists.txt` 的 `VERSION`(就在最顶部,改为 X.0.0) - [x] 更新 `src/librbd/CMakeLists.txt` 的 `VERSION`(底部的 librbd target,改为 1.X.0) - [x] 更新 `src/ceph_release`,填入新的 release name、number 和 type(`'dev'`) - [ ] 初始 tag `vX.0.0`(以便我们能区分于(并排序在)backport 的 (X-1).2.Z 版本之后) **关于打 tag 的说明:** - Tag **必须是 annotated 的**,因为 CMake 通过调用 `git describe --always` 来确定 `CEPH_GIT_NICE_VER`。 - `vX.0.0` 是特殊的,它们**手工推送**(不像由 Jenkins 处理的 `vX.2.n`)。 - `vX.0.0` 应指向 **kickoff 分支中第一个 commit 之前**的某个 commit。 #### Define release names and constants 确保 X(理想情况下还有 X+1)被定义: - [x] `src/common/ceph_releases.h`(`ceph_release_t`) - [x] `src/common/ceph_strings.cc`(`ceph_release_name()`) - [x] `src/include/rados.h`(`CEPH_RELEASE_*` 和 `MAX`) - [x] `src/include/rbd/librbd.h`(`LIBRBD_VER_MINOR` 改为 X) - [x] `src/mon/mon_types.h`(`ceph::features::mon::FEATURE_*` 及相关结构体与 helper) - [x] `src/mds/cephfs_features.h`(`CEPHFS_CURRENT_RELEASE`) **Scripts:** - [x] `src/script/backport-resolve-issue`(`releases()`、`ver_to_release()`) - [x] `src/script/backport-create-issue`(`releases()`) - [x] `src/script/ceph-release-notes`(up to X) **Misc:** - [x] 更新 `src/ceph-volume/ceph_volume/__init__.py`(`__release__`) - [x] 更新 `src/cephadm/cephadmlib/constants.py`(`DEFAULT_IMAGE_RELEASE` 和 `LATEST_STABLE_RELEASE` 改为 X) #### Feature bits - [x] 确保 `SERVER_X` 被定义 - [x] 把在 release X-3 中 `DEPRECATED` 的任何特性改为 `RETIRED` - [ ] 寻找那些 (1) 在 X-2 中存在且 (2) **没有客户端依赖**的特性,并把它们标记为「as of X」的 `DEPRECATED` #### Compatsets - [x] `mon/Monitor.h`(`CEPH_MON_FEATURE_INCOMPAT_X`) - [x] `mon/Monitor.cc`(包含在 `get_supported_features()` 中) - [x] `mon/Monitor.cc`(`apply_monmap_to_compatset_features()`) - [x] `mon/Monitor.cc`(`calc_quorum_requirements()`) - [x] `test/cli/monmaptool/feature-set-unset-list.t`(`supported`、`persistent`) #### Mon - [x] `src/tools/monmaptool.cc`:为创建的(新)集群把 `min_mon_release` bump 到 X - [x] `src/test/cli/monmaptool/8.t`:更新 monmaptool 的输出到 X - [x] `qa/standalone/mon/misc` 调整 `TEST_mon_features`(添加 X 的用例并调整 `--mon-debug-no-require-X`) - [x] `qa/standalone/mon/misc` 把 `jqfilter='.monmap.features.persistent | length == N'` bump 到 `N+1` - [x] `mon/MgrMonitor.cc` 调整 `always_on_modules` - [x] `common/options/global.yaml.in` 定义 `mon_debug_no_require_X` - [x] `common/options/global.yaml.in` 移除 `mon_debug_no_require_X-2` - [x] `mon/OSDMonitor.cc` `create_initial`:调整新的 `require_osd_release`,并添加关联的 `mon_debug_no_require_X` - [x] `mon/OSDMonitor.cc` `preprocess_boot`:调整 "disallow boot of " 条件,以在 `require_osd_release` < X-2 时禁止 X - [x] `mon/OSDMonitor.cc`:调整 "osd require-osd-release" 以 (1) 允许设置 X,(2) 检查所有 mon **和** OSD 都有 X - [x] `mon/MonCommands.h`:调整 "osd require-osd-release" 允许的选项以包含 X - [x] `qa/workunits/cephtool/test.sh`:调整 `require-osd-release` 测试 #### OSDMap - [x] `src/osd/OSDMap.cc` 在 `pending_require_osd_release()` 中为 `SERVER_X` 添加 release name 映射 - [x] `OSDMap::get_min_compat_client`:由 core team 评估 #### Code cleanup - [ ] 搜索代码中 "after X-1" 或 "X" 的条件检查 - [ ] 搜索代码中的 X-2 和 X-3(`CEPH_FEATURE_SERVER_*` 和 `ceph_release_t::*`) - [ ] 搜索代码中的 `require_osd_release` - [ ] 搜索代码中的 `min_mon_release` - [ ] 检查 `include/denc.h`,看 `DENC_START` 宏是否仍需引用 squid #### QA suite - [x] 创建 `qa/workunits/test_telemetry_(X-1).sh` - [x] 创建 `qa/workunits/test_telemetry_(X-1)_x.sh` - [x] 创建 `qa/suites/upgrade/(X-1)-x` - [x] 移除 `qa/suites/upgrade/(X-3)-x-*` - [x] 更新 `qa/fs/upgrade/` 移除 (X-3) 并添加 (X-1)(需与 fs team 确认/求助) - [x] 更新 `qa/` upgrade suites 的 `require-osd-release` 调用到 tentacle - [x] 创建 `qa/releases/X.yaml` - [x] 创建 `qa/suites/rados/thrash-old-clients/1-install/(X-1).yaml` - [x] 更新 `qa/suites/rados/encoder/1-task.yaml` 移除 (X-3) 并添加 X #### ceph-build 在 `ceph/ceph-build.git` repo 中: - [x] 添加 version → X 映射(`scripts/build_utils.sh` 中的 `release_from_version()`) - [x] 为 X 添加选项(`ceph-dev-build/build/build_osc` 中的 `case $RELEASE_BRANCH`) - [x] 为 X 添加选项(`ceph-dev-build/build/setup_osc` 中的 `case $RELEASE_BRANCH`) - [x] grep 前一个 release 并添加相关的构建 target ### After dev freeze(开发冻结后) - [ ] 为新 release 创建分支 - [ ] 移除 `doc/releases/*.rst`。这应留下 `doc/releases/releases.yml`,它用于文档构建目的。详见 commit `33d63c3`("doc: remove release notes for release branch") - [ ] cherry-pick `8cf9ad62949516666ad0f2c0bb7726ef68e4d666`("doc: add releases links to toc")。会有轻微的冲突。 - [ ] 在 **RTD**(https://readthedocs.org/dashboard/ceph/redirects/)为新的 major release 添加 redirect - [x] 把 release name 添加到 redmine(使用 https://tracker.ceph.com/custom_fields/16/edit) - [x] 把 release name 添加到 `.github/milestone.yml`,以便 github actions 自动为 backport 添加 milestone(**这个 commit 必须被 backport 到 release 分支**) - [ ] 把 release 分支添加到 nightlies:`qa/crontab/teuthology-cronjobs` ### First release candidate(第一个 RC) - [x] `src/ceph_release`:把 type 改为 `rc` - [ ] **opt-in 所有 telemetry channel**,生成 telemetry 报告,并验证**没有收集敏感细节**(如池名) - [x] 检查 `pg_pool_t`(`osd/osd_types.h`)中是否存在新的 pool flags,如果它们不敏感,就把它们加入 telemetry 的 `basic_pool_flags` 采集 ### First stable release(第一个 stable release) - [x] `src/ceph_release`:把 type 改为 `stable` - [x] **生成用于编码/解码测试的新的 object corpus** —— 见 `corpus` 文档(**本册未包含**) - [x] `src/cephadm/cephadmlib/constants.py`:更新 `LATEST_STABLE_RELEASE` - [x] 在 readthedocs 中激活最新 release - [ ] 把 release 和受支持的 distros 添加到 `doc/start/os-recommendations.rst` 的表格中 > 引自:[Release checklists](https://docs.ceph.com/en/tentacle/dev/release-checklists/) ## 7.4 C++17 与 libstdc++ ABI(`cxx.rst`) > ⚠️ 该页内容**基于 Mimic 时代的切换**,提及 devtoolset-7、Ubuntu Xenial 等,属于历史背景说明。但其结论(**不要依赖新 ABI 或旧 ABI 的具体行为**)至今仍是有价值的编码约束。 ### 核心事实 - Ceph 在 **mimic** 中切换到 **C++17**。要在没有 GCC-7 的老发行版上构建 Ceph,需要从附加仓库安装 GCC-7。 - 在 RHEL/CentOS 上,我们使用 SCLs 中的 **devtoolset-7** 来构建 Ceph。 - **但 devtoolset-7 总是使用旧 ABI,即使定义了 `_GLIBCXX_USE_CXX11_ABI=1`。** 所以在 RHEL/CentOS 上,`std::string` 和 `std::list` 的**旧实现**仍被使用: - `std::string` 仍然是 **copy-on-write** 的; - `std::list::size()` 仍然是 **O(n)** 的。 - 而在 **Ubuntu Xenial** 上,Ceph 是用**新 ABI** 构建的。 **结论**:因为我们仍在使用 libstdc++ 和 devtoolset 在 RHEL/CentOS 上构建包,**请不要依赖新 ABI 或旧 ABI 的行为。** ### 为什么不能依赖「GCC 支持双 ABI」 问题的根源在于**系统共享库与 `libstdc++_nonshared.a` 模型**: - 如果某个符号是从系统共享库中导出的,**我们必须使用它,且不能覆盖它**。 - 双 ABI 支持要求系统共享库中的若干符号表现得不同(例如对于 locale facets,需要注册两倍的数量,一套给旧 ABI,另一套给新 ABI)。 - 因此,如果我们想让构建出的二进制能在**只有旧 ABI 的 libstdc++** 的老发行版上运行,我们**别无选择,只能坚持使用旧 ABI**。 > 引自:[C++17 and libstdc++ ABI](https://docs.ceph.com/en/tentacle/dev/cxx/) ## 7.5 Documenting Ceph(`documenting.rst`) ### 用户文档 - `docs.ceph.com` 上的文档是从 Ceph git 仓库中 `/doc/` 下的 **reStructuredText** 源文件生成的。 - **请确保你的改动是以面向软件最终用户的方式书写的**,除非你是在 `/doc/dev/` 中添加内容——那是开发者专区。 - **所有修改面向用户功能的 pull request 必须包含相应的文档更新**(详见 `SubmittingPatches`)。 - 检查你的 `.rst` 语法是否按预期工作:可以在 github UI 中查看 `.rst` 文件的 diff 时点 "View" 按钮,或者用 `admin/build-doc` 脚本本地构建文档。 ### 代码文档(Code Documentation) C 和 C++ 可以用 **Doxygen** 文档化,使用 **Breathe** 支持的 Doxygen 标记子集。 函数文档的一般格式: ```c /** * Short description * * Detailed description when necessary * * preconditions, postconditions, warnings, bugs or other notes * * parameter reference * return value (if non-void) */ ``` 这**应该在函数被声明的头文件中**,并且**函数应按逻辑类别分组**。`librados C API` 提供了一个完整示例。它通过 `librados.rst` 被拉进 Sphinx,渲染在 `/rados/api/librados`。 生成 HTML 格式的 doxygen 文档: ```bash # cmake --build . --target doxygen ``` HTML 输出在:`build-doc/doxygen/html` ### 绘图(Drawing diagrams) | 工具 | 说明 | |---|---| | **Graphviz** | 可按 Graphviz extension 文档使用。多数时候你会想把真正的 DOT 源码放在单独的文件里:`.. graphviz:: myfile.dot`。入门推荐阅读 *Dot User's Manual* | | **Ditaa** | 可使用 Ditaa(http://ditaa.sourceforge.net/),直接在 rst 中用 ASCII 图画图 | | **Blockdiag** | 如果有需要,我们可以集成 Blockdiag。它是 Graphviz 风格的声明式绘图语言,包含:block diagrams(盒子和箭头,自动布局,与 Ditaa 不同)、sequence diagrams(时间线与消息)、activity diagrams(子系统与活动)、network diagrams(主机、LAN、IP 地址等,可选 Cisco 图标) | | **Inkscape** | 可用来生成可缩放矢量图形。**如果用它生成图,你应该同时提交 SVG 文件和导出的 PNG 文件,并引用 PNG 文件。**(提交 SVG 文件可以让其他人用 Inkscape 更新这些图。HTML5 将支持内联 SVG) | > 引自:[Documenting Ceph](https://docs.ceph.com/en/tentacle/dev/documenting/) ## 7.6 Building Ceph Documentation(`generatedocs.rst`) Ceph 使用 Python 的 **Sphinx** 文档工具。构建 Ceph 文档集需要 4 步。 ### 1. 克隆 Ceph 仓库 ```bash sudo apt-get install git git clone git://github.com/ceph/ceph ``` ### 2. 安装所需工具 要知道需要哪些包,可以直接执行构建命令: ```bash cd ceph admin/build-doc ``` 如果缺少依赖,上面的命令会失败,并给出一条建议你安装所有缺失依赖的命令。 ### 3. 构建文档 一旦安装完所有依赖,执行构建(与上面同一个命令): ```bash cd ceph admin/build-doc ``` 构建完成后,可以进入源码目录查看: ```bash cd build-doc/output ``` 那里应该有一个 `html` 目录和一个 `man` 目录,分别包含 HTML 和 manpage 格式的文档。 `admin/build-doc` 准备环境和构建文档需要很长时间。但你可以只在有改动时重建文档: ```bash admin/build-doc livehtml ``` 该特性底层使用 `sphinx-autobuild`。你也可以向它传递选项,例如构建后打开浏览器: ```bash admin/build-doc livehtml -- --open-browser ``` 详见 `sphinx-autobuild`(https://pypi.org/project/sphinx-autobuild/)。 ### 4. 演示文档(Demo) 构建完成后,可以运行 `serve-doc` 来演示渲染后的文档: ```bash cd ceph admin/serve-doc ``` 这会通过 Python 的 `SimpleHTTPServer` 模块,把 `build-doc/output/html` 目录**在 8080 端口上**提供出来。 > 引自:[Building Ceph Documentation](https://docs.ceph.com/en/tentacle/dev/generatedocs/) ## 7.7 IANA Numbers(`iana.rst`) 这是一份极短的常量速查页: | 项 | 值 | |---|---| | **Private Enterprise Number (PEN) Assignment** | **50495**(组织:`Ceph`) | | **Port number (monitor)** | **3300** | 关于 3300 的小彩蛋(文档原文):That's `0xce4`, or `ce4h`, or (sort of) "ceph." > 💡 补充:3300 是 msgr2 协议的默认 monitor 端口;经典(msgr1)端口 6789 未在本页列出。 > 引自:[IANA Numbers](https://docs.ceph.com/en/tentacle/dev/iana/) --- # 八、调试与观测工具 ## 8.1 Wireshark Dissector(`wireshark.rst`) > ⚠️ **该页极为过时**:原文第一句即为「Wireshark has support for the Ceph protocol and it will be shipped in the **1.12.1** release」——Wireshark 1.12.1 是 2014 年的版本。当前 Wireshark 早已内置 Ceph dissector,**通常不需要再从 git 源码构建**。本页内容应作为历史资料理解。 ### 使用 要使用 Wireshark dissector,你必须从 git 构建它(https://www.wireshark.org/develop.html)。该过程在 *Wireshark Users Guide* 的 "Building and Installing" 一节(https://www.wireshark.org/docs/wsug_html_chunked/ChapterBuildInstall.html)有非常详细的说明。 ### 开发 - Ceph dissector 位于 Wireshark git 中的 **`epan/dissectors/packet-ceph.c`**。 - 在该文件**顶部有一些注释**,解释如何插入新功能或更新现有类型的编码。 - 在你开始 hacking Wireshark 代码之前,应该先看 **`doc/README.developer`** 和 **`doc/README.dissector`** 这两个文档,它们解释了编写 dissector 的基础知识。读完这两个文档后,你应该就准备好可以在 Ceph dissector 上工作了。 - *The Wireshark developers guide*(https://www.wireshark.org/docs/wsdg_html_chunked/)也包含大量有用信息,但它组织性较差,**更适合作为参考而非入门**。 > 引自:[Wireshark Dissector](https://docs.ceph.com/en/tentacle/dev/wireshark/) ## 8.2 在 Kubernetes 中用 Rook 开发 Ceph(`kubernetes.rst`) > ⚠️ **该页自带警告**:「This is *not* official user documentation for setting up production Ceph clusters with Kubernetes. It is aimed at developers who want to hack on Ceph in Kubernetes.」 > ⚠️ 内容也**偏旧**:使用 `go get github.com/rook/rook`、GOPATH、`docker-distribution`、`minikube --driver=docker` 等。 ### 定位 本指南面向**开始在 Kubernetes 环境中运行的 Ceph 开发者**。它假设你可能在 hack Rook、Ceph 或两者,**所以一切都从源码构建**。 ### TL;DR(只改 MGR 模块时) 改完 Python 代码后,从 Ceph 的 `build` 目录运行: ```bash ../src/script/kubejacker/kubejacker.sh '192.168.122.1:5000' ``` 其中 `'192.168.122.1:5000'` 是一个本地 docker registry,而 Rook 的 `CephCluster` CR 使用 `image: 192.168.122.1:5000/ceph/ceph:latest`。 ### 1. 构建 Kubernetes 集群 在安装 Ceph/Rook 之前,确保你有一个可工作的 kubernetes 集群并已加入一些节点(即 `kubectl get nodes` 能看到东西)。本指南的其余部分假设你的开发工作站能网络访问你的 kubernetes 集群,使得 `kubectl` 能从你的工作站工作。 可选方案: - `kubic-terraform-kvm`(https://github.com/kubic-project/kubic-terraform-kvm) - 用 `kubeadm` 自己搭建(https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/) **Tips:** `kubeadm`: - 如果你之前添加过任何用于 kubernetes 包的 yum/deb repo,在使用 packages.cloud.google.com 仓库之前**先禁用它们**。否则你会遇到相当令人困惑的冲突。 - **即使你的发行版已经有 docker**,也要确保你安装的是 docker.com 上 kubeadm 安装说明中提到的版本范围内的版本。特别注意,**CentOS 7、8 中的 docker 不能工作**。 `minikube`: - 启动时传入本地 docker registry 地址: `minikube start --driver=docker --insecure-registry='192.168.122.1:5000'` **Hosted elsewhere**:如果你手头没有任何服务器,可以尝试纯容器提供商如 Google Compute Engine。至于你的 kubernetes 集群能看到什么样的存储设备,效果可能参差不齐。**在启动一个大型集群之前,务必确认它花了你多少钱!** ### 2. 运行 docker registry 在**你的工作站和 kubernetes 集群都能访问**的地方运行它(即 `docker push/pull` 在任何地方都能工作)。这很可能就是你用作 kubernetes master 的那台主机。 1. 安装 `docker-distribution` 包。 2. 如果想配置端口,编辑 `/etc/docker-distribution/registry/config.yml`。 3. 启用 registry 服务: ```bash systemctl enable docker-distribution systemctl start docker-distribution ``` > 你可能需要把这个 registry 标记为 **insecure**。 ### 3. 构建 Rook > 注意:**构建 Rook 不是修改 Ceph 所必需的。** 如果还没有 Go 就先安装。下载 Rook 源码: ```bash go get github.com/rook/rook # 忽略这个警告,因为 Rook 不是传统的 go package can't load package: package github.com/rook/rook: no Go files in /home/jspray/go/src/github.com/rook/rook ``` 现在你会在 `~/go/src/github.com/rook/rook` 有一个 Rook 源码树——你可能想把它克隆到别处,但**留在 GOPATH 里会让你的生活更轻松**。 在 Rook 树的根目录运行 `make` 来构建其二进制和容器: ```bash make ... === saving image build-9204c79b/ceph-amd64 === docker build build-9204c79b/ceph-toolbox-base-amd64 sha256:653bb4f8d26d6178570f146fe637278957e9371014ea9fce79d8935d108f1eaa === docker build build-9204c79b/ceph-toolbox-amd64 sha256:445d97b71e6f8de68ca1c40793058db0b7dd1ebb5d05789694307fd567e13863 === caching image build-9204c79b/ceph-toolbox-base-amd64 ``` 你可以用 `docker image ls` 查看构建出的镜像。你关心的镜像是 tag 以 **`ceph-amd64`** 结尾的那些(用于 Rook operator 和 Ceph 守护进程)和 **`ceph-toolbox-amd64`**(用于运行 CLI 的 "toolbox" 容器)。 ### 4. 构建 Ceph > 注意:**构建 Ceph 不是修改用 Python 编写的 MGR 模块所必需的。** Rook 容器和 Ceph 容器现在是相互独立的。注意 **Rook 的 Ceph 客户端库需要与 Ceph 集群通信,因此需要兼容的 major 版本**。 你可以用类似如下命令运行一个能访问你 Ceph 源码树的 Registry docker 容器: ```bash docker run -i -v /my/ceph/src:/my/ceph/src -p 192.168.122.1:5000:5000 -t --name registry registry:2 ``` 一旦你构建了 Ceph,就可以用 **`kubejacker.sh`** 脚本(从你的 build 目录运行,但要**在你的 build 容器之外**)把生成的二进制注入到 Rook 容器镜像中。 ### 5. 运行 Kubejacker `kubejacker` 需要访问你的 docker registry。执行该脚本以构建一个包含你最新 Ceph 二进制的 docker 镜像: ```bash build$ ../src/script/kubejacker/kubejacker.sh ":" ``` 现在你已经把刚构建的 Rook 和刚构建的 Ceph 打进了一个容器镜像,可以运行了。**下次你在 Ceph 中改了东西,可以重跑这个来更新镜像并重启你的 kubernetes 容器**。如果你改了 Rook,那就重跑 Rook 构建,**以及** Ceph 构建。 ### 6. 运行 Rook 集群 搭建 Rook operator、Ceph 集群和 toolbox 请参考 Rook 文档(https://rook.io/docs/rook/master/ceph-quickstart.html)。 Rook 源码树在 `cluster/examples/kubernetes/ceph/` 中包含示例 `.yaml` 文件。把它们复制到一个工作目录,并按需要编辑以配置你想要的环境: - 确保 `spec.cephVersion.image` 指向你的 docker registry: ```yaml spec: cephVersion: allowUnsupported: true image: 192.168.122.1:5000/ceph/ceph:latest ``` 然后用 `kubectl` 把配置加载进 kubernetes API: ```bash kubectl apply -f ./cluster-test.yaml ``` 用 `kubectl -n rook-ceph get pods` 检查 operator pod、Ceph 守护进程和 toolbox 是否起来了。 一旦一切运行起来,你应该能在 toolbox 容器中打开一个 shell 并运行 `ceph status`。 > **排错提示**:如果你的 mon 服务启动了但其他没启动,可能是它们由于 Kubernetes 网络问题**无法形成 quorum**:检查你的 Kubernetes 集群中的容器能否 ping 通其他节点上的容器。 ### Cheat sheet ```bash # 在 toolbox 容器中打开 shell kubectl -n rook-ceph exec -it $(kubectl -n rook-ceph get pod -l "app=rook-ceph-tools" -o jsonpath="{.items[0].metadata.name}") -- bash # 查看 Rook operator 容器日志 kubectl -n rook-ceph logs -l app=rook-ceph-operator # 查看 ceph-mgr 容器日志 kubectl -n rook-ceph logs -l app=rook-ceph-mgr ``` > 引自:[Hacking on Ceph in Kubernetes with Rook](https://docs.ceph.com/en/tentacle/dev/kubernetes/) ## 8.3 Ceph messenger DPDKStack(`dpdk.rst`) ### 是什么 / 为什么需要 Ceph 的 **DPDKStack** 是 messenger 的一种网络栈实现,用 DPDK 用户态驱动替代 POSIX 协议栈,以降低网络延迟、提升吞吐,配合 Crimson 的「低 CPU / 高性能」方向。 ### 编译 DPDKStack Ceph dpdkstack **默认不编译**,需要重新编译并启用: ```bash do_cmake.sh -DWITH_DPDK=ON ``` 可选项:在有预编译 DPDK 包的发行版上安装 `dpdk-devel` 或 `dpdk-dev`。 ### 设置 DPDK 网卡 多数主流 NIC 支持 **SR-IOV**,可虚拟化为多个 VF NIC。**每个 OSD 通过 DPDK 使用一些专用 NIC;mon、mgr 和 client 则通过 POSIX 协议栈使用 PF NIC。** 加载 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 ``` 把 NIC 绑定到 DPDK 应用: ```bash dpdk-devbind.py -b vfio-pci 0000:xx:yy.z ``` ### 配置 OSD 使用 DPDKStack 默认情况下,DPDK RTE 初始化过程需要 root 权限以访问系统中的各种资源。要授予 `ceph` 用户 root 访问权限: ```bash usermod -G root ceph ``` OSD 使用 **`ms_dpdk_devs_allowlist`** 选择 NIC: **1. 配置单个 NIC** ```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 ``` > 💡 补充:文档只给出了配置片段,**未给出每个选项的默认值与详细说明**(只列出了示例值)。 ### 调试与优化 **1. 日志**:根据日志定位故障,并把日志调到合适的级别: ```ini debug_dpdk=xx debug_ms=xx ``` > 如果日志中包含大量 retransmit 消息,**降低 `ms_dpdk_tcp_wmem` 的值**。 **2. 查看 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" ``` 如果超过阈值,**增大限流阈值或禁用限流**。 **3. 检查网卡是否故障或异常**(获取网卡状态和统计): ```bash ceph daemon osd.$i show_pmd_stats ceph daemon osd.$i show_pmd_xstats ``` **4. TSO 问题**:某些 DPDK 版本(例如 `dpdk-20.11-3.e18.aarch64`)或 NIC TSO 异常,尝试禁用 tso: ```ini ms_dpdk_enable_tso=false ``` **5. 多队列**:如果 VF NIC 支持多队列,可以把更多网卡队列分配给单个 core 以提升性能: ```ini ms_dpdk_hw_queues_per_qp=4 ``` ### 状态与未来工作(Status and Future Work) **性能对比(文档给出的数据)**:与 POSIX Stack 相比,在多并发测试中,DPDKStack 的 **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/) --- ## 文档未明确项 以下是本册范围内**官方文档没有给出答案**的问题,整理供后续追查或实践验证: ## Crimson 1. **Crimson 的正式路线图与 GA 时间表**:`doc/dev/crimson/` 下没有 roadmap 页面,也没有「已完成 / 未完成功能矩阵」。只知道它处于 tech preview、不适合生产。 2. **经典 `ceph-osd` 的线程/锁模型细节**:本册范围内的页面没有描述(属于 `doc/dev/osd_internals/`,另一册),因此 2.2 节的对比表中该格只能标「文档未在本册详述」。 3. **`cyanstore` 的完整配置项**:文档只说明它「不存储数据,用于测量 OSD 开销」,未给出任何配置参数。 4. **`seastore` 的内部设计**:本册收录的 crimson 页面里只有 PoseidonStore 有详细设计;SeaStore 的设计文档**不在** `doc/dev/crimson/`(本册范围内没有),只能从 `crimson.rst` 的零散描述了解。 5. **BackfillMachine 的状态图细节**:示例图是 SVG 二进制文件,本册无法展示;且官方 TODO 明确说异常状态与 happy path 代码走读尚未编写。 6. **`crimson_seastar_num_threads` 与 OSD 数量的具体推荐公式**:只给了「推荐小于 nproc / OSD 数」的定性建议,没有给出考虑 alien thread 后的完整分配公式。 7. **ASan 默认开启的性能影响与如何关闭**:文档只提示「从 git clone 源码构建时 ASan 默认开启」,未说明如何关闭或影响多大。 8. **Crimson 是否支持 RGW / CephFS 等非 OSD 组件**:本册范围内文档未明确。 ## CephFS 9. **`ceph_set_session_timeout()` 的完整签名与单位**:只从示例推断为秒。 10. **Snapdiff 特性本身的实现**:`cephfs-mirroring.rst` 只引用外部链接,未展开。 11. **`fs mirror status` 何时会提供更丰富的信息**:文档只说「Currently (May 2025) 只提供极少信息」,无计划说明。 12. **多个 mirror daemon 的支持时间表**:只说「目前未测试,只推荐单个」。 13. **libcephfs_proxy 的元数据缓存**:明确写着「将在未来版本设计和实现」,当前无。 14. **libcommon / libglobal 之外的库**:`libs.rst` 自带 `todo: document other libraries`。 ## RBD 15. **`rbd export-diff` 默认输出 v1 还是 v2**:`rbd-diff.rst` 未说明。 16. **rbd-export 中 `--export-format=2` 之外还有哪些 export format**:未说明。 17. **rbd-layering 中提到的两项未来优化(细粒度 copy-up、对象存在位图)是否已实现**:文档以「future optimization」表述,未说明当前状态。 ## 组件 18. **Balancer 的具体算法**:`balancer-design.rst` 只讲「为什么」和方向,无评分函数、迭代策略等。 19. **`ceph orch device ls` 不扩展的量化阈值**:`storage_devices_and_osds.rst` 只说 "Does not scale"。 20. **cephadm compliance check 的实际落地状态**:该页是设计讨论文档,`cephadm/index.rst` 收录了它,但文档未说明是否已实现、在哪个版本可用。 21. **cephadm host maintenance 的实现状态**:同样是设计文档(`could be implemented`)。 22. **ceph-volume zfs 的 `compression` tag 正确写法**:文档示例疑为复制粘贴错误(写成 `ceph.vdo=1`)。 23. **RGW `s3_compliance.rst` 中 `?` 状态的确切含义**:未定义。 24. **RGW Admin Ops 中哪些真正「未实现」**:页面标题含 Nonimplemented,但正文未逐项标注。 25. **`Get Zone Info` 的 `control_pool` 字段含义**:文档中描述栏为空。 ## 测试 / CI / 发布 26. **`mstart.sh` 的端口分配规则与目录隔离方式**:`dev_cluster_deployment.rst` 未说明。 27. **`vstart.sh` 的 `MGR`、`NFS` 等环境变量**:未在该页选项表中列出(但在其他页面使用)。 28. **Sepia lab 的具体资源与申请流程**:`sepia.rst` 只有两句话,全部指向需权限的 wiki。 29. **`corpus.rst` 内容**:本册未读取该页(按分工跳过),仅在 release-checklists 中被引用。 30. **`RELEASE_TYPE` 的完整取值集合**:文档中出现了 `STABLE`、`HOTFIX`、`SECURITY`,未说明是否还有其他值。 31. **Tentacle 的 `DISTROS` codemap**:`release-process.rst` 的表格只列到 squid(`jammy centos9 windows bookworm`),Tentacle 的值文档未给出。 ## 环境与工具 32. **macOS / FreeBSD 上构建的当前可用配置**:两页都明显过时(Xcode 9、brew cask install osxfuse 等),Tentacle 时代是否仍支持、支持到什么程度,文档未明确。 33. **Wireshark dissector 的当前状态**:页面停留在 1.12.1,与现状严重脱节。 34. **`development-workflow.rst` 中的角色名单是否仍然有效**:页面从 hammer/infernalis 时代未更新。 35. **`kubernetes.rst` 中的 Rook 构建方式**:使用 `go get` + GOPATH,与当前 Rook 的构建方式不符。 36. **`cxx.rst` 中的 devtoolset-7 / Ubuntu Xenial**:均为历史信息;Tentacle 的编译器基线要求文档未在本页给出。