Flink源码系列——指标监测

1、Metric简介

Flink对于指标监测有一套自己的实现,指标的统计方式有四种,这些指标都实现了Metric这个接口,而Metric这个接口只是一个标识,本身并没有定义如何方法接口,部分子类的继承关系如下所示。
Flink源码系列——指标监测_第1张图片
从图中可以看出,Metric这个接口有四个直接子类,分别是:

Gauge —— 最简单的度量指标,只是简单的返回一个值,比如返回一个队列中当前元素的个数;
Counter —— 计数器,在一些情况下,会比Gauge高效,比如通过一个AtomicLong变量来统计一个队列的长度;
Meter —— 吞吐量的度量,也就是一系列事件发生的速率,例如TPS;
Histogram —— 度量值的统计结果,如最大值、最小值、平均值,以及分布情况等。

以MeterView为例,分析一个Metric的具体实现。MeterView还实现View接口,实现View接口的类,表示其会定期的执行update方法,进行数据的更新。

public class MeterView implements Meter, View {
   /** 底层使用的计算器 */
   private final Counter counter;
   /** 计算平均值的事件跨度 */
   private final int timeSpanInSeconds;
   /** 包含历史数据的循环数组 */
   private final long[] values;
   /** 当前时间在数组中的索引 */
   private int time = 0;
   /** 最新计算的rate */
   private double currentRate = 0;

   public MeterView(int timeSpanInSeconds) {
      this(new SimpleCounter(), timeSpanInSeconds);
   }

   public MeterView(Counter counter, int timeSpanInSeconds) {
      this.counter = counter;
      /** 这里的操作是为了让时间跨度刚好是 UPDATE_INTERVAL_SECONDS 的整数倍 */
      this.timeSpanInSeconds = timeSpanInSeconds - (timeSpanInSeconds % UPDATE_INTERVAL_SECONDS);
      this.values = new long[this.timeSpanInSeconds / UPDATE_INTERVAL_SECONDS + 1];
   }

   @Override
   public void markEvent() {
      this.counter.inc();
   }

   @Override
   public void markEvent(long n) {
      this.counter.inc(n);
   }

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

   @Override
   public double getRate() {
      return currentRate;
   }

   @Override
   public void update() {
      time = (time + 1) % values.length;
      values[time] = counter.getCount();
      currentRate =  ((double) (values[time] - values[(time + 1) % values.length]) / timeSpanInSeconds);
   }
}

从类的属性变量中可以看出,MeterView是在一个Counter计数器的基础之上,封装了一层,从而实现事件每秒的平均速率。以values这个长整型的数组,作为环形数组,实现对最新的历史数据的保存。
在构造函数中,会对入参timeSpanInSeconds这个时间跨度进行修正,使其刚好是UPDATE_INTERVAL_SECONDS的整数倍,另外values数组的长度是timeSpanInSeconds对UPDATE_INTERVAL_SECONDS倍数,再加上1,这样这个数组的最新数据和最老的数据之间的时间间隔就刚好是timeSpanInSeconds。
假设values数组的长度为n,则:

1、索引n-1处的统计值,和索引0处的统计值,时间间隔就是timeSpanInSeconds;
2、由于是环形数组,所以索引0处的统计值,和索引1处的统计值的时间间隔就是timeSpanInSeconds;
3、所以索引i处的统计值,和索引(i+1)%n处的统计值,时间间隔是timeSpanInSeconds;

这个逻辑理清楚了,对update方法的逻辑也就清楚了。

另外,对于Metrics相关概念,可以参考 http://wuchong.me/blog/2015/08/01/getting-started-with-metrics/


2、MetricGroup

为了便于对Metric进行方便的管理和区分,可以对Metric进行分组,MetricGroup就是用来实现这个功能的。
MetricGroup的相关子类的继承关系如下所示。
Flink源码系列——指标监测_第2张图片

1、ProxyMetricGroup —— 这是一个代理类,就是把新Metric或者新的子MetricGroup的注册,委托给代理MetricGroup进行处理;
2、AbstractMetricGroup —— 对新增Metric和子MetricGroup的相关方法进行了实现;

在AbstractMetricGroup中有这些属性

protected final A parent;
private final Map metrics = new HashMap<>();
private final Map groups = new HashMap<>();

