單進程版流計算實現說明

一、概念

單進程版與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隊列中的數據并處理,這個過程將會一直運行下去,只能用戶手動停止。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Date: Nov 17-24, 2017 1. 目的 積累Storm為主的流式大數據處理平臺對實時數據處理的相關...
    一只很努力爬樹的貓閱讀 2,196評論 0 4
  • 這是一個JStorm使用教程,不包含環境搭建教程,直接在公司現有集群上跑任務,關于JStorm集群環境搭建,后續研...
    Coselding閱讀 6,426評論 1 9
  • 原文鏈接Storm Tutorial 本人原創翻譯,轉載請注明出處 這個教程內容包含如何創建topologies及...
    quiterr閱讀 1,641評論 0 6
  • 目錄 場景假設 調優步驟和方法 Storm 的部分特性 Storm 并行度 Storm 消息機制 Storm UI...
    mtide閱讀 17,185評論 30 60
  • 忽然就想煲點雞湯了,聽著外面悉悉索索的雨聲卻完全沒有一點睡意,什么是生活?生活又是什么?我又在過怎樣的生活?這些我...
    賀新涼baby閱讀 164評論 0 0