一 Storm运行目录树
1.1 Storm组件本地目录树
Worker-id 的含义
当前worker的唯一uuid标识,端口号和唯一标识的对应关系在supervisor分配任务的时候已经分配好了,并保存到zookeeper上
Pids下为什么显示那么多进程
第一个是worker的进程号,其他的进程号是worker启动的其他相关进程。
1.2 Storm zookeeper目录树
二 Strom通信机制
2.1 Worker进程间通信(Netty、ZeroMQ)
进程间通信有两种方式
2.1.1 Netty
是一个NIO client-server(客户端服务器)框架,使用Netty可以快速开发网络应用,特点如下
1.扩展性强
2.内部实现复杂
3.有简单的api调用
4.基于NiO实现
5.异步
2.1.2 ZeroMq
基于消息队列的多线程网络库,其对套接字类型、连接处理、帧、甚至路由的底层细节进行抽象,提供跨越多种传输协议的套接字,在网络通信中介于应用层和传输层之间,特点如下
1 .可并行运行分散在分布式系统之间
2.使得Socket编程更加简单、简洁和性能更高(框架一样的一个socket library)
3.个消息处理队列库,可在多个线程、内核和主机盒之间弹性伸缩
2.2 Worker 内部通信技术(Disruptor)
1.Disruptor由LMAX公司开发并开源,能在极端的时间内处理大量的订单信息,同时也称为分裂器,在金融交易平台应用较多
2.Disruptor就是一个队里,应用于“生产者-消费者”模型,使用JDK Queue基于CAS(Compare And Swap/Set 一种类似于乐观锁的在多线程环境下无锁实现同步功能的机制)的实现,相比ArrayBlockingQueue、LinkBlockingQueue等使用锁的实现要快很多。
3.处理速度非常快(快的原因:剖析Disruptor:为什么会这么快?(一)锁的缺点 | 并发编程网 – ifeve.com)
4.允许多个生产者和消费者共享相同数据结构,使用Sequence序号管理器实现对象访问Ring Buffer 环形的缓冲区时的序号管理,实现多个对象访问共享缓冲区时的顺序处理,保证线程安全。
5.Disruptor可以看成一个事件监听或消息机制,在队列中一边生产者放入消息,另外一边消费者并行取出处理
6.底层是单个数据结构:一个ring buffer,RingBuffer的底层是个数组
7.核心组件
Ring Buffer 环形的缓冲区,底层是个数组,负责对通过 Disruptor 进行交换的数据(事件)进行存储和更新。
Sequence 通过顺序递增的序号来编号管理通过其进行交换的数据(事件),对数据(事件)的处理过程总是沿着序号逐个递增处理。计算器是一个64bit long 整数型(可以缓存2^64的数据)
每次生产者要生产数据都要请求序号管理器,获得当前缓冲区空闲位置编号进行生产,当数据还未被消费时序号管理器不会将该序号分配出去。消费者要消费数据时也是从序号管理器中获取序号消费数据,只能消费含有数据的位置。
另一种说法:在每个消费者和生产者之间均有私有的次序管理器功能和上图的序号管理器工作过程一样,不过各个次序管理器之间需要进行通信,确定各自的序号范围避免覆盖。在github上关于Disruptor的解释也是属于这一种,情况如下图。
(访问地址:Introduction · LMAX-Exchange/disruptor Wiki · GitHub)
上面说两种说法大致上是一样的,也就是说消费者和生产者需要通过序号去操作缓冲区的数据。
2.3 Storm 通信过程解析
Worker间的通信:经常需要通过网络跨节点进行,Storm使用ZeroMQ或Netty(0.9以后默认使用)作为进程间通信的消息框架。
Worker进程内部通信:使用Disruptor来完成。
不同topology之间的通信:Storm不负责,需要自己想办法实现,例如使用kafka
上图是目前来说使用最为广泛的一个进程通信的版本
- 对于worker进程来说,为了管理流入和传出的消息,每个worker进程有一个独立的接收线程(对配置的TCP端口supervisor.slots.ports进行监听);
对应Worker接收线程,每个worker存在一个独立的发送线程,它负责从worker的transfer-queue中读取消息,并通过网络发送给其他worker
- 每个executor有自己的incoming-queue和outgoing-queue。
Worker接收线程将收到的消息通过task编号传递给对应的executor(一个或多个)的incoming-queues;每个executor有单独的线程分别来处理spout/bolt的业务逻辑,业务逻辑输出的中间数据会存放在outgoing-queue中,当executor的outgoing-queue中的tuple达到一定的阀值,executor的发送线程将批量获取outgoing-queue中的tuple,并发送到transfer-queue中。使用队列的原因是各个节点处理速度不一样,所以可能会发生速度不匹配数据丢失的情况。
- 每个worker进程控制一个或多个executor线程,用户可在代码中进行配置。其实就是我们在代码中设置的并发度个数。
上图比较直观的展示了worker进程之间的通信过程,但有一些细节讲得不够详细,就比如每个worker中的发送线程又是如何知道接收数据worker的位置呢?
上图是storm启动日志中的内容,也就是说在发送线程队列中接收到的内部处理过的数据(tuple)里包含了数据接收方taskId以及数据的分组规则,发送线程根据taskId信息发送到对应的executor中。executor里面的接收队列和发送队列也就是内部通信中提到的Disruptor。
下面细致看一下整个storm的通信过程
1.首先接收线程Receive Thread与其他worker中发送线程简历TCP连接,把接收的到数据保存在自己的缓存队列Receive Thread中。
2.根据Tuple中包含的taskId,匹配到对应的executor,将数据分别发送到各个executor中的incoming-queue队列里
3.executor处理完数据之后这里就有问题了哦,很明显上图的所有的executor均输入到一个transfer Queue中,但是前面那张图显示的是每一个executor都有一个对应的outgoing-queue,也就是说按照这个图来说executor中并看不出来有outgoing-queue,按照我自己的理解应该是outgoing-queue和incoming-queque都没画出来,其实是存在的,但是那个receive-queque表示的是接收线程内的队列,三个节点表示的是接收线程中队列的内容那就可以解释了(目前大多数观点)。另一种观点认为receive-queque中的三个子队列表示的是对应工作线程中的队列,也就是说并不存在outgoning-queue,各线程操作完数据后直接发送到Transfer Queue中,因为没必要在单个executor中同时设置输入和输出队列作为数据缓存有点多此一举。(如果你有答案麻烦留言告知)。
下面展示上图的过程进一步解析
三 Storm 消息容错机制
3.1精简
使用Acker机制为storm处理数据提供可靠性保障,在spout发送tuple时设置唯一的messageID(锚定发送),系统会自动为每个tuple生成一个rootID(用来标识数据来源于哪个spout)和tupleID(处理完一级之后会重新赋值tupleID)用来做异或运算,确定数据处理是否完成,接收数据处理返回的通知,失败调用fail()方法,成功调用ack()方法,数据失败后的处理方式需要自己在对应的方法的实现(一般是在发送的时候就把tuple和对应的messageID存到Map中,成功时返回ID值删除对应Map值,失败时根据返回的messageID从Map获取tuple值重新发送)。
collector.emit(tuple,messageId)//可靠消息
collector.emit(tuple)//不可靠的消息
3.2深入
Storm 系统中有一组叫做"acker"的特殊的任务(本质上也是一个bolt),它们负责跟踪DAG(有向无环图)中的每个消息。
acker任务保存了spout id到一对值的映射。第一个值就是spout的任务id,通过这个id,acker就知道消息处理完成时该通知哪个spout任务。第二个值是一个64bit的数字,我们称之为"ack val", 它是树中所有消息的随机id的异或计算结果。
ack val表示了整棵树的的状态,无论这棵树多大,只需要这个固定大小的数字就可以跟踪整棵树。
当消息被创建和被应答的时候都会有相同的消息id发送过来做异或。 每当acker发现一棵树的ack val值为0的时候,它就知道这棵树已经被完全处理了。
ack机制里面,发送两种类型的tuple,一种是原始消息(数据Tuple),另外一种是ackTuple<RootId,tupleID>,这是往下一级发送的tuple数据里DataTuple(MessageId(ackTuple)),其中RootID是spout任务的ID,用来标记发送者spout。
3.3过程理解
这里分步骤看一下,当一个tuple从spout产生时,就会产生两个编号,一个用来标识spout节点,一个ID用来做异或运算,这一组数据ackTuple<RootId,tupleID>会被发送到Acker并随着数据发送到下一级节点,当该tuple被成功处理后ackTuple<RootId,tupleID>会把这一组数据又发送到Acker,这样两个一样的tupleID异或肯定为0,当该tuple被处理后还需要发送到下一级bolt时,系统会重新为他新tupleID并和RootID组成新的ackTuple<RootId,tupleID>,过程不变,最后处理完毕时得到的tupleID异或值成功肯定为0,不成功就不为0.
有没有想过为什么会使用发送接收都发送ID的对称呢?这样可以有利于确认是tuple发送的时候出错还是在下一级处理的时候出错,而且采用一个值标记做异或运算就可以确定整个流程处理树的正确性,大大节省了空间
3.4 实现
3.4.1手动实现
编写spout类
实现ack机制需要spout类重写ack()方法和fail()方法
还要在nextTuple()方法向外发送数据时绑定一个唯一id
当spout发送的一条数据被完整处理, storm会调用ack()方法
当spout发送的一条数据被标记为处理失败, storm会调用fail()方法
public class MySpout extends BaseRichSpout {
private SpoutOutputCollector spoutOutputCollector;
//创建一个map用来保存数据
private Map<String, String> msgBuffer = new HashMap<String, String>();
/**
* 初始化方法
* @param map
* @param topologyContext
* @param spoutOutputCollector
*/
public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {
this.spoutOutputCollector = spoutOutputCollector;
}
/**
* 数据发送
*/
public void nextTuple() {
//生成一个唯一编号
String msgId = UUID.randomUUID().toString();
//模拟一条消息
String msg = "this is test message";
//把消息存入map
msgBuffer.put(msgId, msg);
//向下游bolt发送一条数据,并附带唯一编号
spoutOutputCollector.emit(new Values(msg), msgId);
}
/**
* 字段声明
* @param outputFieldsDeclarer
*/
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
outputFieldsDeclarer.declare(new Fields("line"));
}
/**
* 当spout发送的一条数据被完整处理, storm会调用这个方法
* @param msgId 消息的唯一编号
*/
@Override
public void ack(Object msgId) {
System.out.println("消息处理成功了, msgId: "+msgId);
super.ack(msgId);
}
/**
* 当spout发送的一条数据被标记为处理失败, storm会调用这个方法
* @param msgId
*/
@Override
public void fail(Object msgId) {
System.out.println("消息处理失败了需要重发, msgId: "+msgId);
//如果发送数据失败后,从map中取出数据再次发送
String msg = msgBuffer.get(msgId);
spoutOutputCollector.emit(new Values(msg), msgId);
}
}
编写bolt类
在execute()方法中处理完所有业务逻辑后需要调用ack()方法
bolt如果产生了新的数据,需要锚定一点,让新产生的tuple与原有tuple关联
public class MyBolt1 extends BaseRichBolt {
private OutputCollector outputCollector;
/**
* 初始化方法
* @param map
* @param topologyContext
* @param outputCollector
*/
public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {
this.outputCollector = outputCollector;
}
/**
* 数据发送
* @param tuple
*/
public void execute(Tuple tuple) {
//获取数据
String line = tuple.getStringByField("line");
String[] words = line.split(" ");
for (String word : words) {
//将新产生的tuple与原有tuple关联
outputCollector.emit(tuple, new Values(word));
}
//bolt对数据完成处理后发出信号
outputCollector.ack(tuple);
//测试消息处理失败
//outputCollector.fail(tuple);
}
/**
* 字段声明
* @param outputFieldsDeclarer
*/
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
outputFieldsDeclarer.declare(new Fields("word"));
}
}
第二个bolt类
public class MyBolt2 extends BaseRichBolt {
private OutputCollector outputCollector;
public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {
this.outputCollector = outputCollector;
}
public void execute(Tuple tuple) {
//打印出数据
System.out.println(tuple.getStringByField("word"));
//bolt对数据完成处理后发出信号
outputCollector.ack(tuple);
}
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
}
}
编写驱动类
public class MyTopology {
public static void main(String[] args) throws InvalidTopologyException, AuthorizationException, AlreadyAliveException {
//通过TopologyBuilder 封装任务信息
TopologyBuilder topologyBuilder = new TopologyBuilder();
//设置spout获取数据
//SpoutDeclarer setSpout(String id, IRichSpout spout, Number parallelism_hint):参数:id, spout对象, 线程数量
topologyBuilder.setSpout("MySpout", new MySpout(), 1);
//设置splitbolt 对句子进行切割
topologyBuilder.setBolt("MyBolt1", new MyBolt1(), 1).shuffleGrouping("MySpout");
//设置wordcountbolt 对单词进行统计
topologyBuilder.setBolt("MyBolt2", new MyBolt2(), 1).shuffleGrouping("MyBolt1");
//准备一个配置文件
Config config = new Config();
//本地模式
LocalCluster localCluster = new LocalCluster();
localCluster.submitTopology("wordcount", config, topologyBuilder.createTopology());
}
}
1.通过手动开启ack机制方式总结:
1)对spout代码进行修改
1)继承BaseRichSpout,重写ack()方法和fail()方法
2)在nextTuple()方法发送数据时绑定msgId(msgId要保证唯一)
2.对bolt代码修改
1)在execute()方法中处理完所有业务逻辑后需要调用ack()方法outputCollector.ack(tuple);
2)bolt如果产生了新的数据,需要锚定一点,让新产生的tuple与原有tuple关联outputCollector.emit(tuple, new Values(word)); 3)如果想测试失败的情况就在execute()方法中调用fail()方法outputCollector.fail(tuple);
方式二: 通过继承BaseBasicBolt类实现
继承BaseBasicBolt后就不需要定义锚点和调用ack()方法了
修改方式一代码中的两个Bolt类(spout类和驱动类与方式一相同)
MyBolt1 :
public class MyBolt1 extends BaseBasicBolt {
public void execute(Tuple tuple, BasicOutputCollector basicOutputCollector) {
//获取数据
String line = tuple.getStringByField("line");
String[] words = line.split(" ");
for (String word : words) {
//发送数据
basicOutputCollector.emit(new Values(word));
}
}
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
//声明字段
outputFieldsDeclarer.declare(new Fields("word"));
}
}
MyBolt12:
public class MyBolt2 extends BaseBasicBolt {
public void execute(Tuple tuple, BasicOutputCollector basicOutputCollector) {
//打印出数据
System.out.println(tuple.getStringByField("word"));
//失败测时
//throw new FailedException("失败测试");
}
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
}
}
通过继承BaseBasicBolt类实现ack机制总结:
bolt类继承了BaseBasicBolt 就不需要手动添加锚点和调用方法发出成功处理声明
如果想测试失败的情况就抛出在bolt类中throw new FailedException(“失败测试”);
3.5 去掉ACK机制
1.spout发送数据是不带上msgid
2.设置acker数:将参数Config.TOPOLOGY_ACKERS设置为0
3.如果你不在意某个消息派生出来的子孙消息的可靠性,则此消息派生出来的子消息在发送时不要做锚定,即在emit方法中不指定输入消息。因为这些子孙消息没有被锚定在任何tuple tree中,因此他们的失败不会引起任何spout重新发送消息
四 Storm运行流程
4.1 storm启动流程
4.1.1 程序员需要做的
启动命令:
1.在nimbus.host所属的机器上启动 nimbus服务
cd /export/servers/storm/bin/
nohup ./storm nimbus &
2.在nimbus.host所属的机器上启动ui服务
cd /export/servers/storm/bin/
nohup ./storm ui &
3.在其它个点击上启动supervisor服务
cd /export/servers/storm/bin/
nohup ./storm supervisor &
客户端运行storm nimbus时会调用storm的python脚本,该脚本为每个命令编写一个方法,每个方法生成响应的java命令
命令格式:java -server xxxx.ClasName -args
4.1.2 nimbus完成的工作
nimbus启动之后,接收客户端提交的任务
提交任务命令格式:storm jar 【jar路径】 【拓扑包名.拓扑类名】 【拓扑名称】
bin/storm jar examples/storm-starter/storm-starter-topologies-0.9.6.jar storm.starter.WordCountTopology wordcount
1.执行main方法,使用topologyBuilder,createTopology序列化spout对象和bolt对象
2.上传用户的jar到nimbus物理节点workerdir/nimbus/inbox,并改名为UUID字符串
4.1.3 supervisor完成的工作
1.通过watch机制感知numbus在zookeeper上的任务分配信息,从zookerper上拉取属于自己的任务信息
2.根据自己的工作任务分配端口启动worker
4.1.4 worker完成的工作
1.连接zookeeper,拉取任务
2.通过反序列化获取spout和bolt对象
3.根据任务类型,执行spout和bolt任务
4.2 任务提交流程图
4.3 手动实现storm数据执行框架
整个处理过程无非就是多线程
mport java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyStorm {
private Random random = new Random();
private BlockingQueue sentenceQueue = new ArrayBlockingQueue(50000);
private BlockingQueue wordQueue = new ArrayBlockingQueue(50000);
// 用来保存最后计算的结果key=单词,value=单词个数
Map<String, Integer> counters = new HashMap<String, Integer>();
//用来发送句子
public void nextTuple() {
String[] sentences = new String[]{"the cow jumped over the moon",
"an apple a day keeps the doctor away",
"four score and seven years ago",
"snow white and the seven dwarfs", "i am at two with nature"};
String sentence = sentences[random.nextInt(sentences.length)];
try {
sentenceQueue.put(sentence);
System.out.println("send sentence:" + sentence);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//用来切割句子
public void split(String sentence) {
System.out.println("resv sentence" + sentence);
String[] words = sentence.split(" ");
for (String word : words) {
word = word.trim();
if (!word.isEmpty()) {
word = word.toLowerCase();
//collector.emit()
wordQueue.add(word);
System.out.println("split word:" + word);
}
}
}
//用来计算单词
public void wordcounter(String word) {
if (!counters.containsKey(word)) {
counters.put(word, 1);
} else {
Integer c = counters.get(word) + 1;
counters.put(word, c);
}
System.out.println("print map:" + counters);
}
public static void main(String[] args) {
//线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
MyStorm myStorm = new MyStorm();
//发射句子到sentenceQuequ
executorService.submit(new MySpout(myStorm));
//接受一个句子,并将句子切割
executorService.submit(new MyBoltSplit(myStorm));
//接受一个单词,并进行据算
executorService.submit(new MyBoltWordCount(myStorm));
}
public BlockingQueue getSentenceQueue() {
return sentenceQueue;
}
public void setSentenceQueue(BlockingQueue sentenceQueue) {
this.sentenceQueue = sentenceQueue;
}
public BlockingQueue getWordQueue() {
return wordQueue;
}
public void setWordQueue(BlockingQueue wordQueue) {
this.wordQueue = wordQueue;
}
}
class MySpout extends Thread {
private MyStorm myStorm;
public MySpout(MyStorm myStorm) {
this.myStorm = myStorm;
}
@Override
public void run() {
//storm框架在循环调用spout的netxTuple方法
while (true) {
myStorm.nextTuple();
try {
this.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyBoltWordCount extends Thread {
private MyStorm myStorm;
@Override
public void run() {
while (true) {
try {
String word = (String) myStorm.getWordQueue().take();
myStorm.wordcounter(word);
} catch (Exception e) {
System.out.println(e);
}
}
}
public MyBoltWordCount(MyStorm myStorm) {
this.myStorm = myStorm;
}
}
class MyBoltSplit extends Thread {
private MyStorm myStorm;
@Override
public void run() {
while (true) {
try {
String sentence = (String) myStorm.getSentenceQueue().take();
myStorm.split(sentence);
} catch (Exception e) {
System.out.println(e);
}
}
}
public MyBoltSplit(MyStorm myStorm) {
this.myStorm = myStorm;
}
}