Storm是什么
Storm是一个分布式实时计算系统
Storm特点
- 速度快 - 每个节点每秒能处理百万级的数据
- 扩展性好 - 集群本身扩展简单;Topology的每个部分的并行数都能轻易的进行调节;线上环境能够随时进行rebalance,重新分配系统资源的占用
- 容错能力强 - 当Woker死掉,Storm会自动重启它;当节点死掉,运行在它上面的Worker会被重新分配到新的节点上运行
- 可靠性 - 通常,Storm保证每个消息至少被处理一次,消息处理失败时Storm会重放消息;如果想保证消息被精确地处理一次,可以使用Trident
- 多语言支持 - Java、Ruby、Python、Javascript、Perl、PHP
- 易于部署和维护 - 部署简单,维护成本低
Storm应用场景
实时分析、在线机器学习、持续计算、分布式RPC、ETLStorm集群中的节点类型
- Nimbus - 在集群中分发代码;指定任务给集群中的工作节点;监控任何失败。类似于Hadoop中的JobTracker
- Supervisor - 集群中的每个工作节点都运行一个Supervisor守护进程,它接收来自Nimbus的任务,开始/停止本节点上的Worker。类似于Hadoop中的TaskTracker
- Worker - 由Supervisor管理,每个Worker是一个JVM,其中可以运行多个线程
- Zookeeper - Nimbus和Supervisor间的协作是通过zookeeper完成的。另外,Nimbus和Supervisor是快速失败且无状态的,所有的状态都保存在Zookeeper中。这样即使Nimbus和Supervisor被Kill -9,它们也能不受任何影响的重新启动。
Storm编程相关概念
- Stream - Storm中的一个核心抽象,指无限的Tuple序列。在Storm中,一个流可以被转换成另一个流,这都是通过Spout和Bolt来完成的
- Tuple - Storm中的数据模型,可以看做一个事件或消息,在Storm的API中可以直接构造和访问。Tuple中的字段是有序且有名字的,顺序、名字及类型完全取决于前一个Spout/Bolt产生该Tuple的逻辑,Tuple本身支持很多常用的数据类型。
- Spout - 读取数据源,产生数据流,供之后的Bolt进行处理。数据源可以是数据库、文件、消息队列等
- Bolt - 接收数据流,对其进行处理。例如:过滤数据、转换流、对流进行聚合和连接、写入数据库等。
- Topology - 是一个完整计算任务的抽象,类似MapReduce中的Job。Topology包含Spout和Bolt,它清晰的定义数据流在Spout和Bolt中如何流转,也定义自身的一些配置,例如Topology所占用的Worker数量、每个Spout/Bolt的并发数等。Topology的生命周期是从成功提交开始,结束于被kill掉。期间任何一个Spout或者Bolt线程死掉,Storm都尽量保证它们能被重新启动。
- Task - 是一个线程,每一个Task都属于某个具体的Spout/Bolt。一个Spout/Bolt的并发数决定了他所拥有的Task数量,而这些Task由Storm均匀的分布到该Topology所有的Worker中。例如:一个Topology的所有Spout/Bolt的并发数总共为300,它拥有50个Worker可用,那么Storm会尽量让每个Woker执行6个Task,这些Task不一定属于同一个Spout/Bolt
Stream groupings
定义了一个Bolt应该接收哪个数据流,这些数据流又该如何在该Bolt的Task之间分配。Storm默认提供7种分配方式- Shuffle grouping:随机分组,每个Task处理的Tuple数量尽量保证相同
- Fields grouping:按字段分组,在分组字段上具有相同值的Tuple会始终被同一个Task处理。利用这一点可以做进程内的缓存。
- All grouping:同一个Tuple会被所有的Task处理
- Global grouping:所有的Tuple都被同一个Task处理,Storm会选择具有最小id的Task
- None grouping:当前与Shuffle grouping作用一样。原意是不希望使用任何分组策略,之后的版本可能会实现不同的策略
- Direct grouping:直接指定要目标Bolt中的哪个Task负责接收并处理Tuple
- Local or shuffle grouping:如果目标Bolt中的一个或多个Task与当前的生产Task运行在同一个Worker中,Tuple将被随机分配到本Worker中的这些Task中,保证Tuple传输是在Worker内进行的;否则使用Shuffle grouping策略。
Storm编程模型
1.编写Spout。对于常见的数据源,会有相应的开源实现,不用自己动手写2.编写Bolt。有多种实现方式,笔者介绍两种比较常用的方式
实现一:实现接口backtype.storm.topology.IRichBolt,或扩展backtype.storm.topology.base.BaseRichBolt
public interface IRichBolt extends IBolt, IComponent {
}
public interface IBolt extends Serializable {
public void prepare(Map map, TopologyContext tc, OutputCollector oc);
public void execute(Tuple tuple);
public void cleanup();
}
public interface IComponent extends Serializable {
public void declareOutputFields(OutputFieldsDeclarer ofd);
public Map<String, Object> getComponentConfiguration();
}
详细说明:
- declareOutputFields - 声明该Bolt将要产生的Tuple,如Tuple中字段的名字,顺序
- getComponentConfiguration - 必要时可以通过这个方法来配置Bolt如何运行
- prepare - 在Topology开始时,负责当前Bolt相关的初始化工作,如初始化相关变量等。
- cleanup - 在Topology终结时,负责当前Bolt相关的清理工作,如释放数据库连接等。Stom不保证这个方法肯定被调用,一个经验就是在kill一个Topology时要给足延迟时间,让他能够做清理工作
- execute - 处理消息。基于输入的Tuple进行业务处理,然后通过OutputCollector发送0至多个Tuple给下一个Bolt,发送完成后必须调用OutputCollector的ack方法来通知Storm该Tuple被当前Bolt处理完毕。
实现二:实现接口backtype.storm.topology.IBasicBolt,或扩展backtype.storm.topology.base.BaseBasicBolt(常用)
与IRichBolt相比,所有的ack都自动触发,不用再手动ack
3.编写Topology
组装所有的Spout和Bolt,设置每个组件的并发数和数据流的流转策略等,例如
KafkaSpout kafkaSpout = new KafkaSpout(config);
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("kafka-spout", kafkaSpout, 3);
builder.setBolt("avro-deserializer", new AvroDeserializeBolt(), 3).shuffleGrouping("kafka-spout");
builder.setBolt("add-location", new IpToLocationBolt(), 3).fieldsGrouping("avro-deserializer", new Fields("ip"));
builder.setBolt("add-mode-group", new MacToModeGroupBolt(), 3).fieldsGrouping("add-location", new Fields("mac"));
builder.setBolt("track-letv-device", new TrackLetvDevice(), 3).fieldsGrouping("add-mode-group", new Fields("mac"));
builder.setBolt("write-hbase", new SmartHbaseWriterBolt(), 6).shuffleGrouping("track-letv-device”);
Storm开发Tips
- 虽然Storm强调并发,但是在开发Bolt时其实完全可以不用考虑并发编程相关的概念。每一个Task都是一个线程,一个Task拥有一个完整的Bolt实例,在prepare中初始化的变量不要使用static修饰符,在execute方法中处理业务时也很少需要考虑并发编程。像数据库连接池可以不需要了、作为缓存也可以用HashMap实现
- 在不需要手动ack的Bolt实现中,产生Tuple的语句放到execute方法的最后一行是个好习惯
- 如果产生的Tuple中含可变的对象(常见的是Map或Collection相关类),绝对不要让两个以上的Bolt/Task同时访问该Tuple,否则经典的ConcurrentModificationException就要让你挠头了
- 在需要Bolt实现缓存功能的时候,要评估当前业务缓存需要的内存,通过增加Task数量来分散内存压力;同时通过Fields grouping能够极大提高缓存命中率
- 由于Storm的理念是快速失败,任何一个异常都会导致Task死掉并重启,所以需要仔细处理各种可能的异常。另外使用OutputCollector的reportError方法可以将异常输出到nimbus的web界面,当然在节点的日志目录中也可以找到
- 当需要保证Tuple必须被处理时,即使有异常发生(如写入数据库时数据库服务不可用),可以抛出backtype.storm.topology.FailedException来保证Storm会replay这个Tuple
- 每个Bolt只干一件事,保证Bolt的实现简单,易于维护和复用
- Storm不是万能的,某些场景下还是要依靠其他的组件完成任务,如数据库、Redis、Memcached等