metric driven (5) – draw metrics

掌握metric基本技术生成metric原始数据后,接下来考虑的问题是:如何绘图和绘制哪些基本的图。

实际上现在一些metric方案,已经不需要考虑这个问题了,例如Metricbeat的方案,导出数据到elastic search后,所有的图形可以一次性执行一个命令(./metricbeat setup –dashboards)来绘制完,不仅包含主机层面的图,也包含常见的流行服务(redis/apache/nginx)的图像:

再如使用circonus,每个收集到的数据都可以预览,也可以直接使用“quick graph”功能立即绘制存储。

但是假设使用的metric展示系统不能自动绘制,或者自动绘制的不满足需求,这仍然需要考虑绘制的问题,首先要自问的是,不管是什么应用,我们都需要绘制哪些图?

一 系统层面

(1)CPU

CPU指标可以划分为整机CPU和具体应用(进程)的CPU,当整机CPU过高时,可以通过先定位进程后定位线程的方式来定位问题。同时CPU的指标数值有很多,例如下面的一些指标,所以很多metric系统提供的所有数据的采集,而对于cpu利用率的计算需要自己去计算。

[root@vm001~]# top
top – 23:52:07 up 22:11, 1 user, load average: 0.01, 0.00, 0.00
Tasks: 116 total, 1 running, 115 sleeping, 0 stopped, 0 zombie
Cpu(s): 1.7%us, 0.6%sy, 0.0%ni, 97.6%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st

(2)Load

系统load更能真实反映整个系统的情况,根据统计的时间范围,可以划分为下面示例中的三种:最近1、5、10分钟。一般系统load过高时,CPU不定很高,可能是磁盘存在瓶颈等问题,所以还需要具体问题具体分析。

[root@vm001~]# sar -q

10:40:01 PM runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15
10:50:01 PM 4 332 0.04 0.02 0.01

(3)Disk

Disk主要关注两个方面:1 磁盘的剩余容量  2 磁盘的影响速度,包括以下一些常用指标:

  • “io_time” – time spent doing I/Os (ms). You can treat this metric as a device load percentage (Value of 1 sec time spent matches 100% of load).
  • “weighted_io_time” – measure of both I/O completion time and the backlog that may be accumulating.
  • “pending_operations” – shows queue size of pending I/O operations.

(4)Network

Network常见的指标包括建立的tcp连接数目、每秒传输(input/output)的字节数、传输错误发生次数等。

(5)Memory

memory主要包括以下指标,需要注意的是linux系统中,可用的内存不仅指free, 因为linux内存管理的原则是,一旦使用,尽量占用,直到其他应用需要才释放。

[root@vm001 ~]# free -m
total used free shared buffers cached
Mem: 3925 1648 2276 2 248 312
-/+ buffers/cache: 1087 2837
Swap: 3983 0 3983

其中系统层面,还可以将jvm这层纳入到这层里面,例如使用绘制出jmx观察到的所有的jvm的一些关键信息。

二 应用层面

(1) TPS:了解当前的TPS,并判断是否超过系统最大可承载的TPS.

(2)ResponseTime: 获取response time的数据分布,然后排除较长时间的原因,决策是否合并,假设有需要,做合适优化。

(3)Success Ratio :找出所有失败的case,并逐一排查原因,消灭bug或者不合理的地方。

(4)Total Count:有个总体的认识,知道每种api的调用次数和用量分布。

三 用户层面

(1)谁用的最多?

(2)用的最多的业务是什么?

(3)业务的趋势是什么?

除了上面提到的一些基本图表外,我们还可以绘制更多“有趣”图表做更多的事情:

(1) load balance是否均衡

X为主机名,Y为请求总数,不同颜色表示不同类型的请求。

(2) 是否可以安全的淘汰一个接口或者功能:

淘汰一个api或者功能时,很多现实是预期之外的,例如应用多个版本的存在、运维的原因都可能导致仍然有“用量”,所以最安全的方式是实际统计使用情况来决定淘汰的时机。例如下图是某个api的调用次数。可见用量逐渐趋向于0,从8月21号,可以删除这个接口或者功能了。

(3) 预警“入侵”

可以通过变化率来判断是否有入侵存在,正常的峰值及变化率会稳定在一定的范围,但是如果变化率极高,这可能是入侵,应予以预警。如下图,在5月30号,出现极其高的访问量,实际上入侵的发生导致。

(4)预警硬件故障。

一些硬件的可以,也可以通过metric监控到,例如常见的磁盘问题,磁盘一般在彻底损坏之前,都是先出现‘慢’的特征,所以在彻底坏之前,通过磁盘的disk time来判断趋势和变化,也能在彻底损坏前,更换磁盘,例如下面的图中,15号后磁盘的disk time陡增。

 

除了以上一些用法,还有其他一些,例如对某个场景是否发生和发生频率感兴趣,所以记录metric来统计发生的概率,诸如此类,有了数据后,可以做很多有趣的事情。

 

