消息中间件-Kafka 安装集群、消息分发策略、消息的消费原理、java api 操作代码

文章目录

  • Kafka
    • 总览
    • 应用场景
    • Kafka 本身架构
    • Kafka 安装部署
      • 安装
      • 启动、停止 kafka
      • 安装集群环境
    • Kafka Java API 的使用
    • kafka 原理分析
      • topic 和 partition
        • topic
        • partition
        • topic 和 partition 存储
    • 消息分发
      • kafka 消息分发策略
        • 消息默认的分发机制
        • 消费端消费指定的分区
    • 消息的消费原理
      • 分区分配策略
        • Range strategy 范围分区
        • RoundRobin strategy 轮询分区
      • 触发分区分配策略的条件
        • 谁来执行Rebalance 以及管理 consumer 的group ?
          • 如果确定 coordinator
          • JoinGroup 的过程
          • Synchronizing GroupGroup StateState 阶段
      • 保存消费端的消费位置 Offset
    • 消息的存储
      • 零拷贝
      • 消息的文件存储机制
        • LogSegment
        • segment 中 index 和 log 的对应关系
        • 在 partition 中如何通过 offset 查找 message
        • 日志清除策略以及压缩策略
          • 日志清除策略
          • 日志压缩策略
    • partition 的高可用副本机制
      • 副本分配算法 (没有证明出来)?????不过这个算法到感觉是获得broker上的 有哪些partition

Kafka

Kafka 是一款分布式消息发布和订阅系统,具有高性能、高吞吐量的特点而被广泛应用与大数据传输场景。

总览

kafka 是一个分布式消息发布和订阅系统,具有高性能和高吞吐量。

通过kafka可以实现日志收集、行为跟踪、消息的通信。

当produce 生产者发送数据到 broker上的topic 时,会通过 zk的 watcher事件通知给监听的consumer ,此时consumer 可以主动去pull 拉去broker 的消息,这块跟其他mq是不同的。

kafka支持分区(partition),在集群环境下我们通过将消息进行分区来减少此磁盘的压力。

消息分发策略 :默认的消息分发策略是基于key 的hash一致性,当producer 生产消息到broker上时,会根据分发策略存储到不同的分区上。

分区分配策略:默认是范围分区是基于partition进行排序,当多个consumer 同时去消费topic时,会根据分区分配策略去选择对应的partition 进行消费,来减少服务器上的压力。

当真正消费消息,如果增加或减少consumer 或者partition 发生了变化,此时会重新进行分区分配策略,即rebanlance 。在进行Rebalance时,kafka会通过coordinator 来进行管理consumer 下group,当第一个consumer进行启动时,会去跟kafka server 确认coordinator,kafka server会选择broker节点最小的作为coordinator,之后所有的consumer会跟coordinator 进行通信。

应用场景

由于 kafka 具有较好的吞吐量、内置分区、冗余及容错性的优点(可以每秒处理几十万消息),让 kafka 成为了一个很好的大规模消息处理应用的解决方案。在企业级应用上,主要会应用于以下几个方面

  • 行为跟踪:kafka 可以用于跟踪用户浏览页面、搜索及其他行为。通过发布-订阅模式实时记录到对应的topic 中,通过后端大数据平台接入处理分析,并做更进一步的实时处理和监控
  • 日志收集:日志聚合表示从服务器上收集日志文件,然后放到一个集中的平台(文件服务器)进行处理。在实际应用开发中,我们应用程序的log 都会输出到本地的磁盘上,排查问题的话通过linux 命令来搞定,如果应用程序组成了负载均衡集群,并且集群的机器有几十台以上,那么想通过日志快速定位到问题,就是很麻烦的事情了。所以一般都会做一个日志统一收集平台管理log 日志用来快速查询重要应用的问题。所以很多公司的套路都是把应用日志几种到kafka 上,然后分别导入到es 和hdfs 上,用来做实时检索分析和离线统计数据备份等。而另一方面,kafka 本身又提供了很好的api 来集成日志并且做日志收集消息中间件-Kafka 安装集群、消息分发策略、消息的消费原理、java api 操作代码_第1张图片

Kafka 本身架构

kafka 集群包含若干个 Producer(可以是应用节点产生的消息,也可以是通过Flume 收集日志产生的事件),若干个Broker(kafka 支持水平扩展)、若干个ConsumerGroup,以及一个zookeeper 集群。

kafka 通过zookeeper 管理集群配置及服务协同。Producer 使用push 模式将消息发布到broker,consumer 通过监听使用pull 模式从broker 订阅并消费消息。多个broker 协同工作,producer 和consumer 部署在各个业务逻辑中。三者通过zookeeper 管理协调请求和转发。这样就组成了一个高性能的分布式消息发布和订阅系统。

有一个细节是和其他 mq 中间件不同的点,producer 发送消息到broker的过程是push,而consumer 从broker 消费消息的过程是pull,主动去拉数据。而不是broker 把数据主动发送给consumer

消息中间件-Kafka 安装集群、消息分发策略、消息的消费原理、java api 操作代码_第2张图片

Kafka 安装部署

安装

wget https://archive.apache.org/dist/kafka/1.1.0/kafka_2.11-1.1.0.tgz
tar -zxvf kafka_2.11-1.1.0.tgz 

启动、停止 kafka

启动 kafka 时需要安装并启动 zookeeper ,(如果没有搭建 zookeeper 环境,可以直接运行 kafka 内嵌的 zookeeper )

启动 zookeeper 和 kafka

cd /guaoran/zookeeper/zookeeper-3.4.10/bin/
sh zkServer.sh start
cd /guaoran/kafka/kafka_2.11-1.1.0/bin
sh kafka-server-start.sh -daemon ./../config/server.properties

停止 kafka

sh kafka-server-stop.sh 

kafka 基本操作

安装集群环境

修改server.properties 文件

# 修改对应的服务器 节点 三台 分别是 0,1,2 类似zookeeper 的myid
broker.id=0
# 监听修改成本机ip ,ifconfig的ip地址
listeners=PLAINTEXT://192.168.45.135:9092
## 对外的ip地址
advertised.listeners=PLAINTEXT://192.168.45.135:9092
# advertised.host.name=192.168.45.135
# advertised.port=9092
# 修改 kafka 日志 目录,可以采用默认,不过会丢失
log.dirs=/guaoran/kafka/logs
# 配置zookeeper 的集群地址
zookeeper.connect=192.168.45.131:2181,192.168.45.134:2181,192.168.45.135:2181

Kafka Java API 的使用

生产者消息发送

/**
 * @author : guaoran
 * @Description : 
* 生产者消息发送 * @date :2018/11/9 16:00 */
public class KafkaProducerDemo extends Thread { private final static String CONNECT_URL = "192.168.45.131:9092,192.168.45.134:9092,192.168.45.135:9092"; private final KafkaProducer<Integer,String> producer; private final boolean isAysnc; private final String topic; public KafkaProducerDemo(String topic,boolean isAysnc){ this.isAysnc = isAysnc; this.topic = topic; Properties properties = new Properties(); //连接地址 properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,CONNECT_URL); properties.put(ProducerConfig.CLIENT_ID_CONFIG,"KafkaProducerDemo"); // producer 发送消息到 broker 上以后的确认值 // 0 :表示 producer 不需要等待 broker 的消息确认,如果server 宕机 ,数据会丢失 // 1 :表示 producer 只需要获得 kafka 集群中的 leader 节点确认即可 // all(-1) :表示 producer 需要ISR中所有的Replica接受确认,速度较慢,安全性最高,如果只有一个Replica时,并不能一定能避免数据丢失。 properties.put(ProducerConfig.ACKS_CONFIG,"-1"); properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.IntegerSerializer"); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); /* batch.size 生产者发送多个消息到broker上的同一个分区时,为了减少网络请求带来的性能开销,通过批量的方式来提交消息, 可以通过这个参数来控制批量提交的字节数大小,默认是16kb,意味着当一批消息大小达到指定的 batch.size 时会统一发送。 linger.ms producer 默认会把两次发送时间间隔内收集到的所有 request 进行一次聚合,然后再发送,以提高吞吐量。 而linger.ms 就是为每次发送到broker 的请求增加一些 delay,以此来聚合更多的message请求。 batch.size 和 linger.ms 这两个参数是 kafka 的性能优化的关键参数,如果两个都配置,只要满足其中一个要求,就会发送消息到broker max.request.size 设置请求的数据的最大字节数,为了防止发生较大的数据包影响到吞吐量,默认是1MB */ producer = new KafkaProducer<Integer, String>(properties); } @Override public void run() { int num = 0; while(num<50){ String message = "message_"+num; System.out.println("begin...send..."+message); if(isAysnc){//异步发送 producer.send(new ProducerRecord<Integer, String>(topic,message),new Callback(){ @Override public void onCompletion(RecordMetadata recordMetadata, Exception e) { if(recordMetadata != null){ System.out.println("async-offset:"+recordMetadata.offset()+ "->partition:"+recordMetadata.partition()); } } }); }else{//同步发送 try { RecordMetadata recordMetadata = producer.send(new ProducerRecord<Integer, String>(topic,message)).get(); System.out.println("sync-offset:"+recordMetadata.offset()+ "->partition:"+recordMetadata.partition()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } num++; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { new KafkaProducerDemo("test",false).start(); } }

消费者消费消息

/**
 * @author : guaoran
 * @Description : 
* 消费端消息接受 * @date :2018/11/9 16:00 */
public class KafkaConsumerDemo extends Thread { private final static String CONNECT_URL = "192.168.45.131:9092,192.168.45.134:9092,192.168.45.135:9092"; private final KafkaConsumer<Integer,String> consumer; public KafkaConsumerDemo(String topic){ Properties properties = new Properties(); properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,CONNECT_URL); //分组id properties.put(ConsumerConfig.GROUP_ID_CONFIG,"KafkaConsumerDemo1"); // enable.auto.commit 消费者消费消息以后自动提交,只有当消息提交以后,该消息才不会被再次接收到 // 还可以配合 auto.commit.interval.ms 控制自动提交的频率。 properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true"); properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.IntegerDeserializer"); properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); //auto.offset.reset //这个参数是针对新的 groupid中的消费者而言的,当有新的groupid的消费者来消费指定的topic时,对于该参数的配置,会有不同的语义 // 当 kafka 没有初始偏移量或服务器上当前偏移量不再存在时 // latest:自动将偏移重置为最新偏移 // earliest:自动将偏移量重置为最早的偏移量 // none:如果没有找到消费者组的先前偏移,则向用户抛出异常 properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest"); /* max.poll.records 此设置限制每次调用poll 返回的消息数,这样可以更容易的预测每次 poll 间隔要处理的最大值,通过调整此值,可以减少poll 间隔 */ consumer = new KafkaConsumer<Integer, String>(properties); consumer.subscribe(Collections.singletonList(topic)); } @Override public void run() { while(true){ ConsumerRecords<Integer,String> consumerRecords = consumer.poll(1000); for (ConsumerRecord r :consumerRecords) { System.out.println("consumer...receive.."+r.value()); } } } public static void main(String[] args) { new KafkaConsumerDemo("test").start(); } }

kafka 原理分析

topic 和 partition

topic

在kafka 中,topic是一个存储消息的逻辑概念,可以认为是一个消息的集合。每条消息发送到 kafka 集群的消息都有一个类别。每个topic可以有多个生产者向他发送消息,也可以有多个消费者去消费消息

partition

每个topic 可以划分多个分区(每个topic至少有一个分区),同一个topic下的不同分区包含的消息是不同的。每个消息在被添加到分区时,都会被分配一个offset,它是消息再此分区中的唯一编号,kafka通过offset保证消息在分区内的顺序,offset 的顺序不跨分区,即kafka只保证在同一个分区内的消息是有序的。

topic 和 partition 存储

partition 是以文件的形式存储在文件系统中,比如创建一个名为 demo 的topic ,其中有三个partition ,那么在kafka 的数据目录中,就有3三目录 ,demo-0~2,命名规则:topicname-partitionid

kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 3 --topic demo

消息分发

kafka 消息分发策略

消息是kafka 中最基本的数据单元,在kafka中,一条消息有 key 、value 两部分组成,在发送一条消息时,我们可以指定这个 key,那么 producer 会根据 key 和partition 机制来判断当前这条消息应该发送并存储到哪个partition中;我们可以根据需要进行扩展producer 的partition 机制

/**
 * @author : guaoran
 * @Description : 
* 自定义消息分区算法 * @date :2019/1/15 13:35 */
public class TopicPartitionDemo implements Partitioner { private final Random random = new Random(); @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { List<PartitionInfo> partitionInfoList = cluster.partitionsForTopic(topic); //指定发送的分区值 int partitionNum = 0; if(key==null){ // 随机分区 partitionNum = random.nextInt(partitionInfoList.size()); }else{ Math.abs((key.hashCode())%partitionInfoList.size()); } System.err.println("topic="+topic+",key="+key+",value="+value+",partitionNum="+partitionNum); return partitionNum; } }

消息默认的分发机制

默认情况下,kafka 采用的是hash 取模的分区算法。如果key 为null,则会随机分配一个分区。这个随机是在参数”metadata.max.age.ms” 的时间范围内随机选择一个。对于这个时间段内,如果key为null,则只会发送到唯一的分区。这个值默认情况下是10分钟更新一次。

消费端消费指定的分区

//消费指定分区的时候,不需要再订阅 
//consumer.subscribe(Collections.singletonList(topic));
// todo 只消费分区 0 的消息
TopicPartition partitionDemo = new TopicPartition(topic,0);
consumer.assign(Arrays.asList(partitionDemo));

消息的消费原理

在实际生产过程中,每个topic都会有多个partition,多个partition的好处在于,一方面能够对broker上的数据进行分片有效减少消息的容量从而提升io性能。另一方面,为了提高消费端的消费能力,一般会通过多个consumer 去消费同一个topic,也就是消费端的负载均衡机制。

在group.id相同的consumer进行消费同一个topic时,一个consumer消费过得数据在另一consumer中不会被消费到,那么同一个consumer group 里面的consumer 去消费数据的时候,会根据分片进行分配消费分区的数据。如果有三个partition ,同时启动三个group.id 相同的consumer去同时消费同一个topic,最终的结果是三个consumer 会分别消费一个partition 的数据。

分区分配策略

在kafka中存在两种分区分配策略,一种是Range(默认),一种是RoundRobin(轮询)。通过partition.assignment.strategy 参数来设置。

Range strategy 范围分区

Range 策略是对每个主题而言的,首先对同一个主题里面的分区按照序号进行排序。并对消费者按照字母顺序进行排序。假设有10个分区,3个消费者,排完序的分区将会是0-9;消费者线程排完序是C0-0,C1-1,C2-2 。然后将partitions的个数除于消费者线程的总数来决定每个消费者线程将会消费几个分区。如果除不尽,则前面的消费者会多消费一个分区。所以最终结果是:C0消费 0-3分区,C1消费4-6分区,C2消费7-9分区。

如果同时消费两个主题的话,分区数相同,消费者相同,此时,C0消费者比其他消费者线程多消费2个分区,这就是Range Strategy 的一个弊端。最好是分区数是消费者的整数倍。

RoundRobin strategy 轮询分区

轮询分区策略是把所有的partition 和所有consumer 都列出来,然后按照hashcode进行排序。最后通过轮询算法分配partition给消费者。如果所有consumer实例的订阅都是相同的,那么partition会均匀分布。

使用轮询分区策略必须满足两个条件

  1. 每个主题的消费者实例具有相同数量的流
  2. 每个消费者订阅的主题必须是相同的。

触发分区分配策略的条件

当出现以下几种情况时,kafka会进行一次分区分配操作,即 kafka consumer 的rebalance

  1. 同一个consumer group 内新增了消费者
  2. 消费者离开当前的consumer group ,如:主动停机或宕机
  3. topic 分区数量发生了变化

谁来执行Rebalance 以及管理 consumer 的group ?

kafka 提供了一个角色:coordinator 来执行对于consumer group的管理 ,当consumer group 的第一个 consumer 启动的时候,它会去跟 kafka server 确定谁是他们组的 coordinator 。之后该group 内所有成员都会和该coordinator 进行协调通信。

如果确定 coordinator

消费者向kafka 集群中的任意一个broker 发送一个 GroupCoordinatorRequest请求,服务端会返回一个负载最小的broker 节点的id,并将 broker 设置为 coordinator

JoinGroup 的过程

在rebalance 之前,需要保证 coordinator是已经确定好了的,整个Rebalance 的过程分为两个步骤,join 和 sync 。

join:表示加入到consumer group 中,在这一步中,所有成员都会想 coordinator 发送joinGroup的请求。一旦所有成员都发送了joinGroup请求,那么 coordinator会选择一个consumer 担任leader 角色,并把组成员信息和订阅信息发送消费者。

消息中间件-Kafka 安装集群、消息分发策略、消息的消费原理、java api 操作代码_第3张图片

protocol_metadata:序列化后的消费者的订阅信息

leader_id:消费组中的消费者,coordinator会选择一个作为leader,对应是就是member_id

member_metadata :对应消费者的订阅信息

members:consumer group 中全部的消费者的订阅信息

generation_id:年代信息,类似 zk的epoch,对于每轮Rebalance 都会递增该值

Synchronizing GroupGroup StateState 阶段

完成分区分配后,就进入该阶段,主要逻辑是向GroupCoordinator 发送SyncGroupRequest请求,并且处理SyncGroupResponse响应,简单来说,就是leader将消费者对应的partition分配方案同步给consumer group中的所有consumer。

consumer group 的分区分配方案是在客户端执行的。

保存消费端的消费位置 Offset

offset 即 每个消息针对每个consumer group 的偏移量,记录该consumer group 消费到了具体的位置。

在kafka 中,体用了一个__consumer_offsets-* 的一个topic ,把offset 信息写入到这个topic中。默认有50个分区。

查看groupid的offset存储在哪个分区中,计算公式为

  • (“分组id”.hashCode())%__consumer_offsets的分区总数)
System.out.println(Math.abs(("KafkaConsumerDemo1".hashCode())%50));
  • 查看当前consumer group 的offset 信息
bin/kafka-simple-consumer-shell.sh --topic __consumer_offsets  --partition 4 --broker-list 192.168.45.135:9092,192.168.45.131:9092,192.168.45.134:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter"
[groupid,topic,partition]::[OffsetMetadata[offset,..]....]
[KafkaConsumerDemo1,demo,0]::[OffsetMetadata[165,NO_METADATA],CommitTime 1547543212536,ExpirationTime 1547629612536]

消息的存储

为了规避随机读写带来的时间消耗,kafka采用顺序写的方式存储数据。即使是这样,但是I/O操作仍然会造成磁盘的性能瓶颈,所以kafka还有一个性能策略。

零拷贝

一般应用程序有一个buffer空间在用户空间中,来自于网络或者磁盘,无论来自网络或者磁盘,都需要通过内核,也就是说内核中也要有buffer。

1)磁盘到内核 --> 2)内核到应用程序buffer 写数据时 --> 3)应用程序buffer写到内核buffer --> 4)内核buffer写到磁盘

这个过程多了两次拷贝,kafka本身因为不处理数据,所以没有必要把数据放入应用程序的buffer中。所以搞了个基于内核的数据存储和传输,使用sendfile机制,直接基于内核kernel处理。

  • push和pull的模式
    无论有多少producer,都往kafka进行push数据,kafka可以不关心producer的具体位置。consumer是从kafka pull数据,无论有多少消费数据,对kafka基本没有压力。
  • 采用zookeeper来管理brokers和consumers
    zookeeper主要存放元数据信息,这是一种积木式创新的体现。
  • 在consumer端实现消息的一致性
    kafka本身可以保存consumer已经消费过数据的offset,所以如果consumer出错的话,重新启动consumer,就可以从最近的数据开始。

消息的文件存储机制

一个topic 可以有多个partition 在物理磁盘上进行保存,进入到logs目录中,可以找到对应partition下的日志内容

cd /guaoran/kafka/logs/guaoran-0/
ls
00000000000000000000.index  00000000000000000000.log  00000000000000000000.timeindex  leader-epoch-checkpoint

kafka 是通过分段的方式将log分为多个LogSegment,LogSegment是一个逻辑上的概念,一个LogSegment对应磁盘上的一个日志文件和一个索引文件,其中(.log)日志文件是用来记录消息的,(.index)索引文件时用来保存消息的索引。

LogSegment

当kafka producer 不断发送消息,必然会引起partition文件的五险扩张,这样对于消息文件的维护以及被消费的消息的清理都会带来非常大的挑战,所以kafka 以segment 为单位又把partition进行细分。每个partition相当于一个巨型文件被平均分配到多个大小相等的segment数据文件中(每个segment文件中的消息不一定相等),这种特性方便已经被消费的消息的清理,提高磁盘的利用率。

server.properties 中有以下几个配置

# 分段文件的大小
log.segment.bytes=107370
## 消息清理
# 日志消息默认存储7天
log.retention.hours=168 
# 消息的大小,超过这个大小,会清理
log.retention.bytes=1073741824

为了看到明显的效果,将分段文件大小改小了,并进行发送多个消息到 guaoran 的topic中,再次查看

在这里插入图片描述

segment 文件由三部分组成,分别是.index , .log , .timeindex 后缀,

segment 文件命令规则:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一天消息的offset值进行递增。

采用以下命令对 .index 文件进行查看

/guaoran/kafka/kafka_2.11-1.1.0/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.index --print-data-log

结果如下:

offset: 53 position: 4124
offset: 106 position: 8264
...
offset: 1302 position: 103050
offset: 1354 position: 107210

采用以下命令对 .log文件进行查看

/guaoran/kafka/kafka_2.11-1.1.0/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.log --print-data-log

结果如下:

offset: 1301 position: 102970 CreateTime: 1547557716588 payload: message_1301
offset: 1302 position: 103050 CreateTime: 1547557716601 payload: message_1302
offset: 1303 position: 103130 CreateTime: 1547557716612 payload: message_1303
offset: 1304 position: 103210 CreateTime: 1547557716624 payload: message_1304
...
offset: 1353 position: 107130 CreateTime: 1547557717167 payload: message_1353
offset: 1354 position: 107210 CreateTime: 1547557717179 payload: message_1354
offset: 1355 position: 107290 CreateTime: 1547557717183 payload: message_1355

第一个log文件的最后一个offset为1355,所以下一个segment的文件命名为00000000000000001356.log

segment 中 index 和 log 的对应关系

如上面所看查看的index 和log 的文件内容,进行分析

为了提高查找消息的性能,为每一个日志文件添加2个索引,索引文件:offsetIndex 和 TimeIndex ,分别对应 .index 和 .timeindex

.index 文件中存储了索引 以及物理偏移量。.log 文件中存储了消息的内容。索引文件的元数据执行对应数据文件中message 的物理偏移地址。以【1302,103050】为例, log文件中,对应的是滴1302条记录,物理偏移量(position)为103050,position 是ByteBuffer 的指针位置。

在 partition 中如何通过 offset 查找 message

  1. 根据offset 的值,查找 segment 段中的 index 索引文件。由于索引文件命名是以上一个文件的最后一个offset进行命令的,所以,使用二分查找算法能够根据offset快速定位到指定的索引文件。
  2. 找到索引文件后,根据offset进行定位,找到索引文件中的复合范围的索引。(kafka 采用稀疏索引的方式来提高查找性能)
  3. 得到position以后,在到对应的log文件中,从position处开始查找offset对应的消息,将每条消息的offset与目标offset进行比较,知道找到消息

比如找 offset=1303的消息,那么会先找到000.index 文件,找到【1302,103050】 这个索引,在到log文件中,根据 103050 这个position 开始查找offset = 1303的消息,当确定对应的消息后进行返回。

日志清除策略以及压缩策略

日志清除策略

日志是分段存储的,一方面能够减少单个文件内容的大小,另一方面,方便kafka 进行日志清理。日志的清理策略有两个:

  1. 根据消息的保留时间,当消息在kafka中保存的时间超过了指定的时间,就会触发清理过程 log.retention.hours=168 默认7天
  2. 根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息。kafka会启动一个后台线程,定期检查是否存在可以删除的消息。log.retention.bytes=1073741824 默认1G

通过上面这两个参数来设置,当其中任意一个达到要求,都会执行删除。

日志压缩策略

kafka 还提供了日志压缩功能,通过这个功能可以有效的减少日志文件的大小,缓解磁盘紧张的情况,在很多实际场景中,消息的key和value的值之间的对应关系是不断变化的,就像数据库中的数据会不断被修改一样消费者只关心key对应的最新value值。因此,我们可以开启kafka的日志压缩功能,服务端会在后台启动Cleaner 线程池,定期将相同的key进行合并,只保留最新的value值。

默认情况下启动日志清理程序,要在特定主题上启用日志清理,您可以添加特定于日志的属性 log.cleanup.policy=compact ,日志清理程序可以配置为保留最小量的日志的未压缩“头”。通过设置压缩时间延迟来启用此功能。 log.cleaner.min.compaction.lag.ms .

日志压缩的原理消息中间件-Kafka 安装集群、消息分发策略、消息的消费原理、java api 操作代码_第4张图片

partition 的高可用副本机制

kafka 的每个topic 都可以分为多个partition ,并且多个partition 会均匀分布在集群的各个节点下。虽然这种方式能够有效的对数据进行分片,但是杜宇每个partition 来说,都是单点的,当其中一个partition 不可用的时候,那么这部分的消息就没办法进行消费。所以kafka为了提高partition的可靠性而提供了副本的概念(replica),通过副本机制类似实现冗余备份。
每个分区可以有多个副本,并且在副本集合中会存在一个 leader 的副本,所有的读写请求都是由leader 副本来进行处理。剩余的其他副本都做为follower 副本,follower 副本会从leader 副本同步消息日志,仅仅是同步消息。这个有点类似zookeeper 中leader 和follower 的概念,但是follower不会去处理请求,具体的实现方式还是有比较大的差异。所以我们可以认为,副本集会存在一主多从的关系。
一般情况下,同一个分区的多个副本会被均匀分配到集群中的不同broker 上,当leader 副本所在的broker 出现故
障后,可以重新选举新的leader 副本继续对外提供服务。通过这样的副本机制来提高kafka 集群的可用性。

副本分配算法 (没有证明出来)?????不过这个算法到感觉是获得broker上的 有哪些partition

  • 将所有 n 个 Broker 和 待分配的 Partition 排序
  • 将第 i 个partition分配到第(i%n)的 broker上,作为优先 副本
  • 将第 i 个partition 的第 j 个replica分配到第 ((i+j)%n)个broker 上

假设集群中共有4个brokers,存在 topic 为partitionReplication ,有4个partition,每个partition有三个replica。

bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 4 --topic partitionReplication

四个partitions是partitionReplication-0,partitionReplication-1,partitionReplication-2,partitionReplication-3

四个brokers是broker-0,broker-1,broker-2,broker-3

