转载自:https://blog.csdn.net/jediael_lu/article/details/76687124
http://www.cnblogs.com/hseagle/p/3490635.html
Trident是storm的更高层次抽象,相对storm,它主要提供了3个方面的好处:
(1)提供了更高层次的抽象,将常用的count,sum等封装成了方法,可以直接调用,不需要自己实现。
(2)以批次代替单个元组,每次处理一个批次的数据。
(3)提供了事务支持,可以保证数据均处理且只处理了一次。
文章目录
Spout
在Trident中用户定义的Spout需要实现ItridentSpout接口。ItridentSpout的定义:
public interface ITridentSpout<T> extends ITridentDataSource {
interface BatchCoordinator<X> {
X initializeTransaction(long txid, X prevMetadata, X currMetadata);
void success(long txid);
boolean isReady(long txid);
void close();
}
interface Emitter<X> {
void emitBatch(TransactionAttempt tx, X coordinatorMeta, TridentCollector collector);
void success(TransactionAttempt tx);
void close();
}
BatchCoordinator<T> getCoordinator(String txStateId, Map conf, TopologyContext context);
Emitter<T> getEmitter(String txStateId, Map conf, TopologyContext context);
Map<String, Object> getComponentConfiguration();
Fields getOutputFields();
}
它有2个内部接口,BatchCoordinator和Emitter,分别是用于协调的Spout接口和发送消息的Bolt接口。实现一个Spout的主要工作就在于实现这2个接口,创建实际工作的Coordinator和Emitter。Spout中提供了2个get方法用于分别用于指定使用哪个Coordinator和Emitter类,这些类会由用户定义。
getComponentConfiguration用于获取配置信息,getOutputFields获取输出field。
BatchCoordinator
BatchCoordinator接口中有四个方法,下面几个是比较关键的。
initializeTransaction
方法返回一个用户定义的事务元数据。X是用户自定义的与事务相关的数据类型,返回的数据会存储到zk中。 其中txid为事务序列号,prevMetadata是前一个事务所对应的元数据。若当前事务为第一个事务,则其为空。currMetadata是当前事务的元数据,如果是当前事务的第一次尝试,则为空,否则为事务上一次尝试所产生的元数据。
isReady
方法用于判断事务所对应的数据是否已经准备好,当为true时,表示可以开始一个新事务。其参数是当前的事务号。
Emmitter
Emmitter是消息发送节点,会接收协调spout(即MasterBatchCoordinator)的$batch
和$success
流。
MasterBatchCoordinator(1)当收到$batch
消息时,节点便调用emitBatch方法来发送消息。
MasterBatchCoordinator(2)当收到$success
消息时,会调用success方法对事务进行后处理。
Spout实际的消息流
创建Spout后,它是怎么被加载到拓扑真正的Spout中呢?
1、MasterBatchCoordinator
  org.apache.storm.trident.topology.MasterBatchCoordinator
是一个数据流的真正起点:
- 首先调用open方法完成初始化,包括读取之前的拓扑处理到的事务序列号,最多同时处理的tuple数量,每个事务的尝试次数等。
- 然后nextTuple会改变事务的状态,或者是创建事务并发送$batch流。
- 最后,ack方法会根据流的状态向外发送$commit流,或者是重新调用sync方法,开始创建新的事务。
总而言之,MasterBatchCoordinator作为拓扑数据流的真正起点,通过循环发送协调信息,不断的处理数据流。MasterBatchCoordinator的真正作用在于协调消息的起点,里面所有的map,如_activeTx,_attemptIds等都只是为了保存当前正在处理的情况而已。
来看MasterBatchCoordinator的定义:
public class MasterBatchCoordinator extends BaseRichSpout {
一个Trident拓扑的真正逻辑就是从MasterBatchCoordinator开始的,先调用open方法完成一些初始化,然后是在nextTuple中发送$batch
和$commit
流。来看一下open方法
@Override
public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
_throttler = new WindowedTimeThrottler((Number)conf.get(Config.TOPOLOGY_TRIDENT_BATCH_EMIT_INTERVAL_MILLIS), 1);
// 每个MasterBatchSpout可以处理多个ITridentSpout,这里将多个spout的元数据放到_states这个Map中。
for(String spoutId: _managedSpoutIds) {
_states.add(TransactionalState.newCoordinatorState(conf, spoutId));
}
// 从zk中获取当前的transation事务序号,当拓扑新启动时,需要从zk恢复之前的状态。
// 也就是说zk存储的是下一个需要提交的事务序号,而不是已经提交的事务序号。
_currTransaction = getStoredCurrTransaction();
_collector = collector;
// 任何时刻中,一个spout task最多可以同时处理的tuple数量,即已经emite,但未acked的tuple数量。
Number active = (Number) conf.get(Config.TOPOLOGY_MAX_SPOUT_PENDING);
if(active==null) {
_maxTransactionActive = 1;
} else {
_maxTransactionActive = active.intValue();
}
// 每一个事务的当前尝试编号,即_currTransaction这个事务序号中,各个事务的尝试次数。
_attemptIds = getStoredCurrAttempts(_currTransaction, _maxTransactionActive);
for(int i=0; i<_spouts.size(); i++) {
String txId = _managedSpoutIds.get(i);
// 将各个Spout的Coordinator保存在_coordinators这个List中。
_coordinators.add(_spouts.get(i).getCoordinator(txId, conf, context));
}
LOG.debug("Opened {}", this);
}
再看一下nextTuple()方法,它只调用了sync()方法:
@Override
public void nextTuple() {
sync();
}
private void sync() {
// note that sometimes the tuples active may be less than max_spout_pending, e.g.
// max_spout_pending = 3
// tx 1, 2, 3 active, tx 2 is acked. there won't be a commit for tx 2 (because tx 1 isn't committed yet),
// and there won't be a batch for tx 4 because there's max_spout_pending tx active
// 判断当前事务_currTransaction是否为PROCESSED状态,如果是的话,将其状态改为COMMITTING,然后发送$commit流。
// 接收到$commit流的节点会调用finishBatch方法,进行事务的提交和后处理。
TransactionStatus maybeCommit = _activeTx.get(_currTransaction);
if(maybeCommit!=null && maybeCommit.status == AttemptStatus.PROCESSED) {
maybeCommit.status = AttemptStatus.COMMITTING;
_collector.emit(COMMIT_STREAM_ID, new Values(maybeCommit.attempt), maybeCommit.attempt);
LOG.debug("Emitted on [stream = {}], [tx_status = {}], [{}]", COMMIT_STREAM_ID, maybeCommit, this);
}
// 用于产生一个新事务。
// 最多存在_maxTransactionActive个事务同时运行,当前active的事务序号区间处于[_currTransaction,_currTransaction+_maxTransactionActive-1]之间。
// 注意只有在当前事务结束之后,系统才会初始化新的事务,所以系统中实际活跃的事务可能少于_maxTransactionActive。
if(_active) {
if(_activeTx.size() < _maxTransactionActive) {
Long curr = _currTransaction;
for(int i=0; i<_maxTransactionActive; i++) {
// 如果事务序号不存在_activeTx中,则创建新事务,并发送$batch流。当ack被调用时,这个序号会被remove掉。
if(!_activeTx.containsKey(curr) && isReady(curr)) {
// by using a monotonically increasing attempt id, downstream tasks
// can be memory efficient by clearing out state for old attempts
// as soon as they see a higher attempt id for a transaction
Integer attemptId = _attemptIds.get(curr);
if(attemptId==null) {
attemptId = 0;
} else {
attemptId++;
}
// _activeTx记录的是事务序号和事务状态的map,而_activeTx则记录事务序号与尝试次数的map。
_attemptIds.put(curr, attemptId);
for(TransactionalState state: _states) {
state.setData(CURRENT_ATTEMPTS, _attemptIds);
}
// TransactionAttempt包含事务序号和尝试编号2个变量,对应于一个具体的事务。
TransactionAttempt attempt = new TransactionAttempt(curr, attemptId);
final TransactionStatus newTransactionStatus = new TransactionStatus(attempt);
_activeTx.put(curr, newTransactionStatus);
_collector.emit(BATCH_STREAM_ID, new Values(attempt), attempt);
LOG.debug("Emitted on [stream = {}], [tx_attempt = {}], [tx_status = {}], [{}]", BATCH_STREAM_ID, attempt, newTransactionStatus, this);
_throttler.markEvent();
}
// 如果事务序号已经存在_activeTx中,则curr递增,然后再循环检查下一个。
curr = nextTransactionId(curr);
}
}
}
}
nextTuple()主要完成了以下功能:
- 如果事务状态是PROCESSED,则将其状态改为COMMITTING,然后发送commit流。接收到commit流。接收到commit流的节点会调用finishBatch方法,进行事务的提交和后处理 .
- 如果_activeTx.size()小于_maxTransactionActive,则新建事务,放到_activeTx中,同时向外发送
$batch
流(_collector.emit(BATCH_STREAM_ID, new Values(attempt), attempt);
),等待Coordinator的处理。( 当ack方法被 调用时,这个事务会被从_activeTx中移除)。
注意:当前处于acitve状态的应该是序列在[_currTransaction,_currTransaction+_maxTransactionActive-1]
之间的事务。
继续往下,看看ack方法:
@Override
public void ack(Object msgId) {
// 获取事务的状态
TransactionAttempt tx = (TransactionAttempt) msgId;
TransactionStatus status = _activeTx.get(tx.getTransactionId());
LOG.debug("Ack. [tx_attempt = {}], [tx_status = {}], [{}]", tx, status, this);
if(status!=null && tx.equals(status.attempt)) {
// 如果当前状态是PROCESSING,则改为PROCESSED
if(status.status==AttemptStatus.PROCESSING) {
status.status = AttemptStatus.PROCESSED;
LOG.debug("Changed status. [tx_attempt = {}] [tx_status = {}]", tx, status);
} else if(status.status==AttemptStatus.COMMITTING) {
// 如果当前状态是COMMITTING,则将事务从_activeTx及_attemptIds去掉,并发送$success流。
_activeTx.remove(tx.getTransactionId());
_attemptIds.remove(tx.getTransactionId());
_collector.emit(SUCCESS_STREAM_ID, new Values(tx));
_currTransaction = nextTransactionId(tx.getTransactionId());
for(TransactionalState state: _states) {
state.setData(CURRENT_TX, _currTransaction);
}
LOG.debug("Emitted on [stream = {}], [tx_attempt = {}], [tx_status = {}], [{}]", SUCCESS_STREAM_ID, tx, status, this);
}
// 由于有些事务状态已经改变,需要重新调用sync()继续后续处理,或者发送新tuple。
sync();
}
}
还有fail方法和declareOutputFileds方法, isReady方法。
@Override
public void fail(Object msgId) {
TransactionAttempt tx = (TransactionAttempt) msgId;
TransactionStatus stored = _activeTx.remove(tx.getTransactionId());
LOG.debug("Fail. [tx_attempt = {}], [tx_status = {}], [{}]", tx, stored, this);
if(stored!=null && tx.equals(stored.attempt)) {
_activeTx.tailMap(tx.getTransactionId()).clear();
sync();
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// in partitioned example, in case an emitter task receives a later transaction than it's emitted so far,
// when it sees the earlier txid it should know to emit nothing
declarer.declareStream(BATCH_STREAM_ID, new Fields("tx"));
declarer.declareStream(COMMIT_STREAM_ID, new Fields("tx"));
declarer.declareStream(SUCCESS_STREAM_ID, new Fields("tx"));
}
private boolean isReady(long txid) {
if(_throttler.isThrottled()) return false;
//TODO: make this strategy configurable?... right now it goes if anyone is ready
for(ITridentSpout.BatchCoordinator coord: _coordinators) {
if(coord.isReady(txid)) return true;
}
return false;
}
2、TridentSpoutCoordinator
TridentSpoutCoordinator接收来自MasterBatchCoordinator的$success
流与$batch
流,并通过调用用户代码,实现真正的逻辑。此外还向TridentSpoutExecuter发送$batch
流,以触发后者开始真正发送业务数据流。
public class TridentSpoutCoordinator implements IBasicBolt {
可见TridentSpoutCoordinator是一个bolt,在创建TridentSpoutCoordinator时,需要传递一个ITridentSpout对象:
public TridentSpoutCoordinator(String id, ITridentSpout<Object> spout) {
_spout = spout;
_id = id;
}
在prepare方法中使用这个对象来获取到用户定义的Coordinator:
@Override
public void prepare(Map conf, TopologyContext context) {
_coord = _spout.getCoordinator(_id, conf, context);
_underlyingState = TransactionalState.newCoordinatorState(conf, _id);
_state = new RotatingTransactionalState(_underlyingState, META_DIR);
}
_state和_underlyingState保存了zk中的元数据信息。
在execute方法中,TridentSpoutCoordinator接收$success
流与$batch
流:
@Override
public void execute(Tuple tuple, BasicOutputCollector collector) {
TransactionAttempt attempt = (TransactionAttempt) tuple.getValue(0);
if(tuple.getSourceStreamId().equals(MasterBatchCoordinator.SUCCESS_STREAM_ID)) {
_state.cleanupBefore(attempt.getTransactionId());
_coord.success(attempt.getTransactionId());
} else {
long txid = attempt.getTransactionId();
Object prevMeta = _state.getPreviousState(txid);
Object meta = _coord.initializeTransaction(txid, prevMeta, _state.getState(txid));
_state.overrideState(txid, meta);
collector.emit(MasterBatchCoordinator.BATCH_STREAM_ID, new Values(attempt, meta));
}
}
接收到$success
流时,清理了zk中的数据,然后调用用户定义的Coordinator中的success方法。
接收到$batch
流时,初始化一个事务并将其发送出去。由于在trident中消息有可能是重放的,因此需要prevMeta。注意,trident是在bolt中初始化一个事务的。
3、TridentSpoutExecutor
TridentSpoutExecutor也是一个bolt,接收来自TridentSpoutCoordinator的消息流,包括$commit
,$success
与$batch
流,前面2个分别调用用户定义的emmitter的commit与success方法,$batch
流时则调用emmitter的emitBatch方法,开始向外发送业务数据。
核心的execute方法:
@Override
public void execute(BatchInfo info, Tuple input) {
// there won't be a BatchInfo for the success stream
TransactionAttempt attempt = (TransactionAttempt) input.getValue(0);
if(input.getSourceStreamId().equals(MasterBatchCoordinator.COMMIT_STREAM_ID)) {
if(attempt.equals(_activeBatches.get(attempt.getTransactionId()))) {
((ICommitterTridentSpout.Emitter) _emitter).commit(attempt);
_activeBatches.remove(attempt.getTransactionId());
} else {
throw new FailedException("Received commit for different transaction attempt");
}
} else if(input.getSourceStreamId().equals(MasterBatchCoordinator.SUCCESS_STREAM_ID)) {
// valid to delete before what's been committed since
// those batches will never be accessed again
_activeBatches.headMap(attempt.getTransactionId()).clear();
_emitter.success(attempt);
} else {
_collector.setBatch(info.batchId);
_emitter.emitBatch(attempt, input.getValue(1), _collector);
_activeBatches.put(attempt.getTransactionId(), attempt);
}
}
4、总结
MasterBatchCoordinator才是真正的spout,另外2个都是bolt。
MasterBatchCoordinator会调用用户定义的BatchCoordinator的isReady()方法,返回true的话,则会在nextTuple()发送一个id为batch的消息流,作为整个数据流的起点。MasterBatchCoordinator会先判断正在处理的事务数是否少于maxTransactionActive,是的话就继续向外发送batch流。
TridentSpoutCoordinator接到MasterBatchCoordinator的batch流后,在execute()方法中会调用BatchCoordinator的initialTransaction()生成meta消息,并继续向外发送 batch流。
TridentSpoutExecutor接到TridentSpoutCoordinator转发的batch后,会调用用户Emitter类中的emitBatch()方法,开始发送实际的业务数据。
当整个消息被成功处理完后,会调用MasterBatchCoordinator的ack()方法,ack方法会将事务的状态从PROCESSING改为PROCESSED,然后将其改为COMMITTING的状态,并向外发送id为commit的流。当然,如果fail掉了,则会调用fail()方法。
TridentSpoutCoordinator收到commit流的节点会开始提交操作,但trident会按事务号顺序提交事务的,所以由提交bolt来决定是否现在提交,还是先缓存下来之后再提交。当commit流处理完后,MasterBatchCoordinator的ack()方法会被再次调用,同时向外发送success流。
TridentSpoutExecutor收到$success流时,会调用Emitter的success方法。至此整个流程全部完成。
Bolt
(一)概述
1、组件的基本关系
Trident拓扑最终会转化为一个spout和多个bolt,每个bolt对应一个SubTopologyBolt,它通过TridentBoltExecutor适配成一个bolt。而每个SubTopologyBolt则由很多节点组成,具体点说这个节点包括(Stream|Node)两部分,注意,Node不是Stream自身的成员变量,而是一个具体的处理节点。Stream定义了哪些数据流,Node定义和如何进行操作,Node包含了一个ProjectedProccessor等处理器,用于定义如何进行数据处理。
一个SubTopologyBolt包含多个Group,但大多数情况下是一个Group。看TridentTopology#genBoltIds()的代码。在一个SubTopologyBolt中,含有多个节点组是可能的。例如在含有DRPC的Topology中,查询操作也存储操作可以被分配到同一个SubTopologyBolt中。于是该bolt可能收到来自2个节点组的消息。
一个Group有多个Node。符合一定条件的Node会被merge()成一个Group,每个Node表示一个操作。
每个Node与一个Stream一一对应。注意Stream不是指端到端的完整流,而是每一个步骤的处理对象,所有的Stream组合起来才形成完整的流。
每个Node可能有多个父stream,但多个的情况只在merge()调用multiReduce()时使用。每个Stream与node之间创建一条边。见TridentTopology#addSourceNode()方法。
2、用户视角与源码视角
在用户角度来看,通过newStream(),each(),filter()对Stream进行操作。而在代码角度,这些操作会被转化为各种Node节点,它些节点组合成一个SubTopologyBolt,然后经过TridentBoltExecutor适配后成为一个bolt。
从用户层面来看TridentTopology,有两个重要的概念一是Stream,另一个是作用于Stream上的各种Operation。在实现层面来看,无论是stream,还是后续的operation都会转变成为各个Node,这些Node之间的关系通过重要的数据结构图来维护。具体到TridentTopology,实现图的各种操作的组件是jgrapht。
说到图,两个基本的概念会闪现出来,一是结点,二是描述结点之间关系的边。要想很好的理解TridentTopology就需要紧盯图中结点和边的变化。
TridentTopology在转换成为普通的StormTopology时,需要将原始的图分成各个group,每个group将运行于一个独立的bolt中。TridentTopology又是如何知道哪些node应该在同一个group,哪些应该处在另一个group中的呢;如何来确定每个group的并发度(parallismHint)的呢。这些问题的解决都与jgrapht分不开。
在用户看来,所有的操作就是各种各样的数据流与operation的组合,这些组合会被封装成一个Node(即一个Node包含输入流+操作+输出流),符合一定规则的Node会被组合与一个组,组会被放到一个bolt中。
一个blot节点中可能含有多个操作,各个操作间需要进行消息传递
(二)基础类
1、Stream
Stream主要定义了数据流的各种操作,如each(),project()等。
public class Stream implements IAggregatableStream, ResourceDeclarer<Stream> {
final Node _node;
final String _name;
private final TridentTopology _topology;
三个成员变量:
- Node对象,这表明Stream与Node是一一对应的,每个节点对应一个Stream对象。
- name:这个Stream的名称,也等于是这这个Node的名称。
- TridentTopology: 这个Stram所属的拓扑,使用这个变量,可以调用addSourceNode()等方法。
stream中定义了各种各样的trident操作。
projectionValidation()这个方法用于检查field是否存在。project()是只保留指定的fileds:
public Stream project(Fields keepFields) {
projectionValidation(keepFields);
return _topology.addSourcedNode(this, new ProcessorNode(_topology.getUniqueStreamId(), _name, keepFields, new Fields(), new ProjectedProcessor(keepFields)));
}
首先检查一下需要project的field是否存在。然后就在TridentTopology中新增一个节点。
topology.addSourcedNode方法第一个参数就是Stream自身,第二个参数是一个Node的子类ProcessorNode。创建ProcessorNode时,最后一个参数ProjectedProcessor用于指定如何对流进行操作。
addSourcedNode把source和node同时添加进一个拓扑,即一个流与一个节点。注意这里的节点不是source这个Stream自身的成员变量_node,而是一个新建的节点,比如在project()方法中的节点就是一个使用ProjectedProcessor创建的ProcessorNode。
2、Node SpoutNode PartitionNode ProcessorNode
Node表示拓扑中的一个节点,后面3个均是其子类。
事实上拓扑中的节点均用于产生数据或者对数据进行处理。一个拓扑有多个spout/bolt,每个spout/bolt有一个或者多个Group,一个Group有多个Node。
3、Group
节点组是构建SubTopologyBolt的基础,也是Topology中执行优化的基本操作单元,Trident会通过不断的合并节点组来达到最优处理的目的。Group中包含了一组连通的节点。
public class Group {
public final Set<Node> nodes = new HashSet<>();
private final DirectedGraph<Node, IndexedEdge> graph;
private final String id = UUID.randomUUID().toString();
nodes表示节点组中含有的节点,graph表示拓扑的有向图(是整个拓扑的构成的图),id用于唯一标识一个group。
初始状态时,每个Group只有一个Node。outgoingNodes()方法是通过遍历组中节点的方式来获取该节点组所有节点的子节点。incommingNodes()
用于获取该节点组中所有节点的父节点。
4、GraphGrouper
GraphGrouper提供了对节点组进行操作及合并的基本方法。
public class GraphGrouper {
final DirectedGraph<Node, IndexedEdge> graph;
final Set<Group> currGroups;
final Map<Node, Group> groupIndex = new HashMap<>();
graph:与Group相同,即这个拓扑的整个图。
currGroups:当前graph对应的节点组。节点组之间是没有交集的。
groupIndex:是一个反向索引,用于快速查询每个节点所在的节点组。
mergeFully是GraphGrouper的核心算法,它用来计算何时可以对2个节点组进行合并。基本思想是:如果一个节点组只有一个父节点组,那么将这个节点组与父节点组合并;如果一个节点组只有一个子节点组,那么将子节点组与自身节点组合并。反复进行这个过程。
在TridentTopology中设置Spout、Bolt
从TridentTopology到vanilla topology(普通的topology)由三个层次组
成:
1. 面向最终用户的概念stream, operation
2. 利用planner将tridenttopology转换成vanilla topology
3. 执行vanilla topology
从TridentTopology到基本的Topology有三层,下图是一个全局视图。
如何将Stream与原有的Spout及Bolt联系起来呢?关键在于TridentTopology#build
方法。
在创建TridentTopology时,newStream及其后的函数实际上创建了一个含有三大类节点的List, 这三类节点分别是operation, partition, spout。在TridentTopology#build
函数中将节点分类分别加入到boltNodes或spoutNodes,创建一个有向非循环图(DAG)。注意此处的spout或bolt不能等同于普通的spout和bolt。
TridentTopology#build
中会调用TridentTopologyBuilder::buildTopology
,该方法利用在build函数中创建的boltNodes,spoutNodes及生成的graph来创建vanilla topology所需要的bolt及spout。在buildTopology中会看到类似的代码片段:
SpoutDeclarer masterCoord = builder.setSpout(masterCoordinator(batch), new MasterBatchCoordinator(commitIds, batchesToSpouts.get(batch)));
BoltDeclarer scd =
builder.setBolt(spoutCoordinator(id), new TridentSpoutCoordinator(c.commitStateId, (ITridentSpout) c.spout))
.globalGrouping(masterCoordinator(c.batchGroupId), MasterBatchCoordinator.BATCH_STREAM_ID)
.globalGrouping(masterCoordinator(c.batchGroupId), MasterBatchCoordinator.SUCCESS_STREAM_ID);
BoltDeclarer bd = builder.setBolt(id,
new TridentBoltExecutor(
new TridentSpoutExecutor(
c.commitStateId,
c.streamName,
((ITridentSpout) c.spout)),
batchIdsForSpouts,
specs),
c.parallelism);
最终生成的普通Topology,与普通Topology中的Spout相对应的是MasterBatchCoordinator,而在创建TridentTopology使用的spout则成了Bolt,使用于Stream上的各种Operation也存在于多个普通Bolt中。