OpenFaaS架构——gateway

1,888 阅读10分钟

最近因为工作原因接触很多serverless底层实现的工作,所以开始研究这方面的架构。

OpenFaaS中函数是以容器的形式定义的,容器对用户而言并不是抽象的,用户在定义函数时将指定具体的容器镜像。这对于一些容器技术爱好者而言是一个优点

2017年dockerswarm,基于swarm实现了一个serverless架构,这个架构就像一个极简化的openfaas架构,对于学习openfaas的架构来说有所助益

  • API Gateway:负责接受服务调用,路由请求到后端函数实现,并采集服务调用的指标发送给 Prometheus。 Prometheus 则会根据一段时间内服务调用的次数,回调API Gateway 来动态伸缩服务容器实例数量。
  • Function Watchdog:将HTTP请求转发为进程调用,并将请求数据通过 STDIN 传递给进程,而将进程的 STDOUT 作为 HTTP 响应的结果返回给调用者。将函数进程和Function Watchdog打包成一个容器镜像进行部署。其调用流程如下:

概览

首先在容器集群中进行openfaas的安装,直接使用官方推荐arkade的方式来安装openfaas,官方安装指南。arkade是一个golang编写的cli程序,可以通过简单命令将charts和apps安装到kubernetes集群。

arkade install openfaas --load-balancer

安装好openfass后在集群中可以看到如下的信息:

总共启动了启动了7个pod

  • nats提供异步执行函数和队列的功能,openfaas提供将同步函数转化为异步函数的功能,在异步场景中用到了nats streaming,这是一个go编写的消息队列,吞吐量极大,超过了kafka,但是也有以下的特性,不能持久化,离线收不到消息。在openfaas的架构中他承接了gateway发送过来的异步请求,也就是说gateway是一个消息发布者
  • queue-work:消息订阅者,接受到消息之后的执行调用业务逻辑,支持回调url
  • prometheus和alertmanager做云原生开发相关的应该很常见了,这里的alertmanager主要是用来做弹性伸缩的
  • fass-idler提供了将长时间闲置的deployment缩放到0的功能。源码非常简单只有一个main.go文件。持有gateway的auth账号密码构造了openfaas的client,然后用一个for循环和time.sleep来轮训,遍历所有namespace,拿到所有的function对象作为参数,去promethues里面查询5m之内的调用次数(gateway_function_invocation_started),如果等于0,就调用client的ScaleFunction函数将function的deployment的副本数置为0。
  • 我们着重看gateway。

网关gateway

openfaas中的gateway可以归纳划分为以下的几个模块:

  • 为函数请求提供路由,代理转发请求
  • 自动伸缩(代理服务查询外部插件,alertmanager)
  • web ui
  • metric promethues

转发请求

首先明确的是,在通过faas-cli,UI或REST API进行部署之前,每个函数的功能都内置在一个固定的Docker映像中。

查看openfaas源码,发现源码中的gateway工程就是网关的所在,在该module的main.go入口中可以看到, 在读取完了配置和进行安全相关配置后,启动了一个名叫NewHTTPClientReverseProxy的http反向代理server,这个代理简单封装了goalng中的http.Client,是其能够代理上游主机。

httpServer的转发功能,其路由底层用的是gorillatoolkit。

可以看到他为每种操作类型的Function都注册了handler,这里的function基本就涵盖了gateway的所有功能了,这些构建好的function会直接对应不同的http路由:

  • ListFunctions
  • DeployFunction
  • DeleteFunction
  • UpdateFunction
  • QueryFunction
  • InfoHandler
  • SecretHandler
  • NamespaceListerHandler
  • Alert
  • LogProxyHandler

handlers.MakeForwardingProxyHandler方法的返回值为HandlerFunc,该种类型是golang内置的适配器,允许使用普通函数作为HTTP处理程序。传入具有适当签名f的函数,则HandlerFunc(f)是调用参数f的处理程序。

实际MakeForwardingProxyHandler方法返回一个匿名函数

func MakeForwardingProxyHandler(xxx) http.HandlerFunc {
	...
    return func(w http.ResponseWriter, r *http.Request) {
    		//1. 解析请求的基础url,并根据解析结果进行代理。这里有两种解析处理类型,这两种类型的Resolve方法最后都会得到一个url
                1.1 一种是SingleHostBaseURLResolver,基于单个baseurl请求来解析url,也就是简单截断
                1.2 另一种是FunctionAsHostBaseURLResolver,使用来自URL的函数作为主机来解析URL 
            //2. 在forwardRequest前和forwardRequest后,分别向各个HTTPNotifier发送通知
    }
}

