【Flink原理和应用】:Flink的累加器(Accumulator)应用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hxcaifly/article/details/86530055

1. 累加器的简单介绍

累加器是从用户函数和操作中,分布式地统计或者聚合信息。每个并行实例创建并更新自己的Accumulator对象, 然后合并收集器的不同并行实例。在作业结束时由系统合并。

累加器的结果可以从作业执行的结果中获得,也可以从Web运行时监视器中获得。

累加器是受Hadoop/MapReduce计数器的启发。但是要注意添加到累加器的类型可能与返回的类型不同。比如:我们添加单个对象,但是结果返回的是对象的set集合。

可以先看下Flink源码对累加器Accumulator的定义:

package org.apache.flink.api.common.accumulators;

import org.apache.flink.annotation.Public;

import java.io.Serializable;

/**
 * 累加器从用户函数和操作中,分布式地统计信息或聚合。
 * 每个并行实例创建并更新自己的Accumulator对象, 然后合并收集器的不同并行实例。 在作业结束时由系统合并。
 * 结果可以从作业执行的结果中获得,也可以从Web运行时监视器中获得。
 *
 * 累加器是受Hadoop/MapReduce计数器而激发出来的。
 *
 * 添加到收集器的类型可能与返回的类型不同.
 * 例如set类机器: 我们添加单个对象,但是结果返回的是对象的set集合
 * 
 * @param <V> 添加到累加器的值的类型
 *
 * @param <R> 将向客户端报告的累加器结果的类型
 *
 */
@Public
public interface Accumulator<V, R extends Serializable> extends Serializable, Cloneable {
	/**
	 * @param value 要添加到Accumulator对象的值。
	 *
	 */
	void add(V value);

	/**
	 * @return local 当前UDF上下文中的本地值。
	 */
	R getLocalValue();

	/**
	 * 重置本地值。这只影响当前的UDF上下文。
	 */
	void resetLocal();

	/**
	 * 由系统内部使用,用于在作业结束时合并收集器的收集部分。
	 * 
	 * @param other 对要合并的收集器的引用。
	 */
	void merge(Accumulator<V, R> other);

	/**
	 * 复制收集器。所有子类都需要正确地实现克隆,并且不能报错 {@link java.lang.CloneNotSupportedException}
	 *
	 * @return 复制的累加器。
	 */
	Accumulator<V, R> clone();
}

所有我们在任务中自定义的累加器必须要实现这个Accumulator接口。

2. 案例说明

本案例是要实现筛选并计数csv文件中包含空字段的行。并且使用自定义累加器计算csv文件中每列的空字段数。在此案例中,空字段是指那些最多包含空格和制表符等空白字符的字段。

输入文件是纯文本的csv文件,以分号作为字段分隔符,双引号作为字段分隔符和三列。

从这个案例中,我们可以学习到:

  • 自定义累加器
  • tuple数据类型
  • 内联定义函数
  • 命名大型元组类型

3. 代码实现

3.1. 主函数入口分析

从main主函数分析程序的逻辑比较直接点。在代码注释中,主要注释出了四步,因为这四步比较关键。后面的讲述也将围绕这四步来分开讲解。

package org.apache.flink.examples.java.relational;

import org.apache.flink.api.common.JobExecutionResult;
import org.apache.flink.api.common.accumulators.Accumulator;
import org.apache.flink.api.common.functions.RichFilterFunction;
import org.apache.flink.api.java.DataSet;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.configuration.Configuration;

import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class EmptyFieldsCountAccumulator {
	
	private static final String EMPTY_FIELD_ACCUMULATOR = "empty-fields";

	public static void main(final String[] args) throws Exception {

		final ParameterTool params = ParameterTool.fromArgs(args);

		final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
		env.getConfig().setGlobalJobParameters(params);

		// 1. 得到数据集
		final DataSet<StringTriple> file = getDataSet(env, params);

		// 2. 过滤含有空值的行
		final DataSet<StringTriple> filteredLines = file.filter(new EmptyFieldFilter());

		JobExecutionResult result;
		// 3. 执行任务并输出过滤行
		if (params.has("output")) {
			filteredLines.writeAsCsv(params.get("output"));
			// 执行程序
			result = env.execute("Accumulator example");
		} else {
			System.out.println("Printing result to stdout. Use --output to specify output path.");
			filteredLines.print();
			result = env.getLastJobExecutionResult();
		}

		// 4. 通过注册时的key值来获得累加器的结果
		final List<Integer> emptyFields = result.getAccumulatorResult(EMPTY_FIELD_ACCUMULATOR);
		System.out.format("Number of detected empty fields per column: %s\n", emptyFields);
	}
}

