跳到正文

故障排查

按症状排查启动、播放鉴权、频道、内存、节目单、代理和容器中的常见问题。

更新于 Markdown 版本

请先找到与现象相符的章节,再依次查看「症状」「判断」和「处理」。每一节都列出了可直接搜索的日志关键字和接口消息。相关配置见运维

启动失败

配置校验报错

症状:进程立刻退出,日志只有一条 config load failed,带 errconfig 两个字段。

判断:配置在加载阶段做完整校验,任何一项不通过都直接拒绝启动,err 里就是具体原因。常见的几条:

错误信息 原因
auth.users must not be empty 没有配置任何用户
user "x" requires role 用户缺 role 字段
duplicate channel id "x" 频道 ID 重复
channel "x" references unknown upstream "y" 频道引用了不存在的 upstream
channel "x" ingress must be hls or dash ingress 只接受这两个值
channel "x" dash ingress requires global packager.keys_file DASH 频道未被停用,但没有全局密钥文件
egress.default unknown proxy "x" 默认出站线路指向了未定义的 [[proxies]]
egress.playlist_policy must be rewrite|passthrough|auto 播放列表策略取值错误
debug.pprof.listen must use a loopback IP pprof 监听地址不是回环
unsupported config extension "x" 配置文件后缀只接受 .toml.json.jsonc

处理:按提示改配置。注意 auth requires token_private_key, token_private_key_file, or server.data_dir for auto-managed Ed25519 keys 这一条,意思是三者至少要有一个:要么显式注入 Ed25519 私钥,要么留一个数据目录让 Kiln 自动生成。

媒体密钥文件校验失败

症状config load failederrpackager.keys_file: 开头。

判断:密钥文件在启动时一次性完整校验,逐行解析,第一处错误就中止。错误消息带行号:

  • line 3: expected kid:key:这一行没有冒号。
  • line 3: empty kid or key:冒号一侧为空。
  • line 3: key must be 32 hex characters, got 30:KID 与 key 都必须是 32 位十六进制字符。
  • line 3: key must not contain dashes:KID 允许写连字符分组,key 不允许。
  • line 3: kid is not hexadecimal:出现了非十六进制字符。
  • line 3: KID has a conflicting key:同一个 KID 在文件里对应了两个不同的 key。
  • no keys found:文件里全是空行和注释。

处理:修正后重启。密钥是启动时读入的,改文件不会热生效。相对路径按 kiln.toml 所在目录解析,不是按工作目录。

端口被占用

症状listening 之后紧跟一条 http server stoppederrlisten tcp :8080: bind: address already in use,进程退出。

判断与处理

ss -ltnp | grep 8080

要么停掉占用者,要么改 server.listen,或者用 KILN_LISTEN 临时换端口。同一台机器跑多个实例时,除了监听端口,data_dir 也必须各自独立。

数据目录不可写

症状create data dir failedsqlite open failed

判断:systemd unit 带 ProtectSystem=strict,整个文件系统对进程只读,只有 ReadWritePaths=/var/lib/kiln 是可写的。把 data_dir 指到别处一定会失败。

处理:把 data_dir 放回 /var/lib/kiln 下面,并确认属主是 kiln 用户。容器里以只读方式运行时,同样要给数据目录挂一个可写卷。

Lite 拒绝了配置

症状lite 变体启动即退出,错误明确说明某项能力不可用。

判断lite 对超出能力边界的配置采取直接拒绝而不是静默忽略的策略:

  • packager.engine must be native in lite
  • channel "x" requires native packager engine
  • epg is not available in lite
  • OpenTelemetry export is not available in lite
  • pprof is not available in lite

处理:删掉对应配置段,或者换用 core 镜像。

播放返回 401 或 403

先确认正在使用哪一类凭据。四种凭据各有用途,混用是最常见的原因:公开接口不需要凭据,会话 JWT 用于管理界面,管理员 API 令牌用于脚本,路径式播放密钥用于向播放器分发。详见 鉴权

