大数据知识整理01——Kafka SparkStreaming Redis实时流计算

标签 :Java Scala Hadoop Spark Redis Kafka

作者 : Maxchen

版本 : V2.0.2

日期 : 2020/4/27


应用架构说明

采集数据通过producer生产者实时将数据发送至kafka集群,kafka集群针对此类数据专门创建一个topic和多个分区,将数据落地到每一个broker节点。

除了生产者之外,我们在此topic基础上新增一个消费者组(同一数据通过消费者组实现多端多次消费),基于Kafka SparkStreaming技术实时消费数据并实时流计算。

将消费者应用打成jar包,通过Spark-submit部署到集群中运行,由几台机器协同计算,提高效率同时降低单点故障。最终将计算结果写入redis,用于实时大屏展示和分析。

kafka性能测试拓扑图(新).png-97.5kB

应用详细说明

实时服务会从Kafka消费数据,将数据读出来并进行实时分析,这里选择Spark Streaming,因为Spark Streaming提供了与Kafka整合的内置支持,经过Spark Streaming实时计算程序分析,将结果写入Redis,访问Redis可以实时获取计算后的指标数据。

运行环境

  • Redhat 6.5/CentOS 6.5
  • JDK 1.8.0_144
  • CDH 5.12.1
  • Kafka 2.2.0
  • Hadoop 2.6.0
  • ZooKeeper 3.4.5
  • Spark 2.4.0
  • Redis 5.0.3
  • Scala 2.11.8
  • IntelliJ IDEA 2018.1.8

程序依赖包版本

目前选用的程序依赖包版本均从Cloudera官方镜像中获取,运行稳定,组件包含但不限于:
1、kafka_2.11-0.10.2-kafka-2.2.0
2、spark-streaming_2.11-2.4.0.cloudera2
3、spark-streaming-kafka-0-10_2.11-2.4.0.cloudera2
4、spark-redis-2.3.0
5、jedis-2.9.0

计算前的数据样本

计算前的数据基于物联网技术采集,最后采集回来的数据统一处理成json格式。

{
	"TIME": "20200426133629",
	"FXXXID": "XX",
	"WXXID": "XX",
	"xxx1": "1235.30",
	"xxx2": "531237.33",
	"xxx3": "125.91",
	"xxx4": "0.00",
	"xxx5": "1251.61",
	……
}

计算后的数据样本

通过Kafka SparkStreaming实时流计算将上述数据转码且指标重计算,得到如下的json数据。

{
  "TIME": "20200426134536",
  "FXXXID": "XX",
  "WXXID": "XX",
  "Gxxx91": "2512.71",
  "Gxx101": "163.90",
  "Gxx121": "124565.00",
  "Gxx131": "123213.00",
  "Gxx141": "4562.90",
  "Gxx151": "123.14",
  ……
}

代码详细说明

