大家好,我是後來,我會分享我在學習和工作中遇到的點滴,希望有機會我的某篇文章能夠對你有所幫助,所有的文章都會在公衆號首發,歡迎大家關注我的公衆號" 後來X大數據 ",感謝你的支持與認可。
又是一週沒更文了,上週末回運城看牙去了,一直都在路上,太累了。說回正題,關於flink的入門在上一篇已經講過了。
今天主要說一下關於流處理的API,這一篇所有的代碼都是scala。
那麼我們還得回到上次的WordCount代碼,Flink程序看起來像轉換數據集合的常規程序。每個程序都包含相同的基本部分:
- 獲得execution environment
- 加載/創建初始數據
- 指定對此數據的轉換
- 指定將計算結果放在何處
- 觸發程序執行
獲取執行環境
所以要想處理數據,還得從獲取執行環境來說起。StreamExecutionEnvironment是所有Flink程序的基礎,所以我們來獲取一個執行環境。有以下3種靜態方法
- getExecutionEnvironment()
- createLocalEnvironment()
- createRemoteEnvironment(host: String, port: Int, jarFiles: String*)
//獲取上下文環境
val contextEnv = StreamExecutionEnvironment.getExecutionEnvironment
//獲取本地環境
val localEnv = StreamExecutionEnvironment.createLocalEnvironment(1)
//獲取集羣環境
val romoteEnv = StreamExecutionEnvironment.createRemoteEnvironment("bigdata101",3456,2,"/ce.jar")
但一般來說,我們只需要使用第一種getExecutionEnvironment(),因爲**它將根據上下文執行正確的操作;**也即是說,它會根據查詢運行的方式決定返回什麼樣的運行環境,你是IDE執行,它會返回本地執行環境,如果你是集羣執行,它會返回集羣執行環境。
預定義的數據流源
好了,獲取到環境之後,我們就開始獲取數據源,flink自身是支持多數據源的,首先來看幾個預定義的數據流源
- 基於文件
- readTextFile(path)- TextInputFormat 逐行讀取文本文件,並將其作爲字符串返回,只讀取一次。
- readFile(fileInputFormat, path) -根據指定的文件輸入格式讀取文件,只讀取一次。
但事實上,上面的2個方法內部都是調用的readFile(fileInputFormat, path, watchType, interval, pathFilter, typeInfo)
我們來看看源碼:
我們選擇其中第一個比較簡單的方法進入,就看到了下圖,發現其實上述的2種方法最終都會落到這個readFile(fileInputFormat, path, watchType, interval, pathFilter)方法上,只不過後面的參數都是默認值了。
所以,這些參數當然也可以自己指定。好了,這個方法大家也不常用,所以就簡單介紹下,有需要的小夥伴自己試試這些後面的參數。
- 基於套接字
socketTextStream-從套接字讀取。元素可以由定界符分隔。
這裏提到了套接字,這個我在終於懂了TCP協議爲什麼是可靠的,計算機基礎(六)之運輸層是講過的,這裏再說一下:
套接字 socket = {IP地址 : 端口號},示例:192.168.1.99 :3456
代碼使用如下:
val wordDS: DataStream[String] = contextEnv.socketTextStream("bigdata101",3456)
套接字是抽象的,只是爲了表示TCP連接而存在。
- 基於集合
- fromCollection(Seq)-從Java Java.util.Collection創建數據流。集合中的所有元素必須具有相同的類型。
- fromCollection(Iterator)-從迭代器創建數據流。該類指定迭代器返回的元素的數據類型。
- fromElements(elements: _*)-從給定的對象序列創建數據流。所有對象必須具有相同的類型。
- fromParallelCollection(SplittableIterator)-從迭代器並行創建數據流。該類指定迭代器返回的元素的數據類型。
- generateSequence(from, to) -並行生成給定間隔中的數字序列。
這些預設的數據源使用的也不是很多,可以說是幾乎不用。所以大家可以自己嘗試一下。
當然注意,如果使用 fromCollection(Seq),因爲是從Java.util.Collection創建數據流,所以如果你是用scala編程,那麼就需要引入 隱式轉換
import org.apache.flink.streaming.api.scala._
獲取數據源Source
大家也能發現,以上的方法幾乎都是從一個固定的數據源中獲取數據,適合自己測試,但在生產中肯定是不能使用的,所以我們來看看正兒八經的數據源:
官方支持的source與sink如下:
- Apache Kafka(源/接收器)
- Apache Cassandra(接收器)
- Amazon Kinesis Streams(源/接收器)
- Elasticsearch(接收器)
- Hadoop文件系統(接收器)
- RabbitMQ(源/接收器)
- Apache NiFi(源/接收器)
- Twitter Streaming API(源)
- Google PubSub(源/接收器)
加粗的3個日常中比較常用的,那麼也發現其實數據源只有kafka,sink有ES和HDFS,那麼我們先來說說kafka Source,關於Kafka的安裝部署這裏就不講了,自行Google。我們來貼代碼與分析。
Kafka Source
- 在pom.xml中導入kafka依賴
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka-0.11_2.11</artifactId>
<version>1.7.2</version>
</dependency>
這裏就涉及到了版本的問題:大家可以根據自己的版本進行調試。但是注意:目前flink 1.7版本開始,通用Kafka連接器被視爲處於BETA狀態,並且可能不如0.11連接器那麼穩定。所以建議大家使用flink-connector-kafka-0.11_2.11
- 貼測試代碼
import java.util.Properties
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.kafka. FlinkKafkaConsumer011
import org.apache.flink.streaming.api.scala._
/**
* @description: ${kafka Source測試}
* @author: Liu Jun Jun
* @create: 2020-06-10 10:56
**/
object kafkaSource {
def main(args: Array[String]): Unit = {
//獲取執行環境
val env = StreamExecutionEnvironment.getExecutionEnvironment
val properties = new Properties()
//配置kafka連接器
properties.setProperty("bootstrap.servers", "bigdata101:9092")
properties.setProperty("group.id", "test")
//設置序列化方式
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
//設置offset消費方式:可設置的參數爲:earliest,latest,none
properties.setProperty("auto.offset.reset", "latest")
//earliest:當各分區下有已提交的offset時,從提交的offset開始消費;無提交的offset時,從頭開始消費
//latest:當各分區下有已提交的offset時,從提交的offset開始消費;無提交的offset時,消費新產生的該分區下的數據
//none:topic各分區都存在已提交的offset時,從offset後開始消費;只要有一個分區不存在已提交的offset,則拋出異常
val kafkaDS: DataStream[String] = env.addSource(
new FlinkKafkaConsumer011[String](
"test1",
new SimpleStringSchema(),
properties
)
)
kafkaDS.print("測試:")
env.execute("kafkaSource")
}
}
在這個裏面,要注意的是消費offset的方式,3個參數的區別:
- 如果存在已經提交的offest時,不管設置爲earliest 或者latest 都會從已經提交的offest處開始消費
- 如果不存在已經提交的offest時,earliest 表示從頭開始消費,latest 表示從最新的數據消費,也就是新產生的數據.
- none :topic各分區都存在已提交的offset時,從提交的offest處開始消費;只要有一個分區不存在已提交的offset,則拋出異常
關於kafka序列化的設置這個需要根據實際的需求配置。
上面只是簡單的使用Kafka作爲了數據源獲取到了數據,至於怎麼做檢查點以及精準一次性消費這類共性話題,我們之後單獨拿出來再講,這次先把基本的API過一下
Transform 算子
說完了Source,接下來就是Transform,這類的算子可以說是很多了,官網寫的非常全,我把鏈接貼在這裏,大家可以直接看官網:flink官網的轉換算子介紹
而我們常用的也就是下面這些,功能能spark的算子可以說是幾乎一樣。所以我們簡單看一下:
- Map 映射-----以元素爲單位進行映射,會生成新的數據流;DataStream → DataStream
//輸入單詞轉換爲(word,1)
wordDS.map((_,1))
- FlatMap 壓平,DataStream → DataStream
//輸入的一行字符串按照空格切分單詞
dataStream.flatMap(_.split(" "))
- Filter 過濾,DataStream → DataStream
//過濾出對2取餘等於0的數字
dataStream.filter(_ % 2 == 0)
- KeyBy 分組
//計算wordCount,按照單詞分組,這裏的0指的是tuple的位數,因爲(word,1)這類新的流,按照word分組,而word就是第0位
wordDS.map((_,1)).keyBy(0)
- reduce 聚合
//對上述KeyBy後的(word,count)做聚合,合併當前的元素和上次聚合的結果,實現了wordCount
wordDS.map((_,1)).keyBy(0).reduce{
(s1,s2) =>{
(s1._1,s1._2 + s2._2)
}
}
關於雙流join與窗口的算子我在下一站會着重說,這一篇先了解一些常用的簡單的API目的就達到了。
函數
在flink中,對數據處理,除了上述一些簡單的轉換算子外,還經常碰到一些無法通過上述算子解決的問題,於是就需要我們自定義實現UDF函數
關於UDF,UDTF,UDAF
UDF:User Defined Function,用戶自定義函數,一進一出
UDAF:User- Defined Aggregation Funcation 用戶自定義聚合函數,多進一出
UDTF: User-Defined Table-Generating Functions,用戶定義表生成函數,用來解決輸入一行輸出多行
UDF函數類
其實在我們常用的算子如map、filter等都暴露了對應的接口,可以自定義實現:
舉例如map:
val StuDS: DataStream[Stu] = kafkaDS.map(
//在內部我們可以自定義實現MapFunction,從而實現類型的轉換
new MapFunction[ObjectNode, Stu]() {
override def map(value: ObjectNode): Stu = {
JSON.parseObject(value.get("value").toString, classOf[Stu])
}
}
)
富函數Rich Functions
除了上述的函數外,使用多的就還有富函數Rich Functions,所有Flink函數類都有其Rich版本。它與常規函數的不同在於,可以獲取運行環境的上下文,並擁有一些生命週期方法, open,close,getRuntimeContext,和 setRuntimeContext,所以可以實現更復雜的功能,比如累加器和計算器等。
那我們來簡單實現一個累加器
累加器是具有加法運算和最終累加結果的簡單結構,可在作業結束後使用。最簡單的累加器是一個計數器:您可以使用Accumulator.add(V value)方法將其遞增 。在工作結束時,Flink將彙總(合併)所有部分結果並將結果發送給客戶端。
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.api.common.accumulators.IntCounter
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.configuration.Configuration
/**
* @description: ${description}
* @author: Liu Jun Jun
* @create: 2020-06-12 17:55
**/
object AccumulatorTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
import org.apache.flink.streaming.api.scala._
val dataDS = env
.readTextFile("input/word.txt")
//.socketTextStream("bigdata101", 3456)
val resultDS: DataStream[String] = dataDS.map(new RichMapFunction[String, String] {
//第一步:定義累加器
private val numLines = new IntCounter
override def open(parameters: Configuration): Unit = {
super.open(parameters)
//註冊累加器
getRuntimeContext.addAccumulator("num-lines", this.numLines)
}
override def map(value: String): String = {
this.numLines.add(1)
value
}
override def close(): Unit = super.close()
})
resultDS.print("單詞輸入")
val jobExecutionResult = env.execute("單詞統計")
//輸出單詞個數
println(jobExecutionResult.getAccumulatorResult("num-lines"))
}
}
注意:這個案例中,我使用的是有限流,原因是這個累加器的值只有在最後程序的結束的時候才能打印出來,或者是可以直接在Flink UI中體現。
那麼如何實現隨時打印打印出累加器的值呢?那就需要我們自定義實現累加器了:
而實現自定義的累加器我還沒寫完。。。。。
Sink
那麼當我們通過flink對數據處理結束後,要把結果數據放到相應的數據存放點,也就是sink了,方便後續通過接口調用做報表統計。
那麼數據放哪裏呢?
- ES
- redis
- Hbase
- MYSQL
- kafka
ES sink
關於ES的介紹,我也發過一篇文章,只不過是入門級別的,有需要的可以看看,貼鏈接如下:ES最新版快速入門詳解
來吧,貼代碼,注意看其中的註釋
import org.apache.flink.api.common.functions.RuntimeContext
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.datastream.DataStream
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.elasticsearch.{ElasticsearchSinkFunction, RequestIndexer}
import org.apache.flink.streaming.connectors.elasticsearch6.ElasticsearchSink
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer011
import org.apache.http.HttpHost
import org.elasticsearch.action.index.IndexRequest
import org.elasticsearch.client.Requests
/**
* @description: ${description}
* @author: Liu Jun Jun
* @create: 2020-06-01 11:44
**/
object flink2ES {
def main(args: Array[String]): Unit = {
// 1.獲取執行環境
val env: StreamExecutionEnvironment =
StreamExecutionEnvironment.getExecutionEnvironment
//2. 設置並行度爲2
env.setParallelism(2)
//3. 設置關於kafka數據源的配置,主題,節點,消費者組,序列化,消費offset形式
val topic = "ctm_student"
val properties = new java.util.Properties()
properties.setProperty("bootstrap.servers", "bigdata101: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")
// 從kafka中獲取數據
val kafkaDS: DataStream[String] =
env.addSource(
new FlinkKafkaConsumer011[String](
topic,
new SimpleStringSchema(),
properties) )
//添加ES連接
val httpHosts = new java.util.ArrayList[HttpHost]()
httpHosts.add(new HttpHost("bigdata101", 9200))
//創建ESSink對象,在其中對數據進行操作
val esSinkBuilder = new ElasticsearchSink.Builder[String](
httpHosts, new ElasticsearchSinkFunction[String] {
def createIndexRequest(element: String): IndexRequest = {
val json = new java.util.HashMap[String, String]
json.put("data", element)
return Requests.indexRequest()
.index("ws")
.`type`("readingData")
.source(json)
}
//重寫process方法,對輸入的數據進行處理,runtimeContext爲上下文環境,requestIndexer爲操作index的對象
override def process(t: String, runtimeContext: RuntimeContext, requestIndexer: RequestIndexer): Unit = {
//在add方法中,參數可以爲:增、刪、改、行動請求
requestIndexer.add(createIndexRequest(t))
println("saved successfully")
}
})
//測試時這句代碼一定要寫,意思時:每一個請求都進行刷新
//否則在測試的適合,kafka生產幾條數據,但ES中卻查不到,默認ES是5000條消息刷新一次,這就涉及到了ES的架構中索引的刷新頻率,下面會寫相應的配置。
esSinkBuilder.setBulkFlushMaxActions(1)
//真正將數據發送到ES中
kafkaDS.addSink(esSinkBuilder.build())
//觸發執行
env.execute()
}
}
我們在上面的代碼中,初步的通過kafka來獲取數據,然後直接寫到了ES中,但模擬的只是執行單個索引請求,我們在日常的生產中,肯定不是說一次請求刷新一次,這對ES來說,壓力太大了,所以會有批量提交的配置。
- bulk.flush.max.actions:刷新前要緩衝的最大操作數。
- bulk.flush.max.size.mb:刷新前要緩衝的最大數據大小(以兆字節爲單位)。
- bulk.flush.interval.ms:刷新間隔,無論緩衝操作的數量或大小如何。
對於ES現在的版本,還支持配置重試臨時請求錯誤的方式:
- bulk.flush.backoff.enable:如果刷新的一個或多個操作由於臨時原因而失敗,是否對刷新執行延遲退避重試EsRejectedExecutionException。
- bulk.flush.backoff.type:退避延遲的類型,可以是CONSTANT或EXPONENTIAL
- bulk.flush.backoff.delay:延遲的延遲量。對於恆定的退避,這只是每次重試之間的延遲。對於指數補償,這是初始基準延遲。
- bulk.flush.backoff.retries:嘗試嘗試的退避重試次數
Redis Sink
關於Redis大家應該很熟悉了,我們來模擬一下數據處理結束後存入Redis,我這裏模擬的是redis單點。
注意:因爲我們是在IDE中要訪問遠程的redis,所以redis的redis.conf配置文件中,需要修改2個地方:
- 註釋 bind 127.0.0.1,否則只能安裝redis的本機連接,其他機器不能訪問
- 關閉保護模式
protected-mode no
然後就可以啓動redis啦,這裏再把幾個簡單的命令貼一下,做到全套服務,哈哈
-
啓動redis服務:redis-server /usr/local/redis/redis.conf
-
進入redis:
進入的命令:redis-cli
指定IP:redis-cli -h master102
redis中文顯示問題:redis-cli -h master -raw(也就是多加一個 -raw)
多個Redis同時啓動,則需指定端口號訪問 redis-cli -p 端口號 -
關閉redis服務:
- 單實例關閉
如果還未通過客戶端訪問,可直接 redis-cli shutdown
如果已經進入客戶端,直接 shutdown即可. - 多實例關閉
指定端口關閉 redis-cli -p 端口號 shutdown
- 單實例關閉
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.datastream.DataStream
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer011
import org.apache.flink.streaming.connectors.redis.RedisSink
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfig
import org.apache.flink.streaming.connectors.redis.common.mapper.{RedisCommand, RedisCommandDescription, RedisMapper}
/**
* @description: ${連接單節點redis測試}
* @author: Liu Jun Jun
* @create: 2020-06-12 11:23
**/
object flink2Redis {
def main(args: Array[String]): Unit = {
// 轉換
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(2)
val topic = "test1"
val properties = new java.util.Properties()
properties.setProperty("bootstrap.servers", "bigdata101: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")
// 從kafka中獲取數據
val kafkaDS: DataStream[String] =
env.addSource(
new FlinkKafkaConsumer011[String](
topic,
new SimpleStringSchema(),
properties) )
kafkaDS.print("data:")
val conf = new FlinkJedisPoolConfig.Builder().setHost("bigdata103").setPort(6379).build()
kafkaDS.addSink(new RedisSink[String](conf, new RedisMapper[String] {
override def getCommandDescription: RedisCommandDescription = {
new RedisCommandDescription(RedisCommand.HSET,"sensor")
}
override def getKeyFromData(t: String): String = {
t.split(",")(0)
}
override def getValueFromData(t: String): String = {
t.split(",")(1)
}
}))
env.execute()
}
}
flink to Hbase
有的時候,還需要把Flink處理過的數據寫到Hbase,那我們也來簡單試試。
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
/**
* @description: ${flink to Hbase}
* @author: Liu Jun Jun
* @create: 2020-05-29 17:53
**/
object flink2Hbase {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
import org.apache.flink.streaming.api.scala._
val stuDS = env
.socketTextStream("bigdata101",3456)
.map(s => {
//這裏我創建了一個student樣例類,只有name和age
val stu: Array[String] = s.split(",")
student(stu(0),stu(1).toInt)
})
//這裏我們模擬的比較簡單,沒有對數據進行處理,直接寫入到Hbase,這個HBaseSink是自己寫的類,往下看
val hBaseSink: HBaseSink = new HBaseSink("WordCount","info1")
stuDS.addSink(hBaseSink)
env.execute("app")
}
}
case class student(name : String,age : Int)
/**
* @description: ${封裝Hbase連接}
* @author: Liu Jun Jun
* @create: 2020-06-01 14:41
**/
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.{RichSinkFunction, SinkFunction}
import org.apache.hadoop.hbase.{HBaseConfiguration, HConstants, TableName}
import org.apache.hadoop.hbase.client._
import org.apache.hadoop.hbase.util.Bytes
class HBaseSink(tableName: String, family: String) extends RichSinkFunction[student] {
var conn: Connection = _
//創建連接
override def open(parameters: Configuration): Unit = {
conn = HbaseFactoryUtil.getConn()
}
//調用
override def invoke(value: student): Unit = {
val t: Table = conn.getTable(TableName.valueOf(tableName))
val put: Put = new Put(Bytes.toBytes(value.age))
put.addColumn(Bytes.toBytes(family), Bytes.toBytes("name"), Bytes.toBytes(value.name))
put.addColumn(Bytes.toBytes(family), Bytes.toBytes("age"), Bytes.toBytes(value.age))
t.put(put)
t.close()
}
override def close(): Unit = {
}
}
好了,到這裏flink的一些基本流處理API已經差不多說完了,但是關於flink特別重要的 窗口、精準一次性、狀態編程、時間語義等重點還沒說,所以下一篇關於flink的文章就再聊聊這些關鍵點。