Kafka 主题与分区详解

2.3 主题

2.3.1 管理

使用kafka-topics.sh脚本:

选项 说明
--config <String: name=value> 为创建的或修改的主题指定配置信息。支持下述
配置条目:
cleanup.policy
compression.type
delete.retention.ms
file.delete.delay.ms
flush.messages
flush.ms
follower.replication.throttled.replicas
index.interval.bytes
leader.replication.throttled.replicas
max.message.bytes
message.format.version
message.timestamp.difference.max.ms
message.timestamp.type
min.cleanable.dirty.ratio
min.compaction.lag.ms
min.insync.replicas
preallocate
retention.bytes
retention.ms
segment.bytes
segment.index.bytes
segment.jitter.ms
segment.ms
unclean.leader.election.enable
--create 创建一个新主题
--delete 删除一个主题
--delete-config <String: name> 删除现有主题的一个主题配置条目。这些条目就是在--config 中给出的配置条目。
--alter 更改主题的分区数量,副本分配和/或配置条目
--describe 列出给定主题的细节。
--disable-rack-aware 禁用副本分配的机架感知
--force 抑制控制台提示信息
--help 打印帮助信息
--if-exists 如果指定了该选项,则在修改或删除主题的时候,只有主题存在才可以执行。
--if-not-exists 在创建主题的时候,如果指定了该选项,则只有主题不存在的时候才可以执行命令。
--list 列出所有可用的主题。
--partitions <Integer: # of partitions> 要创建或修改主题的分区数。
--replica-assignment
<String:broker_id_for_part1_replica1 :broker_id_for_part1_replica2
,broker_id_for_part2_replica1
:broker_id_for_part2_replica2 , ...>
当创建或修改主题的时候手动指定partition-tobroker的分配关系。
--replication-factor < Integer:replicationfactor > 要创建的主题分区副本数。1表示只有一个副本,也就是Leader副本。
--topic <String: topic> 要创建、修改或描述的主题名称。除了创建,修改和描述在这里还可以使用正则表达式。
--topics-with-overrides if set when describing topics, only show topics that have overridden configs
--unavailable-partitions if set when describing topics, only show partitions whose leader is not available
--under-replicated-partitions if set when describing topics, only show under replicated partitions
--zookeeper <String: urls> 必需的参数:连接zookeeper的字符串,逗号分隔的多个

主题中可以使用的参数定义:

属性 默认值 服务器默认属性 说明
cleanup.policy delete log.cleanup.policy 要么是”delete“要么是”compact“; 这个字符串指明了针对旧日志部分的利用方式;默认方式("delete")将会丢弃旧的部分当他们的回收时间或者尺寸限制到达时。”compact“将会进行日志压缩
compression.type none producer用于压缩数据的压缩类型。默认是无压缩。正确的选项值是none、gzip、snappy。压缩最好用于批量处理,批量处理消息越多,压缩性能越好。
delete.retention.ms 86400000
(24
hours)
log.cleaner.delete.retention.ms 对于压缩日志保留的最长时间,也是客户端消费消息的最长时间,通log.retention.minutes的区别在于一个控制未压缩数据,一个控制压缩后的数据。此项配置可以在topic创建时的置顶参数覆盖
flush.ms None log.flush.interval.ms 此项配置用来置顶强制进行fsync日志到磁盘的时间间隔;例如,如果设置为1000,那么每1000ms就需要进行一次fsync。一般不建议使用这个选项
flush.messages None log.flush.interval.messages 此项配置指定时间间隔:强制进行fsync日志。例如,如果这个选项设置为1,那么每条消息之后都需要进行fsync,如果设置为5,则每5条消息就需要进行一次
fsync。一般来说,建议你不要设置这个值。此参数的设置,需要在"数据可靠性"与"性能"之间做必要的权衡.如果此值过大,将会导致每次"fsync"的时间较长(IO阻塞),如果此值过小,将会导致"fsync"的
次数较多,这也意味着整体的client请求有一定的延迟.物理server故障,将会导致没有fsync的消息丢失.
index.interval.bytes 4096 log.index.interval.bytes 默认设置保证了我们每4096个字节就对消息添加一个索引,更多的索引使得阅读的消息更加靠近,但是索引规模却会由此增大;一般不需要改变这个选项
max.message.bytes 1000000 max.message.bytes kafka追加消息的最大尺寸。注意如果你增大这个尺寸,你也必须增大你consumer的fetch 尺寸,这样consumer才能fetch到这些最大尺寸的消息。
min.cleanable.dirty.ratio 0.5 min.cleanable.dirty.ratio 此项配置控制log压缩器试图进行清除日志的频率。默认情况下,将避免清除压缩率超过50%的日志。这个比率避免了最大的空间浪费
min.insync.replicas 1 min.insync.replicas 当producer设置request.required.acks为-1时,
min.insync.replicas指定replicas的最小数目(必须确认每一个repica的写数据都是成功的),如果这个数目没有达到,producer会产生异常。
retention.bytes None log.retention.bytes 如果使用“delete”的retention 策略,这项配置就是指在删除日志之前,日志所能达到的最大尺寸。默认情况下,没有尺寸限制而只有时间限制
retention.ms 7 days log.retention.minutes 如果使用“delete”的retention策略,这项配置就是指删除日志前日志保存的时间。
segment.bytes 1GB log.segment.bytes kafka中log日志是分成一块块存储的,此配置是指log日志划分成块的大小
segment.index.bytes 10MB log.index.size.max.bytes 此配置是有关offsets和文件位置之间映射的索引文件的大小;一般不需要修改这个配置
segment.jitter.ms 0 log.roll.jitter. The maximum jitter to subtract
from logRollTimeMillis.
segment.ms 7 days log.roll.hours 即使log的分块文件没有达到需要删除、压缩的大小,一旦log 的时间达到这个上限,就会强制新建一个log分块文件
unclean.leader.election.enable true 指明了是否能够使不在ISR中
replicas设置用来作为leader

