一聚教程网:一个值得你收藏的教程网站

热门教程

Java 服务端监控方案(Ganglia Nagios Java 篇)

时间:2022-06-29 00:23:22 编辑:袖梨 来源:一聚教程网

Java 服务端监控方案(一. 综述篇)

换成 Octopress 后,写文章比以前简单多了,博客访问起来速度也快了不少,借这个把过去一段时间的技术积累总结整理一下。作为开始,我准备写一个系列文章来总结一下去年到今年在公司做的一些关于 Java 服务端监控的工作。
对于任何一个服务端应用来说,监控都是至关重要的一环。一个系统在运行过程当中太容易出现故障,网络、存储、系统负载、软件 Bug,任何一个点出现问题都有可能影响到整个系统的稳定运行,因此,监控必不可少。一个完善的系统监控方案要从两个方面帮助我们:
不断检查各项服务的稳定性,出现问题第一时间通知相关人员
记录系统运行的各项指标,帮助运维人员全面掌握系统运行状况,从而做到防患于未然
对于第一个方面,其实就是要做到出了故障第一时间告警,从而能让系统在半夜出现问题的时候能一条短信把可怜的运维同学叫起来恢复服务。这件事, Nagios 基本上是最佳解决方案。它有花样繁多的各种插件,并且编写自定义插件也极其简单,可以很方便地监控从操作系统到应用的方方面面。其配置文件简洁易懂,且十分强大,稍加熟悉就可以很容易根据自己的需要去配置。
对于第二个方面,Nagios 就不是特别适合了。它的工作方式是每隔一段时间去检查服务是否正常,无法记录一些指标,不方便运维去观察系统各种指标的变化情况。当然,通过一些插件似乎能实现这个功能,但是我们选择了别的解决方案:在常用的 Graphite 和 Ganglia 之间,我最终选择了 Ganglia。
Ganglia 是加州大学伯克利分校发起的系统监控项目,为大规模高性能计算集群而设计,采用了 RRD、XML 等成熟的技术实现,单节点开销很小,提供了相当靠谱的容错机制,且很容易扩展和加入自定义的 Metric。Ganglia 的一个比较有名的用户是维基百科,可以通过 这里 访问他们的 Ganglia 实例,从上面可以看到维基百科的集群运行状况。
Ganglia + Nagios,这就是我们的监控系统所选用的解决方案。不过这样做有一个问题是两者都有自己的监控 Agent,Nagios 需要 NPRE,Ganglia 则需要 gmond。显然在每个机器上都安装两个监控系统不好,安装维护麻烦不说,也不符合一个系统只做一件事的原则。
好在这个问题早就有人想到了,办法很简单,在两者之间建立起一座桥梁,让 Nagios 可以直接使用 Ganglia 的数据就好了。解决方案有很多,我会在这个系列的 Nagios 部分详细介绍。
Ganglia 和 Nagios 可以直接监控到系统层面的运行状况如 CPU,Load,磁盘,内存等,但对于 Java 应用,我们还需要额外的手段来收集数据。Java 监控的标准解决方案是 JMX,JVM 本身的很多运行时参数都通过 JMX 暴露出来了,如内存、GC 等相关参数,开发者也可以很简单地自定义 MBean 来将应用本身的参数暴露出来。借助一些工具如 jmxtrans,我们可以定时获取一个 JVM 实例的 JMX 数据,并发送给 Ganglia 或者 Graphite 这种后端。在 Java 和 JMX 部分,我会详细介绍。
最近还发现了一个名叫 Metrics 的 Java 库,可以方便开发者来编写应用监控相关代码,并且它直接支持 Ganglia 和 Graphite 后端。如果不是必须使用 JMX 的话,这会是一个更简洁的替代方案。
简要介绍就到这里了,请期待后面针对 Ganglia、Nagios 和 Java 监控的更详细介绍。

二. Ganglia

Ganglia 是加州大学伯克利分校发起的系统监控项目,为大规模高性能计算集群而设计,采用了 RRD、XML 等成熟的技术实现,单节点开销很小,提供了相当靠谱的容错机制,且很容易扩展和加入自定义的 Metric。Ganglia 的一个比较有名的用户是维基百科,可以通过 这里 访问他们的 Ganglia 实例,从上面可以看到维基百科的集群运行状况。
1. 安装
我们的服务器环境是 Redhat Enterprise Linux 6.4 x86_64 版,本文基于这个发行版来介绍安装和配置。其他发行版或者架构也都大同小异。
Ganglia 主要包含三个组件:gmond、gmetad 和 ganglia-web。其中 gmond 是核心进程,负责发送和接收 metric,每个相关的节点上都需要安装 gmond。 gmetad 是聚合 metric 数据的服务,一般在一台机器上安装即可,如果要考虑高可用性,也可以安装到多台机器上。ganglia-web 则是基于PHP 的 Web 界面,一般只在一台机器上安装即可。
默认 gmond 是按多播的方式工作的,这样同一个网络内的每一个 gmond 都能接收到整个集群所有节点的 metric 数据。这样任何一个监控节点挂了,都可以切换到其他任意存活节点上。不过我们的集群规模较小,没有采取这个方案,而是采用单播的方式,由一个节点接收所有数据,也只有这个节点上安装了 gmetad 和 ganglia-web。这样做比较简单,且实践中稳定性完全足够了。
因此,按照部署方式,gmetad 可能是不需要的,下面的步骤中关于 gmetad 的部分可以忽略。
1.1 安装依赖
首先确保下面的包都正确安装了,这些都能在官方源里有(如果没有购买 RHEL 的授权,可以配置使用 CentOS 的源,也一样能用)。

 yum -y install apr-devel apr-util check-devel cairo-devel
   pango-devel libxml2-devel rpmbuild glib2-devel
   dbus-devel freetype-devel fontconfig-devel gcc-c++
   expat-devel python-devel libXrender-devel pcre-devel
   perl-ExtUtils-MakeMaker
