Files
wiki/存储/Ceph/知识库/11-RGW-多站点与数据服务篇.md
T
2026-09-18 13:49:11 +08:00

4286 lines
194 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第 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: <zone>` | `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 <name> --realm=<realm> --zone=<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 <id> (<name>)` | 当前 realm 的 ID 与名称 |
| `zonegroup <id> (<name>)` | 当前 zonegroup 的 ID 与名称 |
| `zone <id> (<name>)` | **本 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: <ts>` | 最老的尚未应用的增量变更时间 |
| `data sync source: <id> (<name>)` | 一个数据同步源 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=<name> --default
# 2. 重命名 default zonegroup 与 zone
radosgw-admin zonegroup rename --rgw-zonegroup default --zonegroup-new-name=<name>
radosgw-admin zone rename --rgw-zone default --zone-new-name us-east-1 --rgw-zonegroup=<name>
# 3. 重命名 default zonegroup 的 api_name
radosgw-admin zonegroup modify --api-name=<name> --rgw-zonegroup=<name>
# 4. 配置 master zonegroup
radosgw-admin zonegroup modify --rgw-realm=<name> --rgw-zonegroup=<name> \
--endpoints http://<fqdn>:80 --master --default
# 5. 配置 master zone
radosgw-admin zone modify --rgw-realm=<name> --rgw-zonegroup=<name> \
--rgw-zone=<name> --endpoints http://<fqdn>:80 \
--access-key=<access-key> --secret=<secret-key> \
--master --default
# 6. 创建系统用户
radosgw-admin user create --uid=<user-id> \
--display-name="<display-name>" \
--access-key=<access-key> \
--secret=<secret-key> --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=<name>` |
| 设置 realm(从文件) | `radosgw-admin realm set --rgw-realm=<name> --infile=<infilename>` |
| 列出 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=<current-name> --realm-new-name=<new-realm-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=<name> [--rgw-realm=<name>][--master] [--default]` | 视情况 |
| 修改 | `radosgw-admin zonegroup modify --rgw-zonegroup=<zonegroup-name> ...` | 视情况 |
| 设为默认 | `radosgw-admin zonegroup default --rgw-zonegroup=comedy` | 是 |
| 添加 zone | `radosgw-admin zonegroup add --rgw-zonegroup=<name> --rgw-zone=<name>` | 是 |
| 移除 zone | `radosgw-admin zonegroup remove --rgw-zonegroup=<name> --rgw-zone=<name>` | 是 |
| 重命名 | `radosgw-admin zonegroup rename --rgw-zonegroup=<name> --zonegroup-new-name=<name>` | 是 |
| 删除 | `radosgw-admin zonegroup delete --rgw-zonegroup=<name>` | 是 |
| 列表 | `radosgw-admin zonegroup list` | 否 |
| 获取 zonegroup map | `radosgw-admin zonegroup-map get` | 否 |
| 获取单个 zonegroup | `radosgw-admin zonegroup get [--rgw-zonegroup=<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=<zone>]` |
| 设置(从文件) | `radosgw-admin zone set --rgw-zone={zone-name} --infile zone.json` → `period update --commit` |
| 重命名 | `radosgw-admin zone rename --rgw-zone=<name> --zone-new-name=<name>` → `period update --commit` |
**创建 zone:**
```bash
radosgw-admin zone create --rgw-zone=<name> \
[--zonegroup=<zonegroup-name] \
[--endpoints=<endpoint>[,<endpoint>] \
[--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=<name> \
--zone=<name>
radosgw-admin period update --commit
radosgw-admin zone delete --rgw-zone<name>
radosgw-admin period update --commit
```
> ⚠️ **必须先从 zonegroup 移除再删除 zone**,否则 `period update` 会失败。
**删除已删除 zone 的 pool(仅当这些 pool 不再被使用):**
```bash
ceph osd pool rm <del-zone>.rgw.control <del-zone>.rgw.control --yes-i-really-really-mean-it
ceph osd pool rm <del-zone>.rgw.meta <del-zone>.rgw.meta --yes-i-really-really-mean-it
ceph osd pool rm <del-zone>.rgw.log <del-zone>.rgw.log --yes-i-really-really-mean-it
ceph osd pool rm <del-zone>.rgw.otp <del-zone>.rgw.otp --yes-i-really-really-mean-it
ceph osd pool rm <del-zone>.rgw.buckets.index <del-zone>.rgw.buckets.index --yes-i-really-really-mean-it
ceph osd pool rm <del-zone>.rgw.buckets.non-ec <del-zone>.rgw.buckets.non-ec --yes-i-really-really-mean-it
ceph osd pool rm <del-zone>.rgw.buckets.data <del-zone>.rgw.buckets.data --yes-i-really-really-mean-it
```
> ⚠️ **只删除带 zone 名前缀的 pool。** 删除 root pool(例如 `.rgw.root`)会清掉整个系统的配置。
> ⚠️ pool 一旦删除,其中的数据将不可恢复地丢失。
**`zone modify` 支持的 options:**
| 选项 | 说明 |
|------|------|
| `--access-key=<key>` | 系统用户 access key |
| `--secret/--secret-key=<key>` | 系统用户 secret key |
| `--master` | 设为 master zone |
| `--default` | 设为 default zone |
| `--endpoints=<list>` | 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=<zone1,zone2,...>` |
| `directional` | 定向数据流:数据从 source zone 单向流向 dest zone | `--source-zone=<zone>` `--dest-zone=<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=<bucket>]
```
#### 创建 / 修改 / 查看 / 删除 group
```bash
# 创建
radosgw-admin sync group create [--bucket=<bucket>] \
--group-id=<group-id> \
--status=<enabled | allowed | forbidden>
# 修改
radosgw-admin sync group modify [--bucket=<bucket>] \
--group-id=<group-id> \
--status=<enabled | allowed | forbidden>
# 查看
radosgw-admin sync group get [--bucket=<bucket>] \
--group-id=<group-id>
# 删除
radosgw-admin sync group remove [--bucket=<bucket>] \
--group-id=<group-id>
```
#### 创建 / 删除 flow
```bash
# 创建或更新 directional flow
radosgw-admin sync group flow create [--bucket=<bucket>] \
--group-id=<group-id> \
--flow-id=<flow-id> \
--flow-type=directional \
--source-zone=<source_zone> \
--dest-zone=<dest_zone>
# 创建或更新 symmetrical flow(zones 为逗号分隔列表)
radosgw-admin sync group flow create [--bucket=<bucket>] \
--group-id=<group-id> \
--flow-id=<flow-id> \
--flow-type=symmetrical \
--zones=<zones>
# 删除 directional flow
radosgw-admin sync group flow remove [--bucket=<bucket>] \
--group-id=<group-id> \
--flow-id=<flow-id> \
--flow-type=directional \
--source-zone=<source_zone> \
--dest-zone=<dest_zone>
# 从 symmetrical flow 中移除指定 zone
radosgw-admin sync group flow remove [--bucket=<bucket>] \
--group-id=<group-id> \
--flow-id=<flow-id> \
--flow-type=symmetrical \
--zones=<zones>
# 删除整个 symmetrical flow
radosgw-admin sync group flow remove [--bucket=<bucket>] \
--group-id=<group-id> \
--flow-id=<flow-id> \
--flow-type=symmetrical
```
#### 创建 / 删除 pipe
```bash
# 创建或更新 pipe 参数
radosgw-admin sync group pipe create [--bucket=<bucket>] \
--group-id=<group-id> \
--pipe-id=<pipe-id> \
--source-zones=<source_zones> \
[--source-bucket=<source_buckets>] \
[--source-bucket-id=<source_bucket_id>] \
--dest-zones=<dest_zones> \
[--dest-bucket=<dest_buckets>] \
[--dest-bucket-id=<dest_bucket_id>] \
[--prefix=<source_prefix>] \
[--prefix-rm] \
[--tags-add=<tags>] \
[--tags-rm=<tags>] \
[--dest-owner=<owner>] \
[--storage-class=<storage_class>] \
[--mode=<system | user>] \
[--uid=<user_id>]
# 删除 pipe 的特定参数或整个 pipe
radosgw-admin sync group pipe remove [--bucket=<bucket>] \
--group-id=<group-id> \
--pipe-id=<pipe-id> \
[--source-zones=<source_zones>] \
[--source-bucket=<source_buckets>] \
[--source-bucket-id=<source_bucket_id>] \
[--dest-zones=<dest_zones>] \
[--dest-bucket=<dest_buckets>] \
[--dest-bucket-id=<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=<bucket>] \
[--effective-zone-name=<zone>]
```
获取按 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 := [(]<arg> <op> <value> [)][<and|or> ...]
```
支持的 `op`:`<`、`<=`、`==`、`>=`、`>`
示例:
```text
GET /?query=name==foo
```
返回用户有读权限、且名为 `foo` 的所有已索引 key。输出为 XML 的 key 列表,类似 S3 list buckets 响应。
**配置自定义元数据字段(mdsearch):**
```text
POST /{bucket}?mdsearch
x-amz-meta-search: <key [; type]> [, ...]
```
- 多个元数据字段用逗号分隔;
- 可用 `;` 强制指定类型;
- 允许的类型:`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 /<bucket>?mdsearch` |
| 获取自定义元数据配置 | `GET /<bucket>?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": <access>,
"secret": <secret>,
"endpoint": <endpoint>,
"host_style": <path | virtual>,
},
"acls": [ { "type": <id | email | uri>,
"source_id": <source_id>,
"dest_id": <dest_id> } ... ],
"target_path": <target_path>,
}
```
#### 完整配置(Non Trivial Configuration)
```json
{
"default": {
"connection": {
"access_key": <access>,
"secret": <secret>,
"endpoint": <endpoint>,
"host_style": <path | virtual>,
},
"acls": [
{
"type" : <id | email | uri>, # optional, default is id
"source_id": <id>,
"dest_id": <id>
} ... ],
"target_path": <path> # optional
},
"connections": [
{
"connection_id": <id>,
"access_key": <access>,
"secret": <secret>,
"endpoint": <endpoint>,
"host_style": <path | virtual>, # optional
} ... ],
"acl_profiles": [
{
"acls_id": <id>, # acl mappings
"acls": [ {
"type": <id | email | uri>,
"source_id": <id>,
"dest_id": <id>
} ... ],
}
],
"profiles": [
{
"source_bucket": <source>,
"connection_id": <connection_id>,
"acls_id": <mappings_id>,
"target_path": <dest>, # 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": <access>,
"secret": <secret>,
"endpoint": <endpoint>,
"region": <region>,
"host_style": <path | virtual>,
"acls": [ { "type": <id | email | uri>,
"source_id": <source_id>,
"dest_id": <dest_id> } ... ],
"target_path": <target_path>,
"target_storage_class": <target-storage-class>,
"multipart_sync_threshold": {object_size},
"multipart_min_part_size": {part_size},
"retain_head_object": <true | false>
}
```
| 配置项 | 类型 | 说明 |
|--------|------|------|
| `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=<access_key>,secret=<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
<LifecycleConfiguration>
<Rule>
....
<Transition>
....
<StorageClass>CLOUDTIER</StorageClass>
</Transition>
</Rule>
</LifecycleConfiguration>
```
> ⚠️ 由于 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://<target_path>/<source_bucket_name>/<source_object_name>(-<source_object_version_id>)
```
- 若对象为版本化对象,会追加对象的 `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=<access_key>,secret=<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 <value> \
--key <value> \
[--version-id <value>] \
--restore-request (structure) { \
Days=<integer> \
}
```
> `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=<topic-name>
[&Attributes.entry.1.key=amqp-exchange&Attributes.entry.1.value=<exchange>]
[&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=<file path>]
[&Attributes.entry.7.key=OpaqueData&Attributes.entry.7.value=<opaque data>]
[&Attributes.entry.8.key=push-endpoint&Attributes.entry.8.value=<endpoint>]
[&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=<mechanism>]
[&Attributes.entry.12.key=time_to_live&Attributes.entry.12.value=<seconds to live>]
[&Attributes.entry.13.key=max_retries&Attributes.entry.13.value=<retries number>]
[&Attributes.entry.14.key=retry_sleep_duration&Attributes.entry.14.value=<sleep seconds>]
[&Attributes.entry.15.key=Policy&Attributes.entry.15.value=<policy-JSON-string>]
[&Attributes.entry.16.key=user-name&Attributes.entry.16.value=<user-name-string>]
[&Attributes.entry.17.key=password&Attributes.entry.17.value=<password-string>]
[&Attributes.entry.18.key=kafka-brokers&Attributes.entry.18.value=<kafka-broker-list>]
[&Attributes.entry.19.key=ssl-certificate-location&Attributes.entry.19.value=<file path>]
[&Attributes.entry.20.key=ssl-key-location&Attributes.entry.20.value=<file path>]
[&Attributes.entry.21.key=ssl-key-password&Attributes.entry.21.value=<password-string>]
```
> 💡 提示:更新 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]://<fqdn>[:<port]` |
| port | HTTP 默认 **80**,HTTPS 默认 **443** |
| `verify-ssl` | 客户端是否校验服务端证书。**默认 `true`** |
| `cloudevents` | HTTP 头是否按 `S3 CloudEvents Spec` 包含属性。**默认 `false`** |
#### AMQP0.9.1 endpoint
| 属性 | 说明 |
|------|------|
| URI | `amqp[s]://[<user>:<password>@]<fqdn>[:<port>][/<vhost>]` |
| 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://[<user>:<password>@]<fqdn>[:<port]` |
| `use-ssl` | `true` 则使用安全连接连到 broker。**默认 `false`** |
| `ca-location` | 若提供且使用安全连接,将用指定 CA 而非默认 CA 认证 broker |
| user/password | 应在 HTTPS 上提供;否则必须把 `rgw_allow_notification_secrets_in_cleartext` 设为 `true` 才能创建 topic |
| user/password 与 `use-ssl` | 应同时提供;否则 broker 凭据会通过不安全传输发送 |
| `mechanism` | 可与 user/password 一起提供(**默认 `PLAIN`**)。支持的 SASL mechanism 见下表 |
| `user-name` | 连接 Kafka broker 时使用的用户名。若同时提供了 URI user 与该参数,**该参数覆盖 URI user**。安全注意事项同 user/password |
| `password` | 连接 Kafka broker 时使用的密码。若同时提供了 URI password 与该参数,**该参数覆盖 URI password**。安全注意事项同 user/password |
| port | 默认 **9092** |
| `kafka-ack-level` | 不需要 end2end acking。两种取值见下表 |
| `kafka-brokers` | 逗号分隔的 `host:port` Kafka broker 列表。这些 broker(可含 URI 中定义的那个)会被加入 kafka uri,以支持向 Kafka 集群发送通知 |
| `ssl-certificate-location` | 用于 **mTLS** 认证、呈现给 Kafka broker 的 PEM 编码客户端证书路径。必须与 `ssl-key-location` 和 `use-ssl=true` 一起使用;**只指定其中一个会导致连接失败** |
| `ssl-key-location` | 与 `ssl-certificate-location` 对应的 PEM 编码私钥文件路径;必须与 `ssl-certificate-location` 一起提供 |
| `ssl-key-password` | 客户端私钥的密码(若私钥文件被加密)。可选,仅当私钥受密码保护时需要。安全注意事项同 user/password |
**支持的 SASL mechanism:**
- `PLAIN`
- `SCRAM-SHA-256`
- `SCRAM-SHA-512`
- `GSSAPI`
- `OAUTHBEARER`
**`kafka-ack-level` 取值:**
| 取值 | 含义 |
|------|------|
| `none` | 消息只要**发到 broker** 就算 "delivered" |
| `broker` | 消息被 **broker ack** 才算 "delivered"(**默认**) |
#### Attribute 使用注意事项
- 某个参数的 key-value 对**不必与参数位于同一行**,也**不必按特定顺序出现**,但**必须使用相同的索引**;
- Attribute 索引**不必连续**,也**不必从特定值开始**;
- AWS `CreateTopic` 解释了 endpoint attribute 的格式,但 Ceph 使用的是**不同的 key 和 value**。
#### CreateTopic 响应
```xml
<CreateTopicResponse xmlns="https://sns.amazonaws.com/doc/2010-03-31/">
<CreateTopicResult>
<TopicArn></TopicArn>
</CreateTopicResult>
<ResponseMetadata>
<RequestId></RequestId>
</ResponseMetadata>
</CreateTopicResponse>
```
**Topic ARN 格式:**
```text
arn:aws:sns:<zone-group>:<tenant>:<topic>
```
#### Get Topic Attributes
```text
POST
Action=GetTopicAttributes
&TopicArn=<topic-arn>
```
响应:
```xml
<GetTopicAttributesResponse>
<GetTopicAttributesResult>
<Attributes>
<entry>
<key>User</key>
<value></value>
</entry>
<entry>
<key>Name</key>
<value></value>
</entry>
<entry>
<key>EndPoint</key>
<value></value>
</entry>
<entry>
<key>TopicArn</key>
<value></value>
</entry>
<entry>
<key>OpaqueData</key>
<value></value>
</entry>
</Attributes>
</GetTopicAttributesResult>
<ResponseMetadata>
<RequestId></RequestId>
</ResponseMetadata>
</GetTopicAttributesResponse>
```
| 字段 | 说明 |
|------|------|
| `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=<topic-arn>
```
> 该 API **现已废弃(deprecated)**,建议使用 AWS 兼容的 `GetTopicAttributes` API。
响应:
```xml
<GetTopicResponse>
<GetTopicResult>
<Topic>
<User></User>
<Name></Name>
<EndPoint>
<EndpointAddress></EndpointAddress>
<EndpointArgs></EndpointArgs>
<EndpointTopic></EndpointTopic>
<HasStoredSecret></HasStoredSecret>
<Persistent></Persistent>
</EndPoint>
<TopicArn></TopicArn>
<OpaqueData></OpaqueData>
</Topic>
</GetTopicResult>
<ResponseMetadata>
<RequestId></RequestId>
</ResponseMetadata>
</GetTopicResponse>
```
#### Delete Topic
```text
POST
Action=DeleteTopic
&TopicArn=<topic-arn>
```
> ⚠️ 删除一个**未知**的 notification(例如重复删除)**不被视为错误**。
> ⚠️ 删除 topic **不会自动删除**与之关联的所有 notification。
```xml
<DeleteTopicResponse xmlns="https://sns.amazonaws.com/doc/2010-03-31/">
<ResponseMetadata>
<RequestId></RequestId>
</ResponseMetadata>
</DeleteTopicResponse>
```
#### List Topics
```text
POST
Action=ListTopics
```
```xml
<ListTopicsResponse xmlns="https://sns.amazonaws.com/doc/2010-03-31/">
<ListTopicsResult>
<Topics>
<member>
<User></User>
<Name></Name>
<EndPoint>
<EndpointAddress></EndpointAddress>
<EndpointArgs></EndpointArgs>
<EndpointTopic></EndpointTopic>
</EndPoint>
<TopicArn></TopicArn>
<OpaqueData></OpaqueData>
</member>
</Topics>
</ListTopicsResult>
<ResponseMetadata>
<RequestId></RequestId>
</ResponseMetadata>
</ListTopicsResponse>
```
> 若 topic 中任何部分的 endpoint URL 含 user/password 信息,请求**必须走 HTTPS**,否则 "topic list" 请求会被拒绝。
#### Set Topic Attributes
```text
POST
Action=SetTopicAttributes
&TopicArn=<topic-arn>&AttributeName=<attribute-name>&AttributeValue=<attribute-value>
```
> - 传入的 `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 差异
以下标签(及其内部标签)**不被支持**:
| 标签 | 说明 |
|------|------|
| `<QueueConfiguration>` | 不需要,Ceph 把所有目的端都当作 SNS |
| `<CloudFunctionConfiguration>` | 不需要,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 <source 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
<prefix><year-month-day-hour-minute-second>-<16 bytes unique-id>
```
示例:
```text
fish/2024-08-06-09-40-09-0000000002AGQ6W1
```
#### Partitioned
```text
<prefix><source bucket owner>/<zone group>/[tenant:]<source bucket name>/<year>/<month>/<day>/<year-month-day-hour-minute-second>-<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:]<bucket name>` |
| 3 | time,格式 `[day/month/year:hour:minute:second timezone]` |
| 4 | operation,格式 `WEBSITE/REST.<HTTP method>.<resource>` |
| 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:]<bucket name>` |
| 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.<HTTP method>.<resource>` |
| 8 | object key(为空则为 dash) |
| 9 | request URI,格式 `"<HTTP method> <URI> <HTTP version>"` |
| 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
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>NoSuchKey</Code>
<Message>The resource you requested does not exist</Message>
<Resource>/mybucket/myfoto.jpg</Resource>
<RequestId>4442587FB7D0A2F9</RequestId>
</Error>
```
#### Report Response
```xml
HTTP/1.1 200
<?xml version="1.0" encoding="UTF-8"?>
<Payload>
<Records>
<Payload>blob</Payload>
</Records>
<Stats>
<Details>
<BytesProcessed>long</BytesProcessed>
<BytesReturned>long</BytesReturned>
<BytesScanned>long</BytesScanned>
</Details>
</Stats>
<Progress>
<Details>
<BytesProcessed>long</BytesProcessed>
<BytesReturned>long</BytesReturned>
<BytesScanned>long</BytesScanned>
</Details>
</Progress>
<Cont>
</Cont>
<End>
</End>
</Payload>
```
#### 消息类型说明
| 消息类型 | 说明 |
|----------|------|
| **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=<uid for cache user> --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=<the nginx-aws-auth-module dir> --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 <access> --secret <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=<type>`:
```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=<name>
```
```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 <bucket_name> --num-shards <new number of shards>` |
| 列出 resharding 队列 | `radosgw-admin reshard list` |
| 处理队列中的任务 | `radosgw-admin reshard process` |
| 查看 bucket resharding 状态 | `radosgw-admin reshard status --bucket <bucket_name>` |
| 取消待处理的 bucket resharding | `radosgw-admin reshard cancel --bucket <bucket_name>` |
| 手工立即 reshard | `radosgw-admin bucket reshard --bucket <bucket_name> --num-shards <new number of shards>` |
| 设置 bucket 的最小 shard 数 | `radosgw-admin bucket set-min-shards --bucket <bucket_name> --num-shards <min number of 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=<name> [--default]
radosgw-admin realm default --rgw-realm=<name>
radosgw-admin realm rm --rgw-realm=<name>
radosgw-admin realm get --rgw-realm=<name>
radosgw-admin realm set --rgw-realm=<name> --infile=<file>
radosgw-admin realm list
radosgw-admin realm list-periods
radosgw-admin realm pull --url=<url> --access-key=<ak> --secret=<sk>
radosgw-admin realm rename --rgw-realm=<old> --realm-new-name=<new>
# ---- zonegroup ----
radosgw-admin zonegroup create --rgw-zonegroup=<name> [--rgw-realm=<name>] [--master] [--default]
radosgw-admin zonegroup modify --rgw-zonegroup=<name> [...]
radosgw-admin zonegroup default --rgw-zonegroup=<name>
radosgw-admin zonegroup add --rgw-zonegroup=<name> --rgw-zone=<zone>
radosgw-admin zonegroup remove --rgw-zonegroup=<name> --rgw-zone=<zone>
radosgw-admin zonegroup rename --rgw-zonegroup=<name> --zonegroup-new-name=<new>
radosgw-admin zonegroup delete --rgw-zonegroup=<name>
radosgw-admin zonegroup list
radosgw-admin zonegroup get [--rgw-zonegroup=<name>]
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=<name> [--zonegroup=<zg>] [--endpoints=<ep>] \
[--master] [--default] --access-key <ak> --secret <sk>
radosgw-admin zone modify [options] # --access-key/--secret/--master/--default/--endpoints
radosgw-admin zone delete --rgw-zone=<name>
radosgw-admin zone rename --rgw-zone=<name> --zone-new-name=<new>
radosgw-admin zone list
radosgw-admin zone get [--rgw-zone=<name>]
radosgw-admin zone set --rgw-zone=<name> --infile zone.json
# ---- period ----
radosgw-admin period update --commit
radosgw-admin period pull
# ---- system user ----
radosgw-admin user create --uid=<uid> --display-name="<name>" --system
```
### 15.2 同步状态与日志
```bash
radosgw-admin sync status
radosgw-admin sync info [--bucket=<bucket>] [--effective-zone-name=<zone>]
```
### 15.3 sync policy
```bash
radosgw-admin sync policy get [--bucket=<bucket>]
radosgw-admin sync group create [--bucket=<b>] --group-id=<id> --status=<enabled|allowed|forbidden>
radosgw-admin sync group modify [--bucket=<b>] --group-id=<id> --status=<enabled|allowed|forbidden>
radosgw-admin sync group get [--bucket=<b>] --group-id=<id>
radosgw-admin sync group remove [--bucket=<b>] --group-id=<id>
radosgw-admin sync group flow create [--bucket=<b>] --group-id=<id> --flow-id=<fid> \
--flow-type=directional --source-zone=<sz> --dest-zone=<dz>
radosgw-admin sync group flow create [--bucket=<b>] --group-id=<id> --flow-id=<fid> \
--flow-type=symmetrical --zones=<z1,z2>
radosgw-admin sync group flow remove [--bucket=<b>] --group-id=<id> --flow-id=<fid> \
--flow-type=directional --source-zone=<sz> --dest-zone=<dz>
radosgw-admin sync group flow remove [--bucket=<b>] --group-id=<id> --flow-id=<fid> \
--flow-type=symmetrical [--zones=<z1,z2>]
radosgw-admin sync group pipe create [--bucket=<b>] --group-id=<id> --pipe-id=<pid> \
--source-zones=<*> [--source-bucket=<*>] [--source-bucket-id=<id>] \
--dest-zones=<*> [--dest-bucket=<*>] [--dest-bucket-id=<id>] \
[--prefix=<p>] [--prefix-rm] [--tags-add=<k=v,...>] [--tags-rm=<k=v,...>] \
[--dest-owner=<o>] [--storage-class=<sc>] [--mode=<system|user>] [--uid=<u>]
radosgw-admin sync group pipe remove [--bucket=<b>] --group-id=<id> --pipe-id=<pid> [...]
```
### 15.4 sync module / tier
```bash
radosgw-admin zone create --rgw-zonegroup=<zg> --rgw-zone=<z> --endpoints=<ep> --tier-type=<archive|cloud|elasticsearch>
radosgw-admin zone modify --rgw-zone=<z> --tier-type=<t> --tier-config=<k=v>[,<k=v>]
radosgw-admin zone modify --rgw-zone=<z> --tier-config-rm=<key>
radosgw-admin zonegroup placement add --rgw-zonegroup=<zg> --placement-id=<pid> --storage-class=<sc> --tier-type=<cloud-s3|cloud-s3-glacier>
radosgw-admin zonegroup placement modify --rgw-zonegroup=<zg> --placement-id=<pid> --storage-class=<sc> --tier-config=<k=v>[,<k=v>]
radosgw-admin zonegroup placement modify --rgw-zonegroup=<zg> --placement-id=<pid> --storage-class=<sc> --tier-config-rm=<key>
radosgw-admin zonegroup placement rm --rgw-zonegroup=<zg> --placement-id=<pid> --storage-class=<sc>
```
### 15.5 zone features
```bash
radosgw-admin zone modify --rgw-zone=<z> --enable-feature=<f>
radosgw-admin zone modify --rgw-zone=<z> --disable-feature=<f>
radosgw-admin zonegroup modify --rgw-zonegroup=<zg> --enable-feature=<f>
radosgw-admin zonegroup modify --rgw-zonegroup=<zg> --disable-feature=<f>
radosgw-admin period update --commit
```
### 15.6 通知与桶日志
```bash
radosgw-admin topic list [--tenant=<t>] [--uid=<u>]
radosgw-admin topic get --topic=<name> [--tenant=<t>]
radosgw-admin topic rm --topic=<name> [--tenant=<t>]
radosgw-admin topic stats --topic=<name> [--tenant=<t>]
radosgw-admin topic dump --topic=<name> [--tenant=<t>] [--max-entries=<n>]
radosgw-admin bucket logging list --bucket <source bucket>
```
### 15.7 压缩 / reshard / orphan
```bash
radosgw-admin zone placement modify --rgw-zone <z> --placement-id <pid> --storage-class <sc> --compression <lz4|snappy|zlib|zstd|none>
radosgw-admin bucket stats --bucket=<name>
radosgw-admin reshard add --bucket <b> --num-shards <n>
radosgw-admin reshard list
radosgw-admin reshard process
radosgw-admin reshard status --bucket <b>
radosgw-admin reshard cancel --bucket <b>
radosgw-admin bucket reshard --bucket <b> --num-shards <n>
radosgw-admin bucket set-min-shards --bucket <b> --num-shards <n>
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=<bucket-name>]
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` 生成规则 | 源文未说明 |