release: prepare v0.6.0-preview

This commit is contained in:
2026-09-22 01:38:42 +08:00
parent 9c9c8ab59a
commit c5e415ca52
18 changed files with 797 additions and 62 deletions

View File

@@ -12,7 +12,15 @@
- native 请求补齐 `Request.TLS` 连接状态;Linux/arm64 容器通过全量 race、vet、build,以及 TLS 并发、Keep-Alive、慢握手和连接上限回收验收。
- 补充慢请求体超时、大请求体和同连接管线请求验收,确认请求体未完整读取时不会进入业务 Handler,连续请求不会串线。
- 补充 `MaxConnections` 满载拒绝与槽位释放验收,确认请求完成后连接容量可再次使用。
- 增加 8 并发、800 次建连/断连的状态池 churn 验收,覆盖连接对象回收与容量复用。
- 增加高频建连/断连的状态池 churn 验收,覆盖连接对象回收与容量复用。
## v0.6.0-preview
- native 网络层完成慢请求头、慢 Content-Length / chunked body、慢 TLS 握手和 TLS 请求元数据验收,未完整读取的请求不会进入业务 Handler。
- 将 native 连接 churn race 验收扩展为 16 个 worker、3,200 次建连/断连,并覆盖明文与 TLS 的连接上限、Keep-Alive 和槽位回收。
- perfcheck 支持最长 30 分钟持续场景,新增 Linux 文件描述符采样;固定记录服务端堆、goroutine、连接数、accept 数和 FD 回收状态。
- 新增 `scripts/verifyLinux.py`,在 `golang:1.25` Linux 容器内统一执行 race、vet、build 和可选 TLS / 非 TLS 压测。
- 完成 Linux/arm64 30 秒高并发回归:128 并发流量、512 条空闲连接,TLS 与非 TLS 均为 0 错误,连接、goroutine 和 FD 回收正常;原始报告见 `performance/linuxSoak.json` 和 `performance/linuxSoakTLS.json`。
## v0.5.0

View File

@@ -13,13 +13,25 @@ python3 scripts/verifyPerformance.py --load --output /tmp/goOmniPerf.json
# 只运行独立进程矩阵,默认每档 2 秒、3 轮
go run ./internal/perfcheck --duration 2s --count 3 --output /tmp/goOmniPerf.json
# 在 Linux Docker 容器内固定运行 race、vet、build 和 TLS 压测
python3 scripts/verifyLinux.py --load --tls --duration 30s \
--concurrency 128 --idle 512 --count 1 \
--output performance/linuxSoakTLS.json
# 发布候选验收可把持续流量延长到 30 分钟
python3 scripts/verifyLinux.py --load --tls --duration 30m \
--concurrency 128 --idle 512 --count 1 \
--output performance/linuxSoakTLS-30m.json
```
可通过 `--concurrency 1,32,128` / `--idle 128,1024` 调整矩阵;传空字符串可跳过对应场景。工具只监听本机随机端口,每个场景启动独立服务进程,并在结束后关闭客户端、检查连接排空、关闭服务。响应状态、完整响应体、requestId 或传输错误均计入失败,不能把快速返回的错误页算成成功吞吐。
Linux 验收脚本使用 `golang:1.25` 容器,固定执行 `go test -race ./...`、`go vet ./...`、`go build ./...`,再按参数运行 perfcheck。perfcheck 在 Linux 读取 `/proc/self/fd`,报告连接建立期间的文件描述符峰值以及回收后的数量;同时记录服务端堆、goroutine、连接数和累计 accept 数。30 秒报告用于快速回归,30 分钟命令用于发布前持续压测。
## 2026-09-21 本机测量
环境:Go 1.25.0、darwin/arm64、Apple M2 Max、GOMAXPROCS=12。网络实现来自 `3d5f225` 工作树(native 与 v0.5.0 相同);本次新增测量工具,不修改服务端实现。原始测量值见 [tcpBaseline.json](performance/tcpBaseline.json)。报告时间为 UTC,北京时间是 9 月 21 日。
环境:Go 1.25.0、darwin/arm64、Apple M2 Max、GOMAXPROCS=12。网络实现来自 `3d5f225` 工作树(native 与 v0.6.0-preview 相同);本次新增测量工具,不修改服务端实现。原始测量值见 [tcpBaseline.json](performance/tcpBaseline.json)。报告时间为 UTC,北京时间是 9 月 21 日。
每个并发档位持续约 2 秒、重复 3 轮,每轮交替标准库/native 的先后顺序。下表为各指标的三轮中位数;全部流量场景共完成 **2,660,828** 次请求,错误 **0**。另有 128 / 1,024 条空闲长连接的建立、保持和回收测试。
@@ -48,6 +60,7 @@ go run ./internal/perfcheck --duration 2s --count 3 --output /tmp/goOmniPerf.jso
- 并发模型为闭环:每个 worker 等上一个请求完成后发下一个。实际经过时间包含最后请求排空;吞吐只计算成功响应。不能据此推断开放到达流量下的排队或过载表现。
- 延迟从客户端发起请求到完整响应校验完成,包含失败请求。直方图使用每个 2 的幂区间内 16 档,报告桶上界,舍入误差至多约 6.25%。小幅差别不应过度解释。
- 服务端 `runtime.MemStats` 通过独立 stdin/stdout 控制通道读取,客户端内存不计入。累计分配差值包含少量采样开销。堆峰值每 200 ms 采样,可能错过瞬时峰值;这些指标不是进程 RSS。
- Linux 文件描述符来自服务端子进程的 `/proc/self/fd`,包含标准输入输出、监听 socket 和连接;macOS 等没有该路径的平台会省略该字段。回收后的数量应回到启动附近,不能代替内核 socket 缓冲或 RSS 测量。
- 空闲场景在所有连接完成健康请求后做 GC 再采样。新增堆内存包括连接、缓冲区及分摊的运行时开销。`after` 为客户端断开并排空后一次 GC,`afterReclaim` 为第二次 GC,用于观察对象池缓存释放。
- native 配置上限为 8,192 连接 / 4,096 活跃请求;本轮并发低于上限,未验证过载保护容量。标准库没有同样的并发闸门;比较包括各自真实实现的开销。
- 可用 `python3 scripts/verifyPerformance.py --load --tls --duration 2s` 运行 TLS 版本的独立进程矩阵。该模式在每个子进程生成短期自签名证书,客户端仅用于本机基准验证;本报告的历史表格仍是无 TLS 数据。
@@ -60,10 +73,12 @@ macOS arm64 本机的 5 秒 TLS 矩阵见 [tlsBaseline.json](performance/tlsBase
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。
Linux arm64 的 30 秒高并发回归见 [linuxSoak.json](performance/linuxSoak.json) 和 [linuxSoakTLS.json](performance/linuxSoakTLS.json):两份报告均使用 128 并发流量和 512 条空闲连接,所有场景错误为 0;native 非 TLS 约 146,614 req/s、TLS 约 149,720 req/s,空闲连接堆增量分别约 2.57 MiB 和 4.47 MiB,FD 均由 7 增至 519 并回到 7,goroutine 均回到启动附近。
`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 应按这个含义理解,不能作为服务端延迟或独立内存指标。
`BenchmarkTransportRouteDispatch` 和 `BenchmarkTransportApplicationRequest` 继续保留,分别测进程内路由分发和完整框架处理链;不包含 TCP,不能直接与上述网络延迟比较。
后续优先补 Linux 目标环境、持续压测、大请求体/慢客户端、连接反复建立回收和协议边界验证,再通过 profile 决定是否优化对象分配或事件状态机。
后续通过 30 分钟命令完成发布前持续压测,并在目标反向代理和部署环境继续观察内核 socket 缓冲、FD 上限与真实业务流量;再通过 profile 决定是否优化对象分配或事件状态机。

