今天早上在群里有人问了这样一个问题,我当时只看了截图没看他的代码,然后我俩在那聊了半天,最后发现不在一个频道,后面我仔细看了一下,他的代码明白了他的逻辑,我先简单描述一下场景,他在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的干货可以加入下面的星球