参考文献:

1 https://collectd.org/wiki/index.php/Plugin:Disk

2 https://collectd.org/wiki/index.php/Plugin:Memory

metric driven (4) – metric code technology

Metirc记录中用到的技术点

1. AOP
例如想记录所有数据操作的耗时,传统的方式是每个调用之前和之后记录下时间然后相减,示例:

方法调用1:

Instant timeStart = Instant.now();
DataService.listAllUserStories();
long timeDurationInMs = Instant.now().toEpochMilli() - timeStart.toEpochMilli();

方法调用2:

Instant timeStart = Instant.now();
DataService.deleteUserStory(userStoryId);
long timeDurationInMs = Instant.now().toEpochMilli() - timeStart.toEpochMilli();

使用AOP后,直接拦截DataService层的所有调用即可:

@Around("(within(com.dashboard.DataService))")
public Object aroundDataService(ProceedingJoinPoint pjp) throws Throwable {
         Instant timeStart = Instant.now();
         try {
              return pjp.proceed();
         } finally {
             long timeDurationInMs = Instant.now().toEpochMilli() - timeStart.toEpochMilli();
             ……
         }
  }

然后假设对于一些调用并不想记录(例如health check),可以自定义一个注解,然后在拦截时,指定注解即可。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface StepAnnotation {
}

@Around("(within(com.dashboard.DataService)) && annotation(stepAnnotation)")
public Object aroundDataService(ProceedingJoinPoint pjp , StepAnnotation stepAnnotation) throws Throwable
……

2 Thread Local
在记录metric时,除了最基本的一些信息(环境信息、消耗时间、发生时间、操作名等)外,很多调用都需要额外添加一些信息到metric里面,例如创建一个站点的api可能需要记录siteid,删除某个dashboard时,记录dashboard的id,诸如此类,需要绑定的信息可能不尽相同,这个时候可是使用thread local来绑定信息。

public class MetricThreadLocal {

private static final ThreadLocal FEAUTRE_METRIC_THREAD_LOCAL = new ThreadLocal();

public static FeatureMetric getFeatureMetric(){
     return FEAUTRE_METRIC_THREAD_LOCAL.get();
 }

public static void setFeatureMetric(FeatureMetric metricsRecord){
     FEAUTRE_METRIC_THREAD_LOCAL.set(metricsRecord);
 }

public static void setCurrentFeatureMetric(String attributeName, Object attributeValue){
    FeatureMetric featureMetric = getFeatureMetric();
    if(featureMetric != null) {
        featureMetric.setValue(attributeName, attributeValue);
    }
 }

public static void cleanFeatureMetirc(){
       FEAUTRE_METRIC_THREAD_LOCAL.remove();
  }

}

使用thread local时,要注意线程切换的问题:例如,假设想要在metric信息中,绑定trackingid.
(1)Thread切换
使用Thread local时,希望每个请求的日志都能绑定到对应的trackingid上,但是往往事与愿违,存在以下两种不可控情况:

(1)现象: 一些日志始终绑定某个trackingid

使用第三方或者其他人提供的包时,其他人采用的是异步线程去实现的,这个时候,第一个请求会触发第一个线程建立起来,而第一个线程的trackingid会和第一个请求一样(创建的线程的threadlocal会继承创建者):

Thread的构造器实现

        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这样导致,只要这个线程一直存在,就一直是和第一个请求一致。

