Merged
Conversation
Signed-off-by: Hongkun Xu <xuhongkun666@163.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #18290 +/- ##
============================================
- Coverage 63.65% 63.62% -0.04%
Complexity 1659 1659
============================================
Files 3244 3245 +1
Lines 197390 197396 +6
Branches 30555 30555
============================================
- Hits 125646 125587 -59
- Misses 61690 61772 +82
+ Partials 10054 10037 -17
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
We packaged a new
pinot-jdbc-driverbased on Pinot 1.5.0 and rolled it out to production services that usePinotDriverbehind a HikariCP connection pool — a standard, high-throughput JDBC integration pattern.Shortly after the upgrade, those services began reporting
OutOfMemoryErrorin production. The OOMs were not query-related and could not be reproduced with ad-hoc single-connection usage; they correlated strictly with the use of a connection pool and the amount of wall-clock time the service had been running.Screenshot 1 — Production OOM stack trace from the affected service:

Screenshot 2 — Reproduction showing thread count growing by 20 every 5

minutes under HikariCP (pool size = 20):
Screenshot 3 — Same reproduction after this fix; thread count stays flat

across multiple refresh cycles:
Problem
Each
BrokerCacheinstance creates its ownAsyncHttpClient, which in turn creates its own privateNioEventLoopGroup(sized2 * availableProcessors()) andHashedWheelTimerthread. When the Pinot Java / JDBC client is used behind a connection pool (e.g. HikariCP) with N pooled connections, there are NBrokerCacheinstances, and therefore N independent Netty thread pools for a task that is fundamentally shared: periodically refreshing the broker-to-table mapping from the controller.Two behaviors compound the cost:
BrokerCacheUpdaterPeriodicrefreshes every 5 minutes by default. Between refreshes the pooled HTTP connection sits idle for ~5 minutes, well beyond AHC's defaultpooledConnectionIdleTimeoutof 60s, so the connection pool cleaner closes the TCP connection every cycle.NioEventLoopslot via round-robin. If that slot's worker thread has not been lazily spawned yet, it is spawned now. This means the NIO thread count grows by roughly +1 perBrokerCacheper refresh interval, until each privateNioEventLoopGroupreaches its2 * NPROCcap.With N pooled JDBC connections on an M-core host,
BrokerCachealone can contribute up toN * 2 * MNetty NIO threads plusNHashedWheelTimerthreads, growing observably over time.Root cause
Each
BrokerCacheowns a private Netty I/O thread pool for a workload that is effectively homogeneous across all instances (GET/brokers/tablesfrom the controller). The per-instance ownership model is the source of the amplification; the instances do not need private thread pools to remain correct.Fix
Introduce
PinotClientNettyResources, a small holder that exposes a single JVM-wide daemonNioEventLoopGroupand a single JVM-wide daemonHashedWheelTimer.BrokerCachenow injects both into its AHC builder viasetEventLoopGroup(...)andsetNettyTimer(...).AsyncHttpClient natively supports this sharing pattern: when either resource is supplied externally, AHC does not shut it down on
close()(ChannelManager#allowReleaseEventLoopGroupandDefaultAsyncHttpClient#allowStopNettyTimerare both set tofalsein that case). EachBrokerCachestill owns its ownAsyncHttpClient, connection pool, SSL context, timeouts, and headers — only the low-level Netty I/O threads and timer thread are shared.After this change, the total NIO thread count contributed by
BrokerCacherefreshes is bounded by2 * availableProcessors()globally, independent of the number of pooledPinotConnection/Connectioninstances. All shared threads are daemon, so they do not block JVM exit.Scope of change
pinot-clients/pinot-java-client/.../PinotClientNettyResources.javapinot-clients/pinot-java-client/.../BrokerCache.java— two extra builder calls (setEventLoopGroup,setNettyTimer) on the existing AHC configuration.No public API changes. No changes to
JsonAsyncHttpPinotClientTransport,PinotControllerTransport,ControllerBasedBrokerSelector,PinotDriver, orConnectionFactory. Behavior of direct Java-client users ofBrokerCacheis unchanged other than the reduced thread footprint.Why this is safe
NioEventLoopGroupandHashedWheelTimerare thread-safe and explicitly designed to be shared across clients; this is the standard pattern documented by both Netty and AsyncHttpClient.BrokerCache's ownAsyncHttpClientand its own connection pool.BrokerCache.close()continues to close its own AHC cleanly without affecting otherBrokerCacheinstances.Impact on QPS and controller load
No change. Each
BrokerCachestill issues the same number of requests at the same cadence against the same controller endpoint. Sharing I/O threads can only reduce context-switch overhead, never add to it.Testing
PinotDriver, pool size N, on an M-core host: before this change the NIO thread count grows by N everybrokerUpdateFreqInMillis; after this change it is bounded by2 * Mregardless of N (see Screenshot 3 above).BrokerCache/BrokerCacheUpdaterPeriodicunit tests continue to pass; behavior of the constructor andupdateBrokerData()is unchanged from the caller's perspective.Follow-ups (not in this PR)
BrokerCacheUpdaterPeriodic'sScheduledExecutorServicethread.Future#get()call inBrokerCache#getTableToBrokersData().JsonAsyncHttpPinotClientTransportandPinotControllerTransport.