当Shell遇上了NodeJS

序言

无论在传统的企业级系统维护还是在互联网运维中,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-optimistisaacs’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结合编程,或许只需要把平台相关的命令提取出来,只需较少改动就能实现跨平台,可以大大提高工作效率与减少浪费攻城师的时间。个人认为,采 用二者结合的方式具有以下优点:

  1. 采用v8引擎,轻量级模块,较好跨平台性,较底层的系统操作,在系统监控运维等方面具有明显优势,
  2. 采用事件驱动非阻塞IO模型,无线程上下文切换和锁操作,, 可利用多核CPU计算,性能较高,
  3. 开放源代码,社区活跃,模块丰富,底层的扩展实现也较方便。

随着NodeJS不断发展和成熟,国内外厂商越来越多的成功案例与分享,在企业级和互联网系统应用开发和维护中具有更广阔的前景。

参考资料

关于作者

尧飘海,开发工程师,现就职于网易杭州研究院,目前和小组正致力于NodeJS之上的移动游戏引擎和系统运维等方面的研发工作,对服务端的架构设计与应用感兴趣。个人Github地址:http://github.com/piaohai


感谢郑柯对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

This entry was posted in Best Practices, OS. Bookmark the permalink.

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s