我们已经看到如何在 Kubernetes 中将持久存储添加到 Pod 中——这是一个有用的功能,因为 Pod 是 Kubernetes 中的基本构建块,并且它们在许多不同的工作负载构造中使用,例如部署(第 3 章)和作业(第 10 章)。现在,您可以将持久存储添加到它们中的任何一个,并在需要的地方构建有状态的 Pod,只要所有实例的卷规格相同。
工作负载构造如部署的限制在于所有 Pod 共享相同的规格,这为具有 ReadWriteOnce 访问方法的传统卷带来了问题,因为它们只能被单个实例挂载。当您的部署中只有一个副本时,这没问题,但这意味着如果您创建第二个副本,该 Pod 将无法创建,因为卷已经被挂载。
幸运的是,Kubernetes 有一个高级工作负载构造,当我们需要多个 Pod 并且每个 Pod 都有自己的磁盘时(这是一个非常常见的模式),这使我们的生活更轻松。正如 Deployment 是用于管理持续运行服务(通常是无状态服务)的高级构造,StatefulSet 是用于管理有状态服务的构造。
StatefulSet 具有一些有助于构建此类服务的属性。您可以定义一个卷模板,而不是在 PodSpec 中引用单个卷,Kubernetes 将为每个 Pod 创建一个新的持久卷声明 (PVC)(从而解决了使用 Deployment 和卷的问题,其中每个实例获得完全相同的 PVC)。StatefulSet 为每个 Pod 分配一个稳定的标识符,该标识符与特定的 PVC 相关联,并在创建、扩展和更新期间提供排序保证。使用 StatefulSet,您可以获得多个 Pod,并通过使用此稳定标识符来协调它们,从而可能为每个 Pod 分配不同的角色。
9.2.1 部署 StatefulSet
将其付诸实践,让我们看看两个流行的有状态工作负载——MariaDB 和 Redis,以及如何将它们作为 StatefulSet 部署。首先,我们将保持单个 Pod StatefulSet,这是最简单的演示,因为不需要担心多个角色。下一部分将添加具有不同角色的额外副本,以充分利用 StatefulSet 的强大功能。
MariaDB
首先,让我们将上一节中创建的单个 Pod MariaDB 部署转换为使用 StatefulSet,并利用 PVC 模板来避免我们自己创建单独的 PVC 对象。
清单 9.6 Chapter09/9.2.1_StatefulSet_MariaDB/mariadb-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mariadb
spec:
selector: ?
matchLabels: ?
app: mariadb-sts ?
serviceName: mariadb-service ?
replicas: 1
template:
metadata:
labels:
app: mariadb-sts
spec:
terminationGracePeriodSeconds: 10 ?
containers:
- name: mariadb-container
image: mariadb:latest
volumeMounts:
- name: mariadb-pvc ?
mountPath: /var/lib/mysql
resources:
requests:
cpu: 1
memory: 4Gi
env:
- name: MARIADB_ROOT_PASSWORD
value: "your database password"
volumeClaimTemplates: ?
- metadata: ?
name: mariadb-pvc ?
spec: ?
accessModes: ?
- ReadWriteOnce ?
resources: ?
requests: ?
storage: 2Gi ?
---
apiVersion: v1 ?
kind: Service ?
metadata: ?
name: mariadb-service ?
spec: ?
ports: ?
- port: 3306 ?
clusterIP: None ?
selector: ?
app: mariadb-sts ?
? StatefulSet 使用与第 3 章中讨论的 Deployments 相同的匹配标签模式。
? 这是对无头服务的引用,该服务在文件底部定义。
? StatefulSet 要求设置一个优雅终止期。这是 Pod 在被终止之前必须自行退出的秒数。
? MariaDB 数据卷挂载,现在在 volumeClaimTemplates 部分定义
? 使用 StatefulSet,我们可以定义 PersistentVolumeClaim 的模板,就像我们定义 Pod 副本的模板一样。该模板用于创建 PersistentVolumeClaims,将其与每个 Pod 副本关联。
? 此 StatefulSet 的无头服务
这个 StatefulSet 规范与上一节中相同的 MariaDB Pod 的 Deployment 规范有什么不同?除了不同的对象元数据外,还有两个关键变化。第一个区别是 PersistentVolumeClaim 的配置方式。在上一节中,它被定义为一个独立的对象。而在 StatefulSet 中,它被纳入到 volumeClaimTemplates 的定义中,类似于 Deployment 拥有 Pod 模板。在每个 Pod 中,StatefulSet 将根据此模板创建一个 PersistentVolumeClaim。对于单 Pod StatefulSet,您最终会得到类似的结果(但不需要定义单独的 PersistentVolumeClaim 对象),而在创建多个副本 StatefulSet 时,这变得至关重要。图 9.5 显示了 PersistentVolumeClaim(在 Deployment 中使用)和 volumeClaimTemplates (在 StatefulSet 中使用)并排对比。
如果在创建 StatefulSet 后查询 PVC,您会看到使用此模板创建了一个 PVC(为了可读性,某些列已被删除):
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES
mariadb-pvc-mariadb-0 Bound pvc-71b1e 2Gi RWO
主要区别在于,使用模板创建的 PVC 附加了 Pod 名称( mariadb-0 ,在第一个 Pod 的情况下)。因此,它不是 mariadb-pvc (声明模板的名称),而是 mariadb-pvc-mariadb-0 (声明模板名称和 Pod 名称的组合)。
与部署相比,另一个区别是服务在 StatefulSet 中通过 serviceName: mariadb-service 行进行引用,定义如下:
apiVersion: v1
kind: Service
metadata:
mariadb-service
spec:
ports:
- port: 3306
clusterIP: None
selector:
app: mariadb-sts
此服务与书中迄今为止介绍的服务略有不同,因为它被称为无头服务(在规范中用 clusterIP: None 表示)。与其他服务类型不同,未创建虚拟集群 IP 来平衡 Pods 之间的流量。如果您查询此服务的 DNS 记录(例如,通过进入 Pod 并运行 host mariadb-service ),您会注意到它仍然返回一个 A 记录。该记录实际上是 Pod 本身的 IP 地址,而不是虚拟集群 IP。对于具有多个 Pods 的无头服务(如 Redis StatefulSet;见下一节),查询该服务将返回多个 A 记录(即每个 Pod 一个)。
无头服务的另一个有用特性是,StatefulSet 中的 Pods 拥有自己的稳定网络身份。由于 StatefulSet 中的每个 Pod 都是唯一的,并且每个 Pod 都有其自己的附加存储卷,因此能够单独寻址它们是很有用的。这与 Deployment 中的 Pods 不同,后者设计为相同,因此在任何给定请求中连接到哪个 Pod 都无关紧要。为了方便直接连接到 StatefulSet 中的 Pods,每个 Pod 被分配一个递增的整数值,称为序号(0、1、2 等)。如果 StatefulSet 中的 Pod 在中断后被重新创建,它将保留相同的序号,而在 Deployment 中被替换的 Pods 则会被分配一个新的随机名称。
StatefulSet 中的 Pod 可以使用它们的序号通过构造 $STATEFULSET_NAME-$POD_ORDINAL.$SERVICE_NAME. 进行访问。在这个例子中,我们的单个 Pod 可以使用 DNS 地址 mariadb-0.mariadb-service 进行引用。从命名空间外部,您可以附加命名空间(就像任何服务一样)。例如,对于命名空间名为 production, ,Pod 可以通过 mariadb-0-mariadb-service.production.svc 进行访问。
要尝试在 StatefulSet 中运行的这个 MariaDB 实例,我们可以转发端口并使用 kubectl port-forward sts/mariadb 3306:3306 本地连接,但为了更有趣,我们来创建一个使用集群中临时 Pod 的 MariaDB 客户端,并使用服务主机名进行连接。
kubectl run my -it --rm --restart=Never --pod-running-timeout=3m \
--image mariadb -- mariadb -h mariadb-0.mariadb-service -P 3306 -u root -p
这将在集群中创建一个运行 MariaDB 客户端的 Pod,配置为连接到我们 StatefulSet 中的主 Pod。它是临时的,一旦您退出交互会话将被删除,这使得它成为从集群内部进行一次性调试的便捷方式。当 Pod 准备好后,输入在列表 9.6 中找到的 MARIADB_ROOT_PASSWORD 环境变量中的数据库密码,您现在可以执行数据库命令。当您完成时,输入 exit 以结束会话。
Redis
另一个我们可以使用的例子是 Redis。Redis 是 Kubernetes 中非常流行的工作负载部署,具有许多不同的可能用途,通常包括缓存和其他实时数据存储与检索需求。在这个例子中,让我们想象一个缓存用例,其中数据并不是特别重要。您仍然希望将数据持久化到磁盘,以避免在重启时重建缓存,但没有必要进行备份。接下来是一个完全可用的单 Pod Redis 设置,适用于此类应用程序。
要配置 Redis,首先让我们定义我们的配置文件,我们可以将其作为卷挂载到容器中。
清单 9.7 Chapter09/9.2.1_StatefulSet_Redis/redis-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-config
data:
redis.conf: |
bind 0.0.0.0 ?
port 6379 ?
protected-mode no ?
appendonly yes ?
dir /redis/data ?
? 绑定到所有接口,以便其他 Pod 可以连接
? 使用的端口
禁用保护模式,以便集群中的其他 Pod 可以无密码连接
? 启用追加日志将数据持久化到磁盘
? 指定数据目录
需要注意的是,在此配置中,我们将 Redis 状态持久化到 /redis/data 目录,因此如果 Pod 被重新创建,可以重新加载该状态。接下来,我们需要配置将该目录挂载的卷。
此示例未为 Redis 配置身份验证,这意味着集群中的每个 Pod 都将具有读/写访问权限。如果您将此示例用于生产集群,请考虑您希望如何配置集群。
现在让我们继续创建一个 StatefulSet,它将引用此配置并将 /redis/data 目录挂载为 PersistentVolume。
清单 9.8 Chapter09/9.2.1_StatefulSet_Redis/redis-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
selector:
matchLabels:
app: redis-sts
serviceName: redis-service
replicas: 1 ?
template:
metadata:
labels:
app: redis-sts
spec:
terminationGracePeriodSeconds: 10
containers:
- name: redis-container
image: redis:latest
command: ["redis-server"]
args: ["/redis/conf/redis.conf"]
volumeMounts:
- name: redis-configmap-volume ?
mountPath: /redis/conf/ ?
- name: redis-pvc ?
mountPath: /redis/data ?
resources:
requests:
cpu: 1
memory: 4Gi
volumes:
- name: redis-configmap-volume
configMap: ?
name: redis-config ?
volumeClaimTemplates:
- metadata:
name: redis-pvc
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: redis-service
spec:
ports:
- port: 6379
clusterIP: None
selector:
app: redis-sts
? 1 个副本,因为这是一个单角色的 StatefulSet
将清单 9.7 中的配置文件挂载到容器中的一个目录。
? 在 volumeClaimTemplates 部分定义的 Redis 数据卷挂载
? 引用在清单 9.7 中定义的 ConfigMap 对象
与 MariaDB StatefulSet 相比,它的设置类似,除了应用程序特定的差异,例如使用的不同端口、容器镜像以及将配置映射挂载到 /redis/conf. 中
在创建了第 09 章/9.2.1_StatefulSet_Redis 中的资源后,为了连接到 Redis 并验证其工作状态,您可以将端口转发到本地机器,并使用 redis-cli 工具进行连接,如下所示:
$ kubectl port-forward pod/redis-0 6379:6379
Forwarding from 127.0.0.1:6379 -> 6379
$ docker run --net=host -it --rm redis redis-cli
27.0.0.1:6379> INFO
# Server
redis_version:7.2.1
127.0.0.1:6379> exit
所以这是一个单副本 StatefulSet 的两个示例。即使只有一个副本,对于这样的工作负载来说,使用 StatefulSet 比使用 Deployment 更方便,因为 Kubernetes 可以自动处理 PersistentVolumeClaim 的创建。
如果您删除 StatefulSet 对象,PersistentVolumeClaim 对象将保留。如果您随后重新创建 StatefulSet,它将重新附加到相同的 PersistentVolumeClaim,因此不会丢失数据。不过,删除 PersistentVolumeClaim 对象本身可能会删除底层数据,这取决于存储类的配置。如果您关心存储的数据(例如,不仅仅是可以重新创建的缓存),请务必按照第 9.1.3 节中的步骤设置一个 StorageClass,以便在出于任何原因删除 PersistentVolumeClaim 对象时保留底层云资源。
如果我们增加这个 StatefulSet 的副本数量,它会为我们提供具有自己存储卷的新 Pods,但这并不自动意味着它们会相互通信。对于这里定义的 Redis StatefulSet,创建更多副本只会给我们更多独立的 Redis 实例。下一部分将详细介绍如何在单个 StatefulSet 内设置多 Pod 架构,其中每个独特的 Pod 根据 Pod 的序号进行不同的配置,并相互连接。
9.2.2 部署多角色 StatefulSet
StatefulSet 的真正力量在于您需要多个 Pod 时。当设计一个将使用 StatefulSet 的应用程序时,StatefulSet 中的 Pod 副本需要相互了解并作为有状态应用程序设计的一部分进行通信。这就是使用 StatefulSet 类型的好处,因为每个 Pod 在一个称为序号的集合中获得一个唯一标识符。您可以利用这种唯一性和保证的顺序为集合中不同的唯一 Pod 分配不同的角色,并通过更新甚至删除和重新创建来关联相同的持久磁盘。
在这个例子中,我们将从上一节的单个 Pod Redis StatefulSet 开始,通过引入副本角色将其转换为三 Pod 设置。Redis 使用领导者/跟随者复制策略,由一个主 Pod(在 9.2.1 节中,这是唯一的 Pod)和具有副本角色的其他 Pods 组成(不要与 Kubernetes 的“副本”混淆,后者指的是 StatefulSet 或 Deployment 中的所有 Pods)。
基于上一节的示例,我们将为主 Pod 保持相同的 Redis 配置,并为副本添加一个额外的配置文件,其中包含对主 Pod 地址的引用。列表 9.9 是定义这两个配置文件的 ConfigMap。
清单 9.9 Chapter09/9.2.2_StatefulSet_Redis_Multi/redis-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-role-config
data:
primary.conf: | ?
bind 0.0.0.0
port 6379
protected-mode no
appendonly yes
dir /redis/data
replica.conf: | ?
replicaof redis-0.redis-service 6379 ?
bind 0.0.0.0
port 6379
protected-mode no
appendonly yes
dir /redis/data
配置映射中的第一个文件,用于配置主要角色
第二个文件在配置映射中,用于配置副本角色
? 配置 Redis 副本以通过其名称引用主 Pod
ConfigMaps 只是一个方便的方式,让我们为两个角色定义两个配置文件。我们也可以使用 Redis 基础镜像构建自己的容器,并将这两个文件放入其中。但由于这是我们所需的唯一自定义,因此在这里定义它们并将其挂载到我们的容器中更简单。
接下来,我们将更新 StatefulSet 工作负载,以使用一个 init 容器(即在 Pod 初始化期间运行的容器)来设置每个 Pod 副本的角色。运行在这个 init 容器中的脚本查找正在初始化的 Pod 的序号,以确定其角色,并复制该角色的相关配置——请记住,StatefulSets 的一个特殊功能是每个 Pod 被分配一个唯一的序号。我们可以使用 0 的序号值来指定主角色,同时将其余 Pod 分配给副本角色。
该技术可以应用于多种不同的有状态工作负载,其中包含多个角色。如果您在寻找 MariaDB,Kubernetes 文档中提供了一个很好的指南。
清单 9.10 Chapter09/9.2.2_StatefulSet_Redis_Multi/redis-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
selector:
matchLabels:
app: redis-sts
serviceName: redis-service
replicas: 3 ?
template:
metadata:
labels:
app: redis-sts
spec:
terminationGracePeriodSeconds: 10
initContainers:
- name: init-redis ?
image: redis:latest
command: ?
- bash ?
- "-c" ?
- |
set -ex
# Generate server-id from Pod ordinal index.
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo "ordinal ${ordinal}"
# Copy appropriate config files from config-map to emptyDir.
mkdir -p /redis/conf/
if [[ $ordinal -eq 0 ]]; then
cp /mnt/redis-configmap/primary.conf /redis/conf/redis.conf
else
cp /mnt/redis-configmap/replica.conf /redis/conf/redis.conf
fi
cat /redis/conf/redis.conf
volumeMounts:
- name: redis-config-volume ?
mountPath: /redis/conf/ ?
- name: redis-configmap-volume ?
mountPath: /mnt/redis-configmap ?
containers:
- name: redis-container ?
image: redis:latest
command: ["redis-server"]
args: ["/redis/conf/redis.conf"]
volumeMounts:
- name: redis-config-volume ?
mountPath: /redis/conf/ ?
- name: redis-pvc ?
mountPath: /redis/data ?
resources:
requests:
cpu: 1
memory: 4Gi
volumes:
- name: redis-configmap-volume
configMap: ?
name: redis-role-config ?
- name: redis-config-volume ?
emptyDir: {} ?
volumeClaimTemplates:
- metadata:
name: redis-pvc
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: redis-service
spec:
ports:
- port: 6379
clusterIP: None
selector:
app: redis-sts
? 3 个副本用于多角色 StatefulSet
? 在启动时运行一次的初始化容器,用于将配置文件从 ConfigMap 挂载复制到 emptyDir 挂载
运行以下脚本。
? emptyDir 挂载,与主容器共享
? 使用列表 9.9 中的两个文件挂载 ConfigMap
? 主要的 Redis 容器
? emptyDir 挂载,与初始化容器共享
? 在 volumeClaimTemplates 部分定义的 Redis 数据卷挂载
? 引用在清单 9.9 中定义的 ConfigMap 对象
? 定义 emptyDir 卷
这里有一些内容需要解析,所以让我们仔细看看。我们单实例 Redis StatefulSet 的主要区别在于存在一个 init 容器。这个 init 容器,顾名思义,在 Pod 的初始化阶段运行。它挂载了两个卷,一个是 ConfigMap,另一个是新的卷 redis-config-volume :
volumeMounts:
- name: redis-config-volume
mountPath: /redis/conf/
- name: redis-configmap-volume
mountPath: /mnt/redis-configmap
该 redis-config-volume 属于 emptyDir 类型,允许在容器之间共享数据,但如果 Pod 被重新调度,则不会持久化数据(与 PersistentVolume 不同)。我们仅使用该 emptyDir 卷来存储配置的副本,这非常理想。 init 容器运行 YAML 中包含的 bash 脚本:
command:
- bash
- "-c"
- |
set -ex
# Generate server-id from Pod ordinal index.
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
# Copy appropriate config files from config-map to emptyDir.
mkdir -p /redis/conf/
if [[ $ordinal -eq 0 ]]; then
cp /mnt/redis-configmap/primary.conf /redis/conf/redis.conf
else
cp /mnt/redis-configmap/replica.conf /redis/conf/redis.conf
fi
该脚本将根据 Pod 的序号,从 ConfigMap 卷(挂载在 /mnt/redis-configmap )复制两种不同配置中的一种到这个共享的 emptyDir 卷(挂载在 /redis/conf )。也就是说,如果 Pod 是 redis-0 ,则复制 primary.conf 文件;对于其余的,复制 replica.conf 。
主容器然后在 /redis/conf 挂载相同的 redis-config-volume emptyDir 卷。当 Redis 进程启动时,它将使用位于 /redis/conf/redis.conf 的任何配置。
要尝试它,您可以使用端口转发/本地客户端组合连接到主 Pod,或通过创建临时 Pod,如前面部分所述。我们还可以直接使用 exec 连接,以快速写入一些数据,如下所示:
$ kubectl exec -it redis-0 -- redis-cli
127.0.0.1:6379> SET capital:australia "Canberra"
OK
127.0.0.1:6379> exit
然后连接到一个副本并读取它:
$ kubectl exec -it redis-1 -- redis-cli
Defaulted container "redis-container" out of: redis-container, init-redis (init)
127.0.0.1:6379> GET capital:australia
"Canberra"
副本是只读的,因此您将无法直接写入数据:
127.0.0.1:6379> SET capital:usa "Washington"
(error) READONLY You can't write against a read only replica.
127.0.0.1:6379> exit