背景

本篇看看storm是通过什么机制来保证消息至少处理一次的语义的。

storm中的一些原语



要说明上面的问题,得先了解storm中的一些原语,比方:

  1. tuple和message

    在storm中,消息是通过tuple来抽象表示的。每一个tuple知道它从哪里来,应往哪里去,包括了其在tuple-tree(假设是anchored的话)或者DAG中的位置。等等信息。

  2. spout

    spout充当了tuple的发送源,spout通过和其他消息源,比方kafka交互,将消息封装为tuple,发送到流的下游。

  3. bolt

    bolt是tuple的实际处理单元,通过从spout或者还有一个bolt接收tuple,进行业务处理,将自己增加tuple-tree(通过在emit方法中设置anchors)或DAG。然后继续将tuple发送到流的下游。
  4. acker

    acker是一种特殊的bolt,其接收来自spout和bolt的消息,主要功能是追踪tuple的处理情况,假设处理完毕,会向tuple的源头spout发送确认消息,否则,会发送失败消息,spout收到失败的消息,依据配置和自己定义的情况会进行消息的丢弃、重放处理。

spout、bolt、acker的关系

  1. spout将tuple发送给流的下游的bolts.
  2. bolt收到tuple。处理后发送给下游的bolts.
  3. spout向acker发送请求ack的消息.
  4. bolt向acker发送请求ack的消息.
  5. acker向bolt和spout发送确认ack的消息.

简单的关系例如以下所看到的:

上图展示了spout、bolts等形成了一个DAG,怎样追踪这个DAG的运行过程。就是storm保证仅处理一次消息的语义的机制所在。

storm怎样追踪消息(tuple)的处理

spout在调用emit/emitDirect方法发送tuple时,会以单播或者广播的方式,将消息发送给流的下游的component/task/bolt,假设配置了acker,那么会在每次emit调用之后,向acker发送请求ack的消息:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; spout向acker发送请求ack消息
;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; rooted? 表示是否设置了acker
(if (and rooted? (not (.isEmpty out-ids)))
(do
(.put pending root-id [task-id
message-id
{:stream out-stream-id :values values}
(if (sampler) (System/currentTimeMillis))])
(task/send-unanchored task-data
;;表示这是一个流初始化的消息
ACKER-INIT-STREAM-ID
;;将下游组件的out-id和0组成一个异或链。发送给acker用于追踪
[root-id (bit-xor-vals out-ids) task-id]
overflow-buffer)) ;; 假设没有配置acker,则调用自身的ack方法
(when message-id
(ack-spout-msg executor-data task-data message-id
{:stream out-stream-id :values values}
(if (sampler) 0) "0:")))

从上面的代码能够看出,每次emit tuple后,spout会向acker发送一个流ID为ACKER-INIT-STREAM-ID的消息。用于将DAG或者tuple-tree中的节点信息交给acker,acker会利用这个信息来追踪tuple-tree或DAG的完毕。

而spout调用emit/emitDirect方法。将tuple发到下游的bolts。也同一时候会发送用于追踪DAG完毕情况的信息:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; spout向流的下游emit消息
;;;;;;;;;;;;;;;;;;;;;;;;;;;; (let [tuple-id (if rooted?
;; 假设有acker,tuple的MessageId会包括一个<root-id,id>的哈希表
;; root-id和id都是long型64位整数
(MessageId/makeRootId root-id id)
(MessageId/makeUnanchored))
;;实例化tuple
out-tuple (TupleImpl. worker-context
values
task-id
out-stream-id
tuple-id)] ;; 发送至队列,终于发送给流的下游的task/bolt
(transfer-fn out-task
out-tuple
overflow-buffer)
))

这个追踪信息是什么呢?

假设是spout -> bolt或者bolt -> bolt,这个信息就是tuple的MessageId,其内部维护一个哈希表:

// map anchor to id
private Map<Long, Long> _anchorsToIds;

键为root-id,表示spout,值表示tuple在tuple-tree或者DAG的根(spout)或者经过的边(bolt),但这里没有利用不论什么常规意义上的“树”的算法,而是採用异或的方式来存储这个值:

  1. spout -> bolt。值被初始化为一个long型64位整数.
  2. bolt -> bolt,值被初始化为一个long型64位整数,并和_anchorsToIds中的旧值进行按位异或,将结果更新到_anchorsToIds中.

