Flink 的側輸出流,怎麼玩?
來源:安瑞哥是碼農
之所以想到要寫這篇文章呢,因為上篇文章寫過Doris 透過Flink CDC 只需要一個簡單的命令,就能同步整個mysql庫的所有表,其中這個多表的同步過程,就使用到了Flink的側流輸出功能。
所謂側流輸出(也叫旁路輸出),指的是透過對讀取的資料來源(DataStream),根據不同的業務規則進行條件篩選,然後根據篩選後生成的不同資料分支,做不同的業務目的輸出(比如寫到不同的資料目的地中)。
看到Flink官網的如此描述後,我就在想,印象中 Spark 是不是不具備這個功能?
為此,我還專門回過頭去折騰了下 Spark 的 structured stream,試圖根據兩個不同的資料處理邏輯,得到兩股不同的流,用兩股流分別做輸出,虛擬碼如下:
val rawDF = spark.readStream //獲取資料來源
.format("kafka") //確定資料來源的來源格式
.option("kafka.bootstrap.servers", "xxxxx")
.option(....)
.load()
//第1個資料流
val df01 = rawDF.map(//第1個處理邏輯)
//第2個資料流
val df02 = rawDF.map(//第2個處理邏輯)
//第1個資料流輸出
df01.writeStream
.outputMode(OutputMode.Append())
.format(//第1個輸出)
.option("checkpointLocation","xxxx")
.start()
.awaitTermination()
//第2個資料流輸出
df02.writeStream
.outputMode(OutputMode.Append())
.format(//第2個輸出)
.option("checkpointLocation","xxxx")
.start()
.awaitTermination()
你們猜,這個程式碼能執行起來嗎?
其實可以,整個過程不會報錯,但是第2個資料流,永遠都不會輸出。
雖然Spark官網沒有明確說明,一個spark流式任務中,是否能做多個外部輸出,但是透過實際的動手驗證發現:不行。
所以目前來看,對比Spark,側輸出流是Flink的專屬,而且是DataStream API的專屬(SQL API 不具備)。
那麼今天這篇內容,我們就來聊聊Flink的這個側流輸出能力,到底實際用起來怎麼樣?會遇到哪些坑?
0. 準備工作
側輸出流本身是 Flink 內建的功能,所以使用它,不需要跟Flink CDC 一樣引入額外的pom依賴,直接編碼實現就可以。
官方文件給的示例非常的簡單,只演示了一個側流跟一個主流的情況,但是從它的描述來看,使用的時候,其實是可以根據你的業務要求,切分出任意多個側輸出流的。
那為了儘可能測試該功能,我準備從kafka資料來源中,以 client_ip 這個欄位值的最後一個數字作為區分,分成4股不同的流,對每股流的輸出計劃如下:
1. 主輸出流:寫入到kafka的一個topic中;
2. 側輸出流01:寫入到Doris表中;
3. 側輸出流02:直接列印到螢幕輸出;
4. 側輸出流03:寫入到hdfs檔案系統中。
因為官網提到不同的流輸出,可以支援不同的資料型別,所以我也決定不同的資料流,用不同的資料型別來表示。
廢話少嘮,直接上程式碼。
1. 完整編碼
根據官方文件的示例,帶有詳細註釋的編碼如下:
package com.anryg.side_output
import java.time.Duration
import java.util.Properties
import com.alibaba.fastjson.{JSON, JSONObject}
import org.apache.doris.flink.cfg.{DorisExecutionOptions, DorisOptions, DorisReadOptions}
import org.apache.doris.flink.sink.DorisSink
import org.apache.doris.flink.sink.writer.SimpleStringSerializer
import org.apache.flink.api.common.eventtime.WatermarkStrategy
import org.apache.flink.api.common.serialization.{SimpleStringEncoder, SimpleStringSchema}
import org.apache.flink.configuration.MemorySize
import org.apache.flink.connector.base.DeliveryGuarantee
import org.apache.flink.connector.kafka.sink.{KafkaRecordSerializationSchema, KafkaSink}
import org.apache.flink.connector.kafka.source.KafkaSource
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer
import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend
import org.apache.flink.core.fs.Path
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.environment.CheckpointConfig.ExternalizedCheckpointCleanup
import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.functions.sink.filesystem.{OutputFileConfig, StreamingFileSink}
import org.apache.flink.streaming.api.functions.sink.filesystem.bucketassigners.DateTimeBucketAssigner
import org.apache.flink.streaming.api.functions.sink.filesystem.rollingpolicies.DefaultRollingPolicy
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.util.Collector
/**
* @DESC: Flik側輸出流案例測試
* @Auther: Anryg
* @Date: 2023/11/15 10:11
*/
object FlinkSideoutTest01 {
private final val hdfsPrefix = "hdfs://192.168.211.106:8020" //HDFS路徑字首
/**
* @DESC: 定義hdfs輸出方式
* */
private def hdfsSink[Out](path:String): StreamingFileSink[Out] ={
val hdfsSink = StreamingFileSink.forRowFormat(new Path(hdfsPrefix + path),
new SimpleStringEncoder[Out]("UTF-8"))
.withBucketAssigner(new DateTimeBucketAssigner("yyyy-MM-dd")) /**預設基於時間分配器,以小時為單位進行切分yyyy-MM-dd--HH*/
.withOutputFileConfig(OutputFileConfig.builder().withPartPrefix("kafka").withPartSuffix(".csv").build()) //設定生成檔案的配置策略
.withRollingPolicy( //設定檔案的滾動策略,也就是分檔案策略,也可以同時設定檔案的命名規則,這裡暫時用預設
DefaultRollingPolicy.builder()
.withRolloverInterval(Duration.ofSeconds(10)) //檔案滾動間隔,設為10分鐘,即每10分鐘生成一個新檔案
.withInactivityInterval(Duration.ofSeconds(10)) //空閒間隔時間,也就是當前檔案有多久沒有寫入資料,則進行滾動
.withMaxPartSize(MemorySize.ofMebiBytes(1024)) //單個檔案的最大檔案大小,設定為1G
.build())
.build()
hdfsSink
}
/**
* @DESC: 定義Doris的輸出方式
* */
private def dorisSink[Sting](label:String,table:String): DorisSink[String] ={
/**配置Doris相關引數*/
val dorisBuilder = DorisOptions.builder
dorisBuilder.setFenodes("192.168.221.173:8030")
.setTableIdentifier(table) //比如:example_db.dns_logs_from_flink01
.setUsername("root")
.setPassword("")
/**確定資料寫入到Doris的方式,即stream load方式*/
val executionBuilder = DorisExecutionOptions.builder
executionBuilder.setLabelPrefix(label) //streamload label prefix
/**新增額外的資料格式設定,否則寫不進去*/
val properties = new Properties
properties.setProperty("format", "json")
properties.setProperty("read_json_by_line", "true")
executionBuilder.setStreamLoadProp(properties)
val builder = DorisSink.builder[String] //注意這個資料型別String 需要加上
builder.setDorisReadOptions(DorisReadOptions.builder.build) //確定資料讀取策略
.setDorisExecutionOptions(executionBuilder.build) //確定資料執行策略
.setSerializer(new SimpleStringSerializer()) //確定資料資料序列化(寫入)型別
.setDorisOptions(dorisBuilder.build) //新增Doris配置
.build()
builder.build()
}
/**
* @DESC: 定義kafka的輸出方式
* */
private def kafkaSink(topic:String): KafkaSink[String] ={
KafkaSink.builder[String]()
.setBootstrapServers("192.168.211.107:6667")
.setRecordSerializer(
KafkaRecordSerializationSchema.builder()
.setTopic(topic)
.setValueSerializationSchema(new SimpleStringSchema())
.build()
)
.setDeliverGuarantee(DeliveryGuarantee.AT_LEAST_ONCE)
.build()
}
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.enableCheckpointing(10000, CheckpointingMode.AT_LEAST_ONCE)
//env.setParallelism(args(0).toInt)
env.setStateBackend(new EmbeddedRocksDBStateBackend(true)) //新的設定state backend的方式
env.getCheckpointConfig.setCheckpointStorage("hdfs://192.168.211.106:8020/tmp/flink_checkpoint/FLinkSideoutTest01") //設定checkpoint的hdfs目錄
env.getCheckpointConfig.setExternalizedCheckpointCleanup(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION) //設定checkpoint記錄的保留策略
val kafkaSource = KafkaSource.builder() //獲取kafka資料來源
.setBootstrapServers("192.168.211.107:6667")
.setTopics("test")
.setGroupId("SideoutTest01")
.setStartingOffsets(OffsetsInitializer.latest())
.setValueOnlyDeserializer(new SimpleStringSchema())
.build()
import org.apache.flink.streaming.api.scala._ //引入隱私轉換函式
val kafkaDS = env.fromSource(kafkaSource,
WatermarkStrategy.noWatermarks()
,"kafka-data") //讀取資料來源生成DataStream物件
val rawDS = kafkaDS.map(line => {
val rawJson = JSON.parseObject(line) //原始string是一個json,對其進行解析
val message = rawJson.getString("message") //獲取業務資料部分
val msgArray = message.split(",") //指定分隔符進行欄位切分
msgArray
}).filter(_.length == 9) //只留欄位數為9的資料
.filter(array => ! array(0).trim.equals(""))
.map(array => (array(0),array(1).toLowerCase,array(2),array(3),array(4),array(5),array(6).toLowerCase(),array(7),array(8))) //將其轉化成為元組,為了方便下一步賦予schema
/**定義第1個側輸出流的標籤*/
val outputTag01 = OutputTag[(String,String,String,String,String,String,String,String,String)]("side-output01")
/**定義第2個側輸出流的標籤*/
val outputTag02 = OutputTag[String]("side-output02")
/**定義第3個側輸出流的標籤*/
val outputTag03 = OutputTag[(String,String,String)]("side-output03")
val mainDS = rawDS.process(new ProcessFunction[(String,String,String,String,String,String,String,String,String),String] {
override def processElement(value: (String, String, String, String, String, String, String, String, String), ctx: ProcessFunction[(String, String, String, String, String, String, String, String, String), String]#Context, out: Collector[String]): Unit = {
val sufix = value._1.substring(value._1.length - 1) //拿到client_ip的最後一個數字,然後根據其數字來進行資料流分組
if (sufix.equals("1") || sufix.equals("2")) {
out.collect(value._1) /**主輸出流*/
}
else if (sufix.equals("3") || sufix.equals("4") || sufix.equals("5")){
ctx.output(outputTag01, value) /**側輸出流01*/
}
else if (sufix.equals("6") || sufix.equals("7") || sufix.equals("8")) {
ctx.output(outputTag02, value._2) /**側輸出流02*/
}
else {
val out03 = (value._1, value._2, value._3)
ctx.output(outputTag03, out03) /**側輸出流03*/
}
}
})
/**獲取側輸出流01*/
val sideDS01 = mainDS.getSideOutput(outputTag01)
/**獲取側輸出流02*/
val sideDS02 = mainDS.getSideOutput(outputTag02)
/**獲取側輸出流03*/
val sideDS03 = mainDS.getSideOutput(outputTag03)
/**將主輸出流輸出到kafka*/
mainDS.sinkTo(kafkaSink("example_topic"))
/**將第1個側流輸出到Doris表*/
sideDS01.map(array => {
val json = new JSONObject(9) //將資料封裝為json物件
json.put("client_ip", array._1)
json.put("domain",array._2)
json.put("time",array._3)
json.put("target_ip",array._4)
json.put("rcode",array._5)
json.put("query_type",array._6)
json.put("authority_record",array._7)
json.put("add_msg",array._8)
json.put("dns_ip",array._9)
json.toJSONString //轉換為json string
}).sinkTo(dorisSink("side_ouput01", "example_db.flink_side_stream01"))
/**將第2個側流直接列印出來*/
sideDS02.print("side stream02: ")
/**將第3個側流輸出到HDFS的CSV檔案*/
sideDS03.addSink(hdfsSink[(String,String,String)]("/tmp/flink_side-stream03"))
env.execute("FlinkSideoutTest01")
}
}
從程式碼中可以看出來,整個編碼邏輯還是非常清晰的。
只不過對於Flink來說,不同的輸出物件,不管是kafka、Doris、還是HDFS,每一個Sink物件的API風格都不一樣,沒有統一的格式與套路,相比之下,Spark則會更規整一些。
而且隨著 Flink 版本的不同,相同功能的 API 寫法還不一樣,就還挺難受的。
2. 執行結果
整個編碼過程雖然看起來內容比較多,而且涉及到多個不同的Sink物件,但是編寫完卻出奇的順利(居然沒有發現坑)。
一跑就通,出奇的絲滑,簡直 Amazing...
最關鍵是4個輸出都符合預期。
先看主輸出流(寫kafka):
再看第1個側輸出流(寫Doris表):
然後看第2個側輸出流(直接控制檯輸出):
最後看第3個側輸出流(寫HDFS):
總結
雖然說根據官網文件的要求,整個編碼過程很順利,沒有遇到明顯的讓程式跑不起來的坑。
但是,這裡還是有個必須要注意的點,那就是在實踐中,會有這麼個問題,那就是,但凡有任何一個流在sink的時候發生了異常,程式預設是不會退出的。
也就是說,程式在執行過程中有沒有問題,除非你去觀察日誌,否則即便你不對程式碼邏輯進行try...catch,程式本身是會忽略這些異常繼續跑的。
這就需要開發者在編碼時,確定多個流之間的關係,是相互不影響呢,還是要作為一個「事務」而捆綁在一起。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027827/viewspace-2997332/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 輸入輸出流
- win10玩遊戲老是彈出輸入法怎麼辦 玩遊戲shift鍵老彈出輸入法Win10遊戲
- IO流的位元組輸入輸出流(InputStream,OutputStream)
- Java 輸入輸出流Java
- 重學java中的輸入輸出流Java
- 資料流輸出
- python的輸出語句怎麼寫Python
- 如何把檔案輸出流替換成位元組輸出流
- 詳解Java中的IO輸入輸出流!Java
- flink 流的合併
- python怎麼不轉行輸出Python
- [java IO流]之 基本資料型別輸入輸出流Java資料型別
- [java IO流]之 萬能輸出流列印流printWriterJava
- 玩遊戲總是觸發輸入法怎麼辦_win10玩遊戲的時候一直出現輸入法的解決方法遊戲Win10
- 《暖雪》無限貫日流怎麼玩?無限貫日流玩法分享
- [java IO流]之 鍵盤顯示器輸入輸出流(System)Java
- 使用pycharm print不輸出怎麼辦PyCharm
- python中怎麼輸出雙引號Python
- 023--C++養成之路(io流:流的初始化以及基本的輸入輸出)C++
- 字元輸出流_Writer類&FileWriter類介紹和字元輸出流的基本使用_寫出單個字元到檔案字元
- 執行緒間的協作(3)——管道輸入/輸出流執行緒
- [譯] 用 Flask 輸出視訊流Flask
- outputStream(輸出流)轉inputstream(輸入流)以及輸入流如何複用
- Flink的流處理API(二)API
- Flink流處理的演變
- 檔案輸入輸出處理(二)-位元組流
- flink的print()函式輸出的都是物件地址而非物件內容函式物件
- 帶你玩轉Flink流批一體分散式實時處理引擎分散式
- GO的日誌怎麼玩Go
- Python怎麼輸出所有的水仙花數?Python
- python爬取中文輸出亂碼怎麼辦Python
- python3中輸出錯誤怎麼辦?Python
- python怎麼將列印輸出日誌檔案Python
- JDK 18 及以上使用標準輸出流中文輸出亂碼問題JDK
- vscode中文亂碼怎麼解決 vscode輸出亂碼怎麼解決VSCode
- 資料跨境流動需要注意什麼?怎麼實現安全合規的跨境傳輸?
- 轉載:字元輸出流Writer簡要概括字元
- python陣列下標怎麼獲取值並輸出Python陣列