k8s 读书笔记 - kubernetes 基本概念和术语下

DevOps

前言

上一篇文章中,我们介绍了 k8s 中的 Master、Node、Pod、Label、RC & RS、Deployment、HPA & VPA、DaemonSet 这些资源对象信息,接下来我们继续介绍 k8s 中常用的资源对象。

StatefulSet

在 k8s 系统中,Pod 的管理对象 RC、Deployment、DaemonSet 和 Job & CronJob 都是面向 “无状态” 的服务。但现实中有很多服务是 “有状态” 的,特别是一些复杂的中间件集群环境,例如,下图列举这些产品等:

有状态服务列举

这些应用集群之间有 4 个共同特点:

  • (1) 每个节点都有固定的身份ID,通过这个ID,集中的双方可以相互发现并通信。

  • (2) 集群的规模是比较固定的,集群规模不能随意变动。

  • (3) 集群中的每个节点都是有状态的,通常会持久化数据到永久存储中。

  • (4) 如果磁盘损坏,则集群里的某个节点无法正常运行,集群功能受损。

Deplovment/RS 的一个特殊变种

如果通过 RC 或 Deployment 控制 Pod 副本数量来实现上述 “有状态” 的集群,就会发现第 1 点是无法满足的,因为 Pod 的名称是随机产生的,Pod 的 IP 地址也是在运行期才确定且可能有变动的,我们事先无法为每个 Pod 都确定唯一不变的 ID。另外,为了能够在其他节点上恢复某个失败的节点,这种集群中的 Pod 需要挂接某种共享存储,为了解决该问题,k8s 从 v1.4 版本开始引入了 PetSet 这个新的资源对象,并且在 v1.5 版本时更名为 StatefulSet,StatefulSet 从本质上来说,可以看作 Deplovment/RS 的一个特殊变种,它有如下特性:

  • StatefulSet 里的每个 Pod 都有稳定、唯一的网络标识可以用来发现集群内的其他成员。假设 StatefulSet 的名称为 pulsar,那么第 1个 Pod 叫 pulsar-0,第 2个叫 pulsar-1 以此类推。

  • StatefulSet 控制的 Pod 副本的启停顺序是受控的,操作第 n 个 Pod 时,前 0 到 n-1 个 Pod 已经是运行且准备就绪的状态。

  • StatefulSet 里的 Pod 采用稳定的持久化存储卷,通过 PV 或 PVC 来实现,删除 Pod 时默认不会删除与 StatefulSet  相关的存储卷(为了保证数据的安全)。

StatefulSet 除了要与 PV 卷捆绑使用以存储 Pod 的状态数据,还要与 Headless Service(“无头服务”) 配合使用,即 在每个StatefulSet 定义中都要声明它属于哪个 Headless Service。HeadlessService 与普通 Service 的关键区别在于,它没有 Cluster IP(spec:clusterIP 表示为 None),如果解析 Headless Service 的 DNS 域名,则返回的是该 Service 对应的全部 Pod 的 Endpoint 列表StatefulSet 在 Headless Service 的基础上又为 StatefulSet 控制的每个 Pod 实例都创建了一个 DNS 域名,这个域名的格式为:

$ (podname).$(headless service name)

关于 Headless Services 的更多信息,请查看:https://kubernetes.io/zh-cn/docs/concepts/services-networking/service/#headless-services

StatefulSet 示例

首先使用下面的示例创建一个 StatefulSet 。它创建了一个 Headless Service nginx 用来发布 StatefulSet web 中的 Pod 的 IP 地址。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None  # 集群内部 IP 设置为 None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: k8s.gcr.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

监视 StatefulSet 的 Pod 的创建情况:

kubectl get pods -w -l app=nginx

查看  nginx 的 Service

kubectl get service nginx
...
NAME      TYPE         CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx     ClusterIP    None         <none>        80/TCP    12s

查看 StatefulSet 的 web:

kubectl get statefulset web
...
NAME      DESIRED   CURRENT   AGE
web       2         1         20s

对于一个拥有 n 个副本的 StatefulSet,Pod 被部署时是按照 “0 — (n-1)” 的序号顺序创建的。在第一个终端中使用 kubectl get 检查输出。

查看 StatefulSet 创建的 Pod 的顺序信息:

kubectl get pods -w -l app=nginx
...
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         19s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         18s

请注意,直到 web-0 Pod 处于 Running(请参阅 Pod 阶段) 并 Ready(请参阅 Pod 状况中的 type)状态后,web-1 Pod 才会被启动。

再比如一个 6 节点的 Pulsar 的 StatefulSet 集群(Pulsar 集群组成如下)对应的 Headless Service 的名称为 pulsar,StatefulSet 的名称为 pulsar,则 StatefulSet 里的 6 个 Pod 的 DNS 名称分别为 pulsar-0.pulsar、pulsar-1.pulsar、pulsar-2.pulsar、pulsar-3.pulsar、pulsar-4.pulsar、pulsar-5.pulsar,这些 DNS 名称可以直接在集群的配置文件中固定下来。

Pulsar 集群组成

搭建 Pulsar 集群至少需要 3 个组件:ZooKeeper 集群、BookKeeper 集群和 broker 集群(Broker 是 Pulsar 的自身实例)。这三个集群组件如下:

  • ZooKeeper 集群,3(或多) 个 ZooKeeper 节点组成。

  • bookie 集群,也称为 BookKeeper 集群,3(或多) 个 BookKeeper 节点组成。

  • broker 集群,3(或多) 个 Pulsar 节点组成。

按照 Pulsar 组件的组成部分,可以形成 3 种集群部署方案:

  1. 每个组件独立集群环境部署(生产环境推荐),至少需要 9 台宿主机。

  2. 所有组件部署一个集群环境(开发环境推荐),至少需要 3 台宿主机。

  3. 组件分组部署,ZooKeeper 组件独立部署一个集群环境,bookie & broker 组件部署在另一个集群环境,至少需要 6 台宿主机。

官方建议 6 台机器部署 Pulsar 集群环境,我们此处采用方案 3,部署规划如下:

  • 3 台用于运行 Zookeeper 集群,建议使用性能较弱的机器,Pulsar 仅将 Zookeeper 用于与协调有关的定期任务和与配置有关的任务,而不用于基本操作

  • 3 台用于运行 bookie 集群和 broker 集群,建议使用性能强劲的机器。

关于 StatefulSet 更多信息,请查看:https://kubernetes.io/zh-cn/docs/tutorials/stateful-application/basic-stateful-set/

Service

Service 简介

Service 将运行在一组 Pods 上的应用程序公开为网络服务的抽象方法,Service 也是 k8s 里的 核心资源 对象之一。使用 k8s,你无需修改应用程序即可使用 Service 的服务发现机制。k8s 为 Pod 提供自己的 IP 地址,并为一组 Pod 提供相同的 DNS 名, 并且可以在 Pod 之间进行负载均衡。

k8s 里的每个 Service 其实就是 微服务架构(Microservice Architecture)中的一个微服务,上篇文章中讲解的 Pod、RS、Deployment 等资源对象其实都是为 k8s Service 做铺垫的。

pod-service

从上图中可以看到,在 k8s 的 Service 定义了一个服务的访问入口地址,前端应用 Frontend (Pod) 通过该入口地址访问其背后的一组由 Pod 副本组成的集群实例,Service 与其后端 Pod 副本集群之间则是通过 Label Selector 来实现无缝对接的。RC、RS、Deployment 的作用实际上是保证 Service 的服务能力和服务质量始终符合预期标准

