第五章 DataStream API (基礎(chǔ)篇)

一個(gè)Flink程序,其實(shí)就是對(duì)DataStream的各種轉(zhuǎn)換。具體來(lái)說(shuō),代碼基本上由以下幾部分構(gòu)成:

  • 獲取執(zhí)行環(huán)境(Execution Environment);
  • 讀取數(shù)據(jù)源(Source);
  • 定義基于數(shù)據(jù)的轉(zhuǎn)換操作(Transformations);
  • 定義計(jì)算結(jié)果的輸出位置(Sink);
  • 觸發(fā)執(zhí)行程序;


5.1 執(zhí)行環(huán)境

5.1.1 創(chuàng)建執(zhí)行環(huán)境

創(chuàng)建執(zhí)行環(huán)境,通過(guò)調(diào)用StreamExecutionEnviroment類的的靜態(tài)方法。具體有三種:

  • StreamExecutionEnvironment.getExecutionEnvironment,它會(huì)根據(jù)當(dāng)前運(yùn)行的上下文
    直接得到正確的結(jié)果;也就是說(shuō),這個(gè)方法會(huì)根據(jù)當(dāng)前運(yùn)行的方式,自行決定該返回什么樣的
    運(yùn)行環(huán)境;
  • StreamExecutionEnvironment.createLocalEnvironment, 這個(gè)方法返回一個(gè)本地執(zhí)行環(huán)境;
  • StreamExecutionEnvironment.createRemoteEnvironment, 這個(gè)方法返回集群執(zhí)行環(huán)境,調(diào)用時(shí)需要指定JobManager的主機(jī)號(hào)和端口號(hào),并指定要運(yùn)行的jar包;

5.1.2 執(zhí)行模式

  • 流執(zhí)行模式(streaming);
  • 批執(zhí)行模式(batch),有兩種方式進(jìn)行配置:
    • 命令行配置:bin/flink run -Dexecution.runtime-mode=BATCH ...;
    • 代碼中進(jìn)行配置:env.setRuntimeMode(RuntimeExcutionMode.BATCH);
  • 自動(dòng)模式(automatic),在這種模式下,將由程序根據(jù)輸入數(shù)據(jù)源是否有界,來(lái)自動(dòng)選擇執(zhí)行模式。

5.2 數(shù)據(jù)源算子(SOURCE)

Flink可以從各種來(lái)源獲取數(shù)據(jù),然后構(gòu)建DataStream進(jìn)行轉(zhuǎn)換處理。一般將數(shù)據(jù)的輸入來(lái)源稱為數(shù)據(jù)源,而讀取數(shù)據(jù)的算子就是源算子(Source)。因此,Source就是整個(gè)處理程序的輸入端。

Flink有多種讀取源數(shù)據(jù)的方式:

// 定義一個(gè)模擬的用戶行為樣例類
case class Event(user:String, url:String, timestamp:Long)

// 創(chuàng)建執(zhí)行環(huán)境
val env = StreamExecutionEnvironment.getExecutionEnvironment

// 1、從集合讀取數(shù)據(jù)
val clicks = List(Event("Mary", "/.home", 1000L), Event("Bob", "/.cart", 2000L))
val stream1 = env.fromColletctions(clicks)
// 也可以直接將元素列舉出來(lái)通過(guò)fromElements進(jìn)行讀取數(shù)據(jù)
val stream1 = env.fromElements(Event("Mary", "/.home", 1000L), Event("Bob", "/.cart", 2000L))

// 2、從文件讀取數(shù)據(jù):可以是目錄/文件,可以是hdfs文件,也可以是本地文件
val stream2 = env.readTextFile("clicks.csv")

// 3、從socket讀取數(shù)據(jù)
val stream3 = env.socketTextStream("localhost", 777)

