Flink kafka 定製技巧

動態路由:
方案1: 定製一個特殊的KafkaDynamicSink,內嵌多個原生的FlinkKafkaProducer,每個對應一個下游的KAFKA隊列
在OPEN方法中讀取所有KAFKA渠道配置並構建FlinkKafkaProducer並構建一個Map: kafka channelId -> FlinkKafkaProducer

重載INVOKE方法
根據路由規則找到當前流數據對應所有的ChannelId (允許多個),再從MAP中獲取對 FlinkKafkaProducer 並調用其INVOKE方法

核心代碼:
public class DynamicKafkaSink<IN> extends RichSinkFunction<IN> {
    @Override
    public void open(Configuration parameters) throws Exception {
        List<ChannelModel> allChannels = channelRepository.getAll();
        for(ChannelModel nextChannel: allChannels) {
            FlinkKafkaProducer010 nextProducer = (FlinkKafkaProducer010<String>)channelFactory.createChannelProducer(nextChannel,
            FlinkKafkaProducer010.class, Collections.emptyMap());
            nextProducer.setRuntimeContext(this.getRuntimeContext());
            nextProducer.open(parameters);
            producers.put(nextChannel.getChannelId(), nextProducer);
        }
    }
    
    @Override
    public void invoke(IN value) throws Exception {
        List<String> channelIds = channelRouteStrategy.route(value);
        for (String nextChannelId: channelIds) {
            FlinkKafkaProducer010 nextProducer = producers.get(nextChannelId);
            nextProducer.invoke(converted);
        }
    }

}




注意:
Map不能在構造函數中初始化,而要在OPEN方法中初始化,FLINK分佈式特性決定了構造函數和OPEN不在同一個JVM裏執行
類級別的變量需要可序列化,否則需要聲明爲TRANSIENT

每個新構建的FlinkKafkaProducer需要先調用
setRuntimeContext(this.getRuntimeContext())
再調用open 方法才能被使用


優點:
可以路由到不同的BROKER上的TOPIC,在不同的BROKER上隔離性更好

缺陷:
所有的FlinkKafkaProducer只在OPEN的時候創建一次,後面如果添加了新的KAFKA隊列無法被動態感知並路由
更改了FlinkKafkaProducer創建和初始化的過程,從MAIN函數中轉到了KafkaDynamicSink的OPEN方法裏,未經過全面測試,可能存在問題


方案2:方案1的升級版,利用FLINK SPLIT STREAM的特性,根據路由規則將原生數據流分成多個,每個子數據流對應一個下游KAFKA隊列
在FLINK Main 函數中讀取所有KAFKA渠道配置並構建FlinkKafkaProducer並構建一個Map: kafka channelId -> FlinkKafkaProducer
在輸入流上構建一個SplitStream, OutputSelector 中根據路由邏輯返回一組ChannelId
遍歷Map,對於Map中的每個Key (ChannelID) 調用 SplitStream 的 select方法獲取對應的分支流數據,然後路由到對應的 FlinkKafkaProducer

核心代碼:
public static void main(String[] args) {
    List<ChannelModel> allChannels = channelRepository.getAll();
    for(ChannelModel nextChannel: allChannels) {
        FlinkKafkaProducer010 nextProducer = (FlinkKafkaProducer010<String>)channelFactory.createChannelProducer(nextChannel,
        FlinkKafkaProducer010.class, Collections.emptyMap());
        nextProducer.setRuntimeContext(this.getRuntimeContext());
        nextProducer.open(parameters);
        producers.put(nextChannel.getChannelId(), nextProducer);
    }
    
    DataStreamSource<T> source = ....
    SplitStream<T> splitStream = source.split(new OutputSelector<T>() {

        @Override
        public Iterable<String> select(String value) {
            List<String> channelIds = channelRouteStrategy.route(value);
            return channeIds;
        }
    });
    
    for(String nextChannel: producers.keySet()) {
        FlinkKafkaProducer010 target = producers.get(nextChannel);
        splitStream.select(nextChannel).addSink(target);
    }
}



優點:
可以路由到不同的BROKER上的TOPIC,在不同的BROKER上隔離性更好
完全利用FLINK原生的特性,更加簡潔優雅,解決了方案1的第二點不足

缺陷:
所有的FlinkKafkaProducer只在MAIN函數中創建一次,後面如果添加了新的KAFKA隊列無法被動態感知並路由


方案3: 利用FLINK的 KeyedSerializationSchema中的getTargetTopic函數,KeyedSerializationSchema 除了將對象轉化Kafka ProducerRecord
的鍵值對之外還可以動態指定Topic
在FLINK Main 函數中將輸入流通過flatMap 轉化爲 Tuple2, 其中key 是目標所屬的Topic, value 是原生數據
實現一個KeyedSerializationSchema作爲構造函數傳給FlinkKafkaProducer,重載getTargetTopic方法: 返回 tuple2.f0

核心代碼:
class DynaRouteSerializationSchema implements KeyedSerializationSchema {
    
    String getTargetTopic(T element) {
        Tuple2 tuple = (Tuple2)element;
        return tuple.f0;
    }
}

public static void main(String[] args) {
    DataStreamSource<T> source = ....
    DataStream<Tuple2<String, T>> converted = source
    .flatMap(new RichFlatMapFunction<Object, Tuple2<String, T>>() {
        @Override
        public void flatMap(T value, Collector<Tuple2<String, Object>> out)
        throws Exception {
            List<String> channelIds = channelRouteStrategy.route(value);
            for(String nextChannel: channelIds) {
                out.collect(Tuple2.valueOf(nextChannel, value));
            }
        }
    });
    
    

}


優點:
完全利用FLINK原生的特性,代碼量非常少
新增加的TOPIC也可以被路由到,不需要啓停流處理

缺陷:
無法像前兩個方案實現Broker級別的路由,只能做到Topic級別的路由


斷流功能:

有時系統升級或者其他組件不可用,需要暫時停止KAFKA PRODUCER
FLINK 原生機制:
被動反壓:
Kafka09Fetcher 包含了一根獨立的 KafkaConsumerThread,從KAFKA中讀取數據,再交給HANDOVER
HANDOVER可以理解爲一個大小爲1的隊列, Kafka09Fetcher 再從隊列中獲取並處理數據,一旦當處理速度變慢,KafkaConsumerThread
無法將數據寫入HANDOVER, 線程就會被阻塞

另外KeyedDeserializationSchema定義了一個isEndOfStream方法,如果返回true, Kafka09Fetcher就會停止循環並退出,導致整個流處理結束


設計思路:

SignalService:  註冊SignalListener, 利用Curator TreeCache 監聽一個Zookeeper 路徑獲取起動/停止流處理的信號量

SignalListener: 接收ZOOKEEPER變更信息的回調接口

PausableKafkaFetcher: 繼承Flink原生的KafkaFetcher, 監聽到信號變化阻塞ConsumerThread的處理

PausableKafkaConsumer: 繼承Flink原生的KafkaConsumer, 創建PausableKafkaFetcher


核心代碼:

public class PausableKafkaFetcher<T> extends Kafka010Fetcher<T> implements SignalListener {

    private final ReentrantLock pauseLock = new ReentrantLock(true);

    private final Condition pauseCond = pauseLock.newCondition();

    private volatile boolean paused = false;

   

   public void onSignal(String path, String value) {

       try {

            pauseLock.lockInterruptibly();

       } catch(InterruptedException e) {

       }

       try {

           if (SIGNAL_PAUSE.equals(value)) {

               paused = true;

           } else if (SIGNAL_START.equals(value)) {

               paused = false;

           }

           pauseCond.signal(); 

       }

       finally {

           pauseLock.unlock();

       } 

   }


   protected void emitRecord(T record, KafkaTopicPartitionState<TopicPartition> partition, long offset, ConsumerRecord<?,?> consumerRecord) throws Exception {

      super.emitRecord(record, partition, offset, consumerRecord);

      pauseLock.lockInterruptibly();

      try {

         while (paused) {

            pauseCond.await();

         }

      } finally {

         pauseLock.unlock();

      }

  }

}


public class PausableKafkaConsumer<T> extends FlinkKafkaConsumer010<T> {

     public void open(Configuration configuration) {

        signalService = ZKSignalService.getInstance();

        signalService.initialize(zkConfig);

     }

 

     public void cancel() {

         super.cancel();

         unregisterSignal();

     }   

 

     public void close() {

        super.close();

        unregisterSignal();

     }


     private void unregisterSignal() {

         if (signalService != null) {

            String fullPath = WATCH_PREFIX + "/" + watchPath;

            signalService.unregisterSignalListener(fullPath);

         }

     }    


     protected AbstractFetcher createFetcher(...) throws  Exception {

        PausableKafkaFetcher<T> fetcher = new PausableKafkaFetcher<> (...);

        if (signalService != null) {

            String fullPath = WATCH_PREFIX + "/" + watchPath;

            signalService.registerSignalListener(fullPath, fetcher);

        }

        return fetcher

     }

}

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