注意,上一篇文章我们讲到 RC 被 RS 替代,RC 的功能逐步由 RS & Deployment 替换,实际上 Deployment 底层调用了 RS 。

k8s 提供的微服务网格架构

通过分析、识别并建模系统中的所有服务为微服务 —— Kubernees Service,我们的 系终由多个提供不同业务能力而又彼此独立的微服务单元组以的,服分之间通过 TCP/IP 进行通信,从而形成了强大而又灵活的 弹性网格,拥有强大的 分布式能刀、弹性扩展能力、容错能力,程序架构也变得简单和直观许多,如下图所示:

上图中每个 Pod 都会被分配一个单独的 IP 地址,而且每个 Pod 都提供了一个独立 Endpoint ( Pod IP + ContainerPort) 以被客户端访问。现在多个 Pod 副本组成了一个集群来提供服务,那么 客户端 Client 如何来访问它们呢?

负载均衡与服务发现

一般的做法是部署一个负载衡器(软件或硬件),为这组 Pod 开启一个对外的服务端口如 8000 端口,并且将这些 Pod 的 Endpoint 列表加入 8000 端口的转发列表,客户端就可以通过负载均衡器的对外 IP 地址 + 服务端口来访问此服务。客户端的请求最后会被转发到哪个 Pod,由 负载均衡器的算法 所决定。

k8s 也遵循上述常规做法,运行在每个 Node 上的 kube-proxy 进程其实就是一个智能的软件负载均衡器,负责把对 Service 的请求转发到后端的某个 Pod 实例上,并在内部实现服务的负载均衡与会话保持机制。但 k8s 发明了一种很巧妙又影响深远的设计:Service 没有共用一个负载均衡器的 IP 地址,每个 Service 都被分配了一个全局唯一的虚拟 IP( ClusterIP )地址,这样一来,每个服务就变成了具备唯一 IP 地址的通信节点,服务调用就变成了最基础的 TCP 网络通信问题。

我们知道,Pod 的 Endpoint 地址会随着 Pod 的销毁和重建而发生改变,因为新 Pod 的 IP 地址与之前旧 Pod 的不同。而 Service 一旦被创建,k8s 就会自动为它配一个可用的 Cluster IP,而且在 Service 的整个生命周期内,它的 Cluster IP 是不会随之发生改变的。这样一来,服务发现 这个棘手的问题在 k8s 的架构里也得以轻松解决:只要 用 Service 的 Name 与 Service 的 Cluster IP 地址做一个 DNS 域名映射 即可完美解决问题。

Service 示例

Service 在 k8s 中是一个 REST 对象,和 Pod 类似。像所有的 REST 对象一样,Service 定义可以基于 POST 方式,请求 API server 创建新的实例。Service 对象的名称必须是合法的 RFC 1035 标签名称。

通过上面基本概念的介绍,接下来我们看下 Service 定义的 yaml 文件:

apiVersion: v1
kind: Service
metadata:
  name: my-redis-service
  label: 
    name: my-redis-service
spec:
  selector:
    app: my-redis
  ports:
    - protocol: TCP
      port: 6379
      targetPort: 30001

上述 yaml 配置文件创建一个名称为 "my-redis-service" 的 Service 对象,它会将请求代理到使用 TCP 端口 30001,并且具有标签 "app=my-redis" 的 Pod 上。

k8s 为该 Service 服务分配一个 IP 地址( “ClusterIP/集群 IP”),该 IP 地址由 服务代理(kube-proxy) 使用。(请参见下面的 VIP/虚拟IP 和 Service 代理 )。

服务选择符(Label Selector) 的控制器不断扫描与其选择算符匹配的 Pod,然后将所有更新发布到也称为 “my-redis-service” 的 Endpoint 对象。

说明:需要注意的是,Service 能够将一个接收 port 映射到任意的 targetPort。默认情况下,targetPort 将被设置为与 port 字段相同的值。

虚拟 IP 和 Service 代理

在 k8s 集群中,每个 Node 运行一个 kube-proxy 进程kube-proxy 负责为 Service 实现了一种 VIP(虚拟 IP)的形式

为什么不使用 DNS 轮询?

或许有人会问到为什么 k8s 要依赖代理将入站流量转发到后端。是否有其他方法呢?例如,是否可以配置具有多个 A 值(或 IPv6 为 AAAA)的 DNS 记录,并依靠轮询名称解析?

答案是当然不可以,使用服务代理有以下几个原因:

  • DNS 实现的历史由来已久,它不遵守记录 TTL,并且在名称查找结果到期后对其进行缓存。

  • 有些应用程序仅执行一次 DNS 查找,并无限期地缓存结果。

  • 即使应用和库进行了适当的重新解析,DNS 记录上的 TTL 值低或为零也可能会给 DNS 带来高负载,从而使管理变得困难。

k8s 支持以下三种代理模式:

  • userspace 代理模式。

  • iptables 代理模式。

  • IPVS 代理模式。

userspace 代理模式

这种模式,kube-proxy 会监视 k8s 控制平面对 Service 对象和 Endpoints 对象的添加和移除操作对每个 Service,它会在本地 Node 上打开一个端口(随机选择)。任何连接到 “代理端口” 的请求,都会被代理到 Service 的后端 Pods 中的某个上面(如 Endpoints 所报告的一样)。使用哪个后端 Pod,是 kube-proxy 基于 SessionAffinity 来确定的。

最后,它配置 iptables 规则,捕获到达该 Service 的 clusterIP(是虚拟 IP) 和 Port 的请求,并重定向到代理端口,代理端口再代理请求到后端 Pod。

默认情况下,用户空间模式下的 kube-proxy 通过轮转算法选择后端

userspace 代理模式

iptables 代理模式

这种模式,kube-proxy 会监视 k8s 控制节点对 Service 对象和 Endpoints 对象的添加和移除对每个 Service,它会配置 iptables 规则,从而捕获到达该 Service 的 clusterIP 和端口的请求,进而将请求重定向到 Service 的一组后端中的某个 Pod 上面。对于每个 Endpoints 对象,它也会配置 iptables 规则,这个规则会选择一个后端组合。

默认的策略是,kube-proxy 在 iptables 模式下随机选择一个后端

使用 iptables 处理流量具有较低的系统开销,因为流量由 Linux netfilter 处理, 而 无需在用户空间和内核空间之间切换。这种方法也可能更可靠。

如果 kube-proxy 在 iptables 模式下运行,并且 所选的第一个 Pod 没有响应,则连接失败。这与 用户空间模式 不同:在这种情况下,kube-proxy 将检测到与第一个 Pod 的连接已失败, 并会自动使用其他后端 Pod 重试

你可以使用 Pod 就绪探测器 验证后端 Pod 可以正常工作,以便 iptables 模式下的 kube-proxy 仅看到测试正常的后端。这样做意味着你 避免将流量通过 kube-proxy 发送到已知已失败的 Pod

iptables 代理模式

IPVS 代理模式

特性状态Kubernetes v1.11 [stable]

在 ipvs 模式下,kube-proxy 监视 Kubernetes 服务和端点,调用 netlink 接口相应地创建 IPVS 规则, 并定期将 IPVS 规则与 k8s 服务和端点同步。该控制循环可确保 IPVS 状态与所需 状态匹配。访问服务时,IPVS 将流量定向到后端 Pod 之一。