// 4、從kafka讀取數(shù)據(jù)。需要添加依賴 連接工具 flink-connector-kafka
// 創(chuàng)建 FlinkKafkaConsumer 時(shí)需要傳入三個(gè)參數(shù):
// (1) topic,定義了從哪些主題中讀取數(shù)據(jù);
// (2) 第二個(gè)參數(shù)是一個(gè) DeserializationSchema 或者 KeyedDeserializationSchema, 反序列化方式;
// (3) Properties 對(duì)象,設(shè)置了 Kafka 客戶端的一些屬性;
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer
// 創(chuàng)建kafka相關(guān)配置
val properties = new Properties();
properties.setProperty("bootstrap.servers", "hadoop102:9092")
properties.setProperty("group.id", "consumer-group")
properties.setProperty("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("auto.offset.reset", "latest")
//創(chuàng)建一個(gè) FlinkKafkaConsumer 對(duì)象,傳入必要參數(shù),從 Kafka 中讀取數(shù)據(jù)
val stream = env.addSource(new FlinkKafkaConsumer[String](
  "clicks",
  new SimpleStringSchema(),
  properties
))

上面介紹的是直接通過(guò)API讀取數(shù)據(jù)源。另一種比較復(fù)雜的方式是自定義數(shù)據(jù)源,然后通過(guò)env.addSource進(jìn)行讀取。

自定義數(shù)據(jù)源需要實(shí)現(xiàn)SourceFunction接口。主要需要重寫兩個(gè)關(guān)鍵方法:

  • run()方法,使用運(yùn)行時(shí)上下文對(duì)象(SourceContext)向下游發(fā)送數(shù)據(jù);
  • cancel()方法,通過(guò)標(biāo)識(shí)位控制退出循環(huán),來(lái)達(dá)到中斷數(shù)據(jù)源的效果;
package com.whu.chapter05

import org.apache.flink.streaming.api.functions.source.SourceFunction
import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext

import java.util.Calendar
import scala.util.Random


// 調(diào)用
// val stream = env.addSource(new ClickSource)


case class Event(user: String, url: String, timestamp: Long)

// 實(shí)現(xiàn) SourceFunction 接口,接口中的泛型是自定義數(shù)據(jù)源中的類型
class ClickSource(sleepTime:Long=1000L) extends SourceFunction[Event] {
  // 標(biāo)志位,用來(lái)控制循環(huán)的退出
  var running = true

  // 重寫run方法,使用上下文對(duì)象sourceContext調(diào)用collect方法
  override def run(ctx: SourceContext[Event]): Unit = {
    // 實(shí)例化一個(gè)隨機(jī)數(shù)發(fā)生器
    val random = new Random()
    // 供隨機(jī)選擇的用戶名數(shù)組
    val users = Array("Marry", "Bob", "Jack", "Cary")
    // 供選擇的url數(shù)組
    val urls = Array("./home", "./cart", "./fav", "./prod?id=1", "./prod?id=2")

    // 通過(guò)while循環(huán)發(fā)送數(shù)據(jù),running默認(rèn)為true,所以會(huì)一直發(fā)送數(shù)據(jù)
    while (running) {
      // 調(diào)用collect方法向下游發(fā)送數(shù)據(jù)
      ctx.collect(Event(
        users(random.nextInt(users.length)),
        urls(random.nextInt(urls.length)),
        Calendar.getInstance.getTimeInMillis // 當(dāng)前時(shí)間戳
      ))
      // 每隔一秒生成一個(gè)點(diǎn)擊事件,方便觀測(cè)
      Thread.sleep(sleepTime)
    }
  }

  override def cancel(): Unit = {
    // 通過(guò)將running設(shè)置為false來(lái)終止數(shù)據(jù)發(fā)送
    running = false
  }
}

5.3 轉(zhuǎn)換算子(Transformation)

數(shù)據(jù)源讀入數(shù)據(jù)之后,我們就可以使用各種轉(zhuǎn)換算子,講一個(gè)或多個(gè)DataStream轉(zhuǎn)換為新的DataStream。

5.3.1 基本轉(zhuǎn)換算子

  • map, 一個(gè)個(gè)進(jìn)行數(shù)據(jù)轉(zhuǎn)換;
  • filter, 對(duì)數(shù)據(jù)進(jìn)行過(guò)濾;
  • flatmap, 扁平映射,可以理解為先map然后進(jìn)行flatten;

5.3.2 聚合算子(Aggregation)

  • keyBy, 按鍵分區(qū)。對(duì)于Flink來(lái)說(shuō),DataStream是沒(méi)有直接進(jìn)行覺(jué)得API的。要做聚合需要先進(jìn)行分區(qū),這個(gè)操作就是通過(guò)keyBy來(lái)完成的。keyBy()方法需要傳入一個(gè)參數(shù),這個(gè)參數(shù)指定了一個(gè)或一組 key。有很多不同的方法來(lái)指定 key:比如對(duì)于 Tuple 數(shù)據(jù)類型,可以指定字段的位置或者多個(gè)位置的組合。對(duì)于 POJO 類型或 Scala 的樣例類,可以指定字段的名稱(String);另外,還可以傳入 Lambda 表達(dá)式或者實(shí)現(xiàn)一個(gè)鍵選擇器(KeySelector),用于說(shuō)明從數(shù)據(jù)中提取 key 的邏輯。
  • 簡(jiǎn)單聚合,sum、min、max、minBy、maxBy等。都是在指定字段上進(jìn)行聚合操作。min()只計(jì)算指定字段的最小值,其他字段會(huì)保留最初第一個(gè)數(shù)據(jù)的值;而 minBy()則會(huì)返回包含字段最小值的整條數(shù)據(jù)。

指定字段的方式有兩種:指定位置,和指定名稱。元組通過(guò)位置,樣例類通過(guò)字段名稱。

keyBy得到的數(shù)據(jù)流一般稱為KeyedStream。而聚合操作則會(huì)將KeyedStream轉(zhuǎn)換為DataStream。

規(guī)約聚合(reduce)

與簡(jiǎn)單聚合類似,reduce操作也會(huì)將KeyedStream轉(zhuǎn)換為DataStream。他不會(huì)改變流的元素?cái)?shù)據(jù)類型,輸入輸出是一致的。

