关于 kubernetes

1,852 阅读50分钟

Kubernetes

一、Kubernetes 介绍

Kubernetes 是一个可移植的、可扩展的开源平台,用于管理容器化的工作负载和服务,可促进声明式配置和自动化。

  • 服务发现和负载均衡

    • Kubernetes 可以使用 DNS 名称或自己的 IP 地址公开容器,如果到容器的流量很大,Kubernetes 可以负载均衡并分配网络流量,从而使部署稳定。
  • 存储编排

    • Kubernetes 允许您自动挂载您选择的存储系统,例如本地存储、公共云提供商等。
  • 自动部署和回滚

    • 您可以使用 Kubernetes 描述已部署容器的所需状态,它可以以受控的速率将实际状态更改为所需状态。例如,您可以自动化 Kubernetes 来为您的部署创建新容器,删除现有容器并将它们的所有资源用于新容器。
  • 自动二进制打包

    • Kubernetes 允许您指定每个容器所需 CPU 和内存(RAM)。当容器指定了资源请求时,Kubernetes 可以做出更好的决策来管理容器的资源。
  • 自我修复

    • Kubernetes 重新启动失败的容器、替换容器、杀死不响应用户定义的运行状况检查的容器,并且在准备好服务之前不将其通告给客户端。
  • 密钥与配置管理

    • Kubernetes 允许您存储和管理敏感信息,例如密码、OAuth 令牌和 ssh 密钥。您可以在不重建容器镜像的情况下部署和更新密钥和应用程序配置,也无需在堆栈配置中暴露密钥。

容器介绍

用 Linux 容器技术隔离组件:

容器允许你在同一台机器上运行多个服务,不仅提供不同的环境给每个服务,而且将它们互相隔离。容器类似虚拟机,但开销小很多。

一个容器里运行的进程实际上运行在宿主机的操作系统上,就像所有其他进程一样(不像虚拟机,进程是运行在不同的操作系统上的)。但在容器里的进程仍然是和其他进程隔离的。对于容器内进程本身而言,就好像是在机器和操作系统上运行的唯一一个进程。

容器的优势:

  • 敏捷应用程序的创建和部署:与使用 VM 镜像相比,提高了容器镜像创建的简便性和效率。

  • 持续开发、集成和部署:通过快速简单的回滚(由于镜像不可变性),提供可靠且频繁的容器镜像构建和部署。

  • 关注开发与运维的分离:在构建/发布时而不是在部署时创建应用程序容器镜像,从而将应用程序与基础架构分离。

  • 可观察性不仅可以显示操作系统级别的信息和指标,还可以显示应用程序的运行状况和其他指标信号。

  • 跨开发、测试和生产的环境一致性:在便携式计算机上与在云中相同地运行。

  • 云和操作系统分发的可移植性:可在 Ubuntu、RHEL、CoreOS、本地、Google Kubernetes Engine 和其他任何地方运行。

  • 以应用程序为中心的管理:提高抽象级别,从在虚拟硬件上运行 OS 到使用逻辑资源在 OS 上运行应用程序。

  • 松散耦合、分布式、弹性、解放的微服务:应用程序被分解成较小的独立部分,并且可以动态部署和管理 - 而不是在一台大型单机上整体运行。

  • 资源隔离:可预测的应用程序性能。

  • 资源利用:高效率和高密度。

容器实现隔离的机制介绍:

容器隔离是在 Linux 内核之上实现的:

  • Linux 命名空间隔离进程,它使每个进程只看到它自己的系统视图(文件、进程、网络接口、主机名等);

    • Mount(mnt)

    • Process ID(pid)

    • Inter-process communicaion(ipd)

    • UTS

    • User ID(user)

默认情况下,每个 Linux 系统最初仅有一个命名空间。所有系统资源(诸如文件系统、用户 ID、网络接口等)属于这一个命名空间。但是你能创建额外的命名空间,以及在它们之间组织资源。对于一个进程,可以在其中一个命名空间中运行它。进程将只能看到同一个命名空间下的资源。当然,会存在多种类型的多个命名空间,所以一个进程不单单只属于某一个命名空间,而属于每个类型的一个命名空间。

  • Linux 控制组 (cgroups), 限制进程的可用资源,它限制了进程能使用的资源量 (CPU、 内存、 网络带宽等)

Docker 介绍

Docker 是第一个使容器能在不同机器之间移植的系统。它不仅简化了打包应用的流程,也简化了打包应用的库和依赖,甚至整个操作系统的文件系统能被打包成一个简单的可移植的包,这个包可以被用来在任何其他运行 Docker 的机器上使用。

Docker 三个主要概念:

  • 镜像 — Docker 镜像里包含了你打包的应用程序及其所依赖的环境。它包含应用程序可用的文件系统和其他元数据

  • 镜像仓库 — Docker 镜像仓库用于存放 Docker 镜像。以及促进不同人和不同电脑之间共享这些镜像

  • 容器 — Docker 容器通常是一个 Linux 容器,它基于 Docker 镜像被创建。一个运行中的容器是一个运行在 Docker 主机上的进程,但它和主机,以及所有运行在主机上的其他进程都是隔离的。这个进程也是资源受限的,意味着它只能访问和使用分配给它的资源(CPU、内存等)

镜像层

不同镜像可能包含完全相同的层,因为这些 Docker 镜像都是基于另一个镜像之上构建的,不同的镜像都能使用相同的父镜像作为它们的基础镜像。这提升了镜像在网络上的分发效率,当传输某个镜像时,因为相同的层已被之前的镜像传输,那么这些层就不需要再被传输。

层不仅使分发更高效,也有助于减少镜像的存储空间。每一层仅被存一次,当基于相同基础层的镜像被创建成两个容器时,它们就能够读相同的文件。但是如果其中一个容器写入某些文件,另外一个是无法看见文件变更的。因此,即使它们共享文件,仍然彼此隔离。这是因为容器镜像层是只读的。容器运行时,一个新的可写层在镜像层之上被创建。容器中进程写入位于底层的一个文件时,此文件的一个拷贝在顶层被创建, 进程写的是此拷贝。

容器镜像可移植性的限制

理论上,一个容器镜像能运行在任何一个运行 Docker 的机器上。但有一个小警告:一个关于运行在一台机器上的所有容器共享主机Linux内核的警告。如果一个容器化的应用需要一个特定的内核版本,那它可能不能在每台机器上都工作。如果一台机器上运行了一个不匹配的Linux内核版本,或者没有相同内核模块可用,那么此应用就不能在其上运行。

Kubernetes 介绍

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

Kubernetes 集群架构

在硬件级别,Kubernetes 集群由很多节点组成,这些节点被分成以下两种类型:

  • 主节点(控制面板/master):它承载 Kubernetes 控制和管理整个集群系统的控制面板。它包含多个组件,组件可以运行在单个主节点上或者通过副本分别部署在多个主节点以确保高可用性。包括

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

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

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

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

  • 工作节点:运行容器化应用的机器。它们运行用户实际部署的应用。包括

    • Docker、rtk 或其他的容器类型

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

    • kube-proxy:它负责组件之间的负载均衡网络流量

使用 k8s 的好处

  • 简化应用程序部署

  • 更好地利用硬件

  • 健康检查和自我修复

  • 自动扩缩容

二、开始使用 k8s 和 Docker

创建、运行及共享容器镜像

  • 安装 Docker

  • 创建应用

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

  • 基于镜像运行容器

  • 把镜像推送到 Docker Hub,这样任何人在任何地方都可以使用

需要说明的是构建镜像的过程不是由 Docker 客户端进行的,而是将整个目录的文件上传到 Docker 守护进程并在那里进行的。

Docker 命令大全

Docker 命令大全

kubectl 命令大全

kubectl 命令大全

# kubectl --help

annotate       添加或更新一个或多个资源的注释
api-resources  Print the supported API resources on the server
api-versions   列出可用的API版本
apply          通过文件名或标准输入流(stdin)对资源进行配置
attach         连接到正在运行的容器上,以查看输出流或与容器交互(stdin)
auth           Inspect authorization
autoscale      自动扩宿容由副本控制器管理的Pod
certificate    修改 certificate 资源
cluster-info   显示集群信息
completion     Output shell completion code for the specified shell (bash or zsh)
config         修改 kubeconfig 文件
convert        在不同的 API versions 转换配置文件
cordon         标记 node 为 unschedulable
cp             复制 files 和 directories 到 containers 和从容器中复制 files 和 directories
create         从文件或stdin中创建一个或多个资源对象
delete         删除资源对象
describe       显示一个或者多个资源对象的详细信息
diff           Diff live version against would-be applied version
drain          Drain node in preparation for maintenance
edit           通过默认编辑器编辑和更新服务器上的一个或多个资源对象
exec           在Pod的容器中执行一个命令
explain        查看资源的文档
expose         为副本控制器、服务或Pod等暴露一个新的服务
get            显示一个或更多 resources
label          添加或更新一个或者多个资源对象的标签
logs           输出容器在 pod 中的日志
uncordon       标记 node 为 schedulable
patch          使用策略合并补丁过程更新资源对象中的一个或多个字段
plugin         Provides utilities for interacting with plugins
port-forward   将一个或多个本地端口转发到Pod
proxy          运行一个 proxy 到 Kubernetes API server
replace        从文件或stdin中替换资源对象 kubectl replace -f FILENAME
run            在集群中运行一个指定的镜像
rollout        Manage the rollout of a resource
scale          为 Deployment, ReplicaSet, Replication Controller 或者 Job 设置一个新的副本数量
set            为 objects 设置一个指定的特征
taint          更新一个或者多个 node 上的 taints
top            Display Resource (CPU/Memory/Storage) usage
version        输出 client 和 server 的版本信息
wait           Experimental: Wait for a specific condition on one or many resources
$ kubectl get|describe 
  apiservices
  |certificatesigningrequests(csr)
  |clusters
  |clusterrolebindings
  |clusterroles
  |componentstatuses(cs)
  |configmaps(cm)
  |controllerrevisions
  |cronjobs
  |customresourcedefinition(crd)
  |daemonsets(ds)
  |deployments(deploy)
  |endpoints(ep)
  |events(ev)
  |horizontalpodautoscalers(hpa)
  |ingresses(ing)
  |jobs
  |limitranges(limits)
  |namespaces(ns)
  |networkpolicies(netpol)
  |nodes(no)
  |persistentvolumeclaims(pvc)
  |persistentvolumes(pv)
  |poddisruptionbudget(pdb)
  |podpreset
  |pods(po)
  |podsecuritypolicies(psp)
  |podtemplates
  |replicasets(rs)
  |replicationcontrollers(rc)
  |resourcequotas(quota)
  |rolebindings
  |roles
  |secrets
  |serviceaccounts(sa)
  |services(svc)
  |statefulstes
  |storageclasses(sc)
  |... 
  (-o|--output=)yaml|json|wide    // 列出各种 k8s 对象

