improve http group load balancing (#3131)

This commit is contained in:
fatedier 2022-10-19 12:14:35 +08:00 committed by GitHub
parent 3fbe6b659e
commit cf66ca10b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 119 additions and 47 deletions

View File

@ -19,7 +19,7 @@ import (
"strings" "strings"
) )
var version = "0.44.0" var version = "0.45.0"
func Full() string { func Full() string {
return version return version

View File

@ -60,19 +60,28 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
// Modify incoming requests by route policies. // Modify incoming requests by route policies.
Director: func(req *http.Request) { Director: func(req *http.Request) {
req.URL.Scheme = "http" req.URL.Scheme = "http"
url := req.Context().Value(RouteInfoURL).(string) reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo)
routeByHTTPUser := req.Context().Value(RouteInfoHTTPUser).(string) oldHost, _ := util.CanonicalHost(reqRouteInfo.Host)
oldHost, _ := util.CanonicalHost(req.Context().Value(RouteInfoHost).(string))
rc := rp.GetRouteConfig(oldHost, url, routeByHTTPUser) rc := rp.GetRouteConfig(oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
if rc != nil { if rc != nil {
if rc.RewriteHost != "" { if rc.RewriteHost != "" {
req.Host = rc.RewriteHost req.Host = rc.RewriteHost
} }
// Set {domain}.{location}.{routeByHTTPUser} as URL host here to let http transport reuse connections.
// TODO(fatedier): use proxy name instead? var endpoint string
if rc.ChooseEndpointFn != nil {
// ignore error here, it will use CreateConnFn instead later
endpoint, _ = rc.ChooseEndpointFn()
reqRouteInfo.Endpoint = endpoint
frpLog.Trace("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]",
endpoint, oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
}
// Set {domain}.{location}.{routeByHTTPUser}.{endpoint} as URL host here to let http transport reuse connections.
req.URL.Host = rc.Domain + "." + req.URL.Host = rc.Domain + "." +
base64.StdEncoding.EncodeToString([]byte(rc.Location)) + "." + base64.StdEncoding.EncodeToString([]byte(rc.Location)) + "." +
base64.StdEncoding.EncodeToString([]byte(rc.RouteByHTTPUser)) base64.StdEncoding.EncodeToString([]byte(rc.RouteByHTTPUser)) + "." +
base64.StdEncoding.EncodeToString([]byte(endpoint))
for k, v := range rc.Headers { for k, v := range rc.Headers {
req.Header.Set(k, v) req.Header.Set(k, v)
@ -85,12 +94,9 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
Transport: &http.Transport{ Transport: &http.Transport{
ResponseHeaderTimeout: rp.responseHeaderTimeout, ResponseHeaderTimeout: rp.responseHeaderTimeout,
IdleConnTimeout: 60 * time.Second, IdleConnTimeout: 60 * time.Second,
MaxIdleConnsPerHost: 5,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
url := ctx.Value(RouteInfoURL).(string) return rp.CreateConnection(ctx.Value(RouteInfoKey).(*RequestRouteInfo), true)
host, _ := util.CanonicalHost(ctx.Value(RouteInfoHost).(string))
routerByHTTPUser := ctx.Value(RouteInfoHTTPUser).(string)
remote := ctx.Value(RouteInfoRemote).(string)
return rp.CreateConnection(host, url, routerByHTTPUser, remote)
}, },
Proxy: func(req *http.Request) (*url.URL, error) { Proxy: func(req *http.Request) (*url.URL, error) {
// Use proxy mode if there is host in HTTP first request line. // Use proxy mode if there is host in HTTP first request line.
@ -100,7 +106,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
// Normal: // Normal:
// GET / HTTP/1.1 // GET / HTTP/1.1
// Host: example.com // Host: example.com
urlHost := req.Context().Value(RouteInfoURLHost).(string) urlHost := req.Context().Value(RouteInfoKey).(*RequestRouteInfo).URLHost
if urlHost != "" { if urlHost != "" {
return req.URL, nil return req.URL, nil
} }
@ -143,14 +149,6 @@ func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser str
return nil return nil
} }
func (rp *HTTPReverseProxy) GetRealHost(domain, location, routeByHTTPUser string) (host string) {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok {
host = vr.payload.(*RouteConfig).RewriteHost
}
return
}
func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string) (headers map[string]string) { func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string) (headers map[string]string) {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser) vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok { if ok {
@ -160,15 +158,22 @@ func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string)
} }
// CreateConnection create a new connection by route config // CreateConnection create a new connection by route config
func (rp *HTTPReverseProxy) CreateConnection(domain, location, routeByHTTPUser string, remoteAddr string) (net.Conn, error) { func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser) host, _ := util.CanonicalHost(reqRouteInfo.Host)
vr, ok := rp.getVhost(host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
if ok { if ok {
if byEndpoint {
fn := vr.payload.(*RouteConfig).CreateConnByEndpointFn
if fn != nil {
return fn(reqRouteInfo.Endpoint, reqRouteInfo.RemoteAddr)
}
}
fn := vr.payload.(*RouteConfig).CreateConnFn fn := vr.payload.(*RouteConfig).CreateConnFn
if fn != nil { if fn != nil {
return fn(remoteAddr) return fn(reqRouteInfo.RemoteAddr)
} }
} }
return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, domain, location, routeByHTTPUser) return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
} }
func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, passwd string) bool { func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, passwd string) bool {
@ -244,12 +249,7 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req
return return
} }
url := req.Context().Value(RouteInfoURL).(string) remote, err := rp.CreateConnection(req.Context().Value(RouteInfoKey).(*RequestRouteInfo), false)
routeByHTTPUser := req.Context().Value(RouteInfoHTTPUser).(string)
domain, _ := util.CanonicalHost(req.Context().Value(RouteInfoHost).(string))
remoteAddr := req.Context().Value(RouteInfoRemote).(string)
remote, err := rp.CreateConnection(domain, url, routeByHTTPUser, remoteAddr)
if err != nil { if err != nil {
_ = notFoundResponse().Write(client) _ = notFoundResponse().Write(client)
client.Close() client.Close()
@ -278,11 +278,6 @@ func parseBasicAuth(auth string) (username, password string, ok bool) {
} }
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request { func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
newctx := req.Context()
newctx = context.WithValue(newctx, RouteInfoURL, req.URL.Path)
newctx = context.WithValue(newctx, RouteInfoHost, req.Host)
newctx = context.WithValue(newctx, RouteInfoURLHost, req.URL.Host)
user := "" user := ""
// If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header. // If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header.
if req.URL.Host != "" { if req.URL.Host != "" {
@ -294,8 +289,16 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ
if user == "" { if user == "" {
user, _, _ = req.BasicAuth() user, _, _ = req.BasicAuth()
} }
newctx = context.WithValue(newctx, RouteInfoHTTPUser, user)
newctx = context.WithValue(newctx, RouteInfoRemote, req.RemoteAddr) reqRouteInfo := &RequestRouteInfo{
URL: req.URL.Path,
Host: req.Host,
HTTPUser: user,
RemoteAddr: req.RemoteAddr,
URLHost: req.URL.Host,
}
newctx := req.Context()
newctx = context.WithValue(newctx, RouteInfoKey, reqRouteInfo)
return req.Clone(newctx) return req.Clone(newctx)
} }

View File

@ -29,13 +29,18 @@ import (
type RouteInfo string type RouteInfo string
const ( const (
RouteInfoURL RouteInfo = "url" RouteInfoKey RouteInfo = "routeInfo"
RouteInfoHost RouteInfo = "host"
RouteInfoHTTPUser RouteInfo = "httpUser"
RouteInfoRemote RouteInfo = "remote"
RouteInfoURLHost RouteInfo = "urlHost"
) )
type RequestRouteInfo struct {
URL string
Host string
HTTPUser string
RemoteAddr string
URLHost string
Endpoint string
}
type ( type (
muxFunc func(net.Conn) (net.Conn, map[string]string, error) muxFunc func(net.Conn) (net.Conn, map[string]string, error)
httpAuthFunc func(net.Conn, string, string, string) (bool, error) httpAuthFunc func(net.Conn, string, string, string) (bool, error)
@ -75,8 +80,12 @@ func NewMuxer(
return mux, nil return mux, nil
} }
type ChooseEndpointFunc func() (string, error)
type CreateConnFunc func(remoteAddr string) (net.Conn, error) type CreateConnFunc func(remoteAddr string) (net.Conn, error)
type CreateConnByEndpointFunc func(endpoint, remoteAddr string) (net.Conn, error)
// RouteConfig is the params used to match HTTP requests // RouteConfig is the params used to match HTTP requests
type RouteConfig struct { type RouteConfig struct {
Domain string Domain string
@ -87,7 +96,9 @@ type RouteConfig struct {
Headers map[string]string Headers map[string]string
RouteByHTTPUser string RouteByHTTPUser string
CreateConnFn CreateConnFunc CreateConnFn CreateConnFunc
ChooseEndpointFn ChooseEndpointFunc
CreateConnByEndpointFn CreateConnByEndpointFunc
} }
// listen for a new domain name, if rewriteHost is not empty and rewriteFunc is not nil // listen for a new domain name, if rewriteHost is not empty and rewriteFunc is not nil

View File

@ -10,7 +10,7 @@ import (
) )
type HTTPGroupController struct { type HTTPGroupController struct {
// groups by indexKey // groups indexed by group name
groups map[string]*HTTPGroup groups map[string]*HTTPGroup
// register createConn for each group to vhostRouter. // register createConn for each group to vhostRouter.
@ -65,7 +65,7 @@ type HTTPGroup struct {
location string location string
routeByHTTPUser string routeByHTTPUser string
// CreateConnFuncs indexed by echo proxy name // CreateConnFuncs indexed by proxy name
createFuncs map[string]vhost.CreateConnFunc createFuncs map[string]vhost.CreateConnFunc
pxyNames []string pxyNames []string
index uint64 index uint64
@ -91,6 +91,8 @@ func (g *HTTPGroup) Register(
// the first proxy in this group // the first proxy in this group
tmp := routeConfig // copy object tmp := routeConfig // copy object
tmp.CreateConnFn = g.createConn tmp.CreateConnFn = g.createConn
tmp.ChooseEndpointFn = g.chooseEndpoint
tmp.CreateConnByEndpointFn = g.createConnByEndpoint
err = g.ctl.vhostRouter.Add(routeConfig.Domain, routeConfig.Location, routeConfig.RouteByHTTPUser, &tmp) err = g.ctl.vhostRouter.Add(routeConfig.Domain, routeConfig.Location, routeConfig.RouteByHTTPUser, &tmp)
if err != nil { if err != nil {
return return
@ -161,3 +163,36 @@ func (g *HTTPGroup) createConn(remoteAddr string) (net.Conn, error) {
return f(remoteAddr) return f(remoteAddr)
} }
func (g *HTTPGroup) chooseEndpoint() (string, error) {
newIndex := atomic.AddUint64(&g.index, 1)
name := ""
g.mu.RLock()
group := g.group
domain := g.domain
location := g.location
routeByHTTPUser := g.routeByHTTPUser
if len(g.pxyNames) > 0 {
name = g.pxyNames[int(newIndex)%len(g.pxyNames)]
}
g.mu.RUnlock()
if name == "" {
return "", fmt.Errorf("no healthy endpoint for http group [%s], domain [%s], location [%s], routeByHTTPUser [%s]",
group, domain, location, routeByHTTPUser)
}
return name, nil
}
func (g *HTTPGroup) createConnByEndpoint(endpoint, remoteAddr string) (net.Conn, error) {
var f vhost.CreateConnFunc
g.mu.RLock()
f = g.createFuncs[endpoint]
g.mu.RUnlock()
if f == nil {
return nil, fmt.Errorf("no CreateConnFunc for endpoint [%s] in group [%s]", endpoint, g.group)
}
return f(remoteAddr)
}

View File

@ -217,6 +217,29 @@ var _ = ginkgo.Describe("[Feature: Group]", func() {
f.RunProcesses([]string{serverConf}, []string{clientConf}) f.RunProcesses([]string{serverConf}, []string{clientConf})
// send first HTTP request
var contents []string
framework.NewRequestExpect(f).Port(vhostPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("example.com")
}).
Ensure(func(resp *request.Response) bool {
contents = append(contents, string(resp.Content))
return true
})
// send second HTTP request, should be forwarded to another service
framework.NewRequestExpect(f).Port(vhostPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("example.com")
}).
Ensure(func(resp *request.Response) bool {
contents = append(contents, string(resp.Content))
return true
})
framework.ExpectContainElements(contents, []string{"foo", "bar"})
// check foo and bar is ok // check foo and bar is ok
results := doFooBarHTTPRequest(vhostPort, "example.com") results := doFooBarHTTPRequest(vhostPort, "example.com")
framework.ExpectContainElements(results, []string{"foo", "bar"}) framework.ExpectContainElements(results, []string{"foo", "bar"})