目录
基本概念
Kafka的历史
Kakfa最初由Linkedin公司采用Scala语言开发,是一个分布式的、支持多分区、多副本,基于zookeeper协调的分布式消息系统,2010年贡献给Apache基金会并成为顶级开源项目。
以下是kafka官网的介绍:
Apache Kafka is an open-source distributed event streaming platform used by thousands of companies for high-performance data pipelines, streaming analytics, data integration, and mission-critical applications.
(Apache Kafka是一个开放源代码的分布式事件流平台,成千上万的公司使用该平台来实现高性能数据管道,流分析,数据集成和关键任务应用)
Kafka中的术语
borker:Kafka中的服务器节点叫做broker,多个borker组成一个Kafka集群
topic:消息的类别,Kafka根据topic对消息进行分类,一个topic可以包含多个partition,发送消息时必须指定topic
producer:消息的生产者,向broker发送消息
consumer:消息的消费者,从broker拉取消息
consumer group:消费者组,每个消费者都属于一个消费者组,一条消息可以被多个消息者组同时消费,但是只会被消费者组中的一个消费者消费
partition:topic可以切分为多个partition,至少有一个partition,单个partition内部是有序的
单机安装
一、从官网下载安装包:http://kafka.apache.org/,我下载的是2.4.1版本,安装包文件名:kafka_2.11-2.4.1.tgz
二、解压安装包,修改配置文件config/server.properties:
bin/zkServer.sh start
#broker的id,每个borker的id都不相同
broker.id=0
#监听部署机器的IP和端口
listeners=PLAINTEXT://内网IP:9092
advertised.host.name=外网IP
advertised.listeners=PLAINTEXT://外网IP:9092
#消息的存储地址
log.dirs=/usr/local/kafka/kafka_2.11-2.4.1/log/kafka-logs
#zookeeper的地址
zookeeper.connect=zookeeper所在服务器IP:2181
三、启动zookeeper
bin/zkServer.sh start
四、启动Kafka
bin/kafka‐server‐start.sh ‐daemon config/server.properties #后台启动,不会打印日志到控制台
或者用
bin/kafka‐server‐start.sh config/server.properties &
会打印日志
常用命令:
#查看已有的消费者组名:
bin/kafka‐consumer‐groups.sh ‐‐bootstrap‐server IP地址:9092 ‐‐list
#查看消费者组消费的偏移量:
bin/kafka-consumer-groups.sh --bootstrap-server IP地址:9092 --describe --group demoGroup
这里有几个概念
current-offset:当前消费组的已消费偏移量
log-end-offset:主题对应分区消息的结束偏移量(HW)
lag:当前消费组未消费的消息数
每条消息在partition中都有一个唯一的offset,消费者根据offset顺序消费partiontion中的消息并维护这个offset,consumer会将自己消费的offset提交到__consumer_offsets这个topic下的某一个分区(__consumer_offsets-分区号)中,key是consumerGroupId+topic+分区号,value为offset,Kafka默认生成50个存储offset的分区,consumer根据如下公式计算需要提交到哪个分区:hash(counsumerGroupId) % 分区数
#查看topic的Partition、Leader、Replicas、Isr等信息
bin/kafka-topics.sh --describe --zookeeper 172.21.0.3:2181 --topic test1
Leader:当前Partitiion的Leader是哪个节点,每个Partition都可以有多个备份节点,其中有一个Leader节点负责处理读写数据,节点之间相互同步数据
Replicas:当前Partition的备份节点,这里会列出包含Leader在内的所有节点
Isr:当前还存活着并已经备份了数据的备份节点
集群安装
组成Kafka集群只需要在多台服务器安装Kfka并调整配置文件即可
注意每台机器的配置文件中的broker.id互不相同,并且zookeeper.connect要相同,这样才会注册到同一个zookeeper中,全部启动后,在zookeeper中查看是否注册成功
Java集成
非springboot集成
非springboot项目引入maven依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.4.1</version>
</dependency>
生产者demo示例:
public class ProducerDemo {
private final static String TOPIC_NAME = "my-demo-topic";
public static void main(String[] args) throws ExecutionException, InterruptedException {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "IP地址:9092");
/*
消息持久化机制
0:不等待broker回复就发送下一条消息
1:至少等待leader写入成功再发送下一条消息
-1或all:等待写入成功个数大于等于min.insync.replicas配置的个数再发送下一条消息
*/
props.put(ProducerConfig.ACKS_CONFIG, "1");
//重试次数
props.put(ProducerConfig.RETRIES_CONFIG, 3);
//重试间隔设置
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);
//设置发送消息的本地缓冲区,如果设置了该缓冲区,消息会先发送到本地缓冲区,可以提高消息发送性能,默认值是33554432,32MB
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
/*
设置本地线程每次从缓冲区取消息的大小,默认16kb
*/
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
/*
设置发送消息的时间间隔,在设置时间内消息满16kb就立刻发送消息,否则等设置时间后再发送消息即使不满16kb,默认0即立刻发送消息
*/
props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
//序列化器
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
Producer<String, String> producer = new KafkaProducer<String, String>(props);
String msgTime = new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date());
Message message = new Message(String.valueOf(i), "msgTest-" + i, "hello", msgTime);
ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(
TOPIC_NAME, message.getMsgId(), JSON.toJSONString(message));
RecordMetadata metadata = producer.send(producerRecord).get();
System.out.println("同步方式发送消息结果:" + "topic-" + metadata.topic() + "|partition-"
+ metadata.partition() + "|offset-" + metadata.offset());
}
}
消费者demo示例:
public class ConsumerDemo {
private final static String TOPIC_NAME = "my-demo-topic";
private final static String CONSUMER_GROUP_NAME = "demoGroup";
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "IP地址:9092");
// 消费分组名
props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
// 是否自动提交offset,默认就是true,设置为false时需要手动提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
//一次poll最大拉取消息的条数
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 50);
//序列化器
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
consumer.subscribe(Arrays.asList(TOPIC_NAME));
// 消费指定分区
//consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
while (true) {
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : consumerRecords) {
System.out.printf("收到消息:partition = %d,offset = %d, key = %s, value = %s%n", record.partition(),
record.offset(), record.key(), record.value());
}
if (consumerRecords.count() > 0) {
//手动提交
consumer.commitSync();
}
}
}
}
springboot集成
springboot添加mavent依赖:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
配置application.yml文件
server:
port: 8080
spring:
kafka:
bootstrap-servers: ip地址:9092
producer: # 生产者
retries: 3 # 设置大于0的值,则客户端会将发送失败的记录重新发送
batch-size: 16384
buffer-memory: 33554432
acks: 1
# 指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: default-group
enable-auto-commit: false
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
# 手动调用Acknowledgment.acknowledge()后立即提交
ack-mode: MANUAL_IMMEDIATE
springboot生产者示例:
@RestController
public class KafkaController {
private final static String TOPIC_NAME = "my-demo-topic";
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@RequestMapping("/send")
public String send() {
#直接使用封装好的kafkaTemplate
kafkaTemplate.send(TOPIC_NAME, 0, "key", "hello world");
return "send success";
}
}
springboot消费者示例:
public class SpringBootConsumer {
/**
*@KafkaListener注解还有很多参数比如可以控制消费的分区等等
*/
@KafkaListener(topics = "my-demo-topic", groupId = "demoGroup")
public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
System.out.println("消费者消费:" + record);
ack.acknowledge();
}
设计原理
Kafka架构图:
Controller选举机制
broker集群中会选出一个Controller管理集群中分区和ISR的状态,包括重新选举partition分区的leader,广播ISR列表的变更,Kafka集群启动时所有的borker都会在zookeeper创建一个/controller临时节点,能创建成功的broker成为controller,如果controller宕机,临时节点被删除,监听此节点的其他broker重新尝试创建节点
Partition的Leader选举机制
如果partition的leader宕机,controller会从ISR列表中选择第一个borker作为leader,第一个borker一般是同步数据最多的副本,但是如果unclean.leader.election.enable=false,那么ISR列表中副本全部宕机后,将不会重新选举leader,如果设置为true,则会从非ISR列表中的副本中选举leader,但是这样可能会造成数据不一致
消费者Rebalance机制
消费者组中的消费者出现变化或者分区数发生变化,Kafka会Rebalance消费者和消费的分区,比如将宕机的消费者消费的分区分配给其他消费者,但是只有subscirbe不指定消费分区时才会重新分配,cumsermer增加或减少、topic的分区增加、消费者重新订阅topic都会引起rebalance,Kafka在rebalance时消费性能会下降,rebalance分配策略有以下几种:
默认的range策略:按照分区个数平均分配
round-robin:轮询分配
sticky:类似round-robin,但是分配更均匀并尽量不修改分配情况
消费者启动的Rebalance过程:
1、GROUP_COORDINATOR
consumer启动后会请求broker获取GroupCoordinator的地址,GroupCoordinator是每个消费者组选举出的一个broker节点负责管理整个消费者组的心跳信息,consumer消费的offset提交到__consumer_offsets的哪个分区,该分区对应的leader节点就是该group的GroupCoordinator,获取组协调器的地址后,consumer就向GroupCoordinator发送JOIN_GROUP请求
2、JOIN_GROUP
consumer就向GroupCoordinator发送JOIN_GROUP请求后就等待响应,如果该consumer是第一个连接GroupCoordinator的将被指定为该消费者组的leader,把consumer group的所有信息发送给leader,leader负责制定分区方案,consumer收到响应后发送SYNC_GROUP请求
3、SYNC_GROUP
如果是consumer的leader节点,在制定分区方案后和SYNC_GROUP一起上传给GroupCoordinator,非leader节点直接发送SYNC_GROUP请求,GroupCoordinator收到请求后下发分区方案给consumer,这样consumer就可以开始工作了
HW和LEO
HW即高水位(HighWatermark),每个partion取ISR列表中最小的LEO(log-end-offset)作为HW,HW标志着comsuer最大消费位置,leader和follower节点都维护自己的HW,leader刚写入Producer生产的消息是无法被consumer消费到的,只有ISR列表中的所有节点都同步写入并更新HW后,consumer才能消费到最新的消息,HW确保consumer消费到的消息是集群写入成功的
零拷贝
kafka高性能的一部分原因就在于处理数据时使用了零拷贝机制,正常来说,应用在传递数据时需要经过用户态和内核态的流转,利用零拷贝可以直接从网卡接收数据到用户空间或从用户空间发送数据到网卡上,大大节省数据传输耗时
常见问题
消息投递可靠性
根据acks的设置来确保消息投递成功,acks=0:producer不需要等待broker的确认就可以发送下一条消息,能获取最大的吞吐率,但是很有可能丢消息,acks=1:等待leader写磁盘成功再发送下一条消息,吞吐率和可靠性的均衡,如果leader宕机可能会丢消息,acks=-1或all,等待leader和ISR列表中的所有节点都写入成功再发送下一条消息,可以确保不会丢消息但是吞吐率会降低
消息消费可靠性
消费端设置手动提交,处理完消息再提交确保消息者不会丢失消息
顺序消费
kafka不保证消费的有序性,但是可以把消息顺序发送到一个分区,使用一个消费者顺序消费来实现有序性
消息积压
如果生产者发送消息过快或消费者消费太慢导致消费积压,可以增加新的消费者组消费积压分区的消息或者调低消费者每次拉消息的个数避免消费超时导致rebalacne
如果消费者有bug导致消息积压,可以先临时启动消费者将消息保存到数据库等bug修复再消费