介绍 pod

一个 pod 是一组紧密相关的容器,它们总是一起运行在同一工作节点上,以及同一个 Linux 命名空间。每个 pod 就像一个独立的逻辑集器,拥有自己的 IP,主机名,进程等,运行一个独立的应用程序。应用程序可以是单个进程,运行在单个容器中,也可以是一个主应用进程或者其他支持进程,每个进程都在自己的容器中运行。

部署应用到 k8s 时幕后发生的事情:

首先,构建镜像并将其推送到 Docker Hub。因为在本地机器上构建的镜像只能在本地机器上可用,但是需要使它可以访问运行在工作节点上 Docker 守护进程。当运行 kubectl 命令时 通过向 K8s API 服务器发送一个 REST HTTP 请求,在集群中创建一个新的 RC 对象。然后,RC 建了一个新的 pod,调度器将其调度到一个工作节点上。Kubelet 看到 pod 被调度到节点上,就告知 Docker 从镜像中心中拉取指定的镜像,因为本地没有该镜像。下载镜像后,Docker 创建并运行容器。

三、pod: 运行于 k8s 中的容器

pod 是一组并置的容器,代表了 k8s 中的基本构建模块

注意:当一个 pod 中包含多个容器时,这些容器总是运行于同一工作节点上,一个 pod 绝对不会跨越多个工作节点

介绍平坦 pod 间网络

k8s 集群中的所有 pod 都在同一个共享网络地址空间中, 这意味着每个 pod 都可以通过其他 pod 的 IP 地址来实现相互访问。 换句话说,这也表示它们之间没有NAT (网络地址转换) 网关。当两个 pod 彼此之间发送网络数据包时,它们都会将对方的实际 IP 地址看作数据包中的源 IP

pod 是逻辑主机,其行为与非容器世界中的物理主机或虚拟机非常相似。此外,运行在同一个 pod 中的进程与运行在同一物理机或虚拟机上的进程相似,只是每个进程都封装在一个容器之中。

决定何时在 pod 中使用多个容器:

  • 它们需要一起运行还是可以在不同主机运行

  • 它们代表的是一个整体还是相互独立的组件

  • 它们必须一起进性扩缩容还是可以分别进性

apiVersion: v1                                             // API组/API版本号, 这里省略了 API 
kind: Pod                                                  // 资源类型
metadata:                                                  // 元数据
  annotations:                                             // 注解列表
    kubernetes.io/created-by: theway
  creationTimestamp: 2016-03-18T12:37:50Z                  // 创建时间
  generateName: kubia-
  labels:                                                  // 标签列表
    run: kubia
  name: kubia-zxzij                                        // pod 名称
  namespace: default                                       // 所在命名空间
  resourceVersion: "294"
  selfLink: /api/vi/namespace/default/pods/kubia-zxzij
  uid: xxx                                                 // uid
spec:                                                      // 实际说明
  nodeSelector:                                            // 节点选择器
    gpu: "true"                                            //  pod 调度到包含标签 gpu=ture 的节点上
  containers:                                              // 容器列表
  - image: myDockerHubID/kubia                             // 容器镜像
    imagePullPolicy: Always|Never|IfNotPresent             // 镜像拉取策略
    name: kubia                                            // 容器名称
    comand: [String]                                       // 容器的启动命令列表,如果不指定,则使用镜像打包时使用的启动命令
    args: [String]                                         // 容器的启动命令参数列表
    ports:                                                 // 容器端口号列表
    - name: http                                           // 容器监听的 8080 端口 被命名为 http
      containerPort: 8080                                  // 容器监听的端口
      protocol: TCP                                        // 端口协议,支持 TCP  UDP,默认值为 TCP
    resources:                                             // 资源限制和资源请求的设置
      requests:                                            // 资源请求的设置
        cpu: 10m                                           // cpu 请求,单位为 core,容器启动的初始可用数量
        memory: 10Mi                                       // 内存请求,单位可以为 MB,GB 等,容器启动的初始可用数量
      limits:                                              // 资源限制的设置
        cpu: '1'                                           // CPU 限制,单位为 core
        memory: 1000Mi                                     // 内存限制,单位可以为 MB,GB 
    terminationMessagePath: /dev/termination-log
    volumnMounts:                                          // 挂载到容器内部的存储卷配置
    - mountPath: /var/run/secrets/k8s.io/servacc           // 卷挂载路径
      name: default-token-kvcqa                            // 关联使用卷的卷名称
      readOnly: true                                       // 是否为只读模式,默认为读写模式
    livenessProbe:                                         // 存活探针,  Pod 内各容器健康检查的设置,当探测无响应几次之后,系统将自动重启该容器。可以设置的方法包括:exec、httpGet  tcpSocket。对一个容器只需要设置一种健康检查的方法
      exec:                                                //  Pod 内各容器健康检查的设置,exec方式
        command: [String]                                  // exec方式需要指定的命令或者脚本
      httpGet:                                             //  Pod 内各容器健康检查的设置,HTTGet 方式。需要指定path、port
        path: /                                            // HTTP 请求的路径
        port: 8080                                         // 探针连接的网络端口
        host: String
        scheme: String
        httpHeaders:
        - name: String
          value: String
      tcpSocket:                                           //  Pod 内各容器健康检查的设置,tcpSocket 方式
        port: Number
      initialDelaySeconds: Number                          // 初始延迟(Kubernetes 会在第一次探测前等待15秒)
      timeoutSeconds: Number                               // 容器健康检查的探测等待响应的超时时间设置,单位为 s,默认值为1s。若超过该超时时间设置,则将认为该容器不健康,会重启该容器。
      periodSeconds: Number                                // 对容器健康检查的定期探测时间设置,单位为 s,默认 10s 探测一次
      successThreshold: 0
      failureThreshold: 0
    readinessProbe:                                        // pod 的每个容器都会有一个就绪探针
      periodSeconds: 1s                                    // 定义一个就绪探针并每个一秒执行一次
      exec:                                                // 就绪探针将定期在容器内执行 ls/var/ready 命令。如果文件存在,则 ls 命令返回退出码 0 代表成功,否则返回非 0 的退出码,代表失败。
        command:
        - ls
        - /var/ready
    readinessProbe:                                        // HTTP GET 的就绪探针
      periodSeconds: 1s
      httpGet:
        path: /
        port: 8080
    env:                                                   // 容器运行前需设置的环境变量列表
    - name: POD_NAME
      valueFrom:                                           // 引用 pod manifest 中的元数据名称字段,而不是设定一个具体的值
        fieldRef: 
          fieldPath: metadata.name
    - name: CONTAINER_CPU_REQUEST_MILLICORES
      valueFrom:
        resourceFieldRef:                                  // 容器请求的 CPU 和内存使用量是引用 resourceFieldRef 字段而不是 fieldRef 字段
          resource: requests.cpu
          divisor: lm                                      // 对于资源相关的字段,我们定义一个基数单位从而生成每一部分的值
  dnsPolicy: Default|ClusterFirst|ClusterFirstWithHostNet|None     // 是否使用内部的 DNS 服务器(默认:ClusterFirst,具体用法查阅相关文档)
  imagePullSecrets:                                        // pull 镜像时使用的 Secret 名称,以 name:secretkey 格式指定
    name: String
  hostNetwork: Boolean                                     // 是否使用主机网络模式,默认值为 false。设置为 true 表示容器使用宿主机网络,不再使用 docker 网桥,该 Pod 将无法在同一台宿主机上启动第二个副本
  nodeName: gke-kubia-e8...-node-txje
  restartPolicy: Always|Never|OnFailure                    // 定义容器的重启策略
  serviceAccount: default
  serviceAccountName: foo                                  // 使用 foo sa,而不是默认的 default sa
  terminationGracePeriodSeconds: 30
  volumns:                                                 // 在该Pod上定义的共享存储卷列表列表
  - name: String                                           // 共享存储卷的名称
    emptyDir: {}                                          // 类型为 emptyDir 的存储卷
    hostPath:                                             // 类型为 hostPath 的存储卷,表示挂载 Pod 所在宿主机的目录
      path: String                                        // pod 所在主机的目录,将被用于容器中 mount 的目录
    secret:                                               // 类型为 secret 的存储卷,表示挂载集群预定义的 secret 对象到容器内部
      secretName: String
      items:
      - key: String
        path: String
    configMap:                                            // 类型为 configMap 的存储卷,表示挂载集群预定义的 configMap 对象到容器内部
      name: String
      items:
      - key: String
        path: String
  - name: downward                     // 通过将卷的名字设定为 downward 来定义一个 dowanwardAPI 
    downwardAPI:
      items: 
      - path: "podName"                // pod的名称(来自 manifest 文件中的 metadate.name 字段)将被写入 podName 文件中
        fieldRef:
        fieldPath: metadata.name
      - path: "labels"                 // pod 的标签将被保存到 /etc/dowanward/labels 文件中
        fieldRef:
          fieldPath: metadata.labels
      - path: "annotations"           // pod 的标签将被保存到 /etc/dowanward/annotations 文件中
        fieldRef:
          fieldPath: metadata.annotations
      - path: "containerCpuRequestMilliCores"
        resourceFieldRef:
          containerName: main          // 当暴露容器级的元数据时, 必须指定引用资源字段对应的容器名称
          resource: requests.cpu 
          divisor: lm
status:                                                    // pod 当前信息
  conditions:                                              // pod 所处的条件
  - lastProbeTime: null
    lastTransitionTime: null
    status: "True"
    type: Ready
  containerStatuses:                                       // 每个容器的描述和状态
  - containerID: docker://f02...                           // 容器 ID
    image: myDockerHubID/kubia                             // 容器镜像
    imageID: docker://4c...                                // 镜像 ID
    lastState: {}
    name: kubia                                            // 容器名称
    ready: true                                            // 
    restartCount: 0                                        // 容器重启次数
    state:                                                 // 容器状态
      running:                                           
        starteAt: 2016-03-18T12:46:05Z                     // 启动时间
  hostIP: 10.132.0.4                                       // 
  phase: Running
  podIP: 10.0.2.3
  startTime: 2016-03-18T12:44:32Z
$ kubectl create -f xxxx.yaml // 从 xxxx.yaml 创建 pod

向 pod 发送请求

  • 创建与 pod 相关联的 services (在之后介绍该方法)

  • 通过端口转发

$ kubectl port-forward <pod name> <local port>:<pod port>

介绍标签

标签是一种简单却功能强大的 k8s 特性,不仅可以组织 pod, 也可以组织** 所有其他的 k8s 资源 **

定义: 金丝雀发布是指在部署新版本时,先只让一小部分用户体验新版本以观察新版本的表现,然后再向所有用户进行推广,这样可以防止暴露有问题的版本给过多的用户。