因为callable或runnable的task内容不是自己可以控制的范畴,导致再无机会去修改。

	private static final ExecutorService pool= Executors.newFixedThreadPool(3);

	public static final String checkAsync(String checkItem) {
  
			checkFuture= pool.submit(new Callable(checkItem) {
				public String call() throws Exception {
					......  //第三方库,无法修改,如果是自己库,直接MDC.put("TrackingID", trackingID)既可修改,或者更标准的搞法(slf4j支持):

“In such cases, it is recommended that MDC.getCopyOfContextMap() is invoked on the original (master) thread before submitting a task to the executor. When the task runs, as its first action, it should invoke MDC.setContextMapValues() to associate the stored copy of the original MDC values with the new Executor managed thread.”

				}
			});

代码示例的情况没有太大危险,因为线程一旦创建,就不会消亡,所以最多某个首次请求,查询到的日志特别多,后面的请求对不上号。但是如果某个线程池是有timeout回收的,则有可能导致很多次请求查询到的trackingid日志都特别多。

解决方案,不想固定死某个trackingid,则调用那个api前clean掉mdc里的trackingid,这样创建的线程就不会带有,即既然不属于我一个人,干脆放弃。调用完再找回。但是这样修改后,调用过程的log就都没有trackingid了。所以很难完美解决,要么有很多且对不上号的,要么一个都没有。

(2)现象:某个请求中,tracking中途丢失了或者变成别的了。

这是因为调用了第三方库,而第三库做了一些特殊处理,比如


	public String call(String checkItem) { 
              call(checkItem, null)
        }
	public String call(String checkItem, Map config) { 
                        String trackingID = config.get("TrackingID");
                        if(trackingID == null)
                              trackingID = "";
                        MDC.put("TrackingID", trackingID);  //因为没有显示trackingid来调用,导致后面的这段逻辑把之前设置的trackingid给清空了(="")。
                        ......
        }
        

解决方案: 方案(1)显式传入trackingid。而不是直接调用call(String checkItem); 方案(2)既然使用mdc,为什么不去check下mdc里面实现是不是有值,如果有,也算传入了,而不是直接覆盖掉。

以上问题很容易出现在第三方库的调用上,且如果不看代码,很难预知会出现什么清空或一直绑定某个。不管哪种情况,都要意识到所以使用mdc不是完美的,因为很多第三库的调用对于你而言都是不透明且不可修改的。

3 Filter/Task

记录metric第一件需要做的事情是选择好记录的位置,一般常见的就2种,对于web service常见就是各种filter,而对于内部实现,大多是一个thread的task创建的位置好。

   services.add(new MetricsRequestFilter());
   services.add(new MetricsResponseFilter());
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
try {
        ResourceMethodInvoker resourceMethodInvoker = (ResourceMethodInvoker) 
        requestContext.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker");
        if(null != resourceMethodInvoker) {
             Method method = resourceMethodInvoker.getMethod();
             writeFeatureMetricsStart(method, requestContext);
}
   }catch (Exception e){
       logger.error("filter metrics request failed.", e);
}
}
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
try {
          ResourceMethodInvoker resourceMethodInvoker = (ResourceMethodInvoker) requestContext
            .getProperty("org.jboss.resteasy.core.ResourceMethodInvoker");
          if (null != resourceMethodInvoker) {
              Method method = resourceMethodInvoker.getMethod();
              writeFeatureMetricsEnd(method, requestContext, responseContext);
   }
} catch (Exception e) {
          logger.error("filter metrics response failed.", e);
}
}
public abstract class TaskWithMetric implements Runnable {

public void run() {

  TaskExecuteResult taskExecuteResult = null;
  try {
           taskExecuteResult = execute();
   } catch(Exception ex){
           taskExecuteResult = TaskExecuteResult.fromException(ex);
           throw ex;
   } finally {
           MetricThreadLocal.cleanFeatureMetirc();
     if (featureMetric != null) {
           writeMetrics(waitingDurationForThread, featureMetric, taskExecuteResult);
        }
    }
}

(1)Filter优先级

@Priority(value = 10)
public class MetricsRequestFilter implements javax.ws.rs.container.ContainerRequestFilter


4 codahales

com.datastax.driver.core.Metrics



private final Gauge knownHosts = registry.register("known-hosts", new Gauge() {
@Override
public Integer getValue() {
    return manager.metadata.allHosts().size();
}
});

5 JMX

基本现在主流的Java服务都提供jmx监控的方式, 如果只是想做展示,不想自定义更多的,这直接开启即可:

5.1 开启jmx:

-Dcom.sun.management.jmxremote=true 
-Dcom.sun.management.jmxremote.port=8091 //定义port
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.password.file=/conf/jmxremote.password  //定义了用户名和密码
-Dcom.sun.management.jmxremote.access.file=/conf/jmxremote.access  //定义了权限

例如:

jmxremote.password

admin P@ssword123

jmxremote.access

admin readwrite

然后可以通过第三方组件来读取信息以供展示,例如collectd的GenericJMX plugin来获取信息:

<Plugin "java">
  JVMARG "-Djava.class.path=/opt/collectd/share/collectd/java/collectd-api.jar:/opt/collectd/share/collectd/java/generic-jmx.jar"
  LoadPlugin "org.collectd.java.GenericJMX"
  
  <Plugin "GenericJMX">
     <MBean "Memory">
      ObjectName "java.lang:type=Memory"
      InstancePrefix "Memory"
      <Value>
        Type "memory"
        Table true
        InstancePrefix "HeapMemoryUsage`"
        Attribute "HeapMemoryUsage"
      </Value>
      <Value>
        Type "memory"
        Table true
        InstancePrefix "NonHeapMemoryUsage`"
        Attribute "NonHeapMemoryUsage"
      </Value>
    </MBean>
    <MBean "GarbageCollector">
      ObjectName "java.lang:type=GarbageCollector,*"
      InstancePrefix "GarbageCollector`"
      InstanceFrom "name"
      <Value>
        Type "invocations"
        Table false
        Attribute "CollectionCount"
      </Value>
      <Value>
        Type "total_time_in_ms"
        Table false
        Attribute "CollectionTime"
      </Value>
    </MBean>
    <Connection>
      Host "localhost"
      ServiceURL "service:jmx:rmi:///jndi/rmi://localhost:8091/jmxrmi"  //8091为上文中定义的端口
      User "admin" //admin为上文中定义的用户名
      Password "P@ssword123"  //P@ssword123为上文中定义的密码
      Collect "MemoryPool"
      Collect "Memory"
      Collect "GarbageCollector"
      Collect "OperatingSystem"
      Collect "Threading"
      Collect "Runtime"
      Collect "BufferPool"
      Collect "Compilation"
      Collect "GlobalRequestProcessor"
      Collect "ThreadPool"
      Collect "DataSource"
    </Connection>
  </Plugin>
</Plugin>

5.2 自定义jmx:

定义:

public interface MetricsMBean {
	
