Kafka原理与实战

一、Kafka基本理论


Topic:Kafka将消息种子(Feed)分门别类,每一类的消息称之为一个主题(Topic).
Producer:发布消息的对象称之为主题生产者(Kafka topic producer)
Consumer:订阅消息并处理发布的消息的种子的对象称之为主题消费者(consumers)
Broker:已发布的消息保存在一组服务器中,称之为Kafka集群。集群中的每一个服务器都是一个代理(Broker). 消费者可以订阅一个或多个主题(topic),并从Broker拉数据,从而消费这些已发布的消息。

Partition(分区):对于每一个topic, Kafka集群都会维持一个分区日志,Kafka里的消息用主题进行分类,主题下有可以被分为若干个分区。分区本质上是个提交日志,有新消息,这个消息就会以追加的方式写入分区,然后用先入先出的顺序读取。Kafka 中的分区可以分布在不同的服务器(broker)上,也就是说,一个主题可以横跨多个 broker。每一条消息被发送到 broker 之前,会根据分区规则选择存储到哪个具体的分区。如果分区规则设定得合理,所有的消息都可以均匀地分配到不同的分区中。如果一个主题只对应一个文件,那么这个文件所在的机器I/O将会成为这个主题的性能瓶颈,而分区解决了这个问题。在创建主题的时候可以通过指定的参数来设置分区的个数,当然也可以在主题创建完成之后去修改分区的数量,通过增加分区的数量可以实现水平扩展。

Topic写入不同分区

多副本(Replica)机制,通过增加副本数量可以提升容灾能力。同一分区的不同副本中保存的是相同的消息(在同一时刻,副本之间并非完全一样),副本之间是“一主多从”的关系,其中 leader 副本负责处理读写请求,follower 副本只负责与 leader 副本的消息同步。副本处于不同的 broker 中,当 leader 副本出现故障时,从 follower 副本中重新选举新的 leader 副本对外提供服务。Kafka 通过多副本机制实现了故障的自动转移,当 Kafka 集群中某个 broker 失效时仍然能保证服务可用。

图1-3 多副本架构

Kafka的消费模型:
消息模型通常会分为两种: 队列和发布-订阅式。 队列的处理方式是一一对应,而发布-订阅模型,消息被广播给所有的消费者,Kafka为这两种模型提供了单一的消费者抽象模型: 消费者组 (consumer group)。 消费者用一个消费者组名标记自己。

  • 如果所有的消费者都隶属于同一个消费组,那么所有的消息都会被均衡地投递给每一个消费者,即每条消息只会被一个消费者处理,这就相当于点对点模式的应用。
  • 如果所有的消费者都隶属于不同的消费组,那么所有的消息都会被广播给所有的消费者,即每条消息会被所有的消费者处理,这就相当于发布/订阅模式的应用。

Kafka如何保证消息的顺序性?
在MQ方面,尽管MQ服务保证了消息顺序,但是消息还是异步的发送给各个消费者,消费者收到消息的先后顺序不能保证了。这也意味着并行消费将不能保证消息的先后顺序。如果只让一个消费者处理消息,又违背了并行处理的初衷。 在这一点上Kafka做的更好,尽管并没有完全解决上述问题。 Kafka采用了一种分而治之的策略:分区。 因为Topic分区中消息只能由消费者组中的唯一一个消费者处理,所以消息肯定是按照先后顺序进行处理的。但是它也仅仅是保证Topic的一个分区顺序处理,不能保证跨分区的消息先后处理顺序。 所以,如果你想要顺序的处理Topic的所有消息,那就只提供一个分区。

二、Kafka消息发送


消息发送的几个步骤:

  1. 配置生产者客户端参数及创建相应的生产者实例。
  2. 构建待发送的消息。
  3. 发送消息。
  4. 关闭生产者实例。

整个生产者客户端由两个线程协调运行,这两个线程分别为主线程和 Sender 线程(发送线程)。在主线程中由 KafkaProducer 创建消息,然后通过可能的拦截器、序列化器和分区器的作用之后缓存到消息累加器(RecordAccumulator,也称为消息收集器)中。Sender 线程负责从 RecordAccumulator 中获取消息并将其发送到 Kafka 中。

发送整体架构
1.Kafka消息的序列化

Kafka消息在传递的时候都需要进行序列化,即转成二进制数组才能进行传输,因此需要进行消息的序列化。Kafka Producer在发送消息时必须配置的参数为:bootstrap.servers、key.serializer、value.serializer。序列化操作是在拦截器(Interceptor)执行之后并且在分配分区(partitions)之前执行的。

public class KafkaProducerAnalysis {
    public static final String brokerList = "localhost:9092";
    public static final String topic = "topic-demo";

    public static Properties initConfig(){
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
            "org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
            "org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.CLIENT_ID_CONFIG, "producer.client.id.demo");
        return props;
    }

    public static void main(String[] args) {
        Properties props = initConfig();
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        ProducerRecord<String, String> record =
                new ProducerRecord<>(topic, "Hello, Kafka!");
        try {
            producer.send(record);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

除了用于String类型的序列化器之外还有:ByteArray、ByteBuffer、Bytes、Double、Integer、Long这几种类型,它们都实现了org.apache.kafka.common.serialization.Serializer接口。但是如果这些类型的Serializer都不能满足需求,读者可以选择使用如Avro、JSON、Thrift、ProtoBuf或者Protostuff等通用的序列化工具来实现,或使用自定义类型的Serializer来实现。这个地方可以参考《Kafka消息序列化和反序列化(上)》,包含完整的Demo。

2.Kafka消息的发送方式

我们可以通过生成者的send方法进行发送。send方法会返回一个包含RecordMetadataFuture对象。RecordMetadata里包含了目标主题,分区信息和消息的偏移量。在send发送动作发生以后可以采用同步或者异步方式对返回值做处理:

  • 同步非阻塞方式
record =  new ProducerRecord<String,String>("topic","key","value");
Future<RecordMetadata> future = producer.send(record);
RecordMetadata recordMetadata = future.get();
if(null!=recordMetadata){
      logger.info("offset:"+recordMetadata.offset()+
          "-" +"partition:"+recordMetadata.partition());
}
  • 异步回调方式
record = new ProducerRecord<String,String>("topic","key","value");
producer.send(record, new Callback() {
     public void onCompletion(RecordMetadata metadata, Exception exception) {
         if(null!=exception){
             exception.printStackTrace();
         }
         if(null!=metadata){
             logger.info("offset:"+metadata.offset()+"-" +
                "partition:"+metadata.partition());
         }
     }
});
3.Kafka消息的发送确认机制

当producer向leader发送数据时,可以通过request.required.acks参数来设置数据可靠性的级别,可分为三种情况:

  • acks=0 无确认:这意味着producer无需等待来自broker的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
  • acks=1 一次确认(默认):这意味着producer在集群中的leader已成功收到数据并得到确认。如果leader宕机了,则会丢失数据。
  • acks=-1全部确认:producer需要等待集群中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当集群中只有leader时,这样就变成了acks=1的情况。
4.Kafka分区计算

Kafka Producer在调用send方法发送消息至broker的过程中,首先是经过拦截器Inteceptors处理,然后是经过序列化Serializer处理,之后就到了Partitions阶段,即分区分配计算阶段。有些业务场景,需要控制每条消息落到合适的分区中,有些场景则只要根据默认的分配规则即可。在KafkaProducer计算分配时,首先根据的是ProducerRecord中的partition字段指定的序号计算分区。ProducerRecord包含了目标主题,键和值,Kafka的消息都是一个个的键值对,键的主要用途有两个:一,用来决定消息被写往主题的哪个分区,拥有相同键的消息将被写往同一个分区,二,还可以作为消息的附加消息。分区分配计算阶段主要有三种:

  • 如果key为null,则按照一种轮询的方式来计算分区分配
  • 如果key不为null则使用Hash算法(非加密型Hash函数,具备高运算性能及低碰撞率)来计算分区分配。
  • 自定义分区,主要是通过实现Partitione接口的partition()方法。

三、Kafka消息接收


1、消费者与消费者组

按照Kafka默认的消费逻辑设定,一个分区只能被同一个消费组(ConsumerGroup)内的一个消费者消费。Topic会按照分区存消息,而消费者按照组的设定来取消息,往消费者群组里增加消费者是进行横向伸缩能力的主要方式。但是,当我们增加更多的消费者,超过了主题的分区数量,就会有一部分的消费者被闲置,不会接收到任何消息。

Kafka消息

譬如一个组内过多的消费者就会导致,有空闲的消费者将不会收到任何消息。因此,最优的设计就是,consumer group下的consumer thread的数量等于partition数量,这样效率是最高的。

kafka为了保证吞吐量,只允许同一个consumer group下的一个consumer线程去访问一个partition。如果觉得效率不高的时候,可以加partition的数量来横向扩展,那么再加新的consumer thread去消费。如果想多个不同的业务都需要这个topic的数据,起多个consumer group就好了,大家都是顺序的读取message,offset的值互不影响。这样没有锁竞争,充分发挥了横向的扩展性,吞吐量极高,这也就形成了分布式消费的概念。

2.分区再均衡

当消费者群组里的消费者发生变化,或者主题里的分区发生了变化,都会导致再均衡现象的发生,再均衡对保证了Kafka的高可用性和伸缩性。当消费者要加入群组时,会向群组协调器发送一个JoinGroup请求,第一个加入群主的消费者成为群主,群主会获得群组的成员列表,并负责给每一个消费者分配分区,之后再把分配信息反馈到协调器中。当消费者下线或者宕掉时,群组协调器将检测不到来自消费者的心跳,也会启动分区再均衡的发生。

Rebalance的触发条件:(1)Consumer增加或删除会触发 Consumer Group的Rebalance(2)Broker的增加或者减少都会触发 Consumer Rebalance

3.消费者消费消息

一个正常的消费逻辑需要具备以下几个步骤:

  1. 配置消费者客户端参数及创建相应的消费者实例。
  2. 订阅主题。
  3. 拉取消息并消费。
  4. 提交消费位移。
  5. 关闭消费者实例。

创建消费者后,使用subscribe()方法订阅主题,这个方法接受一个主题列表为参数,也可以接受一个正则表达式为参数,DEMO如下:

//主题列表为参数
public void subscribe(Collection<String> topics) ;
//正则表达式为参数
public void subscribe(Pattern pattern, ConsumerRebalanceListener listener) 

通过Poll拉取消息,可设置while循环中不断的Poll。poll方法将会返回一个记录(消息)列表,每一条记录都包含了记录所属的主题信息,分区信息,偏移量,以及键值对。

//参数为控制poll的阻塞时间,使得消费者在指定的毫秒数内一直等待broker返回数据
public ConsumerRecords<K, V> poll(long timeout)

Kafka Consumer的实现不是线程安全的,每个消费数据的线程都需设置自己的KafkaConsumer实例。

消费者客户端示例如下:

public class KafkaConsumerAnalysis {
    public static final String brokerList = "localhost:9092";
    public static final String topic = "topic-demo";
    public static final String groupId = "group.demo";
    public static final AtomicBoolean isRunning = new AtomicBoolean(true);

    public static Properties initConfig(){
        Properties props = new Properties();
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
        props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        props.put(ConsumerConfig.CLIENT_ID_CONFIG, "client.id.demo");
        return props;
    }

    public static void main(String[] args) {
        Properties props = initConfig();
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList(topic));

        try {
            while (isRunning.get()) {
                ConsumerRecords<String, String> records = 
                    consumer.poll(Duration.ofMillis(1000));
                for (ConsumerRecord<String, String> record : records) {
                    System.out.println("topic = " + record.topic() 
                            + ", partition = "+ record.partition() 
                            + ", offset = " + record.offset());
                    System.out.println("key = " + record.key()
                            + ", value = " + record.value());
                    //do something to process record.
                }
            }
        } catch (Exception e) {
            log.error("occur exception ", e);
        } finally {
            consumer.close();
        }
    }
}

4.消费者配置

消费者的配置项较多,有些已经设置了合理的默认值,无需调整。

  • enable .auto.commit,默认值true,表明消费者是否自动提交。为了尽量避免重复数据和数据丢失,可以改为false,自行控制提交时机。
  • partition.assignment.strategy,分区分配策略,有Range和RoundRobin两种策略,详情可参考Kafka分区分配策略——RangeAssignor

默认策略为——RangeAssignor策略,原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。对于每一个topic,RangeAssignor策略会将消费组内所有订阅这个topic的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。

5.消费者提交策略

  • 自动提交,最简单的方式,这种方式虽然效率高但是弊端也很明显,在“再均衡”时间窗口时,容易导致消息的重复消费,就算自动提交时间间隔再短也无法完全避免。
  • 手动同步提交commitSync(),这样可以在必要的时候提交当前偏移量,而不是基于自动提交的时间间隔。
  • 异步提交commitAsync(),上面的同步提交是阻塞式的,需等broker的响应才能走下一步,采用异步提交可以设置回调方法,一旦异步程序可以处理别的事情了。但是异步方式在通信中断或延迟情况下,会导致偏移量错乱的情况!
  • 组合提交,轮询中采用异步方式提高性能,在退出关闭前采用同步保证可靠性。
        try {
            consumer.subscribe(topicList);
            while(true){
                ConsumerRecords<String, String> records = consumer.poll(500);
                for(ConsumerRecord<String, String> record:records){
                    //do business work
                }
                //异步提交
                consumer.commitAsync();
            }
        } catch (CommitFailedException e) {
            System.out.println("Commit failed:");
            e.printStackTrace();
        } finally {
            try {
                //同步提交                
                consumer.commitSync();
            } finally {
                consumer.close();
            }
        }

6.位移提交

每次调用 poll() 方法时,返回的是还没有被消费过的消息集,要做到这一点,就需要记录上一次消费时的消费位移。并且这个消费位移必须做持久化保存,而不是单单保存在内存中,否则消费者重启之后就无法知晓之前的消费位移。且当有新的消费者加入时,那么必然会有再均衡的动作,对于同一分区而言,它可能在再均衡动作之后分配给新的消费者,如果不持久化保存消费位移,那么这个新的消费者也无法知晓之前的消费位移。

在新消费者客户端中,消费位移存储在 Kafka 内部的主题__consumer_offsets 中。这里把将消费位移存储起来(持久化)的动作称为“提交”,消费者在消费完消息之后需要执行消费位移的提交。

7.消费者多线程实现

KafkaConsumer 非线程安全并不意味着我们在消费消息的时候只能以单线程的方式执行。如果生产者发送消息的速度大于消费者处理消息的速度,那么就会有越来越多的消息得不到及时的消费,造成了一定的延迟。

可以通过多线程的方式来实现消息消费,多线程的目的就是为了提高整体的消费能力,主要方式有两种:

  • 1.线程封闭,即为每个线程实例化一个 KafkaConsumer 对象。
image

一个线程对应一个 KafkaConsumer 实例,我们可以称之为消费线程。一个消费线程可以消费一个或多个分区中的消息,所有的消费线程都隶属于同一个消费组。这种实现方式的并发度受限于分区的实际个数:

public class FirstMultiConsumerThreadDemo {
    public static final String brokerList = "localhost:9092";
    public static final String topic = "topic-demo";
    public static final String groupId = "group.demo";

    public static Properties initConfig(){
        Properties props = new Properties();
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
        props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
        return props;
    }

    public static void main(String[] args) {
        Properties props = initConfig();
        int consumerThreadNum = 4;
        for(int i=0;i<consumerThreadNum;i++) {
            new KafkaConsumerThread(props,topic).start();
        }
    }

    public static class KafkaConsumerThread extends Thread{
        private KafkaConsumer<String, String> kafkaConsumer;

        public KafkaConsumerThread(Properties props, String topic) {
            this.kafkaConsumer = new KafkaConsumer<>(props);
            this.kafkaConsumer.subscribe(Arrays.asList(topic));
        }

        @Override
        public void run(){
            try {
                while (true) {
                    ConsumerRecords<String, String> records =
                            kafkaConsumer.poll(Duration.ofMillis(100));
                    for (ConsumerRecord<String, String> record : records) {
                        //处理消息模块    ①
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                kafkaConsumer.close();
            }
        }
    }
}

上面这种多线程的实现方式和开启多个消费进程的方式没有本质上的区别,它的优点是每个线程可以按顺序消费各个分区中的消息。缺点也很明显,每个消费线程都要维护一个独立的TCP连接,如果分区数和 consumerThreadNum 的值都很大,那么会造成不小的系统开销。

  • 2.将处理消息模块改成多线程的实现方式。一般而言,poll() 拉取消息的速度是相当快的,而整体消费的瓶颈也正是在处理消息这一块,通过开启多个 KafkaConsumerThread 实例来进一步提升整体的消费能力,可以减少TCP连接对系统资源的消耗,不过缺点就是对于消息的顺序处理就比较困难了。
3-11

将处理消息模块改成多线程的实现方式,具体实现:

public class ThirdMultiConsumerThreadDemo {
    public static final String brokerList = "localhost:9092";
    public static final String topic = "topic-demo";
    public static final String groupId = "group.demo";

    //省略initConfig()方法,具体请参考代码清单14-1
    public static void main(String[] args) {
        Properties props = initConfig();
        KafkaConsumerThread consumerThread = 
                new KafkaConsumerThread(props, topic,
                Runtime.getRuntime().availableProcessors());
        consumerThread.start();
    }

    public static class KafkaConsumerThread extends Thread {
        private KafkaConsumer<String, String> kafkaConsumer;
        private ExecutorService executorService;
        private int threadNumber;

        public KafkaConsumerThread(Properties props, 
                String topic, int threadNumber) {
            kafkaConsumer = new KafkaConsumer<>(props);
            kafkaConsumer.subscribe(Collections.singletonList(topic));
            this.threadNumber = threadNumber;
            executorService = new ThreadPoolExecutor(threadNumber, threadNumber,
                    0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1000),
                    new ThreadPoolExecutor.CallerRunsPolicy());
        }

        @Override
        public void run() {
            try {
                while (true) {
                    ConsumerRecords<String, String> records =
                            kafkaConsumer.poll(Duration.ofMillis(100));
                    if (!records.isEmpty()) {
                        executorService.submit(new RecordsHandler(records));
                    }    ①
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                kafkaConsumer.close();
            }
        }

    }

    public static class RecordsHandler extends Thread{
        public final ConsumerRecords<String, String> records;

        public RecordsHandler(ConsumerRecords<String, String> records) {
            this.records = records;
        }

        @Override
        public void run(){
            //处理records.
        }
    }
}

四、运维和管理


Kafka对运维的要求较高,掌握的常用运维指令如下:
1、查看topic状态
./kafka-topics.sh --describe --zookeeper zk-1:2181,zk-2:2181,zk-3:2181 --topic applog

2、查看所有topic列表:
sh kafka-topics.sh --zookeeper zk-1:2181,zk-2:2181,zk-3:2181 --list

3、创建topic
sh kafka-topics.sh --zookeeper zk-1:2181,zk-2:2181,zk-3:2181 --create --topic applog --partitions 6 --replication-factor 2

注意:server.properties 设置 delete.topic.enable=true

4.删除主题数据
./kafka-topics.sh --delete --zookeeper zk-1:2181,zk-2:2181,zk-3:2181 --topic applog

5.生产topic的消息
./kafka-console-producer.sh --broker-list kafka-1:9092 ,kafka-2:9092,kafka-3:9092 --topic applog

6.消费topic的消息
./kafka-console-consumer.sh --zookeeper zk-1:2181,zk-2:2181,zk-3:2181 --topic applog

查看topic主题的说明,在Linux主机执行查看详情如下:

$./kafka-topics.sh --zookeeper localhost:2181/ kafka --describe --topic topic-create
Topic:topic-create  PartitionCount:4    ReplicationFactor:2 Configs:
    Topic: topic-create Partition: 0    Leader: 2   Replicas: 2,0   Isr: 2,0
    Topic: topic-create Partition: 1    Leader: 0   Replicas: 0,1   Isr: 0,1
    Topic: topic-create Partition: 2    Leader: 2   Replicas: 1,2   Isr: 2,1
    Topic: topic-create Partition: 3    Leader: 2   Replicas: 2,1   Isr: 2,1

示例中的 Topic 和 Partition 分别表示主题名称和分区号。PartitionCount 表示主题中分区的个数,ReplicationFactor 表示副本因子,而 Configs 表示创建或修改主题时指定的参数配置。Leader 表示分区的 leader 副本所对应的 brokerId,Isr 表示分区的 ISR 集合,Replicas 表示分区的所有的副本分配情况,即AR集合,其中的数字都表示的是 brokerId。

修改主题,当一个主题被创建之后,依然允许我们对其做一定的修改,比如修改分区个数、修改配置等,这个修改的功能就是由 kafka-topics.sh 脚本中的 alter 指令提供的。目前 Kafka 只支持增加分区数而不支持减少分区数。比如我们再将主题 topic-config 的分区数修改为1,就会报出 InvalidPartitionException 的异常。

删除主题,如果确定不再使用一个主题,那么最好的方式是将其删除,这样可以释放一些资源:

  • 删除主题
    ./kafka-topics.sh --delete --zookeeper zk-1:2181,zk-2:2181,zk-3:2181 --topic applog
  • 删除 ZooKeeper 中的节点/config/topics/applog
    delete /config/topics/applog
  • 删除 ZooKeeper 中的节点/brokers/topics/applog 及其子节点
    rmr /brokers/topics/applog
  • 删除集群中所有与主题 applog 有关的文件
    rm -rf applog-*

五、思考题


1.Kafka节点之间如何复制备份的?Kafka的leader选举机制是什么?

复制功能是Kafka架构的核心,因为它可以在个别节点失效时仍能保证 Kafka的可用性和持久性。Kafka的每个Topic被分为若干个分区,每个分区有多个副本。那些副本被保存在 broker上, 每个 broker可以保存成百上千个属于不同主题和分区的副本。所有的事件都直接发送给Leader副本,或者直接从Leader副本读取事件。其他Follower副本只需要与Leader保持同步,并及时复制最新的事件。当Leader副本不可用时,其中一个Follower的同步副本将成为新Leader。分区Leader默认是同步副本,而对于跟随者副本来说,它需要满足以下条件才能被认为是同步的:

  • A node must be able to maintain its session with ZooKeeper (via ZooKeeper's heartbeat mechanism)
    一个节点必须能维持与zookeeper的会话(通过zookeeper的心跳机制)——它在过去的6秒(可配)内向Zookeeper 发送过心跳。
  • If it is a slave it must replicate the writes happening on the leader and not fall "too far" behind
    如果它是一个slave,它必须复制leader并且不能落后"太多"——在过去的10s 内(可配)从Leader那里获取过消息。

如果Follower断开ZK连接,或者获取消息滞后了10s 以上,那么它就被认为是不同步的。一个不同步的副本通过与Zookeeper重新建立连接,或从Leader那里获取最新消息,可以重新变成同步的。

2.Kafka消息是否会丢失?为什么?消息保证有几种方式?`

消息是否丢失的反问题就是消息的可靠性保证,都需要从三个角度去考虑。
消费端
消费端设置了自动确认,在某些极端情况下,消息刚到消费端返回给Broker确认,但是消费端突然挂了,该消息还没来得及处理自然就丢了。解决方案就是关闭自动ACK,等消费端明确处理了消息之后再进行手动确认。

Broker
Kafka的Broker在宕机的时候会进行分区的Leader选举,此时如果其他的follower副本还没有同步到最新的数据,而leader已经挂了,必然就会丢失一部分消息。需要设置:

  • 设置replication.factor>1,要求每个partition必须有至少2个副本。
  • 设置min.insync.replicas>1,要求一个leader至少一个follower副本保持同步。

生产端
生产端如果设置ack=0为无确认模式,消息发出去很容易丢失,为保证消息的可靠性可以有配置参数加以确认:
设置acks=-1则需要所有的副本确认,才能返回ack。
设置retries=N(一个较大的重试次数N),如果没有返回ack则重试N次直至broker收到消息。

3.Kafka最合理的配置是什么?
  • 客户端的配置:acks确认配置选择, batch.size批处理大小,compression.type压缩类型,max.partition.fetch.bytes,同步or异步send模式。Consumer配置Producer配置
  • Broker配置:broker.id集群配置,Broker配置

五、Kafka权威学习网站:


1.《Kafka官方中文网站》
2.《Kafka中文教程》
3.《消息中间件(Kafka/RabbitMQ)收录集》《Kafka技术专栏》,CSDN博客专家朱小厮的干货总结
4.掘金小册.图解 Kafka 之实战指南
5.极客时间.Kafka核心技术与实战

推荐阅读更多精彩内容