Docker 容器终极之书第三版(五)

原文:annas-archive.org/md5/951792ab738574a4713e2995dc6f7c0c

译者:飞龙

协议:CC BY-NC-SA 4.0

第十六章:16

介绍 Kubernetes

本章介绍当前最流行的容器编排工具。它介绍了用于定义和运行分布式、弹性、稳健和高度可用应用程序的 Kubernetes 核心对象。最后,它介绍了 minikube 作为本地部署 Kubernetes 应用程序的一种方式,以及 Kubernetes 与 Docker Desktop 的集成。

我们将讨论以下主题:

  • 理解 Kubernetes 架构

  • Kubernetes 主节点

  • 集群节点

  • Play with Kubernetes 简介

  • Kubernetes 在 Docker Desktop 中的支持

  • Pod 简介

  • Kubernetes ReplicaSets

  • Kubernetes 部署

  • Kubernetes 服务

  • 基于上下文的路由

  • 比较 SwarmKit 和 Kubernetes

阅读完本章后,你应该掌握以下技能:

  • 在餐巾纸上草拟 Kubernetes 集群的高级架构

  • 解释 Kubernetes Pod 的三到四个主要特性

  • 用两到三句话描述 Kubernetes ReplicaSets 的作用

  • 解释 Kubernetes 服务的两到三个主要职责

  • 在 minikube 中创建一个 Pod

  • 配置 Docker Desktop 以使用 Kubernetes 作为编排工具

  • 在 Docker Desktop 中创建一个 Deployment

  • 创建一个 Kubernetes 服务来暴露应用程序服务(内部或外部)到集群中

技术要求

在本章中,如果你想跟随代码示例,你需要安装 Docker Desktop 和一个代码编辑器——最好是 Visual Studio Code:

  1. 请导航到你克隆示例代码库的文件夹。通常,这应该是~/The-Ultimate-Docker-Container-Book

    $ cd ~/The-Ultimate-Docker-Container-Book
    
  2. 创建一个名为ch16的新子文件夹,并进入该文件夹:

    $ mkdir ch16 && cd ch16
    

本章讨论的所有示例的完整解决方案可以在sample-solutions/ch16文件夹中找到,或者直接访问 GitHub:github.com/PacktPublishing/The-Ultimate-Docker-Container-Book/tree/main/sample-solutions/ch16

理解 Kubernetes 架构

一个 Kubernetes 集群由一组服务器组成。这些服务器可以是虚拟机或物理服务器,后者也叫裸金属服务器。集群的每个成员都可以有两种角色之一。它要么是 Kubernetes 主节点,要么是(工作)节点。前者用于管理集群,而后者则运行应用程序工作负载。我将工作节点放在括号中,因为在 Kubernetes 术语中,只有在谈到运行应用程序工作负载的服务器时才会提到节点。但在 Docker 和 Swarm 的术语中,相当于工作节点。我认为“工作节点”这一概念更好地描述了服务器的角色,而不仅仅是一个简单的节点。

在一个集群中,你有一个小且奇数数量的主节点,以及根据需要的多个工作节点。小型集群可能只有几个工作节点,而更现实的集群可能有几十个甚至上百个工作节点。从技术上讲,集群可以拥有的工作节点数量没有限制。然而,实际上,当处理成千上万个节点时,某些管理操作可能会出现显著的性能下降。

在 Kubernetes 工作节点上,我们运行的是 Pods。这是一个在 Docker 或 Docker Swarm 中没有的概念。Pod 是 Kubernetes 集群中的原子执行单元。在很多情况下,一个 Pod 只包含一个容器,但一个 Pod 也可以由多个容器共同运行。我们将在本节稍后对 Pods 进行更详细的描述。

集群中的所有成员需要通过物理网络连接,所谓的底层网络。Kubernetes 为整个集群定义了一个平面网络。Kubernetes 本身并不提供任何网络实现,而是依赖于第三方的插件。

Kubernetes 只是定义了容器网络接口CNI),并将实现留给其他人。CNI 非常简单。它规定,集群中运行的每个 Pod 必须能够与集群中任何其他 Pod 相互连接,而不会发生任何网络地址转换NAT)。同样的要求也适用于集群节点和 Pods 之间,即,直接在集群节点上运行的应用程序或守护进程必须能够访问集群中的每个 Pod,反之亦然。

下图展示了 Kubernetes 集群的高级架构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.1 – Kubernetes 的高级架构图

上图的解释如下:

在顶部框中间,我们有一个 etcd 节点集群。etcd 节点是一个分布式键值存储,在 Kubernetes 集群中用于存储集群的所有状态。etcd 节点的数量必须是奇数,正如 Raft 一致性协议所要求的那样,该协议指定了哪些节点用于相互协调。我们谈论集群状态时,并不包括由运行在集群中的应用程序产生或消耗的数据。相反,我们指的是关于集群拓扑、运行的服务、网络设置、使用的密钥等所有信息。也就是说,这个 etcd 集群对整个集群至关重要,因此,我们永远不应在生产环境或任何需要高可用性的环境中仅运行单个 etcd 服务器。

然后,我们有一个 Kubernetes 主节点集群,它们也在彼此之间形成一个共识组,类似于etcd节点。主节点的数量也必须是奇数。我们可以运行一个单主节点的集群,但在生产环境或关键任务系统中,我们绝不应该这样做。在这种情况下,我们应该始终至少有三个主节点。由于主节点用于管理整个集群,因此我们也在讨论管理平面。

主节点使用etcd集群作为其后端存储。将负载均衡器LB)放在主节点前面,并使用一个知名的完全限定域名FQDN),如admin.example.com,是一种良好的实践。所有用于管理 Kubernetes 集群的工具应该通过这个负载均衡器访问,而不是直接使用其中一个主节点的公共 IP 地址。这在前面图的左上方有展示。

在图的底部,我们有一个工作节点集群。节点的数量可以少到一个,并且没有上限。

Kubernetes 主节点和工作节点彼此通信。这是一种双向通信方式,不同于我们在 Docker Swarm 中看到的那种通信方式。在 Docker Swarm 中,只有管理节点与工作节点通信,而不会有反向通信。所有访问集群中运行的应用程序的入口流量都应该通过另一个负载均衡器。

这是应用程序负载均衡器或反向代理。我们永远不希望外部流量直接访问任何工作节点。

现在我们对 Kubernetes 集群的高层架构有了一个大致的了解,让我们更深入地探讨 Kubernetes 主节点和工作节点。

Kubernetes 主节点

Kubernetes 主节点用于管理 Kubernetes 集群。以下是这样的主节点的高层次图示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.2 – Kubernetes 主节点

在前面的图的底部,我们有基础设施,它可以是本地或云端的虚拟机,或者本地或云端的服务器(通常称为裸金属)。

目前,Kubernetes 主节点仅在 Linux 上运行。支持最流行的 Linux 发行版,如 RHEL、CentOS 和 Ubuntu。在这台 Linux 机器上,我们至少有以下四个 Kubernetes 服务在运行:

  • kubectl用于管理集群和集群中的应用程序。

  • 控制器:控制器,或者更准确地说是控制器管理器,是一个控制循环,它通过 API 服务器观察集群的状态并进行更改,尝试将当前状态或有效状态调整为所需状态,如果它们不同的话。

  • 调度器:调度器是一项服务,它尽力在工作节点上调度 Pods,同时考虑各种边界条件,如资源需求、策略、服务质量要求等。

  • 用于存储集群状态所有信息的etcd。更准确地说,作为集群存储的etcd不一定需要与其他 Kubernetes 服务安装在同一个节点上。有时,Kubernetes 集群配置为使用独立的etcd服务器集群,如图 16.1所示。但选择使用哪种变体是一个高级管理决策,超出了本书的范围。

我们至少需要一个主节点,但为了实现高可用性,我们需要三个或更多主节点。这与我们在学习 Docker Swarm 的管理节点时学到的非常相似。在这方面,Kubernetes 主节点相当于 Swarm 管理节点。

Kubernetes 主节点从不运行应用工作负载。它们的唯一目的是管理集群。Kubernetes 主节点构建了一个 Raft 共识组。Raft 协议是一种标准协议,通常用于一组成员需要做出决策的场景。它被许多知名的软件产品所使用,如 MongoDB、Docker SwarmKit 和 Kubernetes。有关 Raft 协议的更详细讨论,请参见进一步阅读部分中的链接。

在主节点上运行工作负载

有时,特别是在开发和测试场景中,使用单节点 Kubernetes 集群是有意义的,这样它自然就成了主节点和工作节点。但这种场景应该避免在生产环境中使用。

如前所述,Kubernetes 集群的状态存储在etcd节点中。如果 Kubernetes 集群需要高可用性,则etcd节点也必须配置为 HA 模式,通常意味着我们至少在不同的节点上运行三个etcd实例。

我们再一次声明,整个集群状态存储在etcd节点中。这包括所有关于集群节点的信息、所有 ReplicaSets、Deployments、Secrets、网络策略、路由信息等。因此,拥有一个强大的备份策略来保护这个键值存储至关重要。

现在,让我们看看将实际运行集群工作负载的节点。

集群节点

集群节点是 Kubernetes 调度应用工作负载的节点。它们是集群的工作马。一个 Kubernetes 集群可以有几个、几十个、几百个,甚至几千个集群节点。Kubernetes 从一开始就为高扩展性而构建。别忘了,Kubernetes 是以 Google Borg 为模型构建的,后者已经运行了数万个容器多年:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.3 – Kubernetes 工作节点

工作节点——它是集群节点,就像主节点一样——可以在虚拟机、裸金属、内部部署或云中运行。最初,工作节点只能配置在 Linux 上。但自 Kubernetes 1.10 版本以来,工作节点也可以在 Windows Server 2010 或更高版本上运行。拥有包含 Linux 和 Windows 工作节点的混合集群是完全可以接受的。

在每个节点上,我们需要运行以下三项服务:

  • YAMLJSON 格式,它们声明性地描述了一个 Pod。我们将在下一部分了解 Pod 是什么。PodSpecs 主要通过 API 服务器提供给 Kubelet。

  • 从版本 1.9 起,containerd 被用作容器运行时。在此之前,它使用的是 Docker 守护进程。还可以使用其他容器运行时,如 rktCRI-O。容器运行时负责管理和运行 Pod 中的各个容器。

  • kube-proxy:最后是 kube-proxy。它作为一个守护进程运行,是一个简单的网络代理和负载均衡器,用于所有在该节点上运行的应用服务。

现在我们已经了解了 Kubernetes 的架构以及主节点和工作节点,接下来是介绍我们可以用来开发针对 Kubernetes 应用的工具。

Play with Kubernetes 介绍

Play with Kubernetes 是一个由 Docker 赞助的免费沙盒,用户可以在其中学习如何使用 Docker 容器并将其部署到 Kubernetes:

  1. 访问 labs.play-with-k8s.com/

  2. 使用您的 GitHub 或 Docker 凭据登录。

  3. 成功登录后,通过点击屏幕左侧的**+ 添加新实例**按钮,创建第一个集群节点或实例。

  4. 按照屏幕上的指示创建您的 Kubernetes 沙盒集群的第一个主节点。

  5. 使用终端窗口中步骤 1中指示的命令初始化集群主节点。最好直接从那里复制命令。命令应如下所示:

    $ kubeadm init --apiserver-advertise-address \   $(hostname -i) --pod-network-cidr 10.5.0.0/16
    

第一个命令参数使用主机名称来广告 Kubernetes API 服务器的地址,第二个命令定义了集群应使用的子网。

  1. 接下来,如控制台中的步骤 2 所示,在我们的 Kubernetes 集群中初始化网络(注意,以下命令应为单行):

    $ kubectl apply -f https://raw.githubusercontent.com/cloudnativelabs/kube-router/master/daemonset/kubeadm-kuberouter.yaml
    
  2. 通过再次点击添加新实例按钮,创建第二个集群节点。

  3. 一旦节点准备就绪,运行在步骤 4中输出的join命令,其中 <token-1><token-2> 是特定于您集群的:

    $ kubeadm join 192.168.0.13:6443 --token <token-1> \>     --discovery-token-ca-cert-hash <token-2>
    

最好直接从 Play with Kubernetes 中的命令行复制正确的命令。

  1. 一旦第二个节点加入集群,请在第一个节点上运行以下命令,该节点是您初始化集群的地方,用于列出新集群中的节点集合:

    $ kubectl get nodes
    

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

NAME    STATUS   ROLES                  AGE     VERSIONnode1   Ready    control-plane,master   6m28s   v1.20.1
node2   Ready    <none>                 32s     v1.20.1

请注意,撰写本文时,Play with Kubernetes 使用的是 Kubernetes 1.20.1 版本,这个版本现在已经比较旧了。目前可用的最新稳定版本是 1.27.x。但不用担心,我们示例使用的 1.20.x 版本已经足够。

现在,让我们尝试在这个集群上部署一个 pod。暂时不用担心 pod 是什么,我们将在本章后面详细讲解。此时,只需要按现状理解即可。

  1. 在你的章节代码文件夹中,创建一个名为 sample-pod.yaml 的新文件,并添加以下内容:

    apiVersion: v1kind: Podmetadata:  name: nginx  labels:    app: nginxspec:  containers:  - name: nginx    image: nginx:alpine    ports:    - containerPort: 80    - containerPort: 443
    
  2. 现在,为了在 Play with Kubernetes 上运行前述的 pod,我们需要复制前面的 yaml 文件内容,并在我们集群的 node1 上创建一个新文件:

  3. 使用 vi 创建一个名为 sample-pod.yaml 的新文件。

  4. 按下 I(字母 “i”)进入 vi 编辑器的插入模式。

  5. 将复制的代码片段用 Ctrl + V(或在 Mac 上使用 Command + V)粘贴到此文件中。

  6. 按下 Esc 键进入 vi 的命令模式。

  7. 输入 :wq 并按 Enter 键保存文件并退出 vi

提示

为什么在示例中使用 Vi 编辑器?它是任何 Linux(或 Unix)发行版中都已安装的编辑器,因此始终可用。你可以在这里找到 Vi 编辑器的快速教程:www.tutorialspoint.com/unix/unix-vi-editor.htm

  1. 现在让我们使用名为 kubectl 的 Kubernetes CLI 来部署这个 pod。kubectl CLI 已经安装在你 Play with Kubernetes 集群的每个节点上:

    $ kubectl create -f sample-pod.yaml
    

这样做会产生以下输出:

pod/nginx created
  1. 现在列出所有的 pods:

    $ kubectl get pods
    

我们应该看到以下内容:

NAME      READY   STATUS    RESTARTS  AGEnginx     1/1     Running      0      51s
  1. 为了能够访问这个 pod,我们需要创建一个 Service。让我们使用 sample-service.yaml 文件,其中包含以下内容:

    apiVersion: v1kind: Servicemetadata:  name: nginx-servicespec:  type: NodePort  selector:    app: nginx  ports:  - name: nginx-port    protocol: TCP    port: 80    targetPort: http-web-svc
    

再次提醒,不必担心此时Service究竟是什么,我们稍后会解释。

  1. 让我们创建这个 Service:

    $ kubectl create -f sample-service.yaml
    
  2. 现在让我们看看 Kubernetes 创建了什么,并列出集群上定义的所有服务:

    $ kubectl get services
    

我们应该看到类似这样的内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.4 – 服务列表

请注意 PORT(S) 列。在我的情况下,Kubernetes 将 Nginx 的 80 容器端口映射到 31384 节点端口。我们将在下一条命令中使用这个端口。确保你使用的是系统上分配的端口号!

  1. 现在,我们可以使用 curl 访问该服务:

    $ curl -4 http://localhost:31384
    

我们应该收到 Nginx 欢迎页面作为回应。

  1. 在继续之前,请删除你刚才创建的两个对象:

    $ kubectl delete po/nginx$ kubectl delete svc/nginx-service
    

请注意,在前述命令中,po 快捷方式相当于 podpodskubectl 工具非常灵活,允许使用这样的缩写。同样,svcserviceservices 的缩写。

在接下来的部分,我们将使用 Docker Desktop 及其对 Kubernetes 的支持,运行与本部分相同的 pod 和服务。

Docker Desktop 中的 Kubernetes 支持

从 18.01-ce 版本开始,Docker Desktop 开始支持开箱即用的 Kubernetes。开发人员如果希望将容器化应用程序部署到 Kubernetes 中,可以使用这个编排工具,而不是 SwarmKit。Kubernetes 支持默认是关闭的,需要在设置中启用。第一次启用 Kubernetes 时,Docker Desktop 需要一些时间来下载创建单节点 Kubernetes 集群所需的所有组件。与 minikube(它也是单节点集群)不同,Docker 工具提供的版本使用了所有 Kubernetes 组件的容器化版本:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.5 – Docker Desktop 中的 Kubernetes 支持

上述图表大致展示了 Kubernetes 支持是如何被添加到 Docker Desktop 中的。macOS 上的 Docker Desktop 使用 hyperkit 来运行基于 LinuxKit 的虚拟机。Windows 上的 Docker Desktop 使用 Hyper-V 来实现这一结果。在虚拟机内部,安装了 Docker 引擎。引擎的一部分是 SwarmKit,它启用了 Swarm 模式。Docker Desktop 使用 kubeadm 工具在虚拟机中设置和配置 Kubernetes。以下三个事实值得一提:Kubernetes 将其集群状态存储在 etcd 中;因此,我们在这个虚拟机上运行了 etcd。接着,我们有构成 Kubernetes 的所有服务,最后,还有一些支持从 Docker CLI 部署 Docker 堆栈到 Kubernetes 的服务。这个服务不是 Kubernetes 官方发行版的一部分,但它是特定于 Docker 的。

所有 Kubernetes 组件都在 LinuxKit 虚拟机中的容器中运行。这些容器可以通过 Docker Desktop 中的设置进行隐藏。稍后我们将在本节中提供一份完整的 Kubernetes 系统容器列表,前提是你已启用 Kubernetes 支持。

启用 Kubernetes 的 Docker Desktop 相对于 minikube 的一个大优势是,前者允许开发人员使用单一工具构建、测试和运行面向 Kubernetes 的容器化应用程序。甚至可以使用 Docker Compose 文件将多服务应用部署到 Kubernetes 中。