2.3.1.1 创建主题

kafka-topics.sh --zookeeper localhost:2181/myKafka --create --topic topic_x --partitions 1 --replication-factor 1
kafka-topics.sh --zookeeper localhost:2181/myKafka --create --topic topic_test_02 --partitions 3 --replication-factor 1 --config max.message.bytes=1048576 --config segment.bytes=10485760

2.3.1.2 查看主题

kafka-topics.sh --zookeeper localhost:2181/myKafka --list
kafka-topics.sh --zookeeper localhost:2181/myKafka --describe --topic topic_x
kafka-topics.sh --zookeeper localhost:2181/myKafka --topics-with-overrides --describe

2.3.1.3 修改主题

kafka-topics.sh --zookeeper localhost:2181/myKafka --create --topic topic_test_01 --partitions 2 --replication-factor 1
kafka-topics.sh --zookeeper localhost:2181/myKafka --alter --topic topic_test_01 --config max.message.bytes=1048576
kafka-topics.sh --zookeeper localhost:2181/myKafka --describe --topic topic_test_01
kafka-topics.sh --zookeeper localhost:2181/myKafka --alter --topic topic_test_01 --config segment.bytes=10485760
kafka-topics.sh --zookeeper localhost:2181/myKafka --alter --delete-config max.message.bytes --topic topic_test_01

2.3.1.4 删除主题

kafka-topics.sh --zookeeper localhost:2181/myKafka --delete 1 --topic topic_x

其实是给主题一个删除标记,过一段时间删除

2.3.2 增加分区

通过命令行工具操作,主题的分区只能增加,不能减少。否则报错。

通过--alter修改主题的分区数,增加分区。

kafka-topics.sh --zookeeper localhost/myKafka --alter --topic myTop1 --partitions 2

2.3.3 必要参数配置

kafka-topics.sh --config xx=xx --config yy=yy 配置给主题的参数。

属性 默认值 服务器默认属性 说明
cleanup.policy delete log.cleanup.policy 要么是”delete“要么是”compact“; 这个字符串指明了针对旧日志部分的利用方式;默认方式("delete")将会丢弃旧的部分当他们的回收时间或者尺寸限制到达时。”compact“将会进行日志压缩
compression.type none producer用于压缩数据的压缩类型。默认是无压缩。正确的选项值是none、gzip、snappy、lz4。压缩最好用于批量处理,批量处理消息越多,压缩性能越好。
max.message.bytes 1000000 max.message.bytes kafka追加消息的最大字节数。注意如果你增大这个字节数,也必须增大consumer的fetch字节数,这样consumer才能fetch到这些最大字节数的消息。
min.cleanable.dirty.ratio 0.5 min.cleanable.dirty.ratio 此项配置控制log压缩器试图进行清除日志的频率。默认情况下,将避免清除压缩率超过50%的日志。这个比率避免了最大的空间浪费
min.insync.replicas 1 min.insync.replicas min.insync.replicas指定replicas的最小数目(必须确认每一个repica的写数据都是成功的),如果这个数目没有达到,producer会产生异常。
retention.bytes None log.retention.bytes 如果使用“delete”的retention 策略,这项配置就是指在删除日志之前,日志所能达到的最大尺寸。默认情况下,没有尺寸限制而只有时间限制
retention.ms 7 days log.retention.minutes 如果使用“delete”的retention策略,这项配置就是指删除日志前日志保存的时间。
segment.bytes 1GB log.segment.bytes kafka中log日志是分成一块块存储的,此配置是指log日志划分成块的大小
segment.index.bytes 10MB log.index.size.max.bytes 此配置是有关offsets和文件位置之间映射的索引文件的大小;一般不需要修改这个配置
segment.jitter.ms 0 log.roll.jitter. The maximum jitter to subtract from logRollTimeMillis.
segment.ms 7 days log.roll.hours 即使log的分块文件没有达到需要删除、压缩的大小,一旦log的时间达到这个上限,就会强制新建一个log分块文件
unclean.leader.election.enable true 指明了是否能够使不在ISR中 replicas设置用来作为leader