	public int getTotalCount();

}


public class Metrics implements MetricsMBean {
  	
	private int totalCountConnections;
 	
	public Metrics(int totalCountConnections) {
 		this.totalCountConnections = totalCountConnections;
	}
	
	@Override
	public int getTotalCount() {
 		return totalCountConnections;
	}
  
}

启动:

	MBeanServer server = ManagementFactory.getPlatformMBeanServer();
	ObjectName metricsName = new ObjectName("Metrics:name=MetricsNameOne");
	server.registerMBean(new Metrics(100), metricsName);  

总结: 以上几种技术要点很多时候,都是混合在一起使用的,例如将jmx和codahales结合在一起,更方便的统计metric, 以Cassandra的jmx metric作为例子:

org.apache.cassandra.metrics.CassandraMetricsRegistry

    public interface JmxHistogramMBean extends MetricMBean
    {
        long getCount();

        long getMin();

        long getMax();

        double getMean();

        double getStdDev();

        double get50thPercentile();

        double get75thPercentile();

        double get95thPercentile();

        double get98thPercentile();

        double get99thPercentile();

        double get999thPercentile();

        long[] values();
    }

    private static class JmxHistogram extends AbstractBean implements JmxHistogramMBean
    {
        private final Histogram metric;

        private JmxHistogram(Histogram metric, ObjectName objectName)
        {
            super(objectName);
            this.metric = metric;
        }

        @Override
        public double get50thPercentile()
        {
            return metric.getSnapshot().getMedian();
        }

        @Override
        public long getCount()
        {
            return metric.getCount();
        }

        @Override
        public long getMin()
        {
            return metric.getSnapshot().getMin();
        }

        @Override
        public long getMax()
        {
            return metric.getSnapshot().getMax();
        }

        @Override
        public double getMean()
        {
            return metric.getSnapshot().getMean();
        }

        @Override
        public double getStdDev()
        {
            return metric.getSnapshot().getStdDev();
        }

        @Override
        public double get75thPercentile()
        {
            return metric.getSnapshot().get75thPercentile();
        }

        @Override
        public double get95thPercentile()
        {
            return metric.getSnapshot().get95thPercentile();
        }

        @Override
        public double get98thPercentile()
        {
            return metric.getSnapshot().get98thPercentile();
        }

        @Override
        public double get99thPercentile()
        {
            return metric.getSnapshot().get99thPercentile();
        }

        @Override
        public double get999thPercentile()
        {
            return metric.getSnapshot().get999thPercentile();
        }

        @Override
        public long[] values()
        {
            return metric.getSnapshot().getValues();
        }
    }

    private abstract static class AbstractBean implements MetricMBean
    {
        private final ObjectName objectName;

        AbstractBean(ObjectName objectName)
        {
            this.objectName = objectName;
        }

        @Override
        public ObjectName objectName()
        {
            return objectName;
        }
    }

再如,现在新兴的spring metric,也是将以上的一些技术进行了一些组合,提供了良好的封装,例如下面的使用方式非常简易。

@SpringBootApplication
@EnablePrometheusMetrics
public class MyApp {
}

@RestController
@Timed
class PersonController {
    Map<Integer, Person> people = new Map<Integer, Person>();

    public PersonController(MeterRegistry registry) {
        // constructs a gauge to monitor the size of the population
        registry.mapSize("population", people);
    }

    @GetMapping("/api/people")
    public List<Person> listPeople() {
        return people;
    }