// 标签适用于所有 k8s 资源

$ kubectl get po --show-labels

$ kubectl get po -l|L <label key>

$ kubectl get po -l <label key1>=<label value1>, <label key2>=<label value2>...

$ kubectl get po -l '!<label key>'

$ kubectl get po -l <label key>!=<label value>

$ kubectl get po -l <label key> in (x, xx, xxx, ...)

$ kubectl get po -l <label key> notin (x, xx, xxx...)

$ kubectl label po <pod name> <label key>=<label value> // 给现有的 pod 添加标签

$ kubectl label po <pod name> <label key>=<label value> --overwrite // 给现有的 pod 修改标签

注解

$ kubectl annotate pod <pod name> <annotation key>=<annotation value> // 向 pod 上添加注解

$ kubectl annotate pod <pod name> <annotation key>=<annotation value> --overwrite // 修改 pod 上原有的注解

使用命名空间对资源名称进行分组

k8s 命名空间简单地为对象名称提供了一个作用域。此时我们并不会将所有资源都放在同一个命名空间中,而是将它们组织到多个命名空间中,这样可以允许我们多次使用相同的资源名称(跨不同的命名空间); 除了隔离资源,命名空间还可用于仅允许某些用户访问某些特定资源,甚至限制单个用户可用的计算资源数量。

$ kubectl create namespace <namespace name>
apiVersion: v1
kind: Namespace            // 资源类型
metadata:
  name: <namespace name>   // 资源名称
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    kubesphere.io/creator: yangxiaofei95
  creationTimestamp: "2020-04-03T06:58:52Z"
  finalizers:
  - finalizers.kubesphere.io/namespaces
  labels:
    kubesphere.io/workspace: nc00451844
  name: qifei
  ownerReferences:
  - apiVersion: tenant.kubesphere.io/v1alpha1
    blockOwnerDeletion: true
    controller: true
    kind: Workspace
    name: nc00451844
    uid: 04c66727-7288-11ea-90dd-005056bdf71e
  resourceVersion: "48601586"
  selfLink: /api/v1/namespaces/qifei
  uid: 941736c4-7578-11ea-84b7-005056bd9e34
spec:
  finalizers:
  - kubernetes
status:
  phase: Active

停止和删除 pod

$ kubectl delete po <pod name1> <pod name2> ... // 按名称删除 pod

$ kubectl delete po -l <labe key>=<labe value> // 使用标签选择器删除命名空间(其余形式参考 pod)

$ kubectl delete ns <namespace name> // 通过删除整个命名空间来删除pod

$ kubectl delete po --all // 删除当前命名空间中的所有 pod (注意:如果 pod 是由 rc 等控制器创建的,rc 会重新创建新的 pod)

$ kubectl delete all --all // 删除当前命名空间中所有资源

四、副本机制和其他控制器:部署托管的 pod

介绍存活探针

可以为 pod 中的每个容器单独指定存活探针, 检查容器是否还在运行。如果探测失败,k8s 将定期执行探针并重启容器

存活探针探测容器的机制:

  • HTTP GET 探针对容器的 IP 地址(你指定的端口和路径)执行 HTTP GET 请求

    • 如果探测器收到响应,并且相应状态码不代表错误(HTTP 响应状态码是 2xx 或 3xx),则认为探测成功。如果服务器返回错误响应状态码或者根本没有响应,那么探测就被认为是失败的,容器将被重启
  • TCP 套接字探针尝试与容器指定端口建立 TCP 连接。如果连接成功建立,则探测成功。否则,重启容器

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

注意:当容器被强制终止时,会创建一个全新的容器,而不是重新启动原来的容器。务必记得设置一个初始延迟来说明应用程序的启动时间

提示:为了更好地进行存活检查,需要将探针配置为请求特定地 URL 路径(例如: /health),并让应用从内部对内部运行的所有重要组件执行状态检查,以确保它们没有终止或停止响应。请确保 /healtth HTTP 端点不需要认证,否则探测会一直失败,导致你的容器无限重启。一定要检查应用程序的内部,而没有外部因素的影响,例如:服务器无法连接到后端数据库时。

创建有效的存活探针

  • 一定要检查应用程序的内部,而没有任何外部因素的影响。

    • 例如,当服务器无法连接到后端数据库时,前端 Web 服务器的存活探针不应该返回失败。
  • 保持探针轻量

    • 存活探针不应消耗太多的计算资源,并且运行不应该花太长时间。默认情况下,探测器执行的频率相对较高,必须在一秒之内执行完毕。一个过重的探针会大大减慢你的容器运行。探针的 CPU 时间计入容器的 CPU 时间配额,因此使用重量级的存活探针将减少主应用程序进程可用的 CPU 时间。
  • 无须在探针中实现重试循环

    • 探针的失败阙值是可配置的,并且通常在容器被终止之前探针必须失败多次。但即使你将失败阙值设置为 1, Kubernetes 为了确认一次探测的失败,会尝试若干次。因此在探针中自己实现重试循环是浪费精力。

了解 ReplicationController

rc 会持续监听正在运行的 pod 列表,并保证 pod 的数目始终与期望值相符,也就是说 pod 的数量始终与其标签选择器匹配,如果不匹配,rc 将根据所需,采用适当的操作来协调 pod 的数量。

rc 的主要三部分:

  • label selector 标签选择器,用于确定 rc 作用域中有哪些 pod

  • replica count 副本个数,指定应运行的 pod 数量

  • pod template pod 模板,用于创建新的 pod

更改 rc 的标签选择器或 pod 模板的效果

更改 rc 的标签选择器或 pod 模板对现有的 pod 没有任何影响。更改标签选择器会使现有的 pod 脱离 rc 的范围,因此控制器会停止关注它们。在创建 pod 后,rc 也不关心其 pod 的实际‘内容’,因此,更改模板仅影响由此 rc 创建的新 pod。

使用 rc 的好处:

  • 确保一个或多个 pod 持续运行

  • 集群节点发生故障时,它将为故障节点上运行的所有 pod(即受 rc 控制的节点上的那些 pod) 创建替代副本

  • 它能轻松实现 pod 的水平伸缩

注意: pod 实例永远不会重新安置到另一个节点。相反,rc 会创建一个全新的 pod 实例,它与正在替换的实例无关。

创建 rc

apiVersion: v1
kind: ReplicationController                // 资源类型
metadata:                                  // 元数据
  name: kubia                              // rc 名称
spec:
  replicae: 3                              // pod 实际的目标数目
  selector:                                // pod 选择器决定了 rc 的操作对象
    app: kubia
  template:                                // pod 模板
    metadata:
      labels:
        app: kubia
    spec:
      containers
      - image: myDockerHubID/kubia
        name: kubia
        ports:
        - containerPort: 8080

提示: 定义 rc 时不要指定 pod 选择器,让 K8s 从 pod 模板中提取它。这样 YAML 更简短。

k8s 在重新调度 pod 之前会等待一段时间(如果节点因临时网络故障或者 Kubelet 重新启动而无法访问)。如果节点在几分钟之内无法访问,则调度到该节点的 pod 状态将变为 Unknown。此时,rc 将立即启动一个新的 pod。

了解 ReplicaSet

最初,rc 是用于复制和在异常时重新调度节点的唯一 K8s 组件,后来又引入了一个名为 rs 的类似资源。它是新一代的 rc, 并且将其完全替换掉 (rc 最终将被弃用)。你本可以通过创建一个 rs 而不是一个 rc 来开始本章,但是笔者觉得从 K8s 最初提供的组件开始是个好主意。另外,你仍然可以看到使用中的 rc, 所以你最好知道它们。也就是说从现在起,你应该始终创建 rs 而不是 rc。

rs 的行为与 rc 完全相同,但 pod 选择器的表达能力更强。

apiVersion: apps/v1beta2(API组/API版本)            // 属于 apps API 组的 vibeta2 版本
kind: ReplicaSet                                    // 资源类型
metadata:
  name: kubia                                       // 资源名称
spec: 
  replicas: 3                                       // 期望运行的 pod 数量
  selector:                                         // 这里使用了更简单的 matchLabels 选择器
    matchLabels:
      app: kubia
  template:
    ...
// 这里展示 rs 选择器的强大功能
selector:
  matchExpressions:
  - key: app
    operator: In
    values:
    - kubia

可以给选择器添加额外的表达式。如示例,每个表达式都必须包含一个 key、一个 operator(运算符),并且可能还有一个 values 的列表(取决于运算符)

  • In:Label 的值必须与其中一个指定的 values 匹配

  • NotIn:Label 的值与任何指定的 values 不匹配

  • Exists:pod 必须包含一个指定名称的标签(值不重要)。使用此运算符时,不应该指定 values 字段

  • DoesNotExists:pod 不得包含有指定名称的标签。values 属性不得指定

使用 DaemonSet 在每个节点上运行一个 pod

当希望 pod 在集群中的每个节点上运行并且每个节点都需要正好一个运行的 pod 时,可以选择 DaemonSet。

DaemonSet 创建的 pod,已经有一个指定的目标节点并跳过 k8s 调度程序。

DaemonSet 确保创建足够的 pod,并在自己的节点上部署每个 pod。所以并不存在期望的数量这一概念。

有 DaemonSet 管理的 pod,如果节点下线,DaemonSet 不会在其他地方重新创建 pod。但是当一个新节点被添加至集群中时,DaemonSet 会立刻部署一个新的 pod 实例。如果删除其中一个由 DeamonSet 管理的 pod,那么它也会重新创建一个新的 pod。

DaemonSet 将 pod 部署到集群中的所有节点上,除非指定这些 pod 只在部分节点上运行。这是通过 pod 模板中 nodeSelector 属性指定的。

之后,你将了解到节点可以被设置为不可调度的,防止 pod 被部署到节点上,DaemonSet 甚至会将 pod 部署到这些节点上,因为无法调度的属性只会被调度器使用,而 DaemonSet 管理 pod 完全绕过调度器,因为 DaemonSet 的目的是运行系统服务,即使是在不可调度的节点上,系统服务通常也需要运行。

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 的节点(如果不指定,则会在每个节点上创建这个 pod)
        disk: ssd
      containers:
      - image: myDockerHubID/ssd-monitor
        name: main

执行单个任务的 Job

Job 允许你运行一种 pod,该 pod 在内部进程成功结束时,不重启容器。一旦任务完成,该 pod 就被认为处于完成状态。

在发生节点故障时,该节点上由 Job 管理的 pod 将被重新安排到其他节点。如果进程本身异常退出,可以将 Job 配置为重新启动容器。

apiVersion: batch/v1                        // 属于 batch API 组的 v1 版本
kind: Job                                   // 资源类型
metadata:
  name: batch-job
