LiveKit 本地录制落盘路径排查与修复实践
-
- 一、背景
- 二、问题现象(按时间顺序)
- 三、排查过程(证据链)
- 四、根因
- 五、修复方案(仓库代码已改,编译通过)
-
- [1. `LiveKitEgressService.java` --- `buildPublicUrl` 去掉 `recordings/` 前缀](#1.
LiveKitEgressService.java—buildPublicUrl去掉recordings/前缀) - [2. `docker-compose.yml` --- 移除无效的 `working_dir`,注释改为真实行为](#2.
docker-compose.yml— 移除无效的working_dir,注释改为真实行为) - [3. `egress.yaml` --- 注释按真相重写](#3.
egress.yaml— 注释按真相重写)
- [1. `LiveKitEgressService.java` --- `buildPublicUrl` 去掉 `recordings/` 前缀](#1.
- 六、服务器操作步骤
- 七、关键结论与注意事项
- 八、本会话其他事项
-
- [1. 多环境 livekit 配置补齐(已完成)](#1. 多环境 livekit 配置补齐(已完成))
- [2. 会议待办列表不显示问题(已给排查方向,待用户验证)](#2. 会议待办列表不显示问题(已给排查方向,待用户验证))
- [附录:egress v1.14.1 相关源码位置(GitHub)](#附录:egress v1.14.1 相关源码位置(GitHub))
- [附:IT 疑难杂症诊疗室 ------ 技术文章通用大纲模板](#附:IT 疑难杂症诊疗室 —— 技术文章通用大纲模板)
一、背景
录制存储从 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/
二、问题现象(按时间顺序)
-
录制失败:permission denied
egress 日志报:
egress_failed ... "error": "Local upload failed: mkdir /recordings/meeting-xxx/: permission denied", "code": 400宿主目录属主为
root:root(755),容器内 egress 用户无写权限,文件从未生成。 -
修复权限后,文件落盘位置与 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
- 磁盘实际:
-
尝试
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 ✓
七、关键结论与注意事项
- 权限 :宿主录制目录必须允许容器内 egress 用户写入。实测
livekit/egress:v1.14.1的 egress 用户 UID=1001 (非 1000),先docker exec livekit-egress id确认再chown,或直接chmod 777。 - 挂载点 :必须是
/recordings,与后端下发的 filepath 首段recordings严格一致;改挂载点 = 丢文件。 working_dir陷阱:对 egress v1.14.1 的落盘路径无效,只有 service 主进程受影响。- URL 与磁盘的对应关系 :
- URL:
{public-prefix}/livekit-recordings/{roomName}/{时间戳}.ogg(无recordings/) - 磁盘:
{local-dir}/{roomName}/{时间戳}.ogg - DB
file_path:recordings/{roomName}/{时间戳}.ogg(egress 内部 key,与 URL 不同是正常的)
- URL:
- manifest
.json与录制文件同目录落盘,不影响播放。 - 切回对象存储 时恢复 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 路径 + 关键函数)
- 相关日志、配置、命令的补充说明
- 关联问题/后续待办(如本会话其他事项)
写作心法:每篇「疑难杂症」都按「现象 → 证据链 → 根因 → 修复 → 医嘱」五段式推进,让读者既能复现排查思路,又能直接照抄修复命令。