1. 小小的知识点
1.kafka 发布与订阅的分布式(distributed)消息系统,让实时数据在之中排队,缓冲减压,存储实时数据
2.消息队列的应用场景:峰值压力缓冲
3.producer 数据的生产者
consumer 消息的消费者
broker kafka集群的server负责处理消息读写请求,存储数据
topic 消息队列,读写数据的单位
4.一个topic分成多个partition(为了做分布式),分区内部消息强有序分区之间是无序的,每个消息都有一个序号叫offset
5.一个partition中对应一个broker,一个broker可以管理多个partition(理解为一个磁盘文件)
6.消息不经过内内存,直接写入文件,对磁盘文件进行追加
7.根据时间策略删除数据,而不是消费完就删除,7天删除
8.生产者自己决定往哪一个分区内写数据,轮询写或者hash分区
9.消费者自己维护消费到哪个offset 顺序读写 0拷贝 批量处理
10.去中心化的架构,没有页面
11.同一个分区的副本不能放在同一个节点中,副本数不能大于Kafka节点数
2. Kafka搭建
2.1 上传解压修改环境变量
# 解压
tar -xvf kafka_2.11-1.0.0.tgz
# 配置环境变量
vim /etc/profile
export KAFKA_HOME=/usr/local/soft/kafka_2.11-1.0.0
export PATH=$PATH:$KAFKA_HOME/bin
source /etc/profile
2.2 修改配置文件
vim config/server.properties
broker.id=0 每一个节点broker.id 要不一样
zookeeper.connect=master:2181,node1:2181,node2:2181
log.dirs=/usr/local/soft/kafka_2.11-1.0.0/data 数据存放的位置
2.3 将kafka文件同步到node1,node2
# 同步kafka文件
scp -r kafka_2.11-1.0.0/ node1:`pwd`
scp -r kafka_2.11-1.0.0/ node2:`pwd`
# 将master中的而环境变量同步到node1和node2中
scp /etc/profile node1:/etc/
scp /etc/profile node2:/etc/
# 在ndoe1和node2中执行source
source /etc/profile
2.4 修改node1和node2中的broker.id
vim config/server.properties
# node1
broker.id=1
# node2
broker.id=2
2.5 启动kafka
# 1、需要先穷zookeeper, kafka使用zo保存元数据
# 需要在每隔节点中执行启动的命令
zkServer.sh start
# 查看启动的状体
zkServer.sh status
# 2、启动kafka,每个节点中都要启动(去中心化的架构)
# -daemon后台启动
kafka-server-start.sh -daemon /usr/local/soft/kafka_2.11-1.0.0/config/server.properties
kafka-server-stop.sh -daemon /usr/local/soft/kafka_2.11-1.0.0/config/server.properties
3. 使用Kafka
3.1 创建topic
--replication-factor ---每一个分区的副本数量, 同一个分区的副本不能放在同一个节点,副本的数量不能大于kafak集群节点的数量
--partition --分区数, 根据数据量设置
--zookeeper zk的地址,将topic的元数据保存在zookeeper中
命令:
kafka-topics.sh --create --zookeeper master:2181,node1:2181,node2:2181 --replication-factor 3 --partitions 3 --topic cars
在生产和消费数据时,如果topic不存在会自动创建一个分区为1,副本为1的topic
3.2 查看topic描述信息
kafka-topics.sh --describe --zookeeper master:2181,node1:2181,node2:2181 --topic student
3.3 获取所有topic
__consumer_offsetsL kafka 用于保存消费偏移量的topic(不要用)
kafka-topics.sh --list --zookeeper master:2181,node1:2181,node2:2181
3.4 创建控制台生产者
kafka-console-producer.sh --broker-list master:9092,node1:9092,node2:9092 --topic cars
3.5 创建控制台消费者
--from-beginning 从头消费 如果不在执行消费的新的数据
kafka-console-consumer.sh --bootstrap-server master:9092,node2:9092,node2:9092 --from-beginning --topic cars
4. Kafka保存数据的方式
# 1、保存的文件
/usr/local/soft/kafka_2.11-1.0.0/data
# 2,每一个分区每一个副本对应一个目录
# 3、每一个分区目录中可以有多个文件, 文件时滚动生成的,编号会往后加
00000000000000000000.log
00000000000000000001.log
00000000000000000002.log
# 4、滚动生成文件的策略
文件大小
log.segment.bytes=1073741824
时间
log.retention.check.interval.ms=300000
# 5、文件删除的策略,默认是7天,以文件为单位删除
log.retention.hours=168
# 6、节点挂了后重启会进行副本数据同步
5. flink消费和生产kafka案例
5.1 消费kafka中数据
导入依赖:(读什么数据导入什么依赖)
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka</artifactId>
<version>${flink.version}</version>
</dependency>
object Demo5kafka {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment =StreamExecutionEnvironment.getExecutionEnvironment
val source: KafkaSource[String] = KafkaSource
.builder[String]()
.setBootstrapServers("master:9092,node1:9092,node2:9092") //kafka集群broker列表
.setTopics("test_topic2") //指定topic
.setGroupId("my-group") //指定消费者组,一条数据在一个组内只被消费一次
.setStartingOffsets(OffsetsInitializer.earliest()) //读取数据的位置,earliest:读取所有的数据,latest:读取最新的数据
.setValueOnlyDeserializer(new SimpleStringSchema()) //反序列的类
.build
//使用kafka source
val kafkaDS: DataStream[String] = env.fromSource(source, WatermarkStrategy.noWatermarks(), "Kafka Source")
kafkaDS.print()
env.execute()
}
}
5.2 生产数据到kafka
读取数据将DataStream保存到Kafka2中:
有唯一一次和至少一次两种模式
object Demo6KafkaSInk {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
val studentDS: DataStream[String] = env.readTextFile("data/students.json")
// 将数据保存到kafka中 --- kafka sink
// DeliveryGuarantee.EXACTLY_ONCE: 唯一一次
// DeliveryGuarantee.AT_LEAST_ONCE: 至少一次,默认
val sink: KafkaSink[String] = KafkaSink
.builder[String]()
.setBootstrapServers("master:9092,node1:9092,node2:9092") //broker地址
.setRecordSerializer(
KafkaRecordSerializationSchema
.builder[String]()
.setTopic("students_json") //topic
.setValueSerializationSchema(new SimpleStringSchema())
.build())
//.setDeliverGuarantee(DeliveryGuarantee.AT_LEAST_ONCE) //唯一一次
.build()
//使用kafka sink
studentDS.sinkTo(sink)
env.execute()
}
}
6. 通过JavaAPI生产数据
6.1 单个单词生产数据示例(控制台消费)
object Demo1KafkaProducer {
def main(args: Array[String]): Unit = {
/**
* 创建生产者
*/
val properties=new Properties()
//指定Kafka的broker地址
properties.setProperty("bootstrap.servers","master:9092,node1:9092,node2:9092")
//设置key value的序列化类
properties.setProperty("key.serializer","org.apache.kafka.common.serialization.StringSerializer")
properties.setProperty("value.serializer","org.apache.kafka.common.serialization.StringSerializer")
val producer=new KafkaProducer[String,String](properties)
/**
* 向指定的Kafka中生产数据
*/
val record=new ProducerRecord[String,String]("test_topic2","flink")
//生产者向消费者发送数据
producer.send(record)
producer.flush()
producer.close()
}
}
6.2 读取学生表批量生产数据(控制台消费)
//读取学生表将数据批量写入Kafka中
val studentList: List[String] =Source.fromFile("data/students.txt").getLines().toList
//for循环批量写入数据
for(student<-studentList){
val record = new ProducerRecord[String, String]("student", student)
producer.send(record)
producer.flush()
}
producer.close()
6.3 创建JavaAPI消费者(API自产自消)
earliest
当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
latest 默认
当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产认值生的该分区下的数据
none
topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
--------------------------------------------------------------------------------------
object Demo3StudentConsumer {
def main(args: Array[String]): Unit = {
/**
* 创建消费者
*/
val properties = new Properties()
//指定broker的地址
properties.setProperty("bootstrap.servers","master:9092,node1:9092,node2:9092")
//添加key和value的反序列化类,消费者组,读取数据的位置
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("group.id", "asdasd")
properties.setProperty("auto.offset.reset", "earliest")
val consumer = new KafkaConsumer[String, String](properties)
/**
* 订阅一个topic
*/
val topics=new util.ArrayList[String]()
topics.add("student")
consumer.subscribe(topics)
/**
* 消费数据
*/
while(true){
print("正在消费")
val consumerRecords: ConsumerRecords[String, String] =
consumer.poll(Duration.ofSeconds(2))
//解析数据
val records: lang.Iterable[ConsumerRecord[String, String]] = consumerRecords.records("student")
val iterRecord: util.Iterator[ConsumerRecord[String, String]] =records.iterator()
while(iterRecord.hasNext){
val record: ConsumerRecord[String, String] = iterRecord.next()
val topic: String = record.topic()
val offSet: Long = record.offset() //数据偏移量
val key: String = record.key() //数据的key,未指定时为null
val value: String = record.value() //保存的数据
val ts: Long = record.timestamp() //时间戳,默认为存入的时间
println(s"$topic\t$offSet\t$key\t$value\t$ts")
}
}
}
}
7. Canal 实时采集数据
1.阿里云开源的数据采集工具,功能单一
2.Canal监控MySQL的binarylog将其变更的数据(插入删除更新)采集后以json格式将数据存到Kafka中
8. Flume(测试)
指定flume配置文件,监控文件(目录,端口),将采集到的数据保存到hdfs,kafka中(示例:控制台消费者监控)
1.传配置文件,指定相应配置文件启动flume(会占用命令行,出现start说明成功启动)
flume-ng agent -n agent -f ./FlumeToKafka.properties -Dflume.root.logger=DEBUG,console
2.创建监控的文件,打数据
mkdir /usr/flume
echo 'java' >> /usr/flume/log.log
3.控制台消费
kafka-console-consumer.sh --zookeeper master:2181,node1:2181,node2:2181 --from-beginning --topic flume
9. process (Flink底层王者级算子)
9.1不用窗口版示例(只有Java的语法)
示例:读一个socket,计算后统计到控制台
1.process 一行行处理数据,可以返回一行或多行,可以替代map,filter,flatMap,是一个底层的函数
2.processFunction是一个抽象的类,需要实现之,所以匿名内部类重processElement方法:将DS中的数据一条条传给pe,再其中进行处理,可以输出多条数据,相当于flatMap(还可以重写open() close() 方法)
3.重写后的三个参数:
i 相当于一行数据
context相当于上下文对象 (context.timestamp() 可以获取到时间)
collector 用于数据发送到下游
代码:
object ProcessFun {
def main(args: Array[String]): Unit = {
//构建flink环境,设置并行度,连接一个socket
val env: StreamExecutionEnvironment =StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val linesDS: DataStream[String] = env.socketTextStream("master", 8888)
val processDS: DataStream[(String, Int)] =linesDS
.process(new ProcessFunction[String,(String,Int)] {
override def processElement(i: String,
context: ProcessFunction[String, (String, Int)]#Context,
collector: Collector[(String, Int)]): Unit = {
val clazz: String =i.split(",")(4)
collector.collect((clazz,1))
}
})
processDS.print()
env.execute()
}
}
9.2 keyBy之后使用process算子
说明:将单词数量和次数放在一个可变的HashMap中,从context中拿到key,返回集合中的单词数量,有则次数加一后更新集合后返回,没有则先返回0再加一后更新集合再返回,使用put方法更新单词数量,收集数据后发送给下游
object KeyByProcess {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val linesDS: DataStream[String] = env.socketTextStream("master", 8888)
val wordsDS: DataStream[String] =linesDS.flatMap(line=>line.split(","))
val kvDS: DataStream[(String, Int)] =wordsDS.map(word=>(word,1))
val keyByDS: KeyedStream[(String, Int), String] =kvDS.keyBy(kv=>kv._1)
val countDS: DataStream[(String, Int)] =keyByDS
.process(new KeyedProcessFunction[String,(String,Int),(String,Int)]{
val countMap: mutable.Map[String, Int] =new mutable.HashMap[String,Int]()
override def processElement(i: (String, Int),
context: KeyedProcessFunction[String, (String, Int), (String, Int)]#Context,
collector: Collector[(String, Int)]): Unit = {
val key: String =context.getCurrentKey
//从map中获取单词的数量,如果有就返回,如果没有就返回0
var count: Int =countMap.getOrElse(key,0)
//做一个累计
count += 1
//更新map中单词的数量
countMap.put(key,count)
//将数据发送到下游
collector.collect(key,count)
}
})
countDS.print()
env.execute()
}
}
9.3 window中的process