代码分为几大部分:
第一部分: 用配置文件src/main/resources/*.properties保存kafka、redis、spark连接信息以及此次应用的数据转换规则。随后配置加载类LoadProperties.scala,加载配置文件所有信息。
第二部分: kafka连接配置类KafkaScadaConsumer.scala,连接kafka的broker,将读取到的kafka数据序列化,定义消费主题和消费者组,偏移设置等等。
第三部分: redis连接配置类RedisUtils.scala,连接redis,每次spark streaming任务会创建一个redispool。
第四部分: spark连接配置类SparkScadaContext.scala,设置spark运行的任务名称,指定Spark管理节点。
第五部分: 主程序入口KafkaToRedisMain.scala,初始化kafka、redis和spark,从kafka读取数据并转换为数据流,将数据流读取到Spark的rdd中并读取数据转换规则,放入spark集群中实时计算,最后将计算结果保存至redis。

第一部分:实时流任务配置

Kafka到redis的实时流程序配置位于src/main/resources/kafka2redis.properties

# kafka连接设置
# kafka broker连接
bootstrap.servers = test2:9092,test3:9092,test4:9092
# topic名称
topic = test
# group名称
group.id = group
# 偏移设置
auto.offset.reset = latest

# redis连接配置
# redis连接主机
redis.host = 
# redis端口号
redis.port = 6379
# redis密码
redis.password = 
# redis连接超时时间
redis.timeout = 60000

# spark应用配置
# spark应用名称
spark.app.name = test
# spark的master地址
spark.master = local[*]

# 数据流计算及转换规则(如果需要添加变量,则直接在末尾添加外部系统编码即可,多个外部系统编码用逗号隔开,后期可以灵活扩展)
push.data.code = Gxx091,Gxx101,Gxx121,Gxx131,Gxx141,Gxx151,Gxx161,Gxx201,Gxx211,Gxx221,Gxx231,Gxx241,Gxx251,Gxx261,Gxx271,Gxx281,Gxx291,Gxx301,Gxx311,Hx1082,Hx2242,Hx2252,Hx2282,Hx2932,Hx2942,Hx3232,Hx2222,Hx2362,Hx2272,Hx2292,Hx1342,Hx1672,Hx2742,Hx3192,Hx1642,Hx1062,Hx2702,Hx3202,Hx1632,Hx1652,Hxx972,Hx1002,Hx1012,Hx1022,Hxx992,Hxx982,Hx1032,Hx1042,Hx1052,Hx1092,Hx1102,Hx1122,Hx1112,Hx3102,Hx1152,Hx1162,Hx1172,Hx1142,Hx1072,Hx1662,Hx1252,Hx1262,Hx1272,Hx1242,Hx2792,Hx2802,Hx2862,Hx2882,Hx2832,Hx2872,Hx1332,Hx1312,Hx2822,Hx2852,Hx2892,Hx2842,Hx2812,Hx1302,Hx1322,Hxx441,Hxx511,Hxx521,Hxx531

加载上述配置信息:1、读取配置文件目录;2、读取配置文件中每一个properties的配置信息(包括kafka连接、redis连接、spark应用、转换规则)。

package com.maxchen.application.util

import java.io.InputStream
import java.util.Properties

object LoadProperties {

  //加载properties配置文件,文件要放到src/main/resources文件夹下
  val properties = new Properties()

  /**
    * Thread.currentThread().getContextClassLoader方法只在IDEA中可以运行,提交到Spark Job会运行失败,日志提示无法获取*.properties文件
    * 改成this.getClass().getResourceAsStream即可正常运行
    */
  //val path = Thread.currentThread().getContextClassLoader.getResource("kafka2redis.properties").getPath
  //properties.load(new FileInputStream(path))
  val ipstream : InputStream = this.getClass().getResourceAsStream("/kafka2redis.properties")
  properties.load(ipstream)

  // kafka连接设置
  val brokers = properties.getProperty("bootstrap.servers")
  val topic = properties.getProperty("topic")
  val groupId = properties.getProperty("group.id")
  val autoOffsetReset = properties.getProperty("auto.offset.reset")

  // redis连接配置
  val redisHost = properties.getProperty("redis.host")
  val redisPort = properties.getProperty("redis.port")
  val redisPassword = properties.getProperty("redis.password")
  val redisTimeout = properties.getProperty("redis.timeout")

  // spark应用配置
  val sparkAppName = properties.getProperty("spark.app.name")
  val sparkMaster = properties.getProperty("spark.master")

  // 数据流计算及转换规则配置
  val pushDataCode = properties.getProperty("push.data.code")

}

第二部分:kafka连接配置

1、连接Kafka的Broker;
2、自定义{key->value}数据序列化方法;
3、设置消费者组;
4、Kafka偏移设置,latest从分区最新的位置开始读取(数据保持最新,但是可能不连续),earliest从分区最早的位置(起始位置)开始读取(数据连续,但是要持续消费旧数据才能轮到最新数据)。

package com.maxchen.application.kafka

import com.maxchen.application.util.LoadProperties
import org.apache.kafka.common.serialization.StringDeserializer

// 生成KafkaScadaConsumer对象
object KafkaScadaConsumer {

  val brokers = LoadProperties.brokers
  val kafkaParams = Map[String,Object](

    // Kafka的Broker连接
    "bootstrap.servers" -> brokers,

    // 自定义数据序列化
    "key.deserializer" -> classOf[StringDeserializer],
    "value.deserializer" -> classOf[StringDeserializer],

    // 消费者组设置
    "group.id" -> LoadProperties.groupId,

    // 偏移设置
    // "latest"从分区最新的位置开始读取
    // "earliest"从分区最早的位置(起始位置)开始读取
    "auto.offset.reset" -> LoadProperties.autoOffsetReset,
    "enable.auto.commit" -> (true: java.lang.Boolean)

  )

