feat: add readIdleTimeout and pingTimeout config options to ServersTransport

Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
Tom Moulard 2021-11-09 12:16:08 +01:00 committed by GitHub
parent 8e32d1913b
commit 1f17731369
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 204 additions and 38 deletions

View file

@ -52,23 +52,40 @@ spec:
anyOf: anyOf:
- type: integer - type: integer
- type: string - type: string
description: The amount of time to wait until a connection to description: DialTimeout is the amount of time to wait until a
a backend server can be established. If zero, no timeout exists. connection to a backend server can be established. If zero,
no timeout exists.
x-kubernetes-int-or-string: true x-kubernetes-int-or-string: true
idleConnTimeout: idleConnTimeout:
anyOf: anyOf:
- type: integer - type: integer
- type: string - type: string
description: The maximum period for which an idle HTTP keep-alive description: IdleConnTimeout is the maximum period for which an
connection will remain open before closing itself. 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 x-kubernetes-int-or-string: true
responseHeaderTimeout: responseHeaderTimeout:
anyOf: anyOf:
- type: integer - type: integer
- type: string - type: string
description: The amount of time to wait for a server's response description: ResponseHeaderTimeout is the amount of time to wait
headers after fully writing the request (including its body, for a server's response headers after fully writing the request
if any). If zero, no timeout exists. (including its body, if any). If zero, no timeout exists.
x-kubernetes-int-or-string: true x-kubernetes-int-or-string: true
type: object type: object
insecureSkipVerify: insecureSkipVerify:

View file

@ -324,7 +324,7 @@ serversTransport:
`forwardingTimeouts` is about a number of timeouts relevant to when forwarding requests to the backend servers. `forwardingTimeouts` is about a number of timeouts relevant to when forwarding requests to the backend servers.
#### forwardingTimeouts.dialTimeout` #### `forwardingTimeouts.dialTimeout`
_Optional, Default=30s_ _Optional, Default=30s_
@ -349,7 +349,7 @@ serversTransport:
--serversTransport.forwardingTimeouts.dialTimeout=1s --serversTransport.forwardingTimeouts.dialTimeout=1s
``` ```
#### forwardingTimeouts.responseHeaderTimeout` #### `forwardingTimeouts.responseHeaderTimeout`
_Optional, Default=0s_ _Optional, Default=0s_
@ -376,7 +376,7 @@ serversTransport:
--serversTransport.forwardingTimeouts.responseHeaderTimeout=1s --serversTransport.forwardingTimeouts.responseHeaderTimeout=1s
``` ```
#### forwardingTimeouts.idleConnTimeout` #### `forwardingTimeouts.idleConnTimeout`
_Optional, Default=90s_ _Optional, Default=90s_

View file

@ -876,6 +876,78 @@ spec:
idleConnTimeout: "1s" 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) ### Weighted Round Robin (service)
The WRR is able to load balance the requests between multiple services based on weights. The WRR is able to load balance the requests between multiple services based on weights.

View file

@ -1125,23 +1125,40 @@ spec:
anyOf: anyOf:
- type: integer - type: integer
- type: string - type: string
description: The amount of time to wait until a connection to description: DialTimeout is the amount of time to wait until a
a backend server can be established. If zero, no timeout exists. connection to a backend server can be established. If zero,
no timeout exists.
x-kubernetes-int-or-string: true x-kubernetes-int-or-string: true
idleConnTimeout: idleConnTimeout:
anyOf: anyOf:
- type: integer - type: integer
- type: string - type: string
description: The maximum period for which an idle HTTP keep-alive description: IdleConnTimeout is the maximum period for which an
connection will remain open before closing itself. 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 x-kubernetes-int-or-string: true
responseHeaderTimeout: responseHeaderTimeout:
anyOf: anyOf:
- type: integer - type: integer
- type: string - type: string
description: The amount of time to wait for a server's response description: ResponseHeaderTimeout is the amount of time to wait
headers after fully writing the request (including its body, for a server's response headers after fully writing the request
if any). If zero, no timeout exists. (including its body, if any). If zero, no timeout exists.
x-kubernetes-int-or-string: true x-kubernetes-int-or-string: true
type: object type: object
insecureSkipVerify: insecureSkipVerify:

View file

@ -147,6 +147,8 @@ func TestDo_dynamicConfiguration(t *testing.T) {
DialTimeout: 42, DialTimeout: 42,
ResponseHeaderTimeout: 42, ResponseHeaderTimeout: 42,
IdleConnTimeout: 42, IdleConnTimeout: 42,
ReadIdleTimeout: 42,
PingTimeout: 42,
}, },
}, },
}, },

View file

@ -355,7 +355,9 @@
"forwardingTimeouts": { "forwardingTimeouts": {
"dialTimeout": "42ns", "dialTimeout": "42ns",
"responseHeaderTimeout": "42ns", "responseHeaderTimeout": "42ns",
"idleConnTimeout": "42ns" "idleConnTimeout": "42ns",
"readIdleTimeout": "42ns",
"pingTimeout": "42ns"
} }
} }
} }

View file