返回 消息 判断
401 invalid token 会话 JWT 缺失、格式错误或签名不匹配
401 token expired 会话 JWT 过期,按 auth.token_ttl_hours 重新登录
401 invalid access token 路径式播放密钥无效、已停用、已吊销或已过期
403 channel not allowed 凭据有效,但该用户没有这个频道的权限
403 host not allowed 请求的 Host 头不在 security.public_hosts
403 API token permission denied 管理员 API 令牌缺少该路由要求的权限,或者这条路由只允许登录会话访问

凭据类型用错

症状:使用管理员 API 令牌调用 /v1/play/...,或者使用会话 JWT 访问 /p/<token>/...

处理:播放接口只认会话 JWT(/v1/play/...)或路径式播放密钥(/p/<token>/...)。API Token 用于管理接口,且部分路由被标记为仅会话可用,用 Token 访问会返回 403 API token permission denied

play_require_auth

症状:明明关掉了播放鉴权,播放接口还是要凭据。

判断security.play_require_auth 不写时默认要求鉴权。先确认配置里确实写了 play_require_auth = false,再确认环境里没有残留 KILN_PLAY_OPEN=0,环境变量的优先级高于配置文件。

处理:调试环境在 [security] 里写 play_require_auth = false,或设置 KILN_PLAY_OPEN=1

Host 不在允许列表

症状:换了域名或者加了反向代理之后,所有接口统一返回 403 host not allowed

判断:配置了 security.public_hosts 时,中间件会拿请求的 Host 头(去掉端口和方括号后)逐项比对,不匹配就拒绝。列表里写 * 表示放行全部。来自回环地址的 /healthz/readyz 被豁免,所以本机探针照常返回 200,外部请求却全是 403,这个反差本身就是判断依据。

处理:把对外域名加进 public_hosts。反向代理必须原样透传 Host,改写成后端地址会导致同样的 403。

频道起不来

上游不可达

症状:播放接口返回 502,消息为 upstream request failedupstream status 403 Forbidden: ...。日志里能看到 session restarting,带 attemptdelayerr;重试 8 次后变成 session restart budget exceeded,会话进入 failed

判断:重启退避从 2 秒起步,最长 30 秒,连续稳定 90 秒后计数清零。反复重启说明上游确实拿不到内容,而不是偶发抖动。

处理:在服务器上直接验证上游可达性,注意带上频道配置的 headersuser_agent。需要走代理的上游见下面的代理不生效

上游主机没有进允许列表

症状403,消息为 upstream host not allowederr 里是 private host not allowlisted: 192.168.1.10

判断:出站请求会做 SSRF 校验。公网主机默认放行,但回环地址和私有网段只有在 security.allowed_hosts 中显式列出才放行;云元数据地址(例如 169.254.169.254)和链路本地地址一律拒绝。在 upstreamschannels 中声明主机不会获得私网豁免。

处理:把内网源站的主机名或 IP 写进 security.allowed_hosts,然后重启。无论通过配置文件还是管理界面创建上游或频道,都不会自动加入。

引擎不支持这个源

症状502,消息为 engine=native but the source cannot be served natively

判断engine = "native" 表示不允许回退。原生引擎判定自己处理不了时直接失败,不会去找 ffmpeg。常见的不支持原因会体现在 fallback_reason 上:multi_period(多 period 的 MPD)、addressing_unsupportedmanifest_codec_unsupportedno_video_representationno_audio_representationmissing_key_for_kidmanifest_kid_conflicts_with_tenc

处理:把该频道的 packager 改成 auto,让它在必要时回退到兼容引擎;这需要运行在带 ffmpeg 的 full 变体上。或者换一个原生引擎能处理的源。

还有一种情况消息是 native cannot handle this source and ffmpeg would decode it incorrectly,此时即便设成 auto 也不会回退,因为兼容引擎的输出会是错的。这类源只能换源。

