kafka的介绍
kafka是一个分布式的订阅和发布消息系统, 主要的特点是高性能, 高吞吐量, 没有遵守jms的规范, 可以部署集群, 内置分区, 每秒处理几十万消息, 非常适合大数据场景的应用;
应用场景
日志收集, 行为跟踪等不需要非常严格要求消息不丢失的场景
kafka的架构
- producer: 生产者
- consumer: 消费者
- broker: 消息引擎, 可以做集群部署
kafka的相关概念(待修改)
- topic: 和activemq不同的是, 在kafka中没有queue和topic的区分, 所有的消息发布只有topic一种;
- partition: 数据分区
- group: 消费者所属的组
kafka的安装和启动
- 安装zookeeper的集群, 并启动;
- 下载kafka的tar包, 解压, 进入config目录下, 修改server.properties中zookeeper.connect为自己的地址, 多台用逗号隔开, listeners=PLAINTEXT://192.168.1.11:9092修改;
- 启动方式sh ./bin/kafka-server-start.sh ./config/server.properties
- 注意连接时关闭防火墙;
- 以上为单机启动, 如果要做集群的话, 就多部署几台机器, 然后修改server.properties中的zookeeper配置为相同, 以及broker.id保证每个节点唯一(例如broker.id=1, broker.id=2);
启动后的检测命令
- 创建
sh ./bin/kafka-topic.sh --create --zookeeper ip:port --replication-factor 1 --partition 1 --topic test
- 查看命令
sh ./bin/kafka-topic.sh --list --zookeeper ip:port
- 创建发送命令
sh ./bin/kafka-console-producer.sh --broker-list 192.168.1.10:9092 --topic test
- 消息监听
sh ./bin/kafka-console-consumer.sh --bootstrap-server 102.168.1.10:9092 --topic test --from-begining
spring-boot整合kafka
- 添加依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
- 注入KafkaTemplate<String, String>进行消息的发送
- 在方法上添加@KafkaListener(topics = INDEX_TOPIC)注解获取消息, 消费者中可以接受的参数
ConsumerRecord, Acknowledgment, Consumer用来做各种操作;
- 更详细的配置
yml文件中的配置
#============== kafka ===================
kafka.consumer.zookeeper.connect=10.93.21.21:2181
kafka.consumer.servers=10.93.21.21:9092
kafka.consumer.enable.auto.commit=true
kafka.consumer.session.timeout=6000
kafka.consumer.auto.commit.interval=100
kafka.consumer.auto.offset.reset=latest
kafka.consumer.topic=test
kafka.consumer.group.id=test
kafka.consumer.concurrency=10
kafka.producer.servers=10.93.21.21:9092
kafka.producer.retries=0
kafka.producer.batch.size=4096
kafka.producer.linger=1
kafka.producer.buffer.memory=40960
broker的config配置
@Configuration
@EnableKafka
public class KafkaProducerConfig {
@Value("${kafka.producer.servers}")
private String servers;
@Value("${kafka.producer.retries}")
private int retries;
@Value("${kafka.producer.batch.size}")
private int batchSize;
@Value("${kafka.producer.linger}")
private int linger;
@Value("${kafka.producer.buffer.memory}")
private int bufferMemory;
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(ProducerConfig.RETRIES_CONFIG, retries);
props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
props.put(ProducerConfig.LINGER_MS_CONFIG, linger);
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return props;
}
public ProducerFactory<String, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<String, String>(producerFactory());
}
}
consumer的config配置
@Configuration
@EnableKafka
public class KafkaConsumerConfig {
@Value("${kafka.consumer.servers}")
private String servers;
@Value("${kafka.consumer.enable.auto.commit}")
private boolean enableAutoCommit;
@Value("${kafka.consumer.session.timeout}")
private String sessionTimeout;
@Value("${kafka.consumer.auto.commit.interval}")
private String autoCommitInterval;
@Value("${kafka.consumer.group.id}")
private String groupId;
@Value("${kafka.consumer.auto.offset.reset}")
private String autoOffsetReset;
@Value("${kafka.consumer.concurrency}")
private int concurrency;
@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setConcurrency(concurrency);
factory.getContainerProperties().setPollTimeout(1500);
return factory;
}
public ConsumerFactory<String, String> consumerFactory() {
return new DefaultKafkaConsumerFactory<>(consumerConfigs());
}
public Map<String, Object> consumerConfigs() {
Map<String, Object> propsMap = new HashMap<>();
propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, enableAutoCommit);
propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitInterval);
propsMap.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, sessionTimeout);
propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
propsMap.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
return propsMap;
}
@Bean
public Listener listener() {
return new Listener();
}
}
生产者和消费者的使用和上面类似;
一些参数的解释
- ProducerConfig.ACKS_CONFIG: 发送端确认模式, 0(发送给broker,不需要确认, 性能较高, 消息丢失), 1(只要lead而节点确认), -1(需要集群中所有节点确认), 根据自己的业务场景做选择
- ProducerConfig.BATCH_SIZE_CONFIG: kafka中producer会对消息进行批量发送, 为了提高性能(是kafka调优的参数);
- linger.ms:
- PARTITIONER_CLASS_CONFIG: 自定义发送消息到哪个分区, 需要的值是实现了Partitioner接口类的全路径;
- GROUP_ID_CONFIG: 消费者组, 不同的消费组可以消费相同的消息, 但同一个消费者组中, 只有一个consumer可以获取到这条消息;
- AUTO_OFFSET_RESET_CONFIG: 设置为earliest, 新的groupid那么他会从最早的消息开始消费, lastest(取最近的值)和none(topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常)
- ENABLE_AUTO_COMMIT_CONFIG: 自动确认提交, 他和AUTO_COMMIT_INTERVAL_MS_CONFIG配合使用, 每隔多长时间做一次批量确认;
消息的同步发送和异步发送, 以及发送结果回调
1. kafkatemplate.send()是异步发送
2. kafkatemplate.send().get()是同步发送
3. 定义一个Component组件
@Component
public class KafkaSendResultHandler implements ProducerListener {
private static final Logger log = LoggerFactory.getLogger(KafkaSendResultHandler.class);
@Override
public void onSuccess(ProducerRecord producerRecord, RecordMetadata recordMetadata) {
log.info("Message send success : " + producerRecord.toString());
}
@Override
public void onError(ProducerRecord producerRecord, Exception exception) {
log.info("Message send error : " + producerRecord.toString());
}
}
然后使用producer.setProducerListener(listner);
Topic和Partition讲解
- topic: 是一个存储消息的逻辑概念,
- partition: 一个topic可以有不同的partition, 同一个topic下, 不同partition的消息是不同的, 每个消息会追加到partition的末尾
- offset: 每一个partition都有一个offset, 用来记录当前partition中消费的记录
消息的分发
- kafka的消息是key-value的形式, 默认的是使用key的hash取模算法确认发送到哪个partition, 也可以使用自定义发送策略(上面已经讲过);
- 消费者也可以指定消费哪个partition, 如果consumer数量 > partition数量, 会存在某个消费者不能消费, 如果 =, 则是每个consumer负责一个分区, 如果 <, 则是存在consumer消费多个分区的情况;
- consumer在不同的分区消费时不保证顺序性的, 只有在同一个partition下面才会保证顺序性;
分区分配策略
- Range: 范围分区, 如果多个topic的情况下, 会存在某个consumer多消费partition的情况;
- RoundRobin: 轮询
- 什么时候会触发rebalance策略: consumer数量发生变化(消费者始终小于等于partition)
- 谁来管理rebalance: 集群中存在一个coordinator, 去管理consumer, 每个consumer启动时会去连接这个coodinator, 发送joingroup请求, 然后确认一个leader, 选举完之后返回给consumer, 这个过程完成之后, 每个consumer会和coordinator维持一个心跳连接, 用来检测consumer是否还存活, leader会将分区的分发策略发送给coordinator, 然后coodinator在发送给其他consumer
offset机制
- _consumer_offset: 在broker上hi有这样一个topic, 默认是50个分区, 每个group中offset的保存(groupid.hashcode%50)在这50分区中
消息的存储机制
- 位置: topic_0/topic_1/...
- 创建的partition和broker数量不能整除的时候, 会在某个broker上多创建partition
消息写入和读取性能
- 顺序写: 每次消息都会写入文件的最后;
- 零拷贝: 主要是用户空间和内核空间, 以及磁盘空间, 文件读取在这些空间中的拷贝问题通常的拷贝还需要内核态和用户态之间的交互, 而零拷贝则将这一步省略, 提高io效率;
- 消息是使用分段存储的logSegment, 每个logSegment中会有index, timeIndex文件和log文件, logSegment是一个逻辑概念, index的文件名使用消息的数量来定义的, 可以根据offset快速定位到消息位置, index文件和log文件的关系, index中记录的是log文件中消息的记录行;
- 分段的好处是可以进行日志清理, 默认是保留七天, 也可以根据日志文件的大小, 日志还可以进行压缩;
partition副本的概念
- 区分leader和follow副本, es集群的分区也是这种逻辑, follow和lead不再同一台机器上, follow和follow也尽量不再同一台机器上;
- leader挂掉之后, 会从follow中重新选举一个作为leader
- ISR: 维护的是所有和leader保持连接的follow副本集, 如果follow和leader数据相差很大, 则从该ISR列表中移除
- leader负责和客户端的交互, 数据的读写, follower则从leader中读取数据, 不论有没有消息, follower都会发送fetch请求, 但是没有消息的时候, 这个请求会阻塞, 有消息来时, leader会进行唤醒
- LEO: 最新消息的值, broker接收到最新消息的数量
- HW: 消息的水平位
同步的过程
- 把消息写入到对应分区的log文件中, 同时更新leader的LEO;
- 尝试去更新leader的HW的值, 比较自己本身的LEO和REMOTE_LEO的值, 取最小的值最为HW, 但现在更新不了, 因为follower还没有同步消息;
- follower向leader去fetch消息, 同时将自己的offset发送给leader;
- leader根据follower发送的offset(第一次fetch是返回的是0)值更新remote_leo的值;
- 此时leader又去更新HW, 但这是leo和remote_leo都是0, 所以HW=0;
- 然后leader会返回消息给follower, 这时follower会将自己leo和hw分别更新为1和0
- 往后再次同步时就会都修改为1
数据丢失问题
场景
当follow的HW=0, leo=1的时候, follow宕机了, 此时再次重启, 由于kakfa的机制设置(此时, 会将还未达到hw的数据删掉, 因此leo=0),当follow再次去fetch leader时, 恰巧leader挂掉, 此时重新选举了follow为leader, 然后这条消息就丢失了;为了解决这个问题, 引入了朝代的概念(segmentLog中会有一个epoch_point文件), 当leader被重新选举时, 会比较epoch的值, 如果发现自己的epoch比较小, 则不会截断那条消息;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。