OpenFaaS - 以自己的方式运行容器化函数

OpenFaaS - 以自己的方式运行容器化函数

译者注: 本文篇幅较长,有助于了解 FaaS 和 OpenFaaS。作者分别从开发人员和运维人员的视角来了解 OpenFaaS,对了解新的技术是个很好的方式。

本文翻译自 Ivan VelichkoOpenFaaS - Run Containerized Functions On Your Own Terms


长期以来,无服务器(serverless) 对我来说无非就是 AWS Lambda 的代名词。Lambda 提供了一种方便的途径,可以将任意代码附加到平台事件(云实例的状态变更、DynamoDB 记录的更新或新的 SNS 消息)中。但是,我时不时会想到某个逻辑,但其又没大到足以有自己的服务,同时有不适合任何现有服务的范围。因此,我经常将其放入函数中,以便日后使用 CLI 命令或者 HTTP 调用来调用它。

几年前,我来开了 AWS,自那以后,我一直怀念部署无服务器功能的便利性。因此,当我得知 OpenFaaS 项目时惊喜万分。它将在 Kubernetes 集群上部署函数变得简单,甚至仅需要 Containerd 就可以部署到虚拟机上。

有兴趣?那么继续!

无服务器与 FaaS

无服务器 已成为一个流行词,目前其实际含义扔不够清晰。

许多现代平台被视为 无服务器 平台。在 AWS Fargate 或 GCP Cloud Run 上部署容器化服务?无服务器!在 Heroku 上运行应用程序?也可能是无服务器的。

同时,我更喜欢将 FaaS 视为一种具体的设计模式。按照 FaaS 范式,可以部署代码片段(响应某些外部时间执行的函数)。这些函数与事件驱动程序中的回调类似,但是是运行在其他人的的服务器上。由于操作的是函数而不是服务器,顾名思义 FaaS 是无服务器的。

source

OpenFaaS 项目旨在将 Kubernetes 集群或者独立的虚拟机等低级基础设施转化为管理无服务器函数的高级平台。

站在开发人员的角度,这样一个平台看起来是真的无服务器的 – 你只需要知道特定的 CLI/UI/API 来处理 函数 抽象。但站在运维的角度,需要了解 OpenFaaS 如何使用 服务器 来运行这些函数。

就我而言,我经常既是开发又是运维,下面我将尝试从二者展开说明。然而,我认为在评估 UX 时,我们应该明确区分它们。

开发人员眼中的 OpenFaaS

OpenFaaS 创建于 2016 年,现在网上也有大量的教程。这里不会重复介绍,但可以通过以下链接了解:

相反,我将描述我所理解的 OpenFaaS。我希望有助于一些需要评估该技术是否解决其问题的人,以及那些希望更有效地使用该技术的人。

函数运行时

在进入正式编码之前,有必要了解下其未来的执行环境(又名运行时)。或者,简单说:

  • 如何启动函数
  • 如何组织 I/O 操作
  • 如何重置 / 终止函数
  • 如何隔离函数和调用

OpenFaaS 自带多个运行时模式,这些模式针对不同的场景定制。因此,不同的场景下上述问题的答案会略有不同。

OpenFaaS 函数在容器中运行,并且每个容器必须遵守简单的约定 :它作为监听在预设端口(默认为 8080)上的 HTTP 服务器,临时存储并且是无状态的。

然而,OpenFaaS 通过 函数 watchdog(译者注:watchdog 不做翻译)模式避免了用户编写此类服务器。函数 watchdog 是一种轻量级 HTTP 服务器,可以感知如何执行实际函数业务逻辑。因此,安装在容器中的所有内容加上作为入口点的 watchdog,就构成了函数的运行时环境。

经典 watchdog

最简单的开始,或者又是被称为经典 watchdog

这种模式下,watchdog 启动了监听在 8080 端口的轻量级 HTTP 服务器,每个进来的请求都会:

  • 读取请求头和请求体
  • fork 或者 exec 包含实际函数的可执行文件
  • 将请求头和请求体写入到函数进程的 stdin
  • 等待函数进程的退出(或者超市)
  • 读取函数进程的 stdoutstderr
  • 在 HTTP 响应中将去读的字节发送回调用方

