feat: expose HTTP2 header and protocol limits
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 增加默认 8 MiB 请求体上限、128 路同连接多路复用和 2 MiB 慢读取流控验收;超限请求在进入业务 handler 前返回 413。
|
- HTTP/2 实验 Runner 增加标准 1 MiB 头部上限、默认 8 MiB 请求体上限、128 路同连接多路复用、2 MiB 慢读取流控验收和协议错误计数;已声明超限请求在进入业务 handler 前返回 413。
|
||||||
- native TLS 明确限制 ALPN 为 `http/1.1`:缺省自动宣告该协议,误配置 `h2` 等未实现协议时在启动阶段报错。
|
- native TLS 明确限制 ALPN 为 `http/1.1`:缺省自动宣告该协议,误配置 `h2` 等未实现协议时在启动阶段报错。
|
||||||
- native `Server.Stats()` 新增连接和请求过载拒绝计数;新增可信代理配置,访问日志只在 socket 对端属于配置网段时解析 `X-Forwarded-For`。
|
- native `Server.Stats()` 新增连接和请求过载拒绝计数;新增可信代理配置,访问日志只在 socket 对端属于配置网段时解析 `X-Forwarded-For`。
|
||||||
- 新增 transport-neutral `metrics` 包,支持请求数、错误数、in-flight、累计耗时和状态分布,并可直接注册 `/metrics` 接口。
|
- 新增 transport-neutral `metrics` 包,支持请求数、错误数、in-flight、累计耗时和状态分布,并可直接注册 `/metrics` 接口。
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type Server struct {
|
|||||||
MaxConnections int
|
MaxConnections int
|
||||||
MaxConcurrentRequests int
|
MaxConcurrentRequests int
|
||||||
MaxConcurrentStreams uint32
|
MaxConcurrentStreams uint32
|
||||||
|
MaxHeaderBytes int
|
||||||
MaxBodyBytes int64
|
MaxBodyBytes int64
|
||||||
IdleTimeout time.Duration
|
IdleTimeout time.Duration
|
||||||
TLSHandshakeTimeout time.Duration
|
TLSHandshakeTimeout time.Duration
|
||||||
@@ -55,6 +56,7 @@ type Server struct {
|
|||||||
accepted atomic.Uint64
|
accepted atomic.Uint64
|
||||||
rejectedConns atomic.Uint64
|
rejectedConns atomic.Uint64
|
||||||
rejectedReqs atomic.Uint64
|
rejectedReqs atomic.Uint64
|
||||||
|
protocolErrors atomic.Uint64
|
||||||
activeConns atomic.Int64
|
activeConns atomic.Int64
|
||||||
activeRequests atomic.Int64
|
activeRequests atomic.Int64
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
@@ -66,6 +68,7 @@ type Stats struct {
|
|||||||
AcceptedConnections uint64 `json:"acceptedConnections"`
|
AcceptedConnections uint64 `json:"acceptedConnections"`
|
||||||
RejectedConnections uint64 `json:"rejectedConnections"`
|
RejectedConnections uint64 `json:"rejectedConnections"`
|
||||||
RejectedRequests uint64 `json:"rejectedRequests"`
|
RejectedRequests uint64 `json:"rejectedRequests"`
|
||||||
|
ProtocolErrors uint64 `json:"protocolErrors"`
|
||||||
ActiveConnections int64 `json:"activeConnections"`
|
ActiveConnections int64 `json:"activeConnections"`
|
||||||
ActiveRequests int64 `json:"activeRequests"`
|
ActiveRequests int64 `json:"activeRequests"`
|
||||||
}
|
}
|
||||||
@@ -89,6 +92,7 @@ func (s *Server) Stats() Stats {
|
|||||||
AcceptedConnections: s.accepted.Load(),
|
AcceptedConnections: s.accepted.Load(),
|
||||||
RejectedConnections: s.rejectedConns.Load(),
|
RejectedConnections: s.rejectedConns.Load(),
|
||||||
RejectedRequests: s.rejectedReqs.Load(),
|
RejectedRequests: s.rejectedReqs.Load(),
|
||||||
|
ProtocolErrors: s.protocolErrors.Load(),
|
||||||
ActiveConnections: s.activeConns.Load(),
|
ActiveConnections: s.activeConns.Load(),
|
||||||
ActiveRequests: s.activeRequests.Load(),
|
ActiveRequests: s.activeRequests.Load(),
|
||||||
}
|
}
|
||||||
@@ -226,8 +230,11 @@ func (s *Server) Serve(ctx context.Context, listener net.Listener) error {
|
|||||||
protocolServer := &xhttp2.Server{
|
protocolServer := &xhttp2.Server{
|
||||||
MaxConcurrentStreams: s.MaxConcurrentStreams,
|
MaxConcurrentStreams: s.MaxConcurrentStreams,
|
||||||
IdleTimeout: s.IdleTimeout,
|
IdleTimeout: s.IdleTimeout,
|
||||||
|
CountError: func(string) {
|
||||||
|
s.protocolErrors.Add(1)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
shutdownConfig := &http.Server{IdleTimeout: s.IdleTimeout}
|
shutdownConfig := &http.Server{IdleTimeout: s.IdleTimeout, MaxHeaderBytes: s.MaxHeaderBytes}
|
||||||
if err := xhttp2.ConfigureServer(shutdownConfig, protocolServer); err != nil {
|
if err := xhttp2.ConfigureServer(shutdownConfig, protocolServer); err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -175,6 +176,46 @@ func TestServerRejectsBodyAboveLimitAndRecovers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerRejectsHeadersAboveLimitAndRecovers(t *testing.T) {
|
||||||
|
server := New(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
_, _ = io.WriteString(writer, "ok")
|
||||||
|
}))
|
||||||
|
server.MaxHeaderBytes = 1024
|
||||||
|
address, stop := startServer(t, server)
|
||||||
|
defer stop()
|
||||||
|
transport := &xhttp2.Transport{
|
||||||
|
AllowHTTP: true,
|
||||||
|
DialTLSContext: func(ctx context.Context, network, address string, _ *tls.Config) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, network, address)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
client := &http.Client{Transport: transport, Timeout: 3 * time.Second}
|
||||||
|
defer transport.CloseIdleConnections()
|
||||||
|
|
||||||
|
request, err := http.NewRequest(http.MethodGet, "http://"+address+"/too-large", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
request.Header.Set("X-Large", strings.Repeat("x", 2048))
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err == nil {
|
||||||
|
response.Body.Close()
|
||||||
|
if response.StatusCode < http.StatusBadRequest {
|
||||||
|
t.Fatalf("oversized headers unexpectedly succeeded with status %d", response.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err = client.Get("http://" + address + "/recovered")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body, readErr := io.ReadAll(response.Body)
|
||||||
|
response.Body.Close()
|
||||||
|
if readErr != nil || response.StatusCode != http.StatusOK || string(body) != "ok" {
|
||||||
|
t.Fatalf("header limit did not recover: status=%d body=%q err=%v", response.StatusCode, body, readErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServerMultiplexes128Streams(t *testing.T) {
|
func TestServerMultiplexes128Streams(t *testing.T) {
|
||||||
started := make(chan struct{}, 128)
|
started := make(chan struct{}, 128)
|
||||||
release := make(chan struct{})
|
release := make(chan struct{})
|
||||||
@@ -196,7 +237,8 @@ func TestServerMultiplexes128Streams(t *testing.T) {
|
|||||||
address, stop := startServer(t, server)
|
address, stop := startServer(t, server)
|
||||||
defer stop()
|
defer stop()
|
||||||
transport := &xhttp2.Transport{
|
transport := &xhttp2.Transport{
|
||||||
AllowHTTP: true,
|
AllowHTTP: true,
|
||||||
|
StrictMaxConcurrentStreams: true,
|
||||||
DialTLSContext: func(ctx context.Context, network, address string, _ *tls.Config) (net.Conn, error) {
|
DialTLSContext: func(ctx context.Context, network, address string, _ *tls.Config) (net.Conn, error) {
|
||||||
return (&net.Dialer{}).DialContext(ctx, network, address)
|
return (&net.Dialer{}).DialContext(ctx, network, address)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
当前 `transport/native` 是经过 Linux 长连接和反向代理验收的 HTTP/1.1 实现。HTTP/2 暂不直接打开,原因是它把一个 TCP 连接变成多个独立请求流,连接级的 Keep-Alive、超时和并发限制不能直接复用现有 HTTP/1.1 状态机。
|
当前 `transport/native` 是经过 Linux 长连接和反向代理验收的 HTTP/1.1 实现。HTTP/2 暂不直接打开,原因是它把一个 TCP 连接变成多个独立请求流,连接级的 Keep-Alive、超时和并发限制不能直接复用现有 HTTP/1.1 状态机。
|
||||||
|
|
||||||
当前已提供独立的 `transport/http2` 实验 Runner。它已经验证 TLS/h2、h2c、本地连接和请求闸门、GOAWAY 排空以及 RST_STREAM 取消;这些结果用于验证生命周期设计,不代表 native 已经启用 HTTP/2。实验 Runner 还覆盖了默认 8 MiB 请求体上限、128 路同连接 stream 多路复用,以及 2 MiB 慢读取 body 在窗口耗尽后的恢复。
|
当前已提供独立的 `transport/http2` 实验 Runner。它已经验证 TLS/h2、h2c、本地连接和请求闸门、GOAWAY 排空以及 RST_STREAM 取消;这些结果用于验证生命周期设计,不代表 native 已经启用 HTTP/2。实验 Runner 还覆盖了默认 1 MiB 头部上限、8 MiB 请求体上限、128 路同连接 stream 多路复用,以及 2 MiB 慢读取 body 在窗口耗尽后的恢复。
|
||||||
|
|
||||||
## 目标边界
|
## 目标边界
|
||||||
|
|
||||||
@@ -17,12 +17,12 @@
|
|||||||
2. TLS 协商得到 `h2` 后进入 HTTP/2 connection state;其他 ALPN 直接关闭,不能把 HTTP/2 preface 交给 HTTP/1.1 解析器。
|
2. TLS 协商得到 `h2` 后进入 HTTP/2 connection state;其他 ALPN 直接关闭,不能把 HTTP/2 preface 交给 HTTP/1.1 解析器。
|
||||||
3. 每个 stream 创建独立的 `http.Request` 和取消 context。`RST_STREAM` 只取消当前请求,TCP 断开才取消该连接上的所有请求。
|
3. 每个 stream 创建独立的 `http.Request` 和取消 context。`RST_STREAM` 只取消当前请求,TCP 断开才取消该连接上的所有请求。
|
||||||
4. `MaxConcurrentRequests` 是所有连接共享的全局请求闸门;`MaxConcurrentStreams` 限制单连接并发流;两者都必须在收到完整请求体前生效。
|
4. `MaxConcurrentRequests` 是所有连接共享的全局请求闸门;`MaxConcurrentStreams` 限制单连接并发流;两者都必须在收到完整请求体前生效。
|
||||||
5. `MaxBodyBytes` 默认限制请求体为 8 MiB;已声明超限的请求在进入业务 handler 前返回 413,未声明长度的 body 通过受限 reader 读取,避免业务层无意间把整个请求体加载到内存。
|
5. `MaxHeaderBytes` 沿用标准库默认 1 MiB 上限,`MaxBodyBytes` 默认限制请求体为 8 MiB;已声明超限的请求在进入业务 handler 前返回 413,未声明长度的 body 通过受限 reader 读取,避免业务层无意间把整个请求体加载到内存。
|
||||||
6. 服务关闭时停止 accept,发送 GOAWAY 拒绝新流,等待活动流排空;超过 `ShutdownTimeout` 后关闭连接并取消剩余请求。实验 Runner 已覆盖这条顺序。
|
6. 服务关闭时停止 accept,发送 GOAWAY 拒绝新流,等待活动流排空;超过 `ShutdownTimeout` 后关闭连接并取消剩余请求。实验 Runner 已覆盖这条顺序。
|
||||||
|
|
||||||
## 必须可观测的状态
|
## 必须可观测的状态
|
||||||
|
|
||||||
HTTP/2 版本需要在 native `Stats` 之外至少记录:活动 stream、被拒绝 stream、连接级 protocol error、RST_STREAM、GOAWAY 发送次数,以及因流控或并发闸门触发的拒绝。计数必须是原子快照,不能在 metrics handler 中锁住连接状态机。
|
HTTP/2 版本需要在 native `Stats` 之外至少记录:活动 stream、被拒绝 stream、连接级 protocol error、RST_STREAM、GOAWAY 发送次数,以及因流控或并发闸门触发的拒绝。实验 Runner 当前已通过 `ActiveRequests`、`RejectedRequests` 和 `ProtocolErrors` 暴露活动/拒绝/协议错误快照;RST_STREAM 和 GOAWAY 的独立计数仍属于进入 native 前的补齐项。计数必须是原子快照,不能在 metrics handler 中锁住连接状态机。
|
||||||
|
|
||||||
## 进入实现前的验收矩阵
|
## 进入实现前的验收矩阵
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user