From af52a13e891a32cecafa01122187ca1089893548 Mon Sep 17 00:00:00 2001 From: Hongshun Wang Date: Sun, 20 Sep 2026 17:06:56 +0800 Subject: [PATCH 1/2] [client] Replace Unsafe with Netty direct memory in ChunkedAllocationManager and expose metrics. --- .../fluss/client/write/RecordAccumulator.java | 11 +- .../client/write/RecordAccumulatorTest.java | 72 +++++++++++- .../org/apache/fluss/metrics/MetricNames.java | 8 ++ .../memory/ChunkedAllocationManager.java | 103 ++++++++++++----- .../memory/ChunkedAllocationManagerTest.java | 105 +++++++++++++++++- 5 files changed, 266 insertions(+), 33 deletions(-) diff --git a/fluss-client/src/main/java/org/apache/fluss/client/write/RecordAccumulator.java b/fluss-client/src/main/java/org/apache/fluss/client/write/RecordAccumulator.java index 32f4b881e4e..5ff98c64406 100644 --- a/fluss-client/src/main/java/org/apache/fluss/client/write/RecordAccumulator.java +++ b/fluss-client/src/main/java/org/apache/fluss/client/write/RecordAccumulator.java @@ -106,7 +106,7 @@ public final class RecordAccumulator { /** The arrow buffer allocator to allocate memory for arrow log write batch. */ private final BufferAllocator bufferAllocator; - /** The chunked allocation manager factory, stored for explicit native memory release. */ + /** The chunked allocation manager factory, stored for explicit direct memory release. */ private final ChunkedAllocationManager.ChunkedFactory chunkedFactory; /** Coordinates batch memory deallocation with resource destruction. */ @@ -206,6 +206,15 @@ private void registerMetrics(WriterMetricGroup writerMetricGroup) { // The number of user threads blocked waiting for buffer memory to enqueue their records writerMetricGroup.gauge( MetricNames.WRITER_BUFFER_WAITING_THREADS, writerBufferPool::queued); + writerMetricGroup.gauge( + MetricNames.WRITER_ACCUMULATOR_HEAP_MEMORY_USED_BYTES, + () -> writerBufferPool.totalSize() - writerBufferPool.availableMemory()); + writerMetricGroup.gauge( + MetricNames.WRITER_ACCUMULATOR_ARROW_MEMORY_USED_BYTES, + bufferAllocator::getAllocatedMemory); + writerMetricGroup.gauge( + MetricNames.WRITER_ACCUMULATOR_DIRECT_MEMORY_USED_BYTES, + chunkedFactory::getDirectMemoryUsedBytes); } /** Assigns and appends a record using the layout owned by its write context. */ diff --git a/fluss-client/src/test/java/org/apache/fluss/client/write/RecordAccumulatorTest.java b/fluss-client/src/test/java/org/apache/fluss/client/write/RecordAccumulatorTest.java index 4ea64f4efea..f2d2828bd53 100644 --- a/fluss-client/src/test/java/org/apache/fluss/client/write/RecordAccumulatorTest.java +++ b/fluss-client/src/test/java/org/apache/fluss/client/write/RecordAccumulatorTest.java @@ -32,6 +32,8 @@ import org.apache.fluss.metadata.TableDescriptor; import org.apache.fluss.metadata.TableInfo; import org.apache.fluss.metadata.TablePath; +import org.apache.fluss.metrics.Gauge; +import org.apache.fluss.metrics.MetricNames; import org.apache.fluss.record.ChangeType; import org.apache.fluss.record.DefaultKvRecord; import org.apache.fluss.record.IndexedLogRecord; @@ -148,6 +150,58 @@ public void start() { // TODO Add more tests to test lingMs, retryBackoffMs, deliveryTimeoutMs and // nextBatchExpiryTimeMs if we introduced. + @Test + void testAccumulatorMemoryMetrics() throws Exception { + int batchSize = 1024; + TestingWriterMetricGroup metrics = TestingWriterMetricGroup.newInstance(); + RecordAccumulator accum = + createTestRecordAccumulator( + 5000, batchSize, 256, 10L * batchSize, bucketAssigner, metrics); + + try { + assertThat(metrics.getMetrics()) + .containsKeys( + MetricNames.WRITER_ACCUMULATOR_HEAP_MEMORY_USED_BYTES, + MetricNames.WRITER_ACCUMULATOR_ARROW_MEMORY_USED_BYTES, + MetricNames.WRITER_ACCUMULATOR_DIRECT_MEMORY_USED_BYTES); + + Gauge heapMemoryUsed = + (Gauge) + metrics.getMetrics() + .get(MetricNames.WRITER_ACCUMULATOR_HEAP_MEMORY_USED_BYTES); + Gauge arrowMemoryUsed = + (Gauge) + metrics.getMetrics() + .get(MetricNames.WRITER_ACCUMULATOR_ARROW_MEMORY_USED_BYTES); + Gauge directMemoryUsedBytes = + (Gauge) + metrics.getMetrics() + .get(MetricNames.WRITER_ACCUMULATOR_DIRECT_MEMORY_USED_BYTES); + + assertThat(((Number) heapMemoryUsed.getValue()).longValue()).isZero(); + assertThat(((Number) arrowMemoryUsed.getValue()).longValue()).isZero(); + assertThat(((Number) directMemoryUsedBytes.getValue()).longValue()).isZero(); + + bucketAssigner.setBucketId(0); + accum.append( + WriteRecord.forArrowAppend( + DATA1_TABLE_INFO, DATA1_PHYSICAL_TABLE_PATH, row(1, "a"), null), + (bucket, offset, exception) -> {}, + cluster); + + long heapMemory = ((Number) heapMemoryUsed.getValue()).longValue(); + long arrowMemory = ((Number) arrowMemoryUsed.getValue()).longValue(); + long directMemory = ((Number) directMemoryUsedBytes.getValue()).longValue(); + assertThat(heapMemory).isPositive(); + assertThat(arrowMemory).isPositive(); + assertThat(directMemory).isGreaterThanOrEqualTo(arrowMemory); + } finally { + accum.close(); + accum.abortAllBatches(new RuntimeException("test cleanup")); + accum.destroyResources(); + } + } + @Test void testDrainBatches() throws Exception { // test case: node1(tb1, tb2), node2(tb3). @@ -815,6 +869,22 @@ private RecordAccumulator createTestRecordAccumulator( int pageSize, long totalSize, BucketAssigner assigner) { + return createTestRecordAccumulator( + batchTimeoutMs, + batchSize, + pageSize, + totalSize, + assigner, + TestingWriterMetricGroup.newInstance()); + } + + private RecordAccumulator createTestRecordAccumulator( + int batchTimeoutMs, + int batchSize, + int pageSize, + long totalSize, + BucketAssigner assigner, + TestingWriterMetricGroup metrics) { conf.set(ConfigOptions.CLIENT_WRITER_BATCH_TIMEOUT, Duration.ofMillis(batchTimeoutMs)); // TODO client writer buffer maybe removed. conf.set(ConfigOptions.CLIENT_WRITER_BUFFER_MEMORY_SIZE, new MemorySize(totalSize)); @@ -832,7 +902,7 @@ private RecordAccumulator createTestRecordAccumulator( RpcClient.create(conf, TestingClientMetricGroup.newInstance()), TabletServerGateway.class), null), - TestingWriterMetricGroup.newInstance(), + metrics, clock, (tableInfo, path) -> assigner); } diff --git a/fluss-common/src/main/java/org/apache/fluss/metrics/MetricNames.java b/fluss-common/src/main/java/org/apache/fluss/metrics/MetricNames.java index ac60ee5f101..dd9c844efff 100644 --- a/fluss-common/src/main/java/org/apache/fluss/metrics/MetricNames.java +++ b/fluss-common/src/main/java/org/apache/fluss/metrics/MetricNames.java @@ -337,6 +337,14 @@ public class MetricNames { public static final String WRITER_RECORDS_PER_BATCH = "recordsPerBatch"; public static final String WRITER_SEND_LATENCY_MS = "sendLatencyMs"; + // for record accumulator memory + public static final String WRITER_ACCUMULATOR_HEAP_MEMORY_USED_BYTES = + "accumulatorHeapMemoryUsedBytes"; + public static final String WRITER_ACCUMULATOR_ARROW_MEMORY_USED_BYTES = + "accumulatorArrowMemoryUsedBytes"; + public static final String WRITER_ACCUMULATOR_DIRECT_MEMORY_USED_BYTES = + "accumulatorDirectMemoryUsedBytes"; + // for scanner public static final String SCANNER_TIME_MS_BETWEEN_POLL = "timeMsBetweenPoll"; public static final String SCANNER_LAST_POLL_SECONDS_AGO = "lastPollSecondsAgo"; diff --git a/fluss-common/src/main/java/org/apache/fluss/shaded/arrow/org/apache/arrow/memory/ChunkedAllocationManager.java b/fluss-common/src/main/java/org/apache/fluss/shaded/arrow/org/apache/arrow/memory/ChunkedAllocationManager.java index 14e29a2c696..b032c657bdb 100644 --- a/fluss-common/src/main/java/org/apache/fluss/shaded/arrow/org/apache/arrow/memory/ChunkedAllocationManager.java +++ b/fluss-common/src/main/java/org/apache/fluss/shaded/arrow/org/apache/arrow/memory/ChunkedAllocationManager.java @@ -18,11 +18,13 @@ package org.apache.fluss.shaded.arrow.org.apache.arrow.memory; import org.apache.fluss.annotation.VisibleForTesting; -import org.apache.fluss.shaded.arrow.org.apache.arrow.memory.util.MemoryUtil; +import org.apache.fluss.shaded.netty4.io.netty.buffer.ByteBuf; +import org.apache.fluss.shaded.netty4.io.netty.buffer.UnpooledByteBufAllocator; import java.util.ArrayDeque; import java.util.Deque; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; /** * An {@link AllocationManager} that packs small allocations into large pre-allocated chunks using a @@ -79,7 +81,8 @@ *

How it works

* *