Kafka攔截器和Streams

1.1攔截器原理

Producer攔截器(interceptor)是在Kafka 0.10版本被引入的,主要用於實現clients端的定製化控制邏輯。對於producer而言,interceptor使得用戶在消息發送前以及producer回調邏輯前有機會對消息做一些定製化需求,比如修改消息等。同時,producer允許用戶指定多個interceptor按序作用於同一條消息從而形成一個攔截鏈(interceptor chain)。Intercetpor的實現接口是org.apache.kafka.clients.producer.ProducerInterceptor,其定義的方法包括

1、configure(configs)

獲取配置信息和初始化數據時調用。

2、onSend(ProducerRecord)

該方法封裝進KafkaProducer.send方法中,即它運行在用戶主線程中。Producer確保在消息被序列化以及計算分區前調用該方法。用戶可以在該方法中對消息做任何操作,但最好保證不要修改消息所屬的topic和分區,否則會影響目標分區的計算

3、onAcknowledgement(RecordMetadata, Exception)

該方法會在消息被應答或消息發送失敗時調用,並且通常都是在producer回調邏輯觸發之前。onAcknowledgement運行在producer的IO線程中,因此不要在該方法中放入很重的邏輯,否則會拖慢producer的消息發送效率

4、close

關閉interceptor,主要用於執行一些資源清理工作,如前所述,interceptor可能被運行在多個線程中,因此在具體實現時用戶需要自行確保線程安全。另外倘若指定了多個interceptor,則producer將按照指定順序調用它們,並僅僅是捕獲每個interceptor可能拋出的異常記錄到錯誤日誌中而非在向上傳遞。這在使用過程中要特別留意。

1.2攔截器案例

實現一個簡單的雙 interceptor 組成的攔截鏈。第一個 interceptor 會在消息發送前將時間戳信息加到消息 value 的最前部;第二個 interceptor 會在消息發送後更新成功發送消息數或失敗發送消息數。

時間攔截器:

import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
/**
 * 在記錄前面加上時間戳
 */
public class TimerInterceptor implements ProducerInterceptor {
    /**
     * 該方法封裝進 KafkaProducer.send 方法中,即它運行在用戶主線程中。Producer 確保在
     * 消息被序列化以及計算分區前調用該方法
     * 用戶可以在該方法中對消息做任何操作,但最好保證不要修改消息所屬的 topic 和分區,否則會影響目標分區的計算
     *
     * @param record
     * @return
     */
    public ProducerRecord onSend(ProducerRecord record) {
        // 創建一個新的record,將時間戳寫入記錄的最前面,一個record就是一個記錄
        return new ProducerRecord(
                record.topic(),
                record.partition(),
                record.timestamp(),
                record.key(),
                System.currentTimeMillis() + "---  " + record.value().toString());
    }
    /**
     * 該方法會在消息被應答或消息發送失敗時調用,並且通常都是在 producer 回調邏輯觸
     * 發之前。onAcknowledgement 運行在 producer 的 IO 線程中,因此不要在該方法中放入很重的邏輯,
     * 否則會拖慢 producer 的消息發送效率
     *
     * @param metadata
     * @param exception
     */
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
    }
    /**
     * 關閉 interceptor,主要用於執行一些資源清理工作
     */
    public void close() {
    }

    /**
     * 獲取配置信息和初始化數據時調用
     *
     * @param configs
     */
    public void configure(Map<String, ?> configs) {
    }
}

計數攔截器:

import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
/**
 * 統計發送消息成功和發送失敗消息數,並在 producer 關閉時打印這兩個計數器
 */
public class CounterInterceptor implements ProducerInterceptor {

    private int successCount = 0;
    private int errorCount = 0;

    public ProducerRecord onSend(ProducerRecord record) {
        return null;
    }

    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        // 統計失敗和成功的次數
        if (exception == null) {
            successCount++;
        } else {
            errorCount++;
        }
    }
    /**
     * 在關閉的時候保留結果
     */
    public void close() {
        System.out.println("success sent: " + successCount);
        System.out.println("error sent: " + errorCount);
    }
    public void configure(Map<String, ?> configs) {
    }
}

Producer程序:

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.ArrayList;
import java.util.Properties;