reduce方法來(lái)自ReduceFunction接口,該方法接收兩個(gè)輸入事件,經(jīng)過(guò)處理后輸出一個(gè)相同數(shù)據(jù)類型的事件。

一個(gè)簡(jiǎn)單的栗子:

import org.apache.flink.streaming.api.scala._

object TransformationDemo {
  def main(args:Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 添加自定義數(shù)據(jù)源
    env.addSource(new ClickSource)
      .map(r => (r.user, 1L))
      // 按照用戶進(jìn)行分組
      .keyBy(_._1)
      // 計(jì)算每個(gè)用戶的訪問(wèn)頻次
      .reduce((r1, r2) => (r1._1, r1._2+r2._2))
      // 將所有數(shù)據(jù)分到同一個(gè)分區(qū)
      .keyBy(_ => true)
      // 通過(guò)reduce實(shí)現(xiàn)max功能,計(jì)算訪問(wèn)頻次最高的用戶
      .reduce((r1, r2)=> if(r1._2>r2._2) r1 else r2)
      .print()
    
    // 更簡(jiǎn)單的方法是直接keyBy然后sum然后maxBy就行了,這里只是為了演示reduce用法
    env.execute()
  }
}

5.3.3 用戶自定義函數(shù)(UDF)

Flink的DataStream API編程風(fēng)格其實(shí)是一致的:基本都是基于DataStream調(diào)用一個(gè)方法,表示要做一個(gè)轉(zhuǎn)換操作;方法需要傳入一個(gè)參數(shù),這個(gè)參數(shù)都是需要實(shí)現(xiàn)一個(gè)接口。

這個(gè)接口有一個(gè)共同特定:全部都以算子操作名稱 + Function命名,如數(shù)據(jù)源算子需要實(shí)現(xiàn)SourceFunction接口,map算子需要實(shí)現(xiàn)MapFunction接口。我們可以通過(guò)三種方式來(lái)實(shí)現(xiàn)接口。這就是所謂的用戶自定義函數(shù)(UDF)。

  • 自定義函數(shù)類;
  • 匿名類;
  • lambda表達(dá)式;

接下來(lái)對(duì)這三種編程方式做一個(gè)梳理。

函數(shù)類(Function Classes)
package com.whu.chapter05

import org.apache.flink.api.common.functions.FilterFunction
import org.apache.flink.streaming.api.scala._

object TransformationUDFDemo {
 def main(args:Array[String]): Unit = {

   // 自定義filterFunction類, 并接受額外的參數(shù)
   class MyFilter(key:String) extends FilterFunction[Event] {
     override def filter(t: Event): Boolean = {
       t.url.contains(key)
     }
   }

   val env = StreamExecutionEnvironment.getExecutionEnvironment
   env.setParallelism(1)

   // 通過(guò)自定義函數(shù)類
   val stream1 = env.addSource(new ClickSource)
     .filter(new MyFilter("home"))

   // 通過(guò)匿名類
   val stream2 = env.addSource(new ClickSource)
     .filter(new FilterFunction[Event]{
       override def filter(t: Event): Boolean = {
         t.url.contains("home")
       }
     })

   // 最簡(jiǎn)單的lambda 表達(dá)式
   val stream3 = env.addSource(new ClickSource)
     .filter(_.url.contains("home"))
   
   stream1.print("stream1")
   stream2.print("stream2")
   stream3.print("stream3")
   
   env.execute()
 }
}
富函數(shù)類(Rich Function Classes)

