Shell 脚本神器 xargs
涛叔最近工作中要大批量操作线上机器。因为是历史遗留系统,无法使用 Ansible 等现成的框架,只能自己手写 Shell 脚本来处理。这中间就不免会涉及到批量生成列表和并发执行命令的场景。这类工作可以通过 xargs 命令来实现。本篇就来分享具体的实现技巧。
xargs
简单来说就是从标准输出读取一批数据,然后使用这些数据作为参数来调用指定的程序。下面给出一个简单的例子:
$ seq 1 3 | xargs -I % echo %
1
2
3
seq
也是 UNIX 命令,可以根据参数生成一组数列。seq 1 3
会依次输出1,2,3
,每个数占一行。seq
的输出内容会通过管道|
传递给xargs
命令。这里我用xargs
来调用 echo %
命令,其中的%
是参数占位符,由前面-I
参数设置。xargs
会从stdin
中依次读取1,2,3
,并依次执行echo 1; echo 2; echo 3;
。占位符不一定是%
,大家可以根据实际情形灵活指定。
好了,到这里基本就介绍完xargs
的主要功能了。等等,这么简单的东西有什么用呢?其实大有用处。UNIX 哲学是做一件事,并把它做到极致。然后我们可以通过管道把不同的功能组合起来,完成各类复杂的任务。而且xargs
在这种组合过程中发挥了关键作用。
现在介绍几种常见的使用范式。
我们可以复用xargs
生成各类连续型列表。比如我最近的工作中要大量生成机器名:
cat ids.txt|xargs -I % echo demo-%-db-1
这里我事先把对应的 ID 保存到ids.txt
文件中。然后生成一系列形如demo-1-db-1
这样的机器名。
有朋友可能会说用sed
也可以实现。比如上例可以改写成
cat ids.txt|sed 's/\(.*\)/demo-\1-db-1/'
但sed
涉及到正则表达式,相对来说没有xargs
直观。
以上是为每个🆔生成一条数据的例子。有时候我们还需要一次生成多条数据。就比如我最近工作的情景,每组业务服务有两台机器,分别使用-1
和-2
后缀,需要同时处理。这时我们可以这样生成:
cat ids.txt | xargs -I % printf "demo-%-gs-1\ndemo-%-gs-2\n"
注意,核心变化是改用了printf
命令,它类似C语言中的printf()
函数,可以输出转义字符。这里我用了\n
来输出换行。假如xargs
读到数字1
,它就会输出:
demo-1-gs-1
demo-1-gs-2
除了用xargs
指定参数占位符外,在某些特定的场景,我们也可以不指定。此时它的行为会有些差异。它会把读到的内容用空格合成一行再输出,比如:
$ seq 1 3 | xargs echo
1 2 3
我们可以利用这个特性处理一些简单的任务。比如说批量停止深信服 EasyConnect 的进程1:
ps aux | grep EasyCon | awk '{print $2}' | xargs sudo kill
这里使用ps
和grep
查出 EasyConnect 相关的进程信息,然后用awk '{print $2}'
提取ps
输出结果的第二列,也就是进程号;最后利用xargs
执行sudo kill
打死进程。
假如说 EasyConnect 的进程号分号是1001/1002/1003
,那么上面的脚本等价于:
sudo kill 1001 1002 1003
xargs
这种行列转换在有些场景🎬下非常有用。比如我司内部系统支持通过正则表达式来搜索主机资源,但是每次搜索都会重置列表。如果你要查的主机是连续编号,比如gs[1-9]
,这种还好处理。否则,就需要多次打开搜索列表,多次搜索并提交,很不方便。
后来我索性把所有主机ID都加到一个大的正则里,一口气匹配出所有目标:
cat ids | xargs echo | sed 's/ /|/g'
注意最后的sed
,它使用了g
参数,表示要把一行内所有的空格都替换成|
。
网上还有另外一个有名的例子,批量删除 Ubuntu 下的包配置。Ubuntu 下某个包被删除后,它的配置文件可能还保存在磁盘中。像我这样有强迫症的人肯定是不能忍,非得清理掉不成。
具体可以选通过dpgk -l|grep ^rc
来查出所有的遗留配置包。然后再通过awk
提取包名,最后通过xargs
批量删除。
dpkg -l | grep ^rc | awk '{pritn $1}` | xargs sudo apt-get purge -y
我这次使用xargs
调用sudo apt-get purge
命令,而且指定了-y
参数。默认apt-get
会输出确认删除提示,用户输入y
表示确认后才真正删除。但我们是批量操作,无法执行这样的交互式确认,所以要指定-y
参数。
以上命令也可以改为
dpkg -l | grep ^rc | awk '{pritn $1}` | xargs -I % sudo apt-get purge -y %
xargs
会针对每一个包名多次调用sudo apt-get purge
,效果跟上面那条没有差别。
那为什么xargs
要有两种执行模式呢~我想第一个原因是有些程序不支持输入多个参数。像kill 1001; kill 10002
可以简化成kill 1001 1002
,但像是dig
就不能同时查询两个域名。
不过我也不认为这是主要原因。更重要的原因是xargs
需要实现并发执行的效果。我们可以把发送给xargs
的内容看成是任务,每一行一个任务。xargs
不但可以逐个触发对应的处理程序,而且还可以通过-P
参数来并行处理。给大家举个列子:
seq 1 5 | xargs -P 2 -I % curl -O https://example.com/files/%.txt
其效果等价于以下脚本,也就是下载五个 txt 文件:
curl -O https://example.com/files/1.txt
curl -O https://example.com/files/2.txt
curl -O https://example.com/files/3.txt
curl -O https://example.com/files/4.txt
curl -O https://example.com/files/5.txt
如果没有指定-P
参数,xargs
会依次执行上面的五条命令。但我们指定为-P 2
,那么 xargs
会每次并改执行两条命令,如果改为-P 5
,那就会同时下载五个文件。这样可以大大缩减任务执行耗时。
比如,我曾经使用五并发来批量查询 DNS 记录😜
seq 1 100 | xargs -I % echo gs%.example.net | xargs -P 10 -I % q CNAME % @119.29.29.29
再比如我还有十并发来批量备份 MongoDB 数据:
cat dbs.txt | xargs -P 10 -I % mongodump mongodb://root:${pass}@%1.mongodb.rds.aliyuncs.com:3717/% \
--authenticationDatabase admin \
--gzip --archive=/backup/db/%.archive
同样的技巧也可以用于并行处理更复杂的任务。比如 Adam 基于xargs
用单机处理数据2,吞吐量达到了 270MB/s,甚至比 Hadoop 还要快。
xargs
无疑是非常老旧的工具,但它的设计思想却永不过时。UNIX下一次只做一件事并做到极致,然后再将不同的工具组合起来。这种哲学不但启迪了后来的函数式编程,在单机性能日益增强的现代将发挥越来越重要的作用。希望大家都能使用xargs
来提升自己的工作效率。
深信服的 EasyConnect 非常恶心,具体可以看我的专门文章 ../easyconnect-in-docker.html↩︎
https://adamdrake.com/command-line-tools-can-be-235x-faster-than-your-hadoop-cluster.html↩︎