Linux 运维常用命令

共 104 条真会敲的命令,参数逐条说明 —— 按「要干什么」分类

📁 文件与目录 (8)

查找、批量处理、打包 —— 参数记错的代价通常是删错文件或漏掉隐藏目录

ls:看清权限、时间与大小

-l 长格式,显示权限/属主/大小/时间
-a 连 . 和 .. 及所有隐藏文件一起列出
-h 大小带单位(1.2K 而不是 1234),必须和 -l 一起用
-t 按修改时间倒序,最新在最上面
-r 反转排序,配合 -t 就是最旧的在上面
-S 按文件大小倒序 —— 想找「谁占地方」比 -t 直接
--time-style=long-iso 时间显示成 2026-10-29 09:18,比默认好认

# 最新的文件在最上面,带大小和时间
ls -laht --time-style=long-iso

# 按大小找占地方最大的几个(-S)
ls -lhS /var/log | head

find:按条件找文件并处理

-name 按文件名匹配,区分大小写;-iname 不区分
-type f 只要普通文件,-type d 只要目录,-type l 软链接
-mtime +7 修改时间超过 7 天;-mmin -30 30 分钟内改过
-size +50M 大于 50M;-size -1k 小于 1k
-maxdepth 1 不递归子目录,排查时先加它能快一个数量级
-ls 对结果执行 ls -dils,直接带大小、inode、时间
-delete 直接删除,只能删空目录;-exec ... \; 对每个结果执行命令
-prune 跳过某个目录,必须和 -path 配合
-o 是「或者」,串联多个条件时用;-print 显式输出(配 -prune 时必须写)

# 30 分钟内改过的大文件(排查磁盘暴涨)
find /var -type f -mmin -30 -size +50M -ls

# 批量删除 7 天前的日志
find /var/log -name '*.log.*' -mtime +7 -delete

# 跳过 .git 和 node_modules 再找
find . -path ./node_modules -prune -o -name '*.js' -print

grep:递归搜索与上下文

-r 递归目录;-R 递归且跟随软链接
-i 忽略大小写;-n 显示行号;-v 反向,只要不匹配的行
-E 用扩展正则(| 一定要用 -E,否则被当字面量)
-l 只输出文件名,适合先定位再细看
-A3 / -B3 / -C3 显示匹配行的后 3 行 / 前 3 行 / 前后各 3 行
--include='*.conf' 只搜指定后缀;--exclude-dir 跳过目录
-w 全词匹配,避免搜 cat 把 category 也搜出来
-rn 是最常用的组合:递归 + 行号,否则报错不知道去哪一行看

# 在 nginx 配置里找某个 upstream,带前后 3 行
grep -rn -C3 'upstream' /etc/nginx --include='*.conf'

# 多个关键词任一命中(必须 -E)
grep -rnE 'timeout|refused|reset' /var/log/app/error.log

# 只列出含配置的文件名,再逐个看
grep -rli 'proxy_pass' /etc/nginx

tar:打包与解包

-c 创建;-x 解包;-t 只看内容不解;-z 走 gzip;-j 走 bzip2
-f 后面紧跟文件名,所以 -f 永远放最后
-v 列出处理过程,大包时会让输出很慢,重定向掉它
-C 解到指定目录,目录必须已存在
--exclude 打包时排除,注意相对路径的写法要匹配
-czf / -xzf / -tzf 是最常见的三个组合,记这三串足够
解包前用 -t 先看一遍,能避免 tar 炸弹覆盖当前目录

# 打包,排除缓存目录
tar -czf app.tar.gz --exclude='node_modules' --exclude='.git' -C /srv app

# 先看内容,再解到指定目录
tar -tzf app.tar.gz | head -20
tar -xzf app.tar.gz -C /tmp/restore

cp / mv / rm:三个危险参数

rm -rf 递归强制删除,不给确认 —— 变量为空时 rm -rf "$DIR/" 会变成 rm -rf /
-f 强制、不提示;-r 递归目录。这两个合起来就是最容易出事的那串
cp -a 归档模式,保留权限、时间、软链接,备份首选
cp -n 不覆盖已存在文件;mv -n 同理(-n 是 no-clobber)
mv 跨文件系统时是「复制 + 删除」,不是原子操作,中断会两头不全
rm 删掉的文件若被进程占用,空间要等进程退出才释放(见磁盘排查)
test -n "$DIR" 判断变量非空,是 rm 前必做的保护
-- 告诉命令「后面不再有选项」,防止文件名以 - 开头被当成参数

# 安全删除:先确认变量非空,再删
[ -n "$DIR" ] && rm -rf -- "$DIR"

# 完整备份并保留属性
cp -a /srv/app /backup/app-$(date +%F)

du / df:是谁把磁盘吃满了

df -h 看各挂载点使用率;-i 看 inode 使用率(inode 满了也写不进文件)
du -sh 汇总单目录大小;-s 只出总数不列子项,-h 人类可读
du -sh * | sort -h 当前目录下谁最大,sort -h 能正确排 1K/2M/3G
--max-depth=1 只看一层,比全量递归快得多
du 与 df 数字差很多,基本就是「已删除但仍被进程占用」的文件
2>/dev/null 把权限不足的报错丢掉,否则输出被刷满

# 从根往下逐层找,哪一层突然变大就进去看
du -h --max-depth=1 / 2>/dev/null | sort -h | tail -10

# 已删除但被进程占用的文件(df 满、du 却不大时用)
lsof +L1 2>/dev/null | head

ln:软链接与硬链接

ln -s 目标 链接名 —— 参数顺序是「目标在前」,反了会建出个坏链接
-s 建软链接;不加 -s 是硬链接(不能跨文件系统、不能链目录)
-f 覆盖已存在的链接
-n 当链接指向目录时,配合 -f 才能正确替换而不是塞进目录里
-sfn 是发布目录切换的标准组合(current 指向新 release)
软链接存的是路径字符串,目标移动后失效;硬链接指向同一 inode,删一个不影响另一个

ln -sfn /srv/app/releases/20261029 /srv/app/current

stat / file:文件到底是什么

stat 一次给出大小、inode、权限和三个时间 —— 「这个文件真的被改过吗」看它最直接
-c 自定义格式:%y 修改时间、%s 字节数、%U:%G 属主、%a 八进制权限、%n 文件名
-L 跟随软链接,看目标文件本身(不加就是看链接这个「文件」)
三个时间:Access 读过、Modify 内容改过、Change 元数据(权限/属主)改过
-f 看文件系统层的信息(块大小、inode 总数与剩余),和 df 互补
touch -d '2026-10-01 08:00' 文件 改修改时间;配合 find -newer 框定变更范围
file 判断真实类型,比看后缀可靠 —— 后缀可以随便改
stat -c '%a' 是排查「权限到底几位」最快的写法

stat -c '%y %s %U:%G %a %n' /etc/nginx/nginx.conf
stat -L -c '%y %s' /srv/app/current        # 看软链接指向的真实文件
file /usr/local/bin/app

✂️ 文本处理与管道 (8)

改配置、按列统计、拼命令 —— 运维里「手改 200 行配置」的事都该交给 sed/awk

sed:改文件内容(含原地修改)

-i 直接改文件(原地替换)—— 生产上先不加 -i 跑一遍看输出,确认再改
-i.bak 改的同时留一份 .bak 备份,出问题能立刻回滚(强烈建议养成习惯)
-n 不自动打印,配合 p 只输出指定行,等价于「看某几行」
-E 用扩展正则(+ ? | ( ) 不用转义);老系统上是 -r,作用相同
-e 串联多个表达式,也可以用分号写在一个表达式里
s/旧/新/g 的 g 表示「一行里全部替换」,不加 g 只换每行第一处
正则地址:'/^server/,/}/ s/a/b/' 只改某个配置块内部
\1 \2 引用捕获组,& 代表整个匹配内容
-i 后必须跟 -E 或表达式顺序要注意:sed -E -i.bak 's/x/y/' 文件

sed -n '1,20p' /etc/nginx/nginx.conf              # 只看前 20 行
sed -i.bak 's/listen 80;/listen 8080;/' /etc/nginx/sites-enabled/app
sed -E -i.bak 's/^server_name[[:space:]]+([^;]+);/server_name \1.bak;/' app.conf
sed '/^[[:space:]]*#/d;/^[[:space:]]*$/d' app.conf | head        # 去掉注释与空行

awk:按列取值与统计

-F 指定分隔符:-F: 按冒号切,-F'\t' 按制表符切,不写就按连续空白
'{print $1,$3}' 取第 1、3 列;$0 是整行;NF 是列数;NR 是行号
条件过滤直接写在程序前面:'$9==500' 表示第 9 列等于 500
数值比较要转成数字:$9+0 >= 500,否则字符串比较会得出意外结果
'{s+=$7} END {print s}' 是求和模板;计数用 '{c++} END {print c}'
数组统计:{c[$1]++} END {for (k in c) print k, c[k]}
-v 传外部变量:awk -v lim=500 '$3>lim',注意 -v 要写在程序之前
-F 和 -v 都是 awk 自己解析的,必须放在引号内的程序前面
示例里 sort -rn 是按数值倒序,uniq -c 是统计次数,ss -tn 是 TCP 连接(各参数见对应条目)

awk -F: '$3>=1000 {print $1, $3}' /etc/passwd
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
awk -v lim=500 '{ if ($9+0 >= lim) print NR": "$7 }' access.log
ss -tn | awk 'NR>1 {c[$1]++} END {for (k in c) print k, c[k]}'

sort / uniq:排序、去重、算 Top N

sort -n 按数值排(默认字典序会把 10 排在 9 前面)
sort -h 识别 1K/2M/3G 单位,处理 du / df 输出必须用它
sort -r 倒序;-u 排序同时去重
-k2 按第 2 列排序,-k3 同理;多列写 -k1,1 -k2,2
-t: 指定分隔符为冒号(默认按空白切)
uniq -c 统计连续重复次数 —— 所以必须先 sort,否则同样的值会被算成多段
uniq -d 只看重复项,uniq -u 只看唯一项
Top N 的固定套路:sort | uniq -c | sort -rn | head
示例里 du -sh 的 -s 是只出总数不列子项(见 du / df 条目)

