简介
当今的许多 Java 应用程序都依赖于一组复杂的分布式依赖关系和移动部件。很多外部因素都可能对应用程序的性能和可用性造成影响。这些影响基本上都无法完全消除或解决,且难以在预生成环境中准确模拟。Stuff happens。但是,您可以创建并维护一个全面的系统来监控应用程序的整个生态系统,从而显著降低这些事件的严重性和持续时间。
本系列文章给出了实现此类系统的一些模式和技巧。模式,以及我将使用的一些术语,都表示泛指。通过结合示例代码和插图,它们将帮助您理解应用程序性能监控的概念。这种理解强调解决方案的必要性,并能帮助您选择商业或开源的解决方案。您可以扩展和定制一个解决方案,或者根据需要将其作为设计解决方案的蓝图。
第 1 部分:
- 探究应用程序性能管理(APM)系统的属性
- 描述系统监控的常见反面模式
- 列举监控 JVM 性能的方法
- 提供有效插装应用程序源代码的方法
第 2 部分将重点介绍插装 Java 类及资源而无需修改原始源代码的方法。第 3 部分将论述监控 JVM 外部资源的方法,包括主机及其操作系统以及数据库和消息传递系统等远程服务。它还将总结并归纳其他的 APM 问题,如数据管理、数据虚拟化、报告和报警。
APM 系统:模式和反面模式
为让大家正确入门,应当强调,虽然此处介绍的多数与 Java 相关的内容看上去与应用程序和代码性能分析的流程类似,但其实并非 如此。性能分析是一个极具价值的生产前流程,它可以确认您的 Java 代码是否可扩展、高效、快速和足够出色。但是,根据 stuff happens 公理,当您在生产中遇到无法说明的问题时,优秀的开发阶段代码性能分析可能无用武之地。
我的意思是,在生产中实现性能分析的一些方面,并从运行中的应用程序收集一些相同的实时数据及其所有外部依赖关系。该数据由一系列遍及目标的定量测量指标组成,它们为整个系统的健康状况提供细粒度和详细的表示。此外,通过保留这些指标的历史库,您可以捕获准确的基线,以帮助您确认环境仍然健康,或查明特定缺陷的根源和规模。
监控反面模式
完全没有监控资源的应用程序微乎其微,但仍然需要考虑这些反面模式,它们经常出现在运行环境中:
- 盲点:某些系统依赖关系未受监控,或者监控数据不可访问。运行中的数据库可以覆盖所有监控范围,但如果受支持的网络无法全面覆盖,则诊断小组在分析数据库性能和应用服务器症状时将无法看到网络中的故障。
- 黑盒:核心应用程序或者它的某个依赖关系对于其内部可能不具有监控透明性。JVM 是一个不折不扣的黑盒。举例来说,诊断小组正在调查 JVM 中的莫名延时问题,并且只拥有支持操作系统的统计数据(如 CPU 利用率和进程需要的内存大小),则他们可能无法诊断垃圾收集或线程同步问题。
-
脱节和断开的监控系统:应用程序可以由大型共享数据中心托管,其中,依赖关系由一系列共享资源组成,比如说数据库、存储区网络(SAN)库、消息传递及中间件服务。组织有时高度孤立,各小组只负责管理自己的监控和 APM 系统(请参阅 孤立监控的缺陷 侧栏)。没有各依赖关系的整合视图,各组件所有者只能管中窥豹,只见一斑。
图 1 对比了孤立和整合的 APM 系统:
图 1. 孤立和整合 APM 系统的对比
- 事后报告和相关性:为尝试解决孤立监控的问题,运营支持小组可以运行定期进程获取各来源的数据,将这些数据整合到一个地方,然后再生成汇总报表。这种方法有时效率低下且不切实际,因为它需要按照指定频率严格执行,而缺乏实时数据也会对诊断小组当场发现问题的能力产生负面影响。此外,事后聚合有时缺乏足够的粒度,从而导致重要模式隐藏在数据中不被发觉。举例来说,某个报告可能显示某特定服务调用昨天平均耗时 200 毫秒,但却隐藏了它在下午 1:00 到 1:45 间平均耗时 3500 毫秒。
- 定期或随需应变的监控:由于某些工具强制占用较高的资源开销,因此不能(或不应)经常使用它们。结果,它们很少收集数据,或者只在检测到问题后才收集数据。因此,APM 系统只能执行最低基线,而无法在问题恶化前提前报警,并且可能会自己加剧势态的严重性。
- 非持久化监控:许多工具都提供了有用的性能和可用性指标实时显示功能,但它们并不支持持久化指标供长期或短期比较和分析的功能。常见的一种情况是,如果缺少历史上下文,则性能指标将毫无价值,因为没有判断指标优劣的基准。举例来说,当前的 CPU 利用率是 45%。如果不知道历史利用率的情况,则不好判断当前 CPU 利用率负荷的轻重程度。但是,如果知道历史的典型值为百分之 x,可接受的用户性能上限是百分之 y,则情况就大有改观了。
- 对生产前模型的依赖:假设所有潜在问题都可在生产部署之前从环境中清除,则完全依赖生产前监控和系统模型的实践经常会导致运行时监控不够全面。这些假设无法解决不可预测事件和依赖性故障,因此,诊断小组在遇到此类事件时将没有工具和数据可用。
整合 APM 的实现并不排除监控和诊断工具,如 DBA 管理工具集、低级网络分析应用程序和数据中心管理解决方案。这些工具仍然是无价的资源,但如果它们依赖于整合视图的专有性,则难以克服孤立效果的影响。
理想 APM 系统的属性
与刚才讨论的反面模式相反,本系列文章介绍的理想 APM 系统拥有以下属性:
- 渗透力:它监控所有应用程序组件和依赖关系。
- 粒度化:它可以监控层次极低的函数。
- 整合性:收集的所有指标将被发送到支持整合视图的同一逻辑 APM 中。
- 恒定:一周 7 天,一天 24 小时不间断监控。
- 高效:性能数据收集不会对监控目标造成不利影响。
- 实时:可以实时显示、报告和警告监控的资源指标。
- 历史:监控的资源指标将持久化存储在一个数据库中,因此可以查看、比较和报告历史数据。
在深入研究此系统的实现细节之前,了解 APM 系统的一些基本概念是有帮助的。
APM 系统概念
所有 APM 系统都能访问性能数据源 并提供数据收集 和跟踪 实用工具。注意,这些是我自己选择的用于描述一般类别的通用术语。它们并非特定于任何 APM 系统,不同 APM 系统可以使用其他术语表示相同的概念。在本文的其余部分中,我所使用的术语定义如下。
性能数据源
性能数据源(PDS)是性能或可用性数据的来源,这些数据对于反映组件的相对健康状况非常有用。例如,Java Management Extensions (JMX) 服务通常可以提供关于 JVM 健康状况的丰富数据。大多数关系数据库通过 SQL 接口发布性能数据。这两种 PDS 都是直接 源的例子,即可以直接提供性能数据。相反,推断 源测定有意和偶然操作,并且产生性能数据。例如,测试消息可以定期发送,并随后从 Java Message Service (JMS) 服务器中取回,这个往返时间将作为该服务性能的推断测量。
推断源(它的实例被称作综合事务)有时极为有用,因为它们可以通过遍历与实际活动相同的路径来有效测定多个组件或分层调用。综合事务还在监控连续性方面发挥着重要作用,当直接源不能胜任时,它们可以确认系统在相对空闲期的健康状况。
收集和收集器
收集是从 PDS 获取性能或可用性数据的流程。对于直接 PDS,收集器 通常实现一些 API 来访问该数据。要从网络路由器读取统计数据,收集器可以使用简单网络管理协议(Simple Network Management Protocol,SNMP)或 Telnet。对于推断 PDS,收集器用于执行和测定底层操作。
跟踪和跟踪程序
跟踪是收集器向核心 APM 系统交付测量数据的流程。许多商业和开源 APM 系统都提供了一些用于此目的的 API。对于本文中的示例,我实现了一个通用的 Java 跟踪程序接口,将在下节详细讨论。
通常,大多数 APM 系统将跟踪程序提交的数据组织到某种分类的层次结构中。图 2 展示了该数据捕获的一般流程:
图 2. 收集、跟踪和 APM 系统
![收集、跟踪和 APM 系统](https://i-blog.csdnimg.cn/blog_migrate/aacee9b2caec6c6d715606085bc44a2a.png)
图 2 还展示了 APM 系统提供的常用服务:
- 实时显示:近乎实时显示选定指标的图表。
- 报告:生成的指标活动报告。这通常包括一系列固定报告和自定义报告,并能导出数据供用户在别处使用。
- 历史库:包含原始或汇总指标的历史数据库,从而能够查看特定时间范围内的图表和报告。
- 报警:将收集指标确定的具体条件通知相关个体或组的功能。典型的报警方法是电子邮件和某种类型的自定义钩子接口,可以允许运营小组将事件传播给事件处理系统。
公共跟踪 API 在 APM 的目标环境中的实现和应用提供了一些一致性。此外,自定义收集器的目的是让开发人员能够专心获取性能数据,而不必担心跟踪的问题。下一节将介绍解决此问题的 APM 跟踪接口。
ITracer:跟踪程序接口
Java 语言可以很好地充当收集器的实现语言,因为:
- 广泛的平台支持。Java 收集器类可以在大多数目标平台上运行,而无需修改。这使监控架构可以在本地灵活地使用 PDS 合并收集器进程,而不需要使用远程收集。
- 出色的性能(但是会随可用资源而变化)。
- 健壮的并发和同步执行支持。
- 支持一组丰富的通信协议。
- 受第三方 API 的广泛支持,比如 JDBC 实现、SNMP 和专用 Java 接口,因而能支持多种收集器库。
- 受活跃开源社区的支持,它提供了额外的工具和接口,使语言能访问或获取大量来源的数据。
但是,有一点需要注意,您的 Java 收集器必须能够与目标 APM 系统提供的跟踪 API 相结合。如果您的 APM 跟踪机制未提供 Java 接口,则它的一些模式将仍然适用。但是,如果目标 PDS 只基于 Java(如 JMX),而应用程序平台并不基于 Java,则需要一个桥接接口(如 IKVM)和一个 Java-to-.NET 编译器(请参阅 参考资料)。
当缺少官方标准时,不同 APM 产品提供的跟踪 API 也全然不同。因此,我通过实现一个通用的跟踪 Java 接口(名称为org.runtimemonitoring.tracing.ITracer
)抽象了此问题。ITracer
接口是针对专用跟踪 API 的一个通用包装器。此技巧将确保源代码库不会因版本或 API 提供程序而有所不同,并且还支持实现包装 API 中不可用的额外功能。本文中的大多数其余示例都实现了 ITracer
接口和它所支持的一般底层概念。
图 3 是 org.runtimemonitoring.tracing.ITracer
接口的 UML 类图:
图 3. ITracer
接口和工厂类
![ITracer 接口和工厂类](https://i-blog.csdnimg.cn/blog_migrate/3add96db6e71a75c3dc89d81f90bb721.png)
跟踪类别和名称
ITracer
的基本前提是向中央 APM 系统提交一个度量和相关的名称。此活动由 trace
方法实现,该方法因提交的度量而有所不同。各 trace 方法都接受一个 String[] name
参数,其中包含复合名称的上下文组件,其结构特定于 APM 系统。复合名称向 APM 系统指示提交的名称空间和实际的指标名称;因此,复合名称中通常至少包括根类别和度量说明。底层 ITracer
实现应该知道如何通过传递的 String[]
构建复合名称。表 1 演示了复合命名约定的两个示例:
表 1. 示例复合名称
名称结构 | 复合名称 |
---|---|
简单斜杠分隔 | Hosts/SalesDatabaseServer/CPU Utilization/CPU3 |
JMX MBean ObjectName | com.myco.datacenter.apm:type=Hosts,service=SalesDatabaseServer,group=CPU Utilization,instance=CPU3 |
清单 1 是使用此 API 跟踪调用的简短示例:
清单 1. 跟踪 API 调用示例
ITracer simpleTracer = TracerFactory.getInstance(sprops); ITracer jmxTracer = TracerFactory.getInstance(jprops); . . simpleTracer.trace(37, "Hosts", "SalesDatabaseServer", "CPU Utilization", "CPU3", "Current Utilization %"); jmxTracer.trace(37, "com.myco.datacenter.apm", "type=Hosts", "service=SalesDatabaseServer", "group=CPU Utilization", "instance=CPU3", "Current Utilization %"); );
跟踪程序度量数据类型
在此接口中,度量数据可以是以下类型:
-
int
-
long
-
java.util.Date
-
String
APM 系统提供商可能支持其他数据类型的收集度量数据。
跟踪程序类型
选定了具体的度量数据类型(如 long
)之后,可以根据 APM 系统支持的类型来选择解释特定值的方式。还需记住,各 APM 实现可以使用不同的术语来表示本质相同的类型,并且 ITracer
使用了一些通用的命名规则。
ITracer
中表示的跟踪程序类型:
- 平均时间间隔:
trace(long value, String[] name)
和trace(int value, String[] name)
方法将发出时间间隔平均值的跟踪(请参阅 时间间隔 侧栏)。这表示每个提交将被转化为当前时间间隔的聚合值。当新时间间隔开始时,聚合值计数器将重置为零。 - 粘附:
traceSticky(value long, String[] name)
和traceSticky(value int, String[] name)
方法发出粘附值跟踪。这表示,与时间间隔平均指标相反,聚合将它们的值保留在时间间隔中。如果现在跟踪值 5,而此后不再执行跟踪直到第二天某个时刻,则该指标将保持为 5,直到提供了新值。 - 增量:增量跟踪将传递一个数值,但提供给 APM 系统(或由 APM 系统解释)的实际值是此度量与前一度量之间的增量。它们有时被称作 rate 类型,用于反映自己的性能优势。请考虑事务管理程序的提交总数度量值。该数字始终在增加,并且其绝对值几乎没有用处。该数字有用的地方是它增加的速率,因此定期收集它的绝对值并跟踪每次读取数据之间的增量可以反映事务提交的速率。增量跟踪比平均时间间隔和粘附方式的跟踪更为常用,但仍有些用例采用了平均时间间隔。增量跟踪必须能够区分只能增加的度量和同时能增减的度量。小于前值的提交度量应被忽略或造成底层增量重置。
- 事件:这种类型是一种简单的非聚合指标,它表示特定事件在时间间隔内发生的次数的增量计算。由于收集器和跟踪程序都不期望知道特定时刻的运行总数,因此基本的
traceIncident(String[] name)
调用没有指定任何值,并且隐式只增加一次事件增量。当需要计算多次增量时,除了在循环中多次调用该方法之外,另一种较好的方法是通过traceIncident(int value, String[] name)
方法根据value
来计算合值。 - 智能:智能跟踪程序是一个参数化的类型,它与跟踪程序中的某种其他类型相映射。度量值和跟踪类型将作为
String
传递,并且可将可用类型作为常量定义在接口中。当收集器不知道正在收集的数据的类型或跟踪程序类型时,这是一个非常方便的方法,但是也可以直接将收集值和配置的类型名称传递给跟踪程序。
TracerFactory
是一个普通的工厂类,用于根据传递的配置属性创建新 ITracer
实例,或者从缓存中引用已创建的 ITracer
。
收集器模式
收集通常有三种可选模式,这影响到应该使用的跟踪程序类型:
- 轮询:按固定频繁调用收集器,它将检索和跟踪 PDS 中的指标或指标集的当前值。例如,可以每分钟调用一次收集器来读取主机的 CPU 利用率,或通过 JXM 接口从事务管理器读取提交事务的总数。轮询模式的前提是对目标指标的定期采样。因此,对于轮询事件,指标的值将提供给 APM 系统,但是,假定中间时期的值不变。因而,轮询收集器通常使用粘附跟踪程序类型:APM 系统在生成报告时将假定所有轮询事件之间的值不变。图 4 演示了此模式:
图 4. 轮询收集模式
- 监听:这种通用数据模式是 Observer 模式的一种形式。收集器将其自身注册为目标 PDS 的事件监听程序,它将在相关的事件发生时接受回调。作为回调结果发出的跟踪值取决于回调有效负荷本身的内容,但收集器至少可以跟踪每个回调的事件。图 5 演示了此模式:
图 5:监听收集模式
- 截取:在此模式中,收集器将自己作为截取程序插入到目标和它的调用程序之间。对于通过该截取程序的各个活动实例,截取程序将生成一个度量并跟踪它。当截取模式是 request/response 时,收集器可以测定请求数量、响应时间、请求或响应的有效负荷。例如,HTTP 代码服务器可以充当收集器,它可以:
- 计算请求数,可以选择根据 HTTP 类型(
GET
和POST
等)或统一资源标识符(URI)来分类。 - 请求的响应时间。
- 测定请求和响应的大小。
图 6. 截取收集模式
- 计算请求数,可以选择根据 HTTP 类型(
现在,我已经介绍了性能数据跟踪 API、它的底层数据类型和数据收集的模式。接下来,我将通过一些用例和示例来演示 API 的应用。
监控 JVM
从 JVM 开始实现性能监控是个明智的选择。首先,我将介绍所有 JVM 共同的性能指标,然后再介绍企业给应用程序中经常使用的一些 JVM 驻留组件。通常,Java 应用程序实例是受底层操作系统支持的进程,因此,JVM 监控的某些方面最好是从主机 OS 的视角来理解,这些内容将在第 3 部分中介绍。
在 Java Platform, Standard Edition 5 (Java SE) 发行之前,能够在运行时有效和可靠收集的内部及标准化 JVM 诊断信息非常有限。现在,java.lang.management
接口提供了一些有用的监控点,该接口是所有兼容 Java SE 5(和更新版本)的 JVM 版本的标准。这些 JVM 的某些实现提供了额外的属性指标,但是它们的访问模式却基本相同。我将重点介绍可以通过 JVM 的 MXBeans 访问的标准模式 — 部署在 VM 内部的 JMX MBeans 公开了一个管理和监控接口(请参阅 参考资料):
-
ClassLoadingMXBean
:监控类加载系统。 -
CompilationMXBean
:监控编译系统。 -
GarbageCollectionMXBean
:监控 JVM 的垃圾收集器。 -
MemoryMXBean
:监控 JVM 的堆和非堆内存空间。 -
MemoryPoolMXBean
:监控 JVM 分配的内存池。 -
RuntimeMXBean
:监控运行时系统。该 MXBean 提供的有用监控指标很少,但它确实提供了 JVM 的输入参数和启动时间及运行时间,这两者在其他派生指标中都是很有用的。 -
ThreadMXBean
:监控线程系统。
JMX 收集器的前提是它将获取一个 MBeanServerConnection
对象,该对象可以读取部署在 JVM 中的 MBeans 的属性,读取目标属性的值,并使用 ITracer
API 跟踪它们。对于这种类型的收集,决定部署收集器的位置非常关键。可行的选择包括本地部署 和远程部署。
在本地部署中,收集器和它的调用调度程序部署在目标 JVM 中。随后,JMX 收集器组件将使用 PlatformMBeanServer
(可以通过 JVM 内部的 MBeanServerConnection
来连接它)访问 MXBeans。在远程部署中,收集器运行在一个单独的进程中,并使用某种形式的 JMX Remoting 来连接目标 JVM。这可能没有本地部署那么高效,但它不需要在目标系统中部署任何额外的组件。JMX Remoting 不在本文的讨论范围之内,但它的实现方法非常简单:部署一个 RMIConnectorServer
或在 JVM 中启用外部连接(请参阅 参考资料)。
示例 JMX 收集器
本文的示例 JMX 收集器(请阅读 下载 一节,获取本文的完整源代码)包含三个单独的方法,可用于获取 MBeanServerConnection
。该收集器可以:
- 通过调用静态
java.lang.management.ManagementFactory.getPlatformMBeanServer()
方法,为本地 JVM 的平台MBeanServer
获取一个MBeanServerConnection
。 - 通过调用静态
javax.management.MBeanServerFactory.findMBeanServer(String agentId)
方法,为部署在本地 JVM 平台中的备用MBeanServer
获取一个MBeanServerConnection
。注意,一个 JVM 中可以存在多个MBeanServer
,并且,Java Platform, Enterprise Edition (Java EE) 服务器等较为复杂的系统几乎始终拥有特定于应用服务器的MBeanServer
,它是独立的且有别于平台MBeanServer
(请参阅 交叉注册 MBeans 侧边栏)。 - 使用
javax.management.remote.JMXServiceURL
通过标准 RMI Remoting 获取一个远程MBeanServerConnection
。
清单 2 是摘录自 JMXCollector collect()
方法的代码段,它显示了 ThreadMXBean
中的收集和线程跟踪活动。点击 此处 查看完整清单:
清单 2. 示例 JMX 收集器的 collect()
方法的部分代码,它使用 ThreadMXBean
. . objectNameCache.put(THREAD_MXBEAN_NAME, new ObjectName(THREAD_MXBEAN_NAME)); . . public void collect() { CompositeData compositeData = null; String type = null; try { log("Starting JMX Collection"); long start = System.currentTimeMillis(); ObjectName on = null; . . // Thread Monitoring on = objectNameCache.get(THREAD_MXBEAN_NAME); tracer.traceDeltaSticky((Long)jmxServer.getAttribute(on,"TotalStartedThreadCount"), hostName, "JMX", on.getKeyProperty("type"), "StartedThreadRate"); tracer.traceSticky((Integer)jmxServer.getAttribute(on, "ThreadCount"), hostName, "JMX", on.getKeyProperty("type"), "CurrentThreadCount"); . . // Done long elapsed = System.currentTimeMillis()-start; tracer.trace(elapsed, hostName, "JMX", "JMX Collector", "Collection", "Last Elapsed Time"); tracer.trace(new Date(), hostName, "JMX", "JMX Collector", "Collection", "Last Collection"); log("Completed JMX Collection in ", elapsed, " ms."); } catch (Exception e) { log("Failed:" + e); tracer.traceIncident(hostName, "JMX", "JMX Collector", "Collection", "Collection Errors"); } }
清单 2 中的代码将跟踪 TotalThreadsStarted
和 CurrentThreadCount
的值。由于它是轮询收集器,因此两个跟踪都使用粘附选项。但是,由于 TotalThreadsStarted
是一个不断增加的数值,因此最吸引人的地方不是绝对值,而是已创建线程的速率。这样,该跟踪程序将使用 DeltaSticky
选项。
图 7 显示了此收集器创建的 APM 指标树:
图 7. JMX 收集器 APM 指标树
![JMX 收集器 APM 指标树](https://i-blog.csdnimg.cn/blog_migrate/1d8e26133b03894b66d72b6a3efc145e.png)
JMX 收集器的一些方面并未显示在清单 2 中(但是可以在 完整源代码 中看到),比如说调度注册,它将每隔 10 分钟为 collect()
方法创建一个定期回调。
在清单 2 中,不同跟踪程序类型和数据类型的实现方法将由数据源决定。例如:
-
TotalLoadedClasses
和UnloadedClassCount
将作为粘附增量被跟踪,因为它们的值始终递增,而且增量在测定类加载活动方面比绝对值更加有用。 -
ThreadCount
变量可增加或减少,因此它将作为粘附类型被跟踪。 -
收集错误
将作为内部事件被跟踪,它将在收集遇到异常时递增。
为了追求效率,由于目标 MXBeans 的 JMX ObjectName
在目标 JVM 的生存期不会更改,因此收集器使用 ManagementFactory
常量名来缓存名称。
对于 MXBeans 的两种类型 — GarbageCollector
和 MemoryPool
— 准确的 ObjectName
无法预先知晓,但是您可以提供一个通用的模式。在这些情况下,在初次执行收集时,您将对 MBeanServerConnection
发起一个查询,并请求与提供模式相匹配的所有 MBeans 的列表。为避免未来在目标 JVM 的生存期执行查询,返回的匹配 MBean ObjectName
将缓存在内存中。
在某些情况下,收集的目标 MBean 属性可能不是纯数值类型。MemoryMXBean
和 MemoryPoolMXBean
就是这种情况。对于这些情况,属性类型是可查询键和值的 CompositeData
对象。对于 java.lang.management
JVM 管理接口,MXBean 标准采用了 JMX Open Types 模型,在该模型中,所有属性都是语言无关的类型,如 java.lang.Boolean
和 java.lang.Integer
。或者,对于javax.management.openmbean.CompositeType
等复杂类型,这些类型可以被分解为相同简单类型的键/值对。简单类型的完整列表枚举在静态 javax.management.openmbean.OpenType.ALLOWED_CLASSNAMES
字段中。该模型支持一个类型独立层,使 JMX 客户机不用依赖于非标准的类,并且还可以支持非 Java 客户机,因为底层类型相对比较简单。有关 JMX Open Types 的更多信息,请参阅 参考资料。
对于目标 MBean 属性是非标准复杂类型的情况,您需要确保定义该类型的类在收集器的类路径中。并且,您必须实现一些自定义代码来呈现检索到的复杂对象中的有用数据。
如果获取了单个连接并为所有收集保留了该连接,则需要通过错误检测和修复来创建一个新连接,以防止该连接出现故障。某些收集 API 提供断开监控程序,可以提示收集器关闭、消除和创建新连接。如果收集器尝试连接到由于维护而停机或由于其他原因而无法访问的 PDS,则收集器应该以合适的频率轮询并重新连接。跟踪连接的运行时间还可用于在检测到关机时减少收集的频率。这可以减少已超负荷运行了一段时间的目标 JVM 的开销。
这些示例中未实现的两个额外技巧可以改进 JMX 收集器的效率,并减少它在目标 JVM 中运行所需的开销。第一个技巧适用于从一个 MBean 中查询多个属性的情况。借助 getAttributes(ObjectName name, String[] attributes)
,您可以在一个调用中请求多个属性,而不必使用 getAttribute(ObjectName name, String attribute)
一次请求一个属性。这种差异在本地收集中可以忽略,但是在远程收集中却可以显著减少资源的使用,因为它可以减少网络调用的数量。第二个技巧是使用监控收集模式代替轮询模式,从而进一步减少 JMX 公开内存池的轮询开销。MemoryPoolMXBean
支持建立一个使用率阀值,超过该阀值时将触发向监控程序发送一个通知,而监控程序将跟踪该值。当内存使用率增加时,使用率阀值可以相应地增加。这种方法是缺陷是,如果使用率阀值没有微小的增量,则一些粒度级的数据可能会丢失,并且阀值下方的内存使用率模式将变为不可见。
最后一个未实现的技巧是测定运行时间和垃圾收集总运行时间的范围,并实现一些简单的算法来计算垃圾收集器处于活动状态的时间在已运行时间中的百分比。这是一个有用的指标,因为一些垃圾收集器(当前)是大多数应用程序必须要面对的问题。由于某些收集(分别执行了一段时间)是期望执行的,因此运行垃圾收集占用的时间可以更加清楚地反映 JVM 的内存健康状况。根据经验(因应用程序而大不相同),占用任何 15 分钟时间段内的 10% 以上则表示存在潜在问题。
收集器的外部配置
为便于演示收集流程,本文介绍的 JMX 收集器经过了适当简化,但它仅限于硬编码的收集方式。理想情况下,收集器将实现数据访问方式,而外部提供的配置将提供内容。这种设计使收集器更具实用性,且易于重用。对于最高级别的重用,外部配置的收集器应该支持这些配置点:
- PDS 连接工厂指令,为收集器提供用于连接到 PDS 的接口,以及在连接时使用的配置。
- 执行收集的频率。
- 尝试重新连接的频率。
- 收集的目标 MBean,或通配符形式的对象名称。
- 对于各目标,跟踪复合名称或者应该跟踪的度量片段,以及应该跟踪的数据类型。
清单 3 演示了 JMX 收集器的外部配置:
清单 3. JMX 收集器的外部配置示例
<?xml version="1.0" encoding="UTF-8"?> <JMXCollector> <attribute name="ConnectionFactoryClassName"> collectors.jmx.RemoteRMIMBeanServerConnectionFactory </attribute> <attribute name="ConnectionFactoryProperties"> jmx.rmi.url=service:jmx:rmi://127.0.0.1/jndi/rmi://127.0.0.1:1090/jmxconnector </attribute> <attribute name="NamePrefix">AppServer3.myco.org,JMX</attribute> <attribute name="PollFrequency">10000</attribute> <attribute name="TargetAttributes"> <TargetAttributes> <TargetAttribute objectName="java.lang:type=Threading" attributeName="ThreadCount" Category="Threading" metricName="ThreadCount" type="SINT"/> <TargetAttribute objectName="java.lang:type=Compilation" attributeName="TotalCompilationTime" Category="Compilation" metricName="TotalCompilationTime" type="SDINT"/> </TargetAttributes> </attribute> </JMXCollector>
注意,TargetAttribute
元素包含一个名为 type
的属性,它表示智能类型跟踪程序的参数化变量。SINT
类型表示粘附 int
,SDINT
类型表示增量粘附 int
。
通过 JMX 监控应用程序资源
目前为止,我已经讨论了通过 JMX 监控惟一标准的 JVM 资源。但是,许多应用程序架构,如 Java EE,可以通过 JMX 公开重要的特定于应用程序的指标(这取决于供应商)。一个典型的例子是 DataSource
利用率。DataSource
是一个用于将连接池化到外部资源(通常为数据库)的服务,这限制了并发连接的数量,以保护资源不受恶意应用程序的占用。监控数据源是整个监控计划中的关键环节。由于 JMX 抽象层,该流程与之前介绍的类似。
下面是来自 JBoss 4.2 应用服务器实例的典型数据源指标:
- 可用连接数:当前池中可用连接的数量。
- 连接数:连接池与数据库建立的实际物理连接的数量。
- 最大使用连接数:池中正在使用的连接的上限标记。
- 正在使用的连接数:当前正在使用的连接数量。
- 已创建的连接数:为该池创建的连接总数。
- 已部署的连接数:为该池部署的连接总数。
现在,收集器将使用批属性检索,并在一个调用中获取所有属性。惟一需要注意的是,您需要查询返回的数据,以接通不同的数据和跟踪程序类型。DataSource
指标在没有活动时也是不会变化的,因此,要使数值变化,您需要生成一些负载。清单 4 显示 DataSource 收集器的collect()
方法:
清单 4. DataSource 收集器
public void collect() { try { log("Starting DataSource Collection"); long start = System.currentTimeMillis(); ObjectName on = objectNameCache.get("DS_OBJ_NAME"); AttributeList attributes = jmxServer.getAttributes(on, new String[]{ "AvailableConnectionCount", "MaxConnectionsInUseCount", "InUseConnectionCount", "ConnectionCount", "ConnectionCreatedCount", "ConnectionDestroyedCount" }); for(Attribute attribute: (List<Attribute>)attributes) { if(attribute.getName().equals("ConnectionCreatedCount") || attribute.getName().equals("ConnectionDestroyedCount")) { tracer.traceDeltaSticky((Integer)attribute.getValue(), hostName, "DataSource", on.getKeyProperty("name"), attribute.getName()); } else { if(attribute.getValue() instanceof Long) { tracer.traceSticky((Long)attribute.getValue(), hostName, "DataSource", on.getKeyProperty("name"), attribute.getName()); } else { tracer.traceSticky((Integer)attribute.getValue(), hostName, "DataSource",on.getKeyProperty("name"), attribute.getName()); } } } // Done long elapsed = System.currentTimeMillis()-start; tracer.trace(elapsed, hostName, "DataSource", "DataSource Collector", "Collection", "Last Elapsed Time"); tracer.trace(new Date(), hostName, "DataSource", "DataSource Collector", "Collection", "Last Collection"); log("Completed DataSource Collection in ", elapsed, " ms."); } catch (Exception e) { log("Failed:" + e); tracer.traceIncident(hostName, "DataSource", "DataSource Collector", "Collection", "Collection Errors"); } }
图 8 显示了 DataSource 收集器的相应指标树:
图 8. DataSource 收集器指标树
![DataSource 收集器指标树](https://i-blog.csdnimg.cn/blog_migrate/fd9cf51d732c9db20642784a350eaafc.png)
监控 JVM 中的组件
本节介绍的技巧可用于监控应用程序组件、服务、类和方法。相关的主要区域如下:
- 调用速率:调用服务或方法的速率。
- 调用响应速率:服务或方法响应的速率。
- 调用错误率:服务或方法生成错误的比率。
- 调用运行时间:调用在每个间隔时间内的平均、最短和最长运行时间。
- 调用并发性:并发调用服务或方法时执行的线程数。
使用 Java SE 5(和更新版本)ThreadMXBean
的一些实现提供的指标,还可以收集以下指标:
- 系统和用户 CPU 时间:调用某方法占用的 CPU 时间。
- 等待数量和总等待时间:调用某方法或服务时,等待线程的实例数量和总占用时间。当线程进入
WAITING
或TIMED_WAITING
等待状态并暂停另一个线程的活动时将发生等待事件。 - 阻塞数量和总阻塞时间:在调用某个方法或服务时,处于
BLOCKED
状态的线程的实例数量和总占用时间。当线程等待监控锁进入或重新进入同步阻塞时会发生阻塞事件。
还可以使用备选工具集和本机接口来确定这些指标和其他指标,但这通常涉及某种级别的开销,从而造成不必要的生产运行时监控。已经说过,指标本身,甚至在收集时,是低级的。它们的作用也许仅限于分析趋势,并且很难与无法通过其他手段确定的因果效应相关联。
所有上述指标都可以通过插装类和方法的流程来收集,以便于收集和跟踪目标 APM 系统的性能数据。可以采用各种技巧来直接插装 Java 类,或者通过它们来间接计算性能指标:
- 源代码插装:最基本的技巧是在源代码阶段添加插装,这样编译和部署后的类就已经在运行时包含了插装。在某些情况下,这种方法具有意义,并且一些特定的实践使它成为可行的流程和投资。
- 截取:通过截取程序(执行测定和跟踪)转移调用,可以实现有效和准确的跟踪,而无需接触目标类、它们的源代码和运行时字节码。这种实践简单可取,因为存在许多 Java EE 框架和其他流行的 Java 框架:
- 通过配置支持抽象。
- 支持类注入和通过接口引用。
- 有某些情况下直接支持截取栈概念。执行流程经过定义了配置的对象栈,其作用是接收调用并执行一些处理,然后继续传递。
- 字节码插装:该流程将字节码注入到应用程序类中。注入的字节码将添加性能数据收集插装,该插装被作为新类的一部分调用。这个流程有时极为有效,因为插装是完全经过编译的字节码,并且代码的执行路径以最细化的方式扩展,同时仍然能够收集数据。它的另一个优点是无需修改初始源代码,并且其对环境的配置更改也可能最少。此外,通用模式和字节码注入技巧允许对源代码不可用的类和库进行插装,许多第三方类属于这种情况。
- 类包装:该流程使用另一个类来包装或替换目标类,前者实现了相同功能,同时也包含了插装。
在本文的第 1 部分中,我只讨论基于源代码的插装;您将在第 2 部分中了解更多关于截取、字节码插装和类包装的信息。(从拓扑学的角度来说,截取、字节码插装和类包装的本质完全相同,但它们实现结果的操作有稍微不同的含义)。
异步插装
异步插装是类插装中的基本问题。上一节讨论了轮询性能数据的概念。如果轮询完成得足够好,则它应该不会对核心应用程序性能或开销造成影响。相反,插装应用程序代码本身会直接修改和影响核心代码的执行。任何插装的目标都必须是无论如何,不产生危害。开销损失必须尽可能接近忽略不计。事实上,消除测量本身中的极细微的损失是不可能的,但是,在获取性能数据之后,保持其余跟踪进程异步是非常重要的。可以采用若干种模式来实现异步跟踪。图 9 演示了异步跟踪的实现方法概览:
图 9. 异步跟踪
![异步跟踪](https://i-blog.csdnimg.cn/blog_migrate/974d05a298b1590a1074aa9f2e3d9938.png)
图 9 演示了一个简单的插装截取程序,它通过捕获调用的起始时间来测量它的运行时间,然后将测量数据(运行时间和指标复合名称)分发给处理队列。然后,线程池读取该队列,获取测量数据并完成跟踪流程。
源代码中的 Java 类插装
本节将讨论如何实现源代码级插装,并将提供一些最佳实践和示例代码。文章还介绍了一些新的跟踪结构,我将在源代码插装的上下文中阐明它们的操作和它们的插装模式。
虽然其他选择已经流行,但源代码插装在某些实例中是无法避免的;在某些情况下,它是惟一的解决方案。借助一些明智的预防措施,它可以实现良好的效果。需要考虑的事项包括:
- 如果插装代码的方案可用,并且无法实现配置更改来垂直影响插装,则使用可配置和灵活的跟踪 API 非常重要。
- 抽象的跟踪 API 类似于事件记录 API(如 log4j),它们的共用属性包括:
- 运行时冗长控制:log4j 记录程序和附加程序的冗长等级可以在系统启动时配置,并随后在运行时修改。同样,跟踪 API 应该能够根据分级名称模式来控制哪些指标名称受跟踪支持。
- 输出端点配置:log4j 通过记录程序发起记录声明,并将它分发给附加程序。经过配置,附加程序可以将记录流发送给各种输出,如文件、套接字和电子邮件。跟踪 API 不需要多样的输出方式,但抽象专有或特定于 APM 系统的库的能力将保护源代码不受外部配置的更改。
- 在某些情况下,通过其他方法来跟踪具体的项目不太可行。通常,我将这种情况称作上下文跟踪。我使用该术语描述的性能数据并不是很重要,但它为主要数据添加上下文。
上下文跟踪
上下文跟踪受具体的应用程序影响极大,但是可以考虑一个经过简化的例子:含有 processPayroll(long clientId)
方法的 payroll-processing 类。当被调用时,该方法计算并存储各客户员工的薪水。您可以通过各种方法插装该方法,但是,执行中的底层模式清楚表明,调用时间的增加与员工的数量不成比例。因此,研究 processPayroll
的运行时间趋势没有上下文可供参考,除非您知道程序每次处理的员工数量。简单来讲,对于特定的时间段,processPayroll
平均耗时 x 毫秒。无法确定这个值反映的性能是好还是坏,因为您不知道它处理的员工数量是 1 还是 150,而两种情况反映的性能差别巨大。清单 5 在代码中显示了这个简化的概念:
清单 5. 上下文跟踪的例子
public void processPayroll(long clientId) { Collection<Employee> employees = null; // Acquire the collection of employees //... //... // Process each employee for(Employee emp: employees) { processEmployee(emp.getEmployeeId(), clientId); } }
此处的主要挑战是,根据大多数插装技巧,processPayroll()
方法中的任何东西都是不可触及的。因此,虽然能够插装processPayroll
甚至 processEmployee
,但是却无法跟踪员工的数量,从而不能为方法的性能数据提供上下文。清单 6 显示了一个拙劣的硬编码示例(且有点效率不高),它将捕获上面提到的上下文数据:
清单 6. 上下文跟踪示例
public void processPayrollContextual(long clientId) { Collection<Employee> employees = null; // Acquire the collection of employees employees = popEmployees(); // Process each employee int empCount = 0; String rangeName = null; long start = System.currentTimeMillis(); for(Employee emp: employees) { processEmployee(emp.getEmployeeId(), clientId); empCount++; } rangeName = tracer.lookupRange("Payroll Processing", empCount); long elapsed = System.currentTimeMillis()-start; tracer.trace(elapsed, "Payroll Processing", rangeName, "Elapsed Time (ms)"); tracer.traceIncident("Payroll Processing", rangeName, "Payrolls Processed"); log("Processed Client with " + empCount + " employees."); }
清单 6 中的关键部分是 tracer.lookupRange
调用。Ranges 是指定的收集,它由数值范围限制键控,并且拥有一个表示数值范围名称的String
值。不再跟踪薪水处理的简单无格式运行时间,清单 6 将员工计数划分为范围,有效分隔运行时间并根据基本类似的员工计数将它们分组。图 10 显示了 APM 系统生成的指标树:
图 10:根据范围分组的薪水处理时间
![根据范围分组的薪水处理时间](https://i-blog.csdnimg.cn/blog_migrate/520dbc037191d972568798600b8de69d.png)
图 11 演示了根据员工计数划分的薪水处理运行的时间,它揭示了员工数量和运行时间之间的相互关系:
图 11. 各范围的薪水处理运行时间
![各范围的薪水处理运行时间](https://i-blog.csdnimg.cn/blog_migrate/c404f5c060fbefb9edf33b1b18198ba9.png)
跟踪程序配置属性允许在属性文件中包括 URL,并能在其中定义范围和阀值(我将简单介绍一下阀值)。属性将在跟踪程序的构造时间被读取,并为 tracer.lookupRange
实现提供后台数据。清单 7 显示了 Payroll Processing
范围的示例配置。我选择使用java.util.Properties
的 XML 表示,因为它更能兼容奇怪的字符。
清单 7. 范围配置示例
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>Payroll Process Range</comment> <entry key="L:Payroll Processing">181+ Emps,10:1-10 Emps,50:11-50 Emps, 80:51-80 Emps,120:81-120 Emps,180:121-180 Emps</entry> </properties>
注入外部定义的范围可以使您的应用程序不必频繁更新源代码,这受益于预期的调整和服务水平协议(SLA)在业务方面的变更。当范围和阀值更改生效之后,您只需更新外部文件,而不是应用程序本身。
跟踪阀值和 SLA
外部可配置上下文跟踪的灵活性支持以更加准确和粒度化的方式来定义和测量性能阀值。范围 定义一系列数值区间,可以在其中对测量数据进行分类,而阀值 是对范围的进一步分类,它根据测量数据的确定范围对获取的测量数据进行分类。在分析收集的性能数据时,一个常见的需求是确定和报告执行是 “成功” 还是 “失败”(因为它们未在指定时间发生)。这些数据的总和可以作为关于系统运行健康状况和性能的通用成绩单,或者作为某种形式的 SLA 遵从性评价。
使用薪水处理系统示例,考虑一个内部服务级目标,它将薪水的执行时间(在定义的员工数范围之内)定义为 Ok
、Warn
和 Critical
3 个区间。生成阀值计数的流程从概念上来说非常简单。您只需为跟踪程序提供您认为是各类别各区间的上限运行时间的值,并引导跟踪程序为分类的运行时间发起一个 tracer.traceIncident
,然后 — 为简化报告 — 提供一个总数。表 2 显示了一些经过设计的 SLA 运行时间:
表 2. 薪水处理阀值
员工数 | Ok (ms) | Warn (ms) | Critical (ms) |
---|---|---|---|
1-10 | 280 | 400 | >400 |
11-50 | 850 | 1200 | >1200 |
51-80 | 900 | 1100 | >1100 |
81-120 | 1100 | 1500 | >1500 |
121-180 | 1400 | 2000 | >2000 |
181+ | 2000 | 3000 | >3000 |
ITracer
API 使用与范围中相同的 XML(属性)文件中定义的值实现了阀值报告。范围和阀值定义在两个方面稍有不同。首先,阀值定义的关键值是一个正则表达式。当 ITracer
在跟踪一个数值 时,它会检查阀值正则表达式是否匹配被跟踪指标的复合名称。如果匹配,则阀值会将测量数据分类为 Ok
、Warn
或 Critical
,并为跟踪附加一个额外的 tracer.traceIncident
。其次,由于阀值只定义了两个值(根据定义,Critical
值大于 warn
值),因此配置只由两个数值组成。清单 8 显示了之前介绍的薪水处理 SLA 的阀值配置:
清单 8. 薪水处理的阀值配置
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <!-- Payroll Processing Thresholds --> <entry key="Payroll Processing.*81-120 Emps.*Elapsed Time \(ms\)">1100,1500</entry> <entry key="Payroll Processing.*1-10 Emps.*Elapsed Time \(ms\)">280,400</entry> <entry key="Payroll Processing.*11-50 Emps.*Elapsed Time \(ms\)">850,1200</entry> <entry key="Payroll Processing.*51-80 Emps.*Elapsed Time \(ms\)">900,1100</entry> <entry key="Payroll Processing.*121-180 Emps.*Elapsed Time \(ms\)">1400,2000</entry> <entry key="Payroll Processing.*181\+ Emps.*Elapsed Time \(ms\)">2000,3000</entry> </properties>
图 12 显示添加了阀值指标的薪水处理的指标树:
图 12. 添加了阀值的薪水处理指标
![添加了阀值的薪水处理指标](https://i-blog.csdnimg.cn/blog_migrate/1e71eddb294834c9f8c85025950cbb5d.png)
图 13 演示了哪些收集的数据可以表示在饼形图中:
图 13. 薪水处理的 SLA 汇总(1 到 10 名员工)
![薪水处理的 SLA 汇总](https://i-blog.csdnimg.cn/blog_migrate/709d7d38e0d21f504f2765f854813bf4.png)
确保查找上下文和阀值分类的效率和速度非常重要,因为它们在完成实际工作的线程中执行。在 ITracer
实现中,所有指标名称在第一次被跟踪程序发现时,将存储在(线程安全)为具备和不具备阀值的指标指定的映射中。当特定指标的跟踪事件发生后,阀值确定过程占用的时间是一个 Map
查找时间,它的速度通常足够快。如果阀值条目或指标名称的数量非常大,则一种合理的解决方案是推迟阀值确定,并在异步跟踪线程池中处理它们。
第 1 部分结束语
本系列文章的第 1 部分介绍了一些监控反面模式和一些 APM 系统需要的属性。我总结了一些通用性能数据收集模式,并介绍了 ITracer
接口,我将在本系列文章的其余部分继续使用它。我已经演示了监控 JVM 健康状况的技巧,以及如何通过 JMX 获取通用性能数据。最后,我总结了各种实现高效和防代码更改的源代码级插装方法(用于监控原始性能统计数据和上下文派生统计数据),以及如何使用这些统计数据生成关于应用程序 SLA 的报告。第 2 部分将探究插装 Java 系统而无需修改应用程序源代码的技巧,具体方法是使用截取、类包装和动态字节码插装。