JStorm實時計算框架學習

這是一個JStorm使用教程,不包含環境搭建教程,直接在公司現有集群上跑任務,關于JStorm集群環境搭建,后續研究完會考慮額外寫一篇博客。
你如果想了解JStorm是什么,有多牛逼什么什么的,請看最后的參考博客鏈接,里面有各種版本的介紹,我就不在這里總結這種東西了,我相信這些東西你第一次接觸的時候會看,等學了JStorm之后也不會再去看這些東西了。。。

簡介

  • JStorm和MapReduce的一些對比


    compare-table.png

一些關鍵概念

  1. nimbus:主控節點運行Nimbus守護進程,類似于Hadoop中的ResourceManager,負責在集群中分發代碼,對節點分配任務,并監視主機故障。
  2. supervisor:每個工作節點運行Supervisor守護進程,負責監聽工作節點上已經分配的主機作業,啟動和停止Nimbus已經分配的工作進程,類似于Hadoop中的NodeManager。
    supervisor會定時從zookeeper獲取拓補信息topologies、任務分配信息assignments及各類心跳信息,以此為依據進行任務分配。
    在supervisor同步時,會根據新的任務分配情況來啟動新的worker或者關閉舊的worker并進行負載均衡。
  3. worker:Worker是具體處理Spout/Bolt邏輯的進程,根據提交的拓撲中conf.setNumWorkers(3);定義分配每個拓撲對應的worker數量,Storm會在每個Worker上均勻分配任務,一個Worker只能執行一個topology,但是可以執行其中的多個任務線程。
  4. task:任務是指Worker中每個Spout/Bolt線程,每個Spout和Bolt在集群中會執行許多任務,每個任務對應一個線程執行,可以通過TopologyBuilder類的setSpout()和setBolt()方法來設置每個Spout或者Bolt的并行度。
  5. Executor:Task接收到任務就是在Executor中執行的,可以理解為執行Task專門的一個線程。
  6. topology:Storm中Topology的概念類似于Hadoop中的MapReduce Job,是一個用來編排、容納一組計算邏輯組件(Spout、Bolt)的對象(Hadoop MapReduce中一個Job包含一組Map Task、Reduce Task),這一組計算組件可以按照DAG圖的方式編排起來(通過選擇Stream Groupings來控制數據流分發流向),從而組合成一個計算邏輯更加負責的對象,那就是Topology。一個Topology運行以后就不能停止,它會無限地運行下去,除非手動干預(顯式執行bin/storm kill )或意外故障(如停機、整個Storm集群掛掉)讓它終止。
  7. spout:Storm中Spout是一個Topology的消息生產的源頭,Spout應該是一個持續不斷生產消息的組件,例如,它可以是一個Socket Server在監聽外部Client連接并發送消息,可以是一個消息隊列(MQ)的消費者、可以是用來接收Flume Agent的Sink所發送消息的服務,等等。Spout生產的消息在Storm中被抽象為Tuple,在整個Topology的多個計算組件之間都是根據需要抽象構建的Tuple消息來進行連接,從而形成流。
  8. bolt:Storm中消息的處理邏輯被封裝到Bolt組件中,任何處理邏輯都可以在Bolt里面執行,處理過程和普通計算應用程序沒什么區別,只是需要根據Storm的計算語義來合理設置一下組件之間消息流的聲明、分發、連接即可。Bolt可以接收來自一個或多個Spout的Tuple消息,也可以來自多個其它Bolt的Tuple消息,也可能是Spout和其它Bolt組合發送的Tuple消息。
  9. tuple:JStorm中信息傳輸的單位,Storm程序是無限執行下去的,數據流是無止境的,但是每次驅動程序執行的只是一個數據流單位,就是Tuple,Spout的一次nextTuple以及Bolt的一次execute的執行操作的都是一個Tuple。Tuple只要是任意可序列化對象即可。

生命周期

