Kafka基礎-流處理

1. 什麼是流處理?

首先,讓我們說一下什麼是數據流(也稱爲事件流)?它是無邊界數據集的抽象說法,無邊界意味着無限且不斷增長,因爲隨着時間的推移,新數據會不斷地到來。

除了無邊界的特性之外,事件流模型還有其它幾個屬性:

1.1 事件流是有序的

這在交易事件裏是最容易理解的,先在賬戶裏存錢然後消費與先消費再還錢是非常不同的,後者將產生透支費用,而前者不能透支。這是事件流和數據庫表之間的不同點之一:表中的記錄始終被視爲無序的(SQL的“order by”子句不是關係模型的一部分)。

1.2 事件流的數據是不可變的

事件一旦發生,就永遠無法修改。例如取消一個交易事務,這個記錄本身是不會被刪除的,相反,會向流寫入一個附加事件,記錄之前事務的取消。這是事件流和數據庫表之間的另一個不同點:我們可以刪除或更新表中的數據。

1.3 事件流是可重放的

對於大多數業務應用程序來說,能夠重放幾個月前(甚至幾年前)發生的原始事件流是至關重要的,這是爲了分析修正錯誤或執行審計所必須的。

2. 流處理的相關概念

流處理和任何類型的數據處理非常相似,都是接收數據,對數據執行某些操作,例如轉換、聚合等,然後把結果保存在某處。但是,有一些關鍵概念是流處理所特有的:

2.1 時間

時間可能是流處理中最重要的概念,因爲大多數流式應用程序都在一定的時間內執行操作。例如,計算連續5分鐘的股票價格平均值。流處理系統通常參考以下時間概念:

2.1.1 消息創建時間

這是事件被創建的時間。例如,進行測量的時間、出售商品的時間、用戶在網站瀏覽頁面的時間等等。在0.10.0及更高版本中,Kafka會在生產者創建消息時自動添加當前的時間。

2.1.2 消息保存時間

這是事件被保存在Kafka broker的時間。在0.10.0及更高版本中,Kafka broker可以配置自動爲接收到的消息添加當前的時間。

2.1.3 消息被處理時間

這是流處理應用程序接收事件的時間,這時間可以是事件發生的幾毫秒、幾小時或者幾天之後。

2.2 狀態

通常我們是把狀態保存在流處理應用程序本地的變量中,例如一個用於保存移動計數的簡單哈希表。然而,在流處理應用程序中,這不是一個管理狀態的可靠方法,因爲當流處理應用程序停止時,狀態將丟失。流處理有以下2種類型的狀態:

2.2.1 本地或內部狀態

只能由流處理應用程序的特定實例訪問的狀態,該狀態通常是使用應用程序內運行的內存數據庫來維護和管理。使用本地狀態的優點是性能非常快,缺點是受限於可用的內存大小。因此,流處理的許多設計模式都是把數據分到多個子流來處理。

2.2.2 外部狀態

使用外部數據存儲維護的狀態,例如是類似Cassandra的NoSQL數據庫。使用外部狀態的優點是幾乎沒有大小的限制,並且可以從應用程序的多個實例或甚至從不同的應用程序訪問它;缺點是由於引入了外部組件導致額外的延時和複雜性。

2.3 流-表二元性(Stream-Table Duality)

數據流包含從一開始到現在的完整歷史,它表示了過去和當前。它是一系列的事件,其中每個事件都會引起一個變化。而表包含的是某一時刻的狀態,是許多變化的結果。從某種意義上可以說流和表是一個硬幣的兩面,世界總是在變化,有時我們會對引起這些變化的事件感興趣,而有時我們卻對當前的狀態感興趣。我們將這兩者之間來回轉換的內在關係稱爲“流-表二元性”。

2.4 時間窗口

大多數流的操作都是在一定的時間內執行 - 移動的平均值、本週最暢銷商品等等。例如,當計算移動平均值時,我們需要知道:

  • 時間窗口的大小:我們需要計算每5分鐘的平均值?還是每15分鐘?還是每天?越大的時間窗口數據變化反應會越滯後,例如價格上漲需要更長時間才能注意到。
  • 時間窗口移動的頻率(間隔):五分鐘的平均值可以每分鐘,每秒,或每次有新事件時更新。當間隔等於時間窗口的大小時被稱爲翻滾窗口(tumbling window);當時間窗口在每條記錄上移動時被稱爲滑動窗口(sliding window)。
  • 時間窗口保持可更新的時間:例如我們五分鐘移動平均值是計算00:00-00:05時間窗口的平均值。現在一小時後,我們得到了更多的事件,但它們的事件時間顯示爲00:02。我們是否應該更新00:00-00:05期間的平均值?還是忽略它們?理想情況下,我們需要定義一個特定的時間段,在此期間接收到的事件將被添加到各自的時間窗口中。

