feat: expose native server stats
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- 新增 transport-neutral `metrics` 包,支持请求数、错误数、in-flight、累计耗时和状态分布,并可直接注册 `/metrics` 接口。
|
- 新增 transport-neutral `metrics` 包,支持请求数、错误数、in-flight、累计耗时和状态分布,并可直接注册 `/metrics` 接口。
|
||||||
|
- native 新增 `Server.Stats()`,提供 accepted connections、active connections 和 active requests 生命周期指标。
|
||||||
- native 请求体改为流式读取,Content-Length 与 chunked body 不再在进入 Handler 前整体复制到内存;保留大小限制、读超时、trailer 校验和 Keep-Alive 排空。
|
- native 请求体改为流式读取,Content-Length 与 chunked body 不再在进入 Handler 前整体复制到内存;保留大小限制、读超时、trailer 校验和 Keep-Alive 排空。
|
||||||
- 统一标准库与 native 的真实 TCP benchmark,增加固定并发档位、响应状态与读写错误检查。
|
- 统一标准库与 native 的真实 TCP benchmark,增加固定并发档位、响应状态与读写错误检查。
|
||||||
- 新增独立进程并发与长连接内存测量工具,记录成功吞吐、P95/P99、服务端分配、空闲连接堆内存和回收情况;修正并行 `ns/op` 与客户端/服务端混合分配的解释。
|
- 新增独立进程并发与长连接内存测量工具,记录成功吞吐、P95/P99、服务端分配、空闲连接堆内存和回收情况;修正并行 `ns/op` 与客户端/服务端混合分配的解释。
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ if err := app.RunWith(ctx, native.New(nil), ":8080"); err != nil {
|
|||||||
|
|
||||||
`transport/native` 当前支持 HTTP/1.1、Keep-Alive、`Content-Length` 和 chunked 请求体、`http.Flusher` 流式响应、可选 TLS、读写超时和优雅关闭;不支持的传输编码会被拒绝。它会复用连接状态和响应对象,并可通过 `MaxConnections`、`MaxConcurrentRequests` 限制内存峰值。它仍通过标准 HTTP 请求/响应接口连接业务层,适合在现有 Handler / Service 上做网络层验证。
|
`transport/native` 当前支持 HTTP/1.1、Keep-Alive、`Content-Length` 和 chunked 请求体、`http.Flusher` 流式响应、可选 TLS、读写超时和优雅关闭;不支持的传输编码会被拒绝。它会复用连接状态和响应对象,并可通过 `MaxConnections`、`MaxConcurrentRequests` 限制内存峰值。它仍通过标准 HTTP 请求/响应接口连接业务层,适合在现有 Handler / Service 上做网络层验证。
|
||||||
|
|
||||||
|
自研网络层还提供 `server.Stats()`,可读取 accepted、active connections 和 active requests,与上面的业务请求 metrics 组合后能区分连接堆积和 Handler 慢请求。
|
||||||
|
|
||||||
业务代码可以直接使用 `git.yuntuo.hk/k2safe/go-omni/logx` 打结构化日志,不需要依赖具体网络库:
|
业务代码可以直接使用 `git.yuntuo.hk/k2safe/go-omni/logx` 打结构化日志,不需要依赖具体网络库:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ registerRoutes(application) // 与 omni.New() 复用同一份业务注册代码
|
|||||||
|
|
||||||
`transport/native` 是第一版自研 HTTP/1.1 服务端:它自己管理 TCP listener、连接、Keep-Alive、请求解析、响应写回和优雅关闭,再把请求交给现有 `http.Handler`。请求体支持 `Content-Length` 和 chunked,并统一受 `MaxBodyBytes` 限制;其他传输编码会被拒绝。它仍使用标准 `http.Request` / `http.ResponseWriter` 作为业务桥接,因此不会影响现有 Handler / Service。
|
`transport/native` 是第一版自研 HTTP/1.1 服务端:它自己管理 TCP listener、连接、Keep-Alive、请求解析、响应写回和优雅关闭,再把请求交给现有 `http.Handler`。请求体支持 `Content-Length` 和 chunked,并统一受 `MaxBodyBytes` 限制;其他传输编码会被拒绝。它仍使用标准 `http.Request` / `http.ResponseWriter` 作为业务桥接,因此不会影响现有 Handler / Service。
|
||||||
|
|
||||||
|
native 提供 `server.Stats()` 读取已接受连接数、当前连接数和当前请求数,适合和 `metrics.Metrics` 的业务请求指标一起采集;读取不会阻塞连接生命周期。
|
||||||
|
|
||||||
业务 Handler 需要流式返回时可以断言 `http.Flusher` 并调用 `Flush()`;HTTP/1.1 会自动使用 chunked 响应,普通未 Flush 的响应仍自动生成 `Content-Length`。HTTP/1.0 没有 chunked framing,会退回到写完后关闭连接。
|
业务 Handler 需要流式返回时可以断言 `http.Flusher` 并调用 `Flush()`;HTTP/1.1 会自动使用 chunked 响应,普通未 Flush 的响应仍自动生成 `Content-Length`。HTTP/1.0 没有 chunked framing,会退回到写完后关闭连接。
|
||||||
|
|
||||||
Handler 运行超过约 5 ms 时,native 启动一次阻塞读取监测;读到 EOF 或连接错误会取消 `Request.Context()`,流式写入失败也会触发取消。Handler 返回后先停止监测,再复用连接和 Reader;普通短请求不会启动监测 goroutine。
|
Handler 运行超过约 5 ms 时,native 启动一次阻塞读取监测;读到 EOF 或连接错误会取消 `Request.Context()`,流式写入失败也会触发取消。Handler 返回后先停止监测,再复用连接和 Reader;普通短请求不会启动监测 goroutine。
|
||||||
|
|||||||
@@ -62,17 +62,42 @@ type Server struct {
|
|||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
connSlots chan struct{}
|
connSlots chan struct{}
|
||||||
requestSlots chan struct{}
|
requestSlots chan struct{}
|
||||||
|
accepted atomic.Uint64
|
||||||
|
activeConns atomic.Int64
|
||||||
|
activeRequests atomic.Int64
|
||||||
readerPool sync.Pool
|
readerPool sync.Pool
|
||||||
writerPool sync.Pool
|
writerPool sync.Pool
|
||||||
connectionPool sync.Pool
|
connectionPool sync.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stats is a point-in-time view of the native server lifecycle. Counters are
|
||||||
|
// monotonic unless their name starts with Active.
|
||||||
|
type Stats struct {
|
||||||
|
AcceptedConnections uint64 `json:"acceptedConnections"`
|
||||||
|
ActiveConnections int64 `json:"activeConnections"`
|
||||||
|
ActiveRequests int64 `json:"activeRequests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns native connection and request counters without taking the
|
||||||
|
// server lifecycle mutex, so it is safe to call from a metrics endpoint.
|
||||||
|
func (s *Server) Stats() Stats {
|
||||||
|
if s == nil {
|
||||||
|
return Stats{}
|
||||||
|
}
|
||||||
|
return Stats{
|
||||||
|
AcceptedConnections: s.accepted.Load(),
|
||||||
|
ActiveConnections: s.activeConns.Load(),
|
||||||
|
ActiveRequests: s.activeRequests.Load(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type connection struct {
|
type connection struct {
|
||||||
server *Server
|
server *Server
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
active atomic.Bool
|
active atomic.Bool
|
||||||
closeWhenIdle atomic.Bool
|
closeWhenIdle atomic.Bool
|
||||||
closed atomic.Bool
|
closed atomic.Bool
|
||||||
|
counted bool
|
||||||
slotHeld bool
|
slotHeld bool
|
||||||
closeDone chan struct{}
|
closeDone chan struct{}
|
||||||
}
|
}
|
||||||
@@ -222,6 +247,7 @@ func (s *Server) Serve(ctx context.Context, listener net.Listener) error {
|
|||||||
serveErr = err
|
serveErr = err
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
s.accepted.Add(1)
|
||||||
if slots := s.connSlots; slots != nil {
|
if slots := s.connSlots; slots != nil {
|
||||||
select {
|
select {
|
||||||
case slots <- struct{}{}:
|
case slots <- struct{}{}:
|
||||||
@@ -236,8 +262,10 @@ func (s *Server) Serve(ctx context.Context, listener net.Listener) error {
|
|||||||
state.active.Store(false)
|
state.active.Store(false)
|
||||||
state.closeWhenIdle.Store(false)
|
state.closeWhenIdle.Store(false)
|
||||||
state.closed.Store(false)
|
state.closed.Store(false)
|
||||||
|
state.counted = true
|
||||||
state.slotHeld = s.connSlots != nil
|
state.slotHeld = s.connSlots != nil
|
||||||
state.closeDone = make(chan struct{})
|
state.closeDone = make(chan struct{})
|
||||||
|
s.activeConns.Add(1)
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if s.closing {
|
if s.closing {
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
@@ -379,6 +407,7 @@ func (s *Server) serveConnection(baseCtx context.Context, state *connection) {
|
|||||||
requestContext, cancel := context.WithCancel(baseCtx)
|
requestContext, cancel := context.WithCancel(baseCtx)
|
||||||
request = request.WithContext(requestContext)
|
request = request.WithContext(requestContext)
|
||||||
state.active.Store(true)
|
state.active.Store(true)
|
||||||
|
s.activeRequests.Add(1)
|
||||||
writer := s.writerPool.Get().(*responseWriter)
|
writer := s.writerPool.Get().(*responseWriter)
|
||||||
writer.reset(request.Method)
|
writer.reset(request.Method)
|
||||||
writer.configure(request, state.conn, closeAfter, s.WriteTimeout, cancel)
|
writer.configure(request, state.conn, closeAfter, s.WriteTimeout, cancel)
|
||||||
@@ -392,6 +421,7 @@ func (s *Server) serveConnection(baseCtx context.Context, state *connection) {
|
|||||||
writer.reset("")
|
writer.reset("")
|
||||||
s.writerPool.Put(writer)
|
s.writerPool.Put(writer)
|
||||||
state.active.Store(false)
|
state.active.Store(false)
|
||||||
|
s.activeRequests.Add(-1)
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
@@ -403,6 +433,7 @@ func (s *Server) serveConnection(baseCtx context.Context, state *connection) {
|
|||||||
writer.reset("")
|
writer.reset("")
|
||||||
s.writerPool.Put(writer)
|
s.writerPool.Put(writer)
|
||||||
state.active.Store(false)
|
state.active.Store(false)
|
||||||
|
s.activeRequests.Add(-1)
|
||||||
if closeAfter || closing {
|
if closeAfter || closing {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -439,6 +470,7 @@ func (s *Server) serveConnection(baseCtx context.Context, state *connection) {
|
|||||||
writer.reset("")
|
writer.reset("")
|
||||||
s.writerPool.Put(writer)
|
s.writerPool.Put(writer)
|
||||||
state.active.Store(false)
|
state.active.Store(false)
|
||||||
|
s.activeRequests.Add(-1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Request bodies are streamed into the handler, so a malformed
|
// Request bodies are streamed into the handler, so a malformed
|
||||||
@@ -460,6 +492,7 @@ func (s *Server) serveConnection(baseCtx context.Context, state *connection) {
|
|||||||
writer.reset("")
|
writer.reset("")
|
||||||
s.writerPool.Put(writer)
|
s.writerPool.Put(writer)
|
||||||
state.active.Store(false)
|
state.active.Store(false)
|
||||||
|
s.activeRequests.Add(-1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if requestSlot {
|
if requestSlot {
|
||||||
@@ -469,6 +502,7 @@ func (s *Server) serveConnection(baseCtx context.Context, state *connection) {
|
|||||||
writer.reset("")
|
writer.reset("")
|
||||||
s.writerPool.Put(writer)
|
s.writerPool.Put(writer)
|
||||||
state.active.Store(false)
|
state.active.Store(false)
|
||||||
|
s.activeRequests.Add(-1)
|
||||||
if shouldClose {
|
if shouldClose {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -482,6 +516,10 @@ func (c *connection) close() {
|
|||||||
c.server.mu.Lock()
|
c.server.mu.Lock()
|
||||||
delete(c.server.conns, c)
|
delete(c.server.conns, c)
|
||||||
c.server.mu.Unlock()
|
c.server.mu.Unlock()
|
||||||
|
if c.counted {
|
||||||
|
c.server.activeConns.Add(-1)
|
||||||
|
c.counted = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -789,6 +789,57 @@ func TestServerConnectionLimitReleasesAfterRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerStatsTrackLifecycle(t *testing.T) {
|
||||||
|
started := make(chan struct{})
|
||||||
|
release := make(chan struct{})
|
||||||
|
application := omni.NewWithRouter(netrouter.New())
|
||||||
|
application.GET("/hold", func(ctx *omni.Context) error {
|
||||||
|
close(started)
|
||||||
|
<-release
|
||||||
|
return ctx.OK("done")
|
||||||
|
})
|
||||||
|
server, listener, done := startServer(t, application)
|
||||||
|
defer stopServer(t, server, listener, done)
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
waitForStats := func(check func(native.Stats) bool) {
|
||||||
|
t.Helper()
|
||||||
|
deadline := time.Now().Add(time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if check(server.Stats()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Fatalf("stats condition not reached: %+v", server.Stats())
|
||||||
|
}
|
||||||
|
waitForStats(func(stats native.Stats) bool { return stats.ActiveConnections == 1 && stats.AcceptedConnections >= 1 })
|
||||||
|
if _, err := io.WriteString(conn, "GET /hold HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-started:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("hold handler did not start")
|
||||||
|
}
|
||||||
|
waitForStats(func(stats native.Stats) bool { return stats.ActiveRequests == 1 })
|
||||||
|
close(release)
|
||||||
|
response, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: http.MethodGet})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
response.Body.Close()
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected hold response: %d", response.StatusCode)
|
||||||
|
}
|
||||||
|
waitForStats(func(stats native.Stats) bool { return stats.ActiveRequests == 0 })
|
||||||
|
waitForStats(func(stats native.Stats) bool { return stats.ActiveConnections == 0 })
|
||||||
|
}
|
||||||
|
|
||||||
func TestServerConnectionChurnReusesStateSafely(t *testing.T) {
|
func TestServerConnectionChurnReusesStateSafely(t *testing.T) {
|
||||||
application := omni.NewWithRouter(netrouter.New())
|
application := omni.NewWithRouter(netrouter.New())
|
||||||
application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") })
|
application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") })
|
||||||
|
|||||||
Reference in New Issue
Block a user