# 存储预算与保留(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`(条数上限,缺省 5000)仍然有效。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/?…`、`/tv/…`)命中时把过期时间推到「现在 + 详情 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,不参与汇总)。 ## 运行状态页「存储」节(FIX-5) 控制台 → 运行状态最下方。`GET /system/health` 的 `storage.level / alerts` 显示为档位与告警条(横幅也会提示), `GET /system/storage` 显示:数据库体积 / 图片缓存 / 备份 / 会话与证据对照预算的用量条(竖线是告警线)、体积前 10 的表、 `kv_cache` 各类缓存的条数 / 已过期 / 体积、DATA_DIR 各子目录占用、各清理任务的保留期与上次结果。每行「立即清理」调 `POST /system/storage/cleanup/{target}`(已在排队或运行中则合并),结果看计划页的运行记录。明细每分钟刷新,目录用量服务端缓存 5 分钟。 代码:`apps/web/app/components/status/storage-section.tsx`。 ## 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/`; 死元组多说明 autovacuum 跟不上,可手动 `VACUUM (VERBOSE) <表>`。 - 清理摘要一直是「未删完」:积压太多,每次 5 分钟删不完。可以把任务周期临时调短,或在低峰期多触发几次。 - 图片缓存一直 warn:LRU 任务是否启用、最近一次是否失败;磁盘上是否有大量 `.tmp` 残留(不计入用量,可手动删)。 - 历史曲线只能看 24 小时:这是 `heartbeatHours` 的缺省,要看更久就调大。