Shell 脚本与容器 — CodeRoadMapShell 脚本与容器
脚本、cgroup 与 Docker。
Shell 脚本与容器
这一课你将能做到
- 写出带变量、循环、条件判断的 Shell 脚本,把「备份数据库 + 压缩 + 清理旧文件」这类重复运维操作自动化
- 理解 cron 定时任务配置,让脚本每天凌晨自动执行,并能把输出记录到日志
- 说清楚 cgroup/ulimit 对进程的资源限制原理,并能在容器中验证;完成 Docker 的安装与「构建镜像 → 运行本站后端」的全流程
为什么要学
手动敲命令是运维的起点,自动化才是运维的归宿。每天备份 MySQL、每周清理过期日志、发布新版本时一串固定操作——这些事写成脚本 + cron 定时,既不会忘也不会错。而容器(Docker)是当下交付的事实标准:本站后端最终会以容器形式部署,理解容器底层的 cgroup/namespace 隔离原理,你才知道「容器限制内存」到底是怎么实现的。
面试中「Shell 脚本的 $0/$1/$? 是什么」「crontab 五个字段怎么写」「容器和虚拟机的区别」「cgroup 是什么」是本课对应的高频题。
先建立图像
Shell 脚本的本质:把你在终端敲的命令按顺序写进文件,
加上变量、判断、循环,让机器替你做重复劳动。
手工运维 自动化运维
每天: mysqldump ... → crontab 每天 2:00 自动跑 backup.sh
tar czf ... ↓
rm 30天前的旧备份 日志记录 + 失败告警
容器的隔离原理(面试核心):
虚拟机 VM 容器 Container
┌─────────────┐ ┌─────────────┐
│ 应用 │ │ 应用 │
│ Guest OS │ 整个操作系统 │ 依赖库 │ 共享宿主机内核
│ Hypervisor │ │ Docker │
├─────────────┤ ├─────────────┤
│ 宿主机内核 │ │ 宿主机内核 │
└─────────────┘ └─────────────┘
容器隔离的两块基石(都是 Linux 内核特性):
namespace → 隔离「看得到什么」(进程、网络、文件系统各看各的)
cgroup → 限制「能用多少」(CPU、内存配额,ulimit 管句柄等)
跟着做
步骤 1:第一个脚本——变量与参数
mkdir -p ~/scripts && cd ~/scripts
nano deploy.sh
#!/bin/bash
# 本站后端一键部署脚本
APP_DIR="/opt/codeforge"
JAR_NAME="codeforge.jar"
BACKUP_DIR="/opt/backup"
DATE=$(date +%Y%m%d_%H%M%S) # 命令替换:$(...) 把命令输出赋给变量
# 位置参数:$1 是执行脚本时传的第一个参数
NEW_JAR="$1"
# 防御性检查:参数为空就退出
if [ -z "$NEW_JAR" ]; then
echo "用法: $0 <新jar包路径>" # $0 是脚本自己的名字
exit 1
fi
echo "[1/4] 备份当前版本"
cp "$APP_DIR/$JAR_NAME" "$BACKUP_DIR/codeforge-$DATE.jar"
echo "[2/4] 停止服务"
sudo systemctl stop codeforge
echo "[3/4] 部署新版本"
cp "$NEW_JAR" "$APP_DIR/$JAR_NAME"
echo "[4/4] 启动并验证"
sudo systemctl start codeforge
sleep 15
# $? 是上一条命令的退出码:0 成功,非 0 失败
if curl -sf http://localhost:8080/api/health > /dev/null; then
echo "部署成功!版本备份在 $BACKUP_DIR/codeforge-$DATE.jar"
else
echo "健康检查失败,正在回滚..."
cp "$BACKUP_DIR/codeforge-$DATE.jar" "$APP_DIR/$JAR_NAME"
sudo systemctl restart codeforge
exit 1
fi
运行:
chmod +x deploy.sh
./deploy.sh /tmp/codeforge-v2.jar
步骤 2:循环实战——批量处理
#!/bin/bash
# 清理 /var/log/codeforge 下 30 天前的日志
LOG_DIR="/var/log/codeforge"
DAYS=30
count=0
# for 循环遍历 find 的结果
for f in $(find "$LOG_DIR" -name "*.log.*.gz" -mtime +$DAYS); do
rm -f "$f"
count=$((count + 1)) # 算术运算:$(( ))
done
echo "$(date '+%F %T') 清理了 $count 个过期日志文件"
# while 循环:按行读取(处理含空格的文件名更安全)
find "$LOG_DIR" -name "*.tmp" | while read -r file; do
rm -f "$file"
done
步骤 3:cron 定时任务——让脚本自动跑
# 分 时 日 月 周 命令
# 每天凌晨 2:30 备份 MySQL
30 2 * * * /home/ubuntu/scripts/backup_mysql.sh >> /var/log/backup.log 2>&1
# 每周日凌晨 3:00 清理旧日志
0 3 * * 0 /home/ubuntu/scripts/cleanup.sh >> /var/log/cleanup.log 2>&1
# 每 5 分钟做一次健康检查
*/5 * * * * curl -sf http://localhost:8080/api/health || echo "$(date) 健康检查失败" >> /var/log/health-alert.log
crontab -l # 查看当前用户的定时任务
sudo systemctl status cron # Ubuntu 上 cron 服务本身也要在跑
#!/bin/bash
# backup_mysql.sh
BACKUP_DIR="/opt/backup/mysql"
mkdir -p "$BACKUP_DIR"
DATE=$(date +%F)
mysqldump -u backup_user -p'密码' codeforge | gzip > "$BACKUP_DIR/codeforge-$DATE.sql.gz"
# 保留最近 14 天
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +14 -delete
步骤 4:资源限制——ulimit 与 cgroup
# ulimit:限制单个进程的资源(用户级)
ulimit -a # 查看所有限制
ulimit -n # 文件句柄数,默认 1024
ulimit -n 65535 # 临时调大(Java 高并发应用必须调)
# 永久生效:/etc/security/limits.conf
# app soft nofile 65535
# app hard nofile 65535
# cgroup:内核级资源隔离,容器的基础
# systemd 服务里限制内存 1G(上一课学过,底层就是 cgroup)
sudo systemctl set-property codeforge.service MemoryMax=1G
# 直接查看 cgroup 里某服务的限制
cat /sys/fs/cgroup/system.slice/codeforge.service/memory.max 2>/dev/null
步骤 5:安装 Docker 并跑起第一个容器
# 安装(官方仓库方式,包管理课练过)
sudo apt update
sudo apt install -y docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker $USER # 当前用户免 sudo 用 docker(重新登录生效)
# 验证隔离性:容器里的进程看不到宿主机
docker run --rm ubuntu:22.04 ps aux
# 容器内只有 1 个进程(ps 自己)——namespace 隔离的证据
# 容器资源限制:cgroup 的直观体现
docker run --rm -m 256m --cpus=0.5 ubuntu:22.04 cat /sys/fs/cgroup/memory.max
# 输出 268435456(=256MB)——限制真的写进了 cgroup
步骤 6:容器化本站后端——Dockerfile 实战
# 项目根目录新建 Dockerfile
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/codeforge.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Xms512m", "-Xmx768m", "-jar", "app.jar", "--spring.profiles.active=prod"]
# 构建镜像
docker build -t codeforge:1.0 .
# 运行(连宿主机的 MySQL 和 Redis)
docker run -d --name codeforge \
-p 8080:8080 \
-e SPRING_DATASOURCE_URL="jdbc:mysql://host.docker.internal:3306/codeforge" \
codeforge:1.0
# 日常操作
docker ps # 运行中的容器
docker logs -f codeforge # 看日志(相当于 tail -f)
docker exec -it codeforge bash # 进容器排查
docker restart codeforge
docker stats # 实时资源占用(cgroup 数据的可视化)
步骤 7:脚本健壮性三件套
#!/bin/bash
set -euo pipefail
# -e:任何命令失败立即退出(防止错误滚雪球)
# -u:使用未定义变量时报错(防止变量打错字悄悄变空)
# -o pipefail:管道中任何一环失败整体算失败
# 示例:没有 -e 时,cd 失败后 rm 会在错误目录执行!
cd /opt/codeforge
rm -rf tmp/ # 如果 cd 失败,这里删的是当前目录的 tmp/
# 有了 set -e,cd 一失败脚本就停了,安全。
讲清楚要点
- shebang(
#!/bin/bash)决定解释器:必须是第一行,系统据此知道用 bash 执行。脚本没 shebang 又没加执行权限,只能 bash script.sh 运行。
$?、$0、$1、$#、$@ 是脚本五虎将:分别表示上条命令退出码、脚本名、第一个参数、参数个数、所有参数。写参数校验必用。
$(命令) 是命令替换:把命令输出当值用,DATE=$(date +%F) 是最常见用法。老写法反引号 `date` 已不推荐(嵌套困难)。
- crontab 五字段「分 时 日 月 周」:
* 任意,*/5 每 5 个单位,1,15 每月 1 号和 15 号。周字段 0 和 7 都代表周日。
- cron 的环境变量极简:交互终端里的 PATH、alias 在 cron 里不存在。脚本内一律写绝对路径(
/usr/bin/mysqldump 而非 mysqldump),这是 cron 任务「手动能跑定时失败」的第一原因。
- ulimit 管单进程,cgroup 管一组进程:ulimit 是用户/进程级的句柄、栈大小限制;cgroup 是内核级的 CPU/内存/IO 配额,systemd 的 MemoryMax 和 docker 的 -m 底层都是 cgroup。
- 容器 ≠ 轻量虚拟机:容器共享宿主机内核,靠 namespace 隔离视图、cgroup 限制资源;虚拟机有独立内核。所以容器启动毫秒级、镜像 MB 级,但隔离性弱于 VM(同内核,内核漏洞可能影响全部容器)。
你可能会懵的地方
-
脚本里 [ 和 [[ 有什么区别?
[ 是 POSIX 标准(兼容性好但坑多),[[ 是 bash 扩展(支持 &&、||、正则 =~,不易踩空变量坑)。bash 脚本推荐 [[ ]]。
-
为什么 cron 里 * * * * * echo hello >> /tmp/cron.log 没输出?
cron 默认把输出发邮件(本地 mail),而多数服务器没配邮件。显式重定向到文件最靠谱;另外检查 systemctl status cron 服务是否在跑。
-
docker 容器删了数据还在吗?
不在了。容器文件系统随容器删除。数据要持久化必须挂卷:-v /opt/data:/data。MySQL 容器不挂卷等于数据放烟花。
-
set -e 有没有副作用?
有。某些预期会失败的命令(如 grep 没匹配到返回 1)会误杀脚本。可以对预期失败加 || true:grep xxx file || true。
-
容器里 top 显示的内存是宿主机的?
是的。namespace 没隔离 /proc 的部分视图,容器内 top/free 看到的是宿主机总量。查容器真实占用用 docker stats。
错法对照
| 错法 | 问题 | 正解 |
|---|
cron 里写 mysqldump ... 相对命令 | cron 的 PATH 极简,找不到命令 | 写绝对路径 /usr/bin/mysqldump |
脚本没 set -e,cd 失败后继续 rm | 在错误目录执行删除,删错东西 | 开头加 set -euo pipefail |
for f in $(ls *.log) 解析文件名 | 文件名含空格会被拆成两段 | for f in *.log(glob 直接展开)或 find ... | while read |
crontab 写 0 2 1 * * 想每天跑 | 这表示「每月 1 号 2 点」 | 每天:0 2 * * * |
| docker run 数据库不挂 -v | 容器删除数据全丢 | -v mysql-data:/var/lib/mysql |
| 用 root 跑容器内应用 | 容器逃逸后即为宿主机 root | Dockerfile 加 USER 指令降权 |
卡点急救
-
现象:脚本手动执行正常,cron 里就是不跑
三板斧:命令改绝对路径;crontab 行尾加 >> /tmp/xx.log 2>&1 收集报错;systemctl status cron 确认 cron 服务活着。
-
现象:脚本报 bad interpreter: No such file or directory
文件在 Windows 编辑过,换行符是 CRLF。解决:sed -i 's/\r$//' script.sh 或安装 dos2unix 转换。
-
现象:docker 容器起不来,docker ps 里看不到
容器启动即退出了。解决:docker ps -a 看已退出容器 → docker logs 容器名 看退出原因(90% 是配置或端口问题)。
面试常问
-
crontab 五个字段的含义?如何设置每 10 分钟执行一次?
分(0-59)、时(0-23)、日(1-31)、月(1-12)、周(0-7,0/7=周日)。每 10 分钟:*/10 * * * *。每天凌晨 2 点半:30 2 * * *。
-
容器和虚拟机的本质区别?
虚拟机通过 Hypervisor 虚拟化硬件,跑完整 Guest OS,隔离强但笨重(GB 级镜像、分钟级启动);容器是宿主机内核上的隔离进程,用 namespace 隔离视图(PID/网络/文件系统)、cgroup 限制资源(CPU/内存),MB 级镜像、毫秒启动,但共享内核隔离性较弱。
-
Shell 脚本中 $?、$#、$@ 分别是什么?set -e 有什么用?
$? 上一条命令退出码(0 成功);$# 参数个数;$@ 全部参数。set -e 让脚本在任何命令失败时立即退出,防止错误级联(如 cd 失败后继续 rm 造成误删),生产脚本标配 set -euo pipefail。
自测三问
- 写一个 cron 表达式:每周一、三、五的凌晨 1 点执行
/home/ubuntu/scripts/backup.sh,输出追加到 /var/log/backup.log。
- 容器内应用被 OOM Kill,如何确认是 docker 的 -m 限制触发还是宿主机内存不足?
- 为什么生产脚本第一行推荐
#!/bin/bash 紧接着 set -euo pipefail?
小结一句话
脚本自动化的三板斧是「set -euo pipefail 保命、cron 定时、日志留痕」;容器的底层就两个内核特性——namespace 管隔离、cgroup 管配额。
延伸