Apache Beam開發指南

本指南用於指導Beam用戶使用Beam SDK創建數據處理pipeline(pipeline)。本文會引導您用BeamSDK類構建和測試你的pipeline。本文不會詳盡闡述所有內容,但可以看做一門未知的“編程語言”,引導您用編程的方式構建您的Beampipeline。隨着開發指南內容愈加豐富,本文將會包含多種語言的示例代碼,用於展示如何在您的pipeline中實現Beam概念。
1 概述
爲了使用Beam,你首先需要使用BeamSDK中的類創建一個驅動程序(driver program)。你的驅動程序定義(define)了你的pipeline,包括所有的輸入、傳輸、輸出;它也用於設置pipeline的運行參數(execution options)(通常是從命令行獲取到輸入參數)。也包括Pipeline Runner的參數,反過來,它又定義了你的pipeline將運行在什麼後臺中。
BeamSDK提供了一組抽象概念簡化了高可擴展分佈式數據處理過程的結構。這些抽象概念可以同時作用於塊數據和流數據源。當你創建Beampipeline時,你可以認爲你的數據處理任務是在這些抽象概念中執行。抽象概念包括:
1)Pipeline:一個Pipeline封裝了你全部的數據處理任務,從開始到結束。它包括了讀取輸入數據,傳輸數據,輸出數據。所有的Beam驅動程序必須創建一個Pipeline。當你創建Pipeline時,你必須指定Pipeline運行環境和運行方式的參數。
2)PCollection:一個PCollection表述了一個分佈式數據集(distribute data set),你的Beampipeline會操作它。數據集可以是有界的(bounded),意思是它可以來自於確切的源比如一個文件,或者是無界的(unbounded),意思是它也可以來自於持續更新的源通過一個訂閱或者其他途徑。你的pipeline通常通過讀取外部數據源的數據來創建一個初始的PCollection實例,不過你也可以在你的驅動程序內部用內存數據創建一個PCollection實例。從這裏開始,PCollection對象將作爲你pipeline中每一步操作的輸入輸出。
3)Transform:一個Transform代表了一個數據處理操作,也就是你pipeline中的“一步”(a step)。每一個Transform對象把一個或多個PCollection對象作爲輸入,執行一個你提供給PCollection成員的處理函數,然後產出一個或多個輸出PCollection對象。
4)I/O Source和Sink:Beam提供Source和Sink API接口分別代表讀取和寫出數據。Source封裝的代碼用於從外部源讀取數據到你的pipeline,比如雲存儲文件或者一個訂閱的流數據源。Sink類似的封裝了把PCollection數據寫出到一個外部數據槽(sink)。
一個典型的Beam驅動程序工作內容如下:
1)創建一個Pipeline對象並設置pipeline的執行參數,包括Pipeline Runner的參數。
2)爲pipeline數據創建一個初始的PCollection對象,用於通過SourceAPI讀取外部數據源的數據,或者通過創建Transform用內存數據構建一個PCollection對象。
3)應用Transform對象到每一個PCollection對象。Transform對象可以對PCollection數據進行修改,過濾,分組,分析或者其他操作。一個Transform對象創建一個新的輸出PCollection並需要輸入PCollection。一個典型的pipeline隨後反過來把Transform對象應用到每一個新的輸出PCollection直到處理過程結束。
4)輸出最後,傳輸PCollection對象,通常就是用Sink接口把數據寫出到外部源。
5)用指定的Pipeline Runner運行(run)這個pipeline。
當你運行你的Beam驅動程序時,指定的Pipeline Runner會構造一個pipeline工作流程圖(workflow graph),基於之前創建的PCollection對象和應用的Transform對象。該流程圖執行合適的後臺分佈式處理過程,一個後臺異步“job”(或等同於)。
2 創建pipeline對象
Pipeline抽象封裝了數據處理任務所有的數據和執行步驟。你的Beam驅動程序通常起始於構建一個Pipeline對象,然後用該對象作爲基礎創建pipeline的數據集比如PCollection對象和操作比如Transform對象。
爲了使用Beam,你的驅動程序首先必須創建一個BeamSDK類Pipeline的實例(通常在main函數中)。當你創建了你的pipeline對象,你還需要設置一些配置項。你可以在程序中設置配置項,不過通常提前設置(或從命令行讀取)配置項會更簡單,然後創建該對象時傳遞給它。
pipeline配置項定義了很多東西,PipelineRunner定義了pipeline在哪裏執行:本地,或者分佈式後臺。基於pipeline在哪裏執行和指定哪個Runner的要求,配置項可以幫你指定執行過程的其他方面。
爲了設置pipeline的配置項並創建pipeline對象,可以創建一個類型爲PipelineOptions的對象並傳遞給Pipeline.Create()。通常該操作是通過解析命令行參數進行的:
public static void main(String[] args) {
   // 通過解析傳入應用的參數來構建一個PipelineOptions對象
   // 這裏, --help 會打印註冊項,即 --help=PipelineOptionsClassName
   // 會打印指定類的功能.
   PipelineOptions options =
       PipelineOptionsFactory.fromArgs(args).create();


   Pipeline p = Pipeline.create(options);
   
from apache_beam.utils.pipeline_options import PipelineOptions
BeamSDK包含了PipelineOptions各種子類用於不同的Runner。比如,DirectPipelineOptions包含的配置項用於Direct(本地的)Pipeline Runner,而DataflowPipelineOptions包含的配置項用於Google Cloud Dataflow。通過使用實現繼承自類PipelineOptions的接口,你也可以定製自己的PipelineOptions。
3 使用PCollection對象
PCollection抽象表述了可分佈式,多元數據集。你可以把一個PCollection對象當做“Pipeline”的數據;Beam的Transform對象使用PCollection作爲輸入輸出。因此,如果你想在你的Pipeline中使用數據,你必須通過PCollection對象的形式。
當你創建完Pipeline後,你需要用某種方式創建至少一個PCollection對象。該對象將作爲Pipeline第一個操作的輸入。
3.1 創建一個PCollection
創建方式有兩種:通過SourceAPI讀取外部數據源的數據創建一個PCollection對象,或者通過驅動程序內存數據創建。區別在於pipeline對象怎麼獲取數據。SourceAPI包含的適配器可以幫助你從外部數據源讀取大型雲存儲文件,數據庫,或者內容訂閱服務。後者測試和調試目的。
3.1.1 從外部源讀取
爲了從外部源讀取,你需要使用一個Beam提供的I/O適配器。這些適配器在功能上各不相同,但他們都來自於外部數據源並返回一個PCollection對象,它代表了數據源中的數據記錄。
每一個數據源適配器都有一個讀(Read)Transform對象,爲了讀取,你必須把該對象應用到Pipeline對象自身。舉例來說,TextIO.Read,從一個外部文本文檔讀取數據,並返回一個PCollection對象,它的元素類型是String,每個String元素代表了文檔中的一行。把TextIO.Read用於Pipeline對象創建PCollection對象的代碼如下:
public static void main(String[] args) {
    // Create the pipeline.
    PipelineOptions options = 
        PipelineOptionsFactory.fromArgs(args).create();
    Pipeline p = Pipeline.create(options);


    PCollection<String> lines = p.apply(
      "ReadMyFile", TextIO.Read.from("protocol://path/to/some/inputData.txt"));
}
BeamSDK支持的讀取不同數據源可參看API文檔中I/O部分。
3.1.2 從內存數據創建PCollection對象
爲了從內存中Java Collection類創建PCollection對象,你可以使用Beam提供的Create類(一種Transform類)。類似於數據適配器的Read,你可以把Create直接應用到pipeline對象上。
Create接受Java Collection對象和Coder對象作爲參數。Coder對象指定了Collection中元素是如何編碼的。
舉例來說,爲了從內存中list對象創建PCollection對象,使用Beam提供的Create類的transform。然後把該transform直接應用到Pipeline對象上。
下面的代碼演示瞭如何從內存List對象創建PCollection對象:
public static void main(String[] args) {
    // Create a Java Collection, in this case a List of Strings.
    static final List<String> LINES = Arrays.asList(
      "To be, or not to be: that is the question: ",
      "Whether 'tis nobler in the mind to suffer ",
      "The slings and arrows of outrageous fortune, ",
      "Or to take arms against a sea of troubles, ");


    // Create the pipeline.
    PipelineOptions options = 
        PipelineOptionsFactory.fromArgs(args).create();
    Pipeline p = Pipeline.create(options);


    // Apply Create, passing the list and the coder, to create the PCollection.
    p.apply(Create.of(LINES)).setCoder(StringUtf8Coder.of())
}
3.2 PCollection的特性
一個PCollection對象被創建它的Pipeline對象所持有。多個Pipeline不會共享同一個PCollection。在某些方面,PCollection類的功能類似於一個Java Collection類。但是,兩者有一些很關鍵的不同點:
1)元素類型:PCollection的元素可以是任意類型,但是所有元素必須類型相同。然而,爲了支持分佈式處理,Beam需要能把每個單獨元素編碼成一個比特流(因此元素可以在分佈式工作者間傳遞)。BeamSDK提供了一個數據編碼框架,包括通用數據類型的內建編碼,和良好支持的訂製編碼。
2)不變性:一個PCollection對象是不變的。一旦創建,就不能再添加、移除、修改單個元素。一個Beam Transform對象可以處理每一個PCollection元素,然後生成新的Pipeline數據(即一個新的PCollection對象),但它不需要也不會修改原始輸入集。
3)隨機訪問:PCollection不支持隨機訪問單個元素。相反,Beam Transform會單獨訪問PCollection中的每一個元素。
4)數據大小和邊界性:PCollection是大型的不變的元素“包”。PCollection包含的元素數量沒有上限。PCollection會在單機中佔用適量的內存空間,或者代表依靠持久化存儲的非常大規模的分佈式數據集。
PCollection對象的大小可以是有界或無界的。一個有界的PCollection對象代表了一個已知的、大小固定的數據集,而一個無界的PCollection對象代表了一個無大小限制的數據集。PCollection的邊界性依賴於它所代表的數據集。從塊數據讀取,比如一個文件或者一個數據庫,則創建有界PCollection對象。從流或者持續刷新的數據源讀取,比如Pub/Sub或Kafka,則創建無界PCollection(除非你明確指明它不是無界的)。
有界(或無界)性質會影響Beam如何處理你的數據。一個有界PCollection可以用塊任務(batch job)進行處理,它可以一次讀取整個數據集,然後執行有限長度的處理任務。一個無界PCollection用支持運行的流任務處理,就好像整個集合在任何時刻對處理過程都不會全部可用。
當對無界PCollection中的一組元素執行操作時,Beam有一個分割持續刷新數據集的概念叫“分窗(Windowing)”,即邏輯上有限大小的窗口。Beam把每個窗口當做一個數據包,然後持續處理數據集產出的數據包。這些邏輯窗口由和時間元素相關的性質所決定,比如一個timestamp(時間戳)。
5)元素時間戳:每一個PCollection中的元素都有一個內聯的時間戳。每個元素的時間戳都由創建PCollection的Source賦予初值。Source創建一個無界PCollection,並把新元素讀取或添加的時間賦予元素的時間戳。
注意:Source爲固定大小數據集創建有界PCollection時,也會自動賦予時間戳,但通常習慣是給每個元素賦予相同的時間戳(Long.MIN_VALUE)。
時間戳用於天然帶有時間性的PCollection元素時非常有用。如果你的Pipeline讀取一個事件流,比如Tweete或其他設計媒體消息,每個元素可以把事件發出的時間當做元素時間戳。
如果Source沒有給PCollection元素賦值時間,你也可以手動賦值。當元素天然具有時間戳時,你可能希望這麼做,但是時間戳是在元素結構內部的某個角落(比如位於服務器日誌入口的“time”屬性)。
Beam的Transform對象把一個PCollection作爲輸入並輸出帶時間戳的相同的PCollection。
4 應用Transform
在BeamSDK中,Transform代表了Pipeline中的操作。一個Transform對象把一個PCollection對象(或者不止一個PCollection對象)當做輸入,對集合中每個元素執行指定的操作,生成一個新的輸出PCollection對象。爲了調用一個transform,你必須把它應用在輸入PCollection中。
在BeamSDK中,每個transform類都有一個通用apply()方法(或者管道操作符"|")。調用多個Beam transform操作就如同調用方法鏈,不過有一點小小的不同:你把transform用於輸入PCollection,則transform操作本身被當做參數,該操作返回輸出PCollection。下面是常用代碼格式:
[Output PCollection] = [Input PCollection].apply([Transform])
因爲Beam把通用apply方法用於PCollection,你可以有序鏈接transform操作,也可以在transform過程中加入其他transform(稱之爲"複合transform(composite transforms)")。
如何應用pipeline的transform決定了pipeline的結構。最好把你的pipeline理解爲一個有向無環圖,圖的節點是PCollection,邊是transform操作。舉例來說,你可以鏈接transform操作來創建一個有序pipeline,如下:
[Final Output PCollection] = [Initial Input PCollection].apply([First Transform])
.apply([Second Transform])
.apply([Third Transform])
上面的pipeline最終工作流圖像這樣:
[Sequential Graph Graphic]
但是,注意,一個transform操作不消耗或修改輸入集合————要記住,PCollection被定義爲不可變的。這意味着,你可以把多個transform操作應用到同一個輸入PCollection從而創建一個pipeline分支,像這樣:
[Output PCollection 1] = [Input PCollection].apply([Transform 1])
[Output PCollection 2] = [Input PCollection].apply([Transform 2])
上面的分支pipeline的最終工作流圖是這樣:
[Branching Graph Graphic]
你可以創建自己的複合transform操作,在一個大型transform中加入多個子步驟。複合transform
非常有用,特別是在多個地方使用可重複有序執行的簡單步驟。
4.1 BeamSDK中的Transform操作
BeamSDK中的Transform操作提供了一個通用處理框架,你可以用函數對象的方式提供處理邏輯(我們常用“用戶代碼”稱呼它)。用戶代碼被用在輸入的PCollection元素。用戶代碼實例在集羣中被多個不同工作者並行執行,並依賴於你選擇執行pipeline的pipeline runner和後臺。每個工作者運行的用戶代碼生成的輸出元素,最終都被添加到Transform操作生成的輸出PCollection中。
4.2 核心Beam transform操作
Beam提供了下面的transform操作,每一個代表了一個不同的處理範式:
1)ParDo
2)GroupByKey
3)Combine
4)Flatten and Partition
4.2.1 ParDo
ParDo用於通用的並行處理過程。ParDo處理範式類似於Map/Shuffle/Reduce模型的“Map"過程:一個ParDo transform操作處理輸入PCollection中每個元素,執行一些處理函數(用戶代碼),發生0個,1個,或多個元素到一個輸出PCollection。
ParDo在多種常見數據處理過程中非常有用,包括:
1)過濾數據集:你可以訪問PCollection中每個元素,決定輸出它到新的集合或者丟棄它。
2)格式化或類型轉換集合中每個元素:如果你的輸入數據元素有不同的你不想要的類型或格式,你可以用ParDo對每個元素執行轉換操作並把結果輸出到新的PCollection。
3)提取數據集的一部分:如果你的數據記錄包含多個域,你可以把你關心的域解析出來輸出到一個新的PCollection。
4)對數據集每個元素執行計算:你可以用ParDo執行簡單的或複雜的計算,對每個元素或某些元素,然後輸出結果到一個新的PCollection。
在這些規則中,ParDo是一個管道的通用中間步驟。你可以用它提取原始記錄集的一部分域,或者把原始輸入轉變爲不同的格式。你也可以用ParDo把待處理數據轉換爲輸出需要的格式,比如數據庫錶行格式或可打印字符串。
當你使用ParDo操作,你需要用DoFn對象的方式提供用戶代碼。Dofn是一個BeamSDK類,定義了一個分佈式處理的功能。
注意:當你創建一個DoFn的子類時,你的子類必須依照“Beam Transform中編寫用戶代碼的規範”。
使用ParDo
和所有Beam transform相似,在輸入PCollection上調用apply方法並把ParDo作爲參數,就像下面的例子這樣:
// 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.
在這個例子中,我們輸入的PCollection包含的是String值。我們應用ParDo並指定一個函數(ComputeWordLengthFn)來計算每個string對象的長度,然後輸出的PCollection包含的是Integer值,保存了每個單詞的長度。
創建一個DoFn
你傳遞給ParDo的DoFn對象,包含了處理輸入集合元素的業務邏輯。當你使用Beam時,通常你的代碼最重要的部分就是實現DoFn————他們定義了你的管道實際處理數據的任務是什麼。
注意:當你創建你的DoFn時,要謹記“Beam Transform中編寫用戶代碼的規範”,並確保你的代碼嚴格遵照規範。
DoFn每次處理輸入PCollection集合的一個元素。當你新建一個DoFn子類,你需要提供參數類型並匹配輸入輸出元素。如果你的DoFn處理過程輸入String元素併產生Integer元素到輸出集合(像前面的例子那樣),你的類申明應該像這樣:
static class ComputeWordLengthFn extends DoFn<String, Integer> { ... }
在你的DoFn子類內部,你需要寫一個方法,它帶有@ProcessElement註解,提供實際的處理邏輯。你不許手動從輸入集合提取數據。BeamSDK會爲你處理。你的@ProcessElement方法需要接受一個類型爲ProcessContext的對象。該對象允許你訪問一個輸入元素,並給你一個輸出元素的方法。
static class ComputeWordLengthFn extends DoFn<String, Integer> {
  @ProcessElement
  public void processElement(ProcessContext c) {
    // Get the input element from ProcessContext.
    String word = c.element();
    // Use ProcessContext.output to emit the output element.
    c.output(word.length());
  }
}
注意:如果輸入PCollection集合中的元素是鍵值對,你可以通過ProcessContext.element().getKey()訪問鍵或通過ProcessContext.element().getValue()訪問值。
一個DoFn實例經常會被多次調用來處理任意數量的元素。但是,Beam並不保證確切的調用次數;它可能因爲失敗和重試,在一個工作者節點調用多次。同樣的,你也可以通過多次調用處理方法來緩存信息,但是如果你這樣做了,你需要確保你的實現不依賴於調用次數。
在你的處理方法中,你也需要遵守不變性要求,以確保Beam和後臺處理過程可以安全的序列化和緩存值到管道中。你的方法需要遵循下面的要求:
1)你不能用任何方法修改ProcessContext.element() or ProcessContext.sideInput()的返回元素(後者是的輸入元素來自輸入集合)。
2)一旦你用ProcessContext.output() or ProcessContext.sideOutput()輸出一個值,你不能再用任何方法修改它。
輕量級DoFn和其他抽象概念
如果你的函數關係很明確,你可以通過內聯一個輕量級DoFn來使用ParDo,如同一個內部匿名類實例。
下面是之前例子的修改,DoFn被定義爲一個匿名內部類:
// The input PCollection.
PCollection<String> words = ...;


