HDDS-15592 use heap allocation in ChunkBuffer#10536
Conversation
|
|
||
| /** | ||
| * When true, {@link #allocate} uses direct ByteBuffers instead of heap ByteBuffers. | ||
| * Set only by {@link BlockOutputStreamWriteBenchmark} to compare allocation strategies |
There was a problem hiding this comment.
hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/ChunkBuffer.java:36: warning - Tag @link: reference not found: BlockOutputStreamWriteBenchmark
https://github.com/yandrey321/ozone/actions/runs/27780341047/job/82205691733#step:13:2213
| @VisibleForTesting | ||
| // CHECKSTYLE:OFF VisibilityModifier | ||
| AtomicBoolean ALLOCATE_DIRECT = new AtomicBoolean(false); | ||
| // CHECKSTYLE:ON VisibilityModifier |
There was a problem hiding this comment.
Please:
- avoid checkstyle suppression
- do not add
@VisibleForTesting
There was a problem hiding this comment.
I wanted something lightweight for a benchmark that proves performance improvements.
There was a problem hiding this comment.
But this is not the benchmark code.
There was a problem hiding this comment.
this is a switch that is used in the benchmark to find out the diff between heap and direct allocations
There was a problem hiding this comment.
Making it configurable in prod is not a big change: adoroszlai@79cbbf6
There was a problem hiding this comment.
Why should it be configurable at all? The purpose of the benchmarks is to produce evidence so we can make a conclusive decision.
There was a problem hiding this comment.
The benchmark needs to test various implementations. Adding the benchmark code to the master branch in the main Ozone repo requires that those implementations also exist there. Choosing between the implementations using an internal toggle that even requires CHECKSTYLE:OFF-style hacks is ugly.
Results may depend on hardware/software environment. Providing this simple toggle configuration does not add much complexity, but enables users to test in their own environment if needed.
I would prefer adding performance tests in a separate repo:
- various POC implementations can be freely added without affecting production code
- JMH can be used without license problems (HDDS-6202)
- can test different versions of Ozone
- performance tests would not be confused with functional tests (see
TestUnhealthyContainersDerbyPerformance)
There was a problem hiding this comment.
I endorse the idea of an ozone-dev style public repo for POC, benchmarks, & profiling; it would be particularly great if we could keep reference to the benchmark and profiling results themselves for later development. But if the second repo is also Apache, does that let us off the GPL hook?
There was a problem hiding this comment.
But if the second repo is also Apache, does that let us off the GPL hook?
Yes, since we would not be distributing (releasing) code from that repo.
There was a problem hiding this comment.
Removed the switch and use heap allocations by default.
|
cc @rnblough |
There was a problem hiding this comment.
In this PR the benchmark is a shell script but in #10546 it is a java class. We should be consistent with how we create benchmarks and where we put them.
There was a problem hiding this comment.
removed the shell script
| @VisibleForTesting | ||
| // CHECKSTYLE:OFF VisibilityModifier | ||
| AtomicBoolean ALLOCATE_DIRECT = new AtomicBoolean(false); | ||
| // CHECKSTYLE:ON VisibilityModifier |
There was a problem hiding this comment.
Why should it be configurable at all? The purpose of the benchmarks is to produce evidence so we can make a conclusive decision.
|
@yandrey321 , it seems that the benchmark is a mock but does not involve real network IO. If it is it the case, the improvement probably would become negative when network IO is involved. |
For direct buffer, UnsafeByteOperations.unsafeWrap returns NioByteString but not BoundedByteString.
public static void main(String[] args) {
final ByteBuffer buffer = ByteBuffer.allocateDirect(4);
buffer.putInt(0);
System.out.println(UnsafeByteOperations.unsafeWrap(buffer).getClass());
System.out.println(com.google.protobuf.UnsafeByteOperations.unsafeWrap(buffer).getClass());
} |
Yes, in case of direct memory allocation we re getting NioByteString and JVM use slower memory copy function comparing that the one available for BoundedByteString which is used for heap allocated memory. Benchmark proofs better throughput for heap allocated buffers. |
The fix aims to decrease CPU consumption on the client side and improve theoretical end-to-end throughput. In case of slow disks the improvement would be less, but faster disks and less latency on the network side would produce better improvement. Its a small low risk incremental fix. Please also note that the benchmark was run on m1 mac with unified memory architecture, on amd64 the throughput gain should be better. |
For NioByteString (i.e. direct buffer), there is no buffer copying to gRPC/Netty wire buffer. |
|
Performance tip from Google:
|
@szetszwo the benchmark clearly shows that in Ozone context heap allocation provides better throughput. |
Just used async-profiler to check, and it shows a copy does happen for NioByteString — in fact two copies, versus one for the heap-backed BoundedByteString. Setup: the client write path with mocked container RPCs, where the request is serialized via request.writeTo(...) into a pooled direct Netty ByteBuf — the same OutputStreamEncoder + framing gRPC's MessageFramer uses. Same benchmark run twice, only flipping ChunkBuffer allocation (allocateDirect → NioByteString vs allocateHeap → BoundedByteString). CPU event, 1 MB writes, 2 threads. Direct / NioByteString.writeTo — 15.7% of total CPU: Heap / BoundedByteString(LiteralByteString).writeTo — 0.7% of total CPU: Both paths terminate in setBytes → Unsafe.copyMemory into the Netty wire buffer, so neither is zero-copy here. The difference is that OutputStreamEncoder cannot bulk-transfer an array-less (direct) buffer, so for NioByteString it first stages the off-heap bytes into a temporary heap buffer (slice/arraycopy) and only then copies into the pooled direct ByteBuf. That extra staging copy is why the NioByteString serialization path costs ~22× more CPU (15.7% vs 0.7% of total), which lines up with the ~11% end-to-end throughput difference I measured (10,409 vs 11,637 MB/s). So the direct-buffer path isn't avoiding the wire-buffer copy — it's adding one. |
|
I see a significant performance degradation on the client side. The scenario is with MinIO Warp running with 10 concurrent threads through the S3 gateway: after patch: Definitely, the assumption that the patch should increase the performance is wrong. It reintroduces the client heap-pressure problem that HDDS-9843 specifically moved these buffers away from. |
@ss77892 what params did you use for warp test? Let me check if there is way to address this. |
|
WARP_REGION="us-east-1" |
@yandrey321 , Were you using Netty/gRPC or just a Mock? |
the mock is for network communication, so no Netty, but protobuf serialization is not mocked. I saw the similar async profiler call stacks for freon testing in a sense of overhead of NioByteString to protobuf copy |
Please try with netty. Testing with a mock cannot see the real thing. As @ss77892 mentioned, there is a significant performance degradation when using heap buffers. |
I can reproduce Sergey's findings with variation of the existing mock by introducing the back pressure and reducing the heap size for benchmark, when I do that I see ~30% of GC time increase and more objects retained in the heap. It seems like the best fix for this issue is to use Ratis data streaming. |
What changes were proposed in this pull request?
When ChunkBuffer allocates heap buffer UnsafeByteOperations.unsafeWrap() returns a BoundedByteString with a backing array, enabling a single System.arraycopy into the gRPC/Netty wire buffer instead of the slow byte-by-byte NioByteString path for direct buffers.
What is the link to the Apache JIRA
https://issues.apache.org/jira/browse/HDDS-15592
How was this patch tested?
Benchmark run on m3 mac/ARM with 14 cores producing the following results: Heap allocation is consistently +8% to +18% faster across all test.
4096 KB writes
2048 KB writes
1024 KB writes
512 KB writes
256 KB writes