Kafka Rebalance过程与机制-了解Kafka分区分配策略以及如何编写自己的自定义分配器

了解Kafka分区分配策略以及如何编写自己的自定义分配器

 
Apache Kafka Rebalance Protocol如何工作以及如何在内部使用。从Kafka使用者的角度来看,该协议既可用于协调属于同一组的成员,又可在其中分配主题分区所有权。

该协议的关键方面之一是,作为开发人员,我们可以嵌入自己的协议以自定义如何将分区分配给组成员。

在这篇文章中,我们将看到可以为Kafka Client Consumer配置哪些策略,以及如何编写PartitionAssignor实现故障转移策略的自定义。

 PartitionAssignor策略

创建新的Kafka使用者时,我们可以配置策略,该策略将用于在使用者实例之间分配分区。

可以通过属性配置分配策略partition.assignment.strategy 。

以下代码段说明了如何指定分区分配器:

Properties props = new Properties();
...
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, StickyAssignor.class.getName());
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
//...

属于同一组的所有使用者必须声明一个共同的策略。如果使用者尝试以与其他组成员不一致的分配配置加入一个组,您将最终遇到以下异常:

org.apache.kafka.common.errors.InconsistentGroupProtocolException: The group member’s supported protocols are incompatible with those of existing members or first group member tried to join with empty protocol type or empty protocol list.

此属性接受以逗号分隔的策略列表。例如,它允许您通过指定新策略来更新一组使用者,同时暂时保留前一个。作为重新平衡协议的一部分,经纪人协调员将选择所有成员都支持的协议。

策略只是实现接口的类的完全限定名称。 PartitionAssignor.

Kafka客户提供了三种内置策略:RangeRoundRobinStickyAssignor

范围分配器

RangeAssignor是默认的策略。此策略的目的是将几个主题的分区共定位。例如,这对于将来自具有相同分区数和相同键分区逻辑的两个主题的记录进行联接很有用。

为此,该策略将首先使用经纪人协调器分配的member_id将所有使用者按字典顺序排列。然后,它将按数字顺序放置可用的主题分区。最后,对于每个主题,从第一个使用者开始分配分区。

 

示例:RangeAssignor

如您所见,主题A和B的分区0被分配给同一使用者。

在该示例中,最多使用两个使用者,因为每个主题最多有两个分区。如果您打算从多个输入主题中进行消费,并且没有执行需要对分区进行共本地化的操作,则绝对不应使用默认策略。

RoundRobinAssignor

RoundRobinAssignor可用于所有成员平均分配可用分区。如前所述,分配器将在分配每个分区之前按字典顺序排列分区和使用者。

 

示例:RoundRobinAssignor

即使RoundRobin具有最大化使用用户数量的优势,它也有一个主要缺点。实际上,当用户数量改变时(即发生重新平衡时),它不会尝试减少分区移动。

为了说明这种行为,让我们从组中删除使用者2。在这种情况下,主题分区B-1从C1撤消,然后重新分配给C3。相反,主题分区B-0从C3撤消,重新分配给C1。

 

示例:具有重新分配的RoundRobinAssignor

例如,如果使用者在分区分配期间初始化内部缓存,打开资源或连接,则这种不必要的分区移动可能会影响使用者性能。

粘性分配器

StickyAssignor是相当类似ROUNDROBIN但它会尽量减低对两项工作之间的分配运动,同时确保均匀分布。

使用前面的示例,如果使用者C2离开组,则只有分区A-1分配更改为C3。

 

StreamsPartitionAssignor

Kafka Streams带有自己的StreamsPartitionAssignor。它用于在应用程序实例之间分配分区,同时确保它们的共本地化并维护活动和备用任务的状态。

通常,这三个基本分配器适用于大多数用例。但是,您可能具有特定的项目上下文或部署策略,要求您实施自己的策略。

为此,让我们看一下如何实现interface org.apache.kafka.clients.consumer.internals.PartitionAssignor

实施自定义策略

PartitionAssignor接口

PartitionAssignor与其说是复杂的,只包含四种主要方法。

public interface PartitionAssignor {

    Subscription subscription(Set<String> topics);

    Map<String, Assignment> assign(
                         Cluster metadata, 
                         Map<String, Subscription> subscriptions);

    void onAssignment(Assignment assignment);

    String name();
}

首先,subscription()在所有消费者上调用该方法,这些消费者负责创建将发送给代理协调器的订阅。甲订阅包含话题集合,消费者预订和可选地,可以由分配算法中使用某些用户数据。

然后,作为重新平衡协议的一部分,消费者组负责人将收到所有消费者的订阅,并将负责通过该方法执行分区分配assign()

接下来,所有使用者都将从领导者那里收到分配,onAssignment()方法将在每个使用者上被调用。消费者可以使用此方法来维护内部状态。

最后,PartitionAssignor 必须将a分配给该方法返回的唯一名称name()(例如“ range”或“ roundrobin”或“ sticky”)。

故障转移策略

使用默认分配器,可以将组中的所有使用者分配到分区。我们可以将此策略与主动/主动模型进行比较,这意味着所有实例都可能同时获取消息。但是,对于某些生产方案,可能需要执行主动/被动消耗。因此,我建议您实现FailoverAssignor,这实际上是在其他一些消息传递解决方案中可以找到的策略。