spec:
  template:                                 // 你没有指定 pod 选择器,它将根据模板中的标签创建
    metadata:
      labels:
        app: batch-job
    spec:
      activeDeadlineSeconds: 10m             // 限制 pod 完成任务的时间(如果pod运行时间超过此时间,系统将尝试终止 pod, 并将Job标记为失败)
      backoffLimit: 10                       //  pod 标记为失败之前,可以重启的次数,默认值为 6
      completions: 5                         // 该任务必须确保 5  pod 完成(在发生错误时,Job 会创建 5 个或 5 个以上的 pod)
      parallelism: 2                         // 最多两个 pod 可以并行运行
      restartPolicy: OnFailure|Never         // Job 不能使用 Always 为默认的重新启动策略
      containers:
      - image: myDockerHubID/batch-job
        name: main

任务完成后 pod 未被删除的原因是允许你查阅其日志。

了解 CronJob

Job 资源在创建时会立即运行 pod, 但是许多批处理任务需要在特定的时间运行,或者在指定的时间间隔内重复运行。CronJob 可以支持该功能。

apiVersion: batch/v1beta1                     // batch API 组的 v1beta1 版本
kind: CronJob                                 // 资源类型
metadata:
  name: batch-job
spec:
  schedule: "0, 15, 30, 45 * * * *"           // 调度时间表
  startingDeadlineSeconds: 15                 // pod 最迟必须在预定时间后 15 秒内开始运行
  jobTemplate:                                // 创建 Job 的模板
    spec:
      template:
        metadata:
          labels:
            app: periodic-batch-job
        spec:
          restartPolicy: OnFailure
          containers:
          - images: myDockerHubID/batch-job
            name: main

schedule 的时间表解析(从左往右):

  • 分钟

  • 小时

  • 每月中的第几天

  • 星期几

五、服务:让客户端发现 pod 并与之通信

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

通过为前端 pod 创建服务,并且将其配置成可以在集群外部访问,可以暴露一个单一不变的 IP 地址让外部的客户端连接 pod。同理,可以为后台数据库 pod 创建服务,并为其分配一个固定的 IP 地址。尽管 pod 的 I P地址会改变,但是服务的 IP 地址固定不变。另外,通过创建服务,能够让前端的 pod 通过环境变量或 DNS 以及服务名来访问后端服务。

apiVersion: v1
kind: Service
metadata:
  name: kubia                            // 如果没有定义选择器,则要手动配置 endpoint 列表与该服务关联 pod,这里的 name 字段必须和 Endpoint 对象的名字相匹配(选择器在 spec 属性下指定)
  annotations:
    service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"   // 告诉 k8s 无论 pod 的准备状态如何,将所有的 pod 添加到服务中(旧版本)
    service.alpha.kubernetes.io/publishNotReadyAddresses: "true"     // 告诉 k8s 无论 pod 的准备状态如何,将所有的 pod 添加到服务中(新版本)
spec:
  type: ClusterIP|ExternalName|NodePort|LoadBalancer   // 服务类型
  externalTrafficPolicy: Local           // 将外部通信重定向到接收连接的节点上运行的 pod 来阻止额外的网络跳转数(缺点:首先,如果没有本地 pod 存在,则连接将挂起(它不会像不使用 externalTrafficPolicy 那样,将其转发到随机的全局 pod )。因此,需要确保负载平衡器将连接转发给至少具有一个 pod 的节点;其次,某些情况下连接不会均匀分布到所有 pod)
  sessionAffinity: ClientIP|None // 会话亲和性(设置为 ClientIP,会使服务代理将来自一个 client IP 的请求转发至同一个 pod 上)
  ports:
  - name: http            // 将服务的 80 端口映射到被称为 http 的端口
    port: 80              // 该服务可用端口
    targetPort: http      // 目标端口
  - name: https
    port: 443
    targetPort: https
  selector:                 // 选择器(标签选择器应用于整个服务,不能对每个端口做单独的配置。如果不同的 pod 有不同的端口映射关系,需要创建两个服务)
    app: kubia              // 具有 app=kubia 的 pod 都属于该服务
aoiVersion: vi
kind: Pod
metadata:
  name: kubia
spec:
  containers:
  - name: kubia
    ports:
    - name: http                // 端口 8080 被命名为 http
      containerPort: 8080
    - name: https               // 端口 8443 被命名为 https
      containerPort: 8443

为什么要采用命名端口的方式? 最大的好处就是即使更换端口号也无须更改服务的 spec。

服务发现:

  • 通过环境变量发现服务

    • 在 pod 开始运行的时候,k8s 会初始化一系列的环境变量指向现在存在的 service。如果你创建的服务早于客户端 pod 的创建,pod 上的进程可以根据环境变量获得服务的 IP 地址和端口号。
    $ kubectl exec <pod name> env
    
     =>
    
     PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin/usr/bin:/sbin:/bin 
     ...
     KUBIA_SERVICE_HOST=l0.111.249.153   // 这是服务的集群 IP
     KUBIA_SERVICE_PORT=80               // 这是服务所在的端口
    

    回顾本章开始部分的前后端的例子,当前端 pod 需要后端数据库服务 pod 时,可以通过名为 backend-database 的服务将后端 pod 暴露出来,然后前端 pod 通过环境变量 BACKEND_DATABASE_SERVICE_HOST 和 BACKEND_DATABASE_SERVICE_PORT 去获得 IP 地址和端口信息。

    注意: 服务名称中的横杠被转换为下画线,并且当服务名称用作环境变量名称中的前级时,所有的字母都是大写的。

  • 通过 DNS 发现服务

    • kube-system 命名空间包含一个被称作 kube-dns 的 pod,这个 pod 运行 DNS 服务,在集群中其他 pod 都被配置成使用其作为 dns(k8s 通过修改每个容器的 /etc/resolv.conf 文件实现)。运行在 pod 上的进程 DNS 查询都会被 k8s 自身的 DNS 服务器响应,该服务器知道系统中运行的所有服务。

    每个服务从内部 DNS 服务器中获得一个 DNS 条目, 客户端的 pod 在知道服务名称的情况下可以通过全限定域名 FQDN 来访问

    通过 FQDN 连接服务: 再次回顾前端-后端的例子,前端 pod 可以通过打开以下 FQDN 的连接来访问后端数据库服务

    backend-database.default.svc.cluster.local 
    

    backend-database 对应于服务名称,default 表示服务在其中定义的名称空间,而 svc.cluster.local 是在所有集群本地服务名称中使用的可配置集群域后缀。

    注意: 客户端仍然必须知道服务的端口号。如果服务使用标准端口号(例如,HTTP 的 80 端口 或 Postgres 的5432 端口号),这样是没问题的。如果并不是标准端口,客户端可以从环境变量中获取端口号。

    连接一个服务可能比这更简单。如果前端 pod 和数据库 pod 在同一个命名空间下,可以省略 svc.cluster.local 后缀,甚至命名空间。 因此可以使用 backend-database 来指代服务。

连接集群外部的服务

服务并不是和 pod 直接相连的。相反,有一种资源介于两者之间,它就是 endpoint 资源。endpoint 资源就是暴露一个服务的 IP 和端口列表。

$ kubectl get endpoints <service name> // 获取 endpoint

尽管在 spec 服务中定义了 pod 选择器,但在重定向传入连接时不会直接使用它。相反,选择器用于构建 IP 和端口列表,然后存储在 endpoint 资源中。当客户端连接到服务时,服务代理选择这些 IP 和端口对中的一个,并将传入连接重定向到在该位置监听的服务器。

手动配置服务的 endpoint

如果创建了不包含 pod 选择器的服务,k8s 将不会创建 endpoint 资源(毕竟,缺少选择器,将不会知道服务中包含哪些 pod)。这样就需要创建 endpoint 来指定该服务的 endpoint 列表。

apiVersion: v1
kind: Service
metadata:
  name: external-service      // 服务的名称必须与 endpoint 对象名字匹配
spec:                         // 服务中没有定义 选择器
  ports:
    - port: 80
apiVersion: v1
kind: Endpoint
metadata: 
  name: external-service      // endpoint 名称必须和服务名称相匹配
subsets
  - address:                  // 服务将连接重定向到 endpoint  IP 地址
    - ip: 11.11.11.11
    - ip: 22.22.22.22
  ports:
    - port: 80                // endpoint 的目标端口

为外部服务创建别名:除了手动配置服务的 Endpoint 来代替公开外部服务方法,有一种更简单的方法,就是通过其完全限定域名(FQDN)访问外部服务

apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  type: ExternalName                            // spec  type 字段被设置为 ExternalName
  externalName: someapi.somecompany.com         // 实际服务的完全限定域名
  ports:
    - port: 80

将服务暴露给外部客户端

  • 将服务的类型设置成 NodePort,对于 NodePort 服务,每个集群节点在节点本身(因此得名叫 NodePort)上打开一个端口,并将该端口上接收到的流量重定向到基础服务。该服务仅在内部集群 IP 和端口上访问,但也可以通过所有节点上的专用端口访问

  • 将服务类型设为 LoadBalancer,NodePort 类型的一种扩展,使得服务可以通过一个专用的负载均衡器来访问,这是由 k8s 中正在运行的云基础设施提供的。负载均衡器将流量重定向到跨所有节点的节点端口。客户端提供负载均衡器的 IP 连接到服务。

  • 创建一个 Ingress 资源,这是一个完全不同的机制,通过一个 IP 地址公开多个服务,它运行在 Http 层(网络协议第7层)上,因此可以提供比工作在第4层的服务更多的功能。

NodePort 类型的服务

这与常规服务类似(它们的实际类型是 ClusterIP),但是不仅可以通过服务的内部集群 IP 访问 NodePort 访问,还可以通过任何节点的 IP 和预留节点端口访问 NodePort 服务。

apiVersion: v1
kind: Service
metadata:
  name: kubia-nodeport
spec:
  type: NodePort            //  NodePort 设置服务类型
  ports:
    - port: 80              // 服务集群 IP 的端口号
      targetPort: 8080      // 背后 pod 的目标端口号
      nodePort: 30123       // 通过集群节点的 30123 端口可以访问改服务
  selector:
    app: kubia

以上服务可以通过以下地址访问:

  • 10.111.254.223:80

  • <1stnode'sIP>:30123

  • <2ndnode'sIP>:30123

...

LoadBalancer 类型的服务(通过负载均衡器将服务暴露出来)

在云提供商上运行的 k8s 集群通常支持从云基础架构自动提供负载平衡器。所有需要做的就是设置服务的类型为 LoadBadancer 而不是 NodePort。 负载均衡器拥有自己独一无二的可公开访问的 IP 地址,并将所有连接重定向到服务。可以通过负载均衡器的 IP 地址访问服务。

如果 k8s 在不支持 LoadBadancer 服务的环境中运行,则不会调配负载平衡器,但该服务仍将表现得像一个 NodePort 服务。这是因为 LoadBadancer 服务是 NodePort 服务的扩展。

