Versions Compared

Key

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

...

Quota management via Admin Client has gone through a couple drafts of proposals (KIP-248, KIP-422). The common ulterior motive is to remove ZooKeeper from this interaction for a variety of reasons (KIP-500), notwithstanding validation, security, and future extensibility. The goal of this KIP is to provide minimal interface changes to achieve quota management via Admin Client by reusing existing config mechanisms, but also convey as much useful information as possible.

Public Interfaces + Proposed Changes

(1) Quotas should be modifiable via Admin::incrementalAlterConfigs()

The Admin signature would not change:

AlterConfigsResult incrementalAlterConfigs(Map<ConfigResource>, Collection<AlterConfigOp>>)

However, the ConfigResource object would have to be adapted to handle user, client, and user+client configurations. Therefore, enum values must be added for USER and CLIENT. However, this isn't sufficient for specifying a user+client configuration, so an additional USER_CLIENT type with a formatted name must be added, which can be delimited in the same way the path is formed in ZooKeeper today: sanitized-user/clients/sanitized-client. Note the user and client names have to be sanitized (escaped) since they're opaque, and therefore a special constructor should be added to facilitate this. [See Rejected Alternatives[1]]

The ConfigResource would be updated:

...

On the server side, incrementAlterConfigs would be updated to accept these new resource types, and would perform the expected configuration update to ZooKeeper.

ACLs would required WRITE at the cluster level.

(2) Quotas should be describable via Admin::describeConfigs()

The Admin signature would not change:

DescribeConfigsResult describeConfigs(Collection<ConfigResource> resources, DescribeConfigsOptions options);

One thing to address, however, is that user and client quotas can be complex in terms of determining which quotas are being applied. This is, in large part, due to the fact that there's 9 levels of configuration for a particular user. In typical use, it's not anticipated for all these to be used, however it's worth conveying this information for administrative purposes.

To relay which config a particular quota value comes from, the ConfigSource will have to differentiate from each configuration level: 

public class ConfigEntry {
    public enum ConfigSource {
        DYNAMIC_TOPIC_CONFIG, // dynamic topic config that is configured for a specific topic
        DYNAMIC_BROKER_LOGGER_CONFIG, // dynamic broker logger config that is configured for a specific broker
        DYNAMIC_BROKER_CONFIG, // dynamic broker config that is configured for a specific broker
        DYNAMIC_DEFAULT_BROKER_CONFIG, // dynamic broker config that is configured as default for all brokers in the cluster
        STATIC_BROKER_CONFIG, // static broker config provided as broker properties at start up (e.g. server.properties file)
        DEFAULT_CONFIG, // built-in default configuration for configs that have a default value
        UNKNOWN, // source unknown e.g. in the ConfigEntry used for alter requests where source is not set

        // Dynamic user/client configurations, in order of descending precedence.
        DYNAMIC_USER_CLIENT_CONFIG,                 // /config/users/<user>/clients/<client-id>
        DYNAMIC_USER_DEFAULT_CLIENT_CONFIG,         // /config/users/<user>/clients/<default>
        DYNAMIC_USER_CONFIG,                        // /config/users/<user>
        DYNAMIC_DEFAULT_USER_CLIENT_CONFIG,         // /config/users/<default>/clients/<client-id>
        DYNAMIC_DEFAULT_USER_DEFAULT_CLIENT_CONFIG, // /config/users/<default>/clients/<default>
        DYNAMIC_DEFAULT_USER_CONFIG,                // /config/users/<default>
        DYNAMIC_CLIENT_CONFIG,                      // /config/clients/<client-id>
        DYNAMIC_DEFAULT_CLIENT_CONFIG               // /config/clients/<default>
    }
}

ACLs would required READ at the cluster level.

(3) Quotas should be administrated via ConfigCommand (kafka-configs.sh) using --boostrap-server (non-ZK)

...

While improvements have been made to the Admin interface for configuration handling, fitting quotas into the API would be restrictive because they don't fit the natural key-value pairing. Therefore, it'd be beneficial to have a quota-native API for administrating quotas, which would offer a more direct and less error-prone interface, convey additional useful information beyond what a configuration could provide, and provide for future extensibility as quotas types are added or evolve.

Background

Quotas are defined in terms of a user and client ID, where the user acts as a specific opaque principal, and the client ID as a more generic group identifier.

When setting quotas, an administrator has flexibility in how it specifies the user and client ID for which the quota applies to, where the user and client ID may be named, indicated as the default, or omitted entirely. This creates a hierarchy structure for which quotas apply, where the most specific, defined quota will be matched to a request's user and client ID.