现在让我们动手操作:

  1. 首先,我们需要启用 Kubernetes。在 macOS 上,点击菜单栏中的 Docker 图标。在 Windows 上,前往任务栏并选择首选项。在弹出的对话框中,选择Kubernetes,如以下截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.6 – 在 Docker Desktop 中启用 Kubernetes

  1. 然后,勾选启用 Kubernetes复选框。还需要勾选**显示系统容器(**高级)**复选框。

  2. 然后,点击应用并重启按钮。安装和配置 Kubernetes 需要几分钟时间。是时候休息一下,享受一杯好茶了。

  3. 安装完成后(Docker 会通过在 kubectl 中显示绿色状态图标来通知我们),以便访问后者。

  4. 首先,让我们列出我们拥有的所有上下文。我们可以使用以下命令来完成:

    $ kubectl config get-contexts
    

在作者的笔记本电脑上,我们得到以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.7 - kubectl 的上下文列表

在这里,我们可以看到,在作者的笔记本电脑上,我们有三个上下文,其中两个来自于他使用kind。目前,名为kind-demokind上下文仍然处于活动状态,通过CURRENT列中的星号标记。

  1. 我们可以使用以下命令切换到docker-desktop上下文:

    $ kubectl config use-context docker-desktop
    

执行此操作后,会得到以下输出:

Switched to context "docker-desktop"
  1. 现在我们可以使用kubectl访问 Docker Desktop 刚创建的集群:

    $ kubectl get nodes
    

我们应该看到类似以下的内容:

NAME    STATUS   ROLES             AGE      VERSIONnode1     Ready    control-plane  6m28s   v1.25.9

好的,这看起来很熟悉。它与我们在使用 Play with Kubernetes 时看到的几乎相同。作者的 Docker Desktop 使用的 Kubernetes 版本是 1.25.9。我们还可以看到节点是一个master节点,由control-plane角色指示。

  1. 如果我们列出当前在 Docker Desktop 上运行的所有容器,我们会得到以下截图所示的列表(注意,我们使用了--format参数来输出容器 ID 和容器名称):

    $ docker container list --format "table {{.ID}\t{{.Names}}"
    

这将导致以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.8 - Kubernetes 系统容器列表

在前面的列表中,我们可以识别出所有组成 Kubernetes 的现在熟悉的组件,如下所示:

  • API 服务器

  • etcd

  • kube-proxy

  • DNS 服务

  • kube-controller

  • kube-scheduler

通常,我们不希望将这些系统容器混入我们的容器列表中。因此,我们可以在 Kubernetes 的设置中取消选中**显示系统容器(高级)**复选框。

现在,让我们尝试将 Docker Compose 应用程序部署到 Kubernetes。

  1. 进入我们~/``The-Ultimate-Docker-Container-Book文件夹的ch16子文件夹。

  2. docker-compose.yml文件从示例解决方案复制到此位置:

    $ cp ../sample-solutions/ch16/docker-compose.yml .
    
  3. 按照 https://kompose.io/installation/上的说明,在你的机器上安装kompose工具:

    • 在 Mac 上,可以通过$ brew install kompose安装

    • 在 Windows 上,使用$ choco install kubernetes-kompose

  4. 按照以下方式运行kompose工具:

    $ kompose convert
    

该工具应该创建四个文件:

  • db-deployment.yaml

  • pets-data-persistentvolumeclaim.yaml

  • web-deployment.yaml

  • web-service.yaml

  1. 打开web-service.yaml文件,在第 11 行(spec条目)后,添加NodePort条目类型,使其如下所示:

    ...spec:  type: NodePort  ports:    - name: "3000"...
    
  2. 现在我们可以使用kubectl将这四个资源部署到我们的 Kubernetes 集群:

    $ kubectl apply –f '*.yaml'
    

我们应该看到这个:

deployment.apps/db createdpersistentvolumeclaim/pets-data created
deployment.apps/web created
service/web created
  1. 我们需要找出 Kubernetes 将3000服务端口映射到哪个主机端口。使用以下命令来实现:

    $ kubectl get service
    

你应该看到类似以下内容:

NAME       TYPE      CLUSTER-IP   EXTERNAL-IP PORT(S)       AGEkubernetes ClusterIP 10.96.0.1    <none>      443/TCP       10d
web        NodePort  0.111.98.154 <none>   3000:32134/TCP   5m33s

在我的例子中,我们可以看到服务 web 将 3000 端口映射到 32134 主机(或节点)端口。在下面的命令中,我必须使用这个端口。在你的情况下,端口号可能会不同。使用你从上一条命令中得到的数字!

  1. 我们可以使用 curl 测试应用程序:

    $ curl localhost:32134/pet
    

我们将看到它按预期运行:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.9 – 宠物应用程序在 Docker Desktop 上的 Kubernetes 环境中运行

现在,让我们看看在前面的部署之后,Kubernetes 上到底有哪些资源。

  1. 我们可以使用 kubectl 来查看:

    $ kubectl get all
    

这给我们带来了以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.10 – 列出 Docker stack deploy 创建的所有 Kubernetes 对象

Docker 为 web 服务和 db 服务创建了一个 Deployment。它还自动为 web 创建了一个 Kubernetes 服务,以便在集群内访问。

这可以说相当酷,极大地减少了面向 Kubernetes 作为编排平台的团队在开发过程中遇到的摩擦。

  1. 在继续之前,请从集群中删除该堆栈:

    $ kubectl delete –f '*.yaml'
    

现在,我们已经了解了可以用来开发最终将在 Kubernetes 集群中运行的应用程序的工具,接下来是时候了解所有重要的 Kubernetes 对象,这些对象用于定义和管理这样的应用程序。我们将从 pod 开始。

Pod 介绍

与 Docker Swarm 中的可能性相反,你不能直接在 Kubernetes 集群中运行容器。在 Kubernetes 集群中,你只能运行 pod。Pod 是 Kubernetes 中 Deployment 的基本单元。一个 pod 是一个或多个共址容器的抽象,这些容器共享相同的内核命名空间,例如网络命名空间。Docker SwarmKit 中没有类似的概念。多个容器可以共址并共享相同的网络命名空间是一个非常强大的概念。下图展示了两个 pod:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.11 – Kubernetes pods

在前面的图示中,我们有两个 pod,10.0.12.310.0.12.5。这两个 pod 都是由 Kubernetes 网络驱动管理的私有子网的一部分。

一个 pod 可以包含一个或多个容器。所有这些容器共享相同的 Linux 内核命名空间,特别是它们共享网络命名空间。这一点通过围绕容器的虚线矩形表示。由于在同一个 pod 中运行的所有容器共享网络命名空间,每个容器需要确保使用自己的端口,因为在一个网络命名空间中不允许重复端口。在这种情况下,在 Pod 1 中,主容器使用的是 80 端口,而辅助容器使用的是 3000 端口。

来自其他 Pod 或节点的请求可以使用 Pod 的 IP 地址结合相应的端口号来访问单个容器。例如,你可以通过 10.0.12.3:80 访问运行在 Pod 1 主容器中的应用程序。

比较 Docker 容器和 Kubernetes Pod 网络

现在,让我们比较 Docker 的容器网络与 Kubernetes 的 Pod 网络。在下面的图示中,左边是 Docker,右边是 Kubernetes:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.12 – Pod 中共享同一网络命名空间的容器

当创建 Docker 容器且未指定特定网络时,Docker Engine 会创建一个虚拟以太网(veth)端点。第一个容器获得 veth0,下一个获得 veth1,以此类推。这些虚拟以太网端点连接到 Docker 在安装时自动创建的 Linux 桥接器 docker0。流量从 docker0 桥接器路由到每个连接的 veth 端点。每个容器都有自己的网络命名空间。没有两个容器使用相同的命名空间。这是故意的,目的是将容器内运行的应用程序彼此隔离。

对于 Kubernetes Pod,情况则不同。当创建一个新 Pod 时,Kubernetes 首先创建一个所谓的 pause 容器,其目的是创建和管理 Pod 将与所有容器共享的命名空间。除此之外,它没有做任何实际的工作;它只是处于休眠状态。pause 容器通过 veth0 连接到 docker0 桥接器。任何后续加入 Pod 的容器都会使用 Docker Engine 的特殊功能,允许它重用现有的网络命名空间。实现的语法如下所示:

$ docker container create --net container:pause ...

重要部分是 --net 参数,其值为 container:<container name>。如果我们以这种方式创建一个新容器,那么 Docker 不会创建一个新的 veth 端点;该容器将使用与暂停容器相同的 veth 端点。

多个容器共享相同网络命名空间的另一个重要后果是它们相互通信的方式。我们来考虑以下情况:一个 Pod 中包含两个容器,一个监听 80 端口,另一个监听 3000 端口:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.13 – Pod 中的容器通过 localhost 进行通信

当两个容器使用相同的 Linux 内核网络命名空间时,它们可以通过 localhost 相互通信,类似于当两个进程在同一主机上运行时,它们也可以通过 localhost 进行通信。

这一点在前面的图示中得到了说明。从 main 容器中,容器化的应用程序可以通过 http://localhost:3000 访问支持容器内运行的服务。

共享网络命名空间

在了解了这些理论之后,你可能会想知道 Kubernetes 实际上是如何创建一个 Pod 的。

Kubernetes 仅使用 Docker 提供的功能。那么,这种网络命名空间共享是如何工作的呢?首先,Kubernetes 创建了前面提到的所谓 pause 容器。

这个容器的唯一作用就是为该 Pod 保留内核命名空间,并保持它们的存活,即使 Pod 内没有其他容器运行。接下来,让我们模拟创建一个 Pod。我们从创建 pause 容器开始,并使用 Nginx 来实现:

$ docker container run –detach \    --name pause nginx:alpine

现在我们添加第二个容器,命名为 main,并将其连接到与 pause 容器相同的网络命名空间:

$ docker container run --name main \ -d -it \    --net container:pause \
    alpine:latest ash

由于 pause 和示例 containers 都是同一网络命名空间的一部分,它们可以通过 localhost 互相访问。为了证明这一点,我们必须进入主容器执行操作:

$ docker exec -it main /bin/sh

现在我们可以测试连接到运行在 pause 容器中并监听 80 端口的 Nginx。使用 wget 工具进行测试时,我们会得到如下结果:

/ # wget -qO – localhost

这样做会给我们以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.14 – 两个容器共享相同的网络命名空间

输出结果显示我们确实可以在 localhost 上访问 Nginx。这证明了这两个容器共享相同的命名空间。如果这还不够,我们可以使用 ip 工具在两个容器中显示 eth0,并且会得到完全相同的结果,具体来说,就是相同的 IP 地址,这是 Pod 的一个特征:所有容器共享相同的 IP 地址:

/ # ip a show eth0

这将显示以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.15 – 使用 ip 工具显示 eth0 的属性

我们使用以下命令检查 bridge 网络:

$ docker network inspect bridge

之后,我们可以看到只列出了 pause 容器:

[    {
        "Name": "bridge",
        "Id": "c7c30ad64...",
        "Created": "2023-05-18T08:22:42.054696Z",
        "Scope": "local",
        "Driver": "bridge",
…
        "Containers": {
            "b7be6946a9b...": {
                "Name": "pause",
                "EndpointID": "48967fbec...",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
...
     }
]

上面的输出已被简化以提高可读性。

由于 main 容器复用了 pause 容器的端点,因此它没有在 Containers 列表中出现。

在继续之前,请删除两个 pausemain 容器:

$ docker container rm pause main

接下来,我们将讨论 Pod 的生命周期。

Pod 生命周期

本书前面提到过,容器有生命周期。容器首先初始化,运行,然后最终退出。当容器退出时,它可以通过退出代码零优雅地退出,或者通过非零退出代码终止,后者相当于发生了错误。

同样,Pod 也有生命周期。由于一个 Pod 可以包含多个容器,因此其生命周期比单一容器的生命周期稍微复杂一些。Pod 的生命周期可以在下图中看到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.16 – Kubernetes Pod 的生命周期

当 Pod 在集群节点上创建时,它首先进入 待处理 状态。一旦 Pod 的所有容器都启动并运行,Pod 就进入 运行中 状态。只有当所有容器成功运行时,Pod 才会进入此状态。如果要求 Pod 终止,它将请求所有容器终止。如果所有容器以退出代码零终止,则 Pod 进入 成功 状态。这是理想路径。

现在,让我们看看一些导致 Pod 处于 失败 状态的场景。可能有三种情况:

  • 如果在 Pod 启动期间,至少有一个容器无法运行并失败(即它退出时返回非零退出代码),则 Pod 会从待处理状态转入失败状态。

  • 如果 Pod 处于 运行中 状态,而其中一个容器突然崩溃或以非零退出代码退出,则 Pod 将从 运行中 状态转换为 失败 状态。

  • 如果要求 Pod 终止,并且在关闭过程中,至少有一个容器以非零退出代码退出,则 Pod 也会进入 失败 状态。

现在让我们看看 Pod 的规范。

Pod 规范

在 Kubernetes 集群中创建 Pod 时,我们可以使用命令式或声明式的方法。我们在本书前面已经讨论过这两种方法的区别,但为了重新表述最重要的方面,使用声明式方法意味着我们编写一个描述我们想要实现的最终状态的清单。我们将省略协调器的细节。我们想要实现的最终状态也被称为期望状态。一般来说,声明式方法在所有成熟的协调器中都受到强烈推荐,Kubernetes 也不例外。

因此,在本章中,我们将专注于声明式方法。Pod 的清单或规范可以使用 YAMLJSON 格式编写。在本章中,我们将专注于 YAML,因为它对我们人类来说更易于阅读。让我们来看一个示例规范。以下是 pod.yaml 文件的内容,该文件可以在我们 labs 文件夹的 ch16 子文件夹中找到:

apiVersion: v1kind: Pod
metadata:
  name: web-pod
spec:
  containers:
  - name: web
    image: nginx:alpine
    ports:
    - containerPort: 80

Kubernetes 中的每个规范都以版本信息开头。Pod 已经存在了一段时间,因此 API 版本是 v1。第二行指定了我们想要定义的 Kubernetes 对象或资源类型。显然,在这个例子中,我们想要指定一个 pod。接下来是一个包含元数据的块。最基本的要求是给 pod 起个名字。这里我们称其为 web-pod。接下来的块是 spec 块,包含 pod 的规范。最重要的部分(也是这个简单示例中唯一的部分)是列出所有属于该 pod 的容器。我们这里只有一个容器,但也可以有多个容器。我们为容器选择的名字是 web,容器镜像是 nginx:alpine。最后,我们定义了容器暴露的端口列表。

一旦我们编写了这样的规范,就可以使用 Kubernetes CLI kubectl 将其应用到集群中:

  1. 打开一个新的终端窗口,导航到 ch16 子文件夹:

    $ cd ~/The-Ultimate-Docker-Contianer-Book/ch16
    
  2. 在这个示例中,我们将使用 Docker Desktop 的 Kubernetes 集群。因此,确保你正在使用正确的 kubectl CLI 上下文:

    $ kubectl config use-context docker-desktop
    

这将切换上下文到由 Docker Desktop 提供的 Kubernetes 集群。

  1. 在此文件夹中,创建一个名为 pod.yml 的新文件,并将提到的 pod 规范添加到该文件中。保存该文件。

  2. 执行以下命令:

    $ kubectl create -f pod.yaml
    

这将回应 pod "web-pod" created

  1. 然后我们可以列出集群中的所有 pod:

    $ kubectl get pods
    

这样做将为我们提供以下输出:

NAME          READY      STATUS     RESTARTS     AGEweb-pod       1/1        Running        0        2m

正如预期的那样,我们有一个处于 Running 状态的 pod,名称为 web-pod,正如定义的那样。

  1. 我们可以通过使用 describe 命令来获取有关运行中的 pod 的更详细信息:

    $ kubectl describe pod/web-pod
    

这会给我们类似这样的输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.17 – 描述运行在集群中的 pod

注意

前面部分的 pod/web-pod 表示法包括 describe 命令。其他变体也是可能的。例如,pods/web-podpo/web-podpodpo 都是 pods 的别名。

kubectl 工具定义了许多别名,以使我们的生活更加轻松。

describe 命令为我们提供了大量有关 pod 的有价值的信息,其中之一是发生并影响该 pod 的事件列表。该列表会显示在输出的最后。

Containers 部分中的信息与我们在 docker container inspect 输出中找到的非常相似。

我们还可以看到一个 Volumes 部分,其中有一个 Projected 条目类型。它包含集群的根证书作为机密。我们将在下一章讨论 Kubernetes 的机密。另一方面,卷将在接下来讨论。

Pod 和卷

在关于容器的章节中,我们了解了卷及其作用:访问和存储持久数据。由于容器可以挂载卷,因此 pod 也可以。实际上,真正挂载卷的是 pod 中的容器,但这只是一个语义上的细节。首先,让我们看看如何在 Kubernetes 中定义一个卷。Kubernetes 支持各种卷类型,因此我们不会深入探讨这个话题。

让我们通过定义一个名为my-data-claimPersistentVolumeClaim声明,隐式地创建一个本地卷:

  1. 创建一个名为volume-claim.yaml的文件,并将以下规范添加到文件中:

    apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: my-data-claimspec:  accessModes:  - ReadWriteOnce  resources:    requests:      storage: 2Gi
    

我们定义了一个请求 2GB 数据的声明。

  1. 让我们创建这个声明:

    $ kubectl create -f volume-claim.yaml
    

这将产生如下输出:

persistentvolumeclaim/my-data-claim created
  1. 我们可以使用kubectl列出声明(pvcPersistentVolumeClaim的快捷方式),命令如下:

    $ kubectl get pvc
    

这将产生如下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.18 – 集群中 PersistentStorageClaim 对象的列表

在输出中,我们可以看到该声明已经隐式地创建了一个名为pvc-<ID>的卷。

  1. 在继续之前,请先移除该 pod:

    $ kubectl delete pod/web-pod
    

或者,使用定义 pod 的原始文件,命令如下:

$ kubectl delete -f pod.yaml

我们现在可以在 pod 中使用该声明创建的卷了。让我们使用之前使用的修改版 pod 规范:

  1. 创建一个名为pod-with-vol.yaml的文件,并将以下规范添加到文件中:

    apiVersion: v1kind: Podmetadata:  name: web-podspec:  containers:  - name: web    image: nginx:alpine    ports:    - containerPort: 80    volumeMounts:    - name: my-data      mountPath: /data  volumes:  - name: my-data    persistentVolumeClaim:      claimName: my-data-claim
    

在最后四行的volumes块中,我们定义了一个我们希望在该 pod 中使用的卷列表。我们在这里列出的卷可以被 pod 的任何容器使用。在我们的例子中,我们只有一个卷。我们指定了一个名为my-data的卷,它是一个持久卷声明,声明名称就是我们刚刚创建的那个。

然后,在容器规范中,我们有volumeMounts块,在这里我们定义了要使用的卷,以及容器内卷将挂载的(绝对)路径。在我们的例子中,我们将卷挂载到容器文件系统的/data文件夹。

  1. 让我们创建这个 pod:

    $ kubectl create -f pod-with-vol.yaml
    

我们也可以使用声明式的方式:

$ kubectl apply -f pod-with-vol.yaml
  1. 然后,我们可以exec进入容器,通过导航到/data文件夹,创建一个文件并使用以下命令退出容器,以检查卷是否已经挂载:

    $ kubectl exec -it web-pod -- /bin/sh/ # cd /data/data # echo "Hello world!" > sample.txt/data # exit
    

如果我们没错的话,那么这个容器中的数据应该在 pod 的生命周期结束后仍然存在。

  1. 因此,让我们删除这个 pod:

    $ kubectl delete pod/web-pod
    
  2. 然后,我们将重新创建它:

    $ kubectl create -f pod-with-vol.yaml
    
  3. 然后,我们将exec进入 pod 的容器:

    $ kubectl exec -it web-pod  -- ash
    
  4. 最后,我们输出数据:

    / # cat /data/sample.txt
    

这是前面命令产生的输出:

Hello world!

这是我们所预期的。

  1. Ctrl + D退出容器。

  2. 在继续之前,请删除 pod 和持久卷声明。到现在为止,你应该知道怎么做。如果不知道,请回头查看步骤 4

现在我们已经对 Pods 有了较好的理解,让我们研究一下 ReplicaSets 如何帮助管理这些 Pods。

Kubernetes ReplicaSets

在一个对高可用性有要求的环境中,仅有一个 Pod 是远远不够的。如果 Pod 崩溃了怎么办?如果我们需要更新 Pod 内部的应用程序,但又不能承受任何服务中断怎么办?这些问题表明仅有 Pods 是不足够的,我们需要一个更高级的概念来管理多个相同的 Pod 实例。在 Kubernetes 中,ReplicaSet 用于定义和管理在不同集群节点上运行的多个相同 Pod 的集合。ReplicaSet 定义了容器在 Pod 中运行时使用的容器镜像,以及在集群中运行的 Pod 实例数量等。这些属性以及其他许多属性被称为期望状态

ReplicaSet 负责始终确保实际状态与期望状态的一致性,如果实际状态偏离期望状态。以下是一个 Kubernetes ReplicaSet:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.19 – Kubernetes ReplicaSet

在前面的示意图中,我们可以看到一个 ReplicaSet 管理着多个 Pods。这些 Pods 被称为pod-api。ReplicaSet 负责确保在任何给定时间,始终有期望数量的 Pods 在运行。如果某个 Pod 因为某种原因崩溃,ReplicaSet 会在一个有空闲资源的节点上调度一个新的 Pod 替代它。如果 Pod 的数量超过了期望的数量,ReplicaSet 会杀死多余的 Pod。通过这种方式,我们可以说 ReplicaSet 保证了一个自愈且可扩展的 Pod 集合。ReplicaSet 可以包含的 Pod 数量没有上限。

ReplicaSet 规格

类似于我们对 Pods 的学习,Kubernetes 也允许我们以命令式或声明式的方式定义和创建 ReplicaSet。由于在大多数情况下声明式方法是最推荐的方式,我们将专注于这种方法。让我们来看看一个 Kubernetes ReplicaSet 的示例规格:

  1. 创建一个名为replicaset.yaml的新文件,并在其中添加以下内容:

    apiVersion: apps/v1kind: ReplicaSetmetadata:  name: rs-webspec:  selector:    matchLabels:      app: web  replicas: 3  template:    metadata:      labels:        app: web    spec:      containers:      - name: nginx        image: nginx:alpine        ports:        - containerPort: 80
    

这看起来与我们之前介绍的 Pod 规格非常相似。那么,我们来集中注意其区别。首先,在第 2 行,我们看到的是kind,之前是 Pod,现在是ReplicaSet。接着,在第 6 到第 8 行,我们有一个选择器,它决定哪些 Pods 将成为 ReplicaSet 的一部分。在这个例子中,它选择所有标签为app且值为web的 Pods。然后,在第 9 行,我们定义了希望运行的 Pod 副本数量;在这个例子中是三个副本。最后,我们有template部分,它首先定义了元数据,然后定义了规格,其中包含运行在 Pod 内部的容器。在我们的例子中,我们有一个使用nginx:alpine镜像并暴露80端口的单一容器。

其中真正重要的元素是副本数和选择器,选择器指定了由 ReplicaSet 管理的 Pod 集合。

  1. 让我们使用这个文件来创建 ReplicaSet:

    $ kubectl create -f replicaset.yaml
    

这将产生以下结果:

replicaset "rs-web" created
  1. 现在我们列出集群中所有的 ReplicaSets(rs是 ReplicaSet 的快捷方式):

    $ kubectl get rs
    

我们得到了以下结果:

NAME        DESIRED  CURRENT   READY    AGErs-web         3       3         3      51s

在前面的输出中,我们看到有一个名为rs-web的 ReplicaSet,其期望状态是三个(Pod)。当前状态也显示了三个 Pod,并告诉我们所有三个 Pod 都已准备就绪。

  1. 我们还可以列出系统中的所有 Pod:

    $ kubectl get pods
    

这将生成以下输出:

NAME                       READY   STATUS      RESTARTS   AGErs-web-nbc8m    1/1        Running   0                    4m
rs-web-6bxn5    1/1        Running   0                    4m
rs-web-lqhm5    1/1        Running   0                    4m

在这里,我们可以看到我们预期的三个 Pod。Pod 的名称使用了ReplicaSet的名称,并附加了一个唯一的 ID。在READY列中,我们可以看到 Pod 中定义了多少个容器以及它们中有多少个已准备就绪。在我们的案例中,每个 Pod 只有一个容器,并且每个容器都已准备好。因此,Pod 的整体状态是Running。我们还可以看到每个 Pod 被重启了多少次。在我们的例子中,没有任何 Pod 被重启。

接下来,让我们看看 ReplicaSet 是如何帮助我们实现自愈的。

自愈

现在,让我们通过随机杀死其中一个 Pod 来测试自愈 ReplicaSet 的魔力,并观察会发生什么:

  1. 让我们删除前面列表中的第一个 Pod。确保将 Pod 的名称(rs-web-nbc8m)替换为您自己示例中的名称:

    $ kubectl delete po/rs-web-nbc8m
    

上一个命令生成了以下输出:

pod "rs-web-nbc8m" deleted
  1. 现在,让我们再次列出所有 Pod。我们期望只看到两个 Pod,对吗?你错了:

    NAME                       READY   STATUS      RESTARTS   AGErs-web-4r587    1/1        Running   0                    5srs-web-6bxn5    1/1        Running   0                    4m30srs-web-lqhm5    1/1        Running   0                    4m30
    

好的,显然,列表中的第一个 Pod 已经被重新创建,正如我们从AGE列中看到的那样。这是自愈功能在起作用。

  1. 让我们看看描述 ReplicaSet 时会发现什么:

    $ kubectl describe rs
    

这将给我们以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.20 – 描述 ReplicaSet

结果,我们在Events下找到了一个条目,告诉我们 ReplicaSet 创建了一个名为rs-web-4r587的新 Pod。

  1. 在继续之前,请删除 ReplicaSet:

    $ kubectl delete rs/rs-web
    

现在是时候讨论 Kubernetes 的 Deployment 对象了。

Kubernetes 部署

Kubernetes 非常重视单一职责原则。所有 Kubernetes 对象都被设计为执行一项任务,而且只执行这一项任务,而且它们的设计目标是非常出色地完成这项任务。在这方面,我们必须理解 Kubernetes 的 ReplicaSets 和 Deployments。正如我们所学,ReplicaSet 负责实现和协调应用服务的期望状态。这意味着 ReplicaSet 管理一组 Pod。

**部署(Deployment)**通过在 ReplicaSet 基础上提供滚动更新和回滚功能来增强 ReplicaSet。在 Docker Swarm 中,Swarm 服务结合了 ReplicaSet 和 Deployment 的功能。从这个角度来看,SwarmKit 比 Kubernetes 更加单体化。以下图示展示了 Deployment 与 ReplicaSet 的关系:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.21 – Kubernetes 部署

在前面的图示中,ReplicaSet 定义并管理一组相同的 pods。ReplicaSet 的主要特点是自我修复、可扩展,并始终尽最大努力使其状态与期望状态一致。而 Kubernetes 部署(Deployment)则在此基础上增加了滚动更新和回滚功能。在这方面,Deployment 是 ReplicaSet 的封装对象。

我们将在 第十七章 中深入学习滚动更新和回滚,使用 Kubernetes 部署、更新和保护应用程序

在接下来的章节中,我们将深入了解 Kubernetes 服务以及它们如何实现服务发现和路由。

Kubernetes 服务

一旦我们开始处理由多个应用服务组成的应用程序,就需要服务发现。以下图示说明了这个问题:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.22 – 服务发现

在前面的图示中,我们有一个 Web API 服务,需要访问另外三个服务:paymentsshippingorderingWeb API 服务不应关心如何以及在哪里找到这三个服务。在 API 代码中,我们只需要使用我们想要访问的服务名称和其端口号。一个示例是以下 URL,payments:3000,它用于访问 payments 服务的一个实例。

在 Kubernetes 中,支付应用服务由一个 ReplicaSet 的 pods 表示。由于高度分布式系统的特性,我们不能假设 pods 拥有稳定的端点。pod 可以随时出现或消失。如果我们需要从内部或外部客户端访问相应的应用服务,这将是个问题。如果我们不能依赖 pod 端点的稳定性,我们还能做什么呢?

这就是 Kubernetes 服务 发挥作用的地方。它们旨在为 ReplicaSets 或 Deployments 提供稳定的端点,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.23 – Kubernetes 服务为客户端提供稳定的端点

在前面的图示中,我们可以看到一个 Kubernetes 服务。它提供了一个可靠的集群级 IP 地址,也叫做 app=web;也就是说,所有具有名为 app 且值为 web 的标签的 pod 都会被代理。

在接下来的章节中,我们将深入了解基于上下文的路由以及 Kubernetes 如何减轻这一任务。

基于上下文的路由

我们经常需要为 Kubernetes 集群配置基于上下文的路由。Kubernetes 提供了多种方式来实现这一点。目前,首选且最具可扩展性的方法是使用 IngressController。以下图示尝试说明这个 ingress 控制器是如何工作的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 16.24 – 使用 Kubernetes Ingress 控制器的基于上下文的路由

在前面的图中,我们可以看到当使用 IngressController(如 Nginx)时,基于上下文(或第七层)路由是如何工作的。在这里,我们有一个名为 web 的应用服务的部署。这个应用服务的所有 Pod 都有以下标签:app=web。然后,我们有一个名为 web 的 Kubernetes 服务,它为这些 Pod 提供一个稳定的端点。该服务的虚拟 IP 是 52.14.0.13,并且暴露了 30044 端口。也就是说,如果有请求到达 Kubernetes 集群的任何节点,并请求 web 名称和 30044 端口,那么这个请求会被转发到这个服务。然后,服务会将请求负载均衡到其中一个 Pod。

到目前为止,一切顺利,但如何将来自客户端的 ingress 请求路由到 http[s]://example.com/web URL 并定向到我们的 Web 服务呢?首先,我们必须定义从基于上下文的请求到相应的 <服务名>/<端口> 请求的路由。这是通过 Ingress 对象实现的:

  1. 在 Ingress 对象中,我们将 Host 和 Path 定义为源,(服务) 名称和端口为目标。当 Kubernetes API 服务器创建这个 Ingress 对象时,作为 sidecar 运行的 IngressController 进程会拾取这个变化。

  2. 修改 Nginx 反向代理的配置文件。

  3. 通过添加新路由,要求 Nginx 重新加载其配置,因此,它将能够正确地将任何传入的请求路由到 http[s]://example.com/web

在下一节中,我们将通过对比每种调度引擎的一些主要资源,来比较 Docker SwarmKit 和 Kubernetes。

比较 SwarmKit 和 Kubernetes

现在我们已经了解了 Kubernetes 中一些最重要资源的许多细节,接下来比较这两种调度器 SwarmKit 和 Kubernetes 时,通过匹配重要资源来帮助理解。让我们来看看:

SwarmKitKubernetes描述
Swarm集群由各自的调度器管理的服务器/节点集。
节点集群成员作为 Swarm/集群成员的单个主机(物理或虚拟)。
管理节点主节点管理 Swarm/集群的节点。这是控制平面。
工作节点节点运行应用工作负载的 Swarm/集群成员。
容器容器**运行在节点上的容器镜像实例。**注:在 Kubernetes 集群中,我们不能直接运行容器。
任务Pod运行在节点上的服务实例(Swarm)或副本集(Kubernetes)。一个任务管理一个容器,而一个 Pod 包含一个或多个容器,这些容器共享相同的网络命名空间。
服务副本集定义并协调由多个实例组成的应用服务的期望状态。
服务部署部署是带有滚动更新和回滚功能的 ReplicaSet。
路由网格服务Swarm 路由网格提供基于 IPVS 的 L4 路由和负载均衡。Kubernetes 服务是一个抽象,定义了一组逻辑上的 pods 和一种可用于访问它们的策略。它是一个稳定的端点,指向一组 pods
堆栈堆栈**应用程序的定义由多个(Swarm)服务组成。**注意:虽然堆栈在 Kubernetes 中并不是原生支持的,但 Docker 工具 Docker Desktop 会将它们转换为 Kubernetes 集群的部署
网络网络策略Swarm 软件定义网络SDNs)用于防火墙容器。Kubernetes 只定义了一个单一的扁平网络。除非显式定义网络策略来约束 pod 之间的通信,否则每个 pod 都可以访问其他 pod 和/或节点

这就结束了我们对 Kubernetes 的介绍,它目前是最流行的容器编排引擎。

总结

在本章中,我们学习了 Kubernetes 的基础知识。我们概述了其架构,并介绍了用于在 Kubernetes 集群中定义和运行应用程序的主要资源。我们还介绍了 minikube 和 Docker Desktop 中的 Kubernetes 支持。

在下一章中,我们将把应用程序部署到 Kubernetes 集群中。然后,我们将使用零停机策略更新该应用程序的某个服务。最后,我们将使用密钥对在 Kubernetes 中运行的应用程序服务进行敏感数据的加密。敬请期待!

进一步阅读

以下是包含有关我们在本章中讨论的各种主题的详细信息的文章列表:

问题

请回答以下问题以评估您的学习进度:

  1. Kubernetes 集群的高层次架构是什么?

  2. 用几句话简要解释 Kubernetes master 的角色。

  3. 列出每个 Kubernetes(工作节点)节点上需要具备的元素。

  4. 我们无法在 Kubernetes 集群中运行单独的容器。

    1. 正确

    2. 错误

  5. Kubernetes pod 的三个主要特性是什么?

  6. 解释为什么 pod 中的容器可以使用 localhost 相互通信。

  7. pod 中所谓的 pause 容器的作用是什么?

  8. Bob 告诉你:“我们的应用程序由三个 Docker 镜像组成:webinventorydb。由于我们可以在 Kubernetes pod 中运行多个容器,所以我们打算将应用程序的所有服务部署到一个 pod 中。”列出三到四个原因,解释为什么这是一个不好的主意。

  9. 用您自己的话解释为什么我们需要 Kubernetes ReplicaSets。

  10. 在什么情况下我们需要 Kubernetes Deployments?

  11. Kubernetes 服务的主要职责是什么?

  12. 列出至少三种 Kubernetes 服务类型,并解释它们的目的及其差异。

  13. 如何创建一个 Kubernetes 服务,将应用程序服务内部暴露在集群中?

答案

以下是本章中提出问题的一些示例答案:

  1. Kubernetes 集群由控制平面(Kubernetes Master)和多个工作节点组成。控制平面负责维持集群的期望状态,例如正在运行的应用程序和它们使用的容器镜像。工作节点是应用程序部署和运行的服务器。

  2. Kubernetes master 负责管理集群。所有创建对象、重新调度 Pod、管理 ReplicaSet 等请求都发生在 master 上。master 不在生产环境或类似生产环境的集群中运行应用程序工作负载。

  3. 在每个工作节点上,我们有 kubelet、代理和容器运行时。

  4. 答案是 A. 正确。你不能在 Kubernetes 集群上运行独立的容器。Pod 是该集群中部署的最小单元。

  5. Kubernetes Pod 是 Kubernetes 中最小的可部署单元。它可以运行一个或多个共址的容器。以下是三个主要特点:

    1. 一个 Pod 可以封装多个紧密耦合且需要共享资源的容器。

    2. Pod 中的所有容器共享相同的网络命名空间,这意味着它们可以使用 localhost 相互通信。

    3. 每个 Pod 在集群内都有一个独特的 IP 地址。

  6. 所有在 Pod 内运行的容器共享相同的 Linux 内核网络命名空间。因此,这些容器内运行的所有进程可以通过 localhost 互相通信,类似于在主机上直接运行的进程或应用程序如何通过 localhost 进行通信。

  7. pause 容器的唯一作用是为在 Pod 中运行的容器保留命名空间。

  8. 这是一个不好的想法,因为一个 Pod 的所有容器是共址的,这意味着它们运行在同一个集群节点上。而且,如果多个容器运行在同一个 Pod 中,它们只能一起扩展或缩减。然而,应用程序的不同组件(即 webinventorydb)通常在可扩展性或资源消耗方面有非常不同的需求。web 组件可能需要根据流量进行扩展和缩减,而 db 组件则有其他组件没有的存储特殊需求。如果我们将每个组件都运行在各自的 Pod 中,我们在这方面会更具灵活性。

  9. 我们需要一种机制来在集群中运行多个 Pod 实例,并确保实际运行的 Pod 数量始终与期望数量相符,即使个别 Pod 由于网络分区或集群节点故障而崩溃或消失。ReplicaSet 是提供任何应用程序服务可扩展性和自愈能力的机制。

  10. 当我们希望在 Kubernetes 集群中更新应用服务而不导致服务停机时,需要使用 Deployment 对象。Deployment 对象为 ReplicaSets 增加了滚动更新和回滚功能。

  11. Kubernetes 服务是一种抽象方式,用于将运行在一组 Pods 上的应用暴露为网络服务。Kubernetes 服务的主要职责包括以下几点:

    1. 为一组 Pods 提供稳定的 IP 地址和 DNS 名称,帮助发现服务,并支持负载均衡。

    2. 路由网络流量,将其分发到一组 Pods 上,从而提供相同的功能。

    3. 如有必要,允许将服务暴露给外部客户端。

  12. Kubernetes 服务对象用于使应用服务参与服务发现。它们为一组 Pods 提供稳定的端点(通常由 ReplicaSet 或 Deployment 管理)。Kubernetes 服务是定义逻辑 Pods 集合和访问策略的抽象。Kubernetes 服务有四种类型:

    • 每个集群节点上的 3000032767

    • LoadBalancer:此类型通过云服务提供商的负载均衡器(如 AWS 上的 ELB)将应用服务暴露到外部。

    • ExternalName:当需要为集群的外部服务(例如数据库)定义代理时使用。

  13. 创建 Kubernetes 服务时,通常会创建一个服务配置文件(YAMLJSON),该文件指定所需的服务类型(例如,ClusterIP 用于内部通信),以及选择器标签以识别目标 Pods 和网络流量的端口。然后使用 kubectl apply 命令应用此文件。这将创建一个服务,将流量路由到匹配选择器标签的 Pods。

第十七章:17

使用 Kubernetes 部署、更新和保护应用

在上一章中,我们学习了关于容器编排器 Kubernetes 的基础知识。我们对 Kubernetes 的架构进行了概览,并了解了 Kubernetes 用来定义和管理容器化应用的许多重要对象。

在本章中,我们将学习如何将应用程序部署、更新和扩展到 Kubernetes 集群中。我们还将解释如何实现零停机部署,以便无干扰地更新和回滚关键任务应用。最后,我们将介绍 Kubernetes 秘密,作为配置服务和保护敏感数据的一种手段。

本章涵盖以下主题:

  • 部署我们的第一个应用

  • 定义存活性和就绪性

  • 零停机部署

  • Kubernetes 秘密

完成本章后,你将能够完成以下任务:

  • 将一个多服务应用部署到 Kubernetes 集群中

  • 为你的 Kubernetes 应用服务定义存活探针和就绪探针

  • 更新在 Kubernetes 中运行的应用服务,而不会造成停机

  • 在 Kubernetes 集群中定义秘密

  • 配置应用服务以使用 Kubernetes 秘密

技术要求

在本章中,我们将使用本地计算机上的 Docker Desktop。有关如何安装和使用 Docker Desktop 的更多信息,请参阅 第二章设置工作环境

本章的代码可以在这里找到:main/sample-solutions/ch17

请确保你已经按照 第二章 中描述的方式克隆了本书的 GitHub 仓库。

在你的终端中,导航到 ~/The-Ultimate-Docker-Container-Book 文件夹,并创建一个名为 ch17 的子文件夹并进入它:

$ mkdir ch17 & cd ch17

部署我们的第一个应用

我们将把我们的宠物应用——我们在 第十一章 中首次介绍的,使用 Docker Compose 管理容器——部署到 Kubernetes 集群中。我们的集群将使用 Docker Desktop,它提供了一个单节点的 Kubernetes 集群。然而,从部署的角度来看,集群的规模和集群位于云端、公司数据中心或你的工作站并不重要。

部署 Web 组件

提醒一下,我们的应用程序由两个应用服务组成:基于 Node 的 Web 组件和后台 PostgreSQL 数据库。在上一章中,我们学习了需要为每个我们想要部署的应用服务定义一个 Kubernetes 部署对象。我们将首先为 Web 组件执行此操作。和本书中一贯的做法一样,我们将选择声明式方式来定义我们的对象:

  1. 我们将使用由 Docker Desktop 提供的本地 Kubernetes 单节点集群。确保你的 Docker Desktop 安装中已启用 Kubernetes:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.1 – 在 Docker Desktop 上运行 Kubernetes

  1. 在您的代码子文件夹(ch17)中,添加一个名为web-deployment.yaml的文件,内容如下:

图 17.2 – 组件的 Kubernetes 部署定义)

