Apache Beam實戰指南 | 如何結合ClickHouse打造“AI微服務”?

本文是Apache Beam 實戰指南系列文章的第四篇內容,將對 Beam 框架中的 ClickHouseIO 源碼進行剖析,並結合應用示例和代碼解讀帶你進一步瞭解如何結合 Beam 玩轉大數據實時分析數據庫ClickHouse。系列文章第一篇回顧Apache Beam 實戰指南 | 基礎入門、第二篇回顧Apache Beam 實戰指南 | 玩轉 KafkaIO 與 Flink、第三篇回顧Apache Beam實戰指南 | 玩轉大數據存儲HdfsIO

關於Apache Beam實戰指南系列文章

隨着大數據 2.0 時代悄然到來,大數據從簡單的批處理擴展到了實時處理、流處理、交互式查詢和機器學習應用。近年來涌現出諸多大數據應用組件,如 HBase、Hive、Kafka、Spark、Flink 等。開發者經常要用到不同的技術、框架、API、開發語言和 SDK 來應對複雜應用的開發,這大大增加了選擇合適工具和框架的難度,開發者想要將所有的大數據組件熟練運用幾乎是一項不可能完成的任務。

面對這種情況,Google 在 2016 年 2 月宣佈將大數據流水線產品(Google DataFlow)貢獻給 Apache 基金會孵化,2017 年 1 月 Apache 對外宣佈開源 Apache Beam,2017 年 5 月迎來了它的第一個穩定版本 2.0.0。在國內,大部分開發者對於 Beam 還缺乏瞭解,社區中文資料也比較少。InfoQ 期望通過 Apache Beam 實戰指南系列文章 推動 Apache Beam 在國內的普及。

一.概述

loT大時代背景趨勢下,萬物互聯。AI技術逐漸普及,以及延伸到各個行業中。圖片識別,人臉識別等等應用演化出無數的智能應用。大數據也慢慢的從普通大數據演變也向着人工智能的“深數據”轉變 ,傳統的大數據架構正在面臨着前所未有的挑戰。物聯網與互聯網的邊界變得越來越模糊。

在物聯網通過構建集中化、主動化、智能化的視頻運維管理中,對數量龐大、種類繁多的前端攝像機、編解碼設備、門禁設備、對講設備集報警設備等各類安防設備。怎樣實現設備運行狀態實時監測、視頻質量情況智能診斷、設備故障事件第一時間主動告知,並能夠及時、準確分析和定位故障根源,實現運維管理效率和服務管理質量的同步 在以上場景中數據量大,實時快速分析 Apache Beam 起到了怎樣的作用呢?

Apache Beam 在不同的數據源,數據種類進行數據彙集,以流數據方式實時的上報到全國中心。同時進行ETL清洗,把數據實時寫入ClickHouse 或Elasticsearch ,面對每天全國PB及以上的大數據架構是怎麼設計呢?通過一個案例讓我們進行了解一下Beam 是怎樣結合ClickHouse發揮優勢的。

二.案例整體的架構流程圖

2.1 案例架構流程圖

image

  1. 攝像頭以及AI智能設備產生的報警以及抓取的信息上報到後端智能設備。

  2. 智能設備產生的AI分析結果進行通過網關集羣進行傳輸,注意網關集羣地方要做流控及雪崩控制。

  3. 消息通過網關集羣發送到消息中間件。注意:這邊這個規則下發是針對前段的數據進行ETL清洗的清洗規則的下發。

  4. Beam 集羣接收下發規則的更新,並且根據規則進行數據清洗。

  5. 對於文檔性的數據我們實時存儲到實時搜索引擎。

  6. 需要複雜查詢,統計以及報表的數據存儲到ClickHouse

  7. 進行BI套件的展示以及前端大屏幕的展示。

三.技術名稱解釋

Kafka

是一種高吞吐量的分佈式發佈訂閱消息系統。針對流數據支持性比較高,是現在消息中間件應用非常廣泛的開源的消息中間件。

ClickHouse

是一個開源的面向列的數據庫管理系統,能夠使用SQL實時查詢並生成報表或報告。詳細可參考我的文章《比Hive快800倍!大數據實時分析領域黑馬開源ClickHouse》,此外在ClickHouse 18.1.0 以後版本的MergeTree 引擎中已經支持 修改和刪除功能以及標準SQL Join 。