// Apply a ParDo with an anonymous DoFn to the PCollection words.
// Save the result as the PCollection wordLengths.
PCollection<Integer> wordLengths = words.apply(
  "ComputeWordLengths",                     // the transform name
  ParDo.of(new DoFn<String, Integer>() {    // a DoFn as an anonymous inner class instance
      @ProcessElement
      public void processElement(ProcessContext c) {
        c.output(c.element().length());
      }
    }));
如果你的ParDo執行從輸入到輸出元素一對一的映射————也即,對每一個輸入元素,應用方法後只生成唯一的輸出元素,你可以使用更高層的MapElements Map transform操作。MapElements 可以接受一個Java8中匿名lambda函數簡化流程:
// The input PCollection.
PCollection<String> words = ...;


// Apply a MapElements with an anonymous lambda function to the PCollection words.
// Save the result as the PCollection wordLengths.
PCollection<Integer> wordLengths = words.apply(
  MapElements.via((String word) -> word.length())
      .withOutputType(new TypeDescriptor<Integer>() {});
注意:你可以在其他Beam transform操作中用lambda表達式,包括Filter, FlatMapElements, 和Partition.
4.2.2 使用GroupByKey
GroupByKey是一個用來處理鍵值對集合的Beam transform操作。它是一個並行收縮操作,類似於
Map/Shuffle/Reduce模型的“Shuffle”過程。GroupByKey的輸入是一個鍵值對集合代表了一個multimap集合,該集合包含多對同鍵不同值。給定這樣一個集合,你可以用GroupByKey收集所有唯一鍵關聯的值。
GroupByKey通常是聚合數據的好方法。舉例來說,如果你有一個集合保存了用戶訂單記錄,你可能希望通過郵政編碼分組聚合所有的訂單(這裏“key”就是郵政編碼,“value”就是其他記錄)。
讓我們在一個簡單的示例中觀察GroupByKey的結構,我們的數據集由一個文本文檔的單詞組成,並顯示它們所在的行號。我們期望所有的行號(value)共享同一個單詞(key),讓我們可以看到一個具體的單詞在文檔中出現的所有位置。
我們的輸入是一個鍵值對的PCollection,每個單詞是一個鍵,值是單詞在文檔中所在的行號。下面是輸入集合中的一部分鍵值對:
cat, 1
dog, 5
and, 1
jump, 3
tree, 2
cat, 5
dog, 2
and, 2
cat, 9
and, 6
...
GroupByKey收集了同一個key的所有value,輸出一個新的鍵值對,鍵仍然是這個唯一key,而值是輸入集合中和該鍵關聯的所以值的集合。如果我們把GroupByKey應用到我們上面的輸入集合中,則輸出結果類似於:
cat, [1,5,9]
dog, [5,2]
and, [1,2,6]
jump, [3]
tree, [2]
...
因而,GroupByKey代表了一個mulitmap(多個鍵對應單獨值)到一個uni-map(唯一鍵對應值集合)的transform操作。
4.2.3 使用Combine
Combine是一個組合數據中元素或值的Beam transform操作。Combine在整個PCollection上產生作用,有些combine操作組合PCollection鍵值對集合中每個key的所有value。
當你使用Combine操作時,你必須提供函數來執行組合元素或值的邏輯。這個組合函數必須是可替換和有關聯的,也就是這個函數不需要在給定key的所有value上都調用一次。因爲輸入數據(包括value集合)可能是分佈在多個工作者中,組合函數可能調用多次,每次對value集合的子集執行部分組合操作。BeamSDK也提供了一些預置的組合函數用於通用數字組合操作,比如sum,min和max。
普通的組合操作,比如sum,一般可以用普通函數實現。更多的複雜組合操作需要你創建CombinFn子類,它有一個累積類型,不同於輸入/輸出類型。
簡單方法實現的簡單組合:
// Sum a collection of Integer values. The function SumInts implements the interface SerializableFunction.
public static class SumInts implements SerializableFunction<Iterable<Integer>, Integer> {
  @Override
  public Integer apply(Iterable<Integer> input) {
    int sum = 0;
    for (int item : input) {
      sum += item;
    }
    return sum;
  }
}
使用CombinFn實現高級組合
對於更復雜的組合功能,你可以定義一個CombinFn的子類來實現。你需要使用CombinFn來實現組合功能要求的更復雜的累加器,必須執行額外的預處理,可能會改變輸出類型,或者把key納入計算。
一個通用組合功能由四個操作構成。當你創建一個CombinFn的子類時,你必須通過覆寫對應的方法來提供這四個操作:
1)創建累加器:創建一個新的“本地化”累加器。在下面的例子中,就是求平均值,一個本地累加器跟蹤值的累加結果(就是最終求平均值時的分子),並且統計當前已累加值的數量(也就是分母)。這在分佈式環境中可能會調用多次。
2)添加輸入:給累加器添加一個輸入元素,返回累加的結果。在我們的示例中,它會更新sum的值並遞增count。這也可能會被並行調用。
3)合併累加器:合併這些累加器到一個累加器,也就是多個累加器的數據如何在最終計算前進行組合。在求平均值計算的例子中,每一部分參與除法的累加器被合併在一起。這可能在輸出上調用多次。
4)提取輸出:執行最終計算。在計算平均值的例子中,用組合的所有值的累加除以參與累加的值的數量。這在最後合併的累加器上調用一次。
下面的例子演示瞭如何定義一個CombinFn來計算一個平均值:
public class AverageFn extends CombineFn<Integer, AverageFn.Accum, Double> {
  public static class Accum {
    int sum = 0;
    int count = 0;
  }