@ -218,7 +218,7 @@ type HealthCheck struct{}
// ServersTransport options to configure communication between Traefik and the servers. // ServersTransport options to configure communication between Traefik and the servers.
type ServersTransport struct { 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"` 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"` 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"` 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 { 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"` 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"` 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. // SetDefaults sets the default values.
func (f *ForwardingTimeouts) SetDefaults() { func (f *ForwardingTimeouts) SetDefaults() {
f.DialTimeout = ptypes.Duration(30 * time.Second) f.DialTimeout = ptypes.Duration(30 * time.Second)
f.IdleConnTimeout = ptypes.Duration(90 * time.Second) f.IdleConnTimeout = ptypes.Duration(90 * time.Second)
f.PingTimeout = ptypes.Duration(15 * time.Second)
} }

View file

@ -110,6 +110,8 @@ spec:
dialTimeout: 42 dialTimeout: 42
responseHeaderTimeout: 42s responseHeaderTimeout: 42s
idleConnTimeout: 42ms idleConnTimeout: 42ms
readIdleTimeout: 42s
pingTimeout: 42s
--- ---
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1

View file

@ -340,6 +340,20 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
logger.Errorf("Error while reading IdleConnTimeout: %v", err) 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)) id := provider.Normalize(makeID(serversTransport.Namespace, serversTransport.Name))

View file

@ -3618,6 +3618,8 @@ func TestLoadIngressRoutes(t *testing.T) {
DialTimeout: ptypes.Duration(42 * time.Second), DialTimeout: ptypes.Duration(42 * time.Second),
ResponseHeaderTimeout: ptypes.Duration(42 * time.Second), ResponseHeaderTimeout: ptypes.Duration(42 * time.Second),
IdleConnTimeout: ptypes.Duration(42 * time.Millisecond), IdleConnTimeout: ptypes.Duration(42 * time.Millisecond),
ReadIdleTimeout: ptypes.Duration(42 * time.Second),
PingTimeout: ptypes.Duration(42 * time.Second),
}, },
PeerCertURI: "foo://bar", PeerCertURI: "foo://bar",
}, },
@ -3626,6 +3628,7 @@ func TestLoadIngressRoutes(t *testing.T) {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{ ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(30 * time.Second), DialTimeout: ptypes.Duration(30 * time.Second),
IdleConnTimeout: ptypes.Duration(90 * 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, DialTimeout: 30000000000,
ResponseHeaderTimeout: 0, ResponseHeaderTimeout: 0,
IdleConnTimeout: 90000000000, IdleConnTimeout: 90000000000,
ReadIdleTimeout: 0,
PingTimeout: 15000000000,
}, },
DisableHTTP2: true, DisableHTTP2: true,
}, },
@ -4904,6 +4909,8 @@ func TestCrossNamespace(t *testing.T) {
DialTimeout: 30000000000, DialTimeout: 30000000000,
ResponseHeaderTimeout: 0, ResponseHeaderTimeout: 0,
IdleConnTimeout: 90000000000, IdleConnTimeout: 90000000000,
ReadIdleTimeout: 0,
PingTimeout: 15000000000,
}, },
DisableHTTP2: true, DisableHTTP2: true,
}, },

View file

@ -43,13 +43,17 @@ type ServersTransportSpec struct {
// ForwardingTimeouts contains timeout configurations for forwarding requests to the backend servers. // ForwardingTimeouts contains timeout configurations for forwarding requests to the backend servers.
type ForwardingTimeouts struct { 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"` 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. // If zero, no timeout exists.
ResponseHeaderTimeout *intstr.IntOrString `json:"responseHeaderTimeout,omitempty"` 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"` 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 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View file

@ -214,6 +214,16 @@ func (in *ForwardingTimeouts) DeepCopyInto(out *ForwardingTimeouts) {
*out = new(intstr.IntOrString) *out = new(intstr.IntOrString)
**out = **in **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 return
} }

View file

@ -152,16 +152,7 @@ func createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error
return transport, nil return transport, nil
} }
transport.RegisterProtocol("h2c", &h2cTransportWrapper{ return newSmartRoundTripper(transport, cfg.ForwardingTimeouts)
Transport: &http2.Transport{
DialTLS: func(netw, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(netw, addr)
},
AllowHTTP: true,
},
})
return newSmartRoundTripper(transport)
} }
func createRootCACertPool(rootCAs []traefiktls.FileOrContent) *x509.CertPool { func createRootCACertPool(rootCAs []traefiktls.FileOrContent) *x509.CertPool {

View file

@ -1,33 +1,58 @@
package service package service
import ( import (
"crypto/tls"
"net"
"net/http" "net/http"
"time"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"golang.org/x/net/http/httpguts" "golang.org/x/net/http/httpguts"
"golang.org/x/net/http2" "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() transportHTTP1 := transport.Clone()
err := http2.ConfigureTransport(transport) transportHTTP2, err := http2.ConfigureTransports(transport)
if err != nil { if err != nil {
return nil, err 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{ return &smartRoundTripper{
http2: transport, http2: transport,
http: transportHTTP1, http: transportHTTP1,
}, nil }, 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 { type smartRoundTripper struct {
http2 *http.Transport http2 *http.Transport
http *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) { func (m *smartRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// If we have a connection upgrade, we don't use HTTP/2 // If we have a connection upgrade, we don't use HTTP/2
if httpguts.HeaderValuesContainsToken(req.Header["Connection"], "Upgrade") { if httpguts.HeaderValuesContainsToken(req.Header["Connection"], "Upgrade") {