Trident Storm 批处理流…

来源:互联网 发布:免费配货软件下载 编辑:程序博客网 时间:2024/05/27 16:42
本文继《Strom流计算编程模型》之后继续介绍Storm上层高级批处理抽象Trident。
(五)高级抽象
1、Trident State
Storm是对实时流计算的分布式处理框架,在对Stream中封装的Tuple处理过程中涉及到很多中间过程对Tuple进行存储、查询、更新、聚合、分组等变化,Storm的高级抽象Trident将这些变化封装为TridenState对象,使得Trident可以很容易的进行Functions、Filters、partitionAggregate、partitionPersist、projection、Merges、joins等处理。
Trident以容错的方式管理状态State,保证所有的元组Tuple被准确可靠的只处理一次,并可以将State持久化保留在topology的内部,比如说内存和HDFS,也可以放到外部存储当中,比如说Memcached或者Cassandra。这些都是使用同一套TridentAPI。 
假若没有TridentState,那么在灾难故障发生时,应用可能对Tuple的状态不知所措,不清楚该该流入的Tuple是否已经过处理,并成功更新了状态。这里以WordCount举例,当故障发生时,Tuple流被重放,那么对此Tuple流的处理情况将产生疑惑,比如:
1)若此Tuple流从未被成功处理过,需要进行Counter统计;
2)若此Tuple流已经完成Counter统计环节,但在其他环节发生故障,那么不应该再次对此Tuple进行Counter统计;
3)若此Tuple流已经完成整个Counter统计处理,但是在对外部数据库保存时,外部数据库发生故障,那么只应该更新数据库,Tuple树不应再进行处理。
Trident解决上述问题设计完全取决于对Spout的封装,对Spout的容错设计如下:
1) Tuple元组流通过批来处理
2) 每一批元组都有唯一的事务txid,如果这个批因故障被重放,将会拥有之前相同的txid
3) State更新在批中是强制有序的,也就是说批3对应的状态更新一定会在批2的状态更新成功之后再去执行
Trident每个批处理都有三类Spout发射流:non-transactional、transactional、opaquetransactional,因此与之对应的TridentState也有non-transactional、transactional、opaquetransactional三类。而Storm默认的Spout只是实现了IRichSpout,这是一个不带任何事务,并且非批处理的输入发射流。因此,Trident封装了以下几种Spout:
  • ITridentSpout: 最常用的接口,可支持强制事务transactional 和不透明的事务 opaquetransactional 批处理发射流
  • IBatchSpout:  不带任何事务的批处理发射流
  • IPartitionedTridentSpout强制事务 transactional 批处理发射流
  • IOpaquePartitionedTridentSpout: 不透明的事务 opaquetransactional 批处理发射流