图 17.2 – web组件的 Kubernetes 部署定义

前面的部署定义可以在sample-solutions/ch17子文件夹中的web-deployment.yaml文件中找到。它包含了部署web组件所需的指令。代码行如下:

  • 第 7 行:我们将Deployment对象的名称定义为web

  • 第 9 行:我们声明希望运行一个web组件的实例。

  • 第 11 到 13 行:通过Selector,我们定义了哪些 Pods 将成为我们部署的一部分,即那些具有appservice标签,且值分别为petsweb的 Pods。

  • 第 14 行:在从第 11 行开始的 Pod 模板中,我们定义了每个 Pod 将应用appservice标签。

  • 从第 20 行开始:我们定义了将在 Pod 中运行的唯一容器。容器的镜像是我们熟悉的fundamentalsofdocker/ch11-web:2.0镜像,容器的名称将为web

  • 第 23 行和第 24 行:值得注意的是,我们声明容器将端口3000暴露给传入流量。

  1. 请确保您已将kubectl的上下文设置为 Docker Desktop。有关如何设置的详细信息,请参见第二章,《设置工作环境》。使用以下命令:

    $ kubectl config use-context docker-desktop
    

您将收到以下输出:

Switched to context "docker-desktop".
  1. 我们可以使用以下命令部署此Deployment对象:

    $ kubectl create -f web-deployment.yaml
    

前面的命令输出如下信息:

deployment.apps/web created
  1. 我们可以通过 Kubernetes CLI 再次确认该部署是否已创建:

    $ kubectl get all
    

我们应该看到以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.3 – 列出所有在 Kind 中运行的资源

在前面的输出中,我们可以看到 Kubernetes 创建了三个对象——部署(deployment)、相关的ReplicaSet,以及一个 Pod(记住我们指定了只需要一个副本)。当前状态与这三个对象的期望状态一致,所以到目前为止我们没问题。

  1. 现在,web 服务需要公开给外部访问。为此,我们需要定义一个 Kubernetes 类型为NodePortService对象。创建一个名为web-service.yaml的新文件,并向其中添加以下代码:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.4 – 我们的 web 组件的 Service 对象定义

再次提醒,相同的文件可以在sample-solutions/ch17子文件夹中的web-service.yaml文件中找到。

前面的代码行如下:

  • 第 7 行:我们将此Service对象的名称设置为web

  • 第 9 行:我们定义了使用的Service对象类型。由于web组件必须能够从集群外部访问,因此不能是ClusterIP类型的Service对象,必须是NodePortLoadBalancer类型。在前一章中我们讨论了 Kubernetes 服务的各种类型,因此这里不再详细说明。在我们的示例中,我们使用的是NodePort类型的服务。

  • 第 10 到 13 行:我们指定希望通过 TCP 协议暴露端口3000供访问。Kubernetes 会自动将容器端口3000映射到 30,000 到 32,768 范围内的一个空闲主机端口。Kubernetes 最终选择的端口可以通过在服务创建后使用kubectl get servicekubectl describe命令来确定。

  • 第 14 到 16 行:我们定义了此服务将作为稳定端点的 pods 的过滤条件。在这种情况下,它是所有具有appservice标签且值分别为petsweb的 pods。

  1. 现在我们已经有了Service对象的规格说明,我们可以使用kubectl来创建它:

    $ kubectl apply -f web-service.yaml
    
  2. 我们可以列出所有服务,以查看前面命令的结果:

    $ kubectl get services
    

上述命令会产生以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.5 – 为 Web 组件创建的 Service 对象

在前面的输出中,我们可以看到一个名为web的服务已被创建。该服务被分配了一个唯一的ClusterIP10.96.195.255,并且容器端口3000已在所有集群节点的端口30319上发布。

  1. 如果我们想测试这个部署,可以使用curl

    $ curl localhost:30319/
    

这将导致以下输出:

Pets Demo Application

正如我们所看到的,响应是Pets Demo Application,这是我们预期的结果。Web 服务已在 Kubernetes 集群中启动并运行。接下来,我们将部署数据库。

部署数据库

数据库是一个有状态组件,必须与无状态组件(如我们的 Web 组件)不同对待。我们在第九章学习分布式应用架构》和第三章容器编排介绍》中详细讨论了分布式应用架构中有状态和无状态组件的区别。

Kubernetes 为有状态组件定义了一种特殊类型的ReplicaSet对象,这种对象叫做StatefulSet。我们使用这种类型的对象来部署数据库。

  1. 创建一个名为db-stateful-set.yaml的新文件,并将以下内容添加到该文件中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.6 – 用于 DB 组件的 StatefulSet 对象

定义也可以在sample-solutions/ch17子文件夹中找到。

好的,看起来有点吓人,但其实不是。它比 web 组件的部署定义稍长,因为我们还需要定义一个卷,用于 PostgreSQL 数据库存储数据。卷索赔定义在第 25 至 33 行。

我们想要创建一个名为 pets-data 的卷,其最大大小为 100 MB。在第 22 至 24 行,我们使用此卷,并将其挂载到容器中的 /var/lib/postgresql/data,这是 PostgreSQL 期望的位置。在第 21 行,我们还声明 PostgreSQL 正在端口 5432 上监听。

  1. 像往常一样,我们使用 kubectl 部署我们的 StatefulSet

    $ kubectl apply -f db-stateful-set.yaml
    
  2. 现在,如果我们列出集群中的所有资源,我们将能够看到创建的额外对象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.7 – StatefulSet 及其 pod

在这里,我们可以看到已创建了 StatefulSet 和一个 pod。对于两者来说,当前状态与期望状态相符,因此系统是健康的,但这并不意味着此时 web 组件可以访问数据库。服务发现不起作用。请记住,web 组件希望使用 db 服务的名称来访问 db。我们在 server.js 文件中硬编码了 db 主机名。

  1. 为了使集群内的服务发现正常工作,我们还必须为数据库组件定义一个 Kubernetes Service 对象。由于数据库应仅能从集群内部访问,因此我们需要的 Service 对象类型是 ClusterIP

创建一个名为 db-service.yaml 的新文件,并将以下规范添加到其中。它可以在 sample-solutions/ch17 子文件夹中找到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.8 – 为数据库定义的 Kubernetes Service 对象

数据库组件将由此 Service 对象表示。它可以通过名称 db 进行访问,这是服务的名称,如第 4 行所定义。数据库组件不必公开访问,因此我们决定使用 ClusterIP 类型的 Service 对象。第 10 至 12 行的选择器定义了该服务代表具有必要标签的所有 pod 的稳定端点 – 即 app: petsservice: db

  1. 让我们使用以下命令部署此服务:

    $ kubectl apply -f db-service.yaml
    
  2. 现在,我们应该准备好测试该应用程序了。这次我们可以使用浏览器,欣赏肯尼亚马赛马拉国家公园美丽的动物图像:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.9 – 在 Kubernetes 中运行 pets 应用程序的测试

在这种情况下,端口号 30317 是 Kubernetes 自动为我的 web Service 对象选择的端口号。请将此数字替换为 Kubernetes 分配给您的服务的端口号。您可以使用 kubectl get services 命令获取该数字。

这样,我们就成功将宠物应用程序部署到了 Docker Desktop 提供的单节点 Kubernetes 集群中。我们需要定义四个构件才能完成这一操作,它们如下所示:

  • DeploymentService 对象用于 web 组件

  • StatefulSetService 对象用于 database 组件

要从集群中移除应用程序,我们可以使用以下小脚本:

kubectl delete svc/webkubectl delete deploy/web
kubectl delete svc/db
kubectl delete statefulset/db
kubectl delete pvc/pets-data-db-0

请注意该脚本的最后一行。我们正在删除 Kubernetes 自动为 db 部署创建的持久卷声明。当我们删除 db 部署时,这个声明不会被自动删除!持久卷声明与 Docker 卷有点相似(但请注意,它们并不相同)。

使用 kubectl get pvc 命令查看机器上所有声明的列表。

接下来,我们将优化部署。

精简部署过程

到目前为止,我们已经创建了四个需要部署到集群中的构件。这只是一个非常简单的应用程序,由两个组件组成。试想如果是一个更加复杂的应用程序,它会迅速变成一场维护噩梦。幸运的是,我们有几个方法可以简化部署。我们将在这里讨论的方法是,将组成 Kubernetes 应用程序的所有组件定义在一个文件中。

本书未涉及的其他解决方案包括使用包管理器,例如 Helm(helm.sh/)或 Kustomize(kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/),这是 Kubernetes 的原生解决方案。

如果我们的应用程序包含多个 Kubernetes 对象,例如 DeploymentService 对象,那么我们可以将它们都保存在一个文件中,并通过三个破折号分隔各个对象定义。例如,如果我们想在一个文件中包含 web 组件的 DeploymentService 定义,文件内容将如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.10 – 单个文件中的 web 组件部署和服务

您可以在 sample-solutions/ch17/install-web.yaml 文件中找到此文件。

接下来,我们将所有四个对象定义收集到 sample-solutions/ch17/install-pets.yaml 文件中,并可以一次性部署该应用程序:

$ kubectl apply -f install-pets.yaml

这将给出如下输出:

deployment "web" createdservice "web" created
deployment "db" created
service "db" created

类似地,我们创建了一个名为 sample-solutions/ch17/remove-pets.sh 的脚本,用于从 Kubernetes 集群中删除所有宠物应用程序的构件。请注意,该文件在使用之前已通过 chmod +x ./remove-pets.sh 命令设置为可执行文件。现在,我们可以使用以下命令:

$ ./remove-pets.sh

这将产生如下输出:

deployment.apps "web" deletedservice "web" deleted
statefulset.apps "db" deleted
service "db" deleted
persistentvolumeclaim "pets-data-db-0" deleted

或者,您可以使用以下命令:

$ kubectl delete -f install-pets.yaml

这将删除除持久卷声明外的所有资源,而持久卷声明需要手动删除:

$ kubectl delete pvc/pets-data-db-0

在这一部分,我们已经使用在第十一章《使用 Docker Compose 管理容器》中介绍的宠物应用程序,定义了将此应用程序部署到 Kubernetes 集群中所需的所有 Kubernetes 对象。在每个步骤中,我们确保得到了预期的结果,并且一旦所有的工件存在于集群中,我们展示了运行中的应用程序。

定义存活性和就绪性

像 Kubernetes 和 Docker Swarm 这样的容器编排系统大大简化了部署、运行和更新高度分布式、关键任务应用程序的过程。编排引擎自动化了许多繁琐的任务,例如上下扩展、确保所需状态始终得到维护等。

然而,编排引擎不能自动完成所有事情。有时,我们开发人员需要提供一些只有我们才能了解的信息来支持引擎。那么,我说的是什么意思呢?

我们来看一个单一的应用服务。假设它是一个微服务,我们称之为服务 A。如果我们将服务 A 容器化并运行在 Kubernetes 集群上,那么 Kubernetes 可以确保我们在服务定义中要求的五个实例始终运行。如果一个实例崩溃,Kubernetes 可以快速启动一个新实例,从而保持所需状态。但是,如果一个服务实例没有崩溃,而是不健康或还没有准备好处理请求呢?Kubernetes 应该知道这两种情况。但它不能,因为从应用服务的角度来看,健康与否超出了编排引擎的知识范畴。只有我们应用程序的开发人员知道我们的服务何时健康,何时不健康。

比如,应用服务可能正在运行,但由于某些 bug 其内部状态可能已经损坏,可能处于无限循环中,或者可能处于死锁状态。

类似地,只有我们这些应用程序开发人员才知道我们的服务是否准备好工作,或者它是否还在初始化中。虽然强烈建议将微服务的初始化阶段尽可能缩短,但如果某些服务需要较长的时间才能准备好工作,通常也无法避免。在初始化状态下并不意味着不健康。初始化阶段是微服务或任何其他应用服务生命周期中的预期部分。

因此,如果我们的微服务处于初始化阶段,Kubernetes 不应尝试杀死它。但是,如果我们的微服务不健康,Kubernetes 应该尽快将其杀死并替换为一个新的实例。

Kubernetes 有探针的概念,提供了协调引擎和应用开发者之间的连接。Kubernetes 使用这些探针来获取有关当前应用服务内部状态的更多信息。探针在每个容器内本地执行。服务的健康状况探针(也叫存活探针)、启动探针和服务的就绪探针都有对应的定义。我们逐一来看它们。

Kubernetes 存活探针

Kubernetes 使用存活探针来决定何时杀死一个容器,以及何时启动另一个实例来替代它。由于 Kubernetes 在 Pod 层面上操作,如果至少有一个容器报告为不健康,则相应的 Pod 会被杀死。

或者,我们可以换个角度来说:只有当一个 Pod 中的所有容器都报告健康时,Pod 才会被视为健康。

我们可以在 Pod 的规格说明中定义存活探针,如下所示:

apiVersion: v1kind: Pod
metadata:
…
spec:
  containers:
  - name: liveness-demo
    image: postgres:12.10
…
    livenessProbe:
      exec:
        command: nc localhost 5432 || exit –1
      initialDelaySeconds: 10
      periodSeconds: 5

