问题导读 1、Trident是什么? 2、如何使用Trident的API来完成大吞吐量的流式计算? 3、如何使用stream作为输入并计算每个单词的个数? Trident是在storm基础上,一个以实时计算为目标的高度抽象。 它在提供处理大吞吐量数据能力(每秒百万次消息)的同时,也提供了低延时分布式查询和有状态流式处理的能力。 如果你对Pig和Cascading这种高级批处理工具很了解的话,那么应该很容易理解Trident,因为他们之间很多的概念和思想都是类似的。Tident提供了 joins, aggregations, grouping, functions, 以及 filters等能力。除此之外,Trident 还提供了一些专门的原语,从而在基于数据库或者其他存储的前提下来应付有状态的递增式处理。Trident也提供一致性(consistent)、有且仅有一次(exactly-once)等语义,这使得我们在使用trident toplogy时变得容易。 举例说明 让我们一起来看一个Trident的例子。在这个例子中,我们主要做了两件事情: 1、从一个流式输入中读取语句并计算每个单词的个数 2、提供查询给定单词列表中每个单词当前总数的功能 因为这只是一个例子,我们会从如下这样一个无限的输入流中读取语句作为输入:
- 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);
复制代码
这个spout会循环输出列出的那些语句到sentence stream当中,下面的代码会以这个stream作为输入并计算每个单词的个数:
- 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);
复制代码
在这段代码中,我们首先创建了一个TridentTopology对象,该对象提供了相应的接口去构造Trident计算过程。①、TridentTopology类中的newStream方法从输入源(input source)中读取数据,并创建一个新的数据流。在这个例子中,我们使用了上面定义的FixedBatchSpout对象作为输入源。输入数据源同样也可以是如Kestrel或者Kafka这样的队列服务。Trident会在Zookeeper中保存一小部分状态信息来追踪数据的处理情况,而在代码中我们指定的字符串“spout1”就是Zookeeper中用来存储状态信息的Znode节点。 Trident在处理输入stream的时候会把输入转换成batch(包含若干个tuple)来处理。比如说,输入的sentence stream可能会被拆分成如下的batch:  一般来说,这些小的batch中的tuple可能会在数千或者数百万这样的数量级,这完全取决于你的输入的吞吐量。 Trident提供了一系列非常成熟的批处理API来处理这些小batch。这些API和你在Pig或者Cascading中看到的非常类似, 你可以做groupby、join、 aggregation、执行 function和filter等等。当然,独立的处理每个小的batch并不是非常有趣的事情,所以Trident提供了功能来实现batch之间的聚合并可以将这些聚合的结果存储到内存、Memcached、Cassandra或者是一些其他的存储中。同时,Trident还提供了非常好的功能来查询实时状态,这些实时状态可以被Trident更新,同时它也可以是一个独立的状态源。 回到我们的这个例子中来,spout输出了一个只有单一字段“sentence”的数据流。②、在下一行,topology使用了Split函数来拆分stream中的每一个tuple,Split函数读取输入流中的“sentence”字段并将其拆分成若干个word tuple。每一个sentence tuple可能会被转换成多个word tuple,比如说”the cow jumped over the moon” 会被转换成6个 “word” tuples。下面是Split的定义:
- public class Split extends BaseFunction {
- public void execute(TridentTuple tuple, TridentCollector collector) {
- String sentence = tuple.getString(0);
- for(String word: sentence.split(" ")) {
- collector.emit(new Values(word));
- }
- }
- }
复制代码
如你所见,真的很简单。它只是简单的根据空格拆分sentence,并将拆分出的每个单词作为一个tuple输出。 topology的其他部分计算单词的个数并将计算结果保存到了持久存储中。③、首先,word stream被根据“word”字段进行group操作,④、然后每一个group使用Count聚合器进行持久化聚合。persistentAggregate方法会帮助你把一个状态源聚合的结果存储或者更新到存储当中。在这个例子中,单词的数量被保持在内存中,不过我们可以很简单的把这些数据保存到其他的存储当中,如 Memcached、 Cassandra等。如果我们要把结果存储到Memcached中,只是简单的使用下面这句话替换掉persistentAggregate就可以,这当中的”serverLocations”是Memcached cluster的主机和端口号列表:
- .persistentAggregate(MemcachedState.transactional(serverLocations), new Count(), new Fields("count"))
复制代码
persistentAggregate存储的数据就是所有batch聚合的结果。 Trident非常酷的一点就是它提供完全容错的(fully fault-tolerant)、处理一次且仅一次(exactly-once)的语义。这就让你可以很轻松的使用Trident来进行实时数据处理。Trident会把状态以某种形式保持起来,当有错误发生时,它会根据需要来恢复这些状态。 ④续、persistentAggregate方法会把数据流转换成一个TridentState对象。在这个例子当中,TridentState对象代表了所有的单词的数量。我们会使用这个TridentState对象来实现在计算过程中的分布式查询部分。 上面的是topology中的第一部分,topology的第二部分实现了一个低延时的单词数量的分布式查询。这个查询以一个用空格分割的单词列表为输入,并返回这些单词的总个数。这些查询就像普通的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]]"
复制代码
如你所见,除了在storm cluster上并行执行之外,这个查询看上去就是一个普通的RPC调用。这样的简单查询的延时通常在10毫秒左右。当然,更复杂的DRPC调用可能会占用更长的时间,尽管延时很大程度上是取决于你给计算分配了多少资源。 Topology中的分布式查询部分实现如下所示:
- 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来表示这个请求。这个tuple包含了一个叫做“args”的字段,在这个字段中保存了客户端提供的查询参数。在这个例子中,这个参数是一个以空格分割的单词列表。 首先,我们使用Split函数把传入的请求参数拆分成独立的单词。然后对“word”流进行group by操作,之后就可以使用stateQuery来在上面代码中创建的TridentState对象上进行查询。stateQuery接受一个state源(在这个例子中,就是我们的topolgoy所计算的单词的个数)以及一个用于查询的函数作为输入。在这个例子中,我们使用了MapGet函数来获取每个单词的出现个数。由于DRPC stream是使用跟TridentState完全同样的group方式(按照“word”字段进行groupby),每个单词的查询会被路由到TridentState对象管理和更新这个单词的分区去执行。 接下来,我们用FilterNull这个过滤器把从未出现过的单词给过滤掉(说明没有查询该单词),并使用Sum这个聚合器将这些count累加起来得到结果。最终,Trident会自动把这个结果发送回等待的客户端。 Trident在如何最大程度地保证执行topogloy性能方面是非常智能的。在topology中会自动的发生两件非常有意思的事情: 1、读取和更新状态的操作 (比如说 stateQuery和persistentAggregate ) 会自动地批量处理。 如果当前处理的batch中有20次更新需要被同步到存储中,Trident会自动的把这些操作汇总到一起,只做一次读一次写,而不是进行20次读20次写的操作。因此你可以在很方便的执行计算的同时,保证了非常好的性能。 2、Trident的聚合器已经是被优化的非常好了的。Trident并不是简单的把一个group中所有的tuples都发送到同一个机器上面进行聚合,而是在发送之前已经进行过一次部分的聚合。打个比方,Count聚合器会先在每个partition上面进行count,然后把每个分片count汇总到一起就得到了最终的count。这个技术其实就跟MapReduce里面的combiner是一个思想。 让我们再来看一下Trident的另外一个例子。 Reach 这个例子是一个纯粹的DRPC topology,这个topology会计算一个给定URL的reach值,reach值是该URL对应页面的推文能够送达(Reach)的用户数量,那么我们就把这个数量叫做这个URL的reach。要计算reach,你需要获取转发过这个推文的所有人,然后找到所有该转发者的粉丝,并将这些粉丝去重,最后就得到了去重后的用户的数量。如果把计算reach的整个过程都放在一台机器上面,就太困难了,因为这会需要数千次数据库调用以及千万级别数量的tuple。如果使用Storm和Trident,你就可以把这些计算步骤在整个cluster中并行进行(具体哪些步骤,可以参考DRPC介绍一文,该文有介绍过Reach值的计算方法)。 这个topology会读取两个state源:一个将该URL映射到所有转发该推文的用户列表,还有一个将用户映射到该用户的粉丝列表。topology的定义如下:
- TridentState urlToTweeters =
- topology.newStaticState(getUrlToTweetersState());
- TridentState tweetersToFollowers =
- topology.newStaticState(getTweeterToFollowersState());
-
- topology.newDRPCStream("reach")
- .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 One(), new Fields("one"))
- .parallelismHint(20)
- .aggregate(new Count(), new Fields("reach"));
复制代码
这个topology使用newStaticState方法创建了TridentState对象来代表一个外部数据库。使用这个TridentState对象,我们就可以在这个topology上面进行动态查询了。和所有的state源一样,在这些数据库上面的查找会自动被批量执行,从而最大程度的提升效率。 这个topology的定义是非常简单的 – 它仅是一个批处理的任务。 首先,查询urlToTweeters数据库来得到转发过这个URL的用户列表。这个查询会返回一个tweeter列表,因此我们使用ExpandList函数来把其中的每一个tweeter转换成一个tuple。 接下来,我们来获取每个tweeter的follower。我们使用shuffle来把要处理的tweeter均匀地分配到toplology运行的每一个worker中并发去处理。然后查询tweetersToFollowers数据库从而的到每个转发者的粉丝。你可以看到我们为topology的这部分分配了很大的并行度,这是因为这部分是整个topology中最耗资源的计算部分。 |