Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Table of Contents

Status

Current state: "Under DiscussionCanceled"

Discussion thread:  [Change the link from the KIP proposal email archive to your own email thread]

JIRA: KAFKA-18239

: https://lists.apache.org/thread/9l6m1scv765ohbwjkwq5sotjcdjzjp4q

Vote thread: https://lists.apache.org/thread/mlzvnnfx9jwv0v0lql1hocno4xxk7cr8

JIRA:

Jira
serverASF JIRA
serverId5aa69414-a9e9-3523-82ec-879b028fb15b
keyKAFKA-18239

Please keep the discussion Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).

Motivation

In ClientQuotaCallback#updateClusterMetadata, we pass a Cluster object to this method. The Cluster is an immutable object that holds a lot of information, and a new Cluster is created every time there is a change in metadata. For large clusters with many partitions, this can result in significant memory pressure on Kafka.

Additionally, some information in the Cluster object is unnecessary or confusing. For instance, Cluster#controller refers to a random broker node in a KRaft cluster, which is retained for backward compatibility as it represents the ZK controller. Furthermore, in ZK mode, we parse the listener using the listener name from the request. However, in KRaft mode, there is no listener name in the updateClusterMetadata path. As a result, the callback may receive multiple partition info entries for each listener name. (See KAFKA-19122).

To address these issues, this KIP propose deprecating the current method ClientQuotaCallback#updateClusterMetadata(Cluster) and reimplementing it in a more efficient way, providing clearer and more relevant cluster information.

Public Interfaces

This KIP introduces a new

...

interface ClientQuotaCallbackHandler which basically copy all methods in ClientQuotaCallback except the one we want to deprecate.

Code Block
languagejava
boolean updateClusterMetadata(Cluster cluster);

And add a new method to replace the old one, the parameter ClusterMetadata in the new method is a new interface, which will be described below.For backward compatibility, the new method

Code Block
languagejava
boolean updateClusterMetadata(ClusterMetadata clusterMetadata);

Here is the whole content of the new interface:) have a default implementation and trigger the old one.