IPVS 代理模式基于类似于 iptables 模式的 netfilter 挂钩函数, 但是 使用哈希表作为基础数据结构,并且在内核空间中工作。这意味着,与 iptables 模式下的 kube-proxy 相比,IPVS 模式下的 kube-proxy 重定向通信的延迟要短,并且在同步代理规则时具有更好的性能。与其他代理模式相比,IPVS 模式还支持更高的网络流量吞吐量

IPVS 提供了更多选项来平衡后端 Pod 的流量。这些是:

  • rr:轮替(Round-Robin)

  • lc:最少链接(Least Connection),即打开链接数量最少者优先

  • dh:目标地址哈希(Destination Hashing)

  • sh:源地址哈希(Source Hashing)

  • sed:最短预期延迟(Shortest Expected Delay)

  • nq:从不排队(Never Queue)

说明:要在 IPVS 模式下运行 kube-proxy,必须在启动 kube-proxy 之前使 IPVS 在节点上可用。当 kube-proxy 以 IPVS 代理模式启动时,它将验证 IPVS 内核模块是否可用。如果未检测到 IPVS 内核模块,则 kube-proxy 将退回到以 iptables 代理模式运行。

IPVS 代理模式

在这些代理模型中,绑定到服务 IP 的流量:在客户端不了解 k8s 或 Service 服务或 Pod 的任何信息的情况下,将 Port 代理到适当的后端。

如果要 确保每次都将来自特定 Client 客户端的连接传递到同一 Pod, 则可以通过将 service.spec.sessionAffinity 设置为 "ClientIP" (默认值是 "None"),来基于 Client 客户端的 IP 地址选择会话亲和性。你还可以通过适当设置 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds 来设置最大会话停留时间。(默认值为 10800 秒,即 3 小时)。

说明:在 Windows 上,不支持为服务设置最大会话停留时间。

k8s 中 Service 端口的区分

在 k8s 中的 Service 存在以下这些端口,他们各自的职责如下:

  • nodePort,外部流量访问 k8s 集群中 service 入口的一种方式(另一种方式是 LoadBalancer),即nodeIP:nodePort 是提供给外部流量访问 k8s 集群中 service 的入口。

  • port,k8s 集群内部服务之间访问 service 的入口。即 clusterIP:port 是 service 暴露在 clusterIP 上的端口。

  • targetPort,容器暴露(EXPOSE)的端口(最终的流量端口),即具体业务进程在容器内的 targetPort 上提供 TCP/IP 接入。targetPort 是 pod 上的端口,从 port 和nodePort 上来的流量,经过 kube-proxy 流入到后端 pod 的 targetPort 上,最后进入容器。制作容器时暴露的端口一致(使用DockerFile中的EXPOSE)

  • hostPort,这是一种直接定义 Pod 网络的方式。hostPort 是直接将容器的端口与所调度的节点上的端口路由,这样用户就可以通过宿主机的 IP 加上来访问 Pod 了。

说明,hostPort 有个缺点,因为 Pod 重新调度的时候该Pod被调度到的宿主机可能会变动,这样就变化了,用户必须自己维护一个Pod与所在宿主机的对应关系。使用了 hostPort 的容器只能调度到端口不冲突的 Node 上,除非有必要(比如运行一些系统级的 daemon 服务),不建议使用端口映射功能。如果需要对外暴露服务,建议使用 NodePort Service。如果未指定 targetPort,默认情况下 targetPort 与 port 相同。

Service 多端口问题

很多服务都存在多端口的问题,通常一个端口提供业务服务,另外一个端口提供管理服务,比如:MyCat(基于 java 语言编写的数据库中间件,类比数据库代理)、Codis(分布式 Redis 解决方案)、RabbitMQ(RabbitMQ Management) 等常见中间件。k8s 的 Service 支持多个 Endpoint ,有多个 Endpoint 存在的情况下,要求每个 Endpoint 都定义一个名称来区分

举个例子,下面以 Tomcat 多端口的 Service 定义为例:

apiVersion: v1
kind: Service
metadata:
  name: tomcat-service
spec:
  selector:
    app: MyTomcat
  ports:
    - name: http-service-port
      protocol: TCP
      port: 8080
      targetPort: 8080
    - name: https-service-port
      protocol: TCP
      port: 443
      targetPort: 9100
    - name: shutdown-port
      protocol: TCP
      port: 8005
      targetPort: 8005

Service 存在多端口时,为什么要给每个端口都命名?其实这个问题涉及到上面我们提到的 服务发现机制,在 k8s 中每个 Service 都分配有一个唯一的 Cluster IP,只需 Service 的 Name 与 Service 的 Cluster IP 地址做一个 DNS 域名映射,这样就可以通过 Service 的名称 Name 找到对应的 Cluster IP

外部系统访问 Service

为了更深入地理解和掌握 k8s,我们需要弄明白 k8s 的 3 种 IP,这 3 种 IP 分别如下。

  • Node IP:Node 的 IP 地址。

  • Pod IP:Pod 的 IP 地址。

  • Cluster IP:Scrvice 的 IP 地址。

首先,Node IP 是 k8s 集群中每个节点的物理网卡的 IP 地址,是一个真实存在的物理网络,所有属于这个网络的服务器都能通过这个网络直接通信,不管其中是否有部分节点不属于这个 k8s 集群。这也表明在 k8s 集群之外的节点访问 k8s 集群之内的某个节点或者 TCP/IP 服务时,都必须通过 Node IP 通信

其次,Pod IP 是每个 Pod 的 IP 地址,它是 Docker Engine 根据 docker0 网桥的 IP 地址段进行分配的,通常是一个虚拟的二层网络,前面说过, k8s 要求位于不同 Node 上的 Pod 都能够彼此直接通信,所以 k8s 里一个 Pod 里的容器访问另外一个 Pod 里的容器时,就是通过 Pod IP 所在的虚拟二层网络进行通信的,而真实的 TCP/IP 流量是通过 Node IP 所在的物理网卡流出的。

最后,说说 Service 的 Cluster IP,它也是一种虚拟的IP,但更像一个 “伪造” 的 IP 网络,原因有以下几点:

  • Cluster IP 仅仅作用于 k8s Service 这个对象,并由 k8s 管理和分配 IP 地址(来源于 Cluster IP 地址池)。

  • Cluster IP 无法被 Ping,因为没有一个 “实体网络对象” 来响应。

  • Cluster IP 只能结合 Service Port 组成一个具体的通信口,单独的 Cluster IP 不具备 TCP/IP 通信的基础,并且它们属于 k8s 集群这样一个封闭的空间,集群外的节点如果要访问这个通信端口,则需要做一些额外的工作。

  • 在 k8s 集群内, Node IP、Pod IP 与 Culster IP 网络之间的通信,采用的是 k8s 自己设计的一种编程方式的特殊路由规则,与我们熟知的 TCP/IP 路由有很大的不同。

根据上面的分析和总结,我们基本明白了:Service 的 Culster IP 属于 k8s 集群内部的地址,无法在集群外部直接使用这个地址。那么问题来了:实际上在我们开发的业务系统中肯定或多或少的有一部分服务是要提供给 k8s 集群外部的应用程序或者用户来访问使用的。典型的例子就是 web 端的服务模块,上面我们定义 tomcat-service 的例子,怎么提供给外部用户访问呢?