上述逻辑类似于传统的 通用网关接口(CGI)。一方面,每次函数调用都启动单独的进程看起来不够高效,而另一方面,它确实超级方便,因为 任何使用 stdio 流进行 I/O 处理的程序(包括最喜欢的 CLI 工具)都可以部署为 OpenFaaS 函数

提起隔离,我们有必要区分下函数调用

  • OpenFaaS 中的不同函数始终分布在不同的容器中
  • 一个函数可以有一个或多个容器 —— 取决于缩放选项
  • 同一函数的独立调用可能会最终进入同一个容器
  • 同一函数的独立调用将始终使用不同的进程进行

反向代理 watchdog

注意:使用 OpenFaaS 官方术语,本节讨论在 HTTP 模式下运行的 of-watchdog。但我个人认为称之为反向代理 watchdog 更加形象。

如果 经典 运行时类似于 CGI,那么这个运行时模式类似于后来的 FastCGI。运行时希望在 watchdog 后面有一个长期运行的 HTTP 服务器,而不是每次函数调用时创建新的进程。这本质上是 将 watchdog 组件变成反向代理

当容器启动时,反向代理 watchdog 也会创建一个监听在 8080 端口的轻量级 HTTP 服务器。然而,与 经典 watchdog 不同的是反向代理watchdog 只创建一次函数的进程,并将其当成(长期运行的)上游服务器。然后,函数调用转变成到该上游的 HTTP 请求。

然而,反向代理模式并不为了取代经典模式。经典模式的强项在于其函数的编写非常简单。这也是没有 HTTP 服务器的代码的唯一选择。比如使用 Cobol、bash 或者 PowerShell 脚本等等编写的函数。

何时该使用反向代理运行时模式:

  • 函数需要在两次调用之间保持状态:
    • 缓存
    • 持久连接(例如,保持从函数到数据库的连接打开)
    • 有状态函数 🥴
  • 每个函数启动一个进程可能开销很大,为每个调用带来了延迟
  • 你想运行一个(微)服务作为函数 🤔

根据 OpenFaaS 的创建者 Alex Ellis 的解释,FaaS,特别是 OpenFaaS,可以被视为在不依赖服务器抽象的情况下 部署微服务的简化方式。即 FaaS 是无服务器架构的规范示例。

因此,使用反向代理的方式,函数可以被看作是部署微服务的固执的方式。方便、快速、简单。但使用有状态函数时,要留意由于多个调用可能在同一个进程中结束而导致的警告:

  • 在一个进程中结束的并发调用可能会触发代码中的竞争条件(例如,一个带有全局变量的 Go 函数,而全局变量没有锁的保护)。
  • 在一个进程中结束的后续调用可能会导致交叉调用数据泄露(当然,就像传统微服务一样)。
  • 由于该进程在两次调用之间被复用,因此代码中的任何内存泄漏都不会被缓解。

其他运行时模式

经典运行时模式在将函数结果发送回调用方之前缓冲了函数的整个响应。但如果响应的大小超出了容器的内存怎么办?OpenFaaS 提供了响应流另一种运行时模式,该模式仍然为每个调用创建进程,但添加了

另一个又去的场景是从函数中提供静态文件服务。OpenFaaS 也有解决方案

这可能是所有的内置运行时模式。但如果仍未满足需求,OpenFaaS 是一个开源项目!看下现有的watchdog(1 & 2),简洁明了。因此,可以随时提交 PR 或者 issue,让整个社区因你的贡献收益。

编写函数

此时,我们已经了解函数如何在配备了函数 watchdog 的容器中运行。那么最小的函数是什么样子的?

下面的示例将简单的 shell 脚本封装到 OpenFaaS 函数中:

########################################################
# WARNING: Not for Production - No Security Hardening! #
########################################################

# This FROM is just to get the watchdog executable.
FROM ghcr.io/openfaas/classic-watchdog:0.2.0 as watchdog

# FROM this line the actual runtime definion starts.
FROM alpine:latest

# Mandatory step - put the watchdog.
COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog

# Optionally - install extra packages, libs, tools, etc.

# Function's payload - script echoing its STDIN, a bit transformed.
RUN echo '#!/bin/sh' > /echo.sh
RUN echo 'cat | rev | tr "[:lower:]" "[:upper:]"' >> /echo.sh
RUN chmod +x /echo.sh

# Point the watchdog to the actual thingy to run.
ENV fprocess="/echo.sh"

# Start the watchdog server.
CMD ["fwatchdog"]

当构建、部署和调用时,上面的函数作为 回显服务器,倒转并大写其输入。

稍微高级点的例子:一个 Node.js Hello World 脚本作为函数:

########################################################
# WARNING: Not for Production - No Security Hardening! #
########################################################

FROM ghcr.io/openfaas/classic-watchdog:0.2.0 as watchdog

FROM node:17-alpine

COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog

RUN echo 'console.log("Hello World!")' > index.js

ENV fprocess="node index.js"

CMD ["fwatchdog"]

因此,要编写一个简单的函数,只需要在 Dockerfile 中加入:

  • 实际脚本(或可执行文件)
  • 它的所有依赖项——软件包、操作系统库等
  • 首选的 watchdog

然后将 watchdog 指向该脚本(或可执行文件),并将 watchdog 作为入口。有点酷,因为:

  • 可以完全控制函数未来的运行时
  • 可以部署任何可以在容器中作为函数运行的东西

但上述方法有个明显的缺点 – 一个生产就绪的 Dockerfile 可能有上百行。如果我只想运行一个简单的 Node.js/Python 脚本或者一个小的 Go 程序作为函数,要怎么处理 Dockerfile?就不能有一个占位符来粘贴代码片段?

功能模板

OpenFaaS 的美妙之处在于,我们可以两者兼而有之 —— 使用 Dockerfile 进行低级修补或目标语言编写高级脚本!得益于丰富的功能模板库!

$ faas-cli template store list
NAME                     SOURCE             DESCRIPTION
csharp                   openfaas           Classic C# template
dockerfile               openfaas           Classic Dockerfile template
go                       openfaas           Classic Golang template
java8                    openfaas           Java 8 template
...
node14                   openfaas           HTTP-based Node 14 template
node12                   openfaas           HTTP-based Node 12 template
node                     openfaas           Classic NodeJS 8 template
php7                     openfaas           Classic PHP 7 template
python                   openfaas           Classic Python 2.7 template
python3                  openfaas           Classic Python 3.6 template
...
python3-flask            openfaas           Python 3.7 Flask template
python3-http             openfaas           Python 3.7 with Flask and HTTP
...
golang-http              openfaas           Golang HTTP template
...

上述功能模板由 OpenFaaS 作者和社区精心只做。典型的模板附带一个复杂的 Dockerfile,指向虚拟处理程序函数。当引导新函数时,通过 faas-cli new 命令来使用这些模板。例如:

$ faas-cli new --lang python my-fn
Folder: my-fn created.
Function created in folder: my-fn
Stack file written: my-fn.yml

$ cat my-fn/handler.py
def handle(req):
    """ PUT YOUR BUSINESS LOGIC HERE """
    return req

因此,对于模板,编写函数的工作可以归结为简单地将业务逻辑放入响应的处理程序文件中。