  @Override
  public Accum createAccumulator() { return new Accum(); }


  @Override
  public Accum addInput(Accum accum, Integer input) {
      accum.sum += input;
      accum.count++;
      return accum;
  }


  @Override
  public Accum mergeAccumulators(Iterable<Accum> accums) {
    Accum merged = createAccumulator();
    for (Accum accum : accums) {
      merged.sum += accum.sum;
      merged.count += accum.count;
    }
    return merged;
  }


  @Override
  public Double extractOutput(Accum accum) {
    return ((double) accum.sum) / accum.count;
  }
}
如果你組合一個鍵值對的PCollection集合,“預定義鍵值組合”一般夠用了。如果你想基於鍵來改變組合策略(比如,MIN或MAX操作),你可以定義一個KeyedCombineFn用來在組合策略中訪問key。
組合一個PCollection集合爲一個單獨值
使用全局組合把一個給定的PCollection集合的轉換爲一個單獨值,在你的管道中就好像一個PCollection只有一個元素。下面的例子演示瞭如何應用Beam提供的sum組合功能來生成一個單獨的累加值,輸出一個整型PCollection集合:
// Sum.SumIntegerFn() combines the elements in the input PCollection.
// The resulting PCollection, called sum, contains one value: the sum of all the elements in the input PCollection.
PCollection<Integer> pc = ...;
PCollection<Integer> sum = pc.apply(
   Combine.globally(new Sum.SumIntegerFn()));