相关部分在 livenessProbe 部分。首先,我们定义一个 Kubernetes 会在容器内执行的命令作为探针。在我们的例子中,我们有一个 PostreSQL 容器,使用 netcat Linux 工具来探测 5432 端口的 TCP。命令 nc localhost 5432 成功时,表示 Postgres 已经开始监听此端口。

另外两个设置项,initialDelaySecondsperiodSeconds,定义了 Kubernetes 在启动容器后应该等待多长时间才执行第一次探针,以及之后探针应该以多频繁的间隔执行。在我们的例子中,Kubernetes 等待 10 秒钟后执行第一次探针,然后每隔 5 秒执行一次探针。

还可以使用 HTTP 端点来替代命令进行探测。假设我们运行一个来自镜像 acme.com/my-api:1.0 的微服务,且该 API 的端点 /api/health 返回状态 200 (OK) 表示微服务健康,返回 50x (Error) 表示微服务不健康。在这种情况下,我们可以这样定义存活探针:

apiVersion: v1kind: Pod
metadata:
…
spec:
  containers:
  - name: liveness
    image: acme.com/my-api:1.0
…
    livenessProbe:
      httpGet:
        path: /api/health
        port: 3000
      initialDelaySeconds: 5
      periodSeconds: 3

在上面的代码片段中,我定义了存活探针,使其使用 HTTP 协议,并对 localhost5000 端口上的 /api/health 端点执行 GET 请求。记住,探针是在容器内执行的,这意味着我可以使用 localhost。

我们还可以直接使用 TCP 协议来探测容器上的端口。但稍等一下——我们不就是在第一个例子中使用了基于命令的通用存活探针吗?没错,我们确实使用了,但是我们依赖的是容器中是否存在 netcat 工具。我们不能假设这个工具总是存在。因此,依赖 Kubernetes 本身来为我们执行基于 TCP 的探测会更好。修改后的 Pod 规格如下:

apiVersion: v1kind: Pod
metadata:
…
spec:
  containers:
  - name: liveness-demo
    image: postgres:12.10
…
    livenessProbe:
      tcpSocket:
        port: 5432
      initialDelaySeconds: 10
      periodSeconds: 5

这个看起来非常相似。唯一的变化是,探针的类型从 exec 更改为 tcpSocket,并且我们不再提供命令,而是提供要探测的端口。

请注意,我们也可以在这里使用 Kubernetes 的 livenessProbe 配置项中的 failureThreshold。在 Kubernetes 中,livenessProbe 的失败阈值是指容器重启前必须连续发生的最小失败次数。默认值是 3。最小值是 1。如果处理程序返回失败代码,kubelet 会杀死容器并重新启动它。任何大于或等于 200 且小于 400 的代码表示成功,其他任何代码表示失败。

让我们试试这个:

  1. sample-solutions/ch17 文件夹中的 probes 子文件夹复制到你的 ch17 文件夹中。

  2. 使用以下命令构建 Docker 镜像:

    $ docker image build -t demo/probes-demo:2.0 probes
    
  3. 使用 kubectl 部署在 probes-demo.yaml 中定义的示例 pod:

    $ kubectl apply -f probes/probes-demo.yaml
    
  4. 描述 pod,并具体分析输出中的日志部分:

    $ kubectl describe pods/probes-demo
    

在大约前半分钟内,你应该看到以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.11 – 健康 pod 的日志输出

  1. 等待至少 30 秒,然后再次描述 pod。这时,你应该看到以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.12 – pod 状态变为不健康后的日志输出

标记的行表示探针失败,并且 pod 即将被重启。

  1. 如果你获取 pod 列表,你会看到 pod 已经重启了多次:

    $ kubectl get pods
    

这将导致以下输出:

NAME                   READY  STATUS     RESTARTS       AGEprobes-demo  1/1        Running  5 (49s ago)   7m22s
  1. 完成示例后,使用以下命令删除 pod:

    $ kubectl delete pods/probes-demo
    

接下来,我们将查看 Kubernetes 的就绪探针(readiness probe)。

Kubernetes 就绪探针(readiness probes)

Kubernetes 使用就绪探针来决定服务实例——即容器——何时准备好接收流量。现在,我们都知道 Kubernetes 部署和运行的是 pod 而非容器,因此讨论 pod 的就绪状态是有意义的。只有当 pod 中的所有容器都报告为“就绪”时,pod 才会被认为是“就绪”的。如果 pod 报告为“未就绪”,Kubernetes 将会把它从服务负载均衡器中移除。

就绪探针的定义与存活探针相同:只需将 pod 配置中的 livenessProbe 键切换为 readinessProbe。以下是使用我们之前的 pod 配置的示例:

…spec:
  containers:
  - name: liveness-demo
    image: postgres:12.10
…
    livenessProbe:
       tcpSocket:
         port: 5432
      failureThreshold: 2
      periodSeconds: 5
    readinessProbe:
      tcpSocket:
        port: 5432
      initialDelaySeconds: 10
      periodSeconds: 5

请注意,在这个例子中,由于我们现在有了就绪探针(readiness probe),我们不再需要为存活探针设置初始延迟。因此,我已将存活探针的初始延迟条目替换为一个名为 failureThreshold 的条目,表示在发生故障时 Kubernetes 应该重复探测多少次,直到它认为容器不健康。

Kubernetes 启动探针(startup probes)

对 Kubernetes 来说,知道一个服务实例何时启动通常是很有帮助的。如果我们为容器定义了启动探针,那么只要容器的启动探针未成功,Kubernetes 就不会执行存活探针或就绪探针。一旦所有 Pod 容器的启动探针成功,Kubernetes 就会开始执行容器的存活探针和就绪探针。

鉴于我们已经有了存活探针和就绪探针,什么时候我们需要使用启动探针?可能有一些情况需要考虑异常长的启动和初始化时间,例如在将传统应用程序容器化时。我们本可以通过配置就绪探针或存活探针来解决这个问题,但那样做会违背这些探针的目的。后者的探针旨在快速反馈容器的健康状况和可用性。如果我们配置了长时间的初始延迟或持续时间,反而会影响预期效果。

不出所料,启动探针的定义与就绪探针和存活探针相同。以下是一个示例:

spec:  containers:
...
    startupProbe:
      tcpSocket:
        port: 3000
      failureThreshold: 30
      periodSeconds: 5
...

确保你定义了failureThreshold * periodSeconds的乘积,以便它足够大,能够应对最差的启动时间。

在我们的示例中,最大启动时间不应超过 150 秒。

零停机部署

在关键任务环境中,应用程序必须始终保持运行。这些天,我们已经无法容忍停机了。Kubernetes 为我们提供了多种实现这一目标的方法。对集群中的应用程序进行更新而不导致停机被称为零停机部署。在本节中,我们将介绍实现这一目标的两种方法,具体如下:

  • 滚动更新

  • 蓝绿部署

让我们从讨论滚动更新开始。

滚动更新

在上一章中,我们了解到 Kubernetes 的Deployment对象与ReplicaSet对象的区别在于,它在后者的功能基础上增加了滚动更新和回滚功能。让我们使用我们的 Web 组件来演示这一点。我们将需要修改 Web 组件的部署清单或描述。

我们将使用与上一节相同的部署定义,唯一的区别是——我们将运行web组件。以下定义也可以在sample-solutions/ch17/web-deployment-rolling-v1.yaml文件中找到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.13 – 带有五个副本的 Web 组件部署

现在,我们可以像往常一样创建这个部署,同时也创建使我们的组件可访问的服务:

$ kubectl apply -f web-deployment-rolling-v1.yaml$ kubectl apply -f web-service.yaml

一旦我们部署了 Pod 和服务,就可以测试我们的 Web 组件。首先,我们可以使用以下命令获取分配的节点端口:

$ PORT=$(kubectl get svc/web -o jsonpath='{.spec.ports[0].nodePort}')

接下来,我们可以在curl语句中使用$PORT环境变量:

$ curl localhost:${PORT}/

这将提供预期的输出:

Pets Demo Application

如我们所见,应用程序已经启动并运行,返回了预期的消息,Pets Demo Application

我们的开发人员已经创建了 Web 组件的新版本 2.1。新版本的代码可以在sample-solutions/ch17/web文件夹中找到,唯一的变化位于server.js文件的第 12 行:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.14 – Web 组件版本 2.0 的代码更改

我们现在可以按如下方式构建新的镜像(将demo替换为你的 GitHub 用户名):

$ docker image build -t demo/ch17-web:2.1 web

随后,我们可以将镜像推送到 Docker Hub,步骤如下(将demo替换为你的 GitHub 用户名):

$ docker image push demo/ch17-web:2.1

现在,我们希望更新由属于web Deployment对象的 pod 使用的镜像。我们可以通过使用kubectlset image命令来实现:

$ kubectl set image deployment/web \    web=demo/ch17-web:2.1

如果我们再次测试该应用程序,我们将得到一个确认,证明更新确实已发生:

$ curl localhost:${PORT}/

输出显示现在已经安装了版本 2:

Pets Demo Application v2

那么,我们怎么知道在这次更新过程中没有任何停机时间呢?更新是以滚动方式进行的吗?滚动更新到底是什么意思呢?让我们来探讨一下。首先,我们可以通过使用rollout status命令,从 Kubernetes 获取确认,确保部署确实已成功完成:

$ kubectl rollout status deploy/web

命令将返回以下响应:

deployment "web" successfully rolled out

如果我们使用kubectl describe deploy/web描述web部署对象,在输出的末尾,我们将看到以下事件列表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.15 – 在 Web 组件部署描述输出中找到的事件列表

第一个事件告诉我们,在创建部署时,创建了一个名为web-769b88f67ReplicaSet对象,包含五个副本。然后,我们执行了update命令。事件列表中的第二个事件告诉我们,这意味着创建了一个新的ReplicaSet对象,名为web-55cdf67cd,最初只有一个副本。因此,在那个特定时刻,系统上存在六个 pod:五个初始 pod 和一个新的版本的 pod。但是,由于Deployment对象的期望状态要求只有五个副本,Kubernetes 现在将旧的ReplicaSet对象缩减为四个实例,这一点可以从第三个事件中看到。

然后,新的ReplicaSet对象被扩展到两个实例,随后,旧的ReplicaSet对象被缩减到三个实例,依此类推,直到我们得到了五个新的实例,并且所有旧的实例都被淘汰。尽管我们无法看到发生这些变化的具体时间(除了 3 分钟),但事件的顺序告诉我们,整个更新过程是以滚动方式进行的。

在短时间内,一些 web 服务的调用会从旧版本的组件中得到响应,而另一些调用则会从新版本的组件中得到响应,但服务在任何时候都不会中断。

我们还可以列出集群中的 ReplicaSet 对象,以确认我在前面提到的内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.16 – 列出集群中的所有 ReplicaSet 对象

在这里,我们可以看到新的 ReplicaSet 对象有五个实例正在运行,而旧的 ReplicaSet 对象已缩减为零实例。旧的 ReplicaSet 对象仍然存在的原因是 Kubernetes 允许我们回滚更新,在这种情况下,它会重用该 ReplicaSet

如果在更新镜像时出现一些未检测到的 bug 渗入新代码,我们可以使用 rollout undo 命令回滚更新:

$ kubectl rollout undo deploy/web

这将输出以下内容:

deployment.apps/web rolled back

我们可以像这样测试回滚是否成功:

$ curl localhost:${PORT}/

如我们所见,输出显示了这一点:

Pets Demo Application

如果我们列出 ReplicaSet 对象,我们将看到以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.17 – 回滚后列出 ReplicaSet 对象

这确认了旧的 ReplicaSetweb-9d66cd994)对象已被重用,而新的 ReplicaSet 对象已缩减为零实例。

在继续之前,请删除部署和服务:

$ kubectl delete deploy/web$ kubectl delete service/web

但是,有时我们无法或不想容忍旧版本与新版本共存的混合状态。我们希望采取全有或全无的策略。这时,蓝绿部署就派上用场了,我们将在接下来的内容中讨论。

蓝绿部署

如果我们想为宠物应用程序的 web 组件进行蓝绿式部署,可以通过巧妙地使用标签来实现。首先,让我们回顾一下蓝绿部署是如何工作的。以下是一个大致的步骤指南:

  1. web 组件的第一个版本作为 blue 部署。我们将为 pods 添加 color: blue 的标签来实现这一点。

  2. 为这些带有 color: blue 标签的 pods 在 selector 部分部署 Kubernetes 服务。

  3. 现在,我们可以部署版本 2 的 web 组件,但这次,pods 会有一个 color: green 的标签。

  4. 我们可以测试服务的绿色版本,以检查它是否按预期工作。

  5. 现在,我们可以通过更新 Kubernetes 服务来将流量从 blue 切换到 green,我们将修改选择器,使其使用 color: green 标签。

让我们为版本 1 定义一个 Deployment 对象,标记为 blue

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.18 – 为 web 组件指定蓝色部署

上述定义可以在sample-solutions/ch17/web-deployment-blue.yaml文件中找到。

请注意第 8 行,在那里我们将部署的名称定义为web-blue,以便与即将到来的web-green部署区分开来。另外,请注意我们在第 7、15 和 21 行添加了color: blue标签。其他内容与之前相同。

现在,我们可以为网页组件定义Service对象。它将与我们之前使用的相同,但有一个小的改动,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.19 – 支持蓝绿部署的网页组件 Kubernetes 服务

关于本章早些时候使用的服务定义,唯一的区别是第 17 行,它将color: blue标签添加到了选择器中。我们可以在sample-solutions/ch17/web-service-blue-green.yaml文件中找到上述定义。

然后,我们可以使用以下命令部署蓝色版本的web组件:

$ kubectl apply -f web-deploy-blue.yaml

我们可以使用此命令部署其服务:

$ kubectl apply -f web-service-blue-green.yaml

一旦服务启动并运行,我们可以确定其 IP 地址和端口号并进行测试:

$ PORT=$(kubectl get svc/web -o jsonpath='{.spec.ports[0].nodePort}')

然后,我们可以使用curl命令访问它:

$ curl localhost:${PORT}/

这将给我们预期的结果:

Pets Demo Application

现在,我们可以部署web组件的绿色版本。其Deployment对象的定义可以在sample-solutions/ch17/web-deployment-green.yaml文件中找到,内容如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.20 – 网页组件绿色部署规范

有趣的行如下:

  • 第 8 行:命名为web-green,以便与web-blue区分,并支持并行安装

  • 第 7、15 和 21 行:颜色为绿色

  • 第 24 行:现在使用的是本章前面构建的网页镜像版本 2.1

请不要忘记在第 24 行将‘’demo‘’改为你自己的 GitHub 用户名。

现在,我们准备部署这个绿色版本的服务。它应与蓝色服务分开运行:

$ kubectl apply -f web-deployment-green.yaml

我们可以确保两个部署并存,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.21 – 显示集群中运行的部署对象列表

如预期的那样,我们有蓝色和绿色两个版本在运行。我们可以验证蓝色仍然是活跃的服务:

$ curl localhost:${PORT}/

我们应该仍然收到以下输出:

Pets Demo Application

现在是有趣的部分:我们可以通过编辑现有的网页组件服务,将流量从blue切换到green。为此,请执行以下命令:

$ kubectl edit svc/web

将标签的颜色值从blue更改为green。然后,保存并退出编辑器。Kubernetes CLI 将自动更新服务。现在,当我们再次查询网页服务时,将得到如下内容:

$ curl localhost:${PORT}/

这时,我们应该得到以下输出:

Pets Demo Application v2

这证明了流量确实已经切换到web组件的绿色版本(注意curl命令响应末尾的v2)。

注意

如果我们希望坚持声明式的形式,那么最好更新web-service-blue-green.yaml文件并应用新版本,这样所需的状态仍然保存在文件中,避免现实与文件之间可能的不匹配。然而,为了说明,展示的方式是可以接受的。

如果我们意识到绿色部署出现了问题,新版本有缺陷,我们可以通过再次编辑 web 服务并将color标签的值替换为蓝色来轻松切换回蓝色版本。这个回滚是瞬时的,并且应该总是有效。然后,我们可以删除有缺陷的绿色部署并修复组件。一旦我们修复了问题,就可以再次部署绿色版本。

一旦组件的绿色版本按照预期运行并且性能良好,我们可以停用蓝色版本:

$ kubectl delete deploy/web-blue

当我们准备部署新版本 3.0 时,这个版本将成为蓝色版本。我们必须相应地更新ch17/web-deployment-blue.yaml文件并部署它。然后,我们必须将 web 服务从green切换到blue,依此类推。

通过这一点,我们成功地展示了如何在 Kubernetes 集群中实现蓝绿部署,使用的是我们宠物应用程序的web组件。

接下来,我们将学习如何处理 Kubernetes 中应用程序使用的秘密。

Kubernetes 秘密

有时,我们希望在 Kubernetes 集群中运行的服务必须使用机密数据,比如密码、API 密钥或证书,仅举几例。我们希望确保只有授权或专用服务能够查看这些敏感信息。集群中的所有其他服务不应访问这些数据。

出于这个原因,Kubernetes 引入了秘密管理。一个秘密是一个键值对,其中键是秘密的唯一名称,值是实际的敏感数据。秘密存储在etcd中。Kubernetes 可以配置为在静态存储时加密秘密——也就是在etcd中——以及在传输时加密秘密——也就是当秘密从主节点传输到工作节点,这些节点上运行着使用此秘密的服务的 pod 时。

手动定义秘密

我们可以像创建 Kubernetes 中的任何其他对象一样声明式地创建一个秘密。以下是这样一个秘密的 YAML 配置:

apiVersion: v1kind: Secret
metadata:
  name: pets-secret
type: Opaque
data:
  username: am9obi5kb2UK
  password: c0VjcmV0LXBhc1N3MHJECg==

上面的定义可以在sample-solutions/ch17/pets-secret.yaml文件中找到。现在,你可能会想知道这些值是什么。这些是实际的(未加密的)值吗?不是的。它们也不是加密值,而只是base64编码的值。

因此,它们并不完全安全,因为base64编码的值可以轻松还原为明文值。我是如何获得这些值的?这很简单——只需按照以下步骤操作:

  1. 使用 base64 工具按如下方式编码值:

    $ echo "john.doe" | base64
    