  // kafka topic 设置
  val topic = Array(LoadProperties.topic)

}

第三部分:redis连接配置

提示: 针对之前redis断联后服务停止的情况,这里运行lazy懒加载的机制,每次spark streaming任务会创建一个redispool,而不是只创建一次,所以每次用完要手动close掉,不会增加redis连接数,并且redis发生断联时可以不断重试。

package com.maxchen.application.redis

import com.maxchen.application.util.LoadProperties
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
import redis.clients.jedis.JedisPool

object RedisUtils extends Serializable {

  // 读取redis的host以及port
  private val host = LoadProperties.redisHost
  private val port = Integer.valueOf(LoadProperties.redisPort)

  //修复redis连接问题Caused by: java.lang.NoClassDefFoundError: redis/clients/jedis/JedisPool

  // 配置每一个线程连接redis的时长,超时连接将会释放,缓解redis服务器压力
  private val timeout = Integer.valueOf(LoadProperties.redisTimeout)


  //private val poolConfig = new GenericObjectPoolConfig()
  //lazy val pool = new JedisPool(host,port)
  // 运行lazy懒加载的机制,每次spark streaming任务会创建一个redispool,而不是只创建一次,所以每次用完要手动close掉,不会增加redis连接数,并且redis发生断联时可以不断重试
  lazy val pool = new JedisPool(new GenericObjectPoolConfig(), host, port, timeout)

  // 每次执行完一个spark streaming任务之后要将redispool释放
  lazy val hooks = new Thread(){
    override def run(): Unit ={
      //println("Execute hook thread: " + this)
      pool.destroy()
    }
  }
}

第四部分:spark连接配置

kafka到redis通过spark streaming进行实时流计算,任务提交到spark集群并分配给多台机器执行,单台机器挂掉不会影响任务的停止,具备高可用性。

package com.maxchen.application.spark

import com.maxchen.application.util.LoadProperties
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}

//生成sparkStreming对象
object SparkScadaContext {

  // 指定spark任务的master和app名称
  val conf = new SparkConf().setAppName(LoadProperties.sparkAppName).setMaster(LoadProperties.sparkMaster)
  val ssc = new StreamingContext(conf,Seconds(10))

}

第五部分:实时流计算程序入口

从kafka中生成数据流,以rdd放入spark streaming内存流计算,最后得到的结果实时写入redis。

package com.maxchen.application

import com.maxchen.application.kafka.KafkaScadaConsumer
import com.maxchen.application.redis.RedisUtils
import com.maxchen.application.spark.SparkScadaContext
import com.maxchen.application.util.LoadProperties
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.codehaus.jettison.json.JSONObject

import scala.collection.JavaConverters._

/**
  * KafkaToRedis主程序入口
  */
object KafkaToRedisMain {

  def main(args: Array[String]): Unit = {

    //从kafka中生成dStream
    val dStream = KafkaUtils.createDirectStream(
      SparkScadaContext.ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String,String](KafkaScadaConsumer.topic,KafkaScadaConsumer.kafkaParams)
    )

    val pushDataCodeList = LoadProperties.pushDataCode.split(",")

    //转换json,并将结果存放到redis中
    val dbIndex = 0
    dStream.foreachRDD(RDD=>
      RDD.foreachPartition(x=>
        x.foreach{records =>
          val record = new JSONObject(records.value())
          //          println(record)
          val scalaMap = scala.collection.mutable.LinkedHashMap[String,String]()
          scalaMap.put("TIME",record.getString("TIME"))
          scalaMap.put("FXXXID",record.getString("FXXXID"))
          scalaMap.put("WXXID",record.getString("WXXID"))
          for(pushDataCodes <- pushDataCodeList){
            try {
              scalaMap.put(pushDataCodes,record.getString(pushDataCodes.substring(1, 5)))
            } catch {
              case e => record.put(pushDataCodes.substring(1, 5),"")
            }
          }

          //println(map.asJava.values())
          //获取redis对象
          val jedis = RedisUtils.pool.getResource
          //redis密码
          jedis.auth(LoadProperties.redisPassword)
          //选择数据库(0-15)
          jedis.select(dbIndex)

          //val jedis = RedisUtils.getConnections()

          //xxxxdKey
          val livedKey = scalaMap.asJava.get("FXXXID") + "_" + scalaMap.asJava.get("WXXID") + "_xxxxd"
          jedis.set(livedKey,scalaMap.map{ case (k, v) => (k,v) }.asJava.toString)

          //XXRKey
          val errKey = scalaMap.asJava.get("FXXXID") + "_" + scalaMap.asJava.get("WXXID") + "_XXR"
          val errValue = scalaMap.asJava.get("TIME") + "," + scalaMap.asJava.get("HX1352") + "," + scalaMap.asJava.get("HX2212")
          jedis.set(errKey,errValue)

          RedisUtils.pool.returnResource(jedis)

        }
      )
    )