全局分窗:如果你輸入PCollection集合使用了默認的全局分窗,那麼默認行爲是返回一個包含一個元素的PCollection。這個元素的值來自於組合函數的累加器,也就是你使用Combine時指定的組合函數。比如,Beam提供的sum組合函數默認返回了一個零值(空輸入的累加和),而最小值組合函數會返回一個最大值或無限值。
爲了不讓Combine在輸入爲空時返回空的PCollection集合,你可以在使用Combine時指定withoutDefaults(),如下所示:
PCollection<Integer> pc = ...;
PCollection<Integer> sum = pc.apply(
  Combine.globally(new Sum.SumIntegerFn()).withoutDefaults());
非全局分窗:如果你的PCollection集合使用了任何非全局分窗功能,Beam將不再提供默認行爲。你在使用Combine時必須指定下列配置項之一:
1)指定withoutDefaults(),默認是輸入PCollection集合的窗口是空的,則輸出PCollection集合同樣是空的。
2)指定asSingletonView(),輸出會立即轉換爲一個PCollectionView對象,它爲每個當做輸入的空窗口提供了默認值。一般你需要這個選項的情況是,你的管道的Combine操作結果在另一個管道被當做輸入。
在按鍵分組集合中組合值
在創建完一個按鍵分組的集合(比如,通過使用GroupByKey轉換)後,一個常見操作是組合每個鍵關聯的值到一個獨立合併的值。借鑑前面GroupByKey的例子,命名爲groupedWords的按鍵分組PCollection集合如下:
cat, [1,5,9]
dog, [5,2]
and, [1,2,6]
jump, [3]
tree, [2]
...
在上面的PCollection集合中,每個元素有一個字符串關鍵字(比如“cat”),和一個可迭代的整型值(在第一個元素中就是[1,5,9])。如果我們接下來的步驟是聚合這些值(而不是單獨考慮每個值),你可以組合這些可迭代整型值爲一個獨立聚合值,並和每個關鍵字組成鍵值對。使用合併value集合的GroupByKey模型等同於按鍵(PerKey)Combine轉換。你提供給Combine PerKey的聚合函數必須是一個關係收縮函數或一個CombinFn的子類:
// PCollection is grouped by key and the Double values associated with each key are combined into a Double.
PCollection<KV<String, Double>> salesRecords = ...;
PCollection<KV<String, Double>> totalSalesPerPerson =
  salesRecords.apply(Combine.<String, Double, Double>perKey(
    new Sum.SumDoubleFn()));