其次需要安装 libconfuse,这个库虽然在源里有,但版本貌似比较老,我是通过 RPM find 找到 RPM 直接安装的。注意 libconfuse 和 libconfuse-devel 都需要安装,因为编译 Ganglia 时也需要使用。
最后需要安装 rrdtools,这个源里没有,只能自己编译安装。在其官网下载最新版本的源码,解压后按下面的步骤编译安装即可。

./configure --prefix=/usr
make
sudo make install
1.2 编译 Ganglia
从 Ganglia 官网下载 最新版本的源码 ,解压后按照下面的步骤编译安装:

./configure --with-gmetad
make
sudo make install
按这个配置 Ganglia 会被安装到 /usr/local 下,其配置文件在 /usr/local/etc 下。
安装好以后强烈建议把 Ganglia 的两个组件 gmond 和 gmetad 的服务脚本拷贝到 /etc/init.d 下,方便使用 service 和 chkconfig 来管理 Ganglia 的服务。这两个脚本位于源码目录下的 gmond/gmond.init 和 gmetad/gmetad.init。拷贝之前注意修改这两个脚本里的 GMOND 和 GMETAD 两个环境变量的值为正确的可执行文件路径,默认它们都在 /usr/sbin 下,要修改成 /usr/local/sbin。

sudo cp gmond/gmond.init /etc/init.d/gmond
sudo cp gmetad/gmetad.init /etc/init.d/gmetad
2. 配置
注意这里不会提到所有的配置项,而只是列出了关键的部分。实际操作时,请直接在默认配置文件的基础上修改,里面都有详细的注释对配置项作出了解释。
2.1 gmond 配置
gmond 配置文件在 /usr/local/etc/gmond.conf 。
cluster 名称的配置,主要是 name 和 owner,自定义即可。同一个集群下所有的 gmond 都要配置成一样的。

/*
 * The cluster attributes specified will be used as part of the
 * tag that will wrap all hosts collected by this instance.
 */
cluster {
  name = "my-cluster"
  owner = "jerry"
  latlong = "unspecified"
  url = "unspecified"
}
host 配置。注意这个只是给每个 host 取个名字而已,也可以随便命名,不过建议直接实用主机名。

/* The host section describes attributes of the host, like the location */
host {
  location = "host1"
}
多播模式配置
这个是默认的方式,基本上不需要修改配置文件,且所有节点的配置是一样的。这种模式的好处是所有的节点上的 gmond 都有完备的数据,gmetad 连接其中任意一个就可以获取整个集群的所有监控数据,很方便。
其中可能要修改的是 mcast_if 这个参数,用于指定多播的网络接口。如果有多个网卡,要填写对应的内网接口。

/* Feel free to specify as many udp_send_channels as you like.  Gmond
   used to only support having a single channel */
udp_send_channel {
  bind_hostname = yes # Highly recommended, soon to be default.
                       # This option tells gmond to use a source address
                       # that resolves to the machine's hostname.  Without
                       # this, the metrics may appear to come from any
                       # interface and the DNS names associated with
                       # those IPs will be used to create the RRDs.
  mcast_join = 239.2.11.71
  mcast_if = em2
  port = 8649
  ttl = 1
}

/* You can specify as many udp_recv_channels as you like as well. */
udp_recv_channel {
  mcast_join = 239.2.11.71
  mcast_if = em2
  port = 8649
  bind = 239.2.11.71
  retry_bind = true
  # Size of the UDP buffer. If you are handling lots of metrics you really
  # should bump it up to e.g. 10MB or even higher.
  # buffer = 10485760
}
单播模式配置
监控机上的接收 Channel 配置。我们使用 UDP 单播模式,非常简单。我们的集群有部分机器在另一个机房,所以监听了 0.0.0.0,如果整个集群都在一个内网中,建议只 bind 内网地址。如果有防火墙,要打开相关的端口。

/* Alternative UDP channel */
udp_recv_channel {
  bind = 0.0.0.0
  port = 8648
}
被监控的节点上的发送 Channel 配置。同样很简单,host 填写监控节点的 IP,端口填写上面配置的监听端口即可。其中 ttl 设定为 1,因为我们是直接发送到目标节点的,没有经过中间的 gmond 转发。

