Kubernetes-GCP-入门指南-一-

122 阅读46分钟

Kubernetes GCP 入门指南(一)

原文:Beginning Kubernetes on the Google Cloud Platform

协议:CC BY-NC-SA 4.0

一、简介

2016 年,我的最终客户是一家全球物流公司,该公司正在升级一个使用了 15 年的整体清关系统,该系统基于应用服务器(WebLogic)、关系 SQL 数据库(Oracle)和消息队列(TIBCO)。每一层中的每个组件都是一个瓶颈。四节点 WebLogic 集群无法处理负载,最慢的 SQL 查询需要几秒钟才能处理完毕,将消息发布到 TIBCO EMS 队列类似于将羽毛扔进 100 米深的井中。这是一个任务关键型系统,使货物能够通过欧盟各地的清关中心进行转运。这个系统的崩溃(或显著减速)将意味着几个国家和办公室的空架子,等待隔夜文件,逐渐停止。

我被指派领导一个团队负责系统的重新架构。该团队由每个技术层的高度合格的主题专家组成。我们在有助于创建现代的、高度可伸缩的、高度可用的应用的大多数工具方面都有专业知识:用于 NoSQL 数据库的 Cassandra、用于消息传递的 Kafka、用于缓存的 Hazelcast 等等。但是 Java monolith 呢?我们已经将应用逻辑分解到合适的有界上下文中,并且有了一个可靠的微服务架构蓝图,但是无法获得一个容器——更不用说容器编排—专家来帮助我们进行设计。

我不愿意在我已经很忙的日程上增加更多的活动,但我非常渴望看到这个项目完成,我对自己说:“管它呢,我会自己处理的。”第一周,在我绞尽脑汁地思考了这个主题的文献之后,我认为我犯了一个大错误,我应该说服自己离开这个专业领域。市场上有大量自称为“容器编排者”的公司:Docker Swarm、Apache Mesos、Pivotal Cloud Foundry——客户的最爱——当然还有 Kubernetes。每一个都有完全不同的管理容器的概念、工具和工作流程。比如在 Cloud Foundry,纯 Docker 容器是二等公民;开发人员应该将代码直接“推入”平台,并通过一种称为“构建包”的机制,让代码在后台与其运行时相匹配。我对 Pivotal Cloud Foundry 的方法持怀疑态度,这种方法感觉就像 WebLogic 的翻版,在服务器端缺少一个依赖就会导致部署失败。

我试图用建筑师的眼光来处理这个问题,通过理解概念,查看图表,并找出组件如何一起工作,但我就是“不明白”。当遇到诸如“零停机迁移确切地说是是如何工作的?”这样的难题时我只能用模糊的词语来表达。因此,我决定,如果我想回答这些复杂的问题,并深入了解容器编排器,我必须卷起袖子。

在我动手之前,我必须选一个管弦乐队。我根本没有时间去修补每一个。Docker Swarm 在 2016 年感到过于简单,因为企业系统中还需要协调负载均衡器和存储系统之类的东西。Mesos 感觉像是一个通用的解决方案,其中 Docker 是在其开发的后期阶段被硬塞进去的。这给我留下了 Kubernetes,它的文献记录很少,只有它的谷歌遗产作为其看似独特的销售主张。尽管当时还处于起步阶段,但 Kubernetes 似乎关心的是实现高可扩展性和高可用性的一般战略,以及支持上述架构属性的网络和存储资源的复杂编排。Kubernetes 似乎试图解决的权利问题

然而,在当时,库伯内特文学的复杂性是压倒性的。大多数文本开始解释支撑 Kubernetes 集群的所有内部组件(Kubelet、Kube Proxy、etcd 等。)以及他们是如何合作的。解释 Kubernetes 是如何工作的很好,但演示它如何帮助解决世俗问题并不理想。相反,一些其他文本以 30,000 英尺的概述开始,迅速抛出复杂的术语,一次全部:吊舱、复制控制器、扩音器等。

正如我一生中多次做过的那样,我决定把我的发现组织成一个简单的结构,让我原始的猿猴大脑能够理解;所以我开始了我的研究冒险,打算在博客上记录我的见解。但是在这个过程当中发生了意想不到的事情。

当我的实验室实验进行了几周后,我意识到我不再是在研究所谓的“容器编排器”意识到容器——或者更准确地说是 Docker 映像的使用——从架构的角度来看是无关紧要的。Kubernetes 远不止这些。这是一个更基本问题的答案;如何在操作系统级别,而不是在应用级别实现通用的分布式计算。

当硬币落下时,我既兴奋又害怕。《这就是未来》像一张破唱片一样在我脑海里不停播放。Kubernetes 不再是一个“选择”;我处理的问题不是什么样的容器编制器风格最适合实现基于 clearance 微服务的应用。我站在一种技术的前面,这种技术可能会在几年后改变每个人进行通用分布式计算的方式。这就像我第一次在一台只能说 MS-DOS 和 Windows 的电脑上启动 Slackware Linux 的那一刻。

看,在 20 世纪 80 年代,运行 MS-DOS 的 Intel PC 允许一个用户一次运行一个程序。当 GNU/Linux 出现时——承认当时存在其他类似 Unix 的操作系统,如 Xenix、SCO 和 MINIX——像我这样的普通用户第一次有能力在一台计算机上同时运行多个程序。Kubernetes 在进化的道路上更进一步,增加了在不同的计算机上同时运行多个程序的能力。

换句话说,除了硬件抽象之外,现代操作系统的“魔力”在于它有助于并行运行程序,并以水平方式管理它们,从每个程序占用多少 CPU 周期、哪个 CPU 和/或内核支持它们等等中抽象出来。当你启动一个将音频文件压缩成 MP3 的任务时,比方说,通过使用蹩脚的编码器并键入lame equinoxe.wav -o equinoxe.mp3,你不需要关心进程将被分配到哪个 CPU 或内核。想象一下,如果你有 1000 台电脑,而你不必担心哪台电脑会接手这项工作?您可以同时运行多少个 MP3(或视频编码)作业?这正是 Kubernetes 允许您以无缝的方式做到的,这种方式从根本上来说并不比普通 Linux 将用户与任务切换和 CPU/内核分配的不确定性隔离开来更难。

为了解决最初的困境,“Kubernetes 是一个容器编排者吗?”。是的,就 Linux 是可执行和可链接格式(ELF)文件的编排者而言。Kubernetes 不仅仅是运行在多个机器上的 Docker 主机——事实上,它还可以编排 Windows 容器和其他容器类型,如 rkt (Rocket)。此外,Kubernetes 如今已经是通用分布式计算(包括网络和存储资源)的事实上的平台,每个大型云和本地供应商对其无处不在的支持就证明了这一点。

为什么 Kubernetes 在谷歌云平台上

Kubernetes 最初由谷歌设计,并于 2014 年公布。谷歌于 2015 年 7 月 21 日首次发布了 Kubernetes 1.0,当时它将其捐赠给了云原生计算基金会(CNCF)。CNCF 最初成立的主要目的是使 Kubernetes 的供应商中立,由 Linux 基金会管理。Kubernetes 的设计受到了 Borg 的影响,Borg 是谷歌用来运行其数据中心的专有系统。

在其白金会员中,CNCF 包括每一个主要的云供应商(谷歌本身、亚马逊、微软、IBM、甲骨文和阿里巴巴)以及在内部部署领域更熟悉的公司,如 RedHat、英特尔、VMWare、三星和华为。Kubernetes 穿不同的皮肤——它是不同的品牌——取决于供应商。以 AWS 为例,它被称为“亚马逊弹性 Kubernetes 服务”(EKS);在 Azure 中,它被称为“Azure Kubernetes 服务”(AKS)。几乎每个主要的公共和私有云提供商都提供了 Kubernetes 的化身,但是我们选择了谷歌容器引擎(GKE)。为什么呢?

读者不应该妄下结论说“如果它是谷歌制造的,它在谷歌上运行得更好。”这种推理有两点是错误的。首先,Kubernetes 项目自 2015 年开源并移交给 CNCF 以来,除了来自个人爱好者的捐助外,还收到了来自各个赞助商的重大捐助。其次,Kubernetes 是否被认为“运行良好”取决于无数的背景因素:用户与最近的谷歌数据中心的距离、特定的存储和网络需求、特定云供应商的产品目录中可能提供或不提供的特殊云原生服务的选择,等等。

相反,作者选择谷歌云平台(GCP)是因为它的教学便利性,换句话说,它给那些在 Kubernetes 世界迈出第一步的读者带来的好处:

  • 为读者提供 300 美元(美元)或等值的当地货币,让他们体验真实世界的生产级云平台。

  • 一个集成的 Shell(Google Cloud Shell ),它将读者从设置复杂工作站配置的负担中解放出来。端到端的学习之旅只需要一个网络浏览器:Chromebook 或平板电脑就足够了。

  • 高度集成的网络(负载均衡器)和存储资源的可用性,这些资源在本地/本地环境(如 Minikube 或 RedHat OpenShift)中可能不存在或难以设置。

最后,GKE 是一款可供生产的世界级产品,被飞利浦等蓝筹股公司所采用。如果 GCP 的服务产品适合眼前的需求,读者可能会选择将他们公司的工作负载直接迁移到 GCP,而不是——必须——重新投资 AWS 或 Azure 的具体技能。

这本书是给谁的

这本书是为绝对初学者而写的,他们希望将 Kubernetes 带来的基础功能内在化,如动态扩展、零停机部署和外部化配置。初级 Linux 管理员、操作工程师、用任何语言开发基于单片 Linux 的应用的开发人员以及解决方案架构师——他们乐于卷起袖子——都是理想的候选人。

这本书是独立的,不需要云计算、虚拟化或编程等领域的高级知识。甚至不需要精通 Docker。由于 Kubernetes 实际上是一个 Docker 主机,读者可以在谷歌云平台上首次试验 Docker Hub 中的 Docker 和公共 Docker 映像,而无需在本地机器上安装 Docker 运行时。

读者只需要对 Linux 的命令行界面和使用简单 shell 脚本的能力有最低限度的了解,但不一定要写。第九章是一个例外,其中 Python 被广泛用于示例中——鉴于演示 StatefulSets 的动态性的挑战,这是必要的——然而,读者只需要理解如何运行提供的 Python 脚本,而不是它们确切的内部工作方式。

这本书有什么不同

这本书不同于其他看似相似的出版物,因为它优先考虑理解而不是覆盖广泛的大纲,并且它是以严格的自下而上的方式编写的,通常使用小代码示例。

由于这是一本注重教学的书,所以涵盖的主题较少。所选的主题是在循序渐进的基础上慢慢探索的,并使用例子来帮助读者观察、证明和内化【Kubernetes 如何帮助解决问题,而不是它的内部是什么。

自下而上的方法使读者从第一章开始就能提高工作效率,而不必“提前阅读”或等到书的末尾才设置基于 Kubernetes 的工作负载。这种方法也减少了读者在任何给定时间必须在脑海中闪现的概念的数量。

如何使用这本书

这本书有两种阅读方式:被动阅读和主动阅读。

被动方法包括远离电脑阅读书籍。20 世纪 70 年代和 80 年代的许多书都是以被动风格写成的,因为读者家里通常没有电脑。

鉴于作者是在这个时代阅读书籍长大的,本文将被动读者视为一等公民。这意味着所有相关的结果都呈现在书上;读者很少需要自己运行命令或脚本来理解它们的效果。同样,所有代码都在书中重现——除了用文字描述变化的小变化。使用这种方法的唯一问题是,读者可能会倾向于读得太快而忽略重要的细节。避免陷入这个陷阱的一个有效方法是使用荧光笔并在空白处写笔记,这将导致一种自然而有益的减速。

主动阅读模式是指读者在阅读时运行建议的命令和脚本。这种方法有助于以更快的方式内化示例,并且还允许读者尝试他们自己对所提供的代码的修改。如果遵循这种方法,作者建议使用 Google Cloud Shell,而不是与一个潜在的有问题的本地环境进行斗争。

最后,不管读者是选择被动阅读还是主动阅读,作者建议每次阅读“一次完成”一整章。一章中的每一节都介绍了一个重要的新概念,作者希望读者在阅读下一章时能记住这个新概念。表 1-1 以小时为单位提供了每章的预计阅读时间。

表 1-1

每章的预计阅读时间(小时)

|

|

消极的

|

活跃的

| | --- | --- | --- | | 第章 1 | 01 时 | 02 时 30 分 | | 第章 2 | 02 时 | 04 时 30 分 | | 第三章 | 01 时 30 分 | 03 时 30 分 | | 第四章 | 01 时 30 分 | 03 时 30 分 | | 第五章 | 01 时 | 02 时 30 分 | | 第六章 | 01 时 | 02 时 30 分 | | 第章第八章 | 01 时 | 02 时 30 分 | | 第九章 | 02 时 | 04 时 30 分 |

约定