    @GetMapping("/api/person/")
    public Person findPerson(@PathVariable Integer id) {
        return people.get(id);
    }
}

参考文献:
1. https://docs.spring.io/spring-metrics/docs/current/public/prometheus

metric driven (3) – basic terms and concepts

在学习metric之前,需要对metric领域涉及的基本概念有个初步的整体认识:

一 统计学指标:
以下面一段Cassandra的metric做示例:
Cassandra Metrics
Timer:
name=requests, count=171396, min=0.480973, max=4.228569, mean=1.0091534824902724, stddev=0.3463516148968051, median=0.975965, p75=1.1458145, p95=1.4784138999999996, p98=1.6538238999999988, p99=2.185363660000002, p999=4.22124273, mean_rate=0.0, m1=0.0, m5=0.0, m15=0.0, rate_unit=events/millisecond, duration_unit=milliseconds

1.1 集中量
(1)最大值、最小值、平均数:容易理解,不做赘述,即例子中的min=0.480973, max=4.228569,mean=1.0091534824902724,表明最小、最大和平均响应时间;
(2)中位数:一个样本、种群或概率分布中的一个数值,用它可以讲所有数据划分为相等的上下两部分。对于有限的数集,可以通过把所有观察值高低排序后找出正中间的一个作为中位数。如果数据总数是偶数个,通常取最中间的两个数值的平均数作为中位数。如示例中的median=0.975965,代表所有响应时间中最中间的那个数字,可见和平均数(mean=1.0091534824902724)并不相等。

1.2 差异量
(1)全距:将上述的最大值减去最小值即为全距,代表变化的范围,数值越大,说明数据越分散。例子中即为3.747596(max-min),数据跨度并不大;
(2)方差:每个样本值与全体样本值的平均数之差的平方值的平均数,用来度量随机变量和其数学期望(即均值)之间的偏离程度. 数值越小,表明数据分布越集中,例子中即为stddev=0.3463516148968051,表明数据相对很集中。

1.3 地位量
(1)分位数:分位数是将总体的全部数据按大小顺序排列后,处于各等分位置的变量值。例如上面的p95, p999都是这种概念。例如p999=4.22124273,代表99.9%的请求响应时间不大于4.22124273ms;
(2)四分位数:如果分成四等分,就是四分位数;八等分就是八分位数等。四分位数也称为四分位点,它是将全部数据分成相等的四部分,其中每部分包括25%的数据,处在各分位点的数值就是四分位数。四分位数有三个,第一个四分位数就是通常所说的四分位数,称为下四分位数,第二个四分位数就是中位数,第三个四分位数称为上四分位数,分别用Q1、Q2、Q3表示 [1] 。
第一四分位数 (Q1),又称”较小四分位数”,等于该样本中所有数值由小到大排列后第25%的数字。
第二四分位数 (Q2),又称“中位数”,等于该样本中所有数值由小到大排列后第50%的数字,即median=0.975965。
第三四分位数 (Q3),又称“较大四分位数”,等于该样本中所有数值由小到大排列后第75%的数字, 例如p75=1.1458145

二 相关的术语

2.1 QPS/TPS/PV/

QPS: Query Per Second每秒的查询次数;
TPS: Transaction per second 每秒的业务量;
PV: page view: 即文章的浏览量,常用于web服务器页面的统计。

2.2 度量类型

(1) Counter
代表一个递增的值,例如请求数,在server正常运行时,只会增加不会减少,在重启时会归0,例如:Cassandra的metric中的下列指标,都是只增不减,重启归0的值。
Counters:
name=client-timeouts, count=0
name=connection-errors, count=0
name=ignores, count=0
name=ignores-on-write-timeout, count=0
name=other-errors, count=0
name=read-timeouts, count=0
name=retries, count=0
name=speculative-executions, count=0
name=unavailables, count=0
name=write-timeouts, count=0

(2) Gauge
代表一个变化的值,表明是个瞬间的值。例如温度等可变化因素,下例中的Cassandra的metric值都是可以变化:

GAUGE:
name=blocking-executor-queue-depth, value=0
name=connected-to, value=7
name=executor-queue-depth, value=0
name=known-hosts, value=36
name=open-connections, value=8
name=reconnection-scheduler-task-count, value=0
name=task-scheduler-task-count, value=1
name=trashed-connections, value=0

(3) Histogram
即直方图: 主要使用来统计数据的分布情况,最大值、最小值、平均值、中位数,百分比(75%、90%、95%、98%、99%和99.9%)

count=171396, min=0.480973, max=4.228569, mean=1.0091534824902724, stddev=0.3463516148968051, median=0.975965, p75=1.1458145, p95=1.4784138999999996, p98=1.6538238999999988, p99=2.185363660000002, p999=4.22124273

注意关于百分比的相关参数并不是实际整个server运行期间的百分比,而是基于一定的统计方法来计算的值,试想,不可能存储所有的请求的数据,只能从时间和空间两个维度来推算。

例如:
com.codahale.metrics.SlidingWindowReservoir

package com.codahale.metrics;</code>

import static java.lang.Math.min;

/**
* A {@link Reservoir} implementation backed by a sliding window that stores the last {@code N}
* measurements.
*/
public class SlidingWindowReservoir implements Reservoir {
private final long[] measurements;
private long count;

/**
* Creates a new {@link SlidingWindowReservoir} which stores the last {@code size} measurements.
*
* @param size the number of measurements to store
*/
public SlidingWindowReservoir(int size) {
this.measurements = new long[size];
this.count = 0;
}

@Override
public synchronized int size() {
return (int) min(count, measurements.length);
}

@Override
public synchronized void update(long value) {
measurements[(int) (count++ % measurements.length)] = value;
}

@Override
public Snapshot getSnapshot() {
final long[] values = new long[size()];
for (int i = 0; i &lt; values.length; i++) {
synchronized (this) {
values[i] = measurements[i];
}
}
return new Snapshot(values);
}
}

(4) Meters
用来度量某个时间段的平均处理次数(request per second),每1、5、15分钟的TPS。比如一个service的请求数,统计结果有总的请求数,平均每秒的请求数,以及最近的1、5、15分钟的平均TPS:

mean_rate=0.0, m1=0.0, m5=0.0, m15=0.0, rate_unit=events/millisecond, duration_unit=milliseconds

(5) Timer
主要是用来统计某一块代码段的执行时间以及其分布情况,具体是基于Histograms和Meters来实现的,所以结果是上面两个指标的合并结果。

三 Metric处理的一些术语:

3.1 Event time / Ingestion time / Processing Time
在流程序中支持不同概念的时间。以读取一条存储在kafka中的数据为例,存在多个时间字段:

Tue Jul 03 11:02:20 CST 2018 offset = 1472200, key = null, value = {"path":"/opt/application/logs/metrics_07012018_0.24536.log","component":"app","@timestamp":"2018-07-01T03:14:53.982Z","@version":"1","host":"10.224.2.116","eventtype":"metrics","message":{"componentType":"app","metricType":"innerApi","metricName":"demo","componentAddress":"10.224.57.67","featureName":"PageCall","componentVer":"2.2.0","poolName":"test","trackingID":"0810823d-dc92-40e4-8e9a-18774d549e21","timestamp":"2018-07-01T03:14:53.728Z"}}

Event time: 是事件发生的时间,而不考虑事件记录通过什么系统、什么时候到达、以什么样的顺序到达等因素。上例中2018-07-01T03:14:53.728Z即为server日志产生的时间;相当于邮寄快递时的邮寄时间。

Ingestion time:即摄入时间,是事件进入Metric系统的时间,在源操作中每个记录都会获得源的当前时间作为时间戳,当一个metric经过n条系统时,相对每条进入的数据,摄入时间就会存在多个。上例中的”@timestamp”:”2018-07-01T03:14:53.982Z”即为logstash获取这个server log的时间。相当于邮寄快递时,快递员的揽收时间。

Processing Time: 是处理系统开始处理某个事件的时间,上例中“Tue Jul 03 11:02:20 CST”为处理时间。相当于邮寄快递中的签收时间。

明确各种时间概率后,可见事件时间是最重要的。

3.2 tulbmimg window / hop window / session window

(1) Tumbling windows
Tumbling即翻跟头的意思,窗口不可能重叠的;在流数据中进行滚动,这种窗口不存在重叠,也就是说一个metric只可能出现在一个窗口中。

(2)Sliding Windows
是可能存在重叠的,即滑动窗口。

(3)session window
以上两种都是基于时间的,固定的窗口,还存在一种window: session window
当我们需要分析用户的一段交互的行为事件时,通常的想法是将用户的事件流按照“session”来分组。session 是指一段持续活跃的期间,由活跃间隙分隔开。通俗一点说,消息之间的间隔小于超时阈值(sessionGap),如果两个元素的时间戳间隔小于 session gap,则会在同一个session中。如果两个元素之间的间隔大于session gap,且没有元素能够填补上这个gap,那么它们会被放到不同的session中。

通过以上不同类别的基本指标和术语的了解,可以大体了解metric领域的一些知识,为以后的metric工作的进展做铺垫。

参考文献:
https://baike.baidu.com/item/%E4%B8%AD%E4%BD%8D%E6%95%B0
https://www.jianshu.com/p/68ab40c7f347
https://yq.aliyun.com/articles/64818
https://kafka.apache.org/11/documentation/streams/developer-guide/dsl-api.html#windowing
https://prometheus.io/docs/concepts/metric_types/
https://www.app-metrics.io/getting-started/metric-types/
http://www.cnblogs.com/nexiyi/p/metrics_sample_1.html

metric driven (2) – select metrics strategy

对metric方案的选择:

  • 功能性角度:
    单纯衡量metric方案,大多已经满足基本功能,但是除此之外,更需要考虑功能的完整性:
    (1) 是否支持硬件层次(CPU、Memory、Disk、Network等)的数据收集和展示;(2) 是否对常见服务有更轻便的支持。

市场上流行的服务都比较集中,例如数据库有oracle、mysql,缓存有memcached、redis等,服务器容器有tomcat,jetty等,消息中间件有rabbitmq、kafka。所以很多metric系统除了通用方案外,还额外对这些常见服务有更轻便的直接接入支持。

(3) 是否集成Alert功能

Metrics里面含有的数据越丰富可以做的事情也越多:
a. 根据主机metric,判断主机故障,例如磁盘是否快满了;
b. 根据错误信息判断是否当前存在故障;
c. 根据metric趋势,判断是否需要扩容;
d. 根据用户行为信息判断是否存在恶意攻击,

当判断出这些信息,仅仅展示是不够的,更应该是提供预警和报警功能,以立马能够解决。同时报警的通知方式是否多样化(邮件、电话、短信、其他及时通信系统的集成)或者进行了分级(轻重缓解不同,不同方式)。

有了更丰富的功能,则避免多种方案的东拼西凑,有利于一体化。

  • 扩展性角度:
    (1) 容量是否具有可扩容性:
    当数据量小时,传统的Sql数据库甚至excel、csv都能存储所有的历史metric数据,并能满足查询等需求,但是除非可预见业务量永不会有突破,否则初始调研时,就应该考虑容量可扩展的方案。例如influxdb单机版是免费的,但是想使用集群模式的时候就变成了收费模式。所以在不喜欢额外投资,只热衷开源方案的企业,长远计划时则不需要选择这类产品。

(2) 切换新方案或者新增多层方案时,方案的可移植性:
很少有一种metric系统能满足所有需求,特别是定制化需求比较多的时候,而对于初创公司而言,可能更换metric系统更为频繁,所以假设选择的方案本身具有强耦合性,不具有可移植性时,就会带来一些问题:
a. 并存多种metric系统,每种方案都对系统资源有所占用

例如方案A通过发http请求,方案B通过写日志,方案C通过直接操作数据库。最后系统本身变成了metric系统的战场。

b. 切换新老metric系统时,需要做的工作太多。

参考问题a,每种方案的方式都不同,例如使用new relic时,需要的是绑定一个new relic jar,根据这个jar定制的规则,不见得适合其他的metric方案,例如influxdb.所以迁移时,不仅要重新修改代码,甚至修改数据结构。
所以方案本身的扩展性不仅体现在本身容量要具有可扩展性,还在于方案是否容易切换或者与其他方案并存,并与业务系统解耦,所以在实际操作时,可能需要加入一个中间层去解耦,例如常见的ELK增加一个kafka来解耦和隔离变化。