这将导致以下输出:

am9obi5kb2UK

另外,尝试以下操作:

$ echo "sEcret-pasSw0rD" | base64

这将给我们带来以下输出:

c0VjcmV0LXBhc1N3MHJECg==
  1. 使用前面的值,我们可以创建密钥:

    $ kubectl create -f pets-secret.yaml
    

在这里,命令输出如下:

secret/pets-secret created
  1. 我们可以使用以下命令描述密钥:

    $ kubectl describe secrets/pets-secret
    

前面的命令输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.22 – 创建和描述 Kubernetes 密钥

  1. 在密钥描述中,值被隐藏,只有它们的长度会显示。所以,也许密钥现在是安全的了。其实不然。我们可以通过 kubectlget 命令轻松解码这个密钥:

    $ kubectl get secrets/pets-secret -o yaml
    

输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.23 – 解码 Kubernetes 密钥

如前面的截图所示,我们已经恢复了原始的密钥值。

  1. 解码你之前得到的值:

    $ echo "c0VjcmV0LXBhc1N3MHJECg==" | base64 –decode
    

这将导致以下输出:

sEcret-pasSw0rD

因此,结果是这种创建 Kubernetes 密钥的方法只适用于开发环境,在那里我们处理的是非敏感数据。在所有其他环境中,我们需要一种更好的方式来处理密钥。

使用 kubectl 创建密钥

定义密钥的更安全方式是使用 kubectl。首先,我们必须创建包含 base64 编码密钥值的文件,类似于前一节所做的,但这次,我们必须将值存储在临时文件中:

$ echo "sue-hunter" | base64 > username.txt$ echo "123abc456def" | base64 > password.txt

现在,我们可以使用 kubectl 从这些文件创建密钥,如下所示:

$ kubectl create secret generic pets-secret-prod \    --from-file=./username.txt \
    --from-file=./password.txt

这将导致以下输出:

secret "pets-secret-prod" created

然后,密钥可以像手动创建的密钥一样使用。

你可能会问,为什么这种方法比另一种更安全?首先,没有 YAML 文件定义密钥,并且它存储在某些源代码版本控制系统中,例如 GitHub,很多人都可以访问这些系统,因此他们可以看到并解码这些密钥。

只有授权了解密钥的管理员才能看到密钥的值,并使用它们直接在(生产)集群中创建密钥。集群本身受到基于角色的访问控制保护,因此没有授权的人无法访问它,也无法解码集群中定义的密钥。

现在,让我们看看如何使用我们定义的密钥。

在 pod 中使用密钥

假设我们要创建一个 Deployment 对象,其中 web 组件使用我们在前一节中介绍的密钥 pets-secret。我们可以使用以下命令在集群中创建密钥:

$ kubectl apply -f pets-secret.yaml

sample-solutions/ch17/web-deployment-secret.yaml 文件中,我们可以找到 Deployment 对象的定义。我们需要将从第 23 行开始的部分添加到原始的 Deployment 对象定义中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.24 – 带有密钥的 Web 组件的部署对象

在第 29 到 32 行,我们定义了一个名为secrets的卷,该卷来自我们的密钥pets-secret。然后,我们按照第 25 到 28 行的描述,在容器中使用该卷。

我们将密钥挂载到容器文件系统的/etc/secrets路径,并以只读模式挂载该卷。因此,密钥值将作为文件提供给容器,并存放在该文件夹中。文件名将对应于键名,文件内容将是对应键的值。密钥值将以未加密的形式提供给运行在容器内的应用程序。

使用以下命令应用部署:

$ kubectl apply -f web-deployment-secret.yaml

在我们的例子中,由于密钥中有用户名和密码的键,我们将在容器文件系统的/etc/secrets文件夹中找到两个文件,分别名为usernamepasswordusername文件应该包含john.doe值,password文件应该包含sEcret-pasSw0rD值。让我们确认一下:

  • 首先,我们将获取 Pod 的名称:

    $ kubectl get pods
    

这将给我们以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.25 – 查找 Pod 的名称

  • 使用 Pod 的名称,我们可以执行以下屏幕截图中显示的命令来获取密钥:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.26 – 确认容器内可以访问密钥

在前面的输出的第 1 行,我们exec进入运行web组件的容器。然后,在第 2 到 5 行,我们列出了/etc/secrets文件夹中的文件,最后,在最后 3 行,我们展示了两个文件的内容,毫无意外地,显示了明文的密钥值。

由于任何语言编写的应用程序都可以读取简单的文件,因此使用密钥的这种机制非常向后兼容。即使是一个旧的 Cobol 应用程序也能从文件系统中读取明文文件。

离开之前,请删除 Kubernetes 部署:

$ kubectl delete deploy/web

然而,有时应用程序期望密钥在环境变量中可用。

让我们看看 Kubernetes 在这种情况下为我们提供了什么。

环境变量中的密钥值

假设我们的 Web 组件期望PETS_USERNAME环境变量中有用户名,PETS_PASSWORD环境变量中有密码。如果是这样,我们可以修改部署的 YAML 文件,使其如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.27 – 部署映射密钥值到环境变量

在第 25 到 35 行,我们定义了两个环境变量PETS_USERNAMEPETS_PASSWORD,并将pets-secret中的相应键值对映射到它们。

应用更新后的部署:

$ kubectl apply -f web-deployment-secret.yaml

请注意,我们不再需要使用卷;相反,我们直接将 pets-secret 的各个密钥映射到容器内有效的环境变量。以下命令序列显示了秘密值确实可用,并且已经映射到相应的环境变量中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 17.28 – 秘密值已经映射到环境变量

在本节中,我们展示了如何在 Kubernetes 集群中定义秘密,以及如何在作为部署的一部分运行的容器中使用这些秘密。我们展示了秘密如何在容器内部映射的两种变体——使用文件和使用环境变量。

总结

在本章中,我们学习了如何将应用程序部署到 Kubernetes 集群,并为该应用程序设置应用层路由。此外,我们还学习了如何在不引起任何停机的情况下更新 Kubernetes 集群中运行的应用服务。最后,我们使用秘密为集群中运行的应用服务提供敏感信息。

在下一章中,我们将学习不同的技术,这些技术用于监控在 Kubernetes 集群中运行的单个服务或整个分布式应用程序。我们还将学习如何在不更改集群或服务所在集群节点的情况下,排查生产环境中运行的应用服务问题。敬请期待。

进一步阅读

这里有一些链接,提供了本章讨论主题的更多信息:

问题

为了评估你的学习进度,请回答以下问题:

  1. 你有一个由两个服务组成的应用程序,第一个是 Web API,第二个是数据库,如 MongoDB。你想将这个应用程序部署到 Kubernetes 集群中。用简短的几句话解释你会如何进行。

  2. 在 Kubernetes 应用服务的上下文中,liveness 和 readiness 探针是什么?

  3. 请用自己的话描述你需要哪些组件来为你的应用程序建立第七层(或应用层)路由。

  4. 简要列出实施蓝绿部署所需的主要步骤。避免过多细节。

  5. 列举三到四种你会通过 Kubernetes 秘密提供给应用服务的信息类型。

  6. 列出 Kubernetes 在创建秘密时接受的来源。

  7. 如何配置应用服务以使用 Kubernetes 秘密?

答案

下面是本章问题的答案:

  1. 假设我们在注册表中有一个用于两个应用服务的 Docker 镜像——网页 API 和 MongoDB——我们需要做如下操作:

    1. 使用StatefulSet对象定义 MongoDB 的部署;我们将这个部署命名为db-deploymentStatefulSet对象应该有一个副本(复制 MongoDB 稍微复杂一些,超出了本书的范围)。

    2. 定义一个名为db的 Kubernetes 服务,类型为ClusterIP,用于db-deployment

    3. 定义一个网页 API 的部署,命名为web-deployment

    4. 我们将这个服务扩展为三个实例。

    5. 定义一个名为api的 Kubernetes 服务,类型为NodePort,用于web-deployment

    6. 如果我们使用密钥,那么直接在集群中通过kubectl定义这些密钥。

    7. 使用kubectl部署应用。

  2. 存活探针和就绪探针是 Kubernetes 为容器提供的健康检查。存活探针检查容器是否仍在运行,如果没有,Kubernetes 会自动重启它。就绪探针检查容器是否准备好接受请求。如果容器未通过就绪检查,它不会被移除,但在通过就绪探针之前,不会接收任何传入请求。

  3. 为了实现应用的第 7 层路由,我们理想情况下使用IngressController。这是一种反向代理,如 Nginx,它有一个侧车容器监听 Kubernetes 服务器 API 的相关变化,并在检测到变化时更新反向代理的配置并重启它。然后,我们需要在集群中定义 Ingress 资源,定义路由,例如从基于上下文的路由如https://example.com/pets<服务名称>/<端口>或类似api/32001的配对。当 Kubernetes 创建或更改此Ingress对象时,IngressController的侧车容器会捕捉并更新代理的路由配置。

  4. 假设这是一个集群内部的库存服务,那么我们做如下操作:

    1. 部署 1.0 版本时,我们定义一个名为inventory-deployment-blue的部署,并将 Pods 标记为color:blue

    2. 我们为前述部署部署一个类型为ClusterIP的 Kubernetes 服务,名为inventory,并且选择器包含color:blue

    3. 当我们准备部署新版本的payments服务时,我们定义一个该服务的 2.0 版本的部署,并将其命名为inventory-deployment-green。我们给 Pods 添加一个color:green标签。

    4. 现在我们可以对“绿色”服务进行冒烟测试,当一切正常时,我们可以更新库存服务,使选择器包含color:green

  5. 一些形式的信息是机密的,因此应该通过 Kubernetes 密钥提供给服务,包括密码、证书、API 密钥 ID、API 密钥秘密和令牌。

  6. 密钥值的来源可以是文件或 base64 编码的值。

  7. 要配置应用程序使用 Kubernetes 密钥,必须创建一个包含敏感数据的Secret对象。然后,必须修改你的Pod规格,使其包含对Secret对象的引用。此引用可以作为容器规格中的环境变量,或者作为卷挂载,这样你的应用程序就可以使用这些密钥数据。

第十八章:18

在云中运行容器化应用程序。

在上一章中,我们学习了如何将应用程序部署、更新和扩展到 Kubernetes 集群中。我们了解了如何实现零停机部署,以实现不中断的更新和回滚关键应用程序。最后,我们介绍了 Kubernetes 机密作为配置服务和保护敏感数据的手段。

在本章中,我们将概述在云中运行容器化应用程序的三种最流行的方式。我们将探讨每种托管解决方案,并讨论它们的优缺点。

以下是本章中我们将讨论的主题:

  • 为什么选择托管 Kubernetes 服务?

  • Amazon Elastic Kubernetes Service (Amazon EKS) 上运行一个简单的容器化应用程序。

  • 探索 Microsoft 的 Azure Kubernetes Service (AKS)。

  • 理解 Google Kubernetes Engine (GKE)。

阅读完本章后,您将能够执行以下操作:

  • 分析托管 Kubernetes 服务与自管理 Kubernetes 集群相比的优缺点。

  • 在 Amazon EKS 中部署并运行一个简单的分布式应用程序。

  • 部署并在 Microsoft 的 AKS 上运行一个简单的分布式应用程序。

  • 在 GKE 上部署并运行一个简单的分布式应用程序。

技术要求

我们将在本章节中使用 亚马逊网络服务 (AWS),Microsoft Azure 和 Google Cloud;因此,每个平台都需要有一个账户。如果您没有现有账户,可以申请这些云服务提供商的试用账户。

我们还将使用我们实验室在 GitHub 存储库的 ~/The-Ultimate-Docker-Container-Book/sample-solutions/ch18 文件夹中的文件,网址为 github.com/PacktPublishing/The-Ultimate-Docker-Container-Book/tree/main/sample-solutions/ch18

准备放置您自己代码的文件夹。首先,导航至源文件夹,如下所示:

$ cd ~/The-Ultimate-Docker-Container-Book

然后,创建一个 ch18 子文件夹并导航至该文件夹,如下所示:

$ mkdir ch18 & cd ch18

为什么选择托管 Kubernetes 服务?

目前,AWS、Microsoft Azure 和 Google Cloud 是最受欢迎的三大云服务提供商,每个都提供了托管 Kubernetes 服务,如下所述:

  • Amazon EKS:Amazon EKS 是一个托管服务,使您能够在 AWS 上运行 Kubernetes,无需安装、操作和维护自己的 Kubernetes 控制平面或节点。

  • AKS:AKS 是 Microsoft 的托管 Kubernetes 服务。它提供了与 持续集成和持续部署 (CI/CD) 能力以及 Kubernetes 工具集成的开发人员生产力。它还具有完整的容器 CI/CD 平台的 Azure DevOps 项目。

  • GKE:Google 是 Kubernetes 的原始创造者,GKE 是市场上第一个可用的托管 Kubernetes 服务。它提供了先进的集群管理功能,并与 Google Cloud 服务集成。

其他提供商也提供 Kubernetes 即服务KaaS),例如 IBM Cloud Kubernetes 服务、Oracle Kubernetes 容器引擎和 DigitalOcean KubernetesDOKS)。鉴于云市场发展迅速,查看最新的产品和功能始终是一个好主意。

管理一个 Kubernetes 集群,无论是在本地还是在云中,都涉及相当复杂的操作工作,并且需要专业知识。以下是使用托管 Kubernetes 服务通常是首选解决方案的一些原因:

  • 设置和管理的简易性:托管 Kubernetes 服务处理底层基础设施,减少了管理 Kubernetes 集群的操作负担。它们会自动处理 Kubernetes 控制平面的供应、升级、补丁和扩展。

  • 高可用性(HA)和高可扩展性:托管服务通常为你的应用提供开箱即用的高可用性和高可扩展性。它们处理必要的协调工作,以将应用分布到不同的节点和数据中心。

  • 安全与合规性:托管服务通常包括内置的安全功能,如网络策略、基于角色的访问控制RBAC)和与云提供商 身份与访问管理IAM)服务的集成。它们还负责 Kubernetes 软件本身的安全更新。

  • 监控与诊断:托管的 Kubernetes 服务通常包括与监控和日志服务的集成,使得观察和排除应用程序故障变得更加容易。

  • 成本:虽然使用托管服务会产生一定的费用,但其成本通常低于为高效、安全地运营一个 Kubernetes 集群所需的专职人员和基础设施成本。

  • 支持:使用托管 Kubernetes 服务时,你将能够获得云服务提供商的支持。如果你在运行生产工作负载并需要快速解决任何问题,这尤其有价值。

相比之下,运行你自己的 Kubernetes 集群涉及大量的设置和维护工作。从 Kubernetes 的安装和配置,到集群升级、安全补丁、节点供应和扩展的持续任务,再到设置监控和告警,你都需要负责。

管理自己的集群虽然提供了更多的控制和灵活性,但需要大量的时间、资源和专业知识投入。对于许多组织来说,托管服务的好处远远超过了自主管理集群所带来的控制力提升。

在 Amazon EKS 上运行一个简单的容器化应用程序

在这一部分,我们希望在 Amazon EKS 上使用 Fargate 创建一个完全托管的 Kubernetes 集群。创建新集群的过程在 AWS 文档中有详细描述,我们将参考相关页面,以避免重复过多信息。话虽如此,让我们从以下步骤开始。

什么是 Fargate?

AWS Fargate 是由 AWS 提供的无服务器计算引擎,用于容器。它消除了管理底层服务器的需要,让你可以专注于设计和构建应用程序。Fargate 处理容器的部署、扩展和管理,使你可以在无需担心基础设施的情况下启动应用程序。

让我们首先处理一些前提条件,如下所示:

  1. 确保你可以访问一个 AWS 账户。如果没有,你可以在这里获得一个免费的 1 年试用账户:aws.amazon.com/free

  2. 登录到你的 AWS 账户。

  3. 为你的账户创建一对新的访问密钥访问密钥秘密,你将用它们来配置你的 AWS CLI,从而可以通过命令行访问你的账户。

  4. 在屏幕右上角找到你的个人资料,从下拉菜单中选择安全凭证

选择访问密钥(访问密钥 ID 和秘密访问密钥),然后点击创建 访问密钥

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.1 – 将访问密钥 ID 和秘密配对记录在安全位置

  1. 打开一个新的终端。

  2. 确保你已安装 AWS CLI。

在 Mac 上,使用以下命令:

$ brew install awscli

在 Windows 上,使用以下命令:

$ choco install awscli
  1. 在两种情况下,都可以使用以下命令来测试安装是否成功:

    $ aws --version
    
  2. 配置你的 AWS CLI。为此,你需要你在前面步骤 3中创建的AWS 访问密钥 IDAWS 秘密访问密钥,以及你的默认区域

然后,使用以下命令:

$ aws configure

在被询问时输入适当的值。对于默认输出格式,选择 JSON,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.2 – 配置 AWS CLI

  1. 尝试使用如下命令访问你的账户:

    $ aws s3 ls
    

这应该列出为你的账户定义的所有简单存储服务S3)存储桶。你的列表可能为空。这里需要注意的是,命令成功执行即可。

  1. 最后,运行以下命令来再次检查是否已安装 kubectl

    $ kubectl version
    

现在,我们已经准备好创建 Amazon EKS 集群。按照以下步骤操作:

  1. 定义几个环境变量,以便后续使用,如下所示:

    $ export AWS_REGION=eu-central-1$ export AWS_STACK_NAME=animals-stack$ export AWS_CLUSTER_ROLE=animals-cluster-role
    

确保将 eu-central-1 替换为离你最近的 AWS 区域。

  1. 现在,你可以使用以下命令创建所需的 AWS 堆栈,其中包括 VPC、私有和公共子网以及安全组—为了简化操作,使用 AWS 提供的一个示例 YAML 文件:

    $ aws cloudformation create-stack --region $AWS_REGION \    --stack-name $AWS_STACK_NAME \    --template-url https://s3.us-west-2.amazonaws.com/amazon-eks/cloudformation/2020-10-29/amazon-eks-vpc-private-subnets.yaml
    

