Storm一致性事物

  Storm是一个分布式的流处理系统,利用anchor和ack机制保证所有的tuple都被处理成功。如果tuple出错,则可以被重传,但是如何保证出错的tuple只被处理一次呢?换句话说Storm如何保证事物性呢?本节从简单的事物实现入手,最后引出事物型Topology的原理。

一.一致性事物设计

  1.简单设计一,强顺序流

  保证tuple只被处理一次,最简单的方式是将tuple刘变成强顺序的,并且每次只处理一个tuple。从1开始,给给个tuple都加上一个顺序id。在处理tuple的时候,把处理成功的tupleID和计算结果都存储到数据库中。下一个tuple到来的时候,将他和数据库中的id做比较。如果相同说明tuple已经被成功处理,可以忽略,如果不同,根据强顺序性,说明这个tuple没有被处理过,将它的id和计算结果存储到数据库。这种当时的缺点就是每次智能处理一个tuple,没办法实现分布式。

  2.简单设计二,强顺序batch流  

  为了实现分布式,我们可以一次处理一批tuple,称为一个batch。一个batch中的每个tuple可以被并行处理。

  为了保证每个batch只被处理一次,实现方式和上面的一样,只不过数据库中存储的是batch id。batch的中间计算结果先存在局部变量中,当一个batch中所有的tuple都被处理完之后,判断batch id,如果和数据库中的结果不同,则将中间结果更新到数据库。

  如何保证bath中的每个tuple都被处理完呢?可以利用Storm提供的CoordinatedBolt,如下图:
  

  但是强顺序batch流也有局限,每次只能处理一个batch,batch之间无法并行。要想实现真正的分布式事务处理,可以使用storm提供的Transactional Topology。在此之前,我们先详细介绍一下CoordinateBolt的原理。

  3.CoordinateBolt原理

  CoordinateBolt的具体原理如下:

  a.真正执行计算的Bolt外面封装了一个CoordinateBolt。我们将真正执行任务的Bolt称为Real Bolt。

  b.每个CoordinateBolt记录两个值,有哪些Task给我们发送了tuple,以及我们要给那些Task发送信息

  c.Real Bolt发出一个tuple后,其外层的CoordinateBolt会记录下这些tuple发送给了那些Task

  d.等所有的tuple都发送完之后,CoordinateBolt通过另外一个特殊的Stream以emitDirect的方式告诉所有它发送过tuple的Task,它发送了多少tuple给这些task。下游这些task会将这个数字和自己已经接收到的tuple数量对比。如果相等,则说明处理完了所有的tuple。

  e.下游的task会重复上面的步骤,通知其下游

  整个过程如下图所示:

  

  CoordinateBolt主要用于一下场景。1:DRPC,2:Transactional Topology。

  CoordinateBolt对于业务是有入侵的,要使用CoordinateBolt提供的功能,你必须包保证你的每个Bolt发送的每个tuple的第一个field是request-id,所谓我已经处理完我的上游的意思是说当前这个Bolt对于当前这个request-id所需要做的工作做完了。这个request-id在DRPC里面代表一个请求,在Transactional Topology里面代表一个batch。

  4.Transactional Topology

  Storm的Transactional Topology将batch的计算分为两个阶段:process阶段和commit阶段。process阶段可以同时处理多个batch,不用保证顺序性,commit阶段保证batch的强顺序性,并且在commit阶段每次只能处理一个batch。

  下面以storm自带的例子TransactionalGlobalCount,来说明。

   MemoryTransactionalSpout spout = new MemoryTransactionalSpout(DATA, new Fields("word"), PARTITION_TAKE_PER_BATCH);
    TransactionalTopologyBuilder builder = new TransactionalTopologyBuilder("global-count", "spout", spout, 3);
    builder.setBolt("partial-count", new BatchCount(), 5).noneGrouping("spout");
    builder.setBolt("sum", new UpdateGlobalCount()).globalGrouping("partial-count");

    LocalCluster cluster = new LocalCluster();

    Config config = new Config();
    config.setDebug(true);
    config.setMaxSpoutPending(3);

    cluster.submitTopology("global-count-topology", config, builder.buildTopology());

    Thread.sleep(3000);
    cluster.shutdown();
  TransactionalTopologyBuilder共接收了如下的四个参数:

  1.这个Transactional Topology的id,用来在zookeeper中保存当前topology的进度,如果这个topology重启,可以接着之前的进度执行

  2.Spout在这个Topology中的id

  3.一个Transactional Spout。一个Transactional Topology只能有一个Transactional Spout,本例中使用的是MemoryTransactionalSpout,从内存变量DATA中读取数据。

  4.Transactional Spout的并行度(可选)

  下面是BatchCount的定义:

public static class BatchCount extends BaseBatchBolt {
    Object _id;
    BatchOutputCollector _collector;

    int _count = 0;

    @Override
    public void prepare(Map conf, TopologyContext context, BatchOutputCollector collector, Object id) {
      _collector = collector;
      _id = id;
    }

    @Override
    public void execute(Tuple tuple) {
      _count++;
    }

    @Override
    public void finishBatch() {
      _collector.emit(new Values(_id, _count));
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
      declarer.declare(new Fields("id", "count"));
    }
  }

  BatchCount的prepare方法最后一个参数是batchid,在Transactional Topology里面这个id是一个TransactionAttempt对象,Transactional Topology里面发送的tuple,读必须以TransactionAttempt作为第一个field,Storm根据这个field爱判断tuple属于哪个batch。

  TransactionAttempt里面包含两个值,一个是txid,两外一个是attemptid,txid就是上面我们介绍的对于每一个batch里的tuple是唯一的。attemptid是每个batch唯一的一个ID,但是对每一个batch,它reply之后的attemptid和reply之前的attemptid是不一样的,我们可以吧attemptid理解为reply-times,Storm利用这个ID来区别一个batch发射的tuple的版本。

  execute方法会为batch中的每一个tuple执行一次,你应该把这个batch里面的计算状态保持在一个本地变量里面。对于这个例子来说,它在execute方法里面递增tuple的个数。

  最后,当这个Bolt接收到每个batch的所有的tuple之后,finishBatch方法会被执行。

  UpdateGlobalCount的定义如下:

public static class UpdateGlobalCount extends BaseTransactionalBolt implements ICommitter {
    TransactionAttempt _attempt;
    BatchOutputCollector _collector;

    int _sum = 0;

    @Override
    public void prepare(Map conf, TopologyContext context, BatchOutputCollector collector, TransactionAttempt attempt) {
      _collector = collector;
      _attempt = attempt;
    }

    @Override
    public void execute(Tuple tuple) {
      _sum += tuple.getInteger(1);
    }

    @Override
    public void finishBatch() {
      Value val = DATABASE.get(GLOBAL_COUNT_KEY);
      Value newval;
      if (val == null || !val.txid.equals(_attempt.getTransactionId())) {
        newval = new Value();
        newval.txid = _attempt.getTransactionId();
        if (val == null) {
          newval.count = _sum;
        }
        else {
          newval.count = _sum + val.count;
        }
        DATABASE.put(GLOBAL_COUNT_KEY, newval);
      }
      else {
        newval = val;
      }
      _collector.emit(new Values(_attempt, newval.count));
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
      declarer.declare(new Fields("id", "sum"));
    }
  }

   UpdateGlobalCount实现了ICommitter接口,所以Storm会在commit阶段调用finishBatch方法,儿execute方法可以在任何阶段完成。

   在UpdateGlobalCount的finishBatch方法中将当前的txid与数据库中的id作比较。如果相同,则忽略这个batch,如果不同则吧这个batch的计算结果合并到总的结果中,并更新数据库。

  Transactional Topology的运行示意图如下:
  

  
  Transactional Topology的一些特性:
  1.Transactional Topology将事物机制封装好,其内部使用CoordinateBOlt保证一个batch中的tuple被处理完

  2.TransactionalSpout只有一个,它将所有的tuple分组为一个一个的batch,而且保证同一个batch的txid始终一样

  3.BatchBolt处理一个batch中的每一个tuple,对每一个tuple调用execute方法,并在所有的tuple都处理完后调用finishBatch方法

  4.如果batchBolt实现了ICommitter接口,则只能在commit阶段调用finishBatch

原文地址:https://www.cnblogs.com/senlinyang/p/8081701.html