采用 NodePort 是解决上述问题的最直接,有效的常见做法,修改如下:

apiVersion: v1
kind: Service
metadata:
  name: tomcat-service
spec:
  # 提供集群外部用户访问
  type: NodePort
  selector:
    app: MyTomcat
  ports:
    - name: http-service-port
      protocol: TCP
      port: 8080
      targetPort: 8080
      nodePort: 30002
  selector:
    tier: frontend

接下来浏览器访问地址为:http://<nodePort IP>:30002/,就可以看到 Tomcat 的欢迎界面了。

NodePort 的实现方式是在 k8s 集群里的每个 Node 上都为需要外部访问的 Service 开启一个对应的 TCP 监听端口,外部系统只要用任意一个 Node IP  + 具体的 NodePort 即可访问此服务,在任意 Node 上运行 netstat 命令,就可以看到有 NodePort 被监听:

netstart -tlp | grep 30002
...
tcp6  0  0  [::]:30002         [::]:*          LISTEN     1125/kube-proxy

但 NodePort 并没有完全解决外部访问 Service 的问题,比如:负载均衡访问。此时通常需要引入 Load balancer(LB)组件,它独立于 k8s 集群之外,通常有硬件或软件的负载均衡器,比如硬件的有 F5,软件的有 HAProxy 或者 Nginx(ingress-nginx),访问结构如下所示:

NodePort-LoadBalancer

谷歌共有云 GCE 上,只要把 Service 的 type=NodePort 修改为 type=LoadBalancer,k8s 就会自动创建一个对应的 LB 实例并返回它的 IP 地址供给外部客户端使用。其他公有云上面实现了此特性也可以实现上述功能。另外裸机上面的类似机制(Bare Metal Service Load Balancers)也在被开发。

关于 Service 的更多信息,请查看:https://kubernetes.io/zh-cn/docs/concepts/services-networking/service/

Job & CronJob

批处理任务通常并行(或者串行)启动多个计算进程去处理一批工作项 (work item),在处理完成后,整个批处理任务结束。从 k8s v1.2 版本开始支持批处理类型的应用,我们可以通过 k8s Job 这种新的资源对象定义并启动一个批处理任务 Job。与 RC、Deployment、ReplicaSet、DaemonSet 类似,Job 也控制一组 Pod 容器。从这个角度来看,Job 也是一种特殊的 Pod 副本自动控制器,同时 Job 控制 Pod 副本与 RC 等控制器的工作机制有以下重要差别。

(1) Job 所控制的 Pod 副本是短暂运行的,可以将其视为一组 Docker 容器,其中的每个 Docker 容器都仅仅运行一次。当 Job 控制的所有 Pod 副本都运行结束时,对应的 Job 也就结束了。Job 在实现方式上与 RC 等副本控制器不同,Job 生成的 Pod 副本是不能自动重启的,对应 Pod 副本的 RestartPoliy 都被设置为 Never。因此,当对应的 Pod 副本都执行完成时,相应的 Job 也就完成了控制使命,即 Job 生成的 Pod 在 k8s 中是短暂存在的。k8s v1.5版本之后又提供了类似 crontab(Linux crontab 是用来定期执行程序的命令)的定时任务——CronJob,解决了某些批处理任务需要定时周期性重复执行的问题

(2) Job 所控制的 Pod 副本的工作模式能够多实例并行计算,以 TensorFlow 框架为例,可以将一个机器学习的计算任务分布到 10 台机器上,在每台机器上都运行一个 worker 执行计算任务,这很适合通过 Job 生成 10 个Pod 副本同时启动运算。

运行示例 Job

下面是一个 Job 配置示例。它负责计算 π 到小数点后 2000 位,并将结果打印出来。此计算大约需要 10 秒钟完成。

apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    spec:
      containers:
      - name: pi
        image: perl:5.34.0
        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
      restartPolicy: Never
  backoffLimit: 4

检查 Job 的状态:

kubectl describe jobs/pi

关于 Job 的更多信息,请查看:https://kubernetes.io/zh-cn/docs/concepts/workloads/controllers/job/

运行示例 CronJob

CronJob 用于执行周期性的动作,按某种排期表(Schedule)运行 Job(单个任务或多个并行任务),例如备份、报告生成、数据同步等。这些任务中的每一个都应该配置为周期性重复的(例如:每天/每周/每月一次);你可以定义任务开始执行的时间间隔。

下面的 CronJob 示例清单会在每分钟打印出当前时间和问候消息:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox:1.28
            imagePullPolicy: IfNotPresent
            command:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

Cron 时间表语法

# ┌───────────── 分钟 (0 - 59)
# │ ┌───────────── 小时 (0 - 23)
# │ │ ┌───────────── 月的某天 (1 - 31)
# │ │ │ ┌───────────── 月份 (1 - 12)
# │ │ │ │ ┌───────────── 周的某天 (0 - 6)(周日到周一;在某些系统上,7 也是星期日)
# │ │ │ │ │                          或者是 sun,mon,tue,web,thu,fri,sat
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
输入 描述 相当于
@yearly (or @annually) 每年 1 月 1 日的午夜运行一次 0 0 1 1 *
@monthly 每月第一天的午夜运行一次 0 0 1 * *
@weekly 每周的周日午夜运行一次 0 0 * * 0
@daily (or @midnight) 每天午夜运行一次 0 0 * * *
@hourly 每小时的开始一次 0 * * * *

要生成 CronJob 时间表表达式,你还可以使用 crontab.guru 之类的 Web 工具。

  • crontab.guru =》https://crontab.guru/

crontab guru

关于 CronJob 更多信息,请查看:https://kubernetes.io/zh-cn/docs/concepts/workloads/controllers/cron-jobs/

Volume

Container 中的文件在磁盘上是临时存放的,这给 Container 中运行的较重要的应用程序带来一些问题。

  1. 当容器崩溃时文件丢失。kubelet 会重新启动容器,但容器会以干净的状态重启。

  2. 在同一 Pod 中运行 多个容器并共享文件时出现

k8s 卷(Volume) 这一抽象概念能够解决这两个问题。

k8s 支持很多类型的卷。Pod 可以同时使用任意数目的卷类型。临时卷类型的生命周期与 Pod 相同,但持久卷可以比 Pod 的存活期长当 Pod 不再存在时,k8s 也会销毁临时卷;不过 k8s 不会销毁持久卷。对于给定 Pod 中任何类型的卷,在容器重启期间数据不会丢失

卷的核心是一个目录,其中可能存有数据,Pod 中的容器可以访问该目录中的数据。所采用的特定的卷类型将决定该目录如何形成的、使用何种介质保存数据以及目录中存放的内容。

k8s 中支持的 Volume 类型很多,例如:glusterfs、cephfs、configMap、emptyDir、hostPath、local、nfs、persistentVolumeClaim/PersistentVolume、portworxVolume、secret 等。

如何使用 Volume

Volume 的使用也比较简单,在大多数情况下,我们 先在 Pod 上声明一个 Volume,然后在容器里引用该Volume 并挂载(Mount)到容器里的某个目录上

emptyDir

当 Pod 分派到某个 Node 上时,emptyDir 卷会被创建,并且在 Pod 在该节点上运行期间,卷一直存在。就像其名称表示的那样,卷最初是空的。尽管 Pod 中的容器挂载 emptyDir 卷的路径可能相同也可能不同,这些容器都可以读写 emptyDir 卷中相同的文件。当 Pod 因为某些原因被从节点上删除时,emptyDir 卷中的数据也会被永久删除。emptyDir 卷也称为 “临时卷”

