Kafka核心概念介绍

本文详细介绍Kafka的异步化、服务解耦和削峰填谷应用场景,涵盖集群架构、Topic与Partition、副本和ISR概念,以及生产者和消费者的基础搭建、配置和关键参数。深入解析生产者消息发送、序列化与拦截器,以及消费者模型、多线程消费和提交位移策略。
摘要由CSDN通过智能技术生成

1 Kafka 核心概念详解

1.1 Kafka(MQ) 的应用场景

1.1.1 Kafka(MQ)之异步化、服务解耦、削峰填谷

  • 异步化

    kafka_1_1

  • 服务解耦、削峰填谷

    kafka_1_2

1.1.2 Kafka 海量日志收集

kafka_1_3

image-20210203211950113

  • Kafka 之数据同步应用

    kafka_1_4

  • Kafka 之实时计算

    kafka_1_5

1.2 Kafka 基本概念

1.2.1 集群架构概念

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7UDKwvBd-1617803600702)(https://gitee.com/littlefxc/oss/raw/master/images/kafka_1_6.png)]

1.2.2 Topic、Partition

kafka_1_7

1.2.3 副本(replica)

kafka_1_8

1.2.4 ISR详解(In Sync Replicas)

kafka_1_9

上图表示拉取及时的情况

kafka_1_10

上图表示拉取滞后的情况。

PS: 当Kafka集群中的 leader 挂了之后,Kafka集群会重新选举leader,这是只有在 ISR 集合里面的Kafka才会被选举成为leader。

  • HW: High Watermark, 高水位线,消费者只能最多拉取到高水位线的消息

  • LEO: Log End Offset,日志文件的最后一条记录的 offset(偏移量)

  • ISR 集合与 HW 和 LEO 直接存在着密不可分的关系

kafka_1_11

kafka_1_12

kafka_1_13

上图右边的图形表示数据传入到leader节点,但还没有同步到follower节点上

kafka_1_14

上图HW移动了一格,表示 follower1 节点和follower2 节点都同步了第3条数据,而第4条数据因为follower2节点没有同步到,Kafka消费者就消费不了第4条数据。

1.3 Kafka 环境搭建

zookeeper 集群搭建

Kafka 集群搭建

Kafka Manager - Kafka集群管理工具
kafka 命令行工具常用命令行操作

1.4 Kafka 极速入门

1.4.1 构建生产者步骤

  1. 配置生产者参数属性和创建生产者对象
  2. 构建消息:ProducerRecord
  3. 发送消息
  4. 关闭生产者

1.4.2 构建消费者步骤

  1. 配置消费者参数属性和创建消费者对象
  2. 订阅主题
  3. 拉取消息并进行消费处理
  4. 提交消费偏移量,关闭消费者

1.4.3 代码实现

1.4.3.1 配置类
/**
 * kafka配置类
 */
@Configuration
public class KafkaConfig {

    @Bean
    public KafkaProducer<String, String> producerRecord() {

        Properties properties = new Properties();
        // 配置kafka集群地址,不用将全部机器都写上,zk会自动发现全部的kafka broke
        properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092,localhost:9093");
        // 设置发送消息的应答方式
        properties.setProperty(ProducerConfig.ACKS_CONFIG, "all");
        // 重试次数
        properties.setProperty(ProducerConfig.RETRIES_CONFIG, "3");
        // 重试间隔时间
        properties.setProperty(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, "100");
        // 一批次发送的消息大小 16KB
        properties.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "16348");
        // 一个批次等待时间,10ms
        properties.setProperty(ProducerConfig.LINGER_MS_CONFIG, "10");
        // RecordAccumulator 缓冲区大小  32M,如果缓冲区满了会阻塞发送端
        properties.setProperty(ProducerConfig.BUFFER_MEMORY_CONFIG, "33554432");
        // 配置拦截器, 多个逗号隔开
        properties.setProperty(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, "com.xiaolyuh.interceptor.TraceInterceptor");

        Serializer<String> keySerializer = new StringSerializer();
        Serializer<String> valueSerializer = new StringSerializer();

        return new KafkaProducer<>(properties, keySerializer, valueSerializer);
    }

}


1.4.3.2 生产者
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootStudentKafkaApplicationTests {

    @Autowired
    private KafkaProducer<String, String> kafkaProducer;

    @Test
    public void testSyncKafkaSend() throws Exception {
        // 同步发送测试
        for (int i = 0; i < 100; i++) {
            ProducerRecord<String, String> producerRecord = new ProducerRecord<>("test_cluster_topic", "key-" + i, "value-" + i);
            // 同步发送,这里我们还可以指定发送到那个分区,还可以添加header
            kafkaProducer.send(producerRecord, new KafkaCallback<>(producerRecord)).get(50, TimeUnit.MINUTES);
        }

        System.out.println("ThreadName::" + Thread.currentThread().getName());
    }

    @Test
    public void testAsyncKafkaSend() {
        // 异步发送测试
        for (int i = 0; i < 100; i++) {
            ProducerRecord<String, String> producerRecord = new ProducerRecord<>("test_cluster_topic2", "key-" + i, "value-" + i);
            // 异步发送,这里我们还可以指定发送到那个分区,还可以添加header
            kafkaProducer.send(producerRecord, new KafkaCallback<>(producerRecord));
        }

        System.out.println("ThreadName::" + Thread.currentThread().getName());
        // 记得刷新,否则消息有可能没有发出去
        kafkaProducer.flush();
    }
}

/**
 * 异步回调函数,该方法会在 Producer 收到 ack 时调用,当Exception不为空表示发送消息失败。
 *
 * @param <K>
 * @param <V>
 */
class KafkaCallback<K, V> implements Callback {
    private final ProducerRecord<K, V> producerRecord;

    public KafkaCallback(ProducerRecord<K, V> producerRecord) {
        this.producerRecord = producerRecord;
    }

    @Override
    public void onCompletion(RecordMetadata metadata, Exception exception) {
        System.out.println("ThreadName::" + Thread.currentThread().getName());
        if (Objects.isNull(exception)) {
            System.out.println(metadata.partition() + "-" + metadata.offset() + ":::" + producerRecord.key() + "=" + producerRecord.value());
        }

        if (Objects.nonNull(exception)) {
            // TODO  告警,消息落库从发
        }
    }
}
1.4.3.3 消费者

Kafka中的消息消费是一个不断轮询的过程,消费者所要做的就是重复地调用poll()方法,而poll()方法返回的是所订阅的主题(分区)上的一组消息。

@Component
public class KafkaConsumerDemo {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 10,
            0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1));

    @PostConstruct
    public void startConsumer() {
        executor.submit(() -> {
            Properties properties = new Properties();
            properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092,localhost:9093");
            
            // 非常重要的属性配置:与我们的消费者订阅组有关系
            properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "groupId");
            // 消费者提交 offset:自动提交 & 手工提交,默认是自动提交
            properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
            // 请求超时时间
            properties.setProperty(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, "60000");
            // 序列化
            Deserializer<String> keyDeserializer = new StringDeserializer();
            Deserializer<String> valueDeserializer = new StringDeserializer();
            // 创建消费者对象
            KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties, keyDeserializer, valueDeserializer);
            // 订阅感兴趣的主题
            consumer.subscribe(Arrays.asList("test_cluster_topic"));

            // KafkaConsumer的assignment()方法来判定是否分配到了相应的分区,如果为空表示没有分配到分区
            Set<TopicPartition> assignment = consumer.assignment();
            while (assignment.isEmpty()) {
                // 阻塞1秒
                consumer.poll(1000);
                assignment = consumer.assignment();
            }

            // KafkaConsumer 分配到了分区,开始消费
            while (true) {
                // 拉取记录,如果没有记录则柱塞1000ms。
                ConsumerRecords<String, String> records = consumer.poll(1000);
                for (ConsumerRecord<String, String> record : records) {
                    String traceId = new String(record.headers().lastHeader("traceId").value());
                    System.out.printf("traceId = %s, offset = %d, key = %s, value = %s%n", traceId, record.offset(), record.key(), record.value());
                }

                // 异步确认提交
                consumer.commitAsync((offsets, exception) -> {
                    if (Objects.isNull(exception)) {
                        // TODO 告警、落盘、重试
                    }
                });
            }
        });

    }
}
1.4.3.4 拦截器
/**
 * 链路ID
 */