parent —— 用来保存这个MetricGroup的父MetricGroup
metrics —— 这个map,是用来保存当前MetricGroup中注册的Metric;
groups —— 这个map,是用来保存当前MetricGroup中注册子MetricGroup;

通过这个数据结构可以看出,在MetricGroup中,可以建立一个树状的结构,用来存储和归类相关的Metric。


3、MetricReporter

MetricReporter是用来向外披露Metric的监测结果的接口。
由于MetricReporter的子类在实例化时,都是通过反射机制,所以对于其实现子类,需要有一个公共,无参的构造函数,这个接口的定义如下:

public interface MetricReporter {
   void open(MetricConfig config);
   void close();
   void notifyOfAddedMetric(Metric metric, String metricName, MetricGroup group);
   void notifyOfRemovedMetric(Metric metric, String metricName, MetricGroup group);
}

open —— 由于子类都是用无参构造函数,通过反射进行实例化,所以相关初始化的工作都是放在这里进行的,并且这个方法需要在实例化后,就需要调用该方法进行相关初始化的工作;
close —— 这里就是在关闭时,进行资源回收等相关操作的;
notifyOfAddedMetric —— 当有一个新的Metric注册时,会调用该方法来通知MetricReporter;
notifyOfRemovedMetric —— 当有一个Metric被移除时,通过这个方法来通知MetricReporter;


4、MetricRegistry

MetricGroup是用来对Metric进行分组管理,MetricReporter是用来对外披露Metric,而MetricRegistry就是这两者之间的桥梁,通过MetricRegistry,就可以让MetricReporter感知到在MetricGroup中的Metric发生的变化情况。
对于MetricRegistry这个接口,其实现为MetricRegistryImpl,而其在实例化时,构造函数的入参是一个MetricRegistryConfiguration实例。

4.1、MetricRegistryConfiguration

MetricRegistryConfiguration顾名思义,就是MetricRegistry的相关配置参数,主要有三个属性,如下:

/** flink中不同组件的范围格式 */
private final ScopeFormats scopeFormats;

/** 字符串的分隔符,这是一个全局的分隔符 */
private final char delimiter;

/** 配置中每个reporter的名称和其对应的配置对象的列表 */
private final List> reporterConfigurations;

这些属性,都是从配置参数中获取而来,逻辑如下:

public static MetricRegistryConfiguration fromConfiguration(Configuration configuration) {
   /** 获取scopeFormats */
   ScopeFormats scopeFormats;
   try {
      scopeFormats = ScopeFormats.fromConfig(configuration);
   } catch (Exception e) {
      LOG.warn("Failed to parse scope format, using default scope formats", e);
      scopeFormats = ScopeFormats.fromConfig(new Configuration());
   }

   /** 获取分隔符 */
   char delim;
   try {
      delim = configuration.getString(MetricOptions.SCOPE_DELIMITER).charAt(0);
   } catch (Exception e) {
      LOG.warn("Failed to parse delimiter, using default delimiter.", e);
      delim = '.';
   }

   /** 获取MetricReporter相关的配置信息,MetricReporter的配置格式是 metrics.reporters = foo, bar */
   final String definedReporters = configuration.getString(MetricOptions.REPORTERS_LIST);
   List> reporterConfigurations;

   if (definedReporters == null) {
      /** 如果没有配置,则返回空集合 */
      reporterConfigurations = Collections.emptyList();
   } else {
      /** 按模式匹配分割,如上述的配置,则namedReporters={"foo", "bar"} */
      String[] namedReporters = splitPattern.split(definedReporters);

      reporterConfigurations = new ArrayList<>(namedReporters.length);

      for (String namedReporter: namedReporters) {
         /** 
          * 这里是获取一个代理配置对象,就是在原来配置对象的基础上,在查询key时,需要加上这里配置的前缀,
          * 如 metrics.reporter.foo. ,这样就可以获取特定reporter的配置 
          */
         DelegatingConfiguration delegatingConfiguration = new DelegatingConfiguration(
            configuration,
            ConfigConstants.METRICS_REPORTER_PREFIX + namedReporter + '.');

         reporterConfigurations.add(Tuple2.of(namedReporter, (Configuration) delegatingConfiguration));
      }
   }

   return new MetricRegistryConfiguration(scopeFormats, delim, reporterConfigurations);
}

4.1.1 ScopeFormat

上述的ScopeFormats也是配置对象中获取的,如下:

public static ScopeFormats fromConfig(Configuration config) {
   String jmFormat = config.getString(MetricOptions.SCOPE_NAMING_JM);
   String jmJobFormat = config.getString(MetricOptions.SCOPE_NAMING_JM_JOB);
   String tmFormat = config.getString(MetricOptions.SCOPE_NAMING_TM);
   String tmJobFormat = config.getString(MetricOptions.SCOPE_NAMING_TM_JOB);
   String taskFormat = config.getString(MetricOptions.SCOPE_NAMING_TASK);
   String operatorFormat = config.getString(MetricOptions.SCOPE_NAMING_OPERATOR);

   return new ScopeFormats(jmFormat, jmJobFormat, tmFormat, tmJobFormat, taskFormat, operatorFormat);
}

这里就需要介绍ScopeFormat,其类继承关系如下:
这里写图片描述
从图中可以看出,Flink中的每个组件,都有对应的格式。
首先看下ScopeFormat中的主要属性对象:

/** 这是原生格式,比如 .jobmanager ,如果为空,则是  */
private final String format;

/** format按照分割符分割后的数组,如 template= {"", "jobmanager”},被<>包裹的元素,是变量元素 */
private final String[] template;

/** 这是template数组中,变量元素的索引,如""是变量,在template中的索引是0,则 templatePos = {0} */
private final int[] templatePos;

/** 这个是template中变量元素对应的真实的值,在values数组中的位置,详见 构造函数 和 #bindVariables方法 */
private final int[] valuePos;

这里以JobManagerScopeFormat为例进行分析说明,在ScopeFormats中,默认传给JobManagerScopeFormat的构造函数的入参值是 .jobmanager 。
则JobManagerScopeFormat的构造过程如下:

/** format的默认值是 .jobmanager */
public JobManagerScopeFormat(String format) {
   super(format, null, new String[] {
      SCOPE_HOST
   });
}

接着看起父类ScopeFormat的构造过程:

/** 接上面,入参值为 format=".jobmanager" ,parent=null , variables={""} */
protected ScopeFormat(String format, ScopeFormat parent, String[] variables) {
   checkNotNull(format, "format is null");

   /** 将format这个字符串分割, rawComponents = {"", "jobmanager"} */
   final String[] rawComponents = format.split("\\" + SCOPE_SEPARATOR);

   /** 根据rawComponents的第一个元素是为"*",来判断是否要继承父组的范围 */
   final boolean parentAsPrefix = rawComponents.length > 0 && rawComponents[0].equals(SCOPE_INHERIT_PARENT);
   if (parentAsPrefix) {
      /** 需要继承父组的范围,而父组有是null,则抛出异常 */
      if (parent == null) {
         throw new IllegalArgumentException("Component scope format requires parent prefix (starts with '"
            + SCOPE_INHERIT_PARENT + "'), but this component has no parent (is root component).");
      }

      /** 如果以 "*." 开头,则format至少需要有3个字符,否则就是无效字符,设置为 "" */
      this.format = format.length() > 2 ? format.substring(2) : "";

      String[] parentTemplate = parent.template;
      int parentLen = parentTemplate.length;

      /** 将父组的范围和自身的范围,合并到一起 */
      this.template = new String[parentLen + rawComponents.length - 1];
      System.arraycopy(parentTemplate, 0, this.template, 0, parentLen);
      System.arraycopy(rawComponents, 1, this.template, parentLen, rawComponents.length - 1);
   }
   else {
      /** 不需要继承父组的范围,则直接赋值,format=".jobmanager",template={"", "jobmanager"} */
      this.format = format.isEmpty() ? "" : format;
      this.template = rawComponents;
   }

   /** 将 variables={""} 转换为map {"" -> 0} */
   HashMap varToValuePos = arrayToMap(variables);
   List templatePos = new ArrayList<>();
   List valuePos = new ArrayList<>();

   for (int i = 0; i < template.length; i++) {
      final String component = template[i];
      /** 检查当前这个组件是否是一个变量 */
      if (component != null && component.length() >= 3 &&
            component.charAt(0) == '<' && component.charAt(component.length() - 1) == '>') {
         /** 这是一个变量,则从上面的map中,获取其索引 */
         Integer replacementPos = varToValuePos.get(component);
         if (replacementPos != null) {
            templatePos.add(i);
            valuePos.add(replacementPos);
         }
      }
   }

   this.templatePos = integerListToArray(templatePos);
   this.valuePos = integerListToArray(valuePos);
}

经过这个构造过程,ScopeFormat中的四个属性的值如下:

format = “.jobmanager”
template = {“”, “jobmanager”}
templatePos = {0}
valuePos = {0}

对于JobManagerScopeFormat来说,构建一个具体的范围数组的逻辑如下:

public String[] formatScope(String hostname) {
   /** 获取template数组的一份拷贝,深拷贝 */
   final String[] template = copyTemplate();
   final String[] values = { hostname };
   /** 使用hostname替换掉template中索引为0的元素 */
   return bindVariables(template, values);
}

protected final String[] copyTemplate() {
   String[] copy = new String[template.length];
   System.arraycopy(template, 0, copy, 0, template.length);
   return copy;
}

/** 在结合这个逻辑,就知道ScopeFormat中的属性valuePos的作用了 */
protected final String[] bindVariables(String[] template, String[] values) {
   final int len = templatePos.length;
   for (int i = 0; i < len; i++) {
      template[templatePos[i]] = values[valuePos[i]];
   }
   return template;
}

4.2 MetricRegistryImpl

在获取了MetricRegistryConfiguration实例后,在看MetricRegistryImpl的构造函数的实现逻辑。

this.executor = Executors.newSingleThreadScheduledExecutor(new ExecutorThreadFactory("Flink-MetricRegistry"));

这里给executor这个属性,设置了一个单线程可调度的执行器。
接下来主要看下对MetricReporter相关的初始化工作。

/** 变量配置中配置的reporter的配置 */
for (Tuple2 reporterConfiguration: reporterConfigurations) {
   String namedReporter = reporterConfiguration.f0;
   /** reporterConfig是Configuration的子类DelegatingConfiguration,会肯定定义的前缀来找key */
   Configuration reporterConfig = reporterConfiguration.f1;

   /** 获取MetricReporter的具体实现子类的全限定类型,配置的key如:metrics.reporter.foo.class */
   final String className = reporterConfig.getString(ConfigConstants.METRICS_REPORTER_CLASS_SUFFIX, null);
   if (className == null) {
      LOG.error("No reporter class set for reporter " + namedReporter + ". Metrics might not be exposed/reported.");
      continue;
   }

   try {
      /** 获取配置的定期执行的时间间隔,key的格式如:metrics.reporter.foo.interval */
      String configuredPeriod = reporterConfig.getString(ConfigConstants.METRICS_REPORTER_INTERVAL_SUFFIX, null);
      TimeUnit timeunit = TimeUnit.SECONDS;
      long period = 10;

      if (configuredPeriod != null) {
         try {
            String[] interval = configuredPeriod.split(" ");
            period = Long.parseLong(interval[0]);
            timeunit = TimeUnit.valueOf(interval[1]);
         }
         catch (Exception e) {
            LOG.error("Cannot parse report interval from config: " + configuredPeriod +
                  " - please use values like '10 SECONDS' or '500 MILLISECONDS'. " +
                  "Using default reporting interval.");
         }
      }

      /** 实例化MetricReporter的子类 */
      Class reporterClass = Class.forName(className);
      MetricReporter reporterInstance = (MetricReporter) reporterClass.newInstance();

      /** 构造MetricConfig的实例,并把reporterConfig中的配置key-value都添加到metricConfig中 */
      MetricConfig metricConfig = new MetricConfig();
      reporterConfig.addAllToProperties(metricConfig);
      LOG.info("Configuring {} with {}.", reporterClass.getSimpleName(), metricConfig);
      /** 这里就是reporter进行初始化操作的地方 */
      reporterInstance.open(metricConfig);

      /** 如果reporter实现了Scheduled接口,则通过executor进行定期调度执行,执行时间间隔就是上面获取的时间间隔 */
      if (reporterInstance instanceof Scheduled) {
         LOG.info("Periodically reporting metrics in intervals of {} {} for reporter {} of type {}.", period, timeunit.name(), namedReporter, className);
         /** 将reporter封装成一个task,并调度定期更新执行 */
         executor.scheduleWithFixedDelay(
               new MetricRegistryImpl.ReporterTask((Scheduled) reporterInstance), period, period, timeunit);
      } else {
         LOG.info("Reporting metrics for reporter {} of type {}.", namedReporter, className);
      }
      /** 将reporter添加到集合中 */
      reporters.add(reporterInstance);

      /** 获取reporter定制化的分隔符,如果没有设置,则设置为全局分割符 */
      String delimiterForReporter = reporterConfig.getString(ConfigConstants.METRICS_REPORTER_SCOPE_DELIMITER, String.valueOf(globalDelimiter));
      if (delimiterForReporter.length() != 1) {
         LOG.warn("Failed to parse delimiter '{}' for reporter '{}', using global delimiter '{}'.", delimiterForReporter, namedReporter, globalDelimiter);
         delimiterForReporter = String.valueOf(globalDelimiter);
      }
      this.delimiters.add(delimiterForReporter.charAt(0));
   }
   catch (Throwable t) {
      LOG.error("Could not instantiate metrics reporter {}. Metrics might not be exposed/reported.", namedReporter, t);
   }
}

