-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-16446: Improve controller event duration logging #15622
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 13 commits
ec2ce56
238ce5a
af04828
c20ec56
0172e81
b71ab8b
c010cb4
23dff09
d23e4af
7676dd4
a2ded76
17acae0
8993f10
9323eef
dee71c4
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 |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.kafka.controller; | ||
|
|
||
| import org.apache.kafka.common.utils.LogContext; | ||
|
|
||
| import org.slf4j.Logger; | ||
|
|
||
| import java.text.DecimalFormat; | ||
| import java.util.AbstractMap; | ||
| import java.util.Map; | ||
|
|
||
| import static java.util.concurrent.TimeUnit.NANOSECONDS; | ||
| import static java.util.concurrent.TimeUnit.SECONDS; | ||
|
|
||
| /** | ||
| * Track the performance of controller events. Periodically log the slowest events. | ||
| * Log any event slower than a certain threshold. | ||
| */ | ||
| class EventPerformanceMonitor { | ||
| /** | ||
| * The format to use when displaying milliseconds. | ||
| */ | ||
| private static final DecimalFormat MILLISECOND_DECIMAL_FORMAT = new DecimalFormat("#0.00"); | ||
|
|
||
| static class Builder { | ||
| LogContext logContext = null; | ||
| long periodNs = SECONDS.toNanos(60); | ||
| long alwaysLogThresholdNs = SECONDS.toNanos(2); | ||
|
|
||
| Builder setLogContext(LogContext logContext) { | ||
| this.logContext = logContext; | ||
| return this; | ||
| } | ||
|
|
||
| Builder setPeriodNs(long periodNs) { | ||
| this.periodNs = periodNs; | ||
| return this; | ||
| } | ||
|
|
||
| Builder setAlwaysLogThresholdNs(long alwaysLogThresholdNs) { | ||
| this.alwaysLogThresholdNs = alwaysLogThresholdNs; | ||
| return this; | ||
| } | ||
|
|
||
| EventPerformanceMonitor build() { | ||
| if (logContext == null) logContext = new LogContext(); | ||
| return new EventPerformanceMonitor(logContext, | ||
| periodNs, | ||
| alwaysLogThresholdNs); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * The log4j object to use. | ||
| */ | ||
| private final Logger log; | ||
|
|
||
| /** | ||
| * The period in nanoseconds. | ||
| */ | ||
| private long periodNs; | ||
|
|
||
| /** | ||
| * The always-log threshold in nanoseconds. | ||
| */ | ||
| private long alwaysLogThresholdNs; | ||
|
|
||
| /** | ||
| * The name of the slowest event we've seen so far, or null if none has been seen. | ||
| */ | ||
| private String slowestEventName; | ||
|
|
||
| /** | ||
| * The duration of the slowest event we've seen so far, or 0 if none has been seen. | ||
| */ | ||
| private long slowestEventDurationNs; | ||
|
|
||
| /** | ||
| * The total duration of all the events we've seen. | ||
| */ | ||
| private long totalEventDurationNs; | ||
|
Member
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. Recording this is interesting. Since we have a single controller thread and can see its total event duration over a fixed period, we could calculate the idle percentage of the controller. Might be interesting to look at as a high level "busy" metric
Contributor
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. Yeah, that might be interesting. It would probably be better to get idle numbers directly from the queue somehow (since it knows how long it waited...) just to avoid errors adding up over time. |
||
|
|
||
| /** | ||
| * The number of events we've seen. | ||
| */ | ||
| private int numEvents; | ||
|
|
||
| private EventPerformanceMonitor( | ||
| LogContext logContext, | ||
| long periodNs, | ||
| long alwaysLogThresholdNs | ||
| ) { | ||
| this.log = logContext.logger(EventPerformanceMonitor.class); | ||
| this.periodNs = periodNs; | ||
| this.alwaysLogThresholdNs = alwaysLogThresholdNs; | ||
| reset(); | ||
| } | ||
|
|
||
| long periodNs() { | ||
| return periodNs; | ||
| } | ||
|
|
||
| Map.Entry<String, Long> slowestEvent() { | ||
| return new AbstractMap.SimpleImmutableEntry<>(slowestEventName, slowestEventDurationNs); | ||
| } | ||
|
|
||
| /** | ||
| * Reset all internal state. | ||
| */ | ||
| void reset() { | ||
| this.slowestEventName = null; | ||
| this.slowestEventDurationNs = 0; | ||
| this.totalEventDurationNs = 0; | ||
| this.numEvents = 0; | ||
| } | ||
|
|
||
| /** | ||
| * Handle a controller event being finished. | ||
| * | ||
| * @param name The name of the controller event. | ||
| * @param durationNs The duration of the controller event in nanoseconds. | ||
| */ | ||
| void observeEvent(String name, long durationNs) { | ||
| String message = doObserveEvent(name, durationNs); | ||
| if (message != null) { | ||
| log.error("{}", message); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handle a controller event being finished. | ||
| * | ||
| * @param name The name of the controller event. | ||
| * @param durationNs The duration of the controller event in nanoseconds. | ||
| * | ||
| * @return The message to log, or null otherwise. | ||
| */ | ||
| String doObserveEvent(String name, long durationNs) { | ||
| if (slowestEventName == null || slowestEventDurationNs < durationNs) { | ||
| slowestEventName = name; | ||
| slowestEventDurationNs = durationNs; | ||
| } | ||
| totalEventDurationNs += durationNs; | ||
| numEvents++; | ||
| if (durationNs < alwaysLogThresholdNs) { | ||
| return null; | ||
| } | ||
| return "Exceptionally slow controller event " + name + " took " + | ||
| NANOSECONDS.toMillis(durationNs) + " ms."; | ||
| } | ||
|
|
||
| /** | ||
| * Generate a log message summarizing the events of the last period, | ||
| * and then reset our internal state. | ||
| */ | ||
| void generatePeriodicPerformanceMessage() { | ||
| String message = periodicPerformanceMessage(); | ||
| log.info("{}", message); | ||
| reset(); | ||
| } | ||
|
|
||
| /** | ||
| * Generate a log message summarizing the events of the last period. | ||
| * | ||
| * @return The summary string. | ||
| */ | ||
| String periodicPerformanceMessage() { | ||
| StringBuilder bld = new StringBuilder(); | ||
| bld.append("In the last "); | ||
| bld.append(NANOSECONDS.toMillis(periodNs)); | ||
| bld.append(" ms period, "); | ||
| if (numEvents == 0) { | ||
| bld.append("there were no controller events completed."); | ||
| } else { | ||
| bld.append(numEvents).append(" controller events were completed, which took an average of "); | ||
| bld.append(nanosecondsToDecimalMillis(totalEventDurationNs / numEvents)); | ||
| bld.append(" ms each. The slowest event was ").append(slowestEventName); | ||
| bld.append(", which took "); | ||
| bld.append(nanosecondsToDecimalMillis(slowestEventDurationNs)); | ||
| bld.append(" ms."); | ||
| } | ||
| return bld.toString(); | ||
| } | ||
|
|
||
| /** | ||
| * Translate a duration in nanoseconds to a decimal duration in milliseconds. | ||
| * | ||
| * @param durationNs The duration in nanoseconds. | ||
| * @return The decimal duration in milliseconds. | ||
| */ | ||
| static String nanosecondsToDecimalMillis(long durationNs) { | ||
|
Member
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. naming: maybe "formatNanosAsMillis" or something with "format" in the name? |
||
| double number = NANOSECONDS.toMicros(durationNs); | ||
| number /= 1000; | ||
| return MILLISECOND_DECIMAL_FORMAT.format(number); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -175,16 +175,21 @@ | |
| */ | ||
| public final class QuorumController implements Controller { | ||
| /** | ||
| * The maximum records that the controller will write in a single batch. | ||
| * The default maximum records that the controller will write in a single batch. | ||
| */ | ||
| private static final int MAX_RECORDS_PER_BATCH = 10000; | ||
| private static final int DEFAULT_MAX_RECORDS_PER_BATCH = 10000; | ||
|
Comment on lines
-178
to
+180
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. Even though there are no code paths that change the value, within the scope of
Member
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 I understand your comment. Are you just agreeing with the change to |
||
|
|
||
| /** | ||
| * The default minimum event time that can be logged as a slow event. | ||
| */ | ||
| private static final int DEFAULT_MIN_SLOW_EVENT_TIME_MS = 200; | ||
|
|
||
| /** | ||
| * The maximum records any user-initiated operation is allowed to generate. | ||
| * | ||
| * For now, this is set to the maximum records in a single batch. | ||
| */ | ||
| static final int MAX_RECORDS_PER_USER_OP = MAX_RECORDS_PER_BATCH; | ||
| static final int MAX_RECORDS_PER_USER_OP = DEFAULT_MAX_RECORDS_PER_BATCH; | ||
|
|
||
| /** | ||
| * A builder class which creates the QuorumController. | ||
|
|
@@ -213,7 +218,9 @@ public static class Builder { | |
| private ConfigurationValidator configurationValidator = ConfigurationValidator.NO_OP; | ||
| private Map<String, Object> staticConfig = Collections.emptyMap(); | ||
| private BootstrapMetadata bootstrapMetadata = null; | ||
| private int maxRecordsPerBatch = MAX_RECORDS_PER_BATCH; | ||
| private int maxRecordsPerBatch = DEFAULT_MAX_RECORDS_PER_BATCH; | ||
| private long controllerPerformanceSamplePeriodMs = 60000L; | ||
| private long controllerPerformanceAlwaysLogThresholdMs = 2000L; | ||
| private DelegationTokenCache tokenCache; | ||
| private String tokenSecretKeyString; | ||
| private long delegationTokenMaxLifeMs; | ||
|
|
@@ -321,6 +328,16 @@ public Builder setMaxRecordsPerBatch(int maxRecordsPerBatch) { | |
| return this; | ||
| } | ||
|
|
||
| public Builder setControllerPerformanceSamplePeriodMs(long controllerPerformanceSamplePeriodMs) { | ||
| this.controllerPerformanceSamplePeriodMs = controllerPerformanceSamplePeriodMs; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder setControllerPerformanceAlwaysLogThresholdMs(long controllerPerformanceAlwaysLogThresholdMs) { | ||
| this.controllerPerformanceAlwaysLogThresholdMs = controllerPerformanceAlwaysLogThresholdMs; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder setCreateTopicPolicy(Optional<CreateTopicPolicy> createTopicPolicy) { | ||
| this.createTopicPolicy = createTopicPolicy; | ||
| return this; | ||
|
|
@@ -433,7 +450,9 @@ public QuorumController build() throws Exception { | |
| delegationTokenExpiryTimeMs, | ||
| delegationTokenExpiryCheckIntervalMs, | ||
| uncleanLeaderElectionCheckIntervalMs, | ||
| interBrokerListenerName | ||
| interBrokerListenerName, | ||
| controllerPerformanceSamplePeriodMs, | ||
| controllerPerformanceAlwaysLogThresholdMs | ||
| ); | ||
| } catch (Exception e) { | ||
| Utils.closeQuietly(queue, "event queue"); | ||
|
|
@@ -524,6 +543,7 @@ private void handleEventEnd(String name, long startProcessingTimeNs) { | |
| long deltaNs = endProcessingTime - startProcessingTimeNs; | ||
| log.debug("Processed {} in {} us", name, | ||
| MICROSECONDS.convert(deltaNs, NANOSECONDS)); | ||
| performanceMonitor.observeEvent(name, deltaNs); | ||
| controllerMetrics.updateEventQueueProcessingTime(NANOSECONDS.toMillis(deltaNs)); | ||
| } | ||
|
|
||
|
|
@@ -536,6 +556,8 @@ private Throwable handleEventException( | |
| if (startProcessingTimeNs.isPresent()) { | ||
| long endProcessingTime = time.nanoseconds(); | ||
| long deltaNs = endProcessingTime - startProcessingTimeNs.getAsLong(); | ||
| performanceMonitor.observeEvent(name, deltaNs); | ||
| controllerMetrics.updateEventQueueProcessingTime(NANOSECONDS.toMillis(deltaNs)); | ||
| deltaUs = OptionalLong.of(MICROSECONDS.convert(deltaNs, NANOSECONDS)); | ||
| } else { | ||
| deltaUs = OptionalLong.empty(); | ||
|
|
@@ -1446,6 +1468,11 @@ private void replay(ApiMessage message, Optional<OffsetAndEpoch> snapshotId, lon | |
| */ | ||
| private final RecordRedactor recordRedactor; | ||
|
|
||
| /** | ||
| * Monitors the performance of controller events and generates logs about it. | ||
| */ | ||
| private final EventPerformanceMonitor performanceMonitor; | ||
|
|
||
| private QuorumController( | ||
| FaultHandler nonFatalFaultHandler, | ||
| FaultHandler fatalFaultHandler, | ||
|
|
@@ -1477,7 +1504,9 @@ private QuorumController( | |
| long delegationTokenExpiryTimeMs, | ||
| long delegationTokenExpiryCheckIntervalMs, | ||
| long uncleanLeaderElectionCheckIntervalMs, | ||
| String interBrokerListenerName | ||
| String interBrokerListenerName, | ||
| long controllerPerformanceSamplePeriodMs, | ||
| long controllerPerformanceAlwaysLogThresholdMs | ||
| ) { | ||
| this.nonFatalFaultHandler = nonFatalFaultHandler; | ||
| this.fatalFaultHandler = fatalFaultHandler; | ||
|
|
@@ -1574,6 +1603,11 @@ private QuorumController( | |
| this.metaLogListener = new QuorumMetaLogListener(); | ||
| this.curClaimEpoch = -1; | ||
| this.recordRedactor = new RecordRedactor(configSchema); | ||
| this.performanceMonitor = new EventPerformanceMonitor.Builder(). | ||
| setLogContext(logContext). | ||
| setPeriodNs(TimeUnit.MILLISECONDS.toNanos(controllerPerformanceSamplePeriodMs)). | ||
| setAlwaysLogThresholdNs(TimeUnit.MILLISECONDS.toNanos(controllerPerformanceAlwaysLogThresholdMs)). | ||
| build(); | ||
| if (maxIdleIntervalNs.isPresent()) { | ||
| registerWriteNoOpRecord(maxIdleIntervalNs.getAsLong()); | ||
| } | ||
|
|
@@ -1587,7 +1621,7 @@ private QuorumController( | |
| } | ||
| registerElectUnclean(TimeUnit.MILLISECONDS.toNanos(uncleanLeaderElectionCheckIntervalMs)); | ||
| registerExpireDelegationTokens(MILLISECONDS.toNanos(delegationTokenExpiryCheckIntervalMs)); | ||
|
|
||
| registerGeneratePeriodicPerformanceMessage(); | ||
| // OffsetControlManager must be initialized last, because its constructor will take the | ||
| // initial in-memory snapshot of all extant timeline data structures. | ||
| this.offsetControl = new OffsetControlManager.Builder(). | ||
|
|
@@ -1597,7 +1631,6 @@ private QuorumController( | |
| setTime(time). | ||
| build(); | ||
| log.info("Creating new QuorumController with clusterId {}", clusterId); | ||
|
|
||
| this.raftClient.register(metaLogListener); | ||
| } | ||
|
|
||
|
|
@@ -1681,6 +1714,21 @@ private void registerElectUnclean(long checkIntervalNs) { | |
| EnumSet.of(PeriodicTaskFlag.VERBOSE))); | ||
| } | ||
|
|
||
| /** | ||
| * Register the generatePeriodicPerformanceMessage task. | ||
| * | ||
| * This task periodically logs some statistics about controller performance. | ||
| */ | ||
| private void registerGeneratePeriodicPerformanceMessage() { | ||
| periodicControl.registerTask(new PeriodicTask("generatePeriodicPerformanceMessage", | ||
| () -> { | ||
| performanceMonitor.generatePeriodicPerformanceMessage(); | ||
| return ControllerResult.of(Collections.emptyList(), false); | ||
| }, | ||
| performanceMonitor.periodNs(), | ||
| EnumSet.noneOf(PeriodicTaskFlag.class))); | ||
| } | ||
|
|
||
| /** | ||
| * Register the delegation token expiration task. | ||
| * | ||
|
|
||
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.
One reason I didn't go with a fixed threshold originally was that I was concerned about a negative feedback loop with a congested controller. For example, if all the events on the controller are above this threshold due to some bug or external influence, doing the extra logging just makes things worse.
However, thinking about it more, maybe the extra logging wouldn't be such a hit. If the controller is congested and the threshold is a few seconds, then the extra few ms to do logging wouldn't change the situation much.
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.
Right. Once you're at 2 seconds per event, another log message isn't going to change much. Also, I hope we're logging in less than a few ms, although with these logging libraries, ya never know...