Lua集成Kafka实现
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by User.
--- DateTime: 2020/2/27 10:00
---
--[[
脚本对接kafka
]]
-- 指定数据采集脚本运行线程的阈值,若超过这个阈值,则不再采集数据
local DEFAULT_THREADS = 1000
-- broker list
local BROKER_LIST = {
{host = "*.*.*.*",port = 9092},
{host = "*.*.*.*",port = 9092},
{host = "*.*.*.*",port = 9092}
}
-- 设置分区数 分区数一定要和kafka中topic分区数一致
local PARTRTION_NUM = 3
-- kafka中的topic
local TOPIC = "nginxdata"
-- 设置连接参数
local CONNECTION_PARAMS = {
{producer_type = "async"},
{socket_timeout = 3000},
{flush_time = 10000},
{request_timeout = 20000}
}
--[[
指定分区的过程,不指定分区,默认使用默认分区
]]
-- 自定义共享内存的key(nginx的共享内存)
local POLLING_KEY = "POLLING_KEY"
-- 获取nginx的共享内存
local shared_data = ngx.shared.shared_data
-- 从共享内存中获取POLLING_KEY对应的value值
local POLLING_VAL = shared_data : get(POLLING_KEY)
-- 对获取到的value值进行判断
if not value then
POLLING_VALUE = 1
shared_data:set(POLLING_KEY,POLLING_VALUE)
end
-- 计算消息要分发到哪个分区中
local PARTITION_ID = tonumber(POLLING_VALUE) % PARTRTION_NUM
-- 自增key的控制 POLLING_KEY 所对应的value值自增一,使得该key对应的value能够均匀分布到不同的分区中
shared_data:incr(POLLING_KEY,1)
-- 并发控制 次数控制数据采集与否
local isGone = true
-- 获取ngx.var.connections_active进行过载保护,若当前的活跃连接数大于阈值,进行限流保护
if tonumber(nginx.var.connections_active) > tonumber(DEFAULT_THREADS) then
isGone = false
end
-- 数据的采集 采集后的数据拼接起来,作为message传到kafka中
if isGone then
local time_local = ngx.var.time_local
if time_local == nil then
time_local = ""
end
local request = ngx.var.request
if request == nil then
request = ""
end
local request_method = ngx.var.request_method
if request_method == nil then
request_method = ""
end
local content_type = ngx.var.content_type
if content_type == nil then
content_type = ""
end
ngx.req.read_body()
local request_body = ngx.var.request_body
if request_body == nil then
request_body = ""
end
local http_referer = ngx.var.http_referer
if http_referer == nil then
http_referer = ""
end
local remote_addr = ngx.var.remote_addr
if remote_addr == nil then
remote_addr = ""
end
local http_user_agent = ngx.var.http_user_agent
if http_user_agent == nil then
http_user_agent = ""
end
local time_iso8601 = ngx.var.time_iso8601
if time_iso8601 == nil then
time_iso8601 = ""
end
local server_addr = ngx.var.server_addr
if server_addr == nil then
server_addr = ""
end
local http_cookie = ngx.var.http_cookie
if http_cookie == nil then
http_cookie = ""
end
--封装数据,将数据封装成message
local message = time_local .. "#CS#" .. request .. "#CS#" ..
request_method .. "#CS#" .. content_type .. "#CS#" ..
request_body .. "#CS#" .. http_referer .. "#CS#" ..
remote_addr .. "#CS#" .. http_user_agent .. "#CS#" ..
time_iso8601 .. "#CS#" .. server_addr .. "#CS#" .. http_cookie;
-- 导包 引入kafkaProducer
local pro = require "resty.kafka.producer"
-- 获取producer实例 将数据从lua中的缓存写入到kafka,相对于kafka来说,lua的缓存相当于生产者
-- local cli = client:new(broker_list, producer_config)
local producer = pro:new(BROKER_LIST,CONNECTION_PARAMS)
-- 发送数据
-- function _M.send(self, topic, key, message)
local ok,error = producer:send(TOPIC,PARTITION_ID,message)
if not ok then
ngx.log(ngx.ERR,"kafka send err",error)
end
end
脚本具体的执行步骤见反爬虫项目Day3。
在kafka的某一个节点上启动消费者,消费指定分区nginxdata,即可验证是否数据成功导入到kafka中。
实现消费kafka数据过程
lua脚本将数据写入到kafka中后,就可以通过sparkStreaming或者flink来消费数据,将数据进行清洗,脱敏(过滤敏感词),分类,解析,加工,结构化后将数据再次写入到kafka中,之后再进行相依的业务逻辑的处理。
Streaming消费kafka两种方式
Streaming消费kafka有两种方式,分别为receiver和direct
Receiver方式是通过 KafkaUtils.createStream() 方法来创建一个DStream对象,它不关注消费位移的处理,Receiver方式的结构如下图所示。但这种方式在Spark任务执行异常时会导致数据丢失的情况,如果要保证数据的可靠性,需要开启预写式日志,简称WAL(Write Ahead Logs),只有接收到的数据被持久化到WAL之后才会去更新Kafka中的消费位移。接收到的数据和WAL存储位置信息被可靠地存储,如果期间出现故障,这些信息被用来从错误中恢复,并继续处理数据。
WAL的方式可以保证从Kafka中接收的数据不被丢失。但是在某些异常情况下,一些数据被可靠地保存到了WAL中,但是还没有来得及更新消费位移,这样会造成Kafka中的数据被Spark拉取了不止一次。同时在Receiver方式中,Spark中的RDD分区和Kafka的分区并不是相关的,因此增加Kafka中主题的分区数并不能增加Spark处理的并行度,而仅是增加接收器接收数据的并行度。
Direct方式是从Spark1.3开始引入的,它通过KafkaUtils.createDirectStream()方法创建一个DStream对象,该方式中Kafka的一个分区与Spark RDD对应,通过定期扫描所订阅Kafka每个主题的每个分区的最新偏移量以确定当前批处理数据偏移范围。与Receiver方式相比,Direct方式不需要维护一份WAL数据,由Spark Streaming程序自己控制位移的处理,通常通过检查点机制处理消费位移,这样可以保证Kafka中的数据只会被Spark拉取一次。
这种方法相较于Receiver方式的优势在于:
简化的并行:在Receiver的方式中我们提到创建多个Receiver之后利用union来合并成一个Dstream的方式提高数据传输并行度。而在Direct方式中,Kafka中的partition与RDD中的partition是一一对应的并行读取Kafka数据,这种映射关系也更利于理解和优化。
高效:在Receiver的方式中,为了达到0数据丢失需要将数据存入Write Ahead Log中,这样在Kafka和日志中就保存了两份数据,浪费!而第二种方式不存在这个问题,只要我们Kafka的数据保留时间足够长,我们都能够从Kafka进行数据恢复。
精确一次:在Receiver的方式中,使用的是Kafka的高阶API接口从Zookeeper中获取offset值,这也是传统的从Kafka中读取数据的方式,但由于Spark Streaming消费的数据和Zookeeper中记录的offset不同步,这种方式偶尔会造成数据重复消费。而第二种方式,直接使用了简单的低阶Kafka API,Offsets则利用Spark Streaming的checkpoints进行记录,消除了这种不一致性。
Streaming Receiver Demo
package sparkStreaming.day2
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* 基于0.8的receiver
*/
object Demo11_ReceiverDemo {
def main(args: Array[String]): Unit = {
//获取Streaming上下文的环境
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("streaming-receiver")
val sc: StreamingContext = new StreamingContext(conf, Seconds(5))
//设置检查点,保存状态 一般:放到hdfs中;不需要手动创建
sc.checkpoint("检查点目录") //此处用到checkPoint是因为下边用到了 updateStateByKey算子
//获取kafka的数据流
//返回值:DStream of (Kafka message key, Kafka message value)
val kafkaStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(
sc,
"master:2181,slave1:2181,slave2:2181/kafka",
"kafka-streaming",
Map("streaming" -> 1)
)
//数据处理
//_._2.split(" ") tuples中的第二个是value 第一个是key
val tuples: DStream[(String, Int)] = kafkaStream.flatMap(_._2.split(" ")).map((_, 1))
//自定义更新函数
val updateFunction = (value: Seq[Int], state: Option[Int]) => {
val sum = value.sum + state.getOrElse(0)
Option(sum)
}
val sumed: DStream[(String, Int)] = tuples.updateStateByKey(updateFunction)
//将结果打印到控制台
sumed.print()
//开启采集器
sc.start()
//等待采集
sc.awaitTermination()
}
}
Streaming Direct Demo
package sparkStreaming.day2
import kafka.serializer.StringDecoder
import org.apache.spark.{HashPartitioner, SparkConf}
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* 基于0.8的Direct
*/
object Demo12_DirectDemo {
def main(args: Array[String]): Unit = {
//获取streaming上下文环境
val conf: SparkConf = new SparkConf().setAppName("streaming-direct").setMaster("local[*]")
val sc: StreamingContext = new StreamingContext(conf, Seconds(5))
//设置检查点,保存状态 一般:放到hdfs中;不需要手动创建
sc.checkpoint("检查点目录") //此处用到checkPoint是因为下边用到了 updateStateByKey算子
//获取数据流
val kafkaStream: InputDStream[(String, String)] = KafkaUtils.createDirectStream[String,String,StringDecoder,StringDecoder](
sc,
Map[String, String]("metadata.broker.list" -> "master:9092,slave1:9092,slave2:9092"),
Set("streaming")
)
//统计
//@return DStream of (Kafka message key, Kafka message value)
val tuples: DStream[(String, Int)] = kafkaStream.flatMap(_._2.split(" ")).map((_, 1))
//自定义更新函数
val updateFunction = (it : Iterator[(String, Seq[Int], Option[Int])]) =>{
it.map(x=>{
(x._1,x._2.sum + x._3.getOrElse(0))
})
}
val sumed: DStream[(String, Int)] = tuples.updateStateByKey(updateFunction, new HashPartitioner(sc.sparkContext.defaultMinPartitions), true)
//将结果输出到控制台
sumed.print()
//开启采集器
sc.start()
//等待采集
sc.awaitTermination()
}
}
对于消费offset的维护,"enable.auto.commit"该属性设置为true时,自动提交,offset维护在kafka中的topic中(0.9版本之后,0.8版本是维护在zookeeper中的)
消费kafka中的数据-Direct方法
package com.cxl.streamingMonitor.dataprocess.launch
import java.lang
import com.cxl.streamingMonitor.common.util.jedis.PropertiesUtil
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
/**
* 数据处理主类
*/
object DataProcessLaunch {
def main(args: Array[String]): Unit = {
//当应用被停止的时候,进行如下设置可以保证当前批次执行完之后再停止应用。
System.setProperty("spark.streaming.stopGracefullyOnShutdown", "true")
//初始化spark环境
val conf: SparkConf = new SparkConf().setAppName("DataProcessLaunch").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
// new StreamingContext(conf,Seconds(5))
//请求kafka的参数
// consumer grouping
val groupid : String = "nginxdata"
//kafka params
val kafkaparams: Map[String, Object] = Map[String, Object](
// 指定消费kafka的ip和端口
"bootstrap.servers" ->
PropertiesUtil.getStringByKey("default.brokers", "kafkaConfig.properties"),
// 设置kafka的解码方式 反射的方式
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> groupid,
// 从头消费 从最开始
"auto.offset.reset" -> "earliest",
//自动提交false
"enable.auto.commit" -> (false: lang.Boolean)
)
//topic
val topics: Set[String] = Set(PropertiesUtil.getStringByKey("source.nginx.topic", "kafkaConfig.properties"))
//创建流处理方法
val ssc: StreamingContext = setupSsc(sc, kafkaparams, topics)
ssc.start()
ssc.awaitTermination()
}
/**
*
* @param sc
* @param kafkaparams
* @param topics
* @return
*/
def setupSsc(sc: SparkContext, kafkaparams: Map[String, Object], topics: Set[String]): StreamingContext = {
//创建streamingContext
val ssc: StreamingContext = new StreamingContext(sc, Seconds(3))
//消费kafka数据
val kafkaStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topics, kafkaparams)
)
kafkaStream.map(_.value()).foreachRDD(rdd => rdd.foreach(println))
ssc
}
}
未完待续·····