As represented by the current ZK node structure, the order in which quotas are matched are as follows. Note <user> is a specified user name, <client-id> is a specified client ID, and <default> is a special default user/client ID represented as the literal string "<default>".

        /config/users/<user>/clients/<client-id>
        /config/users/<user>/clients/<default>
        /config/users/<user>
        /config/users/<default>/clients/<client-id>
        /config/users/<default>/clients/<default>
        /config/users/<default>
        /config/clients/<client-id>
        /config/clients/<default>

As such, reasoning around quotas can be complex, as it's not immediately obvious which quotas may apply to a given user and/or client ID. Providing descriptive information is a goal of this KIP.

Public Interfaces


class QuotaFilter {
    public static final String DEFAULT_USER;
    public static final String DEFAULT_CLIENT_ID;
    public static final String ANY_USER;
    public static final String ANY_CLIENT_ID;

    String user;
    String clientId;
}


/**
 * The quota types.
 */
public enum QuotaType {
    CONSUMER_BYTE_RATE,
    PRODUCER_BYTE_RATE,
    REQUEST_PERCENTAGE,
}

/**
 * The quota configuration for a specific user and client ID.
 */
public class QuotaConfig {

    /**
     * Where in the hierarchy the quota value was specified.
     */
    public enum Source {
        USER_CLIENT,
        USER_DEFAULT_CLIENT,
        USER,
        DEFAULT_USER_CLIENT,
        DEFAULT_USER_DEFAULT_CLIENT,
        DEFAULT_USER,
        CLIENT,
        DEFAULT_CLIENT,
        DEFAULT,
    }

    /**
     * Information about a quota value.
     */
    public class Value {
        // The source for the value.
        private final Source source;

        // The non-null quota's value.
        private final Double value;

        // Basic constructor/getters.
        public Value(Source source, Double value);
        public Source source();
        public Double value();
    }

    /**
     * Information about the value for a quota type.
     */
    public class Entry {
        // The active quota value.
        private final Value value;

        // List of all values lower that are ignored due to being lower in the hierarchy.
        private final List<Value> overriddenValues;

        // Basic constructor/getters.
        public Entry(Value value, List<Value> overriddenValues);
        public Value value();
        public List<Value> overriddenValues();
    }

    /**
     * Maps quota type to its configuration entry.
     *
     * Note that, depending upon the applied quota filter, this config may not map every
     * quota type to an entry. If a key is not contained in the map, then that quota type's value
     * is not specified.
     */
    public Map<QuotaType, Entry> config();
}

/**
 * Identifies a quota entity for a given user and client ID. If the provided user or client ID is
 * `Optional.empty()`, then the default name is used, otherwise if null, then the name is omitted
 * entirely. At least one of user or client ID must be specified.
 *
 * For example:
 *
 * {user="kafka-user", clientId="kafka-client-id"} corresponds to quota source `USER_CLIENT` with
 * specified user "kafka-user" and client ID "kafka-client-id".
 *
 *  {user="kafka-user", clientId=Optional.empty()} corresponds to quota source `USER_DEFAULT_CLIENT`
 * with specified user "kafka-user" and default client ID.
 *
 * {user=null, clientId="kafka-client-id"`} corresponds to quota source `CLIENT` for client with
 * ID "kafka-client-id".
 */
class QuotaEntity {
    QuotaEntity(Optional<String> user, Optional<String> clientId);
}

class DescribeQuotasOptions {
    // Whether to include the overridden values for every quota type in the result.
    boolean wantOverriddenValues;
}

class DescribeQuotasResult {
    Map<QuotaEntity, QuotaConfig> configs;
}

/**
 * Describes the quotas for the provided filters.
 */
DescribeQuotasResult describeQuotas(Collection<QuotaFilter> quotaFilters);

class QuotaAlteration {
    // The quota type to alter.
    QuotaType type;

    // The new value for the quota, otherwise if null, then the existing value is cleared.
    Double value;
}

class AlterQuotasOptions {
    // Default.
}

class AlterQuotasResult {
    // Maps a quota entity update to its future.
    private final Map<QuotaEntity, KafkaFuture<Void>> futures;
}

/**
 * Alters the quotas as specified for the provided quota entities.
 *
 * @param alterations the alterations to perform
 *  @return the result of the alterations
 */
AlterQuotasResult alterQuotas(Map<QuotaEntity, QuotaAlteration> alterations);



Proposed Changes

Compatibility, Deprecation, and Migration Plan

All changes would be forward-compatible, and no migration plan is necessary. It's outside the scope of this KIP to deprecate any functionality.

Rejected Alternatives

[1] An alternative to a USER_CLIENT resource type is to add a child ConfigResource field to the ConfigResource object, where the only valid resource relationship would be a parent type USER with a child type CLIENT. This has two pitfalls: (1) it's more complex, and a generic parent-child resource hierarchy isn't like to be useful in the long term, and (2) it requires modification to the wire protocol.

...