時間窗口可以和時鐘對齊,例如每分鐘移動的一個5分鐘窗口,第一個時間段是00:00-00:05,第二個是00:01-00:06。或者可以簡單地從應用程序啓動時開始計時,例如03:17-03:22。滑動窗口是永遠不會和時鐘對齊,因爲只要有新記錄就會移動。有關這兩種時間窗口之間的區別,請參見下圖:

3. 流處理設計模式

3.1 單事件處理

流處理的最基本模式是單獨處理每個事件,這也稱爲map/filter模式,因爲它通常用於從流中過濾不必要的事件或轉換每個事件。術語“map”是基於map/reduce模式而來,在map階段轉換事件,reduce階段執行聚合。在此模式中,流處理應用程序從流中消費事件、修改每個事件,然後將事件寫入到另一個流中。例如,一個應用程序從流中讀取日誌消息並將ERROR事件寫入一個高優先級的流中,其餘事件寫入到一個低優先級的流中。另一個例子是把讀取到的事件格式由JSON轉換爲Avro。這種模式可以通過一個簡單的生產者和一個消費者處理,如下圖:

3.2 利用本地狀態處理

大多數流處理應用程序都用於聚合操作,特別是時間窗口的聚合。例如,查找出每天股票交易的最低和最高價格並計算其移動的平均值。此類聚合操作需要維護流的狀態,例如需要保存當前時刻最低和最高的價格並和流中的每個新價格比較,然後更新它們,所有這些都可以使用本地狀態來實現。我們使用Kafka的分區器來保證具有相同股票代碼的事件都會被寫入到相同的分區,然後,應用程序的每個消費者將從各自負責的分區讀取相應的事件,這意味着應用程序的每個消費者都可以維護相應分區的股票子集的狀態。如下圖所示:

當應用程序使用本地狀態時,流處理應用程序會變得非常複雜。以下幾個問題必須要注意:

  • 內存使用情況:本地狀態使用的內存大小必須小於應用程序實例可用的內存大小。
  • 持久化:當應用程序實例停止時,我們需要確保狀態不會丟失,並且當實例再次啓動或由其它實例替換時可以恢復之前的狀態。Kafka Streams在這方面處理地非常好-本地狀態會被保存在嵌入的RocksDB的內存中,而且還會持久化到磁盤以便在重啓後能夠快速恢復。但同時也會把本地狀態所有的改變都發送到Kafka的一個topic中。如果流的其中一個節點故障,本地狀態不會丟失,因爲可以容易地從Kafka的topic中重新讀取並創建它。
  • 負載再均衡:分區有時會被重新分配給不同的消費者。當發生這種情況時,丟失分區的消費者必須保存最後的狀態,而重新分配到的消費者必須知道恢復正確的狀態。

3.3 多階段處理/重新分區

如果你需要根據所有事件來計算結果,例如統計每天交易前十的股票,顯然使用本地狀態不足以實現,因爲所有前十的股票可能位於不同的分區中。我們需要兩個階段來實現:首先分別對每個股票代碼計算每日收益/損失,我們可以在每個實例中使用本地狀態來實現。然後把結果都寫到一個只有單分區的新topic裏,使用1個消費者來計算前十的股票,如下圖所示。但有時候也需要更多的階段來處理結果。

3.4 連接外部系統處理:流-表連接(Stream-Table Join)

有時候流處理需要與外部系統整合,例如根據保存在數據庫的一組規則來驗證事務的合法性,或者使用用戶畫像來豐富用戶的點擊事件,如下圖所示:

但這種模式的問題是與外部系統額外的交互增加了明顯的延時,通常在5-15毫秒之間。在許多情況下,這是不可行的。流處理系統通常每秒可處理100K-500K個事件,但數據庫一般每秒只能處理10K個事件。爲了提高性能,我們需要在流處理應用中緩存數據庫的信息。但是管理這些緩存是極具挑戰性的,例如怎樣防止緩存中的信息變得陳舊?我們可以使用事件流來捕捉數據庫表所有的更改,然後實時更新緩存。捕捉數據庫更改輸出到一個事件流稱爲CDC(change data capture),Kafka Connect工具有多個connectors可以實現CDC把數據庫錶轉換爲事件流。這允許你保存多一份數據庫表的副本,並且只要有更改事件就會收到通知,以便相應地更新副本的數據,如下圖所示:

