一、概念
單進程版與Storm版的流計算實現有許多相似的概念,其中最重要的包括:Topology、Spout、Bolt。Topology是一個由Spout節點和Bolt節點組成的有向無環圖(或稱有方向的樹),一個輕應用可以有一個或多個Topology。Spout是數據的來源,它是Topology的根節點,每個Topology只有一個Spout。Bolt是真正負責處理業務邏輯的節點。
Storm將父節點傳輸給子節點的數據稱為Tuple,單進程版的流計算與此對應的概念是BoltParameter。一組Tuple或BoltParameter組成Stream,Stream是一個抽象概念,沒有具體的實現。
單進程版相當于Storm版的簡化,有一些Storm版的概念這里不會有,比如:
- 分組(grouping)
- Worker
- Task
- Reliability
上面列出的概念基本都與Storm的并行處理有關,單進程版的流計算不存在并發問題,也就沒有這些概念。
在編寫Storm版的流計算程序時,很多非業務邏輯的功能是Storm負責維護和處理的,而單進程版的程序需要自己來處理,這引入了一些新的概念:
- SpoutProcessor,這個類負責維護Spout節點
- BoltProcessor,這個類負責維護Bolt節點
下文將詳細解釋SpoutProcessor和BoltProcessor。
二、Topology的實現
之前提到Topology樹是由Spout和Bolt組成,實際上Spout和Bolt里邊是業務邏輯相關的定義,真正讓它們組成一棵樹的是SpoutProcessor和BoltProcessor,這兩個類都實現了ProcessNode接口,該接口定義如下:
public interface ProcessNode {
/**
* 提取本處理結點的名字
*/
String getName();
/**
* 提取本結點的Id,Id用來記錄節點間的關系,全局唯一
*/
String getId();
/**
* 向本處理結點增加一個兒子結點
*/
void addchild(ProcessNode child);
/**
* 調用本處理結點的處理邏輯對數據進行處理
*/
void run(Object param);
}
SpoutProcesser的定義:
public class SpoutProcessor implements ProcessNode {
private static Logger logger = LoggerFactory.getLogger(SpoutProcessor.class);
/**
* 本拓撲待處理的數據隊列
*/
KafkaDataQueue queue = new KafkaDataQueue();
/**
* 數據生成器
*/
private StreamSpout spout;
/**
* 本處理器對應的下一代處理器
*/
private LinkedList<ProcessNode> childrens;
//后面的代碼省略
SpoutProcesser的第一個變量是KafkaDataQueue,這個數據隊列由KafkaThread類負責寫入數據(它的數據來源是Kafka隊列),由ProcessThread負責讀取數據并處理。關于ProcessThread等線程類后邊詳細介紹。
SpoutProcesser的第二個變量是StreamSpout,這就是Storm中Spout的對應實現,是拓撲樹的根節點。
SpoutProcesser第三個變量:private LinkedList<ProcessNode> childrens;
,這個變量記錄了它的下一級節點有哪些,這是組成Topology樹的關鍵。
public class BoltProcessor implements ProcessNode {
/**
* 本處理器對應的Bolt
*/
private Bolt bolt;
/**
* 本處理器對應的下一代處理器
*/
private LinkedList<ProcessNode> childrens;
//后面的代碼省略
BoltProcessor也是一樣,維護了Bolt的下一級節點列表。
三、線程
單進程版流計算有四個線程:
- KafkaThread,如前所述,它負責讀取Kafka隊列,并把數據放到SpoutProcesser的KafkaDataQueue。每個SpoutProcesser都會收到一份完全相同的數據的拷貝,有點類似于Storm的AllGrouping分組方式。全局只有一個KafkaThread。
- ProcessThread,它通過調用SpoutProcesser的實例來處理KafkaDataQueue中的數據,每個Topology對應一個ProcessThread。
- OutputThread,負責把流計算的結果存儲到MongoDB,全局只有一個OutputThread。
- ShutdownHookThread,當進程退出時,如kill -15 程序退出時,把未處理的kafka數據退回kafka隊列,把已經處理生成的結果存入mongo,減少數據丟失。ShutdownListener類實現了ApplicationListener接口,當監聽到ContextClosedEvent事件時啟動ShutdownHookThread。
四、流計算業務邏輯的實現
關于Groovy腳本是如何在Java程序中運行的,可以參考《Groovy腳本使用方法》、《baas系統腳本說明》。這篇文檔主要介紹StreamSpout、ConvertBolt和StatBolt的實現(AlarmBolt與ConvertBolt類似,不再重復介紹)。
(一)StreamSpout
StreamSpout是根節點的實現,它實現了Spout接口:
public interface Spout {
/**
* 得到本Spout的名字
*/
String getName();
/**
* 得到本Spout的Id
*/
String getId();
/**
* 準備本Bolt
*/
void prepare();
/**
* 提取設備檔案操作對象
*/
IArchives getArchives();
/**
* 設置設備檔案操作對象
*/
void setArchives(IArchives archives);
/**
* 執行Spout內部的判斷邏輯,判別是否應該交由本Spout進行處理
*/
BoltParameter execute(SpoutParameter spoutParameter);
}
prepare方法只在初始化的時候執行一次,它負責做一些準備工作。
execute方法每收到一個數據就會運行一次,處理真正的業務邏輯,數據通過參數BoltParameter傳遞進來,通過返回BoltParameter傳遞給下一級節點。
getArchives方法返回一個IArchives接口,通過這個接口提供的方法可以獲取設備檔案。
StreamSpout的定義(核心片段,非完整代碼):
public class StreamSpout implements Spout {
/**
* 本Spout的配置
*/
private SpoutConfig config;
/**
* 訪問設備檔案的對象
*/
private Archives archives;
@Override
public void prepare() {
}
@Override
public BoltParameter execute(SpoutParameter spoutParameter) {
//省略具體實現
}
//省略后面的代碼
第一個成員變量是SpoutConfig,這就是用戶在輕應用平臺上所做的配置,由單進程流計算的入口類Executor從數據庫中讀取填充。
第二個成員變量Archives,這個對象包含一個deviceId成員變量,當StreamSpout收到數據時,execute方法會給deviceId賦值,在后面的節點中將用來獲取檔案信息。
StreamSpout的prepare方法目前為空,沒有任何準備工作要做。
execute方法首先判斷數據流名稱是否和用戶配置的一致,然后構造BoltParameter對象(由流計算的上下文、內置輸入對象、檔案操作對象組成),返回該對象給下一級節點。
(二)ConvertBolt
ConvertBolt的定義(核心片段,非完整代碼):
public class ConvertBolt extends BaseBolt {
/**
* 本Bolt的配置
*/
private ConvertBoltConfig config;
/**
* 腳本對象
*/
private IConvertProcess process;
/**
* 把數據保存到Mongo的隊列
*/
private OutputQueue outputQueue;
//省略非業務邏輯私有變量
@Override
public void prepare() {
//省略具體實現
}
@Override
public List<BoltParameter> execute(BoltParameter parameter) {
//省略具體實現
}
}
ConvertBolt繼承了BaseBolt,后者非常簡單,不影響整體理解,細節請閱讀源代碼。
第一個成員變量是ConvertBoltConfig,和SpoutConfig一樣,這是用戶在輕應用平臺上所做的配置,由單進程流計算的入口類Executor從數據庫中讀取填充。
第二個成員變量IConvertProcess,用來引用Groovy腳本的實例,執行Groovy腳本的時候用到。
第三個成員變量OutputQueue,OutputThread會將這個隊列的數據存儲到MongoDB。
prepare方法只在初始化的時候執行一次,它負責做一些準備工作,例如解析ConvertBoltConfig并加載Groovy腳本。
execute方法每收到一個數據就會運行一次,處理真正的業務邏輯,數據通過參數BoltParameter傳遞進來,通過返回List<BoltParameter>傳遞給下一級節點。
(三)StatBolt
StatBolt的定義(核心片段,非完整代碼):
public class StatBolt extends BaseBolt {
/**
* 統計單元的配置
*/
private StaticsBoltConfig config;
/**
* 統計腳本對象
*/
private IStatProcess statProcessor;
/**
* 統計緩存對象
*/
private Caches caches;
/**
* 統計過程中訪問Redis的對象,用于保存和提取中間結果
*/
private RedisClient redisClient;
/**
* 把數據存儲到Mongo中的隊列
*/
private OutputQueue outputQueue;
@Override
public void prepare() {
//省略具體實現
}
@Override
public List<BoltParameter> execute(BoltParameter parameter) {
//省略具體實現
}
}
StatBolt和ConvertBolt結構基本一致,相同的部分不再重復說明。
成員變量Caches是內置的統計計算所需的緩存類,在prepare方法中初始化,它實現了ICaches接口,包括單個設備統計用到的group函數和全局統計用到的group(Object group)。雖然名為Caches,但它更多的是作為計算所需的內置對象,實際的緩存功能是由它內部的RedisClient類實現。
execute方法判斷是否到達輸出時間(上一次輸出時間可通過Caches獲取),如果到達則執行輸出腳本,如果沒有到達則執行計算腳本。
五、緩存的實現
采用了兩級緩存機制:Redis和Ehcache,前者是遠程緩存,后者是本地緩存。事實上正是由于遠程緩存性能不夠好才引入了本地緩存,但另一方面,如果只使用本地緩存,程序意外終止時會丟失數據,所以兩者結合使用。
CacheUtils類提供了Ehcache的訪問,RedisClient類除了提供了Redis的訪問,還包含CacheUtils的調用并提供其他類所需的接口方法。
緩存的細節信息見下表:
緩存類型 | 最大數量級 | key值構成 | Ehcache緩存名字 | 備注 |
---|---|---|---|---|
設備檔案 | 設備數量 | arch-、archiveId、deviceId | archiveCache | |
單個統計中間結果 | 設備數量*統計節點數量 | cache-、statId、deviceId | midStatCache | 單個統計時緩存數量遠大于全局統計 |
單個統計的上一次輸出時間 | 設備數量*統計節點數量 | stat_last_time-、statId、deviceId | lastStatOutputCache | 單個統計時緩存數量遠大于全局統計 |
全局統計中間結果 | 檔案字段數量*統計節點數量 | cache-、statId、group | midStatCache | group是檔案字段的值 |
全局統計的上一次輸出時間 | 檔案字段數量*統計節點數量 | stat_last_time-、statId、group | lastStatOutputCache | group是檔案字段的值 |
上一次告警輸出的時間(數據來源為非統計節點) | 設備數量*告警節點數量 | alarm_last-、alarmId、deviceId | lastAlarmCache | |
上一次告警輸出的時間(數據來源為單個統計節點) | 設備數量*告警節點數量 | alarm_last-、alarmId、deviceId | lastAlarmCache | key值看起來和上一行相同,但alarmId實際會不一樣。 |
上一次告警輸出的時間(數據來源為全局統計節點) | 檔案字段數量*告警節點數量 | alarm_last-、alarmId、group | lastAlarmCache | group是檔案字段的值 |
全局統計的維度值列表 | 檔案字段數量*統計節點數量 | statId | statDimensionsCache | value是Set<String>,Set里邊放的group;單線程版本的流計算是存儲在Redis |
六、程序的入口:Executor類
Executor類的init方法是單進程版流計算程序的入口,init方法主要做了兩件事:創建Topology樹和啟動各個線程,當init方法執行完畢之后,單進程流計算程序就開始讀取Kafka隊列中的數據并處理,這個過程將會一直運行下去,只能用戶手動停止。