// The combined value is of a different type than the original collection of values per key.
// PCollection has keys of type String and values of type Integer, and the combined value is a Double.


PCollection<KV<String, Integer>> playerAccuracy = ...;
PCollection<KV<String, Double>> avgAccuracyPerPlayer =
  playerAccuracy.apply(Combine.<String, Integer, Double>perKey(
    new MeanInts())));
4.2.4 使用Flatten和Partition
Flatten和Partition用於轉換存儲相同數據類型的PCollection集合。Flatten合併多個PCollection集合對象爲一個邏輯PCollection對象,而Partition把一個獨立PCollection對象分割爲確切數量的較小的集合。
Flatten
下面的例子演示瞭如何用Flatten轉換來聚合多個PCollection對象:
// Flatten takes a PCollectionList of PCollection objects of a given type.
// Returns a single PCollection that contains all of the elements in the PCollection objects in that list.
PCollection<String> pc1 = ...;
PCollection<String> pc2 = ...;
PCollection<String> pc3 = ...;
PCollectionList<String> collections = PCollectionList.of(pc1).and(pc2).and(pc3);


PCollection<String> merged = collections.apply(Flatten.<String>pCollections());
聚合後集合數據的編碼方式:
默認情況下,輸出PCollection集合的編碼器和輸入PCollectionList的第一個PCollection集合編碼器相同。可是,輸入PCollection對象每個都可能有不同的編碼器,不過只要他們在你選擇的開發語言中的數據類型相同即可。
合併分窗集合:
如果使用Flatten合併的PCollection集合對象有分窗策略,那麼所有要合併的PCollection集合對象必須使用兼容的分窗策略和窗口大小。舉例來說,所以你要合併的集合必須全部(假設)使用相同的每5分鐘固定窗口或每30秒滑動4分鐘窗口。
如果你的管道嘗試把Flatten用在合併窗口不兼容的PCollection集合中,Beam會在管道創建時產生一個IllegalStateException 錯誤。
Partition
Partition按照你提供的分塊函數劃分PCollection集合的元素。分塊函數的邏輯定義瞭如何劃分輸入PCollection集合元素到每個部分結果PCollection集合中。劃分數量必須在構造時就指定。舉例來說,當運行時(也就是構建管道圖時),你可以在命令行輸入劃分數量,但你不能在管道中期定義分塊數。
下面的例子按百分比分組一個PCollection集合:
// Provide an int value with the desired number of result partitions, and a PartitionFn that represents the partitioning function.
// In this example, we define the PartitionFn in-line.
// Returns a PCollectionList containing each of the resulting partitions as individual PCollection objects.
PCollection<Student> students = ...;
// Split students up into 10 partitions, by percentile:
PCollectionList<Student> studentsByPercentile =
    students.apply(Partition.of(10, new PartitionFn<Student>() {
        public int partitionFor(Student student, int numPartitions) {
            return student.getPercentile()  // 0..99
                 * numPartitions / 100;
        }}));


