Beam-介紹

簡介

Beam提供了一套統一的API來處理兩種數據處理模式(批和流),讓我們只需要將注意力專注於在數據處理的算法上,而不用再花時間去對兩種數據處理模式上的差異進行維護。

Beam每6周更新一個小版本。

編程模型

  • 第一層是現有各大數據處理平臺(spark或者flink),在Beam中它們也被稱爲Runner。
  • 第二層,是可移植的統一模型層,各個Runners將會依據中間抽象出來的這個模型思想,提供一套符合這個模型APLs出來,以供上層轉換。
  • 第三層,是SDK層。SDK層將會給工程師提供不同語言版本的API來編寫數據處理邏輯,這些邏輯就會被轉化Runner中相應API來運行。
  • 第四層,是可擴展庫層。工程師可以根據已有的BeamSDK,貢獻分享出更多的新開發者SDK,IO連接器,轉換操作庫等等。
  • 第五層,我們可以看作是應用層,各種應用將會通過下層的BeamSDK或工程師貢獻的開發者SDK來實現。
  • 第六層,社區。

窗口將無邊界數據根據事件時間分成一個個有限數據集。我們可以看看批處理這個特例。在批處理中,我們其實是把一個無窮小到無窮大的時間窗口賦予了數據集。

水印是用來表示與數據事件時間相關聯的輸入完整性的概念。對於事件時間X的水印是指:數據處理邏輯已經得到了所有時間小於X的無邊界數據。在數據處理中,水印是用來測量數據進度的。

觸發器指的是表示在具體什麼時候,數據處理邏輯會真正地出發窗口中的數據被計算。觸發器能讓我們可以在有需要時對數據進行多次運算,例如某時間窗口內數據有更新,這一窗口內的數據結果需要重算。

累加模式指的是如果我們在同一窗口中得到多個運算結果,我們應該如何處理這些運算結果。這些結果之間可能完全不相關,例如與時間先後無關的結果,直接覆蓋以前的運算結果即可。這些結果也可能會重疊在一起。

數據處理常見設計模式:

  • 複製模式通常是將單個數據處理模塊中的數據,完整地複製到兩個或更多的數據處理模塊中,然後再由不同的數據處理模塊進行處理。
  • 過濾掉不符合特定條件的數據。
  • 如果你在處理數據集時並不想丟棄裏面的任何數據,而是想把數據分類爲不同的類別進行處理時,你就需要用到分離式來處理數據。
  • 合併模式會將多個不同的數據轉換集中在一起,成爲一個總數據集,然後將這個總數據集放在一個工作流中進行處理。

PCollection

可並行計算數據集。

Coders通信編碼。

無序-跟分佈式有關。

沒有固定大小。

不可變性。

Pipeline

Beam數據流水線的底層思想其實還是mr得原理,在分佈式環境下,整個數據流水線啓動N個Workers來同時處理PCollection.而在具體處理某一個特定Transform的時候,數據流水線會將這個Transform的輸入數據集PCollection裏面元素分割成不同Bundle,將這些Bundle分發給不同Worker處理。

Beam數據流水線具體會分配多少個Worker,以及將一個PCollection分割成多少個Bundle都是隨機的。但是Beam數據流水線會儘可能讓整個處理流程達到完美並行。

Beam數據流水線錯誤處理:

  • 在一個Transform裏面,如果某一個Bundle裏面的元素因爲任意原因導致處理失敗了,則這個整個Bundle裏面的元素都必須重新處理。
  • 在多步驟Transform上如果處理的一個Bundle元素髮生錯誤了,則這個元素所在的整個Bundle以及這個Bundle有關聯所有Bundle都必須重新處理。

兩個Transforms,第一個Transform會將元素的數值減一,第二Transform會對元素的數值求平方,整個過程分配兩個workers。

Transform

並行處理數據操作

類似spark的map,parDo支持數據輸出到多個PCollection,而Spark得MapReduce的map可以說是單線的,ParDo提供內建的狀態存儲機制,而Spark和mr沒有。

ParDo

使用ParDo時,你需要繼承它提供DoFn(DoFn分佈式處理功能類)類:

