Date: Nov 17-24, 2017
1. 目的
- 積累Storm為主的流式大數據處理平臺對實時數據處理的相關技術
- 積累快捷的Storm部署、開發方式,例如Python和Java。
2. 閱讀資料
- Apache Storm官網Tutorial
- 阿里巴巴JStorm文檔
- intsmaze's blog
- Java 基礎 Serializable 的使用
- Java 高級 Serializable 序列化的源碼分析
- ITindex Storm 系列
3. 閱讀筆記
3.1 Apache Storm官網
3.1.1 Storm主要結構概覽
如上圖所示,Storm是一個流數據處理平臺。它與Hadoop相近,采用Map-Reduce的計算框架,區別在于Hadoop的worker在完成工作后被釋放,而Storm的worker在完成工作后進入等待狀態——等待“上級”分配下一個任務。
Storm的本質是定義一個計算的過程,類似于設計中的數據流圖,即先定義數據處理的流程,再分模塊實現數據處理的細節,結果由末端的節點返回或輸出。
Storm的核心是Clojure
編寫、提供Java
開發接口,核心離工業解主流編程語言(Java
、C/C++
)相對遙遠,阿里巴巴的工程師團隊用Java
重寫了Storm的核心,即為JStorm
。
3.2 阿里巴巴JStorm
3.2.1 JStorm定位
JStorm 是一個分布式實時計算引擎。
JStorm 是一個類似Hadoop MapReduce的系統, 用戶按照指定的接口實現一個任務,然后將這個任務遞交給JStorm系統,JStorm將這個任務跑起來,并且按7 * 24小時運行起來,一旦中間一個Worker 發生意外故障, 調度器立即分配一個新的Worker替換這個失效的Worker。
因此,從應用的角度,JStorm應用是一種遵守某種編程規范的分布式應用。從系統角度, JStorm是一套類似MapReduce的調度系統。 從數據的角度,JStorm是一套基于流水線的消息處理機制。
JStorm | Hadoop | |
---|---|---|
角色 | Nimubs | JobTracker |
Supervisor | TaskTracker | |
Worker | Child | |
應用名稱 | Topology | Job |
編程接口 | Spout/Bolt | Mapper/Reducer |
“設計模式” | 資本主義 | 恐怖主義 |
3.2.2 優點
在Storm和JStorm出現以前,市面上出現很多實時計算引擎,但自Storm和JStorm出現后,基本上可以說一統江湖: 究其優點:
- 開發非常迅速:接口簡單,容易上手,只要遵守Topology、Spout和Bolt的編程規范即可開發出一個擴展性極好的應用,底層RPC、Worker之間冗余,數據分流之類的動作完全不用考慮
- 擴展性極好:當一級處理單元速度,直接配置一下并發數,即可線性擴展性能
- 健壯強:當Worker失效或機器出現故障時, 自動分配新的Worker替換失效Worker
- 數據準確性:可以采用Ack機制,保證數據不丟失。 如果對精度有更多一步要求,采用事務機制,保證數據準確。
- 實時性高: JStorm 的設計偏向單行記錄,因此,在時延較同類產品更低
3.2.3 應用場景
JStorm處理數據的方式是基于消息的流水線處理, 因此特別適合無狀態計算,也就是計算單元的依賴的數據全部在接受的消息中可以找到, 并且最好一個數據流不依賴另外一個數據流。
因此,常常用于:
- 日志分析,從日志中分析出特定的數據,并將分析的結果存入外部存儲器如數據庫。目前,主流日志分析技術就使用JStorm或Storm
- 管道系統, 將一個數據從一個系統傳輸到另外一個系統, 比如將數據庫同步到Hadoop
- 消息轉化器, 將接受到的消息按照某種格式進行轉化,存儲到另外一個系統如消息中間件
- 統計分析器, 從日志或消息中,提煉出某個字段,然后做count或sum計算,最后將統計值存入外部存儲器。中間處理過程可能更復雜。
- 實時推薦系統, 將推薦算法運行在jstorm中,達到秒級的推薦效果
3.2.4 基本概念
[站外圖片上傳中...(image-96ea41-1511406796108)]
- Spout (中文意為水龍頭)即數據的來源、出水口,來源可以是Kafka、DB、HBase、HDFS等。
- Bolt(中文意為插銷)即數據流向過程中的關鍵點、數據流處理點。
- Topology(中文意為拓撲結構)即上述圖中所示的數據處理流程形成的數據流網絡結構。
3.2.5 組件接口
- Spout組件接口:
nextTuple
拉取下一條消息,執行時JStorm框架回不停調用該接口從數據源拉取數據發往Bolt。 - Bolt組件接口:
execute
執行處理邏輯
3.2.6 調度和執行
對于一個Topology,JStorm調度一個/多個Worker (每個Worker對應操作系統的進程),分布到集群的一臺或多臺機器上并行執行。
在一個Worker (進程) 中,分為多個Task (線程),每個線程對應于Spout/Bolt的實現。
工作流程:
- 根據業務設計Topology
- 根據業務流程實現Spout的
nextTuple
接口中的數據輸入- 根據業務細節實現Bolt的
execute
接口中的處理邏輯- 提交Topology開始執行
3.2.6.1 提交Topology時的參數
總Worker數目
if #worker <= 10 then
_topology_master 以Task形式存在,不獨占Worker
else
_topology_master 以Task形式存在,獨占Worker
end
每個component的并行度
并行度(parallelism) 代表有多少個Task線程來執行這個Spout/Bolt。
同一個Component中的Task id一定是連續的。
每個Component之間的關系
聲明Spout和Bolt之間的對應關系,JStorm使用均勻調度算法,奇偶不同數目的Spout/Bolt會存在某個進程只有Spout或只有Bolt的情形。若topology運行過程中掛掉,JStorm會不斷嘗試重啟進程。
3.2.7 消息通信
Spout發消息
-
JStorm 計算消息目標 Task Id列表
if Task_id 在本進程 then 直接將消息放入目標Task執行隊列 else netty跨進程發送至目標Task中 end
3.2.8 實時計算結果輸出
JStorm的Spout或Bolt中會有一個定時往外部存儲寫計算結果的邏輯,將數據按照業務需求被實時或近似實時地輸出。
3.2.9 小結
JStorm是阿里巴巴平臺的產品,相對來說適用于大量數據集群的情況,目前我現有的資源很難使用。因此,選擇Python系的streamparser進行閱讀。
3.3 折騰Storm平臺部署
3.3.1 部署storm平臺
下載[Java 8/9][1]、maven、zookeeper、storm、[lein][2]的release并依次安裝。(以上庫除lein外為storm運行所必須,由于服務器在國外,下載時間較長)
-
將 JDK、maven、zookeeper、storm 等拷貝至
/opt
目錄下,在~/.bash_profile
中將相應目錄加入PATH
:export JAVA_HOME="/opt/jdk8" export MAVEN_HOME="/opt/maven" export ZOO_KEEPER_HOME="/opt/zookeeper" export STORM_HOME="/opt/storm" PATH=$STORM_HOME/bin:$ZOO_KEEPER_HOME/bin:$MAVEN_HOME/bin:JAVA_HOME/bin:$PATH export PATH
?- 進入
/opt/zookeeper/conf
目錄,編輯zoo.cfg
配置文件,如下:tickTime=2000 initLimit=10 syncLimit=5 dataDir=/var/zookeeper # 注意需要對該目錄有寫權限 clientPort=2181
-
進入
/opt/storm/conf
目錄,編輯storm.yaml
配置文件,如下:storm.zookeeper.servers: # 注意此處有空格 - "10.211.55.37" # 填入配置機器的IP,若為集群則在下一行以同樣格式列出 # - "other server ip" # 此處為Nimbus服務器地址,單機運行時無效,系統自動使用本地hostname,[原因待求證] # nimbus.seeds:["host1","host2","host3"] storm.local.dir: "/var/storm" # 需要保證該目錄有寫權限,此處使用root賬戶所以不考慮。 # 設置supervisor slots supervisor.slots.ports: # 注意此處有空格 - 6700 - 6701 - 6702 - 6702 # 此處在storm 1.1.1版的配置模板文件中未提及,但配置后在集群中能看到,[原因待求證]
?
-
啟動zookeeper集群
bin/zkServer.sh start
?
-
在[Master服務器][3]上啟動storm nimbus服務
bin/storm nimbus >> /dev/null &
-
在[Worker服務器][3]上啟動storm supervisor服務
bin/storm supervisor >> /dev/null &
-
在[Master服務器][3]上啟動storm UI工具
bin/storm ui &
在[Master服務器][3]上采用
jps
查看服務的啟動情況,若顯示config_value
則表示服務正在初始化;若顯示nimbus
、supervisor
、core
、jps
、QuorumPeerMain
則說明初始化完畢,打開瀏覽器輸入http://server_host:8080
即可進入Storm UI查看相關信息。
[1] Java8/9 推薦安裝Oracle官網下載的完整版JDK,因為后續的 lein 需要完整的JDK。解壓JDK之后配置系統變量即可。(本次Linux機器采用 Java 8)
[2] lein全稱為leiningen,是自動化管理Clojure腳本的工具,類似于Cargo。lein目前的腳本下載會出現證書不匹配的問題,解決方案為export HTTP_CLIENT="wget --no-check-certificate -O"
。而且,上述設置后,下載release依舊很慢、需要代理,可以直接wget
下載對應的release,放到~/.lein/self-installs/leiningen-2.5.3-standalone.jar
即可,參考這里。lein
是一個可執行腳本,需要放到/usr/bin
或者/usr/local/bin
下面,然后命令行中運行./lein
和lein repl
完成安裝。
[3] 本地測試則僅僅在本機即可
3.3.2 案例工程 WordCount
主要參照《Get Started with Storm》一書,網上有中文版,此處參照為英文原版。
3.3.2.1 前提準備
-
maven
編譯工具,建立pom.xml
來聲明該工程的編譯結構,包括注明編譯需要的maven
版本、編譯所需的storm依賴庫在線地址、以及依賴的storm版本。<repositories> <!-- Repository where we can found the storm dependencies --> <repository> <id>clojars.org</id> <url>http://clojars.org/repo</url> </repository> </repositories>
3.3.2.2 編寫對應代碼文件
1. 建立文件結構
建立對應的文件結構src/main/java/{spouts,bolts}
、/src/main/resources
等,其中resources
文件夾要存放相應的資源文件。
2. 編寫spouts
實例
package spouts;
import ....;
public class WordReader implements IRichSpout {
private .....;
public void ack(Object msgId) {...;}
public void fail(Object msgId) {...;}
public void nextTuple() {...;}
// first method called in ANY spout
public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {...;}
public void close() {}
public void declareOutputFields(OutputFieldsDeclarer declarer) {...;}
}
3. 編寫bolts
實例
package bolts;
import ...;
public class WordNormalizer implements IRichBolt {
private ...;
public void cleanup(){}
public void execute(Tuple input) {...;}
public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {...;}
public void declareOutputFields(OutputFieldsDeclarer declarer) {...;}
}
4. 編寫topology
結構
import ...;
public class TopologyMain {
public static void main(String[] args) throws InterruptedException {
// Topology definition
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout();
builder.setBolt().shuffleGrouping();
builder.setBolt().fieldGrouping();
// Configuration
Config conf = new Config();
conf.put("xxx", args[0]);
conf.setDebug(false);
// Topology run
conf.put(Config.TOPOLOGY_MAX_SPOUT_PENDING, 1);
LocalCluster cluster = new LocalCluster();
cluster.submitTopology("xxxx",conf, builder.createTopology());
Thread.sleep(1000); // sleep to reduce server load
cluster.shutdown();
}
}
5. 使用mvm
帶好相應參數運行
mvn clean install # maven會自動下載相關的包
cd target # 注意目錄下有 `pom.xml` 中標識的輸出的jar包
storm jar output-jar.jar path.to.your.topology # LocalCluster 執行,然后關閉
storm jar output-jar.jar path.to.your.topology name-of-storm # 提交jar至storm集群,循環執行,可在UI中查看
3.4 運行Storm例子程序的問題記錄
3.4.1 存在問題以及解答記錄
1. 程序中的collector
是指的什么?
collector
是用來追蹤處理邏輯上每個emit
的數據是否在下游bolt中被成功處理。collector
是與storm通信的工具,反饋每個任務的處理情況。
2. 程序中collector
最后emit
的Value(…)
是什么結構?
官方文檔解釋:A convenience class for making tuple values using new Values("field1", 2, 3)
syntax.
Value
是構建Tuple
的一個元組類,該類實現了Serializable, Cloneable, Iterable<Object>, Collection<Object>, List<Object>, RandomAccess
等接口,與Bolt中的execute
接口相對應:public void execute(Tuple input, BasicOutputCollector collector)
3. Storm中的Ack/Fail機制中對fail
情形的處理?
為了保證數據能正確的被處理, 對于spout產生的每一個tuple
, storm都會進行跟蹤。
這里面涉及到ack
/fail
的處理,如果一個tuple
處理成功是指這個Tuple以及這個Tuple產生的所有Tuple都被成功處理, 會調用spout的ack
方法;
如果失敗是指這個Tuple或這個Tuple產生的所有Tuple中的某一個tuple處理失敗, 則會調用spout的fail
方法;
在處理tuple
的每一個bolt都會通過OutputCollector來告知storm, 當前bolt處理是否成功。
另外需要注意的,當spout觸發fail
動作時,不會自動重發失敗的tuple
,需要我們在spout中重新獲取發送失敗數據,手動重新再發送一次。
4. Storm中的Ack原理
Storm中有個特殊的task名叫acker
,他們負責跟蹤spout發出的每一個Tuple的Tuple樹(因為一個tuple通過spout發出了,經過每一個bolt處理后,會生成一個新的tuple發送出去)。當acker(框架自啟動的task)發現一個Tuple樹已經處理完成了,它會發送一個消息給產生這個Tuple的那個task。
Acker的跟蹤算法是Storm的主要突破之一,對任意大的一個Tuple樹,它只需要恒定的20字節就可以進行跟蹤。
Acker跟蹤算法的原理:acker對于每個spout-tuple保存一個ack-val的校驗值,它的初始值是0,然后每發射一個Tuple或Ack一個Tuple時,這個Tuple的id就要跟這個校驗值異或一下,并且把得到的值更新為ack-val的新值。那么假設每個發射出去的Tuple都被ack了,那么最后ack-val的值就一定是0。Acker就根據ack-val是否為0來判斷是否完全處理,如果為0則認為已完全處理。
要實現ack機制:
- spout發射tuple的時候指定messageId
- spout要重寫BaseRichSpout的fail和ack方法
- spout對發射的tuple進行緩存(否則spout的fail方法收到acker發來的messsageId,spout也無法獲取到發送失敗的數據進行重發),看看系統提供的接口,只有msgId這個參數,這里的設計不合理,其實在系統里是有cache整個msg的,只給用戶一個messageid,用戶如何取得原來的msg貌似需要自己cache,然后用這個msgId去查詢,太坑爹了
- spout根據messageId對于ack的tuple則從緩存隊列中刪除,對于fail的tuple可以選擇重發。
- 設置acker數至少大于0;Config.setNumAckers(conf, ackerParal);
Storm的Bolt有BasicBolt
和RichBolt
:
在BasicBolt中,BasicOutputCollector
在emit數據的時候,會自動和輸入的tuple相關聯,而在execute
方法結束的時候那個輸入tuple會被自動ack。
使用RichBolt
需要在emit
數據的時候,顯式指定該數據的源tuple要加上第二個參數anchor tuple,以保持tracker鏈路,即collector.emit(oldTuple, newTuple)
;并且需要在execute
執行成功后調用OutputCollector.ack(tuple)
,當失敗處理時,執行OutputCollector.fail(tuple)
。
由一個tuple產生一個新的tuple稱為:anchoring,你發射一個tuple的同時也就完成了一次anchoring。
ack機制即,spout發送的每一條消息,在規定的時間內,spout收到Acker的ack
響應,即認為該tuple
被后續bolt成功處理;在規定的時間內(默認是30秒),沒有收到Acker的ack響應tuple,就觸發fail
動作,即認為該tuple處理失敗,timeout時間可以通過Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS
來設定;或者收到Acker發送的fail
響應tuple,也認為失敗,觸發fail
動作
注意,如果繼承BaseBasicBolt那么程序拋出異常,程序直接異常停止了,不會讓spout進行重發。
5. Fail注意點小結
- 若某個task節點處理的tuple一直失敗,會導致spout節點存儲的tuple數據越來越多,直至內存溢出
- 在某個tuple的眾多子tuple中,若某一個子tuple處理fail,但是其他子tuple仍會執行。即當所有子tuple都執行數據存儲操作,其中一個子tuple出現fail,即使整個處理是fail,但是成功的子tuple仍會執行而不會滾。
- Tuple的追蹤只要是spout開始,可以在任意層次bolt停止追蹤并作出應答。
acker
的數量可以通過Ackertask
組件來設置。 - 一個Topology并不需要太多
acker
,除非storm吞吐量不正常。 - 若不需要保證可靠性,即不追蹤tuple樹的執行情況,則系統里的消息數量會減少一半。
- 關閉消息可靠性的三種方法:
config.Topology_ACKERS=0
- Spout發送消息時不指定消息的
msgId
- 在
emit
方法中不指定輸入消息
6. Anchoring
錨定概念
拓撲是一個消息(Tuple)沿著一個或多個分支的樹節點,每個節點將ack(tuple)或者fail(tuple),這樣當消息失敗時Storm就會知道,并通知Spout重發消息。因為一個Storm拓撲運行在一個高度并發的環境中,跟蹤原始Spout示例的最好辦法是在消息Tuple中包含一個原始Spout的引用,這種行為(技術)被稱為Anchoring(錨定)。
錨點發生的語句在collector.emit(tuple, new Values(word))
中,傳遞元組(emit
方法)使Storm能夠跟蹤原始Spout。collector.ack(tuple)
和collector.fail(tuple)
告訴Spout知道每個消息的處理結果。當消息樹上的每個消息已經被處理,Storm認為來自Spout的元組被完全處理。當消息樹在一個可配置的超時內處理失敗,一個元組被認為是失敗的。處理的每一個元組必須是確認或者失敗,Storm會使用內存來追蹤每個元組,如果不對每個元組進行確認/失敗,最終會耗盡內存。
為了簡化編碼,Storm為Bolt提供了一個IBasicBolt
接口,它會在調用execute()
方法之后正確調用ack()
方法,BaseBasicBolt
類是該接口的一個實現,用來實現自動確認。
7. Storm組件與編程時遇到的概念
名稱 | 解釋 |
---|---|
Nimbus | 負責資源分配和任務調度,Nimbus分配任務到Zookeeper指定目錄。 |
Supervisor | 去Zookeeper指定目錄接受Nimbusf分配的任務,啟停自己的Worker進程。 |
Worker | 運行具體處理組件邏輯的進程(process),Worker的任務分為Spout和Bolt兩種。 |
Task | Worker啟動相應的物理線程(Executor),Worker執行的每一個Spout/Bolt線程成為一個Task,0.8版本后Spout/Bolt的Task可能共享一個Executor。 |
Topology | 拓撲,Storm集群,即定義的數據流處理的DAG。 |
Spout | Storm集群的數據源 |
Bolt | Storm任務的處理邏輯單元,在集群多個機器上并發執行。 |
Tuple | 消息元組,Spout、Bolt用來與Storm集群通信、反饋任務處理成功與否的載體。恒定為20Bit。 |
Stream groupings | 數據流的分組策略,分7種,常見為shuffleGrouping() 、fieldsGrouping() 。 |
Executor | Worker啟動的實際物理線程,一般一個Executor執行一個Task,但也能執行多個Task。 |
Configuration | Topology的配置 |
8. 序列化與反序列化
由于博客上特別提到了Java虛擬機序列化的性能極其辣雞,所以在此記錄。
把對象轉換為字節序列的過程稱為對象的序列化;把字節序列恢復為對象的過程稱為對象的反序列化。
對象的序列化主要有兩種用途:
把對象的字節序列永久地保存到硬盤上,通常存放在一個文件中;
在網絡上傳送對象的字節序列。
在很多應用中,需要對某些對象進行序列化,讓它們離開內存空間,入住物理硬盤,以便長期保存。比如最常見的是Web服務器中的Session對象,當有 10萬用戶并發訪問,就有可能出現10萬個Session對象,內存可能吃不消,于是Web容器就會把一些seesion先序列化到硬盤中,等要用了,再把保存在硬盤中的對象還原到內存中。
當兩個進程在進行遠程通信時,彼此可以發送各種類型的數據。無論是何種類型的數據,都會以二進制序列的形式在網絡上傳送。發送方需要把這個Java對象轉換為字節序列,才能在網絡上傳送;接收方則需要把字節序列再恢復為Java對象。
在Java/Storm中,可以理解為toString()
函數的自定義實現。注意使用transient
修飾的對象無法序列化。
9. declareOutputFields()
函數的具體作用
該Spout代碼里面最核心的部分有兩個:
- 用
collector.emit()
方法發射tuple
。我們不用自己實現tuple
,我們只需要定義tuple
的value
,Storm會幫我們生成tuple
。Values
對象接受變長參數。Tuple中以List
存放Values
,List
的Index
按照new Values(obj1, obj2,…)
的參數的index
,例如我們emit(new Values("v1", "v2"))
, 那么Tuple的屬性即為:{ [ "v1" ], [ "V2" ] }
-
declarer.declare
方法用來給我們發射的value
在整個Stream中定義一個別名。可以理解為key
。該值必須在整個topology
定義中唯一。
3.5 Windows 平臺部署本地測試環境的注意事項
3.5.1 所需安裝包
- Java SE Development Kit 8/9,安裝到非
C:\Program Files\
目錄下,否則storm將無法啟動。 - Apache-maven,解壓到本地目錄,推薦非系統盤
- Zookeeper,解壓到本地目錄,推薦非系統盤
- Storm,解壓到本地目錄,推薦非系統盤
3.5.2 環境變量配置
-
JAVA_HOME
:path\to\your\jdk-file
-
Path
:path\to\your-storm\bin;path\to\your-zookeeper\bin;path\to\your-maven\bin;%JAVA_HOME%\bin
3.5.3 配置文件設定
- 配置
zoo.cfg
,參照3.1 - 配置
storm.yaml
,參照3.1,注意storm.local.dirs
中的目錄使用\\
來表示\
。
3.5.6 啟動集群
zkServer
storm nimbus
storm supervisor
storm ui
# 打開瀏覽器 http://127.0.0.1:8080/index.html