程序印象

Serverless 之 OpenFaaS

2018/08/03 Share

介绍

OpenFaaS - Serverless Functions Made Simple for Docker & Kubernetes https://docs.openfaas.com/,当前支持以下语言:csharp、go、python、node、java、ruby等。

在 OpenFaaS 的UI 中可以通过指定 Docker Image等相关信息添加一个新的 Function,具体界面如下:

从原来上来讲,在我们部署的 Docker Image 中,在编译中会自动加入 Function Watchdog 的程序,该程序是基于 Go 开发的 Http Server,负责将本地镜像中的包含 Function 的可执行程序与 API Gateway 进行一个串联。

安装 OpenFaaS

安装过程参考:https://github.com/openfaas/workshop,为方便进行环境搭建和测试,本文采用 Docker Swarm 的方式。MiniKube 的方式可以参见 Getting started with OpenFaaS on minikube

安装 OpenFaaS CLI

参见:Prepare for OpenFaaS

1
2
3
4
5
6
7
8
# Docker Swarm
$ docker swarm init

# OpenFaaS CLI
$ curl -sL cli.openfaas.com | sudo sh

$ faas-cli help
$ faas-cli version

部署 OpenFaaS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git clone https://github.com/openfaas/faas
$ cd faas && git checkout master
$ ./deploy_stack.sh --no-auth # 部署 faas-swarm 相关的 provider
$ docker service ls
docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
jauscdc7kxsi base64 replicated 1/1 functions/alpine:latest
tqo6mkpf3xg6 echoit replicated 1/1 functions/alpine:latest
prmtd3len7hs func_alertmanager replicated 1/1 prom/alertmanager:v0.15.0-rc.0
i2p4m4187lkg func_faas-swarm replicated 1/1 openfaas/faas-swarm:0.4.0
vizszwmlekg6 func_gateway replicated 1/1 openfaas/gateway:0.8.9 *:8080->8080/tcp
07p9rusy7hiu func_nats replicated 1/1 nats-streaming:0.6.0
wxftz5yscpiz func_prometheus replicated 1/1 prom/prometheus:v2.2.0 *:9090->9090/tcp
zcpqvvj64tv4 func_queue-worker replicated 1/1 openfaas/queue-worker:0.4.8

......

当部署完成后,我们可以通过 http://127.0.0.1:8080/ui/ 参看到已经部署的 Function 并可以进行相关测试。

Go 语言

Go 语言的静态编译方式,可以打造出来体积比较小的镜像出来,非常适用于在 OpenFaaS 中来进行使用。

创建 Hello World 程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ mkdir -p $GOPATH/src/functions && cd $GOPATH/src/functions

$ faas-cli new --lang go gohash
Folder: gohash created.
___ ___ ___
/ _ \ _ __ ___ _ __ | ___|_ _ __ _/ ___|
| | | | '_ \ / _ \ '_ \| |_ / _` |/ _` \___ \
| |_| | |_) | __/ | | | _| (_| | (_| |___) |
\___/| .__/ \___|_| |_|_| \__,_|\__,_|___ /
|_|

Function created in folder: gohash
Stack file written: gohash.yml

# 创建后生成以下目录结构
$ tree
.
├── build
│   └── gohash
│   ├── Dockerfile
│   ├── function
│   │   └── handler.go
│   ├── main.go
│   └── template.yml
├── gohash
│   └── handler.go # 用于编写主逻辑的函数入口
├── gohash.yml # 配置文件
└── template
......

gohash.yml 格式:

1
2
3
4
5
6
7
8
9
provider:
name: faas
gateway: http://127.0.0.1:8080

functions:
gohash:
lang: go
handler: ./gohash
image: gohash:latest

gohash/handler.go 内容如下:

1
2
3
4
5
6
7
8
9
10
package function

import (
"fmt"
)

// Handle a serverless request
func Handle(req []byte) string {
return fmt.Sprintf("Hello, Go. You said: %s", string(req))
}

编译和部署

1
2
3
4
5
6
7
8
9
10
11
12
# 编译程序
$ faas-cli build -f gohash.yml

# 部署程序
$ faas-cli deploy -f gohash.yml

# 调用并测试
$ echo -n "test" | faas-cli invoke gohash
Hello, Go. You said: test

# 删除
$ echo -n "test" | faas-cli delete gohash

镜像细节探究

build/gohash 目录下文件列表如下:

1
2
3
4
5
6
$ tree
├── Dockerfile
├── function
│   └── handler.go
├── main.go
└── template.yml

main.go

首先我们分析一下 main.go,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"io/ioutil"
"log"
"os"

"handler/function"
)

func main() {
input, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("Unable to read standard input: %s", err.Error())
}

fmt.Println(function.Handle(input))
}

通过对于 main.go源码分析,我们可以得知,main函数主要是从 os.Stdin 读取数据,并调用我们 function.Handle 并将调用的结果打印到 os.Stdoutmain.go 起到了一个包装器的作用。

template.yml

1
2
3
4
5
6
language: go
fprocess: ./handler
welcome_message: |
You have created a new function which uses Golang 1.9.7.
To include third-party dependencies, use a vendoring tool like dep:
dep documentation: https://github.com/golang/dep#installation

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
$ cat Dockerfile
FROM golang:1.9.7-alpine3.7 as builder

RUN apk --no-cache add curl \
&& echo "Pulling watchdog binary from Github." \
&& curl -sSL https://github.com/openfaas/faas/releases/download/0.8.9/fwatchdog > /usr/bin/fwatchdog \
&& chmod +x /usr/bin/fwatchdog \
&& apk del curl --no-cache

WORKDIR /go/src/handler
COPY . .

# Run a gofmt and exclude all vendored code.
RUN test -z "$(gofmt -l $(find . -type f -name '*.go' -not -path "./vendor/*" -not -path "./function/vendor/*"))" || { echo "Run \"gofmt -s -w\" on your Golang code"; exit 1; }

RUN CGO_ENABLED=0 GOOS=linux \
go build --ldflags "-s -w" -a -installsuffix cgo -o handler . && \
go test $(go list ./... | grep -v /vendor/) -cover

FROM alpine:3.7
RUN apk --no-cache add ca-certificates

# Add non root user
RUN addgroup -S app && adduser -S -g app app
RUN mkdir -p /home/app

WORKDIR /home/app

COPY --from=builder /usr/bin/fwatchdog .

COPY --from=builder /go/src/handler/function/ .
COPY --from=builder /go/src/handler/handler .

RUN chown -R app /home/app

USER app

ENV fprocess="./handler"

HEALTHCHECK --interval=2s CMD [ -e /tmp/.lock ] || exit 1

CMD ["./fwatchdog"]

在生成的 gohash:latest 的镜像中目录结构如下:

1
2
3
4
5
6
7
~ $ pwd
/home/app
~ $ ls -hl
total 5644
-rwxr-xr-x 1 app root 4.2M Aug 2 05:16 fwatchdog
-rwxr-xr-x 1 app root 1.3M Aug 2 05:16 handler
-rw-r--r-- 1 app root 163 Aug 2 05:15 handler.go

watch dog

watch dog 对于我们编写的 function 函数套上了一层 http 的外壳(通过创建子进程,写入子进程的 stdiin,然后从子进程 stdout 接受响应数据)。

Wtachdog,作为镜像的对外代理程序,必须作为启动的入口,一个简单的 Dockerfile 文件如下:

1
2
3
4
5
6
7
8
9
FROM alpine:3.7

ADD https://github.com/openfaas/faas/releases/download/0.8.0/fwatchdog /usr/bin
RUN chmod +x /usr/bin/fwatchdog

# Define your binary here
ENV fprocess="/bin/cat" # 通过环境变量到处 watchdog 需要派生的子进程二进制

CMD ["fwatchdog"] # 必须将 watchdog 作为镜像运行的入口

对于 watchdog 的配置,主要是通过环境变量的方式进行,可以配置的值如下:

Option Usage
fprocess The process to invoke for each function call (function process). This must be a UNIX binary and accept input via STDIN and output via STDOUT
cgi_headers HTTP headers from request are made available through environmental variables - Http_X_Served_Byetc. See section: Handling headers for more detail. Enabled by default
marshal_request Instead of re-directing the raw HTTP body into your fprocess, it will first be marshalled into JSON. Use this if you need to work with HTTP headers and do not want to use environmental variables via the cgi_headers flag.
content_type Force a specific Content-Type response for all responses
write_timeout HTTP timeout for writing a response body from your function (in seconds)
read_timeout HTTP timeout for reading the payload from the client caller (in seconds)
suppress_lock The watchdog will attempt to write a lockfile to /tmp/ for swarm healthchecks - set this to true to disable behaviour.
exec_timeout Hard timeout for process exec’d for each incoming request (in seconds). Disabled if set to 0
write_debug Write all output, error messages, and additional information to the logs. Default is false
combine_output True by default - combines stdout/stderr in function response, when set to false stderr is written to the container logs and stdout is used for function response

更加具体的功能或者使用说明,参考:https://github.com/openfaas/faas/tree/master/watchdog

watchdog 的主流程:

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
// ...
s := &http.Server{
Addr: fmt.Sprintf(":%d", config.port),
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
MaxHeaderBytes: 1 << 20, // Max header of 1MB
}

http.HandleFunc("/_/health", makeHealthHandler()) // 用于健康检查
http.HandleFunc("/", makeRequestHandler(&config)) // 处理请求
// ...
}

handler.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func makeRequestHandler(config *WatchdogConfig) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case
http.MethodPost,
http.MethodPut,
http.MethodDelete,
http.MethodGet:
pipeRequest(config, w, r, r.Method)
break
default:
w.WriteHeader(http.StatusMethodNotAllowed)

}
}
}

handler.go

主要通过调用 os.exec 相关的函数来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func pipeRequest(config *WatchdogConfig, w http.ResponseWriter, r *http.Request, method string) {
parts := strings.Split(config.faasProcess, " ")
ri := &requestInfo{}
log.Println("Forking fprocess.")
// ...

// 执行目标二进制文件
targetCmd := exec.Command(parts[0], parts[1:]...)

// ...

// 获取目标子进程的 Stdin,后续将请求信息解码有写入
// func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
writer, _ := targetCmd.StdinPipe()

// 根据配置的各种参数,来进行处理写入,并采用 waitgroup 来读取响应
// ...
// func (c *Cmd) CombinedOutput() ([]byte, error)
out, err = targetCmd.Output()

// 将读取到的写入 w http.ResponseWriter 中
}

Gateway 核心功能

Backend 交互

OpenFaas 的 Backend 是通过 Provider 来进行的一层抽象,达到了与 Docker Swarm 或 Kubernets 容器编排平台的解耦,这两者是官方提供支持的。对于 OpenFaas 如何与 Backend 进行交互,可以参见 backend.md

OpenFaas 与 Docker Swam 集群的交互是通过 faas-swarm, 与 Kubernets 平台的集成是通过 faas-netes 。其他的 Provider 也都有社区进行开发。例如 AWS faas-fargate, Nomad faas-nomad, Rancher faas-rancher 等。

部署函数

调用函数

与 Kubernets 的交互图如下:

Gateway 与 Provider 交互

Gateway 通过读取环境变量 functions_provider_url 来与 Provider 进行交互。

gateway/server.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {

osEnv := types.OsEnv{}
readConfig := types.ReadConfig{}
config := readConfig.Read(osEnv)

log.Printf("HTTP Read Timeout: %s", config.ReadTimeout)
log.Printf("HTTP Write Timeout: %s", config.WriteTimeout)

if !config.UseExternalProvider() {
log.Fatalln("You must provide an external provider via 'functions_provider_url' env-var.")
}

// ...
// 在配置文件类 config 中 FunctionsProviderURL 作为 Provider 的 URL 使用
reverseProxy := types.NewHTTPClientReverseProxy(config.FunctionsProviderURL, config.UpstreamTimeout)
// ...
urlResolver := handlers.SingleHostBaseURLResolver{BaseURL: config.FunctionsProviderURL.String()} // config.FunctionsProviderURL

// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Read fetches config from environmental variables.
func (ReadConfig) Read(hasEnv HasEnv) GatewayConfig {
cfg := GatewayConfig{
PrometheusHost: "prometheus",
PrometheusPort: 9090,
}

// ...

if len(hasEnv.Getenv("functions_provider_url")) > 0 {
var err error
cfg.FunctionsProviderURL, err = url.Parse(hasEnv.Getenv("functions_provider_url"))
if err != nil {
log.Fatal("If functions_provider_url is provided, then it should be a valid URL.", err)
}
}

// ....
}

Docker Swarm Provider

参考

  1. Build a Serverless Golang Function with OpenFaaS
CATALOG
  1. 1. 介绍
  2. 2. 安装 OpenFaaS
    1. 2.1. 安装 OpenFaaS CLI
    2. 2.2. 部署 OpenFaaS
  3. 3. Go 语言
    1. 3.1. 创建 Hello World 程序
    2. 3.2. 编译和部署
  4. 4. 镜像细节探究
    1. 4.1. main.go
    2. 4.2. template.yml
    3. 4.3. Dockerfile
    4. 4.4. watch dog
  5. 5. Gateway 核心功能
    1. 5.1. Backend 交互
    2. 5.2. Gateway 与 Provider 交互
    3. 5.3. Docker Swarm Provider
  6. 6. 参考