Elasticsearch在flink中的应用

Flink数据下沉到Elasticsearch示例

简介

  当初做课程设计的时候,找到的flink接入elasticsearch的文章除了flink的文档示例之外版本都挺老的,所以自己按照flink的文档把原来的改造了一下。现在再更新最新版本,然后做一下整理。

  最新版本flink1.12,scala2.12.7,elasticsearch7.10.1,kibana7.10.1。

flink1.10更新至1.12说明

  在开始之前需要提一下flink和es更新之后的一些问题。

1.flink1.11的更新

  提交任务的时候报了IllegalStateException: No ExecutorFactory found to execute the application.原因是缺少flink-clients的依赖。
在这里插入图片描述
意思就是说,从flink1.11开始,flink-streaming-java这个模块不再有flink-clients的依赖了,如果项目需要这个依赖就要在maven的pom.xml文件里自己加上去。

<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_2.12</artifactId>
            <version>${flink.version}</version>
</dependency>

2.flink1.12的更新

  更新了一些过时的方法。

2.1timeWindow过时更新

在这里插入图片描述
timeWindow方法过时了,以后都用window(windowAssigner)这个方法。windowAssigner就是几种window的类型,如TumblingEventTimeWindows,SlidingEventTimeWindows,TumblingProcessingTimeWindows,SlidingProcessingTimeWindows。

2.2默认TimeCharacteristic设置为event time

在这里插入图片描述
以前本来要env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime),从flink1.12开始这句话就不用了。

2.3清理了过时的DataStream#fold()方法

在这里插入图片描述

fold()永远地从api列表里清楚了,以后就用reduce吧。这两个的区别也不是很大,就是fold可以设置一个累加器初始值然后累加,而reduce是两个元素合并。

任务说明

  这个demo来源于https://github.com/dataartisans/flink-streaming-demo。它是flink的官方DataStream APi的demo,不过版本非常老旧,flink0.10.0版本,es1.7.1版本。当初的课程设计是升级到flink1.10版本,并能够把输出下沉到es,然后用kibana可视化显示结果。现在flink已经来到了1.12,那么这个demo也就要更新到最新版。

  这个demo有三个任务,如下:

1.识别流量大的区域

TotalArrivalCount.scala识别纽约市流量大的区域。它接收出租车搭乘事件的流,并计算每个地点租车到达的人数。

2.识别过去十五分钟流量大的区域

SlidingArrivalCount.scala 识别过去十五分钟流量大的区域。它会收集出租车搭乘记录,并每隔5分钟计算出过去15分钟内到达每个地点的人数。这种类型的计算称为滑动窗口(sliding window)。

3.计算流量大的区域之前的流量

一些流处理用例依赖于及时的事件聚合,例如发送通知或警报。 EarlyArrivalCount.scala 扩展了我们之前的滑动窗口应用程序。和之前那个程序一样,它每五分钟计算一次在过去15分钟内到达每个地点的人数。此外,当50人到达一个位置时,它会发出一个早期的部分计数,也就是说,如果超过50、100、150(等等)人到达一个位置,它会发出一个更新的计数。

  另外比较关键的是,大数据分析中比较重要的东西就是模型,一个好的模型可以让我们分析数据做到事半功倍。官方已经为我们提供了一个数据模型TaxiRide。

rideId: Long // 每次搭乘的唯一id
time: DateTime // 开始、结束事件的时间戳
isStart: Boolean // 搭乘是否开始
location: GeoPoint // 上车、下车的经纬度
passengerCnt: short // 乘客数量
travelDist: float // 总的距离,如果是搭乘开始事件,那么值为-1

我们将通过这个模型,去达成这三个任务。那么,接下来就开始吧。

任务一:TotalArrivalCount

  先给出流程图。
