一、说明
OpenFaaS比其它Serverless框架简单,至少社区版组件少,可以拿来学习和自定义修改,但是有个问题就是它的社区版不支持函数数量自动缩减到0(Pro版可以但是得加钱,没办法)。为了方便之后的实验,只好深入源码看看能不能修改了,还好是可行的!
这里使用的是K8s部署的OpenFaaS,一切操作都基于K8s。
OpenFaaS的扩缩容主要是通过Prometheus采集数据指标,在里面配置告警规则,然后通过Alertmanager接收和处理这些告警信息,将对应的告警发送给OpenFaaS的Gateway进行处理。所以主要就是改Prometheus的配置文件以及Gateway的一点源码即可。
二、源码下载和部署
Prometheus和Alertmanager的配置文件在faas-nets的源码里。Gateway源码在faas源码里。所以需要下载两个源码文件夹。
https://github.com/openfaas/faas.git
https://github.com/openfaas/faas-netes.git
1、在K8s上部署OpenFaaS
使用faas-nets源码来部署OpenFaaS在K8s上,在faas-net源码下执行以下命令就完事了
kubectl apply -f namespaces.yml
# 这里创建的是OpenFaaS网页的账号密码,可以改成自己的
kubectl -n openfaas create secret generic basic-auth \
--from-literal=basic-auth-user=admin \
--from-literal=basic-auth-password=admin
# 所有组件的配置文件都在里面,包括Prometheus的,后续在这个文件夹修改规则。
kubectl apply -f ./yaml/
# 监听部署情况
kubectl get pods -n openfaas -w
网络不好的话,配置文件里的镜像可以改成自己的镜像,这样容器镜像下载就快多了。我里面的镜像全自己构建了一遍推送到了自己的私有镜像仓库。
部署成功后就是下面的样子(gateway:v0.23.2,faas-netes:v0.15.2)
# kubectl get pods -n openfaas
NAME READY STATUS RESTARTS AGE
alertmanager-d498888f9-zt5h5 1/1 Running 0 22m
basic-auth-plugin-5b5587fcf4-nwxtf 1/1 Running 0 22m
gateway-7b469b749f-hqbxh 2/2 Running 0 22m
nats-647b476664-dhbbp 1/1 Running 0 22m
prometheus-dd4764755-kqmqt 1/1 Running 0 22m
queue-worker-84bfb5cd9b-7krwp 1/1 Running 0 22m
# kubectl get service -n openfaas
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
alertmanager ClusterIP 10.96.11.136 <none> 9093/TCP 23m
basic-auth-plugin ClusterIP 10.109.200.69 <none> 8080/TCP 23m
gateway ClusterIP 10.107.24.119 <none> 8080/TCP 23m
gateway-external NodePort 10.103.253.100 <none> 8080:31112/TCP 23m
gateway-provider ClusterIP 10.109.147.139 <none> 8081/TCP 23m
nats ClusterIP 10.101.229.232 <none> 4222/TCP 23m
prometheus ClusterIP 10.99.43.35 <none> 9090/TCP 23m
其中gateway-external暴露了31112端口,可以在网页通过服务器地址访问OpenFaaS界面,可以简单的部署官方函数和执行。
2023.5.16更新本文章。 新版本部署有区别。(gateway:v0.26.3,faas-netes:v0.16.7)
# kubectl get pods -n openfaas
NAME READY STATUS RESTARTS AGE
nats-7c5dc767cd-nk48m 1/1 Running 0 89s
queue-worker-66b8b77ddf-rkcqh 1/1 Running 1 (83s ago) 89s
alertmanager-79c5c74bd7-56fgc 1/1 Running 0 90s
gateway-58cd467ccd-wxzks 2/2 Running 1 (81s ago) 90s
prometheus-55fdccc666-n5ltc 1/1 Running 0 89s
# kubectl get service -n openfaas
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
alertmanager ClusterIP 10.43.62.43 <none> 9093/TCP 2m33s
basic-auth-plugin ClusterIP 10.43.106.199 <none> 8080/TCP 2m33s
gateway-external NodePort 10.43.240.154 <none> 8080:31112/TCP 2m33s
gateway ClusterIP 10.43.206.129 <none> 8080/TCP 2m32s
nats ClusterIP 10.43.235.72 <none> 4222/TCP 2m32s
prometheus ClusterIP 10.43.135.90 <none> 9090/TCP 2m32s
2024.5.30我又更新了本文章。 部署的东西没区别了,但有新的修改。(gateway:v0.27.6,faas-netes:v0.18.5)
2、下载命令行工具和部署函数
需要下载faas-cli命令行工具来操作OpenFaaS,执行以下命令即可:
curl -sL https://cli.openfaas.com | sudo sh
将OPENFAAS_URL写入环境变量中,执行如下命令:
echo export OPENFAAS_URL=127.0.0.1:31112 >> ~/.bashrc
配置立即生效,执行:
source ~/.bashrc
用账号密码登录,执行:
faas-cli login -u admin -p admin
简单部署一个官方函数,自定义函数的话可以去看官网文档。
faas-cli store deploy nodeinfo
# faas-cli store deploy nodeinfo
Deployed. 202 Accepted.
URL: http://127.0.0.1:31112/function/nodeinfo
# curl http://127.0.0.1:31112/function/nodeinfo
Hostname: nodeinfo-65dc4b757d-22rqf
Arch: x64
CPUs: 24
Total mem: 63597MB
Platform: linux
Uptime: 5259757.12
部署后它就一直在那,即使很久没调用也不会自己消失
# kubectl get pod -n openfaas-fn
NAME READY STATUS RESTARTS AGE
nodeinfo-65dc4b757d-22rqf 1/1 Running 0 7m39s
三、改源码
1、prometheus-cfg.yml修改
主要修改里面的alert.rules.yml
这个配置,这里我加了APINoInvocation这个告警。规则大概就是当函数副本数大于0,且新建的函数没调用过的话2分钟被灭,调用过一次的函数,如果5分钟内没有再调也得灭。
alert.rules.yml: |
groups:
- name: openfaas
rules:
- alert: service_down
expr: up == 0
- alert: APIHighInvocationRate
expr: sum(rate(gateway_function_invocation_total{code="200"}[10s])) BY (function_name) > 3
for: 5s
labels:
service: gateway
severity: major
annotations:
description: High invocation total on "{{$labels.function_name}}"
summary: High invocation total on "{{$labels.function_name}}"
- alert: APINoInvocation # 告警规则名称
expr: (sum(gateway_service_count) BY (function_name) > 0) and (sum(rate(gateway_function_invocation_total[3m])) BY (function_name) == 0) # promQL语句来匹配规则,当为true时触发
for: 2m # 当上面的语句触发后,2分钟内没有恢复,则告警进入pending状态,2分钟后触发报警
labels:
service: gateway
severity: major
annotations:
description: No invocation on "{{$labels.function_name}}"
summary: No invocation on "{{$labels.function_name}}"
gateway_function_invocation_total
是函数调用计数,可以计算每个函数的调用次数。只有被调用过的函数才会有这个属性。新建的不调用默认就没有,它就不会统计到。
gateway_service_count
是函数副本数,可以查询到新建的函数,如果一个函数实例都没有就是0。
function_name
是每个函数名。这些是在Gateway源码里定义的。
这部分解释可以在faas\gateway\metrics\metrics.go
代码里找到
告警设置
APIHighInvocationRate是一个默认的扩容告警,即10s内函数被多次的调用成功次数的增长率总和超过一定数就告警通知Gateway扩容。
prometheus设置的evaluation_interval
是告警规则计算间隔时间,里面配置的是每15秒计算一次。
APINoInvocation是我设置的告警规则,主要通过gateway_function_invocation_total
和gateway_service_count
来设置表达式。这里设置的是当函数副本数大于0,并且3分钟内没有调用过就告警让Gateway把它灭掉。但是有个问题就是刚创建的函数没被调用过,因此gateway_function_invocation_total
没值,就不会触发这个规则。这里我的方法是修改Gateway的代码,让它创建函数的时候就设置上这个值。
2、Gateway源码修改(gateway和faas-netes)
首先是修改faas\gateway\scaling\ranges.go
默认最小副本为0,它源码里是1。
// DefaultMinReplicas is the minimal amount of replicas for a service.
DefaultMinReplicas = 0
然后在faas\gateway\handlers\alerthandler.go
里面,这个代码主要是接收Alertmanager的告警消息,scaleService
函数就是具体的扩缩容操作了。观察CalculateReplicas
函数发现只要status = "resolved"
就能把副本直接设置为最小实例数,那就直接在它上面加个判断条件就行。这个resolved
状态是Alertmanager发送的告警是否解决的标志,源码里通过判断告警是否解决来决定是否增加还是灭掉多余的函数容器,所以这样设置就是假定告警解决了,让它直接恢复最低函数数量。修改的代码如下:
func scaleService(alert requests.PrometheusInnerAlert, service scaling.ServiceQuery, defaultNamespace string) error {
var err error
serviceName, namespace := middleware.GetNamespace(defaultNamespace, alert.Labels.FunctionName)
if len(serviceName) > 0 {
// 获取现在的副本数
queryResponse, getErr := service.GetReplicas(serviceName, namespace)
if getErr == nil {
status := alert.Status
// 添加这个判断,如果告警名字是APINoInvocation,就直接给我缩到0
if alert.Labels.AlertName == "APINoInvocation" {
status = "resolved"
}
// 计算新的副本数
newReplicas := CalculateReplicas(status, queryResponse.Replicas, uint64(queryResponse.MaxReplicas), queryResponse.MinReplicas, queryResponse.ScalingFactor)
log.Printf("[Scale] function=%s %d => %d.\n", serviceName, queryResponse.Replicas, newReplicas)
if newReplicas == queryResponse.Replicas {
return nil
}
updateErr := service.SetReplicas(serviceName, namespace, newReplicas)
if updateErr != nil {
err = updateErr
}
}
}
return err
}
前面说过如果新建函数如果不调用的话,gateway_function_invocation_total
就没有值,所以最后还得在faas\gateway\metrics\exporter.go
文件修改。Collect
函数就是给Prometheus收集数据,这里面的ServiceReplicasGauge
是计算的每个函数副本数,它所在的循环会遍历所有存在的函数,所以新建的函数也能统计到。Prometheus
提供了一个通过值获取指标的函数,即用GetMetricWithLabelValues
获取一下每个函数的指标,如果不存在就会自己新建一个初始的,这样给GatewayFunctionInvocation
获取一下,就能把新建的函数的调用数给设置为0。这些指标可以在faas\gateway\metrics\metrics.go
找到。修改的代码如下:
// Collect collects data to be consumed by prometheus
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
e.metricOptions.ServiceReplicasGauge.Collect(ch)
e.metricOptions.GatewayFunctionInvocation.Collect(ch)
e.metricOptions.GatewayFunctionsHistogram.Collect(ch)
e.metricOptions.GatewayFunctionInvocationStarted.Collect(ch)
e.metricOptions.ServiceReplicasGauge.Reset()
for _, service := range e.services {
var serviceName string
if len(service.Namespace) > 0 {
serviceName = fmt.Sprintf("%s.%s", service.Name, service.Namespace)
} else {
serviceName = service.Name
}
// Set current replica count
e.metricOptions.ServiceReplicasGauge.
WithLabelValues(serviceName).
Set(float64(service.Replicas))
// 添加这一句,获取一下函数调用数,如果没有,会自动设置为0
e.metricOptions.GatewayFunctionInvocation.GetMetricWithLabelValues(serviceName, "200")
}
e.metricOptions.ServiceMetrics.Counter.Collect(ch)
e.metricOptions.ServiceMetrics.Histogram.Collect(ch)
}
2023.5.16更新本文章。 新版还得改faas-netes(v0.16.7)的一点代码。
首先是修改faas-netes\pkg\handlers\replica_updater.go
,注释掉缩为0的错误。
func MakeReplicaUpdater(defaultNamespace string, clientset *kubernetes.Clientset) http.HandlerFunc {
// ...
// 注释或者删除即可
// if req.Replicas == 0 {
// http.Error(w, "replicas cannot be set to 0 in OpenFaaS CE",
// http.StatusBadRequest)
// return
// }
// ...
}
然后是修改faas-netes\pkg\handlers\replica_reader.go
,它原本设置的最大实例数为10,可以改大点。
const MaxReplicas = 50
最后修改faas-netes\pkg\controller\informers.go
,它逻辑是当实例数为0时自动改成1,必须得改为0。
func applyValidation(deployment *appsv1.Deployment, kubeClient *kubernetes.Clientset) error {
if deployment.Spec.Replicas == nil {
return nil
}
if _, ok := deployment.Spec.Template.Labels["faas_function"]; !ok {
return nil
}
current := *deployment.Spec.Replicas
var target int
if current == 0 {
// 改为0
target = 0
} else if current > handlers.MaxReplicas {
target = handlers.MaxReplicas
} else {
return nil
}
// ...
}
2024.5.30更新本文章。
这一版gateway(v0.27.6) 也限制了函数最大实例数量。
依旧是修改faas\gateway\scaling\ranges.go
,将实例数量改大点。
// DefaultMaxReplicas is the amount of replicas a service will auto-scale up to.
const (
// ...
DefaultMaxReplicas = 50
// ...
)
func MakeHorizontalScalingHandler(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ...
// 这里改为0
if scaleRequest.Replicas < 1 {
scaleRequest.Replicas = 0
}
if scaleRequest.Replicas > DefaultMaxReplicas {
scaleRequest.Replicas = DefaultMaxReplicas
}
// ...
}
}
这一版faas-netes(v0.18.5)又多了限制,最大只能部署15个函数,还有60天商用限制,继续改源码:
修改faas-netes\pkg\handlers\replica_reader.go
,将函数数量改大点,也可以找到使用该参数的地方注释掉,这样就无限制。
const MaxFunctions = 100
修改faas-netes\main.go
,这里有个上网检测,如果机器不能上网就注释掉。
if err := config.ConnectivityCheck(); err != nil {
log.Fatalf("Error checking connectivity, OpenFaaS CE cannot be run in an offline environment: %s", err.Error())
}
四、构建镜像和重新部署
构建镜像
首先可以自己改一下Gateway的Dockerfile
文件,我主要删了5处,如下:
# 删,用不着license-check
# FROM --platform=${BUILDPLATFORM:-linux/amd64} ghcr.io/openfaas/license-check:0.4.0 as license-check
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.22-alpine as build
ENV GO111MODULE=on
ENV CGO_ENABLED=0
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG TARGETOS
ARG TARGETARCH
ARG GIT_COMMIT
ARG VERSION
# 删,用不着license-check
# COPY --from=license-check /license-check /usr/bin/
WORKDIR /gateway
COPY vendor vendor
COPY go.mod go.mod
COPY go.sum go.sum
COPY handlers handlers
COPY metrics metrics
COPY requests requests
COPY types types
COPY plugin plugin
COPY version version
COPY scaling scaling
COPY probing probing
COPY pkg pkg
COPY main.go .
# 删,用不着license-check
# RUN license-check -path ./ --verbose=false "Alex Ellis" "OpenFaaS Authors" "OpenFaaS Author(s)"
# 删,测试浪费时间
# Run a gofmt and exclude all vendored code.
# RUN test -z "$(gofmt -l $(find . -type f -name '*.go' -not -path "./vendor/*"))"
# RUN go test $(go list ./... | grep -v integration | grep -v /vendor/ | grep -v /template/) -cover
RUN CGO_ENABLED=${CGO_ENABLED} GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build --ldflags "-s -w \
-X \"github.com/openfaas/faas/gateway/version.GitCommitSHA=${GIT_COMMIT}\" \
-X \"github.com/openfaas/faas/gateway/version.Version=${VERSION}\" \
-X github.com/openfaas/faas/gateway/types.Arch=${TARGETARCH}" \
-a -installsuffix cgo -o gateway .
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:3.19.1 as ship
# 删
# LABEL org.label-schema.license="Non-commercial use only" \
# org.label-schema.vcs-url="https://github.com/openfaas/faas-netes" \
# org.label-schema.vcs-type="Git" \
# org.label-schema.name="openfaas/faas-netes" \
# org.label-schema.vendor="openfaas" \
# org.label-schema.docker.schema-version="1.0"
# 加一个这个,快一点
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update
RUN addgroup -S app \
&& adduser -S -g app app \
&& apk add --no-cache ca-certificates
WORKDIR /home/app
EXPOSE 8080
EXPOSE 8082
ENV http_proxy ""
ENV https_proxy ""
# 注意本地代码的路径
COPY --from=build /gateway .
COPY assets assets
ARG TARGETPLATFORM
RUN if [ "$TARGETPLATFORM" = "linux/arm/v7" ] ; then sed -ie s/x86_64/armhf/g assets/script/funcstore.js ; elif [ "$TARGETPLATFORM" = "linux/arm64" ] ; then sed -ie s/x86_64/arm64/g assets/script/funcstore.js; fi
RUN chown -R app:app ./
USER app
CMD ["./gateway"]
然后是faas-nets的Dockerfile
文件,如下:
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.22-alpine as build
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG TARGETOS
ARG TARGETARCH
ARG VERSION
ARG GIT_COMMIT
ENV CGO_ENABLED=0
ENV GO111MODULE=on
ENV GOFLAGS=-mod=vendor
WORKDIR /faas-netes
COPY . .
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
--ldflags "-s -w \
-X github.com/openfaas/faas-netes/version.GitCommit=${GIT_COMMIT}\
-X github.com/openfaas/faas-netes/version.Version=${VERSION}" \
-o faas-netes .
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:3.19.1 as ship
# 加一个这个,快一点
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update
RUN apk --no-cache add \
ca-certificates
RUN addgroup -S app \
&& adduser -S -g app app
WORKDIR /home/app
EXPOSE 8080
ENV http_proxy ""
ENV https_proxy ""
COPY --from=build /faas-netes .
RUN chown -R app:app ./
USER app
CMD ["./faas-netes"]
构建镜像和推送到自己仓库
# 这里我用了自己的私有镜像仓库,方便下载和囤镜像,别忘了后面的点(.)
docker build -t 192.168.1.51:5000/gateway:v0.27.6.1 .
# 推到自己仓库
docker push 192.168.1.51:5000/gateway:v0.18.5.1
或者写个Makefile
SERVER?=192.168.1.51:5000
G_IMG_NAME?=gateway
F_IMG_NAME?=faas-netes
G_TAG?=v0.27.6.1
F_TAG?=v0.18.5.1
.PHONY: g
g:
docker build -t $(SERVER)/$(G_IMG_NAME):$(G_TAG) .
docker push $(SERVER)/$(G_IMG_NAME):$(G_TAG)
.PHONY: f
f:
docker build -t $(SERVER)/$(F_IMG_NAME):$(F_TAG) .
docker push $(SERVER)/$(F_IMG_NAME):$(F_TAG)
然后运行make g 或 make g G_TAG=v1.1
重新部署
# 修改prometheus-cfg.yml后,重新apply配置以及重启pod
kubectl apply -f prometheus-cfg.yml
kubectl replace --force -f prometheus-dep.yml
# 修改gateway-dep.yml对应的镜像,直接重新apply配置
kubectl apply -f gateway-dep.yml
五、验证
新建的函数
下面整合了faas-netes和gateway的日志,可以看到nodeinfo
函数创建和被灭掉大约花了2分15秒。
因为刚创建函数调用数为0,会直接触发APINoInvocation这个规则,但是需要等2分钟才激活告警。而Prometheus配置的告警规则计算时间是15秒,所以刚创建的话,函数被灭的时间大致就在2分15秒内。
2022/09/24 16:18:58 Deployment created: nodeinfo.openfaas-fn
2022/09/24 16:18:58 Service created: nodeinfo.openfaas-fn
...
2022/09/24 16:21:13 [Scale] function=nodeinfo 1 => 0.
2022/09/24 16:21:13 SetReplicas [nodeinfo.openfaas-fn] took: 0.0144s
调用一次函数
下面整合了faas-netes和gateway的日志,可以看到nodeinfo
函数经过调用一次重启后和被灭掉大约花了5分9秒。
因为APINoInvocation配置的告警规则是3分钟内没调用过,所以调用后至少得等3分钟再触发告警,然后加上激活等待时间2分钟,以及Prometheus每一次的规则计算时间15秒,时间在5分15秒内,差不多符合。
# curl http://127.0.0.1:31112/function/nodeinfo
2022/09/24 17:36:26 [Scale 0/20] function=nodeinfo 0 => 1 requested
2022/09/24 17:36:26 SetReplicas [nodeinfo.openfaas-fn] took: 0.0201s
2022/09/24 17:36:34 [Ready] function=nodeinfo waited for - 8.0282s
2022/09/24 17:36:34 Forwarded [GET] to /function/nodeinfo - [200] - 0.0284s
2022/09/24 17:36:34 nodeinfo took 0.027710 seconds
...
2022/09/24 17:41:43 [Scale] function=nodeinfo 1 => 0.
2022/09/24 17:41:43 SetReplicas [nodeinfo.openfaas-fn] took: 0.0135s