/* Feel free to specify as many udp_send_channels as you like.  Gmond
   used to only support having a single channel */
udp_send_channel {
  bind_hostname = yes # Highly recommended, soon to be default.
                       # This option tells gmond to use a source address
                       # that resolves to the machine's hostname.  Without
                       # this, the metrics may appear to come from any
                       # interface and the DNS names associated with
                       # those IPs will be used to create the RRDs.
  host = 192.168.221.9
  port = 8648
  ttl = 1
}
接下来是默认收集和发送的 metric 配置。这个保留默认即可。默认收集的 metric 包括:CPU、Load、Memory、Disk 和 Network,基本上涵盖了操作系统级的所有参数。下面是一个配置片段,如果我们要添加自定义监控 metric,也需要按这个格式来配置:

collection_group {
  collect_every = 40
  time_threshold = 180
  metric {
    name = "disk_free"
    value_threshold = 1.0
    title = "Disk Space Available"
  }
  metric {
    name = "part_max_used"
    value_threshold = 1.0
    title = "Maximum Disk Space Used"
  }
}
2.2 gmetad 配置
gmetad 的配置文件位于 /usr/local/etc/gmetad.conf。其中最重要的配置项是 data_source:

data_source "my-cluster" localhost:8648
如果使用的是默认的 8649 端口,则端口部分可以省略。如果有多个集群,则可以指定多个 data_source,每行一个。
最后是 gridname 配置,用于给整个 Grid 命名:

gridname "My Grid"
其他配置保留默认值即可。
2.3 ganglia-web 安装和配置
先从官网下载 ganglia-web 的源码包 并解压。如果使用 Apache 作为 Web 服务器,可以修改 Makefile 中相关的变量并执行 sudo make install 来安装。

# Location where gweb should be installed to (excluding conf, dwoo dirs).
GDESTDIR = /var/www/html/ganglia

# Gweb statedir (where conf dir and Dwoo templates dir are stored)
GWEB_STATEDIR = /var/lib/ganglia-web

# Gmetad rootdir (parent location of rrd folder)
GMETAD_ROOTDIR = /var/lib/ganglia

# User by which your webserver is running
APACHE_USER =  apache
我们使用 Nginx 作为 Web Server,可以直接解压安装包到目标位置,修改相关配置文件即可。要确保系统安装配置好了 Nginx 和 PHP。下面是一个样例 Nginx 配置:

server {
    listen 80;
    server_name monitor.xxx.com;
    charset utf8;
    access_log logs/$host.access.log main;
    root /var/www/ganglia;
    index index.php;
    include mime.server.common;
    auth_digest_user_file /var/www/ganglia/users.htdigest;

    location ~ .*.(php|php5)?$ {
        auth_digest 'XXX Monitor System';
        auth_digest_timeout 60s;
        auth_digest_expires 3600s;
        fastcgi_split_path_info ^(.+?.php)(/.*)$;
        if (!-f $document_root$fastcgi_script_name) {
                return 404;
        }
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi.conf;
        fastcgi_param ganglia_secret yourSuperSecret;
        if ($http_authorization ~ username="([^"]+)") {
            set $htdigest_user $1;
        }
        fastcgi_param  REMOTE_USER   $htdigest_user;
    }
}
请根据部署时的实际情况调整里面的相关参数如 ganglia-web 的路径,以及 PHP-FPM 的地址等。
注意我们是启用了认证的,使用的是 HTTP Digest 认证(需要安装第三方Nginx 模块)。使用自带的 HTTP Basic 认证也是可以的,但建议配合 HTTPS 使用以提升安全性。
其中 ganglia_secret 和 REMOTE_USER 这两个 fastcgi 参数是用于控制 Ganglia 认证的,其中 ganglia_secret 是一个自定义的串,随便填写即可。
ganglia-web 本身也需要配置才能使认证生效。在 ganglia-web 的目录里新增一个 conf.php 文件来写入配置:

#
# 'readonly': No authentication is required.
#             All users may view all resources.
#             No edits are allowed.
# 'enabled': Guest users may view public clusters.
#            Login is required to make changes.
#            An administrator must configure an
#            authentication scheme and ACL rules.
# 'disabled': Guest users may perform any actions,
#             including edits. No authentication is required.
$conf['auth_system'] = 'enabled';

$acl = GangliaAcl::getInstance();

$acl->addRole('admin', GangliaAcl::ADMIN);

?>
其中 admin 用户被配置为管理员,你也可以根据需要为其他用户分配不同的角色。
2.4 启动
用 chkconfig 增加系统服务,并启动 gmond 和 gmetad 服务:

sudo chkconfig --add gmond
sudo chkconfig --add gmetad
sudo service gmond start
sudo service gmetad start
启动系统以后,可以通过 Web 界面访问 Ganglia 查看图形。
3. 扩展
Ganglia 的扩展性很强,可以很方便使用其提供的一些机制来创建自定义的 metric,以收集应用数据等。它主要提供了以下方式:
Python 扩展
gmetric 命令
而 ganglia 的数据格式和协议是完全开放的,第三方应用也完全可以按照其格式发送 metric 数据给 gmond。本系列文章后续的章节会详细介绍的 JMX 和 ganglia 的整合,用的就是这种方式。
Ganglia 官方也将一些 第三方扩展收集到一起了 ,可以用来监控诸如redis、MySQL 等等应用,可以根据需要选用。
这里先介绍通过 Python 扩展的方式来解决 Ganglia 默认的磁盘监控的一点不足之处。
3.1 磁盘监控的小改进
Ganglia 默认的 disk metric,针对的是系统所有的磁盘,它会把所有磁盘总空间、可用空间加起来。这样当某个分区快满了,别的分区空闲很多的时候,通过 ganglia 完全发现不了问题。
好在通过自定义扩展,可以很容易解决这个问题。我在网上找到了一个 ganglia 的 multidisk 扩展,并自己小小改进了一下。相关文件在这个 gist 里。
把其中的 multidisk.py 文件放到 /usr/local/lib64/ganglia/python_modules 下(目录不存在的话先创建出来),把 multidisk.pyconf 配置文件放到 /usr/local/etc/conf.d 下,重启 gmond 即可。
其中配置文件可以根据需要加入分区。其中参数名是挂载点去掉开头的 / ,并将中间的 / 替换成 _ 后,再跟上 _disk_total 和 _disk_free 。特例是根分区,名字是 root_disk_free 和 root_disk_total。这个是我做出的小 hack,原本这个脚本使用的是设备名(dev_sda1)这种。但不同的机器设备名不尽相同,有时候要统一监控某类分区时不方便。
下面是一个例子,监控根分区和 /var 分区。

modules {
  module {
    name = 'multidisk'
    language = 'python'
  }
}

collection_group {
  collect_every = 120
  time_threshold = 20

  metric {
    name = "root_disk_total"
    title = "Root Partition Total"
    value_threshold = 1.0
  }

  metric {
    name = "root_disk_free"
    title = "Root Partition Free"
    value_threshold = 1.0
  }

  metric {
    name = "var_disk_total"
    title = "Var Partition Total"
    value_threshold = 1.0
  }

  metric {
    name = "var_disk_free"
    title = "Var Partition Free"
    value_threshold = 1.0
  }

}
4. 总结
上面差不多把 Ganglia 基本的安装、配置和使用都介绍了一下。图形界面方面没有介绍太多,但相信读者安装配置好以后,自己看看就能明白,或者去维基百科的 Ganglia 实例上操作一下也行。


三. Nagios 篇

介绍了我们监控方案中的核心:Ganglia,而这次我们继续介绍用于告警的 Nagios 系统,以及如何让 Nagios 使用 Ganglia 来作为告警的数据源。
Nagios 原本名叫 NetSaint,是由 Ethan Galstad 和其他一些开发者实现和维护的。它跨平台,可以在主流的所有 UNIX 类操作系统上运行。Nagios 提供一个基于 PHP 和 CGI 的 Web 界面,并包含很多插件,用于监控各种不同的网络服务以及网络设备。Nagios 的基本工作方式就是定期检查用户配置的 host 上的 service,如果发现异常就会产生告警(WARN 或者 CRITICAL 级别),并使用邮件或者短信来通知管理员。
正常的 Nagios 系统需要在被监控的机器上安装 NPRE ,用于远程执行指令来获取监控数据。但我们的方案里已经有 Ganglia 来收集监控数据了,完全可以省略 NPRE,直接从 Ganglia 中获取数据。
下面介绍 Nagios 的安装和配置,以及和 Ganglia 的整合。
1. 安装和运行
Nagios 在 Redhat 的 EPEL 的源里有,但版本比较老(3.5.x),因此我们选择自行下载源码,编译安装。最新版本的 Nagios 4.x 的源码可以从这里下载: http://sourceforge.net/projects/nagios/files/nagios-4.x/ 。
1.1 编译安装 nagios-core
首先要建立必要的用户和组:

sudo groupadd nagios
sudo useradd -g nagios nagios
sudo gpasswd -a nobody nagios
我们的 Nginx 和 fcgiwrap 的用户是 nobody,所以把它加到了 nagios 组,以便运行其 CGI 程序。如果使用 Apache 作为 Web Server,则将 apache 用户加入到 nagios 组即可。
解压源码后进入源码目录,执行下面的命令来编译安装:

./configure --with-command-group=nagios
make
sudo make install
nagios 会被安装到 /usr/local/nagios 下,其配置文件在 etc 子目录下, CGI 在 sbin 下,PHP 和 HTML 以及静态资源在 share 下。
接下来启用 nagios 服务:

sudo chkconfig --add nagios
sudo chkconfig --level 35 nagios on
1.2 配置 Nginx
Nagios 自带了 Apache 的配置,可以直接安装好并启用即可。我们使用的 Nginx,相比之下要麻烦很多。首先 Nginx 并不直接支持 CGI,只支持 FastCGI,因此需要使用 fcgiwrap 这个工具。具体的配置这里不再详细叙述,可以参考 一些网上的教程 。
我们的 Nagios 和 Ganglia 运行在同一台服务器上,且共享了同一个域名,所以配置放到一起了。其中 monigor.foobar.com 是 Ganglia 的地址,而 monitor.foobar.com/nagios 则是 Nagios 的地址。
完整配置如下,其中 location 中包含 /nagios 的是 Nagios 相关配置:

server {
     listen 80;
     server_name monitor.foobar.com;
     charset utf8;
     access_log logs/$host.access.log main;
     root /usr/local/www/ganglia;
     index index.php index.html index.htm;
     include mime.server.common;
     auth_digest_user_file /usr/local/www/ganglia/users.htdigest;

     location ~ ^/nagios/(.*.php)$ {
        set $phpfile $1;
        alias /usr/local/nagios/share/$phpfile;
        auth_digest 'Foobar Monitor System';
        auth_digest_timeout 60s;
        auth_digest_expires 3600s;
        if ($http_authorization ~ username="([^"]+)") {
            set $htdigest_user $1;
        }
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME  $request_filename;
        fastcgi_param AUTH_USER $htdigest_user;
        fastcgi_param REMOTE_USER $htdigest_user;
        include fastcgi.conf;
     }

    location ~ .*.(php|php5)?$ {
        auth_digest 'Foobar Monitor System';
        auth_digest_timeout 60s;
        auth_digest_expires 3600s;
        fastcgi_split_path_info ^(.+?.php)(/.*)$;
        if (!-f $document_root$fastcgi_script_name) {
                return 404;
        }
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi.conf;
        fastcgi_param ganglia_secret super-secret;
        if ($http_authorization ~ username="([^"]+)") {
            set $htdigest_user $1;
        }
        fastcgi_param  REMOTE_USER    $htdigest_user;
     }

     location /nagios {
        alias /usr/local/nagios/share;
        index index.php index.html index.htm;
     }

     location /nagios {
        alias /usr/local/nagios/share;
        index index.php index.html index.htm;
     }

     location /nagios/cgi-bin/ {
        alias /usr/local/nagios/sbin/;
        auth_digest 'Foobar Monitor System';
        auth_digest_timeout 60s;
        auth_digest_expires 3600s;
        if ($http_authorization ~ username="([^"]+)") {
            set $htdigest_user $1;
        }
        fastcgi_param SCRIPT_FILENAME  $request_filename;
        fastcgi_param AUTH_USER $htdigest_user;
        fastcgi_param REMOTE_USER $htdigest_user;
        include fastcgi.conf;
        fastcgi_pass unix:/var/run/fcgiwrap.sock;
     }
}
如果你打算为 nagios 单独设置一个域名,且 path 不是 /nagios,你还需要修改 /usr/share/nagios/etc/cgi.cfg 文件里的 url_html_path 路径为 /,同时 /usr/share/nagios/share 里的 config.inc.php 也需要一些修改:

$cfg['cgi_base_url']='/cgi-bin'; //默认是 /nagios/cgi-bin
1.3 运行
启用 Nagios 服务即可:

sudo service nagios start
配置好 Nginx 后重启或者重载配置文件,确保 fcgiwrap 正常运行,访问 monitor.foobar.com/nagios 即可看到 Nagios 的运行效果。
2. Nagios 配置
Nagios 的配置文件都在 /usr/local/nagios/etc 下,主配置文件是 nagios.cfg,其中可以通过 cfg_file 配置项包含其他配置文件。这些子配置文件一般都放在 objects 子目录下,按照职责分开,比如 hosts.cfg 配置要监控的主机,contacts.cfg 是联系人,commands.cfg 是命令配置等等。

cfg_file=/usr/local/nagios/etc/objects/commands.cfg
cfg_file=/usr/local/nagios/etc/objects/contacts.cfg
cfg_file=/usr/local/nagios/etc/objects/timeperiods.cfg
cfg_file=/usr/local/nagios/etc/objects/templates.cfg
Nagios 的配置提供了模板机制,可以把共同的配置项抽取成一个个模版,然后在定义实际的对象时可以继承这些模板。默认的配置文件已经定义了各种模板,都在 templates.cfg 里。
下面的这些配置都是在默认配置的基础上修改的,并且使用了默认的一些模板。另外我在 nagios.cfg 中把 localhost.cfg 给禁用掉了,因为后面和 Ganglia 整合后,不需要再对本机做监控,而是全部基于 Ganglia 的数据来实现。

#cfg_file=/usr/local/nagios/etc/objects/localhost.cfg
2.1 Host 和 Host Group 配置
首先是主机和组的配置。首先是主机配置:

define host{
   use          linux-server         #使用linux-server模板
   host_name    mysql2.foobar.com    #主机名
   alias        MySQL Slave Server 1 #别名
   address      192.168.1.9          #IP地址
   hostgroups   all-servers     #所属组,可以指定多个,逗号隔开
   }
