Storm-Trident State

《从零开始学Storm》
《Storm实战构建大数据实时计算》
apachecn/storm-doc-zh

Trident State 概述

Trident在读取写入状态源方面有着非常好的抽象。这些状态可以是:

  • 拓扑内部 :如保存在内存中并由 HDFS 支持
  • 外部存储 :在像 Memcached 或者 Cassandra 这样的数据库中

这两种方式,对于而言Trident API是没有区别的.
Trident以容错的方式来管理状态,当遇重试和失败时状态的更新是幂等的。这种方式使得Trident拓扑对每一个消息处理每个消息都被exactly-once(精确处理一次)


Trident 只处理一次语义(exactly-once)
下面通过一个例子,来介绍exactly-once的必要性。
需求假设你正在对一个流进行计数处理,同时把计数结果保存到数据库。
初始方案我们可以在数据库中用一个值来表示这个计数,然后没处理一个Tuple,就将数据库存储的值+1.
但是当错误发生时,Tuple会被重新被处理,此时会引发出一个问题:在进行状态更新时,你完全不知道是否已经成功处理过这个Tuple,可能会出现以下几种情况:

  • a.之前没有处理过这个tuple。这种情况,那么需要把计数+1
  • b.之前已经处理过这个tuple,且已经把计数+1了,在后续环节出错。这种情况无需+1
  • c.之前处理过这个tuple,但是在更新数据库时出错。这种情况,应该更新数据库。

由于可能会出现如上的一些问题,可以看出数据库中只存一个计数是无法区分tuple是否已经被正确处理过的,需要更多的信息来支持。

Trident通过提供如下语义来实现exactly-once:

  • tuples是被分成一组组小的集合(batch)来处理的。
  • 每个batch会被分配一个唯一的id(即事务id,txid),当batch被重新处理时,txid不变。
  • batch之前的状态更新是严格有序的。即batch3必须在batch2完成之后才可以进行状态更新。

使用这些原语,在状态更新时就可以检测到该 batch 的 tuples 是否已被处理, 并采取适当的操作以一致的方式更新状态. 您所采取的操作取决于您的Spout提供的确切语义,有三种容错类型的Spout:

  • 非事务(non-transactional) Spout
  • 事务(transactional) Spout
  • 不透明事务(opaque transactional) Spout

同样有三种容错状态State:

  • 非事务(non-transactional)
  • 事务(transactional) Spout
  • 不透明事务(opaque transactional)

事务(transactional) Spout

Trident是以batch的方式来处理tuple的,每个batch会被分配一个唯一的transaction id(事务ID).spouts 的属性根据他们可以提供的每 batch 中的内容的 保证而有所不同. transactional spout 具有以下属性:

  • 一个batch无论重发多少次,只有一个唯一且不变的事务id,同时它包含的tuple是完全一致的。
  • tuple在batch之间没有重叠,即一个tuple最多只能属于一个batch

例:storm-contrib 具有 一个transactional spout 的实现 针对于 Kafka .
TransactionalTridentKafkaSpout

Transactional Spout可能带来的问题
Transactional Spout简单易懂,为什么不只使用事务Spout,而需要支持其他类型的Spout呢?那是因为在一些极端的情况下,事务Spout可能会存在一些问题。

假如有一个batch tuple在bolt消费的过程中失败了,需要spout重发,这时刚好消息发送中间件故障(节点宕机或者订阅对应的分区无法访问),spout为了保证每个batch tuple的一致性,就只能等待消息中间件恢复,整个处理流程就会卡住。
这就是为什么需要不透明事务spout和非事务spout的原因。

Transactional Spout处理流程(例)
需求:设计一个Topology,统计单词出现的次数,并以KV方式存储在数据库中。key就是单词,value就是单词出现的次数.
方案:根据前面的说明,我们已经知道仅将 count 存储为 value 不足以知道是否已经处理了该batch tuple,我们需要将batch的Transaction id也作为value的一部分存储在数据库中。当更新count时,首先比较当前batch的Transaction id与数据库里的Transaction id进行对比。如果一样则忽略,如果不一致则执行更新操作。Trident 可确保 state updates 跟随 batches 的顺序.
假如有batch tuple,它的txid=3 :

