OpenFaaS架构——faas-provider

1,233 阅读4分钟

简而言之,faas-provider就是提供faas底层承载能力的系统的一个抽象,常见的比如kubernetes这种服务编排系统就能当作底层承载系统,也就是针对kubernetes实现一套faas-provider就能提供faas能力(这个实现就是faas-netes)。

faas-provider的作用

这个faas-provider可用于为OpenFaaS编写自己的后端(类似于kubenetes)。在使用Dockerfile构建镜像的过程中可以将Golang SDK嵌入到项目中,以便可以提供与OpenFaaS网关兼容的faas能力provider程序。

faas-provider为faas系统提供CRUD和函数调用的功能。如果开发者完成了所需的endpoint,那么将能够使用容器编排来作为faas的后端系统。也就是说openfaas指定了一套provider,实现了里面的端点,就能对接openfaas的框架。

faas-provider的模块

路由

因为provider最重要的功能就是连接gateway和faas底层能力集群,所以透传,或者路由是最重要的功能。faas-provider提供了http.Client实例,Serve是他的关键路由方法,从中可以看到函数相关的接口。

func Serve(handlers *types.FaaSHandlers, config *types.FaaSConfig) {

	if config.EnableBasicAuth {
		reader := auth.ReadBasicAuthFromDisk{
			SecretMountPath: config.SecretMountPath,
		}

		credentials, err := reader.Read()
		if err != nil {
			log.Fatal(err)
		}

		handlers.FunctionReader = auth.DecorateWithBasicAuth(handlers.FunctionReader, credentials)
		handlers.DeployHandler = auth.DecorateWithBasicAuth(handlers.DeployHandler, credentials)
		handlers.DeleteHandler = auth.DecorateWithBasicAuth(handlers.DeleteHandler, credentials)
		handlers.UpdateHandler = auth.DecorateWithBasicAuth(handlers.UpdateHandler, credentials)
		handlers.ReplicaReader = auth.DecorateWithBasicAuth(handlers.ReplicaReader, credentials)
		handlers.ReplicaUpdater = auth.DecorateWithBasicAuth(handlers.ReplicaUpdater, credentials)
		handlers.InfoHandler = auth.DecorateWithBasicAuth(handlers.InfoHandler, credentials)
		handlers.SecretHandler = auth.DecorateWithBasicAuth(handlers.SecretHandler, credentials)
		handlers.LogHandler = auth.DecorateWithBasicAuth(handlers.LogHandler, credentials)
	}

	// System (auth) endpoints
	r.HandleFunc("/system/functions", handlers.FunctionReader).Methods("GET")
	r.HandleFunc("/system/functions", handlers.DeployHandler).Methods("POST")
	r.HandleFunc("/system/functions", handlers.DeleteHandler).Methods("DELETE")
	r.HandleFunc("/system/functions", handlers.UpdateHandler).Methods("PUT")

	r.HandleFunc("/system/function/{name:["+NameExpression+"]+}", handlers.ReplicaReader).Methods("GET")
	r.HandleFunc("/system/scale-function/{name:["+NameExpression+"]+}", handlers.ReplicaUpdater).Methods("POST")
	r.HandleFunc("/system/info", handlers.InfoHandler).Methods("GET")

	r.HandleFunc("/system/secrets", handlers.SecretHandler).Methods(http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete)
	r.HandleFunc("/system/logs", handlers.LogHandler).Methods(http.MethodGet)

	r.HandleFunc("/system/namespaces", handlers.ListNamespaceHandler).Methods("GET")

	// Open endpoints
	r.HandleFunc("/function/{name:["+NameExpression+"]+}", handlers.FunctionProxy)
	r.HandleFunc("/function/{name:["+NameExpression+"]+}/", handlers.FunctionProxy)
	r.HandleFunc("/function/{name:["+NameExpression+"]+}/{params:.*}", handlers.FunctionProxy)

	if handlers.HealthHandler != nil {
		r.HandleFunc("/healthz", handlers.HealthHandler).Methods("GET")
	}

	readTimeout := config.ReadTimeout
	writeTimeout := config.WriteTimeout

	tcpPort := 8080
	if config.TCPPort != nil {
		tcpPort = *config.TCPPort
	}

	s := &http.Server{
		Addr:           fmt.Sprintf(":%d", tcpPort),
		ReadTimeout:    readTimeout,
		WriteTimeout:   writeTimeout,
		MaxHeaderBytes: http.DefaultMaxHeaderBytes, // 1MB - can be overridden by setting Server.MaxHeaderBytes.
		Handler:        r,
	}

	log.Fatal(s.ListenAndServe())
}

实现者只要引入provider包,然后调用该Serve方法,传入构造好的handlers作为参数,就能够和gateway打通,比如kubernetes的provider(faas-netes)使用如下实现,