httpServer中配置的重要参数就是functions_provider_url,这个是作为环境变量注入到程序中。如果是在kubenetes的环境中,这个值为我们后面会提到的faas-netes的地址。前面提到的各种类型的function都是往这个地址转发请求。而这个faas-netes就是kubenetes的openfaas provider。函数的调用都是发往这个地址。

以下是环境中的环境变量参数:

可以看到gateway转发请求本身不做任何和部署发布函数相关的事情,只是作为一个代理,把请求转发给相应的 Provider 去处理,比如kubernetes的provider。

需要注意的是gateway不仅可以通过provider代理请求,而且可以不通过provider,直接和函数服务进行通信。

if config.DirectFunctions {
    functionURLResolver = handlers.FunctionAsHostBaseURLResolver{
        FunctionSuffix:    config.DirectFunctionsSuffix,
        FunctionNamespace: config.Namespace,
    }
    functionURLTransformer = trimURLTransformer
} else {
    functionURLResolver = urlResolver
    functionURLTransformer = nilURLTransformer
}

可以看到根据不同配置属性,解析出来的functionURLResolver值是不一样的。解析的结果就直接指向了不同的provider地址。

比较特殊的是gateway为httpserver抽象了通知事件。这个接口有不同的实现类,根据不同类的实现,在事件完成之后做出相应的逻辑操作。比如

  • 日志实现会输出一条关于该次请求信息的日志
  • prometheus metric实现会在request的开始和完成时,都会为这次请求构造metric往prometheus发送数据
type HTTPNotifier interface {      

        Notify(method string, URL string, originalURL string, statusCode int, event string, duration time.Duration)

}

查看api swagger可以看到,openfaas还支持异步函数调用。

那么在main函数中一定会有异步函数的逻辑,我们在handler中可以看到相关的逻辑,这里主要做了四步处理:

  • 读取请求体
  • 将X-Callback-Url参数从参数中 http 的 header 中读出来
  • 实例化用于异步处理的 Request 对象
  • 调用 canQueueRequests.Queue(req),将请求发布到队列中
faasHandlers.QueuedProxy = 
handlers.MakeNotifierWrapper(
    handlers.MakeCallIDMiddleware(
       handlers.MakeQueuedProxy(
           metricsOptions, 
           natsQueue, 
           trimURLTransformer, 
           config.Namespace, 
           functionQuery
           )
       ),
    forwardingNotifiers,
 )

异步函数,Gateway就作为一个发布者,将函数放到队列里。MakeQueuedProxy方法就是做这件事儿的:


// MakeQueuedProxy accepts work onto a queue
func MakeQueuedProxy(metrics metrics.MetricOptions, wildcard bool, canQueueRequests queue.CanQueueRequests) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		body, err := ioutil.ReadAll(r.Body)
		// 省略错误处理代码
		vars := mux.Vars(r)
		name := vars["name"]

		callbackURLHeader := r.Header.Get("X-Callback-Url")
		var callbackURL *url.URL
		if len(callbackURLHeader) > 0 {
			urlVal, urlErr := url.Parse(callbackURLHeader)
			// 省略错误处理代码
			callbackURL = urlVal
		}
		req := &queue.Request{
			Function:    name,
			Body:        body,
			Method:      r.Method,
			QueryString: r.URL.RawQuery,
			Header:      r.Header,
			CallbackURL: callbackURL,
		}
		err = canQueueRequests.Queue(req)
		// 省略错误处理代码
		w.WriteHeader(http.StatusAccepted)
	}
}

自动伸缩

自动伸缩是openfaas特有的功能。会根据不同的需求指标来做自动伸缩。

在上面提到的HandlerFunc集合中,有一个名叫AlertHandlerFunc。这个HandlerFunc就是自动伸缩的主要逻辑。他的实现用MakeNotifierWrapper方法包装。这里的MakeNotifierWrapper包装了一个HandlerFunc。包装器的主要逻辑是在HandlerFunc的执行逻辑中加入拦截器,拦截器往HTTPNotifier中发送数据。

实际运行过程中会向日志模块LoggingNotifier和监控模块PrometheusFunctionNotifier发出已完成事件(event参数的值为**"completed"**)。


faasHandlers.Alert = 
    handlers.MakeNotifierWrapper(
        handlers.MakeAlertHandler(
            externalServiceQuery, 
            config.Namespace
            ), 
        forwardingNotifiers,
    )

这个接口的handler是由handlers.MakeAlertHandler方法生成。MakeAlertHandler 方法接收的参数是 ServiceQuery。ServiceQuery是一个包含providerUrl,client代理,权限认证注入器的对象,这个对象有两个方法,

  • GetReplicas:向provider查询在指定命名空间下,名称为service的函数的最小副本数,最大副本数,缩放因子,可用副本和注解。其中副本数和缩放因子都在函数的label元数据中
  • SetReplicas:post方法向provider传递参数,使得指定的命名空间下的函数扩展到count
type ServiceQuery interface {
      GetReplicas(service, namespace string) (response  ServiceQueryResponse, err error)      
      SetReplicas(service, namespace string, count uint64) error
}

GetReplicas接口的返回示例数据如下:

{
	"name": "hello-java8-fw",
	"image": "x/hello-java8-fw:1.0.0",
	"invocationCount": 0,
    //集群内所需副本
	"replicas": 1,
	"envProcess": "",
	"availableReplicas": 1,
	"labels": {
		"faas_function": "hello-java8-fw"
	},
	"annotations": {
		"prometheus.io.scrape": "false"
	},
	"namespace": "openfaas-fn"
}

ServiceQuery是一个接口。Gateway 中实现这个接口的类是在gateway下的plugin包中的ExternalServiceQuery,这里是解耦操作,可以根据不同的faas实现平台来定制这个实现类,比如常见的kubernetes实现,这两个函数的逻辑是调用gateway中对应的接口,然后让kubernetes的provider去实现具体的逻辑。 这两个gateway接口分别是:

  • system/function/:name
  • system/scale-function/:name

MakeAlertHandler的具体缩放逻辑在handler包的handleAlerts函数中,这个函数拿着被当做参数传进来的prometheusAlert,对函数进行缩放。而正真执行缩放的逻辑在函数scaleService中。

scaleService函数中先是根据命名空间和函数名称向provider查询现在的副本数量,然后用缩放因子的公式计算出新的副本 uint64((float64(maxReplicas) / 100) * float64(scalingFactor)),根据计算出的新的副本数来进行缩放

我们再来看一下prometheus和alertmanager中对openfaas的默认配置。

这是prometheus中的配置,可以看到默认的配置为10s内大于五次200的请求,就会触发告警规则

这是alertmanager中的配置,使用webhook的方式来向gateway发送告警,这里发送告警的目的地就是刚才着重介绍的handlers.MakeAlertHandler。发送告警的目的地就是gateway暴露的端点r.HandleFunc("/system/alert", faasHandlers.Alert).Methods(http.MethodPost),这个端点的handler使用了前面提到的ServiceQuery的实现类,最后的缩放逻辑还是落到了scaleService方法中。

当然也可以用下面这个路由来手动缩放函数

r.HandleFunc("/system/scale-function/{name:["+NameExpression+"]+}", faasHandlers.ScaleFunction).Methods(http.MethodPost)

gateway如何部署函数

无论是用webui还是openfaas-cli,最后都是通过gateway来部署函数,gateway是部署函数的核心。 通过main函数可以看到函数的增删改查都是用的同样的handler,区别仅仅是http的Method不同。

这是handler的构造:

这是增删改查的路由:

通过跟踪方法MakeForwardingProxyHandler可以知道,gateway为这些路由构造了一个新的httpclient,它会按照请求的method来使用不同的method来转发,这里的参数r就是原始请求的*http.Request

upstreamReq, _ := http.NewRequest(r.Method, url, nil)

请求最后会发往上面提到的functions_provider_url=http://127.0.0.1:8081/,也就是faas-netes。在faas-netes中会把参数body解析为FunctionDeployment,然后由faas-netes部署到kubernetes集群中。

gateway如何调用函数

无论是用webui发起的调用请求还是openfaas-cli的invoke命令,或者是生产环境的调用,转发路径也是通过gateway来进行的。 通过main函数可以看到函数的调用用的是如下的handler:

faasHandlers.Proxy = handlers.MakeForwardingProxyHandler(reverseProxy, functionNotifiers, functionURLResolver, functionURLTransformer, nil)

通过跟踪方法MakeForwardingProxyHandler可以知道,这个handler通过两个关键参数baseURLrequestURL构造出了调用的转发地址

url := baseURL + requestURL
if len(r.URL.RawQuery) > 0 {
	url = fmt.Sprintf("%s?%s", url, r.URL.RawQuery)
}

baseurl解析为http://svcName.FunctionSuffix:8080,这里的8080是watchdog的端口。而requestURL被解析为function/svc后面的url。这样再加上querystrinng就组成了一个调用的url。而这些url就指向了具体的函数。

后面我们会讲到这些svc后面挂的pod就是一些具体的嵌入了watchdog的函数镜像。

web ui

这个不用多说,源码中一看就知道,提供了ui访问入口:

ui中查询出来的数据也是通过访问gateway,然后由gateway送往provider,最终由各个provider提供具体的展示数据。

metric

StartServiceWatcher会启动一个ticker,收集服务副本的数量,然后暴露给prometheus来采集