Files
UHD_Media_Tool/docs/runbook/storage.md
T
2026-09-29 17:16:50 +08:00

150 lines
10 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.
# 存储预算与保留(CACHE-2)
按 4C / 8G / 100G 的机器分配:PG ≤ 25 GB(20 GB 告警)、图片缓存 20 GB、备份 20 GB(14 份)、会话与证据 10 GB。
流水数据按保留期定时分批删除,缓存有效期延长,容量不够时按 LRU 淘汰。本轮只有后端与配置,设置页 UI 后续接。
## 设置 `settings.storage`
三组,都在「设置 → 自动化 → 存储与保留」(分区锚点 `storage`,UI 待接)。PATCH 可以只给子字段:
```sh
PATCH /api/v1/settings {"storage":{"retentionDays":{"observation":60}}}
```
| 字段 | 缺省 | 说明 |
|---|---|---|
| `budgets.pgDataGb` / `pgDataWarnGb` | 25 / 20 | PG 库体积(`pg_database_size`,不含 WAL)。≥ 告警线 warn,≥ 预算 critical |
| `budgets.imagesGb` | 20 | 图片代理缓存容量。超出由 `cache.images_lru` 按最久未访问删到 90% |
| `budgets.backupsGb` | 20 | `DATA_DIR/backups`。≥ 80% warn,≥ 预算 critical。份数仍由 `BACKUP_KEEP` 控制 |
| `budgets.sessionsEvidenceGb` | 10 | Agent 会话目录 + `DATA_DIR/tmp/evidence`。≥ 80% warn,≥ 预算 critical |
| `retentionDays.observation` | 90 | Feed 观察记录 |
| `retentionDays.sseEvent` | 7 | SSE 补读事件(发布方另按条数裁剪 10000 条) |
| `retentionDays.heartbeatHours` | 24 | 进程心跳采样(小时)。历史曲线最多只能看这么久 |
| `retentionDays.taskRun` | 60 | 已结束的业务任务 `task`、引擎 run(步骤级联)、运行元数据 |
| `retentionDays.agentEvent` | 90 | 已结束会话的事件流与已消费消息。`agent_run` 本身保留 |
| `retentionDays.audit` | 180 | 审计事件(能力调用审计除外) |
| `retentionDays.capabilityStats` | 90 | 能力调用审计(`target_type=capability`)与已结束的 `capability_invocation` |
| `retentionDays.identityPoolArchive` | 30 | 待识别池 unmatched 条目无新证据多少天后归档(不删) |
| `cacheTtl.tmdbSearchDays` | 7 | TMDB `/search/*`、`/find/*` 响应与识别缓存 |
| `cacheTtl.tmdbDetailDays` | 30 | TMDB 详情 / 季。追踪中的作品命中时续期 |
| `cacheTtl.tmdbMissDays` | 1 | TMDB 空结果与未识别负缓存 |
| `cacheTtl.siteDetailDays` | 7 | 站点详情页扩展内容(`kv_cache` `site.detail`) |
| `cacheTtl.webSearchDays` | 1 | 网页搜索结果 |
被取代的旧字段(保留在 schema 里兼容已存 JSON,服务端不再读取,已列入 `DEPRECATED_SETTINGS_FIELDS`):
`cache.tmdbTtlHours` → `cacheTtl.tmdbSearchDays / tmdbDetailDays`,`cache.emptyResultTtlSec` → `cacheTtl.tmdbMissDays`,
`identity.poolArchiveDays` → `retentionDays.identityPoolArchive`。设置页「元数据」里这三项接 UI 时要换成新字段。
`cache.tmdbMaxSize`(条数上限,缺省 256)仍然有效。TTL 拉长后条数上限会先起作用,需要时调大。
## 缓存有效期
只改 TTL 的取值来源,缓存语义还是对照 MP(见 [porting/moviepilot-tmdb-cache.md](../porting/moviepilot-tmdb-cache.md))。
- TMDB 请求缓存:键以 `/search/` 或 `/find/` 开头的用搜索 TTL,其余用详情 TTL。`results` 为空的用未命中 TTL。
- TMDB 识别缓存:正常条目用搜索 TTL(识别结果就是名称到条目的映射),负缓存 `{id:0}` 用未命中 TTL。
- 追踪中的作品续期:作品有未结束的订阅(active / paused / pending)且有已接受的 TMDB 身份,
它的详情键(`/movie/<id>?…`、`/tv/<id>…`)命中时把过期时间推到「现在 + 详情 TTL」,只延长不缩短。追踪集合每进程缓存 5 分钟。
- 站点详情:`kv_cache` TTL 取 `max(站点详情缓存期, siteDetailDays)`。是否重抓仍按站点详情缓存期
(`sites.defaultDetailCacheSec`,站点定义可更长)判断,下载前复核促销和 H&R 走的是这条。
- 改了 TTL:新写入的条目按新值,已有条目保持原过期时间。
## 图片缓存容量 LRU
- 读盘命中时把文件 atime 显式写成当前时间(`utimes`,不依赖挂载的 relatime),mtime 不动。
- `cache.images_lru`(维护组,30 分钟):总量超过 `imagesGb` 时按 atime 从旧到新删到预算的 90%;没超不动。
- `cache.images_clean`(每天,MP `clear_temp`)照旧按 mtime 删超过 `cache.globalImageCacheDays` 天的文件,作兜底。
- 存储面板里图片超过预算 10% 才告警(说明 LRU 没跟上),不给 critical。
## 保留清理任务
都在维护组,开关和周期在定时任务看板改。首次登记在一个周期后才第一次运行。
| key | 目标 | 默认周期 | 删除条件 |
|---|---|---|---|
| `storage.retention.observation` | `observation` | 6 小时 | `created_at` 早于保留期 |
| `storage.retention.sse_event` | `sse_event` | 1 小时 | `at` 早于保留期 |
| `system.heartbeat_cleanup` | 心跳 | 1 小时 | 进程行 24 小时无心跳;采样早于 `heartbeatHours` |
| `storage.retention.task_run` | `task`、引擎 `workflow_runs`、`tasks_run_meta` / `tasks_fact` / `tasks_op_ledger` | 6 小时 | 早于保留期且已结束(task:succeeded / failed / cancelled / dead;run:completed / succeeded / failed / canceled;账本只删 succeeded) |
| `storage.retention.agent_event` | `agent_run_event`、`agent_run_message` | 6 小时 | 早于保留期且会话已结束;消息只删已消费的 |
| `storage.retention.audit` | `audit_event` | 1 天 | `at` 早于保留期,`target_type` 不是 capability |
| `storage.retention.capability_stats` | `audit_event`(capability)、`capability_invocation` | 1 天 | 早于保留期;调用记录不删 running |
| `storage.retention.identity_pool` | `identity_candidate` | 6 小时 | pending + unmatched,`coalesce(last_attempt_at, created_at)` 早于保留期 → 改为 archived |
| `storage.analyze` | 全库 | 7 天 | `ANALYZE` |
| `cache.images_lru` | 图片缓存 | 30 分钟 | 见上 |
| `cache.sweep` | `kv_cache` | 10 分钟 | 已过期 |
分批删除(`modules/system/retention.ts` 的 `deleteInBatches`):
- 每批 5000 行:`delete … where ctid = any(array(select ctid … where <条件> limit 5000))`,单独事务。
- 每批 `statement_timeout` 15 秒、`lock_timeout` 3 秒。超时就结束本次运行,不算失败,摘要写「未删完,下次继续」。
- 每次运行最多 5 分钟,批间停 50 ms。第一次上线积压多时会分几次删完。
- 删除后空间由 autovacuum 回收复用,不会立即还给文件系统。确实要缩文件用 `pg_repack` 或停机 `VACUUM FULL`。
迁移 `0050_storage_retention`:给 `observation(created_at)`、`sse_event(at)`、`audit_event(at)`、`capability_invocation(created_at)`、
`agent_run_message(created_at)`、`tasks_fact(created_at)`、`tasks_run_meta(created_at)`、`tasks_op_ledger(started_at)` 补索引;
按表设 autovacuum:
- 追加加批量删除的大表(observation、sse_event、audit_event、agent_run_event、process_heartbeat_sample、capability_invocation、task、tasks_fact、tasks_run_meta):
`vacuum_scale_factor=0.05`、`analyze_scale_factor=0.02`、`vacuum_insert_scale_factor=0.1`。
- 频繁原地更新的小表(process_heartbeat、rate_limit_bucket、scheduled_job、kv_cache、outbox_event):
`vacuum_scale_factor=0.02`、`vacuum_threshold=100`、`analyze_scale_factor=0.05`。
查看:`select relname, reloptions from pg_class where reloptions is not null;`
## API
### GET /system/storage(admin)
| 字段 | 含义 |
|---|---|
| `level` | 各项最差档:ok / warn / critical,取不到的项 unknown 不参与 |
| `db` | 库体积对照 `pgDataGb` / `pgDataWarnGb`;`tables` 为体积前 15 的表(总 / 表 / 索引字节、估算行数、死元组、最近 autovacuum / analyze) |
| `kvCache[]` | 每个 region 的条数、其中已过期未清理的条数、字节 |
| `images` | 图片缓存用量、文件数、预算 |
| `backups` | 备份目录用量、份数(不含 `.partial`)、预算 |
| `sessionsEvidence` | 会话目录 + `tmp/evidence` 合计、预算 |
| `dataDir[]` | DATA_DIR 一级目录用量,`cache/`、`tmp/` 再细分一层;会话目录在 DATA_DIR 外时单列 |
| `dirsScannedAt` | 目录遍历时间。遍历结果缓存 5 分钟 |
| `cleanup[]` | 每个清理目标:任务 key、保留期说明、开关、最近运行时间 / 状态 / 摘要、下次运行 |
### POST /system/storage/cleanup/{target}(admin)
`target`:`observation`、`sse_event`、`heartbeat`、`task_run`、`agent_event`、`audit`、`capability_stats`、`identity_pool`、`analyze`、`images`、`kv_cache`。
和定时任务「立即执行」一样,只设置 `run_requested_at`,由调度器执行。已在排队或运行中返回 `queued:false`。
任务还没登记(调度器没同步过)返回 404。触发成功写审计 `storage.cleanup`。
### GET /system/health 的 `storage` 段
`{ level, alerts[] }`,level 计入总档位。库体积实时取;目录用量只用缓存值,过期时后台刷新,本次不等
(进程刚启动时目录项是 unknown,不参与汇总)。
## PG 参数
`docker-compose.yml` 的 postgres 服务按 4C / 8G 给 PG 3G 内存:
| 参数 | 缺省 | 环境变量 | 经验值 |
|---|---|---|---|
| 容器内存上限 | 3g | `PG_MEM_LIMIT` | 机器内存的 35–40% |
| `shared_buffers` | 2GB | `PG_SHARED_BUFFERS` | 容器上限的 60–70%(不超过机器内存的 25–30%) |
| `effective_cache_size` | 5GB | `PG_EFFECTIVE_CACHE_SIZE` | 机器内存的 60–70%(只影响规划,不占内存) |
| `work_mem` | 32MB | `PG_WORK_MEM` | 每个排序 / 哈希一份,连接多时调小 |
| `maintenance_work_mem` | 256MB | `PG_MAINTENANCE_WORK_MEM` | VACUUM / 建索引用 |
| `max_wal_size` | 2GB | `PG_MAX_WAL_SIZE` | 批量删除时减少检查点 |
另外 `shm_size: 512mb`(并行查询的动态共享内存走 /dev/shm)。
按机器调整,例如 2C/4G:`PG_MEM_LIMIT=1536m PG_SHARED_BUFFERS=768MB PG_EFFECTIVE_CACHE_SIZE=2GB PG_WORK_MEM=16MB PG_MAINTENANCE_WORK_MEM=128MB`;
8C/16G:`PG_MEM_LIMIT=6g PG_SHARED_BUFFERS=4GB PG_EFFECTIVE_CACHE_SIZE=10GB PG_WORK_MEM=64MB PG_MAINTENANCE_WORK_MEM=512MB`。
写进 `.env` 后 `docker compose up -d postgres` 重建容器生效(数据在卷里,不丢)。确认:`docker compose exec postgres psql -U umt -c 'show shared_buffers'`。
`shared_buffers` 大于容器上限会在启动时 OOM,两者一起改。
## 常见问题
- 面板上 PG 超预算:先看 `tables` 前几名。流水表大就临时调短对应保留期,再 `POST /system/storage/cleanup/<target>`;
死元组多说明 autovacuum 跟不上,可手动 `VACUUM (VERBOSE) <表>`。
- 清理摘要一直是「未删完」:积压太多,每次 5 分钟删不完。可以把任务周期临时调短,或在低峰期多触发几次。
- 图片缓存一直 warn:LRU 任务是否启用、最近一次是否失败;磁盘上是否有大量 `.tmp` 残留(不计入用量,可手动删)。
- 历史曲线只能看 24 小时:这是 `heartbeatHours` 的缺省,要看更久就调大。