富函數(shù)類也是DataStream API提供的一個(gè)函數(shù)類的接口,所有的Flink函數(shù)類都有其Rich版本。富函數(shù)類一般是已抽象類的形式出現(xiàn)的。例如:RichMapFunction,RichFilterFunction,RichReduceFunction等。

與常規(guī)函數(shù)類的不同主要在于富函數(shù)類可以獲取運(yùn)行環(huán)境的上下文,并擁有一些生命周期方法,所以可以實(shí)現(xiàn)更復(fù)雜的功能。

典型的生命周期方法有:

  • open方法,是RichFunction的初始化方法,會(huì)開啟一個(gè)算子的生命周期。當(dāng)一個(gè)算子的實(shí)際工作方法如map、filter等方法被調(diào)用之前,open會(huì)首先被調(diào)用。所以像文件IO流、數(shù)據(jù)庫(kù)連接、配置文件讀取等等這樣一次性的工作,都適合在open方法中完成;
  • close方法,是生命周期中最后一個(gè)調(diào)用的方法,類似于解構(gòu)方法。一般用來(lái)做一些清理工作。

open、close等生命周期方法對(duì)于一個(gè)并行子任務(wù)來(lái)說(shuō)只會(huì)調(diào)用一次;而對(duì)應(yīng)的,實(shí)際工作方法,如map,對(duì)于每一條數(shù)據(jù)都會(huì)調(diào)用一次。

package com.whu.chapter05

import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala._

object RichFunctionDemo {
  def main(args:Array[String]) : Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(2)

    env.addSource(new ClickSource(10000))
      .map(new RichMapFunction[Event, Long] {
        // 在任務(wù)生命周期開始時(shí)會(huì)執(zhí)行open方法,在控制臺(tái)打印對(duì)應(yīng)語(yǔ)句
        override def open(parameters: Configuration): Unit = {
          println(s"索引為 ${getRuntimeContext.getIndexOfThisSubtask} 的任務(wù)開始")
        }
        override def map(in: Event): Long = {
          in.timeStamp
        }

        override def close(): Unit = {
          println(s"索引為 ${getRuntimeContext.getIndexOfThisSubtask} 的任務(wù)結(jié)束")
        }
      }).print()
    
    env.execute()
  }
}

在上面的例子中可以看到,富函數(shù)類提供了getRuntimeContex方法,可以獲取運(yùn)行時(shí)上下文信息,如程序執(zhí)行的并行度,任務(wù)名稱,任務(wù)狀態(tài)等。

5.3.4 物理分區(qū)(Physical Partitioning)

分區(qū)(partitioning)操作就是要將數(shù)據(jù)進(jìn)行重新分布,傳遞到不同的流分區(qū)去進(jìn)行下一步計(jì)算。keyBy是一種邏輯分區(qū)(logic partitioning)操作。

Flink 對(duì)于經(jīng)過(guò)轉(zhuǎn)換操作之后的 DataStream,提供了一系列的底層操作算子,能夠幫我們實(shí)現(xiàn)數(shù)據(jù)流的手動(dòng)重分區(qū)。為了同 keyBy()相區(qū)別,我們把這些操作統(tǒng)稱為“物理分區(qū)”操作。

常見的物理分區(qū)策略有隨機(jī)分區(qū)、輪詢分區(qū)、重縮放和廣播,還有一種特殊的分區(qū)策略— —全局分區(qū),并且 Flink 還支持用戶自定義分區(qū)策略,下邊我們分別來(lái)做了解。

隨機(jī)分區(qū)(shuffle)

最簡(jiǎn)單的重分區(qū)方式就是直接“洗牌”。通過(guò)調(diào)用 DataStream 的 shuffle()方法,將數(shù)據(jù)隨機(jī)地分配到下游算子的并行任務(wù)中去。

隨機(jī)分區(qū)服從均勻分布(uniform distribution),所以可以把流中的數(shù)據(jù)隨機(jī)打亂,均勻地傳遞到下游任務(wù)分區(qū)。

輪詢分區(qū)(Round-Robin)

