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[0m\n\";$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

https://stackoverflow.com/questions/7686989/running-bash-commands-in-the-background-without-printing-job-and-process-ids

[2]Preventing bash from displaying “Done” when a background command finishes executing

https://superuser.com/questions/305933/preventing-bash-from-displaying-done-when-a-background-command-finishes-execut

[3]Bash脚本实现批量作业并行化

http://bit.ly/2qZ8CZV

[4]GNU Parallel指南

http://www.jb51.cc/article/p-hfnjtnps-bog.html

[5]GNU parallel

http://about.uuspider.com/2015/09/22/parallel.html

[6]GNU Parallel

http://www.jb51.cc/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/

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


用的openwrt路由器,家里宽带申请了动态公网ip,为了方便把2280端口映射到公网,发现经常被暴力破解,自己写了个临时封禁ip功能的脚本,实现5分钟内同一个ip登录密码错误10次就封禁这个ip5分钟,并且进行邮件通知使用步骤openwrt为19.07.03版本,其他版本没有测试过安装bashmsmtpopkg
#!/bin/bashcommand1&command2&wait从Shell脚本并行运行多个程序–杨河老李(kviccn.github.io)
1.先查出MAMP下面集成的PHP版本cd/Applications/MAMP/bin/phpls-ls 2.编辑修改.bash_profile文件(没有.bash_profile文件的情况下回自动创建)sudovim~/.bash_profile在文件的最后输入以下信息,然后保存退出exportPATH="/Applications/MAMP/bin/php/php7.2.20/b
1、先输入locale-a,查看一下现在已安装的语言2、若不存在如zh_CN之类的语言包,进行中文语言包装:apt-getinstalllanguage-pack-zh-hans3、安装好后我们可以进行临时修改:然后添加中文支持: locale-genzh_CN.UTF-8临时修改> export LC_ALL='zh_CN.utf8'> locale永久
BashPerlTclsyntaxdiff1.进制数表示Languagebinaryoctalhexadecimalbash2#[0~1]0[0~7]0x[0~f]or0X[0~f]perl0b[0~1]0[0~7]0x[0~f]tcl0b[0~1]0o[0~7]0x[0~f]bashdifferentbaserepresntationreference2.StringlengthLanguageStr
正常安装了k8s后,使用kubect工具后接的命令不能直接tab补全命令补全方法:yum-yinstallbash-completionsource/usr/share/bash-completion/bash_completionsource<(kubectlcompletionbash)echo"source<(kubectlcompletionbash)">>~/.bashrc 
参考这里启动jar包shell脚本修改过来的#!/bin/bash#默认应用名称defaultAppName='./gadmin'appName=''if[[$1&&$1!=0]]thenappName=$1elseappName=$defaultAppNamefiecho">>>>>>本次重启的应用:$appName<
#一个数字的行#!/bin/bashwhilereadlinedon=`echo$line|sed's/[^0-9]//g'|wc-L`if[$n-eq1]thenecho$linefidone<1.txt#日志切割归档#!/bin/bashcd/data/logslog=1.logmv_log(){[-f$1]&&mv$1$2
#文件增加内容#!/bin/bashn=0cat1.txt|whilereadlinedon=[$n+1]if[$n-eq5]thenecho$lineecho-e"#Thisisatestfile.\n#Testinsertlineintothisfile."elseecho$linefidone#备份/etc目录#
# su - oraclesu: /usr/bin/ksh: No such file or directory根据报错信息:显示无法找到文件 /usr/bin/ksh果然没有该文件,但是发现存在文件/bin/ksh,于是创建了一个软连接,可以规避问题,可以成功切换到用户下,但无法执行系统自带命令。$. .bash_profile-ksh: .: .b
history显示历史指令记录内容,下达历史纪录中的指令主要的使用方法如果你想禁用history,可以将HISTSIZE设置为0:#exportHISTSIZE=0使用HISTIGNORE忽略历史中的特定命令下面的例子,将忽略pwd、ls、ls-ltr等命令:#exportHISTIGNORE=”pwd:ls:ls-ltr:”使用HIS
一.命令历史  1.history环境变量:    HISTSIZE:输出的命令历史条数,如history的记录数    HISTFILESIZE:~/.bash_history保存的命令历史记录数    HISTFILLE:历史记录的文件路径    HISTCONTROL:     ignorespace:忽略以空格开头的命令
之前在网上看到很多师傅们总结的linux反弹shell的一些方法,为了更熟练的去运用这些技术,于是自己花精力查了很多资料去理解这些命令的含义,将研究的成果记录在这里,所谓的反弹shell,指的是我们在自己的机器上开启监听,然后在被攻击者的机器上发送连接请求去连接我们的机器,将被攻击者的she
BashOne-LinersExplained,PartI:Workingwithfileshttps://catonmat.net/bash-one-liners-explained-part-oneBashOne-LinersExplained,PartII:Workingwithstringshttps://catonmat.net/bash-one-liners-explained-part-twoBashOne-LinersExplained,PartII
Shell中变量的作用域:在当前Shell会话中使用,全局变量。在函数内部使用,局部变量。可以在其他Shell会话中使用,环境变量。局部变量:默认情况下函数内的变量也是全局变量#!/bin/bashfunctionfunc(){a=99}funcecho$a输出>>99为了让全局变量变成局部变量
1、多命令顺序执行;  命令1;命令2  多个命令顺序执行,命令之间没有任何逻辑联系&&  命令1&&命令2  逻辑与,当命令1正确执行,才会执行命令2||  命令1||命令2  逻辑或,当命令1执行不正确,才会执行命令2例如:ls;date;cd/home/lsx;pwd;who ddif=输入文件of=输
原博文使用Linux或者unix系统的同学可能都对#!这个符号并不陌生,但是你真的了解它吗?首先,这个符号(#!)的名称,叫做"Shebang"或者"Sha-bang"。Linux执行文件时发现这个格式,会把!后的内容提取出来拼接在脚本文件或路径之前,当作实际执行的命令。 Shebang这个符号通常在Unix系统的脚本
1、历史命令history[选项][历史命令保存文件]选项:-c:  清空历史命令-w:  把缓存中的历史命令写入历史命令保存文件 ~/.bash_historyvim/etc/profile中的Histsize可改存储历史命令数量历史命令的调用使用上、下箭头调用以前的历史命令使用“!n”重复执行第n条历史
目录1.Shell脚本规范2.Shell脚本执行3.Shell脚本变量3.1环境变量3.1.1自定义环境变量3.1.2显示与取消环境变量3.1.3环境变量初始化与对应文件的生效顺序3.2普通变量3.2.1定义本地变量3.2.2shell调用变量3.2.3grep调用变量3.2.4awk调用变量3.3
   http://www.voidcn.com/blog/wszzdanm/article/p-6145895.html命令功能:显示登录用户的信息命令格式:常用选项:举例:w显示已经登录的用户及正在进行的操作[root@localhost~]#w 11:22:01up4days,21:22, 3users, loadaverage:0.00,0.00,0.00USER