func makeApplyHandler(defaultNamespace string, client clientset.Interface) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {

		...
		got, err := client.OpenfaasV1().Functions(namespace).Get(r.Context(), req.Service, opts)
		...
		if miss == false && got != nil {
			...
            //这里的Update方法就实现了对kubernetes中资源的更新
			if _, err = client.OpenfaasV1().Functions(namespace).
				Update(r.Context(), updated, metav1.UpdateOptions{}); err != nil {
				...
			}
		}
	}
}


bootstrapHandlers := types.FaaSHandlers{
		...
		DeployHandler:        makeApplyHandler(functionNamespace, client),
		...
	}

func (s *Server) Start() {
	bootstrap.Serve(s.BootstrapHandlers, s.BootstrapConfig)
}

faas如果是通过provider来代理请求(gateway中的属性direct_functions置为false),就会用到faas-provider中proxy包中的代理client。这个client具有如下的功能:

  • 这些传输值确保http客户端超过设定值后,最终将超时,并防止无限次重试
  • 重用连接,在高流量的情况下防止coreDns被速率限制

Transport: &http.Transport{
    Proxy: http.ProxyFromEnvironment,
    //用于创建http连接
    DialContext: (&net.Dialer{
        Timeout:   timeout,
        KeepAlive: 1 * time.Second,
        DualStack: true,
    }).DialContext,
    MaxIdleConns:          maxIdleConns,
    MaxIdleConnsPerHost:   maxIdleConnsPerHost,
    IdleConnTimeout:       120 * time.Millisecond,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1500 * time.Millisecond,
},
Timeout: timeout,
//禁止重定向,会在重试循环中使用到
CheckRedirect: func(req *http.Request, via []*http.Request) error {
    return http.ErrUseLastResponse
},

这里的MaxIdleConnsMaxIdleConnswPerHost的默认值为1024,可用于调优HTTP代理性能。由于go默认的htt.client在请求完成后会马上关闭连接,所以为了链接复用,这个值可以被设置为希望在同一个ip上轮训的次数,这个值建议至少1k。

与之相反的如果不采用provider转发(direct_functions置为true),那么流量会直接转发的pod中,转发的url形如:http://base64.openfaas-fn.svc.cluster.local:8080

权限验证

provider会在磁盘的固定路径中查找到用户名和密码,

reader := auth.ReadBasicAuthFromDisk{
    SecretMountPath: config.SecretMountPath,
}
credentials, err := reader.Read()

然后和请求头中的basicAuth信息作比对,从而达到权限认证的目的。

func DecorateWithBasicAuth(next http.HandlerFunc, credentials *BasicAuthCredentials) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {

		user, password, ok := r.BasicAuth()

		const noMatch = 0
		if !ok ||
			user != credentials.User ||
			subtle.ConstantTimeCompare([]byte(credentials.Password), []byte(password)) == noMatch {

			w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
			w.WriteHeader(http.StatusUnauthorized)
			w.Write([]byte("invalid credentials"))
			return
		}

		next.ServeHTTP(w, r)
	}
}

定义函数的属性

provider定义了函数部署和函数状态两种结构体,只要遵从这种定义结构,就能作为faas系统的provider提供severless能力。从源码中也可以看到该种类型的定义基本就是按照kunernetes对于集群中资源的定义。

type FunctionDeployment struct {
	Service string `json:"service"`
	Image string `json:"image"`
	Network string `json:"network"`
	EnvProcess string `json:"envProcess"`
	EnvVars map[string]string `json:"envVars"`
	RegistryAuth string `json:"registryAuth,omitempty"`
	Constraints []string `json:"constraints"`
	Secrets []string `json:"secrets"`
	Labels *map[string]string `json:"labels"`
	Annotations *map[string]string `json:"annotations"`
	Limits *FunctionResources `json:"limits"`
	Requests *FunctionResources `json:"requests"`
	ReadOnlyRootFilesystem bool `json:"readOnlyRootFilesystem"`
	Namespace string `json:"namespace,omitempty"`
}

type FunctionStatus struct {
	Name string `json:"name"`
	Image string `json:"image"`
	InvocationCount float64 `json:"invocationCount"`
	Replicas uint64 `json:"replicas"`
	EnvProcess string `json:"envProcess"`
	AvailableReplicas uint64 `json:"availableReplicas"`
	Labels *map[string]string `json:"labels"`
	Annotations *map[string]string `json:"annotations"`
	Namespace string `json:"namespace,omitempty"`
}

FunctionStatus会作为gateway模块中GetReplicas方法的返回类型,用来描述函数的详细状态。

后面我们会讲到faas-provider的kubenetes的实现,也就是openfaas-netes。