Leadaxe:sing-box-lx 将上传写入改为批处理并减少 HTTP 延迟阻塞

在 sing-box-lx issue #58 中,Leadaxe 说明 `sendPacket` 不再同步等待每个 POST 响应,改为受限缓冲批量上传;HTTP/2/3 在请求体写出后继续下一 POST,后台校验响应,每流最多 16 个未完成请求,同时保留超时、熔断和重放语义。注入响应延迟的 fixture 测试在 50/100ms 条件下由 814/1615ms 降至 4.3/4.1ms;单元测试和真实 HTTP/2/3 已验证,live Xray/device 尚未验证,修复随下一稳定版 `v1.14.4-lx.25` 发布。

作者原文

Thanks for the precise report and the fixture — it reproduced as described, and the analysis holds: sendPacket awaited every POST's response inline, and writes were never batched.

Fixed in 420cbf73c4d35e0d6f9fbfb8a87ebe92da3d6e3c (lx, SPEC 141), along the lines of Xray's upload loop:

  • writes are collected in a bounded buffer of sc_max_each_post_bytes (drawn once per stream, as in Xray); Write returns once its bytes are buffered, a full buffer blocks the caller;
  • the upload loop takes the whole buffer as one sequenced POST and moves on as soon as the previous request body has been written (httptrace.WroteRequest — x/net http2 and quic-go both fire it); responses are validated and drained in the background, at most 16 outstanding per stream (the server's reorder window defaults to 30);
  • request-body order, sc_min_posts_interval_ms pacing, the per-POST timeout (now scaled with the payload: a batched POST can be a full megabyte), XMUX request accounting, the breaker and the GetBody replay are unchanged;
  • a failed POST (transport error, non-200, timeout) fails the stream at once through both Write and Read with the cause, instead of leaving a hole in the server's queue;
  • on HTTP/1.1 posts still go one at a time (net/http has no pipelining), but batching applies.

Your fixture on the fixed tree (darwin/arm64, Go 1.26.10):

| Injected response delay | before | after | POSTs |
|---|---:|---:|---:|
| 0 ms | 9.9 ms | 7.4 ms | 1 |
| 50 ms | 814 ms | 4.3 ms | 1 |
| 100 ms | 1615 ms | 4.1 ms | 1 |

The verification you suggested is a unit test now (TestPacketUpNextPostBeforePreviousResponse: POST 0's response is held after its body was consumed, POST 1 must arrive before it is released), plus the same fixture over a real HTTP/2 and a real HTTP/3 transport.

Not yet done: an upload against a live Xray server with controlled latency, and a device run — the pipelining against the server's uploadQueue is reasoned from hub.go/upload_queue.go, not measured. The fix ships with the next stable tag (v1.14.4-lx.25); I'll leave this open until it has been exercised against a real server. If you can run it against yours, that would close it.