    SparkScadaContext.ssc.start()
    SparkScadaContext.ssc.awaitTermination()

  }
}

程序部署指南

打包工具采用IDEA,首先配置打包的信息(为了控制好jar包的大小,这里建议将多余的jar全部删除,保留下图标识的几个核心文件即可):
1、进入Project Structure……设置项目参数;
2、将Directory Content定位到resources目录;
3、将META-INF定位到src目录下;

image.png-290.6kB

image.png-291.9kB

image.png-302.5kB

image.png-304.7kB

4、找到IDEA的Build Artifacts,点进去后再点build。

image.png-470.5kB

image.png-323kB

5、打包好之后会生成以下程序包,lib表示运行spark streaming所需要的jar包,*.properties为程序运行的配置文件,maxchen_kafka_to_redis.jar为这次程序运行的主体。

image.png-269.4kB

6、最后我们将程序包部署到测试集群,用以下命令部署到spark:

spark2-submit \
--master yarn \
--class com.maxchen.application.KafkaToRedisMain \
--jars maxchen_kafka_to_redis.jar \
lib/metrics-core-2.2.0.jar \
lib/spark-streaming-kafka-0-10_2.11-2.4.0.cloudera2.jar \ 
lib/spark-streaming_2.11-2.4.0.cloudera2.jar \ 
lib/kafka_2.11-0.10.2-kafka-2.2.0.jar \
lib/jedis-2.9.0.jar \
lib/commons-pool2-2.4.2.jar

运行报错解析

1、部署后的程序无法读取*.properties

运行报错的代码:

/**
这种获取配置的方法在idea中可以正常运行,但是打包成jar以后,在服务器中运行会提示找不到*.properties配置文件
*/
val path = Thread.currentThread().getContextClassLoader.getResource("kafka2redis.properties").getPath

image.png-200.5kB

建议改为:

val ipstream : InputStream = this.getClass().getResourceAsStream("/kafka2redis.properties")

2、Spark2-submit提交流计算应用报错

image.png-113.5kB
image.png-116.6kB

这是spark服务器缺少jar包依赖造成的,我们需要将以下几个jar包添加到/opt/cloudera/parcels/SPARK2-2.4.0.cloudera2-1.cdh5.13.3.p0.1041012/lib/spark2/jars目录:
lib/metrics-core-2.2.0.jar
lib/spark-streaming-kafka-0-10_2.11-2.4.0.cloudera2.jar
lib/spark-streaming_2.11-2.4.0.cloudera2.jar
lib/kafka_2.11-0.10.2-kafka-2.2.0.jar
lib/jedis-2.9.0.jar
lib/commons-pool2-2.4.2.jar

3、Spark连接Redis时报错

运行报错的代码:

lazy val pool = new JedisPool(host,port)

image.png-95.9kB

这是JedisPool与Spark依赖冲突造成的,修改成Spark支持的GenericObjectPoolConfig即可:

lazy val pool = new JedisPool(new GenericObjectPoolConfig(), host, port, timeout)

4、Spark连接kafka报错

image.png-817.8kB

这是因为spark对应的kafka版本没有设置好:

image.png-22.4kB

我们目前使用的kafka版本为0.10,因此这里改为0.10后不再报此错误:

image.png-21.8kB

5、部分Spark Streaming任务执行时间过长

部分计算节点挂掉会导致任务的执行时间变长,秒级计算的时间会顺延。由于kafka的topic 设置的偏移值为latest,会影响数据的连续性(秒级数据会跳变到后几秒),实时性不会受影响,这并不会影响Spark任务的正常运行,计算节点恢复后会重新恢复连续性。

image.png-411.1kB

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值