1)Transactionalspouts:一个Tuple只会在一个batch中;各个batch之间是没有交集,每个tuple只能属于一个batch;所有Spout发射的Tuple都在batch中,不会有任何遗漏。但Transactionalspouts不足之处是一旦绑定的数据流发射源故障,那么对Tuple的处理将会被挂起。这也就是"opaquetransactional" spouts(不透明事务spout)存在的原因-他们对于丢失源节点这种情况是容错的,仍然能够帮你达到有且只有一次处理的语义。
举例说明Transactional spouts的语义,假设数据库当前记录如下:
man => [count=3, txid=1]dog => [count=4, txid=3]apple => [count=10, txid=2]
此时txid=3的事务携带以下Tuple:["man"]["man"]["dog"],那么由于man当前txid与数据库中txid不一致,man将会被更新count;而dog由于txid一致,则不会被更新,最终数据库存储结果将如下:
man => [count=5, txid=3]dog => [count=4, txid=3]apple => [count=10, txid=2]
2)Opaque transactionalspouts:每个tuple只在一个batch中被成功处理。然而,一个tuple在一个batch中被处理失败后,有可能会在另外的一个batch中被成功处理。
Opaque transactionalspouts在处理批时不会因为当前事务txid与数据库中的txid相同而忽略跳过state的更新。这是因为在state的更新过程之间,batch中的tuple可能已经发生变化了。因此,除了value和txid,还需要存储更多的信息--PreviewValue
继续之前的例子,假设数据库当前记录如下,并且有一个batch对其中一个tuple的单词统计值为2,那么:
{ value = 4, prevValue = 1,  txid = 2 }
如果该batch的txid为3,由于与存储的txid不一致,则前值等于当前值,当前值等于当前值加统计值,即:
{ value = 6,  prevValue = 4, txid = 3 }
如果该batch的txid为2,由于与存储的txid保持一致,但该batch与前面的batch可能tuple的内容完全不同了,因此无视对txid的比较,继续计算统计,则数据存储更新做法是将当前值value更新为前值prevValue+该batch的统计值,即
{ value = 3,  prevValue = 1,  txid = 2 }
3)Non-transactionalspout:不确保每个batch中的tuple的规则。所以他可能是最多被处理过一次(如果tuple被处理失败就不重发设置);或者他也可能会是至少被处理一次(如果tuple在不同的batch中被多次处理的时候)。无论怎样,这种spout是不可能实现有且只有一次被成功处理的语义的。
4) State和Spout 总结
Trident <wbr>Storm <wbr>批处理流计算高级抽象
Opaque transactionalstate有着最为强大的容错性。但这是以存储更多的信息作为代价的。Transactional states需要存储较少的状态信息,但是仅能和 transactional spouts协同工作. Non-transactionalstate所需要存储的信息最少,但是却不能实现有且只有一次被成功处理的语义。
State和Spout类型的选择其实是一种在容错性和存储消耗之间的权衡,你的应用的需要会决定那种组合更适合你。
2、Trident StateAPIs
Trident把所有容错处理逻辑都放在了State里面。作为一个用户,并不需要自己去处理复杂的txid,或者保存很多信息到存储中之类的,所有的逻辑封装TridentState都已经完成了,只需简单的基于Trident StateAPI体系实现和扩展即可。TridentState可以通过TridentTopology的newStaticState方法创建一个静态的State,比如来源与数据库;也可以通过newStream方法创建一个实时的State,比如来源与Kestrel队列!
1)State接口
public interface State {    void beginCommit(Long txid); //开始更新状态    void commit(Long txid);  //结束更新状态}
 这里假设自定义一个保存用户地址信息的State,那么可能代码实现如下:
public class LocationDB implements State {    public void beginCommit(Long txid) {        }    public void commit(Long txid) {        }    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    }}
2)StateFactory接口--State生产工厂
那么自定义用户地址信息的StateFactory定义如下:
public interface StateFactory extends Serializable{    State makeState(Map conf, IMetricsContext metrics, int partitionIndex, int numPartitions)}
public class LocationDBFactory implements StateFactory {   public State makeState(Map conf, int partitionIndex, int numPartitions) {      return new LocationDB();   } }
3)QueryFunction接口、BaseQueryFunction、BaseOperation基类--实现State查询
public interface QueryFunction extends EachOperation {
    List batchRetrieve(S state, List args);
    void execute(TridentTuple tuple, T result, TridentCollector collector);
}
public abstract class BaseQueryFunction extends BaseOperation implements QueryFunction {}
public class BaseOperation implements Operation {
    @Override
    public void prepare(Map conf, TridentOperationContext context) {}
    @Override
    public void cleanup() {}
}
那么自定义查询用户地址信息的State定义如下:
public class QueryLocation extends BaseQueryFunction<</SPAN>LocationDB, String> {    //实现QueryFunction接口定义方法,完成根据输入userid获取location
public List<</SPAN>String> batchRetrieve(LocationDB state, List<</SPAN>TridentTuple> inputs) {        List<</SPAN>String> ret = new ArrayList();        for(TridentTuple input: inputs) {            ret.add(state.getLocation(input.getLong(0)));        }        return ret;    }//实现QueryFunction接口定义方法,发射location
    public void execute(TridentTuple tuple, String location, TridentCollector collector) {        collector.emit(new Values(location));    }    }
那么自定义查询用户地址信息的Topology定义如下:
TridentTopology topology = new TridentTopology();TridentState locations = topology.newStaticState(new LocationDBFactory());topology.newStream("myspout", spout)        .stateQuery(locations, new Fields("userid"), new QueryLocation(), new Fields("location"))
整个Topology的处理流程为:Trident收集了一个batch的userid并将它统一提交给QueryLocation的batchRetrieve进行处理,batchRetrieve根据LocationDB的查询,将返回一个与输入userid列表长度相同,并且与userid一一对应的location列表。
这里你可能发现,每次操作userid将对数据库进行一次location的查询,这样并不优化。因此,LocationDB代码应该调整为:
Trident有另外一种基于partitionPersist抽象的更新State的方法叫做persistentAggregate。在Trident tutorial教程代码中用例如下:
1 WordCount计算样例
https://github.com/storm-amqp/storm-amqp-spout
3)Storm Kestrelhttps://github.com/nathanmarz/storm-kestrel
4) Storm kafka
https://github.com/nathanmarz/storm-contrib/tree/master/storm-kafka
https://github.com/wurstmeister/storm-kafka-0.8-plus
2、NoSQL
https://github.com/granthenke/storm-spring
5、TridentState
1)MemcachedState 
1)StormCassandra
https://github.com/ptgoetz/storm-cassandra

2) StromRedis
https://github.com/sorenmacbeth/storm-redis-pubsub
3)HBase
https://github.com/stormprocessor/storm-hbase

3、Maven Integration
https://github.com/ptgoetz/storm-maven-plugin4、Spring  Integration
5)Stormbeanstalkd
https://github.com/haitaoyao/storm-beanstalkd-spout
教程还是以WordCount为例,首先建立一个实现了IBatchSpout:  不带任何事务的批处理发射流FixedBatchSpout,它将不停的发生语句sentence
FixedBatchSpout spout = new FixedBatchSpout(new Fields("sentence"), 3,               new Values("the cow jumped over the moon"),               new Values("the man went to the store and bought some candy"),               new Values("four score and seven years ago"),               new Values("how many apples can you eat"),spout.setCycle(true);
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"))                       .parallelismHint(6);
整个Topology的处理流程为:首先通过newStream方法接收spout发射流,这里便是上面的sentence spout,当然也可以是Kestrel或者Kafka这样的队列服务;然后对每一个sentence tuple通过Spilt对BaseOperation的实现完成对sentence的拆分,并以word的schema形式发射;接着便按照word进行分组;紧接着通过Count对CombinerAggregator的实现聚合统计,并以count的schema在MemoryMapState中存储;最后为TridentState设置并发执行线程数。
Trident会在Zookeeper中保存一小部分状态信息来追踪数据的处理情况,而在代码中我们指定的字符串“spout1”就是Zookeeper中用来存储metadata信息的Znode节点。
一般来说,这些小的batch中的tuple可能会在数千或者数百万这样的数量级,这完全取决于你的输入的吞吐量。当然,独立的处理每个小的batch并不是非常有趣的事情,所以Trident提供了很多功能来实现batch之间的聚合的结果并可以将这些聚合的结果存储到内存,Memcached, Cassandra或者是一些其他的存储中。同时,Trident还提供了非常好的功能来查询实时状态。这些实时状态可以被Trident更新,同时它也可以是一个独立的状态源。
在本例中WordCount的聚合记录被存储在了内存之中,当然也可以使其存储到外部存储,比如Memcached,如下代码:
其中"serverLocations"是Memcached cluster的主机和端口号列表
(七)其他扩展
1、Spout 扩展
1)StormJMS  
https://github.com/ptgoetz/storm-jms
2)StormAMQP
.persistentAggregate(MemcachedState.transactional(serverLocations), new Count(), new Fields("count"))        MemcachedState.transactional()
Trident非常酷的一点就是它是完全容错的,使得所有的Tupel有且仅只有一次处理的语义。这就让你可以很轻松的使用Trident来进行实时数据处理。Trident会把状态以某种形式保持起来,当有错误发生时,它会根据需要来恢复这些状态。
这里persistentAggregate方法会把数据流转换成一个TridentState对象。在这个例子当中,TridentState对象代表了所有的单词的数量。我们会使用这个TridentState对象来实现在计算过程中的进行分布式查询。
例如在一个低延时的单词数量的分布式查询场景的样例中,该场景需要以一个用空格分割的单词列表为输入,并返回这些单词当天的个数。这些查询是像普通的RPC调用那样被执行的,要说不同的话,那就是他们在后台是并行执行的。下面是执行查询客户端的一个例子:
DRPCClient client = new DRPCClient("drpc.server.location", 3772);System.out.println(client.execute("words", "cat dog the man");// prints the JSON-encoded result, e.g.: "[[5078]]"
对该查询服务端实现如下:
topology.newDRPCStream("words")       .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"));
仍然使用TridentTopology对象来创建DRPC stream,并且我们将这个函数命名为“words”。这个函数名会作为第一个参数在使用DRPC Client来执行查询的时候用到。
每个DRPC请求会被当做只包含一个tuple的batch来处理,该tuple包含了一个叫做“args”的字段,这是由DRPCSpout申明的Filed Schema,其内容就是客户端提供的查询参数"cat dog the man"。那么,首先使用Splict功能把入参拆分成独立的单词。然后对“word” 进行group by操作,之后就可以使用stateQuery来在上面代码中创建的TridentState对象上进行查询。stateQuery接受一个数据源(在这个例子中,就是我们的wordcount TridentState对象)以及一个用于查询的函数作为输入。在这个例子中,我们使用了MapGet函数来获取每个单词的出现个数。由于DRPC stream是使用跟TridentState完全同样的group方式(按照“word”字段进行group),每个单词的查询会被路由到TridentState对象管理和更新这个单词的分区去执行。接下来,我们用FilterNull这个过滤器把TridentState从未出现过的输入单词参数中去掉,并使用Sum这个聚合器将这些count累加起来。最终,Trident会自动把这个结果发送回等待的客户端。
Trident在如何最大程度的保证执行topogloy性能方面是非常智能的。在topology中会自动的发生两件非常有意思的事情:
关于本例完整代码可以查看https://github.com/nathanmarz/storm-starter/blob/master/src/jvm/storm/starter/trident/TridentWordCount.java
一个复杂的Tutorial教程--计算Tweeter的reach值
首先介绍一下什么是reach值,要计算一个URL的reach值,我们需要:
2.1 DRPC实现方式
这里PartialUniquer、CountAggregator都继承于BaseBatchBolt,因此实现了IBatchBolt接口所定义的finishBatch方法。该接口是告诉 LinearDRPCTopologyBuilder 它想在接收到某个request-id的所有tuple之后得到通知,从而可以进行批量处理,在finishBatch回调函数里面,PartialUniquer发射当前这个request-id在这个task上的粉丝数量,而在这个简单设计的背后,Storm是使用CoordinatedBolt来检测什么时候一个bolt接收到某个request的所有的tuple的。而CoordinatedBolt则是利用direct stream来实现这种协调的。这个topology的余下的部分就非常的明了了。
2.2.1 首先,客户端代码依旧类似如下:
DRPCClient client = new DRPCClient("drpc.server.location", 3772);
2.2.2 其次,模拟查询数据源:
TridentState urlToTweeters =       topology.newStaticState(getUrlToTweetersState());TridentState tweetersToFollowers =       topology.newStaticState(getTweeterToFollowersState());
这里newStaticState方法创建了TridentState对象来代表一种外部存储。使用这个TridentState对象,我们就可以在这个topology上面进行动态查询了。和所有的状态源一样,在数据库上面的查找会自动被批量执行,从而最大程度的提升效率。
2.2.3 最后,实现服务端查询逻辑:
topology.newDRPCStream("reach", drpc)
    .stateQuery(urlToTweeters, new Fields("args"), new MapGet(), new Fields("tweeters"))
    .each(new Fields("tweeters"), new ExpandList(), new Fields("tweeter"))
    .shuffle()
    .stateQuery(tweetersToFollowers, new Fields("tweeter"), new MapGet(), new Fields("followers"))
    .parallelismHint(200)
    .each(new Fields("followers"), new ExpandList(), new Fields("follower"))
    .groupBy(new Fields("follower"))
    .aggregate(new Fields("follower"), new One(), new Fields("count-one"))
    .aggregate(new Fields("count-one"), new Sum(), new Fields("reachsum"))
    .parallelismHint(15);
这个topology的定义是非常直观的 - 只是一个简单的批量job的静态处理。首先,查询urlToTweeters数据库来得到tweet过这个URL的人员列表。这个查询会返回一个列表,因此我们使用ExpandList函数来把返回的tweeters转换成tweeter
接下来,我们获取每个tweeter的follower。我们使用shuffle来把要处理的tweeter分布到toplology运行的每一个worker中并发去处理。然后查询follower数据库从而的到每个tweeter的follower。可以看到我们为topology的这部分分配了很大的并行度200,这是因为这部分是整个topology中最耗资源的计算部分。
然后我们在follower上面使用group by操作进行分组,并且对每个组使用一个One聚合器。这个聚合器只是简单的把被group到一起follower聚合数为1,从而实现follower去重,最后再使用Sum聚合器累加去重后follower的count,从而形成reach值。
“One”聚合器的定义如下:
public class One implements CombinerAggregator<</SPAN>Integer> {   public Integer init(TridentTuple tuple) {       return 1;   }   public Integer combine(Integer val1, Integer val2) {       return 1;   }   public Integer zero() {       return 1;   }        }
这重点要将One理解为一个"汇总聚合器", 因为它会在传送结果到其他worker汇总之前进行局部汇总,从而来最大程度上提升性能。Sum也是一个汇总聚合器,因此以Sum作为topology的最终操作是非常高效的。
关于本例完整代码可以查看https://github.com/nathanmarz/storm-starter/blob/master/src/jvm/storm/starter/trident/TridentReach.java
4、Trident API1) Operation
TridentTuple是Trident操作的数据模型--一个被命名的Tuple元组列表。在一个topology中,tridenttuple是在一系列的处理操作(operation)中增量生成的。operation一般以一组tuple作为输入,并以一组tuple作为输出。Operation的输入字段经常是输入tuple的一个子集,而功能字段则是operation的输出。
例如假定你有一个叫做“stream”的stream,它包含了“x”,"y"和"z"三个字段。为了运行一个读取“y”作为输入的过滤器MyFilter,你可以这样写:
stream.each(new Fields("x", "y"), new AddAndMultiply(), new Fields("added", "multiplied"));