["man"]
["man"]
["dog"]

数据库保存了如下信息:

man => [count=3, txid=1]
dog => [count=4, txid=3]
apple => [count=10, txid=2]
  • 单词“man” 对应的txid是1,当前txid=3,说明这个batch中的tuple没有被处理过,所以将txid更新为3,count更新为3+2=5
  • 单词“dog”对应的txid,与当前txid一致忽略更新
  • 单词“apple”未出现,则不变。

更新后数据如下:

man => [count=5, txid=3]
dog => [count=4, txid=3]
apple => [count=10, txid=2]


不透明事务(paque transactional) Spout

Opaque transactional spouts并不能保证一个txid 的batch tuple保持不变,它具有如下属性:tuple只在一个batch中被成功处理,但是如果tuple在一个batch处理失败后,可能会在另一个batch中被处理。也就是说,某个tuple可能第一次在txid=2的batch处理出现,以后也有可能在txid=4的batch中再次出现。

Opaque transactional spouts具有更好的容错性,但是需要额外的“存储空间”。除了value和transaction-id,你还需要在数据库中存储之前的数据(preValue)。
:我们再看看上述单词计数的例子,假如当前数据库中有如下存储信息:

man ==> {
	value = 4
	preValue = 1
	txid = 2
}

接收到下一个batch的transaction-id有如下两种情况(由于trident的保证batch的强顺序性,不可能有第三种情况“小于”):
情景1
下一个batch的txid不等于数据库中记录的txid,如:

batch(txid = 3)
["man"]
["man"]
["dog"]

此时说明batch(txid=2)已经被正确处理,需要将数据库记录中txid更新为3,preValue更改为当前value值4,当前value值更新为4+2=6,结果如下:

 man {
	value = 4 + 2 = 6
	preValue = 4
	txid = 3
}

情景2
下一个batch的txid等于数据库中记录的txid=2,如:

batch(txid =2)
["man"]
["man"]
["dog"]

此时说明了,前一次transaction id对应的batch已经发生变化即上一次的变化发生失败,需要重新更新,我们需要忽略上一次的更新。此种情况下我们只需将value更新为preValue+本次提交的值2即 1 + 2= 3即可,结果如下:

 man {
	value = 1 +2 =3
	preValue = 1
	txid = 2
}

由于 opaque transactional spouts 保证批次之间不 overlap (重叠) - 每个元组都被一个批次成功处理 - 可以根据先前的 value 安全地进行更新.



非事务(non-transactional) Spout

Non-transactional spouts 不对batch中的tuple提供任何保证.
-如果batch处理失败后tuple不重发,那么tuple可能至多处理一次
如果失败后tuple重发,那么可能处理超过一次,即至少处理一次



Spout和State之间的联系

下图显示了 spouts / states 的哪些组合可以实现一次消息传递语义:
在这里插入图片描述

  • 不透明事务状态具有最强的容错能力, 但这需要以 txid 和两个 values 存储在数据库中为代价.
  • 事务状态在数据库中存储较少的状态,但是仅能与事务Spout协同工作。
  • 非事务状态在数据库中存储最少的状态,但是无法实现处理一次语义(exactly-once)


State APIs

实现恰好一次(exact-once)语义是比较复杂的一件事,需要存储较多的状态与实务id。Trident在State中封装了所有的容错逻辑,
作为用户,你无需处理比较实务id、在数据库中存储多个值或类似的事情。只需要简单的编写代码即可,如:

TridentTopology topology = new TridentTopology();        
TridentState wordCounts =
      topology.newStream("spout1", spout)
        .each(new Fields("sentence"), new Split(), new Fields("word"))
        .groupBy(new Fields("word"))
        .persistentAggregate(MemcachedState.opaque(serverLocations), new Count(), new Fields("count"))                
        .parallelismHint(6);