在这里插入图片描述

    //设置执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    //添加数据源
    val rides: DataStream[TaxiRide] = env.addSource(new TaxiRideSource(
      data, maxServingDelay, servingSpeedFactor))

    val cleansedRides = rides
      //过滤掉开始事件
      .filter(new FilterFunction[TaxiRide] {
        override def filter(t: TaxiRide): Boolean = !t.isStart
      })
      //过滤掉不在纽约市的事件
      .filter(new FilterFunction[TaxiRide] {
        override def filter(t: TaxiRide): Boolean = NycGeoUtils.isInNYC(t.location)
      })

    //map算子转换为计算需要的格式cell Id, timestamp, passenger count
    val cellIds: DataStream[(Int, Long, Short)] = cleansedRides
      .map { new MapFunction[TaxiRide,(Int, Long, Short)] {
        override def map(t: TaxiRide):
        (Int, Long, Short) = (NycGeoUtils.mapToGridCell(t.location),
          t.time.getMillis,
          t.passengerCnt)
        }
      }

    val passengerCnts: DataStream[(Int, Long, Int)] = cellIds
      //设cell id为键转为KeydStream
      .keyBy(a => a._1)
      //计算每个cell id的搭乘人数和
      .reduce((r, s) => (s._1, r._2.max(s._2), (r._3 + s._3).toShort))
      .map(r => (r._1, r._2, r._3.toInt))

      //map算子转换回GeoPoint输出格式
    val cntByLocation: DataStream[(Int, Long, GeoPoint, Int)] = passengerCnts
      .map( r => (r._1, r._2, NycGeoUtils.getGridCellCenter(r._1), r._3 ) )

    //打印
    cntByLocation
      .print()

    if (writeToElasticsearch) {
      //下沉到ES,这个后面会单独讲
      cntByLocation
        .addSink(new CntTimeByLocUpsert(elasticsearchHost, elasticsearchPort))
    }

    env.execute("Total passenger count per location")

任务二:SlidingArrivalCount

在这里插入图片描述

    //map算子转换为计算需要的格式cell Id, passenger count
    val cellIds: DataStream[(Int, Short)] = cleansedRides
      .map(new MapFunction[TaxiRide, (Int, Short)] {
        override def map(t: TaxiRide): (Int, Short) = (NycGeoUtils.mapToGridCell(t.location), t.passengerCnt)
      })

    val passengerCnts: DataStream[(Int, Long, Int)] = cellIds
      .keyBy(a => a._1)
      //设置滑动窗口
      .window(SlidingEventTimeWindows.of(Time.minutes(countWindowLength),
        Time.minutes(countWindowFrequency)))//数据处理,窗口里面的元素根据cell id合并,在窗口结束时返回
      .process(new ProcessWindowFunction[(Int, Short), (Int, Long, Int), Int, TimeWindow] {
        override def process(key: Int,
                             context: Context,
                             elements: Iterable[(Int, Short)],
                             out: Collector[(Int, Long, Int)]):
        Unit = out.collect((key, context.window.getEnd, elements.map(_._2).sum))
      })

任务三:EarlyArrivalCount

在这里插入图片描述

val passengerCnts: DataStream[(Int, Long, Int)] = cellIds
      .keyBy(a => a._1)
      .window(SlidingEventTimeWindows.of(Time.minutes(countWindowLength),
        Time.minutes(countWindowFrequency)))
   	  //注册触发器
      .trigger(new EarlyCountTrigger(earlyCountThreshold))
      .process(new ProcessWindowFunction[(Int, Short), (Int, Long, Int), Int, TimeWindow] {
        override def process(key: Int,
                             context: Context,
                             elements: Iterable[(Int, Short)],
                             out: Collector[(Int, Long, Int)]):
        Unit = out.collect((key, context.window.getEnd, elements.map(_._2).sum))
      })
    
    class EarlyCountTrigger(triggerCnt: Int) extends Trigger[(Int, Short), TimeWindow] {
    val stateDescriptor = new ValueStateDescriptor[Integer]("personCnt", Integer.TYPE)

    override def onElement(
                            event: (Int, Short),
                            timestamp: Long,
                            window: TimeWindow,
                            ctx: TriggerContext): TriggerResult = {

      //注册timer
      ctx.registerEventTimeTimer(window.getEnd)

      //获取当前状态,这个状态就是指乘客的数量
      val personCnt = ctx.getPartitionedState(stateDescriptor)
      //更新乘客数量
      personCnt.update(personCnt.value() + event._2)
      if (personCnt.value() < triggerCnt) {
        //没达到要求继续
        TriggerResult.CONTINUE
      }
      else {
        //到达要求的乘客数量(50)
        personCnt.update(0)
        TriggerResult.FIRE
      }
    }

    override def onEventTime(
                              time: Long,
                              window: TimeWindow,
                              ctx: TriggerContext): TriggerResult = {

      //触发器完成计算
      TriggerResult.FIRE_AND_PURGE
    }

    override def onProcessingTime(
                                   time: Long,
                                   window: TimeWindow,
                                   ctx: TriggerContext): TriggerResult = {

      throw new UnsupportedOperationException("I am not a processing time trigger")
    }

    //状态清除
    override def clear(w: TimeWindow, triggerContext: TriggerContext): Unit = {
      triggerContext.getPartitionedState(stateDescriptor).clear()
    }
  }

