核心概念
Trident在读写有状态的数据源方面是有着一流的抽象封装;状态即可以保留在topology的内部,如内存(但易丢失,服务器重启后不可用),也可以放到外部存储当中,如HDFS,Memcached(内存级数据库)或no-sql数据库(如Hbase)。这些都是使用同一套Trident API。
Trident以一种容错的方式来管理状态(状态指结果数据),以至于当你在更新状态的时候你不需要去考虑错误以及重试的情况。这种保证每个消息被处理有且仅有一次的原理会让你更放心的使用Trident的topology。
回顾之前讲解事务章节时:
Spout类型 | DB中存储 |
---|---|
普通Spout | [count = 3] |
事务Spout | [txid = 1, count = 3] |
不透明事务Spout | [txid = 2, value = 3, prevValue = 1] |
核心概念state及API
Opaque transactional state有着最为强大的兼容性,但是这是以存储更多的信息作为代价的。transactional states需要存储较少的状态信息,但是仅能和transactional spouts协同工作。最后,non-transactional state所需要存储的信息最少,但是却不能实现有且仅有一次被成功处理的语义。
state和spout类型的选择其实是一种在容错性和存储消耗之间的权衡,根据应用的需要会进行选择。
State API
Trident把所有容错相关的逻辑都放在了state里面。作为一个用户,你并不需要自己去处理复杂的txid,存储多余的信息到数据库中,或者是任何其他类似的事情。
TridentTopology topology = new TridentTopology();
TridentState wordCounts = topology.newStream("spout1", spout)
.parallelismHint(16)
.each(new Fields("sentence"), new Split(), new Fields("word"))
.groupBy(new Fields("word"))
.persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count"))
.parallelismHint(16);
所有管理state所需的逻辑都在MemoryMapState中封装好了,数据库的更新会自动以batch的形式来进行,以避免多次访问数据库。
state的基本接口只包含下面两个方法:
public interface State {
void beginCommit(Long txid);
void commit(Long txid);
}
如何开发一个自己的state?
假如用数据库来存储用户Location,并且你想要在Trident中去访问这个数据。你的state的实现应该有用户的set、get方法。
import storm.trident.state.State;
public class LocationDB implements State {
@Override
public void beginCommit(Long txid) {
// TODO Auto-generated method stub
}
@Override
public void commit(Long txid) {
// TODO Auto-generated method stub
}
public void setLocation(Long userId, String location) {
// code to access database and set location
}
public String getLocation(Long userId) {
// code to get location from database
}
}
然后你还需要提供给Trident一个StateFactory来在Trident中创建你的state对象。
StateFactory可以如下:
import storm.trident.state.StateFactory;
public class LocationDBFactory implements StateFactory {
@Override
public State makeState(Map conf, IMetricsContext metrics, int partitionIndex, int numPartitions) {
return new LocationDB();
}
}
如何在State上查询和更新
Trident提供了一个类似QueryFunction接口用来实现Trident中在一个source state上查询的功能,如MapGet类。还提供了StateUpdater来实现Trident中更新source state的功能。
例如:写一个查询地址的操作,来查询LocationDB找到用户的Location。
假定这个topology会接受一个用户id作为输入流,如:
TridentTopology topology = new TridentTopology();
TridentState locations = topology.newStaticState(new LocationDBFactory());
topology.newStream("myspout", spout)
.stateQuery(locations, new Fields("userId"), new QueryLocation(), new Fields("location"));
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) {
ret.add(state.getLocation(input.getLong(0)));
}
return ret;
}
@Override
public void execute(TridentTuple tuple, String location, TridentCollector collector) {
collector.emit(new Values(location));
}
}
QueryLocation的执行分为两个部分,batchRetrieve收集一个batch的read操作结果;execute方法负责把结果emit出去。
你可以看到,这段代码为每个输入tuple去查询了一次LocationDB,没利用batch优势,所以以一种更好的操作LocationDB应该是这样的:
public class LocationDB implements State {
@Override
public void beginCommit(Long txid) {
// TODO Auto-generated method stub
}
@Override
public void commit(Long txid) {
// TODO Auto-generated method stub
}
public void setLocationsBulk(List<Long> userIds, List<String> locations) {
// set locations in bulk
}
public String getLocationsBulk(List<Long> userIds) {
// get locations in bulk
return "";
}
}
StateUpdater接口
若要更新State,你需要使用StateUpdater接口。一个StateUpdater的例子用来将新的地址信息更新到LocationDB当中。
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);
}
}
如何在Trident topology中使用上面声明的LocationUpdater?
TridentTopology topology = new TridentTopology();
TridentState locations = topology.newStream("locations", locationsSpout)
.partitionPersist(new LocationDBFactory(), new Fields("userId", "location"), new LocationUpdater());
partitionPersist将会更新state。其内部是将state和一批更新的tuple交给LocationUpdater,由LocationUpdater完成相应的更新操作。
在这段代码中,只是简单的从输入的tuple中提取userId和对应的location,并一起更新到state中。
persistentAggregate持久化聚合
Trident有另外一种更新state的方法叫做persistentAggregate,在之前的word count例子中我们已经见过了。
persistentAggregate是在partitionPersist之上的另外一层抽象,它知道怎么去使用一个Trident聚合器来更新state。
在word count例子中,因为这是一个group好的stream,Trident会期待你提供的state是实现了MapState接口,用来进行group的字段以key的形式存在于state当中,聚合后的结果会以value的形式存储在state当中。
MapState接口如下所示:
import storm.trident.state.State;
import storm.trident.state.ValueUpdater;
public interface MapState<T> extends State{
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);
}
当你在一个未经过group的stream上面进行聚合的话,Trident会期待你的state实现Snapshottable接口:
import storm.trident.state.State;
import storm.trident.state.ValueUpdater;
public interface Snapshottable<T> extends State {
T get();
T update(ValueUpdater updater);
void set(T o);
}
如何实现MapState?
在Trident中实现MapState是非常简单的,它几乎帮你做了所有的事情。
OpaqueMap,TransactionalMap,NonTransactionalMap类实现了所有相关的逻辑,包括容错的逻辑。你只需要将一个IBackingMap的实现提供给这些类就可以了。IBackingMap接口如下所示:
public interface IBackingMap<T> {
List<T> multiGet(List<List<Object>> keys);
void multiPut(List<List<Object>> keys, List<T> vals);
}
OpaqueMap会用OpaqueValue的value来调用multiPut方法,TransactionalMap's会提供 TransactionalValue中的value,而NonTransactionalMap只是简单的把从Topology获取的object传递给multiPut。