跳转到正文

系统架构

rust-srec 是一套自动录播系统,整体设计强调清晰的关注点分离:

  • 控制面:REST API + 配置管理 + 编排/调度
  • 数据面:直播状态探测 + 下载 + 弹幕 + 后处理
  • 持久化层:SQLite + 文件系统输出

系统以 Tokio 为运行时,由 ServiceContainer 统一初始化并管理各类长期运行的服务。

高层拓扑

运行时服务之间的箭头表示逻辑事件路径。ServiceContainer 通过广播订阅、有界队列和处理 任务完成这些接线,而不是让服务彼此直接耦合。

该拓扑中有三个重要的职责边界:

  • ServiceContainer 是组合根与事件接线层,并不持有领域状态的所有权。
  • StreamMonitor 负责探测与过滤平台状态;SessionLifecycle 独占内存中的会话状态机以及 持久化启动/结束决策。
  • 直播事件会先进入下载启动流程;只有下载管理器的 start_with_slot 返回真实下载 ID 后, 才会开始弹幕采集。

运行时根:ServiceContainer

ServiceContainer(位于 rust-srec/src/services/container.rs)负责把所有组件串起来:

  • 初始化仓储与服务(数据库、配置缓存、各类 manager/service)
  • 启动后台任务(scheduler actors、pipeline workers、outbox flushers)
  • 订阅事件流,并在服务之间转发/协调事件
  • 持有用于优雅退出的 CancellationToken

这让系统的生命周期与依赖关系有一个统一的“入口点”,方便定位与演进。

核心组件(按实际实现)

ConfigService(配置合并 + 热更新)

ConfigService 是配置控制面,负责加载并合并四层配置:

  1. 全局默认(Global)
  2. 平台配置(Platform)
  3. 模板配置(Template)
  4. 主播覆盖(Streamer overrides)

它会缓存合并后的结果,并广播 ConfigUpdateEvent,让运行时服务可以无重启响应配置变更。

参见:配置

StreamerManager(运行时状态“事实来源”)

StreamerManager 维护运行时所需的主播元数据(内存态),并对关键变更执行 写穿(write-through) 持久化到 SQLite。

一个重要的正确性细节:启动时会执行 重启恢复,把数据库中遗留的 Live 状态重置为 NotLive,确保 NotLive → Live 这条边能够再次触发下载启动。

Scheduler(Actor 模型编排/调度)

Scheduler 采用 supervisor + actor 的结构:

  • StreamerActor:单个主播的自调度状态循环(自己管理定时)
  • PlatformActor:对支持批量探测的平台进行批量协调
  • Supervisor:负责 actor 生命周期、崩溃恢复、退出汇总

Actor 会调用 StreamMonitor 做真实状态探测;Scheduler 同时订阅配置事件,动态创建/移除 actor。

StreamMonitor(探测 + 过滤 + Outbox)

StreamMonitor 是数据面的探测器,负责:

  • 根据 URL/平台解析直播状态(含过滤:时间/关键词/分类等)
  • 将会话变更委托给 SessionLifecycle
  • 通过 DB-backed Outbox 机制发出 MonitorEvent

Outbox 模式:将“状态/会话变更”与“事件写入 outbox”放在同一 DB 事务里,然后由后台 任务定期/通知触发,把 outbox flush 到 Tokio broadcast 事件流,从而降低 “状态已变更但事件丢失”的风险。

SessionLifecycle(会话状态的唯一所有者)

SessionLifecycle 负责录制状态机,包括滞后窗口(hysteresis)和终止原因分类。新的会话 启动和持久化结束会先提交各自所需的数据库变更,再广播 StartedEnded。滞后阶段的 EndingResumed 属于内存状态转换,其审计写入是 best-effort;在生命周期真正进入 Ended 前,session 的 end_time 保持为空。下载终止事件会回流至此服务。Ended 会驱动 会话完成管道、弹幕清理与下载状态清理;恢复后的 Started 会重启同一会话。

DownloadManager(下载调度 + 引擎抽象)

DownloadManager 负责:

  • 并发控制(含高优先级额外并发槽位)
  • 失败分类与熔断器(按引擎类型、配置以及可选的主播范围隔离)
  • 失败/拒绝事件与 retry-after 提示;由 Scheduler Actor 决定何时重新探测并再次进入下载 启动流程
  • 引擎抽象:
    • 外部进程:ffmpegstreamlink
    • 内置 Rust 引擎:mesio

并通过 DownloadManagerEvent 广播下载生命周期与分段事件。

对于落库后的 session 分段,后端会保留三种不同含义的时间戳:

  • created_at:该分段开始录制的时间
  • completed_at:该分段结束录制的时间
  • persisted_at:该分段元数据写入 SQLite 的时间

DanmuService(弹幕/聊天采集)

弹幕采集以 session 为单位维持连接,以 segment 为单位落盘:

  • session 期间维持 websocket 连接与统计(可选)
  • 由下载分段边界驱动,开启/结束对应的弹幕文件(如 XML)
  • Danmu 事件会转发到 pipeline,用于“视频+弹幕配对”等协调逻辑

PipelineManager(队列 + DAG + WorkerPool)

PipelineManager 是后处理引擎:

  • DB-backed job queue(支持重启恢复)
  • DAG 执行(fan-in / fan-out、fail-fast)
  • CPU/IO 分离的 worker pool
  • 多阶段触发协调:
    • Segment pipeline(单个文件)
    • Paired-segment pipeline(同一分段的 视频 + 弹幕)
    • Session-complete pipeline(会话结束后、所有分段完成后触发)

参见:DAG 管道