我們稱這爲流-表連接是因爲其中的一個流表示本地緩存表的更改。

3.5 流連接(Streaming Join)

有時候你想連接兩個事件流而不是一個流和一個表。當使用一個流來表示一個表,你可以忽略流中的大多數歷史記錄,因爲你只關心當前的記錄。但是當連接兩個流時,你連接的可以是所有記錄,例如嘗試將一個流中的事件與另一個流中具有相同key併發生在同一時間窗口的事件進行匹配。這就是爲什麼流連接也被稱爲窗口連接(windowed-join)的原因。

例如,假設有一個保存用戶查詢行爲的流和一個對查詢結果點擊行爲的流。我們想把查詢和點擊結果相匹配,以便知道哪個查詢結果是最多人點擊的。顯然,我們可以根據查詢條件來匹配查詢結果,但只限定在特定的時間窗口內。我們假設查詢結果的點擊是在用戶輸入查詢條件後幾秒,所以我們在每個流上選取一個幾秒鐘的移動窗口,並匹配每個窗口的結果,如下圖所示:

查詢和點擊的流都是基於相同keys來分區的,這些keys同時也是連接keys。這樣,來自user_id:42的所有點擊事件都會在點擊topic的分區5中,而user_id:42的所有查詢事件都會在查詢topic的分區5中。然後Kafka Streams確保這兩個topics的分區5都會分配給同一個任務,因此,該任務可以讀取user_id:42的所有相關事件。它是使用其嵌入的RocksDB緩存來實現這兩個topics的窗口連接。

3.6 亂序事件(Out-of-Sequence Events)

處理在錯誤時間到達流的事件不僅是流處理的挑戰,而且也是傳統ETL系統的挑戰。在IoT(物聯網)場景中,無序事件經常發生並且預期會發生。例如,一個移動設備丟失WiFi信號幾個小時,然後在重新連接時發送前幾個小時的事件。流應用程序需要能夠處理這些場景,這通常意味着應用程序必須執行以下操作:

  • 識別亂序事件,這需要應用程序檢查事件的時間並與當前時間比較。
  • 定義一個時間段,在此期間將嘗試排解亂序的事件,例如一個延時三小時的事件可以重新被處理,但超過此時間的將會被丟棄。
  • 具有排解亂序事件的功能,這是流應用和批處理任務的主要區別。如果有一個批處理任務,並且在任務完成後還接收到一些事件,我們通常可以重新執行之前的任務。但一個流處理卻不能這樣做,因爲它是一個持續的處理,在任何時刻都需要處理新舊的事件。
  • 能夠更新結果,例如把流處理的結果寫入數據庫。

包括Google的Dataflow和Kafka Streams在內的多個流處理框架內置了對事件時間概念的支持,能夠處理事件時間比當前處理時間舊或新的事件。這通常通過在本地狀態下維護多個可用於更新的聚合窗口來完成,而且開發人員能夠配置保持這些窗口的時間長短。當然,保持聚合窗口可用於更新的時間越長,維護本地狀態所需的內存就越多。

Kafka Streams的API始終把聚合結果寫入到一個結果topic中。那些通常是精煉的topics,這意味着只保存每個key對應的最新值。如果由於一個亂序事件需要更新聚合結果,Kafka Streams將簡單地爲相應的聚合窗口寫入一個新的結果(覆蓋之前的)。

3.7 重新處理

這個模式一般有兩種用例:

  • 我們有一個優化版本的流處理應用程序。我們希望在與舊版本相同的事件流上運行新版本的應用程序,生成新的事件流結果,但不會替換舊版本的結果,而是比較它們,客戶端會使用新的結果。
  • 現有的流處理應用程序有bugs。我們修復了bugs之後希望重新處理事件流並重新計算結果。

