暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Kubernetes 实践

贝贝猫技术分享 2019-10-29
177


引言

月初公司内云计算团队组织了一次 Workshop,介绍了一下如何使用他们的 Kubernetes 产品,并带着我们实践了一下。感觉 Kubernetes 相较于我们现在使用的部署模式,方便了很多,虽然我们现在的系统使用场景并不适合使用 Kubernetes,但是说不定以后就能用到它了,所以我就深入的研究了一下 Kubernetes 的使用和实现机理,最终总结成本文,希望能给大家带来帮助。

介绍

背景

在过去,多数的应用都是大型单体应用,以单个进程或几个进程的方式,运行于几台服务器之上。这些应用的发布周期长,而且迭代也不频繁。每个发布周期结束前,开发者会把应用程序打包后交付给运维团队,运维人员再处理部署、监控事件,并且在硬件发生故障时手动迁移应用。今天,大型单体应用正被逐渐分解成小的、可独立运行的组件,我们称之为微服务。微服务彼此之间解耦,所以它们可以被独立开发、部署、升级、伸缩。这使得我们可以对每一个微服务实现快速迭代,并且迭代的速度可以和市场需求变化的速度保持一致。

但是,随着部署组件的增多和数据中心的增长,配置、管理并保持系统的正常运行变得越来越困难。如果我们想要获得足够高的资源利用率并降低硬件成本,把组件部署在什么地方变得越来越难以决策。手动做所有的事情,显然不太可行。我们需要一些自动化的措施,包括自动调度、配置、监管和故障处理。这正是 Kubernetes 的用武之地。

Kubernetes 使开发者可以自主部署应用,并且控制部署的频率,完全脱离运维团队的帮助。Kubernetes 同时能让运维团队监控整个系统,并且在硬件故障时重新调度应用。系统管理员的工作重心,从监管应用转移到了监管 Kubernetes,以及剩余的系统资源,因为 Kubernetes 会帮助监管所有的应用。

正如己经提到的,一个微服务架构中的组件不仅被独立部署,也被独立开发。因为它们的独立性,出现不同的团队开发不同的组件是很正常的事实,每个团队都有可能使用不同的库并在需求升级时替换它们。如图所示,因为组件之间依赖的差异性,应用程序需要同一个库的不同版本是不可避免的。部署动态链接的应用需要不同版本的共享库,或者需要其他特殊环境,在生产服务器部署并管理这种应用很快会成为运维团队的噩梦。需要在同一个主机上部署的组件数量越大,满足这些组件的所有需求就越难。为了减少这种问题,最理想的做法是让应用在开发和生产阶段可以运行在完全一样的环境下,它们有完全一样的操作系统、库、系统配置、网络环境和其他所有的条件。你也不想让这个环境随着时间推移而改变。如果可能,你想要确保在一台服务器上部署新的应用时,不会影响到机器上已有的应用。

在最近几年中,我们看到了应用在开发流程和生产运维流程中的变化。在过去,开发团队的任务是创建应用并交付给运维团队,然后运维团队部署应用并使它运行。但是现在,公司都意识到,让同一个团队参与应用的开发、部署、运维的整个生命周期更好。这意味着开发者、QA 和运维团队彼此之间的合作需要贯穿整个流程。这种实践被称为 DevOps。

正如你所看到的,Kubernetes 能让我们实现所有这些想法。通过对实际硬件做抽象,然后将自身暴露成一个平台,用于部署和运行应用程序。它允许开发者自己配置和部署应用程序,而不需要系统管理员的任何帮助,让系统管理员聚焦于保持底层基础设施运转正常的同时,不需要关注实际运行在平台上的应用程序。

容器

Kubernetes 使用 Linux 容器技术来提供应用的隔离,所以在钻研 Kubernetes 之前,需要通过熟悉容器的基本知识来更加深入地理解 Kubernetes, 包括认识到存在的容器技术分支,诸如 Docker 或者 rkt。

在前面我们看到在同一台机器上运行的不同组件需要不同的、可能存在冲突的依赖库版本,或者是其他的不同环境需求。

当一个应用程序仅由较少数量的大组件构成时,完全可以接受给每个组件分配专用的虚拟机,以及通过给每个组件提供自己的操作系统实例来隔离它们的环境。但是当这些组件开始变小且数量开始增长时,如果你不想浪费硬件资源,又想持续压低硬件成本,那就不能给每个组件配置一个虚拟机了。但是这还不仅仅是浪费硬件资源,因为每个虚拟机都需要被单独配置和管理,所以增加虚拟机的数量也就导致了人力资源的浪费,因为这增加了系统管理员的工作负担。

开发者不是使用虚拟机来隔离每个微服务环境,而是正在转向 Linux 容器技术。容器允许你在同一台机器上运行多个服务,不仅提供不同的环境给每个服务,而且将它们互相隔离。容器类似虚拟机,但开销小很多。它允许在相同的硬件上运行更多数量的组件。主要是因为每个虚拟机需要运行自己的一组系统进程,这就产生了除组件进程消耗以外的额外计算资源损耗。从另一方面说,一个容器仅仅是运行在宿主机上被隔离的单个进程,仅消耗应用容器消耗的资源,不会有其他进程的开销。当你在一台主机上运行三个虚拟机的时候,你拥有了三个完全分离的操作系统,它们运行并共享一台裸机。在那些虚拟机之下是宿主机的操作系统与一个管理程序,它将物理硬件资源分成较小部分的虚拟硬件资源,从而被每个虚拟机里的操作系统使用。运行在那些虚拟机里的应用程序会执行虚拟机操作系统的系统调用,然后虚拟机内核会通过管理程序在宿主机上的物理来 CPU 执行 x86 指令。多个容器则会完全执行运行在宿主机上的同一个内核的系统调用,此内核是唯一一个在宿主操作系统上执行 x86 指令的内核。CPU 也不需要做任何对虚拟机能做那样的虚拟化。虚拟机的主要好处是它们提供完全隔离的环境,因为每个虚拟机运行在它自己的 Linux 内核上,而容器都是调用同一个内核,这自然会有安全隐患。但是,为了在同一台机器上运行大量被隔离的进程,容器因它的低消耗而成为一个更好的选择。记住,每个虚拟机运行它自己的一组系统服务,而容器则不会,因为它们都运行在同一个操作系统上。那也就意味着运行一个容器不用像虚拟机那样要开机,它的进程可以很快被启动。

那么,容器技术是如何在同一个操作系统上隔离各个进程的呢。有两个机制可用:第一个是 Linux 命名空间,它使每个进程只看到它自己的系统视图(文件、进程、网络接口、主机名等);第二个是 Linux 控制组(cgroups), 它限制了进程能使用的资源量(CPU、 内存、 网络带宽等)。

Kubernetes

Kubernetes 是一个软件系统,它允许你在其上很容易地部署和管理容器化的应用。它依赖于 Linux 容器的特性来运行异构应用,而无须知道这些应用的内部详情,也不需要手动将这些应用部署到每台机器。Kubernetes 使你在数以千计的电脑节点上运行软件时就像所有这些节点是单个大节点一样。它将底层基础设施抽象,这样做同时简化了应用的开发、部署,以及对开发和运维团队的管理。

下图展示了一幅最简单的 Kubernetes 系统图。整个系统由 一个主节点和若干个工作节点组成。开发者把一个应用列表提交到主节点,Kubernetes 会将它们部署到集群的工作节点。组件被部署在哪个节点对于开发者和系统管理员来说都不用关心。开发者能指定一些应用必须一起运行,Kubernetes 将会在一个工作节点上部署它们。其他的将被分散部署到集群中,但是不管部署在哪儿,它们都能以相同的方式互相通信。

Kubernetes 可以被当作集群的一个操作系统来看待。它降低了开发者不得不在他们的应用里实现一些和基础设施相关服务的心智负担。他们现在依赖于 Kubernetes 来提供这些服务,包括服务发现、扩容、负载均衡、自恢复,甚至领导者的选举。Kubernetes 还能在任何时间迁移应用并通过混合和匹配应用来获得比手动调度高很多的资源利用率。

我们已经知道了 Kubernetes 的工作方式,现在让我们近距离看一下 Kubernetes 集群的组成。在硬件层面,Kubernetes 集群由两种节点组成:

  • 主节点:它承载着 Kubernetes 控制和管理整个集群系统的控制面板

  • 工作节点:它们运行用户实际部署的应用

控制面板用于控制集群并使它工作。它包含多个组件,组件可以运行在单个主节点上或者通过副本分别部署在多个主节点以确保高可用性。这些组件是:

  • Kubernetes API 服务器:你和其他控制面板组件都要和它通信

  • Scheduler:它调度你的应用(为应用的每个可部署组件分配一个工作节点〕

  • Controller Manager:它执行集群级别的功能,如复制组件、持续跟踪工作节点、处理节点失败等

  • etcd:一个可靠的分布式数据存储,它能持久化存储集群配置

控制面板的组件持有并控制集群状态,但是它们不运行你的应用程序。这是由工作节点完成的。工作节点是运行容器化应用的机器。运行、监控和管理应用服务的任务是由以下组件完成的:

  • 容器:Docker、rtk 或其他的容器

  • Kubelet:它与 API 服务器通信,并管理它所在节点的容器

  • Kubernetes Service Proxy (kube-proxy),它负责组件之间的负载均衡网络流量

为了在 Kubernetes 中运行应用,首先需要将应用打包进一个或多个容器镜像,再将那些镜像推送到镜像仓库,然后将应用的描述发布到 Kubernetes API 服务器。

该描述包括诸如容器镜像或者包含应用程序组件的容器镜像、这些组件如何相互关联,以及哪些组件需要同时运行在同一个节点上和哪些组件不需要同时运行等信息。此外,该描述还包括哪些组件为内部或外部客户提供服务且应该通过单个 IP 地址暴露,并使其他组件可以发现。

当 API 服务器处理应用的描述时,调度器调度指定组(Pod)的容器到可用的工作节点上,调度是基于每组(Pod)所需的计算资源,以及调度时每个节点未分配的资源。然后,那些节点上的 Kubelet 指示容器运行时(例如 Docker)拉取所需的镜像并运行容器。这里提到的"组"概念,在 Kubernetes 中被称为 Pod,同一个 Pod 可以包含多个镜像,但是同一个 Pod 中的所有进程就如同运行在同一个物理机一样相互可以看见,它们之间未做隔离。

一旦应用程序运行起来,Kubernetes 就会不断地确认应用程序的部署状态始终与你提供的描述相匹配。例如,如果你指出你需要运行五个 web 服务器实例,那么 Kubernetes 总是保持正好运行五个实例。如果实例之一停止了正常工作,比如当进程崩溃或停止响应时,Kubernetes 将自动重启它。当应用程序运行时,可以决定要增加或减少副本量,而 Kubernetes 将分别增加附加的或停止多余的副本。甚至可以把决定最佳副本数目的工作交给 Kubernetes。它可以根据实时指标(如 CPU 负载、内存消耗、每秒查询或应用程序公开的任何其他指标)自动调整副本数。

我们已经说过,Kubernetes 中的容器可能是动态的,它随时可能从一台机器上被销毁,然后在另一台机器上重新启动。那么当容器在集群内频繁调度时,它们该如何正确使用这个容器?当这些容器被复制并分布在整个集群中时,客户端如何连接到提供服务的容器呢?

为了让客户能够轻松地找到提供特定服务的容器,可以告诉 Kubernetes 哪些容 器提供相同的服务,而 Kubernetes 将通过一个静态 IP 地址暴露所有容器,并将该地址暴露给集群中运行的所有应用程序。您可以通过环境变量或者 DNS 服务来共享服务 IP。kube-proxy 将确保到服务能连接到对应的容器上。

Kubernetes 的优点:

  • 简化应用程序部署

  • 更好地利用硬件

  • 健康检查和自修复

  • 自动扩容

试玩环节

在深入学习 Kubernetes 的概念之前,先来看看如何创建一个简单的应用,把它打包成容器镜像并在远端的 Kubernetes 集群中运行。这会对整个 Kubernetes 体系有较好的了解,并且会让接下来几个章节对 Kubernetes 基本概念的学习变得简单。

Docker

正如在之前章节所介绍的,在 Kubernetes 中运行应用需要打包好的容器镜像。本节将会对 Docker 的使用做简单的介绍。接下来将会介绍:

  1. 安装 Docker 并运行第一个 “Hello world” 容器

  2. 创建一个简单的 Node.js 应用并部署在 Docker 中

  3. 把应用打包成可以独立运行的容器镜像

  4. 基于镜像运行容器

  5. 把镜像推送到 DockerHub,这样任何人在任何地方都可以使用

首先,我们需要在 Linux 主机上安装 Docker,我使用了公司内部的云平台,申请了一台虚拟机,因为之后打算用它来做 Kubernetes 集群的节点(Kubernetes 的 master 节点需要至少 2 核 cpu),所以我选用的虚拟机配置如下:

  • CPU:4 核

  • 内存:4G

  • 硬盘:60 GB SSD

  • 系统:CentOS 7.6

第一步,安装并启动 Docker,并运行一个 busybox 的 “Hello world” 容器:

    # 使用 yum 安装 docker
    sudo yum install docker-1.13.1
    # 启动 docker
    sudo systemctl start docker
    # 修改本地 docker 监听的文件的权限
    sudo chmod 777 var/run/docker.sock
    # 运行 busybox 容器
    docker run busybox echo "Hello world"

    busybox 是一个单一可执行文件,包含多种标准 UNIX 命令行工具,如: echo、ls、 gzip 等。在这一步中,我们仅通过一条命令,就下载运行了一个完整的"应用",而不用做其他事情。其背后的原理是:首先,Docker 会检查 busybox:latest 镜像是否己经存在于本机。如果没有,Docker 会从 http://docker.io的 Docker 镜像中心拉取镜像。镜像下载到本机之后,Docker 基于这个镜像创建一个容器并在容器中运行命令。echo 命令打印文字到标准输出流,然后进程终止,容器停止运行。运行其他容器镜像和上述过程是一样的,您只需要执行docker run <image>
    即可。Docker 使用 tag 来表示同一个镜像的不同版本,默认情况下会使用最新的镜像,当您想要执行指定版本的镜像时,可以执行docker run <image>:<tag>

    接下来,创建一个简单的 Node.js 应用并打包部署在 Docker 中:

    接下来我们将构建一个简单的 web 应用,该应用会将自己的主机名作为 HTTP 请求的响应内容。

      // app.js
      const http = require('http');
      const os = require ('os');
      console.log("Kubi a server starting ... ");
      var handler = function(request, response) {
      console.log("Recei ved request from " + request.connection.remoteAddress);
      response.writeHead(200);
      response.end("You've hit " + os.hostname() + "\n");
      };
      var www = http.createServer(handler);
      www.listen(8080);

      为了将上述的 node 应用打包成镜像,我们还需要在 app.js 的相同路径下创建一个 Dockerfile 文件,它包含了构建镜像的指令:

        FROM node:7
        ADD app.js app.js
        ENTRYPOINT ["node", "app.js"]

        From 行定义了镜像的起始内容(构建所基于的基础镜像)。是 node 镜像的 tag 7 版本。第二行中把 app.js 文件从本地文件夹添加到镜像的根目录,保持 app.js 这个文件名。最后一行定义了当镜像被运行时需要被执行的命令,这个例子中,命令是 node app.js

        构建好 app.js 和 Dockerfile 后,只需要执行docker build -t kubia .
        ,Docker 就会自动帮我们把镜像创建出来。镜像不是一个大的二进制块,而是由多层组成的,不同镜像可能会共享分层,这会让存储和传输变得更加高效。你或许会认为每个 Dockerfile 只创建一个新层,但是并不是这样的。构建镜像时,Dockerfile 中每一条单独的指令都会创建一个新层。下图就展示了 Docker 的分层思想,其中也展示了一个名 other:last 的镜像,它就和我们正在构建的镜像共享了部分分层。构建完成后,新的镜像会存储在本地,我们可以使用docker images
        查看刚才构建好的镜像, 可以通过如下命令运行刚才构建出来的镜像:

          docker run --name kubia-container -p 8080:8080 -d kubia
          # 返回 container id
          curl http://localhost:8080
          # 查看运行中的容器
          docker ps
          # 查看更详细的容器描述
          docker inspect kubia-container

          这条命令告知 Docker 基于 kubia 镜像创建一个叫 kubia-container 的新容器。这个容器与命令行分离(-d 标志),这意味着在后台运行。本机上的 8080 端口会被映射到容器内的 8800 端口(-p 8080:8080),所以可以通过 http://localhost:8080 访问这个应用。通过执行docker ps
          我们可以发现,容器内的主机名实际上就是容器 ID。

          我们可以通过如下命令进入到容器内部,该命令中 -i 确保准输入流保持开放。需要在 shell 中输入命令。-t, 分配一个伪终端(TTY)。

            # 进入容器内部
            docker exec -it kubia-container bash
            # 查看容器内进程
            ps -aux
            # 查看容器内文件系统
            ls
            # 退出容器 shell
            exit
            # 查看宿主机是否存在容器内的进程
            ps -aux|grep app.js
            # 停止容器
            docker stop kubia-container
            # 删除容器
            docker rm kubia-container

            通过对比容器内的进程和宿主机进程,你就可以证明运行在容器中的进程是运行在主机操作系统上的。如果你足够敏锐,会发现进程的 ID 在容器中与主机上不同。容器使用独立的 PID Linux 命名空间并且有着独立的系列号,完全独立于进程树。正如拥有独立的进程树一样,每个容器也拥有独立的文件系统。在容器内列出根目录的内容,只会展示容器内的文件,包括镜像内的所有文件,再加上容器运行时创建的任何文件。

            最后,我们试一试将镜像推送到镜像仓库中:

              # 定义镜像的新 tag,<docker hub id>/kubia
              docker tag kubia beikejiedeliulangmao/kubia
              # 登录 docker hub io,并输入账号密码
              docker login
              # 推送本地镜像到 docker 镜像仓库中
              docker push beikejiedeliulangmao/kubia

              如果您和我一样上述操作都执行成功的话,应该就能在 docker hub 中看到刚才提交的镜像了,最后我们确认一下直接从仓库下载并运行镜像的流程是否通畅:

                # 先删除本地的镜像,确保后面的指令从镜像仓库下载镜像,dcb98c6ae282 是我这里的镜像 id
                docker image rm dcb98c6ae282 -f
                # 运行 docker 仓库中的镜像
                docker run -p 8080:8080 -d beikejiedeliulangmao/kubia
                # 测试
                curl http://localhost:8080

                很棒,一切都运转正常,以后应用无论在哪运行,都能确保在完全一致的环境中。我们不用再关心主机是否安装了应用下层依赖的库(Node.js),因为当应用启动后,只会使用镜像内部的 Node。上述的流程,就会是日后使用 Kubernetes 发布应用的例行工作,开发完成-> 构建新的镜像 -> 提交到镜像仓库 -> 使用 Kubernetes 发布最新镜像中的应用。

                Kubernetes 集群

                现在,应用被打包在一个容器镜像中,并通过 Docker Hub 给 Kubernetes 使用。但是,我们还没有自己的 Kubernetes 集群,所以,在进行 Kubernetes 发布应用的试玩环节前,我们先构建一个自己的 Kubernetes 集群。为了让本文更具实际指导意义,我们将构建一个多节点的 Kubernetes 集群,而不是使用第三方云平台的工具或者 Minikube 这样的单机构建工具。

                我们的目标集群规模如下:

                • Master:3 台

                • Etcd:3 台

                • Worker:3 台

                为了达到本节的试玩目标,我又申请了 8 台虚拟机,它们的配置都和 Docker 试玩时使用的虚拟机配置相同。因为我们的云服务并不提供服务器镜像功能,所以准备后机器后,我分别在每台机器上执行了如下准备命令:

                  # 切换到 root 身份
                  sudo -i
                  # 临时性地禁用 SELinux,因为开着它会出现一些权限问题,解决起来很麻烦,所以指导手册上都建议关闭
                  setenforce 0
                  # 修改系统配置文件永久禁用 SELinux, 修改 SeLinux 配置为 SELINUX=permissive, 因为我们的虚拟机默认都是关闭该组件,所以我这里跳过该过程
                  sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' etc/selinux/config
                  # 为了避免遇到防火墙相关问题,禁用防火墙
                  systemctl disable firewalld && systemctl stop firewalld
                  # 在 yum 仓库中添加 Kubernetes
                  cat <<EOF > etc/yum.repos.d/kubernetes.repo
                  [kubernetes]
                  name=Kubernetes
                  baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
                  enabled=1
                  gpgcheck=1
                  repo_gpgcheck=1
                  gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
                  EOF
                  # 安装各种组件:容器运行时,Kubernetes 节点代理,集群管理工具,交互式命令行工具,网络接口插件
                  yum install -y docker-1.13.1 kubelet-1.16.0 kubeadm-1.16.0 kubectl-1.16.0 kubernetes-cni-0.7.5
                  # 开启 docker
                  systemctl enable docker && systemctl start docker
                  # 开启节点代理
                  systemctl enable kubelet && systemctl start kubelet
                  # 为了确保 iptable 的使用设置相关参数
                  cat <<EOF > etc/sysctl.d/k8s.conf
                  net.bridge.bridge-nf-call-ip6tables = 1
                  net.ipv4.ip_forward = 1
                  net.bridge.bridge-nf-call-iptables = 1
                  EOF
                  sysctl --system
                  # 禁用交换分区
                  swapoff -a && sed -i '/swap/s/^/#/' etc/fstab

                  因为默认 Kubernetes 集群的 etcd 是和控制节点运行在一起的,这样当任意一台控制节点宕机时,也就意味着一个 etcd 节点也会宕机,为了让整个集群更加高可用,所以本例中,我们配置并搭设独立的 etcd 集群,这两种方案的对比可以参考官方文档[1]。不过好在 kubeadm 中包含了搭建 etcd 集群的基本工具,我们可以快速的构建起独立的 etcd 集群。

                  搭建 etcd 集群

                  为了搭建独立的 etcd 集群,我们需要重新配置 kubelet 的参数:

                    mkdir etc/systemd/system/kubelet.service.d/
                    cat << EOF > etc/systemd/system/kubelet.service.d/20-etcd-service-manager.conf
                    [Service]
                    ExecStart=
                    # Replace "systemd" with the cgroup driver of your container runtime. The default value in the kubelet is "cgroupfs".
                    ExecStart=/usr/bin/kubelet --address=127.0.0.1 --pod-manifest-path=/etc/kubernetes/manifests --cgroup-driver=systemd
                    Restart=always
                    EOF


                    systemctl daemon-reload
                    systemctl restart kubelet

                    然后,我们使用如下脚本给 kubeadm 创建配置文件:

                      # 直接在命令行执行即可不需要放入脚本文件执行
                      # Update HOST0, HOST1, and HOST2 with the IPs or resolvable names of your hosts
                      export HOST0=10.x.x.x
                      export HOST1=10.x.x.x
                      export HOST2=10.x.x.x


                      # Create temp directories to store files that will end up on other hosts.
                      mkdir -p tmp/${HOST0}/ tmp/${HOST1}/ /tmp/${HOST2}/


                      ETCDHOSTS=(${HOST0} ${HOST1} ${HOST2})
                      NAMES=("infra0" "infra1" "infra2")


                      for i in "${!ETCDHOSTS[@]}"; do
                      HOST=${ETCDHOSTS[$i]}
                      NAME=${NAMES[$i]}
                      cat << EOF > /tmp/${HOST}/kubeadmcfg.yaml
                      apiVersion: "kubeadm.k8s.io/v1beta2"
                      kind: ClusterConfiguration
                      etcd:
                      local:
                      serverCertSANs:
                      - "${HOST}"
                      peerCertSANs:
                      - "${HOST}"
                      extraArgs:
                      initial-cluster: ${NAMES[0]}=https://${ETCDHOSTS[0]}:2380,${NAMES[1]}=https://${ETCDHOSTS[1]}:2380,${NAMES[2]}=https://${ETCDHOSTS[2]}:2380
                      initial-cluster-state: new
                      name: ${NAME}
                      listen-peer-urls: https://${HOST}:2380
                      listen-client-urls: https://${HOST}:2379
                      advertise-client-urls: https://${HOST}:2379
                      initial-advertise-peer-urls: https://${HOST}:2380
                      EOF
                      done

                      如果您没有现成的身份认证秘钥对,就需要像我一样自己给 etcd 集群创建秘钥,我们可以在任意 etcd 节点上通过 kubeadm 创建,命令为kubeadm init phase certs etcd-ca
                      ,它会自动创建如下两个秘钥对:

                      • /etc/kubernetes/pki/etcd/ca.crt

                      • /etc/kubernetes/pki/etcd/ca.key

                      接下来,我们需要将该秘钥拷贝到其他 etcd 节点上去,因为内部服务器不能直接使用 scp 拷贝文件,所以我手动将生成的秘钥对文件分别复制到了另外两台机器的相同路径。

                      准备就绪后,我们要在这三台机器上分别启动 etcd。

                        # 机器 1
                        kubeadm init phase certs etcd-server --config=/tmp/${HOST0}/kubeadmcfg.yaml
                        kubeadm init phase certs etcd-peer --config=/tmp/${HOST0}/kubeadmcfg.yaml
                        kubeadm init phase certs etcd-healthcheck-client --config=/tmp/${HOST0}/kubeadmcfg.yaml
                        kubeadm init phase certs apiserver-etcd-client --config=/tmp/${HOST0}/kubeadmcfg.yaml
                        # 机器 2
                        kubeadm init phase certs etcd-server --config=/tmp/${HOST1}/kubeadmcfg.yaml
                        kubeadm init phase certs etcd-peer --config=/tmp/${HOST1}/kubeadmcfg.yaml
                        kubeadm init phase certs etcd-healthcheck-client --config=/tmp/${HOST1}/kubeadmcfg.yaml
                        kubeadm init phase certs apiserver-etcd-client --config=/tmp/${HOST1}/kubeadmcfg.yaml
                        # 机器 3
                        kubeadm init phase certs etcd-server --config=/tmp/${HOST2}/kubeadmcfg.yaml
                        kubeadm init phase certs etcd-peer --config=/tmp/${HOST2}/kubeadmcfg.yaml
                        kubeadm init phase certs etcd-healthcheck-client --config=/tmp/${HOST2}/kubeadmcfg.yaml
                        kubeadm init phase certs apiserver-etcd-client --config=/tmp/${HOST2}/kubeadmcfg.yaml

                        当上述操作都顺利执行完毕后,确认一下各个 etcd 节点上的文件是否都型如下图,其中 HOST0 在每个机器上应该对应为该机器的 IP 地址:最后,在每台机器上生成静态 pod 清单:

                          # 机器 1,HOST0 是机器 1 的 ip 地址,下同
                          kubeadm init phase etcd local --config=/tmp/${HOST0}/kubeadmcfg.yaml
                          #机器 2
                          kubeadm init phase etcd local --config=/tmp/${HOST1}/kubeadmcfg.yaml
                          # 机器 3
                          kubeadm init phase etcd local --config=/tmp/${HOST2}/kubeadmcfg.yaml

                          这里大家可能会不太懂这个静态 pod 清单的意义。实际上,当 Kubernetes 的 kubelet 组件启动后,会监视本机的所有静态 pod 清单,并自动启动这些静态 pod 对应的容器,换言之,这些 etcd 节点上的 etcd 进程实际上是由 Kubernetes 托管的,它会保证每个节点上的 etcd 正常的在容器中运行,当前节点的 etcd 意外关闭时,kubelet 会自动恢复它。你可以大胆的通过 docker stop
                          来关闭任意节点上的 etcd 容器,当你再执行 docker ps
                          时,你就会发现当前节点的 etcd 实例已经被自动重启了。

                          此外,我们还可以通过 etcdctl 来测试 etcd 的可用性:

                            # 下面的 HOST0 需要修改为准备执行下列脚本的机器 IP
                            docker run --rm -it \
                            --net host \
                            -v /etc/kubernetes:/etc/kubernetes quay.io/coreos/etcd:v3.2.24 etcdctl \
                            --cert-file /etc/kubernetes/pki/etcd/peer.crt \
                            --key-file /etc/kubernetes/pki/etcd/peer.key \
                            --ca-file /etc/kubernetes/pki/etcd/ca.crt \
                            --endpoints https://${HOST0}:2379 cluster-health
                            # 当集群启动后,您可以通过如下命令确认 Kubernetes 都在 etcd 中存储了什么内容
                            docker run --rm -it -e "ETCDCTL_API=3" \
                            --net host \
                            -v /etc/kubernetes:/etc/kubernetes quay.io/coreos/etcd:v3.2.24 etcdctl \
                            --cert=/etc/kubernetes/pki/etcd/peer.crt \
                            --key=/etc/kubernetes/pki/etcd/peer.key \
                            --cacert=/etc/kubernetes/pki/etcd/ca.crt \
                            --endpoints=[https://${HOST0}:2379] get / --prefix --keys-only

                            如果上述过程都正常完成的话,就说明 etcd 集群已经搭建好了,接下来我们搭建 Kubernetes 集群。

                            搭建控制节点

                            在这一步,我们得先把 etcd 服务的证书和 etcd 接口通讯的证书和秘钥拷贝到其中一个控制节点上,拷贝到一台就够了,其他控制节点在加入集群时会自动获取。要拷贝的文件列表如下:

                            • etcd 服务证书:/etc/kubernetes/pki/etcd/ca.crt

                            • etcd 接口证书:/etc/kubernetes/pki/apiserver-etcd-client.crt

                            • etcd 接口秘钥:/etc/kubernetes/pki/apiserver-etcd-client.key

                            证书和秘钥准备完毕后,我们需要创建如下配置文件 kubeadm-config.yaml:

                              # 配置文件中 LOAD_BALANCER_DNS 和 LOAD_BALANCER_PORT 需要修改为您为控制节点申请的 DNS(dns 包含所有控制节点的 ip)和各个控制节点 api server 使用的端口(默认:6443),ETCD_X_IP 改为 etcd 节点的 ip
                              apiVersion: kubeadm.k8s.io/v1beta2
                              kind: ClusterConfiguration
                              kubernetesVersion: stable
                              controlPlaneEndpoint: "LOAD_BALANCER_DNS:LOAD_BALANCER_PORT"
                              etcd:
                              external:
                              endpoints:
                              - https://ETCD_0_IP:2379
                              - https://ETCD_1_IP:2379
                              - https://ETCD_2_IP:2379
                              caFile: /etc/kubernetes/pki/etcd/ca.crt
                              certFile: /etc/kubernetes/pki/apiserver-etcd-client.crt
                              keyFile: /etc/kubernetes/pki/apiserver-etcd-client.key
                              # 填写 `podSubnet` 和 `serviceSubnet` 时一定要注意避开节点的 host ip 域(我这是 10.0.0.0/8,如果不避开该域会导致无法登陆主机) 和 docker 的 ip 域(172.17.0.1/16),同时它们之间也要互不冲突
                              networking:
                              serviceSubnet: "YOUR_SERVICE_SUBNET"
                              podSubnet: "YOUR_POD_SUBNET"

                              修改好配置文件之后,我们通过 kubeadm 的初始化命令kubeadm init --config kubeadm-config.yaml --upload-certs
                              启动首个控制节点。在该命令的返回日志中,会包含命令行工具的验证文件,其他控制节点加入集群的命令,以及工作节点加入集群的命令,您需要将这些命令保存起来方便以后使用,它们的格式大致如下:

                                # 命令行工具验证文件配置
                                mkdir -p $HOME/.kube
                                sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
                                sudo chown $(id -u):$(id -g) $HOME/.kube/config
                                # 控制节点接入命令,需要在其他控制节点执行
                                kubeadm join <LOAD_BALANCER_DNS>:<LOAD_BALANCER_PORT> --token 0ur873.o4zl4fldg7ewrce0 \
                                --discovery-token-ca-cert-hash sha256:7bc51f3ec0f250f3e1e0d812fe61fe9a0a3d94abd66a4971f780ca2a6cbf88bb \
                                --control-plane --certificate-key 28806632940988ed0ed67c68c763485c9a5bb4df43e7860849702419cec71c48
                                # 工作节点接入命令,需要在所有工作节点执行
                                kubeadm join <LOAD_BALANCER_DNS>:<LOAD_BALANCER_PORT> --token 0ur873.o4zl4fldg7ewrce0 \
                                --discovery-token-ca-cert-hash sha256:7bc51f3ec0f250f3e1e0d812fe61fe9a0a3d94abd66a4971f780ca2a6cbf88bb

                                配置好命令行工具的验证文件后,还需要在首个控制节点应用网络插件,您可以通过如下命令做到:

                                  # 先下载网络插件(flannel)的配置文件
                                  wget https://raw.githubusercontent.com/coreos/flannel/b30e6895ef6429d47eaee1a72047ca349b3b5de3/Documentation/kube-flannel.yml
                                  # 修改插件配置文件,加入 cniVersion 的描述,如果不加的话会导致网络插件启动报错
                                  sed -i '/"name": "cbr0"/s/$/\n "cniVersion":"0.3.1",/' kube-flannel.yml
                                  # 除了 cniVersion 之外还要将插件配置文件中 net-conf.json 的 Network 改成您之前设置的 podSubnet(别忘了用\转义'/'),否则在 pod 内部无法访问其他 pod
                                  sed -i 's/"Network": "10.244.0.0\/16"/"Network": "<YOUR_POD_SUBNET>"/g' kube-flannel.yml
                                  # 应用插件配置
                                  kubectl create -f kube-flannel.yml

                                  配置好网络插件后,你就可以在其他节点上按照前面保存的接入命令分别进行节点接入,最后您可以将控制节点的 /etc/kubernetes/admin.conf
                                  拷贝到本地的 $HOME/.kube/config
                                  ,这样您就可以在本地操作整个集群了。配置好 Kubernetes 命令行环境后,可以通过下列命令确认整个集群的运转情况:

                                    # 查看集群中的节点以及状态,正常情况下应该看到各个节点的状态都是 Ready
                                    kubectl get nodes -o wide
                                    # 查看集群中运行的 Pod,正常情况下每个节点都有kube-flannel,kube-proxy,此外控制节点额外有kube-scheduler,kube-controller,kube-apiserver,最后还有2个 coredns
                                    kubectl get pods -A -o wide
                                    # 其中,kube-flannel 这个网络插件负责不同主机 pod 之间的通讯,您可以看到虚拟网卡 flannel.1 的 ip 段和之前配置的 pod-subnet 相同,如果您在 worker 节点执行 ifconfig 则还会看到一个 cni0 网卡,它负责同一个节点内 pod 之间的网络通讯,该网卡的 ip 域实际上就是 host 该节点上分配到的 pod 网段
                                    ifconfig
                                    # 如果最终的效果和本文的描述有出入,可以通过下述命令进行排查
                                    journalctl -xu kubelet.service -f
                                    # 因为我的 log 里会报一个 'Failed to get system container stats for "/system.slice/docker.service": failed to get cgroup stats for "/system.slice/docker.service"' 的错误,所以还执行了下列命令
                                    sed -i '/KUBELET_EXTRA_ARGS=/s/$/--runtime-cgroups=\/systemd\/system.slice --kubelet-cgroups=\/systemd\/system.slice/' /etc/sysconfig/kubelet
                                    # 上述命令在 /etc/sysconfig/kubelet 末尾增加了一句--runtime-cgroups=/systemd/system.slice --kubelet-cgroups=/systemd/system.slice
                                    # 最后重启 kubelet 就能解决上述报错
                                    systemctl restart kubelet

                                    部署应用

                                    配置好 Kubernetes 集群后,我们就通过之前 Docker 试玩环节使用的 Node Demo kubia
                                    来体验一下如何在 Kubernetes 中部署应用。这里我们先通过 ReplicationController 接口来启动该应用,首先我们要创建一个配置文件 rc.yaml:

                                      apiVersion: v1
                                      kind: ReplicationController
                                      metadata:
                                      name: kubia
                                      spec:
                                      replicas: 3 # 同时运行 3 个服务
                                      selector:
                                      app: kubia
                                      template:
                                      metadata:
                                      name: kubia
                                      labels:
                                      app: kubia
                                      spec:
                                      containers:
                                      - name: kubia
                                      image: beikejiedeliulangmao/kubia # 镜像
                                      ports:
                                      - containerPort: 8080

                                      准备好配置文件之后,我们通过命令行工具来注册该 ReplicationController:

                                        kubectl apply -f rc.yaml

                                        如果上述命令正常执行,您将像我一样创建了一个名为 kubia
                                        的 ReplicationController,它会帮助我们维持整个集群恰好有 3 个 kubia 服务(之前的 Node 服务),您可以通过 kubectl get pod -o wide
                                        查看 ReplicationController 为我们创建的 Pod,您可以删除任意一个 Pod 来模拟服务中崩溃,当你再次查看运行中的 pod 时,你会发现 ReplicationController 帮我们又重建了一个新的 Pod。

                                        一个 Pod 是一组紧密相关的容器,它们总是一起运行在同一个工作节点上,以及同一个 Linux 命名空间中。每个 pod 就像一个独立的逻辑机器,拥有自己的 IP、主机名、进程等,运行一个独立的应用程序。应用程序可以是单个进程,运行在单个容器中,也可以是一个主应用进程或者其他支持进程,每个进程都在自己的容器中运行。一个 pod 的所有容器都运行在同一个逻辑机器上,而其他 pod 中的容器,即使运行在同一个工作节点上,也会出现在不同的节点上。当运行 kubectl 命令时,它通过向 Kubernetes API 服务器发送一个 REST HTTP 请求来创建一个 ReplicationController,ReplicationController 创建成功后,会根据配置文件中指定的 Pod 个数,在集群中创建一个相应数量 Pod 对象。然后,调度器将其调度到一个工作节点上。随后,该工作节点上的 Kubelet 看到 pod 被调度到自己身上,就告知 Docker 从镜像中心中拉取指定的镜像,因为本地没有该镜像。下载镜像后,Docker 创建并运行容器。那么,既然服务已经运行起来了,我们怎么访问该服务呢?我们提到过每个 Pod 都有自己的 IP,但是 Pod 可能随时都会崩溃,为了让集群内部的其他服务能够稳定的访问前面创建的 kubia,我们还需要创建一个服务对象,来暴露刚才的 kubia
                                        Pods,这里我们创建一个 ClusterIP 类型的 service,它会在之前配置的 serviceSubnet 网络中暴露服务。

                                          # 创建 service 对象
                                          kubectl expose rc kubia --type=NodePort --name kubia
                                          # 查看 service 对象
                                          kubectl get service

                                          通过上述命令,我们就会看到 kubernetes 为我们创建的 service ip(又名 CLUSTER-IP),您可以在集群中的任意机器中通过 curl <YOUR_SERVICE_IP>:8080
                                          访问刚才创建的服务,kubernetes 会自动帮我们做负载均衡,将请求转发到不同的 Pod 中。

                                          您可能已经注意到了,这时候的 service ip 仍然是只能集群内访问的虚拟 ip,它只是能保证无论 pod 何时崩溃,我们都能通过恒定不变的 service ip 访问其他可用的 kubia 服务。那么如何让集群之外的机器也能访问 kubia 服务呢?有一种方法是创建 LoadBalance 类型的 service,这需要云服务提供商开放一定的 LoadBalance 服务,让 Kubernetes 集群可以自主创建和修改 VIP。不过我所使用的云服务并不提供该功能,所以我手动创建了 VIP,并将 VIP 的 target 指向了所有的节点(master 和 worker),因为前面创建 service 时,使用的 service 类型是 NodePort,Kubernetes 会在所有节点上找一个相同的端口暴露服务,我在手动创建 VIP 时就指定了该端口。

                                          到此为止,我们就构建出了一个稳定可靠的服务集群,并且外部网络就能访问我们的服务。现在让我们来创造更多魔法。

                                          使用 Kubernetes 的一个主要好处是可以简单地扩展部署。让我们看看扩容 pod 有多容易。

                                            # 将 kubia 服务扩容为 6 个 pod
                                            kubectl scale rc kubia --replicas=6

                                            现在已经告诉 Kubernetes 需要确保 pod 始终有六个实例在运行。注意,你没有告诉 Kubernetes 需要采取什么行动,也没有告诉 Kubernetes 增加三个 pod, 只设置新的期望的实例数量并让 Kubernetes 决定需要采取哪些操作来实现期望的状态。

                                            这是 Kubernetes 最基本的原则之一。不是告诉 Kubernetes 应该执行什么操作,而是声明性地改变系统的期望状态,并让 Kubernetes 检查当前的状态是否与期望的状态一致,如果不一致的话,它会自动的进行处理。在整个 Kubernetes 世界中都是这样的。

                                            我们可以查看一下现在的 kubia 服务是否如我们所愿,变成了六个 pod。

                                              # 确认 ReplicationController 的状态
                                              kubectl get rc
                                              # 查看 pod 您可以看到每个 pod 都运行在哪个工作节点上
                                              kubectl get pod -o wide

                                              在试玩环节的最后,让我们看看探索 Kubernetes 集群的另一种方式。到目前为止,我们只使用了 kubectl 命令行工具。实际上 Kubernetes 也提供了一个图形化的 web 用户界面,接下来我们就使用一下它,首先你可以通过如下命令启动 dashboard 功能。

                                                # 启动 dashboard 服务
                                                kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta4/aio/deploy/recommended.yaml
                                                # 打开 dashboard 服务代理,这样我们就可以从本地访问 dashboard 服务
                                                kubectl proxy

                                                成功执行上述命令后,您就可以打开 本地 Dashboard 页面[2]看来是要登录,那么怎么获取登录的信息呢?您可以通过如下命令创建账号并获取 token:

                                                  # 创建账号
                                                  kubectl create serviceaccount cluster-admin-dashboard-sa
                                                  # 赋予权限
                                                  kubectl create clusterrolebinding cluster-admin-dashboard-sa \
                                                  --clusterrole=cluster-admin \
                                                  --serviceaccount=default:cluster-admin-dashboard-sa
                                                  # 获取 token
                                                  kubectl describe secret $(kubectl get secret | grep cluster-admin-dashboard-sa | awk '{print $1}')

                                                  将上述命令获取的 token 输入到登录页面,就能访问 dashboard 的主页面啦!

                                                  常用功能介绍

                                                  上一章已经大致介绍了在 Kubernetes 中创建的基本组件,包括他们的基本功能概述。那么接下来我们将更加详细地介绍所有类型的 Kubernetes 功能(又称对象或资源),以便你理解在何时、如何及为何要使用每一个对象。

                                                  Pod

                                                  我们已经了解到,pod 是一组并置的容器,它是 Kubernetes 中的基本构建模块。在实际应用中我们并不会单独部署容器,更多的是针对一组 pod 的容器进行部署和操作。值得注意的是,当一个 pod 包含多个容器时,这些容器总是运行于同一个工作节点上一个 pod 绝不会跨越多个工作节点。关于为何需要 pod 这种容器?为何不直接使用容器?为何甚至需要同时运行多个容器?难道不能简单地把所有进程都放在一个单独的容器中吗?接下来我们将一一回答上述问题。

                                                  想象一个由多个进程组成的应用程序,无论是通过 ipc (进程间通信)还是本地存储文件进行通信,都要求它们运行于同一台机器上。在 Kubernetes 中,我们经常在容器中运行进程,由于每一个容器都非常像一台独立的机器,此时你可能认为在单个容器中运行多个进程是合乎逻辑的,然而在实践中这种做法并不合理。

                                                  容器被设计为每个容器只运行一个进程(除非进程本身产生子进程)。如果在单个容器中运行多个不相关的进程,那么保持所有进程运行、管理它们的日志等工作就得我们自己来做。例如,我们需要包含一种在进程崩溃时能够自动重启的机制。同时这些进程都将记录到相同的标准输出中,而此时我们将很难确定每个进程分别记录了什么。

                                                  综上所述,我们需要让每个进程运行于自己的容器中,然后我们用 pod 这一层抽象来使多个容器之间的交互就像同一个主机内的多个进程之间的交互一样,而这就是 Docker 和 Kubernetes 期望使用的方式。在包含容器的 pod 下,我们可以同时运行一些密切相关的进程,并为它们提供(几乎) 相同的环境,此时这些进程就好像全部运行于单个容器中一样,同时又保持着一定的隔离。这样一来,我们便能全面地利用容器所提供的特性,同时对这些进程来说它们就像运行在一起一样,实现两全其美。

                                                  容器原理

                                                  Kubernetes 通过配置 Docker 来让一个 pod 内的 所有容器共享相同的 Linux 命名空间,而不是每个容器都有自己的一组命名空间。由于一个 pod 中的所有容器都在相同的 network 和 UTS 命名空间下运行,所以它们都共享相同的主机名和网络接口。同样地,这些容器也都在相同的 IPC 命名空间下运行,因此能够通过 IPC 进行通信。但当涉及文件系统时,情况就有所不同。由于大多数容器的文件系统来自容器镜像,因此默认情况下,每个容器的文件系统与其他容器完全隔离。但我们可以使用名为 Volume 的 Kubernetes 资源来共享文件目录,这一点我们后面会介绍。

                                                  这里需强调的一点是,由于一个 pod 中的容器运行于相同的 Network 命名空间中,因此它们共享相同的 IP 地址和端口空间。此外,一个 pod 中的所有容器也都具有相同的 loopback 网络接口,因此容器可以通过 localhost 与同一 pod 中的其他容器进行通信。

                                                  那么,它是怎么做到共享各类命名空间的呢?实际上,这都依赖于 Pause 容器,它是一个 pod 的 init 容器,当要启动一个 pod 时,kubelet 会先为该 pod 启动一个 pause 容器,它在 pod 中担任 Linux 命名空间共享的基础。随后,kubelet 才回去启动该 pod 内的真正的容器,在这个过程中只要通过 --net=container:pause
                                                  --ipc=contianer:pause
                                                  --pid=container:pause
                                                  这三个参数,就能让所有容器共享 pause 容器的网络空间,ipc 空间和 pid 空间,不过 pid 空间默认是不共享的。好了,我们已经知道了同一个 pod 内的容器是如何通讯的,那么不同的 pod 之间又是如何通讯的呢?Kubernetes 集群中的所有 pod 都在同一个共享网络地址空间中,还记不记得启动集群时最初配置的 PodSubnet,每个 pod 都可以通过其他 pod 的 IP 地址来实现相互访问。换句话说,它们之间没有 NAT(网络地址转换)网关。因此,pod 之间的通信在逻辑上其实是非常简单的。不论是将两个 pod 安排在单一的还是不同的工作节点上,同时不管实际节点间的网络拓扑结构如何,这些 pod 内的容器都能够像在无 NAT 的平坦网络中一样相互通信,就像局域网(LAN)上的计算机一样。此时,每个 pod 都有自己的 IP 地址,并且可以通过这个专门的网络实现 pod 之间互相访问。这个专门的网络通常是由额外的软件基于真实链路实现的,我们前面使用的 flannel 就是其中的一种。

                                                  规划 Pod 中的容器

                                                  虽然我们可以在单个 pod 中同时运行多个容器,但是你要知道 pod 是 Kubernetes 缩扩容的基本单位,为了让缩扩容更有效率,我们应该根据不同应用的扩缩容需求,来决定是否应该将他们塞进同一个 pod 中。为了让缩扩容达到最灵活的水平,你可以认为是尽可能地将不同应用容器分散到不同的 pod 中。

                                                  那么,什么情况下要将多个容器塞进单一 pod 呢?如果一个应用是由一个主进程和多个辅助进程组成,可以考虑将他们放入同一个 pod。主进程和辅助进程需要使用 ipc 通讯,或者它们需要相同的网络地址,或者它们需要共用相同的磁盘。

                                                  通过 YAML 管理 Pod

                                                  在使用 Kubernetes 时,一般都是使用 YAML 来管理 Pod 的,因为这样更加正式,而且可以通过 git 来维护 pod 的配置文件。本文不会解释 YAML 中所有属性的意义,而是简单的教大家怎么用。如果您想知道某一个属性的含义,最好直接访问官方文档[3]。同时您也可以通过 kubectl explain pod
                                                  ,或者 kubectl explain pod.spec
                                                  来查询每个字段的含义:

                                                    # kubectl explain pod
                                                    KIND: Pod
                                                    VERSION: v1


                                                    DESCRIPTION:
                                                    Pod is a collection of containers that can run on a host. This resource is
                                                    created by clients and scheduled onto hosts.


                                                    FIELDS:
                                                    apiVersion <string>
                                                    APIVersion defines the versioned schema of this representation of an
                                                    object. Servers should convert recognized schemas to the latest internal
                                                    value, and may reject unrecognized values. More info:
                                                    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources


                                                    kind <string>
                                                    Kind is a string value representing the REST resource this object
                                                    represents. Servers may infer this from the endpoint the client submits
                                                    requests to. Cannot be updated. In CamelCase. More info:
                                                    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds


                                                    metadata <Object>
                                                    Standard object's metadata. More info:
                                                    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata


                                                    spec <Object>
                                                    Specification of the desired behavior of the pod. More info:
                                                    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status


                                                    status <Object>
                                                    Most recently observed status of the pod. This data may not be up to date.
                                                    Populated by the system. Read-only. More info:
                                                    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

                                                    这里我们简单写一个 YAML 来启动前面的 kubia 容器:

                                                      apiVersion: v1 # 该描述文件遵循 v1 版本的 API
                                                      kind: Pod # 在描述一个 Pod 对象
                                                      metadata: # 元数据,包括名称、命名空间、标签和关于该容器 的其他信息
                                                      name: kubia-manual # pod 名字
                                                      spec: # 包含 pod 内容的实际说明,例如 pod 的容器、卷和其他数据
                                                      containers:
                                                      - image: beikejiedeliulangmao/kubia # 使用的容器
                                                      name: kubia
                                                      ports: # 应用监听的端口,在 pod 定义中指定端口纯粹是展示性的,忽略它们对于客户端是否可以通过端口连接到 pod 不会带来任何影响,但明确定义端口仍是有意义的,在端口定义下,每个使用集群的人都可以快速查看每个 pod 对外暴露的端口,还可以给每个端口指定一个名称
                                                      - containerPort: 8080
                                                      protocol: TCP

                                                      然后我们通过 kubectl create -f kubia-manual.yaml
                                                      启动它,启动成功后可以查看完整的 pod 描述 kubectl get po kubia-manual -o yaml
                                                      ,可以查看所有运行中的 pod kubectl get pod
                                                      ,也可以查看 pod 的 logkubectl logs kubia-manual
                                                      ,容器化的应用程序通常会将日志记录到标准输出和标准错误流,这样当 pod 删除时,日志也会被删除,如果希望 log 永久保存,我们需要使用集群范围的日志系统,这些我们后面会介绍。

                                                      每天或者每次日志文件达到 10MB 大小时,容器日志都会自动轮替。kubectl logs 命令仅显示最后一次轮替后的日志条目。

                                                      在调试阶段,我们可以通过 kubectl port-forward kubia-manual 8888:8080
                                                      创建代理,这样我们就能在本地连接到 pod。

                                                      对于微服务架构,部署的微服务数量可以轻松超过 20 个甚至更多。这些组件可能是副本(部署同一组件的多个副本)和多个不同的发布版本(stable、beta、canary 等)同时运行。这样一来可能会导致我们在系统中拥有数百个 pod, 如果没有可以有效组织这些组件的机制,将会导致产生巨大的混乱。好在我们可以通过 label 来组织 pod 和所有其他 Kubernetes 对象。

                                                      如下图所示,我们给 pod 加上 app 和 rel 两个标签来表示应用名和环境。每个可以访问集群的开发或运维人员都可以通过查看 pod 标签轻松看到系统的结构,以及每个 pod 的角色。label 的内容写在 YAML 的 metadata 中:

                                                        apiVersion: v1 # 该描述文件遵循 v1 版本的 API
                                                        kind: Pod # 在描述一个 Pod 对象
                                                        metadata: # 元数据,包括名称、命名空间、标签和关于该容器 的其他信息
                                                        name: kubia-manual # pod 名字
                                                        labels: # 标签
                                                        app: kubia
                                                        rel: beta

                                                        我们可以通过 kubectl get po --show-labels
                                                        查看 pod 的 labels,也可以通过 kubectl get po -L app,rel
                                                        将 label 直接显示在列上。但是目前来看,label 并没有实际价值。接下来我们用 label 做些真正有意义的事。

                                                        我们可以通过 kubectl get po -l app=kubia
                                                        筛选 pod,匹配表达式可以是:

                                                        • 等于:app=kubia

                                                        • 不等于:app !=kubia

                                                        • 范围匹配:app in (kubia)

                                                        • 范围过滤:app notin (kubia)

                                                        • 多匹配条件:app=kubia,rel=beta

                                                        目前为止,我们还只是在命令行中使用 label,实际上,我们还可以在 YAML 中使用,比如通过 label 来控制 pod 的部署需求,例如:我们可以给 node 打上标记 disk=ssd
                                                        或者 gpu=true
                                                        ,然后在 pod 的描述中选择特定的 node 部署。

                                                          apiVersion: v1 # 该描述文件遵循 v1 版本的 API
                                                          kind: Pod # 在描述一个 Pod 对象
                                                          metadata: # 元数据,包括名称、命名空间、标签和关于该容器 的其他信息
                                                          name: kubia-manual # pod 名字
                                                          spec:
                                                          nodeSelector:
                                                          gpu: true
                                                          disk: ssd

                                                          除标签外,pod 和其他对象还可以包含注解。注解也是键值对,所以它们本质上与标签非常相似。但与标签不同,注解并不是为了保存标识信息而存在的,注解主要用于工具使用,或者 Kubernetes 引入新特性时,也会用注解的方式引入。就像我们前面提到的云提供商的 LB 服务,就是通过注解接入该特性的,只不过它描述的对象是 Service 而不是 pod。

                                                            apiVersion: v1
                                                            kind: Service
                                                            metadata:
                                                            name: nginx
                                                            annotations:
                                                            service.beta.kubernetes.io/aws-load-balancer-type: "nlb"

                                                            再回到如何组织 Kubernetes 资源的问题,除了使用 label 之外,我们还可以使用命名空间,它可以做到资源的隔离,我们可以通过 kubectl get ns
                                                            查看所有的命名空间。而且我们的 kubectl 命令行默认是针对 default 命名空间操作的,如果要操作指定命名空间的资源可以通过 kubectl -n target-ns command
                                                            。除了隔离资源,命名空间还可用于仅允许某些用户访问某些特定资源,甚至限制单个用户可用的计算资源数量,这些我们后面会介绍。

                                                            副本机制

                                                            正如你前面所学到的,pod 代表了 Kubernetes 中的基本部署单元,而且你已知道如何手动创建、监督和管理它们。但是在实际的用例里,你希望你的部署能自动保持运行,并且保持健康,无须任何手动干预。要做到这一点,你几乎不会直接创建 pod , 而是创建 ReplicationController 或 Deployment 这样的资源,接着由它们来创建并管理实际的 pod,并且在它们失败的时候自动重新启动它们。

                                                            Kubernetes 可以通过存活探针 (liveness probe) 检查容器是否还在运行。可以为 pod 中的每个容器单独指定存活探针。如果探测失败,Kubemetes 将定期执行探针并重新启动容器。探针的种类有 3 种:

                                                            • HTTP GET:探针对容器的 IP 地址(你指定的端口和路径)执行 HTTP GET 请求。如果服务器返回错误响应状态码或者根本没有响应,那么探测就被认为是失败的,容器将被重新启动。

                                                            • TCP 套接字:尝试与容器指定端口建立 TCP 连接。如果连接成功建立,则探测成功。否则,容器重新启动。

                                                            • Exec 探针:在容器内执行任意命令,并检查命令的退出状态码。如果状态码是 0, 则探测成功。所有其他状态码都被认为失败。

                                                            我们可以在 YAML 中指定存活探针:

                                                              apiVersion: v1 # 该描述文件遵循 v1 版本的 API
                                                              kind: Pod # 在描述一个 Pod 对象
                                                              metadata: # 元数据,包括名称、命名空间、标签和关于该容器 的其他信息
                                                              name: kubia-manual # pod 名字
                                                              spec:
                                                              containers:
                                                              - image: beikejiedeliulangmao/kubia # 使用的容器
                                                              name: kubia
                                                              livenessProbe: # 使用 http 探针
                                                              httpGet:
                                                              path: /
                                                              port: 8080
                                                              initialDelaySeconds: 15 # 15秒初始延迟
                                                              # 如果没有设置初始延迟,探针将在启动时立即开始探测容器,这通常会导致探测失败,因为应用程序还没准备好开始接收请求。如果失败次数超过值,在应用程序能正确响应请求之前,容器就会重启。

                                                              如果上述 HTTP GET 探针连续 5 次失败就会认为探测失败,并重启容器。

                                                              当你想知道为什么前一个容器终止时,你想看到的是前一个容器的日志,而不是当前容器的。可以通过添加--previous 选项来完成: kubectl logs mypod --previous
                                                              , 通过 kubectl describe po mypod
                                                              可以查看 pod 为何重启。

                                                              对于在生产中运行的 pod, 一定要定义一个存活探针。没有探针的话,Kubernetes 无法知道你的应用是否还活着。只要进程还在运行,Kubernetes 会认为容器是健康的。探针应该尽可能的检查应用的可用性,同时也要尽可能轻量不能耗费太多资源。

                                                              Kubernetes 中有两种副本管理的资源 ReplicationController 和 ReplicaSet,它们本质上是一致的。它们都会持续监控正在运行的 pod 列表,并保证相应”类型”(通过 label 选择)的 pod 的数目与期望相符。如正在运行的 pod 太少,它会根据 pod 模板创建新的副本。如正在运行的 pod 太多,它将删除多余的副本。

                                                              如下就是一个 ReplicationController 的配置文件 YAML:

                                                                apiVersion: v1
                                                                kind: ReplicationController
                                                                metadata:
                                                                name: kubia
                                                                spec:
                                                                replicas: 3
                                                                selector: # 通过 app 标签匹配 pod
                                                                app: kubia
                                                                template: # 通过模板定义 pod
                                                                metadata:
                                                                labels: # 指定 pod 的标签
                                                                app: kubia
                                                                spec:
                                                                containers:
                                                                - image: beikejiedeliulangmao/kubia # 使用的容器
                                                                name: kubia

                                                                模板中的 pod 标签显然必须和 ReplicationController 的标签选择器匹配,否则控制器将无休止地创建新的容器。如果定义 ReplicationController 时不要指定 pod 选择器,会让 Kubernetes 从 pod 模板中提取它。

                                                                您可能会遇到这样的情况,线上环境的某一个服务节点出错了,我们需要把它摘下来确认出错的原因,这时候我们可以取消该 pod 和 ReplicationController 的绑定关系,然后单独确认它的问题,确认完后再将其还回去,这个过程可以通过修改 pod 的标签来实现,只要让 pod 的标签和 ReplicationController 的标签匹配不上就行了,这时候 ReplicationController 会自动创建一个新的 pod 来维持原来的 pod 数量。

                                                                如果你想进行 pod 的缩扩容,可以直接修改 ReplicationController 的副本数量 replicas,ReplicationController 会自动帮你进行缩扩容工作。

                                                                前面说过 Kubernetes 中有两种副本管理的资源 ReplicationController 和 ReplicaSet,那么用哪个呢?可以说 ReplicaSet 新一代的 ReplicationController,我们应该直接使用 ReplicaSet,但是通常我们并不是直接使用 ReplicaSet 资源,而是在创建更高层级的 Deployment 资源时自动创建他们,这个我们后面会介绍。

                                                                ReplicaSet 相较于 ReplicationController,pod 的匹配能力更强,ReplicationController 智能匹配 label=target
                                                                ,而 ReplicaSet 除此之外还支持 in,notin,exists(是否存在一个 label,值不重要),DoesNotExists。

                                                                ReplicationController 和 ReplicaSet 都用于在 Kubernetes 集群上运行部署特定数量的 pod。但是,当你希望 pod 在集群中的每个节点上运行时(并且每个节点都需要正好一个运行的 pod 实例),就需要用到 DaemonSet。它一般用作执行日志收集器和资源监视器。

                                                                让我们假设有一个名为 ssd-monitor 的守护进程,它需要在包含固态驱动器(SSD)的所有节点上运行。你将创建一个 DaemonSet,它在标记为具有 SSD 的所有节点上运行这个守护进程。集群管理员已经向所有此类节点添加了 disk=ssd 的标签,因此你将使用节点选择器创建 DaemonSet,该选择器只选择具有该标签的节点,如图所示。如下的 YAML 就是 ssd-monitor 的配置文件:

                                                                  apiVersion: apps/v1beta2
                                                                  kind: DaemonSet
                                                                  metadata:
                                                                  name: ssd-monitor
                                                                  spec:
                                                                  selector:
                                                                  matchLabels:
                                                                  app: ssd-monitor
                                                                  template:
                                                                  metadata:
                                                                  labels:
                                                                  app: ssd-monitor
                                                                  spec:
                                                                  nodeSelector:
                                                                  disk: ssd
                                                                  containers:
                                                                  - name: main
                                                                  image: luksa/ssd-monitor

                                                                  到目前为止,我们只谈论了需要持续运行的 pod。你会遇到只想运行完成工作后就终止任务的情况。ReplicationController、ReplicaSet 和 DaemonSet 会持续运行任务,永远达不到完成态。这些 pod 中的进程在退出时会重新启动。但是在一个可完成的任务中,其进程终止后,不应该再重新启动。

                                                                  Kubernetes 通过 Job 资源提供了对此的支持,这与我们在本章中讨论的其他资源类似,但它允许你运行一种 pod, 该 pod 在内部进程成功结束时(通过进程返回值判断),不重启容器。一旦任务完成,pod 就被认为处于完成状态。

                                                                  在发生节点故障时,该节点上由 Job 管理的 pod 将按照 ReplicaSet 的 pod 的方式,重新安排到其他节点。如果进程本身异常退出(进程返回错误退出代码时),可以将 Job 配置为重新启动容器。

                                                                  如下的例子就是一个简单的 Job:

                                                                    apiVersion: batch/v1
                                                                    kind: Job
                                                                    metadata:
                                                                    name: batch-job
                                                                    spec:
                                                                    completions: 5 # 需要执行五次
                                                                    paralleism: 2 # 最多 2 个 pod 并发
                                                                    template: # pod 模板
                                                                    metadata:
                                                                    labels:
                                                                    app: batch-job
                                                                    spec:
                                                                    restartPolicy: OnFailure # 出错时重启
                                                                    containers: # 使用的容器
                                                                    - name: main
                                                                    image: luksa/batch-job

                                                                    除了这种一次性的 Job 之外,我们还可以创建周期性的 Job,这种资源叫做 CornJob。在计划的时间内,CronJob 资源会创建 Job 资源,然后 Job 创建 pod。

                                                                      apiVersion: batch/v1beta1
                                                                      kind: CronJob
                                                                      metadata:
                                                                      name: batch-job-corn
                                                                      spec:
                                                                      schedule: "0,15,30,45 * * * *" # 每天每小时的 0 ,15,30,45分钟运行
                                                                      jobTemplate: # 创建 job 用到的模板
                                                                      spec:
                                                                      template: # pod 模板
                                                                      metadata:
                                                                      labels:
                                                                      app: batch-job-corn
                                                                      spec:
                                                                      restartPolicy: OnFailure # 出错时重启
                                                                      containers: # 使用的容器
                                                                      - name: main
                                                                      image: luksa/batch-job

                                                                      如果你不熟悉 cron 时间表格式,你会在网上找到很棒的教程和解释,我这里简单地介绍一下,时间表从左到右包含以下五个条目: 分钟,小时,每月中第几天,月,星期几。

                                                                      服务

                                                                      现在已经学习过了 pod, 以及如何通过 ReplicaSet 和类似资源部署运行。尽管特定的 pod 可以独立地应对外部刺激,现在大多数应用都需要根据外部请求做出响应。pod 需要一 种寻找其他 pod 的方法来使用其他 pod 提供的服务,不像在没有 Kubernetes 的世界,系统管理员要在用户端配置文件中明确指出服务的精确的 IP 地址或者主机名来配置每个客户端应用,但是同样的方式在 Kubernetes 中并不适用,因为 pod 是短暂的,它们随时会启动或者关闭,而且只有在 pod 启动时才会分配 ip,所以并不能提前预估到 ip 地址,而且水平扩容意味着会有多个 pod 提供相同服务,而实际上客户端并不关心 pod 的数量,它只希望有一个不变的 ip 可以用来访问自己期望的服务。

                                                                      Kubernetes 中可以使用服务来为一组功能相同的 pod 提供一个不变的接入口。当服务存在时,它的 IP 地址和端口不会改变。客户端通过 IP 地址和端口号建立连接,这些连接会被路由到提供该服务的任意一个 pod 上。通过这种方式,客户端不需要知道每个单独的提供服务的 pod 的地址,这样这些 pod 就可以在集群中随时被创建或移除。

                                                                      和 ReplicationController 一样,Service 也通过标签的过滤来暴露一组 pod 的服务。

                                                                        apiVersion: v1
                                                                        kind: Service
                                                                        metadata:
                                                                        name: kubia
                                                                        spec:
                                                                        ports: # 可以暴露多个端口
                                                                        - port: 80 # 服务端口
                                                                        targetPort: 8080 # 服务将连接转发到容器端口
                                                                        selector: # 匹配标签
                                                                        app: kubia

                                                                        创建 Service 之后,会被分配一个内部集群 IP,通过这个 IP 无论是在集群内的宿主机上还是集群内的 pod 中都能访问到该服务。下面的例子展示了通过 kubectl exec <pod-name> curl <service-ip>:port
                                                                        在一个 pod 中访问服务的例子。如果希望特定客户端产生的所有请求每次都指向同一个 pod, 可以设置服务的 sessionAffinity 属性为 ClientIP 这种方式将会使服务代理将来自同一个 client IP 的所有请求转发至同一个 pod 上。

                                                                          apiVersion: v1
                                                                          kind: Service
                                                                          spec:
                                                                          sessionAffinity: ClientIP

                                                                          通过服务,我们已经有了一个不变的服务 IP,Pod 内怎么知道服务的 IP 呢?Kubernetes 为客户端提供了发现服务 IP 和端口的方式。一个是通过环境变量指定,它可以在 pod 的 YAML 中配置。另一个方法是通过 DNS 发现服务。Kubernetes 内部也有一个 dns 服务它会根据服务的变化快速做出响应。这种方法相较于环境变量来说更加灵活。Kubernetes 内部 DNS 的规则是..svc.cluster.local,如果客户端和服务在同一个命名空间的话,可以省略命名空间和后缀kubectl exec -it kubia-kqtr6 curl kubia:8080
                                                                          。值得一提的是,DNS 只能帮我们得到 IP 信息,至于服务使用了哪个端口,仍然需要在环境变量中配置。

                                                                          到目前为止,我们只是讨论了 Service 如何暴露集群内部的服务,那么能不能让 service 绑定到集群外的服务呢?这就牵扯到 Service 的 EndPoint 的资源,在创建服务时,它会根据标签选择器找到所有集群内部的 pod,然后生成对应的 EndPoint 资源,当访问服务端口时,实际上会通过 EndPoint 进行转发。

                                                                          我们可以通过 kubectl describe svc kubia
                                                                          查看 service 的 EndPoint。理解了 EndPoint 的概念后,我们回到刚才的问题,因为服务与 EndPoint 的解耦,我们实际上可以为 service 手动加入自定义的 EndPoint。

                                                                            apiVersion: v1
                                                                            kind: Endpoints
                                                                            metadata:
                                                                            name: external-service # endpoint 的名字必须和 service 名字相同,它们通过这一层纽带绑定在一起
                                                                            subsets:
                                                                            - addresses: # endpoint 的 ip
                                                                            - ip: 11.11.11.11
                                                                            - ip: 22.22.22.22
                                                                            ports: # endpoint 的端口
                                                                            - port: 80

                                                                            在前面,我们已经或多或少的提到了集群外的客户端如何访问集群内的 Service。总结一下,总共有三种方案:NodePort,LoadBalance,Ingress。

                                                                            将一组 pod 公开给外部客户端的第一种方法是创建一个服务并将其类型设置为 NodePort。通过创建 NodePort 服务,可以让 Kubernetes 在其所有节点上保留一个端口(所有节点上都使用相同的端口号),并将传入的连接转发给作为服务部分的 pod。

                                                                              apiVersion: v1
                                                                              kind: Service
                                                                              metadata:
                                                                              name: kubia-nodeport
                                                                              spec:
                                                                              type: NodePort # node port 类型的服务
                                                                              ports:
                                                                              - port: 80 # service ip
                                                                              targetPort: 8080 # pod 的目标端口
                                                                              nodePort: 30123 # 通过任意节点的 30123 就可以访问服务,如果不设置该项会随机选用一个端口
                                                                              selector:
                                                                              app: kubia

                                                                              创建成功后,通过 kubectl get svc
                                                                              ,会看到刚才创建的服务,您会发现其中 PORT 列有两个端口80:30123/TCP
                                                                              ,前面的是集群内 Service IP 用到的端口,后面的是 NodePort。在使用 NodePort 类型的服务时,一定要注意尽可能让客户端能够访问到尽可能多的节点 NodePort,这样当任意一个节点宕机后,仍然不影响使用。第二种方案是通过 LoadBalance,在云提供商上运行的 Kubernetes 集群通常支持从云基础架构自动提供负载平衡器。如果 Kubernetes 在不支持 LoadBalancer 服务的环境中运行,则不会调配负载平衡器,但该服务仍将表现得像一个 NodePort 服务。这是因为 LoadBalancer 服务是 NodePort 服务的扩展。

                                                                                apiVersion: v1
                                                                                kind: Service
                                                                                metadata:
                                                                                name: kubia-nodeport
                                                                                spec:
                                                                                type: LoadBalancer # LoadBalancer 类型的服务
                                                                                ports:
                                                                                - port: 80 # service ip
                                                                                targetPort: 8080 # pod 的目标端口

                                                                                创建成功后,可以通过kubectl get svc
                                                                                ,会看到刚才创建的服务,然后通过 EXTERNAL-IP + service ip 访问服务。外部客户端(可以使用 curl)连接到负载均衡器的 80 端口,并路由到其中一个节点上的隐式分配节点端口(实际上就是 NodePort)。之后该连接被转发到一个 pod 实例。当外部客户端通过节点端口连接到服务时(这也包括先通过负载均衡器时的情况),随机选择的 pod 并不一定在接收连接的同一节点上运行。可能需要额外的网络跳转才能到达 pod, 但这种行为并不符合期望。可以通过将服务配置为仅将外部通信重定向到接收连接的节点上运行的 pod 来阻止此额外跳数。

                                                                                  spec:
                                                                                  externalTrafficPolicy: Local

                                                                                  这个参数有一个缺点,如果本地没有该服务的 Pod,它不会自动转发到其他节点的 Pod,而是挂起连接。所以,使用的时候需要确保使用的节点上至少有一个服务 Pod。而且可能会出现流量不平均分配的问题。通常,当集群内的客户端连接到服务时,支持服务的 pod 可以获取客户端的 IP 地址。但是,当通过节点端口接收到连接时,由于对数据包执行了源网络地址转换(SNAT), 因此数据包的源 IP 将发生更改。后端的 pod 无法看到实际的客户端 IP, 这对于某些需要了解客户端 IP 的应用程序来说可能是个问题。不过前面说的 Local LB 方式,因为不涉及额外的跳跃(不执行 SNAT),所以可以保留到客户端 IP。

                                                                                  最后一个暴露服务的方式是 Ingress,和 LB 不同的是,Ingress 只需要一个公网 IP 就能为多个服务提供外部访问能力。当客户端向 Ingress 发送 HTTP 请求时,Ingress 会根据请求的主机名和路径决定请求转发到的服务。因为 Ingress 工作在应用层(HTTP),所以可以提供 Service 无法实现的功能,比如基于 cookie 的会话亲和性。

                                                                                  我们可以通过如下配置文件创建一个 Ingress:

                                                                                    apiVersion: extensions/v1beta1
                                                                                    kind: Ingress
                                                                                    metadata:
                                                                                    name: kubia
                                                                                    spec:
                                                                                    rules: # 可以匹配多个域名
                                                                                    - host: kubia.example.com # 映射服务的域名
                                                                                    http:
                                                                                    paths: # 可以匹配多个地址
                                                                                    - path: /
                                                                                    backend:
                                                                                    serviceName: kubia # 将请求映射到服务的端口
                                                                                    servicePort: 80

                                                                                    和 LoadBalancer 类型的 service 一样,Ingress 也需要云服务提供商的支持,如果创建成功,Ingress 的 ip 地址就能在kubectl get ingress
                                                                                    中看到,随后我们只要将该 ip 和域名绑定到 DNS 中就可以访问了。可能已经从 Ingress 的配置文件中看出它的写法非常像 nginx 的配置文件,实际上 Ingress 的其中一种实现方式就是基于 nginx。你可以像在 nginx 中配置多个域名多个 path 的匹配规则,将他们映射到不同的 service pod 中。同样你也可以像 nginx 一样支持 TLS 认证,一般来说我们会将证书和秘钥存储在 Kubernetes 的 secret 资源中,然后在 Ingress 配置文件中引用它们。

                                                                                      apiVersion: extensions/v1beta1
                                                                                      kind: Ingress
                                                                                      metadata:
                                                                                      name: kubia
                                                                                      spec:
                                                                                      tls: # 所有 tls 设置
                                                                                      - hosts:
                                                                                      - kubia.example.com # 需要使用 tls 的域名
                                                                                      secretName: tls-secret # secret 资源的名字

                                                                                      我们已经知道,如果 pod 的标签与服务的 pod 选择器相匹配,那么 pod 就将作为服务的后端。只要创建了具有适当标签的新 pod,它就成为服务的一部分,并且请求开始被重定向到 pod。那么如果新启动的 pod 还没有准备好接受请求呢,该 pod 可能需要时间来加载配置或数据,或者可能需要执行预热过程以防止第一个用户请求时间太长影响了用户体验。在这种情况下,不希望该 pod 立即开始接收请求,尤其是在运行的实例可以正确快速地处理请求的情况下。不要将请求转发到正在启动的 pod 中,直到完全准备就绪。

                                                                                      在前面,我们介绍了存活探针,通过它可以感知到 Pod 是否运转正常,和它类似,Kubernetes 中还有一个就绪探针,通过它来确认 Pod 是否准备就绪,配置文件的写法也和存活探针一样都在 ReplicationController 的 pod template 中。当 pod 准备就绪时,才会将请求路由到该 pod 中。像存活探针一样,就绪探针也有三种类型的:Exec 探针,HTTP GET 探针,TCP Socket 探针。它们的使用和前面介绍的存活探针一样。

                                                                                      与存活探针不同,如果容器未通过准备检查,不会被终止或重新启动。这是存活探针与就绪探针之间的重要区别。存活探针通过杀死异常的容器并用新的正常容器替代它们来保持 pod 正常工作,而就绪探针确保只有准备好处理请求的 pod 才可以接收它们(请求)。这在容器启动时最为必要,当然容器运行一段时间后也是有用的这个就绪探针依旧会发挥作用,如果任何时刻就绪探针报错,Kubernetes 就会把该 pod 从 server endpoint 中摘除。上面的介绍中,都是涉及和单个 service pod 通讯,那如果我们想要和所有 service pod 同时建立连接时,怎么做呢?一个可行的办法是将 service 的 clusterIp 属性设为 None,那样这个 service 就会变为一个 headless service,当我们访问 service 的 dns 时,dns 会将所有的 pod ip 返回。

                                                                                      服务是 Kubernetes 的一个重要概念,也是让许多开发人员感到困扰的根源。出于这个原因,了解一下如何排除服务故障是很有必要的,如果无法通过服务访问 pod, 应该根据下面的列表进行排查:

                                                                                      • 首先,确保从集群内连接到服务的集群 IP, 而不是从外部。

                                                                                      • 不要通过 ping 服务 IP 来判断服务是否可访问(请记住,服务的集群 IP 是虚拟 IP, 是无法 ping 通的)。

                                                                                      • 如果已经定义了就绪探针,请确保它返回成功;否则该 pod 不会成为服务的一部分。

                                                                                      • 要确认某个容器是服务的一部分,请使用kubectl get endpoints
                                                                                        来检查相应的端点对象。

                                                                                      • 如果尝试通过 FQDN 或其中一部分来访问服务(例如,myservice.mynamespace.svc.cluster.local 或 myservice.mynamespace), 但并不起作用,请查看是否可以使用其集群 IP 而不是 FQDN 来访问服务。

                                                                                      • 检查是否连接到服务公开的端口,而不是目标 pod 端口。

                                                                                      • 尝试直接连接到 pod IP 以确认 pod 正在接收正确端口上的连接。

                                                                                      • 如果无法通过 pod 的 IP 访问应用,请确保应用不是仅绑定到本地主机 127.0.0.1。

                                                                                      我们之前说过,pod 类似逻辑主机,在逻辑主机中运行的进程共享诸如 CPU、RAM、网络接口等资源。但是磁盘并不会共享,需要谨记一点,pod 中的每个容器都有自己独立的文件系统,因为文件系统来自容器镜像。在某些场景下,我们可能希望新的容器可以在之前容器结束的位置继续运行,比如在物理机上重启进程。可能不需要(或者不想要)整个文件系统被持久化,但又希望能保存实际数据的目录。

                                                                                      Kubernetes 通过定义存储卷来满足这个需求,它们不像 pod 这样的顶级资源,而是被定义为 pod 的一部分,并和 pod 共享相同的生命周期。这意味着在 pod 启动时创建卷,并在删除 pod 时销毁卷。因此,在容器重新启动期间,卷的内容将保持不变,在重新启动容器之后,新容器可以识别前一个容器写入卷的所有文件。另外,如果一个 pod 包含多个容器,那这个卷可以同时被所有的容器使用。

                                                                                      假设有一个带有三个容器的 pod,一个容器运行了一个 web 服务器,该 web 服务器的 HTML 页面目录位于 /var/htdocs, 并将站点访问日志存储到 /var/logs 目录中。第二个容器运行了一个代理来创建 HTML 文件,并将它们存放在 /var/html 中,第三个容器处理在 /var/logs 目录中找到的日志(转换、压缩、分析它们或者做其他处理)。这里我们使用名为 emptyDir 的卷,卷被绑定到 pod 的 lifecycle(生命周期)中,只有在 pod 存在时才会存在,但是也有一些类型的卷支持 pod 和卷消失之后,卷的文件也可能保持原样,并可以挂载到新的卷中。除了前面提到的 emptyDir 之外,卷的类型有很多种,不同卷类型有各种用途。这里我们只介绍一些常用的卷。最简单的卷类型是 emptyDir 卷,它从一个空目录开始,运行在 pod 内的应用程序可以写入它需要的任何文件。因为卷的生存周期与 pod 的生存周期相关联,所以当删除 pod 时,卷的内容就会丢失。下面就是一个 emptyDir 卷的简单例子:

                                                                                        apiVersion: v1
                                                                                        kind: Pod
                                                                                        metadata:
                                                                                        name: fortune
                                                                                        spec:
                                                                                        containers: # 定义两个容器,挂在同一个卷到不同的目录
                                                                                        - image: luksa/fortune
                                                                                        name: html-generator
                                                                                        volumeMounts:
                                                                                        - name: html
                                                                                        mountPath: /var/htdocs # 读写模式
                                                                                        - image: nginx:alpine
                                                                                        name: web-server
                                                                                        volumeMounts:
                                                                                        - name: html
                                                                                        mountPath: /usr/share/nginx/html
                                                                                        readOnly: true # 只读模式
                                                                                        ports:
                                                                                        - containerPort: 80
                                                                                        protocol: TCP
                                                                                        volumes: # 创建名为 html 的 emptyDir 卷,并挂在上述 2 个容器中
                                                                                        - name: html
                                                                                        emptyDir: {}

                                                                                        我们可以使用端口转发 kubectl port-forward fortune 8080:80
                                                                                        来确认该 pod 的运转情况,通过 curl http://localhost:8080
                                                                                        访问 pod 中的服务。

                                                                                        我们也可以通过 git 仓库来初始化前面提到的 emptyDir 卷,实际上这是另一种名为 gitRepo 的卷,它通过克隆 Git 仓库并在 pod 启动时(但在创建容器之前)检出特定版本来填充数据。但是要注意 gitRepo 在 pod 启动后并不会自动同步 git 仓库的最新内容。要想在 pod 运行期间同步 git 仓库的内容,我们需要给 pod 加一个负责同步工作的容器。

                                                                                        大多数 pod 应该忽略它们的主机节点,因此它们不应该访问节点文件系统上的任何文件。但是某些系统级别的 pod(切记,这些通常由 DaemonSet 管理)确实需要读取节点的文件或使用节点文件系统来访问节点设备。Kubernetes 通过 hostPath 卷实现了这一点。

                                                                                        hostPath 卷指向节点文件系统上的特定文件或目录。在同一个节点上运行并在其 hostPath 卷中使用相同路径的 pod 可以看到相同的文件。hostPath 卷是我们介绍的第一种类型的持久性存储,因为 gitRepo 和 emptyDir 卷的内容都会在 pod 被删除时被删除,而 hostPath 卷的内容则不会被删除。如果删除了一个 pod, 并且下一个 pod 使用了指向主机上相同路径的 hostPath 卷,则新 pod 将会发现上一个 pod 留下的数据,但前提是必须将其调度到与第一个 pod 相同的节点上,如果调度到了另一个节点,则会找不到数据。

                                                                                        当运行在一个 pod 中的应用程序需要将数据保存到磁盘上,并且即使该 pod 重新调度到另一个节点时也要求具有相同的数据可用。这就不能使用到目前为止我们提到的任何卷类型,由于这些数据需要可以从任何集群节点访问,因此必须将其存储在某种类型的网络存储(NAS) 中,例如 nfs。

                                                                                          volumes:
                                                                                          - name: data
                                                                                          nfs: # 使用 nfs 卷
                                                                                          server: 1.2.3.4 # nfs 服务 ip
                                                                                          path: /some/path # nfs 服务提供的路径

                                                                                          到目前为止,我们探索过的所有待久卷类型都要求 pod 的开发人员了解集群中可用的真实网络存储的基础结构。例如,要创建支持 NFS 协议的卷,开发人员必须知道 NFS 节点所在的实际服务器。这违背了 Kubernetes 的基本理念:"向应用程序及其开发人员隐藏真实的基础设施,使他们不必担心基础设施的具体状态,并使应用程序可在大量云服务商和数据企业之间进行功能迁移"。

                                                                                          理想的情况是,在 Kubernetes 上部署应用程序的开发人员不需要知道底层使用的是哪种存储技术,同理他们也不需要了解应该使用哪些类型的物理服务器来运行 pod, 与基础设施相关的交互是集群管理员独有的控制领域。

                                                                                          当开发人员需要一定数量的持久化存储来进行应用时,可以向 Kubernetes 请求,就像在创建 pod 时可以请求 CPU、内存和其他资源一样。系统管理员可以对集群进行配置让其可以为应用程序提供所需的服务。

                                                                                          在 Kubernetes 集群中为了使应用能够正常请求存储资源,同时避免处理基础设施细节,引入了两个新的资源,分别是持久卷和持久卷声明。

                                                                                          研发人员无须向他们的 pod 中添加特定技术的卷,而是由集群管理员设置底层存储,然后通过 Kubernetes API 服务器创建 PersistentVolume(持久卷,简称 PV)并注册。在创建持久卷时,管理员可以指定其大小和所支持的访问模式。

                                                                                          当集群用户需要在其 pod 中使用持久化存储时,他们首先创建持久卷声明(PersistentVolumeClaim, 简称 PVC)清单,指定所需要的最低容量要求和访问模式,然后用户将待久卷声明清单提交给 Kubernetes API 服务器,Kubernetes 将找到可匹配的待久卷并将其绑定到持久卷声明。

                                                                                          持久卷声明可以当作 pod 中的一个卷来使用,其他用户不能使用相同的持久卷,除非先通过删除持久卷声明绑定来释放。首先,我们假设自己是 Kubernetes 管理员,我们要创建一些持久化卷:

                                                                                            apiVersion: v1
                                                                                            kind: PersistentVolume
                                                                                            metadata:
                                                                                            name: pv
                                                                                            spec:
                                                                                            capacity: # 定义 PV 大小
                                                                                            storage: 1 Gi
                                                                                            accessModes: # 可以被单个客户端挂在为读写模式,或者被多个客户端挂载为只读
                                                                                            - ReadWriteOnce
                                                                                            - ReadWriteMany
                                                                                            persistentVolumeReclaimPolicy: Retain # 当 PVC 被删除,PV 将会被保留(不清理和删除),需要管理员手动清理 PV 才能被下一个 PVC 绑定
                                                                                            nfs: # PV 指定 nfs 设备
                                                                                            server: 1.2.3.4 # nfs 服务 ip
                                                                                            path: /some/path # nfs 服务提供的路径

                                                                                            持久卷不属于任何命名空间, 它跟节点一样是集群层面的资源。假设现在需要部署一个需要持久化存储的 pod,将要用到之前创建的持久卷,所以我们需要现在开始创建一个声明:

                                                                                              apiVersion: v1
                                                                                              kind: PersistentVolumeClaim
                                                                                              metadata:
                                                                                              name: pvc # 将来会在 pod 中使用该名字来查找 PVC
                                                                                              spec:
                                                                                              resources:
                                                                                              requests:
                                                                                              storage: 1Gi # 申请 1GiB 空间
                                                                                              accessModes:
                                                                                              - ReadWriteOnce # 允许单个客户端访问(读写)

                                                                                              当创建好声明,Kubernetes 就会找到适当的持久卷并将其绑定到声明,持久卷的容量必须足够大以满足声明的需求,并且卷的访问模式必须包含声明中指定的访问模式。可以通过 kubectl get pv
                                                                                              确认持久卷与 PVC 的绑定情况。

                                                                                              访问模式:

                                                                                              • RWO---ReadWriteOnce---仅允许单个节点挂载读写。

                                                                                              • ROX---ReadOnlyMany---允许多个节点挂载只读。

                                                                                              • RWX---ReadWriteMany---允许多个节点挂载读写这个卷。

                                                                                              创建好 PVC,之后我们就可以在 Pod 中直接使用:

                                                                                                volumes:
                                                                                                - name: data
                                                                                                persistentVolumeClaim:
                                                                                                claimName: pvc # 通过名字引用 PVC

                                                                                                如你所见,使用持久卷和持久卷声明可以轻松获得持久化存储资源,无须研发人员处理下面实际使用的存储技术,但这仍然需要一个集群管理员来支持实际的存储。幸运的是,Kubernetes 还可以通过动态配置持久卷来自动执行此任务,这个资源叫做 StorageClass,管理员可以创建多种 StorageClass,每种 StorageClass 对应了一种卷类型比如 nfs,在用户创建 PVC 时指定使用的 StorageClass,Kubernetes 会根据该 StorageClass 中配置的 PV 申请方式,自动创建新的 PV 并绑定到 PVC 上。

                                                                                                配置信息和敏感数据

                                                                                                几乎所有的应用都需要配置信息(不同部署示例间的区分设置、访问外部系统的证书等),并且这些配置数据不应该被嵌入应用本身。在 Kubernetes 我们可以通过 ConfigMap 和 Secret 传递配置选项给运行在 Kubernetes 上的应用程序。一般来说 ConfigMap 用来传输普通的配置信息,而 Secret 用来传输敏感信息。

                                                                                                首先,我们要知道在 Pod 的配置文件中,我们可以修改容器的 ENTRYPOINT command 和 arguments。绝大多数情况下,只需要设置自定义参数。命令一般很少被覆盖,除非针对一些未定义 ENTRYPOINT 的通用镜像,例如 busybox。值得注意的是,容器的命令和参数设置在 pod 启动后无法修改。

                                                                                                  kind: Pod
                                                                                                  spec:
                                                                                                  containers:
                                                                                                  - image: some/image
                                                                                                  command: ["/bin/command"]
                                                                                                  args: ["argl", "arg2", "arg3"]

                                                                                                  Kubernetes 允许为 pod 中的每一个容器都指定自定义的环境变量集合,与容器的命令和参数设置相同,环境变量列表无法在 pod 创建后被修改。

                                                                                                    kind: Pod
                                                                                                    spec:
                                                                                                    containers:
                                                                                                    - image: luksa/fortune:env
                                                                                                    env:
                                                                                                    - name: INTERVAL # 定义环境变量
                                                                                                    value: "30"
                                                                                                    - name: USE_INTERVAL # 定义环境变量的同时,引用另一个环境变量
                                                                                                    value: "$(INTERVAL)123"
                                                                                                    name: fortune

                                                                                                    pod 定义硬编码意味着需要有效区分生产环境与开发过程中的 pod 定义。为了能在多个环境下复用 pod 的定义,需要将配置从 pod 定义描述中解耦出来。幸运的是,你可以通过一种叫作 ConfigMap 的资源对象完成解耦,用 valueFrom 字段替代 value 字段使 ConfigMap 成为环境变量值的来源。

                                                                                                    Kubernetes 允许将配置选项分离到单独的资源对象 ConfigMap 中,本质上就是一个键/值对映射,值可以是短字面量,也可以是完整的配置文件。应用无须直接读取 ConfigMap,甚至根本不需要知道其是否存在。映射的内容通过环境变量或者卷文件的形式传递给容器,而并非直接传递给容器。对于不同的环境(开发,测试,生产),我们可以创建多个配置清单,通过命名空间的隔离来保证配置清单可以使用相同的名字,这样我们就能完全复用同一份 Pod 配置文件。我们可以通过命令或者配置文件来创建 ConfigMap:kubectl create configmap fortune-config --from-literal=sleep-interval=25
                                                                                                    kubectl create -f fortune-config.yaml

                                                                                                      apiVersion: v1
                                                                                                      kind: configMap
                                                                                                      data:
                                                                                                      sleeo-interval: 25

                                                                                                      创建好 ConfigMap 后,我们可以在 Pod 中引用 ConfigMap 中的内容

                                                                                                        kind: Pod
                                                                                                        spec:
                                                                                                        containers:
                                                                                                        - image: luksa/fortune:env
                                                                                                        env:
                                                                                                        - name: INTERVAL # 定义环境变量
                                                                                                        valueFrom:
                                                                                                        configMapKeyRef: # 绑定 configmap 和 key
                                                                                                        name: fortune-config
                                                                                                        key: sleeo-interval
                                                                                                        args: ["$(INTERVAL)"] # 通过环境变量传递 ConfigMap 的内容到容器参数中
                                                                                                        name: fortune

                                                                                                        环境变量或者命令行方式一般作为配置较少信息时方案,如果要配置的内容很多时,我们还可以使用 ConfigMap 卷将配置信息暴露为文件,通过这种类型的卷,ConfigMap 中的每个条目都将暴露为一个文件,在容器中可以通过查看文件的内容来获取配置信息。

                                                                                                          apiVersion: v1
                                                                                                          data:
                                                                                                          my-nginx-config.conf: | # 所有条目第一行最后的管道符号表示后续的条目值是多行字面量。
                                                                                                          server {
                                                                                                          listen 80;
                                                                                                          server_name www.example.com;
                                                                                                          ...
                                                                                                          }
                                                                                                          sleep-interval: 25
                                                                                                          kind: ConfigMap

                                                                                                          创建好上述 ConfigMap 之后,我们可以在 Pod 中挂在 configMap 类型的卷:

                                                                                                            apiVersion: v1
                                                                                                            kind: Pod
                                                                                                            metadata:
                                                                                                            name: fortune-configmap-volume
                                                                                                            spec:
                                                                                                            containers:
                                                                                                            - image: nginx:alpine
                                                                                                            name: web-server
                                                                                                            volumeMounts:
                                                                                                            - name: config
                                                                                                            mountPath: /etc/nginx/conf.d
                                                                                                            readOnly: true
                                                                                                            volumes:
                                                                                                            - name: config
                                                                                                            configMap:
                                                                                                            name: fortune-config

                                                                                                            当进行了 configMap 的挂载后,原容器内的 /etc/nginx/conf.d 会被隐藏,只会看到 configMap 的内容,当然我们也可以只挂在 configMap 中的单独几个配置项,这样就只会覆盖同名文件。

                                                                                                            前面已经提到过,使用环境变量传递配置时,当 Pod 运行起来后就无法更改,但是如果使用的是 configMap 卷的话,则不会出现这种问题,一旦配置内容发生变化,容器内的文件也会跟着变化,同时,Kubernetes 有机制能让配置文件的变化事件通知给容器。但是要注意的是,不同容器的相同 ConfigMap 的更新过程并不是同步的,换句话说在进行 ConfigMap 的更新时,会出现多个 Pod 之间配置内容不同步的问题。

                                                                                                            到目前为止传递给容器的所有信息都是比较常规的非敏感数据。然而正如开头提到的,配置通常会包含一些敏感数据,如证书和私钥,需要确保其安全性。为了存储与分发此类信息,Kubernetes 提供了一种称为 Secret 的单独资源对象。Secret 结构与 ConfigMap 类似,均是键/值对的映射。Secret 的使用方法也与 ConfigMap 相同,可以

                                                                                                            • 将 Secret 条目作为环境变量传递给容器

                                                                                                            • 将 Secret 条目暴露为卷中的文件

                                                                                                            前面介绍 Ingress 时提到 TLS 一般都是通过 Secret 来传递证书。我们可以通过 kubectl create secret generic fortune-https --from-file=https.key --from-file=https.cert
                                                                                                            来创建 Secret 资源。当我们通过 kubect1 get secret fortune-https -o yaml
                                                                                                            查看 Secret 资源时,你会发现实际上在显示的时候,它会先通过 BASE64 编码 value 之后显示,这样做的原因是 Secret 不像 ConfigMap 那样只能存储文本,它还能存储二进制文件,所以它会先用 BASE64 编码后,再显示出来。当然并不是说 Secret 只能显示 BASE64 加密后的内容,我们也可以通过 stringData 来设置非二进制数据,这样就不会涉及 BASE64 编码。

                                                                                                            再回到我们前面的 nginx 例子,我们可以再为其挂载一个 secret 卷来植入证书文件,同时修改 nginx 的配置文件,让其使用 secret 中的证书,最后达到的效果如下图所示。我们已经通过挂载 secret 卷至文件夹/etc/nginx/certs 将证书与私钥成功传递给容器。但是有一点要提的是,secret 卷采用的是内存文件挂载,存储在 Secret 中的数据不会写入磁盘,这样就无法被窃取。

                                                                                                            Pod 访问 Kubernetes API

                                                                                                            前面我们已经说过了 ConfigMap 和 Secret 这些资源可以为 Pod 传递预先设定好的资源,但是对于那些不能预先知道的数据,比如 pod 的 IP、 主机名或者是 pod 自身的名称,则需要通过 Kubernetes API 获取。

                                                                                                            在 Kubernetes 中有两类 API,其中一个是 DownloadAPI,它不像传统 REST 服务那样访问,而是像 ConfigMap 那样在 pod 配置文件中引用:

                                                                                                              apiVersion: v1
                                                                                                              kind: Pod
                                                                                                              metadata:
                                                                                                              name: downward
                                                                                                              spec:
                                                                                                              containers:
                                                                                                              - name: main
                                                                                                              image: busybox
                                                                                                              command: ["sleep", "9999999"]
                                                                                                              resources:
                                                                                                              requests:
                                                                                                              cpu: 15m # 指定 pod 的 cpu 需求,如果宿主机的剩余 cpu 资源小于pod需要的量,那么该 pod 就不会被调度到这个节点
                                                                                                              memory: 1OOKi
                                                                                                              env:
                                                                                                              - name: POD_NAME
                                                                                                              valueFrom:
                                                                                                              fieldRef:
                                                                                                              fieldPath: metadata.name # 引用 pod manifest 中的元数据字段
                                                                                                              - name: CONTAINER
                                                                                                              valueFrom:
                                                                                                              resourceFieldRef: # 通过 resourceFieldRef 获取容器请求的 CPU 和 内存使用量
                                                                                                              resource: requests.cpu
                                                                                                              divisor: 1m

                                                                                                              除了环境变量之外,我们也可以通过卷的形式使用 downloadAPI。

                                                                                                                volumes:
                                                                                                                - name: downward
                                                                                                                downwardAPI:
                                                                                                                items:
                                                                                                                - path: "podName"
                                                                                                                fieldRef:
                                                                                                                fieldPath: metadata.name

                                                                                                                正如我们看到的,Downward API 方式并不复杂,它使得应用独立于 Kubernetes。不过通过 Downward API 的方式获取的元数据是相当有限的,如果需要获取更多的元数据,需要使用直接访问 Kubernetes API 服务器的方式。

                                                                                                                我们已经了解到了很多 Kubernetes 的资源类型。但如果打算开发一个可以与 Kubernetes API 交互的应用,要首先了解各种 API 的 REST 接口。我们可以通过 kubectl cluster-info
                                                                                                                来获取 Kubernetes API 的 EndPoint,并通过 kubectl proxy
                                                                                                                来代理这些 API,这样我们就能在本地查看各个 API curl http://localhost:8001

                                                                                                                  curl 127.0.0.1:8001
                                                                                                                  #{
                                                                                                                  # "paths": [
                                                                                                                  # "/api",
                                                                                                                  # "/api/v1",
                                                                                                                  # "/apis",
                                                                                                                  # "/apis/",
                                                                                                                  # "/apis/admissionregistration.k8s.io",
                                                                                                                  # "/apis/admissionregistration.k8s.io/v1",
                                                                                                                  # "/apis/admissionregistration.k8s.io/v1beta1",
                                                                                                                  # ...
                                                                                                                  # ]
                                                                                                                  #}

                                                                                                                  您可以通过这些 API 来操作整个 Kubernetes 集群,你能做到所有 Kubernetes 能做到,比如查看 Pod,修改 Pod,创建 Pod 等等,你可以操作任意资源。但是如果你要在 Pod 中访问这些 API 可不像我们这里使用的 kubectl proxy
                                                                                                                  这么简单,你需要找到 API 服务器的地址,确保是和 API 服务器交互,而不是一个中间人,同时你还要通过服务器的认证,Kubernetes 通过账号和账号权限来管理 Pod 中能使用的资源范围。

                                                                                                                  最简单的访问 Kubernetes API 的方法是在容器中访问 curl https://kubernetes
                                                                                                                  ,而在每个 pod 中都存有一个自己的 Kubernetes API 账号信息,它们存储在容器的 /var/run/secrets/kubernetes.io/serviceaccount/
                                                                                                                  中,该文件夹中有三个文件:

                                                                                                                  • ca.crt:包含了 CA 的证书,用来对 Kubernetes API 服务器证书进行签名

                                                                                                                  • token:获得授权,认证的凭证

                                                                                                                  • namespace:获取 pod 所在的命名空间

                                                                                                                  简要说明 pod 如何与 Kubernetes 交互:

                                                                                                                  • 应用应该验证 API 服务器的证书是否是证书机构所签发,这个证书是在 ca.crt 文件中。

                                                                                                                  • 应用应该将它在 token 文件中持有的凭证通过 Authorization 标头来获得 API 服务器的授权。

                                                                                                                  • 当对 pod 所在命名空间的 API 对象进行 CRUD 操作时,应该使用 namespace 文件来传递命名空间信息到 API 服务器。

                                                                                                                  实际上我们不仅能通过命令行访问 Kubernetes API,在 Go,python,java 等语言中都有相关的库可以用来和 Kubernetes 交互。

                                                                                                                  还有一点是,除了每个 pod 中默认的用户之外,我们还可以在 Kubernetes 中创建特定的账号然后将一些资源的权限绑定到该账号上,然后将该账号绑定到特定的 pod 上。

                                                                                                                  Deployment

                                                                                                                  现在你己经知道如何将应用程序组件打包进容器,将它们分组到 pod 中,使用 ReplicaSet 维持 Pod 的可用性,并为它们提供临时存储或持续化存储,将密钥或配置文件注入,并可以使用 service 来使 pod 之间相互通信。但是,如果我们要升级自己的应用程序时,我们要怎么办,把原来的 Pod 都删了然后手动创建新的么?Kubernetes 当然为你准备的对应的资源,它就是 Deployment,它可以帮助你实现真正的零停机升级过程。

                                                                                                                  在 Kubernetes 中,一般使用的应用升级方式是滚动升级,下图就描述了滚动升级的过程,每当启动一个新版的 Pod 后,才会删除一个久的 Pod,最终将所有 Pod 都滚动升级到最新版本。我们可以通过 Deployment 来进行应用的部署和升级。当创建好一个 Deployment 资源后,它会创建一个 ReplicaSet,而 ReplicaSet 再进行 Pod 的维护。当要更新应用时,我们只需要修改 Deployment 资源的目标状态,Kubernetes 会帮我们处理整个中间过程。

                                                                                                                  我们可以通过如下配置文件创建一个 Deployment:

                                                                                                                    apiVersion: apps/v1beta1
                                                                                                                    kind: Deployment
                                                                                                                    metadata:
                                                                                                                    name: kubia
                                                                                                                    spec:
                                                                                                                    replicas: 3
                                                                                                                    template:
                                                                                                                    metadata:
                                                                                                                    name: kubia
                                                                                                                    labels:
                                                                                                                    app: kubia
                                                                                                                    spec:
                                                                                                                    containers:
                                                                                                                    - image: luksa/kubia:v1
                                                                                                                    name: nodejs

                                                                                                                    创建好配置文件后,可以通过 kubectl create -f kubia-deployment-v1.yaml --record
                                                                                                                    创建该 Deployment 资源,确保在创建时使用了 --record 选项。这个选项会记录历史版本号,在之后的操作中非常有用。创建好后,您可以通过 kubectl rollout status deployment kubia
                                                                                                                    查看 Deployment 的状态。

                                                                                                                    然后,假设我们现在要更新应用,我们只需要执行 kubectl set image deployment kubia nodejs=luksa/kubia:v2
                                                                                                                    就相当于修改了刚才创建的 Deployment 资源。这时候它会自动创建一个新的 ReplicaSet 资源,并滚动的升级 pod,每当新 ReplicaSet 中创建完成一个 pod 的创建就会删除旧的 ReplicaSet 中的一个 Pod,最终将所有 Pod 都更新。你可能会发现 Deployment 并没有在升级成功后直接删除旧的 ReplicaSet,而是保留了它,这是为什么呢?其实,这是为了回滚做准备,假设我们在升级的过程中,发现新版本的应用有 Bug,可以通过 kubectl rollout undo deployment kubia
                                                                                                                    进行回滚,这时候 Deployment 会停止升级,并且慢慢地将原来的 ReplicaSet 的 pod 都恢复回来。

                                                                                                                    为了让我们能够回滚到任意一个版本,Kubernetes 会一直保存之前的 ReplicaSet,我们可以通过 kubectl rollout history deployment kubia
                                                                                                                    查看所有的升级过程,还记得创建 deployment 时的 --record 参数吗?如果不给定这个参数,版本历史中的 CHANGE-CAUSE 这一栏会为空。这也会使用户很难辨别每次的版本做了哪些修改。

                                                                                                                      kubectl rollout history deployment kubia
                                                                                                                      #deployments ”kubia”:
                                                                                                                      #REVISION CHANGE-CAUSE
                                                                                                                      #2 kubectl set image deployment kubia nodejs=luksa/kubia:v2
                                                                                                                      #3 kubectl set image deployment kubia nodejs=luksa/kubia:v3

                                                                                                                      因为 Kubernetes 会为我们记录所有的更新历史,所以我们才可以通过 undo 指令回滚到任意一个特定版本kubectl rollout undo deployment kubia --to-revision=1
                                                                                                                      ,但是如果版本历史保存的过多会让 ReplicaSet 资源很混乱,所以可以通过 revisionHistoryLimit 来控制保存的历史版本数量(默认值是 10)。

                                                                                                                      在 Kubernetes 中,我们可以通过 minReadySeconds,maxSurge 和 maxUnavailable 来控制滚动升级的速率,minReadySeconds 的效果是让 Kubernetes 在 pod 就绪之后继续等待 10 秒,然后继续执行滚动升级,来减缓滚动升级的过程。而其他两个属性的效果如下:除此之外,我们还可以通过 pause
                                                                                                                      resume
                                                                                                                      命令来暂停并恢复滚动升级的过程。

                                                                                                                      有状态的 Pod

                                                                                                                      我们已经知道了每个 Pod 中看到的内容,都是独立的镜像,而且是一次性的,当 Pod 销毁时就会消失。为了解决这个问题,我们可以使用 Kubernetes 的卷资源,但是通过它我们只能保证一个 PVC 与 ReplicaSet 绑定,其中的每个 Pod 都绑定到同一个 PVC 上,但是想象一下如果我们要运行的是数据库 Pod(一个 master,多个 slave),它们每个 pod 都需要独立的持久存储空间,而之前的 ReplicaSet 只能保证所有的 Pod 共用相同的 PVC,而且如果 Pod 重启了,它还需要绑定到原来的 PVC 上这样才会不丢失数据。对于这类应用,Kubernetes 提供了 StatefulSet 来管理。接下来,我们先看看 StatefulSet 的特性。

                                                                                                                      一个 StatefulSet 创建的每个 pod 都有一个从零开始的顺序索引,这个会体现在 pod 的名称和主机名上,同样还会体现在 pod 对应的固定存储上。这些 pod 的名称则是可预知的,因为它是由 StatefulSet 的名称加该实例的顺序索引值组成的。此外,有状态的 pod 有时候需要通过其主机名来定位,而无状态的 pod 则不需要,因为每个无状态的 pod 都是一样的,在需要的时候随便选择一个即可。但对于有状态的 pod 来说,因为它们都是彼此不同的(比如拥有不同的状态),通常希望操作的是其中特定的一个,比如只想通过数据库的 master 进行写操作,slave 进行读操作。基于这个原因,每个 pod 都会有自己的域名,比如a-0.foo.default.svc.cluster.local
                                                                                                                      ,此外也可以通过 DNS 服务查找域名 foo。default.svc.cluster.local
                                                                                                                      ,它将返回所有 pod 的 ip。

                                                                                                                      最后,就像我们例子所说的那样,当 StatefulSet 的每个 Pod 都应该有一个和自己绑定的 PVC,而且当 pod 被删除后再次启动时,它必须挂载上之前的 PVC。

                                                                                                                      清楚了 StatefulSet 的特性后,就让我们来创一个 StatefulSet 资源:

                                                                                                                        apiVersion: apps/v1beta1
                                                                                                                        kind: StatefulSet
                                                                                                                        metadata:
                                                                                                                        name: kubia
                                                                                                                        spec:
                                                                                                                        serviceName: kubia
                                                                                                                        replicas: 2
                                                                                                                        template:
                                                                                                                        metadata:
                                                                                                                        labels:
                                                                                                                        app: kubia
                                                                                                                        spec:
                                                                                                                        containers:
                                                                                                                        - name: kubia
                                                                                                                        image: luksa/kubia-pet
                                                                                                                        ports:
                                                                                                                        - name: http
                                                                                                                        containerPort: 8080
                                                                                                                        volumeMounts: # pod 中 PVC 绑定路径
                                                                                                                        - name: data
                                                                                                                        mountPath: /var/data
                                                                                                                        volumeClaimTemplates: # 创建 PVC 的模板,用于为每个 pod 创建 PVC
                                                                                                                        - metadata:
                                                                                                                        name: data
                                                                                                                        spec:
                                                                                                                        resources:
                                                                                                                        requests:
                                                                                                                        storage: 1Mi
                                                                                                                        accessModes:
                                                                                                                        - ReadWriteOnce

                                                                                                                        创建好上述 StatefulSet 资源后,Kubernetes 会一个接一个的创建相应的 pod,第二个 pod 会在第一 个 pod 运行并且处于就绪状态后创建。StatefulSet 这样的行为是因为:状态明确的集群应用对同时有两个集群成员启动引起的竞争情况是非常敏感的。所以依次启动每个成员是比较安全可靠的。特定的有状态应用集群在两个或多个集群成员同时启动时引起的竞态条件是非常敏感的,所以在每个成员完全启动后再启动剩下的会更加安全。

                                                                                                                        当启动成功后,您会看到两个 pod,和两个 pvc,每个 pod 都有自己绑定的 PVC。kubia-0 <-> data-kubia-0,kubia-1 <-> data-kubia-1。这时候,如果我们删除其中一个 pod,新的 pod 可能会被调度到其他节点上去,但是旧 pod 的全部标记(名称,主机名,存储)实际上都会转移到新的 pod 上。缩容一个 StatefulSet, 然后在完成后再扩容它,与删除一个 pod 后让 StatefulSet 立马重新创建它的表现是没有区别的,删除 pod 会复用原 pod 的 PVC,而缩容后再扩容的话则会创建新的 PVC。需要记住的是,缩容一个 StatefulSet 只会删除对应的 pod, 留下卸载后的持久卷声明。

                                                                                                                        当进行 StatefulSet 的扩容后,新 pod 可能第一时间并没有数据,它可以通过 DNS 来获取现存 pod 的 ip,然后从它们那里同步数据,然后再开始对外暴露服务。


                                                                                                                        高级特性

                                                                                                                        自动扩容

                                                                                                                        我们可以通过调高 ReplicationController、ReplicaSet、Deployment 等可伸缩资源的 replicas 字段,来手动实现 pod 中应用的横向扩容。我们也可以通过增加 pod 容器的资源请求和限制来纵向扩容 pod。虽然如果你能预先知道负载何时会飘升,或者如果负载的变化是较长时间内逐渐发生的,手动扩容也是可以接受的,但指望靠人工干预来处理突发而不可预测的流量增长,仍然不够理想。好在 Kubernetes 可以监控你的 pod, 并在检测到 CPU 使用率或其他度量增长时自动对它们扩容。如果 Kubernetes 运行在云端基础架构之上,它甚至能在现有节点无法承载更多 pod 之时自动新建更多节点。

                                                                                                                        横向 pod 自动伸缩是指由控制器管理的 pod 副本数量的自动伸缩。它由 Horizontal 控制器执行,我们通过创建一个 HorizontalPodAutoscaler(HPA)资源来启用和配置 Horizontal 控制器。该控制器周期性检查 pod 度量,计算满足 HPA 资源所配置的目标数值所需的副本数量,进而调整目标资源(如 Deployment、ReplicaSet、ReplicationController、StatefulSet 等)的 replicas 字段。

                                                                                                                        Autoscaler 本身并不负责采集 pod 度量数据,而是从另外的来源获取。正如前面提到的,pod 与节点度量数据是由运行在每个节点的 kubelet 之上,名为 cAdvisor 的 agent 采集的;这些数据将由集群级的组件 Heapster 聚合。HPA 控制器向 Heapster 发起 REST 调用来获取所有 pod 度量数据。这样的数据流意味着在集群中必须运行 Heapster 才能实现自动伸缩。一旦 Autoscaler 获得了它所调整的资源(Deployment、ReplicaSet、ReplicationController 或 StatefulSet)所辖 pod 的全部度量,它便可以利用这些度量计算出所需的副本数量。它需要计算出一个合适的副本数量,以使所有副本上度量的平均值尽量接近配置的目标值。Autoscaler 控制器通过 Scale 子资源(ReplicaSet 等资源可以暴露该子资源)来修改被伸缩资源的 repplicas 字段。计算出最新的 pod 数量后,它会去更新 Scale 资源的 replicas 字段,剩下的工作就由 Replication 控制器,调度器,kublet 来完成。

                                                                                                                        计算时向上取整

                                                                                                                        下面的图中描述了 HPA 的完整工作流程:那么,我们都能通过哪些指标来进行自动扩容呢?其中自然少不了 CPU,我们可以指定一个目标使用率,这个目标使用率表示的是容器请求的 CPU 资源的使用率,如下所示,如果容器请求了 100 毫核(100/1000 个核心),然后通过命令 kubectl autoscale deployment kubia --cpu-percent=30 --min=1 --max=5
                                                                                                                        并将目标 cpu 使用率定为 30%,最大 pod 数为 5,最小为 1。这时候一旦 CPU 平均使用率高于 30%,就会向上扩容但是最多扩大到 5,反之如果低于目标使用率很多倍,则会向下缩容。

                                                                                                                          resources:
                                                                                                                          requests:
                                                                                                                          cpu: 100m

                                                                                                                          通过 CPU 来进行度量是很容易的,但基于内存的自动伸缩比基于 CPU 的困难很多。主要原因在于,扩容之后原有的 pod 需要有办法释放内存。这只能由应用完成,系统无法代劳。系统所能做的只有杀死并重启应用,希望它能比之前少占用一些内存。

                                                                                                                          除了这些常规指标之外,还有关于 QPS 的扩容,这里就不再赘述了。下面来说一说,集群节点的自动扩容,HPA 在需要的时候会创建更多的 pod 实例。但万一所有的节点都满了,放不下更多 pod 了,怎么办?这时候集群扩容可能就是最好的选择了,一般来说云服务提供商会对接 Kubernetes,让 Kubernetes 可以通过 API 来增加新的节点然后加入集群,这样就能将 Pod 运行在这些新节点上了。当节点利用率不足时,Cluster Autoscaler 也需要能够减少节点的数目。Cluster Autoscaler 通过监控所有节点上请求的 CPU 与内存来实现这一点。如果某个节点上所有 pod 请求的 CPU、内存都不到 50%, 该节点即被认定为不再需要。这并不是决定是否要归还某一节点的唯一因素。Cluster Autoscaler 也会检查是否有系统 pod (仅仅)运行在该节点上(这并不包括每个节点上都运行的服务,比如 DaemonSet 所部署的服务)。如果节点上有系统 pod 在运行,该节点就不会被归还。对非托管 pod, 以及有本地存储的 pod 也是如此, 否则就会造成这些 pod 提供的服务中断。换句话说,只有当 Cluster Autoscaler 知道节点上运行的 pod 能够重新调度到其他节点,该节点才会被归还。

                                                                                                                          当一个节点被选中下线,它首先会被标记为不可调度,随后运行其上的 pod 将被疏散至其他节点。因为所有这些 pod 都属于 ReplicaSet 或者其他控制器,它们的替代 pod 会被创建并调度到其他剩下的节点(这就是为何正被下线的节点要先标记为不可调度的原因)。

                                                                                                                          高级调度

                                                                                                                          前面我们在介绍调度器的时候,提到了筛选规则中有一个是:pod 是否能够容忍节点的污点,这又是什么呢?所谓污点就是集群管理员为集群节点打上的标记,比如kubectl taint node nodel.k8s node-type=production:NoSchedule
                                                                                                                          ,如果 pod 的声明中没有显示的说明自己能够容忍 node-type=production
                                                                                                                          的话就不会被调度到该节点上。这里大家可能会有疑问,污点声明中的 NoSchedule 是什么,它实际上污点关联的效果,加上 NoSchedule 总共有三种效果,它们的意义如下:

                                                                                                                          • NoSchedule 表示如果 pod 没有容忍这些污点,pod 则不能被调度到包含这些污点的节点上。

                                                                                                                          • PreferNoSchedule 是 NoSchedule 的一个宽松的版本,表示尽量阻止 pod 被调度到这个节点上,但是如果没有其他节点可以调度,pod 依然会被调度到这个节点上。

                                                                                                                          • NoExecute 不同于 NoSchedule 以及 PreferNoSchedule, 后两者只在调度期间起作用,而 NoExecute 也会影响正在节点上运行着的 pod。如果在一个节点上添加了 NoExecute 污点,那些在该节点上运行着的 pod, 如果没有容忍这个 NoExecute 污点,将会从这个节点去除。

                                                                                                                          继续回到刚才的例子中,如果我们希望自己的 pod 能被调度的生产环境中的节点上,可以通过如下配置:

                                                                                                                            apiVersion: extensions/v1beta1
                                                                                                                            kind: Deployment
                                                                                                                            metadata:
                                                                                                                            name: prod
                                                                                                                            spec:
                                                                                                                            replicas: 5
                                                                                                                            template:
                                                                                                                            spec:
                                                                                                                            ...
                                                                                                                            tolerations: # 允许 pod 被调到生产环境的节点上
                                                                                                                            - key: node-type
                                                                                                                            operator: Equal
                                                                                                                            value: production
                                                                                                                            effect: NoSchedule

                                                                                                                            除了污点和容忍机制外,我们还提到过亲缘性,这又是什么呢?不知道大家还记不记得之前我们通过 nodeSelector
                                                                                                                            来将 pod 只部署到有 gpu 的节点上。通过亲缘性机制,我们同样可以做到这个。

                                                                                                                              apiVersion: v1
                                                                                                                              kind: Pod
                                                                                                                              metadata:
                                                                                                                              name: kubia-gpu
                                                                                                                              spec:
                                                                                                                              affinity:
                                                                                                                              nodeAffinity:
                                                                                                                              requiredDuringSchedulingignoredDuringExecution: # 要求必须存在如下特性
                                                                                                                              nodeSelectorTerms:
                                                                                                                              - matchExpressions:
                                                                                                                              - key: gpu
                                                                                                                              operator: In
                                                                                                                              values:
                                                                                                                              - "true"

                                                                                                                              那么既然 nodeSelector 可以做到的事,还需增加一个新的亲缘性概念呢?从上面的配置文件中不难看出,亲缘性机制相较于 nodeSelector 表达性更强,除此之外,还能用来描述调度的优先度,而不是像 nodeSelector 那样是必要条件。

                                                                                                                                apiVersion: v1
                                                                                                                                kind: Pod
                                                                                                                                metadata:
                                                                                                                                name: kubia-gpu
                                                                                                                                spec:
                                                                                                                                affinity:
                                                                                                                                nodeAffinity:
                                                                                                                                preferredDuringSchedulingignoredDuringExecution: # 优先级
                                                                                                                                - weight: 80 # 节点优先调度到 zone1,这是最重要的偏好
                                                                                                                                preference:
                                                                                                                                matchExpressions:
                                                                                                                                - key: availability-zone
                                                                                                                                operator: In
                                                                                                                                values:
                                                                                                                                - zone1
                                                                                                                                - weight: 20 # 同时优先调度到独占节点,但是优先度没有上述的高
                                                                                                                                preference:
                                                                                                                                matchExpressions:
                                                                                                                                - key: share-type
                                                                                                                                operator: In
                                                                                                                                values:
                                                                                                                                - dedicated

                                                                                                                                除此之外,我们还可以用亲缘性机制做很多事,比如通过亲缘性,将所有 pod 调度到同一个机架上,这需要你先为节点设置机架标签,然后通过 topologyKey 和 labelSelector 就能达到这个效果。也能通过非亲缘性(anti-affinity),来让 pod 工作在不同的节点上。

                                                                                                                                联合集群

                                                                                                                                我们探讨了 Kubernetes 是如何处理单个机器的故障,甚至整个服务器集群或基础设施的故障的。但是如果整个数据中心出了问题,该怎么办呢?为确保你不受数据中心级别故障的影响,应用程序应同时部署在多个数据中心或云可用区域中。当其中一个数据中心或可用区域变得不可用时,可将客户端请求路由到运行在其余健康数据中心或区域中的应用程序。

                                                                                                                                虽然 Kubernetes 并不要求你在同一个数据中心内运行控制面板和节点,但为了降低它们之间的网络延迟,减少连接中断的可能性,人们还是希望将它们部署到一起。与其将单个集群分散到多个位置,更好的选择是在每个位置都有一个单独的 Kubernetes 集群。

                                                                                                                                Kubernetes 允许你通过 ClusterFederation 将多个集群组合成联合集群。它允许用户在全球不同地点运行多个集群部署和管理应用程序,同时也支持跨不同的云提供商与本地集群(混合云)相结合。ClusterFederation 的目标不仅是为了确保高可用性,还要将多个异构集群合并为一个通过单一管理界面进行管理的超级集群。

                                                                                                                                联合集群就相当于是一个特殊的集群,只不过这个集群的节点是一个个完整的集群。它的控制面板中包含存储联合 API 对象的 etcd,Federation API 服务器和 Federation Controller Manager。在联合集群中,包括 Namespace,ConfigMap,Secret,Service,Ingress,Deployment,ReplicaSet,Job,DaemonSet,HorizontalPodAutoscaler 资源。操作这些资源就像您操作 Kubernetes 集群一样,只不过这时候我们需要使用 Federation API。对于一部分联合对象来说,当你在联合 API 服务器中创建对象的时候,Federation Controller Manager 中运行的控制器会在所有底层 Kubernetes 集群中创建普通的集群内资源,并管理这些资源直到联合对象被删除为止。

                                                                                                                                对于某些联合资源类型,在底层集群中创建的资源是联合资源的精确副本;对于其的联合资源而言,情况有些不同,这些联合资源根本不会在底层集群中创建任何对应的资源。副本与原始联合版本保持同步,但是同步只是单向的,只会从联合服务器到底层集群同步。如果修改底层集群中的资源,则这些更改将不会同步到联合 API 服务器。

                                                                                                                                ReplicaSet 和 Deployment 是特例,它们不会盲目地被复制到底层集群,因为通常这不是用户想要的。毕竟,如果你创建一个期望副本数为 10 的 Deployment,那么可能你希望的并不是在每个底层集群中运行 10 个 pod 副本,而是一共需要 10 个副本。因此,当你在 Deployment 或 ReplicaSet 中指定所需的副本数时,联合控制器会在底层创建总数相同的副本。默认情况下,它们会均匀分布在集群中,当然也可以手动修改。

                                                                                                                                另一方面,联合 Ingress 资源不会导致在底层集群中创建任何 Ingress 对象。你可能还记得,Ingress 代表了外部客户访问服务的单一入口点。因此,联合 Ingress 资源创建了多底层集群范围全局入口点。

                                                                                                                                开发应用最佳实践

                                                                                                                                首先,我们看一看一个实际的应用都应该使用哪些 Kubernetes 资源。一般应用 manifest 包含了一个或者多个 Deployment 和 StatefulSet 对象。这些对象中包含了一个或者多个容器的 pod 模板,每个容器都有一个存活探针,并且为容器提供的服务(如果有的话)提供就绪探针。提供服务的 pod 是通过一个或者多个服务来暴露自己的。当需要从集群外访问这些服务的时候,要么将这些服务配置为 LoadBalancer 或者 NodePort 类型的服务,要么通过 Ingress 资源来开放服务。

                                                                                                                                pod 模板(从中创建 pod 的配置文件)通常会引用两种类型的私密凭据(Secret)。一种是从私有镜像仓库拉取镜像时使用的;另一种是 pod 中运行的进程直接使用的。私密凭据本身通常不是应用 manifest 的一部分,因为它们不是由应用开发者来配置,而是由运维团队来配置的。私密凭据通常会被分配给 ServiceAccount,然后 ServiceAccount 会被分配给每个单独的 pod。

                                                                                                                                一个应用还包含一个或者多个 ConfigMap 对象,可以用它们来初始化环境变量,或者在 pod 中以 configMap 卷来挂载。有一些 pod 会使用额外的卷,例如 emptyDir 或 qitRepo 卷,而需要持久化存储的 pod 则需要 persistentVolumeClaim 卷。PersistentVolumeClaim 也是一个应用 manifest 的一部分,而被 PersistentVolumeClaim 所引用的 StorageClass 则是由系统管理员事先创建的。

                                                                                                                                在某些情况下,一个应用还需要使用任务 Jobs 和定时任务 CronJobs。守护进程集(DaemonSet)通常不是应用部署的一部分,但是通常由系统管理员创建,以在全部或者部分节点上运行系统服务。水平 pod 扩容器(HorizontalPodAutoscaler)可以由开发者包含在应用 manifest 中或者后续由运维团队添加到系统中。集群管理员还会创建 LimitRange 和 ResourceQuota 对象,以控制每个 pod 和所有的 pod (作为一个整体)的计算资源使用情况。

                                                                                                                                注意 Pod 的生命周期

                                                                                                                                我们之前说过,可以将 pod 比作只运行单个应用的虚拟机。尽管在 pod 中运行的应用和虚拟机中运行的应用没什么不同,但是还是存在显著的差异。其中一个例子就是 pod 中运行的应用随时可能会被杀死,因为 Kubernetes 需要将这个 pod 调度到另外一个节点,或者是请求缩容。这就需要我们的应用能够做到如下几点:

                                                                                                                                • 预料到本地 IP 和主机名会发生变化

                                                                                                                                • 预料到写入磁盘的数据会消失

                                                                                                                                • 使用存储卷来跨容器持久化数据

                                                                                                                                • 对于 MYSQL 那类存储类应用,你可能还需要 StatefulSet 来保证 pod 重新调度后,仍与之前的存储卷绑定,进而不丢失数据

                                                                                                                                pod 中运行的应用和手动运行的应用之间的一个不同就是运维人员在手动部署应用的时候知道应用之间的依赖关系,这样他们就可以按照顺序来启动应用。当你使用 Kubernetes 来运行多个 pod 的应用的时候,Kubernetes 没有内置的方法来先运行某些 pod 然后等这些 pod 运行成功后再运行其他 pod。当然你也可以先发布第一个应用的配置,然后等待 pod 启动完毕再发布第二个应用的配置。

                                                                                                                                Kubernetes API 服务器确实是按照 YAML/JSON 文件中定义的对象的顺序来进行处理的,但是仅仅意味着它们在被写入到 etcd 的时候是有顺序的。无法确保 pod 会按照那个顺序启动。但是你可以阻止主容器的启动,直到它的预置条件被满足,这是通过在 pod 中包含一个叫 init 的容器来实现的。

                                                                                                                                一个 pod 可以拥有任意数量的 init 容器。init 容器是顺序执行的,并且仅当最后一个 init 容器执行完毕才会去启动主容器。换句话说,init 容器也可以用来延迟 pod 的主容器的启动。例如,直到满足某一个条件的时候。init 容器可以一直等待直到主容器所依赖的服务启动完成并可以提供服务。当这个服务启动并且可以提供服务之后,init 容器就执行结束了,然后主容器就可以启动了。这样主容器就不会发生在所依赖服务准备好之前使用它的情况了。

                                                                                                                                除此之外,不要忘了 Readiness 探针。如果一个应用在其中一个依赖缺失的情况下无法工作,那么它需要通过它的 Readiness 探针来通知这个情况,这样 Kubernetes 也会知道这个应用没有准备好。

                                                                                                                                我们已经讨论了如果使用 init 容器来介入 pod 的启动过程,另外 pod 还允许你定义两种类型的生命周期钩子:

                                                                                                                                • 启动后(Post-start)钩子

                                                                                                                                  • 这个钩子和主进程是并行执行的,并不是完全启动结束后。

                                                                                                                                • 停止前(Pre-stop)钩子

                                                                                                                                  • 停止前钩子是在容器被终止之前立即执行的。当一个容器需要终止运行的时候,Kubelet 在配置了停止前钩子的时候就会执行这个停止前钩子,并且仅在执行完钩子程序后才会向容器进程发送 SIGTERM 信号。

                                                                                                                                这些生命周期的钩子是基于每个容器来指定的,和 init 容器不同的是,init 容器是应用到整个 pod。这些钩子,如它们的名字所示,是在容器启动后和停止前执行的。生命周期钩子与存活探针和就绪探针相似的是它们都可以:

                                                                                                                                • 在容器内部执行一个命令

                                                                                                                                • 向一个 URL 发送 HTTP GET 请求

                                                                                                                                很多开发者在定义停止前钩子的时候会犯错误,他们在钩子中只向应用发送了 SIGTERM 信号。他们这样做是因为他们没有看到他们的应用接收到 Kubelet 发送的 SIGTERM 信号。应用没有接收到信号的原因并不是 Kubelet 没有发送信号,而是因为在容器内部信号没有被传递给应用的进程。如果你的容器镜像配置是通过执行一个 shell 进程,然后在 shell 进程内部执行应用进程,那么这个信号就被这个 shell 进程吞没了,这样就不会传递给子进程。

                                                                                                                                在这种情况下,合理的做法是让 shell 进程传递这个信号给应用进程,而不是添加一个停止前钩子来发送信号给应用进程。可以通过在作为主进程执行的 shell 进程内处理信号并把它传递给应用进程的方式来实现。或者如果你无法配置容器镜像执行 shell 进程,而是通过直接运行应用的二进制文件,可以通过在 DockerFile 中使用 ENTRYPOINT 或者 CMD 的 exec 方式来实现,即ENTRYPOINT ["/mybinary"]
                                                                                                                                而不是 ENTRYPOINT /mybinary
                                                                                                                                。在通过第一种方式运行二进制文件 mybinary 的容器中,这个进程就是容器的主进程,而在第二种方式中,是先运行一个 shell 作为主进程,然后 mybinary 进程作为 shell 进程的子进程运行。

                                                                                                                                当 Kubelet 意识到需要终止 pod 的时候, ]它开始终止 pod 中的每个容器。Kubelet 会给每个容器一定的时间来优雅地停止。这个时间叫作终止宽限期(Termination GracePeriod), 每个 pod 可以单独配置。在终止进程开始之后,计时器就开始计时,接着按照顺序执行以下事件:

                                                                                                                                1. 执行停止前钩子(如果配置了的话),然后等待它执行完毕

                                                                                                                                2. 向容器的主进程发送 SIGTERM 信号

                                                                                                                                3. 等待容器优雅地关闭或者等待终止宽限期超时,默认 30 秒

                                                                                                                                4. 如果容器主进程没有优雅地关闭,使用 SIGKILL 信号强制终止进程

                                                                                                                                应用应该通过启动关闭流程来响应 SIGTERM 信号,并且在流程结束后终止运行。除了处理 SIGTERM 信号,应用还可以通过停止前钩子来收到关闭通知。在这两种情况下,应用只有固定的时间来干净地终止运行。但是如果你无法预测应用需要多长时间来干净地终止运行,假设你的应用是一个分布式数据存储。在缩容的时候,其中一个 pod 的实例会被删除然后关闭。在这个关闭的过程中,这个 pod 需要将它的数据迁移到其他存活的 pod 上面以确保数据不会丢失。这时候,我推荐你创建一个专注于善后工作的 Job 资源,这个 Job 资源会运行一个新的 pod, 这个 pod 唯一的工作就是把被删除的 pod 的数据迁移到仍然存活的 pod。

                                                                                                                                但是你可能注意了,我们无法保证应用每次都能够成功创建这个 Job 对象。万一当应用要去创建 Job 的时候节点出现故障呢? 我们可以用一个专门的持续运行中的 pod 来持续检查是否存在孤立的数据。当这个 pod 发现孤立的数据的时候,它就可以把它们迁移到仍存活的 pod。当然不一定是一个持续运行的 pod, 也可以使用 CronJob 资源来周期性地运行这个 pod。

                                                                                                                                妥善处理客户端请求

                                                                                                                                毋庸赘言,你希望所有的客户端请求都能够得到妥善的处理。你显然不希望 pod 在启动或者关闭过程中出现断开连接的情况。Kubernetes 本身并没有避免这种事情的发生。你的应用需要遵循一些规则来避免遇到连接断开的情况。

                                                                                                                                首先我们要清楚,当一个 pod 启动的时候,它以服务端点的方式提供给所有的服务,这些服务的标签选择器和 pod 的标签匹配,我们前面说过 pod 需要发送新号给 Kubernetes 通知它自己准备好了之后,它才能变成一个服务端点,否则它不会接受到任何客户端连接。

                                                                                                                                你需要做的第一点就是当且仅当你的应用准备好处理进来的请求的时候,才去让就绪探针返回成功。

                                                                                                                                现在我们来看一下在 pod 生命周期的另一端,当 pod 被删除,如何妥善的处理客户端的连接。我们知道当要删除 Pod 时,会同时触发两条工作线,一条是关闭容器,一条是 kube-proxy 修改 iptables。那么怎么才能让,修改 iptables 的工作先进行然后再删除 pod 容器呢?最简单有效的办法是在进行 pod 的关闭时,等待几秒钟再开始停止接受新的连接。你无法完美地解决这个问题,但是即使增加 5 秒或者 10 秒延迟也会极大提升用户体验,它能保证之后只有少量的连接会流到这个即将关闭的 pod,除此之外,我们还要关闭这个 pod 中不活跃的长连接,然后对于那些活跃的长连接,等处理完最后一个请求后,再开始关闭应用。

                                                                                                                                管理容器

                                                                                                                                为了让 Kubernetes 的容器更好管理,我们应该合理地给镜像打标签,使用多维度而不是单维度的标签,比如:

                                                                                                                                • 资源所属的应用(或者微服务) 的名称

                                                                                                                                • 应用层级(前端、后端,等等)

                                                                                                                                • 运行环境(开发、测试、预发布、生产,等等)

                                                                                                                                • 版本号

                                                                                                                                • 发布类型(稳定版、金丝雀、蓝绿开发中的绿色或者蓝色,等等)

                                                                                                                                • 分片(带分片的系统)

                                                                                                                                可以使用注解来给你的资源添加额外的信息。资源至少应该包括一个描述资源的注解和一个描述资源负责人的注解。在微服务框架中,pod 应该包含一个注解来描述该 pod 依赖的其他服务的名称。这样就很容易展现 pod 之间的依赖关系了。

                                                                                                                                在一个生产环境系统中,你希望使用一个集中式的面向集群的日志解决方案,所以你所有的日志都会被收集并且(永久地)存储在一个中心化的位置。这样你可以查看历史日志,分析趋势。你或许已经听说过由 ElasticSearch、FluentD 和 Kibana 组成的 EFK 栈,它能很好地帮你处理各个 pod 中的日志并整合在一起。

                                                                                                                                当使用 EFK 作为集中式日志记录的时候,每个 Kubernetes 集群节点都会运行一个 FluentD 的代理(通过使用 DaemonSet 作为 pod 来部署),这个代理负责从容器搜集日志,给日志打上和 pod 相关的信息,然后把它们发送给 ElasticSearch, 然后由 ElasticSearch 来永久地存储它们。ElasticSearch 在集群中也是作为 pod 部署的。这些日志可以通过 Kibana 在 Web 浏览器中查看和分析,Kibana 是一个可视化 ElasticSearch 数据的工具。它经常也是作为 pod 来运行的,并且通过一个服务暴露出来。EFK 的三个组件如下图所示。FluentD 代理将日志文件的每一行当作一个条目存储在 ElasticSearch 数据存储中。这里就会有一个问题。当日志输出跨越多行的时候,例如 Java 的异常堆栈,就会以不同条目存储在集中式的日志记录系统中。

                                                                                                                                为了解决这个问题,可以让应用日志输出 JSON 格式的内容而不是纯文本。这样的话,一个多行的日志输出就可以作为一个条目进行存储了,也可以在 Kbiana 中以一个条目的方式显示出来。但是这种做法会让通过 kubectl logs 命令查看日志变得不太人性化了。

                                                                                                                                为了解决这个问题,可以让输出到标准输出终端的日志仍然是用户可读的日志,但是写入日志文件供 FluentD 处理的日志是 JSON 格式。这就要求在节点级别合理地配置 FluentD 代理或者给每一个 pod 增加一个轻量级的日志记录容器。

                                                                                                                                参考内容

                                                                                                                                [1] https://github.com/kubernetes/kubernetes

                                                                                                                                [2] https://kubernetes.io

                                                                                                                                [3] https://github.com/kubernetes/examples

                                                                                                                                [4] 《Kubernetes in Action》

                                                                                                                                [5] https://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/

                                                                                                                                [6] https://jimmysong.io/posts/what-is-a-pause-container/

                                                                                                                                [7] https://vernlium.github.io/2017/09/21/iptables概念介绍及相关操作-k8s-8/

                                                                                                                                [8] https://www.linuxidc.com/Linux/2016-09/134832.htm

                                                                                                                                [9] https://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/

                                                                                                                                [10] https://sookocheff.com/post/kubernetes/understanding-kubernetes-networking-model/

                                                                                                                                [11] https://medium.com/@anilkreddyr/kubernetes-with-flannel-understanding-the-networking-part-2-78b53e5364c7

                                                                                                                                [12] https://jaminzhang.github.io/lb/L4-L7-Load-Balancer-Difference/


                                                                                                                                引用链接

                                                                                                                                [1]

                                                                                                                                官方文档: https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/ha-topology/#

                                                                                                                                [2]

                                                                                                                                本地 Dashboard 页面: http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/.

                                                                                                                                [3]

                                                                                                                                官方文档: https://kubernetes.io/

                                                                                                                                文章转载自贝贝猫技术分享,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                                                                                                                评论