Storm架构
- Nimbus:负责资源分配和任务调度。
- Supervisor:负责接受nimbus分配的任务,启动和停止属于自己管理的worker进程。
- Worker:运行具体处理组件逻辑的进程。
- Task:worker中每一个spout/bolt的线程称为一个task.
- 在storm0.8之后,task不再与物理线程对应,同一个spout/bolt的task可能会共享一个物理线程,该线程称为executor。
编程模型
- Topology:Storm中运行的一个实时应用程序(程序主入口),因为各个组件间的消息流动形成逻辑上的一个拓扑结构。
- Spout:在一个topology中产生源数据流的组件。通常情况下spout会从外部数据源中读取数据,然后转换为topology内部的源数据。Spout是一个主动的角色,其接口中有个nextTuple()函数,storm框架会不停地调用此函数,用户只要在其中生成源数据即可。
- Bolt:在一个topology中接收数据然后执行处理的组件。Bolt可以执行过滤、函数操作、合并、写数据库等任何操作。Bolt是一个被动的角色,其接口中有个execute(Tuple input)函数,在接受到消息后会调用此函数,用户可以在其中执行自己想要的操作。
- Tuple:一次消息传递的基本单元。本来应该是一个key-value的map,但是由于各个组件间传递的tuple的字段名称已经事先定义好,所以tuple中只要按序填入各个value就行了,所以就是一个value list.
- Stream:源源不断传递的tuple就组成了stream。
分组策略
Stream grouping:即消息的partition方法。
Stream Grouping定义了一个流在Bolt任务间该如何被切分。这里有Storm提供的6个Stream Grouping类型:
- 随机分组(Shuffle grouping):随机分发tuple到Bolt的任务,保证每个任务获得相等数量的tuple。 跨服务器通信,浪费网络资源,尽量不适用
- 字段分组(Fields grouping):根据指定字段分割数据流,并分组。例如,根据“user-id”字段,相同“user-id”的元组总是分发到同一个任务,不同“user-id”的元组可能分发到不同的任务。 跨服务器,除非有必要,才使用这种方式。
- 全部分组(All grouping):tuple被复制到bolt的所有任务。这种类型需要谨慎使用。 人手一份,完全不必要
- 全局分组(Global grouping):全部流都分配到bolt的同一个任务。明确地说,是分配给ID最小的那个task。 欺负新人
- 无分组(None grouping):你不需要关心流是如何分组。目前,无分组等效于随机分组。但最终,Storm将把无分组的Bolts放到Bolts或Spouts订阅它们的同一线程去执行(如果可能)。
- 直接分组(Direct grouping):这是一个特别的分组类型。元组生产者决定tuple由哪个元组处理者任务接收。 点名分配AckerBolt 消息容错
- LocalOrShuffle 分组。 优先将数据发送到本地的Task,节约网络通信的资源(大多数情况下最优)。
原理
Storm提交任务的过程(重要)
Storm的disruptor队列
1. spout将数据发送给下游的bolt,实际上是先发送给bolt的输入队列(disruptor)。等bolt处理完之后,再继续发送给下一个bolt的输入队列(disruptor)。
2. disruptor由一个环形的缓冲数组区组成。生产者在自己的序号管理器中,获得可以生产的元素的个数,并生产数据,输入到缓冲区中;消费者在自己的序号管理器中,获得自己可以消费的元素的个数,并从缓冲区中消费数据。
Storm的启动全过程
- client客户端将编译好的jar包进行序列化后,通过RPC框架提交到storm的nimbus中
- nimbus对任务进行校验,分配任务给空闲的worker(6700~6703里面空闲的),并选择任务需要多少个worker,作为assignment对象上传到zookeeper中的目录:/assignment目录下,文件名为:topology-uuid
- supervisor通过watch机制,从zookeeper中获取任务,并创建相应的worker(此处假设启动了supervisor1的6700/6701和supervisor2的6700)。每个worker都是独立的进程。
- worker启动后,从zookeeper中获取到自己分配到的任务。
- worker开始消费kafka的topic: payment下的3个partition中的partition-0的消息。
- 如果spout下一步需要把数据发送到bolt1-5(跨服务器),那么spout会将数据封装成tuple(targetTaskId=5)。从网络输出线程池中获取输出线程,输出到另一个服务器的网络信息的输入线程池中。最终,将数据生产到bolt1-5的输入队列中。
Storm的消息不丢失ACK机制
通过Ack机制,spout发送出去的每一条消息,都可以确定是被成功处理或失败处理, 从而可以让开发者采取动作。比如在Meta中,成功被处理,即可更新偏移量,当失败时,重复发送数据。
因此,通过Ack机制,很容易做到保证所有数据均被处理,一条都不漏。
另外需要注意的,当spout触发fail动作时,不会自动重发失败的tuple,需要spout自己重新获取数据,手动重新再发送一次
ack机制就是, spout发送的每一条消息:
- 在规定的时间内,spout收到Acker的ack响应,即认为该tuple 被后续bolt成功处理
- 在规定的时间内,没有收到Acker的ack响应tuple,就触发fail动作,即认为该tuple处理失败,
- 或者收到Acker发送的fail响应tuple,也认为失败,触发fail动作
另外Ack机制还常用于限流作用: 为了避免spout发送数据太快,而bolt处理太慢,常常设置pending数,当spout有等于或超过pending数的tuple没有收到ack或fail响应时,跳过执行nextTuple, 从而限制spout发送数据。
通过conf.put(Config.TOPOLOGY_MAX_SPOUT_PENDING, pending);设置spout pend数。
这个timeout时间可以通过Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS来设定。Timeout的默认时长为30秒
如何开启、关闭Ack机制
开启:
-
spout 在发送数据的时候带上msgid
-
设置acker数至少大于0;Config.setNumAckers(conf, ackerParal);
在bolt中完成处理tuple时,执行OutputCollector.ack(tuple), 当失败处理时,执行OutputCollector.fail(tuple);推荐使用IBasicBolt, 因为IBasicBolt 自动封装了OutputCollector.ack(tuple), 处理失败时,请抛出FailedException,则自动执行OutputCollector.fail(tuple)
关闭:
- spout发送数据时,不带上msgid
- 设置acker数等于0
Acker机制的基本实现
Storm 系统中有一组叫做"acker"的特殊的任务,它们负责跟踪DAG(有向无环图)中的每个消息。
acker任务保存了spout id到一对值的映射。第一个值就是spout的任务id,通过这个id,acker就知道消息处理完成时该通知哪个spout任务。第二个值是一个64bit的数字,我们称之为"ack val", 它是树中所有消息的随机id的异或计算结果。
<TaskId,<RootId,ackValue>>
Spoutid,<系统生成的id,ackValue>
Task-0,64bit,0
ack val表示了整棵树的的状态,无论这棵树多大,只需要这个固定大小的数字就可以跟踪整棵树。当消息被创建和被应答的时候都会有相同的消息id发送过来做异或。 每当acker发现一棵树的ack val值为0的时候,它就知道这棵树已经被完全处理了