第一個用例很容易實現,因爲Kafka會長時間把整個事件流存儲在一個可伸縮存儲中。這意味着擁有分別生成兩個結果流的兩個版本的流處理應用程序只需要以下步驟:

  • 將新版本的應用程序作爲一個新的消費者組來使用。
  • 配置新版本的應用程序從topic的第一個offset開始處理所有事件。
  • 當新版本的應用程序處理的進度趕上時,讓它繼續處理並將客戶端切換到新的結果流。

第二個用例的實現具有挑戰性,它需要“重置”現在應用程序從頭開始重新處理,重置本地狀態(因此我們不會混淆兩個版本的結果),以及很可能清除之前的輸出流。雖然Kafka Streams有一個重置流處理應用程序狀態的工具,但建議只要有足夠的容量來運行兩個版本的應用程序並生成兩個結果流,就嘗試使用第一種方法。因爲第一種方法更安全,它允許在多個版本之間來回切換和比較結果,並且在清理過程中不會有丟失數據或引起錯誤的風險。

4. Kafka Streams例子

Kafka有兩類流APIs,low-level Processor API和high-level Streams DSL。後續的例子將會使用後者,DSL允許你在流處理應用程序中定義一系列的轉換。轉換可以是簡單的filter或者是複雜的流連接。low-level的API允許你創建自己的轉換,但這通常比較少見。使用DSL API的應用程序始終以StreamBuilder來創建處理的拓撲開始-應用於流中事件轉換的有向無環圖(DAG,Directed Acyclic Graph)。然後從拓撲中創建KafkaStreams的執行對象,啓動KafkaStreams對象將啓動多個線程,每個線程會把處理的拓撲應用到流中的事件。當關閉KafkaStreams對象時,處理會結束。

4.1 Word Count

創建流處理應用程序首先要做的是配置Kafka Streams,它有很多配置屬性,但這裏不做詳細介紹。另外,你也可以通過向Properties對象添加生產者或消費者配置來配置嵌套在Kafka Streams中的生產者和消費者:

public class WordCountExample {

    public static void main(String[] args) {
        Properties props = new Properties();
        // 必須要有一個唯一的ID
        props.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application");
        props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        // 使用默認的序列化和反序列化類
        props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName());
        props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName());

創建處理的拓撲:

StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> source = builder.stream("TextLinesTopic");
KStream<String, Long> counts = source
    // 轉換爲多個word
    .flatMapValues(textLines -> Arrays.asList(textLines.toLowerCase().split("\\W+")))
    // 轉換爲word-word鍵值對
    .map((key, word) -> new KeyValue<>(word, word))
    // 過濾the
    .filter((key, word) -> !word.equals("the"))
    // 按相同word分組
    .groupByKey()
    // 計算每個word的數量
    .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"))
    .toStream();
// 把計算結果寫到另外一個topic中,這裏指定序列化和反序列化類
counts.to("WordsWithCountsTopic", Produced.<String, Long>with(Serdes.String(), Serdes.Long()));

啓動流應用程序:

KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();

4.2 股市統計

本例會讀取一系列股票市場的交易事件,包括股票代碼,要價和要價數量。爲了簡單起見,本例忽略出價,也不會在數據中包含timestamp,而會依賴於Kafka生產者生成的事件時間。然後,我們將創建以下一些統計信息的輸出流,所有統計數據將每秒更新一次。

  • 每五秒鐘的最佳(最低)要價
  • 每五秒鐘的交易數量
  • 每五秒鐘的平均要價

配置Kafka Streams和上例類似:

Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "stockstat-application");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, TradeSerde.class.getName());

不同的是這裏使用了自定義的反序列化類TradeSerde,它使用Google的Gson庫來生成JSON的序列化與反序列化器,封裝在WrapperSerde類裏:

static public final class TradeSerde extends WrapperSerde<Trade> {
    public TradeSerde() {
        super(new JsonSerializer<Trade>(), new JsonDeserializer<Trade>(Trade.class));
    }
}

創建TradeStats類用於統計計算:

public class TradeStats {

    String type;
    String ticker;
    // tracking count and sum so we can later calculate avg price
    int countTrades;
    double sumPrice;
    double minPrice;
    double avgPrice;

