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 接口來定義動態的 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 應用,那更可以考慮使用它了。

系列文章導航


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM