Storm进阶

并行度###

在Storm集群中真正运行Topology的主要有三个实体:worker、executor、task,下图是可以表示他们之间的关系。

数据流模型
对于一个Spout或Bolt,都会有多个task线程来运行,那么如何在两个组件(Spout和Bolt)之间发送tuple元组呢?Storm提供了若干种数据流分发(Stream Grouping)策略用来解决这一问题。在Topology定义时,需要为每个Bolt指定接收什么样的Stream作为其输入(注:Spout并不需要接收Stream,只会发射Stream)。
目前Storm中提供了以下7种Stream Grouping策略:Shuffle Grouping、Fields Grouping、All Grouping、Global Grouping、Non Grouping、Direct Grouping、Local or shuffle grouping。

  • Shuffle Grouping: 随机分组, 随机派发stream里面的tuple,保证每个bolt接收到的tuple数目大致相同。
  • Fields Grouping:按字段分组, 比如按userid来分组, 具有同样userid的tuple会被分到相同的Bolts里的一个task(这句话很关键,代表了我对storm的理解。)而不同的userid则会被分配到不同的bolts里的task。
  • All Grouping:广播发送,对于每一个tuple,所有的bolts都会收到。
  • Global Grouping:全局分组, 这个tuple被分配到storm中的一个bolt的其中一个task。再具体一点就是分配给id值最低的那个task。
  • Non Grouping:不分组,这个分组的意思是说stream不关心到底谁会收到它的tuple。目前这种分组和Shuffle grouping是一样的效果, 有一点不同的是storm会把这个bolt放到这个bolt的订阅者同一个线程里面去执行。
  • Direct Grouping: 直接分组, 这是一种比较特别的分组方法,用这种分组意味着消息的发送者指定由消息接收者的哪个task处理这个消息。 只有被声明为Direct Stream的消息流可以声明这种分组方法。而且这种消息tuple必须使用emitDirect方法来发射。消息处理者可以通过TopologyContext来获取处理它的消息的task的id (OutputCollector.emit方法也会返回task的id)。
  • Local or shuffle grouping:如果目标bolt有一个或者多个task在同一个工作进程worker中,tuple将会被随机发生给这些tasks。否则,和普通的Shuffle Grouping行为一致。

一个运行中Topology的例子:
Topology里包含了三个component,一个是Blue Spout,另外两个分别是Green Bolt和Yellow Bolt。

Config conf = new Config();   
conf.setNumWorkers(2);  
topologyBuilder.setSpout("blue-spout", new BlueSpout(), 2);

topologyBuilder.setBolt("green-bolt", new GreenBolt(), 2) 
           .setNumTasks(4) 
           .shuffleGrouping("blue-spout");

topologyBuilder.setBolt("yellow-bolt", new YellowBolt(), 6)
           .shuffleGrouping("green-bolt");

StormSubmitter.submitTopology(
    "mytopology",
    conf,
    topologyBuilder.createTopology()
);  

图和代码, 很清晰, 通过setBolt和setSpout一共定义2+2+6=10个executor threads 。并且同setNumWorkers设置2个workers, 所以storm会平均在每个worker上run 5个executors (线程)。而对于green-bolt, 定义了4个tasks, 所以每个executor中有2个tasks。

总结

  1. 一个Topology可以包含多个worker ,一个worker只能对应于一个topology。worker process是一个topology的子集。
  2. 一个worker可以包含多个executor,一个executor只能对应于一个component(spout或者bolt)。
  3. Task就是具体的处理逻辑, 一个executor线程可以执行一个或多个tasks。线程就是资源,task就是要运行的任务。

消息的可靠处理

Storm记录级容错