Topology生命周期

  1. 上傳代碼并做校驗(/nimbus/inbox);
  2. 建立本地目錄(/stormdist/topology-id/);
  3. 建立zookeeper上的心跳目錄;
  4. 計算topology的工作量(parallelism hint),分配task-id并寫入zookeeper;
  5. 把task分配給supervisor執行;
  6. 在supervisor中定時檢查是否有新的task,下載新代碼、刪除老代碼,剩下的工作交個小弟worker;
  7. 在worker中把task拿到,看里面有哪些spout/Bolt,然后計算需要給哪些task發消息并建立連接;
  8. 在nimbus將topology終止的時候會將zookeeper上的相關信息刪除;


    topology-lifecycle.png

Spout生命周期

提交時

  1. 構造方法:初始化構造參數,其中包含的必須都是可序列化的
  2. getComponentConfiguration:獲取該類特殊的配置參數,只和該組件相關的配置,通常return null
  3. declareOutputFields:獲取該組件會輸出的流、字段列表,其后續的其他組件訂閱相應的流或者字段需要和這里對應,否則會出錯
  4. 將內存中的該實例序列化為字節碼文件。

在Worker節點中執行

  1. 將傳輸過來的字節碼文件反序列化為類實例
  2. open:初始化這個組件類實例,可以加載消息隊列消費端、JDBC鏈接池等非可序列化對象
  3. activate:該實例設置為活躍狀態(有數據流驅動時)調用,過段時間暫時沒有數據流驅動就會睡眠
  4. nextTuple:循環調用,可在這里從數據源獲取數據emit到下一個節點,JStorm就會自動循環調用執行下去
  5. ack:往后emit的一個Tuple在acker節點察覺成功了,回調通知Spout
  6. fail:往后emit的一個Tuple在acker節點察覺失敗或者超時了,回調通知Spout
  7. deactivate:沒數據流驅動達到一段時間,進入睡眠前調用
  8. close:程序停止時調用,釋放資源

Bolt生命周期

提交時

  1. 構造方法:初始化構造參數,其中包含的必須都是可序列化的
  2. getComponentConfiguration:獲取該類特殊的配置參數,只和該組件相關的配置,通常return null
  3. declareOutputFields:獲取該組件會輸出的流、字段列表,其后續的其他組件訂閱相應的流或者字段需要和這里對應,否則會出錯
  4. 將內存中的該實例序列化為字節碼文件。

在Worker節點中執行

  1. 將傳輸過來的字節碼文件反序列化為類實例
  2. prepare:初始化這個組件類實例,可以加載配置,數據處理類初始化,數據輸出對象初始化
  3. execute:循環調用,可在這里從上個節點獲取Tuple,進行相應處理之后emit到下一個節點,JStorm就會自動循環調用執行下去
  4. cleanup:程序停止時調用,釋放資源

數據流向控制

jstorm-grouping.png
  1. ShuffleGrouping:對符合條件的目標Worker,其中可能的多個Task,隨機分配Task來接收和處理該Tuple
  2. FieldsGrouping:會按照指定的field值進行分配,可以保證相同field對應值的Tuple分配到相同一個Task中執行 —— 可以想象成拿指定的field的值hash取模決定哪個Task(具體算法沒研究)
  3. 除了這兩個其他暫時沒用到,也感覺剩下的比較用不到,等用了再更
  4. 具體的流的聚合和分發,參考這篇博客,例子很詳細:JStorm流的匯聚和分發

數據流傳輸過程

  1. Spout中的數據源取出一份數據(無限循環取出),作為一個Tuple,emit到下一個節點
  2. 根據Spout中declareOutputFields定義的字段和流,查閱后繼訂閱該節點或者流的Bolt,Tuple會被發送到每個訂閱節點或者流的后繼節點當中
  3. 后繼訂閱的Bolt節點接收到該Tuple,使用tuple.getValueByField通過上一節點declareOutputFields的字段名獲取相應的字段值,也可以根據fields的聲明順序使用tuple.getValue通過下標獲取相應的值
  4. 拿到相應的數據之后,進行相關邏輯處理,之后emit到下一個節點當中,以此類推,直到最終節點將數據輸出到mysql、ES、HDFS等存儲系統當中
  5. emit時可以指定相應的streamId來指定當前的數據要傳輸到的哪個streamId當中(該組件的declareOutputFields需要聲明所需的所有streamId),在Topology構建時后繼節點指定該streamId來訂閱相應的數據。

