Shell 黑科技之匿名函数实现任务并行化
shell 作为一门系统级别胶水语言,学习成本低,用起来很方便,但是缺点也显而易见:性能问题一直为人锁诟病。所以 shell 也就多用在简单的系统管理等场合,数据处理等等要求比较高的场合一般会选择 java、Python 等功能更强大、性能更好的语言。
最近用shell写了一个小函数,用来在集群间批量执行命令并返回结果:
for ip in ips
do
ssh work@$ip "echo 1; exit" 2>/dev/null
done
执行下来功能没啥问题,但是性能却一塌糊涂,6台机器执行将近 5s,因为这个 for 循环 ssh 的过程是串行的。
那咱们有没有优化的方案呢?
首先想到的是不依赖任何三方工具或库(实际上我们 RD 也没有权限安装),有没有比较方便的办法。当然有了,每个 ssh 起来放后台不就行了吗? 嗯,说干就干,撸起袖子立马上:
ssh work@$ip "echo 1; exit" > a.txt 2>/dev/null &
...
wait
其实就是最后加了个 & 放后台执行, 嗯执行起来速度确实快了,但是。。。 会出现副作用,会显示类似任务后台执行信息:
work@zz_console 20:24:39 ~ >
echo 1 &
[1] 13013
1
work@zz_console 20:24:44 ~ >
[1]+ Done echo 1
work@zz_console 20:24:44 ~ >
重定向都无济于事。怎么办?为了消除这些信息,自然又想到了子进程 () :
Jun@VAIO 10.252.182.238 19:48:28 ~ >
(echo 1 &)
1
Jun@VAIO 10.252.182.238 20:28:13 ~ >
提示信息看起来完美解决了,但是新的问题又出来了:无法用 wait 等待后台进程执行完毕之后主进程再继续执行。这个问题怎么解决呢? 从 superuser 上的答案来看,又提到了新的思路:
set +m: +m Job control is closed.
但是实际试了下也不行,只能隐去最后一条 Done 的完成信息,初始的信息并不会隐去:
Jun@VAIO 10.252.182.238 20:37:02 ~ >
set +m
Jun@VAIO 10.252.182.238 20:39:52 ~ >
echo 1 &
[1] 3148
Jun@VAIO 10.252.182.238 20:39:57 ~ >
1
Jun@VAIO 10.252.182.238 20:39:58 ~ >
最后 stackoverflow 有人给了个不错的思路:用函数即可解决,因为当前后台任务的提示信息只会在当前shell显示,而函数 {} 创建了子shell/bash,所以不会在当前shell显示提示信息。
function a {
echo "I'm background task $1"
sleep 5
}
function b {
for i in {1..10}; do
a $i &
done
wait
} 2>/dev/null
$ b
I'm background task 1
I'm background task 3
I'm background task 2
I'm background task 4
I'm background task 6
I'm background task 7
I'm background task 5
I'm background task 9
I'm background task 8
I'm background task 10
#And there's a delay of 5 seconds before I get my prompt back.
不过我实际试了下,仅用函数其实并不能完美的解决上述后台等待和副作用的问题,我这里最终用 {} 做匿名函数创建子shell的方式完美的解决了这个问题,让提示信息不在当前shell 显示,并且能用wait等待。最终代码如下:
zzcmd(){
start_time=`date +%s`
c=0
[[ $1 == "" || $2 == "" ]] && cmd_usage && return
ips="`echo "$1"|grep -Po '((d{1,3}.){3}d{1,3})'`"
[[ $ips == "" ]] && ips="$(eval echo "$$1")"
[[ $ips == "" ]] && ips="`cat $1`"
set +m
nanoseconds=`date +%N`
for ip in $ips
do
[[ $ip == "" || $ip == " " || ! $ip =~ ^[0-9]+.[0-9]+.[0-9]+.[0-9]+$ ]] && redEcho "$ip ip invalid..." && continue
{ timeout 15 ssh work@$ip "echo -e "e[41;37;1m ------------------- ${ip} -------------------e[0mn";$2; exit" > ${nanoseconds}_$ip & } 2>/dev/null
((c++))
done
wait
cat ${nanoseconds}_*
rm -f ${nanoseconds}_*
set -m
end_time=`date +%s`
cost_time=$(($end_time-$start_time))
#cost_time_pretty=`date -d@$cost_time +%M"min"%S`
greenEcho "******************* 本次执行 $c 台机器,耗时 ${cost_time}s *******************"
}
总结:
解决问题的关键在于 {} 和 () 的区别,外加 set +m:
- {} 是匿名函数,创建了子 shell 来执行命令
- () 是在当前shell下创建了子进程来执行命令
- set +m 关闭后台任务控制信息显示
后记:
当然了也有很多第三方的工具和库也可以解决这个问题,比如 Ansible、puppet 等自动化运维管理工具,还有GNU的paralle程序等,但都没有这个方便和易于理解。
Refer:
[1] Running bash commands in the background without printing job and process ids
[2] Preventing bash from displaying “Done” when a background command finishes executing
[3] Bash脚本实现批量作业并行化
[4] GNU Parallel指南
https://my.oschina.net/enyo/blog/271612
[5] GNU parallel
http://about.uuspider.com/2015/09/22/parallel.html
[6] GNU Parallel
http://www.voidcn.com/blog/huozhanfeng/article/p-3006344.html
[7] 如何利用多核CPU来加速你的Linux命令 — awk, sed, bzip2, grep, wc等
http://www.vaikan.com/use-multiple-cpu-cores-with-your-linux-commands/
[8] Bash脚本15分钟进阶教程
http://www.vaikan.com/bash-scripting/
- Git命令速记
- linux设备驱动第三篇:如何写一个简单的字符设备驱动
- Tensorflow高级API的进阶--利用tf.contrib.learn建立输入函数
- Spring速查手册(三)——Spring+JDBC
- [WebKit] JavaScriptCore解析--基础篇(一)字节码的生成及抽象语法树的构建详情分析
- Spring速查手册(二)——Bean的作用域
- pyTorch自然语言处理简单例子
- 一文初探Tensorflow高级API使用(初学者篇)
- Spring速查手册——Bean装配
- 回溯法(一)——n皇后问题
- 图的邻接表示法Java版
- 图的遍历(BFS+DFS)
- 工厂模式
- Oracle 12.2新特性 | 基于权重的节点驱逐
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 因用了Insert into select语句,美女同事被开除了!
- 【原创】Java并发编程系列33 | 深入理解线程池(上)
- 算法篇:二分法之k个数之和
- 记一次循环依赖踩坑
- 为什么大家都说 SELECT * 效率低
- 一些恶心的代码片段,吐了....
- 大数据入门:Spark+Kudu的广告业务项目实战笔记(二)
- 给女同事讲完代理后,女同事说:你好棒哦
- 为什么我要从 Windows 切换到 Linux?
- 大白话带你认识 ZooKeeper !重要概念一网打尽!
- 如何在 Node.js 中判断一个文件是否存在?
- 我参与了两个接近100k+star的开源项目!聊聊开源项目贡献指南
- 最新更新 | Kafka - 2.6.0版本发布新特性说明
- 利用Libra进行机器学习和深度学习
- I2C总线架构 之 总线驱动