一. Kafka 入门
1. 介绍
Kafka是由Apache开发的一个开源流处理平台,由Scala和Java编写。目标是为处理实时数据提供一个统一、高吞吐、低延迟的平台。其持久化层本质上是一个“按照分布式事务日志架构的大规模发布/订阅消息队列”, 这使它作为企业级基础设施来处理流式数据非常有价值。(维基百科)
2. kafka特性
-
高吞吐量,低延迟: 每个topic可以拥有多个partition,被同一消费者组下的消费者同时消费。
-
持久性:消息可以持久化到本地磁盘,并且支持数据备份。
-
容错性:消息存储在kafka集群中分为多个partition,每个partition可以拥有多个副本(leader)。
-
可扩展性:支持集群动态扩展
3. 使用场景
-
日志收集
-
消息系统
二. 架构及原理
1. 名词解释
-
Topic:Kafka将消息种子(Feed)分门别类,每一类的消息称之为一个主题(Topic)
-
Partition: 每个Topic中的消息会被分为若干个分区(Partition)
-
Producer: 发布消息的对象称之为主题生产者(Kafka topic producer)
-
Consumer: 订阅消息并处理发布的消息的种子的对象称之为主题消费者(consumer)
-
Consumer Group: 消息的消费群组, 拥有一个或者多个消费者(consumer)
-
Broker: 已发布的消息保存在一组服务器中,称之为Kafka集群。集群中的每一个服务器都是一个代理(Broker)。消费者(Consumer)可以订阅一个或多个主题(topic),并从Broker拉数据,从而消费这些已发布的消息。
-
offset: 消费者消费进度,由消费者组管理,保存在Zookeeper上。
2. Kafka与Zookeeper的关系
-
每个Broker上线的时候,都会到Zookeeper上进行注册,然后创建临时节点,当Broker下线时,临时节点会被删除。
-
Zookeeper不仅存储了Kafka的内部元数据,而且记录了消费组的成员列表、分区的消费进度(offset)、分区的所有者消费者要消费哪些分区的消息由消费组来决定,因为消费组管理所有的消费者,所以它需要知道集群中所有可用的分区和所有存活的消费者,才能执行分区分配算法,而这些信息都需要保存到ZK中。每个消费者都要在Zookeeper的消费组节点下注册对应的消费者节点,在分配到不同的分区后,才会开始各自拉取分区的消息。
3. 分区(Partition)的原理
消息的分区被分布到集群中的多个服务器上。每个服务器处理它分到的分区。 根据配置每个分区还可以复制到其它服务器作为备份容错。每个分区有一个leader,零或多个follower。Leader处理此分区的所有的读写请求,而follower被动的复制数据。如果leader宕机,其它的一个follower会被推举为新的leader。一台服务器可能同时是一个分区的leader,另一个分区的follower。这样可以平衡负载,避免所有的请求都只让一台或者某几台服务器处理。
4. 消息的生产及储存过程
Kafka储存的消息来自于生产者(Producer), 生产者将消息发布到指定的主题(Topic),同时也可以指定发布到主题的某个分区(Partition)中。
首先判断消息是否指定了分区;其次判断消息的是否有key,如果有,则根据key的hash进行分区;否则进行随机分配(轮询)。
Producer从Zookeeper中找到节点的leader信息,然后将消息发送给leader,leader将消息写入本地log,follower从leader pull(拉)消息,成功后写入本地log。
同步模式/异步模式: request.requred.ack=0(不应答) || 1(leader落盘应答) || -1(follower落盘应答) //可以通过初始化producer时的producerconfig进行配置
无论消息是否被消费,kafka 都会保留所有消息。有两种策略可以删除旧数据:
① 基于时间:log.retention.hours=168。
② 基于大小:log.retention.bytes=1073741824
5. 消息的消费模式及过程
① 所有的消费者同属于一个组,则类似于queue模式, 消息会在消费者之间进行负载均衡。
② 所有的消费者都具有不同的组,则是发布订阅模式,消息会广播到每个消费者中。
消费者(Consumer)通过订阅主题(Topic),主动从集群的分区中拉取消息进行消费。一个分区(Partition)只能被同一消费者组中的一个消费者进行消费,避免消息的重复消费。消费者可以消费多个分区(即分区消费完后可以继续消费没有被同组消费者消费过的分区)。对于一个topic, 消费者组中的消费者数不能多于分区数,否则意味着有一些消费者无法接收到消息。
如果一个消费者宕机了,分配给这个消费者的分区需要重新分配给相同组的其他消费者;由于offset是以消费者组为单位进行维护,且保存在Zookeeper上,不关心哪个消费者在消费。所以能够保证消费消息是上一个消费者消费到地方。
6. 分区策略(partition.assignment.strategy)
Range策略是对每个主题而言的,首先对同一个主题里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。然后将partitions的个数除于消费者线程的总数来决定每个消费者线程消费几个分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。
将所有主题的分区组成TopicAndPartition列表,然后对 TopicAndPartition 列表按照 hashCode 进行排序,然后轮询分配(你一个我一个)。使用RoundRobin策略有两个前提条件必须满足:
① 同一个Consumer Group里面的所有消费者的num.streams必须相等。
② 每个消费者订阅的主题必须相同
- 当以下事件发生时,会进行一次分区分配:
- 同一个消费者组里新增消费者
- 消费者离开所在组(shuts down/ crashes)
- 订阅主题新增分区
7. 文件存储机制:
在Kafka文件存储中,同一个topic下有多个不同的partition,每个partiton为一个目录,partition的名称规则为:topic名称+序序号,第一个序号从0开始计,最大的序号为partition数量减1,partition是实际物理上的概念,而topic是逻辑上的概念。partition还可以细分为segment:(.log/.index):
segment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为segment索引文件和数据文件(引入索引文件的目的是便于利用二分查找快速定位message位置)。这两个文件的命令规则为:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。.index文件中存储的是offset和对应的.log文件中的实践偏移量,所以通过offset然后找到文件的实际偏移量就可以读到数据(数据具有固定格式)
三、集群搭建(centos6.5)
1. 环境准备:
jdk1.8
Zookeeper-3.4.14
Scala: 注意版本对应,如 kafka_2.11-2.2.0 ,2.11代表Scala版本,2.2.0为Kafka版本
2. 下载:
http://kafka.apache.org/downloads
3. 部署Zookeeper
- 下载
https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.4.14/
- 解压后进入conf目录,重命名zoo_sample.cfg为zoo.cfg
mv zoo_sample.cfg zoo.cfg
- 配置zoo.cfg
dataDir=/home/hadoop/zookeeper-3.4.14/zkData
#配置多个为集群模式
server.1=master:2888:3888
#server.2=slave1:2888:3888
Server.A=B:C:D。
A是一个数字,表示这个是第几号服务器;
B是这个服务器的ip地址;
C是这个服务器与集群中的Leader服务器交换信息的端口;
D是万一集群中的Leader服务器挂了,需要一个端口来重新进行选举,选出一个
- 在/home/hadoop/zookeeper-3.4.14/zkData目录下创建一个myid的文件,并添加对应server的编号。
vim myid
1
- 启动Zookeeper
bin/zkServer.sh start
- 查看状态
bin/zkServer.sh status
配置单个Zookeeper机器则会默认是standalone,多个则会进行选举,然后选出leader。
4. 部署Kafka
- 解压后进入config目录修改配置:
vim server.properties
#每个broker的值应该是唯一的。
broker.id=1
#运行日志位置
log.dirs=/home/hadoop/apps/kafka_2.11-2.2.0/logs
#master为主机名(在/etc/hosts配置好的),多个的话用逗号(,)隔开
zookeeper.connect=master:2181
#是否允许删除topic
delete.topic.enable=true
- 启动集群
bin/kafka-server-start.sh config/server.properties &
配置了多个broker只需逐个启动即可。
- 创建Topic
bin/kafka-topics.sh --create --zookeeper master:2181 --replication-factor 1 --partitions 1 --topic first
–replication-factor 指定副本数
–partitons 指定分区数
–topic 指定topic名字
- 查看topic
bin/kafka-topics.sh --zookeeper master:2181 --list
- 启动生产者客户端并指定topic
bin/kafka-console-producer.sh --broker-list master:9092 --topic first
- 启动消费者客户端,并指定消费主题
bin/kafka-console-consumer.sh --bootstrap-server master:9092 --from-beginning --topic first
四、Java API
1. Maven相关依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.11</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>1.0.0</version>
</dependency>
2. 编写一个生产者
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class FirstProducer {
public static void main(String[] args) {
Properties properties = new Properties();
// 定义kakfa 服务的地址,不需要将所有broker指定上
properties.put("bootstrap.servers", "192.168.31.209:9092");
// 等待所有副本节点的应答
properties.put("acks", "all");
//key value 序列化
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
for (int i = 0; i < 10; i++) {
//Callback 为发送成功后的回调函数
producer.send(new ProducerRecord<String, String>("first", i + "", "hello, kafka-" + i), new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (recordMetadata != null){
System.out.println(recordMetadata.offset());
}
}
});
}
// 记得加上,不然会出现神奇错误...
producer.close();
}
}
3. 自定义分区
- 自定义分区类,实现Partitioner接口
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
public class TestPartitioner implements Partitioner {
/**
* 控制分区
*/
@Override
public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
//System.out.println(s);
//将所有数据存储到topic的第0号分区上,
return 0;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
- 在实现生产者时加入以下设置
//添加自定义分区类
properties.put("partitioner.class","partitoner.TestPartitioner");
- 可自行观察log.dirs设置的目录路径下first主题分区的log日志动态变化情况
4. 编写一个消费者
import java.util.Arrays;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
public class CustomConsumer {
public static void main(String[] args) {
Properties props = new Properties();
// 定义kakfa 服务的地址,不需要将所有broker指定上
props.put("bootstrap.servers", "192.168.31.209:9092");
// 制定consumer group
props.put("group.id", "test");
// 是否自动确认offset
props.put("enable.auto.commit", "true");
// 自动确认offset的时间间隔
props.put("auto.commit.interval.ms", "1000");
//超时时间
props.put("session.timeout.ms", "30000");
// key的序列化类
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// value的序列化类
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 定义consumer
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 消费者订阅的topic, 可同时订阅多个
consumer.subscribe(Arrays.asList("first"));
while (true) {
// 读取数据,读取超时时间为100ms
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
}
}