中间件Kafka

一、什么是Kafka?

Kafka是一个分布式的基于发布/订阅的模式的消息引擎系统。

  • 削峰填谷
  • 应用解耦
  • 异步处理
  • 消息通讯

Kafka的消费模式

点对点模式

点对点的通信,即一个发送一个接收。

消息被消费之后则删除。

支持多个消费者,但对于一条消息而言,只有一个消费者可以消费,即一条消息只能被一个消费者消费。
点对点消费模式

发布/订阅模式

一个消息发送到消息队列,消费者根据消息队列的订阅拉取消息消费。

消费者消费数据之后,数据不会被清除。 Kafka会默认保留一段时间,然后再删除。
发布订阅消费模式

二、Kafka基础架构图

在这里插入图片描述

  • Producer
    消息生产者,向Kafka中发布消息的角色。
  • Consumer
    消息消费者,即从Kafka中拉取消息消费的客户端。
  • Consumer Group
    消费者组,消费者组则是一组中存在多个消费者,消费者消费Broker中当前Topic的不同分区中的消息,消费者组之间互不影响,所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。某一个分区中的消息只能够一个消费者组中的一个消费者所消费
  • Broker
    经纪人,一台Kafka服务器就是一个Broker,一个集群由多个Broker组成,一个Broker可以容纳多个Topic。
  • Topic
    主题,可以理解为一个队列,生产者和消费者都是面向一个Topic。
  • Partition
    分区,为了实现扩展性,一个非常大的Topic可以分布到多个Broker上,一个Topic可以分为多个Partition,每个Partition是一个有序的队列(分区有序,不能保证全局有序)。
  • Replica
    副本Replication,为保证集群中某个节点发生故障,节点上的Partition数据不丢失,Kafka可以正常的工作,Kafka提供了副本机制,一个Topic的每个分区有若干个副本,一个Leader和多个Follower。
  • Leader
    每个分区多个副本的主角色,生产者发送数据的对象,以及消费者消费数据的对象都是Leader。
  • Follower
    每个分区多个副本的从角色,实时的从Leader中同步数据,保持和Leader数据的同步,Leader发生故障的时候,某个Follower会成为新的Leader。

2.1 基础配置

Broker参数
  • 存储类:
    • log.dirs=/home/kafka1,/home/kafka2,/home/kafka3
  • Zookeeper相关
    • zookeeper.connect=zk1:2181,zk2:2181,zk3:2181/kafka1
  • 连接类
    • listeners=CONTROLLER://localhost:9092
    • listeners.security.protocol.map=CONTROLLER:PLAINTEX
  • Topic管理
    • auto.create.topics.enable : 不能自立为王(为true时,发送topic消息,当topic不存在时会自动创建topic)
    • unclean.leader.election.enable: 宁缺毋烂(为true时,非ISR集合副本会竞争为leader副本,原leader副本数据因截取HW而丢失数据)
    • auto.leader.rebalance.enable:江山不易改(为true时,两个分区的leader副本的中的一方挂掉,另一方接替他的工作进行读写,当挂掉的重启后,它将继续作为leader副本存在,其他follower副本无权竞争leader副本)
  • 数据留存
    • log.retention.{hours|minutes|ms} :数据寿命 hours=168
    • log.rentention.bytes: 祖宅大小 -1 表示没限制
    • message.max.bytes: 祖宅大门宽度,默认1000012=976KB
主题级别的参数
  • 消息保存
    • retention.ms 规定了该 Topic 消息被保存的时长
    • retention.bytes 规定了要为该 Topic 预留多大的磁盘空间
  • 消息大小
    • max.message.bytes Kafka Broker 能够正常接收该 Topic 的最大消息大小
  • JVM参数
    • KAFKA_HEAP_OPTS
    • KAFKA_JVM_PERFORMANCE_OPTS
    • $> export KAFKA_HEAP_OPTS=–Xms6g --Xmx6g
    • $> export KAFKA_JVM_PERFORMANCE_OPTS= -server -
    • XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:+ExplicitGCInvokesConcurrent -Djava.awt.headless=true
  • 操作系统参数
    • 文件描述符限制
    • 文件系统类型
    • Swappiness
    • 提交时间

