由于最近在搭建Zabbix监控服务,需要制作各类监控的模板,如iostat、Nginx、MySQL等,因此会写一些脚本来完成数据采集的工作。又因为近期对Perl语言比较感兴趣,因此决定花些时间学一学,写一个脚本来练练手,于是就有了这样一份笔记。
需求描述
我们将编写一个获取JVM虚拟机状态信息的脚本:
- 启动一个服务进程,通过套接字接收形如“JVMPORT 2181”的请求;
- 执行
netstat
命令,根据端口获取进程号; - 执行
jstat
命令获取JVM的GC信息;jstack
获取线程信息;ps -o pcpu,rss
获取CPU和内存使用情况; - 将以上信息返回给客户端;
之所以需要这样一个服务是因为Zabbix Agent会运行在zabbix用户下,无法获取运行在其他用户下的JVM信息。
此外,Zabbix Agent也需要编写一个脚本来调用上述服务,这个在文章末尾会给出范例代码。
Hello, world!
还是要不免俗套地来一个helloworld,不过我们的版本会稍稍丰富些:
1 | #!/usr/bin/perl |
将该文件保存为hello.pl
,可以用两种方式执行:
1 | $ perl hello.pl |
- 所有的语句都以分号结尾,因此一行中可以有多条语句,但并不提倡这样做。
use
表示加载某个模块,加载strict
模块表示会对当前文件的语法做出一些规范和约束。比如将my $name ...
前的my
去掉,执行后Perl解释器会报错。建议坚持使用该模块。$name
,一个Perl变量。$
表示该变量是一个标量,可以存放数值、字符串等基本类型。其它符号有@
和%
,分别对应数组和哈希表。my
表示声明一个变量,类似的有our
、local
等,将来接触到变量作用域时会了解。- 字符串可以用单引号或双引号括起来,区别是双引号中的变量会被替换成实际值以及进行转移,单引号则不会。如
'Hello, $name!\n'
中的$name
和\n
会按原样输出,而不是替换为“Jerry”和换行符。 print
语句用于将字符串输出到标准输出上。#
表示注释。
正则表达式
我们第一个任务是从“JVMPORT 2181”这样的字符串中提取“2181”这个端口号。解决方案当然是使用正则,而且Perl的强项之一正是文本处理:
1 | my $line = 'JVMPORT 2181'; |
这里假设你知道如何使用正则表达式。
=~
运算符表示将变量和正则表达式进行匹配,如果匹配成功则返回真,失败则返回假。- 匹配成功后,Perl会对全局魔术变量——
$0
至$9
进行赋值,分别表示正则表达式完全匹配到的字符串、第一个子模式匹配到的字符串、第二个子模式,依此类推。 if...else...
是条件控制语句,其中...} else if (...
可以简写为...} elsif (...
。
调用命令行
使用反引号(即大键盘数字1左边的按键):
1 | my $uname = `uname`; |
对于返回多行结果的命令,我们需要对每一行的内容进行遍历,因此会使用数组和foreach
语句:
1 | my $pid; |
$pid
变量的结果是2181端口对应的进程号。- 这个正则可能稍难理解,但对照
netstat
的输出结果来看就可以了。 foreach
是循环语句的一种,用来遍历一个数组的元素,这里则是遍历netstat
命令每一行的内容。注意,foreach
可以直接用for
代替,即for my $line (@netstat) { ... }
。last
表示退出循环。如果要进入下一次循环,可使用next
语句。
数组
下面我们要根据进程号来获取JVM的GC信息:(“2017”为上文获取到的进程号)
1 | $ jstat -gc 2017 |
如何将以上输出结果转换为以下形式?
1 | s0c 192.0 |
这时我们就需要更多地使用数组这一数据结构:
1 | my $pid = 2017; |
- 使用
$jstat[0]
获取数组的第一个元素,数组的下标从0开始,注意这里的$
符号,而非@
。 $#kv_keys
返回的是数组最大的下标,而非数组的长度。for my $i (0 .. 10) {}
则是另一种循环结构,$i
的值从0到10(含0和10)。
对于正则表达式,这里也出现了两个新的用法:
s/A/B/
表示将A的值替换为B,上述代码中是将首尾的空格去除;split(A, B)
函数表示将字符串B按照正则A进行分割,并返回一个数组。
另外在学习过程中还发现了这样一种写法:
1 | my @jstat; |
map
函数会对数组中的每个元素应用第一个参数指向的函数(这里是一个匿名函数),当需要处理的数组元素很多时,这种是首选做法。具体内容读者可以自己去了解。
函数
我们可以用以下命令来获取指定进程的CPU和内存使用率:
1 | $ ps -o pcpu,rss -p 2017 |
格式和jstat
是一样的,为了不再写一遍上文中的代码,我们可以将其封装为函数。
1 | sub kv_parse { |
sub
表示定义一个函数(subroutine),和其他语言不同的是,它没有参数列表,获取参数使用的是魔术变量@_
:
1 | sub hello { |
$_[0]
和shift @_
返回的都是第一参数。不同的是,shift
函数会将这个参数从@_
数组中移除;shift @_
和shift(@_)
是等价的,因为调用函数时参数列表可以不加括号;shift @_
和只写shift
也是等价的,该函数若不指定参数,则默认使用@_
数组。&
符号也是比较特别的,主要作用有两个:一是告诉Perl解释器hello
将是一个用户定义的函数,这样就不会和Perl原生关键字冲突;二是忽略函数原型(prototype)。具体可以参考这篇文章:Subroutines Called With The Ampersand。
当传递一个数组给函数时,该数组不会被作为@_
的第一个元素,而是作为@_
本身。这也是很特别的地方。当传递多个数组,Perl会将这些数组进行拼接:
1 | sub hello { |
小结
对于初学者来讲,本文的信息量可能有些大了。但如果你已经有一定的编程经验(包括Bash),应该可以理解这些内容。
Perl文化的特色是“不只一种做法来完成一件事情”,所以我们可以看到很多不同的写法。但也有一些是大家普遍接受的写法,所以也算是一种规范吧。
下一章我们会继续完成这个监控脚本。
PS:本文的示例代码可以从Github中下载。