目录
背景知识
首先我们需要了解这些背景知识:
Basic:
I 基本概念
II tutorial
III 理解topology的并发性
IV 理解消息完全处理
VI 对象的序列化
VII storm常用模式
Advanced:
II 理解storm的消息传递
III TimeCacheMap实现分析
阅读了以上文章后,storm-starter项目是个不错的开始,下载这个项目的源码,学习它的代码。里面包含了storm各种典型应用demo。
有一些基本的概念,官方wiki没有专门说明,但是比较重要。这里首先介绍下,以便后文引用。
一个“task”指的是负责运行spout和bolt代码逻辑的单独一个线程;
一个“worker”指的是负责向多个task投递消息的网络监听程序(独立的进程),每“worker”代理的“task”由其自身在初始化时候创建;
一个“topology”在storm集群中会创建多个“worker”(分布在多个节点上)帮助其完成其定义的计算逻辑;
序列化中间结果
在storm中,任何由spout和bolt发射出去的元组都是动态类型,即可以是任何自定义类型。Storm将元组视为动态类型,而非类似hadoop的静态类型,有深层考虑,具体请参看此处。
发射序列化后的自定义类型对象与发射ASCII字符串相比,有很多优势:
I 支持复杂类型的中间结果(比如含有容器类的中间结果);
II 不再需要在发射和接收元组时进行繁冗的字符串操作;
III 代码可读性大大增强;
storm内置使用kryo来序列化自定义类型的对象。它比jdk自身的序列化高效30倍以上,其带来的序列化开销非常地小。以这样微小的代价,换来以上三个好处,非常值得。
尝试合并中间结果
storm不能单独作为一个个体提供实时的计算服务。向前,它需要一个消息队列的支持,向后则需要一个key-value存储系统的支持(广义上说任何存储系统都可以理解为key-value模式,比如对关系数据库来说,key是主键,value是记录;对文件来说,key是文件名,value是文件内容等等)。此外,由于storm是增量计算,每一条从MQ中取出的消息都是独立的。它基本可以理解为由两部分组成,“身份”信息和值信息。
即:{ {身份} {值} }。
其中“身份”信息可映射到一组存储系统中需要更新的key,而值信息则是做具体更新操作时需要的数据样本。
其实,不仅是原始消息,storm任何bolt吐出的中间结果都由这两部分组成。
理解了以上内容,我们来看一个典型的storm程序需要做哪些事情:
1、 从消息队列中取数据
2、 对原始消息处理得到中间结果A1
对中间结果A1进行处理得到中间结果A2
……
对中间结果An-1进行处理得到An
3、 根据An的“身份”信息和值信息更新存储系统中的计算结果
从中我们可以看出程序读写存储系统的次数依赖于An的消息数量,而An的数量则依赖于An-1的数量,如果能够合并消息数量,后端bolt处理的压力就会极大的减小,这是一个“金字塔”型的收缩结构。
能够合并的中间并需具备相同的“身份”信息。然而,并不是所有的中间结果都是能合并的。
一个中间结果An是否能够合并,决定于下一个task在对An的处理过程中,无论An是否是合并过的结果,都不影响此task处理An过程的正确性。
在“荧光”项目的开发中,我们设计出了一种CombiningBolt可以对开发者透明地进行中间结果合并,只要开发者自己的中间结果类继承了如下的接口:
并且以类似如下的方式定义自己的topology(将CombiningBolt放在两个计算逻辑bolt之间,并且以中间结果的“身份”信息分组,其中CombiningBolt构造函数可以接受一个int型参数,指明合并多少个中间结果后发送合并后的结果):
下图显示的是“荧光”中使用CombiningBolt合并中间结果的效果,可以看到CombiningBolt与最后的StoreResultBolt ack的消息数量正好1:10。这将redis压力降低为未合并前的1/10。
CombiningBolt代码如下所示:
- package com.tencent.admonitor.storm;
- import java.io.Serializable;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.Iterator;
- import java.util.List;
- import java.util.Map;
- import java.util.concurrent.ConcurrentLinkedQueue;
- import org.apache.log4j.Logger;
- import com.tencent.admonitor.Util.Combinable;
- import backtype.storm.task.TopologyContext;
- import backtype.storm.topology.BasicOutputCollector;
- import backtype.storm.topology.OutputFieldsDeclarer;
- import backtype.storm.topology.base.BaseBasicBolt;
- import backtype.storm.tuple.Fields;
- import backtype.storm.tuple.Tuple;
- import backtype.storm.tuple.Values;
- import backtype.storm.utils.TimeCacheMap;
- import backtype.storm.utils.TimeCacheMap.ExpiredCallback;
- /**
- * CombiningBolt is used for combining intermediate objects which implement the Combinable
- * interface.
- *
- *
- */
- public class CombiningBolt extends BaseBasicBolt{
- private static final Logger LOG = Logger.getLogger(CombiningBolt.class);
- private TimeCacheMap<String,Combinable> combiningMap ;
- private ConcurrentLinkedQueue<Object> expiredList;
- private static int DEFAULT_COMBINING_WINDOW = 10;
- private static int DEFAULT_NUM_KEYS = 10000;
- private static int DEFAULT_EXPIRED_SECONDS= 5;
- private static int NUM_BUCKETS = 30;
- private int combiningWindow;
- private int numKeys;
- private int expiredSeconds;
- public CombiningBolt(){
- this(DEFAULT_COMBINING_WINDOW,DEFAULT_NUM_KEYS,DEFAULT_EXPIRED_SECONDS);
- }
- public CombiningBolt(int CombiningWindow, int numKeys, int expiredSeconds){
- this.combiningWindow = CombiningWindow;
- this.numKeys = numKeys;
- this.expiredSeconds = expiredSeconds;
- }
- @Override
- public void prepare(Map stormConf, TopologyContext context) {
- expiredList = new ConcurrentLinkedQueue<Object>();
- ExpiredCallback<String,Combinable> callBack = new CallBackMove<String,Combinable>();
- combiningMap = new TimeCacheMap(expiredSeconds,NUM_BUCKETS,callBack);
- }
- @Override
- public void execute(Tuple input, BasicOutputCollector collector) {
- String ident = input.getString(0);
- Combinable combinable = (Combinable)input.getValue(1);
- if(!combiningMap.containsKey(ident)){
- if(combiningMap.size() >= numKeys){
- collector.emit(new Values(ident,combinable));
- }else{
- combiningMap.put(ident, combinable);
- }
- }else{
- Combinable old = combiningMap.get(ident);
- Combinable new_combinable = old.combine(combinable);
- if(new_combinable.getCombinedCount() >= combiningWindow){
- collector.emit(new Values(ident,new_combinable));
- combiningMap.remove(ident);
- }else{
- combiningMap.put(ident, new_combinable);
- }
- }
- int length = expiredList.size();
- while(length > 0){
- Combinable e = (Combinable)expiredList.poll();
- if(e != null)
- collector.emit(new Values(e.ident(),e));
- length--;
- }
- }
- @Override
- public void declareOutputFields(OutputFieldsDeclarer declarer) {
- declarer.declare(new Fields("ident","combinable"));
- }
- private class CallBackMove<K,V> implements ExpiredCallback<K,V>,Serializable{
- private static final long serialVersionUID = 1L;
- @Override
- public void expire(K key, V val) {
- expiredList.offer(val);
- }
- }
- }
如何使用自定义配置文件?
开发storm应用时可能需要用到自身的配置文件。storm.yaml只能提供给storm自身的配置系统使用。针对后台最广泛使用的ini配置文件,推荐使用ini4j。它是一个轻量的ini配置文件读取器、具有简单易懂的api。
请在spout和bolt的prepare方法中读取配置文件。任何资源类的初始化都需要放到prepare方法中(比如数据库连接,和读取文件索引类等)。
程序部署中的注意事项
I 打包storm程序请不要把依赖打进去,依赖需要单独部署;
II 你需要一种有效的机制将程序的依赖包分发到所有的storm节点上(可上传到nimbus,通过rsync做目录同步);
III 你需要分发的依赖包集合=你的程序所有的依赖包集合 ? storm依赖包与你的依赖包的交集(否则topology会初始化失败)
IV 在所有supervisor节点上自定义配置文件的内容和路径需要完全一致,路径不一致topology会初始化失败,内容不一致,程序的行为则是未定义的;
如何使用日志系统?如何debug?
在经过一番辛勤劳动之后,你的程序编译成功了。但是,这不代表着它会按照你设定的行为去运行。如何debug成为了一个问题。
最基本也是最原始的思路就是log。在所有你认为可能出错的地方log(尤其是在prepare方法中,这里通常做的是资源类的初始化,如果它们初始化失败,topology则会初始化失败,这样能帮助你快速定位到错误原因)。
Storm默认与log4j集成。你可以再$STORM_HOME/log4j/目录下找log4j的配置文件。通过调整它来控制log4j的行为。
不要急于在真实环境下部署你的程序。想要测试的话,还是首先在Local mode模式下观察吧。如果在local mode下测试的结果是符合预期的,而在真实环境下出错,一般应是非代码的因素造成的。你需要检查节点的资源文件、自定义配置文件、依赖包等等。此外在local mode下,所有日志打印信息是直接输出到屏幕上的,这样方便你看的更加清楚。
下面介绍下storm日志的结构:
所有的日志文件都存放在$STORM_HOME/logs/路径下。
在nimbus节点上:nimbus.log记录的是nimbus启动过程中的输出信息,包括启动时间和各个worker和task初始化过程中打印信息等等。ui.log则记录的storm监控程序启动过程中的输出信息,包括启动时间等等。
在supervisor节点上:supervisor.log记录的则是supervisor的相关启动信息。worker-XXX(一个supervisor节点通常部署了多个worker)记录的是消息传递、和任务执行过程中的输出信息(也就是你代码中的日志打印部分)。storm的设计目标之一是让任务(task)部署对用户透明。这样造成了:当你需要观察一个task的日志输出信息时,你不知道到哪个节点的哪个日志文件去找这个信息。因此,还是强烈建议在local mode下debug你的程序,然后在真实环境做好日志告警。