首先来看一下什么叫做记录级容错?storm允许用户在spout中发射一个新的源tuple时为其指定一个message id, 这个message id可以是任意的object对象。多个源tuple可以共用一个message id,表示这多个源 tuple对用户来说是同一个消息单元。storm中记录级容错的意思是说,storm会告知用户每一个消息单元是否在指定时间内被完全处理了。那什么叫做完全处理呢,就是该message id绑定的源tuple及由该源tuple后续生成的tuple经过了topology中每一个应该到达的bolt的处理。举个例子。在下图中,在spout由message 1绑定的tuple1和tuple2经过了bolt1和bolt2的处理生成两个新的tuple,并最终都流向了bolt3。当这个过程完成处理完时,称message 1被完全处理了。

在storm的topology中有一个系统级组件,叫做acker。这个acker的任务就是追踪从spout中流出来的每一个message id绑定的若干tuple的处理路径,如果在用户设置的最大超时时间内这些tuple没有被完全处理,那么acker就会告知spout该消息处理失败了,相反则会告知spout该消息处理成功了。在刚才的描述中,我们提到了”记录tuple的处理路径”,storm中却是使用了一种非常巧妙的方法做到了。在说明这个方法之前,我们来复习一个数学定理。
A xor A = 0.
A xor B…xor B xor A = 0,其中每一个操作数出现且仅出现两次。
storm中使用的巧妙方法就是基于这个定理。具体过程是这样的:在spout中系统会为用户指定的message id生成一个对应的64位整数,作为一个root id。root id会传递给acker及后续的bolt作为该消息单元的唯一标识。同时无论是spout还是bolt每次新生成一个tuple的时候,都会赋予该tuple一个64位的整数的id。Spout发射完某个message id对应的源tuple之后,会告知acker自己发射的root id及生成的那些源tuple的id。而bolt呢,每次接受到一个输入tuple处理完之后,也会告知acker自己处理的输入tuple的id及新生成的那些tuple的id。Acker只需要对这些id做一个简单的异或运算,就能判断出该root id对应的消息单元是否处理完成了。下面通过一个图示来说明这个过程。
第一步:初始化,spout中绑定message 1生成了两个源tuple,id分别是0010和1011.

第二步:计算一个turple达到第1个bolt

第三步:计算一个turple达到第2个bolt

第四步:消息到达最后一个bolt

即在正常情况下,每个id都会且只会被异或两次,因此最后的结果一定是0,但是容错过程存在一个可能出错的地方,那就是,如果生成的tuple id并不是完全各异的,acker可能会在消息单元完全处理完成之前就错误的计算为0。这个错误在理论上的确是存在的,但是在实际中其概率是极低极低的,完全可以忽略。

高可靠性下Spout需要做些什么
当Spout从队列中读取一个消息,表示它“打开”了队列中某个消息,这意味着,此消息并未从队列中真正删除,而是被置为“pending”状态,它等待来自客户端的应答,被应答之后,此消息才会被真正从队列中删除。处于“pending”状态的消息不会被其他客户端看到。另外,如果一个客户端意外断开连接,则由此客户端“打开”的所有消息都会被重新加入到队列中。当消息被“打开”的时候,队列同时会为这个消息提供一个唯一的标识。
Spout使用这个唯一标识作为这个tuple的id,当ack或fail被调用时,Spout会把ack或者fail连同id一起发送给队列,队列会将消息从队列中真正删除或者将它重新放回队列中。

选择合适的可靠性级别
如果并不需要每个消息必须被处理,那么可以关闭消息的可靠性机制,从而获取较好的性能。关闭消息的可靠处理机制意味着系统中的消息数会减半(每个消息不需要应答了)。另外,关闭消息的可靠性处理机制可以减少消息的大小(不需要每个tuple记录它的根id),从而节省带宽。
有三种方法调整消息的可靠处理机制:

  1. 将参数Config.TOPOLOGY_ACKERS设置为0,通过此方法,当Spout发送一个消息时,它的ack方法将立即被调用。
  2. 第二种方法是Spout发送一个消息时,不指定此消息的id。当需要关闭特定消息的可靠性时,可以使用此方法。
  3. 如果不在意某个消息派生出来的子孙消息的可靠性,则此消息派生出来的子消息在发送时不要做锚定,即在emit方法中不指定输入消息。因此这些子孙消息没有被锚定在任何tuple tree中,因此他们的失败不会引起任何Spout重新发送消息。

