k8s 读书笔记 - kubernetes 基本概念和术语(下)
Posted dotNET跨平台
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了k8s 读书笔记 - kubernetes 基本概念和术语(下)相关的知识,希望对你有一定的参考价值。
前言
上一篇文章 中,我们介绍了 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 种集群部署方案:
每个组件独立集群环境部署(生产环境推荐),至少需要 9 台宿主机。
所有组件部署一个集群环境(开发环境推荐),至少需要 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 做铺垫的。
从上图中可以看到,在 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 通过轮转算法选择后端
。
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
。
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 代理模式运行。
在这些代理模型中,绑定到服务 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),访问结构如下所示:
谷歌共有云 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/
关于 CronJob 更多信息,请查看:https://kubernetes.io/zh-cn/docs/concepts/workloads/controllers/cron-jobs/
Volume
Container 中的文件在磁盘上是临时存放的,这给 Container 中运行的较重要的应用程序带来一些问题。
当容器崩溃时文件丢失
。kubelet 会重新启动容器,但容器会以干净的状态重启。在同一 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 发布版本中该注解会被彻底废弃。
以上是关于k8s 读书笔记 - kubernetes 基本概念和术语(下)的主要内容,如果未能解决你的问题,请参考以下文章