Skip to content

Commit

Permalink
Introduce a fast proxy mode to improve HTTP/1.1 performances with bac…
Browse files Browse the repository at this point in the history
…kends
  • Loading branch information
juliens authored and kevinpollet committed Sep 24, 2024
1 parent a398536 commit aa710d3
Show file tree
Hide file tree
Showing 29 changed files with 2,838 additions and 367 deletions.
10 changes: 7 additions & 3 deletions cmd/traefik/traefik.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/traefik/traefik/v3/pkg/provider/aggregator"
"github.com/traefik/traefik/v3/pkg/provider/tailscale"
"github.com/traefik/traefik/v3/pkg/provider/traefik"
"github.com/traefik/traefik/v3/pkg/proxy"
"github.com/traefik/traefik/v3/pkg/safe"
"github.com/traefik/traefik/v3/pkg/server"
"github.com/traefik/traefik/v3/pkg/server/middleware"
Expand Down Expand Up @@ -281,10 +282,12 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
log.Info().Msg("Successfully obtained SPIFFE SVID.")
}

roundTripperManager := service.NewRoundTripperManager(spiffeX509Source)
transportManager := service.NewTransportManager(spiffeX509Source)
fastProxy := staticConfiguration.Experimental != nil && staticConfiguration.Experimental.FastProxy
proxyBuilder := proxy.NewBuilder(transportManager, semConvMetricRegistry, fastProxy)
dialerManager := tcp.NewDialerManager(spiffeX509Source)
acmeHTTPHandler := getHTTPChallengeHandler(acmeProviders, httpChallengeProvider)
managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, roundTripperManager, acmeHTTPHandler)
managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, transportManager, proxyBuilder, acmeHTTPHandler)

// Router factory

Expand Down Expand Up @@ -318,7 +321,8 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err

// Server Transports
watcher.AddListener(func(conf dynamic.Configuration) {
roundTripperManager.Update(conf.HTTP.ServersTransports)
transportManager.Update(conf.HTTP.ServersTransports)
proxyBuilder.Update(conf.HTTP.ServersTransports)
dialerManager.Update(conf.TCP.ServersTransports)
})

Expand Down
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/BurntSushi/toml v1.4.0
github.com/Masterminds/sprig/v3 v3.2.3
github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000 // No tag on the repo.
github.com/andybalholm/brotli v1.0.6
github.com/andybalholm/brotli v1.1.0
github.com/aws/aws-sdk-go v1.44.327
github.com/cenkalti/backoff/v4 v4.3.0
github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd // No tag on the repo.
Expand Down Expand Up @@ -102,6 +102,11 @@ require (
sigs.k8s.io/yaml v1.4.0
)

require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/valyala/fasthttp v1.55.0
)

require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
dario.cat/mergo v1.0.0 // indirect
Expand Down Expand Up @@ -315,6 +320,7 @@ require (
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/transip/gotransip/v6 v6.23.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/vultr/govultr/v3 v3.9.0 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20240318083951-4fe6125f286e // indirect
Expand Down
7 changes: 5 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 h1:lM7JnA9dEdDFH9XOgRNQMDTQnOjlLkDTNA7c0aWTQ30=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.712/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
Expand Down Expand Up @@ -1039,7 +1039,10 @@ github.com/unrolled/secure v1.0.9 h1:BWRuEb1vDrBFFDdbCnKkof3gZ35I/bnHGyt0LB0TNyQ
github.com/unrolled/secure v1.0.9/go.mod h1:fO+mEan+FLB0CdEnHf6Q4ZZVNqG+5fuLFnP8p0BXDPI=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/static/experimental.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type Experimental struct {
Plugins map[string]plugins.Descriptor `description:"Plugins configuration." json:"plugins,omitempty" toml:"plugins,omitempty" yaml:"plugins,omitempty" export:"true"`
LocalPlugins map[string]plugins.LocalDescriptor `description:"Local plugins configuration." json:"localPlugins,omitempty" toml:"localPlugins,omitempty" yaml:"localPlugins,omitempty" export:"true"`

FastProxy bool `description:"Enable the FastProxy implementation." json:"fastProxy,omitempty" toml:"fastProxy,omitempty" yaml:"fastProxy,omitempty" export:"true"`

// Deprecated: KubernetesGateway provider is not an experimental feature starting with v3.1. Please remove its usage from the static configuration.
KubernetesGateway bool `description:"(Deprecated) Allow the Kubernetes gateway api provider usage." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true"`
}
61 changes: 61 additions & 0 deletions pkg/proxy/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package proxy

import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"time"

"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/metrics"
"github.com/traefik/traefik/v3/pkg/proxy/fast"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
)

// TransportManager manages transport used for backend communications.
// FIXME duplicate??
type TransportManager interface {
Get(name string) (*dynamic.ServersTransport, error)
GetRoundTripper(name string) (http.RoundTripper, error)
GetTLSConfig(name string) (*tls.Config, error)
}

// Builder is a proxy builder which returns a fasthttp or httputil proxy corresponding
// to the ServersTransport configuration.
type Builder struct {
fastProxyBuilder *fast.ProxyBuilder
httputilBuilder *httputil.ProxyBuilder

transportManager httputil.TransportManager
fastProxy bool
}

