ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Ubuntu 24.04 下 systemd 实现 conda 环境 Python 程序与 ollama 开机自启依赖编排

Ubuntu 24.04 下 systemd 实现 conda 环境 Python 程序与 ollama 开机自启依赖编排 开机启动这件事说难不难但如果你没把服务顺序理清楚多半会踩到程序启动了但 ollama 还没就绪跑起来就崩这种坑。这次我以 Ubuntu 24.04 conda 虚拟环境 ollama 为例把整套开机自启方案完整过一遍重点就是让 Python 程序在 ollama 服务之后启动。这篇内容主要解决三件事conda 虚拟环境里的 Python 怎么能被 systemd 正确调用开机时怎么保证 ollama 服务先启动以及中断崩溃之后怎么自动拉起重启。适合刚接触 Ubuntu 服务管理、或者想把自己的本地大模型调用脚本稳定挂在后台的人参考。我会尽量把每一步的为什么也讲清楚少让你在重复试错上花时间。1. 需求拆解与服务时序思路1.1 为什么一定要在 ollama 之后启动很多人的 Python 程序是拿来调用本地 ollama 模型的比如把文本向量化、跑本地对话接口、自动处理文档摘要等等。这类程序有个共同特点启动时会对ollama的HTTP接口发起请求要么是测试连通性要么直接拉模型做推理。如果你是手动执行当然没问题shell 里敲一下python main.py等 ollama 跑起来再敲也来得及。但开机自启就不一样了系统接管了启动顺序你在终端里养成的手感在这里完全失效。如果 Python 程序先于 ollama 启动会出现什么程序尝试请求http://127.0.0.1:11434/api/tags连接被拒绝程序抛异常退出或者进入一个错误的重试死循环。就怕它不退出而是一直在那里报错空转把日志刷得老长等到 ollama 就绪之后它反而冷静下来了——这种隐性故障最难查因为你打开日志一看全都是启动那几分钟的报错记录容易误判成别的问题。所以时序控制的本质是必须在 ollama 监听端口之后再拉起来我们的 Python 服务。这个顺序不能靠猜更不能靠系统启动管得过来大概会先起 ollama必须用 systemd 的依赖关系把它写死。1.2 方案选型为什么推荐 systemd 而不是 rc.local 或 crontab网上也有不少教程推荐用rebootcrontab或者往/etc/rc.local里塞启动命令不能说完全不能用但在 Ubuntu 24.04 上我强烈建议你别这么做。rc.local这种传统方式现在默认不在 Ubuntu 24.04 的启动链路里你需要手动创建服务文件把它救活等于自己绕路去解决一个本来就有正规方案的问题。而reboot的问题是它不提供服务依赖与崩溃自动重启能力程序挂了就挂了不会有人帮你拉起来日志管理也非常原始stdout 有时候都不好找。systemd 的 Unit 文件天生就是干这个的启动顺序用After声明服务依赖用Requires/Wants表达崩溃自动拉起用Restart策略再加上journalctl统一收日志。这就像你雇了一个管家来负责开灯前先合闸这种固定流程而不是每次用胶带把开关贴住。所以下面所有步骤都围绕 systemd 展开不折腾旁门左道。2. 准备工作conda 虚拟环境与 ollama 服务2.1 安装并验证 ollama 作为系统服务先确定一个前提ollama 在 Ubuntu 24.04 上安装后默认会自带一个 systemd 服务单元服务名是ollama.service。这一点非常关键因为我们后面的在 ollama 之后启动要用到这个确切的名称。安装 ollama 最直接的方式是从 Ollama 官网获取对应系统的安装脚本装上之后先做三个检查# 检查服务状态 systemctl status ollama.service # 查看服务当前是否已经自动起来了 systemctl is-active ollama.service # 确认监听端口 ss -lntp | grep 11434如果状态是active (running)并且11434端口有监听说明 ollama 作为服务安装成功了。如果你是用普通用户手动ollama serve跑起来的那开机自启时它不在 systemd 管理范围内后面的依赖关系就没有对象需要先把它转成系统服务管理。有极少数情况下手动下载二进制解压安装的话不会自动生成 service 文件那么你需要先补一个最小的 ollama service 单元# /etc/systemd/system/ollama.service [Unit] DescriptionOllama Local Model Service Afternetwork-online.target Wantsnetwork-online.target [Service] ExecStart/usr/local/bin/ollama serve Restartalways RestartSec3 Userroot [Install] WantedBymulti-user.target写好后执行systemctl daemon-reload systemctl enable --now ollama.service。整个方案的前提是 ollama 本身是系统服务所以这一步务必先落实。2.2 创建 conda 虚拟环境并安装依赖接下来准备 Python 侧的环境。假设你的 conda 已经装好Miniconda 或 Anaconda 都可以。这里注意一个细节不要直接在你的 base 环境里跑业务程序。因为 base 环境通常承担着 conda 自身的组件维护搞坏了会影响整个 conda 工具链而虚拟环境可以随时重建出了问题不牵连其他项目。创建环境并安装依赖# 创建虚拟环境这里以 python 3.12 为例 conda create -n myenv python3.12 -y # 激活后安装项目依赖 conda activate myenv pip install requests # 其他依赖按项目一个个装 # 确认当前环境的python解释器路径 which python我遇到过不少人只在 Windows 上用 conda到了 Ubuntu 上就很自然地去conda activate然后执行python main.py这当然没问题。但我们后面要写 systemd 服务必须在非交互、非登录 shell 环境下直接指定 Python 解释器。所以你最好现在就把那个路径记下来例如/root/miniconda3/envs/myenv/bin/python如果你没把 conda 装在 root 目录那就用自己的路径比如/home/ubuntu/miniconda3/envs/myenv/bin/python。只需要记住一个原则所有依赖包都要装在这个环境里后面不再用pip install装到系统级 Python。有人会想用conda run -n myenv python main.py这种写法来避开路径问题。实测下来conda run在某些场景下会出现输出缓冲异常、退出码不透明的小毛病而且在 systemd 里多套一层反而增加排查难度不如直接把绝对路径灌进去干净。3. 编写 systemd 服务单元文件3.1 Unit 文件模板与参数讲解现在我给一个可以直接复制的模板假设你的项目放在/opt/my-project/主程序是main.py# /etc/systemd/system/myai-worker.service [Unit] DescriptionMy Python AI Worker (conda env) # 等网络和 ollama 都就绪后再启动 Afternetwork-online.target ollama.service Wantsnetwork-online.target Requiresollama.service [Service] Typesimple Userroot WorkingDirectory/opt/my-project # 使用 conda 虚拟环境里的 python ExecStart/root/miniconda3/envs/myenv/bin/python /opt/my-project/main.py # 如果你的程序有子进程可能需要加这个 KillModecontrol-group # 崩溃之后自动重启 Restarton-failure RestartSec5 # 输出缓冲关掉日志更即时 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.target这里几个参数单独解释一下因为它们是这个方案的核心。Afterollama.service只保证 systemd 在启动该服务前先执行 ollama 的启动动作但它不保证 ollama 已经监听端口。它解决的是启动编排先后问题不解决进程内部初始化完毕问题。相当于前面那个管家知道要先合闸但要等多久、灯到底亮了没有那得另说。Requiresollama.service和After的区别比较微妙。Requires表示强依赖如果 ollama.service 启动失败那么我这个服务就不应该启动。如果你只是希望 ollama 先跑但即使 ollama 失败也不影响 Python 程序起来那用Wantsollama.service更合适。我这里写Requires因为我们的业务逻辑明确依赖 ollama 接口ollama 起不来那程序起来也是白起。Restarton-failure加上RestartSec5这是生产环境服务的基本生存保障。程序因为一次网络抖动、断连、异常崩溃退出了systemd 会在 5 秒后自动重新拉起不需要人肉盯着。3.2 关键点ExecStart 必须用 conda 虚拟环境的绝对路径很多人在这里翻车。你在终端里执行conda activate myenv能成功不代表 systemd 启动的服务也能直接用python命令。原因在于 conda 的 activate 其实是一个 shell 函数它会修改当前 shell 的 PATH 环境变量把envs/myenv/bin插到最前面。而 systemd 启动服务的时候环境是很干净的继承的 PATH 可能根本没有 conda 的目录所以你在 ExecStart 里写ExecStartpython main.py大概率会变成找不到 python服务启动失败。解决方案就是上面模板里的绝对路径写法ExecStart/root/miniconda3/envs/myenv/bin/python /opt/my-project/main.py这样不依赖 PATH不需要 activate解释器直接锁定版本和依赖环境。就好比你去后厨点名要某个厨师掌勺而不是说来个人做饭谁是主厨无所谓但你要的是那个特定环境里的 Python 和它绑定的包。另外如果你的项目还需要读取环境变量文件可以在 Unit 文件里加EnvironmentFile/opt/my-project/.env。这里要注意的是如果想要暴露变量给 ExecStart 的进程尽量用Environment逐条写或者用EnvironmentFile引入整个文件比你预想用 shell 脚本source要可靠得多。3.3 ExecStartPre 等待 ollama 端口真正就绪前面说After只管启动顺序不管端口就绪。很多老手为了让服务更皮实会在 ExecStart 之前加一段端口探测脚本叫做ExecStartPre。逻辑就是循环检查 ollama 的接口是否返回正常最多等 60 秒如果期间成功就往下走如果超时就让服务失败避免程序启动后立刻开摆。[Service] ExecStartPre/bin/bash -c for i in $(seq 1 30); do curl -s -o /dev/null http://127.0.0.1:11434/api/tags exit 0; sleep 2; done; exit 1 ExecStart/root/miniconda3/envs/myenv/bin/python /opt/my-project/main.py这条脚本是你自己程序与 ollama 之间的握手环节。curl探测到接口通畅后才认定 ollama 已经就绪。30 次循环、每次 sleep 2 秒也就是最多等待 60 秒。ollama 在冷启动加载模型的时候是可能有几秒延迟的这个窗口够用了。注意如果你的系统里没有 curl先用apt install curl -y装上或者改成用/root/miniconda3/envs/myenv/bin/python -c import urllib.request...来做探测但那样绕远路没必要。4. 实操部署步骤4.1 放置 Unit 文件并加载配置首先把上面写好的 Unit 内容保存到/etc/systemd/system/myai-worker.service。如果你用普通用户操作记得加 sudo。文件命名建议跟服务用途匹配比如myai-worker.service这个文件名就是日后你管理服务的名字。写入之后先别急着启动让 systemd 重新加载配置文件sudo systemctl daemon-reload这一步必胜不可漏掉否则 systemd 还在用内存里的旧配置你怎么操作它都觉得奇怪。4.2 启动服务并设置开机自启先手动启动一次而不是立刻去开 enablesudo systemctl start myai-worker.service sudo systemctl status myai-worker.service如果状态显示active (running)说明基本OK了。接下来设置开机自启sudo systemctl enable myai-worker.serviceenable的本质是在/etc/systemd/system/multi-user.target.wants/下创建一个软链接让系统进入多用户目标时自动拉起该服务。你不需要理解这个软链接机制就能用但知道这点有助于排查为什么已经设置了 enable 还没起来的疑惑。这时可以看一下依赖状态systemctl list-dependencies myai-worker.service systemctl status ollama.service确认两个服务都在运行再往下测试。4.3 验证与测试重启后看效果这一步不要跳过。即使你现在看到服务在跑也必须模拟一次开机过程否则所有依赖关系只是在顺风局里有效。最省事但最真实的方式是重启机器sudo reboot重启后回到系统立刻依次执行systemctl is-active ollama.service systemctl is-active myai-worker.service journalctl -u myai-worker.service -n 50 --no-pager理想情况是ollama 处于 active你的服务也处于 active日志里能看到 Python 程序按预期开始了初始化而且能够访问 ollama 接口。如果不想重启物理机也可以用systemctl restart myai-worker.service来测试服务的自我恢复能力但开机时序的真实效果还是建议重启一次。5. 常见问题与排查记录5.1 conda activate 在 systemd 里用不了怎么办这个问题几乎是本方案的第一大坑。现象是Unit 文件里写了ExecStartconda run -n myenv python main.py服务启动后journalctl显示找不到 conda 命令或者明明 activate 成功了却 import 不到包。原因是 systemd 启动的服务默认不会加载用户的 shell 配置文件conda这个命令不在 PATH 里。你可以在 Unit 里写EnvironmentPATH/root/miniconda3/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin但更稳妥的做法依然是用虚拟环境内 python 的绝对路径来启动完全绕开 activate 这一步。我的习惯是凡是 systemd 里的任务只用绝对路径不搞任何 shell 函数依赖。如果你实在要保留 conda 的激活过程可以套一层 bash 登录壳ExecStart/bin/bash -lc source /root/miniconda3/etc/profile.d/conda.sh conda activate myenv python /opt/my-project/main.py但这个写法也容易踩坑比如bash -lc可能会引入额外的 PATH 覆盖、加载一堆用户 profile导致你在终端完美运行的程序到 systemd 里行为不一致。不如前面模板里的绝对路径干净。5.2 服务启动失败与重启策略排查如果你的服务启动就退了第一步不是改代码而是看两个东西状态码和日志。systemctl status myai-worker.service -l journalctl -u myai-worker.service -f常见的失败信号包括现象大概率原因解决方向状态显示ExecStart路径找不到python 路径不对用which python在 conda 环境内确认绝对路径日志出现ModuleNotFoundError依赖没装在虚拟环境里检查pip list和 python 路径是否对应同一环境日志出现Connection refusedollama 还没起来或等待时间不够加长ExecStartPre探测循环确认 ollama 监听服务一直activating不进入 running脚本卡住检查ExecStartPre是否死循环建议探测脚本只做有限次重试关于Restart策略我给个推荐值业务比较重要的用Restartalways但注意如果代码在启动阶段因为配置错误反复崩溃always可能会导致你改配置时服务一直在重启、难以稳定操作。更合理的是Restarton-failure区别是当进程被管理员手动停止时always会把它拉起来但on-failure不会。我自己的项目习惯是开发测试阶段用Restartno看崩溃日志看完再说基本稳定后改成Restarton-failureRestartSec5。5.3 日志与权限排查日志是排查服务问题的第一现场。systemd 会把服务的 stdout 和 stderr 全部收进 journal直接看journalctl -u myai-worker.service -n 100 journalctl -u myai-worker.service --since 5 minutes ago如果你的程序通过 Python 的print输出但在日志里看不到任何内容检查 Unit 里有没有设置EnvironmentPYTHONUNBUFFERED1。Python 在非终端环境里输出是块缓冲的程序不退出可能就一直攒在内存里日志割裂感很强。权限问题也是一大类。比如你的主程序需要访问模型文件、需要写日志文件到/var/log/myapp/但 Userroot 没权限写入某个受保护的目录或者反过来你用普通用户 Userubuntu但访问 /opt/my-project 下的文件时没有读权限。解决方向很明确要么给目录设置合适的属主要么统一让服务和目录都归同一个用户管理。别写出一个逻辑上没问题但权限上怎么都不对的配置出来。6. 补充我踩过的几个细节坑第一个是 WorkingDirectory。有些人觉得 ExecStart 里已经写了绝对路径WorkingDirectory 就可有可无但很多 Python 程序会隐式依赖当前工作目录去读相对路径的配置文件比如./model_config.json。你在终端里跑的时候当前目录是项目根目录没问题但 systemd 启动时默认工作目录是/相对路径全失效程序会报 FileNotFoundError。所以模板里一定要写WorkingDirectory/opt/my-project除非你代码里全部用了绝对路径。第二个是环境变量的统一。我试过在.bashrc里export一堆变量结果 systemd 完全看不到。最好的做法是把所有业务需要的环境变量集中在/etc/myai-worker.env文件里然后在 Unit 中指定EnvironmentFile/etc/myai-worker.env注意这个文件的格式是KEYVALUE不需要export前缀也不能有 shell 语法。我见过有人往里面写export导致 systemd 直接拒绝加载全部变量这种问题很隐蔽。第三个是关于服务间依赖最后一公里的问题。即便我写了Afterollama.service也无法保证 ollama 一定能完成模型加载或 API 服务稳定。所以我在生产环境里真正交付用的 Unit一定会带上端口检测的ExecStartPre等真正确认api/tags返回 200 之后再拉 Python 程序。这不是防御性编程而是把机器启动的不确定性当成常态来处理。整个流程如果已经走通你可以用一个测试脚本再验证一遍故意停掉 ollama然后重启你的服务看看它是不是因为Requires依赖而拒绝启动再把 ollama 拉起来手动启动你的服务看它是否能自动恢复。把这种故障演练做一遍之后你对这套开机自启组合的理解会比只看文档深刻得多。
返回列表