Flink中的state是什么?
首先flink从消息队列中读取到的每一条数据称之为事件,虽然很多操作中一次只查看一个单独的事件,但是有些操作会记录多个事件信息,这些操作称为有状态的操作。
有状态操作的一些示例:

当应用程序搜索某些事件模式时,状态将会存储目前为止遇到的时间序列。
当聚合每分钟/小时/天,状态会保存挂起的聚合。
在数据流上训练机器模型,状态会保存当前模型的参数的版本。
当需要管理历史事件,状态允许有效的访问历史事件。

Flink需要知道状态的信息,以便使用checkpoing和savepoint增强状态的容错。
了解这个状态的还可以重新调整flink程序,这意味着flink可以跨并行实例重新分布状态。
可查询状态允许在程序运行时,从外部网文状态。
在处理状态时,读取flink的状态也非常的有用。flink提供了不同的状态后端,用来指定状态的存储方式和存储的位置。
Keyed state

可以将Keyed state维护在一个键/值对存储系统中,状态与有状态的操作符严格的分区和分布。因此,对key/value的状态仅仅是在keyed stream上,例如:keyed的分区交换,仅限与当前事件相关联的值。调整和更新流上的key和状态要确保所有的操作的都是本地操9作。在没有事务的情况下保持一致性,这种对齐还允许Flink重新分配状态并透明地调整流分区。

flink 批处理mysql flink 批处理程序 state_大数据


Keyed state进一步组合为key groups。Key groups是flink重新分配的keyed state的原子单位。与定义的最大并行度相等的键组。在任务运行期间,Key groups的操作符每个并行操作符都与一个或多个key groups 的同时工作。

State Persistence

Flink使用stream replay和checkpoint结合来实现容错,即检查点标记流中某个点和操作符中相应状态。流数据的流可以从检查点中恢复,同时恢复操作符的状态和检查点重放记录来保持一致性(仅处理一次语义)
检查点间隔是在执行期间容错开销与恢复时间(需要重复记录的数量)之间进行权衡的一种方法。
容错机制会持续制作分布式快照,对于状态较小的流式应用程序,制作这些快照是非常轻量级的,并且对性能没有太大的影响。流式应用状态存储在一个可维护的文件系统中,通常是分布式文件系统。
在程序运行失败的情况下(如:网络、机器故障或者软件故障),Flink停止分布式流式数据流。在系统重新启动后,可以从最近成功的检查点恢复程序。重新启动并行数据流的一部分处理任何的记录都保证不会影响以前检查点的状态。

Keyed DataStream

如果想要使用keyed state,首先需要在数据流中指定一个key,该key应用于对状态的分区。在java和scala中使用keyby(ketSelector)或者python中用key_by(KeySelector)在数据流上指定key,将会产生一个keyedstream,然后允许keyed state的操作。
key选择器函数接受单个记录作为输入,并返回这个记录key,key可以是任何数据类型的,并且从确定性计算类型中派生。
Flink上的数据模型实际上不是键值对的,因此,不需要将数据类型打包成键值对形式存储,键是虚拟的,它们被定义为实际数据上的函数,用来指导分组操作符。

Broadcast State模式

提供的API

通过一个例子展示Broadcast State提供的接口。假设存在一个序列,序列红中的元素具有不同颜色和形状的图形,我们希望在相同的颜色的图形中找到具有一定顺序模式的图形对(比如在红色图形里,有一个长方形和一个三角形)。同时,寻找的模式也会随着时间而改变。

在这个模式中,我们定义两个流,一个流包含图形(item),具有颜色和形状,另一个流是规则(rule)代表希望寻找的模式。

在图形流中,首先按照颜色将流进行分区(keyby),这确保相同颜色图形会分发到同一个物理机上。

KeyedStream<Iter, color> colorPartitionStream = itemStream.Keyby(new KeySelector<Item, color>(){...});

在规则流中,它应该被广播到所有的的下游task中,下游task应该按照这些规则并根据它寻找符合规则的图形对,接下来两个任务
1.将规则广播到所有的下游task中
2.使用MapStateDescriptor来描述创建broadcast state在下游的存储结构中