View File

@@ -5,7 +5,7 @@ GoOmni 是一套面向业务复用的 Go 后端基础框架。当前默认使用
远程 module 地址是 `git.yuntuo.hk/k2safe/go-omni`。业务项目可以直接引用指定版本:
```sh
go get git.yuntuo.hk/k2safe/go-omni@v0.5.0
go get git.yuntuo.hk/k2safe/go-omni@v0.6.0-preview
```
`v0.4.0` 移除了 Gin 和 Gin 专属的 `WithMode` 配置;`omni.New()` 直接使用标准库路由,已有业务 Handler / Service 无需修改。
@@ -13,7 +13,7 @@ go get git.yuntuo.hk/k2safe/go-omni@v0.5.0
创建新业务项目:
```sh
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.5.0
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.6.0-preview
go-omni new --module example.com/billing ./billing
cd billing
go mod tidy
@@ -200,10 +200,10 @@ userService := service.NewUsers(services)
从远程版本生成并升级一个独立业务模块,可运行:
```sh
python3 scripts/verifyBusiness.py --version v0.5.0
python3 scripts/verifyBusiness.py --version v0.6.0-preview
```
该验收会在临时目录中先固定 `v0.2.0`,再升级到 `v0.5.0`,检查无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。
该验收会在临时目录中先固定 `v0.2.0`,再升级到 `v0.6.0-preview`,检查无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。
标准库与 native 的并发、延迟和长连接内存对照见 [PERFORMANCE.md](PERFORMANCE.md)。`python3 scripts/verifyPerformance.py` 运行检查与 benchmark;增加 `--load --output /tmp/goOmniPerf.json` 可测量独立服务进程并保存报告。

View File

@@ -35,7 +35,7 @@
- 业务试用没有要求修改 Handler / Service;现有公共 API 足以支撑一套分层业务模块。
- 自动验收入口:`python3 scripts/verifyBusiness.py --version v0.4.0`。
## 5. 自研网络层、稳定性与性能基线(进行中,v0.5.0 预览)
## 5. 自研网络层、稳定性与性能基线(v0.6.0-preview 验收完成)
- 已加入标准库路由和完整 GoOmni 请求链 benchmark,输出分配次数和耗时,作为未来自研网络层的对照基线。
- 新增 `transport/native` 第一版自研 HTTP/1.1 服务端:自行管理 TCP listener、Keep-Alive、Content-Length / chunked 请求解析、响应写回、读写超时和优雅关闭;通过 `App.RunWith` 接入,业务 Handler / Service 保持不变。
@@ -44,10 +44,11 @@
- 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/arm64 容器已通过全量 race、vet、build,以及 3 秒 TLS 并发/空闲连接矩阵。原始报告见 [tlsBaselineLinux.json](performance/tlsBaselineLinux.json);慢客户端规模和持续生产压测仍待真实部署环境验证。
- 已补充慢请求头、慢 Content-Length body、慢 chunked body、慢 TLS 握手、大请求体、HTTP/1.0 Keep-Alive、HEAD/204 和管线请求验收;Linux/arm64 容器已通过全量 race、vet、build,以及 3 秒 TLS 并发/空闲连接矩阵。原始报告见 [tlsBaselineLinux.json](performance/tlsBaselineLinux.json)。
- 已验证 `MaxConnections` 满载时快速拒绝新连接、请求完成后释放容量;后续仍需在目标部署环境观察文件描述符、内核 socket 缓冲和反向代理联动。
- 已用 8 个并发 worker 完成 800 次建连/断连 race 验收;真实部署仍需补文件描述符和跨平台 socket 行为观测。
- 已具备全量 race、vet、build 验收;生产压测、长连接规模和真实部署验证仍待完成。
- 自动验收入口:`python3 scripts/verifyPerformance.py`;增加 `--load --output /tmp/goOmniPerf.json` 可重复运行独立进程并发与内存矩阵。
- 已用 16 个并发 worker 完成 3,200 次建连/断连 race 验收;MaxConnections 满载拒绝、槽位释放和 TLS 连接上限均已覆盖。
- 新增 `scripts/verifyLinux.py`,固定在 Go 1.25 Linux 容器内运行 race、vet、build 和可选 TLS/非 TLS perfcheck;perfcheck 记录堆、goroutine、连接和 Linux 文件描述符。
- Linux/arm64 已完成 30 秒、128 并发、512 空闲连接的 TLS 与非 TLS 持续回归,报告见 `performance/linuxSoak.json` 和 `performance/linuxSoakTLS.json`,均为 0 错误且连接、goroutine、FD 回收正常。
- 自动验收入口:`python3 scripts/verifyPerformance.py`;Linux 固定验收入口:`python3 scripts/verifyLinux.py --load --tls --duration 30m`。真实部署仍需按目标代理和内核参数执行 30 分钟发布前压测。
以上阶段以验收结果判断完成。当前仍是基础框架试用阶段;MySQL、适配层契约和一次业务模块试用已完成,自研网络层已有可运行预览,稳定性与生产规模验证尚未完成。

View File

@@ -3,7 +3,7 @@
从远程安装,生成一个带 Handler / Service / Repository / Model 分层的服务骨架:
```sh
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.5.0
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.6.0-preview
go-omni new --module example.com/billing ./billing
cd billing
go mod tidy
@@ -16,7 +16,7 @@ go run ./cmd/server
需要选择其他已发布版本时,使用完整版本号:
```sh
go-omni new --framework-version v0.5.0 --module example.com/billing ./billing
go-omni new --framework-version v0.6.0-preview --module example.com/billing ./billing
```
`--framework-version` 禁用自动本地替换;`main`、`latest` 等浮动引用不接受为固定版本。
@@ -48,8 +48,8 @@ MySQL 项目额外包含 `schema.sql`、MySQL Repository 和可选的数据库
发布验收使用 Python 3 标准库脚本,不需要额外 Python 依赖:
```sh
python3 scripts/verifyScaffold.py --version v0.5.0
python3 scripts/verifyDatabase.py --version v0.5.0
python3 scripts/verifyScaffold.py --version v0.6.0-preview
python3 scripts/verifyDatabase.py --version v0.6.0-preview
```
脚本在源码目录外安装远程 CLI、生成项目,确认固定版本且没有 `replace`,再下载依赖、

View File

@@ -10,7 +10,7 @@ const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
// Source builds can carry an unpublished VCS version (including +dirty).
// Keep this fallback in sync with the next release; installed CLIs use their
// downloaded module version, which also has a module checksum.
const defaultFrameworkVersion = "v0.5.0"
const defaultFrameworkVersion = "v0.6.0-preview"
var frameworkVersionPattern = regexp.MustCompile(`^v[01]\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`)

View File

@@ -46,6 +46,7 @@ type snapshot struct {
Goroutines int `json:"goroutines"`
Connections int64 `json:"connections"`
Accepted int64 `json:"accepted"`
FileDescriptors int `json:"fileDescriptors,omitempty"`
}
type result struct {
@@ -64,6 +65,7 @@ type result struct {
ServerBytesPerRequest float64 `json:"serverBytesPerRequest,omitempty"`
ServerAllocsPerRequest float64 `json:"serverAllocsPerRequest,omitempty"`
SampledPeakHeapAlloc uint64 `json:"sampledPeakHeapAlloc,omitempty"`
SampledPeakFileDescriptors int `json:"sampledPeakFileDescriptors,omitempty"`
IdleHeapDelta int64 `json:"idleHeapDelta,omitempty"`
IdleHeapPerConnection float64 `json:"idleHeapPerConnection,omitempty"`
Before snapshot `json:"before"`
@@ -239,12 +241,21 @@ func serve(adapter string, tlsEnabled bool) error {
StackInuse: memory.StackInuse, TotalAlloc: memory.TotalAlloc,
Mallocs: memory.Mallocs, NumGC: memory.NumGC,
Goroutines: runtime.NumGoroutine(), Connections: tracked.active.Load(), Accepted: tracked.accepted.Load(),
FileDescriptors: openFileDescriptorCount(),
}); err != nil {
return err
}
}
}
func openFileDescriptorCount() int {
entries, err := os.ReadDir("/proc/self/fd")
if err != nil {
return 0
}
return len(entries)
}
type child struct {
cmd *exec.Cmd
input io.WriteCloser
@@ -265,8 +276,9 @@ func startChildMode(adapter string, tlsEnabled bool) (*child, error) {
if err != nil {
return nil, err
}
// This bounds even a broken server or protocol read; it is not a load duration.
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
// This bounds even a broken server or protocol read; it is not a load
// duration. Keep it above the supported 30-minute soak plus drain time.
ctx, cancel := context.WithTimeout(context.Background(), 35*time.Minute)
args := []string{"--server", adapter}
if tlsEnabled {
args = append(args, "--tls")
@@ -451,6 +463,9 @@ func traffic(c *child, concurrency int, duration time.Duration) (result, error)
} else if s.HeapAlloc > r.SampledPeakHeapAlloc {
r.SampledPeakHeapAlloc = s.HeapAlloc
}
if s.FileDescriptors > r.SampledPeakFileDescriptors {
r.SampledPeakFileDescriptors = s.FileDescriptors
}
}
}
r.ElapsedSeconds = time.Since(begin).Seconds()
@@ -472,6 +487,7 @@ func traffic(c *child, concurrency int, duration time.Duration) (result, error)
return r, err
}
r.SampledPeakHeapAlloc = max(r.SampledPeakHeapAlloc, r.During.HeapAlloc)
r.SampledPeakFileDescriptors = max(r.SampledPeakFileDescriptors, r.During.FileDescriptors)
if r.Requests == 0 {
return r, errors.New("no requests completed")
}
@@ -532,6 +548,9 @@ func idle(c *child, count int) (result, error) {
if r.During.Connections != int64(count) {
return r, fmt.Errorf("idle connections: want %d, have %d", count, r.During.Connections)
}
if r.During.FileDescriptors > 0 && r.Before.FileDescriptors > 0 && r.During.FileDescriptors < r.Before.FileDescriptors+count {
return r, fmt.Errorf("idle file descriptors: want at least %d, have %d", r.Before.FileDescriptors+count, r.During.FileDescriptors)
}
r.IdleHeapDelta = int64(r.During.HeapAlloc) - int64(r.Before.HeapAlloc)
r.IdleHeapPerConnection = float64(r.IdleHeapDelta) / float64(count)
for _, conn := range connections {
@@ -586,8 +605,8 @@ func compare(duration time.Duration, concurrencyText, idleText string, count int
if err != nil {
return err
}
if count < 1 || duration < 200*time.Millisecond || duration > 30*time.Second || len(concurrency)+len(idleLevels) == 0 {
return errors.New("count must be positive, duration 200ms..30s, and at least one scenario selected")
if count < 1 || duration < 200*time.Millisecond || duration > 30*time.Minute || len(concurrency)+len(idleLevels) == 0 {
return errors.New("count must be positive, duration 200ms..30m, and at least one scenario selected")
}
report := report{
CreatedAt: time.Now().UTC().Format(time.RFC3339), GoVersion: runtime.Version(),
@@ -598,6 +617,7 @@ func compare(duration time.Duration, concurrencyText, idleText string, count int
"Closed-loop concurrency; throughput is successful responses / actual elapsed time, including drain.",
"Latency percentiles include failures and are histogram upper bounds with <=6.25% rounding.",
"Memory is server-only runtime.MemStats (not RSS or kernel socket memory); sampling interval 200ms.",
"FileDescriptors is read from /proc/self/fd on Linux and omitted on other platforms.",
"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.",
@@ -638,7 +658,7 @@ func compare(duration time.Duration, concurrencyText, idleText string, count int
if scenario.name == "traffic" {
fmt.Printf("%s concurrency=%d run=%d: %.0f req/s p95=%.3fms p99=%.3fms server=%.0f B/req errors=%d\n", adapter, level, run, value.RequestsPerSecond, value.P95Millis, value.P99Millis, value.ServerBytesPerRequest, value.Errors)
} else {
fmt.Printf("%s idle=%d run=%d: server heap delta=%.2f MiB (%.0f B/conn), goroutines=%d->%d->%d errors=%d\n", adapter, level, run, float64(value.IdleHeapDelta)/(1<<20), value.IdleHeapPerConnection, value.Before.Goroutines, value.During.Goroutines, value.After.Goroutines, value.Errors)
fmt.Printf("%s idle=%d run=%d: server heap delta=%.2f MiB (%.0f B/conn), goroutines=%d->%d->%d, fds=%d->%d->%d errors=%d\n", adapter, level, run, float64(value.IdleHeapDelta)/(1<<20), value.IdleHeapPerConnection, value.Before.Goroutines, value.During.Goroutines, value.After.Goroutines, value.Before.FileDescriptors, value.During.FileDescriptors, value.After.FileDescriptors, value.Errors)
}
}
}

265
performance/linuxSoak.json Normal file
View File

@@ -0,0 +1,265 @@
{
"createdAt": "2026-09-21T17:29:24Z",
"goVersion": "go1.25.14",
"platform": "linux/arm64",
"gomaxprocs": 4,
"duration": "30s",
"tls": false,
"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.",
"FileDescriptors is read from /proc/self/fd on Linux and omitted on other platforms.",
"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": 128,
"run": 1,
"requests": 4456189,
"errors": 0,
"elapsedSeconds": 30.001393348,
"requestsPerSecond": 148532.7347403705,
"p50Millis": 0.589824,
"p95Millis": 2.62144,
"p99Millis": 3.538944,
"serverBytesPerRequest": 4427.911252417705,
"serverAllocsPerRequest": 47.07614668049313,
"sampledPeakHeapAlloc": 6219064,
"sampledPeakFileDescriptors": 135,
"before": {
"heapAlloc": 2105600,
"heapInuse": 4202496,
"stackInuse": 1015808,
"totalAlloc": 4172144,
"mallocs": 30855,
"numGc": 2,
"goroutines": 58,
"connections": 56,
"accepted": 56,
"fileDescriptors": 63
},
"during": {
"heapAlloc": 3500920,
"heapInuse": 5947392,
"stackInuse": 2097152,
"totalAlloc": 19735781560,
"mallocs": 209811062,
"numGc": 6570,
"goroutines": 130,
"connections": 128,
"accepted": 128,
"fileDescriptors": 135
},
"after": {
"heapAlloc": 2828640,
"heapInuse": 5054464,
"stackInuse": 1015808,
"totalAlloc": 19735811920,
"mallocs": 209811396,
"numGc": 6571,
"goroutines": 2,
"connections": 0,
"accepted": 128,
"fileDescriptors": 7
},
"afterReclaim": {
"heapAlloc": 1722584,
"heapInuse": 3530752,
"stackInuse": 1015808,
"totalAlloc": 19735822392,
"mallocs": 209811421,
"numGc": 6572,
"goroutines": 2,
"connections": 0,
"accepted": 128,
"fileDescriptors": 7
}
},
{
"adapter": "native",
"scenario": "traffic",
"concurrency": 128,
"run": 1,
"requests": 4398474,
"errors": 0,
"elapsedSeconds": 30.000375472,
"requestsPerSecond": 146613.96501871088,
"p50Millis": 0.622592,
"p95Millis": 2.62144,
"p99Millis": 3.670016,
"serverBytesPerRequest": 4591.297385411395,
"serverAllocsPerRequest": 59.0589788640333,
"sampledPeakHeapAlloc": 4980920,
"sampledPeakFileDescriptors": 135,
"before": {
"heapAlloc": 2202712,
"heapInuse": 4120576,
"stackInuse": 1671168,
"totalAlloc": 4300920,
"mallocs": 32647,
"numGc": 2,
"goroutines": 131,
"connections": 128,
"accepted": 128,
"fileDescriptors": 135
},
"during": {
"heapAlloc": 4382752,
"heapInuse": 6021120,
"stackInuse": 1638400,
"totalAlloc": 20199003096,
"mallocs": 259802030,
"numGc": 9032,
"goroutines": 131,
"connections": 128,
"accepted": 128,
"fileDescriptors": 135
},
"after": {
"heapAlloc": 2260152,
"heapInuse": 4382720,
"stackInuse": 557056,
"totalAlloc": 20199025048,
"mallocs": 259802362,
"numGc": 9033,
"goroutines": 3,
"connections": 0,
"accepted": 128,
"fileDescriptors": 7
},
"afterReclaim": {
"heapAlloc": 1645880,
"heapInuse": 3489792,
"stackInuse": 557056,
"totalAlloc": 20199035520,
"mallocs": 259802387,
"numGc": 9034,
"goroutines": 3,
"connections": 0,
"accepted": 128,
"fileDescriptors": 7
}
},
{
"adapter": "nethttp",
"scenario": "idle",
"concurrency": 512,
"run": 1,
"errors": 0,
"idleHeapDelta": 5162344,
"idleHeapPerConnection": 10082.703125,
"before": {
"heapAlloc": 1516408,
"heapInuse": 3301376,
"stackInuse": 458752,
"totalAlloc": 3006544,
"mallocs": 23238,
"numGc": 1,
"goroutines": 2,
"connections": 0,
"accepted": 0,
"fileDescriptors": 7
},
"during": {
"heapAlloc": 6678752,
"heapInuse": 8675328,
"stackInuse": 4718592,
"totalAlloc": 10467200,
"mallocs": 59319,
"numGc": 4,
"goroutines": 514,
"connections": 512,
"accepted": 512,
"fileDescriptors": 519
},
"after": {
"heapAlloc": 6108264,
"heapInuse": 8069120,
"stackInuse": 491520,
"totalAlloc": 10563168,
"mallocs": 60468,
"numGc": 5,
"goroutines": 2,
"connections": 0,
"accepted": 512,
"fileDescriptors": 7
},
"afterReclaim": {
"heapAlloc": 1806072,
"heapInuse": 3629056,
"stackInuse": 491520,
"totalAlloc": 10573944,
"mallocs": 60497,
"numGc": 6,
"goroutines": 2,
"connections": 0,
"accepted": 512,
"fileDescriptors": 7
}
},
{
"adapter": "native",
"scenario": "idle",
"concurrency": 512,
"run": 1,
"errors": 0,
"idleHeapDelta": 2692536,
"idleHeapPerConnection": 5258.859375,
"before": {
"heapAlloc": 1511104,
"heapInuse": 3309568,
"stackInuse": 425984,
"totalAlloc": 3001496,
"mallocs": 23227,
"numGc": 1,
"goroutines": 3,
"connections": 0,
"accepted": 0,
"fileDescriptors": 7
},
"during": {
"heapAlloc": 4203640,
"heapInuse": 6283264,
"stackInuse": 4718592,
"totalAlloc": 8087336,
"mallocs": 58784,
"numGc": 4,
"goroutines": 515,
"connections": 512,
"accepted": 512,
"fileDescriptors": 519
},
"after": {
"heapAlloc": 4233984,
"heapInuse": 6316032,
"stackInuse": 458752,
"totalAlloc": 8182368,
"mallocs": 60013,
"numGc": 5,
"goroutines": 3,
"connections": 0,
"accepted": 512,
"fileDescriptors": 7
},
"afterReclaim": {
"heapAlloc": 1804784,
"heapInuse": 3686400,
"stackInuse": 458752,
"totalAlloc": 8193144,
"mallocs": 60042,
"numGc": 6,
"goroutines": 3,
"connections": 0,
"accepted": 512,
"fileDescriptors": 7
}
}
]
}

View File

@@ -0,0 +1,265 @@
{
"createdAt": "2026-09-21T17:27:47Z",
"goVersion": "go1.25.14",
"platform": "linux/arm64",
"gomaxprocs": 4,
"duration": "30s",
"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.",
"FileDescriptors is read from /proc/self/fd on Linux and omitted on other platforms.",
"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": 128,
"run": 1,
"requests": 4442303,
"errors": 0,
"elapsedSeconds": 30.001232972,
"requestsPerSecond": 148070.68109987277,
"p50Millis": 0.589824,
"p95Millis": 2.62144,
"p99Millis": 3.670016,
"serverBytesPerRequest": 4475.325703807237,
"serverAllocsPerRequest": 49.07248492504901,
"sampledPeakHeapAlloc": 7609472,
"sampledPeakFileDescriptors": 135,
"before": {
"heapAlloc": 3340488,
"heapInuse": 5988352,
"stackInuse": 2195456,
"totalAlloc": 9430136,
"mallocs": 83639,
"numGc": 5,
"goroutines": 124,
"connections": 122,
"accepted": 122,
"fileDescriptors": 135
},
"during": {
"heapAlloc": 6485192,
"heapInuse": 8716288,
"stackInuse": 1998848,
"totalAlloc": 19890182936,
"mallocs": 218078486,
"numGc": 5603,
"goroutines": 130,
"connections": 128,
"accepted": 128,
"fileDescriptors": 135
},
"after": {
"heapAlloc": 2853976,
"heapInuse": 5251072,
"stackInuse": 884736,
"totalAlloc": 19890213312,
"mallocs": 218078820,
"numGc": 5604,
"goroutines": 2,
"connections": 0,
"accepted": 128,
"fileDescriptors": 7
},
"afterReclaim": {
"heapAlloc": 1745496,
"heapInuse": 3686400,
"stackInuse": 884736,
"totalAlloc": 19890215312,
"mallocs": 218078842,
"numGc": 5605,
"goroutines": 2,
"connections": 0,
"accepted": 128,
"fileDescriptors": 7
}
},
{
"adapter": "native",
"scenario": "traffic",
"concurrency": 128,
"run": 1,
"requests": 4491731,
"errors": 0,
"elapsedSeconds": 30.000919805,
"requestsPerSecond": 149719.77623337405,
"p50Millis": 0.622592,
"p95Millis": 2.62144,
"p99Millis": 3.538944,
"serverBytesPerRequest": 4807.806184297323,
"serverAllocsPerRequest": 61.06011045630293,
"sampledPeakHeapAlloc": 5859960,
"sampledPeakFileDescriptors": 135,
"before": {
"heapAlloc": 2685024,
"heapInuse": 5136384,
"stackInuse": 2621440,
"totalAlloc": 8854080,
"mallocs": 83570,
"numGc": 5,
"goroutines": 125,
"connections": 122,
"accepted": 122,
"fileDescriptors": 135
},
"during": {
"heapAlloc": 3345480,
"heapInuse": 5980160,
"stackInuse": 1605632,
"totalAlloc": 21604226160,
"mallocs": 274349161,
"numGc": 7802,
"goroutines": 131,
"connections": 128,
"accepted": 128,
"fileDescriptors": 135
},
"after": {
"heapAlloc": 2764288,
"heapInuse": 5341184,
"stackInuse": 1605632,
"totalAlloc": 21604248208,
"mallocs": 274349494,
"numGc": 7803,
"goroutines": 3,
"connections": 0,
"accepted": 128,
"fileDescriptors": 7
},
"afterReclaim": {
"heapAlloc": 1654056,
"heapInuse": 3506176,
"stackInuse": 1605632,
"totalAlloc": 21604258680,
"mallocs": 274349519,
"numGc": 7804,
"goroutines": 3,
"connections": 0,
"accepted": 128,
"fileDescriptors": 7
}
},
{
"adapter": "nethttp",
"scenario": "idle",
"concurrency": 512,
"run": 1,
"errors": 0,
"idleHeapDelta": 7274360,
"idleHeapPerConnection": 14207.734375,
"before": {
"heapAlloc": 1515704,
"heapInuse": 3301376,
"stackInuse": 425984,
"totalAlloc": 3023896,
"mallocs": 23663,
"numGc": 1,
"goroutines": 2,
"connections": 0,
"accepted": 0,
"fileDescriptors": 7
},
"during": {
"heapAlloc": 8790064,
"heapInuse": 12066816,
"stackInuse": 4718592,
"totalAlloc": 29570320,
"mallocs": 272972,
"numGc": 9,
"goroutines": 514,
"connections": 512,
"accepted": 512,
"fileDescriptors": 519
},
"after": {
"heapAlloc": 6138608,
"heapInuse": 8765440,
"stackInuse": 458752,
"totalAlloc": 29675376,
"mallocs": 274141,
"numGc": 10,
"goroutines": 2,
"connections": 0,
"accepted": 512,
"fileDescriptors": 7
},
"afterReclaim": {
"heapAlloc": 1820824,
"heapInuse": 3825664,
"stackInuse": 458752,
"totalAlloc": 29677376,
"mallocs": 274163,
"numGc": 11,
"goroutines": 2,
"connections": 0,
"accepted": 512,
"fileDescriptors": 7
}
},
{
"adapter": "native",
"scenario": "idle",
"concurrency": 512,
"run": 1,
"errors": 0,
"idleHeapDelta": 4689576,
"idleHeapPerConnection": 9159.328125,
"before": {
"heapAlloc": 1520864,
"heapInuse": 3284992,
"stackInuse": 458752,
"totalAlloc": 3029160,
"mallocs": 23661,
"numGc": 1,
"goroutines": 3,
"connections": 0,
"accepted": 0,
"fileDescriptors": 7
},
"during": {
"heapAlloc": 6210440,
"heapInuse": 9314304,
"stackInuse": 8880128,
"totalAlloc": 27150344,
"mallocs": 271905,
"numGc": 10,
"goroutines": 515,
"connections": 512,
"accepted": 512,
"fileDescriptors": 519
},
"after": {
"heapAlloc": 6230688,
"heapInuse": 9330688,
"stackInuse": 491520,
"totalAlloc": 27245184,
"mallocs": 273078,
"numGc": 11,
"goroutines": 3,
"connections": 0,
"accepted": 512,
"fileDescriptors": 7
},
"afterReclaim": {
"heapAlloc": 1817296,
"heapInuse": 3702784,
"stackInuse": 491520,
"totalAlloc": 27255960,
"mallocs": 273107,
"numGc": 12,
"goroutines": 3,
"connections": 0,
"accepted": 512,
"fileDescriptors": 7
}
}
]
}

View File

@@ -25,7 +25,7 @@ def free_address():
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--go", default="go")
parser.add_argument("--version", default="v0.5.0", help="Target framework release")
parser.add_argument("--version", default="v0.6.0-preview", help="Target framework release")
parser.add_argument("--from-version", default="v0.2.0", help="Version used before upgrade")
args = parser.parse_args()

View File

@@ -145,7 +145,7 @@ class Server:
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--go", default="go")
parser.add_argument("--version", default="v0.5.0")
parser.add_argument("--version", default="v0.6.0-preview")
parser.add_argument("--cli", help="Use a locally built CLI")
parser.add_argument("--framework-dir", help="Local framework source for pre-release checks")
parser.add_argument("--image", default="mysql:8.4")

84
scripts/verifyLinux.py Executable file
View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Run GoOmni's repeatable Linux checks inside a pinned Go Docker image."""
import argparse
from pathlib import Path
import shutil
import shlex
import subprocess
import sys
def workspace_output(root: Path, value: str) -> tuple[Path, str]:
requested = Path(value)
host_path = requested if requested.is_absolute() else root / requested
host_path = host_path.resolve()
try:
relative = host_path.relative_to(root)
except ValueError as exc:
raise SystemExit("--output must be inside the repository") from exc
host_path.parent.mkdir(parents=True, exist_ok=True)
return host_path, "/workspace/" + relative.as_posix()
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--image", default="golang:1.25", help="Docker image (default: golang:1.25)")
parser.add_argument("--gomaxprocs", type=int, default=4)
parser.add_argument("--load", action="store_true", help="also run the isolated server matrix")
parser.add_argument("--tls", action="store_true", help="run the load matrix over TLS")
parser.add_argument("--duration", default="2s", help="duration per traffic case (up to 30m)")
parser.add_argument("--count", type=int, default=3)
parser.add_argument("--concurrency", default="1,32,128")
parser.add_argument("--idle", default="128,1024")
parser.add_argument("--output", help="JSON report path inside the repository")
args = parser.parse_args()
if args.gomaxprocs < 1 or args.count < 1:
parser.error("--gomaxprocs and --count must be positive")
if args.tls and not args.load:
parser.error("--tls requires --load")
root = Path(__file__).resolve().parent.parent
docker = shutil.which("docker")
if docker is None:
raise SystemExit("docker is required for Linux verification")
commands = [
"go test -race ./...",
"go vet ./...",
"go build ./...",
]
if args.load:
load = [
"go", "run", "./internal/perfcheck",
"--duration", args.duration,
"--count", str(args.count),
"--concurrency", args.concurrency,
"--idle", args.idle,
]
if args.tls:
load.append("--tls")
if args.output:
_, container_path = workspace_output(root, args.output)
load += ["--output", container_path]
commands.append(" ".join(shlex.quote(part) for part in load))
command = " && ".join(commands)
docker_args = [
docker, "run", "--rm", "--pull=missing",
"-v", f"{root}:/workspace",
"-w", "/workspace",
"-e", "GOWORK=off",
"-e", f"GOMAXPROCS={args.gomaxprocs}",
args.image, "sh", "-c", command,
]
print(f"Linux verification image: {args.image}", flush=True)
print(f"Linux verification command: {command}", flush=True)
result = subprocess.run(docker_args, cwd=root)
if result.returncode != 0:
raise SystemExit(f"Linux verification failed with exit code {result.returncode}")
print("PASS Linux verification", flush=True)
if __name__ == "__main__":
main()

View File

@@ -14,6 +14,8 @@ def main():
parser.add_argument("--load", action="store_true", help="Also compare isolated servers and idle connection memory")
parser.add_argument("--tls", action="store_true", help="Run the isolated load matrix over TLS")
parser.add_argument("--duration", default="2s", help="Duration per load scenario")
parser.add_argument("--concurrency", default="1,32,128")
parser.add_argument("--idle", default="128,1024")
parser.add_argument("--output", help="JSON output path for --load")
args = parser.parse_args()
if args.count < 1:
@@ -32,7 +34,8 @@ def main():
]
if args.load:
command = [args.go, "run", "./internal/perfcheck", "--duration", args.duration,
"--count", str(args.count)]
"--count", str(args.count), "--concurrency", args.concurrency,
"--idle", args.idle]
if args.tls:
command.append("--tls")
if args.output:

