From 0c7e59fe8a74d22c37500a84952d12ef6f4b57dd Mon Sep 17 00:00:00 2001 From: Alex Ellis Date: Wed, 21 Mar 2018 10:28:50 +0000 Subject: [PATCH] Add direct_functions mode to gateway for tuning Adds a pair of configuration options for performance tuning. The gateway can now invoke functions directly and can bypass the provider. See updated table in README.md for configuration values. BaseURLResolver is added with unit tests that decouples resolving upstream URL from the reverse proxy client code. - SingleHostBaseURLResolver resolves a single upstream host - FunctionAsHostBaseURLResolver resolves host based upon conventions within the URL of the request to a function for direct access Tested with Kubernetes (faas-netes) and faas-swarm through UI, CLI calling system endpoints and functions directly. Signed-off-by: Alex Ellis (VMware) --- docker-compose.yml | 2 + gateway/Dockerfile | 5 +- gateway/README.md | 12 +++-- gateway/handlers/baseurlresolver_test.go | 55 ++++++++++++++++++++ gateway/handlers/forwarding_proxy.go | 62 ++++++++++++++++++----- gateway/handlers/{proxy.go => metrics.go} | 0 gateway/server.go | 23 ++++++--- gateway/tests/config_test.go | 30 +++++++++++ gateway/types/readconfig.go | 9 ++++ 9 files changed, 172 insertions(+), 26 deletions(-) create mode 100644 gateway/handlers/baseurlresolver_test.go rename gateway/handlers/{proxy.go => metrics.go} (100%) diff --git a/docker-compose.yml b/docker-compose.yml index 6ff92336..0b290ded 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,8 @@ services: dnsrr: "true" # Temporarily use dnsrr in place of VIP while issue persists on PWD faas_nats_address: "nats" faas_nats_port: 4222 + direct_functions: "true" # Functions are invoked directly over the overlay network + direct_functions_suffix: "" deploy: resources: # limits: # Enable if you want to limit memory usage diff --git a/gateway/Dockerfile b/gateway/Dockerfile index 80fe3fc1..e7269dad 100644 --- a/gateway/Dockerfile +++ b/gateway/Dockerfile @@ -1,10 +1,11 @@ FROM golang:1.9.4 as build -WORKDIR /go/src/github.com/openfaas/faas/gateway - RUN curl -sL https://github.com/alexellis/license-check/releases/download/0.2.2/license-check \ > /usr/bin/license-check \ && chmod +x /usr/bin/license-check +WORKDIR /go/src/github.com/openfaas/faas/gateway + + COPY vendor vendor COPY handlers handlers diff --git a/gateway/README.md b/gateway/README.md index 999d5cec..91917b94 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -9,7 +9,7 @@ In summary: * UI built-in * Deploy your own functions or from the Function Store * Instrumentation via Prometheus -* Autoscaling via AlertManager +* Auto-scaling via AlertManager * REST API available ![](https://raw.githubusercontent.com/openfaas/faas/master/docs/of-overview.png) @@ -48,7 +48,9 @@ The gateway can be configured through the following environment variables: | `write_timeout` | HTTP timeout for writing a response body from your function (in seconds). Default: `8` | | `read_timeout` | HTTP timeout for reading the payload from the client caller (in seconds). Default: `8` | | `functions_provider_url` | URL of upstream [functions provider](https://github.com/openfaas/faas-provider/) - i.e. Swarm, Kubernetes, Nomad etc | -| `faas_nats_address` | Address of NATS service. Required for asynchronous mode. | -| `faas_nats_port` | Port for NATS service. Requrired for asynchronous mode. | -| `faas_prometheus_host` | Host to connect to Prometheus. Default: `"prometheus"`. | -| `faas_promethus_port` | Port to connect to Prometheus. Default: `9090`. | +| `faas_nats_address` | Address of NATS service. Required for asynchronous mode | +| `faas_nats_port` | Port for NATS service. Requrired for asynchronous mode | +| `faas_prometheus_host` | Host to connect to Prometheus. Default: `"prometheus"` | +| `faas_promethus_port` | Port to connect to Prometheus. Default: `9090` | +| `direct_functions` | `true` or `false` - functions are invoked directly over overlay network without passing through provider | +| `direct_functions_suffix` | Provide a DNS suffix for invoking functions directly over overlay network | diff --git a/gateway/handlers/baseurlresolver_test.go b/gateway/handlers/baseurlresolver_test.go new file mode 100644 index 00000000..a69ab74d --- /dev/null +++ b/gateway/handlers/baseurlresolver_test.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/url" + "testing" +) + +func TestSingleHostBaseURLResolver(t *testing.T) { + + urlVal, _ := url.Parse("http://upstream:8080/") + r := SingleHostBaseURLResolver{BaseURL: urlVal.String()} + + req, _ := http.NewRequest(http.MethodGet, "http://localhost/function/hello", nil) + + resolved := r.Resolve(req) + want := "http://upstream:8080" + if resolved != want { + t.Logf("r.Resolve failed, want: %s got: %s", want, resolved) + t.Fail() + } +} + +const watchdogPort = 8080 + +func TestFunctionAsHostBaseURLResolver_WithSuffix(t *testing.T) { + suffix := "openfaas-fn.local.cluster.svc." + r := FunctionAsHostBaseURLResolver{FunctionSuffix: suffix} + + req, _ := http.NewRequest(http.MethodGet, "http://localhost/function/hello", nil) + + resolved := r.Resolve(req) + want := fmt.Sprintf("http://hello.%s:%d", suffix, watchdogPort) + + if resolved != want { + t.Logf("r.Resolve failed, want: %s got: %s", want, resolved) + t.Fail() + } +} + +func TestFunctionAsHostBaseURLResolver_WithoutSuffix(t *testing.T) { + suffix := "" + r := FunctionAsHostBaseURLResolver{FunctionSuffix: suffix} + + req, _ := http.NewRequest(http.MethodGet, "http://localhost/function/hello", nil) + + resolved := r.Resolve(req) + want := fmt.Sprintf("http://hello%s:%d", suffix, watchdogPort) + + if resolved != want { + t.Logf("r.Resolve failed, want: %s got: %s", want, resolved) + t.Fail() + } +} diff --git a/gateway/handlers/forwarding_proxy.go b/gateway/handlers/forwarding_proxy.go index 8b85e60e..47af2ffa 100644 --- a/gateway/handlers/forwarding_proxy.go +++ b/gateway/handlers/forwarding_proxy.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "fmt" "io" "log" "net/http" @@ -14,20 +15,22 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +// HTTPNotifier notify about HTTP request/response type HTTPNotifier interface { Notify(method string, URL string, statusCode int, duration time.Duration) } +// BaseURLResolver URL resolver for upstream requests +type BaseURLResolver interface { + Resolve(r *http.Request) string +} + // MakeForwardingProxyHandler create a handler which forwards HTTP requests -func MakeForwardingProxyHandler(proxy *types.HTTPClientReverseProxy, notifiers []HTTPNotifier) http.HandlerFunc { - baseURL := proxy.BaseURL.String() - if strings.HasSuffix(baseURL, "/") { - baseURL = baseURL[0 : len(baseURL)-1] - } - +func MakeForwardingProxyHandler(proxy *types.HTTPClientReverseProxy, notifiers []HTTPNotifier, baseURLResolver BaseURLResolver) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + baseURL := baseURLResolver.Resolve(r) - requestURL := r.URL.String() + requestURL := r.URL.Path start := time.Now() @@ -90,10 +93,12 @@ func copyHeaders(destination http.Header, source *http.Header) { } } +// PrometheusFunctionNotifier records metrics to Prometheus type PrometheusFunctionNotifier struct { Metrics *metrics.MetricOptions } +// Notify records metrics in Prometheus func (p PrometheusFunctionNotifier) Notify(method string, URL string, statusCode int, duration time.Duration) { seconds := duration.Seconds() serviceName := getServiceName(URL) @@ -112,19 +117,52 @@ func (p PrometheusFunctionNotifier) Notify(method string, URL string, statusCode func getServiceName(urlValue string) string { var serviceName string forward := "/function/" - if startsWith(urlValue, forward) { + if strings.HasPrefix(urlValue, forward) { serviceName = urlValue[len(forward):] } return serviceName } -func startsWith(value, token string) bool { - return len(value) > len(token) && strings.Index(value, token) == 0 -} - +// LoggingNotifier notifies a log about a request type LoggingNotifier struct { } +// Notify a log about a request func (LoggingNotifier) Notify(method string, URL string, statusCode int, duration time.Duration) { log.Printf("Forwarded [%s] to %s - [%d] - %f seconds", method, URL, statusCode, duration.Seconds()) } + +// SingleHostBaseURLResolver resolves URLs against a single BaseURL +type SingleHostBaseURLResolver struct { + BaseURL string +} + +// Resolve the base URL for a request +func (s SingleHostBaseURLResolver) Resolve(r *http.Request) string { + + baseURL := s.BaseURL + + if strings.HasSuffix(baseURL, "/") { + baseURL = baseURL[0 : len(baseURL)-1] + } + return baseURL +} + +// FunctionAsHostBaseURLResolver resolves URLs using a function from the URL as a host +type FunctionAsHostBaseURLResolver struct { + FunctionSuffix string +} + +// Resolve the base URL for a request +func (f FunctionAsHostBaseURLResolver) Resolve(r *http.Request) string { + + svcName := getServiceName(r.URL.Path) + + const watchdogPort = 8080 + var suffix string + if len(f.FunctionSuffix) > 0 { + suffix = "." + f.FunctionSuffix + } + + return fmt.Sprintf("http://%s%s:%d", svcName, suffix, watchdogPort) +} diff --git a/gateway/handlers/proxy.go b/gateway/handlers/metrics.go similarity index 100% rename from gateway/handlers/proxy.go rename to gateway/handlers/metrics.go diff --git a/gateway/server.go b/gateway/server.go index 29b0a21c..112dac98 100644 --- a/gateway/server.go +++ b/gateway/server.go @@ -49,14 +49,23 @@ func main() { functionNotifiers := []handlers.HTTPNotifier{loggingNotifier, prometheusNotifier} forwardingNotifiers := []handlers.HTTPNotifier{loggingNotifier, prometheusNotifier} - faasHandlers.Proxy = handlers.MakeForwardingProxyHandler(reverseProxy, functionNotifiers) - faasHandlers.RoutelessProxy = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) - faasHandlers.ListFunctions = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) - faasHandlers.DeployFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) - faasHandlers.DeleteFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) - faasHandlers.UpdateFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) + urlResolver := handlers.SingleHostBaseURLResolver{BaseURL: config.FunctionsProviderURL.String()} + var functionURLResolver handlers.BaseURLResolver - queryFunction := handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) + if config.DirectFunctions { + functionURLResolver = handlers.FunctionAsHostBaseURLResolver{FunctionSuffix: config.DirectFunctionsSuffix} + } else { + functionURLResolver = urlResolver + } + + faasHandlers.Proxy = handlers.MakeForwardingProxyHandler(reverseProxy, functionNotifiers, functionURLResolver) + + faasHandlers.RoutelessProxy = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) + faasHandlers.ListFunctions = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) + faasHandlers.DeployFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) + faasHandlers.DeleteFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) + faasHandlers.UpdateFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) + queryFunction := handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) alertHandler := plugin.NewExternalServiceQuery(*config.FunctionsProviderURL) faasHandlers.Alert = handlers.MakeAlertHandler(alertHandler) diff --git a/gateway/tests/config_test.go b/gateway/tests/config_test.go index 0c9669a8..c5889556 100644 --- a/gateway/tests/config_test.go +++ b/gateway/tests/config_test.go @@ -38,6 +38,36 @@ func TestRead_UseExternalProvider_Defaults(t *testing.T) { t.Log("Default for UseExternalProvider should be false") t.Fail() } + + if config.DirectFunctions != false { + t.Log("Default for DirectFunctions should be false") + t.Fail() + } + + if len(config.DirectFunctionsSuffix) > 0 { + t.Log("Default for DirectFunctionsSuffix should be empty as a default") + t.Fail() + } +} + +func TestRead_DirectFunctionsOverride(t *testing.T) { + defaults := NewEnvBucket() + readConfig := types.ReadConfig{} + defaults.Setenv("direct_functions", "true") + wantSuffix := "openfaas-fn.cluster.local.svc." + defaults.Setenv("direct_functions_suffix", wantSuffix) + + config := readConfig.Read(defaults) + + if config.DirectFunctions != true { + t.Logf("DirectFunctions should be true, got: %v", config.DirectFunctions) + t.Fail() + } + + if config.DirectFunctionsSuffix != wantSuffix { + t.Logf("DirectFunctionsSuffix want: %s, got: %s", wantSuffix, config.DirectFunctionsSuffix) + t.Fail() + } } func TestRead_EmptyTimeoutConfig(t *testing.T) { diff --git a/gateway/types/readconfig.go b/gateway/types/readconfig.go index f52b8917..7eea2f1f 100644 --- a/gateway/types/readconfig.go +++ b/gateway/types/readconfig.go @@ -102,6 +102,9 @@ func (ReadConfig) Read(hasEnv HasEnv) GatewayConfig { cfg.PrometheusHost = prometheusHost } + cfg.DirectFunctions = parseBoolValue(hasEnv.Getenv("direct_functions")) + cfg.DirectFunctionsSuffix = hasEnv.Getenv("direct_functions_suffix") + return cfg } @@ -131,6 +134,12 @@ type GatewayConfig struct { // Port to connect to Prometheus. PrometheusPort int + + // If set to true we will access upstream functions directly rather than through the upstream provider + DirectFunctions bool + + // If set this will be used to resolve functions directly + DirectFunctionsSuffix string } // UseNATS Use NATSor not