LiveKit本地录制落盘路径排查与修复实践

LiveKit 本地录制落盘路径排查与修复实践

一、背景

录制存储从 MinIO/S3 切换为本地目录模式 (egress.yaml 移除 storage.s3 块,egress v1.14.1 自动回退为本地文件输出),由后端 Spring 静态资源映射对外提供播放地址:

  • 宿主机目录:/data/data/oss/livekit-recordings
  • docker-compose 挂载:/data/data/oss/livekit-recordings:/recordings
  • 后端映射:/livekit-recordings/** → file:{livekit.record.local-dir}/(WebConfig)
  • 访问前缀:https://xxx.com/skillApi/livekit-recordings/

二、问题现象(按时间顺序)

  1. 录制失败:permission denied

    egress 日志报:

    复制代码
    egress_failed ... "error": "Local upload failed: mkdir /recordings/meeting-xxx/: permission denied", "code": 400

    宿主目录属主为 root:root(755),容器内 egress 用户无写权限,文件从未生成。

  2. 修复权限后,文件落盘位置与 URL 不匹配

    • 磁盘实际:/data/data/oss/livekit-recordings/meeting-xxx/20261012_181005.ogg(没有 recordings/ 层)
    • 访问 URL:.../skillApi/livekit-recordings/recordings/meeting-xxx/20261012_181005.ogg
    • 结果:500 "系统异常: No static resource recordings/meeting-xxx/..." / 404
  3. 尝试 working_dir: /recordings 无效

    加了 working_dir 并重建容器后(docker exec livekit-egress pwd 输出 /recordings),新录制文件仍然 落在 {挂载点}/meeting-xxx/...,行为与旧容器完全一致。


三、排查过程(证据链)

步骤 证据 结论
查 egress 挂载 docker inspect:/data/data/oss/livekit-recordings → /recordings (rw) 挂载正常
查落盘位置 find 宿主目录:文件在 {挂载点}/meeting-xxx/... 少了一层 recordings/
查进程工作目录 docker exec livekit-egress pwd → /recordings 容器 WorkingDir 生效了
查时间线 容器 StartedAt 18:09:06 < 新目录 Birth 18:26:50 新录制确实由新容器产生
查 DB 数据 video_recording.file_path = recordings/meeting-xxx/xxx.ogg 后端发给 egress 的 filepath 带 recordings/ 前缀,没问题
查镜像 livekit/egress:v1.14.1 与源码一致
查 egress 源码 pkg/server/server_rpc.go → launchProcess() 找到根因

四、根因

egress v1.14.1 的 service 每发起一次录制,都会 fork 一个 run-handler 子进程,子进程的工作目录被硬编码为 /:

go 复制代码
// pkg/server/server_rpc.go - launchProcess()
cmd := exec.Command("egress", "run-handler")
cmd.Dir = "/"          // 硬编码,与容器 WorkingDir / working_dir 无关

真正执行上传的是这个 handler 子进程(storage.NewLocal(&LocalConfig{}) → StorageDir = filepath.Abs("") = 进程 CWD = "/"),因此:

复制代码
上传目标 = / + filepath
         = / + recordings/meeting-xxx/xxx.ogg
         = /recordings/meeting-xxx/xxx.ogg        (容器内)
         → {挂载点}/meeting-xxx/xxx.ogg           (宿主机)

结论:

  • working_dir: /recordings 对落盘位置零作用(只影响 service 主进程,上传在子进程里做)。
  • 挂载点 /recordings 恰好等于 filepath 首段 recordings,文件才没有丢进容器根目录------挂载点必须与 filepath 首段严格对齐,改挂载点就会丢文件。
  • 磁盘实际布局永远是 {local-dir}/{roomName}/{时间戳}.mp4|ogg,URL 里不能带 recordings/。

五、修复方案(仓库代码已改,编译通过)

1. LiveKitEgressService.java --- buildPublicUrl 去掉 recordings/ 前缀

java 复制代码
private String buildPublicUrl(String filepath) {
    String prefix = properties.getRecord().getPublicPrefix();
    if (filepath != null && filepath.startsWith("recordings/")) {
        filepath = filepath.substring("recordings/".length());
    }
    ...
}

新 URL = {public-prefix}/livekit-recordings/{roomName}/{时间戳}.mp4|ogg,与磁盘 {local-dir}/{roomName}/... 精确对齐。

注意:发给 egress 的 filepath(DB file_path)保持不变 ,仍是 recordings/{roomName}/...------它是挂载对齐的关键。

2. docker-compose.yml --- 移除无效的 working_dir,注释改为真实行为

yaml 复制代码
livekit-egress:
  ...
  volumes:
    - ./configs/egress.yaml:/etc/egress.yaml:ro
    # egress v1.14.1 的 handler 子进程工作目录被硬编码为 /(server_rpc.go: cmd.Dir="/"),
    # 上传目标 = / + filepath(recordings/{roomName}/...) = /recordings/{roomName}/...,
    # 挂载 /recordings 后最终落在宿主 {挂载点}/{roomName}/...。
    - /data/data/oss/livekit-recordings:/recordings

3. egress.yaml --- 注释按真相重写

核心要点写入注释,防止以后再按"工作目录"思路排查:

  • handler 子进程工作目录硬编码为 /,working_dir 无效;
  • 挂载点必须与 filepath 首段 recordings 一致;
  • 最终文件 = 容器 /recordings/{roomName}/... = 宿主 {挂载点}/{roomName}/...;
  • URL 由后端 buildPublicUrl 去掉 recordings/ 前缀。

六、服务器操作步骤

bash 复制代码
# 1) 重新打包部署后端(唯一必做项)

# 2) 存量记录的 file_url 修正(不挪文件,磁盘位置本来就是对的):
UPDATE video_recording
SET file_url = REPLACE(file_url, '/livekit-recordings/recordings/', '/livekit-recordings/')
WHERE file_url LIKE '%/livekit-recordings/recordings/%';

# 3) (可选)服务器 compose 里删除 working_dir 并重建 egress,纯清理、不影响上传

验证:重新录制后访问

复制代码
https://xxx.com/skillApi/livekit-recordings/meeting-xxx/20261012_181005.ogg

对应磁盘 /data/data/oss/livekit-recordings/meeting-xxx/20261012_181005.ogg ✓


七、关键结论与注意事项

  1. 权限 :宿主录制目录必须允许容器内 egress 用户写入。实测 livekit/egress:v1.14.1 的 egress 用户 UID=1001 (非 1000),先 docker exec livekit-egress id 确认再 chown,或直接 chmod 777。
  2. 挂载点 :必须是 /recordings,与后端下发的 filepath 首段 recordings 严格一致;改挂载点 = 丢文件。
  3. working_dir 陷阱:对 egress v1.14.1 的落盘路径无效,只有 service 主进程受影响。
  4. URL 与磁盘的对应关系 :
    • URL:{public-prefix}/livekit-recordings/{roomName}/{时间戳}.ogg(无 recordings/)
    • 磁盘:{local-dir}/{roomName}/{时间戳}.ogg
    • DB file_path:recordings/{roomName}/{时间戳}.ogg(egress 内部 key,与 URL 不同是正常的)
  5. manifest .json 与录制文件同目录落盘,不影响播放。
  6. 切回对象存储 时恢复 egress.yaml 的 storage.s3 即可,buildPublicUrl 的去前缀逻辑在 S3 模式下不受影响(webhook 的 location 为 http(s) 时优先采用)。

八、本会话其他事项

1. 多环境 livekit 配置补齐(已完成)

pengpeng / qingniao / renxin / saas / shengnong 五个环境配置文件此前没有 livekit: 块,已补充完整历史 livekit 配置(连接密钥、record、transcript 全量),非仅新增的两个键。

2. 会议待办列表不显示问题(已给排查方向,待用户验证)

POST /todo/item/add 创建成功但 GET /todo/item/listByDate 查不到。待办接口属于独立项目 grapetec-talker-mrg(本仓库无代码)。排查方向:

  • 新建待办初始状态可能是"未排期",而 listByDate 只返回"已排期";
  • 租户/用户隔离(tenantId、initiatorId、participants 不一致);
  • todoType 不匹配、日期格式问题;
  • 建议查 grape_todo_item 表状态字段,并对比 unscheduledList / page 接口。

附录:egress v1.14.1 相关源码位置(GitHub)

文件 要点
pkg/server/server_rpc.go launchProcess():cmd.Dir = "/" ------ 根因所在
pkg/config/output_file.go StorageFilepath = 请求 filepath(保留 recordings/ 前缀);LocalFilepath = TmpDir + 文件名
pkg/pipeline/sink/uploader/uploader.go 无 storage 配置时 getUploader → storage.NewLocal(&storage.LocalConfig{});Prefix 为空
storage/local.go(storage 库) StorageDir = filepath.Abs(conf.StorageDir);UploadFile → path.Join(StorageDir, storagePath),io.Copy
build/egress/entrypoint.sh 无 cd,exec /tini -- egress

附:IT 疑难杂症诊疗室 ------ 技术文章通用大纲模板

以下大纲基于本文《LiveKit 本地录制落盘路径排查与修复实践》的实战结构提炼,可作为「IT 疑难杂症诊疗室」系列文章的通用写作框架。

一、背景(病症概述)

  • 系统/组件在什么业务场景下运行(技术栈、部署形态、版本号)
  • 本次改造或变更引入了什么(如存储切换、配置调整、版本升级)
  • 一句话点出「看似正常、实则异常」的矛盾点,制造悬念

二、问题现象(症状清单,按时间顺序)

  • 按时间线列出用户可感知的异常表现(报错、404/500、行为不一致)
  • 每条现象附原始日志/报错原文(代码块),标注关键字段
  • 说明「尝试过的常规解法及为何无效」,为后文根因做铺垫

三、排查过程(证据链)

  • 用表格 组织排查步骤:步骤 | 证据 | 结论
  • 证据要可复现:docker inspect、find、pwd、DB 查询、源码定位等
  • 每步给出「排除/确认」的明确结论,逐步收窄范围
  • 最后一步指向根因所在(如源码某函数)

四、根因(病灶定位)

  • 用代码块贴出关键源码片段,标注问题行
  • 用路径推导 (/ + filepath = ...)直观展示错误链路
  • 用列表给出 2~4 条核心结论,每条都要「打破一个常见误区」

五、修复方案(对症下药)

  • 按文件/模块拆分小节(### 1. xxx.java、### 2. docker-compose.yml...)
  • 每个小节:改动前 → 改动后(代码块 diff 或注释说明)→ 改动理由
  • 用 blockquote 标注「易错提醒」:哪些地方不能动 、哪些是对齐关键

六、服务器操作步骤(落地执行)

  • 用 bash 代码块给出可复制执行的命令序列
  • 区分「必做项」与「可选清理项」
  • 给出验证方法:访问 URL + 对应磁盘路径,证明修复生效

七、关键结论与注意事项(医嘱)

  • 用有序列表沉淀 4~6 条「血泪教训」,每条独立成句、可直接复用
  • 覆盖:权限、路径对齐、配置陷阱、新旧模式兼容等维度
  • 补充「回退/切换方案」,说明如何恢复原状

八、附录(参考资料)

  • 相关源码文件位置(GitHub 路径 + 关键函数)
  • 相关日志、配置、命令的补充说明
  • 关联问题/后续待办(如本会话其他事项)

写作心法:每篇「疑难杂症」都按「现象 → 证据链 → 根因 → 修复 → 医嘱」五段式推进,让读者既能复现排查思路,又能直接照抄修复命令。

相关推荐
StevenLdh2 小时前
第八期 · 情绪闯关挑战盒:一个「无惩罚」成就系统的数据建模
java·微信小程序
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 入门案例:编写 Dubbo 服务消费者
java·dubbo·交友
IT研究室3 小时前
最新计算机毕业设计选题推荐-基于spring boot的教育学习平台-网站-文档指导-Java-springboot
java·spring boot·课程设计
亚林瓜子3 小时前
Spring Boot 集成 ip2region 离线 IP 库实战指南
java·spring boot·后端·ip·ip2region
盛世宏博智慧档案3 小时前
恒湿净化一体机联动开发:档案馆温湿度阈值判断与启停控制实践
java·后端·struts·恒湿机·加湿除湿消毒净化一体机
vipxieliang3 小时前
ValidX 应用于 HR系统员工信息验证:入职、离职、社保数据
java·spring boot·后端
huaweichenai3 小时前
spring boot实现base64上传
java·spring boot·后端
萧瑟余晖3 小时前
Spring Web综合实战与性能调优详解
java·spring
点纭3 小时前
spring ai1.0实战:ChatClient(2)
java·后端·spring