du -sh /var/log/* | sort -h | tail -5
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
sort -t: -k3 -n /etc/passwd | head

cut / tr:切列与字符替换

cut -d: -f1,7 按分隔符取列:-d 定分隔符,-f 定第几列(可多列)
cut -c1-19 按字符位置切,适合定长日志取时间戳部分;-c1-19 表示第 1 到 19 个字符
cut 只认单一分隔符,多空格对齐的输出用它会被切错,改用 awk
tr 'a-z' 'A-Z' 逐字符映射;tr 只能一对一换字符,替换字符串得用 sed
tr -d 删除字符,最常用的是 tr -d '\r' 去掉 Windows 换行
tr -s 把连续重复字符压成一个,常用于把多空格压成单空格
tr 只能从标准输入读,不能直接接文件名,用 < 重定向
示例里 df -h 的人类可读、cut -f5 取第 5 列(即 Use%)、sort -u 去重,均在各自条目详述

cut -d: -f1,7 /etc/passwd | head
cut -c1-19 /var/log/nginx/access.log | head -3
tr -d '\r' < crlf.txt > lf.txt
df -h | tr -s ' ' | cut -d' ' -f5 | sort -u

xargs:把结果拼成命令参数

xargs 把标准输入的每一行变成后面命令的参数,find | xargs rm 就是这么来的
-0 配合 find -print0 使用:文件名带空格或换行也不会被切开(处理用户上传文件必须加)
find -print0 用 \0 结尾输出,专为 xargs -0 准备
-n1 每次只传一个参数,逐条执行,比一次性传一堆更安全、也能看到是哪条失败
-I {} 占位符,把参数放到命令中间:xargs -I {} cp {} /backup
-p 执行前询问,-t 先打印再执行 —— 不确定拼出什么命令时先用 -t 看一眼
-r 输入为空时不执行(GNU 扩展),否则空输入也会触发一次 rm
-P4 并行 4 个进程,批量压缩/下载时能快数倍
cp --parents 连目录结构一起复制,恢复备份时很有用
示例里 find 的 -name / -mtime / -type 是筛选条件,rm -f 的 -f 是强制不提示

find /var/log -name '*.log.*' -mtime +7 -print0 | xargs -0 -r rm -f
find /srv/uploads -type f -print0 | xargs -0 -I {} cp --parents {} /backup
printf '%s\n' a b c | xargs -n1 -t echo

tee:一边看一边落盘

tee 把输入同时写到文件和标准输出,改配置前留证据最有用
-a 追加而不是覆盖,日志类一律用它
为什么要 sudo tee:sudo echo x > /etc/f 里的重定向是当前 shell 做的,会因权限失败
-i 忽略中断信号,抓一段长输出时不会被 Ctrl-C 打断
管道里接手后要 2>&1 把 stderr 并进来,否则错误信息根本进不了 tee
给命令加时间戳:... | ts 或 | awk '{print strftime("%F %T"), $0}'

./deploy.sh 2>&1 | tee -a /var/log/deploy-$(date +%F).log
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf

jq:解析 JSON 返回

jq 处理接口返回的 JSON,比 grep 靠谱得多 —— 它是按结构取,不是按行匹配
. 取字段:.data .items[0].name;.[] 遍历数组
-r 输出裸字符串(去掉双引号),塞进变量或再管道时必须加
-c 每个结果压成一行,方便再喂给 sort / uniq 统计
'keys' 看对象有哪些字段;'length' 看数组长度;'..' 列出所有路径(结构不明时先摸形状)
select() 过滤:'.[] | select(.status != "ok")'
-C 彩色输出(人看),-M 单色(写进文件或脚本用)
jq 默认格式化输出,-e 用退出码表达「结果是否为真」,脚本里判断用
示例里 curl -s 是静音,否则进度条会混进 JSON 解析(见 curl 条目)

curl -s https://api.example.com/v1/tools | jq -r '.data[].name'
curl -s 'https://api.example.com/v1/list?page=1' | jq -c '.items[] | {id, name}'
jq -r '.[] | select(.status != "ok") | .id' resp.json

diff / wc:比对与计数

diff -u 统一格式输出,就是补丁格式,能直接被 patch 用
diff -r 递归比两个目录;-q 只报「哪些文件不同」,不打印内容
diff -y 并排显示,-W 200 控制侧栏宽度,肉眼比对配置时好用
wc -l 行数(统计日志条数最常用);-c 字节数;-w 词数
wc 输出会带文件名,脚本里取值注意多一列,可用 awk '{print $1}'
comm 更适合「两个有序列表的差集」,但两边都必须先 sort
comm -13 a b 输出只在 b 里的行;-12 输出只在 b 里的,与 -13 相同逻辑要看清列
比对前统一排序与去重,否则「看起来全不一样」其实只是顺序不同

diff -u /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf | head -40
diff -rq /srv/app/releases/20261028 /srv/app/releases/20261029
wc -l /var/log/nginx/access.log
sort a.txt > a.s; sort b.txt > b.s; comm -13 a.s b.s   # 只在 b 里的行

🖥️ 远程登录与文件传输 (8)

登录、免密、传文件、续传 —— 日常最费时间的其实是「怎么把文件弄过去」这一步

ssh:登录远程服务器

ssh 用户@主机 最基本登录;默认 22 端口
-p 2222 指定端口(注意:scp/sftp 用大写 -P,这两个别搞混)
-i 指定私钥文件;默认找 ~/.ssh/id_ed25519、id_rsa
-t 强制分配伪终端 —— 远程跑 sudo、top 这类交互命令必须加
-v 打印握手与认证过程,连不上先看它卡在「网络不通」还是「认证失败」;-vvv 更细
-o 透传配置项:-o ConnectTimeout=5 防卡死,-o BatchMode=yes 禁止任何交互(脚本必须有)
远程只跑一条命令:ssh 主机 '命令',不分配终端,适合批量巡检
-J 跳板机:ssh -J bastion@1.2.3.4 deploy@10.0.0.5(旧写法 -o ProxyCommand)
示例里 df -h 的 -h 是人类可读(见 du / df 条目)

ssh -p 2222 -i ~/.ssh/id_ed25519 deploy@10.0.0.5
ssh -o ConnectTimeout=5 -o BatchMode=yes 10.0.0.5 'uptime; df -h /'
ssh -v -t 10.0.0.5 'sudo systemctl restart app'
ssh -J bastion@1.2.3.4 deploy@10.0.0.5

ssh-keygen / ssh-copy-id:免密登录

ssh-keygen -t ed25519 生成密钥对(比 rsa 短、快,现代系统都支持)
-t 类型:ed25519 首选,rsa 只在老系统上不得已为之,配合 -b 4096 指定长度
-C 注释写清用途/邮箱,手里多把密钥时靠它区分
-f 指定输出文件名,避免覆盖默认密钥(覆盖了原来的免密就废了)
-N '' 空口令,自动化用;交互时可以直接回车
ssh-copy-id 把公钥追加到对方 ~/.ssh/authorized_keys,并自动修好目录与文件权限
-i(ssh-copy-id 的 -i)指定要复制哪把公钥,默认只发 id_*.pub
-R 从 known_hosts 删掉某主机的旧指纹 —— 重装机器后一定先跑它,否则报 REMOTE HOST IDENTIFICATION HAS CHANGED
-lf 查看公钥指纹与类型,核对两台机器上是不是同一把
私钥权限必须 600,目录 700,否则 ssh 直接拒绝使用

ssh-keygen -t ed25519 -C 'deploy@icetools' -f ~/.ssh/id_deploy -N ''
ssh-copy-id -i ~/.ssh/id_deploy.pub deploy@10.0.0.5
ssh-keygen -R 10.0.0.5
ssh-keygen -lf ~/.ssh/id_deploy.pub

~/.ssh/config:别名与跳板

配好之后 ssh app 就等于 ssh -p 2222 deploy@10.0.0.5,不用每次敲一长串
Host 是别名,也是后面所有配置的生效范围
HostName 真实地址;User 登录用户;Port 端口;IdentityFile 私钥路径
IdentitiesOnly yes 只用手指定这把钥匙,避免 agent 里一堆密钥挨个试(试太多次会被服务端封)
ProxyJump 跳板机,等价于命令行的 -J,而且 scp / rsync 自动继承
ServerAliveInterval 30 每 30 秒发心跳,NAT 或云负载均衡会掐空闲连接,长任务必配
ServerAliveCountMax 3 心跳失败 3 次才断,配合上面的间隔
Host * 放在文件末尾,写全局默认(心跳、超时)
权限:config 600、~/.ssh 700,否则报 Bad owner or permissions

# ~/.ssh/config  (chmod 600)
Host app
    HostName 10.0.0.5
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_deploy
    IdentitiesOnly yes
    ServerAliveInterval 30
    ServerAliveCountMax 3

Host db
    HostName 10.0.0.6
    ProxyJump app

Host *
    ServerAliveInterval 30

ssh 隧道:端口转发

-L 本地转发:ssh -L 8080:127.0.0.1:80 主机 —— 把远端 80 映射到本机 8080
-R 反向转发:把本机端口暴露到远端,让外部能访问内网服务
-D 动态转发,起一个 SOCKS 代理(配合 curl --socks5 或浏览器)
-N 只做转发、不开 shell;-f 转后台运行,两者几乎总是成对出现
-g 允许其他机器连本地转发端口(默认只监听 127.0.0.1)
-L 的中间地址是从「远端视角」解析的,写 127.0.0.1 指远端自己
「数据库不让直连」的场景:先用 -L 把端口拉到本地,再连 127.0.0.1
端口被占就换本地端口,lsof -i :8080 看谁占着

ssh -N -f -L 15432:127.0.0.1:5432 deploy@10.0.0.5
ssh -N -L 8080:10.0.0.6:80 app
ssh -D 1080 -N -f app
ssh -g -L 8080:127.0.0.1:80 app

scp:上传 / 下载文件

scp 源 目标 —— 方向决定上传还是下载:本地在前是上传,远端在前是下载
-P 大写指定端口(ssh 是小写 -p,这是最容易搞混的一处)
-r 递归整个目录;不加 -r 传目录会直接报错
-i 指定私钥;-o 透传 ssh 选项,比如 -o ConnectTimeout=5
-C 传输时压缩,文本和日志收益明显;已经把 tar.gz 压过的没意义
-l 8000 限速,单位是 Kbit/s(8000 ≈ 1MB/s),别把生产带宽打满
-p 小写保留时间戳与权限,和 -P 完全无关,别混
scp 没有断点续传 —— 大文件断一次从头再来,几 G 的东西改用 rsync -P

scp -P 2222 app.tar.gz deploy@10.0.0.5:/srv/
scp -r -i ~/.ssh/id_deploy ./dist deploy@10.0.0.5:/srv/app/
scp -C -l 8000 backup.tar.gz deploy@10.0.0.5:/backup/
scp -p deploy@10.0.0.5:/var/log/app/error.log ./

sftp:交互式传文件

sftp 用户@主机 进入交互会话 —— 「一边 ls 一边传」比 scp 盲传更可控
-P 大写端口,-i 指定私钥,和 scp 一致
get 远端文件 下载;put 本地文件 上传;get -r 递归整目录
reget 断点续传下载, reput 对应上传 —— 这是 sftp 比 scp 强的关键一点
-p(get/put 的 -p)保留权限与时间戳
mget / mput 支持通配符批量取传,但默认每个文件都要确认,先执行 prompt off
ls / lls 看远端与本地,cd / lcd 分别切远端与本地目录(两个 l 开头的是本地)
!df -h 以 ! 开头执行本地 shell 命令,传大文件前先用它确认本地空间
bye / exit 退出;heredoc 写脚本可以免交互批量传

sftp -P 2222 -i ~/.ssh/id_deploy deploy@10.0.0.5 <<'EOF'
get -r /srv/app/logs ./
reget backup.tar.gz
put ./dist/app.tar.gz /srv/
EOF

rsync:增量同步与断点续传

-a 归档模式(等于 -rlptgoD),保留权限、时间、软链接、属组,日常首选
-v 显示过程;-z 传输压缩,跨公网必加
-P 等于 --partial --progress:保留半截文件 + 显示进度,这就是「断点续传」的开关
--partial 中断后保留已传部分,下次接着来;不加就每次从零开始
--append 只追加,适合只增不减的日志,不适合会改动的文件
--delete 让目标与源完全一致(会删掉目标多出来的文件)—— 先加 -n 演练
-n 与 --dry-run 同义:只打印将要做什么,不动文件
-e 'ssh -p 2222' 指定远程 shell 与端口,rsync 自己没有端口参数
--exclude 排除单条规则,--exclude-from 从文件读一批规则
--bwlimit=2000 限速,单位 KB/s;--checksum 按内容而非大小时间判断差异
--info=progress2 一行显示总进度,比 -P 的逐文件进度更适合大目录
【最大的坑】源路径末尾带 / 表示复制目录内容,不带 / 表示复制目录本身

rsync -avzP ./dist/ deploy@10.0.0.5:/srv/app/dist/
rsync -avzP -e 'ssh -p 2222' deploy@10.0.0.5:/var/log/app/ ./logs/
rsync -avn --delete /srv/data/ /backup/data/          # 演练,看清楚再真跑
rsync -avz --bwlimit=2000 --exclude='*.tmp' ./ /mnt/nas/app/

wget:下载与断点续传

wget URL 直接下到当前目录;-O 指定输出文件名,-O - 输出到标准输出
-c 断点续传(curl 要写 -C -),大文件挂了重跑同一条命令就继续
-q 安静模式;--progress=dot:giga 进度更省屏
--limit-rate=1m 限速,支持 k/m 后缀;--tries=3 重试次数
-T 30 连接超时秒数,避免卡住;--timeout 是它的长写法
-r 递归下载整个站点,-np 不向父目录爬(防止一不小心下整个域名)
--no-check-certificate 跳过证书校验,只在内部站点诊断时用,别写进脚本
需要自定义请求头、Cookie、登录态时 wget 不如 curl 灵活

wget -c https://example.com/big.iso
wget -O - https://example.com/install.sh | head -40   # 先看内容再决定要不要跑
wget --limit-rate=1m --tries=3 -c -O app.tar.gz https://example.com/app.tar.gz

🔐 权限与用户 (6)

数字位、特殊位、sudo —— 权限配错的典型症状是「明明有密码却进不去」和「服务起不来」

chmod:数字位与符号位

数字位:4 读 / 2 写 / 1 执行,三位依次是 属主 / 组 / 其他
755 目录常用(rwxr-xr-x);644 文件常用(rw-r--r--)
600 只有属主可读写,私钥必须是这个
符号位:u/g/o/a 是对象,+/-/= 是动作,X 是「仅目录加执行位」
-R 递归;对目录递归加 x 时用 a+X 更安全,不会给普通文件加执行位
go-w 表示去掉组和其他人的写权限,配合 a+X 一次改完两类
递归改完先 ls -l 抽查一个文件一个目录,权限错多半是递归改过头

chmod 600 ~/.ssh/id_rsa
chmod -R a+X,go-w /srv/app/public
ls -l /srv/app/public | head    # 递归改完抽查一眼

chown:改属主与属组

chown 用户:组 路径,冒号也可写成点号
只改组用 chown :组 或 chgrp
-R 递归;配 -h 只改链接本身而不改它指向的目标
--reference=源文件 把属主和权限照抄一份,批量同步时很好用
root 才能改属主;普通用户只能改自己文件的属组(且必须在目标组里)
容器挂载卷出现 Permission denied,先核对宿主机目录的 uid 是否等于容器内用户

chown -R www-data:www-data /var/www/app
chown --reference=/srv/app/a /srv/app/b

SUID / SGID / Sticky:三个特殊位

4000 SUID:执行时以文件属主身份运行(/usr/bin/passwd 就是)
2000 SGID:文件以属组身份运行;目录上则新建文件继承该目录的组
1000 Sticky:目录里的文件只有属主能删(/tmp 就是这个)
ls 里出现 s 或 t 就是设了对应位;大写 S/T 表示「设了但缺少 x」
-perm -4000 表示「权限位包含 SUID」,-perm 是 find 的权限匹配条件
-xdev 不跨文件系统 —— 不加会顺着 NFS、容器挂载点一路查下去,慢到怀疑人生
-ls 把命中的文件按 ls -dils 输出,大小与时间一起看到
-type f 只找普通文件,排除目录和软链接
排查提权时先找 SUID:find / -xdev -perm -4000 -type f

# 审计:列出所有 SUID 程序
find / -xdev -perm -4000 -type f -ls 2>/dev/null

# 共享目录:组内协作 + 只有本人能删
chmod 2770 /srv/share

umask:新文件的默认权限

umask 是「要减掉的位」,不是权限本身
022 → 新建目录 755、新建文件 644(文件本来就不带执行位)
077 → 新建目录 700、新建文件 600,适合放密钥的目录
-S 用符号形式(rwxr-xr-x)显示实际默认权限,比看数字直观
临时改:umask 077 只对当前 shell 生效
永久改:写进 /etc/profile 或用户的 ~/.bashrc
排查「为什么新建的密钥别人能读」先看 umask

umask            # 看当前值
umask 077        # 当前会话生效
umask -S         # 用符号形式显示实际默认权限

sudo / su:提权的正确姿势

sudo -i 拿到一个全新的 root 登录 shell(会读 root 的环境变量)
sudo -s 用当前用户的环境变量起 root shell,PATH 不变
sudo -u 用户 以指定用户身份执行,跑服务时常用
sudo -l 看自己被授予了哪些命令,排查「为什么不许我执行」
su - 切到 root 并加载其环境;少了那个减号会带着当前环境,PATH 可能不对
visudo 改 sudoers,它有语法检查;-c 只做语法检查不保存,改完先跑一遍
直接编辑 /etc/sudoers 写错会锁死自己,这是运维里最经典的翻车点之一

sudo -l
sudo -u www-data /usr/bin/env      # 看服务账号的环境
visudo -c                          # 只做语法检查

useradd / usermod / id

useradd -m 建家目录;-s 指定 shell;-G 追加到附加组
没有 -m 时家目录不存在,登录后很多工具会莫名其妙失败
usermod -aG 组 用户 —— 少了 -a 会把用户从其他附加组里踢掉(-a 是 append)
-G 是附加组列表,可以和 -a 连写为 -aG
id 用户 看 uid/gid 和全部组;groups 用户 只看组
userdel -r 连家目录一起删;不加 -r 会留下孤儿文件
改完组之后,已登录的会话不会自动生效,必须重新登录

useradd -m -s /bin/bash deploy
usermod -aG docker deploy
id deploy

⚙️ 进程与资源 (8)

谁在占 CPU、谁握着文件、怎么让它优雅退出 —— 这里错误的操作通常是 kill -9 用太早

ps:两种风格别混用

ps aux 是 BSD 风格:%CPU、%MEM、STAT、COMMAND,日常最常用
ps -ef 是 System V 风格:UID、PID、PPID、C、STIME、CMD,能看父进程
-e 显示所有进程(System V 风格),-o 自定义输出列
混着写 ps -aux 也能跑,但含义会变,别当成习惯
--sort=-%cpu 按 CPU 倒序,负号是降序,逗号可分隔多级排序
--sort=-rss 按实际内存倒序;STAT 里 Z 是僵尸、D 是不可中断、R 是运行中
RSS 是真实占用,VSZ 是虚拟地址空间,基本没有参考价值

ps aux --sort=-%cpu | head
ps -eo pid,ppid,rss,pcpu,cmd --sort=-rss | head

top / htop:交互键比参数有用

top 里按 P 按 CPU 排、M 按内存排、T 按累计时间排
1 展开每个 CPU 核心,判断是单核打满还是整体高
c 显示完整命令行,默认会被截断
H 显示线程;u 只看某个用户;k 杀进程
-b 批处理模式,-n1 只输出一次,适合塞进脚本或日志(-n 是次数)
-p 只看指定 PID,多个用逗号分隔;pgrep 的 -d, 就是让多个 PID 用逗号连接
第一行 load average 三个数分别是 1/5/15 分钟均值,超过核数就说明在排队

top -b -n1 | head -15          # 取一次快照
top -p $(pgrep -d, nginx)      # 只看指定进程

kill:先礼后兵

默认 kill 发 TERM(15),进程可以收尾、写完文件再退出
-15 与 -TERM 等价,写作 -TERM 更可读;日常都靠它先礼
-9 是 KILL,内核直接收回,进程没有机会清理 —— 数据库、消息队列可能留下损坏数据
-1 或 -HUP 常被用来「重载配置」(nginx、sshd 都支持)
-0 不发信号只检查进程是否存在,返回码能直接用于判断
kill 接受的是 PID,不是进程名;按名字用 pkill 或 killall
pkill -HUP nginx 就是按名字给全部 nginx 进程发 HUP
先 TERM 等几秒,还在再 KILL,这条顺序在运维规范里几乎每次都写

kill -TERM 1234 && sleep 3
kill -0 1234 && kill -9 1234
pkill -HUP nginx               # 重载配置

nohup / setsid:让它别跟着终端死

& 只是放后台,终端一关就收到 SIGHUP 一起退出
nohup 忽略 SIGHUP,输出默认进 nohup.out(建议显式重定向)
2>&1 把 stderr 并到 stdout,否则错误信息不知跑哪去了
< /dev/null 把标准输入接到空设备,防止后台进程因读终端而被挂起
setsid 让它脱离当前会话,比 nohup 更彻底
disown 把已在后台的作业从 shell 作业表摘掉
真正的长期服务应该用 systemd,nohup 只适合临时验证

nohup ./run.sh > /var/log/run.log 2>&1 &
setsid ./worker.sh < /dev/null > /var/log/worker.log 2>&1 &

lsof:谁占着这个文件/端口

lsof -i :8080 看谁占了 8080 端口(-i 表示按网络地址过滤)
-p PID 只看某进程打开的全部文件
-u 用户 按用户过滤;-n 不解析主机名,输出更快也不卡 DNS
+D /path 递归看某目录被哪些进程打开(很慢,慎用大目录)
+L1 只列「链接数小于 1」即已被删除但还开着 —— 磁盘没释放的元凶
后面接 sort -k7 -n 是排序技巧:-k7 取第 7 列(SIZE/OFF),-n 按数值比较
没有 lsof 时用 fuser -v 或 /proc/PID/fd 代替

lsof -i :8080
lsof -n +L1 | sort -k7 -n | tail    # 按大小看被占用的已删文件

nice / renice / taskset:让出 CPU

nice 值范围 -20(最高优先)到 19(最低),普通用户只能调高不能调低
nice -n 19 命令 启动时就降优先级,适合备份、压缩这类批处理
-n 后面跟的就是 nice 值,不是「次数」
renice -n 10 -p PID 改正在跑的进程;-p 指定 PID,-u 按用户批量改
taskset -c 0-3 绑核:-c 指定 CPU 列表,能缓解多实例抢同一核心
taskset -cp 0-3 1234 里的 -p 表示「操作已有进程」,后面跟 PID
降优先级不能解决资源不足,只是让关键业务少受干扰
示例里 tar -czf 的 -c 创建、-z gzip、-f 指定文件名(见 tar 条目)

nice -n 19 tar -czf backup.tar.gz /srv/data
renice -n 10 -p 1234
taskset -cp 0-3 1234

watch:盯着变化,而不是看一眼

watch 默认每 2 秒重跑一次命令并清屏,看的是「变化」而不是一次快照
-n 1 改成每 1 秒刷新;-n 后面跟秒数(可以写 0.5)
-d 高亮两次输出之间变化的部分,一眼看出哪里在动
-d=cumulative 累积高亮,变化过的位置一直留着标记 —— 排查偶发波动有用
-t 不显示标题栏(顶部那行命令和间隔),输出更干净
命令必须用单引号包住,否则管道会被当前 shell 先拆掉,watch 只拿到前半截
不要用它监控高频指标(连接数、QPS),该用 vmstat 1 / sar,watch 会漏掉中间过程
示例里 ss -s 是连接汇总、wc -l 是数行数(-l 就是数行),详见各自条目

watch -n 1 -d 'ss -tn state established | wc -l'
watch -d=cumulative -t 'ss -s'

screen / tmux:断网也别让任务死

screen -S deploy 起一个命名会话;关掉终端、断网,任务都还在跑
-list 列出所有会话(也可写 -ls),先看有没有 Attached 状态的旧会话
-r deploy 重新接回指定会话;报 Attached 说明别处还挂着,用 -d -r 强制接管
Ctrl-a d 是 screen 的脱离键;Ctrl-a " 列出窗口;Ctrl-a k 杀掉当前窗口
tmux new -s deploy 新建,tmux attach -t deploy 接回(-t 指定名字)
tmux 里 Ctrl-b d 脱离,Ctrl-b [ 进入翻页模式,能往回滚很久的输出
跑迁移、备份、大文件传输前先起会话 —— 断网不等于任务停了,但你看不到输出了

screen -S deploy
# 之后按 Ctrl-a d 脱离
screen -list
screen -r deploy
tmux new -s deploy

🌐 网络与端口 (8)

服务起不来时,先分清是「没监听」「没放行」还是「连不上」—— 这三步的顺序错了会白查很久

ss:谁在监听哪个端口

-t TCP;-u UDP;-l 只看监听;-n 不解析服务名(纯数字,更快);-p 显示进程
-tulnp 是日常最常用的一串,五个字母各管一件事
-tnp 少了 l 就是「连已建立连接的进程一起看」,排查连接数用它
-a 连已建立的连接一起列;-s 汇总统计,连接数暴涨时先看它
state established 按状态过滤,比 grep ESTAB 准(ESTAB 只是列的缩写)
netstat 已被废弃,新系统上大概率没装,直接用 ss
监听在 127.0.0.1 和 0.0.0.0 是两回事:前者只有本机能连,这是「端口开了但外部连不上」的头号原因

ss -tulnp
ss -tnp state established | head
ss -s

curl:排查接口的瑞士军刀

-s 静音:不显示进度条和错误信息,脚本里几乎必加
-S 配合 -s 用,把错误重新显示出来 —— 所以 -sS 的含义是「安静,但出错要吭声」
-i 连响应头一起输出;-I 只要头(发 HEAD,不看 body)
-v 打印完整握手与请求过程,卡在哪一步一目了然
-L 跟随重定向;排查 301/302 链必须加
-X 指定方法;-d 发 body(默认 POST);-H 加请求头
-o /dev/null 丢掉 body,配合 -w 只输出状态码和耗时
-w '%{http_code} %{time_total}' 自定义输出,比肉眼看快
-m 10 整个请求最多 10 秒;--connect-timeout 3 只管连接阶段
--resolve 域名:443:IP 直接绑定 IP 绕过 DNS,判断是不是解析问题
-x http://proxy:8080 走代理;--noproxy '*' 强制直连(本机排查几乎都要加)
-k 跳过证书校验 —— 只在诊断时用,别写进脚本
-w 里的 \n 是换行,不加就全挤在一行

curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://example.com/
curl -m 10 --connect-timeout 3 -sS -o /dev/null \
  -w 'dns:%{time_namelookup} conn:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n' \
  https://example.com/api
curl -iL --resolve example.com:443:1.2.3.4 https://example.com/
curl -v -X POST -H 'Content-Type: application/json' -d '{"a":1}' http://127.0.0.1:8080/api

测端口:nc / telnet / /dev/tcp

nc -zv 主机 端口 —— -z 只探测不发数据,-v 显示结果,最快
-w3 连接超时 3 秒,避免命令卡住(不写会等很久)
telnet 主机 端口 老系统上一定有,看到 Connected 就是通
bash 内建 /dev/tcp 不需要装任何东西:echo > /dev/tcp/主机/端口
返回 refused 说明能到主机但没人监听;超时说明被防火墙或安全组拦了 —— 这两个错误含义完全不同
跨机器排查永远按「本机 ss → 本机 curl 127.0.0.1 → 同网段 nc → 外网 nc」逐层收敛

nc -zv -w3 10.0.0.5 5432
echo > /dev/tcp/10.0.0.5/5432 && echo open || echo closed

ip:地址、路由、邻居

ip a 看全部网卡地址(等价于旧的 ifconfig);-4 只看 IPv4,-6 只看 IPv6
ip r 看路由表,default 那行就是默认网关
ip neigh 看 ARP 邻居表,同网段不通时看这里有没有条目
ip -s link 看收发包与错误计数,-s 是 statistics;errors/dropped 不为 0 就是链路层问题
show eth0 限定只显示某块网卡,输出不会被一堆虚拟网卡淹没
add / del 临时加删地址,重启即失效
永久配置看发行版:Debian 系 netplan 或 /etc/network/interfaces,RHEL 系 nmcli

ip -4 a
ip r
ip -s link show eth0

dig:DNS 到底解析成了什么

+short 只输出结果,适合脚本
+A / +AAAA / +MX 指定记录类型(不写默认查 A)
@8.8.8.8 指定 DNS 服务器,用来对比「本地解析 vs 公共解析」
+trace 从根开始逐级追踪,定位是哪一层委派错了
查不到时先看 status:NXDOMAIN 是记录不存在,SERVFAIL 是服务器有问题
TTL 决定改记录后要等多久;ANSWER SECTION 里的 TTL 剩多少就代表缓存还剩多久
dig 只看 DNS 层。域名解析对但访问不通,问题在解析之后的网络或端口

dig +short example.com
dig @8.8.8.8 example.com A
dig +trace example.com

tcpdump:抓包确认谁不发 ACK

-i any 所有网卡(也可以写具体网卡名);-n 不解析域名和端口名,快且不会被 DNS 拖住
-nn 表示「IP 和端口都用数字」,排查时比 -n 更清楚
port 443 只抓该端口;host 1.2.3.4 只抓该主机;两者可 and / or 组合
-c 100 抓够 100 个包就停,避免刷屏
-w file.pcap 存下来用 Wireshark 看,比终端里读强得多
-A 按 ASCII 看报文内容,-X 按十六进制 + ASCII(明文协议才有意义)
tcp[tcpflags] & tcp-rst != 0 只抓 RST 包,判断是「谁先断开」
需要 root;生产环境务必加 -c 和过滤条件

tcpdump -i any -nn -c 50 'port 443 and host 1.2.3.4'
tcpdump -i eth0 -nn -w /tmp/cap.pcap 'tcp[tcpflags] & tcp-rst != 0'

openssl s_client:证书到期与证书链

-connect 域名:443 连上去看对方证书;不看 HTTP,只看 TLS 层
-servername 域名 必须写(SNI),否则同一 IP 上的多域名会拿到错误的证书
-showcerts 打印完整证书链,能看出是不是缺了中间证书
-noout 不输出证书原文,只输出你后面要求的字段
-dates 看 notBefore / notAfter,到期排查的第一步
-subject 看证书是签给谁的;-ext subjectAltName 看覆盖了哪些域名
-checkend 2592000 检查 30 天(秒)内是否过期,返回码可直接用于监控
</dev/null 让 s_client 读完就退出,否则会停在那里等输入(不加就一直挂着)

openssl s_client -connect www.icetools.cn:443 -servername www.icetools.cn </dev/null 2>/dev/null \
  | openssl x509 -noout -dates -subject
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null | head -30
openssl s_client -connect example.com:443 -servername example.com -checkend 2592000 </dev/null >/dev/null \
  && echo '30 天内不过期'

防火墙:放行前先确认它在拦

firewall-cmd --list-all 看当前 zone 放开了什么(RHEL 系)
--add-port=8080/tcp 加规则;--permanent 才是永久生效,否则重启即丢
--reload 重载永久规则,加完必须执行,否则规则还没进运行态
ufw status verbose 看规则(Ubuntu)
iptables -L 列出过滤表;-n 不解析名字,-v 显示包计数(能证明规则是否命中)
--line-numbers 显示规则序号,删规则时要按它写 iptables -D INPUT 3
-I INPUT 1 插到最前面,-A 追加到末尾 —— 顺序决定结果
规则放行后仍不通,先怀疑云厂商的安全组,它在这台机器之外

firewall-cmd --list-all
firewall-cmd --add-port=8080/tcp --permanent && firewall-cmd --reload
iptables -L INPUT -n -v --line-numbers

💾 磁盘与文件系统 (5)

「磁盘满了」有四种完全不同的成因:文件多、单文件大、inode 耗尽、已删文件被占用

磁盘满的四步排查

第一步 df -h 确认是哪个挂载点满,别急着重启服务
第二步 df -i 看 inode。inode 满了会报「设备上没有空间」,但 df -h 显示还有余量
第三步 du -h --max-depth=1 逐层下钻找大目录(--max-depth 是层数)
第四步两者数字对不上,用 lsof +L1 找被进程占用的已删文件,重启或 kill 该进程才释放
awk 那句把第 7 列(字节数)累加再换算成 MB,比肉眼估快
日志暴涨通常是第一步就发现的,别急着删,先看是不是某个错误在刷屏

df -h; df -i
du -h --max-depth=1 / | sort -h | tail
lsof +L1 2>/dev/null | awk '{s+=$7} END {print s/1024/1024" MB held"}'

lsblk:先看清盘的结构

默认输出树状结构,能看出 LVM、分区、挂载点的从属关系
-f 显示文件系统类型、UUID、挂载点 —— 写 fstab 前必看
-p 显示完整设备路径,避免 /dev/sda 和 /dev/sda1 搞混
-d 不展开子设备,只看整盘
-o 自定义输出列,NAME,SIZE,FSTYPE,MOUNTPOINT,UUID 是最实用的一组
扩容后如果 lsblk 显示新容量而 df 没变,是文件系统还没 resize

lsblk -f
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,UUID

扩容:分区改了文件系统也要改

云盘扩容后是两步:先扩分区,再扩文件系统,少一步 df 就不会变
growpart /dev/vda 1 扩分区(cloud-utils-growpart 包),第二个参数是分区号
xfs_growfs / 扩 XFS(只能扩大,不能缩小)
resize2fs /dev/vda1 扩 ext4(可扩可缩,缩之前必须先 umount 并检查)
LVM 还要多一步 lvextend,再 resize 文件系统
最后用 df -h 确认容量真的变了(-h 是 human-readable)
操作前先做快照,resize 中途断电有丢数据的可能

growpart /dev/vda 1
xfs_growfs /            # XFS
resize2fs /dev/vda1     # ext4
df -h /

mount / umount / fstab

mount -o 常用选项:ro 只读、remount 重挂、noatime 减少写放大
remount 是最常用的:改完挂载参数不用卸载,业务不中断
umount 报 device is busy,用 lsof +D 或 fuser -vm 找占用者
fstab 里建议用 UUID 而不是 /dev/sdb,设备名可能变
fstab 第四列写 defaults,noatime;第五六列是 dump 和 fsck 顺序,普通数据盘写 0 0
改完 fstab 必须 mount -a 验证;写错会导致下次开机进不了系统
mount -a 是按 fstab 把没挂的都挂上,出错会直接报哪一行有问题

mount -o remount,rw /
fuser -vm /mnt/data
mount -a                      # 验证 fstab 写法

inode 耗尽:文件很小也存不进去

每个文件占一个 inode,inode 总数在格式化时就定了
症状是 df -h 有空间但写文件报 No space left on device
df -i 看 IUse% ,接近 100% 就是它
-xdev 不跨文件系统,避免统计跑到别的挂载点上
-type f 只数文件,不管目录(目录也占 inode,但先看文件分布更有效)
sed 把路径的最后一个 / 之后的内容去掉,得到目录名;sort | uniq -c 统计每个目录多少个文件
常见元凶:session 文件、上传的临时小文件、mail spool、几十万个小文件的缓存目录
根治要重新格式化并调大 -i 参数,短期只能清理小文件
最后的 sort -rn 是「按数值倒序」:-r 倒序、-n 数值比较

df -i
find /var -xdev -type f | sed 's#/[^/]*$##' | sort | uniq -c | sort -rn | head

⏰ 定时任务与时间 (4)

cron 跑不起来八成是 PATH 和输出重定向;时间不准则会连带证书、JWT、日志一起出问题

crontab:五个时间字段

crontab -l 列出当前用户的定时任务;-e 编辑;-r 删除全部(没有二次确认,手别滑)
-u 用户 操作指定用户的 crontab(要用 root 执行)
五个字段依次是 分 时 日 月 周:0 3 * * * 表示每天 3:00
*/5 每 5 分钟;1-5 周一到周五;0,12 表示只在 0 点和 12 点
cron 的环境和登录 shell 完全不同,PATH 极短 —— 脚本里一律写绝对路径
cron 不读 ~/.bashrc,需要环境变量就在 crontab 顶部写,或在脚本里 source
输出必须重定向:>> /var/log/x.log 2>&1,否则出错信息只会进邮件,你永远看不到
查为什么没跑:Debian 系 journalctl -u cron,RHEL 系 /var/log/cron
示例里 curl -sS 是静音但出错要吭声(-s 静音、-S 显示错误),-o /dev/null 丢掉 body

