trident API指南

trident State应用指南

@(博客文章)[storm|大数据]

  • trident State应用指南
  • 一State基础示例
    • 1主类
    • 2Aggregator的用法
      • 1Aggregator接口
      • 2init方法
      • 3aggregate方法
      • 4complete方法
    • 3state的用法
      • 1拓扑定义
      • 2工厂类NameSumStateFactory
      • 3更新类NameSumUpdater
      • 4状态类NameSumState
    • 4state应用思路总结
  • 二MapState
    • 1persistentAggregate
    • 2MapStates
    • 3Demo
      • 1创建一个实现IBackingMap的类实现multiGet和multiPut方法
      • 2创建实现StateFactory的类
    • 3在拓扑中写入state或者查询state
    • 4关于MapState的总结
      • 1基本步骤
      • 2全流程逻辑
      • 3复杂的情况
      • 4其它思考

Trident及State的原理请见另一篇文章:http://blog.csdn.net/lujinhong2/article/details/47132305

一、State基础示例

trident通过spout的事务性与state的事务处理,保证了恰好一次的语义。这里介绍了如何使用state。

完整代码请见 https://github.com/lujinhong/tridentdemo

1、主类

主类定义了拓扑的整体逻辑,这个拓扑通过一个固定的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());

2、Aggregator的用法

这里涉及了一些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() {

    }

}

(1)Aggregator接口

它实现了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个方法的实现。

(2)init方法

@Override
public Map<String, Integer> init(Object batchId,TridentCollector collector) {
    return new HashMap<String, Integer>();
}

仅初始化了一个HashMap对象,这个对象会作为参数传给aggregate和complete方法。对一个batch只执行一次。

(3)aggregate方法

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);
    }
}

(4)complete方法

这里在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();
    } 
}

3、state的用法

(1)拓扑定义

先看一下主类中如何将结果写入state:

partitionPersist(
            new NameSumStateFactory(), new Fields("nameSumKey", "nameSumValue"),
            new NameSumUpdater());

它的定义为:

TridentState storm.trident.Stream.partitionPersist(StateFactory stateFactory, Fields inputFields, StateUpdater updater)

其中的第二个参数比较容易理解,就是输入流的名称,这里是名字与它出现的个数。下面先看一下Facotry。

(2)工厂类:NameSumStateFactory

很简单,它实现了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();  
    } 
}

(3)更新类:NameSumUpdater

这个类继承自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);
    }
}

(4)状态类:NameSumState

这是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类中实现。

4、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。

二、MapState

1、persistentAggregate

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个接口。

2、MapStates

在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 语义的。

3、Demo

完整代码请见 https://github.com/lujinhong/tridentdemo

  • 更详细的可以参考trident-memcached(很全面,但较旧)
    https://github.com/nathanmarz/trident-memcached
  • 或者storm-hbase的State实现等

在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); 
}

详细的步骤如下:

(1)创建一个实现IBackingMap的类,实现multiGet和multiPut方法

主要实现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即可,查询类似。

(2)创建实现StateFactory的类

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);

(3)在拓扑中写入state,或者查询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();

4、关于MapState的总结

(1)基本步骤

(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

(2)全流程逻辑

以事务型状态为例,我们看一下整个存储过程的逻辑:
* 首先,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。非事务型就简单了,不管你来什么,我都直接更新。

(3)复杂的情况

当然,如果觉得TransactionalMap,OpaqueMap, NonTransactionalMap不能满足业务需求,则可以自定义一个实现了MapState接口的类,而不是直接使用它们。

反正这三个类的实现逻辑非常简单,当不能满足业务需要时,看一下源码,然后参考它创建自己的类即可,此时,关键是multiUpdate的实现。

(4)其它思考

key可以是一个很复杂的List,包括多个字段。

你可能感兴趣的:(storm,trident)