输出的功能字段被添加到输入tuple中。因此这个时候,每个tuple中将会有5个字段"x", "y", "z", "added", 和 "multiplied". "added" 和"multiplied"对应于AddAndMultiply输出的第一和第二个字段。
再看一例:
Stream1的“key”和stream2的“x”关联。Trident要求所有的字段要被命名,因为原来的名字将会会覆盖。Join的输入会包含:
1.首先是join字段。例子中stream1中的“key”对应stream2中的“x”。
2.接下来,会把非join字段依次列出来,排列顺序按照传给join的顺序。例子中“a”,“b”对应stream1中的“val1”和“val2”,“c”对应stream2中的“val1”。
当join的流分别来自不通的spout,这些不同的spout会进行同步发射。也就是说,按批次处理的Tuple都来源于不同的Spout组合。
(六)序列化
http://github.com/nathanmarz/storm/wiki/Serialization
7)Merge 和 join
Merge合并多个流到一个流中,代码实现如下:
topology.join(stream1, new Fields("key"), stream2, new Fields("x"), new Fields("key", "a", "b", "c"));
topology.merge(stream1, stream2, stream3);
合并后的流字段将会以第一个流的字段命名。join仅被用于数量很小的batch中,并且由spout发射。考虑一个例子,这里申明了两个流,字段分别定义为 ["key", "val1", "val2"] 和 ["x", "val1"],代码如下:
public class MyFunction extends BaseFunction {    public void execute(TridentTuple tuple, TridentCollector collector) {        for(int i=0; i <</SPAN> tuple.getInteger(0); i++) {            collector.emit(new Values(i));        }    }}
假设目前有一个申明为["a", "b", "c"] 的Tuple流,内容如下:
如果程序编写如下:
mystream.each(new Fields("b"), new MyFunction(), new Fields("d")))
那么将产生一个申明为["a", "b", "c", "d"] 的Tuple流如下输出:
[1, 2, 3, 0][1, 2, 3, 1][4, 1, 6, 0]
2) Filter顾名思义,就是过滤功能。Filter接收一组Tuple流的子集作为输入,并选择将其输出。考虑如下例子:
public class MyFilter extends BaseFunction {    public boolean isKeep(TridentTuple tuple) {        return tuple.getInteger(0) == 1 && tuple.getInteger(1) == 2;    }}
假设目前有一个申明为["a", "b", "c"] 的Tuple流,内容如下:
[1, 2, 3][2, 1, 1][2, 3, 4]
如果程序编写如下:mystream.each(new Fields("b", "a"), new MyFilter())那么将输出如下:[2, 1, 1]3)partitionAggregate
partitionAggregate在每一批的Tuple流中的分区运行一个Funcation,而与Funcation不同的是partitionAggregate将会替换掉输入中的Tuple。考虑如下例子:
mystream.partitionAggregate(new Fields("b"), new Sum(), new Fields("sum"))
假设目前有一个申明为["a", "b"] 的Tuple流,内容如下:
Partition 0:["a", 1]["b", 2]Partition 1:["a", 3]["c", 8]Partition 2:["e", 1]["d", 9]["d", 10]
那么输出将只有一个字段申明为["sum"],其内容如下:
Partition 0:[3]Partition 1:[11]Partition 2:[20]
Trident中有三种Aggregator,分别是:CombinerAggregator, ReducerAggregator, Aggregator。以CombinerAggregator说明:
public interface CombinerAggregator<</SPAN>T> extends Serializable {    T init(TridentTuple tuple);    T combine(T val1, T val2);    T zero();}
CombinerAggregator在处理分区上面的Tuple流是只返回一个唯一的Tuple,并且只申明一个字段。每个输入的Tuple元组CombinerAggregator都会运行init函数,并通过combine函数将Tuple合并,直到只剩最后一个值,然后向外发射出去。如果所负责处理的分区中没有Tuple输入,那么CombinerAggregator将调用zero函数返回。以下例实现为例:
另外ReducerAggregator定义如下:
public interface ReducerAggregator<</SPAN>T> extends Serializable {    T init();    T reduce(T curr, TridentTuple tuple);}
ReducerAggregator通过init函数初始化一个值,并且为所处理的Tuple流迭代该值,并最终返回一个唯一的Tuple,并且只申明一个字段。如为Tuple流实现一个Count计数器的代码如下:
stateQuery和partitionPersist分别用于查询和更新数据源的状态,详细使用参考上节Trident tutorial5) Projection
Projection将仅仅投影保留在函数中定义的Tuple流,假如在流中有一个定义为["a", "b", "c", "d"]的Tuple流,如果代码使用如下:
mystream.project(new Fields("b", "d"))
那么该Tuple流仅仅剩下["b", "d"]字段
5) Repartitioning
重分区操作是通过一个函数改变元组(tuple)在task之间的分布。也可以调整分区数量(比如,如果并发的parallelism hint在repartition之后变大)重分区(repatition)需要网络传输tuple流。下面是重分区的函数:1.      Shuffle:使用随机算法在目标分区中选一个分区发送数据2.      Broadcast:每个元组重复的发送到所有的目标分区。这个在DRPC中很有用。如果你想做在每个分区上做一个statequery。3.      paritionBy:根据一系列分发字段(fields)做一个语义的分区。通过对这些字段取hash值并对目标分区数取模获取目标分区。paritionBy保证相同的分发字段(fields)分发到相同的目标分区。4.      global:所有的tuple分发到相同的分区。这个分区所有的批次相同。5.      batchGobal:本批次的所有tuple发送到相同的分区,不通批次可以在不通的分区。6.      patition:这个函数接受用户自定义的分区函数。用户自定义函数事项 backtype.storm.grouping.CustomStreamGrouping接口。
6) Aggregation
Trident有aggregate和persistentAggregate函数对流做聚合。aggregate是在每个批次上独立运行,persistentAggregate聚合流的所有的批次并将结果存储下来。aggregate函数是在一个流上做全局的聚合。当你在使用ReducecerAggregator或者Aggretator时,这个流首先被分成一个分区,然后聚合函数在这个分区上运行。如果使用CombinerAggreator,Trident先会在每个分区上做一个局部的汇总,然后重分区到为一个单独的分区,在网络传输结束后完成聚合。CombinerAggreator非常有效,在尽可能的情况下多使用。 下面是一个做批次内聚合的例子:
和partitionAggregate一样,聚合的aggregate也可以串联形成链条。但如果将CombinerAggreator和非CombinerAggreator串联,trident就不能做局部汇总的优化。
6)Groupby
GroupBy操作根据特殊的字段对流进行重分区,分组字段相同的元组(tuple)被分到同一个分区,下面是个GroupBy的例子: Trident <wbr>Storm <wbr>批处理流计算高级抽象如果对分组的流进行聚合,聚会会对每个组聚合而不是这个批次聚合。(和关系型数据库的groupby相同)。PersistentAggregate也可以在一个已分组的GroupedStream运行,这种情况下结果将会存储在MapState里面,key是分组字段,value是聚合值。
stream.groupBy(new Fields("val1")).aggregate(new Fields("val2"), new Sum(), new Fields("sum"))
在这个例子中,输出将包含字段"val1" 和 "sum"。
stream.aggregate(new Fields("val2"), new Sum(), new Fields("sum"))
output stream将会只包含一个叫做“sum”的字段,这个sum字段就是“val2”的累积和。
4) SateQuery和PartitionPersist
public class Count implements ReducerAggregator<</SPAN>Long> {    public Long init() {        return 0L;    }    public Long reduce(Long curr, TridentTuple tuple) {        return curr + 1;    }}
Aggregator是最为常见和通用的聚合器了,它的定义如下:
public interface Aggregator<</SPAN>T> extends Operation {    T init(Object batchId, TridentCollector collector);    void aggregate(T state, TridentTuple tuple, TridentCollector collector);    void complete(T state, TridentCollector collector);}
Aggregator可以发射返回任意个数定义的Field的多个Tuple,Aggregator的执行过程如下:
1. 在每批Tuple流达到之前,init函数被执行,并且作为当前聚合的状态State传递给aggregate和complete方法;
2. 在分区中的每批Tuple流的每个Tuple将会执行aggregate方法,该方法将更新状态State,并可选择性的发射tuple;
3. 在该分区中所有的Tuple流都被处理完成后,将执行comple方法。
这里还是以实现一个Count计数器为例,代码如下:
public class CountAgg extends BaseAggregator<</SPAN>CountState> {    static class CountState {        long count = 0;    }    public CountState init(Object batchId, TridentCollector collector) {        return new CountState();    }    public void aggregate(CountState state, TridentTuple tuple, TridentCollector collector) {        state.count+=1;    }    public void complete(CountState state, TridentCollector collector) {        collector.emit(new Values(state.count));    }}
很多情况下,可能需要连续进行多次聚合,从而形成一个链条,可以如下实现:
mystream.chainedAgg()        .partitionAggregate(new Count(), new Fields("count"))        .partitionAggregate(new Fields("b"), new Sum(), new Fields("sum"))        .chainEnd()
该代码将输出一个定义为 ["count", "sum"].的Tuple。
public class Count implements CombinerAggregator<</SPAN>Long> {    public Long init(TridentTuple tuple) {        return 1L;    }    public Long combine(Long val1, Long val2) {        return val1 + val2;    }    public Long zero() {        return 0L;    }}
使用CombinerAggregator替换partitionAggregate的好处在于Trident会自动的优化其在分区局部聚合过程中的性能,减少Tuple传输的网络流量开销。
[1, 2, 3][4, 1, 6][3, 0, 8]
stream.each(new Fields("y"), new MyFilter())
public class MyFilter extends BaseFilter {   public boolean isKeep(TridentTuple tuple) {       return tuple.getInteger(0) <</SPAN> 10;   }}
这会保留所有“y”字段小于10的tuples。因为TridentTuple传给MyFilter进行过滤的输入字段只有字段“y”。
这里需要注意的是,当选择输入字段时,Trident会自动投影tuple的一个子集,这个操作是非常高效的。
2) Funcation
Funcation接受一个Tuple的子集进行处理,并且可以不发射或发射更多的Tuple。输出的Field将在原始的输入后面追加。如果一个Funcation没有发射出Tuple,那么说明输入被全部过滤掉了,否则输入将会重复在输出中再次发射出去。
public class AddAndMultiply extends BaseFunction {   public void execute(TridentTuple tuple, TridentCollector collector) {       int i1 = tuple.getInteger(0);       int i2 = tuple.getInteger(1);       collector.emit(new Values(i1 + i2, i1 * i2));   }}
这个函数接收两个数值作为输入并输出两个新的值:“和”与“乘积”。假定你有一个stream,其中包含“x”,"y"和"z"三个字段。你可以这样使用这个函数:
关于本例完整代码可以查看https://github.com/nathanmarz/storm-starter/blob/master/src/jvm/storm/starter/ReachTopology.java
2.2 Trident实现方式
  • GetTweeters获取所发微薄里面包含制定URL的所有用户。它接收输入流:[id, url], 它输出:[id, tweeter].每一个URL tuple会对应到很多tweetertuple;
  • GetFollowers获取这些tweeter的粉丝。它接收输入流: [id, tweeter], 它输出: [id,follower]
  • PartialUniquer将tweeter的粉丝follower进行fieldsGrouping操作,使得相同的follower只会落在相同的PartialUniquertask进行处理,而在PartialUniquer内部使用Set集合保存接收到的follower。也就是说通过fieldsGrouping保证了相同的follower发到相同的PartialUniquertask进行处理,再利用Set集合的特性,从而实现了对follower的去重工作,并最后将已去重的follower的Set集合大小Size作为输出发射,它的输出流:[id,count] 即输出这个task上统计的粉丝个数。
  • CountAggregator接收所有的PartialUniquertask统计局部数量,并把它们累加起来就算出了该URL的reach值,这类似MapReduce里面的Combiner