crontab -e
crontab -l
# 每天 3:30 备份,输出进日志
30 3 * * * /srv/app/backup.sh >> /var/log/backup.log 2>&1
# 每 5 分钟探一次健康检查
*/5 * * * * /usr/bin/curl -sS -o /dev/null http://127.0.0.1:8080/health

systemd timer:比 cron 更好查

优点很实在:日志进 journalctl、能看下次触发时间、错过能补跑
OnCalendar 写日历时间:*-*-* 03:30:00 每天 3:30;OnUnitActiveSec=5min 每 5 分钟
Persistent=true 关机期间错过的任务,开机后补跑一次 —— cron 做不到这点
Unit= 指定要跑的 service,省略则用同名 .service
systemctl list-timers 看全部定时器和下次触发时间
enable --now 才是「开机自启 + 立刻生效」,只 enable 不会马上跑
测试时 start 的是 .service 不是 .timer,直接 start timer 不会立即执行任务
最后一行 journalctl -u 指定 unit、-n 30 取最近 30 行、--no-pager 不分页

# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target

# 启用与检查
systemctl list-timers
systemctl enable --now backup.timer
journalctl -u backup -n 30 --no-pager

date / timedatectl:时间与时区

date 看当前时间与时区;+%F 是 2026-10-29,+%T 是时分秒,+'%F %T' 最常用
-d '-1 hour' 算一小时前,-d 'next monday' 也行 —— 脚本算时间窗口全靠 -d
-s 直接改系统时间;生产上不要手动改,会打乱日志顺序和证书校验
timedatectl 看时区与 NTP 同步状态,比 date 信息全
set-timezone Asia/Shanghai 改时区(国内机器常见问题就是 UTC 没改)
set-ntp true 打开自动对时;容器里时间是继承宿主机的
时间不准的后果很具体:证书报 not yet valid、JWT 立刻过期、日志时间对不上、定时任务跑错点

