feat: configure gateway transport pools
This commit is contained in:
parent
d4539da9c5
commit
bda9cc03df
@ -16,6 +16,11 @@ gateway:
|
||||
limits:
|
||||
maxConcurrentConnections: 20000
|
||||
requestsPerMinutePerClient: 60000
|
||||
transport:
|
||||
maxIdleConns: 20000
|
||||
maxIdleConnsPerHost: 32
|
||||
maxConnsPerHost: 32
|
||||
tunnelBufferBytes: 32768
|
||||
retry:
|
||||
maxAttempts: 2
|
||||
retryMethods: [GET, HEAD]
|
||||
|
||||
@ -333,6 +333,16 @@ gateway:
|
||||
auth: {mode: none}
|
||||
limits:
|
||||
maxConcurrentConnections: 50000
|
||||
transport:
|
||||
dialTimeout: 10s
|
||||
handshakeTimeout: 15s
|
||||
responseHeaderTimeout: 30s
|
||||
idleConnTimeout: 90s
|
||||
maxIdleConns: 20000
|
||||
maxIdleConnsPerHost: 32
|
||||
maxConnsPerHost: 32
|
||||
tunnelBufferBytes: 32768
|
||||
tunnelIdleTimeout: 5m
|
||||
retry:
|
||||
maxAttempts: 2
|
||||
retryMethods: [GET, HEAD]
|
||||
@ -358,6 +368,31 @@ gateway:
|
||||
- 保留地址、CGNAT 与云元数据端点始终拒绝,不能通过私网/链路本地开关放行。
|
||||
- `maxConcurrentConnections` 是入口准入上限,不是 Proxy 容量上限。
|
||||
|
||||
### 5.1 Gateway 本地传输层
|
||||
|
||||
**gateway.transport** 只配置每个 Gateway Worker 的本地 HTTP Transport 和 CONNECT
|
||||
隧道 I/O,不读取或写入 Redis、PostgreSQL、Provider,也不参与热路径分配。修改
|
||||
这些字段需要滚动重启对应 Gateway Worker 后生效。
|
||||
|
||||
- **dialTimeout**、**handshakeTimeout**、**responseHeaderTimeout**、
|
||||
**idleConnTimeout**:分别限制拨号、上游 TLS/CONNECT 握手、HTTP 响应头和空闲
|
||||
HTTP 连接的时长。
|
||||
- **maxIdleConns**:单个本地 HTTP Transport 的总空闲连接上限。Gateway 分别为代理
|
||||
转发和 direct fallback 建立 Transport,最坏情况下两者都可能保有空闲连接。
|
||||
- **maxIdleConnsPerHost**:单个上游 Host 的空闲 HTTP 连接上限。
|
||||
- **maxConnsPerHost**:单个上游 Host 的 HTTP 活跃加空闲连接上限。它不替代
|
||||
**limits.maxConcurrentConnections**,也不限制已经建立的 CONNECT 隧道。
|
||||
- **tunnelBufferBytes**:每个活跃 CONNECT 隧道的每个复制方向使用一个缓冲区,内存
|
||||
预算至少按 活跃隧道数 * 2 * tunnelBufferBytes 计算。
|
||||
- **tunnelIdleTimeout**:CONNECT 隧道双向没有数据活动时的最长存活时间。
|
||||
|
||||
所有字段均为可选项;数值 0 表示沿用进程默认值:拨号 10s、握手 15s、
|
||||
响应头 30s、空闲 HTTP 连接 90s、总空闲连接 1024、每 Host 空闲连接
|
||||
64、隧道缓冲 32768 字节、隧道空闲 5m。计数和时长不得为负数;
|
||||
maxIdleConnsPerHost 不得大于显式配置的 maxIdleConns。示例中的 20000/32
|
||||
是高并发 HTTP 转发的起点,不是每秒请求量的换算公式,应同时按可用文件描述符、
|
||||
上游限额和观测到的复用率调节。
|
||||
|
||||
## 6. Distribution
|
||||
|
||||
```yaml
|
||||
|
||||
@ -66,6 +66,7 @@ type Listener struct {
|
||||
Limits Limits `yaml:"limits"`
|
||||
Retry Retry `yaml:"retry"`
|
||||
StickySession StickySession `yaml:"stickySession"`
|
||||
Transport GatewayTransport `yaml:"transport"`
|
||||
DestinationPolicy DestinationPolicy `yaml:"destinationPolicy"`
|
||||
}
|
||||
|
||||
@ -127,6 +128,20 @@ type StickySession struct {
|
||||
MaxEntries int `yaml:"maxEntries"`
|
||||
}
|
||||
|
||||
// GatewayTransport configures the local Gateway process connection pools and
|
||||
// tunnel I/O. It intentionally contains no remote state or provider settings.
|
||||
type GatewayTransport struct {
|
||||
DialTimeout Duration `yaml:"dialTimeout"`
|
||||
HandshakeTimeout Duration `yaml:"handshakeTimeout"`
|
||||
ResponseHeaderTimeout Duration `yaml:"responseHeaderTimeout"`
|
||||
IdleConnTimeout Duration `yaml:"idleConnTimeout"`
|
||||
MaxIdleConns int `yaml:"maxIdleConns"`
|
||||
MaxIdleConnsPerHost int `yaml:"maxIdleConnsPerHost"`
|
||||
MaxConnsPerHost int `yaml:"maxConnsPerHost"`
|
||||
TunnelBufferBytes int `yaml:"tunnelBufferBytes"`
|
||||
TunnelIdleTimeout Duration `yaml:"tunnelIdleTimeout"`
|
||||
}
|
||||
|
||||
type DestinationPolicy struct {
|
||||
DenyPrivateNetworks *bool `yaml:"denyPrivateNetworks"`
|
||||
DenyLoopback *bool `yaml:"denyLoopback"`
|
||||
|
||||
@ -469,6 +469,51 @@ func TestValidateGatewayStickySession(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGatewayTransport(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := mustLoadValidConfig(t)
|
||||
cfg.Gateway.Transport = GatewayTransport{
|
||||
DialTimeout: Duration(time.Second),
|
||||
HandshakeTimeout: Duration(2 * time.Second),
|
||||
ResponseHeaderTimeout: Duration(3 * time.Second),
|
||||
IdleConnTimeout: Duration(time.Minute),
|
||||
MaxIdleConns: 500,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
MaxConnsPerHost: 10,
|
||||
TunnelBufferBytes: 32 << 10,
|
||||
TunnelIdleTimeout: Duration(5 * time.Minute),
|
||||
}
|
||||
if err := Validate(cfg); err != nil {
|
||||
t.Fatalf("Validate(valid gateway transport) error = %v", err)
|
||||
}
|
||||
|
||||
cfg.Gateway.Transport.DialTimeout = Duration(-time.Second)
|
||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "transport.dialTimeout") {
|
||||
t.Fatalf("Validate(negative dial timeout) error = %v", err)
|
||||
}
|
||||
|
||||
cfg.Gateway.Transport.DialTimeout = Duration(time.Second)
|
||||
cfg.Gateway.Transport.MaxConnsPerHost = -1
|
||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "transport.maxConnsPerHost") {
|
||||
t.Fatalf("Validate(negative max connections) error = %v", err)
|
||||
}
|
||||
|
||||
cfg.Gateway.Transport.MaxConnsPerHost = 10
|
||||
cfg.Gateway.Transport.MaxIdleConns = 19
|
||||
cfg.Gateway.Transport.MaxIdleConnsPerHost = 20
|
||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "maxIdleConnsPerHost") {
|
||||
t.Fatalf("Validate(inconsistent idle pool) error = %v", err)
|
||||
}
|
||||
|
||||
cfg.Gateway.Transport.MaxIdleConns = 500
|
||||
cfg.Gateway.Transport.MaxIdleConnsPerHost = 20
|
||||
cfg.Distribution.Transport = GatewayTransport{MaxConnsPerHost: 10}
|
||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "distribution transport") {
|
||||
t.Fatalf("Validate(distribution transport) error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMetricsListener(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := mustLoadValidConfig(t)
|
||||
|
||||
@ -113,6 +113,9 @@ func Validate(cfg *Config) error {
|
||||
if err := validateGatewayStickySession(cfg.Gateway); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateGatewayTransport(cfg.Gateway); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if cfg.Distribution.Enabled {
|
||||
if err := validateDistributionClientPolicies(cfg.Distribution.Auth, cfg.Upstreams); err != nil {
|
||||
@ -121,6 +124,9 @@ func Validate(cfg *Config) error {
|
||||
if cfg.Distribution.StickySession.Enabled {
|
||||
return fmt.Errorf("validate distribution stickySession: only supported on gateway")
|
||||
}
|
||||
if gatewayTransportConfigured(cfg.Distribution.Transport) {
|
||||
return fmt.Errorf("validate distribution transport: only supported on gateway")
|
||||
}
|
||||
clientIdentificationMode := cfg.Distribution.ClientIdentification.Mode
|
||||
if clientIdentificationMode == "" {
|
||||
clientIdentificationMode = "sourceIP"
|
||||
@ -154,6 +160,9 @@ func Validate(cfg *Config) error {
|
||||
if cfg.Admin.Enabled && cfg.Admin.StickySession.Enabled {
|
||||
return fmt.Errorf("validate admin stickySession: only supported on gateway")
|
||||
}
|
||||
if cfg.Admin.Enabled && gatewayTransportConfigured(cfg.Admin.Transport) {
|
||||
return fmt.Errorf("validate admin transport: only supported on gateway")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -177,6 +186,53 @@ func validateGatewayStickySession(listener Listener) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateGatewayTransport(listener Listener) error {
|
||||
item := listener.Transport
|
||||
for _, timeout := range []struct {
|
||||
name string
|
||||
value Duration
|
||||
}{
|
||||
{name: "dialTimeout", value: item.DialTimeout},
|
||||
{name: "handshakeTimeout", value: item.HandshakeTimeout},
|
||||
{name: "responseHeaderTimeout", value: item.ResponseHeaderTimeout},
|
||||
{name: "idleConnTimeout", value: item.IdleConnTimeout},
|
||||
{name: "tunnelIdleTimeout", value: item.TunnelIdleTimeout},
|
||||
} {
|
||||
if timeout.value.Value() < 0 {
|
||||
return fmt.Errorf("validate gateway transport.%s: must be non-negative", timeout.name)
|
||||
}
|
||||
}
|
||||
for _, limit := range []struct {
|
||||
name string
|
||||
value int
|
||||
}{
|
||||
{name: "maxIdleConns", value: item.MaxIdleConns},
|
||||
{name: "maxIdleConnsPerHost", value: item.MaxIdleConnsPerHost},
|
||||
{name: "maxConnsPerHost", value: item.MaxConnsPerHost},
|
||||
{name: "tunnelBufferBytes", value: item.TunnelBufferBytes},
|
||||
} {
|
||||
if limit.value < 0 || limit.value > MaximumPoolSize {
|
||||
return fmt.Errorf("validate gateway transport.%s: must be in [0, %d]", limit.name, MaximumPoolSize)
|
||||
}
|
||||
}
|
||||
if item.MaxIdleConns > 0 && item.MaxIdleConnsPerHost > item.MaxIdleConns {
|
||||
return fmt.Errorf("validate gateway transport.maxIdleConnsPerHost: must not exceed maxIdleConns")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func gatewayTransportConfigured(item GatewayTransport) bool {
|
||||
return item.DialTimeout != 0 ||
|
||||
item.HandshakeTimeout != 0 ||
|
||||
item.ResponseHeaderTimeout != 0 ||
|
||||
item.IdleConnTimeout != 0 ||
|
||||
item.MaxIdleConns != 0 ||
|
||||
item.MaxIdleConnsPerHost != 0 ||
|
||||
item.MaxConnsPerHost != 0 ||
|
||||
item.TunnelBufferBytes != 0 ||
|
||||
item.TunnelIdleTimeout != 0
|
||||
}
|
||||
|
||||
func validCanonicalHeaderName(value string) bool {
|
||||
if value == "" || http.CanonicalHeaderKey(value) != value {
|
||||
return false
|
||||
|
||||
@ -156,7 +156,7 @@ func newRuntime(ctx context.Context, configuration *config.Config, options Optio
|
||||
storeOptions.CapacityInvariantObserver = collector
|
||||
}
|
||||
store := snapshot.NewStoreWithOptions(options.ClusterID, options.WorkerID, storeOptions)
|
||||
proxyTransport := transport.New(transport.Config{}, snapshotCredentialResolver{store: store})
|
||||
proxyTransport := transport.New(gatewayTransportConfig(configuration.Gateway), snapshotCredentialResolver{store: store})
|
||||
outcomes, err := gatewayOutcome.NewQueue(gatewayOutcome.QueueOptions{
|
||||
Capacity: defaultOutcomeQueueCapacity, MaxBatch: min(defaultOutcomeBatchSize, configuration.ControlPlane.MaxRuntimeCounters),
|
||||
Metrics: outcomeMetrics,
|
||||
@ -243,6 +243,21 @@ func newRuntime(ctx context.Context, configuration *config.Config, options Optio
|
||||
return &runtime{connection: connection, group: group}, nil
|
||||
}
|
||||
|
||||
func gatewayTransportConfig(listener config.Listener) transport.Config {
|
||||
item := listener.Transport
|
||||
return transport.Config{
|
||||
DialTimeout: item.DialTimeout.Value(),
|
||||
HandshakeTimeout: item.HandshakeTimeout.Value(),
|
||||
ResponseHeaderTimeout: item.ResponseHeaderTimeout.Value(),
|
||||
IdleConnTimeout: item.IdleConnTimeout.Value(),
|
||||
MaxIdleConns: item.MaxIdleConns,
|
||||
MaxIdleConnsPerHost: item.MaxIdleConnsPerHost,
|
||||
MaxConnsPerHost: item.MaxConnsPerHost,
|
||||
TunnelBufferBytes: item.TunnelBufferBytes,
|
||||
TunnelIdleTimeout: item.TunnelIdleTimeout.Value(),
|
||||
}
|
||||
}
|
||||
|
||||
type generatedRuntimeClient struct {
|
||||
client controlplanev1.WorkerControlPlaneClient
|
||||
}
|
||||
|
||||
@ -145,6 +145,34 @@ func TestControlPlaneTransportRequiresDedicatedGatewayTLS(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayTransportConfigMapsListenerSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := gatewayTransportConfig(config.Listener{Transport: config.GatewayTransport{
|
||||
DialTimeout: config.Duration(time.Second),
|
||||
HandshakeTimeout: config.Duration(2 * time.Second),
|
||||
ResponseHeaderTimeout: config.Duration(3 * time.Second),
|
||||
IdleConnTimeout: config.Duration(time.Minute),
|
||||
MaxIdleConns: 500,
|
||||
MaxIdleConnsPerHost: 20,
|
||||
MaxConnsPerHost: 10,
|
||||
TunnelBufferBytes: 32 << 10,
|
||||
TunnelIdleTimeout: config.Duration(5 * time.Minute),
|
||||
}})
|
||||
|
||||
if got.DialTimeout != time.Second ||
|
||||
got.HandshakeTimeout != 2*time.Second ||
|
||||
got.ResponseHeaderTimeout != 3*time.Second ||
|
||||
got.IdleConnTimeout != time.Minute ||
|
||||
got.MaxIdleConns != 500 ||
|
||||
got.MaxIdleConnsPerHost != 20 ||
|
||||
got.MaxConnsPerHost != 10 ||
|
||||
got.TunnelBufferBytes != 32<<10 ||
|
||||
got.TunnelIdleTimeout != 5*time.Minute {
|
||||
t.Fatalf("gatewayTransportConfig() = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapshotReadinessRequiresCurrentSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ type Config struct {
|
||||
IdleConnTimeout time.Duration
|
||||
MaxIdleConns int
|
||||
MaxIdleConnsPerHost int
|
||||
MaxConnsPerHost int
|
||||
MaxErrorResponseBytes int64
|
||||
MaxResponseHeaderBytes int64
|
||||
TunnelBufferBytes int
|
||||
@ -120,6 +121,7 @@ func newHTTPTransport(config Config, proxy func(*http.Request) (*url.URL, error)
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: config.MaxIdleConns,
|
||||
MaxIdleConnsPerHost: config.MaxIdleConnsPerHost,
|
||||
MaxConnsPerHost: config.MaxConnsPerHost,
|
||||
IdleConnTimeout: config.IdleConnTimeout,
|
||||
TLSHandshakeTimeout: config.HandshakeTimeout,
|
||||
ResponseHeaderTimeout: config.ResponseHeaderTimeout,
|
||||
|
||||
@ -88,6 +88,32 @@ func TestRoundTripDirectForwardsWithoutProxyAuthorization(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAppliesConfiguredConnectionPoolLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := New(Config{
|
||||
MaxIdleConns: 256,
|
||||
MaxIdleConnsPerHost: 24,
|
||||
MaxConnsPerHost: 12,
|
||||
}, nil)
|
||||
t.Cleanup(client.CloseIdleConnections)
|
||||
|
||||
for name, item := range map[string]*http.Transport{
|
||||
"proxy": client.client,
|
||||
"direct": client.direct,
|
||||
} {
|
||||
if item.MaxIdleConns != 256 {
|
||||
t.Fatalf("%s MaxIdleConns = %d, want 256", name, item.MaxIdleConns)
|
||||
}
|
||||
if item.MaxIdleConnsPerHost != 24 {
|
||||
t.Fatalf("%s MaxIdleConnsPerHost = %d, want 24", name, item.MaxIdleConnsPerHost)
|
||||
}
|
||||
if item.MaxConnsPerHost != 12 {
|
||||
t.Fatalf("%s MaxConnsPerHost = %d, want 12", name, item.MaxConnsPerHost)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenDirectTunnelDialsTarget(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user