lizining1231 opened a new pull request, #3706:
URL: https://github.com/apache/dubbo-go/pull/3706

   ### Description
   
   Fixes #3694
   
   Triple unary RPCs go through `duplexHTTPCall`, which allocates an `io.Pipe`, 
a per-request `makeRequest` goroutine and a `responseReady` signal per call. 
For small payloads (CPU-dominated hot path) this pipe hand-off machinery is 
pure overhead. On top of that, the fast path never actually ran end-to-end 
because of a protocol-selection defect:
   
   1. **Default protocol defect**: `newClientManager` only passed 
`tri.WithTriple()` on the HTTP/1.1 branch; HTTP/2 (the actual default 
transport) silently fell back to the gRPC protocol , the 
`triple_protocol.Client` default inherited from connect-go. Every end-to-end 
benchmark therefore measured the gRPC compatibility path, and a fast path 
living on `tripleClient.NewConn` was unreachable.
   2. **Per-request pipe overhead**: each unary call creates an `io.Pipe` 
writer task, spawns a `makeRequest` goroutine, and blocks on a `responseReady` 
close-signal hand-off, two execution contexts where one synchronous caller 
context suffices.
   
   ### Changes
   
   **1. Config switch: add an opt-in `UnaryFastPath` toggle, off by default**
   
   `global/triple_config.go`:
   - Add `UnaryFastPath bool` to `TripleConfig` (yaml/json/property mapped) and 
copy it in `Clone`.
   
   `protocol/triple/triple_protocol/option.go` / `client.go` / `protocol.go`:
   - Add `WithUnaryFastPath()` client option and propagate it through 
`clientConfig` / `protocolClientParams` into `NewConn`.
   
   **2. Fast path implementation: replace the duplex pipe machinery with a 
synchronous call**
   
   `protocol/triple/triple_protocol/unary_fastpath.go` (new):
   - `unaryFastPathCall` performs the unary round trip synchronously in the 
caller goroutine: no `io.Pipe`, no `makeRequest` goroutine; the `responseReady` 
signal is resolved synchronously instead of from a separate goroutine;
   - Uses pooled buffers, declares `Content-Length`, reads the response 
directly;
   - The write side (`Send` / `CloseRequest`) is mutex-guarded; `RequestHeader` 
is safe to call concurrently because headers are never mutated after 
construction, so the `StreamingClientConn` concurrency-safe is preserved.
   
   `protocol/triple/triple_protocol/protocol_triple.go`:
   - `tripleClient.NewConn` selects `newUnaryFastPathCall` for unary stream 
types when the switch is on; streaming keeps the duplex path.
   
   `protocol/triple/client.go`:
   - Hoist `tri.WithTriple()` so HTTP/1.1, HTTP/2 and HTTP/3 all explicitly 
select the Triple protocol (HTTP/2 previously fell back to gRPC, making the 
fast path unreachable), and gate `tri.WithUnaryFastPath()` on the config switch.
   
   **3. Tests: conn routing, production bench baseline, body round-trip and 
race coverage**
   
   `protocol/triple/triple_protocol/unary_fastpath_conn_type_test.go`:
   - Asserts `NewConn` routes unary to the fast path and streaming to duplex.
   
   `protocol/triple/triple_protocol/unary_fastpath_bench_test.go`:
   - `BenchmarkUnaryFastPathProduction` drives the production fast path with 
`WithTriple + WithUnaryFastPath` for an A/B baseline against duplex.
   
   `protocol/triple/triple_protocol/unary_fastpath_body_test.go`:
   - Request body read/write round-trips, `Content-Length` declaration and 
pooled buffer reuse.
   
   `protocol/triple/triple_protocol/unary_fastpath_concurrency_test.go`:
   - Concurrent `Send` / `RequestHeader` / `CloseRequest` stay race-free under 
`-race`.
   
   ### Test
   
   `unary_fastpath_conn_type_test.go`
   
   | Test | Description |
   |---|---|
   | `TestUnaryFastPathNewConnType` | Switch on routes unary calls to the fast 
path, off falls back to duplex, streaming always uses duplex |
   
   `unary_fastpath_behavior_test.go`
   
   | Test | Description |
   |---|---|
   | `TestUnaryFastPathWriteAccumulates` | Write appends into the pooled buffer 
without touching the network; CloseWrite hands the whole payload to the 
transport exactly once with an exact Content-Length |
   | `TestUnaryFastPathEmptyBodyUsesNoBody` | An empty request body uses 
`http.NoBody` with Content-Length=0, avoiding chunked encoding and background 
writes |
   | `TestUnaryFastPathConcurrentReadClose` | 8 readers racing 8 closes, 