date '+%F %T'
date -d '-1 hour' '+%F %T'
timedatectl
timedatectl set-timezone Asia/Shanghai

chronyc / ntpq:对时是否正常

chronyc tracking 看当前偏差与层级,System time 一行就是「和真实时间差多少」
偏差几百毫秒以上就要查:源不可达、被防火墙拦了 UDP 123、虚拟机时钟漂移
chronyc sources -v 看每个时间源,-v 显示详细参数(延迟、抖动、是否被选中)
行首 ^* 才是正在用的源,^? 表示不可达
chronyc makestep 立即跳一次时间,只在偏差过大时手动执行,会自动写日志
ntpq -p 是老系统 ntpd 的等价命令,-p 打印对等体列表
云服务器优先用厂商内网 NTP,公网 NTP 常被限速甚至拒绝

chronyc tracking
chronyc sources -v
chronyc makestep
ntpq -p          # 老系统(ntpd)

📋 日志与故障定位 (5)

定位的顺序比命令更重要:先按时间框定,再按关键字收敛,最后看上下文

journalctl:systemd 的统一日志

-u 服务名 只看某个 unit
-f 持续跟踪;--since '10 min ago' 或 --since '2026-10-29 09:00' 按时间框定范围
-p err 只看 error 及以上;优先级从 emerg 到 debug
-n 100 最后 100 行;-r 倒序(最新在上)
--no-pager 不分页,方便重定向到文件或接管道
-o short-iso 时间戳带时区偏移,跨时区排查时必加
--disk-usage 看日志占了多少磁盘;--vacuum-time=7d 只保留 7 天
--vacuum-size=500M 是按体积清理,和 --vacuum-time 二选一