2.3.4 KafkaAdminClient应用

说明

除了使用Kafka的bin目录下的脚本工具来管理Kafka,还可以使用管理Kafka的API将某些管理查看的功能集成到系统中。在Kafka0.11.0.0版本之前,可以通过kafka-core包(Kafka的服务端,采用Scala编写)中的AdminClient和AdminUtils来实现部分的集群管理操作。Kafka0.11.0.0之后,又多了一个AdminClient,在kafka-client包下,一个抽象类,具体的实现是 org.apache.kafka.clients.admin.KafkaAdminClient。

功能与原理介绍

Kafka官网:The AdminClient API supports managing and inspecting topics, brokers, acls, and other Kafka objects。

KafkaAdminClient包含了一下几种功能(以Kafka1.0.2版本为准):

  1. 创建主题:

    1. createTopics(final Collection newTopics, final CreateTopicsOptions options)
  2. 删除主题:

    1. deleteTopics(final Collection topicNames, DeleteTopicsOptions options)
  3. 列出所有主题:

    1. listTopics(final ListTopicsOptions options)
  4. 查询主题:

    1. describeTopics(final Collection topicNames, DescribeTopicsOptions options)
  5. 查询集群信息:

    1. describeCluster(DescribeClusterOptions options)
  6. 查询配置信息:

    1. describeConfigs(Collection configResources, final DescribeConfigsOptions options)
  7. 修改配置信息:

    1. alterConfigs(Map<ConfigResource, Config> configs, final AlterConfigsOptions options)
  8. 修改副本的日志目录:

    1. alterReplicaLogDirs(Map<TopicPartitionReplica, String> replicaAssignment, final AlterReplicaLogDirsOptions options)
  9. 查询节点的日志目录信息:

    1. describeLogDirs(Collection brokers, DescribeLogDirsOptions options)
  10. 查询副本的日志目录信息:

    1. describeReplicaLogDirs(Collection replicas,DescribeReplicaLogDirsOptions options)
  11. 增加分区:

    1. createPartitions(Map<String, NewPartitions> newPartitions, fina lCreatePartitionsOptions options)

其内部原理是使用Kafka自定义的一套二进制协议来实现,详细可以参见Kafka协议。

用到的参数:

属性 说明 重要性
bootstrap.servers 向Kafka集群建立初始连接用到的host/port列表。客户端会使用这里列出的所有服务器进行集群其他服务器的发现,而不管是否指定了哪个服务器用作引导。
这个列表仅影响用来发现集群所有服务器的初始主机。
字符串形式:host1:port1,host2:port2,...
由于这组服务器仅用于建立初始链接,然后发现集群中的所有服务器,因此没有必要将集群中的所有地址写在这里。
一般最好两台,以防其中一台宕掉。
high
client.id 生产者发送请求的时候传递给broker的id字符串。用于在broker的请求日志中追踪什么应用发送了什么消息。
一般该id是跟业务有关的字符串。
medium
connections.max.idle.ms 当连接空闲时间达到这个值,就关闭连接。long型数据,默认:300000 medium
receive.buffer.bytes TCP接收缓存(SO_RCVBUF),如果设置为-1,则使用操作系统默认的值。int类型值,默认65536,
可选值:[-1,...]
medium
request.timeout.ms 客户端等待服务端响应的最大时间。如果该时间超时,则客户端要么重新发起请求,要么如果重试耗尽,请求失败。int类型值,默认:120000 medium
security.protocol 跟broker通信的协议:PLAINTEXT, SSL,
SASL_PLAINTEXT, SASL_SSL.
string类型值,默认:PLAINTEXT
medium
send.buffer.bytes 用于TCP发送数据时使用的缓冲大小(SO_SNDBUF),-1表示使用OS默认的缓冲区大小。
int类型值,默认值:131072
medium
reconnect.backoff.max.ms 对于每个连续的连接失败,每台主机的退避将成倍增加,直至达到此最大值。在计算退避增量之后,添加20%的随机抖动以避免连接风暴。
long型值,默认1000,可选值:[0,...]
low
reconnect.backoff.ms 重新连接主机的等待时间。避免了重连的密集循环。该等待时间应用于该客户端到broker的所有连接。long型值,默认:50 low
retries The maximum number of times to retry a call before failing it.重试的次数,达到此值,失败。
int类型值,默认5。
low
retry.backoff.ms 在发生失败的时候如果需要重试,则该配置表示客户端等待多长时间再发起重试。
该时间的存在避免了密集循环。
long型值,默认值:100。
low

主要操作步骤:

客户端根据方法的调用创建相应的协议请求,比如创建Topic的createTopics方法,其内部就是发送CreateTopicRequest请求。

客户端发送请求至Kafka Broker。

Kafka Broker处理相应的请求并回执,比如与CreateTopicRequest对应的是 CreateTopicResponse。 客户端接收相应的回执并进行解析处理。和协议有关的请求和回执的类基本都在org.apache.kafka.common.requests包中,AbstractRequest和AbstractResponse是这些请求和响应类的两个父类。