说明:容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃期间 emptyDir 卷中的数据是安全的。

emptyDir 的一些用途

  • 缓存空间,例如基于磁盘的归并排序。

  • 为耗时较长的计算任务提供检查点,以便任务能方便地从崩溃前状态恢复执行。

  • 在 Web 服务器容器服务数据时,保存内容管理器容器获取的文件。

取决于你的环境,emptyDir 卷存储在该节点所使用的介质上;这里的介质可以是磁盘或 SSD 或网络存储。但是,你可以将 emptyDir.medium 字段设置为 "Memory",以告诉 k8s 为你挂载 tmpfs(基于 RAM 的文件系统)。虽然 tmpfs 速度非常快,但是要注意它与磁盘不同。tmpfs 在节点重启时会被清除,并且你所写入的所有文件都会计入容器的内存消耗,受容器内存限制约束。

说明:当启用 SizeMemoryBackedVolumes 特性门控 时,你可以为基于内存提供的卷指定大小。如果未指定大小,则基于内存的卷的大小为 Linux 主机上内存的 50%。

emptyDir 配置示例

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}

hostPath

警告:HostPath 卷存在许多安全风险,最佳做法是尽可能避免使用 HostPath。当必须使用 HostPath 卷时,它的范围应仅限于所需的文件或目录,并以只读方式挂载。如果通过 AdmissionPolicy 限制 HostPath 对特定目录的访问,则必须要求 volumeMounts 使用 readOnly 挂载以使策略生效。

hostPath 卷能将主机节点文件系统上的文件或目录挂载到你的 Pod 中。虽然这不是大多数 Pod 需要的,但是它为一些应用程序提供了强大的逃生舱。

例如,hostPath 的一些用法有

  • 运行一个需要访问 Docker 内部机制的容器;可使用 hostPath 挂载 /var/lib/docker 路径。

  • 在容器中运行 cAdvisor 时,以 hostPath 方式挂载 /sys

  • 允许 Pod 指定给定的 hostPath 在运行 Pod 之前是否应该存在,是否应该创建以及应该以什么方式存在。

除了必需的 path 属性之外,你可以选择性地为 hostPath 卷指定 type。支持的 type 值如下:

取值 行为
空字符串(默认) 用于向后兼容,这意味着在安装 hostPath 卷之前不会执行任何检查。
DirectoryOrCreate 如果在给定路径上什么都不存在,那么将根据需要创建空目录,权限设置为 0755,具有与 kubelet 相同的组和属主信息。
Directory 在给定路径上必须存在的目录。
FileOrCreate 如果在给定路径上什么都不存在,那么将在那里根据需要创建空文件,权限设置为 0644,具有与 kubelet 相同的组和所有权。
File 在给定路径上必须存在的文件。
Socket 在给定路径上必须存在的 UNIX 套接字。
CharDevice 在给定路径上必须存在的字符设备。
BlockDevice 在给定路径上必须存在的块设备。

当使用这种类型的卷时要小心,因为:

  • HostPath 卷可能会暴露特权系统凭据(例如 Kubelet)或特权 API(例如容器运行时套接字),可用于容器逃逸或攻击集群的其他部分。

  • 具有相同配置(例如基于同一 PodTemplate 创建)的多个 Pod 会由于节点上文件的不同而在不同节点上有不同的行为。

  • 下层主机上创建的文件或目录只能由 root 用户写入。你需要在 特权容器中以 root 身份运行进程,或者修改主机上的文件权限以便容器能够写入 hostPath 卷。

hostPath 配置示例

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # 宿主上目录位置
      path: /data
      # 此字段为可选,支持的 type 如上表
      type: Directory

nfs

nfs 卷能将 NFS (网络文件系统) 挂载到你的 Pod 中。不像 emptyDir 那样会在删除 Pod 的同时也会被删除,nfs 卷的内容在删除 Pod 时会被保存,卷只是被卸载。这意味着 nfs 卷可以被预先填充数据,并且这些数据可以在 Pod 之间共享。

注意:在使用 NFS 卷之前,你必须运行自己的 NFS 服务器并将目标 share 导出备用。

nfs 配置示例

使用 NFS 网络文件系统 的共享目录存储数据时,我们需要在系统中需要在系统中部署一个 NFS Server。定义 NFS 类型的 Volume 示例如下:

  volumes:
  - name: nfs
    nfs:
      # 修改为你部署的 NFS Server 地址
      server: nfs-server.localhost
      path: /data

portworxVolume

portworxVolume 是一个 可伸缩的块存储层,能够以 超融合(hyperconverged) 的方式与 k8s 一起运行。Portworx 支持对服务器上存储的指纹处理、基于存储能力进行分层以及跨多个服务器整合存储容量。Portworx 可以以 in-guest 方式在虚拟机中运行,也可以在裸金属 Linux 节点上运行。

Portworx Volume 配置示例

portworxVolume 类型的卷可以通过 k8s 动态创建,也可以预先配备并在 Pod 内引用。下面是一个引用预先配备的 portworxVolume 的示例 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: test-portworx-volume-pod
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /mnt
      name: pxvol
  volumes:
  - name: pxvol
    # 此 Portworx 卷必须已经存在
    portworxVolume:
      volumeID: "pxvol"
      fsType: "<fs-type>"

说明:在 Pod 中使用 portworxVolume 之前,你要确保有一个名为 pxvol 的 PortworxVolume 存在。

关于 Volume 的更多信息,请查看:https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/

PV & PVC

之前提到的 Volume 是被定义在 Pod 上的,属于计算资源的一部分,而实际上,网络存储是相对独立于计算资源而存在的一种实体资源。比如在使用虚拟机(VM)的情况下,我们通常会先定义一个网络存储,然后从中划出一个 “网盘” 并挂接到虚拟机上。Persistent Volume(PV) 和与之相关联的 Persistent Volume Claim(PVC) 也起到了类似的作用。

存储的管理是一个与计算实例的管理完全不同的问题。

持久卷(PersistentVolume,PV)  是集群中的一块存储,可以由管理员事先制备, 或者使用存储类(Storage Class)来动态制备。持久卷是集群资源,就像节点也是集群资源一样。

持久卷申领(PersistentVolumeClaim,PVC)  表达的是用户对存储的请求。概念上与 Pod 类似,Pod 会耗用节点资源,而 PVC 申领会耗用 PV 资源Pod 可以请求特定数量的资源(CPU 和 Memory 内存);同样 PVC 申领也可以请求特定的大小和访问模式 (例如,可以要求 PV 卷能够以 ReadWriteOnce、ReadOnlyMany 或 ReadWriteMany 模式之一来挂载)。

尽管 PersistentVolumeClaim 允许用户消耗抽象的存储资源, 常见的情况是针对不同的问题用户需要的是具有不同属性(如,性能)的 PersistentVolume 卷。集群管理员需要能够提供不同性质的 PersistentVolume, 并且这些 PV 卷之间的差别不仅限于卷大小和访问模式,同时又不能将卷是如何实现的这些细节暴露给用户。为了满足这类需求,就有了 存储类(StorageClass) 资源。

