# 渠道与容灾 (/zh/docs/channels)

渠道是什么、上传写到哪里、副本如何补齐、访问如何切换，以及健康探测、编辑、删除与用量告警的规则。

## 渠道是什么 [#渠道是什么]

一个渠道就是一个 rclone 后端实例：一种后端（`s3`、`webdav`、`local`……）加一组后端参数，再加上 ImgYoung 自己的几项属性：

| 属性 | 含义 |
|---|---|
| 渠道名 | 唯一，最长 64 字节，用于界面、日志、指标与告警 |
| 优先级 | 整数，越大越优先；后台表单接受 0–1000，新建默认 100 |
| 服务端加密 | 是否经 rclone crypt 加密存储，创建时固化，见[渠道存储加密](./encryption.mdx) |
| 允许 302 直链 | 访问时是否可以 302 跳转到渠道的公开地址，见[对象存储与 WebDAV](./object-storage.mdx) |

后端参数以映射直接交给 rclone，不经过 `rclone.conf`；参数整体用主密钥加密（AES-256-GCM）后存入元数据库。渠道在管理后台的「存储渠道」页创建与管理，只有管理员可见；对应接口见[渠道管理接口](../api/admin-channels.mdx)。接口返回的渠道对象（列表、创建、编辑、重新探测的响应，以及元数据导出里的 `channels`）带 `created_at`：渠道的创建时刻（UTC），参与加密渠道的密钥派生，建后不变。

**每个渠道最终都持有全部图片的副本**：多个渠道之间是全量镜像，不是分片。规划容量时按每个渠道都要放下全站图片来算。

<Callout type="warn" title="两个实例不能共用同一个渠道路径">
每日对账只回收本服务写出的孤儿对象，必须同时满足：键名是本服务的对象键格式 `<短 ID>_<SHA-256 前 8 位>`；元数据库里没有该渠道上任何状态的副本记录；修改时间不早于渠道的绑定时刻；已超过 48 小时。深探测残留 `.imgyoung-probe-*` 过了 48 小时也会回收。别的程序写进同一路径的文件只计数（对账日志字段 `foreign`），不会删除。两个 ImgYoung 实例不能共用同一个渠道路径：对方写出的对象在本实例看来就是孤儿，会被删除。