集群中各级容错

  • Bolt任务crash引起的消息未被应答。此时,acker中所有与此Bolt任务关联的消息都会因为超时而失败,对应Spout的fail方法将会被调用。
  • acker任务本身失败,它在失败之前持有的所有消息都将因超时而失败。Spout的fail方法将被调用。
  • Spout任务失败,队列会将处于pending状态的所有消息重新放回队列里。
  • Worker失败,Supervisor负责监控Worker中的任务,Supervisor会尝试在本机重启它。
  • Supervisor失败,由于Supervisor是无状态的,只需将它重新启动即可。
  • Nimbus失败。由于Nimbus是无状态的,只需将它重新启动即可。
  • Storm中的集群节点故障,此时Nimbus会将此机器上所有正在运行的任务转移到其他可用的机器上运行。
  • Zookeeper集群中的节点故障,Zookeeper保证少于半数的机器宕机系统仍可正常运行,及时修复故障机器即可。

一致性事务

Storm如何实现既对tuple并行处理,又保证事务性呢?这里先从简单的事务性实现方法入手,逐步引出Transactional Topology的原理。

强顺序流
将tuple流变成强顺序性的,并且每次只处理一个tuple。从1开始,给每个tuple都顺序加上一个id,在处理tuple时,将处理成功的tuple id和计算结果存在数据库中。下一个tuple到来时,将其id与数据库中的id作比较,如果相同,则说明这个tuple已经被成功处理过了,那么忽略,如果不同,则将它的id和计算结果更新到数据库中。
但是这种机制使得系统一次只能处理一个tuple,无法实现分布式计算。

强顺序batch流
为了实现分布式,我们可以每次处理一批tuple,即一个batch,每个bacth中的tuple可以并行处理。这样数据库里存的就是bacth id和bacth的计算结果。
但是这种机制每次只能处理一个batch,batch之间无法并行。

CoordinateBolt的原理

  • 每个CoordinateBolt记录两个值:有哪些task给我发送了tuple以及我要给哪些task发送信息。
  • 真正执行任务的bolt是real bolt,它发出一个tuple后,其外层的CoordinateBolt会记录下这个tuple发送给了哪个task。
  • 所有的tuple发送完了以后,CoordinateBolt会告诉它发送过tuple的task,它发送了多少tuple给这个task,下游task会将这个数字和自己接收到的tuple数量做对比,如果相等,则说明处理完了所有的tuple。

Transactional Topology
Storm提供的Transactional Topology将batch计算分为process和commit两个阶段,process阶段可以同时处理多个batch,不用保证顺序性;commit阶段保证batch的强顺序性,并且一次只能处理一个batch,第一个batch成功提交之前,第二个batch不能被提交。
Transactional Topology里发送的tuple都必须以TransactionAttempt作为第一个field,Storm根据这个field来判断tuple属于哪一个batch。
TransactionAttempt包含两个值:一个是transaction id,另一个是attempt id,transaction id对于每个batch中的tuple是唯一的,不管replay多少次都是一样的。attempt id是每个batch唯一的一个id,但是对于同一个batch,它replay之后的attempt id跟replay之前不一样。
当bolt收到某个batch所有的tuple以后,finishBatch会被调用,将当前的transaction id与数据库中存储的id做比较,如果相同则忽略,不同就把这个batch的计算结果加到总结果中,并更新数据库。

  • TransactionSpout只能有一个,它将所有tuple分组为一个一个的batch,而且保证同一个batch的transaction id始终一样。
  • BatchBolt处理一个batch中所有的tuples。对于每一个tuple调用execute方法,而在整个batch处理完成时调用finishBatch方法。
  • 如果BatchBolt被标记为Committer,则只能在Committer阶段调用finishBolt方法,并且在commit阶段batch是强顺序性的。

posted on 2016-09-27 14:27  LeonNew  阅读(1687)  评论(0编辑  收藏  举报

导航