storm学习(3) Trident

准备

关于Trident的是spout/bolt的高级抽象,类似于 Hibernate/Mybatis与 JDBC的关系。下面是我学习Trident过程的一些记录

理解TridentTopology

与普通的topology一样,Trident也需要创建一个topology

TridentTopology topology = new TridentTopology();

之前的两篇文章了解了创建的topology需要通过submitTopology()方法提交。 注意TridentTopology是不能直接提交,它需要通过build()方法将TridentTopology转化为普通的StormTopology才能提交

topology.build();

最终生成的stormTopology会生成一个MasterBatchCoordinator作为真正的Spout。那么TridentTopology指定的spout去哪里了?首先看如何创建一个ITridentSpout

DiagnosisEventSpout spout = new DiagnosisEventSpout();
Stream inputStream = topology.newStream("event",spout);

newStream时使用的入参spout会裂变成两个bolt,一是TridentSpoutCoordinator,另一个是TridentSpoutExecutor。

小结

TridentTopology最终会生成一个MasterBatchCoordinator作为真正的Spout,而TridentTopology指定的spout会裂变成两个下游bolt,一是TridentSpoutCoordinator,另一个是TridentSpoutExecutor

理解ITridentSpout

用户定义的DiagnosisEventSpout需要实现ItridentSpout接口,它有2个内部接口,分别是BatchCoordinator和Emitter,分别是用于协调的Spout接口和发送消息的Bolt接口(如何协调,下面会介绍)。代码如下

public class DiagnosisEventSpout implements ITridentSpout<Long>{
    private static final long serialVersionUID = 1L;
    SpoutOutputCollector collector;
    //MasterBatchCoordinator会调用用户定义的BatchCoordinator的isReady()方法,如果返回true的话
    //则会发送一个id为 batch的消息流,从而开始一个数据流转
    BatchCoordinator<Long> coordinatot = new DefaultCoordinator();
    //消息发送节点会接收协调spout的$batch和$success流。
    Emitter<Long> emitter = new DiagnosisEventEmitter();

    @Override
    public BatchCoordinator<Long> getCoordinator(String txStateId, Map conf, TopologyContext context) {
       return coordinatot;
    }

    @Override
    public Emitter<Long> getEmitter(String txStateId, Map conf, TopologyContext context) {
        return emitter;
    }

    @Override
    public Map getComponentConfiguration() {
        return null;
    }

    @Override
    public Fields getOutputFields() {
        return new Fields("event");
    }
}

如代码所示,我们使用了DefaultCoordinator作为BatchCoordinator的实现,使用了DiagnosisEventEmitter作为Emitter的实现。

理解ITridentSpout.BatchCoordinator

BatchCoordinator作为ITridentSpout中的协调者,它有3个方法需要关注,首先是

public boolean isReady(long txid) {
        return true;
 }

MasterBatchCoordinator会调用用户定义的BatchCoordinator的batch的消息流,从而开始一个bath的数据流转。首先接受到的bath的是BatchCoordinator另一个重要的方法

public Long initializeTransaction(long txid, Long prevMetadata, Long currMetadata) {
        LOG.info("Initializing Transaction ["+txid+"]");
        return null;
    }

TridentSpoutCoordinator接到MBC的batch流后,会调用BatchCoordinator的initialTransaction()初始化一个消息,并继续向外发送$batch流。TridentSpoutExecutor接到 batch流后,会调用用户代码中的TridentSpoutExecutor#emitBatch()方法,开始发送实际的业务数据。关于TridentSpoutExecutor后面会介绍。initializeTransaction表示一个batch事务的开始,其中txid为事务序列号,prevMetadata是前一个事务所对应的元数据。若当前事务为第一个事务,则其为空。currMetadata是当前事务的元数据。
最后一个方法是

public void success(long txid) {
        LOG.info("Successful Transaction ["+txid+"]");
 }

该方法会在一个事务完成后被调用,txid表示事务的id,与initializeTransaction的txid相对应。
完整的DefaultCoordinator代码如下:

public class DefaultCoordinator implements ITridentSpout.BatchCoordinator<Long>,Serializable {

    private static final long serialVersionUID =1l;
    private static final Logger LOG = org.slf4j.LoggerFactory.getLogger(DefaultCoordinator.class);


    //initializeTransaction方法返回一个用户定义的事务元数据。X是用户自定义的与事务相关的数据类型,返回的数据会存储到zk中。
    //其中txid为事务序列号,prevMetadata是前一个事务所对应的元数据。若当前事务为第一个事务,则其为空。currMetadata是当前事务的元数据,
    // 如果是当前事务的第一次尝试,则为空,否则为事务上一次尝试所产生的元数据。
    //TridentSpoutCoordinator接到MBC的 batch流后,会调用BatchCoordinator的initialTransaction()初始化一个消息,并继续向外发送
    //batch流。TridentSpoutExecutor接到 batch流后,会调用用户代码中的TridentSpoutExecutor#emitBatch()方法,开始发送实际的业务数据。
    @Override
    public Long initializeTransaction(long txid, Long prevMetadata, Long currMetadata) {
        LOG.info("Initializing Transaction ["+txid+"]");
        return null;
    }

    @Override
    public void success(long txid) {
        LOG.info("Successful Transaction ["+txid+"]");
    }

    //isReady方法用于判断事务所对应的数据是否已经准备好,当为true时,表示可以开始一个新事务。其参数是当前的事务号。
    //BatchCoordinator中实现的方法会被部署到多个节点中运行,
    //其中isReady是在真正的Spout(MasterBatchCoordinator)中执行的,其余方法在TridentSpoutCoordinator中执行。
    @Override
    public boolean isReady(long txid) {
        return true;
    }

    @Override
    public void close() {

    }
}

理解ITridentSpout.Emitter

前面讲到BatchCoordinator继续向外发送$batch流。TridentSpoutExecutor接到 batch流后,会调用用户代码中的TridentSpoutExecutor#emitBatch()方法。该方法即定义在ITridentSpout.Emitter中

 public void emitBatch(TransactionAttempt tx, Long coordinatorMeta, TridentCollector collector) {
        for(int i=0 ; i<10000;i++){
        //for(int i=0;i<9;i++){
            List<Object> events = new ArrayList<Object>();
            double lat = new Double(-30+(int)(Math.random()*75 ));
            double lng = new Double(-120+(int)(Math.random()*70));
            long time = System.currentTimeMillis();
            String diag = new Integer(320+(int)(Math.random()*7)).toString();
            DiagnosisEvent event = new DiagnosisEvent(lat,lng,time,diag);
            events.add(event);
            //System.out.println(i);
            collector.emit(events);
        }
    }

完成代码如下:

public class DiagnosisEventSpout implements ITridentSpout<Long>{

    private static final long serialVersionUID = 1L;
    SpoutOutputCollector collector;
    //MasterBatchCoordinator会调用用户定义的BatchCoordinator的isReady()方法,如果返回true的话
    //则会发送一个id为 batch的消息流,从而开始一个数据流转
    BatchCoordinator<Long> coordinatot = new DefaultCoordinator();
    //消息发送节点会接收协调spout的$batch和$success流。
    Emitter<Long> emitter = new DiagnosisEventEmitter();

    @Override
    public BatchCoordinator<Long> getCoordinator(String txStateId, Map conf, TopologyContext context) {
       return coordinatot;
    }

    @Override
    public Emitter<Long> getEmitter(String txStateId, Map conf, TopologyContext context) {
        return emitter;
    }

    @Override
    public Map getComponentConfiguration() {
        return null;
    }

    @Override
    public Fields getOutputFields() {
        return new Fields("event");
    }
}

小结

讲到这里,可能会有点迷惑,因为角色已经比较多了,分别是MasterBatchCoordinator,TridentSpoutCoordinator,TridentSpoutExecutor,用户定义的BatchCoordinator,和用户定义的Emitter。下面对这几个角色完成的数据流发送过程做一次梳理

  1. MasterBatchCoordinator开始时会发送$bach流给TridentSpoutCoordinator。
  2. TridentSpoutCoordinator接到MBC的 batch流后,会调用BatchCoordinator的initialTransaction()初始化一个消息,开始一个事务,并继续向外发送$batch给TridentSpoutExecutor。
  3. TridentSpoutExecutor接到 batch流后,会调用用户代码中的TridentSpoutExecutor#emitBatch()方法,开始发送实际的业务数据。(一个Batch会有多个tuple,因此在emitBatch方法中,会有多次的发射数据流)
  4. TridentSpoutExecutor 在 发 送 完 一 批 batch 后, finishBatch 被 调 用, 通 过emitDirect会告知下一跳:我已经发送了多少消息给你
  5. TridentSpoutExecutor紧接着还会给ack_bolt发送ack消息,ack bolt将ack消息其传达到MasterBatchCoordinator。
  6. MasterBatchCoordinator在收到第一个ack后,将processing状态置为processed。然后调用sync()方法
  7. 当sync()方法接收到事务状态为PROCESSED时,将其改为COMMITTING的状态,并向外发送$commit数据流
  8. TridentSpoutExecutor接收到$commit数据流,对这一Batch的数据进行提交,TridentSpoutExecutor会调用ITridentSpout中的emmitter, emmitter::commit()被执行,TridentSpoutExecutor会当TridentSpoutExecutor的数据提交成功之后,会再次向MasterBatchCoordinator发送ack消息,MasterBatchCoordinator在收到这个tuple之后,会认为针对某一个txid的tuple的处理已经完全实现,
  9. MasterBatchCoordinator收到ack消息后,向外发送$success流,告知TridentSpoutCoordinator,所有的活都已经都完成了,收工。

CoordinateBolt具体原理如下:
1. 真正执行计算的bolt外面封装了一个CoordinateBolt。真正执行任务的bolt我们称为real bolt。
2. 每个CoordinateBolt记录两个值:有哪些task给我发送了tuple(根据topology的grouping信息);我要给哪些tuple发送信息(同样根据groping信息)
3. Real bolt发出一个tuple后,其外层的CoordinateBolt会记录下这个tuple发送给哪个task了。
5. 等所有的tuple都发送完了之后,CoordinateBolt通过另外一个特殊的stream以emitDirect的方式告诉所有它发送过tuple的task,它发送了多少tuple给这个task。下游task会将这个数字和自己已经接收到的tuple数量做对比,如果相等,则说明处理完了所有的tuple。
6. 下游CoordinateBolt会重复上面的步骤,通知其下游

理解BaseFilter

前面我们一定定义并且解释了Spout, 它向外发送的的数据流是一个对象DiagnosisEvent:

public void emitBatch(TransactionAttempt tx, Long coordinatorMeta, TridentCollector collector) {
        for(int i=0 ; i<10000;i++){
        //for(int i=0;i<9;i++){
            List<Object> events = new ArrayList<Object>();
            double lat = new Double(-30+(int)(Math.random()*75 ));
            double lng = new Double(-120+(int)(Math.random()*70));
            long time = System.currentTimeMillis();
            String diag = new Integer(320+(int)(Math.random()*7)).toString();
            DiagnosisEvent event = new DiagnosisEvent(lat,lng,time,diag);
            events.add(event);
            //System.out.println(i);
            collector.emit(events);
        }
    }

DiagnosisEvent的定义如下

public class DiagnosisEvent implements Serializable {
    private static final long serialVersionUID =1L;
    public double lat ;
    public double lng ;
    public long time ;
    public String diagnosisCode ;

    public DiagnosisEvent(double lat ,double lng ,long time, String diagnosisCode){
        super();
        this.time=time;
        this.lat = lat ;
        this.lng = lng ;
        this.diagnosisCode = diagnosisCode;
    }
}

现在想要对所有的数据流进行过滤,只保留diagnosisCode<=322的数据流,可以通过定义个BaseFilter来实现。

Stream inputStream = topology.newStream("event",spout);
inputStream.each(new Fields("event"),new DiseaseFilter())

DiseaseFilter如下

public class DiseaseFilter extends BaseFilter {
    private static final long serialVersionUID = 1l;
    private static final Logger LOG = LoggerFactory.getLogger(DiseaseFilter.class);


    @Override
    public boolean isKeep(TridentTuple tuple) {
        DiagnosisEvent diagnosis = (DiagnosisEvent)tuple.getValue(0);
        Integer code = Integer.parseInt(diagnosis.diagnosisCode);
        //过滤出 code小于322的疾病
        if(code.intValue() <=322){
            LOG.debug("Emitting disease ["+diagnosis.diagnosisCode+"]");
            return true;
        }else{
            LOG.debug("Filtering disease ["+diagnosis.diagnosisCode+"]");
            return false;
        }
    }
}

此时过滤出了所有diagnosisCode<=322的数据流

理解BaseFunction

在过滤出diagnosisCode<=322的数据流后,希望进一步操作,因为数据流对象DiagnosisEvent只包含了经纬度信息,希望在数据流根据经纬度添加city信息

 inputStream.each(new Fields("event"),new DiseaseFilter())
                //声明需要CityAssignment对数据流中的每个tuple执行操作
                //在每个tuple中,CityAssignment会在event字段上运算并且增加一个叫做city的新字段
                //这个字段会附在tuple中向后发射
                .each(new Fields("event"),new CityAssignment(),new Fields("city"))

CityAssignment代码如下:

public class CityAssignment extends BaseFunction{
    private static final long serilVersionUID = 1L ;
    private static final Logger LOG =LoggerFactory.getLogger(CityAssignment.class);

    private static Map<String,double[]> CITIES = new HashMap<String,double[]>();
    {
        double[] phl ={38.875364, -75.249524};
        CITIES.put("PHL",phl);
        double[] nyc ={40.71448,-74.00598};
        CITIES.put("NYC",nyc);
        double[] sf ={-31.4250142,-62.0841809};
        CITIES.put("SF",sf);
        double[] la ={-34.05374,-118.24307};
        CITIES.put("LA",la);

    }

    @Override
    public void execute(TridentTuple tuple, TridentCollector collector) {
        DiagnosisEvent diagnosisEvent = (DiagnosisEvent)tuple.getValue(0);
        double leastDistance = Double.MAX_VALUE;
        String closestCity = "NONE";

        //找出diagnosisEvent所在的城市
        for(Map.Entry<String,double[]> city: CITIES.entrySet()){
            double R= 6371;
            double x = (city.getValue()[0]-diagnosisEvent.lng)*Math.cos((city.getValue()[0]+diagnosisEvent.lng)/2);
            double y = (city.getValue()[1]-diagnosisEvent.lat);
            double d = Math.sqrt(x*x+y*y)*R;
            if(d<leastDistance){
                leastDistance =d;
                closestCity =city.getKey();
            }
        }

        //Emit the value
        List<Object> values = new ArrayList<Object>();
        values.add(closestCity);
        LOG.debug("Closest city to lat=["+ diagnosisEvent.lat+"], lng=["+diagnosisEvent.lng+"]==["
                +closestCity+"],d=["+leastDistance+"]");
        collector.emit(values);
    }
}

继续操作,在原有的数据流上加上2个key,分别是 hour和 city:diagnosisCode:hourSinceEpoch。

 inputStream.each(new Fields("event"),new DiseaseFilter())
                //声明需要CityAssignment对数据流中的每个tuple执行操作
                //在每个tuple中,CityAssignment会在event字段上运算并且增加一个叫做city的新字段
                //这个字段会附在tuple中向后发射
                .each(new Fields("event"),new CityAssignment(),new Fields("city"))
                //在每一个tuple中,HourAssignment会进行运算,并且增加hour和cityDiseaseHour新字段
                .each(new Fields("event", "city"), new HourAssignment(), new Fields("hour", "cityDiseaseHour"))
                //根据 cityDiseaseHour进行分组,cityDiseaseHour是 城市:疾病代码:小时

HourAssignment代码如下:

public class HourAssignment extends BaseFunction {

