feat(pullsync): delivery batching performance analysis - #5540
Conversation
|
One comment regarding TCP (and UDP) Dynamics, a standard internet packet (MTU) is about 1500 bytes. A single 4 KB chunk already requires 3 TCP packets. Whether you flush chunks to the OS individually or in a batch, the OS TCP stack will still stream them out in 1500-byte packets at the maximum rate the network allows. |
|
Until the real cluster measurements are performed, I have run a benchmarks on local machine, but using The benchmark that is executed is: go test -bench=BenchmarkDeliveryLibP2P/n=120 -benchmem -benchtime=5x ./pkg/pullsyncResults without added latency: Than a latency is set with: sudo dnctl pipe 1 config delay 200ms
echo "dummynet out from 127.0.0.1 to 127.0.0.1 pipe 1" | sudo pfctl -f - -eAnd the benchmark is run again, the results are: If you want to reproduce the benchmarks on the local machine, do not forget to remove the latency: sudo pfctl -F all -d
sudo dnctl -q flushThe results show that the latency has an impact on the performance of the pullsync algorithm, between two runs (with and without latency), but there are no differences between the batched and unbatched version of the algorithm. he latency penalty in pullsync is a TCP/IP layer phenomenon governed by the initial handshake and bandwidth delay, not an application-level delivery issue. Both implementations suffer from the exact same latency penalty (TCP Slow Start): When a 200ms delay is injected, both versions see their delivery time increase from ~0.38s to ~1.62s. This ~1.2-second penalty is caused by the initial protocol handshake (the Get -> Offer -> Want) and the TCP congestion window (slow start) ramping up across a high-latency link. Batching application-level data does not improve stream performance: With 200ms of latency, the Batched implementation (1.622s) and the v2.8.1 implementation (1.625s) are essentially identical. This proves that because legacy pullsync already streams data into the socket buffer rather than waiting for individual application-level ACKs, application-level batching provides no benefit. In both cases, the OS segments the stream into identical 1500-byte MTU packets and transmits them at the exact same rate. While providing no speed benefit, the Batched implementation consistently consumes more memory (2.4 MB/op) compared to the legacy v2.8.1 implementation (1.7 MB/op). This is because the batched version must allocate and hold large arrays of chunk data in memory simultaneously before writing them to the socket buffer. In this test, the batching protocol change introduces memory overhead without delivering any network speedup compared to the bee v2.8.1 pullsync implementation. |
Pullsync Batching Performance Analysis
This PR gives the overview of the performance testing and profiling conducted to evaluate the proposed batched chunk delivery (
DeliveryBatch) against the legacy single-message delivery (v2.8.1) in thepullsyncprotocol. It should not be merged, as it is meant to reproduce the results, but it may be used for potential improvements.The goal is to identify if the batching of delivery messages in the pullsync protocol can result to a speedup in overall deliveries.
To simulate real-world networking overhead accurately, a benchmark was written that spins up real
libp2pnodes locally, multiplexes streams, and transmits chunks between a client and server.This approach gives a convenient testing, benchmarking and profiling, but the pullsync performance can be measured on the real network as well.
There are two benchmarks:
Benchmark
There are to benchmarks, one for he currently released bee v2.8.1 pullsync implementation and one for the proposed batched implementation. Both of them with 25 and 120 chunks delivered. Since the max batch size can be 25 chunks because of the limitation of 128KB payload size of protobuf. There is a possiblity to raise that limit, but with a danger to expose some vulnerability in the network.
Run the libp2p benchmark for 100 iterations tracking memory allocations:
go test -v -run=NONE -bench=BenchmarkDeliveryLibP2P -benchtime=100x -benchmem ./pkg/pullsync/delivery_test.go ./pkg/pullsync/pullsync_test.goProfiling
Ran Go's built-in
pproftools against both theBatchedandv2.8.1branches of the benchmark independently, to identify any bottlenecks and/or improvements.Generate CPU and Memory profiles for the Batched implementation:
Generate CPU and Memory profiles for the legacy v2.8.1 implementation:
Analyze:
CPU Profile
Both profiles reveal an identical bottleneck. In both implementations,
runtime.pthread_cond_wait,runtime.usleep, andruntime.pthread_cond_signalconsume the majority of all execution samples. This indicates the CPU is blocking onlibp2pnetwork I/O synchronization than performing protobuf serialization or algorithmic work.Batched:
v2.8.1 (Single Message):
Memory Profile
Batched:
v2.8.1 (Single Message):
Both the batched and unbatched implementations perform nearly identically over
libp2p(~99.07 msvs~99.08 msforn=25and~475.34 msvs~475.29 msforn=120).The separated CPU profiles reveal why this is the case: the top CPU consumers in both implementations are OS-level thread blocking operations (
runtime.pthread_cond_wait,runtime.usleep,runtime.kevent). These account for over 60-80% of all CPU samples depending on the run. Because the program is almost entirely bottlenecked by waiting forlibp2pstream data to traverse network sockets, any optimizations gained by batching protobuf structures in memory are negligible compared to the network latency.The memory profile shows that chunk parsing and cryptographic hashing (
golang.org/x/crypto/sha3.(*state).Sumandpb.(*Delivery).Unmarshal) dominate memory allocations.Even if the delimitedReaderMaxSize is raised to 1MB, allowing larger batches, no performance gains are visible.
Conclusion
Introducing a breaking protocol change to batch protobuf messages yields no statistical CPU performance gain over the multiplexer latency, so the simpler, single-message streaming approach is preferred.
I encourage everyone interested to validate the findings.
Checklist
Description
Open API Spec Version Changes (if applicable)
Motivation and Context (Optional)
Related Issue (Optional)
Screenshots (if appropriate):
AI Disclosure