Code Block
languagediffjava
titleAdd new method to ClientQuotaCallackHandlerNew interface ClientQuotaCallbackHandler.java
@@ -101,13 +114,82 @@ public interface ClientQuotaCallback extends Configurable {
ClientQuotaCallbackHandler {

    /**
     * Quota *callback Thisinvoked isto usefuldetermine ifthe quota computation takes partitions into account.
 metric tags to be applied for a request.
     * TopicsQuota thatlimits are beingassociated deletedwith willquota notmetrics beand includedall inclients `cluster`.
which use the    *same
+     * @deprecatedmetric pleasetags useshare {@link ClientQuotaCallback#updateClusterMetadata(ClusterMetadata)} instead
 the quota limit.
     *
 @param cluster Cluster metadata including* partitions@param andquotaType theirType leadersof ifquota knownrequested
      * @return@param trueprincipal ifThe quotasuser haveprincipal changedof andthe metricconnection configsfor maywhich needquota tois be updatedrequested
      */
+ @param clientId  @Deprecated
The client id associated with boolean updateClusterMetadata(Cluster cluster);

+the request
     /**
+ @return quota metric tags *that Thisindicate callbackwhich isother invokedclients whenevershare therethis arequota
 changes in the cluster metadata, such as
+ */
    Map<String, *String> brokers being added or removed, topics being created or deleted, or partition leadership updates.
+quotaMetricTags(ClientQuotaType quotaType, KafkaPrincipal principal, String clientId);

    /**
     * ThisReturns isthe usefulquota iflimit quotaassociated computationwith takesthe partitionsprovided intometric accounttags.
+ These tags were returned *from
 Topics that are being deleted* willa notprevious becall includedto in `cluster`.
+     *
+{@link #quotaMetricTags(ClientQuotaType, KafkaPrincipal, String)}. This method is
     * @paraminvoked clusterMetadataby quota Clustermanagers metadatato includingobtain partitionsthe andcurrent theirquota leaderslimit ifapplied known
+to a metric when the *first @returnrequest
 true if quotas have changed* andusing metricthese configstags mayis needprocessed. toIt beis updated
+also invoked after a quota */
+update or cluster metadata defaultchange.
 boolean updateClusterMetadata(ClusterMetadata clusterMetadata) {
+ * If the tags are no longer return updateClusterMetadata(toCluster(clusterMetadata));
+    }
+
     /**
 in use after the update, (e.g. this is a {user, client-id} quota metric
     * Closesand thisthe instance.
quota now in use is  */
     void close();
+
+a {user} quota), null is returned.
    static Cluster*
 toCluster(ClusterMetadata clusterMetadata) {
+  * @param quotaType  Type  // ... skip the implementation here ...
+    }

The ClusterMetadata used in ClientQuotaCallback#updateClusterMetadata is also a new interface. The goal of ClusterMetadata is:

  • Eliminate the Cluster object creation, allowing us to avoid the overhead of creating Cluster instances.

  • Provide key information from the Cluster object, such as brokers and partition data, along with helper methods that replicate the functionality of the original Cluster class while omitting unnecessary information (e.g., controller(), unauthorizedTopics(), isBootstrapConfigured()).
Code Block
languagejava
titleClusterMetadata.java
public interface ClusterMetadata {

    /**
     * Get a map of brokers.
     * The key is the broker node ID, and the value is the metadata
     * associated with that broker.of quota requested
     * @param metricTags Metric tags for a quota metric of type `quotaType`
     * @return the quota limit for the provided metric tags or null if the metric tags are no longer in use
     */
    Double quotaLimit(ClientQuotaType quotaType, Map<String, String> metricTags);

    /**
     * Quota configuration update callback that is invoked when quota configuration for an entity is
     */
 updated in the Map<Integer,quorum. BrokerMetadata> brokers();

    /**
     * Get specific broker information.This is useful to track configured quotas if built-in quota configuration
     */
 tools are used Optional<BrokerMetadata>for broker(int nodeId);

quota management.
     /**
     * Get@param partitionquotaType information  Type forof aquota specificbeing topic.updated
     * The@param keyquotaEntity isThe thequota partitionentity ID,for andwhich the valuequota is thebeing metadataupdated
     * associated@param withnewValue that partition.
  The new quota *value
 Return an empty map if*/
 the topic does not exist.
     */
    Map<Integer, PartitionMetadata> partitionsForTopic(String topicvoid updateQuota(ClientQuotaType quotaType, ClientQuotaEntity quotaEntity, double newValue);

    /**
     * GetQuota partitionconfiguration informationremoval forcallback athat specificis node.
invoked when quota configuration for *an The keyentity is
 the TopicPartition, and the value* removed isin the quorum. metadata
This is useful to track *configured quotas associatedif withbuilt-in thatquota partition.configuration
     * tools Returnare anused emptyfor mapquota ifmanagement.
 the node does not exist.*
     */
 @param quotaType  Map<TopicPartition, Type PartitionMetadata> partitionsForNode(int nodeId);

of quota being updated
     /**
     * Get specific partition information.* @param quotaEntity The quota entity for which quota is being updated
     */
    Optional<PartitionMetadata>void partition(TopicPartition topicPartitionremoveQuota(ClientQuotaType quotaType, ClientQuotaEntity quotaEntity);

    /**
     * GetReturns true aif mapany of topics.
the existing quota configs may *have Thebeen keyupdated issince the topiclast ID,call
 and the value is the* topicto name.
this method for the provided */
quota type. Quota updates Map<Uuid,as String> topics();

    /**a result of calls to
     * Get ClusterResource{@link #updateClusterMetadata(ClusterMetadata)}, which includes the cluster ID.{@link #updateQuota(ClientQuotaType, ClientQuotaEntity, double)}
     */
 and {@link  ClusterResource clusterResource();

    /**#removeQuota(ClientQuotaType, ClientQuotaEntity)} are automatically processed.
     * GivenSo partitioncallbacks metadata,that returnrely theonly subset of the replicas that are offline.on built-in quota configuration tools always return false. Quota callbacks
     * with Ifexternal therequota isconfiguration noor offlinecustom replicareconfigurable inquota theconfigs partition,that returnaffect anquota emptylimits array.must
     */
 return true if int[] offlineReplicas(PartitionMetadata partitionMetadata);
}

Unlike the Cluster#brokers using the org.apache.kafka.common.Node to present broker information.  ClusterMetadata use the new BrokerMetadata interface. The reason behind it is  ClientQuotaCallback#updateClusterMetadata does not filter by listener name like the old ZK-based path. Thus, using the existing Node is not appropriate since now we may have multiple listeners in the broker node. Additionally, DynamicTopicClusterQuotaPublisher can access BrokerRegistration, and we can simply add a new interface to BrokerRegistration which already includes the necessary information, making the ClusterMetadata implementation much more efficient.

Code Block
languagejava
titleBrokerMetadata.java
public interface BrokerMetadata {

    /*existing metric configs may need to be updated. This method is invoked on every request
     * and hence is expected to be handled by callbacks as a simple flag that is updated when quotas change.
     *
     * @param ThequotaType brokerType nodeof idquota
     */
    intboolean idquotaResetRequired(ClientQuotaType quotaType);

    /**
     * TheThis listenerscallback ofis theinvoked brokerwhenever node
there are changes in the */
    Map<Stringcluster metadata, Endpoint> listeners();

such as
     /**
     * Whether if this node is fenced* brokers being added or removed, topics being created or deleted, or partition leadership updates.
     */
 This is useful if boolean fenced();

    /**quota computation takes partitions into account.
     * TheTopics rackthat forare this nodebeing deleted will not be included.
     */
     * @param  Optional<String> rack();
}

PartitionMetadata is similar to BrokerMetadata. We can leverage the existing PartitionRegistration object to create a new interface that provides partition metadata efficiently, avoiding the need to convert PartitionRegistration into PartitionInfo.

The main difference is isAvailable(), as PartitionRegistration does not include this information. However, since Cluster#availablePartitionsForTopic allows users to query available partitions, providing a default method here for convenience.

Code Block
languagejava
titlePartitionMetadata.java
public interface PartitionMetadata {clusterMetadata Cluster metadata including partitions and their leaders if known
     * @return true if quotas have changed and metric configs may need to be updated
     */
    boolean updateClusterMetadata(ClusterMetadata clusterMetadata);

    /**
     * TheCloses nodethis IDinstance.
 of the node currently acting as the leader for this partition or -1 if there is no leader.
     */
    intvoid leaderclose();
}


For backward compatibility, the old interface ClientQuotaCallback will extend the new interface ClientQuotaCallbackHandler, and in the old interface we'll add a new method updateClusterMetadata(ClusterMetadata) have a default implementation and trigger the old one.

The toCluster method is a static method to covert the ClusterMetadata object to Cluster object.

Code Block
languagejava
titleold interface ClientQuotaCallback.java
/**
 * @deprecated, please use {@link ClientQuotaCallbackHandler} instead 
 */
@Deprecated
public interface ClientQuotaCallback extends ClientQuotaCallbackHandler {
    /**
     * The complete set of replicas for this partition, regardless of whether they are alive or up-to-date.
     */
    int[] replicas();

    /**
     * This Thecallback subsetis ofinvoked replicaswhenever thatthere are changes in sync,the meaningcluster theymetadata, are up-to-date and ready to take over as the leader if
     * the current leader failssuch as 
     * brokers being added or removed, topics being created or deleted, or partition leadership updates.
     */
    int[] inSyncReplicas();

 This is useful if quota computation takes partitions into account.
     /**
 Topics that are being *deleted Checkwill ifnot thebe partitionincluded isin available`cluster`.
     */
    default boolean isAvailable() {
 * @deprecated please use {@link ClientQuotaCallbackHandler#updateClusterMetadata(ClusterMetadata)} instead
     * @param cluster Cluster  return leader() != Node.noNode().id();
    }
}

Proposed Changes

In DynamicTopicClusterQuotaPublisher#onMetadataUpdate, invoke the new method introduced in this KIP.

Code Block
languagediff
titleDynamicTopicClusterQuotaPublisher#onMetadataUpdate
@@ -52,8 +51,8 @@ class DynamicTopicClusterQuotaPublisher (
     try {metadata including partitions and their leaders if known
     * @return true if quotas have changed and metric configs may need to be updated
       quotaManagers.clientQuotaCallbackPlugin().ifPresent(plugin => {
    */
    @Deprecated
    boolean updateClusterMetadata(Cluster cluster);

    default ifboolean updateClusterMetadata(delta.topicsDelta() != null || delta.clusterDelta() != null) {
-ClusterMetadata clusterMetadata) {
        return  val cluster = MetadataCache.toCluster(clusterId, newImage)
-          if (plugin.get().updateClusterMetadata(cluster)) {
+          val clusterMetadata = new DefaultClusterMetadata(clusterId, newImage)
+          if (plugin.get().updateClusterMetadata(clusterMetadata)) {
             quotaManagers.fetch.updateQuotaMetricConfigs()
             quotaManagers.produce.updateQuotaMetricConfigs()
             quotaManagers.request.updateQuotaMetricConfigs()

Make PartitionRegistration implement PartitionMetadata and BrokerRegistration implement BrokerMetadata. In DefaultClusterMetadata (the implementation of ClusterMetadata), we can use BrokerRegistration and PartitionRegistration to return the necessary information without duplicating data. This avoids the overhead of creating new objects.

For example, the implementation of ClusterMetadata#partitionsForTopic(String) simply wraps the map returned by TopicImage.partitions().

Code Block
languagejava
titleClusterMetadata#partitionsForTopic(String)
metadataImage.topics().getTopic(topic).partitions().entrySet().stream()
        .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));

The implementation for ClusterMetadata#partition(TopicPartition topicPartition) is similarly efficient.

Code Block
languagejava
titleClusterMetadata#partition(TopicPartition)
metadataImage.topics().getTopic(topicPartition.topic()).partitions().get(topicPartition.partition());

Other methods in ClusterMetadata will follow a similar pattern.

Compatibility, Deprecation, and Migration Plan

This KIP should be backward-compatible, as the deprecated method updateClusterMetadata(Cluster) is still supported and invoked in the new method.

However, users are expected to implement the new method ClientQuotaCallback#updateClusterMetadata(ClusterMetadata). The old one - ClientQuotaCallback#updateClusterMetadata(Cluster) will be removed in a future major release.

Test Plan

We will rely on the existing CustomQuotaCallbackTest.java to ensure the deprecated method still works.

Additionally, we will add some new tests in this test class to ensure the new method work as expected.

Rejected Alternatives

...

updateClusterMetadata(toCluster(clusterMetadata));
    }
}


The ClusterMetadata is also a new interface. The goal of ClusterMetadata is:

  • Eliminate the Cluster object creation, allowing us to avoid the overhead of creating Cluster instances.

  • Provide key information from the Cluster object, such as brokers and partition data, along with helper methods that replicate the functionality of the original Cluster class while omitting unnecessary information (e.g., controller(), unauthorizedTopics(), isBootstrapConfigured()).
Code Block
languagejava
titleClusterMetadata.java
public interface ClusterMetadata {      
    
    /**
     * Get a list of Kafka broker node IDs in the cluster.
     */
    List<Integer> brokerIds();

    /**
     * Get specific broker information.
     */
    Optional<BrokerMetadata> broker(int nodeId);

    /**
     * Get specific partition information.
     */
    Optional<PartitionMetadata> partition(TopicPartition topicPartition);

    /**
     * Get partition information of specific topic.
     * The key is the partition id, and the value is the metadata
     * associated with that partition. 
     * Return an empty map if topic does not exist.
     */
    Map<Integer, PartitionMetadata> partitions(String topic);

    /**
     * Get partition information of specific topic and specific node.
     * The key is the partition id, and the value is the metadata
     * associated with that partition.
     * Return an empty map if the topic or node id does not exist.
     */
    Map<Integer, PartitionMetadata> partitions(String topic, int nodeId);

    /**
     * Get a map of topics.
     * The key is the topic id, and the value is the topic name.
     */
    Map<Uuid, String> topics();

    /**
     * Get ClusterResource that includes cluster id
     */
    ClusterResource clusterResource();

    /**
     * Given a partition metadata, return the subset of the replicas that are offline.
     * Return an empty list if there is no offline replicas.
     */
    List<Integer> offlineReplicas(PartitionMetadata partitionMetadata);
}


Unlike the Cluster#brokers using the org.apache.kafka.common.Node to present broker information.  ClusterMetadata use the new BrokerMetadata interface. The reason behind it is  ClientQuotaCallback#updateClusterMetadata does not filter by listener name like the old ZK-based path. Thus, using the existing Node is not appropriate since now we may have multiple listeners in the broker node. Additionally, DynamicTopicClusterQuotaPublisher can access BrokerRegistration, and we can simply add a new interface to BrokerRegistration which already includes the necessary information, making the ClusterMetadata implementation much more efficient.

Code Block
languagejava
titleBrokerMetadata.java
public interface BrokerMetadata {

    /**
     * The broker node id
     */
    int id();

    /**
     * The listeners of the broker node.
     */
    Map<String, Endpoint> listeners();

    /**
     * Whether if this node is fenced
     */
    boolean fenced();

    /**
     * The rack for this node
     */
    Optional<String> rack();
}


PartitionMetadata is similar to BrokerMetadata. We can leverage the existing PartitionRegistration object to create a new interface that provides partition metadata efficiently, avoiding the need to convert PartitionRegistration into PartitionInfo.

The main difference is isAvailable(), as PartitionRegistration does not include this information. However, since Cluster#availablePartitionsForTopic allows users to query available partitions, providing a default method here for convenience.

Code Block
languagejava
titlePartitionMetadata.java
public interface PartitionMetadata {

    /**
     * The node id of the node currently acting as a leader for this partition or -1 if there is no leader
     */
    int leader();

    /**
     * The complete set of replicas for this partition regardless of whether they are alive or up-to-date
     */
    List<Integer> replicas();

    /**
     * The subset of the replicas that are in sync, that is caught-up to the leader and ready to take over as leader if
     * the leader should fail. Note that there is no guarantee the first element is leader, please use leader()
     * to get the leader of partition.
     */
    List<Integer> inSyncReplicas();

    /**
     * Check if the partition is available
     */
    default boolean isAvailable() {
       return leader() != Node.noNode().id();
    }
}

Proposed Changes

In DynamicTopicClusterQuotaPublisher#onMetadataUpdate, invoke the new method introduced in this KIP. And since now we are using the new interface, all ClientQuotaCallback invocation in the current code base (except the tests) should be replaced with  ClientQuotaCallbackHandler.

Code Block
languagediff
titleDynamicTopicClusterQuotaPublisher#onMetadataUpdate
@@ -52,8 +51,8 @@ class DynamicTopicClusterQuotaPublisher (
     try {
       quotaManagers.clientQuotaCallbackPlugin().ifPresent(plugin => {
         if (delta.topicsDelta() != null || delta.clusterDelta() != null) {
-          val cluster = MetadataCache.toCluster(clusterId, newImage)
-          if (plugin.get().updateClusterMetadata(cluster)) {
+          val clusterMetadata = new DefaultClusterMetadata(clusterId, newImage)
+          if (plugin.get().updateClusterMetadata(clusterMetadata)) {
             quotaManagers.fetch.updateQuotaMetricConfigs()
             quotaManagers.produce.updateQuotaMetricConfigs()
             quotaManagers.request.updateQuotaMetricConfigs()

Make PartitionRegistration implement PartitionMetadata and BrokerRegistration implement BrokerMetadata. In DefaultClusterMetadata (the implementation of ClusterMetadata), we can use BrokerRegistration and PartitionRegistration to return the necessary information without duplicating data. This avoids the overhead of creating new objects.

For example, the implementation of ClusterMetadata#partitions(String) simply wraps the map returned by TopicImage.partitions().

Code Block
languagejava
titleClusterMetadata#partitionsForTopic(String)
metadataImage.topics().getTopic(topic).partitions().entrySet().stream()
        .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));

The implementation for ClusterMetadata#partition(TopicPartition topicPartition) is similarly efficient.

Code Block
languagejava
titleClusterMetadata#partition(TopicPartition)
metadataImage.topics().getTopic(topicPartition.topic()).partitions().get(topicPartition.partition());

Other methods in ClusterMetadata will follow a similar pattern.

Compatibility, Deprecation, and Migration Plan

This KIP should be backward-compatible, as the deprecated method updateClusterMetadata(Cluster) is still supported and invoked in the new method.

However, users are expected to implement the new interface ClientQuotaCallbackHandler. The old one - ClientQuotaCallback will be removed in a future major release.

Test Plan

We will rely on the existing CustomQuotaCallbackTest.java to ensure the deprecated method still works.

Additionally, we will add some new tests in this test class to ensure the new method work as expected.

Rejected Alternatives

  • Add the new method updateClusterMetadata(ClusterMetadata) to the old interface ClientQuotaCallback, while doing this have a downside is that once we remove that deprecated function, it will break compilation for anyone who already implementing it.

    Code Block
    languagejava
    public interface ClientQuotaCallback extends Configurable {
    
        // ...skip ...//
     
        @Deprecated
        boolean updateClusterMetadata(Cluster cluster);
    
        default boolean updateClusterMetadata(ClusterMetadata clusterMetadata) {
            return updateClusterMetadata(toCluster(clusterMetadata));
        }
    }