    private static final long serialVersionUID =1L;
    private static final Logger LOG = LoggerFactory.getLogger(HourAssignment.class);

    @Override
    public void execute(TridentTuple tuple, TridentCollector collector) {

        DiagnosisEvent diagnosis = (DiagnosisEvent)tuple.getValue(0);
        String city =(String)tuple.getValue(1);

        long timestamp = diagnosis.time;
        //毫秒转换为小时
        long hourSinceEpoch = timestamp/1000/60/60;

        LOG.debug("key ==[" +city+ ":+"+hourSinceEpoch+"]");
        String key = city +":" + diagnosis.diagnosisCode+":"+hourSinceEpoch;

        List<Object> values = new ArrayList<Object>();
        values.add(hourSinceEpoch);
        values.add(key);
        collector.emit(values);

    }
}

理解persistentAggregate

最后的结果通过cityDiseaseHour聚合之后计数,并且打印/持久化

inputStream.each(new Fields("event"),new DiseaseFilter())
                //声明需要CityAssignment对数据流中的每个tuple执行操作
                //在每个tuple中,CityAssignment会在event字段上运算并且增加一个叫做city的新字段
                //这个字段会附在tuple中向后发射
                .each(new Fields("event"),new CityAssignment(),new Fields("city"))
                //在每一个tuple中,HourAssignment会进行运算,并且增加hour和cityDiseaseHour新字段
                .each(new Fields("event", "city"), new HourAssignment(), new Fields("hour", "cityDiseaseHour"))
                //根据 cityDiseaseHour进行分组,cityDiseaseHour是 城市:疾病代码:小时
                .groupBy(new Fields("cityDiseaseHour"))
                //统计并且持久化
                //partitionPersist 是一个接收 Trident 聚合器作为参数并对 state 数据源进行更新的方法
                //Trident 需要你提供一个实现 MapState 接口的 state。被分组的域就是 state 中的 key,
                // 而聚合的结果就是 state 中的 value
                .persistentAggregate(new OutbreakTrendFactory(),new Count(),new Fields("count"))

groupBy(new Fields(“cityDiseaseHour”))方法会将相同cityDiseaseHour分配到一起,因此直接对得到的key进行相加即可。partitionPersist 是一个接收 Trident 聚合器作为参数并对 state 数据源进行更新的方法,Trident 需要你提供一个实现 MapState 接口的 state。实现 MapState 接口非常简单,Trident 几乎已经为你做好了所有的准备工作。OpaqueMap、TransactionalMap、与 NonTransactionalMap类都分别实现了各自的容错性语义。 你只需要为这些类提供一个用于对不同的 key/value 进行 multiGets 与 multiPuts 处理的 IBackingMap 实现类。IBackingMap实现类如下:

public class OutbreakTrendBackingMap implements IBackingMap<Long> {

    private static final Logger LOG = LoggerFactory.getLogger(OutbreakTrendBackingMap.class);
    Map<String,Long> storage = new ConcurrentHashMap<String,Long>();


    //multiUpdate会先调用multiGet,获取keys的values
    //然后调用 multiPut,将keys和values作为参数传入
    @Override
    public List<Long> multiGet(List<List<Object>> keys) {
        System.out.println("multiGet");
        List<Long> values = new ArrayList<Long>();
        for(List<Object> key: keys){
            Long value = storage.get(key.get(0));
            System.out.println(key.get(0)+" "+value);
            if(value==null){
                values.add(new Long(0));
            }else{
                values.add(value);
            }
        }
        return values;
    }

    @Override
    public void multiPut(List<List<Object>> keys, List<Long> vals) {
        System.out.println("multiPut");
        for(int i=0;i<keys.size();i++){
            LOG.info("Persisting ["+keys.get(i).get(0)+"] ==> ["+vals.get(i)+"]");
            storage.put( (String)keys.get(i).get(0),vals.get(i) );
        }
    }
}

IBackingMap会在multiUpdate方法中先调用multiGet,获取keys的values,然后将values操作(这个例子的操作就是根据key进行new Count()操作)然后调用 multiPut,将keys和values作为参数传入multiPut() 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值