1. 程式人生 > >Java 服務端監控方案(四. Java 篇)

Java 服務端監控方案(四. Java 篇)

競爭 get @override ida area json 成本 nds 在線

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 的運行很簡單,啟動相應的服務即可(確保 javaPATH 裏):

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 服務端監控方案(二. Ganglia 篇)
  • Java 服務端監控方案(三. Nagios 篇)

Java 服務端監控方案(四. Java 篇)