使用模板时,了解使用那种 watchdog模式 很重要:

  • 使用经典的类似 CGI 的 watchdog,处理程序通常被编写为接受和返回纯字符串的函数(例如:python3php7
  • HTTP 模式下,使用 of-watchdog时,处理程序看起来更像 HTTP 处理程序接受请求并返回响应结构(例如:python3-httpnode17)。

函数商店

你的最佳函数是什么?对,你不需要写。OpenFaaS 接受这种想法,并带来了函数商店(经过社区测试并根据过往经验选择的 OpenFaaS 函数精选索引)。

该商店包含一些有趣的函数,可以一键部署到现有的 OpenFaaS 中:

$ faas-cli store list

FUNCTION                        DESCRIPTION
NodeInfo                        Get info about the machine that you'r...
alpine                          An Alpine Linux shell, set the "fproc...
env                             Print the environment variables prese...
sleep                           Simulate a 2s duration or pass an X-S...
shasum                          Generate a shasum for the given input
Figlet                          Generate ASCII logos with the figlet CLI
curl                            Use curl for network diagnostics, pas...
SentimentAnalysis               Python function provides a rating on ...
hey                             HTTP load generator, ApacheBench (ab)...
nslookup                        Query the nameserver for the IP addre...
SSL/TLS cert info               Returns SSL/TLS certificate informati...
Colorization                    Turn black and white photos to color ...
Inception                       This is a forked version of the work ...
Have I Been Pwned               The Have I Been Pwned function lets y...
Face Detection with Pigo        Detect faces in images using the Pigo...
Tesseract OCR                   This function brings OCR - Optical Ch...
Dockerhub Stats                 Golang function gives the count of re...
QR Code Generator - Go          QR Code generator using Go
Nmap Security Scanner           Tool for network discovery and securi...
ASCII Cows                      Generate a random ASCII cow
YouTube Video Downloader        Download YouTube videos as a function
OpenFaaS Text-to-Speech         Generate an MP3 of text using Google'...
Docker Image Manifest Query     Query an image on the Docker Hub for ...
face-detect with OpenCV         Detect faces in images. Send a URL as...
Face blur by Endre Simo         Blur out faces detected in JPEGs. Inv...
Left-Pad                        left-pad on OpenFaaS
normalisecolor                  Automatically fix white-balance in ph...
mememachine                     Turn any image into a meme.
Business Strategy Generator     Generates a Business Strategy (using ...
Image EXIF Reader               Reads EXIF information from an URL or...
Open NSFW Model                 Score images for NSFW (nudity) content.
Identicon Generator             Create an identicon from a provided s...

这些函数实际上是存储在 Docker Hub 或者 Quay 等公共库的容器镜像,可以自由复用。

场景示例:

  • 使用 env 函数调试函数接收的HTTP标头
  • 使用 curl 函数从 OpenFaaS 部署内部测试连接
  • 从运行多个副本的函数中使用 hey 来增加负载

函数的构建和部署

由于函数是在容器中运行的,因此需要有人为这些容器构建镜像。无论你喜不喜欢,这都是开发人员的事情。OpenFaaS 提供了方便的 faas-cli build 命令,但没有服务器端构建。因此,要么需要(在安装 Docker 的机器上)手动运行 faas-cli build,要么使用 CI/CD 完成。

接下来,构建好的镜像需要通过 faas-cli push 到仓库。显然,这种仓库也应该可以从 OpenFaaS 服务器端访问。否则,使 用faas-cli deploy 部署函数时会失败。

开发人员的工作流程如下:

调用函数

函数部署后,可以通过向 $API_HOST:$API_PORT/function/<fn-name> 端点发送 GET、POST、PUT 或者 DELET HTTP 请求来调用它。常见的调用方式有:

  • 各种钩子(webhook)
  • faas-cli invoke
  • event connectors

前两个选项相当简单。使用函数作为作为 webhook 处理器(GitHub、IFTTT 等)很方便,每个函数开发人员都已经安装了 faas-cli,因此可以成为日常脚本编写的组成部分。

那什么是事件连接器?

在本文开头是我对 AWS Lambda 与 AWS 平台事件紧密集成的温暖回忆。请记住,可以在响应新 SQS/SNS 消息、新的 Kinesis 记录、EC2 实例生命周期等事件时调用 Lambda。OpenFaaS 函数是否存在类似的东西呢?

显然,OpenFaaS 无法开箱即用地与任何生态系统集成。然而,它提供了一种名为事件连接器模式的通用解决方案。

官方支持的连接器:

OpenFaaS 还提供了很小的连接器-sdk库来简化连接器的开发。

运维眼中的 OpenFaaS

开发眼中的 OpenFaaS 是个黑盒,提供简单的 API 来部署和调用函数。然而,作为运维可能会从了解 一点 OpenFaaS 内部原理中受益。

OpenFaaS 通用架构

OpenFaaS 有一个简单但强大的架构,允许使用不同的基础设施作为后端。如果已经有了 Kubernetes 集群,可以通过在上面部署 OpenFaaS 轻松将其变成 FaaS 解决方案。但是如果旧的虚拟(或物理)机,仍然可以在上面安装 OpenFaaS,并获得差不多功能的更小的 FaaS 解决方案。

上面的架构中唯一面向用户的组件是 API 网关。OpenFaaS 的 API 网关:

  • 暴露 API 来管理和调用函数
  • 提供内置的 UI 来管理函数
  • 处理函数的自动缩放
  • 预计后面会有兼容的 OpenFaaS 提供商

因此,当开发人员运行 faas-cli deployfaas-cli list 或使用 curl $API_URL/function/foobar 调用函数等内容时,请求将发送到上述的 API 网关。

上图中的另一个重要组成部分是  faas-provider。它不是一个具体的组件,而更像是接口。任何实现(非常简洁)的提供商 API 的软件都可以成为提供商。OpenFaaS 提供商:

  • 管理功能(部署、列表、缩放、删除)
  • 调用函数
  • 暴露一些系统信息

两个最注明的提供商是 faas-netes(Kubernetes 上的 OpenFaaS)和 faasd(Containerd 上的 OpenFaaS)。下面,将介绍他们的实现。

Kubernetes 上的 OpenFaaS(faas-nets)

当部署在 Kubernetes 上时,OpenFaaS 利用了该平台开箱即用的强大原语。

关键要点:

  • API 网关成为标准(部署+服务)对。因此,可以随心所欲地扩展它。也可以随心所欲地把它暴露出来
  • 每个函数也成为(部署+服务)对。可能不会直接处理函数,但对于 faas-netes,缩放变得就像调整相应的副本数一样简单
  • 高可用性和开箱即用的水平缩放 - 同一功能的 pod 可以(而且应该)跨多个集群节点运行。
  • Kubernetes 作为一个数据库工作;例如,当运行 faas-cli list 等命令来获取当前部署的函数列表时,faas-netes 只会将其转换为相应的 Kubernetes API 查询

Containerd 上的 OpenFaaS(faasd)

对于没有使用 Kubernetes 集群的人来说,OpenFaaS 提供了名为 faasd 的替代轻量级提供商。它可以安装在(虚拟或物理)服务器上,,并利用 containerd 来管理容器。正如我之前写的那样,容器是一个在 Docker 和 Kubernetes 下使用的较低级别的容器管理器。结合 CNI 插件,它成为编写容器调度器的构建组件,OpenFaaS 的 faasd 是个很好的讲究案例:

关键要点:

  • 被设计为在 IoT 设备上或 VM 中运行
  • 使用 containerd 的原生 pause (通过cgroup freezers)和超快速函数冷启动快速扩展到零
  • 它可以在每台服务器上运行比 faas-netes 多十倍的函数,并且可以有效地使用更便宜的硬件,包括树莓派
  • containerd 和 faasd 作为 systemd 服务进行管理,因此会自动处理日志、重启等
  • 没有 Kubernetes DNS,但 faasd 确保 DNS 在函数之间共享以简化互操作
  • containerd 扮演着数据库的角色(比如 faas-cli list 变成了类似 ctr container list的操作 ),所以如果服务器挂了,所有状态就会丢失,每个函数都需要重新部署
  • 没有开箱即用的高可用性或水平扩展(参见 issue/225

总结

对你所使用的软件有个好的心智模型是是有益的,它可以提高开发效率,防止单例场景的发生,并简化了故障排查。

以 OpenFaaS 为例,区分开发人员和运维人员对系统的看法可能是个很好的思路。从开发人员角度来看,这是一个简单而强大的无服务器解决方案,主要关注 FaaS 场景。该解决方案由一个用于管理和调用函数的简洁 API、一个涵盖开发人员工作流的命令行工具以及一个函数模板库组成。无服务器函数以不同的运行时模式(类 CGI、反向代理)在容器中运行,并提供不同的隔离和状态保障。

运维人员的角度来看,OpenFaaS 是一个具有灵活架构的模块化系统,可以部署在不同类型的基础设施之上:从树莓派到裸机或虚拟机、以及成熟的 Kubernetes、OpenShift 或 Docker Swarm 集群。当然,每种选择都有其优缺点,需要详细评估取舍。但即使现有选项都不合适,简单的 faas-provider 抽象允许开发自己的后端来运行无服务器功能。

上述内容主要集中在 OpenFaaS 基础知识上。但是 OpenFaaS 也有一些高级功能。通过以下链接进一步了解:

资源

作者介绍: Ivan Velichko Software Engineer at heart, SRE at day job, Tech Storyteller at night.

comments powered by Disqus