其中Schedule接口,只有一个report接口。

public interface Scheduled {
   void report();
}

实现Scheduled接口的reporter,表示其需要被定期调度执行,定期执行的就是其report方法,没有实现Scheduled接口的reporter方法,是不会被定期调度的。

1、Slf4jReporter这个MetricReporter的子类就实现了Scheduled接口,而其report方法,就是将注册的Metric的信息打印到日志里;
2、JMXReporter这个子类是没有实现Scheduled接口的,但可以通过JMX服务来获取注册的Metric的信息。


4.3、添加Metric的过程

Metric的添加逻辑的入口在AbstractMetricGroup的addMetric方法中,逻辑如下:

protected void addMetric(String name, Metric metric) {
   if (metric == null) {
      LOG.warn("Ignoring attempted registration of a metric due to being null for name {}.", name);
      return;
   }
   /** 只有group仍然打开的情况下, 才添加这个metric */
   synchronized (this) {
      if (!closed) {
         /**
          * 在没有进行"contains"校验下, 立即进行put操作, 来优化常见的情况(没有碰撞)
          * 碰撞的情况后面会处理。
          */
         Metric prior = metrics.put(name, metric);

         /** 检查与其他度量名称的冲突 */
         if (prior == null) {
            /** 这个名字还没有其他指标,也就是与注册在当前group下的metric没有名称冲突 */

            if (groups.containsKey(name)) {
               /** 与注册在group下的子groups的名称由冲突,这里给出warn日志, 而不是fail, 因为metrics是工具, 当使用错误时, 不应该使得程序失败 */
               LOG.warn("Name collision: Adding a metric with the same name as a metric subgroup: '" +
                     name + "'. Metric might not get properly reported. " + Arrays.toString(scopeComponents));
            }

            /** 这里就是桥梁起作用的地方 */
            registry.register(metric, name, this);
         }
         else {
            /** 有碰撞, 放回原来的metric */
            metrics.put(name, prior);

            LOG.warn("Name collision: Group already contains a Metric with the name '" +
                  name + "'. Metric will not be reported." + Arrays.toString(scopeComponents));
         }
      }
   }
}

上述逻辑就是把Metric注册到当前Group中,接着看调用了MetricRegistry的register里的逻辑。