综上,如果要自定义实现一个功能,只需要三个步骤:

  1. 自定义XXXOptions;
  2. 自定义XXXResult返回值;
  3. 自定义Call,然后挑选合适的XXXRequest和XXXResponse来实现Call类中的3个抽象方法。
package com.lagou.kafka.demo;
import org.apache.kafka.clients.admin.*;
import org.apache.kafka.common.KafkaFuture;
import org.apache.kafka.common.Node;
import org.apache.kafka.common.TopicPartitionInfo;
import org.apache.kafka.common.config.ConfigResource;
import org.apache.kafka.common.requests.DescribeLogDirsResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

public class MyAdminClient {
    private KafkaAdminClient client;
    @Before
    public void before() {
        Map<String, Object> conf = new HashMap<>();
        conf.put("bootstrap.servers", "node1:9092");
        conf.put("client.id", "adminclient-1");
        client = (KafkaAdminClient) KafkaAdminClient.create(conf);
    }
    @After
    public void after() {
        client.close();
    }
    @Test
    public void testListTopics1() throws ExecutionException,
    InterruptedException {
        ListTopicsResult listTopicsResult = client.listTopics();
        // KafkaFuture<Collection<TopicListing>> listings =
        listTopicsResult.listings();
        // Collection<TopicListing> topicListings = listings.get();
        //
        // topicListings.forEach(new Consumer<TopicListing>() {
        // @Override
        // public void accept(TopicListing topicListing) {
        // boolean internal = topicListing.isInternal();
        // String name = topicListing.name();
        // String s = topicListing.toString();
        // System.out.println(s + "\t" + name + "\t" + internal);
        // }
        // });
        // KafkaFuture<Set<String>> names = listTopicsResult.names();
        // Set<String> strings = names.get();
        //
        // strings.forEach(name -> {
        // System.out.println(name);
        // });
        // KafkaFuture<Map<String, TopicListing>> mapKafkaFuture =
        listTopicsResult.namesToListings();
        // Map<String, TopicListing> stringTopicListingMap =
        mapKafkaFuture.get();
        //
        // stringTopicListingMap.forEach((k, v) -> {
        // System.out.println(k + "\t" + v);
        // });
        ListTopicsOptions options = new ListTopicsOptions();
        options.listInternal(false);
        options.timeoutMs(500);
        ListTopicsResult listTopicsResult1 = client.listTopics(options);
        Map<String, TopicListing> stringTopicListingMap =
            listTopicsResult1.namesToListings().get();
        stringTopicListingMap.forEach((k, v) -> {
            System.out.println(k + "\t" + v);
        });
        // 关闭管理客户端
        client.close();
    }
    @Test
    public void testCreateTopic() throws ExecutionException,
    InterruptedException {
        Map<String, String> configs = new HashMap<>();
        configs.put("max.message.bytes", "1048576");
        configs.put("segment.bytes", "1048576000");
        NewTopic newTopic = new NewTopic("adm_tp_01", 2, (short) 1);
        newTopic.configs(configs);
        CreateTopicsResult topics = client.createTopics(Collections.singleton(newTopic));
        KafkaFuture<Void> all = topics.all();
        Void aVoid = all.get();
        System.out.println(aVoid);
    }
    @Test
    public void testDeleteTopic() throws ExecutionException,
    InterruptedException {
        DeleteTopicsOptions options = new DeleteTopicsOptions();
        options.timeoutMs(500);
        DeleteTopicsResult deleteResult = client.deleteTopics(Collections.singleton("adm_tp_01"), options);
        deleteResult.all().get();
    }
    @Test
    public void testAlterTopic() throws ExecutionException,
    InterruptedException {
        NewPartitions newPartitions = NewPartitions.increaseTo(5);
        Map<String, NewPartitions> newPartitionsMap = new HashMap<>();
        newPartitionsMap.put("adm_tp_01", newPartitions);
        CreatePartitionsOptions option = new CreatePartitionsOptions();
        // Set to true if the request should be validated without creating
        new partitions.
            // 如果只是验证,而不创建分区,则设置为true
            // option.validateOnly(true);
            CreatePartitionsResult partitionsResult = client.createPartitions(newPartitionsMap, option);
        Void aVoid = partitionsResult.all().get();
    }
    @Test
    public void testDescribeTopics() throws ExecutionException,
    InterruptedException {
        DescribeTopicsOptions options = new DescribeTopicsOptions();
        options.timeoutMs(3000);
        DescribeTopicsResult topicsResult = client.describeTopics(Collections.singleton("adm_tp_01"), options);
        Map<String, TopicDescription> stringTopicDescriptionMap = topicsResult.all().get();
        stringTopicDescriptionMap.forEach((k, v) -> {
            System.out.println(k + "\t" + v);
            System.out.println("=======================================");
            System.out.println(k);
            boolean internal = v.isInternal();
            String name = v.name();
            List<TopicPartitionInfo> partitions = v.partitions();
            String partitionStr = Arrays.toString(partitions.toArray());
            System.out.println("内部的?" + internal);
            System.out.println("topic name = " + name);
            System.out.println("分区:" + partitionStr);
            partitions.forEach(partition -> {
                System.out.println(partition);
            });
        });
    }
    @Test
    public void testDescribeCluster() throws ExecutionException,
    InterruptedException {
        DescribeClusterResult describeClusterResult = client.describeCluster();
        KafkaFuture<String> stringKafkaFuture = describeClusterResult.clusterId();
        String s = stringKafkaFuture.get();
        System.out.println("cluster name = " + s);
        KafkaFuture<Node> controller = describeClusterResult.controller();
        Node node = controller.get();
        System.out.println("集群控制器:" + node);
        Collection<Node> nodes = describeClusterResult.nodes().get();
        nodes.forEach(node1 -> {
            System.out.println(node1);
        });
    }
    @Test
    public void testDescribeConfigs() throws ExecutionException,
    InterruptedException, TimeoutException {
        ConfigResource configResource = new ConfigResource(ConfigResource.Type.BROKER, "0");
        DescribeConfigsResult describeConfigsResult = client.describeConfigs(Collections.singleton(configResource));
        Map<ConfigResource, Config> configMap = describeConfigsResult.all().get(15, TimeUnit.SECONDS);
        configMap.forEach(new BiConsumer<ConfigResource, Config>() {
            @Override
            public void accept(ConfigResource configResource, Config config) {
                ConfigResource.Type type = configResource.type();
                String name = configResource.name();
                System.out.println("资源名称:" + name);
                Collection<ConfigEntry> entries = config.entries();
                entries.forEach(new Consumer<ConfigEntry>() {
                    @Override
                    public void accept(ConfigEntry configEntry) {
                        boolean aDefault = configEntry.isDefault();
                        boolean readOnly = configEntry.isReadOnly();
                        boolean sensitive = configEntry.isSensitive();
                        String name1 = configEntry.name();
                        String value = configEntry.value();
                        System.out.println("是否默认:" + aDefault + "\t是否
                                           只读?" + readOnly + "\t是否敏感?" + sensitive
                                           + "\t" + name1 + " --> " + value);
                    }
                });
                ConfigEntry retries = config.get("retries");
                if (retries != null) {
                    System.out.println(retries.name() + " -->" +
                                       retries.value());
                } else {
                    System.out.println("没有这个属性");
                }
            }
        });
    }
    @Test
    public void testAlterConfig() throws ExecutionException,
    InterruptedException {
        // 这里设置后,原来资源中不冲突的属性也会丢失,直接按照这里的配置设置
        Map<ConfigResource, Config> configMap = new HashMap<>();
        ConfigResource resource = new ConfigResource(ConfigResource.Type.TOPIC, "adm_tp_01");
        Config config = new Config(Collections.singleton(new ConfigEntry("segment.bytes", "1048576000")));
        configMap.put(resource, config);
        AlterConfigsResult alterConfigsResult = client.alterConfigs(configMap);
        Void aVoid = alterConfigsResult.all().get();
    }
    @Test
    public void testDescribeLogDirs() throws ExecutionException,
    InterruptedException {
        DescribeLogDirsOptions option = new DescribeLogDirsOptions();
        option.timeoutMs(1000);
        DescribeLogDirsResult describeLogDirsResult = client.describeLogDirs(Collections.singleton(0), option);
        Map<Integer, Map<String, DescribeLogDirsResponse.LogDirInfo>> integerMapMap = describeLogDirsResult.all().get();
        integerMapMap.forEach(new BiConsumer<Integer, Map<String, DescribeLogDirsResponse.LogDirInfo>>() {
            @Override
            public void accept(Integer integer, Map<String, DescribeLogDirsResponse.LogDirInfo> stringLogDirInfoMap) {
                System.out.println("broker.id = " + integer);
                stringLogDirInfoMap.forEach(new BiConsumer<String, DescribeLogDirsResponse.LogDirInfo>() {
                    @Override
                    public void accept(String s, DescribeLogDirsResponse.LogDirInfo logDirInfo) {
                        System.out.println("log.dirs:" + s);
                        // 查看该broker上的主题/分区/偏移量等信息
                        // logDirInfo.replicaInfos.forEach(new
                        BiConsumer<TopicPartition, DescribeLogDirsResponse.ReplicaInfo>() {
                            // @Override
                            // public void accept(TopicPartition
                            topicPartition, DescribeLogDirsResponse.ReplicaInfo replicaInfo) {
                                // int partition =
                                topicPartition.partition();
                                // String topic = topicPartition.topic();
                                // boolean isFuture = replicaInfo.isFuture;
                                // long offsetLag = replicaInfo.offsetLag;
                                // long size = replicaInfo.size;
                                // System.out.println("partition:" +
                                partition + "\ttopic:" + topic
                                    // + "\tisFuture:" + isFuture
                                    // + "\toffsetLag:" + offsetLag
                                    // + "\tsize:" + size);
                                    // }
                                    // });
                            }
                        });
                    }
                });
            }
        }

2.3.5 偏移量管理

Kafka 1.0.2,__consumer_offsets主题中保存各个消费组的偏移量。早期由zookeeper管理消费组的偏移量。

查询方法:
通过原生 kafka 提供的工具脚本进行查询。工具脚本的位置与名称为bin/kafka-consumer-groups.sh首先运行脚本,查看帮助:

参数 说明
--all-topics 将所有关联到指定消费组的主题都划归到reset-offsets 操作范围。
--bootstrapserver
<String: server to
connect to>
必须:(基于消费组的新的消费者): 要连接的服务器地址。
--by-duration duration> 距离当前时间戳的一个时间段。格式:'PnDTnHnMnS'
--command-config <String:command config property file> 指定配置文件,该文件内容传递给Admin Client和消费者。
--delete 传值消费组名称,删除整个消费组与所有主题的各个分区偏移量和所有者关系。
如: --group g1 --group g2 。
传值消费组名称和单个主题,仅删除该消费组到指定主题的分区偏移量和所属关系。
如: --group g1 --group g2 --topic t1 。
传值一个主题名称,仅删除指定主题与所有消费组分区偏移量以
及所属关系。
如: --topic t1
注意:消费组的删除仅对基于ZK保存偏移量的消费组有效,并且要小心使用,仅删除不活跃的消费组。
--describe 描述给定消费组的偏移量差距(有多少消息还没有消费)。
--execute 执行操作。支持的操作: reset-offsets 。
--export 导出操作的结果到CSV文件。支持的操作: reset-offsets 。
--from-file <String: path
to CSV file>
重置偏移量到CSV文件中定义的值。
--group <String: consumer
group>
目标消费组。
--list 列出所有消费组。
--new-consumer 使用新的消费者实现。这是默认值。随后的发行版中会删除这一操作。
--reset-offsets 重置消费组的偏移量。当前一次操作只支持一个消费组,并且该
消费组应该是不活跃的。
有三个操作选项
1. (默认)plan:要重置哪个偏移量。
2. execute:执行reset-offsets 操作。
3. process:配合--export 将操作结果导出到CSV格式。
可以使用如下选项:
--to-datetime
--by-period
--to-earliest
--to-latest
--shift-by
--from-file
--to-current 。
必须选择一个选项使用。
要定义操作的范围,使用:
--all-topics
--topic 。
必须选择一个,除非使用--from-file 选项。
--shift-by <Long:number-of-offsets > 重置偏移量n个。n可以是正值,也可以是负值。
--timeout <Long: timeout (ms)> 对某些操作设置超时时间。
如:对于描述指定消费组信息,指定毫秒值的最大等待时间,以获取正常数据(如刚创建的消费组,或者消费组做了一些更改操作)。默认时间: 5000 。
--to-current 重置到当前的偏移量。
--to-datetime <String: datetime> 重置偏移量到指定的时间戳。格式:'YYYY-MMDDTHH:mm:SS.sss'
--to-earliest 重置为最早的偏移量
--to-latest 重置为最新的偏移量
--to-offset <Long: offset> 重置到指定的偏移量。
--topic <String: topic> 指定哪个主题的消费组需要删除,或者指定哪个主题的消费组需要包含到reset-offsets 操作中。对于reset-offsets 操作,还可以指定分区: topic1:0,1,2 。其中0,1,2表示要包含到操作中的分区号。重置偏移量的操作支持多个主题一起操作。
--zookeeper <String: urls> 必须,它的值,你懂的。--zookeeper node1:2181/myKafka 。

这里我们先编写一个生产者,消费者的例子:

我们先启动消费者,再启动生产者, 再通过 bin/kafka-consumer-groups.sh 进行消费偏移量查询,由于kafka 消费者记录group的消费偏移量有两种方式 :

1)kafka 自维护 (新)