public class TraceInterceptor implements ProducerInterceptor<String, String> {
    private int errorCounter = 0;
    private int successCounter = 0;

    /**
     * 最先调用,读取配置信息,只调用一次
     */
    @Override
    public void configure(Map<String, ?> configs) {
        System.out.println(JSON.toJSONString(configs));
    }

    /**
     * 它运行在用户主线程中,在消息序列化和计算分区之前调用,这里最好不小修改topic 和分区参数,否则会出一些奇怪的现象。
     *
     * @param record
     * @return
     */
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {

        Headers headers = new RecordHeaders();
        headers.add("traceId", UUID.randomUUID().toString().getBytes(Charset.forName("UTF8")));
        // 修改消息
        return new ProducerRecord<>(record.topic(), record.partition(), record.key(), record.value(), headers);
    }

    /**
     * 该方法会在消息从 RecordAccumulator 成功发送到 Kafka Broker 之后,或者在发送过程 中失败时调用。
     * 并且通常都是在 producer 回调逻辑触发之前调用。
     * onAcknowledgement 运行在 producer 的 IO 线程中,因此不要在该方法中放入很重的逻辑,否则会拖慢 producer 的消息 发送效率。
     *
     * @param metadata
     * @param exception
     */
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        if (Objects.isNull(exception)) {
            // TODO  出错了
        }
    }

    /**
     * 关闭 interceptor,主要用于执行一些资源清理工作,只调用一次
     */
    @Override
    public void close() {
        System.out.println("==========close============");
    }
}

1.5 Kafka 基本配置参数讲解

  • 配置文件: $KAFKA_HOME/config/server.properties

    • zookeeper.connect

      CS格式参数,可以指定值为zk1:2181,zk2:2181,zk3:2181,不同Kafka集群可以指定:zk1:2181,zk2:2181,zk3:2181/kafka1,chroot只需要写一次

    • listeners

      设置内网访问Kafka服务的监听器

    • broker.id

      每个broker都可以用一个唯一的非负整数id进行标识;这个id可以作为broker的“名字”,并且它的存在使得broker无须混淆consumers就可以迁移到不同的host/port上。你可以选择任意你喜欢的数字作为id,只要id是唯一的即可。

    • log.dir 和 log.dirs

      kafka存放数据的路径。这个路径并不是唯一的,可以是多个,路径之间只需要使用逗号分隔即可;每当创建新partition时,都会选择在包含最少partitions的路径下进行。

    • message.max.bytes

      server可以接收的消息最大尺寸。重要的是,consumer和producer有关这个属性的设置必须同步,否则producer发布的消息对consumer来说太大。

  • 详细配置参数

1.6 Kafka 之生产者

1.6.1 发送消息:ProducerRecord

public class ProducerRecord<K, V> {
    private final String topic;
    private final Integer partition;
    private final Headers headers;
    private final K key;
    private final V value;
    private final Long timestamp;
    // ...
}

PS: 一条消息会通过 Key 去计算出来实际的 partition,按照 partitiion 去存储的。

