先交代环境,因为后面每个坑都和这些设定有关:
项 | 情况 |
|---|---|
宿主机 | CM311-1A 电视盒子,Amlogic S905L3A,2G 内存 |
系统 | Armbian(Debian 系) |
青龙 | Docker 部署,镜像自带 otask.sh / task 那套启动链 |
脚本 | JS 与 Python 混用,Python 脚本依赖仓库里的 notify.py 等公共模块 |
通知 | Bark 推送 |
青龙本身对环境变量的管理有好几层,加上它启动 Python 任务的方式比较特殊,几个坑叠加起来,报错信息会把你往完全错误的方向带。以下按「排查成本从高到低」排列,第一个坑我耗的时间最长。
报错长这样:
Traceback (most recent call last):
File "/ql/data/scripts/xxx/yyy.py", line 5, in <module>
import notify
ModuleNotFoundError: No module named 'notify'90% 的教程会告诉你:依赖没装,pip install 一下。但对 notify 这个模块,装包是死路——PyPI 上根本没有(或不是你想要的那个)。notify.py 通常是脚本仓库自带的文件,和脚本在同一个目录,靠 Python 的路径搜索机制被找到。
所以真正的问题是:任务进程的模块搜索路径(sys.path)里没有脚本所在目录。
翻青龙容器里的 /ql/shell/otask.sh,对 .py 任务有类似这样的逻辑:
export PYTHONPATH="${dir_preload}:${dir_config}:${PYTHONPATH}"其中 dir_preload 是 /ql/shell/preload,排在最前面。而这个目录里躺着一个青龙官方的 sitecustomize.py,末尾有一句模块级的 import task_before。
关键在于 Python 解释器启动时的规则:site 模块会按顺序尝试导入 sitecustomize,只导入第一个找到的。于是发生了这样的事:
sitecustomize.py(往 sys.path 里 append 脚本目录),放在依赖缓存目录的 site-packages 里/ql/shell/preload 排最前,官方的 sitecustomize.py 先被找到并导入sitecustomize.py 永远不会执行task_before 钩子执行失败或者啥也没干,脚本目录照样不在 sys.path 里 → import notify 失败也就是说,报错说「缺 notify」,实际缺的是一次正常的路径注入。报错信息和真实原因隔了一层,这是整个排查里最费时间的地方。
两步,都不需要重启容器:
第一步:用官方钩子 task_before.py。 青龙支持 /ql/data/config/task_before.py,会在任务前被调用。往里写:
import sys
sys.path.append("/ql/data/scripts")第二步:把补丁文件改名。 如果你在依赖目录放过自己的 sitecustomize.py,把它改名成 usercustomize.py。site 模块先 import sitecustomize 再 import usercustomize,两者可以共存,这样你的路径注入在所有进程(包括不走 otask.sh 的进程)里都能生效。
注意两点:
sys.path.append,不要用 insert(0),防止遮蔽标准库这是我踩过的最阴的一个坑:docker exec 进容器,python -c "import notify" 一切正常,但面板里跑任务就是报错。
原因:docker exec 起的 shell 是干净环境,PYTHONPATH 没被 otask.sh 改写;面板任务走的路径完全不同。唯一可信的验证方式是模拟面板的调用链:
docker exec qinglong bash -c 'cd /ql/data/scripts && QL_DIR=/ql task <仓库目录>/<脚本路径>.py'这条命令等价于你在面板里点「运行」。它通过了,面板里才会通过。
第二个大坑是环境变量。青龙的配置有三层,优先级从低到高:
environment / docker run -e(进容器 Config.Env)/ql/data/db 的 sqlite 里)/ql/data/config/config.sh 和 extra.sh,任务启动前被 source,优先级最高排查「改了没生效」的第一原则:先怀疑有更高优先级的同名变量在覆盖你。常见场景是三层里都有 HTTP_PROXY,你在面板里改了,但 config.sh 里还有一行旧的,任务实际用的是旧的。
验收不能只看文件,要用任务的真实视角看:
docker exec qinglong bash -c 'source /ql/data/config/config.sh; env | grep -i proxy'这打印出来的才是任务进程实际拿到的值。
还有两个附带的小坑:
export VAR="" 不是「恢复默认值」,而是显式把变量覆盖成空串。config.sh 里要填就填真值,不要的整行删掉,绝不留空赋值environment 或 command 后,必须 docker compose up -d --force-recreate。否则 docker compose up -d 只会说一句 Running 0.0s——容器没重建,配置当然没变有个需求想在容器启动时执行 git config,我写进了 compose 的 command:,重启后 docker inspect 一看——根本没执行。
原因:青龙的镜像入口脚本(entrypoint)不会执行你传入的 command,那段配置是死代码,不报错、不生效,纯摆设。
替代方案是给任务进程配 GIT_CONFIG_GLOBAL 环境变量,指向仓库目录里的一个文件:
GIT_CONFIG_GLOBAL=/ql/data/.gitconfig注意把 user.name、user.email、http.postBuffer 一起写进这个文件。另外不要用 bind mount 单文件去覆盖容器里的 /root/.gitconfig——单文件挂载在容器重建后容易出 inode 问题,目录挂载更稳。
这套环境里配了 HTTP 代理(HTTP_PROXY / HTTPS_PROXY),容器里跑的任务会继承这几个变量。但容器之间互访不能走代理,NO_PROXY 写错就会被截走。症状很有迷惑性:
容器 A 用容器名访问容器 B,回 503,不是超时也不是 404。
看到 503 第一反应是「服务挂了」,实际上是请求被代理截走了——代理服务器不认识内网容器名,回了 503。
NO_PROXY 有三种常见的无效写法,全都见过:
NO_PROXY=172.*|192.168.*|10.* —— curl 和 Python requests 都不解析这种写法,整条丢弃NO_PROXY=172.16.0.0/12 —— 对 IP 地址有效,但对主机名无效NO_PROXY="" —— 赋值动作本身就会覆盖上层的正确值第 2 条的根因值得单独说:Python 的 requests 不做 DNS 解析来匹配 NO_PROXY,只做字符串的 hostname.endswith(条目) 判断。主机名永远不会匹配上 CIDR。curl 会解析 DNS,所以同一个 NO_PROXY 下 curl 正常、Python 不正常,又是一个两边行为不一致的陷阱。
正确口径一句话:IP 用 CIDR 兜底,主机名逐字列出。
NO_PROXY=localhost,127.0.0.1,qinglong,yyb-go,host.docker.internal,172.16.0.0/12,192.168.0.0/16,10.0.0.0/8验收标准也不止是「env 值对了」,要看 Python 的真实判断:
import urllib.request
print(urllib.request.proxy_bypass("qinglong")) # 必须是 True
import requests
print(requests.get("http://qinglong:5700/api/system").status_code) # 必须 200两个都过,才算修好。
青龙里跑签到类脚本,同一个脚本常常存在多份副本(不同仓库目录各一份)。排查问题时你打开的日志,可能对应的是另一份禁用着的副本,你在那份里改半天当然没用。
锁定真身的办法:
cat /ql/data/config/crontab.list每行任务后面带的路径才是实际执行的文件。再拿日志目录名对一下:
ls /ql/data/log/<任务log_name>/先确认「跑的是哪一份」,再去改代码。另外 crontab.list 行首的 # 注释状态和面板里的启用状态可能不一致,以面板为准。
回头看,这五个坑里有三个的共性是:报错信息 / 表面现象,和真实原因之间隔了一层。
No module named 'notify' → 不是缺模块,是路径注入被打断所以排查这类问题,我的习惯变成了:先找一条能复现任务真实环境的验证命令(比如 task 命令全链路跑一遍),再动配置。验证命令不存在的话,先造一条,再排查——不然每改一次都只能「面板点运行 + 祈祷」,排查效率天差地别。
机器还在跑,欢迎交流。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。