2.2 生产者

Kafka生产者架构

Kafka生产者架构

为什么需要分区?
  • 方便在集群中扩展;
  • 可以提高并发。
生产者分区策略

生产者是通过ProducerRecord来封装message的,通过源码可知:
在这里插入图片描述

  • 指明partition(这里的指明是指第几个分区)的情况下,直接将指明的值作为partition的值;
  • 没有指明partition的情况下,但是存在值key,此时将key的hash值与topic的partition总数进行取余得到partition值;
  • 值与partition均无的情况下,第一次调用时随机生成一个整数,后面每次调用在这个整数上自增,将这个值与topic可用的partition总数取余得到partition值,即round-robin(轮询)算法。
public class DefaultPartitioner implements Partitioner {
	public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
		// key不存在的时候执行
	    if (keyBytes == null) {
	        return stickyPartitionCache.partition(topic, cluster);
	    } 
	    List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
	    int numPartitions = partitions.size();
	    // hash the keyBytes to choose a partition
	    // hash取模选择分区
	    return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
	}
}

public class StickyPartitionCache {
    private final ConcurrentMap<String, Integer> indexCache;
    public StickyPartitionCache() {
        this.indexCache = new ConcurrentHashMap<>();
    }

    public int partition(String topic, Cluster cluster) {
        Integer part = indexCache.get(topic);
        if (part == null) {
            return nextPartition(topic, cluster, -1);
        }
        return part;
    }
    public int nextPartition(String topic, Cluster cluster, int prevPartition) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        Integer oldPart = indexCache.get(topic);
        Integer newPart = oldPart;
        // Check that the current sticky partition for the topic is either not set or that the partition that 
        // triggered the new batch matches the sticky partition that needs to be changed.
        if (oldPart == null || oldPart == prevPartition) {
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() < 1) {
                Integer random = Utils.toPositive(ThreadLocalRandom.current().nextInt());
                newPart = random % partitions.size();
            } else if (availablePartitions.size() == 1) {
                newPart = availablePartitions.get(0).partition();
            } else {
            	// 如果Key为null,则会随机分配一个分区
                while (newPart == null || newPart.equals(oldPart)) {
                    Integer random = Utils.toPositive(ThreadLocalRandom.current().nextInt());
                    newPart = availablePartitions.get(random % availablePartitions.size()).partition();
                }
            }
            // Only change the sticky partition if it is null or prevPartition matches the current sticky partition.
            if (oldPart == null) {
                indexCache.putIfAbsent(topic, newPart);
            } else {
                indexCache.replace(topic, prevPartition, newPart);
            }
            return indexCache.get(topic);
        }
        return indexCache.get(topic);
    }
}
自定义分区
/**
 * 业务分区器
 */
public class BizPartitioner implements Partitioner {

    private Random random = new Random();

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 获取集群中指定topic的所有分区信息
        List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
        int numOfPartition = partitionInfos.size();
        int partitionNum = 0;
        // key没有设置
        if (key == null) {
            // 随机指定分区
            partitionNum = random.nextInt(numOfPartition);
        } else {
            partitionNum = Math.abs((value.hashCode())) % numOfPartition;
        }
        System.out.println("key->" + key + ",value->" + value + "->send to partition:" + partitionNum);
        return partitionNum;
    }

    @Override
    public void close() {

    }

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

    }
}

/**
 * 测试类
 */
public class BizPartitionerTest {
    
    public static void main(String[] args) {
        Map<String, Object> configs = Maps.newHashMap();
        configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9091");
        configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 添加分区对应的业务分区,自定义实现类
        configs.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, BizPartitioner.class);
        // 创建生产者实例
        Producer producer = new KafkaProducer(configs);
        try {
            for (int i = 0; i < 10; i++) {
                // 创建消息
                ProducerRecord record = new ProducerRecord("Hello-Kafka", "hello kitty " + i + "!");
                // 发送并获取 发送结果
                RecordMetadata metadata = (RecordMetadata) producer.send(record).get();
                System.out.println(String.format("topic=%s, msg=%s, partition=%s, offset=%s, timestamp=%s",
                        metadata.topic(), record.value(),
                        metadata.partition(), metadata.offset(), metadata.timestamp()));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        producer.close();
    }
}

2.3 消费者

2.3.1 消费者基础架构图

消费者架构图

2.3.2 消费者消费指定分区

KafkaConsumer通过kafkaConsumer.assign(Arrays.asList(new TopicPartition(“Hello-Kafka”, 0)));来消费指定的分区。

public final class TopicPartition implements Serializable {
    private static final long serialVersionUID = -613627415771699627L;

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

