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) <alexellis2@gmail.com>
This commit is contained in:
Alex Ellis 2018-03-21 10:28:50 +00:00
parent a841e3d7f3
commit 0c7e59fe8a
9 changed files with 172 additions and 26 deletions

View File

@ -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

View File

@ -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

View File

@ -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 |

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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) {

View File

@ -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