journalctl -u nginx -p err --since '30 min ago' --no-pager
journalctl -u app -f -o short-iso
journalctl --disk-usage

tail -f 与 -F 的区别

-f 按文件描述符跟,日志被轮转(rename)后会继续跟旧文件,看起来「日志不更新了」
-F 按文件名跟,轮转后自动切到新文件 —— 长期盯着日志必须用这个
-n 100 先显示最后 100 行再跟;-n0 表示一行历史也不看,只跟新内容
-Fn0 就是把 -F 和 -n0 连写,看实时流最常用的组合
--pid=PID 进程退出时自动停止跟随
grep --line-buffered 让 grep 每行立即输出,不加会缓冲,管道里半天看不到东西
配合起来:tail -Fn0 app.log | grep --line-buffered -i error
多文件:tail -F a.log b.log 会带 ==> 分隔,别只跟一个

tail -F /var/log/app/error.log
tail -Fn0 /var/log/nginx/access.log | grep --line-buffered -i 500

dmesg:内核说了什么

OOM、磁盘 IO 错误、网卡 link down、驱动异常都在这里
-T 显示人类可读时间;-w 持续跟踪
-l err,warn 只看指定级别(-l 后面跟级别列表,逗号分隔)
dmesg 环形缓冲区会被刷掉,重要信息要及时取;journalctl -k 是同一份数据的持久化版本
看到「Out of memory: Killed process」就是被 OOM killer 杀了,不是程序自己崩的
接 grep -iE 时:-i 忽略大小写,-E 用扩展正则(| 才生效,详见 grep 条目)

dmesg -T -l err,warn | tail -30
dmesg -T | grep -iE 'oom|killed process'

OOM:为什么进程突然没了

确认:dmesg -T | grep -i -A2 'killed process',-A2 是把匹配行后面 2 行也带出来(被杀进程的内存信息在那)
journalctl -k 里信息更完整,含当时的内存状态
systemctl status 显示 status=9/KILL 或 signal=KILL 时,基本就是被 OOM 杀的
--no-pager 避免输出被分页器接管,脚本或截图里都需要
不是程序崩溃:程序日志里会戛然而止,没有异常堆栈
找原因:free -h 看可用内存(-h 人类可读),再按 RSS 排序看谁在涨
容器里还要看 cgroup 限制:/sys/fs/cgroup/memory.max

dmesg -T | grep -i -A2 'killed process'
systemctl status app --no-pager | head -20
free -h

logrotate:别让日志吃满盘

配置放 /etc/logrotate.d/,每个服务一个文件
daily / weekly 轮转周期;rotate 7 保留 7 份
compress 压缩旧日志,delaycompress 让最新一份先不压(避免正在写的文件被压)
copytruncate 复制后清空,适合不能重开文件的程序;但它有丢日志的窗口
postrotate 里发信号让进程重开文件,比 copytruncate 更干净
-d 只演练不执行,改完务必先跑一次看它打算做什么
-f 强制执行一次,用来立刻验证配置生效(不等轮转周期)
missingok 文件不存在不报错,notifempty 空文件不轮转 —— 这两个几乎每个配置都该有

# /etc/logrotate.d/app
/var/log/app/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    postrotate
        systemctl reload app > /dev/null 2>&1 || true
    endscript
}

# 先演练,再强制跑一次验证
logrotate -d /etc/logrotate.d/app
logrotate -f /etc/logrotate.d/app

🚀 systemd 服务 (5)

服务起不来的绝大多数原因就三种:路径不对、用户没权限、依赖没起来 —— status 里都写着

systemctl:日常就这几个

start / stop / restart / reload —— reload 是重读配置不重启进程,程序支持才有意义
status 会给出最近几行日志和退出码,比猜有用;--no-pager 避免被分页器接管
enable 开机自启;--now 表示同时立刻启动
disable 取消自启但不停止;要停得单独 stop
list-units --failed 一眼看出哪些挂了;--all 连没启动的也列
is-active / is-enabled 返回码能直接用在脚本判断里
cat 打印 systemd 实际读到的那份 unit(含 drop-in 覆盖),改完先看它

systemctl status app --no-pager
systemctl enable --now app
systemctl list-units --failed

写一个可用的 unit 文件

放 /etc/systemd/system/名字.service,写完必须 daemon-reload
ExecStart 必须用绝对路径,且程序不能自行 fork(否则要 Type=forking)
Restart=on-failure 是重启策略,配 RestartSec=5 避免狂重启
User / Group 决定权限,改成非 root 时家目录和文件属主也要跟着改
EnvironmentFile 把环境变量放独立文件,别写死在 ExecStart 里
WorkingDirectory 影响相对路径,很多程序缺它会找不到自己的配置文件
StandardOutput=journal 让日志进 journalctl,方便统一排查
After 只排队不保证成功,网络就绪要配 Wants=network-online.target

# /etc/systemd/system/app.service
[Unit]
Description=App
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/srv/app
EnvironmentFile=/etc/app/env
ExecStart=/srv/app/bin/app
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

服务起不来,按这个顺序看

第一步 systemctl status 看 Active 和最后几行报错,多数问题到这里就清楚了
第二步 journalctl -u 服务 -n 50 --no-pager 看完整报错栈(-n 是行数)
第三步确认 ExecStart 路径存在且可执行:ls -l 那个路径
第四步手跑一遍:用 sudo -u 服务用户 执行 ExecStart 的命令,通常立刻报出真实原因
第五步看权限:SELinux 用 ausearch -m avc -ts recent(-m 指定记录类型 avc,-ts 指定起始时间)
AppArmor 的拒绝记录在 dmesg 里;容器里还要看 docker logs
status=203/EXEC 是路径或权限问题;status=1 是程序自己退出码 1

systemctl status app --no-pager
journalctl -u app -n 50 --no-pager
sudo -u deploy /srv/app/bin/app     # 手跑,看真实报错
ausearch -m avc -ts recent           # SELinux 拒绝记录

daemon-reload 什么时候必须跑

改了 .service 文件 → 必须 daemon-reload,否则 systemd 用的还是旧定义
只改 EnvironmentFile 里的内容 → 不用 reload,但必须 restart 服务才生效
新建 unit → reload 之后才能 enable
已经 start 的服务不会因为 reload 自动用新配置,得 restart
改完忘记 reload 的典型症状:改了 ExecStart 却毫无变化,status 里仍是旧命令
cat 用来确认 systemd 眼里这份文件长什么样(会显示 drop-in 覆盖后的结果)

systemctl daemon-reload && systemctl restart app
systemctl cat app            # 确认 systemd 眼里这份文件长什么样

依赖顺序:After 不等于 Requires