PV 可以被理解成 k8s 集群中的某个网络存储对应的一块存储,它与 Volume 类似,但有以下区别。

  • PV 只能是网络存储,不属于任何 Node,但可以在每个 Node 上访问。

  • PV 并不是被定义在 Pod 上的,而是独立于 Pod 之外定义的。

  • PV 目前支持的类型包括:gcePersistentDisk(已弃用)、awsElasticBlockStore(已弃用)、azureFile(已弃用)、azureDisk(已弃用)、fc(Fibre Channel)、flocker(已弃用)、nfs、iscsi、rbd(Rados Block Device),cephfs、cinder(已弃用)、 glusterfs、vsphereVolume(已弃用)、Quobyte Volumes、VMware Photon、Portworx Volumes、ScalelO Volumes 和 HostPath(仅供单机测试)。

注意 PV 支持的类型,有部分随着 k8s 版本的升级,已经被弃用了,了解最新情况,请自行查看 k8s 官网。

持久卷(PersistentVolume,PV)

每个 PV 对象都包含 spec 部分和 status 部分,分别对应卷的 规约 和 状态PersistentVolume 对象的名称必须是合法的 DNS 子域名。

下面给出了 NFS 类型的 PV 的一个 YAML 定义文件,声明了需要 5Gi 的存储空间:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv0003
spec:
  # 指定 PV 卷容量
  capacity:
    storage: 5Gi
  # PV 卷模式
  volumeMode: Filesystem
  # PV 卷访问模式
  accessModes:
    - ReadWriteOnce
  # PV 卷回收策略
  persistentVolumeReclaimPolicy: Recycle
  # 指定 PV 卷所属类
  storageClassName: slow
  # 挂载选项
  mountOptions:
    - hard
    - nfsvers=4.1
  # 指定 PV 卷类型 nfs
  nfs:
    path: /tmp
    server: 172.17.0.2

容量(capacity)

通常情况,每个 PV 卷都有确定的存储容量。容量属性是使用 PV 对象的 capacity 属性来设置的。

目前,存储大小是可以设置和请求的唯一资源。未来可能会包含 IOPS、吞吐量等属性。

卷模式(volumeMode)

特性状态:Kubernetes v1.18 [stable]

针对 PV 持久卷,k8s 支持两种卷模式(volumeModes):Filesystem(文件系统) 和 Block(块)。volumeMode 是一个可选的 API 参数。如果该参数被省略,默认的卷模式是 Filesystem

  • volumeMode 属性设置为 Filesystem 的卷会被 Pod 挂载(Mount) 到某个目录。如果卷的存储来自某块设备而该设备目前为空,k8s 会在第一次挂载卷之前在设备上创建文件系统。

  • volumeMode 设置为 Block,以便将卷作为原始块设备来使用。这类卷以块设备的方式交给 Pod 使用,其上没有任何文件系统。这种模式对于为 Pod 提供一种使用最快可能方式来访问卷而言很有帮助, Pod 和卷之间不存在文件系统层。另外,Pod 中运行的应用必须知道如何处理原始块设备。关于如何在 Pod 中使用 volumeMode: Block 的卷, 可参阅 原始块卷支持。

访问模式(accessModes)

PersistentVolume 卷可以用资源提供者所支持的任何方式挂载到宿主系统上。

在命令行接口(CLI)中,访问模式也使用以下缩写形式:

  • RWO - ReadWriteOnce,卷可以被一个 Node 以读写方式挂载。ReadWriteOnce 访问模式也允许运行在同一节点上的多个 Pod 访问卷。

  • ROX - ReadOnlyMany,卷可以被多个 Node 以只读方式挂载。

  • RWX - ReadWriteMany,卷可以被多个 Node 以读写方式挂载。

  • RWOP - ReadWriteOncePod,卷可以被单个 Pod 以读写方式挂载。如果你想确保整个集群中只有一个 Pod 可以读取或写入该 PVC, 请使用该访问模式。这只支持 CSI 卷以及需要 Kubernetes 1.22 以上版本。

说明:k8s 使用卷访问模式来匹配 PersistentVolumeClaim 和 PersistentVolume。在某些场合下,卷访问模式也会限制 PersistentVolume 可以挂载的位置。卷访问模式并不会在存储已经被挂载的情况下为其实施写保护。即使访问模式设置为 ReadWriteOnce、ReadOnlyMany 或 ReadWriteMany,它们也不会对卷形成限制。例如,即使某个卷创建时设置为 ReadOnlyMany,也无法保证该卷是只读的。如果访问模式设置为 ReadWriteOncePod,则卷会被限制起来并且只能挂载到一个 Pod 上。

类(StorageClass)

每个 PV 可以属于某个类(Class),通过将其 storageClassName 属性设置为某个 StorageClass 的名称来指定。特定类的 PV 卷只能绑定到请求该类存储卷的 PVC 申领。未设置 storageClassName 的 PV 卷没有类设定,只能绑定到那些没有指定特定存储类的 PVC 申领。

早前,k8s 使用 Annotation (注解) volume.beta.kubernetes.io/storage-class 而不是 storageClassName 属性。这一注解目前仍然起作用,不过在将来的 k8s 发布版本中该注解会被彻底废弃。

回收策略(persistentVolumeReclaimPolicy)

