【Flink 精選】Kafka Consumer 源碼詳解

本文首先進行 Flink Kafka Consumer 原理分析,結合 SourceFunction 和 Kafka Client API 詳解源碼。


1.Flink Kafka Consumer 原理

本文基于 flink-1.11 分析 Kafka Consumer 原理。

FlinkKafkaConsumer 主要是繼承基類 RichParallelSourceFunction,不但可以執行 run(...) 方法讀取數據,而且擁有狀態、metric 和多并發等功能。

1.1 RichParallelSourceFunction 分析

RichParallelSourceFunction 與父類的繼承關系,如下圖所示。一方面,RichParallelSourceFunction 間接實現接口 SourceFunction,可以執行 run(...) 方法讀取數據;另一方面,RichParallelSourceFunction 間接實現接口 RichFunction,擁有狀態、metric 和多并發等功能。因此,RichParallelSourceFunction 是有狀態的和多并發的 Source 基類

ParallelSourceFunction 是接口 SourceFunction 的子類。共同點是 Source 的基類,需要實現 run() 讀取數據。不同點是前者提供多并發的能力,后者的并發度只能為 1;
AbstractRichFunction 是接口 RichFunction 的實現類,可以提供 open() 方法獲取 RuntimeContext,而 RuntimeContext 擁有 metric、subtasks 信息、accumulator、state 等功能;

RichParallelSourceFunction繼承圖.jpg

1.2 Flink Kafka Consumer 流程分析

如下圖所示,Flink Kafka Consumer 流程主要分為 ①主線程循環獲取緩存數據,發送到下游;②消費線程循環消費 Kafka 數據,保存到緩存

Handover.next:Handover 類的 next 屬性,即 ConsumerRecords 類型的緩存數據。Handover 的主要作用是協調主線程和消費線程,有序地消費 Kafka 和發送數據到下游算子

Flink Kafka Consumer流程圖.JPG

(1)主線程

主線程獲取緩存的 Handover.next 對象即 ConsumerRecords,發送到下游算子。首先創建 KafkaFetcher,同時內部創建消費線程 KafkaConsumerThread。然后,調用 KafkaFetcher.runFetchLoop() 方法,啟動消費線程、循環獲取緩存數據;最后,根據分區往下游發送數據

(2)消費線程

消費線程 KafkaConsumerThread 主要循環消費 Kafka 數據,保存到緩存。首先,主線程啟動消費線程。接著,KafkaConsumer 從 Kafka Broker 循環 poll 數據,同時保持到緩存中。

2.Flink Kafka Consumer 源碼詳解

問題1:如何使用 FlinkKafkaConsumer ?如何直接使用 KafkaClient API ?


