最近因为工作原因接触很多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集合中,有一个名叫Alert的HandlerFunc。这个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通过两个关键参数baseURL和requestURL构造出了调用的转发地址
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来采集