View File

@@ -91,7 +91,7 @@ def exerciseServer(binary, directory, env, stopSignal, cleanupMarker=None):
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--go", default="go", help="Go executable")
parser.add_argument("--version", default="v0.5.0", help="Released CLI version to install")
parser.add_argument("--version", default="v0.6.0-preview", help="Released CLI version to install")
parser.add_argument("--cli", help="Use a locally built CLI instead of installing")
parser.add_argument("--framework-version", help="Explicit version passed to the CLI")
parser.add_argument("--framework-dir", help="Local checkout for pre-release verification")

View File

@@ -13,7 +13,7 @@ from verifyScaffold import MODULE, run
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--go", default="go")
parser.add_argument("--version", default="v0.5.0")
parser.add_argument("--version", default="v0.6.0-preview")
parser.add_argument("--framework-dir", help="Use a local checkout before release")
args = parser.parse_args()
env = dict(os.environ, GOWORK="off")

View File

@@ -103,7 +103,7 @@ func TestContract(t *testing.T) {
```sh
go test -race ./transport/... ./examples/reference
python3 scripts/verifyTransports.py --version v0.5.0
python3 scripts/verifyTransports.py --version v0.6.0-preview
```
第一条验证标准库适配器复用完整 Handler / Service / Repository。第二条在源码目录外创建无 replace 的消费模块,从远程版本导入同一套验收包;自研适配器也应使用同一个公共测试包。

View File

@@ -749,7 +749,7 @@ func TestServerConnectionChurnReusesStateSafely(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.MaxConnections = 16
server.MaxConnections = 32
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
@@ -759,8 +759,10 @@ func TestServerConnectionChurnReusesStateSafely(t *testing.T) {
waitForServer(t, listener.Addr().String())
defer stopServer(t, server, listener, done)
const workers = 8
const requestsPerWorker = 100
// Repeated short-lived connections exercise accept/close, connection-pool
// reuse and the MaxConnections gate under a bursty workload.
const workers = 16
const requestsPerWorker = 200
errors := make(chan error, workers)
var group sync.WaitGroup
for worker := 0; worker < workers; worker++ {

View File

@@ -0,0 +1,72 @@
package native_test
import (
"context"
"io"
"net"
"net/http"
"sync/atomic"
"testing"
"time"
"git.yuntuo.hk/k2safe/go-omni/transport/native"
)
func TestServerSlowHeadersCloseBeforeHandler(t *testing.T) {
var handlerCalls atomic.Int32
server := native.New(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
handlerCalls.Add(1)
}))
server.ReadHeaderTimeout = 40 * time.Millisecond
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.Serve(context.Background(), listener) }()
defer stopServer(t, server, listener, done)
waitForServer(t, listener.Addr().String())
conn, err := net.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
if _, err := io.WriteString(conn, "GET /health HTTP/1.1\r\nHost: local"); err != nil {
t.Fatal(err)
}
requirePeerClose(t, conn)
if handlerCalls.Load() != 0 {
t.Fatal("handler ran before slow headers completed")
}
}
func TestServerSlowChunkedBodyClosesBeforeHandler(t *testing.T) {
var handlerCalls atomic.Int32
server := native.New(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
handlerCalls.Add(1)
}))
server.ReadTimeout = 40 * time.Millisecond
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.Serve(context.Background(), listener) }()
defer stopServer(t, server, listener, done)
waitForServer(t, listener.Addr().String())
conn, err := net.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
request := "POST /body HTTP/1.1\r\nHost: local\r\nTransfer-Encoding: chunked\r\n\r\n4\r\npa"
if _, err := io.WriteString(conn, request); err != nil {
t.Fatal(err)
}
requirePeerClose(t, conn)
if handlerCalls.Load() != 0 {
t.Fatal("handler ran before slow chunked body completed")
}
}