verified race-free under `-race` |
   | `TestUnaryFastPathRequestHeaderConcurrent` | RequestHeader races with 
Write and CloseWrite safely, per the streaming client conn contract |
   | `TestUnaryFastPathWriteAfterTransportError` | After a transport failure 
writes are rejected with io.EOF and Read surfaces CodeUnavailable |
   | `TestUnaryFastPathSetErrorRejectsWrite` | A stored error rejects writes 
before the body is sent, surfacing the concrete error |
   | `TestUnaryFastPathWireConsistent` | The fast path wire bytes are identical 
to duplex, so the protocol does not drift |
   | `TestUnaryFastPathContextCancel` | Context cancellation aborts the 
in-flight request and Read surfaces CodeCanceled |
   | `TestUnaryFastPathEndToEnd` | End-to-end against a real HTTP/2 server 
through the real NewConn entry, covering header/trailer interop and response 
reading |
   | `TestUnaryFastPathProtoJSONCodec` | The JSON codec works end to end, 
matching duplex behavior to keep codec generality |
   | `TestUnaryFastPathGRPCKeepsDuplex` | gRPC protocol calls are unaffected by 
the switch and keep using duplex even when enabled, preserving gRPC behavior |
   
   `unary_fastpath_body_test.go`
   
   | Test | Description |
   |---|---|
   | `TestUnaryFastPathBodyReturnOnAsyncClose` | Close returns the buffer to 
the pool exactly once, Read returns EOF afterwards, and pooled reuse stays 
clean |
   | `TestUnaryFastPathBodyAbortServer` | A non-2xx early response still 
returns the buffer to the pool, surfaces CodePermissionDenied, and propagates 
the error metadata |
   
   `unary_fastpath_concurrency_test.go`
   
   | Test | Description |
   |---|---|
   | `TestUnaryFastPathWriteCloseConcurrent` | Concurrent Write/CloseWrite over 
30 rounds with no torn body observed by the server, pinning the write-side lock 
boundary |
   | `TestUnaryFastPathWriteAfterClose` | Write after bodySent returns io.EOF, 
mirroring duplex close semantics |
   
   `unary_fastpath_bench_test.go`
   
   | Benchmark | Description |
   |---|---|
   | `BenchmarkUnaryDuplex` | Control group: the generated client with the fast 
path disabled (duplex) |
   | `BenchmarkUnaryFastPathProduction` | Treatment group: differs from the 
control only by the WithUnaryFastPath option |
   
   ### Validation
   
   # Dubbo-Go Triple Protocol Performance Benchmark Summary Report
   
   ### 128 bytes payload
   
   | Metric | Concurrency | duplex | fastpath | Improvement |
   | --- | --- | --- | --- | --- |
   | QPS | 50 | 5,329.3 | 6,997.9 | +31.3% ↑ |
   | QPS | 100 | 5,409.2 | 6,024.1 | +11.4% ↑ |
   | P99 latency (ms) | 50 | 15.99 | 12.67 | -20.8% ↑ |
   | P99 latency (ms) | 100 | 30.58 | 29.74 | -2.7% ↑ |
   
   ### 1024 bytes payload
   
   | Metric | Concurrency | duplex | fastpath | Improvement |
   | --- | --- | --- | --- | --- |
   | QPS | 50 | 4,300.3 | 5,196.4 | +20.8% ↑ |
   | QPS | 100 | 4,368.5 | 5,605.8 | +28.3% ↑ |
   | P99 latency (ms) | 50 | 20.40 | 18.22 | -10.7% ↑ |
   | P99 latency (ms) | 100 | 37.48 | 26.95 | -28.1% ↑ |
   
   ### 16384 bytes payload
   
   | Metric | Concurrency | duplex | fastpath | Improvement |
   | --- | --- | --- | --- | --- |
   | QPS | 50 | 2,584.2 | 2,871.6 | +11.1% ↑ |
   | QPS | 100 | 2,428.2 | 2,815.7 | +16.0% ↑ |
   | P99 latency (ms) | 50 | 37.28 | 30.90 | -17.1% ↑ |
   | P99 latency (ms) | 100 | 81.24 | 69.07 | -15.0% ↑ |
   
   ### 1048576 bytes (1MiB) payload
   
   | Metric | Concurrency | duplex | fastpath | Improvement |
   | --- | --- | --- | --- | --- |
   | QPS | 50 | 101.3 | 101.7 | +0.4% ↑ |
   | QPS | 100 | 104.3 | 114.4 | +9.8% ↑ |
   | P99 latency (ms) | 50 | 1,430.17 | 1,479.12 | +3.4% ↓ |
   | P99 latency (ms) | 100 | 3,179.88 | 2,874.32 | -9.6% ↑ |
   
   ## benchstat significance test (count=10)
   
   ```bash
   goos: linux
   goarch: amd64
   pkg: dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol
   cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
                   │ duplex10_1mib.txt │          fastpath10_1mib.txt          │
                   │      sec/op       │        sec/op     vs base             │
   Unary/128B-12        147.1µ ±  8%      117.6µ ± 11%  -20.08% (p=0.000 n=10)
   Unary/1024B-12       169.9µ ± 18%      106.0µ ± 30%  -37.59% (p=0.000 n=10)
   Unary/16384B-12      226.1µ ± 26%      173.1µ ± 12%  -23.43% (p=0.000 n=10)
   Unary/1MiB-12        3.662m ± 19%      3.450m ± 10%        ~ (p=0.912 n=10)
   geomean              379.3µ            293.8µ        -22.55%
   ```
   
   *Note: the names above come from a unified `BenchmarkUnary` run; in the 