本书使用了以下印刷惯例:

  • 斜体引入一个新的术语或概念,值得读者关注。

  • 大写的名词如“服务”或“部署”指的是 Kubernetes 对象类型,而不是它们在英语中的常规含义。

  • 文本用于指代语法元素,如 YAML 或 JSON、命令或参数。后者还包括用户自定义的名称,如my-cluster

  • <IN-BRACKETS-FIXED-WIDTH>文本是指命令参数。

  • dot.separated.fixed-width-text用于指各种 Kubernetes 对象类型中的属性。可以通过运行kubectl explain <PROPERTY>命令获得这些属性的详细描述,例如kubectl explain pod.spec.containers。运行kubectl要求我们首先设置一个 Kubernetes 集群,我们将在下面的小节中介绍这个集群。在某些情况下,如果上下文暗示父属性,则可以跳过父属性。例如,有时可以使用spec.containers或简单的containers,而不是pod.spec.containers

此外,为了简洁起见,并使该文本更容易理解,命令的输出(kubectl在大多数情况下)可以修改如下:

  • 如果一个命令生成一个包含多列的表格显示,那么与当前讨论无关的列可能会被忽略。

  • 如果命令产生与上下文无关的警告(例如,它们警告用户新 API 中的特性),则这样的警告可能不会被显示。

  • 只要能改进格式,就可以添加或删除空白。

  • 长标识符可以通过用星号替换所述标识符内的样板词片段来缩短。例如,标识符gke-my-cluster-default-pool-4ff6f64a-6f4v代表一个 Kubernetes 节点。我们可以将这样的标识符显示为gke-*-4ff6f64a-6f4v,其中*代表my-cluster-default-pool

  • 每当日期部分使示例超出书的列长度和/或当包括整个日期时间字符串无助于手头的讨论时,可以省略日期部分。例如,像Wed Aug 28 09:17:27 DST 2019 – Started这样的日志行可以简单地显示为17:27 - Started

最后,源代码清单和命令可能会使用反斜杠\来表示多个参数,否则这些参数可能会使用一个长行来编写:

$ gcloud container clusters create my-cluster \
    --issue-client-certificate \
    --enable-basic-auth

读者可以随意忽略反斜线(从而忽略回车),如有必要,可以将由多行组成的示例写成一个长行。

设置 GCP 环境

截至本书付印时,谷歌为其平台上的新账户提供 300 美元的信用额度。300 美元足够多次尝试本书中的例子,甚至运行一些额外的宠物项目一段时间。创建帐户的步骤如下:

  1. 前往 https://cloud.google.com/

  2. 单击“免费试用”按钮。

  3. 按照提示和问题进行操作。

  4. 询问时输入地址和信用卡。

信用卡在信用用完之前不会被扣费,但是需要验证用户的身份。当信用额度用完时,它还可以用来支付谷歌服务。

一旦建立了帐户,下一步就是启用谷歌 Kubernetes 引擎(GKE) API,这样我们就可以从命令行与它进行交互。步骤如下:

  1. 前往 https://console.google.com

  2. 点击左上角的汉堡按钮菜单,显示主菜单。

  3. 选择位于“计算”标题下的“Kubernetes 引擎”,它会将浏览器重定向到 https://console.cloud.google.com/kubernetes

  4. 选择“集群”

  5. 寻找一条消息说“Kubernetes 引擎 API 正在被启用。”

请不要单击“创建集群”、“部署容器”或其他类似选项,因为我们将严格按照下一节中的说明从命令行工作。

使用谷歌云外壳

谷歌遵循代码优先的哲学。这是因为无论我们在 GCP 上执行什么操作——比如启动 Kubernetes 集群——我们都更喜欢编写脚本,以避免将来重复。因此,学习基于 GUI 的工作流,然后使用命令行重新学习相同的等效工作流,是不必要的重复工作。为了拥抱代码优先的方法,我们将使用 Google Cloud Shell 作为本书中所有示例的实际环境。

Google Shell 环境,作为一个一流的公民功能,总是可以在顶部菜单的左侧使用一个命令提示符图标,如图 1-1 所示。读者可能想知道,就学习一项只与谷歌捆绑在一起的技术而言,使用谷歌云外壳是否是一种虚假的经济。一点也不。

img/486631_1_En_1_Fig1_HTML.jpg

图 1-1

左上角菜单栏上的 Google Cloud Shell 图标

与微软的 PowerShell 不同,谷歌的云 Shell 并不是 Bash 的替代品。这是一个基于网络的终端——想想运行在网络浏览器上的 Putty 或 iTerm 它自动连接到一个小型的全功能 Linux 虚拟环境。所述虚拟机为我们提供了一个主目录,用于存储我们的文件,以及我们需要用于本书的预安装和预认证的所有实用程序和命令:

  • gcloud命令,它是 Google SDK 的一部分,已经通过验证,并且在我们登录时指向我们的默认项目

  • kubectl命令,它是 Kubernetes 客户端套件的一部分,一旦创建,就会被认证并指向我们基于 Google 的集群

  • python3命令,在第九章中用于演示状态集

  • curl命令,用于与 web 服务器交互

  • git命令,从 GitHub 下载代码示例

在本地机器上安装所述软件包的步骤取决于读者的机器运行的是 Windows、macOS 还是 Linux——以及在后一种情况下的具体发行版。谷歌在 https://cloud.google.com/sdk/install 为每个操作系统提供指令。我们建议读者在读完这本书后只设置一个本地环境。同样,如果读者运行的是微软 Windows 10,我们强烈建议使用 Linux (WSL)的 Windows 子系统,而不是 Cygwin 或其他伪 Unix 环境。有关 WSL 的好处以及如何安装的更多信息,请参考 https://docs.microsoft.com/en-us/windows/wsl/

注意

本书中的许多例子提示读者打开多个窗口或标签来观察各种 Kubernetes 对象的实时行为。Google Cloud Shell 允许打开多个标签页,但是它们对于“并排”比较没有用。使用 TMUX 通常更方便,它是预先安装的,默认情况下正在运行。TMUX 是一个终端复用器,它允许将一个屏幕分成多个面板,还有许多其他功能。对于本书的范围,以下 TMUX 命令应该足够了:

水平分割屏幕

Ctrl+B(一次)然后"(双引号字符)

垂直分割屏幕

Ctrl + B(一次),然后是%(百分比字符)

在打开的面板间移动(因此它们被选中)

Ctrl + B(一次)和箭头键

增大/减小所选面板的尺寸

Ctrl + B(一次),然后 Ctrl +箭头键

关闭面板

在选定的面板上键入exit

有关 TMUX 的更多信息,请键入man tmux

下载源代码并运行示例

本书中包含的大型程序清单包括一个注释,其文件名通常位于第一行或第二行,这取决于第一行是否用于调用命令——使用 shebang 符号——如下所示:

#!/bin/sh
# create.sh
gcloud container clusters create my-cluster \
  --issue-client-certificate \
  --enable-basic-auth \
  --zone=europe-west2-a

本书包含的所有文件的源代码位于 GitHub 上的 https://github.com/egarbarino/kubernetes-gcp 。Google Cloud Shell 默认安装了 Git。以下命令序列将存储库克隆到本地主目录,并提供文件夹列表,一章一个文件夹:

$ cd ~ # Be sure we are in the home directory

$ git clone \

    https://github.com/egarbarino/kubernetes-gcp.git

$ cd kubernetes-gcp

$ ls
chp1  chp2  chp3  chp4  chp5  chp6  chp7  chp8 ...

为简洁起见,本文中的代码清单不包括章节号作为文件名的一部分。比如之前看到的代码显示的是create.sh而不是chp1/create.sh;读者应该在执行给定章节的代码之前切换到相关目录。例如:

$ cd chp1

$ ls
create.sh  destroy.sh

创建和销毁 Kubernetes 集群

在创建 Kubernetes 集群之前,我们必须决定要运行它的地理位置。谷歌使用术语区域来指代地理位置(例如,都柏林相对于伦敦),而区域则代表隔离区域(在电力供应、网络、计算等方面)。)的环境。然而,区域标识符包括地区和区域。例如,europe-west2-a选择了europe-west2内的区域a,这反过来标识了英国伦敦的一个谷歌数据中心。这里,我们将使用gcloud config set compute/zone <ZONE>命令。

$ gcloud config set compute/zone europe-west2-a
Updated property [compute/zone]:

同样的设置也可以作为一个标志提供,我们稍后会了解到。现在我们准备使用gcloud container clusters create <NAME>命令启动我们的 Kubernetes 集群:

$ gcloud container clusters create my-cluster \
    --issue-client-certificate \
    --enable-basic-auth
Creating cluster my-cluster in europe-west2-a...
Cluster is being health-checked (master is healthy)
done
kubeconfig entry generated for my-cluster.
NAME       LOCATION       MASTER_VERSION NUM_NODES
my-cluster europe-west2-a 1.12.8-gke.10  3

默认集群由三个虚拟机组成(称为节点)。在 Kubernetes 1.12 之前,gcloud container clusters create命令本身就足以创建一个简单的 Kubernetes 集群,但是现在,有必要使用显式标志,以便让用户知道默认设置不一定是最安全的。特别

  • 为了避免设置自定义证书的复杂性,有必要使用--issue-client-certificate标志。

  • --enable-basic-auth标志对于避免建立更健壮的认证机制是必要的。

其他标志可能是必要的,也可能不是必要的,这取决于我们是否有全局默认值:

  • --project=<NAME>标志表示我们是否想要在其他项目中而不是默认项目中创建集群。Google Cloud Shell 通常会自动指向默认项目。如果有疑问,我们可以通过发出gcloud project lists命令来列出项目的数量。同样,如果我们想永久改变默认项目,我们可以使用gcloud config set project <NAME>命令。

  • --zone=<ZONE>标志指示将在其中创建群集的计算区域。可用区域的完整列表可以通过发出gcloud compute zones list命令获得。然而,对于更人性化的列表,包括设施所在的实际城市和国家,URL https://cloud.google.com/compute/docs/regions-zones/ 更有用。可以预先使用gcloud config set compute/zone <ZONE>命令指定默认区域,这正是我们在前面的例子中所做的。本章文件夹下提供的脚本,create.shdestroy.sh,使用此标志设置区域。读者可以根据自己的喜好修改这些脚本。

  • --num-nodes=<NUMBER>标志设置 Kubernetes 集群将包含的虚拟机(节点)数量。这有助于试验更小和更大的集群,尤其是在高可用性场景中。

  • --cluster-version=<VERSION>标志指定了主节点和从节点的 Kubernetes 版本。命令gcloud container get-server-config列出了当前可用的版本。这本书已经过 ?? 版本的检验。如果忽略此标志,服务器将选择最新的稳定版本。如果一些例子因为新版本中引入的向后破坏的变化而失败,那么当运行gcloud container get-server-config命令时,读者可以指定哪个是1.13版本下的最新版本。或者,读者可以使用本章文件夹下提供的misc/create_on_v13.sh脚本。

一旦我们完成了 Kubernetes 集群,我们就可以通过发出gcloud container clusters delete <NAME>命令来处理它。添加--async--quiet标志也是有用的,这样删除过程就可以在后台进行,而不会让文本污染屏幕:

$ gcloud container clusters delete my-cluster \
    --async --quiet

作者建议,只要有必要运行示例,就运行 Kubernetes 集群,以充分利用分配的信用。让一个三节点的 Kubernetes 集群运行几天,由于失误,可以很容易地增加一个价值超过 100 美元的账单,在这个过程中蒸发掉三分之一的免费信用。

本文中的所有例子都假设一个名为my-cluster的 Kubernetes 集群。为了方便起见,脚本create.shdestroy.sh包含在本章的文件夹下。还有一个叫做misc/create_on_v13.sh的脚本,如果 GKE 选择了一个与 1.13 太不一致的默认版本,就不可能重现例子了。

在库伯内特斯思考

在 Kubernetes 中,几乎每个组件类型都被实现为通用的资源,在本文中,我们也经常称之为 Kubernetes 对象,或简称为对象——因为资源具有我们可以检查和更改的属性。以相对水平的方式管理资源;使用kubectl命令的大多数日常操作包括

  • 列出现有对象

  • 检查对象的属性

  • 创建新对象和改变现有对象的属性

  • 删除对象

让我们从对象列表开始。我们可以使用kubectl api-resources命令列出支持的资源类型。这些还不是实例本身,而是 Kubernetes 世界中存在的对象类型:

$ kubectl api-resources
NAME          SHORTNAMES  NAMESPACED  KIND
...
configmaps    cm          true        ConfigMap
endpoints     ep          true        Endpoints
events        ev          true        Event
limitranges   limits      true        LimitRange
namespaces    ns          false       Namespace
nodes         no          false       Node
...

注意,其中一种资源类型叫做nodes。节点是支持我们的 Kubernetes 集群的计算资源,所以这个类的对象保证预先存在——除非我们已经决定建立一个没有工作节点的集群。为了列出对象实例,我们使用kubectl get <RESOURCE-TYPE>命令。在这种情况下,<RESOURCE-TYPE>就是nodes:

$ kubectl get nodes
NAME                 STATUS  AGE   VERSION
gke-*-4ff6f64a-6f4v  Ready   92m   v1.12.8-gke.10
gke-*-4ff6f64a-d8nx  Ready   92m   v1.12.8-gke.10
gke-*-4ff6f64a-nw0m  Ready   92m   v1.12.8-gke.10