ffmpeg 不可用

症状503,消息为 ffmpeg compatibility engine is not available;或者需要回退时消息为 source requires ffmpeg compatibility, but the ffmpeg engine is not available/readyz 同时返回 503not_ready

判断:启动日志里有两个字段直接给出答案:

packager_engine=auto ffmpeg_available=false

进程启动时会做一次依赖探测,找不到就打印 ffmpeg compatibility engine unavailable,带 dependencyerr 字段,err 形如 find ffmpeg dependency "ffmpeg": exec: "ffmpeg": executable file not found in $PATH

处理

  • corelite 镜像不含 ffmpeg,需要兼容回退就换 full
  • Windows 发行版不含 ffmpeg,自行安装并加入 PATH
  • [ffmpeg].binary 指到了不存在的路径时,改成绝对路径或确保它在 PATH 里。
  • [ffmpeg].mode = "docker" 时,探测的是 docker 这个命令本身,宿主机上没有 docker 客户端一样会判定为不可用。

缺少解密密钥

症状400,消息为 no global keys configured;或者启动阶段就报 channel "x" dash ingress requires global packager.keys_file

处理:配置 [packager].keys_file,每行一对 kid:key。频道级 keys 字段已经移除,密钥只能全局配置。

播放中途停滞

症状:频道能起来,播放几分钟后卡住。日志里出现会话重启,errno segment published for 3m0s while the manifest kept updating

判断:这是 packager.stall_timeout_sec(默认 180 秒)的停滞检测。它的触发条件很具体:上游清单还在正常刷新,但所有轨道都在超时窗口内没有产出过新分片。两个条件必须同时成立。

  • 清单本身拉不动(上游不可达)不算停滞,走的是上面的上游错误路径。
  • 只要还有任意一条轨道在推进,就不算停滞。

命中之后会话被判定为致命错误并重启重排,重新解析清单、重新选轨。

处理:偶尔一次通常是上游侧的编码中断,重启重排就恢复了,不必调参。频繁触发说明上游长期只更新清单不产出分片,需要从上游查。确实需要更长的容忍窗口时调大 stall_timeout_sec,但这只会推迟恢复,不会修复根因。

内存偏高

症状:容器 RSS 超出预期,或者被 OOM Killer 杀掉。

判断:先用启动日志确认实际生效的档位与预算:

resource_profile=standard resource_constrained=true
effective_memory_mb=768 memory_limit_mb=192 effective_go_memory_limit_mb=192
inflight_mb=64 max_segment_mb=32 gc_percent=100 drop_file_cache=true

对照运维里的档位表检查是否落在预期档位。几个常见误解:

  • 这些数字是 Go 堆和媒体工作集的软预算,不是容器总 RSS 保证。SQLite、goroutine 栈、内核页缓存都在预算之外。
  • full 镜像回退到兼容引擎时会拉起 FFmpeg 子进程,它的内存完全在 Go 软目标之外。日志里的 FFmpeg memory is outside the Go soft limit 警告就是提醒这件事,带 ffmpeg_scope 字段区分是子进程还是外部容器。这条是提示(advisory_only=true),不影响运行。
  • 设了 GOMEMLIMIT 时,配置里的 server.memory_limit_mb 不再写入 Go 软目标。

处理

  • 调小 packager.inflight_bytes。这是跨频道共享的分片内存预算,调小能压低峰值内存,代价是高码率源的冷启动变慢。
  • 减少并发活跃频道数,或者降低 start_segmentsprefetch_segments
  • 需要一个可预测的单进程内存边界时,用 corelite 的原生引擎,不引入 FFmpeg 子进程。
  • 想验证低资源行为,用 KILN_RESOURCE_MODE=constrained 强制进入最紧的档位。

EPG 不刷新

症状:节目单为空或长期不更新。

判断:按顺序核对三件事。

源是否启用