// You can extract each partition from the PCollectionList using the get method, as follows:
PCollection<Student> fortiethPercentile = studentsByPercentile.get(4);
4.3 編寫Beam transform用戶代碼的規範
當你爲一個Beam轉換編寫用戶代碼時,你必須牢記分佈執行性。舉例來說,可能有很多份你的函數的拷貝,並行運行在很多不同機器上,而這些拷貝之間沒有依賴性,沒有交互或共享狀態。依賴於你選擇的Pipeline Runner和運行後臺,用戶代碼的每份拷貝可能被重試或運行多次。同樣的,你需要十分小心用戶代碼間的狀態依賴等狀況。
通常來講,用戶代碼必須至少滿足下列要求:
1)你的功能對象必須是可序列化的。
2)你的功能對象必須是線程兼容的,而且要意識到BeamSDK不是線程安全的。
另外,建議你的功能對象是冪等的(idempotent)。
注意:這樣要求應用在Dofn子類(用於ParDo轉換的功能對象),CombinFn子類(用於Combine轉換的功能對象),WindowFn(用於Window轉換的功能對象)。
4.3.1 可序列化
你提供給transform操作的任何功能對象都必須是可完全序列化的。因爲在你的處理集羣中,功能的拷貝會被序列化併發往遠程工作者。用戶代碼的基類,包括DoFn,CombineFn和WindowFn,甚至包括實現的Serializable接口;無論如何,你的子類不能添加任何非可序列化成員。
下面是其他一些你需要牢記的序列化因素:
1)你的功能對象的瞬時狀態域不會被髮往工作者實例,因爲他們不能自動序列化。
2)避免在序列化前加載包含某個域的大量數據。
3)你的功能對象的單個實例不能共享數據。
4)一個功能對象在使用以後被修改是不起作用的。
5)用匿名內聯類實例申明你的功能對象時一定要小心。在非靜態環境中,你的內聯類實例將隱式包含一個指針,指向該閉合類和該類的狀態。該閉合類也應該是可序列化的,因而同樣的考慮需要用在功能函數自身和用到它的外部類。
4.3.2 線程兼容性
你的功能對象必須是線程兼容的。功能對象的每一個實例只被一個工作者實例訪問,除非你顯示創建了自己的線程。注意,無論何時,BeamSDK都是線程不安全的。如果你在用戶代碼中創建了自己的線程,你必須自己進行同步操作。注意,功能對象的靜態成員不會被傳遞給工作者實例,而且多個功能實例會被不同的線程訪問。
4.3.3 冪等
建議你的功能對象是冪等的————也就是說,它可以重複或重試多次而不會產生意外結果。Beam模型不保證你的代碼被調用或重試的次數;同樣的,保持功能對象冪等性就是保持管道輸出的確定性,因而你的transform操作行爲會更可預測也更易於調試。
5 側面輸入和側面出端
5.1 側面輸入
除了主輸入PCollection集合外,你還可以提供附加輸入到一個ParDo轉換操作(按照側面輸入的格式)。一個側面輸入是一個附加輸入,你的DoFn每次處理一個主輸入PCollection集合元素時都可以訪問它。當你指定了一個側面輸入,你就創建了一些其他數據的視圖,可以讀取自ParDo轉換內部的DoFn函數處理每個元素時。
當你的ParDo需要在處理輸入集合元素時注入附加數據時,側面輸入會非常有用,但是附加數據需要在運行時定義(而且不能是硬編碼)。這些值被輸入數據定義,或者依賴於管道的不同塊。
示例:
// Pass side inputs to your ParDo transform by invoking .withSideInputs.
  // Inside your DoFn, access the side input by using the method DoFn.ProcessContext.sideInput.


  // The input PCollection to ParDo.
  PCollection<String> words = ...;


  // A PCollection of word lengths that we'll combine into a single value.
  PCollection<Integer> wordLengths = ...; // Singleton PCollection


  // Create a singleton PCollectionView from wordLengths using Combine.globally and View.asSingleton.
  final PCollectionView<Integer> maxWordLengthCutOffView =
     wordLengths.apply(Combine.globally(new Max.MaxIntFn()).asSingletonView());




  // Apply a ParDo that takes maxWordLengthCutOffView as a side input.
  PCollection<String> wordsBelowCutOff =
  words.apply(ParDo.withSideInputs(maxWordLengthCutOffView)
                    .of(new DoFn<String, String>() {
      public void processElement(ProcessContext c) {
        String word = c.element();
        // In our DoFn, access the side input.
        int lengthCutOff = c.sideInput(maxWordLengthCutOffView);
        if (word.length() <= lengthCutOff) {
          c.output(word);
        }
  }}));
