请先找到与现象相符的章节,再依次查看「症状」「判断」和「处理」。每一节都列出了可直接搜索的日志关键字和接口消息。相关配置见运维。
启动失败
配置校验报错
症状:进程立刻退出,日志只有一条 config load failed,带 err 与 config 两个字段。
判断:配置在加载阶段做完整校验,任何一项不通过都直接拒绝启动,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 failed,err 以 packager.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 stopped,err 为 listen tcp :8080: bind: address already in use,进程退出。
判断与处理:
ss -ltnp | grep 8080要么停掉占用者,要么改 server.listen,或者用 KILN_LISTEN 临时换端口。同一台机器跑多个实例时,除了监听端口,data_dir 也必须各自独立。
数据目录不可写
症状:create data dir failed 或 sqlite 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 litechannel "x" requires native packager engineepg is not available in liteOpenTelemetry export is not available in litepprof is not available in litetls 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>/...。
处理:开启 play_require_auth 时,/v1/playlist.m3u 与 /v1/play/... 接受会话或预览 JWT(Bearer 或 ?token=),不接受管理员 API Token;/p/<token>/... 只认路径里的播放密钥。关闭播放鉴权时,前两组接口公开且生成的地址不带令牌。API Token 只用于管理接口,且部分管理路由仅允许登录会话。
play_require_auth
症状:明明关掉了播放鉴权,/v1/playlist.m3u 或 /v1/play/... 还是要凭据。
判断: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 failed 或 upstream status 403 Forbidden: ...。日志里能看到 session restarting,带 attempt、delay 和 err;重试 8 次后变成 session restart budget exceeded,会话进入 failed。
判断:重启退避从 2 秒起步,最长 30 秒,连续稳定 90 秒后计数清零。反复重启说明上游确实拿不到内容,而不是偶发抖动。
处理:在服务器上直接验证上游可达性,注意带上频道配置的 headers 与 user_agent。需要走代理的上游见下面的代理不生效。
如果日志显示 HTTPS 源重定向到公网主机的 http://...:80,可按 频道管理 为该上游或直接 URL 频道开启 upgrade_insecure_redirects。它只处理这种错误的重定向,不会修复普通网络、DNS 或 TLS 证书问题。
上游主机没有进允许列表
症状:403,消息为 upstream host not allowed,err 里是 private host not allowlisted: 192.168.1.10。
判断:出站请求会做 SSRF 校验。公网主机默认放行,但回环地址和私有网段只有在 security.allowed_hosts 中显式列出才放行;云元数据地址(例如 169.254.169.254)和链路本地地址一律拒绝。在 upstreams 或 channels 中声明主机不会获得私网豁免。
处理:把内网源站的主机名或 IP 写进 security.allowed_hosts,然后重启。无论通过配置文件还是管理界面创建上游或频道,都不会自动加入。
引擎不支持这个源
症状:502,消息为 engine=native but the source cannot be served natively。
判断:engine = "native" 表示不允许回退。原生引擎判定自己处理不了时直接失败,不会去找 ffmpeg。常见的不支持原因会体现在 fallback_reason 上:multi_period(多 period 的 MPD)、addressing_unsupported、manifest_codec_unsupported、no_video_representation、no_audio_representation、missing_key_for_kid、manifest_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 同时返回 503 与 not_ready。
判断:启动日志里有两个字段直接给出答案:
packager_engine=auto ffmpeg_available=false进程启动时会做一次依赖探测,找不到就打印 ffmpeg compatibility engine unavailable,带 dependency 和 err 字段,err 形如 find ffmpeg dependency "ffmpeg": exec: "ffmpeg": executable file not found in $PATH。
处理:
core与lite镜像不含 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 字段已经移除,密钥只能全局配置。
播放中途停滞
先按错误发生阶段区分三个超时:
| 阶段 | 默认值 | 含义 |
|---|---|---|
| 等待响应头 | 固定且不可配置 | 已建立请求,但源站一直没有返回响应头 |
packager.fetch_stall_sec |
30 秒 | 响应正文连续没有任何新字节,覆盖清单、分片和错误正文;有字节进展就重新计时,不是总下载时长 |
packager.stall_timeout_sec |
180 秒 | 清单仍在刷新,但所有轨道一直没有发布新分片;-1 可关闭 |
慢速大分片只要持续有字节到达,可以超过 30 秒。只有传输真正停住时才会命中 fetch_stall_sec;若是清单持续更新却没有新媒体,则命中下一节的发布停滞检测。
症状:频道能起来,播放几分钟后卡住。日志里出现会话重启,err 为 no 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_segments与prefetch_segments。 - 需要一个可预测的单进程内存边界时,用
core或lite的原生引擎,不引入 FFmpeg 子进程。 - 想验证低资源行为,用
KILN_RESOURCE_MODE=constrained强制进入最紧的档位。
EPG 不刷新
症状:节目单为空或长期不更新。
判断:按顺序核对三件事。
源是否启用
EPG 没有总开关,是否有输出只看有没有已启用的源。内置源默认全部停用,需要在管理界面或配置里逐个启用。启动日志的 epg_sources 字段就是当前生效的源数量,为 0 说明一个都没启用。lite 变体不支持 EPG,配置里存在已启用的源会直接拒绝启动。
体积超限
日志里出现 EPG refresh failed,err 含 EPG source exceeds decompressed size limit 时,说明解压后的 XMLTV 超过了 epg.max_source_bytes。默认上限是 64 MiB,但在低资源档位下会被自动压到 4 MiB 到 64 MiB 之间的某个值,启动日志的 epg_max_source_mb 就是实际生效值。本轮写入会回滚,已有 epg.db 内容继续提供并被标记为过期。
代理与网络
EPG refresh failed 的 err 是连接超时或 TLS 错误时,说明节目单源不可达。每个 EPG 源都可以单独指定 proxy。设为 auto 时按全局网络出口规则选择线路;留空或设为 direct 时直接连接。
处理:磁盘库默认是 data_dir/epg/epg.db。先修复网络、代理或体积上限并手动刷新;只有确认数据库损坏时,才停服务、把 epg 目录移到备份位置后重启。旧版 <sha256>.cache 不会被新版导入。刷新间隔由 epg.refresh_interval_min 控制,默认 360 分钟,改小之后要注意源站的速率限制。详见 EPG。
代理不生效
症状:配置了代理,但出站流量仍然直连,或者报代理相关错误。
规则没命中
判断:Kiln 按优先级从高到低检查规则,采用第一条匹配的规则;如果都不匹配,就使用 egress.default。priority 数值越小,优先级越高。规则分为五种:host_exact、host_suffix(默认)、host_regex、channel_id、url_regex。
host_suffix 匹配的是主机名本身或者以 .pattern 结尾的主机名,不是简单的字符串包含。写 example.com 能命中 edge.example.com,但命中不了 notexample.com。
处理:把更具体的规则的 priority 调小。确认匹配的是主机名而不是完整 URL,后者要用 url_regex。
线路不存在或已停用
判断:活动规则指向不存在或已停用的线路时,配置加载与热更新都会直接失败,不会静默改成直连。错误会指出规则引用了 unknown or disabled proxy;egress.default 引用未知线路也会拒绝启动。
处理:检查 [[proxies]] 里的 id 是否拼写一致,以及该线路是否被停用。
DNS、TUN 或 fake-IP 环境
判断:所有线路都由 Kiln 本机解析目标、执行 SSRF 校验,并把固定 IP 交给直连或代理;socks5h 也不会触发第二次代理侧解析。probe target dns lookup failed 或 resolved to a non-public address 因此会在代理接管前出现。明文 HTTP 上游经 HTTP(S) 转发代理无法同时固定拨号 IP 与保留原始 Host,这个组合会直接被拒绝。
处理:让 Kiln 使用能返回真实可达地址的 DNS。整机启用 TUN 或 fake-IP、拿不到真实地址时,设 egress.trust_proxy_dns = true 把解析交回代理,代价是 Kiln 不再校验目标 IP,详见出站代理。
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/meminfo 的 MemTotal,取其中最小的正值。嵌套 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_mb 与 effective_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 字段。
判断:core 与 full 镜像内置了 KILN_RUNTIME_VARIANT,手工设成了无法识别的值才会看到这条警告。启动日志的 runtime_variant 字段是最终生效值。
处理:不要手工设置这个变量,除非在自建镜像里。
健康检查一直不通
判断:core 与 full 的 HEALTHCHECK 用 wget --no-check-certificate 先探 HTTPS,再回退 HTTP;lite 基于 scratch,用二进制子命令探 HTTP。按镜像手工复现:
docker exec kiln sh -c 'wget -q --no-check-certificate -O /dev/null https://127.0.0.1:8080/healthz || wget -q -O /dev/null http://127.0.0.1:8080/healthz'
docker exec kiln kiln -healthcheck http://127.0.0.1:8080/healthz如果手工 -healthcheck 指向自动生成的自签名 HTTPS,默认校验会失败。分离监听优先检查明文端口,HTTPS-only 则配置匹配部署主机名的证书或使用上面的镜像探测方式,详见 HTTPS 与分离监听。
/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.tomlsystemd 环境下用 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 并重启。
指标里先看哪几个
/metrics 里 kiln_packager_segment_fetch_errors_total 和 kiln_packager_manifest_errors_total 的增速最能说明上游状况,kiln_packager_cache_bytes 反映当前占用的分片缓存,kiln_session_info 的 state 标签能一眼看出哪些会话进了 failed。
提交问题时带上什么
kiln -version的完整输出。- 启动日志里
kiln starting那一整行。 - 复现步骤,以及对应时段的日志片段。请自行去掉播放令牌与上游凭据;Kiln 自己写日志时已经把
/p/路径里的令牌截成前缀了,但你粘贴的配置和 URL 不会被自动处理。
问题追踪在 GitHub。