LinearDRPCTopologyBuilder builder = new LinearDRPCTopologyBuilder("reach");builder.addBolt(new GetTweeters(), 3);builder.addBolt(new GetFollowers(), 12).shuffleGrouping();builder.addBolt(new PartialUniquer(), 6).fieldsGrouping(new Fields("id", "follower"));builder.addBolt(new CountAggregator(), 2).fieldsGrouping(new Fields("id"));
  • 获取所有微薄里面包含这个URL的人
  • 获取这些人的粉丝
  • 把这些粉丝去重
  • 获取这些去重之后的粉丝个数— 这就是reach
2 Reach值计算样例
  • 对状态的读写操作 (比如说 persistentAggregate 和stateQuery)会自动的以batch的形式进行。所以,假设有20次更新需要被同步到存储中,Trident会自动的把这些操作汇总到一起,只做一次读一次写,而不是进行20次读20次写的操作。因此你可以在很方便的执行计算的同时,保证了非常好的性能。
  • Trident的聚合器已经是被优化的非常好了的。Trident并不是简单的把一个group中所有的tuples都发送到同一个机器上面进行聚合,而是在发送之前已经进行过一次部分的聚合。打个比方,Count聚合器会先在每个partition上面进行count,然后把每个分片count汇总到一起就得到了最终的count。这个技术其实就跟MapReduce里面的combiner是一个思想。