目前的回收策略有:

  • Retain -- 手动回收。

  • Recycle -- 基本擦除 (rm -rf /thevolume/*)。

  • Delete -- 诸如 AWS EBS、GCE PD、Azure Disk 或 OpenStack Cinder 卷这类关联存储资产也被删除。

目前,仅 NFS 和 HostPath 支持回收(Recycle)。AWS EBS、GCE PD、Azure Disk 和 Cinder 卷都支持删除(Delete)。

挂载选项(mountOptions)

k8s 管理员可以指定持久卷被挂载到 Node 节点上时使用的附加挂载选项(mountOptions)。

说明:并非所有持久卷类型都支持挂载选项。

以下卷类型支持挂载选项:

  • awsElasticBlockStore(已弃用)

  • azureDisk(已弃用)

  • azureFile(已弃用)

  • cephfs

  • cinder (已弃用于 v1.18)

  • gcePersistentDisk(已弃用)

  • glusterfs

  • iscsi

  • nfs

  • quobyte (已弃用于 v1.22)

  • rbd

  • storageos (已弃用于 v1.22)

  • vsphereVolume

k8s 不对挂载选项(mountOptions)执行合法性检查。如果挂载选项是非法的,挂载就会失败

早前,k8s 使用 Annotation (注解) volume.beta.kubernetes.io/mount-options 而不是 mountOptions 属性。这一注解目前仍然起作用,不过在将来的 k8s 发布版本中该注解会被彻底废弃。

节点亲和性(nodeAffinity)

每个 PV 卷可以通过 设置节点亲和性(nodeAffinity)来定义一些约束,进而限制从哪些节点上可以访问此卷。使用这些卷的 Pod 只会被调度到节点亲和性规则所选择的节点上执行。要设置节点亲和性,配置 PV 卷 .spec 中的 nodeAffinity

说明:对大多数类型的卷而言,你不需要设置节点亲和性字段。AWS EBS、 GCE PD 和 Azure Disk 卷类型都能自动设置相关字段。你需要为 local 卷显式地设置此属性。

阶段状态(Phase)

每个卷会处于以下阶段(Phase)之一:

  • Available(可用)-- 卷是一个空闲资源,尚未绑定到任何 PVC;

  • Bound(已绑定)-- 该卷已经绑定到某 PVC;

  • Released(已释放)-- 所绑定的 PVC 已被删除,但是资源尚未被集群回收;

  • Failed(失败)-- 卷的自动回收操作失败。

命令行接口能够显示绑定到某 PV 卷的 PVC 对象。

持久卷申领(PersistentVolumeClaim,PVC)

每个 PVC 对象都有 spec 和 status 部分,分别对应 PVC 申领的 规约 和 状态PersistentVolumeClaim 对象的名称必须是合法的 DNS 子域名。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  # 资源限制
  resources:
    requests:
      storage: 8Gi
  storageClassName: slow
  selector:
    matchLabels:
      release: "stable"
    matchExpressions:
      - {key: environment, operator: In, values: [dev]}

上面 PVC 的 YAML 定义文件,其中 accessModes、volumeMode 和 PV 是一样的。

选择符(selector)

PVC 申领可以设置标签选择算符来进一步过滤卷集合。只有标签与选择算符相匹配的卷能够绑定到申领上。选择符包含两个字段:

  • matchLabels - 卷 必须包含 带有该值的标签。

  • matchExpressions - 通过设定 键(key)、值(value)列表和操作符(operator) 来构造的需求。合法的操作符有 In、NotIn、Exists 和 DoesNotExist

来自 matchLabels 和 matchExpressions 的所有需求都按逻辑与的方式组合在一起。这些需求都必须被满足才被视为匹配

类( StorageClass)

PVC 申领 可以通过为 storageClassName 属性设置 StorageClass 的名称来请求 特定的存储类。只有所请求的类的 PV 卷,即 storageClassName 值与 PVC 设置相同的 PV 卷, 才能绑定到 PVC 申领

PVC 申领不必一定要请求某个类。如果 PVC 的 storageClassName 属性值设置为 "", 则被视为要请求的是没有设置存储类的 PV 卷,因此这一 PVC 申领只能绑定到未设置存储类的 PV 卷(未设置注解或者注解值为 "" 的 PV 对象)在系统中不会被删除, 因为这样做可能会引起数据丢失。未设置 storageClassName 的 PVC 与此大不相同, 也会被集群作不同处理。具体筛查方式取决于 DefaultStorageClass 准入控制器插件 是否被启用。

  • 如果 准入控制器插件被启用,则管理员可以设置一个默认的 StorageClass。所有未设置 storageClassName 的 PVC 都只能绑定到隶属于默认存储类的 PV 卷。设置默认 StorageClass 的工作是通过将对应 StorageClass 对象的注解 storageclass.kubernetes.io/is-default-class 赋值为 true 来完成的。如果管理员未设置默认存储类,集群对 PVC 创建的处理方式与未启用准入控制器插件时相同。如果设定的默认存储类不止一个,准入控制插件会禁止所有创建 PVC 操作。

  • 如果 准入控制器插件被关闭,则不存在默认 StorageClass 的说法。所有未设置 storageClassName 的 PVC 都只能绑定到未设置存储类的 PV 卷。在这种情况下,未设置 storageClassName 的 PVC 与 storageClassName 设置为 "" 的 PVC 的处理方式相同。

取决于安装方法,默认的 StorageClass 可能在集群安装期间由 插件管理器(Addon Manager) 部署到集群中。

当某 PVC 除了请求 StorageClass 之外还 设置了 selector,则这两种需求会按 逻辑与 关系处理:只有隶属于所请求类且带有所请求标签的 PV 才能绑定到 PVC

说明:目前,设置了非空 selector 的 PVC 对象无法让集群为其动态制备 PV 卷。

早前,Kubernetes 使用注解 volume.beta.kubernetes.io/storage-class 而不是 storageClassName 属性。这一注解目前仍然起作用,不过在将来的 Kubernetes 发布版本中该注解会被彻底废弃。

Namespace

Namespace(命名空间,简称 ns)是 k8s 系统中另外一个重要的概念,Namespace 在很多情况下用于实现多租户的资源隔离。它提供一种机制,将同一集群中的资源划分为相互隔离的组。Namespace 将通过集群内部的资源对象 “分配” 到不同的 Namespace 中,形成逻辑上分组的不同项目、小组或用户组,便于不同的分组在共享使用整个集群资源的同时还能被分别管理。

k8s 集群在启动后会创建 3 个 Namespace,通过 kubectl 可以查看:

kubectl get ns --show-labels # 显示namespace的label
  • default 没有指明使用其它命名空间的对象所使用的默认命名空间。

  • kube-system k8s 系统创建对象所使用的命名空间。

  • kube-public 这个命名空间是自动创建的,所有用户(包括未经过身份验证的用户)都可以读取它。这个命名空间主要用于集群使用,以防某些资源在整个集群中应该是可见和可读的。这个命名空间的公共方面只是一种约定,而不是要求。

说明,如果不特别指明 ns,则用户创建的 Pod、RC、Service 都将被系统创建到 default 的命名空间。

Namespace 的定义

Namespace 的定义很简单。如下所示的 YAML 定了名为 development 的 ns :

apiVersion: v1
kind: Pod
metadata:
  name: busybox
  namespace: development
spec:
  containers:
  - name: busybox
    image: busybox:1.33.2
    command: 
    - sleep
    - "3600"

使用 kubectl 查看上面创建的 Pod :

# 指定命名空间查看 Pod
kubectl get pod --namespace=development
# 以下命令等效
kubectl get pod -n development

当每个租户创建一个 ns 来实现多租户的资源隔离时,还能结合 k8s 的资源配额(resource)管理,限定不同租户能使用的资源,例如:CPU、Memory 使用量 等。

Annotation

Annotation (注解)与 Label 类似,也使用 key/value 键值对 的形式进行定义。不同的是 Label 具有严格的命名规则,它定义的是 k8s 对象的元数据(Metadata),并且作用于 Label SelectorAnnotation 则是用户任意定义的附加信息,以便于外部工具查找。在很多时候, k8s 的模块自身会通过 Annotation 标记资源对象的一些特殊信息。

通常来说,用 Annotation 来记录的信息如下。

  • build 信息、release 信息、Docker 镜像(image)信息等,例如 timestamp(时间截)、release id号、PR号、镜像 Hash 值、Docker Registry 地址等。

  • 日志库、监控库、分析库等资源库的地址信息。

  • 程序调试工具信息,例如工具名称、版本号等。

  • 团队的联系信息,例如电话号码、负责人名称、网址等。

ConfigMap

存在的问题和 Docker 提供的解决方案

为了能够准确和深刻理解 k8s 中 ConfigMap 资源对象的功能和价值,需要从 Docker 说起。我们知道,Docker通过将程序、依赖库、数据及配置文件 “打包固化” 到一个不变的镜像文件中的做法,解决了应用的部署的难题,但这同时带来了棘手的问题,即 配置文件中的参数在运行期如何修改的问题。我们不可能在启动 Docker 容器后再修改容器里的配置文件,然后用新的配置文件重启容器里的用户主进程。为了解决这个问题,Docker 提供了两种方式:

  1. 在运行时通过容器的 环境变量 来传递参数。

  2. 通过 Docker Volume 将容器外的配置文件映射到容器内。

这两种方式都有其优势和缺点,在大多数情况下,后一种方式更合适我们的系统,因为 大多数应用通常从一个或多个配置文件中读取参数。但这种方式也有明显的缺陷:我们必须在目标主机上先创建好对应的配置文件,然后才能映射到容器里。

上述缺陷在 分布式环境 下变得更为严重(或突显),因为无论采用哪种方式,写入(修改)多台服务器上的某个指定文件,并确保这些文件保持一致,都是一个很难完成的目标。此外,在大多数情况下,我们都希望能集中管理系统的配置参数,而不是管理一堆配置文件。针对上述问题,k8s 给出了一个很巧妙的设计实现,如下所述。

k8s 如何巧妙的解决上述问题

首先,把所有的配置项都当作 key-value 字符串,当然 value 可以来自某个文本文件,比如配置项 password=123456、user=root、host=192.168.8.4 用于表示连接 FTP 服务器的配置参数。这些配置项可以作为 Map表中的一个项,整个 Map 的数据可以被持久化存储在 k8s 的 etcd 数据库中,然后提供 API 以方便 k8s 相关组件或客户应用 CRUD 操作这些数据,上述专门用来保存配置参数的 Map 就是 k8s 的 ConfigMap 资源对象。

接下来,k8s 提供了一种内建机制,将存储在 etcd 中的 ConfigMap 通过 Volume 映射的方式变成目标 Pod 内的配置文件,不管目标 Pod 被调度到哪台服务器上,都会完成自动映射。进一步地,如果 ConfigMap 中的 key-value 数据被修改,则映射到 Pod 中的 “配置文件” 也会随之自动更新

于是,k8s ConfigMap 就成了分布式系统中最为简单(使用方法简单,但背后实现比较复杂)且对应用无侵入的配置中心。ConfigMap 配置集中化的一种简单方案如下图所示:

k8s ConfigMap 集中化配置中心

ConfigMap 示例

ConfigMap 的示例,它的 一些键只有一个值,其他键的值看起来像是 配置的片段格式

apiVersion: v1
kind: ConfigMap
metadata:
  name: game-demo
data:
  # 类属性键;每一个键都映射到一个简单的值
  player_initial_lives: "3"
  ui_properties_file_name: "user-interface.properties"

  # 类文件键
  game.properties: |
    enemy.types=aliens,monsters
    player.maximum-lives=5    
  user-interface.properties: |
    color.good=purple
    color.bad=yellow
    allow.textmode=true    

你可以使用四种方式来使用 ConfigMap 配置 Pod 中的容器:

  • 在容器 container 命令和参数内。

  • 在容器 container 的环境变量 env。

  • 在只读卷里面添加一个文件,让应用来读取。

  • 编写代码在 Pod 中运行,使用 k8s API 来读取 ConfigMap。

配置 Pod 使用上面创建的名称叫 game-demo 的 ConfigMap 对象资源:

apiVersion: v1
kind: Pod
metadata:
  name: configmap-demo-pod
spec:
  containers:
    - name: demo
      image: alpine
      command: ["sleep", "3600"]
      env:
        # 定义环境变量
        - name: PLAYER_INITIAL_LIVES # 请注意这里和 ConfigMap 中的键名是不一样的
          valueFrom:
            configMapKeyRef:
              name: game-demo           # 这个值来自 ConfigMap
              key: player_initial_lives # 需要取值的键
        - name: UI_PROPERTIES_FILE_NAME
          valueFrom:
            configMapKeyRef:
              name: game-demo
              key: ui_properties_file_name
      volumeMounts:
      - name: config
        mountPath: "/config"
        readOnly: true
  volumes:
    # 你可以在 Pod 级别设置卷,然后将其挂载到 Pod 内的容器中
    - name: config
      configMap:
        # 提供你想要挂载的 ConfigMap 的名字
        name: game-demo
        # 来自 ConfigMap 的一组键,将被创建为文件
        items:
        - key: "game.properties"
          path: "game.properties"
        - key: "user-interface.properties"
          path: "user-interface.properties"

关于 ConfigMap 更多信息,请查看:https://kubernetes.io/zh-cn/docs/concepts/configuration/configmap/

Secret

Secret 是一种包含少量敏感信息例如密码、令牌或密钥的对象。这样的信息可能会被放在 Pod 规约中或者镜像中。使用 Secret 意味着你不需要在应用程序代码中包含机密数据。

由于创建 Secret 可以独立于使用它们的 Pod, 因此在创建、查看和编辑 Pod 的工作流程中暴露 Secret(及其数据)的风险较小。k8s 和在集群中运行的应用程序也可以对 Secret 采取额外的预防措施,例如避免将机密数据写入非易失性存储。

Secret 类似于 ConfigMap 但专门用于保存机密数据(base64 编码值)

注意:默认情况下,Kubernetes Secret 未加密地存储在 API 服务器的底层数据存储(etcd)中。任何拥有 API 访问权限的人都可以检索或修改 Secret,任何有权访问 etcd 的人也可以。此外,任何有权限在命名空间中创建 Pod 的人都可以使用该访问权限读取该命名空间中的任何 Secret;这包括间接访问,例如创建 Deployment 的能力。

为了安全地使用 Secret,请至少执行以下步骤:

  1. 为 Secret 启用静态加密

  2. 启用或配置 RBAC 规则来限制读取和写入 Secret 的数据(包括通过间接方式)。需要注意的是,被准许创建 Pod 的人也隐式地被授权获取 Secret 内容。

  3. 在适当的情况下,还可以 使用 RBAC 等机制来限制允许哪些主体创建新 Secret 或替换现有 Secret

Secret 的应用

Pod 可以用三种方式之一来使用 Secret:

  1. 作为挂载到一个或多个容器上的卷(volume)中的文件。

  2. 作为容器(container)的环境变量。

  3. 由 kubelet 在为 Pod 拉取镜像时使用。

k8s 控制面(kube-controller-manager)也使用 Secret;例如,引导 Token 令牌 Secret 是一种帮助自动化节点注册的机制

配置 Pod 文件以使用 Secret

打开默认编辑器,更新 data 字段中存放的 base64 编码的 Secret 值;然后配置 Pod 通过 卷(volumes)来挂载(volumeMounts)名为 mysecret 的 Secret。例如:

apiVersion: v1
data:
  # 填写 base 64 编码值
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm
kind: Secret
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: { ... }
  creationTimestamp: 2020-01-22T18:41:56Z
  name: mysecret
  namespace: default
  resourceVersion: "164619"
  uid: cfee02d6-c137-11e5-8d73-42010af00002
type: Opaque
---
apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  containers:
  - name: mypod
    image: redis
    volumeMounts:
    - name: foo
      mountPath: "/etc/foo"
      readOnly: true
  volumes:
  - name: foo
    secret:
      secretName: mysecret
      optional: false # 默认设置,意味着 "mysecret" 必须已经存在

关于 Secret 更多信息,请查看:https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/

总结

上述这些组件是 k8s 系统的核心组件,它们共同构成了 k8s 系统的框架和计算模型。通过对它们进行灵活组合,用户就可以快速、方便地对容器集群进行配置、创建和管理。除了本章所介绍的核心组件,在 k8s 系统中还有许多辅助配置的资源对象,例如 LimitRange(限制范围 )、ResourceOuota(资源配额)、Pod Readiness(Pod 就绪态)、容器探针(livenessProbe、readinessProbe 和 startupProbe)。另外,一些系统内部使用的对象 Binding(绑定),Event(事件) 等请参考 Kubernetes 的 API 文档。

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...