diff --git a/config/config.go b/config/config.go index 231cedb88..304aa509f 100644 --- a/config/config.go +++ b/config/config.go @@ -19,6 +19,7 @@ type Config struct { ProfilePath string Insecure bool GlobMatchingDisabled bool + GlobCacheSize int } type CertSource struct { diff --git a/config/default.go b/config/default.go index 1574a4d16..52584aafe 100644 --- a/config/default.go +++ b/config/default.go @@ -103,4 +103,6 @@ var defaultConfig = &Config{ SpanHost: "localhost:9998", TraceID128Bit: true, }, + + GlobCacheSize: 1000, } diff --git a/config/load.go b/config/load.go index cccef573b..b8a7aed2a 100644 --- a/config/load.go +++ b/config/load.go @@ -209,6 +209,7 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c f.StringVar(&cfg.Tracing.SpanHost, "tracing.SpanHost", defaultConfig.Tracing.SpanHost, "Host:Port info to add to spans") f.BoolVar(&cfg.Tracing.TraceID128Bit, "tracing.TraceID128Bit", defaultConfig.Tracing.TraceID128Bit, "Generate 128 bit trace IDs") f.BoolVar(&cfg.GlobMatchingDisabled, "glob.matching.disabled", defaultConfig.GlobMatchingDisabled, "Disable Glob Matching on routes, one of [true, false]") + f.IntVar(&cfg.GlobCacheSize, "glob.cache.size", defaultConfig.GlobCacheSize, "sets the size of the glob cache") f.StringVar(&cfg.Registry.Custom.Host, "registry.custom.host", defaultConfig.Registry.Custom.Host, "custom back end hostname/port") f.StringVar(&cfg.Registry.Custom.Scheme, "registry.custom.scheme", defaultConfig.Registry.Custom.Scheme, "custom back end scheme - http/https") diff --git a/config/load_test.go b/config/load_test.go index a25fa4f17..3fab9c9a3 100644 --- a/config/load_test.go +++ b/config/load_test.go @@ -1096,6 +1096,13 @@ func TestLoad(t *testing.T) { cfg: func(cfg *Config) *Config { return nil }, err: errors.New("missing 'file' in auth 'foo'"), }, + { + args: []string{"-glob.cache.size", "1000"}, + cfg: func(cfg *Config) *Config { + cfg.GlobCacheSize = 1000 + return cfg + }, + }, { args: []string{"-cfg"}, cfg: func(cfg *Config) *Config { return nil }, diff --git a/docs/content/ref/glob.cache.size.md b/docs/content/ref/glob.cache.size.md new file mode 100644 index 000000000..ea55f20ca --- /dev/null +++ b/docs/content/ref/glob.cache.size.md @@ -0,0 +1,9 @@ +--- +title: "glob.cache.size" +--- + +`glob.cache.size` Sets the globCache size used for matching on route lookups. + +The default is + + glob.cache.size = 1000 diff --git a/fabio.properties b/fabio.properties index cd7f99795..fd9b80863 100644 --- a/fabio.properties +++ b/fabio.properties @@ -483,6 +483,7 @@ # # proxy.gzip.contenttype = + # proxy.auth configures one or more auth schemes. # # Each auth scheme is configured with a list of @@ -524,6 +525,7 @@ # proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd # name=myotherauth;type=basic;file=p/other-creds.htpasswd;realm=myrealm + # log.access.format configures the format of the access log. # # If the value is either 'common' or 'combined' then the logs are written in @@ -722,6 +724,7 @@ # # registry.consul.tls.cafile = + # registry.consul.tls.capath the path to the folder containing CA certificates. # # This is the full path to the folder with CA certificates while using TLS transport to @@ -890,6 +893,7 @@ # # registry.consul.serviceMonitors = 1 + # registry.consul.pollInterval configures the poll interval # for route updates. If Poll interval is set to 0 the updates will # be disabled and fall back to blocking queries. Other values can @@ -947,6 +951,7 @@ # # registry.custom.path = + # registry.custom.queryparams is the query parameters used in the custom back # end API Call # @@ -971,6 +976,12 @@ # # glob.matching.disabled = false +# glob.cache.size sets the globCache size used for matching on route lookups. +# +# The default is +# +# glob.cache.size = 1000 + # metrics.target configures the backend the metrics values are # sent to. @@ -1246,6 +1257,7 @@ # # tracing.TracingEnabled = false + # tracing.CollectorType sets what type of collector is used. # Currently only two types are supported http and kafka # diff --git a/main.go b/main.go index 408515a5e..e7d5ad1dc 100644 --- a/main.go +++ b/main.go @@ -148,6 +148,10 @@ func main() { } func newGrpcProxy(cfg *config.Config, tlscfg *tls.Config) []grpc.ServerOption { + + //Init Glob Cache + globCache := route.NewGlobCache(cfg.GlobCacheSize) + statsHandler := &proxy.GrpcStatsHandler{ Connect: metrics.DefaultRegistry.GetCounter("grpc.conn"), Request: metrics.DefaultRegistry.GetTimer("grpc.requests"), @@ -157,6 +161,7 @@ func newGrpcProxy(cfg *config.Config, tlscfg *tls.Config) []grpc.ServerOption { proxyInterceptor := proxy.GrpcProxyInterceptor{ Config: cfg, StatsHandler: statsHandler, + GlobCache: globCache, } handler := grpc_proxy.TransparentHandler(proxy.GetGRPCDirector(tlscfg)) @@ -171,6 +176,10 @@ func newGrpcProxy(cfg *config.Config, tlscfg *tls.Config) []grpc.ServerOption { func newHTTPProxy(cfg *config.Config) http.Handler { var w io.Writer + + //Init Glob Cache + globCache := route.NewGlobCache(cfg.GlobCacheSize) + switch cfg.Log.AccessTarget { case "": log.Printf("[INFO] Access logging disabled") @@ -223,7 +232,7 @@ func newHTTPProxy(cfg *config.Config) http.Handler { Transport: newTransport(nil), InsecureTransport: newTransport(&tls.Config{InsecureSkipVerify: true}), Lookup: func(r *http.Request) *route.Target { - t := route.GetTable().Lookup(r, r.Header.Get("trace"), pick, match, cfg.GlobMatchingDisabled) + t := route.GetTable().Lookup(r, r.Header.Get("trace"), pick, match, globCache, cfg.GlobMatchingDisabled) if t == nil { notFound.Inc(1) log.Print("[WARN] No route for ", r.Host, r.URL) diff --git a/proxy/grpc_handler.go b/proxy/grpc_handler.go index acc45d5b1..863d834d8 100644 --- a/proxy/grpc_handler.go +++ b/proxy/grpc_handler.go @@ -74,6 +74,7 @@ func GetGRPCDirector(tlscfg *tls.Config) func(ctx context.Context, fullMethodNam type GrpcProxyInterceptor struct { Config *config.Config StatsHandler *GrpcStatsHandler + GlobCache *route.GlobCache } type targetKey struct{} @@ -153,7 +154,7 @@ func (g GrpcProxyInterceptor) lookup(ctx context.Context, fullMethodName string) Header: headers, } - return route.GetTable().Lookup(req, req.Header.Get("trace"), pick, match, g.Config.GlobMatchingDisabled), nil + return route.GetTable().Lookup(req, req.Header.Get("trace"), pick, match, g.GlobCache, g.Config.GlobMatchingDisabled), nil } type GrpcStatsHandler struct { diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index 99aaeed52..9489cf6b7 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -33,6 +33,9 @@ const ( globDisabled = true ) +//Global GlobCache for Testing +var globCache = route.NewGlobCache(1000) + func TestProxyProducesCorrectXForwardedSomethingHeader(t *testing.T) { var hdr http.Header = make(http.Header) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -194,7 +197,7 @@ func TestProxyStripsPath(t *testing.T) { Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add mock /foo/bar " + server.URL + ` opts "strip=/foo"`)) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() @@ -230,7 +233,7 @@ func TestProxyHost(t *testing.T) { }, }, Lookup: func(r *http.Request) *route.Target { - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() @@ -277,7 +280,7 @@ func TestHostRedirect(t *testing.T) { Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { r.Host = "c.com" - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() @@ -316,7 +319,7 @@ func TestPathRedirect(t *testing.T) { proxy := httptest.NewServer(&HTTPProxy{ Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() @@ -484,7 +487,7 @@ func TestProxyHTTPSUpstream(t *testing.T) { Transport: &http.Transport{TLSClientConfig: tlsClientConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add srv / " + server.URL + ` opts "proto=https"`)) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() @@ -511,7 +514,7 @@ func TestProxyHTTPSUpstreamSkipVerify(t *testing.T) { }, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add srv / " + server.URL + ` opts "proto=https tlsskipverify=true"`)) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer proxy.Close() @@ -712,7 +715,7 @@ func BenchmarkProxyLogger(b *testing.B) { Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add mock / " + server.URL)) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, Logger: l, } diff --git a/proxy/listen_test.go b/proxy/listen_test.go index 1e80e6f93..e811f695d 100644 --- a/proxy/listen_test.go +++ b/proxy/listen_test.go @@ -23,8 +23,6 @@ func TestGracefulShutdown(t *testing.T) { })) defer srv.Close() - globDisabled := false - // start proxy addr := "127.0.0.1:57777" var wg sync.WaitGroup @@ -35,7 +33,7 @@ func TestGracefulShutdown(t *testing.T) { Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString("route add svc / " + srv.URL)) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globDisabled) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, } l := config.Listen{Addr: addr} diff --git a/proxy/ws_integration_test.go b/proxy/ws_integration_test.go index 3f83a5a51..dcf487b74 100644 --- a/proxy/ws_integration_test.go +++ b/proxy/ws_integration_test.go @@ -33,8 +33,6 @@ func TestProxyWSUpstream(t *testing.T) { defer wssServer.Close() t.Log("Started WSS server: ", wssServer.URL) - globDisabled := false - routes := "route add ws /ws " + wsServer.URL + "\n" routes += "route add ws /wss " + wssServer.URL + ` opts "proto=https"` + "\n" routes += "route add ws /insecure " + wssServer.URL + ` opts "proto=https tlsskipverify=true"` + "\n" @@ -46,7 +44,7 @@ func TestProxyWSUpstream(t *testing.T) { InsecureTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString(routes)) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globDisabled) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) defer httpProxy.Close() @@ -58,7 +56,7 @@ func TestProxyWSUpstream(t *testing.T) { InsecureTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(bytes.NewBufferString(routes)) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globDisabled) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globCache, globEnabled) }, }) httpsProxy.TLS = tlsServerConfig() diff --git a/registry/consul/service.go b/registry/consul/service.go index 39b38be03..040cf1eb0 100644 --- a/registry/consul/service.go +++ b/registry/consul/service.go @@ -40,14 +40,12 @@ func (w *ServiceMonitor) Watch(updates chan string) { } else { q = &api.QueryOptions{RequireConsistent: true, WaitIndex: lastIndex} } - checks, meta, err := w.client.Health().State("any", q) if err != nil { log.Printf("[WARN] consul: Error fetching health state. %v", err) time.Sleep(time.Second) continue } - log.Printf("[DEBUG] consul: Health changed to #%d", meta.LastIndex) // determine which services have passing health checks diff --git a/route/glob_cache.go b/route/glob_cache.go new file mode 100644 index 000000000..2e515053a --- /dev/null +++ b/route/glob_cache.go @@ -0,0 +1,65 @@ +package route + +import ( + "github.com/gobwas/glob" + "sync" +) + +// GlobCache implements an LRU cache for compiled glob patterns. +type GlobCache struct { + // m maps patterns to compiled glob matchers. + m sync.Map + + // l contains the added patterns and serves as an LRU cache. + // l has a fixed size and is initialized in the constructor. + l []string + + // h is the first element in l. + h int + + // n is the number of elements in l. + n int +} + +func NewGlobCache(size int) *GlobCache { + return &GlobCache{ + l: make([]string, size), + } +} + +// Get returns the compiled glob pattern if it compiled without +// error. Otherwise, the function returns nil. If the pattern +// is not in the cache it will be added. +func (c *GlobCache) Get(pattern string) (glob.Glob, error) { + // fast path + if glb, ok := c.m.Load(pattern); ok { + //Type Assert the returned interface{} + return glb.(glob.Glob), nil + } + + // try to compile pattern + glbCompiled, err := glob.Compile(pattern) + if err != nil { + return nil, err + } + + // if the LRU buffer is not full just append + // the element to the buffer. + if c.n < len(c.l) { + c.m.Store(pattern, glbCompiled) + c.l[c.n] = pattern + c.n++ + return glbCompiled, nil + } + + // otherwise, remove the oldest element and move + // the head. Note that once the buffer is full + // (c.n == len(c.l)) it will never become smaller + // again. + // TODO add logging for cache full - How will this impact performance + c.m.Delete(c.l[c.h]) + c.m.Store(pattern, glbCompiled) + c.l[c.h] = pattern + c.h = (c.h + 1) % c.n + return glbCompiled, nil +} diff --git a/route/glob_cache_test.go b/route/glob_cache_test.go new file mode 100644 index 000000000..cdb57a6b6 --- /dev/null +++ b/route/glob_cache_test.go @@ -0,0 +1,73 @@ +package route + +import ( + "reflect" + "sort" + "testing" +) + +func TestGlobCache(t *testing.T) { + c := NewGlobCache(3) + + keys := func() []string { + var kk []string + c.m.Range(func(k, v interface{}) bool { + kk = append(kk, k.(string)) + return true + }) + sort.Strings(kk) + return kk + } + + c.Get("a") + // TODO add back in when sync.Map supports Len function + // TODO https://github.com/golang/go/issues/20680 + //if got, want := len(c.m), 1; got != want { + // t.Fatalf("got len %d want %d", got, want) + //} + if got, want := keys(), []string{"a"}; !reflect.DeepEqual(got, want) { + t.Fatalf("got %v want %v", got, want) + } + if got, want := c.l, []string{"a", "", ""}; !reflect.DeepEqual(got, want) { + t.Fatalf("got %v want %v", got, want) + } + + c.Get("b") + // TODO add back in when sync.Map supports Len function + // TODO https://github.com/golang/go/issues/20680 + //if got, want := len(c.m), 2; got != want { + // t.Fatalf("got len %d want %d", got, want) + //} + if got, want := keys(), []string{"a", "b"}; !reflect.DeepEqual(got, want) { + t.Fatalf("got %v want %v", got, want) + } + if got, want := c.l, []string{"a", "b", ""}; !reflect.DeepEqual(got, want) { + t.Fatalf("got %v want %v", got, want) + } + + c.Get("c") + // TODO add back in when sync.Map supports Len function + // TODO https://github.com/golang/go/issues/20680 + //if got, want := len(c.m), 3; got != want { + // t.Fatalf("got len %d want %d", got, want) + //} + if got, want := keys(), []string{"a", "b", "c"}; !reflect.DeepEqual(got, want) { + t.Fatalf("got %v want %v", got, want) + } + if got, want := c.l, []string{"a", "b", "c"}; !reflect.DeepEqual(got, want) { + t.Fatalf("got %v want %v", got, want) + } + + c.Get("d") + // TODO add back in when sync.Map supports Len function + // TODO https://github.com/golang/go/issues/20680 + //if got, want := len(c.m), 3; got != want { + // t.Fatalf("got len %d want %d", got, want) + //} + if got, want := keys(), []string{"b", "c", "d"}; !reflect.DeepEqual(got, want) { + t.Fatalf("got %v want %v", got, want) + } + if got, want := c.l, []string{"d", "b", "c"}; !reflect.DeepEqual(got, want) { + t.Fatalf("got %v want %v", got, want) + } +} diff --git a/route/issue57_test.go b/route/issue57_test.go index 364a95f4e..0262b0d66 100644 --- a/route/issue57_test.go +++ b/route/issue57_test.go @@ -33,7 +33,7 @@ func TestIssue57(t *testing.T) { if err != nil { t.Fatalf("%d: got %v want nil", i, err) } - target := tbl.Lookup(req, "", rrPicker, prefixMatcher, globEnabled) + target := tbl.Lookup(req, "", rrPicker, prefixMatcher, globCache, globEnabled) if target == nil { t.Fatalf("%d: got %v want %v", i, target, want) } diff --git a/route/route_bench_test.go b/route/route_bench_test.go index 343246bee..284ecb82c 100644 --- a/route/route_bench_test.go +++ b/route/route_bench_test.go @@ -126,7 +126,7 @@ func benchmarkGet(t Table, match matcher, pick picker, pb *testing.PB) { k, n := len(reqs), 0 //Glob Matching True for pb.Next() { - t.Lookup(reqs[n%k], "", pick, match, globEnabled) + t.Lookup(reqs[n%k], "", pick, match, globCache, globEnabled) n++ } } diff --git a/route/table.go b/route/table.go index 040345612..e81da21ab 100644 --- a/route/table.go +++ b/route/table.go @@ -320,13 +320,20 @@ func normalizeHostNoLower(host string, tls bool) string { // matchingHosts returns all keys (host name patterns) from the // routing table which match the normalized request hostname. -func (t Table) matchingHosts(req *http.Request) (hosts []string) { +func (t Table) matchingHosts(req *http.Request, globCache *GlobCache) (hosts []string) { host := normalizeHost(req.Host, req.TLS != nil) for pattern := range t { normpat := normalizeHost(pattern, req.TLS != nil) - // TODO setup compiled GLOBs in a separate MAP - // TODO Issue #548 - g := glob.MustCompile(normpat) + + // Issue 548 + // + //Get Compiled Glob from LRU cache + g, err := globCache.Get(normpat) + if err != nil { + log.Print("[Error] Compiling glob - ", err) + g = glob.MustCompile(normpat) + } + if g.Match(host) { hosts = append(hosts, pattern) } @@ -396,7 +403,7 @@ func ReverseHostPort(s string) string { // or nil if there is none. It first checks the routes for the host // and if none matches then it falls back to generic routes without // a host. This is useful for a catch-all '/' rule. -func (t Table) Lookup(req *http.Request, trace string, pick picker, match matcher, globDisabled bool) (target *Target) { +func (t Table) Lookup(req *http.Request, trace string, pick picker, match matcher, globCache *GlobCache, globDisabled bool) (target *Target) { var hosts []string if trace != "" { @@ -413,7 +420,7 @@ func (t Table) Lookup(req *http.Request, trace string, pick picker, match matche if globDisabled { hosts = t.matchingHostNoGlob(req) } else { - hosts = t.matchingHosts(req) + hosts = t.matchingHosts(req, globCache) } if trace != "" { diff --git a/route/table_test.go b/route/table_test.go index c5ca2e246..934581460 100644 --- a/route/table_test.go +++ b/route/table_test.go @@ -19,6 +19,9 @@ const ( globDisabled = true ) +//Global GlobCache for Testing +var globCache = NewGlobCache(1000) + func TestTableParse(t *testing.T) { genRoutes := func(n int, format string) (a []string) { for i := 0; i < n; i++ { @@ -560,7 +563,7 @@ func TestTableLookupIssue448(t *testing.T) { } for i, tt := range tests { - if got, want := tbl.Lookup(tt.req, "", rndPicker, prefixMatcher, globEnabled).URL.String(), tt.dst; got != want { + if got, want := tbl.Lookup(tt.req, "", rndPicker, prefixMatcher, globCache, globEnabled).URL.String(), tt.dst; got != want { t.Errorf("%d: got %v want %v", i, got, want) } } @@ -636,7 +639,7 @@ func TestTableLookup(t *testing.T) { } for i, tt := range tests { - if got, want := tbl.Lookup(tt.req, "", rndPicker, prefixMatcher, tt.globEnabled).URL.String(), tt.dst; got != want { + if got, want := tbl.Lookup(tt.req, "", rndPicker, prefixMatcher, globCache, globEnabled).URL.String(), tt.dst; got != want { t.Errorf("%d: got %v want %v", i, got, want) } } @@ -655,7 +658,7 @@ func TestTableLookup_656(t *testing.T) { } req := httptest.NewRequest("GET", "http://example.com/foo", nil) - target := tbl.Lookup(req, "redirect", rrPicker, prefixMatcher, false) + target := tbl.Lookup(req, "redirect", rrPicker, prefixMatcher, globCache, globDisabled) if target == nil { t.Fatal("No route match")