从0开始学Kafka(下)

引言

文章相关代码已收录至我的github,欢迎star:lsylulu/myarticle
从之前的描述中,可以得知,Producer通过主动Push的方式将消息发布到Broker,Consumer通过Pull从Broker消费数据。还有这样设计的动机,本文重点以Consumer为切入口,了解一下其中的API与Rebalance算法。

文章导读

  • Producer使用简介
  • High Level Consumer、Consumer Group、Rebalance机制
  • High Level API的使用
  • Low Level Consumer
  • Consumer Offset的管理(Log Compaction,Log Deletion)
  • Kafka高性能实现原理

一、Producer使用简介

依赖:

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>0.10.1.0</version>
        </dependency>

属性:

        Properties props=new Properties();
        props.put("bootstrap.servers","kafka0:9042");
        props.put("acks","all");
        props.put("retries",3);
        props.put("batch.size",16384);
        props.put("linger.ms",1);
        props.put("buffer.memory",33554432);
        props.put("key.serializier", StringSerializer.class.getName());
        props.put("value.serializier", StringSerializer.class.getName());
        props.put("partition.class", HashPartitioner.class.getName());

Kafka支持自定义负载均衡。从属性可以看到我指定的Partitioner,也就是实现了一个Producer对Broker的负载均衡策略。

public class HashPartitioner implements Partitioner {

  public HashPartitioner(VerifiableProperties verifiableProperties) {}

  @Override
  public int partition(Object key, int numPartitions) {
    if ((key instanceof Integer)) {
      return Math.abs(Integer.parseInt(key.toString())) % numPartitions;
    }
    return Math.abs(key.hashCode() % numPartitions);
  }
}

新版的Producer支持send成功后的回调操作,这里用lambda对其方法进行实现。

        Producer<String, String> producer = new KafkaProducer<String, String>(props);
        for(int i=0;i<10;i++){
            ProducerRecord record=new ProducerRecord<String,String>("topic1",Integer.toString(i));
            producer.send(record,(metadata, exception)-> {
                if(metadata!=null){
                    //输出成功发送消息的元信息
                    System.out.println(metadata);
                }
                if(exception!=null){
                    exception.printStackTrace();
                }
            });
        }
        producer.close();

二、High Level Consumer、Consumer Group、Rebalance机制

根据Kafka提供的API不同,可以讲Consumer划分为:High Level Consumer和Low Level Consumer(也叫Simple Consumer)。虽然说0.9版本开始讲两种Consumer合二为一了,但在API上还是有assign和subscribe的区分的。下面先来看看High Level Consumer。

2.1 High Level API的应用场景

1.很多应用场景下,客户程序只是希望从Kafka顺序读取并处理数据,而不太关心具体的Offset。High Level API围绕着Consumer Group这个逻辑概念展开,它屏蔽了每个Topic的每个Partition的Offset管理细节。

2.同时也希望提供一些语义,例如同一条消息只被某一个Consumer消费(单播)或被所有Consumer消费(广播)。

因此,Kafka High Level API提供了一个从Kafka消费数据的高层抽象,从而屏蔽掉其中的细节,并提供丰富的语义。

High Level Consumer是基于ConsumerGroup来实现的,首先了解一下什么是Consumer Group。

2.2 Consumer Group

High Level Consumer将从某个Partition读取的最后一条消息的offset存于Zookeeper中(从0.8.2开始同时支持将Offset存于Zookeeper中和专用的Kafka Topic中)。

思考
为什么要支持存储于专用的Kafka Topic中?
因为Zookeeper中只有Leader才能处理写请求,过分依赖Zookeeper会让其成为kafka性能上的短板。

这个Offset基于客户程序提供给Kafka的名字来保存,这个名字被称为Consumer Group。Consumer Group是整个Kafka集群全局唯一的,而不是针对于某个Topic。也就是说,一组Consumer Group共享一个Offset,以此实现消息在同一个Consumer Group中只能被消费一次。每个High Level Consumer实例都属于一个Consumer Group,若不指定则属于默认的Group。

说的有些抽象,配合下图理解能更到位~
img

图中屏蔽了Follower,Kafaka是消息订阅系统,所有的值都是顺序存储,消息都是append only到Partition中的,一旦删除,其他的Consumer Group就无法消费。

到这里为止,可以简单归纳一下ConsumerGroup的一些性质:

  • 消息被消费后,并不会被删除,只是相应的offset加一
  • 对于每条消息,在同一个Consumer Group里只会被一个Consumer消费。
  • 不同Consumer Group可消费同一条消息。

扩展阅读:
Kafka consumer如何加入consumer group

2.3 Consumer的Rebalance算法

我们都知道,在一个ConsumerGroup中,一个Partition中的数据只能由一个Consumer消费。那么Kafka是如何规定Consumer应该消费哪条Partition的数据呢?合理的分配才能显得相对均匀。Rebalance算法就是为这个而生的。

Rebalance的时机

  • Consumer增加或减少;
  • Broker增加或减少。

基于以下控制策略来实现Rebalance的触发(在Zookeeper中):

  1. 在/consumers/[consumer-group]/下注册id。
  2. 设置对/consumers/[consumer-group] 的watcher。
  3. 设置对/brokers/ids的watcher。
  4. zk下设置watcher的路径节点更改,触发consumer rebalance。

Kafka的Rebalance算法是也是基于Zookeeper来实现的。大概过程是:

  1. 将目标Topic下的所有Partirtion排序,存于PTP_T
  2. 对某 Consumer Group下所有Consumer 排序,存CGC_G,第 i 个Consumer 记为CiC_i
  3. N=size(PTP_T)/size(CGC_G),向上取整。
  4. 解除CiC_i对原来分配的Partition的消费权(i从0开始)。
  5. 将第i*N到(i+1)*N-1个 partition 分配给CiC_i

潜在问题

羊群效应(Herd Effect):一个被watch的zk节点变化,导致大量的watcher通知需要被发送给客户端,导致在通知期间其他操作延迟。

脑裂(Split Brain):每个Consumer都是通过zk保存的元数据来判断group中其他各成员的状态,以及Broker的状态。由于Zookeeper只保证最终的一致性,所以不同的Consumer在同一时刻可能连接在不同的zk服务器上,看到的元数据就可能不一样,基于不一样的元数据,执行Rebalance就会产生不一致的结果。

后续版本Kafka对Rebalance的优化

(1)通过延迟进入Preparing Rebalance状态减少Reblance次数
一个Group通常包含很多Consumer,当系统启动时,Consumer陆续加入,采用延迟的方式,让先加入的Consumer进入Initial Rebalance的状态,等延迟时间过后,才从Initial Rebalance转换为Preparing Rebalance状态。从而降低Rebalance的频率。

(2)引入静态成员ID,Consumer重新加入时,保持旧的标识

运行过程中,Consumer超时或重启引起的Reblance无法避免,其中一个原因就是,Consumer重启后,身份标识会变。简单说就是Kafka不确认新加入的consumer是否是之前挂掉的那个。

在Kafka2.0中引入了静态成员ID,使得consumer重新加入时,可以保持旧的标识,这样Kafka就知道之前挂掉的Consumer又恢复了,从而不需要Rebalance。就算发生了Rebalance,也尽量让其他Consumer保持原有的Partition,提高重分配的性能。

扩展阅读:
Kafka Consumer Rebalance
WeCoding:Kafka对reblance的优化,你了解嘛

三、 High Level API的使用

High Level API的使用分为低版本和高版本,是有些区别的。

3.1 低版本High Level Consumer(0.8)

为了避免配置的冗余,先讲将环境和配置贴出来。

依赖:

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.11</artifactId>
            <version>0.8.2.2</version>
        </dependency>

注:先用0.8版本依赖,高版本的新API后面会提到。

配置:
我觉得写代码前还是很有必要会顾一下架构图的,由于Consumer是通过Zookeeper获取集群信息的,因此需要配置ZK的IP+Port。后续还会涉及到与Kafka交互所以会有对应参数的配置。

img

        String topic = "topic1";
        Properties props = new Properties();
        props.put("zookeeper.connect", "192.168.29.100:2181");
        props.put("zookeeper.session.timeout.ms","3600000");
        //调用High Level API必填
        props.put("group.id", "group1");
        props.put("client.id", "consumer1");
        props.put("consumer.id","consumer1");
        //开启自动提交(默认开启)
        props.put("auto.commit.enable", "true");
        //初始化从头开始读
        props.put("auto.offset.reset", "smallest");
        //自动提交offset偏移量间隔时间修改为6s(默认60s)
        props.put("auto.commit.interval.ms", "6000");

重点代码:
里面重点部分都会有详细解释,如果没有看懂,可以对照下面的解释看。

        ConsumerConfig consumerConfig = new ConsumerConfig(props);
        ConsumerConnector consumerConnector = Consumer.createJavaConsumerConnector(consumerConfig);

        Map<String, Integer> topicCountMap = new HashMap<String, Integer>();
        //key是topic,value是该Topic数据消费的线程数,表示该Topic使用多少个线程进行数据消费操作
        //一般地,分区数==线程数,这样通常能够达到最大的吞吐量。
        //超过N的配置只是浪费系统资源,因为多出的线程不会被分配到任何分区。
        //详见解释1
        topicCountMap.put(topic, 1);
        Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap =
                consumerConnector.createMessageStreams(topicCountMap);
        //拿到KafkaStream其实就相当于拿到对应Topic的数据了
        //解释2
        KafkaStream<byte[], byte[]> kafkaStream = consumerMap.get(topic).get(0);
        ConsumerIterator<byte[], byte[]> iterator = kafkaStream.iterator();
        while (iterator.hasNext()) {
            //迭代消息,输出元数据
            MessageAndMetadata<byte[], byte[]> messageAndMetadata = iterator.next();
            String message =
                    String.format("Consumer ID:%s, Topic:%s, GroupID:%s, PartitionID:%s, Offset:%s, Message Key:%s, Message Payload: %s",
                            consumerid,
                            messageAndMetadata.topic(), groupid, messageAndMetadata.partition(),
                            messageAndMetadata.offset(), new String(messageAndMetadata.key()),new String(messageAndMetadata.message()));
            System.out.println(message);
        }

解释1:
这里主要解释”消费者的线程数“是什么意思。
kafka底层会为每个topic生成对应的消费线程。从一个blockingQueue中取数据。同时,在后台为Kafka的每个Broker生成一个fetch线程拉取消息数据,放入blockingQueue,等待消费,而对于blockingQueue的消费的线程数就是上述的count。

扩展阅读:
【原创】如何确定Kafka的分区数、key和consumer线程数 - huxihx - 博客园

解释2:
这里解释”consumerMap.get(topic).get(0)“的意思。
这个集合所承载的就是之前定义的消费线程。指定取某一个消费线程,拿出流数据,然后可以遍历该数据,该方法会是阻塞的。

3.2 高版本High Level Consumer(0.10)

依赖:

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.11</artifactId>
            <version>0.10.1.0</version>
        </dependency>
        <dependency>

配置:

        Properties props = new Properties();
        props.put("bootstrap.servers","192.168.29.100:9092");
        props.put("group.id","group2");
        props.put("client.id","consumer2");
        props.put("enable.auto.commit","true");
        props.put("auto.commit.interval.ms", "1000");
        props.put("key.deserializier", StringSerializer.class.getName());
        props.put("value.deserializier", StringSerializer.class.getName());

重点代码:
这里做一个解释,subscribe方法在0.8版本就有,不过并不支持回调函数,也就是ConsumerRebalanceListener。

        KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);

        consumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener() {
            @Override
            //parition原先被当前consumer消费,经过rebalance后不再被当前consumer消费了,就会调用
            public void onPartitionsRevoked(Collection<TopicPartition> collection) {
                collection.forEach(topicPartition->{
                    System.out.printf("revoked partition for client %s : %s-%s %n",clientid,topicPartition.topic(),topicPartition.partition());
                });
            }
           
            @Override
            //parition原先不被当前consumer消费,经过rebalance后将分配给当前consumer消费的Partition时调用
            public void onPartitionsAssigned(Collection<TopicPartition> collection) {
                collection.forEach(topicPartition->{
                    System.out.printf("assign partition for client %s : %s-%s %n",clientid,topicPartition.topic(),topicPartition.partition());
                });
            }
        });
        while(true){
            //从阻塞队列中取消息,最高延迟为100ms
            ConsumerRecords<String,String> records=consumer.poll(100);
            //停止对此Topic的partition0消费,同理可用resume方法让该partition能够被消费
            consumer.pause(Arrays.asList(new TopicPartition(topic,0)));
            records.forEach(record-> System.out.printf("client: %s,topic: %s,partition: %d,AotoCommitDemo: %d,key: %s",record.partition(),record.offset(),record.key(),record.value()));
        }

这个代码明显比之前哪种方式简洁,High Level Consumer自动Rebalance,所以不需要指定Partition

四、Low Level Consumer

使用Low Level Consumer (Simple Consumer)的主要原因是,用户希望比Consumer Group更好的控制数据的消费,“粒度更细”。

4.1 Low Level API的应用场景

1.同一条消息读多次,方便Replay。
2.只消费某个Topic的部分Partition。
3.管理事务,从而确保每条消息被处理一次(Exactly once)。当读取某个消息的Consumer失败了,Offset并没有加1,下次可以接着读,保证消息一定被处理一次。

与High Level Consumer相对,Low Level Consumer要求用户做大量的额外工作。其中包括:

  • 在应用程序中跟踪处理Offset,并决定下一条消费哪条消息。不会同步到Zookeeper。但是为了kafka manager方便监控,一般也会手动的同步到Zookeeper上。
  • 获知每个Partition的Leader。
  • 处理Leader的变化。
  • 处理多Consumer的协作。

4.2 Low Level API的使用

4.2.1 低版本Low Level Consumer(0.8)

依赖:

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.11</artifactId>
            <version>0.8.2.2</version>
        </dependency>

重点代码:

        final String topic="topic1";
        String clientID="consumer1";
        //设定leader broker的IP,port,timeout,bufferSize
        SimpleConsumer simpleConsumer=new SimpleConsumer("192.168.29.100",9092,100000,64*100000,clientID);
        //这里设定了topic,partition,offset和fetchSize
        FetchRequest req=new FetchRequestBuilder().clientId(clientID)
                .addFetch(topic,0,0L,100000).addFetch(topic,1,0L,5000).addFetch(topic,2,0L,100000).build();
        //通过封装的FetchRequest获取响应结果
        FetchResponse fetchResponse=simpleConsumer.fetch(req);
        ByteBufferMessageSet messageSet=fetchResponse.messageSet(topic,1);
        //遍历消息
        for(MessageAndOffset messageAndOffset:messageSet){
            ByteBuffer payload=messageAndOffset.message().payload();
            long offset=messageAndOffset.offset();
            byte[] bytes=new byte[payload.limit()];
            payload.get(bytes);
            System.out.println("AotoCommitDemo:"+offset+", payload:"+new String(bytes,"UTF-8"));
        }

扩展阅读:
0.8.0 SimpleConsumer Example

4.2.2 高版本Low Level Consumer(0.10)

依赖:

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_2.11</artifactId>
            <version>0.10.1.0</version>
        </dependency>
        <dependency>

配置:

        Properties props = new Properties();
        props.put("bootstrap.servers","192.168.29.100:9092");
        props.put("group.id","group2");
        props.put("client.id","consumer2");
        props.put("enable.auto.commit","true");
        props.put("auto.commit.interval.ms", "1000");
        props.put("key.deserializier", StringSerializer.class.getName());
        props.put("value.deserializier", StringSerializer.class.getName());
        props.put("auto.AotoCommitDemo.reset", "earliest");

重点代码:
以assign的方式,Low Level Consumer,需要指定目标Partiton。

        KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);

        consumer.assign(Arrays.asList(new TopicPartition(topic,0),new TopicPartition(topic,1)));
        while(true){
            ConsumerRecords<String,String> records=consumer.poll(100);
            records.forEach(record-> System.out.printf("client: %s,topic: %s,partition: %d,AotoCommitDemo: %d,key: %s",record.partition(),record.offset(),record.key(),record.value()));
        }

五、Consumer Offset的管理

Offset是message的唯一标识符。在前两章的配置中,我们见过“props.put(“enable.auto.commit”,“true”);”,都是自动提交Offset。这种方式很简单,但多数情况下,不会使用。因为不论从Kafka集群中拉取的数据是否被处理成功,Offset都会被更新,如果执行错误可能会出现数据丢失的情况。所以多数情况下我们会选择手动提交方式。

如果是Low Level Consumer,手工管理offset时需要:

  • 每次从特定Partition的特定offset开始fetch特定大小的消息;
  • 完全由Consumer应用程序决定下一次fetch的起始offset

5.1 Commit的手动提交方式

下面就展示了手动同步和异步提交的方式,以High Level Consumer为例:

属性:

        Properties props = new Properties();
        props.put("bootstrap.servers","192.168.29.100:9092");
        props.put("group.id","group1");
        props.put("client.id","consumer1");
        props.put("enable.auto.commit","false");
        props.put("key.deserializier", StringSerializer.class.getName());
        props.put("value.deserializier", StringSerializer.class.getName());
        props.put("max.poll.interval.ms", "300000");
        props.put("max.poll.records", "500");
        props.put("auto.offset.reset", "earliest");
        //将offset存储到Kafka的Topic
        props.put("offset.storage","kafka");
        //自动存储到对应的介质中
        props.put("dual.commit.enabled","true");

重点代码:

        KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
        consumer.subscribe(Arrays.asList(topic));
        AtomicLong atomicLong=new AtomicLong();
        while(true){
            ConsumerRecords<String,String> records=consumer.poll(100);
            records.forEach(record->{
                System.out.printf("client: %s,topic: %s,partition: %d,AotoCommitDemo: %d,key: %s",record.partition(),record.offset(),record.key(),record.value());
//                //1.同步commit
//                if(atomicLong.get()%10==0){
//                    //组的commit上一次消费的offset
//                    consumer.commitSync();
//                }
                //2.异步commit
                if(atomicLong.get()%10==0){
                    //提供了异步的回调操作,如果commit成功,则执行该方法
                    //解释1
                    consumer.commitAsync((Map<TopicPartition, OffsetAndMetadata> offsets,Exception exception)->{
                        offsets.forEach((TopicPartition partition,OffsetAndMetadata offset)->{
                            System.out.printf("commit %s-%d-%d %n",partition.topic(),partition.partition(),offset.offset());
                            offset.offset();
                        });
                        if(null!=exception){
                            exception.printStackTrace();
                        }
                    });
                }
            });
        }

解释1:
采用异步提交可以通过匿名内部类的方式进行回调,实现了OffsetCommitCallback中的

void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception);

方法,这里用lambda表达式实现,因此如果commit成功,又由于是批量提交的,就遍历这些成功提交的Offset。

扩展阅读:
Kafka Client API 基本使用

5.2 Kafka对Offset的Log管理

Kafka会对每次消费的Offset,key,Value做记录。这些日志可能用于大数据的数据聚合或者容灾恢复,它的存储还是很有必要的。但是log的体积随着时间的推移会越来越大,如果不及时清理,很可能造成对内存的占用。可以同过Topic的cleanup.policy设置清理策略。默认为delete,也可设置为compaction,也可同时设置。

5.2.1 Log Compaction

主要思想是:系统关心的是它原本的最新状态而不是历史时刻中的每一个状态。下面看一下Log Compaction的过程:

img

图片来源于网络

Log Compaction对于有相同key的不同value值,只保留最后一个版本。如果应用只关心key对应的最新value值,可以开启Kafka的日志清理功能,Kafka会定期将相同key的消息进行合并,只保留最新的value值。

整体过程是:
前提
Kafka中的每个日志清理线程使用名为“SkimpyOffsetMap”的对象来构建key与offset的映射关系的哈希表。

(1)日志清理需要遍历两次日志文件,第一次遍历把每个key的哈希值和最后出现的offset都保存在SkimpyOffsetMap中,映射模型如下图所示。

(2)第二次遍历检查每个消息是否符合保留条件,如果符合就保留下来,否则就会被清理掉。假设一条消息的offset为O1,这条消息的key在SkimpyOffsetMap中所对应的offset为O2,如果O1>=O2,说明在O1在O2之后进行消费,即为满足保留条件。

扩展阅读:
https://blog.csdn.net/u013256816/article/details/80487758

5.2.2 Log Deletion

之前的Log Compaction是以key为维度来保留的,而Log Deletion是按照条件直接删除不符合的所有日志。
调度策略由broker端参数log.retention.check.interval.ms,默认为5分钟。也就是5分钟扫描一次看看有没有符合条件的log就删除。这个条件包括,时间,日志大小和日志的起始偏移量。

基于时间

日志删除任务会扫描超过阈值时间(retentionMs)的过期日志,阈值时间可通过可以通过broker端参数log.retention.hours、log.retention.minutes以及log.retention.ms来配置。系统会转换成时间戳来计算,如果删除的日志分段总数是所有的日志分段的数量时,必须要保证有一个活跃的日志分段activeSegment。会先切分出一个新的日志分段作为activeSegment,然后再执行删除操作。

基于日志大小

日志删除任务会扫描超过设定的retentionSize的日志,加入可删除的日志分段的文件集合deletableSegments。retentionSize可以通过broker端参数log.retention.bytes来配置,表示日志文件的总大小,默认值为-1,表示无穷大。删除任务会先计算日志的总大小和当前日志大小的差值。然后从日志文件中的第一个日志分段开始进行查找可删除的日志分段的文件集合。

基于日志起始偏移量

简单来说,就是判断日志分段的下一个日志分段的起始偏移量baseOffset是否小于等于logStartOffset,若是则删除偏移量之前的日志分段。

扩展阅读:
https://blog.csdn.net/u013256816/article/details/80418297

六、Kafka高性能实现原理

6.1 高效使用磁盘

1.顺序写磁盘
顺序写磁盘性能高于随机写内存。

2.Append Only
数据不更新无记录级的数据删除(只会删除整个segment)。

3.充分利用Page Cache

  • I/O Scheduler将连续的小块写组装成大块的物理写从而提高性能。
  • I/O Scheduler会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间。
  • 充分利用所有空闲内存(非JVM内存)。
    应用层cache也会有对应的page cache与之对应,直接使用page cache可增大可用cache。如使用heap内的cache,会增加GC负担。
  • 读操作可直接在page cache内进行。如果进程重启,JVM内的cache会失效,但page cache仍然可用。
  • 可通过如下参数强制flush,但并不建议这么做
    log.flush.interval.messages=10000
    log.flush.interval.ms=1000

4.支持多Directory(可使用多Drive)
充分利用多磁盘的优势。

6.2 利用Linux零拷贝

涉及方法:

  • File.read(fileDesc,buf,len)
  • Socket.send(socket,buf,len)

传统模式

传统模式下数据从文件传输到网络需要4次数据拷贝4次上下文切换2次系统调用

img

4次数据拷贝:DMA将数据从磁盘中拷贝到内核的read缓冲区;CPU将读缓存拷贝到应用缓存中;CPU将数据拷贝到内核的Socket缓冲区;DMA将缓冲区中的数据拷贝到网卡的缓冲区中(NIO buffer)。

4次上下文切换:刚开始由用户态切换到内核态DMA读取数据到缓冲区;由内核态切换到用户态进行CPU拷贝到应用程序;由用户态到内核态拷贝到内核的socket缓冲区;最终拷贝结束由内核态切换回用户态。

2次系统调用:系统调用read;系统调用socket。

零拷贝

通过NIO的transferTo/transferFrom调用操作系统的sendfile实现零拷贝。

img

总共发生2次内核数据拷贝2次上下文切换1次系统调用,消除了CPU数据拷贝。

6.3 批处理和压缩

Producer和Consumer均支持批量处理数据,消息按条数积累或者按时间的积累从而减少了网络传输的开销(比如异步Commit就是批处理)。
Producer可将数据压缩后发送给Broker,从而减少网络传输代价。目前支持Snappy, Gzip和LZ4压缩。

6.4 Partition实现高性能

通过Partition实现了并行处理和水平扩展。

1.Partition是Kafka(包括Kafka Stream)并行处理的最小单位。high level api就是一个partition只能被一个consumer消费,partition越多,并行度就越高。

2.不同Partition可处于不同的Broker(节点),充分利用多机资源。

3.同一Broker(节点)上的不同Partition可置于不同的Directory,如果节点上
有多个Disk Drive,可将不同的Drive对应不同的Directory,从而使Kafka充分利用多Disk Drive的磁盘优势。

6.5 ISR实现一致性,持久性之间的动态平衡

1.ISR实现了可用性和一致性的动态平衡

2.ISR可容忍更多的节点失败

  • Majority Quorum如果要容忍f个节点失败,则至少需要2f+1个节点。
  • ISR如果要容忍f个节点失败,至少需要f+1个节点。

3.如何处理Replica Crash

  • Leader crash后,ISR中的任何Replica皆可竞选成为Leader。
  • 如果所有Replica都Crash,可选择让第一个Recover的Replica或者第一个在ISR中的Replica成为Leader。
  • 配置:unclean.leader.election.enable=true

总结

总体上来说,内容还是挺丰富的,如果有错误,欢迎指出。
本文适合加入收藏点赞系列~

参考文章:
Kafka Consumer Rebalance
https://blog.csdn.net/silviakafka/article/details/77162075
Kafka如何实现每秒上百万的高并发写入
WeCoding:Kafka对reblance的优化,你了解嘛
Kafka Client API 基本使用
https://blog.csdn.net/u013256816/article/details/80487758
https://blog.csdn.net/u013256816/article/details/80418297
白天不懂夜的黑:Kafka史上最详细原理总结

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章