Support all 2xx HTTP status code for health check.

This commit is contained in:
Ludovic Fernandez 2018-05-22 09:22:03 +02:00 committed by Traefiker Bot
parent 5c4692a0df
commit e2bac47a0a
3 changed files with 36 additions and 32 deletions

View file

@ -454,13 +454,12 @@ The deprecated way:
#### Health Check #### Health Check
A health check can be configured in order to remove a backend from LB rotation as long as it keeps returning HTTP status codes other than `200 OK` to HTTP GET requests periodically carried out by Traefik. A health check can be configured in order to remove a backend from LB rotation as long as it keeps returning HTTP status codes other than `2xx` to HTTP GET requests periodically carried out by Traefik.
The check is defined by a path appended to the backend URL and an interval (given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)) specifying how often the health check should be executed (the default being 30 seconds). The check is defined by a path appended to the backend URL and an interval (given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)) specifying how often the health check should be executed (the default being 30 seconds).
Each backend must respond to the health check within 5 seconds. Each backend must respond to the health check within 5 seconds.
By default, the port of the backend server is used, however, this may be overridden. By default, the port of the backend server is used, however, this may be overridden.
A recovering backend returning 200 OK responses again is being returned to the A recovering backend returning `2xx` responses again is being returned to the LB rotation pool.
LB rotation pool.
For example: For example:
```toml ```toml

View file

@ -174,6 +174,7 @@ func (b *BackendHealthCheck) addHeadersAndHost(req *http.Request) *http.Request
if b.Options.Hostname != "" { if b.Options.Hostname != "" {
req.Host = b.Options.Hostname req.Host = b.Options.Hostname
} }
for k, v := range b.Options.Headers { for k, v := range b.Options.Headers {
req.Header.Set(k, v) req.Header.Set(k, v)
} }
@ -183,26 +184,28 @@ func (b *BackendHealthCheck) addHeadersAndHost(req *http.Request) *http.Request
// checkHealth returns a nil error in case it was successful and otherwise // checkHealth returns a nil error in case it was successful and otherwise
// a non-nil error with a meaningful description why the health check failed. // a non-nil error with a meaningful description why the health check failed.
func checkHealth(serverURL *url.URL, backend *BackendHealthCheck) error { func checkHealth(serverURL *url.URL, backend *BackendHealthCheck) error {
client := http.Client{
Timeout: backend.requestTimeout,
Transport: backend.Options.Transport,
}
req, err := backend.newRequest(serverURL) req, err := backend.newRequest(serverURL)
if err != nil { if err != nil {
return fmt.Errorf("failed to create HTTP request: %s", err) return fmt.Errorf("failed to create HTTP request: %s", err)
} }
req = backend.addHeadersAndHost(req) req = backend.addHeadersAndHost(req)
resp, err := client.Do(req) client := http.Client{
if err == nil { Timeout: backend.requestTimeout,
defer resp.Body.Close() Transport: backend.Options.Transport,
} }
switch { resp, err := client.Do(req)
case err != nil: if err != nil {
return fmt.Errorf("HTTP request failed: %s", err) return fmt.Errorf("HTTP request failed: %s", err)
case resp.StatusCode != http.StatusOK:
return fmt.Errorf("received non-200 status code: %v", resp.StatusCode)
} }
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return fmt.Errorf("received non-2xx status code: %v", resp.StatusCode)
}
return nil return nil
} }

View file