編程例子講解

  • 這個例子是一個單詞計數程序,通過一組字符串數組中隨機獲取一個檔次作為數據源往后輸出,在后續節點統計各個單詞被獲取的總次數。
  • 包括RandomSentenceSpout、SplitBolt、CountBolt三個節點,各個節點并行度都為1,是一個最簡單的單條鏈式的拓撲,如下


    jstorm-example.jpg

RandomSentenceSpout

  • 表示數據源,這里用從數組隨機獲取一個元素作為模擬數據源獲取,日常開發通常是從MQ中獲取相應數據進行數據流驅動。
/**
 * RandomSentenceSpout實現了IRichSpout接口
 * Spout需要實現的接口可以是:
 *    1,IRichSpout:最基本的Spout,繼承自ISpout, IComponent,沒有任何特殊方法(一般用這個)
 *    2,IControlSpout:繼承自IComponent,包括open,close,activate,deactivate,nextTuple,ack(Object msgId),fail等方法
 */
public class RandomSentenceSpout implements IRichSpout {

    private static final long serialVersionUID = 4058847280819269954L;
    private static final Logger logger = Logger.getLogger(RandomSentenceSpout.class);
    //可以理解為JStorm的數據傳輸管道,通過這個對象將這個組件的數據傳輸到下一個組件當中
    private SpoutOutputCollector _collector;
    //隨機生成對象
    private Random _rand;
    private String component;

    /**
     * Spout初始化的時候調用
     */
    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
        _collector = collector;
        _rand = new Random();
        component = context.getThisComponentId();
    }

    /**
     * 系統框架會不斷調用
     */
    public void nextTuple() {
        //模擬數據源
        String[] sentences = new String[]{"Hello world! This is my first programme of JStorm",
                "Hello JStorm,Nice to meet you!", "Hi JStorm, do you have a really good proformance",
                "Goodbye JStorm,see you tomorrow"};
        //隨機取出字符串
        String sentence = sentences[_rand.nextInt(sentences.length)];
        //將得到的字符串輸出到下一個組件
        //!!!這里Values中值填充順序要和下面declareOutputFields中字段聲明順序一致
        _collector.emit(new Values(sentence), Time.currentTimeSecs());
        Utils.sleep(1000);
    }

    @Override
    public void ack(Object arg0) {
        logger.debug("ACK!");
    }

    public void activate() {
        logger.debug("ACTIVE!");
    }

    public void close() {

    }

    public void deactivate() {

    }

    public void fail(Object arg0) {
        logger.debug("FAILED!");
    }

    /**
     * 聲明框架有哪些輸出的字段
     */
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        //下一個組件通過word這個關鍵字拿到這個組件往后輸出的單詞sentence
        declarer.declare(new Fields("word"));
    }

    public Map<String, Object> getComponentConfiguration() {
        return null;
    }
}

SplitBolt

  • 將獲取的字符串通過空白符分割,并轉為小寫之后輸出到下一個節點。
/**
 * IBasicBolt:繼承自IComponent,包括prepare,execut,cleanup等方法
 */
public class SplitBolt extends BaseBasicBolt {

    private static final long serialVersionUID = 7104767103420386784L;
    private static final Logger logger = Logger.getLogger(SplitBolt.class);
    private String component;

    /**
     * cleanup方法在bolt被關閉的時候調用, 它應該清理所有被打開的資源。(基本只能在local mode使用)
     * 但是集群不保證這個方法一定會被執行。比如執行task的機器down掉了,那么根本就沒有辦法來調用那個方法。
     * cleanup設計的時候是被用來在local mode的時候才被調用(也就是說在一個進程里面模擬整個storm集群),
     * 并且你想在關閉一些topology的時候避免資源泄漏。
     * (非 Javadoc)
     * @see backtype.storm.topology.base.BaseBasicBolt#cleanup()
     */
    @Override
    public void cleanup() {

    }