	// 构造函数指定主题和分区
    public TopicPartition(String topic, int partition) {
        this.partition = partition;
        this.topic = topic;
    }
}

/**
 * 消费指定分区的实现
 */
public class AssignConsumer {
    public static final AtomicBoolean isRunning = new AtomicBoolean(true);

    public static void main(String[] args) {
        Map configs = new HashMap<>();
        configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, ServerConfig.BOOTSTRAP_SERVERS_CONFIG);
        configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        configs.put(ConsumerConfig.GROUP_ID_CONFIG, "group.demo");
        configs.put(ConsumerConfig.CLIENT_ID_CONFIG, "consumer.client.id.demo");
        KafkaConsumer consumer = new KafkaConsumer(configs);
        // 分配对应的分区
        consumer.assign(Lists.newArrayList(new TopicPartition("Hello-Kafka", 0)));
        while (isRunning.get()) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
            for (ConsumerRecord<String, String> record : records) {
                System.out.println(String.format("topic=%s, partition=%s, offset=%s, key=%s, value=%s",
                        record.topic(), record.partition(), record.offset(), record.key(), record.value()));
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
2.3.3 什么时候会触发分区分配策略(消费再均衡Rebalance)?
  • 消费者组中的消费者数量发生变化;
  • 订阅主题数量发生变化;
  • 订阅主题的分区数发生变化。
2.3.4 消费者分区分配策略

在kafka中,存在三种消费者分区分配策略,分别是Range分配策略(默认)、 RoundRobin分配策略(轮询)、 StickyAssignor分配策略(粘滞)。

// 消费端ConsumerConfig通过这个属性来执行分区策略
public static final String PARTITION_ASSIGNMENT_STRATEGY_CONFIG = "partition.assignment.strategy";
Range分配策略(范围分区)

范围分区按主题为基础。对于每个主题,我们以数字顺序排序可用分区,以字典顺序排序消费者。然后将分区数除以消费者总数,以确定分配给每个消费者的分区数。如果它没有平均划分,那么前几个消费者将有一个额外的分区。

假设 n = 分区数/消费者数量;
m = 分区数%消费者数量;
那么前m个消费者每个分配n+1个分区,后面的(消费者数量-m)个消费者每个分配n个分区

例如,假设有两个消费者C0和C1,两个主题t0和t1,每个主题有3个分区,因此分区为t0p0、t0p1、t0p2、t1p0、t1p1和t1p2。
分配策略将会是:

  • C0: [t0p0, t0p1, t1p0, t1p1]
  • C1: [t0p2, t1p2]
public class RangeAssignor extends AbstractPartitionAssignor {

    @Override
    public String name() {
        return "range";
    }

    private Map<String, List<MemberInfo>> consumersPerTopic(Map<String, Subscription> consumerMetadata) {
        Map<String, List<MemberInfo>> topicToConsumers = new HashMap<>();
        for (Map.Entry<String, Subscription> subscriptionEntry : consumerMetadata.entrySet()) {
            String consumerId = subscriptionEntry.getKey();
            MemberInfo memberInfo = new MemberInfo(consumerId, subscriptionEntry.getValue().groupInstanceId());
            for (String topic : subscriptionEntry.getValue().topics()) {
                put(topicToConsumers, topic, memberInfo);
            }
        }
        return topicToConsumers;
    }

    @Override
    public Map<String, List<TopicPartition>> assign(Map<String, Integer> partitionsPerTopic,
                                                    Map<String, Subscription> subscriptions) {
        Map<String, List<MemberInfo>> consumersPerTopic = consumersPerTopic(subscriptions);

        Map<String, List<TopicPartition>> assignment = new HashMap<>();
        for (String memberId : subscriptions.keySet())
            assignment.put(memberId, new ArrayList<>());

        for (Map.Entry<String, List<MemberInfo>> topicEntry : consumersPerTopic.entrySet()) {
            String topic = topicEntry.getKey();
            List<MemberInfo> consumersForTopic = topicEntry.getValue();

            Integer numPartitionsForTopic = partitionsPerTopic.get(topic);
            if (numPartitionsForTopic == null)
                continue;

            Collections.sort(consumersForTopic);

            int numPartitionsPerConsumer = numPartitionsForTopic / consumersForTopic.size();
            int consumersWithExtraPartition = numPartitionsForTopic % consumersForTopic.size();

            List<TopicPartition> partitions = AbstractPartitionAssignor.partitions(topic, numPartitionsForTopic);
            for (int i = 0, n = consumersForTopic.size(); i < n; i++) {
                int start = numPartitionsPerConsumer * i + Math.min(i, consumersWithExtraPartition);
                int length = numPartitionsPerConsumer + (i + 1 > consumersWithExtraPartition ? 0 : 1);
                assignment.get(consumersForTopic.get(i).memberId).addAll(partitions.subList(start, start + length));
            }
        }
        return assignment;
    }
}
RoundRobin分配策略(轮询分区)

The round robin assignor lays out all the available partitions and all the available consumers. It then proceeds to do a round robin assignment from partition to consumer. If the subscriptions of all consumer instances are identical, then the partitions will be uniformly distributed. (i.e., the partition ownership counts will be within a delta of exactly one across all consumers.)

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

例如,假设有两个消费者C0和C1,两个主题t0和t1,每个主题有3个分区,因此分区为t0p0、t0p1、t0p2、t1p0、t1p1和t1p2。

分配策略后将会是:

  • C0: [t0p0, t0p2, t1p1]
  • C1: [t0p1, t1p0, t1p2]

当消费者实例之间的订阅不同时,分配流程仍然以轮询方式考虑每个消费者实例,但如果没有订阅主题,则跳过该实例。与订阅相同的情况不同,这可能导致不平衡的分配。

例如,我们有三个消费者C0、C1、C2和三个主题t0、t1、t2,分别有1、2和3个分区。因此,分区为t0p0、t1p0、t1p1、t2p0、t2p1、t2p2。C0订阅t0;C1订阅t0, t1;C2订阅了t0 t1 t2。

分配策略后将会是:

  • C0: [t0p0]
  • C1: [t1p0]
  • C2: [t1p1, t2p0, t2p1, t2p2]
public class RoundRobinAssignor extends AbstractPartitionAssignor {

    @Override
    public Map<String, List<TopicPartition>> assign(Map<String, Integer> partitionsPerTopic,
                                                    Map<String, Subscription> subscriptions) {
        Map<String, List<TopicPartition>> assignment = new HashMap<>();
        List<MemberInfo> memberInfoList = new ArrayList<>();
        for (Map.Entry<String, Subscription> memberSubscription : subscriptions.entrySet()) {
            assignment.put(memberSubscription.getKey(), new ArrayList<>());
            memberInfoList.add(new MemberInfo(memberSubscription.getKey(),
                                              memberSubscription.getValue().groupInstanceId()));
        }

        CircularIterator<MemberInfo> assigner = new CircularIterator<>(Utils.sorted(memberInfoList));

        for (TopicPartition partition : allPartitionsSorted(partitionsPerTopic, subscriptions)) {
            final String topic = partition.topic();
            while (!subscriptions.get(assigner.peek().memberId).topics().contains(topic))
                assigner.next();
            assignment.get(assigner.next().memberId).add(partition);
        }
        return assignment;
    }

    private List<TopicPartition> allPartitionsSorted(Map<String, Integer> partitionsPerTopic,
                                                     Map<String, Subscription> subscriptions) {
        SortedSet<String> topics = new TreeSet<>();
        for (Subscription subscription : subscriptions.values())
            topics.addAll(subscription.topics());

        List<TopicPartition> allPartitions = new ArrayList<>();
        for (String topic : topics) {
            Integer numPartitionsForTopic = partitionsPerTopic.get(topic);
            if (numPartitionsForTopic != null)
                allPartitions.addAll(AbstractPartitionAssignor.partitions(topic, numPartitionsForTopic));
        }
        return allPartitions;
    }

    @Override
    public String name() {
        return "roundrobin";
    }

}
Stricky分配策略(粘滞)

The sticky assignor serves two purposes. First, it guarantees an assignment that is as balanced as possible; Second, it preserved as many existing assignment as possible when a reassignment occurs. This helps in saving some of the overhead processing when topic partitions move from one consumer to another.

  • topic partitions are still distributed as evenly as possible, and
  • topic partitions stay with their previously assigned consumers as much as possible.

StickyAssignor策略主要有两个目的:

  • 分区的分配尽可能的均匀;
  • 分区的分配尽可能和上次分配保持相同。
public class StickyAssignor extends AbstractStickyAssignor {

    // these schemas are used for preserving consumer's previously assigned partitions
    // list and sending it as user data to the leader during a rebalance
    static final String TOPIC_PARTITIONS_KEY_NAME = "previous_assignment";
    static final String TOPIC_KEY_NAME = "topic";
    static final String PARTITIONS_KEY_NAME = "partitions";
    private static final String GENERATION_KEY_NAME = "generation";

    static final Schema TOPIC_ASSIGNMENT = new Schema(
        new Field(TOPIC_KEY_NAME, Type.STRING),
        new Field(PARTITIONS_KEY_NAME, new ArrayOf(Type.INT32)));
    static final Schema STICKY_ASSIGNOR_USER_DATA_V0 = new Schema(
        new Field(TOPIC_PARTITIONS_KEY_NAME, new ArrayOf(TOPIC_ASSIGNMENT)));
    private static final Schema STICKY_ASSIGNOR_USER_DATA_V1 = new Schema(
        new Field(TOPIC_PARTITIONS_KEY_NAME, new ArrayOf(TOPIC_ASSIGNMENT)),
        new Field(GENERATION_KEY_NAME, Type.INT32));

    private List<TopicPartition> memberAssignment = null;
    private int generation = DEFAULT_GENERATION; // consumer group generation

    @Override
    public String name() {
        return "sticky";
    }

    @Override
    public void onAssignment(Assignment assignment, ConsumerGroupMetadata metadata) {
        memberAssignment = assignment.partitions();
        this.generation = metadata.generationId();
    }

    @Override
    public ByteBuffer subscriptionUserData(Set<String> topics) {
        if (memberAssignment == null)
            return null;

        return serializeTopicPartitionAssignment(new MemberData(memberAssignment, Optional.of(generation)));
    }

    @Override
    protected MemberData memberData(Subscription subscription) {
        ByteBuffer userData = subscription.userData();
        if (userData == null || !userData.hasRemaining()) {
            return new MemberData(Collections.emptyList(), Optional.empty());
        }
        return deserializeTopicPartitionAssignment(userData);
    }

    // visible for testing
    static ByteBuffer serializeTopicPartitionAssignment(MemberData memberData) {
        Struct struct = new Struct(STICKY_ASSIGNOR_USER_DATA_V1);
        List<Struct> topicAssignments = new ArrayList<>();
        for (Map.Entry<String, List<Integer>> topicEntry : CollectionUtils.groupPartitionsByTopic(memberData.partitions).entrySet()) {
            Struct topicAssignment = new Struct(TOPIC_ASSIGNMENT);
            topicAssignment.set(TOPIC_KEY_NAME, topicEntry.getKey());
            topicAssignment.set(PARTITIONS_KEY_NAME, topicEntry.getValue().toArray());
            topicAssignments.add(topicAssignment);
        }
        struct.set(TOPIC_PARTITIONS_KEY_NAME, topicAssignments.toArray());
        if (memberData.generation.isPresent())
            struct.set(GENERATION_KEY_NAME, memberData.generation.get());
        ByteBuffer buffer = ByteBuffer.allocate(STICKY_ASSIGNOR_USER_DATA_V1.sizeOf(struct));
        STICKY_ASSIGNOR_USER_DATA_V1.write(buffer, struct);
        buffer.flip();
        return buffer;
    }

    private static MemberData deserializeTopicPartitionAssignment(ByteBuffer buffer) {
        Struct struct;
        ByteBuffer copy = buffer.duplicate();
        try {
            struct = STICKY_ASSIGNOR_USER_DATA_V1.read(buffer);
        } catch (Exception e1) {
            try {
                // fall back to older schema
                struct = STICKY_ASSIGNOR_USER_DATA_V0.read(copy);
            } catch (Exception e2) {
                // ignore the consumer's previous assignment if it cannot be parsed
                return new MemberData(Collections.emptyList(), Optional.of(DEFAULT_GENERATION));
            }
        }

        List<TopicPartition> partitions = new ArrayList<>();
        for (Object structObj : struct.getArray(TOPIC_PARTITIONS_KEY_NAME)) {
            Struct assignment = (Struct) structObj;
            String topic = assignment.getString(TOPIC_KEY_NAME);
            for (Object partitionObj : assignment.getArray(PARTITIONS_KEY_NAME)) {
                Integer partition = (Integer) partitionObj;
                partitions.add(new TopicPartition(topic, partition));
            }
        }
        // make sure this is backward compatible
        Optional<Integer> generation = struct.hasField(GENERATION_KEY_NAME) ? Optional.of(struct.getInt(GENERATION_KEY_NAME)) : Optional.empty();
        return new MemberData(partitions, generation);
    }
}
2.3.5 消费者协调器和消费者组协调器

消费者入组过程

第一阶段 (FIND_COORDINATOR)

选择一个负载最小的Broker,发送查找组协调器请求。

第二阶段 (JOIN_GROUP)

找到组协调器后,申请加入,Join_Group_Request请求。

第三阶段(SYNC_GROUP)

各成员继续发送Sync_Group_Request请求。同时leader消费者根据策略分区分配。

第四阶段(HEARTBEAT)

三、文件存储

文件存储
在($KAFKA_HOME/config/server.properties中)配置 log.dirs=/tmp/kafka-logs 来设置Kafka消息文件存储目录。

一个topic分为多个分区partition,一个partition分为多个segmenet文件,segment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为segment索引文件和数据文件。

其中以“.index”索引文件中的元数据[3, 348]为例,在“.log”数据文件表示第3个消息,在全局partition中表示170410+3=170413个消息,该消息的物理偏移地址为348。
index和log文件对应关系

四、数据可靠性

Kafka中每个topic的每个partition可以分为多个副本,一主多从。并且存在一个leader的副本,所有的读写请求都是由leader 副本来进行处理,其他副本都做为follower副本,follower副本会从leader副本同步消息日志。

./kafka-topics.sh --create --zookeeper 127.0.0.1:2181 --replication-factor 2 --partitions 3 --topic demo_topic

创建了一个demo_topic的主题,对这个主题创建3个partition分区,和2个副本。kafka集群中的一个broker中最多只能有一个副本。 如下图所示:

三个分区连个副本的分布

4.1 AR 和 ISR

  • AR是指分区中所有的副本。
  • ISR是指与leader副本保持同步状态的副本集合。

ISR数据保存在Zookeeper的 /brokers/topics/[topic]/partitions/[partition]/state 节点中,可以通过zookeeper查看kafka的消息。

./bin/zkCli.sh -server 127.0.0.1:2181
ls /brokers
ls /brokers/topics/[topic]/partitions/[partition]/state
何时发送ack?

当producer向leader副本发送数据时,可以通过request.required.acks参数来设置数据可靠性的级别:

  • 1(默认):Producer在ISR中的Leader副本成功收到数据并得到确认。缺点:如果leader宕机了,则会丢失数据。
  • 0:Producer无需等待来自Broker的确认而继续发送下一批消息。优点:数据传输效率最高。缺点:数据可靠性是最低的。
  • -1:Producer需要等待ISR中的所有Follower副本都确认接收到数据后才算一次发送完成,优点:可靠性最高。
    此时存在一种数据重复的问题:当Leader副本接收数据(数据1)成功,多个Follower副本去同步Leader副本的数据,此时,Leader突然宕机了,假如FollowerA副本同步完成了,但是FollowerB副本还未同步完成,那么由于还未同步完成HW还是原来的值,此时,Zookeeper从Follower副本中选举出一个作为Leader副本,如果原FollowerA副本选举成为Leader副本了,数据1会继续往已同步原FollowerA副本中发送消息,这样就出现了重复消息。如下图所示:
    在这里插入图片描述
ISR

Follower副本同步Leader副本的全部日志之后,会更新副本的LastCaughtUpTimeMs标识,副本管理器会启动一个副本过期检查的定时任务定时检查LastCaughtUpTimeMs标识,当LastCaughtUpTimeMs与当前时间的差值大于replica.lag.time.max.ms参数配置的时间时,ISR集合会踢出Follower副本。ISR确保数据不丢失并且提高吞吐量

同步复制要求所有能工作的follower都复制完,这条消息才会被commit,这种复制方式极大的影响了吞吐率。
异步复制方式下,follower异步的从leader复制数据,数据只要被leader写入log就被认为已经commit,这种情况下如果follower都还没有复制完,落后于leader时,突然leader宕机,则会丢失数据。
而Kafka的这种使用ISR的方式则很好的均衡了确保数据不丢失以及吞吐率。

4.2 LEO(日志末端位移) 和 HW(高水位)

  • LEO是指每个分区中最后一条消息的下一个位置。
    Follower副本的LEO日志末端位移大于等于Leader的LEO日志末端位移时,说明这个副本已经“追赶上”Leader副本了。

  • HW是指ISR集合中最小的LEO。
    当Leader副本宕机后,新选举出来一个Leader副本,其他的Follower副本会截断日志到HW的位置,保证了数据的一致性。(当原Leader副本重启后,因为Follower副本的HW不能高于Leader副本的HW,所以截断至Leader副本HW的位置,此时,原Leader副本丢失了一部分数据,此问题可使用kakfa epoch解决)

在这里插入图片描述
一次完整的消息同步周期步骤:

  1. 当生产者给主题分区发送一条消息后,Leader 副本成功将消息写入了本地磁盘,故 LEO 值被更新为 1;
  2. Follower 再次尝试从 Leader 拉取消息,此时有数据,Follower 副本也成功地更新 LEO 为 1。此时,Leader 和 Follower 副本的 LEO 都是 1,但各自的高水位依然是 0,还没有被更新;
  3. 在新一轮的拉取请求中,由于位移值是 0 的消息已经拉取成功,因此 Follower 副本这次请求拉取的是位移值 =1 的消息。Leader 副本接收到此请求后,更新远程副本 LEO 为 1,然后更新 Leader 高水位为 1。做完这些之后,它会将当前已更新过的高水位值 1 发送给 Follower 副本。Follower 副本接收到以后,也将自己的高水位值更新成 1。

什么是远程副本?
如下图所示,Broker 0 上保存了某分区的 Leader 副本和所有 Follower 副本的 LEO 值,而 Broker 1 上仅仅保存了该分区的某个 Follower 副本。Kafka 把 Broker 0 上保存的这些 Follower 副本又称为远程副本(Remote Replica)。Kafka 副本机制在运行过程中,会更新 Broker 1 上 Follower 副本的高水位和 LEO 值,同时也会更新 Broker 0 上 Leader 副本的高水位和 LEO 以及所有远程副本的 LEO,但它不会更新远程副本的高水位值,也就是我在图中标记为灰色的部分。在这里插入图片描述

4.3 Leader Epoch

kakfa为了避免数据丢失保证数据可靠性的情况下,引入了一个Leader epoch来解决这个问题,Leader epoch实际上一对值分别是epoch版本号和Start Offset起始偏移量。

只要Leader副本发送变更操作,那么epoch版本号就+1,offect表示这个版本号刚开始写入消息时的偏移量。
比如:(epoch:0;start offset:0) -> (epoch:1;start offset:10)。
说明第一个Leader epoch表示版本号epoch是0,这个版本的 Leader 从偏移量 0 开始保存消息;
保存了10个数据以后,第二个Leader epoch表示epoch版本号增加到1,这个版本的起始位移是10。

follower副本会根据版本号来判断数据是否一致,不一致的情况下其会根据HW截断日志,从截断位置开始同步Leader副本的日志,避免数据丢失。
在这里插入图片描述

五、Kafka高性能

5.1 顺序写入、批量、压缩

在发送消息的时候,kafka不会直接将少量数据发送出去,否则每次发送少量的数据会增加网络传输频率,降低网络传输效率。kafka会先将消息缓存在内存中,当超过一个的大小或者超过一定的时间,那么会将这些消息进行批量发送。

当然网络传输时数据量小也可以减小网络负载,kafaka会将这些批量的数据进行压缩,将一批消息打包后进行压缩,发送broker服务器后,最终这些数据还是提供给消费者用,所以数据在服务器上还是保持压缩状态,不会进行解压,而且频繁的压缩和解压也会降低性能,最终还是以压缩的方式传递到消费者的手上。

5.2 页缓存

数据写入也缓存后就说明成功了,有操作系统来同步到磁盘。
在这里插入图片描述

5.3 零copy

在这里插入图片描述

六、Kafka Eagle 监控

下载Kafka Eagle

  1. 解压缩
  2. 配置环境变量
[root@localhost bin]# vim /etc/profile

export CLASSPATH=$JAVA_HONE/lib:$JAVA_HOME/jre/lib:$CLASSPATH
export PATH=$JAVA_HOME/bin:$PATH

[root@localhost bin]# source /etc/profile
  1. 修改$JAVA_HOME/conf/system-config.properties 配置文件
######################################
# multi zookeeper & kafka cluster list
######################################
kafka.eagle.zk.cluster.alias=cluster1
cluster1.zk.list=localhost:2181

######################################
# zookeeper enable acl
######################################
cluster1.zk.acl.enable=false
cluster1.zk.acl.schema=digest
cluster1.zk.acl.username=test
cluster1.zk.acl.password=test123

######################################
# broker size online list
######################################
cluster1.kafka.eagle.broker.size=20

######################################
# zk client thread limit
######################################
kafka.zk.limit.size=32

######################################
# kafka eagle webui port
######################################
kafka.eagle.webui.port=8048

######################################
# kafka jmx acl and ssl authenticate
######################################
cluster1.kafka.eagle.jmx.acl=false
cluster1.kafka.eagle.jmx.user=keadmin
cluster1.kafka.eagle.jmx.password=keadmin123
cluster1.kafka.eagle.jmx.ssl=false
cluster1.kafka.eagle.jmx.truststore.location=/Users/dengjie/workspace/ssl/certificates/kafka.truststore
cluster1.kafka.eagle.jmx.truststore.password=ke123456

######################################
# kafka offset storage
######################################
cluster1.kafka.eagle.offset.storage=kafka

######################################
# kafka jmx uri
######################################
cluster1.kafka.eagle.jmx.uri=service:jmx:rmi:///jndi/rmi://%s/jmxrmi

######################################
# kafka metrics, 15 days by default
######################################
kafka.eagle.metrics.charts=true
kafka.eagle.metrics.retain=15

######################################
# kafka sql topic records max
######################################
kafka.eagle.sql.topic.records.max=5000

######################################
# delete kafka topic token
######################################
kafka.eagle.topic.token=keadmin

######################################
# kafka sasl authenticate
######################################
cluster1.kafka.eagle.sasl.enable=false
cluster1.kafka.eagle.sasl.protocol=SASL_PLAINTEXT
cluster1.kafka.eagle.sasl.mechanism=SCRAM-SHA-256
cluster1.kafka.eagle.sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="kafka" password="kafka-eagle";
cluster1.kafka.eagle.sasl.client.id=
cluster1.kafka.eagle.blacklist.topics=
cluster1.kafka.eagle.sasl.cgroup.enable=false
cluster1.kafka.eagle.sasl.cgroup.topics=

######################################
# kafka mysql jdbc driver address
######################################
kafka.eagle.driver=com.mysql.cj.jdbc.Driver
kafka.eagle.url=jdbc:mysql://localhost:3060/ke?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
kafka.eagle.username=root
kafka.eagle.password=123456
  1. 启动
[root@localhost bin]# ./ke.sh start
[root@localhost bin]# ./ke.sh status
[2021-03-25 09:17:57] INFO : Kafka Eagle is running, [6759] .

启动成功
5. 查看
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

抽抽了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值