http://jerrypeng.me/2014/08/08/server-side-java-monitoring-java/
这个漫长的系列文章今天要迎来最后一篇了也是真正与 Java 有关的部分。前面介绍了我们的监控方案的 Ganglia 和 Nagios 及其整合的部分这一次则介绍如何记录 Java 应用内的性能参数并将其暴露给监控系统。
主要介绍的内容有 JMX 以及将监控 JMX 并发送数据到 Ganglia 的 jmxtrans同时还会介绍我实现的一个简单的记录性能参数的方法。
1. JMX
JMX 基本上是 Java 应用监控的标准解决方案JVM 本身的诸多性能指标如内存使用、GC、线程等都有对应的 JMX 参数可供监控。自定义 MBean 也是十分简单的一件事。可以用两种方式来定义 MBean第一种是通过自定义接口和对应的实现类另一种则是实现 javax.management.DynamicMBean 接口来定义动态的 MBean。我们采用的是第二种方式因此略过第一种方式的介绍有兴趣的读者请参考Java Tutorial 里的教程和 Javalobby 上的文章。
下面是我们内部使用的 MetricMBean使用 DynamicMBean 实现
public class MetricsMBean implementsDynamicMBean {private final Mapmetrics;public MetricsMBean(Mapmetrics) {this.metrics new HashMap(metrics);
}
OverridepublicObject getAttribute(String attribute)throwsAttributeNotFoundException,
MBeanException,
ReflectionException {
Metric metricmetrics.get(attribute);if (metric null) {throw new AttributeNotFoundException("Attribute " attribute " not found");
}returnmetric.getValue();
}
Overridepublic voidsetAttribute(Attribute attribute)throwsAttributeNotFoundException,
InvalidAttributeValueException,
MBeanException,
ReflectionException {//我们仅仅需要做监控没有设置属性的需要所以直接抛异常
throw new UnsupportedOperationException("Setting attribute is not supported");
}
OverridepublicAttributeList getAttributes(String[] attributes) {
AttributeList attrList newAttributeList();for(String attr : attributes) {
Metric metricmetrics.get(attr);if (metric ! null)
attrList.add(newAttribute(attr, metric.getValue()));
}returnattrList;
}
OverridepublicAttributeList setAttributes(AttributeList attributes) {//我们仅仅需要做监控没有设置属性的需要所以直接抛异常
throw new UnsupportedOperationException("Setting attribute is not supported");
}
OverridepublicObject invoke(String actionName,
Object[] params,
String[] signature)throwsMBeanException, ReflectionException {//方法调用也是不需要实现的
throw new UnsupportedOperationException("Invoking is not supported");
}
OverridepublicMBeanInfo getMBeanInfo() {
SortedSet names new TreeSet(metrics.keySet());
List attrInfos new ArrayList(names.size());for(String name : names) {
attrInfos.add(newMBeanAttributeInfo(name,"long","Metric " name,true,false,false));
}return newMBeanInfo(getClass().getName(),"Application Metrics",
attrInfos.toArray(newMBeanAttributeInfo[attrInfos.size()]),null,null,null);
}
}
其中 Metric 是我们设计的一个接口用于定义不同的监控指标
public interfaceMetric {longgetValue();
}
最后是一个工具类 Metrics 用于注册和创建 MBean
public classMetrics {private static final Logger log LoggerFactory.getLogger(Metrics.class);private static final Metrics instance newMetrics();private Map metrics new HashMap();public staticMetrics instance() {returninstance;
}privateMetrics() {
}publicMetrics register(String name, Metric metric) {
metrics.put(name, metric);return this;
}public voidcreateMBean() {
MetricsMBean mbean newMetricsMBean(metrics);
MBeanServer serverManagementFactory.getPlatformMBeanServer();try{final String name MetricsMBean.class.getPackage().getName()
":type" MetricsMBean.class.getSimpleName();
log.debug("Registering MBean: {}", name);
server.registerMBean(mbean,newObjectName(name));
}catch(Exception e) {
log.warn("Error registering trafree metrics mbean", e);
}
}
}
在应用启动的时候这样调用以注册指标并创建 MBean
//createMaxValueMetric 和 createCountMetric 可以基于同一份数据来得到//最大值和次数的指标详见下面 AverageMetric 的具体实现。
Metrics.instance()
.register("SearchAvgTime", MetricLoggers.searchTime)
.register("SearchMaxTime", MetricLoggers.searchTime.createMaxValueMetric())
.register("SearchCount", MetricLoggers.searchTime.createCountMetric())
.createMBean();
其中注册时指定的名称也是最后从通过 JMX 看到的属性名。
当然上面只是我们内部的监控框架的做法你需要关注的是如何实现自定义 MBean 而已。
上面提到的 Metric 接口我并没有给出实现。下面介绍我们内部常用的一个实现 AverageMetric (平均值指标)。它可以记录某个性能数值并计算单位时间内的平均值最大值和次数。例如上面的 MetricLoggers 中定义的 searchTime它用来记录我们系统的搜索功能的一分钟平均耗时一分钟最大耗时和一分钟的搜索次数。
public classMetricLoggers {public static final AverageMetric searchTime newAverageMetric();
}
在实际的搜索功能处记录耗时
long startTime System.currentTimeMillis();
doSearch(request);long timeCost System.currentTimeMillis() -startTime;
MetricLoggers.searchTime.log(timeCost);
这样通过 JMX 就可以监控到我们系统过去一分钟内的平均搜索耗时最大搜索耗时以及搜索次数。
下面是 AverageMetric 类的具体实现比较长请慢慢看。基本思路就是使用 AtomicReference 和一个值对象通过非阻塞算法来实现并发。经过测试在并发度不高的情况下性能不错但在线程很多竞争激烈的时候不是很好。再次重申这个实现仅供参考。
public classTimeWindowSupport {final longtimeWindow;
TimeWindowSupport(longtimeWindow) {this.timeWindow timeWindow;
}longcurrentSlot() {return System.currentTimeMillis() /timeWindow;
}
}public class AverageMetric extends TimeWindowSupport implementsMetric {final AtomicReference currentValue new AtomicReference();private volatile Value lastValue null;public AverageMetric(longtimeWindow) {super(timeWindow);
}publicAverageMetric() {super(TimeUnit.MINUTES.toMillis(1));
}publicValue getLastValue() {long slot currentSlot();while(true) {
Value curValuecurrentValue.get();if (curValue ! null curValue.slot) {if(currentValue.compareAndSet(curValue, Value.create(slot))) {
lastValuecurValue;break;
}
}else{break;
}
}returnlastValue;
}public void log(longvalue) {long slot currentSlot();while (true) {
Value curValuecurrentValue.get();if (curValue null) {if (currentValue.compareAndSet(null, Value.create(slot, value)))return;
}else if (slot curValue.slot) {if(currentValue.compareAndSet(curValue, curValue.add(value)))return;
}else{if(currentValue.compareAndSet(curValue, Value.create(slot, value))) {
lastValuecurValue;return;
}
}
}
}/*** 基于同样的数据创建一个计数度量其返回值是过去的单位时间内的log事件发生次数
*
*return返回计数度量*/
publicMetric createCountMetric() {return newMetric() {
Overridepublic longgetValue() {
Value valgetLastValue();if (val ! null)return (long) val.n;else
return 0L;
}
};
}/*** 基于同样的数据创建一个最大值度量其返回值是过去的单位时间内记录的最大数值
*
*return返回最大值度量*/
publicMetric createMaxValueMetric() {return newMetric() {
Overridepublic longgetValue() {
Value valgetLastValue();if (val ! null)returnval.max;else
return 0L;
}
};
}
Overridepublic longgetValue() {
Value lastValuegetLastValue();long lastSlot currentSlot() - 1;if (lastValue ! null 0 lastValue.slot)return lastValue.total /lastValue.n;else
return 0L;
}static classValue {final longslot;final intn;final longtotal;final longmax;
Value(long slot, int n, long total, longmax) {this.slot slot;this.n n;this.total total;this.max max;
}static Value create(long slot, longvalue) {return new Value(slot, 1, value, value);
}static Value create(longslot) {return new Value(slot, 0, 0, 0);
}
Value add(longvalue) {return new Value(this.slot,this.n 1,this.total value,
(value> this.max) ? value : this.max);
}
}
}
2. jmxtrans
有了 JMX我们还缺少最后一环将监控数据发给我们前面辛苦搭建的监控系统。我们的核心系统是 Ganglia所以要将数据发送给它。我们选择的是 jmxtrans 这个解决方案。它本身也是用 Java 实现的使用 JSON 作为配置文件。
2.1 安装
它提供了 debrpm 和标准的 zip 包 很方便安装。按照发行版选择安装即可。
2.2 配置
jmxtrans 的配置文件在 /var/lib/jmxtrans 下使用 JSON 格式。针对要监控的每个应用创建一个 JSON 文件按下面的格式配置即可。下面我附加了注释但实际的配置文件如果有这种注释貌似会报错请注意。
{"servers": [ {"host" : "localhost", //JMX IP
"port" : "19008", //JMX 端口
//别名用于Ganglia对参数来源的识别写成本机IP和Hostname即可
"alias" : "192.168.221.29:fly2save02","queries": [
{"outputWriters": [ {"class" : "com.googlecode.jmxtrans.model.output.GangliaWriter","settings": {"groupName" : "myapp", //Ganglia里的参数组名
"host" : "192.168.1.9", //Ganglia的IP
"port" : 8648, //Ganglia的端口
"slope" : "BOTH","units" : "bytes", //参数单位
"tmax" : 60,"dmax" : 0,"sendMetadata": 30}
} ],"obj" : "java.lang:typeMemory", //要监控的 MBean 的标识
"resultAlias" : "app", //别名使用别名可以避免名称过长
"attr" : [ "HeapMemoryUsage", "NonHeapMemoryUsage" ] //要监控的MBean属性
},//要监控多个 MBean需要写多组 query其中 outputWriters 部分会冗
//余这个比较恶心。
{"outputWriters": [ {"class" : "com.googlecode.jmxtrans.model.output.GangliaWriter","settings": {"groupName" : "myapp","host" : "192.168.1.9","port" : 8648,"slope" : "BOTH","tmax" : 60,"dmax" : 0,"sendMetadata": 30}
} ],"obj" : "com.trafree.metrics:typeMetricsMBean", //我们应用的MBean
"resultAlias" : "app"
//未指定attr意味着要监控所有属性
}
]
} ]
}
更详细的配置请参考官方WIKI。
2.3 运行
首先应用一定要打开 JMX Remote为应用添加如下的 JVM 参数。
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port19008
-Dcom.sun.management.jmxremote.local.onlytrue
-Dcom.sun.management.jmxremote.authenticatefalse
-Dcom.sun.management.jmxremote.sslfalse
我们的应用和 jmxtrans 是运行在同一台机器上的所以把 local.only 改成了 true仅允许本地连接同时去掉了认证和 SSL 的支持。如果你们的部署方式不同请按需求调整。
jmxtrans 的运行很简单启动相应的服务即可(确保 java 在 PATH 里)
chkconfig --add jmxtrans
/etc/init.d/jmxtrans start
3. 总结以及其他解决方案介绍
至此我们的完整监控方案基本成型了。借助 GangliaNagiosJMX 和 jmxtrans我们可以完整地监控从 OS 到应用的方方面面可以很轻松地做告警支持也可以很方便地查看历史趋势。
下面 Show 两张图是我们的核心机票检索引擎的性能参数在 Ganglia 和 Nagios 里的样子
Ganglia 的聚合视图堆叠展示多个实例上的同一指标
从 Nagios 里看到的这些服务的状态若从 OK 变成 WARN/CRITICAL我们会马上收到邮件
终于完成了这个系列的文章欢迎读者留下自己的想法欢迎交流。
3.1 其他方案
在研究这些的时候我也发现了一些其他的解决方案在这里一并提一下感兴趣的可以深入研究下(欢迎交流)
collectd 是 Ganglia 的一个不错的替代品貌似更加轻量一些性能也很不错应该更适合小集群。他也可以和 Nagios 很好地整合。
Metrics 是一个 Java 库提供了用于记录系统指标的各种工具基本上是我们自己实现的 MetricMBean 的最佳替代品功能强大并且支持很多常用组件如 JettyEhcacheLog4j 等并且可以发送数据到 Ganglia。如果早点发现这个我可能就不会自己写上面介绍的那一套方案了。对了它还有 Clojure 绑定如果是 Clojure 应用那更可以考虑使用它了。
系列文章导航
【文章原创作者:防ddos攻击 http://www.558idc.com/shsgf.html 复制请保留原URL】