2)zookpeer 维护 (旧) ,已经逐渐被废弃

所以 ,脚本只查看由broker维护的,由zookeeper维护的可以将--bootstrap-server 换成--zookeeper 即可。

2.4 分区

2.4.1 副本机制

image-20220710222208538

Kafka在一定数量的服务器上对主题分区进行复制。当集群中的一个broker宕机后系统可以自动故障转移到其他可用的副本上,不会造成数据丢失。

--replication-factor 3 1leader+2follower

  1. 将复制因子为1的未复制主题称为复制主题。
  2. 主题的分区是复制的最小单元。
  3. 在非故障情况下,Kafka中的每个分区都有一个Leader副本和零个或多个Follower副本。
  4. 包括Leader副本在内的副本总数构成复制因子。
  5. 所有读取和写入都由Leader副本负责。
  6. 通常,分区比broker多,并且Leader分区在broker之间平均分配。

Follower分区像普通的Kafka消费者一样,消费来自Leader分区的消息,并将其持久化到自己的日志中。允许Follower对日志条目拉取进行批处理。

同步节点定义:

  1. 节点必须能够维持与ZooKeeper的会话(通过ZooKeeper的心跳机制)
  2. 对于Follower副本分区,它复制在Leader分区上的写入,并且不要延迟太多 Kafka提供的保证是,只要有至少一个同步副本处于活动状态,提交的消息就不会丢失。

