什么是metrics:

Flink 提供的 Metrics 可以在 Flink 内部收集一些指标,通过这些指标让开发人员更好地理解作业或集群的状态。由于集群运行后很难发现内部的实际状况,跑得慢或快,是否异常等,开发人员无法实时查看所有的 Task 日志,比如作业很大或者有很多作业的情况下,该如何处理?此时 Metrics 可以很好的帮助开发人员了解作业的当前状况。

Metric Types

Metrics 的类型如下:

1、常用的如 Counter,写过 mapreduce 作业的开发人员就应该很熟悉 Counter,其实含义都是一样的,就是对一个计数器进行累加,即对于多条数据和多兆数据一直往上加的过程。
2、Gauge,Gauge 是最简单的 Metrics,它反映一个值。比如要看现在 Java heap 内存用了多少,就可以每次实时的暴露一个 Gauge,Gauge 当前的值就是heap使用的量。
3、Meter,Meter 是指统计吞吐量和单位时间内发生“事件”的次数。它相当于求一种速率,即事件次数除以使用的时间。
4、Histogram,Histogram 比较复杂,也并不常用,Histogram 用于统计一些数据的分布,比如说 Quantile、Mean、StdDev、Max、Min 等。

Metric Group

Metric 在 Flink 内部有多层结构,以 Group 的方式组织,它并不是一个扁平化的结构,Metric Group + Metric Name 是 Metrics 的唯一标识。

Metric Group 的层级有 TaskManagerMetricGroup 和TaskManagerJobMetricGroup,每个 Job 具体到某一个 task 的 group,task 又分为 TaskIOMetricGroup 和 OperatorMetricGroup。

Operator 下面也有 IO 统计和一些 Metrics,整个层级大概如下图所示。Metrics 不会影响系统,它处在不同的组中,并且 Flink支持自己去加 Group,可以有自己的层级。

•TaskManagerMetricGroup
  •TaskManagerJobMetricGroup
    •TaskMetricGroup
      •TaskIOMetricGroup
      •OperatorMetricGroup
        •${User-defined Group} / ${User-defined Metrics}
        •OperatorIOMetricGroup
•JobManagerMetricGroup
  •JobManagerJobMetricGroup

JobManagerMetricGroup 相对简单,相当于 Master,它的层级也相对较少。

Metrics 定义还是比较简单的,即指标的信息可以自己收集,自己统计,在外部系统能够看到 Metrics 的信息,并能够对其进行聚合计算。

如何使用 Metrics?

System Metrics

System Metrics,将整个集群的状态已经涵盖得非常详细。具体包括以下方面:

Master 级别和 Work 级别的 JVM 参数,如 load 和 time;其 Memory 划分也很详细,包括 heap 的使用情况、non-heap 的使用情况、direct 的使用情况,以及 mapped 的使用情况;Threads 可以看到具体有多少线程;还有非常实用的 Garbage Collection。
Network 使用比较广泛,当需要解决一些性能问题的时候,Network 非常实用。Flink 不只是网络传输,还是一个有向无环图的结构,可以看到它的每个上下游都是一种简单的生产者消费者模型。Flink 通过网络相当于标准的生产者和消费者中间通过有限长度的队列模型。如果想要评估定位性能,中间队列会迅速缩小问题的范围,能够很快的找到问题瓶颈。

•CPU
•Memory
•Threads
•Garbage Collection
•Network
•Classloader
•Cluster
•Availability
•Checkpointing
•StateBackend•IO


