feat: observe HTTP2 reset and goaway frames
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- 新增独立 `transport/http2` 实验 Runner:支持 h2c/TLS h2、连接上限、全局请求并发闸门、GOAWAY 排空、RST_STREAM 单流取消和生命周期统计;不改变 native HTTP/1.1 默认路径。
|
- 新增独立 `transport/http2` 实验 Runner:支持 h2c/TLS h2、连接上限、全局请求并发闸门、GOAWAY 排空、RST_STREAM 单流取消和生命周期统计;不改变 native HTTP/1.1 默认路径。
|
||||||
- HTTP/2 实验 Runner 增加标准 1 MiB 头部上限、默认 8 MiB 请求体上限、128 路同连接多路复用、2 MiB 慢读取流控验收、协议错误和 GOAWAY 计数;已声明超限请求在进入业务 handler 前返回 413。
|
- HTTP/2 实验 Runner 增加标准 1 MiB 头部上限、默认 8 MiB 请求体上限、128 路同连接多路复用、2 MiB 慢读取流控验收、协议错误、RST_STREAM 和 GOAWAY 帧计数;已声明超限请求在进入业务 handler 前返回 413。
|
||||||
- 新增可配置时长的 HTTP/2 TLS soak:默认 3 秒,Linux 验收可通过 `GO_OMNI_HTTP2_SOAK_DURATION=30m` 覆盖,混合验证 128 路 stream、慢 handler、请求体、Keep-Alive 和退出回收。
|
- 新增可配置时长的 HTTP/2 TLS soak:默认 3 秒,Linux 验收可通过 `GO_OMNI_HTTP2_SOAK_DURATION=30m` 覆盖,混合验证 128 路 stream、慢 handler、请求体、Keep-Alive 和退出回收。
|
||||||
- 新增 `scripts/verifyHTTP2.py`,固定在 Go 1.25 Linux 容器中运行 HTTP/2 TLS race 长测。
|
- 新增 `scripts/verifyHTTP2.py`,固定在 Go 1.25 Linux 容器中运行 HTTP/2 TLS race 长测。
|
||||||
- native TLS 明确限制 ALPN 为 `http/1.1`:缺省自动宣告该协议,误配置 `h2` 等未实现协议时在启动阶段报错。
|
- native TLS 明确限制 ALPN 为 `http/1.1`:缺省自动宣告该协议,误配置 `h2` 等未实现协议时在启动阶段报错。
|
||||||
|
|||||||
106
transport/http2/frameObserver.go
Normal file
106
transport/http2/frameObserver.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package http2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
frameTypeRSTStream byte = 0x3
|
||||||
|
frameTypeGoAway byte = 0x7
|
||||||
|
)
|
||||||
|
|
||||||
|
type frameDirection uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
frameRead frameDirection = iota
|
||||||
|
frameWrite
|
||||||
|
)
|
||||||
|
|
||||||
|
// observedConn keeps the HTTP/2 implementation opaque while exposing the
|
||||||
|
// small amount of frame-level telemetry that x/net/http2 does not provide.
|
||||||
|
// It parses only the nine-byte frame header and skips payload bytes without
|
||||||
|
// buffering them, so large DATA frames cannot increase telemetry memory.
|
||||||
|
type observedConn struct {
|
||||||
|
net.Conn
|
||||||
|
onFrame func(frameDirection, byte)
|
||||||
|
readParser frameParser
|
||||||
|
writeParser frameParser
|
||||||
|
writeMu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *observedConn) Read(buffer []byte) (int, error) {
|
||||||
|
n, err := c.Conn.Read(buffer)
|
||||||
|
if n > 0 {
|
||||||
|
c.readParser.observe(buffer[:n], frameRead, c.onFrame)
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *observedConn) Write(buffer []byte) (int, error) {
|
||||||
|
c.writeMu.Lock()
|
||||||
|
defer c.writeMu.Unlock()
|
||||||
|
n, err := c.Conn.Write(buffer)
|
||||||
|
if n > 0 {
|
||||||
|
c.writeParser.observe(buffer[:n], frameWrite, c.onFrame)
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// observedTLSConn preserves the connection state hook used by x/net/http2 so
|
||||||
|
// handlers still receive Request.TLS after frame observation is added.
|
||||||
|
type observedTLSConn struct {
|
||||||
|
*observedConn
|
||||||
|
state tls.ConnectionState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *observedTLSConn) ConnectionState() tls.ConnectionState { return c.state }
|
||||||
|
|
||||||
|
type frameParser struct {
|
||||||
|
prefaceRemain int
|
||||||
|
header [9]byte
|
||||||
|
headerBytes int
|
||||||
|
payloadRemain int
|
||||||
|
frameType byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *frameParser) observe(buffer []byte, direction frameDirection, onFrame func(frameDirection, byte)) {
|
||||||
|
for len(buffer) > 0 {
|
||||||
|
if p.prefaceRemain > 0 {
|
||||||
|
skip := p.prefaceRemain
|
||||||
|
if skip > len(buffer) {
|
||||||
|
skip = len(buffer)
|
||||||
|
}
|
||||||
|
p.prefaceRemain -= skip
|
||||||
|
buffer = buffer[skip:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p.payloadRemain > 0 {
|
||||||
|
skip := p.payloadRemain
|
||||||
|
if skip > len(buffer) {
|
||||||
|
skip = len(buffer)
|
||||||
|
}
|
||||||
|
p.payloadRemain -= skip
|
||||||
|
buffer = buffer[skip:]
|
||||||
|
if p.payloadRemain > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
onFrame(direction, p.frameType)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
copied := copy(p.header[p.headerBytes:], buffer)
|
||||||
|
p.headerBytes += copied
|
||||||
|
buffer = buffer[copied:]
|
||||||
|
if p.headerBytes < len(p.header) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.payloadRemain = int(p.header[0])<<16 | int(p.header[1])<<8 | int(p.header[2])
|
||||||
|
p.frameType = p.header[3]
|
||||||
|
p.headerBytes = 0
|
||||||
|
if p.payloadRemain == 0 {
|
||||||
|
onFrame(direction, p.frameType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,6 +160,9 @@ func TestShutdownSendsGoAwayAndDrains(t *testing.T) {
|
|||||||
if err := rctx.Err(); err != nil {
|
if err := rctx.Err(); err != nil {
|
||||||
t.Fatalf("graceful shutdown canceled active request: %v", err)
|
t.Fatalf("graceful shutdown canceled active request: %v", err)
|
||||||
}
|
}
|
||||||
|
if stats := s.Stats(); stats.GoAwayFrames != 1 {
|
||||||
|
t.Fatalf("expected one observed GOAWAY frame, got %+v", stats)
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case err := <-done:
|
case err := <-done:
|
||||||
t.Fatalf("Serve returned before drain: %v", err)
|
t.Fatalf("Serve returned before drain: %v", err)
|
||||||
@@ -214,6 +217,9 @@ func TestRSTStreamCancelsOnlyOneRequest(t *testing.T) {
|
|||||||
case <-time.After(time.Second):
|
case <-time.After(time.Second):
|
||||||
t.Fatal("RST did not cancel request")
|
t.Fatal("RST did not cancel request")
|
||||||
}
|
}
|
||||||
|
if stats := s.Stats(); stats.RSTStreams != 1 {
|
||||||
|
t.Fatalf("expected one observed RST_STREAM, got %+v", stats)
|
||||||
|
}
|
||||||
release <- struct{}{}
|
release <- struct{}{}
|
||||||
if got := p.body(t, 3); got != "/survive" {
|
if got := p.body(t, 3); got != "/survive" {
|
||||||
t.Fatalf("sibling body=%q", got)
|
t.Fatalf("sibling body=%q", got)
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ type Server struct {
|
|||||||
rejectedReqs atomic.Uint64
|
rejectedReqs atomic.Uint64
|
||||||
protocolErrors atomic.Uint64
|
protocolErrors atomic.Uint64
|
||||||
goAwaySent atomic.Uint64
|
goAwaySent atomic.Uint64
|
||||||
|
rstStreams atomic.Uint64
|
||||||
|
goAwayFrames atomic.Uint64
|
||||||
activeConns atomic.Int64
|
activeConns atomic.Int64
|
||||||
activeRequests atomic.Int64
|
activeRequests atomic.Int64
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
@@ -72,6 +74,8 @@ type Stats struct {
|
|||||||
RejectedRequests uint64 `json:"rejectedRequests"`
|
RejectedRequests uint64 `json:"rejectedRequests"`
|
||||||
ProtocolErrors uint64 `json:"protocolErrors"`
|
ProtocolErrors uint64 `json:"protocolErrors"`
|
||||||
GoAwaySent uint64 `json:"goAwaySent"`
|
GoAwaySent uint64 `json:"goAwaySent"`
|
||||||
|
RSTStreams uint64 `json:"rstStreams"`
|
||||||
|
GoAwayFrames uint64 `json:"goAwayFrames"`
|
||||||
ActiveConnections int64 `json:"activeConnections"`
|
ActiveConnections int64 `json:"activeConnections"`
|
||||||
ActiveRequests int64 `json:"activeRequests"`
|
ActiveRequests int64 `json:"activeRequests"`
|
||||||
}
|
}
|
||||||
@@ -97,6 +101,8 @@ func (s *Server) Stats() Stats {
|
|||||||
RejectedRequests: s.rejectedReqs.Load(),
|
RejectedRequests: s.rejectedReqs.Load(),
|
||||||
ProtocolErrors: s.protocolErrors.Load(),
|
ProtocolErrors: s.protocolErrors.Load(),
|
||||||
GoAwaySent: s.goAwaySent.Load(),
|
GoAwaySent: s.goAwaySent.Load(),
|
||||||
|
RSTStreams: s.rstStreams.Load(),
|
||||||
|
GoAwayFrames: s.goAwayFrames.Load(),
|
||||||
ActiveConnections: s.activeConns.Load(),
|
ActiveConnections: s.activeConns.Load(),
|
||||||
ActiveRequests: s.activeRequests.Load(),
|
ActiveRequests: s.activeRequests.Load(),
|
||||||
}
|
}
|
||||||
@@ -356,7 +362,23 @@ func (s *Server) serveConn(ctx context.Context, protocolServer *xhttp2.Server, b
|
|||||||
}
|
}
|
||||||
s.protocolConns[conn] = struct{}{}
|
s.protocolConns[conn] = struct{}{}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
protocolServer.ServeConn(conn, &xhttp2.ServeConnOpts{
|
observed := &observedConn{
|
||||||
|
Conn: conn,
|
||||||
|
readParser: frameParser{prefaceRemain: 24},
|
||||||
|
onFrame: func(direction frameDirection, frameType byte) {
|
||||||
|
if direction == frameRead && frameType == frameTypeRSTStream {
|
||||||
|
s.rstStreams.Add(1)
|
||||||
|
}
|
||||||
|
if direction == frameWrite && frameType == frameTypeGoAway {
|
||||||
|
s.goAwayFrames.Add(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var protocolConn net.Conn = observed
|
||||||
|
if tlsConn, ok := conn.(*tls.Conn); ok {
|
||||||
|
protocolConn = &observedTLSConn{observedConn: observed, state: tlsConn.ConnectionState()}
|
||||||
|
}
|
||||||
|
protocolServer.ServeConn(protocolConn, &xhttp2.ServeConnOpts{
|
||||||
Context: ctx, Handler: handler, BaseConfig: baseConfig,
|
Context: ctx, Handler: handler, BaseConfig: baseConfig,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
## 必须可观测的状态
|
## 必须可观测的状态
|
||||||
|
|
||||||
HTTP/2 版本需要在 native `Stats` 之外至少记录:活动 stream、被拒绝 stream、连接级 protocol error、RST_STREAM、GOAWAY 发送次数,以及因流控或并发闸门触发的拒绝。实验 Runner 当前已通过 `ActiveRequests`、`RejectedRequests`、`ProtocolErrors` 和 `GoAwaySent` 暴露活动/拒绝/协议错误/优雅关闭快照;RST_STREAM 的独立计数仍属于进入 native 前的补齐项。计数必须是原子快照,不能在 metrics handler 中锁住连接状态机。
|
HTTP/2 版本需要在 native `Stats` 之外至少记录:活动 stream、被拒绝 stream、连接级 protocol error、RST_STREAM、GOAWAY 发送次数,以及因流控或并发闸门触发的拒绝。实验 Runner 当前已通过 `ActiveRequests`、`RejectedRequests`、`ProtocolErrors`、`RSTStreams`、`GoAwaySent` 和 `GoAwayFrames` 暴露活动/拒绝/协议错误/客户端重置/优雅关闭快照;帧观察器只解析 9 字节头部并跳过 payload,不会缓存大 body。计数必须是原子快照,不能在 metrics handler 中锁住连接状态机。
|
||||||
|
|
||||||
## 进入实现前的验收矩阵
|
## 进入实现前的验收矩阵
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user