3.2. 逻辑实现关键步骤分析

3.2.1. 得到数据集

final DataSet<StringTriple> file = getDataSet(env, params);

这里继续看下getDataSet(env, params)方法:

/**
 * 得到数据集
 * @param env
 * @param params
 * @return
 */
@SuppressWarnings("unchecked")
private static DataSet<StringTriple> getDataSet(ExecutionEnvironment env, ParameterTool params) {
	// 如果指定了input参数
	if (params.has("input")) {
		return env.readCsvFile(params.get("input"))
			.fieldDelimiter(";")
			.pojoType(StringTriple.class);
	// 否则,读取默认的数据集
	} else {
		System.out.println("Executing EmptyFieldsCountAccumulator example with default input data set.");
		System.out.println("Use --input to specify file input.");
		return env.fromCollection(getExampleInputTuples());
	}
}

这步是定义了获取输入数据集的逻辑。如果指定了input参数,那么将直接读取指定的csv文件,否则就读取默认的数据集。这个默认的数据集,我们继续看getExampleInputTuples()方法。

/**
*
 * 得到例子输入Tuple
 * @return
 */
private static Collection<StringTriple> getExampleInputTuples() {
	Collection<StringTriple> inputTuples = new ArrayList<StringTriple>();
	inputTuples.add(new StringTriple("John", "Doe", "Foo Str."));
	inputTuples.add(new StringTriple("Joe", "Johnson", ""));
	inputTuples.add(new StringTriple(null, "Kate Morn", "Bar Blvd."));
	inputTuples.add(new StringTriple("Tim", "Rinny", ""));
	inputTuples.add(new StringTriple("Alicia", "Jackson", "  "));
	return inputTuples;
}

默认数据集是Collection<StringTriple>集合。这个集合的元素类型是StringTriple。StringTriple是自定义的三元组数据结构。

/**
 * 当数据集有比较多的字段时,那么推荐是用POJOs,而不是TupleX
 */
public static class StringTriple extends Tuple3<String, String, String> {

	public StringTriple() {}

	public StringTriple(String f0, String f1, String f2) {
		super(f0, f1, f2);
	}
}

其实StringTriple就是一个三元的Tuple数据结构。不过当字段比较多时,还是不建议应用Tuple数据结构,建议直接应用POJOs要好点。

3.2.2. 过滤含有空值的行

这步是执行过滤操作:

final DataSet<StringTriple> filteredLines = file.filter(new EmptyFieldFilter());

EmptyFieldFilter类实现如下:

/**
* 此函数筛选所有具有一个或多个空字段的传入元组
 *
 * 这样做的同时,它还计算带有累加器(在下注册)的每个属性的空字段数。
 * {@link EmptyFieldsCountAccumulator#EMPTY_FIELD_ACCUMULATOR}).
 */
public static final class EmptyFieldFilter extends RichFilterFunction<StringTriple> {

	// 在每个筛选函数实例中创建新的收集器
	// 以后可以合并累加器
	private final VectorAccumulator emptyFieldCounter = new VectorAccumulator();

	@Override
	public void open(final Configuration parameters) throws Exception {
		super.open(parameters);

		// 注册收集器实例
		getRuntimeContext().addAccumulator(EMPTY_FIELD_ACCUMULATOR,
				this.emptyFieldCounter);
	}

	@Override
	public boolean filter(final StringTriple t) {
		boolean containsEmptyFields = false;

		// 遍历tuple的所有字段,寻找有没有空值
		for (int pos = 0; pos < t.getArity(); pos++) {

			final String field = t.getField(pos);
			if (field == null || field.trim().isEmpty()) {
				containsEmptyFields = true;

				// 如果遇到空字段,请更新累加器
				this.emptyFieldCounter.add(pos);
			}
		}

		return !containsEmptyFields;
	}
}

上述过滤逻辑一开始就初始化了一个累加器VectorAccumulator ,然后把累加器注册到上下文执行环境中。这里累加器VectorAccumulator的定义,应该是我们本文的重点了,下面我们重点分析这一块。

VectorAccumulator的定义逻辑

/**
 * 这个累加器保持一个计数向量. 调用 {@link #add(Integer)} 来增加第n-th列的值.
 * 矢量的大小是自动管理的.
 */
public static class VectorAccumulator implements Accumulator<Integer, ArrayList<Integer>> {

	/** 存储累积向量分量. */
	private final ArrayList<Integer> resultVector;

	/**
	 * 构造函数
	 */
	public VectorAccumulator(){
		this(new ArrayList<Integer>());
	}

	public VectorAccumulator(ArrayList<Integer> resultVector){
		this.resultVector = resultVector;
	}

	/**
	 * 将指定位置的结果向量分量增加1.
	 */
	@Override
	public void add(Integer position) {
		updateResultVector(position, 1);
	}

	/**
	 * 将指定位置的结果向量分量增加指定的增量。
	 */
	private void updateResultVector(int position, int delta) {
		// 如果position超出了列的最大索引,那么就再起一列。
		while (this.resultVector.size() <= position) {
			this.resultVector.add(0);
		}

		// 增加该列的值,列索引为position
		final int component = this.resultVector.get(position);
		this.resultVector.set(position, component + delta);
	}

	@Override
	public ArrayList<Integer> getLocalValue() {
		return this.resultVector;
	}

	@Override
	public void resetLocal() {
		// 如果应重用收集器实例,则清除结果向量
		this.resultVector.clear();
	}

	@Override
	public void merge(final Accumulator<Integer, ArrayList<Integer>> other) {
		// 合并两个累加器 
		final List<Integer> otherVector = other.getLocalValue();
		for (int index = 0; index < otherVector.size(); index++) {
			updateResultVector(index, otherVector.get(index));
		}
	}

	@Override
	public Accumulator<Integer, ArrayList<Integer>> clone() {
		return new VectorAccumulator(new ArrayList<Integer>(resultVector));
	}

	@Override
	public String toString() {
		return StringUtils.join(resultVector, ',');
	}
}

VectorAccumulator是累加元素类型为Integer,然后返回类型为ArrayList<Integer>的累加器,其功能是对空列进行计数累加。其中position参数表示列的所以,比如0表示第0列,1表示第1列等。

分析完VectorAccumulator的逻辑。让我们再重新回到EmptyFieldFilter的filter方法的具体逻辑:其逻辑是对于每行数据,遍历其每列的值,如果某一列有空值,那么就过滤掉该元素,那么最后保留下来就只用没有空值的行了。

另外filter中执行了如下逻辑:

this.emptyFieldCounter.add(pos);

这一步是对空字段进行累加。其中pos表示列的索引。

3.2.3. 执行任务并输出过滤行

这一步简单,就是执行任务,然后打印出结果。

3.2.4. 通过注册时的key值来获得累加器的结果

累加器在生成时,我们是通过:

getRuntimeContext().addAccumulator(EMPTY_FIELD_ACCUMULATOR,
					this.emptyFieldCounter);

注册到了执行上下文环境中,当任务执行完了,其累加器的值,其实保留在了内存中。

通过key值去获取累加器,然后把累加器的值打印出来。

4. 程序正确执行的结果:

(John,Doe,Foo Str.)
Number of detected empty fields per column: [1, 0, 3]

上述打印结果不仅打印出来了没有空值列的行数据。然后也打印出了累加器结果,即表示第0列有1处空值,第1列没有空值,第2列有3处空值。

5. 总结

累加器其实也算是一种有状态(state)的计算,这种状态计算其实在实际应用中非常广泛。学习该案例,我们可以对累加器的用法有一定的理解。

猜你喜欢

转载自blog.csdn.net/hxcaifly/article/details/86530055