Elasticsearch与Kibana

  完成了上面的任务,接下来讨论如何将结果下沉到ES。我们需要定义一个ElasticsearchSink,它继承自RichSinkFunction。例子程序如下:

abstract class ElasticsearchUpsertSink[T](host: String, port: Int, cluster: String, index: String)
  extends RichSinkFunction[T] {
  //使用http协议的rest client,也有使用tcp的transport client,不过已经弃用了
  private var client: RestHighLevelClient = _

  def insertJson(record: T): Map[String, AnyRef]

  def updateJson(record: T): Map[String, AnyRef]

  def indexKey(record: T): String

  @throws[Exception]
  override def open(parameters: Configuration) {

    client = new RestHighLevelClient(RestClient
      .builder(new HttpHost("localhost", 9200)))
  }

  @throws[Exception]
  override def invoke(value: T, context: SinkFunction.Context): Unit = {
    //更新文档

    //文档不存在就新增
    val indexRequest = new IndexRequest(index)
      .id(indexKey(value))
      .source(mapAsJavaMap(insertJson(value)))

    //存在文档就更新
    val updateRequest = new UpdateRequest(index, indexKey(value))
      .doc(mapAsJavaMap(updateJson(value)))
      .upsert(indexRequest)

    client.update(updateRequest, RequestOptions.DEFAULT).getResult
  }
}

github上面也给出了es的操作指南,但是版本比较低,下面给出es7的操作。

首先启动es,创建索引:

curl -XPUT "http://localhost:9200/nyc-idx"

需要注意的是,ES7以上的版本已经去除了type,在提供的api里面,原来的type()方法也标注过时了,所以操作如下:

curl -XPUT "http://localhost:9200/nyc-idx/_mapping" -d'
{
   "properties" : {
     "cnt": {"type": "integer"},
     "location": {"type": "geo_point"},
     "time": {"type": "date"}
   }
}'

在demo里开启es。这里要注意的就是ES的端口号。ES9200用于http协议,9300用于tcp协议以及ES集群内部的通讯。之前如果用的是TransportClient做es sink,那么端口应该使用9300。

    // Elasticsearch parameters
    val writeToElasticsearch = true // set to true to write results to Elasticsearch
    val elasticsearchHost = "127.0.0.1" // look-up hostname in Elasticsearch log output
    val elasticsearchPort = 9200

然后运行程序就能输出了。如果要清除数据的话,输入:

curl -XDELETE 'http://localhost:9200/nyc-idx'

  但是光光elasticsearch不太好看这个结果是怎么样的,这里就需要用到kibana。启动kibana,配置之前创建的索引,配置数据的时间范围选择2013-01-01到2013-01-06的绝对时间范围以找到数据。

结果应该会如下图所示:
在这里插入图片描述

flink1.12文档地址 https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/

flink1.12release note https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/release-notes/flink-1.12.html

elasticsearch https://www.elastic.co/cn/elastic-stack

kibana https://www.elastic.co/cn/kibana

原官方demo地址 https://github.com/dataArtisans/flink-streaming-demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值