ElasticSearch

ElasticSearch是一個基於Lucene的實時搜索服務器。現在應用雲計算,大數據,LoT等方面比較廣泛。本文中運用它來做數據備份。

四.Apache Beam ClickHouseIO源碼剖析

Apache Beam ClickHouseIO 對ClickHouse支持依賴情況

ClickHouseIO是ClickHouse的API封裝,主要負責ClickHouse讀取和寫入消息。如果想使用ClickHouseIO,必須依賴beam-sdks-java-io-clickhouse ,ClickHouseIO 同時支持多個版本的ClickHouse,使用時現在只有V2.11.0版本在maven 中心倉庫已經釋放,其他的版本沒有釋放。需要下載源碼自己進行編譯。

Apache Beam ClickHouseIO 對各個clickhouse-jdbc 版本的支持情況如下表:

image

表4-1 ClickHouseIO 與clickhouse-jdbc 依賴關係表

ClickHouse數據類型 與Apache Beam 數據類型轉換情況

Apache Beam 在本次案例中選擇的是最新的版本V2.11.0 ,因爲其他版本的clickhouse-jdbc 沒有釋放。因爲ClickHouse 更確切的是一個關係型數據庫,但是它的數據格式跟Beam 底層轉換的時候還是存在着部分的不同點,我們通過一張表看一下ClickHouse的數據格式和Apache Beam 的數據格式有哪些不一樣?

image

表4-2 ClickHouse數據類型 與Apache Beam 數據類型轉換對照表

