# 第 11 册 · RGW 对象存储篇(三):多站点、同步与数据服务 > 基于 Ceph 官方文档整理 | 文档版本:**Tentacle v20.2.4** | 抓取日期:2026-09-17 | 源文:ceph/ceph@v20.2.4 `doc/radosgw/` > 官网入口:https://docs.ceph.com/en/tentacle/radosgw/ --- ## 本册导读 本册是 RGW 对象存储四册中的**第三册**,聚焦「数据在多个站点之间如何流动、如何被二次消费」。 | 分册 | 主题 | 本册是否覆盖 | |------|------|--------------| | 第 9 册(第一册) | RGW 基础:部署、frontend、pools/placement、用户与桶对象基础操作、S3/Swift 接口 | 否 | | 第 10 册(第二册) | RGW 安全与策略:STS / IAM / Keystone / LDAP / OIDC、bucket policy、服务端加密(Vault / KMIP / Barbican)、MFA、role、Lua 脚本、multitenancy | 否 | | **第 11 册(本册)** | **多站点(realm / zonegroup / zone / period)、同步策略与同步模块、云分层与云恢复、zone features、事件通知、桶日志、S3 Select、缓存与压缩、动态分片、孤立对象** | **是** | | 第 12 册(第四册) | 运维观测与排障:metrics、troubleshooting、NFS、QAT / UADK 加速、config-ref | 否 | > 💡 补充:上表的四册划分是为便于学习而做的编排建议,官方 `doc/radosgw/index.rst` 并不存在「四册」这种分法。本册覆盖的页面清单以文首【输入文件】为准。 本册涉及的官方页面与对应章节: | 官方页面 | 本册章节 | |----------|----------| | `multisite.rst` | 一、二、三 | | `multisite-sync-policy.rst` | 四 | | `sync-modules.rst` / `elastic-sync-module.rst` / `archive-sync-module.rst` / `cloud-sync-module.rst` | 五 | | `cloud-transition.rst` / `cloud-restore.rst` | 六 | | `zone-features.rst` | 七 | | `notifications.rst` | 八 | | `s3-notification-compatibility.rst` | 九 | | `bucket_logging.rst` | 十 | | `s3select.rst` | 十一 | | `rgw-cache.rst` / `d3n_datacache.rst` | 十二 | | `compression.rst` / `dynamicresharding.rst` | 十三 | | `orphans.rst` | 十四 | --- ## 一、多站点总览与核心概念 ### 1.1 单 zone 与多站点配置形态 **单 zone 配置(Single-zone)** 通常由两样东西构成: 1. 一个 `zonegroup`,其中包含一个 `zone`; 2. 一个或多个 `ceph-radosgw` 实例,客户端请求在这些实例之间做负载均衡。 典型的单 zone 配置是:多个 `ceph-radosgw` 实例共同使用**同一个 Ceph 存储集群**。 **多站点配置(Multi-site)** 自 Jewel 引入、Kraken 起支持多种形态: | 形态 | 说明 | 数据隔离性 | |------|------|-----------| | **Multi-zone** | 一个 `zonegroup` 内包含多个 `zone`。每个 zone 由一个或多个 `ceph-radosgw` 实例组成,**每个 zone 由各自独立的 Ceph 存储集群支撑** | 同一 zonegroup 内 zone 之间互相复制全部数据;每个 zone 都是 active 的,都能接收写操作 | | **Multi-zonegroups** | 一个 realm 下多个 zonegroup(旧称 "region")。同一 zonegroup 内的两个 zone 共享对象数据;同一 realm 下不同 zonegroup 之间共享**全局对象命名空间** | bucket 归属于创建它的 zonegroup(可被 `LocationConstraint` 覆盖);对象数据**只复制到本 zonegroup 内的其他 zone**;其他 zonegroup 收到该 bucket 的请求会被重定向到 bucket 所属 zonegroup | | **Multiple Realms** | realm 是 zonegroup 的容器,realm 之间命名空间完全独立 | 每个 realm 可有自己的一套配置;不同 realm 之间不共享命名空间 | **multi-zonegroup 的典型用途**:多个互联站点共享用户与 bucket 命名空间,但只希望对象数据在站点子集内复制。例如「多个站点共享一份用户体系,但每两个站点互为备份」,就可以建多个各含 2 个 zone 的 zonegroup,避免把全部对象复制到全部 zone。 ### 1.2 核心概念关系表 | 概念 | 层级 | 定义 | 关键特性 | |------|------|------|----------| | `realm` | 最高层 | zonegroup 的容器,可在同一套硬件上支持多个命名空间与多套配置 | 全局唯一命名空间;关联一个 `period`;为兼容 Infernalis 及更早版本,**默认不会自动创建 realm**,但新建集群建议显式创建 | | `zonegroup` | 第二层 | 定义一个或多个 zone 的地理位置;旧称 "region" | 每个 realm 必须有且仅有一个 **master zonegroup**;zonegroup 内的 zone 之间复制数据;跨 zonegroup 只共享命名空间 | | `zone` | 第三层 | 由一个或多个 `ceph-radosgw` 实例组成的逻辑组;同一 zone 内的所有 RGW 服务**同一套 pools、同一个集群**上的 RADOS 对象 | 每个 zonegroup 必须有一个 **master zone**;可有多个 secondary zone;zone 可标记 `read_only` | | `period` | 配置版本 | 表示 zonegroup 与 zone 配置在某一时刻的状态 | 每次修改 zonegroup / zone 后都应 `period update --commit`;提交后 epoch +1,其他 zone 才能收到新配置 | | `epoch` | 版本号 | period 内用于跟踪配置变更的序号 | realm 输出中的 `"epoch"` 字段;新建 realm 时 `epoch: 1` | | `bucket` / `object` | 数据层 | bucket 属于 zone(进而属于 zonegroup、realm);bucket 内含 object | bucket 归属于创建它的 zonegroup | > 💡 补充:官方 `multisite.rst` 中并未出现「`datalog` / `bilog` / `mdlog` 三层日志的架构图详解」,只在「Check the logs」一节提到多站点部署下可以检查 `mdlog`(元数据日志)、`bilog`(bucket index 日志)、`datalog`(数据日志)。下文 1.4 节的同步架构描述基于这些日志在 `sync status` 输出中的体现(metadata sync / data sync)整理,属于推断性补充。 ### 1.3 自 Infernalis 以来的功能性变化(Kraken 起) | 变化 | 说明 | |------|------| | **active-active zone 模式** | 每个 RGW 都可配置为 active-active,允许向非 master zone 写入 | | **配置容器化** | 多站点配置存放在 `realm` 中;realm 保存 zonegroup、zone 以及带多个 epoch 的 `period` | | **去 agent 化** | 从 Kraken 起,`ceph-radosgw` 守护进程自身负责跨 zone 同步,不再需要独立的同步 agent | | **默认 active-active** | 上述新同步机制使得默认即为 active-active,而非 active-passive | ### 1.4 同步架构与流程 **三类日志(仅多站点部署存在):** | 日志 | 全称 | 承载内容 | 对应 `sync status` 段落 | 相关 shard 数(默认值取自 src/common/options/*.yaml.in @ v20.2.4) | |------|------|----------|--------------------------|--------------------------------------------------| | `mdlog` | metadata log | 用户、bucket 等元数据变更 | `metadata sync` | `rgw_md_log_max_shards` = **64**(int) | | `datalog` | data log | 对象数据变更 | `data sync source: ` | `rgw_data_log_num_shards` = **128**(int) | | `bilog` | bucket index log | bucket 索引变更 | (由 data sync 消费) | 文档未明确 | **同步的两条主线:** 1. **metadata sync(元数据同步)**:secondary zone 从 master zone 拉取 `mdlog`,把用户、bucket 等元数据补齐。输出形如 `full sync: 0/64 shards` + `incremental sync: 64/64 shards`。 2. **data sync(数据同步)**:zone 从 zonegroup 内的其他 zone(data sync source)拉取 `datalog`,按 bucket 复制对象。输出每个 source 一段,形如 `full sync: 0/128 shards` + `incremental sync: 128/128 shards`。 **full sync 与 incremental sync:** - `full sync`:初次同步时把该 shard 上的全量数据搬过去; - `incremental sync`:全量完成后转为增量,只搬新增/变更的日志条目。 **对象校验:** 默认情况下对象同步成功后**不做二次校验**。把 `rgw_sync_obj_etag_verify` 设为 `true` 后,会使用 **MD5 校验和**验证从源传到目的的数据完整性(包括通过 HTTP 从远端获取的任何对象,含多站点同步)。该选项会增加计算量,**可能降低 RGW 性能**。 > 默认值取自 src/common/options/*.yaml.in @ v20.2.4:`rgw_sync_obj_etag_verify` = `false`(bool)。 **日志裁剪(trim):** 通常无需手工 trim,因为 `rgw_sync_log_trim_interval` 默认为 **20 分钟**(源码中记为 `20_min`)。除非该值被手工改为 `0`,否则不需要 trim。 > 默认值取自 src/common/options/*.yaml.in @ v20.2.4:`rgw_sync_log_trim_interval` = `20_min` = 1200 秒(int)。 ### 1.5 部署模式:active-active 与 active-passive | 模式 | 创建方式 | 写能力 | 适用场景 | |------|----------|--------|----------| | **active-active**(默认) | `radosgw-admin zone create` 时**不加** `--read-only`、`--master`(secondary zone) | 任何 zone 都可写,写后复制到 zonegroup 内其他 zone | 容灾 + 可作为 CDN 基础;地理就近写入 | | **active-passive** | 创建 secondary zone 时加 `--read-only` | 只有 master zone 可写,secondary 只读 | 只希望从区做备份/只读副本 | > ⚠️ 重要约束:**元数据操作(例如创建用户)必须在 master zone 所在主机上执行**。bucket 操作 master 和 secondary 都能接收,但 **secondary 会把 bucket 操作重定向到 master zone**;如果 master zone 宕机,**bucket 操作会失败,而 object 操作仍可成功**。 ### 1.6 前提与假设 | 项 | 要求 | |----|------| | 存储集群数 | 至少 **2 个** Ceph 存储集群(多 zone 形态下每个 zone 由独立集群支撑) | | RGW 实例数 | 至少 2 个(每个集群一个) | | 地理位置 | 建议地理分离;同机房也可以工作 | | master | 必须有一个 master zonegroup 和一个 master zone;每个 zonegroup 必须有一个 master zone | | 网络 | **不推荐**在 WAN 延迟较高的情况下跑单一跨地域 Ceph 存储集群 | | 示例主机名 | 本文档沿用官方示例:`rgw1`(master zone `us-east`)、`rgw2`(secondary zone `us-west`),realm 名 `movies`,zonegroup 名 `us` | > 引自:[Multi-Site](https://docs.ceph.com/en/tentacle/radosgw/multisite/) --- ## 二、多站点部署全流程 本节给出从零搭建「master zone + secondary zone」的完整命令序列。 ### 2.1 流程总览 ``` 1. realm create(master 上) 2. zonegroup create --master --default(master 上) 3. zone create --master --default(master 上) 4. 删除 default zonegroup / zone 及其 pool(仅限全新集群!) 5. user create --system(系统用户,master 上) 6. zone modify 注入 access-key / secret → period update --commit 7. 修改 ceph.conf 加 rgw_zone → 启动 RGW --- 以下在 secondary 上 --- 8. realm pull(拉取 realm 配置) 9. realm default(若只有一个 realm) 10. zone create(不带 --master / --default,可选 --read-only) 11. 删除 secondary 上的 default zone / pool(可选) 12. 修改 ceph.conf 加 rgw_zone=us-west → period update --commit → 启动 RGW 13. radosgw-admin sync status 检查 ``` ### 2.2 配置 master zone #### 步骤 1:创建 realm ```bash radosgw-admin realm create --rgw-realm={realm-name} [--default] ``` 示例: ```bash radosgw-admin realm create --rgw-realm=movies --default ``` - 若集群只有这一个 realm,加 `--default`:之后 `radosgw-admin` 默认使用该 realm; - 不加 `--default`:后续添加 zonegroup / zone 时必须显式用 `--rgw-realm` 或 `--realm-id`。 回显示例: ```json { "id": "0956b174-fe14-4f97-8b50-bb7ec5e1cf62", "name": "movies", "current_period": "1950b710-3e63-4c41-a19e-46a715000980", "epoch": 1 } ``` > Ceph 会为 realm 生成唯一 ID,后续可用于重命名 realm。 #### 步骤 2:创建 master zonegroup ```bash radosgw-admin zonegroup create --rgw-zonegroup={name} --endpoints={url} \ [--rgw-realm={realm-name}|--realm-id={realm-id}] --master --default ``` 示例: ```bash radosgw-admin zonegroup create --rgw-zonegroup=us --endpoints=http://rgw1:80 \ --rgw-realm=movies --master --default ``` 回显示例: ```json { "id": "f1a233f5-c354-4107-b36c-df66126475a6", "name": "us", "api_name": "us", "is_master": "true", "endpoints": [ "http:\/\/rgw1:80" ], "hostnames": [], "hostnames_s3website": [], "master_zone": "", "zones": [], "placement_targets": [], "default_placement": "", "realm_id": "0956b174-fe14-4f97-8b50-bb7ec5e1cf62" } ``` #### 步骤 3:创建 master zone > ⚠️ **zone 必须在将属于该 zone 的 Ceph Object Gateway 节点上创建。** ```bash radosgw-admin zone create --rgw-zonegroup={zone-group-name} \ --rgw-zone={zone-name} \ --master --default \ --endpoints={http://fqdn}[,{http://fqdn}] ``` 示例: ```bash radosgw-admin zone create --rgw-zonegroup=us --rgw-zone=us-east \ --master --default \ --endpoints=http://rgw1:80 ``` > 此处**不指定** `--access-key` 和 `--secret`,它们会在下一步创建系统用户后写入 zone。 #### 步骤 4:删除 default zonegroup 与 zone(仅限全新集群) > ⚠️ **如果 `default` zone 已在存数据,千万不要删除,否则数据将不可恢复地丢失。** ```bash radosgw-admin zonegroup delete --rgw-zonegroup=default --rgw-zone=default radosgw-admin period update --commit radosgw-admin zone delete --rgw-zone=default radosgw-admin period update --commit radosgw-admin zonegroup delete --rgw-zonegroup=default radosgw-admin period update --commit ``` 删除 default pools: ```bash ceph osd pool rm default.rgw.control default.rgw.control --yes-i-really-really-mean-it ceph osd pool rm default.rgw.data.root default.rgw.data.root --yes-i-really-really-mean-it ceph osd pool rm default.rgw.gc default.rgw.gc --yes-i-really-really-mean-it ceph osd pool rm default.rgw.log default.rgw.log --yes-i-really-really-mean-it ceph osd pool rm default.rgw.users.uid default.rgw.users.uid --yes-i-really-really-mean-it ``` #### 步骤 5:创建系统用户(system user) `ceph-radosgw` 守护进程在拉取 realm 和 period 信息前必须完成认证,因此需要在 master zone 创建 `--system` 用户: ```bash radosgw-admin user create --uid="{user-name}" --display-name="{Display Name}" --system ``` 示例: ```bash radosgw-admin user create --uid="synchronization-user" --display-name="Synchronization User" --system ``` 记下输出中的 `access_key` 与 `secret_key`——secondary zone 需要它们来向 master zone 认证。 #### 步骤 6:把系统用户写入 master zone 并提交 period ```bash radosgw-admin zone modify --rgw-zone={zone-name} --access-key={access-key} --secret={secret} radosgw-admin period update --commit ``` #### 步骤 7:更新 period ```bash radosgw-admin period update --commit ``` > 更新 period 会改变 epoch,并确保其他 zone 能收到更新后的配置。 #### 步骤 8:更新 Ceph 配置文件 在 master zone 主机的 ceph.conf 中,为相应实例条目增加 `rgw_zone`: ```ini [client.rgw.rgw1] host = rgw1 rgw frontends = "civetweb port=80" rgw_zone=us-east ``` #### 步骤 9:启动网关 ```bash systemctl start ceph-radosgw@rgw.`hostname -s` systemctl enable ceph-radosgw@rgw.`hostname -s` ``` 若集群由 `cephadm` 部署(容器化),`systemctl` 无服务对象可用,应改为: ```bash ceph orch apply rgw --realm= --zone= --placement --port ``` ### 2.3 配置 secondary zone #### 步骤 1:拉取 realm 配置 使用 master zonegroup 中 master zone 的 URL、access key、secret: ```bash radosgw-admin realm pull --url={url-to-master-zone-gateway} \ --access-key={access-key} --secret={secret} ``` > 拉取 realm 配置同时会取回远端的当前 period 配置,并使其成为本机的当前 period。 若该 realm 是唯一 realm,设为默认: ```bash radosgw-admin realm default --rgw-realm={realm-name} ``` #### 步骤 2:创建 secondary zone ```bash radosgw-admin zone create --rgw-zonegroup={zone-group-name} \ --rgw-zone={zone-name} \ --access-key={system-key} --secret={secret} \ --endpoints=http://{fqdn}:80 \ [--read-only] ``` 示例: ```bash radosgw-admin zone create --rgw-zonegroup=us --rgw-zone=us-west \ --access-key={system-key} --secret={secret} \ --endpoints=http://rgw2:80 ``` > ⚠️ **不要**加 `--master` 或 `--default`。Kraken 起所有 zone 默认 active-active;如需 active-passive(secondary 只读),加 `--read-only`。 #### 步骤 3:删除 secondary 上的 default zone / pool(可选,仅限全新) ```bash radosgw-admin zone delete --rgw-zone=default ceph osd pool rm default.rgw.control default.rgw.control --yes-i-really-really-mean-it ceph osd pool rm default.rgw.data.root default.rgw.data.root --yes-i-really-really-mean-it ceph osd pool rm default.rgw.gc default.rgw.gc --yes-i-really-really-mean-it ceph osd pool rm default.rgw.log default.rgw.log --yes-i-really-really-mean-it ceph osd pool rm default.rgw.users.uid default.rgw.users.uid --yes-i-really-really-mean-it ``` #### 步骤 4:更新 Ceph 配置文件 ```ini [client.rgw.rgw2] host = rgw2 rgw frontends = "civetweb port=80" rgw_zone=us-west ``` #### 步骤 5:更新 period 并启动网关 ```bash radosgw-admin period update --commit systemctl start ceph-radosgw@rgw.`hostname -s` systemctl enable ceph-radosgw@rgw.`hostname -s` ``` ### 2.4 zonegroup / zone 的 ceph.conf 设置项 配置 default zonegroup 与 zone 时,pool 名会带上 zone 名前缀,例如 `default.rgw.control`。可在每个 `[client.radosgw.{instance-name}]` 段中修改下列设置: | 配置项 | 说明 | 类型 | 默认值 | |--------|------|------|--------| | `rgw_zone` | 该网关实例的 zone 名 | String | None(默认值取自 src/common/options/*.yaml.in @ v20.2.4:`str`,无 `default:` 字段;未设置时可用 `radosgw-admin zone default` 配置集群级默认) | | `rgw_zonegroup` | 该网关实例的 zonegroup 名 | String | None(默认值取自 src/common/options/*.yaml.in @ v20.2.4:`str`,无 `default:` 字段;未设置时可用 `radosgw-admin zonegroup default` 配置集群级默认) | | `rgw_zonegroup_root_pool` | zonegroup 的 root pool | String | `.rgw.root`(默认值取自 src/common/options/*.yaml.in @ v20.2.4) | | `rgw_zone_root_pool` | zone 的 root pool | String | `.rgw.root`(默认值取自 src/common/options/*.yaml.in @ v20.2.4) | | `rgw_default_zone_group_info_oid` | 存放 default zonegroup 的 OID。官方**不建议**修改 | String | `default.zonegroup`(源文表格给出;在 `rgw.yaml.in` 中未检索到同名条目) | > 引自:[Multi-Site](https://docs.ceph.com/en/tentacle/radosgw/multisite/) --- ## 三、多站点运维 ### 3.1 查看同步状态 ```bash radosgw-admin sync status ``` 典型输出: ```text realm b3bc1c37-9c44-4b89-a03b-04c269bea5da (earth) zonegroup f54f9b22-b4b6-4a0e-9211-fa6ac1693f49 (us) zone adce11c9-b8ed-4a90-8bc5-3fc029ff0816 (us-2) metadata sync syncing full sync: 0/64 shards incremental sync: 64/64 shards metadata is behind on 1 shards oldest incremental change not applied: 2017-03-22 10:20:00.0.881361s data sync source: 341c2d81-4574-4d08-ab0f-5a2a7b168028 (us-1) syncing full sync: 0/128 shards incremental sync: 128/128 shards data is caught up with source source: 3b5d1a3f-3f27-4e4a-8f34-6072d4bb1275 (us-3) syncing full sync: 0/128 shards incremental sync: 128/128 shards data is caught up with source ``` #### 输出字段逐项解读 | 字段 | 含义 | |------|------| | `realm ()` | 当前 realm 的 ID 与名称 | | `zonegroup ()` | 当前 zonegroup 的 ID 与名称 | | `zone ()` | **本 zone** 的 ID 与名称(即执行命令所在 zone) | | `metadata sync syncing` | 元数据同步正在进行中(`syncing` 为状态) | | `full sync: 0/64 shards` | 还需做全量同步的 shard 数 / 总 shard 数。`0/64` 表示全量已完成 | | `incremental sync: 64/64 shards` | 处于增量同步状态的 shard 数 / 总 shard 数。`64/64` 表示全部进入增量阶段 | | `metadata is caught up with master` | 元数据已追平 master zone | | `metadata is behind on 1 shards` | 有 1 个 shard 落后 | | `oldest incremental change not applied: ` | 最老的尚未应用的增量变更时间 | | `data sync source: ()` | 一个数据同步源 zone(zonegroup 内其他 zone),每个源一段 | | `data is caught up with source` | 该 source 的数据已追平 | | (无 `caught up` 行) | 该 source 尚未追平,需结合 shard 数与时间戳判断 | #### shard 的两种类型 | 类型 | 定义 | 处理建议 | |------|------|----------| | **Behind shards** | 需要数据同步(全量或增量)才能追上的 shard | 正常等待同步即可 | | **Recovery shards** | 同步过程中遇到错误、被标记重试的 shard。错误多为小问题(例如获取 bucket 锁失败) | 通常**会自行恢复**,无需干预 | #### 另一份「已追平」的输出示例 ```text realm f3239bc5-e1a8-4206-a81d-e1576480804d (earth) zonegroup c50dbb7e-d9ce-47cc-a8bb-97d9b399d388 (us) zone 4c453b70-4a16-4ce8-8185-1893b05d346e (us-west) metadata sync syncing full sync: 0/64 shards metadata is caught up with master incremental sync: 64/64 shards data sync source: 1ee9da3e-114d-4ae3-a8a4-056e8a17f532 (us-east) syncing full sync: 0/128 shards incremental sync: 128/128 shards data is caught up with source ``` ### 3.2 检查日志 仅多站点部署可检查三类日志:`mdlog`(元数据日志)、`bilog`(bucket index 日志)、`datalog`(数据日志)。可以 list 也可以 trim。 - **通常不需要手工 trim**:`rgw_sync_log_trim_interval` 默认 20 分钟; - 只有当 `rgw_sync_log_trim_interval` 被手工设为 `0` 时,才需要 trim。 > 💡 补充:源文只说明「可以 list 并 trim」这三类日志,但**未给出具体的 `radosgw-admin mdlog list` / `bilog list` / `datalog list` 命令语法**,因此此处不臆造命令。相关子命令语法请见官方 `adminops.rst`(不在本册范围内)。 ### 3.3 故障处理要点 | 现象 | 原因 / 说明 | 处理 | |------|-------------|------| | `metadata is behind on N shards` | 元数据 shard 落后 | 等待;若长时间不收敛,检查 master zone 与网络 | | 出现 recovery shards | 同步中遇到错误被标记重试(多为锁竞争等小问题) | 通常自愈;持续存在再排查 | | secondary 上 bucket 操作失败 | bucket 操作会被重定向到 master zone,**master 宕机则失败** | 恢复 master,或执行 failover(见 3.5) | | object 操作在 master 宕机时仍成功 | object 操作不依赖 master zone | 无需处理 | | 同步的对象内容可能不一致 | 默认不做同步后校验 | 打开 `rgw_sync_obj_etag_verify`(注意性能损耗) | | 日志无限增长 | `rgw_sync_log_trim_interval` 被设为 0 | 恢复为默认值或手工 trim | ### 3.4 更改 metadata master zone(promote) > ⚠️ **极其重要**:提升一个尚未完成元数据同步的 zone 为 master,会导致它无法服务剩余条目,**这些元数据变更会丢失**。建议先等该 zone 的 `radosgw-admin sync status` 完成元数据同步,再执行 promote。 > > 同样地,若 promote 期间旧 master 仍在处理元数据变更,这些变更很可能丢失。建议**先关闭旧 master zone 上的所有 radosgw 实例**;新 master 提升完成后,旧 master 可用 `radosgw-admin period pull` 拉取新 period 并重启网关。 将 zonegroup `us` 中的 zone `us-2` 提升为 metadata master: ```bash radosgw-admin zone modify --rgw-zone=us-2 --master radosgw-admin zonegroup modify --rgw-zonegroup=us --master radosgw-admin period update --commit ``` 这会生成一个新的 period,`us-2` 中的 radosgw 实例会把该 period 推送给其他 zone。 ### 3.5 Failover(故障切换到 secondary zone) 1. 让 secondary zone 成为 master 与 default zone: ```bash radosgw-admin zone modify --rgw-zone={zone-name} --master --default ``` 若集群是 active-passive(secondary 为 read-only),需要去掉只读状态才能写入: ```bash radosgw-admin zone modify --rgw-zone={zone-name} --master --default \ --read-only=false ``` 2. 更新 period 使变更生效: ```bash radosgw-admin period update --commit ``` 3. 重启 Ceph Object Gateway: ```bash systemctl restart ceph-radosgw@rgw.`hostname -s` ``` ### 3.6 Failback(从故障切换恢复原状) 原 master zone 恢复后,按顺序执行: ```bash # 1. 在已恢复的 zone 上,从当前 master zone 拉取最新 realm 配置 radosgw-admin realm pull --url={url-to-master-zone-gateway} \ --access-key={access-key} --secret={secret} # 2. 让已恢复的 zone 重新成为 master 与 default zone radosgw-admin zone modify --rgw-zone={zone-name} --master --default # 3. 更新 period radosgw-admin period update --commit # 4. 重启已恢复 zone 的 Ceph Object Gateway systemctl restart ceph-radosgw@rgw.`hostname -s` # 5. 若 secondary 需要回到只读配置 radosgw-admin zone modify --rgw-zone={zone-name} --read-only # 6. 更新 period radosgw-admin period update --commit # 7. 重启 secondary zone 的 Ceph Object Gateway systemctl restart ceph-radosgw@rgw.`hostname -s` ``` ### 3.7 从单站点迁移到多站点 若已有一个使用 `default` zonegroup / zone 的单站点部署,按以下步骤迁移: ```bash # 1. 创建 realm radosgw-admin realm create --rgw-realm= --default # 2. 重命名 default zonegroup 与 zone radosgw-admin zonegroup rename --rgw-zonegroup default --zonegroup-new-name= radosgw-admin zone rename --rgw-zone default --zone-new-name us-east-1 --rgw-zonegroup= # 3. 重命名 default zonegroup 的 api_name radosgw-admin zonegroup modify --api-name= --rgw-zonegroup= # 4. 配置 master zonegroup radosgw-admin zonegroup modify --rgw-realm= --rgw-zonegroup= \ --endpoints http://:80 --master --default # 5. 配置 master zone radosgw-admin zone modify --rgw-realm= --rgw-zonegroup= \ --rgw-zone= --endpoints http://:80 \ --access-key= --secret= \ --master --default # 6. 创建系统用户 radosgw-admin user create --uid= \ --display-name="" \ --access-key= \ --secret= --system # 7. 提交更新后的配置 radosgw-admin period update --commit # 8. 重启 Ceph Object Gateway systemctl restart ceph-radosgw@rgw.`hostname -s` ``` 完成后,再按 2.3 节在 master zonegroup 中创建 secondary zone。 ### 3.8 realm / zonegroup / zone 的完整命令参考 #### Realms | 操作 | 命令 | |------|------| | 创建 realm | `radosgw-admin realm create --rgw-realm={realm-name} [--default]` | | 设为默认 realm | `radosgw-admin realm default --rgw-realm=movies` | | 删除 realm | `radosgw-admin realm rm --rgw-realm={realm-name}` | | 获取 realm | `radosgw-admin realm get --rgw-realm=` | | 设置 realm(从文件) | `radosgw-admin realm set --rgw-realm= --infile=` | | 列出 realm | `radosgw-admin realm list` | | 列出 realm periods | `radosgw-admin realm list-periods` | | 拉取 realm | `radosgw-admin realm pull --url={url-to-master-zone-gateway} --access-key={access-key} --secret={secret}` | | 重命名 realm | `radosgw-admin realm rename --rgw-realm= --realm-new-name=` | **realm get 输出示例:** ```json { "id": "0a68d52e-a19c-4e8e-b012-a8f831cb3ebc", "name": "movies", "current_period": "b0c5bbef-4337-4edd-8184-5aeab2ec413b", "epoch": 1 } ``` > ⚠️ realm 重命名注意事项:realm **不属于 period**,因此重命名只作用于本地,执行 `realm pull` 时不会被拉走。若 realm 含多个 zone,需在**每个 zone 上**各执行一次 rename。 > > ⚠️ **不要用 `realm set` 修改 `name` 参数**——那只会改内部名,`--rgw-realm` 仍会期待旧名字。 > 每个 realm 关联一个 period;每次修改 zonegroup 或 zone 后都应更新并提交 period。 #### Zonegroups | 操作 | 命令 | 是否需要 `period update --commit` | |------|------|----------------------------------| | 创建 | `radosgw-admin zonegroup create --rgw-zonegroup= [--rgw-realm=][--master] [--default]` | 视情况 | | 修改 | `radosgw-admin zonegroup modify --rgw-zonegroup= ...` | 视情况 | | 设为默认 | `radosgw-admin zonegroup default --rgw-zonegroup=comedy` | 是 | | 添加 zone | `radosgw-admin zonegroup add --rgw-zonegroup= --rgw-zone=` | 是 | | 移除 zone | `radosgw-admin zonegroup remove --rgw-zonegroup= --rgw-zone=` | 是 | | 重命名 | `radosgw-admin zonegroup rename --rgw-zonegroup= --zonegroup-new-name=` | 是 | | 删除 | `radosgw-admin zonegroup delete --rgw-zonegroup=` | 是 | | 列表 | `radosgw-admin zonegroup list` | 否 | | 获取 zonegroup map | `radosgw-admin zonegroup-map get` | 否 | | 获取单个 zonegroup | `radosgw-admin zonegroup get [--rgw-zonegroup=]` | 否 | | 设置 zonegroup(从文件) | `radosgw-admin zonegroup set --infile zonegroup.json` | 是 | | 设置 zonegroup map | `radosgw-admin zonegroup-map set --infile zonegroupmap.json` | 是 | **zonegroup list 输出示例:** ```json { "default_info": "90b28698-e7c3-462c-a42d-4aa780d24eda", "zonegroups": [ "us" ] } ``` > 若收到 `failed to read zonegroup map` 错误,先以 root 身份执行 `radosgw-admin zonegroup-map update`。 **zonegroup get 输出示例:** ```json { "id": "90b28698-e7c3-462c-a42d-4aa780d24eda", "name": "us", "api_name": "us", "is_master": "true", "endpoints": [ "http:\/\/rgw1:80" ], "hostnames": [], "hostnames_s3website": [], "master_zone": "9248cab2-afe7-43d8-a661-a40bf316665e", "zones": [ { "id": "9248cab2-afe7-43d8-a661-a40bf316665e", "name": "us-east", "endpoints": [ "http:\/\/rgw1" ], "log_meta": "true", "log_data": "true", "bucket_index_max_shards": 0, "read_only": "false" }, { "id": "d1024e59-7d28-49d1-8222-af101965a939", "name": "us-west", "endpoints": [ "http:\/\/rgw2:80" ], "log_meta": "false", "log_data": "true", "bucket_index_max_shards": 0, "read_only": "false" } ], "placement_targets": [ { "name": "default-placement", "tags": [] } ], "default_placement": "default-placement", "realm_id": "ae031368-8715-4e27-9a99-0c9468852cfe" } ``` **zonegroup 配置项(用于 `zonegroup set` 的 JSON)逐项说明:** | 序号 | 字段 | 必填 | 说明 | |------|------|------|------| | 1 | `name` | 必填 | zonegroup 名称 | | 2 | `api_name` | 可选 | zonegroup 的 API 名 | | 3 | `is_master` | 必填 | 是否为 master zonegroup。**只能有一个 master zonegroup** | | 4 | `endpoints` | 可选 | zonegroup 内所有 endpoint 列表。可用多个域名指向同一 zonegroup;正斜杠需转义为 `\/`;可带端口 `fqdn:port` | | 5 | `hostnames` | 可选 | zonegroup 内所有 hostname 列表。`rgw dns name` 设置会自动加入此列表;**修改后需重启网关守护进程** | | 6 | `master_zone` | 可选 | zonegroup 的 master zone。未指定则用 default zone。**每个 zonegroup 只能有一个 master zone** | | 7 | `zones` | 可选 | zonegroup 内所有 zone 的列表。每个 zone 含 name(必填)、endpoints(可选)、以及是否记录 metadata/data 操作的开关(**默认 false**) | | 8 | `placement_targets` | 可选 | placement target 列表。每个 target 含 name(必填)与 tags(可选,用于限制只有带该 tag 的用户可用,对应用户信息中的 `placement_tags` 字段) | | 9 | `default_placement` | 可选 | 对象索引与对象数据的默认 placement target,**默认 `default-placement`**;也可在用户信息中为每个用户设置默认 placement | > ⚠️ `default` zonegroup 的 `is_master` 默认为 `true`。若要新建 zonegroup 并使其成为 master,必须先把 `default` zonegroup 的 `is_master` 设为 `false`,或删除 `default` zonegroup。 **zonegroup map 示例(含 `master_zonegroup` 与配额):** ```json { "zonegroups": [ { "key": "90b28698-e7c3-462c-a42d-4aa780d24eda", "val": { "id": "90b28698-e7c3-462c-a42d-4aa780d24eda", "name": "us", "api_name": "us", "is_master": "true", "endpoints": [ "http:\/\/rgw1:80" ], "hostnames": [], "hostnames_s3website": [], "master_zone": "9248cab2-afe7-43d8-a661-a40bf316665e", "zones": [ { "id": "9248cab2-afe7-43d8-a661-a40bf316665e", "name": "us-east", "endpoints": [ "http:\/\/rgw1" ], "log_meta": "true", "log_data": "true", "bucket_index_max_shards": 0, "read_only": "false" }, { "id": "d1024e59-7d28-49d1-8222-af101965a939", "name": "us-west", "endpoints": [ "http:\/\/rgw2:80" ], "log_meta": "false", "log_data": "true", "bucket_index_max_shards": 0, "read_only": "false" } ], "placement_targets": [ { "name": "default-placement", "tags": [] } ], "default_placement": "default-placement", "realm_id": "ae031368-8715-4e27-9a99-0c9468852cfe" } } ], "master_zonegroup": "90b28698-e7c3-462c-a42d-4aa780d24eda", "bucket_quota": { "enabled": false, "max_size_kb": -1, "max_objects": -1 }, "user_quota": { "enabled": false, "max_size_kb": -1, "max_objects": -1 } } ``` > zonegroup map 中只能有一个 `is_master` 为 `true` 的 zonegroup,且必须在 map 末尾被指定为 `master_zonegroup`。 #### Zones | 操作 | 命令 | |------|------| | 创建 | 见下方代码块 | | 删除 | 先从 zonegroup 移除 → `period update --commit` → `zone delete` → `period update --commit` | | 修改 | `radosgw-admin zone modify [options]` | | 列表 | `radosgw-admin zone list` | | 获取 | `radosgw-admin zone get [--rgw-zone=]` | | 设置(从文件) | `radosgw-admin zone set --rgw-zone={zone-name} --infile zone.json` → `period update --commit` | | 重命名 | `radosgw-admin zone rename --rgw-zone= --zone-new-name=` → `period update --commit` | **创建 zone:** ```bash radosgw-admin zone create --rgw-zone= \ [--zonegroup=[,] \ [--master] [--default] \ --access-key $SYSTEM_ACCESS_KEY \ --secret $SYSTEM_SECRET_KEY radosgw-admin period update --commit ``` > 只有 zonegroup 中的一个 zone 可以是 master zone(`--master`)。加 `--rgw-zonegroup` 可把新 zone 直接加入指定 zonegroup。 **删除 zone:** ```bash radosgw-admin zonegroup remove --zonegroup= \ --zone= radosgw-admin period update --commit radosgw-admin zone delete --rgw-zone radosgw-admin period update --commit ``` > ⚠️ **必须先从 zonegroup 移除再删除 zone**,否则 `period update` 会失败。 **删除已删除 zone 的 pool(仅当这些 pool 不再被使用):** ```bash ceph osd pool rm .rgw.control .rgw.control --yes-i-really-really-mean-it ceph osd pool rm .rgw.meta .rgw.meta --yes-i-really-really-mean-it ceph osd pool rm .rgw.log .rgw.log --yes-i-really-really-mean-it ceph osd pool rm .rgw.otp .rgw.otp --yes-i-really-really-mean-it ceph osd pool rm .rgw.buckets.index .rgw.buckets.index --yes-i-really-really-mean-it ceph osd pool rm .rgw.buckets.non-ec .rgw.buckets.non-ec --yes-i-really-really-mean-it ceph osd pool rm .rgw.buckets.data .rgw.buckets.data --yes-i-really-really-mean-it ``` > ⚠️ **只删除带 zone 名前缀的 pool。** 删除 root pool(例如 `.rgw.root`)会清掉整个系统的配置。 > ⚠️ pool 一旦删除,其中的数据将不可恢复地丢失。 **`zone modify` 支持的 options:** | 选项 | 说明 | |------|------| | `--access-key=` | 系统用户 access key | | `--secret/--secret-key=` | 系统用户 secret key | | `--master` | 设为 master zone | | `--default` | 设为 default zone | | `--endpoints=` | endpoint 列表 | 修改后需执行 `radosgw-admin period update --commit`。 **`default` zone 的 `zone get` 输出示例(pool 布局):** ```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", "system_key": { "access_key": "", "secret_key": ""}, "placement_pools": [ { "key": "default-placement", "val": { "index_pool": ".rgw.buckets.index", "data_pool": ".rgw.buckets"} } ] } ``` > 引自:[Multi-Site](https://docs.ceph.com/en/tentacle/radosgw/multisite/) --- ## 四、同步策略(Multisite Sync Policy) > `.. versionadded:: Octopus` ### 4.1 为什么需要 sync policy | 旧机制 | 新机制 | |--------|--------| | zonegroup 级粗粒度配置(`sync_from*`) | **sync policy** 取代之 | | 所有 bucket 被**对称**对待:每个 data zone 持有该 bucket 的一份镜像,与其他 zone 相同 | bucket 可以**发散(diverge)**:一个 bucket 可以从不同 zone 中**不同名、不同 ID** 的 bucket 拉数据 | | 同步源与同步目的**总是同一个 bucket** | 不再如此 | sync policy 可以配置在 **zonegroup 级**(一旦配置就取代旧式 `sync_from*` 配置),也可以配置在 **bucket 级**。 ### 4.2 三层模型:group / flow / pipe | 层级 | 名称 | 作用 | 可否在 bucket 级定义 | |------|------|------|----------------------| | 第 1 层 | **group**(sync policy group) | 策略容器,带 status;内部含 flow 列表与 pipe 列表 | 可 | | 第 2 层 | **flow**(data-flow) | 定义 zone 之间数据**流动方向** | bucket 策略**继承 zonegroup 的 flow**,只能是 zonegroup flow 的子集 | | 第 3 层 | **pipe** | 定义**实际哪些 bucket** 可以使用这些 flow,以及关联属性(如源对象前缀、tag、目标 owner、storage class 等) | 可 | **flow 的两种类型:** | flow-type | 说明 | 参数 | |-----------|------|------| | `symmetrical` | 对称数据流:一组 zone 之间互相同步 | `--zones=` | | `directional` | 定向数据流:数据从 source zone 单向流向 dest zone | `--source-zone=` `--dest-zone=` | **通配符(wildcard):** - zone 通配符 `*`:匹配所有符合 flow 规则的 zone; - bucket 通配符 `*`:在 bucket policy 上下文中表示**当前 bucket 实例**。 ### 4.3 group 状态机与优先级 一个 sync policy group 可以处于 3 种状态: | 状态值 | 含义 | 说明 | |--------|------|------| | `enabled` | sync is allowed and enabled | 同步**被允许且已启用** | | `allowed` | sync is allowed | 同步**被允许但未启用**(需由 bucket 级策略 enable 才真正同步) | | `forbidden` | sync(按本 group 定义)不被允许,**可以覆盖其他 group** | 优先级最高,用于禁止 | **优先级判定(由高到低):** | 优先级 | 状态 | 效果 | |--------|------|------| | 1(最高) | `forbidden` | 覆盖其他 group 的允许/启用结论 | | 2 | `enabled` | 实际执行同步 | | 3(最低) | `allowed` | 允许但不主动执行;bucket 级可提升为 enabled | > 💡 补充:官方源文给出了三种状态的语义,但**没有给出一张显式的「优先级表」**。上表的优先级顺序由以下源文事实推导:(a) `forbidden` 的描述中明确写了 "can override other groups";(b) `allowed` 的描述是 "sync is allowed" 而 `enabled` 是 "sync is allowed and enabled",且 Example 3 展示了「zonegroup 设为 allowed + bucket 设为 enabled」才真正同步的用法。 **bucket 级策略的继承与限制:** | 维度 | 规则 | |------|------| | data flow | bucket 级策略**继承** zonegroup 策略的 flow,只能是其**子集** | | pipe | 同理,但 bucket 策略**可以 enable 那些在 zonegroup 策略中未 enabled(但并非 forbidden)的 pipe** | > ⚠️ **重要**: > - 对 **zonegroup 策略**的任何修改都必须在 **zonegroup 的 master zone** 上执行,并且需要 `period update --commit`; > - 对 **bucket 策略**的修改也必须在 **zonegroup 的 master zone** 上执行,但**不需要** period update,RGW 会**动态处理**。 **推荐实践(源文建议):** 整 zone 镜像的容灾场景无需在 bucket 上配置任何东西。若要精细控制 bucket 级同步,推荐做法是:在 zonegroup 层用通配符把 pipe 设为 `allowed`,然后在 bucket 层把特定同步设为 `enabled`;必要时再用 bucket 级策略把数据流动限制在特定 zone。 ### 4.4 S3 Replication API | 项 | 说明 | |----|------| | 已实现 | S3 bucket replication API,允许用户在不同 bucket 之间创建复制规则 | | 与 AWS 差异 | AWS 的 replication 允许**同一 zone 内**的 bucket 复制;**RGW 目前不允许** | | RGW 扩展 | RGW 的 API 新增了一个 **`Zone` 数组**,允许用户选择该 bucket 同步到哪些 zone | ### 4.5 配置命令全集 #### 获取策略 ```bash radosgw-admin sync policy get [--bucket=] ``` #### 创建 / 修改 / 查看 / 删除 group ```bash # 创建 radosgw-admin sync group create [--bucket=] \ --group-id= \ --status= # 修改 radosgw-admin sync group modify [--bucket=] \ --group-id= \ --status= # 查看 radosgw-admin sync group get [--bucket=] \ --group-id= # 删除 radosgw-admin sync group remove [--bucket=] \ --group-id= ``` #### 创建 / 删除 flow ```bash # 创建或更新 directional flow radosgw-admin sync group flow create [--bucket=] \ --group-id= \ --flow-id= \ --flow-type=directional \ --source-zone= \ --dest-zone= # 创建或更新 symmetrical flow(zones 为逗号分隔列表) radosgw-admin sync group flow create [--bucket=] \ --group-id= \ --flow-id= \ --flow-type=symmetrical \ --zones= # 删除 directional flow radosgw-admin sync group flow remove [--bucket=] \ --group-id= \ --flow-id= \ --flow-type=directional \ --source-zone= \ --dest-zone= # 从 symmetrical flow 中移除指定 zone radosgw-admin sync group flow remove [--bucket=] \ --group-id= \ --flow-id= \ --flow-type=symmetrical \ --zones= # 删除整个 symmetrical flow radosgw-admin sync group flow remove [--bucket=] \ --group-id= \ --flow-id= \ --flow-type=symmetrical ``` #### 创建 / 删除 pipe ```bash # 创建或更新 pipe 参数 radosgw-admin sync group pipe create [--bucket=] \ --group-id= \ --pipe-id= \ --source-zones= \ [--source-bucket=] \ [--source-bucket-id=] \ --dest-zones= \ [--dest-bucket=] \ [--dest-bucket-id=] \ [--prefix=] \ [--prefix-rm] \ [--tags-add=] \ [--tags-rm=] \ [--dest-owner=] \ [--storage-class=] \ [--mode=] \ [--uid=] # 删除 pipe 的特定参数或整个 pipe radosgw-admin sync group pipe remove [--bucket=] \ --group-id= \ --pipe-id= \ [--source-zones=] \ [--source-bucket=] \ [--source-bucket-id=] \ [--dest-zones=] \ [--dest-bucket=] \ [--dest-bucket-id=] ``` **pipe 参数逐项说明:** | 参数 | 取值 | 说明 | |------|------|------| | `--source-zones` | zone 列表或 `*` | 通配符表示任何符合 flow 规则的 zone | | `--dest-zones` | zone 列表或 `*` | 同上 | | `--source-bucket` | bucket 名或 `*` | 通配符表示当前 bucket(在 bucket policy 上下文中) | | `--dest-bucket` | bucket 名或 `*` | 同上 | | `--source-bucket-id` | bucket ID | 指定源 bucket 的 ID | | `--dest-bucket-id` | bucket ID | 指定目的 bucket 的 ID | | `--prefix` | 字符串 | 过滤源对象前缀 | | `--prefix-rm` | 开关 | 移除已设置的 prefix | | `--tags-add` | `key=value` 逗号列表 | 追加 tag 过滤条件 | | `--tags-rm` | `key=value` 逗号列表 | 移除 tag 过滤条件 | | `--dest-owner` | owner 名 | 强制指定目标对象的 owner;若选择 `user` 模式,**只能设置目标 bucket 的 owner** | | `--storage-class` | storage class 名 | 指定目标对象的 storage class | | `--mode` | `system` \| `user` | 执行同步操作时所使用的模式 | | `--uid` | user id | `user` 模式下以哪个用户身份执行(用于权限校验) | > **prefix 与 tags 可以组合**,此时对象必须**同时满足**两者才会被同步。还可以使用 `priority` 参数:当多条规则匹配且源/目的相同时,用它决定采用哪条规则。 #### 查看 sync info ```bash radosgw-admin sync info [--bucket=] \ [--effective-zone-name=] ``` 获取按 sync policy 推导出的**预期同步源与目标**。 输出字段说明: | 字段 | 含义 | |------|------| | `sources` | 本 bucket 期望的同步来源条目 | | `dests` | 本 bucket 期望的同步目标条目 | | `hints` | bucket 依赖提示(其他 bucket 的 policy 间接引用了本 bucket) | | `resolved-hints-1` / `resolved-hints` | 已解析的依赖提示(resolved hints) | > 由于 bucket 可以定义「从我这里流向另一个 zone 的另一个 bucket」的策略,创建策略时还会生成一份 **bucket 依赖列表**,作为同步时的提示(hints)。**一个 bucket 引用另一个 bucket 并不意味着真的会同步**——还要看 data flow 是否允许。 ### 4.6 十个官方示例 > 示例环境:3 个 zone —— `us-east`(master zone)、`us-west`、`us-west-2`。 #### 示例 1:两 zone 完全镜像 ```bash radosgw-admin sync group create --group-id=group1 --status=allowed radosgw-admin sync group flow create --group-id=group1 \ --flow-id=flow-mirror --flow-type=symmetrical \ --zones=us-east,us-west radosgw-admin sync group pipe create --group-id=group1 \ --pipe-id=pipe1 --source-zones='*' \ --source-bucket='*' --dest-zones='*' \ --dest-bucket='*' radosgw-admin sync group modify --group-id=group1 --status=enabled radosgw-admin period update --commit radosgw-admin sync info --bucket=buck ``` 在 `us-east` 上查看: ```json { "sources": [ { "id": "pipe1", "source": { "zone": "us-west", "bucket": "buck:115b12b3-....4409.1" }, "dest": { "zone": "us-east", "bucket": "buck:115b12b3-....4409.1" }, "params": { } } ], "dests": [ { "id": "pipe1", "source": { "zone": "us-east", "bucket": "buck:115b12b3-....4409.1" }, "dest": { "zone": "us-west", "bucket": "buck:115b12b3-....4409.1" } } ] } ``` > 输出中的 `"id"` 字段反映的是**生成该条目的 pipe 规则**;单条规则可以生成多个同步条目。 在 `us-west` 上查看时,source/dest 方向相反(sources 为 us-east→us-west,dests 为 us-west→us-east)。 #### 示例 2:定向 —— 整个 zone 备份 新增第三个 zone `us-west-2`,作为 `us-west` 的副本,但数据不回流: ```bash radosgw-admin sync group flow create --group-id=group1 \ --flow-id=us-west-backup --flow-type=directional \ --source-zone=us-west --dest-zone=us-west-2 radosgw-admin period update --commit ``` - `us-west` 的 `sync info` 会有**两个 dests**(`us-east` 与 `us-west-2`); - `us-west-2` 只有 sources,**`dests` 为空数组**。 #### 示例 3:只镜像特定 bucket 先把 zonegroup 组改为 `allowed`(允许但未启用): ```bash radosgw-admin sync group modify --group-id=group1 --status=allowed radosgw-admin period update --commit ``` 再为**已存在**的 bucket `buck2` 创建 bucket 级策略: ```bash radosgw-admin sync group create --bucket=buck2 \ --group-id=buck2-default --status=enabled radosgw-admin sync group pipe create --bucket=buck2 \ --group-id=buck2-default --pipe-id=pipe1 \ --source-zones='*' --dest-zones='*' ``` > 注意: > - bucket **必须先存在**才能设置其策略; > - 修改 bucket 策略的管理命令必须在 **master zone** 上运行; > - **不需要** period update; > - 无需改动 data flow——它继承自 zonegroup 策略。 #### 示例 4:把 bucket 同步限制到特定 zone 只把 `buck3` 同步到 `us-east`(从任何 flow 允许同步进 `us-east` 的 zone): ```bash radosgw-admin sync group create --bucket=buck3 \ --group-id=buck3-default --status=enabled radosgw-admin sync group pipe create --bucket=buck3 \ --group-id=buck3-default --pipe-id=pipe1 \ --source-zones='*' --dest-zones=us-east ``` #### 示例 5:从不同的 bucket 同步 > 注意:bucket 同步**目前只在跨 zone 之间生效,同一 zone 内不生效**。 让 `buck4` 从 `buck5` 拉数据: ```bash radosgw-admin sync group create --bucket=buck4 \ --group-id=buck4-default --status=enabled radosgw-admin sync group pipe create --bucket=buck4 \ --group-id=buck4-default --pipe-id=pipe1 \ --source-zones='*' --source-bucket=buck5 \ --dest-zones='*' ``` 限制为只同步源自 `us-west` 的数据: ```bash radosgw-admin sync group pipe modify --bucket=buck4 \ --group-id=buck4-default --pipe-id=pipe1 \ --source-zones=us-west --source-bucket=buck5 \ --dest-zones='*' ``` 在 `us-west` 上查看 `buck5` 的 sync info(注意 `buck5` 自身策略为空,但能**间接发现** `buck4` 在从它同步): ```json { "sources": [], "dests": [], "hints": { "sources": [], "dests": [ "buck4:115b12b3-....14433.2" ] }, "resolved-hints-1": { "sources": [], "dests": [ { "id": "pipe1", "source": { "zone": "us-west", "bucket": "buck5" }, "dest": { "zone": "us-east", "bucket": "buck4:115b12b3-....14433.2" } }, { "id": "pipe1", "source": { "zone": "us-west", "bucket": "buck5" }, "dest": { "zone": "us-west-2", "bucket": "buck4:115b12b3-....14433.2" } } ] }, "resolved-hints": { "sources": [], "dests": [] } } ``` #### 示例 6:同步到不同的 bucket 让 `buck6` 「推」数据到 `buck5`: ```bash radosgw-admin sync group create --bucket=buck6 \ --group-id=buck6-default --status=enabled radosgw-admin sync group pipe create --bucket=buck6 \ --group-id=buck6-default --pipe-id=pipe1 \ --source-zones='*' --source-bucket='*' \ --dest-zones='*' --dest-bucket=buck5 ``` > 通配符 bucket 名在 bucket sync policy 上下文中表示**当前 bucket**。 > 与示例 5 组合后:写数据到 `us-east` 的 `buck6` → 同步到 `us-west` 的 `buck5` → 再分发到 `us-east` 与 `us-west-2` 的 `buck4`。 > 注意:**内部实现上,数据始终是由目的 zone 从源端拉取的(pull)**。 #### 示例 7:源端过滤(prefix / tags) 只同步以 `foo/` 开头的对象(`buck8` → `buck9`): ```bash radosgw-admin sync group create --bucket=buck8 \ --group-id=buck8-default --status=enabled radosgw-admin sync group pipe create --bucket=buck8 \ --group-id=buck8-default --pipe-id=pipe-prefix \ --prefix=foo/ --source-zones='*' --dest-zones='*' \ --dest-bucket=buck9 ``` 同步带 `color=blue` 或 `color=red` tag 的对象: ```bash radosgw-admin sync group pipe create --bucket=buck8 \ --group-id=buck8-default --pipe-id=pipe-tags \ --tags-add=color=blue,color=red --source-zones='*' \ --dest-zones='*' --dest-bucket=buck9 ``` 在 `us-east` 查看 `buck8`: ```json { "sources": [], "dests": [ { "id": "pipe-prefix", "source": { "zone": "us-east", "bucket": "buck8:115b12b3-....14433.5" }, "dest": { "zone": "us-west", "bucket": "buck9" }, "params": { "source": { "filter": { "prefix": "foo/", "tags": [] } } } }, { "id": "pipe-tags", "source": { "zone": "us-east", "bucket": "buck8:115b12b3-....14433.5" }, "dest": { "zone": "us-west", "bucket": "buck9" }, "params": { "source": { "filter": { "tags": [ { "key": "color", "value": "blue" }, { "key": "color", "value": "red" } ] } } } } ] } ``` > 这里没有 sources,只有两个不同的 dests(每条配置一个)。同步发生时,会为每个对象选择匹配的规则。 #### 示例 8:目的端参数 —— storage class ```bash radosgw-admin sync group create --bucket=buck10 \ --group-id=buck10-default --status=enabled radosgw-admin sync group pipe create --bucket=buck10 \ --group-id=buck10-default \ --pipe-id=pipe-storage-class \ --source-zones='*' --dest-zones=us-west-2 \ --storage-class=CHEAP_AND_SLOW ``` #### 示例 9:目的端参数 —— 目标 owner 转换 把目标对象 owner 设为目标 bucket 的 owner(需指定目标 bucket 的 uid): ```bash radosgw-admin sync group create --bucket=buck11 \ --group-id=buck11-default --status=enabled radosgw-admin sync group pipe create --bucket=buck11 \ --group-id=buck11-default --pipe-id=pipe-dest-owner \ --source-zones='*' --dest-zones='*' \ --dest-bucket=buck12 --dest-owner=joe ``` #### 示例 10:目的端参数 —— user mode user mode 会确保该用户既有读源对象的权限、又有写目标 bucket 的权限,因此需要指定执行上下文用户的 uid: ```bash radosgw-admin sync group pipe modify --bucket=buck11 \ --group-id=buck11-default --pipe-id=pipe-dest-owner \ --mode=user --uid=jenny ``` > 引自:[Multisite Sync Policy](https://docs.ceph.com/en/tentacle/radosgw/multisite-sync-policy/) --- ## 五、同步模块(Sync Modules) > `.. versionadded:: Kraken` ### 5.1 概念与原理 | 项 | 说明 | |----|------| | 定位 | Sync Modules 构建在 multisite 框架之上,用于把数据和元数据**转发到外部 tier** | | 触发 | 每当数据发生变化时执行一组动作(元数据操作,如创建 bucket / user,也视为「数据变更」) | | 一致性模型 | 远端站点是**最终一致**的,变更**异步**传播 | | 典型场景 | 把对象存储备份到外部云集群或自定义备份方案(磁带库);把元数据索引到 ElasticSearch | | 配置作用域 | sync module 配置是 **zone 本地的** | | 导出/消费 | sync module 决定该 zone 是**导出数据**,还是**只能消费在其他 zone 上被修改的数据** | **已支持的 sync plugin(自 Luminous):** | plugin | tier-type | 说明 | |--------|-----------|------| | `rgw` | (默认) | **默认** sync plugin,在 zone 之间同步数据。**无需显式配置** | | `elasticsearch` | `elasticsearch` | 把其他 zone 的元数据写入 ElasticSearch | | `log` | `log` | 一个简单(trivial)的 sync plugin,记录远端 zone 中发生的元数据操作 | | `cloud` | `cloud` | 把 zone 数据同步到远端云服务(见 5.4) | | `archive` | `archive` | 归档 zone,保留对象历史版本(见 5.5) | ### 5.2 配置流程(通用) 假设已有 `us-east`、`us-west` 两个 zone,现在要新增第三个 zone `us-east-es`,它只处理来自其他站点的**元数据**(可与 `us-east` 在同一或不同 Ceph 集群),且该 zone 的 RGW **不直接服务终端用户请求**。 ```bash # 1. 创建第三个 zone radosgw-admin zone create --rgw-zonegroup=us --rgw-zone=us-east-es \ --access-key={system-key} --secret={secret} --endpoints=http://rgw-es:80 # 2. 为该 zone 配置 sync module radosgw-admin zone modify --rgw-zone={zone-name} --tier-type={tier-type} --tier-config={set of key=value pairs} # 例如 elasticsearch radosgw-admin zone modify --rgw-zone={zone-name} --tier-type=elasticsearch \ --tier-config=endpoint=http://localhost:9200,num_shards=10,num_replicas=1 # 3. 更新 period radosgw-admin period update --commit # 4. 启动该 zone 的 radosgw systemctl start ceph-radosgw@rgw.`hostname -s` systemctl enable ceph-radosgw@rgw.`hostname -s` ``` ### 5.3 ElasticSearch Sync Module > `.. versionadded:: Kraken` > ⚠️ **注意**:截至 2020 年 5 月 31 日,只支持 **Elasticsearch 6 及以下**,**不支持 Elasticsearch 7**。 #### 写入 ElasticSearch 的元数据格式(自 Luminous 起) ```json { "_index" : "rgw-gold-ee5863d6", "_type" : "object", "_id" : "34137443-8592-48d9-8ca7-160255d52ade.34137.1:object1:null", "_score" : 1.0, "_source" : { "bucket" : "testbucket123", "name" : "object1", "instance" : "null", "versioned_epoch" : 0, "owner" : { "id" : "user1", "display_name" : "user1" }, "permissions" : [ "user1" ], "meta" : { "size" : 712354, "mtime" : "2017-05-04T12:54:16.462Z", "etag" : "7ac66c0f148de9519b8bd264312c4d64" } } } ``` #### ElasticSearch tier-type 配置项 | 配置项 | 类型 | 说明 | |--------|------|------| | `endpoint` | string | 要访问的 Elasticsearch 服务端点 | | `num_shards` | integer | 数据同步初始化时 Elasticsearch 配置的 shard 数。**初始化后不可更改**——任何变更都需要重建 Elasticsearch 索引并重新初始化数据同步流程 | | `num_replicas` | integer | 数据同步初始化时 Elasticsearch 配置的副本数 | | `explicit_custom_meta` | true \| false | 是否索引**所有**用户自定义元数据;若为 false,用户需在 bucket 级配置要索引哪些自定义元数据项。**默认 false** | | `index_buckets_list` | 逗号分隔字符串列表 | 为空则索引所有 bucket;否则只索引此处列出的 bucket。支持前缀(`foo*`)与后缀(`*bar`) | | `approved_owners_list` | 逗号分隔字符串列表 | 为空则(在其他限制之下)索引所有 owner 的 bucket;否则只索引指定 owner 的 bucket。同样支持前缀/后缀 | | `override_index_path` | string | 非空则用作 elasticsearch 索引路径;否则索引路径在同步初始化时自动生成 | #### 终端用户元数据查询 > `.. versionadded:: Luminous` | 关注点 | 说明 | |--------|------| | 安全 | 由于 ES 集群现在存有对象元数据,**ES endpoint 不应暴露给公网**,只能由集群管理员访问 | | 鉴权难题 | 直接让用户查 ES 需要 ES 以类似 RGW 的方式认证用户,这不现实 | | 解决方案 | Luminous 起,**metadata master zone 的 RGW 可以直接服务终端用户请求**,无需暴露 ES endpoint,且由 RGW 完成认证与授权 | | API 位置 | bucket API 中新增查询,**所有请求必须发往 metadata master zone** | **查询语法:** ```text GET /{bucket}?query={query-expr} ``` 请求参数: | 参数 | 说明 | |------|------| | `max-keys` | 返回的最大条目数 | | `marker` | 分页 marker | 表达式语法: ```text expression := [(] [)][ ...] ``` 支持的 `op`:`<`、`<=`、`==`、`>=`、`>` 示例: ```text GET /?query=name==foo ``` 返回用户有读权限、且名为 `foo` 的所有已索引 key。输出为 XML 的 key 列表,类似 S3 list buckets 响应。 **配置自定义元数据字段(mdsearch):** ```text POST /{bucket}?mdsearch x-amz-meta-search: [, ...] ``` - 多个元数据字段用逗号分隔; - 可用 `;` 强制指定类型; - 允许的类型:`string`(默认)、`integer`、`date`。 示例:把 `x-amz-meta-year` 索引为 int,`x-amz-meta-release-date` 索引为 date,`x-amz-meta-title` 索引为 string: ```text POST /mybooks?mdsearch x-amz-meta-search: x-amz-meta-year;int, x-amz-meta-release-date;date, x-amz-meta-title;string ``` | 操作 | 语法 | |------|------| | 删除自定义元数据配置 | `DELETE /?mdsearch` | | 获取自定义元数据配置 | `GET /?mdsearch` | > 引自:[ElasticSearch Sync Module](https://docs.ceph.com/en/tentacle/radosgw/elastic-sync-module/) ### 5.4 Cloud Sync Module > `.. versionadded:: Mimic` | 特性 | 说明 | |------|------| | 作用 | 把 zone 数据同步到远端云服务 | | 方向 | **单向**——数据不会从远端 zone 同步回来 | | 目标 | 支持同时同步到**多个云 provider**;目前支持的 provider 是 **AWS(S3)兼容**的那些 | | 凭据 | 必须配置远端云对象存储服务的用户凭据 | | bucket 映射 | 由于多数云服务限制单用户可创建的 bucket 数,源对象/源 bucket 到目标的映射**可配置**;可为不同 bucket 与 bucket 前缀配置不同目标 | | ACL | **源 ACL 不会被保留**;但可以把特定源用户映射为特定目标用户 | | mtime / ETag | 由于 API 限制,**无法保留原始对象修改时间与 ETag**——cloud sync module 会把它们作为**元数据属性**存放在目标对象上 | #### 简明配置(Trivial Configuration) ```json { "connection": { "access_key": , "secret": , "endpoint": , "host_style": , }, "acls": [ { "type": , "source_id": , "dest_id": } ... ], "target_path": , } ``` #### 完整配置(Non Trivial Configuration) ```json { "default": { "connection": { "access_key": , "secret": , "endpoint": , "host_style": , }, "acls": [ { "type" : , # optional, default is id "source_id": , "dest_id": } ... ], "target_path": # optional }, "connections": [ { "connection_id": , "access_key": , "secret": , "endpoint": , "host_style": , # optional } ... ], "acl_profiles": [ { "acls_id": , # acl mappings "acls": [ { "type": , "source_id": , "dest_id": } ... ], } ], "profiles": [ { "source_bucket": , "connection_id": , "acls_id": , "target_path": , # optional } ... ], } ``` > 注:Trivial 配置可以与 Non-trivial 配置共存。 #### 配置项逐项说明 | 配置项 | 类型 | 说明 | |--------|------|------| | `connection` | container | 到远端云服务的连接,内含 `connection_id`、`access_key`、`secret`、`endpoint`、`host_style` | | `access_key` | string | 该连接使用的远端云 access key | | `secret` | string | 远端云服务的 secret key | | `endpoint` | string | 远端云服务 endpoint 的 URL | | `host_style` | `path` \| `virtual` | 访问远端云 endpoint 时使用的 host style,**默认 `path`** | | `acls` | array | `acl_mappings` 列表 | | `acl_mapping` | container | 每个 `acl_mapping` 含 `type`、`source_id`、`dest_id`,定义对每个对象执行的 ACL 变更(把源 user id 转换为目标 id) | | `type` | `id` \| `email` \| `uri` | `id`=用户 id;`email`=按 email 定义用户;`uri`=按 uri 定义用户(组) | | `source_id` | string | 源 zone 中的用户 ID | | `dest_id` | string | 目标端的用户 ID | | `target_path` | string | 定义目标路径的生成方式:一个前缀,源对象名追加在其后。可包含以下变量 | | `acl_profiles` | array | `acl_profile` 数组 | | `acl_profile` | container | 每个 profile 含 `acls_id`(代表该 profile 的字符串)与 `acls`(`acl_mappings` 列表) | | `profiles` | array | profile 列表,每项含 `source_bucket`、`target_path`、`connection_id`、`acls_id` | | `source_bucket` | string | bucket 名,或以 `*` 结尾的 bucket 前缀,定义该 profile 的源 bucket(s) | **`target_path` 可用变量:** | 变量 | 含义 | |------|------| | `sid` | 代表 sync instance ID 的唯一字符串 | | `zonegroup` | zonegroup 名 | | `zonegroup_id` | zonegroup ID | | `zone` | zone 名 | | `zone_id` | zone id | | `bucket` | 源 bucket 名 | | `owner` | 源 bucket owner ID | 示例:`target_path = rgwx-${zone}-${sid}/${owner}/${bucket}` #### S3 专有配置项 ```json { "multipart_sync_threshold": {object_size}, "multipart_min_part_size": {part_size} } ``` | 配置项 | 类型 | 说明 | |--------|------|------| | `multipart_sync_threshold` | integer | 大于等于该大小的对象将使用 multipart upload 同步到云端 | | `multipart_min_part_size` | integer | 使用 multipart upload 同步对象时的最小 part 大小 | > 上述两项**具体默认值:文档未明确**(源文只给出占位符 `{object_size}` / `{part_size}`,`rgw.yaml.in` 中也未检索到同名条目)。 #### 如何配置 ```bash # 1. 创建新 zone,tier-type 设为 cloud radosgw-admin zone create --rgw-zonegroup={zone-group-name} \ --rgw-zone={zone-name} \ --endpoints={http://fqdn}[,{http://fqdn}] \ --tier-type=cloud # 2. 配置 tier radosgw-admin zone modify --rgw-zonegroup={zone-group-name} \ --rgw-zone={zone-name} \ --tier-config={key}={val}[,{key}={val}] # 嵌套值用「点」访问,例如: radosgw-admin zone modify --rgw-zonegroup={zone-group-name} \ --rgw-zone={zone-name} \ --tier-config=connection.access_key={key},connection.secret={secret} ``` **数组条目访问规则:** - 用方括号指定具体条目,例如 `profiles[0]`; - 用 `[]` **追加**新条目; - 索引 `-1` 引用**最后一个**条目; - **目前无法**在同一条命令中先创建新条目再引用它。 示例:为以 `{prefix}` 开头的 bucket 创建新 profile: ```bash radosgw-admin zone modify --rgw-zonegroup={zone-group-name} \ --rgw-zone={zone-name} \ --tier-config=profiles[].source_bucket={prefix}'*' radosgw-admin zone modify --rgw-zonegroup={zone-group-name} \ --rgw-zone={zone-name} \ --tier-config=profiles[-1].connection_id={conn_id},profiles[-1].acls_id={acls_id} ``` 删除条目用 `--tier-config-rm={key}`。 > 引自:[Cloud Sync Module](https://docs.ceph.com/en/tentacle/radosgw/cloud-sync-module/) ### 5.5 Archive Sync Module > `.. versionadded:: Nautilus` | 特性 | 说明 | |------|------| | 原理 | 利用 S3 对象的 **RGW versioning** 特性,维护一个 **archive zone**,捕获对象在其他 zone 中被更新时的**历次版本** | | 删除 | archive zone 中的对象**只能通过 archive zone 关联的网关删除** | | 部署价值 | 若干**非版本化**的 zone 通过各自网关互相复制数据与元数据(mirror 配置)为终端用户提供高可用,而 archive zone 负责捕获数据与元数据的更新 | | 空间收益 | 在 multizone 配置中部署 archive zone,可以在**单个 zone** 中获得 S3 对象历史灵活性,同时省去「版本化 S3 对象在其他 zone 各存一份副本」的空间开销 | #### 配置 ```bash radosgw-admin zone create --rgw-zonegroup={zone-group-name} \ --rgw-zone={zone-name} \ --endpoints={http://fqdn}[,{http://fqdn}] \ --tier-type=archive ``` > 引自:[Archive Sync Module](https://docs.ceph.com/en/tentacle/radosgw/archive-sync-module/) > 引自(本节其余):[Sync Modules](https://docs.ceph.com/en/tentacle/radosgw/sync-modules/) --- ## 六、云分层与云恢复 ### 6.1 Cloud Transition(云分层) #### 概述 | 项 | 说明 | |----|------| | 功能 | 作为 **object lifecycle** 的一部分,把 S3 对象 transition 到远端云服务 | | 方向 | **单向**——数据不能从远端 zone transition 回来 | | 目标 | 支持把数据 transition 到**多个云 provider**;支持 **AWS(S3)兼容**的云 provider | | 实现 | 使用特殊 storage class,tier type 为 **`cloud-s3`** 或 **`cloud-s3-glacier`**,来配置远端云 S3 对象存储服务 | | 定义位置 | 这些 class 定义在 **zonegroup placement target** 之上,且与常规 storage class 不同,**不需要 data pool** | | 凭据 | 必须配置远端云对象存储服务的用户凭据 | | ACL | **源 ACL 不会被保留**;但可把特定源用户映射为特定目标用户 | #### tier-type | tier-type | 说明 | |-----------|------| | `cloud-s3` | 常规 S3 兼容对象存储服务 | | `cloud-s3-glacier` | S3 Glacier 或 Tape 存储服务 | #### Cloud Storage Class Tier Configuration ```json { "access_key": , "secret": , "endpoint": , "region": , "host_style": , "acls": [ { "type": , "source_id": , "dest_id": } ... ], "target_path": , "target_storage_class": , "multipart_sync_threshold": {object_size}, "multipart_min_part_size": {part_size}, "retain_head_object": } ``` | 配置项 | 类型 | 说明 | |--------|------|------| | `access_key` | string | 远端云 S3 access key | | `secret` | string | 远端云 S3 服务的 secret key | | `endpoint` | string | 远端云 S3 服务的 URL | | `region` | string | 远端云 S3 服务的 region 名 | | `host_style` | `path` \| `virtual` | 访问远端云 S3 服务时使用的 host style,**默认 `path`** | | `acls` | array | `acl_mappings` 列表 | | `acl_mapping` | container | 含 `type`、`source_id`、`dest_id`,定义每个对象上的 ACL 变更(把源 userid 转换为目标 userid) | | `type` | `id` \| `email` \| `uri` | `id`=userid;`email`=按 email 定义用户;`uri`=按 uri 定义用户(组) | | `source_id` | string | 源 zone 中的用户 ID | | `dest_id` | string | 目标端的用户 ID | | `target_path` | string | 定义目标路径如何构造:一个前缀,源 `bucket-name/object-name` 追加在其后。**未指定时**生成为 `rgwx-${zonegroup}-${storage-class}-cloud-bucket`。示例:`target_path = rgwx-archive-${zonegroup}/` | | `target_storage_class` | string | 对象 transition 到的目标 storage class。**未指定时** transition 到 `STANDARD` | | `retain_head_object` | `true` \| `false` | `true`:保留已 transition 到云服务的对象的**元数据**;`false`(**默认**):transition 后对象被删除。**对 current 版本化对象该选项被忽略** | | `multipart_sync_threshold` | integer | 大于等于该大小的对象将用 multipart upload transition 到云端 | | `multipart_min_part_size` | integer | 使用 multipart upload transition 对象时的最小 part 大小 | > `multipart_sync_threshold` / `multipart_min_part_size` 的**具体默认值:文档未明确**(源文为占位符)。下文的 `zonegroup placement add` 输出示例里出现过 `33554432`(=32 MiB),但源文未声明这是默认值。 #### 如何配置 > ⚠️ 若此前**未做** Multisite Configuration,系统会为你创建 `default` zone 与 zonegroup,此时对 zone/zonegroup 的修改**必须重启 RGW 守护进程**才生效;若已为 multisite 创建了 realm,则 `radosgw-admin period update --commit` 提交后即生效。 ```bash # 1. 添加 storage class radosgw-admin zonegroup placement add --rgw-zonegroup={zone-group-name} \ --placement-id={placement-id} \ --storage-class={storage-class-name} \ --tier-type=cloud-s3 ``` 示例: ```bash radosgw-admin zonegroup placement add --rgw-zonegroup=default \ --placement-id=default-placement \ --storage-class=CLOUDTIER --tier-type=cloud-s3 ``` 添加后的输出: ```json [ { "key": "default-placement", "val": { "name": "default-placement", "tags": [], "storage_classes": [ "CLOUDTIER", "STANDARD" ], "tier_targets": [ { "key": "CLOUDTIER", "val": { "tier_type": "cloud-s3", "storage_class": "CLOUDTIER", "retain_head_object": "false", "s3": { "endpoint": "", "access_key": "", "secret": "", "host_style": "path", "target_storage_class": "", "target_path": "", "acl_mappings": [], "multipart_sync_threshold": 33554432, "multipart_min_part_size": 33554432 } } } ] } } ] ``` > ⚠️ 一旦创建了 `--tier-type=cloud-s3` 或 `--tier-type=cloud-s3-glacier` 的 storage class,**之后不能改为其他 storage class 类型**。 ```bash # 2. 配置 tier radosgw-admin zonegroup placement modify --rgw-zonegroup={zone-group-name} \ --placement-id={placement-id} \ --storage-class={storage-class-name} \ --tier-config={key}={val}[,{key}={val}] ``` 示例: ```bash radosgw-admin zonegroup placement modify --rgw-zonegroup default \ --placement-id default-placement \ --storage-class CLOUDTIER \ --tier-config=endpoint=http://XX.XX.XX.XX:YY,\ access_key=,secret=, \ multipart_sync_threshold=44432, \ multipart_min_part_size=44432, \ retain_head_object=true ``` **嵌套值访问:** 用「点」`.` 分隔,类似 `jq` 访问 JSON 嵌套字段。注意**该点号仅用于 `--tier-config` 内部的 key 访问**,不要与 realm / zonegroup / zone 的 Ceph RGW 命名模式混淆。 ```bash radosgw-admin zonegroup placement modify --rgw-zonegroup={zone-group-name} \ --placement-id={placement-id} \ --storage-class={storage-class-name} \ --tier-config=acls.source_id=${source-id}, \ acls.dest_id=${dest-id} ``` **数组条目:** 用方括号引用具体条目;用空的 `[]` 追加新条目。 ```bash radosgw-admin zonegroup placement modify --rgw-zonegroup={zone-group-name} \ --placement-id={placement-id} \ --storage-class={storage-class-name} \ --tier-config=acls[].source_id=${source-id}, \ acls[${source-id}].dest_id=${dest-id}, \ acls[${source-id}].type=email ``` **删除条目:** 用 `--tier-config-rm={key}`。 ```bash radosgw-admin zonegroup placement modify --rgw-zonegroup default \ --placement-id default-placement \ --storage-class CLOUDTIER \ --tier-config-rm=acls.source_id=testid radosgw-admin zonegroup placement modify --rgw-zonegroup default \ --placement-id default-placement \ --storage-class CLOUDTIER \ --tier-config-rm=target_path ``` **删除 storage class:** ```bash radosgw-admin zonegroup placement rm --rgw-zonegroup={zone-group-name} \ --placement-id={placement-id} \ --storage-class={storage-class-name} ``` 示例: ```bash radosgw-admin zonegroup placement rm --rgw-zonegroup default \ --placement-id default-placement \ --storage-class CLOUDTIER ``` 删除后: ```json [ { "key": "default-placement", "val": { "name": "default-placement", "tags": [], "storage_classes": [ "STANDARD" ] } } ] ``` #### 对象修改与限制 **在 lifecycle 规则中使用云 storage class:** ```xml .... .... CLOUDTIER ``` > ⚠️ 由于 transition 是单向的,配置 S3 lifecycle 规则时,**云 storage class 应放在所有 transition 目标 storage class 的最后**。transition 到云之后,后续规则(若有)**不再适用**。 **mtime 与 ETag 的保留问题:** 由于 API 限制,无法保留原始对象修改时间与 ETag,它们会作为元数据属性存放在目标对象上: ```text x-amz-meta-rgwx-source: rgw x-amz-meta-rgwx-source-etag: ed076287532e86365e841e92bfc50d8c x-amz-meta-rgwx-source-key: lc.txt x-amz-meta-rgwx-source-mtime: 1608546349.757100363 x-amz-meta-rgwx-versioned-epoch: 0 ``` **两个额外属性(用于让云服务识别来源并映射用户自定义 `x-amz-meta-` 属性):** | 属性 | 取值 | 说明 | |------|------|------| | `x-rgw-cloud` | `true` / `false` | 对象从 RGW transition 而来时**默认为 `true`** | | `x-rgw-cloud-keep-attrs` | `true` / `false` | 默认 `true`:云服务应映射并存储所有 `x-amz-meta-*` 属性,做不到则操作应失败;`false`:云服务可忽略这些属性,只存对象数据 | **`retain_head_object=true` 的效果:** 源对象的元数据会被保留(含更新后的 `storage-class` 与 `object-size`),但对该对象执行 `GET` 仍会失败并返回 **`InvalidObjectState`** 错误。其他针对原始源对象的操作只作用于其元数据条目,不影响已 transition 的对象。 ```text $ s3cmd info s3://bucket/lc.txt s3://bucket/lc.txt (object): File size: 12 Last mod: Mon, 21 Dec 2020 10:25:56 GMT MIME type: text/plain Storage: CLOUDTIER MD5 sum: ed076287532e86365e841e92bfc50d8c SSE: none Policy: none CORS: none ACL: M. Tester: FULL_CONTROL x-amz-meta-s3cmd-attrs: atime:1608466266/ctime:1597606156/gid:0/gname:root/md5:ed076287532e86365e841e92bfc50d8c/mode:33188/mtime:1597605793/uid:0/uname:root $ s3cmd get s3://bucket/lc.txt lc_restore.txt download: 's3://bucket/lc.txt' -> 'lc_restore.txt' [1 of 1] ERROR: S3 error: 403 (InvalidObjectState) ``` **目标对象命名格式(避免跨 bucket 名字冲突 —— 源 bucket 名会作为前缀):** ```text s3:////(-) ``` - 若对象为版本化对象,会追加对象的 `versionid`。 #### 版本化对象(Versioned Objects) 对版本化与锁定对象,采用与 `LifecycleExpiration` 类似的语义: | 情况 | 行为 | |------|------| | 对象是 **current** | transition 到云之后,它被置为 noncurrent,并创建 delete marker | | 对象是 **noncurrent 且被锁定** | **跳过**其 transition | #### 未来工作(官方列出) - 发送 presigned redirect,或对已 transition 到云的对象做 read-through; - 支持 transition 到其他云 provider(如 Azure)。 > 引自:[Cloud Transition](https://docs.ceph.com/en/tentacle/radosgw/cloud-transition/) ### 6.2 Cloud Restore(云恢复) #### 概述与前提 | 项 | 说明 | |----|------| | 功能 | 把已 transition 到远端 S3 endpoint 的对象恢复到**本地 RGW** 部署 | | 支持范围 | 目前支持从 **S3 兼容云服务**恢复 | | 必要前提 | 配置 storage class 时,必须在 `tier-config` 中把 **`retain_head_object` 设为 `true`** | | 恢复方式 1 | 使用 **S3 `RestoreObject`** API。恢复出的副本只在指定的 `days` 天数内保留;**若不提供 `days`,恢复出的副本被视为永久副本(permanent),按常规对象对待** | | 恢复方式 2 | 启用 `allow_read_through` 后,可用 **S3 `GetObject`** API **临时**恢复对象 | #### 恢复相关 tier-config 配置项 | 配置项 | 类型 | 说明 | |--------|------|------| | `restore_storage_class` | string | 对象数据要恢复到的 storage class。**默认 `STANDARD`** | | `allow_read_through` | `true` \| `false` | 若为 true,启用 read-through,可用 `S3 GetObject` API 恢复对象 | | `read_through_restore_days` | integer | 通过 read-through 恢复的对象保留时长。**默认 1 天** | | `glacier_restore_days` | integer | 对象在远端云服务上被恢复(retrieve)后保留的时长 | | `glacier_restore_tier_type` | `Standard` \| `Expedited` | 云服务内部的检索类型,可能对应不同定价。支持 `Standard` 与 `Expedited` | #### 配置示例(含 read-through) ```bash radosgw-admin zonegroup placement modify --rgw-zonegroup default \ --placement-id default-placement \ --storage-class CLOUDTIER \ --tier-config=endpoint=http://XX.XX.XX.XX:YY,\ access_key=,secret=, \ retain_head_object=true, \ restore_storage_class=COLDTIER, \ allow_read_through=true, \ read_through_restore_days=10 ``` #### S3 Glacier 专有配置 要恢复归档在 **S3 Glacier 或 Tape** 云 storage class 中的对象,**必须先在该云服务上把数据 retrieve 出来**,才能被读取并下载回 RGW。为此: 1. storage class 必须以 `--tier-type=cloud-s3-glacier` 配置; 2. 相应设置 `glacier_restore_days` 与 `glacier_restore_tier_type`。 ```bash radosgw-admin zonegroup placement add --rgw-zonegroup=default \ --placement-id=default-placement \ --storage-class=CLOUDTIER-GLACIER --tier-type=cloud-s3-glacier radosgw-admin zonegroup placement modify --rgw-zonegroup default \ --placement-id default-placement \ --storage-class CLOUDTIER \ --tier-config=endpoint=http://XX.XX.XX.XX:YY,\ access_key=XXXXX,secret=YYYYY, \ retain_head_object=true, \ target_storage_class=Glacier, \ ............ \ ............ \ restore_storage_class=COLDTIER, \ glacier_restore_days=2, \ glacier_restore_tier_type=Expedited ``` 配置结果示例: ```json [ { "key": "default-placement", "val": { "name": "default-placement", "tags": [], "storage_classes": [ "CLOUDTIER-GLACIER", "STANDARD" ], "tier_targets": [ { "key": "CLOUDTIER-GLACIER", "val": { "tier_type": "cloud-s3-glacier", "storage_class": "CLOUDTIER-GLACIER", "retain_head_object": "true", "s3": { "endpoint": http://XX.XX.XX.XX:YY, "access_key": "XXXXX", "secret": "YYYYY", "host_style": "path", "target_storage_class": "Glacier" } "allow_read_through": true, "read_through_restore_days": 10, "restore_storage_class": "COLDTIER", "s3-glacier": { "glacier_restore_days": 2 "glacier_restore_tier_type": "Expedited" } } } ] } } ] ``` #### 使用 S3 RestoreObject CLI 恢复 ```bash aws s3api restore-object --bucket \ --key \ [--version-id ] \ --restore-request (structure) { \ Days= \ } ``` > `Days` 是可选的;**不提供则对象被永久恢复**。 **示例 1 —— 恢复 10 天(可指定版本):** ```bash aws s3api restore-object --bucket bucket1 --key doc1.rtf \ [--version-id 3sL4kqtJlcpXroDTDmJ+rmSpXd3dIbrHY+MTRCxf3vjVBH40Nr8X8gdRQBpUMLUo] \ --restore-request Days=10 \ .... ``` 把对象 `doc1.rtf`(可选指定版本)恢复 10 天。 > 这些临时副本的保留期可以**通过重新发起请求并指定新周期来更新**。 **示例 2 —— 永久恢复:** ```bash aws s3api restore-object --bucket bucket1 --key doc2.rtf --restore-request {} .... ``` 把 `doc2.rtf` 永久恢复,之后按常规对象对待。 #### 使用 S3 GetObject CLI 恢复(read-through) 需确保 `allow_read_through` tier-config 选项已启用: ```bash aws s3api get-object --bucket bucket1 --key doc3.rtf .... ``` 会把 `doc3.rtf` 恢复 `read_through_restore_days` 天。 > 若对象恢复耗时过长,上述 CLI 命令**可能超时**。可先确认恢复状态再重新发起命令。 #### 验证恢复状态 **方式 1 —— S3 HeadObject:** 响应中若包含 `x-amz-restore` 头,说明恢复正在进行或已恢复出副本。 ```bash aws s3api head-object --key doc1.rtf --bucket bucket1 .... ``` **方式 2 —— `radosgw-admin`:** ```bash radosgw-admin object stat --bucket bucket1 --object doc1.rtf ``` #### 恢复后对象的属性 | 维度 | 行为 | |------|------| | **Storage** | 对象恢复到 tier-config 中 `restore_storage_class` 配置的 storage class。但按 AWS `restore-object` 语义,**已恢复对象的 storage class 应保持不变**,因此**临时副本**的 `x-amz-storage-class` 仍反映原来的 cloud-tier storage class | | **mtime** | 已 transition 和已恢复对象的 `mtime` **保持不变** | | **Lifecycle** | **临时副本**不会再被 transition 到云;但与 cloud-transitioned 对象一样,可被常规 LC 过期规则或外部 S3 `delete` 请求删除。**永久副本**按常规对象对待,受适用的 LC 策略约束 | | **Replication** | **临时副本不会被复制**,只保留在发起 restore 请求的那个 zone。**永久副本**像其他常规对象一样被复制 | | **Versioned Objects** | 若对象已被 cloud-transition,它处于 non-current 状态。恢复后,**同一个 non-current 对象**会被下载的数据更新,其 `HEAD` 对象也相应被修改 | #### 未来工作(官方列出) - Admin Ops - Notifications > 引自:[Cloud Restore](https://docs.ceph.com/en/tentacle/radosgw/cloud-restore/) --- ## 七、Zone Features ### 7.1 基本概念 | 概念 | 说明 | |------|------| | `supported_features` | 每个 **zone** 列出它支持哪些 feature | | `enabled_features` | 每个 **zonegroup** 列出它启用了哪些 feature | | 启用条件 | 一个 feature 要在 zonegroup 中启用,**必须先被其所有 zone 支持** | | 新建时 | 创建新的 zone 与 zonegroup 时,**所有已知 feature 都被标记为 supported**,其中部分(见下表)**默认 enabled** | | 升级后 | 升级已有 zone 后,**新 feature 必须手工启用** | ### 7.2 支持的 feature | Feature | 引入版本 | 新建时默认 | |---------|----------|-----------| | `resharding` | Reef | **Enabled** | | `compress-encrypted` | Reef | **Disabled** | | `notification_v2` | Squid | **Enabled** | #### `resharding` | 项 | 说明 | |----|------| | 作用 | 允许在 multisite 配置中对 bucket 做 reshard,而**不中断其对象的复制** | | 与 `rgw_dynamic_resharding` 的关系 | 启用 `rgw_dynamic_resharding` 时,它在**每个 zone 上独立运行**,各 zone 可为同一 bucket 选择不同的 shard 数 | | 手工 reshard | 用 `radosgw-admin bucket reshard` 手工 reshard 时,**只修改所在 zone 的 bucket** | | 标记 supported 的前提 | 只有在**该 zone 的所有 RGW 与 OSD 都升级完成后**,才能把该 zone feature 标记为 supported | > ⚠️ 注意:Reef 之前的版本中,multisite 部署**不支持** dynamic resharding。 #### `compress-encrypted` | 项 | 说明 | |----|------| | 作用 | 支持在同一对象上同时使用 **Server-Side Encryption** 与 **Compression**。对象数据在加密**之前**被压缩 | | 历史问题 | Reef 之前,multisite 无法正确复制此类对象,因此**所有 zone 都必须升级到 Reef 或更高版本**才能启用 | | 安全警告 | 压缩率可能泄露加密数据的信息,使攻击者能区分两个同样大小的对象是否包含相同数据。**出于这些安全考虑,该 feature 默认禁用** | #### `notification_v2` | 项 | 说明 | |----|------| | 作用 | 选择启用 bucket notification 与 topic 的新 **"v2" 元数据格式** | | 与 v1 的区别 | v1 不支持 multisite 复制且难以扩展到大量 topic;**v2 支持 multisite 复制,且可扩展到很多 topic** | | 迁移 | 一旦 realm 中**所有 zonegroup** 都启用了该 feature,一个后台进程会把现有的 v1 topic 与 bucket notification 转换为 v2 格式 | ### 7.3 命令 | 操作 | 执行位置 | 命令 | |------|----------|------| | 为 zone 增加 feature 支持 | 包含该 zone 的集群 | `radosgw-admin zone modify --rgw-zone={zone-name} --enable-feature={feature-name}` + `radosgw-admin period update --commit` | | 移除 zone 的 feature 支持 | 包含该 zone 的集群 | `radosgw-admin zone modify --rgw-zone={zone-name} --disable-feature={feature-name}` + `radosgw-admin period update --commit` | | 启用 zonegroup feature | realm 中的**任意**集群 | `radosgw-admin zonegroup modify --rgw-zonegroup={zonegroup-name} --enable-feature={feature-name}` + `radosgw-admin period update --commit` | | 禁用 zonegroup feature | realm 中的**任意**集群 | `radosgw-admin zonegroup modify --rgw-zonegroup={zonegroup-name} --disable-feature={feature-name}` + `radosgw-admin period update --commit` | > ⚠️ `period update` 命令**只在该 zone 属于某个 realm 时才有效**;否则所有 radosgw 都需要重启才能感知变更。 > 引自:[Zone Features](https://docs.ceph.com/en/tentacle/radosgw/zone-features/) --- ## 八、事件通知(Bucket Notifications) > `.. versionadded:: Nautilus` > `.. versionchanged:: Squid`:可通过 `notification_v2` zone feature 启用新的 "v2" Topic 与 Notification 元数据格式。从旧版本升级后启用该 feature 会**触发已有 Topic 与 Notification 元数据的迁移**;全新部署(greenfield)直接使用新格式。新格式允许数据在 zonegroup 内的 zone 之间同步。 ### 8.1 概览 | 项 | 说明 | |----|------| | 作用 | 当 bucket 上发生特定事件时,把信息**发送出 radosgw** | | 支持的 endpoint | **HTTP endpoints**、**AMQP0.9.1 endpoints**、**Kafka endpoints** | | topic | 用户可以创建 topic。topic 实体由名字定义,是 **per-tenant** 的;用户只能把自己拥有的 topic(通过 notification 配置)关联到**自己拥有的 bucket** | | notification 实体 | 必须为特定 bucket 创建 notification 实体才能发送事件通知。可为**部分事件类型**创建,也可为**全部 "Removed" 与 "Created" 事件类型**创建(**这是默认**) | | 过滤 | 可基于 (1) key、(2) 对象上附带的 metadata 属性、(3) 对象 tag 的**前缀与后缀**匹配来过滤事件;也可对这些使用**正则表达式**匹配 | | 关系 | notification 与 topic 是**多对多**关系:一个 topic 可接收多个 notification;一个 notification 也可投递到多个 topic | | 前置条件 | 要启用 bucket notifications API,`rgw_enable_apis` 配置参数中必须包含 `"notifications"` | > 默认值取自 src/common/options/*.yaml.in @ v20.2.4:`rgw_enable_apis` = `s3, s3website, swift, swift_auth, admin, sts, iam, notifications`(str)。 ### 8.2 通知可靠性:同步 vs 异步 #### 同步通知(Synchronous) | 特性 | 说明 | |------|------| | 时机 | 作为触发它的操作的一部分发送 | | ack 时机 | 只有**通知被发送到 topic 配置的 endpoint** 之后,操作才被确认(acked) | | 延迟 | 通知的往返时间(发送到 endpoint + 收到确认)**叠加到操作本身的延迟上** | | 失败处理 | ⚠️ **即使通知失败、无法投递或超时,原始的触发操作仍被视为成功** | #### 异步通知(Asynchronous / Persistent) | 特性 | 说明 | |------|------| | 时机 | 通知先被提交到**持久化存储**,之后异步发送到 topic 配置的 endpoint | | 提交条件 | **只有当触发操作成功时**,通知才会被提交到持久化存储 | | 延迟 | 只增加「通知提交到持久化存储」的延迟 | | 队列打满 | 若 topic 的 endpoint 长时间不可用,为该 topic 分配的持久化存储**最终会被填满**。此时触发操作会以 **`503 Service Unavailable`** 失败,告知客户端可以稍后重试 | | 失败处理 | 若通知出错、无法投递或超时,会**一直重试直到被成功确认** | | 重试控制 | 可用 `time_to_live` / `max_retries` 设置时间/重试上限;用 `retry_sleep_duration` 控制重试频率 | > 💡 建议:为把异步通知带来的延迟降到最低,建议把 **"log" pool 放在快速介质上**。 ### 8.3 Topic 管理 CLI | 操作 | 命令 | |------|------| | 列出(可按 tenant / uid) | `radosgw-admin topic list [--tenant={tenant}] [--uid={user}]` | | 获取指定 topic | `radosgw-admin topic get --topic={topic-name} [--tenant={tenant}]` | | 删除 topic | `radosgw-admin topic rm --topic={topic-name} [--tenant={tenant}]` | | 查看持久化 topic 统计(reservations / entries / size) | `radosgw-admin topic stats --topic={topic-name} [--tenant={tenant}]` | | 以 JSON dump 某个持久化 topic 的所有待发通知 | `radosgw-admin topic dump --topic={topic-name} [--tenant={tenant}] [--max-entries={max-entries}]` | ### 8.4 通知性能统计指标 | 指标 | 含义 | |------|------| | `persistent_topic_size` | 队列大小(字节) | | `persistent_topic_len` | 当前在队列中等待的通知条数 | | `pubsub_push_ok` | 所有通知中**成功推送**到 endpoint 的事件的累计计数器 | | `pubsub_push_fail` | 所有通知中**推送失败**的事件的累计计数器 | | `pubsub_push_pending` | 已推送到 endpoint 但**尚未 ack 或 nack** 的事件数(gauge)。**不包含**持久化队列中等待的通知,只统计在持久化与非持久化两种情况下**正在飞行中**的通知 | > ⚠️ `pubsub_event_lost` 是按**每个 notification 的每个 event** 递增的;而 `pubsub_push_ok` 与 `pubsub_push_fail` 是按**每个 notification 的每次 push 动作**递增的。 ### 8.5 Bucket Notification REST API —— Topic > 所有 topic 操作中,参数都经过 URL 编码,并以 `application/x-www-form-urlencoded` 作为 content type 放在**消息体**中发送。 #### Create a Topic ```text POST Action=CreateTopic &Name= [&Attributes.entry.1.key=amqp-exchange&Attributes.entry.1.value=] [&Attributes.entry.2.key=amqp-ack-level&Attributes.entry.2.value=none|broker|routable] [&Attributes.entry.3.key=verify-ssl&Attributes.entry.3.value=true|false] [&Attributes.entry.4.key=kafka-ack-level&Attributes.entry.4.value=none|broker] [&Attributes.entry.5.key=use-ssl&Attributes.entry.5.value=true|false] [&Attributes.entry.6.key=ca-location&Attributes.entry.6.value=] [&Attributes.entry.7.key=OpaqueData&Attributes.entry.7.value=] [&Attributes.entry.8.key=push-endpoint&Attributes.entry.8.value=] [&Attributes.entry.9.key=persistent&Attributes.entry.9.value=true|false] [&Attributes.entry.10.key=cloudevents&Attributes.entry.10.value=true|false] [&Attributes.entry.11.key=mechanism&Attributes.entry.11.value=] [&Attributes.entry.12.key=time_to_live&Attributes.entry.12.value=] [&Attributes.entry.13.key=max_retries&Attributes.entry.13.value=] [&Attributes.entry.14.key=retry_sleep_duration&Attributes.entry.14.value=] [&Attributes.entry.15.key=Policy&Attributes.entry.15.value=] [&Attributes.entry.16.key=user-name&Attributes.entry.16.value=] [&Attributes.entry.17.key=password&Attributes.entry.17.value=] [&Attributes.entry.18.key=kafka-brokers&Attributes.entry.18.value=] [&Attributes.entry.19.key=ssl-certificate-location&Attributes.entry.19.value=] [&Attributes.entry.20.key=ssl-key-location&Attributes.entry.20.value=] [&Attributes.entry.21.key=ssl-key-password&Attributes.entry.21.value=] ``` > 💡 提示:更新 topic 使用与创建**相同的命令**(但使用已存在 topic 的名字和不同的 endpoint 值)。 > ⚠️ 任何**已与该 topic 关联的 notification 必须重新创建**,topic 才会更新。 **请求参数逐项说明:** | 参数 | 说明 | |------|------| | `push-endpoint` | 发送推送通知的 endpoint URI | | `OpaqueData` | 在 topic 配置中设置的透传数据,会添加到由该 topic 触发的**所有**通知中 | | `persistent` | 到该 endpoint 的通知是否持久化(=异步)。**默认 `false`** | | `time_to_live` | 保留通知的时间上限(秒)。默认取自 `rgw_topic_persistency_time_to_live`;提供该值则覆盖全局值;**0 表示无限期** | | `max_retries` | 通知过期前的最大重试次数。默认取自 `rgw_topic_persistency_max_retries`;提供该值则覆盖全局值;**0 表示无限重试** | | `retry_sleep_duration` | 控制重试通知的频率。默认取自 `rgw_topic_persistency_sleep_duration`;提供该值则覆盖全局值;**0 表示重试之间没有延迟** | | `Policy` | 控制除 topic owner 之外谁可以访问该 topic。传入的 policy 需为类似 bucket policy 的 **JSON 字符串** | **Policy 示例:** ```json { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam::usfolks:user/fred:subuser"]}, "Action": ["sns:GetTopicAttributes","sns:Publish"], "Resource": ["arn:aws:sns:default::mytopic"], }] } ``` **Policy 中目前支持的 action:** | Action | 含义 | |--------|------| | `sns:GetTopicAttributes` | 列出或获取已有 topic | | `sns:SetTopicAttributes` | 为已有 topic 设置属性 | | `sns:DeleteTopic` | 删除已有 topic | | `sns:Publish` | 能够在已有 topic 上创建/订阅 notification | **相关配置项默认值(默认值取自 src/common/options/*.yaml.in @ v20.2.4):** | 配置项 | 类型 | 默认值 | |--------|------|--------| | `rgw_topic_persistency_time_to_live` | uint | **0**(保留持久化 topic 的时间,秒) | | `rgw_topic_persistency_max_retries` | uint | **0**(持久通知的最大发送尝试次数;1 表示不重试,0 表示无限重试) | | `rgw_topic_persistency_sleep_duration` | uint | **0**(同一持久通知两次尝试之间的最小间隔秒数;实际间隔可能更长) | | `rgw_allow_notification_secrets_in_cleartext` | bool | **false**(允许通过非加密 HTTP 消息发送密码等 secret) | #### HTTP endpoint | 属性 | 说明 | |------|------| | URI | `http[s]://[::@][:][/]` | | user/password | 默认 `guest`/`guest`。**必须只在 HTTPS 上提供**,否则 topic 创建请求会被拒绝 | | port | 非加密连接默认 **5672**,SSL 加密连接默认 **5671** | | vhost | 默认 `/` | | `verify-ssl` | 客户端是否校验服务端证书。**默认 `true`** | | `ca-location` | 若提供且使用安全连接,将用指定的 CA 认证 broker,**不使用默认 CA** | | `amqp-exchange` | **必填**。exchange 必须已存在,且必须能基于 topic 路由消息 | | `amqp-ack-level` | 不需要 end2end acking;消息在投递到最终目的地前可能驻留在 broker 中。三种方式见下表 | **`amqp-ack-level` 取值:** | 取值 | 含义 | |------|------| | `none` | 消息只要**发到 broker** 就算 "delivered" | | `broker` | 消息被 **broker ack** 才算 "delivered"(**默认**) | | `routable` | broker **能路由到 consumer** 才算 "delivered" | > 💡 提示:topic-name 会被用作 AMQP topic(topic exchange 的 "routing key")。 #### Kafka endpoint | 属性 | 说明 | |------|------| | URI | `kafka://[:@][: ``` **Topic ARN 格式:** ```text arn:aws:sns::: ``` #### Get Topic Attributes ```text POST Action=GetTopicAttributes &TopicArn= ``` 响应: ```xml User Name EndPoint TopicArn OpaqueData ``` | 字段 | 说明 | |------|------| | `User` | 创建该 topic 的用户名 | | `Name` | topic 名 | | `EndPoint` | JSON 格式的 endpoint 参数,包含下列子字段 | | ↳ `EndpointAddress` | push-endpoint URL | | ↳ `EndpointArgs` | push-endpoint 参数 | | ↳ `EndpointTopic` | 发送到 endpoint 的 topic 名(可与上面的 topic 名不同) | | ↳ `HasStoredSecret` | endpoint URL 是否含 user/password 信息。为 `true` 时请求**必须走 HTTPS**,否则 "topic get" 请求会被拒绝 | | ↳ `Persistent` | topic 是否为持久化 topic | | ↳ `TimeToLive` | 保留通知的时间上限(秒) | | ↳ `MaxRetries` | 通知过期前的最大重试次数 | | ↳ `RetrySleepDuration` | 重试通知的频率控制 | | `TopicArn` | topic 的 ARN | | `OpaqueData` | topic 上设置的透传数据 | | `Policy` | topic 上设置的任何访问权限 | #### Get Topic Information(已废弃) ```text POST Action=GetTopic &TopicArn= ``` > 该 API **现已废弃(deprecated)**,建议使用 AWS 兼容的 `GetTopicAttributes` API。 响应: ```xml ``` #### Delete Topic ```text POST Action=DeleteTopic &TopicArn= ``` > ⚠️ 删除一个**未知**的 notification(例如重复删除)**不被视为错误**。 > ⚠️ 删除 topic **不会自动删除**与之关联的所有 notification。 ```xml ``` #### List Topics ```text POST Action=ListTopics ``` ```xml ``` > 若 topic 中任何部分的 endpoint URL 含 user/password 信息,请求**必须走 HTTPS**,否则 "topic list" 请求会被拒绝。 #### Set Topic Attributes ```text POST Action=SetTopicAttributes &TopicArn=&AttributeName=&AttributeValue= ``` > - 传入的 `AttributeName` 会被更新;若不存在则**被创建**; > - 传入**任何不受支持的 `AttributeName` 会导致 400 错误**。 **有效的 `AttributeName` 列表:** | AttributeName | 说明 | |---------------|------| | `push-endpoint` | 发送推送通知的 endpoint URI | | `OpaqueData` | 透传数据,添加到该 topic 触发的所有通知 | | `persistent` | 是否持久化(=异步)。**默认 `false`** | | `time_to_live` | 保留通知的时间上限(秒) | | `max_retries` | 通知过期前的最大重试次数 | | `retry_sleep_duration` | 控制重试通知的频率 | | `Policy` | 控制除 owner 外谁可访问该 topic | | `verify-ssl` | 客户端是否必须校验服务端证书。**默认 `true`** | | `use-ssl` | 是否使用安全连接连到 broker。**默认 `false`** | | `cloudevents` | HTTP 头是否按 S3 CloudEvents Spec 包含属性 | | `amqp-exchange` | exchange 必须已存在且能基于 topic 路由消息 | | `amqp-ack-level` | AMQP ack 级别(无需 end2end 确认) | | `ca-location` | 指定 CA 认证 broker | | `mechanism` | 与 user/password 一起提供(**默认 `PLAIN`**) | | `kafka-ack-level` | Kafka ack 级别(无需 end2end 确认) | | `kafka-brokers` | 以逗号分隔的 host 或 host:port(**默认端口 9092**)列表设置 endpoint | | `ssl-certificate-location` | mTLS 用的 PEM 客户端证书路径,必须与 `ssl-key-location` 一起提供,只给一个会导致连接失败 | | `ssl-key-location` | 与客户端证书对应的 PEM 私钥路径,必须与 `ssl-certificate-location` 一起提供 | | `ssl-key-password` | 加密私钥的密码(可选) | ### 8.6 Notification 与 Events Notification 的详细定义位于官方 `Bucket Operations` 页面(不在本册范围内)。两点注意事项: - **"Abort Multipart Upload" 请求不会发出 notification**; - **"Initiate Multipart Upload" 与 "POST Object" 请求都会发出 `s3:ObjectCreated:Post` notification**。 **事件为 JSON 格式**(与实际 endpoint 类型无关),且与 S3 兼容: ```json {"Records":[ { "eventVersion":"2.1", "eventSource":"ceph:s3", "awsRegion":"zonegroup1", "eventTime":"2019-11-22T13:47:35.124724Z", "eventName":"ObjectCreated:Put", "userIdentity":{ "principalId":"tester" }, "requestParameters":{ "sourceIPAddress":"" }, "responseElements":{ "x-amz-request-id":"503a4c37-85eb-47cd-8681-2817e80b4281.5330.903595", "x-amz-id-2":"14d2-zone1-zonegroup1" }, "s3":{ "s3SchemaVersion":"1.0", "configurationId":"mynotif1", "bucket":{ "name":"mybucket1", "ownerIdentity":{ "principalId":"tester" }, "arn":"arn:aws:s3:zonegroup1::mybucket1", "id":"503a4c37-85eb-47cd-8681-2817e80b4281.5332.38" }, "object":{ "key":"myimage1.jpg", "size":"1024", "eTag":"37b51d194a7513e45b56f6524f2d51f2", "versionId":"", "sequencer": "F7E6D75DC742D108", "metadata":[], "tags":[] } }, "eventId":"", "opaqueData":"me@example.com" } ]} ``` **事件字段逐项说明:** | 字段 | 说明 | |------|------| | `awsRegion` | zonegroup | | `eventTime` | 事件被触发的时间戳 | | `eventName` | 支持的事件见 S3 Notification Compatibility。**注意 eventName 值不带 `s3:` 前缀** | | `userIdentity.principalId` | 触发变更的用户 | | `requestParameters.sourceIPAddress` | **不支持** | | `responseElements.x-amz-request-id` | 原始变更请求的 request ID | | `responseElements.x_amz_id_2` | 发生变更的 RGW | | `s3.configurationId` | 产生该事件的 notification ID | | `s3.bucket.name` | bucket 名 | | `s3.bucket.ownerIdentity.principalId` | bucket 的 owner | | `s3.bucket.arn` | bucket 的 ARN | | `s3.bucket.id` | bucket 的 ID(**这是 S3 notification API 的扩展**) | | `s3.object.key` | 对象 key | | `s3.object.size` | 对象大小 | | `s3.object.eTag` | 对象的 etag | | `s3.object.versionId` | 对象版本(若 bucket 版本化)。执行 copy 时包含目标对象的版本;创建 delete marker 时包含 delete marker 的版本 | | `s3.object.sequencer` | “每个对象的变更”的单调递增标识符(十六进制格式) | | `s3.object.metadata` | 对象上设置的、以 `x-amz-meta-` 发送的任何 metadata(**S3 notification API 的扩展**) | | `s3.object.tags` | 对象上设置的任何 tag(**S3 notification API 的扩展**) | | `s3.eventId` | 事件的唯一 ID,可用于 acking(**S3 notification API 的扩展**) | | `s3.opaqueData` | topic 配置中设置的 "opaque data",添加到该 topic 触发的所有通知中(**S3 notification API 的扩展**) | > 引自:[Bucket Notifications](https://docs.ceph.com/en/tentacle/radosgw/notifications/) --- ## 九、S3 事件通知兼容性 ### 9.1 支持的目的端(Destination) | 平台 | 支持的目的端 | |------|-------------| | **AWS** | **SNS**、**SQS**、**Lambda**(AWS 内部目的端) | | **Ceph** | **HTTP/S**、**Kafka**、**AMQP** | > Ceph 使用 **SNS ARN** 来表示 HTTP/S、Kafka 和 AMQP 目的端。 ### 9.2 Notification Configuration XML 差异 以下标签(及其内部标签)**不被支持**: | 标签 | 说明 | |------|------| | `` | 不需要,Ceph 把所有目的端都当作 SNS | | `` | 不需要,Ceph 把所有目的端都当作 SNS | ### 9.3 REST API 扩展 | 扩展 | 说明 | |------|------| | 删除特定 notification 或 bucket 上的所有 notification(用 `DELETE` 动词) | 在 S3 中,只有在 bucket 被删除、或在 bucket 上设置了空 notification 时,所有 notification 才会被删除 | | 获取**特定** notification 的信息(当 bucket 上存在多个时) | 在 S3 中只能一次获取 bucket 上的所有 notification | | 除基于对象 key 前缀/后缀过滤之外,还支持: | (a) 基于**正则表达式**匹配过滤;(b) 基于对象上附加的 **metadata 属性**过滤;(c) 基于**对象 tag** 过滤 | | 上述每一种额外过滤都是对 S3 API 的扩展 | 使用它需要扩展客户端 SDK(除非你用的是纯 HTTP) | | 允许过滤重叠 | 同一个事件可以作为不同的 notification 被发送 | ### 9.4 事件记录中不支持的字段 事件记录遵循 AWS `Event Message Structure` 的格式,但 **`requestParameters.sourceIPAddress` 字段会被发送为空**。 ### 9.5 事件类型(Event Types)全表 | Event | 说明 | |-------|------| | `s3:ObjectCreated:*` | Supported | | `s3:ObjectCreated:Put` | Supported | | `s3:ObjectCreated:Post` | Supported | | `s3:ObjectCreated:Copy` | Supported | | `s3:ObjectCreated:CompleteMultipartUpload` | Supported | | `s3:ObjectRemoved:*` | Supported | | `s3:ObjectRemoved:Delete` | Supported | | `s3:ObjectRemoved:DeleteMarkerCreated` | Supported | | `s3:ObjectLifecycle:Expiration:Current` | **Ceph extension** | | `s3:ObjectLifecycle:Expiration:NonCurrent` | **Ceph extension** | | `s3:ObjectLifecycle:Expiration:DeleteMarker` | **Ceph extension** | | `s3:ObjectLifecycle:Expiration:AbortMultipartUpload` | **Ceph extension** | | `s3:ObjectLifecycle:Transition:Current` | **Ceph extension** | | `s3:ObjectLifecycle:Transition:NonCurrent` | **Ceph extension** | | `s3:LifecycleExpiration:*` | Supported。等价于 `s3:LifecycleExpiration:Delete` + `s3:LifecycleExpiration:DeleteMarkerCreated` | | `s3:LifecycleExpiration:Delete` | Supported。等价于 `s3:ObjectLifecycle:Expiration:Current` | | `s3:LifecycleExpiration:DeleteMarkerCreated` | Supported。等价于 `s3:ObjectLifecycle:Expiration:DeleteMarker` | | `s3:LifecycleTransition` | Supported。等价于 `s3:ObjectLifecycle:Transition:Current` | | `s3:ObjectSynced:*` | **Ceph extension** | | `s3:ObjectSynced:Create` | **Ceph extension** | | `s3:ObjectSynced:Delete` | **Ceph extension** | | `s3:ObjectSynced:DeletionMarkerCreated` | 已定义,**Ceph extension(不会产生)** | | `s3:Replication:*` | Supported。等价于 `s3:ObjectSynced:Create` + `s3:ObjectSynced:Delete` | | `s3:Replication:Create` | Supported。等价于 `s3:ObjectSynced:Create` | | `s3:Replication:Delete` | Supported。等价于 `s3:ObjectSynced:Delete` | | `s3:Replication:DeletionMarkerCreated` | 已定义,Supported(**不会产生**) | | `s3:ObjectRestore:Post` | Not applicable | | `s3:ObjectRestore:Complete` | Not applicable | | `s3:ReducedRedundancyLostObject` | Not applicable | > ⚠️ 注意: > - `s3:ObjectRemoved:DeleteMarkerCreated` 事件呈现的是**对象最新版本**的信息; > - 对于 multipart upload,流程结束时会发送 `ObjectCreated:CompleteMultipartUpload` notification; > - `s3:ObjectSynced:Create` 事件在对象**成功同步到某个 zone** 时发送,**必须为每个 zone 显式设置**。 ### 9.6 Topic 配置兼容性 Ceph 的 topic 管理 API 派生自 AWS Simple Notification Service API。SNS API 的大部分对 Ceph 不适用,**只实现了以下 action**: - `CreateTopic` - `DeleteTopic` - `ListTopics` **Ceph 对 topic 配置的扩展:** | 扩展点 | 说明 | |--------|------| | `GetTopic` | 允许获取**特定** topic,而非该用户的所有 topic | | `CreateTopic` | (1) 允许设置 **endpoint attributes**;(2) 允许设置 **opaque data**,它会被随通知发送到 endpoint | > 引自:[S3 Bucket Notifications Compatibility](https://docs.ceph.com/en/tentacle/radosgw/s3-notification-compatibility/) --- ## 十、桶日志(Bucket Logging) > `.. versionadded:: T` ### 10.1 概述 | 项 | 说明 | |----|------| | 作用 | 记录对 bucket 的**所有访问**。日志数据可用于监控 bucket 活动、检测未授权访问、洞察 bucket 使用情况,也可作为 bucket 变更的 journal | | 存储 | 日志记录以**对象**形式存放在**另一个 bucket** 中,可后续分析 | | 配置层级 | **bucket 级**,可随时启用或禁用 | | 复用 | 一个 log bucket 可以**累积来自多个 bucket** 的日志 | | 建议 | 建议为**每个 bucket 配置不同的 "prefix"**,这样不同 bucket 的日志会存放在 log bucket 中的不同对象里 | **log bucket 的约束:** | 约束 | 说明 | |------|------| | 必须先存在 | 启用 logging 之前 log bucket 必须已创建 | | 不能自记 | log bucket **不能**是被记录 bucket 本身 | | 不能套娃 | log bucket **不能**自己开启 logging | | 不能加密 | log bucket **不能**设置任何加密(**包括 SSE-S3 with AES-256**) | | 不能压缩 | log bucket **不能**设置任何压缩 | | 不能有 RequestPayer | log bucket 不得启用 RequestPayer | | 同 zonegroup | 源 bucket 与 log bucket **必须在同一个 zonegroup** | | 可跨账号 | 源 bucket 与 log bucket **可以属于不同 account**(需设置适当的 bucket policy) | | 可带对象锁 | log bucket **可以**启用 object lock 并带默认保留期 | | 唯一 ID | 日志对象名中 16 字节唯一 ID 部分是**字典序有序的随机字符串**:由 **10 字节计数器** + **6 字节随机字母数字串**组成(若计数器不可用,则为随机字母数字串) | ### 10.2 日志可靠性 | 机制 | 说明 | |------|------| | 延迟可见 | 出于性能考虑,即使日志记录已写入持久化存储,日志对象也要**经过一段可配置的时间**后才会出现在 log bucket 中(或当达到**最大对象大小 128MB** 时) | | 滚动时间 | 该时间(秒)可按源 bucket 通过 Ceph 对 REST API 的扩展设置,也可通过 `rgw_bucket_logging_obj_roll_time` **全局设置**。**未设置时默认 5 分钟** | | 懒惰添加 | 日志对象是「懒」添加到 log bucket 的——若不再有记录写入该对象,即使配置的时间已过,它也可能仍留在 bucket 之外 | | Flush | 可强制把某源 bucket 上所有 logging object 刷出并落盘,无论时间是否到达、是否还有记录写入。**禁用 bucket logging、修改 logging 配置、或删除 bucket 时会自动 flush** | | 异步 vs 立即 | 由写入日志记录触发的添加是**异步**的,触发日志的操作会立即完成、不等待;**flush 时是立即添加**的。这可能导致在待添加对象补齐之前,bucket 内的日志出现**临时空洞** | > 默认值取自 src/common/options/*.yaml.in @ v20.2.4:`rgw_bucket_logging_obj_roll_time` = **300**(uint,秒)。 **查看某个源 bucket 当前待添加到 log bucket 的日志对象:** ```bash radosgw-admin bucket logging list --bucket ``` #### 两种日志类型 | 类型 | 默认 | 写入时机 | 失败影响 | |------|------|----------|----------| | **Standard** | **是(默认)** | 在 bucket 操作**完成之后**写入 log bucket | 日志操作可能失败,但**客户端无感知** | | **Journal** | 否 | 在 bucket 操作**完成之前**写入 log bucket | 若日志操作失败,则**操作不被执行**并向客户端**返回错误**(下表 "Fails Operation" 为 `No` 的操作除外)。支持基于被记录对象 key 的**前缀/后缀**过滤,也可使用**正则表达式** | > ⚠️ Journal 模式下可能出现「日志已成功写入但 bucket 操作失败」的情况,因为日志是先写的。 **Journal 模式支持的操作:** | Operation | Operation Name | Fails Operation | |-----------|----------------|-----------------| | `PutObject` | `REST.PUT.OBJECT` | Yes | | `DeleteObject` | `REST.DELETE.OBJECT` | No | | `DeleteObjects` | `REST.POST.DELETE_MULTI_OBJECT` | No | | `CompleteMultipartUpload` | `REST.POST.UPLOAD` | Yes | | `CopyObject` | `REST.PUT.OBJECT` | Yes | | `PutObjectAcl` | `REST.PUT.ACL` | Yes | | `PutObjectLegalHold` | `REST.PUT.LEGAL_HOLD` | Yes | | `PutObjectRetention` | `REST.PUT.RETENTION` | Yes | | `PutObjectTagging` | `REST.PUT.OBJECT_TAGGING` | Yes | | `DeleteObjectTagging` | `REST.DELETE.OBJECT_TAGGING` | No | #### 多站点(Multisite) 在多 zone 部署中,**每个 zone 使用各自的日志对象**,直到该日志对象被添加到 log bucket。**添加到 log bucket(即 flush)之后,它才会被复制到其他 zone**。这意味着在同一时间段内,可能有**多于一个**日志对象持有相关日志记录。 ### 10.3 Bucket Logging Policy - **只有源 bucket 的 owner**允许启用或禁用 bucket logging; - 一个 bucket 要被用作 log bucket,**必须有允许这件事的 bucket policy**(即使源 bucket 与 log bucket 属于同一用户或 account); - 该 bucket policy 必须允许 `logging.s3.amazonaws.com` 这个 service principal 对 log bucket 执行 **`s3:PutObject`** action; - policy 还应指定预期写入日志的**源 bucket 与 account**。 ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowLoggingFromSourceBucket", "Effect": "Allow", "Principal": { "Service": "logging.s3.amazonaws.com" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::log-bucket-name/prefix*", "Condition": { "StringEquals": { "aws:SourceAccount": "source-account-id" }, "ArnLike": { "aws:SourceArn": "arn:aws:s3:::source-bucket-name" } } } ] } ``` ### 10.4 Bucket Logging Quota | 项 | 说明 | |----|------| | 适用范围 | bucket quota 与 user quota 都会作用到 **log bucket** | | 检查时机 | **每次写入日志记录时**检查配额;在**日志对象被添加到 log bucket 时**更新 | | Journal 模式 | 若配额超限,logging 操作失败,进而**bucket 操作也失败** | | Standard 模式 | logging 操作被跳过,但 **bucket 操作继续** | ### 10.5 日志对象 Key 格式 #### Simple ```text -<16 bytes unique-id> ``` 示例: ```text fish/2024-08-06-09-40-09-0000000002AGQ6W1 ``` #### Partitioned ```text //[tenant:]////-<16 bytes unique-id> ``` 示例: ```text fish/testid/default/fish-bucket/2024/08/06/2024-08-06-10-11-18-0000000011D1FGPA ``` ### 10.6 日志记录格式 日志记录是**空格分隔的字符串列**。 #### Journal 格式(Ceph 扩展,用最少数据做 journaling) | 列序 | 内容 | |------|------| | 1 | bucket owner(为空则为 dash) | | 2 | bucket name(为空则为 dash),格式 `[tenant:]` | | 3 | time,格式 `[day/month/year:hour:minute:second timezone]` | | 4 | operation,格式 `WEBSITE/REST..` | | 5 | object key(为空则为 dash) | | 6 | object size(为空则为 dash) | | 7 | version id(为空则为 dash) | | 8 | eTag(为空则为 dash) | 示例: ```text testid fish [06/Aug/2024:09:40:09 +0000] REST.PUT.OBJECT myfile - 512 4cfdfc1f58e762d3e116787cb92fac60 testid fish [06/Aug/2024:09:40:28 +0000] REST.DELETE.OBJECT myfile - - 4cfdfc1f58e762d3e116787cb92fac60 ``` #### Standard 格式(基于 AWS Logging Record Format) | 列序 | 内容 | |------|------| | 1 | bucket owner(为空则为 dash) | | 2 | bucket name(为空则为 dash),格式 `[tenant:]` | | 3 | time,格式 `[day/month/year:hour:minute:second timezone]`,timezone 为 UTC offset | | 4 | client IP address(为空则为 dash) | | 5 | user 或 account(为空则为 dash) | | 6 | request ID | | 7 | operation,格式 `WEBSITE/REST..` | | 8 | object key(为空则为 dash) | | 9 | request URI,格式 `" "` | | 10 | HTTP status(为 0 则为 dash)。**注意多数情况下日志是在 status 已知之前写入的** | | 11 | error code(为空则为 dash) | | 12 | bytes sent(为 0 则为 dash) | | 13 | object size(为 0 则为 dash) | | 14 | total time(**不支持,始终为 dash**) | | 15 | turnaround time(毫秒) | | 16 | referer(为空则为 dash) | | 17 | user agent(为空则为 dash),**位于双引号内** | | 18 | version id(为空则为 dash) | | 19 | host id,取自 `x-amz-id-2`(为空则为 dash) | | 20 | signature version(为空则为 dash) | | 21 | cipher suite(为空则为 dash) | | 22 | authentication type(常规 auth 为 `AuthHeader`;presigned URL 为 `QueryString`;未认证为 dash) | | 23 | host header(为空则为 dash) | | 24 | TLS version(为空则为 dash) | | 25 | access point ARN(**不支持,始终为 dash**) | | 26 | ACL flag(若授权需要 ACL 则为 `Yes`,否则为 dash) | 示例: ```text testid fish [06/Aug/2024:09:30:25 +0000] - testid 9e369a15-5f43-4f07-b638-de920b22f91b.4179.15085270386962380710 REST.PUT.OBJECT myfile "PUT /fish/myfile HTTP/1.1" 200 - 512 512 - - - - - - - - - localhost - - testid fish [06/Aug/2024:09:30:51 +0000] - testid 9e369a15-5f43-4f07-b638-de920b22f91b.4179.7046073853138417766 REST.GET.OBJECT myfile "GET /fish/myfile HTTP/1.1" 200 - - 512 - - - - - - - - - localhost - - testid fish [06/Aug/2024:09:30:56 +0000] - testid 9e369a15-5f43-4f07-b638-de920b22f91b.4179.10723158448701085570 REST.DELETE.OBJECT myfile "DELETE /fish/myfile1 HTTP/1.1" 200 - - 512 - - - - - - - - - localhost - - ``` > 引自:[Bucket Logging](https://docs.ceph.com/en/tentacle/radosgw/bucket_logging/) --- ## 十一、S3 Select ### 11.1 概述 | 项 | 说明 | |----|------| | 定位 | S3 Select 引擎在**客户端与 Ceph 后端节点之间建立一条高效的管道**,越靠近后端存储效果越好 | | 功能 | 用**类 SQL 语法**从一个 S3 对象中选出受限的数据子集 | | 上层价值 | 便于使用更高层的分析应用(例如 **SPARK-SQL**);只取结构化数据中合适的子集,**降低延迟、提升吞吐** | | 性能特征 | **对象越大、查询越精确,s3select 性能收益越明显** | **示例场景**:用户需要从一个数 GB 大小的 CSV 对象中,取出按另一列过滤后的某一列: ```sql select customer-id from s3Object where age>30 and age<65; ``` 不使用 S3 Select 时,整个 S3 对象必须先经 RGW 从 OSD 取回,再做过滤与抽取;把查询「下推」到 radosgw 可省下大量网络与 CPU 开销。 ### 11.2 基本工作流 | 步骤 | 说明 | |------|------| | 1 | S3 Select 查询通过 `AWS-CLI` 发给 RGW(`select-object-content`) | | 2 | 认证与权限参数随 POST 消息传入。`RGWSelectObj_ObjStore_S3::send_response_data` 是入口点,按输入的对象 key 处理每个取到的 chunk;它也是**第一个处理输入查询**的地方——抽取查询及其他 CLI 参数 | | 3 | RGW 对**每个新取到的 chunk(最大 4 MB)**执行 S3 Select 查询 | | 4 | CSV 行有时会被 chunk 边界「切断」,这些断行(每个 chunk 的首行或末行)在查询处理时被**跳过**;被跳过的断行会被保存,并与下一个断行(属于下一个 chunk)**合并后才处理** | | 5 | 每个处理完的 chunk,按 AWS 规范格式化为输出消息发回客户端 | **支持的响应类型:** ```text {:event-type,records} {:content-type,application/octet-stream} {:message-type,event} ``` > 对于**聚合查询**,最后一个 chunk 应被标识为输入结束。 **基本功能:** s3select 有一套与 AWS 兼容的确定功能集,架构上支持基本算术表达式、逻辑与比较表达式,包括**嵌套函数调用**与**类型转换操作符**。 ### 11.3 错误处理 | 类别 | 说明 | |------|------| | **统一返回** | 检测到错误时,RGW 返回 **400 Bad Request**,并把特定错误消息发回客户端 | | **Syntax error(语法错误)** | s3select parser 拒绝不符合本文档所述语法定义的用户请求。引擎会生成**指出错误位置**的错误消息,RGW 以特定的 error response 发回 | | **Processing Time error(运行时错误)** | 运行时引擎可能检测到只在处理时才出现的错误,用另一类错误消息描述,RGW 同样以特定的 error response 发回 | > ⚠️ **限制/未明确项**:官方源文**没有给出一张完整的错误码表**(只给了 `400 Bad Request` 与一个 `NoSuchKey` 的 XML 响应示例)。其余错误码:**文档未明确**。 ### 11.4 功能支持总表(Features Support) > 目前只实现了 AWS select command 的一部分,下表描述当前实现的功能。 | Feature | Detailed | Example / Description | |---------|----------|------------------------| | Arithmetic operators | `^ * % / + - ( )` | `select (int(_1)+int(_2))*int(_9) from s3object;` | | (同上) | `%` modulo | `select count(*) from s3object where cast(_1 as int)%2 = 0;` | | (同上) | `^` power-of | `select cast(2^10 as int) from s3object;` | | Compare operators | `> < >= <= = !=` | `select _1,_2 from s3object where (int(_1)+int(_3))>int(_5);` | | logical operator | `AND OR NOT` | `select count(*) from s3object where not (int(_1)>123 and int(_5)<200);` | | logical operator | `is null` | 对表达式中的 null 指示返回 true/false | | logical operator | `is not null` | 对表达式中的 null 指示返回 true/false | | logical operator and NULL | unknown state | `select count(*) from s3object where null and (3>2);` → 返回 **0** | | Arithmetic operator with NULL | unknown state | `select count(*) from s3object where (null+1) and (3>2);` → 返回 **0** | | compare with NULL | unknown state | `select count(*) from s3object where (null*1.5) != 3;` → 返回 **0** | | missing column | unknown state | `select count(*) from s3object where _1 is null;` | | 过滤谓词返回非空结果的行 | —— | `select count(*) from s3object where (_1 > 12 and _2 = 0) is not null;` —— 该谓词在 `_1` 或 `_2` 为 null 时返回 null | | projection column | 类似 switch/case default | `select case cast(_1 as int) + 1 when 2 then "a" when 3 then "b" else "c" end from s3object;` | | projection column | 类似 if/then/else | `select case when (1+1=(2+1)*3) then 'case_1' when ((4*3)=(12)) then 'case_2' else 'case_else' end, age*2 from s3object;` | | logical operator | `coalesce {expression,expression ...}`:返回第一个非 null 参数 | `select coalesce(nullif(5,5),nullif(1,1.0),age+12) from s3object;` | | logical operator | `nullif {expr1,expr2}`:两参数相等则返回 null,否则返回第一个 | `select nullif(cast(_1 as int),cast(_2 as int)) from s3object;` | | logical operator | `{expression} in ( .. {expression} ..)` | `select count(*) from s3object where 'ben' in (trim(_5),substring(_1,char_length(_1)-3,3),last_name);` | | logical operator | `{expression} between {expression} and {expression}` | `select count(*) from s3object where substring(_3,char_length(_3),1) between "x" and trim(_1) and substring(_3,char_length(_3)-1,1) = ":";` | | logical operator | `{expression} like {match-pattern}` | `select count(*) from s3object where first_name like '%de_';` / `select count(*) from s3object where _1 like "%a[r-s];` | | logical operator | `{expression} like {match-pattern} escape {char}` | `select count(*) from s3object where "jok_ai" like "%#_ai" escape "#";` | | true / false predicate as a projection | —— | `select (cast(_1 as int)>123 = true) from s3object where address like '%new-york%';` | | an alias to predicate as a projection | —— | `select (_1 like "_3_") as *likealias*,_1 from s3object where *likealias* = true and cast(_1 as int) between 800 and 900;` | | casting operator | `int` | `select cast(123 as int)%2 from s3object;` | | casting operator | `float` | `select cast(123.456 as float)%2 from s3object;` | | casting operator | `string` | `select cast('ABC0-9' as string),cast(substr('ab12cd',3,2) as int)*4 from s3object;` | | casting operator | `bool` | `select cast(5 as bool) from s3object;` | | casting operator | `timestamp` | `select cast(substring('publish on 2007-01-01',12,10) as timestamp) from s3object;` | | **non AWS** casting operator | `int()` | `select int(_1),int( 1.2 + 3.4) from s3object;` | | **non AWS** casting operator | `float()` | `select float(1.2) from s3object;` | | **not AWS** casting operator | `to_timestamp()` | `select to_timestamp('1999-10-10T12:23:44Z') from s3object;` | | Aggregation Function | `sum` | `select sum(int(_1)) from s3object;` | | Aggregation Function | `avg` | `select avg(cast(_1 a float) + cast(_2 as int)) from s3object;` | | Aggregation Function | `min` | `select min( int(_1) * int(_5) ) from s3object;` | | Aggregation Function | `max` | `select max(float(_1)),min(int(_5)) from s3object;` | | Aggregation Function | `count` | `select count(*) from s3object where (int(_1)+int(_3))>int(_5);` | | Timestamp Functions | `extract` | `select count(*) from s3object where extract(year from to_timestamp(_2)) > 1950 and extract(year from to_timestamp(_1)) < 1960;` | | Timestamp Functions | `date_add` | `select count(0) from s3object where date_diff(year,to_timestamp(_1),date_add(day,366,to_timestamp(_1))) = 1;` | | Timestamp Functions | `date_diff` | `select count(0) from s3object where date_diff(month,to_timestamp(_1),to_timestamp(_2)) = 2;` | | Timestamp Functions | `utcnow` | `select count(0) from s3object where date_diff(hours,utcnow(),date_add(day,1,utcnow())) = 24;` | | Timestamp Functions | `to_string` | `select to_string(to_timestamp("2009-09-17T17:56:06.234567Z"),"yyyyMMdd-H:m:s") from s3object;` → 结果 `"20090917-17:56:6"` | | String Functions | `substring` | `select count(0) from s3object where int(substring(_1,1,4))>1950 and int(substring(_1,1,4))<1960;` | | substring with `from` 负数 | 视为 first | `select substring("123456789" from -4) from s3object;` | | substring with `from` 零、`for` 越界 | 合法,等同 (first,last) | `select substring("123456789" from 0 for 100) from s3object;` | | String Functions | `trim`(默认) | `select trim(' foobar ') from s3object;` | | String Functions | `trim(trailing from ...)` | `select trim(trailing from ' foobar ') from s3object;` | | String Functions | `trim(leading from ...)` | `select trim(leading from ' foobar ') from s3object;` | | String Functions | `trim(both '12' from ...)` | `select trim(both '12' from '1112211foobar22211122') from s3objects;` | | String Functions | `lower` / `upper` | `select lower('ABcD12#$e') from s3object;` | | String Functions | `char_length` / `character_length` | `select count(*) from s3object where char_length(_3)=3;` | | Complex queries | —— | `select sum(cast(_1 as int)), max(cast(_3 as int)), substring('abcdefghijklm',(2-1)*3+sum(cast(_1 as int))/sum(cast(_1 as int))+1, (count() + count(0))/count(0)) from s3object;` | | alias support | —— | `select int(_1) as a1, int(_2) as a2 , (a1+a2) as a3 from s3object where a3>100 and a3<300;` | ### 11.5 NULL 处理 **NULL 的定义**:在 ceph-s3select 中,NULL 与其他 DB 系统一样是合法值,表示 **missing / unknown**。 - **任何算术运算中 NULL 都不能产生值**(`a + NULL` 产生 NULL); - **任何与 NULL 的比较结果都是 NULL**(unknown)。 **NULL 真值表:** | A is NULL | Result (NULL=UNKNOWN) | |-----------|----------------------| | `NOT A` | NULL | | `A OR False` | NULL | | `A OR True` | True | | `A OR A` | NULL | | `A AND False` | False | | `A AND True` | NULL | | `A and A` | NULL | ### 11.6 函数接口 #### Timestamp Functions(AWS 规范已**完全实现**) | 函数 | 说明 | |------|------| | `to_timestamp( string )` | 把字符串转换为 timestamp 基本类型。可转换以下格式:`YYYY-MM-DDTHH:mm:ss.SSSSSS+/-HH:mm`、`YYYY-MM-DDTHH:mm:ss.SSSSSSZ`、`YYYY-MM-DDTHH:mm:ss+/-HH:mm`、`YYYY-MM-DDTHH:mm:ssZ`、`YYYY-MM-DDTHH:mm+/-HH:mm`、`YYYY-MM-DDTHH:mmZ`、`YYYY-MM-DDT`、`YYYYT`。**字符串格式中缺失的时间(或部分)用 0 替代;缺失的 month 与 day 默认为 1**。时区部分格式为 `+/-HH:mm` 或 `Z`("Z" 表示 UTC),时区取值范围 **-12:00 ~ +14:00** | | `extract(date-part from timestamp)` | 从输入 timestamp 中抽取 date-part 并以 integer 返回。**支持的 date-part**:`year`、`month`、`week`、`day`、`hour`、`minute`、`second`、`timezone_hour`、`timezone_minute` | | `date_add(date-part, quantity, timestamp)` | 把 quantity(integer)加到 timestamp 的 date-part 上,结果以 timestamp 返回,**计算包含时区**。**支持的 date-part**:`year`、`month`、`day`、`hour`、`minute`、`second` | | `date_diff(date-part, timestamp, timestamp)` | 返回 integer:两个 timestamp 按 date-part 计算的差值,**计算包含时区**。**支持的 date-part**:`year`、`month`、`day`、`hour`、`minute`、`second` | | `utcnow()` | 返回当前时间的 timestamp | | `to_string(timestamp, format_pattern)` | 按给定字符串格式返回输入 timestamp 的字符串表示 | #### `to_string` 格式参数全表 | Format | Example | Description | |--------|---------|-------------| | `yy` | 69 | 2-digit year | | `y` | 1969 | 4-digit year | | `yyyy` | 1969 | Zero-padded 4-digit year | | `M` | 1 | Month of year | | `MM` | 01 | Zero-padded month of year | | `MMM` | Jan | Abbreviated month year name | | `MMMM` | January | Full month of year name | | `MMMMM` | J | Month of year first letter(**注意:不能用于 `to_timestamp` 函数**) | | `d` | 2 | Day of month (1-31) | | `dd` | 02 | Zero-padded day of month (01-31) | | `a` | AM | AM or PM of day | | `h` | 3 | Hour of day (1-12) | | `hh` | 03 | Zero-padded hour of day (01-12) | | `H` | 3 | Hour of day (0-23) | | `HH` | 03 | Zero-padded hour of day (00-23) | | `m` | 4 | Minute of hour (0-59) | | `mm` | 04 | Zero-padded minute of hour (00-59) | | `s` | 5 | Second of minute (0-59) | | `ss` | 05 | Zero-padded second of minute (00-59) | | `S` | 0 | Fraction of second(precision: 0.1,range: 0.0-0.9) | | `SS` | 6 | Fraction of second(precision: 0.01,range: 0.0-0.99) | | `SSS` | 60 | Fraction of second(precision: 0.001,range: 0.0-0.999) | | `SSSSSS` | 60000000 | Fraction of second(maximum precision: 1 nanosecond,range: 0.0-0999999999) | | `n` | 60000000 | Nano of second | | `X` | +07 or Z | Offset in hours or "Z" if the offset is 0 | | `XX` or `XXXX` | +0700 or Z | Offset in hours and minutes or "Z" if the offset is 0 | | `XXX` or `XXXXX` | +07:00 or Z | Offset in hours and minutes or "Z" if the offset is 0 | | `X` | 7 | Offset in hours | | `xx` or `xxxx` | 700 | Offset in hours and minutes | | `xxx` or `xxxxx` | +07:00 | Offset in hours and minutes | #### Aggregation Functions | 函数 | 说明 | |------|------| | `count()` | 按匹配条件(若有)的行数返回 integer | | `sum(expression)` | 对所有匹配条件(若有)的行,返回表达式的汇总值 | | `avg(expression)` | 对所有匹配条件(若有)的行,返回表达式的平均值 | | `max(expression)` | 返回所有匹配条件(若有)的表达式结果中的最大值 | | `min(expression)` | 返回所有匹配条件(若有)的表达式结果中的最小值 | #### String Functions | 函数 | 说明 | |------|------| | `substring(string,from,to)` | 即 `substring( string from start [ for length ] )`:按 from、to 输入从输入字符串中抽取子串。也支持 `substring(string from )` 与 `substring(string from for)` 形式 | | `char_length` | 返回字符串中的字符数(`character_length` 同义) | | `trim` | 即 `trim ( [[leading \| trailing \| both remove_chars] from] string )`:从目标字符串去除 leading/trailing(或 both)字符,**默认为空白字符** | | `upper` / `lower` | 转换为大写 / 小写 | #### SQL LIMIT 操作符 `LIMIT` 用于限制查询处理的行数。**达到用户设置的 limit 后,RGW 停止获取额外的 chunk**。 > ⚠️ 源文中 `LIMIT` 一节标注了 `TODO : add examples, for aggregation and non-aggregation queries.`,即**官方尚未给出 LIMIT 的示例**。 #### Alias(别名) | 特性 | 说明 | |------|------| | 作用 | 别名是 s3select 语言的重要组成部分,在对象列很多或查询很复杂时显著提升可读性 | | 实现 | 解析含 alias 的语句时,会把 alias 替换为对正确 projection column 的引用;执行时该引用与其他表达式一样被求值 | | 循环引用防护 | 存在自引用(或循环引用)导致栈溢出(死循环)的风险,因此在求值 alias 时会**校验是否存在循环引用** | | 结果缓存 | alias 维护一个**结果缓存**,重复使用同一 alias 不会再次求值,而是从缓存返回 | | 缓存失效 | **每换一行,缓存失效**(因为结果可能不同) | #### 测试框架(源文列出 3 类) | 编号 | 方法 | |------|------| | (1) | 与可信引擎对比:C/C++ 编译器是可靠的表达式求值器,且算术/逻辑表达式语法与 s3select 相同,因此框架运行等价表达式并校验结果。有专门的表达式生成器,每个测试会话生成不同的表达式集合 | | (2) | 比较「语法不同但语义等价」的查询结果,验证不同运行时流程在同一随机数据集上产生一致结果。例如在含 1-1000 随机数的 dataset 上,`select count(*) from s3object where char_length(_3)=3;` 与 `select count(*) from s3object where cast(_3 as int)>99 and cast(_3 as int)<1000;` 结果相同 | | (3) | 常量数据集:传统测试方式,查询处理常量数据集,结果与常量结果比对 | #### 额外语法支持 | 特性 | 示例 | |------|------| | 表别名 | `select s._1 from s3object s where s._2 = ‘4’;` | | 大小写不敏感 | `Select SUM(Cast(_1 as int)) FROM S3Object;` | | 结尾分号可选 | `select count(*) from s3object` | ### 11.7 发送查询到 RGW 任何 HTTP 客户端都可向 RGW 发送 `s3-select` 请求,但必须符合 AWS Request syntax。使用 AWS CLI 时需遵循 AWS command reference。 ```bash aws --endpoint-url http://localhost:8000 s3api select-object-content --bucket {BUCKET-NAME} --expression-type 'SQL' --scan-range '{"Start" : 1000, "End" : 1000000}' --input-serialization '{"CSV": {"FieldDelimiter": "," , "QuoteCharacter": "\"" , "RecordDelimiter" : "\n" , "QuoteEscapeCharacter" : "\\" , "FileHeaderInfo": "USE" }, "CompressionType": "NONE"}' --output-serialization '{"CSV": {"FieldDelimiter": ":", "RecordDelimiter":"\t", "QuoteFields": "ALWAYS"}}' --key {OBJECT-NAME} --request-progress '{"Enabled": True}' --expression "select count(0) from s3object where int(_1)<10;" output.csv ``` #### Input Serialization | 参数 | 取值 / 说明 | |------|-------------| | `FileHeaderInfo` | **NONE**:第一行不是 header。**IGNORE**:第一行是 header,但**不能**用 header 值在表达式中指示列;可以用列位置(`_1`、`_2` …)指示列,例如 `SELECT s._1 FROM S3OBJECT s`。**USE**:第一行是 header,可以用 header 值在表达式中标识列,例如 `SELECT column_name FROM S3OBJECT` | | `QuoteEscapeCharacter` | 单个字符,用于在已被转义的值内部转义引号字符 | | `RecordDelimiter` | 单个字符,用于分隔输入中的各条记录;可指定任意分隔符以替代默认值 | | `FieldDelimiter` | 单个字符,用于分隔记录中的各个字段;可指定任意分隔符 | #### Output Serialization(AWS CLI 示例) ```bash aws s3api select-object-content \ --bucket "mybucket" \ --key keyfile1 \ --expression "SELECT * FROM s3object s" \ --expression-type 'SQL' \ --request-progress '{"Enabled": false}' \ --input-serialization '{"CSV": {"FieldDelimiter": ","}, "CompressionType": "NONE"}' \ --output-serialization '{"CSV": {"FieldDelimiter": ":", "RecordDelimiter":"\\t", "QuoteFields": "ALWAYS"}}' /dev/stdout ``` | 参数 | 取值 / 说明 | |------|-------------| | `QuoteFields` | 是否在输出字段周围使用引号。**ALWAYS**:总是使用引号。**ASNEEDED**:仅在需要时使用引号(**未实现**) | | `RecordDelimiter` | 单个字符,用于分隔输出中的各条记录;可指定任意分隔符 | | `FieldDelimiter` | 用于分隔记录中各个字段的值;可指定任意分隔符 | #### Scan Range Option `--scan-range` 让客户端**只扫描和处理对象的选定部分**,跳过不感兴趣的部分,从而减少 I/O 与带宽。 > ⚠️ 源文中该节标注了 `TODO : different data-sources (CSV, JSON, Parquet)`,即不同数据源下的 scan range 行为**文档未明确**。 ### 11.8 CSV 解析行为 s3-select 引擎内含一个 CSV parser,按以下规则解析 s3 对象: | 规则 | 说明 | |------|------| | 行结束 | 每行以 `row-delimiter` 结尾 | | 字段分隔 | `field-separator` 分隔相邻列;**连续的 `field separator` 定义 NULL 列** | | 引号 | `quote-character` **覆盖** `field separator`——引号之间的 `field separator` 被当作普通字符 | | 转义 | `escape character` 使特殊字符失去特殊含义,**但 `row delimiter` 除外** | **CSV 解析规则示例:** | Feature | Description | input ==> tokens | |---------|-------------|------------------| | NULL | 连续的 field delimiter | `,,1,,2,` ==> `{null}{null}{1}{null}{2}{null}` | | QUOTE | quote character 覆盖 field delimiter | `11,22,"a,b,c,d",last` ==> `{11}{22}{"a,b,c,d"}{last}` | | Escape | escape char 覆盖元字符,escape 被移除 | `11,22,str=\"abcd\"\,str2=\"123\",last` ==> `{11}{22}{str="abcd",str2="123"}{last}` | | row delimiter | 无闭合引号时,row delimiter 作为行的结束 | `11,22,a="str,44,55,66` ==> `{11}{22}{a="str,44,55,66}` | | csv header info | `FileHeaderInfo` 标签 | `"USE"`:第一行每个 token 都是列名;`"IGNORE"`:跳过第一行 | ### 11.9 JSON 支持 | 项 | 说明 | |----|------| | 集成方式 | 一个 JSON reader 已集成进 `s3select-engine`,让客户端能用 SQL 语句扫描并抽取 JSON 文档中的信息 | | 架构 | CSV、Parquet、JSON 的 data reader 与 parser **与 SQL 引擎本身分离**,因此这些 reader **共用同一套 SQL 引擎** | | 嵌套 | JSON 文档中的值可以以各种方式嵌套(在 object 或 array 内),object 与 array 可**无限制**互相嵌套 | | 定位 | 用 SQL 查询 JSON 文档中的特定值时,客户端必须在 SELECT 语句中通过**路径(path)**指定该值的位置 | | 行列结构 | SQL 引擎以**行式**处理 SELECT 语句,用语句中指定的列做投影计算,每行包含这些列的值。但**JSON 文档本身没有像 CSV 或 Parquet 那样的行列结构**——查询 JSON 文档时,**是 SQL 语句本身定义了行与列** | | 行边界 | `FROM` 子句定义行边界,类似 CSV 用 row delimiter 定义行、Parquet 用 row group 定义行。语句 `SELECT ... FROM s3object[*].aaa.bb.cc` 指示 reader 搜索路径 `aaa.bb.cc`,并**基于该路径的出现来定义行边界**:reader 遇到该路径时一行开始,退出该路径最内层(此例为 object `cc`)时该行结束 | > ⚠️ **注意(源文原话)**:查询 JSON 文档的语义**可能会变化**,未必与当前描述的方法一致。 > ⚠️ 源文中该节标注了 `TODO : relevant example for object and array values.` **JSON 查询示例:** ```json { "firstName": "Joe", "lastName": "Jackson", "gender": "male", "age": "twenty", "address": { "streetAddress": "101", "city": "San Diego", "state": "CA" }, "firstName": "Joe_2", "lastName": "Jackson_2", "gender": "male", "age": 21, "address": { "streetAddress": "101", "city": "San Diego", "state": "CA" }, "phoneNumbers": [ { "type": "home1", "number": "734928_1","addr": 11 }, { "type": "home2", "number": "734928_2","addr": 22 }, { "type": "home3", "number": "734928_3","addr": 33 }, { "type": "home4", "number": "734928_4","addr": 44 }, { "type": "home5", "number": "734928_5","addr": 55 }, { "type": "home6", "number": "734928_6","addr": 66 }, { "type": "home7", "number": "734928_7","addr": 77 }, { "type": "home8", "number": "734928_8","addr": 88 }, { "type": "home9", "number": "734928_9","addr": 99 }, { "type": "home10", "number": "734928_10","addr": 100 } ], "key_after_array": "XXX", "description" : { "main_desc" : "value_1", "second_desc" : "value_2" } } ``` ```sql -- the from-clause define a single row. -- _1 points to root object level. -- _1.age appears twice in Documnet-row, the last value is used for the operation. select _1.firstname,_1.key_after_array,_1.age+4,_1.description.main_desc,_1.description.second_desc from s3object[*]; -- expected_result = Joe_2,XXX,25,value_1,value_2 ``` ```sql -- the from-clause points the phonenumbers array (it defines the _1) -- each element in phoneNumbers array define a row. -- in this case each element is an object contains 3 keys/values. -- the query "can not access" values outside phonenumbers array, the query can access only values appears on _1.phonenumbers path. select cast(substring(_1.number,1,6) as int) *10 from s3object[*].phonenumbers where _1.type='home2'; -- expected_result = 7349280 ``` ### 11.10 Parquet 支持 | 项 | 状态 | |----|------| | 是否存在 Parquet reader | 源文两处提到 Parquet 的 data reader 与 parser(与 CSV、JSON 并列),说明**存在 Parquet 支持** | | 语法文档 | **文档未明确**——源文未给出任何 Parquet 的查询语法、示例或限制;相关段落标注为 `TODO` | ### 11.11 数据类型 > ⚠️ **文档未明确**:官方 s3select 文档**没有**单独列出一张「支持的数据类型」表。从 cast 与函数描述中可以确定引擎至少处理以下类型: | 类型 | 来源依据 | |------|----------| | `int` / integer | `cast(... as int)`、`int(...)`、`extract` 返回 integer、`date_diff` 返回 integer | | `float` | `cast(... as float)`、`float(...)` | | `string` | `cast(... as string)`、`to_string(...)` | | `bool` | `cast(... as bool)`、true/false predicate | | `timestamp` | `cast(... as timestamp)`、`to_timestamp(...)`、`utcnow()` | | NULL | 独立的 NULL 语义章节(unknown state) | ### 11.12 BOTO3 示例 > 由于 AWS-cli 支持,使用 BOTO3 是「自然且简单」的。 ```python import pprint def run_s3select(bucket,key,query,column_delim=",",row_delim="\n",quot_char='"',esc_char='\\',csv_header_info="NONE"): s3 = boto3.client('s3', endpoint_url=endpoint, aws_access_key_id=access_key, region_name=region_name, aws_secret_access_key=secret_key) result = "" try: r = s3.select_object_content( Bucket=bucket, Key=key, ExpressionType='SQL', InputSerialization = {"CSV": {"RecordDelimiter" : row_delim, "FieldDelimiter" : column_delim,"QuoteEscapeCharacter": esc_char, "QuoteCharacter": quot_char, "FileHeaderInfo": csv_header_info}, "CompressionType": "NONE"}, OutputSerialization = {"CSV": {}}, Expression=query, RequestProgress = {"Enabled": progress}) except ClientError as c: result += str(c) return result for event in r['Payload']: if 'Records' in event: result = "" records = event['Records']['Payload'].decode('utf-8') result += records if 'Progress' in event: print("progress") pprint.pprint(event['Progress'],width=1) if 'Stats' in event: print("Stats") pprint.pprint(event['Stats'],width=1) if 'End' in event: print("End") pprint.pprint(event['End'],width=1) return result run_s3select( "my_bucket", "my_csv_object", "select int(_1) as a1, int(_2) as a2 , (a1+a2) as a3 from s3object where a3>100 and a3<300;") ``` ### 11.13 S3 SELECT 响应 #### Error Response ```xml NoSuchKey The resource you requested does not exist /mybucket/myfoto.jpg 4442587FB7D0A2F9 ``` #### Report Response ```xml HTTP/1.1 200 blob
long long long
long long long
``` #### 消息类型说明 | 消息类型 | 说明 | |----------|------| | **Records message** | 可包含单条记录、部分记录或多条记录。根据结果大小,一个响应可包含一条或多条此类消息 | | **Error message** | 检测到错误时,RGW 返回 **400 Bad Request**,并按错误类型把特定错误消息发回客户端 | | **Continuation message** | Ceph S3 **周期性**发送此消息以保持 TCP 连接打开。这些消息在响应中**随机出现**,客户端必须检测消息类型并作相应处理 | | **Progress message** | 若请求了进度信息,Ceph S3 会周期性发送此消息,包含**已开始但尚未完成**的查询的进度信息 | | **Stats message** | Ceph S3 在**请求结束时**发送此消息,包含查询的统计信息 | | **End message** | 表示请求已完成,且不会再发送消息。**在客户端收到 End message 之前,不应假定请求已完成** | > 引自:[Ceph s3 select](https://docs.ceph.com/en/tentacle/radosgw/s3select/) --- ## 十二、缓存与性能 ### 12.1 RGW Data Caching and CDN(基于 Nginx) > `.. versionadded:: Octopus` #### 原理 | 项 | 说明 | |----|------| | 作用 | 让 RGW 能**安全地缓存对象**,把负载从集群上卸载下来,使用 **Nginx** 实现 | | 缓存时机 | 对象**首次被访问后**会存入 Nginx 缓存目录;数据已缓存时无需再从 RGW 取 | | 权限校验 | 缓存命中时仍会**向 RGW 做一次权限检查**,确保请求用户有访问权限 | | 依赖 | 基于 Nginx 模块 `ngx_http_auth_request_module`、`nginx-aws-auth-module`,以及提供 Lua 能力的 **OpenResty** | | 当前范围 | **仅缓存 AWSv4 请求(仅 S3 请求)**:对第一个 GET 请求 cache-in,后续 GET 请求 cache-out;对 `PUT`、`POST`、`HEAD`、`DELETE`、`COPY` 请求**透明透传** | > 💡 注意:`D3N RGW Data Cache` 是另一种**原生实现在 RADOS Gateway 内部**的数据缓存机制(见 12.2)。 #### 两个新 API | API | 说明 | |-----|------| | **Auth API** | 缓存用它来验证用户能否访问缓存数据。它利用 RGW 对客户端凭据与存储访问策略的认知:若被封装的请求会被授权,则返回成功 | | **Cache API** | 提供安全地覆写 `Range` 头的能力,使 Nginx 能在 S3 之上使用自己的 **smart cache**。使用该 API 可在客户端请求特定 range 时**预读(read ahead)**对象。后续访问已缓存对象时,Nginx 从缓存中满足已缓存 range 的请求;未缓存的 range 则从 RGW 读取(并缓存) | #### 创建 cache 用户 ```bash radosgw-admin user create --uid= --display-name="cache user" --caps="amz-cache=read" ``` 该用户可以向 RGW 发送 Cache API 头 **`X-Amz-Cache`**,其中包含原始请求的头部(在修改 `Range` 头之前): - 各原始头部之间用 **ASCII 码 177(十进制)** 的字符分隔; - 每个原始头部与其值之间用 **ASCII 码 178(十进制)** 的字符分隔。 RGW 会检查该用户是否为已授权用户、且为 cache user。两项检查通过后,RGW 会用 `X-Amz-Cache` 中保存的原始头部**重新校验用户权限**,并在此过程中**覆写 `Range` 头**。 #### 部署步骤 ```bash # 1. 下载 OpenResty 源码 wget https://openresty.org/download/openresty-1.15.8.3.tar.gz # 2. 克隆 Nginx AWS 认证模块 git clone https://github.com/kaltura/nginx-aws-auth-module # 3. 解包 tar xvzf openresty-1.15.8.3.tar.gz cd openresty-1.15.8.3 # 4. 编译(需 pcre、openssl 等库) sudo yum install pcre-devel openssl-devel gcc curl zlib-devel nginx ./configure --add-module= --with-http_auth_request_module --with-http_slice_module --conf-path=/etc/nginx/nginx.conf gmake -j $(nproc) sudo gmake install sudo ln -sf /usr/local/openresty/bin/openresty /usr/bin/nginx ``` **示例配置文件位置**:https://github.com/ceph/ceph/tree/main/examples/rgw/rgw-cache | 文件 | 放置位置 | |------|----------| | `nginx.conf` | `/etc/nginx/nginx.conf` | | `nginx-lua-file.lua` | `/etc/nginx/nginx-lua-file.lua` | | `nginx-default.conf` | `/etc/nginx/conf.d/nginx-default.conf` | **最可能需要按环境调整的参数(位于 `nginx-default.conf`):** ```nginx proxy_cache_path /data/cache levels=2:2:2 keys_zone=mycache:999m max_size=20G inactive=1d use_temp_path=off; ``` ```nginx server rgw1:8000 max_fails=2 fail_timeout=5s; server rgw2:8000 max_fails=2 fail_timeout=5s; server rgw3:8000 max_fails=2 fail_timeout=5s; ``` > ⚠️ 必须用具有 `amz-cache` caps 的用户的 **access key 与 secret key** 替换 `nginx.conf` 中的示例值: ```bash radosgw-admin user create --uid=cacheuser --display-name="cache user" --caps="amz-cache=read" --access-key --secret ``` **可选配置:** | 需求 | 做法 | |------|------| | 使用 Nginx slicing(适合流式场景) | 用 `nginx-slicing.conf` 代替 `nginx-default.conf` | | 不使用 prefetch caching | 用 `nginx-noprefetch.conf` 代替 `nginx-default.conf`。**禁用 prefetch 后,Nginx 会分别缓存每个 range 请求,range 请求之间的重叠部分会被重复从 RGW 取**。例如客户端先请求 0-4095 再请求 0-4096,两个请求都会完整地从 RGW 取 | **启动 Nginx(OpenResty):** ```bash systemctl restart nginx ``` #### 性能附注 在某些场景(例如开发环境)下,**禁用认证**可能显著提升性能,因为它省去了到 RADOS Gateway 的 auth API 调用(是否提升取决于硬件)。做法是注释掉 `nginx-default.conf` 中的这一行: ```nginx #auth_request /authentication; ``` > 引自:[RGW Data Caching and CDN](https://docs.ceph.com/en/tentacle/radosgw/rgw-cache/) ### 12.2 D3N RGW Data Cache #### 原理 | 项 | 说明 | |----|------| | 全称 | **Datacenter-Data-Delivery Network (D3N)** | | 手段 | 使用 **NVMe flash 或 DRAM** 等高速存储,在**访问侧**缓存数据集 | | 收益 | 让大数据作业可以利用每个 Rados Gateway 节点边缘上的算力与快速存储资源 | | 背景 | 很多数据中心有低成本的集中式存储库(**data lake**),存 TB/PB 级数据集;Hadoop、Spark 等分布式大数据分析集群必须访问相对较远的集中式 data lake。即使数据中心网络设计良好,**集群到 data lake 的带宽通常远低于边缘节点本地固态存储的带宽** | | 效果 | D3N 通过加速对 data lake 的**重复读取**来提升大数据作业性能。**Rados Gateways 充当后端对象存储(OSDs)的缓存服务器**,在本地存数据以供复用 | #### 架构 - 缓存服务器位于数据中心中**潜在网络与存储瓶颈的访问侧**; - D3N 的**两层逻辑缓存**构成一个传统的缓存层次结构(**注:目前只有 layer 1 cache 被 upstream**):离客户端越近的缓存访问延迟与开销越低,层次越高(需要多跳访问)的缓存越慢; - **离客户端最近的 layer 1 缓存服务器**处理对象请求时:把请求拆成 block,返回本地已缓存的 block,并把未命中的请求转发给下一层中由一致性哈希确定的 **block home location**; - 缓存未命中会被转发到后续逻辑缓存层,直到最顶层的未命中通过向 data lake(Rados)发起请求来解决。 #### 实现 - D3N 缓存同时支持 **S3** 与 **Swift** 对象存储接口; - D3N **目前只缓存 tail 对象**,因为它们是不可变的(**默认是大于 4MB 的对象的各个部分**); - 对比:NGINX `RGW Data cache and CDN` 支持缓存**所有大小**的对象。 #### 需求 - 一块 **SSD**(`/dev/nvme`、`/dev/pmem`、`/dev/shm`)或类似的块存储设备,需**已格式化**(**除 XFS 外的文件系统未做测试**)并挂载,用作 cache backing store; - 根据设备性能,**多个 RGW 可以共享同一块设备**,但每个 RGW 在该设备文件系统上需要**各自的独立目录**。 #### 限制 | 限制 | 说明 | |------|------| | 压缩对象 | D3N **不会缓存**被 `Rados Gateway Compression` 压缩的对象(**OSD 层压缩是支持的**) | | 加密对象 | D3N **不会缓存**被 `Rados Gateway Encryption` 加密的对象 | | chunk/stripe 不一致 | 若 `rgw_max_chunk_size` 与 `rgw_obj_stripe_size` 配置值**不相等**,D3N **会被禁用** | > 默认值取自 src/common/options/*.yaml.in @ v20.2.4:`rgw_max_chunk_size` = **4_M**(size);`rgw_obj_stripe_size` = **4_M**(size)。二者默认相等,因此默认不会触发该禁用条件。 #### 配置 在**每个 Rados Gateway 的 ceph.conf client 段**中加入以下配置(以 `[client.rgw.8000]` 为例): ```ini [client.rgw.8000] rgw_d3n_l1_local_datacache_enabled = true rgw_d3n_l1_datacache_persistent_path = "/mnt/nvme0/rgw_datacache/client.rgw.8000/" rgw_d3n_l1_datacache_size = 10737418240 ``` > 上例假设 cache backing-store 固态设备挂载在 `/mnt/nvme0`,且有 `10 GB` 可用空间用于缓存。 > ⚠️ **persistent path 目录必须在启动 Gateway 之前创建**(`mkdir -p /mnt/nvme0/rgw_datacache/client.rgw.8000/`)。 **容器化部署**中,缓存目录应作为 volume 挂载: ```yaml extra_container_args: - "-v" - "/mnt/nvme0/rgw_datacache/client.rgw.8000/:/mnt/nvme0/rgw_datacache/client.rgw.8000/" ``` **同一机器上共存多个 Gateway** 时,为每个 Gateway 配置独立的 persistent path,例如 `[client.rgw.8001]`: ```ini [client.rgw.8001] rgw_d3n_l1_datacache_persistent_path = "/mnt/nvme0/rgw_datacache/client.rgw.8001/" ``` > 💡 在多 Gateway 共存配置中,建议**不使用 balancer**,把不同工作负载的客户端分配给各自的 Gateway,以避免缓存数据重复。 > ⚠️ **每次重启 Rados Gateway,缓存目录内容会被清空(purged)。** #### D3N 配置项 | 配置项 | 类型 | 默认值 | 说明 | |--------|------|--------|------| | `rgw_d3n_l1_local_datacache_enabled` | bool | **false**(默认值取自 src/common/options/*.yaml.in @ v20.2.4) | 启用 datacenter-scale dataset delivery local cache | | `rgw_d3n_l1_datacache_persistent_path` | str | **`/tmp/rgw_datacache/`**(默认值取自 src/common/options/*.yaml.in @ v20.2.4) | 存放本地缓存对象数据的目录路径 | | `rgw_d3n_l1_datacache_size` | size | **1_G**(默认值取自 src/common/options/*.yaml.in @ v20.2.4) | datacache 在磁盘上的最大大小(字节) | | `rgw_d3n_l1_eviction_policy` | str | **lru**(默认值取自 src/common/options/*.yaml.in @ v20.2.4) | 选择 d3n 缓存的驱逐策略 | #### 日志 - `radosgw.*.log` 中与 D3N 相关的日志行包含字符串 **`d3n`**(大小写不敏感); - 底层 D3N 日志可通过 `debug_rgw_datacache` 子系统开启(**最高 `debug_rgw_datacache=30`**)。 > 引自:[D3N RGW Data Cache](https://docs.ceph.com/en/tentacle/radosgw/d3n_datacache/) --- ## 十三、压缩与动态分片 ### 13.1 Compression(压缩) > `.. versionadded:: Kraken` | 项 | 说明 | |----|------| | 功能 | Ceph Object Gateway 支持对**上传的对象**做服务端压缩 | | 插件 | 可使用任何已有的 compression plugin | **支持的 compression plugin:** - `lz4` - `snappy` - `zlib` - `zstd` > ⚠️ Reef 版本增加了 `compress-encrypted` zonegroup feature,用于启用「压缩 + `Server-Side Encryption`」的组合(详见第七章)。 #### 配置 在 **Zone 的 placement target** 上,为某个 storage class 启用压缩,方法是给 `radosgw-admin zone placement modify` 传入 `--compression=`: ```bash radosgw-admin zone placement modify \ --rgw-zone default \ --placement-id default-placement \ --storage-class STANDARD \ --compression zlib ``` 输出片段: ```json { ... "placement_pools": [ { "key": "default-placement", "val": { "index_pool": "default.rgw.buckets.index", "storage_classes": { "STANDARD": { "data_pool": "default.rgw.buckets.data", "compression_type": "zlib" } }, "data_extra_pool": "default.rgw.buckets.non-ec", "index_type": 0, } } ], ... } ``` **要点:** | 项 | 说明 | |----|------| | `type` 含义 | 写入**新对象数据**时使用的 compression plugin 名 | | 已存在对象 | **每个被压缩的对象会记住自己用的是哪个 plugin**,因此修改该设置**既不影响 Ceph 解压已有对象的能力,也不要求对已有对象重新压缩** | | 作用范围 | 压缩设置适用于**通过该 placement target 上传的所有新对象** | | 禁用 | 把 `type` 设为**空字符串**或 `none` 即可禁用压缩 | > 若此前未做 Multisite Configuration,系统会为你创建 `default` zone。 #### 统计 ```bash radosgw-admin bucket stats --bucket= ``` ```json { ... "usage": { "rgw.main": { "size": 1075028, "size_actual": 1331200, "size_utilized": 592035, "size_kb": 1050, "size_kb_actual": 1300, "size_kb_utilized": 579, "num_objects": 104 } }, ... } ``` | 字段 | 说明 | |------|------| | `size` / `size_kb` | 未压缩数据大小(其他命令与 API 报告的对象与 bucket 大小都基于**未压缩数据**) | | `size_actual` / `size_kb_actual` | 实际占用大小 | | `size_utilized` / `size_kb_utilized` | **压缩后数据的总大小**,分别以字节与 KB 为单位 | > 引自:[Compression](https://docs.ceph.com/en/tentacle/radosgw/compression/) ### 13.2 Dynamic Bucket Index Resharding(动态分片) > `.. versionadded:: Luminous` #### 背景与原理 | 项 | 说明 | |----|------| | 问题 | 一个 bucket index 对象中的条目过多会导致性能问题 | | 历史 | Luminous 之前,改变 bucket shard 数(resharding)只能**离线**进行,需停用 RGW 服务;**Luminous 起支持在线 bucket resharding** | | 触发机制 | 每个 bucket index shard 在条目数达到一定阈值前都能高效处理;超过后系统可能出现性能问题。dynamic resharding 会**自动检测**这种情况并**自动增加** bucket index 使用的 shard 数,从而降低每个 shard 的条目数 | | 用户感知 | **对用户透明**;resharding 期间对目标 bucket 的**写入可能被短暂阻塞,但读取不受影响** | | 上限 | 默认情况下 dynamic bucket index resharding 最多只能把 shard 数增加到 **1999**,该上界是可配置参数 | | 选数策略 | 可能时,该过程会选择一个**质数**个 shard,以便把条目更均匀地分布到各 bucket index shard 上 | | 执行模型 | resharding 机会的检测作为一个**后台进程周期性扫描所有 bucket**;需要 resharding 的 bucket 被加入队列;**一个线程在后台逐个处理**队列中的 resharding 任务 | | 缩减(Tentacle 起) | 自 **Tentacle** 起,dynamic resharding **具备减少 shard 数的能力**。一旦注意到允许缩减的条件,会**先延迟一段时间**再真正执行,以防近期对象数又增加——目的是**避免抖动(thrashing)**,即对象数上下波动的 bucket 被反复 reshard | #### Multisite 注意事项 > ⚠️ **Reef 之前的 Ceph 版本,RGW 在 multisite 部署中不支持 dynamic resharding**。参见 `resharding` zone feature(第七章)。 #### 配置项全表 | 配置项 | 类型 | 默认值(默认值取自 src/common/options/*.yaml.in @ v20.2.4) | 说明 | |--------|------|------------------|------| | `rgw_dynamic_resharding` | bool | **true** | 启用动态 resharding。若为 true,RGW 会对「每 shard 对象数偏高」的 bucket 动态增加 shard 数 | | `rgw_max_objs_per_shard` | uint | **100000** | dynamic resharding 允许的**每 bucket index shard 最大对象数**。RGW 会在超过该值时触发自动 reshard | | `rgw_max_dynamic_shards` | uint | **1999** | dynamic resharding **自身**能够创建的最大 bucket index shard 数。**这不限制用户请求的(手工)shard 数** | | `rgw_dynamic_resharding_may_reduce` | bool | **true** | dynamic resharding 是否允许**减少** shard 数(当看起来 shard 过多时) | | `rgw_dynamic_resharding_reduction_wait` | uint | **120** | 缩减 bucket index shard 数前的**延迟小时数**(为避免对象数频繁上下波动的 bucket 被反复 reshard) | | `rgw_reshard_bucket_lock_duration` | uint | **360** | reshard 锁(bucket reshard lock 与 reshard log lock)的**超时秒数**。reshard 进行过程中这些锁可以被续期/延长。**若太短,reshard 可能无法完成** | | `rgw_reshard_thread_interval` | uint | **600** | 处理 reshard log 条目之间的间隔秒数 | | `rgw_reshard_num_logs` | uint | **16** | (源文未给出 desc 内容) | | `rgw_reshard_progress_judge_interval` | uint | **120** | 判定 bucket reshard 是否卡在 block 状态的间隔秒数 | | `rgw_reshard_progress_judge_ratio` | float | **0.5** | reshard 进度判定间隔的随机浮动比例。给 `rgw_reshard_progress_judge_interval` 增加一个随机延迟,用于决定何时判定 reshard 进程 | #### Admin 命令 | 操作 | 命令 | |------|------| | 把 bucket 加入 resharding 队列 | `radosgw-admin reshard add --bucket --num-shards ` | | 列出 resharding 队列 | `radosgw-admin reshard list` | | 处理队列中的任务 | `radosgw-admin reshard process` | | 查看 bucket resharding 状态 | `radosgw-admin reshard status --bucket ` | | 取消待处理的 bucket resharding | `radosgw-admin reshard cancel --bucket ` | | 手工立即 reshard | `radosgw-admin bucket reshard --bucket --num-shards ` | | 设置 bucket 的最小 shard 数 | `radosgw-admin bucket set-min-shards --bucket --num-shards ` | > ⚠️ 一旦 bucket resharding 任务从初始的 `not-resharding` 状态转换为 `in-progress` 状态,就**无法取消**。 #### reshard status 输出解读 输出是一个 JSON 数组,**每个 shard 一个元素**,每个元素含 3 个属性:`reshard_status`、`new_bucket_instance_id`、`num_shards`。 **1) resharding 之前:** ```json [ { "reshard_status": "not-resharding", "new_bucket_instance_id": "", "num_shards": -1 } ] ``` **2) resharding 进行中:** ```json [ { "reshard_status": "in-progress", "new_bucket_instance_id": "1179f470-2ebf-4630-8ec3-c9922da887fd.8652.1", "num_shards": 2 }, { "reshard_status": "in-progress", "new_bucket_instance_id": "1179f470-2ebf-4630-8ec3-c9922da887fd.8652.1", "num_shards": 2 } ] ``` **3) resharding 完成之后:** ```json [ { "reshard_status": "not-resharding", "new_bucket_instance_id": "", "num_shards": -1 }, { "reshard_status": "not-resharding", "new_bucket_instance_id": "", "num_shards": -1 } ] ``` #### 手工 reshard 的选数建议 | 建议 | 说明 | |------|------| | 预估峰值 | 管理员必须**预估每个 bucket 的对象数峰值**。理想目标是**任意时刻每个 shard 不超过 100000 条条目** | | 使用质数 | **质数**个 bucket index shard 在均匀分布条目方面更有效。例如 **7001 比 7000 更好**(前者是质数) | #### 设置 bucket 最小 shard 数 由于 dynamic resharding 现在**可以减少** shard 数,管理员可能希望防止 shard 数变得过低(例如预期未来对象数会增加)。`bucket set-min-shards` 允许设置**每 bucket 的最小值**。 > ⚠️ 但这**不能阻止**管理员手工 reshard 到更低的 shard 数。 #### 排障 **Stale Instances(陈旧实例):** > Luminous 12.2.11 与 Mimic 13.2.5 **之前**的集群会遗留陈旧的 bucket instance 条目,且不会自动清理。该问题还影响 lifecycle 策略——它们不再应用于已 reshard 的 bucket。两者都可通过 `radosgw-admin` 命令修复。 ```bash # 列出集群中可清理的 stale instances radosgw-admin reshard stale-instances list # 清理集群中的 stale instances radosgw-admin reshard stale-instances delete ``` > ⚠️ **不应在 multisite 部署中执行 stale instances 清理。** **Lifecycle Fixes(生命周期修复):** 对于含已 reshard 实例的集群,旧的 lifecycle 进程很可能在 reshard 期间因 bucket instance 变化而标记并删除 lifecycle 处理。虽然较新 Ceph 版本(Mimic 13.2.6 与 Luminous 12.2.12 起)部署的 bucket 已修复该问题,但**更老的、有 lifecycle 策略且经历过 resharding 的 bucket 必须手工修复**: ```bash radosgw-admin lc reshard fix --bucket {bucketname} ``` > 若未提供 `--bucket` 参数,该命令会尝试修复**集群中所有 bucket** 的 lifecycle 策略。 **Object Expirer Fixes(对象过期修复):** 旧集群上受 Swift object expiration 约束的对象,可能在 bucket 被 reshard 后从 log pool 中丢失且从未被删除。若其过期时间**早于**集群升级时间就会发生;若过期时间在升级**之后**,对象会被正确处理。`radosgw-admin` 提供两个子命令管理这些 expire-stale 对象: ```bash # 列出(以 JSON 格式显示对象名与过期时间) radosgw-admin objects expire-stale list --bucket {bucketname} # 删除(发起删除,并以 JSON 格式显示对象名、过期时间与删除状态) radosgw-admin objects expire-stale rm --bucket {bucketname} ``` > 引自:[RGW Dynamic Bucket Index Resharding](https://docs.ceph.com/en/tentacle/radosgw/dynamicresharding/) --- ## 十四、孤立对象(Orphans) > `.. versionadded:: Luminous` ### 14.1 产生原因 | 项 | 说明 | |----|------| | 定义 | Orphans(孤立对象)是指**其关联的 RGW 对象被移除后遗留下来**的 **RADOS 对象** | | 正常路径 | 通常这些 RADOS 对象会被**自动移除**——要么立即移除,要么通过称为 **"garbage collection"(垃圾回收)** 的过程移除 | | 产生原因 | 在 RGW 的历史上,可能存在**阻止这些 RADOS 对象被删除的 bug** | | 后果 | 这些 RADOS 对象**占用 Ceph 集群空间却毫无用处** | | 视角 | 从 RGW 的视角看,我们把这类 RADOS 对象称为 "orphans" | ### 14.2 已废弃的 `orphans find` `radosgw-admin` 曾有(或曾有)三个子命令用于管理 orphans,但这些子命令**已经(或即将)被废弃**: ```bash radosgw-admin orphans find ... radosgw-admin orphans finish ... radosgw-admin orphans list-jobs ... ``` **废弃的两个关键原因:** | 编号 | 原因 | |------|------| | 1 | 这些子命令**没有被积极维护**,因此没有跟上 RGW 在特性与更新方面的演进。结果是,**它们能准确识别真正 orphan 的可信度目前很低** | | 2 | 这些子命令把**中间结果存放在集群自身**。当集群管理员面临存储空间不足、想通过删除 orphans 来解决时,这会是个问题——**中间结果可能进一步加剧集群存储容量的紧张** | 基于以上原因,**"orphans find" 已被废弃**。 ### 14.3 替代工具:`rgw-orphan-list` | 项 | 说明 | |----|------| | 引入原因 | 由于 "orphans find" 被废弃,RGW 现在包含一个额外工具 **`rgw-orphan-list`** | | 工作方式 | 运行时它会列出可用 pool,并**提示用户输入 data pool 的名字**。随后(可能需要较长时间)该工具会产出**一个本地文件**,其中包含指定 pool 中**看起来是 orphan** 的 RADOS 对象 | | 后续 | 管理员可自由检查该文件,然后决定采取何种行动,例如把这些 RADOS 对象从指定 pool 中移除 | | 存储位置 | **所有中间结果存放在本地文件系统**,而非 Ceph 集群。因此运行 `rgw-orphan-list` **对集群存储消耗没有明显影响** | #### 三条警告 | 警告 | 内容 | |------|------| | **WARNING: Experimental Status** | `rgw-orphan-list` 工具是新的,因此目前被视为**实验性(experimental)**。产出的 orphan 列表在用于大规模删除操作**之前应进行 "sanity check"(合理性检查)** | | **WARNING: Specifying a Data Pool** | 若指定的 pool **不是 RGW data pool**,工具的结果将是**错误的**——在该 pool 上找到的**所有** RADOS 对象都会被错误地判定为 orphan | | **WARNING: Unindexed Buckets** | RGW 允许存在 **unindexed bucket**(不维护内容索引的 bucket)。这不是典型配置,但**受支持**。由于 `rgw-orphan-list` 工具使用 **bucket indices**来判断「应该存在哪些 RADOS 对象」,**unindexed bucket 中的对象会被错误地列为 orphan** | ### 14.4 RADOS List(子步骤) 计算 orphan 列表的一个子步骤是:把每个 **RGW 对象**映射到其对应的 **RADOS 对象集合**。这通过 `radosgw-admin` 的一个子命令完成: ```bash radosgw-admin bucket radoslist [--bucket={bucket-name}] ``` - 该子命令会产出支撑所有 RGW 对象的 RADOS 对象列表; - 若指定了 bucket,则只产出与**该 bucket 中 RGW 对象**对应的 RADOS 对象列表。 #### 注意:共享的 Bucket Markers | 项 | 说明 | |----|------| | 命名方案 | 一些管理员知道,对应 RGW 对象的 RADOS 对象命名方案中包含一个**对给定 bucket 唯一的 "marker"** | | 反直觉现象 | 然而,对应**某个 RGW bucket 内容**的 RADOS 对象,**可能包含一个指向不同 bucket 的 marker** | | 原因 | 这是 RGW **"shallow copy"(浅拷贝)优化**的结果:较大的对象在 bucket 之间复制时,**只有 "head" 对象被真正复制,tail 对象被共享**。那些被共享的对象会包含**原始 bucket 的 marker** | > 引自:[Orphan List and Associated Tooling](https://docs.ceph.com/en/tentacle/radosgw/orphans/) --- ## 十五、命令速查表 ### 15.1 realm / zonegroup / zone / period ```bash # ---- realm ---- radosgw-admin realm create --rgw-realm= [--default] radosgw-admin realm default --rgw-realm= radosgw-admin realm rm --rgw-realm= radosgw-admin realm get --rgw-realm= radosgw-admin realm set --rgw-realm= --infile= radosgw-admin realm list radosgw-admin realm list-periods radosgw-admin realm pull --url= --access-key= --secret= radosgw-admin realm rename --rgw-realm= --realm-new-name= # ---- zonegroup ---- radosgw-admin zonegroup create --rgw-zonegroup= [--rgw-realm=] [--master] [--default] radosgw-admin zonegroup modify --rgw-zonegroup= [...] radosgw-admin zonegroup default --rgw-zonegroup= radosgw-admin zonegroup add --rgw-zonegroup= --rgw-zone= radosgw-admin zonegroup remove --rgw-zonegroup= --rgw-zone= radosgw-admin zonegroup rename --rgw-zonegroup= --zonegroup-new-name= radosgw-admin zonegroup delete --rgw-zonegroup= radosgw-admin zonegroup list radosgw-admin zonegroup get [--rgw-zonegroup=] radosgw-admin zonegroup set --infile zonegroup.json radosgw-admin zonegroup-map get radosgw-admin zonegroup-map set --infile zonegroupmap.json radosgw-admin zonegroup-map update # ---- zone ---- radosgw-admin zone create --rgw-zone= [--zonegroup=] [--endpoints=] \ [--master] [--default] --access-key --secret radosgw-admin zone modify [options] # --access-key/--secret/--master/--default/--endpoints radosgw-admin zone delete --rgw-zone= radosgw-admin zone rename --rgw-zone= --zone-new-name= radosgw-admin zone list radosgw-admin zone get [--rgw-zone=] radosgw-admin zone set --rgw-zone= --infile zone.json # ---- period ---- radosgw-admin period update --commit radosgw-admin period pull # ---- system user ---- radosgw-admin user create --uid= --display-name="" --system ``` ### 15.2 同步状态与日志 ```bash radosgw-admin sync status radosgw-admin sync info [--bucket=] [--effective-zone-name=] ``` ### 15.3 sync policy ```bash radosgw-admin sync policy get [--bucket=] radosgw-admin sync group create [--bucket=] --group-id= --status= radosgw-admin sync group modify [--bucket=] --group-id= --status= radosgw-admin sync group get [--bucket=] --group-id= radosgw-admin sync group remove [--bucket=] --group-id= radosgw-admin sync group flow create [--bucket=] --group-id= --flow-id= \ --flow-type=directional --source-zone= --dest-zone= radosgw-admin sync group flow create [--bucket=] --group-id= --flow-id= \ --flow-type=symmetrical --zones= radosgw-admin sync group flow remove [--bucket=] --group-id= --flow-id= \ --flow-type=directional --source-zone= --dest-zone= radosgw-admin sync group flow remove [--bucket=] --group-id= --flow-id= \ --flow-type=symmetrical [--zones=] radosgw-admin sync group pipe create [--bucket=] --group-id= --pipe-id= \ --source-zones=<*> [--source-bucket=<*>] [--source-bucket-id=] \ --dest-zones=<*> [--dest-bucket=<*>] [--dest-bucket-id=] \ [--prefix=

] [--prefix-rm] [--tags-add=] [--tags-rm=] \ [--dest-owner=] [--storage-class=] [--mode=] [--uid=] radosgw-admin sync group pipe remove [--bucket=] --group-id= --pipe-id= [...] ``` ### 15.4 sync module / tier ```bash radosgw-admin zone create --rgw-zonegroup= --rgw-zone= --endpoints= --tier-type= radosgw-admin zone modify --rgw-zone= --tier-type= --tier-config=[,] radosgw-admin zone modify --rgw-zone= --tier-config-rm= radosgw-admin zonegroup placement add --rgw-zonegroup= --placement-id= --storage-class= --tier-type= radosgw-admin zonegroup placement modify --rgw-zonegroup= --placement-id= --storage-class= --tier-config=[,] radosgw-admin zonegroup placement modify --rgw-zonegroup= --placement-id= --storage-class= --tier-config-rm= radosgw-admin zonegroup placement rm --rgw-zonegroup= --placement-id= --storage-class= ``` ### 15.5 zone features ```bash radosgw-admin zone modify --rgw-zone= --enable-feature= radosgw-admin zone modify --rgw-zone= --disable-feature= radosgw-admin zonegroup modify --rgw-zonegroup= --enable-feature= radosgw-admin zonegroup modify --rgw-zonegroup= --disable-feature= radosgw-admin period update --commit ``` ### 15.6 通知与桶日志 ```bash radosgw-admin topic list [--tenant=] [--uid=] radosgw-admin topic get --topic= [--tenant=] radosgw-admin topic rm --topic= [--tenant=] radosgw-admin topic stats --topic= [--tenant=] radosgw-admin topic dump --topic= [--tenant=] [--max-entries=] radosgw-admin bucket logging list --bucket ``` ### 15.7 压缩 / reshard / orphan ```bash radosgw-admin zone placement modify --rgw-zone --placement-id --storage-class --compression radosgw-admin bucket stats --bucket= radosgw-admin reshard add --bucket --num-shards radosgw-admin reshard list radosgw-admin reshard process radosgw-admin reshard status --bucket radosgw-admin reshard cancel --bucket radosgw-admin bucket reshard --bucket --num-shards radosgw-admin bucket set-min-shards --bucket --num-shards radosgw-admin reshard stale-instances list radosgw-admin reshard stale-instances delete radosgw-admin lc reshard fix --bucket {bucketname} radosgw-admin objects expire-stale list --bucket {bucketname} radosgw-admin objects expire-stale rm --bucket {bucketname} radosgw-admin bucket radoslist [--bucket=] rgw-orphan-list ``` ### 15.8 S3 Select / 云恢复 ```bash aws --endpoint-url http://localhost:8000 s3api select-object-content \ --bucket {BUCKET-NAME} --key {OBJECT-NAME} \ --expression-type 'SQL' \ --expression "select count(0) from s3object where int(_1)<10;" \ --input-serialization '{"CSV": {"FieldDelimiter": ",", "RecordDelimiter": "\n", "FileHeaderInfo": "USE"}, "CompressionType": "NONE"}' \ --output-serialization '{"CSV": {"FieldDelimiter": ":", "RecordDelimiter":"\t", "QuoteFields": "ALWAYS"}}' \ --request-progress '{"Enabled": true}' output.csv aws s3api restore-object --bucket bucket1 --key doc1.rtf --restore-request Days=10 aws s3api restore-object --bucket bucket1 --key doc2.rtf --restore-request {} aws s3api get-object --bucket bucket1 --key doc3.rtf aws s3api head-object --bucket bucket1 --key doc1.rtf radosgw-admin object stat --bucket bucket1 --object doc1.rtf ``` --- ## 文档未明确项 以下条目在 v20.2.4 官方源文中**未给出明确信息**,本册不做臆测: | # | 项目 | 说明 | |---|------|------| | 1 | `rgw_default_zone_group_info_oid` 的真实默认值 | `multisite.rst` 表格写 `default.zonegroup`,但在 `rgw.yaml.in` 中未检索到同名条目 | | 2 | `multipart_sync_threshold` / `multipart_min_part_size` 的默认值 | cloud-sync-module 与 cloud-transition 两处都只给占位符;`cloud-transition.rst` 的示例输出里出现过 `33554432`,但源文未声明其为默认值 | | 3 | S3 Select 的完整错误码表 | 源文只说明「400 Bad Request + 语法错误 / 运行时错误」两类,并给了 `NoSuchKey` 的 XML 示例 | | 4 | S3 Select 支持的数据类型清单 | 源文未单独列出;本册从 cast / 函数描述反推出 int / float / string / bool / timestamp / NULL | | 5 | S3 Select 的 Parquet 查询语法与限制 | 只提到存在 Parquet reader/parser,相关段落标注 `TODO` | | 6 | S3 Select `LIMIT` 的示例 | 源文标注 `TODO : add examples, for aggregation and non-aggregation queries.` | | 7 | JSON 查询中 object 与 array 值相关示例 | 源文标注 `TODO : relevant example for object and array values.` | | 8 | Scan Range 在 CSV / JSON / Parquet 不同数据源下的行为 | 源文标注 `TODO : different data-sources (CSV, JSON, Parquet)` | | 9 | `mdlog` / `bilog` / `datalog` 的 list / trim 具体命令语法 | 源文只说「可以 list 也可以 trim」 | | 10 | `rgw_reshard_num_logs` 的描述 | 源文 `confval` 对应条目在 `rgw.yaml.in` 中 `desc` 为空 | | 11 | sync policy group 三种状态的显式优先级表 | 源文未给出;本册表格依据 `forbidden` 的 "can override other groups" 描述推导 | | 12 | ElasticSearch sync module 各 tier-config 项的默认值 | 除 `explicit_custom_meta`(false)与 `host_style`(path)外,其余未说明 | | 13 | `rgw_zone` / `rgw_zonegroup` 的默认值 | `rgw.yaml.in` 中为 `str` 类型且无 `default:` 字段;源文表格写 `None`;可通过 `radosgw-admin zone default` / `zonegroup default` 设置集群级默认 | | 14 | 桶日志 REST API 的具体配置语法 | 源文在 "Bucket Logging REST API" 一节指向 `Bucket Operations` 页面(不在本册范围内) | | 15 | Notification 的 REST API 具体配置语法 | 源文在 "Notifications" 一节指向 `Bucket Operations` 页面(不在本册范围内) | | 16 | `rgw-orphan-list` 的命令行参数与输出文件格式 | 源文只描述其为交互式工具,未给参数 | | 17 | D3N `rgw_d3n_l1_eviction_policy` 除 `lru` 外的可选值 | 源文未列出 | | 18 | cloud sync module `connection.host_style` 之外的 `connection_id` 生成规则 | 源文未说明 |