Java 服務端監控方案(四. Java 篇)
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
下面是我們內部使用的 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
|
|
我們的應用和 jmxtrans 是運行在同一臺機器上的,所以把 local.only
改成了 true
,僅允許本地連接,同時去掉了認證和 SSL 的支持。如果你們的部署方式不同,請按需求調整。
jmxtrans 的運行很簡單,啟動相應的服務即可(確保 java
在 PATH
裏):
1
2
|
|
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 服務端監控方案(二. Ganglia 篇)
- Java 服務端監控方案(三. Nagios 篇)
Java 服務端監控方案(四. Java 篇)