@ -19,14 +19,14 @@ const healthCheckInterval = 100 * time.Millisecond
type testHandler struct { type testHandler struct {
done func() done func()
healthSequence []bool healthSequence []int
} }
func TestSetBackendsConfiguration(t *testing.T) { func TestSetBackendsConfiguration(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
startHealthy bool startHealthy bool
healthSequence []bool healthSequence []int
expectedNumRemovedServers int expectedNumRemovedServers int
expectedNumUpsertedServers int expectedNumUpsertedServers int
expectedGaugeValue float64 expectedGaugeValue float64
@ -34,7 +34,15 @@ func TestSetBackendsConfiguration(t *testing.T) {
{ {
desc: "healthy server staying healthy", desc: "healthy server staying healthy",
startHealthy: true, startHealthy: true,
healthSequence: []bool{true}, healthSequence: []int{http.StatusOK},
expectedNumRemovedServers: 0,
expectedNumUpsertedServers: 0,
expectedGaugeValue: 1,
},
{
desc: "healthy server staying healthy (StatusNoContent)",
startHealthy: true,
healthSequence: []int{http.StatusNoContent},
expectedNumRemovedServers: 0, expectedNumRemovedServers: 0,
expectedNumUpsertedServers: 0, expectedNumUpsertedServers: 0,
expectedGaugeValue: 1, expectedGaugeValue: 1,
@ -42,7 +50,7 @@ func TestSetBackendsConfiguration(t *testing.T) {
{ {
desc: "healthy server becoming sick", desc: "healthy server becoming sick",
startHealthy: true, startHealthy: true,
healthSequence: []bool{false}, healthSequence: []int{http.StatusServiceUnavailable},
expectedNumRemovedServers: 1, expectedNumRemovedServers: 1,
expectedNumUpsertedServers: 0, expectedNumUpsertedServers: 0,
expectedGaugeValue: 0, expectedGaugeValue: 0,
@ -50,7 +58,7 @@ func TestSetBackendsConfiguration(t *testing.T) {
{ {
desc: "sick server becoming healthy", desc: "sick server becoming healthy",
startHealthy: false, startHealthy: false,
healthSequence: []bool{true}, healthSequence: []int{http.StatusOK},
expectedNumRemovedServers: 0, expectedNumRemovedServers: 0,
expectedNumUpsertedServers: 1, expectedNumUpsertedServers: 1,
expectedGaugeValue: 1, expectedGaugeValue: 1,
@ -58,7 +66,7 @@ func TestSetBackendsConfiguration(t *testing.T) {
{ {
desc: "sick server staying sick", desc: "sick server staying sick",
startHealthy: false, startHealthy: false,
healthSequence: []bool{false}, healthSequence: []int{http.StatusServiceUnavailable},
expectedNumRemovedServers: 0, expectedNumRemovedServers: 0,
expectedNumUpsertedServers: 0, expectedNumUpsertedServers: 0,
expectedGaugeValue: 0, expectedGaugeValue: 0,
@ -66,7 +74,7 @@ func TestSetBackendsConfiguration(t *testing.T) {
{ {
desc: "healthy server toggling to sick and back to healthy", desc: "healthy server toggling to sick and back to healthy",
startHealthy: true, startHealthy: true,
healthSequence: []bool{false, true}, healthSequence: []int{http.StatusServiceUnavailable, http.StatusOK},
expectedNumRemovedServers: 1, expectedNumRemovedServers: 1,
expectedNumUpsertedServers: 1, expectedNumUpsertedServers: 1,
expectedGaugeValue: 1, expectedGaugeValue: 1,
@ -77,6 +85,7 @@ func TestSetBackendsConfiguration(t *testing.T) {
test := test test := test
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
// The context is passed to the health check and canonically cancelled by // The context is passed to the health check and canonically cancelled by
// the test server once all expected requests have been received. // the test server once all expected requests have been received.
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -320,7 +329,7 @@ func (lb *testLoadBalancer) removeServer(u *url.URL) {
lb.servers = append(lb.servers[:i], lb.servers[i+1:]...) lb.servers = append(lb.servers[:i], lb.servers[i+1:]...)
} }
func newTestServer(done func(), healthSequence []bool) *httptest.Server { func newTestServer(done func(), healthSequence []int) *httptest.Server {
handler := &testHandler{ handler := &testHandler{
done: done, done: done,
healthSequence: healthSequence, healthSequence: healthSequence,
@ -328,21 +337,14 @@ func newTestServer(done func(), healthSequence []bool) *httptest.Server {
return httptest.NewServer(handler) return httptest.NewServer(handler)
} }
// ServeHTTP returns 200 or 503 HTTP response codes depending on whether the // ServeHTTP returns HTTP response codes following a status sequences.
// current request is marked as healthy or not. // It calls the given 'done' function once all request health indicators have been depleted.
// It calls the given 'done' function once all request health indicators have
// been depleted.
func (th *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (th *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if len(th.healthSequence) == 0 { if len(th.healthSequence) == 0 {
panic("received unexpected request") panic("received unexpected request")
} }
healthy := th.healthSequence[0] w.WriteHeader(th.healthSequence[0])
if healthy {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}
th.healthSequence = th.healthSequence[1:] th.healthSequence = th.healthSequence[1:]
if len(th.healthSequence) == 0 { if len(th.healthSequence) == 0 {