然后是主机组配置:

define hostgroup{
   hostgroup_name  mysql-slaves                #组名
   alias           MySQL Slaves                #组别名
   members         mysql2.foobar.com,mysql3.foobar.com #组成员
   }
可以看到,主机和组的归属关系,即可在 host 处指定,也可以在 hostgroup 处指定。前者使用成员很多的组,后者适合成员较少的组。我这里定义了一个叫 all-servers 的组,包含所有主机,比较方便用于配置所有服务器上都需要监控的 service,比如 Load、磁盘、内存等。
2.2 Command 配置
Command 是 Nagios 实际检查一项服务时要执行的命令。命令可以是 Nagios 内置的插件提供,第三方插件提供,甚至可以自己写脚本实现。这里列举的是我们检查 MySQL Slave 状态的一个 Command。

define command{
 command_name  check_mysql_slave #指令名称
 #命令行
 command_line /usr/local/nagios/plugins/check_mysql_slavestatus.sh -H $HOSTNAME$ -P 3306 -u xxx -p xxx -w $ARG1$ -c $ARG2$
}
这个脚本是网上找的一个第三方插件,其实就是一个 Shell 脚本。可以看到 command_line 定义里有一些变量: $HOSTNAME$,$ARG1,$ARG2$。到 Nagios 实际执行的时候,它们都会被替换成实际的值。其中 $HOSTNAME$ 会被替换成主机名,$ARG1 和 $ARG2 则会被替换成 service 定义处指定的参数。在这个例子中,这两个参数用于指定 MySQL Slave 的 Seconds Behind Master 的告警阈值,$ARG1 是 WARN 阈值,$ARG2 是 CRITICAL 阈值。
2.3 Service 配置
Service 是最核心的配置,它用于定义某个 host 或 host group 上要检查哪些服务,使用什么指令来检查。以上面的 MySQL Slaves 为例:

define service{
   use                    generic-service    #使用的模板
   hostgroup_name         mysql-slaves       #要检查的主机组
   service_description    MySQL Slave Status #服务描述
   check_command          check_mysql_slave!1200!2400 #命令
   }
其中如果使用 hostgroup_name 的话,则针对一个主机组,也可以使用 host_name 来指定单个主机。
check_command 指定命令和参数,用 ! 分隔。对于这个例子,我们指定 Slave 落后于 Master 的时间在 1200 秒以上则发出 WARN 级别告警,在 2400 秒以上则发出 CRITICAL 级别告警。
上面差不多就是 Nagios 的主要配置了,可以看到还是很简单的。
2.4 Contact 配置
Contact 是联系人配置,在 contacts.cfg 文件里,主要是用于设置用户和用户组。

define contact{
   contact_name                    jerry
   use                             generic-contact
   alias                           Jerry Peng
   email                           jerry.peng@foobar.com
   }
其中最重要的就是 email 配置了,这个决定了你能不能收到 Nagios 的告警邮件。
联系人组方面,我们没有进行细分,仅仅使用了默认的 admins 组,默认的服务模板 generic-service 里定义的联系人组也是它,因此这个组里的用户能收到所有告警邮件。

define contactgroup{
   contactgroup_name       admins
   alias                   Nagios Administrators
   members                 jerry,drizzt
   }
如果你们的服务比较多,希望不同的管理员负责不同的服务,你可以多定义几个组,并且在定义 service 的时候使用 contact_groups 配置项来指定联系人组。
3. 和 Ganglia 的整合
我们的监控方案是以 Ganglia 为核心,因此 Nagios 的告警也主要基于 Ganglia 的数据来实现(当然有些类型的监控直接通过 Nagios 来做更简单,比如上面举的 MySQL Slave 监控)。两者的整合方式有很多,Ganglia 的 Github WIKI 上有一个总结。
我们采用的是第一个方案: Ganglia Web Nagios Script 。这个方案是 Ganglia 内置的,装好就有,比较简单方便。它是通过 Ganglia Web 中的一个 PHP 脚本,外加一个 Bash 脚本实现的。原理很简单,Bash 脚本通过 curl 访问 Web 系统的 PHP 脚本,传入要检查的参数和主机,以及告警阈值即可。
Command 定义如下:

define command{
 command_name  check_ganglia_metric
 command_line  /bin/sh /usr/local/www/ganglia/nagios/check_ganglia_metric.sh host=$HOSTNAME$ metric_name=$ARG1$ operator=$ARG2$ critical_value=$ARG3$
}
其中的三个参数分别为要监控的 metric 名称,操作符(more/less), CRITICAL 阈值。
下面是使用这个 Command 来定义的若干 Service:
所有主机的根分区磁盘空间监控,剩余空间小于 20G 时告警:

define service{
  use                             generic-service
  hostgroup_name                  all-servers
  service_description             Root Partition Free Space
  check_command                   check_ganglia_metric!root_disk_free!less!20
  }
所有主机的一分钟 loadavg 检查,大于 16 时告警(我们的都是16核以上的机器):

