diff --git a/docs/content/reference/static-configuration/cli.txt b/docs/content/reference/static-configuration/cli.txt index b4c087f1f..dfece6186 100644 --- a/docs/content/reference/static-configuration/cli.txt +++ b/docs/content/reference/static-configuration/cli.txt @@ -477,6 +477,10 @@ 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. +--serverstransport.forwardingtimeouts.idleconntimeout (Default: "90s") + The maximum period for which an idle HTTP keep-alive connection to a backend + server will remain open before closing itself. + --serverstransport.insecureskipverify (Default: "false") Disable SSL certificate verification. diff --git a/docs/content/reference/static-configuration/env.md b/docs/content/reference/static-configuration/env.md index 3e092aa04..09eb82a09 100644 --- a/docs/content/reference/static-configuration/env.md +++ b/docs/content/reference/static-configuration/env.md @@ -462,6 +462,10 @@ The amount of time to wait until a connection to a backend server can be establi `TRAEFIK_SERVERSTRANSPORT_FORWARDINGTIMEOUTS_RESPONSEHEADERTIMEOUT`: 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. (Default: ```0```) +`TRAEFIK_SERVERSTRANSPORT_FORWARDINGTIMEOUTS_IDLECONNTIMEOUT`: +The maximum period for which an idle HTTP keep-alive connection to a backend +server will remain open before closing itself. (Default: ```90s```) + `TRAEFIK_SERVERSTRANSPORT_INSECURESKIPVERIFY`: Disable SSL certificate verification. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index d87c889c6..4c7d08810 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -9,6 +9,7 @@ [ServersTransport.ForwardingTimeouts] DialTimeout = 42 ResponseHeaderTimeout = 42 + IdleConnTimeout = 5 [EntryPoints] diff --git a/integration/fixtures/timeout/keepalive.toml b/integration/fixtures/timeout/keepalive.toml new file mode 100644 index 000000000..c01dce754 --- /dev/null +++ b/integration/fixtures/timeout/keepalive.toml @@ -0,0 +1,31 @@ +[global] +checkNewVersion = false +sendAnonymousUsage = false + +[log] +level = "DEBUG" + +[serversTransport.forwardingTimeouts] + idleConnTimeout = "{{ .IdleConnTimeout }}" + +[entryPoints] + [entryPoints.web] + address = ":8000" + +[api] + +[providers] + [providers.file] + +[http.routers] + [http.routers.router1] + Service = "keepalive" + Rule = "PathPrefix(`/keepalive`)" + +[http.services] + [http.services.keepalive] + [http.services.keepalive.LoadBalancer] + passHostHeader = true + [[http.services.keepalive.LoadBalancer.Servers]] + URL = "{{ .KeepAliveServer }}" + Weight = 1 diff --git a/integration/integration_test.go b/integration/integration_test.go index 1355b7767..150638c2b 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -49,6 +49,7 @@ func init() { check.Suite(&HeadersSuite{}) check.Suite(&HostResolverSuite{}) check.Suite(&HTTPSSuite{}) + check.Suite(&KeepAliveSuite{}) check.Suite(&LogRotationSuite{}) check.Suite(&MarathonSuite{}) check.Suite(&MarathonSuite15{}) diff --git a/integration/keepalive_test.go b/integration/keepalive_test.go new file mode 100644 index 000000000..7dad0153b --- /dev/null +++ b/integration/keepalive_test.go @@ -0,0 +1,105 @@ +package integration + +import ( + "math" + "net" + "net/http" + "net/http/httptest" + "os" + "time" + + "github.com/containous/traefik/integration/try" + "github.com/go-check/check" + checker "github.com/vdemeester/shakers" +) + +type KeepAliveSuite struct { + BaseSuite +} + +type KeepAliveConfig struct { + KeepAliveServer string + IdleConnTimeout string +} + +type connStateChangeEvent struct { + key string + state http.ConnState +} + +func (s *KeepAliveSuite) TestShouldRespectConfiguredBackendHttpKeepAliveTime(c *check.C) { + idleTimeout := time.Duration(75) * time.Millisecond + + connStateChanges := make(chan connStateChangeEvent) + noMoreRequests := make(chan bool, 1) + completed := make(chan bool, 1) + + // keep track of HTTP connections and their status changes and measure their idle period + go func() { + connCount := 0 + idlePeriodStartMap := make(map[string]time.Time) + idlePeriodLengthMap := make(map[string]time.Duration) + + maxWaitDuration := 5 * time.Second + maxWaitTimeExceeded := time.After(maxWaitDuration) + moreRequestsExpected := true + + // Ensure that all idle HTTP connections are closed before verification phase + for moreRequestsExpected || len(idlePeriodLengthMap) < connCount { + select { + case event := <-connStateChanges: + switch event.state { + case http.StateNew: + connCount++ + case http.StateIdle: + idlePeriodStartMap[event.key] = time.Now() + case http.StateClosed: + idlePeriodLengthMap[event.key] = time.Since(idlePeriodStartMap[event.key]) + } + case <-noMoreRequests: + moreRequestsExpected = false + case <-maxWaitTimeExceeded: + c.Logf("timeout waiting for all connections to close, waited for %v, configured idle timeout was %v", maxWaitDuration, idleTimeout) + c.Fail() + close(completed) + return + } + } + + c.Check(connCount, checker.Equals, 1) + + for _, idlePeriod := range idlePeriodLengthMap { + // Our method of measuring the actual idle period is not precise, so allow some sub-ms deviation + c.Check(math.Round(idlePeriod.Seconds()), checker.LessOrEqualThan, idleTimeout.Seconds()) + } + + close(completed) + }() + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + })) + + server.Config.ConnState = func(conn net.Conn, state http.ConnState) { + connStateChanges <- connStateChangeEvent{key: conn.RemoteAddr().String(), state: state} + } + server.Start() + defer server.Close() + + config := KeepAliveConfig{KeepAliveServer: server.URL, IdleConnTimeout: idleTimeout.String()} + file := s.adaptFile(c, "fixtures/timeout/keepalive.toml", config) + + defer os.Remove(file) + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + + err := cmd.Start() + c.Check(err, checker.IsNil) + defer cmd.Process.Kill() + + err = try.GetRequest("http://127.0.0.1:8000/keepalive", time.Duration(1)*time.Second, try.StatusCodeIs(200)) + c.Check(err, checker.IsNil) + + close(noMoreRequests) + <-completed +} diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 99022af8c..d149ef37b 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -109,11 +109,13 @@ func (a *RespondingTimeouts) SetDefaults() { type ForwardingTimeouts struct { DialTimeout types.Duration `description:"The amount of time to wait until a connection to a backend server can be established. If zero, no timeout exists." export:"true"` ResponseHeaderTimeout types.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." export:"true"` + IdleConnTimeout types.Duration `description:"The maximum period for which an idle HTTP keep-alive connection will remain open before closing itself" export:"true"` } // SetDefaults sets the default values. func (f *ForwardingTimeouts) SetDefaults() { f.DialTimeout = types.Duration(30 * time.Second) + f.IdleConnTimeout = types.Duration(90 * time.Second) } // LifeCycle contains configurations relevant to the lifecycle (such as the shutdown phase) of Traefik. diff --git a/pkg/server/roundtripper.go b/pkg/server/roundtripper.go index 90b640e06..f5a99f61f 100644 --- a/pkg/server/roundtripper.go +++ b/pkg/server/roundtripper.go @@ -63,6 +63,7 @@ func createHTTPTransport(transportConfiguration *static.ServersTransport) (*http if transportConfiguration.ForwardingTimeouts != nil { transport.ResponseHeaderTimeout = time.Duration(transportConfiguration.ForwardingTimeouts.ResponseHeaderTimeout) + transport.IdleConnTimeout = time.Duration(transportConfiguration.ForwardingTimeouts.IdleConnTimeout) } if transportConfiguration.InsecureSkipVerify {