public void register(Metric metric, String metricName, AbstractMetricGroup group) {
   synchronized (lock) {
      if (isShutdown()) {
         LOG.warn("Cannot register metric, because the MetricRegistry has already been shut down.");
      } else {
         if (reporters != null) {
            /** 通知所有的reporters,注册了一个metric,以及对应的metricName,group */
            for (int i = 0; i < reporters.size(); i++) {
               MetricReporter reporter = reporters.get(i);
               try {
                  if (reporter != null) {
                     /**
                      * 这里会将group,以及这个reporter在reporters这个列表中的索引,一起封装到FrontMetricGroup这个代理类中
                      * 这里封装索引的目的,是可以通过 #getDelimiter 方法,获取这个reporter配置的特制分隔符
                      */
                     FrontMetricGroup front = new FrontMetricGroup>(i, group);
                     /** 然后调用reporter的接口方法,通知reporter */
                     reporter.notifyOfAddedMetric(metric, metricName, front);
                  }
               } catch (Exception e) {
                  LOG.warn("Error while registering metric.", e);
               }
            }
         }
         try {
            /** 如果queryService不为null,则也通知它 */
            if (queryService != null) {
               MetricQueryService.notifyOfAddedMetric(queryService, metric, metricName, group);
            }
         } catch (Exception e) {
            LOG.warn("Error while registering metric.", e);
         }
         try {
            /** 如果metric实现了View接口,则将其添加到定期更新的viewUpdater中 */
            if (metric instanceof View) {
               if (viewUpdater == null) {
                  viewUpdater = new ViewUpdater(executor);
               }
               viewUpdater.notifyOfAddedView((View) metric);
            }
         } catch (Exception e) {
            LOG.warn("Error while registering metric.", e);
         }
      }
   }
}

从上述逻辑,可以看出MetricRegistry所起的桥梁作用了,它会再依次通知配置的各个reporter,前面已经介绍过AbstractReporter这个抽象子类实现。


4.4、View接口

View接口的定义如下:

public interface View {
   /** metrics更新的间隔 */
   int UPDATE_INTERVAL_SECONDS = 5;

   /** 被定期调用进行metric更新的方法 */
   void update();
}

实现了View接口的Metric,需要定期的调用update方法,进行状态的更新,而这个定期更新的功能是通过ViewUpdater实现的,其构造函数中,就是在executor中添加了一个定期执行的task。

public ViewUpdater(ScheduledExecutorService executor) {
   executor.scheduleWithFixedDelay(new ViewUpdaterTask(lock, toAdd, toRemove), 5, UPDATE_INTERVAL_SECONDS, TimeUnit.SECONDS);
}

新增一个Metric时,通知viewUpdater时的逻辑如下:

public void notifyOfAddedView(View view) {
   synchronized (lock) {
      toAdd.add(view);
   }
}

就是想toAdd这个Set中添加一个新的元素,通过lock这个锁来实现同步。
而ViewUpdaterTask的run方法中,就会调用注册的Metric的update方法,同时更新几个Set。逻辑如下:

public void run() {
   for (View toUpdate : this.views) {
      toUpdate.update();
   }

   synchronized (lock) {
      views.addAll(toAdd);
      toAdd.clear();
      views.removeAll(toRemove);
      toRemove.clear();
   }
}

4.5、MetricQueryService

在MetricRegistryImpl中有一个属性queryService,是一个ActorRef,对应的具体实现是MetricQueryService。在MetricQueryService中也维护了注册的各种Metric,并且也是从MetricRegistry那里接受Metric的添加和删除的消息。

/** 用来接收Metric添加的消息 */
public static void notifyOfAddedMetric(ActorRef service, Metric metric, String metricName, AbstractMetricGroup group) {
   service.tell(new AddMetric(metricName, metric, group), null);
}

/** 用于接收Metric删除的消息 */
public static void notifyOfRemovedMetric(ActorRef service, Metric metric) {
   service.tell(new RemoveMetric(metric), null);
}

MetricQueryService在接收到这类消息后,会在onReceive方法中根据不同的消息类型进行相应的处理,添加和删除Metric就是在四类Metric对应的map属性上进行相应的添加删除操作。以此来实现对Metric信息的维护。
onReceive方法中还会接收到一类消息,叫CreateDump消息,接收到这个消息后,就会把当前所有的Metric数据进行序列化操作,得到一个MetricDumpSerialization.MetricSerializationResult序列化后的结果实例,并发送给请求者。
对于Metric的序列化和反序列化的实现都在MetricDumpSerialization这个类中。

1、通过MetricDumpSerializer进行序列化,序列化后的结果为MetricSerializationResult;
2、通过MetricDumpDeserializer进行反序列化,反序列化后的结果为MetricDump;


你可能感兴趣的:(Flink)