diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa8a10..ec9424a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - native 响应实现 `http.Flusher`:HTTP/1.1 流式响应自动使用 chunked framing,普通响应继续自动生成 `Content-Length`。 - native 在长 Handler 运行期间监测客户端断开并取消请求 context,流式写失败同步取消;短请求延迟启动监测,管线数据保留给下一次请求。补充断开、监测退出和长连接复用验收。 - native 增加 `ServeTLS` / `RunTLS` 和 `TLSConfig` 启动入口,App.RunWith 可以直接启用 TLS;补充真实 TLS 客户端握手、请求和优雅退出验收。 +- native 请求补齐 `Request.TLS` 连接状态;Linux/arm64 容器通过全量 race、vet、build,以及 TLS 并发、Keep-Alive、慢握手和连接上限回收验收。 - 补充慢请求体超时、大请求体和同连接管线请求验收,确认请求体未完整读取时不会进入业务 Handler,连续请求不会串线。 - 补充 `MaxConnections` 满载拒绝与槽位释放验收,确认请求完成后连接容量可再次使用。 - 增加 8 并发、800 次建连/断连的状态池 churn 验收,覆盖连接对象回收与容量复用。 diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 6dbf7aa..2822ab8 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -54,6 +54,12 @@ go run ./internal/perfcheck --duration 2s --count 3 --output /tmp/goOmniPerf.jso ## benchmark 口径修正 +## TLS 验收记录 + +macOS arm64 本机的 5 秒 TLS 矩阵见 [tlsBaseline.json](performance/tlsBaseline.json):1 / 32 / 128 并发和 128 / 1,024 条空闲连接均为 0 错误;native 在 128 并发约 114,232 req/s,1,024 条空闲 TLS 连接的服务端堆增量约 8.94 MiB,连接关闭后 goroutine 回到基线。 + +Linux arm64 容器使用 Go 1.25.14、GOMAXPROCS=4 运行 3 秒矩阵,见 [tlsBaselineLinux.json](performance/tlsBaselineLinux.json):1 / 32 / 128 并发和 128 / 512 条空闲连接均为 0 错误;native 在 128 并发约 99,876 req/s,512 条空闲 TLS 连接的服务端堆增量约 4.47 MiB,连接关闭后 goroutine 回到基线。容器验证同时通过全量 race、vet、build。 + `BenchmarkTransportTCPApplicationRequest/{nethttp,native}/concurrency{1,32,128}` 使用同一测试入口、客户端、应用与响应检查。原先独立的 `BenchmarkNativeApplicationRequest` 已合并,避免两个入口配置漂移。`verifyPerformance.py` 会一并运行。 并行 benchmark 的 **`ns/op` 是总经过时间除以请求数,反映吞吐成本,不是单请求延迟**。`B/op` / `allocs/op` 统计同一进程中的客户端与服务端总分配。此前报告的 native 15.4–16.7 µs/op、约 7.8 KB/op、100 allocs/op 应按这个含义理解,不能作为服务端延迟或独立内存指标。 diff --git a/ROADMAP.md b/ROADMAP.md index 3902717..746cf16 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -44,7 +44,7 @@ - native 已支持受 `MaxBodyBytes` 约束的 `Content-Length` / chunked 请求体,仍使用标准 `http.Request` / `http.ResponseWriter` 作为桥接;解析器、连接抽象和生产压测将在后续阶段继续下沉。 - native 响应已支持标准 `http.Flusher`;HTTP/1.1 流式响应使用 chunked framing,普通响应继续走 `Content-Length`,HTTP/1.0 回退到关闭连接分帧。 - native 长请求已补客户端断开及流式写失败取消,监测退出后再复用 Reader;已验收普通/流式断开、慢 Handler 后连续 Keep-Alive 和管线数据不被误消费。短请求延迟启动监测,已有管线数据时取消依赖后续写失败或 Handler 返回。 -- 已补充慢请求体超时、大请求体、HTTP/1.0 Keep-Alive、HEAD/204、管线请求和基础 TLS 客户端验收;Linux、慢客户端规模、TLS 吞吐和持续运行压测仍待在目标环境验证。 +- 已补充慢请求体超时、大请求体、HTTP/1.0 Keep-Alive、HEAD/204、管线请求和基础 TLS 客户端验收;Linux/arm64 容器已通过全量 race、vet、build,以及 3 秒 TLS 并发/空闲连接矩阵。原始报告见 [tlsBaselineLinux.json](performance/tlsBaselineLinux.json);慢客户端规模和持续生产压测仍待真实部署环境验证。 - 已验证 `MaxConnections` 满载时快速拒绝新连接、请求完成后释放容量;后续仍需在目标部署环境观察文件描述符、内核 socket 缓冲和反向代理联动。 - 已用 8 个并发 worker 完成 800 次建连/断连 race 验收;真实部署仍需补文件描述符和跨平台 socket 行为观测。 - 已具备全量 race、vet、build 验收;生产压测、长连接规模和真实部署验证仍待完成。 diff --git a/performance/tlsBaseline.json b/performance/tlsBaseline.json new file mode 100644 index 0000000..5066f95 --- /dev/null +++ b/performance/tlsBaseline.json @@ -0,0 +1,592 @@ +{ + "createdAt": "2026-09-20T21:27:15Z", + "goVersion": "go1.25.0", + "platform": "darwin/arm64", + "gomaxprocs": 12, + "duration": "5s", + "tls": true, + "notes": [ + "Loopback HTTP/1.1 health response, fixed requestId, optional TLS, no access logs or database.", + "Each case starts a fresh server process; load client runs in the parent process on the same machine.", + "Closed-loop concurrency; throughput is successful responses / actual elapsed time, including drain.", + "Latency percentiles include failures and are histogram upper bounds with \u003c=6.25% rounding.", + "Memory is server-only runtime.MemStats (not RSS or kernel socket memory); sampling interval 200ms.", + "Idle memory measured after a forced GC; delta includes heap growth amortized across connections, excluding stacks.", + "After is after closing clients and one forced GC; afterReclaim is a second GC to observe pooled-cache release.", + "Native limits: 8192 connections / 4096 active requests. These cases do not benchmark overload rejection." + ], + "results": [ + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 1, + "run": 1, + "requests": 86935, + "errors": 0, + "elapsedSeconds": 5.000022, + "requestsPerSecond": 17386.92349753661, + "p50Millis": 0.0512, + "p95Millis": 0.1024, + "p99Millis": 0.180224, + "serverBytesPerRequest": 4514.537988152068, + "serverAllocsPerRequest": 49.1346753321447, + "sampledPeakHeapAlloc": 3349712, + "before": { + "heapAlloc": 1678936, + "heapInuse": 3661824, + "stackInuse": 655360, + "totalAlloc": 3167032, + "mallocs": 24353, + "numGc": 2, + "goroutines": 3, + "connections": 1, + "accepted": 1 + }, + "during": { + "heapAlloc": 3337216, + "heapInuse": 5341184, + "stackInuse": 753664, + "totalAlloc": 395638392, + "mallocs": 4295876, + "numGc": 238, + "goroutines": 3, + "connections": 1, + "accepted": 1 + }, + "after": { + "heapAlloc": 1774168, + "heapInuse": 3792896, + "stackInuse": 688128, + "totalAlloc": 395642616, + "mallocs": 4295888, + "numGc": 239, + "goroutines": 2, + "connections": 0, + "accepted": 1 + }, + "afterReclaim": { + "heapAlloc": 1741008, + "heapInuse": 3620864, + "stackInuse": 688128, + "totalAlloc": 395644800, + "mallocs": 4295894, + "numGc": 240, + "goroutines": 2, + "connections": 0, + "accepted": 1 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 1, + "run": 1, + "requests": 91798, + "errors": 0, + "elapsedSeconds": 5.000019125, + "requestsPerSecond": 18359.52977479861, + "p50Millis": 0.049152, + "p95Millis": 0.090112, + "p99Millis": 0.16384, + "serverBytesPerRequest": 4635.560426153075, + "serverAllocsPerRequest": 60.11798731998518, + "sampledPeakHeapAlloc": 3267488, + "before": { + "heapAlloc": 1653880, + "heapInuse": 3678208, + "stackInuse": 622592, + "totalAlloc": 3145792, + "mallocs": 24330, + "numGc": 2, + "goroutines": 4, + "connections": 1, + "accepted": 1 + }, + "during": { + "heapAlloc": 1828360, + "heapInuse": 4022272, + "stackInuse": 688128, + "totalAlloc": 428680968, + "mallocs": 5543041, + "numGc": 264, + "goroutines": 4, + "connections": 1, + "accepted": 1 + }, + "after": { + "heapAlloc": 1687208, + "heapInuse": 3817472, + "stackInuse": 655360, + "totalAlloc": 428685256, + "mallocs": 5543054, + "numGc": 265, + "goroutines": 3, + "connections": 0, + "accepted": 1 + }, + "afterReclaim": { + "heapAlloc": 1668072, + "heapInuse": 3588096, + "stackInuse": 655360, + "totalAlloc": 428687440, + "mallocs": 5543060, + "numGc": 266, + "goroutines": 3, + "connections": 0, + "accepted": 1 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 32, + "run": 1, + "requests": 443035, + "errors": 0, + "elapsedSeconds": 5.000301084, + "requestsPerSecond": 88601.66469127762, + "p50Millis": 0.278528, + "p95Millis": 1.015808, + "p99Millis": 1.507328, + "serverBytesPerRequest": 4524.947022244292, + "serverAllocsPerRequest": 49.153904319071856, + "sampledPeakHeapAlloc": 4957328, + "before": { + "heapAlloc": 2174976, + "heapInuse": 5406720, + "stackInuse": 1277952, + "totalAlloc": 4859520, + "mallocs": 39778, + "numGc": 4, + "goroutines": 34, + "connections": 32, + "accepted": 32 + }, + "during": { + "heapAlloc": 3437936, + "heapInuse": 6717440, + "stackInuse": 2490368, + "totalAlloc": 2009569424, + "mallocs": 21816678, + "numGc": 1167, + "goroutines": 34, + "connections": 32, + "accepted": 32 + }, + "after": { + "heapAlloc": 2301792, + "heapInuse": 4833280, + "stackInuse": 2195456, + "totalAlloc": 2009575408, + "mallocs": 21816709, + "numGc": 1168, + "goroutines": 2, + "connections": 0, + "accepted": 32 + }, + "afterReclaim": { + "heapAlloc": 1983832, + "heapInuse": 3915776, + "stackInuse": 2195456, + "totalAlloc": 2009577336, + "mallocs": 21816714, + "numGc": 1169, + "goroutines": 2, + "connections": 0, + "accepted": 32 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 32, + "run": 1, + "requests": 422203, + "errors": 0, + "elapsedSeconds": 5.0001875, + "requestsPerSecond": 84437.43359624014, + "p50Millis": 0.294912, + "p95Millis": 1.015808, + "p99Millis": 1.507328, + "serverBytesPerRequest": 4655.5327176737255, + "serverAllocsPerRequest": 60.24485614739829, + "sampledPeakHeapAlloc": 3953128, + "before": { + "heapAlloc": 1991704, + "heapInuse": 5013504, + "stackInuse": 1081344, + "totalAlloc": 4644928, + "mallocs": 39578, + "numGc": 3, + "goroutines": 35, + "connections": 32, + "accepted": 32 + }, + "during": { + "heapAlloc": 2969168, + "heapInuse": 6266880, + "stackInuse": 1212416, + "totalAlloc": 1970224808, + "mallocs": 25475137, + "numGc": 1687, + "goroutines": 35, + "connections": 32, + "accepted": 32 + }, + "after": { + "heapAlloc": 2053344, + "heapInuse": 5062656, + "stackInuse": 851968, + "totalAlloc": 1970231512, + "mallocs": 25475175, + "numGc": 1688, + "goroutines": 3, + "connections": 0, + "accepted": 32 + }, + "afterReclaim": { + "heapAlloc": 1754136, + "heapInuse": 3792896, + "stackInuse": 851968, + "totalAlloc": 1970233696, + "mallocs": 25475181, + "numGc": 1689, + "goroutines": 3, + "connections": 0, + "accepted": 32 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 128, + "run": 1, + "requests": 564759, + "errors": 0, + "elapsedSeconds": 5.000520667, + "requestsPerSecond": 112940.03916972512, + "p50Millis": 0.8192, + "p95Millis": 3.2768, + "p99Millis": 4.718592, + "serverBytesPerRequest": 4499.995148373023, + "serverAllocsPerRequest": 49.13783401415471, + "sampledPeakHeapAlloc": 7449240, + "before": { + "heapAlloc": 3409760, + "heapInuse": 6938624, + "stackInuse": 2457600, + "totalAlloc": 9389504, + "mallocs": 81651, + "numGc": 8, + "goroutines": 118, + "connections": 116, + "accepted": 116 + }, + "during": { + "heapAlloc": 5261072, + "heapInuse": 9068544, + "stackInuse": 3276800, + "totalAlloc": 2550802264, + "mallocs": 27832685, + "numGc": 775, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 3142160, + "heapInuse": 6086656, + "stackInuse": 2162688, + "totalAlloc": 2550814744, + "mallocs": 27832752, + "numGc": 776, + "goroutines": 2, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 2014424, + "heapInuse": 4038656, + "stackInuse": 2162688, + "totalAlloc": 2550816928, + "mallocs": 27832758, + "numGc": 777, + "goroutines": 2, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 128, + "run": 1, + "requests": 571218, + "errors": 0, + "elapsedSeconds": 5.000520459, + "requestsPerSecond": 114231.70941574985, + "p50Millis": 0.720896, + "p95Millis": 3.538944, + "p99Millis": 5.24288, + "serverBytesPerRequest": 4630.001169430935, + "serverAllocsPerRequest": 60.118471406713375, + "sampledPeakHeapAlloc": 6594168, + "before": { + "heapAlloc": 2900712, + "heapInuse": 6209536, + "stackInuse": 1966080, + "totalAlloc": 9196776, + "mallocs": 86400, + "numGc": 6, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "during": { + "heapAlloc": 5161328, + "heapInuse": 8462336, + "stackInuse": 1998848, + "totalAlloc": 2653936784, + "mallocs": 34427153, + "numGc": 1026, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2950048, + "heapInuse": 6201344, + "stackInuse": 851968, + "totalAlloc": 2653948208, + "mallocs": 34427217, + "numGc": 1027, + "goroutines": 3, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1824888, + "heapInuse": 3809280, + "stackInuse": 851968, + "totalAlloc": 2653950392, + "mallocs": 34427223, + "numGc": 1028, + "goroutines": 3, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 128, + "run": 1, + "errors": 0, + "idleHeapDelta": 1857928, + "idleHeapPerConnection": 14515.0625, + "before": { + "heapAlloc": 1632120, + "heapInuse": 3538944, + "stackInuse": 524288, + "totalAlloc": 3074624, + "mallocs": 23744, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 3490048, + "heapInuse": 5808128, + "stackInuse": 1835008, + "totalAlloc": 9802696, + "mallocs": 86273, + "numGc": 5, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2814128, + "heapInuse": 5062656, + "stackInuse": 720896, + "totalAlloc": 9818288, + "mallocs": 86380, + "numGc": 6, + "goroutines": 2, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1732536, + "heapInuse": 3661824, + "stackInuse": 720896, + "totalAlloc": 9820216, + "mallocs": 86385, + "numGc": 7, + "goroutines": 2, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 128, + "run": 1, + "errors": 0, + "idleHeapDelta": 1212976, + "idleHeapPerConnection": 9476.375, + "before": { + "heapAlloc": 1626208, + "heapInuse": 3448832, + "stackInuse": 491520, + "totalAlloc": 3068760, + "mallocs": 23727, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 2839184, + "heapInuse": 5210112, + "stackInuse": 1802240, + "totalAlloc": 9147832, + "mallocs": 85868, + "numGc": 5, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2838168, + "heapInuse": 5169152, + "stackInuse": 688128, + "totalAlloc": 9163568, + "mallocs": 85970, + "numGc": 6, + "goroutines": 3, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1729792, + "heapInuse": 3563520, + "stackInuse": 688128, + "totalAlloc": 9166056, + "mallocs": 85980, + "numGc": 7, + "goroutines": 3, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 1024, + "run": 1, + "errors": 0, + "idleHeapDelta": 14527832, + "idleHeapPerConnection": 14187.3359375, + "before": { + "heapAlloc": 1626760, + "heapInuse": 3497984, + "stackInuse": 491520, + "totalAlloc": 3069240, + "mallocs": 23740, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 16154592, + "heapInuse": 20807680, + "stackInuse": 9207808, + "totalAlloc": 56229808, + "mallocs": 522226, + "numGc": 12, + "goroutines": 1026, + "connections": 1024, + "accepted": 1024 + }, + "after": { + "heapAlloc": 10815896, + "heapInuse": 14376960, + "stackInuse": 720896, + "totalAlloc": 56293624, + "mallocs": 522449, + "numGc": 13, + "goroutines": 2, + "connections": 0, + "accepted": 1024 + }, + "afterReclaim": { + "heapAlloc": 2202576, + "heapInuse": 4227072, + "stackInuse": 720896, + "totalAlloc": 56296208, + "mallocs": 522460, + "numGc": 14, + "goroutines": 2, + "connections": 0, + "accepted": 1024 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 1024, + "run": 1, + "errors": 0, + "idleHeapDelta": 9372528, + "idleHeapPerConnection": 9152.859375, + "before": { + "heapAlloc": 1625904, + "heapInuse": 3457024, + "stackInuse": 491520, + "totalAlloc": 3068472, + "mallocs": 23724, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 10998432, + "heapInuse": 14934016, + "stackInuse": 9240576, + "totalAlloc": 51165352, + "mallocs": 519121, + "numGc": 13, + "goroutines": 1027, + "connections": 1024, + "accepted": 1024 + }, + "after": { + "heapAlloc": 11019640, + "heapInuse": 15032320, + "stackInuse": 6553600, + "totalAlloc": 51226512, + "mallocs": 519340, + "numGc": 14, + "goroutines": 3, + "connections": 0, + "accepted": 1024 + }, + "afterReclaim": { + "heapAlloc": 2196104, + "heapInuse": 4153344, + "stackInuse": 6553600, + "totalAlloc": 51229000, + "mallocs": 519350, + "numGc": 15, + "goroutines": 3, + "connections": 0, + "accepted": 1024 + } + } + ] +} diff --git a/performance/tlsBaselineLinux.json b/performance/tlsBaselineLinux.json new file mode 100644 index 0000000..f5c475f --- /dev/null +++ b/performance/tlsBaselineLinux.json @@ -0,0 +1,592 @@ +{ + "createdAt": "2026-09-21T17:11:34Z", + "goVersion": "go1.25.14", + "platform": "linux/arm64", + "gomaxprocs": 4, + "duration": "3s", + "tls": true, + "notes": [ + "Loopback HTTP/1.1 health response, fixed requestId, optional TLS, no access logs or database.", + "Each case starts a fresh server process; load client runs in the parent process on the same machine.", + "Closed-loop concurrency; throughput is successful responses / actual elapsed time, including drain.", + "Latency percentiles include failures and are histogram upper bounds with \u003c=6.25% rounding.", + "Memory is server-only runtime.MemStats (not RSS or kernel socket memory); sampling interval 200ms.", + "Idle memory measured after a forced GC; delta includes heap growth amortized across connections, excluding stacks.", + "After is after closing clients and one forced GC; afterReclaim is a second GC to observe pooled-cache release.", + "Native limits: 8192 connections / 4096 active requests. These cases do not benchmark overload rejection." + ], + "results": [ + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 1, + "run": 1, + "requests": 36326, + "errors": 0, + "elapsedSeconds": 3.000028418, + "requestsPerSecond": 12108.551966390074, + "p50Millis": 0.08192, + "p95Millis": 0.1024, + "p99Millis": 0.139264, + "serverBytesPerRequest": 4475.917854979904, + "serverAllocsPerRequest": 49.04346748885096, + "sampledPeakHeapAlloc": 3669264, + "before": { + "heapAlloc": 1555696, + "heapInuse": 3522560, + "stackInuse": 491520, + "totalAlloc": 3107240, + "mallocs": 24268, + "numGc": 2, + "goroutines": 3, + "connections": 1, + "accepted": 1 + }, + "during": { + "heapAlloc": 1789280, + "heapInuse": 3874816, + "stackInuse": 819200, + "totalAlloc": 165699432, + "mallocs": 1805821, + "numGc": 82, + "goroutines": 3, + "connections": 1, + "accepted": 1 + }, + "after": { + "heapAlloc": 1645816, + "heapInuse": 3629056, + "stackInuse": 753664, + "totalAlloc": 165700968, + "mallocs": 1805830, + "numGc": 83, + "goroutines": 2, + "connections": 0, + "accepted": 1 + }, + "afterReclaim": { + "heapAlloc": 1625904, + "heapInuse": 3522560, + "stackInuse": 720896, + "totalAlloc": 165701872, + "mallocs": 1805836, + "numGc": 84, + "goroutines": 2, + "connections": 0, + "accepted": 1 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 1, + "run": 1, + "requests": 36410, + "errors": 0, + "elapsedSeconds": 3.000096335, + "requestsPerSecond": 12136.276950586656, + "p50Millis": 0.08192, + "p95Millis": 0.1024, + "p99Millis": 0.131072, + "serverBytesPerRequest": 4807.387201318319, + "serverAllocsPerRequest": 61.041444658060975, + "sampledPeakHeapAlloc": 3473824, + "before": { + "heapAlloc": 1546544, + "heapInuse": 3563520, + "stackInuse": 524288, + "totalAlloc": 3099040, + "mallocs": 24253, + "numGc": 2, + "goroutines": 4, + "connections": 1, + "accepted": 1 + }, + "during": { + "heapAlloc": 1987016, + "heapInuse": 4014080, + "stackInuse": 524288, + "totalAlloc": 178136008, + "mallocs": 2246772, + "numGc": 96, + "goroutines": 4, + "connections": 1, + "accepted": 1 + }, + "after": { + "heapAlloc": 1557080, + "heapInuse": 3637248, + "stackInuse": 425984, + "totalAlloc": 178137480, + "mallocs": 2246784, + "numGc": 97, + "goroutines": 3, + "connections": 0, + "accepted": 1 + }, + "afterReclaim": { + "heapAlloc": 1543112, + "heapInuse": 3473408, + "stackInuse": 425984, + "totalAlloc": 178138128, + "mallocs": 2246789, + "numGc": 98, + "goroutines": 3, + "connections": 0, + "accepted": 1 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 32, + "run": 1, + "requests": 297651, + "errors": 0, + "elapsedSeconds": 3.000748959, + "requestsPerSecond": 99192.23636061586, + "p50Millis": 0.172032, + "p95Millis": 1.179648, + "p99Millis": 2.097152, + "serverBytesPerRequest": 4478.059122932562, + "serverAllocsPerRequest": 49.0660605877353, + "sampledPeakHeapAlloc": 4147936, + "before": { + "heapAlloc": 2046152, + "heapInuse": 4554752, + "stackInuse": 884736, + "totalAlloc": 4736656, + "mallocs": 39461, + "numGc": 3, + "goroutines": 34, + "connections": 32, + "accepted": 32 + }, + "during": { + "heapAlloc": 3132976, + "heapInuse": 5529600, + "stackInuse": 1212416, + "totalAlloc": 1337635432, + "mallocs": 14644023, + "numGc": 642, + "goroutines": 34, + "connections": 32, + "accepted": 32 + }, + "after": { + "heapAlloc": 1946392, + "heapInuse": 4136960, + "stackInuse": 884736, + "totalAlloc": 1337638376, + "mallocs": 14644047, + "numGc": 643, + "goroutines": 2, + "connections": 0, + "accepted": 32 + }, + "afterReclaim": { + "heapAlloc": 1657656, + "heapInuse": 3596288, + "stackInuse": 884736, + "totalAlloc": 1337639280, + "mallocs": 14644053, + "numGc": 644, + "goroutines": 2, + "connections": 0, + "accepted": 32 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 32, + "run": 1, + "requests": 271586, + "errors": 0, + "elapsedSeconds": 3.008536543, + "requestsPerSecond": 90271.79697448004, + "p50Millis": 0.172032, + "p95Millis": 1.114112, + "p99Millis": 2.097152, + "serverBytesPerRequest": 4810.125264188876, + "serverAllocsPerRequest": 61.068320900193676, + "sampledPeakHeapAlloc": 3706424, + "before": { + "heapAlloc": 1833336, + "heapInuse": 4136960, + "stackInuse": 1048576, + "totalAlloc": 4566744, + "mallocs": 39398, + "numGc": 3, + "goroutines": 35, + "connections": 32, + "accepted": 32 + }, + "during": { + "heapAlloc": 2032480, + "heapInuse": 4661248, + "stackInuse": 851968, + "totalAlloc": 1310929424, + "mallocs": 16624699, + "numGc": 742, + "goroutines": 35, + "connections": 32, + "accepted": 32 + }, + "after": { + "heapAlloc": 1875360, + "heapInuse": 4186112, + "stackInuse": 524288, + "totalAlloc": 1310933392, + "mallocs": 16624734, + "numGc": 743, + "goroutines": 3, + "connections": 0, + "accepted": 32 + }, + "afterReclaim": { + "heapAlloc": 1593792, + "heapInuse": 3465216, + "stackInuse": 524288, + "totalAlloc": 1310934296, + "mallocs": 16624740, + "numGc": 744, + "goroutines": 3, + "connections": 0, + "accepted": 32 + } + }, + { + "adapter": "nethttp", + "scenario": "traffic", + "concurrency": 128, + "run": 1, + "requests": 322494, + "errors": 0, + "elapsedSeconds": 3.001132293, + "requestsPerSecond": 107457.44223012164, + "p50Millis": 0.589824, + "p95Millis": 2.752512, + "p99Millis": 25.165824, + "serverBytesPerRequest": 4475.7173528809835, + "serverAllocsPerRequest": 49.07075790557344, + "sampledPeakHeapAlloc": 7137248, + "before": { + "heapAlloc": 3503480, + "heapInuse": 6340608, + "stackInuse": 2228224, + "totalAlloc": 9426048, + "mallocs": 83598, + "numGc": 5, + "goroutines": 124, + "connections": 122, + "accepted": 122 + }, + "during": { + "heapAlloc": 6849936, + "heapInuse": 9125888, + "stackInuse": 2162688, + "totalAlloc": 1452818040, + "mallocs": 15908623, + "numGc": 415, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2816896, + "heapInuse": 5300224, + "stackInuse": 1048576, + "totalAlloc": 1452825560, + "mallocs": 15908667, + "numGc": 416, + "goroutines": 2, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1717496, + "heapInuse": 3710976, + "stackInuse": 1015808, + "totalAlloc": 1452826464, + "mallocs": 15908673, + "numGc": 417, + "goroutines": 2, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "native", + "scenario": "traffic", + "concurrency": 128, + "run": 1, + "requests": 299953, + "errors": 0, + "elapsedSeconds": 3.003243168, + "requestsPerSecond": 99876.36139359062, + "p50Millis": 0.622592, + "p95Millis": 2.752512, + "p99Millis": 32.505856, + "serverBytesPerRequest": 4817.3341823552355, + "serverAllocsPerRequest": 61.15272726060416, + "sampledPeakHeapAlloc": 5668056, + "before": { + "heapAlloc": 2118392, + "heapInuse": 4661248, + "stackInuse": 1605632, + "totalAlloc": 6143952, + "mallocs": 56351, + "numGc": 4, + "goroutines": 61, + "connections": 58, + "accepted": 58 + }, + "during": { + "heapAlloc": 3375320, + "heapInuse": 5996544, + "stackInuse": 1802240, + "totalAlloc": 1451117792, + "mallocs": 18399295, + "numGc": 528, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2766336, + "heapInuse": 5382144, + "stackInuse": 1769472, + "totalAlloc": 1451125632, + "mallocs": 18399341, + "numGc": 529, + "goroutines": 3, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1658104, + "heapInuse": 3588096, + "stackInuse": 1769472, + "totalAlloc": 1451126280, + "mallocs": 18399346, + "numGc": 530, + "goroutines": 3, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 128, + "run": 1, + "errors": 0, + "idleHeapDelta": 1838600, + "idleHeapPerConnection": 14364.0625, + "before": { + "heapAlloc": 1521656, + "heapInuse": 3260416, + "stackInuse": 458752, + "totalAlloc": 3029904, + "mallocs": 23674, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 3360256, + "heapInuse": 5718016, + "stackInuse": 1540096, + "totalAlloc": 9686296, + "mallocs": 86122, + "numGc": 5, + "goroutines": 130, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2682728, + "heapInuse": 4792320, + "stackInuse": 425984, + "totalAlloc": 9695376, + "mallocs": 86176, + "numGc": 6, + "goroutines": 2, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1604552, + "heapInuse": 3440640, + "stackInuse": 425984, + "totalAlloc": 9696584, + "mallocs": 86186, + "numGc": 7, + "goroutines": 2, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 128, + "run": 1, + "errors": 0, + "idleHeapDelta": 1191856, + "idleHeapPerConnection": 9311.375, + "before": { + "heapAlloc": 1521040, + "heapInuse": 3325952, + "stackInuse": 458752, + "totalAlloc": 3029464, + "mallocs": 23663, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 2712896, + "heapInuse": 4939776, + "stackInuse": 2555904, + "totalAlloc": 9083760, + "mallocs": 85842, + "numGc": 5, + "goroutines": 131, + "connections": 128, + "accepted": 128 + }, + "after": { + "heapAlloc": 2711312, + "heapInuse": 4907008, + "stackInuse": 425984, + "totalAlloc": 9091576, + "mallocs": 85898, + "numGc": 6, + "goroutines": 3, + "connections": 0, + "accepted": 128 + }, + "afterReclaim": { + "heapAlloc": 1608416, + "heapInuse": 3416064, + "stackInuse": 425984, + "totalAlloc": 9092784, + "mallocs": 85908, + "numGc": 7, + "goroutines": 3, + "connections": 0, + "accepted": 128 + } + }, + { + "adapter": "nethttp", + "scenario": "idle", + "concurrency": 512, + "run": 1, + "errors": 0, + "idleHeapDelta": 7269176, + "idleHeapPerConnection": 14197.609375, + "before": { + "heapAlloc": 1521080, + "heapInuse": 3383296, + "stackInuse": 458752, + "totalAlloc": 3029480, + "mallocs": 23669, + "numGc": 1, + "goroutines": 2, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 8790256, + "heapInuse": 11960320, + "stackInuse": 4718592, + "totalAlloc": 29547624, + "mallocs": 272853, + "numGc": 9, + "goroutines": 514, + "connections": 512, + "accepted": 512 + }, + "after": { + "heapAlloc": 6121872, + "heapInuse": 8749056, + "stackInuse": 458752, + "totalAlloc": 29580224, + "mallocs": 272941, + "numGc": 10, + "goroutines": 2, + "connections": 0, + "accepted": 512 + }, + "afterReclaim": { + "heapAlloc": 1812232, + "heapInuse": 3817472, + "stackInuse": 458752, + "totalAlloc": 29581432, + "mallocs": 272951, + "numGc": 11, + "goroutines": 2, + "connections": 0, + "accepted": 512 + } + }, + { + "adapter": "native", + "scenario": "idle", + "concurrency": 512, + "run": 1, + "errors": 0, + "idleHeapDelta": 4687096, + "idleHeapPerConnection": 9154.484375, + "before": { + "heapAlloc": 1520480, + "heapInuse": 3293184, + "stackInuse": 458752, + "totalAlloc": 3028752, + "mallocs": 23658, + "numGc": 1, + "goroutines": 3, + "connections": 0, + "accepted": 0 + }, + "during": { + "heapAlloc": 6207576, + "heapInuse": 9035776, + "stackInuse": 8880128, + "totalAlloc": 27129544, + "mallocs": 271820, + "numGc": 9, + "goroutines": 515, + "connections": 512, + "accepted": 512 + }, + "after": { + "heapAlloc": 6228280, + "heapInuse": 9027584, + "stackInuse": 491520, + "totalAlloc": 27168240, + "mallocs": 271921, + "numGc": 10, + "goroutines": 3, + "connections": 0, + "accepted": 512 + }, + "afterReclaim": { + "heapAlloc": 1812240, + "heapInuse": 3645440, + "stackInuse": 491520, + "totalAlloc": 27169448, + "mallocs": 271931, + "numGc": 11, + "goroutines": 3, + "connections": 0, + "accepted": 512 + } + } + ] +} diff --git a/transport/README.md b/transport/README.md index bef1268..ff3edd7 100644 --- a/transport/README.md +++ b/transport/README.md @@ -48,7 +48,7 @@ if err := application.RunWith(ctx, server, ":8443"); err != nil { } ``` -需要自己持有 listener 或配置证书文件时,分别使用 `ServeTLS` 或 `RunTLS`;TLS 握手仍受 native 的读超时、连接上限和优雅关闭控制。 +需要自己持有 listener 或配置证书文件时,分别使用 `ServeTLS` 或 `RunTLS`;TLS 握手仍受 native 的读超时、连接上限和优雅关闭控制,Handler 可通过 `Request.TLS` 读取标准 `tls.ConnectionState`。 ## 可移植路由规则(v0.3.0) diff --git a/transport/native/server.go b/transport/native/server.go index a6b7a74..6526968 100644 --- a/transport/native/server.go +++ b/transport/native/server.go @@ -614,6 +614,10 @@ func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBod RemoteAddr: conn.RemoteAddr().String(), Trailer: trailer, } + if tlsConn, ok := conn.(*tls.Conn); ok { + state := tlsConn.ConnectionState() + request.TLS = &state + } if chunked { request.TransferEncoding = []string{"chunked"} } diff --git a/transport/native/server_test.go b/transport/native/server_test.go index 7310b4e..f41b398 100644 --- a/transport/native/server_test.go +++ b/transport/native/server_test.go @@ -903,36 +903,6 @@ func TestAppRunWithNativeTLS(t *testing.T) { } } -func TestNativeTLSSlowHandshakeTimesOut(t *testing.T) { - application := omni.NewWithRouter(netrouter.New()) - application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") }) - server := native.New(application) - server.ReadHeaderTimeout = 30 * time.Millisecond - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - certificate := testCertificate(t) - done := make(chan error, 1) - go func() { - done <- server.ServeTLS(context.Background(), listener, &tls.Config{Certificates: []tls.Certificate{certificate}}) - }() - waitForServer(t, listener.Addr().String()) - defer stopServer(t, server, listener, done) - - conn, err := net.Dial("tcp", listener.Addr().String()) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - if err := conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil { - t.Fatal(err) - } - if _, err := conn.Read(make([]byte, 1)); err == nil { - t.Fatal("slow TLS handshake was not closed") - } -} - func testCertificate(t *testing.T) tls.Certificate { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) diff --git a/transport/native/tls_test.go b/transport/native/tls_test.go new file mode 100644 index 0000000..4f4815c --- /dev/null +++ b/transport/native/tls_test.go @@ -0,0 +1,188 @@ +package native_test + +import ( + "bufio" + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io" + "net" + "net/http" + "sync/atomic" + "testing" + "time" + + "git.yuntuo.hk/k2safe/go-omni/transport/native" +) + +func startNativeTLS(t *testing.T, server *native.Server) (string, *tls.Config) { + t.Helper() + certificate := testCertificate(t) + leaf, err := x509.ParseCertificate(certificate.Certificate[0]) + if err != nil { + t.Fatal(err) + } + roots := x509.NewCertPool() + roots.AddCert(leaf) + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + done := make(chan error, 1) + go func() { + done <- server.ServeTLS(context.Background(), listener, &tls.Config{ + Certificates: []tls.Certificate{certificate}, MinVersion: tls.VersionTLS12, + NextProtos: []string{"http/1.1"}, + }) + }() + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + t.Errorf("TLS shutdown: %v", err) + } + select { + case err := <-done: + if err != nil { + t.Errorf("TLS serve: %v", err) + } + case <-ctx.Done(): + t.Error("TLS serve did not exit") + } + }) + return listener.Addr().String(), &tls.Config{ + RootCAs: roots, ServerName: "localhost", MinVersion: tls.VersionTLS12, + NextProtos: []string{"http/1.1"}, + } +} + +func requirePeerClose(t *testing.T, conn net.Conn) { + t.Helper() + conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + _, err := conn.Read(make([]byte, 1)) + if err == nil { + t.Fatal("expected server to close the connection") + } + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + t.Fatalf("client timed out; server did not close the connection: %v", err) + } +} + +func TestNativeTLSSlowHandshakeTimesOut(t *testing.T) { + for _, prefix := range []string{"", "\x16\x03\x01\x00\x64\x01"} { + t.Run(fmt.Sprintf("partialBytes=%d", len(prefix)), func(t *testing.T) { + var calls atomic.Int32 + server := native.New(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { calls.Add(1) })) + server.ReadHeaderTimeout = 50 * time.Millisecond + address, _ := startNativeTLS(t, server) + conn, err := net.DialTimeout("tcp", address, time.Second) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + if _, err := io.WriteString(conn, prefix); err != nil { + t.Fatal(err) + } + requirePeerClose(t, conn) + if calls.Load() != 0 { + t.Fatal("unfinished TLS handshake reached the handler") + } + }) + } +} + +func TestNativeTLSConnectionLimitAndKeepAlive(t *testing.T) { + var calls atomic.Int32 + server := native.New(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + _, _ = io.WriteString(w, "healthy") + })) + server.MaxConnections = 1 + address, config := startNativeTLS(t, server) + dial := func() (*tls.Conn, error) { + return tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", address, config) + } + first, err := dial() + if err != nil { + t.Fatal(err) + } + defer first.Close() + first.SetDeadline(time.Now().Add(3 * time.Second)) + reader := bufio.NewReader(first) + for index := 0; index < 3; index++ { + if _, err := io.WriteString(first, "GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n"); err != nil { + t.Fatal(err) + } + response, err := http.ReadResponse(reader, &http.Request{Method: http.MethodGet}) + if err != nil { + t.Fatal(err) + } + body, err := io.ReadAll(response.Body) + response.Body.Close() + if err != nil || string(body) != "healthy" || response.Close { + t.Fatalf("Keep-Alive response body=%q err=%v close=%v", body, err, response.Close) + } + } + // A completed TLS handshake proves the first slot is held before testing + // rejection. TCP accept can still succeed; the TLS handshake must fail. + rejected, err := dial() + if err == nil { + rejected.Close() + t.Fatal("TLS connection exceeded MaxConnections") + } + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + t.Fatalf("over-limit handshake hung: %v", err) + } + first.Close() + deadline := time.Now().Add(2 * time.Second) + var next *tls.Conn + for time.Now().Before(deadline) { + next, err = dial() + if err == nil { + break + } + time.Sleep(time.Millisecond) + } + if err != nil { + t.Fatalf("TLS connection capacity not released: %v", err) + } + defer next.Close() + next.SetDeadline(time.Now().Add(time.Second)) + if _, err := io.WriteString(next, "GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"); err != nil { + t.Fatal(err) + } + response, err := http.ReadResponse(bufio.NewReader(next), &http.Request{Method: http.MethodGet}) + if err != nil { + t.Fatal(err) + } + body, err := io.ReadAll(response.Body) + response.Body.Close() + if err != nil || string(body) != "healthy" || calls.Load() != 4 { + t.Fatalf("recovered response=%q calls=%d err=%v", body, calls.Load(), err) + } +} + +func TestNativeTLSRequestMetadata(t *testing.T) { + server := native.New(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.TLS == nil || !r.TLS.HandshakeComplete || r.TLS.NegotiatedProtocol != "http/1.1" { + http.Error(w, "missing TLS request metadata", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + })) + address, config := startNativeTLS(t, server) + client := &http.Client{Transport: &http.Transport{TLSClientConfig: config}, Timeout: 2 * time.Second} + defer client.CloseIdleConnections() + response, err := client.Get("https://" + address + "/") + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusNoContent { + t.Fatalf("TLS metadata unavailable to handler: status=%d", response.StatusCode) + } +}