Java 运行时监控,第 3 部分: 监控应用程序生态系统的性能与可用性

监控主机、数据库和通信;性能数据的管理与可视化

本文为 Java™ 应用程序运行时监控 系列 的第三部分,也是最后一部分,主要介绍在监视应用程序支持和依赖服务的性能和可用性时应使用哪些策略与技巧。所谓支持和依赖服务包括底层主机操作系统、运行数据库以及通信基础设施。文章结尾针对性能数据管理问题以及数据的报告和可视化做了论述。

Nicholas Whitehead, 高级技术架构师, ADP

Nicholas Whitehead 在 ADP 的 Small Business Services 部门担任高级技术架构师,该公司坐落在美国新泽西州的弗朗汉姆公园。他拥有 10 余年 Java 开发的经验,所涉及的行业包括银行投资、电子商务和商业软件。在开发和支持生产应用程序(其中一些是他自己的)方面的经验一直鼓舞着他学习和实现性能管理系统。



2008 年 9 月 02 日

在本系列(共三篇文章)的 第 1 部分第 2 部分 中,我介绍了监控 Java 应用程序的技巧和模式,在这两部分中我把重点放在了 JVM 和应用程序类上。在这最后一期中,我将介绍从应用程序的依赖项(诸如底层操作系统、网络或者应用程序的后端数据库)收集性能与可用性数据的技巧。在文章结尾我将论述管理收集数据的模式以及报告和可视化数据的方法。

基于 Spring 的收集器

第 2 部分 中,我实现了一个用于管理监控服务的基本的基于 Spring 的组件模型。该模型的基本原理及益处有:

  • 使用基于 XML 的配置,使得管理大量用于配置更复杂性能数据收集器的参数集变得更加容易。
  • 采用关注点分离 的结构,这样就可以使用更简单的组件,这些组件之间的相互交互可以通过注入 Spring 的依赖项来实现。
  • Spring 给简单的收集 bean 提供了一个生命周期,该周期由初始化启动停止 操作组成,还提供了将 Java 管理扩展(Java Management Extension,JMX)管理接口公开给 bean 的选项,这样就可以在运行时进行控制、监控和故障排除。

下面我将在本文的每个小节中介绍有关基于 Spring 的收集器的更多细节。

监控主机和操作系统

Java 应用程序总是运行于底层硬件和支持 JVM 的操作系统之上。一个全面的监控基础设施中最关键的组成就是从硬件和 OS — 通常是通过 OS 收集 — 那里收集性能、健康状况和可用性指标的能力。本节就涵盖了一些通过在 第 1 部分 中介绍的 ITracer 类获取这类数据并一直跟踪到应用程序性能管理系统(application performance management,APM)的技巧。

典型的 OS 性能指标

下面这份摘要列出了典型指标,这些指标跨域操作系统的多个部分相关。虽然数据收集的细节迥异,而且数据的解释也必须在给定的 OS 上下文中进行,但是这些指标在大多数标准主机上基本都是等效的:

  • CPU 使用:表示特定主机上的 CPU 的占用情况。单位通常为百分比的使用率,在较低的级别将 CPU 忙碌时间表示为消逝的时钟时间的某个特定时期的百分比。主机可以有多个 CPU,而 CPU 又可以包含多个内核,但多个内核通常都被 OS 抽象出来代表一个 CPU。例如,一个带有两个双核 CPU 的主机会被说成有四个 CPU。指标通常可以按照每个 CPU 收集或者作为总资源利用率收集,后者表示所有处理器的总体使用情况。到底是要分别监控每一个 CPU 还是监控总体 CPU,通常要取决于软件的本质及其内部架构。标准的多线程 Java 应用程序通常默认平衡所有 CPU 上的负载,所以监控总体较合适。但在某些情况下,个别 OS 进程是 “特定于” 特定 CPU 的,这时总体指标可能无法捕获到适当级别的粒度。

    CPU 的使用通常被拆分成四个范畴:

    • 系统:执行系统的或者 OS 内核级的活动耗费的处理器时间
    • 用户:执行用户活动耗费的处理器时间
    • I/O 等待:处于空闲状态等待完成某个 I/O 请求耗费的处理器时间
    • 空闲:暗指没有进行任何处理器活动
    另外两个相关指标为运行队列长度(即等候 CPU 时间的请求的待处理事项)和上下文转换(即将处理器时间分配从一个进程转换到另一个进程的实例)。
  • 内存:最简单的内存指标为可用或使用中的物理内存的百分比。其他需要考虑的有虚拟内存、内存分配率和重新分配率以及表明内存有哪些区域被使用的更细粒度的指标。
  • 磁盘与 I/O:磁盘指标为每一个逻辑或物理磁盘设备的可用或使用中的磁盘空间的简单(但是至关重要的)报告,还有这些设备的读取和写入速率。
  • 网络:指网络接口上的数据传输速率和错误发生率,它通常被分为高级的网络协议范畴,如 TCP 和 IP。
  • 进程与进程组:可以说前面所述的指标都是特定主机的总活动。它们也可以划分为相同的指标,但是代表个别进程或相关进程组的消耗或活动。监控进程对资源的使用情况有助于解释主机上的每一个应用程序或者服务消耗的资源比例。有些应用程序只可以实例化一个进程;在其他情况下,一个诸如 Apache 2 Web Server 这样的服务可以实例化代表一个逻辑服务的一群进程。

代理与无代理

不同的 OS 有着不同的性能数据获取机制。我将呈现的收集数据的方式很多,但是在监控领域您可能经常要区别的是基于代理的无代理的 监控。也就是说在某些情况下,无需在目标主机上安装其他特定的软件也可以收集数据。但显然监控通常都会涉及到某种代理,因为监控总是需要一个接口,数据要通过它来读取。所以这里真正区别的是是使用通常出现在给定 OS 中的代理 — 诸如 Linux® 服务器上的 SSH — 还是安装其他专用于监控和使收集的数据对外部收集器可用的软件。两种方法都涉及到如下的权衡标准:

  • 代理需要安装其他的软件并可能需要应用定期的维护补丁。在带有大量主机的环境中,管理软件工作不利于使用代理。
  • 如果代理实际上是与应用程序相同的进程的一部分的话,哪怕它是一个单独的进程,代理进程的故障也将会蒙蔽监控。虽然主机本身仍在运行且健康状况良好,但是 APM 一定会因为无法到达代理而假定主机已停机。
  • 安装在主机上的代理可能要比无代理远程监控器的数据收集能力和事件监听能力强得多。而且,报告总体指标可能需要收集好几个原始底层指标,远程收集这些指标的效率会很低。而本地的代理则能够快速地收集数据,再合计数据,然后将合计的数据提供给远程监控器。

归根结底,最佳的解决方案可能就是既实现无代理的监控又实现基于代理的监控:本地代理负责收集大多数指标,而远程监控器负责检查诸如服务器的运行情况和本地代理的状态这样的基本内容。

代理也可以有不同的选项。自治 代理按照自己的计划收集数据,反之,响应 代理按请求递送数据。此外,有些代理只将数据提供给请求程序,而有些则直接或间接地跟踪数据一直到 APM 系统。

接下来我将呈现监控带有 Linux 和 UNIX® OS 的主机的技巧。


监控 Linux 和 UNIX 主机

监控代理可以用来实现专门的本机库以从 Linux 和 UNIX OS 收集性能数据。但是 Linux 和大多数 UNIX 变体都有很多内置数据收集工具,这些工具使得数据可以通过称为 /proc 的虚拟文件系统进行访问。该文件看起来像是普通文件系统目录里面的普通文本文件,但其实它们是常驻内存型数据结构,是通过文本文件抽取的。由于这种数据可以很容易地通过大量标准命令行的实用工具或自定义的工具来读取和解析,所以这些文件较易于使用,而且它们的输出既可以是通用的也可以是专用的。而且它们的性能也非常好,因为本质上它们是直接来源于内存的数据。

常见的用于从 /proc 中抽取性能数据的工具是 pssariostatvmstat(参见 参考资料 查阅有关这些工具的参考文献)。因此,一个有效地监控 Linux 和 UNIX 主机的方法就是执行 shell 命令并解析响应。类似的监控器可以用于很多种 Linux 和 UNIX 实现;虽然它们之间都有着些许差异,但是,使用一种可以完全重用收集过程的方式格式化数据是很简单的。相反,专用的本机库可能要根据每一个 Linux 和 UNIX 发行版而进行重编码或重构(但它们正在读取的 /proc 数据有可能相同)。而编写专用于监控某一特定情况或可以标准化返回数据的格式这样的自定义 shell 命令很容易,并且开销较低。

现在我将展示几种调用 shell 命令和跟踪返回数据的方法。

Shell 命令的执行

要在一个 Linux 主机上执行数据收集监控,就一定要调用一个 shell。它可以是 bashcshksh 或其他任何允许调用目标脚本或命令并可以检索输出的、受支持的 shell。最通常的选择包括:

  • 本地 shell:如果目标主机上运行着 JVM 的话,那么线程可以通过调用 java.lang.Process 来访问这种 shell。
  • 远程 Telnet 或 rsh:这两个服务都允许调用 shell 和 shell 命令,但由于它们的安全性相对较低,所以很少使用它们。它们在大多数现代发行版上的默认状态为禁用。
  • 安全 Shell(SSH):SSH 是最为常用的远程 shell。它提供了对 Linux shell 的完全访问,而且被公认是安全的。在文中基于 Shell 的例子里,我将主要使用该机制。大多数 OS 都提供 SSH 服务,包括所有 UNIX 系列、Microsoft® Windows®、OS/400 及 z/OS。

图 1 展示了本地 shell 与远程 shell 的基本差异:

图 1. 本地 shell 与远程 shell
本地 shell 与远程 shell

要用服务器启动一个无人值守的对话需要进行一些设置。首先必须要创建一个由私钥和公钥组成的 SSH 密匙对。然后将公钥置于目标服务器,私钥置于远程监控服务器 —— 数据收集器可以在此获取该私钥。完成上述操作之后,数据收集器便能够提供私钥及其密码短语(passphrase),并能够访问目标服务器上的安全远程 shell 了。使用了密匙对之后,目标帐户的密码就是多余的了,根本不需要它。具体设置步骤如下:

  1. 确保目标主机在本地的已知主机的文件中有入口。这个文件列出了已知 IP 地址或名称以及为每一个已知 IP 地址或名称验证的相关 SSH 公钥。在用户级别,该文件通常为用户主目录中的 ~/.ssh/known_hosts 文件。
  2. 用监控帐户(例如,monitoruser)连接到目标服务器。
  3. 在主目录中创建一个名为 .ssh 的子目录。
  4. 将目录改为 e .ssh 目录并发布 ssh-keygen -t dsa 命令。该命令提示密钥名和密码短语。然后会生成两个叫做 monitoruser_dsa(私钥)和 monitoruser._dsa.pub(公钥)的文件。
  5. 将私钥复制到一个安全的可访问的位置,数据收集器将从这个位置运行。
  6. cat monitoruser_dsa.pub >> authorized_keys 命令将私钥内容追加到 .ssh 目录中名为 authorized_keys 的文件中。

清单 1 展示了我刚才所描述的过程:

清单 1. 创建一个 SSH 密匙对
whitehen@whitehen-desktop:~$ mkdir .ssh
whitehen@whitehen-desktop:~$ cd .ssh
whitehen@whitehen-desktop:~/.ssh$ ssh-keygen -t dsa
Generating public/private dsa key pair.
Enter file in which to save the key (/home/whitehen/.ssh/id_dsa): whitehen_dsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in whitehen_dsa.
Your public key has been saved in whitehen_dsa.pub.
The key fingerprint is:
46:cd:d4:e4:b1:28:d0:41:f3:ea:3b:8a:74:cb:57:e5 whitehen@whitehen-desktop
whitehen@whitehen-desktop:~/.ssh$ cat whitehen_dsa.pub >> authorized_keys
whitehen@whitehen-desktop:~/.ssh$

现在数据收集器已经能够使用 SSH 连接到目标 Linux 主机,该 SSH 连接名为 whitehen-desktop,它运行着 Ubuntu Linux。

这个例子的数据收集器将使用一个名为 org.runtimemonitoring.spring.collectors.shell.ShellCollector 的通用收集器类来实现。该类的一个实例将以 UbuntuDesktopRemoteShellCollector 这个名称部署在一个 Spring 上下文中。但要完成整个过程还需要一些其他的依赖项:

  • 需要有一个调度器来每分钟调用一次收集器。该调度器由 java.util.concurrent.ScheduledThreadPoolExeutor 的一个实例来实现,它既可以提供一个有计划的回调机制,又可以提供一个线程池。这个调度器将以 CollectionScheduler 这个名称部署于 Spring。
  • SSH shell 实现对服务器调用命令并返回结果。这个可以通过 org.runtimemonitoring.spring.collectors.shell.ssh.JSchRemoteShell 的一个实例来实现。这个类是一个名为 org.runtimemonitoring.spring.collectors.shell.IRemoteShell 的 Shell 接口的实现,它将以 UbuntuDesktopRemoteShell 这个名称部署于 Spring。
  • 该收集器不会硬编码一组命令及其相关解析例程,而是使用 org.runtimemonitoring.spring.collectors.shell.commands.CommandSet 的一个实例,它将以 UbuntuDesktopCommandSet 这个名称部署于 Spring 中。命令集从一个 XML 文档载入,该文档表述了:
    • 将要用来执行 shell 的目标平台
    • 将要执行的命令
    • 将如何解析返回数据并将其映射到 APM 跟踪命名空间
    稍候我将提供有关这些定义的更多细节。图 2 大致解释了收集器、shell 和命令集三者之间的关系:
图 2. 收集器、shell 和命令集
收集器、shell 和命令集

下面我将专门介绍一些关于专用于生成性能数据的命令以及它们的配置方法的简短示例。一个经典的例子就是 sar 命令。Linux 手册(参见 参考资料)对 sar 的定义是收集、报告或者保存系统活动信息。该命令非常灵活,它有超过 20 个参数,这些参数可以结合起来使用。一个简单的选择就是调用 sar -u 1 3,它报告了在三个时间间隔内(一个时间间隔为一秒)度量的 CPU 使用。清单 2 展示了它的输出:

清单 2. sar 命令的输出
whitehen@whitehen-desktop:~$ sar -u 1 3
Linux 2.6.22-14-generic (whitehen-desktop)      06/02/2008

06:53:24 PM     CPU     %user     %nice   %system   %iowait    %steal     %idle
06:53:25 PM     all      0.00      0.00      0.00      0.00      0.00    100.00
06:53:26 PM     all      0.00     35.71      0.00      0.00      0.00     64.29
06:53:27 PM     all      0.00     20.79      0.99      0.00      0.00     78.22
Average:        all      0.00     18.73      0.33      0.00      0.00     80.94

该输出可以划分成开头、标题、三个时间间隔的数据读数和一个读数汇总平均值。这里的目标是要执行该 shell 命令、捕获输出,然后解析输出并跟踪到 APM 系统。输出数据的格式是够简单的,但却可能根据具体的版本而不同(轻微或显著的不同),而且其他 sar 选项也会返回完全不同的数据(更不用说其他的命令了,它们当然会返回不同的数据格式)。例如,清单 3 展示了一个显示活动的 socket 行为的 sar 执行:

清单 3. 显示 socket 行为的 sar
whitehen@whitehen-desktop:~$ sar -n SOCK 1
Linux 2.6.22-14-generic (whitehen-desktop)      06/02/2008

06:55:10 PM    totsck    tcpsck    udpsck    rawsck   ip-frag
06:55:11 PM       453         7         9         0         0
Average:          453         7         9         0         0

因此,现在所需要的是一个解决方案:怎样在不重新编码收集器的情况下快速配置不同的数据。还可以将诸如 totsck 这样的含糊词语翻译成像 Total Used Sockets 这样的更易读的短语,以免收集到的记录会干扰 APM 系统。

在某些情况下,您可以选择以 XML 格式获取这个数据。例如,SysStat 包(参见 参考资料)中的 sadf 命令会以 XML 格式生成很多被经常收集的 Linux 监控数据。XML 格式增加了数据的可预测性和结构,并真正排除了分析数据、将数据映射到跟踪名称空间和解码模糊词语的需要。然而,这些工具对于您想监控的可以访问 shell 的系统可能是不可用的,因此需要一种灵活的文本解析和映射解决方案。

承接上面两个关于 sar 的应用的例子,接下来我将呈现一个设置 Spring bean 定义以监控这些数据的例子。所有引用的例子都包含在本文的示例代码中(参见 下载)。

首先,SpringCollector 实现的主要入口点为 org.runtimemonitoring.spring.collectors.SpringCollector。它采用了一个参数:Spring bean 配置文件所在的目录的名称。SpringCollector 载入了任何带有 .xml 扩展名的文件,并将他们当作 bean 描述符。该目录为位于项目根目录中的 ./spring-collectors 目录(稍后我将在本文中概述此目录中的所有文件。有多个文件可以选择,而且可以将所有的定义捆绑成一个,但要用虚构的函数单独隔开,以保持一定的顺序)。这个例子中的三个 bean 定义代表 shell 收集器、shell 和命令集。清单 4 展示了它们的描述符:

清单 4. shell 收集器、shell 与命令集的 Bean 描述符
<!-- The Collector -->
<bean id="UbuntuDesktopRemoteShellCollector"
  class="org.runtimemonitoring.spring.collectors.shell.ShellCollector"
  init-method="springStart">
  <property name="shell" ref="UbuntuDesktopRemoteShell"/>
  <property name="commandSet" ref="UbuntuDesktopCommandSet"/>
  <property name="scheduler" ref="CollectionScheduler"/>
  <property name="tracingNameSpace" value="Hosts,Linux,whitehen-desktop"/>
  <property name="frequency" value="5000"/>
</bean>

<!-- The Shell -->
<bean id="UbuntuDesktopRemoteShell"
   class="org.runtimemonitoring.spring.collectors.shell.ssh.JSchRemoteShell"
   init-method="init"
   destroy-method="close">
   <property name="userName" value="whitehen"/>
   <property name="hostName" value="whitehen-desktop"/>
   <property name="port" value="22"/>
   <property name="knownHostsFile"
      value="C:/Documents and Settings/whitehen/.ssh/known_hosts"/>
   <property name="privateKey"
      value="C:/keys/whitehen/ubuntu-desktop/whitehen_dsa"/>
   <property name="passphrase" value="Hello World"/>
</bean>

<!-- The CommandSet -->
<bean id="UbuntuDesktopCommandSet"
   class="org.runtimemonitoring.spring.collectors.shell.commands.CommandSet">
   <constructor-arg type="java.net.URL"
      value="file:///C:/projects//RuntimeMonitoring/commands/xml/UbuntuDesktop.xml"/>
</bean>

清单 4 中的 CommandSet 只有一个 idUbuntuDesktopCommandSet)和另一个 XML 文件的 URL。这是因为命令集太大,我不想因为它们而使 Spring 文件显得很混乱。稍后我将描述 CommandSet

清单 3 中的第一个 bean 为 UbuntuDesktopRemoteShellCollector。它的 bean id 值可以是任意的描述性的值,但是当从另一个 bean 引用该 bean 时需要保持一致。这个例子中的类为 org.runtimemonitoring.spring.collectors.shell.ShellCollector,它是一个通过类似于 Shell 的接口来收集数据的通用类。其他重要属性有:

  • shell:收集器用来从 shell 命令调用和检索数据的 shell 类的实例。Spring 用 UbuntuDesktopCommandSet 的 bean id 来注入该 Shell 的实例。
  • commandSet:代表一组命令和相关解析、跟踪名称空间映射指令的 CommandSet 实例。Spring 用 UbuntuDesktopRemoteShell 的 bean id 注入该命令集的示例。
  • scheduler:一个调度线程池的引用,该线程池管理数据收集的调度,将这项工作具体分配给一个线程来完成。
  • tracingNameSpace:跟踪名称空间的前缀,它控制着这些指标将被跟踪到 APM 树中的哪个位置。
  • frequency:数据收集频率,以毫秒为单位。

清单 4 中的第二个 bean 为 shell,它是一个名为 org.runtimemonitoring.spring.collectors.shell.ssh.JSchRemoteShell 的 SSH shell 的实现。该类使用从 JCraft.com(参见 参考资料)下载的 JSch 来实现。它的其他重要属性有:

  • userName:用户用来连接到 Linux 服务器的名称
  • hostName:连接到的 Linux 服务器的名称(或 IP 地址)。
  • port:Linux 服务器端口,sshd 在这个端口上监听。
  • knownHostFile:一个包含主机名称和 SSH 服务器的 SSH 证书的文件,该 SSH 服务器对于运行 SSH 客户机的本地主机是 “已知的”(有趣的是,SSH 中的这个安全机制恰好颠倒了传统的安全结构,使用这种机制,除非主机是 “已知的” 并可以给出匹配的证书,否则客户机不会信任主机,并拒绝连接)。
  • privateKey:用来验证 SSH 服务器的 SSH 私钥文件。
  • passPhrase:用来解锁私钥的密码短语。它的外表与密码类似,只是它没有被传送到服务器,而且它只用于本地解密私钥。

清单 5 展示了 CommandSet 的内部细节:

清单 5. CommandSet 内部细节
<CommandSet name="UbuntuDesktop">
   <Commands>
      <Command>
         <shellCommand>sar -u 1</shellCommand>
         <paragraphSplitter>\n\n</paragraphSplitter>
    <Extractors>
       <CommandResultExtract>
          <paragraph id="1" name="CPU Utilization"/>
          <columns entryName="1" values="2-7" offset="1">
             <remove>PM</remove>
          </columns>
          <tracers default="SINT"/>
               <filterLine>Average:.*</filterLine>
          <lineSplit>\n</lineSplit>
       </CommandResultExtract>
    </Extractors>
      </Command>
      <Command>
         <shellCommand>sar -n SOCK 1</shellCommand>
    <paragraphSplitter>\n\n</paragraphSplitter>
    <Extractors>
       <CommandResultExtract>
          <paragraph id="1" name="Socket Activity"/>
          <columns values="1-5" offset="1">
             <remove>PM</remove>
             <namemapping from="ip-frag" to="IP Fragments"/>
             <namemapping from="rawsck" to="Raw Sockets"/>
             <namemapping from="tcpsck" to="TCP Sockets"/>
             <namemapping from="totsck" to="Total Sockets"/>
             <namemapping from="udpsck" to="UDP Sockets"/>
          </columns>
          <tracers default="SINT"/>
               <filterLine>Average:.*</filterLine>
          <lineSplit>\n</lineSplit>
       </CommandResultExtract>
    </Extractors>
      </Command>
   </Commands>
</CommandSet>

CommandSet 负责管理 shell 命令和解析指令。由于每一个 Linux 或 UNIX 系统的输出 —— 即便是对相同命令的输出 —— 都会有些不同,因此每一种类型的受监控的主机通常都会有一个对应的 CommandSet。要使用 XML 详述 CommandSet 背后的每一个选项,可能要占用很长的篇幅,因为它一直在不断演变,而且会根据情况的不同而改变,因此我只简短的概述一下其中一些标记,内容如下:

  • <shellCommand>:定义将要传给 shell 的实际命令。
  • <paragraphSplitter>:有些命令,或组成多个命令链的命令,可能会返回多个文本片段。这些文本片段被称为段落。正则表达式(regex)在这里指定了划分段落的标准。命令对象将结果分成多个段落,并将所需段落传递到底层提取器。
  • <Extractors> 和其中包含的 <CommandResultExtract> 标记:这些结构定义解析和映射。
  • <paragraph>:提取器使用 id 属性中的基于零的索引来定义它想要从结果中抽取的段落,所有从该段落中跟踪的指标都被归入到以该段落名定义的跟踪名称空间中。
  • <columns>:如果定义了 entryName 的话,那么每一行中编入索引的列都会被添加到跟踪名称空间。这是针对左侧列包含一个指标分界的情况而言的。例如,sar 的一个选项将会分别为每一个 CPU 报告 CPU 使用,CPU 编号列于第二列中。在 清单 5 中,entryName 提取出 all 限定符,该限定符表明报告为所有 CPU 的总体汇总。values 属性代表每一行中需要被跟踪的行,而 offset 负责保持数据行中的列数和相应标题间的平衡。
  • <tracers>:它定义默认跟踪类型,并允许为与指定的标题或 entryName 有关的值定义不同的跟踪器类型。
  • <filterLine>:如果定义了它,regex 会忽略整行文本不匹配的数据行。
  • <lineSplit>:它定义用于解析每一个段落中各行的分隔 regex。

图 3 展示了这个例子的 APM 树:

图 3. Ubuntu Desktop 监控的 APM 树
Ubuntu Desktop 监控的 APM 树

如果不喜欢这个树的外观的话,您还有其他的选择。发送到服务器的命令很容易修改,可以修改它们使其传递一连串的 grepawksed 命令,以此来将数据重新格式化为很少需要解析的格式。例如,参见清单 6:

清单 6. 格式化命令内部的命令输出
whitehen@whitehen-desktop:~$ sar -u 1 | grep Average | \ 
   awk '{print "SINT/User:"$3"/System:"$5"/IOWait:"$6}'
SINT/User:34.00/System:66.00/IOWait:0.00

另外一个可以提供最佳配置、灵活性和性能的选择就是使用动态脚本,这种方法在其他格式化工具不可用或输出格式极其笨拙的情况下尤为适用。在接下来的例子中,我配置了一个 Telnet shell 用以从 Cisco CSS 负载均衡器中收集负载均衡状态数据。输出格式和内容对于任何种类的标准化解析来说都是很重要的问题,而这个 shell 支持的命令有限。清单 7 展示了命令的输出:

清单 7. CSS Telnet 命令的输出
Service Name                     State     Conn  Weight  Avg   State
                                                         Load  Transitions

ecommerce1_ssl                   Alive         0      1   255            0
ecommerce2_ssl                   Down          0      1   255            0
admin1_ssl                       Alive         0      1     2         2982
admin2_ssl                       Down          0      1   255            0
clientweb_ssl                    Alive         0      1   255            0

XML 元素与属性

有些应用程序明显偏好使用 XML 元素而非属性,而另一些则偏好使用属性。我没有什么强烈的偏好,但我尽量通过如下两个经验法则保持一致性:

  • 简短的、固定的命名值易于读取。
  • 在值文本很长或包含必须转义的无效字符(诸如 &")的情况下,我使用元素,因为数据可以包装在 CDATA 块中,该块告诉 XML 解析器不要解析这样的内容。

清单 8 展示了用于执行和解析命令的命令集。注意 <preFormatter beanName="FormatCSSServiceResult"/> 标记。它引用了一个包含几行 Groovy 脚本的 Spring bean。Telnet shell 命令的原始输出被传给 Groovy 脚本,然后返回值以一种更友好的格式被传给命令数据提取器。还要注意的是,为了标记为 Status 的列中的值,跟踪器类型被覆盖成了 STRING 类型。眼光尖锐的读者将会注意到这个列不存在,但是 Groovy 脚本的一部分工作就是解决两个列均使用 State 名的问题(您知道这其中的原委),所以 Groovy 脚本将第一个列重命名为 Status。

清单 8. CSS CommandSet
<CommandSet name="CiscoCSS">
   <Commands>
      <Command>
         <shellCommand>show service summary</shellCommand>
       <paragraphSplitter>\n\n\n\n</paragraphSplitter>
       <preFormatter beanName="FormatCSSServiceResult"/>
       <Extractors>
          <CommandResultExtract>
             <paragraph id="0" name="Service Summary" header="true"/>
        <columns entryName="0" values="1-5" offset="0"/>
        <tracers default="SINT">
           <tracer type="STRING">Status</tracer>
        </tracers>
        <lineSplit>\n</lineSplit>
          </CommandResultExtract>
       </Extractors>
         </Command>
   </Commands>
</CommandSet>

Groovy bean 的益处有很多。首先,它的脚本是动态配置的,所以可以在运行时更改它。其次,该 bean 可以检测出源发生了变更,并会在下一次调用它时调用 Groovy 编译器,所以它的性能是足够好的。此外,此种语言含有丰富的解析功能,且容易编写。 清单 9 展示了包含内联源代码文本的 Groovy bean:

清单 9. Groovy 格式化 bean
<bean id="FormatCSSServiceResult"
   class="org.runtimemonitoring.spring.groovy.GroovyScriptManager"
   init-method="init" lazy-init="false">
   <property name="sourceCode"><value><![CDATA[
      String[] lines = formatTarget.toString().split("\r\r\n");
      StringBuffer buff = new StringBuffer();
      lines.each() {
         if(!(
            it.contains("Load  Transitions") ||
            it.contains("show service summary") ||
            it.trim().length() < 1)) {
               buff.append(it).append('\n');
           }
      }
      return buff.toString()
      .replaceFirst("State", "Status")
      .replaceFirst("Service Name", "ServiceName")
      .replace("State", "Transitions");
      ]]></value>
   </property>
</bean>

图 4 展示了 CSS 监控的 APM 指标树

图 4. CSS 监控的 APM 树
CSS 监控的 APM 树

SSH 连接

最后一个要考虑的有关 Linux/UNIX shell 收集的问题就是 SSH 连接的问题了。所有 Shell 类的基本接口为 org.runtimemonitoring.spring.collectors.shell.IShell。它定义了一个名为 issueOSCommand() 的方法的两种变体,命令在这个方法中被作为参数传递并且返回结果。在我的使用远程 SSH 类 org.runtimemonitoring.spring.collectors.shell.ssh.JSchRemoteShell 的例子中,底层 shell 调用建立在 Apache Ant 中 SSHEXEC 任务的实现的基础上(参见 参考资料)。这种方法的优点在于它很简单,但是它有一个不可避免的缺点:要为每一个发出的命令创建一个新的连接。这显然会降低效率。一个远程的 shell 只可以每分钟轮询一次,但每一个轮询周期可以执行几个命令来获取监控数据的适当范围。问题是要在监控时窗期间(跨多个轮询周期)保持开放会话是很难的。它需要更详细地检查和解析返回数据,提供不同的 shell 类型以及不断显示 shell 提示,当然 shell 提示不包括在预期返回值中。

目前为止,我一直努力处理长期存活的会话 shell 实现。另外一个选择就是折衷:即为每一个轮询周期模式保留一个连接,但同时试着用一个命令捕获所有数据。这个可以通过追加命令或者(在某些情况下)通过对一个命令使用多个选项来实现。例如,我的 SuSE Linux 服务器上的 sar 版本拥有一个 -A 选项,该选项返回一个包含 sar 支持的所有指标的示例;该命令与 sar -bBcdqrRuvwWy -I SUM -n FULL -P ALL 等效。返回的数据将拥有多个段落,但是用一个命令集来解析它应该没有问题。要查看这样的例子,请参见本文中名为 Suse9LinuxEnterpriseServer.xml 的示例代码中的命令集定义(参见 下载)。


监控时窗

Microsoft Windows 与 Linux/UNIX 之间的本质差别必然导致性能数据收集也迥然各异。Windows 本身几乎没有可以提供丰富的性能报告数据的命令行工具。性能数据也无法通过像相对简单的 /proc 文件系统这样的东西来获取。Windows Performance Manager(WPM)— 也称为 SysMon、System Monitor 或者 Performance Monitor — 是从 Windows 主机获取性能度量的标准接口。它的功能很强大而且拥有大量有用的指标。此外,很多基于 Windows 的软件包都通过 WPM 发布了它们自己的指标。Windows 还通过 WPM 提供了制图、报告和警报设施. 图 5 展示了一个 WPM 实例的屏幕截图:

图 5. Windows Performance Manager
Windows Performance Manager

WPM 管理一组性能计数器,即引用了某一具体指标的复合命名的对象。组成复合名称的有:

  • 性能对象:性能指标的广义范畴,如:处理器内存
  • 实例:当有多个可能成员时,某些性能对象要用一个实例来划分。例如,处理器既有代表单个 CPU 的实例,又有总实例。相反,内存则是一个 “纯” 性能对象,这是因为内存只有一个表现形式。
  • 计数器:实例(如果可用的话)以及性能对象内部的指标的粒度名。例如,处理器实例 0 有一个名为 % Idle Time 的计数器。

根据这些名称片段,表达这些对象的命名约定和语法为:

  • 带实例: \性能对象(实例名)\计数器名
  • 不带实例: \性能对象\计数器名

WPM 最大的缺点就是它访问这个数据有些困难,尤其是远程获取,如果是非 Windows 平台的话,那么困难更大。我将呈现很多使用基于 ITracer 的收集器捕获 WPM 数据的方法。下面总结了一些主要方法:

  • 读取日志文件:可以配置 WPM 以将所有收集的指标记录在日志文件中,然后就可以读取、解析和跟踪该日志文件。
  • 数据库查询:可以配置 WPM 以将所有收集的指标记录在 SQL Server 数据库中,收集到的指标可以在这个数据库中被读取、解析和跟踪。
  • Win32 API:使用 Win32 API(.NET、C++、Visual Basic 等)编写的客户机可以使用 WPM 的 API 直接连接到 WPM。
  • 自定义代理:自定义代理可以安装在目标 Windows 服务器上,该目标 Windows 服务器要能够为外部请求 —— 请求非 Windows 客户机中的 WPM 数据 —— 充当代理服务器。
  • 简单网络管理协议(Simple Network Management Protocol,SNMP):SNMP 是一个代理的一个实例,该代理更加强调它对设备、主机等的监控能力。稍后我将在本文中论述 SNMP。
  • WinRM:WinRM 是 WS-Management 规范的 Windows 实现,它概述了如何使用 Web 服务来管理系统。由于 Web 服务是独立于语言和平台的,因此 WinRM 必然会给非 Windows 客户机提供 WPM 指标访问。虽然 WinRM 可以被当作是代理的另一种形式,但它将成为 Windows 2008 的标准配置,这将把它推向无代理解决方案的舞台。最有趣的是,Java Specification Request 262(Web Services Connector for JMX Agent)承诺要与基于 Windows 的、WS-Management 服务直接交互。

在接下来的例子中,我将使用本地 Windows shell 和代理实现呈现一个理论上的概念验证。

本地 Windows shell

作为一个简单的概念验证,我已经创建了一个可使用 C# 执行的名为 winsar.exe 的 Windows 命令行。它的用途是提供一些与 Linux/UNIX sar 命令相同的对性能统计的命令行访问。使用该命令行的语法很简单:winsar.exe Category Counter Raw Instance.

实例名是强制使用的,除非计数器不是实例计数器,非实例计数器名称可以全是(*)。计数器名也是强制使用的,但也可以全是(*)。Rawtrue 或者 false。清单 10 展示了使用基于实例的计数器和非基于实例的计数器的例子:

清单 10. 使用了非基于实例的和基于实例的计数器的 winsar
C:\NetProjects\WinSar\bin\Debug>winsar Memory "% Committed Bytes In Use" false
 %-Committed-Bytes-In-Use
 79.57401
 
C:\NetProjects\WinSar\bin\Debug>winsar LogicalDisk "Current Disk Queue Length" false C:
         Current-Disk-Queue-Length
C:  2

因为我的目的是重建某种类似 sar 的东西,数据是以杂乱无章的(非格式化的)表格形式输出的,因此可以使用标准 shell 命令集来解析这些数据输出。对于基于实例的计数器来说,实例位于数据行的第一列,计数器名位于标题行。而对于非基于实例的计数器来说,数据行的第一个字段中无名称。为了能够清晰解析,任何带有空格的名称都填充 “-” 字符。这样做虽然会很难看但却可以使解析比较容易。

为这些统计信息(为进行演示而进行了删减)设置一个收集器是相当简单的。shell 实现是一个 org.runtimemonitoring.spring.collectors.shell.local.WindowsShell,命令集引用了 winsar.exe 和参数。shell 还可以被实现为使用 SSH 的远程 shell,要使用 SSH 需要在目标 Windows 主机上安装一个 SSH 服务器。然而,这个解决方案的效率非常低,这主要的原因是由于该实现是基于 .NET 的;在如此短暂的时间内反复启动 Common Language Runtime(CLR)是一种低效的实践。

另一个可能的解决方案是用原生 C++ 重写 winsar。这个就交给 Windows 编程专家来实现吧。.NET 解决方案的效率是可以提高的,但是程序必须要作为后台进程而一直运行,还要以其他某种方式提供对 WPM 数据的请求,并不会在每一个请求结束后终止。为了达到这个目标,我实现了 winsar 中的第二个选项,在这个选项中由 -service 参数来启动程序、读入一个名为 winsar.exe.config 的配置文件并通过 Java Message Service(JMS)监听请求。除了个别项外,文件的内容非常清楚明了。jmsAssembly 项指一个 .NET 汇编的名称,该 .NET 汇编包含着 JBoss 4.2.2 客户机库(它提供 JMS 功能)的一个 .NET 版本。这个汇编是使用 IKVM(参见 参考资料)创建的。respondTopic 引用了公共主题的名称,响应是在该公共主题中发布的,而不是使用私有主题来发布,因此其他监听器也可以接收到数据。commandSet 是一个命令集的引用,该命令集可供一般接收者用来解析和跟踪数据。清单 11 展示了 winsar.exe.config 文件:

清单 11. winsar.exe.config 文件
<configuration>
   <appSettings>
      <add key="java.naming.factory.initial"
         value="org.jnp.interfaces.NamingContextFactory"/>
      <add key="java.naming.factory.url.pkgs"
         value="org.jboss.naming:org.jnp.interfaces"/>
      <add key="java.naming.provider.url" value="10.19.38.128:1099"/>
      <add key="connectionFactory" value="ConnectionFactory"/>
      <add key="listenTopic" value="topic/StatRequest"/>
      <add key="respondTopic" value="topic/StatResponse"/>
      <add key="jmsAssembly" value="JBossClient422g" />
      <add key="commandSet" value="WindowsServiceCommandSet" />
   </appSettings>
</configuration>

在 Spring 中实现这个收集器以使用该服务在概念上和设置 shell 是一回事。事实上,收集器本身就是 org.runtimemonitoring.spring.collectors.shell.ShellCollector 的一个名为 org.runtimemonitoring.spring.collectors.shell.DelegatingShellCollector 的扩展。不同的是这个 shell 扮演着一个普通收集器的角色,并发出对数据的请求,但是数据是通过 JMS 接收的,通过另一个组件解析和跟踪的。被实现的名为 org.runtimemonitoring.spring.collectors.shell.jms.RemoteJMSShell 的 Shell 在行为上类似 shell,但它通过 JMS 分配命令,如图 6 所示:

图 6. 委托收集器
委托收集器

由于这种策略看起来非常适合全面部署代理,所以相同的基于 JMS 的代理是用 Java 代码实现的,它可以部署于任何支持 JVM 的 OS 上。图 7 展示了一个 JMS 发布/订阅性能数据收集系统:

图 7. JMS 发布/订阅监控
JMS pub/sub 监控

另外 JMS 代理发挥功能的方式也不同。本例中解释的模式展示的是目标主机上的一个请求监听 代理,因为这些代理在启动后不会执行任何操作,直到它们从中央监控系统接收到一个请求才执行。然而,这些代理可以按照自己的计划自主地 收集数据并将它发布到相同的 JMS 服务器。监听代理的优势有两个:第一,可以配置收集参数并保存在一个中央位置,而不用发送到每一个目标主机。第二(虽然在本例中没有实现),由于中央请求监听器发出请求,所以监听器可以在特定的已知服务器没有响应的情况下触发一个报警条件。图 8 显示了组合服务器的 APM 树:

图 8. Windows 和 Linux 服务器的 APM 树
Windows 和 Linux 服务器的 APM 树

实现 NSClient

NSClient 和 NC_Net 使用几乎相同的套接字协议来从 Windows 服务器请求数据。nsclient4j(参见 参考资料)是一个 Java API,它包装了这个协议并使 WPM 指标对于任何带有 JVM 的系统可用。本文的示例代码中有一个 nsclient4j Spring 收集器的例子,它位于 /conf/spring-collectors/nsclientj.xml 目录中。

winsar 是一个简单的、早期的原型,它有几个缺点,包括:

  • 通过编程访问一些 WPM 计数器(如处理器对象)会生成空的或原始的指标。所以诸如 CPU % Utilization 这样的指标是可以直接读取的。所需要解决的是如何在规定的时间段内多次读取这些指标,然后就可以计算出 CPU 使用了。winsar 没有这个功能,但是诸如 NSClient 和 NC_Net(参见 参考资料)这样的类似代理却可以提供这个功能:
  • 诚然,将 JMS 作为远程代理的传输机制有它的妙处,但是也有局限。NSClient 和 NC_Net both 使用低级但简单的套接字协议来请求和接收数据。这些服务的本来用途之一就是要将 Windows 数据提供给 Nagios,这是一个几乎排斥 Linux 平台的网络监控系统,因此来自客户端的图像中实际上可能没有 Win32 API。

最后,就像我在前面提到的,SpringCollector 应用程序用一个参数引导,它是一个含有配置 XML 文件的目录。这个目录为 /conf/spring-collectors,它位于示例代码包的根目录中。前面的例子中使用的具体文件为:

  • shell-collectors.jmx:它包含所有 shell 收集器的定义。
  • management.xml:它包含 JMX 管理 bean 和收集调度器。
  • commandsets.xml:它包含 shell 收集器的命令集的定义。这些定义引用了 /commands 中的外部 XML 文件。
  • shells.xml:它包含所有 shell 的定义。
  • jms.xml:它包含 JMS 连接工厂和主题的定义,还有 Java Naming 和 Directory Interface(JNDI)上下文。
  • groovy.xml:它包含 Groovy 格式化程序 bean。

我对 OS 监控的论述就到此为止了。接下来,我将介绍数据库系统的监控。


用 JDBC 监控数据库系统

我经常遇到这种情况,就是认为 DBA 以及他们的工具和应用程序的职责仅仅是监控数据库。然而,要为性能和可用性数据实现非孤立的、集中式 APM 储存库,需要在 DBA 的工作中再补充 一些监视整合的 APM 的工作。我将展示一些用名为 JDBCCollector 的 Spring 收集器来收集数据(这些数据在某种情况下很可能没有受到监控,而且是非常有用的指标)的技术。

您应该考虑的数据收集的广义范畴为:

  • 简单的可用性和响应时间:这是一个简单的机制,可以用它来定期连接到数据库、发布一个简单的查询并跟踪响应时间,或者在连接失败时跟踪服务器停机指标。连接失败不一定表明数据库正在经历硬停机,但它至少可以肯定应用程序一端的通信有问题。孤立的数据库监控可能永远无法指示出数据库连通性问题,但是记住这个还是很有用的,因为可以从那里 连接到一个服务并不意味着也可以从这里 连接。
  • 上下文数据:回顾一下 第 1 部分 中的上下文跟踪 的概念,您可以从应用程序数据的周期性采样获得一些有用的信息。在很多情况下,数据库中的数据活动模式和应用程序设施的行为或性能有着很大的联系。
  • 数据库性能表:很多数据库以表或视图的形式,抑或是通过储存过程来公开内部性能和载入指标。同样,数据可以轻松地通过 JDBC 访问。这个领域显然覆盖了传统 DBA 监控,但是数据库性能通常与应用程序性能的联系非常紧密,以至于如果收集两组指标却不通过一个整合系统来关联它们的话,这简直就是一种巨大的浪费。

JDBCCollector 非常简单。基本上,它由一个查询和一串映射语句组成,它定义了将查询的结果映射到跟踪名称空间的方式。思考一下清单 12 中的 SQL:

清单 12. 一个简单的 SQL 查询
SELECT schemaname, relname, 
SUM(seq_scan) as seq_scan, 
SUM(seq_tup_read) as seq_tup_read
FROM pg_stat_all_tables
where schemaname = 'public'
and seq_scan > 0
group by schemaname, relname
order by seq_tup_read desc

该查询选择一个表格中的四个列。我想将每一个从查询返回的行映射到一个跟踪名称空间,这个名称空间由每行的一部分数据组成。切记名称空间是由片断名称加上指标名称组成的。使用字母值和/或行标记来指定这些值也就定义了映射。行标记代表编号列中的值,如 {2}。处理片断和指标名称时,字母值保持原样不变,而标记在查询结果中使用当前行的各个列值动态替换,如图 9 所示:

图 9. JDBCCollector 映射
JDBCCollector 映射

在图 9 中,我呈现的是对查询的一行响应,但每为每一个返回的行定义一次映射,映射过程就会发生一次。片断的值是 {1}、{2},所以跟踪名称空间的片段的部分是 {"public", "sales_order"}。指标名是字母值,因而保持不变,指标值在第一次映射时被定义为 1,在第二次映射时被定义为 2,它们分别代表着 345916722637。具体的实现该过程便可以进一步澄清它。


使用 JDBC 的上下文跟踪

运行的数据库中的应用程序数据也许包含有用的且有趣的上下文数据。应用程序数据本身并不一定是与性能相关的,但如果抽样这些数据,把它与表示 Java 类的性能、JVM 的健康状况和服务器性能统计信息的历史指标联系起来的话,就可以清楚地了解系统在某一具体的时间段内到底在做些什么。假想您现在正在监控一个极其繁忙的电子商务 Web 站点。您的客户所发出的订单被记录在一个名为 sales_order 的表中,该表还记录了一个惟一的 ID 和发出订单的时间戳。你可以通过抽样在最后 n 分钟内输入的记录数来得出提交订单的速率。

这里是 ITracer 的增量(delta)功能的又一个用武之地,因为我可以设置 JDBCCollector 来从某个特定时间点开始查询行数,并将这个值作为一个增量而跟踪它。结果会得到一个可以描述您的网站有多繁忙的指标(或许还有很多其他指标)。这个指标也会成为一个颇有价值的历史参考。例如,如果知道输入的订单数达到每个周期 50 个时数据库的速度就会减慢的话,那么将会很有用。硬性的、特定的经验数据可以简化容量和增长情况的规划。

接下来我将要实现这个例子。JDBCCollector 使用与前面的例子相同的调度器 bean,它还有一个与我在 第 2 部分 中涉及到的完全相同的 JDBC DataSource。这些数据库收集器是在 /conf/spring-collectors/jdbc-collectors.xml 文件中定义的。清单 13 展示了第一个收集器:

清单 13. 订单执行速率收集器
<bean name="OrderFulfilmentRateLast5s"
  class="org.runtimemonitoring.spring.collectors.jdbc.JDBCCollector"
   init-method="springStart">
   <property name="dataSource" ref="RuntimeDataSource" />
   <property name="scheduler" ref="CollectionScheduler" />
   <property name="query">
      <value><![CDATA[
   select count(*) from sales_order
   where order_date > ? and order_date < ?
   ]]></value>
   </property>
   <property name="logErrors" value="true" />
   <property name="tracingNameSpace" value="Database Context" />
   <property name="objectName" 
      value="org.runtime.db:name=OrderFulfilmentRateLast5s,type=JDBCCollector" />
   <property name="frequency" value="5000" />
   <property name="binds">
      <map>
         <entry><key><value>1</value></key><ref bean="RelativeTime"/></entry>
         <entry><key><value>2</value></key><ref bean="CurrentTime"/></entry>
      </map>
   </property>
   <property name="queryMaps">
      <set>
         <bean class="org.runtimemonitoring.spring.collectors.jdbc.QueryMap">
            <property name="valueColumn" value="0"/>
            <property name="segments" value="Sales Order Activity"/>
            <property name="metricName" value="Order Rate"/>
            <property name="metricType" value="SINT"/>
         </bean>
      </set>
   </property>
</bean>


<bean name="RelativeTime" 
   class="org.runtimemonitoring.spring.collectors.jdbc.RelativeTimeStampProvider">
   <property name="period" value="-5000" />
</bean>

<bean name="CurrentTime" 
   class="org.runtimemonitoring.spring.collectors.jdbc.RelativeTimeStampProvider">
   <property name="period" value="10" />
</bean>

这个例子中的收集器 bean 名为 OrderFulfilmentRateLast5s,类为 org.runtimemonitoring.spring.collectors.jdbc.JDBCCollector。标准 scheduler 收集器被注入,它被当作 JDBC DataSource 的一个引用,RuntimeDataSourcequery 定义要执行的 SQL。SQL 查询既可以把字母值用作参数也可以使用绑定变量(就像在这个例子中一样)。这个例子是人为构造的,因为 order_date 的两个值很容易用 SQL 语法来表示,但是通常情况下,只有需要提供某些外部值时才会用到绑定变量。

为了提供绑定外部值的能力,我需要先实现 org.runtimemonitoring.spring.collectors.jdbc.IBindVariableProvider 接口,然后再将该类作为一个 Spring 管理的 bean 来实现。在这个例子中,我使用了 org.runtimemonitoring.spring.collectors.jdbc.RelativeTimeStampProvider 的两个实例,还有一个通过被传递的 period 属性来提供当前时间戳偏移量的 bean。这些 bean 是 RelativeTime,它返回当前时间减去 5 秒得出的值,还有 CurrentTime,它返回 “现在” 时间加上 10 毫秒的值。这些 bean 的引用通过 binds 属性(一个映射)注入到收集器 bean。映射中的每一个入口值都要与要使用该映射的 SQL 语句中的绑定变量相匹配,这一点至关重要,如若不然就会发生错误或意外的结果。

实际上,我利用了这些绑定变量来获取在大约最后 5 秒钟输入系统的销售订单数。这需要对生产表格执行大量查询,因此要适当调节收集的频率和时间窗口(即,Relative 的时间段)以避免在数据库上实现不恰当的负载。为了帮助正确地调节这些设置,收集器跟踪收集时间一直到 APM 系统,所以运行时间可以用来度量查询开销。更高级的收集器实现会减缓收集的频率,因为监控查询的运行时间增加了。

上面所呈现的映射是通过 queryMaps 属性定义的,该定义使用了一个 org.runtimemonitoring.spring.collectors.jdbc.QueryMap 类型的内部 bean。它有四个简单的属性:

  • valueColumn:应该作为跟踪值而绑定的每一行中的列的索引,该索引是基于零的。在这个例子中,我绑定了 count(*) 的值。
  • segments:跟踪名称空间片断,它被定义为一个单个字母。
  • metricName:指标名称的跟踪名称空间,同样被定义成一个字母。
  • metricTypeITracer 指标类型,它被定义为一个 sticky int

如果想从每一个执行的查询跟踪多个值,收集器允许为每一个收集器定义多个 queryMap。接下来我将向您展示的例子会使用 rowToken 把返回数据的值注入跟踪名称空间,但是现在的例子使用的是字母值。但是,要设计一个使用相同查询的例子的话,我会将查询改为 select count(*), 'Sales Order Activity', 'Order Rate' from sales_order where order_date > ? and order_date < ?。这使得我所期望的片断和指标名称返回于查询中。要想映射它们,我可以将 segments 配置为 {1},将 metricName 设置为 {2}。在某些扩展的情况中,metricType 甚至可能来源于数据库,而且值也可以用 rowToken 来表示。 图 10 显示了这些收集的指标的 APM 树:

图 10. 销售订单速率监控
销售订单速率监控

数据库性能监控

JDBCCollector 可以使用相同的进程来从数据库性能视图中获取和跟踪性能数据。在这个使用了 PostgreSQL 的例子中,这些表 — 称为统计视图— 的名称的前缀为 pg_stat。很多其他的数据库也都拥有类似的视图,并可以使用 JDBC 来访问。在这个例子中,我将使用同一个繁忙的电子商务网站并设置一个 JDBCCollector 来监控最为繁忙的 5 个表上的表格和索引活动。具体的 SQL 展示在清单 14 中:

清单 14. 表格与索引活动监控器
<bean name="DatabaseIO"
   class="org.runtimemonitoring.spring.collectors.jdbc.JDBCCollector"
   init-method="springStart">
   <property name="dataSource" ref="RuntimeDataSource" />
   <property name="scheduler" ref="CollectionScheduler" />
   <property name="availabilityNameSpace"
      value="Database,Database Availability,Postgres,Runtime" />
   <property name="query">
      <value><![CDATA[
   SELECT schemaname, relname, SUM(seq_scan) as seq_scan,
   SUM(seq_tup_read) as seq_tup_read,
   SUM(idx_scan) as idx_scan, SUM(idx_tup_fetch) as idx_tup_fetch,
   COALESCE(idx_tup_fetch,0) + seq_tup_read
      + seq_scan + COALESCE(idx_scan, 0) as total
   FROM pg_stat_all_tables
   where schemaname = 'public'
   and (COALESCE(idx_tup_fetch,0) + seq_tup_read
      + seq_scan + COALESCE(idx_scan, 0)) > 0
   group by schemaname, relname, idx_tup_fetch,
      seq_tup_read, seq_scan, idx_scan
   order by total desc
   LIMIT 5 ]]>
      </value>
   </property>
   <property name="tracingNameSpace"
     value="Database,Database Performance,Postgres,Runtime,Objects,{beanName}"
   />
   <property name="frequency" value="20000" />
   <property name="queryMaps">
    <set>
    <bean class="org.runtimemonitoring.spring.collectors.jdbc.QueryMap">
      <property name="valueColumn" value="2"/>
      <property name="segments" value="{0},{1}"/>
      <property name="metricName" value="SequentialScans"/>
      <property name="metricType" value="SDLONG"/>
    </bean>
    <bean class="org.runtimemonitoring.spring.collectors.jdbc.QueryMap">
      <property name="valueColumn" value="3"/>
      <property name="segments" value="{0},{1}"/>
      <property name="metricName" value="SequentialTupleReads"/>
      <property name="metricType" value="SDLONG"/>
    </bean>
    <bean class="org.runtimemonitoring.spring.collectors.jdbc.QueryMap">
      <property name="valueColumn" value="4"/>
      <property name="segments" value="{0},{1}"/>
      <property name="metricName" value="IndexScans"/>
      <property name="metricType" value="SDLONG"/>
    </bean>
    <bean class="org.runtimemonitoring.spring.collectors.jdbc.QueryMap">
      <property name="valueColumn" value="5"/>
      <property name="segments" value="{0},{1}"/>
      <property name="metricName" value="IndexTupleReads"/>
      <property name="metricType" value="SDLONG"/>
    </bean>
    </set>
</property>
</bean>

查询每隔 20 分钟为最繁忙的 5 个表检索如下的值:

  • 数据库模式的名称
  • 表的名称
  • 顺序扫描的总数
  • 顺序扫描检索出的元组总数
  • 索引扫描的总数
  • 索引扫描检索出的元组总数

后四列是持续增长的值,所以我使用的是 SDLONG 类型的指标,它是一个 sticky 增量 long。注意,在 清单 14 中,我配置了四个 QueryMap 来将四列值映射到跟踪名称空间。

在这个场景中,我构造了一个有用的例子,而没有在 sales_order 表上创建索引。因此,监控将会揭示更多的顺序扫描(按照数据库用语来说为表扫描),它是一个低效的检索数据的机制,这是因为它要读取表中的每一行。顺序元组读取— 主要指使用顺序扫描读取的行的数量 — 也一样。行和元组之间有一个很大的差别,但是在这里没有关系。要弄清楚这个差异,您可以查看 PostgreSQL 文档网站(参见 参考资料)。看一看 APM 显示的这些统计信息,很明显我的数据库遗漏了一个索引。如图 11 所示:

图 11. 顺序读取
顺序读取

注意到了这一点后,我赶紧触发了两个 SQL 语句来索引表格。随后的结果是两个顺序操作都降到了零,而本来为零的索引操作现在却运行起来。如图 12 所示:

图 12. 索引后
索引后

这个索引的创建在整个系统中发生了连锁反应。另一个明显稳定下来的指标是数据库主机上的 User CPU %。如图 13 所示:

图 13. 索引后的 CPU
索引后的 CPU

数据库可用性

我要介绍的有关 JDBC 的最后一个方面是数据库可用性。数据库可用性的最简单的形式是选择标准 JDBCCollector。如果收集器被配置了一个 availabilityNameSpace 值,那么收集器将两个指标跟踪到配置的名称空间:

  • 可用性:可以连接到数据库的话,值为 1,无法连接到数据库的话,值为 0
  • 连接时间:获取连接消耗的运行时间

当用数据源或连接池来获取连接时,连接时间通常都是很快的。但大多数 JDBC 连接池系统都能在分发连接前执行一个可配置的 SQL 语句,所以测试是合法的。在重负载的情况下,连接的获取会有一个非零的运行时间。此外,可以为用于测试可用性的 JDBCCollector 设置单独的数据源。这个单独的数据源可以配置为不共享连接,所以每一个轮询周期都会启动一个新的连接。图 14 显示了我的 PostgreSQL 运行时数据库的可用性检查 APM 树。请参考 清单 14,查看使用 availabilityNameSpace 属性的例子。

图 14. 运行时数据库可用性检查
运行时数据库可用性检查

我见过需要多个连锁查询来决定一个特定状态的情况。例如,一个最终的状态需要对 Database A 进行查询,但它所需的参数只有对 Database B 进行查询才可以确定。这种情况可以用两个 JDBCCollector 来解决,但要特殊考虑如下几点:

  • 按时间先后排列的第一个查询(针对 Database B)被配置为惰性的,因为它没有进行调度(收集的频率为零就意味着无调度)。JDBCCollector 的实例同样实现了 IBindVariableProvider,这意味着它也能够给另一个收集器提供绑定变量和绑定。
  • 第二个收集器把第一个收集器定义为一个绑定,该绑定将会在第一次查询后实现。

我对数据库监控的论述就到此为止了。我必须补充说明一下,这一节重点介绍的是数据库监控,尤其是通过 JDBC 接口的数据库监控。完成一个典型的数据库监控还要监控数据库所在的 OS、单个数据库进程或数据库进程组、还有一些有必要访问数据库服务的相关的网络资源。


监控 JMS 和消息传递系统

本节将描述监控消息传递服务的健康状况和性能的技巧。消息传递服务,如实现 JMS — 同样指面向消息的中间件(message-oriented middleware,MOM)— 的服务,在很多应用程序中都起着至关重要的作用。它们和其他应用程序依赖项一样也需要监控。通常,消息传递服务提供同步的或者说是 “即发即弃(fire-and-forget)” 调用点。监控这些点的难度要大一些,因为从很多角度来看,该服务看起来都运行得很好,服务的调用被频繁地分配,并且运行的时间也很短。仍然保持神秘的是上游瓶颈,消息要经过这里被转发到下一个目标,但消息在这里的传输速度很慢或者根本无法通过。

由于大多数消息传递服务都存在于 JVM 中,抑或是作为一个或多个本机进程而存在于一个主机(或一组主机)上,监控点包括一些与目标服务有关的相同的点。这些目标服务可能包括标准 JVM JMX 属性、支持主机上的监控资源、网络响应,以及诸如内存大小和 CPU 使用这样的服务进程特征。

我将概述消息传递服务监控的四个范畴,它们当中的三个都是专用于 JMS 的,另一个则涉及到专有的 API:

  • 为了度量一个服务的吞吐性能,收集器会定时向该服务发送一组综合的 测试消息,然后等待它们的返回。发送、接收和整个往返过程的总运行时间都被度量、跟踪,同时被度量和跟踪的还有所有失败或超时事件。
  • 很多基于 Java 的 JMS 产品都通过 JMX 公开指标和监控点,所以我将简短地回顾一下如何使用 Spring 收集器实现 JMX 监控。
  • 有些消息传递服务为通信代理的管理提供了一个私有的 API。这些 API 通常包含提取运行中服务的性能指标的能力。
  • 如果缺少上述选项中的任意一个的话,都可以使用诸如 javax.jms.QueueBrowser 这样的标准 JMS 构造来检索有用的指标。

通过综合测试消息来监控消息传递服务

综合消息的前提是将测试消息的发送和接收安排到目标消息传递服务并度量消息的发送、接收和整个往返过程的运行时间。为了设计消息的返回并准确地度量从远程位置发送消息的响应时间,最佳的解决方案就是部署远程代理,它的任务是:

  1. 监听中央监控器的测试信息
  2. 检索它们
  3. 给每一个收到的消息添加一个时间戳
  4. 重新将它们发送回消息传递服务,目的是返回到中央监控器

然后中央监控器可以分析返回的消息,得出进程中的每一个跳跃点(hop)的运行时间并跟踪到 APM 系统。如图 15 所示:

图 15. 综合消息
综合消息

虽然这个方法涉及到了监控的大多方面,但它还是有一些缺点:

  • 它需要部署和管理一个远程代理。
  • 它需要在消息传递服务上为测试消息传输创建其他的队列。
  • 有些消息传递服务允许自动动态创建一级队列,但很多消息传递服务都需要通过管理接口或管理 API 来手动创建队列。

这里介绍的另一个专用于 JMS(但可能在其他消息传递系统中有等效体)的选择是使用临时队列或主题。临时队列可以由标准 JMS API 自动创建,所以不需要介入任何管理。这些临时构造有很多额外的优势,但只有最初的创建者可以看到,其他所有 JMS 参与者都无法看到。

在这个场景中,我将使用 JMSCollector,它可以在启动时自动创建一个临时队列。当调度器提示时,它会将很多测试消息发送给目标 JMS 服务上的临时队列,然后再收回它们。这就有效地测试了 JMS 服务器的吞吐量,而无需创建具体队列或部署远程代理。如图 16 所示:

图 16. 带有临时队列的综合消息
带有临时队列的综合消息

这个场景的 Spring 收集器类为 org.runtimemonitoring.spring.collectors.jms.JMSCollector。配置依赖项相当的简单明了,而且大多数依赖项已经在前面的例子中设置过了。JMS 连通性需要一个 JMS javax.jms.ConnectionFactory。我用来获取它的 Spring bean 与在 Windows WPM 收集的例子中用来获取 JMS 连接工厂的 Spring bean 相同。在这里重新修改一下,它需要 org.springframework.jndi.JndiTemplate 类型的 Spring bean 的一个实例,该类型的 Spring bean 可以给我的目标 JMS 服务提供一个 JNDI 连接;它还需要 org.springframework.jndi.JndiObjectFactoryBean 类型的 Spring bean 的一个实例,该类型的 Spring bean 可以使用 JNDI 连接来查找 JMS 连接工厂。

为了使综合消息负载的组成灵活一些,JMSCollector 被配置了名为 org.runtimemonitoring.spring.collectors.jms.ISyntheticMessageFactory 的接口的一组实现。实现这个接口的对象提供了一组测试消息。收集器会调用每一个配置的工厂,并使用所提供的消息来执行往返测试。用这种方式,我就能够测试我的 JMS 服务器上的吞吐量,因为负载会随着消息大小和消息计数的变化而变化。

每一个 ISyntheticMessageFactory 都有一个可配置的、任意的名称,JMSCollector 把它添加到跟踪名称空间。清单 15 展示了完全的配置:

清单 15. 综合消息 JMSCollector
<!-- The JNDI Provider -->
<bean id="jbossJndiTemplate" class="org.springframework.jndi.JndiTemplate">
   <property name="environment"><props>
      <prop key="java.naming.factory.initial">
         org.jnp.interfaces.NamingContextFactory
      </prop>
      <prop key="java.naming.provider.url">
         localhost:1099
      </prop>
      <prop key="java.naming.factory.url.pkgs">
         org.jboss.naming:org.jnp.interfaces
      </prop>
   </props></property>
</bean>

<!-- The JMS Connection Factory Provider -->
<bean id="RealJMSConnectionFactory"
   class="org.springframework.jndi.JndiObjectFactoryBean">
   <property name="jndiTemplate" ref="jbossJndiTemplate" />
   <property name="jndiName" value="ConnectionFactory" />
</bean>

<!-- A Set of Synthetic Message Factories -->
<bean id="MessageFactories" class="java.util.HashSet">
   <constructor-arg><set>
     <bean
      class="org.runtimemonitoring.spring.collectors.jms.SimpleSyntheticMessageFactory">
         <property name="name" value="MapMessage"/>
         <property name="messageCount" value="10"/>
      </bean>
     <bean
     class="org.runtimemonitoring.spring.collectors.jms.ByteArraySyntheticMessageFactory">
         <constructor-arg type="java.net.URL"
       value="file:///C:/projects3.3/RuntimeMonitoring/lib/jta26.jar"/>
         <property name="name" value="ByteMessage"/>
         <property name="messageCount" value="1"/>
      </bean></set>
   </constructor-arg>
</bean>

<!-- The JMS Collector -->
<bean id="LocalJMSSyntheticMessageCollector"
   class="org.runtimemonitoring.spring.collectors.jms.JMSCollector"
   init-method="springStart">
   <property name="scheduler" ref="CollectionScheduler" />
   <property name="logErrors" value="true" />
   <property name="tracingNameSpace" value="JMS,Local,Synthetic Messages" />
   <property name="frequency" value="5000" />
   <property name="messageTimeOut" value="10000" />
   <property name="initialDelay" value="3000" />
   <property name="messageFactories" ref="MessageFactories"/>
   <property name="queueConnectionFactory" ref="RealJMSConnectionFactory"/>
</bean>

清单 15 实现了两个消息工厂,它们是:

  • javax.jms.MapMessage 工厂,它将当前 JVM 的属性载入到每一条消息的负载中,并被配置为每个周期发送 10 条消息
  • javax.jms.ByteMessage 工厂,它将 JAR 文件中的字节载入到每一条消息的负载中,并被配置为每个周期发送 10 条消息

图 17 显示了综合消息监控的 APM 树。注意字节负载大小被附加到 javax.jms.ByteMessage 消息工厂名称的末尾。

图 17. 带有临时队列的综合消息的 APM 树
带有临时队列的综合消息的 APM 树

通过 JMX 监控消息传递服务

诸如 JBossMQ 和 ActiveMQ 这样的消息传递服务通过 JMX 公开了一个管理接口。我曾在 第 1 部分 中介绍过基于 JMX 的监控。下面我将简短地回顾一下这种监控,然后我将介绍基于 org.runtimemonitoring.spring.collectors.jmx.JMXCollector 类的 Spring 收集器以及使用它来监控 JBossMQ 实例的方法。由于 JMX 是一个恒定的标准,所以可以使用同一过程来监控任何公开 JMX 的指标,而且它的应用范围很广。

JMXCollector 有两个依赖项:

  • 一个名为 LocalRMIAdaptor 的 bean 为本地 JBossMQ 提供了一个 javax.management.MBeanServerConnection。在这种情况下,连接是通过发出一个对 JBoss org.jboss.jmx.adaptor.rmi.RMIAdaptor 的 JNDI 查找来获取的。假定可以提供任何可用的验证凭证,并且 Spring org.springframework.jmx.support 包提供了很多工厂 bean 来获取 MBeanServerConnection(参见 参考资料)的不同实现,这样的话其他的提供者通常很容易获取。
  • JMX 收集属性的一个配置文件,它打包在包含 org.runtimemonitoring.spring.collectors.jmx.JMXCollection 的实例的收集 bean 中。这些是对 JMXCollector 发出的关于要收集哪些属性的指令。

JMXCollection 类展示了一些常见于 JMX 监控器的属性。基本的配置属性有:

  • targetObjectName:它是旨在收集的 MBean 的完整 JMX ObjectName 名称,但它也可以是一个通配符。收集器为所有与通配符模式相匹配的 MBeans 查询 JMX 代理,然后从每一个代理中收集数据。
  • segments:它是 APM 跟踪名称空间的一个片断,收集到的指标被跟踪到了这个 APM 跟踪名称空间。
  • metricNames:它可以是一组指标名称,每一个 MBean 属性都要被映射到该指标名称;也可以是一个 * 字符,它指导收集器使用 MBean 提供的属性名。
  • attributeNames:它是一组必须从每一个目标 MBean 中收集的 MBean 属性名。
  • metricTypes 或者 defaultMetricType:前者是必须用于每一个属性的一组指标类型,后者是必须应用于所有属性的某个指标类型。

MBean ObjectName 通配符功能非常强大,因为它可以有效地发现监控目标,而无需为每一个目标配置监控器。对于 JMS 对列,JBossMQ 为每一个队列创建了一个单独的 MBean,所以如果我想监控每一个队列中的消息数(称为队列长度),我只需要指定诸如 jboss.mq.destination:service=Queue,* 这样的通用通配符,JMS 队列 MBean 的所有实例都是从其中收集到的。但是,由于这些对象是自动发现的,所以如何动态确定队列名就成了另一个难题。在本例中,我知道被发现的 MBean 的 ObjectName name 属性值就是队列的名称。例如,一个被发现的 MBean 的对象名可能是 jboss.mq.destination:service=Queue,name=MyQueue。因此,我需要一种将被发现的对象的属性映射到跟踪名称空间的方式,这样就可以从每一个源中划分出被跟踪的指标。方法就是以与 JDBCCollector 中的 rowToken 相类似的形式来使用标记。JMXCollector 支持的标记有:

  • {target-property:name}:该标记用源自目标 MBean 的 ObjectName 的命名属性来取代。例子:{target-property:name}
  • {this-property:name}:该标记用源自收集器的 ObjectName 的命名属性来取代。例子:{this-property:type}
  • {target-domain:index}:该标记用目标 MBean 的 ObjectName 域的索引片断来取代。例子:{target-domain:2}
  • {this-domain:index}:该标记用收集器的 ObjectName 域的索引片断来取代。例子:{target-domain:0}

清单 16 展示了经过删减的 JBossMQ JMXCollector XML 配置:

清单 16. 本地 JBossMQ JMXCollector
<!-- The JBoss RMI MBeanServerConnection Provider -->
<bean id="LocalRMIAdaptor"
   class="org.springframework.jndi.JndiObjectFactoryBean">
   <property name="jndiTemplate" ref="jbossJmxJndiTemplate" />
   <property name="jndiName" value="jmx/invoker/RMIAdaptor" />
</bean>

<!-- The JBossMQ JMXCollection Profile -->
<bean id="StandardJBossJMSProfile"
   class="org.runtimemonitoring.spring.collectors.collections.InitableHashSet"
   init-method="init" >
   <constructor-arg><set>
      <bean class="org.runtimemonitoring.spring.collectors.jmx.JMXCollection">
         <property name="targetObjectName" value="*:service=Queue,*"/>
         <property name="segments" value="Destinations,Queues,{target-property:name}"/>
         <property name="metricNames" value="*"/>
         <property name="attributeNames"
       value="QueueDepth,ScheduledMessageCount,InProcessMessageCount,ReceiversCount"/>
         <property name="defaultMetricType" value="SINT"/>
       </bean>
       <bean class="org.runtimemonitoring.spring.collectors.jmx.JMXCollection">
         <property name="targetObjectName" value="jboss.mq:service=DestinationManager"/>
         <property name="segments" value="Destniations,{target-property:service}"/>
         <property name="metricNames" value="*"/>
         <property name="attributeNames" value="ClientCount"/>
         <property name="defaultMetricType" value="SINT"/>
       </bean>
       <!-- MBeans Also Included: Topics, ThreadPool, MessageCache -->
       </set>
    </constructor>
</bean>

<!-- The JMXCollector for local JBoss MQ Server -->
<bean id="LocalJBossCollector"
   class="org.runtimemonitoring.spring.collectors.jmx.JMXCollector"
      init-method="springStart">
   <property name="server" ref="LocalRMIAdaptor" />
   <property name="scheduler" ref="CollectionScheduler" />
   <property name="logErrors" value="true" />
   <property name="tracingNameSpace" value="JMS,Local" />
   <property name="objectName"
      value="org.runtime.jms:name=JMSQueueMonitor,type=JMXCollector" />
   <property name="frequency" value="10000" />
   <property name="initialDelay" value="10" />
   <property name="jmxCollections" ref="StandardJBossJMSProfile"/>
</bean>

图 18 显示了用 JMXCollector 监控的 JBossMQ 服务器的 JMS 队列的 APM 树:

图 18. JBossMQ 队列的 JMX 监控的 APM 树
JBossMQ 队列的 JMX 监控的 APM 树

用队列浏览器监控 JMS 队列

如果缺少足够用于监控 JMS 队列的管理 API 的话,可以使用 javax.jms.QueueBrowser。队列浏览器的行为与 javax.jms.QueueReceiver 的行为类似,不同的是获取的信息不会从队列中移除,而且在被浏览器检索之后仍然可以发送。队列深度通常都是一种重要的指标。据观察在很多消息传递系统中,消息生产者要多于消息消费者。这种严重的不平衡从代理队列中消息的数量可以看出来。因此,如果无法用其他的方式访问队列深度的话,那么最后一招就是使用队列浏览器了。该技巧有很多的缺点。为了计数队列中的消息数,队列浏览器一定要检索队列中的每一条信息(然后删除它们)。这样做的效率是很低的,而且要耗费比使用管理 API 多得多的收集时间 — 而且可能在 JMS 服务器的资源上开销更高。队列浏览的另一个问题是,对于繁忙的系统来说,计数在浏览完成的时候很可能就已经是错误的了。虽说如此,要是为了监控的话,近似值还是可以接受的;更何况在高负载的系统中,即便是对队列深度在给定瞬间的高精确度的度量在下一个瞬间也会是无用的了。

队列浏览有一个好处:在浏览队列的信息的过程中,可以确定最老的消息的年龄。这是一个很难获取的指标,即便是使用最好的 JMS 管理 API 也很难获取,而且在某些情况下,它可能是一个至关重要的监控点。思考一下用于传输重要信息的 JMS 队列。消息生产者和消息消费者有着明显的区别,而流量的模式是这样的:对队列深度执行一次标准的轮询通常显示一到两条信息。通常情况下,导致这个问题的原因是存在一定的延迟,而轮询的频率却是一分钟,队列中的消息不同于轮询到轮询间的消息。它们相同么?它们可能不是相同的消息,在这种情况下,上述的情况就是很正常的。但也可能是消息生产者和消息消费者同时遭遇失败,因此队列中被观察的这对消息在每个轮询中是相同的消息。在这种场景中,在监控队列深度的同时监控最老的消息的年龄就会使情况变得很清晰了:正常情况下,消息的年龄要少于几秒,但如果生产者和消费者同时失败的话,从 APM 出现明显的数据只会占用两个轮询周期间的时间。

这个功能在 Spring 收集器的 org.runtimemonitoring.spring.collectors.jmx.JMSBrowserCollector 中有所展示。它的另外两个配置属性有 javax.jms.ConnectionFactory,它与 JMSCollector 类似,还有要浏览的队列的集合。清单 17 展示了该收集器的配置:

清单 17. 本地 JBossMQ JMSBrowserCollector
<!-- A collection of Queues to browse -->
<bean id="BrowserMonitorQueues" class="java.util.HashSet">
   <constructor-arg>
      <set>
         <bean id="QueueA"
            class="org.springframework.jndi.JndiObjectFactoryBean">
            <property name="jndiTemplate" ref="jbossJndiTemplate" />
            <property name="jndiName" value="queue/A" />
         </bean>
               <bean id="QueueB"
            class="org.springframework.jndi.JndiObjectFactoryBean">
            <property name="jndiTemplate" ref="jbossJndiTemplate" />
            <property name="jndiName" value="queue/B" />
         </bean>
      </set>
   </constructor-arg>
</bean>

<!-- the JMS Queue Browser -->
<bean id="LocalQueueBrowserCollector"
   class="org.runtimemonitoring.spring.collectors.jms.JMSBrowserCollector"
     init-method="springStart">
   <property name="scheduler" ref="CollectionScheduler" />
   <property name="logErrors" value="true" />
   <property name="tracingNameSpace" value="JMS,Local,Queue Browsers" />
   <property name="frequency" value="5000" />
   <property name="initialDelay" value="3000" />
   <property name="queueConnectionFactory" ref="RealJMSConnectionFactory"/>
   <property name="queues" ref="BrowserMonitorQueues"/>
</bean>

图 19 展示了该收集器的 APM 树:

图 19. JMSBrowserCollector 的 APM 树
APM tree for JMSBrowserCollector

作为一个测试机制,一个载入脚本开始循环,在循环中向每一个队列发送了几百条消息。在每一个循环中,被清除的队列是随机选取的。因此,每一个队列中的消息年龄的上限都会随着时间的推移而随意变化。

使用私有 API 监控消息传递系统

有些消息传递系统拥有实现诸如监控这样的管理功能的私有 API。一些消息传递系统使用请求/响应 的模式来用它们自己的消息传递系统提交管理请求。ActiveMQ(参见 参考资料)提供了一个 JMS 通信管理 API 以及一个 JMX 管理 API。实现一个私有的管理 API 需要一个自定义的收集器。在这个小节中,我将呈现 WebSphere® MQ(原来称为 MQ Series)的收集器。该收集器结合使用了两种 API:

  • MS0B: WebSphere MQ Java classes for PCF:PCF API 是 WebSphere MQ 的一个管理 API。
  • The core WebSphere MQ Java classes:原来称为 MA88 的 API 已经被合并到了核心 WebSphere MQ Java 类库中(参见 参考资料)。

其实使用两个 API 是多余的,但是毕竟展示了两种不同的私有 API 的使用方法。

Spring 收集器实现是一个名为 org.runtimemonitoring.spring.collectors.mq.MQCollector 的类。它监控 WebSphere MQ 服务器上的所有队列,收集每一个队列的队列深度以及当前打开的输入/输出处理的数量。清单 18 展示了 org.runtimemonitoring.spring.collectors.mq.MQCollector 的配置:

清单 18. WebSphere MQ 收集器
<bean id="MQPCFAgentCollector"
   class="org.runtimemonitoring.spring.collectors.mq.MQCollector"
      init-method="springStart">
   <property name="scheduler" ref="CollectionScheduler" />
   <property name="logErrors" value="true" />
   <property name="tracingNameSpace" value="MQ, Queues" />
   <property name="frequency" value="5000" />
   <property name="initialDelay" value="3000" />
   <property name="channel" value="SERVER1.QM2"/>
   <property name="host" value="192.168.7.32"/>
   <property name="port" value="50002"/>
</bean>

这里独特的配置属性有:

  • hostWebSphere MQ 服务器的主机名称的 IP 地址
  • port:WebSphere MQ 进程在该端口监听连接
  • channel:要连接到的 WebSphere MQ 通道

注意这个例子不包含任何的验证方面。

图 20 展示了为 org.runtimemonitoring.spring.collectors.mq.MQCollector 生成的 APM 树:

图 20. MQCollector 的 APM 树
APM Tree for MQCollector

我对消息传递服务监控的论述就到此为止。正如我在前面提到的,接下来我将介绍如何用 SNMP 进行监控。


使用 SNMP 进行监控

SNMP 原本是作为应用层协议而创建的,该协议用来在诸如路由器、防火墙和交换机这样的网络设备间交换信息。这也许仍然是它最常用的功能,但是它也可以充当监控性能和可用性的灵活的标准化协议。SNMP 的整个主题以及它作为一个监控工具的实现超出了本文的讨论范围。但是,SNMP 在监控领域已经非常普及,因此难免会有所遗漏。

SNMP 中的核心结构之一就是代理,它负责代理针对某一个设备的 SNMP 请求。SNMP 相对简单且较低级,所以 SNMP 代理可以简单且快捷地嵌套到多种硬件设备和软件服务中。因此 SNMP 就是一个在应用程序生态系统中监控大量服务的标准化协议。另外,SNMP 被广泛用于通过扫描一系列的 IP 地址和端口来执行发现。从监控的角度看,SNMP 的这个应用可以节省一些管理时间,因为它能够自动填充和更新一个监控目标的集中式清单。SNMP 在很多方面都与 JMX 极其相似。虽然两者之间还是有一些明显的差别,但还是可以在两者间找出一些相同的内容,而且 JMX 和 SNMP 间的互操作性被广泛地支持与实现。表 1 总结了一些两者之间的等价内容:

表 1. SNMP 和 JMX 对比
SNMP 结构等效 JMX 结构
代理代理或者 MBeanServer
管理程序客户机、MBeanServerConnection、协议适配器
MIBMBeanObjectNameMBeanInfo
OIDObjectNameObjectName+ Attribute
陷阱JMX Notification
GETSETgetAttributesetAttribute
BULKGETgetAttributes

单从监控角度考虑,发布一个 SNMP 查询时,我需要了解的重要因素有:

  • 主机地址:目标 SNMP 代理所在的 IP 地址或主机名。
  • 端口:目标 SNMP 代理在其上进行监听的端口。由于一个网络地址可能在处理很多个 SNMP 代理,每一个代理都需要在不同的端口上进行监听。
  • 协议版本:SNMP 协议已经经历过多次的修订了,而支持级别会根据不同的代理而变化。可选的版本有:1、2c 和 3。
  • 社区:SNMP 社区是一个松散定义的管理域。如果社区不是已知的话,SNMP 客户机就无法凭代理发出请求,所以发挥着松散形式的验证作用。
  • OID:这是一个指标或一组指标的惟一标示符。其格式为一系列用点分隔的整数。例如,Linux 主机的一分钟载入的 SNMP OID 为 .1.3.6.1.4.1.2021.10.1.3.1,由 1、5 和 15 分钟载入的指标子集的 OID 为 .1.3.6.1.4.1.2021.10.1.3

除了社区以外,有些代理还可以定义其他层面的验证。

在我深入论述 SNMP API 之前,要注意 SNMP 指标可以使用两个普通的命令行实用程序来检索,它们是: snmpget,它检索一个 OID 的值;snmpwalk,它检索 OID 值的子集。记住了这点,我就能够经常扩展我的 ShellCollector CommandSet 来跟踪 SNMP OID 值了。清单 19 示范了一个 snmpwalk 的例子,它带有在 Linux 主机上检索 1、5 和 15 分钟载入的原始的输出。我使用的是该协议的 2c 版本和公共社区。

清单 19. snmpwalk 示例
$> snmpwalk -v 2c -c public 127.0.0.1 .1.3.6.1.4.1.2021.10.1.3
UCD-SNMP-MIB::laLoad.1 = STRING: 0.22
UCD-SNMP-MIB::laLoad.2 = STRING: 0.27
UCD-SNMP-MIB::laLoad.3 = STRING: 0.26

$> snmpwalk -v 2c -c public 127.0.0.1 .1.3.6.1.4.1.2021.10.1.3 \
   | awk '{ split($1, name, "::"); print name[2] " " $4}'
laLoad.1 0.32
laLoad.2 0.23
laLoad.3 0.22

第二条命令可以轻松地在我的 Linux 命令集中表现出来,如清单 20 所示:

清单 20. 处理 snmpwalk 命令的 CommandSet
<CommandSet name="LinuxSNMP">
   <Commands><Command>
      <shellCommand><![CDATA[snmpwalk -v 2c -c public 127.0.0.1
      .1.3.6.1.4.1.2021.10.1.3 | awk '{ split($1, name, "::"); print name[2] "
      " $4}']]></shellCommand>
         <Extractors><CommandResultExtract>
             <paragraph id="0" name="System Load Summary" header="false"/>
                <columns entryName="0" values="1" offset="0">
                   <namemapping from="laLoad.1" to="1 Minute Load"/>
                    <namemapping from="laLoad.2" to="5 Minute Load"/>
                    <namemapping from="laLoad.3" to="15 Minute Load"/>
                </columns>
                <tracers default="SINT"/>
                <lineSplit>\n</lineSplit>
          </CommandResultExtract></Extractors>
      </Command></Commands>
</CommandSet>

商用的、开源 SNMP Java API 有好多个。我实现了一个基本的 Spring 收集器,它的名称为 org.runtimemonitoring.spring.collectors.snmp.SNMPCollector,该收集器的实现使用到了一个名为 joeSNMP 的开源 API(参见 参考资料)。该收集器拥有如下的重要配置属性:

  • hostName:目标主机的主机名或 IP 地址。
  • port:目标 SNMP 代理正在其上进行监听的端口的数量(默认为 161)。
  • targets:一组由 org.runtimemonitoring.spring.collectors.snmp.SNMPCollection 的实例组成的 SNMP OID 目标。该 bean 的配置属性有:
    • nameSpace:跟踪名称空间的后缀。
    • oid:跟踪名称空间的后缀。
  • protocol:SNMP 协议, 0 表示 v1,1 表示 v2(默认为 v1)。
  • community:SNMP 社区(默认为 public)。
  • retries:尝试操作的次数(默认为 1)。
  • timeOut:以 ms 为单位的 SNMP 调用的超时时间(默认为 5000)。

清单 21 展示了 SNMPCollector 设置的示例配置,该配置的目的是监控本地 JBoss 应用服务器:

清单 21. SNMPCollector 的配置
<!-- Defines the SNMP OIDs I want to collect and the mapped name -->
<bean id="JBossSNMPProfile" class="java.util.HashSet">
   <constructor-arg><set>
     <bean class="org.runtimemonitoring.spring.collectors.snmp.SNMPCollection">
             <property name="nameSpace" value="Free Memory"/>
             <property name="oid" value=".1.2.3.4.1.2"/>
     </bean>
     <bean class="org.runtimemonitoring.spring.collectors.snmp.SNMPCollection">
             <property name="nameSpace" value="Max Memory"/>
             <property name="oid" value=".1.2.3.4.1.3"/>
     </bean>
     <bean class="org.runtimemonitoring.spring.collectors.snmp.SNMPCollection">
             <property name="nameSpace" value="Thread Pool Queue Size"/>
             <property name="oid" value=".1.2.3.4.1.4"/>
     </bean>
     <bean class="org.runtimemonitoring.spring.collectors.snmp.SNMPCollection">
             <property name="nameSpace" value="TX Manager, Rollback Count"/>
             <property name="oid" value=".1.2.3.4.1.7"/>
     </bean>
     <bean class="org.runtimemonitoring.spring.collectors.snmp.SNMPCollection">
             <property name="nameSpace" value="TX Manager, Current Count"/>
             <property name="oid" value=".1.2.3.4.1.8"/>
     </bean>
  </set></constructor-arg>
</bean>

<!-- Configures an SNMP collector for my local JBoss Server -->
<bean id="LocalJBossSNMP"
   class="org.runtimemonitoring.spring.collectors.snmp.SNMPCollector"
   init-method="springStart">
   <property name="scheduler" ref="CollectionScheduler" />
   <property name="logErrors" value="true" />
   <property name="tracingNameSpace" value="Local,JBoss,SNMP" />
   <property name="frequency" value="5000" />
   <property name="initialDelay" value="3000" />
   <property name="hostName" value="localhost"/>
   <property name="port" value="1161"/>
   <property name="targets" ref="JBossSNMPProfile"/>
</bean>

该收集器的确是有些缺点,毕竟配置有些长,而且由于它要为每一个 OID 做出一个调用而非批量收集,导致运行时效率低下。示例代码中的 snmp-collectors.xml 文件(参见 下载)也包含了一个监控 Linux 服务器的 SNMP 收集器的配置示例。图 21 显示了 APM 系统指标树:

图 21. SNMPCollector 的 APM 树
SNMPCollector 的 APM 树

此时,您可能了解到如何创建收集器了。要覆盖整个环境,可能需要集中不同类型的收集器,如果您对此感兴趣的话,您可以参见本文的示例代码,那里包含有其他监控目标的收集器示例。它们都位于 org.runtimemonitoring.spring.collectors 包中。表 2 是这些收集器的一个概述:

表 2. 其他收集器示例
收集目标
Web 服务:检查安全 Web 服务的响应时间和可用性 webservice.MutualAuthWSClient
Web 服务和 URL 检查webservice.NoAuthWSClient
Apache Web Server、性能和可用性apacheweb.ApacheModStatusCollector
Pingnetwork.PingCollector
NTop:一个收集详细网络统计信息的实用程序network.NTopHostTrafficCollector

数据管理

收集繁忙的可用性和性能数据的系统所面临的一个最复杂的难题就是:如何有效地将收集到的数据持久化到普通指标数据库中。对于要使用的数据库和持久化机制要考虑的问题有:

  • 指标数据库一定要支持对历史指标的适当快速的、简单的查询,以实现数据可视化、数据报告和数据分析。
  • 指标数据库一定要保留数据的历史和粒度,从而支持报告的时窗、准确性和所需精确度。
  • 持久化机制一定要足够好地、并行地运行,避免影响前端监控的活跃性。
  • 指标数据的检索和储存要能够并行运行,且两者都不会对方有任何负面的影响。
  • 从数据库中请求数据的请求要能够支持一段时间内的聚合。
  • 数据库中的数据要以适当的方式存储,允许使用一种时间序列模式检索数据或使用某种机制保证多个数据点能够在有效时间段相同的情况下相互关联。

鉴于这些考虑,您需要:

  • 一个性能良好的、可伸缩的、带有很多磁盘空间的数据库。
  • 一个带有有效搜索算法的数据库。本质上讲,由于指标将会通过复合名称来储存,所以解决方案就是将复合名称储存为一个字符串并使用某种形式的模式匹配来指定目标查询指标。很多关系数据库都支持内置于 SQL 语法的正则表达式,这是一个通过复合名称查询的理想方法,但是这个方法比较慢,这是因为它通常排除使用索引。但是很多关系数据库同样支持功能索引,该索引能够在使用模式匹配搜索时用来加快查询的速度。另外一个选择是完全标准化 数据并分别取出复合名称的单个片断(参见下面的 标准化数据库结构和扁平数据库结构 )。
  • 限制写入数和写入数据库的总数据量的策略就是实现一连串的分层聚合和合并。这可以在数据被写入数据库前通过保持一个总体缓冲器来完成。在指定的时间段内,将所有标记为储存的指标写入一个累计缓冲器,它可以保留指标有效启动时间和终止时间,以及在该段时间内的平均的、最小的和最大的指标值。这样,指标值就可以在写入数据库前被聚合和合并。例如,如果一个收集器的频率是 15 秒,而聚合时间是 1 分钟的话, 4 个单个指标将会被合并成为 1 个。这里假设可以允许持久化的数据丢失粒度,所以要在低数据量的低粒度和高数据量的高粒度之间做出权衡。同时,指标的实时可视化可以通过从聚合前的常驻内存循环缓冲器呈现图形来实现,这样就只有持久化的数据被聚合了。当然,您有权选择哪些指标需要持久化。
  • 另外一个减少储存数据量的策略是实现一个采样频率,在这个采样频率中只有每 y 行中的 x 个指标会被储存。这也会降低持久化数据的粒度,但会占用比维护聚合缓冲器更少的资源(尤其是内存)。
  • 持久化的指标数据还可以在指标数据库中累积并清除。同样,在容忍的粒度丢失范围内,可以将数据库中几个时间段内的数据分小时、天、周、月等累计到汇总表格中;你也可以清除原始数据;仍然保留适当的有用的指标数据。
  • 为了避免数据持久化过程的活动影响到数据收集器本身,有必要实现一个非侵入式的后台线程,它能够将离散的指标数据的收集刷新到数据储存进程。
  • 对于多个拥有不同有效时窗的数据集,创建一个类似时间序列的报告有些困难。思考一下图形中表示时间的 x 轴,和表示特定指标的值和多个序列(直线或曲线)(代表来源于不同资源集的相同指标)的 y 轴。如果读取每一个序列的有效时间戳有很大差别的话,就一定要传输数据以保持图形表示有效。这个可以通过将数据聚合到所有绘制的序列中最不一致的时窗来实现,同样粒度也会丢失。更简单的解决方案是维护一个在所有指标值中都一致的、有效的时间戳。时间序列数据库有这个功能。常用的是 RRDTool,它跨序列中所有不同的数据值有效地实施一致的、均衡分布的时间戳。为了使有效的时间戳在关系数据库中保持一致,一个绝佳的策略就是用一个单一的、统一的调度程序对所有指标取样。例如,一个计时器可能每两分钟触发一次,结果导致在那一刻捕获了所有指标的一个单一的 “交换”,而且所有的指标都用相同的时间戳标记,然后放入队列中等待持久化。

显然,要以一种最佳的方式来解决上述的每一个问题是很难的,所以必须要考虑折衷的办法。图 22 展示了一个数据收集器在概念上的数据持久化流程:

图 22. 数据管理流程
数据管理流程

图 22 表示概念数据收集器(像本文中呈现的 Spring 收集器)的数据流。目的是通过一些层推动单个指标跟踪度量,直到它们被持久化。具体的过程为:

  1. 数据收集器是随意配置的,为了实现带有如下属性的指标持久化:
    • 启用持久化:True 或 false。
    • 指标名称过滤器:一个正则表达式。匹配正则表达的指标名会被标记为持久化。
    • 采样计数:会跳过持久化的指标收集的数量,也就是说 0 意味着全部持久化。
    • 历史缓存大小:必须保存在缓存中的被跟踪的指标的数量。为了实现实时可视化,必须为历史缓存启用大多数指标,即使这些指标未被标记为持久化,这样它们可以呈现为实时图表。
  2. 收集器缓存就是很多的离散指标读数,该读数与收集器生成的各种指标的数量相等。缓存遵循先入先出(first in, first out,FIFO)原则,所以当历史缓存变满时,最老的指标会被清除,为最新的指标腾出空间。缓存支持注册缓存事件监听器,它们可以被告知有哪些指标被从缓存中移除,又有哪些被存入。
  3. 收集器总计是一系列由收集器生成的指标实例的两个或更多循环缓存。由于循环缓冲器中的每一个缓存实例变为活动状态,它就会为历史缓存注册新的跟踪事件并聚合每一个输入的新值,有效地合并给定时间段内的数据流。该总计包含:
    • 该时间段的启动和终止时间(终止时间在时间段结束前是未定的)。
    • 该时间段内的最小、最大和平均读数。
    • 指标类型。
  4. 中央计时器会在每个时间段末尾触发一个刷新事件。当计时器触发后,收集器总计循环缓冲器索引是递增的,而历史缓存事件被传递给缓冲器中的下一个缓存。然后 “关闭的” 缓冲器中的每一个聚合的总计都会有一个公用的时间戳应用于该总计,并被写入待存入数据库的持久化队列。

    注意当循环缓冲器递增时,下一个缓冲元素的最小的、最大的和平均值将会是零,除了 sticky 指标类型。

  5. 持久化线程池中的一池线程从持久化队列中读取总计项并将它们写入数据库。

相同收集器总计被合并的总计时间段的长度要取决于中央刷新计时器的频率。该时间段越长,写入数据库的数据就越少,代价是数据粒度将会丢失。

标准化数据库结构与扁平数据库结构

RRDTool:集全部监视功能为一体的工具

RRDTool(Round Robin Database Tool)是一个强大的工具,它在一个单独的可执行文件中提供了大量功能(参见 参考资料)。RRDTool 不会为您收集任何实际数据,但是它通常被与 snmpgetsnmpwalk 和其他命令行工具结合使用来创建一种综合监控解决方案。RRDTool 使您能够创建循环(round-robin)的时间序列数据库文件(循环特性使文件大小保持不变,避免文件不受控制地增长),以及针对多个聚合调度的一些选项。然后,它可以从文件查询并导出数据,生成丰富的图表以实现数据可视化和动态报告。添加一个简单的 Web 服务器来持续刷新包含图表和交叉表格数据的页面,从而得到一个功能强大的监控解决方案。

一些专用数据库(比如 RRDTool)可以用于指标存储,但是通常使用的是关系数据库。在关系数据库中执行指标存储时,需要考虑两个一般选项:标准化数据或保持一种更加扁平的结构。这主要关系到如何存储复合指标名称;根据数据建模最佳实践,应该对所有其他引用数据进行标准化。

完全标准化的数据库结构的好处在于,通过将指标复合名称分解为独立的片段,实际上任何数据库都可以利用索引功能来加速查询。它们存储的冗余数据更少,从而使数据库更小,数据密度更高,性能也更好。其缺点在于复杂性和查询的大小:即使相对简单的复合指标名称或指标名称的模式(例如主机 11 到 93 之间的 % User CPU)都要求 SQL 包含一些连接和大量谓词。这个缺点可以通过使用数据库视图和硬存储常用指标名解码来减轻。每个单独指标的存储需要分解复合名称来定位指标引用数据项(或创建一个新的引用数据项),但是这种性能开销可以通过在持久过程中缓存所有引用数据来减轻。

图 23 展示一种用于在标准化结构中存储复合指标的模型:

图 23. 一种用于指标存储的标准化模型
一种用于指标存储的标准化模型

在图 23 中,为每个惟一的复合指标分配了一个惟一的 METRIC_ID,为每个惟一的片段分配了一个 SEGMENT_ID。然后在一个包含 METRIC_IDSEGMENT_ID 和片段出现顺序的关联实体中构建复合名称。指标类型存储在引用表 METRIC_TYPE 中,而指标值本身存储在 METRIC_INSTANCE 中(包含值、时间戳的开始和结束属性,以及对指标类型和惟一 METRIC_ID 的引用)。

另一方面,扁平模型结构非常简单,如图 24 所示:

图 24. 用于指标存储的扁平模型
用于指标存储的扁平模型

在本例中,我将指标名称从复合名称中分离开来,剩余的片段仍然保持其在 segments 列中的原有模式。同样,如果实现的数据库引擎能够使用基于模式的谓词(比如正则表达式)执行适合较宽的文本列的查询,那么这种模型非常易于查询。这种优点应当重视。使用简化的查询结构,能够显著地简化数据分析、可视化和报告工具,而且在紧急的分类会话期间,加快查询的编写速度可能是最重要的方面!

如果需要持久化数据存储,挑选正确的数据库是非常关键的一步。使用关系数据库是一种可行的方法,因为关系数据库能够很好地执行,并且可以从中提取数据并针对您的需要进行格式化。时间序列驱动的数据的生成比较复杂,但是通过正确地分组和聚合 —— 以及使用其他工具,比如 JFreeChart(参见 参考资料)—— 可以生成出色的典型报告和图表。如果想要实现一种更加专门化的数据库(比如 RRDTool),那么请做好心理准备,因为这种数据库的提取和报告需要很长时间。如果数据库不支持 ODBC、JDBC 等标准,那么将不能使用常用的标准化报告工具。

对数据管理的讨论到此为止。本文最后一节将提供一些用于实时可视化数据的技术。


可视化和报告

在某些情况下,将实现插装和性能数据收集器,数据将会流入您的 APM 系统中。那么下一个逻辑步骤就是实时地查看数据的可视化表示。我在这里宽泛地使用实时 这个词,其含义是可视化地表示最近收集的数据。

常用的数据可视化术语是指示板。指示板能够可视化地呈现您所考虑的数据模式或活动的所有方面,它们惟一的限制就是数据的质量和数量。本质上,指示板告诉您生态系统中正在发生什么。指示板在 APM 系统中的一个强大之处是,它能够通过一种统一的显示形式表示各种各样的数据(即从不同来源收集的数据)。例如,一种显示形式可以在指示板上同时显示数据库中 CPU 的最近使用情况和当前趋势、应用服务器之间的网络活动,以及当前登录到您的应用程序的用户数量。在本节中,我将提供一些不同的数据可视化样式,以及我在本文前面提供的 Spring 收集器的一个可视化层实现示例。

Spring 收集器可视化层的前提条件包括:

  • 将一个缓存类实例部署为一个 Spring bean。可以配置缓存类实例来保存任何数量的 ITracer 历史跟踪,但它的大小是固定的并且遵循先进先出的规则。一个缓存可以配置为保存 50 条历史记录,这意味着一旦缓存被填满,它将保存最近的 50 条历史记录。
  • Spring 收集器在 Spring XML 配置文件中使用 cacheConfiguration 进行配置,使用一个缓存进程封装 ITracer 实例。配置还将收集器与定义的缓存实例相关联。收集的跟踪记录像平常一样进行处理,但是添加了与收集器相关联的缓存实例。我们看一下前面的示例,如果缓存的历史记录大小为 50,并且收集器每隔 30 秒收集一次,那么当缓存被填满时,它将保存收集器在最近 25 分钟内收集的历史跟踪记录。
  • Spring 收集器实例部署了大量呈现类。呈现器(Renderer)是一些用于实现 org.runtimemonitoring.spring.rendering.IRenderer 接口的类。它们的工作是从缓存获取一系列数据并呈现这些数据的某种可视化形式。从呈现器定期检索可视化媒体将生成最新的表示,或者与缓存数据一样新的表示。
  • 呈现内容然后可以在一种指示板上下文(比如一个 Web 浏览器)中传递到一个客户机或某种形式的富客户机。

图 25 列出了这一过程:

图 25. 缓存和呈现
缓存和呈现

本例中的缓存实现是 org.runtimemonitoring.spring.collectors.cache.TraceCacheStore。可以将其他对象注册为缓存事件监听器,这样,在其他事件中,呈现器可以监听表示新值被添加到缓存的新缓存项 事件。通过这种方式,呈现器能够实际地缓存它们生成的内容,并在新数据可用时使缓存无效。来自呈现器的内容通过一个称为 org.runtimemonitoring.spring.rendering.MediaServlet 的 servlet 传递到客户机指示板。该 servlet 解析它接收到的请求,定位呈现器,并请求内容(所有内容都以一个字节数组的形式呈现和传递)和内容的 MIME 类型。字节数组和 MIME 类型然后流动到客户机,以供客户机解释。处理来自基于 URL 的服务的图形内容是最理想的,因为它们可以被 Web 浏览器、富客户机及它们之间的所有内容使用。当呈现器从媒体服务器接收到一个内容请求时,内容将从缓存传递,除非该缓存被一个缓存事件标记为 dirty。通过这种方式,呈现器不需要对每个请求重新生成它们的内容。

以字节数组格式生成、缓存和传递可视化的媒体非常有用,因为这种格式占用的空间最小,并且大多数客户机都可以在提供了 MIME 类型的情况下重新建立内容。由于这种实现在内存中缓存生成的内容,所以我使用了一种压缩方案。总体内存消耗随着缓存内容的增多而显著增加;同样,如果内容附带提供了压缩算法标记,那么大多数客户机都能够解压缩。例如,时下流行的大多数浏览器都支持 gzip 解压缩。然而,合理的压缩级别不一定要非常高(对于较大的图像来说,30-40% 的压缩率就不错了),因此,呈现实现可以缓存到磁盘,或者如果磁盘访问开销更高,动态地重新生成内容可能会需要更少的资源。

在这里,使用一个具体示例将有助于理解。我设置了两个 Apache Web Server 收集器来监控大量繁忙的 worker 线程。每个收集器都分配有一个缓存,而且我设置了少量的呈现器,用以提供图表来显示每个服务器上繁忙的 worker 线程。在本例中,呈现器生成一个 PNG 文件来显示两个服务器的时间序列线图及其序列。一个服务器的收集器和缓存设置如清单 22 所示:

清单 22. 一个 Apache Web Server 收集器和缓存
<!-- The Apache Collector -->
<bean id="Apache2-AP02"
   class="org.runtimemonitoring.spring.collectors.apacheweb.ApacheModStatusCollector"
   init-method="springStart">
   <property name="scheduler" ref="CollectionScheduler" />
   <property name="logErrors" value="true" />
   <property name="tracingNameSpace" value="WebServers,Apache" />
   <property name="frequency" value="15000" />
   <property name="initialDelay" value="3000" />
   <property name="modStatusURL" value="http://WebAP02/server-status?auto" />
   <property name="name" value="Apache2-AP02" />
   <property name="cacheConfiguration">
      <bean
         class="org.runtimemonitoring.spring.collectors.cache.CacheConfiguration">
         <property name="cacheStores" ref="Apache2-AP02-Cache"/>
      </bean>
   </property>
</bean>

<!-- The Apache Collector Cache -->
<bean id="Apache2-AP02-Cache"
   class="org.runtimemonitoring.spring.collectors.cache.TraceCacheStore">
   <constructor-arg value="50"/>
</bean>

注意收集器中的 cacheConfiguration 属性,以及它如何引用称为 Apache2-AP02-Cache 的缓存对象。

我还设置了一个呈现器,它是 org.runtimemonitoring.spring.rendering.GroovyRenderer 的一个实例。这个呈现器将所有呈现任务委派给文件系统的一个底层 Groovy 脚本。这是最理想的方法,因为我能够在运行时对其进行调整,从而调整生成的图的细节。这个呈现器的一般属性包括:

  • groovyRenderer:一个对 org.runtimemonitoring.spring.groovy.GroovyScriptManager 的引用,它被配置为从一个目录载入 Groovy 脚本。这也是我用于将从 Telnet 会话返回的数据传递到我的 Cisco CSS 的类。
  • dataCaches:一组缓存,呈现器从中请求数据并呈现出来。呈现器也注册为当缓存添加新项时从缓存接收事件。当它这样做时,它将其内容标记为 dirty,并对下一个请求重新生成内容。
  • renderingProperties:传递给呈现器的默认属性,指定生成的图形的具体细节,比如图像的默认大小。您将会在下面看到,这些属性可以被客户机请求覆盖。
  • metricLocatorFilters:一个收集器缓存,包含收集器生成的每个指标的缓存跟踪。这个属性允许指定一个正则表达式数组来筛选您想要的指标。

缓存设置如清单 23 所示:

清单 23. 用于 Apache Web Server 繁忙 worker 线程监控的图形呈现器
<bean id="Apache2-All-BusyWorkers-Line"
   class="org.runtimemonitoring.spring.rendering.GroovyRenderer"
         init-method="init"
         lazy-init="false">
   <property name="groovyRenderer">
      <bean class="org.runtimemonitoring.spring.groovy.GroovyScriptManager">
         <property name="sourceUrl" value="file:///groovy/rendering/multiLine.groovy"/>
      </bean>
   </property>
   <property name="dataCaches">
      <set>
         <ref bean="Apache2-AP01-Cache"/>
          <ref bean="Apache2-AP02-Cache"/>
      </set>
   </property>
   <property name="renderingProperties">
      <value>
        xSize=700
        ySize=300
        title=Apache Servers Busy Workers
        xAxisName=Time
        yAxisName=# of Workers Busy
      </value>
   </property>
   <property name="metricLocatorFilters" value=".*/Busy Workers"/>
</bean>

呈现器非常容易实现,但是我发现,我经常需要对它们进行调整,因此这里列出的 Groovy 方法非常适用于对新的图表类型或者新的图形包进行快速原型化。编译 Groovy 代码时,性能非常好,实现出色的内容缓存也不是问题。动态热更新功能和功能强大的 Groovy 语法使动态更新变得非常轻松。随后,当我确定了呈现器要做的工作以及它应该支持哪些选项时,我将它们移植到 Java 代码。

指标名称由 org.runtimemonitoring.tracing.Trace 类生成。这个类的每个实例表示一个 ITracer 读操作,所以它封装了跟踪的值、时间戳和完整的名称空间。指标的名称就是完整的名称空间。在本例中,我所显示的指标是 WebServers/Apache/Apache2-AP01/Busy Workers,因此,在清单 23 的呈现器中定义的过滤器使用该指标来呈现。生成的 JPG 如图 26 所示:

图 26. 呈现的 Apache 繁忙 worker 线程
呈现的 Apache 繁忙 worker 线程

不同的客户机可能需要不同的呈现图。例如,一个客户机可能需要一个更小的图像。能够动态调整大小的图像通常比较合适。另一个客户机可能也需要一个更小的图像,但不想要标题(在其自己的 UI 中提供标题)。MediaServlet 允许在内容请求期间实现其他选项。这些选项被附加到内容请求的 URL 之后,并以 REST 格式进行处理。基本格式是媒体 servlet 路径(可以进行配置)后接缓存名称,或 /media/Apache2-All-BusyWorkers-Line。每个呈现器能够支持不同的选项。对于我们上面使用的呈现器,以下选项就是一个很好的示例:

  • 默认 URI:/media/Apache2-All-BusyWorkers-Line
  • 缩小到 300 X 300:/media/Apache2-All-BusyWorkers-Line/300/300
  • 缩小到 300 X 300,带有最小的标题和轴名称:/media/Apache2-All-BusyWorkers-Line/300/300/BusyWorkers/Time/#Workers

图 27 展示了两个没有标题的缩小的饼图,使用了 URI Apache2-AP02-WorkerStatus-Pie/150/150/ /:

图 27. 缩小的 Apache Server worker 线程池图像
缩小的 Apache Server worker 线程池图像

呈现器可以生成请求内容的客户机能够显示的几乎任何格式的内容。图像格式可以是 JPG、PNG 或 GIF。也支持其他图像格式,但是对于定位于 Web 浏览器客户机的静态图像,PNG 和 GIF 可能最适合。其他格式选择包括基于文本的标记,比如 HTML。浏览器和富客户机都能够呈现 HTML 片段,HTML 是显示独立数据字段和交叉表格的理想选择。纯文本也非常有用。例如,一个 Web 浏览器客户机可以从呈现器检索表示系统生成的事件消息的文本,并将其插入到文本框和列表框中。其他类型的标记也非常实用。许多呈现适用于浏览器的数据包的富客户机和客户端读入定义图形的 XML 文档,然后这些图形可以在客户端生成,这样可以优化性能。

客户端呈现能够提供额外的优化。如果客户机能够呈现自己的可视化,那么它就可以将缓存更新直接传递到客户机,绕过呈现器,除非需要使用呈现器来添加标记。通过这种方式,客户机可以订阅缓存更新事件,并接受这些事件,更新其自己的可视化。可以通过多种方式将数据传递到客户机。在浏览器客户机中,一种简单的 Ajax 风格的轮询程序能够周期性地检查服务器的更新,并实现一个能够将任何更新插入到数据结构中的处理程序来处理浏览器中的呈现。其他选项稍微复杂,涉及到使用 Comet 模式的实际数据流,要求一个到服务器的连接始终保持打开,而且当服务器写入数据时,这些数据就会被客户机读取(参见 参考资料)。对于富客户机,使用一个消息传递系统是最理想的方法,客户机可以在其中订阅数据更新提要。ActiveMQ 既能够与 Jetty Web 服务器结合使用,又具有其 Comet 功能,因此可以创建一个基于浏览器的 JavaScript JMS 客户机并订阅队列和主题。

客户端上丰富的呈现选择还提供了扁平图像不具有的功能,比如单击元素以向下钻取的功能 — APM 指示板的一种常见需求,向下钻取用于导航或更详细地查看图表中的特定项。一个示例就是 Visifire,它是一种开源绘图工具,与 Silverlight(参见 参考资料)结合使用。清单 24 展示一个 XML 片段,该片段生成一个显示各个数据库服务器的 CPU 使用情况的条形图:

清单 24. 数据库平均 CPU 使用情况的图形呈现器
<vc:Chart xmlns:vc="clr-namespace:Visifire.Charts;assembly=Visifire.Charts" 
    Theme="Theme3">
         <vc:Title Text="Average CPU Utilization on  Database Servers"/>
         <vc:AxisY Prefix="%" Title="Utilization"/>
         <vc:DataSeries Name="Utilization" RenderAs="Column">
                  <vc:DataPoint AxisLabel="DB01" YValue="13"/>
                  <vc:DataPoint AxisLabel="DB02" YValue="57"/>
                  <vc:DataPoint AxisLabel="DB03" YValue="41"/>
                  <vc:DataPoint AxisLabel="DB04" YValue="10"/>
                  <vc:DataPoint AxisLabel="DB05" YValue="30"/>
         </vc:DataSeries>
</vc:Chart>

这段 XML 非常普通,所以很容易为其创建一个呈现器,并且外观非常不错。客户端呈现器也可以为可视化添加动画效果,这对 APM 系统显示没什么价值,但是在一些情况下可能非常有帮助。图 28 展示了在启用了 Silverlight 客户机的浏览器中生成的图形:

图 28. 一个 VisiFire Silverlight 呈现的图表
一个 VisiFire Silveright 呈现的图表

APM 指示板中包含所有标准图标类型。最常见的是折线图、多线图、条形图和饼图。一些图表通常是组合而成的,比如将条形图和折线图组合以显示两种不同类型数据的重叠情况。在另外一些情形中,一个折线图具有两条 y 轴,以便在同一个图表中表示数量差距较大的数据序列。绘制一个百分比值和一个标量值时通常属于这种情况,比如路由器上的 % CPU 利用率与传递的字节数。

在一些场景下,可以创建专门的小部件来以一种自定义方式表示数据,或者以一种直观的方式显示数据。例如,枚举的符号显示一个与监控目标状态一致的特定图标。因为状态通常由有限的值表示,使用图形表示有些大材小用了,所以可以采用一些类似交通灯的显示方式来表示,比如红色表示下降、黄色表示警告、绿色表示良好。另一个流行的小部件是刻度盘(通常表示为一种速度计)。我认为使用刻度盘浪费了屏幕空间,因为它们仅显示一个数据矢量,不包含历史记录。折线图既能显示相同的数据,还能够显示历史趋势。一个例外是,多指针的刻度盘能够显示高/低/当前状态等范围。但是在很大程度上,它们只能提供一种视觉上的吸引力,比如图 29 中的刻度盘,通过 “high/low/current” 显示了在过去一小时内每秒获得的数据库块缓冲区情况:

图 29. 示例刻度盘小部件
示例刻度盘小部件

依我看来,可视化的好处在于信息密度高。屏幕空间是有限的,但我想要查看尽可能多的数据。可以通过多种方式来提高数据密度,但自定义图形表示是一种有趣的方式,它将多种维度的数据组合到一个小的图片之中。图 30 展示了一种不错的示例,该示例来自一个已经退役的数据库监控产品:

图 30. Savant 缓存命中率显示
Savant 缓存命中率显示

图 30 显示了与数据库缓冲命中率相关的一些数据矢量:

  • 水平轴表示在缓存中找到的数据的百分比。
  • 垂直轴表示当前的命中率。
  • X 表示命中率趋势。
  • 灰色的圆圈(在这个图像中几乎看不见)表示标准偏差。灰色圆圈的直径越大,缓存性能的偏差就越大。
  • 黄色的球表示过去一小时内的缓存性能。

提高数据密度的第二种方法是 Sparkline,这是一种要求更低的方法。这个词是由数据可视化专家(参见 参考资料)根据 “嵌入到文字、数字和图像中的小型、高分辨率图形” 得出的。它通常用于显示大量的财务统计信息。尽管缺乏上下文,但它们的用途是显示跨许多指标的相对趋势。Sparkline 呈现器由 org.runtimemonitoring.spring.rendering.SparkLineRenderer 类实现,它为 Java 库实现开源的 Sparklines(参见 参考资料)。图 31 中演示了两个 Sparklines,展示了基于条和线的显示效果:

图 31. 显示 Apache 2 繁忙 worker 线程的 Sparklines
显示 Apache 2 繁忙 worker 线程的 Sparklines

此处和附加的代码中列出的示例都非常简单,但一个 APM 系统显然需要更高级和详细的指示板。而且,大多数用户都不希望从头创建指示板。APM 系统通常拥有某种指示板生成器,允许用户查看或搜索可用指标的存储库,并挑选要嵌入到指示板中的指标以及应该显示的格式。图 32 显示了我使用 APM 系统创建的指示板的一部分:

图 32. 指示板示例
指示板示例

结束语

现在,本系列 已经全部结束了。我提供了一些指导、一些常见的性能监控技术,以及您能够实现的特定开发模式,可以使用这些模式增强或构建您自己的监控系统。收集和分析好的数据可以显著提高应用程序的正常运行时间和性能。我鼓励开发人员参与到监控生产应用程序的过程中来:要确定您编写的软件在负荷状态下运行时实际发生了什么,这是最好的信息来源。作为改进的一部分,这种反馈的价值不可估量。希望您的监控过程充满乐趣!

致谢

非常感谢 Sandeep Malhotra 在 Web 服务收集器方面提供的帮助。


下载

描述名字大小
本文的示例代码j-rtm3.zip316KB

参考资料

学习

获得产品和技术

  • JCraft:访问 JCraft 开源 SSH Java 库的站点。
  • Apache Ant:Ant 是一种基于 Java 和 XML 的构建脚本工具,包含通过 SSH 执行远程 shell 命令的任务。
  • Nagios:Nagios 是一种开源网络监控程序。
  • IKVM:IKVM 是一个编译器,它将 Java 字节码转换为 .NET CLR 字节码。
  • NSClientNC_Net:两个 Windows 服务,通过套接字连接提供 WPM 指标。
  • nsclient4j:NSClient 和 NC_Net 的一种 Java 客户机 API。
  • Apache ActiveMQ:来自 Apache Software Foundation 的一种开源 JMS 实现。
  • WebSphere MQ:IBM® 的一种 JMS 兼容的消息传递服务。
  • OpenNMS:OpenNMS 是 joeSNMP Java API 的所有者。
  • RRDtool:一种循环的时间序列数据存储工具。
  • MRTG:Multi Router Traffic Grapher 工具。
  • JFreeChart:一种用于从原始数据生成图表的工具。
  • Visifire:一种用于在 Silverlight 中呈现图表的工具。
  • Microsoft Silverlight:一种基于标记的富客户机。
  • Sparklines for Java:一个关于使用 Java 代码创建 Sparkline 图形的开源库。
  • CA/Wily Introscope:一个商用 Java 和 Web 应用程序性能管理解决方案。
  • 访问 IBM Tivoli®Monitoring for Transaction Performance 信息中心。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Linux
ArticleID=334508
ArticleTitle=Java 运行时监控,第 3 部分: 监控应用程序生态系统的性能与可用性
publish-date=09022008