Allow to tap multiple function services at one time (#1434)

The router taps function service one by one which is inefficient and
increases the burden of executor. This PR aggregates all requests into
one to solve the problem mentioned above.
This commit is contained in:
Ta-Ching Chen
2019-11-25 18:44:48 +08:00
committed by GitHub
parent ccc551112b
commit 6d2fe08973
7 changed files with 169 additions and 91 deletions
+45 -1
View File
@@ -20,6 +20,8 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"io/ioutil"
"net/http"
"strings"
@@ -32,6 +34,7 @@ import (
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
ferror "github.com/fission/fission/pkg/error"
"github.com/fission/fission/pkg/executor/client"
)
func (executor *Executor) getServiceForFunctionApi(w http.ResponseWriter, r *http.Request) {
@@ -115,6 +118,7 @@ func (executor *Executor) getServiceForFunction(fn *fv1.Function) (string, error
}
// find funcSvc and update its atime
// TODO: Deprecated tapService
func (executor *Executor) tapService(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
@@ -137,6 +141,45 @@ func (executor *Executor) tapService(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
// find funcSvc and update its atime
func (executor *Executor) tapServices(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
executor.logger.Error("failed to read tap service request", zap.Error(err))
http.Error(w, "Failed to read request", http.StatusInternalServerError)
return
}
tapSvcReqs := []client.TapServiceRequest{}
err = json.Unmarshal(body, &tapSvcReqs)
if err != nil {
executor.logger.Error("failed to decode tap service request",
zap.Error(err),
zap.String("request-payload", string(body)))
http.Error(w, "Failed to decode tap service request", http.StatusBadRequest)
return
}
errs := &multierror.Error{}
for _, req := range tapSvcReqs {
svcHost := strings.TrimPrefix(req.ServiceUrl, "http://")
err = executor.fsCache.TouchByAddress(svcHost)
if err != nil {
errs = multierror.Append(errs,
errors.Wrapf(err, "'%v' failed to tap function '%v/%v' with service url '%v'",
req.FnMetadata.Namespace, req.FnMetadata.Name, req.ServiceUrl, req.FnExecutorType))
}
}
if errs.ErrorOrNil() != nil {
executor.logger.Error("error tapping function service", zap.Error(errs))
http.Error(w, "Not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
}
func (executor *Executor) healthHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
@@ -144,7 +187,8 @@ func (executor *Executor) healthHandler(w http.ResponseWriter, r *http.Request)
func (executor *Executor) GetHandler() http.Handler {
r := mux.NewRouter()
r.HandleFunc("/v2/getServiceForFunction", executor.getServiceForFunctionApi).Methods("POST")
r.HandleFunc("/v2/tapService", executor.tapService).Methods("POST")
r.HandleFunc("/v2/tapService", executor.tapService).Methods("POST") // for backward compatibility
r.HandleFunc("/v2/tapServices", executor.tapServices).Methods("POST")
r.HandleFunc("/healthz", executor.healthHandler).Methods("GET")
return r
}
+61 -30
View File
@@ -26,29 +26,36 @@ import (
"strings"
"time"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
ferror "github.com/fission/fission/pkg/error"
"github.com/pkg/errors"
"go.opencensus.io/plugin/ochttp"
"go.uber.org/zap"
"golang.org/x/net/context/ctxhttp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ferror "github.com/fission/fission/pkg/error"
)
type Client struct {
logger *zap.Logger
executorUrl string
tappedByUrl map[string]bool
requestChan chan string
httpClient *http.Client
}
type (
Client struct {
logger *zap.Logger
executorUrl string
tappedByUrl map[string]TapServiceRequest
requestChan chan TapServiceRequest
httpClient *http.Client
}
TapServiceRequest struct {
FnMetadata metav1.ObjectMeta
FnExecutorType fv1.ExecutorType
ServiceUrl string
}
)
func MakeClient(logger *zap.Logger, executorUrl string) *Client {
c := &Client{
logger: logger.Named("executor_client"),
executorUrl: strings.TrimSuffix(executorUrl, "/"),
tappedByUrl: make(map[string]bool),
requestChan: make(chan string),
tappedByUrl: make(map[string]TapServiceRequest),
requestChan: make(chan TapServiceRequest),
httpClient: &http.Client{
Transport: &ochttp.Transport{},
},
@@ -87,40 +94,64 @@ func (c *Client) service() {
ticker := time.NewTicker(time.Second * 5)
for {
select {
case serviceUrl := <-c.requestChan:
c.tappedByUrl[serviceUrl] = true
case svcReq := <-c.requestChan:
c.tappedByUrl[svcReq.ServiceUrl] = svcReq
case <-ticker.C:
urls := c.tappedByUrl
c.tappedByUrl = make(map[string]bool)
if len(urls) > 0 {
go func() {
for u := range urls {
err := c._tapService(u)
if err != nil {
c.logger.Error("error tapping function service address", zap.Error(err), zap.String("address", u))
}
}
c.logger.Debug("tapped services in batch", zap.Int("service_count", len(urls)))
}()
if len(c.tappedByUrl) == 0 {
continue
}
urls := c.tappedByUrl
c.tappedByUrl = make(map[string]TapServiceRequest)
go func() {
svcReqs := []TapServiceRequest{}
for _, req := range urls {
svcReqs = append(svcReqs, req)
}
c.logger.Debug("tapped services in batch", zap.Int("service_count", len(urls)))
err := c._tapService(svcReqs)
if err != nil {
c.logger.Error("error tapping function service address", zap.Error(err))
}
}()
}
}
}
func (c *Client) TapService(serviceUrl *url.URL) {
c.requestChan <- serviceUrl.String()
func (c *Client) TapService(fnMeta metav1.ObjectMeta, executorType fv1.ExecutorType, serviceUrl *url.URL) {
c.requestChan <- TapServiceRequest{
FnMetadata: metav1.ObjectMeta{
Name: fnMeta.Name,
Namespace: fnMeta.Namespace,
ResourceVersion: fnMeta.ResourceVersion,
UID: fnMeta.UID,
},
FnExecutorType: executorType,
// service url is for executor to know which
// pod/service is currently used to serve user function.
ServiceUrl: serviceUrl.String(),
}
}
func (c *Client) _tapService(serviceUrlStr string) error {
executorUrl := c.executorUrl + "/v2/tapService"
func (c *Client) _tapService(tapSvcReqs []TapServiceRequest) error {
executorUrl := c.executorUrl + "/v2/tapServices"
resp, err := http.Post(executorUrl, "application/octet-stream", bytes.NewReader([]byte(serviceUrlStr)))
body, err := json.Marshal(tapSvcReqs)
if err != nil {
return err
}
resp, err := http.Post(executorUrl, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return ferror.MakeErrorFromHTTP(resp)
}
return nil
}
+28 -28
View File
@@ -54,9 +54,9 @@ type (
logger *zap.Logger
fmap *functionServiceMap
executor *executorClient.Client
function *metav1.ObjectMeta
function *fv1.Function
httpTrigger *fv1.HTTPTrigger
functionMetadataMap map[string]*metav1.ObjectMeta
functionMap map[string]*fv1.Function
fnWeightDistributionList []FunctionWeightDistribution
tsRoundTripperParams *tsRoundTripperParams
isDebugEnv bool
@@ -150,7 +150,7 @@ func (roundTripper *RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Re
// Set forwarded host header if not exists
roundTripper.addForwardedHostHeader(req)
fnMeta := roundTripper.funcHandler.function
fnMeta := &roundTripper.funcHandler.function.Metadata
// Metrics stuff
startTime := time.Now()
@@ -239,7 +239,7 @@ func (roundTripper *RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Re
// tapService before invoking roundTrip for the serviceUrl
if serviceUrlFromCache {
go roundTripper.funcHandler.tapService(serviceUrl)
go roundTripper.funcHandler.tapService(roundTripper.funcHandler.function, serviceUrl)
}
// modify the request to reflect the service url
@@ -388,25 +388,25 @@ func (roundTripper *RetryingRoundTripper) closeContext() {
}
}
func (fh *functionHandler) tapService(serviceUrl *url.URL) {
func (fh *functionHandler) tapService(fn *fv1.Function, serviceUrl *url.URL) {
if fh.executor == nil {
return
}
fh.executor.TapService(serviceUrl)
fh.executor.TapService(fn.Metadata, fn.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType, serviceUrl)
}
func (fh functionHandler) handler(responseWriter http.ResponseWriter, request *http.Request) {
if fh.httpTrigger != nil && fh.httpTrigger.Spec.FunctionReference.Type == types.FunctionReferenceTypeFunctionWeights {
// canary deployment. need to determine the function to send request to now
fnMetadata := getCanaryBackend(fh.functionMetadataMap, fh.fnWeightDistributionList)
if fnMetadata == nil {
fn := getCanaryBackend(fh.functionMap, fh.fnWeightDistributionList)
if fn == nil {
fh.logger.Error("could not get canary backend",
zap.Any("metadataMap", fh.functionMetadataMap),
zap.Any("fnMap", fh.functionMap),
zap.Any("distributionList", fh.fnWeightDistributionList))
// TODO : write error to responseWrite and return response
return
}
fh.function = fnMetadata
fh.function = fn
fh.logger.Debug("chosen function backend's metadata", zap.Any("metadata", fh.function))
}
@@ -414,7 +414,7 @@ func (fh functionHandler) handler(responseWriter http.ResponseWriter, request *h
setPathInfoToHeader(request)
// system params
setFunctionMetadataToHeader(fh.function, request)
setFunctionMetadataToHeader(&fh.function.Metadata, request)
director := func(req *http.Request) {
if _, ok := req.Header["User-Agent"]; !ok {
@@ -423,7 +423,7 @@ func (fh functionHandler) handler(responseWriter http.ResponseWriter, request *h
}
}
fnTimeout := fh.functionTimeoutMap[fh.function.GetUID()]
fnTimeout := fh.functionTimeoutMap[fh.function.Metadata.GetUID()]
if fnTimeout == 0 {
fnTimeout = fv1.DEFAULT_FUNCTION_TIMEOUT
}
@@ -437,7 +437,7 @@ func (fh functionHandler) handler(responseWriter http.ResponseWriter, request *h
proxy := &httputil.ReverseProxy{
Director: director,
Transport: rrt,
ErrorHandler: getProxyErrorHandler(fh.logger, fh.function),
ErrorHandler: getProxyErrorHandler(fh.logger, &fh.function.Metadata),
}
defer func() {
@@ -483,12 +483,10 @@ func findCeil(randomNumber int, wtDistrList []FunctionWeightDistribution) string
}
// picks a function to route to based on a random number generated
func getCanaryBackend(fnMetadatamap map[string]*metav1.ObjectMeta, fnWtDistributionList []FunctionWeightDistribution) *metav1.ObjectMeta {
func getCanaryBackend(fnMap map[string]*fv1.Function, fnWtDistributionList []FunctionWeightDistribution) *fv1.Function {
randomNumber := rand.Intn(fnWtDistributionList[len(fnWtDistributionList)-1].sumPrefix + 1)
fnName := findCeil(randomNumber, fnWtDistributionList)
return fnMetadatamap[fnName]
return fnMap[fnName]
}
// getProxyErrorHandler returns a reverse proxy error handler
@@ -574,28 +572,30 @@ func (fh *functionHandler) getServiceEntry() (serviceUrl *url.URL, serviceUrlFro
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
fnMeta := &fh.function.Metadata
// Use throttle to limit the total amount of requests sent
// to the executor to prevent it from overloaded.
recordObj, err := fh.svcAddrUpdateThrottler.RunOnce(
crd.CacheKey(fh.function),
crd.CacheKey(fnMeta),
func(firstToTheLock bool) (interface{}, error) {
var u *url.URL
// Get service entry from executor and update cache if its the first goroutine
if firstToTheLock { // first to the service url
fh.logger.Debug("calling getServiceForFunction",
zap.String("function_name", fh.function.Name))
zap.String("function_name", fnMeta.Name))
u, err = fh.getServiceEntryFromExecutor(ctx)
if err != nil {
fh.logger.Error("error getting service url from executor",
zap.Error(err),
zap.String("function_name", fh.function.Name))
zap.String("function_name", fnMeta.Name))
return nil, err
}
// add the address in router's cache
fh.logger.Info("assigning service url for function",
zap.String("url", u.String()),
zap.String("function_name", fh.function.Name))
fh.fmap.assign(fh.function, u)
zap.String("function_name", fnMeta.Name))
fh.fmap.assign(fnMeta, u)
} else {
u, err = fh.getServiceEntryFromCache()
if err != nil {
@@ -613,9 +613,9 @@ func (fh *functionHandler) getServiceEntry() (serviceUrl *url.URL, serviceUrlFro
e := "error updating service address entry for function"
fh.logger.Error(e,
zap.Error(err),
zap.String("function_name", fh.function.Name),
zap.String("function_namespace", fh.function.Namespace))
return nil, false, errors.Wrapf(err, "%s %s_%s", e, fh.function.Name, fh.function.Namespace)
zap.String("function_name", fnMeta.Name),
zap.String("function_namespace", fnMeta.Namespace))
return nil, false, errors.Wrapf(err, "%s %s_%s", e, fnMeta.Name, fnMeta.Namespace)
}
record, ok := recordObj.(svcEntryRecord)
@@ -629,7 +629,7 @@ func (fh *functionHandler) getServiceEntry() (serviceUrl *url.URL, serviceUrlFro
// getServiceEntryFromCache returns service url entry returns from cache
func (fh *functionHandler) getServiceEntryFromCache() (serviceUrl *url.URL, err error) {
// cache lookup to get serviceUrl
serviceUrl, err = fh.fmap.lookup(fh.function)
serviceUrl, err = fh.fmap.lookup(&fh.function.Metadata)
if err != nil {
var errMsg string
@@ -642,7 +642,7 @@ func (fh *functionHandler) getServiceEntryFromCache() (serviceUrl *url.URL, err
if e.Code == ferror.ErrorNotFound {
return nil, nil
}
errMsg = fmt.Sprintf("Error getting function %v;s service entry from cache: %v", fh.function.Name, err)
errMsg = fmt.Sprintf("Error getting function %v;s service entry from cache: %v", fh.function.Metadata.Name, err)
}
return nil, ferror.MakeError(http.StatusInternalServerError, errMsg)
}
@@ -652,7 +652,7 @@ func (fh *functionHandler) getServiceEntryFromCache() (serviceUrl *url.URL, err
// getServiceEntryFromExecutor returns service url entry returns from executor
func (fh *functionHandler) getServiceEntryFromExecutor(ctx context.Context) (*url.URL, error) {
// send a request to executor to specialize a new pod
service, err := fh.executor.GetServiceForFunction(ctx, fh.function)
service, err := fh.executor.GetServiceForFunction(ctx, &fh.function.Metadata)
if err != nil {
statusCode, errMsg := ferror.GetHTTPError(err)
fh.logger.Error("error from GetServiceForFunction",
+7 -5
View File
@@ -57,12 +57,12 @@ func TestFunctionProxying(t *testing.T) {
backendURL := createBackendService(testResponseString)
log.Printf("Created backend svc at %v", backendURL)
fn := &metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault}
fnMeta := metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault}
logger, err := zap.NewDevelopment()
panicIf(err)
fmap := makeFunctionServiceMap(logger, 0)
fmap.assign(fn, backendURL)
fmap.assign(&fnMeta, backendURL)
httpTrigger := &fv1.HTTPTrigger{
Metadata: metav1.ObjectMeta{
@@ -78,9 +78,11 @@ func TestFunctionProxying(t *testing.T) {
}
fh := &functionHandler{
logger: logger,
fmap: fmap,
function: fn,
logger: logger,
fmap: fmap,
function: &fv1.Function{
Metadata: metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault},
},
tsRoundTripperParams: &tsRoundTripperParams{
timeout: 50 * time.Millisecond,
timeoutExponent: 2,
+9 -9
View File
@@ -50,7 +50,7 @@ type (
// a distribution of requests across two functions.
resolveResult struct {
resolveResultType
functionMetadataMap map[string]*metav1.ObjectMeta
functionMap map[string]*fv1.Function
functionWtDistributionList []FunctionWeightDistribution
}
@@ -134,12 +134,13 @@ func (frr *functionReferenceResolver) resolveByName(namespace, name string) (*re
}
f := obj.(*fv1.Function)
functionMetadataMap := make(map[string]*metav1.ObjectMeta, 1)
functionMetadataMap[f.Metadata.Name] = &f.Metadata
functionMap := map[string]*fv1.Function{
f.Metadata.Name: f,
}
rr := resolveResult{
resolveResultType: resolveResultSingleFunction,
functionMetadataMap: functionMetadataMap,
resolveResultType: resolveResultSingleFunction,
functionMap: functionMap,
}
return &rr, nil
@@ -147,7 +148,7 @@ func (frr *functionReferenceResolver) resolveByName(namespace, name string) (*re
func (frr *functionReferenceResolver) resolveByFunctionWeights(namespace string, fr *fv1.FunctionReference) (*resolveResult, error) {
functionMetadataMap := make(map[string]*metav1.ObjectMeta)
functionMap := make(map[string]*fv1.Function)
fnWtDistrList := make([]FunctionWeightDistribution, 0)
sumPrefix := 0
@@ -167,19 +168,18 @@ func (frr *functionReferenceResolver) resolveByFunctionWeights(namespace string,
}
f := obj.(*fv1.Function)
functionMetadataMap[f.Metadata.Name] = &f.Metadata
functionMap[f.Metadata.Name] = f
sumPrefix = sumPrefix + functionWeight
fnWtDistrList = append(fnWtDistrList, FunctionWeightDistribution{
name: functionName,
weight: functionWeight,
sumPrefix: sumPrefix,
})
}
rr := resolveResult{
resolveResultType: resolveResultMultipleFunctions,
functionMetadataMap: functionMetadataMap,
functionMap: functionMap,
functionWtDistributionList: fnWtDistrList,
}
+10 -11
View File
@@ -142,7 +142,7 @@ func (ts *HTTPTriggerSet) getRouter(fnTimeoutMap map[types.UID]int) *mux.Router
fmap: ts.functionServiceMap,
executor: ts.executor,
httpTrigger: &trigger,
functionMetadataMap: rr.functionMetadataMap,
functionMap: rr.functionMap,
fnWeightDistributionList: rr.functionWtDistributionList,
tsRoundTripperParams: ts.tsRoundTripperParams,
isDebugEnv: ts.isDebugEnv,
@@ -158,8 +158,8 @@ func (ts *HTTPTriggerSet) getRouter(fnTimeoutMap map[types.UID]int) *mux.Router
// deployment. For more details, please check "handler" function of functionHandler.
if rr.resolveResultType == resolveResultSingleFunction {
for _, metadata := range fh.functionMetadataMap {
fh.function = metadata
for _, fn := range fh.functionMap {
fh.function = fn
}
}
@@ -185,20 +185,19 @@ func (ts *HTTPTriggerSet) getRouter(fnTimeoutMap map[types.UID]int) *mux.Router
// Internal triggers for each function by name. Non-http
// triggers route into these.
for _, function := range ts.functions {
m := function.Metadata
for i := range ts.functions {
fn := ts.functions[i]
fh := &functionHandler{
logger: ts.logger.Named(m.Name),
logger: ts.logger.Named(fn.Metadata.Name),
fmap: ts.functionServiceMap,
function: &m,
function: &fn,
executor: ts.executor,
tsRoundTripperParams: ts.tsRoundTripperParams,
isDebugEnv: ts.isDebugEnv,
svcAddrUpdateThrottler: ts.svcAddrUpdateThrottler,
functionTimeoutMap: fnTimeoutMap,
}
muxRouter.HandleFunc(utils.UrlForFunction(function.Metadata.Name, function.Metadata.Namespace), fh.handler)
muxRouter.HandleFunc(utils.UrlForFunction(fn.Metadata.Name, fn.Metadata.Namespace), fh.handler)
}
// Healthz endpoint for the router.
@@ -263,8 +262,8 @@ func (ts *HTTPTriggerSet) initFunctionController() (k8sCache.Store, k8sCache.Con
// update resolver function reference cache
for key, rr := range ts.resolver.copy() {
if key.namespace == fn.Metadata.Namespace &&
rr.functionMetadataMap[fn.Metadata.Name] != nil &&
rr.functionMetadataMap[fn.Metadata.Name].ResourceVersion != fn.Metadata.ResourceVersion {
rr.functionMap[fn.Metadata.Name] != nil &&
rr.functionMap[fn.Metadata.Name].Metadata.ResourceVersion != fn.Metadata.ResourceVersion {
// invalidate resolver cache
ts.logger.Debug("invalidating resolver cache")
err := ts.resolver.delete(key.namespace, key.triggerName, key.triggerResourceVersion)
+9 -7
View File
@@ -32,12 +32,12 @@ import (
func TestRouter(t *testing.T) {
// metadata for a fake function
fn := &metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault}
fnMeta := metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault}
// and a reference to it
fr := fv1.FunctionReference{
Type: types.FunctionReferenceTypeFunctionName,
Name: fn.Name,
Name: fnMeta.Name,
}
// start a fake service
@@ -49,7 +49,7 @@ func TestRouter(t *testing.T) {
// set up the cache with this fake service
fmap := makeFunctionServiceMap(logger, 0)
fmap.assign(fn, testServiceUrl)
fmap.assign(&fnMeta, testServiceUrl)
// HTTP trigger set with a trigger for this function
triggers, _, _ := makeHTTPTriggerSet(logger, fmap, nil, nil, nil, nil,
@@ -81,12 +81,14 @@ func TestRouter(t *testing.T) {
triggerResourceVersion: "1234",
}
fnMetaMap := make(map[string]*metav1.ObjectMeta, 1)
fnMetaMap[fn.Name] = fn
fnMetaMap := make(map[string]*fv1.Function, 1)
fnMetaMap[fnMeta.Name] = &fv1.Function{
Metadata: fnMeta,
}
rr := resolveResult{
resolveResultType: resolveResultSingleFunction,
functionMetadataMap: fnMetaMap,
resolveResultType: resolveResultSingleFunction,
functionMap: fnMetaMap,
}
frr.refCache.Set(nfr, rr)