Skip to content
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

Memory Leak related to io.netty.util.concurrent.FastThreadLocal in Vertx code #5078

Closed
bolbat opened this issue Jan 25, 2024 · 6 comments
Closed
Assignees
Labels
Milestone

Comments

@bolbat
Copy link

bolbat commented Jan 25, 2024

Version

Vertx: bug since 4.4.x, available on 4.5.1
Netty: version doesn't matter, tested with latest 4.1.106.Final
Java: tested on 17 and 21, but could be reproduced on any supported version

Context

FastThreadLocal feature designed to be faster than basic ThreadLocal and internally use InternalThreadLocalMap which internally have:

  • static nextIndex field - to provide unique variable index for each FastThreadLocal instance
  • instance indexedVariables field - to hold variables for specific FastThreadLocal instance

Size of indexedVariables field depends on a static nextIndex field
If someone create a lot of FastThreadLocal instance - this global static index will be increased by one for each instance
As result for all InternalThreadLocalMap instances each invocation of setIndexedVariable method will resize indexedVariables array if index is bigger than array size (resizing indexedVariables arrays in all instance because of global static index)

Consequently all FastThreadLocalThread instances (including VertxThread) and FastThreadLocal fields will have continuously increasing indexedVariables arrays in their InternalThreadLocalMap instances till JVM will be restarted

Our case

On our production load one instance serving tens thousands requests per second and doing much more outgoing requests
Each instance use 64 physical cores and use 128 threads for Netty event-pool
For around 8h uptime each of this 128 threads have own indexedVariables array instance with length 8_000_000+ and use 60MB+ memory
99.999% values in each array instance is empty and filled with InternalThreadLocalMap.UNSET object
Usually each thread / array store only 3-15 values from features that use FastThreadLocal in a right way

Heap dump details

Screenshot 2024-01-25 at 16 45 44 Screenshot 2024-01-25 at 16 46 23

What caused this?

After some investigation I've found that there some instances of FastThreadLocal stored as not static fields and could be created a lot of times if parent object also created a lot of times

How to find exactly which code does this?

I've implemented debug feature (with ByteBuddy constructor interceptor) for FastThreadLocal and collected StackTraces stats with information about who creating this instances

It looks like this
  • Stats collected for threads who instantiated FastThreadLocal 1000+ times
  • StackTraces trimmed so as not to take up too much space, but still with all required information to see the problem
Instantiation[10177] StackTrace: java.lang.Throwable
	at io.netty.util.concurrent.FastThreadLocal.<init>(FastThreadLocal.java:127)
	at io.vertx.core.net.impl.pool.CombinerExecutor.<init>(CombinerExecutor.java:36)
	at io.vertx.core.net.impl.pool.SimpleConnectionPool.<init>(SimpleConnectionPool.java:224)
	at io.vertx.core.net.impl.pool.ConnectionPool.pool(ConnectionPool.java:48)
	at io.vertx.core.http.impl.SharedClientHttpStreamEndpoint.<init>(SharedClientHttpStreamEndpoint.java:72)
	at io.vertx.core.http.impl.HttpClientImpl$1.create(HttpClientImpl.java:362)
	at io.vertx.core.net.impl.pool.ConnectionManager.lambda$getConnection$1(ConnectionManager.java:50)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1740)
	at io.vertx.core.net.impl.pool.ConnectionManager.getConnection(ConnectionManager.java:50)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:368)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:325)
	at io.vertx.core.http.impl.HttpClientImpl.request(HttpClientImpl.java:191)
	at io.vertx.ext.web.client.impl.HttpContext.handleCreateRequest(HttpContext.java:488)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:372)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.createRequest(HttpContext.java:220)
	at io.vertx.ext.web.client.impl.HttpContext.handlePrepareRequest(HttpContext.java:418)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:369)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.prepareRequest(HttpContext.java:208)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:538)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:420)
	....


