共 104 条真会敲的命令,参数逐条说明 —— 按「要干什么」分类
查找、批量处理、打包 —— 参数记错的代价通常是删错文件或漏掉隐藏目录
-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
-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
-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
-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
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)
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 -s 目标 链接名 —— 参数顺序是「目标在前」,反了会建出个坏链接
-s 建软链接;不加 -s 是硬链接(不能跨文件系统、不能链目录)
-f 覆盖已存在的链接
-n 当链接指向目录时,配合 -f 才能正确替换而不是塞进目录里
-sfn 是发布目录切换的标准组合(current 指向新 release)
软链接存的是路径字符串,目标移动后失效;硬链接指向同一 inode,删一个不影响另一个
ln -sfn /srv/app/releases/20261029 /srv/app/current
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
改配置、按列统计、拼命令 —— 运维里「手改 200 行配置」的事都该交给 sed/awk
-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 # 去掉注释与空行
-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 -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 | headcut -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 把标准输入的每一行变成后面命令的参数,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 echotee 把输入同时写到文件和标准输出,改配置前留证据最有用
-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,比 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.jsondiff -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 里的行
登录、免密、传文件、续传 —— 日常最费时间的其实是「怎么把文件弄过去」这一步
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 -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 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-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 源 目标 —— 方向决定上传还是下载:本地在前是上传,远端在前是下载
-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 用户@主机 进入交互会话 —— 「一边 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
-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 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
数字位、特殊位、sudo —— 权限配错的典型症状是「明明有密码却进不去」和「服务起不来」
数字位: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 :组 或 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
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 是「要减掉的位」,不是权限本身
022 → 新建目录 755、新建文件 644(文件本来就不带执行位)
077 → 新建目录 700、新建文件 600,适合放密钥的目录
-S 用符号形式(rwxr-xr-x)显示实际默认权限,比看数字直观
临时改:umask 077 只对当前 shell 生效
永久改:写进 /etc/profile 或用户的 ~/.bashrc
排查「为什么新建的密钥别人能读」先看 umask
umask # 看当前值 umask 077 # 当前会话生效 umask -S # 用符号形式显示实际默认权限
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 -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
谁在占 CPU、谁握着文件、怎么让它优雅退出 —— 这里错误的操作通常是 kill -9 用太早
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 里按 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 发 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 # 重载配置
& 只是放后台,终端一关就收到 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 -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 值范围 -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 默认每 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 -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
服务起不来时,先分清是「没监听」「没放行」还是「连不上」—— 这三步的顺序错了会白查很久
-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
-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/apinc -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 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
+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
-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'
-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
「磁盘满了」有四种完全不同的成因:文件多、单文件大、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"}'默认输出树状结构,能看出 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 -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 总数在格式化时就定了
症状是 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
cron 跑不起来八成是 PATH 和输出重定向;时间不准则会连带证书、JWT、日志一起出问题
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
优点很实在:日志进 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 看当前时间与时区;+%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 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)
定位的顺序比命令更重要:先按时间框定,再按关键字收敛,最后看上下文
-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
-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
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'
确认: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
配置放 /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服务起不来的绝大多数原因就三种:路径不对、用户没权限、依赖没起来 —— status 里都写着
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
放 /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 拒绝记录
改了 .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 硬依赖,对方失败本服务也失败
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
装之前先查来源,删之前先看会带走谁 —— 生产机上尤其如此
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 的继任者,新系统上 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
容器出问题九成是「你以为的配置」和「实际生效的配置」不一致 —— 先用 inspect 对齐事实,再看日志
-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}}'-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
-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'
--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--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-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 .
-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
K8s 排障的顺序几乎是固定的:看状态 → 看事件 → 看日志 → 进容器。跳过任何一步都是在猜
-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
尾部 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
-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
-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
和 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
-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
格式是 资源/名字 本地端口:远程端口,不需要 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
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
top 只能告诉你「这个进程占 CPU」,回答不了「它在等什么」。这一组工具回答的是后一个问题
-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
-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 是实时热点函数排行,不需要知道 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
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 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 -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
进程 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
服务器上经常只有 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 前插 / 后插 / 下方新行 / 上方新行
/pattern 向下搜;?pattern 向上搜
n 跳到下一个匹配;N 跳到上一个
* 直接搜光标下的词,不用输入;# 反向搜
:set hlsearch 高亮全部匹配;:noh 取消高亮(搜完满屏黄块时用)
:%s/old/new/g 全文替换,g 表示每行的所有匹配;不加 g 只换每行第一个
:%s/old/new/gc 每次替换前确认 —— 改配置文件必须加 c,一次错误替换能让服务起不来
:10,20s/old/new/g 只替换第 10 到 20 行
:.,$s/old/new/g 从当前行到文件末尾
分隔符可以换::s#/usr/local/old#/usr/local/new#g,路径里有 / 时不用层层转义
替换前先 /pattern 数一下匹配数量,心里有底再动手
:%s/old/new/g 全文替换(每行所有匹配) :%s/old/new/gc 每次替换前确认(改配置必加 c) :10,20s/old/new/g 只替换第 10-20 行 :.,$s/old/new/g 当前行到文件末尾 :s#/usr/local/old#/usr/local/new#g 换分隔符,路径不用转义 :noh 取消搜索高亮
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 时强制写回
「服务器卡了」不是一个问题,是四个: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
先看 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_timeping -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