對於ClickHouse的中是怎樣把數據 轉換成Apache Beam的數據的呢?其實它的轉換是用ClickHouseWriter.java 這個文件中的writeValue() 的switch 語句

 switch (columnType.typeName()) {
      case FLOAT32:
        stream.writeFloat32((Float) value);
        break;

      case FLOAT64:
        stream.writeFloat64((Double) value);
        break;
     .....

ClickHouseIO源碼解析

ClickHouseIO源碼鏈接如下:

https://github.com/apache/beam/blob/v2.11.0/sdks/java/io/clickhouse/src/main/java/org/apache/beam/sdk/io/clickhouse/ClickHouseIO.java

在ClickHouseIO裏面最主要的方法是ClickHouse的寫方法,以及幾個重要的API的的屬性參數。

ClickHouseIO寫操作

 public static <T> Write<T> write(String jdbcUrl, String table) 
 {...
  1. 源碼中的寫入類型 Beam 中是給定的是泛型類型,是可以制定自己自定義以及現有的數據類型。當然我們開發中一般很多時候以json 類型和類對象爲主,當然也有其他類型如KV類型等。 寫入方法中傳了兩個比較簡單的參數,String jdbcUrl 相當於咱們常用的MySQL連接地址一樣的字符串,在Beam 和ClickHouse的連接也選擇了相同的方式。String table 這裏是要進行操作的表名稱,如果寫入數據需要先檢查是否存在相應的表。如果是單機直接寫單機主機IP和端口就可以,集羣則填寫 Master Node 節點的地址,具體示例如下:
ClickHouseIO.<Row>write("jdbc:clickhouse://101.201.*.*:8123/Alarm", "AlarmTable")
  1. 設置ClickHouse最大添加的塊大小,注意這個塊相當於MySQL中的 Batch 條數,並不是存儲的塊大小。 在Beam ClickHouseIO和ClickHouse官方的兩個默認值分別爲1000000和1048576。如果是數據量特別大以及大數據遷移導入的時候設置100W行數據插入,速度約在2-5秒中,速度是非常快的。實時的場景可以採用Beam 窗口方式,1-2分鐘批量添加一批。
public Write<T> withMaxInsertBlockSize(long value) {
      return toBuilder().maxInsertBlockSize(value).build();
  }
  1. 設置每次寫入ClickHouse的最大重試次數,Beam 默認爲5次。
 public Write<T> withMaxRetries(int value) {
      return toBuilder().maxRetries(value).build();
 }
  1. 設置是否啓用分佈式節點的分片同步複製數據,如果是正式生產環境建議開啓。很多場景都要保證數據的完整性。
withInsertDistributedSync(@Nullable Boolean value) {..

設置數據複製的副本數量,服務器默認爲禁用此設置,需要服務器配置,0表示禁用,null表示服務器默認值。

 withInsertQuorum(@Nullable  Long  value){..
  1. 設置插入塊數據中有重複數據,進行刪除重複數據。默認爲啓用,null表示服務器默認值。
 withInsertDeduplicate(Boolean  value){..
  1. 設置操作失敗後的退出初始時間和總時間。
  withMaxCumulativeBackoff(Duration  value)
  withInitialBackoff(Duration  value){

關於性能的注意事項

(1)數據壓縮

對於loT場景下3-5年的警情數據需要進行冷數據壓縮,而節省空間開銷。ClickHouse的數據可以採用數據壓縮的方式進行壓縮。

(2)物化視圖

ClickHouse 針對多維大數據查詢,支持物化視圖的建立。

五. Apache Beam 和ClickHouse實戰

本節通過解讀一個真正的ClickHouseIO和Apache Beam實戰案例,幫助大家更深入地瞭解ClickHouse和Apache Beam的運用。

設計架構圖和設計思路解讀

image

Apache Beam 外部數據流程圖

設計思路:設備事件,報警等消息通過Netty集羣 把消息發送到Kafka集羣 Apache Beam 程序通過 KafkaIO 接收前端業務消息 並且寫入ClickHouse 。

image

Apache Beam 內部數據處理流程圖

Apache Beam 程序通過kafkaIO讀取Kafka集羣的數據,進行數據格式轉換。通過ClickHouseIO寫操作把消息寫入ClickHouse。最後把程序運行在Flink的計算平臺上。

軟件環境和版本說明

  • 系統版本 centos 7

  • Kafka集羣版本: kafka_2.11-2.0.0.tgz

  • Flink 版本:flink-1.5.2-bin-hadoop27-scala_2.11.tgz

  • ClickHouse 19.3.5

ClickHouse 集羣或單機以及Docker 可以在開源中文社區獲取,大家可以去網上搜一下配置文章,操作比較簡單,這裏就不贅述了。

實踐步驟

1)在pom文件中添加jar引用

      <!-- 本地運行Runners -->
<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-runners-direct-java</artifactId>
<version>2.11.0</version>
</dependency>
<!-- 運行Runners核心jar -->
<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-runners-core-java</artifactId>
<version>2.11.0</version>
</dependency>
<!-- 引入Beam clickhouse jar -->
<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-sdks-java-io-clickhouse</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>ru.yandex.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.1.47</version>
</dependency>
<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-sdks-java-core</artifactId>
<version>2.11.0</version>
</dependency>
   <!-- BeamSQL -->
<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-sdks-java-extensions-sql</artifactId>
<version>2.11.0</version>
</dependency>
       <!--引入elasticsearch-->
<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-sdks-java-io-elasticsearch</artifactId>
<version>2.11.0</version>
</dependency>
<!-- kafka -->
<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-sdks-java-io-kafka</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.0.0</version>
</dependency>
<!-- Flink -->
<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-runners-flink_2.11</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_2.11</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-core</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-runtime_2.11</artifactId>
<version>1.5.2</version>
<!--<scope>provided</scope> -->
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_2.11</artifactId>
<version>1.5.2</version>
<!--<scope>provided</scope> -->
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-metrics-core</artifactId>
<version>1.5.2</version>
<!--<scope>provided</scope> -->
</dependency>

2)新建kafkaToClickhouseIO.java類

image

3)KafkaToClickhouseIO編寫以下代碼:

public static void main(String[] args) {

// 創建管道工廠
PipelineOptions options = PipelineOptionsFactory.create();
// 顯式指定PipelineRunner:FlinkRunner必須指定如果不制定則爲本地
options.setRunner(FlinkRunner.class);
Pipeline pipeline = Pipeline.create(options);// 設置相關管道
// 這裏kV後說明kafka中的key和value均爲String類型
PCollection<KafkaRecord<String, String>> lines = 
 pipeline.apply(KafkaIO.<String,String>read().withBootstrapServers("101.*.*.77:9092")// 必需設置kafka的服務器地址和端口
.withTopic("TopicAlarm")// 必需,設置要讀取的kafka的topic名稱 .withKeyDeserializer(StringDeserializer.class)// 必需序列化key .withValueDeserializer(StringDeserializer.class)// 必需序列化value
.updateConsumerProperties(ImmutableMap.<String, Object>of("auto.offset.reset", "earliest")));// 這個屬性kafka最常見的
//設置Schema 的的字段名稱和類型
final Schema type = Schema.of(
Schema.Field.of("alarmid", FieldType.STRING), Schema.Field.of("alarmTitle", FieldType.STRING),
Schema.Field.of("deviceModel", FieldType.STRING), Schema.Field.of("alarmSource", FieldType.INT32), Schema.Field.of("alarmMsg", FieldType.STRING));
        //從kafka中讀出的數據轉換成AlarmTable實體對象
PCollection<AlarmTable> kafkadata = lines.apply("Remove Kafka Metadata", ParDo.of(new DoFn<KafkaRecord<String, String>, AlarmTable>() {
private static final long serialVersionUID = 1L;
@ProcessElement
public void processElement(ProcessContext ctx) {
Gson gon = new Gson();
AlarmTable modelTable = null;
try {//進行序列號代碼
modelTable = gon.fromJson(ctx.element().getKV().getValue(),AlarmTable.class);
} catch (Exception e) {
System.out.print("json序列化出現問題:" + e);
}
ctx.output(modelTable);//回傳實體
}
}));
//備份寫入Elasticsearch
String[] addresses = { "http://101.*.*.77:9200/" };
PCollection<String> jsonCollection=kafkadata
.setCoder(AvroCoder.of(AlarmTable.class))
.apply("covert json", ParDo.of(new DoFn<AlarmTable, String>() {
private static final long serialVersionUID = 1L;
@ProcessElement
public void processElement(ProcessContext ctx) {
Gson gon = new Gson();
String	jString="";
try {// 進行序列號代碼
jString = gon.toJson(ctx.element());
System.out.print("序列化後的數據:" + jString);
} catch (Exception e) {
System.out.print("json序列化出現問題:" + e);
}
ctx.output(jString);// 回傳實體
}
}));
// 所有的Beam 數據寫入ES的數據統一轉換成json 纔可以正常插入
jsonCollection.apply( ElasticsearchIO.write().withConnectionConfiguration(ElasticsearchIO.ConnectionConfiguration.create(addresses, "alarm", "TopicAlarm")
));

PCollection<Row> modelPCollection = kafkadata
//.setCoder(AvroCoder.of(AlarmTable.class))//如果上面設置下面就不用設置
.apply(ParDo.of(new DoFn<AlarmTable, Row>() {//實體轉換成Row
private static final long serialVersionUID = 1L;
@ProcessElement
   public void processElement(ProcessContext c) {
AlarmTable modelTable = c.element();
System.out.print(modelTable.getAlarmMsg());
Row alarmRow = Row.withSchema(type)
    .addValues(modelTable.getAlarmid(),modelTable.getAlarmTitle(), modelTable.getDeviceModel(), modelTable.getAlarmSource(), modelTable.getAlarmMsg()).build();//實體賦值Row類型
c.output(alarmRow);
}
}));
      //寫入ClickHouse
modelPCollection.setRowSchema(type).apply(
ClickHouseIO.<Row>write("jdbc:clickhouse://101.201.56.77:8123/Alarm", "AlarmTable").withMaxRetries(3)// 重試次數
.withMaxInsertBlockSize(5) // 添加最大塊的大小
.withInitialBackoff(Duration.standardSeconds(5))
.withInsertDeduplicate(true) // 重複數據是否刪除
.withInsertDistributedSync(false));
pipeline.run().waitUntilFinish();
}

AlarmTable.java 爲從數據庫映射出來的實體對象類,注意此處爲沒有任何業務邏輯的實體對象。

image

4)打包jar,本示例是簡單的實戰,可以採用Docker 虛擬化自動部署。

image

image

5)通過Apache Flink Dashboard 提交job,也可以用後臺用命令提交。

image

6)查看結果,視圖中顯示着運行着一直等待接收kafka隊列的消息。如果有消息會自動插入Clickhouse.