apiVersion: v1
kind: Service
metadata:
  name: kubia-loadbalancer
spec:
  type: LoadBalancer                     // 该服务从 k8s 集群的基础架构获取负载均衡器
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: kubia
$ kubectl get svc kubia-loadbalancer

=>

NAME                     CLUSTER-IP         EXTERNAL-IP             PORT(s)          AGE
kubia-loadbalancer       10.111.241.153     130.211.53.173          80:32143/TCP     2m

以上服务可以通过以下地址访问:

  • 130.211.53.173

通过 Ingress 暴露服务

只有 Ingress 控制器在集群中运行,Ingress 资源才能正常运行。

每个 LoadBalancer 服务都需要自己的负载均衡器,以及独有的公网 IP 地址,而 Ingress 只需要一个公网 IP 就能为许多服务提供访问。当。

注意: Ingress 功能的支持因不同的 Ingress 拉制器实现而异,因此请检查特定实现的文档以确定支持的内容。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kubia
spec:
  tls:                                          // 在这个属性下包含了所有的 TLS 的配置
    - host
      - kubia.example.com                       // 将接收来自 kubia.example.com 主机的 TLS 连接
      secretName: <secretvolume name>                 //  tls-secret 中获得之前创立的私钥和证书
  rules:
  - host:kubia.example.com                      // Ingress  kubia.example.com 域名映射到你的服务
    http:
      paths:
      - path: /                                 // 将所有的请求发送到 kubia-nodeport 服务的 80 端口
        backend:
          serviceName: kubia-nodeport
          servicePort: 80
$ kubectl get ingress     // 获取 Ingress 信息

=> 

NAME       HOSTS                ADDRESS                PORTS        AGE
kubia      kubia.example.com    192.168.99.100         80           29 

确保在 Ingress 中配置的 Host 指向 Ingress 的 IP 地址:通过配置 DNS 服务器将 kubia.example.com 解析为 此 IP 地址,或者在 /etc/hosts 文件(windows 系统为 C:\windows\system32\drivers\etc\hosts)中添加下面一行内容

192.168.99.100 kubia.example.com

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kubia
spec:
  rules:
    - host:kubia.example.com
      http:
        paths:
          - path: /foo
            backend:
              serviceName: kubia
              servicePort: 80
          - path: /bar
            backend:
              serviceName: bar
              servicePort: 80
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kubia
spec:
  rules:
    - host:foo.example.com
      http:
        paths:
          - path: /
            backend:
              serviceName: kubia
              servicePort: 80
    - host:bar.example.com
      http:
        paths:
          - path: /
            backend:
              serviceName: kubia
              servicePort: 80

就绪探针

提示: 应该始终定义一个就绪探针,即使它只是向基准 URL 发送 HTTP 请求样简单。

存活探针确保异常容器自动重启来保持应用程序的正常运行

就绪探针会定期调用,并确定特定的 pod 是否接收客户端请求。当容器的准备就绪探测返回成功时,表示容器已准备好接收请求。

  • Exec 探针:执行进程的地方。容器的状态由进程的退出状态码确定

  • HTTP GET 探针,向容器发送 HTTP GET 请求,通过响应的 HTTP 状态码判断容器是否准备好。

  • TCP socket 探针,它打开一个 TCP 连接到容器的指定端口。如果连接已建立,则认为容器已准备就绪。

使用 headless 服务来发现独立的 pod

k8s 允许客户通过 DNS 查找发现 pod IP。通常,当执行服务的 DNS 查找时,DNS 服务器会返回单个 IP(服务集群的 IP)。但是,如果告诉 k8s 不需要为服务提供集群 IP(通过在服务 spec 中将 clusterIP 字段设置为 None 来完成此操作),则 DNS 服务器会返回 pod IP 而不是单个服务 IP。

apiVersion: v1
kind: Servicer
metadata:
  name: kubia-headless
spec:
  clusterIP: None               // 这使得服务成为 headless 服务
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: kubia

排除服务故障

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

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

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

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

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

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

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

六、卷:将磁盘挂载到容器

我们之前说过,pod 类似逻辑主机,在逻辑主机中运行的进程共享诸如 CPU、 RAM、网络接口等资源。入们会期望进程也能共享磁盘,但事实并非如此。需要谨记一点,pod 中的每个容器都有自己独立的文件系统,因为文件系统来自容器镜像。每个新容器都是通过在构建镜像时加入的详细配置文件来启动的。将此与 pod 中容器重新启动的现象结合起来你就会意识到新容器并不会识别前一个容器写入文件系统内的任何内容,即使新启动的容器运行在同一个 pod 中。

k8s 的卷是 pod 的一个组成部分,因此像容器一样在 pod 的规范中就定义了。它们不是独立的 Kubernetes 对象,也不能单独创建或删除。pod 中的所有容器都可以使用卷,但必须先将它挂载在每个需要访问它的容器中。在每个容器中,都可以在其文件系统的任意位置挂载卷。

卷类型:

  • emptyDir: 用于存储临时数据的简单空目录

  • gitRepo: 通过检出 Git 仓库的内容阿来初始化的卷

  • hostPath: 用于将目录从工作节点的文件系统挂载到 pod 中

  • nfs:挂载到 pod 中的 NFS 共享卷

  • configMap, secret, downwardAPI: 用于将 k8s 部分资源和集群信息公开给 pod 的特殊类型的卷

  • persistentVolumnClaim: 一种使用预置或动态配置的持久存储类型

  • gcePersistentDisk (Google 高效能型存储磁盘卷),awsElasticBlockStore (AmazonWeb 服务弹性块存储卷),azureDisk (Microso Azure 磁盘卷):用于挂载云服务商提供的特定存储类型。

...

emptyDir 卷

一个 emptyDir 卷对于在同 pod 中运行的容器之间共享文件特别有用。但是它也可以被单个容器用于将数据临时写入磁盘,例如在大型数据集上执行排序操作时,没有那么多内存可供使用。数据也可以写入容器的文件系统本身(还记得容器的顶层读写层吗?),但是这两者之间存在着细微的差别。容器的文件系统甚至可能是不可写的,所以写到挂载的卷可能是唯一的选择。

apiVersion: v1
kind: Pod
metadata:
  name: fortune
spec:
  containers:
    - image: dockerhubid/fortune
      name: html-generator
      volumnMounts:                               // 名为 html 的卷挂载在容器的 /var/htdocs 
        - name: html
          mountPath: /var/htdocs
    - image: nginx:alpine
      name: web-server
      volumnMounts:                               // 与上面相同的卷挂载在 /user/share/nginx/html 上,设为只读
        - name: html
          mountPath: /user/share/nginx/html
          readyOnly: true
      ports:
        - containerPort: 80
          protocol: TCP
  volumns:                                        // 一个名为 html 的单独 emptyDir 卷,挂载在上面的两个容器中
    - name: html
      emptyDir: {}

指定用于 EMPTYDIR 的介质

volumns:
  - name: html
    emptyDir:
      medium: Memory     // 通知 k8s  tmfs 文件系统(存在内存而非硬盘)上创建 emptyDir

GitRepo 卷

gitRepo 实际上也是一种 emptyDir 卷,它通过克隆 Git 仓库并在 pod 启动时(但在创建容器之前)检出特定版本来填充数据。

注意在创 gitRepo 卷后,它并不能和对应 repo 保持同步。当向 Git 仓库推送新增的提交时,卷中的文件将不会被更新。然而,如果所用 pod 是由 ReplicationController 管理的,删除这个 pod 将触发新建一个新的 pod,而这个新 pod 的卷中将包含最新的提交。

apiVersion: v1
kind: Pod
metadata:
  name: gitrepo-volumn-pod
spec:
  containers:
  - image: nginx:alpine
    name: web-server
    volumnMounts:
    - name: html
      mountPath: /user/share/nginx/html
      readOnly: true
    ports:
    - port: 80
      protocol: TCP
  volumes:
    name: html
    gitRepo:                                            // 你正在创建一个 gitRepo 
      repository: https://github.com/xxx/xxx/xxx.git    // 这个卷克隆自一个仓库
      revision: master                                  // 检出主分支
      directory: .                                      //  repo 克隆到卷的根目录
    

介绍 sidecar 容器

Git 同步进程不应该运行在与 Nginx 站点服务器相同的容器中,而是在第二个容器 sidecar container。它是一种容器,增加了对 pod 主容器的操作。可以将一个 sidecar 添加到 pod 中,这样就可以使用现有的容器镜像,而不是将附加逻辑填入主应用程序的代码中,这会使它过于复杂和不可复用。

hostPath 卷

hostPath 卷指向节点文件系统上的特定文件或目录。在同一节点上运行并在其 hostPath 卷中使用相同路径的 pod 可以看到相同的文件。

提示:请记住仅当需要在节点上读取或写入系统文件时才使用 hostPath,切勿使用它们来持久化跨 pod 的数据

持久卷和持久卷声明

研发人员无需向他们的 pod 中添加特定技术的卷,而是由集群管理员设置底层存储,然后通过 k8s API 服务器创建持久卷并注册。在创建持久卷时,管理员可以指定其大小和所支持的访问模式。当集群中的用户需要在 pod 中使用持久化存储时,他们首先创建持久化卷声明清单,指定所需要的最低容量和访问模式,然后用户将持久卷声明清单提交给 k8s 服务器,k8s 服务器将找到可匹配的持久卷并将其绑定到持久卷声明。

持久卷声明可以当作 pod 的一个卷来使用,其他用户不能使用相同的卷,除非先通过删除持久卷声明绑定来释放。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: mongodb-pv
spec:
  capacity:                                    // 定义 PersistentVolumn 的大小
    storage: 1Gi
  accessModes:
    - ReadWriteOnce                            // 仅允许单个节点挂载读写
  persistentVolumeReclaimPolicy: Retain|Delete|Recycle        // Retain: 当声明被释放后,PV 将会被保留(不清理和删除); 删除底层存储(Delete);删除卷的内容并使卷可用于再次声明,通过这种方式,持久卷可以被不同的持久卷声明和 pod 反复使用(Recycle)
  gcePersistentDisk:                           // PersistentVolume 指定支持之前创建的 GCE 持久磁盘
    pdName: mongodb                            // 持久磁盘的名称必须与管理员创建的实际 PersistentDisk(持久磁盘) 一致
    fsType: ext4                               // 文件的系统类型为 EXT4(一种 Linux 文件系统)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc                            // 持久卷声明的名称
spec:
  resources:
    requests:                                  // 申请 1GiB 的存储空间
      storage: 1Gi                              
    accessModes:
    - ReadWriteOnce                            // 运行单个客户端访问(同时支持读取和写入)
    storageClassName: ""                       // 如果未指定,将使用默认 sc 创建动态的 pv;如果指定为特定的 sc 名称,将使用 sc 配置中的卷插件创建 pv; 如果值设为空字符串,可确保 pvc 绑定到预先配置的 pv (如果尚未将 storageClassName 属性设置为空字符串,则尽管已存在适当的预配置待久卷,但动态卷置备程序仍将配置新的持久卷)

