概述
设计拓扑时,一件很重要的事情就是要考虑消息的可靠性。如果消息不能被处理而丢失是很严重的问题,我们需要决定如何处理丢失的消息,如何与拓扑作为一个整体处理。例如,处理银行存款的时候,事物一致性是很重要的,不能失去任何消息,任何消息都要被处理。在Storm中,根据每个拓扑的需要,保证消息可靠性,这涉及一个平衡:一个可靠的拓扑必须处理丢失的消息,这就需要更多的资源;一个不可靠的拓扑可能会丢失一些消息,但不占用资源。不管你选择哪一种可靠性策略,Storm都可以提供工具来实现它。
实现
管理Spout的可靠性,可以在发射元组的时候,在元组里面包含一个消息ID,如下:
collector.emit(new Values(...),tupleID);
当元组处理 成功时 调用 ack() 方法,当元组处理 失败时 调用 fail() 方法。当元组被所有的目标Bolt和所有的锚定Bolt所处理时,认为元组处理成功。当如下情况发生时,元组处理会失败:
- collector.fail(tuple)方法被目标Spout调用。
- 处理时间超过配置的超时时间。
我们以银行业务来深入了解,消息的可靠性,有一下要求:
- 如果一个事物失败,则重发消息。
- 如果事物失败了多次,则终止拓扑。
在拓扑中,有1个Spout和1个Bolt。Spout会发送100个随机的事物ID,Bolt在接收一个元组时有80%的可能性会失败。Bolt使用Map来发射事物消息元组,因此很容易对消息进行重发。
Spout的主要成员变量定义如下:
//最大失败次数
private static final Integer MAX_FAILS = 2;
//全部的消息Map
private Map<Integer, String> messages;
//消息的失败次数计数Map
private Map<Integer, Integer> transactionFailureCount;
//发送的消息Map
private Map<Integer, String> toSend;
//SpoutOutputCollector对象
private SpoutOutputCollector collector;
在open()方法中进行一系列的初始化:
@Override
public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
//创建随机数生成器
Random random = new Random();
//出事化相关对象
messages = new HashMap<Integer, String>();
toSend = new HashMap<Integer, String>();
transactionFailureCount = new HashMap<Integer, Integer>();
//遍历100此,模拟100条消息
for (int i=100; i<100; i++){
//添加消息到"全部的消息Map集合"
messages.put(i, "transaction_" + random.nextInt());
//每条消息的错误次数都置为0
transactionFailureCount.put(i, 0);
}
//把"全部的消息Map集合" 添加到"发送的消息Map集合"
toSend.putAll(messages);
//初始化发射器
this.collector = collector;
}
nextTuple()方法定义如下:
@Override
public void nextTuple() {
if (!toSend.isEmpty()) {// 如果发射消息的Map集合不为空
// 遍历发射消息的Map集合进行消息的发射
for (Map.Entry<Integer, String> transactionEntry : toSend.entrySet()) {
// 获取消息ID
Integer transactionId = transactionEntry.getKey();
// 获取消息内容
String transactionMessage = transactionEntry.getValue();
// 把消息发射出去(带上ID号)
collector.emit(new Values(transactionMessage), transactionId);
}
// 把发射消息的Map集合清空,以便存储要重新发射的消息
toSend.clear();
//休眠1秒钟
Utils.sleep(1000);
}
}
如果toSend集合不为空的话,就说明存在消息等待发送,则把每个消息的ID和消息的内容作为一个元组发送,然后清空toSend集合(方便存储需要重发的消息),这里要注意的是在nextTuple中调用Map的clear()方法是安全的,因为nextTuple()、fail()、ack()方法是修改Map的方法,他们都运行在相同的线程中。
messages和failCounterMessages两个Map用来跟踪等待发送的事物消息以及每笔交易已经失败的次数。
ack()方法表示操作成功时的相应,这里通过msgId来删除每个列表中的事物消息:
@Override
public void ack(Object msgId) {
//如果消息发送成功,则从"所有消息Map集合"中删除一条消息
messages.remove(msgId);
//错误计数Map也要相应的删除一条记录
transactionFailureCount.remove(msgId);
}
fail()方法决定是否重发一个事物消息,或者事物消息已经失败了太多次而最终是完全失败(不能被重发),如果在拓扑中使用广播分组,那么任何Bolt实例失败,Spout的fail()方法都会被调用。
@Override
public void fail(Object msgId) {
//获取事物ID
Integer transactionId = (Integer) msgId;
//通过事物ID获取事物失败的次数
Integer failures = transactionFailureCount.get(transactionId) + 1;
//判断事物失败的次数是否大于最大允许失败次数
if (failures >= MAX_FAILS){
//如果失败次数大于或者等于最大允许失败次数,终止拓扑并抛出异常
throw new RuntimeException("该事物失败过多,已经不能再被发送");
} else {
//如果失败的次数小于最大允许失败的次数,我们保存失败次数,并把该消息放入"发送消息集合Map"进行重新发送
transactionFailureCount.put(transactionId, failures);
//放入toSend集合从新发送
toSend.put(transactionId, messages.get(transactionId));
}
}
fail()方法会检查已经失败的事物的次数。如果一个事物失败了很多次,超过最大允许失败次数,会抛出一个RuntimeException异常并终止运行中的Worker进程。否则,保存失败技术,把事物消息放到toSend集合进行重发。
Storm节点不维护状态。如果在内存中存储消息,并且节点又宕机了,将会失去所有的信息。所以,Storm节点的状态由外部的Zookeeper集群所维护。Storm是一种快速失败的系统。如果抛出一个异常,拓扑将会失败。但Storm会在一个一致性状态中重启进程,然后正常恢复进程的执行。