k8s 之 StatefulSet

深入理解StatefulSet(一):拓扑状态

k8s有状态与无状态的区别

无状态服务:deployment

Deployment被设计用来管理无状态服务的pod,每个pod完全一致.什么意思呢?

无状态服务内的多个Pod创建的顺序是没有顺序的. 无状态服务内的多个Pod的名称是随机的.pod被重新启动调度后,它的名称与IP都会发生变化. 无状态服务内的多个Pod背后是共享存储的.

有状态服务:StatefulSet

Deployment组件是为无状态服务而设计的,其中的Pod名称,主机名,存储都是随机,不稳定的,并且Pod的创建与销毁也是无序的.这个设计决定了无状态服务并 不适合数据库领域的应用.

而Stateful管理有状态的应用,它的Pod有如下特征:

唯一性: 每个Pod会被分配一个唯一序号. 顺序性: Pod启动,更新,销毁是按顺序进行. 稳定的网络标识: Pod主机名,DNS地址不会随着Pod被重新调度而发生变化. 稳定的持久化存储: Pod被重新调度后,仍然能挂载原有的PV,从而保证了数据的完整性和一致性.

总结: 本文主要介绍了无状态和有状态服务在K8S中的典型应用场景.

通过对Deployment部署无状态服务所遇到问题的分析,引出了Stateful新的部署组件.它是通过支持Pod一些特性(e.g. 名称唯一性,稳定的网络标识, 稳定的持久化存储等)来实现在K8S中部署运维有状态服务.

牢记: Stateful有状态服务,每个Pod有独立的PVC/PV存储组件

StatefulSet 的工作原理之前,必须先为你讲解一个 Kubernetes 项目中非常实用的概念:Headless Service。

Service 是 Kubernetes 项目中用来将 一组 Pod 暴露给外界访问的一种机制。

比如,一个 Deployment 有 3 个 Pod,那么我就可以 定义一个 Service。然后,用户只要能访问到这个 Service,它就能访问到某个具体的 Pod。

那么,这个 Service 又是如何被访问的呢?

第一种方式:通过 VIP(Virtual IP,即:虚拟 IP),如:当访问 10.0.23.1 这个 Service 的 IP 地址时(VIP),它会把请求转发到该 Service 所代理的某一个 Pod 上。

第二种方式: 以service 的 DNS 方式,但可分为2种处理方法:

(1)是 Normal Service。这种情况下,你访问"my-svc.mynamespace.svc.cluster.local"解析到的,正是 my-svc 这个 Service 的 VIP,后面的流程就跟 VIP 方式一致了。

(2)正是 Headless Service,当你访问"my-svc.mynamespace.svc.cluster.local"解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。

区别在于,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录 的方式解析出被代理 Pod 的 IP 地址。

标准的 Headless Service 对应的 YAML 文件

yaml 复制代码
apiVersion: v1
Kind: Service
metadata:
  name: nginx
  lables:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx  

可以观察看到 ClusterIP 字段的值为 None,也就是所谓的 "无头服务" 没有VIP作为头。

当这个 Service 被创建出来后并不会被分配一个 VIP,而是会以 DNS 记录的方式暴露出它所代理的Pod。

是如何给一组 Pod 暴露端口给外界访问的呢?在 YAML 你是观察到有一个 Selector 字段,他就是使用 Label Selector 机制选择出所有带了 app=nginx 标签的 Pod,都会被这个 Service 代理起来,这样可以做到负载均衡Pod的访问了。

StatefulSet 又是如何使用这个 DNS 记录来维持 Pod 的拓扑状态的呢?

示例编写一个 StatefulSet 的 YAML 文件,如下所示:

yaml 复制代码
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: nginx:1.9.1
        ports:
        - containerPort: 80
          name: web

该 YAML 文件和平时我们写的 Deployment 都差不多,唯一区别就是多一个 ServiceName=nginx 字段。

这个字段的作用,告诉 StatefulSet 控制器,在执行控制循环(Control Loop)时,请使用 nginx 这个 Headless Service 来保证 Pod 的 "可解析身份"

创建完后可以快速实时查看stateful创建的状态:kubectl get pods -w -l app=nginx

试用 nslookup 命令,解析一下 Pod 对应的 Headless Service:

nslookup web-0.nginx

总结:

StatefulSet 控制器作用:使用Pod模版创建 Pod时对他们进行编号,并且按照顺序逐一完成创建工作。StatefulSet 控制循环 发现 Pod的 "实际状态" 与 "期望状态" 不一致,则需要新建或删除 Pod 进行 "调谐" 时会严格按照这些Pod编号的顺序逐一完成这波操作。

深入理解StatefulSet(二):存储状态

在前面的 Stateful 讲解了它是如何保证应用实例的拓扑状态,在 Pod 删除和创建的过程中保持稳定。

今天讲解 StatefulSet 对存储状态的管理机制。

在 Pod 中有个 Volume (卷),想要容器挂载到外面地方三存储或本地机器,只需要在Pod里面加上spec.volumes 字段即可,如 Volume 类型 hostPath就挂在本地机器。

有一个问题,对于开发人员来说,如果你并不知道有哪些 Volume 类型可以用,要怎么办呢?作为开发人员对持久存储项目如Ceph、GlusterFS 都一窍不通,让开发人员来写已经超出了知识储备了

后来 kubernetes 项目引入了一组叫 "Persistent Volume Claim" 和 "Persistent Volume" 的 API 对象,降低用户声明和使用持久 Volume 的门槛。

假设我现在是开发人员,现在想要一个 Volume ,需要两步即可。
第一步:申请。定义一个PVC声明想要的 Volume的属性:

yaml 复制代码
apiVersion: v1
Kind: PersistentVolumeClaim
metadata:
  name: pv-claim
spec:
  accessModes:
  - ReadWirteOnce
  resources:
    requests:
      storage: 1Gi

开发人员定义了一个 PVC,描述只需要的属性定义,如:storage: 1Gi 表示想要 Volume 大小至少为 1GB;accessModes: ReadWirteOnce 表示这个 Volume的挂载方式是可读写,且只能被挂载在一个节点而非多个节点共享。

第二步:在应用的 Pod 中,声明刚才你定义的 PVC 即可使用。

yaml 复制代码
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-storeage
volumes:
  - name: pv-storage
    persistentVolumeClaim:
      claimName: pv-claim

以上就是申请好一个 1GB 大小的 Volume了,不需要关系 Volume 的类型,然后创建这个 PVC 对象,Kubernetes 就会自动为它绑定一个符合条件的 Volume。

可是,这些符合条件的 Volume 又是从哪里来的呢?这个需要运维人员去维护 PV 对象了。

Kubernetes 中 PVC 和 PV 的设计,实际上类似于"接口"和"实现"的思想。开发者 只要知道并会使用"接口",即:PVC;而运维人员则负责给"接口"绑定具体的实现,即: PV。

而 PVC、PV 的设计,也使得 StatefulSet 对存储状态的管理成为了可能。回顾:

yaml 复制代码
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: nginx:1.9.1
        ports:
        - containerPort: 80
          name: web
      volumeMounts:
      - name: www
        mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
    name: www
  spec:
    accessModel:
    - ReadWriteOnce
    resources:
      requests:
        storage: 1Gi

volumeClaimTemplates 字段,从名字就可以 看出来,它跟 Deployment 里 Pod 模板(PodTemplate)的作用类似。

也就是说,凡是被这 个 StatefulSet 管理的 Pod,都会声明一个对应的 PVC;而这个 PVC 的定义,就来自于 volumeClaimTemplates 这个模板字段。更重要的是,这个 PVC 的名字,会被分配一个与这个 Pod 完全一致的编号。

这个自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这就意味着这个 Pod 可以 挂载并使用这个 PV 了。

如果你使用 kubectl delete 命令删除这两个 Pod,这些 Volume 里的文件会不会丢失呢?

前面介绍过,在被删除之后这两个 Pod 会被按照编号顺序被重启创建出来。如果你在创建新的容器通过本地访问去访问,会发现请求依然会返回,说明原先与名叫web-0的Pod绑定PV,Pod被重新创建后,依然同新的名字web-0的Pod绑定了在一起。

这是怎么做到的呢?

分析:当你把一个 Pod,比如 web-0,删除之后,这个 Pod 对应的 PVC 和 PV,并不会被删除,而这个 Volume 里已经写入的数据,也依然会保存在远程存储服务里(比如,我们在这个 例子里用到的 Ceph 服务器)。

当删除后,StatefulSet 控制器发现,一个名叫 web-0 的 Pod 消失了。所以,控制器就会重新创建 一个新的、名字还是叫作 web-0 的 Pod 来,"纠正"这个不一致的情况。

需要注意的是,在这个新的 Pod 对象的定义里,它声明使用的 PVC 的名字,还是叫作:wwwweb-0。这个 PVC 的定义,还是来自于 PVC 模板(volumeClaimTemplates),这是 StatefulSet 创建 Pod 的标准流程。

通过这种方式,Kubernetes 的 StatefulSet 就实现了对应用存储状态的管理。

深入理解StatefulSet(三):有状态应用实践

官网:案例

部署一个 MySQL 集群,如何使用 StatefulSet 将它的集群搭建过程"容器化"

