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 implements DynamicMBean {

    private final Map<String, Metric> metrics;

    public MetricsMBean(Map<String, Metric> metrics) {
this.metrics = new HashMap<>(metrics);
} @Override
public Object getAttribute(String attribute)
throws AttributeNotFoundException,
MBeanException,
ReflectionException {
Metric metric = metrics.get(attribute);
if (metric == null) {
throw new AttributeNotFoundException("Attribute " + attribute + " not found");
}
return metric.getValue();
} @Override
public void setAttribute(Attribute attribute)
throws AttributeNotFoundException,
InvalidAttributeValueException,
MBeanException,
ReflectionException {
// 我们仅仅需要做监控,没有设置属性的需要,所以直接抛异常
throw new UnsupportedOperationException("Setting attribute is not supported");
} @Override
public AttributeList getAttributes(String[] attributes) {
AttributeList attrList = new AttributeList();
for (String attr : attributes) {
Metric metric = metrics.get(attr);
if (metric != null)
attrList.add(new Attribute(attr, metric.getValue()));
}
return attrList;
} @Override
public AttributeList setAttributes(AttributeList attributes) {
// 我们仅仅需要做监控,没有设置属性的需要,所以直接抛异常
throw new UnsupportedOperationException("Setting attribute is not supported");
} @Override
public Object invoke(String actionName,
Object[] params,
String[] signature) throws MBeanException, ReflectionException {
// 方法调用也是不需要实现的
throw new UnsupportedOperationException("Invoking is not supported");
} @Override
public MBeanInfo getMBeanInfo() {
SortedSet<String> names = new TreeSet<>(metrics.keySet());
List<MBeanAttributeInfo> attrInfos = new ArrayList<>(names.size());
for (String name : names) {
attrInfos.add(new MBeanAttributeInfo(name,
"long",
"Metric " + name,
true,
false,
false));
}
return new MBeanInfo(getClass().getName(),
"Application Metrics",
attrInfos.toArray(new MBeanAttributeInfo[attrInfos.size()]),
null,
null,
null);
} }

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

public interface Metric {

    long getValue();
}

最后是一个工具类 Metrics 用于注册和创建 MBean:

public class Metrics {

    private static final Logger log = LoggerFactory.getLogger(Metrics.class);
private static final Metrics instance = new Metrics();
private Map<String, Metric> metrics = new HashMap<>(); public static Metrics instance() {
return instance;
} private Metrics() {
} public Metrics register(String name, Metric metric) {
metrics.put(name, metric);
return this;
} public void createMBean() {
MetricsMBean mbean = new MetricsMBean(metrics);
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
try {
final String name = MetricsMBean.class.getPackage().getName() +
":type=" +
MetricsMBean.class.getSimpleName();
log.debug("Registering MBean: {}", name);
server.registerMBean(mbean, new ObjectName(name));
} catch (Exception e) {
log.warn("Error registering trafree metrics mbean", e);
}
} }

在应用启动的时候这样调用以注册指标并创建 MBean:

// createMaxValueMetric 和 createCountMetric 可以基于同一份数据来得到
// 最大值和次数的指标,详见下面 AverageMetric 的具体实现。
Metrics.instance()
.register("SearchAvgTime", MetricLoggers.searchTime)
.register("SearchMaxTime", MetricLoggers.searchTime.createMaxValueMetric())
.register("SearchCount", MetricLoggers.searchTime.createCountMetric())
.createMBean();

其中注册时指定的名称也是最后从通过 JMX 看到的属性名。

当然上面只是我们内部的监控框架的做法,你需要关注的是如何实现自定义 MBean 而已。

上面提到的 Metric 接口,我并没有给出实现。下面介绍我们内部常用的一个实现 AverageMetric (平均值指标)。它可以记录某个性能数值,并计算单位时间内的平均值,最大值和次数。例如上面的 MetricLoggers 中定义的 searchTime,它用来记录我们系统的搜索功能的一分钟平均耗时,一分钟最大耗时和一分钟的搜索次数。

public class MetricLoggers {
public static final AverageMetric searchTime = new AverageMetric();
}

在实际的搜索功能处记录耗时:

long startTime = System.currentTimeMillis();
doSearch(request);
long timeCost = System.currentTimeMillis() - startTime; MetricLoggers.searchTime.log(timeCost);

这样通过 JMX 就可以监控到我们系统过去一分钟内的平均搜索耗时,最大搜索耗时以及搜索次数。

下面是 AverageMetric 类的具体实现,比较长,请慢慢看。基本思路就是使用 AtomicReference 和一个值对象,通过非阻塞算法来实现并发。经过测试,在并发度不高的情况下性能不错,但在线程很多,竞争激烈的时候不是很好。再次重申,这个实现仅供参考。