绑定时刻是渠道记录开始对应当前存储位置的时刻：新建渠道时等于创建时刻，[编辑](#编辑渠道)改动配置时重置；从快照恢复（后台恢复，或把快照放成 `<数据目录>/imgyoung.db.restore` 再启动）时全部渠道一起重置。完整规则见[元数据库备份](../operations/backup.mdx)的「对账 GC」。
</Callout>

## 内置 local 渠道 [#内置-local-渠道]

全新的元数据库（渠道表为空）首次启动时，会自动建一个名为 `local` 的渠道：后端 `local`，优先级 100，存储位置是 `<数据目录>/blobs`。这样不做任何配置也能上传。

- 只在渠道表为空时播种。删除或改名之后，重启不会让它重新出现。
- 它和元数据库、主密钥在同一台机器上，主机损坏即全部丢失。只有它一个渠道时，「存储渠道」页会提示「只有本机磁盘一个渠道」。接入一个异地渠道后，存量图片会自动补齐过去；元数据库快照每轮写到每个健康渠道，下一轮备份起新渠道上也有快照。

## 优先级：主渠道与备份渠道 [#优先级主渠道与备份渠道]

ImgYoung 不设单独的「主渠道」开关，主备完全由优先级表达：

- **主渠道**：健康、且不在清理中的渠道里优先级最高的一个。列表里标为「主写」。
- **备份渠道**：其余渠道。正常情况下上传不直接写它们，由异步补齐把副本复制过去；主渠道写入失败顺延、或 `min_success` 大于 1 时才会同步写入。
- 优先级相同时，先创建的渠道排在前面。

主渠道变为异常时，下一个健康渠道自动接替，无需人工切换；它恢复健康后重新成为主渠道。

## 上传写入 [#上传写入]

每次上传都按优先级从高到低遍历当前健康的渠道，同步写入，直到成功份数达到 `min_success`：

1. 写入某个渠道失败（含单次 10 分钟的写入超时），顺延到下一个健康渠道。
2. 凑够份数即返回成功。第一个写成功的渠道就是这张图的首个副本所在。
3. 其余渠道（包括暂时异常的渠道）都记一条「待补齐」副本，交给异步补齐。

`min_success` 在「设置 → 运行配置 → 访问与链接 → 最少同步副本」，默认 1，最小 1。健康渠道不足或全部写失败时，上传返回 503 `存储渠道暂不可用,请稍后重试`（S3 协议上传返回 503 `ServiceUnavailable`），不落元数据；已写成功的那几份对象成为孤儿，由每日对账回收。

<Callout type="info">
`min_success` 大于当前健康渠道数时，所有上传都会失败。设为 2 的意思是：至少两个渠道同时写成功才算上传成功，换来的是写入时延与对多渠道可用性的依赖。
</Callout>

## 异步补齐 [#异步补齐]

副本状态存在元数据库的副本表里，这张业务表是唯一依据；任务队列（River，同一个 SQLite 库）只负责触发，同一张图在同一渠道上同时最多只有一个在途任务。

| 副本状态 | 含义 |
|---|---|
| `pending` | 待补齐 |
| `uploading` | 补齐进行中 |
| `done` | 就绪，可用于访问 |
| `failed` | 重试用尽仍失败 |

- **复制过程**：从优先级最高、健康且已有就绪副本的其他渠道读出，流式写入目标渠道。任务队列 4 个并发（与删除、排空任务共用），单个补齐任务超时 15 分钟。
- **失败重试**：失败后回到 `pending`，由 River 按指数退避重试：第 n 次失败后约等 n⁴ 秒（1 秒、16 秒、81 秒……），最多 25 次，前后约 20 天；最后一次仍失败记为 `failed`。
- **周期对账**：启动时与之后每 10 分钟各做一次：为每个在役渠道补种缺失的副本行；把健康渠道上的 `failed` 副本复位为 `pending`；把全部未完成副本重新入队（已在队列中的自动去重）。后台「设置 → 运行配置 → 引导配置」里显示的「副本对账间隔」就是它。
- **进程重启不丢**：任务和副本状态都在 SQLite 里。正常停机时在途的补齐任务被取消，副本状态留在业务表里，下次启动由对账重新入队；进程被强杀时正在执行的任务，约 16 分钟后由 River 重新排程。
- **新渠道回填存量**：创建渠道时，在同一个事务里为全部存量图片登记待补齐副本，随后立即开始补齐。与创建同时进行的上传如果漏登记了新渠道，周期对账会补上。
- **每日对账 GC**：每 24 小时对健康渠道做一次双向核对。本服务写出、却没有任何副本记录的对象超过 48 小时后删除（孤儿，判定条件见本页开头）；元数据库记为就绪、渠道上却不存在的副本改回 `pending` 重新补齐（幻影）。

## 访问容灾 [#访问容灾]

访问一张图时，候选渠道是：**健康、不在清理中、且这张图的副本已就绪**的渠道，按优先级排序。

- **代理模式**（`access_mode = proxy`，默认）：按顺序读取，某个渠道读失败就切到下一个，对访问者透明。没有任何候选或全部读失败时返回 503 `渠道暂不可用`。
- **302 模式**（`access_mode = redirect`）：候选中第一个「允许 302 直链、未加密、配置了公开访问地址」的渠道直接 302 跳转；没有这样的候选就回到代理。带处理参数的请求（如 `?w=`）始终走代理。

副本还没补齐到某个渠道时，该渠道不会成为这张图的候选。访问模式与缓存细节见[访问与链接](../guides/access.mdx)。

## 健康探测 [#健康探测]

健康状态决定一个渠道能否承接上传与访问。探测分两档：

| | 轻探测 | 深探测 |
|---|---|---|
| 做什么 | 查询一个约定不存在的对象 `.imgyoung-health-probe`（从不写入）；查到或得到「不存在」都算可达 | 写入一个随机键的小对象 `.imgyoung-probe-<随机十六进制>`，读回核对内容，再删除 |
| 成本 | 只读，零写删 | 一次写、读、删 |
| 超时 | 10 秒 | 写与读共 20 秒，删除另有 10 秒 |
| 何时运行 | 周期循环，每 `health_check_seconds`（默认 60 秒）| 创建渠道后、编辑保存后、点「重新探测」、点「测试连接」；周期循环里异常渠道的轻探测通过时 |

周期循环的规则是**轻探测只降不升**：

- 健康渠道每轮只做轻探测，失败即标为异常。
- 异常渠道先做轻探测；通过后再做一次深探测，写、读、删全部通过才恢复健康。
- 只读成功不能推翻深探测的失败结论。例如只读凭证能通过轻探测，却写不进去，这类渠道不会被轻探测「救活」。

周期探测只用只读的轻探测：对按量计费的存储，每分钟写删一次探测对象是无谓的费用；写路径的问题由事件触发的深探测与恢复前的确认把关。

各渠道并行探测、各自计时，一个慢渠道不会拖累其他渠道。清理中的渠道不参与探测。停机时正在进行的一轮作废，不写入结果。

探测结果写回元数据库，在「存储渠道」页的「状态」列可见：「健康」「异常」或「清理中」；鼠标悬停在「异常」上显示失败原因（已翻译成排障提示，不含凭证）；下方小字是最近一次探测的时间，从未探测过显示「未探测」。常见失败原因与排查见[对象存储与 WebDAV](./object-storage.mdx)的「常见报错与排查」。

<Callout type="info" title="渠道无法装载时">
某个渠道的配置无法解密、或后端初始化失败时，该渠道会被跳过（日志 `渠道装载失败,本渠道跳过,健康检查时重试`），其余渠道照常服务。它在列表里显示为「异常」，失败原因以 `装载失败:` 开头。健康检查每一轮都会先重试装载，点「重新探测」也会先尝试装载；装上之后还要通过一次深探测才恢复健康、参与选路。启动时全部渠道都无法装载，则启动失败。
</Callout>

## 副本水位 [#副本水位]

「副本」列显示该渠道上的副本统计：「就绪」「待补」（含补齐进行中）「失败」。待补长期不降、或出现失败，说明这个渠道写入有问题：先看状态列的失败原因，点「重新探测」确认。渠道恢复健康后，失败副本会在下一次周期对账时重新补齐。

## 测试连接 [#测试连接]

创建与编辑表单底部的「测试连接」用表单当前内容做一次深探测，**不保存、不建渠道**：

- 通过显示「连接正常（写读删探测通过）」；失败显示「连接失败：」加原因，原因同样经过翻译，并抹掉表单里填的凭证。
- 不会创建存储桶，也不套加密层：验证的是后端连通性与写、读、删权限。
- 编辑时，保持掩码的凭证会先还原成已保存的原值再探测；沿用的已存令牌如果在测试中被刷新，新令牌直接写回该渠道。
- 本次表单里提交的令牌如果在测试中被服务商刷新（失败时也可能发生），新令牌交还给表单：结论下方提示「服务商在测试时刷新了授权令牌，表单已换用新令牌，保存时提交的就是它；原令牌可能已失效。」，授权栏显示「已授权」，之后保存提交的就是新令牌。详见[网盘与 OAuth](./cloud-drives.mdx)。

## 创建渠道 [#创建渠道]

「存储渠道」页点「新建渠道」，先选存储类型，再填表单：

<Steps>
<Step>
选类型：「对象存储」「网盘」下是内置模板，「高级」下是「自定义」（直接填 rclone 后端名与参数）。各模板的字段见[对象存储与 WebDAV](./object-storage.mdx)与[网盘与 OAuth](./cloud-drives.mdx)。
</Step>
<Step>
填写渠道名、模板字段、优先级，选择「服务端加密」；对象存储可开「允许 302 直链」与「自动创建存储桶」。
</Step>
<Step>
点「测试连接」，通过后点「创建」。
</Step>
</Steps>

创建时先用填写的参数试构造一次后端（需授权的模板还会先补齐授权带出的参数，如 OneDrive 的驱动器），参数非法直接拒绝；入库后立即做一次深探测。深探测失败不会撤销创建：渠道保留，状态为「异常」，并提示「渠道已创建，但探测失败：」加原因，改好配置后点「重新探测」即可。

## 编辑渠道 [#编辑渠道]

行菜单「编辑」打开与创建相同的表单，可以改：渠道名、模板字段（或自定义参数）、优先级、「允许 302 直链」。

- **不能改**：「服务端加密」（表单只显示「已加密」或「未加密」，接口拒绝变更并返回 409）；存储类型（编辑沿用渠道创建时的模板；自定义渠道与内置 local 渠道以自定义表单打开）；「自动创建存储桶」只在创建时出现。
- **凭证掩码回读**：编辑表单回读配置时，凭证不下发，以固定的掩码值 `__imgyoung_secret_keep__` 代替。掩码范围以 rclone 后端的选项元数据为准：rclone 标记为密码或敏感信息的参数都掩码，除密码、密钥与授权令牌外，还包括访问密钥 ID（Access Key、SecretId、Key ID）、WebDAV 用户名、应用 Client ID、OneDrive 的 `drive_id` 等；模板表单与自定义参数同一口径，这些栏以掩码输入框显示。保持掩码原样提交即沿用已保存的原值，输入新值即替换；掩码值填在不属于凭证的参数上会被拒绝（400）。
- 配置整体替换，不做增量合并。保存后只重建这一个渠道的驱动实例，其他渠道正在进行的读写不受影响；随后立即深探测，失败时提示「渠道已保存，但探测失败：」加原因。
- **绑定时刻重置**：后端名或任何配置参数（包括公开访问地址）有改动，即把渠道的绑定时刻重置为保存时刻。重新输入同一个密码也算改动，因为 rclone 编码密码时带随机 IV。只改渠道名、优先级或「允许 302 直链」不重置，令牌刷新写回也不重置。重置之后，新位置上早于这一刻的本服务格式对象只计数、不回收，见[元数据库备份](../operations/backup.mdx)的「对账 GC」。

<Callout type="warn" title="不要用编辑来迁移存储位置">
编辑不会触发回填。改了桶、前缀或服务端点后，这个渠道上原有的副本仍记为就绪，读取会失败并切到其他渠道，直到每日对账发现对象缺失、重新补齐。要换存储位置，应新建一个渠道，等「副本」列的「待补」归零后再删除旧渠道。
</Callout>

## 删除渠道 [#删除渠道]

行菜单「删除」打开确认框，勾选项「同时删除本服务写在该渠道上的数据」决定走哪条路径：

| | 删除并清理（默认勾选）| 仅移除（取消勾选）|
|---|---|---|
| 立即效果 | 渠道转为「清理中」，立刻退出上传选路、补齐与探测 | 渠道与其副本记录立即删除 |
| 远端对象 | 本服务写的对象（图片副本、元数据库快照等）由后台排空任务逐个删除，删完自动移除渠道；别的程序写的文件不动 | 图片副本与元数据库快照原样保留，需要时自行到存储商处理 |
| 适用 | 正常下线一个渠道 | 凭证已失效、桶已不存在，或清理卡住 |

两条路径都有**唯一副本保护**：如果某张图在其他渠道上没有就绪副本（清理中的渠道不算），删除会被拒绝，返回 409 `该渠道持有部分图片的唯一副本,删除会丢数据;请先迁移或删除相关图片`。检查与删除在同一事务中完成，两个渠道先后删除也不会把同一张图的副本删光。在其他渠道上仍是「待补」的副本不算数，等它们补齐后再删。

清理的细节：

- 排空任务先按副本记录逐批删除图片对象并删掉对应记录，进度保存在元数据库里，中断后从剩余部分继续。
- 图片副本删完后，再删 `_imgyoung/` 下的全部对象（含 `_imgyoung/backup/` 下的元数据库快照）与根目录下的深探测残留 `.imgyoung-probe-*`，然后才删渠道记录。别的程序写的文件不动。完成时的日志 `渠道已清理完毕并删除` 带字段 `objects`（图片副本数）与 `system_objects`（系统对象数）。
- 单次排空最长 1 小时，超时后由下一次排空从剩余部分继续。
- 删除对象一失败，原因就写进状态列（以 `清理渠道对象失败,会自动重试` 开头），同时开启「渠道排空失败」告警。排空按退避持续自动重试，不会停止，渠道恢复可用后会自行清完。排空自身超时或停机取消不算失败。
- 凭证已失效或存储桶已不存在时，用行菜单里唯一的「直接移除」：停止清理并立即移除渠道，尚未删除的远端对象原样保留。

## 导出 rclone 配置 [#导出-rclone-配置]

行菜单「导出 rclone 配置」（清理中的渠道没有这一项）下载渠道的 rclone 配置文件 `imgyoung-channel-<渠道 ID>.rclone.conf`。标准 rclone 凭它就能脱离 ImgYoung 读写这个渠道上的数据：

- 远端 `imgyoung-<渠道 ID>`：渠道的后端，参数与 ImgYoung 交给后端的完全一致，含访问凭证与授权令牌。
- 远端 `imgyoung-<渠道 ID>-crypt`：只有加密渠道才有，是指向渠道存储路径的加密层，经它读写的就是明文。
- 文件开头的注释写明各远端的含义与用法示例。

文件里有渠道凭证（加密渠道还有解密密码），弹窗提示「和主密钥一样离线妥善保存：拿到它的人可以直接读写这个渠道上的全部数据。」。每次下载都记一条审计日志「导出渠道 rclone 配置」。文件的用法与注意事项见[渠道存储加密](./encryption.mdx)的「自救」。

## 用量监控与告警 [#用量监控与告警]

渠道用量按「该渠道上已就绪副本的图片大小之和」统计，是逻辑用量：不含元数据库快照与孤儿对象，也不等于存储商账单。

- 每 `usage_report_seconds`（默认 300 秒）刷新一次，写入指标 `imgyoung_channel_storage_bytes{channel="…"}`；「总览」页的「存储渠道」卡片展示各渠道的已存字节与占比。
- 告警阈值是「设置 → 运行配置 → 可靠性与渠道 → 渠道用量告警阈值」（`channel_quota_gb`，单位 GB，按 2³⁰ 字节计）；0 表示只上报、不告警。超过阈值开启「渠道用量超阈值」告警。

与渠道相关的告警共四类，和健康探测同一节奏评估，记录在「设置 → 告警历史」，配置了告警 Webhook 时同步外发：

| 告警 | 开启条件 | 恢复条件 |
|---|---|---|
| 渠道不健康 | 在役渠道健康标记为异常 | 探测恢复健康 |
| 副本补齐失败 | 渠道上存在 `failed` 副本 | 渠道上不再有 `failed` 副本（渠道健康时，周期对账会把它们复位重试）|
| 渠道用量超阈值 | 用量大于阈值 | 用量回落或阈值调高 |
| 渠道排空失败 | 清理中渠道删除对象失败 | 渠道被移除（清完自动移除或直接移除）|

其他相关指标：`imgyoung_channels_healthy_total`（健康渠道数）、`imgyoung_channels_total`（已装载且不在清理中的渠道数，含异常渠道）、`imgyoung_channel_puts_total{channel, result}`（向渠道写对象的次数，`result` 为 `ok` 或 `error`，含同步写入与异步补齐）。指标与告警的接入见[监控与告警](../operations/monitoring.mdx)。

## 接下来 [#接下来]

<Cards>
  <Card title="对象存储与 WebDAV" href="./object-storage.mdx" />
  <Card title="网盘与 OAuth" href="./cloud-drives.mdx" />
  <Card title="渠道存储加密" href="./encryption.mdx" />
  <Card title="元数据库备份" href="../operations/backup.mdx" />
</Cards>
