Kubernetes-开发指南-全-

Kubernetes 开发指南(全)

原文:zh.annas-archive.org/md5/DCD16B633B67524B76A687C2FBCAAD70

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

发现自己不仅负责编写代码,还负责运行代码的责任越来越普遍。虽然许多公司仍然有一个运维组(通常更名为 SRE 或 DevOps),他们可以提供专业知识,但开发人员(就像你)经常被要求扩展自己的知识和责任范围。

将基础设施视为代码已经有一段时间了。几年前,我可能会描述边界为 Puppet 由运维人员使用,而 Chef 由开发人员使用。随着云的出现和发展,以及最近 Docker 的发展,所有这些都发生了变化。容器提供了一定程度的控制和隔离,以及非常吸引人的开发灵活性。当使用容器时,您很快就会发现自己想要一次使用多个容器,以实现责任的隔离和水平扩展。

Kubernetes 是一个由 Google 开源的项目,现在由云原生计算基金会托管。它展示了 Google 在容器中运行软件的经验,并使其对您可用。它不仅包括运行容器,还将它们组合成服务,水平扩展它们,以及提供控制这些容器如何相互交互以及如何向外界暴露的手段。

Kubernetes 提供了一个由 API 和命令行工具支持的声明性结构。Kubernetes 可以在您的笔记本电脑上使用,也可以从众多云提供商中利用。使用 Kubernetes 的好处是能够使用相同的一套工具和相同的期望,无论是在本地运行,还是在您公司的小型实验室,或者在任何数量的较大云提供商中运行。这并不完全是 Java 从过去的日子里承诺的一次编写,随处运行;更多的是,我们将为您提供一致的一套工具,无论是在您的笔记本电脑上运行,还是在您公司的数据中心,或者在 AWS、Azure 或 Google 等云提供商上运行。

这本书是您利用 Kubernetes 及其功能开发、验证和运行代码的指南。

这本书侧重于示例和样本,带您了解如何使用 Kubernetes 并将其整合到您的开发工作流程中。通过这些示例,我们关注您可能想要利用 Kubernetes 运行代码的常见任务。

这本书是为谁准备的

如果您是一名全栈或后端软件开发人员,对测试和运行您正在开发的代码感兴趣、好奇或被要求负责,您可以利用 Kubernetes 使该过程更简单和一致。如果您正在寻找 Node.js 和 Python 中面向开发人员的示例,以了解如何使用 Kubernetes 构建、测试、部署和运行代码,那么本书非常适合您。

本书涵盖内容

第一章《为开发设置 Kubernetes》涵盖了 kubectl、minikube 和 Docker 的安装,以及使用 minikube 运行 kubectl 来验证您的安装。本章还介绍了 Kubernetes 中节点、Pod、容器、ReplicaSets 和部署的概念。

第二章《在 Kubernetes 中打包您的代码》解释了如何将您的代码打包到容器中,以便在 Python 和 Node.js 中使用 Kubernetes 进行示例。

第三章《与 Kubernetes 中的代码交互》涵盖了如何在 Kubernetes 中运行容器,如何访问这些容器,并介绍了 Kubernetes 的 Services、Labels 和 Selectors 概念。

第四章《声明性基础设施》介绍了如何以声明性结构表达您的应用程序,以及如何扩展以利用 Kubernetes 的 ConfigMaps、Annotations 和 Secrets 概念。

第五章《Pod 和容器生命周期》探讨了 Kubernetes 中容器和 Pod 的生命周期,以及如何公开来自您的应用程序的钩子以影响 Kubernetes 运行您的代码,以及如何优雅地终止您的代码。

第六章《Kubernetes 中的后台处理》解释了 Kubernetes 中作业和 CronJob 的批处理概念,并介绍了 Kubernetes 如何处理持久性,包括持久卷、持久卷索赔和有状态集。

第七章《监控和指标》涵盖了 Kubernetes 中的监控,以及如何利用 Prometheus 和 Grafana 捕获和显示有关 Kubernetes 和您的应用程序的指标和简单仪表板。

第八章,日志和跟踪,解释了如何使用 ElasticSearch、FluentD 和 Kibana 在 Kubernetes 中收集日志,以及如何设置和使用 Jaeger 进行分布式跟踪。

第九章,集成测试,介绍了利用 Kubernetes 的测试策略,以及如何在集成和端到端测试中利用 Kubernetes。

第十章,故障排除常见问题和下一步,回顾了您在开始使用 Kubernetes 时可能遇到的一些常见痛点,并解释了如何解决这些问题,并概述了 Kubernetes 生态系统中一些可能对开发人员和开发流程感兴趣的项目。

为了充分利用本书

您需要满足以下软件和硬件要求:

  • Kubernetes 1.8

  • Docker 社区版

  • kubectl 1.8(Kubernetes 的一部分)

  • VirtualBox v5.2.6 或更高版本

  • minikube v0.24.1

  • MacBook 或 Linux 机器,内存为 4GB 或更多

下载示例代码文件

您可以从您在www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packtpub.com

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用以下最新版本解压或提取文件夹:

  • Windows 上的 WinRAR/7-Zip

  • Mac 上的 Zipeg/iZip/UnRarX

  • Linux 上的 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上,网址如下:

如果代码有更新,将在现有的 GitHub 存储库中进行更新。

我们还有其他代码包来自我们丰富的书籍和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

代码块设置如下:

import signal
import sys
def sigterm_handler(_signo, _stack_frame):
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler) 

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:

import signal
**import sys**
def sigterm_handler(_signo, _stack_frame):
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler) 

任何命令行输入或输出都是这样写的:

 kubectl apply -f simplejob.yaml 

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。例如:“从管理面板中选择系统信息。”

警告或重要说明会显示为这样。提示和技巧显示为这样。

第一章:为开发设置 Kubernetes

欢迎来到面向开发人员的 Kubernetes!本章首先将帮助您安装工具,以便您可以在开发中充分利用 Kubernetes。安装完成后,我们将与这些工具进行一些交互,以验证它们是否正常工作。然后,我们将回顾一些您作为开发人员想要了解的基本概念,以有效地使用 Kubernetes。我们将介绍 Kubernetes 中的以下关键资源:

  • 容器

  • Pod

  • 节点

  • 部署

  • 副本集

开发所需的工具

除了您通常使用的编辑和编程工具之外,您还需要安装软件来利用 Kubernetes。本书的重点是让您可以在本地开发机器上完成所有操作,同时也可以在将来如果需要更多资源,扩展和利用远程 Kubernetes 集群。Kubernetes 的一个好处是它以相同的方式处理一个或一百台计算机,让您可以利用您软件所需的资源,并且无论它们位于何处,都可以一致地进行操作。

本书中的示例将使用本地机器上终端中的命令行工具。主要的工具将是kubectl,它与 Kubernetes 集群通信。我们将使用 Minikube 在您自己的开发系统上运行的单台机器的微型 Kubernetes 集群。我建议安装 Docker 的社区版,这样可以轻松地构建用于 Kubernetes 内部的容器:

可选工具

除了kubectlminikubedocker之外,您可能还想利用其他有用的库和命令行工具。

jq是一个命令行 JSON 处理器,它可以轻松解析更复杂的数据结构中的结果。我会将其描述为更擅长处理 JSON 结果的 grep 表亲。您可以按照stedolan.github.io/jq/download/上的说明安装jq。关于jq的详细信息以及如何使用它也可以在stedolan.github.io/jq/manual/上找到。

启动本地集群

一旦 Minikube 和 Kubectl 安装完成,就可以启动一个集群。值得知道你正在使用的工具的版本,因为 Kubernetes 是一个发展迅速的项目,如果需要从社区获得帮助,知道这些常用工具的版本将很重要。

我在撰写本文时使用的 Minikube 和kubectl的版本是:

  • Minikube:版本 0.22.3

  • kubectl:版本 1.8.0

您可以使用以下命令检查您的副本的版本:

minikube version

这将输出一个版本:

minikube version: v0.22.3

如果在按照安装说明操作时还没有这样做,请使用 Minikube 启动 Kubernetes。最简单的方法是使用以下命令:

minikube start

这将下载一个虚拟机映像并启动它,以及在其上的 Kubernetes,作为单机集群。输出将类似于以下内容:

Downloading Minikube ISO
 106.36 MB / 106.36 MB [============================================] 100.00% 0s
Getting VM IP address...
Moving files into cluster...
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.

Minikube 将自动创建kubectl访问集群和控制集群所需的文件。完成后,可以获取有关集群的信息以验证其是否正在运行。

首先,您可以直接询问minikube关于其状态:

minikube status
minikube: Running
cluster: Running
kubectl: Correctly Configured: pointing to minikube-vm at 192.168.64.2

如果我们询问kubectl关于其版本,它将报告客户端的版本和正在通信的集群的版本:

kubectl version

第一个输出是kubectl客户端的版本:

Client Version: version.Info{Major:"1", Minor:"7", GitVersion:"v1.7.5", GitCommit:"17d7182a7ccbb167074be7a87f0a68bd00d58d97", GitTreeState:"clean", BuildDate:"2017-08-31T19:32:26Z", GoVersion:"go1.9", Compiler:"gc", Platform:"darwin/amd64"}

立即之后,它将通信并报告集群上 Kubernetes 的版本:

Server Version: version.Info{Major:"1", Minor:"7", GitVersion:"v1.7.5", GitCommit:"17d7182a7ccbb167074be7a87f0a68bd00d58d97", GitTreeState:"clean", BuildDate:"2017-09-11T21:52:19Z", GoVersion:"go1.8.3", Compiler:"gc", Platform:"linux/amd64"}

我们也可以使用kubectl来请求有关集群的信息:

kubectl cluster-info

然后会看到类似以下的内容:

Kubernetes master is running at https://192.168.64.2:8443

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

此命令主要让您知道您正在通信的 API 服务器是否正在运行。我们可以使用附加命令来请求关键内部组件的特定状态:

kubectl get componentstatuses
NAME                 STATUS    MESSAGE              ERROR
scheduler            Healthy   ok
etcd-0               Healthy   {"health": "true"}
controller-manager   Healthy   ok

Kubernetes 还报告并存储了许多事件,您可以请求查看这些事件。这些显示了集群内部发生的情况:

kubectl get events
LASTSEEN   FIRSTSEEN   COUNT     NAME       KIND      SUBOBJECT   TYPE      REASON                    SOURCE                 MESSAGE
2m         2m          1         minikube   Node                  Normal    Starting                  kubelet, minikube      Starting kubelet.
2m         2m          2         minikube   Node                  Normal    NodeHasSufficientDisk     kubelet, minikube      Node minikube status is now: NodeHasSufficientDisk
2m         2m          2         minikube   Node                  Normal    NodeHasSufficientMemory   kubelet, minikube      Node minikube status is now: NodeHasSufficientMemory
2m         2m          2         minikube   Node                  Normal    NodeHasNoDiskPressure     kubelet, minikube      Node minikube status is now: NodeHasNoDiskPressure
2m         2m          1         minikube   Node                  Normal    NodeAllocatableEnforced   kubelet, minikube      Updated Node Allocatable limit across pods
2m         2m          1         minikube   Node                  Normal    Starting                  kube-proxy, minikube   Starting kube-proxy.
2m         2m          1         minikube   Node                  Normal    RegisteredNode            controllermanager      Node minikube event: Registered Node minikube in NodeController

重置和重新启动您的集群

如果您想清除本地 Minikube 集群并重新启动,这是非常容易做到的。发出一个delete命令,然后start Minikube 将清除环境并将其重置为一个空白状态:

minikube delete Deleting local Kubernetes cluster...
Machine deleted.

minikube start
Starting local Kubernetes v1.7.5 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.

查看 Minikube 内置和包含的内容

使用 Minikube,您可以通过一个命令为 Kubernetes 集群启动基于 Web 的仪表板:

minikube dashboard

这将打开一个浏览器,并显示一个 Kubernetes 集群的 Web 界面。如果您查看浏览器窗口中的 URL 地址,您会发现它指向之前从kubectl cluster-info命令返回的相同 IP 地址,运行在端口30000上。仪表板在 Kubernetes 内部运行,并且它不是唯一的运行在其中的东西。

Kubernetes 是自托管的,支持 Kubernetes 运行的支持组件,如仪表板、DNS 等,都在 Kubernetes 内部运行。您可以通过查询集群中所有 Pod 的状态来查看所有这些组件的状态:

kubectl get pods --all-namespaces
NAMESPACE     NAME                          READY     STATUS    RESTARTS   AGE
kube-system   kube-addon-manager-minikube   1/1       Running   0          6m
kube-system   kube-dns-910330662-6pctd      3/3       Running   0          6m
kube-system   kubernetes-dashboard-91nmv    1/1       Running   0          6m

请注意,在此命令中我们使用了--all-namespaces选项。默认情况下,kubectl只会显示位于默认命名空间中的 Kubernetes 资源。由于我们还没有运行任何东西,如果我们调用kubectl get pods,我们将只会得到一个空列表。Pod 并不是唯一的 Kubernetes 资源;您可以查询许多不同的资源,其中一些我将在本章后面描述,更多的将在后续章节中描述。

暂时,再调用一个命令来获取服务列表:

kubectl get services --all-namespaces

这将输出所有服务:

NAMESPACE     NAME                   CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
default       kubernetes             10.0.0.1     <none>        443/TCP         3m
kube-system   kube-dns               10.0.0.10    <none>        53/UDP,53/TCP   2m
kube-system   kubernetes-dashboard   10.0.0.147   <nodes>       80:30000/TCP    2m

请注意名为kubernetes-dashboard的服务具有Cluster-IP值和端口80:30000。该端口配置表明,在支持kubernetes-dashboard服务的 Pod 中,它将把来自端口30000的任何请求转发到容器内的端口80。您可能已经注意到,Cluster IP 的 IP 地址与我们之前在kubectl cluster-info命令中看到的 Kubernetes 主节点报告的 IP 地址非常不同。

重要的是要知道,Kubernetes 中的所有内容都在一个私有的、隔离的网络上运行,通常无法从集群外部访问。我们将在未来的章节中更详细地介绍这一点。现在,只需知道minikube在其中有一些额外的特殊配置来暴露仪表板。

验证 Docker

Kubernetes 支持多种运行容器的方式,Docker 是最常见、最方便的方式。在本书中,我们将使用 Docker 来帮助我们创建在 Kubernetes 中运行的镜像。

您可以通过运行以下命令来查看您安装的 Docker 版本并验证其是否可操作:

docker  version

kubectl一样,它将报告docker客户端版本以及服务器版本,您的输出可能看起来像以下内容:

Client:
 Version: 17.09.0-ce
 API version: 1.32
 Go version: go1.8.3
 Git commit: afdb6d4
 Built: Tue Sep 26 22:40:09 2017
 OS/Arch: darwin/amd64
Server:
 Version: 17.09.0-ce
 API version: 1.32 (minimum version 1.12)
 Go version: go1.8.3
 Git commit: afdb6d4
 Built: Tue Sep 26 22:45:38 2017
 OS/Arch: linux/amd64
 Experimental: false

通过使用docker images命令,您可以查看本地可用的容器镜像,并使用docker pull命令,您可以请求特定的镜像。在下一章的示例中,我们将基于 alpine 容器镜像构建我们的软件,因此让我们继续拉取该镜像以验证您的环境是否正常工作:

docker pull alpine 
Using default tag: latest
latest: Pulling from library/alpine
Digest: sha256:f006ecbb824d87947d0b51ab8488634bf69fe4094959d935c0c103f4820a417d
Status: Image is up to date for alpine:latest

然后,您可以使用以下命令查看镜像:

docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest 76da55c8019d 3 weeks ago 3.97MB</strong>

如果在尝试拉取 alpine 镜像时出现错误,这可能意味着您需要通过代理工作,或者以其他方式受限制地访问互联网以满足您的镜像需求。如果您处于这种情况,您可能需要查看 Docker 关于如何设置和使用代理的信息。

清除和清理 Docker 镜像

由于我们将使用 Docker 来构建容器镜像,了解如何摆脱镜像将是有用的。您已经使用docker image命令看到了镜像列表。还有一些由 Docker 维护的中间镜像在输出中是隐藏的。要查看 Docker 存储的所有镜像,请使用以下命令:

docker images -a

如果您只按照前文拉取了 alpine 镜像,可能不会看到任何其他镜像,但在下一章中构建镜像时,这个列表将会增长。

您可以使用docker rmi命令后跟镜像的名称来删除镜像。默认情况下,Docker 将尝试维护容器最近使用或引用的镜像。因此,您可能需要强制删除以清理镜像。

如果您想重置并删除所有镜像并重新开始,有一个方便的命令可以做到。通过结合 Docker 镜像和docker rmi,我们可以要求它强制删除所有已知的镜像:

docker rmi -f $(docker images -a -q)

Kubernetes 概念 - 容器

Kubernetes(以及这个领域中的其他技术)都是关于管理和编排容器的。容器实际上是一个包裹了一系列 Linux 技术的名称,其中最突出的是容器镜像格式和 Linux 如何隔离进程,利用 cgroups。

在实际情况下,当有人谈论容器时,他们通常意味着有一个包含运行单个进程所需的一切的镜像。在这种情况下,容器不仅是镜像,还包括关于如何调用和运行它的信息。容器还表现得好像它们有自己的网络访问权限。实际上,这是由运行容器的 Linux 操作系统共享的。

当我们想要编写在 Kubernetes 下运行的代码时,我们总是在谈论将其打包并准备在容器内运行。本书后面更复杂的示例将利用多个容器一起工作。

在容器内运行多个进程是完全可能的,但这通常被视为不好的做法,因为容器理想上适合表示单个进程以及如何调用它,并且不应被视为完整虚拟机的同一物体。

如果你通常使用 Python 进行开发,那么你可能熟悉使用类似pip的工具来下载你需要的库和模块,并使用类似python your_file的命令来调用你的程序。如果你是 Node 开发人员,那么你更可能熟悉使用npmyarn来安装你需要的依赖,并使用node your_file来运行你的代码。

如果你想把所有这些打包起来,在另一台机器上运行,你可能要重新执行所有下载库和运行代码的指令,或者可能将整个目录压缩成 ZIP 文件,然后将其移动到想要运行的地方。容器是一种将所有信息收集到单个镜像中的方式,以便可以轻松地移动、安装和在 Linux 操作系统上运行。最初由 Docker 创建,现在由Open Container InitiativeOCI)(www.opencontainers.org)维护规范。

虽然容器是 Kubernetes 中的最小构建块,但 Kubernetes 处理的最小单位是 Pod。

Kubernetes 资源 - Pod

Pod 是 Kubernetes 管理的最小单位,也是系统其余部分构建在其上的基本单位。创建 Kubernetes 的团队发现让开发人员指定应始终在同一操作系统上运行的进程,并且应该一起运行的进程组合应该是被调度、运行和管理的单位是值得的。

在本章的前面,您看到 Kubernetes 的一个基本实例有一些软件在 Pod 中运行。Kubernetes 的许多部分都是使用这些相同的概念和抽象来运行的,这使得 Kubernetes 能够自托管其自己的软件。一些用于运行 Kubernetes 集群的软件是在集群之外管理的,但越来越多地利用了 Pod 的概念,包括 DNS 服务、仪表板和控制器管理器,它们通过 Kubernetes 协调所有的控制操作。

一个 Pod 由一个或多个容器以及与这些容器相关的信息组成。当您询问 Kubernetes 有关一个 Pod 时,它将返回一个数据结构,其中包括一个或多个容器的列表,以及 Kubernetes 用来协调 Pod 与其他 Pod 以及 Kubernetes 应该如何在程序失败时采取行动的各种元数据。元数据还可以定义诸如亲和性之类的东西,影响 Pod 在集群中可以被调度的位置,以及如何获取容器镜像等期望。重要的是要知道,Pod 不打算被视为持久的、长期存在的实体。

它们被创建和销毁,本质上是短暂的。这允许单独的逻辑 - 包含在控制器中 - 来管理规模和可用性等责任。正是这种分工的分离使得 Kubernetes 能够在发生故障时提供自我修复的手段,并提供一些自动扩展的能力。

由 Kubernetes 运行的 Pod 具有一些特定的保证:

  • 一个 Pod 中的所有容器将在同一节点上运行。

  • 在 Pod 中运行的任何容器都将与同一 Pod 中的其他容器共享节点的网络

  • Pod 中的容器可以通过挂载到容器的卷共享文件。

  • 一个 Pod 有一个明确的生命周期,并且始终保留在它启动的节点上。

在实际操作中,当您想要了解 Kubernetes 集群上运行的内容时,通常会想要了解 Kubernetes 中运行的 Pod 及其状态。

Kubernetes 维护并报告 Pod 的状态,以及组成 Pod 的每个容器的状态。容器的状态包括RunningTerminatedWaiting。Pod 的生命周期稍微复杂一些,包括严格定义的阶段和一组 PodStatus。阶段包括PendingRunningSucceededFailedUnknown,阶段中包含的具体细节在kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase中有文档记录。

Pod 还可以包含探针,用于主动检查容器的某些状态信息。Kubernetes 控制器部署和使用的两个常见探针是livenessProbereadinessProbelivenessProbe定义容器是否正在运行。如果没有运行,Kubernetes 基础设施会终止相关容器,然后应用为 Pod 定义的重启策略。readinessProbe用于指示容器是否准备好提供服务请求。readinessProbe的结果与其他 Kubernetes 机制(稍后我们将详细介绍)一起使用,以将流量转发到相关容器。通常,探针被设置为允许容器中的软件向 Kubernetes 提供反馈循环。您可以在kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes找到有关探针的更多详细信息,如何定义它们以及它们的用途。我们将在未来的章节中详细讨论探针。

命名空间

Pod 被收集到命名空间中,用于将 Pod 组合在一起以实现各种目的。在之前使用--all-namespaces选项请求集群中所有 Pod 的状态时,您已经看到了命名空间的一个示例。

命名空间可用于提供资源使用配额和限制,对 Kubernetes 在集群内部创建的 DNS 名称产生影响,并且在将来可能会影响访问控制策略。如果在通过kubectl与 Kubernetes 交互时未指定命名空间,则命令会假定您正在使用名为default的默认命名空间。

编写您的 Pods 和容器的代码

成功使用 Kubernetes 的关键之一是考虑您希望代码如何运行,并将其结构化,使其清晰地适应 Pod 和容器的结构。通过将软件解决方案结构化为将问题分解为符合 Kubernetes 提供的约束和保证的组件,您可以轻松地利用并行性和容器编排,以像使用单台机器一样无缝地使用多台机器。

Kubernetes 提供的保证和抽象反映了 Google(和其他公司)在以大规模、可靠和冗余的方式运行其软件和服务方面多年的经验,利用水平扩展的模式来解决重大问题。

Kubernetes 资源-节点

节点是一台机器,通常运行 Linux,已添加到 Kubernetes 集群中。它可以是物理机器或虚拟机。在minikube的情况下,它是一台运行 Kubernetes 所有软件的单个虚拟机。在较大的 Kubernetes 集群中,您可能有一台或多台专门用于管理集群的机器,以及单独的机器用于运行工作负载。Kubernetes 通过跟踪节点的资源使用情况、调度、启动(如果需要,重新启动)Pod,以及协调连接 Pod 或在集群外部公开它们的其他机制来管理其资源。

节点可以(而且确实)与它们关联的元数据,以便 Kubernetes 可以意识到相关的差异,并在调度和运行 Pod 时考虑这些差异。Kubernetes 可以支持各种各样的机器共同工作,并在所有这些机器上高效运行软件,或者将 Pod 的调度限制在只有所需资源的机器上(例如,GPU)。

网络

我们之前提到,Pod 中的所有容器共享节点的网络。此外,Kubernetes 集群中的所有节点都应该相互连接并共享一个私有的集群范围网络。当 Kubernetes 在 Pod 中运行容器时,它是在这个隔离的网络中进行的。Kubernetes 负责处理 IP 地址,创建 DNS 条目,并确保一个 Pod 可以与同一 Kubernetes 集群中的另一个 Pod 进行通信。

另一个资源是服务,我们稍后会深入研究,这是 Kubernetes 用来在私有网络中向其他 Pod 公开或处理集群内外的连接的工具。默认情况下,在这个私有的隔离网络中运行的 Pod 不会暴露在 Kubernetes 集群之外。根据您的 Kubernetes 集群是如何创建的,有多种方式可以打开从集群外部访问您的软件的途径,我们将在稍后的服务中详细介绍,包括负载均衡器、节点端口和入口。

控制器

Kubernetes 的构建理念是告诉它你想要什么,它知道如何去做。当您与 Kubernetes 交互时,您断言您希望一个或多个资源处于特定状态,具有特定版本等等。控制器是跟踪这些资源并尝试按照您描述的方式运行软件的大脑所在的地方。这些描述可以包括运行容器镜像的副本数量、更新 Pod 中运行的软件版本,以及处理节点故障的情况,其中您意外地失去了集群的一部分。

Kubernetes 中使用了各种控制器,它们大多隐藏在我们将进一步深入研究的两个关键资源后面:部署和 ReplicaSets。

Kubernetes 资源 – ReplicaSet

ReplicaSet 包装了 Pod,定义了需要并行运行多少个 Pod。ReplicaSet 通常又被部署包装。ReplicaSets 不经常直接使用,但对于表示水平扩展来说至关重要——表示要运行的并行 Pod 的数量。

ReplicaSet 与 Pod 相关联,并指示集群中应该运行多少个该 Pod 的实例。ReplicaSet 还意味着 Kubernetes 有一个控制器来监视持续状态,并知道要保持运行多少个 Pod。这就是 Kubernetes 真正开始为您工作的地方,如果您在 ReplicaSet 中指定了三个 Pod,而其中一个失败了,Kubernetes 将自动为您安排并运行另一个 Pod。

Kubernetes 资源 – 部署

在 Kubernetes 上运行代码的最常见和推荐方式是使用部署,由部署控制器管理。我们将在接下来的章节中探讨部署,直接指定它们并使用诸如 kubectl run 等命令隐式创建它们。

Pod 本身很有趣,但受限,特别是因为它旨在是短暂的。如果一个节点死掉(或者被关机),那个节点上的所有 Pod 都将停止运行。ReplicaSets 提供了自我修复的能力。它们在集群内工作,以识别 Pod 何时不再可用,并尝试调度另一个 Pod,通常是为了使服务恢复在线,或者继续工作。

部署控制器包装并扩展了 ReplicaSet 控制器,主要负责推出软件更新并管理更新部署资源的过程。部署控制器包括元数据设置,以了解要保持运行多少个 Pod,以便通过添加容器的新版本来启用无缝滚动更新软件,并在您请求时停止旧版本。

代表 Kubernetes 资源

Kubernetes 资源通常可以表示为 JSON 或 YAML 数据结构。Kubernetes 专门设计成可以保存这些文件,当您想要运行软件时,可以使用诸如kubectl deploy之类的命令,并提供先前创建的定义,然后使用它来运行您的软件。在我们的下一章中,我们将开始展示这些资源的具体示例,并为我们的使用构建它们。

当我们进入下一个和未来的章节中的示例时,我们将使用 YAML 来描述我们的资源,并通过kubectl以 JSON 格式请求数据。所有这些数据结构都是针对 Kubernetes 的每个版本进行正式定义的,以及 Kubernetes 提供的用于操作它们的 REST API。所有 Kubernetes 资源的正式定义都在源代码控制中使用 OpenAPI(也称为Swagger)进行维护,并可以在github.com/kubernetes/kubernetes/tree/master/api/swagger-spec上查看。

总结

在本章中,我们安装了minikubekubectl,并使用它们启动了一个本地 Kubernetes 集群,并简要地与之交互。然后,我们简要介绍了一些关键概念,我们将在未来的章节中更深入地使用和探索,包括容器、Pod、节点、部署和 ReplicaSet。

在下一章中,我们将深入探讨将软件放入容器所需的步骤,以及如何在自己的项目中设置容器的技巧。我们将以 Python 和 Node.js 为例,演示如何将软件放入容器,你可以将其作为自己代码的起点。

第二章:将您的代码打包以在 Kubernetes 中运行

在本章中,我们将深入探讨使用 Kubernetes 所需的第一件事:将软件放入容器中。我们将回顾容器是什么,如何存储和共享镜像,以及如何构建容器。然后,本章继续进行两个示例,一个是 Python,另一个是 Node.js,它们将引导您如何将这些语言的简单示例代码构建成容器,并在 Kubernetes 中运行它们。本章的部分内容包括:

  • 容器镜像

  • 制作自己的容器

  • Python 示例-制作容器镜像

  • Node.js 示例-制作容器镜像

  • 给您的容器镜像打标签

容器镜像

使用 Kubernetes 的第一步是将您的软件放入容器中。Docker 是创建这些容器的最简单方法,而且这是一个相当简单的过程。让我们花点时间来查看现有的容器镜像,以了解在创建自己的容器时需要做出什么选择:

docker pull docker.io/jocatalin/kubernetes-bootcamp:v1

首先,您将看到它下载具有奥秘 ID 的文件列表。您会看到它们并行更新,因为它尝试在可用时抓取它们:

v1: Pulling from jocatalin/kubernetes-bootcamp
5c90d4a2d1a8: Downloading  3.145MB/51.35MB
ab30c63719b1: Downloading  3.931MB/18.55MB
29d0bc1e8c52: Download complete
d4fe0dc68927: Downloading  2.896MB/13.67MB
dfa9e924f957: Waiting

当下载完成时,输出将更新为“提取”,最后为“拉取完成”:

v1: Pulling from jocatalin/kubernetes-bootcamp
5c90d4a2d1a8: Pull complete
ab30c63719b1: Pull complete
29d0bc1e8c52: Pull complete
d4fe0dc68927: Pull complete
dfa9e924f957: Pull complete
Digest: sha256:0d6b8ee63bb57c5f5b6156f446b3bc3b3c143d233037f3a2f00e279c8fcc64af
Status: Downloaded newer image for jocatalin/kubernetes-bootcamp:v1

您在终端中看到的是 Docker 正在下载构成容器镜像的层,将它们全部汇集在一起,然后验证输出。当您要求 Kubernetes 运行软件时,Kubernetes 正是执行这个相同的过程,下载镜像然后运行它们。

如果您现在运行以下命令:

docker images

您将看到(也许还有其他)列出的镜像类似于这样:

REPOSITORY                                         TAG                 IMAGE ID            CREATED             SIZE
jocatalin/kubernetes-bootcamp                      v1                  8fafd8af70e9        13 months ago       211MB

该镜像的大小为211MB,当我们指定jocatalin/kubernetes-bootcamp:v1时,您会注意到我们同时指定了一个名称jocatalin/kubernetes-bootcamp和一个标签v1。此外,该镜像具有一个IMAGE ID8fafd8af70e9),这是整个镜像的唯一 ID。如果您要为镜像指定一个名称而没有标签,那么默认情况下会假定您想要一个默认标签latest

让我们深入了解刚刚下载的镜像,使用docker history命令:

docker history jocatalin/kubernetes-bootcamp:v1
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
8fafd8af70e9        13 months ago       /bin/sh -c #(nop)  CMD ["/bin/sh" "-c" "no...   0B
<missing>           13 months ago       /bin/sh -c #(nop) COPY file:de8ef36ebbfd53...   742B
<missing>           13 months ago       /bin/sh -c #(nop)  EXPOSE 8080/tcp              0B
<missing>           13 months ago       /bin/sh -c #(nop) CMD ["node"]                  0B
<missing>           13 months ago       /bin/sh -c buildDeps='xz-utils'     && set...   41.5MB
<missing>           13 months ago       /bin/sh -c #(nop) ENV NODE_VERSION=6.3.1        0B
<missing>           15 months ago       /bin/sh -c #(nop) ENV NPM_CONFIG_LOGLEVEL=...   0B
<missing>           15 months ago       /bin/sh -c set -ex   && for key in     955...   80.8kB
<missing>           15 months ago       /bin/sh -c apt-get update && apt-get insta...   44.7MB
<missing>           15 months ago       /bin/sh -c #(nop) CMD ["/bin/bash"]             0B
<missing>           15 months ago       /bin/sh -c #(nop) ADD file:76679eeb94129df...   125MB

这明确了我们之前在下载容器时看到的情况:容器镜像由层组成,每一层都建立在它下面的层之上。Docker 镜像的层非常简单——每一层都是执行命令和命令在本地文件系统上所做的任何更改的结果。在之前的docker history命令中,您将看到任何更改底层文件系统大小的命令报告的大小。

镜像格式是由 Docker 创建的,现在由OCIOpen Container Initiative)Image Format 项目正式指定。如果您想进一步了解,可以在github.com/opencontainers/image-spec找到格式和所有相关细节。

容器镜像以及镜像中的每个层通常都可以在互联网上找到。我在本书中使用的所有示例都是公开可用的。可以配置 Kubernetes 集群使用私有镜像仓库,Kubernetes 项目的文档中有关于如何执行该任务的详细说明,网址为kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/。这种设置更加私密,但设置起来更加复杂,因此在本书中,我们将继续使用公开可用的镜像。

容器镜像还包括如何运行镜像、要运行什么、要设置哪些环境变量等信息。我们可以使用docker inspect命令查看所有这些细节:

docker inspect jocatalin/kubernetes-bootcamp:v1

上述命令生成了相当多的内容,详细描述了容器镜像以及其中运行代码所需的元数据:

[
    {
        "Id": "sha256:8fafd8af70e9aa7c3ab40222ca4fd58050cf3e49cb14a4e7c0f460cd4f78e9fe",
        "RepoTags": [
            "jocatalin/kubernetes-bootcamp:v1"
        ],
        "RepoDigests": [
            "jocatalin/kubernetes-bootcamp@sha256:0d6b8ee63bb57c5f5b6156f446b3bc3b3c143d233037f3a2f00e279c8fcc64af"
        ],
        "Parent": "",
        "Comment": "",
        "Created": "2016-08-04T16:46:35.471076443Z",
        "Container": "976a20903b4e8b3d1546e610b3cba8751a5123d76b8f0646f255fe2baf345a41",
        "ContainerConfig": {
            "Hostname": "6250540837a8",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "8080/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "NPM_CONFIG_LOGLEVEL=info",
                "NODE_VERSION=6.3.1"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"/bin/sh\" \"-c\" \"node server.js\"]"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:87ef05c0e8dc9f729b9ff7d5fa6ad43450bdbb72d95c257a6746a1f6ad7922aa",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": [],
            "Labels": {}
        },
        "DockerVersion": "1.12.0",
        "Author": "",
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 211336459,
        "VirtualSize": 211336459,

除了基本配置之外,Docker 容器镜像还可以包含运行时配置,因此通常会有一个重复的部分,在ContainerConfig键下定义了大部分你所说的内容:

        "Config": {
            "Hostname": "6250540837a8",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "8080/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "NPM_CONFIG_LOGLEVEL=info",
                "NODE_VERSION=6.3.1"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "node server.js"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:87ef05c0e8dc9f729b9ff7d5fa6ad43450bdbb72d95c257a6746a1f6ad7922aa",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": [],
            "Labels": {}
        },

最后一个部分包括了文件系统的叠加的显式列表以及它们如何组合在一起:

"GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/b38e59d31a16f7417c5ec785432ba15b3743df647daed0dc800d8e9c0a55e611/diff:/var/lib/docker/overlay2/792ce98aab6337d38a3ec7d567324f829e73b1b5573bb79349497a9c14f52ce2/diff:/var/lib/docker/overlay2/6c131c8dd754628a0ad2c2aa7de80e58fa6b3f8021f34af684b78538284cf06a/diff:/var/lib/docker/overlay2/160efe1bd137edb08fe180f020054933134395fde3518449ab405af9b1fb6cb0/diff",
                "MergedDir": "/var/lib/docker/overlay2/40746dcac4fe98d9982ce4c0a0f6f0634e43c3b67a4bed07bb97068485cd137a/merged",
                "UpperDir": "/var/lib/docker/overlay2/40746dcac4fe98d9982ce4c0a0f6f0634e43c3b67a4bed07bb97068485cd137a/diff",
                "WorkDir": "/var/lib/docker/overlay2/40746dcac4fe98d9982ce4c0a0f6f0634e43c3b67a4bed07bb97068485cd137a/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:42755cf4ee95900a105b4e33452e787026ecdefffcc1992f961aa286dc3f7f95",
                "sha256:d1c800db26c75f0aa5881a5965bd6b6abf5101dbb626a4be5cb977cc8464de3b",
                "sha256:4b0bab9ff599d9feb433b045b84aa6b72a43792588b4c23a2e8a5492d7940e9a",
                "sha256:aaed480d540dcb28252b03e40b477e27564423ba0fe66acbd04b2affd43f2889",
                "sha256:4664b95364a615be736bd110406414ec6861f801760dae2149d219ea8209a4d6"
            ]
        }
    }
]

JSON 转储中包含了很多信息,可能比你现在需要或关心的要多。最重要的是,我想让你知道它在config部分下指定了一个cmd,分为三个部分。这是如果你run容器时默认会被调用的内容,通常被称为Entrypoint。如果你把这些部分组合起来,想象自己在容器中运行它们,你将会运行以下内容:

/bin/sh -c node server.js

Entrypoint定义了将要执行的二进制文件,以及任何参数,并且是指定你想要运行什么以及如何运行的关键。Kubernetes 与这个相同的Entrypoint一起工作,并且可以用命令和参数覆盖它,以运行你的软件,或者运行你在同一个容器镜像中存储的诊断工具。

容器注册表

在前面的例子中,当我们调用命令来拉取容器时,我们引用了www.docker.com/,这是 Docker 的容器注册表。在使用 Kubernetes 或阅读有关 Kubernetes 的文档时,你经常会看到另外两个常见的注册表:gcr.io,谷歌的容器注册表,以及quay.io,CoreOS 的容器注册表。其他公司在互联网上提供托管的容器注册表,你也可以自己运行。目前,Docker 和 Quay 都为公共镜像提供免费托管,因此你会经常在文档和示例中看到它们。这三个注册表还提供私有镜像仓库的选项,通常需要相对较小的订阅费用。

公开可用的镜像(以及在这些镜像上进行层叠)的一个好处是,它使得非常容易组合你的镜像,共享底层层。这也意味着这些层可以被检查,并且可以搜索常见的层以查找安全漏洞。有几个旨在帮助提供这些信息的开源项目,还有几家公司成立了帮助协调信息和扫描的公司。如果你为你的镜像订阅了一个镜像仓库,它们通常会在其产品中包括这种漏洞扫描。

作为开发人员,当您在代码中使用库时,您对其操作负有责任。您已经负责熟悉这些库的工作方式(或者不熟悉),并在它们不按预期工作时处理任何问题。通过指定整个容器的灵活性和控制权,您同样负责以相同的方式包含在容器中的所有内容。

很容易忘记软件构建所依赖的层,并且您可能没有时间跟踪所有可能出现的安全漏洞和问题,这些问题可能已经出现在您正在构建的软件中。来自 Clair 等项目的安全扫描(github.com/coreos/clair)可以为您提供有关潜在漏洞的出色信息。我建议您考虑利用可以为您提供这些详细信息的服务。

创建您的第一个容器

使用 Docker 软件和docker build命令很容易创建一个容器。这个命令使用一个详细说明如何创建容器的清单,称为 Dockerfile。

让我们从最简单的容器开始。创建一个名为 Dockerfile 的文件,并将以下内容添加到其中:

FROM alpine
CMD ["/bin/sh", "-c", "echo 'hello world'"]

然后,调用build

docker build .

如果您看到这样的响应:

"docker build" requires exactly 1 argument.
See 'docker build --help'.
Usage: docker build [OPTIONS] PATH | URL | -
Build an image from a Dockerfile

然后,您要么在命令中缺少.,要么在与创建 Dockerfile 的目录不同的目录中运行了该命令。.告诉docker在哪里找到 Dockerfile(.表示在当前目录中)。

您应该看到类似以下的输出:

Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM alpine
latest: Pulling from library/alpine
88286f41530e: Pull complete
Digest: sha256:f006ecbb824d87947d0b51ab8488634bf69fe4094959d935c0c103f4820a417d
Status: Downloaded newer image for alpine:latest
 ---> 76da55c8019d
Step 2/2 : CMD /bin/sh -c echo 'hello world'
 ---> Running in 89c04e8c5d87
 ---> f5d273aa2dcb
Removing intermediate container 89c04e8c5d87
Successfully built f5d273aa2dcb

这个图像只有一个 ID,f5d273aa2dcb,没有名称,但这对我们来说已经足够了解它是如何工作的。如果您在本地运行此示例,您将获得一个唯一标识容器图像的不同 ID。您可以使用docker run f5d273aa2dcb命令在容器图像中运行代码。这应该会导致您看到以下输出:

hello world

花点时间在刚刚创建的图像上运行docker history f5d273aa2dcbdocker inspect f5d273aa2dcb

完成后,我们可以使用以下命令删除刚刚创建的 Docker 图像:

docker rmi **f5d273aa2dcb** 

如果您在删除图像时遇到错误,这可能是因为您有一个引用本地图像的已停止容器,您可以通过添加-f来强制删除。例如,强制删除本地图像的命令将是:

docker rmi -f f5d237aa2dcb

Dockerfile 命令

Docker 有关于如何编写 Dockerfile 的文档,网址是docs.docker.com/engine/reference/builder/,以及他们推荐的一套最佳实践,网址是docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/。我们将介绍一些常见且重要的命令,以便你能够构建自己的容器镜像。

以下是一些重要的 Dockerfile 构建命令,你应该知道:

  1. FROM (docs.docker.com/engine/reference/builder/#from): FROM描述了你用作构建容器基础的图像,并且通常是 Dockerfile 中的第一个命令。Docker 最佳实践鼓励使用 Debian 作为基础 Linux 发行版。正如你之前从我的示例中看到的,我更喜欢使用 Alpine Linux,因为它非常紧凑。你也可以使用 Ubuntu、Fedora 和 CentOS,它们都是更大的图像,并在其基本图像中包含了更多的软件。如果你已经熟悉某个 Linux 发行版及其使用的工具,那么我建议你利用这些知识来制作你的第一个容器。你还经常可以找到专门构建的容器来支持你正在使用的语言,比如 Node 或 Python。在撰写本文时(2017 年秋),我下载了各种这些图像来展示它们的相对大小:
REPOSITORY     TAG         IMAGE ID        CREATED          SIZE
alpine         latest      76da55c8019d    2 days ago       3.97MB
debian         latest      72ef1cf971d1    2 days ago       100MB
fedora         latest      ee17cf9e0828    2 days ago       231MB
centos         latest      196e0ce0c9fb    27 hours ago     197MB
ubuntu         latest      8b72bba4485f    2 days ago       120MB
ubuntu         16.10       7d3f705d307c    8 weeks ago      107MB
python         latest      26acbad26a2c    2 days ago       690MB
node           latest      de1099630c13    24 hours ago     673MB
java           latest      d23bdf5b1b1b    8 months ago     643MB

正如你所看到的,这些图像的大小差异很大。

你可以在hub.docker.com/explore/上探索这些(以及各种其他基本图像)。

  1. RUN (docs.docker.com/engine/reference/builder/#run): RUN描述了您在构建的容器映像中运行的命令,最常用于添加依赖项或其他库。如果您查看其他人创建的 Dockerfile,通常会看到RUN命令用于使用诸如apt-get install ...rpm -ivh ...的命令安装库。使用的命令取决于基本映像的选择;例如,apt-get在 Debian 和 Ubuntu 基本映像上可用,但在 Alpine 或 Fedora 上不可用。如果您输入一个不可用的RUN命令(或只是打字错误),那么在运行docker build命令时会看到错误。例如,在构建 Dockerfile 时:
FROM alpine
RUN apt-get install nodejs
Results in the following output:
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM alpine
 ---> 76da55c8019d
Step 2/2 : RUN apt-get install nodejs
 ---> Running in 58200129772d
/bin/sh: apt-get: not found

/bin/sh -c apt-get install nodejs 命令返回了非零代码:127

  1. ENV (docs.docker.com/engine/reference/builder/#env): ENV定义了将在容器映像中持久存在并在调用软件之前设置的环境变量。这些也在创建容器映像时设置,这可能会导致意想不到的影响。例如,如果您需要为特定的RUN命令设置环境变量,最好是使用单个RUN命令而不是使用ENV命令来定义它。例如,在 Debian 基础映像上使用ENV DEBIAN_FRONTEND非交互可能会混淆后续的RUN apt-get install …命令。在您想要为特定的RUN命令启用它的情况下,您可以通过在单个RUN命令前临时添加环境变量来实现。例如:
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y ...
  1. COPY (docs.docker.com/engine/reference/builder/#copy): COPY(或ADD命令)是将您自己的本地文件添加到容器中的方法。这通常是将代码复制到容器映像中运行的最有效方式。您可以复制整个目录或单个文件。除了RUN命令之外,这可能是您使用代码创建容器映像的大部分工作。

  2. WORKDIR (docs.docker.com/engine/reference/builder/#workdir):WORKDIR创建一个本地目录,然后将该目录作为以后所有命令(RUNCOPY等)的基础。对于期望从本地或相对目录运行的RUN命令,例如 Node.js npm等安装工具,这可能非常方便。

  3. LABEL (docs.docker.com/engine/reference/builder/#label):LABEL添加的值可在docker inspect中看到,并通常用作容器内部的责任人或内容的参考。MAINTAINER命令以前非常常见,但已被LABEL命令取代。标签是基于基本镜像构建的,并且是可累加的,因此您添加的任何标签都将与您使用的基本镜像的标签一起包含在内。

  4. CMD (docs.docker.com/engine/reference/builder/#cmd)和ENTRYPOINT (docs.docker.com/engine/reference/builder/#entrypoint):CMD(和ENTRYPOINT命令)是您指定当有人运行容器时要运行什么的方式。最常见的格式是 JSON 数组,其中第一个元素是要调用的命令,而第二个及以后的元素是该命令的参数。CMDENTRYPOINT旨在单独使用,这种情况下,您可以使用CMDENTRYPOINT来指定要运行的可执行文件和所有参数,或者一起使用,这种情况下,ENTRYPOINT应该只是可执行文件,而CMD应该是该可执行文件的参数。

示例 - Python/Flask 容器镜像

为了详细了解如何使用 Kubernetes,我创建了两个示例应用程序,您可以下载或复制以便跟随并尝试这些命令。其中一个是使用 Flask 库的非常简单的 Python 应用程序。示例应用程序直接来自 Flask 文档(flask.pocoo.org/docs/0.12/)。

您可以从 GitHub 上下载这些代码,网址为github.com/kubernetes-for-developers/kfd-flask/tree/first_container。由于我们将不断改进这些文件,因此此处引用的代码可在first_container标签处获得。如果您想使用 Git 获取这些文件,可以运行以下命令:

git clone https://github.com/kubernetes-for-developers/kfd-flask

然后,进入存储库并检出标签:

cd kfd-flask
git checkout tags/first_container

让我们从查看 Dockerfile 的内容开始,该文件定义了构建到容器中的内容以及构建过程。

我们创建这个 Dockerfile 的目标是:

  • 获取并安装底层操作系统的任何安全补丁

  • 安装我们需要用来运行代码的语言或运行时

  • 安装我们的代码中未直接包含的任何依赖项

  • 将我们的代码复制到容器中

  • 定义如何以及何时运行

FROM alpine
# load any public updates from Alpine packages
RUN apk update
# upgrade any existing packages that have been updated
RUN apk upgrade
# add/install python3 and related libraries
# https://pkgs.alpinelinux.org/package/edge/main/x86/python3
RUN apk add python3
# make a directory for our application
RUN mkdir -p /opt/exampleapp
# move requirements file into the container
COPY . /opt/exampleapp/
# install the library dependencies for this application
RUN pip3 install -r /opt/exampleapp/requirements.txt
ENTRYPOINT ["python3"]
CMD ["/opt/exampleapp/exampleapp.py"]

此容器基于 Alpine Linux。我欣赏容器的小巧尺寸,并且容器中没有多余的软件。您将看到一些可能不熟悉的命令,特别是apk命令。这是一个命令行工具,用于帮助安装、更新和删除 Alpine Linux 软件包。这些命令更新软件包存储库,升级镜像中安装的和预先存在的所有软件包,然后从软件包中安装 Python 3。

如果您已经熟悉 Debian 命令(如apt-get)或 Fedora/CentOS 命令(如rpm),那么我建议您使用这些基本 Linux 容器进行您自己的工作。

接下来的两个命令在容器中创建一个目录/opt/exampleapp来存放我们的源代码,并将所有内容复制到指定位置。COPY命令将本地目录中的所有内容添加到容器中,这可能比我们需要的要多。您可以在将来创建一个名为.dockerignore的文件,该文件将根据模式ignore一组文件,以便在COPY命令中忽略一些不想包含的常见文件。

接下来,您将看到一个RUN命令,该命令安装应用程序的依赖项,这些依赖项来自名为requirements.txt的文件,该文件包含在源代码库中。在这种情况下,将依赖项保存在这样的文件中是一个很好的做法,而pip命令就是为了支持这样做而创建的。

最后两个命令分别利用了 ENTRYPOINTCMD。对于这个简单的例子,我可以只使用其中一个。两者都包括在内是为了展示它们如何一起使用,CMD 本质上是传递给 ENTRYPOINT 中定义的可执行文件的参数。

构建容器

我们将使用 docker build 命令来创建容器。在终端窗口中,切换到包含 Dockerfile 的目录,并运行以下命令:

docker build .

您应该看到类似以下的输出:

Sending build context to Docker daemon    107kB
Step 1/9 : FROM alpine
 ---> 76da55c8019d
Step 2/9 : RUN apk update
 ---> Running in f72d5991a7cd
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/community/x86_64/APKINDEX.tar.gz
v3.6.2-130-gfde2d8ebb8 [http://dl-cdn.alpinelinux.org/alpine/v3.6/main]
v3.6.2-125-g93038b573e [http://dl-cdn.alpinelinux.org/alpine/v3.6/community]
OK: 8441 distinct packages available
 ---> b44cd5d0ecaa
Removing intermediate container f72d5991a7cd

Dockerfile 中的每一步都会反映在 Docker 构建镜像时发生的输出,对于更复杂的 Dockerfile,输出可能会非常庞大。在完成构建过程时,它会报告总体成功或失败,并且还会报告容器的 ID:

Step 8/9 : ENTRYPOINT python3
 ---> Running in 14c58ace8b14
 ---> 0ac8be8b042d
Removing intermediate container 14c58ace8b14
Step 9/9 : CMD /opt/exampleapp/exampleapp.py
 ---> Running in e769a65fedbc
 ---> b704504464dc
Removing intermediate container e769a65fedbc
Successfully built 4ef370855f35

当我们构建容器时,如果没有其他信息,它会在本地创建一个我们可以使用的镜像(它有一个 ID),但它没有名称或标签。在选择名称时,通常要考虑您托管容器镜像的位置。在这种情况下,我使用的是 CoreOS 的 Quay.io 服务,该服务为开源容器镜像提供免费托管。

要为刚刚创建的镜像打标签,我们可以使用 docker tag 命令:

docker tag 4ef370855f35 quay.io/kubernetes-for-developers/flask

这个标签包含三个相关部分。第一个 quay.io 是容器注册表。第二个 (kubernetes-for-developers) 是您容器的命名空间,第三个 (flask) 是容器的名称。我们没有为容器指定任何特定的标签,所以 docker 命令将使用 latest。

您应该使用标签来表示发布或开发中的其他时间点,以便您可以轻松地返回到这些时间点,并使用 latest 来表示您最近的开发工作,因此让我们也将其标记为一个特定的版本:

docker tag 4ef370855f35 quay.io/kubernetes-for-developers/flask:0.1.0

当您与他人共享镜像时,明确指出您正在使用的镜像是一个非常好的主意。一般来说,只考虑自己使用代码,每当与其他人共享镜像时,都要使用明确的标签。标签不一定是一个版本,虽然它的格式有限制,但几乎可以是任何字符串。

您可以使用 docker push 命令将已经标记的镜像传输到容器仓库。您需要先登录到您的容器仓库:

docker login quay.io

然后你可以推送这个镜像:

docker push quay.io/kubernetes-for-developers/flask

推送是指一个仓库,[quay.io/kubernetes-for-developers/flask]

0b3b7598137f: Pushed
602c2b5ffa76: Pushed 217607c1e257: Pushed
40ca06be4cf4: Pushed 5fbd4bb748e7: Pushed
0d2acef20dc1: Pushed
5bef08742407: Pushed
latest: digest: sha256:de0c5b85893c91062fcbec7caa899f66ed18d42ba896a47a2f4a348cbf9b591f size: 5826

通常,您希望从一开始就使用标签构建您的容器,而不是必须执行额外的命令。为此,您可以使用-t <your_name>选项将标签信息添加到build命令中。对于本书中的示例,我使用的名称是kubernetes-for-developers,因此我一直在使用以下命令构建示例:

docker build -t quay.io/kubernetes-for-developers/flask .

如果您正在按照此示例操作,请在先前命令中的quay.io/kubernetes-for-developers/flask .处使用您自己的值。您应该看到以下输出:

Sending build context to Docker daemon    107kB
Step 1/9 : FROM alpine
 ---> 76da55c8019d
Step 2/9 : RUN apk update
 ---> Using cache
 ---> b44cd5d0ecaa
Step 3/9 : RUN apk upgrade
 ---> Using cache
 ---> 0b1caea1a24d
Step 4/9 : RUN apk add python3
 ---> Using cache
 ---> 1e29fcb9621d
Step 5/9 : RUN mkdir -p /opt/exampleapp
 ---> Using cache
 ---> 622a12042892
Step 6/9 : COPY . /opt/exampleapp/
 ---> Using cache
 ---> 7f9115a50a0a
Step 7/9 : RUN pip3 install -r /opt/exampleapp/requirements.txt
 ---> Using cache
 ---> d8ef21ee1209
Step 8/9 : ENTRYPOINT python3
 ---> Using cache
 ---> 0ac8be8b042d
Step 9/9 : CMD /opt/exampleapp/exampleapp.py
 ---> Using cache
 ---> b704504464dc
Successfully built b704504464dc
Successfully tagged quay.io/kubernetes-for-developers/flask:latest

花点时间阅读该输出,并注意在几个地方报告了Using cache。您可能还注意到,该命令比您第一次构建镜像时更快。

这是因为 Docker 尝试重用任何未更改的层,以便它不必重新创建该工作。由于我们刚刚执行了所有这些命令,它可以使用在创建上一个镜像时制作的缓存中的层。

如果运行docker images命令,您现在应该看到它被列出:

REPOSITORY                                TAG       IMAGE ID      CREATED          SIZE
quay.io/kubernetes-for-developers/flask   0.1.0     b704504464dc  2 weeks ago         70.1MB
quay.io/kubernetes-for-developers/flask   latest    b704504464dc  2 weeks ago         70.1MB

随着您继续使用容器映像来存储和部署您的代码,您可能希望自动化创建映像的过程。作为一般模式,一个良好的构建过程应该是:

  • 从源代码控制获取代码

  • docker build

  • docker tag

  • docker push

这是我们在这些示例中使用的过程,您可以使用最适合您的工具自动化这些命令。我建议您设置一些可以快速且一致地在命令行上运行的东西。

运行您的容器

现在,让我们运行刚刚制作的容器。我们将使用kubectl run命令来指定最简单的部署——只是容器:

kubectl run flask --image=quay.io/kubernetes-for-developers/flask:latest --port=5000 --save-config
deployment “flask” created

要查看这是在做什么,我们需要向集群询问我们刚刚创建的资源的当前状态。当我们使用kubectl run命令时,它将隐式为我们创建一个 Deployment 资源,并且正如您在上一章中学到的,Deployment 中有一个 ReplicaSet,ReplicaSet 中有一个 Pod:

kubectl get deployments
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
flask     1         1         1            1           20h
kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
flask-1599974757-b68pw   1/1       Running   0          20h

我们可以通过请求与 Kubernetes 部署资源flask相关联的原始数据来获取有关此部署的详细信息:

kubectl get deployment flask -o json

我们也可以轻松地请求以YAML格式的信息,或者查询这些细节的子集,利用 JsonPath 或kubectl命令的其他功能。JSON 输出将非常丰富。它将以一个指示来自 Kubernetes 的 apiVersion 的键开始,资源的种类以及关于资源的元数据:

{
    "apiVersion": "extensions/v1beta1",
    "kind": "Deployment",
    "metadata": {
        "annotations": {
            "deployment.kubernetes.io/revision": "1"
        },
        "creationTimestamp": "2017-09-16T00:40:44Z",
        "generation": 1,
        "labels": {
            "run": "flask"
        },
        "name": "flask",
        "namespace": "default",
        "resourceVersion": "51293",
        "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/flask",
        "uid": "acbb0128-9a77-11e7-884c-0aef48c812e4"
    },

在此之下通常是部署本身的规范,其中包含了大部分正在运行的核心内容。

    "spec": {
        "replicas": 1,
        "selector": {
            "matchLabels": {
                "run": "flask"
            }
        },
        "strategy": {
            "rollingUpdate": {
                "maxSurge": 1,
                "maxUnavailable": 1
            },
            "type": "RollingUpdate"
        },
        "template": {
            "metadata": {
                "creationTimestamp": null,
                "labels": {
                    "run": "flask"
                }
            },
            "spec": {
                "containers": [
                    {
                        "image": "quay.io/kubernetes-for-developers/flask:latest",
                        "imagePullPolicy": "Always",
                        "name": "flask",
                        "ports": [
                            {
                                "containerPort": 5000,
                                "protocol": "TCP"
                            }
                        ],
                        "resources": {},
                        "terminationMessagePath": "/dev/termination-log",
                        "terminationMessagePolicy": "File"
                    }
                ],
                "dnsPolicy": "ClusterFirst",
                "restartPolicy": "Always",
                "schedulerName": "default-scheduler",
                "securityContext": {},
                "terminationGracePeriodSeconds": 30
            }
        }
    },

最后一部分通常是状态,它指示了部署的当前状态,即您请求信息的时间。

    "status": {
        "availableReplicas": 1,
        "conditions": [
            {
                "lastTransitionTime": "2017-09-16T00:40:44Z",
                "lastUpdateTime": "2017-09-16T00:40:44Z",
                "message": "Deployment has minimum availability.",
                "reason": "MinimumReplicasAvailable",
                "status": "True",
                "type": "Available"
            }
        ],
        "observedGeneration": 1,
        "readyReplicas": 1,
        "replicas": 1,
        "updatedReplicas": 1
    }
}

在 Kubernetes 中运行 Pod 时,请记住它是在一个沙盒中运行的,与世界其他部分隔离开来。Kubernetes 有意这样做,这样您就可以指定 Pod 应该如何连接以及集群外部可以访问什么。我们将在后面的章节中介绍如何设置外部访问。与此同时,您可以利用kubectl中的两个命令直接从开发机器获取访问权限:kubectl port-forwardkubectl proxy

这两个命令都是通过将代理从您的本地开发机器到 Kubernetes 集群,为您提供对正在运行的代码的私人访问权限。port-forward命令将打开一个特定的 TCP(或 UDP)端口,并安排所有流量转发到集群中的 Pod。代理命令使用已经存在的 HTTP 代理来转发 HTTP 流量进出您的 Pod。这两个命令都依赖于知道 Pod 名称来建立连接。

Pod 名称

由于我们正在使用一个 Web 服务器,使用代理是最合理的,因为它将根据 Pod 的名称将 HTTP 流量转发到一个 URL。在这之前,我们将使用port-forward命令,如果您的编写不使用 HTTP 协议,这将更相关。

你需要的关键是创建的 Pod 的名称。当我们之前运行kubectl get pods时,你可能注意到名称不仅仅是flask,而是在名称中包含了一些额外的字符:flask-1599974757-b68pw。当我们调用kubectl run时,它创建了一个部署,其中包括一个包裹在 Pod 周围的 Kubernetes ReplicaSet。名称的第一部分(flask)来自部署,第二部分(1599974757)是分配给创建的 ReplicaSet 的唯一名称,第三部分(b68pw)是分配给创建的 Pod 的唯一名称。如果你运行以下命令:

kubectl get replicaset 

结果将显示副本集:

NAME               DESIRED   CURRENT   READY     AGE
flask-1599974757   1         1         1         21h

你可以看到 ReplicaSet 名称是 Pod 名称的前两部分。

端口转发

现在我们可以使用该名称来要求kubectl设置一个代理,将我们指定的本地端口上的所有流量转发到我们确定的 Pod 关联的端口。通过使用以下命令查看使用部署创建的 Pod 的完整名称:

kubectl get pods

在我的例子中,结果是flask-1599974757-b68pw,然后可以与port-forward命令一起使用:

kubectl port-forward flask-1599974757-b68pw 5000:5000

输出应该类似于以下内容:

Forwarding from 127.0.0.1:5000 -> 5000
Forwarding from [::1]:5000 -> 5000

这将转发在本地机器上创建的任何流量到 Podflask-1599974757-b68pw的 TCP 端口5000上的 TCP 端口5000

你会注意到你还没有回到命令提示符,这是因为命令正在积极运行,以保持我们请求的特定隧道活动。如果我们取消或退出kubectl命令,通常通过按Ctrl + C,那么端口转发将立即结束。kubectl proxy的工作方式相同,因此当你使用诸如kubectl port-forwardkubectl proxy的命令时,你可能希望打开另一个终端窗口单独运行该命令。

当命令仍在运行时,打开浏览器并输入此 URL:http://localhost:5000。响应应该返回Index Page。当我们调用kubectl run命令时,我特意选择端口5000来匹配 Flask 的默认端口。

代理

你可以使用的另一个命令来访问你的 Pod 是kubectl proxy命令。代理不仅提供对你的 Pod 的访问,还提供对所有 Kubernetes API 的访问。要调用代理,运行以下命令:

kubectl proxy

输出将显示类似于以下内容:

Starting to serve on 127.0.0.1:8001

port-forward命令一样,在代理终止之前,您在终端窗口中将不会收到提示。在它处于活动状态时,您可以通过这个代理访问 Kubernetes REST API 端点。打开浏览器,输入 URL http://localhost:8001/

您应该看到一个类似以下的 JSON 格式的 URL 列表:

{
 "paths": [ "/api", "/api/v1", "/apis", "/apis/", "/apis/admissionregistration.k8s.io", "/apis/admissionregistration.k8s.io/v1alpha1", "/apis/apiextensions.k8s.io", "/apis/apiextensions.k8s.io/v1beta1", "/apis/apiregistration.k8s.io",

这些都是直接访问 Kubernetes 基础设施。其中一个 URL 是/api/v1 - 尽管它没有被明确列出,它使用 Kubernetes API 服务器来根据名称为 Pod 提供代理。当我们调用我们的run命令时,我们没有指定一个命名空间,所以它使用了默认的命名空间,称为default。查看 Pod 的 URL 模式是:

http://localhost:8001/api/v1/proxy/namespaces/<NAME_OF_NAMESPACE>/pods/<POD_NAME>/

在我们的 Pod 的情况下,这将是:

http://localhost:8001/api/v1/proxy/namespaces/default/pods/flask-1599974757-b68pw/

如果您在浏览器中打开一个由 Kubernetes 集群分配的 Pod 名称创建的 URL,它应该显示与使用port-forward命令看到的相同的输出。

代理是如何知道连接到容器上的端口 5000 的?

当您运行容器时,Kubernetes 并不会神奇地知道您的代码正在侦听哪些 TCP 端口。当我们使用kubectl run命令创建此部署时,我们在该命令的末尾添加了--port=5000选项。Kubernetes 使用这个选项来知道程序应该在端口5000上侦听 HTTP 流量。如果您回顾一下kubectl get deployment -o json命令的输出,您会看到其中一个部分在containers键下包括我们提供的镜像、部署的名称和一个数据结构,指示访问容器的默认端口为5000。如果我们没有提供额外的细节,代理将假定我们希望在端口80访问容器。由于我们的开发容器上没有在端口80上运行任何内容,您会看到类似于以下的错误:

Error: 'dial tcp 172.17.0.4:80: getsockopt: connection refused'
Trying to reach: 'http://172.17.0.4/'

从您的应用程序获取日志

有更多的方法可以与在容器中运行的代码进行交互,我们将在以后的章节中介绍。如果您运行的代码没有侦听 TCP 套接字以提供 HTTP 流量,或者类似的内容,那么通常您希望查看您的代码创建的输出以知道它是否正在运行。

容器专门设置为捕获您指定的可执行文件的 STDOUT 和 STDERR 的任何输出,并将其捕获到日志中,可以使用另一个kubectl命令检索这些日志:kubectl logs。与proxyport-forward命令一样,此命令需要知道您要交互的 Pod 的名称。

运行以下命令:

kubectl logs flask-1599974757-b68pw

您应该看到类似以下的输出:

 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 996-805-904

示例 - Node.js/Express 容器映像

此示例遵循与 Python 示例相同的模式,即使用 Express 库构建的简单 Node.js 应用程序,以详细介绍如何使用 Kubernetes。如果您更熟悉 JavaScript 开发,则此示例可能更有意义。示例应用程序直接来自 Express 文档(expressjs.com/en/starter/generator.html)。

您可以从 GitHub 下载此代码副本github.com/kubernetes-for-developers/kfd-nodejs/tree/first_container。由于我们将使这些文件发展,此处引用的代码可在first_container标签处获得。如果要使用 Git 检索这些文件,可以使用以下命令:

git clone https://github.com/kubernetes-for-developers/kfd-nodejs
cd kfd-nodejs
git checkout tags/first_container

与 Python 示例一样,我们将从 Dockerfile 开始。提醒一下,这是定义构建到容器中的内容以及构建方式的文件。此 Dockerfile 的目标是:

  • 获取并安装基础操作系统的任何关键安全补丁

  • 安装我们将需要用来运行我们的代码的语言或运行时

  • 安装我们的代码中未直接包含的任何依赖项

  • 将我们的代码复制到容器中

  • 定义如何以及何时运行

FROM alpine
# load any public updates from Alpine packages
RUN apk update
# upgrade any existing packages that have been updated
RUN apk upgrade
# add/install python3 and related libraries
# https://pkgs.alpinelinux.org/package/edge/main/x86/python3
RUN apk add nodejs nodejs-npm
# make a directory for our application
WORKDIR /src
# move requirements file into the container
COPY package.json .
COPY package-lock.json .
# install the library dependencies for this application
RUN npm install --production
# copy in the rest of our local source
COPY . .
# set the debug environment variable
ENV DEBUG=kfd-nodejs:*
CMD ["npm", "start"]

与 Python 示例一样,此容器基于 Alpine Linux。您将看到一些可能不熟悉的命令,特别是apk命令。作为提醒,此命令用于安装、更新和删除 Alpine Linux 软件包。这些命令更新 Alpine 软件包存储库,升级图像中安装的所有已安装和预先存在的软件包,然后从软件包安装nodejsnpm。这些步骤基本上使我们得到一个可以运行 Node.js 应用程序的最小容器。

接下来的命令在容器中创建一个目录/src来存放我们的源代码,复制package.json文件,然后使用npm安装运行代码所需的依赖项。npm install命令与--production选项一起使用,只安装package.json中列出的运行代码所需的项目 - 开发依赖项被排除在外。Node.js 通过其package.json格式轻松而一致地维护依赖关系,将生产所需的依赖与开发所需的依赖分开是一个良好的做法。

最后两个命令利用了ENVCMD。这与 Python 示例不同,我在 Python 示例中使用了CMDENTRYPOINT来突出它们如何一起工作。在这个示例中,我使用ENV命令将DEBUG环境变量设置为与 Express 文档中示例指令匹配。然后CMD包含一个启动我们代码的命令,简单地利用npm运行package.json中定义的命令,并使用之前的WORKDIR命令为该调用设置本地目录。

构建容器

我们使用相同的docker build命令来创建容器:

docker build .

你应该看到类似以下的输出:

Sending build context to Docker daemon  197.6kB
Step 1/11 : FROM alpine
 ---> 76da55c8019d
Step 2/11 : RUN apk update
 ---> Using cache
 ---> b44cd5d0ecaa

就像你在基于 Python 的示例中看到的那样,Dockerfile 中的每个步骤都会反映出输出,显示 Docker 根据你的指令(Dockerfile)构建容器镜像的过程:

Step 9/11 : COPY . .
 ---> 6851a9088ce3
Removing intermediate container 9fa9b8b9d463
Step 10/11 : ENV DEBUG kfd-nodejs:*
 ---> Running in 663a2cd5f31f
 ---> 30c3b45c4023
Removing intermediate container 663a2cd5f31f
Step 11/11 : CMD npm start
 ---> Running in 52cf9638d065
 ---> 35d03a9d90e6
Removing intermediate container 52cf9638d065
Successfully built 35d03a9d90e6

与 Python 示例一样,这将构建一个只有 ID 的容器。这个示例还利用 Quay 来公开托管图像,因此我们将适当地获取图像,以便上传到 Quay:

docker tag 35d03a9d90e6 quay.io/kubernetes-for-developers/nodejs

与 Python 示例一样,标签包含三个相关部分 - quay.io 是容器注册表。第二个(kubernetes-for-developers)是容器的命名空间,第三个(nodejs)是容器的名称。与 Python 示例一样,使用相同的命令上传容器,引用nodejs而不是flask

docker login quay.io docker push quay.io/kubernetes-for-developers/nodejs
The push refers to a repository [quay.io/kubernetes-for-developers/nodejs]
0b6165258982: Pushed
8f16769fa1d0: Pushed
3b43ed4da811: Pushed
9e4ead6d58f7: Pushed
d56b3cb786f1: Pushedfad7fd538fb6: Pushing [==================>                                ]  11.51MB/31.77MB
5fbd4bb748e7: Pushing [==================================>                ]  2.411MB/3.532MB
0d2acef20dc1: Pushing [==================================================>]  1.107MB
5bef08742407: Pushing [================>                                  ]  1.287MB/3.966MB

完成后,你应该看到类似以下的内容:

The push refers to a repository [quay.io/kubernetes-for-developers/nodejs]
0b6165258982: Pushed
8f16769fa1d0: Pushed
3b43ed4da811: Pushed
9e4ead6d58f7: Pushed
d56b3cb786f1: Pushed
fad7fd538fb6: Pushed
5fbd4bb748e7: Pushed
0d2acef20dc1: Pushed
5bef08742407: Pushed
latest: digest: sha256:0e50e86d27a4b29b5b10853d631d8fc91bed9a37b44b111111dcd4fd9f4bc723 size: 6791

与 Python 示例一样,你可能希望在同一命令中构建和标记。对于 Node.js 示例,该命令将是:

docker build -t quay.io/kubernetes-for-developers/nodejs:0.2.0 .

如果在构建图像后立即运行,应该显示类似以下的输出:

Sending build context to Docker daemon  197.6kB
Step 1/11 : FROM alpine
 ---> 76da55c8019d
Step 2/11 : RUN apk update
 ---> Using cache
 ---> b44cd5d0ecaa
Step 3/11 : RUN apk upgrade
 ---> Using cache
 ---> 0b1caea1a24d
Step 4/11 : RUN apk add nodejs nodejs-npm
 ---> Using cache
 ---> 193d3570516a
Step 5/11 : WORKDIR /src
 ---> Using cache
 ---> 3a5d78afa1be
Step 6/11 : COPY package.json .
 ---> Using cache
 ---> 29724b2bd1b9
Step 7/11 : COPY package-lock.json .
 ---> Using cache
 ---> ddbcb9af6ffc
Step 8/11 : RUN npm install --production
 ---> Using cache
 ---> 1556a20af49a
Step 9/11 : COPY . .
 ---> Using cache
 ---> 6851a9088ce3
Step 10/11 : ENV DEBUG kfd-nodejs:*
 ---> Using cache
 ---> 30c3b45c4023
Step 11/11 : CMD npm start
 ---> Using cache
 ---> 35d03a9d90e6
Successfully built 35d03a9d90e6
Successfully tagged quay.io/kubernetes-for-developers/nodejs:latest

与使用 Docker 缓存的图像层相比,速度会快得多,因为它使用了先前构建的图像层。

如果运行docker images命令,你现在应该能看到它被列出来了:

REPOSITORY                                TAG       IMAGE ID      CREATED          SIZE
quay.io/kubernetes-for-developers/nodejs  0.2.0    46403c409d1f  4 minutes ago    81.9MB

如果你将自己的镜像推送到quay.io作为容器仓库,除了这些命令,你可能需要登录网站并将镜像设为公开。默认情况下,quay.io会保持镜像私有,即使是公共的,直到你在他们的网站上批准它们的公开。

运行你的容器

现在,让我们运行刚刚创建的容器。我们将使用kubectl run命令,就像 Python 示例一样,但是用nodejs替换 flask 来指定我们刚刚创建和上传的容器:

kubectl run nodejs --image=quay.io/kubernetes-for-developers/nodejs:0.2.0 --port=3000
deployment “nodejs” created

为了查看它的运行情况,我们需要向集群请求刚刚创建的资源的当前状态:

kubectl get deployments
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nodejs    1         1         1            1           1d
kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
nodejs-568183341-2bw5v   1/1       Running   0          1d

kubectl run命令不受语言限制,并且与 Python 示例的方式相同。在这种情况下创建的简单部署被命名为nodejs,我们可以请求与之前 Python 示例相同类型的信息:

kubectl get deployment nodejs -o json

JSON 输出应该会非常详细,并且会有多个部分。输出的顶部会有关于部署的apiVersionkindmetadata

{
    "apiVersion": "extensions/v1beta1",
    "kind": "Deployment",
    "metadata": {
        "annotations": {
            "deployment.kubernetes.io/revision": "1"
        },
        "creationTimestamp": "2017-09-16T10:06:30Z",
        "generation": 1,
        "labels": {
            "run": "nodejs"
        },
        "name": "nodejs",
        "namespace": "default",
        "resourceVersion": "88886",
        "selfLink": "/apis/extensions/v1beta1/namespaces/default/deployments/nodejs",
        "uid": "b5d94f83-9ac6-11e7-884c-0aef48c812e4"
    },

通常,在这之下会有spec,其中包含了你刚刚要求运行的核心内容:

    "spec": {
        "replicas": 1,
        "selector": {
            "matchLabels": {
                "run": "nodejs"
            }
        },
        "strategy": {
            "rollingUpdate": {
                "maxSurge": 1,
                "maxUnavailable": 1
            },
            "type": "RollingUpdate"
        },
        "template": {
            "metadata": {
                "creationTimestamp": null,
                "labels": {
                    "run": "nodejs"
                }
            },
            "spec": {
                "containers": [
                    {
                        "image": "quay.io/kubernetes-for-developers/nodejs:0.2.0",
                        "imagePullPolicy": "IfNotPresent",

                        "name": "nodejs",
                        "ports": [
                            {
                                "containerPort": 3000,
                                "protocol": "TCP"
                            }
                        ],
                        "resources": {},
                        "terminationMessagePath": "/dev/termination-log",
                        "terminationMessagePolicy": "File"
                    }
                ],
                "dnsPolicy": "ClusterFirst",
                "restartPolicy": "Always",
                "schedulerName": "default-scheduler",
                "securityContext": {},
                "terminationGracePeriodSeconds": 30
            }
        }
    },

最后一部分是status,它指示了部署的当前状态(截至请求此信息时):

    "status": {
        "availableReplicas": 1,
        "conditions": [
            {
                "lastTransitionTime": "2017-09-16T10:06:30Z",
                "lastUpdateTime": "2017-09-16T10:06:30Z",
                "message": "Deployment has minimum availability.",
                "reason": "MinimumReplicasAvailable",
                "status": "True",
                "type": "Available"
            }
        ],
        "observedGeneration": 1,
        "readyReplicas": 1,
        "replicas": 1,
        "updatedReplicas": 1
    }
}

当 Pod 在 Kubernetes 中运行时,它是在一个与世隔绝的沙盒中运行的。Kubernetes 故意这样做,这样你就可以指定哪些系统可以相互通信,以及可以从外部访问什么。对于大多数集群,Kubernetes 的默认设置允许任何 Pod 与任何其他 Pod 通信。就像 Python 示例一样,你可以利用kubectl中的两个命令之一来从开发机器直接访问:kubectl port-forward 或kubectl proxy。

端口转发

现在我们可以使用这个名称来要求kubectl设置一个代理,将我们指定的本地端口的所有流量转发到我们确定的 Pod 关联的端口。Node.js 示例在不同的端口上运行(端口3000而不是端口5000),因此命令需要相应地更新:

kubectl port-forward nodejs-568183341-2bw5v 3000:3000

输出应该类似于以下内容:

Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000

这会将在本地机器上创建的任何流量转发到nodejs-568183341-2bw5v Pod 上的 TCP 端口3000

就像 Python 示例一样,因为命令正在运行以保持这个特定的隧道活动,所以你还没有得到一个命令提示符。提醒一下,你可以通过按下 Ctrl + C 来取消或退出 kubectl 命令,端口转发将立即结束。

当命令仍在运行时,打开浏览器并输入此 URL:http://localhost:3000。响应应该返回说 Index Page。当我们调用 kubectl run 命令时,我特意选择端口 3000 来匹配 Express 的默认端口。

代理

由于这是一个基于 HTTP 的应用程序,我们也可以使用 kubectl proxy 命令来访问我们代码的响应:

kubectl proxy

输出将显示类似于以下内容:

Starting to serve on 127.0.0.1:8001

提醒一下,在代理终止之前,你不会在终端窗口中得到提示符。就像 Python 示例一样,我们可以根据我们在调用 kubectl run 命令时使用的 Pod 名称和命名空间来确定代理将用于转发到我们容器的 URL。由于我们没有指定命名空间,它使用的是默认的 default。访问 Pod 的 URL 模式与 Python 示例相同:

http://localhost:8001/api/v1/proxy/namespaces/<NAME_OF_NAMESPACE>/pods/<POD_NAME>/

在我们的 Pod 的情况下,这将是:

http://localhost:8001/api/v1/proxy/namespaces/default/pods/nodejs-568183341-2bw5v/

如果你在浏览器中打开一个由你的 Kubernetes 集群分配的 Pod 名称创建的 URL,它应该显示与使用 port-forward 命令看到的相同的输出。

获取应用程序的日志

就像 Python 示例一样,Node.js 示例也会将一些输出发送到 STDOUT。由于容器专门设置来捕获你指定的可执行文件的 STDOUTSTDERR 的任何输出,并将其捕获到日志中,相同的命令将起作用,以显示来自 Node.js 应用程序的日志输出:

kubectl logs nodejs-568183341-2bw5v

这应该显示类似于以下的输出:

> kfd-nodejs@0.0.0 start /src
> node ./bin/www
Sat, 16 Sep 2017 10:06:41 GMT kfd-nodejs:server Listening on port 3000
GET / 304 305.615 ms - -
GET /favicon.ico 404 54.056 ms - 855
GET /stylesheets/style.css 200 63.234 ms - 111
GET / 200 48.033 ms - 170
GET /stylesheets/style.css 200 1.373 ms - 111

给你的容器图像打标签

在 Docker 图像上使用 :latest 标签非常方便,但很容易导致混淆,不知道到底在运行什么。如果你使用 :latest,那么告诉 Kubernetes 在加载容器时始终尝试拉取新的镜像是一个非常好的主意。我们将在第四章中看到如何设置这一点,声明式基础设施,当我们谈论声明性地定义我们的应用程序时。

另一种方法是制作显式标签,使用标签进行构建,并使用docker tag将映像标记为latest以方便使用,但在提交到源代码控制时保持特定的标签。对于本示例,选择的标签是0.2.0,使用语义化版本表示要与容器一起使用的值,并与git tag匹配。

在制作这个示例时使用的步骤是:

git tag 0.2.0
docker build -t quay.io/kubernetes-for-developers/nodejs:0.2.0 .
git push origin master --tags
docker push quay.io/kubernetes-for-developers/nodejs

摘要

在本章中,我们回顾了容器的组成,如何在互联网上存储和共享容器,以及一些用于创建自己的容器的命令。然后,我们利用这些知识在 Python 和 Node.js 中进行了示例演示,分别创建了简单的基于 Web 的服务,将它们构建成容器映像,并在 Kubernetes 中运行它们。在下一章中,我们将深入探讨如何与打包成容器的代码进行交互,并探索在开发过程中充分利用容器和 Kubernetes 的技巧。

第三章:在 Kubernetes 中与您的代码交互

在上一章中,我们介绍了制作容器镜像,并使用 Python 和 Node.js 创建了简单的示例。在本章中,我们将扩展与正在运行的代码交互的简要介绍,并深入了解如何查看代码的运行情况,运行其他命令,并从这些 Pod 中进行调试的更多细节。

本章的各节包括:

  • 编写软件以在 Pod 中运行的实用注释

  • 从您的容器和 Pod 中获取日志

  • 与正在运行的 Pod 交互

  • Kubernetes 概念—标签和选择器

  • Kubernetes 资源—服务

  • 从您的 Pod 中发现服务

编写软件以在容器中运行的实用注释

要在开发过程中使用 Kubernetes,其中一个基本要求是在容器中运行代码。正如您所见,这为您的开发过程增加了一些步骤。它还在如何构造代码和与之交互方面增加了一些约束,主要是为了让您能够利用这些约束,让 Kubernetes 来运行进程、连接它们,并协调任何输出。这与许多开发人员习惯的在本地开发机器上运行一个或多个进程,甚至需要额外服务来运行应用程序(如数据库或缓存)的习惯非常不同。

本节提供了一些有关如何更有效地使用容器的提示和建议。

获取可执行代码的选项

除了在创建容器时定义的ENTRYPOINTCMD之外,容器镜像还可以通过ENV命令定义环境变量。ENTRYPOINTCMD和环境变量可以在执行时或在定义部署时被覆盖或更新。因此,环境变量成为向容器传递配置的最常见方式之一。

编写软件以利用这些环境变量将是重要的。在创建软件时,请确保您可以利用环境变量以及代码中的命令行参数。大多数语言都有一个库,可以支持选项作为命令行参数或环境变量。

在下一章中,我们将看到如何设置配置并在部署时将其传递给您的容器。

构建容器镜像的实用注释

以下是维护容器镜像的建议和实用建议:

  • 在源代码存储库中保留一个 Dockerfile。如果您的应用程序源代码本身位于 Git 存储库中,那么在存储库中包含一个 Dockerfile 是非常合理的。您可以引用要从相对目录复制或添加的文件,该目录是您的源代码所在的位置。在存储库的根目录中看到 Dockerfile 是很常见的,或者如果您正在从一个包含许多项目的 monorepo 中工作,可以考虑在与项目源代码相同的目录中创建一个 Docker 目录:

  • 如果您想利用 Docker Hub、Quay 或其他容器存储库上的自动 Docker 构建,自动化系统期望 Dockerfile 位于 Git 存储库的根目录中。

  • 保持一个单独的脚本(如果需要)用于创建容器镜像。更具体地说,不要将创建容器镜像的过程与代码生成、编译、测试或验证混在一起。这将清晰地区分出您可能需要的开发任务,具体取决于您的语言和框架。这将允许您在自动化流水线中在需要时包含它。

  • 在基础镜像中添加额外工具可能非常诱人,以便进行调试、支持新的或额外的诊断工作等。明确和有意识地选择要在镜像中包含的额外工具。我建议最小化额外工具的使用,不仅因为它们会使镜像变得更大,而且通常那些在调试中非常有效的工具也会给黑客提供更容易利用的选项:

  • 如果您发现必须在镜像中添加调试工具,请考虑在子目录中创建第二个 Dockerfile,该文件添加到第一个文件中,并且只包含您想要添加的调试工具。如果这样做,我建议您在镜像的名称中添加一个-debug以明确表明该镜像已安装额外的工具。

  • 构建容器镜像时,要考虑其生产使用,并将其作为默认设置。对于容器来说,这通常表示容器中提供的环境变量的默认值。一般来说,尽量不要在容器镜像中包含单元测试、开发任务等所需的依赖项:

  • 在 Node.js 的情况下,使用环境变量ENV=PROD,这样npm就不会包含开发依赖项,或者使用命令行npm install —production明确地将它们剥离。

  • 在创建容器后,将整个容器视为只读文件系统。如果您想要有某个地方来写入本地文件,请明确标识该位置并在容器中设置一个卷。

发送程序输出

kubectl logs(以及 Docker 的等效命令:docker logs)默认将stdoutstderr组合在一起,并将任何显示为日志的内容传递给容器。您可能也有过在代码中创建特定的日志记录功能,将日志写入磁盘上的文件位置的经验。一般来说,将日志写入文件系统位置不鼓励在容器内运行的软件中,因为要将其包含在一般日志记录中意味着必须再次读取它,这会不必要地增加磁盘 I/O。

如果您希望在应用程序中支持聚合日志记录的方法,那么通常希望在容器和/或 Pod 之外定义一些内容来帮助捕获、传输和处理这些日志。

一般来说,如果您编写程序将日志记录到stdoutstderr,那么运行这些容器的容器和 Kubernetes 通常会帮助您更轻松地访问这些细节。

日志

获取有关代码运行情况的最常见方法通常是通过日志。每种语言和开发环境都有自己的模式来公开这些细节,但基本上,它可以简单地通过打印语句发送一行文本到stdout,这对于快速和简单的调试无疑是最一致的方法。当您在 Kubernetes 中部署和运行代码时,它可以访问来自每个 Pod 和容器的日志,其中日志在这种情况下是将数据发送到stdoutstderr

如果您现有的开发模式将输出写入特定的文件位置,也许您的框架包括在文件增长时旋转这些日志文件的功能,您可能希望考虑只是将数据发送到stdout和/或stderr,以便 Kubernetes 可以使这种协调工作。

具有多个容器的 Pod

到目前为止,我们的示例都很简单,一个 Pod 中只有一个容器。一个 Pod 可以同时拥有多个容器,获取日志的命令可以指定使用哪个容器。如果只有一个容器,就不需要指定要使用哪个容器。

如果需要指定特定的容器,可以使用-c选项,或者将其添加到logs命令中。例如,如果有一个名为webapp的 Pod,其中包含两个容器flaskbackground,你想要查看background容器的日志,可以使用kubectl logs webapp backgroundkubectl logs webapp -c background命令。

同样,定义部署中的 Pod 和容器也有一个快捷方式。与其通过 Kubernetes 分配的名称来指定完整的 Pod 名称,你可以只用部署名称作为 Pod 名称的前缀。例如,如果我们之前使用kubectl run flask image=…命令创建了一个部署,我们可以使用以下命令:

kubectl logs deployment/flask

这样就不需要查找特定的 Pod 名称,然后根据该名称请求日志了。

流式传输日志

通常希望能够连续查看容器的日志,随着容器提供信息而更新。你可以使用-f选项来实现这一点。例如,要查看与flask部署相关的 Pod 的更新日志,可以运行以下命令:

kubectl logs deployment/flask -f

当你与该服务交互,或者该服务写入stdout并进行正常日志记录时,你会看到输出流到你的控制台。

之前的日志

日志通常是特定于活动容器的。然而,通常需要查看如果容器失败时日志中可能包含的内容,或者如果你部署更新后出现了意外情况。Kubernetes 会保留对任何 Pod 的先前容器的引用(如果存在),这样你在需要时就可以获取这些信息。只要日志对 Kubernetes 可用,你可以使用-p选项来实现这一点。

时间戳

日志输出也可以包含时间戳,尽管默认情况下不会。你可以通过添加--timestamps选项来获取带有时间戳前缀的日志消息。例如:

kubectl logs deployment/flask --timestamps

然后你可能会看到以下内容:

2017-09-16T03:54:20.851827407Z  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
2017-09-16T03:54:20.852424207Z  * Restarting with stat
2017-09-16T03:54:21.163624707Z  * Debugger is active!
2017-09-16T03:54:21.165358607Z  * Debugger PIN: 996-805-904

值得注意的是,时间戳来自运行容器的主机,而不是您的本地机器,因此这些日志中的时区通常不是您所在的时区。所有时间戳都包括完整的时区细节(通常设置为 UTC-0 时区),因此值可以很容易地转换。

更多的调试技术

有几种调试技术可以处理部署到现有集群中的代码。这些包括:

  • 容器镜像的交互式部署

  • 附加到运行中的 Pod

  • 在现有 Pod 中运行第二个命令

镜像的交互式部署

您还可以使用 kubectl run 命令启动与 Pod 的交互会话。这对于登录并查看容器镜像中可用的内容,或者在您复制到容器镜像中的软件的上下文中非常有用。

例如,如果您想运行一个 shell 来查看我用于 Python 示例的基本 Alpine 容器镜像内部,您可以运行以下命令:

kubectl run -i -t alpine-interactive --image=alpine -- sh

-i选项是告诉它使会话交互,并且-t选项(几乎总是与-i选项一起使用)表示它应为交互式输出分配一个 TTY 会话(终端会话)。结尾的-- sh是一个覆盖,提供一个特定的命令来调用这个会话,在这种情况下是sh`,要求执行 shell。

当您调用此命令时,它仍然设置一个部署,当您退出交互式 shell 时,输出将告诉您如何重新连接到相同的交互式 shell。输出将看起来像下面这样:

Session ended, resume using 'kubectl attach alpine-interactive-1535083360-4nxj8 -c alpine-interactive -i -t' command when the pod is running

如果您想要终止该部署,您需要运行以下命令:

kubectl delete deployment alpine-interactive

这种技术对于在 Kubernetes 集群中启动和运行容器镜像,并让您与之交互的 shell 访问非常有用。如果您习惯使用 Python、Node.js 或类似的动态语言,那么能够加载所有库并激活 REPL 以便与之交互或交互式地查看运行环境,将会非常有用。

例如,我们可以使用相同的 Python 镜像来为我们的 Flask 应用程序做这个。要将其作为一个可以稍后删除的交互式会话启动,使用以下命令:

kubectl run -i -t python-interactive --image=quay.io/kubernetes-for-developers/flask:latest --command -- /bin/sh 

此命令可能需要一些时间才能完成,因为它将等待 Kubernetes 下载映像并启动它,使用我们最初放置的命令(/bin/sh),而不是我们为其定义的入口点。不久之后,您应该在终端窗口中看到类似以下的一些输出:

If you don't see a command prompt, try pressing enter.
/ #

在这一点上,您可以调用 Python 并直接与 Python REPL 交互,加载代码并执行所需的操作。以下是一些示例命令,以显示这可能如何工作:

cd /opt/exampleapp
/opt/exampleapp # python3
Python 3.6.1 (default, May 2 2017, 15:16:41)
[GCC 6.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.environ
environ({'KUBERNETES_PORT': 'tcp://10.0.0.1:443', 'KUBERNETES_SERVICE_PORT': '443', 'HOSTNAME': 'python-interactive-666665880-hwvvp', 'SHLVL': '1', 'OLDPWD': '/', 'HOME': '/root', 'TERM': 'xterm', 'KUBERNETES_PORT_443_TCP_ADDR': '10.0.0.1', 'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'KUBERNETES_PORT_443_TCP_PORT': '443', 'KUBERNETES_PORT_443_TCP_PROTO': 'tcp', 'KUBERNETES_PORT_443_TCP': 'tcp://10.0.0.1:443', 'KUBERNETES_SERVICE_PORT_HTTPS': '443', 'PWD': '/opt/exampleapp', 'KUBERNETES_SERVICE_HOST': '10.0.0.1'})
>>> import flask
>>> help(flask.app)
Help on module flask.app in flask:
NAME
 flask.app
DESCRIPTION
 flask.app
 ~~~~~~~~~
This module implements the central WSGI application object.
:copyright: (c) 2015 by Armin Ronacher.
 :license: BSD, see LICENSE for more details.
CLASSES
 flask.helpers._PackageBoundObject(builtins.object)
 Flask
class Flask(flask.helpers._PackageBoundObject)
 | The flask object implements a WSGI application and acts as the central
 | object. It is passed the name of the module or package of the
 | application. Once it is created it will act as a central registry for
 | the view functions, the URL rules, template configuration and much more.
 |
 | The name of the package is used to resolve resources from inside the
 | package or the folder the module is contained in depending on if the
 | package parameter resolves to an actual python package (a folder with
>>> exit()
/opt/exampleapp #

与部署交互完成后,您可以通过按下Ctrl + D或输入exit来退出 shell。

Session ended, resume using 'kubectl attach python-interactive-666665880-hwvvp -c python-interactive -i -t' command when the pod is running

这将保持部署运行,因此您可以使用上述命令重新附加到它,或者在需要时删除部署并重新创建它。要删除它,您将使用以下命令:

kubectl delete deployment python-interactive
deployment "python-interactive" deleted

连接到正在运行的 Pod

如果您的 Pod 正在运行,并且您想从该容器映像的上下文中运行一些命令,您可以附加一个交互式会话。您可以通过kubectl attach命令来执行此操作。Pod 必须处于活动状态才能使用此命令,因此如果您试图弄清楚为什么 Pod 未正确启动,此命令可能不会有帮助。

附加到 Pod 将连接stdin到您的进程,并将stdoutstderr的任何内容呈现在屏幕上,因此它更像是kubectl logs -f命令的交互版本。为了使其有用,您指定的容器还需要接受stdin。您还需要显式启用 TTY 才能连接到它。如果不这样做,您经常会看到以下内容作为输出的第一行:

Unable to use a TTY - container flask did not allocate one

如果您之前使用以下命令从nodejs示例创建了一个部署:

kubectl run nodejs --image=quay.io/kubernetes-for-developers/nodejs:latest —-port=3000

您可以使用以下命令附加到此 Pod:

kubectl attach deployment/express -i -t

这将返回一个警告消息:

Unable to use a TTY - container flask did not allocate one
If you don't see a command prompt, try pressing enter.

此后,当您与服务交互时,您将在终端窗口中看到stdout流。

如果您的应用程序将其日志打印到stdout并且您希望在与代码交互时观看这些日志,例如使用 Web 浏览器,这将非常有效。要使用 Web 浏览器与正在运行的 Pod 进行交互,请记住使用kubectl proxykubectl port-forward命令,通常从另一个终端窗口,将访问从您的笔记本电脑路由到集群中的 Pod。

在许多情况下,您最好使用我们之前描述的带有-f选项的kubectl logs命令。主要区别在于,如果您已经启用了应用程序以对来自stdin的输入做出反应,并且使用了定义了stdin和 TTY 的命令运行它,那么您可以直接使用kubectl attach命令与其交互。

在容器中运行第二个进程

我经常发现在 Pod 中运行额外的命令比尝试附加到 Pod 更有用。您可以使用kubectl exec命令来实现这一点。

截至 Kubernetes 1.8,kubectl exec不支持我们用于日志或附加命令的部署/名称快捷方式,因此您需要指定要与之交互的特定 Pod 名称。如果您只想在 Pod 中打开交互式 shell,可以运行以下命令:

kubectl get pods
NAME                   READY STATUS  RESTARTS AGE
flask-1908233635-d6stj 1/1   Running 0        1m

使用运行中的 Pod 的名称,调用kubectl exec在其中打开交互式 shell:

kubectl exec flask-1908233635-d6stj -it -- /bin/sh # ps aux
PID USER TIME COMMAND
 1 root 0:00 python3 /opt/exampleapp/exampleapp.py
 12 root 0:00 /bin/sh
 17 root 0:00 ps aux

您还可以使用此功能调用内置于容器中的任何命令。例如,如果您有一个收集和导出诊断数据的脚本或进程,您可以调用该命令。或者,您可以使用killall -HUP python3这样的命令,它将向所有正在运行的python3进程发送HUP信号。

Kubernetes 概念-标签

在第一个示例中,您看到创建部署还创建了一个 ReplicaSet 和相关的 Pod,以便运行您的软件。

Kubernetes 具有非常灵活的机制,用于连接和引用其管理的对象。 Kubernetes 项目使用资源上的一组标签,称为标签,而不是具有非常严格的可以连接的层次结构。有一个匹配机制来查询和找到相关的标签,称为选择器。

标签在格式上相当严格定义,并且旨在将 Kubernetes 中的资源分组在一起。它们不打算标识单个或唯一的资源。它们可用于描述一组 Kubernetes 资源的相关信息,无论是 Pod、ReplicaSet、Deployment 等。

正如我们之前提到的,标签是键-值结构。标签中的键大小受限,并且可能包括一个可选的前缀,后跟一个/字符,然后是键的其余部分。如果提供了前缀,则预期使用 DNS 域。Kubernetes 的内部组件和插件预期使用前缀来分组和隔离它们的标签,前缀kubernetes.io保留用于 Kubernetes 内部标签。如果未定义前缀,则被认为完全由用户控制,并且你需要维护自己关于非前缀标签意义一致性的规则。

如果你想使用前缀,它需要少于 253 个字符。前缀之外的键的最大长度为 63 个字符。键也只能由字母数字字符、-_.指定。Unicode 和非字母数字字符不支持作为标签。

标签旨在表示关于资源的语义信息,拥有多个标签不仅可以接受,而且是预期的。你会看到标签在 Kubernetes 示例中被广泛使用,用于各种目的。最常见的是感兴趣的维度,例如:

  • 环境

  • 版本

  • 应用程序名称

  • 服务层级

它们也可以用来跟踪你感兴趣的任何基于你的组织或开发需求的分组。团队、责任领域或其他语义属性都是相当常见的。

标签的组织

当你拥有超过“只是一点点”的资源时,对资源进行分组对于维护对系统的理解至关重要,同时也让你能够根据其责任而不是个体名称或 ID 来思考资源。

你应该考虑创建并维护一个包含你使用的标签及其含义和意图的实时文档。我更喜欢在部署目录中的README.md中进行这项工作,我会在那里保存 Kubernetes 声明,我发现你设置的任何约定都对理解至关重要,特别是当你作为团队的一部分工作时。即使你是独自工作,这也是一个很好的实践:今天对你来说很明显的东西,也许在六个月甚至更长时间内对未来的你来说完全晦涩难懂。

您还有责任清楚地了解自己标签的含义。Kubernetes 不会阻止您混淆或重复使用简单的标签。我们将在本章后面讨论的一种资源称为服务,专门使用标签来协调对 Pods 的访问,因此保持清晰地使用这些标签非常重要。在不同的 Pods 之间重用标签键可能会导致非常意外的结果。

Kubernetes 概念-选择器

在 Kubernetes 中,选择器用于基于它们具有(或不具有)的标签将资源连接在一起。选择器旨在提供一种在 Kubernetes 中检索一组资源的方法。

大多数kubectl命令支持-l选项,允许您提供选择器以过滤其查找的内容。

选择器可以基于相等性表示特定值,也可以基于集合表示允许基于多个值进行过滤和选择。相等选择器使用=!=。集合选择器使用innotinexists。您可以将这些组合在一个选择器中,通过在它们之间添加,来创建更复杂的过滤器和选择条件。

例如,您可以使用标签app来表示提供特定应用程序服务的 Pods 分组-在这种情况下,使用值flasktier来表示front-endcacheback-end层的值。可能返回与该应用相关的所有资源的选择器可能是:

app=flask

并且刚刚返回支持此应用程序的前端资源的选择器:

app=flask,tier in (front-end)

如果您想列出所有与选择app=flask匹配的 Pods,您可以使用以下命令:

kubectl get pods -l app=flask

查看标签

我们之前通过kubectl run命令创建的部署放置了标签并将它们用作选择器。正如您之前看到的,您可以使用kubectl get -o json命令获取 Kubernetes 资源的所有底层详细信息。

类似的命令是kubectl describe,旨在提供资源及其最近历史的人类可读概述:

kubectl describe deployment flask

这将提供类似以下的输出:

Name: flask
Namespace: default
CreationTimestamp: Sat, 16 Sep 2017 08:31:00 -0700
Labels: pod-template-hash=866287979
 run=flask
Annotations: deployment.kubernetes.io/revision=1
kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"apps/v1beta1","kind":"Deployment","metadata":{"annotations":{},"labels":{"run":"flask"},"name":"flask","namespace":"default"},"spec":{"t...
Selector: app=flask
Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
 Labels: app=flask
 Containers:
 flask:
 Image: quay.io/kubernetes-for-developers/flask:latest
 Port: 5000/TCP
 Environment: <none>
 Mounts: <none>
 Volumes: <none>
Conditions:
 Type Status Reason
 ---- ------ ------
 Available True MinimumReplicasAvailable
 Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: flask-866287979 (1/1 replicas created)
Events:
 FirstSeen LastSeen Count From SubObjectPath Type Reason Message
 --------- -------- ----- ---- ------------- -------- ------ -------
 2d 2d 1 deployment-controller Normal ScalingReplicaSet Scaled up replica set flask-866287979 to 1

您会注意到其中有两个标签,runpod-template-hash,以及一个选择器,app=flask。例如,您可以使用kubectl get命令行查询这些确切的标签:

kubectl get deployment -l run=flask

这将返回匹配该选择器的部署:

NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
flask     1         1         1            1           2d

以及对 Pods 的等效请求选择器

kubectl get pods -l app=flask

这将返回匹配app=flask选择器的 Pods:

NAME                    READY     STATUS    RESTARTS   AGE
flask-866287979-bqg5w   1/1       Running   0          2d

在这个部署中,Pod 是使用选择器app=flask从部署中引用的。

注意:您可以与kubectl get一起使用选择器来一次请求多种资源。例如,如果您使用app=flask标记了所有相关资源,那么您可以使用诸如kubectl get deployment,pod -l app=flask的命令来查看部署和 Pod。

正如您所看到的,当您交互式地创建和运行资源时,通常会隐式使用一些常见的标签结构。kubectl run创建部署时,使用runpod-template-hashapp键具有特定的含义。

标签也可以在资源已经存在后,使用kubectl label命令进行交互式应用。例如,要为 Pod 应用一个名为 enabled 的标签,您可以使用以下命令:

kubectl label pods your-pod-name enable=true

这使您可以交互式地将资源分组在一起,或者提供一种一致的方式来推出、更新,甚至移除一组资源。

使用 kubectl 列出带有标签的资源

kubectl get命令默认会显示基本信息,通常是您要查找的资源的名称和状态。您可以扩展它显示的列,以包括特定的标签,这通常可以使在处理大量不同的 Pods、部署和 ReplicaSets 时更容易找到您要查找的内容。kubectl使用-L选项和逗号分隔的标签键列表作为标题显示。

如果您想显示 Pods 以及标签键runpod-template-hash,命令将是:

kubectl get pods -L run,pod-template-hash

然后您可能会看到以下输出:

NAME READY STATUS RESTARTS AGE RUN POD-TEMPLATE-HASH
flask-1908233635-d6stj 1/1 Running 1 20h flask 1908233635

自动标签和选择器

Kubernetes 包括许多命令,可以自动为您创建许多资源。当这些命令创建资源时,它们还会应用自己的约定标签,并使用这些标签将资源联系在一起。一个完美的例子就是我们现在已经使用了好几次的命令:kubectl run

例如,当我们使用:

kubectl run flask --image=quay.io/kubernetes-for-developers/flask:latest

这创建了一个名为flask的部署。当部署的控制器被创建时,这反过来导致了为该部署创建一个 ReplicaSet,而 ReplicaSet 控制器又创建了一个 Pod。我们之前看到这些资源的名称都是相关的,它们之间也有相关的标签。

部署flask是使用run=flask标签创建的,使用kubectl命令的名称作为键,并且我们在命令行上提供的名称作为值。部署还具有选择器run=flask,以便它可以将其控制器规则应用于为其创建的相关 ReplicaSets 和 Pods。

查看创建的 ReplicaSet,您将看到run=flask标签以及与使用pod-template-hash键为 ReplicaSet 创建的名称相对应的标签。这个 ReplicaSet 还包括相同的选择器来引用为其创建的 Pods。

最后,Pod 具有相同的选择器,这就是当需要时 ReplicaSet 和部署如何知道与 Kubernetes 中的哪些资源进行交互。

以下是总结了前面示例中自动创建的标签和选择器的表格:

部署 ReplicaSet Pod
名称 flask flask-1908233635 flask-1908233635-d6stj
标签 run=flask pod-template-hash=1908233635 run=flask pod-template-hash=1908233635 run=flask
选择器 run=flask pod-template-hash=1908233635,run=flask

Kubernetes 资源 - 服务

到目前为止,我们探讨的所有细节都与在 Kubernetes 中运行的单个容器相关。当一起运行多个容器时,利用 Kubernetes 的重大好处开始发挥作用。能够将一组做同样事情的 Pods 组合在一起,以便我们可以对它们进行扩展和访问,这就是 Kubernetes 资源服务的全部内容。

服务是 Kubernetes 资源,用于提供对 Pod(或 Pods)的抽象,不考虑正在运行的特定实例。在一个容器(或一组容器)提供的内容与另一层,比如数据库之间提供一个层,允许 Kubernetes 独立扩展它们,更新它们,处理扩展问题等。服务还可以包含数据传输的策略,因此您可以将其视为 Kubernetes 中的软件负载均衡器。

服务也是用于将 Pod 公开给彼此或将容器公开给 Kubernetes 集群外部的关键抽象。服务是 Kubernetes 管理 Pod 组之间以及进出它们的流量的核心。

服务的高级用法还允许您为集群之外的资源定义服务。这可以让您以一致的方式使用服务,无论您需要运行的端点是来自 Kubernetes 内部还是集群外部。

Kubernetes 包括一个expose命令,可以基于集群内已经运行的资源创建服务。例如,我们可以使用以下命令暴露我们之前使用的flask部署示例:

kubectl expose deploy flask --port 5000
service "flask" exposed

大多数服务将定义一个 ClusterIP,Kubernetes 将处理所有动态链接资源的工作,当匹配相关选择器的 Pod 被创建和销毁时。您可以将其视为 Kubernetes 内部的简单负载均衡器构造,并且它将在 Pod 可用时内部转发流量,并停止向失败或不可用的 Pod 发送流量。

如果您使用expose命令请求我们刚刚创建的服务的详细信息,您将看到列出了 ClusterIP:

kubectl get service flask

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
flask ClusterIP 10.0.0.168 <none> 5000/TCP 20h

定义服务资源

服务规范在版本 1.8 的文档中非常简单,可以在kubernetes.io/docs/api-reference/v1.8/#service-v1-core找到。Kubernetes 中的所有资源都可以以声明方式定义,我们将在第四章“声明式基础设施”中更深入地研究这一点。资源也可以使用 YAML 和 JSON 来定义。为了查看可以包含在服务资源中的细节,我们将查看其 YAML 规范。规范的核心包括名称、为提供服务的 Pod 选择器以及与服务相关的端口。

例如,我们的flask Pod 的简单服务声明可能是:

kind: Service
apiVersion: v1
metadata:
 name: service
spec:
 selector:
 run: flask
 ports:
 - protocol: TCP
 port: 80
    targetPort: 5000

这定义了一个服务,使用选择器run: flask选择要前置的 Pod,并在 TCP 端口80上接受任何请求,并将其转发到所选 Pod 的端口5000。服务支持 TCP 和 UDP。默认是 TCP,所以我们严格来说不需要包含它。此外,targetPort 可以是一个字符串,指的是端口的名称,而不仅仅是端口号,这允许服务之间具有更大的灵活性,并且可以根据开发团队的需求移动特定的后端端口,而无需进行太多的仔细协调以保持整个系统的运行。

服务可以定义(和重定向)多个端口 - 例如,如果您想要支持端口80443的访问,可以在服务上定义它。

端点

服务不需要选择器,没有选择器的服务是 Kubernetes 用来表示集群外部服务的方式。为了实现这一点,您创建一个没有选择器的服务,以及一个新的资源,即端点,它定义了远程服务的网络位置。

如果您正在将服务迁移到 Kubernetes,并且其中一些服务是集群外部的,这提供了一种将远程系统表示为内部服务的方式,如果以后将其移入 Kubernetes,则无需更改内部 Pod 连接或利用该资源的方式。这是服务的高级功能,也不考虑授权。另一个选择是不将外部服务表示为服务资源,而是在 Secrets 中简单地引用它们,这是我们将在下一章中更深入地研究的功能。

例如,如果您在互联网上以 IP 地址1.2.3.4的端口1976上运行远程 TCP 服务,则可以定义一个服务和端点来引用Kubernetes 外部系统:

kind: Service
apiVersion: v1
metadata:
 name: some-remote-service
spec:
 ports:
 - protocol: TCP
 port: 1976
 targetPort: 1976

这将与以下Endpoints定义一起工作:

kind: Endpoints
apiVersion: v1
metadata:
 name: some-remote-service
subsets:
 - addresses:
 - ip: 1.2.3.4
 ports:
 - port: 1976

服务类型 - ExternalName

前面的 Endpoint 定义有一个变体,它只提供 DNS 引用,称为ExternalName服务。像Endpoint定向服务一样,它不包括选择器,但也不包括任何端口引用。相反,它只定义了一个外部 DNS 条目,可以用作服务定义。

以下示例为 Kubernetes 内部提供了一个服务接口,用于外部 DNS 条目my.rest.api.example.com

kind: Service
apiVersion: v1
metadata:
 name: another-remote-service
 namespace: default
spec:
 type: ExternalName
 externalName: my.rest.api.example.com

与其他服务不同,其他服务提供 TCP 和 UDP(ISO 网络堆栈上的第 4 层)转发,ExternalName只提供 DNS 响应,不管理任何端口转发或重定向。

无头服务

如果有理由要明确控制您连接和通信的特定 Pod,可以创建一个不分配 IP 地址或转发流量的服务分组。这种服务称为无头服务。您可以通过在服务定义中明确设置 ClusterIP 为None来请求此设置:

例如,无头服务可能是:

kind: Service
apiVersion: v1
metadata:
 name: flask-service
spec:
 ClusterIP: None
 selector:
 app: flask

对于这些服务,将创建指向支持服务的 Pod 的 DNS 条目,并且该 DNS 将在与选择器匹配的 Pod 上线(或消失)时自动更新。

注意:要注意 DNS 缓存可能会妨碍无头服务的使用。在建立连接之前,您应该始终检查 DNS 记录。

从 Pod 内部发现服务

有两种方式可以从 Pod 内部看到服务。第一种是通过环境变量添加到与服务相同命名空间中的所有 Pod。

当您添加服务(使用kubectl createkubectl apply)时,该服务将在 Kubernetes 中注册,此后启动的任何 Pod 都将设置引用该服务的环境变量。例如,如果我们创建了前面的第一个示例服务,然后运行:

kubectl get services

我们会看到列出的服务:

NAME            CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
flask           10.0.0.61    <none>        80/TCP    2d
kubernetes      10.0.0.1     <none>        443/TCP   5d

如果您查看容器内部,您会看到与先前列出的两个服务相关联的环境变量。这些环境变量是:

env
KUBERNETES_PORT=tcp://10.0.0.1:443
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.0.0.1
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP=tcp://10.0.0.1:443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_HOST=10.0.0.1
FLASK_SERVICE_PORT_80_TCP_ADDR=10.0.0.61
FLASK_SERVICE_PORT_80_TCP_PORT=80
FLASK_SERVICE_PORT_80_TCP_PROTO=tcp
FLASK_SERVICE_PORT_80_TCP=tcp://10.0.0.61:80
FLASK_SERVICE_SERVICE_HOST=10.0.0.61
FLASK_SERVICE_SERVICE_PORT=80
FLASK_SERVICE_PORT=tcp://10.0.0.61:80

(前面的输出已经重新排序,以便更容易看到值,并删除了一些多余的环境变量。)

对于每个服务,都定义了环境变量,提供了 IP 地址、端口和协议,还有一些名称变体。请注意,这个 IP 地址不是任何底层 Pod 的 IP 地址,而是 Kubernetes 集群中的 IP 地址,服务将其作为访问所选 Pod 的单个端点进行管理。

警告:服务的顺序很重要!如果 Pod 在定义服务之前存在,那么该服务的环境变量将不会存在于这些 Pod 中。重新启动 Pods,或将其缩减到0然后再次启动(强制容器被杀死和重新创建)将解决此问题,但通常最好始终首先定义和应用您的服务声明。

服务的 DNS

最初并不是核心分发的一部分,现在在 1.3 版(以及更高版本)的所有集群中都包含了一个集群附加组件,为 Kubernetes 提供了内部 DNS 服务。例如,Minikube 包括了这个附加组件,并且很可能已经在您的集群中运行。

将创建一个 DNS 条目,并与定义的每个服务协调,以便您可以请求<service><service>.<namespace>的 DNS 条目,并且内部 DNS 服务将为您提供正确的内部 IP 地址。

例如,如果我们使用expose命令公开flask部署,该服务将在我们的容器中列出 DNS。我们可以打开一个交互式终端到现有的 Pod,并检查 DNS:

kubectl exec flask-1908233635-d6stj -it -- /bin/sh
/ # nslookup flask
nslookup: can't resolve '(null)': Name does not resolve
Name: flask
Address 1: 10.0.0.168 flask.default.svc.cluster.local

每个服务在 DNS 中都会获得一个内部 A 记录(地址记录)<servicename>.<namespace>.svc.cluster.local,作为快捷方式,它们通常可以在 Pods 中被引用为<servicename>.<namespace>.svc,或者更简单地为所有在同一命名空间中的 Pods 的<servicename>

注意:只有在您明确尝试引用另一个命名空间中的服务时,才应该添加命名空间。不带命名空间会使您的清单本质上更具重用性,因为您可以将整个服务堆栈与静态路由配置放入任意命名空间。

在集群外部公开服务

到目前为止,我们讨论的一切都是关于在 Kubernetes 集群内部表示服务。服务概念也是将应用程序暴露在集群外部的方式。

默认服务类型是 ClusterIP,我们简要介绍了类型ExternalName,它是在 Kubernetes 1.7 中添加的,用于提供外部 DNS 引用。还有另外两种非常常见的类型,NodePortLoadBalancer,它们专门用于在 Kubernetes 集群之外公开服务。

服务类型 - LoadBalancer

LoadBalancer服务类型在所有 Kubernetes 集群中都不受支持。它最常用于云提供商,如亚马逊、谷歌或微软,并与云提供商的基础设施协调,以设置一个外部LoadBalancer,将流量转发到服务中。

定义这些服务的方式是特定于您的云提供商的,并且在 AWS、Azure 和 Google 之间略有不同。 LoadBalancer服务定义还可能包括推荐的注释,以帮助定义如何处理和处理 SSL 流量。有关每个提供程序的具体信息,可以在 Kubernetes 文档中找到。有关 LoadBalancer 定义的文档可在kubernetes.io/docs/concepts/services-networking/service/#type-loadbalancer上找到。

服务类型 - NodePort

当您在本地使用 Kubernetes 集群,或者在我们的情况下,在 Minikube 上的虚拟机上使用开发机器时,NodePort 是一种常用的服务类型,用于暴露您的服务。NodePort 依赖于运行 Kubernetes 的基础主机在您的本地网络上可访问,并通过所有 Kubernetes 集群节点上的高端口公开服务定义。

这些服务与默认的 ClusterIP 服务完全相同,唯一的区别是它们的类型是NodePort。如果我们想要使用expose命令创建这样一个服务,我们可以在之前的命令中添加一个--type=Nodeport选项,例如:

kubectl delete service flask
kubectl expose deploy flask --port 5000 --type=NodePort

这将导致一个定义看起来像以下的东西:

kubectl get service flask -o yaml

apiVersion: v1
kind: Service
metadata:
 creationTimestamp: 2017-10-14T18:19:07Z
 labels:
 run: flask
 name: flask
 namespace: default
 resourceVersion: "19788"
 selfLink: /api/v1/namespaces/default/services/flask
 uid: 2afdd3aa-b10c-11e7-b586-080027768e7d
spec:
 clusterIP: 10.0.0.39
 externalTrafficPolicy: Cluster
 ports:
 - nodePort: 31501
 port: 5000
 protocol: TCP
 targetPort: 5000
 selector:
 run: flask
 sessionAffinity: None
 type: NodePort
status:
 loadBalancer: {}

注意nodePort: 31501。这是服务暴露的端口。启用了这个选项后,以前我们必须使用端口转发或代理来访问我们的服务,现在可以直接通过服务来做。

Minikube 服务

Minikube 有一个服务命令,可以非常容易地获取和访问这个服务。虽然您可以使用minikube ip获取您的minikube主机的 IP 地址,并将其与先前的端口组合在一起,但您也可以使用minikube service命令在一个命令中创建一个组合的 URL:

minikube service flask --url

这应该返回一个像这样的值:

http://192.168.64.100:31505

而且minikube有一个有用的选项,如果你使用以下命令,可以打开一个浏览器窗口:

minikube service flask
Opening kubernetes service default/flask in default browser...

如果您启用了一个服务,但没有 Pod 支持该服务,那么您将看到一个连接被拒绝的消息。

您可以使用以下命令列出从您的minikube实例暴露的所有服务:

minikube service list

然后您将看到类似以下的输出:

|-------------|----------------------|-----------------------------|
| NAMESPACE   |       NAME           |           URL               |
|-------------|----------------------|-----------------------------|
| default     | flask                | http://192.168.99.100:31501 |
| default     | kubernetes           | No node port                |
| kube-system | kube-dns             | No node port                |
| kube-system | kubernetes-dashboard | http://192.168.99.100:30000 |
|-------------|----------------------|-----------------------------|

示例服务 - Redis

我们将在 Kubernetes 中创建一个示例服务,向您展示如何连接服务,并使用它们来设计您的代码。Redis(redis.io)是一个超级灵活的数据存储,您可能已经很熟悉了,它很容易从 Python 和 Node.js 中使用。

Redis 已经作为一个容器可用,并且很容易在 Docker Hub(hub.docker.com/)上找到作为一个容器镜像。有几个选项可用,相关标签在 Docker Hub 网页上列出:

我们可以使用kubectl run命令使用这个镜像创建一个部署,然后使用kubectl expose命令创建一个服务来映射到部署中的 Pod:

kubectl run redis --image=docker.io/redis:alpine

我们将创建一个名为redis的部署,并通过该部署下载镜像并开始运行它。我们可以看到 Pod 正在运行:

kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
flask-1908233635-d6stj   1/1       Running   1          1d
redis-438047616-3c9kt    1/1       Running   0          21s

您可以使用kubectl exec命令在此 Pod 中运行交互式 shell,并直接查询运行中的redis实例:

kubectl exec -it redis-438047616-3c9kt -- /bin/sh
/data # ps aux
PID   USER     TIME   COMMAND
    1 redis      0:22 redis-server
   24 root       0:00 /bin/sh
   32 root       0:00 ps aux
/data # which redis-server
/usr/local/bin/redis-server
/data # /usr/local/bin/redis-server --version
Redis server v=4.0.2 sha=00000000:0 malloc=jemalloc-4.0.3 bits=64
build=7f502971648182f2
/data # exit

我们可以使用NodePort在我们的集群实例内部和minikube外部暴露这个服务。redis的默认端口是6379,所以我们需要确保在我们的服务中包含这个端口:

kubectl expose deploy redis --port=6379 --type=NodePort
service "redis" exposed

如果我们列出可用的服务:

kubectl get services
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE
flask        NodePort    10.0.0.39    <none>        5000:31501/TCP   3h
kubernetes   ClusterIP   10.0.0.1     <none>        443/TCP          1d
redis        NodePort    10.0.0.119   <none>        6379:30336/TCP   15s

我们将看到redis在端口30336上使用NodePort暴露。minikube service命令在这里不会立即有帮助,因为 redis 不是基于 HTTP 的 API,但是使用minikube ip,我们可以组合一个命令来通过其命令行界面与redis交互:

minikube ip
**192.168.99.100** 

要与redis交互,我们可以使用redis-cli命令行工具。如果您没有这个工具,您可以从redis.io/download下载并按照本例进行操作:

redis-cli -h 192.168.99.100 -p 30336
192.168.99.100:30336>
192.168.99.100:30336> ping
PONG  

查找 Redis 服务

有了 Redis 服务正在运行,我们现在可以从我们自己的 Pod 中使用它。正如我们之前提到的,有两种方法可以定位服务:基于服务名称的环境变量将设置为主机 IP 和端口,或者您可以使用基于服务名称的 DNS 条目。

环境变量只会在服务之后创建的 Pod 上设置。如果您仍然像我们之前的示例那样运行flask Pod,那么它将不会显示环境变量。如果我们创建一个新的 Pod,甚至是一个临时的 Pod,那么它将包括环境变量中的服务。这是因为环境变量是根据 Pod 创建时的 Kubernetes 状态设置的,并且在 Pod 的生命周期内不会更新。

然而,DNS 会根据集群的状态动态更新。虽然不是即时的,但这意味着在服务创建后,DNS 请求将开始返回预期的结果。而且因为 DNS 条目是根据命名空间和服务名称可预测的,它们可以很容易地包含在配置数据中。

注意:使用 DNS 进行服务发现,而不是环境变量,因为 DNS 会随着您的环境更新,但环境变量不会。

如果您仍在运行 Flask 或 Node.js Pod,请获取 Pod 名称并在其中打开一个 shell:

kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
flask-1908233635-d6stj   1/1       Running   1          2d
redis-438047616-3c9kt    1/1       Running   0          1d
kubectl exec flask-1908233635-d6stj -it -- sh 

然后,我们可以查找我们刚刚在默认命名空间中创建的 Redis 服务,它应该被列为redis.default

/ # nslookup redis.default
nslookup: can't resolve '(null)': Name does not resolve
Name:      redis.default
Address 1: 10.0.0.119 redis.default.svc.cluster.local

从 Python 中使用 Redis

一旦我们可以访问我们的 Python Pod,我们可以调用 Python 进行交互并访问 Redis。请记住,当我们创建这个 Pod 时,我们没有包含任何用于 Redis 的 Python 库。在这个示例中,我们可以即时安装它们,但这种更改只对这个单独的 Pod 有效,并且只在这个 Pod 的生命周期内有效。如果 Pod 死掉,任何更改(比如添加 Redis 库)都将丢失。

这是一个很好的工具,可以交互式地动态尝试各种操作,但请记住,您需要将任何所需的更改合并到创建容器的过程中。

flask Pod 中,转到我们设置的代码目录,我们可以使用 PIP 添加 Redis 库:

# cd /opt/exampleapp/
/opt/exampleapp # pip3 install redis
Collecting redis
  Downloading redis-2.10.6-py2.py3-none-any.whl (64kB)
    100% |████████████████████████████████| 71kB 803kB/s
Installing collected packages: redis
Successfully installed redis-2.10.6

现在,我们可以交互式地尝试从 Python 中使用 Redis:

/opt/exampleapp # python3
Python 3.6.1 (default, May  2 2017, 15:16:41)
[GCC 6.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import redis
>>> redis_db = redis.StrictRedis(host="redis.default", port=6379, db=0)
>>> redis_db.ping()
True
>>> redis_db.set("hello", "world")
True
>>> redis_db.get("hello")
b'world'

为了匹配这一点并为我们的 Python 代码启用这个库,我们需要将它添加到 Docker 构建过程中使用的requirements.txt文件中,以安装所有依赖项。然后我们需要重新构建容器并将其推送到注册表,然后重新创建 Pods,以便使用新的镜像。

更新 Flask 部署

此更新过程的步骤如下:

  • 在源代码控制中更新代码或依赖项

  • 构建并标记一个新的 Docker 镜像

  • 将 Docker 镜像推送到容器存储库

  • 更新 Kubernetes 中的部署资源以使用这个新镜像

通过逐步进行这个示例,可以突出显示您可以开始推出代码更新,直接或通过添加其他服务到您的应用程序。

在这个示例中,我们不会立即更改任何代码,我们只是想包含 Redis Python 库,以便它可用。为了做到这一点,我们通常会使用 PIP 来安装我们想要的库。通过我们的 Python 示例,我们通过依赖项列表requirements.txt使用 PIP 安装所有所需的库,这在 Docker 构建过程中被调用:

  • 更新requirements.txt文件以包括 Redis:
Flask==0.12.2
redis

不指定特定版本是向 PIP 表明您希望它找到最新版本并安装它。如果您已经知道 redis 库的版本,或者想要明确地固定它,您可以添加它,比如 ==2.10.6(类似于之前添加的 Flask)。

  • 重新构建 docker 镜像:
docker build .
Sending build context to Docker daemon  162.8kB
Step 1/9 : FROM alpine
…
Removing intermediate container d3ee8e22a095
Successfully built 63635b37136a

在这个例子中,我明确地重新构建了一个没有标签的镜像,打算在第二步中添加标签:

  • build 打标签

要给一个 build 打标签,使用以下命令:

docker tag <image_id> <container_repository>/<group_name>/<container_name>:<tag>

我在这个例子中使用的命令是:

docker tag 63635b37136a quay.io/kubernetes-for-developers/flask:0.1.1 

一旦构建的镜像有您想要关联的标签(在本例中,我们使用了 0.1.1),您可以为其添加多个值的标签,以便以不同的方式引用该镜像。一旦标记完成,您需要使这些镜像可用于您的集群。

  • 推送容器镜像:
docker push quay.io/kubernetes-for-developers/flask:0.1.1
The push refers to a repository [quay.io/kubernetes-for-developers/flask]
34f306a8fb12: Pushed
801c9c3c42e7: Pushed
e94771c57351: Pushed
9c99a7f27402: Pushed
993056b64287: Pushed
439786010e37: Pushed
5bef08742407: Layer already exists
0.1.1: digest: sha256:dc734fc37d927c6074b32de73cd19eb2a279c3932a06235d0a91eb66153110ff size: 5824

容器标签不需要以点版本格式。在这种情况下,我选择了一个简单和有上下文的标签,但也是明确的,而不是重复使用 latest,这可能会导致对我们正在运行的 latest 产生一些混淆。

注意:使用有意义的标签,并且在运行 Kubernetes 时避免使用 latest 作为标签。如果一开始就使用明确的标签,您将节省大量时间来调试确切运行的版本。甚至像 Git 哈希或非常明确的时间戳都可以用作标签。

现在,我们可以更新部署,指示 Kubernetes 使用我们创建的新镜像。Kubernetes 支持几个命令来实现我们想要的效果,比如 kubectl replace,它将采用 YAML 或 JSON 格式的更改规范,您可以更改任何值。还有一个较旧的命令 kubectl rolling-update,但它只适用于复制控制器。

注意:复制控制器是 ReplicaSet 的早期版本,并已被 ReplicaSets 和 Deployments 所取代。

kubectl rolling-update 命令已被 kubectl setkubectl rollout 命令的组合所取代,这适用于部署以及一些其他资源。kubectl set 命令有助于频繁更新一些常见更改,比如更改部署中的镜像、在部署中定义的环境变量等等。

kubectl apply命令类似于kubectl replace,它接受一个文件(或一组文件)并动态应用到所有引用的 kubernetes 资源的差异。我们将在下一章更深入地研究使用kubectl apply命令,我们还将研究如何在声明文件中维护应用程序及其结构的定义,而不是依赖于交互式命令的顺序和调用。

正如你所看到的,有很多选择可以进行; 所有这些都归结为更改在 Kubernetes 中定义的资源,以便让它执行某些操作。

让我们选择最一般的选项,并使用kubectl replace命令,逐步进行过程,以清楚地说明我们正在改变什么。

首先,获取我们正在更改的部署:

kubectl get deploy flask -o yaml --export > flask_deployment.yaml

现在,在文本编辑器中打开flask_deployment.yaml文件,并找到指定图像的行。当前图像版本可以在文件中的template -> spec -> containers下找到,并应该读取类似以下内容:

- image: quay.io/kubernetes-for-developers/flask:latest

编辑文件并更改为引用我们更新的标签:

- image: quay.io/kubernetes-for-developers/flask:0.1.1

现在,我们可以使用kubectl replace命令告诉 Kubernetes 更新它:

kubectl replace -f flask_deployment.yaml
deployment "flask" replaced

此更改将启动与部署相关的资源的更新,这种情况下进行滚动更新或部署。部署控制器将自动为部署创建新的 ReplicaSet 和 Pod,并在可用后终止旧的。在此过程中,该过程还将维护规模或运行的副本数量,并可能需要一些时间。

注意:你还应该知道kubectl edit命令,它允许你指定一个资源,比如一个部署/flask,并直接编辑它的 YAML 声明。当你保存用kubectl edit打开的编辑器窗口时,它会执行之前kubectl replace命令的操作。

你可以使用kubectl get pods来查看这个过程:

kubectl get pods
NAME                     READY     STATUS              RESTARTS   AGE
flask-1082750864-nf99q   0/1       ContainerCreating   0          27s
flask-1908233635-d6stj   1/1       Terminating         1          2d
redis-438047616-3c9kt    1/1       Running             0          1d

由于只有一个具有单个容器的 Pod,完成时间不会太长,完成后你会看到类似以下的内容:

kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
flask-1082750864-nf99q   1/1       Running   0          1m
redis-438047616-3c9kt    1/1       Running   0          1d

你可能会注意到副本集哈希已经改变,以及 Pod 的唯一标识符。如果我们现在使用交互式会话访问此 Pod,我们可以看到库已加载。这一次,我们将直接使用 Python 的交互式 REPL:

kubectl exec flask-1082750864-nf99q -it -- python3
Python 3.6.1 (default, Oct  2 2017, 20:46:59)
[GCC 6.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import redis
>>> redis_db = redis.StrictRedis(host="redis.default", port=6379, db=0)
>>> redis_db.ping()
True
>>> redis_db.get('hello')
b'world'
>>> exit()

部署和滚动

更改部署中的镜像会启动一个部署。部署的滚动更新是一个异步过程,需要时间来完成,并由部署中定义的值控制。如果您查看我们转储到 YAML 并更新的资源文件,您将看到我们使用kubectl run命令创建部署时创建的默认值。

spec -> strategy下,您将看到如何处理更新的默认规范:

 strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1 **type: RollingUpdate** 

截至 Kubernetes 1.8,有两种可用的策略:RecreateRollingUpdateRollingUpdate是默认值,旨在用于在进行代码更新时保持服务可用性的主要用例。Recreate 的操作方式不同:在创建新的具有更新版本的 pod 之前,杀死所有现有的 pod,这可能会导致短暂的中断。

RollingUpdate由两个值控制:maxUnavailablemaxSurge,它们提供了一些控制,以便在更新进行时您可以拥有最少数量的可用 pod 来处理您的服务。您可以在kubernetes.io/docs/concepts/workloads/controllers/deployment/的文档中找到有关这两个控制选项的详细信息,以及一些其他影响部署过程的选项。

部署历史

Kubernetes 还维护着一个历史记录(其长度也可以受到控制)用于部署。您可以通过kubectl rollout命令查看部署的状态以及其历史记录。

例如,要查看我们刚刚执行的部署状态:

kubectl rollout status deployment/flask
deployment "flask" successfully rolled out

您可以使用以下命令查看部署更改的历史记录:

kubectl rollout history deployment/flask
deployments "flask"
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

change-cause作为部署资源的注释进行跟踪,(截至 Kubernetes 1.8)由于我们使用默认的kubectl run命令创建了部署,因此它不存在。有一个--record=true选项,可以与kubectl runkubectl set和其他一些明确设置这些注释的命令一起使用。我们将在下一章节中详细讨论注释。

我们可以继续创建一个注释,以匹配我们刚刚执行的操作,使用以下命令:

kubectl annotate deployment flask kubernetes.io/change-cause='deploying image 0.1.1'
deployment "flask" annotated

现在,如果我们查看历史记录,您将看到以下内容显示:

kubectl rollout history deployment/flask
deployments "flask"
REVISION  CHANGE-CAUSE
1         <none>
2         deploying image 0.1.1

您可以使用history命令的--revision选项获取更详细的信息。例如:

kubectl rollout history deployment flask --revision=2

这将返回类似以下内容:

deployments "flask" with revision #2
Pod Template:
  Labels:  pod-template-hash=1082750864
  run=flask
  Annotations:  kubernetes.io/change-cause=deploying image 0.1.1
  Containers:
   flask:
    Image:  quay.io/kubernetes-for-developers/flask:0.1.1
    Port:  <none>
   Environment:  <none>
    Mounts:  <none>
  Volumes:  <none>

您可以看到我们刚刚创建的注释,以及我们更改的容器镜像版本。

回滚

部署资源包括回滚到先前版本的能力。最简单的形式是kubectl rollout undo命令。如果您想要回滚到先前镜像运行的 Pods,您可以使用以下命令:

kubectl rollout undo deployment/flask

这将逆转该过程,执行相同的步骤,只是回到先前的部署资源配置。

如果您有多个版本,您可以使用--revision选项回滚到特定版本。您还可以使用rollout status命令和-w选项观察过程更新。例如,如果您刚刚调用了undo命令,您可以使用以下命令观察进度:

kubectl rollout status deployment/flask -w
Waiting for rollout to finish: 0 of 1 updated replicas are available...
deployment "flask" successfully rolled out

即使您撤消或回滚到先前版本,部署历史记录仍会不断向前滚动版本号。如果您熟悉使用 Git 进行源代码控制,这与使用git revert命令非常相似。如果您在撤消后查看历史记录,您可能会看到以下内容:

kubectl rollout history deployment/flask
deployments "flask"
REVISION  CHANGE-CAUSE
2         <none>
3         <none>

使用 kubectl set 命令进行更新

更新容器镜像是一个非常常见的任务。您也可以直接使用kubectl set命令更新该值,就像我们之前提到的那样。如果部署资源已添加change-cause注释,那么使用kubectl set命令将在您进行更改时更新该注释。例如:

# delete the deployment entirely
kubectl delete deployment flask
deployment "flask" deleted
# create a new deployment with the run command
kubectl run flask --image=quay.io/kubernetes-for-developers/flask:latest
deployment "flask" created
# add the initial annotation for change-cause
kubectl annotate deployment/flask kubernetes.io/change-cause='initial deployment'
deployment "flask" annotated
# update the container version with kubectl set
kubectl set image deployment/flask flask=quay.io/kubernetes-for-developers/flask:0.1.1
deployment "flask" image updated

如果您现在查看历史记录,它将包括使用set命令进行的更改:

kubectl rollout history deployment/flask
deployments "flask"
REVISION  CHANGE-CAUSE
1         initial deployment
2         kubectl set image deployment/flask flask=quay.io/kubernetes-for-developers/flask:0.1.1

当您使用代码创建服务和部署时,您可能会发现使用这些命令快速创建部署并更新它们非常方便。

注意:避免在引用容器镜像时使用latest标签的另一个原因:更新部署需要更改部署规范。如果您只是在部署后面更新镜像,部署将永远不知道何时更新它。

到目前为止,我们描述的升级都是幂等的,并且期望您可以无缝地向前或向后更改容器。这期望您创建和部署的容器镜像是无状态的,并且不必管理现有的持久数据。这并不总是如此,Kubernetes 正在积极添加对处理这些更复杂需求的支持,这个功能称为 StatefulSets,我们将在未来的章节中进一步讨论。

总结

在本章中,我们首先回顾了一些关于如何开发代码以在容器中运行的实用注意事项。我们讨论了从程序中获取日志的选项,以及在代码运行时访问 Pods 的一些技术。然后,我们回顾了 Kubernetes 的标签和选择器的概念,展示了它们在我们迄今为止使用的命令中的用法,然后看了一下 Kubernetes 服务概念,以公开一组 Pods(例如在部署中)给彼此,或者对 Kubernetes 集群外部。最后,我们结束了本章,看了一下部署的推出,以及您如何推出更改,以及查看这些更改的历史记录。

第四章:声明式基础设施

Kubernetes 本质上是一个声明性系统。在之前的章节中,我们已经使用诸如kubectl runkubectl expose之类的命令探讨了 Kubernetes 及其一些关键概念。这些命令都是命令式的:现在就做这件事。Kubernetes 通过将这些资源作为对象本身来管理这些资源。kubectl和 API 服务器将这些请求转换为资源表示,然后存储它们,各种控制器的工作是了解当前状态并按照请求进行操作。

我们可以直接利用声明性结构-所有服务、Pod 和更多内容都可以用 JSON 或 YAML 文件表示。在本章中,我们将转而将您的应用程序定义为声明性基础设施。我们将把现有的简单 Kubernetes Pod 放入您可以与代码一起管理的声明中;存储在源代码控制中并部署以运行您的软件。我们还将介绍 ConfigMaps 和 Secrets,以允许您定义配置以及应用程序结构,并探讨如何使用它们。

本章的部分包括:

  • 命令式与声明式

  • 声明您的第一个应用程序

  • Kubernetes 资源-注释

  • Kubernetes 资源-ConfigMap

  • Kubernetes 资源-秘密

  • 使用 ConfigMap 的 Python 示例

命令式与声明式命令

到目前为止,我们的示例主要集中在快速和命令式的命令,例如kubectl run来创建一个部署,然后运行我们的软件。这对于一些快速操作很方便,但不容易暴露 API 的全部灵活性。要利用 Kubernetes 提供的所有选项,通常更有效的方法是管理描述您想要的部署的文件。

在使用这些文件时,您可以使用kubectl createkubectl deletekubectl replace命令,以及-f选项来指定要使用的文件。命令式命令对于简单的设置很容易有效,但您很快就需要一系列重复使用的命令,以充分利用所有功能。您可能会将这些命令集存储在一个备忘单中,但这可能会变得繁琐,而且并不总是清晰明了。

Kubernetes 还提供了一种声明性机制,利用kubectl apply命令,该命令接受文件,审查当前状态,并根据需要管理更新-创建、删除等,同时保持更改的简单审计日志。

我建议对于任何比运行单个进程更复杂的事情使用kubectl apply命令,这可能是您开发的大多数服务。您在开发中可能不需要审计跟踪。但在暂存/金丝雀环境或生产环境中可能需要,因此熟悉并熟悉它们对于理解它们是有利的。

最重要的是,通过将应用程序的描述放在文件中,您可以将它们包含在源代码控制中,将它们视为代码。这为您提供了一种一致的方式来在团队成员之间共享该应用程序结构,所有这些成员都可以使用它来提供一致的环境。

kubectl apply命令有一个-f选项,用于指定文件或文件目录,以及一个-R选项,如果您正在建立一个复杂的部署,它将递归地遍历目录。

随着我们在本书中的进展,我将使用 YAML 格式(带有注释)的声明性命令和配置来描述和操作 Kubernetes 资源。如果您有强烈的偏好,也可以使用 JSON。

注意:如果您想要一个命令行工具来解析 YAML,那么有一个等价于jq用于 JSON 的工具:yq。我们的示例不会详细介绍,但如果您想使用该工具,可以在yq.readthedocs.io找到更多信息。

一堵墙的 YAML

这些配置看起来是什么样子的?其中绝大多数是以 YAML 格式进行管理,选项和配置可能看起来令人不知所措。Kubernetes 中的每个资源都有自己的格式,其中一些格式正在发生变化并处于积极开发中。您会注意到一些 API 和对象结构将积极引用alphabeta,以指示项目中这些资源的成熟状态。该项目倾向于以非常保守的方式使用这些术语:

  • alpha倾向于意味着这是一个早期实验,数据格式可能会发生变化,但很可能会存在实现最终目标的东西

  • beta比纯粹的实验更加可靠,很可能可以用于生产负载,尽管特定的资源格式尚未完全确定,并且可能会在 Kubernetes 发布过程中略微更改

请注意,随着 Kubernetes 的新版本发布,alpha 和 beta API 会不断发展。如果您使用较早版本,它可能会变得不推荐使用,并最终不可用。您需要跟踪这些更新与您正在使用的 Kubernetes 版本。

资源的正式文档、选项和格式托管在kubernetes.io的参考文档下。在我写这篇文章时,当前发布的版本是 1.8,该版本的参考文档可在kubernetes.io/docs/api-reference/v1.8/上找到。该文档是从 Kubernetes 项目源代码生成的,并在每次发布时更新,通常每三个月左右发布一次。

除了浏览参考文档之外,您还可以从现有的 Kubernetes 对象中获取声明。当您使用kubectl get命令请求 Kubernetes 资源时,可以添加-o yaml --export选项。

如果您更喜欢该格式,-o yaml选项可以改为-o json--export将剥离一些与 Kubernetes 内部资源的当前状态和身份相关的多余信息,并且不会对您在外部存储时有所帮助。

尽管在 1.8 版本中,这种能力还不完全,但您应该能够要求在一个命名空间中的所有资源,存储这些配置,并使用这些文件来精确地复制它。在实践中,会有一些小问题,因为导出的版本并不总是完全符合您的要求。在这一点上,更好的做法是管理自己的声明文件。

最后,我建议使用 YAML 作为这些声明的格式。您可以使用 JSON,但 YAML 允许您在声明中添加注释,这对于其他人阅读这些文件非常有用——这是 JSON 格式所没有的功能。

创建一个简单的部署

让我们首先看看kubectl run为我们创建了什么,然后从那里开始。我们使用以下命令创建了之前的简单部署:

kubectl run flask --image=quay.io/kubernetes-for-developers/flask:0.1.1 --port=5000

在示例中,我们使用kubectl get deployment flask -o json命令转储了声明的状态。让我们重复一下,只是使用-o yaml --export选项:

kubectl get deployment flask -o yaml --export

输出应该看起来像下面这样:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
 annotations:
 deployment.kubernetes.io/revision: "1"
 creationTimestamp: null
 generation: 1
 labels:
 run: flask
 name: flask
 selfLink: /apis/extensions/v1beta1/namespaces/default/deployments/flask
spec:
 replicas: 1
 selector:
 matchLabels:
 run: flask
 strategy:
 rollingUpdate:
 maxSurge: 1
 maxUnavailable: 1
 type: RollingUpdate
 template:
 metadata:
 creationTimestamp: null
 labels:
 run: flask
 spec:
 containers:
 - image: quay.io/kubernetes-for-developers/flask:latest
 imagePullPolicy: Always
 name: flask
 ports:
 - containerPort: 5000
 protocol: TCP
 resources: {}
 terminationMessagePath: /dev/termination-log
 terminationMessagePolicy: File
 dnsPolicy: ClusterFirst
 restartPolicy: Always
 schedulerName: default-scheduler
      securityContext: {}
 terminationGracePeriodSeconds: 30
status: {}

任何 Kubernetes 资源的一般格式都将具有相同的顶部四个对象:

  • apiVersion

  • kind

  • metadata

  • spec

如果您从 Kubernetes 检索信息,您将看到第五个键:status。状态不需要由用户定义,并且在检索对象以共享其当前状态时由 Kubernetes 提供。如果您在kubectl get命令上错过了--export选项,它将包括状态。

您会看到对象中散布着元数据,因为这些对象彼此相关,并在概念上相互构建。尽管它们可能被合并(如前面所示)成单个引用,但元数据被包含在每个资源中。对于我们创建的部署,它使用了部署的声明性引用,它包装了一个 ReplicaSet,该 ReplicaSet 又包装了一个 Pod。

您可以在以下 URL 中查看每个的正式定义:

您可能会注意到 ReplicaSet 和 Deployment 几乎是相同的。部署扩展了 ReplicaSet,并且每个部署实例都至少有一个 ReplicaSet。部署包括声明性选项(以及责任),用于如何在运行软件上执行更新。Kubernetes 建议在部署代码时,使用部署而不是直接使用 ReplicaSet,以便精确指定您希望它在更新时如何反应。

在部署speckubernetes.io/docs/api-reference/v1.8/#deploymentspec-v1beta2-apps)中,所有在模板键下的项目都是从 Pod 模板规范中定义的。您可以在kubernetes.io/docs/api-reference/v1.8/#podtemplatespec-v1-core查看 Pod 模板规范的详细信息。

如果您查看在线文档,您会看到许多我们没有指定的选项。当它们没有被指定时,Kubernetes 仍会使用规范中定义的默认值填充这些值。

您可以根据需要指定完整或轻量级的选项。所需字段的数量非常少。通常只有在您想要不同于默认值的值时,才需要定义可选字段。例如,对于一个部署,必需字段是名称和要部署的镜像。

在为自己的代码创建声明时,我建议保持一组最小的 YAML 声明。这将有助于更容易理解您的资源声明,并且与大量使用注释一起,应该使得生成的文件易于理解。

声明您的第一个应用程序

继续选择一个示例并创建一个部署声明,然后尝试使用该声明创建一个。

我建议创建一个名为 deploy 的目录,并将您的声明文件放在其中。这是使用 flask 示例:

flask.yml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: flask
 labels:
 run: flask
spec:
 template:
 metadata:
 labels:
 app: flask
 spec:
 containers:
 - name: flask
 image: quay.io/kubernetes-for-developers/flask:0.1.1
 ports: 
 - containerPort: 5000

在尝试您的文件之前,请删除现有的部署:

kubectl delete deployment flask

使用 --validate 选项是一个很好的做法,可以让 kubectl 检查文件,并且您可以将其与 --dry-run 一起使用,将文件与 Kubernetes 中的任何现有内容进行比较,以便让您明确知道它将要执行的操作。 YAML 很容易阅读,但不幸的是,由于其使用空格来定义结构,很容易出现格式错误。使用 --validate 选项,kubectl 将警告您缺少字段或其他问题。如果没有它,kubectl 通常会悄悄地失败,只是简单地忽略它不理解的内容:

kubectl apply -f deploy/flask.yml --dry-run --validate

您应该看到以下结果:

deployment "flask" created (dry run)

如果您不小心打了错字,您将在输出中看到报告的错误。我在一个键中故意打了错字,metadata,结果如下:

error: error validating "deploy/flask.yml": error validating data: found invalid field metdata for v1.PodTemplateSpec; if you choose to ignore these errors, turn validation off with --validate=false

一旦您确信数据经过验证并且将按预期工作,您可以使用以下命令创建对象:

kubectl apply -f deploy/flask.yml

即使在尝试运行代码时,仍然很容易犯一些不明显的小错误,但在尝试运行代码时会变得清晰。您可以使用 kubectl get 命令来检查特定资源。我建议您还使用 kubectl describe 命令来查看有关 Kubernetes 的所有相关事件,而不仅仅是资源的状态:

kubectl describe deployment/flask
 Name: flask
Namespace: default
CreationTimestamp: Sun, 22 Oct 2017 14:03:27 -0700
Labels: run=flask
Annotations: deployment.kubernetes.io/revision=1
 kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"apps/v1beta1","kind":"Deployment","metadata":{"annotations":{},"labels":{"run":"flask"},"name":"flask","namespace":"default"},"spec":{"t...
Selector: app=flask
Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
 Labels: app=flask
 Containers:
 flask:
 Image: quay.io/kubernetes-for-developers/flask:0.1.1
 Port: 5000/TCP
 Environment: <none>
 Mounts: <none>
 Volumes: <none>
Conditions:
 Type Status Reason
 ---- ------ ------
 Available True MinimumReplicasAvailable
 Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: flask-2003485262 (1/1 replicas created)
Events:
 Type Reason Age From Message
 ---- ------ ---- ---- -------
 Normal ScalingReplicaSet 5s deployment-controller Scaled up replica set flask-2003485262 to 1

一旦您对声明的工作原理感到满意,请将其与您的代码一起存储在源代码控制中。本书的示例部分将转移到使用存储的配置,并且本章和以后的章节将更新 Python 和 Node.js 示例。

如果要创建 Kubernetes 资源,然后使用kubectl apply命令对其进行管理,应在运行kubectl runkubectl create命令时使用--save-config选项。这将明确添加kubectl apply在运行时期望存在的注释。如果它们不存在,命令仍将正常运行,但会收到警告:

Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply

ImagePullPolicy

如果在尝试事物时在代码中使用:latest标签,您可能已经注意到imagePullPolicy的值被设置为Always

imagePullPolicy: Always

这告诉 Kubernetes 始终尝试从容器存储库加载新的 Docker 镜像。如果使用的标签不是:latest,那么默认值(IfNotPresent)只会在本地缓存中找不到容器镜像时尝试重新加载它们。

这是一种在频繁更新代码时非常有用的技术。我建议只在独自工作时使用这种技术,因为分享:latest的确切含义可能很困难,并且会导致很多混乱。

在任何暂存或生产部署中使用:latest标签通常被认为是一种不好的做法,仅仅是因为它引用的内容不确定。

审计跟踪

当您使用kubectl apply命令时,它会自动在 Kubernetes 资源中的注释中为您维护审计跟踪。如果使用以下命令:

kubectl describe deployment flask

您将看到类似以下的相当可读的输出:

Name: flask
Namespace: default
CreationTimestamp: Sat, 16 Sep 2017 08:31:00 -0700
Labels: run=flask
Annotations: deployment.kubernetes.io/revision=1
kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"apps/v1beta1","kind":"Deployment","metadata":{"annotations":{},"labels":{"run":"flask"},"name":"flask","namespace":"default"},"spec":{"t...
Selector: app=flask
Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
 Labels: app=flask
 Containers:
 flask:
 Image: quay.io/kubernetes-for-developers/flask:0.1.1
 Port: 5000/TCP
 Environment: <none>
 Mounts: <none>
 Volumes: <none>
Conditions:
 Type Status Reason
 ---- ------ ------
 Available True MinimumReplicasAvailable
 Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: flask-866287979 (1/1 replicas created)
Events:
 FirstSeen LastSeen Count From SubObjectPath Type Reason Message
 --------- -------- ----- ---- ------------- -------- ------ ------
 2d 2d 1 deployment-controller Normal ScalingReplicaSetScaled up replica set flask-866287979 to 1

我提到的审计跟踪包含在注释kubectl.kubernetes.io/last-applied-configuration中,其中包括最后应用的配置。由于该注释相当长,因此在此输出中对其进行了修剪。如果要转储整个对象,可以查看完整的详细信息,如下所示:

kubectl get deployment flask -o json

我们感兴趣的信息是metadata | annotations kubectl.kubernetes.io/last-applied-configuration。该注释中的完整细节可能如下所示:

Kubernetes 资源-注释

标签和选择器用于对 Kubernetes 资源进行分组和选择,而注释提供了一种添加特定于资源的元数据的方法,这些元数据可以被 Kubernetes 或其运行的容器访问。

正如您刚才看到的,kubectl apply在调用时会自动应用一个注释,以跟踪资源的最后应用配置状态。在上一章中,您可能已经注意到部署控制器用于跟踪修订版本的注释deployment.kubernetes.io/revision,我们还谈到了kubernetes.io/change-cause注释,该注释被kubectl用于显示部署发布的更改历史。

注释可以是简单的值或复杂的块(如kubectl.kubernetes.io/last-applied-configuration的情况)。到目前为止的示例是 Kubernetes 工具使用注释共享信息,尽管注释也用于在容器中共享信息供应用程序使用。

您可以使用它们来包含诸如添加版本控制修订信息、构建编号、相关的可读联系信息等信息。

与标签一样,注释可以使用命令kubectl annotate来添加。一般来说,注释使用与标签相同的键机制,因此任何包含kubernetes.io前缀的注释都是来自 Kubernetes 项目的内容。

标签旨在对 Kubernetes 对象(Pod、部署、服务等)进行分组和组织。注释旨在为实例(或一对实例)提供特定的附加信息,通常作为注释本身的附加数据。

在 Pod 中公开标签和注释

Kubernetes 可以直接在容器中公开有关 Pod 的数据,通常作为特定文件系统中的文件,您的代码可以读取和使用。标签、注释等可以通过容器规范作为文件在您的容器中提供,并使用 Kubernetes 所谓的downwardAPI

这可以是一种方便的方式,可以在容器中公开注释信息,例如构建时间,源代码引用哈希等,以便您的运行时代码可以读取和引用这些信息。

为了使 Pod 的标签和注释可用,您需要为容器定义一个卷挂载,然后指定downwardAPI和卷挂载点中的项目。

更新flask部署文件:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: flask
 labels:
 run: flask
 annotations:
 example-key: example-data
spec:
 template:
 metadata:
 labels:
 app: flask
 spec:
 containers:
 - name: flask
 image: quay.io/kubernetes-for-developers/flask:0.1.1
 ports:
 - containerPort: 5000
 volumeMounts:
          - name: podinfo
 mountPath: /podinfo
 readOnly: false
 volumes:
 - name: podinfo
 downwardAPI:
 items:
 - path: "labels"
 fieldRef:
 fieldPath: metadata.labels
 - path: "annotations"
 fieldRef:
 fieldPath: metadata.annotations

下面部分的细节标识了一个挂载点——将在容器内创建的目录结构。它还指定卷应该使用downwardAPI与特定的元数据;在这种情况下,是标签和注释。

当您指定卷挂载位置时,请注意不要指定已经存在并且有文件的位置(例如/等),否则容器可能无法按预期运行。挂载点不会抛出错误-它只是覆盖容器中该位置可能已经存在的任何内容。

您可以使用以下命令应用此更新的声明:

kubectl apply -f ./flask.yml

现在我们可以打开一个 shell 到正在运行的 Pod,使用以下命令:

kubectl exec flask-463137380-d4bfx -it -- sh

然后在活动的 shell 中运行以下命令:

ls -l /podinfo
total 0
lrwxrwxrwx    1 root     root            18 Sep 16 18:14 annotations -> ..data/annotations
lrwxrwxrwx    1 root     root            13 Sep 16 18:14 labels -> ..data/labels
cat /podinfo/annotations
kubernetes.io/config.seen="2017-09-16T18:14:04.024412807Z"
kubernetes.io/config.source="api"
kubernetes.io/created-by="{\"kind\":\"SerializedReference\",\"apiVersion\":\"v1\",\"reference\":{\"kind\":\"ReplicaSet\",\"namespace\":\"default\",\"name\":\"flask-463137380\",\"uid\":\"d262ca60-9b0a-11e7-884c-0aef48c812e4\",\"apiVersion\":\"extensions\",\"resourceVersion\":\"121204\"}}\n"
cat /podinfo/labels
app="flask"
pod-template-hash="463137380"

您可以通过以下方式将其与 Pod 本身的注释进行比较:

kubectl describe pod flask-463137380-d4bfx
Name: flask-463137380-d4bfx
Namespace: default
Node: minikube/192.168.64.3
Start Time: Sat, 16 Sep 2017 11:14:04 -0700
Labels: app=flask
pod-template-hash=463137380
Annotations: kubernetes.io/created-by={"kind":"SerializedReference","apiVersion":"v1","reference":{"kind":"ReplicaSet","namespace":"default","name":"flask-463137380","uid":"d262ca60-9b0a-11e7-884c-0aef48c812e4","a...
Status: Running
IP: 172.17.0.5
Created By: ReplicaSet/flask-463137380
Controlled By: ReplicaSet/flask-463137380

有关 Pod 的各种数据可以在 Pod 中公开,并且可以通过环境变量将相同的数据公开给 Pod。可以公开的完整数据集在 Kubernetes 文档中有详细说明(kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/)。

尽管使用这种机制来提供传递配置数据的方法可能看起来方便和明显,但 Kubernetes 提供了额外的功能,专门用于为容器内的代码提供配置,包括密码、访问令牌和其他机密信息所需的私有配置。

Kubernetes 资源 - ConfigMap

当您将容器创建为代码的只读实例时,您很快就会需要一种方式来提供标志或配置的小改变。也许更重要的是,您不希望在容器映像中包含诸如 API 密钥、密码或身份验证令牌等私人详细信息。

Kubernetes 支持两种资源来帮助并链接这种类型的信息。第一种是 ConfigMap,可以单独使用或跨 Pod 用于应用部署,为应用程序提供更新和传播配置的单一位置。Kubernetes 还支持 Secret 的概念,这是一种更加严格控制和仅在需要时才公开的配置类型。

例如,一个人可能会使用 ConfigMap 来控制示例 Redis 部署的基本配置,并使用 Secret 来分发敏感的身份验证凭据,以供客户端连接。

创建 ConfigMap

您可以使用kubectl create configmap命令创建 ConfigMap,其中配置的数据可以在命令行上设置,也可以来自您存储的一个或多个文件。它还支持加载文件目录以方便使用。

从命令行创建单个键/值对非常简单,但可能是管理配置的最不方便的方式。例如,运行以下命令:

kubectl create configmap example-config --from-literal=log.level=err

这将创建一个名为example-config的 ConfigMap,其中包含一个键/值对。您可以使用以下命令查看加载的所有配置列表:

kubectl get configmap
NAME             DATA      AGE
example-config   0         2d

并使用以下命令查看 ConfigMap:

kubectl describe configmap example-config
Name: example-config
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
log.level:
----
err
Events: <none>

您还可以请求以 YAML 格式获取原始数据:

kubectl get configmap example-config -o yaml --export apiVersion: v1
data:
 log.level: err
kind: ConfigMap
metadata:
 creationTimestamp: null
 name: example-config
 selfLink: /api/v1/namespaces/default/configmaps/example-config

您还可以请求以 JSON 格式获取原始数据:

kubectl get configmap example-config -o json --export {
 "apiVersion": "v1",
 "data": {
 "log.level": "err"
 },
 "kind": "ConfigMap",
 "metadata": {
 "creationTimestamp": null,
 "name": "example-config",
 "selfLink": "/api/v1/namespaces/default/configmaps/example-config"
 }
}

从字面值创建的配置的值通常是字符串。

如果您想创建代码可以解析为不同类型(数字、布尔值等)的配置值,则需要将这些配置指定为文件,或者在 YAML 或 JSON 格式的 ConfigMap 对象中定义它们为块。

如果您希望将配置分开存储在不同的文件中,它们可以以简单的key=value格式具有多行,每行一个配置。kubectl create configmap <name> --from-file <filename>命令将加载这些文件,创建一个基于文件名的configmap名称,其中包含来自文件的所有相关数据。如果您已经有要处理的配置文件,可以使用此选项基于这些文件创建 ConfigMaps。

例如,如果您想要一个名为config.ini的配置文件加载到 ConfigMap 中:

[unusual]
greeting=hello
onoff=true
anumber=3

您可以使用以下命令创建一个iniconfig ConfigMap:

kubectl create configmap iniconfig --from-file config.ini --save-config

将数据转储回 ConfigMap:

kubectl get configmap iniconfig -o yaml --export

应该返回类似以下的内容:

apiVersion: v1
data:
 config.ini: |
 [unusual]
 greeting=hello
 onoff=true
 anumber=3
kind: ConfigMap
metadata:
 name: iniconfig
 selfLink: /api/v1/namespaces/default/configmaps/iniconfig

YAML 输出中的管道符号(|)定义了多行输入。这些类型的配置不会直接作为环境变量可用,因为它们对于该格式是无效的。一旦将它们添加到 Pod 规范中,它们可以作为文件提供。将它们添加到 Pod 规范中与使用向下 API 在 Pod 的容器中公开标签或注释的方式非常相似。

管理 ConfigMaps

一旦您创建了一个 ConfigMap,就不能使用kubectl create命令用另一个 ConfigMap 覆盖它。您可以删除它并重新创建,尽管更有效的选项是像其他 Kubernetes 资源一样管理配置声明,使用kubectl apply命令进行更新。

如果您在尝试一些想法时使用kubectl create命令创建了初始的 ConfigMap,您可以开始使用kubectl apply命令来管理该配置,方式与我们之前在部署中使用的方式相同:导出 YAML,然后从该文件中使用kubectl apply

例如,要获取并存储我们之前在 deploy 目录中创建的配置,您可以使用以下命令:

kubectl get configmap example-config -o yaml --export > deploy/example-config.yml

在 Kubernetes 的 1.7 版本中,导出中添加了一些字段,这些字段并不是严格需要的,但如果您将它们留在那里也不会有任何问题。查看文件时,您应该看到类似以下内容:

apiVersion: v1
data:
 log.level: err
kind: ConfigMap
metadata:
 creationTimestamp: null
 name: example-config
 selfLink: /api/v1/namespaces/default/configmaps/example-config

dataapiVersionkind和 metadata 的键都是关键的,但 metadata 下的一些子键并不是必需的。例如,您可以删除metadata.creationTimestampmetadata.selfLink

现在您在 Kubernetes 中仍然有 ConfigMap 资源,因此第一次运行kubectl apply时,它会警告您正在做一些有点意外的事情:

kubectl apply -f deploy/example-config.yml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
configmap "example-config" configured

您可以通过在kubectl create命令中使用--save-config选项来摆脱此警告,这将包括kubectl apply期望存在的注释。

此时,kubectl apply已应用了其差异并进行了相关更新。如果您现在从 Kubernetes 中检索数据,它将具有kubectl apply在更新资源时添加的注释。

kubectl get configmap example-config -o yaml --export
apiVersion: v1
data:
 log.level: err
kind: ConfigMap
metadata:
 annotations:
 kubectl.kubernetes.io/last-applied-configuration: |
 {"apiVersion":"v1","data":{"log.level":"err"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"example-config","namespace":"default"}}
 creationTimestamp: null
 name: example-config
 selfLink: /api/v1/namespaces/default/configmaps/example-config

将配置暴露到您的容器映像中

有两种主要方法可以将配置数据暴露到您的容器中:

  • 将一个或多个 ConfigMaps 的键连接到为您的 Pod 设置的环境变量中

  • Kubernetes 可以将一个或多个 ConfigMaps 中的数据映射到挂载在 Pod 中的卷中。

主要区别在于环境变量通常在调用容器启动时设置一次,并且通常是简单的字符串值,而作为卷中数据挂载的 ConfigMaps 可以更复杂,并且如果更新 ConfigMap 资源,它们将被更新。

请注意,目前不存在明确告知容器 ConfigMap 值已更新的机制。截至 1.9 版本,Kubernetes 不包括任何方式来向 Pod 和容器发出更新的信号。

此外,作为文件挂载公开的配置数据不会立即更新。更新 ConfigMap 资源和在相关 Pod 中看到更改反映之间存在延迟。

环境变量

在定义 Pod 规范时,除了强制的名称和镜像键之外,您还可以指定一个 env 键。环境键需要一个名称,并且您可以添加一个使用 valueFrom: 引用 ConfigMap 中的数据的键。

例如,要将我们的示例配置公开为环境变量,您可以将以下段添加到 Pod 规范中:

env:
 - name: LOG_LEVEL_KEY
 valueFrom:
 configMapKeyRef:
 name: example-config
 Key: log.level

在 Pod 规范中,您可以包含多个环境变量,并且每个环境变量可以引用不同的 ConfigMap,如果您的配置分成多个部分,以便更容易(或更合理)进行管理。

您还可以将整个 ConfigMap 映射为环境变量作为单个块的所有键/值。

您可以使用 envFrom 而不是在 env 下使用单独的键,并指定 ConfigMap,例如:

envFrom:
 - configMapRef:
 name: example-config

使用此设置,每当 Pod 启动时,所有配置数据键/值都将作为环境变量加载。

您可以在 ConfigMap 中创建不适合作为环境变量的键,例如以数字开头的键。在这些情况下,Kubernetes 将加载所有其他键,并在事件日志中记录失败的键,但不会抛出错误。您可以使用 kubectl get events 查看失败的消息,其中将显示因为无效而跳过的每个键。

如果您想要使用 ConfigMap 值中的一个作为在容器内运行的命令传递的参数,也可以这样做。当您通过 env 和名称指定环境变量时,您可以在 Pod 规范中的其他地方引用该变量使用 $(ENVIRONMENT_VARIABLE_NAME)

例如,以下 spec 片段在容器调用中使用了环境变量:

spec:
 containers:
 - name: test-container
 image: gcr.io/google_containers/busybox
 command: [ "/bin/sh", "-c", "echo $(LOG_LEVEL_KEY)" ]
 env:
 - name: LOG_LEVEL_KEY
 valueFrom:
 configMapKeyRef:
 name: example-config
 key: log.level

在容器内部将 ConfigMap 暴露为文件

将 ConfigMap 数据暴露到容器中的文件中,与如何将注释和标签暴露到容器中非常相似。Pod 规范有两个部分。第一部分是为容器定义一个卷,包括名称和应该挂载的位置:

 volumeMounts:
 - name: config
 mountPath: /etc/kconfig
 readOnly: true

第二部分是一个卷描述,引用了相同的卷名称,并将 ConfigMap 列为属性,指示从哪里获取这些值:

 volumes:
 - name: config
 configMap:
 name: example-config

一旦应用了该规范,这些值将作为容器内的文件可用:

ls -al /etc/kconfig/
total 12
drwxrwxrwx    3 root     root          4096 Sep 17 00:57 .
drwxr-xr-x    1 root     root          4096 Sep 17 00:57 ..
drwxr-xr-x    2 root     root          4096 Sep 17 00:57 ..9989_17_09_00_57_49.704362876
lrwxrwxrwx    1 root     root            31 Sep 17 00:57 ..data -> ..9989_17_09_00_57_49.704362876
lrwxrwxrwx    1 root     root            16 Sep 17 00:57 log.level -> ..data/log.level
cat /etc/kconfig/log.level
Err

您可以使用环境变量或配置文件来为应用程序提供配置数据,只需取决于哪种方式更容易或更符合您的需求。我们将更新示例,使用 ConfigMaps 并将 ConfigMaps 添加到部署中,并在示例应用程序的代码中引用这些值。

对 ConfigMaps 的依赖

如果您开始在 Pod 规范中引用 ConfigMap,那么您正在为资源创建对该 ConfigMap 的依赖。例如,如果您添加了一些前面的示例来将example-data暴露为环境变量,但尚未将example-config ConfigMap 添加到 Kubernetes 中,当您尝试部署或更新 Pod 时,它将报告错误。

如果发生这种情况,错误通常会在kubectl get pods中报告,或者在事件日志中可见:

kubectl get pods
NAME                     READY     STATUS                                  RESTARTS   AGE
flask-4207440730-xpq8t   0/1       configmaps "example-config" not found   0          2d
kubectl get events
LASTSEEN   FIRSTSEEN   COUNT     NAME                     KIND         SUBOBJECT                TYPE      REASON                  SOURCE                  MESSAGE
2d         2d          1         flask-4207440730-30vn0   Pod                                   Normal    Scheduled               default-scheduler       Successfully assigned flask-4207440730-30vn0 to minikube
2d         2d          1         flask-4207440730-30vn0   Pod                                   Normal    SuccessfulMountVolume   kubelet, minikube       MountVolume.SetUp succeeded for volume "podinfo"
2d         2d          1         flask-4207440730-30vn0   Pod                                   Normal    SuccessfulMountVolume   kubelet, minikube       MountVolume.SetUp succeeded for volume "default-token-s40w4"
2d         2d          2         flask-4207440730-30vn0   Pod          spec.containers{flask}   Normal    Pulling                 kubelet, minikube       pulling image "quay.io/kubernetes-for-developers/flask:latest"
2d         2d          2         flask-4207440730-30vn0   Pod          spec.containers{flask}   Normal    Pulled                  kubelet, minikube       Successfully pulled image "quay.io/kubernetes-for-developers/flask:latest"
2d         2d          2         flask-4207440730-30vn0   Pod          spec.containers{flask}   Warning   Failed                  kubelet, minikube       Error: configmaps "example-config" not found
2d         2d          2         flask-4207440730-30vn0   Pod                                   Warning   FailedSync              kubelet, minikube       Error syncing pod
2d         2d          1         flask-4207440730         ReplicaSet                            Normal    SuccessfulCreate        replicaset-controller   Created pod: flask-4207440730-30vn0
2d         2d          1         flask                    Deployment                            Normal    ScalingReplicaSet       deployment-controller   Scaled up replica set flask-4207440730 to 1

如果在事后添加 ConfigMap,当 Pod 需要的资源可用时,Pod 将启动。

Kubernetes 资源 - Secrets

ConfigMaps 非常适用于一般配置,但很容易被看到,这可能不是期望的。对于一些配置,例如密码、授权令牌或 API 密钥,通常希望有一种更受控制的机制来保护这些值。这就是资源 Secrets 旨在解决的问题。

Secrets 通常是单独创建(和管理)的,并且在内部 Kubernetes 使用base64编码存储这些数据。

您可以通过首先将值写入一个或多个文件,然后在create命令中指定这些文件来在命令行上创建一个 secret。Kubernetes 将负责进行所有相关的base64编码并将其存储起来。例如,如果您想要存储数据库用户名和密码,您可以执行以下操作:

echo -n “admin” > username.txt
echo -n “sdgp63lkhsgd” > password.txt
kubectl create secret generic database-creds --from-file=username.txt --from-file=password.txt

请注意,在命名 secret 的名称时,您可以使用任何字母数字字符,-.,但不允许使用下划线。

如果您使用以下命令:

kubectl get secrets

您可以看到我们刚刚创建的秘密:

NAME                  TYPE                                  DATA      AGE
database-creds        Opaque                                2         2d
default-token-s40w4   kubernetes.io/service-account-token   3         5d

通过使用以下方式:

kubectl describe secret database-creds
Name: database-creds
Namespace: default
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
password.txt: 18 bytes
username.txt: 11 bytes

您会看到秘密报告为类型Opaque,并且与数据关联的字节数。

您仍然可以使用以下方式获取秘密:

kubectl get secret database-creds -o yaml --export

这将显示base64编码的值:

apiVersion: v1
data:
  password.txt: 4oCcc2RncDYzbGtoc2dk4oCd
  username.txt: 4oCcYWRtaW7igJ0=
kind: Secret
metadata:
  creationTimestamp: null
  name: database-creds
  selfLink: /api/v1/namespaces/default/secrets/database-creds
type: Opaque

如果您base64解码该值,您将看到原始版本:

echo "4oCcc2RncDYzbGtoc2dk4oCd" | base64 --decode
“sdgp63lkhsgd”

请注意,任何可以访问您的 Kubernetes 集群资源的人都可以检索和查看这些秘密。此外,我不建议您像其他声明一样管理秘密,存储在源代码控制中。这样做会在您的源代码控制系统中暴露这些秘密(以base64形式)。

将秘密暴露到容器中

我们可以以与暴露 ConfigMaps 非常相似的方式将秘密暴露给 Pod。与 ConfigMaps 一样,您可以选择将秘密作为环境变量或作为卷中的文件暴露,由 Pod 指定。

暴露秘密的格式看起来与暴露 ConfigMap 值的格式相同,只是在规范中使用secretKeyRef而不是configMapRef

例如,要将前面的示例秘密密码作为环境变量暴露,您可以在 Pod 规范中使用以下内容:

 env:
 - name: DB_PASSWORD
 valueFrom:
 secretKeyRef:
 name: database-creds
 key: password.txt

然后在容器内查看,环境变量容器DB_PASSWORD

kubectl exec flask-509298146-ql1t9 -it -- sh
env | grep DB
DB_PASSWORD=“sdgp63lkhsgd”

更好的方法是利用 Kubernetes 包含的将秘密挂载为容器内文件的能力。配置与暴露 ConfigMap 值非常相似,只是在规范中将 Secret 定义为卷属性,而不是 ConfigMap。

在规范中,您需要为容器定义一个volumeMount,指示其在容器中的位置:

 volumeMounts:
 - name: secrets
 mountPath: "/secrets"

然后定义如何从秘密中填充该卷的内容:

 volumes:
 - name: secrets
 secret:
 secretName: database-creds
 items:
 - key: password.txt
 path: db_password

部署使用此配置后,容器中将有一个/secrets/db_password文件,其中包含来自我们秘密的内容:

/ # ls -l /secrets/
total 0
lrwxrwxrwx    1 root     root            18 Sep 17 00:49 db_password -> ..data/db_password
/ # ls -l /secrets/db_password
lrwxrwxrwx    1 root     root            18 Sep 17 00:49 /secrets/db_password -> ..data/db_password
/ # cat /secrets/db_password
“sdgp63lkhsgd”

秘密和安全性-秘密有多秘密?

合理地说,但在 Kubernetes 1.8 中至少不是密码学安全的。如果您从安全的角度看待秘密,那么对秘密的约束要比将值留在 ConfigMap 中好,但安全配置文件有显着的限制。

在本质上,密钥的数据以明文(尽管编码文本)存储在 etcd 3.0 中,etcd 3.0 是 Kubernetes 1.8 的基础。它不使用静态密钥来保留(和访问)密钥。如果您正在运行自己的 Kubernetes 集群,请注意,未经保护的 etcd 代表集群整体安全性的一个重大弱点。

对于许多应用程序和用例,这是完全可以接受的,但如果您需要在开发和生产环境中适应更高的安全配置文件,那么您将需要查看与 Kubernetes 配合使用的工具。最常讨论的替代/扩展是 Vault,这是 HashiCorp 的一个开源项目。您可以在www.vaultproject.io找到有关 Vault 的更多详细信息。

Kubernetes 项目在秘密和秘密管理方面也在不断发展。在 1.7 版本中,Kubernetes 包括基于角色的访问控制RBAC),该项目正在根据路线图维护和开发,以改进 Kubernetes 的功能,提高其安全配置文件的能力,并在未来支持更容易与外部秘密管理源(如 Vault)协调。

示例-使用 ConfigMap 的 Python/Flask 部署

这个示例建立在我们之前的 Python/Flask 示例之上。此扩展将添加一个使用环境变量和结构化文件的 ConfigMap,以及用于消耗和使用这些值的代码更新。

首先,添加一个包含顶级值和更深层配置的 ConfigMap。顶级值将公开为环境变量,多行 YAML 将公开为容器内的文件:

# CONFIGURATION FOR THE FLASK APP
kind: ConfigMap
apiVersion: v1
metadata:
 name: flask-config
data:
 CONFIG_FILE: “/etc/flask-config/feature.flags“
 feature.flags: |
 [features]
 greeting=hello
 debug=true

这个 ConfigMap 与部署的 Pod 规范映射,使用envFrom键,并作为卷提供文件映射:

 spec:
 containers:
 - name: flask
 image: quay.io/kubernetes-for-developers/flask:latest
 ports:
 - containerPort: 5000
 envFrom:
 - configMapRef:
 name: flask-config
 volumeMounts:
 - name: config
 mountPath: /etc/flask-config
 volumes:
 - name: config
 configMap:
 name: flask-config

此更新对部署有一个名为flask-config的 ConfigMap 的依赖。如果 ConfigMap 没有加载,并且我们尝试加载更新的部署,它将不会更新部署,直到该 ConfigMap 可用。为了避免意外丢失文件的情况,您可以将 ConfigMap 和部署规范放在同一个 YAML 文件中,用新行上的---分隔。然后,您可以在使用kubectl apply命令时按照指定的顺序部署多个资源。

您还可以将每个资源保存在单独的文件中,如果这样更容易理解或管理,主要取决于您的偏好。kubectl apply命令包括选项来引用目录中的所有文件,包括递归地 - 因此,对文件进行排序和结构化;但是,最好自己管理它们。

为了匹配这个示例,github.com/kubernetes-for-developers/kfd-flask上的代码有一个标签,您可以使用它来一次更新所有文件:

git checkout 0.2.0

(如果您跳过了之前的示例,您可能需要首先克隆存储库:git clone https://github.com/kubernetes-for-developers/kfd-flask

更新代码后,部署更新:

kubectl apply -f deploy/

部署后,您可以使用kubectl exec在 Pod 中运行交互式 shell,并检查部署和已暴露的内容。

侧边栏 - JSONPATH

我们可以使用类似以下命令查找特定的 Pod:

kubectl get pods -l app=flask

这将仅查找与app=flask选择器匹配的 Pod,并打印出类似以下的人类可读输出:

NAME                     READY     STATUS    RESTARTS   AGE
flask-2376258259-p1cwb   1/1       Running   0          8m

这些相同的数据以结构化形式(JSON、YAML 等)可用,我们可以使用诸如jq之类的工具进行解析。Kubectl 包括两个额外的选项,使其成为一个更方便的工具 - 您可以使用JSONPATH 或 GO_TEMPLATE来挖掘特定的值。使用内置到kubectl客户端的JSONPATH,而不是执行前面的两步骤来获取 Pod 名称,您可以直接获取我们想要使用的特定细节,即名称:

kubectl get pods -l app=flask -o jsonpath='{.items[*].metadata.name}'

这应该返回以下内容:

flask-2376258259-p1cwb

这可以很容易地嵌入到一个 shell 命令中,使用$()来内联执行它。这最终会成为一个更复杂的命令,但它会处理我们询问 Kubernetes 相关 Pod 名称的步骤,这对于许多交互命令来说至关重要。

例如,我们可以使用以下命令在与此部署相关联的 Pod 中打开交互式 shell:

kubectl exec $(kubectl get pods -l app=flask \
-o jsonpath='{.items[*].metadata.name}') \
-it -- /bin/sh

这获取了 Pod 的名称,并将其嵌入到kubectl exec中,以使用/bin/sh命令运行交互式会话。

一旦您打开了这个会话,您可以查看已设置的环境变量,如下所示:

env

这将显示设置的所有环境变量,其中之一应该是以下内容:

CONFIG_FILE=/etc/flask-config/feature.flags

您可以查看更复杂的配置数据:

cat $CONFIG_FILE
[features]
greeting=hello
debug=true

我们精心制作了 ConfigMap,根据我们放入部署规范的内容,为该文件的正确位置。如果我们更改部署规范,但不更改 ConfigMap,则嵌入在环境变量CONFIG_FILE中的位置将不正确。

使用 Kubernetes 部署、ConfigMap 和服务规范的 YAML,存在许多未抽象出的重复数据。从开发人员的角度来看,这将感到尴尬,违反了常见的不要重复自己的口头禅。有很多重复和小改变的地方,不幸地影响了部署规范。

Kubernetes 项目正在发展与这些文件交互的方式,努力使生成相关配置更加与仍处于早期开发阶段的项目相匹配。随着 Kubernetes 的不断成熟,在定义资源声明时,这应该会演变为更具有代码样式的特质。

在 Python/Flask 中使用 ConfigMap

在 Python 中,您可以使用 os.environ 查看环境变量,例如:

import os
os.environ.get('CONFIG_FILE’)

在代码中使用os.environ.get时,您可以设置默认值来处理环境变量未设置的情况:

import os
os.environ.get('CONFIG_FILE’,’./feature.flags’)

我们在这里设置CONFIG_FILE环境变量,以向您展示如何完成此操作,但严格来说,不一定需要读取配置文件-更多是为了方便您在需要时覆盖该值。

Python 还包括一个模块来解析和读取 INI 风格的配置文件,就像我们在 ConfigMap 中添加的那样。继续使用示例:

from configparser import SafeConfigParser
from pathlib import Path
# initialize the configuration parser with all the existing environment variables
parser = SafeConfigParser(os.environ)

从这里开始,ConfigParser 已加载了名为DEFAULT的部分,其中包含所有环境变量,我们可以检索其中的一个:

Python 3.6.1 (default, May  2 2017, 15:16:41)
[GCC 6.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> from configparser import SafeConfigParser
>>> from pathlib import Path
>>> # initialize the configuration parser with all the existing environment variables
... parser = SafeConfigParser(os.environ)
>>> parser.get('DEFAULT','CONFIG_FILE')
'/etc/flask-config/feature.flags'

我们可以使用基于存储在 ConfigMap 中的 INI 文件的部分来扩展解析器,该文件在文件系统上公开为/etc/flask-config/feature.flags,代码如下:

# default location of ./feature.flags is used if the environment variable isn’t set
config_file = Path(os.environ.get('CONFIG_FILE','/opt/feature.flags'))
# verify file exists before attempting to read and extend the configuration
if config_file.is_file():
 parser.read(os.environ.get('CONFIG_FILE'))

现在解析器将加载来自环境变量的DEFAULT部分和来自 ConfigMap 数据的'features'部分:

>>> parser.sections()
['features']
>>> parser.getboolean('features','debug')
True

ConfigParser 还可以使您在代码中包含默认值:

>>> parser.getboolean('features','something-else’,fallback=False)
False

然后我们使用这种代码来根据 ConfigMap 设置调试启用或禁用:

if __name__ == '__main__':
 debug_enable = parser.getboolean('features','debug',fallback=False)
 app.run(debug=debug_enable,host='0.0.0.0')

您可以在docs.python.org/3/library/configparser.html找到有关如何利用 Python 3 的 ConfigParser 的更多详细信息。

摘要

在本章中,我们详细讨论了如何充分利用 Kubernetes 的声明性特性,并通过规范文件来管理我们的应用程序。我们还讨论了 Annotations、ConfigMap 和 Secrets 以及如何创建并在 Pods 内部使用它们。我们在本章中还更新了我们的 Python 和 Node.js 应用程序,以使用 ConfigMaps 来运行我们之前设置的示例代码,并简要介绍了如何利用 kubectl 中内置的JSONPATH来使该工具更具即时提供所需特定信息的强大功能。

第五章:Pod 和容器的生命周期

由于 Kubernetes 是一个声明性系统,为 Pod 和容器提供的生命周期和钩子是代码可以采取行动的地方。Pod 有一个生命周期,容器也有一个生命周期,Kubernetes 提供了许多地方,您可以向系统提供明确的反馈,以便它按照您的意愿运行。在本章中,我们将深入探讨预期的生命周期,可用的钩子以及如何使用它们的示例。

主题将包括:

  • Pod 生命周期

  • 容器生命周期

  • 探针

  • 容器钩子:post-start 和 pre-stop

  • 初始化容器

  • 如何处理优雅关闭

Pod 生命周期

Pod 的生命周期是几个组件的聚合,因为 Pod 有许多移动部分,可以处于各种状态,它的表示是 Kubernetes 如何管理你的代码运行,与各种控制器一起工作的控制和反馈循环。

Pod 生命周期的状态有:

  • 挂起:Pod 已通过 API 创建,并正在被调度,加载并在其中一个节点上运行的过程中

  • 运行:Pod 完全运行,并且软件在集群中运行

  • 成功(或)失败:Pod 已完成操作(正常或崩溃)

  • 还有第四种状态:未知,这是一个相当罕见的情况,通常只在 Kubernetes 内部出现问题时才会出现,它不知道容器的当前状态,或者无法与其底层系统通信以确定该状态。

如果您在容器中运行长时间运行的代码,那么大部分时间将花在运行中。如果您在 Kubernetes 中使用 Job 或 CronJob 运行较短的批处理代码,则最终状态(成功或失败)可能是您感兴趣的。

容器生命周期

由于每个 Pod 可以有一个或多个容器,因此容器也有一个由它们单独管理的状态。容器的状态更简单,而且非常直接:

  • 等待

  • 运行

  • 终止

容器状态都有与之关联的时间戳,指示集群记录容器处于该状态的时间。如果经过了多个状态的处理,还会有一个最后状态字段。由于容器相当短暂,因此通常会看到先前状态为 Terminated,其中包括容器启动时间、完成时间、退出代码以及有关其终止原因的字符串条目。

以下是在您的 Pod 处理一段时间后容器状态的示例(在此示例中,经过多次更新后):

您可以在 kubectl describe pod 命令的输出中以人类可读的格式看到额外的详细信息,这通常是快速了解 Pod 内发生的情况最方便的方法。

所有状态都包含额外的信息,以提供正在发生的详细信息。API 中有一个正式的 PodStatus 对象可用。每个状态都有可用的额外详细信息,而且通常状态对象包括一系列常见的条件,这些条件通常在描述或原始 YAML 的输出中是可见的。

使用 kubectl get pod ... -o yaml 命令,您可以以机器可解析的形式看到数据,并且可以看到在 describe 命令中未公开的额外详细信息。在以下截图中,您可以看到与 Pod 和容器状态相关的输出,包括条件、容器状态和相关时间戳:

随着 Kubernetes 对象经历其生命周期,条件被添加到其状态中。

在 Pod 状态 pending 中,通常会添加两个条件:InitializedPodScheduled。如果集群无法运行请求的 Pod,则可能会看到条件 Unschedulable 而不是 PodScheduled。当 Pod 处于 Running 状态时,还有一个与之相关的条件 Ready,它会影响 Kubernetes 对代码的管理。

部署、ReplicaSets 和 Pods

Pods 不是唯一利用和暴露条件的 Kubernetes 资源。部署也使用条件来表示详细信息,例如代码更新的部署进度以及部署的整体可用性。

在使用部署时,您将看到的两个条件是:

  • 进展中

  • 可用

当底层 Pod 的最小副本数可用时(默认为 1)时,Available 将为 true。当 ReplicaSets 及其相关的 Pods 被创建并且可用时,将设置 Progressing。

Kubernetes 在其内部资源之间的关系上使用了一致的模式。正如我们在前一章中讨论的那样,部署将有关联的副本集,而副本集将有关联的 Pod。您可以将此可视化为一系列对象,其中较高级别负责监视和维护下一级别的状态:

我们一直在关注 Pod 的状态及其生命周期,因为那里代表了代码并且实际在运行。在大多数情况下,您将创建一个部署,然后该部署将有自己的状态和条件。这将进而创建一个 ReplicaSet,而 ReplicaSet 将创建 Pod 或 Pods。

当正在创建一个 Pod 时,系统将首先尝试创建 API 资源本身,然后它将尝试在集群中找到一个运行它的位置。当资源已创建时,将向状态添加 Initialized 条件。当集群已确定在哪里运行 Pod 时,将添加 PodScheduled 条件。如果集群无法找到一个可以按照您描述的方式运行 Pod 的位置,则将向状态添加Unschedulable条件。

获取当前状态的快照

您可以使用kubectl describekubectl get命令查看 Kubernetes 对代码状态的当前快照。如果您只是想要交互式地查看状态,那么kubectl describe命令是最有价值的。请记住,Kubernetes 管理与运行代码相关的一系列对象,因此如果您想要查看完整的快照,您将需要查看每个对象的状态:部署、ReplicaSet 和 Pods。实际上,查看部署的状态,然后跳转到 Pods 通常会为您提供所需的任何细节。

您可以通过使用kubectl get pod查看 Pod 的原始数据,或者使用describe命令来查看 Kubernetes 对您的代码正在做什么。您需要查找StatusConditions。例如,当我们之前创建了一个nodejs应用程序部署时,创建了一系列对象:

kubectl get deploy
NAME   DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
nodejs 1       1       1          1         8h
kubectl get rs
NAME              DESIRED CURRENT READY AGE
nodejs-6b9b87d48b 1       1       1     8h
kubectl get pod
NAME                    READY STATUS  RESTARTS AGE
nodejs-6b9b87d48b-ddhjf 1/1   Running 0        8h

您可以使用kubectl describe命令查看部署的当前状态快照:

kubectl describe deploy nodejs

这将呈现类似于以下信息:

您可以使用kubectl describe通过查看 ReplicaSet 获取其他详细信息:

kubectl describe rs nodejskubectl describe deploy nodejs 

最后,再次使用它来查看部署和 ReplicaSet 创建的 Pod:

kubectl describe pod nodejs

kubectl describe输出底部列出的事件将显示与 Pod 相关的发生的顺序。

如果您想在脚本中使用状态,或者以其他方式使用程序解析输出,那么可以使用kubectl get命令,指定输出的数据格式,如 YAML。例如,可以使用以下命令检索 YAML 中的相同 Pod 输出:

 kubectl get pod nodejs-6b9b87d48b-lcgvd -o yaml

输出底部的状态键下将保存状态快照信息:

虽然在kubectl describe的输出中没有显示,但每个条件都有最后更新时间、上次更改时间、类型和状态。此外,每个容器都列有其自己的状态。

您可以在未来的 Kubernetes 版本中看到的 Pod 条件列表可能会增加,今天包括以下内容:

  • PodScheduled:当 Pod 已在节点上调度并且开始将其加载到节点上的过程时转换为 true。

  • 已初始化:当 Pod 的所有容器已加载,并且定义的任何初始化容器已运行完成时,将标记为 true。

  • Ready:Pod 已根据规范加载和启动。在就绪探测和存活探测成功完成(如果定义了任一或两者),此值不会标记为 true。

  • Unschedulable:当 Kubernetes 集群无法将可用资源与 Pod 的需求匹配时,将列出并断言此条件。

有时状态(例如SucceededFailed)还会包括一个Reason,其中包括一些文本输出,旨在使理解发生了什么变得更容易。正如您可以从前面的输出中看到的那样,所有状态更改都包括时间戳。由于这是一个时间点的快照,时间戳可以提供线索,以了解发生的顺序以及多久之前发生的。

最后需要注意的是,与 Pod 相关的事件通常会提供有用的描述性注释,说明在启动 Pod 时发生了什么(或者未发生什么)。利用从描述、Pod 状态、条件和事件中提供的所有细节,可以提供最佳的状态更新,这些更新是 Pod 日志之外的外部状态更新。

Pod 的生命周期还包括您可以指定的钩子或反馈机制,以允许您的应用程序提供有关其运行情况的反馈。其中一个机制是Ready条件,您之前已经见过。Kubernetes 允许您的应用程序提供特定的反馈,以确定它是否准备好接受流量,以及它的健康状况。这些反馈机制称为探针,可以选择在 Pod 规范中定义。

探针

Kubernetes 中启用的两种探针是存活探针和就绪探针。它们是互补的,但在意图和用法上有所不同,并且可以为 Pod 中的每个容器定义。

存活探针

最基本的探针是存活探针。如果定义了存活探针,它将提供一个命令或 URL,Kubernetes 可以使用它来确定 Pod 是否仍在运行。如果调用成功,Kubernetes 将假定容器是健康的;如果未能响应,则可以根据定义的restartPolicy来处理 Pod。结果是二进制的:要么探针成功,Kubernetes 认为您的 Pod 正在运行,要么失败,Kubernetes 认为您的 Pod 不再可用。在后一种情况下,它将根据定义的 RestartPolicy 来选择要执行的操作。

restartPolicy的默认值为Always,这意味着如果 Pod 中的容器失败,Kubernetes 将始终尝试重新启动它。您可以定义的其他值包括OnFailureNever。当容器重新启动时,Kubernetes 将跟踪重新启动发生的频率,并且如果它们在快速连续发生,则会减慢重新启动的频率,最多在重新启动尝试之间间隔五分钟。重新启动次数在kubectl describe的输出中作为restartcount进行跟踪和可见,并且在kubectl get的数据输出中作为restartCount键进行跟踪。

如果未明确定义存活探针,则假定探针将成功,并且容器将自动设置为活动状态。如果容器本身崩溃或退出,Kubernetes 将做出反应并根据restartPolicy重新启动它,但不会进行其他活动检查。这允许您处理代码已经冻结或死锁并且不再响应的情况,即使进程仍在运行。

可以定义存活探针来通过以下三种方法检查 Pod 的健康状况:

  • ExecAction:这在 Pod 内部调用命令以获取响应,并且该命令调用的退出代码的结果用于存活检查。除0之外的任何结果都表示失败。

  • TCPSocketAction:这尝试打开一个套接字,但除了尝试打开它之外,不会操作或与套接字交互。如果套接字打开,则探针成功,如果失败或在超时后失败,则探针失败。

  • HTTPGetAction:类似于套接字选项,这将作为指定的 URI 对您的 Pod 进行 HTTP 连接,并且 HTTP 请求的响应代码用于确定存活探针的成功/失败。

还有许多变量可以配置此探针的具体内容:

  • activeDeadlineSeconds(默认情况下未设置):此值通常与作业一起使用,而不是长时间运行的 Pod,以对作业的最长运行时间设置最大限制。此数字将包括初始化容器所花费的任何时间,稍后在本章中将进一步讨论这一点。

  • initialDelaySeconds(默认情况下未设置):这允许您指定在开始探测检查之前的秒数。默认情况下未设置,因此实际上默认为 0 秒。

  • timeoutSeconds(默认为 1):如果命令或 URL 请求需要很长时间才能返回,这提供了一个超时。如果超时在命令返回之前到期,则假定它已失败。

  • periodSeconds(默认为 10):这定义了 Kubernetes 运行探测的频率——无论是调用命令、检查套接字可用性还是进行 URL 请求。

  • successThreshold(默认为 1):这是探测需要返回成功的次数,以便将容器的状态设置为“活动”。

  • failureThreshold(默认为 3):这是触发将容器标记为不健康的探测的最小连续失败次数。

如果您定义一个 URL 来请求并将其他所有内容保持默认状态,正常模式将需要三个失败响应——超时或非 200 响应代码——然后才会考虑将容器标记为“死亡”并应用restartPolicy。默认情况下,每次检查间隔为 10 秒,因此在这些默认情况下,您的容器在系统应用restartPolicy之前可能会死亡长达 30 秒。

如果您正在使用基于 HTTP 的探测,可以在进行 HTTP 请求时定义许多其他变量。

  • host:默认为 Pod IP 地址。

  • scheme:HTTP 或 https。Kubernetes 1.8 默认为 HTTP

  • path:URI 请求的路径。

  • HttpHeaders:要包含在请求中的任何自定义标头。

  • port:进行 HTTP 请求的端口。

就绪探测

第二个可用的探测是就绪探测,通常与活跃探测并行使用。就绪探测只有在应用程序准备好并能够处理正常请求时才会做出积极响应。例如,如果您希望等待直到数据库完全可操作,或者预加载一些可能需要几秒钟的缓存,您可能不希望在这些操作完成之前对就绪探测返回积极响应。

与活跃探测一样,如果未定义,则系统会假定一旦您的代码运行,它也准备好接受请求。如果您的代码需要几秒钟才能完全运行,那么定义和利用就绪探测是非常值得的,因为这将与任何服务一起自动更新端点,以便在无法处理流量时不会将流量路由到实例。

配置就绪探针的相同选项可用,其中之一是 ExecActionTCPSocketActionHTTPGetAction。与存活探针一样,可以使用相同的变量来调整探针请求的频率、超时以及触发状态更改的成功和/或失败次数。如果您修改了存活探针中的值,那么您可能不希望将就绪探针设置为比存活探针更频繁。

作为提醒,当就绪探针失败时,容器不会自动重新启动。如果您需要该功能,您应该使用存活探针。就绪探针专门设置为允许 Pod 指示它尚不能处理流量,但它预计很快就能够。随着探针的更新,Pod 状态将被更新为设置 Ready 为正或负,并且相关的 Ready 条件也将被更新。随着这一过程的发生,使用此 Pod 的任何服务都将收到这些更新的通知,并将根据就绪值更改发送流量(或不发送)。

您可以将就绪探针视为断路器模式的实现,以及负载分担的一种手段。在运行多个 Pod 的情况下,如果一个实例过载或出现一些临时条件,它可以对就绪探针做出负面响应,Kubernetes 中的服务机制将把任何进一步的请求转发到其他 Pod。

向我们的 Python 示例添加探针

与以前的示例一样,代码在 GitHub 中可用在 github.com/kubernetes-for-developers/kfd-flask 项目中。我不会展示所有更改,但您可以使用此命令从分支 0.3.0 检出代码:git checkout 0.3.0。从该代码构建的 Docker 镜像同样可以在 quay.io 仓库的 0.3.0 标签下找到。

在此更新中,该项目包括了 Redis 的辅助部署,以匹配上一章中的一些概念。部署的规范也已更新,特别添加了存活探针和就绪探针。更新后的部署规范现在如下:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
 name: flask
 labels:
 run: flask
spec:
 template:
 metadata:
 labels:
 app: flask
 spec:
 containers:
 - name: flask
 image: quay.io/kubernetes-for-developers/flask:0.3.0
 imagePullPolicy: Always
 ports:
 - containerPort: 5000
 envFrom:
 - configMapRef:
 name: flask-config
 volumeMounts:
 - name: config
 mountPath: /etc/flask-config
 readOnly: true
 livenessProbe:
 httpGet:
 path: /alive
 port: 5000
 initialDelaySeconds: 1
 periodSeconds: 5
 readinessProbe:
 httpGet:
 path: /ready
 port: 5000
 initialDelaySeconds: 5
 periodSeconds: 5
 volumes:
 - name: config
 configMap:
 name: flask-config

探针以粗体显示。两个探针都使用与应用程序的其余部分相同的端口(5000),以及它们自己各自的端点。就绪探针设置为延迟一秒开始检查,就绪探针设置为延迟五秒开始检查,两者都设置为稍微更紧密的频率,为五秒。

Python 代码也已经更新,主要是为了实现响应于就绪和活动探针的/alive/ready方法。

就绪探针是最简单的,只是回复一个静态响应,仅保留底层 flask 代码对 HTTP 请求的响应验证:

@app.route('/alive')
def alive():
 return "Yes"

就绪探针扩展了这种模式,但在回复肯定之前验证了底层服务(在本例中为 Redis)是否可用和响应。这段代码实际上并不依赖于 Redis,但在您自己的代码中,您可能依赖于远程服务可用,并且有一些方法可以指示该服务是否可用和响应。如前所述,这实际上是断路器模式的一种实现,并且与服务构造一起,允许 Kubernetes 帮助将负载定向到可以响应的实例。

在这种情况下,我们利用了 Python 库中公开的redis ping()功能:

@app.route('/ready')
def ready():
 if redis_store.ping():
 return "Yes"
 else:
 flask.abort(500)

代码中的其他更新初始化了代码中的redis_store变量,并将 DNS 条目添加到configMap中,以便应用程序代码可以使用它。

运行 Python 探针示例

如果您查看0.3.0分支,您可以调查此代码并在您自己的 Minikube 实例或另一个 Kubernetes 集群中本地运行它。要查看代码:

git clone https://github.com/kubernetes-for-developers/kfd-flask

cd kfd-flask

git checkout 0.3.0

kubectl apply -f deploy/

最后一个命令将创建redis-master的服务和部署,以及 Python/flask 代码的服务、configmap 和部署。然后,您可以使用kubectl describe命令查看定义的探针及其值:

kubectl describe deployment flask

您还可以查看正在运行的单个 flask Pod 的日志,并查看正在处理的请求:

kubectl log deployment/flask
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 177-760-948
172.17.0.1 - - [21/Dec/2017 14:57:50] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:57:53] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:57:55] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:57:58] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:00] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:03] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:05] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:08] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:10] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:13] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:15] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:18] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:20] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:23] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:25] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:28] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:30] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:33] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:35] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:38] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:40] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:43] "GET /ready HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:45] "GET /alive HTTP/1.1" 200 -
172.17.0.1 - - [21/Dec/2017 14:58:48] "GET /ready HTTP/1.1" 200 -
...

向我们的 Node.js 示例添加探针

向基于 Node.js/express 的应用程序添加示例探测与 Python 应用程序完全相同的模式。与 Python 示例一样,此代码和规范可在 GitHub 的github.com/kubernetes-for-developers/kfd-nodejs项目下的分支0.3.0中找到。

探测器向 Node.js 部署添加了几乎相同的规范:

livenessProbe:
  httpGet:
  path: /probes/alive
  port: 3000
  initialDelaySeconds: 1
  periodSeconds: 5 readinessProbe:   httpGet:
  path: /probes/ready
  port: 3000
  initialDelaySeconds: 5
  periodSeconds: 5

在这种情况下,探测器请求与应用程序提供的相同的 HTTP 响应和相同的端口。URI 路径更长,利用了应用程序的结构,该结构使用单个代码段用于特定 URI 下的路由,因此我们能够将就绪性和存活性探测器捆绑到一个新的probes.js路由中。

主应用程序已更新以创建一个探测器路由并在应用程序启动时绑定它,然后路由本身的代码提供响应。

probes.js的代码如下:

var express = require('express');
var router = express.Router();
var util = require('util');
var db = require('../db');

/* GET liveness probe response. */
router.get('/alive', function(req, res, next) {
 res.send('yes');
});

/* GET readiness probe response. */
router.get('/ready', async function(req, res, next) {
 try {
 let pingval = await db.ping()
 if (pingval) {
 res.send('yes');
 } else {
 res.status(500).json({ error: "redis.ping was false" })
 }
 } catch (error) {
 res.status(500).json({ error: error.toString() })
 }
});

module.exports = router;

与前面的 Python 示例一样,存活性探测返回静态响应,仅用于验证express是否仍然响应 HTTP 请求。就绪性探测更为复杂,它在异步等待/捕获中包装了db.ping()并检查其值。如果为负,或发生错误,则返回500响应。如果为正,则返回静态的积极结果。

使用kubectl describe deployment nodejs将显示配置,其中包含操作探测器,非常类似于 Python 示例,而kubectl log nodejs-65498dfb6f-5v7nc将显示来自探测器的请求得到了响应:

GET /probes/alive 200 1.379 ms - 3
Thu, 21 Dec 2017 17:43:51 GMT express:router dispatching GET /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router query : /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router expressInit : /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router logger : /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router jsonParser : /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router urlencodedParser : /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router cookieParser : /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router serveStatic : /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router router : /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router dispatching GET /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router trim prefix (/probes) from url /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router router /probes : /probes/ready
Thu, 21 Dec 2017 17:43:51 GMT express:router dispatching GET /ready
GET /probes/ready 200 1.239 ms - 3
Thu, 21 Dec 2017 17:43:54 GMT express:router dispatching GET /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router query : /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router expressInit : /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router logger : /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router jsonParser : /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router urlencodedParser : /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router cookieParser : /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router serveStatic : /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router router : /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router dispatching GET /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router trim prefix (/probes) from url /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router router /probes : /probes/alive
Thu, 21 Dec 2017 17:43:54 GMT express:router dispatching GET /alive
GET /probes/alive 200 1.361 ms - 3

我们可以通过终止 Redis 服务来测试就绪性探测的操作。如果我们调用以下命令:

kubectl delete deployment redis-master

kubectl get pods的结果很快就会显示 Pod 是活动的,但不是ready的:

kubectl get pods
NAME                         READY STATUS      RESTARTS AGE
nodejs-65498dfb6f-5v7nc      0/1   Running     0        8h
redis-master-b6b8774f9-sjl4w 0/1   Terminating 0        10h

redis-master部署关闭时,您可以从 Node.js 部署中获取一些有趣的细节。使用kubectl describe来显示部署:

kubectl describe deploy nodejs

并使用kubectl describe来查看相关的 Pods:

kubectl describe pod nodejs

请注意,Condition Ready现在是false,而 Node.js 容器的状态为Running,但ReadyFalse

如果重新创建或恢复 Redis 部署,则服务将像您期望的那样全部恢复在线。

容器生命周期钩子

Kubernetes 还提供了一些在每个容器的生命周期中可以在容器的设置和拆卸时间使用的钩子。这些称为容器生命周期钩子,为每个容器定义,而不是为整个 Pod 定义。当您想要为 Pod 中的多个容器配置一些特定于容器的附加功能时,这些钩子非常有用。

每个容器可以定义两个钩子:post-start 和 pre-stop。post-start 和 pre-stop 钩子预期至少被调用一次,但 Kubernetes 不保证这些钩子只会被调用一次。这意味着虽然可能很少见,post-start 或 pre-stop 钩子可能会被调用多次。

这些钩子都不接受参数,并且以与容器运行命令相同的方式定义。当使用时,它们预期是自包含的、相对短暂的命令,总是返回。当这些钩子被调用时,Kubernetes 暂停对容器的管理,直到钩子完成并返回。因此,对于这些钩子调用的可执行文件不要挂起或无限运行至关重要,因为 Kubernetes 没有一种方式来监视这种情况并响应无法完成或返回值的失败。

在 post-start 的情况下,容器状态直到 post-start 钩子完成之前不会转移到运行状态。post-start 钩子也不能保证在容器的主要命令之前或之后被调用。在 pre-stop 的情况下,容器直到 pre-stop 钩子完成并返回后才会被终止。

这两个钩子可以使用 Exec 和 HTTP 两种处理程序之一来调用:Exec 在容器内部以及与容器相同的进程空间中运行特定命令,就像使用kubectl exec一样。HTTP 处理程序设置用于针对容器的 HTTP 请求。在任何情况下,如果钩子返回失败代码,容器将被终止。

这些钩子的日志不会在 Pod 事件或日志中公开。如果处理程序失败,它会广播一个事件,可以使用kubectl describe命令查看。这两个事件分别是FailedPostStartHookFailedPreStopHook

预停钩子在你想要外部命令被调用来干净地关闭运行中的进程时非常有用,比如调用 nginx -s quit。如果你正在使用别人的代码,尤其是它有一个比正确响应 SIGTERM 信号更复杂的关闭过程,这将特别有用。我们将在本章稍后讨论如何优雅地关闭 Kubernetes。

后启动钩子在你想要在容器内创建一个信号文件,或者在容器启动时调用 HTTP 请求时经常有用。更常见的情况是在主代码启动之前进行初始化或前置条件验证,而有另一个选项可用于该功能:初始化容器。

初始化容器

初始化容器是可以在你的 Pod 上定义的容器,并且将在定义它们之前的特定顺序中被调用,然后才会启动你的主容器(或容器)。初始化容器在 Kubernetes 1.6 版本中成为 Pod 规范的正常部分。

这些容器可以使用相同的容器镜像并简单地具有替代命令,但它们也可以使用完全不同的镜像,利用 Kubernetes Pod 共享网络和文件系统挂载的保证来进行初始化和设置工作,以便在主容器运行之前进行。这些容器还使用命名空间,因此它们可以获得主容器没有的特定访问权限;因此,它们可以访问主容器无法访问的 Kubernetes Secrets。

初始化容器预期具有可以运行到完成并以成功响应退出的代码。正如之前提到的,这些容器也按顺序调用,并不会并行运行;每个容器都必须在下一个容器启动之前完成。当所有容器都完成时,Kubernetes 初始化 Pod 并运行定义的容器(或容器)。如果初始化容器失败,那么 Pod 被认为已经失败,并且整个套件被终止(或更具体地说,根据 restartPolicy 处理)。

初始化容器允许您在运行主进程之前进行各种设置。您可能要做的一些例子包括编写主 Pod 容器需要的配置文件,验证服务在启动主容器之前是否可用和活动,检索和初始化内容,比如从 Git 存储库或文件服务中拉取数据供主容器使用,或者在启动主容器之前强制延迟。

在初始化容器运行时,Pod 状态将显示Init:,后面跟着一些初始化容器特定的状态。如果一切顺利,符合预期,它将报告列出的初始化容器数量以及已经完成运行的数量。如果初始化容器失败,那么Init:后面将跟着ErrorCrashLoopBackOff

初始化容器在 Pod 规范中被指定在与主容器相同级别的位置,并且作为一个列表,每个初始化容器都有自己的名称、镜像和要调用的命令。例如,我们可以在 Python flask 规范中添加一个init容器,它只会在 Redis 可用时才返回。一个例子可能是以下内容:

spec:
 template:
 metadata:
 labels:
 app: flask
 spec:
 containers:
 - name: flask
 image: quay.io/kubernetes-for-developers/flask:0.2.0
 ports:
 - containerPort: 5000
 envFrom:
 - configMapRef:
 name: flask-config
 volumeMounts:
 - name: config
 mountPath: /etc/flask-config
 readOnly: true
 volumes:
 - name: config
 configMap:
 name: flask-config
 initContainers:
      - name: init-myservice
 image: busybox
 command: ['sh', '-c', 'until nslookup redis-master; do echo waiting for redis; sleep 2; done;']

在这种情况下,初始化容器的代码只是一个在 shell 中编写的循环,检查是否有对redis-master的 DNS 条目的响应,并且会一直运行直到成功。如果在redis-master服务建立并具有相关的 DNS 条目之前查看 Pod,您将看到该 Pod 的状态列出为Init:0/1

例如,kubectl get pods:

NAME                  READY STATUS   RESTARTS AGE
flask-f48f89687-8p8nj 0/1   Init:0/1 0        8h
kubectl describe deploy/flask

您可能会注意到,这个输出与之前的例子不匹配;在前面的输出中,命令是在寻找对redis的 DNS 响应,而我们将服务命名为redis-service

在这种情况下,初始化容器将永远无法完成,Pod 将无限期地保持在pending状态。在这种情况下,您需要手动删除部署,或者如果您进行了修改以使其工作,您需要手动删除那些卡在初始化状态的 Pod,否则它们将无法被清理。

一旦初始化容器成功完成,您可以通过 Pod 的kubectl describe输出或者再次通过kubectl get命令暴露的数据来查看结果。

以下是你将从kubectl describe中看到的输出的扩展示例。

describe的输出超出了单个终端页面;你应该继续向下滚动以查看以下内容:

快速交互式测试

如果你试图创建一个快速的一行初始化容器,尤其是当你使用非常简化的容器比如busybox时,交互式地尝试命令通常是很有用的。你想要的命令可能不可用,所以最好快速尝试一下,以验证它是否能按你的期望工作。

要交互式地运行一个busybox容器,并在完成后删除它,你可以使用以下命令:

kubectl run tempinteractive -it --rm --restart=Never --image=busybox -- /bin/sh

然后在容器内尝试这个命令:

处理优雅的关闭

在生命周期钩子中,我们提到了可以定义和启用的 pre-stop 钩子,但如果你正在编写自己的代码,那么尊重 Kubernetes 用于告诉容器关闭的 SIGTERM 信号可能同样容易。

如果你不熟悉 SIGTERM,它是 Linux 内核支持的功能之一——用于向运行中的进程发送中断的一种方式。进程可以监听这些信号,你可以选择它们在接收到时如何响应。有两个信号是你不能“忽略”的,无论你实现了什么,操作系统都会强制执行:SIGKILL 和 SIGSTOP。Kubernetes 在想要关闭容器时使用的信号是 SIGTERM。

你将收到这个信号的事件类型不仅仅是错误或用户触发的删除,还包括当你使用部署所使用的滚动更新机制进行代码更新时。如果你利用了任何自动扩展功能,它也可能发生,这些功能可以动态增加(和减少)replicaSet中的副本数量。

当你响应这个信号时,通常会想要保存任何需要的状态,关闭任何连接,然后终止你的应用程序。

如果您正在创建一个其他人也会通过 Kubernetes 使用的服务,那么您可能想要做的第一件事之一是更改一个内部变量,以触发任何就绪探针以响应false,然后休眠几秒钟,然后进行任何最终操作和终止。这将允许 Kubernetes 中的服务构造重定向任何进一步的连接,并且所有活动连接都可以完成、排空并礼貌地关闭。

一旦 Kubernetes 发送信号,它就会启动一个计时器。该计时器的默认值为 30 秒,如果您需要或希望更长的值,可以在 Pod 规范中使用terminateGracePeriodSeconds的值进行定义。如果容器在计时器到期时尚未退出,Kubernetes 将尝试使用 SIGKILL 信号强制退出。

例如,如果您调用了kubectl delete deploy nodejs,然后看到 Pods 保持一段时间处于Terminating状态,那就是发生了这种情况。

Python 中的 SIGTERM

例如,如果您想在 Python 中处理 SIGTERM,那么您可以导入 signal 模块并引用一个处理程序来执行任何您想要的操作。例如,一个简单的立即关闭并退出的代码可能是:

import signal
import sys

def sigterm_handler(_signo, _stack_frame):
    sys.exit(0)

signal.signal(signal.SIGTERM, sigterm_handler)

信号处理程序的逻辑可以像您的代码要求的那样复杂或简单。

Node.js 中的 SIGTERM

例如,如果您想在 Node.js 中处理 SIGTERM,那么您可以使用在每个 Node.js 进程中隐式创建的 process 模块来处理信号并退出应用程序。与之前的 Python 示例相匹配,一个简单的关闭立即并退出的代码可能如下所示:

/**
 * SIGTERM handler to terminate (semi) gracefully
 */
process.on(process.SIGTERM, function() {
    console.log('Received SIGTERM signal, now shutting down...');
    process.exit(0);
})

总结

在这一章中,我们首先深入了解了 Pod 的生命周期和状态细节,展示了多种揭示相关细节的方式,并描述了 Kubernetes 在运行软件时的内部操作。然后,我们看了一下您的程序可以通过活跃性和就绪性探针提供的反馈循环,并回顾了在 Python 和 Node.js 中启用这些探针的示例。在探针和代码如何与 Kubernetes 清洁地交互之后,我们看了一下启动和初始化以及优雅关闭的常见情况。

在下一章中,我们将看一下如何使用 Kubernetes 和开源提供应用程序的基本可观察性,特别是监控和日志记录。

第六章:Kubernetes 中的后台处理

Kubernetes 包括对一次性(也称为批处理)计算工作的支持,以及支持异步后台工作的常见用例。在本章中,我们将介绍 Kubernetes 的作业概念及其邻居 CronJob。我们还将介绍 Kubernetes 如何处理和支持持久性,以及 Kubernetes 中可用的一些选项。然后,我们将介绍 Kubernetes 如何支持异步后台任务以及 Kubernetes 可以如何表示、操作和跟踪这些任务的方式。我们还将介绍如何设置从消息队列操作的工作代码。

本章涵盖的主题包括:

  • 工作

  • CronJob

  • 使用 Python 和 Celery 的工作队列示例

  • Kubernetes 中的持久性

  • 有状态集

  • 自定义资源定义CRD

工作

到目前为止,我们所涵盖的大部分内容都集中在持续运行的长期进程上。Kubernetes 还支持较短的、离散的软件运行。Kubernetes 中的作业专注于在一定时间内结束并报告成功或失败的离散运行,并构建在与长期运行软件相同的构造之上,因此它们在其核心使用 pod 规范,并添加了跟踪成功完成数量的概念。

最简单的用例是运行一个单独的 pod,让 Kubernetes 处理由于节点故障或重启而导致的任何故障。您可以在作业中使用的两个可选设置是并行性和完成。如果不指定并行性,默认值为1,一次只会安排一个作业。您可以将这两个值都指定为整数,以并行运行多个作业以实现多个完成,并且如果作业是从某种工作队列中工作,则可以不设置完成。

重要的是要知道,完成和并行设置并不是保证 - 因此 Pod 内的代码需要能够容忍多个实例运行。同样,作业需要能够容忍容器在容器失败时重新启动(例如,在使用restartPolicy OnFailure时),以及处理任何初始化或设置,如果在重新启动时发现自己在新的 Pod 上运行(这可能发生在节点故障的情况下)。如果作业使用临时文件、锁定或从本地文件进行工作,它应该在启动时验证状态,并且不应该假定文件始终存在,以防在处理过程中发生故障。

当作业运行完成时,系统不会创建更多的 Pod,但也不会删除 Pod。这样可以让您查询成功或失败的 Pod 状态,并查看 Pod 内容器的任何日志。已经完成的 Pod 不会出现在kubectl get pods的简单运行中,但如果您使用-a选项,它们将会出现。您需要删除已完成的作业,当您使用kubectl delete删除作业时,相关的 Pod 也将被删除和清理。

例如,让我们运行一个示例作业来看看它是如何工作的。一个简单的作业,只需打印hello world,可以使用以下 YAML 指定:

apiVersion: batch/v1 kind: Job metadata:
 name: helloworld spec:
 template: metadata: name: helloworld spec: containers: - name: simple image: busybox command: ["/bin/echo", "'hello world'"] restartPolicy: Never

然后,您可以使用kubectl createkubectl apply来运行此作业:

kubectl apply -f simplejob.yaml

预期的kubectl get jobs命令将显示存在的作业及其当前状态。由于这个作业非常简单,它可能会在您运行命令查看其当前状态之前完成:

kubectl get jobs
NAME         DESIRED   SUCCESSFUL   AGE helloworld   1         1            3d

与 Pod 一样,您可以使用kubectl describe命令获取更详细的状态和输出:

kubectl describe job helloworld
Name:           helloworld Namespace:      default Selector:       controller-uid=cdafeb57-e7c4-11e7-89d4-b29f363a60d7 Labels:         controller-uid=cdafeb57-e7c4-11e7-89d4-b29f363a60d7
 job-name=helloworld Annotations:    kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"batch/v1","kind":"Job","metadata":{"annotations":{},"name":"helloworld","namespace":"default"},"spec":{"backoffLimit":4,"template":{"met... Parallelism:    1 Completions:    1 Start Time:     Sat, 23 Dec 2017 01:36:50 -0800 Pods Statuses:  0 Running / 1 Succeeded / 0 Failed Pod Template:
 Labels:  controller-uid=cdafeb57-e7c4-11e7-89d4-b29f363a60d7 job-name=helloworld Containers: simple: Image:  busybox Port:   <none> Command: /bin/echo 'hello world' Environment:  <none> Mounts:       <none> Volumes:        <none> Events:
 Type    Reason            Age   From            Message ----    ------            ----  ----            ------- Normal  SuccessfulCreate  3d    job-controller  Created pod: helloworld-2b2xt

如果您运行kubectl get pods命令,您将看不到 Podhelloworld-2b2xt在 Pod 列表中,但运行kubectl get pods -a将显示 Pod,包括仍然存在的已完成或失败的 Pod:

NAME                           READY     STATUS      RESTARTS   AGE
helloworld-2b2xt               0/1       Completed   0          3d

如果您只是想亲自查看 Pod 的状态,可以使用kubectl describe获取详细信息,以人类可读的形式显示信息:

kubectl describe pod helloworld-2b2xt

这是一个示例:

如果您像在此示例中一样使用 shell 脚本创建一个简单的作业,很容易出错。在这些情况下,默认情况是 Kubernetes 会重试运行 pod,使得 pod 在系统中处于失败状态供您查看。在这种情况下,设置一个退避限制可以限制系统重试作业的次数。如果您不指定此值,它将使用默认值为六次。

一个命令中的简单错误可能看起来像下面这样:

kubectl describe job helloworld
Name:           helloworld Namespace:      default Selector:       controller-uid=6693f83a-e7c7-11e7-89d4-b29f363a60d7 Labels:         controller-uid=6693f83a-e7c7-11e7-89d4-b29f363a60d7
 job-name=helloworld Annotations:    kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"batch/v1","kind":"Job","metadata":{"annotations":{},"name":"helloworld","namespace":"default"},"spec":{"template":{"metadata":{"name":"h... Parallelism:    1 Completions:    1 Start Time:     Sat, 23 Dec 2017 01:55:26 -0800 Pods Statuses:  0 Running / 0 Succeeded / 6 Failed Pod Template:
 Labels:  controller-uid=6693f83a-e7c7-11e7-89d4-b29f363a60d7 job-name=helloworld Containers: simple: Image:  busybox Port:   <none> Command: /bin/sh echo 'hello world' Environment:  <none> Mounts:       <none> Volumes:        <none> Events:
 Type     Reason                Age   From           Message ----     ------                ----  ----            ------- Normal   SuccessfulCreate      3d    job-controller  Created pod: helloworld-sz6zj Normal   SuccessfulCreate      3d    job-controller  Created pod: helloworld-vtzh7 Normal   SuccessfulCreate      3d    job-controller  Created pod: helloworld-2gh74 Normal   SuccessfulCreate      3d    job-controller  Created pod: helloworld-dfggg Normal   SuccessfulCreate      3d    job-controller  Created pod: helloworld-z2llj Normal   SuccessfulCreate      3d    job-controller  Created pod: helloworld-69d4t Warning  BackoffLimitExceeded  3d    job-controller  Job has reach the specified backoff limit

并查看pods

kubectl get pods -a
NAME               READY     STATUS    RESTARTS   AGE helloworld-2gh74   0/1       Error     0          3d helloworld-69d4t   0/1       Error     0          3d helloworld-dfggg   0/1       Error     0          3d helloworld-sz6zj   0/1       Error     0          3d helloworld-vtzh7   0/1       Error     0          3d helloworld-z2llj   0/1       Error     0          3d

每个 pod 的日志都将可用,因此您可以诊断出了什么问题。

如果您犯了一个错误,那么您可能会想要快速修改作业规范,并使用kubectl apply来修复错误。系统认为作业是不可变的,因此如果您尝试快速修复并应用它,将会收到错误。在处理作业时,最好删除作业并创建一个新的。

作业与 Kubernetes 中其他对象的生命周期无关,因此,如果您考虑使用作业来初始化持久性存储中的数据,请记住您需要协调运行该作业。在您希望在服务启动之前每次检查一些逻辑以预加载数据的情况下,最好使用初始化容器,就像我们在上一章中探讨的那样。

一些常见的适合作业的情况包括将备份加载到数据库中、创建备份、进行一些更深入的系统内省或诊断,或者运行超出带宽清理逻辑。在所有这些情况下,您希望知道您编写的函数已经完成,并且成功运行。在失败的情况下,您可能希望重试,或者仅仅通过日志了解发生了什么。

CronJob

CronJobs 是建立在作业基础上的扩展,允许您指定它们运行的重复计划。该名称源自一个用于调度重复脚本的常见 Linux 实用程序cron。CronJobs 在 Kubernetes 版本 1.7 中是 alpha 版本,在版本 1.8 中转为 beta 版本,并且在版本 1.9 中仍然是 beta 版本。请记住,Kubernetes 规范可能会发生变化,但往往相当稳定,并且具有预期的 beta 实用性,因此 CronJobs 的 v1 版本可能会有所不同,但您可以期望它与本文提供的内容非常接近。

规范与作业密切相关,主要区别在于种类是 CronJob,并且有一个必需的字段 schedule,它接受一个表示运行此作业的时间的字符串。

此字符串的格式是五个数字,可以使用通配符。这些字段表示:

  • 分钟(0-59)

  • 小时(0-23)

  • 月份的日期(1-31)

  • 月份(1-12)

  • 星期几(0-6)

*或?字符可以在这些字段中的任何一个中使用,表示任何值都可以接受。字段还可以包括*/和一个数字,这表示在一些间隔内定期发生的实例,由相关数字指定。这种格式的一些例子是:

  • 12 * * * *:每小时在整点后 12 分钟运行

  • */5 * * * *:每 5 分钟运行

  • 每周六午夜运行

还有一些特殊的字符串可以用于一些更容易阅读的常见事件:

  • @yearly

  • @monthly

  • @weekly

  • @daily

  • @hourly

CronJob 有五个额外的字段,可以指定,但不是必需的。与作业不同,CronJobs 是可变的(就像 pod、部署等一样),因此这些值可以在创建 CronJob 后更改或更新。

第一个是startingDeadlineSeconds,如果指定,将限制作业在 Kubernetes 未满足其指定的启动作业时间限制时可以启动的时间。如果时间超过startingDeadlineSeconds,该迭代将标记为失败。

第二个是concurrencyPolicy,它控制 Kubernetes 是否允许多个相同作业的实例同时运行。默认值为Allow,这将允许多个作业同时运行,备用值为ForbidReplaceForbid将在第一个作业仍在运行时将以下作业标记为失败,而Replace将取消第一个作业并尝试再次运行相同的代码。

第三个字段是suspended,默认值为False,可以用于暂停计划中作业的任何进一步调用。如果作业已经在运行,并且将suspend添加到 CronJob 规范中,那么当前作业将运行到完成,但不会安排任何进一步的作业。

第四和第五个字段是successfulJobsHistoryLimitfailedJobsHistoryLimit,默认值分别为31。默认情况下,Kubernetes 将清理超出这些值的旧作业,但保留最近的成功和失败,包括日志,以便根据需要进行检查。

创建 CronJob 时,您还需要选择(并在规范中定义)restartPolicy。CronJob 不允许Always的默认值,因此您需要在OnFailureNever之间进行选择。

每分钟打印hello world的简单 CronJob 可能如下所示:

apiVersion: batch/v1beta1 kind: CronJob metadata:
 name: helloworld spec:
 schedule: "*/1 * * * *" jobTemplate: spec: template: spec: containers: - name: simple image: busybox command: ["/bin/sh", "-c", "echo", "'hello world'"] restartPolicy: OnFailure

使用kubectl apply -f cronjob.yaml创建此作业后,您可以使用kubectl get cronjob查看摘要输出:

NAME         SCHEDULE      SUSPEND   ACTIVE    LAST SCHEDULE   AGE helloworld   */1 * * * *   False     1         3d              3d

或者,使用kubectl describe cronjob helloworld查看更详细的输出:

Name:                       helloworld Namespace:                  default Labels:                     <none> Annotations:                kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"batch/v1beta1","kind":"CronJob","metadata":{"annotations":{},"name":"helloworld","namespace":"default"},"spec":{"jobTemplate":{"spec":{"... Schedule:                   */1 * * * * Concurrency Policy:         Allow Suspend:                    False Starting Deadline Seconds:  <unset> Selector:                   <unset> Parallelism:                <unset> Completions:                <unset> Pod Template:
 Labels:  <none> Containers: simple: Image:  busybox Port:   <none> Command: /bin/sh -c echo 'hello world' Environment:     <none> Mounts:          <none> Volumes:           <none> Last Schedule Time:  Sat, 23 Dec 2017 02:46:00 -0800 Active Jobs:         <none> Events:
 Type    Reason            Age   From                Message ----    ------            ----  ----                ------- Normal  SuccessfulCreate  3d    cronjob-controller  Created job helloworld-1514025900 Normal  SawCompletedJob   3d    cronjob-controller  Saw completed job: helloworld-1514025900 Normal  SuccessfulCreate  3d    cronjob-controller  Created job helloworld-1514025960 Normal  SawCompletedJob   3d    cronjob-controller  Saw completed job: helloworld-1514025960

从此输出中,您可能会猜到 CronJob 实际上是根据您定义的时间表和规范中的模板创建作业。每个作业都基于 CronJob 的名称获得自己的名称,并且可以独立查看:

您可以使用kubectl get jobs命令查看从前面的 CronJob 定义的时间表创建的作业:

kubectl get jobs
NAME                    DESIRED   SUCCESSFUL   AGE helloworld-1514025900   1         1            3d helloworld-1514025960   1         1            3d **helloworld-1514026020   1         1            3d** 

您还可以使用kubectl get pods-a选项查看从这些作业中创建并运行到完成的 Pod:

kubectl get pods -a
NAME                          READY     STATUS      RESTARTS   AGE helloworld-1514025900-5pj4r   0/1       Completed   0          3d helloworld-1514025960-ckshh   0/1       Completed   0          3d helloworld-1514026020-gjrfh   0/1       Completed   0          3d

使用 Python 和 Celery 的工作队列示例

CronJob 很适合在特定时间表上运行重复任务,但另一个常见的需求是更多或更少地不断处理一系列工作项。作业很适合运行单个任务直到完成,但如果需要处理的事务量足够大,保持不断的处理过程可能更有效。

适应这种工作的常见模式使用消息队列,如下所示:

通过消息队列,您可以拥有一个 API 前端,用于异步创建要运行的工作,将其移动到队列中,然后有多个工作进程从队列中拉取相关工作。亚马逊有一个支持这种处理模式的基于 Web 的服务,称为简单队列服务SQS)。这种模式的巨大好处是将工作人员与请求解耦,因此您可以根据需要独立扩展每个部分。

您可以在 Kubernetes 中做完全相同的事情,将队列作为服务运行,并将连接到该队列的工作程序作为部署。Python 有一个流行的框架 Celery,它可以从消息中进行后台处理,支持多种队列机制。我们将看看如何设置一个示例队列和工作进程,以及如何在 Kubernetes 中利用 Celery 这样的框架。

Celery worker example

Celery 自 2009 年以来一直在开发和使用,早于 Kubernetes 存在。它是为了部署在多台机器上而编写的。这在我们的示例中可以很好地转化为容器。您可以在docs.celeryproject.org/en/latest/获取有关 Celery 的更多详细信息。

在这个例子中,我们将设置一个包含 RabbitMQ 部署和我们自己的容器celery-worker的部署,用来处理来自该队列的作业:

此示例的部署和源代码可在 GitHub 上找到github.com/kubernetes-for-developers/kfd-celery/。您可以使用以下命令获取此代码:

git clone https://github.com/kubernetes-for-developers/kfd-celery -b 0.4.0 cd kfd-celery

RabbitMQ 和配置

此示例使用了一个包含 Bitnami 的 RabbitMQ 的容器。该镜像的源代码可在github.com/bitnami/bitnami-docker-rabbitmq找到,并且容器镜像公开托管在 DockerHub 上hub.docker.com/r/bitnami/rabbitmq/

RabbitMQ 有大量可用的选项,以及多种部署方式,包括集群支持 HA。在这个例子中,我们使用了一个单一的 RabbitMQ 副本在部署中支持名为message-queue的服务。我们还设置了一个ConfigMap,其中包含一些我们可能想要调整的变量,尽管在这个例子中,这些值与容器内的默认值相同。该部署确实使用了持久卷,以便在发生故障时为队列启用持久性。我们将在本章后面详细介绍持久卷以及如何使用它们。

我们创建的ConfigMap将被 RabbitMQ 容器和我们的工作程序部署使用。ConfigMap名为queue-config.yaml,内容如下:

--- apiVersion: v1 kind: ConfigMap metadata:
  name: bitnami-rabbitmq-config data:
  RABBITMQ_USERNAME: "user"
  RABBITMQ_PASSWORD: "bitnami"
  RABBITMQ_VHOST: "/"
  RABBITMQ_NODE_PORT_NUMBER: "5672"
  RABBITMQ_MANAGER_PORT_NUMBER: "15672"
  WORKER_DEBUG_LEVEL: "info"

要部署它,您可以使用以下命令:

kubectl apply -f deploy/queue-config.yaml
configmap "bitnami-rabbitmq-config" created

ConfigMap是基于 Bitnami RabbitMQ 容器的文档创建的,该容器支持通过环境变量设置许多配置项。您可以在 Docker Hub 网页或 GitHub 源中查看容器可以接受的所有细节。在我们的情况下,我们设置了一些最常见的值。

注意:您可能希望使用密钥而不是在ConfigMap中包含值来更正确地设置用户名和密码。

您可以查看部署的规范:

这是如何部署实例的:

kubectl apply -f deploy/rabbitmq.yml service "message-queue" created persistentvolumeclaim "rabbitmq-pv-claim" created deployment "rabbitmq" created

Celery worker

为了创建一个 worker,我们制作了一个非常类似于 Flask 容器的自己的容器镜像。Dockerfile 使用 Alpine Linux,并明确将 Python 3 加载到该镜像中,然后从requirements.txt文件安装要求,并添加了两个 Python 文件。第一个celery_conf.py是直接从 Celery 文档中获取的一些任务的 Python 定义。第二个submit_tasks.py是一个简短的示例,旨在交互式运行以创建工作并将其发送到队列中。容器还包括两个 shell 脚本:run.shcelery_status.sh

在所有这些情况下,我们使用了从前面的ConfigMap中获取的环境变量来设置 worker 的日志输出,以及与 Kubernetes 内的 RabbitMQ 通信的主机、用户名和密码。

Dockerfile 使用run.sh脚本作为其命令,因此我们可以使用此 shell 脚本设置任何环境变量并调用 Celery。因为 Celery 最初是作为一个命令行工具编写的,所以使用 shell 脚本来设置和调用您想要的内容非常方便。以下是对run.sh的更详细介绍:

该脚本设置了两个 shell 脚本选项,-e-x。第一个(-e)是为了确保如果我们在脚本中犯了拼写错误或命令返回错误,脚本本身将返回错误。第二个(-x)将在STDOUT中回显脚本中调用的命令,因此我们可以在容器日志输出中看到它。

下一行中的DEBUG_LEVEL使用 shell 查找默认环境变量:WORKER_DEBUG_LEVEL。如果设置了,它将使用它,而WORKER_DEBUG_LEVEL是早期添加到ConfigMap中的。如果值未设置,它将使用默认值info,因此如果ConfigMap中缺少该值,我们仍将在此处有一个合理的值。

如前所述,Celery 是作为命令行实用程序编写的,并利用 Python 的模块加载来完成其工作。 Python 模块加载包括从当前目录工作,因此我们明确更改为包含 Python 代码的目录。最后,脚本调用命令启动 Celery worker。

我们在脚本celery_status.sh中使用类似的结构,该脚本用于提供用于 worker 容器的活动性和可用性探针的 exec 命令,其关键思想是如果命令celery status返回而不出现错误,则容器正在有效地与 RabbitMQ 通信,并且应完全能够处理任务。

包含将被调用的逻辑的代码都在celery_conf.py中:

您可以看到,我们再次利用环境变量来获取与 RabbitMQ 通信所需的值(主机名、用户名、密码和vhost),并从环境变量中组装这些默认值,如果未提供。主机名默认值(message-queue)也与我们的服务定义中的服务名称匹配,该服务定义了 RabbitMQ 的前端,为我们提供了一个稳定的默认值。代码的其余部分来自 Celery 文档,提供了两个示例任务,我们也可以分别导入和使用。

您可以使用以下命令部署 worker:

kubectl apply -f deploy/celery-worker.yaml

这应该报告已创建的部署,例如:

deployment "celery-worker" created

现在,您应该有两个部署一起运行。您可以使用kubectl get pods来验证这一点:

NAME                            READY     STATUS    RESTARTS   AGE celery-worker-7c59b58df-qptlc   1/1       Running   0          11m rabbitmq-6c656f667f-rp2zm       1/1       Running   0          14m

要更加交互地观察系统,请运行此命令:

kubectl log deploy/celery-worker -f

这将从celery-worker流式传输日志,例如:

这将显示celery-worker部署的日志,因为它们发生。打开第二个终端窗口并调用以下命令以运行一个临时 pod 并获得交互式 shell:

kubectl run -i --tty \ --image quay.io/kubernetes-for-developers/celery-worker:0.4.0 \ --restart=Never --image-pull-policy=Always --rm testing /bin/sh

在 shell 中,您现在可以运行脚本来生成一些任务供 worker 处理:

python3 submit_tasks.py

这个脚本的一个例子是:

这个脚本将无限期地运行,大约每五秒调用一次 worker 中的两个示例任务,在显示日志的窗口中,您应该看到输出更新,显示来自 Celery worker 的记录结果:

Kubernetes 的持久性

到目前为止,我们所有的例子,甚至代码,都基本上是无状态的。在上一章中,我们介绍了使用 Redis 的容器,但没有为它指定任何特殊的东西。默认情况下,Kubernetes 将假定与 pod 关联的任何资源都是临时的,如果节点失败或部署被删除,所有关联的资源都可以并且将被删除。

也就是说,我们所做的几乎所有工作都需要在某个地方存储和维护状态——数据库、对象存储,甚至是持久的内存队列。Kubernetes 包括对持久性的支持,截至目前为止,它仍在快速变化和发展。

Kubernetes 最早的支持是卷,可以由集群管理员定义,并且我们已经看到了一些这种构造的变体,配置被暴露到容器中使用 Downward API 在第四章中,声明式基础设施

另一种可以轻松使用的卷是emptyDir,您可以在 pod 规范中使用它来创建一个空目录,并将其挂载到一个或多个容器中。这通常在本地节点可用的存储上创建,但包括一个选项来指定memory的介质,您可以使用它来创建一个临时的内存支持文件系统。这会占用节点上更多的内存,但为您的 pod 创建一个非常快速的临时文件系统。如果您的代码想要在磁盘上使用一些临时空间,保持定期的检查点,或者加载临时内容,这可能是一个非常好的管理空间的方法。

正如我们在配置中指定的那样,当您使用卷时,您将其指定在卷下,并在volumeMounts下进行相关条目,指示您在每个容器上使用它的位置。

我们可以修改我们的 Flask 示例应用程序,使其具有一个内存支持的临时空间:

 spec: containers: - name: flask image: quay.io/kubernetes-for-developers/flask:0.3.0 imagePullPolicy: Always ports: - containerPort: 5000 volumeMounts: - name: config mountPath: /etc/flask-config readOnly: true - name: cache-volume mountPath: /opt/cache volumes: - name: config configMap: name: flask-config - name: cache-volume emptyDir: medium: Memory

如果我们部署规范的这个版本并在容器中打开一个交互式 shell,你可以看到/opt/cache被列为tmpfs类型的卷:

df -h Filesystem                Size      Used Available Use% Mounted on overlay                  15.3G      1.7G     12.7G  12% / tmpfs                  1000.1M         0   1000.1M   0% /dev tmpfs                  1000.1M         0   1000.1M   0% /sys/fs/cgroup tmpfs                  1000.1M         0   1000.1M   0% /opt/cache /dev/vda1                15.3G      1.7G     12.7G  12% /dev/termination-log /dev/vda1                15.3G      1.7G     12.7G  12% /etc/flask-config /dev/vda1                15.3G      1.7G     12.7G  12% /etc/resolv.conf /dev/vda1                15.3G      1.7G     12.7G  12% /etc/hostname /dev/vda1                15.3G      1.7G     12.7G  12% /etc/hosts shm                      64.0M         0     64.0M   0% /dev/shm tmpfs                  1000.1M     12.0K   1000.1M   0% /run/secrets/kubernetes.io/serviceaccount tmpfs                  1000.1M         0   1000.1M   0% /proc/kcore tmpfs                  1000.1M         0   1000.1M   0% /proc/timer_list tmpfs                  1000.1M         0   1000.1M   0% /proc/timer_stats tmpfs                  1000.1M         0   1000.1M   0% /sys/firmware

如果我们没有指定类型为Memory的介质,那么该目录将显示在本地磁盘上:

df -h Filesystem                Size      Used Available Use% Mounted on overlay                  15.3G      1.7G     12.7G  12% / tmpfs                  1000.1M         0   1000.1M   0% /dev tmpfs                  1000.1M         0   1000.1M   0% /sys/fs/cgroup /dev/vda1                15.3G      1.7G     12.7G  12% /dev/termination-log /dev/vda1                15.3G      1.7G     12.7G  12% /etc/flask-config /dev/vda1                15.3G      1.7G     12.7G  12% /opt/cache /dev/vda1                15.3G      1.7G     12.7G  12% /etc/resolv.conf /dev/vda1                15.3G      1.7G     12.7G  12% /etc/hostname /dev/vda1                15.3G      1.7G     12.7G  12% /etc/hosts shm                      64.0M         0     64.0M   0% /dev/shm tmpfs                  1000.1M     12.0K   1000.1M   0% /run/secrets/kubernetes.io/serviceaccount tmpfs                  1000.1M         0   1000.1M   0% /proc/kcore tmpfs                  1000.1M         0   1000.1M   0% /proc/timer_list tmpfs                  1000.1M         0   1000.1M   0% /proc/timer_stats tmpfs                  1000.1M         0   1000.1M   0% /sys/firmware

如果您在云服务提供商上使用卷,那么您可以使用他们的持久卷之一。在这些情况下,您需要在云服务提供商那里创建一个持久磁盘,该磁盘对您的 Kubernetes 集群中的节点是可访问的,但这样可以使数据存在于任何 pod 或节点的生命周期之外。每个云提供商的卷都是特定于该提供商的,例如awsElasticBlockStoreazureDiskgcePersistentDisk

还有许多其他类型的卷可用,大多数取决于您的集群是如何设置的以及该设置中可能有什么可用的。您可以从卷的正式文档kubernetes.io/docs/concepts/storage/volumes/中了解所有支持的卷。

持久卷和持久卷索赔

如果您想要使用持久卷,而不受构建集群的特定位置的限制,您可能想要利用两个较新的 Kubernetes 资源:PersistentVolumePersistentVolumeClaim。这些资源将提供卷的具体细节与您期望使用这些卷的方式分开,两者都属于动态卷分配的概念,这意味着当您将代码部署到 Kubernetes 时,系统应该从已经识别的磁盘中提供任何持久卷。Kubernetes 管理员将需要指定至少一个,可能更多的存储类,用于定义可供集群使用的持久卷的一般行为和后备存储。如果您在亚马逊网络服务、谷歌计算引擎或微软的 Azure 上使用 Kubernetes,这些公共服务都预定义了存储类可供使用。您可以在文档kubernetes.io/docs/concepts/storage/storage-classes/中查看默认的存储类及其定义。如果您在本地使用 Minikube 进行尝试,它也有一个默认的存储类定义,使用的卷类型是HostPath

定义PersistentVolumeClaim以与部署中的代码一起使用非常类似于使用EmptyDir定义配置卷或缓存,唯一的区别是您需要在引用它之前创建persistentVolumeClaim资源。

我们可能用于 Redis 存储的persistentVolumeClaim示例可能是:

apiVersion: v1 kind: PersistentVolumeClaim metadata:
 name: redis-pv-claim labels: app: redis-master spec:
 accessModes: - ReadWriteOnce resources: requests: storage: 1Gi

这将创建一个可供我们容器使用的 1GB 卷。我们可以将其添加到 Redis 容器中,通过名称引用这个persistentVolumeClaim来给它提供持久存储:

apiVersion: apps/v1beta1 kind: Deployment metadata:
 name: redis-master spec:
 replicas: 1 template: metadata: labels: app: redis role: master tier: backend spec: containers: - name: redis-master image: redis:4 ports: - containerPort: 6379 volumeMounts: - name: redis-persistent-storage mountPath: /data volumes: - name: redis-persistent-storage persistentVolumeClaim: claimName: redis-pv-claim

选择/datamountPath是为了与 Redis 容器的构建方式相匹配。如果我们查看该容器的文档(来自hub.docker.com/_/redis/),我们可以看到内置配置期望所有数据都从/data路径使用,因此我们可以用我们自己的persistentVolumeClaim覆盖该路径,以便用一些能够在部署的生命周期之外存在的东西来支持该空间。

如果您将这些更改部署到 Minikube,您可以看到集群中反映出的结果资源:

kubectl get persistentvolumeclaims NAME             STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE redis-pv-claim   Bound     pvc-f745c6f1-e7d8-11e7-89d4-b29f363a60d7   1Gi        RWO            standard       3d kubectl get persistentvolumes NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM                    STORAGECLASS   REASON    AGE pvc-f745c6f1-e7d8-11e7-89d4-b29f363a60d7   1Gi        RWO            Delete           Bound     default/redis-pv-claim   standard                 3d

我们还可以打开一个交互式终端进入 Redis 实例,看看它是如何设置的:

kubectl exec -it redis-master-6f944f6c8b-gm2cb -- /bin/sh # df -h Filesystem      Size  Used Avail Use% Mounted on overlay          16G  1.8G   13G  12% / tmpfs          1001M     0 1001M   0% /dev tmpfs          1001M     0 1001M   0% /sys/fs/cgroup /dev/vda1        16G  1.8G   13G  12% /data shm              64M     0   64M   0% /dev/shm tmpfs          1001M   12K 1001M   1% /run/secrets/kubernetes.io/serviceaccount tmpfs          1001M     0 1001M   0% /sys/firmware

有状态集

在动态配置之后,当您考虑持久性系统时,无论它们是经典数据库、键值数据存储、内存缓存还是基于文档的数据存储,通常希望具有某种冗余和故障转移的方式。 ReplicaSets 和部署在支持某些功能方面走得相当远,特别是对于持久卷,但是将它们更完全地集成到 Kubernetes 中将极大地有利于这些系统,以便我们可以利用 Kubernetes 来处理这些系统的生命周期和协调。这项工作的起点是 Stateful Sets,它们的工作方式类似于部署和 ReplicaSet,因为它们管理一组 pod。

Stateful Sets 与其他系统不同,它们还支持每个 pod 具有稳定的、唯一的标识和特定的有序扩展,无论是向上还是向下。 Stateful Sets 在 Kubernetes 中相对较新,首次出现在 Kubernetes 1.5 中,并在 1.9 版本中进入 beta。 Stateful Sets 还与我们之前提到的特定服务密切合作,即无头服务,需要在 Stateful Set 之前创建,并负责 pod 的网络标识。

作为提醒,无头服务是一种没有特定集群 IP 的服务,而是为与其关联的所有 pods 提供独特的服务标识作为单独的端点。这意味着任何使用该服务的系统都需要知道该服务具有特定标识的端点,并且需要能够与其想要通信的端点进行通信。当创建一个与无头服务相匹配的 Stateful Set 时,pods 将根据 Stateful Set 的名称和序号获得一个标识。例如,如果我们创建了一个名为 datastore 的 Stateful Set 并请求了三个副本,那么 pods 将被创建为 datastore-0datastore-1datastore-2。Stateful Sets 还有一个 serviceName 字段,该字段包含在服务的域名中。以完成这个示例,如果我们将 serviceName 设置为 db,那么为 pods 创建的关联 DNS 条目将是:

  • datastore-0.db.[namespace].svc.cluster.local

  • datastore-1.db.[namespace].svc.cluster.local

  • datastore-2.db.[namespace].svc.cluster.local

随着副本数量的变化,Stateful Set 也会明确而谨慎地添加和删除 pods。它会按照从最高编号开始顺序终止 pods,并且不会在较低编号的 pods 报告ReadyRunning之前终止较高编号的 pods。从 Kubernetes 1.7 开始,Stateful Sets 引入了一个可选字段 podManagementPolicy 来改变这一行为。默认值 OrderedReady 的操作如上所述,另一个选项 Parallel 则不会按顺序操作,也不需要较低编号的 pods 处于RunningReady状态才能终止一个 pod。

滚动更新,类似于部署,对于 Stateful Sets 也略有不同。它由updateStrategy可选字段定义,如果未明确设置,则使用OnDelete设置。使用此设置,Kubernetes 不会删除旧的 pod,即使在规范更新后,需要您手动删除这些 pod。当您这样做时,系统将根据更新后的规范自动重新创建 pod。另一个值是RollingUpdate,它更类似于部署,会自动终止和重新创建 pod,但会明确遵循顺序,并验证 pod 在继续更新下一个 pod 之前是否准备就绪和运行RollingUpdate还有一个额外的(可选)字段,partition,如果指定了一个数字,将使RollingUpdate自动在一部分 pod 上操作。例如,如果分区设置为3,并且有6个副本,则只有 pod 345会在规范更新时自动更新。Pod 012将被保留,即使它们被手动删除,它们也将以先前的版本重新创建。分区功能可用于分阶段更新或执行分阶段部署。

使用 Stateful Set 的 Node.js 示例

应用程序内的代码不需要 Stateful Set 机制,但让我们将其用作易于理解的更新,以展示您可能如何使用 Stateful Set 以及如何观察其运行。

此更新的代码可在 GitHub 上的项目中找到:github.com/kubernetes-for-developers/kfd-nodejs,分支为 0.4.0。该项目的代码没有更改,只是将部署规范更改为 Stateful Set。您可以使用以下命令获取此版本的代码:

git clone https://github.com/kubernetes-for-developers/kfd-nodejs -b 0.4.0 cd kfd-nodejs

服务定义已更改,删除了Nodeport类型,并将clusterIP设置为None。现在nodejs-service的新定义如下:

kind: Service apiVersion: v1 metadata:
 name: nodejs-service spec:
 ports: - port: 3000 name: web clusterIP: None selector: app: nodejs

这将设置一个无头服务,用于与 Stateful Set 一起使用。从部署到 Stateful Set 的更改同样简单,将类型Deployment替换为类型StatefulSet,并添加serviceName、副本和设置选择器的值。我还添加了一个带有持久卷索赔的数据存储挂载,以展示它如何与您现有的规范集成。现有的ConfigMaplivenessProbereadinessProbe设置都得到了保留。最终的StatefulSet规范现在如下所示:

apiVersion: apps/v1beta1 kind: StatefulSet metadata:
 name: nodejs spec:
 serviceName: "nodejs" replicas: 5 selector: matchLabels: app: nodejs template: metadata: labels: app: nodejs spec: containers: - name: nodejs image: quay.io/kubernetes-for-developers/nodejs:0.3.0 imagePullPolicy: Always ports: - containerPort: 3000 name: web envFrom: - configMapRef: name: nodejs-config volumeMounts: - name: config mountPath: /etc/nodejs-config readOnly: true - name: datastore mountPath: /opt/data livenessProbe: httpGet: path: /probes/alive port: 3000 initialDelaySeconds: 1 periodSeconds: 5 readinessProbe: httpGet: path: /probes/ready port: 3000 initialDelaySeconds: 5 periodSeconds: 5 volumes: - name: config configMap: name: nodejs-config updateStrategy: type: RollingUpdate volumeClaimTemplates: - metadata: name: datastore spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi

自上一章中更新了代码以使用带有就绪探针的 Redis 后,我们将希望确保我们的 Redis 已经运行起来,以便这个 Stateful Set 能够继续进行。您可以使用以下命令部署更新后的 Redis 服务定义集:

kubectl apply -f deploy/redis.yaml

现在,我们可以利用kubectl get的观察选项(-w)来观察 Kubernetes 如何设置 Stateful Set 并进行仔细的进展。打开一个额外的终端窗口并运行以下命令:

kubectl get pods -w -l app=nodejs

起初,您不应该看到任何输出,但随着 Kubernetes 在 Stateful Set 中的进展,更新将会出现。

在您原始的终端窗口中,使用以下命令部署我们已更新为StatefulSet的规范:

kubectl apply -f deploy/nodejs.yaml

您应该会看到serviceconfigmapstatefulset对象都已创建的响应:

service "nodejs-service" unchanged configmap "nodejs-config" unchanged statefulset "nodejs" created

在您观看 pod 的窗口中,当第一个容器上线时,您应该会看到输出开始出现。每当观察触发器发现我们设置的描述符(-l app=nodejs)中的一个 pod 有更新时,输出中会出现一行:

NAME       READY     STATUS    RESTARTS   AGE nodejs-0   0/1       Pending   0          2h nodejs-0   0/1       Pending   0         2h nodejs-0   0/1       ContainerCreating   0         2h nodejs-0   0/1       Running   0         2h nodejs-0   1/1       Running   0         2h nodejs-1   0/1       Pending   0         2h nodejs-1   0/1       Pending   0         2h nodejs-1   0/1       ContainerCreating   0         2h nodejs-1   0/1       Running   0         2h nodejs-1   1/1       Running   0         2h nodejs-2   0/1       Pending   0         2h nodejs-2   0/1       Pending   0         2h nodejs-2   0/1       ContainerCreating   0         2h nodejs-2   0/1       Running   0         2h nodejs-2   1/1       Running   0         2h nodejs-3   0/1       Pending   0         2h nodejs-3   0/1       Pending   0         2h

我们设置的定义有五个副本,因此总共会生成五个 pod。您可以使用以下命令查看该部署的状态:

kubectl get sts nodejs
NAME      DESIRED   CURRENT   AGE nodejs    5         5         2h

在上述命令中,stsstatefulset的缩写。您还可以使用以下命令以人类可读的形式获得当前状态的更详细视图:

kubectl describe sts nodejs

如果您编辑规范,将副本更改为两个,然后应用更改,您将看到 pod 按照设置的相反顺序被拆除,最高序数号先。以下命令:

kubectl apply -f deploy/nodejs.yml

应该报告:

service "nodejs-service" unchanged configmap "nodejs-config" unchanged statefulset "nodejs" configured

在观看 pod 的窗口中,您将看到nodejs-4开始终止,并且会一直持续到nodejs-3,然后是nodejs-2终止。

如果您运行一个临时的pod来查看 DNS:

kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh

您可以使用nslookup命令验证pods的 DNS 值:

/ # nslookup nodejs-1.nodejs-service
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name: nodejs-1.nodejs-service
Address 1: 172.17.0.6 nodejs-1.nodejs-service.default.svc.cluster.local

/ # nslookup nodejs-0.nodejs-service
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name: nodejs-0.nodejs-service
Address 1: 172.17.0.4 nodejs-0.nodejs-service.default.svc.cluster.local

自定义资源定义

Stateful Sets 并不自动匹配所有可用的持久存储,其中一些甚至对管理应用程序的生命周期有更复杂的逻辑要求。随着 Kubernetes 考虑如何支持扩展其控制器以支持更复杂的逻辑,该项目从 Operators 的概念开始,即可以包含在 Kubernetes 项目中的外部代码,并且截至 Kubernetes 1.8 版本已经发展为更明确地使用CustomResourceDefinitions。自定义资源扩展了 Kubernetes API,并允许创建自定义 API 对象,并与自定义控制器匹配,您还可以将其加载到 Kubernetes 中以处理这些对象的生命周期。

自定义资源定义超出了我们在本书中将涵盖的范围,尽管您应该意识到它们的存在。您可以在项目的文档站点上获取有关自定义资源定义以及如何扩展 Kubernetes 的更多详细信息:kubernetes.io/docs/concepts/api-extension/custom-resources/.

有许多通过开源项目提供的 Operators 可利用自定义资源定义来管理 Kubernetes 中特定的应用程序。CoreOS 团队支持用于管理 Prometheus 和etcd的 Operators 和自定义资源。还有一个名为 Rook 的开源存储资源和相关技术,它使用自定义资源定义来运行。

截至目前,如何最好地在 Kubernetes 中运行持久存储的广泛集合仍在不断发展。有许多示例可以演示如何在 Kubernetes 中运行您选择的数据库或 NoSQL 数据存储,同时还支持冗余和故障转移。这些系统大多是通过各种机制来支持其管理,其中很少有系统对自动扩展和冗余提供了很多支持。有许多支持各种数据存储的技术可作为示例。一些更复杂的系统使用 Operators 和这些自定义资源定义;其他系统使用一组 Pod 和容器以更简单的复制集来实现其目标。

总结

在本章中,我们回顾了 Kubernetes 提供的作业和 CronJobs,以支持批处理和定期批处理处理。我们还通过一个 Python 示例来了解如何使用 RabbitMQ 设置 Celery 工作队列,并配置这两个部署以共同工作。然后,我们看了一下 Kubernetes 如何通过卷、PersistentVolume和其自动创建部署所需卷的PersistentVolumeClaims的概念来提供持久性。Kubernetes 还支持 Stateful Sets,用于需要稳定标识和持久卷的部署变体,我们看了一个简单的 Node.js 示例,将我们之前的部署示例转换为 Stateful Set。最后,我们通过查看用于扩展 Kubernetes 的自定义资源定义来结束本章。

在下一章中,我们开始研究如何利用 Kubernetes 获取所有这些结构的信息。我们将回顾如何捕获和查看指标,利用 Kubernetes 和其他开源项目,以及从 Kubernetes 鼓励的水平扩展系统中整理日志的示例。

第七章:监控和指标

在之前的章节中,我们调查了 Kubernetes 对象和资源中使用的声明性结构。以 Kubernetes 帮助我们运行软件为最终目标,在本章中,我们将看看在更大规模运行应用程序时,如何获取更多信息,以及我们可以用于此目的的一些开源工具。Kubernetes 已经在收集和使用有关集群节点利用情况的一些信息,并且在 Kubernetes 内部有越来越多的能力开始收集特定于应用程序的指标,甚至使用这些指标作为管理软件的控制点。

在本章中,我们将深入探讨基本可观察性的这些方面,并介绍如何为您的本地开发使用设置它们,以及如何利用它们来收集、聚合和公开软件运行的详细信息,当您扩展它时。本章的主题将包括:

  • 内置指标与 Kubernetes

  • Kubernetes 概念-服务质量

  • 使用 Prometheus 捕获指标

  • 安装和使用 Grafana

  • 使用 Prometheus 查看应用程序指标

内置指标与 Kubernetes

Kubernetes 内置了一些基本的仪表来了解集群中每个节点消耗了多少 CPU 和内存。确切地捕获了什么以及如何捕获它在最近的 Kubernetes 版本(1.5 到 1.9)中正在迅速发展。许多 Kubernetes 安装将捕获有关底层容器使用的资源的信息,使用一个名为 cAdvisor 的程序。这段代码是由 Google 创建的,用于收集、聚合和公开容器的操作指标,作为能够知道最佳放置新容器的关键步骤,基于节点的资源和资源可用性。

Kubernetes 集群中的每个节点都将运行并收集信息的 cAdvisor,并且这反过来又被kubelet使用,这是每个节点上负责启动、停止和管理运行容器所需的各种资源的本地代理。

cAdvisor 提供了一个简单的基于 Web 的 UI,您可以手动查看任何节点的详细信息。如果您可以访问节点的端口4194,那么这是默认位置,可以公开 cAdvisor 的详细信息。根据您的集群设置,这可能不容易访问。在使用 Minikube 的情况下,它很容易直接可用。

如果您已安装并运行 Minikube,可以使用以下命令:

minikube ip

获取运行单节点 Kubernetes 集群的开发机器上虚拟机的 IP 地址,可以访问运行的 cAdvisor,然后在浏览器中导航到该 IP 地址的4194端口。例如,在运行 Minikube 的 macOS 上,您可以使用以下命令:

open http://$(minikube ip):4194/

然后您将看到一个简单的 UI,显示类似于这样的页面:

向下滚动一点,您将看到一些仪表和信息表:

下面是一组简单的图表,显示了 CPU、内存、网络和文件系统的使用情况。这些图表和表格将在您观看时更新和自动刷新,并代表了 Kubernetes 正在捕获的有关集群的基本信息。

Kubernetes 还通过其自己的 API 提供有关自身(其 API 服务器和相关组件)的指标。在通过kubectl代理使 API 可用后,您可以使用curl命令直接查看这些指标:

kubectl proxy

并在单独的终端窗口中:

curl http://127.0.0.1:8001/metrics

许多 Kubernetes 的安装都使用一个叫做 Heapster 的程序来从 Kubernetes 和每个节点的 cAdvisor 实例中收集指标,并将它们存储在诸如 InfluxDB 之类的时间序列数据库中。从 Kubernetes 1.9 开始,这个开源项目正在从 Heapster 进一步转向可插拔的解决方案,常见的替代方案是 Prometheus,它经常用于短期指标捕获。

如果您正在使用 Minikube,可以使用minikube插件轻松将 Heapster 添加到本地环境中。与仪表板一样,这将在其自己的基础设施上运行 Kubernetes 的软件,这种情况下是 Heapster、InfluxDB 和 Grafana。

这将在 Minikube 中启用插件,您可以使用以下命令:

minikube addons enable heapster
heapster was successfully enabled

在后台,Minikube 将启动并配置 Heapster、InfluxDB 和 Grafana,并将其创建为一个服务。您可以使用以下命令:

minikube addons open heapster

这将打开一个到 Grafana 的浏览器窗口。该命令将在设置容器时等待,但当服务端点可用时,它将打开一个浏览器窗口:

Grafana 是一个用于显示图表和从常见数据源构建仪表板的单页面应用程序。在 Minikube Heapster 附加组件创建的版本中,Grafana 配置了两个仪表板:集群和 Pods。如果在默认视图中选择标记为“主页”的下拉菜单,您可以选择其他仪表板进行查看。

在 Heapster、InfluxDB 和 Grafana 协调收集和捕获环境的一些基本指标之前,可能需要一两分钟,但相当快地,您可以转到其他仪表板查看正在运行的信息。例如,我在本书的前几章中部署了所有示例应用程序,并转到了集群仪表板,大约 10 分钟后,视图看起来像这样:

通过仪表板向下滚动,您将看到节点的 CPU、内存、文件系统和网络使用情况,以及整个集群的视图。您可能会注意到 CPU 图表有三条线被跟踪——使用情况、限制和请求——它们与实际使用的资源、请求的数量以及对 pod 和容器设置的任何限制相匹配。

如果切换到 Pods 仪表板,您将看到该仪表板中有当前在集群中运行的所有 pod 的选择,并提供每个 pod 的详细视图。在这里显示的示例中,我选择了我们部署的flask示例应用程序的 pod:

向下滚动,您可以看到包括内存、CPU、网络和磁盘利用率在内的图表。Heapster、Grafana 和 InfluxDB 的集合将自动记录您创建的新 pod,并且您可以在 Pods 仪表板中选择命名空间和 pod 名称。

Kubernetes 概念-服务质量

当在 Kubernetes 中创建一个 pod 时,它也被分配了一个服务质量类,这是基于请求时提供的有关 pod 的数据。这在调度过程中提供了一些前期保证,并在后续管理 pod 本身时使用。支持的三种类别是:

  • 保证

  • 可突发

  • 最佳努力

分配给您的 Pod 的类别是基于您在 Pod 内的容器中报告的 CPU 和内存利用率的资源限制和请求。在先前的示例中,没有一个容器被分配请求或限制,因此当它们运行时,所有这些 Pod 都被分类为BestEffort

资源请求和限制在 Pod 内的每个容器上进行定义。如果我们为容器添加请求,我们要求 Kubernetes 确保集群具有足够的资源来运行我们的 Pod(内存、CPU 或两者),并且它将作为调度的一部分验证可用性。如果我们添加限制,我们要求 Kubernetes 监视 Pod,并在容器超出我们设置的限制时做出反应。对于限制,如果容器尝试超出 CPU 限制,容器将被简单地限制到定义的 CPU 限制。如果超出了内存限制,容器将经常被终止,并且您可能会在终止的容器的reason描述中看到错误消息OOM killed

如果设置了请求,Pod 通常被设置为“可突发”的服务类别,但有一个例外,即当同时设置了限制,并且该限制的值与请求相同时,将分配“保证”服务类别。作为调度的一部分,如果 Pod 被认为属于“保证”服务类别,Kubernetes 将在集群内保留资源,并且在超载时,会倾向于首先终止和驱逐BestEffort容器,然后是“可突发”容器。集群通常需要预期会失去资源容量(例如,一个或多个节点失败)。在这些情况下,一旦将“保证”类别的 Pod 调度到集群中,它将在面对此类故障时具有最长的寿命。

我们可以更新我们的flask示例 Pod,以便它将以“保证”的服务质量运行,方法是为 CPU 和内存都添加请求和限制:

 spec: containers: - name: flask image: quay.io/kubernetes-for-developers/flask:0.4.0 resources: limits: memory: "100Mi" cpu: "500m" requests: memory: "100Mi" cpu: "500m"

这为 CPU 和内存都设置了相同值的请求和限制,例如,100 MB 的内存和大约半个核心的 CPU 利用率。

通常认为,在生产模式下运行的所有容器和 Pod,至少应该定义请求,并在最理想的情况下也定义限制,这被认为是最佳实践。

为您的容器选择请求和限制

如果您不确定要使用哪些值来设置容器的请求和/或限制,确定这些值的最佳方法是观察它们。使用 Heapster、Prometheus 和 Grafana,您可以看到每个 pod 消耗了多少资源。

有一个三步过程,您可以使用您的代码来查看它所占用的资源:

  1. 运行您的代码并查看空闲时消耗了多少资源

  2. 为您的代码添加负载并验证负载下的资源消耗

  3. 设置了约束条件后,再运行一个持续一段时间的负载测试,以确保您的代码符合定义的边界

第一步(空闲时审查)将为您提供一个良好的起点。利用 Grafana,或者利用您集群节点上可用的 cAdvisor,并简单地部署相关的 pod。在前面的示例中,我们在本书的早期示例中使用 flask 示例进行了这样的操作,您可以看到一个空闲的 flask 应用程序大约消耗了 3 毫核(.003% 的核心)和大约 35 MB 的 RAM。这为请求 CPU 和内存提供了一个预期值。

第二步通常最好通过运行逐渐增加的负载测试(也称为坡道负载测试)来查看您的 pod 在负载下的反应。通常,您会看到负载随请求线性增加,然后产生一个弯曲或拐点,开始变得瓶颈。您可以查看相同的 Grafana 或 cAdvisor 面板,以显示负载期间的利用率。

如果您想生成一些简单的负载,可以使用诸如 Apache benchmark(httpd.apache.org/docs/2.4/programs/ab.html)之类的工具生成一些特定的负载点。例如,要运行一个与 Flask 应用程序配合使用的交互式容器,可以使用以下命令:

kubectl run -it --rm --restart=Never \
--image=quay.io/kubernetes-for-developers/ab quicktest -- sh

此镜像已安装了 curlab,因此您可以使用此命令验证您是否可以与我们在早期示例中创建的 Flask 服务进行通信:

curl -v http://flask-service.default:5000/

这应该返回一些冗长的输出,显示连接和基本请求如下:

* Trying 10.104.90.234...
* TCP_NODELAY set
* Connected to flask-service.default (10.104.90.234) port 5000 (#0)
> GET / HTTP/1.1
> Host: flask-service.default:5000
> User-Agent: curl/7.57.0
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Type: text/html; charset=utf-8
< Content-Length: 10
< Server: Werkzeug/0.13 Python/3.6.3
< Date: Mon, 08 Jan 2018 02:22:26 GMT
<
* Closing connection 0

一旦您验证了一切都按您的预期运行,您可以使用 ab 运行一些负载:

ab -c 100 -n 5000 http://flask-service.default:5000/ 
This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking flask-service.default (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests
Server Software: Werkzeug/0.13 Server Hostname: flask-service.default
Server Port: 5000
Document Path: /
Document Length: 10 bytes
Concurrency Level: 100
Time taken for tests: 3.454 seconds
Complete requests: 5000
Failed requests: 0
Total transferred: 810000 bytes
HTML transferred: 50000 bytes
Requests per second: 1447.75 [#/sec] (mean)
Time per request: 69.072 [ms] (mean)
Time per request: 0.691 [ms] (mean, across all concurrent requests)
Transfer rate: 229.04 [Kbytes/sec] received

Connection Times (ms)
 min mean[+/-sd] median max
Connect: 0 0 0.3 0 3
Processing: 4 68 7.4 67 90
Waiting: 4 68 7.4 67 90
Total: 7 68 7.2 67 90

Percentage of the requests served within a certain time (ms)
 50% 67
 66% 69
 75% 71
 80% 72
 90% 77
 95% 82
 98% 86
 99% 89
 100% 90 (longest request)

您将看到 cAdvisor 中资源使用量的相应增加,或者大约一分钟后,在 Heapster 中看到 Grafana。为了在 Heapster 和 Grafana 中获得有用的值,您将希望运行更长时间的负载测试,因为这些数据正在被聚合——最好是在几分钟内运行负载测试,因为一分钟是 Grafana 与 Heapster 聚合的基本级别。

cAdvisor 将更快地更新,如果您正在查看交互式图表,您将会看到它们随着负载测试的进行而更新:

在这种情况下,您会看到我们的内存使用量基本保持在 36 MB 左右,而我们的 CPU 在负载测试期间达到峰值(这是您可能会预期到的应用程序行为)。

如果我们应用了前面的请求和限制示例,并更新了 flask 部署,那么当 CPU 达到大约 1/2 核心 CPU 限制时,您会看到负载趋于平稳。

这个过程的第三步主要是验证您对 CPU 和内存需求的评估是否符合长时间运行的负载测试。通常情况下,您会运行一个较长时间的负载(至少几分钟),并设置请求和限制来验证容器是否能够提供预期的流量。这种评估中最常见的缺陷是在进行长时间负载测试时看到内存缓慢增加,导致容器被 OOM 杀死(因超出内存限制而被终止)。

我们在示例中使用的 100 MiB RAM 比这个容器实际需要的内存要多得多,因此我们可以将其减少到 40 MiB 并进行最终验证步骤。

在设置请求和限制时,您希望选择最有效地描述您的需求的值,但不要浪费保留的资源。要运行更长时间的负载测试,请输入:

ab -c 100 -n 50000 http://flask-service.default:5000/

Grafana 的输出如下:

使用 Prometheus 捕获指标

Prometheus 是一个用于监控的知名开源工具,它与 Kubernetes 社区之间正在进行相当多的共生工作。Kubernetes 应用程序指标以 Prometheus 格式公开。该格式包括countergaugehistogramsummary的数据类型,以及一种指定与特定指标相关联的标签的方法。随着 Prometheus 和 Kubernetes 的发展,Prometheus 的指标格式似乎正在成为该项目及其各个组件中的事实标准。

有关此格式的更多信息可在 Prometheus 项目的文档中在线获取:

除了指标格式外,Prometheus 作为自己的开源项目提供了相当多样的功能,并且在 Kubernetes 之外也被使用。该项目的架构合理地展示了其主要组件:

Prometheus 服务器本身是我们将在本章中研究的内容。在其核心,它定期地扫描多个远程位置,从这些位置收集数据,将其存储在短期时间序列数据库中,并提供一种查询该数据库的方法。Prometheus 的扩展允许系统将这些时间序列指标导出到其他系统以进行长期存储。此外,Prometheus 还包括一个警报管理器,可以配置为根据从时间序列指标中捕获和派生的信息发送警报,或更一般地调用操作。

Prometheus 并不打算成为指标的长期存储,并且可以与各种其他系统一起工作,以在长期内捕获和管理数据。常见的 Prometheus 安装保留数据 6 至 24 小时,可根据安装进行配置。

Prometheus 的最小安装包括 Prometheus 服务器本身和服务的配置。但是,为了充分利用 Prometheus,安装通常更加广泛和复杂,为 Alertmanager 和 Prometheus 服务器分别部署,可选地为推送网关部署(允许其他系统主动向 Prometheus 发送指标),以及一个 DaemonSet 来从集群中的每个节点捕获数据,将信息暴露和导出到 Prometheus 中,利用 cAdvisor。

更复杂的软件安装可以通过管理一组 YAML 文件来完成,就像我们在本书中之前所探讨的那样。有其他选项可以管理和安装一组部署、服务、配置等等。我们将利用这类工作中更常见的工具之一,Helm,而不是记录所有的部分,Helm 与 Kubernetes 项目密切相关,通常被称为Kubernetes 的包管理器

您可以在项目的文档网站helm.sh上找到有关 Helm 的更多信息。

安装 Helm

Helm 是一个由命令行工具和在 Kubernetes 集群中运行的软件组成的双重系统,命令行工具与之交互。通常,您需要的是本地的命令行工具,然后再用它来安装所需的组件到您的集群中。

有关安装 Helm 命令行工具的文档可在项目网站上找到:github.com/kubernetes/helm/blob/master/docs/install.md.

如果您在本地使用 macOS,则可以通过 Homebrew 获得,并且可以使用以下命令进行安装:

brew install kubernetes-helm

或者,如果您是从 Linux 主机工作,Helm 项目提供了一个脚本,您可以使用它来安装 Helm:

curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh
chmod 700 get_helm.sh
./get_helm.sh

安装 Helm 后,您可以使用helm init命令在集群中安装运行的组件(称为 Tiller)。您应该会看到如下输出:

$HELM_HOME has been configured at /Users/heckj/.helm.

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.
Happy Helming!

除了为其使用设置一些本地配置文件之外,这还在kube-system命名空间中为其集群端组件Tiller进行了部署。如果您想更详细地查看它,可以查看该部署:

kubectl describe deployment tiller-deploy -n kube-system

此时,您已经安装了 Helm,并且可以使用命令 Helm version 验证安装的版本(包括命令行和集群上的版本)。这与kubectl version 非常相似,报告了其版本以及与之通信的系统的版本。

helm version 
Client: &amp;version.Version{SemVer:"v2.7.2", GitCommit:"8478fb4fc723885b155c924d1c8c410b7a9444e6", GitTreeState:"clean"}
Server: &amp;version.Version{SemVer:"v2.7.2", GitCommit:"8478fb4fc723885b155c924d1c8c410b7a9444e6", GitTreeState:"clean"}

现在,我们可以继续设置 Helm 的原因:安装 Prometheus。

使用 Helm 安装 Prometheus

Helm 使用一组配置文件来描述安装需要什么,以什么顺序以及使用什么参数。这些配置称为图表,并在 GitHub 中维护,其中维护了默认的 Helm 存储库。

您可以使用命令helm repo list查看 Helm 正在使用的存储库。

helm repo list 
NAME URL
stable https://kubernetes-charts.storage.googleapis.com
local http://127.0.0.1:8879/charts

此默认值是围绕 GitHub 存储库的包装器,您可以在github.com/kubernetes/charts上查看存储库的内容。查看所有可用于使用的图表的另一种方法是使用命令helm search

确保您拥有存储库的最新缓存是个好主意。您可以使用命令helm repo update将缓存更新到最新状态,以在 GitHub 中镜像图表。

更新后的结果应该报告成功,并输出类似于:

help repo update

Hang tight while we grab the latest from your chart repositories...
...Skip local chart repository
...Successfully got an update from the "stable" chart repository
Update Complete.  Happy Helming!

我们将使用 stable/Prometheus 图表(托管在github.com/kubernetes/charts/tree/master/stable/prometheus)。我们可以使用 Helm 将该图表拉取到本地,以便更详细地查看它。

helm fetch --untar stable/prometheus 

此命令从默认存储库下载图表并在名为 Prometheus 的目录中本地解压缩。查看目录,您应该会看到几个文件和一个名为templates的目录:

.helmignore
Chart.yaml
README.md
templates
values.yaml

这是图表的常见模式,其中Chart.yaml描述了将由图表安装的软件。values.yaml是一组默认配置值,这些值在将要创建的各种 Kubernetes 资源中都会使用,并且模板目录包含了将被渲染出来以安装集群中所需的所有 Kubernetes 资源的模板文件集合。通常,README.md将包括values.yaml中所有值的描述,它们的用途以及安装建议。

现在,我们可以安装prometheus,我们将利用 Helm 的一些选项来设置一个发布名称并使用命名空间来进行安装。

helm install prometheus -n monitor --namespace monitoring

这将安装prometheus目录中包含的图表,将所有组件安装到命名空间monitoring中,并使用发布名称monitor为所有对象添加前缀。如果我们没有指定这些值中的任何一个,Helm 将使用默认命名空间,并生成一个随机的发布名称来唯一标识安装。

调用此命令时,您将看到相当多的输出,描述了在过程开始时创建的内容及其状态,然后是提供有关如何访问刚刚安装的软件的信息的注释部分:

NAME: monitor
LAST DEPLOYED: Sun Jan 14 15:00:40 2018
NAMESPACE: monitoring
STATUS: DEPLOYED
RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
monitor-prometheus-alertmanager 1 1s
monitor-prometheus-server 3 1s

==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
monitor-prometheus-alertmanager Bound pvc-be6b3367-f97e-11e7-92ab-e697d60b4f2f 2Gi RWO standard 1s
monitor-prometheus-server Bound pvc-be6b8693-f97e-11e7-92ab-e697d60b4f2f 8Gi RWO standard 1s

==> v1/Service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
monitor-prometheus-alertmanager ClusterIP 10.100.246.164 <none> 80/TCP 1s
monitor-prometheus-kube-state-metrics ClusterIP None <none> 80/TCP 1s
monitor-prometheus-node-exporter ClusterIP None <none> 9100/TCP 1s
monitor-prometheus-pushgateway ClusterIP 10.97.187.101 <none> 9091/TCP 1s
monitor-prometheus-server ClusterIP 10.110.247.151 <none> 80/TCP 1s

==> v1beta1/DaemonSet
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
monitor-prometheus-node-exporter 1 1 0 1 0 <none> 1s

==> v1beta1/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
monitor-prometheus-alertmanager 1 1 1 0 1s
monitor-prometheus-kube-state-metrics 1 1 1 0 1s
monitor-prometheus-pushgateway 1 1 1 0 1s
monitor-prometheus-server 1 1 1 0 1s

==> v1/Pod(related)
NAME READY STATUS RESTARTS AGE
monitor-prometheus-node-exporter-bc9jp 0/1 ContainerCreating 0 1s
monitor-prometheus-alertmanager-6c59f855d-bsp7t 0/2 ContainerCreating 0 1s
monitor-prometheus-kube-state-metrics-57747bc8b6-l7pzw 0/1 ContainerCreating 0 1s
monitor-prometheus-pushgateway-5b99967d9c-zd7gc 0/1 ContainerCreating 0 1s
monitor-prometheus-server-7895457f9f-jdvch 0/2 Pending 0 1s

NOTES:
The prometheus server can be accessed via port 80 on the following DNS name from within your cluster:
monitor-prometheus-server.monitoring.svc.cluster.local

Get the prometheus server URL by running these commands in the same shell:
 export POD_NAME=$(kubectl get pods --namespace monitoring -l "app=prometheus,component=server" -o jsonpath="{.items[0].metadata.name}")
 kubectl --namespace monitoring port-forward $POD_NAME 9090

The prometheus alertmanager can be accessed via port 80 on the following DNS name from within your cluster:
monitor-prometheus-alertmanager.monitoring.svc.cluster.local

Get the Alertmanager URL by running these commands in the same shell:
 export POD_NAME=$(kubectl get pods --namespace monitoring -l "app=prometheus,component=alertmanager" -o jsonpath="{.items[0].metadata.name}")
 kubectl --namespace monitoring port-forward $POD_NAME 9093

The prometheus PushGateway can be accessed via port 9091 on the following DNS name from within your cluster:
monitor-prometheus-pushgateway.monitoring.svc.cluster.local

Get the PushGateway URL by running these commands in the same shell:
 export POD_NAME=$(kubectl get pods --namespace monitoring -l "app=prometheus,component=pushgateway" -o jsonpath="{.items[0].metadata.name}")
 kubectl --namespace monitoring port-forward $POD_NAME 9093

For more information on running prometheus, visit:
https://prometheus.io/

helm list将显示您已安装的当前发布:

NAME REVISION UPDATED STATUS CHART NAMESPACE
monitor 1 Sun Jan 14 15:00:40 2018 DEPLOYED prometheus-4.6.15 monitoring

您可以使用helm status命令,以及发布的名称,来获取图表创建的所有 Kubernetes 资源的当前状态:

helm status monitor

注释部分包含在模板中,并在每次状态调用时重新呈现,通常编写以包括有关如何访问软件的说明。

您可以安装图表而无需显式先检索它。Helm 首先使用任何本地图表,但会回退到搜索其可用存储库,因此我们可以只使用以下命令安装相同的图表:

**helm install stable/prometheus -n monitor --namespace monitoring**

您还可以让 Helm 混合values.yaml和其模板,以呈现出它将创建的所有对象并简单显示它们,这对于查看所有部件将如何组合在一起很有用。执行此操作的命令是helm template,要呈现用于创建 Kubernetes 资源的 YAML,命令将是:

helm template prometheus -n monitor --namespace monitoring

helm template命令确实需要图表在本地文件系统上可用,因此,虽然helm install可以从远程存储库中工作,但您需要使用helm fetch将图表本地化,以便利用helm template命令。

使用 Prometheus 查看指标

使用注释中提供的详细信息,您可以设置端口转发,就像我们在本书中之前所做的那样,并直接访问 Prometheus。从注释中显示的信息如下:

export POD_NAME=$(kubectl get pods --namespace monitoring -l "app=prometheus,component=server" -o jsonpath="{.items[0].metadata.name}")

kubectl --namespace monitoring port-forward $POD_NAME 9090

这将允许您直接使用浏览器访问 Prometheus 服务器。在终端中运行这些命令,然后打开浏览器并导航到http://localhost:9090/

您可以通过查看http://localhost:9090/targets上的目标列表来查看 Prometheus 正在监视的当前状态:

切换到 Prometheus 查询/浏览器,网址为http://localhost:9090/graph,以查看 Prometheus 收集的指标。收集了大量的指标,我们特别感兴趣的是与之前在 cAdvisor 和 Heapster 中看到的信息匹配的指标。在 Kubernetes 1.7 版本及更高版本的集群中,这些指标已经移动,并且是由我们在屏幕截图中看到的kubernetes-nodes-cadvisor作业专门收集的。

在查询浏览器中,您可以开始输入指标名称,它将尝试自动完成,或者您可以使用下拉菜单查看所有可能的指标列表。输入指标名称container_memory_usage_bytes,然后按Enter以以表格形式查看这些指标的列表。

良好指标的一般形式将具有一些指标的标识符,并且通常以单位标识符结尾,在本例中为字节。查看表格,您可以看到收集的指标以及每个指标的相当密集的键值对。

这些键值对是指标上的标签,并且在整体上类似于 Kubernetes 中的标签和选择器的工作方式。

一个示例指标,重新格式化以便更容易阅读,如下所示:

container_memory_usage_bytes{
  beta_kubernetes_io_arch="amd64",
  beta_kubernetes_io_os="linux",
  container_name="POD",
  id="/kubepods/podf887aff9-f981-11e7-92ab-e697d60b4f2f/25fa74ef205599036eaeafa7e0a07462865f822cf364031966ff56a9931e161d",
  image="gcr.io/google_containers/pause-amd64:3.0",
  instance="minikube",
  job="kubernetes-nodes-cadvisor",
  kubernetes_io_hostname="minikube",
  name="k8s_POD_flask-5c7d884fcc-2l7g9_default_f887aff9-f981-11e7-92ab-e697d60b4f2f_0",
  namespace="default",
  pod_name="flask-5c7d884fcc-2l7g9"
}  249856

在查询中,我们可以通过在查询中包含与这些标签匹配的内容来过滤我们感兴趣的指标。例如,与特定容器相关联的所有指标都将具有image标签,因此我们可以仅过滤这些指标:

container_memory_usage_bytes{image!=""}

您可能已经注意到,命名空间和 pod 名称也包括在内,我们也可以进行匹配。例如,只查看与我们部署示例应用程序的默认命名空间相关的指标,我们可以添加namespace="default"

container_memory_usage_bytes{image!="",namespace="default"}

这开始变得更合理了。虽然表格将向您显示最近的值,但我们感兴趣的是这些值的历史记录。如果您选择当前查询上的图形按钮,它将尝试呈现出您选择的指标的单个图形,例如:

由于指标还包括container_name以匹配部署,您可以将其调整为单个容器。例如,查看与我们的flask部署相关的内存使用情况:

container_memory_usage_bytes{image!="",namespace="default",container_name="flask"}

如果我们增加flask部署中副本的数量,它将为每个容器创建新的指标,因此为了不仅查看单个容器而且一次查看多个集合,我们可以利用 Prometheus 查询语言中的聚合运算符。一些最有用的运算符包括sumcountcount_valuestopk

我们还可以使用这些相同的聚合运算符将指标分组在一起,其中聚合集合具有不同的标签值。例如,在将flask部署的副本增加到三个后,我们可以查看部署的总内存使用情况:

sum(container_memory_usage_bytes{image!="",
namespace="default",container_name="flask"})

然后再次按照 Pod 名称将其分解为每个容器:

sum(container_memory_usage_bytes{image!="",
namespace="default",container_name="flask"}) by (name)

图形功能可以为您提供一个良好的视觉概览,包括堆叠值,如下所示:

随着图形变得更加复杂,您可能希望开始收集您认为最有趣的查询,以及组合这些图形的仪表板,以便能够使用它们。这将引导我们进入另一个开源项目 Grafana,它可以很容易地在 Kubernetes 上托管,提供仪表板和图形。

安装 Grafana

Grafana 本身并不是一个复杂的安装,但配置它可能会很复杂。Grafana 可以插入多种不同的后端系统,并为它们提供仪表板和图形。在我们的示例中,我们希望它从 Prometheus 提供仪表板。我们将设置一个安装,然后通过其用户界面进行配置。

我们可以再次使用 Helm 来安装 Grafana,由于我们已经将 Prometheus 放在监控命名空间中,我们将用相同的方式处理 Grafana。我们可以使用helm fetch并安装来查看图表。在这种情况下,我们将直接安装它们:

helm install stable/grafana -n viz --namespace monitoring

在生成的输出中,您将看到一个秘密、ConfigMap 和部署等资源被创建,并且在注释中会有类似以下内容:

NOTES:
1\. Get your 'admin' user password by running:

kubectl get secret --namespace monitoring viz-grafana -o jsonpath="{.data.grafana-admin-password}" | base64 --decode ; echo

2\. The Grafana server can be accessed via port 80 on the following DNS name from within your cluster:

viz-grafana.monitoring.svc.cluster.local

Get the Grafana URL to visit by running these commands in the same shell:

export POD_NAME=$(kubectl get pods --namespace monitoring -l "app=viz-grafana,component=grafana" -o jsonpath="{.items[0].metadata.name}")
 kubectl --namespace monitoring port-forward $POD_NAME 3000

3\. Login with the password from step 1 and the username: admin

注释首先包括有关检索秘密的信息。这突出了一个功能,您将看到在几个图表中使用:当它需要一个机密密码时,它将生成一个唯一的密码并将其保存为秘密。这个秘密直接可供访问命名空间和kubectl的人使用。

使用提供的命令检索 Grafana 界面的密码:

kubectl get secret --namespace monitoring viz-grafana -o jsonpath="{.data.grafana-admin-password}" | base64 --decode ; echo

然后打开终端并运行这些命令以访问仪表板:

export POD_NAME=$(kubectl get pods --namespace monitoring -l "app=viz-grafana,component=grafana" -o jsonpath="{.items[0].metadata.name}")

kubectl --namespace monitoring port-forward $POD_NAME 3000

然后,打开浏览器窗口,导航至https://localhost:3000/,这将显示 Grafana 登录窗口:

现在,使用用户名admin登录;密码是您之前检索到的秘密。这将带您进入 Grafana 中的主页仪表板,您可以在那里配置数据源并将图形组合成仪表板:

点击“添加数据源”,您将看到一个带有两个选项卡的窗口:配置允许您设置数据源的位置,仪表板允许您导入仪表板配置。

在配置下,将数据源的类型设置为 Prometheus,在名称处,您可以输入prometheus。在类型之后命名数据源有点多余,如果您的集群上有多个 Prometheus 实例,您会希望为它们分别命名,并且特定于它们的目的。在 URL 中添加我们的 Prometheus 实例的 DNS 名称,以便 Grafana 可以访问它:http://monitor-prometheus-server.monitoring.svc.cluster.local。在使用 Helm 安装 Prometheus 时,这个相同的名称在注释中列出了。

点击“仪表板”选项卡,并导入 Prometheus 统计信息和 Grafana 指标,这将为 Prometheus 和 Grafana 本身提供内置仪表板。点击返回到“配置”选项卡,向下滚动,并点击“添加”按钮设置 Prometheus 数据源。当您添加时,您应该会看到“数据源正在工作”。

现在,您可以导航到内置仪表板并查看一些信息。网页用户界面的顶部由下拉菜单组成,左上角导航到整体 Grafana 配置,下一个列出了您设置的仪表板,通常从主页仪表板开始。选择我们刚刚导入的 Prometheus Stats 仪表板,您应该会看到有关 Prometheus 的一些初始信息:

Grafana 项目维护了一系列仪表板,您可以搜索并直接使用,或者用作灵感来修改和创建自己的仪表板。您可以搜索已共享的仪表板,例如,将其限制为来自 Prometheus 并与 Kubernetes 相关的仪表板。您将看到各种各样的仪表板可供浏览,其中一些包括屏幕截图,网址为grafana.com/dashboards?dataSource=prometheus&amp;search=kubernetes

您可以使用仪表板编号将其导入到 Grafana 的实例中。例如,仪表板 1621 和 162 是用于监视整个集群健康状况的常见仪表板:

这些仪表板的最佳价值在于向您展示如何配置自己的图形并制作自己的仪表板。在每个仪表板中,您可以选择图形并选择编辑以查看使用的查询和显示选择,并根据您的值进行微调。每个仪表板也可以共享回 Grafana 托管站点,或者您可以查看配置的 JSON 并将其保存在本地。

Prometheus 运营商正在努力使启动 Prometheus 和 Grafana 变得更容易,预先配置并运行以监视您的集群和集群内的应用程序。如果您有兴趣尝试一下,请参阅 CoreOS 托管的项目 README github.com/coreos/prometheus-operator/tree/master/helm,也可以使用 Helm 进行安装。

现在您已经安装了 Grafana 和 Prometheus,您可以使用它们来遵循类似的过程,以确定您自己软件的 CPU 和内存利用率,同时运行负载测试。在本地运行 Prometheus 的一个好处是它提供了收集有关您的应用程序的指标的能力。

使用 Prometheus 查看应用程序指标

虽然您可以在 Prometheus 中添加作业以包括从特定端点抓取 Prometheus 指标的配置,但我们之前进行的安装包括一个将根据 Pod 上的注释动态更新其查看内容的配置。 Prometheus 的一个好处是它支持根据注释自动检测集群中的更改,并且可以查找支持服务的 Pod 的端点。

由于我们使用 Helm 部署了 Prometheus,您可以在values.yaml文件中找到相关的配置。查找 Prometheus 作业kubernetes-service-endpoints,您将找到配置和一些关于如何使用它的文档。如果您没有本地文件,可以在github.com/kubernetes/charts/blob/master/stable/prometheus/values.yaml#L747-L776上查看此配置。

此配置查找集群中具有注释prometheus.io/scrape的服务。如果设置为true,那么 Prometheus 将自动尝试将该端点添加到其正在监视的目标列表中。默认情况下,它将尝试访问 URI/metrics上的指标,并使用与服务相同的端口。您可以使用其他注释来更改这些默认值,例如,prometheus.io/path = "/alternatemetrics"将尝试从路径/alternatemetrics读取指标。

通过使用服务作为组织指标收集的手段,我们有一个机制,它将根据 Pod 的数量自动扩展。而在其他环境中,您可能需要每次添加或删除实例时重新配置监控,Prometheus 和 Kubernetes 无缝协作捕获这些数据。

这种能力使我们能够轻松地从我们的应用程序中公开自定义指标,并让 Prometheus 捕获这些指标。这可以有几种用途,但最明显的是更好地了解应用程序的运行情况。有了 Prometheus 收集指标和 Grafana 作为仪表板工具,您还可以使用这种组合来创建自己的应用程序仪表板。

Prometheus 项目支持多种语言的客户端库,使其更容易收集和公开指标。我们将使用其中一些库来向您展示如何为我们的 Python 和 Node.js 示例进行仪器化。在直接使用这些库之前,非常值得阅读 Prometheus 项目提供的有关如何编写指标导出器以及其对指标名称的预期约定的文档。您可以在项目网站找到这些文档:prometheus.io/docs/instrumenting/writing_exporters/

使用 Prometheus 的 Flask 指标

您可以在github.com/prometheus/client_python找到从 Python 公开指标的库,并可以使用以下命令使用pip进行安装:

pip install prometheus_client

根据您的设置,您可能需要使用**sudo pip install prometheus_client**使用pip安装客户端。

对于我们的flask示例,您可以从github.com/kubernetes-for-developers/kfd-flask的 0.5.0 分支下载完整的示例代码。获取此更新示例的命令如下:

git clone https://github.com/kubernetes-for-developers/kfd-flask -b 0.5.0

如果您查看exampleapp.py,您可以看到我们使用两个指标的代码,即直方图和计数器,并使用 flask 框架在请求开始和请求结束时添加回调,并捕获该时间差:

FLASK_REQUEST_LATENCY = Histogram('flask_request_latency_seconds', 'Flask Request Latency',
 ['method', 'endpoint'])
FLASK_REQUEST_COUNT = Counter('flask_request_count', 'Flask Request Count',
 ['method', 'endpoint', 'http_status'])

def before_request():
   request.start_time = time.time()

def after_request(response):
   request_latency = time.time() - request.start_time
   FLASK_REQUEST_LATENCY.labels(request.method, request.path).observe(request_latency)
   FLASK_REQUEST_COUNT.labels(request.method, request.path, response.status_code).inc()
   return response

该库还包括一个辅助应用程序,使得生成 Prometheus 要抓取的指标非常容易:

@app.route('/metrics')
def metrics():
   return make_response(generate_latest())

该代码已制作成容器映像quay.io/kubernetes-for-developers/flask:0.5.0。有了这些添加,我们只需要将注释添加到flask-service

kind: Service
apiVersion: v1
metadata:
   name: flask-service
   annotations:
       prometheus.io/scrape: "true"
spec:
  type: NodePort
  ports:
  - port: 5000
  selector:
      app: flask

从示例目录中使用kubectl apply -f deploy/部署后,该服务将由单个 pod 支持,并且 Prometheus 将开始将其作为目标。如果您使用kubectl proxy命令,您可以查看此生成的特定指标响应。在我们的情况下,pod 的名称是flask-6596b895b-nqqqz,因此可以轻松查询指标http://localhost:8001/api/v1/proxy/namespaces/default/pods/flask-6596b895b-nqqqz/metrics

这些指标的示例如下:

flask_request_latency_seconds_bucket{endpoint="/",le="0.005",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="0.01",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="0.025",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="0.05",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="0.075",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="0.1",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="0.25",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="0.5",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="0.75",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="1.0",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="2.5",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="5.0",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="7.5",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="10.0",method="GET"} 13.0 flask_request_latency_seconds_bucket{endpoint="/",le="+Inf",method="GET"} 13.0 flask_request_latency_seconds_count{endpoint="/",method="GET"} 13.0 flask_request_latency_seconds_sum{endpoint="/",method="GET"} 0.0012879371643066406 
# HELP flask_request_count Flask Request Count 
# TYPE flask_request_count counter flask_request_count{endpoint="/alive",http_status="200",method="GET"} 645.0 flask_request_count{endpoint="/ready",http_status="200",method="GET"} 644.0 flask_request_count{endpoint="/metrics",http_status="200",method="GET"} 65.0 flask_request_count{endpoint="/",http_status="200",method="GET"} 13.0

您可以在此示例中看到名为flask_request_latency_secondsflask_request_count的指标,并且您可以在 Prometheus 浏览器界面中查询相同的指标。

使用 Prometheus 的 Node.js 指标

JavaScript 具有与 Python 类似的客户端库。实际上,使用express-prom-bundle来为 Node.js express 应用程序提供仪表板甚至更容易,该库反过来使用prom-client。您可以使用以下命令安装此库以供您使用:

npm install express-prom-bundle --save

然后您可以在您的代码中使用它。以下内容将为 express 设置一个中间件:

const promBundle = require("express-prom-bundle");
const metricsMiddleware = promBundle({includeMethod: true});

然后,您只需包含中间件,就像您正在设置此应用程序一样:

app.use(metricsMiddleware)

github.com/kubernetes-for-developers/kfd-nodejs上的示例代码已经更新,您可以使用以下命令从 0.5.0 分支检查此代码:

git clone https://github.com/kubernetes-for-developers/kfd-nodejs -b 0.5.0

与 Python 代码一样,Node.js 示例包括使用注释prometheus.io/scrape: "true"更新服务:

kind: Service
apiVersion: v1
metadata:
 name: nodejs-service
 annotations:
   prometheus.io/scrape: "true"
spec:
 ports:
 - port: 3000
   name: web
 clusterIP: None
 selector:
   app: nodejs

Prometheus 中的服务信号

您可以通过三个关键指标来了解您服务的健康和状态。服务仪表板通常会基于这些指标进行仪表化和构建,作为了解您的服务运行情况的基线,这已经相对普遍。

这些网络服务的关键指标是:

  • 错误率

  • 响应时间

  • 吞吐量

错误率可以通过使用http_request_duration_seconds_count指标中的标签来收集,该指标包含在express-prom-bundle中。我们可以在 Prometheus 中使用的查询。我们可以匹配响应代码的格式,并计算 500 个响应与所有响应的增加数量。

Prometheus 查询可以是:

sum(increase(http_request_duration_seconds_count{status_code=~"⁵..$"}[5m])) / sum(increase(http_request_duration_seconds_count[5m]))

在我们自己的示例服务上几乎没有负载,可能没有错误,这个查询不太可能返回任何数据点,但你可以用它作为一个示例来探索构建你自己的错误响应查询。

响应时间很难测量和理解,特别是对于繁忙的服务。我们通常包括一个用于处理请求所需时间的直方图指标的原因是为了能够查看这些请求随时间的分布。使用直方图,我们可以在一个窗口内聚合请求,然后查看这些请求的速率。在我们之前的 Python 示例中,flask_request_latency_seconds是一个直方图,每个请求都带有它在直方图桶中的位置标签,使用的 HTTP 方法和 URI 端点。我们可以使用这些标签聚合这些请求的速率,并使用以下 Prometheus 查询查看中位数、95^(th)和 99^(th)百分位数:

中位数:

histogram_quantile(0.5, sum(rate(flask_request_latency_seconds_bucket[5m])) by (le, method, endpoint))

95^(th)百分位数:

histogram_quantile(0.95, sum(rate(flask_request_latency_seconds_bucket[5m])) by (le, method, endpoint))

99^(th)百分位数:

histogram_quantile(0.99, sum(rate(flask_request_latency_seconds_bucket[5m])) by (le, method, endpoint))

吞吐量是关于在给定时间范围内测量请求总数的,可以直接从flask_request_latency_seconds_count中派生,并针对端点和方法进行查看:

sum(rate(flask_request_latency_seconds_count[5m])) by (method, endpoint)

总结

在本章中,我们介绍了 Prometheus,并展示了如何安装它,使用它从您的 Kubernetes 集群中捕获指标,并展示了如何安装和使用 Grafana 来提供仪表板,使用在 Prometheus 中临时存储的指标。然后,我们看了一下如何从您自己的代码中暴露自定义指标,并利用 Prometheus 来捕获它们,以及一些您可能有兴趣跟踪的指标的示例,如错误率、响应时间和吞吐量。

在下一章中,我们将继续使用工具来帮助我们捕获日志和跟踪,以便观察我们的应用程序。

第八章:日志和跟踪

当我们最初开始使用容器和 Kubernetes 时,我们展示了如何使用kubectl log命令从任何一个容器中获取日志输出。随着我们希望获取信息的容器数量增加,轻松找到相关日志的能力变得越来越困难。在上一章中,我们看了如何聚合和收集指标,在本章中,我们扩展了相同的概念,看看如何聚合日志并更好地了解容器如何与分布式跟踪一起工作。

本章的主题包括:

  • 一个 Kubernetes 概念- DaemonSet

  • 安装 Elasticsearch,Fluentd 和 Kibana

  • 使用 Kibana 查看日志

  • 使用 Jeager 进行分布式跟踪

  • 将跟踪添加到您的应用程序的一个例子

一个 Kubernetes 概念- DaemonSet

我们现在使用的一个 Kubernetes 资源(通过 Helm)是 DaemonSet。这个资源是围绕 pod 的一个包装,与 ReplicaSet 非常相似,但其目的是在集群中的每个节点上运行一个 pod。当我们使用 Helm 安装 Prometheus 时,它创建了一个 DaemonSet,在 Kubernetes 集群中的每个节点上运行 node-collector。

在应用程序中运行支持软件有两种常见模式:第一种是使用 side-car 模式,第二种是使用 DaemonSet。side-car 是指在您的 pod 中包含一个容器,其唯一目的是与主要应用程序一起运行并提供一些支持,但是外部的角色。一个有用的 side-car 的例子可能是缓存,或某种形式的代理服务。运行 side-car 应用程序显然会增加 pod 所需的资源,如果 pod 的数量相对较低,或者与集群的规模相比它们是稀疏的,那么这将是提供支持软件的最有效方式。

当您运行的支持软件在单个节点上可能被复制多次,并且提供的服务相当通用(例如日志聚合或指标收集)时,在集群中的每个节点上运行一个单独的 pod 可能会更有效。这正是 DaemonSet 的用武之地。

我们之前使用 DaemonSet 的示例是在集群中的每个节点上运行一个 node-collector 实例。node-collector DaemonSet 的目的是收集有关每个节点操作的统计数据和指标。Kubernetes 还使用 DaemonSet 来运行自己的服务,例如在集群中的每个节点上运行的 kube-proxy。如果您正在使用覆盖网络连接您的 Kubernetes 集群,例如 Weave 或 Flannel,它也经常使用 DaemonSet 运行。另一个常见的用例是我们将在本章中更多地探讨的用例,即收集和转发日志。

DaemonSet 规范的必需字段与部署或作业类似;除了apiVersionkindmetadata之外,DaemonSet 还需要一个包含模板的 spec,该模板用于在每个节点上创建 pod。此外,模板可以具有nodeSelector来匹配一组或子集可用的节点。

查看 Helm 在安装prometheus时创建的 YAML。您可以了解到 DaemonSet 的数据是如何布局的。以下输出来自命令:

helm template prometheus -n monitor --namespace monitoring

Helm 生成的 DaemonSet 规范如下:

apiVersion: extensions/v1beta1 kind: DaemonSet metadata:
  labels:
  app: prometheus
  chart: prometheus-4.6.17
  component: "node-exporter"
  heritage: Tiller
  release: monitor
  name: monitor-prometheus-node-exporter spec:
  updateStrategy:
  type: OnDelete   template:
  metadata:
  labels:
  app: prometheus
  component: "node-exporter"
  release: monitor
  spec:
  serviceAccountName: "default"
  containers:
 - name: prometheus-node-exporter
  image: "prom/node-exporter:v0.15.0"
  imagePullPolicy: "IfNotPresent"
  args:
 - --path.procfs=/host/proc
 - --path.sysfs=/host/sys
  ports:
 - name: metrics
  containerPort: 9100
  hostPort: 9100
  resources:
 {}  volumeMounts:
 - name: proc
  mountPath: /host/proc
  readOnly: true
 - name: sys
  mountPath: /host/sys
  readOnly: true
  hostNetwork: true
  hostPID: true
  volumes:
 - name: proc
  hostPath:
  path: /proc
 - name: sys
  hostPath:
  path: /sys

这个 DaemonSet 在每个节点上运行一个单一的容器,使用镜像prom/node-exporter:0.15,从卷挂载点(/proc/sys非常特定于 Linux)收集指标,并在端口9100上公开它们,以便prometheus通过 HTTP 请求进行抓取。

安装和使用 Elasticsearch、Fluentd 和 Kibana

Fluentd 是经常用于收集和聚合日志的软件。托管在www.fluentd.org,就像 prometheus 一样,它是由Cloud Native Computing Foundation (CNCF)管理的开源软件。在谈论聚合日志时,问题早在容器出现之前就存在,ELK 是一个常用的缩写,代表了一个解决方案,即 Elasticsearch、Logstash 和 Kibana 的组合。在使用容器时,日志来源的数量增加,使得收集所有日志的问题变得更加复杂,Fluentd 发展成为支持与 Logstash 相同领域的软件,专注于使用 JSON 格式的结构化日志,路由和支持处理日志的插件。Fluentd 是用 Ruby 和 C 编写的,旨在比 LogStash 更快,更高效,而 Fluent Bit (fluentbit.io)也延续了相同的模式,具有更小的内存占用。您甚至可能会看到 EFK 的引用,它代表 Elasticsearch、Fluentd 和 Kibana 的组合。

在 Kubernetes 社区中,捕获和聚合日志的常见解决方案之一是 Fluentd,甚至在 Minikube 的最新版本中作为可以使用的插件之一内置。

如果您正在使用 Minikube,可以通过启用 Minikube 插件来轻松尝试 EFK。尽管 Fluentd 和 Kibana 在资源需求方面相对较小,但 Elasticsearch 的资源需求较高,即使是用于小型演示实例。Minikube 使用的默认 VM 用于创建单节点 Kubernetes 集群,分配了 2GB 的内存,这对于运行 EFK 和任何其他工作负载是不够的,因为 ElasticSearch 在初始化和启动时需要使用 2GB 的内存。

幸运的是,您可以要求 Minikube 启动并为其创建的 VM 分配更多内存。要了解 Elasticsearch、Kibana 和 Fluentd 如何协同工作,您应该至少为 Minikube 分配 5GB 的 RAM 启动,可以使用以下命令完成:

minikube start --memory 5120

然后,您可以使用 Minikube add-ons 命令查看 Minikube 启用和禁用的插件。例如:

minikube addons list
- addon-manager: enabled
- coredns: enabled
- dashboard: enabled
- default-storageclass: enabled
- efk: disabled
- freshpod: disabled
- heapster: disabled
- ingress: disabled
- kube-dns: disabled
- registry: disabled
- registry-creds: disabled
- storage-provisioner: enabled

启用 EFK 只需使用以下命令即可:

 minikube addons enable efk
efk was successfully enabled

enabled并不意味着立即运行。FluentD 和 Kibana 会很快启动,但 ElasticSearch 需要更长的时间。作为附加组件意味着 Kubernetes 内的软件将管理 kube-system 命名空间内的容器,因此获取有关这些服务当前状态的信息不会像kubectl get pods那样简单。您需要引用-n kube-system或使用选项--all-namespaces

kubectl get all --all-namespaces
NAMESPACE NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
kube-system deploy/coredns 1 1 1 1 5h
kube-system deploy/kubernetes-dashboard 1 1 1 1 5h
NAMESPACE NAME DESIRED CURRENT READY AGE
kube-system rs/coredns-599474b9f4 1 1 1 5h
kube-system rs/kubernetes-dashboard-77d8b98585 1 1 1 5h
NAMESPACE NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
kube-system deploy/coredns 1 1 1 1 5h
kube-system deploy/kubernetes-dashboard 1 1 1 1 5h
NAMESPACE NAME DESIRED CURRENT READY AGE
kube-system rs/coredns-599474b9f4 1 1 1 5h
kube-system rs/kubernetes-dashboard-77d8b98585 1 1 1 5h
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system po/coredns-599474b9f4-6fp8z 1/1 Running 0 5h
kube-system po/elasticsearch-logging-4zbpd 0/1 PodInitializing 0 3s
kube-system po/fluentd-es-hcngp 1/1 Running 0 3s
kube-system po/kibana-logging-stlzf 1/1 Running 0 3s
kube-system po/kube-addon-manager-minikube 1/1 Running 0 5h
kube-system po/kubernetes-dashboard-77d8b98585-qvwlv 1/1 Running 0 5h
kube-system po/storage-provisioner 1/1 Running 0 5h
NAMESPACE NAME DESIRED CURRENT READY AGE
kube-system rc/elasticsearch-logging 1 1 0 3s
kube-system rc/fluentd-es 1 1 1 3s
kube-system rc/kibana-logging 1 1 1 3s
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default svc/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5h
kube-system svc/elasticsearch-logging ClusterIP 10.109.100.36 <none> 9200/TCP 3s
kube-system svc/kibana-logging NodePort 10.99.88.146 <none> 5601:30003/TCP 3s
kube-system svc/kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 5h
kube-system svc/kubernetes-dashboard NodePort 10.98.230.226 <none> 80:30000/TCP 5h

你可以看到 Minikube 附加管理器将 EFK 作为三个 ReplicaSets 加载,每个运行一个单独的 pod,并且使用从虚拟机暴露为 NodePort 的服务进行前端。使用 Minikube,您还可以使用以下命令查看服务列表:

minikube service list
|-------------|-----------------------|----------------------------|
| NAMESPACE   | NAME                  | URL                        |
|-------------|-----------------------|----------------------------|
| default     | kubernetes            | No node port               |
| kube-system | elasticsearch-logging | No node port               |
| kube-system | kibana-logging        | http://192.168.64.32:30003 |
| kube-system | kube-dns              | No node port               |
| kube-system | kubernetes-dashboard  | http://192.168.64.32:30000 |
|-------------|-----------------------|----------------------------|

使用 EFK 进行日志聚合。

Fluentd 作为从所有容器收集日志的源开始。它使用与命令kubectl logs相同的底层来源。在集群内,每个正在运行的容器都会生成日志,这些日志以某种方式由容器运行时处理,其中最常见的是 Docker,它在每个主机上为每个容器维护日志文件。

设置 Fluentd 的 Minikube 附加组件使用ConfigMap,它引用了加载这些日志文件的位置,并包含了用于注释来自 Kubernetes 的信息的附加规则。当 Fluentd 运行时,它会跟踪这些日志文件,从每个容器中读取更新的数据,将日志文件输出解析为 JSON 格式的数据结构,并添加 Kubernetes 特定的信息。相同的配置还详细说明了输出的处理方式,在 Minikube 附加组件的情况下,它指定了一个端点,即elasticsearch-logging服务,用于发送这些结构化的 JSON 数据。

Elasticsearch 是一个流行的开源数据和搜索索引,得到了Elastic.co的企业支持。虽然它需要相当多的资源来运行,但它的扩展性非常好,并且对于添加各种数据源并为这些数据提供搜索界面具有非常灵活的结构。您可以在github.com/elastic/elasticsearch的 GitHub 存储库中获取有关 ElasticSearch 工作原理的更多详细信息。

Kibana 是这个三部曲的最后一部分,为搜索存储在 Elasticsearch 中的内容提供了基于 Web 的用户界面。由Elastic.co维护,它提供了一些仪表板功能和 Elasticsearch 的交互式查询界面。您可以在www.elastic.co/products/kibana上找到更多关于 Kibana 的信息。

在使用 Minikube 时,集群中的所有内容都在单个节点上,因此在较大的集群中使用相同类型的框架会有限制和差异。如果您正在使用具有多个节点的远程集群,您可能需要查看类似 Helm 这样的工具来安装 Elasticsearch、Fluentd 和 Kibana。许多支持 Kubernetes 的服务提供商也已经设置了类似的机制和服务,用于聚合、存储和提供容器日志的可搜索索引。Google Stackdriver、Datadog 和 Azure 都提供了类似的机制和服务,专门针对其托管解决方案。

使用 Kibana 查看日志

在本书中,我们将探讨如何使用 Kibana,并将其作为 Minikube 的附加组件。启用后,当 pod 完全可用并报告为“就绪”时,您可以使用以下命令访问 Kibana:

minikube service kibana-logging -n kube-system

这将打开一个由kibana-logging服务支持的网页。首次访问时,网页将要求您指定一个默认索引,该索引将用于 Elasticsearch 构建其搜索索引:

点击“创建”,采用提供的默认设置。logstash-*的默认索引模式并不意味着它必须来自logstash作为数据源,而已经从 Fluentd 发送到 ElasticSearch 的数据将直接可访问。

一旦您定义了默认索引,下一个显示的页面将向您展示已添加到 Elasticsearch 中的所有字段,因为 Fluentd 已经从容器日志和 Kubernetes 元数据中获取了数据:

您可以浏览此列表,查看按字段名称捕获的内容,这将让您对可供浏览和搜索的内容有一点了解。

要查看从系统流出的日志,网页左上角的“发现”按钮将带您进入一个由我们刚刚创建的这些索引构建的视图,默认情况下将反映 Fluentd 正在收集的所有日志:

您看到的日志主要来自 Kubernetes 基础架构本身。为了更好地了解如何使用日志记录,启动我们之前创建的示例,并将它们扩展到多个实例以查看输出。

我们将从github.com/kubernetes-for-developers/kfd-flask获取 Flask 和 Redis 的两层示例应用程序。

git clone https://github.com/kubernetes-for-developers/kfd-flask -b 0.5.0
kubectl apply -f kfd-flask/deploy/

这将部署我们之前的 Python 和 Redis 示例,每个示例只有一个实例。一旦这些 pod 处于活动状态,返回并刷新带有 Kibana 的浏览器,它应该会更新以显示最新的日志。您可以在窗口顶部设置 Kibana 正在总结的时间段,并且如果需要,可以将其设置为定期自动刷新。

最后,让我们将 Flask 部署扩展到多个实例,这将使学习如何使用 Kibana 变得更容易:

kubectl scale deploy/flask --replicas=3

按应用程序筛选

有效使用 Kibana 的关键是筛选出您感兴趣的数据。默认的发现视图设置为让您了解特定来源的日志有多大,我们可以使用筛选来缩小我们想要查看的范围。

在查看数据时,从左侧滚动列表中滚动下去,每个字段都可以用作筛选器。如果您点击其中一个,例如 Kubernetes.labels.app,Kibana 将为您总结此字段在您正在查看的时间跨度内收集了哪些不同的值。

在前面的示例中,您可以看到在时间跨度内的两个app标签是flaskkubernetes-dashboard。我们可以通过点击带有加号的放大镜图标来将其限制为仅包含这些值的日志项:

带有减号符号的放大镜图标用于设置排除筛选器。由于我们之前使用kubectl scale命令创建了多个实例,您可以在字段列表中向下滚动到kubernetes.pod_name,并查看列出的并报告与第一个筛选器匹配的 pod:

您现在可以将过滤器细化为仅包括其中一个,或排除其中一个 pod,以查看所有剩余的日志。随着您添加过滤器,它们将出现在屏幕顶部,通过单击该引用,您可以删除、固定或暂时禁用该过滤器。

Lucene 查询语言

您还可以使用 Lucene 查询语言,这是 ElasticSearch 默认使用的语言,以便将搜索细化到字段内的数据,制作更复杂的过滤器,或以更精确的方式跟踪数据。Lucene 查询语言超出了本书的范围,但您可以在 Kibana 文档中获得很好的概述。

Lucene 的搜索语言是围绕搜索非结构化文本数据而设计的,因此搜索单词就像输入一个单词那样简单。多个单词被视为单独的搜索,因此如果您要搜索特定短语,请将短语放在引号中。搜索解析器还将理解简单布尔搜索的显式 OR 和 AND。

查询语法的默认设置是搜索所有字段,您可以指定要搜索的字段。要这样做,命名字段,后跟冒号,然后是搜索词。例如,要在字段log中搜索error,您可以使用此搜索查询:

log:error

此搜索查询还支持通配符搜索,使用字符?表示任何单个未知字符,*表示零个或多个字符。您还可以在查询中使用正则表达式,通过用/字符包装查询,例如:

log:/*error*/

这将在日志字段中搜索errorerrors

注意:因为 Lucene 会分解字段,正则表达式会应用于字符串中的每个单词,而不是整个字符串。因此,当您想要搜索组合词而不是包含空格的短语或字符串时,最好使用正则表达式。

Lucene 查询语言还包括一些高级搜索选项,可以容纳拼写错误和轻微变化,这可能非常有用。语法包括使用~字符作为通配符进行模糊搜索,允许拼写的轻微变化,转置等。短语还支持使用~作为变体指示符,并用于进行接近搜索,即短语中两个单词之间的最大距离。要了解这些特定技术的工作原理以及如何使用它们,请查阅ElasticSearch 查询 DSL 文档

在生产环境中运行 Kibana

Kibana 还有各种其他功能,包括设置仪表板,制作数据可视化,甚至使用简单的机器学习来搜索日志数据中的异常。这些功能超出了本书的范围。您可以在 Kibana 用户指南中了解更多信息www.elastic.co/guide/en/kibana/current/

运行更复杂的开发者支持工具,如 Elasticsearch,Fluentd 和 Kibana,是一项比我们在本书中所涵盖的更复杂的任务。有一些关于使用 Fluentd 和 Elasticsearch 作为附加组件的文档,就像你之前在 Minikube 示例中看到的那样。EFK 是一个需要管理的复杂应用程序。有几个 Helm 图表可能适合您的需求,或者您可能希望考虑利用云提供商的解决方案,而不是自己管理这些组件。

使用 Jaeger 进行分布式跟踪

当您将服务分解为多个容器时,最难理解的是请求的流动和路径,以及容器之间的交互方式。随着您扩展并使用更多容器来支持系统中的组件,了解哪些容器是哪些以及它们如何影响请求的性能将成为一个重大挑战。对于简单的系统,您通常可以添加日志记录并通过日志文件查看。当您进入由数十甚至数百个不同容器组成的服务时,这个过程变得不太可行。

这个问题的一个解决方案被称为分布式跟踪,它是一种追踪容器之间请求路径的方法,就像性能分析器可以追踪单个应用程序内的请求一样。这涉及使用支持跟踪库的库或框架来创建和传递信息,以及一个外部系统来收集这些信息并以可用的形式呈现出来。最早的例子可以在谷歌系统 Dapper 的研究论文中找到,受 Dapper 启发的早期开源实现被称为 Zipkin,由 Twitter 的工作人员制作。相同的概念已经重复出现多次,2016 年,一群人开始合作进行各种跟踪尝试。他们成立了 OpenTracing,现在是 Cloud Native Compute Foundation 的一部分,用于指定在各种系统和语言之间共享跟踪的格式。

Jaeger 是 OpenTracing 标准的一个实现,受 Dapper 和 Zipkin 启发,由 Uber 的工程师创建,并捐赠给 Cloud Native Compute Foundation。Jaeger 的完整文档可在jaeger.readthedocs.io/上找到。Jaeger 于 2017 年发布,目前正在积极开发和使用中。

还有其他跟踪平台,特别是 OpenZipkin(zipkin.io),也可用,因此 Jaeger 并不是这个领域的唯一选择。

跨度和跟踪

在分布式跟踪中,有两个常见的术语,你会反复看到:跨度和跟踪。跨度是在分布式跟踪中被追踪的最小单位,代表一个接收请求并返回响应的单个过程。当该过程向其他服务发出请求以完成其工作时,它会将信息与请求一起传递,以便被请求的服务可以创建自己的跨度并引用请求的跨度。这些跨度中的每一个都被收集并从每个过程中导出,然后可以进行分析。所有共同工作的跨度的完整集合被称为跟踪。

添加、收集和传输所有这些额外信息对每个服务都是额外的开销。虽然这些信息很有价值,但它也可能产生大量信息,如果每个交互的服务都创建并发布每个跟踪,处理跟踪系统所需的数据处理量将呈指数级增长。为了为跟踪提供价值,跟踪系统已经实施了抽样,以便不是每个请求都被跟踪,但是有一个合理的数量,仍然有足够的信息来获得系统整体操作的良好表示。

不同的跟踪系统处理方式不同,服务之间传递的数据量和数据类型仍然在不断变化。此外,不遵循请求/响应模式的服务(如后台队列或扇出处理)并不容易被当前的跟踪系统所表示。数据仍然可以被捕获,但呈现处理的一致视图可能会更加复杂。

当您查看跟踪的详细信息时,通常会看到一个火焰图样式的输出,显示了每个跟踪花费的时间以及正在处理它的服务。例如,这是 Jaeger 文档中的一个跟踪详细视图示例:

Jaeger 分布式跟踪的架构

与 Elasticsearch、Fluentd 和 Kibana(EFK)类似,Jaeger 是一个收集和处理大量信息的复杂系统。它在这里展示:

这是 Jaeger 在 2017 年在 Uber 工作的架构。配置使用了我们之前提到的 side-car 模式,每个容器都运行一个附近的容器,使用 UDP 收集来自仪器的跨度,然后将这些跨度转发到基于 Cassandra 的存储系统。设置 Cassandra 集群以及单独的收集器和查询引擎远比在本地开发环境中容易创建的要多得多。

幸运的是,Jaeger 还有一个全包选项,可以用来尝试和学习如何使用 Jaeger 以及它的功能。全包选项将代理、收集器、查询引擎和 UI 放在一个单一的容器映像中,不会持久存储任何信息。

Jaeger 项目有一体化选项,以及利用 Elasticsearch 进行持久化的 Helm 图表和变体,这些都在 GitHub 上进行了记录和存储,网址为github.com/jaegertracing/jaeger-kubernetes。事实上,Jaeger 项目通过利用 Kubernetes 来测试他们对 Jaeger 和每个组件的开发。

尝试 Jaeger

您可以通过使用 Jaeger 的一体化开发设置来尝试当前版本。由于他们在 GitHub 上维护这个版本,您可以直接使用以下命令从那里运行:

kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-kubernetes/master/all-in-one/jaeger-all-in-one-template.yml

这将创建一个部署和一些服务前端:

deployment "jaeger-deployment" created
service "jaeger-query" created
service "jaeger-collector" created
service "jaeger-agent" created
service "zipkin" created

jaeger-deployment pod 报告准备就绪时,您可以使用以下命令访问 Jaeger 查询界面:

minikube service jaeger-query

生成的网页应该如下所示:

默认情况下,Jaeger 系统正在报告自己的操作,因此当您使用查询界面时,它也会生成自己的跟踪,您可以开始调查。窗口左侧的“查找跟踪”面板应该显示在服务 jaeger-query 上,如果您点击底部的“查找跟踪”按钮,它将根据默认参数进行搜索:

此页面显示了找到的所有跟踪的时间以及它们所花费的时间,允许您通过 API 端点(在此用户界面中称为操作)深入挖掘它们,限制时间跨度,并提供了一个大致表示查询处理时间的粗略表示。

这些跟踪都由单个 span 组成,因此非常简单。您可以选择其中一个 span 并查看跟踪详细信息,包括展开它捕获和传递的信息以及这些跟踪。查看完全展开的详细信息应该显示如下:

让我们看看如何向您自己的应用程序添加追踪。

示例-向您的应用程序添加追踪

我们需要做几件事情来启用我们示例应用程序的追踪:

  • 添加库和代码以生成跟踪

  • 向您的 pod 添加一个追踪收集器边车

让我们先看看如何启用追踪边车,我们将使用之前在本书中构建的 Python Flask 示例。

这个例子的代码在线上的 GitHub 项目中github.com/kubernetes-for-developers/kfd-flask,这个添加的分支是0.6.0。您可以使用以下命令在本地获取此项目的代码:

git clone https://github.com/kubernetes-for-developers/kfd-flask -b 0.6.0

向您的 pod 添加跟踪收集器

实现 open-tracing 的库通常使用非常轻量级的网络连接,比如 UDP,来从我们的代码发送跟踪信息。UDP 不能保证连接,这也意味着如果网络过于拥挤,跟踪信息可能会丢失。OpenTracing 和 Jaeger 通过利用 Kubernetes 的一个保证来最小化这种情况:同一个 pod 中的两个容器将被放置在同一个节点上,共享相同的网络空间。如果我们在 pod 中的另一个容器中运行一个捕获 UDP 数据包的进程,网络连接将全部在同一个节点上,并且干扰的可能性非常小。

Jaeger 项目有一个镜像,它监听各种端口以捕获这些跟踪信息,并将其转发到存储和查询系统。容器jaegertracing/jaeger-agent发布到 DockerHub,并保持非常小的镜像大小(版本 1.2 为 5 MB)。这个小尺寸和靠近我们应用程序的好处使它非常适合作为一个辅助容器运行:在我们的 pod 中支持主要进程的另一个容器。

我们可以通过向我们 flask 部署(deploy/flask.yaml)中定义的 pod 添加另一个容器来实现这一点:

 - name: jaeger-agent
   image: jaegertracing/jaeger-agent
   ports:
   - containerPort: 5775
     protocol: UDP
   - containerPort: 5778
   - containerPort: 6831
     protocol: UDP
   - containerPort: 6832
     protocol: UDP
   command:
   - "/go/bin/agent-linux"
   - "--collector.host-port=jaeger-collector:14267"

这个例子是基于 Jaeger 部署文档,它提供了如何在 Docker 中使用它的示例,但不是直接在 Kubernetes 中使用。

重要的是要注意我们在这个容器中的命令。默认情况下,容器运行/go/bin/agent-linux,但没有任何选项。为了将数据发送到我们本地安装的 Jaeger,我们需要告诉收集器要发送到哪里。目的地由选项--collector.host-port定义。

在这种情况下,我们将 Jaeger all-in-one 安装到默认命名空间中,并包括一个名为jaeger-collector的服务,因此该服务将直接可用于此 pod。如果您在集群中安装了更强大的 Jaeger,您可能还将其定义在不同的命名空间中。例如,Jaeger 的 Helm 安装将安装到一个名为jaeger-infra的命名空间中,在这种情况下,collector.host-port选项的值需要更改以反映这一点:jaeger-collector.jaeger-infra.svc:14267

这里 Jaeger 还使用了多个端口,故意允许代理从备用语言使用的多种传统机制中收集。我们将使用 UDP 端口6382用于python jaeger-tracing客户端库。

添加生成跟踪的库和代码

我们首先为跟踪添加了两个库到我们的项目中:jaeger-clientflask_opentracingflask-opentracing将跟踪添加到 Flask 项目中,以便您可以轻松地自动跟踪所有 HTTP 端点。OpenTracing 项目不包括任何收集器,因此我们还需要一个库来收集和发送跟踪数据到某个地方,这里是 jaeger-client。

该示例还添加了 requests 库,因为在这个示例中,我们将添加一个进行远程请求、处理响应并返回值的 HTTP 端点,并对该序列进行跟踪。

导入库并初始化跟踪器非常简单:

import opentracing
from jaeger_client import Config
from flask_opentracing import FlaskTracer

# defaults to reporting via UDP, port 6831, to localhost
def initialize_tracer():
    config = Config(
        config={
            'sampler': {
                'type': 'const',
                'param': 1
            },
            'logging': True
        },
        service_name='flask-service'
    )
    # also sets opentracing.tracer
    return config.initialize_tracer() 

Jeager 建议您间接使用一种方法来初始化跟踪器,如前所示。在这种情况下,配置将采样器设置为转发所有请求;在生产中使用时,您需要仔细考虑这一配置选项,因为在高负载服务中跟踪每个请求可能会很繁重。

在创建 Flask 应用程序后立即初始化跟踪器:

app = Flask(__name__)flask_tracer = FlaskTracer(initialize_tracer, True, app, ["url_rule"])

这将与 Flask 一起使用,为所有@app.routes添加跟踪,每个路由将被标记为基于 Python 函数名称的操作。您还可以使用不同的配置设置仅跟踪特定路由,并在 Flask 路由上添加跟踪注释。

重建 Flask 图像并部署它将立即开始生成跟踪,并且在侧车中运行 jaeger-agent 的情况下,本地jaeger dev实例将立即显示跟踪。您应该看到一个名为flask-service的服务,基于我们的应用程序名称,并且它应该在其中列出多个操作:

活动,就绪和指标操作是启用以支持活动性和就绪性探针以及prometheus指标的 Flask 路由,这已在我们的示例 pod 上定义,它们正在获得一致的连接,从而生成与请求相关的跟踪。

这本身就很有用,但尚未告诉您方法中的哪个部分花费了更多或更少的时间。您可以使用flask-opentracing安装的opentracing库在您感兴趣的方法或代码段周围添加跟踪 span,以下代码片段显示了如何使用跟踪 span 包装我们在就绪探针中使用的对 Redis 的调用,以便它将单独显示出来:

@app.route('/ready')
def ready():
  parent_span = flask_tracer.get_span()
  with opentracing.tracer.start_span('redis-ping', child_of=parent_span) as span:
    result = redis_store.ping()
    span.set_tag("redis-ping", result)
  if result:
    return "Yes"
  else:
    abort(500)

关键在于获取我们为每个请求生成的当前跟踪 span,使用flask_tracer.get_span(),然后在with语句中使用它,这将在该上下文中执行的代码块中添加 span。我们还可以在 span 上使用方法,该方法在该代码块中可用。我们使用set_tag方法添加一个带有 ping 结果值的标签,以便在特定的跟踪输出中可用。

我们将继续添加一个@app.route称为/remote,以进行对 GitHub 的远程 HTTP 请求,并在其周围添加跟踪以将其显示为子 span:

@app.route('/remote')
def pull_requests():
    parent_span = flask_tracer.get_span()
    github_url = "https://api.github.com/repos/opentracing/opentracing-python/pulls"

    with opentracing.tracer.start_span('github-api', child_of=parent_span) as span:
        span.set_tag("http.url",github_url)
        r = requests.get(github_url)
        span.set_tag("http.status_code", r.status_code)

    with opentracing.tracer.start_span('parse-json', child_of=parent_span) as span:
        json = r.json()
        span.set_tag("pull_requests", len(json))
        pull_request_titles = map(lambda item: item['title'], json)
    return 'PRs: ' + ', '.join(pull_request_titles)

这个例子类似于就绪探针,只是我们在不同的代码段中包装不同的部分,并明确命名它们:github-apiparse-json

在添加代码时,您可以使用kubectl deletekubectl apply等命令来重新创建部署并将其构建并推送到您的容器注册表。对于这些示例,我的模式是从项目的主目录运行以下命令:

kubectl delete deploy/flask
docker build -t quay.io/kubernetes-for-developers/flask:0.6.0 .
docker push quay.io/kubernetes-for-developers/flask
kubectl apply -f deploy/

您将需要用项目中的值替换图像注册表引用和 Docker 标记。

然后,使用以下命令检查部署的状态:

kubectl get pods 
NAME                              READY STATUS RESTARTS AGE
flask-76f8c9767-56z4f             0/2   Init:0/1 0 6s
jaeger-deployment-559c8b9b8-jrq6c 1/1   Running 0 5d
redis-master-75c798658b-cxnmp     1/1   Running 0 5d

一旦它完全在线,您将看到它报告为就绪:

NAME                              READY STATUS RESTARTS AGE
flask-76f8c9767-56z4f             2/2   Running 0 1m
jaeger-deployment-559c8b9b8-jrq6c 1/1   Running 0 5d
redis-master-75c798658b-cxnmp     1/1   Running 0 5d

2/2 显示有两个容器正在运行 Flask pod,我们的主要代码和 jaeger-agent side-car。

如果您使用 Minikube,还可以使用服务命令轻松在浏览器中打开这些端点:

minikube service list

|-------------|----------------------|----------------------------|
| NAMESPACE   | NAME                 | URL                        |
|-------------|----------------------|----------------------------|
| default     | flask-service        | http://192.168.64.33:30676 |
| default     | jaeger-agent         | No node port               |
| default     | jaeger-collector     | No node port               |
| default     | jaeger-query         | http://192.168.64.33:30854 |
| default     | kubernetes           | No node port               |
| default     | redis-service        | No node port               |
| default     | zipkin               | No node port               |
| kube-system | kube-dns             | No node port               |
| kube-system | kubernetes-dashboard | http://192.168.64.33:30000 |
| kube-system | tiller-deploy        | No node port               |
|-------------|----------------------|----------------------------|

任何具有节点端口设置的服务都可以通过诸如以下命令轻松在本地打开:

minikube service flask-service
minikube service jaeger-query

添加、构建和部署此代码后,您可以在 Jaeger 中看到跟踪。将浏览器定向到/remote发出一些请求以从请求生成跨度,并且在 Jaeger 查询浏览器中,您应该看到类似以下内容:

Jaeger 查询窗口的顶部将显示表示查询时间和相对持续时间的点,您将看到它找到的各种跟踪列表-在我们的情况下有四个。如果选择一个跟踪,您可以进入详细视图,其中将包括子跨度。单击跨度以从中获取更多详细信息:

通过跨度详细视图,您可以查看在代码中设置的任何标签,并且您可以看到github-api调用在响应/remote请求时花费了大部分时间(265/266 毫秒)。

添加跟踪的考虑事项

跟踪是一个非常强大的工具,但也伴随着成本。每个跟踪都会(虽然很小)增加一些处理和管理的开销。您可能会很兴奋地将跟踪添加到应用程序中的每个方法,或者将其构建到一个库中,该库会将跟踪和跨度创建附加到每个方法调用中。这是可以做到的,但您很快会发现您的基础设施被跟踪信息所淹没。

跟踪也是一个工具,当直接与运行代码的责任直接相关时,它具有最大的好处。请注意,随着您添加跟踪,还会添加许多辅助处理,以捕获、存储和查询跟踪生成的数据。

处理权衡的一个好方法是有意识地、迭代地和缓慢地添加跟踪-以获得您需要的可见性。

OpenTracing 作为一个标准得到了许多供应商的支持。OpenTracing 也是一个不断发展的标准。在撰写本书时,人们正在讨论如何最好地共享和处理跨进程请求中携带的跨度数据(通常称为“行李”)。像追踪本身一样,添加数据可以增加价值,但这也带来了更大的请求成本和更多的处理需求来捕获和处理信息。

总结

在本章中,我们介绍了使用 Fluentd 和 Jaeger 进行日志记录和跟踪。我们展示了如何部署它并使用它,在代码运行时捕获和聚合数据。我们演示了如何使用 Elasticsearch 查询数据。我们还看了如何查看 Jaeger 跟踪以及如何向代码添加跟踪。

在下一章中,我们将探讨如何使用 Kubernetes 来支持和运行集成测试,以及如何将其与持续集成一起使用。

第九章:集成测试

到目前为止,我们已经了解了如何在 Kubernetes 中运行代码并描述服务。我们还研究了如何利用其他工具来获取有关代码在每个 pod 和整体上运行情况的信息。本章将在此基础上,探讨如何使用 Kubernetes 来验证代码,以及不同验证测试的示例,以及如何利用 Kubernetes 进行验证测试的建议。

本章的主题包括:

  • 使用 Kubernetes 的测试策略

  • 使用 Bats 进行简单验证

  • 示例 - 使用 Python 测试代码

  • 示例 - 使用 Node.js 测试代码

  • 使用 Kubernetes 进行持续集成

使用 Kubernetes 的测试策略

在软件工程中,开发和验证过程中使用了各种测试。在这个分类中,有一些测试类型非常适合利用 Kubernetes 的优势。与测试相关的术语可能含糊不清,令人困惑,因此为了清晰起见,我们将简要回顾我将使用的术语以及这些测试类型之间的区别。这里没有详细介绍这些主题的更多变体,但为了描述 Kubernetes 最有效的地方,这个列表已经足够:

  • 单元测试:单元测试是测试的最低级别;它侧重于应用程序中的接口、实现和模块。单元测试通常意味着仅对测试重点的组件进行隔离测试。这些测试通常旨在非常快速,可以直接在开发人员的系统上运行,并且通常不需要访问相关代码可能依赖的外部服务。这些测试通常不涉及状态或持久性,主要关注业务逻辑和接口验证。

  • 功能测试:功能测试是从单元测试中提升的下一步,意味着代码库针对其基础系统进行测试,而无需伪装、模拟或其他层,否则会假装像远程依赖一样运行。这种测试通常应用于服务的子集,测试和验证完整的服务,并使用即时依赖项(通常是数据库或持久性存储)。功能测试通常意味着对持久性存储中的状态进行验证,以及在代码运行过程中如何改变。

  • 集成测试:集成测试将软件的所有必需部分组合在一起,并验证各个组件以及组件之间的工作和交互。系统的状态通常在集成测试中被定义或设置为关键设置,因为状态在系统中被表示和验证,所以测试往往是有序和更线性的,通常使用组合交互来验证代码的工作方式(以及失败方式)。

功能测试和集成测试之间存在模糊的界限,前者通常专注于验证整体服务的子集,而后者代表着服务的大部分或整个系统。

  • 端到端测试:集成测试可能意味着测试系统的一部分,而端到端测试则特指测试和验证整个系统及其所有依赖关系。通常,端到端测试和集成测试是可以互换使用的。

  • 性能测试:在先前的术语中,重点是代码和任何相关依赖项之间的验证范围,性能测试侧重于验证类型而不是范围。这些测试意在衡量代码和服务的效率或利用率;它们利用了多少 CPU 和内存,以及在给定一组基础资源的情况下它们的响应速度。它们不是专注于代码的正确性,而是专注于规模和基础服务需求的验证。性能测试通常需要依赖系统不仅正常运行,而且需要充分资源以提供准确的结果,并且在某种程度上具有期望隔离,以防止外部资源限制人为地限制结果。

  • 交互/探索性测试:交互测试,有时也被称为探索性测试,再次不是关于范围的术语,而是意味着一种意图。这些测试通常需要系统的至少一部分处于运行状态,并且通常意味着整个系统处于运行状态,如果不是为了支持高水平的请求。这些测试侧重于让人们与系统进行交互,而不需要预定义或结构化的事件流,这种设置通常也用于接受验证或其他测试类型的改进,作为验证测试本身的手段。

审查测试所需的资源

当我们遍历测试的分类时,运行测试所需的计算资源和时间通常会增长并变得更加重要。根据正在开发的软件的范围,很可能需要比单台机器可以容纳的资源更多。而低级别的测试通常可以优化以利用计算机内的所有可能资源,端到端测试的串行性质往往效率较低,并且在验证过程中需要更多时间。

在建立测试时,您需要意识到验证软件所需的计算资源的大小。这可能对应于确定给定 pod 需要多少内存和 CPU 的过程,并且需要意识到基于您正在测试和想要实现的内容,所有依赖项的所有资源。

在大多数示例中,我们一直在使用 Minikube,但是现代开发和依赖关系很容易超出 Minikube 单节点集群所能提供的资源量。

在测试中使用 Kubernetes 最有效的地方是,您希望设置和使用与集成测试和测试场景相对应的环境的大部分内容,并且期望具有所有依赖项运行的完整系统。

当您专注于集成、端到端以及先前概述的分类的后续部分时,您当然可以在开发过程中使用 Kubernetes 运行诸如单元测试或功能测试之类的测试,尽管您可能会发现,在集成、端到端以及先前概述的分类的后续部分时,从 Kubernetes 中获得更多的好处。

由于 Kubernetes 擅长描述服务的期望状态并保持其运行,因此在您希望设置大部分或许多服务相互交互的地方,它可以被非常有效地使用。此外,如果您期望测试需要更多时间和资源,Kubernetes 也是一个很好的选择,因为它要求您将代码锁定到离散的、有版本的容器中,这也可能需要大量的时间和处理。

使用 Kubernetes 进行测试的模式

有很多种方式可以使用 Kubernetes 进行测试,而您需要确定的第一件事情之一是,您正在运行被测试系统的位置,以及您正在运行将验证该系统的测试的位置。

在 Kubernetes 中测试本地和系统测试

最常见的模式,特别是在开发测试时,是从开发机器上运行测试,针对在 Kubernetes 中运行的代码。创建测试后,可以使用相同的模式来针对托管代码的 Kubernetes 集群运行测试,从而实现持续集成服务。当您开始进行开发时,您可能能够在本地开发机器上运行所有这些操作,使用 Minikube。总的来说,这种模式是一个很好的开始方式,并解决了通过在您想要获得反馈的地方运行测试来获得反馈的问题——无论是在您自己的开发系统上,还是在代表您运行的 CI 系统上。

在之前的模式中,以及本书中大多数示例中,我们使用了默认命名空间,但所有命令都可以通过简单地在 kubectl 命令中添加-n <namespace>来包括一个命名空间作为选项。

如果您测试的系统超出了 Minikube 的支持范围,常见的解决方案是开始使用远程集群,无论是由您、您的 IT 团队还是云提供商管理。当您开始使用远程计算时,共享和隔离变得重要,特别是对于依赖于系统状态的测试,其中对该状态的控制对于理解验证是否正确非常关键。Kubernetes 通常具有良好的隔离性,并且利用命名空间的工作方式可以使代码的设置和测试变得更加容易。您可以通过在单个命名空间中运行相关的 pod 和服务,并通过利用每个服务的短 DNS 名称在它们之间进行一致引用来利用命名空间。这可以被视为一个堆栈,您可以有效地并行部署许多这样的堆栈。

命名空间支持各种资源的配额,您会想要查看定义的内容并验证您是否设置了足够的配额。特别是在共享环境中,使用配额来限制消耗是常见的。

Kubernetes 中的测试和 Kubernetes 命名空间中的系统

主题的一个变化是在 Kubernetes 中打包和运行您的测试 - 无论是在相同的命名空间中,还是在与您的被测系统不同的命名空间中。这比在本地运行测试要慢,因为它要求您将测试打包到容器中,就像您对代码所做的那样。权衡是拥有非常一致的方式来运行这些测试并与被测系统进行交互。

如果您在一个非常多样化的开发环境中工作,每个人的设置都略有不同,那么这种模式可以整合测试,以便每个人都有相同的体验。此外,当本地测试需要通过暴露的服务(例如使用 Minikube 的 NodePort,或者在提供程序上使用LoadBalancer)访问远程 Kubernetes 时,您可以通过使用服务名称来简化访问,无论是在相同的命名空间中还是在包含命名空间的较长的服务名称中。

在 Kubernetes 中运行测试的另一个挑战是获取结果。虽然完全可以收集结果并将其发布到远程位置,但这种模式并不常见。使用这种模式时更常见的解决方案是拥有一个专门用于测试的集群,其中还包括一些持续集成基础设施,可以作为集群的一部分,或者与集群并行并具有专用访问权限,然后运行测试并捕获结果作为测试自动化的一部分。我们将在本章后面更深入地研究持续集成。

使用 Bats 进行简单验证

一个相当普遍的愿望是简单地部署所有内容并进行一些查询,以验证生成的系统是否可操作。当您执行这些操作时,它们经常被捕获在 Makefiles 或 shell 脚本中,作为验证功能基线的简单程序。几年前,开发了一个名为 Bats 的系统,它代表 Bash 自动化测试系统,旨在使使用 shell 脚本运行测试变得更加方便。

有几个示例使用 Bats 来测试部署在 Kubernetes 中的系统。这些测试通常很简单易懂,易于扩展和使用。您可以在其 GitHub 主页github.com/sstephenson/bats上找到更多关于 Bats 的信息。您可能也会在一些与 Kubernetes 相关的项目中看到 Bats 的使用,用于简单验证。

Bitnami 已经建立了一个示例 GitHub 存储库,用作使用 Bats 和 Minikube 的起点,并且设计为与 Travis.CI 等外部 CI 系统一起使用。您可以在github.com/bitnami/kubernetes-travis找到示例。

如果您使用 Bats,您将需要有辅助脚本来设置您的部署,并等待相关部署报告就绪,或者在设置时失败测试。在 Bitnami 示例中,脚本cluster_common.bashlibtest.bash具有这些辅助函数。如果您想使用这条路径,可以从他们的存储库中开始,并更新和扩展它们以匹配您的需求。

集成测试从加载库和创建本地集群开始,然后部署正在测试的系统:

# __main__ () {
. scripts/cluster_common.bash
. scripts/libtest.bash
# Create the 'minikube' or 'dind' cluster
create_k8s_cluster ${TEST_CONTEXT}
# Deploy our stack
bats tests/deploy-stack.bats

deploy-stacks.bats可以表示为一个 Bats 测试,在 Bitnami 示例中,它验证了 Kubernetes 工具在本地是否都已定义,然后将部署本身封装为一个测试:

这是来自示例github.com/bitnami/kubernetes-travis/blob/master/tests/deploy-stack.bats

# Bit of sanity
@test "Verify needed kubernetes tools installed" {
 verify_k8s_tools
}
@test "Deploy stack" {
# Deploy the stack we want to test
./scripts/deploy.sh delete >& /dev/null || true
./scripts/deploy.sh create
   k8s_wait_for_pod_running --namespace=kube-system -lname=traefik-ingress-lb
   k8s_wait_for_pod_running -lapp=my-nginx
}

脚本deploy.sh设置为删除或创建和加载清单,就像我们在本书中早些时候所做的那样,使用kubectl createkubectl deletekubectl apply命令。

完成后,集成测试继续获取对集群的访问。在 Bitnami 示例中,他们使用 Kubernetes Ingress 来一致地访问集群,并设置脚本来捕获和返回访问底层系统的 IP 地址和 URL 路径通过Ingress。您也可以使用kubectl port-forwardkubectl proxy,就像我们在本书中早些时候展示的那样:

# Set env vars for our test suite
# INGRESS_IP: depend on the deployed cluster (dind or minikube)
INGRESS_IP=$(get_ingress_ip ${TEST_CONTEXT})
# URL_PATH: Dynamically find it from 1st ingress rule
URL_PATH=$(kubectl get ing -ojsonpath='{.items[0].spec.rules[0].http.paths[0].path}')
# Verify no empty vars:
: ${INGRESS_IP:?} ${URL_PATH:?}

设置完成后,再次使用 Bats 调用集成测试,并捕获整个过程的退出代码,并用于反映测试是否成功或失败:

# With the stack ready, now run the tests thru bats:
export SVC_URL="http://my-nginx.default.svc${URL_PATH:?}"
export ING_URL="${INGRESS_IP:?}${URL_PATH:?}"
bats tests/integration-tests.bats
exit_code=$?

[[ ${exit_code} == 0 ]] && echo "TESTS: PASS" || echo "TESTS: FAIL"
exit ${exit_code}
# }

虽然这很容易入门,但在 bash 中编程很快就成为了自己的专业领域,而基本的 bash 使用频繁且易于理解,但在该示例中的一些更复杂的辅助功能可能需要一些挖掘才能完全理解。

如果您在使用 shell 脚本时遇到问题,常见的调试解决方案是在脚本顶部附近添加set -x。在 bash 中,这会打开命令回显,以便将脚本中的所有命令回显到标准输出,以便您可以看到发生了什么。

一个很好的模式是使用您熟悉的语言编写测试。您经常可以利用这些语言的测试框架来帮助您。您可能仍然希望使用像 Bitnami 示例那样的 shell 脚本来设置和部署代码到您的集群,并且对于测试,使用您更熟悉的语言的逻辑和结构。

示例 - 使用 Python 进行集成测试

在 Python 的情况下,这里的示例代码使用 PyTest 作为测试框架。示例代码可以在 GitHub 上找到,位于存储库的 0.7.0 分支中github.com/kubernetes-for-developers/kfd-flask/

您可以使用以下命令下载示例:

git clone https://github.com/kubernetes-for-developers/kfd-flask/ -b 0.7.0

在这个示例中,我改变了代码结构,将应用程序本身的所有 Python 代码移动到src目录下,遵循了 PyTest 的推荐模式。如果您以前没有使用过 PyTest,请查看他们的最佳实践docs.pytest.org/en/latest/goodpractices.html,这是非常值得的。

如果您查看代码或下载它,您还会注意到一个新文件test-dependencies.txt,其中定义了一些特定于测试的依赖项。Python 没有一个将生产环境的依赖项与开发或测试中使用的依赖项分开的清单,所以我自己分开了这些依赖项:

pytest
pytest-dependency
kubernetes
requests

实际的集成测试存放在e2e_tests目录下,主要作为一个模式,让您在正常开发过程中可以有一个本地目录用于创建任何单元测试或功能测试。

我在这个示例中使用的模式是利用我们在 Kubernetes 中的代码,并在集群外部访问它,利用 Minikube。如果您的环境需要比您本地开发机器上可用的资源更多,同样的模式也可以很好地与托管在 AWS、Google 或 Azure 中的集群配合使用。

e2e_tests中的README文件显示了如何运行测试的示例。我利用pipvirtualenv来设置本地环境,安装依赖项,然后使用 PyTest 直接运行测试:

virtualenv .venv
source .venv/bin/activate
pip3 install -r test-requirements.txt
pytest -v

如果你运行这些测试,你应该会看到类似以下的输出:

======= test session starts =======
platform darwin -- Python 3.6.4, pytest-3.4.2, py-1.5.2, pluggy-0.6.0 -- /Users/heckj/src/kfd-flask/e2e_tests/.venv/bin/python3.6
cachedir: .pytest_cache
rootdir: /Users/heckj/src/kfd-flask/e2e_tests, inifile:
plugins: dependency-0.3.2
collected 7 items

tests/test_smoke.py::test_kubernetes_components_healthy PASSED [ 14%]
tests/test_smoke.py::test_deployment PASSED [ 28%]
tests/test_smoke.py::test_list_pods PASSED [ 42%]
tests/test_smoke.py::test_deployment_ready PASSED [ 57%]
tests/test_smoke.py::test_pods_running PASSED [ 71%]
tests/test_smoke.py::test_service_response PASSED [ 85%]
tests/test_smoke.py::test_python_client_service_response PASSED [100%]

======= 7 passed in 1.27 seconds =======

PyTest 包括大量的插件,包括一种以 JUnit XML 格式导出测试结果的方法。您可以通过使用--junitxml选项调用 PyTest 来获得这样的报告:

pytest --junitxml=results.xml

这些测试中的代码利用了我们迄今为止构建的示例:我们的部署 YAML 和我们用代码在存储库中制作的图像。测试对集群的可用性和健康进行了简单的验证(以及我们是否可以与其通信),然后使用kubectl来部署我们的代码。然后等待代码部署,定义了最大超时时间,然后继续与服务交互并获得简单的响应。

这个例子主要是为了向您展示如何与远程 Kubernetes 集群交互,包括使用python-kubernetes客户端库。

PyTest 和 pytest-dependency

PyTest 首先是一个单元测试框架。单元测试框架通常对集成测试有不同的需求,幸运的是,PyTest 有一种方法允许开发人员指定一个测试需要在另一个测试之前运行和完成。这是通过pytest-dependency插件完成的。在代码中,您会看到一些测试用例被标记为依赖标记。要使用这个插件,您需要定义哪些测试可以成为依赖目标,以及任何需要在其后运行的测试,您需要定义它们依赖的测试:

@pytest.mark.dependency()
def test_kubernetes_components_healthy(kube_v1_client):
    # iterates through the core kuberneters components to verify the cluster is reporting healthy
    ret = kube_v1_client.list_component_status()
    for item in ret.items:
        assert item.conditions[0].type == "Healthy"
        print("%s: %s" % (item.metadata.name, item.conditions[0].type))

这个测试检查集群是否可访问并且响应正常。这个测试不依赖于其他任何测试,所以它只有基本的注释,而下面的测试将指定这个测试需要在运行之前完成,使用这个注释:

@pytest.mark.dependency(depends=["test_kubernetes_components_healthy"])

这可能会使测试注释非常冗长,但允许您明确定义执行顺序。默认情况下,大多数单元测试框架不保证特定的执行顺序,当您测试包含状态和对该状态的更改的系统时,这可能是至关重要的——这正是我们进行集成测试的内容。

PyTest 固定装置和 python-kubernetes 客户端

前面的示例还利用了一个简单的文本 fixture,为我们提供了一个 Python Kubernetes 客户端的实例,以便与集群进行交互。Python 客户端可能难以使用,因为它是从 OpenAPI 规范生成的,并且对于每个 API 端点都有类设置,而这些端点有好几个。特别是,随着 Kubernetes API 的各个部分通过 alpha、beta 和最终发布阶段的演变,这些 API 端点将移动,这意味着您使用的客户端代码可能需要随着您与之交互的 Kubernetes 集群版本的升级而更改。

python-kubernetes客户端确实带有现成的源代码和所有方法的生成索引,我建议如果您要使用客户端,最好随时准备好这些。代码存放在github.com/kubernetes-client/python,发布版本存储在分支中。我使用的版本是 5.0,与 Kubernetes 版本 1.9 配对,并支持早期版本。包含所有 OpenAPI 生成方法文档的README可在github.com/kubernetes-client/python/blob/release-5.0/kubernetes/README.md找到。

一个 PyTest fixture 为其他测试设置了客户端:

@pytest.fixture
def kube_v1_client():
    kubernetes.config.load_kube_config()
    v1 = kubernetes.client.CoreV1Api()
    return v1

在这种情况下,客户端加载本地可用的kubeconfig以访问集群。根据您的开发环境,您可能需要调查其他身份验证到集群的替代方法。

虽然可以使用 python-kubernetes 客户端进行部署,但示例还展示了如何使用本地kubectl命令行与集群进行交互。在这种情况下,与在 Python 中定义要部署的完整定义相比,代码行数要少得多:

@pytest.mark.dependency(depends=["test_kubernetes_components_healthy"])
def test_deployment():
    # https://docs.python.org/3/library/subprocess.html#subprocess.run
    # using check=True will throw an exception if a non-zero exit code is returned, saving us the need to assert
    # using timeout=10 will throw an exception if the process doesn't return within 10 seconds
    # Enables the deployment
    process_result = subprocess.run('kubectl apply -f ../deploy/', check=True, shell=True, timeout=10)

如果您想利用其他工具部署您的代码,这种机制可能非常有价值,并且在编写集成测试时始终是一个有用的后备。还要注意,这个测试依赖于我们之前提到的测试,强制它在集群健康验证测试之后运行。

请注意,当系统失败时调试这些命令可能会更加困难,因为很多事情都发生在实际测试之外,比如这样的命令。您需要了解调用测试的进程,它相对于您的环境的权限等。

等待状态变化

部署后,我们期望部署和服务都变为活动状态,但这并不是瞬间发生的。根据您的环境,它可能会发生得非常快,也可能会发生得相当慢。集成测试的问题在于无法知道何时完成某些操作,并通过调用sleep()来解决问题,等待更长时间。在这个例子中,我们明确检查状态,而不是只等待任意时间,希望系统已准备就绪:

@pytest.mark.dependency(depends=["test_deployment_ready"])
def test_pods_running(kube_v1_client):
    TOTAL_TIMEOUT_SECONDS = 300
    DELAY_BETWEEN_REQUESTS_SECONDS = 5
    now = time.time()
    while (time.time() < now+TOTAL_TIMEOUT_SECONDS):
        pod_list = kube_v1_client.list_namespaced_pod("default")
        print("name\tphase\tcondition\tstatus")
        for pod in pod_list.items:
            for condition in pod.status.conditions:
                print("%s\t%s\t%s\t%s" % (pod.metadata.name, pod.status.phase, condition.type, condition.status))
                if condition.type == 'Ready' and condition.status == 'True':
                    return
        time.sleep(DELAY_BETWEEN_REQUESTS_SECONDS)
    assert False

此示例的部署最大超时时间为300秒,包括在继续之前请求环境状态的短暂延迟。如果超过总超时时间,测试将报告失败,并且通过使用pytest-dependency,所有依赖于此的后续测试都不会运行,从而中断测试过程以报告失败。

访问部署

最后两个测试突出了与集群内运行的代码交互的两种方式。

第一个示例期望设置并运行提供对测试之外的集群的访问,并简单地使用 Python 的requests库直接发出 HTTP 请求:

@pytest.mark.dependency(depends=["test_deployment_ready"])
def test_service_response(kubectl_proxy):
    NAMESPACE="default"
    SERVICE_NAME="flask-service"
    URI = "http://localhost:8001/api/v1/namespaces/%s/services/%s/proxy/" % (NAMESPACE, SERVICE_NAME)
    print("requesting %s" % (URI))
    r = requests.get(URI)
    assert r.status_code == 200

这是一个非常基本的测试,而且相当脆弱。它使用了代码中早期定义的 PyTest 夹具来设置kubectl proxy的调用,以提供对集群的访问:

@pytest.fixture(scope="module")
def kubectl_proxy():
    # establish proxy for kubectl communications
    # https://docs.python.org/3/library/subprocess.html#subprocess-replacements
    proxy = subprocess.Popen("kubectl proxy &", stdout=subprocess.PIPE, shell=True)
    yield
    # terminate the proxy
    proxy.kill()

虽然这通常有效,但当事情失败时,要追踪问题就更难了,而且在设置(和拆除)分叉 shell 命令中,夹具机制并不完全可靠。

第二个示例使用 python-kubernetes 客户端通过一系列方法访问服务,这些方法允许您通过 Kubernetes 附带的代理轻松调用 HTTP 请求。客户端配置负责对集群进行身份验证,并且您可以通过直接利用客户端而不是使用外部代理来访问代码,通过代理访问:

@pytest.mark.dependency(depends=["test_deployment_ready"]) def test_python_client_service_response(kube_v1_client):
    from pprint import pprint
    from kubernetes.client.rest import ApiException
    NAMESPACE="default"
    SERVICE_NAME="flask-service"
    try:
        api_response = kube_v1_client.proxy_get_namespaced_service(SERVICE_NAME, NAMESPACE)
        pprint(api_response)
        api_response = kube_v1_client.proxy_get_namespaced_service_with_path(SERVICE_NAME, NAMESPACE, "/metrics")
        pprint(api_response)
    except ApiException as e:
        print("Exception when calling CoreV1Api->proxy_get_namespaced_service: %s\n" % e)

如果您不需要在 HTTP 请求中操纵标头或以其他方式复杂化,这种机制非常适用,当使用通用的 Python 客户端(如requests)时更易于访问。有一整套支持各种 HTTP/REST 风格调用的方法,所有这些方法都以proxy为前缀:

  • proxy_get

  • proxy_delete

  • proxy_head

  • proxy_options

  • proxy_patch

  • proxy_put

每个都映射到以下端点:

  • namespaced_pod

  • namespaced_pod_with_path

  • namespaced_service

  • namespaced_service_with_path

这使您可以在标准的 REST 命令中发送命令,直接发送到 pod 或服务端点。with_path选项允许您定义与 pod 或服务上交互的特定 URI。

示例-使用 Node.js 进行集成测试

Node.js 示例与 Python 示例类似,使用了 mocha、chai、supertest 和 JavaScript kubernetes 客户端。示例代码可以在 GitHub 上找到,位于存储库的 0.7.0 分支中github.com/kubernetes-for-developers/kfd-nodejs/

您可以使用以下命令下载示例:

git clone https://github.com/kubernetes-for-developers/kfd-nodejs/ -b 0.7.0

我利用了 Node.js 的机制,将开发依赖项与生产依赖项分开,并将大部分这些依赖项添加到了package.json中。我还继续在test目录中直接设置了一个简单的单元测试,并在e2e-tests目录中设置了一个单独的集成测试。我还设置了命令,以便您可以通过npm运行这些测试:

npm test

对于单元测试,代码在本地运行,并利用supertest来访问本地计算机上的 JavaScript 运行时中的所有内容。这不包括任何远程服务或系统(例如与依赖于 Redis 的端点进行交互):

> kfd-nodejs@0.0.0 test /Users/heckj/src/kfd-nodejs
> mocha --exit

express app
GET / 200 283.466 ms - 170
 ✓ should respond at the root (302ms)
GET /probes/alive 200 0.930 ms - 3
 ✓ should respond at the liveness probe point

 2 passing (323ms)

e2e_tests目录中,有一个类似于 Python 测试的模拟,用于验证集群是否正常运行,设置部署,然后访问该代码。可以使用以下命令调用此模拟:

npm run integration

调用测试将显示类似以下内容:

> kfd-nodejs@0.0.0 integration /Users/heckj/src/kfd-nodejs
> mocha e2e_tests --exit

kubernetes
 cluster
 ✓ should have a healthy cluster
 ✓ should deploy the manifests (273ms)
 should repeat until the pods are ready
 - delay 5 seconds...
 ✓ check to see that all pods are reporting ready (5016ms)
 should interact with the deployed services
 ✓ should access by pod...

 4 passing (5s)

使用 mocha 和 chai 的 Node.js 测试和依赖项

测试代码本身位于e2e_tests/integration_test.js,我利用 mocha 和 chai 以 BDD 风格的结构布置了测试。使用 mocha 和 chai 的 BDD 结构的一个便利的副作用是,测试可以由describeit包装,这样结构化了测试的运行方式。describe块内的任何内容都没有保证的顺序,但您可以嵌套describe块以获得所需的结构。

验证集群健康

JavaScript Kubernetes 客户端与 Python 客户端以类似的方式生成,从 OpenAPI 定义中映射到 Kubernetes 的发布版本。你可以在github.com/kubernetes-client/javascript找到客户端,尽管这个存储库没有与 Python 客户端相同级别的生成文档。相反,开发人员已经花了一些精力用 TypeScript 反映了客户端中的类型,这导致编辑器和 IDE 在编写测试时能够做一定程度的自动代码补全:

const k8s = require('@kubernetes/client-node');
var chai = require('chai')
 , expect = chai.expect
 , should = chai.should();

var k8sApi = k8s.Config.defaultClient();

describe('kubernetes', function() {
  describe('cluster', function() {
    it('should have a healthy cluster', function() {
       return k8sApi.listComponentStatus()
       .then((res) => {
         // console.log(util.inspect(res.body));
         res.body.items.forEach(function(component) {
         // console.log(util.inspect(value));
         expect(component.conditions[0].type).to.equal("Healthy");
         expect(component.conditions[0].status).to.equal("True");
       })
     }, (err) => {
        expect(err).to.be.null;
     });
   }) // it

代码的嵌套可能会使缩进和跟踪正确级别变得相当棘手,因此测试代码利用 promise 来简化回调结构。前面的示例使用了一个 Kubernetes 客户端,它会自动从运行它的环境中获取凭据,这是几个这些客户端的特性,因此如果你希望安排特定的访问,要注意这一点。

Python 客户端有一个方法list_component_status,而 JavaScript 模式则使用 CamelCase 格式将名称紧凑在一起,因此这里的调用是listComponentStatus。然后结果通过一个 promise 传递,我们遍历各种元素来验证集群组件是否都报告为健康状态。

示例中留下了一些被注释掉的代码,用于检查返回的对象。由于外部文档很少,我发现在开发测试时查看返回的内容很方便,常见的技巧是使用util.inspect函数并将结果记录到STDOUT中:

const util = require('util');
console.log(util.inspect(res.body));

使用 kubectl 部署

在 Python 示例之后,我在命令行上使用kubectl部署代码,从集成测试中调用它:

it('should deploy the manifests', function() {
  var manifest_directory = path.normalize(path.join(path.dirname(__filename), '..', '/deploy'))
  const exec = util.promisify(require('child_process').exec);
  return exec('kubectl apply -f '+manifest_directory)
  .then((res) => {
    // console.log(util.inspect(res));
    expect(res.stdout).to.not.be.null;
    expect(res.stderr).to.be.empty;
  }, (err) => {
    expect(err).to.be.null;
  })
})

这段特定的代码取决于你在哪里有这个测试用例,以及它相对于存储清单的部署目录的位置,就像前面的示例一样,它使用 promises 来链接调用的执行的验证。

等待 pod 变得可用

等待和重试的过程在 Node.js、promises 和 callbacks 中更加棘手。在这种情况下,我利用了 mocha 测试库的一个功能,允许对测试进行重试,并操纵测试结构的整体超时,以获得相同的结果:

describe('should repeat until the pods are ready', function() {
  // Mocha supports a retry mechanism limited by number of retries...
  this.retries(30);
  // an a default timeout of 20,000ms that we can increase
  this.timeout(300000);

it('check to see that all pods are reporting ready', function() {
   return new Promise(function(resolve, reject) {
       console.log(' - delay 5 seconds...')
       setTimeout(() => resolve(1), 5000);
   }).then(function(result) {
       return k8sApi.listNamespacedPod('default')
      .then((res) => {
         res.body.items.forEach(function(pod) {
           var readyCondition = _.filter(pod.status.conditions, { 'type': 'Ready' })
          //console.log("checking: "+pod.metadata.name+" ready: "+readyCondition[0].status);
          expect(readyCondition[0].status).to.equal('True')
        }) // pod forEach
    })
  })
}) // it

}) // describe pods available

通过在测试中返回 promises,每个测试已经是异步的,并且具有 mocha 提供的预设超时为20秒。在每个describe中,您可以调整 mocha 运行测试的方式,例如将整体超时设置为五分钟,并断言测试最多可以重试30次。为了减慢检查迭代,我还包括了一个超时 promise,它在调用集群检查之前引入了五秒的延迟。

与部署进行交互

与 Python 示例相比,与部署进行交互的代码更简单,利用了 Kubernetes 客户端和代理:

describe('should interact with the deployed services', function() {
  // path to access the port through the kubectl proxy:
  // http://localhost:8001/api/v1/namespaces/default/services/nodejs-service:web/proxy/
 it('should access by pod...', function() {
   return k8sApi.proxyGETNamespacedServiceWithPath("nodejs-service:web", "default", "/")
   .then(function(res) {
      // console.log(util.inspect(res,{depth:1}));
      expect(res.body).to.not.be.null;
    });
  })
}) // interact with the deployed services

在这个分支中,我将运行的代码从有状态集更改为部署,因为获取对无头端点的代理访问证明很复杂。有状态集可以通过 DNS 轻松从集群内部访问,但在当前客户端代码中似乎不容易支持映射到外部。

与 Python 代码一样,有一系列调用可以通过客户端进行 REST 风格的请求:

  • proxyGET

  • proxyDELETE

  • proxyHEAD

  • proxyOPTIONS

  • proxyPATCH

  • proxyPUT

并且每个都映射到端点:

  • namespacedPod

  • namespacedPodWithPath

  • namespacedService

  • namespacedServiceWithPath

这为您提供了一些灵活性,可以将标准的 REST 命令发送到 Pod 直接或服务端点。与 Python 代码一样,withPath选项允许您定义与 Pod 或服务上交互的特定 URI。

如果您在诸如 Visual Studio Code 之类的编辑器中编写这些测试,代码完成将帮助提供一些在文档中否则缺失的细节。以下是代码完成显示method选项的示例:

当您选择一种方法时,TypeScript 注释也可用于显示 JavaScript 方法期望的选项:

与 Kubernetes 的持续集成

一旦您有集成测试,获取一些操作来验证这些测试非常重要。如果您不运行测试,它们实际上是无用的-因此在开发过程中始终调用测试的方法非常重要。通常会看到持续集成为开发做了大量自动化工作。

开发团队有许多选项可帮助您进行持续集成,甚至是更高级的持续部署。以下工具是在撰写时可用的概述,并且由使用容器和/或 Kubernetes 中的代码的开发人员使用:

  • Travis.CI:Travis.CI(travis-ci.org/)是一个托管的持续集成服务,因为该公司提供了免费服务,并且可以轻松地与 GitHub 对接以用于公共和开源存储库。相当多的开源项目利用 Travis.CI 进行基本测试验证。

  • Drone.IO:Drone.IO(drone.io/)是一个托管或本地的持续集成选项,也是开源软件本身,托管在github.com/drone/drone。Drone 拥有广泛的插件库,包括一个 Helm 插件(github.com/ipedrazas/drone-helm),这使得它对一些使用 Helm 部署软件的开发团队很有吸引力。

  • Gitlab:Gitlab(about.gitlab.com/)是一个开源的源代码控制解决方案,包括持续集成。与 Drone 一样,它可以在您的本地环境中使用,或者您可以使用托管版本。之前的选项对源代码控制机制是不可知的,Gitlab CI 与 Gitlab 紧密绑定,有效地使其只有在您愿意使用 Gitlab 时才有用。

  • Jenkins:Jenkins(jenkins.io/)是 CI 解决方案的鼻祖,最初被称为 Hudson,并且在各种环境中被广泛使用。一些提供商提供了 Jenkins 的托管版本,但它主要是一个您需要自己部署和管理的开源解决方案。它有大量(也许是压倒性的)插件和选项可供选择,特别是一个 Kubernetes 插件(github.com/jenkinsci/kubernetes-plugin),可以让 Jenkins 实例在 Kubernetes 集群中运行其测试。

  • Concourse:Concourse(concourse-ci.org/),类似于 Jenkins,是一个开源项目,而不是一个托管解决方案,它是在 CloudFoundry 项目中构建的,专注于部署管道作为一种第一类概念(对于一些较老的项目,如 Jenkins,它相对较新)。与 Drone 一样,它被设置为一个持续交付管道,并且是您开发过程的一个重要部分。

示例-在 Travis.CI 中使用 Minikube

之前的示例展示了使用 Bats 运行测试,是由 Bitnami 团队创建的,并且他们还利用了相同的示例存储库来构建和部署代码到托管在 Travis.CI 上的 Minikube 实例。他们的示例存储库在线上github.com/bitnami/kubernetes-travis,它安装了 Minikube 以及其他工具来构建和部署到一个小的 Kubernetes 实例。

Travis.CI 通过一个.travis.yml文件进行配置,有关如何配置以及可用选项的文档托管在docs.travis-ci.com上。Travis.CI 默认情况下会尝试理解正在使用的语言,并将其构建脚本定位到该语言,主要专注于对每个拉取请求和合并到存储库的构建进行运行。

Node.js 示例添加了一个示例.travis.yml,用于设置和运行当前的集成测试:

language: node_js
node_js:
 - lts/*
cache:
 directories:

 - "node_modules"
sudo: required
services:
 - docker
env:
- CHANGE_MINIKUBE_NONE_USER=true

before_script:
- curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v1.9.0/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/
- curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/
- sudo minikube start --vm-driver=none --kubernetes-version=v1.9.0
- minikube update-context
- JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl get nodes -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 1; done

script:
- npm run integration

language在我们的示例中设置为nodejs,它定义了 Travis 的运行方式的很大一部分。我们定义了使用哪些版本的 Node.js(lts/*),默认情况下系统会使用npm,运行npm test来验证我们的构建。这将运行我们的单元测试,但不会调用我们的集成测试。

您可以通过操纵键before_scriptscript下的值来扩展测试之前发生的事情以及测试使用的内容。在前面的示例中,我们通过从它们的发布位置下载它们来预加载minikubekubectl,然后启动 Minikube 并等待直到命令kubectl get nodes返回正面结果。

通过在关键脚本下添加npm run integration,我们覆盖了默认的 Node.js 行为,而是运行我们的集成测试。当示例被开发时,更新被推送到了 0.7.0 分支,该分支作为主存储库的拉取请求是开放的。这些更新的结果被发布到托管解决方案,可在travis-ci.org/kubernetes-for-developers/kfd-nodejs上找到。例如,以下是一个显示成功构建的构建页面:

下一步

这个示例构建并不涵盖从源代码到容器再到部署的整个过程。相反,它依赖于在源代码控制中管理的预构建镜像,并在部署清单中设置了标签。Travis.CI 确实包括使用 Docker 构建镜像的能力,并有关于如何利用 Docker 测试单个容器的文档,网址为docs.travis-ci.com/user/docker/

Travis 还具有存储凭据以构建和推送 Docker 镜像到镜像存储库的能力,并最近增加了分阶段构建的能力,这样您就可以在容器构建中进行流水线处理,然后在集成测试中利用它。

您需要更新 Kubernetes 声明以使用相关镜像,而这个示例并没有展示这个过程。启用这种功能的常见模式涉及对我们在示例中存储在 deploy 目录中的清单进行模板化,并使用传入的特定变量进行渲染。

Helm (docs.helm.sh/) 是实现这一需求的一种方式:我们可以有一个charts目录,而不是一个带有清单的deploy目录,并将清单编写为模板。 Helm 使用values文件,可以根据需要创建,以提供用于渲染模板的变量,并在创建带有标签的 Docker 镜像后,该标签值可以添加到values文件中并用于部署。

另一个选择是一个名为 ksonnet 的新项目(ksonnet.io),它构建在一个开源库jsonnet.org/上,以提供一个基于原型的可组合模板样式语言,用于构建 Kubernetes。ksonnet 相对较新,仍在建立中。使用 Helm,您可以利用 Go 模板,并且在创建图表时需要对该格式有一定的了解。ksonnet 有自己的模板编写风格,您可以在项目网站上找到教程和示例:ksonnet.io/tour/welcome

示例-使用 Jenkins 和 Kubernetes 插件

虽然不是托管解决方案,但 Jenkins 是最常用的持续集成工具之一。在 Kubernetes 集群上运行 Jenkins 实例非常简单,并且由于 Kubernetes 特定插件的存在,它还可以在 Kubernetes 集群中进行所有构建。

以这种方式安装 Jenkins 的最快方法之一是使用 Helm。默认的 Helm 存储库包括一个维护的图表,用于运行 Jenkins,以及使用 Jenkins Kubernetes 插件的配置。我们将使用的图表可在 GitHub 上找到github.com/kubernetes/charts/tree/master/stable/jenkins。您还可以在该图表安装的 Jenkins Kubernetes 插件的详细信息wiki.jenkins.io/display/JENKINS/Kubernetes+Plugin

使用 Helm 安装 Jenkins

在这个示例中,我将演示如何在 Minikube 集群上设置和安装 Jenkins 到您的本地机器,以便进行实验。您可以使用非常类似的过程安装到任何 Kubernetes 集群,但是您需要根据目标集群进行一些修改。

如果您的笔记本电脑上尚未安装 Helm,可以按照项目网站上的说明进行安装:docs.helm.sh/using_helm/#installing-helm。一旦在本地系统上安装了命令行客户端,您就可以启动其余的工作。

第一步是将 Helm 安装到您的集群并更新存储库。这可以通过运行两个命令来完成:

helm init

输出将非常简洁,类似于以下内容:

$HELM_HOME has been configured at /Users/heckj/.helm.

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.

Please note: by default, Tiller is deployed with an insecure 'allow unauthenticated users' policy.
For more information on securing your installation see: https://docs.helm.sh/using_helm/#securing-your-helm-installation
Happy Helming!

正如它提到的那样,Tiller 是 Helm 的服务器端组件,负责协调从helm命令行工具调用的安装。默认情况下,helm init将 Tiller 安装到kube-system命名空间中,因此您可以使用以下命令在集群中查看它:

kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
coredns-599474b9f4-gh99f 1/1 Running 0 3m
kube-addon-manager-minikube 1/1 Running 0 3m
kubernetes-dashboard-77d8b98585-f4qh9 1/1 Running 0 3m
storage-provisioner 1/1 Running 0 3m
tiller-deploy-865dd6c794-5b9g5 1/1 Running 0 3m

一旦处于Running状态,最好加载最新的存储库索引。它已经安装了许多图表,但是图表会定期更新,这将确保您拥有最新的图表:

helm repo update

更新过程通常非常快,返回类似以下内容:

Hang tight while we grab the latest from your chart repositories...
...Skip local chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈

它提到的stable图表存储库是托管在 GitHub 上的 Kubernetes 项目的一个:github.com/kubernetes/charts。在该存储库中,有一个包含所有图表的stable目录。如果您使用helm search命令,它将显示图表和相关版本的列表,与 GitHub 存储库匹配。

使用helm search jenkins命令将显示我们将要使用的目标:

NAME CHART VERSION APP VERSION DESCRIPTION
stable/jenkins 0.14.1 2.73 Open source continuous integration server. It s...

请注意,图表除了报告的应用程序版本外,还有图表版本。许多图表包装现有的开源项目,并且图表与它们部署的系统分开维护。Kubernetes 项目中的stable存储库中的图表力求成为构建图表的示例,并且对整个社区有用。在这种情况下,图表版本是0.14.1,并且报告部署 Jenkins 版本为2.73

您可以使用helm inspect命令获取有关特定图表的更多详细信息,例如:

 helm inspect stable/jenkins

这将向您显示大量的输出,从以下内容开始:

appVersion: "2.73"
description: Open source continuous integration server. It supports multiple SCM tools
 including CVS, Subversion and Git. It can execute Apache Ant and Apache Maven-based
 projects as well as arbitrary scripts.
home: https://jenkins.io/
icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png
maintainers:
- email: lachlan.evenson@microsoft.com
 name: lachie83
- email: viglesias@google.com
 name: viglesiasce
name: jenkins
sources:
- https://github.com/jenkinsci/jenkins
- https://github.com/jenkinsci/docker-jnlp-slave
version: 0.14.1

---
# Default values for jenkins.
# This is a YAML-formatted file.
# Declare name/value pairs to be passed into your templates.
# name: value

## Overrides for generated resource names
# See templates/_helpers.tpl
# nameOverride:
# fullnameOverride:

Master:
 Name: jenkins-master
 Image: "jenkins/jenkins"
 ImageTag: "lts"
 ImagePullPolicy: "Always"
# ImagePullSecret: jenkins
 Component: "jenkins-master"
 UseSecurity: true

顶部是输入图表存储库索引的信息,用于提供helm search命令的结果,之后的部分是图表支持的配置选项。

大多数图表都力求具有并使用良好的默认值,但是预期您可能会在适当的地方提供覆盖的值。在将 Jenkins 部署到 Minikube 的情况下,我们将要这样做,因为图表使用的默认values.yaml期望使用LoadBalancer,而 Minikube 不支持。

您可以在helm inspect的扩展输出中查看values.yaml的完整详细信息。在使用 Helm 安装任何内容之前,最好看看它代表您做了什么,以及它提供了哪些配置值。

我们将创建一个小的yaml文件来覆盖默认值之一:Master.ServiceType。如果您扫描helm inspect命令的输出,您将看到将其更改以在 Minikube 上安装的引用。

创建一个名为jenkins.yaml的文件,内容如下:

Master:
  ServiceType: NodePort

现在,我们可以看到当我们要求其安装时 Helm 将创建什么,使用--dry-run--debug选项获取详细输出:

helm install stable/jenkins --name j \
-f jenkins.yaml --dry-run --debug

运行此命令将向您的终端屏幕转储大量信息,即 Helm 将代表您安装的所有内容的呈现清单。您可以看到部署、秘密、配置映射和服务。

您可以通过运行完全相同的命令来开始安装过程,减去--dry-run--debug选项:

helm install stable/jenkins --name j -f jenkins.yaml

这将为您提供它创建的所有 Kubernetes 对象的列表,然后是一些注释:

NAME: j
LAST DEPLOYED: Sun Mar 11 20:33:34 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Pod(related)
NAME READY STATUS RESTARTS AGE
j-jenkins-6ff797cc8d-qlhbk 0/1 Init:0/1 0 0s
==> v1/Secret
NAME TYPE DATA AGE
j-jenkins Opaque 2 0s
==> v1/ConfigMap
NAME DATA AGE
j-jenkins 3 0s
j-jenkins-tests 1 0s
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
j-jenkins Bound pvc-24a90c2c-25a6-11e8-9548-0800272e7159 8Gi RWO standard 0s
==> v1/Service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
j-jenkins-agent ClusterIP 10.107.112.29 <none> 50000/TCP 0s
j-jenkins NodePort 10.106.245.61 <none> 8080:30061/TCP 0s
==> v1beta1/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
j-jenkins 1 1 1 0 0s

NOTES:
1\. Get your 'admin' user password by running:
 printf $(kubectl get secret --namespace default j-jenkins -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo
2\. Get the Jenkins URL to visit by running these commands in the same shell:
 export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services j-jenkins)
 export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
 echo http://$NODE_IP:$NODE_PORT/login

3\. Login with the password from step 1 and the username: admin

For more information on running Jenkins on Kubernetes, visit:
https://cloud.google.com/solutions/jenkins-on-container-engine

生成的注释被呈现为模板,并通常提供有关如何访问服务的说明。您始终可以使用helm status命令重复获取相同的信息。

当我们调用 Helm 时,我们将此命名为release j以使其简短和简单。要获取有关此版本当前状态的信息,请使用以下命令:

helm status j

这是一个相当大的安装,安装需要一段时间。您可以使用诸如kubectl get events -w之类的命令观看从此安装中滚出的事件。这将随着部署的进行而更新事件,输出看起来类似于以下内容:

2018-03-11 20:08:23 -0700 PDT 2018-03-11 20:08:23 -0700 PDT 1 minikube.151b0d76e3a375e1 Node Normal NodeReady kubelet, minikube Node minikube status is now: NodeReady

2018-03-11 20:38:28 -0700 PDT 2018-03-11 20:38:28 -0700 PDT 1 j-jenkins-6ff797cc8d-qlhbk.151b0f1b339a1485 Pod spec.containers{j-jenkins} Normal Pulling kubelet, minikube pulling image "jenkins/jenkins:lts"

2018-03-11 20:38:29 -0700 PDT 2018-03-11 20:38:29 -0700 PDT 1 j-jenkins-6ff797cc8d-qlhbk.151b0f1b7a153b09 Pod spec.containers{j-jenkins} Normal Pulled kubelet, minikube Successfully pulled image "jenkins/jenkins:lts"

2018-03-11 20:38:29 -0700 PDT 2018-03-11 20:38:29 -0700 PDT 1 j-jenkins-6ff797cc8d-qlhbk.151b0f1b7d270e5e Pod spec.containers{j-jenkins} Normal Created kubelet, minikube Created container

2018-03-11 20:38:30 -0700 PDT 2018-03-11 20:38:30 -0700 PDT 1 j-jenkins-6ff797cc8d-qlhbk.151b0f1b8359a5e4 Pod spec.containers{j-jenkins} Normal Started kubelet, minikube Started container

一旦部署完全可用,您可以开始使用注释中的说明访问它。

访问 Jenkins

图表和图像一起制作一些秘密,因为部署正在进行中,以保存诸如访问 Jenkins 的密码之类的东西。注释包括一个命令,用于从 Kubernetes 获取此密码并在您的终端上显示它:

printf $(kubectl get secret --namespace default j-jenkins -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo

运行该命令并复制输出,因为我们需要它来登录到您的 Jenkins 实例。接下来的命令告诉您如何获取访问 Jenkins 的 URL。您可以使用这些命令获取信息并打开浏览器访问 Jenkins。如果您将其部署到 Minikube,还可以使用 Minikube 打开相关服务的浏览器窗口:

minikube service j-jenkins

第一页将为您提供凭据请求。使用admin作为用户名和您在前面命令中读取的密码:

然后,登录应该为您提供对 Jenkins 的管理访问权限:

更新 Jenkins

当您连接时,在前面的示例中,您可能会看到一个红色菜单项和一个数字。这是 Jenkins 提醒您应立即考虑更新的方式。我强烈建议您单击该数字并查看它所呈现的内容:

虽然图表和基本图像是维护的,但无法提前确定的更新或考虑因素可能会变得可用。特别是,Jenkins 的插件可能会得到更新,并且 Jenkins 会审查现有的插件以进行可能的更新。您可以单击此页面上的按钮来运行更新,重新启动 Jenkins,或了解更多关于其建议的信息。

Jenkins 图表包括一个persistent-volume-claim,用于存储插件更新,因此,除非您禁用它,您可以安全地加载 Jenkins 插件的更新,并告诉它重新启动以使这些插件更新生效。

示例管道

安装的一个好处是,您创建的作业可以运行完全在 Kubernetes 集群内构建和运行的管道。管道可以被定义为您在 Jenkins 内部使用工具构建的东西,您可以直接输入它们,或者您可以从源代码控制中加载它们。

Python/Flask 应用程序的示例代码具有基本的 Jenkinsfile,以向您展示这如何工作。 Jenkinsfile 已添加到 0.7.0 分支,您可以在github.com/kubernetes-for-developers/kfd-flask/blob/0.7.0/Jenkinsfile上在线查看。

管道设置为从源代码控制中使用,构建 Docker 镜像,并与 Kubernetes 交互。示例不会将图像推送到存储库或部署图像,与之前的 Travis.CI 示例遵循相同的模式。

要在 Jenkins 的实例中启用此示例,您需要导航到 Jenkins 的首页并选择 New Item。然后,选择 Multibranch Pipeline 并将作业命名为kfd-flask-pipeline

创建后,输入的关键项目是来自源代码控制的内容位置。您可以输入https://github.com/kubernetes-for-developers/kfd-flask来使用此示例:

保存配置,它应该构建示例,连接到 GitHub,获取管道,然后配置并运行它。

加载各种图像可能需要相当长的时间,一旦完成,结果将在 Jenkins 中可用:

在管道示例中,它从源代码控制中检出,使用基于分支和git commit的标签名称构建新的 Docker 镜像,然后与 Kubernetes 交互,向您显示正在运行的集群中当前活动的 Pod 的列表。

Jenkins 与我们的 Travis.CI 示例有相同的需求,例如更改清单以运行完整的序列,您可以通过使用 Helm 或者 ksonnet 来构建前面的示例来解决这个问题。

管道的下一步

您可以使用 Jenkins 管道做的事情远远超出了我们在这里可以涵盖的范围,但是管道和 Kubernetes 插件附加功能的完整文档都可以在线获得:

总结

在本章中,我们深入探讨了在测试代码时如何使用 Kubernetes。我们研究了您可能在集成测试中探索的模式。我们指出了使用 shell 脚本在 Kubernetes 中运行集成测试的简单示例,然后更深入地探讨了使用 Python 和 Node.js 的示例,这些示例使用 Kubernetes 运行集成测试。最后,我们总结了本章,概述了可以使用集群的持续集成的可用选项,并探讨了两个选项:使用 Travis.CI 作为托管解决方案以及如何在自己的 Kubernetes 集群上使用 Jenkins。

在下一章中,我们将看看如何将我们探索过的多个部分汇集在一起,并展示如何在 Kubernetes 上运行代码的基准测试。

第十章:故障排除常见问题和下一步

之前的章节探讨了如何在开发过程中使用 Kubernetes。在本章中,我们通过查看一些您可能遇到的常见错误来总结示例。我们将看看如何理解它们,诊断问题的技术以及如何解决它们。本章还回顾了一些新兴项目,这些项目正在形成以帮助开发人员使用 Kubernetes。

本章主题包括:

  • 常见错误及其解决方法

  • 开发人员的新兴项目

  • 与 Kubernetes 项目交互

常见错误及其解决方法

在整本书中,我们提供了一些示例,说明了如何使用 Kubernetes。在开发这些示例时,我们遇到了您可能会遇到的所有相同问题,其中一些令人困惑,而且并不总是清楚如何确定问题所在以及如何解决它,使系统正常工作。本节将介绍您可能会看到的一些错误,讨论如何诊断它们,并为您提供一些技术,以帮助您了解如果您自己遇到这些问题。

数据验证错误

当您为 Kubernetes 编写自己的清单并直接使用它们时,很容易犯一些简单的错误,导致错误消息:error validating ...

幸运的是,这些错误非常容易理解,但非常不方便。为了说明这个例子,我创建了一个略有问题的部署清单:

使用此清单运行kubectl apply时,您将收到一个错误:

error: error validating "test.yml": error validating data: [ValidationError(Deployment.spec.template.spec.containers[0]): unknown field "named" in io.k8s.api.core.v1.Container, ValidationError(Deployment.spec.template.spec.containers[0]): missing required field "name" in io.k8s.api.core.v1.Container]; if you choose to ignore these errors, turn validation off with --validate=false

在这种情况下,我犯了一个细微的拼写错误,错误地命名了一个必需字段name,这在错误中被突出显示为missing required field

如果您包含了系统不知道的额外字段,您也会收到一个错误,但是稍有不同:

error: error validating "test.yml": error validating data: ValidationError(Deployment.spec.template.spec.containers[0]): unknown field "color" in io.k8s.api.core.v1.Container; if you choose to ignore these errors, turn validation off with --validate=false

在这种情况下,理解消息的关键是unknown field部分。这些消息还引用了通过数据结构到确切发生错误的路径。在前面的示例中,这是Deployment(在kind键中定义的对象)以及其中的spec -> template -> spec -> container。错误消息还确切定义了 Kubernetes API 尝试根据的对象:io.k8s.api.core.v1.Container。如果您对所需内容感到困惑,您可以使用此信息在 Kubernetes 网站上查找文档。这些对象是有版本的(请注意对象名称中的v1),在这种情况下,您可以在 Kubernetes 的参考文档中找到完整的定义。

参考文档是根据 Kubernetes 的每个版本发布的,对于 1.9 版本,该文档位于kubernetes.io/docs/reference/generated/kubernetes-api/v1.9/。文档还包括一些示例细节以及三列视图中的定义:

导航文档

文档遵循我们从 Kubernetes 对象本身看到的相同模式:它们由较小的基元组成。当您浏览文档时,例如查看屏幕截图中显示的部署,您经常会看到对封装的对象的引用,并且要深入了解细节,您可能需要引用其中一些对象区域。例如,部署示例封装了 Pods,因此为了正确定义模板中的所有属性,您可能需要参考 Pod v1 核心文档。

ErrImagePull

ErrImagePull 可能是最常见的问题,幸运的是很容易调试和诊断。当发生这种情况时,您将看到ErrImagePull作为状态消息,表明 Kubernetes 无法检索您在清单中指定的图像。当简单地请求 pod 状态时,最常见的情况是:

kubectl get pods

NAME                  READY STATUS       RESTARTS AGE
flask-659c86495-vlplb 0/1   ErrImagePull 0        4s

您可以立即使用kubectl describe命令获取有关为什么发生此错误的更详细信息。从技术上讲,这并不完全是一个错误条件,因为 Kubernetes 在等待状态下,希望图像变得可用。

在此示例中,我们可以使用以下命令获得更多详细信息:

kubectl describe pod flask-659c86495-vlplb

这提供了诸如此类的信息:

Name: flask-659c86495-vlplb
Namespace: default
Node: minikube/192.168.99.100
Start Time: Sat, 17 Mar 2018 14:56:09 -0700
Labels: app=flask
 pod-template-hash=215742051
Annotations: <none>
Status: Pending
IP: 172.17.0.4
Controlled By: ReplicaSet/flask-659c86495
Containers:
 flask:
 Container ID:
 Image: quay.io/kubernetes-for-developers/flask:0.2.1
 Image ID:
 Port: 5000/TCP
 State: Waiting
   Reason: ImagePullBackOff
 Ready: False
 Restart Count: 0
 Environment: <none>
 Mounts:
 /var/run/secrets/kubernetes.io/serviceaccount from default-token-bwgcr (ro)
Conditions:
 Type Status
 Initialized True
 Ready False
 PodScheduled True
Volumes:
 default-token-bwgcr:
 Type: Secret (a volume populated by a Secret)
 SecretName: default-token-bwgcr
 Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: <none>

从这个细节中可以看出容器处于等待状态,通常与 pod 相关的事件提供了最有用的信息。信息很密集,因此通常在调用此命令时有一个更宽的终端窗口可用,这样更容易解析:

您可以看到 Kubernetes 已经采取的处理步骤:

  1. Kubernetes 安排了 pod

  2. 安排 pod 的节点尝试检索请求的图像

  3. 它报告了一个警告,说找不到图像,将状态设置为ErrImagePull,并开始使用回退重试

首先要做的是验证您请求的图像是否确实是您打算请求的图像。在这种情况下,我故意打了一个错字,请求了一个不存在的图像。

另一个常见的问题可能是图像确实存在,但由于某种原因不允许被拉取。例如,当您首次创建一个容器并将其推送到quay.io时,它会保持该容器构建为私有,直到您明确访问网站并将其公开为止。

如果您从私有存储库中拉取图像,但使用的凭据无效(或在更新过程中变得无效),则可能会出现相同的错误消息。

验证访问权限的最佳调试技术之一,至少对于公共图像,是尝试自己检索图像。如果您在本地安装了 Docker,只需调用 Docker 的pull命令即可。在这种情况下,我们可以验证图像:

docker pull quay.io/kubernetes-for-developers/flask:0.2.1

来自 Docker 命令行的错误响应相当直接:

Error response from daemon: manifest for quay.io/kubernetes-for-developers/flask:0.2.1 not found

CrashLoopBackOff

您可能会发现您的 pod 报告状态为CrashLoopBackOff,这是另一个非常常见的错误状态。

这是一个仅在调用容器后发生的错误条件,因此可能会延迟出现。通常在调用kubectl get pods时会看到它:

NAME                          READY STATUS           RESTARTS AGE
flask-6587cb9b66-zzw8v        1/2   CrashLoopBackOff 2        1m
redis-master-75c798658b-4x9h5 1/1   Running          0        1m

这明确意味着 pod 中的一个容器意外退出,可能是以非零错误代码退出。了解发生了什么的第一步是利用kubectl describe命令获取更多细节。在这种情况下:

kubectl describe pod flask-6587cb9b66-zzw8v

浏览生成的内容,查看 pod 中每个容器的状态:

在前面的例子中,您可以看到 Jaeger 收集器容器处于 Running 状态,而 Ready 报告为 True。然而,flask 容器处于 Terminated 状态,原因只有 Error,退出代码为 2

通常提供关于容器退出原因的至少一些信息的步骤是利用 kubectl logs 命令,查看我们在 STDOUTSTDERR 中报告的内容。

如果您调用 kubectl logs 和 pod 名称,您可能还会看到此错误:

kubectl logs flask-6587cb9b66-zzw8v

Error from server (BadRequest): a container name must be specified for pod flask-6587cb9b66-zzw8v, choose one of: [jaeger-agent flask] or one of the init containers: [init-myservice]

这只是要求您在识别容器时更具体。在这个例子中,我们使用了一个具有初始化容器以及两个容器的 pod:主容器和 Jaeger 收集器 side-car。简单地将容器附加到命令的末尾,或者使用 -c 选项与容器名称,就可以实现您想要的效果:

kubectl logs flask-6587cb9b66-zzw8v -c flask

python3: can't open file '/opt/exampleapp/exampleapp': [Errno 2] No such file or directory

返回的内容以及其有多大用处,将取决于您如何创建容器以及您的 Kubernetes 集群使用的容器运行时。

提醒一下,kubectl logs 还有 -p 标志,这在检索容器的上一次运行日志时非常有用。

如果出于某种原因,您并不完全确定容器中设置了什么,我们可以使用一些 Docker 命令直接在本地检索并检查容器镜像,这通常可以阐明问题所在。

提醒一下,拉取镜像:

docker pull quay.io/kubernetes-for-developers/flask:latest

然后,进行检查:

docker inspect quay.io/kubernetes-for-developers/flask:latest

向下滚动到内容,您可以看到容器将尝试运行的内容以及它的设置方式:

在这个特定的例子中,我故意在被调用的 Python 文件名中引入了一个拼写错误,省略了 .py 扩展名。当您查看此输出时,这可能并不明显,但请特别查找 EntryPointCmd,并尝试验证这些是否是预期的值。在这种情况下,入口点是 python3,命令是随之被调用的:/opt/exampleapp/exampleapp

开始并检查镜像

由于这可能不太清楚,除了实际检查镜像之外,诊断此类问题的常见方法是使用替代命令运行镜像,例如 /bin/sh,并使用交互式会话来查看并进行验证和调试。如果您安装了 Docker,可以在本地执行此操作;在这样做时,请确保明确覆盖 entrypoint 和命令以交互式运行命令:

docker run -it --entrypoint=/bin/sh \
quay.io/kubernetes-for-developers/flask:latest -i

然后,您可以手动调用容器将要运行的内容,python3 /opt/exampleapp/exampleapp,并在那里进行任何额外的调试。

如果您没有在本地安装 Docker,您也可以在 Kubernetes 集群中执行相同的操作。如果 Pod 已经存在,您可以像之前一样使用kubectl exec,但是当容器崩溃时,通常是不可用的,因为容器尚未运行。

在这些情况下,使用kubectl run创建一个全新的、短暂的、临时的部署是一个不错的选择:

kubectl run -i --tty interactive-pod \
--image=quay.io/kubernetes-for-developers/flask:latest \
--restart=Never --command=true /bin/sh

如果您想要覆盖容器的入口点,您将需要在选项中小心使用--command=true,否则入口点将被设置为python3。如果没有该选项,kubectl run命令将假定您正在尝试传递不同的参数以与默认入口点一起使用。

您可能还会发现,当您尝试调用这样的命令时,会收到以下消息:

Error from server (AlreadyExists): pods "interactive-pod" already exists

当您创建一个类似的裸部署时,容器退出(或出现错误)后,Pod 不会被删除。使用kubectl get pods命令加上-a选项应该会显示存在的 Pod:

kubectl get pods -a

NAME                          READY STATUS           RESTARTS AGE
flask-6587cb9b66-zzw8v        1/2   CrashLoopBackOff 14       49m
interactive-pod               0/1   Completed        0        5m
redis-master-75c798658b-4x9h5 1/1   Running          0        49m

您可以删除它以再次尝试运行它:

kubectl delete pod interactive-test

向容器添加自己的设置

当您使用包含您没有创建的代码和系统的容器时,您可能会希望在容器中运行任何进程之前设置一些环境变量或者建立一些配置文件。这在使用其他预构建的开源软件时非常常见,特别是那些没有已经建立好的容器可以利用的软件。

处理这种情况的一种常见技术是将一个 shell 脚本添加到容器中,然后将入口点和参数设置为运行该脚本。如果这样做,请确保包括适当的选项来调用脚本。

一个常见的例子是使用/bin/bash -c /some/script来调用脚本。很容易忽略-c参数,这可能会导致一个非常令人困惑的错误消息:

/bin/sh: can't open '/some/script'

当引用的脚本没有设置为可执行时,且您没有包括-c选项来让 shell 尝试读取和解释您指定的文件时,就会发生这种情况。

服务没有可用的端点

最难以发现的问题之一是为什么我的服务没有按照我期望的那样工作?在这些情况下常见的错误是这样的消息:

no endpoints available for service

如果您已经一起创建了部署和服务,并且一切似乎都在运行,但当您访问服务端点时看到这个输出:

{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {

  },
  "status": "Failure",
  "message": "no endpoints available for service \"flask-service\"",
  "reason": "ServiceUnavailable",
  "code": 503
}

在这种情况下,当使用kubectl proxy通过代理使用 URL 访问服务端点flask-service时,我收到了这条消息:

http://localhost:8001/api/v1/proxy/namespaces/default/services/flask-service

在这些情况下,使用kubectl describe命令获取有关服务设置的详细信息:

kubectl describe service flask-service

密切注意为服务设置的选择器,然后将其与您认为应该匹配的部署进行比较。在这种情况下,选择器是app=flaskapp,看看我们的部署的详细信息:

kubectl describe deploy flask

您应该立即验证的是容器是否正在运行和正常运行,而在这种情况下是的。接下来要做的事情是查看部署的标签,在这种情况下,您会看到它们设置为app=flask,而不是app=flaskapp,这就是为什么该服务没有响应的原因。

查看支持服务的 pod 发生的情况的另一种方法是使用kubectl get命令具体请求 pod。例如,我们可以使用这个命令:

kubectl get pods -l app=flaskapp -o wide

由于我们还没有使用相关标签设置任何 pod,我们会收到这样的响应:

No resources found.

标签和选择器是 Kubernetes 中许多元素松散耦合在一起的方式。由于松散耦合,Kubernetes 不会验证您是否正确设置了将 pod 绑定到服务的正确值。不确保标签和选择器是正确的是一个容易犯的错误,除了没有按预期响应外,不会显示为错误。

卡在 PodInitializing

您可能会遇到一种情况,您的 pod 似乎在初始化时出现挂起的情况,特别是当您首次设置涉及卷挂载和 ConfigMaps 的配置时。

kubectl get pods的状态看起来是这样的:

NAME                          READY STATUS   RESTARTS AGE
flask-6bc4b4c8dc-cm6gx        0/2   Init:0/1 0        7m
redis-master-75c798658b-p4t7c 1/1   Running  0        7m

而且状态没有改变。尝试获取正在发生的日志,比如:

kubectl logs flask-6bc4b4c8dc-cm6gx init-myservice

导致这个消息的结果是:

Error from server (BadRequest): container "init-myservice" in pod "flask-6bc4b4c8dc-cm6gx" is waiting to start: PodInitializing

在这种情况下,最好的做法是使用kubectl describe来获取 pod 和最近事件的详细信息:

kubectl describe pod flask

在这里,您将看到输出显示所有容器都处于“等待”状态,原因是PodInitializing

事件可能需要几分钟的时间才能真正显示出发生了什么,但几分钟后它们应该会出现:

您会看到警告FailedMount,以及相关的信息:

MountVolume.SetUp failed for volume "config" : configmaps "flaskConfig" not found

这需要一些时间才能出现,因为 Kubernetes 在尝试挂载卷时提供了一些较长的超时时间,以及重试。在这种情况下,错误是 Pod 规范中引用了一个不存在的 ConfigMap:flaskConfig

缺失的资源

在许多方面,我们刚刚描述的这个问题与标签和选择器的错误非常相似,但表现出来的方式完全不同。底层系统会尽力寻找卷、ConfigMaps、秘钥等,并允许您以任何顺序创建它们。如果您打错字,或者 ConfigMap、秘钥或卷的引用不正确或者丢失,那么 Pod 最终将失败。

这些资源都是动态引用的。在进行引用时,Kubernetes 提供了重试和超时,但在实际寻找相关资源并最终失败之前,无法明确验证故障。这可能会使调试这些问题变得更加耗时。当您首次寻找问题失败的原因时,可能不是所有的信息都是可见的(例如卷挂载失败、缺失的 ConfigMap 或秘钥等)。最佳选择是密切关注kubectl describe命令中的事件,并明确寻找事件中的警告,这些问题最终会出现在那里。

一些开发团队正在通过生成清单来解决这个问题,使用他们自己验证过的程序来创建适当的链接并确保它们是正确的。

开发人员的新兴项目

寻找替代方案来帮助使用 Kubernetes 的开发过程开始暴露出大量的开发中项目。在编写本书时,Kubernetes 从版本 1.7 进化到 Kubernetes v1.10 的 beta 版本。与此同时,大量的项目已经开始在 Kubernetes 周围建立自己,努力帮助消除在开发工作流程中积极使用 Kubernetes 时的一些问题。

Linters

在上一节中,我们谈到了 Kubernetes 无法预先验证的缺失组件,但我们可以自己查找。与验证相关的三个项目是 kubeval、kube-lint 和 kubetest,这里进行了描述:

kubeval 由 Gareth Rushgrove 创建,用于在尝试应用之前验证清单和配置文件。当您从自己的代码创建清单或使用另一个项目时,此工具非常有用。它无法检查所有内容,但是可以进行出色的首次检查。

由 Vic Iglesias 创建,kube-lint 更像是一个早期实验或功能原型,而不是一个不断发展的项目。它旨在根据一组常见规则验证一组 Kubernetes 清单。其中许多最佳实践和常见模式来自 Helm 项目,Vic 在其中积极参与,并且 Kubernetes 项目内正在讨论可能的方式来使用lint命令进行更多此类验证。

Gareth Rushgrove 也开发了 kubetest,用于在 Kubernetes 配置文件上运行测试。与明确封装最佳实践和规则不同,它更多地以单元测试的形式编写,允许对文件集进行断言,并让您指定自己的约束。

Helm

我们在前几章中提到并使用了 Helm,用它在 Kubernetes 中安装软件,以便我们可以利用它。Helm 可在helm.sh找到,版本 2 已经相当稳定,并且被许多开发团队积极使用。代表收集的最佳实践的图表可在github.com/kubernetes/charts找到,并且随着它们封装的软件和 Kubernetes 的进步而更新:

Helm 版本 3 是 Helm 的下一个重大进步,打破了他们在版本 2 中一直保持的一些向后兼容性保证。随着过渡,项目团队非常清楚地表示将有明确的迁移路径,并且当前的图表和示例将在项目发展过程中保持有用。Helm v3 的愿景细节仍在形成,这个项目无疑将是更大的 Kubernetes 生态系统中的关键项目。

在第 2 版中,它将自己定位为 Kubernetes 的软件包管理器,主要专注于成为一种一致的方式(和示例)来打包一组 pod、部署、服务、ConfigMaps 等,并将它们作为一个整体在 Kubernetes 中部署。许多团队创建了他们自己的图表,并将 Helm 集成到他们的持续集成流水线中,使用 Helm 来渲染清单,随着基础软件的更新并作为该过程的一部分进行部署。

Helm 的一个缺点可能是为自己的软件创建模板相当复杂。Helm 使用的模板系统称为 sprig,可能对许多开发团队来说并不熟悉。Helm 的下一个主要修订版,正在本书出版时被定义,希望解决许多挑战,包括使开发人员更容易编写和发布图表。这个项目真的值得关注。

ksonnet

ksonnet (ksonnet.io) 也被提及为另一种用于模板化和渲染 Kubernetes 清单的手段。ksonnet 以不同的方式处理任务,专注于模板化清单,并使这些模板非常容易组合:

ksonnet 使用用户的凭据,并不尝试管理其渲染的发布状态,而是专注于模板化。它建立在一个名为 Jsonnet 的库之上,为 JSON 模板添加了一些编程方面的内容。

ksonnet 是一个相当新的项目,开始受到其他项目的关注。他们表示他们也在积极与 Helm 社区合作,并希望将 ksonnet 作为创建图表的替代方式。

Brigade

Brigade,可在brigade.sh找到,采取了一种略有不同的方法来解决部署到 Kubernetes 的问题。它不是专注于模板化和用于以编程方式生成 Kubernetes 清单的 DSL,而是更倾向于使用 Kubernetes 及其事件作为一流公民进行脚本编写和编程:

来自 Azure 的微软团队构建了 Brigade 来扩展 JavaScript,将 Kubernetes 对象和事件公开为可以组合成工作流程和管道的元素。如果您的开发团队熟悉 JavaScript,那么 Brigade 可能是一种特别吸引人的协调和与 Kubernetes 交互的手段。

skaffold

Skaffold 可以在github.com/GoogleCloudPlatform/skaffold找到,由 Google 团队开发!

它是这些面向开发人员的项目中最新的一个,专注于成为一个命令行工具,以实现从检入代码到源代码控制,通过构建容器到更新和部署 Kubernetes 清单的过程。它还被设置为一个更大工具链中的一个组件,并已连接到其他项目,特别是 Helm,用于部署部分。

img

在将工具和项目视为组件时,值得注意的是托管在genuinetools.org下的 img 项目,可以在 GitHub 上找到github.com/genuinetools/img。本书中的示例都使用 Docker 构建容器映像,而 img 则构建在 Docker 团队从其产品演变出来的基础工具包上,以支持创建容器。最重要的是,img 项目允许在没有守护程序或以显着权限运行的情况下创建 Docker 映像。这使得在 Kubernetes 集群内构建容器或更一般地而言,无需赋予该过程对托管它的系统具有广泛权限的过程更加容易。

genuinetools 项目托管了许多其他有用的组件,其中大多数都专注于替代容器运行时。

Draft

微软团队的另一个工具 Draft 可以在draft.sh找到,它是一个专注于优化从源代码更改到在 Kubernetes 中部署并查看这些更改的工具:

Draft 专注于简单的命令和本地配置文件,用于为您的应用程序创建本地 Helm 图表,并简化在 Kubernetes 集群上运行它的过程,封装了构建容器、将其推送到容器注册表,然后部署更新的清单以进行升级的重复过程。

与其他一些工具一样,Draft 建立在 Helm 的基础上并使用 Helm。

ksync

ksync,可在vapor-ware.github.io/ksync/找到,采用了一种非常不同的开发工具策略。与其优化构建和部署到 Kubernetes 集群的时间不同,它专注于扩展代理功能,以便进入集群并操纵特定容器内的代码:

使用 Docker 进行开发的常见模式是挂载包含解释代码的本地目录(例如本书中的 Python 和 JavaScript 示例),并让容器运行该代码,以便您可以即时编辑并快速重新启动和重试。 ksync 通过在本地开发机器和集群内同时运行来模拟此功能,监视本地更改并将其反映到 Kubernetes 中。

ksync 专注于单个 pod 内软件的开发过程。因此,虽然它无法帮助部署所有支持的应用程序,但它可能会加快 Kubernetes 中单个组件的开发过程。

远程呈现

Telepresence,可在www.telepresence.io找到,是另一个专注于为开发人员提供从本地机器到 Kubernetes 集群的更紧密访问的项目:

由 Datawire 创建,该公司还为开发人员提供其他与 Kubernetes 一起使用的项目。Telepresence 创建了一个双向代理,将连接和响应转发到 Kubernetes 中 pod 内的进程,该进程在您本地的开发机器上运行。

ksync 复制您的代码并在 Kubernetes 内运行,而 Telepresence 则允许您在自己的机器上运行代码,并将其透明地连接,就像它是在 Kubernetes 内运行的 pod 一样。

与 Kubernetes 项目交互

在讨论所有这些项目时,你可以获得关于如何与 Kubernetes 一起工作的最多信息的项目就是 Kubernetes 本身。该项目托管了一个网站,其中包括正式文档、博客、社区日历、教程等,网址为kubernetes.io/

这个网站是获取更多信息的绝佳起点,但当然不是唯一的资源。

Kubernetes 项目实际上非常庞大,以至于几乎不可能有任何单个人来跟踪项目内正在进行的所有努力、发展、项目和兴趣。为了提供指导,Kubernetes 项目已经建立了一些专门关注这些兴趣的小组,即特别兴趣小组或 SIG。这些小组是 Kubernetes 的半正式子项目,每个小组都专注于 Kubernetes 的某个特定子集。毫不奇怪,许多这些 SIG 在具体方面有重叠,并且在 Kubernetes 中发现一个贡献者同时活跃于多个 SIG 是很常见的。

SIG 的完整列表可在线获取,并在github.com/kubernetes/community/blob/master/sig-list.md上进行维护。每个 SIG 都有特定的人被指定为领导者,定期举行会议,其中许多人维护在线笔记,甚至记录他们的在线会议。这些 SIG 都松散协调以推进 Kubernetes,并由 Kubernetes 指导委员会和许多社区经理进行协调。

还有一些不太正式的工作小组,专注于特定或短暂的兴趣,没有特定的领导或出席。所有这些 SIG 和工作小组一起,可以创造大量的信息和深度,可供查阅,并且有一个非常开放的社区,可以与项目相关的人进行互动。

社区还管理着 SIG 会议和活动的日历,可在kubernetes.io/community/上获取,并定期在 Kubernetes 博客上发布文章,网址为blog.kubernetes.io

Slack

Kubernetes 贡献者之间常见的互动方式是使用 Slack 的在线聊天频道。Kubernetes 拥有大量专门用于 SIGs、工作组和项目的互动频道。任何人都可以加入,您可以在slack.k8s.io注册以获取访问权限。

如果您是 Slack 或 Kubernetes 的新手,那么#kubernetes-users#kubernetes-novices频道可能会特别感兴趣。整个社区团队还举办他们所谓的办公时间,这是在 YouTube 上直播的直播,以及 Slack 频道#office-hours

YouTube

如果您喜欢视频流,Kubernetes 社区提供了一个 YouTube 频道,网址为www.youtube.com/c/KubernetesCommunity/。这些视频包括社区会议的录像,以及定期 Kubernetes 会议的会议内容。许多 SIGs 也记录他们的定期会议并在 YouTube 上发布,尽管它们并不是通过这个频道进行一致协调的。如果您想找到相关内容,最好通过每个单独的 SIG 来追踪,尽管您可能能够在该频道的播放列表下找到您想要的内容,网址为www.youtube.com/channel/UCZ2bu0qutTOM0tHYa_jkIwg/playlists

Stack Overflow

Kubernetes 社区的成员也在 Stack Overflow 上观看并回答问题。前面提到的办公时间鼓励人们在 Stack Overflow 上发布问题,并在办公时间上寻求互动帮助。您可以在stackoverflow.com/questions/tagged/kubernetes找到与 Kubernetes 相关的问题。如果您在 Google 上搜索与 Kubernetes 相关的主题,您可能也会在 Stack Overflow 上找到已经提出和回答的问题的结果。

邮件列表和论坛

Kubernetes 有一个通用的邮件列表/论坛,以及每个 SIG 和经常为每个工作组的邮件列表。常见的论坛包括:

github.com/kubernetes/community/blob/master/sig-list.md上的 SIG 列表还包括了每个 SIG 的个人邮件列表的参考链接。

关于 Kubernetes 的信息并非只有一种途径,社区非常努力地提供多种获取信息、提问和鼓励参与的方式。

摘要

在本章中,我们涉及了在开发和部署到 Kubernetes 时可能遇到的一些问题,然后介绍了一些项目,这些项目可能会对帮助您或您的团队加快开发过程并利用 Kubernetes 提供帮助。本章的最后部分讨论了 Kubernetes 项目本身,您如何与之交互,以及在哪里找到更多信息来利用这一奇妙的工具集。

posted @ 2024-05-20 12:00  绝不原创的飞龙  阅读(50)  评论(0编辑  收藏  举报