介绍
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 Cli
安装过程参考:https://github.com/openfaas/workshop,为方便进行环境搭建和测试,本文采用 Docker Swarm 的方式。MiniKube 的方式可以参见 Getting started with OpenFaaS on minikube
安装 OpenFaaS CLI
# Docker Swarm
$ docker swarm init
# OpenFaaS CLI
$ curl -sL cli.openfaas.com | sudo sh
$ faas-cli help
$ faas-cli version
部署 OpenFaaS
$ git clone https://github.com/openfaas/faas
$ cd faas && git checkout master
$ ./deploy_stack.sh --no-auth
$ 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 程序
$ 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
格式:
provider:
name: faas
gateway: http://127.0.0.1:8080
functions:
gohash:
lang: go
handler: ./gohash
image: gohash:latest
gohash/handler.go
内容如下:
package function
import (
"fmt"
)
// Handle a serverless request
func Handle(req []byte) string {
return fmt.Sprintf("Hello, Go. You said: %s", string(req))
}
编译和部署
# 编译程序
$ 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
目录下文件列表如下:
$ tree
├── Dockerfile
├── function
│ └── handler.go
├── main.go
└── template.yml
main.go
首先我们分析一下 main.go
,内容如下:
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.Stdout
。main.go
起到了一个包装器的作用。
template.yml
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
$ 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
的镜像中目录结构如下:
~ $ 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 文件如下:
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_By etc. 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 的主流程:
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)) // 处理请求
// ...
}
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)
}
}
}
主要通过调用 os.exec
相关的函数来实现。
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 中
}