From 1f17731369d614c385f083597c597d61f0bff6d0 Mon Sep 17 00:00:00 2001 From: Tom Moulard Date: Tue, 9 Nov 2021 12:16:08 +0100 Subject: [PATCH] feat: add readIdleTimeout and pingTimeout config options to ServersTransport Co-authored-by: Kevin Pollet --- ...traefik.containo.us_serverstransports.yaml | 31 ++++++-- docs/content/routing/overview.md | 6 +- docs/content/routing/services/index.md | 72 +++++++++++++++++++ integration/fixtures/k8s/01-traefik-crd.yml | 31 ++++++-- pkg/anonymize/anonymize_config_test.go | 2 + .../testdata/anonymized-dynamic-config.json | 6 +- pkg/config/dynamic/http_config.go | 7 +- .../crd/fixtures/with_servers_transport.yml | 2 + pkg/provider/kubernetes/crd/kubernetes.go | 14 ++++ .../kubernetes/crd/kubernetes_test.go | 7 ++ .../crd/traefik/v1alpha1/serverstransport.go | 10 ++- .../traefik/v1alpha1/zz_generated.deepcopy.go | 10 +++ pkg/server/service/roundtripper.go | 11 +-- pkg/server/service/smart_roundtripper.go | 33 +++++++-- 14 files changed, 204 insertions(+), 38 deletions(-) diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml index ca4da861f..aac46790b 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_serverstransports.yaml @@ -52,23 +52,40 @@ spec: anyOf: - type: integer - type: string - description: The amount of time to wait until a connection to - a backend server can be established. If zero, no timeout exists. + description: DialTimeout is the amount of time to wait until a + connection to a backend server can be established. If zero, + no timeout exists. x-kubernetes-int-or-string: true idleConnTimeout: anyOf: - type: integer - type: string - description: The maximum period for which an idle HTTP keep-alive - connection will remain open before closing itself. + description: IdleConnTimeout is the maximum period for which an + idle HTTP keep-alive connection will remain open before closing + itself. + x-kubernetes-int-or-string: true + pingTimeout: + anyOf: + - type: integer + - type: string + description: PingTimeout is the timeout after which the HTTP/2 + connection will be closed if a response to ping is not received. + x-kubernetes-int-or-string: true + readIdleTimeout: + anyOf: + - type: integer + - type: string + description: ReadIdleTimeout is the timeout after which a health + check using ping frame will be carried out if no frame is received + on the HTTP/2 connection. If zero, no health check is performed. x-kubernetes-int-or-string: true responseHeaderTimeout: anyOf: - type: integer - type: string - description: The amount of time to wait for a server's response - headers after fully writing the request (including its body, - if any). If zero, no timeout exists. + description: ResponseHeaderTimeout is the amount of time to wait + for a server's response headers after fully writing the request + (including its body, if any). If zero, no timeout exists. x-kubernetes-int-or-string: true type: object insecureSkipVerify: diff --git a/docs/content/routing/overview.md b/docs/content/routing/overview.md index 30bcaf41c..3a99fe9e2 100644 --- a/docs/content/routing/overview.md +++ b/docs/content/routing/overview.md @@ -324,7 +324,7 @@ serversTransport: `forwardingTimeouts` is about a number of timeouts relevant to when forwarding requests to the backend servers. -#### forwardingTimeouts.dialTimeout` +#### `forwardingTimeouts.dialTimeout` _Optional, Default=30s_ @@ -349,7 +349,7 @@ serversTransport: --serversTransport.forwardingTimeouts.dialTimeout=1s ``` -#### forwardingTimeouts.responseHeaderTimeout` +#### `forwardingTimeouts.responseHeaderTimeout` _Optional, Default=0s_ @@ -376,7 +376,7 @@ serversTransport: --serversTransport.forwardingTimeouts.responseHeaderTimeout=1s ``` -#### forwardingTimeouts.idleConnTimeout` +#### `forwardingTimeouts.idleConnTimeout` _Optional, Default=90s_ diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index d6a639796..b25fad3d8 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -876,6 +876,78 @@ spec: idleConnTimeout: "1s" ``` +##### `forwardingTimeouts.readIdleTimeout` + +_Optional, Default=0s_ + +`readIdleTimeout` is the timeout after which a health check using ping frame will be carried out +if no frame is received on the HTTP/2 connection. +Note that a ping response will be considered a received frame, +so if there is no other traffic on the connection, +the health check will be performed every `readIdleTimeout` interval. +If zero, no health check is performed. + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + serversTransports: + mytransport: + forwardingTimeouts: + readIdleTimeout: "1s" +``` + +```toml tab="File (TOML)" +## Dynamic configuration +[http.serversTransports.mytransport.forwardingTimeouts] + readIdleTimeout = "1s" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: ServersTransport +metadata: + name: mytransport + namespace: default + +spec: + forwardingTimeouts: + readIdleTimeout: "1s" +``` + +##### `forwardingTimeouts.pingTimeout` + +_Optional, Default=15s_ + +`pingTimeout` is the timeout after which the HTTP/2 connection will be closed +if a response to ping is not received. + +```yaml tab="File (YAML)" +## Dynamic configuration +http: + serversTransports: + mytransport: + forwardingTimeouts: + pingTimeout: "1s" +``` + +```toml tab="File (TOML)" +## Dynamic configuration +[http.serversTransports.mytransport.forwardingTimeouts] + pingTimeout = "1s" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: ServersTransport +metadata: + name: mytransport + namespace: default + +spec: + forwardingTimeouts: + pingTimeout: "1s" +``` + ### Weighted Round Robin (service) The WRR is able to load balance the requests between multiple services based on weights. diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 889f0c40c..fbde10895 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1125,23 +1125,40 @@ spec: anyOf: - type: integer - type: string - description: The amount of time to wait until a connection to - a backend server can be established. If zero, no timeout exists. + description: DialTimeout is the amount of time to wait until a + connection to a backend server can be established. If zero, + no timeout exists. x-kubernetes-int-or-string: true idleConnTimeout: anyOf: - type: integer - type: string - description: The maximum period for which an idle HTTP keep-alive - connection will remain open before closing itself. + description: IdleConnTimeout is the maximum period for which an + idle HTTP keep-alive connection will remain open before closing + itself. + x-kubernetes-int-or-string: true + pingTimeout: + anyOf: + - type: integer + - type: string + description: PingTimeout is the timeout after which the HTTP/2 + connection will be closed if a response to ping is not received. + x-kubernetes-int-or-string: true + readIdleTimeout: + anyOf: + - type: integer + - type: string + description: ReadIdleTimeout is the timeout after which a health + check using ping frame will be carried out if no frame is received + on the HTTP/2 connection. If zero, no health check is performed. x-kubernetes-int-or-string: true responseHeaderTimeout: anyOf: - type: integer - type: string - description: The amount of time to wait for a server's response - headers after fully writing the request (including its body, - if any). If zero, no timeout exists. + description: ResponseHeaderTimeout is the amount of time to wait + for a server's response headers after fully writing the request + (including its body, if any). If zero, no timeout exists. x-kubernetes-int-or-string: true type: object insecureSkipVerify: diff --git a/pkg/anonymize/anonymize_config_test.go b/pkg/anonymize/anonymize_config_test.go index 8f8176ece..d9536e75e 100644 --- a/pkg/anonymize/anonymize_config_test.go +++ b/pkg/anonymize/anonymize_config_test.go @@ -147,6 +147,8 @@ func TestDo_dynamicConfiguration(t *testing.T) { DialTimeout: 42, ResponseHeaderTimeout: 42, IdleConnTimeout: 42, + ReadIdleTimeout: 42, + PingTimeout: 42, }, }, }, diff --git a/pkg/anonymize/testdata/anonymized-dynamic-config.json b/pkg/anonymize/testdata/anonymized-dynamic-config.json index c71b0cb0a..c55b17a5b 100644 --- a/pkg/anonymize/testdata/anonymized-dynamic-config.json +++ b/pkg/anonymize/testdata/anonymized-dynamic-config.json @@ -355,7 +355,9 @@ "forwardingTimeouts": { "dialTimeout": "42ns", "responseHeaderTimeout": "42ns", - "idleConnTimeout": "42ns" + "idleConnTimeout": "42ns", + "readIdleTimeout": "42ns", + "pingTimeout": "42ns" } } } @@ -474,4 +476,4 @@ } } } -} \ No newline at end of file +} diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index edeaf83e0..ea4e97964 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -218,7 +218,7 @@ type HealthCheck struct{} // ServersTransport options to configure communication between Traefik and the servers. type ServersTransport struct { - ServerName string `description:"ServerName used to contact the server" json:"serverName,omitempty" toml:"serverName,omitempty" yaml:"serverName,omitempty"` + ServerName string `description:"ServerName used to contact the server." json:"serverName,omitempty" toml:"serverName,omitempty" yaml:"serverName,omitempty"` InsecureSkipVerify bool `description:"Disable SSL certificate verification." json:"insecureSkipVerify,omitempty" toml:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty" export:"true"` RootCAs []traefiktls.FileOrContent `description:"Add cert file for self-signed certificate." json:"rootCAs,omitempty" toml:"rootCAs,omitempty" yaml:"rootCAs,omitempty"` Certificates traefiktls.Certificates `description:"Certificates for mTLS." json:"certificates,omitempty" toml:"certificates,omitempty" yaml:"certificates,omitempty" export:"true"` @@ -234,11 +234,14 @@ type ServersTransport struct { type ForwardingTimeouts struct { DialTimeout ptypes.Duration `description:"The amount of time to wait until a connection to a backend server can be established. If zero, no timeout exists." json:"dialTimeout,omitempty" toml:"dialTimeout,omitempty" yaml:"dialTimeout,omitempty" export:"true"` ResponseHeaderTimeout ptypes.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists." json:"responseHeaderTimeout,omitempty" toml:"responseHeaderTimeout,omitempty" yaml:"responseHeaderTimeout,omitempty" export:"true"` - IdleConnTimeout ptypes.Duration `description:"The maximum period for which an idle HTTP keep-alive connection will remain open before closing itself" json:"idleConnTimeout,omitempty" toml:"idleConnTimeout,omitempty" yaml:"idleConnTimeout,omitempty" export:"true"` + IdleConnTimeout ptypes.Duration `description:"The maximum period for which an idle HTTP keep-alive connection will remain open before closing itself." json:"idleConnTimeout,omitempty" toml:"idleConnTimeout,omitempty" yaml:"idleConnTimeout,omitempty" export:"true"` + ReadIdleTimeout ptypes.Duration `description:"The timeout after which a health check using ping frame will be carried out if no frame is received on the HTTP/2 connection. If zero, no health check is performed." json:"readIdleTimeout,omitempty" toml:"readIdleTimeout,omitempty" yaml:"readIdleTimeout,omitempty" export:"true"` + PingTimeout ptypes.Duration `description:"The timeout after which the HTTP/2 connection will be closed if a response to ping is not received." json:"pingTimeout,omitempty" toml:"pingTimeout,omitempty" yaml:"pingTimeout,omitempty" export:"true"` } // SetDefaults sets the default values. func (f *ForwardingTimeouts) SetDefaults() { f.DialTimeout = ptypes.Duration(30 * time.Second) f.IdleConnTimeout = ptypes.Duration(90 * time.Second) + f.PingTimeout = ptypes.Duration(15 * time.Second) } diff --git a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml index 994941f06..33c1a11a8 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml @@ -110,6 +110,8 @@ spec: dialTimeout: 42 responseHeaderTimeout: 42s idleConnTimeout: 42ms + readIdleTimeout: 42s + pingTimeout: 42s --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 7c2b2d051..5a63d80c0 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -340,6 +340,20 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) logger.Errorf("Error while reading IdleConnTimeout: %v", err) } } + + if serversTransport.Spec.ForwardingTimeouts.ReadIdleTimeout != nil { + err := forwardingTimeout.ReadIdleTimeout.Set(serversTransport.Spec.ForwardingTimeouts.ReadIdleTimeout.String()) + if err != nil { + logger.Errorf("Error while reading ReadIdleTimeout: %v", err) + } + } + + if serversTransport.Spec.ForwardingTimeouts.PingTimeout != nil { + err := forwardingTimeout.PingTimeout.Set(serversTransport.Spec.ForwardingTimeouts.PingTimeout.String()) + if err != nil { + logger.Errorf("Error while reading PingTimeout: %v", err) + } + } } id := provider.Normalize(makeID(serversTransport.Namespace, serversTransport.Name)) diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 5ab7b5023..3411d6f67 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -3618,6 +3618,8 @@ func TestLoadIngressRoutes(t *testing.T) { DialTimeout: ptypes.Duration(42 * time.Second), ResponseHeaderTimeout: ptypes.Duration(42 * time.Second), IdleConnTimeout: ptypes.Duration(42 * time.Millisecond), + ReadIdleTimeout: ptypes.Duration(42 * time.Second), + PingTimeout: ptypes.Duration(42 * time.Second), }, PeerCertURI: "foo://bar", }, @@ -3626,6 +3628,7 @@ func TestLoadIngressRoutes(t *testing.T) { ForwardingTimeouts: &dynamic.ForwardingTimeouts{ DialTimeout: ptypes.Duration(30 * time.Second), IdleConnTimeout: ptypes.Duration(90 * time.Second), + PingTimeout: ptypes.Duration(15 * time.Second), }, }, }, @@ -4873,6 +4876,8 @@ func TestCrossNamespace(t *testing.T) { DialTimeout: 30000000000, ResponseHeaderTimeout: 0, IdleConnTimeout: 90000000000, + ReadIdleTimeout: 0, + PingTimeout: 15000000000, }, DisableHTTP2: true, }, @@ -4904,6 +4909,8 @@ func TestCrossNamespace(t *testing.T) { DialTimeout: 30000000000, ResponseHeaderTimeout: 0, IdleConnTimeout: 90000000000, + ReadIdleTimeout: 0, + PingTimeout: 15000000000, }, DisableHTTP2: true, }, diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go index d30adf012..4fce5e0be 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/serverstransport.go @@ -43,13 +43,17 @@ type ServersTransportSpec struct { // ForwardingTimeouts contains timeout configurations for forwarding requests to the backend servers. type ForwardingTimeouts struct { - // The amount of time to wait until a connection to a backend server can be established. If zero, no timeout exists. + // DialTimeout is the amount of time to wait until a connection to a backend server can be established. If zero, no timeout exists. DialTimeout *intstr.IntOrString `json:"dialTimeout,omitempty"` - // The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). + // ResponseHeaderTimeout is the amount of time to wait for a server's response headers after fully writing the request (including its body, if any). // If zero, no timeout exists. ResponseHeaderTimeout *intstr.IntOrString `json:"responseHeaderTimeout,omitempty"` - // The maximum period for which an idle HTTP keep-alive connection will remain open before closing itself. + // IdleConnTimeout is the maximum period for which an idle HTTP keep-alive connection will remain open before closing itself. IdleConnTimeout *intstr.IntOrString `json:"idleConnTimeout,omitempty"` + // ReadIdleTimeout is the timeout after which a health check using ping frame will be carried out if no frame is received on the HTTP/2 connection. If zero, no health check is performed. + ReadIdleTimeout *intstr.IntOrString `json:"readIdleTimeout,omitempty"` + // PingTimeout is the timeout after which the HTTP/2 connection will be closed if a response to ping is not received. + PingTimeout *intstr.IntOrString `json:"pingTimeout,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index a8b7154cb..c82f45868 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -214,6 +214,16 @@ func (in *ForwardingTimeouts) DeepCopyInto(out *ForwardingTimeouts) { *out = new(intstr.IntOrString) **out = **in } + if in.ReadIdleTimeout != nil { + in, out := &in.ReadIdleTimeout, &out.ReadIdleTimeout + *out = new(intstr.IntOrString) + **out = **in + } + if in.PingTimeout != nil { + in, out := &in.PingTimeout, &out.PingTimeout + *out = new(intstr.IntOrString) + **out = **in + } return } diff --git a/pkg/server/service/roundtripper.go b/pkg/server/service/roundtripper.go index ee1bdd768..68b3b0bbf 100644 --- a/pkg/server/service/roundtripper.go +++ b/pkg/server/service/roundtripper.go @@ -152,16 +152,7 @@ func createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error return transport, nil } - transport.RegisterProtocol("h2c", &h2cTransportWrapper{ - Transport: &http2.Transport{ - DialTLS: func(netw, addr string, cfg *tls.Config) (net.Conn, error) { - return net.Dial(netw, addr) - }, - AllowHTTP: true, - }, - }) - - return newSmartRoundTripper(transport) + return newSmartRoundTripper(transport, cfg.ForwardingTimeouts) } func createRootCACertPool(rootCAs []traefiktls.FileOrContent) *x509.CertPool { diff --git a/pkg/server/service/smart_roundtripper.go b/pkg/server/service/smart_roundtripper.go index 9a3028720..5643e60ed 100644 --- a/pkg/server/service/smart_roundtripper.go +++ b/pkg/server/service/smart_roundtripper.go @@ -1,33 +1,58 @@ package service import ( + "crypto/tls" + "net" "net/http" + "time" + "github.com/traefik/traefik/v2/pkg/config/dynamic" "golang.org/x/net/http/httpguts" "golang.org/x/net/http2" ) -func newSmartRoundTripper(transport *http.Transport) (http.RoundTripper, error) { +func newSmartRoundTripper(transport *http.Transport, forwardingTimeouts *dynamic.ForwardingTimeouts) (http.RoundTripper, error) { transportHTTP1 := transport.Clone() - err := http2.ConfigureTransport(transport) + transportHTTP2, err := http2.ConfigureTransports(transport) if err != nil { return nil, err } + if forwardingTimeouts != nil { + transportHTTP2.ReadIdleTimeout = time.Duration(forwardingTimeouts.ReadIdleTimeout) + transportHTTP2.PingTimeout = time.Duration(forwardingTimeouts.PingTimeout) + } + + transportH2C := &h2cTransportWrapper{ + Transport: &http2.Transport{ + DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + AllowHTTP: true, + }, + } + + if forwardingTimeouts != nil { + transportH2C.ReadIdleTimeout = time.Duration(forwardingTimeouts.ReadIdleTimeout) + transportH2C.PingTimeout = time.Duration(forwardingTimeouts.PingTimeout) + } + + transport.RegisterProtocol("h2c", transportH2C) + return &smartRoundTripper{ http2: transport, http: transportHTTP1, }, nil } +// smartRoundTripper implements RoundTrip while making sure that HTTP/2 is not used +// with protocols that start with a Connection Upgrade, such as SPDY or Websocket. type smartRoundTripper struct { http2 *http.Transport http *http.Transport } -// smartRoundTripper implements RoundTrip while making sure that HTTP/2 is not used -// with protocols that start with a Connection Upgrade, such as SPDY or Websocket. func (m *smartRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { // If we have a connection upgrade, we don't use HTTP/2 if httpguts.HeaderValuesContainsToken(req.Header["Connection"], "Upgrade") {