define service{
   use                             generic-service
   hostgroup_name                  all-servers
   service_description             Load One
   check_command                   check_ganglia_metric!load_one!more!16
   }
一个 Java 应用的性能参数告警,核心引擎的处理时间超过两秒时告警(该参数通过 JMX 暴露出来,使用 jmxtrans发送给 Ganglia,后续章节会对此做详细介绍):

define service{
   use                             generic-service
   host_name                       engine1.foobar.com
   service_description             Engine Process Time
   check_command                   check_ganglia_metric!engine1.ProcessTime!more!2000
   }
可以看到,任何 Ganglia 中的 Metric 都可以用这样方式拿来做告警,十分方便。
3. 总结
至此,我们的监控方案基本上成形了,通过这个方案,我们既可以查看某个监控参数的变化情况,也可以针对它们做告警机制。
写了几篇了,与 Java 有关的事情都还没影,的确有点标题党的嫌疑了。不要急,有了这些基础系统,剩下的与 Java 有关的事情就简单了,无非是想法办法记录应用内的一些监控参数,并整合到 Ganglia 里而已。下一次我就详细对这个进行介绍,并分享我们的经验。


Java 篇

主要介绍的内容有 JMX 以及将监控 JMX 并发送数据到 Ganglia 的 jmxtrans,同时还会介绍我实现的一个简单的记录性能参数的方法。
1. JMX
JMX 基本上是 Java 应用监控的标准解决方案,JVM 本身的诸多性能指标如内存使用、GC、线程等都有对应的 JMX 参数可供监控。自定义 MBean 也是十分简单的一件事。可以用两种方式来定义 MBean,第一种是通过自定义接口和对应的实现类,另一种则是实现 javax.management.DynamicMBean 接口来定义动态的 MBean。我们采用的是第二种方式,因此略过第一种方式的介绍,有兴趣的读者请参考Java Tutorial 里的教程和 Javalobby 上的文章。
下面是我们内部使用的 MetricMBean,使用 DynamicMBean 实现:

public class MetricsMBean implements DynamicMBean {

    private final Map metrics;

    public MetricsMBean(Map metrics) {
        this.metrics = new HashMap<>(metrics);
    }

    @Override
    public Object getAttribute(String attribute)
            throws AttributeNotFoundException,
                   MBeanException,
                   ReflectionException {
        Metric metric = metrics.get(attribute);
        if (metric == null) {
            throw new AttributeNotFoundException("Attribute " + attribute + " not found");
        }
        return metric.getValue();
    }

    @Override
    public void setAttribute(Attribute attribute)
            throws AttributeNotFoundException,
                   InvalidAttributeValueException,
                   MBeanException,
                   ReflectionException {
        // 我们仅仅需要做监控,没有设置属性的需要,所以直接抛异常
        throw new UnsupportedOperationException("Setting attribute is not supported");
    }

    @Override
    public AttributeList getAttributes(String[] attributes) {
        AttributeList attrList = new AttributeList();
        for (String attr : attributes) {
            Metric metric = metrics.get(attr);
            if (metric != null)
                attrList.add(new Attribute(attr, metric.getValue()));
        }
        return attrList;
    }

    @Override
    public AttributeList setAttributes(AttributeList attributes) {
        // 我们仅仅需要做监控,没有设置属性的需要,所以直接抛异常
        throw new UnsupportedOperationException("Setting attribute is not supported");
    }

    @Override
    public Object invoke(String actionName,
                         Object[] params,
                         String[] signature) throws MBeanException, ReflectionException {
        // 方法调用也是不需要实现的
        throw new UnsupportedOperationException("Invoking is not supported");
    }

    @Override
    public MBeanInfo getMBeanInfo() {
        SortedSet names = new TreeSet<>(metrics.keySet());
        List attrInfos = new ArrayList<>(names.size());
        for (String name : names) {
            attrInfos.add(new MBeanAttributeInfo(name,
                                                 "long",
                                                 "Metric " + name,
                                                 true,
                                                 false,
                                                 false));
        }
        return new MBeanInfo(getClass().getName(),
                             "Application Metrics",
                             attrInfos.toArray(new MBeanAttributeInfo[attrInfos.size()]),
                             null,
                             null,
                             null);
    }

}
其中 Metric 是我们设计的一个接口,用于定义不同的监控指标:

public class Metrics {

    private static final Logger log = LoggerFactory.getLogger(Metrics.class);
    private static final Metrics instance = new Metrics();
    private Map metrics = new HashMap<>();

    public static Metrics instance() {
        return instance;
    }

    private Metrics() {
    }

    public Metrics register(String name, Metric metric) {
        metrics.put(name, metric);
        return this;
    }

    public void createMBean() {
        MetricsMBean mbean = new MetricsMBean(metrics);
        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
        try {
            final String name = MetricsMBean.class.getPackage().getName() +
                                ":type=" +
                                MetricsMBean.class.getSimpleName();
            log.debug("Registering MBean: {}", name);
            server.registerMBean(mbean, new ObjectName(name));
        } catch (Exception e) {
            log.warn("Error registering trafree metrics mbean", e);
        }
    }

}
在应用启动的时候这样调用以注册指标并创建 MBean:
 