NotificationService(事件分发)

NotificationService 订阅监控/下载/会话/管道事件,并分发到 Discord / Email / Gotify / Telegram / Webhook 通道,包含重试、熔断与 dead-letter 持久化。可选的浏览器 Web Push 由 WebPushService 处理。

参见:通知

关键流程

录制生命周期(端到端)

API 请求流(控制面)

配置 JWT 后,大多数受保护路由使用 JWT 中间件。完整健康检查与就绪检查会在处理器内部 校验 bearer token;未配置 JWT 鉴权时也会返回 401。liveness 路由保持公开。WebSocket、 媒体与流代理路由使用各自文档中说明的查询参数鉴权路径。

事件驱动通信

跨服务协调主要依赖 Tokio broadcast

事件流发布者典型消费者备注
ConfigUpdateEventConfigServiceStreamerManagerSchedulerServiceContainer驱动 Actor 变更、运行时重配置与资源清理
MonitorEventStreamMonitorServiceContainerNotificationService通过 DB outbox 发出,提高一致性
DownloadManagerEventDownloadManagerSchedulerNotificationServiceServiceContainer 处理器处理器将分段交给 PipelineManager,将终止结果交给 SessionLifecycle
SessionTransitionSessionLifecycleServiceContainer 处理器、NotificationServiceEnded 驱动清理与会话管道;恢复后的 Started 重启同一会话
DanmuEventDanmuServiceServiceContainer 处理器处理器将分段配对交给 PipelineManager,将终止信号交给下载/会话处理
PipelineEventPipelineManagerNotificationService作业生命周期与可观测性

关于限流/节流

PipelineManager 内置可选的节流系统(ThrottleController)。若注入 DownloadLimitAdjuster,可以根据队列压力动态调节下载并发。

输出根写入门

下载管理器内置了一个输出根写入门downloader::output_root_gate),它工作在文件系统边界上,作为运行在网络/进程边界上的引擎熔断器(circuit breaker)的互补机制。设计目标是:当文件系统出现单点故障(磁盘写满、绑定挂载失效、权限丢失)时,不让这次故障级联成数十次每主播的重试,淹没日志和数据库 outbox。

Healthy ──(record_failure:启动前 ENOENT / 运行时 ENOSPC / 启动探测)──► Degraded

                            (mark_healthy:下一次真实 ensure_output_dir 成功)│
Healthy ◄────────────────────────────────────────────────────────────────┘

关键特性:

  • 无锁热路径。在 Healthy 状态下,check() 只做一次原子加载加一次 DashMap::get,没有互斥锁,也没有空跑成本。
  • 基于 CAS 的单飞冷却。当根处于 Degraded 时,每个冷却窗口(默认 30 秒)只允许一个调用方通过,去尝试真实的 create_dir_all;其他并发调用方以缓存的错误快速拒绝。这借鉴了 CircuitBreaker 的 half-open 模式。
  • 没有后台探测任务。真实的 ensure_output_dir 调用本身就是探测——写入门复用实际的下载尝试作为探测信号。容器启动时会运行一次有界的一次性探测,以便在第一秒就发现已经坏掉的挂载点。
  • 恢复钩子。在 Degraded → Healthy 的切换时,写入门会清除所有因它而退避的主播的 consecutive_error_countdisabled_untillast_error(通过 "output-root blocked:" 前缀过滤)。受影响的主播整队会在同一次监视周期内恢复。
  • 每次状态切换只发出一条通知Healthy → Degraded 的 CAS 同时也是决定"哪个调用方负责发出 critical 级 output_path_inaccessible 通知"的位置——无论有多少并发主播受影响,用户只会看到一条告警。

写入门在 /api/health 中以一个聚合的 output-root 组件暴露,列出所有 Degraded 根及其分类后的 io::ErrorKind、被拒绝次数和上次尝试的时间。参见通知系统文档了解事件形态,以及 Docker 故障排查了解挂载失效的失败模式。

可观测性、健康检查与优雅退出

  • 日志:使用 tracing,支持动态调整过滤器并带日志保留清理
  • 健康检查:
    • GET /api/health/live(无鉴权,适合作为容器 liveness)
    • GET /api/healthGET /api/health/ready 需要有效 bearer token;未配置 JWT 鉴权时 返回 401
  • 退出:
    • 独立后端把 SQLite、套接字和录制文件都限制在操作系统隔离的工作进程中。父进程的专用 线程会观测终止信号,即使启动或异步运行时工作阻塞,也会立即启用绝对关闭期限。看门狗会 持续运行,直到持久化标记更新、终止诊断和父进程退出全部完成。
    • ServiceContainer 在工作进程内执行分阶段优雅关闭;在最终分段事实持久化之前, 必需的事件消费者会保持运行。
    • 所有支持的平台都通过 SIGINT 触发优雅退出;Unix 还会处理 SIGTERM。父进程和工作 进程都注册了信号处理,因此像 KillMode=control-grouppkill 那样直接打到工作进程 的信号,与经由控制管道转发的信号走同一条优雅收尾流程。工作进程内部的致命故障仍会让它 立即失败退出;父进程随后终止其后代并保留恢复状态。
    • 被强制终止或崩溃的工作进程会在 SQLite 旁保留未清理运行世代标记,提醒下次启动可能 需要恢复。更早的恢复事项不会被之后正常退出的运行世代清除;该标记只负责检测, 并不负责重放文件。
    • 标记只保留最早和最新的未清理世代,中间的世代记为数量,因此反复重启也不会让它无限 变大。启动和退出时的提示会写明还有多少个世代等待恢复。

基于 MIT 许可证发布。