Instantiation[119312] StackTrace: java.lang.Throwable
	at io.netty.util.concurrent.FastThreadLocal.<init>(FastThreadLocal.java:127)
	at io.vertx.core.net.impl.pool.CombinerExecutor.<init>(CombinerExecutor.java:36)
	at io.vertx.core.net.impl.pool.SimpleConnectionPool.<init>(SimpleConnectionPool.java:224)
	at io.vertx.core.net.impl.pool.ConnectionPool.pool(ConnectionPool.java:48)
	at io.vertx.core.http.impl.SharedClientHttpStreamEndpoint.<init>(SharedClientHttpStreamEndpoint.java:72)
	at io.vertx.core.http.impl.HttpClientImpl$1.create(HttpClientImpl.java:362)
	at io.vertx.core.net.impl.pool.ConnectionManager.lambda$getConnection$1(ConnectionManager.java:50)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1740)
	at io.vertx.core.net.impl.pool.ConnectionManager.getConnection(ConnectionManager.java:50)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:368)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:325)
	at io.vertx.core.http.impl.HttpClientImpl.request(HttpClientImpl.java:191)
	at io.vertx.ext.web.client.impl.HttpContext.handleCreateRequest(HttpContext.java:488)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:372)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.createRequest(HttpContext.java:220)
	at io.vertx.ext.web.client.impl.HttpContext.handlePrepareRequest(HttpContext.java:418)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:369)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.prepareRequest(HttpContext.java:208)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:538)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:420)
	....


Instantiation[87958] StackTrace: java.lang.Throwable
	at io.netty.util.concurrent.FastThreadLocal.<init>(FastThreadLocal.java:127)
	at io.vertx.core.net.impl.pool.CombinerExecutor.<init>(CombinerExecutor.java:36)
	at io.vertx.core.net.impl.pool.SimpleConnectionPool.<init>(SimpleConnectionPool.java:224)
	at io.vertx.core.net.impl.pool.ConnectionPool.pool(ConnectionPool.java:48)
	at io.vertx.core.http.impl.SharedClientHttpStreamEndpoint.<init>(SharedClientHttpStreamEndpoint.java:72)
	at io.vertx.core.http.impl.HttpClientImpl$1.create(HttpClientImpl.java:362)
	at io.vertx.core.net.impl.pool.ConnectionManager.lambda$getConnection$1(ConnectionManager.java:50)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1740)
	at io.vertx.core.net.impl.pool.ConnectionManager.getConnection(ConnectionManager.java:50)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:368)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:325)
	at io.vertx.core.http.impl.HttpClientImpl.request(HttpClientImpl.java:191)
	at io.vertx.ext.web.client.impl.HttpContext.handleCreateRequest(HttpContext.java:488)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:372)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.createRequest(HttpContext.java:220)
	at io.vertx.ext.web.client.impl.HttpContext.handlePrepareRequest(HttpContext.java:418)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:369)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.prepareRequest(HttpContext.java:208)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:538)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:420)
	....


Instantiation[16292] StackTrace: java.lang.Throwable
	at io.netty.util.concurrent.FastThreadLocal.<init>(FastThreadLocal.java:127)
	at io.vertx.core.net.impl.pool.CombinerExecutor.<init>(CombinerExecutor.java:36)
	at io.vertx.core.net.impl.pool.SimpleConnectionPool.<init>(SimpleConnectionPool.java:224)
	at io.vertx.core.net.impl.pool.ConnectionPool.pool(ConnectionPool.java:48)
	at io.vertx.core.http.impl.SharedClientHttpStreamEndpoint.<init>(SharedClientHttpStreamEndpoint.java:72)
	at io.vertx.core.http.impl.HttpClientImpl$1.create(HttpClientImpl.java:362)
	at io.vertx.core.net.impl.pool.ConnectionManager.lambda$getConnection$1(ConnectionManager.java:50)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1740)
	at io.vertx.core.net.impl.pool.ConnectionManager.getConnection(ConnectionManager.java:50)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:368)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:325)
	at io.vertx.core.http.impl.HttpClientImpl.request(HttpClientImpl.java:191)
	at io.vertx.ext.web.client.impl.HttpContext.handleCreateRequest(HttpContext.java:488)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:372)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.createRequest(HttpContext.java:220)
	at io.vertx.ext.web.client.impl.HttpContext.handlePrepareRequest(HttpContext.java:418)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:369)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.prepareRequest(HttpContext.java:208)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:538)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:420)
	....


Instantiation[16971] StackTrace: java.lang.Throwable
	at io.netty.util.concurrent.FastThreadLocal.<init>(FastThreadLocal.java:127)
	at io.vertx.core.net.impl.pool.CombinerExecutor.<init>(CombinerExecutor.java:36)
	at io.vertx.core.net.impl.pool.SimpleConnectionPool.<init>(SimpleConnectionPool.java:224)
	at io.vertx.core.net.impl.pool.ConnectionPool.pool(ConnectionPool.java:48)
	at io.vertx.core.http.impl.SharedClientHttpStreamEndpoint.<init>(SharedClientHttpStreamEndpoint.java:72)
	at io.vertx.core.http.impl.HttpClientImpl$1.create(HttpClientImpl.java:362)
	at io.vertx.core.net.impl.pool.ConnectionManager.lambda$getConnection$1(ConnectionManager.java:50)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1740)
	at io.vertx.core.net.impl.pool.ConnectionManager.getConnection(ConnectionManager.java:50)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:368)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:325)
	at io.vertx.core.http.impl.HttpClientImpl.request(HttpClientImpl.java:191)
	at io.vertx.ext.web.client.impl.HttpContext.handleCreateRequest(HttpContext.java:488)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:372)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.createRequest(HttpContext.java:220)
	at io.vertx.ext.web.client.impl.HttpContext.handlePrepareRequest(HttpContext.java:418)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:369)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.prepareRequest(HttpContext.java:208)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:538)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:420)
	....


Instantiation[10942] StackTrace: java.lang.Throwable
	at io.netty.util.concurrent.FastThreadLocal.<init>(FastThreadLocal.java:127)
	at io.vertx.core.net.impl.pool.CombinerExecutor.<init>(CombinerExecutor.java:36)
	at io.vertx.core.net.impl.pool.SimpleConnectionPool.<init>(SimpleConnectionPool.java:224)
	at io.vertx.core.net.impl.pool.ConnectionPool.pool(ConnectionPool.java:48)
	at io.vertx.core.http.impl.SharedClientHttpStreamEndpoint.<init>(SharedClientHttpStreamEndpoint.java:72)
	at io.vertx.core.http.impl.HttpClientImpl$1.create(HttpClientImpl.java:362)
	at io.vertx.core.net.impl.pool.ConnectionManager.lambda$getConnection$1(ConnectionManager.java:50)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1740)
	at io.vertx.core.net.impl.pool.ConnectionManager.getConnection(ConnectionManager.java:50)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:368)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:325)
	at io.vertx.core.http.impl.HttpClientImpl.request(HttpClientImpl.java:191)
	at io.vertx.ext.web.client.impl.HttpContext.handleCreateRequest(HttpContext.java:488)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:372)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.createRequest(HttpContext.java:220)
	at io.vertx.ext.web.client.impl.HttpContext.handlePrepareRequest(HttpContext.java:418)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:369)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.prepareRequest(HttpContext.java:208)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:538)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:420)
	....


Instantiation[247390] StackTrace: java.lang.Throwable
	at io.netty.util.concurrent.FastThreadLocal.<init>(FastThreadLocal.java:127)
	at io.vertx.core.net.impl.pool.CombinerExecutor.<init>(CombinerExecutor.java:36)
	at io.vertx.core.net.impl.pool.SimpleConnectionPool.<init>(SimpleConnectionPool.java:224)
	at io.vertx.core.net.impl.pool.ConnectionPool.pool(ConnectionPool.java:48)
	at io.vertx.core.http.impl.SharedClientHttpStreamEndpoint.<init>(SharedClientHttpStreamEndpoint.java:72)
	at io.vertx.core.http.impl.HttpClientImpl$1.create(HttpClientImpl.java:362)
	at io.vertx.core.net.impl.pool.ConnectionManager.lambda$getConnection$1(ConnectionManager.java:50)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1740)
	at io.vertx.core.net.impl.pool.ConnectionManager.getConnection(ConnectionManager.java:50)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:368)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:325)
	at io.vertx.core.http.impl.HttpClientImpl.request(HttpClientImpl.java:191)
	at io.vertx.ext.web.client.impl.HttpContext.handleCreateRequest(HttpContext.java:488)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:372)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.createRequest(HttpContext.java:220)
	at io.vertx.ext.web.client.impl.HttpContext.handlePrepareRequest(HttpContext.java:418)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:369)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.prepareRequest(HttpContext.java:208)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:538)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:420)
	....


