@(博客文章)[storm|大数据]
Trident及State的原理请见另一篇文章:http://blog.csdn.net/lujinhong2/article/details/47132305
trident通过spout的事务性与state的事务处理,保证了恰好一次的语义。这里介绍了如何使用state。
完整代码请见 https://github.com/lujinhong/tridentdemo
主类定义了拓扑的整体逻辑,这个拓扑通过一个固定的spout循环产生数据,然后统计消息中每个名字出现的次数。
拓扑中先将消息中的内容提取出来成name, age, title, tel4个field,然后通过project只保留name字段供统计,接着按照name分区后,为每个分区进行聚合,最后将聚合结果通过state写入map中。
storm.trident.Stream Origin_Stream = topology
.newStream("tridentStateDemoId", spout)
.parallelismHint(3)
.shuffle()
.parallelismHint(3)
.each(new Fields("msg"), new Splitfield(),
new Fields("name", "age", "title", "tel"))
.parallelismHint(3)
.project(new Fields("name")) //其实没什么必要,上面就不需要发射BCD字段,但可以示范一下project的用法
.parallelismHint(3)
.partitionBy(new Fields("name")); //根据name的值作分区
Origin_Stream.partitionAggregate(new Fields("name"), new NameCountAggregator(),
new Fields("nameSumKey", "nameSumValue")).partitionPersist(
new NameSumStateFactory(), new Fields("nameSumKey", "nameSumValue"),
new NameSumUpdater());
这里涉及了一些trident常用的API,但project等相对容易理解,这里只介绍partitionAggregate的用法。
再看看上面代码中对partitionAggregate的使用:
Origin_Stream.partitionAggregate(new Fields("name"), new NameCountAggregator(),
new Fields("nameSumKey", "nameSumValue"))
第一,三个参数分别表示输入流的名称与输出流的名称。中间的NameCountAggregator是一个Aggregator的对象,它定义了如何对输入流进行聚合。我们看一下它的代码:
public class NameCountAggregator implements Aggregator<Map<String, Integer>> {
private static final long serialVersionUID = -5141558506999420908L;
@Override
public Map<String, Integer> init(Object batchId,TridentCollector collector) {
return new HashMap<String, Integer>();
}
//判断某个名字是否已经存在于map中,若无,则put,若有,则递增
@Override
public void aggregate(Map<String, Integer> map,TridentTuple tuple, TridentCollector collector) {
String key=tuple.getString(0);
if(map.containsKey(key)){
Integer tmp=map.get(key);
map.put(key, ++tmp);
}else{
map.put(key, 1);
}
}
//将聚合后的结果emit出去
@Override
public void complete(Map<String, Integer> map,TridentCollector collector) {
if (map.size() > 0) {
for(Entry<String, Integer> entry : map.entrySet()){
System.out.println("Thread.id="+Thread.currentThread().getId()+"|"+entry.getKey()+"|"+entry.getValue());
collector.emit(new Values(entry.getKey(),entry.getValue()));
}
map.clear();
}
}
@Override
public void prepare(Map conf, TridentOperationContext context) {
}
@Override
public void cleanup() {
}
}
它实现了Aggregator接口,这个接口有3个方法:
public interface Aggregator<T> extends Operation {
T init(Object batchId, TridentCollector collector);
void aggregate(T val, TridentTuple tuple, TridentCollector collector);
void complete(T val, TridentCollector collector);
}
init方法:在处理batch之前被调用。init的返回值是一个表示聚合状态的对象,该对象会被传递到aggregate和complete方法。
aggregate方法:为每个在batch分区的输入元组所调用,更新状态
complete方法:当batch分区的所有元组已经被aggregate方法处理完后被调用。
除了实现Aggregator接口,还可以实现ReducerAggregator或者CombinerAggregator,它们使用更方便。详见《从零开始学storm》或者官方文档
https://storm.apache.org/documentation/Trident-API-Overview.html
下面我们看一下这3个方法的实现。
@Override
public Map<String, Integer> init(Object batchId,TridentCollector collector) {
return new HashMap<String, Integer>();
}
仅初始化了一个HashMap对象,这个对象会作为参数传给aggregate和complete方法。对一个batch只执行一次。
aggregate方法对于batch内的每一个tuple均执行一次。这里将这个batch内的名字出现的次数放到init方法所初始化的map中。
@Override
public void aggregate(Map<String, Integer> map,TridentTuple tuple, TridentCollector collector) {
String key=tuple.getString(0);
if(map.containsKey(key)){
Integer tmp=map.get(key);
map.put(key, ++tmp);
}else{
map.put(key, 1);
}
}
这里在complete将aggregate处理完的结果发送出去,实际上可以在任何地方emit,比如在aggregate里面。
这个方法对于一个batch也只执行一次。
@Override
public void complete(Map<String, Integer> map,TridentCollector collector) {
if (map.size() > 0) {
for(Entry<String, Integer> entry : map.entrySet()){
System.out.println("Thread.id="+Thread.currentThread().getId()+"|"+entry.getKey()+"|"+entry.getValue());
collector.emit(new Values(entry.getKey(),entry.getValue()));
}
map.clear();
}
}
先看一下主类中如何将结果写入state:
partitionPersist(
new NameSumStateFactory(), new Fields("nameSumKey", "nameSumValue"),
new NameSumUpdater());
它的定义为:
TridentState storm.trident.Stream.partitionPersist(StateFactory stateFactory, Fields inputFields, StateUpdater updater)
其中的第二个参数比较容易理解,就是输入流的名称,这里是名字与它出现的个数。下面先看一下Facotry。
很简单,它实现了StateFactory,只有一个方法makeState,返回一个State类型的对象。
public class NameSumStateFactory implements StateFactory {
private static final long serialVersionUID = 8753337648320982637L;
@Override
public State makeState(Map arg0, IMetricsContext arg1, int arg2, int arg3) {
return new NameSumState();
}
}
这个类继承自BaseStateUpdater,它的updateState对batch的内容进行处理,这里是将batch的内容放到一个map中,然后调用setBulk方法
public class NameSumUpdater extends BaseStateUpdater<NameSumState> {
private static final long serialVersionUID = -6108745529419385248L;
public void updateState(NameSumState state, List<TridentTuple> tuples, TridentCollector collector) {
Map<String,Integer> map=new HashMap<String,Integer>();
for(TridentTuple t: tuples) {
map.put(t.getString(0), t.getInteger(1));
}
state.setBulk(map);
}
}
这是state最核心的类,它实现了大部分的逻辑。NameSumState实现了State接口:
public interface State {
void beginCommit(Long txid);
void commit(Long txid);
}
分别在提交之前与提交成功的时候调用,在这里只打印了一些信息。
另外NameSumState还定义了如何处理NameSumUpdater传递的消息:
public void setBulk(Map<String, Integer> map) {
// 将新到的tuple累加至map中
for (Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
if (this.map.containsKey(key)) {
this.map.put(key, this.map.get(key) + map.get(key));
} else {
this.map.put(key, entry.getValue());
}
}
System.out.println("-------");
// 将map中的当前状态打印出来。
for (Entry<String, Integer> entry : this.map.entrySet()) {
String Key = entry.getKey();
Integer Value = entry.getValue();
System.out.println(Key + "|" + Value);
}
}
即将NameSumUpdater传送过来的内容写入一个HashMap中,并打印出来。
此处将state记录在一个HashMap中,如果需要记录在其它地方,如mysql,则使用jdbc写入mysql代替下面的map操作即可。
事实上,这个操作不一定要在state中执行,可以在任何类中,但建议还是在state类中实现。
(1)使用state,你不再需要比较事务id,在数据库中同时写入多个值等内容,而是专注于你的逻辑实现
(2)除了实现State接口,更常用的是实现MapState接口,下次补充。
(3)在拓扑中指定了StateFactory,这个工厂类找到相应的State类。而Updater则每个批次均会调用它的方法。State中则定义了如何保存数据,这里将数据保存在内存中的一个HashMap,还可以保存在mysql, hbase等等。
(4)trident会自动比较txid的值,如果和当前一样,则不更改状态,如果是当前txid的下一个值,则更新状态。这种逻辑不需要用户处理。
(5)如果需要实现透明事务状态,则需要保存当前值与上一个值,在update的时候2个要同时处理。即逻辑由自己实现。在本例子中,大致思路是在NameSumState中创建2个HashMap,分别对应当前与上一个状态的值,而NameSumUpdater每次更新这2个Map。
Trident有另外一种更新State的方法叫做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 聚合器来更新State。在这个例子当中,因为这是一个group好的stream,Trident会期待你提供的state是实现了MapState接口的。用来进行group的字段会以key的形式存在于State当中,聚合后的结果会以value的形式存储在State当中。MapState接口看上去如下所示:
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接口:
public interface Snapshottable<T> extends State {
T get();
T update(ValueUpdater updater);
void set(T o);
}
MemoryMapState 和 MemcachedState 都实现了上面的2个接口。
在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’s会用OpaqueValue的value来调用multiPut方法,TransactionalMap’s会提供TransactionalValue中的value,而NonTransactionalMaps只是简单的把从Topology获取的object传递给multiPut。
Trident还提供了一种CachedMap类来进行自动的LRU cache。
另外,Trident 提供了 SnapshottableMap 类将一个MapState 转换成一个 Snapshottable 对象.
大家可以看看 MemcachedState的实现,从而学习一下怎样将这些工具组合在一起形成一个高性能的MapState实现。MemcachedState是允许大家选择使用opaque transactional, transactional, 还是 non-transactional 语义的。
完整代码请见 https://github.com/lujinhong/tridentdemo
在Trident中实现MapState是非常简单的,它和单纯的State不同点在于:OpaqueMap, TransactionalMap 和 NonTransactionalMap会实现相关的容错逻辑,只需为这些类提供一个IBackingMap接口实现,调用multiGet和multiPut方法访问各自的K/V值。
public interface IBackingMap<T> {
List<T> multiGet(List<List<Object>> keys);
void multiPut(List<List<Object>> keys, List<T> vals);
}
详细的步骤如下:
主要实现multiGet和multiPut的方法,实现如何从state中读写数据。
multiGet 的参数是一个List,可以根据key来查询数据,key本身也是一个List,以方便多个值组合成key的情形。
multiPut的参数是一个List类型的keys和一个List类型的values,它们的size应该是相等的,把这些值写入state中。
public class MemoryMapStateBacking<T> implements IBackingMap<T> {
Map<List<Object>, T> db = new HashMap<List<Object>, T>();
@Override
public List<T> multiGet(List<List<Object>> keys) {
List<T> ret = new ArrayList();
for (List<Object> key : keys) {
ret.add(db.get(key));
}
return ret;
}
@Override
public void multiPut(List<List<Object>> keys, List<T> vals) {
for (int i = 0; i < keys.size(); i++) {
List<Object> key = keys.get(i);
T val = vals.get(i);
db.put(key, val);
}
}
}
这里将k/v写入了一个HashMap中,如果需要写入mysql,则只需要使用jdbc,把db.put改为写入mysql即可,查询类似。
public class MemoryMapStateFacotry implements StateFactory{
@Override
public State makeState(Map conf, IMetricsContext metrics,
int partitionIndex, int numPartitions) {
return TransactionalMap.build((IBackingMap<TransactionalValue>) new MemoryMapStateBacking());
}
}
很简单,就返回一个实现了MapState接口的类对象,通过把上面定义的MemoryMapStateBacking对象传入TransactionalMap.build作参数即可。当然还可以使用:
NonTransactionalMap.build(state);
OpaqueMap.build(state);
//这个流程用于统计单词数据,结果将被保存在wordCounts中
TridentState wordCounts =
topology.newStream("spout1", spout)
.parallelismHint(16)
.each(new Fields("sentence"), new Split(), new Fields("word"))
.groupBy(new Fields("word"))
.persistentAggregate(new MemoryMapStateFacotry(), new Count(),
new Fields("count")).parallelismHint(16);
//这个流程用于查询上面的统计结果
topology.newDRPCStream("words", drpc)
.each(new Fields("args"), new Split(), new Fields("word"))
.groupBy(new Fields("word"))
.stateQuery(wordCounts, new Fields("word"), new MapGet(), new Fields("count"))
.each(new Fields("count"), new FilterNull())
.aggregate(new Fields("count"), new Sum(), new Fields("sum"));
return topology.build();
(1)创建一个实现IBackingMap的类,实现multiGet和multiPut方法
(2)创建实现StateFactory的类,它的makeState返回一个实现了MapState接口的对象,可以通过:
mapState = TransactionalMap.build(_iBacking);
其中_iBacking就是第一步实现类的对象。当然还可以使用
mapState = NonTransactionalMap.build(state);
mapState = OpaqueMap.build(state);
TransactionalMap,OpaqueMap, NonTransactionalMap已经通过判断txid的值实现了相应的事务逻辑,以TransactionalMap为例,它的源码中会判断batch中的txid与state中已经存储的是否相同,或者同的话则新值等于旧值即可:
if(_currTx!=null && _currTx.equals(val.getTxid()) && !retval.cached)
(3)在拓扑中使用persistentAggregate写入state
以事务型状态为例,我们看一下整个存储过程的逻辑:
* 首先,persistentAggregate收到一批数据,它的第一个参数返回的是事务型的MapState
* 然后,TransactionalMap在multiUpdate中会判断这个事务的txid与当前state中的txid是否一致。
* 如果txid一致的话,则保持原来的值即可,如果txid不一致,则更新数值。
* 如果更新数据呢?它是拿新来的值和state中的原有的值,使用persistentAggregate中第2个参数定义的类方法作聚合计算。
persistentAggregate的第2个参数定义了数据是如何更新的,而IBackingMap中的multiGet和multiPut只定义了如何向state中存取数据。
比如此处的Count,它会将将2个数据相加:
@Override
public Long combine(Long val1, Long val2) {
return val1 + val2;
}
因此新来的统计次数与原有的统计次数加起来即是新的总和。
而对于透明事务状态,不管txid是否一致,都需要修改state,同时将当前state保存一下,成为preState。非事务型就简单了,不管你来什么,我都直接更新。
当然,如果觉得TransactionalMap,OpaqueMap, NonTransactionalMap不能满足业务需求,则可以自定义一个实现了MapState接口的类,而不是直接使用它们。
反正这三个类的实现逻辑非常简单,当不能满足业务需要时,看一下源码,然后参考它创建自己的类即可,此时,关键是multiUpdate的实现。
key可以是一个很复杂的List,包括多个字段。