故障转移策略的基本思想是多个使用者可以加入同一组。但是,所有分区一次都分配给一个使用者。如果该使用者失败或被停止,则所有分区均分配给下一个可用使用者。通常,将分区分配给第一个使用者,但是在我们的示例中,我们将为每个实例附加一个优先级。因此,优先级最高的实例将优于其他实例。

让我们来说明一下这种策略。在下面的示例中,C1具有最高优先级,因此所有分区均已分配给它。

 

如果使用者失败,则所有分区均分配给下一个使用者(即C2)。

 

代码实现:

首先,让我们创建一个称为的新Java类FailoverAssignor代替实现接口PartitionAssignor,我们将扩展抽象类AbstractPartitionAssignor。此类已经实现了该assign(Cluster,Map<String, Subscription>)方法,并执行所有逻辑以获取每个预订的可用分区。它还声明了我们必须实现的以下抽象方法:

Map<String, List<TopicPartition>> assign(
                           Map<String, Integer> partitionsPerTopic,
                           Map<String, Subscription> subscriptions);

但是在执行此操作之前,我们需要使其可FailoverAssignor配置,以便可以为每个使用者分配优先级。幸运的是,Kafka提供了Configurable我们可以实现以检索客户端配置的接口。

到目前为止,完整的代码是这样的:

public class FailoverAssignor extends AbstractPartitionAssignor implements Configurable {

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

  @Override
  public void configure(final Map<String, ?> configs) { 
    // TODO
  }
 @Override
  public Subscription subscription(final Set<String> topics) { 
    // TODO
  }
  @Override
  Map<String, List<TopicPartition>> assign(
                         Map<String, Integer> partitionsPerTopic,
                         Map<String, Subscription> subscriptions)  {
  // TODO
  }
}

在上面的代码中,方法configure是在FailoverAssignor实例初始化之后立即调用的KafkaConsumer

为了遵循Kafka编码约定,我们将创建一个所谓的第二类,该类FailoverAssignorConfig将扩展公共类AbstractConfig

public class FailoverAssignorConfig extends AbstractConfig {

    public static final String CONSUMER_PRIORITY_CONFIG = "assignment.consumer.priority";
    public static final String CONSUMER_PRIORITY_DOC = "The priority attached to the consumer that must be used for assigning partition. " +
            "Available partitions for subscribed topics are assigned to the consumer with the highest priority within the group.";

    private static final ConfigDef CONFIG;

    static {
        CONFIG = new ConfigDef()
                .define(CONSUMER_PRIORITY_CONFIG,             ConfigDef.Type.INT, Integer.MAX_VALUE,
                        ConfigDef.Importance.HIGH, CONSUMER_PRIORITY_DOC);
    }

    public FailoverAssignorConfig(final Map<?, ?> originals) {
        super(CONFIG, originals);
    }

    public int priority() {
        return getInt(CONSUMER_PRIORITY_CONFIG);
    }
}

现在,该configure()方法可以简单地实现如下:

public void configure(final Map<String, ?> configs) {
    this.config = new FailoverAssignorConfig(configs);
}

然后,我们需要实现该subscription()方法,以便通过用户数据字段共享消费者优先级。注意,用户数据必须作为字节缓冲区传递。

@Override
public Subscription subscription(final Set<String> topics) {
    ByteBuffer userData = ByteBuffer.allocate(4)
      .putInt(config.priority())
      .flip();
    return new Subscription(
        new ArrayList<>(topics), 
        ByteBuffer.wrap(userData)
     );
}

接下来,我们可以实现该assign()方法:

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

// Generate all topic-partitions using the number 
/ of partitions for each subscribed topic.
final List<TopicPartition> assignments = partitionsPerTopic
    .entrySet()
    .stream()
    .flatMap(entry -> {
        final String topic = entry.getKey();
        final int numPartitions = entry.getValue();
        return IntStream.range(0, numPartitions)
            .mapToObj( i -> new TopicPartition(topic, i));
    }).collect(Collectors.toList());

// Decode consumer priority from each subscription and
Stream<ConsumerPriority> consumerOrdered = subscriptions.entrySet()
    .stream()
    .map(e -> {
        int priority = e.getValue().userData().getInt();
        String memberId = e.getKey();
        return new ConsumerPriority(memberId, priority);
    })
    .sorted(Comparator.reverseOrder());

// Select the consumer with the highest priority
ConsumerPriority priority = consumerOrdered.findFirst().get();

final Map<String, List<TopicPartition>> assign = new HashMap<>();
subscriptions.keySet().forEach(memberId -> assign.put(memberId, Collections.emptyList()));
assign.put(priority.memberId, assignments);
return assign;
}

最后,我们可以像这样使用自定义分区分配器:

roperties props = new Properties();
...
props.put(
    ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,   
    FailoverAssignor.class.getName()
);
props.put(FailoverAssignorConfig.CONSUMER_PRIORITY_CONFIG, "10");

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

结论

Kafka Clients允许您为使用者实施自己的分区分配策略。这对于适应特定的部署方案非常有用,例如本文中使用的故障转移示例。另外,可以利用在重新平衡期间将用户数据传输给消费者领导者的能力来实现更复杂和有状态的算法,例如为Kafka Stream开发的算法

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值