Skip to content

Commit c57acec

Browse files
author
Tirtha Chatterjee
committed
Log the sizes allocated through the SimpleMemoryPool for analysis.
1 parent 4de9f63 commit c57acec

9 files changed

Lines changed: 185 additions & 10 deletions

File tree

clients/src/main/java/org/apache/kafka/common/memory/GarbageCollectedMemoryPool.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.kafka.common.memory;
1818

19+
import java.util.Optional;
1920
import org.apache.kafka.common.metrics.Sensor;
2021
import org.apache.kafka.common.utils.Utils;
2122

@@ -42,7 +43,7 @@ public class GarbageCollectedMemoryPool extends SimpleMemoryPool implements Auto
4243
private volatile boolean alive = true;
4344

4445
public GarbageCollectedMemoryPool(long sizeBytes, int maxSingleAllocationSize, boolean strict, Sensor oomPeriodSensor) {
45-
super(sizeBytes, maxSingleAllocationSize, strict, oomPeriodSensor, null);
46+
super(sizeBytes, maxSingleAllocationSize, strict, oomPeriodSensor, null, Optional.empty());
4647
this.alive = true;
4748
this.gcListenerThread = new Thread(gcListener, "memory pool GC listener");
4849
this.gcListenerThread.setDaemon(true); //so we dont need to worry about shutdown
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package org.apache.kafka.common.memory;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.concurrent.atomic.AtomicInteger;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
10+
public class MemoryPoolStatsStore {
11+
private static final Logger log = LoggerFactory.getLogger(MemoryPoolStatsStore.class);
12+
13+
private final AtomicInteger[] histogram;
14+
private final int maxSizeBytes;
15+
private final int segmentSizeBytes;
16+
17+
public static class Range {
18+
public final int startInclusive;
19+
public final int endInclusive;
20+
21+
public Range(int startInclusive, int endInclusive) {
22+
this.startInclusive = startInclusive;
23+
this.endInclusive = endInclusive;
24+
}
25+
26+
@Override
27+
public String toString() {
28+
return "Range{" + "startInclusive=" + startInclusive + ", endInclusive=" + endInclusive + '}';
29+
}
30+
}
31+
32+
public MemoryPoolStatsStore(int segments, int maxSizeBytes) {
33+
histogram = new AtomicInteger[segments];
34+
this.maxSizeBytes = maxSizeBytes;
35+
segmentSizeBytes = (int) Math.ceil((double) maxSizeBytes / segments);
36+
for (int segmentIndex = 0; segmentIndex < segments; segmentIndex++) {
37+
histogram[segmentIndex] = new AtomicInteger();
38+
}
39+
}
40+
41+
private int getSegmentIndexForBytes(int bytes) {
42+
if (bytes == 0) {
43+
throw new IllegalArgumentException("Requested zero bytes for allocation.");
44+
}
45+
if (bytes > maxSizeBytes) {
46+
log.debug("Requested bytes {} for allocation exceeds maximum recorded value {}", bytes, maxSizeBytes);
47+
return -1;
48+
} else {
49+
return (bytes - 1) / segmentSizeBytes;
50+
}
51+
}
52+
53+
public void recordAllocation(int bytes) {
54+
try {
55+
final int segmentIndex = getSegmentIndexForBytes(bytes);
56+
if (segmentIndex != -1) {
57+
histogram[segmentIndex].incrementAndGet();
58+
}
59+
} catch (IllegalArgumentException e) {
60+
log.error("Encountered error when trying to record memory allocation for request", e);
61+
}
62+
}
63+
64+
public synchronized Map<Range, Integer> getFrequencies() {
65+
Map<Range, Integer> frequenciesMap = new HashMap<>();
66+
for (int segmentIndex = 0; segmentIndex < histogram.length; segmentIndex++) {
67+
frequenciesMap.put(new Range(
68+
segmentIndex * segmentSizeBytes + 1,
69+
segmentIndex * segmentSizeBytes + segmentSizeBytes
70+
), histogram[segmentIndex].intValue());
71+
}
72+
return frequenciesMap;
73+
}
74+
75+
public synchronized void clear() {
76+
for (AtomicInteger atomicInteger : histogram) {
77+
atomicInteger.set(0);
78+
}
79+
}
80+
}

clients/src/main/java/org/apache/kafka/common/memory/SimpleMemoryPool.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.apache.kafka.common.memory;
1818

1919
import java.nio.ByteBuffer;
20+
import java.util.Optional;
2021
import java.util.concurrent.atomic.AtomicLong;
2122

2223
import org.apache.kafka.common.metrics.Sensor;
@@ -37,10 +38,13 @@ public class SimpleMemoryPool implements MemoryPool {
3738
protected final AtomicLong availableMemory;
3839
protected final int maxSingleAllocationSize;
3940
protected final AtomicLong startOfNoMemPeriod = new AtomicLong(); //nanoseconds
41+
private final Optional<MemoryPoolStatsStore> memoryPoolStatsStore;
4042
protected volatile Sensor oomTimeSensor;
4143
protected volatile Sensor allocateSensor;
4244

43-
public SimpleMemoryPool(long sizeInBytes, int maxSingleAllocationBytes, boolean strict, Sensor oomPeriodSensor, Sensor allocateSensor) {
45+
public SimpleMemoryPool(long sizeInBytes, int maxSingleAllocationBytes, boolean strict, Sensor oomPeriodSensor,
46+
Sensor allocateSensor, Optional<MemoryPoolStatsStore> memoryPoolStatsStore) {
47+
this.memoryPoolStatsStore = memoryPoolStatsStore;
4448
if (sizeInBytes <= 0 || maxSingleAllocationBytes <= 0 || maxSingleAllocationBytes > sizeInBytes)
4549
throw new IllegalArgumentException("must provide a positive size and max single allocation size smaller than size."
4650
+ "provided " + sizeInBytes + " and " + maxSingleAllocationBytes + " respectively");
@@ -57,7 +61,8 @@ public ByteBuffer tryAllocate(int sizeBytes) {
5761
if (sizeBytes < 1)
5862
throw new IllegalArgumentException("requested size " + sizeBytes + "<=0");
5963
if (sizeBytes > maxSingleAllocationSize)
60-
throw new IllegalArgumentException("requested size " + sizeBytes + " is larger than maxSingleAllocationSize " + maxSingleAllocationSize);
64+
throw new IllegalArgumentException(
65+
"requested size " + sizeBytes + " is larger than maxSingleAllocationSize " + maxSingleAllocationSize);
6166

6267
long available;
6368
boolean success = false;
@@ -114,6 +119,7 @@ public boolean isOutOfMemory() {
114119
//allows subclasses to do their own bookkeeping (and validation) _before_ memory is returned to client code.
115120
protected void bufferToBeReturned(ByteBuffer justAllocated) {
116121
this.allocateSensor.record(justAllocated.capacity());
122+
memoryPoolStatsStore.ifPresent(sizeStore -> sizeStore.recordAllocation(justAllocated.capacity()));
117123
log.trace("allocated buffer of size {} ", justAllocated.capacity());
118124
}
119125

clients/src/test/java/org/apache/kafka/common/network/SelectorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ public void testPartialReceiveGracefulClose() throws Exception {
692692
public void testMuteOnOOM() throws Exception {
693693
//clean up default selector, replace it with one that uses a finite mem pool
694694
selector.close();
695-
MemoryPool pool = new SimpleMemoryPool(900, 900, false, null, sensor);
695+
MemoryPool pool = new SimpleMemoryPool(900, 900, false, null, sensor, Optional.empty());
696696
selector = new Selector(NetworkReceive.UNLIMITED, 5000, metrics, time, "MetricGroup",
697697
new HashMap<String, String>(), true, false, channelBuilder, pool, new LogContext());
698698

clients/src/test/java/org/apache/kafka/common/network/SslSelectorTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.apache.kafka.common.network;
1818

1919
import java.nio.channels.SelectionKey;
20+
import java.util.Optional;
2021
import javax.net.ssl.SSLEngine;
2122

2223
import org.apache.kafka.common.config.SecurityConfig;
@@ -288,7 +289,7 @@ public void testRenegotiationFails() throws Exception {
288289
public void testMuteOnOOM() throws Exception {
289290
//clean up default selector, replace it with one that uses a finite mem pool
290291
selector.close();
291-
MemoryPool pool = new SimpleMemoryPool(900, 900, false, null, sensor);
292+
MemoryPool pool = new SimpleMemoryPool(900, 900, false, null, sensor, Optional.empty());
292293
//the initial channel builder is for clients, we need a server one
293294
String tlsProtocol = "TLSv1.2";
294295
File trustStoreFile = File.createTempFile("truststore", ".jks");

core/src/main/scala/kafka/network/SocketServer.scala

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import kafka.utils._
3838
import org.apache.kafka.common.config.ConfigException
3939
import org.apache.kafka.common.config.internals.QuotaConfigs
4040
import org.apache.kafka.common.errors.InvalidRequestException
41-
import org.apache.kafka.common.memory.{MemoryPool, RecyclingMemoryPool, SimpleMemoryPool}
41+
import org.apache.kafka.common.memory.{MemoryPool, RecyclingMemoryPool, MemoryPoolStatsStore, SimpleMemoryPool}
4242
import org.apache.kafka.common.metrics._
4343
import org.apache.kafka.common.metrics.stats.Percentiles.BucketSizing
4444
import org.apache.kafka.common.metrics.stats.{Avg, CumulativeSum, Max, Meter, Percentile, Percentiles, Rate}
@@ -79,9 +79,19 @@ class SocketServer(val config: KafkaConfig,
7979
val time: Time,
8080
val credentialProvider: CredentialProvider,
8181
val observer: Observer,
82-
val apiVersionManager: ApiVersionManager)
82+
val apiVersionManager: ApiVersionManager,
83+
val memoryPoolStatsStore: Optional[MemoryPoolStatsStore])
8384
extends Logging with KafkaMetricsGroup with BrokerReconfigurable {
8485

86+
def this(config: KafkaConfig,
87+
metrics: Metrics,
88+
time: Time,
89+
credentialProvider: CredentialProvider,
90+
observer: Observer,
91+
apiVersionManager: ApiVersionManager) {
92+
this(config, metrics, time, credentialProvider, observer, apiVersionManager, Optional.empty())
93+
}
94+
8595
private val maxQueuedRequests = config.queuedMaxRequests
8696

8797
private val nodeId = config.brokerId
@@ -100,7 +110,7 @@ class SocketServer(val config: KafkaConfig,
100110
private val percentiles = (1 to 9).map( i => new Percentile(metrics.metricName("MemoryPoolAllocateSize%dPercentile".format(i * 10), MetricsGroup), i * 10))
101111
// At current stage, we do not know the max decrypted request size, temporarily set it to 10MB.
102112
memoryPoolAllocationSensor.add(new Percentiles(400, 0.0, 10485760, BucketSizing.CONSTANT, percentiles:_*))
103-
private val memoryPool = if (config.queuedMaxBytes > 0) new SimpleMemoryPool(config.queuedMaxBytes, config.socketRequestMaxBytes, false, memoryPoolUsageSensor, memoryPoolAllocationSensor)
113+
private val memoryPool = if (config.queuedMaxBytes > 0) new SimpleMemoryPool(config.queuedMaxBytes, config.socketRequestMaxBytes, false, memoryPoolUsageSensor, memoryPoolAllocationSensor, memoryPoolStatsStore)
104114
else if (config.socketRequestCommonBytes > 0) new RecyclingMemoryPool(config.socketRequestCommonBytes, config.socketRequestBufferCacheSize, memoryPoolAllocationSensor)
105115
else MemoryPool.NONE
106116
// data-plane

core/src/main/scala/kafka/server/KafkaConfig.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ object Defaults {
8484
val AllowPreferredControllerFallback = true
8585
val MissingPerTopicConfig = "-1"
8686

87+
val MemoryPoolStatsLoggingEnable = false
88+
val MemoryPoolStatsMaxSize = Integer.MAX_VALUE
89+
val MemoryPoolStatsNumSegments = 1000
90+
val MemoryPoolStatsLoggingFrequencyMinutes = 60
91+
8792
val UnofficialClientLoggingEnable = false
8893
val UnofficialClientCacheTtl = 1
8994
val ExpectedClientSoftwareNames = util.Arrays.asList(
@@ -441,6 +446,10 @@ object KafkaConfig {
441446
val UnofficialClientLoggingEnableProp = "unofficial.client.logging.enable"
442447
val UnofficialClientCacheTtlProp = "unofficial.client.cache.ttl"
443448
val ExpectedClientSoftwareNamesProp = "expected.client.software.names"
449+
val MemoryPoolStatsLoggingEnableProp = "memory.pool.stats.logging.enable"
450+
val MemoryPoolStatsMaxSizeProp = "memory.pool.stats.max.size"
451+
val MemoryPoolStatsNumSegmentsProp = "memory.pool.stats.num.segments"
452+
val MemoryPoolStatsLoggingFrequencyMinutesProp = "memory.pool.stats.logging.frequency.minutes"
444453

445454
/************* Authorizer Configuration ***********/
446455
val AuthorizerClassNameProp = "authorizer.class.name"
@@ -780,6 +789,10 @@ object KafkaConfig {
780789
val UnofficialClientLoggingEnableDoc = "Controls whether logging occurs when an ApiVersionsRequest is received from a client unsupported by LinkedIn, such as an Apache Kafka client."
781790
val UnofficialClientCacheTtlDoc = "The amount of time (in hours) for the identity of an unofficial client to live in the local cache to avoid duplicate log messages."
782791
val ExpectedClientSoftwareNamesDoc = "The software names of clients that are supported by LinkedIn, such as Avro, Raw, and Tracking clients."
792+
val MemoryPoolStatsLoggingEnableDoc = "Specifies whether memory pool statistics should be logged."
793+
val MemoryPoolStatsMaxSizeDoc = "Maximum size of memory allocation which will be recorded if memory pool statistics logging is enabled."
794+
val MemoryPoolStatsNumSegmentsDoc = "The number of segments into which the memory pool statistics histogram will be broken."
795+
val MemoryPoolStatsLoggingFrequencyMinutesDoc = "The frequency in minutes at which memory pool statistics will be recorded."
783796

784797
/************* Authorizer Configuration ***********/
785798
val AuthorizerClassNameDoc = s"The fully qualified name of a class that implements s${classOf[Authorizer].getName}" +
@@ -1214,6 +1227,10 @@ object KafkaConfig {
12141227
.define(UnofficialClientLoggingEnableProp, BOOLEAN, Defaults.UnofficialClientLoggingEnable, LOW, UnofficialClientLoggingEnableDoc)
12151228
.define(UnofficialClientCacheTtlProp, LONG, Defaults.UnofficialClientCacheTtl, LOW, UnofficialClientCacheTtlDoc)
12161229
.define(ExpectedClientSoftwareNamesProp, LIST, Defaults.ExpectedClientSoftwareNames, LOW, ExpectedClientSoftwareNamesDoc)
1230+
.define(MemoryPoolStatsLoggingEnableProp, BOOLEAN, Defaults.MemoryPoolStatsLoggingEnable, LOW, MemoryPoolStatsLoggingEnableDoc)
1231+
.define(MemoryPoolStatsMaxSizeProp, INT, Defaults.MemoryPoolStatsMaxSize, LOW, MemoryPoolStatsMaxSizeDoc)
1232+
.define(MemoryPoolStatsNumSegmentsProp, INT, Defaults.MemoryPoolStatsNumSegments, LOW, MemoryPoolStatsNumSegmentsDoc)
1233+
.define(MemoryPoolStatsLoggingFrequencyMinutesProp, INT, Defaults.MemoryPoolStatsLoggingFrequencyMinutes, LOW, MemoryPoolStatsLoggingFrequencyMinutesDoc)
12171234

12181235
/************* Authorizer Configuration ***********/
12191236
.define(AuthorizerClassNameProp, STRING, Defaults.AuthorizerClassName, LOW, AuthorizerClassNameDoc)
@@ -1723,6 +1740,11 @@ class KafkaConfig(val props: java.util.Map[_, _], doLog: Boolean, dynamicConfigO
17231740
def unofficialClientCacheTtl = getLong(KafkaConfig.UnofficialClientCacheTtlProp)
17241741
def expectedClientSoftwareNames = getList(KafkaConfig.ExpectedClientSoftwareNamesProp)
17251742

1743+
def memoryPoolStatsLoggingEnable = getBoolean(KafkaConfig.MemoryPoolStatsLoggingEnableProp)
1744+
def memoryPoolStatsMaxSize = getInt(KafkaConfig.MemoryPoolStatsMaxSizeProp)
1745+
def memoryPoolStatsNumSegments = getInt(KafkaConfig.MemoryPoolStatsNumSegmentsProp)
1746+
def memoryPoolStatsLoggingFrequencyMinutes = getInt(KafkaConfig.MemoryPoolStatsLoggingFrequencyMinutesProp)
1747+
17261748
def getNumReplicaAlterLogDirsThreads: Int = {
17271749
val numThreads: Integer = Option(getInt(KafkaConfig.NumReplicaAlterLogDirsThreadsProp)).getOrElse(logDirs.size)
17281750
numThreads

core/src/main/scala/kafka/server/KafkaServer.scala

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import kafka.utils._
3737
import kafka.zk.{AdminZkClient, BrokerInfo, KafkaZkClient}
3838
import org.apache.kafka.clients.{ApiVersions, ManualMetadataUpdater, NetworkClient, NetworkClientUtils}
3939
import org.apache.kafka.common.internals.Topic
40+
import org.apache.kafka.common.memory.MemoryPoolStatsStore
4041
import org.apache.kafka.common.message.ApiMessageType.ListenerType
4142
import org.apache.kafka.common.message.ControlledShutdownRequestData
4243
import org.apache.kafka.common.metrics.Metrics
@@ -46,13 +47,14 @@ import org.apache.kafka.common.requests.{ControlledShutdownRequest, ControlledSh
4647
import org.apache.kafka.common.security.scram.internals.ScramMechanism
4748
import org.apache.kafka.common.security.token.delegation.internals.DelegationTokenCache
4849
import org.apache.kafka.common.security.{JaasContext, JaasUtils}
49-
import org.apache.kafka.common.utils.{AppInfoParser, LogContext, Time, Utils, PoisonPill}
50+
import org.apache.kafka.common.utils.{AppInfoParser, LogContext, PoisonPill, Time, Utils}
5051
import org.apache.kafka.common.{Endpoint, Node, TopicPartition}
5152
import org.apache.kafka.metadata.BrokerState
5253
import org.apache.kafka.server.authorizer.Authorizer
5354
import org.apache.kafka.server.log.remote.storage.RemoteLogManagerConfig
5455
import org.apache.zookeeper.client.ZKClientConfig
5556

57+
import java.util.Optional
5658
import scala.collection.{Map, Seq}
5759
import scala.jdk.CollectionConverters._
5860
import scala.collection.mutable.{ArrayBuffer, Buffer}
@@ -323,13 +325,43 @@ class KafkaServer(
323325

324326
observer = Observer(config)
325327

328+
def initializeMemoryPoolStats(): Optional[MemoryPoolStatsStore] = {
329+
if (!config.memoryPoolStatsLoggingEnable) {
330+
return Optional.empty()
331+
}
332+
333+
info(s"Memory pool stats logging is enabled, segments = " +
334+
s"${config.memoryPoolStatsNumSegments}, max size = ${config.memoryPoolStatsMaxSize}, " +
335+
s"logging frequency in minutes = ${config.memoryPoolStatsLoggingFrequencyMinutes}")
336+
val memoryPoolStatsStore = new MemoryPoolStatsStore(config.memoryPoolStatsNumSegments, config.memoryPoolStatsMaxSize)
337+
val requestStatsLogger = new MemoryPoolStatsLogger()
338+
339+
def publishHistogramToLog(): Unit = {
340+
info("Publishing memory pool stats")
341+
requestStatsLogger.logStats(memoryPoolStatsStore)
342+
memoryPoolStatsStore.clear()
343+
}
344+
345+
val histogramPublisher = new KafkaScheduler(threads = 1, "histogram-publisher-")
346+
histogramPublisher.startup()
347+
histogramPublisher.schedule(name = "publish-histogram-to-log",
348+
fun = publishHistogramToLog,
349+
period = config.memoryPoolStatsLoggingFrequencyMinutes.toLong,
350+
unit = TimeUnit.MINUTES)
351+
352+
Optional.of(memoryPoolStatsStore)
353+
}
354+
355+
val memoryPoolStatsStore = initializeMemoryPoolStats()
356+
326357
// Create and start the socket server acceptor threads so that the bound port is known.
327358
// Delay starting processors until the end of the initialization sequence to ensure
328359
// that credentials have been loaded before processing authentications.
329360
//
330361
// Note that we allow the use of KRaft mode controller APIs when forwarding is enabled
331362
// so that the Envelope request is exposed. This is only used in testing currently.
332-
socketServer = new SocketServer(config, metrics, time, credentialProvider, observer, apiVersionManager)
363+
socketServer = new SocketServer(
364+
config, metrics, time, credentialProvider, observer, apiVersionManager, memoryPoolStatsStore)
333365
socketServer.startup(startProcessingRequests = false)
334366

335367
/* start replica manager */
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package kafka.server
2+
3+
import com.typesafe.scalalogging.Logger
4+
import kafka.utils.Logging
5+
import org.apache.kafka.common.memory.MemoryPoolStatsStore
6+
7+
import scala.collection.JavaConverters._
8+
9+
object MemoryPoolStatsLogger {
10+
private val logger = Logger("memory.pool.stats.logger")
11+
}
12+
13+
class MemoryPoolStatsLogger extends Logging {
14+
override lazy val logger = MemoryPoolStatsLogger.logger
15+
16+
def logStats(memoryPoolStatsStore: MemoryPoolStatsStore): Unit = {
17+
val frequencyList = memoryPoolStatsStore.getFrequencies.asScala.toSeq.sortBy(_._1.startInclusive)
18+
frequencyList.foreach {
19+
case (range, frequency) =>
20+
info(s"[${range.startInclusive}-${range.endInclusive}] = $frequency")
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)