image

看一下Clickhouse 數據庫:

image

最後就可以進行各種報表統計,數據計算等操作。

image

寫入Elasticsearch 結果

image

六.實戰解析

本次實戰在源碼分析中已經做過詳細解析,在這裏不做過多的描述,只選擇部分問題再重點解釋一下。此外,如果還沒有入門,甚至連管道和Runner等概念都還不清楚,建議先閱讀本系列的第一篇文章《Apache Beam實戰指南之基礎入門》

1.在ClickHouseIO 有個很關鍵的關鍵字Schema,Row 這幾個關鍵字在各個版本有一定的API的變化。希望實戰者要注意,如下表。

image

通過上個表格可以一目瞭然的看到在BeamAPI演進過程中的Row 和Schema變化。在Beam2.5+以後版本都是基本沒有太大變動,只是做API的優化以及實戰過程中的優化。如果是2.4版本則是:

image

總體來說2.4版本其實還是很穩定的。2.5版本是一共過渡版本,往後的改動不是API的大變化改動。

  1. Coder是Beam SDK 中非常重要的一個協議轉換的角色。如果不設置會出現 “No Coder has been manually specified; you may do so using .setCoder()” 的錯誤提示。其實在Flink 的數據轉換中也是存在的,例如文中的 我是設置了對象AvroCoder,大家都知道Avro是一種序列化和反序列化的格式。
