准备
关于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。下面对这几个角色完成的数据流发送过程做一次梳理
- MasterBatchCoordinator开始时会发送$bach流给TridentSpoutCoordinator。
- TridentSpoutCoordinator接到MBC的 batch流后,会调用BatchCoordinator的initialTransaction()初始化一个消息,开始一个事务,并继续向外发送$batch给TridentSpoutExecutor。
- TridentSpoutExecutor接到 batch流后,会调用用户代码中的TridentSpoutExecutor#emitBatch()方法,开始发送实际的业务数据。(一个Batch会有多个tuple,因此在emitBatch方法中,会有多次的发射数据流)
- TridentSpoutExecutor 在 发 送 完 一 批 batch 后, finishBatch 被 调 用, 通 过emitDirect会告知下一跳:我已经发送了多少消息给你
- TridentSpoutExecutor紧接着还会给ack_bolt发送ack消息,ack bolt将ack消息其传达到MasterBatchCoordinator。
- MasterBatchCoordinator在收到第一个ack后,将processing状态置为processed。然后调用sync()方法
- 当sync()方法接收到事务状态为PROCESSED时,将其改为COMMITTING的状态,并向外发送$commit数据流
- TridentSpoutExecutor接收到$commit数据流,对这一Batch的数据进行提交,TridentSpoutExecutor会调用ITridentSpout中的emmitter, emmitter::commit()被执行,TridentSpoutExecutor会当TridentSpoutExecutor的数据提交成功之后,会再次向MasterBatchCoordinator发送ack消息,MasterBatchCoordinator在收到这个tuple之后,会认为针对某一个txid的tuple的处理已经完全实现,
- 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() 。