在jstorm中通过acker机制来保证spout作为拓扑树的源头能够清楚知道每一条数据的处理情况。
在DefaultTopologyAssignContext中在在准备topology中,会根据之前设置的acker并行度(topology.acker.executor参数)来给topology的结构中插入相应数量的acker。
public static void add_acker(Map stormConf, StormTopology ret) {
String key = Config.TOPOLOGY_ACKER_EXECUTORS;
Integer ackerNum = JStormUtils.parseInt(stormConf.get(key), 0);
// generate outputs
HashMap<String, StreamInfo> outputs = new HashMap<>();
ArrayList<String> fields = new ArrayList<>();
fields.add("id");
outputs.put(ACKER_ACK_STREAM_ID, Thrift.directOutputFields(fields));
outputs.put(ACKER_FAIL_STREAM_ID, Thrift.directOutputFields(fields));
IBolt ackerbolt = new Acker();
// generate inputs
Map<GlobalStreamId, Grouping> inputs = acker_inputs(ret);
// generate acker which will be stored in topology
Bolt acker_bolt = Thrift.mkBolt(inputs, ackerbolt, outputs, ackerNum);
// add every bolt two output stream
// ACKER_ACK_STREAM_ID/ACKER_FAIL_STREAM_ID
for (Entry<String, Bolt> e : ret.get_bolts().entrySet()) {
Bolt bolt = e.getValue();
ComponentCommon common = bolt.get_common();
List<String> ackList = JStormUtils.mk_list("id", "ack-val");
common.put_to_streams(ACKER_ACK_STREAM_ID, Thrift.outputFields(ackList));
List<String> failList = JStormUtils.mk_list("id");
common.put_to_streams(ACKER_FAIL_STREAM_ID, Thrift.outputFields(failList));
bolt.set_common(common);
}
// add every spout output stream ACKER_INIT_STREAM_ID
// add every spout two intput source
// ((ACKER_COMPONENT_ID, ACKER_ACK_STREAM_ID), directGrouping)
// ((ACKER_COMPONENT_ID, ACKER_FAIL_STREAM_ID), directGrouping)
for (Entry<String, SpoutSpec> kv : ret.get_spouts().entrySet()) {
SpoutSpec bolt = kv.getValue();
ComponentCommon common = bolt.get_common();
List<String> initList = JStormUtils.mk_list("id", "init-val", "spout-task");
common.put_to_streams(ACKER_INIT_STREAM_ID, Thrift.outputFields(initList));
GlobalStreamId ack_ack = new GlobalStreamId(ACKER_COMPONENT_ID, ACKER_ACK_STREAM_ID);
common.put_to_inputs(ack_ack, Thrift.mkDirectGrouping());
GlobalStreamId ack_fail = new GlobalStreamId(ACKER_COMPONENT_ID, ACKER_FAIL_STREAM_ID);
common.put_to_inputs(ack_fail, Thrift.mkDirectGrouping());
}
ret.put_to_bolts(ACKER_COMPONENT_ID, acker_bolt);
}
以上是给将acker作为bolt插入在topology结构中方法,根据所配置的acker的并行度来创建相应数量的acker,并同时给其余的spout和bolt设置相应的streamId,以确保能与相应的acker进行数据交流。
这里可以看到以上实现的acker类的具体实现,acker继承了IBolt类,因此可以直接作为Bolt来放入topology的结构中。
@Override
public void execute(Tuple input) {
Object id = input.getValue(0);
AckObject curr = pending.get(id);
String stream_id = input.getSourceStreamId();
if (Acker.ACKER_INIT_STREAM_ID.equals(stream_id)) {
if (curr == null) {
curr = new AckObject();
curr.val = input.getLong(1);
curr.spout_task = input.getInteger(2);
pending.put(id, curr);
} else {
// bolt's ack first come
curr.update_ack(input.getValue(1));
curr.spout_task = input.getInteger(2);
}
} else if (Acker.ACKER_ACK_STREAM_ID.equals(stream_id)) {
if (curr != null) {
curr.update_ack(input.getValue(1));
} else {
// two case
// one is timeout
// the other is bolt's ack first come
curr = new AckObject();
curr.val = input.getLong(1);
pending.put(id, curr);
}
} else if (Acker.ACKER_FAIL_STREAM_ID.equals(stream_id)) {
if (curr == null) {
// do nothing
// already timeout, should go fail
return;
}
curr.failed = true;
} else {
LOG.info("Unknown source stream, " + stream_id + " from task-" + input.getSourceTask());
return;
}
Integer task = curr.spout_task;
if (task != null) {
if (curr.val == 0) {
pending.remove(id);
List values = JStormUtils.mk_list(id);
collector.emitDirect(task, Acker.ACKER_ACK_STREAM_ID, values);
} else {
if (curr.failed) {
pending.remove(id);
List values = JStormUtils.mk_list(id);
collector.emitDirect(task, Acker.ACKER_FAIL_STREAM_ID, values);
}
}
}
// add this operation to update acker stats
collector.ack(input);
long now = System.currentTimeMillis();
if (now - lastRotate > rotateTime) {
lastRotate = now;
Map<Object, AckObject> tmp = pending.rotate();
if (tmp.size() > 0) {
LOG.warn("Acker's timeout item size:{}", tmp.size());
}
}
}
以上是,acker的excute()方法,实现的比较长。
首先会根据传入的tuple的messageId在自己以messageId和AckObject为键值对的Map中去寻找对应的AckObject来对应此次数据流对应的ack状态。
支持了ack的spout都会在发送消息的时候带上相应的messageId,以此类推,那么没有带上messageId的数据都将不会在acker中做处理。
在init中,会首先根据messageId来寻找是否存在对应的ackObjcet,如果不存在,说明这里是spout自发送数据后,第一次ack,那么将会创建一个ackObjcet,并且将val赋为所带着的随机数,并设置好spout的taskId,并在map中存放。
那么这个随机数是在什么时候设置的呢,把目光放在专门用来支持ack机制的spout,ACKTransactionSpout类里实现了AckSpoutOutputCollector,其ack()方法在发送ack消息之前,会随机生成一个随机数。
public List<Integer> emit(String streamId, List<Object> tuple, Object messageId) {
if (messageId != null) {
addPendingTuple(currBatchId, streamId, messageId, tuple);
tuple.add(Utils.generateId(random));
} else {
// for non-anchor tuples, use 0 as default rootId
tuple.add(0l);
}
return delegate.emit(streamId, tuple, null);
}
这里会给tuple携带一个随机数,也就是赋值在ackObject里的val字段里的数据。
在init中,如果已经存在了相应的ackObject,说明这是第一次从bolt向ack提交,那么ackObject则会调用update_ack()方法,来对其进行相应的操作。
public void update_ack(Object value) {
synchronized (this) {
val = JStormUtils.bit_xor(val, value);
}
}
这个方法很简单,只是进行相应的异或操作,那么这里的bolt传来的rootId是怎样的呢?可以看到实现在了AckTransactionBolt里的AckOutputCollecror的ack()方法。
@Override
public void ack(Tuple input) {
Pair<Long, Integer> pendingBatch = tracker.getPendingBatch(((TupleImplExt) input).getBatchId(), input.getSourceStreamId());
if (pendingBatch != null) {
long rootId = getRootId(input);
if (rootId != 0)
pendingBatch.setFirst(JStormUtils.bit_xor(pendingBatch.getFirst(), rootId));
}
delegate.ack(input);
}
这里会将之前产生的随机数rootId与之前的rootId异或的结果,传给acker。
由此可以看到,如果第一次spout传给acker的val为x,第一次bolt产生的rootId为y,那么bolt将会把x和y异或的结果传给acker,而把y单独传给下一个bolt做异或操作。此时,acker里对应这个消息树的val的值为x与y与x异或的结果,也就是y。
那么看到streamId为ack的处理,也变得很简单,就是对rootId简单的异或操作。
如果streamId为fail,那么则会导致把这个ackObject的状态赋为fail。
由此可以看到,假设一个消息树存在一个spout,三个不同的bolt,能保证其完整的状态量为0的时候,是经过a^b^a^c^b^c^d^d。那么结果就是0,确保消息完成。
之后如果ackObject的val已经为0则通知spout已经处理完毕,如果不是,则继续,但如果状态已经是fail,则需要通知spout该消息已经失败。
可以看到,在更新了一次ackObject之后,如果此时更新的时间与上一次更新时间之间大于了规定的timeout,则其map则会对最后没有更新过的ack数据进行清理,通过RotatingMap的rotate()方法。
主要存放ackObject的map是一个RotatingMap,其构造如下。
private Deque<Map<K, V>> buckets;
其中是一个map组成的队列,队列的大小取决于acker的timeout_bucket_num参数。看到其put()方法。
@Override
public void put(K key, V value) {
Iterator<Map<K, V>> it = buckets.iterator();
Map<K, V> bucket = it.next();
bucket.put(key, value);
while (it.hasNext()) {
bucket = it.next();
bucket.remove(key);
}
}
也就是说,新加入的acker数据都将处于队列的第一个位置,而其rotate()方法则如下会清理队列最后一个map,并在队列顶部加入一个新的map。由此可以得出,如果一个ack数据进入,如果该acker在timeout_bucket_num * timeout时间里没有被更新过,则在下一次别的ack数据进来时将会被清除。