EPG 没有总开关,是否有输出只看有没有已启用的源。内置源默认全部停用,需要在管理界面或配置里逐个启用。启动日志的 epg_sources 字段就是当前生效的源数量,为 0 说明一个都没启用。lite 变体不支持 EPG,配置里存在已启用的源会直接拒绝启动。

体积超限

日志里出现 EPG refresh failederrEPG source exceeds decompressed size limit 时,说明解压后的 XMLTV 超过了 epg.max_source_bytes。默认上限是 64 MiB,但在低资源档位下会被自动压到 4 MiB 到 64 MiB 之间的某个值,启动日志的 epg_max_source_mb 就是实际生效值。已缓存的内容超限时同样会被丢弃。

代理与网络

EPG refresh failederr 是连接超时或 TLS 错误时,说明节目单源不可达。每个 EPG 源都可以单独指定 proxy。设为 auto 时按全局网络出口规则选择线路;留空或设为 direct 时直接连接。

处理:磁盘缓存默认落在 data_dir/epg。缓存本身是可重建的,怀疑缓存损坏时可以直接删掉目录再重启。刷新间隔由 epg.refresh_interval_min 控制,默认 360 分钟,改小之后要注意源站的速率限制。详见 EPG

代理不生效

症状:配置了代理,但出站流量仍然直连,或者报代理相关错误。

规则没命中

判断:Kiln 按优先级从高到低检查规则,采用第一条匹配的规则;如果都不匹配,就使用 egress.defaultpriority 数值越小,优先级越高。规则分为五种:host_exacthost_suffix(默认)、host_regexchannel_idurl_regex

host_suffix 匹配的是主机名本身或者以 .pattern 结尾的主机名,不是简单的字符串包含。写 example.com 能命中 edge.example.com,但命中不了 notexample.com

处理:把更具体的规则的 priority 调小。确认匹配的是主机名而不是完整 URL,后者要用 url_regex

线路不存在或已停用

判断:活动规则指向不存在或已停用的线路时,配置加载与热更新都会直接失败,不会静默改成直连。错误会指出规则引用了 unknown or disabled proxyegress.default 引用未知线路也会拒绝启动。

处理:检查 [[proxies]] 里的 id 是否拼写一致,以及该线路是否被停用。

FFmpeg 容器连不上 Kiln

症状[ffmpeg].mode = "docker" 时,FFmpeg 容器无法连接 Kiln 启动的本地安全代理。

判断:Kiln 会让 FFmpeg 容器通过 egress.docker_proxy_host(默认 host.docker.internal)访问这个短期代理,并为默认主机名自动加入 host-gateway 映射。上游代理由 Kiln 自己连接,不会把凭据直接交给 FFmpeg。

处理:确认 docker_proxy_host 能从 FFmpeg 容器访问到 Kiln 进程;非标准 Docker 网络可以把它改成宿主机或 Kiln 所在网络的可达地址。

更完整的路由模型见出站代理

容器场景

探测不到预期档位

症状:启动日志的 resource_profile 不是预期档位,或者显示 configured

判断configured 表示配置值保持不变。resource_mode = "performance" 会主动关闭自适应;在 auto 模式下看到 configured,则说明内存探测没有取得有效上限。探测会读取 cgroup v2 的 memory.max、cgroup v1 的 memory.limit_in_bytes/proc/meminfoMemTotal,取其中最小的正值。嵌套 cgroup、非常规挂载布局或未设置内存限制时,结果可能不符合预期。

处理:用环境变量直接覆盖探测结果,跳过不确定的自动判定:

docker run --rm --cpus=1 --memory=192m --memory-swap=192m \
  -e KILN_RESOURCE_MEMORY_MB=192 -e KILN_RESOURCE_CPUS=1 \
  -v "$PWD/kiln.toml:/etc/kiln/kiln.toml:ro" \
  ghcr.io/babywbx/kiln:core

