這是一個JStorm使用教程,不包含環境搭建教程,直接在公司現有集群上跑任務,關于JStorm集群環境搭建,后續研究完會考慮額外寫一篇博客。
你如果想了解JStorm是什么,有多牛逼什么什么的,請看最后的參考博客鏈接,里面有各種版本的介紹,我就不在這里總結這種東西了,我相信這些東西你第一次接觸的時候會看,等學了JStorm之后也不會再去看這些東西了。。。
簡介
-
JStorm和MapReduce的一些對比
compare-table.png
一些關鍵概念
- nimbus:主控節點運行Nimbus守護進程,類似于Hadoop中的ResourceManager,負責在集群中分發代碼,對節點分配任務,并監視主機故障。
- supervisor:每個工作節點運行Supervisor守護進程,負責監聽工作節點上已經分配的主機作業,啟動和停止Nimbus已經分配的工作進程,類似于Hadoop中的NodeManager。
supervisor會定時從zookeeper獲取拓補信息topologies、任務分配信息assignments及各類心跳信息,以此為依據進行任務分配。
在supervisor同步時,會根據新的任務分配情況來啟動新的worker或者關閉舊的worker并進行負載均衡。 - worker:Worker是具體處理Spout/Bolt邏輯的進程,根據提交的拓撲中conf.setNumWorkers(3);定義分配每個拓撲對應的worker數量,Storm會在每個Worker上均勻分配任務,一個Worker只能執行一個topology,但是可以執行其中的多個任務線程。
- task:任務是指Worker中每個Spout/Bolt線程,每個Spout和Bolt在集群中會執行許多任務,每個任務對應一個線程執行,可以通過TopologyBuilder類的setSpout()和setBolt()方法來設置每個Spout或者Bolt的并行度。
- Executor:Task接收到任務就是在Executor中執行的,可以理解為執行Task專門的一個線程。
- topology:Storm中Topology的概念類似于Hadoop中的MapReduce Job,是一個用來編排、容納一組計算邏輯組件(Spout、Bolt)的對象(Hadoop MapReduce中一個Job包含一組Map Task、Reduce Task),這一組計算組件可以按照DAG圖的方式編排起來(通過選擇Stream Groupings來控制數據流分發流向),從而組合成一個計算邏輯更加負責的對象,那就是Topology。一個Topology運行以后就不能停止,它會無限地運行下去,除非手動干預(顯式執行bin/storm kill )或意外故障(如停機、整個Storm集群掛掉)讓它終止。
- spout:Storm中Spout是一個Topology的消息生產的源頭,Spout應該是一個持續不斷生產消息的組件,例如,它可以是一個Socket Server在監聽外部Client連接并發送消息,可以是一個消息隊列(MQ)的消費者、可以是用來接收Flume Agent的Sink所發送消息的服務,等等。Spout生產的消息在Storm中被抽象為Tuple,在整個Topology的多個計算組件之間都是根據需要抽象構建的Tuple消息來進行連接,從而形成流。
- bolt:Storm中消息的處理邏輯被封裝到Bolt組件中,任何處理邏輯都可以在Bolt里面執行,處理過程和普通計算應用程序沒什么區別,只是需要根據Storm的計算語義來合理設置一下組件之間消息流的聲明、分發、連接即可。Bolt可以接收來自一個或多個Spout的Tuple消息,也可以來自多個其它Bolt的Tuple消息,也可能是Spout和其它Bolt組合發送的Tuple消息。
- tuple:JStorm中信息傳輸的單位,Storm程序是無限執行下去的,數據流是無止境的,但是每次驅動程序執行的只是一個數據流單位,就是Tuple,Spout的一次nextTuple以及Bolt的一次execute的執行操作的都是一個Tuple。Tuple只要是任意可序列化對象即可。
生命周期
Topology生命周期
- 上傳代碼并做校驗(/nimbus/inbox);
- 建立本地目錄(/stormdist/topology-id/);
- 建立zookeeper上的心跳目錄;
- 計算topology的工作量(parallelism hint),分配task-id并寫入zookeeper;
- 把task分配給supervisor執行;
- 在supervisor中定時檢查是否有新的task,下載新代碼、刪除老代碼,剩下的工作交個小弟worker;
- 在worker中把task拿到,看里面有哪些spout/Bolt,然后計算需要給哪些task發消息并建立連接;
-
在nimbus將topology終止的時候會將zookeeper上的相關信息刪除;
topology-lifecycle.png
Spout生命周期
提交時
- 構造方法:初始化構造參數,其中包含的必須都是可序列化的
- getComponentConfiguration:獲取該類特殊的配置參數,只和該組件相關的配置,通常
return null
- declareOutputFields:獲取該組件會輸出的流、字段列表,其后續的其他組件訂閱相應的流或者字段需要和這里對應,否則會出錯
- 將內存中的該實例序列化為字節碼文件。
在Worker節點中執行
- 將傳輸過來的字節碼文件反序列化為類實例
- open:初始化這個組件類實例,可以加載消息隊列消費端、JDBC鏈接池等非可序列化對象
- activate:該實例設置為活躍狀態(有數據流驅動時)調用,過段時間暫時沒有數據流驅動就會睡眠
- nextTuple:循環調用,可在這里從數據源獲取數據emit到下一個節點,JStorm就會自動循環調用執行下去
- ack:往后emit的一個Tuple在acker節點察覺成功了,回調通知Spout
- fail:往后emit的一個Tuple在acker節點察覺失敗或者超時了,回調通知Spout
- deactivate:沒數據流驅動達到一段時間,進入睡眠前調用
- close:程序停止時調用,釋放資源
Bolt生命周期
提交時
- 構造方法:初始化構造參數,其中包含的必須都是可序列化的
- getComponentConfiguration:獲取該類特殊的配置參數,只和該組件相關的配置,通常
return null
- declareOutputFields:獲取該組件會輸出的流、字段列表,其后續的其他組件訂閱相應的流或者字段需要和這里對應,否則會出錯
- 將內存中的該實例序列化為字節碼文件。
在Worker節點中執行
- 將傳輸過來的字節碼文件反序列化為類實例
- prepare:初始化這個組件類實例,可以加載配置,數據處理類初始化,數據輸出對象初始化
- execute:循環調用,可在這里從上個節點獲取Tuple,進行相應處理之后emit到下一個節點,JStorm就會自動循環調用執行下去
- cleanup:程序停止時調用,釋放資源
數據流向控制
- ShuffleGrouping:對符合條件的目標Worker,其中可能的多個Task,隨機分配Task來接收和處理該Tuple
- FieldsGrouping:會按照指定的field值進行分配,可以保證相同field對應值的Tuple分配到相同一個Task中執行 —— 可以想象成拿指定的field的值hash取模決定哪個Task(具體算法沒研究)
- 除了這兩個其他暫時沒用到,也感覺剩下的比較用不到,等用了再更
- 具體的流的聚合和分發,參考這篇博客,例子很詳細:JStorm流的匯聚和分發
數據流傳輸過程
- Spout中的數據源取出一份數據(無限循環取出),作為一個Tuple,emit到下一個節點
- 根據Spout中declareOutputFields定義的字段和流,查閱后繼訂閱該節點或者流的Bolt,Tuple會被發送到每個訂閱節點或者流的后繼節點當中
- 后繼訂閱的Bolt節點接收到該Tuple,使用
tuple.getValueByField
通過上一節點declareOutputFields的字段名獲取相應的字段值,也可以根據fields的聲明順序使用tuple.getValue
通過下標獲取相應的值 - 拿到相應的數據之后,進行相關邏輯處理,之后emit到下一個節點當中,以此類推,直到最終節點將數據輸出到mysql、ES、HDFS等存儲系統當中
- 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中主要實現的接口如下:
- execute:雖然和IBolt中名字、參數一致,但是增加了一些默認邏輯
- 入參的input.getValue(0)表示批次(BatchId)。
- 發送消息時collector.emit(new Values(batchId, value)),發送的列表第一個字段表示批次(BatchId)。
- commit:批次成功時調用,常見的是修改offset。
- 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。主要涉及到下面幾種接口:
- 在本地完成的操作
- Function:自定義操作。
- Filters:自定義過濾。
- partitionAggregate:對同批次的數據進行local combiner操作。
- project:只保留stream中指定的field。
- stateQuery、partitionPersist:查詢和持久化。
- 決定Tuple如何分發到下一個處理環節
- shuffle:隨機。
- broadcast:廣播。
- partitionBy:以某一個特定的field進行hash,分到某一個分區,這樣該field位置相同的都會放到同一個分區。
- global:所有tuple發到指定的分區。
- batchGlobal:同一批的tuple被放到相同的分區(不同批次不同分區)。
- partition:用戶自定義的分區策略。
- 不同partition處理結果的匯聚操作
- aggregate:只針對同一批次的數據。
- persistentAggregate:針對所有批次進行匯聚,并將中間狀態持久化。
- 對stream中的tuple進行重新分組,后續的操作將會對每一個分組獨立進行(類似sql中的group by)
- groupBy
- 將多個Stream融合成一個
- merge:多個流進行簡單的合并。
- join:多個流按照某個KEY進行UNION操作(只能針對同一個批次的數據)。
在這里有一個jstorm中使用Trident的簡單例子。
故障恢復
-
節點故障
- Nimbus故障。Nimbus本身無狀態,所以Nimbus故障不會影響正在正常運行任務,另外Nimbus HA保證Nimbus故障后可以及時被備份Nimbus接管。
- Supervisors節點故障。Supervisor故障后,Nimbus會將故障節點上的任務遷移到其他可用節點上繼續運行,但是Supervisor故障需要外部監控并及時手動重啟。
- Worker故障。Worker健康狀況監控由Supervisor負責,當Woker出現故障時,Supervisor會及時在本機重試重啟。
- Zookeeper節點故障。Zookeeper本身具有很好的故障恢復機制,能保證至少半數以上節點在線就可正常運行,及時修復故障節點即可。
-
任務失敗
- Spout失敗。消息不能被及時被Pull到系統中,造成外部大量消息不能被及時處理,而外部大量計算資源空閑。
- Bolt失敗。消息不能被處理,Acker持有的所有與該Bolt相關的消息反饋值都不能回歸到0,最后因為超時最終Spout的fail將被調用。
- Acker失敗。Acker持有的所有反饋信息不管成功與否都不能及時反饋到Spout,最后同樣因為超時Spout的fail將被調用。
- 任務失敗后,需要Nimbus及時監控到并重新分配失敗任務。
JStorm使用感受
- JStorm各個節點之間是松耦合的,各個節點之間的通信只和Tuple數據流結構相關,其他處理邏輯各自獨立
- JStorm不處理數據的存儲服務,計算結果自行存儲到HDFS、HBase、Mysql、ElasticSearch等存儲系統當中
- JStorm的拓撲節點設計中,應該把延時操作分發到多個節點當中執行,每個節點只處理各自單一的功能邏輯,如上面的例子,我把單詞分割和單詞計數分成兩個Bolt來實現,這才是流式計算的特點,讓數據流動起來,而不是在一個節點完成所有工作,也保證了程序可用性更強
- JStorm各個節點內部的處理邏輯非常開放,想怎么處理都行,只要最終往后輸出相應的Tuple即可,編程時非常自由,不像MapReduce,很多操作都在MR模型中得到限制