使用Perl发送邮件
在之前的监控系统中,邮件系统配置为只能本地调用mail命令发送,这样如果要在每台数据库服务上发送邮件就比较麻烦,需要通过ssh远程调用邮件服务器上的发送脚本。如果要发送文件,还要先将文件复制到邮件服务器上。
随着机器数量的增加,这种方式越来越不灵活。因此希望能通过传统的smtp的方式在任何主机上都能发送邮件,于是花了点时间研究了一下sendmail,根据这篇文章很轻松的让sendmail跑了起来:
修改sendmail配置模板/etc/mail/sendmail.mc
define(QUEUE_DIR, `/var/spool/mqueue/q*')dnl TRUST_AUTH_MECH(`EXTERNAL DIGEST-MD5 CRAM-MD5 LOGIN PLAIN')dnl define(`confAUTH_MECHANISMS', `EXTERNAL GSSAPI DIGEST-MD5 CRAM-MD5 LOGIN PLAIN')dnl dnl DAEMON_OPTIONS(`Port=smtp,Addr=127.0.0.1, Name=MTA')dnl DAEMON_OPTIONS(`Port=25, Name=MTA')dnl 第一行是新增,支持多个队列 第二三行去掉注释 第四行加上注释 第五行新增
根据模板生成配置文件
m4 /etc/mail/sendmail.mc > /etc/sendmail.cf
建立多个队列的目录
cd /var/spool/mqueue/ mkdir q1 q2 q3 q4 q5 q6
启动sendmail
/etc/rc.d/init.d/sendmail start
若要随机自启动sendmail,修改一下chkconfig设置即可
chkconfig --level 35 sendmail on
测试了一下发送邮件成功:
mail -s "test mail" NinGoo@test.com < /tmp/test.log
在CPAN上搜索了一下,Mail:Sender模块发送邮件比较方便,支持多附件,语法也比较简单。需要安装如下两个组件:
MIME::Base64 Mail::Sender
发送邮件的perl示例代码如下,为便于查看邮件发送的详细情况,这里开启了debug模式:
#!/usr/bin/perl -w
use Mail::Sender;
open my $DEBUG, ">> /tmp/mail.log" or die "Can't open the debug file: $!\n";
$sender = new Mail::Sender
{smtp => 'smtp_server',
from => 'send@mail.com'};
#发送普通邮件
$sender->MailMsg(
{to => 'NinGoo@test.com',
subject => 'test mail',
msg => 'hello word',
debug => $DEBUG
});
#发送带附件的邮件
$sender->MailFile(
{to => 'NinGoo@test.com',
subject => 'test mail with attached file',
msg => 'hello word',
file => '/tmp/test.txt'
debug => $DEBUG
});
经过测试,在邮件服务器本机发送成功,但在远程主机上则失败了,通过debug的log文件,发现错误如下:
>> 550 5.7.1 < NinGoo@test.com>... Relaying denied. Proper authentication required.
因为是内部监控使用,可以配置远程发送到特定的域的邮件无须认证,修改/etc/mail/relay-domains文件,每行加入一个目标域,如taobao.com。
修改配置后注意重启sendmail
/etc/rc.d/init.d/sendmail restart
发送正常的log如下:
>> 250 2.0.0 n69DbJwt011946 Message accepted for delivery
在Crontab中调度Perl程序的环境变量问题
在corntab中调度perl程序时,如何获取并设置环境变量是一大问题。在shell脚本中,我一般是通过直接调用~/.profile或者~/.bash_profile(视shell类型而定)来解决。而Perl中则没有一个现成的比较方便的方法。
老楼最近在一篇blog中给出了一种方法,基本上能解决上述问题,但主要是针对ORACLE_HOME,其他的还需要再写一个差不多的函数,并且还需要配置文件,通用性不是太好。
我的办法是,直接利用.profile或者.bash_profile,在perl中循环将所有的环境变量一次性设置好。这个设置过程,我写成了一个函数放在公共模块中,只需要在脚本中包含该模块,然后调用一次该函数即可。因为要兼容Oracle和MySQL,按照我们的安装规范,Oracle安装用户为oracle,而MySQL数据库的安装用户为mysql,相应的脚本使用相应的用户。自动化是需要建立在规范之上的,下面是函数代码:
sub set_env{
my $user=`whoami`;
chomp($user);
my $profile="/home/".$user."/.profile";
if (! -e $profile ){
$profile="/home/".$user."/.bash_profile"
}
open(NEWENV, ". $profile && env|");
while (){
if (/(\w+)=(.*)/){
$ENV{$1}="$2";
}
}
close NEWENV;
}
MySQL如何获取当前执行的SQL
在MySQL的客户端中通过show [full] processlist可以看到当前所有线程,以及线程的状态,当然也包括执行中的sql,但是显示的语句不完全,并且有很多空闲线程会造成干扰。
这里要介绍一个非常好用的开源工具innotop(sourceforge已经被墙了,要过去先找梯子),由于是用perl写的,所以需要安装相关的perl模块,包括DBI,DBD::mysql,Term::ReadKey和Time::HiRes,这些模块都可以到CPAN找到。
用innotop的Q模式则可以完美的解决获取当前运行的SQL的问题。innotop -m Q 或者innotop进入后再按shift+q进入Query list模式:
Query List (? for help) mysql01, 75+03:16:16, 774.20 QPS, 83 thd, 5.1.24-rc-log CXN When Load QPS Slow QCacheHit KCacheHit BpsIn BpsOut my120 Now 0.00 774.20 0 40.22% 100.00% 207.98k 1.46M my120 Total 0.00 212.69 2 29.70% 100.00% 56.90k 402.15k CXN Cmd ID User Host DB Time Query mysql01 Query 20936 poster 192.168.1.1 poster 00:00 select a.poster_id, a.pic_id, a.gmt_create, a.gmt_modified, a.url, a.no
然后按e并输入thread ID显示执行计划或者按f显示完整sql语句,或者按o显示系统优化过的语句(需要MySQL的版本支持EXPLAIN EXTENDED)。个人感觉,还是e最有用,其他两个选项,则有点鸡肋了。
Query List (? for help) mysql01, 75+03:16:16, 774.20 QPS, 83 thd, 5.1.24-rc-log
EXPLAIN PARTITIONS
select a.poster_id, a.pic_id, a.gmt_create, a.gmt_modified, a.url, a.notes,
a.type, a.indexed, a.user_id, a.user_nick, b.pic_path
from poster.poster_pic a inner join poster.picture b on
(a.pic_id = b.id) where a.poster_id = 3390 order by a.indexed
______________ Sub-Part 1 ______________ ________ Sub-Part 1 ________
Select Type: SIMPLE Select Type: SIMPLE
Table: a Table: b
Partitions: Partitions:
Type: ref Type: eq_ref
Poss. Keys: PRIMARY Poss. Keys: PRIMARY
Index: PRIMARY Index: PRIMARY
Key Length: 4 Key Length: 8
Index Ref: const Index Ref: poster.a.PIC_ID
Row Count: 14 Row Count: 1
Special: Using where; Using filesort Special:
Press e to explain, f for full query, o for optimized query
Query List (? for help) mysql01, 75+03:16:16, 774.20 QPS, 83 thd, 5.1.24-rc-log select a.poster_id, a.pic_id, a.gmt_create, a.gmt_modified, a.url, a.notes, a.type, a.indexed, a.user_id, a.user_nick, b.pic_path from poster.poster_pic a inner join poster.picture b on (a.pic_id = b.id) where a.poster_id = 3390 order by a.indexed Press e to explain, f for full query, o for optimized query
Query List (? for help) mysql01, 75+03:16:16, 774.20 QPS, 83 thd, 5.1.24-rc-log select a.poster_id, a.pic_id, a.gmt_create, a.gmt_modified, a.url, a.notes, a.type, a.indexed, a.user_id, a.user_nick, b.pic_path from poster.poster_pic a inner join poster.picture b on (a.pic_id = b.id) where a.poster_id = 3390 order by a.indexed Note: select `poster`.`a`.`POSTER_ID` AS `poster_id`,`poster`.`a`.`PIC_ID` AS `pic_id`,`poster`.`a`.`GMT_CREATE` AS `gmt_create`, `poster`.`a`.`GMT_MODIFIED` AS `gmt_modified`, `poster`.`a`.`URL` AS `url`,`poster`.`a`.`NOTES` AS `notes`, `poster`.`a`.`TYPE` AS `type`,`poster`.`a`.`INDEXED` AS `indexed`, `poster`.`a`.`USER_ID` AS `user_id`,`poster`.`a`.`user_nick` AS `user_nick`,`poster`.`b`.`PIC_PATH` AS `pic_path` from `poster`.`poster_pic` `a` join `poster`.`picture` `b` where ((`poster`.`b`.`ID` = `poster`.`a`.`PIC_ID`) and (`poster`.`a`.`POSTER_ID` = 3390)) order by `poster`.`a`.`INDEXED` Press e to explain, f for full query, o for optimized query
那么innotop是从哪里取的数据呢?应该是通过information_schema.processlist来获得完整的sql语句,并且根据COMMAND来过滤掉空闲线程的。
mysql> desc information_schema.processlist; +---------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +---------+-------------+------+-----+---------+-------+ | ID | bigint(4) | NO | | 0 | | | USER | varchar(16) | NO | | | | | HOST | varchar(64) | NO | | | | | DB | varchar(64) | YES | | NULL | | | COMMAND | varchar(16) | NO | | | | | TIME | bigint(7) | NO | | 0 | | | STATE | varchar(64) | YES | | NULL | | | INFO | longtext | YES | | NULL | | +---------+-------------+------+-----+---------+-------+ 8 rows in set (0.00 sec)
用Perl的hash数组实现个性化监控
对于DBA来说,一个准确稳定的监控系统,不啻于一柄尚方宝剑。几十上百套系统,如果每天都靠人工来检查,工作量之大无法想象,而且人工也无法做到实时捕获错误。
但是这么多数据库系统,每个库承载的压力不一样,对于整个系统的重要度也不一样,负责的DBA也不可能是同一个人。如果都按同样的KPI同样的门限来做监控,则有些重要的系统可能无法准确的告警,有些不重要的系统却又会频繁误报。
比如系统的load,有些核心库由于采用了比较高端的硬件,即使一直在20~30左右都是正常的,而一些边缘的库则可能超过5就比较危险了,所以对于load,不同的库必须设置不同的检查门限。
如果要通过shell脚本来实现这个需求,可能需要写一堆的if或者case判断,一旦要做配置变更就十分的头疼。不过perl的hash数组可以很好的解决这个问题。
#!/usr/bin/perl -w
# creator: NinGoo
############################################################
use strict;
# 配置各主机各KPI的检查门限
# 这里配置了两个门限值,达到第一个值发IM提醒,达到第二个值发手机短信
# server, load1 load2
my %cutoff=(“db1″, [ 30, 40 ],
“db2″, [ 20, 35 ],
“db3″, [ 10, 18 ]
);
# 配置各个门限值的数组中的位置,这样在以后即使修改门限的位置,也不需要修改功能代码
# 将配置和功能分开,将极大的简化后续维护工作
my ($load1,$load2)=(0,1);
############################################################
# 检查load的函数
sub check_load{
my($server)=@_;
my $load=get_server_load(); #获得数据库主机当前load,具体实现这里就不赘述了
my $cutoff1=$cutoff{$server}[$load1];
my $cutoff2=$cutoff{$server}[$load2];
if ( $load > $cutoff2){
my $mesg=$server.” load more than “.$cutoff2.”,now is “.$load.”\n”;
send_mobile($server,$mesg); # 大于load2,发送手机告警
}
elsif( $load > $cutoff1){
my $mesg=$server.” load more than “.$cutoff1.”,now is “.$load.”\n”;
send_wangwang($server,$mesg); # 大于load1,发送旺旺告警
}
}
############################################################
#循环检查所有主机的load
foreach my $server(keys(%cutoff)){
print “—- “.$server.” —–\n”;
check_load($server);
}
在send_mobile和send_wangwang实现告警发送的函数中,传入了$server参数,同样可以使用hash数组配置发送给不同的责任人,基本思想和这里没有大的差别,就不重复贴代码了。