/**
 * 主程序
 * 需求:實現一個簡單的雙 interceptor 組成的攔截鏈。
 * 第一個 interceptor 會在消息發送前將時間戳信息加到消息 value 的最前部;
 * 第二個 interceptor 會在消息發送後更新成功發送消息數
 */
public class ProduceInterceptor {
    public static void main(String[] args) {
        // 1.設置配置信息
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");
        props.put("acks", "all");
        props.put("retries", 0);
        props.put("batch.size", 16384);
        props.put("linger.ms", 1);
        props.put("buffer.memory", 33554432);
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        // 2.構建攔截器
        ArrayList<String> interceptors = new ArrayList<String>();
        interceptors.add("com.myStudy.interceptor.TimerInterceptor");
        interceptors.add("com.myStudy.interceptor.CounterInterceptor");

        String topic = "first";

        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors); 

        // 3.發送消息
        for (int i = 0; i < 10; i++) {
            ProducerRecord<String, String> record = new ProducerRecord<String, String>(topic, "messsage" + i);
            producer.send(record);
        }
        // 4. 一定要關閉producer,這樣纔會調用interceptor的close方法
        producer.close();
    }
}

1.3瞭解Kafka Streams

Kafka Streams。Apache Kafka開源項目的一個組成部分。是一個功能強大,易於使用的庫。用於在Kafka上構建高可分佈式、拓展性,容錯的應用程序。它建立在流處理的一系列重要功能基礎之上,比如正確區分事件事件和處理時間,處理遲到數據以及高效的應用程序狀態管理。
下面的列表強調了Kafka Streams的幾個關鍵功能,使得Kafka Streams成爲構建流處理應用程序、持續查詢、轉換和微服務等場景的新選擇。

  • 功能強大
    高拓展性,彈性,容錯
    有狀態和無狀態處理
    基於事件時間的Window,Join,Aggergations
  • 輕量級
    無需專門的集羣
    沒有外部以來
    一個庫,而不是框架
  • 完全集成
    100%的Kafka 0.10.0版本兼容
    易於集成到現有的應用程序
    程序部署無需手工處理(這個指的應該是Kafka多分區機制對Kafka Streams多實例的自動匹配)
  • 實時性
    毫秒級延遲
    並非微批處理
    窗口允許亂序數據
    允許遲到數據

近看Kafka Streams

在我們深入Kafka Streams的概念和架構細節以及按部就班認識Kafka Streams之前,我們先來對上面提出的列表做更多的介紹。

  • 更簡單的流處理:Kafka Streams的設計目標爲一個輕量級的庫,就像Kafka的Producer和Consumer似得。可以輕鬆將Kafka Streams整合到自己的應用程序中。對應用程序的額外要求僅僅是打包和部署到應用程序所在集羣罷了。
  • 除了Apache Kafka之外沒有任何其它外部依賴, 並且可以在任何Java應用程序中使用。不需要爲流處理需求額外部署一個其它集羣。操作和維護團隊肯定會很高興這一點。
  • 使用Kafka作爲內部消息通訊存儲介質,而不是像其它流處理框架似得,重新加入其它外部組件來做消息通訊。Kafka Streams使用Kafka的分區水平拓展來對數據做有序高效的處理。這樣同時兼顧了高性能,高擴展性,並使操作簡便。這種決策的好處是,你不必瞭解和調整兩個不同的消息傳輸層(數據在不同伸縮介質中間移動和流處理的獨立消息處理層),同樣,Kafka的性能和高可靠性方面的改進,都會使得Kafka Streams直接受益。也可以同時藉助Kafka社區強大的開發能力。
  • 允許和其他資源管理和配置共聚焦集成。因此,Kafka Streams能夠更加無縫的集成到現有的開發、打包、部署和業務實踐當中去。你可以自由地使用自己喜歡的工具,比如java 應用服務器,Puppet, Ansible,Mesos,Yarn,Docket, 甚至在一臺手工運行你自己應用程序進行驗證的機器上。
  • 支持本地狀態容錯。這樣就可以進行非常高效快速的包含狀態的Join和Window 聚合操作。本地狀態被保存在Kafka中,在機器故障的時候,其他機器可以自動恢復這些狀態繼續處理。
  • 每次處理一條數據以實現低延時,這對於欺詐監測等場景是至關重要的。這也是Kafka Streams和其他基於微批處理的流處理框架的不同。