所有管理不透明事务状态所需要的逻辑MemcachedState.opaque内处理,此外它自动以batch批量更新来减少访问数据库的次数。

STATE 接口
基本 State interface (状态接口)只有两种方法:

//org.apache.storm.trident.state.State
public interface State {
    //当状态更新开始时, 被调用
    // can be null for things like partitionPersist occuring off a DRPC stream
    void beginCommit(Long txid); 
    
    //当状态更新结束时,被调用
    void commit(Long txid);
}

Trident State 的查询和更新
Trident 提供 :

  • QueryFunction接口 , 用于编写查询State源的Trident 操作
  • StateUpdater 接口, 用于编写更新State源的Trident 操作
//org.apache.storm.trident.Stream

 public Stream stateQuery(
	 	TridentState state, 
	 	Fields inputFields, 
	 	QueryFunction function, 
	 	Fields functionFields
 	) {};
 	
 public TridentState partitionPersist(
		 StateFactory stateFactory, 
		 Fields inputFields, 
		 StateUpdater updater, 
	 	Fields functionFields
	 	) {};

QueryFunction
:假设有一个存储用户位置的信息(userid : location)的本地数据库,并且希望使用Trident访问它。

  • a.你的State实现类会有用于获取和设置用户位置信息的方法:
public class LocationDB implements State {
	@Override
	public void beginCommit(Long txid) {
	}

	@Override
	public void commit(Long txid) {
	}

	public void setLoacation(Long userId,String location){
		//访问数据库:更新用户位置信息。
	}

	public String getLocation(Long userId){
		//访问数据库: 查询userId对应的位置信息
		return null;
	}
}
  • b.您可以向 Trident 提供一个 StateFactory,用于在 Trident任务中创建 State 对象的实例。
public class LocationDBFactory implements StateFactory {
	@Override
	public State makeState(Map conf, IMetricsContext metrics, int partitionIndex, int numPartitions) {
		return new LocationDB();
	}
}
  • c.定义topology,用来查询用户位置信息
public static void main(String[] args) {
		ITridentSpout spout = buildSpout();
		TridentTopology topology = new TridentTopology();
		TridentState locations = topology.newStaticState(new LocationDBFactory());

		topology.newStream("myspout", spout)
				.stateQuery(locations, new Fields("userid"), new QueryLocation(), new Fields("location"));
	}
  • d.1 定义QueryFunction接口的实现类:QueryLocation
public class QueryLocation extends BaseQueryFunction<LocationDB, String> {

	@Override
	public List<String> batchRetrieve(LocationDB state, List<TridentTuple> inputs) {
		List<String> ret = new ArrayList();
		for(TridentTuple input: inputs) {
			Long userId = input.getLongByField("userid");
			ret.add(state.getLocation(userId));
		}
		return ret;
	}

	@Override
	public void execute(TridentTuple tuple, String location, TridentCollector collector) {
		collector.emit(new Values(location));
	}
}
  • d.2 QueryFunction 分两个步骤执行.

    • 首先, Trident 将一批读取合并在一起, 并将它们传递给 batchRetrieve 。
    • batchRetrieve 将接收多个userids,并将接收多个userids对应的location依次查询出来,并返回。
  • d.3 QueryFunction这个代码没有利用 Trident 的批处理, 因为它只是一次查询一个 LocationDB . 所以写一个更好的方法来编写 LocationDB 就是这样的:

public class LocationDB implements State {
	//省略其他代码....
	
	public void setLocationsBulk(List<Long> userIds, List<String> locations) {
		// 批量设置用户位置信息
	}

	public List<String> bulkGetLocations(List<Long> userIds) {
		// 批量查询
		return null;
	}
}

QueryFunction中直接调用state.bulkGetLocations(userIds);,从而减少到数据库的 访问次数, 此代码将更加高效.。

StateUpdater
要更新State,可以使用StateUpdater接口 ,可以通过下例LocationUpdater来更新用户的位置信息:

public class LocationUpdater extends BaseStateUpdater<LocationDB> {
	@Override
	public void updateState(LocationDB state, List<TridentTuple> tuples, TridentCollector collector) {
		List<Long> ids = new ArrayList<Long>();
		List<String> locations = new ArrayList<String>();
		for(TridentTuple t: tuples) {
			ids.add(t.getLong(0));
			locations.add(t.getString(1));
		}
		//批量更新用户位置信息
		state.setLocationsBulk(ids, locations);
	}
}

以下是在TridentTopology中使用此操作的方法:

TridentState state =
				topology.newStream("locations", spout)
						.partitionPersist(new LocationDBFactory(), new Fields("userid", "location"), new LocationUpdater())
	
  • partitionPersist 操作更新 State. StateUpdater 收到该 State 和一批具有该 State 更新的元组. 该代码只是从输入元组中获取用户名和位置, 并将批量集合放入 States .
  • partitionPersist 返回表示由 TridentTopology 更新的位置数据块的 TridentState 对象, 然后, 您可以在 topology 中的其他地方的 stateQuery 操作中使用此 state .
  • TridentCollector作为输入对象传递给了StateUpdaters ,元组被发送到这个采集器,并会转发到 “new values stream”,并可以通过TridentState .newValuesStream()访问该流,并进一步处理。

persistentAggregate
还有一种更新的方法叫做persistentAggregate,如下:

TridentTopology topology = new TridentTopology();        
TridentState wordCounts =
      topology.newStream("spout1", spout)
        .each(new Fields("sentence"), new Split(), new Fields("word"))
        .groupBy(new Fields("word"))
        .persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count"))

persistentAggregate 是在 partitionPersist 之上的另外一层抽象,它知道怎么去使用一个 Trident aggregator (Trident 聚合器)来更新 State .在这个例子当中, 因为这是一个 grouped stream (分组流):
-Trident 会期待你提供的 state 是实现了 "MapState" 接口的.
-用来进行 group 的字段会以 key 的形式存在于 State 当中
-聚合后的结果会以 value 的形式存储在 State

MapState接口

//org.apache.storm.trident.state.map.MapState
public interface MapState<T> extends ReadOnlyMapState<T> {
	//此方法继承自ReadOnlyMapState
	 List<T> multiGet(List<List<Object>> keys);
	 
    List<T> multiUpdate(List<List<Object>> keys, List<ValueUpdater> updaters);
    void multiPut(List<List<Object>> keys, List<T> vals);
}

Snapshottable
当你在一个 non-grouped streams 上面进行 aggregations (聚合)的话, Trident 会期待你的 State 对象实现 “Snapshottable” 接口:

//org.apache.storm.trident.state.snapshot.Snapshottable
public interface Snapshottable<T> extends ReadOnlySnapshottable<T> {
	//此方法继承自ReadOnlySnapshottable
 	T get();    
 
    T update(ValueUpdater updater);
    void set(T o);
}

实现MapState
在 Trident 中实现 MapState 是非常简单的, 它几乎帮你做了所有的事情. OpaqueMap , TransactionalMap , 和 NonTransactionalMap 类实现了所有相关的逻辑, 包括容错的逻辑。你只需要将一个知道如何执行相应 key/values 的 multiGet 和 multiPuts 的 IBackingMap 的实现提供给这些类就可以了. IBackingMap 接口看上去如下所示:

public interface IBackingMap<T> {
    List<T> multiGet(List<List<Object>> keys); 
    void multiPut(List<List<Object>> keys, List<T> vals); 
}
  • paqueMap 会用 OpaqueValue 的 value 来调用 multiPut 方法,
  • TransactionalMap 会提供 TransactionalValue 中的 value
  • NonTransactionalMaps 只是简单的把从 Topology 获取的 object 传递给 multiPut .

Trident 还提供了一种 CachedMap 类来进行自动的LRU cache (缓存) map key/vals .

最后, Trident 提供了 SnapshottableMap 类, 通过将 global aggregations (全局聚合)存储到 fixed key (固定密钥)中将一个 MapState 转换成一个 Snapshottable 对象.

猜你喜欢

转载自blog.csdn.net/it_freshman/article/details/82981934