// createMaxValueMetric 和 createCountMetric 可以基于同一份数据来得到
// 最大值和次数的指标,详见下面 AverageMetric 的具体实现。
Metrics.instance()
       .register("SearchAvgTime", MetricLoggers.searchTime)
       .register("SearchMaxTime", MetricLoggers.searchTime.createMaxValueMetric())
       .register("SearchCount", MetricLoggers.searchTime.createCountMetric())
       .createMBean();
其中注册时指定的名称也是最后从通过 JMX 看到的属性名。
当然上面只是我们内部的监控框架的做法,你需要关注的是如何实现自定义 MBean 而已。
上面提到的 Metric 接口,我并没有给出实现。下面介绍我们内部常用的一个实现 AverageMetric (平均值指标)。它可以记录某个性能数值,并计算单位时间内的平均值,最大值和次数。例如上面的 MetricLoggers 中定义的 searchTime,它用来记录我们系统的搜索功能的一分钟平均耗时,一分钟最大耗时和一分钟的搜索次数。
 
public class MetricLoggers {
    public static final AverageMetric searchTime = new AverageMetric();
}
在实际的搜索功能处记录耗时:
 
long startTime = System.currentTimeMillis();
doSearch(request);
long timeCost = System.currentTimeMillis() - startTime;

MetricLoggers.searchTime.log(timeCost);
这样通过 JMX 就可以监控到我们系统过去一分钟内的平均搜索耗时,最大搜索耗时以及搜索次数。
下面是 AverageMetric 类的具体实现,比较长,请慢慢看。基本思路就是使用 AtomicReference 和一个值对象,通过非阻塞算法来实现并发。经过测试,在并发度不高的情况下性能不错,但在线程很多,竞争激烈的时候不是很好。再次重申,这个实现仅供参考。
 
public class TimeWindowSupport {
    final long timeWindow;

    TimeWindowSupport(long timeWindow) {
        this.timeWindow = timeWindow;
    }

    long currentSlot() {
        return System.currentTimeMillis() / timeWindow;
    }
}


public class AverageMetric extends TimeWindowSupport implements Metric {

    final AtomicReference currentValue = new AtomicReference();
    private volatile Value lastValue = null;

    public AverageMetric(long timeWindow) {
        super(timeWindow);
    }

    public AverageMetric() {
        super(TimeUnit.MINUTES.toMillis(1));
    }

    public Value getLastValue() {
        long slot = currentSlot();
        while(true) {
            Value curValue = currentValue.get();
            if (curValue != null && slot != curValue.slot) {
                if (currentValue.compareAndSet(curValue, Value.create(slot))) {
                    lastValue = curValue;
                    break;
                }
            } else {
                break;
            }
        }
        return lastValue;
    }

    public void log(long value) {
        long slot = currentSlot();
        while (true) {
            Value curValue = currentValue.get();
            if (curValue == null) {
                if (currentValue.compareAndSet(null, Value.create(slot, value)))
                    return;
            } else if (slot == curValue.slot) {
                if (currentValue.compareAndSet(curValue, curValue.add(value)))
                    return;
            } else {
                if (currentValue.compareAndSet(curValue, Value.create(slot, value))) {
                    lastValue = curValue;
                    return;
                }
            }
        }
    }

    /**
     * 基于同样的数据,创建一个计数度量,其返回值是过去的单位时间内的log事件发生次数
     *
     * @return 返回计数度量
     */
    public Metric createCountMetric() {
        return new Metric() {
            @Override
            public long getValue() {
                Value val = getLastValue();
                if (val != null)
                    return (long) val.n;
                else
                    return 0L;
            }
        };
    }

    /**
     * 基于同样的数据,创建一个最大值度量,其返回值是过去的单位时间内记录的最大数值
     *
     * @return 返回最大值度量
     */
    public Metric createMaxValueMetric() {
        return new Metric() {
            @Override
            public long getValue() {
                Value val = getLastValue();
                if (val != null)
                    return val.max;
                else
                    return 0L;
            }
        };
    }

    @Override
    public long getValue() {
        Value lastValue =  getLastValue();
        long lastSlot = currentSlot() - 1;
        if (lastValue != null && lastValue.n != 0 && lastSlot == lastValue.slot)
            return lastValue.total / lastValue.n;
        else
            return 0L;
    }

    static class Value {
        final long slot;
        final int n;
        final long total;
        final long max;

        Value(long slot, int n, long total, long max) {
            this.slot = slot;
            this.n = n;
            this.total = total;
            this.max = max;
        }

        static Value create(long slot, long value) {
            return new Value(slot, 1, value, value);
        }

        static Value create(long slot) {
            return new Value(slot, 0, 0, 0);
        }

        Value add(long value) {
            return new Value(this.slot,
                             this.n + 1,
                             this.total + value,
                             (value > this.max) ? value : this.max);
        }
    }
}
2. jmxtrans
有了 JMX,我

热门栏目