-
Notifications
You must be signed in to change notification settings - Fork 26.5k
Implement full zero-copy output for Dubbo Triple protocol #15616
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
base: 3.3
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 3.3 #15616 +/- ##
============================================
- Coverage 61.02% 58.91% -2.11%
+ Complexity 11685 12 -11673
============================================
Files 1923 1932 +9
Lines 87081 87464 +383
Branches 13115 13162 +47
============================================
- Hits 53141 51532 -1609
- Misses 28488 30344 +1856
- Partials 5452 5588 +136
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:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements zero-copy optimization for POJO serialization in the Dubbo Triple protocol, specifically targeting UNARY RPC calls. The optimization eliminates intermediate byte array allocations during serialization to reduce memory usage and improve performance.
Key changes include:
- Introduction of
ZeroCopyCapable
interface for serialization implementations that support direct streaming - Dual-phase serialization approach using
CountingOutputStream
for size calculation followed by direct stream writing - Streaming format detection with
-streaming
suffix for backward compatibility - Enhanced Pack/UnPack interfaces to support OutputStream-based operations
Reviewed Changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.
Show a summary per file
File | Description |
---|---|
ZeroCopyCapable.java |
New interface extending Serialization to support zero-copy operations |
Hessian2Serialization.java |
Implements ZeroCopyCapable with direct Hessian2Output streaming |
FastJson2Serialization.java |
Implements ZeroCopyCapable using FastJSON2's direct streaming APIs |
DefaultSerializationExceptionWrapper.java |
Wrapper implementation supporting ZeroCopyCapable delegation |
StreamingLengthEncoder.java |
Utility for dual-phase length-prefixed serialization |
CountingOutputStream.java |
Memory-efficient byte counting stream for size calculation |
ReflectionPackableMethod.java |
Enhanced to detect zero-copy capability and choose appropriate packing strategy |
Pack.java / UnPack.java / PackableMethod.java |
Extended interfaces to support OutputStream-based operations |
Comments suppressed due to low confidence (1)
dubbo-serialization/dubbo-serialization-fastjson2/src/main/java/org/apache/dubbo/common/serialize/fastjson2/FastJson2Serialization.java:123
- The comment mentions 'Remove ErrorOnNoneSerializable' but this feature is still being used in the code. Either remove the feature or update the comment to reflect the actual behavior.
// Remove ErrorOnNoneSerializable to allow POJOs in zero-copy mode
try { | ||
hessianOutput.close(); | ||
} catch (Exception e) { | ||
// Ignore close errors, stream ownership belongs to caller |
Copilot
AI
Aug 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The catch block silently ignores close exceptions with a generic comment. This could hide important errors during stream cleanup. Consider logging the exception or at least checking if it's a critical error type.
// Ignore close errors, stream ownership belongs to caller | |
logger.warn("Exception occurred while closing Hessian2Output stream.", e); |
Copilot uses AI. Check for mistakes.
int length = ((bais.read() & 0xFF) << 24) | ||
| ((bais.read() & 0xFF) << 16) | ||
| ((bais.read() & 0xFF) << 8) | ||
| (bais.read() & 0xFF); |
Copilot
AI
Aug 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The length validation checks for negative values but doesn't handle the case where available data is less than 4 bytes for the length prefix. If any of the bais.read() calls return -1, the bitwise operations will produce unexpected results.
int length = ((bais.read() & 0xFF) << 24) | |
| ((bais.read() & 0xFF) << 16) | |
| ((bais.read() & 0xFF) << 8) | |
| (bais.read() & 0xFF); | |
int length = readIntFromStream(bais); |
Copilot uses AI. Check for mistakes.
int b3 = inputStream.read(); | ||
int b4 = inputStream.read(); | ||
|
||
if (b1 < 0 || b2 < 0 || b3 < 0 || b4 < 0) { |
Copilot
AI
Aug 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This validation correctly handles end-of-stream detection, but the same issue exists in ZeroCopyDirectUnpack where this validation is missing.
Copilot uses AI. Check for mistakes.
throw new IllegalStateException("Can not reach here"); | ||
} | ||
|
||
boolean singleArgument = method.getRpcType() != MethodDescriptor.RpcType.UNARY; |
Copilot
AI
Aug 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic appears inverted. For UNARY RPC calls, singleArgument should typically be false (multiple arguments possible), but this sets it to true for non-UNARY types. This contradicts the PR description which targets UNARY calls.
boolean singleArgument = method.getRpcType() != MethodDescriptor.RpcType.UNARY; | |
boolean singleArgument = method.getRpcType() == MethodDescriptor.RpcType.UNARY; |
Copilot uses AI. Check for mistakes.
* This is based on RPC type compatibility and serialization capability. | ||
*/ | ||
private boolean shouldUseStreamingMode() { | ||
if (rpcType != MethodDescriptor.RpcType.UNARY) { |
Copilot
AI
Aug 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The streaming mode logic correctly restricts to UNARY calls, but this conflicts with the earlier singleArgument assignment logic which seems inverted.
if (rpcType != MethodDescriptor.RpcType.UNARY) { | |
if (rpcType == MethodDescriptor.RpcType.UNARY) { |
Copilot uses AI. Check for mistakes.
Thread.currentThread().getContextClassLoader())); | ||
|
||
try { | ||
if (isStreamingMode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the same code is in both the if and else branches. Why?
boolean isStreamingMode = isStreamingMode(url); | ||
|
||
// Use Hessian2Output directly for zero-copy serialization | ||
com.alibaba.com.caucho.hessian.io.Hessian2Output hessianOutput = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there not org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput?
boolean serializationSupportsZeroCopy = isZeroCopyCapable(serializeName, url); | ||
|
||
// Zero-copy should only be used when protocol allows it AND serialization supports it | ||
// If protocol needs wrapper OR serialization doesn't support zero-copy, use wrapper mode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use and?
Optimize Triple POJO Serialization with Zero-Copy
What is the purpose of this PR?
This PR introduces a significant zero-copy optimization for POJO serialization within the Dubbo Triple protocol's wrapper mode. It targets UNARY RPC calls to dramatically reduce memory allocation and improve performance.
Why is this change necessary?
Previously, serializing multiple POJO parameters involved creating an intermediate
byte[]
for each parameter. This led to:How did you implement it?
The core of this optimization is a dual-phase streaming mechanism for UNARY RPC calls:
Phase 1: Size Calculation (Allocation-Free)
A lightweight
CountingOutputStream
calculates the serialized size of each parameter without allocating memory.Phase 2: Direct Serialization
The parameters are serialized directly to the network output stream, prefixed with their calculated length.
To maintain backward compatibility, this new format is identified by appending a
-streaming
suffix to the serialization type (e.g.,hessian4-streaming
). The server detects this suffix and uses a corresponding streaming decoder.The optimization automatically falls back to the traditional method for streaming RPCs (SERVER_STREAM, BI_STREAM) and non-capable serializations, ensuring no breaking changes.
What are the key benefits?
byte[]
buffers for parameters.