序言
无论在传统的企业级系统维护还是在互联网运维中,Shell 脚本的编写与维护常常必不可少,在系统管理员或开发人员工作中占比重比较大的一部分。Shell 脚本的严格语法格式对于一般的运维人员来说,常常会在一不留神下而抓狂或查找半天才发现是因为多了或少了一个空格或某语包括号不匹配而导致的错误,不但大大的浪费了脚本维护人员的工作时间,还可能影响到工程进度甚至项目的发布里程碑等。当然,对于非纯 Geek 来说,最重要的还是影响心情,特别是对于一些较复杂的脚本需求,更是必须小心谨慎,因此越来越多的开发人员必须借助于 Python、Perl、Ruby 等相关的脚本语言来实现,但是常由于平台的特性或者语言的限制,对系统级的命令调用或者异常处理有限制,最终解决起来并不是十分优雅。 NodeJS 的出现或许会给这些开发人员带来一些新的选择。
NodeJS 从诞生起发展非常迅速,社区活动非常活跃,目前扩展模块达到 1500 多个,并且每天都有不同的模块提交。它是构建在 JavaScript 引擎 V8 之上的 JavaScript 环境,它采用基于单线程的异步事件驱动 I/O 模型,具有非常高的性能,同时能够支持多种平台。日前国外的很多大的软件或互联网公司如 Microsoft,ebay,yahoo 等都在使用 NodeJS,国内的网易,淘宝,新浪等互联网企业也有很多分享和成功的线上案例应用。言归正传,希望下文的内容能给不熟悉或不喜欢 nix 平台 Shell 脚本开发或 WIN 平台下的批处理编写的工程师带来一些帮助,为简单起见,本文采用 Nix 平台为示例,WIN 平台的用户请参考自行修改或与作者联系。
首先,我承认 Shell 脚本中系统命令再加上 sed,awk 等瑞士军刀在一起工作已经相当强大,如果你想了解 NodeJS 的强大之处和如何结合 Shell 产生强大的工作效率,并且还能具有很好的灵活性,那就让我们继续旅程吧:
示例
先看一段简单的采用 Shell 脚本执行一段命令得到其执行时间的脚本 diffa.sh:
#!/bin/bash START=$(date +%s) # prepare things du -m /home > /tmp/output # done END=$(date +%s) DIFF=$(( $END - $START )) echo " 化了 $DIFF 秒搞定 " chmod +x diff.sh sh diff.sh
执行上面的脚本后,结果如下:
化了 0 秒搞定
用户首次执行一般会耗时几秒,多次执行的结果可能会在 0-1 秒之间随机显示。因为 du 的输出重定向,整个脚本的执行时间非常短,并且脚本中采用的是秒数级别的范围,如果需要得到这个脚本的准确执行时间只能用纳秒来进行,并在 Shell 做除法运算,把脚本修改一下 diffb.sh。
START=$(date +%s%N) du -m /home/ >/tmp/output END=$(date +%s%N) DIFF=$(($END - $START)) SUM=`expr $DIFF / 1000000` echo " 化了 $SUM MS 搞定 "
执行一下上面的 diffb 脚本就可以得到运行的结果了,需要提醒的是上面的脚本中各表达式的格式都是即定的,如果开发人员不小心多了一个空格或少了一空格,都将导致脚本错误。下面采用 NodeJS 来试试看,首上下载与安装 NodeJS 环境,过程非常简单,具体请参考官方网站或直接 apt-get 之类的操作。编写如下 diffc.js 脚本:
#!/usr/bin/env node var util = require('util'), spawn = require('child_process').spawn, ls = spawn('du', ['-m', '/home/']); var start = +new Date(); ls.stdout.on('data', function (data) { //console.log('stdout: ' + data); }); ls.stderr.on('data', function (data) { console.log('stderr: ' + data); }); ls.on('exit', function (code) { var end = +new Date(); console.log(end - start); });
注:上面 require 中引用的都是系统内置模块,spawn 的格式为 spawn(command, [args], [options]),其他请参阅官方文档。
同样,chmod +x 对脚本赋予执行权限,执行脚本./diff.js,结果如下:
1113
上面示例中显示的时间直接是毫秒级别,代码格式没有严格的限制,流程的控制也会更加灵活,特别是在异常情况下,可以根据用户的需求处理更小的细节。当然,我承认这个示例需求有些诡异,但是做这样的比较,并不是说要二者一决高下,只是换一种前端攻城师喜欢的方法去实现一些系统运维需求。在这里 NodeJS 脚本本身也是依赖于系统 Shell 的强大基础之上。
深入一点
以上示例可以看到,在 Shell 环境中,NodeJS 内置模块实现常用的功能是即方便又灵活,Linux Shell 环境中比较强大的功能之一就是支持输入输出重定向功能,用符号 < 和 > 来表示。0、1 和 2 分别表示标准输入、标准输出和标准错误信息输出, 用来指定需要重定向的标准输入或输出,比如 2>error.log 表示将错误信息输出到文件 err.log 中。类似的,NodeJS 中可以直接采用超复杂的命令来搞定,一般对于我们这些非系统管理员有一定的难度,下面引入强大点的模块 procstreams ,它可以实现输出流重定向等功能,首先用户需要执行 npm install procstreams 安装模块,编写示例如下 wc.js:
#!/usr/bin/env node var p = require('procstreams'); p('cat app.log').pipe('wc -l').data(function (stdout, stderr) { console.log(stdout); });
wc.js 脚本代码是借助于 Shell 命令实现统计 app.log 的行数,相当于 Shell 环境中的 cat app.log | wc -l 功能,输出的结果可以再根据需要再进行灵活处理,另外它还支持 then、and 和 or 等操作,类似 Shell 脚本中的 ;、&& 和||操作。在实现复杂或交互的功能时,甚至可以完全采用交互的方式进行操作输入。
另外,用户执行脚本的时候还需要处理复杂一些的参数对应, node-optimist 及 isaacs’s nopt 之类的模块可以非常简单的帮助攻城师实现这样的功能,如实现根据用户的输入的参数执行需要的系统命令,并可以做相关的逻辑处理的 opt.js:
#!/usr/bin/env node var util = require('util'), spawn = require('child_process').spawn; var argv = require('optimist').argv; var cmd = argv.cmd; var args = argv.args var option = argv.opt var ls = spawn(cmd, [args, option]); ls.stdout.on('data', function (data) { if (!data || !! data) console.log(' i believe it'); }); ls.stderr.on('data', function (data) { console.log('It\'s a miracle!'); }); ls.on('exit', function (code) { console.log('it.justHappened();'); });
用户使用如下对应格式执行代码:./opt.js --cmd=ls --args=/m --opt=/home, 然后只需要在代码相关处添加对应的逻辑代码,把注意力放在业务层,采用 js 的流程控制实现业务逻辑的分离。
实际应用
在企业线上或系统运维中,常需要对一些进程进行监控和报警,以便通知相关系统管理人员,如下 Shell 脚本 agenta.sh 实现了对 tomcat6 进程监控,如果不存在自动重启。
#!/bin/sh pid=`ps aux| grep "tomcat6" | grep -v grep | sed -n '1P' | awk '{print $2}'` if [ -z $pid ]; then echo "begin restart,please waiting..." sudo /etc/init.d/tomcat6 restart exit 1 else echo -e "exist ,don't need restart" fi
脚本编写人员在经过一番努力与折腾后,完成了代码编写与调试工作,然后需要通过系统的 crontab 功能添加如 0-59/2 * * * * sh agent.sh 的定时任务,如果系统管理员把 crontab 的权限给禁用了,那就需要得到系统管理员的帮助了。下面使用 Nodejs 来实现同样的功能,先假设读者对 grep、sed 和 awk 等常用命令的使用有大概了解,代码如下 agentb.js:
var p = require('procstreams'); var exec = require('child_process').exec; setInterval(function () { exec("ps aux| grep 'tomcat6' | grep -v grep | sed -n '1P' | awk '{print $2}'", function (err, output) { if (err) throw err; if (output.length > 0) console.log('exist,don\'t need restart'); else exec('sudo /etc/init.d/tomcat6 restart', function (err2, out2) {}); }) }, 1000 * 60 * 2);
示例代码中 setInterval 的函数的作用通过设置一个回调函数和间隔执行时间来实现定时监控。运行代码后,同样可以实现进程监控的功能,也许你会说上面的 Shell 命令还是很多的。因为你觉得直接使用 Shell 脚本会更简单,可是如果你经历过为空格或配置之类的调试过程,或者需求更加复杂时,采用 NodeJS 会让你觉得非常轻松。更重要的是,编写脚本后,在执行脚本时你可以直接通过 chrome debug 工具设置断点与单步调试,或者在 chrome 浏览器上进行图形化调试等操作,具体请参考 node-inspector。现在,agentb.js 代码中的 Shell 命令还是太长了太复杂,调试起来也不太方便,使用 procstreams 做一下简化,实现代码 agentc.js 如下:
var p = require('procstreams'); setInterval(function () { p("ps aux").pipe('grep tomcat6').pipe('grep -v grep').pipe('sed -n 1P').pipe("awk $2") .data(function (stdout, stderr) { if (stderr) throw stderr; if (stdout.length > 0) { console.log('exist,don\'t need restart'); } else { console.log('restart,waiting...'); p('sudo /etc/init.d/tomcat6 restart', function (stdout, stderror) { console.log(stdout); }); } }); }, 1000 * 60 * 2);
agentc 代码中通过 pipe 操作可以实现对每个步骤的输入进行详细的跟踪与调试,但是脚本中还是需要对系统的很多内置命令有大概的了解,需要对操作系统的相关功能或语法格式比较熟悉,使用起来还是有点不习惯。攻城师都喜欢编程时能控制住自己把握的,或者在使用简单的命令的情况下,就能实现需要的功能,再次简化代码后得到 agentd.js
var p = require('procstreams'); var serviceName = 'tomcat6'; var interval = 1000 * 60 * 2; setInterval(function () { p("ps aux").pipe('grep ' + serviceName).data(function (stdout, stderr) { if ( !! stdout && stdout.indexOf(serviceName) == 0) { console.log('exist,don\'t need restart'); } else { console.log('restart,waiting...'); p('sudo /etc/init.d/tomcat6 restart', function (stdout, stderror) {}); } }); }, interval);
在经过这次修改之后,对系统命令的掌握程度要求明显更低了,题外话,用户对系统命令了解的越详细越好,但如果使用简单即美的指导去实现同样的需求,何乐而不为。代码中 serviceName 和 interval 参数可以通过 node-optimist 模块动态给定,这样就可以实现一份代码监控多个进程,并且不需要系统管理员的帮助去添加定时任务的操作。当然,希望这样操作不会影响系统功能或在权限范围内。
总结
尽管 Linux 的 Shell 环境编程非常的强大,但是编写或调试 Shell 脚本时常令人抓狂不已,也没有很好的图形化调试工具。当然脚本较复杂时,尤其在需求跨平台时,脚本改动比常较大,日前,开发人员需要根据平台的不同,准备多套脚本代码,如 tomcat,apache 等,如果采用简单 Shell 和 NodeJS 结合编程,或许只需要把平台相关的命令提取出来,只需较少改动就能实现跨平台,可以大大提高工作效率与减少浪费攻城师的时间。个人认为,采用二者结合的方式具有以下优点:
- 采用 v8 引擎,轻量级模块,较好跨平台性,较底层的系统操作,在系统监控运维等方面具有明显优势,
- 采用事件驱动非阻塞 IO 模型,无线程上下文切换和锁操作,, 可利用多核 CPU 计算,性能较高,
- 开放源代码,社区活跃,模块丰富,底层的扩展实现也较方便。
随着 NodeJS 不断发展和成熟,国内外厂商越来越多的成功案例与分享,在企业级和互联网系统应用开发和维护中具有更广阔的前景。
参考资料
- http://NodeJS.org
- http://www.infoq.com/cn/articles/tyq-NodeJS-event
- http://www.catonmat.net/blog/
- http://www.cNodeJS.org
关于作者
尧飘海,开发工程师,现就职于网易杭州研究院,目前和小组正致力于 NodeJS 之上的移动游戏引擎和系统运维等方面的研发工作,对服务端的架构设计与应用感兴趣。个人 Github 地址: http://github.com/ piaohai。
感谢郑柯对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论