Adds mirroring service
This commit is contained in:
parent
fd24b1898e
commit
602a2ea541
10 changed files with 465 additions and 10 deletions
|
@ -353,6 +353,53 @@ http:
|
||||||
- url: "http://private-ip-server-2/"
|
- url: "http://private-ip-server-2/"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Mirroring (service)
|
||||||
|
|
||||||
|
The mirroring is able to mirror requests sent to a service to other services.
|
||||||
|
|
||||||
|
This strategy can be defined only with [File](../../providers/file.md).
|
||||||
|
|
||||||
|
```toml tab="TOML"
|
||||||
|
[http.services]
|
||||||
|
[http.services.mirroring]
|
||||||
|
[http.services.mirroring.mirroring]
|
||||||
|
service = "app"
|
||||||
|
[[http.services.mirroring.mirroring.mirrors]]
|
||||||
|
name = "mirror"
|
||||||
|
percent = 10
|
||||||
|
|
||||||
|
[http.services.app]
|
||||||
|
[http.services.app.loadBalancer]
|
||||||
|
[[http.services.appv1.loadBalancer.servers]]
|
||||||
|
url = "http://private-ip-server-1/"
|
||||||
|
|
||||||
|
[http.services.mirror]
|
||||||
|
[http.services.mirror.loadBalancer]
|
||||||
|
[[http.services.mirror.loadBalancer.servers]]
|
||||||
|
url = "http://private-ip-server-2/"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="YAML"
|
||||||
|
http:
|
||||||
|
services:
|
||||||
|
mirroring:
|
||||||
|
mirroring:
|
||||||
|
service: app
|
||||||
|
mirrors:
|
||||||
|
- name: mirror
|
||||||
|
percent: 10
|
||||||
|
|
||||||
|
app:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://private-ip-server-1/"
|
||||||
|
|
||||||
|
mirror:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://private-ip-server-2/"
|
||||||
|
```
|
||||||
|
|
||||||
## Configuring TCP Services
|
## Configuring TCP Services
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
44
integration/fixtures/mirror.toml
Normal file
44
integration/fixtures/mirror.toml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
[global]
|
||||||
|
checkNewVersion = false
|
||||||
|
sendAnonymousUsage = false
|
||||||
|
|
||||||
|
[api]
|
||||||
|
|
||||||
|
[log]
|
||||||
|
level = "DEBUG"
|
||||||
|
|
||||||
|
[entryPoints]
|
||||||
|
|
||||||
|
[entryPoints.web]
|
||||||
|
address = ":8000"
|
||||||
|
|
||||||
|
[providers.file]
|
||||||
|
filename = "{{ .SelfFilename }}"
|
||||||
|
|
||||||
|
## dynamic configuration ##
|
||||||
|
|
||||||
|
[http.routers]
|
||||||
|
[http.routers.router]
|
||||||
|
service = "mirror"
|
||||||
|
rule = "Path(`/whoami`)"
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
[http.services.mirror.mirroring]
|
||||||
|
service = "service1"
|
||||||
|
[[http.services.mirror.mirroring.mirrors]]
|
||||||
|
name = "mirror1"
|
||||||
|
percent = 10
|
||||||
|
[[http.services.mirror.mirroring.mirrors]]
|
||||||
|
name = "mirror2"
|
||||||
|
percent = 50
|
||||||
|
|
||||||
|
[http.services.service1.loadBalancer]
|
||||||
|
[[http.services.service1.loadBalancer.servers]]
|
||||||
|
url = "{{ .MainServer }}"
|
||||||
|
[http.services.mirror1.loadBalancer]
|
||||||
|
[[http.services.mirror1.loadBalancer.servers]]
|
||||||
|
url = "{{ .Mirror1Server }}"
|
||||||
|
[http.services.mirror2.loadBalancer]
|
||||||
|
[[http.services.mirror2.loadBalancer.servers]]
|
||||||
|
url = "{{ .Mirror2Server }}"
|
||||||
|
|
|
@ -2,6 +2,7 @@ package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -9,6 +10,7 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -671,3 +673,111 @@ func (s *SimpleSuite) TestWRRSticky(c *check.C) {
|
||||||
c.Assert(repartition[server1], checker.Equals, 4)
|
c.Assert(repartition[server1], checker.Equals, 4)
|
||||||
c.Assert(repartition[server2], checker.Equals, 0)
|
c.Assert(repartition[server2], checker.Equals, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SimpleSuite) TestMirror(c *check.C) {
|
||||||
|
var count, countMirror1, countMirror2 int32
|
||||||
|
|
||||||
|
main := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
atomic.AddInt32(&count, 1)
|
||||||
|
}))
|
||||||
|
|
||||||
|
mirror1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
atomic.AddInt32(&countMirror1, 1)
|
||||||
|
}))
|
||||||
|
|
||||||
|
mirror2 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
atomic.AddInt32(&countMirror2, 1)
|
||||||
|
}))
|
||||||
|
|
||||||
|
mainServer := main.URL
|
||||||
|
mirror1Server := mirror1.URL
|
||||||
|
mirror2Server := mirror2.URL
|
||||||
|
|
||||||
|
file := s.adaptFile(c, "fixtures/mirror.toml", struct {
|
||||||
|
MainServer string
|
||||||
|
Mirror1Server string
|
||||||
|
Mirror2Server string
|
||||||
|
}{MainServer: mainServer, Mirror1Server: mirror1Server, Mirror2Server: mirror2Server})
|
||||||
|
defer os.Remove(file)
|
||||||
|
|
||||||
|
cmd, output := s.traefikCmd(withConfigFile(file))
|
||||||
|
defer output(c)
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
|
err = try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("mirror1", "mirror2", "service1"))
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
response, err := http.DefaultClient.Do(req)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
c.Assert(response.StatusCode, checker.Equals, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
countTotal := atomic.LoadInt32(&count)
|
||||||
|
val1 := atomic.LoadInt32(&countMirror1)
|
||||||
|
val2 := atomic.LoadInt32(&countMirror2)
|
||||||
|
|
||||||
|
c.Assert(countTotal, checker.Equals, int32(10))
|
||||||
|
c.Assert(val1, checker.Equals, int32(1))
|
||||||
|
c.Assert(val2, checker.Equals, int32(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SimpleSuite) TestMirrorCanceled(c *check.C) {
|
||||||
|
var count, countMirror1, countMirror2 int32
|
||||||
|
|
||||||
|
main := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
atomic.AddInt32(&count, 1)
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
}))
|
||||||
|
|
||||||
|
mirror1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
atomic.AddInt32(&countMirror1, 1)
|
||||||
|
}))
|
||||||
|
|
||||||
|
mirror2 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
atomic.AddInt32(&countMirror2, 1)
|
||||||
|
}))
|
||||||
|
|
||||||
|
mainServer := main.URL
|
||||||
|
mirror1Server := mirror1.URL
|
||||||
|
mirror2Server := mirror2.URL
|
||||||
|
|
||||||
|
file := s.adaptFile(c, "fixtures/mirror.toml", struct {
|
||||||
|
MainServer string
|
||||||
|
Mirror1Server string
|
||||||
|
Mirror2Server string
|
||||||
|
}{MainServer: mainServer, Mirror1Server: mirror1Server, Mirror2Server: mirror2Server})
|
||||||
|
defer os.Remove(file)
|
||||||
|
|
||||||
|
cmd, output := s.traefikCmd(withConfigFile(file))
|
||||||
|
defer output(c)
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
|
err = try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("mirror1", "mirror2", "service1"))
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
newCtx, _ := context.WithTimeout(req.Context(), time.Second)
|
||||||
|
req = req.WithContext(newCtx)
|
||||||
|
http.DefaultClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
countTotal := atomic.LoadInt32(&count)
|
||||||
|
val1 := atomic.LoadInt32(&countMirror1)
|
||||||
|
val2 := atomic.LoadInt32(&countMirror2)
|
||||||
|
|
||||||
|
c.Assert(countTotal, checker.Equals, int32(5))
|
||||||
|
c.Assert(val1, checker.Equals, int32(0))
|
||||||
|
c.Assert(val2, checker.Equals, int32(0))
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ type HTTPConfiguration struct {
|
||||||
type Service struct {
|
type Service struct {
|
||||||
LoadBalancer *ServersLoadBalancer `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"`
|
LoadBalancer *ServersLoadBalancer `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"`
|
||||||
Weighted *WeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-"`
|
Weighted *WeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-"`
|
||||||
|
Mirroring *Mirroring `json:"mirroring,omitempty" toml:"mirroring,omitempty" yaml:"mirroring,omitempty" label:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +k8s:deepcopy-gen=true
|
// +k8s:deepcopy-gen=true
|
||||||
|
@ -46,6 +47,22 @@ type RouterTLSConfig struct {
|
||||||
|
|
||||||
// +k8s:deepcopy-gen=true
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
||||||
|
// Mirroring holds the Mirroring configuration.
|
||||||
|
type Mirroring struct {
|
||||||
|
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty"`
|
||||||
|
Mirrors []MirrorService `json:"mirrors,omitempty" toml:"mirrors,omitempty" yaml:"mirrors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
||||||
|
// MirrorService holds the MirrorService configuration.
|
||||||
|
type MirrorService struct {
|
||||||
|
Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"`
|
||||||
|
Percent int `json:"percent,omitempty" toml:"percent,omitempty" yaml:"percent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
||||||
// WeightedRoundRobin is a weighted round robin load-balancer of services.
|
// WeightedRoundRobin is a weighted round robin load-balancer of services.
|
||||||
type WeightedRoundRobin struct {
|
type WeightedRoundRobin struct {
|
||||||
Services []WRRService `json:"services,omitempty" toml:"services,omitempty" yaml:"services,omitempty"`
|
Services []WRRService `json:"services,omitempty" toml:"services,omitempty" yaml:"services,omitempty"`
|
||||||
|
|
|
@ -306,7 +306,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||||
Middlewares: test.middlewaresConfig,
|
Middlewares: test.middlewaresConfig,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil)
|
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil)
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
||||||
|
@ -407,7 +407,7 @@ func TestAccessLog(t *testing.T) {
|
||||||
Middlewares: test.middlewaresConfig,
|
Middlewares: test.middlewaresConfig,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil)
|
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil)
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
||||||
|
@ -693,7 +693,7 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||||
Middlewares: test.middlewareConfig,
|
Middlewares: test.middlewareConfig,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil)
|
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport, nil, nil)
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(map[string]*runtime.MiddlewareInfo{})
|
responseModifierFactory := responsemodifiers.NewBuilder(map[string]*runtime.MiddlewareInfo{})
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
||||||
|
@ -767,7 +767,7 @@ func BenchmarkRouterServe(b *testing.B) {
|
||||||
Middlewares: map[string]*dynamic.Middleware{},
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}, nil)
|
serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}, nil, nil)
|
||||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
|
||||||
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, responseModifierFactory)
|
||||||
|
@ -808,7 +808,7 @@ func BenchmarkService(b *testing.B) {
|
||||||
Services: serviceConfig,
|
Services: serviceConfig,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}, nil)
|
serviceManager := service.NewManager(rtConf.Services, &staticTransport{res}, nil, nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
|
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ func (s *Server) createTCPRouters(ctx context.Context, configuration *runtime.Co
|
||||||
|
|
||||||
// createHTTPHandlers returns, for the given configuration and entryPoints, the HTTP handlers for non-TLS connections, and for the TLS ones. the given configuration must not be nil. its fields will get mutated.
|
// createHTTPHandlers returns, for the given configuration and entryPoints, the HTTP handlers for non-TLS connections, and for the TLS ones. the given configuration must not be nil. its fields will get mutated.
|
||||||
func (s *Server) createHTTPHandlers(ctx context.Context, configuration *runtime.Configuration, entryPoints []string) (map[string]http.Handler, map[string]http.Handler) {
|
func (s *Server) createHTTPHandlers(ctx context.Context, configuration *runtime.Configuration, entryPoints []string) (map[string]http.Handler, map[string]http.Handler) {
|
||||||
serviceManager := service.NewManager(configuration.Services, s.defaultRoundTripper, s.metricsRegistry)
|
serviceManager := service.NewManager(configuration.Services, s.defaultRoundTripper, s.metricsRegistry, s.routinesPool)
|
||||||
middlewaresBuilder := middleware.NewBuilder(configuration.Middlewares, serviceManager)
|
middlewaresBuilder := middleware.NewBuilder(configuration.Middlewares, serviceManager)
|
||||||
responseModifierFactory := responsemodifiers.NewBuilder(configuration.Middlewares)
|
responseModifierFactory := responsemodifiers.NewBuilder(configuration.Middlewares)
|
||||||
routerManager := router.NewManager(configuration, serviceManager, middlewaresBuilder, responseModifierFactory)
|
routerManager := router.NewManager(configuration, serviceManager, middlewaresBuilder, responseModifierFactory)
|
||||||
|
|
104
pkg/server/service/loadbalancer/mirror/mirror.go
Normal file
104
pkg/server/service/loadbalancer/mirror/mirror.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package mirror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mirroring is an http.Handler that can mirror requests.
|
||||||
|
type Mirroring struct {
|
||||||
|
handler http.Handler
|
||||||
|
mirrorHandlers []*mirrorHandler
|
||||||
|
rw http.ResponseWriter
|
||||||
|
routinePool *safe.Pool
|
||||||
|
|
||||||
|
lock sync.RWMutex
|
||||||
|
total uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new instance of *Mirroring.
|
||||||
|
func New(handler http.Handler, pool *safe.Pool) *Mirroring {
|
||||||
|
return &Mirroring{
|
||||||
|
routinePool: pool,
|
||||||
|
handler: handler,
|
||||||
|
rw: blackholeResponseWriter{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mirroring) inc() uint64 {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
m.total++
|
||||||
|
return m.total
|
||||||
|
}
|
||||||
|
|
||||||
|
type mirrorHandler struct {
|
||||||
|
http.Handler
|
||||||
|
percent int
|
||||||
|
|
||||||
|
lock sync.RWMutex
|
||||||
|
count uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mirroring) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
m.handler.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-req.Context().Done():
|
||||||
|
// No mirroring if request has been canceled during main handler ServeHTTP
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
m.routinePool.GoCtx(func(_ context.Context) {
|
||||||
|
total := m.inc()
|
||||||
|
for _, handler := range m.mirrorHandlers {
|
||||||
|
handler.lock.Lock()
|
||||||
|
if handler.count*100 < total*uint64(handler.percent) {
|
||||||
|
handler.count++
|
||||||
|
handler.lock.Unlock()
|
||||||
|
// When a request served by m.handler is successful, req.Context will be cancelled,
|
||||||
|
// which would trigger a cancellation of the ongoing mirrored requests.
|
||||||
|
// Therefore, we give a new, non-cancellable context to each of the mirrored calls,
|
||||||
|
// so they can terminate by themselves.
|
||||||
|
handler.ServeHTTP(m.rw, req.WithContext(contextStopPropagation{req.Context()}))
|
||||||
|
} else {
|
||||||
|
handler.lock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMirror adds an httpHandler to mirror to.
|
||||||
|
func (m *Mirroring) AddMirror(handler http.Handler, percent int) error {
|
||||||
|
if percent < 0 || percent >= 100 {
|
||||||
|
return errors.New("percent must be between 0 and 100")
|
||||||
|
}
|
||||||
|
m.mirrorHandlers = append(m.mirrorHandlers, &mirrorHandler{Handler: handler, percent: percent})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type blackholeResponseWriter struct{}
|
||||||
|
|
||||||
|
func (b blackholeResponseWriter) Header() http.Header {
|
||||||
|
return http.Header{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b blackholeResponseWriter) Write(bytes []byte) (int, error) {
|
||||||
|
return len(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b blackholeResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
type contextStopPropagation struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c contextStopPropagation) Done() <-chan struct{} {
|
||||||
|
return make(chan struct{})
|
||||||
|
}
|
79
pkg/server/service/loadbalancer/mirror/mirror_test.go
Normal file
79
pkg/server/service/loadbalancer/mirror/mirror_test.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package mirror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMirroringOn100(t *testing.T) {
|
||||||
|
var countMirror1, countMirror2 int32
|
||||||
|
handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
pool := safe.NewPool(context.Background())
|
||||||
|
mirror := New(handler, pool)
|
||||||
|
err := mirror.AddMirror(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
atomic.AddInt32(&countMirror1, 1)
|
||||||
|
}), 10)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = mirror.AddMirror(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
atomic.AddInt32(&countMirror2, 1)
|
||||||
|
}), 50)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
mirror.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.Stop()
|
||||||
|
|
||||||
|
val1 := atomic.LoadInt32(&countMirror1)
|
||||||
|
val2 := atomic.LoadInt32(&countMirror2)
|
||||||
|
assert.Equal(t, 10, int(val1))
|
||||||
|
assert.Equal(t, 50, int(val2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMirroringOn10(t *testing.T) {
|
||||||
|
var countMirror1, countMirror2 int32
|
||||||
|
handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
pool := safe.NewPool(context.Background())
|
||||||
|
mirror := New(handler, pool)
|
||||||
|
err := mirror.AddMirror(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
atomic.AddInt32(&countMirror1, 1)
|
||||||
|
}), 10)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = mirror.AddMirror(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
atomic.AddInt32(&countMirror2, 1)
|
||||||
|
}), 50)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
mirror.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.Stop()
|
||||||
|
|
||||||
|
val1 := atomic.LoadInt32(&countMirror1)
|
||||||
|
val2 := atomic.LoadInt32(&countMirror2)
|
||||||
|
assert.Equal(t, 1, int(val1))
|
||||||
|
assert.Equal(t, 5, int(val2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidPercent(t *testing.T) {
|
||||||
|
mirror := New(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}), safe.NewPool(context.Background()))
|
||||||
|
err := mirror.AddMirror(nil, -1)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
err = mirror.AddMirror(nil, 101)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containous/alice"
|
"github.com/containous/alice"
|
||||||
|
@ -19,8 +20,10 @@ import (
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/emptybackendhandler"
|
"github.com/containous/traefik/v2/pkg/middlewares/emptybackendhandler"
|
||||||
metricsMiddle "github.com/containous/traefik/v2/pkg/middlewares/metrics"
|
metricsMiddle "github.com/containous/traefik/v2/pkg/middlewares/metrics"
|
||||||
"github.com/containous/traefik/v2/pkg/middlewares/pipelining"
|
"github.com/containous/traefik/v2/pkg/middlewares/pipelining"
|
||||||
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
"github.com/containous/traefik/v2/pkg/server/cookie"
|
"github.com/containous/traefik/v2/pkg/server/cookie"
|
||||||
"github.com/containous/traefik/v2/pkg/server/internal"
|
"github.com/containous/traefik/v2/pkg/server/internal"
|
||||||
|
"github.com/containous/traefik/v2/pkg/server/service/loadbalancer/mirror"
|
||||||
"github.com/containous/traefik/v2/pkg/server/service/loadbalancer/wrr"
|
"github.com/containous/traefik/v2/pkg/server/service/loadbalancer/wrr"
|
||||||
"github.com/vulcand/oxy/roundrobin"
|
"github.com/vulcand/oxy/roundrobin"
|
||||||
)
|
)
|
||||||
|
@ -31,8 +34,9 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewManager creates a new Manager
|
// NewManager creates a new Manager
|
||||||
func NewManager(configs map[string]*runtime.ServiceInfo, defaultRoundTripper http.RoundTripper, metricsRegistry metrics.Registry) *Manager {
|
func NewManager(configs map[string]*runtime.ServiceInfo, defaultRoundTripper http.RoundTripper, metricsRegistry metrics.Registry, routinePool *safe.Pool) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
|
routinePool: routinePool,
|
||||||
metricsRegistry: metricsRegistry,
|
metricsRegistry: metricsRegistry,
|
||||||
bufferPool: newBufferPool(),
|
bufferPool: newBufferPool(),
|
||||||
defaultRoundTripper: defaultRoundTripper,
|
defaultRoundTripper: defaultRoundTripper,
|
||||||
|
@ -43,6 +47,7 @@ func NewManager(configs map[string]*runtime.ServiceInfo, defaultRoundTripper htt
|
||||||
|
|
||||||
// Manager The service manager
|
// Manager The service manager
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
|
routinePool *safe.Pool
|
||||||
metricsRegistry metrics.Registry
|
metricsRegistry metrics.Registry
|
||||||
bufferPool httputil.BufferPool
|
bufferPool httputil.BufferPool
|
||||||
defaultRoundTripper http.RoundTripper
|
defaultRoundTripper http.RoundTripper
|
||||||
|
@ -62,7 +67,14 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string, respons
|
||||||
return nil, fmt.Errorf("the service %q does not exist", serviceName)
|
return nil, fmt.Errorf("the service %q does not exist", serviceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.LoadBalancer != nil && conf.Weighted != nil {
|
value := reflect.ValueOf(*conf.Service)
|
||||||
|
var count int
|
||||||
|
for i := 0; i < value.NumField(); i++ {
|
||||||
|
if !value.Field(i).IsNil() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count > 1 {
|
||||||
return nil, errors.New("cannot create service: multi-types service not supported, consider declaring two different pieces of service instead")
|
return nil, errors.New("cannot create service: multi-types service not supported, consider declaring two different pieces of service instead")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +95,13 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string, respons
|
||||||
conf.AddError(err, true)
|
conf.AddError(err, true)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
case conf.Mirroring != nil:
|
||||||
|
var err error
|
||||||
|
lb, err = m.getLoadBalancerMirrorServiceHandler(ctx, serviceName, conf.Mirroring, responseModifier)
|
||||||
|
if err != nil {
|
||||||
|
conf.AddError(err, true)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
sErr := fmt.Errorf("the service %q does not have any type defined", serviceName)
|
sErr := fmt.Errorf("the service %q does not have any type defined", serviceName)
|
||||||
conf.AddError(sErr, true)
|
conf.AddError(sErr, true)
|
||||||
|
@ -92,6 +111,27 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string, respons
|
||||||
return lb, nil
|
return lb, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getLoadBalancerMirrorServiceHandler(ctx context.Context, serviceName string, config *dynamic.Mirroring, responseModifier func(*http.Response) error) (http.Handler, error) {
|
||||||
|
serviceHandler, err := m.BuildHTTP(ctx, config.Service, responseModifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := mirror.New(serviceHandler, m.routinePool)
|
||||||
|
for _, mirrorConfig := range config.Mirrors {
|
||||||
|
mirrorHandler, err := m.BuildHTTP(ctx, mirrorConfig.Name, responseModifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.AddMirror(mirrorHandler, mirrorConfig.Percent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handler, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) getLoadBalancerWRRServiceHandler(ctx context.Context, serviceName string, config *dynamic.WeightedRoundRobin, responseModifier func(*http.Response) error) (http.Handler, error) {
|
func (m *Manager) getLoadBalancerWRRServiceHandler(ctx context.Context, serviceName string, config *dynamic.WeightedRoundRobin, responseModifier func(*http.Response) error) (http.Handler, error) {
|
||||||
// TODO Handle accesslog and metrics with multiple service name
|
// TODO Handle accesslog and metrics with multiple service name
|
||||||
if config.Sticky != nil && config.Sticky.Cookie != nil {
|
if config.Sticky != nil && config.Sticky.Cookie != nil {
|
||||||
|
|
|
@ -80,7 +80,7 @@ func TestGetLoadBalancer(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetLoadBalancerServiceHandler(t *testing.T) {
|
func TestGetLoadBalancerServiceHandler(t *testing.T) {
|
||||||
sm := NewManager(nil, http.DefaultTransport, nil)
|
sm := NewManager(nil, http.DefaultTransport, nil, nil)
|
||||||
|
|
||||||
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("X-From", "first")
|
w.Header().Set("X-From", "first")
|
||||||
|
@ -332,7 +332,7 @@ func TestManager_Build(t *testing.T) {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
manager := NewManager(test.configs, http.DefaultTransport, nil)
|
manager := NewManager(test.configs, http.DefaultTransport, nil, nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if len(test.providerName) > 0 {
|
if len(test.providerName) > 0 {
|
||||||
|
@ -345,4 +345,18 @@ func TestManager_Build(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMultipleTypeOnBuildHTTP(t *testing.T) {
|
||||||
|
manager := NewManager(map[string]*runtime.ServiceInfo{
|
||||||
|
"test@file": {
|
||||||
|
Service: &dynamic.Service{
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{},
|
||||||
|
Weighted: &dynamic.WeightedRoundRobin{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, http.DefaultTransport, nil, nil)
|
||||||
|
|
||||||
|
_, err := manager.BuildHTTP(context.Background(), "test@file", nil)
|
||||||
|
assert.Error(t, err, "cannot create service: multi-types service not supported, consider declaring two different pieces of service instead")
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME Add healthcheck tests
|
// FIXME Add healthcheck tests
|
||||||
|
|
Loading…
Reference in a new issue