一. 前言
最近公司因为内部项目调动,我们组接收一个维护storm的任务,看代码流程都是通过ACK机制来保证从spout发出tuple在后面的各个环节已经全部执行成功。自己也好奇是怎么实现的呢? 毕竟storm程序同时处理很多个tuple,更要命的是一个tuple可能被切分成多个tuple,而且会切分成多少个tuple并不知道(e.g. 根据某个标识符进行split("特殊标识")等)
查看官网说明:http://storm.apache.org/releases/1.2.3/Guaranteeing-message-processing.html (1.2.3版本)以及各个blog,感觉写的蛮简单,看了不是很明白(毕竟storm 1.x及之前的版本是用Clojure 语言,该语言用户量不会很大,从2.x开始用java来重写),于是打算自己看2.0.0版本的storm代码,理解其中的原理。
二.ACK原理分析
案例:
ack原理的本质:是一个数 异或 “自己本身” 肯定是等于 0 的。设想我们有这样一个通用程序 :一个为A的spout 发送了一个tuple;boltB接收到A发送的tuple,逻辑处理后,又发送了一个新的tuple并且ack表示处理成功;boltC接收到boltB发送的tuple,逻辑处理并且ack 即: spout -A( 业务逻辑处理,emit a tuple) --> bolt -B(业务逻辑处理,emit a tuple , ack ) --> bolt-C(业务逻辑逻辑,ack)
我们先假设spout或者bolt emit 一个tuple时会产生一个64位bit的随机数,其次 有一个全局异或值:VAL(初始值为0), spout执行emit 或者 bolt执行ack方法的时候会触发 VAL 异或 与自己生成随机数 相关的值(后面流程图会讲)。
根据流程图:
spout-A emit a tuple: 生成一个随机数(用random-A 表示),并且执行异或,VAL = VAL ^ random-A,因为VAL初始值0,结果VAL变成了random-A
bolt-B eimt a tuple 并且 ack操作: emit a tuple时生成一个随机数random-B,然后再执行ack,通过anchors机制,可以获取上游spout-A的信息,获取random-A。执行 random-temp-B = random -A ^ random-B(random-temp-B 就是上述红线标注的 和自己生成随机数相关的值) ,random-temp-B再与全局异或值VAL异或。 即 VAL = VAL ^ random-temp-B ,替换变量就是VAL = random-A ^ (random-A ^ random-B)
bolt-C ack 操作: 因为botl-C没有emit a tuple,即不会产生随机数。 同样的道理 random-temp-C = random-B (anchors机制从上游组件的tuple拿出其生成的随机数),调用ack方法后和全局异或值VAL异或。即 VAL = VAL ^ random-temp-C, 替换变量后
VAL = random-A ^ (random-A ^ random-B) ^ random-B. 当然最终的值VAL 肯定为0
(上述过程可以大致理解为storm实现的机制,当然真实肯定比这要复杂一点,比如上游的anchors有多个 等情况)
源码分析:
storm 版本为storm 2.0.0版本,这里仅仅分析了 列出了核心的类以及方法,并对重点步骤进行代码注释 (还是比较简单的,有兴趣的同学可以自己看看)
1. 在spout.emit时候,SpoutOutputCollectorImpl.sendSpoutMsg方法会被调用
private List<Integer> sendSpoutMsg(String stream, List<Object> values, Object messageId, Integer outTaskId) throws
InterruptedException {
/**..此处代码被删除..**/
//判断是否需要 ACK机制
final boolean needAck = (messageId != null) && hasAckers;
final List<Long> ackSeq = needAck ? new ArrayList<>() : null;
//当执行全局VAL异或的时候,用于标识 哪些tuple划分为同一次“完全处理”
final long rootId = needAck ? MessageId.generateId(random) : 0;
for (int i = 0; i < outTasks.size(); i++) { // perf critical path. don't use iterators.
Integer t = outTasks.get(i);
MessageId msgId;
if (needAck) {
// 当emit发送一个tuple时,需要生成一个随机值
long as = MessageId.generateId(random);
msgId = MessageId.makeRootId(rootId, as);
ackSeq.add(as);
} else {
msgId = MessageId.makeUnanchored();
}
//构建一个tuple,并发送出去(注意这里的this.taskId,一个tuple需要被哪个task执行,这里其实已经记录好了)
final TupleImpl tuple =
new TupleImpl(executor.getWorkerTopologyContext(), values, executor.getComponentId(), this.taskId, stream, msgId);
AddressedTuple adrTuple = new AddressedTuple(t, tuple);
executor.getExecutorTransfer().tryTransfer(adrTuple, executor.getPendingEmits());
}
if (needAck) {
boolean sample = executor.samplerCheck();
TupleInfo info = new TupleInfo();
info.setTaskId(this.taskId);
info.setStream(stream);
info.setMessageId(messageId);
pending.put(rootId, info);
//构建参与和全局VAL 异或的值,Utils.bitXorVals(ackSeq) 这里等于上面emit a tuple时生成的随机值,并发送出去。其中ack类型为Acker.ACKER_INIT_STREAM_ID
List<Object> ackInitTuple = new Values(rootId, Utils.bitXorVals(ackSeq), this.taskId);
taskData.sendUnanchored(Acker.ACKER_INIT_STREAM_ID, ackInitTuple, executor.getExecutorTransfer(), executor.getPendingEmits());
}
/** 代码省略 **/
return outTasks;
}
2. 我们知道storm内部有个ACK 程序实时处理spout emit的tuple是否已经完全成功处理。该ACK其实也是一个bolt(特殊的bolt)。我们直接查看Acker.execute
public void execute(Tuple input) {
if (TupleUtils.isTick(input)) {
Map<Object, AckObject> tmp = pending.rotate();
LOG.debug("Number of timeout tuples:{}", tmp.size());
return;
}
boolean resetTimeout = false;
String streamId = input.getSourceStreamId();
//spout 源码分析中的rootId
Object id = input.getValue(0);
AckObject curr = pending.get(id);
if (ACKER_INIT_STREAM_ID.equals(streamId)) {
if (curr == null) {
//我们所说的全局异或变量VAL,就是AckObject中的一个字段,初始话值是0
curr = new AckObject();
pending.put(id, curr);
}
//VAL 和 传过来的值进行异或 同时 VAL更新为 异或后的值
curr.updateAck(input.getLong(1));
curr.spoutTask = input.getInteger(2);
} else if (ACKER_ACK_STREAM_ID.equals(streamId)) { //调用ack的时候
//和上面同理
if (curr == null) {
curr = new AckObject();
pending.put(id, curr);
}
curr.updateAck(input.getLong(1));
} else if (ACKER_FAIL_STREAM_ID.equals(streamId)) {
// For the case that ack_fail message arrives before ack_init
if (curr == null) {
curr = new AckObject();
}
curr.failed = true;
pending.put(id, curr);
} else if (ACKER_RESET_TIMEOUT_STREAM_ID.equals(streamId)) {
resetTimeout = true;
if (curr == null) {
curr = new AckObject();
}
pending.put(id, curr);
} else if (Constants.SYSTEM_FLUSH_STREAM_ID.equals(streamId)) {
collector.flush();
return;
} else {
LOG.warn("Unknown source stream {} from task-{}", streamId, input.getSourceTask());
return;
}
/***代码省略**/
collector.ack(input);
}
3. Bolt进行emit 新的tuple 以及 执行ack操作 。会相应执行BoltOutputCollectorImpl类中的boltEmit方法以及ack方法
private List<Integer> boltEmit(String streamId, Collection<Tuple> anchors, List<Object> values,
Integer targetTaskId) throws InterruptedException {
List<Integer> outTasks;
if (targetTaskId != null) {
outTasks = task.getOutgoingTasks(targetTaskId, streamId, values);
} else {
outTasks = task.getOutgoingTasks(streamId, values);
}
for (int i = 0; i < outTasks.size(); ++i) {
Integer t = outTasks.get(i);
MessageId msgId;
if (ackingEnabled && anchors != null) {
final Map<Long, Long> anchorsToIds = new HashMap<>();
//上游可能有多个anchors
for (Tuple a : anchors) {
//这里的a表示为anchor上游的tuple
Set<Long> rootIds = a.getMessageId().getAnchorsToIds().keySet();
if (rootIds.size() > 0) {
//需要发送新的tuple,生成一个随机数
long edgeId = MessageId.generateId(random);
//构建的tuple实例有一个字段_outAckVal, updateAckVal(edgeId)代码为 _outAckVal = _outAckVal ^ val,初始值为0,更新_outAckVal。 主要处理上游的tuple可能被发送到多个下游的实例
((TupleImpl) a).updateAckVal(edgeId);
for (Long root_id : rootIds) {
putXor(anchorsToIds, root_id, edgeId);
}
}
}
msgId = MessageId.makeId(anchorsToIds);
} else {
msgId = MessageId.makeUnanchored();
}
//构建一个tuple实例,并transter到下游
TupleImpl tupleExt = new TupleImpl(
executor.getWorkerTopologyContext(), values, executor.getComponentId(), taskId, streamId, msgId);
xsfer.tryTransfer(new AddressedTuple(t, tupleExt), executor.getPendingEmits());
}
return outTasks;
}
public void ack(Tuple input) {
if (!ackingEnabled) {
return;
}
//上游anchors 过来的tuple
long ackValue = ((TupleImpl) input).getAckVal();
//anchorsToIds 中的key就是spout.emit阶段生成的rootid
Map<Long, Long> anchorsToIds = input.getMessageId().getAnchorsToIds();
for (Map.Entry<Long, Long> entry : anchorsToIds.entrySet()) {
//Utils.bitXor(entry.getValue(), ackValue) ,entry.getValues (就是上游anchors tuple实例中与生成随机数相关的值,注意是相关) ackValue:上游anchors tuple实例中的_outAckVal字段(正常实例化时值为0,在emitBolt会和当前tuple生成的随机数异或)
task.sendUnanchored(Acker.ACKER_ACK_STREAM_ID,
new Values(entry.getKey(), Utils.bitXor(entry.getValue(), ackValue)),
executor.getExecutorTransfer(), executor.getPendingEmits());
}
/**代码省略**/
}
}