1.6.2 必要的参数配置项

  • bootstrap.servers:逗号分隔符,多个地址,防止单点故障

  • key.serializer, value.serializer:kafka实际发送的是二进制的内容,所以必须序列化

  • client.id:kafka 对应生产者的ID。如果不设置,Kafka 内部会自动生成一个非空字符串

  • 简化的配置Key: ProducerConfig

  • KafkaProducer 是线程安全的(kafka消费者不是线程安全的)

1.6.3 发送消息的3种方法

image-20210225221350755

Kafka 发送消息提供了 3 种方法:

  • sendOffsetsToTransaction: 事务相关
  • send(ProducerRecord<K,V>):Future:异步,但是使用 Future.get()方法相当于同步
  • send(ProducerRecord<K,V>, Callback):Future:异步,返回值会放到 Callback 回调函数里

1.6.4 KafkaProducer 消息发送重试机制

  • retries 参数
  • 可重试异常(例如:网络抖动) & 不可重试异常(例如:磁盘满了、消息体积太大)

1.7 Kafka 之生产者重要参数详解

  • acks: 指定发送消息后,Broker端至少有多少个副本接收到该消息;默认 acks = 1;(Broker端只要主分区写入成功,就可以给客户端去回送响应,如果leader宕机了,则会丢失数据

    这里写图片描述

  • acks = 0:生产者发送消息之后不需要等待任何服务端的响应;(这种情况下数据传输效率最高,但是数据可靠性确是最低的。)

  • acks = -1 或者 acks=all:生产者在发送消息之后,需要等待 ISR 中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应。

    你以为这样就能保证数据不丢失了吗?例如当ISR中的成员只有leader的时候,就相当于 acks=1 了。

    那么该怎么样保证数据的可靠性能?还需要min.insync.replicas这个参数(可以在broker或者topic层面进行设置)的配合,这样才能发挥最大的功效。

    • min.insync.replicas这个参数设定ISR中的最小副本数是多少,默认值为1,当且仅当request.required.acks参数设置为-1时,此参数才生效。

      如果ISR中的副本数少于min.insync.replicas配置的数量时,客户端会返回异常:org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required。

    • ISR中的flower全部完成数据同步后,leader此时挂掉,会重新选举leader,数据不会丢失。

    这里写图片描述

    • 数据发送到leader后 ,部分ISR的副本同步,leader此时挂掉。比如follower1和follower2都有可能变成新的leader, producer端会得到返回异常,producer端会重新发送数据,数据可能会重复。

    这里写图片描述

  • max.request.size:该参数用来限制生产者客户端能发送的消息的最大值,默认 1M(10485768)

  • retries 和 retry.backoff.msretries: 重试次数和重试间隔,默认100

  • compression.type: 这个参数用来指定消息的压缩方式,默认值为 none , 可选配置:gzip,snappy 和 lz4

  • connections.max.idle.ms:这个参数用来指定在多久之后关闭限制的连接,默认值是54000ms,即9分钟

  • linger.ms:这个参数用来指定生产者发送 ProducerBatch 之前等待更多消息(ProducerRecord)加入ProducerBatch的时间,默认值为0

  • batch.size:累计多少条消息,则一次进行批量发送

  • buffer.memory:缓存提升性能参数,默认 32 M

  • receive.buffer.bytes: 这个参数用来设置Socket接受消息缓冲区(SO_RECBUF)的大小,默认值为32678(B),即32KB

  • send.buffer.bytes: 这个参数用来设置Socket发送消息缓存区(SO_SNDBUF)的大小,默认值为131072(B),即128KB。

  • request.timeout.ms: 这个参数用来配置Producer等待请求响应的最长时间,默认值为 3000 ms

1.8 Kafka 之拦截器

拦截器(interceptor):Kafka对应着有生产者和消费者两种拦截器

生产者实现接口:org.apache.kafka.clients.producer.ProducerInterceptor

消费者实现接口:org.apache.kafka.clients.consumer.ConsumerInterceptor

1.9 Kafka 之序列化和反序列化

  • 序列化反序列化:生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送Kafka;而在对侧,消费者需要用反序列化器(Derializer)把从Kafka中收到的字节数组转换成相应的对象。

  • 序列化接口:org.apache.kafka.common.serialization.Serializer

    除了用于String类型的序列化器之外还有:ByteArray、ByteBuffer、Bytes、Double、Integer、Long这几种类型,它们都实现了org.apache.kafka.common.serialization.Serializer接口,此接口有三种方法:

    • public void configure(Map<String, ?> configs, boolean isKey):用来配置当前类。

    • public byte[] serialize(String topic, T data):用来执行序列化。

    • public void close():用来关闭当前序列化器。一般情况下这个方法都是个空方法,如果实现了此方法,必须确保此方法的幂等性,因为这个方法很可能会被KafkaProducer调用多次。

    如何自定义序列化?

    /**
     * 要序列化的类
     */
    @Data
    public class User{
      private String id;
      private String name;
    }
    
    /**
     * 序列化实现类
     */
    public class UserSerializer implements Serializer<User> {
        public void configure(Map<String, ?> configs, boolean isKey) {}
        public byte[] serialize(String topic, User data) {
            if (data == null) {
                return null;
            }
            byte[] id, name;
            try {
                if (data.getId() != null) {
                    id = data.getId().getBytes("UTF-8");
                } else {
                    name = new byte[0];
                }
                if (data.getName() != null) {
                    name = data.getName().getBytes("UTF-8");
                } else {
                    name = new byte[0];
                }
                ByteBuffer buffer = ByteBuffer.allocate(4+4+id.length + name.length);
                buffer.putInt(id.length);
                buffer.put(id);
                buffer.putInt(name.length);
                buffer.put(name);
                return buffer.array();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return new byte[0];
        }
        public void close() {}
    }
    
    // 使用: 记得也要将相应的String类型改为User类型,如:
    properties.put("value.serializer", "com.examples.fengxuechao.UserSerializer");
    Producer<String,User> producer = new KafkaProducer<String,User>(properties);
    User user = new User("1", "Hi");
    ProducerRecord<String, User> producerRecord = new ProducerRecord<String, User>(topic,user);
    
    
  • 反序列化接口:org.apache.kafka.common.serialization.Derializer

    同接口同样有 3 个方法:

    • public void configure(Map<String, ?> configs, boolean isKey):用来配置当前类。
    • public byte[] serialize(String topic, T data):用来执行反序列化。如果data为null建议处理的时候直接返回null而不是抛出一个异常。
    • public void close():用来关闭当前序列化器。

    如何反序列化?

    public class UserDeserializer implements Deserializer<User> {
        public void configure(Map<String, ?> configs, boolean isKey) {}
        public User deserialize(String topic, byte[] data) {
            if (data == null) {
                return null;
            }
            if (data.length < 8) {
                throw new SerializationException("Size of data received by UserDeserializer is shorter than expected!");
            }
            ByteBuffer buffer = ByteBuffer.wrap(data);
            int idLen, nameLen;
            String id, name;
            idLen = buffer.getInt();
            byte[] idBytes = new byte[idLen];
            buffer.get(idBytes);
            nameLen = buffer.getInt();
            byte[] nameBytes = new byte[nameLen];
            buffer.get(nameBytes);
            try {
                id = new String(idBytes, "UTF-8");
                name = new String(nameBytes, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new SerializationException("Error occur when deserializing!");
            }
            return new User(name,address);
        }
        public void close() {}
    }
    

其实序列化完全可以和Avro、ProtoBuf等联合使用,而且更加的方便快捷。不过,如无必要,用默认的String序列化就可以了(使用自定义的序列化就不容易变了,如User类要添加一个属性)。

1.10 Kafka 之分区器

image-20210325223028295

上图是生产者发送消息后会经历一系列的过程:

  1. 生产者发送消息
  2. 拦截器
  3. 序列化
  4. 分区:如果消息中没有指定分区,就会使用分区器
  5. 到达Broker

生产者消息

public class ProducerRecord<K, V> {
    // 所要发送的topic
    private final String topic;       
    // 指定的partition序号
    private final Integer partition;  
    // 一组键值对,与RabbitMQ中的headers类似,kafka0.11.x版本才引入的一个属性
    private final Headers headers;    
    // 消息的key
    private final K key;
    // 消息的value,即消息体
    private final V value;
    // 消息的时间戳
    private final Long timestamp;
    // ...
}

org.apache.kafka:kafka-clients:2.0.1中的 KafkaProducerpartition源码如下:

/**
     * computes partition for given record.
     * if the record has partition returns the value otherwise
     * calls configured partitioner class to compute the partition.
     */
    private int partition(ProducerRecord<K, V> record, byte[] serializedKey, byte[] serializedValue, Cluster cluster) {
        Integer partition = record.partition();
        return partition != null ?
                partition :
                partitioner.partition(
                        record.topic(), record.key(), serializedKey, record.value(), serializedValue, cluster);
    }

可以看出的确是先判断有无指明ProducerRecord的partition字段,如果没有指明,则再进一步计算分区。上面这段代码中的partitioner在默认情况下是指Kafka默认实现的org.apache.kafka.clients.producer.DefaultPartitioner,其源码如下:

/**
 * The default partitioning strategy:
 * <ul>
 * <li>If a partition is specified in the record, use it
 * <li>If no partition is specified but a key is present choose a partition based on a hash of the key
 * <li>If no partition or key is present choose a partition in a round-robin fashion
 */
public class DefaultPartitioner implements Partitioner {

    private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();

    public void configure(Map<String, ?> configs) {}

    /**
     * Compute the partition for the given record.
     *
     * @param topic The topic name
     * @param key The key to partition on (or null if no key)
     * @param keyBytes serialized key to partition on (or null if no key)
     * @param value The value to partition on or null
     * @param valueBytes serialized value to partition on or null
     * @param cluster The current cluster metadata
     */
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        if (keyBytes == null) {
            int nextValue = nextValue(topic);
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() > 0) {
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                return availablePartitions.get(part).partition();
            } else {
                // no partitions are available, give a non-available partition
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
            // hash the keyBytes to choose a partition
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

    private int nextValue(String topic) {
        AtomicInteger counter = topicCounterMap.get(topic);
        if (null == counter) {
            counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
            AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
            if (currentCounter != null) {
                counter = currentCounter;
            }
        }
        return counter.getAndIncrement();
    }

    public void close() {}

}

由上源码可以看出partition的计算方式:

  1. 如果key为null,则按照一种轮询的方式来计算分区分配
  2. 如果key不为null则使用称之为murmur的Hash算法(非加密型Hash函数,具备高运算性能及低碰撞率)来计算分区分配。

当然我们也可自定义自己的分区器,如:

public class UserPartitioner implements Partitioner {

    private final AtomicInteger atomicInteger = new AtomicInteger(0);

    @Override
    public void configure(Map<String, ?> configs) {}

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        if (null == keyBytes || keyBytes.length<1) {
            return atomicInteger.getAndIncrement() % numPartitions;
        }
        //借用String的hashCode的计算方式
        int hash = 0;
        for (byte b : keyBytes) {
            hash = 31 * hash + b;
        }
        return hash % numPartitions;
    }

    @Override
    public void close() {}
}

1.11 Kafka 之消费者与消费者组

1.11.1 概念

image-20210325232006969

说明:

  • 一个Topic可以有多个分区

  • 一个主题可以有多个消费者组

  • 一个消费者组可以有多个消费者,一个消费者只能属于一个消费者组

  • 每一个分区可以被多个消费者组消费,每一个分区只能被一个消费者组中的一个消费者所消费,详见下图

    image-20210329224142774

    image-20210329224552763

    一个消费者组内的消费者数量多于分区时,多出来的消费者不做任何事。

1.11.2 消息中间件模型

  • 点对点(P2P,Point-to-Point)模式

    点对点模式是基于队列的,消息生产者发送消息到队列,消息消费者从队列接受消息。

  • 发布/订阅(Pub/Sub)模式

    发布/订阅模式定义了如何向一个内容节点发布和订阅消息,这个内容节点成为主题(Topic),主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者从主题中订阅消息

  • Kafka同时支持两种消息投递模式,而这正是得益于消费者与消费者组模型的契合

    • 所有的消费者都隶属于同一个消费组,相当于点对点模型
    • 所有的消费者都隶属于不同的消费者组,相当于发布/订阅模型

1.11.3 Kafka 消费者必要参数方法

  • bootstrap.servers: 用来指定连接Kafka集群所需的broker地址清单
  • key.deserializer 和 value.deserializer: 反序列化参数
  • group.id: 消费者所属消费组
  • subscribe:消费主题订阅,支持集合/标准正则表达式
  • assign:只订阅主题的某个分区

1.11.4 kafka 消费者提交位移

image-20210331223055144

在实际的工作中一般采用手动提交位移的方式,这样会有比较好的容错性,我们会知道这条消息到底有没有消费成功,如果处理失败,那我们可以再次提交等兜底的策略。

Kafka 自动提交参数

- 自动提交:enable.auto.commit, 默认 true
- 提交周期间隔:auto.commit.interval.ms,默认值为 5 秒

手工提交参数

  • enable.auto.commit,配置为 false
  • 提交方式:commitSync &commitAsync
  • 同步提交:整体提交 & 分区提交

1.11.5 消费者subscribe 与 assign 详解

image-20210331233706053

从上图中可以看到 subscribe 方法有 4 个重载的方法,对于 KafkaConsumer 消息的订阅,可以有多个主题,也可以支持正则表达式匹配。

假如我们只想要订阅一个 partition 呢?
使用 assign 方法

// 源码
public class PartitionInfo {

    private final String topic; // 主题
    private final int partition; 分区
    private final Node leader; // 主节点
    private final Node[] replicas; // Kafka 节点
    private final Node[] inSyncReplicas; // ISR Kafka 节点
    private final Node[] offlineReplicas; // OSR Kafka节点
    
    // 省略方法
}

// 源码
public final class TopicPartition implements Serializable {

    private int hash = 0;
    private final int partition;
    private final String topic;
}

# 拉取某个主题下的所有分区
List<PartitionInfo> tpInfoList = consumer.partitionsFor("topic")
# 订阅主题为 topic 的 第 0 个分区,0 是从 PartitionInfo 中取来的
consumer.assign(Arrays.asList(new TopicPartition("topic", 0)))

1.11.6 Kafka消费者之多线程

  • KafkaProducer 是线程安全的,但是KafkaConsumer却是线程非安全的
  • KafkaConsumer中定义了一个 acquire方法用来检测是否只有一个线程在操作,如果有其它线程操作则会抛出 ConcurrentModifactionException
  • KafkaConsumer在执行所有动作时都会先执行 acquire 方法检测是否线程安全

image-20210406221942118

image-20210406224758405

image-20210406230608886

1.11.7 Kafka 消费者重要参数

性能调优参考

  • fetch.min.bytes: 一次拉取最小数据量,默认1B
  • fetch.max.bytes: 一次拉取最大数据量,默认50M
  • max.partition.fetch.bytes: 一次fetch请求,从一个partition中取得的records最大大小,默认1M
  • fetch.max.wait.ms: Fetch 请求发给broker后,在broker中可能会被阻塞的时长,默认500
  • fetch.poll.records: Consumer 每次调用 poll() 时取到的records的最大数,默认 500 条

参考资源

kafka数据可靠性深度解读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值