public class TimeWindowSupport {
final long timeWindow; TimeWindowSupport(long timeWindow) {
this.timeWindow = timeWindow;
} long currentSlot() {
return System.currentTimeMillis() / timeWindow;
}
} public class AverageMetric extends TimeWindowSupport implements Metric { final AtomicReference<Value> currentValue = new AtomicReference<Value>();
private volatile Value lastValue = null; public AverageMetric(long timeWindow) {
super(timeWindow);
} public AverageMetric() {
super(TimeUnit.MINUTES.toMillis(1));
} public Value getLastValue() {
long slot = currentSlot();
while(true) {
Value curValue = currentValue.get();
if (curValue != null && slot != curValue.slot) {
if (currentValue.compareAndSet(curValue, Value.create(slot))) {
lastValue = curValue;
break;
}
} else {
break;
}
}
return lastValue;
} public void log(long value) {
long slot = currentSlot();
while (true) {
Value curValue = currentValue.get();
if (curValue == null) {
if (currentValue.compareAndSet(null, Value.create(slot, value)))
return;
} else if (slot == curValue.slot) {
if (currentValue.compareAndSet(curValue, curValue.add(value)))
return;
} else {
if (currentValue.compareAndSet(curValue, Value.create(slot, value))) {
lastValue = curValue;
return;
}
}
}
} /**
* 基于同样的数据,创建一个计数度量,其返回值是过去的单位时间内的log事件发生次数
*
* @return 返回计数度量
*/
public Metric createCountMetric() {
return new Metric() {
@Override
public long getValue() {
Value val = getLastValue();
if (val != null)
return (long) val.n;
else
return 0L;
}
};
} /**
* 基于同样的数据,创建一个最大值度量,其返回值是过去的单位时间内记录的最大数值
*
* @return 返回最大值度量
*/
public Metric createMaxValueMetric() {
return new Metric() {
@Override
public long getValue() {
Value val = getLastValue();
if (val != null)
return val.max;
else
return 0L;
}
};
} @Override
public long getValue() {
Value lastValue = getLastValue();
long lastSlot = currentSlot() - 1;
if (lastValue != null && lastValue.n != 0 && lastSlot == lastValue.slot)
return lastValue.total / lastValue.n;
else
return 0L;
}
static class Value {
final long slot;
final int n;
final long total;
final long max;
Value(long slot, int n, long total, long max) {
this.slot = slot;
this.n = n;
this.total = total;
this.max = max;
}
static Value create(long slot, long value) {
return new Value(slot, 1, value, value);
}
static Value create(long slot) {
return new Value(slot, 0, 0, 0);
}
Value add(long value) {
return new Value(this.slot,
this.n + 1,
this.total + value,
(value > this.max) ? value : this.max);
}
}
}

2. jmxtrans

有了 JMX,我们还缺少最后一环:将监控数据发给我们前面辛苦搭建的监控系统。我们的核心系统是 Ganglia,所以要将数据发送给它。我们选择的是 jmxtrans 这个解决方案。它本身也是用 Java 实现的,使用 JSON 作为配置文件。

2.1 安装

它提供了 deb,rpm 和标准的 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:type=Memory", //要监控的 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:type=MetricsMBean", //我们应用的MBean
"resultAlias" : "app"
//未指定attr意味着要监控所有属性
}
]
} ]
}

更详细的配置请参考官方WIKI

2.3 运行

首先应用一定要打开 JMX Remote,为应用添加如下的 JVM 参数。

1
2
3
4
5
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=19008
-Dcom.sun.management.jmxremote.local.only=true
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

我们的应用和 jmxtrans 是运行在同一台机器上的,所以把 local.only 改成了 true,仅允许本地连接,同时去掉了认证和 SSL 的支持。如果你们的部署方式不同,请按需求调整。

jmxtrans 的运行很简单,启动相应的服务即可(确保 java 在 PATH 里):

1
2
chkconfig --add jmxtrans
/etc/init.d/jmxtrans start

3. 总结以及其他解决方案介绍

至此,我们的完整监控方案基本成型了。借助 Ganglia,Nagios,JMX 和 jmxtrans,我们可以完整地监控从 OS 到应用的方方面面,可以很轻松地做告警支持,也可以很方便地查看历史趋势。

下面 Show 两张图,是我们的核心机票检索引擎的性能参数在 Ganglia 和 Nagios 里的样子:

  • Ganglia 的聚合视图,堆叠展示多个实例上的同一指标

  • 从 Nagios 里看到的这些服务的状态,若从 OK 变成 WARN/CRITICAL,我们会马上收到邮件

终于完成了这个系列的文章,欢迎读者留下自己的想法,欢迎交流。

3.1 其他方案

在研究这些的时候,我也发现了一些其他的解决方案,在这里一并提一下,感兴趣的可以深入研究下(欢迎交流):

  • collectd 是 Ganglia 的一个不错的替代品,貌似更加轻量一些,性能也很不错,应该更适合小集群。他也可以和 Nagios 很好地整合。
  • Metrics 是一个 Java 库,提供了用于记录系统指标的各种工具,基本上是我们自己实现的 MetricMBean 的最佳替代品,功能强大,并且支持很多常用组件如 Jetty,Ehcache,Log4j 等,并且可以发送数据到 Ganglia。如果早点发现这个,我可能就不会自己写上面介绍的那一套方案了。对了,它还有 Clojure 绑定,如果是 Clojure 应用,那更可以考虑使用它了。

