kafka是一个分布式、支持分区的(partition)、多副本的(replica)、基于zookeeper的协调式的分布式消息系统,最大的特性就是可以实时处理大量数据来满足各种需求场景:比如hadoop的批处理系统、低延迟的实时系统、Storm/Spark流式处理引擎、web/nginx日志、访问日志、消息服务等
使用场景
基本概念
名词 | 解释 |
---|---|
Broker | 消息中间件处理结点,一个Kafka结点就是一个Broker,一个或多个Broker可以组成一个Kafka集群 |
Topic | Kafka根据topic对消息进行归类,发布到Kafka集群的每条消息都需要指定一个topic |
Producer | 消息生产者,像Broker发送消息的客户端 |
Consumer | 消息消费者,从Broker读取消息的客户端 |
ConsumerGroup | 每个Consumer属于一个特定的Consumer Group,一条消息可以被多个不同的Consumer Group消费,但是一个Consumer Group只能有一个Consumer能够消费该消息 |
Partition | 物理上的概念,一个topic可以分成多个partition,每个partition内部的消息是有序的 |
./kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
./kafka-topics.sh --list --zookeeper localhost:2181
./kafka-console-producer.sh --broker-list 192.168.164.1:9092 --topic test
# 从最后一条消息的偏移量+1开始消费,即消费端连接之前已存的消息不会被这个消费端消费
./kafka-console-consumer.sh --bootstrap-server 192.168.164.1:9092 --topic test
# 从头开始消费
./kaka-console-consumer.sh --bootstrap-server 192.168.164.1:9092 --from-beginning --topic test
单播消息
如果多个消费者在一个消费组中,那么只有一个消费者可以收到订阅的topic中的消息,换言之,同一个消费组只能有一个消费者收到一个topic中的消息,在创建消费客户端时通过指定消费组的id来控制消费端属于哪个消费组
./kafka-console-consumer.sh--bootstrap-server 192.168.164.1:9092 --consumer-property group.id=testGroup --topic test
多播消息
不同的消费者订阅同一个topic,如果这些消费者在不同的消费组中,这些消费者可以同时收到这些消息
./kafka-console-consumer.sh--bootstrap-server 192.168.164.1:9092 --consumer-property group.id=testGroup1 --topic test
./kafka-console-consumer.sh--bootstrap-server 192.168.164.1:9092 --consumer-property group.id=testGroup2 --topic test
查看消费组的相关信息
#查看当前topic下有哪些消费组
./kafka-consumer-groups.sh --bootstrap-server 192.168.164.1:9092 --list
#查看消费组中的具体信息,比如当前偏移量、最后一条消息的偏移量、堆积的消息数量
./kafka-consumer-groups.sh --bootstrap-server 192.168.164.1:9092 --describe --group testGroup
创建多分区的topic:
./kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 -- partitions 2 --topic test1
hash(consumerGroupId)%_consumer_offsets分区数
来求得准备三个server.properties文件:
broker.id=0
listeners=PLAINNEXT://192.168.164.1:9092
log.dir=/usr/local/data/kafka-logs-0
broker.id=1
listeners=PLAINNEXT://192.168.164.1:9093
log.dir=/usr/local/data/kafka-logs-1
broker.id=2
listeners=PLAINNEXT://192.168.164.1:9094
log.dir=/usr/local/data/kafka-logs-2
通过命令启动三台broker
./kafka-server-start.sh -daemon ../config/server0.properties
./kafka-server-start.sh -daemon ../config/server1.properties
./kafka-server-start.sh -daemon ../config/server2.properties
副本是对分区的备份,在集群中,不同的副本会被部署在不同的broker上,多个副本在kafka集群的多个broker中,会有一个副本作为leader,其他都是follower
集群中有多个broker,创建topic时可以指明topic有多少个partition,消息会被拆分到不同的partition中存储,可以为partition创建多个副本,不同的副本存放在不同的broker里,某一个broker宕机,其他broker还可以保证正常的运行
/***
* @author shaofan
* @Description kafka生产者
*/
public class MyProducer {
private final static String TOPIC_NAME="java-test";
public static void main(String[] args) throws ExecutionException, InterruptedException {
Properties props = new Properties();
// 配置kafka集群的地址端口
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.164.1:9092,192.168.164.1:9093,192.168.164.1:9094");
// 配置键序列化方式,将发送的key序列化为字节数组
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 配置值序列化方式,将发送的value序列化为字节数组
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
// 配置生产者的消息确认机制
props.put(ProducerConfig.ACKS_CONFIG,1);
// 配置发送消息缓冲区大小
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
// 配置kafka单次从缓冲区拉去的数据大小
props.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
// 当缓冲区剩余数据不足单词拉取的数据时,经过该时间ms会将剩余数据都拉到kafka
props.put(ProducerConfig.LINGER_MS_CONFIG,10);
// 根据配置创建kafka生产者对象
Producer<String,String> producer = new KafkaProducer<>(props);
// 需要发送的消息记录,指定需要发送的目标topic和消息键值,这里没有指定分区,会根据hash计算分区,也可以在第二个参数传入指定的分区号
ProducerRecord<String,String> producerRecord = new ProducerRecord<>(TOPIC_NAME,"1","hello");
try{
// 同步发送消息并获取元数据,在消息发送后会进入阻塞,等带消息结果
RecordMetadata metadata = producer.send(producerRecord).get();
if(metadata!=null){
System.out.println("同步发送消息结果:topic-"+metadata.topic()+"|partition-"+metadata.partition()+"|offset-"+metadata.offset());
}
}catch(Exception e){
System.out.println("消息发送失败");
}
// 异步发送消息,消息发送后不会进入阻塞,在异步回调中获取消息结果
producer.send(producerRecord, (metadata1, exception) -> {
if(metadata1!=null){
System.out.println("异步发送消息结果:topic-"+ metadata1.topic()+"|partition-"+ metadata1.partition()+"|offset-"+ metadata1.offset());
}
if(exception!=null){
exception.printStackTrace();
}
});
Thread.sleep(2000);
}
}
ack相关配置
/***
* @author shaofan
* @Description kafka消费者
*/
public class MyConsumer {
private final static String TOPIC_NAME="java-test";
private final static String CONSUMER_GROUP_NAME="testGroup";
public static void main(String[] args) {
Properties prop = new Properties();
// 配置kafka地址
prop.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.164.1:9092,192.168.164.1:9093,192.168.164.1:9094");
// 配置消费组名
prop.put(ConsumerConfig.GROUP_ID_CONFIG,CONSUMER_GROUP_NAME);
// 配置反序列化
prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
prop.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
// 配置长轮询poll最大消息数
prop.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,500);
// 消费者发送心跳的间隔
prop.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG,1000);
// 如果超过这个时间kafka都没有接收到消费者的心跳,则会将这个消费者踢出消费组,进行rebalance,将分区分配给其他消费者
prop.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG,10*1000);
// 如果两次消费的时间超出了这个时间间隔,则kafka认为这个消费者的性能较低,将它踢出消费组,进行rebalance
prop.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG,30*1000);
KafkaConsumer<String,String> consumer = new KafkaConsumer<String, String>(prop);
// 订阅topic
consumer.subscribe(Arrays.asList(TOPIC_NAME));
// 循环接收消息
while(true){
ConsumerRecords<String,String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息:partition=%d|offset=%d|key=%s|value=%s",record.partition(),record.offset(),record.key(),record.value());
}
if(!records.isEmpty()){
// 手动同步提交offset
//consumer.commitSync();
// 手动异步提交offset,当前线程不会阻塞
consumer.commitAsync((offsets, exception) -> {
if(exception!=null){
exception.printStackTrace();
}
});
}
}
}
}
offset自动提交和手动提交
长轮询poll消息
新消费组的消费offset规则
新消费组中的消费者在启动后,默认从当前分区的最后一条消息的offset+1开始消费(消费新消息),可以通过配置ConsumerConfig.AUTO_OFFSET_RESET_CONFIG为earliest第一次从头开始消费(默认是Lastest,即消费新消息)
配置文件
server:
port: 8080
spring:
kafka:
bootstrap-servers: 192.168.164.1:9092,192.168.164.1:9093,192.168.164.1:9094
producer:
# 重发次数,如果发送消息没有收到ack则会进行重试
retries: 3
# kafka单次从缓冲区读取的数据大小
batch-size: 16384
# 缓冲区大小
buffer-memory: 33554432
# ack模式,1表示需要leader收到消息
acks: 1
# 键值的序列化类
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
# 消费组id
group-id: default-group
# 是否开启自动提交
enable-auto-commit: false
# 首次连接offset规则,earliest表示从第一条消息开始消费
auto-offset-reset: earliest
# 键值的序列化类
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
# 一次轮询最多拉取的消息数量
max-poll-records: 500
listener:
# 消费者手动提交模式
# record 当记录被消费者监听器处理后提交
# batch 当一批poll的数据被消费者监听器处理后提交
# time 当一批poll的数据被消费者监听器处理后,距离上次提交时间大于ack-time时提交
# count 当一批poll的数据被消费者监听器处理后,被处理的消息数大于ack-count时提交
# count_time count或time满足一个即提交
# manual 当一批poll的数据被消费者监听器处理后,手动调用Acknowledgement.acknowledge()时提交
# manual_immediate 只要调用Acknowledgement.acknowledge()即提交,一般使用这种
ack-mode: manual_immediate
生产者
@RestController
@RequestMapping("msg")
public class KafkaController {
private final static String TOPIC_NAME="java-test";
@Autowired
private KafkaTemplate<String,String> kafkaTemplate;
@PostMapping("send")
public String sendMessage(String msg){
kafkaTemplate.send(TOPIC_NAME,0,"key",msg);
return "success";
}
}
消费者
@Component
public class KafkaConsumer {
@KafkaListener(topics = "java-test",groupId = "MyGroup1")
public void group1(ConsumerRecord<String,String> record, Acknowledgment acknowledgment){
System.out.println(record.value());
// 提交offset
acknowledgment.acknowledge();
}
}
在@KafkaListener中可以通过topicPartitions属性指定多个@TopicPartition注解来描述消费的分区,通过concurrency属性来指定kafka创建的消费者数量;在@TopicPartition中可以指定消费的topic、partition、partitionOffsets等
Kafka集群中的broker在zk中创建临时序号结点,序号最小的结点(即最先创建的结点)将会作为集群的controller,负责管理整个集群中的所有分区和副本的状态:
在消费者没有指明分区消费的前提下,当消费组里的消费者和分区的关系发生变化,就会触发rebalance,这个机制会重新调整消费者消费哪个分区,在触发rebalance之前,消费者消费哪个分区有三种策略:
HW俗称高水位,取一个partition对应的ISR中最小的LEO(log-end-offset)设为HW,consumer最多只能消费到HW所在的位置,另外每个replica都有HW,leader和follower各自负责更新自己的HW的状态。对于leader新写入的消息,consumer不能立刻消费,leader会等待该消息所有ISR中的replicas同步更新HW,保证了如果leader所在的broker失效,该消息仍然可以从新选举出来的leader中获取
在防止消息丢失的基础上,如果生产者发完消息,因为网络抖动没有收到ack,但是broker已经收到了消息,此时下生产者会进行重试,于是broker就会收到多条相同的消息,造成消息重复消费
消息的消费者的消费速度远赶不上生产者生产消息的速度,就会导致kafka中大量数据没有被消费,当消息堆积雨来越多,会导致消费者寻址的性能越来越差,从而造成kafka性能降低,导致其他服务的访问性能也变慢,造成服务雪崩