当第 0 个 partition 进行分配 (0%4)=0 ,即broker-0 上存放partitionReplication-0 作为优先 副本

当第 0 个 partition 的第 1个replica 进行分配 ((0+1)%4)=1,即broker-1 上存放partitionReplication-0的一个副本

当第 0 个 partition 的第 2个replica 进行分配 ((0+2)%4)=2,即broker-2 上存放partitionReplication-0的一个副本

即:isr:[0,1,2]

get /brokers/topics/partitionReplication/partitions/0/state
{"controller_epoch":1,"leader":0,"version":1,"leader_epoch":0,"isr":[0,2,3]}

获得partitionReplication主题的partition的第0个副本的存储位置,其中leader副本存储在broker-0 中

当第 1 个 partition 进行分配 (1%4)=1 ,即broker-1 上存放partitionReplication-1 作为优先 副本

当第 1 个 partition 的第 1个replica 进行分配 ((1+1)%4)=2,即broker-2 上存放partitionReplication-1的一个副本

当第 1 个 partition 的第 2个replica 进行分配 ((1+2)%4)=3,即broker-3 上存放partitionReplication-1的一个副本

即 :isr:[1,2,3]

get /brokers/topics/partitionReplication/partitions/1/state
{"controller_epoch":1,"leader":1,"version":1,"leader_epoch":0,"isr":[1,3,0]}

当第 2 个 partition 进行分配 (2%4)=2 ,即broker-2 上存放partitionReplication-2 作为优先 副本

当第 2 个 partition 的第 1个replica 进行分配 ((2+1)%4)=3,即broker-3 上存放partitionReplication-2的一个副本

当第 2 个 partition 的第 2个replica 进行分配 ((2+2)%4)=0,即broker-0 上存放partitionReplication-2的一个副本

即:isr:[2,3,0]

get /brokers/topics/partitionReplication/partitions/2/state
{"controller_epoch":1,"leader":2,"version":1,"leader_epoch":0,"isr":[2,0,1]}

当第 3 个 partition 进行分配 (3%4)=3 ,即broker-3 上存放partitionReplication-3 作为优先 副本

当第 3 个 partition 的第 1个replica 进行分配 ((3+1)%4)=0,即broker-0 上存放partitionReplication-1的一个副本

当第 3 个 partition 的第 2个replica 进行分配 ((3+2)%4)=1,即broker-1 上存放partitionReplication-1的一个副本

即:isr:[3,0,1]

get /brokers/topics/partitionReplication/partitions/3/state
{"controller_epoch":1,"leader":3,"version":1,"leader_epoch":0,"isr":[3,1,2]}

按照上面的算法来看,理论上的结果和实际的结果存在严重的不同,可是能看出 isr数组中,除了leader所在的broker 外,其他的 两个副本的跟推测的除了leader所在broker外两个副本好像顺序存在区别,我画个表格对比看下:

partition 理论ISR 实际ISR
0 [0,1,2] [0,2,3]
1 [1,2,3] [1,3,0]
2 [2,3,0] [2,0,1]
3 [3,0,1] [3,1,2]

于是可以很明显的看出

  • 当 partition=0 时,理论的副本集与 partition=3 的实际副本集相同,
  • 当 partition=1 时,理论的副本集与 partition=0 的实际副本集相同
  • 当 partition=2 时,理论的副本集与 partition=1 的实际副本集相同
  • 当 partition=3 时,理论的副本集与 partition=1 的实际副本集相同

于是在网上查找,说是 kafka是先随机挑选一个broker放置分区0,然后再按顺序放置其他分区。

于是我又测试一遍,创建两个topic,都是4个partition,3个replica

bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 4 --topic partitionReplication2
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 4 --topic wozaiceshiyibian

结果如下:

partition partitionReplication2的ISR wozaiceshiyibian的ISR
0 [2,0,1] [3,2,0]
1 [3,1,2] [0,3,1]
2 [0,2,3] [1,0,2]
3 [1,3,0] [2,1,3]

你可能感兴趣的:(消息中间件)