系列文章导航

Java 服务端监控方案(四. Java 篇)的更多相关文章

  1. RPC学习--C#使用Thrift简介,C#客户端和Java服务端相互交互

    本文主要介绍两部分内容: C#中使用Thrift简介 用Java创建一个服务端,用C#创建一个客户端通过thrift与其交互. 用纯C#实现Client和Server C#服务端,Java客户端 其中 ...

  2. 如何有效快速提高Java服务端开发人员的技术水平?

    我相信很多工作了3-5年的开发人员都会经常问自己几个问题: 1.为什么总是感觉技术没有质的提高? 2.如何能够有效和快速的提高自身的技术水平? 3.如何进入到一个牛逼的大公司,认识牛逼的人? 这篇文章 ...

  3. Java 服务端入门和进阶指南

    作者:谢龙 链接:https://www.zhihu.com/question/29581524/answer/44872235 来源:知乎 著作权归作者所有,转载请联系作者获得授权. 现在互联网上资 ...

  4. 俯瞰 Java 服务端开发

    原文首发于 github ,欢迎 star . Java 服务端开发是一个非常宽广的领域,要概括其全貌,即使是几本书也讲不完,该文将会提到许多的技术及工具,但不会深入去讲解,旨在以一个俯瞰的视角去探寻 ...

  5. Java服务端性能优化

    <Java程序性能优化>说性能优化包含五个层次:设计调优.代码调优.JVM调优.数据库调优.操作系统调优. 常用的几个代码优化方案: 使用单例 对于IO处理.数据库连接.配置文件解析加载等 ...

  6. C#使用Thrift简介,C#客户端和Java服务端相互交互

    C#使用Thrift简介,C#客户端和Java服务端相互交互 本文主要介绍两部分内容: C#中使用Thrift简介 用Java创建一个服务端,用C#创建一个客户端通过thrift与其交互. 用纯C#实 ...

  7. thrift例子:python客户端/java服务端

    java服务端的代码请看上文. 1.说明: 这两篇文章其实解决的问题是,当使用python去访问大数据线上集群的时候,遇到两个问题: 1)python-hadoop和python-hive相关包链接不 ...

  8. IOS IAP APP内支付 Java服务端代码

    IOS IAP APP内支付 Java服务端代码   场景:作为后台需要为app提供服务,在ios中,app内进行支付购买时需要进行二次验证. 基础:可以参考上一篇转载的博文In-App Purcha ...

  9. 那些年,我们见过的 Java 服务端乱象

    导读 查尔斯·狄更斯在<双城记>中写道:“这是一个最好的时代,也是一个最坏的时代.” 移动互联网的快速发展,出现了许多新机遇,很多创业者伺机而动:随着行业竞争加剧,互联网红利逐渐消失,很多 ...

随机推荐

  1. Git 将本地库添加到远程仓库

    git remote add origin ssh://admin@127.0.0.1:29418/Prjs/prj1.git git push -u origin master

  2. CSS3 伸缩布局盒模型

    CSS3引入的布局模式Flexbox布局,主要思想是让容器有能力让其子项目能够改变其宽度,高度,以最佳方式填充可用空间.Flex容器使用Flex项目可以自动放大与收缩,用来填补可用的空闲空间.更重要的 ...

  3. C# 会话,进程,线程,线程安全

    会话->进程->线程 b/s网站中,每个用户的访问为一次会话,会话中包含CPU为用户在内存中开辟空间存储的会话信息, 如Session,进程,会话拥有一个进程,同一进程下可以拥有多个线程. ...

  4. Derive representation formula from Green’s identity

    This article introduces how to derive the representation formula used in BEM from Green's identity. ...

  5. nginx做代理安装docker

    1.环境 服务器IP 服务用途 是否能 联网 192.168.126.128 docker服务器(简称B服务器) 不能 192.168.126.130 nginx服务器(简称A服务器) 能 2.背景 ...

  6. 分解数据表(将一个datatable按数据量分隔成多个table)

    /// <summary> /// 分解数据表 /// </summary> /// <param name="originalTab">需要分 ...

  7. orleans exception序列化

    options.FallbackSerializationProvider = typeof(ILBasedSerializer).GetTypeInfo();

  8. 从小白到区块链工程师:第一阶段:Go语言环境的搭建(1)

    一,Golang语言简介 2009年由谷歌公司推出,由C语言之父Ken Thompson主导研发.Go(又称Golang)是Google开发的一种静态强类型.编译型.并发型,并具有垃圾回收功能的编程语 ...

  9. js基础梳理-如何理解作用域和作用域链?

    本文重点是要梳理执行上下文的生命周期中的建立作用域链,在此之前,先回顾下关于作用域的一些知识. 1.什么是作用域(scope)? 在<JavaScritp高级程序设计>中并没有找到确切的关 ...

  10. SparkException: Could not find CoarseGrainedScheduler or it has been stopped.

    org.apache.spark.SparkException: Could not find CoarseGrainedScheduler or it has been stopped. at or ...