// The input PCollection of Strings.
PCollection<String> words = ...;
// The DoFn to perform on each element in the input PCollection.
static class ComputeWordLengthFn extends DoFn<String, Integer> { ... }
// Apply a ParDo to the PCollection "words" to compute lengths for each word.
PCollection<Integer> wordLengths = words.apply(
ParDo
.of(new ComputeWordLengthFn())); // The DoFn to perform on each element, which
// we define above.

過濾一個數據集:

@ProcessElement
public void processElement(@Element T input, OutputReceiver<T> out) {
if (IsNeeded(input)) {
out.output(input);
}
}

格式換一個數據集:

@ProcessElement
public void processElement(@Element String csvLine, OutputReceiver<tf.Example> out) {
out.output(ConvertToTfExample(csvLine));
}

 提取一個數據集的特定值

@ProcessElement
public void processElement(@Element Item item, OutputReceiver<Integer> out) {
out.output(item.price());
}

GroupByKey 

Key/Value 數據集按Key歸併

Pipeline I/O

讀取數據集
 

PCollection<String> inputs = p.apply(TextIO.read().from(filepath));

PCollection<String> inputs = p.apply(TextIO.read().from("filepath/.../YYYY-MM-*.csv");

PCollection<String> inputs = p.apply(TextIO.read().from("filepath/.../YYYY/MM/*.csv");

//數據集合並

PCollection<String> input1 = p.apply(TextIO.read().from(filepath1);
PCollection<String> input2 = p.apply(TextIO.read().from(filepath2);
PCollection<String> input3 = p.apply(TextIO.read().from(filepath3);
PCollectionList<String> collections = PCollectionList.of(input1).and(input2).and(input3);
PCollection<String> inputs = collections.apply(Flatten.<String>pCollections());

輸出數據集
 

output.apply(TextIO.write().to(filepath));

output.apply(TextIO.write().to(filepath/output));

output.apply(TextIO.write().to(filepath/output).withSuffix(".csv"));

在Beam裏面,Read和Write的Transform都是在名爲I/O連接器類面實現。例如文件讀取FileIO.TFRecordIO,基於流處理KafkaIO,PubsubIO,基於數據可JdbcIO,RedisIO等等。並不可能支持所有外部源(自定義I/O連接器)。

自定義I/O連接器,通常指的就是實現Read Transform和Write Transform 這兩種操作,這兩種操作都有各自實現方法。

自定義讀取操作:

讀取有界數據集

  • 1.兩個 Transform 接口,ParDo 和 GroupByKey 來模擬讀取數據的邏輯。
  • 2.繼承 BoundedSource 抽象類來實現一個子類去實現讀取邏輯。

讀取無界數據集

如果讀取的是無界數據集的話,那我們就必須繼承 UnboundedSource 抽象類來實現一個子類去實現讀取邏輯。

無論是 BoundedSource 抽象類還是 UnboundedSource 抽象類,其實它們都是繼承了 Source 抽象類。爲了能夠在分佈式環境下處理數據,這個 Source 抽象類也必須是可序列化的,也就是說 Source 抽象類必須實現 Serializable 這個接口。、

多文件路徑數據集

從多文件路徑中讀取數據集相當於用戶轉入一個 glob 文件路徑,我們從相應的存儲系統中讀取數據出來。比如說讀取“filepath/**”中的所有文件數據,我們可以將這個讀取轉換成以下的 Transforms:

  • 獲取文件路徑的 ParDo:從用戶傳入的 glob 文件路徑中生成一個 PCollection的中間結果,裏面每個字符串都保存着具體的一個文件路徑。
  • 讀取數據集 ParDo:有了具體 PCollection的文件路徑數據集,從每個路徑中讀取文件內容,生成一個總的 PCollection 保存所有數據。

NoSQL數據庫中讀取數據

NoSQL 這種外部源通常允許按照鍵值範圍(Key Range)來並行讀取數據集。我們可以將這個讀取轉換成以下的 Transforms:

  • 確定鍵值範圍 ParDo:從用戶傳入的要讀取數據的鍵值生成一個 PCollection 保存可以有效並行讀取的鍵值範圍。
  • 讀取數據集 ParDo:從給定 PCollection 的鍵值範圍,讀取相應的數據,並生成一個總的 PCollection 保存所有數據。

關係數據庫讀取數據集

從傳統的關係型數據庫查詢結果通常都是通過一個 SQL Query 來讀取數據的。所以,這個時候只需要一個 ParDo,在 ParDo 裏面建立與數據庫的連接並執行 Query,將返回的結果保存在一個 PCollection 裏。

自定義輸出

相比於讀取操作,輸出操作會簡單很多,只需要在一個 ParDo 裏面調用相應文件系統的寫操作 API 來完成數據集的輸出。

如果我們的輸出數據集是需要寫入到文件去的話,Beam 也同時提供了基於文件操作的 FileBasedSink 抽象類給我們,來實現基於文件類型的輸出操作。像很常見的 TextSink 類就是實現了 FileBasedSink 抽象類,並且運用在了 TextIO 中的。

如果我們要自己寫一個自定義的類來實現 FileBasedSink 的話,也必須實現 Serializable 這個接口,從而保證輸出操作可以在分佈式環境下運行。

同時,自定義的類必須具有不可變性(Immutability)。怎麼理解這個不可變性呢?其實它指的是在這個自定義類裏面,如果有定義私有字段(Private Field)的話,那它必須被聲明爲 final。如果類裏面有變量需要被修改的話,那每次做的修改操作都必須先複製一份完全一樣的數據出來,然後再在這個新的變量上做修改。

設計Beam Pipeline

1.輸入數據存儲位置

2.輸入數據格式

3.數據進行哪些Transform

4.輸出數據格式

Beam的Transform單元測試

一般來說,Transform 的單元測試可以通過以下五步來完成:

  • 1.創建一個 Beam 測試 SDK 中所提供的 TestPipeline 實例。
  • 2.創建一個靜態(Static)的、用於測試的輸入數據集。
  • 3.使用 Create Transform 來創建一個 PCollection 作爲輸入數據集。
  • 4.在測試數據集上調用我們需要測試的 Transform 上並將結果保存在一個 PCollection 上。
  • 5.使用 PAssert 類的相關函數來驗證輸出的 PCollection 是否是我所期望的結果。

Transform單元測試示例

final class TestClass {
static final List<Integer> INPUTS = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

public void testFn() {
Pipeline p = TestPipeline.create();
PCollection<Integer> input = p.apply(Create.of(INPUTS)).setCoder(VarIntCoder.of());
PCollection<String> output = input.apply(ParDo.of(new EvenNumberFn()));
PAssert.that(output).containsInAnyOrder(2, 4, 6, 8, 10);
p.run();
}
}

Beam的端到端的測試

在 Beam 中,端到端的測試和 Transform 的單元測試非常相似。唯一的不同點在於,我們要爲所有的輸入數據集創建測試數據集,而不是隻針對某一個 Transform 來創建。對於在數據流水線的每一個應用到 Write Transfrom 的地方,我們都需要用到 PAssert 類來驗證輸出數據集。

步驟

  • 創建一個 Beam 測試 SDK 中所提供的 TestPipeline 實例。
  • 對於多步驟數據流水線中的每個輸入數據源,創建相對應的靜態(Static)測試數據集。
  • 使用 Create Transform,將所有的這些靜態測試數據集轉換成 PCollection 作爲輸入數據集。
  • 按照真實數據流水線邏輯,調用所有的 Transforms 操作。
  • 在數據流水線中所有應用到 Write Transform 的地方,都使用 PAssert 來替換這個 Write Transform,並且驗證輸出的結果是否我們期望的結果相匹配。
//測試用例
final class TestClass {

static final List<String> INPUTS =
Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8", "9", "10");

static class EvenNumberFn extends DoFn<Integer, Integer> {
@ProcessElement
public void processElement(@Element Integer in, OutputReceiver<Integer> out) {
if (in % 2 == 0) {
out.output(in);
}
}
}

static class ParseIntFn extends DoFn<String, Integer> {
@ProcessElement
public void processElement(@Element String in, OutputReceiver<Integer> out) {
out.output(Integer.parseInt(in));
}
}

public void testFn() {
Pipeline p = TestPipeline.create();
PCollection<String> input = p.apply(Create.of(INPUTS)).setCoder(StringUtf8Coder.of());
PCollection<Integer> output1 = input.apply(ParDo.of(new ParseIntFn())).apply(ParDo.of(new EvenNumberFn()));
PAssert.that(output1).containsInAnyOrder(2, 4, 6, 8, 10);
PCollection<Integer> sum = output1.apply(Combine.globally(new SumInts()));
PAssert.that(sum).is(30);
p.run();
}
}

運行模式

直接運行模式

如果是在命令行中指定 Runner 的話,那麼在調用這個程序時候,需要指定這樣一個參數–runner=DirectRunner。比如:

mvn compile exec:java -Dexec.mainClass=YourMainClass \     -Dexec.args="--runner=DirectRunner" -Pdirect-runner

PipelineOptions options =       PipelineOptionsFactory.fromArgs(args).create();

一般我們會把 runner 通過命令行指令傳遞進程序。就需要使用 PipelineOptionsFactory.fromArgs(args) 來創建 PipelineOptions。PipelineOptionsFactory.fromArgs() 是一個工廠方法,能夠根據命令行參數選擇生成不同的 PipelineOptions 子類。

pom.xml
<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-runners-direct-java</artifactId>
<version>2.9.0</version>
<scope>runtime</scope>
</dependency>

使用 Java Beam SDK 時,我們要給程序添加 Direct Runner 的依賴關係。在下面這個 maven 依賴關係定義文件中,我們指定了 beam-runners-direct-java 這樣一個依賴關係。

我們先從直接運行模式開始講。這是我們在本地進行測試,或者調試時傾向使用的模式。在直接運行模式的時候,Beam 會在單機上用多線程來模擬分佈式的並行處理。

spark運行模式

目前使用 Spark Runner 必須使用 Spark 2.2 版本以上。

Spark Runner 爲在 Apache Spark 上運行 Beam Pipeline 提供了以下功能:

  • Batch 和 streaming 的數據流水線;
  • 和原生 RDD 和 DStream 一樣的容錯保證;
  • 和原生 Spark 同樣的安全性能;
  • 可以用 Spark 的數據回報系統;
  • 使用 Spark Broadcast 實現的 Beam side-input。
<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-runners-spark</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.10</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.10</artifactId>
<version>${spark.version}</version>
</dependency>

使用 SparkPipelineOptions 傳遞進 Pipeline.create() 方法。常見的創建方法是從命令行中讀取參數來創建 PipelineOption,使用的是 PipelineOptionsFactory.fromArgs(String[]) 這個方法。在命令行中,你需要指定 runner=SparkRunner:

mvn exec:java -Dexec.mainClass=YourMainClass \    -Pspark-runner \    -Dexec.args="--runner=SparkRunner \      --sparkMaster=spark master url>"

也可以在 Spark 的獨立集羣上運行,這時候 spark 的提交命令,spark-submit。

spark-submit --class YourMainClass --master spark://HOST:PORT target/...jar --runner=SparkRunner

當 Beam 程序在 Spark 上運行時,你也可以同樣用 Spark 的網頁監控數據流水線進度。

flink運行模式

Flink Runner 是 Beam 提供的用來在 Flink 上運行 Beam Pipeline 的模式。你可以選擇在計算集羣上比如 Yarn/Kubernetes/Mesos 或者本地 Flink 上運行。Flink Runner 適合大規模,連續的數據處理任務,包含了以下功能:

  • 以 Streaming 爲中心,支持 streaming 處理和 batch 處理;
  • 和 flink 一樣的容錯性,和 exactly-once 的處理語義;
  • 可以自定義內存管理模型;

和其他(例如 YARN)的 Apache Hadoop 生態整合比較好。

<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-runners-flink-1.6</artifactId>
<version>2.13.0</version>
</dependency>

flink runner

mvn exec:java -Dexec.mainClass=YourMainClass \    -Pflink-runner \    -Dexec.args="--runner=FlinkRunner \      --flinkMaster=flink master url>"

google dataflow運行模式

Beam Pipeline 也能直接在雲端運行。Google Cloud Dataflow 就是完全託管的 Beam Runner。當你使用 Google Cloud Dataflow 服務來運行 Beam Pipeline 時,它會先上傳你的二進制程序到 Google Cloud,隨後自動分配計算資源創建 Cloud Dataflow 任務。

<dependency>
<groupId>org.apache.beam</groupId>
<artifactId>beam-runners-google-cloud-dataflow-java</artifactId>
<version>2.13.0</version>
<scope>runtime</scope>
</dependency>

mvn -Pdataflow-runner compile exec:java \      -Dexec.mainClass=YourMainClass> \      -Dexec.args="--project=PROJECT_ID> \      --stagingLocation=gs://STORAGE_BUCKET>/staging/ \      --output=gs://STORAGE_BUCKET>/output \      --runner=DataflowRunner"

Beam Window

窗口

PCollectionString> input = p.apply(TextIO.read().from(filepath));PCollectionString> batchInputs = input.apply(Window.String>into(new GlobalWindows()));

需要注意的是,你在處理有界數據集的時候,可以不用顯式地將一個窗口分配給一個 PCollection 數據集。但是,在處理無邊界數據集的時候,你必須要顯式地分配一個窗口給這個無邊界數據集。而這個窗口不可以是前面提到的全局窗口,否則在運行數據流水線的時候會直接拋出異常錯誤。

固定窗口(Fixed Window)

通常由一個靜態窗口大小定義

PCollectionString> input = p.apply(KafkaIO.Long, String>read()).apply(Values.String>create());PCollectionString> fixedWindowedInputs = input.apply(Window.String>into(FixedWindows.of(Duration.standardHours(1))));

滑動窗口

一個靜態窗口大小和一個滑動週期定義而來

PCollectionString> input = p.apply(KafkaIO.Long, String>read()).apply(Values.String>create());PCollectionString> slidingWindowedInputs = input.apply(Window.String>into(SlidingWindows.of(Duration.standardHours(1)).every(Duration.standardMinutes(30))));

會話窗口(Session Window)

會話窗口主要是用於記錄持續了一段時間的活動數據集。在一個會話窗口中的數據集,如果將它裏面所有的元素按照時間戳來排序的話,那麼任意相鄰的兩個元素它們的時間戳相差不會超過一個定義好的靜態間隔時間段(Gap Duration)。

PCollectionString> input = p.apply(KafkaIO.Long, String>read()).apply(Values.String>create());PCollectionString> sessionWindowedInputs = input.apply(Window.String>into(Sessions.withGapDuration(Duration.standardMinutes(5))));

 

示例

經典例子wordcount

public class BeamMinimalWordCountTest {
    @SuppressWarnings("serial")
    public static void main(String[] args) {
            PipelineOptions options = PipelineOptionsFactory.create();
            options.setRunner(DirectRunner.class); // 顯式指定PipelineRunner:DirectRunner(Local模式)

            Pipeline pipeline = Pipeline.create(options);

            // 讀取本地文件,構建第一個PTransform
            pipeline.apply("ReadLines", TextIO.read().from("文件url"))
                    .apply("ExtractWords", ParDo.of(new DoFn<String, String>() {
                        // 對文件中每一行進行處理(實際上Split)
                        @ProcessElement
                        public void processElement(ProcessContext c) {
                            for (String word : c.element().split("[\\s:\\,\\.\\-]+")) {
                                if (!word.isEmpty()) {
                                    c.output(word);
                                }
                            }
                        }

                    }))
                    // 統計每一個Word的Count
                    .apply(Count.<String> perElement()) 
                    .apply("ConcatResultKVs", MapElements.via( 
                            // 拼接最後的格式化輸出(Key爲Word,Value爲Count)
                            new SimpleFunction<KV<String, Long>, String>() {

                                @Override
                                public String apply(KV<String, Long> input) {
                                    return input.getKey() + ": " + input.getValue();
                                }

                            }))
                    .apply(TextIO.write().to("wordcount"));
            // 輸出結果

            pipeline.run().waitUntilFinish();
    }
}

引用蔡元楠《大規模數據處理實戰》

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