    //接收消息之后被調用的方法
    @Override
    public void execute(Tuple input, BasicOutputCollector collector) {
        //以下兩個方式獲取前驅節點發送過來的sentence,一個根據fieldName,一個根據字段聲明順序
//        String sentence = input.getValueByField("word");
        String sentence = input.getString(0);
        String[] words = sentence.split("[,|\\s+]");
        for (String word : words) {
            word = word.trim();
            //將非空單詞輸出到下一個節點
            if (!word.isEmpty()) {
                word = word.toLowerCase();
                collector.emit(new Values(word));
            }
        }
    }
    /**
     * prepare方法在worker初始化task的時候調用.
     *
     * prepare方法提供給bolt一個Outputcollector用來發射tuple。
     * Bolt可以在任何時候發射tuple — 在prepare, execute或者cleanup方法里面, 或者甚至在另一個線程里面異步發射。
     * 這里prepare方法只是簡單地把OutputCollector作為一個類字段保存下來給后面execute方法 使用。
     */
    @Override
    public void prepare(Map stromConf, TopologyContext context) {
        component = context.getThisComponentId();
    }

    /**
     * declearOutputFields方法僅在有新的topology提交到服務器,
     * 用來決定輸出內容流的格式(相當于定義spout/bolt之間傳輸stream的name:value格式),
     * 在topology執行的過程中并不會被調用.
     * (非 Javadoc)
     * @see backtype.storm.topology.IComponent#declareOutputFields(backtype.storm.topology.OutputFieldsDeclarer)
     */
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("word"));
    }
}

CountBolt

  • 這個組件接收到的每個值都是單個單詞,通過一個內存Map統計各個單詞總數
  • 后臺設置一個異步線程10s一次輸出當前Map中的各個單詞總數
  • 日常開發通常在這個終端節點將實時計算得到的結果輸出到HDFS、mysql、HBase、ElasticSearch等存儲系統當中
public class CountBolt extends BaseBasicBolt {
    private Integer id;
    private String name;
    private Map<String, Integer> counters;
    private String component;
    private static final Logger LOG = Logger.getLogger(CountBolt.class);
    //異步輸出結果集的子線程
    private AsyncLoopThread statThread;

    /**
     * On create
     */
    @Override
    public void prepare(Map stormConf, TopologyContext context) {
        this.counters = new HashMap<String, Integer>();
        this.name = context.getThisComponentId();
        this.id = context.getThisTaskId();
        //異步循環輸出結果集
        this.statThread = new AsyncLoopThread(new statRunnable());

        LOG.info(stormConf.get("abc") + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        component = context.getThisComponentId();
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("word", "count"));
        // declarer.declareStream("coord-"+"word-counter", new Fields("epoch","ebagNum"));
        // LOG.info("set stream coord-"+component);
    }

    //接收消息之后被調用的方法
    @Override
    public void execute(Tuple input, BasicOutputCollector collector) {
//        String str = input.getString(0);
        String str = input.getStringByField("word");
        if (!counters.containsKey(str)) {
            //單詞計數
            counters.put(str, 1);
        } else {
            //單詞計數
            Integer c = counters.get(str) + 1;
            counters.put(str, c);
        }
    }

    /**
     * 異步輸出結果集的死循環子線程
     */
    class statRunnable extends RunnableCallback {

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {

                }
                LOG.info("\n-- Word Counter [" + name + "-" + id + "] --");
                for (Map.Entry<String, Integer> entry : counters.entrySet()) {
                    LOG.info(entry.getKey() + ": " + entry.getValue());
                }
                LOG.info("");
            }
        }
    }
}

WordCountTopology主入口,拓撲構建

  • 這里通過setSpout和setBolt將上面的三個節點連接成線 —— 即最開始說明的鏈式拓撲圖
  • JStorm提交執行相關執行參數統一寫入一個Properties或Yaml配置文件中,命令行執行第一個參數是該配置文件的路徑
public class WordCountTopology {
    private static Logger LOG = LoggerFactory.getLogger(WordCountTopology.class);

    //裝載配置文件配置參數
    private static Map conf = new HashMap<Object, Object>();

    public static void main(String[] args) throws Exception {
        if (args.length == 0) {
            System.err.println("Please input configuration file");
            System.exit(-1);
        }
        //加載配置文件配置到內存
        LoadConf(args[0]);
        //構建JStorm拓撲
        TopologyBuilder builder = setupBuilder();
        System.out.println("Topology準備提交");
        //提交任務到集群
        submitTopology(builder);
        System.out.println("Topology提交完成");
    }