宕机如何恢复

  1. 少部分副本宕机:当leader宕机了,会从follower选择一个作为leader。当宕机的重新恢复时,会把之前commit的数据清空,重新从leader里pull数据。
  2. 全部副本宕机

当全部副本宕机了有两种恢复方式

1、等待ISR中的一个恢复后,并选它作为leader。(等待时间较长,降低可用性)

2、选择第一个恢复的副本作为新的leader,无论是否在ISR中。(并未包含之前leader commit的数据,因此造成数据丢失)

2.4.2 Leader选举

下图中:

分区P1的Leader是0,ISR是0和1

分区P2的Leader是2,ISR是1和2

分区P3的Leader是1,ISR是0,1,2

image-20220710223116291

生产者和消费者的请求都由Leader副本来处理。Follower副本只负责消费Leader副本的数据和 Leader保持同步。

对于P1,如果0宕机会发生什么?

Leader副本和Follower副本之间的关系并不是固定不变的,在Leader所在的broker发生故障的时候,就需要进行分区的Leader副本和Follower副本之间的切换,需要选举Leader副本。

如何选举?

如果某个分区所在的服务器出了问题,不可用,kafka会从该分区的其他的副本中选择一个作为新的Leader。之后所有的读写就会转移到这个新的Leader上。现在的问题是应当选择哪个作为新的Leader。

