-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
qingtong
committed
May 15, 2024
1 parent
9c5199e
commit 5bb2bbe
Showing
2 changed files
with
288 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
--- | ||
title: "从 Docker 到云原生(03)— StatefulSet" | ||
date: "2024-05-20" | ||
cta: "kubernetes, cloud-native" | ||
spoiler: "深入理解 statefulSet" | ||
--- | ||
|
||
[上一节](/blog/from-docker-to-cloud-native-02/) 主要聊到了 Kubernetes 里的核心组件,包括 Pod, Deployment, Service 和 Ingress。其中 Deployment 的设计是对 Pod 的抽象,以及基于 Deployment 能实现“水平扩缩容”的应用编排能力。 | ||
|
||
但 Deployment 在面对有状态类(Stateful)应用就有心无力了。问题的根源出自于 Deployment 对应用做了一个简单化的假设:应用的不同 Pod 副本是无差别一致的,Pod 之间没有顺序之分,也无所谓在哪台宿主机上运行。Deployment 可以基于 Pod 模板(Pod template)创新副本,也可以按需结束任意一个 Pod。 | ||
|
||
但实际场景中,并非所有应用都满足这样的要求,尤其是分布式应用,实例之间往往有依赖关系,比如主从关系,主备关系;还有存储类应用,它的多个实例往往会在本地持久化数据;一旦实例结束,即使重建以后,实例和数据的对应关系已经丢失,从而导致应用失败。 | ||
|
||
所以,这种实例之间存在不对等关系,以及实例对外部数据有依赖关系的应用,称为有状态应用(Stateful App)。 | ||
|
||
Kubernetes 使用 StatefulSet 组件实现了对有状态应用的编排。 | ||
|
||
## StatefulSet | ||
|
||
StatefulSet 的设计是把应用抽象为了两种情况: | ||
|
||
1. 拓扑(topology)状态。应用的多个实例之间不是完全对等的,应用实例必须按照顺序依次启动。比如应用的主节点 A 必须先于节点 B 启动。而如果删除 A 和 B 两个 Pod,它们再次被创建也必须严格遵循这个顺序运行。并且,新建的 Pod 标识必须和原来一样,这样原先的访问者仍然能用同样的方法继续访问 Pod。 | ||
2. 存储状态。应用的多个实例分别绑定不同的存储数据。即使 Pod 被重新创建过,对于应用实例来说,前后访问到的数据还是同一份。这种情况最典型的例子就是一个数据库应用的多个存储实例。 | ||
|
||
StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时,能够为新 Pod 恢复这些状态。 | ||
|
||
在讲 StatefulSet 的核心原理时,要先来介绍一下[上一节](/blog/from-docker-to-cloud-native-02/) 提到过的 Headerless Service。 | ||
|
||
## Headless Service | ||
|
||
Service 是将 Pod 暴露给外部访问的一种方式,比如,一个 Deployment 实现了 3 个 Pod 副本,那就可以定义一个 Service,用户只要能访问到 Service,就能访问到对应的 Pod。 | ||
|
||
在有状态应用(Stateful App)中,我们需要保证 Service 能够访问到指定的 Pod,那就需要分配 Pod 一个唯一标识,即使 Pod 被删除重建后依然保持同一个唯一标识,在 Kubernetes 里面就通过 Headless Service 组件来实现。 | ||
|
||
以下是一个 Headless Service YAML 文件: | ||
|
||
```yaml | ||
apiVersion: v1 | ||
kind: Service | ||
metadata: | ||
name: my-service | ||
spec: | ||
# Headless Servive | ||
clusterIP: None | ||
selector: | ||
app: my-app | ||
ports: | ||
- protocol: TCP | ||
port: 80 | ||
targetPort: 9376 | ||
``` | ||
Headless Service 和标准 Service 最大的区别在于 `spec.clusterIP` 设置为 `None`,即这个 Service 没有一个 VIP 作为“头”。该 Service 创建以后不会被分配 VIP,而是会以 DNS 记录的方式暴露出它所代理的 Pod。 | ||
|
||
DNS(Domain Name System,域名系统)在 Kubernetes 里面是用于服务发现和负载均衡的一种实现,它允许 Pod 和 Service 使用节点名(而不是 IP 地址)进行通信,简化了集群内部服务之间的相互访问。 | ||
|
||
Service 在创建时,Kubernetes 会分配一个 DNS 名称。例如,一个名为 `my-service` 的 Headless Service 在默认命名空间中,可以通过`my-service.default.svc.cluster.local` 这样的方式访问。同时该 Service 为每个 Pod 也会创建 DNS 记录,如 `pod-name.my-service.default.svc.cluster.local`,这个 DNS 地址会直接解析到该 Pod 的 IP 地址。 | ||
|
||
这个 DNS 记录,正式 Kubernetes 为 Pod 分配的唯一可解析身份(resolvable identity),有了这个可解析身份,只要知道了 Pod 名称及其对应的 Service 名称,就可以非常确定通过这条 DNS 记录访问到 Pod 的 IP 地址。 | ||
|
||
### StatefulSet | ||
|
||
基于下面 YAML 我们创建一个 StatefulSet: | ||
```yaml | ||
apiVersion: v1 | ||
kind: Service | ||
metadata: | ||
name: nginx | ||
labels: | ||
app: nginx | ||
spec: | ||
ports: | ||
- port: 80 | ||
name: web | ||
# 定义 Headless Service | ||
clusterIP: None | ||
selector: | ||
app: nginx | ||
--- | ||
apiVersion: apps/v1 | ||
kind: StatefulSet | ||
metadata: | ||
name: web | ||
spec: | ||
selector: | ||
matchLabels: | ||
app: nginx # has to match .spec.template.metadata.labels | ||
# 使用上面定义的 Headless Service | ||
serviceName: "nginx" | ||
replicas: 3 # by default is 1 | ||
minReadySeconds: 10 # by default is 0 | ||
template: | ||
metadata: | ||
labels: | ||
app: nginx # has to match .spec.selector.matchLabels | ||
spec: | ||
terminationGracePeriodSeconds: 10 | ||
containers: | ||
- name: nginx | ||
image: registry.k8s.io/nginx-slim:0.8 | ||
ports: | ||
- containerPort: 80 | ||
name: web | ||
# 挂载持久卷 | ||
volumeClaimTemplates: | ||
- metadata: | ||
# 对应 pvc 资源的前缀名称 | ||
name: www | ||
spec: | ||
accessModes: [ "ReadWriteOnce" ] | ||
storageClassName: "my-storage-class" | ||
resources: | ||
requests: | ||
storage: 1Gi | ||
``` | ||
相比于 deployment,StatefulSet 区别在于多了一个`spec.serviceName` 字段,该字段作用就是告诉 StatefulSet controller,在执行控制循环是使用 `nginx` 这个 `Headless Service`。 | ||
|
||
通过 `kubectl` 创建 service 和 statefulSet 之后,我们可以看到两个对象: | ||
|
||
```bash | ||
$ kubectl apply -f statefulSet.yaml | ||
service/nginx created | ||
statefulset.apps/web created | ||
$ kubectl get svc nginx | ||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE | ||
nginx ClusterIP None <none> 80/TCP 18s | ||
$ kubectl get statefulSet web | ||
NAME READY AGE | ||
web 0/3 33s | ||
$ kubectl get pod -l name=nginx | ||
NAME READY STATUS RESTARTS AGE | ||
web-0 1/1 Running 0 2m | ||
web-1 1/1 Running 0 20s | ||
``` | ||
|
||
可以看到,StatefulSet 给所有 Pod 命名规则是`<statefulSet-name>-[0]`, `<statefulSet-name>-[1]` 这样的编号递增规则,编号与 Pod 实例一一对应。 | ||
|
||
更重要的是,Pod 的创建也是严格按照编号顺序进行的。比如,在 web-0 进入 Running 之前,web-1 会一直处于 Pending 状态。 | ||
|
||
当两个 Pod 都进入 Running 状态后,就可以查看各自唯一的网络身份了。 | ||
|
||
我们可以尝试以 DNS 方式访问这个 Headless Service: | ||
|
||
```bash | ||
# 启动一个临时 Pod,--rm 代表退出就会被删除 | ||
$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh | ||
# 通过 nslookup 查看 service 对应的网络地址 | ||
$ nslookup nginx.default.svc.cluster.local | ||
Server: 10.96.0.10 | ||
Address: 10.96.0.10:53 | ||
Name: nginx.default.svc.cluster.local | ||
Address: 10.244.0.8 | ||
Name: nginx.default.svc.cluster.local | ||
Address: 10.244.0.6 | ||
Name: nginx.default.svc.cluster.local | ||
Address: 10.244.0.7 | ||
# 查看 pod 对应的网络地址 | ||
$ nslookup web-0.nginx.default.svc.cluster.local | ||
Server: 10.96.0.10 | ||
Address: 10.96.0.10:53 | ||
Name: web-0.nginx.default.svc.cluster.local | ||
Address: 10.244.0.6 | ||
``` | ||
|
||
可以看到,DNS 查询 Service 会返回代理的两个 EndPoint(POD) 对应的 IP,这个 IP 跟通过 DNS 查询 pod 返回是保持一致的。 | ||
|
||
需要注意的是,尽快 DNS 记录是保持不变的,但它解析到的 IP 地址并不是固定不变,这就意味着,对于有状态应用实例的访问,只能通过 DNS 记录方式访问,而绝不应该直接访问 pod 的 IP 地址。 | ||
|
||
### PV 和 PVC | ||
|
||
在聊 StatefulSet 如何解决分布式存储状态的一致性前,要先聊另外一个概念 `PV` 和 `PVC`。 | ||
|
||
PV(PersistentVolume,持久卷)是集群中的一块存储,可以由 admin 事先生成。持久卷是集群资源,就像节点也是集群资源一样,它们拥有独立于 Pod 的生命周期。 | ||
|
||
```yaml | ||
apiVersion: v1 | ||
kind: PersistentVolume | ||
metadata: | ||
name: pv0003 | ||
spec: | ||
capacity: | ||
storage: 5Gi | ||
volumeMode: Filesystem | ||
accessModes: | ||
- ReadWriteOnce | ||
persistentVolumeReclaimPolicy: Recycle | ||
storageClassName: slow | ||
mountOptions: | ||
- hard | ||
- nfsvers=4.1 | ||
nfs: | ||
path: /tmp | ||
server: 172.17.0.2 | ||
``` | ||
作为应用开发者,可能对分布式存储项目(Ceph/HDFS/GFS)不甚了解,自然无法编写对应的 Volume 配置文件,而且有暴露公司基础设施敏感信息(秘钥、管理员密码)等的风险。所以 Kubernetes 又引入了 PVC(PersistentVolumeClaim,持久卷申领)API 对象。 | ||
|
||
定义一个 PVC,声明想要的 Volumn 属性: | ||
|
||
```yaml | ||
kind: PersistentVolumeClaim | ||
apiVersion: v1 | ||
metadata: | ||
# 定义 claim 名称 | ||
name: pv-claim | ||
spec: | ||
# 可读写权限 | ||
accessModes: | ||
- ReadWriteOnce | ||
# 资源占用 | ||
resources: | ||
requests: | ||
storage: 1Gi | ||
--- | ||
apiVersion: v1 | ||
kind: Pod | ||
metadata: | ||
name: pv-pod | ||
spec: | ||
containers: | ||
- name: pv-container | ||
image: nginx | ||
ports: | ||
- containerPort: 80 | ||
name: "http-server" | ||
volumeMounts: | ||
- mountPath: "/usr/share/nginx/html" | ||
name: pv-storage | ||
volumes: | ||
- name: pv-storage | ||
# 申领 volumn 名称 | ||
persistentVolumeClaim: | ||
claimName: pv-claim | ||
``` | ||
PVC 表达的是Pod对存储的请求。概念上与 Pod 类似。 Pod 会耗用节点资源,而 PVC 申领会耗用 PV 资源。有了 PVC 后,在需要使用持久卷的 Pod 定义里只需要声明使用这个 PVC 即可,这为使用者隐去了很多关于存储的信息。而具体存储的信息就交给 PV 实现即可。 | ||
|
||
PVC 和 PV 的设计思想可以参考面向对象领域的接口(interface)和实现(implement)。开发者只需知道并会使用接口,即 PVC,具体的接口绑定实现则由运维人员负责,即 PV。 | ||
|
||
### StatefulSet 的 PVC 模板 | ||
|
||
在 `StatefulSet` yaml 配置中,声明了一个 `volumeClaimTemplates` 字段: | ||
|
||
```yaml | ||
volumeClaimTemplates: | ||
- metadata: | ||
name: www | ||
spec: | ||
accessModes: [ "ReadWriteOnce" ] | ||
storageClassName: "my-storage-class" | ||
resources: | ||
requests: | ||
storage: 1Gi | ||
``` | ||
`volumeClaimTemplates` 和 Deployment 里的 Pod template 类似。也就是说,凡是被 StatefulSet 管理的 Pod,都会声明一个对应的 PVC,而 PVC 的定义,就来自于 volumeClaimTemplates 这个模板字段。更重要的是,这个 PVC 的名字会被分配一个与 Pod 完全一致的编号,相当于 PVC 就与 Pod 绑定了。 | ||
|
||
StatefulSet创建的这些PVC,都以“PVC名-StatefulSet名-序号”这个格式命名的。 | ||
|
||
对于上面这个StatefulSet来说,它创建出来的 Pod 和 PVC 的名称如下: | ||
|
||
``` | ||
Pod: web-0, web-1 | ||
PVC: www-web-0, www-web-1 | ||
``` | ||
StatefulSet 架构如下: | ||
![image](./images/statefulset-pvc-pv.png) | ||
假设现在 Pod-web-0 被删除,对应的 PVC 和 PV 并不会被删除,volumn 写入的数据依然存储在远程存储服务上。此时 StatefulSet 控制器会重新生成一个名为 Pod-web-0 的 Pod 对象,用来纠正这种不一致的情况。 | ||
新生成的 Pod 对象,由于 volumeClaimTemplates 的存在,它声明使用的 PVC 名称还是 www-web-0,由于 PVC 的生命周期是独立于 Pod,这样新 Pod 就接管了以前旧 Pod 留下的数据。通过这种方式,Kubernetes 的 StatefulSet 就完成了对应用存储状态的管理。 | ||
总结一下: | ||
1. 首先 StatefulSet 控制管理的是 Pod。每个 Pod 携带了固定的编号,且不随 Pod 调度而变化。 | ||
2. Kubernetes 通过 Headless Service 为这些有编号的 Pod,在 DNS 服务器中生成带有相同编号的 DNS 记录。只要 StatefulSet 保证 Pod 名字编号不变,那么 Service 类似于 `pod-0.my-service.default.svc.cluster.local` 的记录就不会变,而这条记录对应的的 IP 地址,会随着 Pod 的删除和重建而自动更新。 | ||
3. 最后 StatefulSet 为每个 Pod 分配并创建一个相同编号的 PVC,这样 Kubernetes 可以通过 PVC 绑定对应的 PV,从而保证每个 Pod 都有一个独立的 Volumn。 | ||
**参考** | ||
1. [深入理解StatefulSet](https://juejin.cn/post/6962417834425057310?searchId=20240514210028836D79D9BC05892E7FF2) |