-
Notifications
You must be signed in to change notification settings - Fork 15.1k
KAFKA-2209 - Change quotas dynamically using DynamicConfigManager #298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c495e76
721e746
da73c0d
b6defc5
329a9ee
ded0497
e2b4daf
54d9e8c
3c3d950
5c3aa7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ | |
| */ | ||
| package kafka.server | ||
|
|
||
| import java.util.concurrent.{DelayQueue, TimeUnit} | ||
| import java.util.concurrent.{ConcurrentHashMap, DelayQueue, TimeUnit} | ||
|
|
||
| import kafka.utils.{ShutdownableThread, Logging} | ||
| import org.apache.kafka.common.MetricName | ||
|
|
@@ -36,27 +36,22 @@ private case class ClientSensors(quotaSensor: Sensor, throttleTimeSensor: Sensor | |
| /** | ||
| * Configuration settings for quota management | ||
| * @param quotaBytesPerSecondDefault The default bytes per second quota allocated to any client | ||
| * @param quotaBytesPerSecondOverrides The comma separated overrides per client. "c1=X,c2=Y" | ||
| * @param numQuotaSamples The number of samples to retain in memory | ||
| * @param quotaWindowSizeSeconds The time span of each sample | ||
| * | ||
| */ | ||
| case class ClientQuotaManagerConfig(quotaBytesPerSecondDefault: Long = | ||
| ClientQuotaManagerConfig.QuotaBytesPerSecondDefault, | ||
| quotaBytesPerSecondOverrides: String = | ||
| ClientQuotaManagerConfig.QuotaBytesPerSecondOverrides, | ||
| numQuotaSamples: Int = | ||
| ClientQuotaManagerConfig.DefaultNumQuotaSamples, | ||
| quotaWindowSizeSeconds: Int = | ||
| ClientQuotaManagerConfig.DefaultQuotaWindowSizeSeconds) | ||
|
|
||
| object ClientQuotaManagerConfig { | ||
| val QuotaBytesPerSecondDefault = Long.MaxValue | ||
| val QuotaBytesPerSecondOverrides = "" | ||
| // Always have 10 whole windows + 1 current window | ||
| val DefaultNumQuotaSamples = 11 | ||
| val DefaultQuotaWindowSizeSeconds = 1 | ||
| val MaxThrottleTimeSeconds = 30 | ||
| // Purge sensors after 1 hour of inactivity | ||
| val InactiveSensorExpirationTimeSeconds = 3600 | ||
| } | ||
|
|
@@ -73,8 +68,8 @@ class ClientQuotaManager(private val config: ClientQuotaManagerConfig, | |
| private val metrics: Metrics, | ||
| private val apiKey: String, | ||
| private val time: Time) extends Logging { | ||
| private val overriddenQuota = initQuotaMap(config.quotaBytesPerSecondOverrides) | ||
| private val defaultQuota = Quota.lessThan(config.quotaBytesPerSecondDefault) | ||
| private val overriddenQuota = new ConcurrentHashMap[String, Quota]() | ||
| private val defaultQuota = Quota.upperBound(config.quotaBytesPerSecondDefault) | ||
| private val lock = new ReentrantReadWriteLock() | ||
| private val delayQueue = new DelayQueue[ThrottledResponse]() | ||
| val throttledRequestReaper = new ThrottledRequestReaper(delayQueue) | ||
|
|
@@ -124,13 +119,12 @@ class ClientQuotaManager(private val config: ClientQuotaManagerConfig, | |
| // Compute the delay | ||
| val clientMetric = metrics.metrics().get(clientRateMetricName(clientId)) | ||
| throttleTimeMs = throttleTime(clientMetric, getQuotaMetricConfig(quota(clientId))) | ||
| clientSensors.throttleTimeSensor.record(throttleTimeMs) | ||
| delayQueue.add(new ThrottledResponse(time, throttleTimeMs, callback)) | ||
| delayQueueSensor.record() | ||
| // If delayed, add the element to the delayQueue | ||
| logger.debug("Quota violated for sensor (%s). Delay time: (%d)".format(clientSensors.quotaSensor.name(), throttleTimeMs)) | ||
| } | ||
| // If the request is not throttled, a throttleTime of 0 ms is recorded | ||
| clientSensors.throttleTimeSensor.record(throttleTimeMs) | ||
| throttleTimeMs | ||
| } | ||
|
|
||
|
|
@@ -160,10 +154,10 @@ class ClientQuotaManager(private val config: ClientQuotaManagerConfig, | |
| } | ||
|
|
||
| /** | ||
| * Returns the consumer quota for the specified clientId | ||
| * @return | ||
| * Returns the quota for the specified clientId | ||
| */ | ||
| private[server] def quota(clientId: String): Quota = overriddenQuota.getOrElse(clientId, defaultQuota) | ||
| def quota(clientId: String): Quota = | ||
| if (overriddenQuota.containsKey(clientId)) overriddenQuota.get(clientId) else defaultQuota; | ||
|
|
||
| /* | ||
| * This function either returns the sensors for a given client id or creates them if they don't exist | ||
|
|
@@ -172,8 +166,8 @@ class ClientQuotaManager(private val config: ClientQuotaManagerConfig, | |
| private def getOrCreateQuotaSensors(clientId: String): ClientSensors = { | ||
|
|
||
| // Names of the sensors to access | ||
| val quotaSensorName = apiKey + "-" + clientId | ||
| val throttleTimeSensorName = apiKey + "ThrottleTime-" + clientId | ||
| val quotaSensorName = getQuotaSensorName(clientId) | ||
| val throttleTimeSensorName = getThrottleTimeSensorName(clientId) | ||
| var quotaSensor: Sensor = null | ||
| var throttleTimeSensor: Sensor = null | ||
|
|
||
|
|
@@ -231,28 +225,49 @@ class ClientQuotaManager(private val config: ClientQuotaManagerConfig, | |
| ClientSensors(quotaSensor, throttleTimeSensor) | ||
| } | ||
|
|
||
| private def getThrottleTimeSensorName(clientId: String): String = apiKey + "ThrottleTime-" + clientId | ||
|
|
||
| private def getQuotaSensorName(clientId: String): String = apiKey + "-" + clientId | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A very minor comment here about style -- when we write one line function in scala in kafka, do we prefer to avoid "{}" and implement function in one line?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure we are very consistent about this but I have observed it both ways. I'll clean it up to make it a single line |
||
| private def getQuotaMetricConfig(quota: Quota): MetricConfig = { | ||
| new MetricConfig() | ||
| .timeWindow(config.quotaWindowSizeSeconds, TimeUnit.SECONDS) | ||
| .samples(config.numQuotaSamples) | ||
| .quota(quota) | ||
| } | ||
|
|
||
| /* Construct a Map of (clientId -> Quota) | ||
| * The input config is specified as a comma-separated K=V pairs | ||
| /** | ||
| * Overrides quotas per clientId | ||
| * @param clientId client to override | ||
| * @param quota custom quota to apply | ||
| */ | ||
| private def initQuotaMap(input: String): Map[String, Quota] = { | ||
| // If empty input, return an empty map | ||
| if (input.trim.length == 0) | ||
| Map[String, Quota]() | ||
| else | ||
| input.split(",").map(entry => { | ||
| val trimmedEntry = entry.trim | ||
| val pair: Array[String] = trimmedEntry.split("=") | ||
| if (pair.length != 2) | ||
| throw new IllegalArgumentException("Incorrectly formatted override entry (%s). Format is k1=v1,k2=v2".format(entry)) | ||
| pair(0) -> new Quota(pair(1).toDouble, true) | ||
| }).toMap | ||
| def updateQuota(clientId: String, quota: Quota) = { | ||
| /* | ||
| * Acquire the write lock to apply changes in the quota objects. | ||
| * This method changes the quota in the overriddenQuota map and applies the update on the actual KafkaMetric object (if it exists). | ||
| * If the KafkaMetric hasn't been created, the most recent value will be used from the overriddenQuota map. | ||
| * The write lock prevents quota update and creation at the same time. It also guards against concurrent quota change | ||
| * notifications | ||
| */ | ||
| lock.writeLock().lock() | ||
| try { | ||
| logger.info(s"Changing quota for clientId $clientId to ${quota.bound()}") | ||
|
|
||
| if (quota.equals(defaultQuota)) | ||
| this.overriddenQuota.remove(clientId) | ||
| else | ||
| this.overriddenQuota.put(clientId, quota) | ||
|
|
||
| // Change the underlying metric config if the sensor has been created | ||
| val allMetrics = metrics.metrics() | ||
| val quotaMetricName = clientRateMetricName(clientId) | ||
| if (allMetrics.containsKey(quotaMetricName)) { | ||
| logger.info(s"Sensor for clientId $clientId already exists. Changing quota to ${quota.bound()} in MetricConfig") | ||
| allMetrics.get(quotaMetricName).config(getQuotaMetricConfig(quota)) | ||
| } | ||
| } finally { | ||
| lock.writeLock().unlock() | ||
| } | ||
| } | ||
|
|
||
| private def clientRateMetricName(clientId: String): MetricName = { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove MaxThrottleTimeSeconds since it is not used?