Flink的operator chain引发的问题

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章