Storm0.7.0实现了一个新特性——事务性拓扑,这一特性使消息在语义上确保你可以安全的方式重发消息,并保证它们只会被处理一次。在不支持事务性拓扑的情况下,你无法在准确性,可扩展性,以空错性上得到保证的前提下完成计算。
NOTE:事务性拓扑是一个构建于标准Storm spout和bolt之上的抽象概念。
设计
在事务性拓扑中,Storm以并行和顺序处理混合的方式处理元组。spout并行分批创建供bolt处理的元组(译者注:下文将这种分批创建、分批处理的元组称做批次)。其中一些bolt作为提交者以严格有序的方式提交处理过的批次。这意味着如果你有每批五个元组的两个批次,将有两个元组被bolt并行处理,但是直到提交者成功提交了第一个元组之后,才会提交第二个元组。
NOTE: 使用事务性拓扑时,数据源要能够重发批次,有时候甚至要重复多次。因此确认你的数据源——你连接到的那个spout——具备这个能力。 这个过程可以被描述为两个阶段: 处理阶段 纯并行阶段,许多批次同时处理。 提交阶段 严格有序阶段,直到批次一成功提交之后,才会提交批次二。 这两个阶段合起来称为一个Storm事务。
NOTE: Storm使用zookeeper储存事务元数据,默认情况下就是拓扑使用的那个zookeeper。你可以修改以下两个配置参数键指定其它的zookeeper——transactional.zookeeper.servers和transactional.zookeeper.port。
接下来就看看如何在一个事务性拓扑中实现spout。
Spout
一个事务性拓扑的spout与标准spout完全不同。
public class TestTransactionalSpout extends BaseTransactionalSpout<TransactionMetadata>{
正如你在这个类定义中看到的,TestTransactionalSpout继承了带范型的BaseTransactionalSpout。指定的范型类型的对象是事务元数据集合。
协调者Coordinator
下面是本例的协调者实现。
public static class TestTransactionalSpoutCoordinator implements ITransactionalSpout.Coordinator<TransactionMetadata> {
TransactionMetadata lastTransactionMetadata;
public TestTransactionalSpoutCoordinator () {
}
@Override
public TransactionMetadata initializeTransaction(BigInteger txid, TransactionMetadata prevMetadata) {
//处理代码
}
@Override
public boolean isReady() {
return true;
}
@Override
public void close() {
}
}
值得一提的是,在整个拓扑中只会有一个提交者实例。
第一个方法是isReady。在initializeTransaction之前调用它确认数据源已就绪并可读取。此方法应当相应的返回true或false。
最后,执行initializeTransaction。正如你看到的,它接收txid和prevMetadata作为参数。第一个参数是Storm生成的事务ID,作为批次的惟一性标识。prevMetadata是协调器生成的前一个事务元数据对象。
在这个例子中,首先确认有多少tweets可读。只要确认了这一点,就创建一个TransactionMetadata对象。元数据对象一经返回,Storm把它跟txid一起保存在zookeeper。这样就确保了一旦发生故障,Storm可以利用分发器(译者注:Emitter,见下文)重新发送批次。
Emitter
创建事务性spout的最后一步是实现分发器(Emitter)。实现如下:
public static class TestTransactionalSpoutEmitter implements ITransactionalSpout.Emitter<TransactionMetadata> {
public TestTransactionalSpoutEmitter() {}
@Override
public void emitBatch(TransactionAttempt tx, TransactionMetadata coordinatorMeta, BatchOutputCollector collector) {
//处理代码
/**
分发器从数据源读取数据并从数据流组发送数据。分发器应当问题能够为相同的事务id和事务元数据发送相同的批次。这样,如果在处理批次的过程中发生了故障,Storm就能够利用分发器重复相同的事务id和事务元数据,并确保批次已经重复过了.
*/
}
@Override
public void cleanupBefore(BigInteger txid) {}
@Override
public void close() {
}</pre>
<pre>
}
在这里emitBatch是个重要方法。
Bolts
首先看一下这个拓扑中的标准bolt:
public class TestSplitterBolt implements IBasicBolt{
private static final long serialVersionUID = 1L;
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declareStream("users", new Fields("txid","id","test"));
}
@Override
public Map<String, Object> getComponentConfiguration() {
return null;
}
@Override
public void prepare(Map stormConf, TopologyContext context) {}
@Override
public void execute(Tuple input, BasicOutputCollector collector) {
//业务代码
}
@Override
public void cleanup(){}
}
TestSplitterBolt接收元组。HashtagSplitterBolt的实现。
public class HashtagSplitterBolt implements IBasicBolt{
private static final long serialVersionUID = 1L;
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declareStream("hashtags", new Fields("txid","tweet_id","hashtag"));
}
@Override
public Map<String, Object> getComponentConfiguration() {
return null;
}
@Override
public void prepare(Map stormConf, TopologyContext context) {}
@Oerride
public void execute(Tuple input, BasicOutputCollector collector) {
//业务代码
}
@Override
public void cleanup(){}
}
现在看看TestHashTagJoinBolt的实现。首先要注意的是它是一个BaseBatchBolt。这意味着,execute方法会操作接收到的元组,但是不会分发新的元组。批次完成时,Storm会调用finishBatch方法。
public void execute(Tuple tuple) {
//业务代码
}
在批次处理完成时,调用finishBatch方法。
@Override
public void finishBatch() {
//后续处理代码
}
提交者bolts
我们已经学习了,批次通过协调器和分发器怎样在拓扑中传递。在拓扑中,这些批次中的元组以并行的,没有特定次序的方式处理。
在这里向数据库保存提交的最后一个事务ID。为什么要这样做?记住,如果事务失败了,Storm将会尽可能多的重复必要的次数。如果你不确定已经处理了这个事务,你就会多算,事务拓扑也就没有用了。所以请记住:保存最后提交的事务ID,并在提交前检查。
分区的事务Spouts
对一个spout来说,从一个分区集合中读取批次是很普通的。通过实现IPartitionedTransactionalSpout,Storm提供了一些工具用来管理每个分区的状态并保证重播的能力。
下面我们修改TestTransactionalSpout,使它可以处理数据分区。
首先,继承BasePartitionedTransactionalSpout,它实现了IPartitionedTransactionalSpout。
public class TestPartitionedTransactionalSpout extends
BasePartitionedTransactionalSpout<TransactionMetadata> {
...
}
然后告诉Storm谁是你的协调器。
public static class TestPartitionedTransactionalCoordinator implements Coordinator {
@Override
public int numPartitions() {
return 4;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void close() {}
}
在这个例子里,协调器很简单。numPartitions方法,告诉Storm一共有多少分区。而且你要注意,不要返回任何元数据。对于IPartitionedTransactionalSpout,元数据由分发器直接管理。
下面是分发器的实现:
public static class TestPartitionedTransactionalEmitter
implements Emitter<TransactionMetadata> {
@Override
public TransactionMetadata emitPartitionBatchNew(TransactionAttempt tx,
BatchOutputCollector collector, int partition,
TransactionMetadata lastPartitioonMeta) {
//业务处理代码
}
@Override
public void emitPartitionBatch(TransactionAttempt tx, BatchOutputCollector collector,
int partition, TransactionMetadata partitionMeta) {
//业务处理代码
}
@Override
public void close() {}
}
这里有两个重要的方法,emitPartitionBatchNew,和emitPartitionBatch。对于emitPartitionBatchNew,从Storm接收分区参数,该参数决定应该从哪个分区读取批次。在这个方法中,决定获取哪些数据,生成相应的元数据对象,调用emitPartitionBatch,返回元数据对象,并且元数据对象会在方法返回时立即保存到zookeeper。
Storm会为每一个分区发送相同的事务ID,表示一个事务贯穿了所有数据分区。通过emitPartitionBatch读取分区中的数据,并向拓扑分发批次。如果批次处理失败了,Storm将会调用emitPartitionBatch利用保存下来的元数据重复这个批次。
模糊的事务性拓扑
到目前为止,你可能已经学会了如何让拥有相同事务ID的批次在出错时重播。但是在有些场景下这样做可能就不太合适了。然后会发生什么呢?
事实证明,你仍然可以实现在语义上精确的事务,不过这需要更多的开发工作,你要记录由Storm重复的事务之前的状态。既然能在不同时刻为相同的事务ID得到不同的元组,你就需要把事务重置到之前的状态,并从那里继续。另外,在之前的一个事务被取消时,每个并行处理的事务都要被取消。这是为了确保你没有丢失任何数据。
你的spout可以实现IOpaquePartitionedTransactionalSpout,而且正如你看到的,协调器和分发器也很简单。
public static class TestOpaquePartitionedTransactionalSpoutCoordinator implements IOpaquePartitionedTransactionalSpout.Coordinator {
@Override
public boolean isReady() {
return true;
}
}
public static class TestOpaquePartitionedTransactionalSpoutEmitter
implements IOpaquePartitionedTransactionalSpout.Emitter<TransactionMetadata> {
@Override
public TransactionMetadata emitPartitionBatch(TransactionAttempt tx,
BatchOutputCollector collector, int partion,
TransactionMetadata lastPartitonMeta) {
//处理代码
return null;
}
private void emitMessage(TransactionAttempt tx, BatchOutputCollector collector,
int partition, TransactionMetadata partitionMeta) {
//处理代码
}
@Override
public int numPartitions() {
return 4;
}
@Override
public void close() {}
}
最有趣的方法是emitPartitionBatch,它获取之前提交的元数据。你要用它生成批次。这个批次不需要与之前的那个一致,你可能根本无法创建完全一样的批次。剩余的工作由提交器bolts借助之前的状态完成。
转载于:https://blog.51cto.com/tener/1736754