.setCoder(AvroCoder.of(AlarmTable.class))

在項目實戰中Beam的序列化是可以自定義的,但是都必須重寫encode和decode,用過Netty的都知道在接收和回傳消息都需要編碼器和解碼器,在Beam中 多了一共驗證的方法verifyDeterministic()驗證類型正確性。個人是不建議自定義編碼的,因爲在Beam coders 中已經提供了45種的編碼類型,基本覆蓋了java的所有的類型編碼。再有就是因爲自己寫的自定義編碼還需要大量的穩定性測試以及性能測試。

  1. ElasticsearchIO 的寫操作,在Beam 中從kafka流出的數據 是同時可以寫入Elasticsearch和Clickhouse的。我們可以理解成一根水管,我們對接了兩根子水管,一根是A水管 Elasticsearch和B水管Clickhouse。A和B的數據是相同的如圖 A和B的數據對比。

image

  1. Elasticsearch 寫入的格式要求。

因爲Elasticsearch 是一個文檔性質數據庫所以在寫入的時候之前所有的數據都要轉換成相應的Json 格式的數據。

  1. Elasticsearch生產環境一般都是要做高可用集羣,在Beam 提供了 Elasticsearch 集羣的配置數組寫法如下:
String[] addresses = { "http://101.*.*.77:9200/" };
  1. Elasticsearch 的索引和分片設置

Elasticsearch 索引相當於MySQL的數據庫名,分片相當於MySQL的表名。 在Beam 設置比較簡單,設置成功後,也不需要單獨手工創建分片名和字段屬性。執行Beam程序後自動創建分片及字段並寫入數據。文中我設置的索引和分片爲alarm和TopicAlarm。

  1. 實戰延伸,應對萬變需求,規則調整。

在實際項目中,例如警情的去重,區域事件的實時統計,在loT場景中AI人臉識別的人像特徵值獲取上報等等 都有自己的規則, 雖然規則不是很頻繁,但是做爲整體架構設計要做到靈活易用。這個時候就會結合我們的Beam SQL 進行規則,可以通過Spring boot 或Spring Cloud結合 一些配置管理系統等做一些規則下發,當然通過中間件也可以進行實現。
8. 支持ClickHouse的可視化界面工具有哪些?

image

七.小結

在loT場景下,Apache Beam作爲大一統的技術框架,隨着人工智能飛速發展,賦能於前後端設備,並對分析後的結果數據做實時性的處理,起到了"閃送"數據、清洗數據的功能;而ClickHouse則作爲後端的數據實時分析系統飛速提供結果 , Apache Beam +ClickHouse組成了實時清洗分析一條龍架構。此外,Apache Beam 和ClickHouse 完美結合構建了屬於自己的"AI微服務" ,用於對這些深數據快速加工並支撐不同場景的應用落地。

作者介紹

張海濤,目前就職於海康威視雲基礎平臺,負責雲計算大數據的基礎架構設計和中間件的開發,專注雲計算大數據方向。Apache Beam 中文社區發起人之一,如果想進一步瞭解最新 Apache Beam 和ClickHouse動態和技術研究成果,請加微信 cyrjkj 入羣共同研究和運用。

傳送門:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章