側面輸入和分窗:
一個可分窗的PCollection集合可能是無限大的,所以不能壓縮爲單值(或單集合類對象)。當你爲可分窗PCollection集合創建了一個PCollectionView,這個PCollectionView就代表了每個窗口的一個獨立實體(每個窗口一個單例實體,每個窗口一個列表實體,等等)。
Beam使用主輸入元素窗口爲側面輸入元素檢索合適的窗口。Beam把主輸入元素的窗口投影到側面輸入窗口配置,然後用結果窗口作爲側面輸入。如果主輸入和側面輸入有完全相同的窗口,那麼投射操作就提供了相對精確的窗口。但是,如果輸入有不同的窗口,Beam會用投影來選擇最合適的側面輸入窗口。
比如,如果主輸入用固定一分鐘時長分窗,側面輸入用固定一小時時長分窗,Beam投影主輸入窗口來代替側面輸入窗口的設置,然後從適當的一小時長側面輸入窗口獲取側面輸入數據。
如果主輸入元素有不止一個窗口,那麼processElement會被調用多次,每個窗口一次。每次調用processElement都會投影主輸入元素“當前”窗口,並且因此可能每次產生不同的側面輸入視圖。
如果側面輸入有多個觸發點,Beam使用最近的觸發點。如果你用的側面輸入只有一個全局窗口,並且指定了一個觸發器,那麼該操作會很有用。
側面輸出
一般ParDo會一直生成一個主輸出PCollection集合(即apply的返回值),不過你也可以讓ParDo生成任意數量的附帶輸出PCollection集合。如果你選擇輸出多個集合,ParDo會捆綁返回所有的輸出PCollection集合(包括主輸出)。
示例:
// To emit elements to a side output PCollection, create a TupleTag object to identify each collection that your ParDo produces.
// For example, if your ParDo produces three output PCollections (the main output and two side outputs), you must create three TupleTags.
// The following example code shows how to create TupleTags for a ParDo with a main output and two side outputs:


  // Input PCollection to our ParDo.
  PCollection<String> words = ...;


  // The ParDo will filter words whose length is below a cutoff and add them to
  // the main ouput PCollection<String>.
  // If a word is above the cutoff, the ParDo will add the word length to a side output
  // PCollection<Integer>.
  // If a word starts with the string "MARKER", the ParDo will add that word to a different
  // side output PCollection<String>.
  final int wordLengthCutOff = 10;


  // Create the TupleTags for the main and side outputs.
  // Main output.
  final TupleTag<String> wordsBelowCutOffTag =
      new TupleTag<String>(){};
  // Word lengths side output.
  final TupleTag<Integer> wordLengthsAboveCutOffTag =
      new TupleTag<Integer>(){};
  // "MARKER" words side output.
  final TupleTag<String> markedWordsTag =
      new TupleTag<String>(){};