// NewBuilder creates and returns a new Builder instance.
func NewBuilder(transportManager TransportManager, semConvMetricsRegistry *metrics.SemConvMetricsRegistry, fastProxy bool) *Builder {
return &Builder{
fastProxyBuilder: fast.NewProxyBuilder(transportManager),
httputilBuilder: httputil.NewProxyBuilder(transportManager, semConvMetricsRegistry),
transportManager: transportManager,
fastProxy: fastProxy,
}
}

// Update is the handler called when the dynamic configuration is updated.
func (b *Builder) Update(newConfigs map[string]*dynamic.ServersTransport) {
b.fastProxyBuilder.Update(newConfigs)
}

// Build builds an HTTP proxy for the given URL using the ServersTransport with the given name.
func (b *Builder) Build(configName string, targetURL *url.URL, shouldObserve, passHostHeader bool, flushInterval time.Duration) (http.Handler, error) {
serversTransport, err := b.transportManager.Get(configName)
if err != nil {
return nil, fmt.Errorf("getting ServersTransport: %w", err)
}

if !b.fastProxy || !serversTransport.DisableHTTP2 && (targetURL.Scheme == "https" || targetURL.Scheme == "h2c") {
return b.httputilBuilder.Build(configName, targetURL, shouldObserve, passHostHeader, flushInterval)
}

return b.fastProxyBuilder.Build(configName, targetURL, passHostHeader)
}
120 changes: 120 additions & 0 deletions pkg/proxy/fast/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package fast

import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"reflect"
"time"

"github.com/traefik/traefik/v3/pkg/config/dynamic"
)

// TransportManager manages transport used for backend communications.
type TransportManager interface {
Get(name string) (*dynamic.ServersTransport, error)
GetTLSConfig(name string) (*tls.Config, error)
}

// ProxyBuilder handles the connection pools for the FastProxy proxies.
type ProxyBuilder struct {
transportManager TransportManager

// lock isn't needed because ProxyBuilder is not called concurrently.
pools map[string]map[string]*connPool
proxy func(*http.Request) (*url.URL, error)

// not goroutine safe.
configs map[string]*dynamic.ServersTransport
}

// NewProxyBuilder creates a new ProxyBuilder.
func NewProxyBuilder(transportManager TransportManager) *ProxyBuilder {
return &ProxyBuilder{
transportManager: transportManager,
pools: make(map[string]map[string]*connPool),
proxy: http.ProxyFromEnvironment,
configs: make(map[string]*dynamic.ServersTransport),
}
}

// Update updates all the round-tripper corresponding to the given configs.
// This method must not be used concurrently.
func (r *ProxyBuilder) Update(newConfigs map[string]*dynamic.ServersTransport) {
for configName := range r.configs {
if _, ok := newConfigs[configName]; !ok {
delete(r.pools, configName)
}
}

for newConfigName, newConfig := range newConfigs {
if !reflect.DeepEqual(newConfig, r.configs[newConfigName]) {
delete(r.pools, newConfigName)
}
}

r.configs = newConfigs
}

// Build builds a new ReverseProxy with the given configuration.
func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, passHostHeader bool) (http.Handler, error) {
proxyURL, err := r.proxy(&http.Request{URL: targetURL})
if err != nil {
return nil, fmt.Errorf("getting proxy: %w", err)
}

cfg, err := r.transportManager.Get(cfgName)
if err != nil {
return nil, fmt.Errorf("getting ServersTransport: %w", err)
}

var responseHeaderTimeout time.Duration
if cfg.ForwardingTimeouts != nil {
responseHeaderTimeout = time.Duration(cfg.ForwardingTimeouts.ResponseHeaderTimeout)
}

tlsConfig, err := r.transportManager.GetTLSConfig(cfgName)
if err != nil {
return nil, fmt.Errorf("getting TLS config: %w", err)
}

pool := r.getPool(cfgName, cfg, tlsConfig, targetURL, proxyURL)
return NewReverseProxy(targetURL, proxyURL, passHostHeader, responseHeaderTimeout, pool)
}

func (r *ProxyBuilder) getPool(cfgName string, config *dynamic.ServersTransport, tlsConfig *tls.Config, targetURL *url.URL, proxyURL *url.URL) *connPool {
pool, ok := r.pools[cfgName]
if !ok {
pool = make(map[string]*connPool)
r.pools[cfgName] = pool
}

if connPool, ok := pool[targetURL.String()]; ok {
return connPool
}

idleConnTimeout := 90 * time.Second
dialTimeout := 30 * time.Second
if config.ForwardingTimeouts != nil {
idleConnTimeout = time.Duration(config.ForwardingTimeouts.IdleConnTimeout)
dialTimeout = time.Duration(config.ForwardingTimeouts.DialTimeout)
}

proxyDialer := newDialer(dialerConfig{
DialKeepAlive: 0,
DialTimeout: dialTimeout,
HTTP: true,
TLS: targetURL.Scheme == "https",
ProxyURL: proxyURL,
}, tlsConfig)

connPool := newConnPool(config.MaxIdleConnsPerHost, idleConnTimeout, func() (net.Conn, error) {
return proxyDialer.Dial("tcp", addrFromURL(targetURL))
})

r.pools[cfgName][targetURL.String()] = connPool

return connPool
}
Loading

0 comments on commit aa710d3

Please sign in to comment.