背景

本篇看看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 2961 共点圆 cdq+凸包+三分

    题目大意 两种操作 1)插入一个过原点的圆 2)询问一个点是否在所有的圆中 分析 在圆中则在半径范围内 设圆心 \(x,y\) 查询点\(x_0,y_0\) 则\(\sqrt{(x-x_0)^2+(y ...

  2. Java--消除重复数字后的最大值

    描述: 一个长整型数字,消除重复的数字后,得到最大的一个数字. 如12341 ,消除重复的1,可得到1234或2341,取最大值2341. 42234,消除4 得到4223 或者 2234 ,再消除2 ...

  3. 共享内存之——system V共享内存

    System V 的IPC对象有共享内存.消息队列.信号灯(量). 注意:在IPC的通信模式下,不管是共享内存.消息队列还是信号灯,每个IPC的对象都有唯一的名字,称为"键(key)&quo ...

  4. sync fsync fdatasync

    传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速 缓存,大多数磁盘I/O都通过缓冲进行.当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队 ...

  5. Day 17 编码+文本编辑+函数

    知识点篇: #! /usr/bin/env python # -*- coding: utf-8 -*- # __author__ = "DaChao" # Date: 2017/ ...

  6. 美图秀秀web开发文档

    Xiuxiu 组件 import React, { Component } from 'react'; class XiuXiu extends Component { componentDidMou ...

  7. js中加“var”和不加“var”的区别,看完觉得这么多年js白学了

    Javascript声明变量的时候,虽然用var关键字声明和不用关键字声明,很多时候运行并没有问题,但是这两种方式还是有区别的.可以正常运行的代码并不代表是合适的代码. var num = 1: 是在 ...

  8. Codeforces 934 C.A Twisty Movement-前缀和+后缀和+动态规划

    C. A Twisty Movement   time limit per test 1 second memory limit per test 256 megabytes input standa ...

  9. R语言table()函数

    R语言table()函数比较有用,两个示例尤其是混淆矩阵这个案例比较有用: 例子一:统计频次 z<-c(1,2,2,4,2,7,1,1);z1<-table(z);summary(z1); ...

  10. Codeforces 895C Square Subsets(状压DP 或 异或线性基)

    题目链接  Square Subsets 这是白书原题啊 先考虑状压DP的做法 $2$到$70$总共$19$个质数,所以考虑状态压缩. 因为数据范围是$70$,那么我们统计出$2$到$70$的每个数的 ...