    public TradeStats add(Trade trade) {

        if (trade.type == null || trade.ticker == null)
            throw new IllegalArgumentException("Invalid trade to aggregate: " + trade.toString());

        if (this.type == null)
            this.type = trade.type;
        if (this.ticker == null)
            this.ticker = trade.ticker;

        if (!this.type.equals(trade.type) || !this.ticker.equals(trade.ticker))
            throw new IllegalArgumentException("Aggregating stats for trade type " + this.type + " and ticker "
                    + this.ticker + " but recieved trade of type " + trade.type + " and ticker " + trade.ticker);

        if (countTrades == 0)
            this.minPrice = trade.price;

        this.countTrades = this.countTrades + 1;
        this.sumPrice = this.sumPrice + trade.price;
        this.minPrice = this.minPrice < trade.price ? this.minPrice : trade.price;

        return this;
    }

    public TradeStats computeAvgPrice() {
        this.avgPrice = this.sumPrice / this.countTrades;
        return this;
    }

}

然後就可以創建處理的拓撲:

StreamsBuilder builder = new StreamsBuilder();
KStream<String, Trade> source = builder.stream("stocks");
KStream<Windowed<String>, TradeStats> stats = source
    // 按股票代碼分組
    .groupByKey()
    // 創建5秒的時間窗口,每1秒移動一次
    .windowedBy(TimeWindows.of(5000).advanceBy(1000))
    // 對時間窗口內的Trade對象執行聚合,這裏是簡單的add到TradeStats對象裏進行統計數量、最低價
    .<TradeStats>aggregate(() -> new TradeStats(), (k, v, tradestats) -> tradestats.add(v),
        Materialized.<String, TradeStats, WindowStore<Bytes, byte[]>>as("trade-aggregates")
            .withValueSerde(new TradeStatsSerde()))
    .toStream()
    // 計算平均價格並返回包含統計信息的TradeStats對象
    .mapValues((trade) -> trade.computeAvgPrice());
// 寫到另外一個topic中
stats.to("stockstats-output", Produced.keySerde(WindowedSerdes.timeWindowedSerdeFrom(String.class)));

最後啓動流應用程序:

KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();

完整代碼可以參見:https://github.com/gwenshap/kafka-streams-stockstats

4.3 豐富網站的點擊事件

本例會展示如何通過流連接來豐富網站的點擊事件。我們將生成一個模擬用戶點擊的流、一個用於更新用戶檔案數據庫的流和網頁搜索的流。然後,我們將連接這三個流來獲得每個用戶行爲的360度視圖。例如,用戶搜索了什麼?點擊了什麼搜索結果?是否在用戶檔案中更新了“感興趣的事物”?通過連接這些類型可以爲分析提供豐富的數據,產品的推薦通常是基於這些信息。例如,用戶搜索自行車,點擊品牌“Trek”的搜索結果鏈接,並對旅行感興趣,因此我們可以將Trek、頭盔和適合自行車旅行相關的廣告推送給該用戶。

以下是創建處理的拓撲:

StreamsBuilder builder = new StreamsBuilder();
// 分別創建二個流,一個是點擊流,另外一個是搜索流
KStream<Integer, PageView> views = builder.stream("clicks.pages.views",
    Consumed.with(Serdes.Integer(), new PageViewSerde()));
KStream<Integer, Search> searches = builder.stream("clicks.search",
    Consumed.with(Serdes.Integer(), new SearchSerde()));
// 創建緩存的用戶檔案表(這個是通過一個流來更新)
KTable<Integer, UserProfile> profiles = builder.table("clicks.user.profile",
    Materialized.<Integer, UserProfile, KeyValueStore<Bytes, byte[]>>as("profile-store"));

// 先連接點擊流和用戶檔案
KStream<Integer, UserActivity> viewsWithProfile = views.leftJoin(profiles, (view, profile) -> {
    if (profile != null)
        return new UserActivity(profile.getUserID(), profile.getUserName(), profile.getZipcode(),
            profile.getInterests(), "", view.getPage());
    else
        return new UserActivity(-1, "", "", null, "", view.getPage());
});

// 用上面連接的結果再連接最後一個流
KStream<Integer, UserActivity> userActivityKStream = viewsWithProfile.leftJoin(searches,
    (userActivity, search) -> {
        if (search != null)
            return userActivity.updateSearch(search.getSearchTerms());
        else
            return userActivity.updateSearch("");
    // 僅僅關聯點擊搜索後1秒的相關點擊事件
    }, JoinWindows.of(1000), Joined.with(Serdes.Integer(), new UserActivitySerde(), new SearchSerde()));

完整代碼可以參見:https://github.com/gwenshap/kafka-clickstream-enrich
    
5. Kafka Streams架構概述

