主要内容摘要
- 生产者
- 消费者
- 再均衡操作
- 副本和日志存储
- 延迟操作
- 控制器
- 学习目标:
掌握下面这些概念
1、给一份学习使用的docker-compose搭建环境,供学习使用。
2、了解Kafka的一些核心概念
3、熟悉客户端组件及常用的使用方式及原理
4、协调者处理消费者再均衡
5、副本与日志存储
5、控制器
Kafka docker-compose
version: '2'
services:
zookeeper:
image: wurstmeister/zookeeper
ports:
- "2181:2181"
volumes:
- "./zookeeper/data:/data"
- "./zookeeper/datalog:/datalog"
kafka:
image: wurstmeister/kafka
ports:
- "9092" # kafka 把9092端口随机映射到主机的端口
environment:
KAFKA_ADVERTISED_HOST_NAME: 10.2.46.144 #本机ip
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_CREATE_TOPICS: test:1:1
KAFKA_DELETE_TOPIC_ENABLE: "true"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./kafka/logs:/kafka
kafka-manager:
image: sheepkiller/kafka-manager
links:
- zookeeper
environment:
ZK_HOSTS: zookeeper:2181
APPLICATION_SECRET: letmein
KM_ARGS: -Djava.net.preferIPv4Stack=true
ports:
- "9000:9000"
kafka的角色
消息系统:
作为消息中间件都具备系统解耦、冗余存储、流量削峰、缓冲、异步通信、扩展性、可恢复性等功能。同时具有消息顺序性和回溯消费的功能。
存储系统
可以把消息持久化到磁盘,并且降低消息丢失的风险,需要设置数据保留策略为“永久”,或启用主题的日志压缩功能。
流式处理平台
有一个完整的流式处理的类库
重要的概念
主题
Kafka中的消息是以主题为单位归类的,逻辑上生产者往主题推送消息,消费者从主题拉取消息。在物理的存储上,消息是存在具体的分区的。
分区
一个主题可以分为多个分区,一个分区只能属于一个主题。同一个主题的不同分区存储的消息是不同的,分区在存储层可以看做是一个可追加的日志文件,消息在被加入到分区的时候会分配一个特定的偏移量(offset)。
offset是消息在分区的唯一标识,Kafka通过它来保证消息在分区内的顺序性,不过offset并不跨越分区,也就是说,Kafka并不保证主题有序,只能保证分区有序。
- 那如何保证分区内的消息是有序的呢?
设置多个分区
如果主题只有一个分区,也就只有一个文件,那这台机器的I/O将会成为这个主题的性能瓶颈,Kafka的分区可以分布在不同的服务器上,设置多个分区可以进行水平的扩展,从而解决这个问题。分区的数量可以在创建主题的时候指定,也可以在后期动态的修改。
- 如何动态的修改某个主题的分区数量呢?
分区多副本
分区存储消息,那如果分区所在的服务器崩了,那消息不就都不能使用了吗?因此Kafka引入了多副本的概念,我们可以为某个分区设置多个副本来提升容灾能力。副本之间是“一主多从”的关系,leader副本负责处理读写请求,follower副本只负责与leader同步。当leader副本出现故障时,从follower副本中重新选举一个处理作为leader副本,从而实现了故障转移。
AR相关概念
消息会先发送到leader副本,之后follower副本会从leader副本拉取消息进行同步,同步会有快慢问题。分区所有的副本统称为AR(Assigned Replicas),所有与leader副本保持一定程度同步的副本(包括leader副本)称为ISR(In-Sync Replica)。这里的“一定程度”就是滞后的范围,可以通过参数配置。与leader副本同步滞后过多的副本称为OSR(Out-of-Sync Replicas),故AR = ISR + OSR。
leader副本维护和跟踪ISR集合中所有follower副本的滞后状态,当滞后太多会被剔除,当“追上”后再加入到ISR中。
LEO & HW
HW(High Watermark):标识一个特定的消息偏移量offset,消费者只能消费到这个offset之前的消息。
图 1-1 分区中各种偏移量的说明
图1-1代表一个日志文件,文件中有9条消息,第一条消息的offset为0,最后一条消息的offset为8;该日志文件的HW为6.
LEO(Log End Offset):标识当前日志文件中下一条待写入消息的offset,如上图中的消息9的位置。
ISR集合中的每个副本都会维护自身的LEO,而集合中最小的LEO即为分区的HW。
- 分区的HW是什么?
由此可见,Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。使用这种ISR的方式有效的权衡了数据可靠性和性能之间的关系。
生产者
生产者相对比较简单
目标:保证消息已经发到服务端并且不丢失(acks)
一般配置例子
spring:
kafka:
bootstrap-servers: xx:9092,xx:9092,xx:9092
producer:
acks: all
retries: 3
batch-size: 50
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
//deal?
System.out.println(metadata.partition() + ":" + metadata.offset());
}else{
exception.printStackTrace();
}
}
});
分区器
指定消息发送到各个分区。
如果指定了key,会对可以进行HASH算法,来得到分区号;如果不指定key,会以轮询的方式发送到各个可用的分区。
生产者内部原理
消费者
消费者组
Kafka中存在一个消费者组的概念,每个消费者都属于一个消费者组。当消息发布到主题之后,只会被订阅它的消费者组中的其中一个消费者消费。每个分区只能被一个消费者组中的一个消费者所消费。
这里就有一个分配策略的概念:
对一个消费组而言, 一个分区被一个消费者所消费,如果订阅主题有6个分区,一个消费者消费6个分区,两个消费者各消费3个分区,三个消费者个消费两个分区;这样消费者具有横向伸性,可以通过增加或减少消费者数量来提升或降低整体的消费能力。(默认使用的是RangeAssignor分配策略)
1、其他的分配策略?
消息的两种投递模式
- 点对点模式:如果所有消费者都隶属于一个消费者组,那么所有的消息都会被均衡地投递给每一消费者,每条消息都只会被一个消费者消费。
- 发布/订阅模式: 如果每个消费者都属于不同的消费者组,那么所有的消息都会被投递给所有的消费者,每条消息会被所有的消费者消费。
消费者客户端
具体步骤:
1. 配置消费者客户端;
bootstrap.servers: 格式:host:port,默认为"",不必配置全; group.id: 默认值为"",不配置会报错;client.id: 会生成"consumer-"与数字的拼接。
2. 订阅主题;
订阅主题,如果前后订阅了不同的主题,那么以最后一次的为准;
还有一个带正则参数的方法;
第二个参数是再均衡监听器;
还可以直接订阅某些主题的特定分区,通过assign()方法来实现。
可以通过partitionsFor来查询指定主题的元数据信息
List<PartitionInfo> partitionsFor(String topic)
例子:通过assign方法来获取各个分区的position、committed offset、lastConsumedOffset
Properties props = initConfig();
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
List<PartitionInfo> partitionInfos = consumer.partitionsFor(topic);
partitionInfos.stream().forEach(partitionInfo -> {
int partition = partitionInfo.partition();
TopicPartition tp = new TopicPartition(topic, partition);
consumer.assign(Arrays.asList(tp));
long lastConsumedOffset = -1;
while (true) {
ConsumerRecords<String, String> records = consumer.poll(1000);
if (records.isEmpty()) {
break;
}
List<ConsumerRecord<String, String>> partitionRecords
= records.records(tp);
lastConsumedOffset = partitionRecords