原文:
annas-archive.org/md5/fc665ee4d43ecedc83fe58ea86b72525
译者:飞龙
前言
容器技术今天已经非常成熟。Docker 作为帮助普及容器的软件包,现在已经被成千上万的开发人员作为日常 DevOps 工具使用。我可以说,Docker 容器引擎现在已经变成了一款无聊的软件。对于一个基础设施级的软件包而言,“无聊”意味着高质量和稳定性。我每天都在使用它,而我知道你们也将它作为工具链的一部分。但我们不再对 Docker 的新版本发布感到兴奋。就像我们对 Linux 内核发布的感受一样。带着这样的感觉,我认为容器的黄金时代已经在不久前结束。
Docker 的崛起发生在 2013 年。它的“文艺复兴”时期是在 2014 到 2016 年之间。2016 年,Docker Swarm 和 Kubernetes 之间的许多编排引擎竞争达到了巅峰。Swarm2K 项目曾是我一生中难得的经历。Docker 后来在 2017 年宣布也将支持 Kubernetes。这场竞赛也就此结束。
几天前,即 2018 年 3 月,就在本书即将出版之际,Docker 的创始人 Solomon Hykes 离开了 Docker Inc.。Docker 这家公司一直在缓慢而强烈地从初创公司向企业业务转型。这对我们意味着什么?企业意味着稳定,而初创公司意味着冒险。让我们迈向新的冒险——容器之后的无服务器时代。
本书讨论的主题是无服务器。它是容器和微服务之后的自然进化,方式各异。首先,Docker 容器成为了函数的部署单元,成为函数即服务(FaaS)架构中的一个基本工作单元。其次,微服务架构正在逐步演变成 FaaS 架构。FaaS 实际上可以部署在本地或云端。当整个 FaaS 堆栈由云服务提供商管理时,它就变成了完全的无服务器架构。
但是,在这之间会有一些东西。那就是混合无服务器 FaaS 架构。这种架构是我希望读者在本书中发现并享受的核心思想。它是我们在成本、自行管理服务器以及服务器控制程度之间取得平衡的一个点。
本书详细介绍了三个主要的 Docker FaaS 平台,分别是OpenFaas、OpenWhisk 和Fn Project。这些项目都处于早期阶段,并且正在积极成熟。因此,对于读者和我来说,这是一个很好的机会,可以一起学习并迎接这一新的浪潮。让我们一起努力。
本书适合的人群
如果你是一个开发人员、Docker 工程师、DevOps 工程师,或者任何对在无服务器环境中使用 Docker 感兴趣的相关人员,那么本书适合你。
如果你是本科生或研究生,本书同样适合你,用来加强你在无服务器和云计算领域的知识。
本书内容
第一章,无服务器与 Docker,介绍了无服务器和 Docker。我们将在本章中找到它们之间的关系。我们还将学习通过研究多个 FaaS 平台架构总结出的常见架构。到本章结束时,我们将学会如何在所有三个 FaaS 平台——OpenFaaS、The Fn Project 和 OpenWhisk 上实现“Hello World”。
第二章,Docker 与 Swarm 集群,回顾了容器技术、命名空间和 cgroups。然后,我们将介绍 Docker,包括如何安装它,如何使用其基本命令,并理解它的构建、分发和运行工作流。进一步地,我们将回顾 Docker 内置的编排引擎——Docker Swarm。我们将学习如何设置集群并了解 Docker Swarm 的内部工作原理。接着,我们将学习如何设置 Docker 网络、将其附加到容器,并了解如何在 Docker Swarm 中扩展服务。
第三章,无服务器框架,讨论了无服务器框架,包括 AWS Lambda、Google Cloud Functions、Azure Functions 和 IBM Cloud Functions 等平台。我们将在本章结束时介绍一个与 FaaS 平台无关的框架——无服务器框架。
第四章,Docker 上的 OpenFaaS,解释了如何使用 OpenFaaS。我们将探索它的架构和组件。接着,我们将学习如何使用其提供的工具和模板来准备、构建和部署函数,如何在 Swarm 之上准备其集群,如何使用其用户界面,以及 OpenFaaS 如何利用 Docker 多阶段构建。我们还将讨论如何使用 Prometheus 来监控 FaaS 平台。
第五章,Fn 项目,探索了另一个 FaaS 平台。类似于第四章,Docker 上的 OpenFaaS,我们将从其架构和组件开始,然后通过一组 CLI 命令来构建、打包和部署函数到 Fn 平台。本章后续部分,我们将学习如何使用其内置 UI 来监控该平台。此外,我们还将使用一个熟悉的工具来帮助分析其日志。
第六章,Docker 上的 OpenWhisk,讨论了 OpenWhisk,这是本书中的第三个也是最后一个 FaaS 平台。我们将了解它的概念和架构。
第七章,操作 FaaS 集群,讨论了使用 Docker Swarm 准备和操作生产级 FaaS 集群的几种技术。我们将讨论如何用另一种易于使用的容器网络插件替代整个网络层。我们还将展示如何实现新的路由网格机制,以避免当前入口实现中的漏洞。此外,我们将讨论一些高级话题,如 分布式追踪 及其实现方法。我们甚至会涵盖通过使用竞价实例来降低成本的概念,并展示如何在这个动态基础设施上实现 Swarm。
第八章,将它们汇总起来,解释了如何实现一个异构的 FaaS 系统,结合所有三个 FaaS 平台在一个强大的产品级 Swarm 集群中无缝运行。我们将展示一个基于移动端的银行转账用例,同时包括一个遗留包装器、一个移动后端 WebHook,以及使用 FaaS 的流数据处理。这里的附加亮点是,我们还为用例添加了区块链,展示它们的互操作性。
第九章,无服务器的未来,通过先进的概念和研究原型实现来总结本书,这些内容超越了当前的无服务器和 FaaS 技术。
如何充分利用本书
读者应该了解 Linux 和 Docker 命令的基础知识。虽然这不是强制性的,但如果读者对网络协议有基本了解并且对云计算概念有所熟悉,将会是一个很大的加分项。
尽管可以使用 MacBook 或 Windows 操作系统的 PC 来运行本书中的示例,但强烈建议读者使用 Ubuntu Linux 16.04 及以上版本。使用 MacBook 或 Windows 的读者可以通过虚拟机上的 Linux 或云实例来运行示例。
下载示例代码文件
您可以从您在 www.packtpub.com 的账户下载本书的示例代码文件。如果您是在其他地方购买的本书,您可以访问 www.packtpub.com/support 并注册,文件将直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
请在 www.packtpub.com 登录或注册。
-
选择 SUPPORT 选项卡。
-
点击 Code Downloads & Errata。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
下载完成后,请确保使用最新版本的工具解压或提取文件夹:
-
Windows 用 WinRAR/7-Zip
-
Mac 用 Zipeg/iZip/UnRarX
-
7-Zip/PeaZip for Linux
本书的代码包也托管在 GitHub 上,地址是 github.com/PacktPublishing/Docker-for-Serverless-Applications
。如果代码有更新,GitHub 上的现有代码库也会进行更新。
我们的丰富书籍和视频目录中还有其他代码包,您可以访问 github.com/PacktPublishing/
。快来看看吧!
使用的约定
本书中使用了多种文本约定。
文本中的代码
: 表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。示例:“我们将尝试使用echoit
函数输出hello world
,并使用 OpenFaaS。”
代码块如下所示:
FROM ubuntu
RUN apt-get update && apt-get install -y nginx
EXPOSE 80
ENTRYPOINT ["nginx", "-g", "daemon off;"]
任何命令行输入或输出都按如下方式书写:
$ curl -sSL https://get.docker.com | sudo sh
$ docker swarm init --advertise-addr=eth0
粗体: 表示新术语、重要词汇或屏幕上显示的词汇。例如,菜单或对话框中的词语会以这种方式显示。示例:“以下截图展示了浏览器运行 OpenFaaS Portal。”
警告或重要说明以这种方式呈现。
提示和技巧以这种方式呈现。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 发送电子邮件至 feedback@packtpub.com
,并在邮件主题中注明书名。如果您对本书的任何部分有疑问,请通过 questions@packtpub.com
联系我们。
勘误表: 尽管我们已尽力确保内容的准确性,但难免会出现错误。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误表提交”链接,并填写相关信息。
盗版: 如果您在互联网上发现我们作品的非法复制品,无论形式如何,我们将非常感激您提供其位置地址或网站名称。请通过 copyright@packtpub.com
联系我们,并附上相关材料的链接。
如果你有兴趣成为作者: 如果你在某个主题方面有专长,且有意撰写或参与书籍的编写,请访问 authors.packtpub.com。
评论
请留下评论。阅读并使用本书后,不妨在您购买书籍的网站上留下评论。潜在读者可以通过您的无偏见评价做出购买决策,我们可以了解您对我们的产品的看法,作者也能看到您对其书籍的反馈。谢谢!
有关 Packt 的更多信息,请访问 packtpub.com。
第一章:无服务器与 Docker
说到容器,大多数人已经知道如何将应用程序打包成容器作为部署单元。Docker 允许我们以其 事实标准 格式将应用程序部署到几乎所有地方,从我们的笔记本电脑、QA 集群、客户站点,甚至是公共云,如下图所示:
图 1.1:将 Docker 容器部署到各种基础设施和平台
如今,在公共云上运行 Docker 容器已被视为常态。我们已经从按需启动云实例、按需付费的账单中受益。无需等待硬件购买,我们还可以通过敏捷方法并使用持续交付管道来更快地优化资源。
根据 Docker 的一份报告,总拥有成本(TCO)在其客户使用 Docker 将现有应用程序迁移到云端时减少了 66%。不仅可以大幅降低 TCO,使用 Docker 的公司还可以将上市时间从几个月缩短为几天。这是一个巨大的胜利。
将容器部署到云基础设施,如 AWS、Google Cloud 或 Microsoft Azure,已经简化了许多事情。云基础设施使组织无需购买自己的硬件,也不需要专门的团队来维护这些硬件。
然而,即使在使用公共云基础设施时,组织仍然需要某些角色,例如架构师,来负责站点可靠性和可扩展性。一些人被称为 SRE,即 站点可靠性工程师。
此外,组织还需要处理系统级的软件包和依赖项。由于软件堆栈会不断变化,他们需要自己进行应用程序安全性和操作系统内核的修补。在许多场景中,这些组织中的团队必须根据负载峰值的需求,意外地扩展集群的规模。此外,工程师还需要尽可能地将集群缩小,以减少云计算费用,因为这是一种按需付费的模式。
开发人员和工程团队总是努力提供出色的用户体验和站点可用性。在此过程中,按需实例的过度配置或低效利用可能会带来高昂的成本。根据 AWS 的一份白皮书,d0.awsstatic.com/whitepapers/optimizing-enterprise-economics-serverless-architectures.pdf
,低效利用的实例多达已配置机器的 85%。
无服务器计算平台,如 AWS Lambda、Google Cloud Functions、Azure Functions 和 IBM Cloud Functions,旨在解决这些过度配置和低效利用的问题。
本章将涵盖以下主题:
-
无服务器
-
无服务器 FaaS 的常见架构
-
无服务器/FaaS 使用案例
-
Hello world,FaaS/Docker 方式
什么是无服务器?
试着想象我们生活在一个完全由智能软件驱动的世界。
那将是一个我们可以在不做任何事情的情况下开发软件的世界。只需要说出我们希望运行什么样的软件,几分钟后,它就会出现在互联网上,为许多用户提供服务。我们只需要为用户发出的请求付费。嗯,那样的世界太不真实了。
现在,让我们更加现实一点,想象一个我们仍然需要自己开发软件的世界。至少目前,我们不需要关心任何服务器的配置和管理。实际上,这至少是一个对开发者来说最好的世界,我们可以将应用部署到数百万用户面前,而无需担心任何服务器,甚至不需要知道这些服务器在哪里。我们唯一真正想要的,是创建一个能够按规模解决业务需求、价格合理的应用。无服务器平台就是为了解决这些问题而创建的。
作为对开发者和快速增长企业的回应,无服务器平台似乎是一个巨大的胜利。但它们究竟是什么?
无服务器与 FaaS 之间的关系
下图展示了事件驱动编程、FaaS 和无服务器 FaaS 的位置关系,其中无服务器 FaaS 是 FaaS 和无服务器之间的交集区域:
图 1.2:展示无服务器与 FaaS 之间关系的维恩图
无服务器是一种范式转变,使得开发者不再需要担心服务器的配置和运营。计费方式是按请求计费。此外,公共云上有许多有用的服务供我们选择,我们可以将它们连接起来并用来解决业务问题,从而完成任务。
无服务器架构中的应用通常使用第三方服务来完成其他任务,如身份验证、数据库系统或文件存储。虽然无服务器应用不一定需要使用这些第三方服务,但以这种方式架构应用能够充分利用基于云的无服务器平台。这种架构中的前端应用通常是一个厚重、强大的前端,例如单页面应用或移动应用。
这一无服务器计算转变的执行引擎是 Function as a Service 或 FaaS 平台。FaaS 平台是一种计算引擎,允许我们编写一个简单、自包含、单一目的的函数来处理或计算任务。FaaS 平台的计算单元是一个推荐无状态的函数。这个无状态属性使得函数可以被平台完全管理和扩展。
FaaS 平台不一定要运行在无服务器环境中,比如 AWS Lambda,但也有许多 FaaS 实现,比如 OpenFaaS、Fn 项目和 OpenWhisk,允许我们在自己的硬件上部署和运行 FaaS。如果 FaaS 平台运行在无服务器环境中,它将被称为无服务器 FaaS。例如,我们在本地运行 OpenWhisk,那么它就是我们的 FaaS 平台。但当它在 IBM Cloud 上运行作为 IBM Cloud Functions 时,它就是一个无服务器 FaaS。
每个 FaaS 平台都被设计为使用事件驱动的编程模型,以便能够高效地连接到公共云上的其他服务。通过异步事件模型和函数的无状态特性,这种环境使得无服务器 FaaS 成为下一代计算的理想模型。
无服务器 FaaS 的缺点
那么这种方法的缺点是什么呢?它们如下:
-
我们基本上不拥有服务器。 当我们需要对基础设施进行细粒度控制时,无服务器模型并不适用。
-
无服务器 FaaS 有很多限制,尤其是函数执行的时间限制,以及每个函数实例的内存限制。它还引入了一种固定且特定的应用程序开发方式。可能直接将现有系统迁移到 FaaS 会有些困难。
-
如果不允许将所有工作负载迁移出组织,那么在私有或混合基础设施上完全使用无服务器平台是不可能的。无服务器架构的一个真正好处是云上存在便捷的公共服务。
Docker 来救援
本书讨论了我们自己的基础设施上的 FaaS 与无服务器 FaaS 之间的平衡。我们尝试通过选择三个主要的 FaaS 平台来简化和统一 FaaS 的部署模型,这些平台允许我们将 Docker 容器作为函数部署,且我们在本书中会详细讨论这些平台。
以 Docker 容器作为部署单元(函数),Docker 作为开发工具,Docker 作为编排引擎和网络层,我们可以开发无服务器应用程序,并将其部署在我们可用的硬件上,部署在我们自己的私有云基础设施上,或是一个混合云,将我们的硬件与公共云的硬件混合在一起。
其中一个最重要的点是,使用具备 Docker 技能的小型开发团队足够轻松地管理这种基础设施。
回顾一下前面的图 1.2。如果你在阅读完本章后有所启发,让我们猜测一下这本书将讨论的内容。我们应该处于这个图中的哪个位置呢?答案将在本章结束时揭晓。
无服务器 FaaS 的常见架构
在进入其他技术章节之前,本书在撰写过程中对至少六个无服务器 FaaS 平台的常见架构进行了调查和研究,结果如图所示。这是现有 FaaS 平台的提炼概述,如果你想创建一个新的平台,它是一个推荐的架构:
图 1.3:描述 FaaS 平台常见架构的框图
系统层
从下到上的架构描述如下:
-
我们有一些物理或虚拟机器。这些机器可以位于公有云或私有云中。有些可能是位于防火墙内的物理设备,或者位于组织内部。它们可以混合在一起,作为混合基础设施。
-
下一层是 操作系统,当然还有内核。我们需要一个支持容器隔离的现代内核操作系统,例如 Linux,或者至少兼容 runC。Windows 或 Windows Server 2016 拥有基于 Hyper-V 的隔离,兼容 Docker。
-
架构中的下一层是 容器运行时(系统级)。我们强调它是系统级容器运行时,因为它并不是用来直接运行 FaaS 函数的。这个层级负责为集群提供服务。
-
接下来是可选的容器编排引擎,或 容器编排器 层。这个层级包括 Docker Swarm 或 Kubernetes。本书中我们使用 Docker Swarm,但你可能会发现本书介绍的一些 FaaS 平台并未使用任何编排工具。基本上,只有 Docker 和容器网络就足够让 FaaS 平台有效启动和运行。
FaaS 层
现在,我们将讨论实际的 FaaS 层。我们将从左到右展开讨论:
-
整个架构的前沿组件是 FaaS 网关。在一些实现中,网关是可选的,但在许多实现中,这个组件有助于提供 HTTPS 服务并缓存一些静态内容,例如平台的 UI 部分。网关实例有助于提高吞吐量。它通常是一个无状态的基于 HTTP 的反向代理。因此,这个组件易于扩展。
-
启动器 是 FaaS 最重要的组件之一。启动器负责模拟实际的调用请求给平台的其他部分。在 OpenWhisk 中,这个组件被称为 控制器,例如。在 Fn 中,其 Fn 服务器内部的部分充当 启动器。
-
消息总线 是 FaaS 平台的消息骨干。一些没有此组件的架构在实现异步调用或重试模式时会遇到困难,进而影响平台的鲁棒性。消息总线将启动器与执行器解耦。
-
执行器是执行真正函数调用的组件。它连接到自己的容器运行时(应用级别),启动真正的函数执行顺序。所有结果和日志将被写入中央日志存储。
-
日志存储是平台的唯一真相来源。它应该设计成存储几乎所有内容,从函数活动到每次调用的错误日志。
-
容器运行时(应用级别)是负责启动函数容器的组件。在本书中,我们简单地使用 Docker 及其底层引擎作为运行时组件。
Serverless/FaaS 的使用案例
Serverless/FaaS 是一种通用的计算模型。因此,几乎可以使用这种编程范式实现任何类型的工作负载。Serverless/FaaS 的使用案例可以从常规 Web 应用的 API、移动应用的 RESTful 后台、日志或视频处理的函数、WebHook 系统的后台,到流数据处理程序等。
图 1.4:演示项目的框图
在第八章,将它们整合在一起,我们将讨论一个如前图所示的系统,并涵盖以下使用案例:
-
WebHook 系统的 API:在前图中,你可能看到UI 的后台。该系统允许我们定义一个 WebHook,并将其实现为 FaaS 函数,使用本书后面章节中讨论的某个框架。
-
用于封装遗留系统的 API:在前图的右上角,我们会看到一组函数连接到Chrome Headless(一个功能完备的运行中的 Google Chrome 实例)。该函数将一组命令封装起来,指示 Google Chrome 为我们操作遗留系统。
-
APIs 作为其他服务的抽象:在右下角有两个简单的模块,第一个是运行在 FaaS 平台上的函数,连接到第二个模块,Mock Core Bank System,它是一个更复杂的 REST API。这个系统部分展示了如何使用 FaaS 函数作为抽象层,简化复杂系统的接口。
-
流数据处理:我们还将实现一个数据处理代理,一个事件监听器,它监听一个事件源——你可能会看到以太坊的标志,旁边有一个从左侧连接过来的圆圈。这个代理会监听来自源的数据流,然后调用运行在 FaaS 平台上的函数。
Hello world,FaaS/Docker 方式
本书涵盖了 FaaS 在 Docker 上的三大主要框架。因此,如果我选择一个特定框架用于第一章的hello world程序,那就不太公平了。我会让你根据自己的喜好选择一个。
以下是在 Linux 机器上的常见设置。对于 Mac 或 Windows 用户,请跳过此步骤并下载 Docker for Mac 或 Docker for Windows:
$ curl -sSL https://get.docker.com | sudo sh
如果你选择在本章中使用 OpenFaaS,可以通过使用 Play with Docker (labs.play-with-docker.com/
) 来简化此设置过程,该平台会自动在单节点 Docker Swarm 上安装 OpenFaaS。
当我们安装好 Docker 后,只需初始化 Swarm,以使我们的单节点集群准备好运行:
$ docker swarm init --advertise-addr=eth0
如果之前的命令失败,尝试将网络接口名称更改为与你的名称匹配。但如果仍然失败,只需输入机器的其中一个 IP 地址即可。
如果一切设置成功,让我们开始在各个 FaaS 平台上运行一系列的 hello world 程序。
Hello OpenFaas
我们将尝试使用 OpenFaaS 运行 echoit
函数进行 hello world
。首先,从 github.com/openfaas/faas
克隆项目,并只进行一层深度克隆,以加快克隆过程:
$ git clone --depth=1 https://github.com/openfaas/faas
然后,进入 faas
目录,并使用以下命令简单地部署 OpenFaaS 默认堆栈:
$ cd faas
$ docker stack deploy -c docker-compose.yml func
等待堆栈启动完成。然后,我们用 curl
命令进行 hello world
:
$ curl -d "hello world." -v http://localhost:8080/function/func_echoit
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /function/func_echoit HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
> Content-Length: 12
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 12 out of 12 bytes
< HTTP/1.1 200 OK
< Content-Length: 12
< Content-Type: application/x-www-form-urlencoded
< Date: Fri, 23 Mar 2018 16:37:30 GMT
< X-Call-Id: 866c9294-e243-417c-827c-fe0683c652cd
< X-Duration-Seconds: 0.000886
< X-Start-Time: 1521823050543598099
<
* Connection #0 to host localhost left intact
hello world.
玩了一会儿后,我们也可以使用 docker stack rm
来移除所有正在运行的服务:
$ docker stack rm func
Hello OpenWhisk
让我们快速进入 OpenWhisk。要使用 OpenWhisk 进行 hello world
,我们还需要一个 docker-compose
二进制文件。请访问 github.com/docker/compose/releases
并按照那里的说明进行安装。
使用 OpenWhisk 时,整个堆栈的启动时间可能会比 OpenFaaS 稍长。但是,由于 hello world
已经内置,整体命令会更简洁。
首先,从其 GitHub 仓库克隆 OpenWhisk 开发工具:
$ git clone --depth=1 https://github.com/apache/incubator-openwhisk-devtools devtools
然后进入 devtools/docker-compose
目录,并使用以下命令手动拉取镜像:
$ cd devtools/docker-compse
$ docker-compose pull
$ docker pull openwhisk/nodejs6action
之后,只需调用 make quick-start
来执行设置:
$ make quick-start
等待 OpenWhisk 集群启动。这可能需要最多 10 分钟。
之后,运行以下命令 make hello-world
来注册并调用 hello world
操作:
$ make hello-world
creating the hello.js function ...
invoking the hello-world function ...
adding the function to whisk ...
ok: created action hello
invoking the function ...
invokation result: { "payload": "Hello, World!" }
{ "payload": "Hello, World!" }
deleting the function ...
ok: deleted action hello
确保你处于一个快速的网络环境中。OpenWhisk 拉取 invoke 和 controller 时的慢速往往会导致 make quick-start
失败。
要清理环境,只需使用 make destroy
命令来终止目标:
$ make destroy
向 Fn 项目问好
这是本书中覆盖的另一个 FaaS 项目。我们通过安装 Fn CLI 快速完成 hello world
。然后使用它启动一个本地 Fn 服务器,创建一个应用程序,并创建一个路由,将其链接到应用程序下的一个预构建的 Go
函数。之后,我们将使用 curl
命令测试部署的 hello world
函数。
这是安装 Fn 客户端的标准命令:
$ curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sudo sh
之后,我们可以使用 fn
命令。让我们启动一个 Fn 服务器。使用 --detach
使其在后台运行:
$ fn start --detach
好的,如果我们看到一个容器 ID,就可以开始了。接下来,快速创建一个 Fn 应用程序,并将其命名为 goapp
:
$ fn apps create goapp
然后,我们已经在 Docker Hub 上有一个预构建的镜像,名为chanwit/fn_ch1:0.0.2
。只需使用它即可。我们使用fn routes create
命令将新路由与镜像连接。此步骤的目的是实际定义一个函数:
$ fn routes create --image chanwit/fn_ch1:0.0.2 goapp /fn_ch1
/fn_ch1 created with chanwit/fn_ch1:0.0.2
好的,路由已准备好。现在,我们可以使用 curl
命令直接调用我们在 Fn 上的 hello world
程序:
$ curl -v http://localhost:8080/r/goapp/fn_ch1
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /r/goapp/fn_ch1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 26
< Content-Type: application/json; charset=utf-8
< Fn_call_id: 01C99YJXCE47WG200000000000
< Xxx-Fxlb-Wait: 383.180124ms
< Date: Fri, 23 Mar 2018 17:30:34 GMT
<
{"message":"Hello World"}
* Connection #0 to host localhost left intact
好的,看起来 Fn 一切都按照预期正常工作。让我们在它完成后移除服务器:
$ docker rm -f fnserver
练习
每章末尾都会有一组问题,帮助我们复习当前章节的内容。让我们尝试不翻回章节内容,直接回答每个问题:
-
无服务器架构的定义是什么?
-
FaaS 的定义是什么?
-
描述 FaaS 和无服务器之间的区别?
-
Docker 在无服务器应用程序世界中的角色是什么?
-
FaaS 的常见架构是什么样的?
-
尝试解释为什么我们处于下图中的阴影区域:
图 1.5:FaaS 和本书涵盖的无服务器区域的范围
总结
本章介绍了无服务器架构和 Docker,定义了无服务器架构和 FaaS。我们了解了无服务器的优势、何时使用它以及何时避免使用它。无服务器 FaaS 是由供应商在公共云上运行的 FaaS 平台,而 FaaS 可能需要在私有、混合或本地环境中运行。这时,我们可以使用 Docker。Docker 将帮助我们构建 FaaS 应用程序,并为运行基于容器的函数准备容器基础设施。
我们预览了将在后续章节中一步步构建的演示项目。然后,我们快速地在三个领先的 FaaS 平台上完成了 Docker 的 hello world
示例,展示了在我们自己的 Docker 集群上运行 FaaS 平台是多么简单。
在下一章中,我们将回顾容器的概念以及它背后的技术。我们还将介绍 Docker 及其工作流程,接着我们将学习 Docker Swarm 集群的概念以及如何准备它。最后,我们将讨论 Docker 如何适应无服务器的世界。
第二章:Docker 和 Swarm 集群
本章将回顾容器技术,并介绍 Docker 及其编排引擎,以及 Docker Swarm 模式。然后,我们将讨论为什么需要 Docker 基础设施来部署和运行无服务器(serverless)和函数即服务(FaaS)应用。 本章涵盖的主题如下:
-
容器与 Docker
-
设置 Docker Swarm 集群
-
使用 Docker 执行容器网络操作
-
为什么 Docker 适合无服务器和 FaaS 基础设施
什么是容器?
在讨论 Docker 之前,最好先了解一下软件容器背后的技术。
虚拟机是一种常见的虚拟化技术,并已被云服务提供商和企业广泛采用。实际上,软件容器(简称容器)也是一种虚拟化技术,但它们与虚拟机有所不同。关键的区别在于每个容器共享主机机器的内核,而每个虚拟机都有自己独立的内核。基本上,容器在操作系统级别使用虚拟化技术,而不是虚拟机监控程序。下图展示了容器和虚拟机堆栈的比较:
图 2.1:容器与虚拟机的对比
Linux 的容器技术依赖于两个重要的内核功能,命名空间和控制组(cgroups)。命名空间将一个进程隔离,使其拥有自己的一组全局资源,如进程 ID(PID)和网络。控制组或控制组提供了一种计量和限制资源的机制,如 CPU 使用率、内存、块 I/O 和网络带宽:
图 2.2:Linux 能力—容器使用的命名空间和控制组
使用 Linux 的命名空间和控制组(cgroups)功能的核心引擎叫做runC。它是一个用于启动和运行容器的工具,采用开放容器倡议(OCI)格式。Docker 在起草这个规范中起了重要作用,因此 Docker 容器镜像与 OCI 规范兼容,因此可以通过 runC 运行。Docker 引擎本身在底层使用runC来启动每个容器。
什么是 Docker?
过去,容器的管理和使用相当困难。Docker 基本上是一套帮助我们准备、管理和执行容器的技术。在虚拟机的世界里,我们需要一个虚拟机监控程序(hypervisor)来处理所有虚拟机实例。类似地,在容器的世界里,我们使用 Docker 作为容器引擎来处理与容器相关的一切事务。
不可否认,Docker 是目前最流行的容器引擎。使用 Docker 时,我们遵循 Docker 本身推荐的三个概念:构建(build)、运输(ship)和运行(run)。
-
Build-Ship-Run的工作流程是由 Docker 的理念优化的。在Build步骤中,我们可以快速构建和销毁容器镜像。作为开发者,我们可以将容器构建步骤作为我们开发周期的一部分。
-
在Ship步骤中,我们将容器镜像运送到不同的地方,从开发笔记本到 QA 服务器,再到预生产服务器。我们将容器镜像发送到公共集线器存储,或者存储到我们公司内部的私有注册中心。最终,我们将容器镜像送到生产环境中运行。
-
在Run步骤中,Docker 帮助我们使用 Swarm 集群准备生产环境。我们从容器镜像启动容器。我们可以调度容器在集群中的特定部分运行,并设置一些特定约束。我们使用 Docker 命令管理容器的生命周期:
图 2.3:Build-ship-run
安装 Docker
在开始构建-运送-运行步骤之前,我们需要在机器上安装 Docker。在 Linux 上,我们使用经典的安装方法,Docker 社区版(CE或Docker-CE):
$ curl -sSL https://get.docker.com | sudo bash
本书中,我们将使用 Debian 或 Ubuntu 机器来演示 Docker。在 Debian/Ubuntu 机器上,我们将通过apt-get
获取 Docker 的最稳定版本(截至写作时),并将其降级到版本 17.06.2。如果我们已经有了更新版本的 Docker,例如 17.12 或 18.03,它将被降级到 17.06.2:
$ sudo apt-get install docker-ce=17.06.2~ce-0~ubuntu
对于 macOS 和 Windows 系统,我们可以从 Docker 官网下载安装 Docker:
-
Docker for Mac:
www.docker.com/docker-mac
-
Docker for Windows:
www.docker.com/docker-windows
要检查已安装的 Docker 版本,我们可以使用docker version
命令:
$ docker version
Client:
Version: 17.06.2-ce
API version: 1.30
Go version: go1.8.3
Git commit: cec0b72
Built: Tue Sep 5 20:00:33 2017
OS/Arch: linux/amd64
Server:
Version: 17.06.2-ce
API version: 1.30 (minimum version 1.12)
Go version: go1.8.3
Git commit: cec0b72
Built: Tue Sep 5 19:59:26 2017
OS/Arch: linux/amd64
Experimental: true
docker version
打印出来的信息分为客户端和服务器两个部分。客户端部分告诉我们关于docker
二进制文件的信息,用于发出命令。服务器部分则告诉我们dockerd
(Docker 引擎)的版本。
从前面的代码片段中我们可以看到,客户端和服务器的版本都是 17.06.2-ce*,即稳定版 17.06 Community Edition 的第二次更新。服务器允许最低版本为 1.12 的 Docker 客户端进行连接。API 版本告诉我们,dockerd
实现了远程 API 版本 1.30。
如果我们预计将使用下一个稳定版本的 Docker,我们应该选择即将发布的 17.06.3、17.09.x 或 17.12.x 版本。
构建容器镜像
我们使用 Docker 准备软件及其执行环境,将它们打包到文件系统中。我们称这个步骤为构建容器镜像。好了,让我们开始吧。我们将在 Ubuntu 上构建我们自己的 NGINX 服务器版本my-nginx
,作为 Docker 镜像。请注意,容器镜像和 Docker 镜像在本书中会互换使用。
我们创建一个名为my-nginx
的目录并切换到该目录:
$ mkdir my-nginx
$ cd my-nginx
然后,我们创建一个名为 Dockerfile 的文件,内容如下:
FROM ubuntu
RUN apt-get update && apt-get install -y nginx
EXPOSE 80
ENTRYPOINT ["nginx", "-g", "daemon off;"]
我们将逐行解释 Dockerfile 的内容:
-
首先,它表示我们希望使用名为
ubuntu
的镜像作为我们的基础镜像。这个ubuntu
镜像存储在 Docker Hub 上,这是一个由 Docker Inc.托管的中央镜像注册服务器。 -
其次,它表示我们希望使用
apt-get
命令安装 NGINX 和相关的软件包。这里的技巧是,ubuntu
是一个普通的 Ubuntu 镜像,没有任何软件包信息,因此我们需要在安装软件包之前运行apt-get update
。 -
第三,我们希望这个镜像为我们的 NGINX 服务器在容器内打开端口
80
。 -
最后,当我们从这个镜像启动一个容器时,Docker 将在容器内部为我们运行
nginx -g daemon off;
命令。
现在,我们已经准备好构建我们的第一个 Docker 镜像。输入以下命令来开始构建镜像。请注意,命令的末尾有一个点:
$ docker build -t my-nginx .
你现在会看到类似以下内容的输出,输出中会有不同的哈希值,所以不用担心。步骤 2 到 4 会花费几分钟时间完成,因为它会将 NGINX 软件包下载并安装到镜像文件系统中。只需确保有四个步骤,并且最后以Successfully tagged my-nginx:latest
的消息结尾*:*
Sending build context to Docker daemon 2.048kB
Step 1/4 : FROM ubuntu
---> ccc7a11d65b1
Step 2/4 : RUN apt-get update && apt-get install -y nginx
---> Running in 1f95e93426d3
...
Step 3/4 : EXPOSE 8080
---> Running in 4f84a2dc1b28
---> 8b89cae986b0
Removing intermediate container 4f84a2dc1b28
Step 4/4 : ENTRYPOINT nginx -g daemon off;
---> Running in d0701d02a092
---> 0a393c45ed34
Removing intermediate container d0701d02a092
Successfully built 0a393c45ed34
Successfully tagged my-nginx:latest
我们现在在本地计算机上拥有一个名为my-nginx:latest
的 Docker 镜像。我们可以使用docker image ls
命令(或者使用旧版命令docker images
)来检查该镜像是否真的存在:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
my-nginx latest 0a393c45ed34 18 minutes ago 216MB
基本上,这就是 Docker 的构建概念。接下来,我们继续讨论镜像发布。
镜像发布
我们通常通过 Docker 注册表发布 Docker 镜像。由 Docker Inc.托管的公共注册表称为Docker Hub。要将 Docker 镜像发布到注册表中,我们使用docker push
命令。当我们启动一个容器时,它的镜像会在运行之前自动检查并下载到主机上。下载过程可以通过docker pull
命令显式完成。下图展示了不同环境和注册表之间的推送/拉取行为:
图 2.4:镜像推送和拉取工作流
在前面的图示中,开发人员从 Docker 公共注册表(Docker Hub)拉取镜像,然后从他们自己的 Docker 私有注册表推送和拉取镜像。在开发环境中,每个环境会通过某种机制来触发拉取镜像并运行它们。
要检查我们的 Docker 守护进程是否允许通过非加密的 HTTP 与 Docker 注册表进行不安全的交互,我们可以执行docker info
,然后用grep
查找Registries
关键字。
请注意,不建议在生产环境中使用不安全的 Docker 注册表。已经提醒过你了!
$ docker info | grep -A3 Registries
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false
好的,看到127.0.0.0/8
表示我们被允许这么做。我们将会在127.0.0.1:5000
上运行一个本地的 Docker 注册表。让我们来设置它。
要启动本地 Docker 注册表,只需从 Docker 注册表 V2 镜像中运行:
$ docker container run --name=registry -d -p 5000:5000 registry:2
6f7dc5ef89f070397b93895527ec2571f77e86b8d2beea2d8513fb30294e3d10
我们应该检查它现在是否已经启动并运行:
$ docker container ls --filter name=registry
CONTAINER ID IMAGE COMMAND CREATED STATUS
6f7dc5ef89f0 registry:2 "/entrypoint.sh /e.." 8 seconds ago Up
container run
命令及其他相关命令将在运行容器部分再次讨论。
回想一下,我们已经构建了一个名为my-nginx
的镜像。我们可以检查它是否仍然存在,这次我们使用--filter reference
来选择仅以nginx
结尾的镜像名称:
$ docker image ls --filter reference=*nginx
REPOSITORY TAG IMAGE ID CREATED SIZE
my-nginx latest a773a4303694 1 days ago 216MB
nginx latest b8efb18f159b 2 months ago 107MB
我们还可以通过简化命令为docker image ls *nginx
,它会得到相同的结果。
让我们为镜像打标签。我们将my-nginx
打标签为127.0.0.1:5000/my-nginx
,这样它就可以推送到我们的私有 Docker 注册表中。我们可以使用docker image tag
命令(对于旧版的顶级命令是docker tag
)来完成这项操作:
$ docker image tag my-nginx 127.0.0.1:5000/my-nginx
我们可以再次使用image ls
检查,确认tag
命令已经成功执行:
$ docker image ls 127.0.0.1:5000/my-nginx
REPOSITORY TAG IMAGE ID CREATED SIZE
127.0.0.1:5000/my-nginx latest a773a4303694 1 days ago 216MB
好的,看来不错!我们现在可以将my-nginx
镜像推送到本地仓库,当然使用docker image push
,因为 Docker 仓库就在我们机器本地,整个过程非常快速。
你会发现当你尝试执行命令时,哈希值与下面列出的不同。这是正常现象,请忽略它。
现在,执行以下命令将my-nginx
镜像推送到本地私有仓库:
$ docker image push 127.0.0.1:5000/my-nginx
The push refers to a repository [127.0.0.1:5000/my-nginx]
b3c96f2520ad: Pushed
a09947e71dc0: Pushed
9c42c2077cde: Pushed
625c7a2a783b: Pushed
25e0901a71b8: Pushed
8aa4fcad5eeb: Pushed
latest: digest: sha256:c69c400a56b43db695 ... size: 1569
最难的部分已经顺利完成。现在我们回到简单的部分:将镜像推送到 Docker Hub。在继续之前,如果你还没有 Docker ID,请先在hub.docker.com/
注册一个。
要将镜像存储到那里,我们必须使用<docker id>/<image name>
格式为镜像打标签。要将my-nginx
推送到 Docker Hub,我们需要将它打标签为<docker id>/my-nginx
。我将在此使用我的 Docker ID。请将<docker id>
替换为你注册的 Docker ID:
$ docker image tag my-nginx chanwit/my-nginx
在推送之前,我们需要先使用docker login
命令登录 Docker Hub。请使用-u
和你的 Docker ID 指定帐户。我们会被要求输入密码;如果一切正常,命令会显示Login Succeeded
:
$ docker login -u chanwit
Password:
Login Succeeded
请注意,我们的用户名和密码不安全地存储在~/.docker/config.json
中,因此请尽量不要忘记输入docker logout
。
运行容器
现在,让我们从my-nginx
镜像启动一个容器。我们将使用docker container run
命令(旧版的顶级命令是docker run
)。这样做是为了以后台进程运行我们的容器,使用-d
,并将主机的8080
端口绑定到容器的80
端口(-p 8080:80
)。我们通过--name
指定容器名称。如果容器启动成功,我们将得到一个哈希值,例如4382d778bcc9
,它是我们运行的容器的 ID:
$ docker container run --name=my-nginx -d -p 8080:80 my-nginx
4382d778bcc96f70dd290e8ef9454d5a260e87366eadbd1060c7b6e087b3df26
打开网页浏览器并访问http://localhost:8080
,我们将看到 NGINX 服务器正在运行:
图 2.5:容器内运行 NGINX 的示例
现在,我们的 NGINX 服务器作为后台容器运行,并通过主机的8080
端口提供服务。我们可以使用docker container ls
命令(或者老式的、顶层的docker ps
)来列出所有正在运行的容器:
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED ...
4382d778bcc9 my-nginx "nginx -g 'daemon ..." 2 seconds ago ...
6f7dc5ef89f0 registry:2 "/entrypoint.sh /e..." 2 hours ago ...
我们可以使用命令docker container start
、stop
、pause
或kill
等来控制容器的生命周期。
如果我们希望强制移除正在运行的容器,可以使用docker container rm -f <容器 ID 或名称>
命令。我们可以先移除所有正在运行的my-nginx
实例和私有注册表,然后继续进行 Docker Swarm 集群的操作:
$ docker container rm -f my-nginx registry
my-nginx
registry
Docker Swarm 集群
集群是由一组机器连接在一起共同完成工作的。Docker 主机是安装了 Docker 引擎的物理或虚拟机。我们通过将多个 Docker 主机连接在一起来创建 Docker Swarm 集群。我们将每个 Docker 主机称为 Docker Swarm 节点,简称节点。
在版本 1.12 中,Docker 引入了 Swarm 模式,这是一个新的编排引擎,用以替代旧的 Swarm 集群,现称为Swarm 经典模式。Swarm 经典模式和 Swarm 模式的主要区别在于,Swarm 经典模式使用外部服务,如 Consul、etcd 或 Apache ZooKeeper 作为其键/值存储,而 Swarm 模式则内置了这个键/值存储。因此,Swarm 模式能够保持最小的编排延迟,并且比 Swarm 经典模式更加稳定,因为它不需要与外部存储进行交互。Swarm 模式的单体架构有助于修改其算法。例如,我的一项研究工作实现了蚁群优化,以改进 Swarm 在非均匀集群上运行容器的方式。
根据我们实验室的实验,我们发现 Swarm 经典模式在扩展到 100–200 个节点时存在限制。而使用 Swarm 模式,我们与 Docker 社区合作进行的实验表明,它可以扩展到至少 4,700 个节点。
结果可以通过项目 Swarm2K (github.com/swarmzilla/swarm2k
) 和 Swarm3K (github.com/swarmzilla/swarm3k
) 在 GitHub 上公开获取。
Swarm 模式性能的关键在于它建立在嵌入式etcd库之上。嵌入式 etcd 库提供了一种机制,以分布式方式存储集群的状态。所有状态信息都保存在 Raft 日志数据库中,并采用 Raft 共识算法。
本节中,我们将讨论如何在 Swarm 模式下设置集群。
设置集群
要创建一个完全功能的单节点 Swarm 集群,我们只需输入以下命令:
$ docker swarm init Swarm initialized: current node (jbl2cz9gkilvu5i6ahtxlkypa) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-470wlqyqbsxhk6gps0o9597izmsjx4xeht5cy3df5sc9nu5n6u-9vlvcxjv5jjrcps4trjcocaae 192.168.1.4:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
我们称这个过程为 Swarm 集群初始化。该过程通过准备/var/lib/docker/swarm
目录来初始化新集群,该目录用于存储与集群相关的所有状态。以下是/var/lib/docker/swarm
的内容,如果需要,可以进行备份:
$ sudo ls -al /var/lib/docker/swarm
total 28
drwx------ 5 root root 4096 Sep 30 23:31 .
drwx--x--x 12 root root 4096 Sep 29 15:23 ..
drwxr-xr-x 2 root root 4096 Sep 30 23:31 certificates
-rw------- 1 root root 124 Sep 30 23:31 docker-state.json
drwx------ 4 root root 4096 Sep 30 23:31 raft
-rw------- 1 root root 67 Sep 30 23:31 state.json
drwxr-xr-x 2 root root 4096 Sep 30 23:31 worker
如果主机上有多个网络接口,之前的命令将失败,因为 Docker Swarm 要求我们使用 IP 地址或某个特定网络接口来指定广播地址。
在以下示例中,我使用我的wlan0
IP 地址作为集群的广播地址。这意味着任何在 Wi-Fi 网络上的机器都可以尝试加入这个集群:
$ docker swarm init --advertise-addr=192.168.1.4:2377
类似地,我们可以使用网络接口的名称进行广播,例如eth0
:
$ docker swarm init --advertise-addr=eth0
选择最适合你工作环境的样式。
初始化后,我们获得了一个完全工作的单节点集群。为了强制某个节点离开当前集群,我们使用以下命令:
$ docker swarm leave --force
Node left the swarm.
如果我们在单节点集群上运行此命令,集群将被销毁。如果你在这里运行上述命令,请不要忘记在进入下一部分之前再次初始化集群,使用docker swarm init
。
主节点和工作节点
回顾一下,我们曾用“Docker 主机”一词来指代安装了 Docker 的机器。当我们将这些主机组合在一起形成集群时,有时我们称它们中的每一个为 Docker 节点。
Swarm 集群由两种类型的 Docker 节点组成,主节点和工作节点。例如,我们说节点mg0
具有主节点角色,节点w01
具有工作节点角色。我们通过将其他节点加入主节点(通常是第一个主节点)来形成集群。docker swarm join
命令要求安全令牌不同,以允许节点以主节点或工作节点身份加入。请注意,我们必须在每个节点上运行docker swarm join
命令,而不是在主节点上运行:
# Login to each node
$ docker swarm join --token SWMTKN-1-27uhz2azpesmsxu0tlli2e2uhdr2hudn3e2x5afilc02x1zicc-9wd3glqr5i92xmxvpnzdwz2j9 192.168.1.4:2377
主节点负责控制集群。Docker 推荐的最佳实践是,主节点的数量应为奇数,这是最佳配置。我们应该从三个主节点开始,并且主节点数量应为奇数。如果有三个主节点,允许其中一个失败,集群仍然能够正常运行。
下表显示了从一个到六个主节点的可能配置。例如,一个包含三个主节点的集群允许一个主节点失败,集群仍然能够保持运行。如果两个主节点失败,集群将无法操作,无法启动或停止服务。然而,在这种状态下,正在运行的容器不会停止,仍然继续运行:
主节点 | 维持集群所需的主节点数量 | 允许失败的主节点数 |
---|---|---|
1 | 1 | 0 |
2 | 2 | 0 |
3 (最佳) | 2 | 1 |
4 | 3 | 1 |
5 (最佳) | 3 | 2 |
6 | 4 | 2 |
在失去大多数主节点后,恢复集群的最佳方法是尽可能快地将失败的主节点恢复上线。
在生产集群中,我们通常不在主节点上调度运行的任务。主节点需要有足够的 CPU、内存和网络带宽来正确处理节点信息和 Raft 日志。我们通过指挥一个主节点来控制集群。例如,我们可以通过向主节点发送以下命令来列出集群中的所有节点:
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
wbb8rb0xob * mg0 Ready Active Leader
结果中显示的是当前集群中所有节点的列表。我们可以通过查看MANAGER STATUS
列来判断mg0
节点是否为管理节点。如果一个管理节点是集群的主管理节点,MANAGER STATUS
会显示它是Leader
。如果这里有另外两个管理节点,状态会显示它们是Follower
。下面是这个 leader/follower 机制的工作原理。当我们向领导节点发送命令时,领导节点会执行命令,并更改集群的状态。然后,集群状态会通过将此更改发送给其他管理节点(即 follower)来更新。如果我们向 follower 发送命令,它不会自己执行,而是将命令转发给领导节点。基本上,集群的所有命令都由领导节点执行,follower 只会将更改更新到它们自己的 Raft 日志中。
如果有新的管理节点想要加入,我们需要为其提供一个主令牌。输入docker swarm join-token manager
命令来获取安全令牌,以便将节点加入到管理角色的集群中:
$ docker swarm join-token manager
To add a manager to this swarm, run the following command:
docker swarm join --token SWMTKN-1-2c6finlm9d97q075kpwxcn59q93vbpfaf5qp13awjin3s3jopw-5hex62dfsd3360zxds46i6s56 192.168.1.4:2377
尽管任务作为容器可以在两种类型的节点上运行,但我们通常不会将任务提交到主节点上运行。我们仅使用工作节点来运行生产中的任务。为了将工作节点加入集群,我们将工作令牌传递给加入命令。使用docker swarm join-token worker
来获取工作令牌。
服务与任务
随着新调度引擎的引入,Docker 在 1.12 版本中引入了服务和任务的新抽象。一个服务可以由多个任务实例组成。我们将每个实例称为副本。每个任务实例作为容器在 Docker 节点上运行。
可以使用以下命令创建服务:
$ docker service create \
--replicas 3 \
--name web \
-p 80:80 \
--constraint node.role==worker \
nginx
该 web 服务由三个任务组成,这些任务通过--replicas
进行指定。任务由调度引擎提交并在选定的节点上运行。服务的名称 web 可以通过虚拟 IP 地址解析。位于同一网络上的其他服务(例如反向代理服务)可以引用它。我们使用--name
来指定服务的名称。
我们将在下面的图示中继续讨论此命令的细节:
图 2.6:Swarm 集群运行示意图
假设我们的集群由一个管理节点和五个工作节点组成。管理节点没有高可用性设置;这将留给读者自己解决。
我们从管理节点开始。由于我们不希望它接受任何计划任务,因此将管理节点设置为drained。这是最佳实践,我们可以通过以下方式使节点进入 drained 状态:
$ docker node update --availability drain mg0
此服务将在路由网格的80
端口上发布。路由网格是 Swarm 模式内执行负载均衡的一种机制。每个工作节点的80
端口将会开放,以提供此服务。当请求到达时,路由网格会自动将请求路由到某个节点的特定容器(任务)。
路由网格依赖于一个使用 overlay 驱动程序的 Docker 网络,即 ingress
。我们可以使用 docker network ls
列出所有活跃的网络:
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
c32139129f45 bridge bridge local
3315d809348e docker_gwbridge bridge local
90103ae1188f host host local
ve7fj61ifakr ingress overlay swarm
489d441af28d none null local
我们找到一个 ID 为 ve7fj61ifakr
的网络,它是 swarm
范围下的一个 overlay
网络。正如信息所暗示的那样,这种网络仅在 Docker Swarm 模式下工作。要查看此网络的详细信息,我们使用 docker network inspect ingress
命令:
$ docker network inspect ingress
[
{
"Name": "ingress",
"Id": "ve7fj61ifakr8ybux1icawwbr",
"Created": "2017-10-02T23:22:46.72494239+07:00",
"Scope": "swarm",
"Driver": "overlay",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "10.255.0.0/16",
"Gateway": "10.255.0.1"
}
]
},
}
]
我们可以看到,ingress
网络的子网是 10.255.0.0/16
,这意味着默认情况下我们可以在该网络中使用 65,536 个 IP 地址。这个数字是通过 docker service create -p
在单个 Swarm 模式集群中创建的任务(容器)的最大数量。当我们在非 Swarm 环境中使用 docker container run -p
时,这个数字不会受到影响。
要创建一个 Swarm 范围的 overlay 网络,我们使用 docker network create
命令:
$ docker network create --driver overlay appnet
lu29kfat35xph3beilupcw4m2
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
lu29kfat35xp appnet overlay swarm
c32139129f45 bridge bridge local
3315d809348e docker_gwbridge bridge local
90103ae1188f host host local
ve7fj61ifakr ingress overlay swarm
489d441af28d none null local
我们可以使用 docker network ls
命令再次检查,看到 appnet
网络与 overlay
驱动程序和 swarm
范围。在这里你的网络 ID 会有所不同。要将服务附加到特定网络,我们可以将网络名称传递给 docker service create
命令。例如:
$ docker service create --name web --network appnet -p 80:80 nginx
上面的示例创建了 web
服务并将其附加到 appnet
网络。只有当 appnet
是 Swarm 范围时,这个命令才有效。
我们可以通过使用 docker service update
命令并配合 --network-add
或 --network-rm
选项,动态地将网络从当前正在运行的服务中移除或重新附加。请尝试以下命令:
$ docker service update --network-add appnet web
web
在这里,我们可以观察到 docker inspect web
命令的结果。你会发现一段 JSON 输出,最后一块看起来如下:
$ docker inspect web
...
"UpdateStatus": {
"State": "completed",
"StartedAt": "2017-10-09T15:45:03.413491944Z",
"CompletedAt": "2017-10-09T15:45:21.155296293Z",
"Message": "update completed"
}
}
]
这意味着服务已经更新,并且更新过程已完成。现在,我们将看到 web
服务附加到 appnet
网络:
图 2.7:Swarm 范围覆盖网络的 Gossip 通信机制
Overlay 网络依赖于在端口 7946
上实现的 gossip 协议,支持 TCP 和 UDP,同时还使用 Linux 的 VXLAN,通过 UDP 端口 4789
实现。该 overlay 网络的实现是以性能为目标的。网络将仅覆盖必要的主机,并在需要时逐步扩展。
我们可以通过增加或减少副本数来扩展服务。扩展服务可以使用 docker service scale
命令来完成。例如,如果我们希望将 web
服务扩展到五个副本,可以执行以下命令:
$ docker service scale web=5
当服务扩展,并且其任务调度到新节点时,所有绑定到此服务的相关网络将自动扩展以覆盖新节点。在下图中,我们有两个应用服务副本,并且我们希望通过命令 docker service scale app=3
将其从两个扩展到三个。新的副本 app.3 将被调度到工作节点 w03。然后,绑定到此应用服务的覆盖网络也将扩展以覆盖节点 w03。网络范围的 gossip 通信负责网络扩展机制:
图 2.8:Swarm 范围的网络扩展
Docker 与无服务器
Docker 将如何对我们有所帮助?在处理应用开发时,Docker 可以用来简化开发工具链。我们可以将编写无服务器应用所需的一切打包进一个单一的容器镜像中,并让整个团队使用它。这确保了工具版本的一致性,并确保它们不会弄乱我们的开发机器。
然后我们将使用 Docker 来准备我们的基础设施。实际上,无服务器(serverless)意味着开发人员不需要维护自己的基础设施。然而,在公共云不可选的情况下,我们可以使用 Docker 来简化基础设施的提供。使用与第三方无服务器平台相同的架构,在我们公司的基础设施上,我们可以最小化运维成本。后续章节将讨论如何操作我们自己的基于 Docker 的 FaaS 基础设施。
对于无服务器应用本身,我们使用 Docker 作为无服务器函数的封装器。我们将 Docker 作为工作单元,这样任何类型的二进制文件都可以集成到我们的无服务器平台中,从遗留的 COBOL、C 或 Pascal 程序,到用现代语言编写的程序,如 Node.js、Kotlin 或 Crystal。在 Docker 17.06+ 版本中,还可以跨多硬件架构形成 Swarm 集群。我们甚至可以在与主机 COBOL 程序相同的集群上托管基于 Windows 的 C# 函数。
练习
为了帮助你更好地记住和理解本章中描述的 Docker 概念和实践,尝试在不返回章节内容的情况下回答以下问题。开始吧:
-
什么是容器?容器和虚拟机之间的关键区别是什么?
-
启用容器技术的 Linux 内核主要功能有哪些?请至少列举其中两个。
-
Docker 工作流的关键概念是什么?
-
Dockerfile 是做什么的?你使用哪个 Docker 命令与其交互?
-
Dockerfile 中的 ENTRYPOINT 指令是什么?
-
我们使用哪个命令来列出所有 Docker 镜像?
-
我们使用哪个命令来形成一个 Docker Swarm 集群?
-
Swarm classic 和 Swarm mode 之间的主要区别是什么?
-
请解释服务与任务之间的关系。
-
我们如何创建一个包含五个副本的 NGINX 服务?
-
我们如何将 NGINX 服务的副本数缩减到两个?
-
形成具有高可用性属性的 Swarm 集群所需的最小节点数是多少?为什么?
-
被称为路由网格一部分的网络是什么?它有多大?
-
Swarm 集群使用哪些端口号?它们的用途是什么?
-
网络范围的 Gossip 通信的主要优势是什么?
总结
本章首先讨论了容器的概念。然后我们回顾了 Docker 是什么,如何安装它,以及 Docker 的构建、分发和运行工作流程。接着,我们学习了如何形成 Docker Swarm 集群及 Swarm 主节点和工作节点。我们了解了如何通过设置奇数个主节点来正确构建一个稳健的 Swarm 集群。然后我们学习了 Docker Swarm 的服务和任务概念。最后,我们学习了 Docker 如何融入无服务器应用开发。
在下一章,我们将回顾无服务器框架和平台,以理解它们的整体架构和局限性。
第三章:无服务器框架
本章讨论无服务器框架。它们是什么?纯粹的无服务器框架目前有哪些限制?Docker 如何部分解决无服务器框架的限制?我们将从了解 AWS Lambda 开始,然后是 Azure Functions 和 Google Cloud Functions。我们会简要提及 IBM Cloud Functions,但实际上它的引擎是 OpenWhisk,接下来的几章会详细讨论它。
我们还将在本章最后一部分讨论无服务器框架,一个帮助我们开发云独立无服务器应用程序的工具包。
AWS Lambda
在云服务提供商提供的无服务器架构中,AWS Lambda 是最受欢迎的,并且具有一些高级功能。
FaaS/无服务器是微服务的自然进化,或者我们可以把它看作是对微服务架构的扩展。在许多场景中,我们可以使用函数或 Lambda 来补充我们的微服务架构。如果你已经是 AWS 客户,将代码从 EC2 迁移到 Lambda 是完全自然的,并且能够节省大量资金。下面的图表展示了一个使用AWS Lambda与S3 存储桶和DynamoDB的简单用例:
图 3.1:在 AWS 上使用 Lambda 函数的简单用例
在 S3 中,可以触发事件到特定的端点。我们将 Lambda 函数的端点放在那里。当用户上传或更改 S3 存储桶中的内容时,它会触发一个调用请求发送到 Lambda 函数。这可以视为一种 WebHooks 的形式。之后,Lambda 函数接收事件并开始计算其应用逻辑。完成后,Lambda 将结果传输并存储到 DynamoDB 实例中。
我们将在第八章中演示一个类似的场景,将它们全部整合起来。
限制
Lambda 支持多种语言运行时;例如,Node.js、Go、Java、Python 和 C#。每个 AWS Lambda 都有一些限制,来限制每次调用所使用的资源。在内存方面,Lambda 支持的 RAM 范围从 128 MB 到 3,008 MB,按 64 MB 的增量分配。如果内存使用超过限制,函数将自动终止。
在磁盘空间方面,Lambda 函数可以使用/tmp
目录,最多 512 MB。这个磁盘卷是临时性的,因此在 Lambda 完成工作后,它会被清除。此外,Lambda 函数中允许的文件描述符数量限制为 1,024,而在单次调用中可以分叉的进程和线程数也限制为 1,024。
对于每个请求,同步 HTTP 调用的请求体大小限制为 6 MB,而异步、事件触发调用的请求体大小限制为 128 KB。
这里最重要的方面是时间限制。AWS Lambda 允许一个函数运行最长不超过 5 分钟(或 300 秒)。如果执行时间超过 5 分钟,函数将被自动终止。
Lambda 终止
Lambda 背后的技术实际上是基于容器的,这意味着它将一个函数与其他实例隔离开来。容器的沙箱为每个配置提供特定的资源。
Lambda 函数可以通过多种方式终止:
-
超时:如前所述,当达到 5 分钟限制时,不管当前函数在做什么,都会停止执行。
-
受控终止:如果函数提供了回调,并且回调被执行以调用
context.done()
方法,则无论函数正在做什么,函数都会终止。 -
默认终止:函数结束并正常终止。同时,不会调用回调来触发
context.done()
方法。这种情况将被视为默认终止。 -
函数崩溃 或调用了
process.exit()
:如果函数发生崩溃或产生了段错误,函数将终止,因此容器也会停止。
容器重用
存在一种情况,即刚刚终止的函数容器可以被重用。
重用已完成的函数容器的能力可以大大减少启动时间,因为初始化过程会被完全跳过。同时,如果一个容器被重用,之前执行时写入/tmp
目录的文件可能仍然存在,这是一个缺点。
本地可执行文件
Lambda 实际上是为了在任何语言中运行代码而设计的,因为 Lambda 的沙箱只是一个容器。诀窍在于,我们可以使用一个 Node.js 程序在上传之前执行任何与 ZIP 文件一起打包的二进制文件。
值得注意的是,在为 Lambda 准备我们自己的二进制文件时,它必须是静态编译的,或者与 Amazon Linux 提供的共享库相匹配(因为 Lambda 上使用的所有容器都是基于 Amazon Linux 的)。我们有责任自己跟踪 Amazon Linux 的版本。
一个像 LambCI 这样的项目(github.com/lambci/docker-lambda
)可以帮助解决这个问题。LambCI 提供了一个本地沙箱环境,作为 Docker 容器,通过安装相同的软件、库、文件结构和权限来模拟 AWS Lambda 环境。它还定义了相同的一组环境变量以及其他行为。此外,用户名和组也被定义为与 Lambda 匹配,例如sbx_user1051
。
有了这个本地环境,我们可以在这个 Docker 容器内安全地测试我们的代码,并确保它在 Lambda 上运行时不会出问题。
Azure Functions
Azure Functions 是微软提供的无服务器计算平台,作为 Azure 云的一部分。所有设计目标与其他无服务器/FaaS 服务相同,Azure Functions 使我们能够执行应用逻辑,而无需管理自己的基础设施。
Azure Functions 在被事件触发时,以脚本的形式运行程序。当前版本的 Azure Functions 支持如 C#、F#、PHP、Node.js 或 Java 等语言运行时。对于 Azure 来说,支持 C# 和 F# 作为其功能的第一语言是很自然的,因为它们是微软自有的编程语言。无论如何,目前仅有 C#、F# 和 JavaScript(Node.js)是正式支持的语言。
使用 C#、F# 或 .NET 语言时,Azure Functions 允许我们通过 NuGet(.NET 的著名包管理器)安装依赖项。如果我们使用 Node.js 编写 JavaScript,Azure 还提供了对 NPM 包管理的访问。
类似于其他云服务提供商,Azure Functions 在访问其他 Azure 服务时具有优势,例如 Azure Cosmos DB、Azure Event Hubs、Azure Storage 和 Azure Service Bus。
很有趣的一点是,Azure Functions 的定价模型与 Amazon 或 Google 的产品有所不同。在 Azure 中,有两种定价计划可以满足不同的需求。
第一个是消费计划。这与其他云服务提供商提供的计划类似,我们只需为代码执行的时间付费。第二个是应用服务计划。在这种情况下,函数被视为其他应用程序的应用服务的一部分。如果函数属于这一类别,我们无需额外支付费用。
Azure Functions 的一个有趣特点是它的触发和绑定机制。Azure Functions 允许定义如何触发一个函数,以及如何在每个函数的输入和输出之间进行数据绑定,这些配置是分开的。这些机制有助于避免在调用函数时进行硬编码,以及在函数调用链中进行数据的进出转换。
扩展性
在 Azure 中,有一个组件可以实时监控每个 Azure Function 的请求数量。这个组件被称为扩展控制器。它收集数据,然后做出决定,来扩展或缩减该功能实例的数量。Azure 引入了应用服务的概念,一个功能应用可能包含多个功能实例。
所有决策都是基于启发式算法来处理不同类型的事件触发器。当功能扩展时,所有与该功能相关的资源也会被扩展。如果没有请求发送到该功能应用,功能实例的数量将自动缩减为零。
限制
每个函数实例将由函数应用的主机限制为 1.5 GB 的内存,这就像多个函数实例共享资源的一个组语义。所有函数都共享同一资源。
一个函数应用最多可以同时容纳 200 个函数实例。但没有并发限制。实际上,一个函数实例可以接受一个或多个请求。
每个事件触发器,例如,Azure Service Bus,都有其独特的启发式方式来扩展底层函数。
持久化函数
Azure Functions 最先进的扩展之一是持久化函数。持久化函数是一种在无服务器计算环境中实现有状态函数的技术。通过这个持久化扩展,提供了更多的状态管理、检查点和重启的概念。我们从这种函数中获得的是一个有状态的工作流,并且会有一个驱动程序作为协调者来调用其他函数,如下图所示:
图 3.2:Azure 中使用持久化函数扩展的协调器函数
当它完成调用其他函数后,无论是同步还是异步,协调器函数将允许将状态保存为本地变量。如果调用过程必须重新开始,或运行此协调器函数的虚拟机重新启动时,还会有一个检查点技术来继续/恢复协调器的状态。
Google Cloud Functions
谷歌公司提供的无服务器计算服务被称为Google Cloud Functions(GCF)。
本节中我们通常称其为 GCF。像其他无服务器平台一样,GCF 提供了执行环境和 SDK,帮助我们开发和管理整个函数生命周期。它提供了一个 SDK 帮助我们开始使用该框架。GCF 主要支持的语言是 JavaScript,并且有一个 Node.js Docker 镜像供我们使用。通过 Docker,构建一个函数非常方便。部署时,也可以通过 Google Cloud CLI 工具轻松部署。GCF 自然允许我们高效地连接到其他 Google 基于服务:
图 3.3:使用 Google Cloud Functions 实现的常见物联网用例
上述图示展示了一个在 Google Cloud 上实现的常见用例。它是一个使用所有 Google Cloud 服务的物联网管道示例。Google Cloud Function 用来计算来自消息队列的数据,并将其分发到大数据堆栈和 Firebase。Firebase 服务充当移动应用程序的后端即服务(BaaS)。在后面的章节中,我们将展示使用Parse 平台实现的类似 BaaS。
概述
在 FaaS 或无服务器平台中,函数的定义是它应该专注于单一的目标。由于函数的特性,它不应该过于复杂。正如我们在第一章中描述的,无服务器与 Docker,无服务器 FaaS 实际上是事件驱动编程模型的一个子集。GCF 上的所有云函数都遵循这种行为。我们应用程序流水线的每个单独组件通过将事件发送给另一个组件来连接。此外,事件是可以被监控的。当我们从源接收到一个事件时,关联的云函数将被触发并执行。
GCF 支持的函数必须使用 JavaScript 编写,或者使用能够转译为 JavaScript 的语言。本文写作时,执行函数的环境是 Node.js v6.11.5。基本上,开发人员会使用与该版本匹配的任何 Node.js 运行时。使用 JavaScript 和 Node.js 可以带来良好的可移植性,并允许开发人员在本地测试函数。此外,使用 Node.js 可以访问大量的 Node.js 库,包括平台提供的 API(cloud.google.com/nodejs/apis
),这些库有助于简化开发和集成。
GCF 被设计为连接或粘合服务的层。在某些用例中,我们使用函数来扩展现有的云服务。
通过事件驱动模型,函数可以监听并等待直到文件上传事件被触发,也就是当某些文件被放入云存储时。我们还可以监听远程区块链环境中的日志变化,或者我们订阅一个 Pub/Sub 主题并接收通知以触发函数。
我们通常将一些复杂的业务逻辑放在函数内部。Google 拥有的云函数能够访问 GCP 的凭据系统,因此它可以与大量的 GCP 服务进行身份验证。这个特性通常使得云函数在其平台上非常有用。
所有基础设施和系统软件层由 Google 的平台完全管理,因此我们只需关心我们的代码。自动扩展也是这种平台的常见特性。当触发次数增多时,额外的计算资源将自动进行配置。部署的函数将自动扩展以处理数百万次请求,而无需我们进一步的配置。
FaaS 函数的精细粒度概念使得这种计算非常适合实现自包含的 API 和 WebHooks(我们将在后续章节中演示)。Google Cloud Functions 支持多种工作负载的方面,例如数据处理/ELT、WebHooks、实现 API、作为移动应用程序的后端,以及接收来自 IoT 设备的流数据。
GCF 支持无服务器计算的多个方面。目前显而易见的局限性是它仅支持 Node.js 作为编程语言。GCF 在内部使用容器包装 Node.js 代码,并部署到其内部编排的 FaaS 系统上。这项工程的一部分已作为名为distroless的项目开源。我们可以通过提议的声明式容器的概念在最后一章中实现类似的功能。使用这个概念允许我们像 GCF 一样部署只包含应用程序的工作负载。
所有这些由 GCF 允许的用例将在后续章节中使用 Docker 和 FaaS 平台展示不同的方法。
执行模型
Google 为我们处理一切,包括硬件级别、操作系统、网络和应用程序运行时。在 GCF 上部署的函数将在一个自动管理的平台上运行。每个云函数将在基于容器的隔离环境中单独执行,这是一个安全的执行上下文。每个函数独立运行,不会干扰其他函数,同时共享同一主机。这与 Docker 和其他容器实现使用的概念相同。
在撰写本文时,Google Cloud Functions 选择仅支持运行在 Node.js v6.11.5 上的 JavaScript;然而,文档称他们将通过尽快与长期支持(LTS)版本的发布保持 Node.js 版本的更新。我们可以确信,Node.js 运行时的所有补丁版本和次要更新都将与上游发布匹配。
正如之前提到的,云函数也被放置在一个容器中。在谷歌云函数的情况下,其根文件系统基于Debian。 GCF 的基础镜像定期更新,并作为 Docker 镜像提供。可以从gcr.io/google-appengine/nodejs
拉取。以下是系统通过继承镜像并向其中安装 Node.js 版本 6.11.5 来准备基础镜像的方式:
FROM gcr.io/google-appengine/nodejs
RUN install_node v6.11.5
无状态性
在编写无服务器 FaaS 函数时,无状态是首选模型。为什么?因为在完全托管的执行环境中,我们不能期望我们函数的状态被保留。因此最好不要将任何东西保存到函数的本地存储中。如果我们需要内存,例如可能跨函数实例共享的全局变量,这些变量必须由外部存储服务显式管理。
在某些情况下,说一个函数是完全无状态的,会让我们没有充分利用该函数的执行上下文。正如我们所知道的,函数实际上是在容器隔离中运行的。当然,函数在执行期间向本地存储写入一些数据是完全可以的,但不期望将状态共享到容器外部。当我们在容器的上下文中说“无状态”时,它很可能指的是“无共享”(share-nothing)模式,而不是“无状态”本身。无共享模型是更适合用来描述基于容器的 FaaS 无状态性的术语。
超时
一般来说,无服务器平台通常会限制云函数的执行时间,以防止平台计算资源的过度使用。对于 Google Cloud Functions,默认的超时时间设为 1 分钟,用户可以根据需要将其延长至 9 分钟。当函数超时,运行的代码会被终止。例如,如果一个函数被计划在启动后 3 分钟运行,而超时时间设置为 2 分钟,那么这个函数将永远不会运行。
执行保证
在函数执行过程中,可能会发生错误。如果函数失败,它可能不会只执行一次。执行模型取决于函数的类型。
例如,一个简单的同步 HTTP 请求最多会被调用一次。这意味着函数调用将失败,并且不会重试。调用方需要自己处理错误和重试策略。
虽然异步函数至少会被调用一次,这是这些异步调用的特性,因此我们需要为该类型函数可能被多次调用的情况做好准备。此外,这些函数要修改的状态应该是幂等的且具备鲁棒性。例如,我们可能需要实现一个状态机来控制系统的状态。
IBM Cloud Functions
IBM Cloud Functions 是 IBM Cloud 提供的一项服务,它由 Apache OpenWhisk 提供支持;实际上是 IBM 向 Apache 基金会捐赠了 OpenWhisk。我们在本书后面有专门的章节介绍 OpenWhisk。
IBM 提供的 Cloud Functions 服务,在概念上与其他函数服务非常相似。函数围绕应用的业务逻辑进行封装,并在由 IBM 管理的事件驱动的 FaaS 环境中运行。
函数旨在响应来自其他 Web 或移动应用的直接 HTTP 调用,或者响应由其他支持的系统触发的事件,例如 Cloudant。IBM Cloud 提供了 Cloudant,这是一个建立在 CouchDB 之上的商业支持的 JSON 数据存储。我们可以在 Cloudant 系统中准备一个触发器,并在 Cloudant 中的数据发生变化时触发事件来调用 IBM Cloud Functions 中定义的函数。
函数的设计目标在各云服务提供商之间通常是相同的。它们为我们开发者提供了一种方式,让我们只专注于编写应用的业务逻辑,然后将代码作为云函数上传到各自的云服务。
要进一步探索 OpenWhisk 背后的概念,该引擎是 IBM Cloud 的一部分,请随时跳转到第六章,在 Docker 上运行 OpenWhisk,以了解更多关于 OpenWhisk 的信息。
Serverless 框架
Serverless 框架是一个应用开发框架和工具,适用于无服务器计算模式。这个框架与无服务器并无直接关系,它们只是共享了相同的名字,请不要混淆。
Serverless 框架的作者认为,无服务器应用是云原生生态系统中应用开发的下一个进化。这种应用需要一定程度的自动化,这一理念成为了框架的起源。
设计理念将托管服务和函数视为紧密耦合的实体。为了围绕它们构建应用,工具应提供构建、测试和部署命令,以使整个开发生命周期实现完全自动化。
还应有一种一致的方式来构建、测试和部署无服务器应用到多个云服务提供商,同时最小化代码变更。框架应该根据以下内容帮助配置每个云服务提供商的设置:
-
语言运行时
-
由应用开发者选择的云服务提供商
通过这种抽象层级,框架带来了实际的优势,使开发人员可以专注于应用的业务逻辑,而不是不断调整云配置以适应不同的云服务提供商。
Serverless 框架的创始人描述了四个优点:
-
Serverless 框架有助于加速开发过程,因为该框架包含基于 CLI 的命令来创建项目、构建,并且还帮助在相同的开发环境中测试应用。它节省了时间,因为 Serverless 框架独立于任何云服务提供商。框架还有一个机制,可以将新版本部署到云端,并允许在失败时回滚到之前的版本。
-
使用 Serverless 框架,我们可以独立于任何云服务提供商开发代码。因此,具有良好编程风格的代码可以在不同的云服务提供商之间迁移。例如,我们可以通过简单地将 YAML 文件中的提供商从 AWS Lambda 切换为 Google Cloud,然后重新部署,就能轻松迁移我们的函数。但实际上,这只是整个问题的一部分。真正让你绑定到某个供应商的不是代码,而是供应商提供的服务。因此,明智地选择支持的服务,可以有效解决这个问题。
-
Serverless Framework 帮助实现 基础设施即代码 (IaC) 。通过可以通过一组 API 进行部署的方式,我们实现了一定程度的自动化。这使我们能够将系统完全部署为多云应用程序。
-
最后,这个框架被广泛使用,并且有一个非常活跃的社区。这也是选择工具时的一个重要因素。由于他们为框架选择了 JavaScript 和 Node.js 作为基础语言,社区积极开发框架扩展。因此,向框架中添加新的提供商相对容易。一个值得注意的社区支持的提供商是 Kubeless。
练习
让我们通过尝试回答问题来复习一下,而不回顾内容:
-
AWS Lambda 的时间限制是多久?
-
你为什么认为云提供商限制 FaaS 函数的计算时间?
-
什么是 Azure 的持久性函数?它们有什么好处?
-
我们如何仅使用 Docker 测试 AWS Lambda 程序?
-
IBM Cloud Functions 背后的引擎是什么?你认为 IBM 开源它背后的原因是什么?
-
什么是 Serverless Framework?为什么它很重要?
-
我们如何使 FaaS 函数跨云提供商工作?你认为这真的可能吗?
-
请解释无状态和“无共享”模型之间的区别。
总结
在本节中,我们讨论了四个主要的无服务器计算平台、它们的一些特性和局限性。我们还讨论了 Serverless Framework,这是一个旨在帮助构建、测试和部署应用程序到多个无服务器计算平台的框架和工具。
在接下来的三章中,我们将看到云提供商提供的无服务器平台和允许我们使用 Docker 技术自行部署的无服务器/FaaS 平台的真正不同之处。
第四章:OpenFaaS 在 Docker 上
本章将介绍 OpenFaaS,这是一个使用软件容器作为部署单元的无服务器框架。OpenFaaS 最初是设计用来在 Docker Swarm 模式下运行并利用编排引擎的。
本章将从介绍 OpenFaaS 和解释其架构开始。然后,我们将讨论如何使用 OpenFaaS 来准备和部署函数。最后,本章将结束于如何为 OpenFaaS 安装 Grafana/Prometheus 仪表盘。
什么是 OpenFaaS?
OpenFaaS 是一个用于构建无服务器应用程序的框架和基础设施准备系统。它起源于 Docker Swarm 中的无服务器框架,现在支持其他类型的基础设施后端,如 Kubernetes 或 Hyper.sh。OpenFaaS 中的函数是容器。通过利用 Docker 的容器技术,任何用任何语言编写的程序都可以打包成一个函数。这使我们能够充分重用现有代码,消费各种 web 服务事件,而无需重写代码。OpenFaaS 是现代化旧系统以在云基础设施上运行的一个绝佳工具。
在云原生领域,有多个无服务器框架。然而,一些问题需要由 OpenFaaS 的原创作者 Alex Ellis 来解决。推动框架创建的动力在于塑造以下具有吸引力的特性:
-
易用性:基本上,许多无服务器框架由于由大公司构建并且是无服务器服务,天生就很复杂。另一方面,OpenFaaS 的目标是成为一个足够简单的无服务器技术栈,让开发者和小公司能够在自己的硬件上轻松部署和使用。OpenFaaS 还附带一个现成的 UI 门户,允许我们在浏览器中尝试函数调用。OpenFaaS 内置了自动扩展能力。它会自动测量函数调用的负载,并根据需求扩展或缩减实例。
-
可移植性:在容器生态系统中,有多个编排引擎,尤其是 Docker Swarm 和 Google 的 Kubernetes。OpenFaaS 最初设计时是为了在 Swarm 上运行,后来也支持 Kubernetes。它的功能在这些编排引擎之间是可移植的。OpenFaaS 不仅在运行时具有可移植性,它的功能实际上就是一个普通的 Docker 容器。这意味着任何类型的工作负载都可以作为函数容器重新打包,并简单地部署到 OpenFaaS 集群上。OpenFaaS 可以在任何基础设施上运行,包括本地硬件、私有云和公共云。
-
架构与设计的简洁性:OpenFaaS 的架构简单。它包括一个 API 网关,用于接受请求。然后,API 网关将请求传递给集群中的容器和带有看门狗的函数。看门狗是 OpenFaaS 的一个组件,稍后将在下一节中讨论。网关还会跟踪函数调用的次数。当请求量较大时,网关会触发编排引擎按需扩展函数的副本。
-
开放且可扩展的平台:OpenFaaS 设计为开放且可扩展的。凭借这种开放性和可扩展性,OpenFaaS 支持的 FaaS 后端数量随着时间的推移不断增加,因为任何人都可以为 OpenFaaS 提供新的后端。例如,如果我们想出于性能原因直接在容器运行时(如容器)中运行函数,我们可以通过为其编写一个新的 containerd 后端来扩展 OpenFaaS。
-
与语言无关:我们可以用任何 Linux 或 Windows 支持的语言编写 OpenFaaS 函数,然后将其打包为 Docker 或 OCI 容器镜像。
架构
我们曾经以单体式的方式构建系统。现在我们使用微服务。微服务可以被分解成更小的函数。显然,函数是架构演化的下一个步骤。
单体式是一种软件架构,其中包含可区分的软件关注点。每个服务都构建在一个单独的部署模块中。
微服务架构则将一个单一的庞大模块内的协调服务分离出来,形成外部松耦合的服务。
函数即服务(FaaS)是另一个分离层次。在这种架构中,微服务被拆分为更细粒度的单元,即函数:
图 4.1:单体式、微服务和函数架构
OpenFaaS 组件
本节解释了 OpenFaaS 的组成部分。这些组件包括 API 网关、函数看门狗以及 Prometheus 实例。它们都运行在 Docker Swarm 或 Kubernetes 编排引擎之上。API 网关和 Prometheus 实例作为服务运行,而函数看门狗作为函数容器的一部分运行。容器运行时可以是任何现代版本的 Docker 或 containerd:
图 4.2:OpenFaaS 架构概述
客户端可以是 curl
、faas-cli
或任何能够连接到 API 网关并调用函数的基于 HTTP 的客户端。函数容器在集群中由 API 网关管理,容器内有一个作为旁路进程(这种实现模式允许另一个旁路进程与主进程在同一个容器中运行)的函数看门狗。每个服务通过默认的主覆盖网络 func_functions
进行通信:
图 4.3:运行在 Docker Swarm 上的 OpenFaaS 内部架构
函数监控程序
函数监控程序是 OpenFaaS 的一个组件。它负责将实际的工作代码封装在函数程序周围。函数程序的要求仅仅是通过 标准输入(stdin)接受输入,并将结果输出到 标准输出(stdout)。
API 网关(gateway
)通过覆盖网络连接到函数容器。每个函数容器包含以下内容:
-
函数监控程序,
fwatchdog
-
用任何语言编写的某个函数程序
描述函数容器的 Dockerfile 必须有一个 fprocess
环境变量,指向函数程序名称和参数:
图 4.4:容器中函数监控程序与函数程序之间的交互
命令行界面
OpenFaaS 命令行界面是使用 OpenFaaS 的另一种方式。CLI 的最新版本可以直接从安装脚本 cli.openfaas.com
获取。对于 Linux 和 macOS,可以使用以下命令安装 CLI:
$ curl -sL https://cli.openfaas.com | sudo sh
当前,安装脚本支持运行在 ARM、ARM64 和 x64 芯片上的 macOS 和 Linux。CLI 被设计用来管理 OpenFaaS 函数的生命周期。我们可以使用 CLI 提供的子命令来构建、部署和调用函数。
CLI 实际上通过 API 网关暴露的一组控制平面 API 来控制 OpenFaaS。
API 网关
OpenFaaS API 网关提供路由机制,将你的函数暴露给外部世界。
当一个函数被外部请求调用时,函数的度量指标将被收集并放入 Prometheus 实例中。API 网关持续监控每个函数的请求数量,并通过 Docker Swarm API 按需扩展服务副本。基本上,OpenFaaS 完全利用 Docker Swarm 的调度机制进行自动扩展。API 网关还配备了内置用户界面,称为 UI 门户。该界面允许我们通过浏览器定义和调用函数。
安装 OpenFaaS
在开发机器上安装 OpenFaaS 极其简单。确保你安装了 Docker 17.05 或更高版本,安装完成后就可以开始使用了。
首先,我们需要初始化一个 Swarm 集群。单节点 Swarm 就足够在开发环境中使用:
$ docker swarm init
如果由于机器具有 多个网络接口 而无法初始化 Swarm,我们必须为参数 --advertise-addr
指定一个 IP 地址或接口名称。
OpenFaaS 可以通过直接从 GitHub 克隆源代码并运行deploy_stack.sh
脚本来启动。以下示例演示了如何启动 OpenFaaS 的版本 0.6.5。请注意,此目录中有docker-compose.yml
,该文件将被docker_stack.sh
用来部署 OpenFaaS Docker 堆栈:
$ git clone https://github.com/openfaas/faas \
cd faas \
git checkout 0.6.5 \
./deploy_stack.sh
Cloning into 'faas'...
remote: Counting objects: 11513, done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 11513 (delta 16), reused 19 (delta 8), pack-reused 11484
Receiving objects: 100% (11513/11513), 16.64 MiB | 938.00 KiB/s, done.
Resolving deltas: 100% (3303/3303), done.
Note: checking out '0.6.5'.
HEAD is now at 5a58db2...
Deploying stack
Creating network func_functions
Creating service func_gateway
Creating service func_alertmanager
Creating service func_echoit
Creating service func_nodeinfo
Creating service func_wordcount
Creating service func_webhookstash
Creating service func_decodebase64
Creating service func_markdown
Creating service func_base64
Creating service func_hubstats
Creating service func_prometheus
我们现在看到,多个服务已部署到 Docker Swarm 集群中。实际上,这是通过在 bash 脚本中运行docker stack deploy
命令实现的。OpenFaaS 使用的 Docker 堆栈名称是func
。
为了检查func
堆栈中的服务是否正确部署,我们使用docker stack ls
列出堆栈及其运行的服务:
$ docker stack ls
NAME SERVICES
func 11
现在我们知道有一个名为func
的 11 个服务的堆栈。让我们使用docker stack services func
查看它们的详细信息。我们使用格式化参数来让docker stack services func
命令显示每个服务的名称和端口。你可以省略--format
来查看每个服务的所有信息:
$ docker stack services func --format "table {{.Name}}\t{{.Ports}}"
NAME PORTS
func_hubstats
func_markdown
func_echoit
func_webhookstash
func_prometheus *:9090->9090/tcp
func_gateway *:8080->8080/tcp
func_decodebase64
func_base64
func_wordcount
func_alertmanager *:9093->9093/tcp
func_nodeinfo
一切启动并运行后,可以通过http://127.0.0.1:8080
打开 OpenFaaS 门户。以下截图显示了浏览器中运行的 OpenFaaS 门户。所有可用的函数都列在左侧面板中。点击某个函数名称后,主面板将显示该函数的详细信息。我们可以通过点击主面板上的 INVOKE 按钮来操作每个函数:
图 4.5:OpenFaaS 用户界面调用示例函数
我们将在下一节学习如何准备一个函数,以便在 OpenFaaS 平台上运行。
准备一个函数
在函数可以部署和调用之前,我们需要准备一个二进制程序并将其打包成函数容器。
以下是将程序打包成函数容器的步骤:
-
创建一个包含
FROM
指令的 Dockerfile,以从基础镜像派生出它。你甚至可以使用 Alpine 基础镜像。 -
使用
ADD
指令将函数监视程序二进制文件添加到镜像中。函数监视程序的名称是fwatchdog
,可以在 OpenFaaS 发布页面找到。 -
将函数程序添加到镜像中。我们通常使用
COPY
指令来完成此操作。 -
使用
ENV
指令定义名为fprocess
的环境变量,指向我们的函数程序。 -
使用
EXPOSE
指令暴露8080
端口给此容器镜像,当然,端口号是8080
。 -
定义此容器镜像的入口点。我们使用
ENTRYPOINT
指令指向fwatchdog
。
我们将做一些稍微不寻常的操作,但这是正确的方法,以准备一个函数容器。我们使用 Docker 的一个特性,称为多阶段构建,通过一个 Dockerfile 来编译程序并打包函数容器。
什么是多阶段构建?多阶段构建特性允许一个 Dockerfile 在构建过程中有多个构建阶段连接在一起。
使用这项技术,我们可以通过丢弃来自前一个构建阶段的较大镜像层,来构建一个非常小的 Docker 镜像。此功能需要 Docker 17.05 或更高版本。
打包 C 程序
这是一个不寻常但简单的函数示例。在这个示例中,我们将尝试将 C 程序编译、打包并部署为一个函数。为什么是 C 程序?基本上,如果我们知道可以打包 C 程序,那么任何传统程序都可以以类似的方式进行编译和打包。
我们知道,当设计一个函数时,它从 stdin
接收输入并将输出发送到 stdout
。然后,C 程序将通过 printf()
向 stdout
发送一条简单的语句:
#include <stdio.h>
int main() {
printf("%s\n", "hello function");
return 0;
}
通常情况下,这个 C 程序可以在复制并打包为容器之前使用 gcc
编译。但为了使 Dockerfile 自包含,将使用多阶段构建技术,通过单个 docker build
命令来编译和打包它作为一个函数。
以下多阶段的 Dockerfile 包含两个阶段。State 0
从 Alpine 3.6 镜像开始,然后安装 gcc
和 musl-dev
用于编译 C 程序。此阶段有一个命令来静态构建 C 程序,gcc -static
,这样它就不需要任何共享对象库:
###############
# State 0
###############
FROM alpine:3.6
RUN apk update apk add gcc musl-dev
COPY main.c /root/
WORKDIR /root/
RUN gcc -static -o main main.c
###############
# State 1
###############
FROM alpine:3.6
ADD https://github.com/openfaas/faas/releases/download/0.6.5/fwatchdog /usr/bin/
RUN chmod +x /usr/bin/fwatchdog
EXPOSE 8080
COPY --from=0 /root/main /usr/bin/func_c
ENV fprocess="/usr/bin/func_c"
ENTRYPOINT ["/usr/bin/fwatchdog"]
Stage 1 同样从 Alpine 3.6 基础镜像开始。它直接从 OpenFaaS GitHub 发布页面添加 fwatchdog
二进制文件,并将其模式更改为可执行 (chmod +x
)。此 Dockerfile 最重要的部分是在它从上一阶段 Stage 0 复制主二进制文件。这可以通过使用 COPY
指令与 --from
参数来完成。func_c
容器镜像的构建过程如下所示:
图 4.6:示例中的多阶段构建工作流程示意图
以下是之前 Dockerfile 中的代码,展示了如何使用 COPY
指令在阶段之间复制文件。在 Stage 1 中,COPY --from=0
表示该命令会将文件或一组文件从 Stage 0 复制到 Stage 1。在之前的示例中,它会将 /root/main
文件从 Stage 0 更改为 Stage 1 中的 /usr/bin/func_c
:
COPY --from=0 /root/main /usr/bin/func_c
在多阶段 Dockerfile 准备好之后,下一步是使用该 Dockerfile 执行 docker build
。
在执行此操作之前,将设置一个环境变量 DOCKER_ID
为你的 Docker ID。如果你没有 Docker ID,请访问 hub.docker.com
并在那里注册。使用此 DOCKER_ID
变量,你可以在不每次更改我的 Docker ID 为你的情况下执行这些命令:
$ export DOCKER_ID="chanwit" # replace this to yours Docker ID.
$ docker build -t $DOCKER_ID/func_c . # <- please note that there's a dot here.
函数容器的运行状态将类似于 图 4.7 中所示的镜像堆栈。最底层是操作系统内核之上的根文件系统。接下来的层次是基础镜像和依次叠加的镜像层,利用联合文件系统的能力。最上层是每个运行容器的可写文件系统,代表一个 OpenFaaS 函数:
图 4.7:作为运行容器的函数,顶部有一个可写的文件系统层
使用多阶段构建时,我们可以创建一个非常小的镜像,仅包含作为函数所需的二进制文件。通过丢弃整个 Stage 0 的镜像层(包括所有编译器和依赖项),最终镜像的大小被减少到大约 11 MB。可以通过运行 docker image ls $DOCKER_ID/func_c
来检查:
$ docker image ls $DOCKER_ID/func_c
REPOSITORY TAG IMAGE ID CREATED SIZE
chanwit/func_c latest b673f7f37036 35 minutes ago 11.6MB
请注意,OpenFaaS 机制会首先从仓库查找镜像。因此,在将容器镜像用作函数之前,将镜像推送到 Docker Hub 或您的仓库会更安全。这可以通过 docker image push
命令简单完成。请注意,在推送镜像之前,可能需要使用 docker login
进行身份验证:
$ docker image push $DOCKER_ID/func_c
使用 UI 定义和调用函数
在 OpenFaaS 上定义和调用函数非常简单。在推送镜像后,可以通过 OpenFaaS UI 门户来定义函数。首先,打开 http://127.0.0.1:8080/ui
。然后,你将在左侧面板中看到一个可点击的标签 CREATE NEW FUNCTION。点击后,将弹出定义函数的对话框。它需要该函数的 Docker 镜像名称;在这个例子中,镜像名称是 chanwit/func_c
。再次提醒,请不要忘记将我的 Docker ID 改为你的 Docker ID。其次,定义时需要一个函数名称。就命名为 func_c
。第三,我们需要定义 fprocess
字段的值,指向用于调用二进制程序的命令行。在这个示例中,命令行将在容器内简单地是 /usr/bin/func_c
。如果函数程序需要某些参数,也请在这里包含它们。最后,函数定义需要一个 Docker 覆盖网络的名称,以便 API 网关连接到函数容器。只需在此处包含默认的网络 func_functions
。需要特别注意的是,如果 OpenFaaS 堆栈部署到另一个环境,并且有不同的覆盖网络名称,必须记得指定正确的名称:
图 4.8:通过 UI 定义 OpenFaaS 函数
如果一切正常,点击 CREATE 来定义该函数。创建后,func_c
函数将显示在左侧面板中。点击函数名称将显示函数调用的主面板,如下所示:
图 4.9:调用func_c
函数及其响应体
如果一个函数需要任何输入,可以将文本或 JSON 格式的输入数据放置为请求体。然而,func_c
函数不接受任何输入,因此只需按下 INVOKE 按钮,函数就会被调用。在此示例中,调用过程已完成,状态为 OK:200
。API 网关从函数的二进制文件/usr/bin/func_c
获取标准输出,并以文本格式显示为响应体。
使用 OpenFaaS CLI
OpenFaaS CLI(faas-cli
)是一个命令行工具,帮助管理、准备和调用函数。在 Linux 上,可以使用以下命令安装 OpenFaaS CLI:
$ curl -sSL https://cli.openfaas.com | sudo sh
在 macOS 上,可以通过brew
使用以下命令进行安装:
$ brew install faas-cli
或者,在 Windows 上,可以直接从 OpenFaaS GitHub 仓库下载faas-cli.exe
并手动运行。
然而,我们假设每个示例都运行在 Linux 系统上。在以下示例中,将使用 OpenFaaS 的 Go 语言模板创建hello
函数,该模板可以在 GitHub 的openfaas/fass-cli
库中的template/go
目录下找到。
本地所有模板将存储在工作目录的template/
目录下。如果模板目录不存在,所有模板将从 GitHub 的openfaas/faas-cli
获取。从 OpenFaaS 0.6 版本开始,那里提供了 10 个适用于五种不同编程语言的模板。
定义一个新函数
要创建一个 Go 语言编写的函数,我们使用faas-cli new --lang=go hello
命令:
$ faas-cli new --lang=go hello
2017/11/15 18:42:28 No templates found in current directory.
2017/11/15 18:42:28 HTTP GET https://github.com/openfaas/faas-cli/archive/master.zip
2017/11/15 18:42:38 Writing 287Kb to master.zip
2017/11/15 18:42:38 Attempting to expand templates from master.zip
2017/11/15 18:42:38 Fetched 10 template(s) : [csharp go-armhf go node-arm64 node-armhf node python-armhf python python3 ruby] from https://github.com/openfaas/faas-cli
2017/11/15 18:42:38 Cleaning up zip file...
Folder: hello created.
___ _____ ____
/ _ \ _ __ ___ _ __ | ___|_ _ __ _/ ___|
| | | | '_ \ / _ \ '_ \| |_ / _` |/ _` \___ \
| |_| | |_) | __/ | | | _| (_| | (_| |___) |
\___/| .__/ \___|_| |_|_| \__,_|\__,_|____/
|_|
Function created in folder: hello
Stack file written: hello.yml
创建函数后,我们可以通过运行tree -L 2 .
命令检查函数目录的结构。该命令会显示两级深度的目录结构,如下所示:
$ tree -L 2 .
.
├── hello
│ └── handler.go
├── hello.yml
└── template
├── csharp
├── go
├── go-armhf
├── node
├── node-arm64
├── node-armhf
├── python
├── python3
├── python-armhf
└── ruby
首先,我们将查看hello.yml
文件中的函数定义。在hello.yml
文件中,有两个顶级项,provider
和functions
。
provider
块告诉我们其提供者的名称是faas
,即 Docker Swarm 中的默认 OpenFaaS 实现。同时,它告诉我们网关端点位于http://localhost:8080
,即 API 网关的一个实例正在运行。在生产环境中,这个 URL 可以更改为指向实际的 IP 地址。
functions
块列出了所有已定义的函数。在这个例子中,只有hello
函数。该块告诉我们这个函数是用 Go 编程语言编写的(lang: go
)。函数的处理程序由handler: ./hello
指定,指向包含真实工作函数源文件的目录(./hello/handler.go
)。在此示例中,输出镜像的名称由image: hello
指定。在构建函数之前,我们会将镜像名称更改为<your Docker ID>/hello:v1
,因为不建议使用:latest
标签,这是最佳实践。
############
# hello.yml
############
provider:
name: faas
gateway: http://localhost:8080
functions:
hello:
lang: go
handler: ./hello
image: hello # change this line to <your Docker ID>/hello:v1
构建并推送
我们将编辑最后一行,将其改为 image: chanwit/hello:v1
。再次提醒,不要忘记将我的 Docker ID 替换为你自己的。然后我们使用 faas-cli build
命令进行构建。我们使用 -f
来指定函数定义文件。请注意,构建这个 Dockerfile 会有两个阶段和 17 个步骤:
$ faas-cli build -f ./hello.yml
[0] > Building: hello.
Clearing temporary build folder: ./build/hello/
Preparing ./hello/ ./build/hello/function
Building: chanwit/hello:v1 with go template. Please wait..
Sending build context to Docker daemon 6.144kB
Step 1/17 : FROM golang:1.8.3-alpine3.6
---> fd1ada53b403
...
Step 17/17 : CMD ./fwatchdog
---> Running in a904f6659c33
---> f3b8ec154ee9
Removing intermediate container a904f6659c33
Successfully built f3b8ec154ee9
Successfully tagged chanwit/hello:v1
Image: chanwit/hello:v1 built.
[0] < Builder done.
Go 函数模板将从 template/go
目录复制到 build/hello
目录。然后,处理器文件 hello/handler.go
会被复制到 build/hello/function/handler.go
。程序的入口点在 build/hello/main.go
中定义,它又调用处理器函数。在构建过程中,faas-cli
会内部执行 docker build
命令。Dockerfile 中定义的步骤将用于编译和打包函数。
下图解释了 Dockerfile、源文件和模板之间的关系:
图 4.10:Go 语言的 OpenFaaS 模板及其相关组件
构建完成后,我们再次检查目录结构。这次运行 tree -L 3 .
来显示三层深度的目录,因为我们需要检查由 faas-cli build
命令创建的 build
目录的内容:
$ tree -L 3 .
.
├── build
│ └── hello
│ ├── Dockerfile
│ ├── function
│ ├── main.go
│ └── template.yml
├── hello
│ └── handler.go
├── hello.yml
└── template
我们可以直接将构建的镜像推送到 Docker 仓库,同样使用 faas-cli push
命令。使用 -f
来指定规范文件。规范文件中 functions.image
的值将用于推送:
$ faas-cli push -f hello.yml
[0] > Pushing: hello.
The push refers to a repository [docker.io/chanwit/hello]
8170484ad942: Pushed
071849fe2878: Pushed
a2e6c9f93e16: Pushed
76eeaa2cc808: Pushed
3fb66f713c9f: Pushed
v1: digest: sha256:fbf493a6bb36ef92f14578508f345f055f346d0aecc431aa3f84a4f0db04e7cb size: 1367
[0] < Pushing done.
部署与调用
要部署新构建的函数,我们使用 faas-cli deploy
命令。它通过 -f
参数读取函数规范,类似于其他子命令。在这个例子中,它使用提供者的网关值来部署函数。如果 Docker Swarm 上已经有一个以前运行的函数作为服务,旧的函数将在部署新函数之前被删除。部署完成后,手动调用该函数的 URL(例如通过 curl
)将显示出来:
$ faas-cli deploy -f hello.yml
Deploying: hello.
Removing old function.
Deployed.
URL: http://localhost:8080/function/hello
200 OK
要获取集群中所有正在运行的函数,我们可以运行 faas-cli list
命令。该命令还会显示每个函数的调用次数以及函数实例的副本数。当调用频率足够高时,副本数量会自动增加。所有这些信息都存储在 Prometheus 实例中。我们将在下一节通过 Grafana 仪表板更好地查看这些信息:
$ faas-cli list
Function Invocations Replicas
func_echoit 0 1
func_wordcount 0 1
func_webhookstash 0 1
func_markdown 0 1
func_hubstats 0 1
func_decodebase64 0 1
hello 0 1
func_base64 0 1
func_nodeinfo 0 1
hello
函数通过 stdin
接受输入,并通过 stdout
输出结果。为了测试函数的调用,我们将一句话回显并通过管道传递给 faas-cli invoke
命令的 stdin
。这个调用通过 OpenFaaS 框架处理,所有的调用统计数据都会记录在集群中的 Prometheus 实例上:
$ echo "How are you?" | faas-cli invoke hello
Hello, Go. You said: How are you?
模板
预定义的模板对于字符串处理和开发简单函数来说可能足够了,但当事情变得复杂时,了解如何自己调整 OpenFaaS 模板就变得非常重要。
在这一部分,将调整 Go 模板以简化示例中的构建步骤数量。可以在template/go/Dockerfile
找到以下 Go 模板的 Dockerfile。此 Dockerfile 已经使用了多阶段构建技术:
###################
# State 0
###################
FROM golang:1.8.3-alpine3.6
# ... lines removed for brevity
###################
# State 1
###################
FROM alpine:3.6
RUN apk --no-cache add ca-certificates
# Add non root user
RUN addgroup -S app adduser -S -g app app \
mkdir -p /home/app \
chown app /home/app
WORKDIR /home/app
COPY --from=0 /go/src/handler/handler .
COPY --from=0 /usr/bin/fwatchdog .
USER app
ENV fprocess="./handler"
CMD ["./fwatchdog"]
模板可以托管在自定义 Git 存储库中。以下是一个模板存储库的结构,可以通过template
子命令获取。第一级必须是名为template/
的目录。在template
目录内,可能会有多个目录,例如,在以下结构中有go/
目录:
$ tree .
.
├── README.md
└── template
└── go
├── Dockerfile
├── function
│ └── handler.go
├── main.go
├── README.md
└── template.yml
将整个模板源代码存储在 GitHub 存储库中后,可以使用faas-cli template pull
稍后进行拉取和调整:
$ faas-cli template pull https://github.com/chanwit/faas-templates
Fetch templates from repository: https://github.com/chanwit/faas-templates
2017/11/16 15:44:46 HTTP GET https://github.com/chanwit/faas-templates/archive/master.zip
2017/11/16 15:44:48 Writing 2Kb to master.zip
2017/11/16 15:44:48 Attempting to expand templates from master.zip
2017/11/16 15:44:48 Fetched 1 template(s) : [go] from https://github.com/chanwit/faas-templates
2017/11/16 15:44:48 Cleaning up zip file...
在拉取调整后的模板之后,可以重新构建镜像,并将构建步骤数量减少到15:
$ faas-cli build -f hello.yml
[0] > Building: hello.
Clearing temporary build folder: ./build/hello/
Preparing ./hello/ ./build/hello/function
Building: chanwit/hello:v1 with go template. Please wait..
Sending build context to Docker daemon 7.68kB
Step 1/15 : FROM golang:1.8.3-alpine3.6
---> fd1ada53b403
...
Step 15/15 : CMD ./fwatchdog
---> Using cache
---> 23dfcc80a031
Successfully built 23dfcc80a031
Successfully tagged chanwit/hello:v1
Image: chanwit/hello:v1 built.
[0] < Builder done.
OpenFaaS 仪表板
在 Grafana 平台上有一个良好的 OpenFaaS 仪表板。要使 Grafana 与 OpenFaaS 配合工作,Grafana 服务器必须在相同的网络上。我们可以使用以下命令通过docker service create
在 OpenFaaS 堆栈外运行 Grafana 服务器。它通过--network=func_functions
参数与 OpenFaaS 堆栈进行链接:
$ docker service create --name=grafana \
--network=func_functions \
-p 3000:3000 grafana/grafana
或者,可以在http://localhost:3000
打开仪表板。使用用户名admin
和密码admin
登录:
图 4.11:Grafana 主页仪表板
在将其用作仪表板数据源之前,必须创建并指向 Prometheus 服务器的数据源。首先,数据源名称必须为prometheus
。其次,URL 需要指向http://prometheus:9090
。之后,我们可以点击保存和测试按钮。如果数据源设置正确,将显示绿色弹出窗口:
图 4.12:在 Grafana 中定义一个新的 Prometheus 数据源
接下来,可以使用仪表板的 ID 导入 OpenFaaS 仪表板。我们将使用仪表板号3434
,然后点击加载以准备导入仪表板:
图 4.13:在 Grafana 中导入仪表板的屏幕
接下来,对话框将更改为从 Grafana.com 导入仪表板。在这里,它将要求我们包括仪表板名称。我们可以将其保留为默认名称。它还会询问我们想要使用哪个数据源。选择之前步骤中已定义的 Prometheus 数据源。之后,点击导入按钮完成导入过程:
图 4.14:设置仪表板名称并选择其 Prometheus 数据源
以下是仪表板的展示。它在一个框中显示了网关的健康状态,并以仪表的形式显示网关服务的数量。总函数调用统计以线形图展示,配有数字。在测试中,Go 编写的 hello
函数被线性调用超过 20,000 次。在测试过程中,函数副本的数量从 5 个扩展到了 20 个。然而,由于测试是在单机上进行的,因此调用速率没有显著变化:
图 4.15:OpenFaaS 仪表板的实际操作
以下是允许 OpenFaaS 自动扩展函数副本的机制。首先,当客户端通过 API 网关请求函数调用时,该调用将存储在 Prometheus 中。在 Prometheus 内部,有一个 Alert Manager,它负责在预定义规则匹配时触发事件。OpenFaaS 为 Alert Manager 定义了一条规则,通过将事件与其 Alert Handler URL http://gateway:8080/system/alert
关联,来扩展副本数量。这个 Alert Handler 将负责计算副本数量,检查最大副本限制,并通过 Swarm 客户端 API 向集群发送 scale
命令,从而扩展某个函数的副本。下图展示了这个自动扩展机制背后的步骤:
图 4.16:OpenFaaS 在 Docker Swarm 中自动扩展函数服务副本的告警机制
练习
以下是帮助你回顾本章中需要记住和理解的所有主题的问题列表:
-
使用 OpenFaaS 有什么优势?
-
请描述 OpenFaaS 的架构。各个组件是如何相互通信的?
-
我们如何在 Docker Swarm 上部署 OpenFaaS 堆栈?
-
为什么 OpenFaaS 使用多阶段构建?
-
我们如何为 Node.js 创建一个新的 OpenFaaS 函数?
-
我们如何构建并打包一个 OpenFaaS 函数?
-
OpenFaaS 使用的覆盖网络的默认名称是什么?
-
什么是函数模板?它的用途是什么?
-
描述准备自定义模板并将其托管在 GitHub 上的步骤。
-
我们如何为 OpenFaaS 定义 Grafana 仪表板?
摘要
本章讨论了 OpenFaaS 及其架构,以及我们如何将其作为无服务器框架在 Docker Swarm 上部署函数。OpenFaaS 具有多个令人信服的特性,特别是其易用性。本章展示了在 Docker Swarm 基础设施中部署 OpenFaaS 堆栈非常简单。接着,本章继续讨论了如何定义、构建、打包和部署 OpenFaaS 函数。它还讨论了如何调整和准备自定义模板的高级话题。
监控 OpenFaaS 非常简单,因为它内置了 Prometheus。我们只需要安装 Grafana 仪表板并将其连接到 Prometheus 数据源,就能获得一个现成的仪表板,帮助我们操作 OpenFaaS 集群。
下一章将介绍 Fn 项目,它允许我们在普通的 Docker 基础设施上部署 FaaS 平台。
第五章:Fn 项目
本章介绍了一个 FaaS 平台,Fn 项目。它是由 Oracle Inc. 团队开发的另一个出色的 FaaS 框架。Fn 是其中一个最简单的项目,允许我们在纯 Docker 基础设施上部署 FaaS 平台。
本章从讨论 Fn 项目是什么开始。然后我们将继续探讨它的组件如何组织,以及它的整体架构。接下来,我们将学习如何使用 Fn CLI 准备和部署函数。本章最后将讨论如何使用 Fn 子项目来管理其 UI、扩展和监控 Fn 集群本身。
本章将涵盖以下内容:
-
Fn 项目
-
Fn 的架构
-
使用 Fn CLI
-
部署本地函数
-
在 Docker Swarm 上部署 Fn
-
使用内置 UI 监控 Fn
-
使用熟悉的工具进行日志分析
Fn 项目
Fn 项目最初由Iron.io团队(www.iron.io/
)在 Iron function 名称下构思。此后,两位创始人加入了 Oracle,并将 Iron function 分支为新的项目 Fn。
Fn 是一个框架和系统,用于开发和部署无服务器/FaaS 应用程序。与 OpenFaaS 不同,Fn 不使用任何编排器级别的功能来管理函数容器。
Fn 不仅支持通过其自身基础设施进行部署;它还允许你将相同的函数部署到 AWS Lambda。然而,我们这里只讨论如何将函数部署到其自身的基础设施,当然,这个基础设施是基于 Docker 的。
Fn 背后有几个设计原因。
Fn 项目致力于开源。它原生支持 Docker,这意味着我们可以将 Docker 容器作为其部署单元——一个函数。Fn 支持任何编程语言的开发。Fn 基础设施是用 Go 编程语言编写的,旨在能够在各处部署,包括公共云、私有云,甚至混合基础设施。Fn 还支持从 AWS 导入 Lambda 函数并将它们部署到自身的基础设施中。
如前所述,基于 Docker 的无服务器/FaaS 基础设施基本上旨在平衡控制整个系统与基础设施的维护和管理的便利性。Fn 也有与这一理念相一致的设计目标。
Fn 的架构
Fn 服务器的最简单设置只是启动一个独立的 Fn 容器;然而,更完整的架构将如图所示。本章最后将演示集群实现。下图展示了 Fn 架构的概览:
图 5.1:Fn FaaS 集群的架构
与常见的 FaaS 架构一样,Fn 也有API 网关,在前面的图示中为Fn LB。Fn LB 基本上是一个负载均衡器。它将来自客户端的请求转发到每个Fn Server。在 Fn Server 的实现中,没有像 Fn 架构核心中的事件总线那样的发起者和执行者的分离概念。因此,Fn Server 也充当执行者,在其关联的 Docker 引擎上执行函数。
Fn 服务器连接到一个Log Store,它可以是一个独立的数据库系统或一个数据库管理系统的集群。所有从 Fn 函数发送到标准错误的数据显示都会记录到Log Store。
Fn UI 和 Fn LB 是额外的组件,有助于在生产环境中改进 Fn 项目。Fn UI 是用户界面服务器,如仪表盘,用于 Fn,而 Fn LB 是负载均衡器,用于在集群中的 Fn 节点之间进行轮询。
在 Fn Server 中有一个执行者代理的概念。该代理负责控制运行时环境。在 Fn 中,运行时是 Docker。因此,在本章节中,执行者代理也称为Docker 代理。在默认配置下,Fn Server 中的 Docker 代理连接到本地 Docker 引擎,并通过本地 Unix 套接字启动 Fn 函数:
图 5.2:显示 Fn 集群在 Swarm 范围网络上的示意图
上面的图示展示了一个在 Swarm 范围覆盖网络上的运行 Fn 集群。为了组成一个集群,我们将使用一个可附加的 Swarm 范围网络。每个 Fn Server 实例都需要连接到该网络。当请求被发送到网关或直接发送到服务器时,它将被传递到EntryPoint。EntryPoint 是一个特定语言的程序,它包装了真正的函数程序。例如,在使用 Java 构建的 Fn 函数中,EntryPoint 是类 com.fnproject.fn.runtime.EntryPoint
。这个 Java 类内部的代码通过 Java 的反射技术调用真正的函数:
图 5.3:一个 Fn 函数与 STDIN、STDOUT 交互并将日志写入 STDERR,且将日志委托给存储
Fn 服务器将请求主体以STDIN的形式发送到Function容器。当EntryPoint接收到STDIN流后,它会将数据内容转换为匹配函数签名类型的格式。在前面的图示中,签名是String。因此,函数体被转换为字符串。发送到STDOUT的输出将被转发到Fn Server并作为结果发送出去,而发送到STDERR的输出将被捕获并存储在Log Store中。
使用 Fn CLI
本节将讨论如何使用 Fn CLI 的基本功能,这是一个控制 Fn 的命令行工具。我们先从 Fn CLI 的安装开始。确保你的系统中存在curl
命令:
$ curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh
安装完前面的命令后,通过输入fn
检查其版本和帮助信息。撰写时,命令行的当前版本是0.4.43
。由于变化迅速,你可以预期使用不同版本的情况:
$ fn
fn 0.4.43
Fn command line tool
ENVIRONMENT VARIABLES:
FN_API_URL - Fn server address
FN_REGISTRY - Docker registry to push images to, use username only to push to Docker Hub - [[registry.hub.docker.com/]USERNAME]
COMMANDS:
...
fn
提供了几个子命令,例如:
-
fn start
是docker run
命令的一个简单包装器。此子命令启动新的 Fn Server 实例。默认地址将是http://localhost:8080
。然而,Fn CLI 将尝试连接到在环境变量中定义的FN_API_URL
地址(如果已设置)。 -
fn update
是用于将最新版本的 Fn Server 拉取到本地 Docker 镜像的命令。 -
fn init
是用于初始化一个骨架以开发新函数的命令。它接受--runtime
参数,用于生成特定语言的模板,例如 Go 语言。 -
fn apps
包含用于创建、更新和删除应用程序的子命令,是一种命名空间或包,用于将多个函数组合在一起。要求函数必须在一个应用程序下定义。 -
fn routes
是一组用于定义指向函数容器的路由的命令。例如,我们有一个名为demo
的应用程序,然后可以定义路由hello
并将其指向 Docker 容器镜像test/hello:v1
。一个应用程序可以有多个路由:
图 5.4:Fn 应用程序及其路由之间的关系
这是 Fn 如何在应用程序下组织路由的示例。例如,Fn 的 API URL 是http://localhost:8080
。我们可能有一个名为demo
的应用程序,其中包含一个名为hello
的路由,该路由为容器镜像test/hello:v1
创建。所有这些组合在一起形成了一个访问该函数的完整 URL。
让我们部署一个本地函数
首先,执行fn start
以启动一个独立的 Fn Server 实例。服务器通过设置日志级别为info
(默认设置)启动。Fn Server 然后连接到数据存储,即日志存储。当前的实现是 SQLite3。之后,代理将启动。Docker 代理使用其默认配置连接到本地 Docker 引擎。最后,Fn 开始监听端口8080
:
$ fn start
time="2018-03-17T08:48:39Z" level=info msg="Setting log level to" level=info
time="2018-03-17T08:48:39Z" level=info msg="datastore dialed" datastore=sqlite3 max_idle_connections=256
time="2018-03-17T08:48:40Z" level=info msg="agent starting cfg=&{MinDockerVersion:17.06.0-ce FreezeIdle:50ms EjectIdle:1s HotPoll:200ms HotLauncherTimeout:1h0m0s AsyncChewPoll:1m0s MaxResponseSize:0 MaxLogSize:1048576 MaxTotalCPU:0 MaxTotalMemory:0 MaxFsSize:0}"
time="2018-03-17T08:48:40Z" level=info msg="no docker auths from config files found (this is fine)" error="open /root/.dockercfg: no such file or directory"
______
/ ____/___
/ /_ / __ \
/ __/ / / / /
/_/ /_/ /_/
v0.3.381
time="2018-03-17T08:48:41Z" level=info msg="available memory" availMemory=12357627495 cgroupLimit=9223372036854771712 headRoom=1373069721 totalMemory=13730697216
time="2018-03-17T08:48:41Z" level=info msg="sync and async ram reservations" ramAsync=9886101996 ramAsyncHWMark=7908881596 ramSync=2471525499
time="2018-03-17T08:48:41Z" level=info msg="available cpu" availCPU=4000 totalCPU=4000
time="2018-03-17T08:48:41Z" level=info msg="sync and async cpu reservations" cpuAsync=3200 cpuAsyncHWMark=2560 cpuSync=800
time="2018-03-17T08:48:41Z" level=info msg="Fn serving on `:8080`" type=full
为了检查 Docker 是否正确启动了 Fn Server,我们可以使用docker ps
查看正在运行的容器。可以在另一个终端执行此操作:
$ docker ps --format="table {{.ID}}\t{{.Names}}\t{{.Ports}}"
CONTAINER ID NAMES PORTS
ab5cd794b787 fnserver 2375/tcp, 0.0.0.0:8080->8080/tcp
好的,现在我们已经看到 Fn Server 正在端口8080
上运行,并且通过docker ps
看到映射0.0.0.0:8080->8080/tcp
。
在启动fn start
命令的当前目录中,容器将其data
目录映射到主机的$PWD/data
。该目录包含 SQLite3 数据库文件,用于存储日志和信息。在生产环境中,我们将用 MySQL DBMS 替换它,例如:
$ tree
.
└── data
├── fn.db
└── fn.mq
1 directory, 2 files
要查看应用程序列表,只需使用fn apps list
命令:
$ fn apps list
no apps found
好吧,由于我们刚刚启动了服务器实例,因此没有新创建的应用程序。我们将创建一个,命名为demo
,然后再次使用fn apps list
命令来双重确认创建的应用:
$ fn apps create demo
Successfully created app: demo
$ fn apps list
demo
现在我们将开始开发一个函数。在这个示例中,我们使用 Java 运行时,稍后我们将尝试使用 Go 的另一种运行时。
让我们初始化新函数。我们使用fn init
来创建一个新的函数项目。此命令需要--runtime
来指定我们希望使用的语言运行时。
func.yaml
是我们的函数描述文件。它包含版本号、运行时和函数的入口点:
$ fn init --runtime java hello
Creating function at: /hello
Runtime: java
Function boilerplate generated.
func.yaml created.
我们将尝试学习如何构建和部署一个函数。所以让我们先不修改任何内容,直接构建它。要构建函数,只需使用fn build
。而要部署函数,我们有fn deploy
来为我们处理整个过程。
这是 Fn 的构建行为。在调用fn build
命令后,构建过程开始,使用生成的 Dockerfile。生成的镜像将被打标签并由 Docker 引擎本地存储。例如,示例中的镜像将被本地标记为hello:0.0.1
。然后,使用fn deploy
命令时,需要--registry
来将镜像远程存储在 Docker Hub 上。在此示例中,使用的是我的 Docker ID,请记得将其更改为您的 ID。
fn deploy
命令的工作方式如下。
首先,它增加了函数的版本号。其次,它使用--registry
将函数的镜像推送到 Docker Hub,作为仓库名称。因此,hello:0.0.2
在 Docker Hub 上变成了chanwit/hello:0.0.2
。
然后,fn deploy
将使用新构建镜像的名称,在--app
指定的应用程序下注册一个新路由:
$ fn build
Building image hello:0.0.1
Function hello:0.0.1 built successfully.
$ fn deploy --app demo --registry chanwit
Deploying hello to app: demo at path: /hello
Bumped to version 0.0.2
Building image chanwit/hello:0.0.2
Pushing chanwit/hello:0.0.2 to docker registry...The push refers to repository [docker.io/chanwit/hello]
07a85412c682: Pushed
895a2a3582de: Mounted from fnproject/fn-java-fdk
5fb388f17d37: Mounted from fnproject/fn-java-fdk
c5e4fcfb11b0: Mounted from fnproject/fn-java-fdk
ae882186dfca: Mounted from fnproject/fn-java-fdk
aaf375487746: Mounted from fnproject/fn-java-fdk
51980d95baf3: Mounted from fnproject/fn-java-fdk
0416abcc3238: Mounted from fnproject/fn-java-fdk
0.0.2: digest: sha256:c7539b1af68659477efac2e180abe84dd79a3de5ccdb9b4d8c59b4c3ea429402 size: 1997
Updating route /hello using image chanwit/hello:0.0.2...
让我们检查一下新注册的路由。我们使用fn routes list <app>
命令来列出应用程序<app>
下的所有路由。在下面的示例中,列出了demo
的所有路由:
$ fn routes list demo
path image endpoint
/hello chanwit/hello:0.0.2 localhost:8080/r/demo/hello
上一个命令还列出了每个路由的端点。通过端点,我们基本上可以像普通 HTTP 端点一样使用curl
与其交互。不要忘记为curl
设置-v
详细选项。通过此选项,我们可以检查 HTTP 头部中隐藏的内容。
让我们看一下 HTTP 响应头中标记为粗体的行。这里有一些额外的条目,Fn_call_id
和Xxx-Fxlb-Wait
。
头部信息,Fn_call_id
,是每次调用的标识符。此 ID 在我们启用 Fn 的分布式追踪时也将使用。头部信息,Xxx-Fxlb-Wait
,是 Fn LB 收集的信息,它可以知道此函数的等待时间:
$ curl -v localhost:8080/r/demo/hello
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /r/demo/hello HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 13
< Content-Type: text/plain
< Fn_call_id: 01C8SPGSEK47WGG00000000000
< Xxx-Fxlb-Wait: 78.21µs
< Date: Sat, 17 Mar 2018 10:01:43 GMT
<
* Connection #0 to host localhost left intact
Hello, world!
再试一次使用 Golang
让我们尝试使用另一种运行时 Go 创建下一个函数。与 Java 不同,Go 语言代码在 Fn 函数内没有明确的入口点概念。幸运的是,Fn 的执行模型足够简单,因此这个问题非常微不足道:
$ fn init --runtime go hello_go
Creating function at: /hello_go
Runtime: go
Function boilerplate generated.
func.yaml created.
这是 Go Fn 函数的文件列表:
$ cd hello_go
$ tree .
.
├── func.go
├── func.yaml
└── test.json
文件func.go
当然就是函数程序本身,而func.yaml
是 Fn 的函数描述符。其中一个有趣的文件是test.json
——它是包含功能测试的测试数据文件。目前,我们可以使用fn test
命令来测试正向路径,但无法测试负向结果。
我们首先来看一下func.yaml
,了解它的内容。每次部署时,version
会自动增加。这里的runtime
是go
,因为我们在fn init
时指定了--runtime
参数。这里的entrypoint
不应修改。就保持原样,信任我:
$ cat func.yaml
version: 0.0.1
runtime: go
entrypoint: ./func
Go 代码可以直接消费 STDIN。最好的方法是将输入作为 JSON 传递,并使用 Go 的encoding/json
包来处理数据。以下是从原始 Fn 示例修改的程序。这个程序被修改为简化输出过程,并添加了错误检查和日志记录:
package main
import (
"encoding/json"
"fmt"
"os"
)
type Message struct {
Name string
}
func main() {
m := &Message{Name: "world"}
err := json.NewDecoder(os.Stdin).Decode(m)
if err != nil {
fmt.Fprintf(os.Stderr, "err JSON Decode: %s\n", err.Error())
os.Exit(250)
}
fmt.Printf(`{"success": "Hello %s"}`, m.Name);
os.Exit(0)
}
在每个程序中,我们都需要检查错误并处理它们。如前面的示例所示,我们检查编码时发生的错误,然后将错误消息打印到 Go 中的标准错误文件os.Stderr
。然后我们只需使用代码> 0
退出进程。在这里,我们使用250
。
让我们总结一下 Fn 中的错误处理和日志记录。首先,写入 STDERR 的消息将被存储在日志中。其次,使用错误代码退出进程,即> 0
。Fn 随后会将容器执行标记为错误。
让我们来看一下实际操作。确保我们在func.go
中有之前的代码示例,并使用fn deploy
命令将其部署:
$ fn deploy --app demo --registry chanwit
Deploying hello_go to app: demo at path: /hello_go
Bumped to version 0.0.2
Building image chanwit/hello_go:0.0.2 .......
Pushing chanwit/hello_go:0.0.2 to docker registry...The push refers to repository [docker.io/chanwit
/hello_go]
00a6a1467505: Pushed
96252b84ae14: Pushed
97dedccb7128: Mounted from fnproject/go
c9e8b5c053a2: Mounted from fnproject/go
0.0.2: digest: sha256:8a57737bff7a8e4444921959532716654230af0534b93dc6be247ac88e4e7ef2 size: 1155
Updating route /hello_go using image chanwit/hello_go:0.0.2...
如果fn deploy
的最后一行显示路由已更新,那就表示已经准备好。
接下来,我们将使用fn call
命令来调用该函数,该函数现在已经注册为demo
应用程序下的一个路由。尝试在没有参数的情况下调用它,这会导致错误:
$ fn call demo /hello_go
{"error":{"message":"container exit code 250"}}
ERROR: error calling function: status 502
这是我们预期的结果。它是一次没有输入的调用。因此,encoding/json
抛出了错误,程序将日志消息写入了 STDERR(在之前的代码中没有显示)。最后,函数返回了250
。通过这个消息,我们看到fn call
打印出函数容器以250
代码退出。所以错误得到了正确处理。
这里没有日志消息,但我们稍后会回到它们。
接下来,我们将进行一次成功的调用。为了使其显示为绿色,只需使用echo
命令传递 JSON 主体。JSON 主体将通过管道传递给fn call
,并转换为 HTTP 请求,然后它将被 Fn 服务器接收并再次序列化为函数程序的 STDIN。
成功的 JSON 块是我们对一个正常工作的程序的预期输出。
使用fn call
调用远程函数的语法是,我们需要传递应用程序名称和路由名称,这样它才能被调用:
$ echo '{"Name": "chanwit"}' | fn call demo /hello_go
{"success": "Hello chanwit"}
检查呼叫日志和错误
要查看所有调用日志,请使用fn calls
命令。请注意,命令是带有 s 的calls
。fn calls list
命令接受应用程序的名称。需要关注的属性是ID
和Status
。以下示例显示了两个调用日志,第一个是error
,第二个是success
,按时间倒序排列:
$ fn calls list demo
ID: 01C8VRGN9R47WGJ00000000000
App: demo
Route: /hello_go
Created At: 2018-03-18T05:15:04.376Z
Started At: 2018-03-18T05:15:04.738Z
Completed At: 2018-03-18T05:15:07.519Z
Status: success
ID: 01C8VRFE3647WGE00000000000
App: demo
Route: /hello_go
Created At: 2018-03-18T05:14:24.230Z
Started At: 2018-03-18T05:14:24.566Z
Completed At: 2018-03-18T05:14:27.375Z
Status: error
现在,我们选择第二个调用 ID 来获取日志消息。用于检索日志的命令是fn logs get
。它需要应用程序名称和调用 ID:
$ fn logs get demo 01C8VRFE3647WGE00000000000
err JSON Decode: EOF
之前的日志消息是 Go 程序输出到os.Stderr
的内容。
在 Docker Swarm 上部署 Fn
在这个示例中,我们在 Swarm 范围内的网络上启动一个 Fn 集群。
从部署网络开始,我们使用weaveworks/net-plugin
作为骨干网络,以确保稳定性。请注意,网络必须是可附加的,并且子网必须位于10.32.0.0/16
的范围内。所以,10.32.3.0/24
在这里完全合适:
$ docker network create \
--driver weaveworks/net-plugin:2.1.3
--attachable \
--subnet 10.32.3.0/24 \
fn_net
然后,我们为数据存储准备一个卷。由于本节还希望展示一个产品级的设置,我们使用 MySQL 作为存储,而不是默认的 SQLite3。使用 MySQL 使我们能够横向扩展 Fn 服务器的数量。
卷将使用docker volume create
命令创建。如果我们想要设置一个 MySQL 集群,设置会比这个稍微复杂一些,但这本书不会涉及:
$ docker volume create mysql_vol
这是启动 MySQL 实例的docker run
命令。我们将实例连接到先前创建的网络fn_net
。我们在此指定网络别名,以确保服务必须通过名称mysql
来访问。所有环境变量的设计是为了设置用户名、密码和默认数据库fn_db
。不要忘记将卷mysql_vol
绑定到容器内的/var/lib/mysql
。这是为了确保数据在容器被移除时仍然存活:
$ docker run \
--detach \
--name mysql \
--network fn_net \
--network-alias mysql \
-e MYSQL_DATABASE=fn_db \
-e MYSQL_USER=func \
-e MYSQL_PASSWORD=funcpass \
-e MYSQL_RANDOM_ROOT_PASSWORD=yes \
-v mysql_vol:/var/lib/mysql \
mysql
下一步是启动 Fn 服务器。本节展示了如何启动两个指向相同日志存储(MySQL)的 Fn 服务器。每个 Fn 服务器都连接到fn_net
。第一个实例命名为fn_0
。Fn 服务器需要FN_DB_URL
来指向外部日志存储,可能是 PostgreSQL 或 MySQL。只需像以下命令中所示,输入完整的 URL。我们还将容器命名为fn_0
,以便于管理。
当有这样的设置时,Fn 服务器变得完全无状态,所有状态将被存储在数据库外部。所以,当出现问题时,完全可以安全地移除 Fn 服务器容器:
$ docker run --privileged \
--detach \
--network fn_net \
--network-alias fn_0 \
--name fn_0 \
-e "FN_DB_URL=mysql://func:funcpass@tcp(mysql:3306)/fn_db" \
fnproject/fnserver
让我们启动另一个,fn_1
。基本上,这应该在一个单独的节点(物理或虚拟)上完成:
$ docker run --privileged \
--detach \
--network fn_net \
--network-alias fn_1 \
--name fn_1 \
-e "FN_DB_URL=mysql://func:funcpass@tcp(mysql:3306)/fn_db" \
fnproject/fnserver
好的,在设置好所有 Fn Server 实例后,现在是时候将它们聚合在一起了。我们使用 Fn LB 作为所有 Fn Servers 前面的负载均衡器。与其他容器类似,我们只需创建并将其附加到 fn_net
。作为 FaaS 网关,我们还将其端口暴露到 8080
(从其内部端口 8081
),使 Fn CLI 可以连接到 Fn 集群而无需任何特殊设置。网络别名仅在我们需要其他服务连接到该网关时使用。
接下来,发送一个 Fn Server 节点列表作为命令行参数。
当前,节点列表配置仅允许直接传递给容器。只需以 <name>:<port>
格式输入它们,并用 逗号 分隔:
$ docker run --detach \
--network fn_net \
--network-alias fnlb \
--name fnlb \
-p 8080:8081 \
fnproject/fnlb:latest --nodes fn_0:8080,fn_1:8080
好的,现在是时候验证一切是否正常运行了。我们使用 docker ps
命令仔细检查所有容器:
$ docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Command}}\t{{.Ports}}"
CONTAINER ID NAMES COMMAND PORTS
ce4f8e9bc300 fnlb "./fnlb --nodes fn_0…" 0.0.0.0:8080->8081/tcp
dae4fb892b4d fn_1 "preentry.sh ./fnser…" 2375/tcp
8aefeb9e19ef fn_0 "preentry.sh ./fnser…" 2375/tcp
67bd136c331a mysql "docker-entrypoint.s…" 3306/tcp
在接下来的两部分中,我们将介绍如何通过 Fn UI 监控 Fn 的运行情况,以及如何查看并可能进一步分析存储在数据库中的日志。
使用 Fn UI 进行监控
Fn UI 是为 Fn 创建的用户界面项目。它提供了一个简单的仪表盘,并配备易于使用的时间序列图表,以便实时监控函数的运行情况。要启动 Fn UI,我们创建并将容器附加到 fn_net
,同时将端口发布到 4000
。Fn UI 需要一个 Fn Server 的 URL。但是它们都位于 Fn LB 后面,因此我们只需将 FN_API_URL
设置为 Fn LB 的位置。
请注意,它们都在 fn_net
网络内部相互连接,因此 URL 显示为 http://fnlb:8081
,使用的是 fnlb
在网络中的实际名称和端口:
$ docker run --detach \
--network fn_net \
--network-alias fnui \
-p 4000:4000 \
-e "FN_API_URL=http://fnlb:8081" fnproject/ui
设置好 Fn UI 实例后,浏览到 localhost:8080
打开仪表盘。我们将看到列出的所有应用程序,如以下截图所示。也可以在此管理应用程序,例如创建或删除。如果不希望屏幕一直自动刷新,可以取消选中“自动刷新”:
图 5.5:显示 Fn 应用程序列表的 Fn 仪表盘
选择一个应用程序后,您可以通过单击仪表盘中的“运行函数”按钮来执行函数,如以下截图所示。如果在执行函数时发生错误并且执行失败,例如,将弹出通知,如以下示例所示。
要执行该函数,请将有效载荷以 JSON 形式放入并按下运行按钮:
图 5.6:用于调用函数的对话框
当函数调用完成时,它的名称和计数将出现在已完成的图表中。以下是调用函数的 curl
命令。多次运行它以查看图表变化:
$ curl -X POST -d '{"Name":"chanwit"}' http://localhost:8080/r/demo/hello_go
还有一个运行图表,显示了仍在并行运行的函数数量。以下截图展示了这些图表的运行情况:
图 5.7:显示 Fn 函数不同状态的图表
让我们看看当我们运行一些无效输入的请求时会发生什么。以下是命令:
$ curl -X POST -d '' http://localhost:8080/r/demo/hello_go
这样,hello_go
函数将以代码 250
退出,并出现在失败图表中。我们反复运行它,以使失败次数增加,如下图所示:
图 5.8:右下方显示失败函数增量的图表
我们现在已经知道如何使用 Fn UI 来监控函数调用。接下来,我们将使用一个简单的 DBMS 界面来帮助浏览 Fn 服务器收集的日志。
使用 MyAdmin 查看呼叫日志
以 MySQL 作为中央日志存储,我们可以通过任何工具轻松访问 MySQL 以查询或分析日志。在这个例子中,我们使用一个简单的 MyAdmin 界面连接到 MySQL 后端。以下是启动 MyAdmin 的 docker run
命令。
我们只需将 MyAdmin 实例附加到相同的网络,并告诉 MyAdmin 连接到 mysql
,即后端数据库的服务名称:
$ docker run --detach \
--name myadmin \
--network fn_net \
--network-alias myadmin \
-p 9000:80 \
-e PMA_HOST=mysql \
phpmyadmin/phpmyadmin
浏览到暴露的端口,在这个例子中是端口号 9000
,并使用在 MySQL 设置期间设置的用户名和密码(func
/funcpass
)登录。以下截图显示了 phpMyAdmin 的登录页面:
图 5.9:将连接到 Fn 日志数据库的 phpMyAdmin 登录页面
在 phpMyAdmin 面板内,查看 fn_db
参数,我们将看到用于存储 Fn 信息的所有表,如下图所示。表 apps
的数据是通过命令 fn apps create
创建的。例如,我们想要查看的是 calls
表和 logs
表。calls
表的内容可以通过 fn calls list
检索,logs
表的内容也可以通过类似的方式使用 fn logs get
检索。但是,当我们能够直接访问 logs
时,我们甚至可以直接使用可用数据进行一些分析:
图 5.10:phpMyAdmin 中所有 Fn 表的列表
以下截图显示了 calls
表的内容。表中有一个状态列,允许我们有效地筛选出呼叫的状态:成功或错误。还有一个 stats
列,包含一些时间信息,将由 Fn UI 检索并显示:
图 5.11:calls
表中的 Fn 呼叫日志数据
以下截图显示了 logs
表。在 logs
表中,它只是为每个条目打上呼叫 ID 的标记。log
列显示了我们打印到 STDERR 的日志消息。我们可以通过尝试向 hello_go
函数发送一些无效输入来查看不同的错误行为。由于这个表如此易于访问,我们可以有效地排除 Fn 函数的问题,而不需要安装其他额外的工具:
图 5.12:从函数的 STDERR 捕获的 Fn 日志数据
好的,如果我们能够让 MyAdmin 显示日志数据,看来一切正常。最后,为了确认所有容器是否都在运行以及它们的状态,只需再次使用docker ps
命令检查所有正在运行的容器:
$ docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Command}}\t{{.Ports}}"
CONTAINER ID NAMES COMMAND PORTS
70810f341284 fnui "npm start" 0.0.0.0:4000->4000/tcp
ce4f8e9bc300 fnlb "./fnlb --nodes fn_0…" 0.0.0.0:8080->8081/tcp
dae4fb892b4d fn_1 "preentry.sh ./fnser…" 2375/tcp
8aefeb9e19ef fn_0 "preentry.sh ./fnser…" 2375/tcp
8645116af77d myadmin "/run.sh phpmyadmin" 9000/tcp, 0.0.0.0:9000->80/tcp
67bd136c331a mysql "docker-entrypoint.s…" 3306/tcp
练习
现在是时候回顾本章的所有内容了:
-
Fn 架构是什么样的?
-
该架构与其他 FaaS 平台有何不同?
-
Fn 服务器的角色是什么?
-
我们如何配置 Fn 服务器以使用外部数据存储?
-
Fn 的 Java 运行时和 Go 运行时所使用的技术有何不同?
-
应用程序和路由是如何组织的?
-
Fn LB 的角色是什么?
-
Fn UI 的角色是什么?
-
我们如何查看之前调用的结果?
-
我们如何检查失败调用的日志信息?
-
描述一个 Fn 函数如何与 STDIN、STDOUT 和 STDERR 交互?
总结
本章讨论了 Fn 项目、其组件和架构。我们开始使用 Fn 及其命令行工具 Fn CLI。
接着我们讨论了 Fn 函数的结构,例如它如何与 STDIN、STDOUT 和 STDERR 交互。我们学习了如何构建和部署 Fn 函数,包括使用 Java 和 Go 运行时。
然后我们在 Docker Swarm 上形成了一个 Fn 集群,并将 Fn 服务器实例与外部数据库存储(MySQL)连接。我们使用 Fn LB(由同一团队专门实现的负载均衡器)对 Fn 实例进行了负载均衡。
使用 Fn UI,我们学会了如何利用它监控 Fn 的调用。通过 MyAdmin,我们能够直接在 MySQL 中浏览调用和错误日志。像 MyAdmin 这样的简单工具可以在不准备复杂工具链的情况下实现相同的分析结果。
下一章将介绍 OpenWhisk,这是 Apache 项目中的另一个无服务器技术栈,以及 IBM 在其云中提供无服务器服务所使用的技术栈。