// Passing Output Tags to ParDo:
// After you specify the TupleTags for each of your ParDo outputs, pass the tags to your ParDo by invoking .withOutputTags.
// You pass the tag for the main output first, and then the tags for any side outputs in a TupleTagList.
// Building on our previous example, we pass the three TupleTags (one for the main output and two for the side outputs) to our ParDo.
// Note that all of the outputs (including the main output PCollection) are bundled into the returned PCollectionTuple.


  PCollectionTuple results =
      words.apply(
          ParDo
          // Specify the tag for the main output, wordsBelowCutoffTag.
          .withOutputTags(wordsBelowCutOffTag,
          // Specify the tags for the two side outputs as a TupleTagList.
                          TupleTagList.of(wordLengthsAboveCutOffTag)
                                      .and(markedWordsTag))
          .of(new DoFn<String, String>() {
            // DoFn continues here.
            ...
          }
DoFn函數:
// Inside your ParDo's DoFn, you can emit an element to a side output by using the method ProcessContext.sideOutput.
// Pass the appropriate TupleTag for the target side output collection when you call ProcessContext.sideOutput.
// After your ParDo, extract the resulting main and side output PCollections from the returned PCollectionTuple.
// Based on the previous example, this shows the DoFn emitting to the main and side outputs.


  .of(new DoFn<String, String>() {
     public void processElement(ProcessContext c) {
       String word = c.element();
       if (word.length() <= wordLengthCutOff) {
         // Emit this short word to the main output.
         c.output(word);
       } else {
         // Emit this long word's length to a side output.
         c.sideOutput(wordLengthsAboveCutOffTag, word.length());
       }
       if (word.startsWith("MARKER")) {
         // Emit this word to a different side output.
         c.sideOutput(markedWordsTag, word);
       }
     }}));
5 Pipeline I/O
當你創建一個管道時,你通常需要從外部源讀取數據,比如外部數據槽中的文件或者一個數據庫。同樣的,你可能想讓你的管道輸出結果數據到類似的外部數據槽。Beam爲一些通用數據存儲類型提供了讀寫轉換(transform)操作。如果想讓管道讀寫內置轉換不支持的數據存儲格式,可以實現自制的讀寫轉換。
注意:關於如何實現自制Beam IO轉換的指南正在編寫中。。。
5.1 讀取輸入數據
讀取轉換操作從外部源讀取數據,然後管道使用的PCollection數據集合。在構造管道並創建一個新PCollection集合的過程中,隨時可以進行讀取轉換操作,雖然它通常是在管道啓動以後。
使用讀取轉換示例:
PCollection<String> lines = p.apply(TextIO.Read.from("gs://some/inputData.txt"));  
5.2 寫出輸出數據
寫出轉換操作把一個PCollection數據集合寫入一個外部數據源。通常是在管道結束時,用寫出操作輸出管道的最終結果。不過,你也可以在管道的任意時間執行寫出操作輸出一個PCollection集合的數據。
示例:
output.apply(TextIO.Write.to("gs://some/outputData"));
5.3 基於文件的輸入輸出數據
5.3.1 從多個位置讀取
很多讀取轉換操作都支持讀取多個輸入文件,文件名匹配某種佔位符。佔位符是由文件系統定義的,遵從文件系統定義的一致性模型。下面的TextIO實例使用了一種佔位操作(*)來讀取所有在指定位置匹配的輸入文件,文件名用“input-”開頭,“.csv”結尾:
p.apply(“ReadFromText”,
    TextIO.Read.from("protocol://my_bucket/path/to/input-*.csv");
爲了從不同數據源讀取數據到一個PCollection集合中,要求讀取過程獨立,然後用Flatten操作創建一個PCollection集合。
5.3.2 寫出到多個輸出文件
在基於文件輸出數據時,寫出轉換默認寫出到多個輸出文件。當你傳遞一個文件名給一個輸出操作時,文件名被用於所有寫出操作生成的輸出文件的前綴。你可以通過指定後綴爲每個輸出文件添加後綴。
下面的寫出操作示例寫出多個輸出文件到一個地址。每個文件前綴是“numbers”,一個數字的標識,後綴是“.csv”:
records.apply("WriteToText",
    TextIO.Write.to("protocol://my_bucket/path/to/numbers")
                .withSuffix(".csv"));
5.4 Beam提供的I/O API
具體參見API文檔。
File-based:AvroIO,HDFS,TextTIO,XML
Messaging:JMS,Kafka,Kinesis,Google Cloud PubSub
Database:MongoDB,JDBC,Google BigQuery,Google Cloud Bigtable, Google Cloud Datastore
6 運行管道
運行管道需要使用run方法。程序會發送管道的詳細說明到一個管道runner,後者會構造運行管道的一系列實際操作。管道默認是異步執行的:
pipeline.run();
如果想阻塞執行,可以加上waitUntilFinish方法:
pipeline.run().waitUntilFinish();
注意:本部分指南在持續更新中。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章