标签 :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消费数据
,将数据读出来并进行实时分析,这里选择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目录下;
4、找到IDEA的Build Artifacts,点进去后再点build。
5、打包好之后会生成以下程序包,lib表示运行spark streaming所需要的jar包,*.properties为程序运行的配置文件,maxchen_kafka_to_redis.jar
为这次程序运行的主体。
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
建议改为:
val ipstream : InputStream = this.getClass().getResourceAsStream("/kafka2redis.properties")
2、Spark2-submit提交流计算应用报错
这是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)
这是JedisPool与Spark依赖冲突造成的,修改成Spark支持的GenericObjectPoolConfig
即可:
lazy val pool = new JedisPool(new GenericObjectPoolConfig(), host, port, timeout)
4、Spark连接kafka报错
这是因为spark对应的kafka版本没有设置好:
我们目前使用的kafka版本为0.10,因此这里改为0.10后不再报此错误:
5、部分Spark Streaming任务执行时间过长
部分计算节点挂掉会导致任务的执行时间变长,秒级计算的时间会顺延。由于kafka的topic 设置的偏移值为latest,会影响数据的连续性(秒级数据会跳变到后几秒),实时性不会受影响,这并不会影响Spark任务的正常运行,计算节点恢复后会重新恢复连续性。