此外,Kafka Streams在設計上基於豐富的開發經驗,具有很強的實用性。它提供了流處理所有的必要的原語,允許應用程序從Kafka中讀取流數據,處理數據並且將結果寫回Kafka或者發送到其他外部系統中取。提供了高層次的比如Filter,Map,Join等DSL操作以及低級別API供開發者選擇使用。
最後,Kafka Streams爲拓展開發者提供幫助,它入門門檻低,開發路徑平滑,你可以快速編寫和運行一個小規模的應用程序進行驗證,因爲你完全不需要安裝或者瞭解其他分佈式流處理平臺。並且只需要將應用程序部署在多個實例上就可以在大批量的生產工作中實現負載均衡。Kafka Streams透明地使用Kafka並行操作模型處理同一應用程序的多個實例來實現負載均衡。
綜上所述,Kafka Streams是構建流處理應用中的一個引人注目的選擇,請給它一個試用的機會,並運行你的第一個Hello World流處理程序。文檔的下一章將帶你開始由淺入深編寫Kafka Streams應用程序。

1.4 Kafka爲什麼那麼快?

Broker

不同於Redis和MemcacheQ等內存消息隊列,Kafka的設計是把所有的Message都要寫入速度低容量大的硬盤,以此來換取更強的存儲能力。實際上,Kafka使用硬盤並沒有帶來過多的性能損失,“規規矩矩”的抄了一條“近道”。

首先,說“規規矩矩”是因爲Kafka在磁盤上只做Sequence I/O,由於消息系統讀寫的特殊性,這並不存在什麼問題。關於磁盤I/O的性能,引用一組Kafka官方給出的測試數據(Raid-5,7200rpm):

Sequence I/O: 600MB/s

Random I/O: 100KB/s

所以通過只做Sequence I/O的限制,規避了磁盤訪問速度低下對性能可能造成的影響。

接下來我們再聊一聊Kafka是如何“抄近道的”。

首先,Kafka重度依賴底層操作系統提供的PageCache功能。當上層有寫操作時,操作系統只是將數據寫入PageCache,同時標記Page屬性爲Dirty。

當讀操作發生時,先從PageCache中查找,如果發生缺頁才進行磁盤調度,最終返回需要的數據。實際上PageCache是把儘可能多的空閒內存都當做了磁盤緩存來使用。同時如果有其他進程申請內存,回收PageCache的代價又很小,所以現代的OS都支持PageCache。

使用PageCache功能同時可以避免在JVM內部緩存數據,JVM爲我們提供了強大的GC能力,同時也引入了一些問題不適用與Kafka的設計。

如果在Heap內管理緩存,JVM的GC線程會頻繁掃描Heap空間,帶來不必要的開銷。如果Heap過大,執行一次Full GC對系統的可用性來說將是極大的挑戰。

所有在在JVM內的對象都不免帶有一個Object Overhead(千萬不可小視),內存的有效空間利用率會因此降低。

所有的In-Process Cache在OS中都有一份同樣的PageCache。所以通過將緩存只放在PageCache,可以至少讓可用緩存空間翻倍。

如果Kafka重啓,所有的In-Process Cache都會失效,而OS管理的PageCache依然可以繼續使用。

PageCache還只是第一步,Kafka爲了進一步的優化性能還採用了Sendfile技術。在解釋Sendfile之前,首先介紹一下傳統的網絡I/O操作流程,大體上分爲以下4步。

OS 從硬盤把數據讀到內核區的PageCache。

用戶進程把數據從內核區Copy到用戶區。

然後用戶進程再把數據寫入到Socket,數據流入內核區的Socket Buffer上。

OS 再把數據從Buffer中Copy到網卡的Buffer上,這樣完成一次發送。

在這裏插入圖片描述

整個過程共經歷兩次Context Switch,四次System Call。同一份數據在內核Buffer與用戶Buffer之間重複拷貝,效率低下。其中2、3兩步沒有必要,完全可以直接在內核區完成數據拷貝。這也正是Sendfile所解決的問題,經過Sendfile優化後,整個I/O過程就變成了下面這個樣子。

在這裏插入圖片描述

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