访问模式的缩写:

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

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

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

apiVersion: v1
kind: Pod
metadata:
  name: mongodb
spec:
  containers:
  - image: mongo
    name: mongodb
    volumeMounts:
    - name: mongodb-data
      mountPath: /data/db
    ports:
    - containerPort: 27017
      protocol: TCP
  volumes:
  - name: mongodb-data
    persistentVolumeClaim:
      claimName: mongodb-pvc               //  pod 卷中通过名称引用持久卷声明

持久卷的动态卷配置

集群管理员可以创建一个持久卷配置,并定义一个或多个 StorageClass 对象,从而让用户选择他们想要的持久卷类型而不仅仅只是创建持久卷。用户可以在其持久卷声明中引用 StoragClass,而配置程序在配置持久存储时将采用这一点。

注意: 与持久卷类似,StorageClass 资源并非命名空间。

k8s 包括最流行的云服务提供商的置备程序 provisioner,所以管理员并不总是需要创建一个置备程序。但是如果 k8s 部署在本地,则需要配置定制的置备程序。

与管理员预先提供一组持久卷不同的是,它们需要定义一个或多个 StorageClass,并允许系统在每次通过持久卷声明请求时创建一个新的持久卷。最重要的是,不可能耗尽持久卷(很明显,你可以用完存储空间 )

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: kubernetes.io/gce-pd          // 用于配置持久卷的卷插件
parameters:                                // 传递给 parameters 的参数
  type: pd-ssd
  zone: europe-westl-b

StorageClass 资源指定当持久卷声明请求此 StorageClass 时应使用哪个置备程序来提供持久卷。StorageClass 定义中定义的参数将传递给置备程序,并具体到每个供应器插件。 StorageClass 使用 GCE 持久磁盘的预配置器,这意味着当 k8s 在 GCE 中运行时可供使用。对于其他云提供商,需要使用其他的置备程序

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc
spec:
  storageClassName: fast                       //  pvc 请求自定义存储类
  resources:
    requests:
      storage:100Mi
    accessModes:
    - ReadWriteOnce

除了指定大小和访问模式,持久卷声明现在还会指定要使用的存储类别。在创建声明时,持久卷由 fast StorageClass 资源中引用的 provisioner 创建。即使现有手动设置的持久卷与持久卷声明匹配,也可以使用 provisioner。

StorageClasses 的好处在于,声明是通过名称引用它们的。因此,只要 StorageClass 名称在所有这些名称中相同, PVC 定义便可跨不同集群移植。

不指定存储类的动态配置

除了你自己创建的存储类,还存在 standard 存储类并标记为默认存储类。如果持久卷声明没有明确指出要使用哪个存储类,默认存储类会 用于动态提供持久卷的内容。

// 在 Google Kubernetes 引擎上(不同引擎的存储类会有所不同)
$ kubectl get sc
NAME                      TYPE 
fast                      kubernetes.io/gce-pd
standard(default)         kubernetes.io/gce-pd

总而言之,将持久化存储附加到一个容器的最佳方式是仅创建 PVC (如果需要,可以使用明确指定的 storgeClassName) 和容器(其通过名称引用 PVC), 其他所有内容都由动态持久卷置备程序处理。

七、ConfigMap 和 Secret: 配置应用程序

传递配置选项给容器化应用程序的方法:

  • 将配置嵌入应用本身

  • 向容器传递命令行参数

  • 通过特殊类型的卷将配置文件挂载到容器中

  • 为每个容器设置自定义环境变量

向容器传递命令行参数

Dockerfile中的两种指令分别定义命令与参数这两个部分:

  • ENTRYPOINT 定义容器启动时被调用的可执行程序

  • CMD 指定传递给 ENTRYPOINT 的参数

指令支持以下两种形式: 两者的区别在于指定的命令是否是在 shell 中被调用

  • shell 形式 一 如 ENTRYPOINT node app.js

  • exec 形式 一 如 ENTRYPOINT ["node", "app.js"]

可配置化 fortune 镜像中的间隔参数

#!/bin/bash 
trap "exit" SIGINT 
INTERVAL=$1 
echo Configured to generate new fortune every $INTERVAL seconds 
mkdir -p /var/htdocs 
while 
do 
  echo $(date) Writing fortune to /var/htdocs/index.html
  /usr/games/fortune > /var/htdocs/index.html
  sleep $INTERVAL 
done

fortune 镜像的 Dockerfile

FROM ubuntu:latest 
RUN apt-get update; apt-get -y install fortune 
ADD fortuneloop.sh /bin/fortuneloop.sh
ENTRYPOINT ["/bin/fortuneloop.sh"]                // exec 形式的 ENTRYPOINT 指令
CMD ["10"]                                        // 可执行程序的默认参数

在 Kubernetes 中覆盖命令和参数

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

绝大多数情况下,只需要设置自定义参数。命令一般很少被覆盖,除非针对一些未定义 ENTRYPOINT 的通用镜像。command 和 args 字段在 pod 创建后无法被修改

少量参数值的设置可以使用上述的数组表示。多参数值情况下可以采用如下标记:

args:

  • foo
  • bar
  • "15"

提示: 字符串值无须用引号标记,数值需要

为容器设置环境变量

注意与容器的命令和参数设置相同,环境变量列表无法在 pod 创建后被修改

通过环境变量配置化 fortune 镜像中的间隔值:

如果应用由 Java 编写,需要使用 System.getenv("INTERVAL"), 同样地,对应到 Node.JS 与 Python 中分别是 process.env.INTERVAL 与 os.environ ['INTERVAL']

#!/bin/bash 
trap "exit" SIGINT 
echo Configured to generate new fortune every $INTERVAL seconds 
mkdir -p /var/htdocs 
while : 
do 
  echo $(date) Writing fortune to /var/htdocs/index.html
  /usr/games/fortune > /var/htdocs/index.html
  sleep $INTERVAL 
done

构建完新镜像(镜像的 tag 变更为 luksa/fortune:env)并推送至 Docker Hub 之后,可以通过创建一个新 pod 来运行它

kind: Pod 
spec: 
  containers: 
  - image: luksa/fortune:env
    env: 
    - name: INTERVAL 
      value: "30" 
    name: html-generator

注意不要忘记在每个容器中,Kubernetes 会自动暴露相同命名空间下每个 service 对应的环境变量。这些环境变量基本上可以被看作自动注入的配置。

在环境变量值中引用其他环境变量

env:
- name: FIRST_VAR 
  value 
- name: SECOND_VAR 
  value :"$(FIRST_VAR)bar"

利用 ConfigMap 解耦配置

Kubernetes 许将配置选项分离到单独的资源对象 ConfigMap 中,本质上就是一个键/值对映射,值可以是短字面量,也可以是完整的配置文件。应用无须直接读取 ConfigMap 甚至根本不需要知道其是否存在。映射的内容通过环境变量或者卷文件的形式传递给容器,而并非直接传递给容器。命令行参数的定义中可以通过 $(ENV_VAR) 语法引用环境变量,因而可以达到将 ConfigMap 的条目当作命令行参数传递给进程的效果。

$ kubectl create configmap my-config --from-file=foo.json --from-file=bar=foobar.conf --from-file=config-ops/ --from-literal=some=thing
apiVersion : v1
kind: Pod 
metadata: 
 name: fortune-env-from-configrmap
spec: 
  containers: 
  - image: luksa/fortune:env
    env:
    - name: INTERVAL                     // 设置环境变量
      valueFrom:
        configMapKeyRef:                 //  ConfigMap 初始化,不设置固定值
          optional: true                 // 即便 ConfigMap 不存在,容器正常启动
          name: fortune-config           // 引用 ConfigMap 的名称
          key: sleep-interval            // 环境变量被设置为 ConfigMap 下对应键的值

注意:创建 pod 时引用的 ConfigMap 不存在时 Kubernetes 会正常调度 pod 并尝试运行所有的容器。然而引用不存在的 ConfigMap 的容器会启动失败,其余容器能正常启动。如果之后创建了这个缺失的 ConfigMap, 失败容器会自动启动,无须重新创建 pod。

// 一次性传递 ConfigMap 的所有条目作为环境变量
spec:
  containers:
  - image: some-image
    envFrom:                    // 使用 envFrom 字段而不是 env 字段
    - prefix: CONFIG_         // 所有环境变量均包含前缀 CONFIG_
      configMapRef:           // 引用名为 my-config-map 的 ConfigMap
        name: my-config-map

注意: 前缀设置是可选的,若不设置前缀,环境变量的名称与 ConfigMap 的键名相同。如果 ConfigMap 的某键名格式不正确,创建环境变量时会忽略对应的条目。

// 传递 ConfigMap 条目作为命令行参数
apiVersion : v1
kind: Pod 
metadata: 
 name: fortune-env-from-configrmap
spec: 
  containers: 
  - image: luksa/fortune:arg               // 使用第一个参数读取间隔值的镜像,而不是读取环境变量的镜像
    env:
    - name: INTERVAL                     // 设置环境变量
      valueFrom:
        configMapKeyRef:                 //  ConfigMap 初始化,不设置固定值
          optional: true                 // 即便 ConfigMap 不存在,容器正常启动
          name: fortune-config           // 引用 ConfigMap 的名称
          key: sleep-interval            // 环境变量被设置为 ConfigMap 下对应键的值
    args: ["$(INTERVAL)"]                  // 在参数设置中引用环境变量

使用 configMap 卷将条目暴露为文件

