zoukankan      html  css  js  c++  java
  • 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 应用,那更可以考虑使用它了。

    系列文章导航

  • 相关阅读:
    redhat linux tftp
    mysql
    mysql操作!
    【Android】第一个JNI测试程序
    【android】【google map api v2】google 地图 api v2
    【Android】【转】内存耗用:VSS/RSS/PSS/USS
    【Android】获取Mac地址【2】
    【Android】每个Activity中加入引导界面
    【JNI】javah使用(初步)
    【Android】Eclipse svn插件安装说明
  • 原文地址:https://www.cnblogs.com/hyl8218/p/7927746.html
Copyright © 2011-2022 走看看