current
   codebase the control is `BenchmarkUnaryDuplex` and the treatment is
   `BenchmarkUnaryFastPathProduction`.*
   
   | Payload | duplex (sec/op) | fastpath (sec/op) | Change | p-value | 
Significance |
   | --- | --- | --- | --- | --- | --- |
   | 128B | 147.1µs | 117.6µs | -20.08% | 0.000 | Significant |
   | 1KiB | 169.9µs | 106.0µs | -37.59% | 0.000 | Significant |
   | 16KiB | 226.1µs | 173.1µs | -23.43% | 0.000 | Significant |
   | 1MiB | 3.662ms | 3.450ms | -5.8% | 0.912 | Not significant |
   | geomean | 379.3µs | 293.8µs | -22.55% | - | Overall positive |
   
   # -race test
   ```bash
   lizining@Y:~/projects/dubbo-go$ go test -race ./protocol/triple/... 2>&1 | 
grep -Ev "no test files|\(cached\)"
   go test -race -count=1 ./protocol/triple/triple_protocol/
   # 3 rounds of writer-side concurrency testing
   go test -race -count=3 -run 
'TestUnaryFastPathWriteCloseConcurrent|TestUnaryFastPathSetErrorRejectsWrite|TestUnaryFastPathWriteAfterTransportError|TestUnaryFastPathRequestHeaderConcurrent|TestUnaryFastPathConcurrentReadClose|TestUnaryFastPathWriteAfterClose|TestUnaryFastPathGRPCKeepsDuplex|TestUnaryFastPathEndToEnd'
 ./protocol/triple/triple_protocol/
   go test -race ./protocol/triple/... 2>&1 | grep FAIL || echo "无失败"
   ok      dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol    
31.804s
   ok      dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol    
1.457s
   无失败
   lizining@Y:~/projects/dubbo-go$ 
   ```
   
   # pprof Benchmark
   
   **Scenario:** Triple unary · 128B · 100 concurrent requests
   
   The fast path removes the per-request `io.Pipe` pipe, the standalone 
`makeRequest` goroutine, and the `responseReady` channel handoff. Total 
goroutines decrease by 31%, and `syscall`/`futex` CPU usage drops.
   
   ### Goroutine Profile (310 → 213, -31%)
   
   duplex
   <img width="1600" height="900" alt="image" 
src="https://github.com/user-attachments/assets/23d1ff6e-1b44-4bc6-b47f-0e1a019d375a";
 />
   fastpath
   <img width="1600" height="1040" alt="image" 
src="https://github.com/user-attachments/assets/a305c789-981c-4f7f-8e47-4eb5920318a3";
 />
   
   | | duplex | fastpath |
   | :--- | :---: | :---: |
   | Total goroutines | 310 | 213 |
   | http2 connection-related frames | 26 | 16 |
   | pipe-related frames | 3 | 1 |
   
   ### CPU Profile
   duplex
   <img width="1600" height="820" alt="image" 
src="https://github.com/user-attachments/assets/2ff6cc33-02c3-41a8-a768-490f01f8166a";
 />
   fastpath
   <img width="1600" height="1060" alt="image" 
src="https://github.com/user-attachments/assets/f32230c3-d174-4722-b423-59016ab9112c";
 />
   
   | Hotspot | duplex | fastpath | Change |
   | :--- | :---: | :---: | :---: |
   | `syscall.Syscall6` | 28.82% | 24.64% | **-4.2%** |
   | `runtime.futex` | 12.41% | 10.25% | **-2.2%** |
   
   ### Checklist
   - [x] I confirm the target branch is `develop`
   - [x] I have run `make fmt` to format my code
   - [x] I have run `make test` to run local tests
   - [x] I have added tests that prove my fix is effective or that my feature 
works


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to