// my-nginx-config.conf 文件
server { 
  listen          80; 
  server_name     www.kubia-example.com; 
  gzip on; 
  gzip_types text/ plain application/xrnl; 
  location / { 
    root /usr/share/nginx/html; 
    index index.html index.htm;
}

创建一个新文件夹 confimap-files 并将上面的配置文件存储于 configmap-files/my-nginx-config.conf中。另外在该文件夹中添加一个名为 sleep-interval 的文本文件,写入值为 25,使ConfigMap 同样包含条目 sleep-iterval。

$ kubectl create configmap fortune-config -from-file=configmap-files

Nginx 需读取配置文件 /etc/nginx/nginx.conf, 而 Nginx 镜像内的这个文件包含默认配置,并不想完全覆盖这个配置文件。幸运的是,默认配置文件会自动嵌入子文件夹 /etc/nginx/conf.d/ 下的所有 conf 文件,因此只需要将你的配置文件置于该子文件夹中即可。

apiVersion: vl 
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
    ...
volumes:
- name: config
  configMap:
    name: fortune-config
    items:                            // 选择包含在卷中的条目
    - key: my-nginx-config.conf       // 该键对应的条目被包含
      path: pzip.conf                 // 条目的值被存储在该文件中

指定单个条目时需同时设置条目的键名称以及对应的文件名。如果采用上面的配置文件创建pod, /etc/nginx/conf.d 文件夹是比较干净的,仅包含所需的 gzip.conf

注意:挂载某一文件夹会隐藏该文件夹中已存在的文件

ConfigMap 独立条目作为文件被挂载且不隐藏文件夹中的其他文件: volumeMount 额外的 subPath 字段可以被用作挂载卷中的某个独立文件或者是文件夹,无需挂载完整卷

spec: 
  containers: 
  - image: some/image
    volumeMounts: 
    - name: myvolume
      mountPath: /etc/someconfig.conf      // 挂载至某一文件而不是文件夹
      subPath: myconfig.conf               // 仅挂载指定的条目 myconfig.conf,而不是完整的卷
// 默认权限为 644(-rw-r-r--)
volumes: 
- name: config
  configMap: 
    name: fortune-config 
    defaultMode: "6600"      // 设置所有文件的权限为 -rw-rw------

更新应用配置且不重启应用程序

将 ConfigMap 暴露为卷可以达到配置热更新的效果,无须 新创建 pod 或者重启容器。

了解文件被自动更新的过程:

你可能会疑惑在 Kubernetes 更新完 configMap 卷中的所有文件之前,应用是否会监听到文件变化并主动进行重载。幸运的是,这不会发生,所有的文件会被自动一次性更新。Kubernetes 通过符号链接做到这一点。如果尝试列出 configMap 卷挂载位置的所有文件,会看到如下内容。

$ kubectl exec -it fortune-configmap-volume -c web-server -- ls -lA /etc/nginx/conf.d

=>

total 4
drwxr-xr-x ... 12:15 .. 4984_09_04_12_15_06.865837643
lrwxrwxrwx ... 12:15 .. data -> ..4984_09_04_12_15_06.865837643
lrwxrwxrwx ... 12:15 my-nginx-config.conf -> ..data/my-nginx-config.conf
lrwxrwxrwx ... 12:15 sleep-interval -> .. data/sleep-interval 

可以看到被挂载的 configMap 卷中的文件是 ..data 文件夹中文件的符号链接,而 ..data 文件夹同样是 ..4984_09_04_12_15_06.865837643 的符号链接。每当 ConfigMap 被更新后,Kubernetes 会创建一个这样的文件夹,写入所有文件并重新将符号 ..data 链接至新文件夹,通过这种方式可以一次性修改所有文件。

挂载至已存在文件夹的文件不会被更新

涉及到更新 configMap 卷需要提出一个警告:如果挂载的是容器中的单个文件而不是完整的卷,ConfigMap 更新之后对应的文件不会被更新!

如果现在你需要挂载单个文件并且在修改源 ConfigMap 的同时会自动修改这个文件,一种方案是挂载完整卷至不同的文件夹并创建指向所需文件的符号链接。符号链接可以原生创建在容器镜像中,也可以在容器启动时创建。

了解更新ConfigMap的影响

关键点在于应用是否支持重载配置。ConfigMap 更新之后创建的 pod 会使用新配置, 而之前的 pod 依旧使用旧配置,这会导致运行中的不同实例的配置不同。这也不仅限于新 pod, 如果 pod 中的容器因为某种原因重启了,新进程同样会使用新配置。因此如果应用不支持主动重载配置,那么修改某些运行 pod 所使用的 ConfigMap 并不是一个好主意。

有一点仍需注意,由于 configMap 卷中文件的更新行为对于所有运行中示例而言不是同步的,因此不同 pod 中的文件可能会在长达一分钟的时间内出现不一致的清况。

Secret

为了存储与分发此类信息,Kubernetes 提供了一种称为 Secret 的单独资源对象。Secret 结构与 ConfigMap 类似,均是键/值对的映射。 Secret 的使用方法也与 ConfigMap 相同,可以

  • 将 Secret 条目作为环境变量传递给容器
  • 将 Secret 条目暴露为卷中的文件

k8s 通过仅仅将 Secret 分发到需要访问 Secret 的 pod 所在的机器节点来保障其安全性。另外 Secret 只会存储在节点的内存中,永不写入物理存储,这样从节点上删除 Secret 时就不需要擦除磁盘了。

注意 default-token Secret 默认会被挂载至每个容器。可以通过设置 pod 定义中的 automountServiceAccountToken 字段为 false, 或者设置 pod 使用的服务账户中的相同字段为 false 来关闭这种默认行为(本书后面会对服务账户进行讲解)。

Secret 大小限于 1MB

通过 secret 卷将 Secret 暴露给容器之后,Secret 条目的值会被解码并以真实形式(纯文本或二进制)写入对应的文件。通过环境变量暴露Secret条目亦是如此。在这两种情况下,应用程序均无须主动解码,可直接读取文件内容或者查找环境变量。

挂载 fortune secret pod

apiVersion: v1
kind: Pod
metadata:
  name: fortune-https
spec:
  containers:
  - image: luksa/fortune:env
    name: html-generator
    env:
    - name: FOO_SECRET                      // 通过 Secret 条目设置环境变量
      valueFrom:
        secretKeyRef:
          name: foryune-https               // Secret 的键
          key: foo                          // Secret 的名称
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: certs                        // 配置 nginx  /etc/nginx/certs 中读取证书和密钥文件,需将 secret 卷挂载于此
      mountPath: /etc/nginx/certs/
      readOnly: true
  volumes:
  - name: certs                       // 这里引用 fortune-https Secret 来定义 secret 
    secret:
      secretName: fortune-https

注意与 configMap 卷相同,secret 卷同样支持通过 defaultModes 属性指定卷中文件的默认权限

Secret 卷存储于内存

通过挂载 secret 卷至文件夹 /etc/nginx/certs 将证书与私钥成功传递给容器。secret 卷采用内存文件系统列出容器的挂载点,如下面的代码清单所示。

$ kubectl exec fortune-https -c web-server --mount | grep certs
trnpfs on /etc/ng /certs type tmpfs (ro,relatirne)

由于使用的是 tmpfs,存储在 Secret 中的数据不会写入磁盘,这样就无法被窃取

提示: 由于敏感数据可能在无意中被暴露,通过环境变量暴露 Secret 给容器之前请再三思考。为了确保安全性,请始终采用 secret 卷的方式暴露 Secret

了解镜像拉取 Secret

apiVersion: v1
kind: Pod
metadata:
  name: private-pod
spec:
  imagePullSecret:                 // 能够从私有镜像仓库中拉取镜像
  - name: mydockerhubsecret
  containers:
  - image: username/private:tag
    name: main

八、从应用访问 pod 元数据以及其他资源

通过 Downward API 传递元数据

Downward API 允许我们通过环境变量或者文件(在 Downward API 卷中)传递 pod 的元数据

目前我们可以给容器传递以下数据:

  • pod 的名称

  • pod 的 IP

  • pod 所在的命名空间

  • pod 运行节点的名称

  • pod 运行所归属的服务账户的名称

  • 每个容器请求的 CPU 和内存的使用量

  • 每个容器可以使用的 CPU 和内存的限制

  • pod 的标签

  • pod 的注解

列表中的大部分项目既可以通过环境变量也可以通过 downwardAPI 卷传递给容器,但是标签和注解只可以通过卷暴露(可以在 pod 运行时修改标签和注解。如我们所愿,当标签和注解被修改后,Kubernetes 会更新存有相关信息的文件,从而使 pod 可以获取最新的数据)。部分数据可以通过其他方式获取(例如,可以直接从操作系统获取),但是 DownwardAPI 提供了一种更加便捷的方式

通过环境变量暴露元数据

apiVersion: v1
kind: Pod 
metadata: 
  name: downward 
spec: 
  containers: 
  - name: main 
    image: busybox 
    command: ["sleep", "9999999") 
    resources:
      requests: 
        cpu: 15m 
        memory: lOOKi
      limits:
        cpu: 100m 
        memory: 4Mi 
    env: 
    - name: POD_NAME
      valueFrom:
      fieldRef: 
        fieldPath: metadata.name 
    - name: POD_NAMESPACE
      valueFrom: 
        fieldRef: 
          fieldPath: metadata.namespace
    - name: POD_IP 
      valueFrom:
        fieldRef: 
          fieldPath: status.podIP 
    - name: NODE_NAME
      valueFrom:
        fieldRef: 
          fieldPath: spec.nodeName 
    - name: SERVICE_ACCOUNT
      valueFrom: 
        fieldRef: 
          fieldPath: spec.serviceAccountName
    - name: CONTAINER_CPU_REQUEST_MILLICORES
      valueFrom:
        resourceFieldRef:                      // 容器请求的 CPU 和内存使用量是引用 resourceFieldRef 字段而不是 fieldRef 字段
          resource: requests.cpu 
          divisor: lm                          // 对于资源相关的字段,我们定义一个基数单位从而生成每一部分的值
    - name: CONTAINER_MEMORY_LIMIT_KIBIBYTES
      valueFrom:
        resourceFieldRef: 
          resource: limits.memory
          divisor: lKi

通过 downwardAPI 卷来传递元数据

apiVersion: vl 
kind: Pod 
metadata:
  name: downward 
labels: 
  foo: bar 
  annotations:
    key1: value1 
    key2: |
      multi
      line 
      value 
spec: 
  containers: 
  - name: main
    image: busybox
    command: ["sleep", "9999999"]
    resources:
      requests: 
        cpu: 15m 
        memory: 100Ki 
      limits:
        cpu: 100m 
        memory: 4Mi 
    volumeMounts:                            //  /etc/downward 下挂载 downward 这个卷
    - name: downward
      mountPath: /etc/downward
  volumes: 
  - name: downward                           // 通过将卷的名字设定为 downward 来定义一个 dowanwardAPI 
    downwardAPI:
      items: 
      - path: "podName"                      // pod的名称(来自 manifest 文件中的 metadate.name 字段)将被写入 podName 文件中
        fieldRef:
        fieldPath: metadata.name 
      - path: "podNamespace"
        fieldRef:
          fieldPath: metadata.namespace
      - path: "labels"                       // pod 的标签将被保存到 /etc/dowanward/labels 文件中
        fieldRef:
          fieldPath: metadata.labels
      - path: "annotations"                 // pod 的标签将被保存到 /etc/dowanward/annotations 文件中
        fieldRef:
          fieldPath: metadata.annotations
      - path: "containerCpuRequestMilliCores"
        resourceFieldRef:
          containerName: main                // 当暴露容器级的元数据时, 必须指定引用资源字段对应的容器名称
          resource: requests.cpu 
          divisor: lm 
      - path: "containerMemoryLimitBytes"
        resourceFieldRef: 
          containerName: main 
          resource: limits.memory
          divisor: 1

与 Kubernetes API 服务器交互

通过 Kubectl proxy 访问 API 服务器

因为服务器使用 HTTPS 协议并且需要授权, kubectl proxy 命令启动了一个代理服务来接收来自你本机的 HTTP 连接并转发至 API 服务器,同时处理身份认证,所以不需要每次请求都上传认证凭证

$ kubectl proxy // 通过代理访问 API 服务器

$ curl localhost:8001  // 我们也无须传递其他任何参数,因为 kubectl 已经知晓所需的所有参数(API 服务器,URL,认证凭证等)。一旦启动,代理服务器就将在本地端口 8001 接收连接请求

=>

{
  "paths": [ 
    "/api", 
    "/api/vl", 
    "/apis", 
    "/apis/apps", 
    "/apis/apps/vlbetal",
    ...
    "/apis/batch",
    "/apis/batch/vl",
    "/apis/batch/v2alphal",
    ...
}

$ curl http://localhost:8001/apis/batch      // 访问在 apis/batch 目录下的 endpoint 清单

$ curl http://localhost:8001/apis/batch/v1   // 访问在 batch/v1 中的资源类型

$ curl http://localhost:8001/apis/batch/vl/jobs  // 通过在 /apis/batch/vl/jobs 路径运行一个 GET 请求,可以获取集群中所有 Job 的清单

从 pod 内部与 API 服务器进行交互

想要从 pod 内部与 API 服务器进行交互,需要关注以下三件事情:

  • 确定 API 服务器的位置

  • 确保是与 API 服务器进行交互,而不是一个冒名者

  • 通过服务器的认证,否则将不能查看任何内容以及进行任何操作

** 详细请查阅相关文档 **

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

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

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

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

通过 ambassador 容器简化与 API 服务器的交互

想象一下,如果一个应用需要查询 API 服务器(此外还有其他原因)。除了像之前章节讲到的直接与 API 服务器交互,可以在主容器运行的同时,启动一个 ambassador 容器,并在其中运行 kubectl proxy 命令,通过它来实现与 API 服务器的交互。 在这种模式下,运行在主容器中的应用不是直接与 API 服务器进行交互,而是通过 HTTP 协议(不是 HTTPS 协议)与 ambassador 连接,并且由 ambassador 通过 HTTPS 协议来连接 API 服务器,对应用透明地来处理安全问题。这种方式同样使用了默认凭证 Secret 卷中的文件

使用客户端库与 API 服务器交互

** 具体请查阅相关信息 **

九、Deployment: 声明式地升级应用

更新运行在pod内的应用程序:

  • 直接删除所有现有的 pod, 然后创建新的 pod

  • 也可以先创建新的 pod, 并等待它们成功运行之后,再删除旧的 pod。可以先创建所有新的 pod, 然后一次性删除所有旧的 pod, 或者按顺序创建新的 pod, 然后逐渐删除旧的 pod

这两种方法各有优缺点。第一种方法将会导致应用程序在一定的时间内不可用。使用第二种方法,你的应用程序需要支持两个版本同时对外提供服务。如果你的应用程序使用数据库存储数据,那么新版本不应该对原有的数据格式或者数据本身进行修改,从而导致之前的版本运行异常。

$ kubectl rolling-update kubia-vl kubia-v2 --image=luksa/kubia:v2   // 使用 kubia-v2 版本应用来替换运行着 kubia-v1 的 RC, 将新的复制控制器命名为 kubia-v2, 并使用 luksa/kubia:v2 作为容器镜像

$ kubectl rollout undo deployment kubia   // 回滚到上一版本

$ kubectl rollout history deployment kubia  // 显示所有升级的版本

$ kubectl rollout undo deployment kubia --to-revision=1  // 回滚到一个特定的 Deployment 版本

$ kubectl set image deployment kubia nodejs=luksa/kubia:v4  // 更改 deployment 的 pod 模板的镜像

$ kubectl rollout pause deployment kubia  // 暂停滚动更新(类似于金丝雀版本)

$ kubectl rollout resume deployment kubia  // 恢复滚动升级

使用 Deployment声明式地升级应用

Deployment 是一种更高阶资源,用于部署应用程序并以声明的方式升级应用,而不是通过 RC 或 RS 进行部署,它们都被认为是更底层的概念。当创建一个 Deployment 时,RS 资源也会随之创建(最终会有更多的资源被创建)在使用 Deployment 时,实际的 pod 是由 Deployment 的 RS 创建和管理的,而不是由 Deployment 直接创建和管理的

注意:修改 Deployment 自身的属性不会触发滚动更新,只有修改 pod 模板属性 Deployment 才会执行更新操作

apiVersion: apps/v1beta1              // 注意 API 和版本
kind: Deployment                      // 创建 Deployment 资源
metadata:                             // Deployment 的名称中不再需要包含版本号
  name: kubia
spec:
  strategy:
    rollingUpdate:
      maxSurge: 1                     // 期望的副本数之外,最多允许超出的 od 实例的数量
      maxUnavailable: 0               // 决定了在滚动升级期间,相对于期望副本数能够允许有多少个实例处于不可用状态。
  type: RollingUpdate
  replicas: 3
  progressDeadlineSeconds: 10m        // 判定 Deployment 滚动升级失败的超时时间
  minReadySeconds: 10                // 指定新创建的 pod 至少要成功运行多久之后,才能将其视为可用减慢滚动升级速率(避免部署出错版本的应用)
  revisionHistoryLimit: 5            // 历史版本的数量
  template:
    metadata:
      name: kubia
    spec:
      containers:
      - image: luksa/kubia:v1
        name: nodejs

注意你将在 extensions/v1beta1 中找到 Deployment 资源的旧版本,在 apps/v1beta2 中找到新版本,它们有不同的必需字段和不同的默认值

不同的 Deployment 升级策略:

  • RollingUpdate: 滚动更新

  • Recreate:一次性删除所有旧版本的 pod,然后创建新的 pod(类似使用 RC 或 RS 管理更新)

注意如果 Deployment 中的 pod 模板引用了一个 ConfigMap(或 Secret), 那么更改 ConfigMap 资原本身将不会触发升级操作。如果真的需要修改应用程序的配置并想触发更新的话,可以通过创建一个新的 ConfigMap 并修改 pod 模板引用新的 ConfigMap。

与 RS 类似, 所有新的 pod 现在都由新的 RS 管理。与以前不同的是,旧的 RS 仍然会被保留这样 Deployment 可以非常容易地回滚到先前部署的版本,它可以让 K8s 取消最后一次部署的 Deployment

就绪探针如何阻止出错版本的滚动升级

当新的 pod 启动时,就绪探针会每隔一秒发起请求, 如果返回失败,pod 会从 Service 的 endpoint 中移除

十、StatefulSet: 部署有状态的多副本应用

了解 Statefulset

Statefulset 保证了 pod 在重新调度后保留它们的标识和状态。它让你方便地扩容、缩容;Statefulset 会指定期望的副本个数,它决定了在同一时间内运行的 pod 的数量;pod 是依据 Statefulset 的 pod 模板创建的;Statefulset 创建的 pod 副本并不是完全一样的。每个 pod 都可以拥有一组独立的数据卷(持久化状态)而有所区别。另外 pod 的名字都是规律的(固定的)而不是每个新 pod 都随机获取一个名字。

提供稳定的网络标识

一个 Statefulset 创建的每个 pod 都有一个从零开始的顺序索引,这个会体现在 pod 的名称和主机名上,同样还会体现在 pod 对应的固定存储上。这些 pod 的名称则是可预知的,因为它是由 Statefulset 的名称加该实例的顺序索引值组成的。

当一个 Statefulset 管理的一个 pod 实例消失后,Statefulset 会保证重启一个新的 pod 实例替换它,新的 pod 会拥有与之前 pod 完全一致的名称和主机名。

扩缩容 Statefulset

扩容一个 Statefulset 会使用下一个还没用到的顺序索引值创建一个新的 pod 实例。

缩容 Statefulset 会最先删除最高索的实例,所以缩容的结果是可预知的。

因为 Statefulset 缩容任何时候只会操作一个 pod 例,所以有状态应用的缩容不会很迅速。

Statefulset 在有实例不健康的情况下是不允许做缩容操作的。若一个实例是不健康的,而这时再缩容一个实例的话,也就意味着同时失去了两个集群成员。

Statefulset 拥有一个或多个卷声明模板,这些持久卷声明会在创建 pod 前创建出来,绑定到一个 pod 实例上。

扩容 StatefulSet 增加一个副本数时,会创建两个或更 API 对象。但是对缩容来说,则只会删除一个 pod,而遗留下之前创建的声明。当一个声明被删除后,与之绑定的持久卷就会被回收或删除,则其上面的数据就会丢失。基于这个原因,当你需要释放特定的持久卷时,需要手动删除持久卷声明。

创建三个持久卷的文件描述:

apiVersion: v1
kind: List                                    // 创建多个持久卷的文件描述
items:
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-a                                // 持久卷名称 pv-a,pv-b,pv-c
  spec:
    capacity:                                 // 每个持久卷的大小为 1 Mi
      storage: 1Mi
    accessModes:
    - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle    // 当卷被声明释放后,空间会被回收再利用
    gcePersistentDisk:                        // 指定这个卷使用 GCE 持久磁盘和指定的存储策略
      pdName: pv-a
      fsType: nfs4
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-b
  spec:
    ...
  ...

注意: 可以通过在同一 YAML 文件中添加三个横杠(---)来区分定义多个资原,这里使用另外一种方去,定义一个 List 对象,然后把各个资源作为 List 对象的各个项目。上述两种方法的效果是一样的。

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  clusterIP: None   // Statefulset 的控制 Service 必须是 headless 服务
  selector:         
    app: kubia
  ports:
  - name: http
    port: 80
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: kubia
spec:
  serviceName: kubia
  replicas: 2
  template:
    metadata:
      labels:                     // StatefulSet 创建的 pod 都具有 app=kubia 的标签
        app: kubia
    spec:
      containers:
      - image: luksa/kubia-pet
        name: kubia
        ports:
        - name: http
          containerPort: 8080
        volumeMounts:
        - name: data                  // pod 中的容器会把 PVC 数据卷嵌入指定的目录
          mountPath: /var/data
  volumeClaimTemplates:               // 持久卷声明模板
  - metadata:
      name: data
    spec:
      resources:
        requests:
          storage: 1Mi
      accessModes:
      - ReadWriteOnce

一个用来访问有状态 pod 的常规 Service: 因为它不是外部暴露的 Service, 所以只能在你的集群内部访问它

apiVersion: v1
kind: Service
metadata:
  name: kubia-public
spec:
  ports:
  - port: 80
    targetPort: 8080

小结

关于 k8s 的概念性资源就介绍到这里,本文章只适合想要接触 k8s,了解 k8s 的基础概念的朋友。