Refactor caching in poolmgr

Move separate caches to one cache -- functionServiceCache.  It can be
looked up by function, can update atime by address, and can be deleted
by podname.  This removes the other caches.  Some of the concurrency
logic is still a bit hairy; it might be better not to use fission.Cache.
This commit is contained in:
Soam Vasani
2016-11-05 22:03:48 -07:00
parent 8491aaf7fc
commit 5650fca3fd
7 changed files with 340 additions and 177 deletions
+27 -44
View File
@@ -37,29 +37,29 @@ import (
type funcSvc struct { type funcSvc struct {
function *fission.Metadata // function this pod/service is for function *fission.Metadata // function this pod/service is for
environment *fission.Environment // env it was obtained from environment *fission.Environment // env it was obtained from
serviceName string // name of k8s svc address string // Host:Port or IP:Port that the service can be reached at.
podName string // pod name (within the function namespace)
reaped bool // if true, the pod has been deleted
ctime time.Time ctime time.Time
atime time.Time atime time.Time
} }
type API struct { type API struct {
poolMgr *GenericPoolManager poolMgr *GenericPoolManager
functionEnv *cache.Cache // map[fission.Metadata]fission.Environment functionEnv *cache.Cache // map[fission.Metadata]fission.Environment
functionService *cache.Cache // map[fission.Metadata]*funcSvc fsCache *functionServiceCache
urlFuncSvc *cache.Cache // map[string]*funcSvc controller *controllerclient.Client
controller *controllerclient.Client
//functionService *cache.Cache // map[fission.Metadata]*funcSvc
//urlFuncSvc *cache.Cache // map[string]*funcSvc
} }
func MakeAPI(gpm *GenericPoolManager, controller *controllerclient.Client) *API { func MakeAPI(gpm *GenericPoolManager, controller *controllerclient.Client, fsCache *functionServiceCache) *API {
return &API{ return &API{
poolMgr: gpm, poolMgr: gpm,
functionEnv: cache.MakeCache(0), functionEnv: cache.MakeCache(time.Minute, 0),
functionService: cache.MakeCache(time.Minute), fsCache: fsCache,
urlFuncSvc: cache.MakeCache(time.Minute), controller: controller,
controller: controller,
} }
} }
@@ -121,53 +121,40 @@ func (api *API) getFunctionEnv(m *fission.Metadata) (*fission.Environment, error
func (api *API) getServiceForFunction(m *fission.Metadata) (string, error) { func (api *API) getServiceForFunction(m *fission.Metadata) (string, error) {
// Check function -> svc map // Check function -> svc map
log.Printf("[%v] Checking for cached function service", m.Name) log.Printf("[%v] Checking for cached function service", m.Name)
result, err := api.functionService.Get(*m) fsvc, err := api.fsCache.GetByFunction(m)
if err == nil { if err == nil {
// Ok: return svc name // Cached, return svc name
svc := result.(*funcSvc) return fsvc.address, nil
svc.atime = time.Now()
return svc.serviceName, nil
} }
// None exists, so create a new funcSvc: api.fsCache.Log()
// None exists, so create a new funcSvc:
log.Printf("[%v] No cached function service found, creating one", m.Name) log.Printf("[%v] No cached function service found, creating one", m.Name)
// from Func -> get Env // from Func -> get Env
log.Printf("[%v] getting environment for function", m.Name) log.Printf("[%v] getting environment for function", m.Name)
env, err := api.getFunctionEnv(m) env, err := api.getFunctionEnv(m)
if err != nil { if err != nil {
return "", err return "", err
} }
// from Env -> get GenericPool // from Env -> get GenericPool
log.Printf("[%v] getting generic pool for env", m.Name) log.Printf("[%v] getting generic pool for env", m.Name)
pool, err := api.poolMgr.GetPool(env) pool, err := api.poolMgr.GetPool(env)
if err != nil { if err != nil {
return "", err return "", err
} }
// from GenericPool -> get one function container // from GenericPool -> get one function container
// (this also adds to the cache)
log.Printf("[%v] getting function service from pool", m.Name) log.Printf("[%v] getting function service from pool", m.Name)
funcSvc, err := pool.GetFuncSvc(m) funcSvc, err := pool.GetFuncSvc(m)
if err != nil { if err != nil {
return "", err return "", err
} }
// add to cache return funcSvc.address, nil
err = api.functionService.Set(*m, funcSvc)
if err != nil {
// log and ignore error
log.Printf("Error caching function service: %v", err)
}
// cache by svc hostname, for tapService()
err = api.urlFuncSvc.Set(funcSvc.serviceName, funcSvc)
if err != nil {
// log and ignore error
log.Printf("Error caching function service by name: %v", err)
}
return funcSvc.serviceName, nil
} }
// find funcSvc and update its atime // find funcSvc and update its atime
@@ -180,16 +167,12 @@ func (api *API) tapService(w http.ResponseWriter, r *http.Request) {
svcName := string(body) svcName := string(body)
svcHost := strings.TrimPrefix(svcName, "http://") svcHost := strings.TrimPrefix(svcName, "http://")
log.Printf("tap svc: %v", svcHost) err = api.fsCache.TouchByAddress(svcHost)
funcSvcI, err := api.urlFuncSvc.Get(svcHost)
if err != nil { if err != nil {
log.Printf("funcSvc tap error: %v", err)
http.Error(w, "Not found", 404) http.Error(w, "Not found", 404)
return return
} }
(funcSvcI.(*funcSvc)).atime = time.Now()
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
+196
View File
@@ -0,0 +1,196 @@
/*
Copyright 2016 The Fission Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package poolmgr
import (
"log"
"time"
"github.com/platform9/fission"
"github.com/platform9/fission/cache"
)
type fscRequestType int
const (
TOUCH fscRequestType = iota
LISTOLD
LOG
)
type (
functionServiceCache struct {
byFunction *cache.Cache // function -> funcSvc : map[fission.Metadata]*funcSvc
byAddress *cache.Cache // address -> function : map[string]fission.Metadata
byPod *cache.Cache // podname -> function : map[string]fission.Metadata
requestChannel chan *fscRequest
}
fscRequest struct {
requestType fscRequestType
address string
age time.Duration
responseChannel chan *fscResponse
}
fscResponse struct {
podNames []string
error
}
)
func MakeFunctionServiceCache() *functionServiceCache {
fsc := &functionServiceCache{
byFunction: cache.MakeCache(0, 0),
byAddress: cache.MakeCache(0, 0),
byPod: cache.MakeCache(0, 0),
requestChannel: make(chan *fscRequest),
}
go fsc.service()
return fsc
}
func (fsc *functionServiceCache) service() {
for {
req := <-fsc.requestChannel
resp := &fscResponse{}
switch req.requestType {
case TOUCH:
// update atime for this function svc
resp.error = fsc._touchByAddress(req.address)
case LISTOLD:
// get svcs idle for > req.age
byPodCopy := fsc.byPod.Copy()
pods := make([]string, 0)
for podNameI, fsvcI := range byPodCopy {
fsvc := fsvcI.(*funcSvc)
if time.Now().Sub(fsvc.atime) > req.age {
podName := podNameI.(string)
pods = append(pods, podName)
}
}
resp.podNames = pods
case LOG:
funcCopy := fsc.byFunction.Copy()
log.Printf("Cache has %v entries", len(funcCopy))
for mI, fsvcI := range funcCopy {
m := mI.(fission.Metadata)
fsvc := fsvcI.(*funcSvc)
log.Printf("%v:%v\t%v", m.Name, m.Uid, fsvc.podName)
}
}
req.responseChannel <- resp
}
}
func (fsc *functionServiceCache) GetByFunction(m *fission.Metadata) (*funcSvc, error) {
fsvcI, err := fsc.byFunction.Get(*m)
if err != nil {
return nil, err
}
// update atime
fsvc := fsvcI.(*funcSvc)
fsvc.atime = time.Now()
fsvcCopy := *fsvc
return &fsvcCopy, nil
}
func (fsc *functionServiceCache) Add(fsvc funcSvc) (error, *funcSvc) {
err, existing := fsc.byFunction.Set(*fsvc.function, &fsvc)
if err != nil {
if existing != nil {
f := existing.(*funcSvc)
err2 := fsc.TouchByAddress(f.address)
if err2 != nil {
return err2, nil
}
fCopy := *f
return err, &fCopy
}
return err, nil
}
now := time.Now()
fsvc.ctime = now
fsvc.atime = now
err, _ = fsc.byAddress.Set(fsvc.address, *fsvc.function)
if err != nil {
log.Printf("error caching fsvc: %v", err)
return err, nil
}
err, _ = fsc.byPod.Set(fsvc.podName, *fsvc.function)
if err != nil {
log.Printf("error caching fsvc: %v", err)
return err, nil
}
return nil, nil
}
func (fsc *functionServiceCache) TouchByAddress(address string) error {
responseChannel := make(chan *fscResponse)
fsc.requestChannel <- &fscRequest{
requestType: TOUCH,
address: address,
responseChannel: responseChannel,
}
resp := <-responseChannel
return resp.error
}
func (fsc *functionServiceCache) _touchByAddress(address string) error {
mI, err := fsc.byAddress.Get(address)
if err != nil {
return err
}
m := mI.(fission.Metadata)
fsvcI, err := fsc.byFunction.Get(m)
if err != nil {
return err
}
fsvc := fsvcI.(*funcSvc)
fsvc.atime = time.Now()
return nil
}
func (fsc *functionServiceCache) DeleteByPod(podName string) error {
mI, err := fsc.byPod.Get(podName)
if err != nil {
return err
}
m := mI.(fission.Metadata)
fsvcI, err := fsc.byFunction.Get(m)
if err != nil {
return err
}
fsvc := fsvcI.(*funcSvc)
fsc.byFunction.Delete(m)
fsc.byAddress.Delete(fsvc.address)
fsc.byPod.Delete(podName)
return nil
}
func (fsc *functionServiceCache) Log() {
log.Printf("--- FunctionService Cache Contents")
responseChannel := make(chan *fscResponse)
fsc.requestChannel <- &fscRequest{
requestType: LOG,
responseChannel: responseChannel,
}
<-responseChannel
log.Printf("--- FunctionService Cache Contents End")
}
+72
View File
@@ -0,0 +1,72 @@
package poolmgr
import (
"log"
"testing"
"github.com/platform9/fission"
"time"
)
func TestFunctionServiceCache(t *testing.T) {
fsc := MakeFunctionServiceCache()
if fsc == nil {
log.Panicf("error creating cache")
}
var fsvc *funcSvc
now := time.Now()
fsvc = &funcSvc{
function: &fission.Metadata{
Name: "foo",
Uid: "1212",
},
environment: &fission.Environment{
Metadata: fission.Metadata{
Name: "foo-env",
Uid: "2323",
},
RunContainerImageUrl: "fission/foo-env",
},
address: "xxx",
podName: "yyy",
ctime: now,
atime: now,
}
err, _ := fsc.Add(*fsvc)
if err != nil {
fsc.Log()
log.Panicf("Failed to add fsvc: %v", err)
}
f, err := fsc.GetByFunction(fsvc.function)
if err != nil {
fsc.Log()
log.Panicf("Failed to get fsvc: %v", err)
}
fsvc.atime = f.atime
fsvc.ctime = f.ctime
if *f != *fsvc {
fsc.Log()
log.Panicf("Incorrect fsvc \n(expected: %#v)\n (found: %#v)", fsvc, f)
}
err = fsc.TouchByAddress(fsvc.address)
if err != nil {
fsc.Log()
log.Panicf("Failed to touch fsvc: %v", err)
}
err = fsc.DeleteByPod(fsvc.podName)
if err != nil {
fsc.Log()
log.Panicf("Failed to delete fsvc: %v", err)
}
_, err = fsc.GetByFunction(fsvc.function)
if err == nil {
fsc.Log()
log.Panicf("found fsvc while expecting empty cache", err)
}
}
+37 -36
View File
@@ -25,8 +25,10 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
"github.com/dchest/uniuri"
"k8s.io/client-go/1.4/kubernetes" "k8s.io/client-go/1.4/kubernetes"
"k8s.io/client-go/1.4/pkg/api" "k8s.io/client-go/1.4/pkg/api"
"k8s.io/client-go/1.4/pkg/api/v1" "k8s.io/client-go/1.4/pkg/api/v1"
@@ -39,17 +41,16 @@ import (
type ( type (
GenericPool struct { GenericPool struct {
env *fission.Environment env *fission.Environment
replicas int32 // num containers replicas int32 // num containers
deployment *v1beta1.Deployment // kubernetes deployment deployment *v1beta1.Deployment // kubernetes deployment
namespace string // namespace to keep our resources namespace string // namespace to keep our resources
podReadyTimeout time.Duration // timeout for generic pods to become ready podReadyTimeout time.Duration // timeout for generic pods to become ready
controllerUrl string controllerUrl string
idlePodReapTime time.Duration // pods unused for idlePodReapTime are deleted idlePodReapTime time.Duration // pods unused for idlePodReapTime are deleted
fsCache *functionServiceCache // cache funcSvc's by function, address and podname
useSvc bool // create service for useSvc bool // create service
podFuncSvc *cache.Cache // map[pod.ObjectMeta.Name]*funcSvc poolInstanceId string // small random string to uniquify pod names
kubernetesClient *kubernetes.Clientset kubernetesClient *kubernetes.Clientset
requestChannel chan *choosePodRequest requestChannel chan *choosePodRequest
} }
@@ -70,7 +71,8 @@ func MakeGenericPool(
kubernetesClient *kubernetes.Clientset, kubernetesClient *kubernetes.Clientset,
env *fission.Environment, env *fission.Environment,
initialReplicas int32, initialReplicas int32,
namespace string) (*GenericPool, error) { namespace string,
fsCache *functionServiceCache) (*GenericPool, error) {
log.Printf("Creating pool for environment %v", env.Metadata) log.Printf("Creating pool for environment %v", env.Metadata)
// TODO: in general we need to provide the user a way to configure pools. Initial // TODO: in general we need to provide the user a way to configure pools. Initial
@@ -84,9 +86,10 @@ func MakeGenericPool(
podReadyTimeout: 5 * time.Minute, // TODO make this an env param? podReadyTimeout: 5 * time.Minute, // TODO make this an env param?
controllerUrl: controllerUrl, controllerUrl: controllerUrl,
idlePodReapTime: 3 * time.Minute, // TODO make this configurable idlePodReapTime: 3 * time.Minute, // TODO make this configurable
fsCache: fsCache,
poolInstanceId: uniuri.NewLen(8),
useSvc: false, useSvc: false,
podFuncSvc: cache.MakeCache(0),
} }
// create the pool // create the pool
@@ -271,7 +274,8 @@ func (gp *GenericPool) specializePod(metadata *fission.Metadata) (*v1.Pod, error
// A pool is a deployment of generic containers for an env. This // A pool is a deployment of generic containers for an env. This
// creates the pool but doesn't wait for any pods to be ready. // creates the pool but doesn't wait for any pods to be ready.
func (gp *GenericPool) createPool() error { func (gp *GenericPool) createPool() error {
poolDeploymentName := fmt.Sprintf("%v-%v", gp.env.Metadata.Name, gp.env.Metadata.Uid) poolDeploymentName := fmt.Sprintf("%v-%v-%v",
gp.env.Metadata.Name, gp.env.Metadata.Uid, strings.ToLower(gp.poolInstanceId))
podLabels := map[string]string{ podLabels := map[string]string{
"pool": poolDeploymentName, "pool": poolDeploymentName,
@@ -419,31 +423,28 @@ func (gp *GenericPool) GetFuncSvc(m *fission.Metadata) (*funcSvc, error) {
fsvc := &funcSvc{ fsvc := &funcSvc{
function: m, function: m,
environment: gp.env, environment: gp.env,
serviceName: svcHost, address: svcHost,
podName: pod.ObjectMeta.Name,
ctime: time.Now(), ctime: time.Now(),
atime: time.Now(), atime: time.Now(),
} }
gp.podFuncSvc[pod.ObjectMeta.Name].Set(fsvc)
err, existingFsvc := gp.fsCache.Add(*fsvc)
if err != nil {
// Some other thread beat us to it -- return the other thread's fsvc and clean up
// our own. TODO: this is grossly inefficient, improve it with some sort of state
// machine
log.Printf("func svc already exists: %v", existingFsvc.podName)
go gp.CleanupFunctionService(fsvc)
return existingFsvc, nil
}
return fsvc, nil return fsvc, nil
} }
func (gp *GenericPool) idlePodReaper() { func (gp *GenericPool) CleanupFunctionService(fsvc *funcSvc) {
for { // delete pod
podmap := gp.podFuncSvc.Copy() // remove ourselves from fsCache
for podNameI, funcSvcI := range podmap { }
podName := podNameI.(string)
funcSvc := funcSvcI.(*funcSvc) func (gp *GenericPool) idlePodReaper() {
lastAccessTime := funcSvc.atime
if time.Now().Sub(lastAccessTime) < gp.idlePodReapTime {
continue
}
log.Printf("Reaping idle pod %v (last used at %v)", podName, lastAccessTime)
err := gp.kubernetesClient.Core().Pods(gp.namespace).Delete(podName)
if err != nil {
log.Printf("Error reaping pod: %v", err)
continue
}
}
}
} }
-93
View File
@@ -1,93 +0,0 @@
package poolmgr
import (
"github.com/platform9/fission"
"fmt"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
"log"
"net/http"
"testing"
)
func getKubeClient() *unversioned.Client {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err := kubeConfig.ClientConfig()
if err != nil {
panic("failed loading client config")
}
client := unversioned.NewOrDie(config)
return client
}
type staticHandler struct {
resp string
}
func (s *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(s.resp))
}
// staticHttpServer starts an http server at port and responds to any
// request with the given response. Use this to mock the controller
// raw function fetch HTTP endpoint.
func staticHttpServer(port int, response string) {
s := &staticHandler{resp: response}
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), s))
}
func TestGenericPool(t *testing.T) {
namespace := "fission-test"
client := getKubeClient()
_, err := client.Namespaces().Create(&api.Namespace{
ObjectMeta: api.ObjectMeta{
Name: namespace,
Labels: map[string]string{},
},
})
if err != nil {
log.Panicf("failed to create namespace: %v", err)
}
// destroys everything in the namespace
defer client.Namespaces().Delete(namespace)
env := &fission.Environment{
Metadata: fission.Metadata{
Name: "test-env",
Uid: "",
},
RunContainerImageUrl: "fission/testing",
}
gp, err := MakeGenericPool(client, env, 3, namespace)
if err != nil {
log.Panicf("failed to make generic pool: %v", err)
}
log.Printf("Pool created")
// test specialization
testFunc := `
module.exports = function (context, callback) {
callback(200, "Hello, world!");
}
`
go staticHttpServer(2222, testFunc)
m := fission.Metadata{
Name: "foo",
Uid: "xxx-yyy",
}
fsvc, err := gp.GetFuncSvc(&m)
if err != nil {
log.Fatalf("Error getting function svc: %v", err)
}
log.Printf("fsvc: %v", fsvc)
}
+4 -2
View File
@@ -33,6 +33,7 @@ type (
namespace string namespace string
controllerUrl string controllerUrl string
controllerClient *client.Client controllerClient *client.Client
fsCache *functionServiceCache
requestChannel chan *request requestChannel chan *request
} }
@@ -46,13 +47,14 @@ type (
} }
) )
func MakeGenericPoolManager(controllerUrl string, kubernetesClient *kubernetes.Clientset, namespace string) *GenericPoolManager { func MakeGenericPoolManager(controllerUrl string, kubernetesClient *kubernetes.Clientset, namespace string, fsCache *functionServiceCache) *GenericPoolManager {
gpm := &GenericPoolManager{ gpm := &GenericPoolManager{
pools: make(map[fission.Environment]*GenericPool), pools: make(map[fission.Environment]*GenericPool),
kubernetesClient: kubernetesClient, kubernetesClient: kubernetesClient,
namespace: namespace, namespace: namespace,
controllerUrl: controllerUrl, controllerUrl: controllerUrl,
controllerClient: client.MakeClient(controllerUrl), controllerClient: client.MakeClient(controllerUrl),
fsCache: fsCache,
requestChannel: make(chan *request), requestChannel: make(chan *request),
} }
go gpm.service() go gpm.service()
@@ -68,7 +70,7 @@ func (gpm *GenericPoolManager) service() {
var err error var err error
pool, ok := gpm.pools[*req.env] pool, ok := gpm.pools[*req.env]
if !ok { if !ok {
pool, err = MakeGenericPool(gpm.controllerUrl, gpm.kubernetesClient, req.env, 3, gpm.namespace) pool, err = MakeGenericPool(gpm.controllerUrl, gpm.kubernetesClient, req.env, 3, gpm.namespace, gpm.fsCache)
if err != nil { if err != nil {
req.responseChannel <- &response{error: err} req.responseChannel <- &response{error: err}
continue continue
+4 -2
View File
@@ -56,9 +56,11 @@ func StartPoolmgr(controllerUrl string, namespace string, port int) error {
return err return err
} }
gpm := MakeGenericPoolManager(controllerUrl, kubernetesClient, namespace) fsCache := MakeFunctionServiceCache()
gpm := MakeGenericPoolManager(controllerUrl, kubernetesClient, namespace, fsCache)
api := MakeAPI(gpm, controllerClient, fsCache)
api := MakeAPI(gpm, controllerClient)
go api.Serve(port) go api.Serve(port)
return nil return nil