After 只管启动顺序,不保证对方成功启动
Requires 硬依赖,对方失败本服务也失败
Wants 软依赖,对方失败本服务照常启动 —— 多数场景用这个
网络相关:network.target 只表示网络栈初始化完,要等地址可用得用 network-online.target
After=network-online.target 必须配 Wants=network-online.target 才真的等
服务启动太早导致「地址解析失败」,十有八九是这个
验证依赖是否满足:systemctl list-dependencies 服务名,看树状关系

[Unit]
Wants=network-online.target
After=network-online.target

# 检查
systemctl list-dependencies app

📦 包管理 (4)

装之前先查来源,删之前先看会带走谁 —— 生产机上尤其如此

apt(Debian / Ubuntu)

apt update 只更新索引,不升级任何包 —— 这是最容易记反的
apt upgrade 按索引升级已装包;full-upgrade 会为满足依赖而卸载包,慎用
install -y 免交互确认,适合脚本;不加 -y 会停下来等输入
apt purge 连配置文件一起删;只 remove 会留下配置
apt list --installed 看已装;apt list --upgradable(--upgradable 是筛选条件)看可升级的
apt-cache show 包名 看版本和描述
apt-mark hold 锁住版本,防止被批量升级带走

apt update && apt install -y nginx
apt list --upgradable
apt purge nginx-common

dnf / yum(RHEL / CentOS)

dnf 是 yum 的继任者,新系统上 yum 通常是软链
install / remove / update 三个动词与 apt 对不上,别混着敲
install -y 免交互,和 apt 一致
provides 按文件或命令反查包名,找「这个命令是哪个包装的」
history 能看到每次事务,history undo last 回滚上一次 —— 升级搞坏了很救命
--enablerepo 临时启用某个源;--nogpgcheck 只在不该用的时候才用
module 机制(dnf module list)在同一软件多版本并存时需要指定

dnf install -y nginx
dnf provides /usr/sbin/ss
dnf history undo last

这个文件/命令属于哪个包

RHEL 系:rpm -qf /usr/bin/ss —— -q 是查询模式,-f 表示按文件反查包名
Debian 系:dpkg -S /usr/bin/ss;找不到时先 apt-file update 再用 apt-file search
不知道完整路径用命令名反查:dnf provides '*/ss' 或 apt-file search bin/ss
dpkg -L 包名 列出一个包装了哪些文件,改配置前很有用
rpm -qc 包名 只列配置文件(-qc 是 -q 加 -c(config))
找不到某个命令时先确认是不是根本没装,而不是 PATH 错了

rpm -qf /usr/bin/ss
dpkg -S /usr/bin/ss
dpkg -L nginx-common | head -20

清理磁盘上的包缓存

apt clean 清全部 .deb 缓存;apt autoclean 只清过期的
dnf clean all 清缓存与元数据
apt autoremove 删掉「当初作为依赖装、现在没人要」的包,加 --purge 连配置一起
autoremove 的 -y 仍然要加,否则会停在确认提示上
清理前先 du -sh 确认真的占地方(-s 汇总,-h 人类可读)
容器镜像里 apt install 后不 clean,镜像会白白大几十 MB

du -sh /var/cache/apt /var/cache/dnf 2>/dev/null
apt clean && apt autoremove --purge -y
dnf clean all

🐳 Docker 与容器 (8)

容器出问题九成是「你以为的配置」和「实际生效的配置」不一致 —— 先用 inspect 对齐事实,再看日志

docker ps:容器到底在不在、为什么一直重启

-a 连已退出的容器一起列出 —— 只看运行中的会漏掉刚崩掉的那个
-q 只输出容器 ID,方便接 docker rm 或 docker logs
-s 显示可写层占用,排查「容器把磁盘吃满」用
--format 自定义列,'{{.Names}} {{.Status}}' 比默认表格好读,也方便脚本处理
--filter 按条件过滤:status=exited 找退出的,name=web 按名字找
--no-trunc 不截断长 ID 和完整命令
-n 5 只显示最近创建的 5 个(--last 的简写)
Status 里出现 Up 3 seconds (restarting) 就是无限重启,去 docker logs --tail 看为什么

# 全部容器(含已退出)
docker ps -a --format '{{.Names}}  {{.Status}}'

# 找已退出/反复重启的
docker ps -a --filter status=exited -n 5

# 谁把磁盘吃满了
docker ps -s --format '{{.Names}}  {{.Size}}'

docker logs:崩溃现场都在这里

-f 持续跟踪,等价于 tail -f
--tail 200 只看最后 200 行 —— 容器跑久了日志几万行,不加会刷屏到看不出问题
-t 每行加时间戳,和宿主日志对时间线必需
--since 10m 只看最近 10 分钟;也接受 2026-10-29T10:00:00 这种绝对时间
--until 与 --since 配合截取一段时间
-i 用在 grep 上表示忽略大小写,error / ERROR / Error 一次抓全
日志落在 /var/lib/docker/containers/<ID>/<ID>-json.log,容器一删就没了 ——
重要服务要让它打进 journald 或外部收集,别只留在 docker 里

docker logs -f --tail 200 -t myapp
docker logs --since 10m myapp | grep -i error

docker exec:进容器看现场

-it 交互终端;缺 -t 没有提示符,缺 -i 无法输入,两个要一起给
-u root 用 root 进去 —— 镜像默认用户读不了配置文件时用
-w /app 指定工作目录,省得进去再 cd
-e KEY=val 临时注入环境变量,验证「是不是环境变量没传对」
-- 后面的参数不再被 docker 解析,命令自身带 - 开头时必须加
-c 用在 bash 上是把后面整串当命令执行,bash -c 'a && b' 一次跑多步
-l 用在 ls 上是长格式,看清权限和属主
进不去先看容器是不是 running:docker ps -a 里 Exited 的 exec 必然失败,
那不是 exec 的问题,是主进程已经退出了
镜像里没有 bash 就试 sh,alpine 基础镜像通常只有 sh
distroless 镜像连 sh 都没有,只能用 docker cp 拷文件出来,或临时起调试容器

docker exec -it -u root myapp sh
docker exec myapp env | grep -i database
docker exec -it myapp bash -c 'ls -l /app'

docker inspect:真正生效的配置

--format 用 Go 模板取字段,-f 是简写
常用路径:.State.Status 运行状态、.State.ExitCode 退出码、
.NetworkSettings.Networks 拿容器 IP、.Mounts 看挂载、.Config.Env 环境变量、
.HostConfig.RestartPolicy 重启策略
{{json .}} 输出原始 JSON,接 jq 更好处理
--type image 查镜像而不是容器
compose 里的值可能被命令行 -e 覆盖,只有 inspect 看到的才是真正生效的
.Config.Env 里常有密码和 token,贴给别人的时候先过滤掉

docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' myapp
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' myapp
docker inspect --format '{{json .Mounts}}' myapp | jq

docker stats:实时资源占用

--no-stream 只取一次快照就退出,适合脚本和定时记录
--format 自定义输出列,便于 grep 和入库
-a 连已停止的容器一起统计(它们占 0,但在列表里)
MEM LIMIT 是该容器的上限,MEM % 接近 100 说明马上要 OOM
CPU % 按核数折算,多核机器上单容器超过 100% 是正常的
容器被 OOMKilled 时 stats 什么也看不到(进程已经没了),要看 docker inspect 的 ExitCode 137,
以及 dmesg 里的 oom-kill 记录

docker stats --no-stream --format '{{.Name}} {{.CPUPerc}} {{.MemUsage}}'
docker stats --no-stream -a

docker build:构建与缓存

-t 打标签,规范写法是 名字:版本,别只用 latest
-f 指定 Dockerfile 路径(文件不在构建目录时)
--no-cache 完全不用缓存 —— 怀疑「改了代码镜像没变」时先用它排除
--build-arg 传构建参数,必须在 Dockerfile 里用 ARG 声明过才生效
--target 只构建到某个阶段,调试多阶段构建的中间层
--progress=plain 输出完整构建日志;默认的 TTY 模式会把并行日志折叠成一行,出错时看不到细节
最后那个 . 是构建上下文,不是 Dockerfile 路径 —— 上下文过大会明显变慢,用 .dockerignore 排除 node_modules 和 .git

docker build -t myapp:1.2.3 .
docker build -f docker/Dockerfile.prod --build-arg NODE_ENV=prod --progress=plain -t myapp:prod .

docker compose:一键起停与看日志

-f 指定 compose 文件;多环境用两个 -f 叠加,后者覆盖前者
-p 指定项目名 —— 默认取目录名,同机多套环境必须显式指定,否则容器和网络会互相覆盖
-d 后台运行;不加则前台附着,Ctrl+C 就停
--build 启动前先重建镜像
up 创建并启动;down 停止并删除容器和网络
logs -f --tail 100 跟踪日志;末尾写服务名就只跟那一个
config 校验语法并输出合并后的最终配置 —— 改完 compose 先跑这个,比直接 up 安全
compose v2 是 docker compose(空格),v1 是 docker-compose(连字符),参数有细微差别,脚本里先确认是哪个

docker compose -f docker-compose.yml -f docker-compose.prod.yml -p shop up -d --build
docker compose -p shop logs -f --tail 100 api
docker compose -p shop config

清理:磁盘被镜像和容器日志吃满