Instantiation[148494] StackTrace: java.lang.Throwable
	at io.netty.util.concurrent.FastThreadLocal.<init>(FastThreadLocal.java:127)
	at io.vertx.core.net.impl.pool.CombinerExecutor.<init>(CombinerExecutor.java:36)
	at io.vertx.core.net.impl.pool.SimpleConnectionPool.<init>(SimpleConnectionPool.java:224)
	at io.vertx.core.net.impl.pool.ConnectionPool.pool(ConnectionPool.java:48)
	at io.vertx.core.http.impl.SharedClientHttpStreamEndpoint.<init>(SharedClientHttpStreamEndpoint.java:72)
	at io.vertx.core.http.impl.HttpClientImpl$1.create(HttpClientImpl.java:362)
	at io.vertx.core.net.impl.pool.ConnectionManager.lambda$getConnection$1(ConnectionManager.java:50)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1740)
	at io.vertx.core.net.impl.pool.ConnectionManager.getConnection(ConnectionManager.java:50)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:368)
	at io.vertx.core.http.impl.HttpClientImpl.doRequest(HttpClientImpl.java:325)
	at io.vertx.core.http.impl.HttpClientImpl.request(HttpClientImpl.java:191)
	at io.vertx.ext.web.client.impl.HttpContext.handleCreateRequest(HttpContext.java:488)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:372)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.createRequest(HttpContext.java:220)
	at io.vertx.ext.web.client.impl.HttpContext.handlePrepareRequest(HttpContext.java:418)
	at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:369)
	at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:362)
	at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:329)
	at io.vertx.ext.web.client.impl.HttpContext.prepareRequest(HttpContext.java:208)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:538)
	at io.vertx.ext.web.client.impl.HttpRequestImpl.send(HttpRequestImpl.java:420)
	....

Now we see that problem is related to HttpClient component and especially to io.vertx.core.net.impl.pool.CombinerExecutor
It's generating a lot of instances and as consequence created a lot of FastThreadLocal instances

Cause of this is the next commit: f15cf1c
In scope of this PR: #4750
And this ticket: #4749

Additional information

I've checked who potentially can also cause this problem if someone change code without understanding FastThreadLocal implementation details

Already problem or high risk:

Can be a problem:

  • io.netty.handler.ssl.util.FingerprintTrustManagerFactory.tlmd
  • io.netty.handler.codec.marshalling.ThreadLocalMarshallerProvider.marshallers
  • io.netty.buffer.PooledByteBufAllocator.PoolThreadLocalCache.PoolThreadLocalCache(PooledByteBufAllocator, boolean)
  • io.netty.util.Recycler.threadLocal
  • io.netty.handler.codec.marshalling.ThreadLocalUnmarshallerProvider.unmarshallers

Good examples how to use FastThreadLocal feature in a safe way (just use as static final fields)

  • io.netty.buffer.ByteBufUtil.BYTE_ARRAYS
  • io.netty.handler.codec.CodecOutputList.CODEC_OUTPUT_LISTS_POOL
  • io.netty.handler.ssl.util.SimpleKeyManagerFactory.CURRENT_SPI
  • io.netty.handler.ssl.util.SimpleTrustManagerFactory.CURRENT_SPI
  • io.netty.handler.codec.http.HttpHeaderDateFormat.dateFormatThreadLocal
  • io.netty.util.concurrent.ImmediateEventExecutor.DELAYED_RUNNABLES
  • io.netty.util.concurrent.ImmediateEventExecutor.RUNNING
  • io.netty.handler.codec.DateFormatter.INSTANCES
  • io.netty.util.internal.ThreadExecutorMap.mappings
  • io.netty.channel.ChannelHandlerMask.MASKS
  • io.netty.handler.codec.http.websocketx.WebSocketUtil.MD5
  • io.netty.handler.codec.http.websocketx.WebSocketUtil.SHA1
  • io.netty.channel.DefaultChannelPipeline.nameCaches
  • io.netty.channel.ChannelOutboundBuffer.NIO_BUFFERS
@bolbat bolbat added the bug label Jan 25, 2024
@vietj vietj added this to the 4.5.2 milestone Jan 25, 2024
@vietj vietj self-assigned this Jan 25, 2024
@bolbat
Copy link
Author

