ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux命令组合实战:从管道原理到高频运维配方

Linux命令组合实战:从管道原理到高频运维配方 上个月排查一台测试服务器的磁盘告警我当着四个同事的面用了三行命令定位到占空间的目录十分钟不到就把问题解决了。后来这段对话被截到群里有同事说我“秀操作”。其实真没用什么高深技巧只是把du、sort、head这三个再普通不过的命令用管道串在了一起。Linux 命令的乐趣恰恰在这里单看每个命令都平平无奇但当你学会把它们按场景拼起来一条条原本要手动重复的操作就会变成流水线一次执行解决问题。这篇文章想聊的就是这种“命令组合”的玩法。我会从组合的底层原理讲起给出一套可以直接抄走的高频配方再分享一些脚本化、跨工具链串联的进阶经验最后把这些年踩过的典型坑一并列出来。读完你能带走的东西很明确遇到问题时知道用哪几个命令搭积木以及怎么避免把组合玩成事故现场。无论你是刚开始学 linux 常用命令的新手还是已经写了几年 linux 脚本的运维和开发应该都能找到对应自己阶段的干货。1. 命令组合的底层逻辑管道、子进程与I/O协作方式想玩好组合先得理解 shell 里“组合”究竟是怎么发生的。很多人把管道理解成简单的数据接线两头一插就完事这个认知会让你在处理复杂组合时处处碰壁。1.1 管道不是接线是进程间的接力赛管道|在底层的真实动作是shell 先创建两个子进程左边命令的标准输出接到一个缓冲区右边命令的标准输入从同一个缓冲区读取。两个命令是同时运行的不是等左边跑完再跑右边。这个过程在 man 手册里叫 pipe本质上就是一段内核内存。这里有个非常实用的推论当你执行tail -f app.log | grep ERROR时grep 会跟着 tail 源源不断读数据只要 app.log 有新行写入它就能实时过滤。很多做日志追踪的人爱上这条命令就是因为它利用了两个进程的并行特性。同样的原理也解释了为什么cat hugefile.log | grep key在数据量很大时也能快速出结果——两个命令并行跑管道缓冲区边写边读不必等 cat 全部读完。不过并行也意味着一点靠前的命令如果一直不结束比如tail -f靠后的命令也会一直挂在那里。你要是用组合命令去写自动化任务一定要考虑到命令是否可能永久阻塞。经验做法是给可能长期运行的命令加超时工具比如timeout 5 tail -f app.log五秒拿不到新数据就主动退出避免脚本被挂死。1.2 重定向、退出码和 / || / ; 的差别组合命令不止管道一种形式。重定向、、21负责文件与程序之间的数据流、||、;则负责控制命令之间的执行顺序。这三者的区别曾经坑过我很久;表示不管前一条命令成功还是失败后一条都执行。适合互不依赖的多条命令按顺序跑比如cd /tmp; pwd; ls。表示前一条成功才执行后一条。适合有依赖关系的组合比如cd /project make make install任何一环失败都应该停下来避免在错误状态上继续操作。||表示前一条失败才执行后一条。常用于兜底逻辑比如mkdir /data/backup || echo 创建失败。很多人会忽略一个细节管道组合的退出码默认只取决于最后一个命令。比如grep key bigfile.log | tee result.txt如果 grep 匹配不到关键字结果文件是空的但整条管道往往还是返回 0因为 tee 成功了。这会让自动化脚本误判“一切正常”。后面第 5 章我会专门讲怎么用set -o pipefail把这个隐患解决掉这里先标记一下。1.3 通用的“积木命令”grep、awk、sed 该学会到什么程度命令组合的威力建立在基本功上。我建议把这三条命令的重点场景吃透不需要背全参数但要清楚它们各自擅长什么grep是筛选器按正则表达式从文本流里挑出需要的行。重点参数-E扩展正则、-v反选、-i忽略大小写、-r递归目录。awk是按列拆解。默认按空白拆分$1、$2表示第几列。重点学会awk {print $N}、awk -F, {print $1}按逗号拆分以及用 BEGIN/END 做简单统计。sed是文本替换和按行操作工具。对于组合命令来说最多用到的就是sed -n 10,20p打印第10到20行和sed s/old/new/g全局替换。用生活化的话说grep 像筛子awk 像切菜刀sed 像修正液。管道组合的本质就是把“筛选→切割→修正”三步串起来。2. 高频运维配方日志分析、磁盘清理与端口探测的组合套路有了底层逻辑打底下面直接进入实战。我挑了三类最常遇到的问题每一类给出可以直接套用的组合配方和解释。2.1 磁盘告警时du、sort、find 联合定位开头那个故事里的三行命令是df -h du -sh /data/* 2/dev/null | sort -rh | head -20 find /data -xdev -type f -size 500M -exec ls -lh {} \;第一行df -h看文件系统整体使用率确认是不是真的满了。第二行是核心du -sh /data/*算出/data下每个一级目录的总大小2/dev/null把权限不足的报错丢到黑洞sort -rh里的-h能识别 K、M、G 这些人类可读单位并按数字倒序排序head -20只取最大的前 20 个。第三行find进一步找出超过 500M 的大文件-xdev防止 find 跨出当前文件系统避免把其他挂载点也扫进来-exec ls -lh {} \;显示出具体大小。这套组合的精髓在于“由面到点”逐层缩小范围。先看整体再按目录看最后按文件看。你不需要在一开始就梭哈一条复杂到哭的长命令分步组合反而更好排查。清完垃圾之后如果想知道哪些目录还能再瘦身可以把第二行的head -20改成awk $1 ~ /G/ {print}只挑出上 G 的目录然后逐个进去细查。2.2 日志聚合统计sort 与 uniq 的固定组合日志分析最常见的需求是统计类问题访问量最高的 IP 是哪些、报错最频繁的模块是哪个、用户最常点的 URL 是哪几条。标准配方如下awk {print $1} access.log | sort | uniq -c | sort -rn | head -10解释一下每一步awk {print $1}从日志里抽出第一列通常是客户端 IPsort把所有 IP 排成有序列表uniq -c对相邻相同行计数sort -rn按计数结果做数值倒序head -10取前十。这条命令是日志分析里最容易复制的模式换一个字段就能回答不同问题。这里有个值得记住的硬规则uniq 的计数值只对“相邻”的重复行有效所以用它之前必须先排序。如果你跳过 sort 直接uniq -c只会看到零散的数字完全得不出正确结论。这个坑我在刚做运维时踩过不止一次看输出还以为是统计工具出了问题其实是自己跳过了步骤。如果想统计报错频率可以组合过滤grep -i error app.log | awk {print $2} | sort | uniq -c | sort -rn | head假设日志第二列是模块名这条命令就能快速告诉你哪个模块的 error 最多。想进一步提升统计准确性多用grep -Ev ^(#|$)这类组合过滤掉空行和注释行别用两条grep -v串联一条扩展正则既干净又少占管道。2.3 进程与端口排查ps、ss、nc 组合拳排查服务状态是 Linux 运维的家常便饭。我常用的组合是ps aux --sort-%mem | head -15 ss -tlnp | grep :8080 nc -zvw3 10.10.10.5 3306ps aux --sort-%mem按内存占用排序进程略掉表头的话可以接--no-heading。想按 CPU 排序就把%mem换成%cpu。如果想快速看所有进程占用的总内存百分比可以这样ps aux --no-heading | awk {sum $4} END {print sum%}第四列是内存占用百分比awk 累加出总量。这个数字和大不大心里有数。端口探测方面老牌命令 telnet 依然有效执行telnet 10.10.10.5 3306如果黑屏或输出 Connected 就说明端口通。但它有时候卡住不退出脚本里不太友好。我更推荐用 nc 来探测nc -zvw3里-z表示只扫描不发送数据-v输出详细连接信息-w3是三秒超时。通不通一眼见分晓很适合写进自动检查脚本。本地有没有进程在监听端口交给ss -tlnp查最直接输出里的 PID 还能帮你追到具体是哪个服务。3. 把命令组合固化成脚本从一次性操作到可持续复用的工具组合命令最大的风险是什么是每次都要重新敲一遍敲的过程容易手抖出错。所以我的习惯是一条组合命令在终端里验证通过三次以上就把它固化成脚本或函数。从“一次性操作”到“可持续复用工具”这是效率的分水岭。3.1 参数化脚本一开始就给脚本留好“接口”写脚本不要写死路径哪怕是给自己用也一样。路径写死意味着换台机器、换个目录就要从头改脚本非常蠢。我通常会写成像下面这样的结构#!/bin/bash # 用法: disk-top.sh [目录] [条数] target_dir${1:-/data} top_n${2:-10} if [ ! -d $target_dir ]; then echo 目录不存在: $target_dir 2 exit 1 fi du -sh $target_dir/* 2/dev/null | sort -rh | head -$top_n关键点有三个一是用${1:-/data}定义默认值不给参数时自动落到默认目录给参数时使用参数二是加一个目录存在性检查避免手误把不存在的路径传给后面的命令到时候 du 报一堆错三是所有用到变量和路径的地方都加了双引号防止路径里带空格导致组合命令被拆解得乱七八糟。和|的区别在脚本里尤其重要。如果你想把 du 的输出同时保存到文件和显示到屏幕可以这样du -sh $target_dir/* 2/dev/null | tee disk_report.txt | sort -rh | head -$top_ntee在这里像是一个“分流接头”在管道中间把数据复制了一份到文件剩下继续往下传。它是我认为脚本化组合时最容易被低估的命令之一。3.2 xargs 的正确打开方式处理带空格文件名的唯一优雅姿势命令行组合里有一条著名的坎如果你用for f in $(find /path -name *.log); do ...; done这种写法一旦文件名里有空格循环变量$f就会被拆成两段后面的命令指不定操作了什么文件。正确解法是用 find 配合 xargsfind /tmp -name *.tmp -print0 | xargs -0 rm -f这里的-print0让 find 用 null 字符分隔文件名-0告诉 xargs 也按 null 字符读取。只要两端都用 -0文件名里就算有空格、换行、特殊字符都会被当成一个完整的名字传给 xargs。如果你需要在管道后半段把文件名嵌入到一条自定义命令里用-I参数find /data -type f -name *.jar -print0 | xargs -0 -I {} sh -c ls -lh {} | tail -1-I {}的意思是把读到的每一行内容放到后面{}所在的位置。这里的双引号不能省因为文件名可能含空格不引的话ls又会把名字拆开。我自己更喜欢把这类复杂场景直接挪进脚本写 while 循环可读性会更好例如find /data -type f -name *.conf -print0 | while IFS read -r -d file; do echo 处理文件: $file grep -H server_name $file donewhile 循环的写法更好调试也方便在循环体里加多个操作。后面第 5 章还会补充一个相关问题。3.3 用 alias 和函数收拢高频组合很多命令组合其实不需要写成完整的脚本一条 alias 就够。比如我每台机器上都固定配置的alias topmemps aux --sort-%mem | head -10 alias topcpups aux --sort-%cpu | head -10 alias myiphostname -I | awk {print \$1} alias portsss -tlnpalias 适合定义“不带参数”的固定命令。如果你需要传参就写成一个 shell 函数。函数的好处是能用$1、$2接收参数也能写多行逻辑。比如我把日志统计定义成logtop函数logtop() { local file$1 local column${2:-1} local n${3:-10} awk -v col$column {print $col} $file | sort | uniq -c | sort -rn | head -n $n }把这段放进~/.bashrc每次开终端就能直接执行logtop /var/log/nginx/access.log 1 10。注意awk的变量传递用了-v这是 awk 从 shell 拿变量的标准姿势比直接拼字符串安全不会因为变量里带单引号或空格把 awk 语法搞坏。4. 跨工具链组合git、docker、vim 里面的命令串联玩法很多人一提到命令组合脑子里只有 shell 自带的命令。实际上 git、docker、vim 这些“大块头”工具的性命中也藏着组合思想这部分组合能大幅提高日常开发效率。4.1 git 日志与管道搭配快速定位提交线索git 的输出本身就是文本流所以天然适合再接管道继续处理。我最常用的一条git log --oneline -20 | grep -i fix把最近的 20 条提交只取一行摘要然后筛出跟 fix 相关的记录。如果你想看某次提交具体改了哪些文件接上管道也很快git show --stat 3a4b5c6 | head -30调试许久怀疑是某段历史代码引入问题时可以搜索所有提交的补丁内容git log -p --all -- src/core/parser.c | grep -B3 -A3 timeout这条组合是在所有分支的提交记录里搜 parser.c 中出现过的 timeout 相关改动并带上下文显示。理解 git 内部逻辑后你会发现这种“把 git 输出喂给 grep/awk”的做法比在图形化客户端里点点点效率高出一个档次。4.2 docker/containerd 资源监控组合stats 输出如何变干净容器环境里排查资源占用也是高频场景。docker stats的输出默认是动态刷新屏幕的表格直接接管道往往拿不到想要的数据。我习惯先加--no-stream拿一次快照再用 awk 过滤docker stats --no-stream | grep -v CONTAINER | awk {print $2, $3, $4, $7}比如第 3 列是 CPU 百分比第 7 列是内存占用这样输出就干净得像一张自己定义的报表。想按内存排序再加 sort 即可。容器进程太多的时候加上sort -k2 -hr这类参数能快速定位“最吃资源的容器”。用完的镜像和悬空层会占不少磁盘清理前先看一眼是合理的docker system df如果确认要清再执行docker system prune -f。containerd 命令在这些场景里是更多还是类似思路containerd 本身偏底层了日常用 ctr 命令查镜像和容器时注意ctr 的很多输出默认本来就是平铺文本反而更适合直接接 grep。核心诀窍仍然是那句先让工具输出稳定格式再交给 grep/awk 做二次加工。4.3 vim 调用外部命令把编辑器当命令中转站vim 的长度是编辑器但它的命令行模式可以调用 shell 命令这一点经常被忽略。最经典的几个在 vim 里给当前行的每一行加上行号:%! cat -n:!的意思是让后面的 shell 命令处理当前缓冲区内容处理结果替换回文件。cat -n会给每行编号所以上面的命令等于把整个文件每行补上行号。想去掉行号再执行:%! cat就行。在写技术文章或核对数据时我还经常用:r !date %Y-%m-%d:r !是把外部命令的输出插入到当前光标所在行下方。一键插入当前日期方便随手记录操作时间。如果你在运维时习惯先在本地起草变更说明再贴到工单里这招很实用。vim 的替换功能配合外部命令也能玩出花。一次性把所有foo替换成bar是:%s/foo/bar/g这本身是 vim 内部命令相当于文本编辑流程里的一个“内置组合”。如果替换逻辑特别复杂我常常先把文件内容过滤一遍再打开grep -E ERROR|WARN app.log | sed s/\[[0-9]*\]//g clean.log vim clean.log先在外面把日志清洗好再进编辑器和数据“单挑”。这种工作流比在 vim 里折腾一堆宏要直观得多。5. 踩坑记录管道退出码、引号转义与误删风险的安全实践最后一章必须写踩坑。命令组合越熟练越容易在常见陷阱上翻车。下面每条都是我用教训换来的。5.1 管道退出码陷阱为什么脚本在组合失败后还能继续跑前面提过管道命令的退出码默认看最后一个命令。这意味着如果前面的命令失败了只要最后一个命令“成功”整条管道就会返回 0。实际场景里非常危险grep -i 启动成功 service.log | wc -l假如 service.log 目录不存在grep 会输出错误并返回 2但 wc -l 仍然返回 0。于是脚本判断“日志行数为 0”以为服务没启动直接跳过后续警告。真正的问题却被吞掉了。解决办法是在 bash 脚本开头加上set -o pipefailpipefail会命令管道返回值取“所有命令中最大的那个非零值”即只要任何一环失败整条管道就失败。搭配set -e可以让脚本在失败时立刻退出。但我建议不要盲目加set -e有些命令本来就会返回非 0比如 grep 没匹配到脚本会提前退出。更稳的做法是脚本开头加set -uo pipefail-u 检测未定义变量然后对“允许失败”的部分显式加|| true或 if 判断。5.2 带空格文件名、通配符空匹配两条最常踩的地雷我在 3.2 节提到过文件名包含空格时简单的for f in $(find ...)会裂开。这里再演示一个更隐蔽的版本for log in /data/logs/*.log; do gzip $log done当/data/logs/下没有任何.log文件时*.log通配符不会展开成空而是原样保留为字符串/data/logs/*.log。于是循环会尝试 gzip 这个不存在的“星号文件”报错一堆。解决方案是在脚本开头声明shopt -s nullglob让通配符在没有匹配时展开为空列表或者在循环里加文件存在性判断[ -f $log ] || continue。带空格的问题我再用一条对比说清楚# 错误写法包含空格的路径被拆开 for conf in $(find /etc -name *.conf); do cp $conf /tmp/backup/ done # 正确写法用 while find -print0 find /etc -name *.conf -print0 | while IFS read -r -d conf; do cp $conf /tmp/backup/ done第一种写法里$(find ...)的输出会按照空格、换行被拆成多个词只要路径里有空格就断。第二种写法用 null 字符作为分隔符文件名再怪也能原封不动传给循环体。这不是理论问题我实际遇到过备份脚本因为路径里带空格把文件复制到错误位置的情况。5.3 别急着执行echo 预览、sudo tee 与可回滚操作最后这条是我在任何产线上都要强调的原则在新组合命令真正执行前先把它改写成一个“只打印不执行”的版本确认无误再开真枪。最简单的方式是把rm换成echofind /data -name *.tmp -print0 | xargs -0 -I {} echo 将删除: {}这行命令会把将要删除的文件路径全部打印出来你扫一眼就知道这条命令打击面是否正确。确认没问题后再把echo换成rm执行。批量删除这种事九十%的误删事故都出在“自信满满直接执行”包括我自己十年前也干过删错文件夹的事故。多花十秒钟预览能省掉一整天的恢复时间。另一个容易踩的坑是重定向写系统文件。如果你想把一条命令的结果写入/etc/sysctl.conf之类的系统文件直接echo 1 /etc/sysctl.conf在普通用户下必然 Permission denied。有人会想改成echo 1 | sudo tee -a /etc/sysctl.conf这是对的因为sudo tee能拿到写入权限。但要记住管道左边是普通用户权限右边才提权左边命令本身不一定有权限访问所有文件得按需调整。再补一个跟备份相关的小习惯执行大量会产生破坏性的命令前先打包留个可回滚快照。比如要批量修改配置前跑一句tar czf /tmp/config_backup_$(date %Y%m%d_%H%M%S).tar.gz /etc/nginx/conf.d/这算是命令组合的一种安全化实践。价格很便宜时间只需要几秒钟但出事的时候它就是你最可靠的后路。从最底层的管道原理到脚本化固化再到 git、docker、vim 的跨工具串联最后到踩坑记录命令组合的本质始终是“理解每个工具擅长什么再把它们按正确的顺序接到一起去”。如果你现在还算不上高手也不用急着背所有参数建议从磁盘清理、日志统计、端口探测这样的小场景开始把每条组合里的每一步都拆开看明白。等哪一天你遇到新问题脑子里能自动跳出“这活可以用哪几条命令组合掉搞定”的时候这条 Linux 技能线就算真正打通了。
返回列表