輪詢也是一種常見的重分區(qū)方式。簡(jiǎn)單來(lái)說(shuō)就是“發(fā)牌”,按照先后順序?qū)?shù)據(jù)做依次分發(fā)。通過(guò)調(diào)用 DataStream的.rebalance()方法,就可以實(shí)現(xiàn)輪詢重分區(qū)。rebalance()使用的是 Round-Robin 負(fù)載均衡算法,可以將輸入流數(shù)據(jù)平均分配到下游的并行任務(wù)中去。


重縮放分區(qū)(rescale)

重縮放分區(qū)和輪詢分區(qū)非常相似。當(dāng)調(diào)用 rescale()方法時(shí),其實(shí)底層也是使用 Round-Robin算法進(jìn)行輪詢,但是只會(huì)將數(shù)據(jù)輪詢發(fā)送到下游并行任務(wù)的一部分中,也就是說(shuō),“發(fā)牌人”如果有多個(gè),那么 rebalance()的方式是每個(gè)發(fā)牌人都面向所有人發(fā)牌;而rescale()的做法是分成小團(tuán)體,發(fā)牌人只給自己團(tuán)體內(nèi)的所有人輪流發(fā)牌。



當(dāng)下游任務(wù)(數(shù)據(jù)接收方)的數(shù)量是上游任務(wù)(數(shù)據(jù)發(fā)送方)數(shù)量的整數(shù)倍時(shí),rescale()的效率明顯會(huì)更高。比如當(dāng)上游任務(wù)數(shù)量是 2,下游任務(wù)數(shù)量是 6 時(shí),上游任務(wù)其中一個(gè)分區(qū)的數(shù)據(jù)就將會(huì)平均分配到下游任務(wù)的 3 個(gè)分區(qū)中。

廣播(broadcast)

這種方式其實(shí)不應(yīng)該叫作“重分區(qū)”,因?yàn)榻?jīng)過(guò)廣播之后,數(shù)據(jù)會(huì)在不同的分區(qū)都保留一份,可能進(jìn)行重復(fù)處理。可以通過(guò)調(diào)用 DataStream 的 broadcast()方法,將輸入數(shù)據(jù)復(fù)制并發(fā)送到下游算子的所有并行任務(wù)中去。

全局分區(qū)(global)

全局分區(qū)也是一種特殊的分區(qū)方式。這種做法非常極端,通過(guò)調(diào)用.global()方法,會(huì)將所有的輸入流數(shù)據(jù)都發(fā)送到下游算子的第一個(gè)并行子任務(wù)中去。這就相當(dāng)于強(qiáng)行讓下游任務(wù)并行度變成了 1,所以使用這個(gè)操作需要非常謹(jǐn)慎,可能對(duì)程序造成很大的壓力。

自定義分區(qū)

當(dāng) Flink 提 供 的 所 有 分 區(qū) 策 略 都 不 能 滿 足 用 戶 的 需 求 時(shí) , 我 們 可 以 通 過(guò) 使 用partitionCustom()方法來(lái)自定義分區(qū)策略。
在調(diào)用時(shí),方法需要傳入兩個(gè)參數(shù),第一個(gè)是自定義分區(qū)器(Partitioner)對(duì)象,第二個(gè)是應(yīng)用分區(qū)器的字段,它的指定方式與 keyBy 指定 key 基本一樣:可以通過(guò)字段名稱指定,也可以通過(guò)字段位置索引來(lái)指定,還可以實(shí)現(xiàn)一個(gè) KeySelector 接口。

栗子:

package com.whu.chapter05

import org.apache.flink.api.common.functions.Partitioner
import org.apache.flink.streaming.api.scala._

object PartitioningDemo {
  def main(args:Array[String]) : Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    // 讀取數(shù)據(jù)源
    val stream = env.addSource(new ClickSource())

    // 隨機(jī)分區(qū)(shuffle)
    stream.shuffle.print("shuffle").setParallelism(4)

    // 輪詢分區(qū)(rebalance, Round-Robin)
    stream.rebalance.print("rebalance").setParallelism(4)

    // 重縮放分區(qū)(rescale)
    stream.rescale.print("rescale").setParallelism(4)

    // 廣播 (broadcast)
    stream.broadcast.print("broadcast").setParallelism(4)

    // 全局分區(qū)(global)
    stream.global.print("global").setParallelism(4)

