今天早上在羣裏有人問了這樣一個問題,我當時只看了截圖沒看他的代碼,然後我倆在那聊了半天,最後發現不在一個頻道,後面我仔細看了一下,他的代碼明白了他的邏輯,我先簡單描述一下場景,他在Flink流開始的時候直接把原數據分別sink到了es和gp庫,然後把處理過的流拿了兩個測流輸出,最後分別把這兩個測流sink到了es和gp,相當於他一共有4個sink,但是他在Flink的UI上面看到的DAG圖是這樣的,如下圖所示
他就很鬱悶,不應該是4個sink嗎,怎麼只顯示了2個,而且他是先做了2個sink,然後處理完,最後又做了兩個sink,他想要的效果是這樣的,如下圖所示
跟DAG的圖顯示完全不一樣,其實並不是DAG顯示錯了,也不是他的代碼有問題,是因爲他不熟悉Flink的operator chain,Flink默認是開啓operator chain的,他會將多個operator,串在一起作爲一個operator chain來執行,這樣可以提高程序的性能,那怎麼才能讓DAG顯示成他想要的效果呢,其實很簡單,有兩種方法,第一在最後一個算子的地方(sink前面)改變算子的併發(和前面的併發不一樣),第二是最後一個算的地方,調用disableChaining方法就可以了,下面看一個demo
package flink
import java.sql
import java.sql.Connection
import java.util.Properties
import org.apache.commons.dbcp2.BasicDataSource
import org.apache.flink.api.common.functions.RuntimeContext
import org.apache.flink.streaming.api.scala._
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.collector.selector.OutputSelector
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction
import org.apache.flink.streaming.api.scala.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.client.Requests
import org.elasticsearch.common.xcontent.XContentType
/**
* Flink消費多個topic的數據,sink到不同的表
*/
object MoreTopic {
private val broker = "***"
private val group_id = "***"
private val topic = "***"
def main(args: Array[String]): Unit = {
//獲取流的環境;
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(8)
val properties = new Properties()
properties.setProperty("bootstrap.servers", broker)
properties.setProperty("group.id", group_id)
// 設置動態監測topic和paritition的變化
properties.setProperty("flink.partition-discovery.interval-millis","1000")
// 用正則匹配符合條件的多個topic
val consumer = new FlinkKafkaConsumer011[String](java.util.regex.Pattern.compile("jason-test-[0-9]"), new SimpleStringSchema, properties)
// 設置從最新的處開始消費
consumer.setStartFromLatest()
val datastream = env.addSource(consumer)
.filter(_.nonEmpty)
.flatMap(_.split(","))
.map(_.trim.toString)
datastream.map((_,1)).disableChaining()
.addSink(new MySQLSink_Topic("a"))
.name("mysql_sink_before_a")
datastream.map((_,1)).disableChaining()
.addSink(new MySQLSink_Topic("b"))
.name("mysql_sink_before_b")
// 拆分流,把流拆分成一個包含hello,一個包含jason的流
val split_ds = datastream
.rebalance
.split(new OutputSelector[String]{
override def select(value: String) = {
val list = new java.util.ArrayList[String]()
if(value.contains("hello")){
list.add("h")
}else{
list.add("j")
}
list
}
})
// 如果想要後面的4個sink在DAG圖顯示並行,需要在最後一個算子上禁用operator chain,或者設置一個和上面不一樣的並行度
val h_ds = split_ds.select("h").map((_,1)).setParallelism(1)
val j_ds = split_ds.select("j").map((_,1)).setParallelism(1)
h_ds.addSink(new MySQLSink_Topic("a")).name("h_ds_mysql_sink")
j_ds.addSink(new MySQLSink_Topic("b")).name("j_ds_mysql_sink")
val httpHosts = new java.util.ArrayList[HttpHost]()
// http:port 9200 tcp:port 9300
httpHosts.add(new HttpHost("***", 0, "http"))
httpHosts.add(new HttpHost("***", 0, "http"))
httpHosts.add(new HttpHost("***", 0, "http"))
val elasticsearchSink = new ElasticsearchSinkFunction[(String,Int)] {
override def process(element: (String,Int), ctx: RuntimeContext, indexer: RequestIndexer) {
val json = new java.util.HashMap[String, String]()
json.put("name", element._1)
json.put("count", element._2.toString)
println("要寫入es的內容: " + element)
val r = Requests.indexRequest.index("**").`type`("**").source(json,XContentType.JSON)
indexer.add(r)
}
}
val esSinkBuilder = new ElasticsearchSink.Builder[(String,Int)](httpHosts, elasticsearchSink)
// 設置刷新前緩衝的最大算子操作量
esSinkBuilder.setBulkFlushMaxActions(1)
h_ds.addSink(esSinkBuilder.build()).name("h_ds_es-sink")
j_ds.addSink(esSinkBuilder.build()).name("j_ds_es-sink")
env.execute("Multiple Topic Multiple Sink")
}
}
/**
* 把結果保存到mysql裏面
*/
class MySQLSink_Topic(table: String) extends RichSinkFunction[(String,Int)] with Serializable {
var connection: sql.Connection = _
var ps: sql.PreparedStatement = _
var statement: java.sql.Statement = _
val username = "***"
val password = "***"
val drivername = "***"
val url = "***"
private var conn: Connection = _
/**
* 打開mysql的連接
* @param parameters
*/
override def open(parameters: Configuration): Unit = {
conn = getConn(new BasicDataSource)
val sql = "insert into "+ table +"(name,count) values(?,?)"
println(sql)
ps = conn.prepareStatement(sql)
}
/**
* 處理數據後寫入mysql
* @param value
*/
override def invoke(row: (String,Int)): Unit = {
ps.setString(1,row._1)
ps.setLong(2,row._2)
ps.execute()
}
/**
* 關閉mysql的連接
*/
override def close(): Unit = {
if (ps != null) {
ps.close()
}
if (connection != null) {
connection.close()
}
}
/**
* 創建mysql的連接池
* @param ds
* @return
*/
def getConn(ds: BasicDataSource): Connection = {
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://master:3306/test");
ds.setUsername("mysql");
ds.setPassword("12345678");
ds.setInitialSize(10);
ds.setMaxTotal(15);
ds.setMinIdle(10);
var con: Connection = null
con = ds.getConnection
con
}
}
我直接拿之前的代碼改了一下,我的兩個sink是es和mysql,後面的邏輯和他稍微不同,我這裏用的是分流,他用的是測流輸出,當然我所有的數據都是sink到同一個表的,所以前面加了map處理數據的結構,所以我一共有6個sink,看下面的DAG圖
這裏把最下面的map和後面的兩個sink去掉就和他要的效果一樣了,到這大家應該明白怎麼回事了.
這裏再說兩個小細節方面的東西,先看一張下面的圖片
大家可能對這裏有疑問爲什麼我只有8個slot,卻能運行74個task,這裏的74代表的是task數,就是你所有的operator的併發的總數,也就是上面DAG裏面的8*9+1+1
還有一個小問題是記錄數的問題,如下圖所示
這個地方很多同學也有疑惑,說爲什麼我明明有數據,這裏的Records sent
顯示爲0呢,是因爲print或者是sink的sent就是0,只有received,因爲他們沒有向下一個算子發送數據.
如果有寫的不對的地方,歡迎大家指正,如果有什麼疑問,可以加QQ羣:340297350,更多的Flink和spark的乾貨可以加入下面的星球