// 一个map descriptor,它描述了规则名称以及存储map的存储结构
/**
     * 创建一个新的MapStateDescriptor,指定名称和类型信息
     *
     * @param name MapStateDescriptor的名称.
     * @param keyTypeInfo 状态key的类型信息
     * @param valueTypeInfo 状态中值的类型信息
    public MapStateDescriptor(
            String name, TypeInformation<UK> keyTypeInfo, TypeInformation<UV> valueTypeInfo) {
        super(name, new MapTypeInfo<>(keyTypeInfo, valueTypeInfo), null);
*/
MapStateDescriptor<String, Rule> ruleStateDescriptor = new MapStateDescriptor<>(
	'RulesBroadcastState',
	BasicTypeInfo.STRING_TYPE_INFO,
	TypeInformation.of(new TypeHInt<rule>(){}));

// 广播流,将规则广播出去并创建broadcast state
BroadcastStream<Rule> ruleBroadcastStream = ruleStream.broadcast(ruleStateDescriptor);

为了使用规则筛选图形中的序列:
1.将两个流关联起来
2.完成模式识别逻辑
为了关联一个广播流(BroadcastStream)和一个非广播流(keyed或者none-keyed),我们可以调用非广播流中的方法connect(),并将BroadcastStream当做参数传入。这个方法返回的参数是BroadcastConnectStream,具有类型方法process(),传入一个特殊的CoprocessorFunction来书写我们的模式逻辑。具体传入的process()属于哪个类型取决于非广播流的类型:

  • 如果流是一个keyed流,那就是KeyedBroadcastProcessFunction类型
  • 如果流式non-keyed流,那就是BroadcastprocessFunction

connect()方法需要由非广播流来进行调用,BroadcastStream作为参数传入。

DataStream<String> output = colorPartitionStream.connect(ruleBroadcastStream )
                                                .process(
                                                	// KeyedBroadcasrProcessFunction中类型参数表示;
                                                	// 1.key/stream中的类型
                                                	// 2.非广播流中元素类型
                                                	// 3.广播流中元素类型
                                                	// 4.结果的类型,在这里是String
                                                	new KeyedBroadcastProcessFunction<Color, Item, Rule, String>
												)

BroadcastProcessFunction和KeyedBroadcastProcessFunction

在传入的BroadcastProcessFunction或者keyedBroadcastProcessFunction中,我们需要实现两个方法。processBroadcastElement()方法负责处理广播流中的元素。peocessElement()负责处理非广播流中的元素。两个子类型定义如下:

public abstract class BroadcastProcessFunction<IN1, IN2, OUT> extends BaseBroadcastProcessFunction{
	public abstract void processElement(IN1 value, ReadOnlyContext ctx,Collectoe Collector<OUT> out) throws Exception;
	public abstract void processBroadElement(IN2 value, Context ctx, Collector<OUT> out) throws Exception;
}
public abstract class KeyedBroadcastProcessFunction<KS, IN1, IN2, OUT>{
	public abstract void processElement(IN1 value, ReadOnlyCOntext ctx, Collector<OUT> out) throws Exception;
	public abstract void processBroadElement(IN1 value, Context ctx, Collector<OUT> out) throws Exception;
	public void oneTimer(long timestamp, OnTimerContext ctx, Collector<OUT> out) throws Exception;
}

需要注意的是processBroadcastElement()负责处理广播流中的元素,而peocessElement()负责另一个流的元素。两个方法第二个参数不同,均有一下的方法:

1.得到广播流中的存贮状态:ctx.getBroadcastState(MapStateDescriptor<K, V>stateDesccriptor)
2.查询元素的时间戳:ctx.timestamp()
3.查询目前的watermark:ctx.currentWatermark()
4.目前的处理时间(processing time):ctx.currentProcessingTime()
5.产生旁路的输出:ctx.output(OutputTag outputtag, X value)

在getBroadcastState()方法中传入stateDescriptor应该与调用的.broadcast(ruleStateDescriptor)的参数相同。
这两个方法的区别在于broadcast State的访问权限不同。在处理广播流元素这端,是具有读写权限的,而对于处理非广播流元素这端是只读的。这样做的原因是,Flink中是不存在跨task通讯的。为了保证broadcast state在所有的并发实例中是一致的,我们在处理广播流元素的时候给予写的权限,在所有task中都可以看到这些元素,并且要求对这些处理的元素是一致的,那么最终所有broadcast state是一致的。
processBroadcastElement()的实现必须在所有的并发实例中具有确定性的结果。
同时,KeyedBroadcastProcessFunction在Keyed Stream上工作,所以它提供了一些BroadcastProcessFunction没有的功能:

1.processElement()的参数ReadOnlyContext提供了方法能够访问Flink的定时器任务,可以注册事件定时器(event-time timer)或者处理时间的定时器(processing-time timer).。当定时器触发时,会调用onTimer()方法,提供了OnTimerContext,它具有ReadOnlyContext的全部功能,并且提供:

  • 查询当前触发的是一个事件还是定时器。
  • 查询定时器关联的key
    2.processBroadcastElement()方法中的参数Conetxt会提供方法applyTokeyedState(StateDescriptor<S, VS>)stateDescriptor对应的state中所有key的存储状态进行某些操作。

注册一个定时器只能在KeyedBroadcastFunction的processElement()方法中进行。在processBroadcastElement()方法中不能注册定时器,因为广播中并没有关联的key。

例子中,实现KeyedBroadcastProcessFunction如下:

// 存储部分匹配的结果,即匹配了一个元素,正在等待第二个元素
// 我们用一个数组来存储,因为可能同时有很多个元素正在等待
new KeyedBroadcastProcessFunction() {
	private final MapStateDescriptor<String, List<Item>> mapStateDesc = new MapStateDescriptor<>(
	'items',
	BasicTypeInfo.STRING_TYPE_INFO,
	new ListTypeInfo<>(Item.class));

// 与之前的ruleStateDescriptor相同
private final MapStateDescriptor<String, Rule> ruleStateDescriptor = new MapStateDescriptor<>(
	'RuleBroadcastState',
	BasicTypeInfo.STRING_TYPE_NIFO,
	TypeInfomation.of(new TypeHint<Rule>() {}));

@override
public void processElement(Item value,
						   ReadOnlyContext ctx,
						   Collector<String> out throws Exception{
					
							final MapState<String, LIst<Item>> state = getRuntimeContext().getMapState(MapStateDesc);
							final Shape shape = value.getShape();
							for (Map.Entry<String, Rule> entry:
								ctx:getBroadcastState(ruleStateDescriptor).immutableEntries){
									final String ruleName = entry.getBy();
									final Rule rule = entry.getvalue();
									List<Item> stored = state.get(ruleName);
									if(stored == null){
										stoed = new ArrayList<>();	
									}
									if (shape == rule.second && !stored.isEmpty()){
										for (Item i : stored){
											out.collect('MATCH:' + i + " - " + vlaue);
											// 不需要额外的 else{} 段来考虑 rule.first == rule.second 的情况
                                    if (shape.equals(rule.first)) {
                                    	stored.add(value);
                                    }
    
                                   if (stored.isEmpty()) {
                                   	state.remove(ruleName);
                                   } else {
                                   	state.put(ruleName, stored);
										}
									}
								} 
						   }

重要注意事项

在使用broadcast state的要时刻注意以下事项:

  • 没有跨task的通讯:如上所述,这就是为什么只有在(keyed)-BroadcastFunction中处理广播流元素的方法里可以更改broadcast state的内容。同时,用户需要保证所有task对于broad state的处理方式是一致的,否则会造成不同的task读取broad state时内容不一致的情况,最终导致结果不一致。
  • broadcast state在不同的task时间顺序可能是不同的,虽然广播流中元素的过程能够保证所有的下游 task 全部能够收到,但在不同 task 中元素的到达顺序可能不同。 所以 broadcast state 的更新不能依赖于流中元素到达的顺序。
  • 所有的 task 均会对 broadcast state 进行 checkpoint:虽然所有 task 中的 broadcast state 是一致的,但当 checkpoint 来临时所有 task 均会对 broadcast state 做 checkpoint。 这个设计是为了防止在作业恢复后读文件造成的文件热点。当然这种方式会造成 checkpoint 一定程度的写放大,放大倍数为 p(=并行度)。Flink 会保证在恢复状态/改变并发的时候数据没有重复且没有缺失。 在作业恢复时,如果与之前具有相同或更小的并发度,所有的 task 读取之前已经 checkpoint 过的 state。在增大并发的情况下,task 会读取本身的 state,多出来的并发(p_new - p_old)会使用轮询调度算法读取之前 task 的 state。
    不使用 RocksDB state backend: broadcast state 在运行时保存在内存中,需要保证内存充足。这一特性同样适用于所有其他 Operator State。