然后建立Trident Topology对单词进行分组统计,实现代码如下:
persistentAggregate需要使用一个Trident聚合器aggregator来更新State。在本例中,因为这是一个已分组好的GroupedStream中,因此进行分组的字段会以key的形式存在于State当中,而分组聚合的结果会以value的形式存储在State当中。当然这里Trident需要你提供一个实现了MapState的接口,这里便是MemoryMapState。persistentAggregate由GroupedStream实现,而partitionPersist由Stream实现。
public interface MapState<</SPAN>T> extends State {    List<</SPAN>T> multiGet(List<</SPAN>List<</SPAN>Object>> keys);    List<</SPAN>T> multiUpdate(List<</SPAN>List<</SPAN>Object>> keys, List<</SPAN>ValueUpdater> updaters);    void multiPut(List<</SPAN>List<</SPAN>Object>> keys, List<</SPAN>T> vals);}
然而,当需要对一个没有进行过分组的Stream进行聚合的时候,则需要实现Snapshottable接口
public interface Snapshottable<</SPAN>T> extends State {    T get();    T update(ValueUpdater updater);    void set(T o);}
Trident的MemoryMapState 和 MemcachedState同时实现了MapState和Snapshottable接口。
在Trident中实现MapState是非常简单的,它几乎帮你做了所有的事情。OpaqueMap, TransactionalMap, 和 NonTransactionalMap 类实现了所有相关的逻辑,包括容错的逻辑。
另外,Trident还提供了一种CachedMap类来进行自动的LRU cache, 也提供了 SnapshottableMap 类将一个MapState 转换成一个 Snapshottable 对象。
3、Trident tutorial
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"))
public class LocationDB implements State {    public void beginCommit(Long txid) {        }        public void commit(Long txid) {        }    public void setLocationsBulk(List<</SPAN>Long> userIds, List<</SPAN>String> locations) {      // set locations in bulk    }    public List<</SPAN>String> bulkGetLocations(List<</SPAN>Long> userIds) {      // get locations in bulk    }}
public class QueryLocation extends BaseQueryFunction<</SPAN>LocationDB, String> {
这样将减少对数据库的访问,从而提高性能!
TridentTopology topology = new TridentTopology();TridentState locations =     topology.newStream("locations", locationsSpout)        .partitionPersist(new LocationDBFactory(), new Fields("userid", "location"), new LocationUpdater())
整个Topology的处理流程为:Trident收集了一个batch的userid和location并将它统一提交给LocationUpdater的updateState进行处理。
5)MapState接口、Snapshottable接口--实现State聚合
那么自定义更新用户地址信息的State定义如下:
public class LocationUpdater extends BaseStateUpdater<</SPAN>LocationDB> {    public void updateState(LocationDB state, List<</SPAN>TridentTuple> tuples, TridentCollector collector) {        List<</SPAN>Long> ids = new ArrayList<</SPAN>Long>();        List<</SPAN>String> locations = new ArrayList<</SPAN>String>();        for(TridentTuple t: tuples) {            ids.add(t.getLong(0));            locations.add(t.getString(1));        }        state.setLocationsBulk(ids, locations);    }}
那么自定义更新用户地址信息的Topology定义如下:
4)StateUpdater接口、BaseStateUpdater抽象类--实现State更新
public interface StateUpdater extends Operation {
    void updateState(S state, List tuples, TridentCollector collector);
}
public abstract class BaseStateUpdater extends BaseOperation implements StateUpdater {}
 //实现QueryFunction接口定义方法,完成根据输入userid批量获取location
    public List<</SPAN>String> batchRetrieve(LocationDB state, List<</SPAN>TridentTuple> inputs) {        List<</SPAN>Long> userIds = new ArrayList<</SPAN>Long>();        for(TridentTuple input: inputs) {            userIds.add(input.getLong(0));        }        return state.bulkGetLocations(userIds);    }//实现QueryFunction接口定义方法,发射location    public void execute(TridentTuple tuple, String location, TridentCollector collector) {        collector.emit(new Values(location));    }    }
那么相应的QueryLocation调整为:
https://github.com/nathanmarz/trident-memcached
2) MongoState
https://github.com/eldenbishop/trident-mongodb
https://github.com/sjoerdmulder/trident-mongodb
https://github.com/wilbinsc/storm-mongo
3)RedisState
https://github.com/kstyrc/trident-redis
0 0