bolbat commented Jan 25, 2024

If someone would need code to debug this situation I will share my as example
Code is very ad hock, just for fast debugging

Add this dependencies to ByteBuddy library

		<dependency>
			<groupId>net.bytebuddy</groupId>
			<artifactId>byte-buddy</artifactId>
		</dependency>
		<dependency>
			<groupId>net.bytebuddy</groupId>
			<artifactId>byte-buddy-agent</artifactId>
		</dependency>

Instrument related code with something like this
Call instrumentForDebug on some early application startup stage

	public static void instrumentForDebug() {
		ByteBuddyAgent.install();
		new ByteBuddy()
				.redefine(FastThreadLocal.class)
				.visit(Advice.to(FastThreadLocalInstantiationInterceptor.class).on(ElementMatchers.isDefaultConstructor()))
				.make()
				.load(InternalThreadLocalMap.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent())
				.getLoaded();
	}

	public static final class FastThreadLocalInstantiationInterceptor {

		public static final Map<String, AtomicInteger> STATS = Maps.mutable.empty();

		@OnMethodEnter
		public static void intercept() {
			final String stackTraceString = ExceptionUtils.getStackTrace(new Throwable());

			final AtomicInteger existing = STATS.get(stackTraceString);
			if (existing == null) {
				STATS.put(stackTraceString, new AtomicInteger(1));
			} else {
				existing.incrementAndGet();
			}
		}

	}

Prepare some debug information and log it or get by some debug API like I did

	private void debugFastThreadLocalInstantiationStats(final StringBuilder sb, final int minInstantiations) {
		line(sb.append("FastThreadLocal Instantiation Stats"));

		FastThreadLocalInstantiationInterceptor.STATS.forEach((k, v) -> {
			final int instantiations = v.get();
			if (instantiations >= minInstantiations) {
				line(sb.append("Instantiation[").append(instantiations).append("] ")
						.append("StackTrace: ").append(k));
				line(sb);
			}
		});

	}

	private void line(final StringBuilder sb) {
		sb.append(System.lineSeparator());
	}

@bolbat
Copy link
Author

bolbat commented Jan 25, 2024

And example how OldGen heap looks like with ZGC on Java 21 with this problem
Screenshot 2024-01-25 at 17 45 13

@vietj
Copy link
Member

vietj commented Jan 26, 2024

thanks @bolbat

@franz1981
Copy link
Contributor

franz1981 commented Jan 26, 2024

FYI, I think you could the the leak detection of a sync profiler (adding --live to the cmd line options) to detect this or just allocation profiling.

Thanks for the analysis, although by reading this I don't see the problem in the fast thread local, but instead in the high number of combiner executors created: any idea why @vietj ?

Said that, fast thread locals could be "smarter" in resizing the array, if it knows that a combiner is no longer reachable, and could do a better job.
This means making it autocloseable and handle thread local dispose somehow and we should have a clean life cycle for it already, just need to use it to enforce thread local disposal

@franz1981
Copy link
Contributor

@bolbat And I was very wrong, it seems there's no way to prevent indexes to keep on growing there, so, the fix made by @vietj should be the one to solve AND improve performance in one go (as the original Pr I sent was meant to do, sorry for that :"( )

@vietj vietj closed this as completed Jan 29, 2024
@bolbat
Copy link
Author

bolbat commented Jan 30, 2024

Confirming that problem was fixed in 4.5.2-SNAPSHOT
Heap looks stable, InternalThreadLocalMap map index and size is near 50 elements, WebClient works fine.

Heap OldGen graph for last 2 days from one node:
Screenshot 2024-01-30 at 08 23 00

tsegismont added a commit to tsegismont/vertx-micrometer-metrics that referenced this issue Jan 30, 2024
Related to eclipse-vertx/vert.x#5078

FastThreadLocal can cause a memory-leak if not used as a class constant.

Signed-off-by: Thomas Segismont <[email protected]>
tsegismont added a commit to vert-x3/vertx-micrometer-metrics that referenced this issue Feb 14, 2024
Related to eclipse-vertx/vert.x#5078

FastThreadLocal can cause a memory-leak if not used as a class constant.

Signed-off-by: Thomas Segismont <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants