Serverless之OpenFaaS入门

介绍

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

参见:Prepare for OpenFaaS

# 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.Stdoutmain.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_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

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

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 相关的函数来实现。

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 核心功能

参考

  1. Build a Serverless Golang Function with OpenFaaS

发表评论

电子邮件地址不会被公开。 必填项已用*标注