上面的例子講述瞭如何使用Kafka Streams API實現一些衆所周知的流處理設計模式。但是爲了更好地理解Kafka Streams實際是怎樣工作和擴展,我們需要深入瞭解API背後的一些設計原則。

5.1 創建拓撲

每個流應用程序都實現並執行至少一個拓撲。拓撲(在其它流處理框架中也稱爲有向無環圖DAG,Directed Acyclic Graph)是一系列的操作和轉換,每個事件從輸入流動到輸出。下圖是上面Word Count例子的拓撲圖:

即使是簡單的處理流程也有拓撲圖,它是由多個處理器組成,也就是拓撲圖中的橢圓形節點。大多數處理器實現數據過濾、映射、聚合等操作。還有源處理器,它從一個topic讀取數據並將其傳遞,和接收處理器,它從之前的處理器讀取數據併發到另外一個topic。拓撲圖始終從一個或多個源處理器開始,並以一個或多個接收處理器結束。

5.2 擴展拓撲

Kafka Streams通過在一個應用的實例中允許運行多個線程和在應用的分佈實例之間支持負載均衡來進行擴展。Streams引擎通過切分任務來並行化一個拓撲的執行,任務的數量由Streams引擎決定,並取決於應用程序處理的topics的分區數。每個任務負責一部分分區,對每個讀取的事件,任務將在最終將結果寫入接收處理器之前按順序執行適用於該分區的所有處理步驟。那些任務是Kafka Streams並行處理的基本單元,因爲每個任務都可以獨立於其它任務執行。如下圖所示:

應用程序的開發人員可以選擇每個應用程序實例將執行的線程數。如果有多個線程可用,每個線程會執行應用程序創建任務的子集。如果應用程序的多個實例運行在多個服務器,則每個服務器的每個線程會執行不同的任務。如果想提高處理性能,就啓動更多的線程;如果服務器資源不足,就增加服務器,這就是流應用程序擴展的方式。Kafka會自動協調工作,它將爲每個任務分配相應的分區,並且每個任務都是獨立處理事件和維護自己的本地狀態。如下圖所示:

有時候處理步驟可能需要來自多個分區的結果,這會在任務之間創建依賴關係。例如,如果我們連接兩個流,就像上述點擊事件的例子,我們需要每個流中的數據才能得出結果。Kafka Streams通過分配所有需要的分區給同一個任務,以便該任務可以讀取所有相關分區的事件並執行連接操作。這就是爲什麼Kafka Streams目前要求參與連接操作的所有topics具有相同數量的分區並根據連接key進行分區。

任務之間依賴關係的另一個例子是當我們的應用程序需要重新分區時。例如在上述點擊事件的例子中,所有事件都是使用用戶ID作爲key值,但如果我們想按每個頁面或者按zip code生成統計信息,我們需要按頁面或者zip code重新分區然後執行聚合。如果任務1處理分區1的數據,然後到達需要重新分區的處理器(groupBy操作),則需要重新移動數據。與其它流處理器框架不同的是,Kafka Streams通過使用新的keys和分區把事件寫入一個新的topic。然後另一組的任務從這個新的topic讀取事件並繼續處理,這個重新分區的步驟把原來的拓撲結構分爲兩個有各自任務的子拓撲。第二組的任務依賴於第一組,因爲它是處理來自第一個子拓撲的結果。但是,第一組和第二組的任務仍然可以獨立和並行地運行,因爲第一組的任務以它自己的速率把數據寫入topic,而第二組的任務只是從這個topic讀取事件並處理,它們之間沒有通訊也沒有共享資源,不需要運行在相同的線程或服務器上。這是Kafka其中的一個優點,也就是減少管道不同部分的依賴關係。如下圖所示:

5.3 處理故障

Kafka是高可用的,因此我們保存在Kafka的數據也是高可用的。如果應用程序故障並需要重啓,可以從Kafka的流中查找其故障前提交的最後offset並繼續處理。但請注意,如果是本地狀態存儲丟失,例如需要替換服務器,流應用程序總是可以從存儲在Kafka的更改日誌重新創建本地狀態。

Kafka Streams還利用其消費者協調器來爲任務提供高可用性。如果一個任務失敗但流應用程序的線程或其它實例還處於活動狀態,其中一個可用的線程會重新啓動該任務。這與消費者組通過把分區分配給剩餘消費者之一來處理組內其中一個消費者的故障類似。

END O(∩_∩)O

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