《从零开始学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 对象.