主题
界面
主题
你写的代码最终要跑在服务器上,而服务器基本都是 Linux,而且没有图形界面 ✓
本地: IDEA 里点按钮 → 编译、运行、看日志
服务器: ssh 登录 → 命令行 → 看日志、改配置、排查问题所以后端工程师必须能在"只有命令行"的环境里工作 ✓
本节的第一个认知:
命令行不是"老古董",它是"可编程、可组合、可远程"的操作方式 ✓
图形界面能做的事有限,命令行能做的事由你组合决定 ✓
完成本节后,你应该能够:
/、~、/etc、/var/log、/opt);tail -f 看实时日志 ★(这是最常用的一条);find / grep 定位文件与内容,并知道排除构建产物 ✓;grep / sed / awk 的分工;tar 打包解压;cd / ls)吗?先看三个只有命令行能高效完成的场景:
| 场景 | 图形界面 | 命令行 |
|---|---|---|
| 在 2 万个文件里找出"哪些文件包含某个配置" | 逐个打开 ✗ | grep -rl "xxx" . 一秒 ✓ |
| 看服务"现在正在打印什么" | 下载日志文件再看 ✗ | tail -f app.log ✓ 实时 |
| 统计日志里"今天有多少 500 错误" | 导入 Excel ✗ | grep -c " 500 " app.log ✓ |
| 批量改名 / 批量替换 | 手工 ✗ | 一行脚本 ✓ |
更根本的一点:服务器上的很多操作只能通过命令行完成(没有桌面、只有 SSH)✓
| 理由 | 说明 |
|---|---|
| 免费与开源 | 无授权成本 ✓ |
| 稳定 | 服务器常年不重启 ✓ |
| 生态 | 绝大多数服务端软件(Nginx、Redis、Kafka、Docker、K8s)首选/仅支持 Linux ✓ |
| 可远程、可脚本化 | 运维自动化的基础 ✓ |
Java 的"一次编译到处运行"在这里的意义:
同一个 jar,可以在 Windows 开发、在 Linux 运行 ✓ ——但"运行起来之后怎么管它"是 Linux 的技能 ✓
四个必须记住的路径符号:
| 符号 | 含义 |
|---|---|
/ | 根目录(Linux 一切从 / 开始,没有 C 盘 D 盘)✓ |
~ | 当前用户的家目录(如 /home/deploy)✓ |
. | 当前目录 |
.. | 上级目录 |
绝对路径 vs 相对路径:
/opt/app/logs/app.log # 绝对路径:从 / 开始 ✓
logs/app.log # 相对路径:从当前目录开始
./start.sh # 明确表示"当前目录下的 start.sh"
../config/app.yml # 上级目录的 config 下常用目录(记住这张表,登录服务器后不会迷路):
| 目录 | 放什么 |
|---|---|
/etc | 配置文件(如 /etc/nginx/nginx.conf)★ |
/var/log | 日志(如 /var/log/nginx/access.log)★ |
/opt | 第三方软件 / 自己的应用(很多团队把应用放这里)★ |
/home/<user> | 用户的家目录 |
/tmp | 临时文件(可能被清理 ⚠️) |
/usr/bin | 系统命令 |
/proc | 内核与进程的虚拟文件系统(cat /proc/<pid>/status 能看进程状态)★ |
⚠️ 一个新手常犯的错误:把应用装在 /tmp 或用户的家目录 ✗ ——应该用 /opt 或 /srv,并且有明确的部署用户 ✓
pwd # 我在哪
ls -la # 列出(含隐藏文件、详细权限)★
cd /opt/app # 切换目录
mkdir -p logs/archive # 建目录(-p:父目录不存在就一起建)★
cp -r conf conf.bak # 复制目录(-r)
mv app.log app.log.1 # 改名 / 移动
rm -rf old/ # 删除(-r 递归、-f 强制)⚠️ **危险**ls -l 的输出(实测):
-rw-r--r-- 1 testuser 1000 6494 Sep 21 16:26 README.md
-rw-r--r-- 1 testuser 1000 2555 Sep 21 15:32 labs/pom.xml| 字段 | 含义 |
|---|---|
-rw-r--r-- | 权限(详见 《Linux 文件、用户与权限管理》) |
6494 | 字节大小 |
Sep 21 16:26 | 修改时间 |
⚠️ rm -rf 的三条保命经验:
| 经验 | 说明 |
|---|---|
rm -rf 前先 ls 一遍 | 确认路径没写错 ✗(写成 rm -rf /opt/app/ vs rm -rf / opt/app/ —— 一个空格的差别 ✗✗) |
| 变量要用引号 | rm -rf "$DIR"/* ——如果 $DIR 为空,就变成 rm -rf /* ✗✗✗ |
| 重要数据先备份 | 或者至少用"移动到一个待删目录"代替直接删 ✓ |
| 命令 | 用途 | 典型 |
|---|---|---|
cat | 全部输出 | 小文件 ✓ |
less ★ | 分页查看(可搜索、可上下翻) | 大文件首选 ✓ |
head -n 20 | 看开头 | 看文件结构 ✓ |
tail -n 100 ★ | 看结尾 | 看最新日志 ✓ |
tail -f ★★★ | 实时跟踪 | 看服务现在在打什么 ✓✓ |
| `tail -f | grep ERROR` | 只看错误 ★★ |
tail -f 为什么是"后端第一命令":
线上出问题时,你第一个要做的往往是"看看它现在在打什么日志" ✓
less用于"翻历史日志",tail -f用于"看当下" ✓
less 的常用键(记住这几个就够):
| 键 | 作用 |
|---|---|
/关键词 | 向下搜索 ★ |
n / N | 下一个 / 上一个匹配 ✓ |
G / g | 跳到末尾 / 开头 ✓ |
q | 退出 |
find 与 grep 这是本节最有价值的一组命令 ✓
find:按条件找文件 find . -name "*.log" # 按名字
find /var/log -type f -mtime -1 # 1 天内修改过的文件 ★
find . -size +100M # 大于 100MB 的文件 ★
find . -name "*.log" -exec rm {} \; # 找到并删除 ⚠️grep:在内容里找 grep "ERROR" app.log # 找包含 ERROR 的行
grep -n "ERROR" app.log # 带行号 ★
grep -r "config" /etc/nginx/ # 递归目录 ★
grep -rl "reflog" docs # ★ 只列出**文件名**(-l)
grep -c " 500 " access.log # 计数 ★
grep -v "DEBUG" app.log # 反向(排除)★
grep -o "v2\.[0-9]\.[0-9]-[a-z-]*" README.md # 只输出匹配的部分 ★实测(一):grep -rl 的结果
$ grep -rl "reflog" docs
docs/.vitepress/dist/00-start/tools/S0-004.html
docs/.vitepress/dist/03-enterprise/engineering/S3-030.html
docs/.vitepress/dist/assets/00-start_tools_S0-004.md.CJO1a9ot.js
docs/.vitepress/dist/assets/03-enterprise_engineering_S3-030.md.z43-M84M.js
docs/.vitepress/dist/assets/03-enterprise_engineering_S3-030.md.z43-M84M.lean.js⚠️ 注意:前 5 个结果全是构建产物(docs/.vitepress/dist/)——它们只是"打包时把源码复制了进去",不是真正的源文件 ✗
排查时搜到构建产物,会浪费大量时间(在几百个 .js 里找"哪个才是真的")✗
实测(二):排除之后
$ grep -rl "reflog" docs --exclude-dir=dist
docs/00-start/tools/S0-004.md
docs/03-enterprise/engineering/S3-030.md★ 只剩 2 个真正的源文件 ✓✓
这个对比说明了什么:
grep默认会搜所有文件——包括构建产物、依赖目录(node_modules)、打包输出 ⚠️所以搜索时"排除"是必备习惯,不是可选优化 ✓
常用的排除参数:
| 参数 | 作用 |
|---|---|
--exclude-dir=dist ★ | 排除目录(可多次指定)✓ |
--exclude="*.min.js" | 排除文件模式 ✓ |
--exclude-dir={dist,target,node_modules} | 一次排除多个(bash 的花括号展开)★ |
实测(三):grep -o + sort -u 提取并去重
$ grep -o "v2[.][0-9][.][0-9]-[a-z-]*" README.md | sort -u
v2.0.0-ta
v2.1.0-data-access
v2.2.0-spring-boot-engineering
v2.3.0-mysql-performance
v2.4.0-web-basics
v2.5.0-engineering★ 这条命令做了三件事:
grep -o → 只输出"匹配到的那一段"(而不是整行)✓
sort -u → 排序并去重 ✓
管道 | → 把前一个的输出当后一个的输入 ✓这就是"组合"的力量:两个小工具接起来,完成了"从文档里提取所有版本号"这件没有单个命令能做的事 ✓✓
这是本节的核心 ★★★
|:把上一个命令的输出,变成下一个命令的输入 cat app.log | grep ERROR | wc -l # 数一数有多少错误
ps -ef | grep java # 找 java 进程 ★
ls -l | sort -k5 -n # 按大小排序实测(组合出"最长的讲义"):
$ wc -l docs/03-enterprise/web/*.md | sort -n | tail -6
948 docs/03-enterprise/web/S3-018.md
1156 docs/03-enterprise/web/S3-021.md
1160 docs/03-enterprise/web/S3-020.md
1186 docs/03-enterprise/web/S3-019.md
4450 total三步拆解:
| 步骤 | 命令 | 作用 |
|---|---|---|
| ① | wc -l *.md | 统计每个文件的行数 |
| ② | sort -n | 按数字排序(不是字典序)✓ |
| ③ | tail -6 | 取最后 6 行(= 最大的几个)✓ |
| 符号 | 作用 |
|---|---|
> | 覆盖写入 ⚠️(会清空原文件) |
>> ★ | 追加写入(保留原内容)✓ |
2> | 只写错误输出 |
2>&1 ★ | 把错误也合并到标准输出 |
< | 从文件读输入 |
/dev/null | 丢弃输出("黑洞")✓ |
几个典型用法:
./start.sh > app.log 2>&1 & # ★ 后台启动,日志全部写进 app.log
echo "export JAVA_HOME=..." >> ~/.bashrc # ★ 追加(用 > 会毁掉整个配置文件 ✗✗)
grep ERROR app.log > errors.txt # 错误行存下来
command > /dev/null 2>&1 # 完全静默⚠️ > 与 >> 用错的后果(这是真实事故):
echo "xxx" > ~/.bashrc # ✗ 你的 shell 配置被清空了
echo "xxx" >> ~/.bashrc # ✓⚠️ 2>&1 的一个常见误解:顺序很重要 ✓
command > file 2>&1 # ✓ 正确:先把 stdout 指向 file,再把 stderr 指向"stdout 当前的位置"(也是 file)
command 2>&1 > file # ✗ 错误:先把 stderr 指向"当时的 stdout"(终端),再把 stdout 指向 file
# 结果:错误还是打在屏幕上 ✗关于"退出码"(这是脚本与 CI 的基础):
$ ls not-exist; echo "退出码=$?"
退出码=2| 退出码 | 含义 |
|---|---|
| 0 | 成功 ✓ |
| 非 0 | 失败(1 = 一般错误、2 = 用法错误、127 = 命令不存在)✓ |
$? 是"上一个命令的退出码" ★ ——这就是脚本能做"if 判断"与 CI 能判断成败的依据 ✓
| 工具 | 定位 | 典型 |
|---|---|---|
grep | 筛选行 | grep ERROR app.log ✓ |
sed | 按行编辑(替换、删除) | sed 's/old/new/g' file ✅ 批量替换 ★ |
awk | 按列处理 / 统计 ★ | awk '{print $1}' access.log 取第一列 |
awk 的一个实用例子(统计访问日志的状态码分布):
awk '{print $9}' access.log | sort | uniq -c | sort -rn | head拆解:awk 取第 9 列 → sort 排序 → uniq -c 计数 → sort -rn 按数字倒序 → head 取前几 ✓
★ 又是"组合" ——每个工具只做一件小事 ✓
tar -czf app.tar.gz app/ # 打包并 gzip 压缩 ★
tar -xzf app.tar.gz # 解压 ★
tar -tzf app.tar.gz # ★ 只看内容不解压(确认包里有什么,很重要 ✓)
tar -czf logs-$(date +%F).tar.gz logs/ # 带日期的归档名 ★参数记忆:c 创建、x 解压、t 查看、z gzip、f 指定文件名 ✓
⚠️ 两个实用提醒:
| 提醒 | 说明 |
|---|---|
解压前先 tar -tzf 看一眼 | 避免"解压出一堆散文件"污染目录 ✗ |
| 打包时不要包含绝对路径 | tar -czf x.tar.gz /opt/app 解压时会覆盖原路径 ⚠️ ——用 -C 先切目录 ✓ |
本节的思想内核 ★★★:
① 每个程序只做好一件事② 程序之间用"文本流"连接(这就是为什么日志是文本,而不是二进制) ③ 用管道组合出你想做的事
对比:
| "大而全"的工具 | Unix 小工具组合 | |
|---|---|---|
| 学习成本 | 要学一个庞大的软件 | 每个命令学 5 分钟 ✓ |
| 能做的事 | 软件提供什么就做什么 ✗ | 组合决定 ✓ |
| 可复用 | 弱 | 强(同一个 sort 到处用)✓ |
一个具体的体现(本节的实测):
"找出最长的 5 篇讲义" → wc -l | sort -n | tail -6
"提取所有版本号" → grep -o | sort -u
"统计状态码分布" → awk | sort | uniq -c | sort -rn | head没有一个命令是"用来做这些事"的——但组合起来就能做 ✓✓
tail -f /opt/app/logs/app.log# ① 看服务现在在打什么
tail -f app.log | grep -i error
# ② 统计今天有多少 500
grep "$(date '+%d/%b/%Y')" access.log | grep -c ' 500 '
# ③ 找出最慢的 10 个请求(假设日志最后一列是耗时)
awk '{print $NF, $7}' access.log | sort -rn | head -10
# ④ 把相关的日志单独存一份,便于后续分析
grep 'ERROR' app.log > /tmp/errors-$(date +%F).txt为什么不好:
| 问题 | 说明 |
|---|---|
| 日志很大(GB 级) | 下载慢、本地打不开 ✗ |
| 可能含敏感信息 | 下载到本机有合规风险 ⚠️ |
| 慢且重复 | 每次都要重新下载 ✗ |
正确做法:在服务器上用命令行分析(grep/awk/sort),只把"结论"(几行)带回来 ✓
⚠️ 关于本节的环境:本机是 Windows,演示用的是 Git Bash(MinGW) ✓
它能演示:文件系统操作、文本处理、管道、脚本(本节绝大部分内容)✓ 它不能演示:systemd、apt、
/proc的真实行为 —— 这些在 《Linux 服务部署与运维》 里以概念方式讲解,并明确标注 ✓
运行方式(Windows 上):
& "C:\Program Files\Git\bin\bash.exe" -c 'cd <仓库根目录>; <你要跑的命令>'(注意用单引号包裹,避免 PowerShell 抢走 $、<、> 等符号 ⚠️)
实测(一):基本信息与统计
pwd
find docs -name '*.md' | wc -l
wc -l docs/03-enterprise/web/*.md | sort -n | tail -6<仓库根目录>
93
948 docs/03-enterprise/web/S3-018.md
1156 docs/03-enterprise/web/S3-021.md
1160 docs/03-enterprise/web/S3-020.md
1186 docs/03-enterprise/web/S3-019.md
4450 total实测(二):grep 的"排除构建产物"效果 ★
grep -rl "reflog" docs | head -5 # 不排除
grep -rl "reflog" docs --exclude-dir=dist | head -5 # 排除后不排除:
docs/.vitepress/dist/00-start/tools/S0-004.html
docs/.vitepress/dist/03-enterprise/engineering/S3-030.html
docs/.vitepress/dist/assets/00-start_tools_S0-004.md.CJO1a9ot.js
...
排除后:
docs/00-start/tools/S0-004.md
docs/03-enterprise/engineering/S3-030.md实测(三):grep -o + sort -u
grep -o "v2[.][0-9][.][0-9]-[a-z-]*" README.md | sort -uv2.0.0-ta
v2.1.0-data-access
v2.2.0-spring-boot-engineering
v2.3.0-mysql-performance
v2.4.0-web-basics
v2.5.0-engineering实测(四):退出码
ls not-exist; echo "退出码=$?"退出码=2四处实测的结论:
| 实测 | 结论 |
|---|---|
| **`find | wc -l` = 93** |
| 排除 dist 后从 5 个变 2 个 ★ | 搜索时排除构建产物是必备习惯 |
| **`grep -o | sort -u` 提取出 6 个版本号** ★ |
| 退出码=2 | 命令的成败是可判定的(脚本与 CI 的基础)✓ |
sort -n 换成 sort:观察 100 排在 20 前面(字典序 vs 数值序)✗ ——理解 -n 为什么重要 ✓;grep -rl 不加 --exclude-dir:观察构建产物混进来(体会"排除"的价值)✓;> 误用成覆盖:在一个临时文件上试 echo a > f.txt; echo b > f.txt,观察只剩 b ✗ ——然后换成 >> ✓;2>&1 > file 与 > file 2>&1 对比:观察错误输出去哪了 ✓;ls /; echo $? 与 ls /not-exist; echo $? 对比 ✓。rm -rf 写错路径 见 5.3 节。这是最危险的命令 ✗
特别提醒:变量没加引号 是隐蔽杀手 ✓
rm -rf $DIR/* # ✗ DIR 为空 → rm -rf /*
rm -rf "$DIR"/* # ✓ 至少在 DIR 为空时变成 "rm -rf /*" 但仍危险 —— 更好的是先判断
[ -n "$DIR" ] && rm -rf "$DIR"/*见 5.5、7(二)。浪费时间且容易改错文件 ✗
> 覆盖重要配置文件 见 5.6 节。>> 是追加,> 是覆盖 ✓
grep 时不加引号 grep ERROR app.log # 简单场景 OK
grep "500 Internal" app.log # ★ 有空格必须加引号,否则被当成两个参数| 会传递"失败" command1 | command2 # 默认:command2 的退出码才是整体的退出码
# 想让任一环节失败就整体失败 → set -o pipefail(见 S3-035)✓ls -la | head -5 # ✗ PowerShell 的 ls 是别名,head 不存在修:用 Git Bash(本节的方式)或 WSL ✓
sudo 当"万能前缀" sudo chmod 777 /opt/app # ✗ 777 是"所有人可读写执行",很危险 ✗✗正确做法:最小权限(《Linux 文件、用户与权限管理》)✓
目标:把"查找、管道、重定向"练熟。
步骤:
find ... | xargs wc -l 或 find -name '*.md' -exec wc -l {} +)✓;grep -rl "Spring" docs vs grep -rl "Spring" docs --exclude-dir=dist,对比结果数量 ✓;echo a > /tmp/t.txt; echo b >> /tmp/t.txt; cat /tmp/t.txt(观察 > 与 >> 的差别)✓;awk:ls -l | awk '{print $5, $9}'(取"大小"与"文件名"两列)✓;$? ✓。验收方式:能说出第 3 步两个结果的差距、第 4 步 > 与 >> 的差别。
扩展任务:写一个"日志分析"命令组合:
awk 取耗时列、sort -rn、head;基础题
grep -r 默认不会搜索构建产物目录。(对 / 错)> 表示 ______,>> 表示 ______;把标准错误合并到标准输出用 ______。代码阅读题
cat app.log | grep "ERROR" | awk '{print $2}' | sort | uniq -c | sort -rn | head -5app.log 有 2GB,这条命令有什么问题?怎么改进?编程题
find -mtime +7 找旧日志;tar -czf 打包(文件名带日期);tar 的退出码)★;综合任务
提示 1(方向):第 1 题的方向是"它默认什么都搜"。
提示 2(关键点):第 5 题——cat 一个 2GB 文件会怎样?能不能省掉 cat?
提示 3(接近答案):第 7 题的"信息层次"——Linux 命令看"这台机器现在怎样",慢查询日志看"数据库怎样",链路追踪看"一次请求经过了哪些服务"。
grep -r 会搜索所有文件(包括 dist/、target/、node_modules/)✗实测证据(7(二)):不排除时前 5 个结果全是构建产物 ✓
为什么它是"默认行为":grep 不知道哪些是构建产物 ——那是你的项目知识 ✓
所以要显式排除:
grep -r "xxx" . --exclude-dir={dist,target,node_modules,.git}更省事的做法:配一个别名或一个脚本 ✓
# ~/.bashrc
alias g='grep -rIn --exclude-dir={dist,target,node_modules,.git,.idea}'
# 之后:g "ERROR" .-I 也很有用:跳过二进制文件 ✓(否则 grep 会输出 "Binary file xxx matches" 这种无用的行)
2>&1 ✓记忆:一个 > 是"冲掉",两个 >> 是"接着写" ✓
2>&1 的含义拆解:
2 = 标准错误(stderr)的文件描述符
> = 重定向
&1 = "文件描述符 1 现在指向的地方"(标准输出)
合起来 = 让 stderr 去 stdout 现在去的地方 ✓⚠️ 那个 & 不能少:2>1 是"把错误写进一个叫 1 的文件" ✗(完全不同)
| 理由 | 说明 |
|---|---|
| ① 可组合性 ★★ | N 个小工具能组合出远多于 N 种用途 ✓ ——这是"指数级"的能力增长 |
| ② 每个都好学好记 | sort 就是排序,wc 就是计数 —— 不需要学一个庞大的软件 ✓ |
| ③ 可复用 | 学会 sort -n,在所有场景都能用 ✓ |
一个对比:
"大而全"的工具: 提供了 100 个功能,但第 101 件事做不了 ✗
小工具组合: 30 个命令,能组合出几乎无限种用途 ✓还有一个工程上的好处:
组合出来的命令是"可分享的" ——一句话就能写进运维手册、发到群里 ✓
"你跑一下
awk '{print $9}' access.log | sort | uniq -c | sort -rn | head"比"你打开 XX 软件,点第三个标签,再点导出,再……"要高效得多 ✓✓
第 4 题:逐步拆解
cat app.log # ① 输出整个文件
| grep "ERROR" # ② 只保留含 ERROR 的行
| awk '{print $2}' # ③ 取每行的第 2 列(假设是错误类型/接口名)
| sort # ④ 排序(让相同的挨在一起)
| uniq -c # ⑤ 合并相邻相同行,并计数 ★
| sort -rn # ⑥ 按计数**倒序**排序(r=reverse, n=按数值)
| head -5 # ⑦ 取前 5结果:"出现最多的 5 种错误(按第 2 列归类)及其次数" ✓
★ 关键在第 ④ 步:uniq 只能合并"相邻的"重复行 ——所以必须先 sort ✓
(这是新手最容易错的一点:以为 uniq 自己会去重,其实它只处理相邻行 ✗)
第 5 题:2GB 文件的问题
| 问题 | 说明 |
|---|---|
cat 是多余的 ✗ | **`cat file |
| 内存 | sort 需要把数据放进内存(或临时文件)——2GB 的匹配结果可能撑爆内存 ⚠️ |
| 耗时 | 全文件扫描 2GB 本身就要时间 |
改进版:
grep "ERROR" app.log | awk '{print $2}' | sort | uniq -c | sort -rn | head -5进一步的优化:
| 优化 | 做法 |
|---|---|
| 先缩小范围 | 如果只要今天的:`grep "$(date '+%F')" app.log |
用 awk 一次做完筛选+取列 ★ | `awk '/ERROR/ {print $2}' app.log |
甚至用 awk 做计数 ★★ | `awk '/ERROR/ {count[$2]++} END {for (k in count) print count[k], k}' app.log |
最后那条的价值:
原来的管道:cat → grep → awk → sort → uniq → sort → head (7 个进程)
优化后: awk → sort → head (3 个进程)"能用一个工具做完的,就别用一串管道" ——但前提是它仍然可读 ✓
可读性与性能的权衡:日常排查用管道(清晰),超大文件或高频任务用 awk(快) ✓
⚠️ 一个安全提醒:处理 GB 级日志时注意磁盘 ——grep ... > errors.txt 也可能写出一个巨大的文件 ⚠️ (先 grep -c 看看有多少条,再决定要不要落盘 ✓)
#!/usr/bin/env bash
set -uo pipefail
LOG_DIR="${1:-/var/log/myapp}"
ARCHIVE_DIR="${2:-/var/log/myapp/archive}"
DAYS="${3:-7}"
mkdir -p "$ARCHIVE_DIR"
today=$(date '+%F')
archive="$ARCHIVE_DIR/logs-$today.tar.gz"
echo "归档目录 : $LOG_DIR"
echo "保留天数 : $DAYS 天以内的日志不归档"
# ① 找出旧日志(-mtime +7 = 7 天前修改的;-type f 只处理文件)
mapfile -t old_logs < <(find "$LOG_DIR" -maxdepth 1 -type f -name '*.log' -mtime "+$DAYS")
if [ "${#old_logs[@]}" -eq 0 ]; then
echo "没有需要归档的日志 ✓"
exit 0
fi
echo "待归档文件数 : ${#old_logs[@]}"
# ② 打包(用 -C 切目录,避免把绝对路径打进去)
# 注意:用 printf '%s\0' + tar --null 处理"文件名含空格"的情况
printf '%s\0' "${old_logs[@]}" | tar -czf "$archive" --null -T -
# ③ ★ 关键:检查打包是否成功,成功才删除
if [ $? -ne 0 ] || [ ! -s "$archive" ]; then
echo "✗ 打包失败或归档文件为空,**不删除原文件**"
exit 1
fi
echo "归档文件 : $archive"
echo "归档大小 : $(du -h "$archive" | cut -f1)"
# ④ 确认后再删
rm -f "${old_logs[@]}"
echo "已删除原文件 : ${#old_logs[@]} 个 ✓"
echo "归档完成 ✓"关键点说明:
| # | 做法 | 为什么 |
|---|---|---|
| ① | find -mtime +7 而不是 +7 天"以内" | -mtime +7 = 严格早于 7 天前 ✓(-7 是 7 天内)⚠️ |
| ② | 用 -C 或相对路径打包 | 避免解压时覆盖原路径 ✗ |
| ③ | ★ 检查 $? 与 -s(文件非空) | 打包失败时删除 = 数据永久丢失 ✗✗✗ |
| ④ | 先 echo 再 rm | 可追溯(留下了"删了哪些"的记录)✓ |
| ⑤ | printf ... \0 + tar --null -T - | 正确处理文件名含空格的情况 ✓(新手常漏) |
为什么"先确认打包成功再删原文件"不能省
因为删除是不可逆的,而打包是可能失败的 ✓
打包失败的原因(都不是罕见情况):
| 原因 | 说明 |
|---|---|
| 磁盘满了 ✗ | 归档文件写了一半 → tar 失败,但退出码可能"看起来成功"(所以要检查 -s 非空)✓ |
| 权限不足 | 某些日志文件读不了 ✗ |
| 文件被占用/正在写入 | 日志还在被写 → 打包出的内容不完整 ⚠️ |
| 路径错误 | 变量拼错 → 打包了空内容 ✗ |
一个真实的事故模式:
tar -czf archive.tar.gz logs/ && rm -rf logs/
# 看起来很严谨(用了 &&),但——
# 如果磁盘满了,tar 可能创建了 archive.tar.gz 却只写了一部分
# && 判断"tar 退出码为 0"就继续删了 ✗
# 结果:归档是坏的,原文件没了 ✗✗✗所以要做"双重确认":
tar -czf "$archive" ... || exit 1 # ① 退出码
[ -s "$archive" ] || exit 1 # ② 文件非空
tar -tzf "$archive" >/dev/null || exit 1 # ③ ★ 归档能被正常读回来(最强的验证)第 ③ 条最稳:"能解压/能列出内容"才算真的成功 ✓
这与 《Git 分支、合并与回退》 讲的"reset --hard 前先备份"是同一种谨慎:
对不可逆操作,都要先有一个"可验证的成功"作为前提 ✓
而且验证要"真的读回来",而不是"看退出码" ——退出码可能在"部分失败"时也是 0 ⚠️
补充:生产环境的日志归档还应该考虑:
| 考虑 | 说明 |
|---|---|
| 日志轮转(logrotate) | 通常交给工具做,比自己写脚本更可靠 ✓ |
| 归档保留多久 | 合规要求(如 6 个月)✓ |
| 归档也要有清理规则 | 否则归档目录自己会撑爆磁盘 ✗ |
排查流程(含命令)
| 步 | 做什么 | 命令 | 看什么 / 说明什么 |
|---|---|---|---|
| ① | 确认现象 | curl -w "%{time_total}\n" -o /dev/null -s http://localhost:8080/api/orders | 到底多慢?("很慢"没有信息量)✓ |
| ② | 看服务在不在 | ps -ef | grep java / systemctl status myapp | 进程是否还活着 ✓ |
| ③ | 看实时日志 ★ | tail -f logs/app.log | grep -i error | 有没有异常在刷 ✓ |
| ④ | 看错误数量趋势 | grep -c ERROR logs/app.log | 是不是突然变多 ✓ |
| ⑤ | 看机器负载 ★ | top / uptime | CPU、内存、load ——load 高说明"排队" ✓ |
| ⑥ | 看磁盘 | df -h | 磁盘满了会导致各种诡异问题 ✓ |
| ⑦ | 看连接/端口 | ss -tlnp | grep 8080 | 端口在不在监听 ✓ |
| ⑧ | 看 GC(Java) | jstat -gcutil <pid> 1000 10 | 频繁 Full GC → 内存问题 ✓ |
| ⑨ | 看数据库 | 见 《执行计划与慢查询定位》 的慢查询 | 慢在哪条 SQL ✓ |
| ⑩ | 看依赖服务 | curl 下游 / 看超时日志 | 是不是被别人拖慢 ✓ |
每一步"在看什么":
| 层 | 命令 | 回答的问题 |
|---|---|---|
| 应用 | tail -f | "我的代码在报错吗?" |
| 进程 | top / jstat | "这台机器的资源够吗?" |
| 系统 | df -h / uptime | "机器本身健康吗?" |
| 依赖 | curl / 日志 | "是不是被下游拖慢?" |
| 数据 | 慢查询 | "是不是 SQL 慢?" |
"不该做什么":
| 不该做 | 为什么 |
|---|---|
| 一上来就重启 ✗✗ | 重启会清掉现场(内存、连接、线程栈)——你再也没法知道刚才发生了什么 ✓ |
| 直接改生产配置 ✗ | 没有经过验证的改动 = 第二次事故 ✗ |
| 在生产上跑"测试性"的重命令 | 如 rm、DROP、全表更新 ✗✗ |
| 只改现象不改原因(如调大超时) | 只是把问题藏起来 ⚠️ |
| 不记录就操作 | 事后无法复盘 ✗ |
★ 第一条最重要:"先保留现场,再恢复服务"
重启前至少做:
· jstack <pid> > /tmp/thread-$(date +%s).txt # 线程栈
· jmap -histo:live <pid> > /tmp/histo.txt # 内存直方图(或 heap dump)
· 复制一份最近的日志为什么:重启能恢复服务,但只有现场能告诉你"为什么会这样" ✓ ——否则同样的问题一定会再来一次 ✓
如何留下证据:
| 证据 | 怎么留 |
|---|---|
| 命令的输出 | command > /tmp/排查-$(date +%s).txt 2>&1 ✓ |
| 完整操作记录 ★ | script 命令(记录整个终端会话)或用带日志的终端工具 ✓ |
| 时间线 | 记录"几点发现、几点做了什么、效果如何" ✓ |
| 快照 | 线程栈、堆、GC 日志、慢查询 ✓ |
对照 《执行计划与慢查询定位》 与 《日志框架与日志规范》:信息层次
| 层次 | 工具 | 回答 | 粒度 |
|---|---|---|---|
| 机器/进程(本节) | top、df、ps、jstat | "这台机器现在怎样" | 一台机器 ✓ |
| 数据库(《执行计划与慢查询定位》) | 慢查询、EXPLAIN | "哪条 SQL 慢、为什么" | 一条 SQL ✓ |
| 日志(《日志框架与日志规范》) | 应用日志 + traceId | "某一次请求发生了什么" | 一次请求 ✓ |
| 指标(《日志框架与日志规范》) | Prometheus/Grafana | "整体趋势与当前水位" | 聚合 ✓ |
| 链路追踪(《日志框架与日志规范》) | Jaeger/SkyWalking | "一次请求慢在哪一段" | 跨服务 ✓ |
它们的协作方式(一个完整的排查):
① 指标(Grafana)发现:"订单接口 P99 从 200ms 涨到 3s" ← 发现异常
② 机器层(top):CPU 正常、内存正常、磁盘正常 ← 排除机器问题
③ 日志(按 traceId):看到一次请求里 DB 段耗时 2.8s ← 缩小到数据库
④ 慢查询日志(S3-015):找到那条 SQL,EXPLAIN 发现没走索引 ← 定位原因
⑤ 链路追踪:确认"就是这个 DB 段"(并看看有没有别的服务也慢) ← 交叉验证一句话总结:
Linux 命令是"底层体检"(这台机器怎么了),慢查询是"专科检查"(数据库怎么了),日志/指标/链路是"病历与监护仪"(这次请求怎么了、长期趋势如何)。 ✓
排查问题时,通常是"监护仪发现异常 → 体检排除环境 → 病历定位到具体请求 → 专科确认根因" ✓
这也呼应了 《执行计划与慢查询定位》 开头那句:"先测量,再优化" ✓
/、~、/etc、/var/log、/opt 各自放什么;rm -rf 的风险);tail -f 看实时日志 ★;grep 查找内容,并记得排除构建产物 ★;> 与 >>,知道 2>&1 的作用与顺序;$? 是退出码及其意义;sort | uniq -c 的用法(以及"必须先 sort");tar 的打包/查看/解压;《Linux 文件、用户与权限管理》:理解 ls -l 里那串 -rw-r--r-- 到底是什么意思。