    //!!!!這里通過setSpout和setBolt設置各個節點之間的連接關系,
    // 是這里把所有各自獨立的節點用線連接起來,構建成一張具體的任務執行拓撲圖
    private static TopologyBuilder setupBuilder() throws Exception {
        TopologyBuilder builder = new TopologyBuilder();
        /*
         * 設置spout和bolt,完整參數為
         * 1,spout的id(即name)
         * 2,spout對象
         * 3,executor數量即并發數,也就是設置多少個executor來執行spout/bolt(此項沒有默認null)
         */
        //setSpout,聲明Spout名稱Id為sentence-spout,并行度1
        builder.setSpout("sentence-spout", new RandomSentenceSpout(), 1);
        //setBolt:SplitBolt的grouping策略是上層隨機分發,CountBolt的grouping策略是按照上層字段分發
        //如果想要從多個Bolt獲取數據,可以繼續設置grouping
        //聲明Bolt名稱Id為split-bolt,并行度1
        builder.setBolt("split-bolt", new SplitBolt(), 1)
                //設置該Bolt的數據源為sentence-spout的輸出
                .shuffleGrouping("sentence-spout");
        //聲明Bolt名稱Id為count-bolt,并行度1
        builder.setBolt("count-bolt", new CountBolt(), 1)
                //設置該Bolt的數據源為sentence-spout和split-bolt的輸出
                //fieldsGrouping保證相同word對應的值發送到同一個Task節點,這是單詞計數業務需要
                .fieldsGrouping("split-bolt", new Fields("word"))
                .fieldsGrouping("sentence-spout", new Fields("word"));
        return builder;
    }

    //提交任務到JStorm集群
    private static void submitTopology(TopologyBuilder builder) {
        try {
            if (local_mode(conf)) {//本地模式,需要有本地JStorm環境支持
                LocalCluster cluster = new LocalCluster();
                cluster.submitTopology(
                        String.valueOf(conf.get("topology.name")), conf,
                        builder.createTopology());

                Thread.sleep(200000);
                cluster.shutdown();
            } else {
                StormSubmitter.submitTopology(
                        String.valueOf(conf.get("topology.name")), conf,
                        builder.createTopology());
            }
        } catch (Exception e) {
            LOG.error(e.getMessage(), e.getCause());
        }
    }

    //加載Properties配置文件
    private static void LoadProperty(String prop) {
        Properties properties = new Properties();
        try {
            InputStream stream = new FileInputStream(prop);
            properties.load(stream);
        } catch (FileNotFoundException e) {
            System.out.println("No such file " + prop);
        } catch (Exception e1) {
            e1.printStackTrace();

            return;
        }
        conf.putAll(properties);
    }

    //加載Yaml配置文件
    private static void LoadYaml(String confPath) {
        Yaml yaml = new Yaml();
        try {
            InputStream stream = new FileInputStream(confPath);
            conf = (Map) yaml.load(stream);
            if (conf == null || conf.isEmpty() == true) {
                throw new RuntimeException("Failed to read config file");
            }
        } catch (FileNotFoundException e) {
            System.out.println("No such file " + confPath);
            throw new RuntimeException("No config file");
        } catch (Exception e1) {
            e1.printStackTrace();
            throw new RuntimeException("Failed to read config file");
        }
    }

    //根據后綴名選擇加載配置文件方案
    private static void LoadConf(String arg) {
        if (arg.endsWith("yaml")) {
            LoadYaml(arg);
        } else {
            LoadProperty(arg);
        }
    }

    public static boolean local_mode(Map conf) {
        String mode = (String) conf.get(Config.STORM_CLUSTER_MODE);
        if (mode != null) {
            if (mode.equals("local")) {
                return true;
            }
        }
        return false;
    }
}

批量用法

基本的用法是每次處理一個tuple,但是這種效率比較低,很多情況下是可以批量獲取消息然后一起處理,批量用法對這種方式提供了支持。打開代碼可以很明顯地發現jstorm和storm的有著不小的區別:

// storm 中的定義
public interface IBatchSpout extends Serializable {
    void open(Map conf, TopologyContext context);
    void emitBatch(long batchId, TridentCollector collector);// 批次發射tuple
    void ack(long batchId); // 成功處理批次
    void close();
    Map getComponentConfiguration();
    Fields getOutputFields();
}
// jstorm中的定義
public interface IBatchSpout extends IBasicBolt, ICommitter, Serializable {
}

另外如果用批次的話就需要改用BatchTopologyBuilder來構建拓撲結構,在IBatchSpout中主要實現的接口如下:

  1. execute:雖然和IBolt中名字、參數一致,但是增加了一些默認邏輯
  • 入參的input.getValue(0)表示批次(BatchId)。
  • 發送消息時collector.emit(new Values(batchId, value)),發送的列表第一個字段表示批次(BatchId)。
  1. commit:批次成功時調用,常見的是修改offset。
  2. revert:批次失敗時調用,可以在這里根據offset取出批次數據進行重試。

Ack機制

  • 為保證無數據丟失,Storm/JStorm使用了非常漂亮的可靠性處理機制,如圖當定義Topology時指定Acker,JStorm除了Topology本身任務外,還會啟動一組稱為Acker的特殊任務,負責跟蹤Topolgogy DAG中的每個消息。每當發現一個DAG被成功處理完成,Acker就向創建根消息的Spout任務發送一個Ack信號。Topology中Acker任務的并行度默認parallelism hint=1,當系統中有大量的消息時,應該適當提高Acker任務的并行度。

  • Acker按照Tuple Tree的方式跟蹤消息。當Spout發送一個消息的時候,它就通知對應的Acker一個新的根消息產生了,這時Acker就會創建一個新的Tuple Tree。當Acker發現這棵樹被完全處理之后,他就會通知對應的Spout任務。

  • Acker任務保存了數據結構Map<MessageID,Map< TaskID, Value>>,其中MessageID是Spout根消息ID,TaskID是Spout任務ID,Value表示一個64bit的長整型數字,是樹中所有消息的隨機ID的異或結果。通過TaskID,Acker知道當消息樹處理完成后通知哪個Spout任務,通過MessageID,Acker知道屬于Spout任務的哪個消息被成功處理完成。Value表示了整棵樹的的狀態,無論這棵樹多大,只需要這個固定大小的數字就可以跟蹤整棵樹。當消息被創建和被應答的時候都會有相同的MessageID發送過來做異或。當Acker發現一棵樹的Value值為0的時候,表明這棵樹已經被成功處理完成。

  • 舉例說明具體流程,以下為拓撲:


    ack-example.jpg
  • Acker數據的變化過程:(算法)

Step1:A發送T0給B后:
產生一個隨機數r0,樹種存R0:R0=r0
<id0,<taskA,R0>>
# ---------
Step2.B接收到T0并成功處理后向C發送T1,向D發送T2:
接收到上級傳過來的R0,自己傳給兩個下家,產生兩個隨機數代表下家存入樹中:R1=R0^r1^r2=r0^r1^r2
<id0,<taskA,R0^R1>>
=<id0,<taskA,r0^r0^r1^r2>>
=<id0,<taskA,r1^r2>>
# ---------
Step3.C接收到T1并成功處理后:
接收到上家傳過來的r1,沒有下家:R2=r1
<id0,<taskA,r1^r2^R2>>
=<id0,<taskA,r1^r2^r1>>
=<id0,<taskA,r2>>
# ---------
Step4.D接收到T2并成功處理后:
接收到上家傳過來的r2,沒有下家:R3=r2
<id0,<taskA,r2^R3>>
=<id0,<taskA,r2^r2>>
=<id0,<taskA,0>>

當結果為0時Acker可以通知taskA根消息id0的消息樹已被成功處理完成,調用Spout的ack方法通知,若超時發現消息樹中值不為0,調用Spout中的fail。

  • 整體節點間通信:


    jstorm-ack.png
  • 需要指出的是,Acker并不是必須的,當實際業務可以容忍數據丟失情況下可以不用Acker,對數據丟失零容忍的業務必須打開Acker,另外當系統的消息規模較大是可適當增加Acker的并行度。