  • 技术性能角度:

1. Invasive->Non-invasive
从技术角度看,选择的metric方案本身是否具有侵入性是需要考虑的第一要素,一般而言,侵入性方案提供的功能更具有可定制性和丰富性,但是代价是对系统本身会有一定的影响,例如new relic,除了常用的功能外,还能根据不同的数据库类型显示slow query等,但是它采用的方案是使用java agent在class 被加载之前对其拦截,已插入我们的监听字节码。所以实际运行的代码已不单纯是项目build出的package。不仅在业务执行前后做一些额外的操作,同时也会共享同一个jvm内的资源:例如cpu和memory等。所以在使用new relic时,要求“开辟”更多点的内存,同时也要求给项目本身的影响做一定的评估。当然new relic本身也考虑到,对系统本身的影响,所以引入了“熔断器”来保护应用程序:

com.newrelic.agent.config.CircuitBreakerConfig:

	this.memoryThreshold = ((Integer) this.getProperty("memory_threshold", Integer.valueOf(20))).intValue();
	this.gcCpuThreshold = ((Integer) this.getProperty("gc_cpu_threshold", Integer.valueOf(10))).intValue();

com.newrelic.agent.circuitbreaker.CircuitBreakerService:

内存控制:

double percentageFreeMemory = 100.0D * ((double) (Runtime.getRuntime().freeMemory()
						+ (Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory()))
						/ (double) Runtime.getRuntime().maxMemory());

CPU控制:

获取年老代:

GarbageCollectorMXBean lowestGCCountBean = null;
Agent.LOG.log(Level.FINEST, "Circuit breaker: looking for old gen gc bean");
boolean tie = false;
long totalGCs = this.getGCCount();
Iterator arg5 = ManagementFactory.getGarbageCollectorMXBeans().iterator();

while (true) {
	while (arg5.hasNext()) {
		GarbageCollectorMXBean gcBean = (GarbageCollectorMXBean) arg5.next();
		Agent.LOG.log(Level.FINEST, "Circuit breaker: checking {0}", gcBean.getName());
		if (null != lowestGCCountBean
				&amp;amp;amp;amp;&amp;amp;amp;amp; lowestGCCountBean.getCollectionCount() &amp;amp;amp;lt;= gcBean.getCollectionCount()) {
			if (lowestGCCountBean.getCollectionCount() == gcBean.getCollectionCount()) {
				tie = true;
			}
		} else {
			tie = false;
			lowestGCCountBean = gcBean;
		}
	}

	if (this.getGCCount() == totalGCs &amp;amp;amp;amp;&amp;amp;amp;amp; !tie) {
		Agent.LOG.log(Level.FINEST, "Circuit breaker: found and cached oldGenGCBean: {0}",
				lowestGCCountBean.getName());
		this.oldGenGCBeanCached = lowestGCCountBean;
		return this.oldGenGCBeanCached;
	}

	Agent.LOG.log(Level.FINEST, "Circuit breaker: unable to find oldGenGCBean. Best guess: {0}",
			lowestGCCountBean.getName());
	return lowestGCCountBean;
}
				
 

年老代GC时间占比计算:

	long currentTimeInNanoseconds = System.nanoTime();
	long gcCpuTime = this.getGCCpuTimeNS() - ((Long) this.lastTotalGCTimeNS.get()).longValue();
	long elapsedTime = currentTimeInNanoseconds - ((Long) this.lastTimestampInNanoseconds.get()).longValue();
	double gcCpuTimePercentage = (double) gcCpuTime / (double) elapsedTime * 100.0D;

2  Tcp -> Udp

使用tcp方式可靠性高,但是效率低,占用资源多,而使用udp可靠性低,但是效率高,作为metric数据本身,udp本身更适合,因为不是核心数据,丢弃少数也无所谓。

3 Sync->Async
同步方式直接影响业务请求响应时间,假设写metric本身消耗10ms,则请求响应也相应增加对应时间,但是使用异步时,不管是操作时间长的问题还是操作出错,都不会影响到业务流程。同时也容易做batch处理或者其他额外的控制。

4 Single-> Batch
对于metric数据本身,需要考察是否提供了batch的模式,batch因为数据内容更集中,从而可以减少网络开销次数和通信“头”格式的额外重复size等,同时batch方式也更容易采用压缩等手段来节约空间,毕竟metric数据本身很多字段key应该都是相同的。当然要注意的是过大的batch引发的问题,例如udp对size大小本身有限制,batch size过大时,操作时间会加长,是否超过timeout的限制。
以influxdb为例,使用udp模式的batch(小于64k)和single时,时间消耗延时如下表:

Mode\ms 300 500 800 1000
Single 34 85 111 173
batch 25 22 28 29
  • 总结

通过以上分析,可知选择一个metric系统不应该仅仅局限当前需求,而更应该从多个角度兼顾未来发展,同时对应用产生侵入性低、隔离变化、易于切换都是选择方案必须追求的要素,否则没有搞成想要的metrics却拉倒了应用则得不偿失。

 

metric driven (1) – aware spike erosion

现象

查看某个host的最近2周的load(last 1 minute)情况,可观察到尖峰时刻发生在5月3号,数值为0.167.

当试图查看具体什么时间点达到尖峰时(缩小时间范围),发现这个尖峰时刻的数据值不再是0.167,而是2:

从现象上表明:时间范围越大,数据的可信度越低,继续查看图1中的其他低点的尖峰,缩小时间范围后,竟然超过了第一尖峰,验证了这个设想。

原因

按常理,绘制图像的时候,直接将所有的数据展示出来即可,简单明了,但是实际操作中,对于一个时间范围较小的数据量较小时,并不存在问题,但是考虑到假设需要展示1年或者一个数据量超级大的数据时,存在两个问题:

  • 性能问题:显而易见,“点”越多,绘制的时间越长,性能也越差,可能等几分钟才能等到一幅图渲染完成。例如下图:如此稠密的图,展示就已经耗费很多时间,而实际上稠密的部分并无太多意义:

  • 显示问题:假设所用的电脑屏幕分辨率是1920 x 1080,同时X轴以时间为单位,当展示大范围的时间范围(例如1年)的时候,则每个肉眼有价值的点是“1年*365天*24小时/1920=4.56天”,而56天的数据范围,要不全部展示(则出现问题1),要不采取一定的策略,仅展示一部分。

所以结合现实(电脑分辨率)和性能问题(数据量太大),很多metric绘制采取了一定的策略。例如上文提及中观察到奇怪现象原因在于:

当展示一段时间范围内的数据时,采用的是平均值。所以当时间范围越大,尖峰越不准确,因为这个尖峰实际上所显示时间范围的平均值。

解决:

了解现象的原因,自然可以注意到并接纳这个现象,同时也大体能思考出如何解决, 既然展示的是平均值,那真正需要“尖峰”,展示最大值即可。

(1)对于没有提供辅助方案或者懒于处理数据的用户:

接纳这个现象,真正需要尖峰数据时,多选择尖峰时刻,多缩小范围。

(2)很多metric系统提供了辅助方案:

例如Cassandra自带的opscenter中的一些图: 显示平均值之外,显示最大值和最小值:

例如circonus提供了Aggregation Overlays,在原有默认展示图的基础之上,展示各种定制的“图层”

增加图层后:最大值变成了6.1,而不是最开始的0.167,时间点也不是图1所展示的尖峰时刻。

小结

注意metric图形中是否有spike erosion现象,如果存在,则接受之并适当处理,才能获取到真正想要的数据。而不是诧异于数据的诡异。

再如一类似例子:

下图为redis监控的一个指标,表示当前连接的客户端的数目,但是奇怪的是,偶尔会出现小数结果:例如图中的37.5.

实际上,虽然是gauge的值类型,也是整型值,但是显示时间范围不同时,怎么展示值是个问题?是最后一个数据,还是第一个数据,还是平均数还是最大值,而在这里可以注意到value type是average value即平均值。例如时间范围选择2天时,会以10分钟为频度计算平均值,所以会出现小数点值。

总结:metric的阅读中,可能存在很多无法理解的现象,要考虑到实际绘制的原理和数据如何产生等问题,才能有效理解metric.