    // 自定義分區(qū)
    stream.partitionCustom(new Partitioner[Event] {
      // 根據(jù) key 的奇偶性計(jì)算出數(shù)據(jù)將被發(fā)送到哪個(gè)分區(qū)
      override def partition(k: Event, i: Int): Int = {
        k.timeStamp.toInt % 2
      }
    }, "user"
    ).print()
    
    env.execute()
  }
}

5.4 輸出算子(Sink)

5.4.1 連接到外部系統(tǒng)

Flink的DataStream API專門提供了向外部寫入數(shù)據(jù)的方法:addSink。與addSource類似,addSink方法對(duì)應(yīng)著一個(gè)Sink算子,主要就是用來(lái)實(shí)現(xiàn)與外部系統(tǒng)鏈接、并將數(shù)據(jù)提交寫入的;Flink程序中所有對(duì)外的輸出操作,一般都是利用Sink算子完成的。

與addSource類似,addSink也支持自定義sink算子SinkFunction。在這個(gè)接口中只需要重寫一個(gè)方法invoke(),用來(lái)將指定的值寫入到外部系統(tǒng)中。這個(gè)方法在每條數(shù)據(jù)記錄到來(lái)時(shí)都會(huì)調(diào)用。Flink官方提供了諸多第三方系統(tǒng)連接器:


除 Flink 官方之外,Apache Bahir 作為給 Spark 和 Flink 提供擴(kuò)展支持的項(xiàng)目,也實(shí)現(xiàn)了一
些其他第三方系統(tǒng)與 Flink 的連接器:


5.4.2 輸出到文件

Flink有一些非常簡(jiǎn)單粗暴的輸出到文件的預(yù)實(shí)現(xiàn)方法,如writeAsCsv等,目前這些簡(jiǎn)單的方法已經(jīng)要被棄用。

Flink專門提供了一個(gè)流式文件系統(tǒng)連接器:StreamingFileSink,它繼承自抽象類RichSinkFunction,而且繼承了Flink的檢查點(diǎn)機(jī)制,用來(lái)確保精確一次(exactly)的一致性語(yǔ)義。

StreamingFileSink支持行編碼(row-encoded)和批量編碼(bulk-encoded,比如parquet)格式。這兩種不同的方式都有各自的構(gòu)建器(builder),調(diào)用方法如下:

  • 行編碼:StreamingFileSink.forRowFormat (basePath, rowEncoder);
  • 批量編碼:StreamingFileSink.forBulkFormat (basePath,bulkWriterFactory);

在創(chuàng)建行或批量Sink時(shí),我們需要傳入兩個(gè)參數(shù),用來(lái)指定存儲(chǔ)桶的基本路徑和數(shù)據(jù)的編碼邏輯。

package com.whu.chapter05

import org.apache.flink.api.common.serialization.SimpleStringEncoder
import org.apache.flink.streaming.api.scala._
import org.apache.flink.core.fs.Path
import org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink
import org.apache.flink.streaming.api.functions.sink.filesystem.rollingpolicies.DefaultRollingPolicy

import java.util.concurrent.TimeUnit


object SinkToFileDemo {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    
    val stream = env.addSource(new ClickSource())
    
    val fileSink = StreamingFileSink.forRowFormat(
      new Path("./output"),
      new SimpleStringEncoder[String]("UTF-8")
    )
      // 通過(guò).withRollingPolicy()方法指定滾動(dòng)邏輯
      .withRollingPolicy(
        DefaultRollingPolicy.builder()
          .withMaxPartSize(1024*1024*1024)
          .withRolloverInterval(TimeUnit.MINUTES.toMillis(15))
          .withInactivityInterval(TimeUnit.MINUTES.toMillis(5))
          .build()
      ).build()
    
    stream.map(_.toString).addSink(fileSink)
  }
}

上面創(chuàng)建了一個(gè)簡(jiǎn)單的文件 Sink,通過(guò) withRollingPolicy()方法指定了一個(gè)“滾動(dòng)策略”。上面的代碼設(shè)置了在以下 3 種情況下,我們就會(huì)滾動(dòng)分區(qū)文件:

  • 至少包含 15 分鐘的數(shù)據(jù);
  • 最近 5 分鐘沒(méi)有收到新的數(shù)據(jù);
  • 文件大小已達(dá)到1GB;

輸出到其他系統(tǒng)

略。

參考:
FLink教程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評(píng)論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,193評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,411評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,657評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容