JStorm事務

事務拓撲并不是新的東西,只是在原始的ISpout、IBolt上做了一層封裝。在事務拓撲中以并行(processing)和順序(commiting)混合的方式來完成任務,使用Transactional Topology可以保證每個消息只會成功處理一次。不過需要注意的是,在Spout需要保證能夠根據BatchId進行多次重試,在這里有一個基本的例子,這里有一個不錯的講解。

Trident

這次一種更高級的抽象(甚至不需要知道底層是怎么map-reduce的),所面向的不再是spout和bolt,而是stream。主要涉及到下面幾種接口:
  1. 在本地完成的操作
  • Function:自定義操作。
  • Filters:自定義過濾。
  • partitionAggregate:對同批次的數據進行local combiner操作。
  • project:只保留stream中指定的field。
  • stateQuery、partitionPersist:查詢和持久化。
  1. 決定Tuple如何分發到下一個處理環節
  • shuffle:隨機。
  • broadcast:廣播。
  • partitionBy:以某一個特定的field進行hash,分到某一個分區,這樣該field位置相同的都會放到同一個分區。
  • global:所有tuple發到指定的分區。
  • batchGlobal:同一批的tuple被放到相同的分區(不同批次不同分區)。
  • partition:用戶自定義的分區策略。
  1. 不同partition處理結果的匯聚操作
  • aggregate:只針對同一批次的數據。
  • persistentAggregate:針對所有批次進行匯聚,并將中間狀態持久化。
  1. 對stream中的tuple進行重新分組,后續的操作將會對每一個分組獨立進行(類似sql中的group by)
  • groupBy
  1. 將多個Stream融合成一個
  • merge:多個流進行簡單的合并。
  • join:多個流按照某個KEY進行UNION操作(只能針對同一個批次的數據)。

這里有一個jstorm中使用Trident的簡單例子。

故障恢復

  1. 節點故障

    • Nimbus故障。Nimbus本身無狀態,所以Nimbus故障不會影響正在正常運行任務,另外Nimbus HA保證Nimbus故障后可以及時被備份Nimbus接管。
    • Supervisors節點故障。Supervisor故障后,Nimbus會將故障節點上的任務遷移到其他可用節點上繼續運行,但是Supervisor故障需要外部監控并及時手動重啟。
    • Worker故障。Worker健康狀況監控由Supervisor負責,當Woker出現故障時,Supervisor會及時在本機重試重啟。
    • Zookeeper節點故障。Zookeeper本身具有很好的故障恢復機制,能保證至少半數以上節點在線就可正常運行,及時修復故障節點即可。
  2. 任務失敗

    • Spout失敗。消息不能被及時被Pull到系統中,造成外部大量消息不能被及時處理,而外部大量計算資源空閑。
    • Bolt失敗。消息不能被處理,Acker持有的所有與該Bolt相關的消息反饋值都不能回歸到0,最后因為超時最終Spout的fail將被調用。
    • Acker失敗。Acker持有的所有反饋信息不管成功與否都不能及時反饋到Spout,最后同樣因為超時Spout的fail將被調用。
    • 任務失敗后,需要Nimbus及時監控到并重新分配失敗任務。

JStorm使用感受

  1. JStorm各個節點之間是松耦合的,各個節點之間的通信只和Tuple數據流結構相關,其他處理邏輯各自獨立
  2. JStorm不處理數據的存儲服務,計算結果自行存儲到HDFS、HBase、Mysql、ElasticSearch等存儲系統當中
  3. JStorm的拓撲節點設計中,應該把延時操作分發到多個節點當中執行,每個節點只處理各自單一的功能邏輯,如上面的例子,我把單詞分割和單詞計數分成兩個Bolt來實現,這才是流式計算的特點,讓數據流動起來,而不是在一個節點完成所有工作,也保證了程序可用性更強
  4. JStorm各個節點內部的處理邏輯非常開放,想怎么處理都行,只要最終往后輸出相應的Tuple即可,編程時非常自由,不像MapReduce,很多操作都在MR模型中得到限制

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容