只有那些跟Leader保持同步的Follower才应该被选作新的Leader。Kafka会在Zookeeper上针对每个Topic维护一个称为ISR(in-sync replica,已同步的副本)的集合,该集合中是一些分区的副本。只有当这些副本都跟Leader中的副本同步了之后,kafka才会认为消息已提交,并反馈给消息的生产者。

如果这个集合有增减,kafka会更新zookeeper上的记录。如果某个分区的Leader不可用,Kafka就会从ISR集合中选择一个副本作为新的Leader。显然通过ISR,kafka需要的冗余度较低,可以容忍的失败数比较高。

假设某个topic有N+1个副本,kafka可以容忍N个服务器不可用。

为什么不用少数服从多数的方法?

少数服从多数是一种比较常见的一致性算发和Leader选举法。它的含义是只有超过半数的副本同步了,系统才会认为数据已同步;选择Leader时也是从超过半数的同步的副本中选择。这种算法需要较高的冗余度,跟Kafka比起来,浪费资源。

譬如只允许一台机器失败,需要有三个副本;而如果只容忍两台机器失败,则需要五个副本。而kafka的ISR集合方法,分别只需要两个和三个副本。

如果所有的ISR副本都失败了怎么办?

此时有两种方法可选,

  1. 等待ISR集合中的副本复活
  2. 选择任何一个立即可用的副本,而这个副本不一定是在ISR集合中。需要设置unclean.leader.election.enable=true,这两种方法各有利弊,实际生产中按需选择。 如果要等待ISR副本复活,虽然可以保证一致性,但可能需要很长时间。而如果选择立即可用的副本,则很可能该副本并不一致。

总结:
Kafka中Leader分区选举,通过维护一个动态变化的ISR集合来实现,一旦Leader分区丢掉,则从ISR中随机挑选一个副本做新的Leader分区。

如果ISR中的副本都丢失了,则:

  1. 可以等待ISR中的副本任何一个恢复,接着对外提供服务,需要时间等待。
  2. 从OSR中选出一个副本做Leader副本,此时会造成数据丢失

2.4.3 分区分配策略

image-20220711104949255

在Kafka中,每个Topic会包含多个分区,默认情况下一个分区只能被一个消费组下面的一个消费者消费,这里就产生了分区分配的问题。

Kafka中提供了多重分区分配算法(PartitionAssignor)的实现:RangeAssignor、RoundRobinAssignor、StickyAssignor。

2.4.3.1 RangeAssignor

PartitionAssignor接口用于用户定义实现分区分配算法,以实现Consumer之间的分区分配。

消费组的成员订阅它们感兴趣的Topic并将这种订阅关系传递给作为订阅组协调者的Broker。协调者选择其中的一个消费者来执行这个消费组的分区分配并将分配结果转发给消费组内所有的消费者。Kafka默认采用RangeAssignor的分配算法。

RangeAssignor对每个Topic进行独立的分区分配。对于每一个Topic,首先对分区按照分区ID进行数值排序,然后订阅这个Topic的消费组的消费者再进行字典排序,之后尽量均衡的将分区分配给消费者。这里只能是尽量均衡,因为分区数可能无法被消费者数量整除,那么有一些消费者就会多分配到一些分区。

image-20220711105407989

大致算法如下:

assign(topic, consumers) {
    // 对分区和Consumer进行排序
    List<Partition> partitions = topic.getPartitions();
    sort(partitions);
    sort(consumers);
    // 计算每个Consumer分配的分区数
    int numPartitionsPerConsumer = partition.size() / consumers.size();
    // 额外有一些Consumer会多分配到分区
    int consumersWithExtraPartition = partition.size() % consumers.size();
    // 计算分配结果
    for (int i = 0, n = consumers.size(); i < n; i++) {
        // 第i个Consumer分配到的分区的index
        int start = numPartitionsPerConsumer * i + Math.min(i, consumersWithExtraPartition);
        // 第i个Consumer分配到的分区数
        int length = numPartitionsPerConsumer + (i + 1 > consumersWithExtraPartition ? 0 : 1);
        // 分装分配结果
        assignment.get(consumersForTopic.get(i)).addAll(partitions.subList(start, start + length));
    }
}

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

这种分配方式明显的一个问题是随着消费者订阅的Topic的数量的增加,不均衡的问题会越来越严重,比如上图中4个分区3个消费者的场景,C0会多分配一个分区。如果此时再订阅一个分区数为4的Topic,那么C0又会比C1、C2多分配一个分区,这样C0总共就比C1、C2多分配两个分区了,而且随着Topic的增加,这个情况会越来越严重。

字典序靠前的消费组中的消费者比较“贪婪”。

image-20220711105726278

2.4.3.2 RoundRobinAssignor

RoundRobinAssignor的分配策略是将消费组内订阅的所有Topic的分区及所有消费者进行排序后尽量均衡的分配(RangeAssignor是针对单个Topic的分区进行排序分配的)。如果消费组内,消费者订阅的Topic列表是相同的(每个消费者都订阅了相同的Topic),那么分配结果是尽量均衡的(消费者之间分配到的分区数的差值不会超过1)。如果订阅的Topic列表是不同的,那么分配结果是不保证“尽量均衡”的,因为某些消费者不参与一些Topic的分配。

image-20220711110019558

相对于RangeAssignor,在订阅多个Topic的情况下,RoundRobinAssignor的方式能消费者之间尽量均衡的分配到分区(分配到的分区数的差值不会超过1——RangeAssignor的分配策略可能随着订阅的Topic越来越多,差值越来越大)。

对于消费组内消费者订阅Topic不一致的情况:假设有两个消费者分别为C0和C1,有2个TopicT1、T2,分别拥有3和2个分区,并且C0订阅T1和T2,C1订阅T2,那么RoundRobinAssignor的分配结果如下:

image-20220711110154887

看上去分配已经尽量的保证均衡了,不过可以发现C0承担了4个分区的消费而C1订阅了T2一个分区,是不是把T2P0交给C1消费能更加的均衡呢?

2.4.3.3 StickyAssignor

尽管RoundRobinAssignor已经在RangeAssignor上做了一些优化来更均衡的分配分区,但是在一些情况下依旧会产生严重的分配偏差,比如消费组中订阅的Topic列表不相同的情况下。

更核心的问题是无论是RangeAssignor,还是RoundRobinAssignor,当前的分区分配算法都没有考虑上一次的分配结果。显然,在执行一次新的分配之前,如果能考虑到上一次分配的结果,尽量少的调整分区分配的变动,显然是能节省很多开销的。

StickyAssignor 的目标:

  1. 分区的分配尽量的均衡
  2. 每一次重分配的结果尽量与上一次分配结果保持一致

当这两个目标发生冲突时,优先保证第一个目标。第一个目标是每个分配算法都尽量尝试去完成的,而第二个目标才真正体现出StickyAssignor特性的。

我们先来看预期分配的结构,后续再具体分析StickyAssignor的算法实现。

例如:

  • 有3个Consumer:C0、C1、C2
  • 有4个Topic:T0、T1、T2、T3,每个Topic有2个分区
  • 所有Consumer都订阅了这4个分区

StickyAssignor的分配结果如下图所示(增加RoundRobinAssignor分配作为对比):

image-20220711110727157

如果消费者1宕机,则按照RoundRobin的方式分配结果如下:打乱从新来过,轮询分配:

image-20220711110834227

按照Sticky的方式:仅对消费者1分配的分区进行重分配,红线部分。最终达到均衡的目的。

image-20220711110857613

2.4.3.4 自定义分配策略

自定义的分配策略必须要实现org.apache.kafka.clients.consumer.internals.PartitionAssignor接口。PartitionAssignor接口的定义如下:

Subscription subscription(Set<String> topics);
String name();
Map<String, Assignment> assign(Cluster metadata, Map<String, Subscription> subscriptions);
void onAssignment(Assignment assignment);
class Subscription {
    private final List<String> topics;
    private final ByteBuffer userData;
}
class Assignment {
    private final List<TopicPartition> partitions;
    private final ByteBuffer userData;
}
posted @ 2022-07-11 11:18  Maple~  阅读(159)  评论(0编辑  收藏  举报