<RESOURCE-TYPE>还有一个的简称,是nodesno。Kubectl 通常也接受同一个资源类型的单数(以及复数)版本。例如,在节点的情况下,kubectl get nodeskubectl get node,kubectl get no都是等价的。

一旦我们获得了对象实例的列表,我们就可以检查给定的选定对象的特定属性。这里我们使用命令kubectl describe <RESOURCE-TYPE>/<OBJECT-IDENTIFIER>来获得一个快速的屏幕摘要。我们选择第一个节点,因此<RESOURCE-TYPE>将是nodes,,而<OBJECT-IDENTIFIER>将是gke-*-4ff6f64a-6f4v:

$ kubectl describe nodes/gke-*4ff6f64a-6f4v
...
Addresses:
  InternalIP:   10.154.0.23
  ExternalIP:   35.197.220.185
  ...
System Info:
  Machine ID:       af2b11a0b29eb76e2592b4dd64f16308
  System UUID:      AF2B11A0-B29E-B76E-*-B4DD64F16308
  Boot ID:          22ba7558-7748-45f4-*-789d16d343d2
  Kernel Version:   4.14.127+
  Operating System: linux
  Architecture:     amd64
...

实际对象的底层逻辑属性结构可以通过添加-o json-o yaml标志,使用kubectl get <RESOURCE-TYPE>/<OBJECT-IDENTIFIER>命令以 JSON 或 YAML 格式获得。例如:

$ kubectl get nodes/gke-*-4ff6f64a-6f4v -o yaml
kapiVersion: v1
kind: Node
metadata:
  name: gke-my-cluster-default-pool-4ff6f64a-6f4v
  resourceVersion: "19152"
  uid: 89b70bf4-c330-11e9-9bab-42010a9a0178
  ...
spec:
  podCIDR: 10.0.0.0/24
  ...
status:
  addresses:
  - address: 10.154.0.23
    type: InternalIP
  - address: 35.197.220.185
    type: ExternalIP
...

每个属性的语法和描述可以使用kubectl explain <PROPERTY>命令学习,其中<PROPERTY>由点分隔的字符串组成,其中第一个元素是资源类型,随后的元素是各种嵌套属性。例如:

$ kubectl explain nodes.status.addresses.address
KIND:     Node
VERSION:  v1

FIELD:    address <string>

DESCRIPTION:
     The node address.

就改变一个对象的属性而言,大多数命令如kubectl scale(在第三章中讨论)最终会改变一个或多个对象的属性。然而,我们可以使用kubectl patch <RESOURCE-TYPE>/<OBJECT-IDENTIFIER> -p '<NEW-JSON>'命令直接修改属性。在下面的例子中,我们将一个名为greeting的新标签的值hello添加到gke-*-4ff6f64a-6f4v:

$ kubectl patch nodes/gke-*-4ff6f64a-6f4v \
    -p '{"metadata":{"labels":{"greeting":"hello"}}}'
node/gke-*-4ff6f64a-6f4v patched

使用刚才显示的kubectl get command可以检查结果。然而,在大多数情况下,很少需要使用kubectl patch,因为要么有一个专门的命令来间接产生预期的变化——比如kubectl scale——要么我们将使用kubectl apply命令刷新对象的整个属性集。这个命令和kubectl create一样,将在本书中广泛讨论。

最后,处理对象包括运行 kubectl delete <RESOURCE-TYPE>/<OBJECT-IDENTIFIER>命令。假设名为gke-*-4ff6f64a-6f4v的节点被视为任何其他常规对象,我们可以继续删除它:

$ kubectl delete nodes/gke-*-6f4v
node "gke-*-4ff6f64a-6f4v" deleted

添加--force标志将加快进程,当<OBJECT-IDENTIFIER>被省略时可以使用--all标志,以便删除声明的资源类型下的所有对象实例。或者,kubectl delete all --all可以用来清除默认名称空间中所有用户创建的资源,但是它不会销毁与名称空间无关的节点。大多数对象类型是在特定的命名空间中声明的;由于 pod 是驻留在名称空间中的对象的典型类型,我们将在第二章的末尾讨论这个主题。

注意

这本书经常会提示读者“清理环境”,或者从“新鲜的环境”开始这种请求也可以以意见的形式包括在内。例如:

# Clean up the environment first

$ kubectl apply -f server.yaml

还有一个隐含的假设是,读者也将以一个干净的环境开始每一章。清理环境意味着删除先前在 Kubernetes 集群中创建的所有对象,这样它们就不会占用当前示例所需的资源,同时也避免了名称冲突。

读取器可以通过发出kubectl get all –all命令列出默认名称空间中所有用户创建的对象,并逐个删除每个对象,或者使用kubectl delete all –all命令。如果这不起作用,或者读者觉得环境可能仍然被以前的工件污染,解决方案是关闭 Kubernetes 集群并启动一个新的集群。

这本书是如何组织的

这本书由九章组成。图 1-2 提供了一个高层次的架构图,可作为章节指南:

  • 第一章,“简介”,包括使用 Google Cloud Shell、下载源代码示例和设置 Kubernetes 集群的说明。

  • 第二章“Pod”介绍了 Pod 资源类型,这是 Kubernetes 中最基本的构建块。读者将学习如何引导 Pods 运行,从简单的一次性命令到 web 服务器。操作方面,如设置 CPU 和 RAM 约束,以及使用标签和注释组织 pod,也将包括在内。在本章结束时,读者将能够运行 Kubernetes 集群中运行的应用并与之交互,就像它们运行在本地机器上一样。

  • 第三章,“部署和扩展”,通过引入部署控制器帮助读者将 Pod 提升到一个新的水平,部署控制器允许按需扩展 Pod 和无缝迁移,包括升级 Pod 版本时的蓝/绿部署和回滚。此外,使用水平 Pod 自动缩放(HPA)控制器演示了自动缩放的动态特性。

  • 第四章“服务发现”,教读者如何在公共互联网上以及在 Kubernetes 集群中使用 Pods。此外,本章还解释了服务控制器如何与部署控制器交互,以促进零停机部署以及应用的正常启动和关闭。

  • 第五章,“配置映射和秘密”,展示了如何通过使用配置映射或秘密控制器来存储配置,从而将配置从应用中外部化出来。同样,还将检查 Secrets controller 存储 Docker 注册表凭证和 TLS 证书的能力。

  • 第六章“作业”着眼于使用作业控制器运行批处理进程的情况——不同于稳定的 web 服务器。Kubernetes 的并行化能力有助于减少大型、计算成本高的多项目批处理作业的总处理时间,这一点也受到了特别关注。

  • 第七章“cron Jobs”描述了 CronJob 控制器,它可以以循环方式运行作业,并且依赖于大多数类 Unix 系统中 cron 守护程序的 crontab 文件所使用的相同语法。

  • 第八章“DaemonSets”解释了如何在 Kubernetes 集群的每个节点上部署本地可用的 pod,以便消费 pod 可以通过本地 TCP 连接或本地文件系统从更快的访问时间中受益。

  • 第九章,即最后一章“StatefulSets”,通过带领读者完成使用 StatefulSet 控制器实现原始键/值存储的过程,展示了高度可伸缩的有状态支持服务的本质。在本章结束时,读者将理解为什么云原生(托管)数据存储提供了几乎无与伦比的优势,以及如果读者选择推出自己的数据存储,StatefulSet 控制器提供的工具机制。

请注意,图 1-2 中的箭头表示逻辑流,而不是网络连接。同样,所描绘的节点是工作者节点。主节点,以及在其中运行的对象,由 GCP 管理,不在本初学者手册的范围之内。

img/486631_1_En_1_Fig2_HTML.jpg

图 1-2

Kubernetes 高级架构和章节指南

二、Pods

Pods 是 Kubernetes 集群中最基本的工作单元。一个 Pod 包含一个或多个容器,这些容器将一起部署在同一台机器上,因此可以使用本地数据交换机制(Unix 套接字、TCP 回送设备,甚至内存支持的共享文件夹)来实现更快的通信。

分组在 Pods 中的容器不仅通过避免网络往返来实现更快的通信,它们还可以使用共享资源,比如文件系统。

Pod 的一个关键特征是,一旦部署,它们就共享相同的 IP 地址和端口空间。与 vanilla Docker 不同,Kubernetes Pod 中的容器不在孤立的虚拟网络中运行。

从概念的角度来看,值得理解的是,pod 是位于特定节点中的运行时对象。部署到 Kubernetes 中的 Pod 不是“映像”或“磁盘”,而是实际的、有形的、消耗 CPU 周期的、网络可访问的资源。

在这一章中,我们将首先看看如何启动 Pods 并与之交互,就像它们是本地 Linux 进程一样;我们将学习如何指定参数、通过流水线输入和输出数据,以及连接到公开的网络端口。然后,我们将研究 Pod 管理的更高级的方面,例如与多容器 Pod 交互、设置 CPU 和 RAM 约束、挂载外部存储以及检测健康状况。最后,我们将展示标签注释的用处,它们不仅有助于标记、组织和选择窗格,还有助于标记、组织和选择大多数其他 Kubernetes 对象类型。

发射逃生舱的最快方法

使用最少的键击启动 Pod 的最快方法是发出kubectl run <NAME> --image=<URI>命令,其中<NAME>Pod 的前缀(稍后将详细介绍),而<URI>要么是一个 Docker Hub 映像,如nginx:1.7.9,要么是在其他 Docker 注册表中完全合格的 Docker URI,如谷歌自己的;例如,谷歌的 Hello World Docker 图片位于gcr.io/google-samples/hello-app:1.0

为简单起见,让我们从 Docker Hub 启动一个运行最新版本 Nginx web 服务器的 Pod:

$ kubectl run nginx --image=nginx
deployment.apps/nginx created

现在,尽管这是运行 Pod 最简单的方法,但它实际上会产生比我们可能需要的更复杂的设置。具体来说,它创建了一个部署控制器和一个复制集控制器(将在第三章中介绍)。反过来,ReplicaSet 控制器恰好控制一个被分配了随机名称的 Pod:nginx-8586cf59-8t9z9。我们可以使用kubectl get <RESOURCE-TYPE>命令检查生成的部署、复制集和 Pod 对象,其中<RESOURCE-TYPE>分别是deploymentreplicaset(对于复制集)和pod:

$ kubectl get deployment
NAME   DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
nginx  1       1       1          1         0s

$ kubectl get replicaset
NAME             DESIRED  CURRENT  READY  AGE
nginx-8586cf59   1        1        1      5m

$ kubectl get pod
NAME                 READY STATUS  RESTARTS AGE
nginx-8586cf59-8t9z9 1/1   Running 0        12s

虽然这是运行 Pod 的最简单的方法,但是从简洁的角度来说,Pod 被分配了随机的名称,我们需要使用kubectl get pod或其他机制(如标签选择器)来解决这个问题——这将在本章末尾介绍。此外,我们不能在完成后直接删除 Pod,因为部署和复制集控制器会再次创建它。事实上,要处置我们刚刚创建的 Pod,我们需要删除 Pod 的部署对象,而不是 Pod 本身:

$ kubectl delete deployment/nginx
deployment.extensions "nginx" deleted

默认情况下,kubectl run命令创建部署的原因是因为它的重启策略,重启策略由--restart=<VALUE>标志控制,如果不指定,它将被设置为Always。相反,如果我们将<VALUE>设置为OnFailure,那么 Pod 只有在失败时才会重启。然而,OnFailure策略也没有创造一个干净的容器。创建了一个作业对象(一个控制器,就像一个部署),我们将在到达第六章时讨论它。第三个也是最后一个可能的值是Never,它只创建一个单独的 Pod,其他什么都不创建;这正是我们在本章范围内所需要的。

注意

Kubernetes 将来会反对这样的行为,即无论何时省略了--restart标志,kubectl run命令都会“意外地”创建一个部署。当我们到达第三章时,我们将再次讨论这个话题。

发射单个吊舱

kubectl run <NAME> --image<IMAGE> --restart=Never命令(请注意--restart=Never标志)创建一个单独的 Pod,不创建其他对象。产生的 Pod 将完全按照提供的<NAME>参数命名:

$ kubectl run nginx --image=nginx --restart=Never
pod/nginx created

$ kubectl get pod
NAME      READY     STATUS    RESTARTS   AGE
nginx     1/1       Running   0          0s

如何访问 Nginx web 服务器本身是我们将在前面几节讨论的内容。现在,知道 Nginx 按照 Dockerfile 初始化设置运行就足够了。我们可以通过指定一个命令作为kubectl run的最后一个参数来覆盖输入命令。例如:

# Clean up the environment first
$ kubectl run nginx --image=nginx --restart=Never \
    /usr/sbin/nginx
pod/nginx created

然而,这里的问题是,默认情况下,nginx启动容器内部的进程,并以一个成功状态代码退出,从而结束 Pod 的执行——因此 Nginx web 服务器本身:

$ kubectl get pod
NAME      READY     STATUS      RESTARTS   AGE
nginx     0/1       Completed   0          0s

当强制运行 Pods 时,我们必须始终记住,无论是 web 服务器还是其他应用,进入过程都必须在某种循环中保持暂停,而不是立即完成并退出。在 Nginx 的情况下,我们需要传递-g 'daemon off;'标志。然而,将一个命令作为最后一个参数传递在这里不起作用,因为这些标志将被解释为kubectl run的额外参数。这里的解决方案是使用--command标志和双连字符语法:kubectl run ... --command -- <CMD> [<ARG1> ... <ARG2>]。在双连字符--之后,我们可以写一个带有参数的长命令:

# Clean up the environment first
$ kubectl run nginx --image=nginx --restart=Never \
    --command -- /usr/sbin/nginx -g 'daemon off;'
pod/nginx created

$ kubectl get pod
NAME      READY     STATUS    RESTARTS   AGE
nginx     1/1       Running   0          0s

如果--command标志不存在,那么--之后的参数将被认为是适用于映像‘docker file’命令的参数。然而,请注意,大多数映像没有入口命令,所以--command标志很少是必要的。换句话说,--之后的第一个参数通常被解释为一个命令,即使没有--command标志,因为公共 Docker 映像,比如 Docker Hub 上的映像,很少声明一个ENTRYPOINT属性。在前面的几节中会有更多的介绍。

注意

所展示的例子显示了 HTTP 日志,它需要一个 HTTP 客户端,比如curl首先与 Pod 进行交互。否则,当使用 Nginx 映像连接到 Pod 时,阅读器将不会体验到任何输出。Pod 网络端口的暴露及其与外部 TCP 客户端的交互将在本章前面的几个章节中解释,并在第四章中深入介绍。

使用前面显示的简单语法运行 Pods 的另一个结果是,Pods 被发送到后台,我们看不到它们的输出。我们可以通过发出kubectl attach <POD-NAME>命令连接到正在运行的容器的第一个进程。例如:

$ kubectl attach nginx
Defaulting container name to nginx.
127.0.0.1 - [12:16:00] "GET / HTTP/1.1" 200 612
127.0.0.1 - [12:16:01] "GET / HTTP/1.1" 200 612
127.0.0.1 - [12:16:01] "GET / HTTP/1.1" 200 612
...

Ctrl+C 会把控制权还给我们。我们也可以使用kubectl logs <POD-NAME>,我们将在后面单独讨论。要从后台运行的 Pod 进行处置,使用kubectl delete pod/<NAME>命令。

启动单个 Pod 来运行命令

默认情况下,窗格在后台运行。调试它们需要我们连接到它们,查询它们的日志,或者在它们内部运行一个 shell 在它们终止之前。然而,我们可以发射一个吊舱,并与它保持连接,这样我们就可以立即看到它的输出。这是通过添加--attach标志来实现的:

$ kubectl run nginx --image=nginx --restart=Never \
    --attach
127.0.0.1 - [13:11:03] "GET / HTTP/1.1" 200 612
127.0.0.1 - [13:11:04] "GET / HTTP/1.1" 200 612
127.0.0.1 - [13:11:05] "GET / HTTP/1.1" 200 612

请注意,如前所述,除非我们访问 nginx web 服务器,否则我们不会看到流量日志,稍后我们将对此进行解释。

诸如由 HTTP 服务器生成的那些日志通常是连续生成的;如果我们只想简单地运行一个命令,然后忘记 Pod 会怎么样?嗯,原则上我们只需要使用--attach标志并将所需的命令传递给 Pod 的容器。例如,让我们使用 Docker Hub 中的alpine Docker 映像来运行date命令:

$ kubectl run alpine --image=alpine \
    --restart=Never --attach date
Fri Sep 21 13:25:02 UTC 2018

票据

Docker 映像基于 Alpine Linux。它的大小只有 5 MB,比普通的ubuntu映像小一个数量级——普通映像是 Docker 初学者的首选。alpine映像的好处是,无论何时需要额外的实用程序,它都提供对相当完整的包存储库的访问。

考虑到我们没有向date传递参数,我们可以将它作为kubectl的最后一个参数,而不是使用双连字符--语法。然而,如果我们想再次知道日期和时间,会发生什么呢?

$ kubectl run alpine --image=alpine \
    --restart=Never --attach date
Error from server (AlreadyExists): pods "alpine"
already exists

哦,这当然很烦人;我们不能使用相同的名称第二次运行 Pod。这个问题可以通过使用kubectl delete pod/alpine命令删除 Pod 来解决,但是过一会儿就会变得乏味。幸运的是,Kubernetes 团队考虑到了这个用例,并添加了一个可选标志,--rm (remove),这导致 Pod 在命令结束后被删除:

$ kubectl run alpine --image=alpine \
    --restart=Never --attach --rm date
Fri Sep 21 13:30:35 UTC 2018
pod "alpine" deleted

$ kubectl run alpine --image=alpine --restart=Never --attach --rm date
Fri Sep 21 13:30:40 UTC 2018
pod "alpine" deleted

请注意,--rm仅在以附加模式启动 Pod 时起作用,如果 Pod 在循环中运行,Ctrl+C 不会终止 Pod,就像 Nginx 进程默认情况下所做的那样。

目前为止一切顺利。现在我们知道了如何在 Kubernetes 中运行一次性命令,就好像它是一个本地 Linux 机器一样。不过,我们的本地 Linux 机器不仅运行“一劳永逸”的命令,还允许通过流水线向它们输入数据。例如,假设我们想要运行wc(字数统计)命令,并提供一个作为输入的本地 /etc/resolv.conf文件:

$ wc /etc/resolv.conf
  4  24 182 /etc/resolv.conf

$ cat /etc/resolv.conf | \
    kubectl run alpine --image=alpine \
     --restart=Never --attach --rm wc
        0         0         0
pod "alpine" deleted

前面的例子不成立。为什么呢?这是因为--attach标志仅将 Pod 的容器 STDOUT (标准输出)连接到控制台,而不是 STDIN (标准输入)。为了将 STDIN 传输到我们基于 Alpine 的 Pod,需要一个不同的标志,简称为--stdin-i-i标志还会自动将--attach设置为真,因此不需要同时使用这两个标志:

$ cat /etc/resolv.conf | \
    kubectl run alpine --image=alpine \
    --restart=Never -i --rm wc
        4        24       182
pod "alpine" deleted

交互式运行 Pod

到目前为止,我们已经看到了如何运行后台应用,如 web 服务器和一次性命令。如果我们希望通过 shell 或者通过启动一个命令(如mysql客户机)来交互执行命令,该怎么办?然后,我们所要做的就是指定我们想要一个使用--tty标志或-t的终端。我们可以将-t-i组合在一个标志中,得到-ti:

$ kubectl run alpine --image=alpine \
    --restart=Never -ti --rm sh
If you don't see a command prompt, try pressing enter.
/ # ls
bin    etc    lib    mnt    root   sbin   sys    usr
dev    home   media  proc   run    srv    tmp    var
/ # date
Fri Sep 21 16:16:03 UTC 2018
/ # exit
pod "alpine" deleted

与现有 Pod 交互

正如我们之前提到的,我们可以使用kubectl attach命令来访问 Pod 的容器主进程,但是这不允许运行其他命令:我们只能被动地考虑已经在运行的进程的输出。运行一个 shell 并附加到它是行不通的,因为 shell 会立即退出,除非我们创建一个人工循环:

$ kubectl run alpine --image=alpine \
    --restart=Never sh
pod/alpine created

$ kubectl attach alpine
error: cannot attach a container in a completed pod; current phase is Succeeded

现在让我们继续,创建一个人工循环,并尝试再次连接:

# Clean up the environment first

$ kubectl run alpine --image=alpine \
    --restart=Never -- \
    sh -c "while true; do echo 'doing nothing' ; \
    sleep 1; done"
pod/alpine created

$ kubectl attach alpine
Defaulting container name to alpine.
Use 'kubectl describe pod/alpine -n default' to see
all of the containers in this pod.
If you don't see a command prompt, try pressing enter.
doing nothing
doing nothing
doing nothing
...

现在,容器停留在运行状态(我们可以使用kubectl get pod来确保万无一失),因此可以使用kubectl exec <POD-NAME> <COMMAND>命令对其运行命令:

$ kubectl get pod
NAME      READY     STATUS    RESTARTS   AGE
alpine    1/1       Running   0          30s


$ kubectl exec alpine date
Fri Sep 21 16:50:58 UTC 2018

kubectl run类似,kubectl exec命令采用-i标志,这样它可以将 STDIN 通过流水线传输到 Pod 的容器中:

$ cat /etc/resolv.conf | kubectl exec alpine -i wc
        4        24       182

-t标志可用于打开一个控制台并运行一个新的 shell,以便我们可以执行故障排除练习和/或直接在 Pod 的容器内运行新命令:

$ kubectl exec alpine -ti sh
/ # ps
PID USER     TIME  COMMAND
  1 root     0:00 sh -c while true; do ...
408 root     0:00 sh
417 root     0:00 sleep 1
418 root     0:00 ps
/ # exit

检索和跟踪 Pod 的日志

在 Kubernetes 中,Pod 的日志是容器的第一个进程(使用 PID 1 运行)的输出,而不是物理日志文件(例如,/var/log中某处带有.log扩展名的文件)。原则上,我们可以只使用kubectl attach,但是这个命令不记得在发出它之前产生的输出。我们只能看到从连接开始的输出。

相反,kubectl logs <POD-NAME>显示了默认容器的第一个进程自启动以来在 STDOUT 上转储的所有内容——不包括本书范围之外的缓冲限制:

$ kubectl logs alpine
doing nothing
doing nothing
doing nothing
...

如果我们数一数kubectl logs alpine发出的谱线,我们会看到它们会不断增加:

$ kubectl logs alpine | wc
   1431    2862   20034


# Wait one second

$ kubectl logs alpine | wc
   1432    2864   20048

然而,在大多数情况下,我们想要类似于kubectl attach的行为。是的,我们想知道在之前发生了什么,但是一旦我们赶上了,我们想继续关注新的变化,类似于tail -f Unix 命令。嗯,就像在tail的情况下一样,-f标志允许我们随着更多的输出产生而“跟随”日志:

$ kubectl logs -f alpine
doing nothing
doing nothing
...

在本例中,直到我们按 Ctrl+C 中止会话,命令提示符才会出现。

与 Pod 的 TCP 端口交互

在前面的章节中,我们已经看到了启动包含 Nginx web 服务器的 Pod 的例子:

# Clean up the environment first

$ kubectl run nginx --image=nginx \
    --restart=Never --rm --attach
127.0.0.1 - [06:22:08] "GET / HTTP/1.1" 200 612
127.0.0.1 - [06:22:44] "GET / HTTP/1.1" 200 612
127.0.0.1 - [06:22:45] "GET / HTTP/1.1" 200 612

现在,我们首先如何访问 web 服务器,比方说,通过使用curl命令,以便我们可以生成我们在所示输出中看到的请求?嗯,这取决于我们是想从本地计算机还是从 Kubernetes 集群中的另一个 Pod 访问 Pod。

让我们从第一种情况开始。当从我们的本地计算机访问一个 Pod 时,我们需要创建一个从某个本地可用端口(比如说1080)到nginx Pod(默认为80)的桥(称为端口转发)。用于此目的的命令是kubectl port-forward <POD-NAME> <LOCAL-PORT>:<POD-PORT>

# Assume the nginx Pod is still running

$ kubectl port-forward nginx 1080:80
Forwarding from 127.0.0.1:1080 -> 80
Forwarding from [::1]:1080 -> 80

现在,在一个不同的窗口中,我们可以通过访问当前的本地端口 1080 与nginx Pod 进行交互:

# run on a different window, tab or shell
$ curl http://localhost:1080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

第二种情况是从另一个 Pod 内部访问 Pod 的 TCP 端口,而不是我们的本地计算机。这里的挑战是,除非我们设置一个服务(在第四章中讨论),否则 Pod 名称不会自动成为可访问的主机名:

$ kubectl run alpine --image=alpine \
    --restart=Never --rm -ti sh
/ # ping nginx
ping: bad address 'nginx'

相反,我们需要的是找出 Pod 的 IP 地址。每个 Pod 都分配有一个唯一的 IP 地址,这样不同的 Pod 之间就不会发生端口冲突。找出一个 Pod 的 IP 地址的最快方法是发出带有包含IP列的-o wide标志的kubectl get pod命令:

$ kubectl get pod -o wide
NAME   READY STATUS  RESTARTS AGE IP
alpine 1/1   Running 0        2m  10.36.2.8
nginx  1/1   Running 0        7m  10.36.1.5

现在我们可以返回到我们的alpine窗口,使用10.36.1.5而不是nginx:

/ # ping 10.36.1.5
PING 10.36.1.5 (10.36.1.5): 56 data bytes
64 bytes from 10.36.1.5: seq=0 ttl=62 time=1.370 ms
64 bytes from 10.36.1.5: seq=1 ttl=62 time=0.354 ms
64 bytes from 10.36.1.5: seq=2 ttl=62 time=0.364 ms
...

在 Alpine 上,wget是预装的,而不是curl,但它的作用是一样的:

# wget -q http://10.36.1.5 -O -
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

-o wide标志应用到kubectl get pod对于简单的手工检查来说很好,但是在脚本化的自动化场景中,我们可能希望以编程的方式获得 Pod 的 IP 地址。在这种情况下,我们可以使用以下命令从其 JSON 表示中查询 Pod 的pod.status.podIP字段:

$ kubectl get pod/nginx -o jsonpath \
    --template="{.status.podIP}"
10.36.1.5

我们将在本章的后面讨论 Pod 的 JSON 表示。关于 JSONPath 查询的更多信息可以从 http://goessner.net/articles/JsonPath/ 获取。

从 Pod 传输文件或将文件传输到 Pod

除了通过 TCP 连接到 Pods,通过流水线将数据传入和传出它们,并在它们内部打开 shells 之外,我们还可以下载和上传文件。文件传输(用 Kubernetes 的行话来说,复制或cp)是通过使用kubectl cp <FROM-FILE> <TO-FILE>命令来实现的,只要使用了<POD-NAME>:path格式,<*-FILE>就会变成一个 Pod 源或接收器。

例如,nginx 的index.html文件被下载到我们当前的目录,如下所示:

$ kubectl cp \
    nginx:/usr/share/nginx/html/index.html \
    index.html
$ head index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

现在让我们覆盖这个文件,并将其上传回nginx Pod

$ echo "<html><body>Hello World</body></html>" > \
    index.html
$ kubectl cp \
    index.html \
    nginx:/usr/share/nginx/html/index.html

最后,搭建一座桥梁来证明我们文件传输的结果:

$ kubectl port-forward nginx 1080:80
Forwarding from 127.0.0.1:1080 -> 80
# In a different window or tab

$ curl http://localhost:1080
<html><body>Hello World</body></html>

选择 Pod 的容器

如引言中所述,一个 Pod 可以容纳多个容器。在我们到目前为止看到的所有例子中,特别是在运行命令或从现有 Pod 获取日志时,似乎 Pod 和 Docker 映像之间存在1:1关系。例如,当我们发出命令kubectl logs nginx时,似乎nginx舱和容器是同一个东西:

$ kubectl logs nginx
127.0.0.1 - [06:22:08] "GET / HTTP/1.1" 200 612
127.0.0.1 - [06:22:44] "GET / HTTP/1.1" 200 612
127.0.0.1 - [06:22:45] "GET / HTTP/1.1" 200 612
...

嗯,这只是 Kubernetes 很好,为我们自动选择了第一个也是唯一的容器。其实kubectl logs nginx可以认为是kubectl logs nginx -c nginx的简化版。标志-c--container的快捷方式:

$ kubectl logs nginx -c nginx
127.0.0.1 - [06:22:08] "GET / HTTP/1.1" 200 612
127.0.0.1 - [06:22:44] "GET / HTTP/1.1" 200 612
127.0.0.1 - [06:22:45] "GET / HTTP/1.1" 200 612
...

现在,我们如何判断一个 Pod 是否有多个容器?最简单的方法就是发出一个kubectl get pod命令,并在READY列下查看运行/声明的值。例如,每个 Kubernetes 的 DNS pod 由四个容器组成,在下面的示例中,每个容器都已启动并运行(如4/4值所示):

$ kubectl get pod --all-namespaces
NAMESPACE    NAME             READY  STATUS
default      nginx            1/1    Running
kube-system  fluentd-*-tr69s  2/2    Running
kube-system  heapster-*-5rks2 3/3    Running
kube-system  kube-dns-*-48wxf 4/4    Running
...

现在不需要关心名称空间,因为我们将在本章末尾讨论这个问题,但是只要我们引用非用户创建的 Pod,我们就需要指定kube-system名称空间(通过-n kube-system标志)。除非另有说明,用户创建的 pod(如nginx)位于default名称空间中。

注意

如第一章所述,长标识符可以通过用星号替换所述标识符中的样板词片段来缩短。在这个特定的部分中,每当空间受限时,名为kube-dns-5dcfcbf5fb-48wxf的 Pod 也被称为kube-dns-*-48wxf。请注意,这不是通配符语法;pod 必须始终以其全名引用。

回到最初的讨论,如果我们试图获取四容器 Pod(如kube-dns-5dcfcbf5fb-48wxf)的日志(或执行命令),那么 Pod 和容器之间 1:1 映射的假象就会消失:

$ kubectl logs -n kube-system pod/kube-dns-*-48wxf
Error from server (BadRequest):
  a container name must be specified
  for pod kube-dns-*-48wxf, choose one of:
[kubedns dnsmasq sidecar prometheus-to-sd]

从显示的结果中可以看出,我们被要求指定一个特定的 Pod,这是使用-c标志完成的。接下来,我们再次运行kubectl logs,但是使用-c sidecar标志指定sidecar容器:

$ kubectl logs -n kube-system -c sidecar \
    pod/kube-dns-*-48wxf
I0922 06:14:50 1 main.go:51] Version v1.14.8.3
I0922 06:14:50 1 server.go:45] Starting server ...
...

在这种情况下,该命令已经足够友好地通知我们哪些容器是可用的,但是并不是所有的命令都必须这样做。我们可以通过运行kubectl describe pod/<NAME>并查看Containers下面第一个缩进的名称来找出容器的名称:

$ kubectl describe -n kube-system \
    pod/kube-dns-*-48wxf
...
Containers:
  kubedns:
    ...
  dnsmasq:
    ...
  sidecar:
    ...
  prometheus-to-sd:
    ...
...

更程序化的方法是使用 JSONPath 查询 Pod 的 JSON pod.spec.containers.name字段:

$ kubectl get -n kube-system pod/kube-dns-*-48wxf \
  -o jsonpath --template="{.spec.containers[*].name}"
kubedns dnsmasq sidecar prometheus-to-sd

故障排除窗格

到目前为止,在我们检查的所有 Pod 交互用例中,假设一直是考虑中的 Pod 至少运行过一次。如果 Pod 根本不启动,没有日志,也没有像 web 服务器这样的 TCP 服务可供我们使用,那该怎么办?一个 Pod 可能由于各种原因而无法运行:它可能具有不稳定的启动配置,它可能需要过多的 CPU 和 RAM,而这些在 Kubernetes 集群中目前是不可用的,等等。然而,Pods 经常无法初始化的一个常见原因是引用的 Docker 映像不正确。例如,在下面的例子中,我们故意将我们最喜欢的 web 服务器拼错为nginex(在x前加上一个e)而不是nginx:

# Clean up the environment first

$ kubectl run nginx --image=nginex \
    --restart=Never
pod/nginx created

$ kubectl get pod
NAME      READY     STATUS             RESTARTS   AGE
nginx     0/1       ErrImagePull       0          2s
nginx     0/1       ImagePullBackOff   0          15s

尽管ImagePullBackOff告诉了我们一些关于映像的信息,但是我们可以使用kubectl describe pod/<NAME>命令找到更多的细节。该命令提供了一个全面的报告,并在最后说明了相关的 Pod 生命周期事件:

$ kubectl describe pod/nginx
...
Type    Reason  Age     Message
----    ------  ----    -------
...
Normal  Pulling 1m (x3) pulling image "nginex"
Warning Failed  1m (x3) Failed to pull image "nginex"

rpc error: code = Unknown desc =
   Error response from daemon:
     repository nginex not found:
       does not exist or no pull access
  ...

在本例中,我们看到没有找到nginex,Pod 控制器尝试了三次(x3)来获取映像,但没有成功。

kubectl describe命令的主要优点是它在一个人可读的报告中总结了 Pod 最重要的细节。当然,每当我们想要捕获一个特定的细节时,这是没有用的,比如 Pod 被分配到的节点或者它的 IP 地址。

包括 Pods 在内的所有 Kubernetes 对象都被表示为一个对象,其属性可以用 JSON 和 YAML 两种格式呈现。为了获得所述对象结构,我们必须使用常规的kubectl get pod/<NAME>命令并分别添加-o json-o yaml标志:

$ kubectl get pod/nginx -o json | head
{
    "apiVersion": "v1",
    "kind": "Pod",
    "metadata": {
        "annotations": {
            "kubernetes.io/limit-ranger":
               "LimitRanger plugin set:
                cpu request for container nginx"
        },
        "creationTimestamp": "2018-09-22T10:19:10Z",
        "labels": {
            "run": "nginx"

$ kubectl get pod/nginx -o yaml | head
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubernetes.io/limit-ranger:
        'LimitRanger plugin set:
           cpu request for container nginx'
  creationTimestamp: 2018-09-22T10:19:10Z
  labels:
    run: nginx
  name: nginx

所有属性都遵循 JSON 格式的层次结构,可以使用kubectl explain <RESOURCE-TYPE>[.x][.y][.z]命令进行查询,其中x.y.z是嵌套属性。例如:

$ kubectl explain pod
$ kubectl explain pod.spec
$ kubectl explain pod.spec.containers
$ kubectl explain pod.spec.containers.ports

一般来说,大多数 Kubernetes 对象遵循相当一致的结构:

apiVersion: v1 # The object's API version
kind: Pod      # The object/resource type.
metadata:      # Name, label, annotations, etc.
   ...
spec:          # Static properties (e.g. containers)
   ...
status:        # Runtime properties (e.g. podIP)
   ...

可以使用使用--template={}标志指定的 JSONPath 查询检索特定字段,并使用-o jsonpath标志将输出类型更改为jsonpath。例如:

$ kubectl get pod/nginx -o jsonpath \
    --template="{.spec.containers[*].image}"
nginex

Pod 清单

Pod 清单是以声明方式描述 Pod 属性的文件。所有的 POD 都被公式化为一个对象结构。每当我们使用诸如kubectl run这样的命令时,我们实际上是在动态地创建一个 Pod 清单。事实上,我们可以通过向大多数命令添加--dry-run-o yaml标志来查看结果清单。例如:

# Clean up the environment first

$ kubectl run nginx --image=nginx --restart=Never \
    --dry-run=true -o yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: nginx
  name: nginx
spec:
  containers:
  - image: nginx
    imagePullPolicy: IfNotPresent
    name: nginx
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Never
status: {}

我们可以将这个输出保存到一个文件中,比如nginx.yaml,并通过发出kubectl apply -f <MANIFEST>命令从这个文件中创建 Pod:

$ kubectl run nginx --image=nginx --restart=Never \
    --dry-run=true -o yaml > nginx.yaml

$ kubectl apply -f nginx.yaml
pod/nginx created

$ kubectl get pods
NAME      READY     STATUS    RESTARTS   AGE
nginx     1/1       Running   0          0s

我们还可以稍微清理一下nginx.yaml,删除空属性,那些有合理默认值的属性,以及那些只在运行时填充的属性——所有属性和值都在.status下。下面的版本叫做nginx-clean.yaml,是一个由最少的强制属性组成的 Pod 清单:

# nginx-clean.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx

使用kubectl apply -f <MANIFEST>创建的 Pod 可以通过引用对象名(例如kubectl delete pod/nginx来删除,但是 Kubernetes 可以在使用kubectl delete -f <MANIFEST>语法时直接从清单文件中提取对象名:

$ kubectl delete -f nginx-clean.yaml
pod "nginx" deleted

注意

原则上,应该通过发出kubectl create -f <MANIFEST>命令来创建一个全新的 Pod,而不是本教材中使用的基于apply的表单。我们更喜欢基于apply的表单的原因是,如果它已经在运行,它还会更新现有的 Pod(或其他资源类型)。

声明容器的网络端口

Pod 内的所有容器共享相同的端口空间。同样,尽管指定端口号(并命名它们)不是强制性的,但只要声明了两个或更多端口,就必须命名端口。此外,当端口在 pod 清单上正式声明时,服务公开(第四章 ??)需要的步骤更少。底线是声明网络端口是良好的 Pod 清单卫生,所以让我们在下面名为nginx-port.yaml:的清单示例中看看如何做

# nginx-port.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    ports:
    - containerPort: 80
      name: http
      protocol: TCP

.containerPort属性是强制的。默认情况下,.protocol属性的值是TCP。只有在有一个端口被声明的情况下,.name属性才是可选的。如果有多个端口,则每个端口都必须有一个不同的名称。使用kubectl explain pod.spec.containers.ports可以列出其他可选属性。

设置容器的环境变量

许多 Docker 应用映像期望以环境变量的形式定义设置。Mysql 就是一个很好的例子,它至少需要有MYSQL_ROOT_PASSWORD env 变量。环境变量在pod.spec.containers.env被定义为一个数组,其中每个元素由namevalue属性组成:

# mysql.yaml
apiVersion: v1
kind: Pod
metadata:
  name: mysql
spec:
  containers:
  - image: mysql
    name: mysql
    ports:
    - containerPort: 3306
      name: mysql
      protocol: TCP
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: mypassword

即使在使用内部mysql客户端时,连接 MySQL 服务器也确实需要密码:

$ kubectl apply -f mysql.yaml
pod/mysql created


# Wait until mysql transitions to Running


$ kubectl get pod/mysql
NAME    READY   STATUS    RESTARTS   AGE
mysql   1/1     Running   0          105s

$ echo "show databases" | kubectl exec -i mysql \

    -- mysql --password=mypassword
Database
information_schema
mysql
performance_schema
sys

覆盖容器的命令

在本章的前面,我们已经看到了kubectl run命令允许我们覆盖 Docker 映像的默认命令。这也是我们用来运行由可任意处理的 Docker 映像支持的任意命令的机制。例如,如果我们只想检查 UTC 的日期,我们可以如下进行:

$ kubectl run alpine --image=alpine \
    --restart=Never --rm --attach -- date --utc
Sun Sep 23 11:32:03 UTC 2018
pod "alpine" deleted

同样的机制也可以用于执行简单的 shell 脚本,并且不限于运行一次性命令。例如,以下 shell 脚本在无限循环中每秒打印一次当前日期:

$ kubectl run alpine --image=alpine \
    --restart=Never --attach -- \
    sh -c "while true; do date; sleep 1; done"
Sun Sep 23 11:40:59 UTC 2018
Sun Sep 23 11:41:00 UTC 2018
Sun Sep 23 11:41:01 UTC 2018
...

在这两种情况下,kubectl run所做的是用一个数组填充spec.containers.args属性,该数组的第一个元素是命令,第二个和随后的参数是命令的参数。我们可以通过运行kubectl get pod/<POD-NAME> -o yaml并查看下面的内容pod.spec来检查这一点:

$ kubectl get pod/alpine -o yaml | \
    grep "spec:" -A 5
spec:
  containers:
  - args:
    - sh
    - -c
    - while true; do echo date; sleep 1; done

当从头开始创建 Pod 清单时,我们可以使用尖括号数组符号,例如,args: ["sh","-c","while true; do date; sleep 1; done"]。然而,对于长脚本,除了前面显示的数组元素的 YAML 连字符语法之外,我们还可以使用 YAML 流水线语法。我们经常在本文中使用这种方法来提高可读性。这是可用的多线 YAML 选项方法之一。欲了解更多信息,请参考 https://yaml-multiline.info/ ,这有助于找出每个给定多线用例的最佳策略。

这个功能很有用,因为它允许我们以更容易阅读的方式嵌入脚本,如清单alpine-script.yaml所示:

# alpine-script.yaml
apiVersion: v1
kind: Pod
metadata:
 name: alpine
spec:
  containers:
  - name: alpine
    image: alpine
    args:
    - sh
    - -c
    - |
      while true;
        do date;
        sleep 1;
      done

我们仍然必须记住,脚本将作为单个参数传递,因此分号之类的语句结束标记仍然是必要的。我们可以检查alpine-script.yaml脚本的 JSON 表示,看看它是如何翻译的,如下所示:

$ kubectl apply -f alpine-script.yaml
pod/alpine created

$ kubectl get pod/alpine -o json | \
    grep "\"args\"" -A 4
    "args": [
        "sh",
        "-c",
        "while true;\n  do date;\n  sleep 1;\ndone\n"
    ],

在我们结束本节之前,值得一提的是,Docker 映像有一个使用ENTRYPOINT声明在 Docker 文件中定义的入口点命令的概念。由于历史原因以及 Kubernetes 和 Docker 之间的术语差异,pod.spec.containers.args属性以及在kubectl run ... --kubectl exec ... --之后提供的参数会覆盖 Dockerfile 的CMD声明。就其本身而言,CMD既声明了命令,也可能声明了它的参数,例如CMD ["sh", "-c", "echo Hello"]。然而,如果还存在一个ENTRYPOINT声明,这对开发人员来说是一个诅咒,那么规则会以一种扭曲的方式改变:CMD将成为任何入口点命令的默认参数,从而成为 Kubernetes 的pod.spec.container.args属性的默认参数。

大多数现成的 Docker Hub 映像,如nginxbusyboxalpine等,不包含声明。但是如果存在,那么我们需要使用pod.spec.containers.command来覆盖它,然后将pod.spec.containers.args作为所述命令的参数。希望表 2-1 中的例子有助于澄清区别。

表 2-1

Docker 和 Kubernetes 指定命令的结果

|

煤矿管理局

|

ENTRYPOINT(入口点)

|

K8S(消歧义)。args

|

K8S。命令

|

结果

| | --- | --- | --- | --- | --- | | 尝试 | n/a | n/a | n/a | 尝试 | | 尝试 | n/a | ["嘘"] | n/a | 嘘 | | n/a | ["bash"] | n/a | n/a | 尝试 | | [" c "," ls"] | ["bash"] | n/a | n/a | 巴沙尔·c·ls | | n/a | ["bash"] | ["-c ","日期"] | n/a | bash -c 日期 | | [" c "," ls"] | ["bash"] | ["-c ","日期"] | n/a | bash -c 日期 | | [" c "," ls"] | ["bash"] | ["-c ","日期"] | ["嘘"] | sh -c 导弹 |

每当需要覆盖 Dockerfile 的ENTRYPOINT以及命令形式kubectl runkubectl exec时,必须添加--command标志,以便双连字符--之后的第一个参数被视为命令,而不是入口点的第一个参数。例如,下面的祈使句

# Clean up the environment first

$ kubectl run alpine --image=alpine \
    --restart=Never --command -- sh -c date
pod/alpine created

$ kubectl logs alpine
Tue Sep 25 20:28:22 UTC 2018

等效于下面的声明性代码:

# alpine-mixed.yaml
apiVersion: v1
kind: Pod
metadata:
 name: alpine
spec:
  containers:
  - name: alpine
    image: alpine
    command:
    - sh
    args:
    - -c
    - date
$ kubectl apply -f alpine-mixed.yaml
pod/alpine created

$ kubectl logs alpine
Tue Sep 25 20:30:10 UTC 2018

在实践中,如前所述,很少需要处理表 2-1 中呈现的排列,因为大多数 Docker 映像不声明麻烦的ENTRYPOINT参数,因此,习惯上简单地使用pod.spec.containers.args作为数组,其中第一个元素是命令,第二个和后面的元素是它的参数。

管理容器的 CPU 和 RAM 需求

每当我们启动一个 Pod 时,Kubernetes 都会找到一个具有足够 CPU 和 RAM 资源的节点来运行在该 Pod 中声明的容器。同样,每当 Pod 容器运行时,Kubernetes 通常不允许它接管整个节点的 CPU 和内存资源,以免损害同一节点上运行的其他容器。

如果我们不指定任何 CPU 或内存界限,Pod 的容器通常会被赋予默认值,这些值通常是使用名称空间范围的 LimitRanger 对象定义的——这超出了本书的范围。

为什么有必要对计算资源进行细粒度控制,而不是让 Kubernetes 使用默认值?因为当涉及到我们的 Kubernetes 的计算资源时,我们想要节俭。每个节点通常由整个虚拟机(或者在极端情况下甚至是物理机)支持,即使没有容器在其上运行,也必须为其提供资金。

这意味着对于一个生产系统来说,让 Kubernetes 为我们的 Pods 容器分配任意的 CPU 和内存边界并不是一个好的成本和利用率策略。例如,一个小的 C 或 Golang 应用可能需要几兆字节,而一个单一的、容器化的 Java 应用本身可能需要 1GB 以上。在第一种情况下,我们希望告诉 Kubernetes 只分配最少的所需资源:换句话说,在所有条件相同的情况下,为 C 或 Golang 应用分配比 Java 应用小得多的计算资源更好。

现在让我们切入正题,展示 Pod 清单是什么样子的,它包括 CPU 和内存的明确界限:

# nginx-limited.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
    - image: nginx
      name: nginx
      # Compute Resource Bounds
      resources:
        requests:
          memory: "64Mi"
          cpu: "500m"
        limits:
          memory: "128Mi"
          cpu: "500m"

我们可以看到,在nginx-limited.yaml上,我们指定了两次.memory.cpu属性,一次在pod.spec.resources.requests下,然后又一次在pod.spec.resources.limits下。有什么区别?区别在于,第一个是先决条件绑定,而第二个是运行时绑定。请求定义了在 Kubernetes 部署 Pod 和相关容器之前,节点中必须可用的最低计算资源级别。限制,相反,在Kubernetes 将容器部署到一个节点之后,建立允许容器使用的最大计算资源级别。

现在让我们更详细地讨论一下 CPU 和内存界限的表达方式。

CPU 资源以 cpu 单位来衡量,这与 AWS 和 Azure 使用的数量级相同(分别是 vCPU 和 vCore)。然而,它也相当于英特尔 CPU 上的一个超线程——在这种情况下,它可能不是一个物理内核。

Kubernetes 使用的默认度量是毫微微核,值的后缀是 m 。一个毫核心(1000m)正好分配一个 CPU 单元。一些例子如表 2-2 所示。

表 2-2

示例 millipore 值的 cpu 分配

|

例子

|

结果

|

意义

| | --- | --- | --- | | 64m | 64/1000 = 0.064 | CPU 核心的 6.4% | | 128 米 | 128/1000 = 0.128 | CPU 内核的 12.8% | | 500 米 | 500/1000 = 0.5 | CPU 核心的 50% | | 1000 米 | 1000/1000 = 1 | 恰好一个 CPU 内核 | | 2000 米 | 2000/1000 = 2 | 正好两个 CPU 内核 | | 2500 米 | 2500/1000 = 2.5 | 两个 CPU 内核+另一个 CPU 内核的 50% |

也允许分数,例如,0.5 的值将被解释为 500m。然而,从大多数在线例子来看,millicores 似乎是 Kubernetes 社区的首选。

现在让我们把注意力转向记忆。与 CPU 不同,内存总是定义一个绝对值,而不是相对值。内存最终以字节为单位,但通常使用更大的度量单位。可以用十进制和二进制形式指定值:见表 2-3 。

表 2-3

样本内存值的结果,以字节为单位

|

后缀

|

价值

|

例子

|

以字节为单位的示例

| | --- | --- | --- | --- | | 不适用的 | Five hundred and twelve | Five hundred and twelve | Five hundred and twelve | | 千公斤 | One thousand | 128K | One hundred and twenty-eight thousand | | 谁(kibi) | One thousand and twenty-four | 128Ki | One hundred and thirty-one thousand and seventy-two | | 百万英镑 | 1000² | 128 米 | One hundred and twenty-eight million | | 米(mebi) | 1024² | 128 米 | One hundred and thirty-four million two hundred and seventeen thousand seven hundred and twenty-eight | | 千兆克 | 1000³ | 第一代 | One billion | | Gi(如) | 1024³ | 1Gi | One billion seventy-three million seven hundred and forty-one thousand eight hundred and twenty-four |

关于请求限制的最后一点评论是,它们并不相互排斥。请求指定了容器在正常情况下运行的界限,而限制代表了最大上限。就 CPU 而言,包括 GKE 在内的大多数 Kubernetes 实现通常会对容器进行节流,但是超过限制值的内存消耗可能会导致突然终止。

注意

指定一个过于悲观的pod.spec.resources.limits值可能会导致灾难性的后果;整个舰队的吊舱可能会不断被杀死和重建,因为他们一再超过指定的上限。在决定使用哪些值之前,最好先在现实条件下对适用应用的运行时行为进行采样。

Pod 卷和卷装载

Kubernetes 中的卷是一种抽象,用于使类似 Unix 的文件系统可以从 Pod 的容器中访问。容器自己的文件系统和卷之间的主要区别在于,大多数卷类型超越了容器的生命周期。换句话说,每当容器崩溃、存在或重新启动时,写入容器文件系统的文件就会丢失。

类似于 Unix 中的mount命令,卷提供了封装实际存储机制及其位置的抽象。就容器而言,卷只是一个本地目录。但是,卷的实施和属性可能会有很大差异:

  • 它可能只是一个临时文件系统,这样单个 Pod 中的容器就可以交换数据。这样的卷类型称为emptyDir

  • 它可能是节点的文件系统中的一个目录,例如hostPath——如果 Pod 被调度到不同的节点,将无法访问该目录。

  • 可能是网络存储设备比如谷歌云存储卷(简称gcePersistentVolume)或者 NFS 服务器。

让我们从最常见、最简单的卷开始:Pod 中的临时文件系统,称为emptyDiremptyDir卷类型与 Pod 生命周期密切相关,可以使用 tmpfs(一种 RAM 支持的文件系统)来获得更快的读/写速度。这是同一 Pod 中两个或更多容器交换数据的默认卷类型。声明 Pod 卷涉及两个方面:

  1. spec.volumes下声明并命名(我们将使用data)Pod 级别的卷,并指定卷类型:在我们的例子中为emptyDir

  2. spec.containers.volumeMounts下的每个适当的容器中安装相关的卷,并在容器中指定将用于访问引用卷的路径(我们选择了/data/)

我们现在将把dataspec.volumesspec.containers.volumeMounts声明组装成一个完整的 Pod 清单文件,称为alpine-emptyDir.yaml:

# alpine-emptyDir.yaml
apiVersion: v1
kind: Pod
metadata:
 name: alpine
spec:
  volumes:
    - name: data
      emptyDir:
  containers:
  - name: alpine
    image: alpine
    args:
    - sh
    - -c
    - |
      date >> /tmp/log.txt;
      date >> /data/log.txt;
      sleep 20;
      exit 1; # exit with error
    volumeMounts:
      - mountPath: "/data"
        name: "data"

alpine-emptyDir.yaml清单将运行一个 shell 脚本,将date命令的输出记录到/tmp/log.txt/data/log.txt中。然后,它将等待 20 秒并出错退出,这将强制容器重启,除非pod.spec.restartPolicy属性被设置为Never

目标是运行 Pod 并让它“崩溃”至少两次:

$ kubectl apply -f alpine-emptyDir.yaml
pod/alpine created

$ kubectl get pod -w
NAME    READY STATUS             RESTARTS  AGE
alpine  1/1   Running            0          0s
alpine  0/1   Error              0         18s
alpine  1/1   Running            1         19s
alpine  0/1   Error              1         39s
alpine  0/1   CrashLoopBackOff   1         54s
alpine  1/1   Running            2         54s
...

在第三次重启时,我们查询/tmp/log.txt/data/log.txt:的内容

$ kubectl exec alpine -- \
    sh -c "cat /tmp/log.txt ; \
    echo "---" ; cat /data/log.txt"
Wed Sep 26 07:20:38 UTC 2018
---

Wed Sep 26 07:19:43 UTC 2018
Wed Sep 26 07:20:04 UTC 2018
Wed Sep 26 07:20:38 UTC 2018

正如所料,/tmp/log.txt只显示了日期时间戳的一个实例,而/data/log.txt显示了三个实例,尽管容器已经崩溃了三次。这是因为,如前所述,emptyDir与 POD 的生命周期息息相关。事实上,删除并重启 Pod 将会删除emptyDir,因此在重启 Pod 后立即查询/data/log.txt将只显示一个条目:

$ kubectl delete -f alpine-emptyDir.yaml
pod "alpine" deleted


$ kubectl apply -f alpine-emptyDir.yaml
pod/alpine created


$ kubectl exec alpine -- cat /data/log.txt
Wed Sep 26 11:25:09 UTC 2018

一种似乎是emptyDir卷型的更稳定的替代品是hostPath卷型。这种类型的卷会在节点的文件系统中装入一个实际目录:

# alpine-hostPath.yaml
...
spec:
  volumes:
    - name: data
      hostPath:
        path: /var/data
...

hostPath卷类型对于以只读方式访问 Kubernetes 文件(如/var/log/kube-proxy.log)很有用,但对于存储我们自己的文件来说,它不是一个好的卷类型。主要有两个原因。首先,除非我们指定一个节点选择器(在前面的几节中有更多关于标签和选择器的内容),否则 Pods 可能会被安排在任何随机的节点上运行。这意味着一个 Pod 可能最初在节点6m9k上运行,但是在删除和重建事件之后在7tck上运行:

$ kubectl get pods -o wide
NAME   READY STATUS  RESTARTS AGE IP         NODE
alpine 1/1   Running 8        23m 10.36.0.10 *-6m9k


$ kubectl delete pod/alpine
pod/alpine deleted

$ kubectl apply -f alpine-hostPath.yaml
pod/alpine created


$ kubectl get pods -o wide
NAME   READY STATUS  RESTARTS AGE IP         NODE
alpine 1/1   Running 1        23m 10.36.0.11 *-7tck

不鼓励使用hostPath存储用户文件的第二个原因是 Kubernetes 节点本身可能会因为升级、打补丁等原因而被破坏和重新创建。,对用户创建的数据的保存和/或稳定名称的使用没有任何保证。同样,一般故障也可能阻止节点恢复。在这种情况下,无论何时恢复和/或重新创建新节点,都不会保留或回收其系统卷。

外部卷和 Google 云存储

我们在上一节中看到的emptyDirhostPath卷类型仅适用于 Kubernetes 集群。前者绑定到 Pod 的生命周期,而后者绑定到节点的分配。

对于严重的长期数据持久性,我们经常需要访问完全独立于 Pod 和整个 Kubernetes 集群本身的企业级存储。一种这样的存储是 GCP 的谷歌云存储,我们在其中定义的卷被简单地称为 ?? 磁盘。

让我们继续使用gcloud command:创建一个 1GB 的磁盘

$ gcloud compute disks create my-disk --size=1GB \
    --zone=europe-west2-a
Created
NAME     ZONE            SIZE_GB  TYPE         STATUS
my-disk  europe-west2-a  1        pd-standard  READY

现在,我们在 Pod 清单中要做的就是使用pdName属性声明一个gcePersistentDisk卷类型和引用my-disk。由于磁盘是通用的块设备,我们还需要使用fsType属性指定特定的文件系统类型:

# alpine-disk.yaml
...
spec:
  volumes:
    - name: data
      gcePersistentDisk:
        pdName: my-disk
        fsType: ext4
...

除了更改卷类型之外,我们还将修改 shell 脚本,以便它能够跟踪/data/log.txt,而不是以错误结束:

# alpine-disk.yaml
...
spec:
  containers:
  - name: alpine
    image: alpine
    args:
    - sh
    - -c
    - date >> /data/log.txt; cat /data/log.txt
...

Pod 将运行一次并完成,生成一个日期条目,现在可以使用kubectl logs进行检查:

$ kubectl apply -f alpine-disk.yaml
pod/alpine created

# Wait until the pod's status is Running first

$ kubectl logs alpine
Fri Sep 28 15:26:08 UTC 2018

我们现在将删除Kubernetes 群集本身,并开始一个新的、全新的群集,以证明存储具有分离的生命周期:

$ ~/kubernetes-gcp/chp1/destroy.sh

# Wait a couple of minutes

$ ~/kubernetes-gcp/chp1/create.sh
Creating cluster...

如果我们应用alpine-disk.yaml Pod 清单,我们将看到除了刚才生成的日期条目之外,上次运行的日期条目仍然存在:

$ kubectl apply -f alpine-disk.yaml
pod/alpine created

$ kubectl logs alpine
Fri Sep 28 15:26:07 UTC 2018
Fri Sep 28 15:46:11 UTC 2018

其他云供应商也有类似的卷类型。比如 AWS 里有awsElasticBlockStorage而 Azure 里有azureDisk。Kubernetes 团队几乎在每个新版本中都不断增加对其他存储机制的支持。如需最新名单,请查看 https://kubernetes.io/docs/concepts/storage/volumes/

有关每种卷类型的设置和字段的更多信息,也可以使用kubectl explain命令,例如kubectl explain pod.spec.volumes.azureDisk

Pod 健康和生命周期

Kubernetes 能够通过一种叫做探针的机制持续监控 Pod 的健康状态。可以在两个不同的类别下声明探测:就绪存活:

  • 准备就绪 : 容器准备就绪以服务用户请求,以便 Kubernetes 可以决定是否添加从服务负载均衡器中移除Pod。

  • 活跃度 : 容器正在按照设计者的意图运行*,以便 Kubernetes 可以决定容器是否“卡住”并且必须*重启。**

    # Pod manifest file snippet
    spec:
      containers:
         - image: ...
           readinessProbe:
             # configuration here
           livenessProbe:
             # configuration here
    
    

readinessProbelivenessProbe下声明了相同类型的探针,其中典型的有基于命令的HTTPTCP :

  • 基于命令的 : Kubernetes 在容器内运行命令,检查结果是否成功(返回代码= 0)。

  • HTTP : Kubernetes 查询一个 HTTP URL,检查返回码是否大于等于 200 但小于 400。

  • TCP : Kubernetes 只是检查它是否设法打开了一个指定的 TCP 端口。

让我们从基于命令的探测开始。这是最容易实现的,因为它涉及到运行任意命令;只要退出状态代码为 0,容器就被认为是健康的。例如:

# Pod manifest file snippet
spec:
  containers:
    - image: ...
      livenessProbe:
        exec:
          command:
          - cat
          - /tmp/healthy
        initialDelaySeconds: 5
        periodSeconds: 5

在这个代码片段中,只有当/tmp/healthy存在时,cat /tmp/healthy才会返回退出代码 0。这种方法允许向不暴露网络接口的应用添加健康探测器,或者即使暴露了网络接口,这种接口也不能被检测以提供健康状态信息。

现在让我们来看看 HTTP 探针。最基本的 HTTP 探测只是查看 web 服务器的 HTTP 响应状态,检查它是否在 200 和 399 之间;它不要求任何特定的返回主体。任何其他代码都会导致错误。例如,下面的代码片段可以看作是运行curl -I http://localhost:8080/healthy命令并检查HTTP头的状态:

# Pod manifest file snippet
spec:
  containers:
     - image: ...
       livenessProbe:
         httpGet:
           path: /healthy
           port: 8080
         initialDelaySeconds: 5
         timeoutSeconds: 1

其他属性包括

  • host:托管 URL 的主机名;默认情况下,这是 Pod IP。

  • scheme : HTTP(默认)或 HTTPS(将跳过证书验证)。

  • httpHeaders:自定义 HTTP 头。

最后一种探针是 TCP。在最基本的配置中,TCP 探测器只是测试 TCP 端口是否可以打开。从这个意义上说,它甚至比 HTTP 探测更原始,因为它不需要特定的响应。要实现 TCP 探测,我们只需要指定tcpSocket探测类型及其port属性:

# Pod manifest file snippet
spec:
  containers:
     - image: ...
       livenessProbe:
         tcpSocket:
           port: 9090
         initialDelaySeconds: 15
         periodSeconds: 20

既然我们已经介绍了三种不同的探测类型,那么让我们来看看在就绪性和活性上下文中使用它们之间的区别,以及其他附加属性的含义,比如我们还没有讨论过的initialDelaySecondsperiodSeconds

就绪探测器和活跃度探测器的区别在于,就绪探测器告诉 Kubernetes 容器是否应该从服务对象中移除,而服务对象通常通过负载均衡器向外部消费者公开(参见第四章),而活跃度探测器告诉 Kubernetes 容器是否必须重启。从配置的角度来看,两种情况下的低级检查是相同的。只需将特定的探测命令(如httpGet)放在livenessProbereadinessProbe下即可:

# Pod manifest file snippet
spec:
  containers:
     - image: ...
       # A health check failure will result in a
       # container restart
       livenessProbe:
         httpGet:
         ...
# Pod manifest file snippet
spec:
  containers:
     - image: ...
       # A health check failure will result in the
       # container being taken off the load balancer
       readinessProbe:
         httpGet:
         ...

单个容器定义通常同时具有就绪性和活性探测。它们并不相互排斥。

在我们结束这一部分之前,剩下要讨论的是如何频繁以及在何种条件下一个探测将导致声明一个 Pod 为无响应,或者不能服务请求,分别是在活跃度和就绪上下文的情况下。使用initialDelaySecondstimeoutSecondsperiodSecondsfailureThreshold属性来实现对探测器的细粒度行为控制。例如:

# Pod manifest file snippet
spec:
  containers:
     - image: ...
       livenessProbe:
         httpGet:
           path: /healthy
           port: 8080
         initialDelaySeconds: 5
         timeoutSeconds: 1
         periodSeconds: 10
         failureThreshold: 3
         successThreshold: 1

让我们一次看一个属性:

initialDelaySeconds: 5

这里我们说,我们希望在探测开始之前至少等待五秒钟。例如,对于启动可能需要一段时间的 web 服务器,这很有用:

timeoutSeconds: 1

如果我们的服务响应有点慢,我们可能希望给它额外的时间。在这种情况下,我们在端口 8080 上的 http 服务器几乎必须立即响应(在一秒钟内):

periodSeconds: 10

我们不能每一秒钟都发垃圾邮件。我们应该做一个检查,对结果满意,然后回来做另一个测试。该属性控制探测器运行的频率:

failureThreshold: 3

我们应该仅仅因为一个错误就认为探测检查失败吗?肯定不会。此属性控制需要多少次失败才能认为容器对外部世界失败:

successThreshold: 1

这很奇怪。成功门槛有什么用?嗯,有了failureThreshold,我们可以控制需要多少个连续的故障来解释一个容器故障(无论是在就绪性还是活性上下文中)。这就像一个计数器:一次失败,两次失败,三次失败,然后…砰!。但是我们如何重置这个计数器呢?通过计数成功的探测检查。默认情况下,只需要一个成功的结果就可以将计数器重置为零,但我们可能会更悲观,等待两个或更多。

表 2-4 总结了所讨论的属性,并显示了它们的默认值和最小值。

表 2-4

探测器属性的默认值和最小值

|

探测属性名称

|

默认

|

最低限度

| | --- | --- | --- | | initialDelaySeconds | 不适用的 | 不适用的 | | periodSeconds | Ten | one | | timeoutSeconds | one | one | | successThreshold | 1* | one | | failureThreshold | three | one |

我们还可以看看这些属性是如何在一些关键阶段适应 Pod 生命周期的。这一分类可能有助于从另一个角度阐明它们的适用性:

  1. **容器已创建:**此时,探测器还没有运行。在转换到由initialDelaySeconds属性设置的(2)之前有一个等待状态。

  2. **探测开始:**这是当失败和成功计数器被设置为零并且转换到(3)时发生的。

  3. **运行探测检查:**当执行特定检查(如 HTTP)时,由timeoutSeconds属性设置的超时计数器启动。如果检测到故障或超时,则转换到(4)。如果没有超时并且检测到成功状态,则转移到(5)。

  4. **失败:**这种情况下,失败计数器递增,成功计数器置零。然后,有一个到(6)的过渡。

  5. **成功:**在这种情况下,成功计数器递增,并转换到(6)。如果成功计数器大于或等于successThreshold属性,则失败计数器被设置为零。

  6. **确定故障:**如果故障计数器大于或等于由failureThreshold属性指定的值,探测器报告故障——该动作将取决于它是就绪还是活动探测器。否则,将有一个由periodSeconds属性确定的等待状态,然后将发生到(3)的转换。

这个关于 Pod 生命周期的视图是以探针的行为为中心的。第九章对 Pod 生命周期进行了更全面的描述,有助于理解其在服务控制器和有状态服务方面的含义。

名称空间

名称空间是 Kubernetes 中的一个通用概念,而不是 Pods 的专有概念,但是在 Pods 的上下文中了解它们很方便,因为最终,Kubernetes 中的所有工作负载都位于 Pods 中,并且所有 Pods 都位于一个名称空间中。

名称空间是 Kubernetes 用来按照用户定义的标准隔离资源的机制。例如,名称空间可以隔离开发生命周期环境,如开发、测试、登台和生产。他们还可以帮助组织相关资源,而不一定打算建立一个中国墙;例如,一个名称空间可能用于将“产品目录”组件组合在一起,而另一个名称空间用于“订单履行”组件。

让我们以一种相当经验性的方式来看待名称空间。运行kubectl get pod时,似乎什么都没有,除非我们启动自己的 Pod,如alpine:

$ kubectl get pod
NAME      READY     STATUS    RESTARTS   AGE
alpine    1/1       Running   0          31m

这是一种错觉,因为大多数 Kubernetes 命令的目标是用户第一个对象运行的名称空间default。事实上,大多数 Kubernetes 命令都可以被认为隐含了标志-n default,这是--namespace=default的快捷方式:

$ kubectl get pod -n default
NAME      READY     STATUS    RESTARTS   AGE
alpine    1/1       Running   0          33m

正如我们之前提到的,Kubernetes 中几乎所有的工作负载都位于 pod 中。这不仅适用于用户创建的工件,也适用于 Kubernetes 基础设施组件。Kubernetes 自己的大部分实用程序和进程都是作为常规的 Pods 实现的,但是它们在默认情况下看起来是不可见的,因为它们恰好位于一个名为kube-system的独立名称空间中:

$ kubectl get pod -n kube-system
NAME                    READY STATUS   RESTARTS   AGE
event-exporter-*-vsmlb  2/2   Running  0          2d
fluentd-gcp-*-gz4nc     2/2   Running  0          2d
fluentd-gcp-*-lq2lx     2/2   Running  0          2d
fluentd-gcp-*-srg92     2/2   Running  0          2d
heapster-*-xwmvv        3/3   Running  0          2d
kube-dns-*-p95tp        4/4   Running  0          2d
kube-dns-*-wjzqz        4/4   Running  0          2d
...

这些都是普通的 POD。我们可以对它们运行我们目前所学的所有命令。我们只需要记住给我们使用的每个命令添加-n kube-system标志。否则 Kubernetes 会假设-n default。例如,让我们看看在名为kube-dns-*-p95tp的 Pod 中找到的第一个容器内部正在运行什么进程:

$ kubectl exec -n kube-system kube-dns-*-p95tp ps
Defaulting container name to kubedns.
PID USER TIME COMMAND
  1 root 4:35 /kube-dns --domain=cluster.local. ...
 12 root 0:00 ps

如果我们想要识别给定 Pod 被分配到的名称空间,我们可以将标志--all-namespaceskubectl get命令一起使用。例如:

$ kubectl get pod --all-namespaces
NAMESPACE    NAME                   READY STATUS
default      alpine                 1/1   Running
kube-system  event-exporter-*-vsmlb 2/2   Running
kube-system  fluentd-gcp-*-gz4nc    2/2   Running
...

请注意在这个输出中,第一列是如何标识定义每个 Pod 的名称空间的。我们还可以使用kubectl get namespaces列出现有的名称空间本身:

$ kubectl get namespace
NAME          STATUS    AGE
default       Active    2d
kube-public   Active    2d
kube-system   Active    2d

名称空间是 Kubernetes 对象之间最难的逻辑分离形式。假设我们定义了三个不同的名称空间,分别叫做ns1ns2ns3:

$ kubectl create namespace ns1
namespace/ns1 created
$ kubectl create namespace ns2
namespace/ns2 created
$ kubectl create namespace ns3
namespace/ns3 created

我们现在可以在每个名称空间中运行一个名为nginx的 Pod,而不会有任何 Pod 名称冲突:

$ kubectl run nginx --image=nginx --restart=Never \
    --namespace=ns1
pod/nginx created
$ kubectl run nginx --image=nginx --restart=Never \
    --namespace=ns2
pod/nginx created
$ kubectl run nginx --image=nginx --restart=Never \
    --namespace=ns3
pod/nginx created

$ kubectl get pod –-all-namespaces | grep nginx
ns1       nginx   1/1   Running     0    1m
ns2       nginx   1/1   Running     0    1m
ns3       nginx   1/1   Running     0    1m

标签

标签只是用户定义的(或 Kubernetes 生成的)键/值对,它们与一个 Pod(以及任何其他 Kubernetes 对象)相关联。它们对于描述元信息的小元素(键和值都限制在 63 个字符以内)非常有用,例如

  • 一系列相关对象(例如,同一 Pod 的副本)

  • 版本号

  • 环境(例如,开发、试运行、生产)

  • 部署类型(例如,canary 发布或 A/B 测试)

标签是 Kubernetes 中的一个基本概念,因为它是促进编排的机制。当多个 pod(或其他对象类型)被“编排”时,控制器对象(如部署)管理一群 pod 的方式是选择它们的标签,我们将在第三章中看到。

例如,innocent kubectl run命令为每个 Pod 添加一个名为“run”的标签,其值为 Pod 的给定的名称:

$ kubectl run nginx --image=nginx --restart=Never
pod/nginx created

$ kubectl get pods --show-labels
NAME   READY  STATUS    RESTARTS   AGE    LABELS
nginx  1/1    Running   0          44s    run=nginx

正如在这个输出中看到的,--show-labels标志显示了已经为列出的对象声明的标签。标签既可以强制设置,也可以声明设置。例如,下面的 Pod 清单将标签envauthor分别设置为prodErnie:

# nginx-labels.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: prod
    author: Ernie
spec:
  containers:
  - image: nginx
    name: nginx
  restartPolicy: Never

这相当于以下命令式语法:

$ kubectl run nginx --image=nginx --restart=Never \
    -l "env=prod,author=Ernie"

-l标志是--labels="<LABELS>"的快捷方式,其中<LABELS>是逗号分隔的键/值对列表。通过运行kubectl apply -f nginx-labels.yaml和使用之前看到的命令性命令,我们可以观察到两个用户定义的标签authorenv已经被设置,而不是默认的run=nginx标签:

$ kubectl get pods --show-labels
NAME  READY STATUS  AGE LABELS
nginx 1/1   Running 3m  author=Ernie,env=prod

在我们继续之前,让我们通过创建两个标签略有不同的 nginx Pods 来增加一点复杂性:

$ kubectl run nginx1 --image=nginx --restart=Never \
    -l "env=dev,author=Ernie"
pod/nginx1 created

$ kubectl run nginx2 --image=nginx --restart=Never \
    -l "env=dev,author=Mohit"
pod/nginx2 created

标签的用处不仅仅在于向离散对象添加任意元数据(目前只是 Pods,因为我们还没有涉及其他资源类型),还在于处理它们的集合。尽管标签是与模式无关的,但是认为我们是在数据库中定义列类型还是有帮助的。例如,现在我们知道了 podnginxnginx1nginx2有一个共同点,即它们分别通过envauthor标签声明了它们的环境和作者,我们可以使用-L <LABEL1,LABEL2,...>标志指定我们希望这些值被列为特定的列:

$ kubectl get pods -L env,author
NAME    READY   STATUS  RESTARTS  AGE   ENV   AUTHOR
nginx   1/1     Running 0         11m   prod  Ernie
nginx1  1/1     Running 0         6m    dev   Ernie
nginx2  1/1     Running 0         5m    dev   Mohit

如果我们不能制定诸如“给我作者是 Ernie 的对象”或“那些有caution标签的对象”这样的查询,标签就没有用了这种表达式被称为选择器表达式。第一个问题是基于等式的表达式,而第二个问题是基于集合的表达式。选择器表达式或简称为选择器在许多控制器对象的清单中声明,以将它们与它们的依赖项连接起来,但它们也可以通过-l <SELECTOR-EXPRESSION>标志强制表达,这是--selector=<SELECTOR-EXPRESSION>的快捷方式。

例如,第一个问题表述如下:

$ kubectl get pods -l author=Ernie
NAME      READY     STATUS    RESTARTS   AGE
nginx     1/1       Running   0          30m
nginx1    1/1       Running   0          24m

我们也可以否定等式表达式,并要求那些 Pods 的作者不是 Ernie:

$ kubectl get pods -l author!=Ernie
NAME      READY     STATUS    RESTARTS   AGE
nginx2    1/1       Running   0          24m

基于集合的问题是关于成员资格的。几分钟前,我们曾询问过标签名为caution的吊舱。在这种情况下,我们只需指定标签名称:

$ kubectl get pods -l caution
No resources found.

的确,我们还没有定义一个caution标签。要询问那些没有标签的对象,我们只需在标签前加上感叹号,如下所示:

$ kubectl get pods -l \!caution
NAME      READY     STATUS    RESTARTS   AGE
nginx     1/1       Running   0          37m
nginx1    1/1       Running   0          32m
nginx2    1/1       Running   0          31m

一种更高级的基于集合的选择器是我们使用<LABEL> in (<VALUE1>,<VALUE2>,...)语法测试多个值的选择器(notin用于求反)。例如,让我们列出那些作者是 Ernie 或 Mohit 的 pod:

$ kubectl get pods -l "author in (Ernie,Mohit)"
NAME      READY     STATUS    RESTARTS   AGE
nginx     1/1       Running   0          50m
nginx1    1/1       Running   0          44m
nginx2    1/1       Running   0          43m

也可以在运行时使用kubectl label <RESOURCE-TYPE>/<OBJECT-IDENTIFIER> <KEY>=<VALUE>命令更改标签。如果我们正在改变一个现有的标签,还必须添加--overwrite标志。例如:

$ kubectl label pod/nginx author=Bert --overwrite
pod/nginx labeled

$ kubectl get pods -L author
NAME   READY  STATUS    RESTARTS   AGE    AUTHOR
nginx  1/1    Running   0          51m    Bert
nginx1 1/1    Running   0          46m    Ernie
nginx2 1/1    Running   0          45m    Mohit

现在,我们可以通过请求那些作者既不是 Ernie 也不是 Mohit 的 pod 来再次运行基于集合的查询:

$ kubectl get pods -l "author notin (Ernie,Mohit)"
NAME      READY     STATUS    RESTARTS   AGE
nginx     1/1       Running   0          54m

最后,可以使用kubectl label <RESOURCE-TYPE>/<OBJECT-IDENTIFIER> <KEY>-移除标签(注意末尾的减号)。以下两条语句添加和删除nginxcaution标签:

$ kubectl label pod/nginx caution=true
pod/nginx labeled

$ kubectl label pod/nginx caution-
pod/nginx labeled

释文

注释类似于标签,因为它们是一种基于键/值的元数据。但是,它们的目的是存储不可识别、不可选择的数据—选择表达式对注释不起作用。

在大多数情况下,注释是静态的,而不是易变的元数据。它们在pod.metadata.annotations内声明:

# Pod manifest file snippet
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  annotations:
    author: Michael Faraday
    e-mail: michael@faraday.com

此外,注释不像标签值那样有 63 个字符的限制。它们可能包含无结构的长字符串。

注释的检索通常是通过 JSONPath 来完成的,因为这并不是针对选择器表达式的。例如,如果author被定义为nginx上的注释字段,我们可以使用以下命令:

$ kubectl get pod/nginx -o jsonpath \
    --template="{.metadata.annotations.author}"
Michael Faraday

摘要

在本章中,我们学习了如何使用 Kubernetes Pods 启动、交互和管理容器化的应用。这包括参数的传递、数据的进出以及网络端口的暴露。然后,我们探讨了更高级的特性,比如 CPU 和 RAM 约束的设置、外部存储的安装以及使用探针进行健康检查的工具。最后,我们展示了标签和注释如何帮助标记、组织和选择窗格——以及几乎任何其他 Kubernetes 对象类型。

本章中所获得的理解足以将 Kubernetes 集群视为一台拥有大量 CPU 和 RAM 的巨型计算机,可以在其中部署整体工作负载。从这个意义上说,这一章是独立的。例如,以一种整体的方式安装一个传统的三层应用,如 WordPress——禁止在公共互联网上公开,在第四章中讨论——不需要我们在这里讨论的特性。

接下来的章节都是关于通过使用 Kubernetes 控制器来超越传统单片的特性,这些控制器为我们提供了高级功能,如高可用性、容错、服务发现、作业调度和分布式数据存储的工具。