/**
* 示例1:  Flink DataStream API 使用 FlinkKafkaConsumer 
**/
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//SimpleStringSchema為數據字段解析類
env.addSource(new FlinkKafkaConsumer<>("eventTopic", new SimpleStringSchema(), properties)


/**
* 示例2:  KafkaClient API 直接使用 KafkaConsumer 
**/
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
consumer.poll(Duration.ofMillis(100));

問題2:FlinkKafkaConsumer 內部是如何使用 KafkaClient API ?

① 初始化

執行 env.addSource 的時候會創建 StreamSource 算子對象;StreamSource 構造函數中將 function 即 FlinkKafkaConsumer 對象傳給父類 AbstractUdfStreamOperator 的 userFunction 變量;

StreamExecutionEnvironment源碼:

    public <OUT> DataStreamSource<OUT> addSource(SourceFunction<OUT> function, String sourceName, TypeInformation<OUT> typeInfo) {
        // 省略...
        // function 即 FlinkKafkaConsumer 
        final StreamSource<OUT, ?> sourceOperator = new StreamSource<>(function);
        // 省略...
    }

AbstractUdfStreamOperator源碼:

    // userFunction 即 FlinkKafkaConsumer 
    public AbstractUdfStreamOperator(F userFunction) {
        this.userFunction = requireNonNull(userFunction);
        checkUdfCheckpointingPreconditions();
    }

② Task 啟動和運行

Task 實現 Java多線程接口 Runnable。Task 啟動后,函數調用鏈如下 Task.run() -> Task.doRun() -> StreamTask.invoke() -> StreamTask.runMailboxLoop() -> MailboxProcessor.runMailboxLoop() -> MailboxProcessor.runMailboxStep() -> SourceStreamTask .processInput()。processInput() 方法里面啟動線程 sourceThread.start()。上述的關鍵源碼,如下所示。

StreamTask 源碼如下:

    @Override
    public final void invoke() throws Exception {
            // 省略...
            // 調用 MailboxProcessor.runMailboxLoop()
            runMailboxLoop();
            // 省略...
    }

MailboxProcessor 源碼如下:

    public void runMailboxLoop() throws Exception {
        // 省略...
        // 循環執行 runMailboxStep
        while (runMailboxStep(localMailbox, defaultActionContext)) {
        }
    }

    private boolean runMailboxStep(TaskMailbox localMailbox, MailboxController defaultActionContext) throws Exception {
        if (processMail(localMailbox)) {
            // 執行 mailboxDefaultAction.runDefaultAction,即執行 SourceStreamTask .processInput()
            mailboxDefaultAction.runDefaultAction(defaultActionContext); // lock is acquired inside default action as needed
            return true;
        }
        return false;
    }

SourceStreamTask 源碼如下:

    @Override
    protected void processInput(MailboxDefaultAction.Controller controller) throws Exception {
        // 由于目前沒有輸入,TaskMailbox 先暫停 loop 主線程
        controller.suspendDefaultAction();

        sourceThread.setTaskDescription(getName());
        sourceThread.start();
        // 省略...
    }

    private class LegacySourceFunctionThread extends Thread {
        // 省略...
        @Override
        public void run() {
            try {
                // 執行 source function 的 run() 方法
                mainOperator.run(lock, getStreamStatusMaintainer(), operatorChain);
                completionFuture.complete(null);
            } catch (Throwable t) {
                completionFuture.completeExceptionally(t);
            }
        }
        // 省略...
    }

③ 消費 Kafka

FlinkKafkaConsumerBase 間接實現了 SourceFunction 接口,主要實現 run() 方法。然后,在 run() 方法創建了一個 KafkaFetcher 對象,并主要調用 KafkaFetcher.runFetchLoop()。最終,運行消費線程 KafkaConsumerThread,并 while 循環地 poll Kafka 數據。上述的關鍵源碼,如下所示。

FlinkKafkaConsumerBase 源碼如下:

    @Override
    public void run(SourceContext<T> sourceContext) throws Exception {
        // 省略...
        // 創建 KafkaFetcher 對象 
        this.kafkaFetcher = createFetcher(
                sourceContext,
                subscribedPartitionsToStartOffsets,
                watermarkStrategy,
                (StreamingRuntimeContext) getRuntimeContext(),
                offsetCommitMode,
                getRuntimeContext().getMetricGroup().addGroup(KAFKA_CONSUMER_METRICS_GROUP),
                useMetrics);

        // 省略...
        // kafkaFetcher 執行 runFetchLoop(),即循環消費數據
        kafkaFetcher.runFetchLoop();
        // 省略...
    }

KafkaFetcher 源碼如下:

    @Override
    public void runFetchLoop() throws Exception {
        try {
            // 啟動消費線程 KafkaConsumerThread 
            consumerThread.start();

            while (running) {
                // 獲取協調者 Handover 的 next 緩存值 
                final ConsumerRecords<byte[], byte[]> records = handover.pollNext();

                // 從partition 獲取 數據
                for (KafkaTopicPartitionState<T, TopicPartition> partition : subscribedPartitionStates()) {

                    List<ConsumerRecord<byte[], byte[]>> partitionRecords =
                        records.records(partition.getKafkaPartitionHandle());
                    // 向下游發送數據

                    partitionConsumerRecordsHandler(partitionRecords, partition);
                }
            }
        }
        finally {
            consumerThread.shutdown();
        }

KafkaConsumerThread 源碼如下,run() 方法中創建 KafkaClient API 的 KafkaConsumer,并使用 KafkaConsumer.poll() 消費數據

@Override
    public void run() {
        // 省略...
        // 從主線程獲取的 handover 賦值給本地變量...
        final Handover handover = this.handover;
        // 省略...
        try {
            // 創建 KafkaConsumer
            this.consumer = getConsumer(kafkaProperties);
        }
        catch (Throwable t) {
            handover.reportError(t);
            return;
        }
            // 省略...
            ConsumerRecords<byte[], byte[]> records = null;
            // while 循環消費 Kafka
            while (running) {
                // 省略...
                if (records == null) {
                    try {
                        // KafkaConsumer poll 數據,即使用 KafkaClient API 的 KafkaConsumer 消費數據
                        records = consumer.poll(pollTimeout);
                    }
                    catch (WakeupException we) {
                        continue;
                    }
                }

                try {
                        // 把 Kafka 的數據保存在 Handover 的緩存中
                    handover.produce(records);
                    records = null;
                }
                // 省略...
            }
    }

問題3:Handover 是如何協調消費線程和主線程,使得前者可以及時消費和保存數據,而后者也可以及時獲取數據 ?

Handover 的關鍵方法是 produce() 保存緩存數據 nextpollNext() 獲取緩存數據 next,主要作用是在消費線程和主線程下,保證同一個緩存數據 next ,在同一時間內是不能既更新(寫),也輸出(讀),即保證原子性操作 next。

Handover 源碼如下:

    /**
    * consumer 線程把 Kafka 數據保存到 next 
    **/
    public void produce(final ConsumerRecords<byte[], byte[]> element)
            throws InterruptedException, WakeupException, ClosedException {

        checkNotNull(element);

        synchronized (lock) {
            // 循環判斷 next 是否為 null
            while (next != null && !wakeupProducer) {
                // lock 會釋放當前的鎖,該 consumer 線程進入 waiting 狀態
                lock.wait();
            }
            // 省略...
            else if (error == null) {
                // 寫 next
                next = element;
                // 喚醒 lock(使得處于 waiting 狀態的 main 線程能夠繼續執行)
                lock.notifyAll();
            }
            // 省略...
        }
    }

    /**
    * main 線程讀取 next 
    **/
    public ConsumerRecords<byte[], byte[]> pollNext() throws Exception {
        synchronized (lock) {
            // 循環判斷 next 是否為 null
            while (next == null && error == null) {
                // lock 會釋放當前的鎖,該 main 線程進入 waiting 狀態
                lock.wait();
            }
            // 讀取 next
            ConsumerRecords<byte[], byte[]> n = next;
            if (n != null) {
                next = null;
                // 喚醒 lock(使得處于 waiting 狀態的 consumer 線程能夠繼續執行)
                lock.notifyAll();
                return n;
            }
            // 省略...
        }
    }

Java 多線程的等待/通知機制:Object 的 wait()、notify/notifyAll()
① 當線程執行 wait() 方法的時候,會釋放當前的鎖,然后讓出CPU,進入等待狀態
② 當線程執行 notify/notifyAll() 方法的時候,會喚醒一個或多個正處于等待狀態的線程,然后繼續往下執行,直到執行完synchronized 代碼塊的代碼或是中途遇到 wait() ,再次釋放鎖。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380