• 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是强顺序性的。
  • 相关阅读:
    CFgym102394I
    Infinite Fraction Path (后缀数组)
    2016ACM/ICPC亚洲区沈阳站-重现赛
    2sat学习笔记
    bzoj4176
    bzoj3309
    6C
    3U
    3T
    3R
  • 原文地址:https://www.cnblogs.com/LeonNew/p/5912687.html
Copyright © 2020-2023  润新知