首先,用自然语言来描述一下我们想要部署的"有状态应用"。

  1. 是一个"主从复制"(Maser-Slave Replication)的 MySQL 集群;
  2. 有 1 个主节点(Master);
  3. 有多个从节点(Slave);
  4. 从节点需要能水平扩展;
  5. 所有的写操作,只能在主节点上执行;
  6. 读操作可以在所有节点上执行。

在常规环境里,部署这样一个主从模式的 MySQL 集群的主要难点在于:如何让从节点能够拥有 主节点的数据,即:如何配置主(Master)从(Slave)节点的复制与同步。

第一步:通过 XtraBackup 将 Master 节点的数据备份到指定目录。

第二步:配置 Slave 节点。Slave 节点在第一次启动前,需要先把 Master 节点的备份数据,连 同备份信息文件,一起拷贝到自己的数据目录(/var/lib/mysql)下。然后,我们执行这样一句 SQL:

第三步:启动 Slave 节点。在这一步,我们需要执行这样一句 SQL:

第四步:在这个集群中添加更多的 Slave 节点。

将部署 MySQL 集群的流程迁移到 Kubernetes 项目上,需要 能够"容器化"地解决下面的"三座大山":

  1. Master 节点和 Slave 节点需要有不同的配置文件(即:不同的 my.cnf)

    1. Master 节点和 Salve 节点需要能够传输备份信息文件;
  2. 在 Slave 节点第一次启动之前,需要执行一些初始化 SQL 操作;

"第一座大山:Master 节点和 Slave 节点需要有不同的配置文件",很容易处理:我们只需要给主从节点分别准备两份不同的 MySQL 配置文件,然后根据 Pod 的序号(Index)挂载进去即可。

配置文件信息保存在 ConfigMap 里供 Pod 使 用。它的定义如下所示:

yaml 复制代码
apiVersion: v1 
kind: ConfigMap 
metadata:
  name: mysql 
  labels:
    app: mysql
data:
  master.cnf: |
    [mysql]
    log-bin
slave.cnf: |
  [mysql]
  super-read-only
  • master.cnf 开启了 log-bin,即:使用二进制日志文件的方式进行主从复制,这是一个标准 的设置。
  • slave.cnf 的开启了 super-read-only,代表的是从节点会拒绝除了主节点的数据同步操作之 外的所有写操作,即:它对用户是只读的。

接下来,我们需要创建两个 Service 来供 StatefulSet 以及用户使用。这两个 Service 的定义如 下所示:

yaml 复制代码
```yaml
# 为 StatefulSet 成员提供稳定的 DNS 表项的无头服务(Headless Service)
apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
    app.kubernetes.io/name: mysql
spec:
  ports:
  - name: mysql
    port: 3306
  clusterIP: None # Headless Service,作用为 Pod 分配 DNS 记录来固定它的拓扑状态
  selector:
    app: mysql
---
# 用于连接到任一 MySQL 实例执行读操作的客户端服务
# 对于写操作,你必须连接到主服务器:mysql-0.mysql
apiVersion: v1
kind: Service
metadata:
  name: mysql-read
  labels:
    app: mysql
    app.kubernetes.io/name: mysql
    readonly: "true"
spec:
  ports:
  - name: mysql
    port: 3306
  selector:
    app: mysql

两个 Service 都代理了所有携带 app=mysql 标签的 Pod,也就是所有的 MySQL Pod。端口映射都是用 Service 的 3306 端口对应 Pod 的 3306 端口。

第二座大山:Master 节点和 Salve 节点需要能够传输备份文件"的 问题。

相关推荐
syty20204 分钟前
K8s是什么
容器·kubernetes·dubbo
江团1io01 小时前
微服务雪崩问题与系统性防御方案
微服务·云原生·架构
Evan Wang2 小时前
使用Terraform管理阿里云基础设施
阿里云·云原生·terraform
向上的车轮3 小时前
基于go语言的云原生TodoList Demo 项目,验证云原生核心特性
开发语言·云原生·golang
灵犀物润3 小时前
Kubernetes 配置检查与发布安全清单
安全·容器·kubernetes
360智汇云4 小时前
k8s交互桥梁:走进Client-Go
golang·kubernetes·交互
xy_recording4 小时前
Day20 K8S学习
学习·容器·kubernetes
衍余未了5 小时前
k8s 内置的containerd配置阿里云个人镜像地址及认证
java·阿里云·kubernetes
九章云极AladdinEdu5 小时前
Kubernetes设备插件开发实战:实现GPU拓扑感知调度
人工智能·机器学习·云原生·容器·kubernetes·迁移学习·gpu算力
泡沫冰@5 小时前
K8S集群管理(4)
云原生·容器·kubernetes