假设是spout -> acker。或者bolt -> acker。那么用于追踪的是tuple的values:

  1. spout -> acker : [root-id (bit-xor-vals out-ids) task-id]
  2. bolt -> acker : [root (bit-xor id ack-val) ..]

以下给出上面调用的bit-xor-vals和bit-xor方法的代码:

(defn bit-xor-vals
[vals]
(reduce bit-xor 0 vals)) (defn bit-xor
"Bitwise exclusive or"
{:inline (nary-inline 'xor)
:inline-arities >1?
:added "1.0"}
([x y] (. clojure.lang.Numbers xor x y))
([x y & more]
(reduce1 bit-xor (bit-xor x y) more)))

演示样例

说起来有点抽象,看个样例。

假设我们有1个spout。n个bolt。1个acker:

1.spout

spout发送tuple到下游的bolts:

;; id_1是发送到bolt_1的tuple-id,依此类推
spout :
->bolt_1 : id_1
->bolt_2 : id_2
..
->bolt_n : id_n

2.bolt

bolt收到tuple。在execute方法中进行必要的处理,然后调用emit方法,最后调用ack方法:

;; bolt_1调用emit方法,追踪消息的这样一个值:让id_1和bid_1按位进行异或.
;; bid_1和id_1相似,是个long型的64位随机整数,在emit这一步生成
bolt_1 emit : id_1 ^ bid_1 ;; bolt_1调用ack方法,并将值表达为例如以下方式的异或链的结果
bolt_1 ack : 0 ^ bid_1 ^ id_1 ^ bid_1 = 0 ^ id_1

以上。能够看出bolt进行了emit-ack组合后,其自身在异或链中的作用消失了,也就是说tuple在此bolt得到了处理。

(当然,此时的ack还没有得到acker的确认。假设acker确认了,那么上面所说的tuple在bolt得到了处理就成立了。

来看看acker的确认。

3.acker

acker收到来自spout的tuple:

;; spout发消息给acker,tuple的MessageId包括以下的异或链的结果
spout -> acker : 0 ^ id_1 ^ id_2 ^ .. ^ id_n ;; acker收到来spout的消息,对tuple的ackVal进行处理,例如以下所看到的:
acker : 0 ^ (0 ^ id_1 ^ id_2 ^ .. ^ id_n) = 0 ^ id_1 ^ id_2 ^ .. ^ id_n

acker收到来自bolt的tuple:

;; bolt_1发消息给acker:
bolt_1 -> acker : 0 ^ id_1 ;; acker维护的相应此tuple的源spout的ackVal :
ackVal : 0 ^ id_1 ^ id_2 ^ .. ^ id_n ;; acker进行确认,也就是拿上面的两个值进行异或:
acker : (0 ^ id_1) ^ (0 ^ id_1 ^ id_2 ^ .. ^ id_n) = 0 ^ id_2 ^ .. ^ id_n

能够看出。bolt_1向acker请求ack,acker收到请求ack,异或之后,id_1的作用消失。也就是说。bolt_1已处理完毕这个tuple。

所以。在acker看来,假设某个bolt的处理完毕,则此bolt在异或链中的作用就消失了。

假设全部的bolt 都得到处理。那么acker将会观察到ackVal值变成了0:

ackVal = 0
= (0 ^ id_1) ^ (0 ^ id_1 ^ .. ^ id_n) ^ .. (0 ^ id_n)
= (0 ^ 0) ^ (id_1 ^ id_1) ^ (id_2 ^ id_2) ^ .. ^ (id_n ^ id_n)

假设出现了ackVal = 0,说明两个可能:

  1. spout发送的tuple都处理完毕。tuple-tree或者DAG已完毕。
  2. 概率性出错。也就是说在极小的概率下。即使不按上面的确认流程来走,异或链的结果也可能出现0.但这个概率极小。小到什么程度呢?

    用官方的话说就是。假设每秒发送1万个ack消息,50,000,000年时才可能发生这样的情况。

假设ackVal不为0,说明tuple-tree或DAG没有完毕。

假设长时间不为0,通过超时。能够触发一个超时回调。在这个回调中调用spout的fail方法,来进行重放。

如此,就保证了消息处理不会漏掉。但可能会反复。

结语

以上。就是storm保证消息至少处理一次的语义的机制 。

storm是怎样保证at least once语义的的更多相关文章

  1. storm是如何保证at least once语义的?

    storm中的一些原语: 要说明上面的问题,得先了解storm中的一些原语,比如: tuple和messagetuple:在storm中,消息是通过tuple来抽象表示的,每个tuple知道它从哪里来 ...

  2. storm如何保证at least once语义?

    背景 前期收到的问题: 1.在Topology中我们可以指定spout.bolt的并行度,在提交Topology时Storm如何将spout.bolt自动发布到每个服务器并且控制服务的CPU.磁盘等资 ...

  3. storm基础框架分析

    背景 前期收到的问题: 1.在Topology中我们可以指定spout.bolt的并行度,在提交Topology时Storm如何将spout.bolt自动发布到每个服务器并且控制服务的CPU.磁盘等资 ...

  4. Storm介绍及与Spark Streaming对比

    Storm介绍 Storm是由Twitter开源的分布式.高容错的实时处理系统,它的出现令持续不断的流计算变得容易,弥补了Hadoop批处理所不能满足的实时要求.Storm常用于在实时分析.在线机器学 ...

  5. storm(二)消息的可靠处理

    storm 通过 trident保证了对消息提供不同的级别.beast effort,at least once, exactly once. 一个tuple 从spout流出,可能会导致大量的tup ...

  6. Storm VS Flink ——性能对比

    1.背景 Apache Flink 和 Apache Storm 是当前业界广泛使用的两个分布式实时计算框架.其中 Apache Storm(以下简称"Storm")在美团点评实时 ...

  7. Storm实践(一):基础知识

    storm简介 Storm是一个分布式实时流式计算平台,支持水平扩展,通过追加机器就能提供并发数进而提高处理能力:同时具备自动容错机制,能自动处理进程.机器.网络等异常. 它可以很方便地对流式数据进行 ...

  8. Storm集群安装部署步骤【详细版】

    作者: 大圆那些事 | 文章可以转载,请以超链接形式标明文章原始出处和作者信息 网址: http://www.cnblogs.com/panfeng412/archive/2012/11/30/how ...

  9. Storm与Spark Streaming比较

    前言spark与hadoop的比较我就不多说了,除了对硬件的要求稍高,spark应该是完胜hadoop(Map/Reduce)的.storm与spark都可以用于流计算,但storm对应的场景是毫秒级 ...

随机推荐

  1. bzoj 3190 [JLOI2013]赛车 半平面交+细节处理

    题目大意 这里有一场赛车比赛正在进行,赛场上一共有N辆车,分别称为g1,g2--gn.赛道是一条无限长的直线.最初,gi位于距离起跑线前进ki的位置.比赛开始后,车辆gi将会以vi单位每秒的恒定速度行 ...

  2. 洛谷 [P1948] 电话线

    二分答案 首先,最大值最小,就是二分答案 #include <iostream> #include <cstdio> #include <algorithm> #i ...

  3. ajax cache enable and ajax concurrency!

    Today, forget to close ajax cache which leads to duplicate result from cache as to Jquery, this way, ...

  4. 不需 porting 就可充電的 charger ic。

    Origin : 今天同事問我一個關於配有 RT9458 charger ic 的手機的問題, 這手機可能要送到廠商那, 需要 porting charger ic, 看了一下,跟他說這個充電部份不需 ...

  5. 三、 java运算符与流程控制

    赋值运算 赋值运算符:=,+=,-=,×=,/=,%= class fuzhiyunsuan{ public static void main(String[] args){ int i1 = 10; ...

  6. wpf LookUpEdit PopupContentTemplate

    <dxg:LookUpEdit Name="searchLookUpEdit" HorizontalAlignment="Stretch" PopupHe ...

  7. C#连接OleDBConnection数据库的操作

    对于不同的.net数据提供者,ADO.NET采用不同的Connection对象连接数据库.这些Connection对我们屏蔽了具体的实现细节,并提供了一种统一的实现方法. Connection类有四种 ...

  8. 在 Mac OS X 环境中从源代码编译安装 FFmpeg

    最近因为一个项目要求,需要开发实时视频编解码功能,准备采用 FFmpeg 以 x264 方式进行实时的视频编解码.Windows 以及 Linux 环境下的 FFmpeg 动态库.头文件等资源都非常容 ...

  9. LeetCode OJ--Construct Binary Tree from Inorder and Postorder Traversal *

    http://oj.leetcode.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/ 知道二叉树的中序 ...

  10. 洛谷—— P1561 [USACO12JAN]爬山Mountain Climbing

    https://daniu.luogu.org/problemnew/show/P1561 题目描述 Farmer John has discovered that his cows produce ...