📚前言
📒FDE系列内容总纲:
【大纲】FDE 前沿部署工程师学习系列教程-CSDN博客
🚄前置课程列表:
见文档结尾附录。
🚀阶段2·Day 39:Linux 综合实战 — 让服务无人值守
FDE 学习系列教程 · 第二阶段 · 第 4 周 · Day 4预计时长:2.5 小时 | 难度:★★★☆☆ | 前置知识:Day 36-38 全部内容
📌一句话目标:用 nohup / screen 让 FastAPI 后台常驻,用 tail -f 盯日志,用 crontab 配置定时备份和健康检查,实现服务挂了自动重启。
🧑🤝🧑 开场:你不可能 24 小时守着终端
先想一个问题:你的 FastAPI 现在是怎么跑的?
uvicorn main:app --reload它占着当前终端窗口——窗口一关、SSH 一断、电脑一锁屏,服务立刻没了。这在开发时没问题,但在客户现场是致命的:客户的系统得 7×24 小时跑着,你总不能让一台电脑永远开着一个黑窗口、还不许碰。
今天解决四件事,让服务具备"无人值守"能力:
┌────────────────────────────────────────────────────────┐ │ ① 后台常驻:关终端/断 SSH,服务照样跑(nohup/screen) │ │ ② 实时盯日志:服务在后台干了啥,随时能看(tail -f) │ │ ③ 定时任务:备份/巡检到点自动执行(crontab) │ │ ④ 自愈:服务挂了,健康检查脚本自动重启 │ └────────────────────────────────────────────────────────┘这就是把一个"开发模式的程序"变成"服务器上常驻服务"的关键一步。下周学 Docker 之前,先用最朴素的方式体验一遍"部署"这件事。
📖 一、先理解:为什么关终端程序就会死?
要理解 nohup,先得知道一个概念——信号(Signal)。
而nohup的字面意思就是no hangup(别挂断)——让程序忽略 SIGHUP 信号,终端关了它也装作没听见,继续跑。
🖥️ 二、nohup:让服务后台常驻
准备:确保项目就绪
cd ~/fde_project ls # main.py venv/ source venv/bin/activate确保main.py里有/health接口(健康检查要用),最小版本:
from fastapi import FastAPI import uvicorn app = FastAPI() @app.get("/") def root(): return {"msg": "FDE API running in background"} @app.get("/health") def health(): return {"status": "ok"} if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)一条命令后台启动
nohup uvicorn main:app --host 0.0.0.0 --port 8000 > app.log 2>&1 &别看这一行短,每个符号都有讲究,逐个拆解:
nohup 忽略挂断信号,关终端不死 uvicorn main:app --host 0.0.0.0 --port 8000 你的服务命令 > app.log 标准输出(1)写进 app.log 2>&1 错误输出(2)合并到标准输出(1) → 正常日志和报错都进 app.log & 扔到后台运行,终端立刻还给你敲完回车,你会看到类似:
[1] 9876 ← [1]是后台任务号,9876 是进程 PID nohup: ignoring input and redirecting stderr to stdout终端立刻可以继续用了——服务在后台跑着。
验证服务真的活着
# ① 用 curl 访问 curl http://localhost:8000/ # {"msg":"FDE API running in background"} curl http://localhost:8000/health # {"status":"ok"} # ② 查进程 ps aux | grep uvicorn | grep -v grep # fde 9876 ... uvicorn main:app --host 0.0.0.0 --port 8000 # ③ 看日志 cat app.log终极测试:关掉这个终端窗口
直接关掉Ubuntu 窗口,然后重新开一个:
ps aux | grep uvicorn | grep -v grep # 进程还在! curl http://localhost:8000/health # 服务照样响应! # {"status":"ok"}这就是后台常驻。这一步成功,你会真切体会到"部署"和"开发运行"的区别。
停止后台服务
# 找到 PID 再 kill(标准做法) ps aux | grep uvicorn | grep -v grep kill 9876 # 或者一条命令按名字杀 pkill -f "uvicorn main:app" # 确认端口释放 ps aux | grep uvicorn | grep -v grep # 没输出 = 已停止📖 三、screen:给服务开一个"关不掉的房间"
nohup 有个缺点:服务扔后台后,你没法再回到它的交互界面。screen 解决了这个问题——它能创建一个可以随时"进出"的终端会话。
┌────────────────────────────────────────────────────┐ │ screen 就像一个"独立的终端房间": │ │ │ │ 你走进房间,启动 uvicorn │ │ 按 Ctrl+A, D 暂时离开房间(detach,服务继续跑) │ │ 关电脑/断网都不影响房间里的程序 │ │ 随时 screen -r 回到房间,服务的画面原封不动 │ └────────────────────────────────────────────────────┘ 对比 nohup: nohup = 把程序扔后台,只能通过日志文件看输出 screen = 程序在房间里跑,随时能"推门进去"看实时界面安装与使用
sudo apt install -y screen # ① 创建一个名叫 fde_api 的会话并进入 screen -S fde_api # 屏幕会清一下,你已经在"房间"里了,正常启动服务(前台方式即可) cd ~/fde_project source venv/bin/activate uvicorn main:app --host 0.0.0.0 --port 8000 # ② 脱离会话:先按住 Ctrl+A,松开,再按一下 D # (detach)。你回到了原始终端,但服务还在房间里跑 # 底部会提示:[detached from 9876.fde_api] # ③ 查看当前有哪些会话 screen -ls # There is a screen on: # 9876.fde_api (09/18/26 15:00:00) (Detached) # ④ 重新进入会话(resume) screen -r fde_api # 你会看到 uvicorn 的实时输出画面,跟离开时一模一样 # ⑤ 彻底结束会话:进入后 Ctrl+C 停掉服务,再输入 exit # 或者从外部直接杀掉:screen -X -S fde_api quit选 nohup 还是 screen?
场景 | 推荐 |
|---|---|
就想让服务安静地跑,输出全进日志 | nohup(简单,配日志文件足够) |
需要随时回去看程序的实时交互界面 | screen |
要同时挂多个需要观察的任务 | 开多个 screen 会话,命名区分 |
生产环境长期稳定运行 | systemd 服务(更正规,本系列后续阶段讲) |
💡 FDE 在客户现场临时部署、跑长任务(数据迁移、批量脚本),screen 极其常用——断 SSH 不怕,重连
screen -r接着看。今天 nohup 和 screen 都会用即可。
🖥️ 四、tail -f:实时盯后台日志
服务进了后台,怎么知道它在干嘛、有没有报错?不能 cat 一下看一下,得用实时追踪:
tail -f app.log # 屏幕停住不动,app.log 一旦有新内容立刻刷出来 # 新开另一个终端 curl 几次接口,这边能看到请求日志实时滚动 # Ctrl+C 退出追踪(只退出查看,不影响服务)tail -f 的进阶组合
# 只盯着错误看(管道过滤) tail -f app.log | grep ERROR # 同时盯错误和警告(-E 正则) tail -f app.log | grep -E "ERROR|WARNING" # 先看最后 50 行,再继续实时追踪 tail -n 50 -f app.log # 同时追踪多个日志文件 tail -f app.log backup.log📌 排障标准动作记住:服务后台跑着 → 新开终端
tail -f app.log→ 另一个终端复现操作 → 实时观察日志输出。两个终端配合,问题无所遁形。
📖 五、crontab:让任务到点自己跑
备份脚本写好了,难道每天凌晨 2 点爬起来手动敲?当然不用。crontab是 Linux 内置的"定时任务调度器",你写好时间表,它到点自动执行。
crontab 基本命令
crontab -e # 编辑当前用户的定时任务表(第一次会让你选编辑器,选 nano 即可) crontab -l # list:查看当前所有定时任务 crontab -r # remove:清空所有定时任务(⚠️ 慎用)cron的特殊符号
| 符号 | 含义 | 例 |
|---|---|---|
* | 任意值 | * * * * *每分钟 |
, | 枚举 | 0,30 * * * *每半点 |
- | 范围 | 0 9-18 * * *9~18 点整 |
/ | 步长 | */10 * * * *每 10 分钟 |
特殊字符串(快捷写法)
| 字符串 | 等价于 |
|---|---|
@reboot | 开机启动时执行一次 |
@hourly | 0 * * * * |
@daily | 0 0 * * * |
@weekly | 0 0 * * 0 |
@monthly | 0 0 1 * * |
@yearly | 0 0 1 1 * |
cron 时间表的五颗星
这是 cron 的核心语法,一条任务长这样:
┌───────── 分钟 (0-59) │ ┌─────── 小时 (0-23) │ │ ┌───── 日 (1-31) │ │ │ ┌─── 月 (1-12) │ │ │ │ ┌─ 星期 (0-7,0和7都是周日) │ │ │ │ │ * * * * * 要执行的命令对着例子理解最快:
0 2 * * * 每天凌晨 2:00 整 */5 * * * * 每 5 分钟 30 8 * * 1-5 每周一到周五的 8:30 0 */2 * * * 每 2 小时(0点、2点、4点...) 0 0 1 * * 每月 1 号的 0:00 30 9 * * 6 每周六上午 9:30 * * * * * 每分钟(调试时临时用)┌────────────────────────────────────────────────────────┐ │ 符号速查: │ │ * 每一(每个时间单位都匹配) │ │ */5 每隔 5(能整除时触发) │ │ 1,3,5 枚举:第1、3、5 │ │ 1-5 范围:1 到 5 │ └────────────────────────────────────────────────────────┘配置两个实战定时任务
先把昨天的备份脚本放到固定位置并确保有执行权限:
chmod +x ~/fde_lab/backup.sh mkdir -p ~/backups ~/logs执行crontab -e,在文件末尾加入:
# 每天凌晨 2:00 备份 fde_project,日志追加到 backup.log 0 2 * * * /home/fde/fde_lab/backup.sh /home/fde/fde_project /home/fde/backups >> /home/fde/logs/backup.log 2>&1 # 每 5 分钟做一次服务健康检查(health_check.sh 下一节写) */5 * * * * /home/fde/fde_lab/health_check.sh >> /home/fde/logs/health.log 2>&1保存退出(nano:Ctrl+O 回车保存,Ctrl+X 退出),看到crontab: installing new crontab就生效了。
crontab -l # 确认任务已写入cron 的三个血泪坑
⚠️坑 1:命令必须用绝对路径!
cron 执行时没有你登录后的 PATH 环境,
uvicorn、python3这些命令可能"找不到"。所以:
脚本路径写全:
/home/fde/fde_lab/backup.sh,不能写./backup.sh脚本内部用到的命令也尽量写绝对路径,或在脚本开头加
source ~/.bashrc/ 手动设 PATH⚠️坑 2:输出一定要重定向到文件
cron 任务没有终端,输出没地方去(有些系统会给你发本地邮件,多数直接丢弃)。务必像上面那样
>> xxx.log 2>&1,否则出了错你啥都看不到。⚠️坑 3:cron 不会自动激活虚拟环境
你的 uvicorn 装在 venv 里,cron 直接调会找不到。健康检查/启动脚本里要先
source /home/fde/fde_project/venv/bin/activate,或直接用 venv 里的绝对路径:/home/fde/fde_project/venv/bin/uvicorn。
不用等到凌晨:临时验证任务
把备份任务临时改成*/2 * * * *(每 2 分钟),等两分钟后检查:
ls -lt ~/backups/ # 看是否生成了新备份包 cat ~/logs/backup.log # 看备份日志验证完记得改回0 2 * * *,别让它每 2 分钟备份一次把磁盘塞满。
🖥️ 六、健康检查脚本:服务挂了自动拉起来
这是今天的综合产物。思路很朴素:定时探测服务 → 活着就记一笔 → 连续探测失败就重启。
新建health_check.sh:
#!/bin/bash # ========================================== # 健康检查 + 自动重启脚本 # 由 crontab 每 5 分钟调用一次 # ========================================== set -euo pipefail # ---------- 配置区 ---------- API_URL="http://localhost:8000/health" MAX_RETRY=3 # 最多重试次数 WAIT_SECONDS=10 # 每次重试间隔 PROJECT_DIR="/home/fde/fde_project" VENV_UVICORN="/home/fde/fde_project/venv/bin/uvicorn" # --------------------------- log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" } RETRY=0 while [ $RETRY -lt $MAX_RETRY ]; do # curl -s 静默 -f HTTP 错误码也算失败;输出丢进黑洞,只看退出码 if curl -sf "$API_URL" > /dev/null 2>&1; then log "✅ 服务正常" exit 0 # 探测成功,脚本结束 fi RETRY=$((RETRY + 1)) log "⚠️ 第 ${RETRY}/${MAX_RETRY} 次探测失败,${WAIT_SECONDS} 秒后重试..." sleep "$WAIT_SECONDS" done # 三次都失败,判定服务挂了 → 重启 log "🚨 连续 ${MAX_RETRY} 次探测失败,判定服务异常,开始重启..." cd "$PROJECT_DIR" # 先清理可能残留的旧进程(防止端口占用) pkill -f "uvicorn main:app" 2>/dev/null || true sleep 2 # 用 venv 里的 uvicorn 绝对路径后台启动 nohup "$VENV_UVICORN" main:app --host 0.0.0.0 --port 8000 \ > app.log 2>&1 & sleep 5 # 等服务启动 # 重启后再验证一次 if curl -sf "$API_URL" > /dev/null 2>&1; then log "🔄 服务已成功重启并恢复正常" else log "❌ 重启后服务仍不可用,需要人工介入!" exit 1 fiset -euo pipefail
严格模式,让脚本"遇错早退",是健壮脚本的关键:
| 选项 | 作用 |
|---|---|
-e | 任何命令返回非 0 就立即退出 |
-u | 使用未定义变量时报错退出 |
-o pipefail | 管道中任一环节失败,整条管道算失败 |
注意:后面多处用
|| true显式"忽略失败",正是为了绕开-e。
为什么要"连续 3 次"?避免偶发抖动(如一次 GC、瞬时高负载)误触发重启。
全部用绝对路径——因为 cron 环境
PATH很精简,相对路径会找不到。MAX_RETRY × WAIT_SECONDS = 30 秒,即单次 cron 运行最多容忍 30 秒不可用才重启。curl -sf参数含义(关键)参数 作用 -ssilent,静默,不输出进度条 -ffail,HTTP 状态码 ≥400 时返回非 0(否则 500 也会被当成功) 配合
> /dev/null 2>&1丢弃输出,只看退出码:0=健康,非 0=异常。循环逻辑
探测成功 → 打 ✅,
exit 0结束(不重启)。失败 → 计数 +1,打 ⚠️,
sleep 10后重试。最多 3 次。注意:第 3 次失败后仍会 sleep 10 秒再跳出循环(轻微浪费,但无害)。
为什么要"重试 3 次"而不是一次失败就重启?
探测失败 ≠ 服务一定挂了,可能是: · 服务正在处理一个慢请求,响应慢了 · 系统那一瞬间 CPU 繁忙 · 网络抖了一下 所以设计成"连续 3 次、每次间隔 10 秒都失败"才判定死亡, 避免一次抖动就误杀好进程。这叫"容错/防抖"思想。实测自愈效果
chmod +x health_check.sh # ① 确保 cron 已配置(*/5 那行),或先手动跑一次验证脚本本身没问题 ./health_check.sh # 2026-09-18 15:10:00 ✅ 服务正常 # ② 模拟服务挂掉:手动杀掉 uvicorn pkill -f "uvicorn main:app" curl http://localhost:8000/health # 已经连不上了 # ③ 立即手动跑健康检查(不用等 5 分钟),观察自愈全过程 ./health_check.sh # ⚠️ 第 1/3 次探测失败,10 秒后重试... # ⚠️ 第 2/3 次探测失败,10 秒后重试... # ⚠️ 第 3/3 次探测失败,10 秒后重试... # 🚨 连续 3 次探测失败,判定服务异常,开始重启... # 🔄 服务已成功重启并恢复正常 # ④ 再验证 curl http://localhost:8000/health # {"status":"ok"} 活了!今天的完整部署全景
📝 本课小结
能力 | 工具/命令 | 关键点 |
|---|---|---|
后台常驻 |
| 忽略 SIGHUP,输出进日志,& 放后台 |
会话管理 |
| 创建/进入/脱离可恢复的终端房间 |
看实时日志 |
| 配合 grep 只看错误 |
停止服务 |
| 先找进程再杀 |
定时任务 |
| 五颗星:分 时 日 月 周 |
备份定时 |
| 每天 2 点 |
巡检定时 |
| 每 5 分钟 |
健康检查 |
| 失败重试 3 次再重启 |
cron 三坑 | 绝对路径/重定向/venv | 一个都不能忘 |
🧠 核心认知:让服务"无人值守"靠的是一套组合拳——nohup/screen 解决"不死",日志解决"可观测",crontab 解决"自动触发",健康检查解决"自愈"。这正是生产环境运维思想的缩影(虽然真正的生产环境会用 systemd/Docker/监控平台,但核心逻辑今天你已经亲手实现了一遍)。
📋 课后练习
练习 1:nohup 后台验证(约 20 分钟)
用 nohup 启动 FastAPI,确认
app.log生成、curl能访问关掉终端窗口,重开后确认进程还在(
ps aux | grep)用
tail -f app.log在另一个终端实时看请求日志,然后用 curl 打几次/health,观察日志滚动用 pkill 停掉服务
练习 2:screen 会话(约 15 分钟)
创建名为
work的 screen 会话,在里面运行top用 Ctrl+A,D 脱离,
screen -ls确认会话 Detachedscreen -r work回到会话,确认 top 画面还在,最后退出并关闭会话
练习 3:crontab + 健康检查(约 30 分钟)
配置 cron:每分钟把当前时间追加到
~/logs/min.log(* * * * * date >> ~/logs/min.log),等 2-3 分钟后tail验证,验证后删除该任务配置每天凌晨 2 点的备份任务,用
crontab -l确认手动杀掉 uvicorn,运行
health_check.sh,观察"3 次重试 → 自动重启 → 恢复"全过程,并查看health.log
🔭 下节预告
今天你的服务已经能后台常驻、定时备份、挂了自愈,像个真正的服务器程序了。明天是本周收官,把 Shell 脚本打磨到"生产级":
set -euo pipefail严格模式:为什么你的脚本明明出错了还傻乎乎往下跑?trap 信号处理:脚本被 Ctrl+C 打断时,如何自动清理临时文件
case 语句:写一个
./service.sh start|stop|status|restart这样的服务管理脚本,一键掌控服务
最后做本周总复习和验收。明天见!
🌍附录:前置课程列表
阶段一:认知启蒙(AI 认知与 FDE 角色)
AI 认知
【FDE系列】阶段1Day 1:AI 层级关系 — 四个嵌套的圈-CSDN博客
【FDE系列】阶段1Day 2:AI 三阶段发展史 — 会认 → 会判断 → 会创造-CSDN博客
【FDE系列】阶段1Day 3:符号 AI vs 机器学习 — 两条路线的本质区别-CSDN博客
【FDE系列】阶段1Day 4:Transformer 的历史意义 — 2017 年的分水岭-CSDN博客
【FDE系列】阶段1Day 5:本周复习与自测 — 检验你的 AI 认知地基-CSDN博客
【FDE系列】阶段1Day 6:Transformer 架构 — 一张图纸盖出千千万万栋楼-CSDN博客
【FDE系列】阶段1Day 7:LLM 本质 — 文字接龙机器-CSDN博客
【FDE系列】阶段1Day 8:Token — 模型眼中的最小单位-CSDN博客
【FDE系列】阶段1Day 9:AI 幻觉 — 为什么会一本正经地胡说八道-CSDN博客
【FDE系列】阶段1Day 10:上下文窗口 — 模型的记忆力上限 + 本周复习-CSDN博客
【FDE系列】阶段1Day 11:Prompt — 给模型立规矩-CSDN博客
【FDE系列】阶段1Day 12:Memory — 让模型记住上下文
【FDE系列】阶段1Day 13:RAG — 给模型配图书管理员-CSDN博客
【FDE系列】阶段1Day 14:Tool Use — 让模型动手操作-CSDN博客
【FDE系列】阶段1Day 15:MCP — 统一的工具接口标准 + 第三周复习-CSDN博客
FDE 基础概念
【FDE系列】阶段1Day 16:什么是 FDE — 把 AI 变成客户结果的人-CSDN博客
【FDE系列】阶段1Day 17:FDE vs 传统实施 — 三大本质区别-CSDN博客
【FDE系列】阶段1Day 18:FDE 三重身份 + C6 胜任力模型-CSDN博客
【FDE系列】阶段1Day 19:七阶段行动路径 + 行业经验的价值-CSDN博客
【FDE系列】阶段1Day 20:阶段总结与产出物 — 第一阶段收官-CSDN博客
阶段二:技术地基(Python + FastAPI + SQL + Docker + API 集成)
Python基础
【FDE系列】阶段2:Day 21:Python 环境搭建 — 写出你的第一行代码-CSDN博客
【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 — Python 的“记忆“和“判断“-CSDN博客
【FDE系列】阶段2:Day 23:循环与函数 — 让代码跑 100 遍、把逻辑打包复用-CSDN博客
【FDE系列】阶段2:Day 24:数据结构 — 列表、字典、集合、元组-CSDN博客
【FDE系列】阶段2:Day 25:文件读写与 JSON — 让程序连通外部数据(第一周收官)-CSDN博客
【FDE系列】阶段2:Day 26:模块化编程 — 把代码拆成“抽屉柜“-CSDN博客
【FDE系列】阶段2:Day 27:异常处理与日志 — 让程序“摔不烂、查得到“-CSDN博客
FastAPI入门到进阶
【FDE系列】阶段2:Day 28:FastAPI 入门 — 把你的函数变成 API 服务-CSDN博客
【FDE系列】阶段2:Day 29:FastAPI 进阶 — Pydantic 模型与完整 CRUD 实战-CSDN博客
【FDE系列】阶段2:Day 30:生产代码规范 — 测试、类型注解、配置管理(第二周收官)-CSDN博客
SQL基础
【FDE系列】阶段2:Day 31:SQL 基础 — 增删改查一把梭-CSDN博客
【FDE系列】阶段2:Day 32:多表查询 — JOIN 与聚合-CSDN博客
【FDE系列】阶段2:Day 33:进阶查询 — 窗口函数与 CTE-CSDN博客
【FDE系列】阶段2:Day 34:数据清洗 — 把脏数据捋干净-CSDN博客
【FDE系列】阶段2:Day 35:Python + SQL — 工单接入 MySQL + 本周收官-CSDN博客
【FDE系列】阶段2:Day 36:Linux 入门与文件操作 — 扔掉鼠标的第一天-CSDN博客
【FDE系列】阶段2:Day 37:权限、进程与文本三剑客-CSDN博客
【FDE系列】阶段2:Day 38:Shell 脚本 — 把命令串起来自动跑-CSDN博客