•详见: [https://ci.apache.org/projects/flink/flink-docs-release-1.8/monitoring/metrics.html#system-metrics](https://ci.apache.org/projects/flink/flink-docs-release-1.8/monitoring/metrics.html)
运维集群的人会比较关心 Cluster 的相关信息,如果作业太大,则需要非常关注 Checkpointing,它有可能会在一些常规的指标上无法体现出潜在问题。比如 Checkpointing 长时间没有工作,数据流看起来没有延迟,此时可能会出现作业一切正常的假象。另外,如果进行了一轮 failover 重启之后,因为 Checkpointing 长时间没有工作,有可能会回滚到很长一段时间之前的状态,整个作业可能就直接废掉了。
RocksDB 是生产环境当中比较常用的 state backend 实现,如果数据量足够大,就需要多关注 RocksDB 的 Metrics,因为它随着数据量的增大,性能可能会下降。

自定义Metrics:

除了系统的 Metrics 之外,Flink 支持自定义 Metrics ,即 User-defined Metrics。上文说的都是系统框架方面,对于自己的业务逻辑也可以用 Metrics 来暴露一些指标,以便进行监控。

User-defined Metrics 现在提及的都是 datastream 的 API,table、sql 可能需要 context 协助,但如果写 UDF,它们其实是大同小异的。

Datastream 的 API 是继承 RichFunction ,继承 RichFunction 才可以有 Metrics 的接口。然后通过 RichFunction 会带来一个 getRuntimeContext().getMetricGroup().addGroup(…) 的方法,这里就是 User-defined Metrics 的入口。通过这种方式,可以自定义 user-defined Metric Group。如果想定义具体的 Metrics,同样需要用getRuntimeContext().getMetricGroup().counter/gauge/meter/histogram(…) 方法,它会有相应的构造函数,可以定义到自己的 Metrics 类型中。

使用代码示例:

Counter:
用与存储数值类型,比如统计数据输入、输出总数量。

public class MyMapper extends RichMapFunction<String, String> {
  private transient Counter counter;

  @Override
  public void open(Configuration config) {
    this.counter = getRuntimeContext()
      .getMetricGroup()
      .counter("myCounter");
  }

  @Override
  public String map(String value) throws Exception {
    this.counter.inc();
    return value;
  }
}

Gauge:
可以用来存储任何类型,前提要实现org.apache.flink.metrics.Gauge接口,重写getValue方法,如果返回类型为Object则该类需要重写toString方法。

有些场景下,需要根据业务计算出指标,则Gauge使用起来更灵活。

public class MyMapper extends RichMapFunction<String, String> {
  private transient int valueToExpose = 0;

  @Override
  public void open(Configuration config) {
    getRuntimeContext()
      .getMetricGroup()
      .gauge("MyGauge", new Gauge<Integer>() {
        @Override
        public Integer getValue() {
          return valueToExpose;
        }
      });
  }

  @Override
  public String map(String value) throws Exception {
    valueToExpose++;
    return value;
  }
}

 

项目实现代码举例:

添加自定义监控指标,以flink1.5的Kafka读取以及写入为例,添加rps、dirtyData等相关指标信息。�kafka读取和写入重点是先拿到RuntimeContex初始化指标,并传递给要使用的序列类,通过重写序列化和反序列化方法,来更新指标信息。
不加指标的kafka数据读取、写入Demo。
public class FlinkEtlTest {
    private static final Logger logger = LoggerFactory.getLogger(FlinkEtlTest.class);

    public static void main(String[] args) throws Exception {
        final ParameterTool params = ParameterTool.fromArgs(args);
        String jobName = params.get("jobName");

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        /** 设置kafka数据 */
        String topic = "myTest01";
        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "localhost:9092");
        props.setProperty("zookeeper.quorum", "localhost:2181/kafka");

        // 使用FlinkKafkaConsumer09以及SimpleStringSchema序列化类,读取kafka数据
        FlinkKafkaConsumer09<String> consumer09 = new FlinkKafkaConsumer09(topic, new SimpleStringSchema(), props);
        consumer09.setStartFromEarliest();

        // 使用FlinkKafkaProducer09和SimpleStringSchema反序列化类,将数据写入kafka
        String sinkBrokers = "localhost:9092";
        FlinkKafkaProducer09<String> myProducer = new FlinkKafkaProducer09<>(sinkBrokers, "myTest01", new SimpleStringSchema());


        DataStream<String> kafkaDataStream = env.addSource(consumer09);
        kafkaDataStream = kafkaDataStream.map(str -> {
            logger.info("map receive {}",str);
            return str.toUpperCase();
        });

        kafkaDataStream.addSink(myProducer);

        env.execute(jobName);
    }

    
}

下面重新复写flink的

FlinkKafkaConsumer09
FlinkKafkaProducer09

方法,加入metrics的监控。

为kafka读取添加相关指标
  • 继承FlinkKafkaConsumer09,获取它的RuntimeContext,使用当前MetricGroup初始化指标参数。

 

public class CustomerFlinkKafkaConsumer09<T> extends FlinkKafkaConsumer09<T> {

    CustomerSimpleStringSchema customerSimpleStringSchema;
    // 构造方法有多个
    public CustomerFlinkKafkaConsumer09(String topic, DeserializationSchema valueDeserializer, Properties props) {
        super(topic, valueDeserializer, props);
        this.customerSimpleStringSchema = (CustomerSimpleStringSchema) valueDeserializer;
    }

    @Override
    public void run(SourceContext sourceContext) throws Exception {
        //将RuntimeContext传递给customerSimpleStringSchema
        customerSimpleStringSchema.setRuntimeContext(getRuntimeContext());
       // 初始化指标
        customerSimpleStringSchema.initMetric();
        super.run(sourceContext);
    }
}

 

重写SimpleStringSchema类的反序列化方法,当数据流入时变更指标。

public class CustomerSimpleStringSchema extends SimpleStringSchema {

    private static final Logger logger = LoggerFactory.getLogger(CustomerSimpleStringSchema.class);

    public static final String DT_NUM_RECORDS_RESOVED_IN_COUNTER = "dtNumRecordsInResolve";
    public static final String DT_NUM_RECORDS_RESOVED_IN_RATE = "dtNumRecordsInResolveRate";
    public static final String DT_DIRTY_DATA_COUNTER = "dtDirtyData";
    public static final String DT_NUM_BYTES_IN_COUNTER = "dtNumBytesIn";
    public static final String DT_NUM_RECORDS_IN_RATE = "dtNumRecordsInRate";

    public static final String DT_NUM_BYTES_IN_RATE = "dtNumBytesInRate";
    public static final String DT_NUM_RECORDS_IN_COUNTER = "dtNumRecordsIn";



    protected transient Counter numInResolveRecord;
    //source RPS
    protected transient Meter numInResolveRate;
    //source dirty data
    protected transient Counter dirtyDataCounter;

    // tps
    protected transient Meter numInRate;
    protected transient Counter numInRecord;

    //bps
    protected transient Counter numInBytes;
    protected transient Meter numInBytesRate;



    private transient RuntimeContext runtimeContext;

    public void initMetric() {
        numInResolveRecord = runtimeContext.getMetricGroup().counter(DT_NUM_RECORDS_RESOVED_IN_COUNTER);
        numInResolveRate = runtimeContext.getMetricGroup().meter(DT_NUM_RECORDS_RESOVED_IN_RATE, new MeterView(numInResolveRecord, 20));
        dirtyDataCounter = runtimeContext.getMetricGroup().counter(DT_DIRTY_DATA_COUNTER);

        numInBytes = runtimeContext.getMetricGroup().counter(DT_NUM_BYTES_IN_COUNTER);
        numInRecord = runtimeContext.getMetricGroup().counter(DT_NUM_RECORDS_IN_COUNTER);

        numInRate = runtimeContext.getMetricGroup().meter(DT_NUM_RECORDS_IN_RATE, new MeterView(numInRecord, 20));
        numInBytesRate = runtimeContext.getMetricGroup().meter(DT_NUM_BYTES_IN_RATE , new MeterView(numInBytes, 20));



    }
    // 源表读取重写deserialize方法
    @Override
    public String deserialize(byte[] value) {
        // 指标进行变更
        numInBytes.inc(value.length);
        numInResolveRecord.inc();
        numInRecord.inc();
        try {
            return super.deserialize(value);
        } catch (Exception e) {
            dirtyDataCounter.inc();
        }
        return "";
    }


    public void setRuntimeContext(RuntimeContext runtimeContext) {
        this.runtimeContext = runtimeContext;
    }
}

 

代码中使用自定义的消费者进行调用:

CustomerFlinkKafkaConsumer09<String> consumer09 = new CustomerFlinkKafkaConsumer09(topic, new CustomerSimpleStringSchema(), props);
为kafka写入添加相关指标
  • 继承FlinkKafkaProducer09类,重写open方法,拿到RuntimeContext,初始化指标信息传递给CustomerSinkStringSchema。
public class  CustomerFlinkKafkaProducer09<T> extends FlinkKafkaProducer09<T> {

    public static final String DT_NUM_RECORDS_OUT = "dtNumRecordsOut";
    public static final String DT_NUM_RECORDS_OUT_RATE = "dtNumRecordsOutRate";

    CustomerSinkStringSchema schema;

    public CustomerFlinkKafkaProducer09(String brokerList, String topicId, SerializationSchema serializationSchema) {
        super(brokerList, topicId, serializationSchema);
        this.schema = (CustomerSinkStringSchema) serializationSchema;
    }



    @Override
    public void open(Configuration configuration) {
        producer = getKafkaProducer(this.producerConfig);

        RuntimeContext ctx = getRuntimeContext();
        Counter counter = ctx.getMetricGroup().counter(DT_NUM_RECORDS_OUT);
        //Sink的RPS计算
        MeterView meter = ctx.getMetricGroup().meter(DT_NUM_RECORDS_OUT_RATE, new MeterView(counter, 20));
        // 将counter传递给CustomerSinkStringSchema
        schema.setCounter(counter);

        super.open(configuration);
    }

}

 

重写SimpleStringSchema的序列化方法

public class CustomerSinkStringSchema extends SimpleStringSchema {

    private static final Logger logger = LoggerFactory.getLogger(CustomerSinkStringSchema.class);

    private Counter sinkCounter;

    @Override
    public byte[] serialize(String element) {
        logger.info("sink data {}", element);
        sinkCounter.inc();
        return super.serialize(element);  //复写serialize方法,序列化继续使用父类提供的序列化方法
    }

    public void setCounter(Counter counter) {
        this.sinkCounter = counter;
    }
}

 

新的kafkaSinkApi使用

CustomerFlinkKafkaProducer09<String> myProducer = new CustomerFlinkKafkaProducer09<>(sinkBrokers, "mqTest01", new CustomerSinkStringSchema());

 

获取 Metrics

这样就可以在监控框架里面看到采集的指标信息了,

比如flink_taskmanager_job_task_operator_dtDirtyData指标,dtDirtyData是自己添加的指标,前面的字符串是operator默认使用的metricGroup。

获取 Metrics 有三种方法,首先可以在 WebUI 上看到;其次可以通过 RESTful API 获取,RESTful API 对程序比较友好,比如写自动化脚本或程序,自动化运维和测试,通过 RESTful API 解析返回的 Json 格式对程序比较友好;最后,还可以通过 Metric Reporter 获取,监控主要使用 Metric Reporter 功能。

数据分析:

分析任务有时候为什么特别慢呢?

当定位到某一个 Task 处理特别慢时,需要对慢的因素做出分析。分析任务慢的因素是有优先级的,可以从上向下查,由业务方面向底层系统。因为大部分问题都出现在业务维度上,比如查看业务维度的影响可以有以下几个方面,并发度是否合理、数据波峰波谷、数据倾斜;其次依次从 Garbage Collection、Checkpoint Alignment、State Backend 性能角度进行分析;最后从系统性能角度进行分析,比如 CPU、内存、Swap、Disk IO、吞吐量、容量、Network IO、带宽等。