覆盖之后 effective_memory_mbeffective_cpu_milli 会直接反映你给的值。核对方法见运维

只读挂载

症状:以 --read-only 运行时启动失败,报 create data dir failed

判断:即便是 lite,也要写自动生成的登录密钥和临时媒体文件。只读根文件系统必须给数据目录单独挂一个可写卷。

处理

docker run --rm -p 8080:8080 --read-only \
  --cap-drop=ALL --security-opt=no-new-privileges \
  -v "$PWD/kiln.toml:/etc/kiln/kiln.toml:ro" \
  -v kiln-lite-data:/var/lib/kiln \
  ghcr.io/babywbx/kiln:lite

运行时变体不对

症状:日志里出现 unknown runtime variant; using standalone 警告,带 environment 字段。

判断corefull 镜像内置了 KILN_RUNTIME_VARIANT,手工设成了无法识别的值才会看到这条警告。启动日志的 runtime_variant 字段是最终生效值。

处理:不要手工设置这个变量,除非在自建镜像里。

健康检查一直不通

判断corefullHEALTHCHECKwget/healthzlite 基于 scratch,没有 shell 与 wget,用的是二进制自带的子命令。手工验证:

docker exec kiln wget -q -O /dev/null http://127.0.0.1:8080/healthz
docker exec kiln kiln -healthcheck http://127.0.0.1:8080/healthz

/healthz 通而 /readyz 不通,说明是兼容引擎缺失,回到上面的 ffmpeg 不可用

收集诊断信息

日志在哪

部署方式 命令
systemd journalctl -u kiln -f,历史用 journalctl -u kiln --since "1 hour ago"
Docker docker logs -f kiln
Windows 服务 配置文件目录下的 kiln.log,上一代为 kiln.log.1
前台运行 直接看终端输出

提高日志详细度

默认级别是 info,此时 /healthz/readyz/ 以及包含 /live//u/ 的高频路径都被降到 debug,不会出现在日志里。要看完整的分片请求流水,临时提级:

KILN_LOG_LEVEL=debug KILN_LOG_FORMAT=json kiln -config /etc/kiln/kiln.toml

systemd 环境下用 systemctl edit kiln 加一行 Environment=KILN_LOG_LEVEL=debug,Docker 直接加 -e KILN_LOG_LEVEL=debug。排查完记得改回来,debug 在高并发下的日志量相当可观。

日志重定向到文件时着色会自动关闭。想在保留终端的同时去掉 ANSI 序列,用 KILN_LOG_COLOR=never 或者设置 NO_COLOR

采集 pprof

内存或 CPU 异常时,临时开一次 pprof。监听地址必须是回环 IP,采完立刻关掉。

开启

配置里加上,然后重启。启动日志会多一条 pprof listening

[debug.pprof]
enabled = true
listen = "127.0.0.1:6060"

采集

远程机器先做端口转发:ssh -L 6060:127.0.0.1:6060 host

go tool pprof http://127.0.0.1:6060/debug/pprof/heap
go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30
go tool pprof http://127.0.0.1:6060/debug/pprof/block
go tool pprof http://127.0.0.1:6060/debug/pprof/mutex

关闭

enabled 改回 false 并重启。

指标里先看哪几个

/metricskiln_packager_segment_fetch_errors_totalkiln_packager_manifest_errors_total 的增速最能说明上游状况,kiln_packager_cache_bytes 反映当前占用的分片缓存,kiln_session_infostate 标签能一眼看出哪些会话进了 failed

提交问题时带上什么

  • kiln -version 的完整输出。
  • 启动日志里 kiln starting 那一整行。
  • 复现步骤,以及对应时段的日志片段。请自行去掉播放令牌与上游凭据;Kiln 自己写日志时已经把 /p/ 路径里的令牌截成前缀了,但你粘贴的配置和 URL 不会被自动处理。

问题追踪在 GitHub

导航

输入以搜索…

↑↓ 移动↵ 打开Esc 关闭