请花点时间下载并查看前面的 YAML 文件,以了解该命令具体在配置什么内容。

  1. 在接下来的几个步骤中,您需要定义正确的设置,以授予集群所需的访问权限:

    1. 首先使用以下命令创建一个 IAM 角色:
    $ aws iam create-role \    --role-name $AWS_CLUSTER_ROLE \    --assume-role-policy-document file://"eks-cluster-role-trust-policy.json"
    
    1. 继续通过此命令将必要的 Amazon EKS 管理的 IAM 策略附加到刚刚创建的角色:
    $ aws iam attach-role-policy \    --policy-arn arn:aws:iam::aws:policy/AmazonEKSClusterPolicy \    --role-name $AWS_CLUSTER_ROLE
    
  2. 现在,我们继续进行一些交互步骤,使用 Amazon EKS 控制台:console.aws.amazon.com/eks/home#/clusters

注意

确保控制台右上角显示的 AWS 区域是您要创建集群的 AWS 区域(例如,在作者的案例中是eu-central-1)。如果不是,请选择 AWS 区域名称旁边的下拉菜单并选择您要使用的 AWS 区域。

  1. 若要创建集群,请选择添加集群命令,然后选择创建。如果您没有看到此选项,请首先在左侧导航窗格中选择集群

  2. animals-cluster上。

  3. 选择animals-cluster-role

  4. 所有其他设置可以保持为默认值。

  5. 选择下一步

  • vpc-00x0000x000x0x000 | animals-stack-VPC上。注意名称的后缀,表示它是我们刚刚定义的那个。*同样,您可以保持其他设置为默认值。*选择下一步继续。*我们无需更改配置日志记录页面上的任何内容,因此请选择下一步。*同样的情况适用于选择插件页面,因此选择下一步。*再一次,在配置已选择的插件设置页面上,无需做任何操作,因此请选择下一步。*最后,在审查并创建页面,选择创建。*在集群名称的右侧,集群状态为创建中,持续几分钟,直到集群配置过程完成,如下图所示。在状态变为活动之前,请勿继续进行下一步:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.3 – 创建 EKS 集群

  1. 不幸的是,我们还没有完成。我们需要创建一个信任策略并将其附加到我们的集群。为此,按照以下步骤操作:

    1. 首先创建一个pod-execution-role-trust-policy.json文件,并将以下内容添加到其中:
    {  "Version": "2012-10-17",  "Statement": [    {      "Effect": "Allow",      "Condition": {        "ArnLike": {          "aws:SourceArn": "arn:aws:eks:<region-code>:<account-no>:fargateprofile/animals-cluster/*"        }      },      "Principal": {        "Service": "eks-fargate-pods.amazonaws.com"      },      "Action": "sts:AssumeRole"    }  ]}
    

在前面的代码中,将<region-code>替换为您的 AWS 区域代码(在我的案例中是eu-central-1),将<account-no>替换为您的账户号码。您可以在 AWS 控制台左上角的个人资料中找到后者。

  1. 使用刚刚配置的信任策略,使用以下命令创建一个Pod 执行 IAM 角色
$ aws iam create-role \    --role-name AmazonEKSFargatePodExecutionRole \
    --assume-role-policy-document file://"pod-execution-role-trust-policy.json"
  1. 最后,使用以下命令将所需的角色和策略连接在一起:
$ aws iam attach-role-policy \    --policy-arn arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy \
    --role-name AmazonEKSFargatePodExecutionRole
  1. animals-cluster集群上。

  2. animals-cluster页面上,执行以下操作:

    1. 选择animals-profile

    2. 对于在前一步创建的AmazonEKSFargatePodExecutionRole角色。

    3. 选择子网下拉框,并取消选择名称中包含Public的任何子网。仅支持在 Fargate 上运行的 Pods 使用私有子网。

    4. 选择下一步

  • default下。* 然后选择下一步。* 在审查并创建页面,审查你的 Fargate 配置文件的信息,并选择创建。* 几分钟后,Fargate 配置文件配置部分的状态将从创建中变为活动。在状态变为活动之前,不要继续执行下一步。* 如果你计划将所有 Pods 部署到 Fargate(不使用 Amazon EC2 节点),请按以下步骤创建另一个 Fargate 配置文件并在 Fargate 上运行默认的名称解析器(CoreDNS)。

注意

如果你不这样做,目前将不会有任何节点。

  1. animals-profile下。

  2. Fargate 配置文件下,选择添加 Fargate 配置文件

  3. 名称字段中输入CoreDNS

  4. 对于你在步骤 13中创建的AmazonEKSFargatePodExecutionRole角色。

  5. 单击其名称中的Public。Fargate 仅支持私有子网中的 Pods。

  6. 选择下一步

  7. kube-system下。

  8. 选择匹配标签,然后选择添加标签

  9. 字段中输入k8s-app作为kube-dns。这是必要的,以便将默认的名称解析器(CoreDNS)部署到 Fargate。

  10. 选择下一步

  11. 审查并创建页面,审查 Fargate 配置文件的信息并选择创建

  12. 运行以下命令,删除 CoreDNS Pods 上的默认eks.amazonaws.com/compute-type : ec2注解:

    kubectl patch deployment coredns \    -n kube-system \    --type json \    -p='[{"op": "remove", "path": "/spec/template/metadata/annotations/eks.amazonaws.com~1compute-type"}]'
    

注意

系统会根据你添加的 Fargate 配置文件标签创建并部署两个节点。你不会在节点组中看到任何列出的内容,因为 Fargate 节点不适用,但你将在计算标签中看到新的节点。

若需更详细的解释,可以按照以下链接中的逐步指南来创建集群:

docs.aws.amazon.com/eks/latest/userguide/getting-started-console.xhtml (开始使用 Amazon EKS – AWS 管理控制台和 AWS CLI)

当你的集群准备好后,可以继续执行以下步骤:

  1. 配置kubectl以访问 AWS 上的新集群,如下所示:

    $ aws eks update-kubeconfig --name animals-cluster
    

响应应类似于以下内容:

Added new context arn:aws:eks:eu-central-...:cluster/animals-cluster to /Users/<user-name>/.kube/config

这里,<user-name>对应于你正在使用的机器上的用户名。

  1. 双重检查kubectl是否使用了正确的上下文——即刚为 AWS 上的集群创建并添加到你的~/.kube/config文件中的上下文:

    $ kubectl config current-context
    

答案应类似于以下内容:

arn:aws:eks:eu-central-...:cluster/animals-cluster

如果另一个上下文是活动状态,请使用kubectl config use-context命令,并结合正确的 AWS 上下文。

  1. 使用kubectl列出集群上的所有资源,像这样:

    $ kubectl get all
    

此时的答案应如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.4 – Amazon EKS – kubectl get all

  1. 要查看集群的节点,请使用以下命令:

    $ kubectl get nodes
    

然后你应该看到类似以下的内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.5 – EKS 集群中节点的列表

  1. 导航到本章的ch18文件夹,创建一个aws-eks子文件夹,然后进入该文件夹:

    $ cd ~/The-Ultimate-Docker-Container-Book/ch18$ mkdir aws-eks && cd aws-eks
    
  2. 在此子文件夹中,创建一个名为deploy-nginx.yaml的文件,内容如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.6 – 在 Amazon EKS 上部署 nginx 的规范

  1. 使用kubectl将我们的部署部署到集群,如下所示:

    $ kubectl apply -f deploy-nginx.yaml
    
  2. 使用以下命令观察 Pod 的创建过程:

    $ kubectl get pods -w
    

然后等待它们准备就绪:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.7 – 列出部署到 AWS 的 Pods

  1. 等待它们在1/1的值。

  2. 在 AWS 控制台中,导航到你的集群。

  3. web Pods 和两个coredns Pods 已创建。

  4. 计算选项卡中,观察到已创建多个 Fargate 节点。

  5. 深入到节点以查看已部署到其上的 Pod。

  6. 进一步深入到 Pod,并观察其详细信息视图中显示的事件列表。

恭喜你——你已在 AWS 上创建了一个完全托管的 Kubernetes 集群,并使用kubectl在其上创建了第一个部署!正如你所知,这是一项相当了不起的成就。结果表明,在讨论的所有云提供商中,AWS 需要远远比其他更多的步骤才能运行一个 Kubernetes 集群。

在离开之前,并为了避免意外费用,请确保清理掉在此练习期间创建的所有资源。为此,请按照以下步骤操作:

  1. 使用kubectl删除先前的部署:

    $ kubectl delete -f deploy-nginx.yaml
    
  2. 定位你的animals-cluster集群并选择它。

  3. animals-profileCoreDNS配置文件中删除它们。

  4. 当删除这两个配置文件时(可能需要几分钟),然后点击删除集群按钮以摆脱该集群。

  5. 删除你创建的 VPC AWS CloudFormation 堆栈。

  6. 打开AWS CloudFormation控制台,网址为console.aws.amazon.com/cloudformation

  7. 选择animals-stack堆栈,然后选择删除

  8. 删除 animals-stack确认对话框中,选择删除堆栈

  9. 删除你创建的 IAM 角色。

  10. 打开 IAM 控制台,网址为console.aws.amazon.com/iam/

  11. 在左侧导航窗格中,选择角色

  12. 从列表中选择你创建的每个角色(myAmazonEKSClusterRole,以及AmazonEKSFargatePodExecutionRolemyAmazonEKSNodeRole)。选择删除,输入请求的确认文本,然后选择删除

或者,按照 AWS 文档中第 5 步:删除资源部分的步骤执行:

docs.aws.amazon.com/eks/latest/userguide/getting-started-console.xhtml

这是一次相当了不起的成就!创建和管理一个 EKS 集群需要比我们预期更多的细节知识。我们将看到,其他提供商在这方面更加用户友好。

现在我们大致了解了 Amazon EKS 的功能,接下来让我们看看全球第二大云服务提供商的产品组合。

探索微软的 AKS

要在 Azure 中实验微软的容器相关服务,我们需要一个 Azure 账户。你可以创建一个试用账户或使用现有账户。你可以在这里获得免费试用账户:azure.microsoft.com/en-us/free/

微软在 Azure 上提供了不同的容器相关服务。最易使用的可能是 Azure 容器实例,它承诺是运行容器的最快和最简单方式,无需配置任何虚拟机VMs),也不需要采用更高级的服务。如果你只想在托管环境中运行单个容器,这项服务非常有用。设置非常简单。在 Azure 门户(portal.azure.com)中,你首先创建一个新的资源组,然后创建一个 Azure 容器实例。你只需要填写一个简短的表格,填写容器名称、使用的镜像和要打开的端口等属性。容器可以通过公共或私有 IP 地址提供,并且如果容器崩溃,它会自动重启。这里有一个不错的管理控制台,例如用于监控资源消耗,如 CPU 和内存。

第二个选择是Azure 容器服务ACS),它提供了一种简化集群虚拟机创建、配置和管理的方式,这些虚拟机经过预配置可运行容器化应用。ACS 使用 Docker 镜像,并提供三种编排工具的选择:Kubernetes、Docker Swarm 和分布式云操作系统DC/OS)(由 Apache Mesos 提供支持)。微软声称其服务能够扩展到数万个容器。ACS 是免费的,只有计算资源会收费。

在这一部分,我们将重点讨论基于 Kubernetes 的最流行的产品。它叫做 AKS,可以在这里找到:azure.microsoft.com/en-us/services/kubernetes-service/。AKS 使你可以轻松地在云中部署应用并在 Kubernetes 上运行它们。所有复杂和繁琐的管理任务都由微软处理,你可以完全专注于你的应用程序。这意味着你永远不必处理安装和管理 Kubernetes、升级 Kubernetes 或升级底层 Kubernetes 节点操作系统等任务。这些都由 Microsoft Azure 的专家处理。此外,你永远不必处理 etc 或 Kubernetes 主节点。这些都被隐藏起来,你唯一需要交互的是运行你应用的 Kubernetes 工作节点。

准备 Azure CLI

话虽如此,让我们开始。我们假设你已经创建了一个免费试用账户,或者正在使用 Azure 上的现有账户。有多种方式可以与 Azure 账户进行交互。我们将使用在本地计算机上运行的 Azure CLI。我们可以将 Azure CLI 下载并安装到本地计算机,或者在本地 Docker Desktop 上的容器内运行它。由于本书的主题是容器,我们选择后者。

最新版本的 Azure CLI 可以在 Docker Hub 上找到。让我们拉取它:

$ docker image pull mcr.microsoft.com/azure-cli:latest

我们将从这个 CLI 运行一个容器,并在这个容器内部的 shell 中执行所有后续命令。现在,我们需要克服一个小问题——这个容器中没有安装 Docker 客户端。但是我们还需要运行一些 Docker 命令,因此我们必须创建一个从前面提到的镜像派生的自定义镜像,其中包含 Docker 客户端。为此所需的 Dockerfile 可以在 sample-solutions/ch18 子文件夹中找到,其内容如下:

FROM mcr.microsoft.com/azure-cli:latestRUN apk update && apk add docker

第 2 行,我们仅使用 Alpine 包管理器 apk 来安装 Docker。然后我们可以使用 Docker Compose 来构建并运行这个自定义镜像。对应的 docker-compose.yml 文件如下:

version: "2.4"services:
  az:
    image: fundamentalsofdocker/azure-cli
    build: .
    command: tail -F anything
    working_dir: /app
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    - .:/app

注意

tail -F anything 命令用于保持容器运行,并且用于挂载 Docker 套接字和当前文件夹到 volumes 部分。

提示

如果你在 Windows 上运行 Docker Desktop,则需要定义 COMPOSE_CONVERT_WINDOWS_PATHS 环境变量,才能挂载 Docker 套接字。你可以在 Bash shell 中使用 export COMPOSE_CONVERT_WINDOWS_PATHS=1,或在运行 PowerShell 时使用 $Env:COMPOSE_CONVERT_WINDOWS_PATHS=1。更多详情请参见以下链接:github.com/docker/compose/issues/4240

现在,让我们构建并运行这个容器,步骤如下:

$ docker compose up --build -d

接下来,让我们进入 az 容器并在其中运行 Bash shell,使用以下命令:

$ docker compose exec az /bin/bash

你应该会看到如下输出:

376f1e715919:/app #

注意,你的哈希码(376f1e...)代表容器内的主机名将会不同。为了简化后续命令的阅读,我们将省略哈希码。

正如你所注意到的,我们发现自己正在容器内的 Bash shell 中运行。首先,我们来检查 CLI 的版本:

# az --version

这将生成类似如下的输出:

azure-cli                         2.49.0core                              2.49.0
telemetry                          1.0.8
Dependencies:
msal                              1.20.0
azure-mgmt-resource               22.0.0
Python location '/usr/local/bin/python'
Extensions directory '/root/.azure/cliextensions'
Python (Linux) 3.10.11 (main, May 11 2023, 23:59:31) [GCC 12.2.1 20220924]
Legal docs and information: aka.ms/AzureCliLegal
Your CLI is up-to-date.

好的——我们运行的版本是 2.49.0。接下来,我们需要登录我们的账户。执行此命令:

# az login

你将看到以下消息:

To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code <code> to authenticate.

按照指示通过浏览器登录。一旦成功认证了 Azure 账户,你可以返回终端并且应该已成功登录,输出结果会显示如下:

[  {
    "cloudName": "AzureCloud",
    "id": "<id>",
    "isDefault": true,
    "name": "<account name>",
    "state": "Enabled",
    "tenantId": "<tenant-it>",
    "user": {
      "name": <your-email>,
      "type": "user"
    }
  }
]

现在,我们已经准备好将容器镜像首先迁移到 Azure。

在 Azure 上创建容器注册表

首先,我们创建一个名为 animal-rg 的新资源组。在 Azure 中,资源组用于逻辑上将一组相关资源归为一类。为了获得最佳的云体验并保持低延迟,选择一个靠近您的数据中心位置非常重要。请按照以下步骤操作:

  1. 你可以使用以下命令列出所有区域:

    # az account list-locations
    

输出应如下所示:

[  {
    "displayName": "East Asia",
    "id": "/subscriptions/186760.../locations/eastasia",
    "latitude": "22.267",
    "longitude": "114.188",
    "name": "eastasia",
    "subscriptionId": null
  },
...
]

这将给你一长串所有可供选择的区域。使用名称——例如,eastasia——来标识你选择的区域。在我的例子中,我将选择 westeurope。请注意,并非所有列出的区域都有效用于资源组。

  1. 创建资源组的命令很简单;我们只需要为组指定名称和位置,如下所示:

    # az group create --name animals-rg --location westeurope{  "id": "/subscriptions/186.../resourceGroups/animals-rg",  "location": "westeurope",  "managedBy": null,  "name": "animals-rg",  "properties": {    "provisioningState": "Succeeded"  },  "tags": null,  "type": "Microsoft.Resources/resourceGroups"}
    

确保你的输出显示 "``provisioningState": "Succeeded"

注意

在生产环境中运行容器化应用时,我们希望确保能够从容器注册表中自由地下载相应的容器镜像。到目前为止,我们一直从 Docker Hub 下载镜像,但这通常是不可行的。出于安全原因,生产系统的服务器通常无法直接访问互联网,因此无法连接到 Docker Hub。让我们遵循这一最佳实践,并假设我们即将创建的 Kubernetes 集群也面临相同的限制。

那么,我们该怎么办呢?解决方案是使用一个接近我们集群并且处于相同安全上下文中的容器镜像注册表。在 Azure 中,我们可以创建一个 Azure 容器注册表ACR)实例并在其中托管我们的镜像,接下来我们将执行以下操作:

  1. 让我们首先创建一个注册表,如下所示:

    # az acr create --resource-group animals-rg \    --name <acr-name> --sku Basic
    

请注意 <acr-name> 必须是唯一的。在我的例子中,我选择了 gnsanimalsacr 这个名称。缩短后的输出如下所示:

Registration succeeded.{
  "adminUserEnabled": false,
  "creationDate": "2023-06-04T10:31:14.848776+00:00",
...
  "id": "/subscriptions/186760ad...",
  "location": "westeurope",
  "loginServer": "gnsanimalsacr.azurecr.io",
  "name": " gnsanimalsacr ",
...
  "provisioningState": "Succeeded",
  1. 成功创建容器注册表后,我们需要使用以下命令登录该注册表:

    # az acr login --name <acr-name>
    

对前述命令的响应应为:

Login Succeeded

一旦我们成功登录到 Azure 上的容器注册表,我们需要正确标记我们的容器,以便我们能够将其推送到 ACR。接下来将描述如何标记和推送镜像到 ACR。

将我们的镜像推送到 ACR

一旦成功登录到 ACR,我们可以标记我们的镜像,以便它们可以推送到注册表中。为此,我们需要知道 ACR 实例的 URL。它如下所示:

<acr-name>.azurecr.io

我们现在使用前面提到的 URL 来标记我们的镜像:

# docker image tag fundamentalsofdocker/ch11-db:2.0 \    <acr-name>.azurecr.io/db:2.0
# docker image tag fundamentalsofdocker/ch11-web:2.0 \
    <acr-name>.azurecr.io/web:2.0

然后,我们可以将其推送到我们的 ACR 实例:

# docker image push <acr-name>.azurecr.io/db:2.0# docker image push <acr-name>.azurecr.io/web:2.0

为了确认我们的镜像确实位于 ACR 实例中,我们可以使用此命令:

# az acr repository list --name <acr-name> --output table

这应该会给你以下输出:

Result--------
Db
web

事实上,我们刚刚推送的两个镜像已列出。

到此,我们已准备好创建 Kubernetes 集群。

创建一个 Kubernetes 集群

我们将再次使用自定义的 Azure CLI,运行在 Docker 容器中来创建 Kubernetes 集群。我们需要确保集群能够访问我们刚刚创建的 ACR 实例,镜像就在其中。所以,创建名为 animals-cluster 的集群,并配置两个工作节点的命令如下所示:

# az aks create \    --resource-group animals-rg \
    --name animals-cluster \
    --node-count 2 \
    --generate-ssh-keys \
    --attach-acr <acr-name>

这个命令需要一些时间,但几分钟后,我们应该会收到一份 JSON 格式的输出,包含有关新创建集群的所有详细信息。

要访问集群,我们需要 kubectl。我们可以通过以下命令轻松地在 Azure CLI 容器中安装它:

# az aks install-cli

安装了 kubectl 后,我们需要必要的凭证来使用该工具操作我们在 Azure 上的新 Kubernetes 集群。我们可以通过以下命令获取所需的凭证:

# az aks get-credentials --resource-group animals-rg \    --name animals-cluster

命令应返回以下内容:

Merged "animals-cluster" as current context in /root/.kube/config

在前面命令成功执行后,我们可以列出集群中的所有节点,如下所示:

# kubectl get nodes

这将为我们提供以下列表:

NAME STATUS ROLES AGE VERSIONaks-nodepool1-12528297-vmss000000 Ready agent 4m38s v1.25.68
aks-nodepool1-12528297-vmss000001 Ready agent 4m32s v1.25.68

正如预期的那样,我们有两个工作节点正在运行。这些节点上运行的 Kubernetes 版本是 v1.25.68

我们现在准备将应用程序部署到这个集群。在接下来的部分,我们将学习如何将应用程序部署到 Kubernetes。

将我们的应用程序部署到 Kubernetes 集群

为了部署应用程序,我们可以使用 kubectl apply 命令:

# kubectl apply -f animals.yaml

上述命令的输出应该类似于此:

deployment.apps/web createdservice/web created
deployment.apps/db created
service/db created

现在,我们要测试应用程序。记住,我们为 Web 组件创建了一个类型为 LoadBalancer 的服务。该服务将应用程序暴露到互联网。

该过程可能需要一些时间,因为 AKS 需要为此服务分配一个公共 IP 地址,这只是其中的一项任务。我们可以通过以下命令来观察:

# kubectl get service web --watch

请注意,上述命令中的 --watch 参数。它允许我们监控命令的执行进度。最初,我们应该看到类似这样的输出:

NAME   TYPE           CLUSTER-IP    EXTERNAL-IP  PORT(S)          AGEweb    LoadBalancer   10.0.38.189   <pending>    3000:32127/TCP   5s

公共 IP 地址标记为 pending。几分钟后,它应该会变成这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.8 – Microsoft AKS 上动物应用的 LoadBalancer 服务

现在我们的应用已经准备好,可以通过 IP 地址 20.76.160.79 和端口号 3000 访问。

请注意,负载均衡器将内部端口 32127 映射到外部端口 3000;这点我第一次并未注意到。

让我们来看看。在新的浏览器标签页中,访问 http://20.76.160.79:3000/pet,你应该能够看到我们熟悉的应用:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.9 – 我们的示例应用在 AKS 上运行

至此,我们已成功将分布式应用部署到 Azure 托管的 Kubernetes 上。我们无需担心安装或管理 Kubernetes,可以专注于应用本身。

请注意,你还可以通过 Azure 门户 portal.azure.com/ 管理你的 Azure 资源组、容器注册表和集群。它的界面与此类似:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.10 – 显示动物资源组的 Microsoft Azure 门户

请熟悉该门户,并尝试深入了解集群、节点和部署情况。

现在我们已经完成了应用的实验,不应忘记删除 Azure 上的所有资源,以避免产生不必要的费用。我们可以通过删除资源组来删除所有已创建的资源,操作如下:

# az group delete --name animal-rg --yes --no-wait

Azure 在容器工作负载方面有一些很有吸引力的服务,并且由于 Azure 主要提供开源的编排引擎,如 Kubernetes、Docker Swarm、DC/OS 和 Rancher,其锁定效应不像 AWS 那么明显。

从技术角度看,如果我们最初在 Azure 上运行容器化应用,后来决定迁移到其他云服务提供商,我们依然可以保持灵活性。成本应该是有限的。

注意

值得注意的是,当你删除资源组时,AKS 集群使用的Azure Active DirectoryAAD)服务主体并不会被删除。

有关如何删除服务主体的详细信息,请参阅在线帮助页面。你可以在这里找到相关信息:learn.microsoft.com/en-us/powershell/module/azuread/remove-azureadserviceprincipal?view=azureadps-2.0

接下来是 Google 的 GKE 服务。

了解 GKE

Google 是 Kubernetes 的发明者,并且至今仍是其背后的推动力。因此,你可以合理预期,Google 会提供一个吸引人的托管 Kubernetes 服务。

现在让我们快速看一下。要继续,你需要有一个 Google Cloud 账户,或者在此处创建一个测试账户:console.cloud.google.com/freetrial。请按照以下步骤操作:

  1. 在主菜单中,选择Kubernetes 引擎。第一次操作时,它会花费几分钟初始化 Kubernetes 引擎。

  2. 接下来,创建一个新项目并命名为massai-mara;这可能需要一些时间。

  3. 一旦准备好,我们可以通过点击弹出窗口中的创建集群来创建一个集群。

  4. animals-cluster上选择离你最近的区域。在作者的例子中,这是europe-west1。然后点击下一步:网络

  5. 保持所有设置为默认值,并点击下一步: 高级设置

  6. 再次保持所有设置为默认值,然后点击下一步:审核 并创建

  7. 审核你的集群设置,如果一切看起来正常,就点击创建集群,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.11 – GKE 集群创建向导的审核和创建视图

这将再次花费一些时间来为我们配置集群。

  1. 集群创建完毕后,我们可以通过点击视图右上角的云端终端图标来打开 Cloud Shell。它应该是这样显示的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.12 – 第一个 Kubernetes 集群已准备好,并且 GKE 中打开了 Cloud Shell

  1. 现在我们可以通过以下命令将实验室的 GitHub 仓库克隆到这个环境中:

    $ git clone https://github.com/PacktPublishing/The-Ultimate-Docker-Container-Book.git ~/src
    
  2. 切换到正确的文件夹,你将在其中找到示例解决方案:

    $ cd ~/src/sample-solutions/ch18/gce
    

现在你应该能在当前文件夹中找到一个animals.yaml文件,你可以使用它将animals应用部署到我们的 Kubernetes 集群中。

  1. 通过运行以下命令查看文件内容:

    $ less animals.yaml
    

它与我们在上一章中使用的相同文件几乎内容一致。两者的区别在于:

  • 我们使用LoadBalancer类型的服务(而不是NodePort)来公开web组件。请注意,我们在 Azure AKS 上也做了相同的操作。

  • 我们没有为 PostgreSQL 数据库使用卷,因为在 GKE 上正确配置StatefulSet比在 Minikube 或 Docker Desktop 这样的产品中更复杂。其结果是,如果db Pod 崩溃,我们的animals应用将不会持久化状态。如何在 GKE 上使用持久卷超出了本书的范围。

同时请注意,我们没有使用Google 容器注册表GCR)来托管容器镜像,而是直接从 Docker Hub 拉取它们。这非常简单——就像我们在关于 AKS 的章节中学到的内容一样——在 Google Cloud 中创建这样的容器注册表非常容易。

  1. 在继续之前,我们需要设置gcloudkubectl凭证。以下是我们需要执行的代码:

    $ gcloud container clusters \   get-credentials animals-cluster --zone <zone>
    

请将<zone>替换为你在第 5 步创建集群时选择的相同区域。

前面命令的响应应该是这样的:

Fetching cluster endpoint and auth data.kubeconfig entry generated for animals-cluster.
  1. 让我们通过运行以下命令查看为该集群创建了哪些节点:

    $ kubectl get nodes
    

你应该会看到类似这样的内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.13 – GCE 上的集群节点

我们可以看到在集群中创建了两个节点,并且部署的 Kubernetes 版本显然是v1.25.8

  1. 完成这些之后,是时候部署应用程序了,运行以下命令:

    $ kubectl apply -f animals.yaml
    

输出应如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.14 – 在 GKE 上部署应用程序

  1. 一旦对象创建完成,我们可以观察LoadBalancer web服务,直到它分配到一个公共 IP 地址,如下所示:

    $ kubectl get svc/web –watch
    

前面的命令输出如下:

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEweb LoadBalancer 10.57.129.72 <pending> 3000: 32384/TCP 32s
web LoadBalancer 10\. 57.129.72 35.195.160.243 3000: 32384/TCP 39s

输出中的第二行显示了负载均衡器创建仍在待处理状态时的情况,而第三行则显示了最终状态。按Ctrl + C退出–watch命令。显然,我们已经分配到了公共 IP 地址35.195.160.243,端口是3000

  1. 然后,我们可以使用这个 IP 地址并导航到http://<IP 地址>:3000/pet,我们应该会看到熟悉的动物图片。

  2. 花点时间,使用你熟悉的各种kubectl命令来分析 GKE 集群中的情况。

  3. 同时,花点时间使用 GCE 的网页门户,深入查看你的集群详情。特别是,查看集群的可观测性标签。

  4. 一旦你完成了与应用程序的交互,删除 Google Cloud 控制台中的集群和项目,以避免不必要的费用。

  5. 你可以在 Cloud Shell 中使用gcloud命令行界面来删除集群,如下所示:

    $ gcloud container clusters delete animals-cluster
    

这会花费一点时间。或者,你也可以通过网页门户进行相同的操作。

  1. 接下来列出你所有的项目,如下所示:

    $ gcloud projects list
    
  2. 接下来,你可以使用以下命令删除之前创建的项目:

    $ gcloud projects delete <project-id>
    

在这里,你应该从之前的list命令中获得正确的<project-id>值。

我们在 GKE 上创建了一个托管的 Kubernetes 集群。然后,我们使用通过 GKE 门户提供的 Cloud Shell,首先克隆我们实验室的 GitHub 仓库,然后使用kubectl工具将animals应用程序部署到 Kubernetes 集群中。

在查看托管的 Kubernetes 解决方案时,GKE 是一个极具吸引力的选择。它让你轻松启动项目,并且由于 Google 是 Kubernetes 的主要推动者,我们可以放心,始终能利用 Kubernetes 的全部功能。

总结

本章首先介绍了如何在 Amazon EKS 上使用 Fargate 创建一个完全托管的 Kubernetes 集群,并在该集群上部署一个简单的应用程序。然后,你学习了如何在 Azure AKS 上创建托管的 Kubernetes 集群,并运行animals应用程序,随后又进行了相同的操作来使用 Google 的托管 Kubernetes 解决方案——GKE。

你准备好解锁保持生产环境健康的秘密了吗?在下一章中,我们将深入探讨监控和排查在生产环境中运行的应用程序。我们将探索多种技术,用于对单个服务和整个分布式应用程序进行监控,尤其是它们在 Kubernetes 集群上运行时的情况。但这还不是全部——你还将学习如何基于关键指标创建警报。而当事情出现问题时,我们将指导你如何在不干扰集群或节点的情况下,排查运行中的应用程序。敬请期待,因为这一章将为你提供必要的工具,让你自信地在大规模环境中维护应用程序。

问题

为了评估你的知识,请回答以下问题:

  1. 列出几个你会选择托管 Kubernetes 服务(如 Amazon EKS、Microsoft 的 AKS 或 Google 的 GKE)来运行应用程序的原因。

  2. 列举使用托管 Kubernetes 解决方案(如 Amazon EKS、Azure AKS 或 Google GKE)时,考虑将容器镜像托管在相应云服务提供商的容器注册表中的两个原因。

答案

以下是本章问题的一些示例答案:

  1. 以下是考虑托管 Kubernetes 服务的一些原因:

    • 你不想,或者没有资源来安装和管理 Kubernetes 集群。

    • 你希望集中精力在为你的业务带来价值的事情上,而大多数情况下,这些事情是应该在 Kubernetes 上运行的应用程序,而不是 Kubernetes 本身。

    • 你更倾向于选择按需付费的成本模型。

    • 你的 Kubernetes 集群节点会自动修补和更新。

    • 升级 Kubernetes 版本且不产生停机时间是简单且直接的。

  2. 将容器镜像托管在云服务提供商的容器注册表(例如 Microsoft Azure 上的 ACR)的两个主要原因如下:

    • 镜像离你的 Kubernetes 集群地理位置较近,因此延迟和传输网络成本最低。

    • 生产或类似生产的集群理想情况下应该与互联网隔离,因此 Kubernetes 集群节点无法直接访问 Docker Hub。

<think>好的,用户提到Docker无法关闭,我需要详细分析可能的原因和解决方案。首先,Docker关闭问题可能涉及多个方面,比如服务未正确停止、容器仍在运行、资源占用导致无法终止,或者权限问题。 第一步,用户可能尝试了基本的命令如`systemctl stop docker`但没成功。这时候要考虑是否有容器或进程仍在运行,阻止Docker正常停止。需要检查所有容器是否已经停止,使用`docker ps -a`查看,并强制停止仍在运行的容器。 接下来,如果服务无法停止,可能是systemd的问题。可以尝试重新加载systemd配置,或者强制杀死Docker进程。但需注意强制终止可能导致数据问题,需谨慎操作。 另外,资源占用也是一个常见原因,例如Docker占用了大量内存或CPU,导致无法正常关闭。使用`top`或`htop`查看资源使用情况,必要时手动终止相关进程。 还有可能是文件锁或套接字未释放,检查`/var/run/docker.sock`是否存在,尝试删除后重启服务。此外,安装的Docker版本是否存在已知的bug,需要更新到最新版本或应用补丁。 权限问题也不能忽视,确保用户有执行管理命令的权限,使用`sudo`或者加入docker用户组。 最后,如果所有方法无效,重启系统可能是最后的解决办法,但需告知用户这会终止所有运行中的容器和服务。 总结步骤时,需要结构清晰,分点列出可能的原因和对应的解决方法,确保用户能一步步排查问题。同时提醒用户注意数据安全和备份,避免强制操作导致的数据丢失。</think>当Docker无法正常关闭时,通常由以下原因导致。请按以下步骤排查: --- ### **一、常见原因分析** 1. **容器仍在运行** Docker默认要求所有容器停止后才能关闭服务。若存在未停止的容器,会导致`docker.service`无法终止。 2. **进程或资源占用** Docker守护进程(`dockerd`)或其子进程(如`containerd`)可能因资源锁或异常占用无法退出。 3. **权限问题** 未使用`sudo`或当前用户无操作Docker的权限。 4. **系统服务依赖冲突** 某些Linux发行版中,Docker可能与其他服务(如防火墙`ufw`)存在依赖关系。 --- ### **二、逐步解决方案** #### **步骤1:强制停止所有容器** ```bash # 停止所有运行中的容器 docker stop $(docker ps -q) # 若容器无法正常停止,强制删除(谨慎操作!会丢失未持久化的数据) docker rm -f $(docker ps -a -q) ``` #### **步骤2:关闭Docker服务** ```bash # 使用systemctl尝试停止服务 sudo systemctl stop docker # 若失败,强制终止进程(极端情况) sudo pkill -9 dockerd # 杀死dockerd进程 sudo pkill -9 containerd # 杀死containerd进程 ``` #### **步骤3:检查并释放资源** ```bash # 查看Docker相关进程是否残留 ps aux | grep -E 'dockerd|containerd' # 手动卸载Docker相关模块(适用于Linux) sudo umount /var/lib/docker/overlay2/* sudo rmmod overlay ``` #### **步骤4:处理系统级依赖** ```bash # 重新加载systemd配置 sudo systemctl daemon-reload # 重启Docker套接字(某些系统需要) sudo systemctl restart docker.socket ``` #### **步骤5:排查文件锁冲突** ```bash # 检查Docker套接字文件 ls -l /var/run/docker.sock # 若文件被锁定,尝试删除后重启服务(谨慎操作!) sudo rm -f /var/run/docker.sock sudo systemctl start docker ``` --- ### **三、终极方案:系统重启** 若上述方法均无效,重启操作系统可强制终止所有Docker进程: ```bash sudo reboot ``` ⚠️ **注意**:重启会导致所有未持久化的容器数据丢失。 --- ### **四、预防措施** 1. **更新Docker版本** 老版本可能存在已知的关闭问题: ```bash sudo apt-get update && sudo apt-get upgrade docker-ce ``` 2. **配置容器自动清理** 在`/etc/docker/daemon.json`中添加: ```json { "live-restore": false, "shutdown-timeout": 30 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值