system df 按镜像 / 容器 / 卷 / 构建缓存四类汇总占用,-v 展开到每个对象
prune 删除未使用的对象;默认只删悬空镜像(<none>),加 -a 才删所有没被容器引用的镜像
--volumes 连数据卷一起清 —— 卷里可能躺着数据库,删了不可恢复
--filter until=24h 只删 24 小时前的,避免误删正在使用的
顺序建议:先 system df -v 看清是谁占的,再针对性 prune,别一上来就 system prune -a
容器日志也会吃满磁盘:/var/lib/docker/containers/*/*-json.log,
根治办法是在 daemon.json 里给 log-opts 配 max-size 和 max-file

docker system df -v
docker image prune -a --filter until=24h
docker system prune --volumes

☸️ Kubernetes 常用 (8)

K8s 排障的顺序几乎是固定的:看状态 → 看事件 → 看日志 → 进容器。跳过任何一步都是在猜

kubectl get:有哪些、在哪、什么状态

-A 所有命名空间 —— 找不到资源时先加它,多数情况是找错 ns 了
-n 指定命名空间;不写就用当前 context 的默认 ns
-o wide 多出 IP 和所在节点两列,排查网络与调度问题必看
-o yaml 输出完整定义,改配置前先导出来对照
--show-labels 显示标签 —— Service 选不到 Pod,多半是标签对不上
-l app=api 按标签筛选,比 grep 名字可靠
--sort-by=.status.startTime 按启动时间排,最新在最下面
资源名有简写:pod/po、svc、deploy、rs、no、cm、secret

kubectl get pod -A -o wide
kubectl get pod -n shop -l app=api --show-labels
kubectl get pod -A --sort-by=.status.startTime

kubectl describe:出问题时第一个该敲的

尾部 Events 是 K8s 排障信息量最大的地方 ——
ImagePullBackOff、FailedScheduling、Unhealthy、OOMKilled 的原因都写在这里
-n 不加而找错命名空间,得到的是 NotFound,而不是任何有用的提示
Events 默认只保留最近 1 小时,事后复盘要靠日志收集,别指望回头还能看到
describe node 底部的 Allocated resources 显示已分配的 request / limit 占比,
Pod 一直是 Pending 时,先看节点是不是被 request 占满了(而不是 CPU 真的用满了)
describe 只看配置和事件,不显示应用日志,那是 kubectl logs 的事

kubectl describe pod -n shop api-7d9f-abc12
kubectl describe node k8s-node-2 | tail -30

kubectl logs:当前日志与崩溃前的日志

-f 持续跟踪输出
-n 指定命名空间,不写就用当前 context 的默认 ns
--tail 200 最后 200 行,Pod 跑久了不加会刷屏
-c 指定容器 —— 一个 Pod 里有 sidecar 时必须指定,否则提示 ambiguous 并列出可用容器
--previous 看上一个容器的日志:CrashLoopBackOff 会把容器反复重启,
当前容器的日志是「刚启动就崩」的那一段,真正死因在上一个容器里
--since 10m 只看最近 10 分钟
--timestamps 每行加时间戳
资源可以写 deploy/api 这种「类型/名字」形式,直接看某个 Deployment 的 Pod 日志

kubectl logs -f --tail 200 -n shop deploy/api
kubectl logs -n shop api-7d9f-abc12 --previous
kubectl logs -n shop api-7d9f-abc12 -c istio-proxy --since 10m

kubectl exec / cp:进容器与拷文件

-it 交互终端,两个要一起给
-n 指定命名空间
-c 指定容器,多容器 Pod 必需
-- 后面的不再被 kubectl 解析,命令自身带 - 开头时必须加
cp 的格式是 源 目标,Pod 侧写成 pod:路径
cp 依赖容器里有 tar,distroless 镜像会直接失败
kubectl debug 是临时注入一个调试容器并共享网络命名空间,
--image 指定调试镜像(busybox 够用)、--target 指定要调试的那个容器 ——
生产镜像没 shell 时这是唯一正道,比改镜像重新发布快得多

kubectl exec -it -n shop api-7d9f-abc12 -- sh
kubectl cp -n shop api-7d9f-abc12:/app/logs/app.log ./app.log
kubectl debug -it -n shop api-7d9f-abc12 --image=busybox --target=api

kubectl get events:集群级事件

和 describe 尾部的 Events 同源,但能跨资源一次看全,适合「不知道是哪个对象出问题」
-n 指定命名空间
--sort-by=.lastTimestamp 按时间排,最新在最后
--field-selector type=Warning 只看告警,噪声立刻少一个量级
--field-selector involvedObject.name=api-7d9f-abc12 定位到具体对象
-A 所有命名空间,集群级问题(节点 NotReady、驱逐)只有加了才看得到
Warning 里的 FailedScheduling 带的是调度器原话,例如
insufficient cpu、node(s) had taint —— 直接告诉你为什么调度不上,
不用再去猜资源还是污点问题
事件默认 TTL 约 1 小时,Pod 反复重启时要及时抄下来

kubectl get events -A --sort-by=.lastTimestamp | tail -20
kubectl get events -n shop --field-selector type=Warning
kubectl get events -n shop --field-selector involvedObject.name=api-7d9f-abc12

kubectl apply:部署、回滚与重启

-f 指定 manifest 文件或目录;-f - 从标准输入读
--dry-run=client 不提交,只把最终 YAML 打出来 —— 用来生成模板,省得手写
--dry-run=server 提交到服务端校验(含准入 webhook),比 client 严格得多
-o yaml 配合 dry-run 才有输出意义
rollout status 等滚动更新完成,卡住时它会告诉你卡在哪一步
rollout undo 回滚到上一版;--to-revision=3 回到指定版本,配合 history 查版本号
rollout restart 只重启 Pod 不改配置,改完 ConfigMap 或 Secret 后常用
-n 指定命名空间
apply 是声明式,重复执行幂等;create 是命令式,第二次会报 AlreadyExists

kubectl apply -f deploy/api.yaml --dry-run=client -o yaml > api.gen.yaml
kubectl rollout status deploy/api -n shop
kubectl rollout undo deploy/api -n shop --to-revision=3
kubectl rollout restart deploy/api -n shop

kubectl port-forward:本地验证集群内服务

格式是 资源/名字 本地端口:远程端口,不需要 NodePort 或 Ingress
-n 指定命名空间
--address 0.0.0.0 让同网段的机器也能访问;默认只绑 127.0.0.1,别人连不上
它经 kube-apiserver 的隧道转发,不经过 Service ——
所以「服务本身通不通」和「网络策略有没有拦」是两件事,先用它排前者
断开终端就停止,长期用要放进 tmux 或 screen
排障顺序:port-forward 能通 → 服务是好的,问题在 Ingress / Service / NetworkPolicy;
port-forward 不通 → Pod 或端口本身有问题

kubectl port-forward -n shop svc/api 8080:80
kubectl port-forward -n shop --address 0.0.0.0 svc/api 8080:80
kubectl port-forward -n shop pod/api-7d9f-abc12 5432:5432

kubectl top / drain:节点资源与维护

top node 和 top pod 依赖 metrics-server,没装会报 Metrics API not available
--sort-by=cpu 或 --sort-by=memory 按列排序,找资源大户
--containers 把 Pod 拆到容器级,看出是哪个 sidecar 吃内存
-A 所有命名空间
describe node 里的 Allocated resources 是 request / limit 的分配比例 ——
调度失败先看这里:常见是 request 写太大,把节点「账面」占满而实际 CPU 很闲
cordon 标记节点不可调度,不影响已在运行的 Pod
drain 驱逐节点上的 Pod 以便维护;--ignore-daemonsets 忽略 DaemonSet
(它们本来就不该被驱逐)、--delete-emptydir-data 允许删掉用 emptyDir 的 Pod(临时数据会丢)
维护完 uncordon 恢复调度

kubectl top node
kubectl top pod -A --sort-by=memory --containers
kubectl drain k8s-node-2 --ignore-daemonsets --delete-emptydir-data

🔬 进程级定位(strace / perf) (7)

top 只能告诉你「这个进程占 CPU」,回答不了「它在等什么」。这一组工具回答的是后一个问题

strace -p:进程卡在哪一个系统调用上

-p 附加到已运行的进程;也可以直接跟命令,从启动就开始跟
-f 连带跟踪 fork 出来的子进程 —— 不加的话主进程只是等子进程,看不到真正干活的那些
-T 显示每次调用的耗时(<0.000012>),找慢在哪一步靠它
-tt 时间戳精确到微秒
-s 200 字符串最多显示 200 字符;默认 32,会把关键路径截断成 ...
-o out.txt 输出到文件,几万行的输出直接刷屏没法分析
-c 结束时按系统调用分类汇总次数和总耗时 —— 先用它看哪一类最费时,
再针对性用 -T 定位单次慢调用
-p 挂上去一点输出都没有,说明卡在不可中断的 IO 或内核锁上,
下一步该看 /proc/PID/stack 和 /proc/PID/wchan
生产机慎用:-f 跟全量调用会把进程拖慢几倍,加上 -c 或过滤能减轻

strace -p 1234 -T -tt -s 200
strace -f -p 1234 -o /tmp/st.txt
strace -c -p 1234

strace -e trace:只看关心的那一类调用

-e trace= 是 strace 最有用的参数,没有之一
-e trace=file 只看文件相关(openat、stat、unlink),排查「文件找不到 / 权限不足」
-e trace=network 只看网络(connect、sendto、recvfrom),排查「连不上 / 超时」
-e trace=openat,connect 逗号分隔,精确指定几个调用
-e trace=desc 只看文件描述符操作,判断卡在哪个 fd 上
-e signal=SIGSEGV 只跟信号
过滤能把几万行压到几十行 —— 正确顺序是:先 -c 看哪类调用耗时最多,
再用 -e trace= 只跟那一类
openat 返回 ENOENT 是路径不存在,EACCES 是权限不够,EAGAIN 是非阻塞没数据,
三个错误码的处理方向完全不同,别看到 error 就一律查权限
其余参数与上一条 strace -p 完全一致:-f 跟子进程、-p 指定进程、-T 显示单次耗时、-tt 微秒时间戳、-o 写入文件

strace -f -e trace=file -p 1234 -T
strace -f -e trace=network -p 1234 -tt
strace -o net.txt -e trace=openat,connect -f ./app

perf top:CPU 到底花在哪个函数

perf top 是实时热点函数排行,不需要知道 PID,直接看全机
-p 只看某个进程
-F 99 采样频率 99Hz,避开 100Hz 整数以躲开定时器共振造成的假热点
-g 显示调用图;top 里 -g 视野有限,真正的调用链要靠 record
函数名显示成 unknown 说明缺 debuginfo,或者二进制符号被 strip 掉了 ——
不是 perf 坏了,是那个程序没带符号表
需要 root 或放行 kernel.perf_event_paranoid,容器里通常要 --privileged 或加 SYS_ADMIN
和 top 的区别:top 回答「哪个进程」,perf top 回答「哪个函数」,后者才是优化的起点

perf top
perf top -p 1234 -F 99
perf top -g

perf record:采样出火焰图数据

record 记录采样,report 交互查看,script 导出文本 —— 三件套
-F 99 每秒采样 99 次
-g 记录调用链,火焰图必需,不加只有孤立的函数名
-p 指定进程;-a 采集全部 CPU(全系统)
--call-graph dwarf 用 DWARF 信息展开调用栈,比默认的 fp 准确,
代价是数据文件大一个数量级;程序编译时加了 -fomit-frame-pointer 就必须用它
-o 指定输出文件,默认是当前目录的 perf.data
-i 指定 report / script 读哪个数据文件
-- 后面跟命令,可以直接从 perf 启动程序并采样启动阶段
火焰图要的是 script 的折叠输出,再交给 FlameGraph 的 stackcollapse 脚本处理
容器里默认 perf_event_paranoid 通常是 2,看不到内核符号,
需要 sysctl kernel.perf_event_paranoid=-1 才完整

perf record -F 99 -g -p 1234 -o /tmp/perf.data
perf report -i /tmp/perf.data
perf script -i /tmp/perf.data > /tmp/perf.txt

pidstat:单个进程的 CPU、IO、内存

pidstat 1 每 1 秒输出一次,持续刷新;后面再加一个数字表示输出几次就停
-p ALL 显示所有进程;-p 1234 只看一个
-d 磁盘 IO(kB_rd/s 读、kB_wr/s 写、iodelay 等待毫秒)
-r 内存(缺页、RSS)
-u CPU 使用率(默认就是这个)
-t 线程级拆分,找出是哪个线程在吃 CPU —— 多线程程序必用
-w 上下文切换,cswch/s 是自愿切换(等资源),nvcswch/s 是非自愿(被抢)
属于 sysstat 包,最小化安装的系统要先装 sysstat
比 top 好在能按固定间隔长期采样并写入文件,事后算均值、画曲线

pidstat -p 1234 1 10
pidstat -d -p ALL 1 5
pidstat -t -p 1234 1

iostat / vmstat:磁盘与内存瓶颈

iostat -x 1 扩展统计,每秒刷新
-x 输出扩展指标,没有它就是另一套简表
%util 接近 100% 说明磁盘已满负荷;await 是单次 IO 的平均等待毫秒
rkB/s 和 wkB/s 是每秒读写量 —— 大 await 加上小吞吐说明盘本身慢,
大 await 加上大吞吐说明是真忙,两者的扩容方向不同
vmstat 1 每秒一行:r 运行队列长度(持续大于核数说明 CPU 不够)、
b 阻塞进程数、si / so 换入换出(不为 0 说明内存不够开始用 swap)
wa 是 IO 等待占 CPU 时间的比例,高说明卡在磁盘而不是 CPU
-d 在 vmstat 里是磁盘统计,只显示磁盘那几列
第一次输出是开机以来的平均值,从第二行开始才是实时值 —— 别拿第一行下结论

iostat -x 1 5
vmstat 1 5
vmstat -d 1

gdb / gcore:进程 hang 住时抓调用栈

进程 hang 住、strace 又没有任何输出(卡在不可中断状态)时,要看用户态栈卡在哪
gdb -p 附加到进程会暂停它 —— 抓完必须 detach 再退出,否则进程一直停着,等于把服务停了
-ex 让 gdb 执行一条命令后继续,可以连写多个,实现「附上 → 打栈 → 脱离」一条龙
thread apply all bt 对所有线程打完整调用栈,单线程程序用 bt 就够
gcore 生成 core 文件,事后慢慢分析,比在线调试安全
-o 指定 core 的输出路径(不带 .PID 后缀)
Java 进程不要用 gdb —— JVM 的栈 gdb 看不懂,用 jstack
perf 的 --call-graph 抓的是采样点,看 hang 要看完整栈,两者不能互相替代

gdb -p 1234 -ex 'thread apply all bt' -ex detach
gcore -o /tmp/core.1234 1234

⌨️ Vim 与终端效率 (6)

服务器上经常只有 vi,改配置绕不开它。记住 20 个键就够了,不用学全

打开、保存、退出

vim +120 文件 直接跳到第 120 行 —— 看报错日志给的行号,这是最快的方式
-u NONE 不加载任何配置和插件,用来排除「是不是 vimrc 写坏了」
-w /tmp/keys.log 记录所有按键,排查「我到底按了什么」
:w 保存;:w! 强制保存(文件只读或权限不足时)
:q 退出,有未保存修改会被拒绝;:q! 放弃修改强退
:wq 或 :x 保存并退出,区别是 :x 在没有修改时不更新时间戳
:e! 丢弃当前修改重新加载 —— 改坏了想重来时用
:saveas /tmp/a.conf 另存为,改配置前先备份的标准动作
不小心改了只读文件::q! 是最快的挽回,别去点保存

vim +120 /etc/nginx/nginx.conf
vim -u NONE /etc/nginx/nginx.conf

:w      保存        :q!     放弃修改退出
:wq     保存退出    :x      有改动才存盘
:e!     重新加载    :saveas /tmp/a.conf  另存为

移动:不改内容只看

gg 文件首行;G 文件末行;42G 跳到第 42 行
0 行首;^ 行首第一个非空白字符(对齐缩进时用这个);$ 行尾
w 下一个词首;b 上一个词首;e 当前位置的词尾
Ctrl+f 下一页;Ctrl+b 上一页;Ctrl+d 下半页;Ctrl+u 上半页
% 跳到匹配的括号 —— 查配置块的闭合位置特别快
'' 回到上次位置(两个单引号);直接回车也行
大文件里别用方向键,hjkl 和上面这些组合快得多
改之前先 :set nu 显示行号,报错行号才有意义

gg        文件首行            G        文件末行
42G       第 42 行            0        行首
^         行首首个非空白      $        行尾
w / b / e 下词首 / 上词首 / 词尾
Ctrl+f    下一页              Ctrl+b   上一页
%         跳到匹配的括号      ''       回到上次位置

编辑:删除、复制、粘贴、撤销

dd 删除整行 —— 其实是剪切,能被 p 粘贴回来
yy 复制整行;5yy 一次复制 5 行
p 粘贴到当前行下方;P 粘贴到上方
u 撤销;Ctrl+r 重做(撤销撤多了用它)
x 删一个字符;r 用一个字符替换当前字符,不进入插入模式
cc 改写整行,内容清空但保留缩进
d$ 删到行尾;d0 删到行首;dw 删一个词
. 重复上一次修改 —— Vim 里最值钱的一个键,删 20 行是 dd 加按 19 次 .
i 光标前插入;a 光标后插入;o 下方新起一行;O 上方新起一行
Esc 回到普通模式,忘了这一步会打出一堆字符

dd / yy / p    删行 / 复制行 / 粘贴
5yy            复制 5 行
u / Ctrl+r     撤销 / 重做
x / r          删字符 / 替换一个字符
cc             改写整行(保留缩进)
.              重复上次修改(最值钱的一个键)
i / a / o / O  前插 / 后插 / 下方新行 / 上方新行

块操作与批量处理

Ctrl+v 进入块选择,移动光标框选一列 —— 批量注释、批量对齐靠它
选中后 I 在每行行首同时插入,输入完按 Esc 才真正生效(不按 Esc 会以为没反应)
选中后 A 在每行行尾同时追加
选中后 c 替换整块内容,d 直接删除整块
:g/pattern/d 删除所有匹配的行
:g/pattern/s/old/new/g 只对匹配的行做替换,比全局替换安全
:sort 对选中行排序;:sort u 排序并去重
:normal @a 对每个选中行执行一次寄存器 a 里的宏,重复性操作用它
:set nu 先开行号,块操作时不容易看错范围

Ctrl+v  I  Esc      块选中后每行行首插入(批量注释)
Ctrl+v  A  Esc      块选中后每行行尾追加
:g/^#/d             删除所有以 # 开头的行
:g/ERROR/s/old/new/g  只在含 ERROR 的行里替换
:sort u             选中行排序去重

粘贴错乱、换行符、编码

从网页或 Windows 复制代码进来,缩进一层层往右跑:先 :set paste 再粘,粘完 :set nopaste
(也可以 :set pastetoggle=<F2> 用 F2 来回切)
文件在 Windows 编辑过,行尾出现 ^M::set ff=unix 再 :w 保存
:set ff 单独执行可以查看当前是 dos 还是 unix
中文乱码::set fileencoding=utf-8 后保存;如果只是显示乱码不想改文件,
用 :e ++enc=gbk 按 GBK 重新读一次
:w !sudo tee % 解决「忘了用 sudo 打开,改完存不了」——
把内容通过 sudo 写回当前文件名,比重新打开再改一遍快
这些设置都是临时的,想永久生效写进 ~/.vimrc

:set paste           粘贴前关掉自动缩进
:set nopaste         粘完恢复
:set ff=unix         去掉 Windows 的 ^M
:set fileencoding=utf-8   修正编码后保存
:w !sudo tee %      忘了 sudo 时强制写回

🔥 排障套路 (6)

「服务器卡了」不是一个问题,是四个:CPU、内存、IO、网络。先用一条命令分清是哪一个

第一眼:到底卡在哪一层

uptime 看 load average 三个值:1/5/15 分钟。超过核数说明在排队
如果 load 高但 CPU 空闲(top 里 %id 很高),基本是 IO 等待,看 wa 列
free -h 看 available 那一列,不是 free 那一列(-h 人类可读)
df -h 和 df -i 一起看:-h 看容量,-i 看 inode,磁盘满会引发各种莫名其妙的问题
vmstat 1 5 每秒一次采样 5 次,看 r(排队)、b(阻塞)、wa(IO 等待)、si/so(换页)
先分清层次再动手,比一上来就重启服务有效得多

uptime
free -h
df -h; df -i
vmstat 1 5

load 高但 CPU 不高

先看 vmstat 的 wa 列:持续大于 10 就是 IO 在拖
再看 b 列(阻塞进程数),持续大于核数说明大量进程卡在等资源
定位是哪个进程:pidstat -d 1 5(-d 只报 IO 统计,每 1 秒一次共 5 次)
ps -eo 的 -e 是全部进程,-o 自定义列,state 一列能看到 D(不可中断睡眠)
awk '$1=="D"' 过滤出处于 D 状态的进程 —— 这些是卡在 IO 上的
常见原因:数据库全表扫描、日志同步写、备份任务在跑、NFS/云盘抖动
D 状态的进程 kill 不掉,只能等 IO 返回或重启机器

vmstat 1 5
pidstat -d 1 5
ps -eo state,pid,cmd | awk '$1=="D"'

内存到底够不够

free -h 里 available 才是「还能给新进程用多少」;free 少是正常的,Linux 会拿空闲内存做缓存
buff/cache 高不是问题,需要时会自动回收
swap 用得多说明真的紧张,vmstat 里 si/so 持续不为 0 就是在颠簸
按内存排进程:ps -eo pid,rss,comm --sort=-rss(-e 全部、-o 选列、--sort 排序键前置负号为降序)
RSS 是实际占用,VSZ 是虚拟地址空间,基本没有参考价值
容器里看的是 cgroup 限制(/sys/fs/cgroup/memory.max),不是宿主机 total
怀疑内存泄漏时对比两次 RSS:间隔十分钟各抓一次,看谁的数值单向增长

free -h
vmstat 1 5    # 看 si / so
ps -eo pid,rss,comm --sort=-rss | head

接口慢:从外到内逐层计时

第一层 curl -w 拿到 DNS / 连接 / 首字节 / 总耗时四项分解
curl -o /dev/null 丢掉 body,-s 静音不显示进度条,-w 自定义输出(三者合起来只看数字)
第二层在服务器上 curl 127.0.0.1,排除网络与网关
第三层看应用日志的请求耗时,对比 nginx 的 $request_time 与 $upstream_response_time
两者差得多说明卡在中间层(网关、代理、连接池)
数据库慢查询日志别忘了开,很多「接口慢」其实是 SQL 慢
只有 urltime 高而上游时间低,通常是被限流或排队

curl -o /dev/null -s -w 'dns:%{time_namelookup} conn:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n' https://example.com/api
# nginx log_format 里加:
# $request_time $upstream_response_time

网络丢包与抖动

ping -c 50 发 50 个包看 loss% 和 mdev(抖动),一次 ping 证明不了什么
ping -M do -s 1472 测路径 MTU:-M do 禁止分片,-s 指定包大小,不通说明 MTU 有问题
mtr -r -c 50:-r 直接输出报告不分屏,-c 指定发包数,比 traceroute 强在能看每跳丢包率
注意 ICMP 被限速会伪装成丢包,用 TCP 探测更准(nc -z 或 curl)
ethtool -S 网卡 看 rx/tx 错误与丢包计数(-S 是 statistics)
接 grep -iE 是忽略大小写 + 扩展正则
同机房内丢包先怀疑网卡/网线,跨地域先怀疑运营商链路

ping -c 50 10.0.0.5
mtr -r -c 50 10.0.0.5
ethtool -S eth0 | grep -iE 'err|drop'

巡检一把梭

登录一台陌生机器,先跑这一串拿到全貌,再决定查什么
每段都标了在回答什么问题,别当黑盒抄
参数速查(都在上面条目里讲过):--sort=-%cpu 按 CPU 倒序,-h 人类可读,-i inode
grep -vE 是「排除 + 扩展正则」,这里用来滤掉 tmpfs 这类虚拟文件系统
ss -tulnp 拆开是 t/TCP、u/UDP、l/监听、n/数字、p/进程
systemctl 的 --failed 只看失败单元、--no-pager 不分页
dmesg 的 -T 可读时间、-l 只取指定级别
生产机上避免任何写操作 —— 这套全部只读,输出可直接贴进工单

echo "== 负载 =="; uptime
echo "== CPU 前 5 =="; ps aux --sort=-%cpu | head -6
echo "== 内存前 5 =="; ps aux --sort=-%mem | head -6
echo "== 内存 =="; free -h
echo "== 磁盘 =="; df -h | grep -vE 'tmpfs|overlay'
echo "== inode =="; df -i | grep -vE 'tmpfs|overlay'
echo "== 监听端口 =="; ss -tulnp
echo "== 失败服务 =="; systemctl list-units --failed --no-pager
echo "== 内核错误 =="; dmesg -T -l err | tail -10