本文为 Java™ 应用程序运行时监控 系列 的第三部分,也是最后一部分,主要介绍在监视应用程序支持和依赖服务的性能和可用性时应使用哪些策略与技巧。所谓支持和依赖服务包括底层主机操作系统、运行数据库以及通信基础设施。文章结尾针对性能数据管理问题以及数据的报告和可视化做了论述。
在本系列(共三篇文章)的 第 1 部分 和 第 2 部分 中,我介绍了监控 Java 应用程序的技巧和模式,在这两部分中我把重点放在了 JVM 和应用程序类上。在这最后一期中,我将介绍从应用程序的依赖项(诸如底层操作系统、网络或者应用程序的后端数据库)收集性能与可用性数据的技巧。在文章结尾我将论述管理收集数据的模式以及报告和可视化数据的方法。
在 第 2 部分 中,我实现了一个用于管理监控服务的基本的基于 Spring 的组件模型。该模型的基本原理及益处有:
下面我将在本文的每个小节中介绍有关基于 Spring 的收集器的更多细节。
Java 应用程序总是运行于底层硬件和支持 JVM 的操作系统之上。一个全面的监控基础设施中最关键的组成就是从硬件和 OS — 通常是通过 OS 收集 — 那里收集性能、健康状况和可用性指标的能力。本节就涵盖了一些通过在 第 1 部分 中介绍的 ITracer
类获取这类数据并一直跟踪到应用程序性能管理系统(application performance management,APM)的技巧。
下面这份摘要列出了典型指标,这些指标跨域操作系统的多个部分相关。虽然数据收集的细节迥异,而且数据的解释也必须在给定的 OS 上下文中进行,但是这些指标在大多数标准主机上基本都是等效的:
CPU 的使用通常被拆分成四个范畴:
不同的 OS 有着不同的性能数据获取机制。我将呈现的收集数据的方式很多,但是在监控领域您可能经常要区别的是基于代理的 和无代理的 监控。也就是说在某些情况下,无需在目标主机上安装其他特定的软件也可以收集数据。但显然监控通常都会涉及到某种代理,因为监控总是需要一个接口,数据要通过它来读取。所以这里真正区别的是是使用通常出现在给定 OS 中的代理 — 诸如 Linux® 服务器上的 SSH — 还是安装其他专用于监控和使收集的数据对外部收集器可用的软件。两种方法都涉及到如下的权衡标准:
归根结底,最佳的解决方案可能就是既实现无代理的监控又实现基于代理的监控:本地代理负责收集大多数指标,而远程监控器负责检查诸如服务器的运行情况和本地代理的状态这样的基本内容。
代理也可以有不同的选项。自治 代理按照自己的计划收集数据,反之,响应 代理按请求递送数据。此外,有些代理只将数据提供给请求程序,而有些则直接或间接地跟踪数据一直到 APM 系统。
接下来我将呈现监控带有 Linux 和 UNIX® OS 的主机的技巧。
回页首
监控代理可以用来实现专门的本机库以从 Linux 和 UNIX OS 收集性能数据。但是 Linux 和大多数 UNIX 变体都有很多内置数据收集工具,这些工具使得数据可以通过称为 /proc 的虚拟文件系统进行访问。该文件看起来像是普通文件系统目录里面的普通文本文件,但其实它们是常驻内存型数据结构,是通过文本文件抽取的。由于这种数据可以很容易地通过大量标准命令行的实用工具或自定义的工具来读取和解析,所以这些文件较易于使用,而且它们的输出既可以是通用的也可以是专用的。而且它们的性能也非常好,因为本质上它们是直接来源于内存的数据。
常见的用于从 /proc 中抽取性能数据的工具是 ps
、sar
、iostat
和 vmstat
(参见 参考资料 查阅有关这些工具的参考文献)。因此,一个有效地监控 Linux 和 UNIX 主机的方法就是执行 shell 命令并解析响应。类似的监控器可以用于很多种 Linux 和 UNIX 实现;虽然它们之间都有着些许差异,但是,使用一种可以完全重用收集过程的方式格式化数据是很简单的。相反,专用的本机库可能要根据每一个 Linux 和 UNIX 发行版而进行重编码或重构(但它们正在读取的 /proc 数据有可能相同)。而编写专用于监控某一特定情况或可以标准化返回数据的格式这样的自定义 shell 命令很容易,并且开销较低。
现在我将展示几种调用 shell 命令和跟踪返回数据的方法。
要在一个 Linux 主机上执行数据收集监控,就一定要调用一个 shell。它可以是 bash
、csh
、ksh
或其他任何允许调用目标脚本或命令并可以检索输出的、受支持的 shell。最通常的选择包括:
java.lang.Process
来访问这种 shell。rsh
:这两个服务都允许调用 shell 和 shell 命令,但由于它们的安全性相对较低,所以很少使用它们。它们在大多数现代发行版上的默认状态为禁用。图 1 展示了本地 shell 与远程 shell 的基本差异:
要用服务器启动一个无人值守的对话需要进行一些设置。首先必须要创建一个由私钥和公钥组成的 SSH 密匙对。然后将公钥置于目标服务器,私钥置于远程监控服务器 —— 数据收集器可以在此获取该私钥。完成上述操作之后,数据收集器便能够提供私钥及其密码短语(passphrase),并能够访问目标服务器上的安全远程 shell 了。使用了密匙对之后,目标帐户的密码就是多余的了,根本不需要它。具体设置步骤如下:
ssh-keygen -t dsa
命令。该命令提示密钥名和密码短语。然后会生成两个叫做 monitoruser_dsa(私钥)和 monitoruser._dsa.pub(公钥)的文件。cat monitoruser_dsa.pub >> authorized_keys
命令将私钥内容追加到 .ssh 目录中名为 authorized_keys 的文件中。清单 1 展示了我刚才所描述的过程:
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.llectors.shell.ShellCollector
的通用收集器类来实现。该类的一个实例将以 UbuntuDesktopRemoteShellCollector
这个名称部署在一个 Spring 上下文中。但要完成整个过程还需要一些其他的依赖项:
urrent.ScheduledThreadPoolExeutor
的一个实例来实现,它既可以提供一个有计划的回调机制,又可以提供一个线程池。这个调度器将以 CollectionScheduler
这个名称部署于 Spring。org.llectors.shell.ssh.JSchRemoteShell
的一个实例来实现。这个类是一个名为 org.llectors.shell.IRemoteShell
的 Shell 接口的实现,它将以UbuntuDesktopRemoteShell
这个名称部署于 Spring。org.llectors.shellmands.CommandSet
的一个实例,它将以 UbuntuDesktopCommandSet
这个名称部署于 Spring 中。命令集从一个 XML 文档载入,该文档表述了: 下面我将专门介绍一些关于专用于生成性能数据的命令以及它们的配置方法的简短示例。一个经典的例子就是 sar
命令。Linux 手册(参见 参考资料)对 sar
的定义是收集、报告或者保存系统活动信息。该命令非常灵活,它有超过 20 个参数,这些参数可以结合起来使用。一个简单的选择就是调用 sar -u 1 3
,它报告了在三个时间间隔内(一个时间间隔为一秒)度量的 CPU 使用。清单 2 展示了它的输出:
sar
命令的输出whitehen@whitehen-desktop:~$ sar -u 1 3 Linux 2.6.22-14-generic (whitehen-desktop) 06/02/200806: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
执行:
sar
whitehen@whitehen-desktop:~$ sar -n SOCK 1 Linux 2.6.22-14-generic (whitehen-desktop) 06/02/200806: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.llectors.SpringCollector
。它采用了一个参数:Spring bean 配置文件所在的目录的名称。SpringCollector
载入了任何带有 .xml 扩展名的文件,并将他们当作 bean 描述符。该目录为位于项目根目录中的 ./spring-collectors 目录(稍后我将在本文中概述此目录中的所有文件。有多个文件可以选择,而且可以将所有的定义捆绑成一个,但要用虚构的函数单独隔开,以保持一定的顺序)。这个例子中的三个 bean 定义代表 shell 收集器、shell 和命令集。清单 4 展示了它们的描述符:
<!-- The Collector --> <bean id="UbuntuDesktopRemoteShellCollector"class="org.llectors.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.llectors.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.llectors.shellmands.CommandSet"><constructor-arg type="java.URL"value="file:///C:/projects//RuntimeMonitoring/commands/l"/> </bean>
清单 4 中的 CommandSet
只有一个 id
(UbuntuDesktopCommandSet
)和另一个 XML 文件的 URL。这是因为命令集太大,我不想因为它们而使 Spring 文件显得很混乱。稍后我将描述 CommandSet
。
清单 3 中的第一个 bean 为 UbuntuDesktopRemoteShellCollector
。它的 bean id
值可以是任意的描述性的值,但是当从另一个 bean 引用该 bean 时需要保持一致。这个例子中的类为 org.llectors.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.llectors.shell.ssh.JSchRemoteShell
的 SSH shell 的实现。该类使用从 JCraft(参见 参考资料)下载的 JSch 来实现。它的其他重要属性有:
userName
:用户用来连接到 Linux 服务器的名称hostName
:连接到的 Linux 服务器的名称(或 IP 地址)。port
:Linux 服务器端口,sshd
在这个端口上监听。knownHostFile
:一个包含主机名称和 SSH 服务器的 SSH 证书的文件,该 SSH 服务器对于运行 SSH 客户机的本地主机是 “已知的”(有趣的是,SSH 中的这个安全机制恰好颠倒了传统的安全结构,使用这种机制,除非主机是 “已知的” 并可以给出匹配的证书,否则客户机不会信任主机,并拒绝连接)。privateKey
:用来验证 SSH 服务器的 SSH 私钥文件。passPhrase
:用来解锁私钥的密码短语。它的外表与密码类似,只是它没有被传送到服务器,而且它只用于本地解密私钥。 清单 5 展示了 CommandSet
的内部细节:
CommandSet
内部细节<CommandSet name="UbuntuDesktop"><Commands><Command><shellCommand>sar -u 1</shellCommand><paragraphSplitter>nn</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>nn</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 树:
如果不喜欢这个树的外观的话,您还有其他的选择。发送到服务器的命令很容易修改,可以修改它们使其传递一连串的 grep
、awk
和 sed
命令,以此来将数据重新格式化为很少需要解析的格式。例如,参见清单 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 展示了命令的输出:
Service Name State Conn Weight Avg StateLoad Transitionsecommerce1_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 元素而非属性,而另一些则偏好使用属性。我没有什么强烈的偏好,但我尽量通过如下两个经验法则保持一致性:
&
或"
)的情况下,我使用元素,因为数据可以包装在CDATA
块中,该块告诉 XML 解析器不要解析这样的内容。 清单 8 展示了用于执行和解析命令的命令集。注意 <preFormatter beanName="FormatCSSServiceResult"/>
标记。它引用了一个包含几行 Groovy 脚本的 Spring bean。Telnet shell 命令的原始输出被传给 Groovy 脚本,然后返回值以一种更友好的格式被传给命令数据提取器。还要注意的是,为了标记为 Status 的列中的值,跟踪器类型被覆盖成了 STRING
类型。眼光尖锐的读者将会注意到这个列不存在,但是 Groovy 脚本的一部分工作就是解决两个列均使用 State 名的问题(您知道这其中的原委),所以 Groovy 脚本将第一个列重命名为 Status。
CommandSet
<CommandSet name="CiscoCSS"><Commands><Command><shellCommand>show service summary</shellCommand><paragraphSplitter>nnnn</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:
<bean id="FormatCSSServiceResult"class="org.vy.GroovyScriptManager"init-method="init" lazy-init="false"><property name="sourceCode"><value><![CDATA[String[] lines = String().split("rrn");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');}}String().replaceFirst("State", "Status").replaceFirst("Service Name", "ServiceName").replace("State", "Transitions");]]></value></property> </bean>
图 4 展示了 CSS 监控的 APM 指标树
最后一个要考虑的有关 Linux/UNIX shell 收集的问题就是 SSH 连接的问题了。所有 Shell 类的基本接口为org.llectors.shell.IShell
。它定义了一个名为 issueOSCommand()
的方法的两种变体,命令在这个方法中被作为参数传递并且返回结果。在我的使用远程 SSH 类 org.llectors.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
等效。返回的数据将拥有多个段落,但是用一个命令集来解析它应该没有问题。要查看这样的例子,请参见本文中名为 l 的示例代码中的命令集定义(参见 下载)。
回页首
Microsoft Windows 与 Linux/UNIX 之间的本质差别必然导致性能数据收集也迥然各异。Windows 本身几乎没有可以提供丰富的性能报告数据的命令行工具。性能数据也无法通过像相对简单的 /proc 文件系统这样的东西来获取。Windows Performance Manager(WPM)— 也称为 SysMon、System Monitor 或者 Performance Monitor — 是从 Windows 主机获取性能度量的标准接口。它的功能很强大而且拥有大量有用的指标。此外,很多基于 Windows 的软件包都通过 WPM 发布了它们自己的指标。Windows 还通过 WPM 提供了制图、报告和警报设施. 图 5 展示了一个 WPM 实例的屏幕截图:
WPM 管理一组性能计数器,即引用了某一具体指标的复合命名的对象。组成复合名称的有:
根据这些名称片段,表达这些对象的命名约定和语法为:
WPM 最大的缺点就是它访问这个数据有些困难,尤其是远程获取,如果是非 Windows 平台的话,那么困难更大。我将呈现很多使用基于ITracer
的收集器捕获 WPM 数据的方法。下面总结了一些主要方法:
在接下来的例子中,我将使用本地 Windows shell 和代理实现呈现一个理论上的概念验证。
作为一个简单的概念验证,我已经创建了一个可使用 C# 执行的名为 的 Windows 命令行。它的用途是提供一些与 Linux/UNIX
sar
命令相同的对性能统计的命令行访问。使用该命令行的语法很简单: Category Counter Raw Instance
.
实例名是强制使用的,除非计数器不是实例计数器,非实例计数器名称可以全是(*
)。计数器名也是强制使用的,但也可以全是(*
)。Raw
是 true
或者 false
。清单 10 展示了使用基于实例的计数器和非基于实例的计数器的例子:
winsar
C:NetProjectsWinSarbinDebug>winsar Memory "% Committed Bytes In Use" false%-Committed-Bytes-In-Use79.57401C:NetProjectsWinSarbinDebug>winsar LogicalDisk "Current Disk Queue Length" false C:Current-Disk-Queue-Length C: 2
因为我的目的是重建某种类似 sar
的东西,数据是以杂乱无章的(非格式化的)表格形式输出的,因此可以使用标准 shell 命令集来解析这些数据输出。对于基于实例的计数器来说,实例位于数据行的第一列,计数器名位于标题行。而对于非基于实例的计数器来说,数据行的第一个字段中无名称。为了能够清晰解析,任何带有空格的名称都填充 “-” 字符。这样做虽然会很难看但却可以使解析比较容易。
为这些统计信息(为进行演示而进行了删减)设置一个收集器是相当简单的。shell 实现是一个org.llectors.shell.local.WindowsShell
,命令集引用了 和参数。shell 还可以被实现为使用 SSH 的远程 shell,要使用 SSH 需要在目标 Windows 主机上安装一个 SSH 服务器。然而,这个解决方案的效率非常低,这主要的原因是由于该实现是基于 .NET 的;在如此短暂的时间内反复启动 Common Language Runtime(CLR)是一种低效的实践。
另一个可能的解决方案是用原生 C++ 重写 winsar
。这个就交给 Windows 编程专家来实现吧。.NET 解决方案的效率是可以提高的,但是程序必须要作为后台进程而一直运行,还要以其他某种方式提供对 WPM 数据的请求,并不会在每一个请求结束后终止。为了达到这个目标,我实现了 winsar
中的第二个选项,在这个选项中由 -service
参数来启动程序、读入一个名为 fig
的配置文件并通过 Java Message Service(JMS)监听请求。除了个别项外,文件的内容非常清楚明了。jmsAssembly
项指一个 .NET 汇编的名称,该 .NET 汇编包含着 JBoss 4.2.2 客户机库(它提供 JMS 功能)的一个 .NET 版本。这个汇编是使用 IKVM(参见 参考资料)创建的。respondTopic
引用了公共主题的名称,响应是在该公共主题中发布的,而不是使用私有主题来发布,因此其他监听器也可以接收到数据。commandSet
是一个命令集的引用,该命令集可供一般接收者用来解析和跟踪数据。清单 11 展示了 fig 文件:
<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.llectors.shell.ShellCollector
的一个名为org.llectors.shell.DelegatingShellCollector
的扩展。不同的是这个 shell 扮演着一个普通收集器的角色,并发出对数据的请求,但是数据是通过 JMS 接收的,通过另一个组件解析和跟踪的。被实现的名为org.llectors.shell.jms.RemoteJMSShell
的 Shell 在行为上类似 shell,但它通过 JMS 分配命令,如图 6 所示:
由于这种策略看起来非常适合全面部署代理,所以相同的基于 JMS 的代理是用 Java 代码实现的,它可以部署于任何支持 JVM 的 OS 上。图 7 展示了一个 JMS 发布/订阅性能数据收集系统:
另外 JMS 代理发挥功能的方式也不同。本例中解释的模式展示的是目标主机上的一个请求监听 代理,因为这些代理在启动后不会执行任何操作,直到它们从中央监控系统接收到一个请求才执行。然而,这些代理可以按照自己的计划自主地 收集数据并将它发布到相同的 JMS 服务器。监听代理的优势有两个:第一,可以配置收集参数并保存在一个中央位置,而不用发送到每一个目标主机。第二(虽然在本例中没有实现),由于中央请求监听器发出请求,所以监听器可以在特定的已知服务器没有响应的情况下触发一个报警条件。图 8 显示了组合服务器的 APM 树:
NSClient 和 NC_Net 使用几乎相同的套接字协议来从 Windows 服务器请求数据。nsclient4j(参见 参考资料)是一个 Java API,它包装了这个协议并使 WPM 指标对于任何带有 JVM 的系统可用。本文的示例代码中有一个 nsclient4j Spring 收集器的例子,它位于 /conf/l 目录中。
winsar
是一个简单的、早期的原型,它有几个缺点,包括:
winsar
没有这个功能,但是诸如 NSClient 和 NC_Net(参见 参考资料)这样的类似代理却可以提供这个功能: 最后,就像我在前面提到的,SpringCollector
应用程序用一个参数引导,它是一个含有配置 XML 文件的目录。这个目录为 /conf/spring-collectors,它位于示例代码包的根目录中。前面的例子中使用的具体文件为:
我对 OS 监控的论述就到此为止了。接下来,我将介绍数据库系统的监控。
回页首
我经常遇到这种情况,就是认为 DBA 以及他们的工具和应用程序的职责仅仅是监控数据库。然而,要为性能和可用性数据实现非孤立的、集中式 APM 储存库,需要在 DBA 的工作中再补充 一些监视整合的 APM 的工作。我将展示一些用名为 JDBCCollector
的 Spring 收集器来收集数据(这些数据在某种情况下很可能没有受到监控,而且是非常有用的指标)的技术。
您应该考虑的数据收集的广义范畴为:
JDBCCollector
非常简单。基本上,它由一个查询和一串映射语句组成,它定义了将查询的结果映射到跟踪名称空间的方式。思考一下清单 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 所示:
JDBCCollector
映射 在图 9 中,我呈现的是对查询的一行响应,但每为每一个返回的行定义一次映射,映射过程就会发生一次。片断的值是 {1}、{2}
,所以跟踪名称空间的片段的部分是 {"public", "sales_order"}
。指标名是字母值,因而保持不变,指标值在第一次映射时被定义为 1
,在第二次映射时被定义为 2
,它们分别代表着 3459
和 16722637
。具体的实现该过程便可以进一步澄清它。
回页首
运行的数据库中的应用程序数据也许包含有用的且有趣的上下文数据。应用程序数据本身并不一定是与性能相关的,但如果抽样这些数据,把它与表示 Java 类的性能、JVM 的健康状况和服务器性能统计信息的历史指标联系起来的话,就可以清楚地了解系统在某一具体的时间段内到底在做些什么。假想您现在正在监控一个极其繁忙的电子商务 Web 站点。您的客户所发出的订单被记录在一个名为 sales_order
的表中,该表还记录了一个惟一的 ID 和发出订单的时间戳。你可以通过抽样在最后 n 分钟内输入的记录数来得出提交订单的速率。
这里是 ITracer
的增量(delta)功能的又一个用武之地,因为我可以设置 JDBCCollector
来从某个特定时间点开始查询行数,并将这个值作为一个增量而跟踪它。结果会得到一个可以描述您的网站有多繁忙的指标(或许还有很多其他指标)。这个指标也会成为一个颇有价值的历史参考。例如,如果知道输入的订单数达到每个周期 50 个时数据库的速度就会减慢的话,那么将会很有用。硬性的、特定的经验数据可以简化容量和增长情况的规划。
接下来我将要实现这个例子。JDBCCollector
使用与前面的例子相同的调度器 bean,它还有一个与我在 第 2 部分 中涉及到的完全相同的 JDBC DataSource。这些数据库收集器是在 /conf/l 文件中定义的。清单 13 展示了第一个收集器:
<bean name="OrderFulfilmentRateLast5s"class="org.llectors.jdbc.JDBCCollector"init-method="springStart"><property name="dataSource" ref="RuntimeDataSource" /><property name="scheduler" ref="CollectionScheduler" /><property name="query"><value><![CDATA[select count(*) from sales_orderwhere 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.llectors.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.llectors.jdbc.RelativeTimeStampProvider"><property name="period" value="-5000" /> </bean><bean name="CurrentTime" class="org.llectors.jdbc.RelativeTimeStampProvider"><property name="period" value="10" /> </bean>
这个例子中的收集器 bean 名为 OrderFulfilmentRateLast5s
,类为 org.llectors.jdbc.JDBCCollector
。标准 scheduler
收集器被注入,它被当作 JDBC DataSource 的一个引用,RuntimeDataSource
。query
定义要执行的 SQL。SQL 查询既可以把字母值用作参数也可以使用绑定变量(就像在这个例子中一样)。这个例子是人为构造的,因为 order_date
的两个值很容易用 SQL 语法来表示,但是通常情况下,只有需要提供某些外部值时才会用到绑定变量。
为了提供绑定外部值的能力,我需要先实现 org.llectors.jdbc.IBindVariableProvider
接口,然后再将该类作为一个 Spring 管理的 bean 来实现。在这个例子中,我使用了org.llectors.jdbc.RelativeTimeStampProvider
的两个实例,还有一个通过被传递的 period
属性来提供当前时间戳偏移量的 bean。这些 bean 是 RelativeTime
,它返回当前时间减去 5 秒得出的值,还有 CurrentTime
,它返回 “现在” 时间加上 10 毫秒的值。这些 bean 的引用通过 binds
属性(一个映射)注入到收集器 bean。映射中的每一个入口值都要与要使用该映射的 SQL 语句中的绑定变量相匹配,这一点至关重要,如若不然就会发生错误或意外的结果。
实际上,我利用了这些绑定变量来获取在大约最后 5 秒钟输入系统的销售订单数。这需要对生产表格执行大量查询,因此要适当调节收集的频率和时间窗口(即,Relative
的时间段)以避免在数据库上实现不恰当的负载。为了帮助正确地调节这些设置,收集器跟踪收集时间一直到 APM 系统,所以运行时间可以用来度量查询开销。更高级的收集器实现会减缓收集的频率,因为监控查询的运行时间增加了。
上面所呈现的映射是通过 queryMaps
属性定义的,该定义使用了一个 org.llectors.jdbc.QueryMap
类型的内部 bean。它有四个简单的属性:
valueColumn
:应该作为跟踪值而绑定的每一行中的列的索引,该索引是基于零的。在这个例子中,我绑定了 count(*)
的值。segments
:跟踪名称空间片断,它被定义为一个单个字母。metricName
:指标名称的跟踪名称空间,同样被定义成一个字母。metricType
:ITracer
指标类型,它被定义为一个 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 树:
JDBCCollector
可以使用相同的进程来从数据库性能视图中获取和跟踪性能数据。在这个使用了 PostgreSQL 的例子中,这些表 — 称为统计视图— 的名称的前缀为 pg_stat
。很多其他的数据库也都拥有类似的视图,并可以使用 JDBC 来访问。在这个例子中,我将使用同一个繁忙的电子商务网站并设置一个 JDBCCollector
来监控最为繁忙的 5 个表上的表格和索引活动。具体的 SQL 展示在清单 14 中:
<bean name="DatabaseIO"class="org.llectors.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 totalFROM pg_stat_all_tableswhere schemaname = 'public'and (COALESCE(idx_tup_fetch,0) + seq_tup_read+ seq_scan + COALESCE(idx_scan, 0)) > 0group by schemaname, relname, idx_tup_fetch,seq_tup_read, seq_scan, idx_scanorder by total descLIMIT 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.llectors.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.llectors.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.llectors.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.llectors.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 所示:
注意到了这一点后,我赶紧触发了两个 SQL 语句来索引表格。随后的结果是两个顺序操作都降到了零,而本来为零的索引操作现在却运行起来。如图 12 所示:
这个索引的创建在整个系统中发生了连锁反应。另一个明显稳定下来的指标是数据库主机上的 User CPU %。如图 13 所示:
我要介绍的有关 JDBC 的最后一个方面是数据库可用性。数据库可用性的最简单的形式是选择标准 JDBCCollector
。如果收集器被配置了一个availabilityNameSpace
值,那么收集器将两个指标跟踪到配置的名称空间:
1
,无法连接到数据库的话,值为 0
当用数据源或连接池来获取连接时,连接时间通常都是很快的。但大多数 JDBC 连接池系统都能在分发连接前执行一个可配置的 SQL 语句,所以测试是合法的。在重负载的情况下,连接的获取会有一个非零的运行时间。此外,可以为用于测试可用性的 JDBCCollector
设置单独的数据源。这个单独的数据源可以配置为不共享连接,所以每一个轮询周期都会启动一个新的连接。图 14 显示了我的 PostgreSQL 运行时数据库的可用性检查 APM 树。请参考 清单 14,查看使用 availabilityNameSpace
属性的例子。
我见过需要多个连锁查询来决定一个特定状态的情况。例如,一个最终的状态需要对 Database A 进行查询,但它所需的参数只有对 Database B 进行查询才可以确定。这种情况可以用两个 JDBCCollector
来解决,但要特殊考虑如下几点:
JDBCCollector
的实例同样实现了 IBindVariableProvider
,这意味着它也能够给另一个收集器提供绑定变量和绑定。我对数据库监控的论述就到此为止了。我必须补充说明一下,这一节重点介绍的是数据库监控,尤其是通过 JDBC 接口的数据库监控。完成一个典型的数据库监控还要监控数据库所在的 OS、单个数据库进程或数据库进程组、还有一些有必要访问数据库服务的相关的网络资源。
本节将描述监控消息传递服务的健康状况和性能的技巧。消息传递服务,如实现 JMS — 同样指面向消息的中间件(message-oriented middleware,MOM)— 的服务,在很多应用程序中都起着至关重要的作用。它们和其他应用程序依赖项一样也需要监控。通常,消息传递服务提供同步的或者说是 “即发即弃(fire-and-forget)” 调用点。监控这些点的难度要大一些,因为从很多角度来看,该服务看起来都运行得很好,服务的调用被频繁地分配,并且运行的时间也很短。仍然保持神秘的是上游瓶颈,消息要经过这里被转发到下一个目标,但消息在这里的传输速度很慢或者根本无法通过。
由于大多数消息传递服务都存在于 JVM 中,抑或是作为一个或多个本机进程而存在于一个主机(或一组主机)上,监控点包括一些与目标服务有关的相同的点。这些目标服务可能包括标准 JVM JMX 属性、支持主机上的监控资源、网络响应,以及诸如内存大小和 CPU 使用这样的服务进程特征。
我将概述消息传递服务监控的四个范畴,它们当中的三个都是专用于 JMS 的,另一个则涉及到专有的 API:
javax.jms.QueueBrowser
这样的标准 JMS 构造来检索有用的指标。综合消息的前提是将测试消息的发送和接收安排到目标消息传递服务并度量消息的发送、接收和整个往返过程的运行时间。为了设计消息的返回并准确地度量从远程位置发送消息的响应时间,最佳的解决方案就是部署远程代理,它的任务是:
然后中央监控器可以分析返回的消息,得出进程中的每一个跳跃点(hop)的运行时间并跟踪到 APM 系统。如图 15 所示:
虽然这个方法涉及到了监控的大多方面,但它还是有一些缺点:
这里介绍的另一个专用于 JMS(但可能在其他消息传递系统中有等效体)的选择是使用临时队列或主题。临时队列可以由标准 JMS API 自动创建,所以不需要介入任何管理。这些临时构造有很多额外的优势,但只有最初的创建者可以看到,其他所有 JMS 参与者都无法看到。
在这个场景中,我将使用 JMSCollector
,它可以在启动时自动创建一个临时队列。当调度器提示时,它会将很多测试消息发送给目标 JMS 服务上的临时队列,然后再收回它们。这就有效地测试了 JMS 服务器的吞吐量,而无需创建具体队列或部署远程代理。如图 16 所示:
这个场景的 Spring 收集器类为 org.llectors.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.llectors.jms.ISyntheticMessageFactory
的接口的一组实现。实现这个接口的对象提供了一组测试消息。收集器会调用每一个配置的工厂,并使用所提供的消息来执行往返测试。用这种方式,我就能够测试我的 JMS 服务器上的吞吐量,因为负载会随着消息大小和消息计数的变化而变化。
每一个 ISyntheticMessageFactory
都有一个可配置的、任意的名称,JMSCollector
把它添加到跟踪名称空间。清单 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><beanclass="org.llectors.jms.SimpleSyntheticMessageFactory"><property name="name" value="MapMessage"/><property name="messageCount" value="10"/></bean><beanclass="org.llectors.jms.ByteArraySyntheticMessageFactory"><constructor-arg type="java.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.llectors.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
消息工厂名称的末尾。
诸如 JBossMQ 和 ActiveMQ 这样的消息传递服务通过 JMX 公开了一个管理接口。我曾在 第 1 部分 中介绍过基于 JMX 的监控。下面我将简短地回顾一下这种监控,然后我将介绍基于 org.llectors.jmx.JMXCollector
类的 Spring 收集器以及使用它来监控 JBossMQ 实例的方法。由于 JMX 是一个恒定的标准,所以可以使用同一过程来监控任何公开 JMX 的指标,而且它的应用范围很广。
JMXCollector
有两个依赖项:
LocalRMIAdaptor
的 bean 为本地 JBossMQ 提供了一个 javax.management.MBeanServerConnection
。在这种情况下,连接是通过发出一个对 JBoss org.jboss.i.RMIAdaptor
的 JNDI 查找来获取的。假定可以提供任何可用的验证凭证,并且 Springorg.springframework.jmx.support
包提供了很多工厂 bean 来获取 MBeanServerConnection
(参见 参考资料)的不同实现,这样的话其他的提供者通常很容易获取。org.llectors.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 配置:
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.llections.InitableHashSet"init-method="init" ><constructor-arg><set><bean class="org.llectors.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.llectors.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.llectors.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 树:
如果缺少足够用于监控 JMS 队列的管理 API 的话,可以使用 javax.jms.QueueBrowser
。队列浏览器的行为与 javax.jms.QueueReceiver
的行为类似,不同的是获取的信息不会从队列中移除,而且在被浏览器检索之后仍然可以发送。队列深度通常都是一种重要的指标。据观察在很多消息传递系统中,消息生产者要多于消息消费者。这种严重的不平衡从代理队列中消息的数量可以看出来。因此,如果无法用其他的方式访问队列深度的话,那么最后一招就是使用队列浏览器了。该技巧有很多的缺点。为了计数队列中的消息数,队列浏览器一定要检索队列中的每一条信息(然后删除它们)。这样做的效率是很低的,而且要耗费比使用管理 API 多得多的收集时间 — 而且可能在 JMS 服务器的资源上开销更高。队列浏览的另一个问题是,对于繁忙的系统来说,计数在浏览完成的时候很可能就已经是错误的了。虽说如此,要是为了监控的话,近似值还是可以接受的;更何况在高负载的系统中,即便是对队列深度在给定瞬间的高精确度的度量在下一个瞬间也会是无用的了。
队列浏览有一个好处:在浏览队列的信息的过程中,可以确定最老的消息的年龄。这是一个很难获取的指标,即便是使用最好的 JMS 管理 API 也很难获取,而且在某些情况下,它可能是一个至关重要的监控点。思考一下用于传输重要信息的 JMS 队列。消息生产者和消息消费者有着明显的区别,而流量的模式是这样的:对队列深度执行一次标准的轮询通常显示一到两条信息。通常情况下,导致这个问题的原因是存在一定的延迟,而轮询的频率却是一分钟,队列中的消息不同于轮询到轮询间的消息。它们相同么?它们可能不是相同的消息,在这种情况下,上述的情况就是很正常的。但也可能是消息生产者和消息消费者同时遭遇失败,因此队列中被观察的这对消息在每个轮询中是相同的消息。在这种场景中,在监控队列深度的同时监控最老的消息的年龄就会使情况变得很清晰了:正常情况下,消息的年龄要少于几秒,但如果生产者和消费者同时失败的话,从 APM 出现明显的数据只会占用两个轮询周期间的时间。
这个功能在 Spring 收集器的 org.llectors.jmx.JMSBrowserCollector
中有所展示。它的另外两个配置属性有 javax.jms.ConnectionFactory
,它与 JMSCollector
类似,还有要浏览的队列的集合。清单 17 展示了该收集器的配置:
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.llectors.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 树:
JMSBrowserCollector
的 APM 树作为一个测试机制,一个载入脚本开始循环,在循环中向每一个队列发送了几百条消息。在每一个循环中,被清除的队列是随机选取的。因此,每一个队列中的消息年龄的上限都会随着时间的推移而随意变化。
有些消息传递系统拥有实现诸如监控这样的管理功能的私有 API。一些消息传递系统使用请求/响应 的模式来用它们自己的消息传递系统提交管理请求。ActiveMQ(参见 参考资料)提供了一个 JMS 通信管理 API 以及一个 JMX 管理 API。实现一个私有的管理 API 需要一个自定义的收集器。在这个小节中,我将呈现 WebSphere® MQ(原来称为 MQ Series)的收集器。该收集器结合使用了两种 API:
其实使用两个 API 是多余的,但是毕竟展示了两种不同的私有 API 的使用方法。
Spring 收集器实现是一个名为 org.llectors.mq.MQCollector
的类。它监控 WebSphere MQ 服务器上的所有队列,收集每一个队列的队列深度以及当前打开的输入/输出处理的数量。清单 18 展示了org.llectors.mq.MQCollector
的配置:
<bean id="MQPCFAgentCollector"class="org.llectors.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>
这里独特的配置属性有:
host
WebSphere MQ 服务器的主机名称的 IP 地址port
:WebSphere MQ 进程在该端口监听连接channel
:要连接到的 WebSphere MQ 通道注意这个例子不包含任何的验证方面。
图 20 展示了为 org.llectors.mq.MQCollector
生成的 APM 树:
MQCollector
的 APM 树我对消息传递服务监控的论述就到此为止。正如我在前面提到的,接下来我将介绍如何用 SNMP 进行监控。
回页首
SNMP 原本是作为应用层协议而创建的,该协议用来在诸如路由器、防火墙和交换机这样的网络设备间交换信息。这也许仍然是它最常用的功能,但是它也可以充当监控性能和可用性的灵活的标准化协议。SNMP 的整个主题以及它作为一个监控工具的实现超出了本文的讨论范围。但是,SNMP 在监控领域已经非常普及,因此难免会有所遗漏。
SNMP 中的核心结构之一就是代理,它负责代理针对某一个设备的 SNMP 请求。SNMP 相对简单且较低级,所以 SNMP 代理可以简单且快捷地嵌套到多种硬件设备和软件服务中。因此 SNMP 就是一个在应用程序生态系统中监控大量服务的标准化协议。另外,SNMP 被广泛用于通过扫描一系列的 IP 地址和端口来执行发现。从监控的角度看,SNMP 的这个应用可以节省一些管理时间,因为它能够自动填充和更新一个监控目标的集中式清单。SNMP 在很多方面都与 JMX 极其相似。虽然两者之间还是有一些明显的差别,但还是可以在两者间找出一些相同的内容,而且 JMX 和 SNMP 间的互操作性被广泛地支持与实现。表 1 总结了一些两者之间的等价内容:
SNMP 结构 | 等效 JMX 结构 |
---|---|
代理 | 代理或者 MBeanServer |
管理程序 | 客户机、MBeanServerConnection 、协议适配器 |
MIB | MBean 、ObjectName 、MBeanInfo |
OID | ObjectName 和 ObjectName + Attribute 名 |
陷阱 | JMX Notification |
GET 、SET | getAttribute 、setAttribute |
BULKGET | getAttributes |
单从监控角度考虑,发布一个 SNMP 查询时,我需要了解的重要因素有:
.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 版本和公共社区。
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 所示:
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.llectors.snmp.SNMPCollector
,该收集器的实现使用到了一个名为 joeSNMP 的开源 API(参见 参考资料)。该收集器拥有如下的重要配置属性:
hostName
:目标主机的主机名或 IP 地址。port
:目标 SNMP 代理正在其上进行监听的端口的数量(默认为 161
)。targets
:一组由 org.llectors.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 应用服务器:
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.llectors.snmp.SNMPCollection"><property name="nameSpace" value="Free Memory"/><property name="oid" value=".1.2.3.4.1.2"/></bean><bean class="org.llectors.snmp.SNMPCollection"><property name="nameSpace" value="Max Memory"/><property name="oid" value=".1.2.3.4.1.3"/></bean><bean class="org.llectors.snmp.SNMPCollection"><property name="nameSpace" value="Thread Pool Queue Size"/><property name="oid" value=".1.2.3.4.1.4"/></bean><bean class="org.llectors.snmp.SNMPCollection"><property name="nameSpace" value="TX Manager, Rollback Count"/><property name="oid" value=".1.2.3.4.1.7"/></bean><bean class="org.llectors.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.llectors.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 做出一个调用而非批量收集,导致运行时效率低下。示例代码中的 l 文件(参见 下载)也包含了一个监控 Linux 服务器的 SNMP 收集器的配置示例。图 21 显示了 APM 系统指标树:
SNMPCollector
的 APM 树 此时,您可能了解到如何创建收集器了。要覆盖整个环境,可能需要集中不同类型的收集器,如果您对此感兴趣的话,您可以参见本文的示例代码,那里包含有其他监控目标的收集器示例。它们都位于 org.llectors
包中。表 2 是这些收集器的一个概述:
收集目标 | 类 |
---|---|
Web 服务:检查安全 Web 服务的响应时间和可用性 | webservice.MutualAuthWSClient |
Web 服务和 URL 检查 | webservice.NoAuthWSClient |
Apache Web Server、性能和可用性 | apacheweb.ApacheModStatusCollector |
Ping | network.PingCollector |
NTop:一个收集详细网络统计信息的实用程序 | network.NTopHostTrafficCollector |
回页首
收集繁忙的可用性和性能数据的系统所面临的一个最复杂的难题就是:如何有效地将收集到的数据持久化到普通指标数据库中。对于要使用的数据库和持久化机制要考虑的问题有:
鉴于这些考虑,您需要:
显然,要以一种最佳的方式来解决上述的每一个问题是很难的,所以必须要考虑折衷的办法。图 22 展示了一个数据收集器在概念上的数据持久化流程:
图 22 表示概念数据收集器(像本文中呈现的 Spring 收集器)的数据流。目的是通过一些层推动单个指标跟踪度量,直到它们被持久化。具体的过程为:
注意当循环缓冲器递增时,下一个缓冲元素的最小的、最大的和平均值将会是零,除了 sticky 指标类型。
相同收集器总计被合并的总计时间段的长度要取决于中央刷新计时器的频率。该时间段越长,写入数据库的数据就越少,代价是数据粒度将会丢失。
RRDTool(Round Robin Database Tool)是一个强大的工具,它在一个单独的可执行文件中提供了大量功能(参见 参考资料)。RRDTool 不会为您收集任何实际数据,但是它通常被与 snmpget
、snmpwalk
和其他命令行工具结合使用来创建一种综合监控解决方案。RRDTool 使您能够创建循环(round-robin)的时间序列数据库文件(循环特性使文件大小保持不变,避免文件不受控制地增长),以及针对多个聚合调度的一些选项。然后,它可以从文件查询并导出数据,生成丰富的图表以实现数据可视化和动态报告。添加一个简单的 Web 服务器来持续刷新包含图表和交叉表格数据的页面,从而得到一个功能强大的监控解决方案。
一些专用数据库(比如 RRDTool)可以用于指标存储,但是通常使用的是关系数据库。在关系数据库中执行指标存储时,需要考虑两个一般选项:标准化数据或保持一种更加扁平的结构。这主要关系到如何存储复合指标名称;根据数据建模最佳实践,应该对所有其他引用数据进行标准化。
完全标准化的数据库结构的好处在于,通过将指标复合名称分解为独立的片段,实际上任何数据库都可以利用索引功能来加速查询。它们存储的冗余数据更少,从而使数据库更小,数据密度更高,性能也更好。其缺点在于复杂性和查询的大小:即使相对简单的复合指标名称或指标名称的模式(例如主机 11 到 93 之间的 % User CPU)都要求 SQL 包含一些连接和大量谓词。这个缺点可以通过使用数据库视图和硬存储常用指标名解码来减轻。每个单独指标的存储需要分解复合名称来定位指标引用数据项(或创建一个新的引用数据项),但是这种性能开销可以通过在持久过程中缓存所有引用数据来减轻。
图 23 展示一种用于在标准化结构中存储复合指标的模型:
在图 23 中,为每个惟一的复合指标分配了一个惟一的 METRIC_ID
,为每个惟一的片段分配了一个 SEGMENT_ID
。然后在一个包含METRIC_ID
、SEGMENT_ID
和片段出现顺序的关联实体中构建复合名称。指标类型存储在引用表 METRIC_TYPE
中,而指标值本身存储在METRIC_INSTANCE
中(包含值、时间戳的开始和结束属性,以及对指标类型和惟一 METRIC_ID
的引用)。
另一方面,扁平模型结构非常简单,如图 24 所示:
在本例中,我将指标名称从复合名称中分离开来,剩余的片段仍然保持其在 segments
列中的原有模式。同样,如果实现的数据库引擎能够使用基于模式的谓词(比如正则表达式)执行适合较宽的文本列的查询,那么这种模型非常易于查询。这种优点应当重视。使用简化的查询结构,能够显著地简化数据分析、可视化和报告工具,而且在紧急的分类会话期间,加快查询的编写速度可能是最重要的方面!
如果需要持久化数据存储,挑选正确的数据库是非常关键的一步。使用关系数据库是一种可行的方法,因为关系数据库能够很好地执行,并且可以从中提取数据并针对您的需要进行格式化。时间序列驱动的数据的生成比较复杂,但是通过正确地分组和聚合 —— 以及使用其他工具,比如 JFreeChart(参见 参考资料)—— 可以生成出色的典型报告和图表。如果想要实现一种更加专门化的数据库(比如 RRDTool),那么请做好心理准备,因为这种数据库的提取和报告需要很长时间。如果数据库不支持 ODBC、JDBC 等标准,那么将不能使用常用的标准化报告工具。
对数据管理的讨论到此为止。本文最后一节将提供一些用于实时可视化数据的技术。
在某些情况下,将实现插装和性能数据收集器,数据将会流入您的 APM 系统中。那么下一个逻辑步骤就是实时地查看数据的可视化表示。我在这里宽泛地使用实时 这个词,其含义是可视化地表示最近收集的数据。
常用的数据可视化术语是指示板。指示板能够可视化地呈现您所考虑的数据模式或活动的所有方面,它们惟一的限制就是数据的质量和数量。本质上,指示板告诉您生态系统中正在发生什么。指示板在 APM 系统中的一个强大之处是,它能够通过一种统一的显示形式表示各种各样的数据(即从不同来源收集的数据)。例如,一种显示形式可以在指示板上同时显示数据库中 CPU 的最近使用情况和当前趋势、应用服务器之间的网络活动,以及当前登录到您的应用程序的用户数量。在本节中,我将提供一些不同的数据可视化样式,以及我在本文前面提供的 Spring 收集器的一个可视化层实现示例。
Spring 收集器可视化层的前提条件包括:
ITracer
历史跟踪,但它的大小是固定的并且遵循先进先出的规则。一个缓存可以配置为保存 50 条历史记录,这意味着一旦缓存被填满,它将保存最近的 50 条历史记录。cacheConfiguration
进行配置,使用一个缓存进程封装 ITracer
实例。配置还将收集器与定义的缓存实例相关联。收集的跟踪记录像平常一样进行处理,但是添加了与收集器相关联的缓存实例。我们看一下前面的示例,如果缓存的历史记录大小为 50,并且收集器每隔 30 秒收集一次,那么当缓存被填满时,它将保存收集器在最近 25 分钟内收集的历史跟踪记录。org.dering.IRenderer
接口的类。它们的工作是从缓存获取一系列数据并呈现这些数据的某种可视化形式。从呈现器定期检索可视化媒体将生成最新的表示,或者与缓存数据一样新的表示。图 25 列出了这一过程:
本例中的缓存实现是 org.llectors.cache.TraceCacheStore
。可以将其他对象注册为缓存事件监听器,这样,在其他事件中,呈现器可以监听表示新值被添加到缓存的新缓存项 事件。通过这种方式,呈现器能够实际地缓存它们生成的内容,并在新数据可用时使缓存无效。来自呈现器的内容通过一个称为 org.dering.MediaServlet
的 servlet 传递到客户机指示板。该 servlet 解析它接收到的请求,定位呈现器,并请求内容(所有内容都以一个字节数组的形式呈现和传递)和内容的 MIME 类型。字节数组和 MIME 类型然后流动到客户机,以供客户机解释。处理来自基于 URL 的服务的图形内容是最理想的,因为它们可以被 Web 浏览器、富客户机及它们之间的所有内容使用。当呈现器从媒体服务器接收到一个内容请求时,内容将从缓存传递,除非该缓存被一个缓存事件标记为 dirty。通过这种方式,呈现器不需要对每个请求重新生成它们的内容。
以字节数组格式生成、缓存和传递可视化的媒体非常有用,因为这种格式占用的空间最小,并且大多数客户机都可以在提供了 MIME 类型的情况下重新建立内容。由于这种实现在内存中缓存生成的内容,所以我使用了一种压缩方案。总体内存消耗随着缓存内容的增多而显著增加;同样,如果内容附带提供了压缩算法标记,那么大多数客户机都能够解压缩。例如,时下流行的大多数浏览器都支持 gzip
解压缩。然而,合理的压缩级别不一定要非常高(对于较大的图像来说,30-40% 的压缩率就不错了),因此,呈现实现可以缓存到磁盘,或者如果磁盘访问开销更高,动态地重新生成内容可能会需要更少的资源。
在这里,使用一个具体示例将有助于理解。我设置了两个 Apache Web Server 收集器来监控大量繁忙的 worker 线程。每个收集器都分配有一个缓存,而且我设置了少量的呈现器,用以提供图表来显示每个服务器上繁忙的 worker 线程。在本例中,呈现器生成一个 PNG 文件来显示两个服务器的时间序列线图及其序列。一个服务器的收集器和缓存设置如清单 22 所示:
<!-- The Apache Collector --> <bean id="Apache2-AP02"class="org.llectors.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="WebAP02/server-status?auto" /><property name="name" value="Apache2-AP02" /><property name="cacheConfiguration"><beanclass="org.llectors.cache.CacheConfiguration"><property name="cacheStores" ref="Apache2-AP02-Cache"/></bean></property> </bean><!-- The Apache Collector Cache --> <bean id="Apache2-AP02-Cache"class="org.llectors.cache.TraceCacheStore"><constructor-arg value="50"/> </bean>
注意收集器中的 cacheConfiguration
属性,以及它如何引用称为 Apache2-AP02-Cache
的缓存对象。
我还设置了一个呈现器,它是 org.dering.GroovyRenderer
的一个实例。这个呈现器将所有呈现任务委派给文件系统的一个底层 Groovy 脚本。这是最理想的方法,因为我能够在运行时对其进行调整,从而调整生成的图的细节。这个呈现器的一般属性包括:
groovyRenderer
:一个对 org.vy.GroovyScriptManager
的引用,它被配置为从一个目录载入 Groovy 脚本。这也是我用于将从 Telnet 会话返回的数据传递到我的 Cisco CSS 的类。dataCaches
:一组缓存,呈现器从中请求数据并呈现出来。呈现器也注册为当缓存添加新项时从缓存接收事件。当它这样做时,它将其内容标记为 dirty,并对下一个请求重新生成内容。renderingProperties
:传递给呈现器的默认属性,指定生成的图形的具体细节,比如图像的默认大小。您将会在下面看到,这些属性可以被客户机请求覆盖。metricLocatorFilters
:一个收集器缓存,包含收集器生成的每个指标的缓存跟踪。这个属性允许指定一个正则表达式数组来筛选您想要的指标。缓存设置如清单 23 所示:
<bean id="Apache2-All-BusyWorkers-Line"class="org.dering.GroovyRenderer"init-method="init"lazy-init="false"><property name="groovyRenderer"><bean class="org.vy.GroovyScriptManager"><property name="sourceUrl" value="file:///groovy/vy"/></bean></property><property name="dataCaches"><set><ref bean="Apache2-AP01-Cache"/><ref bean="Apache2-AP02-Cache"/></set></property><property name="renderingProperties"><value>xSize=700ySize=300title=Apache Servers Busy WorkersxAxisName=TimeyAxisName=# of Workers Busy</value></property><property name="metricLocatorFilters" value=".*/Busy Workers"/> </bean>
呈现器非常容易实现,但是我发现,我经常需要对它们进行调整,因此这里列出的 Groovy 方法非常适用于对新的图表类型或者新的图形包进行快速原型化。编译 Groovy 代码时,性能非常好,实现出色的内容缓存也不是问题。动态热更新功能和功能强大的 Groovy 语法使动态更新变得非常轻松。随后,当我确定了呈现器要做的工作以及它应该支持哪些选项时,我将它们移植到 Java 代码。
指标名称由 acing.Trace
类生成。这个类的每个实例表示一个 ITracer
读操作,所以它封装了跟踪的值、时间戳和完整的名称空间。指标的名称就是完整的名称空间。在本例中,我所显示的指标是 WebServers/Apache/Apache2-AP01/Busy Workers
,因此,在清单 23 的呈现器中定义的过滤器使用该指标来呈现。生成的 JPG 如图 26 所示:
不同的客户机可能需要不同的呈现图。例如,一个客户机可能需要一个更小的图像。能够动态调整大小的图像通常比较合适。另一个客户机可能也需要一个更小的图像,但不想要标题(在其自己的 UI 中提供标题)。MediaServlet
允许在内容请求期间实现其他选项。这些选项被附加到内容请求的 URL 之后,并以 REST 格式进行处理。基本格式是媒体 servlet 路径(可以进行配置)后接缓存名称,或 /media/Apache2-All-BusyWorkers-Line。每个呈现器能够支持不同的选项。对于我们上面使用的呈现器,以下选项就是一个很好的示例:
图 27 展示了两个没有标题的缩小的饼图,使用了 URI Apache2-AP02-WorkerStatus-Pie/150/150/ /:
呈现器可以生成请求内容的客户机能够显示的几乎任何格式的内容。图像格式可以是 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 使用情况的条形图:
<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 客户机的浏览器中生成的图形:
APM 指示板中包含所有标准图标类型。最常见的是折线图、多线图、条形图和饼图。一些图表通常是组合而成的,比如将条形图和折线图组合以显示两种不同类型数据的重叠情况。在另外一些情形中,一个折线图具有两条 y 轴,以便在同一个图表中表示数量差距较大的数据序列。绘制一个百分比值和一个标量值时通常属于这种情况,比如路由器上的 % CPU 利用率与传递的字节数。
在一些场景下,可以创建专门的小部件来以一种自定义方式表示数据,或者以一种直观的方式显示数据。例如,枚举的符号显示一个与监控目标状态一致的特定图标。因为状态通常由有限的值表示,使用图形表示有些大材小用了,所以可以采用一些类似交通灯的显示方式来表示,比如红色表示下降、黄色表示警告、绿色表示良好。另一个流行的小部件是刻度盘(通常表示为一种速度计)。我认为使用刻度盘浪费了屏幕空间,因为它们仅显示一个数据矢量,不包含历史记录。折线图既能显示相同的数据,还能够显示历史趋势。一个例外是,多指针的刻度盘能够显示高/低/当前状态等范围。但是在很大程度上,它们只能提供一种视觉上的吸引力,比如图 29 中的刻度盘,通过 “high/low/current” 显示了在过去一小时内每秒获得的数据库块缓冲区情况:
依我看来,可视化的好处在于信息密度高。屏幕空间是有限的,但我想要查看尽可能多的数据。可以通过多种方式来提高数据密度,但自定义图形表示是一种有趣的方式,它将多种维度的数据组合到一个小的图片之中。图 30 展示了一种不错的示例,该示例来自一个已经退役的数据库监控产品:
图 30 显示了与数据库缓冲命中率相关的一些数据矢量:
提高数据密度的第二种方法是 Sparkline,这是一种要求更低的方法。这个词是由数据可视化专家(参见 参考资料)根据 “嵌入到文字、数字和图像中的小型、高分辨率图形” 得出的。它通常用于显示大量的财务统计信息。尽管缺乏上下文,但它们的用途是显示跨许多指标的相对趋势。Sparkline 呈现器由 org.dering.SparkLineRenderer
类实现,它为 Java 库实现开源的 Sparklines(参见 参考资料)。图 31 中演示了两个 Sparklines,展示了基于条和线的显示效果:
此处和附加的代码中列出的示例都非常简单,但一个 APM 系统显然需要更高级和详细的指示板。而且,大多数用户都不希望从头创建指示板。APM 系统通常拥有某种指示板生成器,允许用户查看或搜索可用指标的存储库,并挑选要嵌入到指示板中的指标以及应该显示的格式。图 32 显示了我使用 APM 系统创建的指示板的一部分:
现在,本系列 已经全部结束了。我提供了一些指导、一些常见的性能监控技术,以及您能够实现的特定开发模式,可以使用这些模式增强或构建您自己的监控系统。收集和分析好的数据可以显著提高应用程序的正常运行时间和性能。我鼓励开发人员参与到监控生产应用程序的过程中来:要确定您编写的软件在负荷状态下运行时实际发生了什么,这是最好的信息来源。作为改进的一部分,这种反馈的价值不可估量。希望您的监控过程充满乐趣!
非常感谢 Sandeep Malhotra 在 Web 服务收集器方面提供的帮助。
ps
、sar
、iostat
、vmstat
命令和更多信息。joeSNMP
Java API 的所有者。本文发布于:2024-02-01 03:23:31,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170672901233515.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |