1、ConfigMap(配置信息存储)

​

​

看图,三台nginx,他们在同一业务、环境下,配置信息也应该一样。某天需要修改nginx配置,假设nginx有300台,难道要一台一台修改吗?这时候引出了配置中心ConfigMap(cm),(如果不能满足需求可以开发自定义配置中心)

首先在每个节点上安装一个agent端,agent端的作用就是监控配置中心的资源是否发生更新动作,如果有agent端就会去配置中心下载最新的配置文件来替换当前的,然后触发nginx重载,使配置生效。如果配置更新了新版本的话比如v3,还可以经agent端指向到新版本上进行更新与重载

cm功能在k8s 1.2版本中引入,许多应用程序会从配置文件、命令行参数或环境变量中读取配置信息。ConfigMap API给我们提供了向容器中注入配置信息的机制,cm可以被用来保存单个属性,也可以用来保存整个配置文件或者JSON二进制等对象

ConfigMap的创建方式

cm的创建方式也分为声明式和命令式,但是由于cm通常变动频繁、需要临时配置调试并且配置内容大多来自外部文件。导致声明式创建方式写yaml清单有点繁琐,所以命令式创建较为常用。

先看下命令式的创建,分为四种,常用的需要学习,非常用作为补充了解,避免看到了不认识

字面值创建(常用)

kubectl create configmap <cmName> --from-literal=name=tom --from-literal=password=pass

#这种方式创建值是一对一对的

​

文件创建(常用)

kubectl create configmap <cmName> --from-file=<fileName>

#这种方式创建,文件名会变成“键”,文件内容会变成“值”,文件内容要一行一行的写

​

目录创建

kubectl create cm <cmName> --from-file=<dirPath>

#此方式创建,目录下的每个文件名作为键,文件内容作为值

​

env文件创建

kubectl create cm <cmName> --from-env-file=<fileName>

#此方式乍一看和从文件创建一样,但是这个方式创建的cm,它是以文件中内容为键值的,而不是文件名为“键”,内容为“值”,仔细看可以看出区别

#所以从以文件为基础创建cm的话,文件内容键值必须以键值对方式,一行一行的写

​


再来看声明式创建,内容和命令式创建没什么不同,就是创建方式需要写yaml清单

基础yaml格式

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm1
data:
  name: jim
  password: pass

多行字符串格式

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm1
data:
  app.conf: |
    database.host=mysql.local
    database.port=3306
    logs.level=INFO
  script.sh: |
    #!/bin/bash
    echo "Hello World"

复杂数据格式

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm1
data:
  config.json: |
    {
      "name": "tom",
      "debug": true,
      "timeout": 30
    }

ConfigMap的使用方式

注入机制是k8s提供的一种将配置信息注入容器应用的机制。它分为三种使用方式,

作为环境变量注入

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm1
data:
  name: jim
  password: pass

---

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm2
data:
  logs_level: INFO
  debug_level: BUG

---

apiVersion: v1
kind: Pod
metadata:
  name: test-cm
spec:
  containers:
  - name: test-cm-container
    image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
    imagePullPolicy: IfNotPresent
    command:
    - sh
    - -c
    - env
    env:  # 定义容器的环境变量
    - name: USERNAME    #变量名
      valueFrom:    #环境变量引用
        configMapKeyRef:      #指定变量从ConfigMap中引用
          name: cm1    #指定引用的ConfigMap名称
          key: name    #这个key后面跟的是键名,告诉我们值要去哪个键找。实际是引用键后面的值
    - name: PASSWORD
      valueFrom:
        configMapKeyRef:
          name: cm1
          key: password
    envFrom:    # 使用ConfigMap将多个键值对作为环境变量传递
    - configMapRef:    
        name: cm2
  restartPolicy: Never  #Pod的重启策略:不会自动重启,不加这个的话容器运行结束会不断的重启。因为pod的重启策略是Always,不管什么原因结束pod他都会重启

创建对象,可以看到两个cm被创建了出来,pod执行完env命令后变成了完成状态,pod日志可以发现容器执行env命令的打印结果,变量都已经添加进去了 

​

​从清单中可以看到pod引用环境变量的方式有两种env和envFrom。两种的区别就是
    env:灵活,精准,将值取出给要赋予的变量,可以看到引用变量的时候键名也可以更改
    envFrom:它的使用场景一般在cm中所有的键都成为环境变量是使用,范围更广,省事儿

两种方式按需使用

作为命令行参数使用        

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm1
data:
  name: tom
  password: pass

---

apiVersion: v1
kind: Pod
metadata:
  name: test-cm
spec:
  containers:
  - name: test-cm-contianer
    image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
    imagePullPolicy: IfNotPresent
    command:
    - sh
    - -c
    - echo $USERNAME $PASSWORD
    env:
    - name: USERNAME
      valueFrom:
        configMapKeyRef: 
          name: cm1
          key: name
    - name: PASSWORD
      valueFrom:
        configMapKeyRef:
          name: cm1
          key: password

​

作为挂载卷使用

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm1
data:
  name: tom
  password: pass

---

apiVersion: v1
kind: Pod
metadata:
  name: test-cm
spec:
  containers:
  - name: test-cm-container
    image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
    imagePullPolicy: IfNotPresent
    command:
    - sh
    - -c
    - sleep 3600
    volumeMounts:    #定义容器中的挂载卷
    - name: cm-volume    #挂载的卷的名称
      mountPath: /etc/config    #卷挂载到容器内的路径
  volumes:    #定义pod中使用的卷
  - name: cm-volume  #挂载卷的名字
    configMap:        #卷的数据来源
      name: cm1
  restartPolicy: Never

启动资源对象后进入pod,cd /etc/config会有两个文件,key就是文件名,values就是文件内容。

这里需要说明一点是,cm作为卷挂载时,本质就是把 data 中的每个键变成一个文件。 而 --from-file 创建cm时,也是把每个文件名变成一个键(key)。它们的设计哲学是相通的:文件名/键名 → 文件,文件内容/键值 → 文件内容,

简单说:Volume 挂载时的"键",就相当于从文件创建时的"文件名"。

​

作为挂载卷的热更新​

​

还是这张图,可以看到cm作为卷挂载的时候,查看了文件的详细信息。发现这两个文件是以软连接的方式存在的,为什么会这样?

在本章开始我就描述了一个场景:后端服务,其配置文件通过配置中心进行动态管理。配置中心的更新方式是直接覆盖目标文件(而非共享读取)。假设服务的配置文件是一个软链接,它指向真实的配置文件。当配置中心更新配置时,它会直接覆盖这个软链接所指向的真实文件。在覆盖操作执行的那一瞬间,如果服务正在读取该配置文件,可能会读取到不完整或损坏的数据

为了解决上述问题,k8s设计了基于软链接原子切换的热更新机制:

初始状态:一个软链接指向第一个配置文件(且只有这一个链接指向它)。

热更新流程:

  1. 配置中心将新配置写入第二个新文件

  2. 将软链接的指向原子性地切换到第二个文件

  3. 删除第一个文件(旧配置)

为什么这样做有效:

  1. 切换链接指向是原子操作,不会出现"读到一半被覆盖"的情况

  2. 正在读取旧配置的服务进程,由于已经打开了旧文件的文件句柄,即使文件被删除也能正常读完

  3. 新请求打开软链接时,会得到新配置文件

好,理论结束,开始实践,将nginx配置导入到一个文件中,创建cm对象

vim nginx.conf
server {
    listen 80 default_server;
    server_name  example.com www.example.com;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

​

​再创建一个deployment控制器,调用cm作为挂载卷

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cm-deploy
spec:
  replicas: 3
  selector:
    matchLabels:
      app: stu
  template:
    metadata:
      labels:
        app: stu
    spec:
      containers:
      - name: cm-nginx
        image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nginx:1.27.0
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - name: config-volume
          mountPath: /etc/nginx/conf.d/
      volumes:
      - name: config-volume
        configMap:
          name: nginx-default

进入pod内查看默认文件,可以看到调用是成功的

​

写个死循环一直cat默认文件

while true ;do cat /etc/nginx/conf.d/nginx.conf ;sleep 2 ;done

kubectl edit​修改cm,将监听端口改为8080试试看,观察pod内部是否能够热更新成功。需要一点时间来进行更新,并不是瞬时操作,他需要自己去安排。比如有1000个pod,配置一旦改变了这1000个pod都需要改变,此刻服务器io压力是巨大的

​

​访问一个pod测试下是可以访问成功的,但是8080访问失败,因为pod内的nginx容器没有重启,新配置未应用,访问的还是80端口。nginx不支持这种监控文件变化重启自身的操作,所以k8s给了一个接口

​

​作为挂载卷的热更新方法

deployment.sepc.template.annotations

注意一点,控制器级别的annotations修改是不会重建pod的,pod级别修改才行

更新cm目前并不会触发相关pod的滚动更新,可以通过kubectl edit修改annotations的方式强制触发滚动更新,如果没有这个字段就加上即可

它的原理是,Deployment 控制器会监控其 Pod 模板(spec.template)的变更。当修改了 spec.template.metadata.annotations 中的任意内容(比如加一个时间戳),Deployment 控制器会认为这是一个新的、期望的状态,于是就会执行一次滚动更新,创建新 Pod,销毁旧 Pod。

新 Pod 启动时,会重新从 ConfigMap 拉取最新的配置,然后 Nginx 容器启动并加载最新配置,但是有点不方便的是,新Pod启动后,IP也会随之改变。每次修改完配置后都要改一下这个参数,感觉不是很好用

​

​修改之后立马pod立马进行了重启,再次访问8080试下

​

​

​作为挂载卷禁止更新

configmap.immutable

k8s给不可改变的cm和secret提供了一个可选配置,可以设置各个secret和cm为禁止修改。对于大量使用cm的集群(至少成千上万各不相同的cm供pod挂载),禁止修改他们的数据有以下好处

  1. 防止意外(或非预期的)更新导致应用程序中断
  2. 通过将cm标记为禁止修改来关闭kube-apiserver对其的监视,从而显著降低kube-apiserver的负载提升集群性能

这个选项是不可逆的,添加这个字段后cm无法做任何修改,删除这个参数也做不到,只能删除cm对象,重新建立

​

2、Secret(敏感信息存储)

secret对象类型以编码的方式用来保存敏感信息,例如密码、OAuth令牌和SSH密钥。将这些信息放在secret中比放在pod的定义或者容器镜像中来说更加安全和灵活,是编码不是加密

secret特性有三个

  1. k8s通过仅仅将secret分发到需要访问secret的pod所在机器节点来保障其安全性
  2. secret只会存储在节点的内存中,永不写入物理存储,这样从节点删除secret时就不需要擦除磁盘数据
  3. 从k8s 1.7开始,etcd会以加密形式存储secret,一定程度保证了secret安全性

演示下编码

[root@k8s-master ~]# echo test |base64 
dGVzdAo=
[root@k8s-master ~]# echo dGVzdAo= |base64 -d
test
[root@k8s-master ~]#

如果解码没有编码过的数据会报错

[root@k8s-master ~]# echo 666 |base64 -d
뭢ase64: 输入无效
[root@k8s-master ~]# 

Secret的类型

创建 Secret 时,你可以使用 Secret 资源的 type 字段,或者与其等价的 kubectl 命令行参数(如果有的话)为其设置类型。 Secret 类型有助于对 Secret 数据进行编程处理。

Kubernetes 提供若干种内置的类型,用于一些常见的使用场景。 针对这些类型,Kubernetes 所执行的合法性检查操作以及对其所实施的限制各不相同

类型作用
Opaque用户定义的任意数据,默认
kubernetes.io/service-account-token服务账号令牌
kubernetes.io/dockercfg~/.dockercfg 文件的序列化形式
kubernetes.io/dockerconfigjson~/.docker/config.json 文件的序列化形式
kubernetes.io/basic-auth用于基本身份认证的凭据
kubernetes.io/ssh-auth用于 SSH 身份认证的凭据
kubernetes.io/tls用于 TLS 客户端或者服务器端的数据
bootstrap.kubernetes.io/token启动引导令牌数据

这里就先将各种类型列出来,作为补充了解,方便后续碰到相关的进行查找,我们本章主要讲的就是opaque默认类型,同时也是最通用和常用的类型

Opaque

先来做一个最简单的演示,来看下secret所达成的效果

#用户和密码先进行编码

[root@k8s-master ~]# echo admin |base64 
YWRtaW4K
[root@k8s-master ~]# echo 12345 |base64 
MTIzNDUK


apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  username: YWRtaW4K
  password: MTIzNDUK

#secret中,values会自动进行解码,所以一旦使用Opaque类型切记要先进行数据编码,不然解码会出错

 可以用kubectl describe查一下,secret的数据段是看不到的,但是cm可以

​

​

​想要获取数据段用-o yaml方式,可以看到是编码形式显示的,然后使用base64解码就得到了想要的结果

​

​

​作为环境变量注入

apiVersion: apps/v1
kind: Deployment
metadata:
  name: opaque-secret-env-deploy
  labels:
    app: opaque-secret-env
spec:
  replicas: 5
  selector:
    matchLabels:
      app: op-se-env-pod
  template:
    metadata:
      labels:
        app: op-se-env-pod
    spec:
      containers:
        - name: myapp-container
          image: nginx:v1
          imagePullPolicy: IfNotPresent
          env:
            - name: TEST_USER
              valueFrom:
                secretKeyRef:
                  name: mysecret
                  key: username
            - name: TEST_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysecret
                  key: password

 随便进入一个pod可以看到容器内环境变量已经添加了代码中引入的变量,并且都已经进行了解码

​

​作为挂载卷使用

apiVersion: apps/v1
kind: Deployment
metadata:
  name: secret-opaque-volume
spec:
  replicas: 3
  selector:
    matchLabels:
      app: stu
  template:
    metadata:
      labels:
        app: stu
    spec:
      containers:
      - name: opaque-container
        image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
        imagePullPolicy: IfNotPresent
        command:
        - sh
        - -c
        - sleep 3600
        volumeMounts:
        - name: secret-volume
          mountPath: "/data"
      volumes:
      - name: secret-volume
        secret:
          secretName: secret-opaque

进入pod容器内部,看下挂载卷,在顺便看下/data/..data/目录下的源文件,发现源文件是一个644权限

​

​

​既然是管理安全的那就只让一个人看就好了,将它修改下权限修改挂载卷的权限

需要在运行的控制器中将defaultMode字段进行修改,默认是420,此字段在deployment.spec.tamplate.spec.volumes.secret.defaultMode

修改后会发现所有的pod都进行了重建。因为k8s中对 Pod.Spec 的更新做了一些限制,防止某些字段被修改,所以不允许在 Pod 已经运行后修改。defaultMode 是与卷权限相关的配置,属于卷配置的一部分,因此一旦 Pod 创建后,卷相关的设置不允许更新

apiVersion: apps/v1
kind: Deployment
metadata:
  name: secret-opaque-volume
spec:
  replicas: 3
  selector:
    matchLabels:
      app: stu
  template:
    metadata:
      labels:
        app: stu
    spec:
      containers:
      - name: opaque-container
        image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
        imagePullPolicy: IfNotPresent
        command:
        - sh
        - -c
        - sleep 3600
        volumeMounts:
        - name: secret-volume
          mountPath: "/data"
      volumes:
      - name: secret-volume
        secret:
          secretName: secret-opaque
          defaultMode: 256    #十进制数字,转换八进制后,会变成400权限,不加这个字段会默认填充,默认值是420,也就是644权限

 可以看到挂载卷权限已被修改

​

指定某个键进行挂载

apiVersion: apps/v1
kind: Deployment
metadata:
  name: secret-opaque-volume
spec:
  replicas: 3
  selector:
    matchLabels:
      app: stu
  template:
    metadata:
      labels:
        app: stu
    spec:
      containers:
      - name: opaque-container
        image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
        imagePullPolicy: IfNotPresent
        command:
        - sh
        - -c
        - sleep 3600
        volumeMounts:
        - name: secret-volume
          mountPath: "/data"
      volumes:
      - name: secret-volume
        secret:
          secretName: secret-opaque
          defaultMode: 256
          items:
          - key: username
            path: 1/username

#itemes.path:这个字段,他是以mountPath字段中写的路径为基础的相对路径,且必须是相对路径。items.path的意思就是/data/1/username,文件名就是键,内容就是值。

#挂载的同时可以更改键名,username可任意修改为符合要求的键名,

#如果想直接挂载到mountPath字段路径下,就在path字段中写键名即可,无需写任务

​

看下挂载的值没有问题。再看下文件类型,它和cm不一样,是一个标准的文件类型而不是一个链接文件,这就意味着它不支持热更新。不信的话可以修改被挂载的secret试一下

作为卷挂载热更新

上面的栗子不支持热更新,这里把热更新实验一下。还是上面的代码

apiVersion: apps/v1
kind: Deployment
metadata:
  name: secret-opaque-volume
spec:
  replicas: 3
  selector:
    matchLabels:
      app: stu
  template:
    metadata:
      labels:
        app: stu
    spec:
      containers:
      - name: opaque-container
        image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
        imagePullPolicy: IfNotPresent
        command:
        - sh
        - -c
        - sleep 3600
        volumeMounts:
        - name: secret-volume
          mountPath: "/data"
      volumes:
      - name: secret-volume
        secret:
          secretName: secret-opaque
          defaultMode: 256
          items:
          - key: username
            path: sername    #唯一不同的是这里

​

​看到区别了么?当挂载路径在根目录的时候,文件会变成链接文件,这时候才支持热更新。

也就是说,如果希望挂载卷后续支持更新就挂载到mountPath字段的路径下,不更新就新建子目录进行挂载

对信息进行更新测试一下,记住sedret中要修改的信息都要先进性编码

​

​

​

 更新成功,更新过程比较久,不到一分钟

作为卷挂载禁止更新

它和cm一样有禁止更新的操作,此操作不可逆

​

3、Downward API(集群信息存储)

严格来说Downward API并不是存储,它只是借用了挂载文件的形态,让你能在容器里像读文件一样方便地读到自己的元数据,但它不具备存储的任何核心特性(持久化、大容量、可写),但是为了方便理解,就归类为存储

Downward API 是 Kubernetes 提供的一种机制,让 Pod 里的容器在不直接调用 Kubernetes API 的情况下,也能获取到自身的元数据(Metadata)和状态信息。

ps:这个不直接调用kubernetes API意思是,代码中没有向 kubernetes.default.svc 或 API Server 的 Pod IP 发起任何网络请求,数据来源是本地环境变量或文件(这两个东西可以AI一下,有功夫可以深入了解,本章就不多做解释了)

这些信息可作为环境变量注入到容器中或者挂载到容器中,以便容器可以获取有关其运行环境的各种信息,如pod名称、命名空间、标签等。说白了就是将pod的信息暴露给容器使用

downward API中的字段分为Pod和容器资源两个级别,我列个表格出来,方便各位同志查找

podfieldRef(Pod级别)备注envvolume
metadata.namepod名称√√
metadata.namespacepod的命名空间√√
metadata.uidPod 的唯一 ID√√
metadata.annotations['<KEY>']Pod单个注解 <KEY> 的值√√
metadata.labels['<KEY>']Pod单个标签 <KEY> 的值√√
spec.serviceAccountNamePod的安全服务账号名称√×
spec.nodeNamePod运行时所处的节点名称√×
status.hostIPPod 所在节点的主 IP 地址√×
status.hostIPs当前pod所在节点双栈IP√×
status.podIPPod 的主 IP 地址(通常为IPv4)√×
status.podIPs当前pod双栈IP√×
metadata.labelspod所有标签×√
metadata.annotationspod全部注解×√
resourceFieldRef(容器资源级别)备注envvolume
limits.cpu容器的 CPU 限制值√√
limits.memory容器的内存限制值√√
limits.ephemeral-storage容器的临时存储的限制值√√
requests.cpu容器的 CPU 请求值√√
requests.memory容器的内存请求值√√
requests.ephemeral-storage容器的临时存储的请求值√√
limits.hugepages-*容器的巨页限制值官网扒的,后续补充
resource: requests.hugepages-*容器的巨页请求值官网扒的,后续补充

如果没有为容器指定 CPU 和内存限制时尝试使用 Downward API 暴露该信息,那么 kubelet 默认会根据 节点可分配资源 计算并暴露 CPU 和内存的最大可分配值

作为环境变量注入

apiVersion: v1
kind: Pod
metadata:
  name: downwards
  labels:
    app: stu
  annotations:
    version: "v1"
spec:
  containers:
  - name: downwards-container
    image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
    imagePullPolicy: IfNotPresent
    command:
    - env
    env:
    - name: POD_NAME
      valueFrom:
        fieldRef:
          fieldPath: metadata.name
    - name: POD_NAMESPACE
      valueFrom:
        fieldRef:
          fieldPath: metadata.namespace
    - name: POD_UID
      valueFrom:
        fieldRef:
          fieldPath: metadata.uid
    - name: POD_LABELS
      valueFrom:
        fieldRef:
          fieldPath: metadata.labels['app']    #单个标签的意思就是只能选择一个标签键
    - name: POD_ANNOTATIONS
      valueFrom:
        fieldRef:
          fieldPath: metadata.annotations['version']    #同上
    - name: POD_SERVICEACCOUNTNAME
      valueFrom:
        fieldRef:
          fieldPath: spec.serviceAccountName
    - name: POD_PODIP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP
    - name: POD_HOSTIP
      valueFrom:
        fieldRef:
          fieldPath: status.hostIP
    - name: POD_NODENAME
      valueFrom:
        fieldRef:
          fieldPath: spec.nodeName
    - name: LIMITS__CPU
      valueFrom:
        resourceFieldRef:
          resource: limits.cpu
    - name: LIMITS_MEMORY
      valueFrom:
        resourceFieldRef:
          resource: limits.memory
    - name: LIMITS_STORAGE
      valueFrom:
        resourceFieldRef:
          resource: limits.ephemeral-storage
    - name: REQUEST_CPU
      valueFrom:
        resourceFieldRef:
          resource: requests.cpu      
    - name: REQUEST_MEMORY
      valueFrom:
        resourceFieldRef:
          resource: requests.memory
    - name: REQUEST_STORAGE
      valueFrom:
        resourceFieldRef:
          resource: requests.ephemeral-storage
  restartPolicy: Never

​

图中可以看到将pod的信息都暴露给了容器运行,只关注命名空间,podIP和pod名称就行,剩下的后面才会学到

作为卷挂载使用

apiVersion: v1
kind: Pod
metadata:
  name: da-volume
  labels:
    good: study
    day: up
  annotations:
    one: "v1"
    two: "v2"
spec:
  containers:
  - name: da-volume-container
    image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
    imagePullPolicy: IfNotPresent
    command: ["sleep","3600"]
    volumeMounts:
    - name: da-volume
      mountPath: /etc/podinfo
  volumes:
  - name: da-volume
    downwardAPI:
      items:
      - path: podname
        fieldRef:
          fieldPath: metadata.name
      - path: namespace
        fieldRef:
          fieldPath: metadata.namespace
      - path: uid
        fieldRef:
          fieldPath: metadata.uid
      - path: oneannotations
        fieldRef:
          fieldPath: metadata.annotations['one']
      - path: onelabels
        fieldRef:
          fieldPath: metadata.labels['good']
      - path: allannotations
        fieldRef:
          fieldPath: metadata.annotations
      - path: alllabels
        fieldRef:
          fieldPath: metadata.labels
      - path: licpu
        resourceFieldRef:
          containerName: da-volume-container
          resource: limits.cpu
      - path: limem
        resourceFieldRef:
          containerName: da-volume-container
          resource: limits.memory
      - path: listorage
        resourceFieldRef:
          containerName: da-volume-container
          resource: limits.ephemeral-storage
      - path: recpu
        resourceFieldRef:
          containerName: da-volume-container
          resource: requests.cpu
      - path: remem
        resourceFieldRef:
          containerName: da-volume-container
          resource: requests.memory
      - path: restorage
        resourceFieldRef:
          containerName: da-volume-container
          resource: requests.ephemeral-storage
  restartPolicy: Never

​

可以看到相关信息都以卷的形式挂载到了/etc/podinfo下面变成了文件。​

DownwardAPI 扩展

上面的实验案例是将pod和容器的元数据传递给了他们内部运行的进程。但是这种方式仅仅只暴露自身pod的元数据给容器,且只有部分元数据可以暴露,也无法做到跨pod引用数据

所以基于官方的apiserver接口就登场了,直接从接口拿数据,而不是通过官方提供的机制取数据

这个实验暂时不要理解,照做就行,后面学到安全再回来看,一切都会理解的,这里先做出来是为了让同志们知道还有别的方法获取pod信息

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: test-api
subjects:
  - kind: ServiceAccount
    name: test-api
    namespace: default
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io
kubectl create sa test-api
apiVersion: v1
kind: Pod
metadata:
  name: curl
spec:
  serviceAccountName: test-api
  containers:
  - name: curl-container
    image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/yauritux/busybox-curl:latest
    imagePullPolicy: IfNotPresent
    command: ["sleep","3600"]

#镜像我换了新的busybox,因为带curl命令
kubectl exec -it <podName> -- /bin/sh

进入容器执行下方三条命令
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CAPATH="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

准备工作做完后执行此命令,就可以从接口中访问当前命名空间所有pod的任何信息
curl -H "Authorization: Bearer $TOKEN" --cacert $CAPATH https://kubernetes/api/v1/namespaces/$NS/pods

​

4、volume(数据持久化)

容器磁盘上的文件声明周期是短暂的,这就使得在容器中运行重要应用时会出现一些问题。首先当容器崩溃时,kublet会重启它,但是容器中的文件将丢失,且会以容器镜像最初的状态重新启动容器。其次在pod中同时运行多个容器时,这些容器之间通常需要共享文件,k8s中的volume就很好的解决了这些问题

kubernetes官方提供了很多类型的卷,并且很多卷类型在新版本中已经被弃用或者移除了。这里只演示几种常用的卷

emptyDir

对于定义了 emptyDir 卷的 Pod,在 Pod 被指派到某节点时此卷会被创建。 就像其名称所表示的那样,emptyDir 卷最初是空的。尽管 Pod 中的容器挂载 emptyDir 卷的路径可能相同也可能不同,但这些容器都可以读写 emptyDir 卷中相同的文件。 当 Pod 因为某些原因被从节点上删除时,emptyDir 卷中的数据也会被永久删除。

apiVersion: v1
kind: Pod
metadata:
  name: volume
spec:
  containers:
  - name: nginx
    image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nginx:1.27.0
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: logs-volume
      mountPath: /var/log/nginx
  - name: busybox
    image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
    imagePullPolicy: IfNotPresent
    command: 
    - tail
    - -f 
    - /logs/access.log
    volumeMounts:
    - name: logs-volume
      mountPath: /logs
  volumes:
  - name: logs-volume
    emptyDir: {}

​

​curl对podIP进行访问,然后查看busybox日志就出现了nginx的访问成功的记录,access.log这个文件busybox肯定是没有的,这是nginx记录访问成功的日志文件,这就证明了两个容器时共享一个挂载卷的

kubelet的工作目录(root-dir参数控制)默认为/var/lib/kubelet,会为每个使用了emptyDir:{}的pod创建一个目录,这个目录处于pod所在节点,格式为

/var/lib/kubelet/pods/{podid}/volumes/kubernetes.io~empty-dir/

所有放在emptyDir的数据,最终都是落在了上述路径中,可以看下文件内容或在文件中添加一些字符,再去pod查看日志确定准确性

​

​

#用内存作为emptyDir卷,特定场景使用手段,不做演示,代码放在这里了解即可
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: registry.k8s.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir:
      sizeLimit: 500Mi
      medium: Memory

ps:谁会用内存当持久卷用?知不知道内存多贵?

hostPath(常用)

hostPath卷将主机节点文件系统中的文件或目录挂载在集群中,hostPath用途如下

  1. 运行一个需要访问节点级系统组件的容器 (例如一个将系统日志传输到集中位置的容器,使用只读挂载 /var/log 来访问这些日志)
  2. 让存储在主机系统上的配置文件可以被静态pod以只读方式访问;与普通 Pod 不同,静态 Pod 无法访问 ConfigMap。

除了必须的path字段,用户还可以为hostPath卷指定type,因为类型太多不一一举例,就选个常用的类型来进行实验

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

  使用hostPath有几点需要注意

  1. 由于每个节点上的环境都不同,具有相同配置的pod在不同节点上的行为可能会有所不同,(例如用Template创建的pod和单独写的pod就会不同)
  2. 当k8s按照计划添加资源感知调度时,将无法考虑hostPath使用的资源
  3. 当k8s使用 hostPath 挂载主机上的目录时,容器能否写入取决于宿主机上那个目录原本的权限。如果它在宿主机上只有 root 能写,那么容器里也必须以 root 运行(并且是特权模式),或者你必须事先在主机上放宽权限,否则容器会写失败
apiVersion: v1
kind: Pod
metadata:
  name: hostpath
spec:
  containers:
  - name: hostpath-container
    image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
    imagePullPolicy: IfNotPresent
    command: ["sleep","3600"]
    volumeMounts:
    - name: hostpath-volume
      mountPath: /tmp/testpod
  volumes:
  - name: hostpath-volume
    hostPath:
      type: Directory
      path: /data    #这个/data是最后pod分配节点的/data,不是master的/data
        type: Directory

 启动对象发现过了很久还没有创建好,kubectl describe查一下。看到报错说/data不是一个目录,为什么?因为directory类型要求挂载的目录必须存在,而我没有创建

在所有节点都创建一个/data目录,因为不知道pod会调度到哪个节点,并在里面加个文件,删除pod再次创建​​

 可以看到成功了,或者用DirectoryOrCreate类型都可以,前提是没有这么个目录

​​

5、PV/PVC

  • PV(Persistent Volume):持久化卷,是集群中的一块存储,可以由管理员事先供应,或者使用存储类(Storage Class)来动态供应。它是一种抽象的存储资源,独立于使用它的 Pod。例如,它可以是一块网络存储(如 NFS、Ceph 等)或者云存储(如 AWS EBS、Azure Disk 等)在 Kubernetes 集群中的抽象表示。
  • PVC(Persistent Volume Claim):持久化卷声明,是用户对于存储的请求。它类似于用户向存储系统 “申请” 一定量的存储资源,比如指定存储的大小、访问模式(如只读、读写)等要求。

关系阐述

  • PVC 是对 PV 的请求:用户通过创建 PVC 来请求存储资源,PVC 定义了所需要的存储资源的规格和特性。例如,一个 PVC 可能会声明需要 10G大小的存储,并且是读写模式(ReadWriteOnce)。Kubernetes 会根据这个请求在已有的 PV 中查找或者动态创建符合要求的 PV 来绑定这个 PVC。
  • PV 和 PVC 的绑定关系:当 PVC 的请求与 PV 的属性相匹配时(如存储大小、访问模式等条件满足),它们就会绑定在一起。这种绑定是一对一的关系,一个 PVC 只能绑定一个 PV,一个 PV 也只能被一个 PVC 绑定。绑定之后,Pod 就可以通过 PVC 来使用对应的 PV 所提供的存储资源。例如,一个运行数据库的 Pod 可以通过挂载一个绑定了合适 PV 的 PVC 来将数据持久化存储到对应的存储介质中。
  • 生命周期的关联:PVC 的生命周期通常会受到与其绑定的 PV 的影响。如果 PV 被删除或者不可用,与之绑定的 PVC 的状态也会受到影响。例如,如果 PV 对应的存储设备出现故障,PVC 所代表的存储请求就无法正常满足,可能会导致使用该 PVC 的 Pod 出现存储相关的问题。同时,当 PVC 被删除时,与之绑定的 PV 可能会根据其回收策略(如保留、删除、回收)来进行相应的处理

所以这个逻辑是 PV 持久卷的背后是实体存储,PVC 和 Pod 是进行绑定的,而 PVC 和 PV 进行了匹配,所以等于 Pod 使用了背后的存储,如下图

​​

关联条件

  1. 容量:PV的值不小于PVC要求,可以大于最好一致
  2. 读写策略:完全匹配
    • 单节点读写--ReadWriteOnce--RWO
    • 多节点只读--ReadOnlyMany--ROX
    • 多节点读写--ReadWriteMany--RWX
  3. 存储类:PV的类与PVC的类必须一致,不存在包容降级关系(包容降级这个指的是公司中自定义的存储等级。比如公司有3个等级的存储,pvc写了匹配1级的,就不会降级匹配)

存储类型不一定都支持三种读写策略,比如iscsi不支持RWX

PV回收策略

​​

假设现在需要杀死一个pod,pvc就成为了孤立的存在,pvc不死pv不会被释放。这种情况下数据不想保留,就可以将pvc杀死。pvc死掉之后相应的pv没有人去关联它了,这时需要pv自行处理。分为三种情况

  • Retain(保留): 手动回收。就是pv处于未就绪状态,现在一个pvc要来进行关联(符合条件情况下),pv不允许绑定,因为pv内部很可能存在高价值的数据,允许绑定会将高价值数据覆盖掉。这时要由运维人员将数据备份,在手动恢复它为可用状态
  • Recycle(回收):简单擦除。当pod和pvc被杀死且pv没有被关联,它会拉取一个特定镜像来创建pod。这个pod的作用就是执行rm -rf /thevolume/*删除上一个留下的数据,这个不推荐用,一方面是拉镜像是国外的镜像源,这个镜像源还不能在pvc清单中配置,比较费劲。另方面是此功能官方在1.14版本就准备废弃掉了,转而推荐动态供应storagteClass
  • Delete(删除): 删除存储卷。pod和pvc被杀起且pv没有被关联,就会把自己清理掉,这个pv就不存在了

当前只有 nfs 和 hostPath 卷类型支持回收(Recycle),但是nfs不支持delete策略

PV状态

  • Avaliable(可用):空闲资源还没有被任何声明(PVC)所绑定
  • Bound(已绑定):卷已经被PVC绑定
  • Relaeased(已释放):PVC被删除,但是资源还未被集群重新声明
  • Failed(失败):该卷的自动回收失败

命令行会显示绑定到PV的PVC的名称

PV/PVC的创建

理论结束,开始实践。此次实验用nfs当作pv,因为NFS的优点是配置简单、成本低,支持多Pod读写共享(比如用于存放配置文件或日志),在自建机房或本地开发测试环境:NFS(网络文件系统)和Local PV(本地存储)最为常见,适合对性能要求不高的场景,

首先要部署一个nfs服务器来作为pv使用,可以新开一个虚拟机或者在master节点安装

#搭建nfs服务器,每个节点都安装
yum -y install nfs-utils rpcbind
systemctl start rpcbind
systemctl start nfs-server.service

#因为服务名称错误导致启动nfs报错的同志,用systemctl list-unit-files |grep -i nfs过滤

回到master节点,我选了此节点作为nfs服务器

mkdir /nfsdata
useradd nfsnobody
chmod 666 /nfsdata
chown nfsnobody /nfsdata
cd /nfsdata

#为了更好呈现pvc效果,多建几个目录进行pv制作,并且每个目录下都有内容
for i in {1..10} ;do mkdir $i ;echo $i > $i/index.html ;done

vim /etc/exports
/nfsdata/1 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/2 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/3 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/4 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/5 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/6 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/7 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/8 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/9 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/10 *(rw,no_root_squash,no_all_squash,sync)

exportfs -r

showmount -e <hostIP>    #查看共享

#可以在集群中其他节点测试挂载nfs目录
mount -t nfs <nfsServerIP:/path> <localPath>

#这样就模拟了十个nfs的共享存储,并且将十个目录以读写权限共享给所有客户端,保留客户端 root 权限(无映射限制),并确保数据同步写入磁盘

将nfs抽象为PV

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfspv1
spec:
  capacity:
    storage: 0.9Gi
  accessModes: 
  - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /nfsdata/1
    server: 192.168.100.101
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfspv2
spec:
  capacity:
    storage: 500Mi
  accessModes:
  - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /nfsdata/2
    server: 192.168.100.101
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfspv3
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /nfsdata/3
    server: 192.168.100.101
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfspv4
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy:  Retain
  storageClassName: nfs
  nfs:
    path: /nfsdata/4
    server: 192.168.100.101
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfspv5
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /nfsdata/5
    server: 192.168.100.101

创建无头服务,给等下的控制器使用

apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  clusterIP: None # 配置为无头服务(Headless Service),允许直接访问 Pod
  ports:
    - name: web
      port: 80
  selector:
    app: nginx

创建PVC,设置PV筛选条件来选择PV使用

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  serviceName: nginx # 关联的 Service 名称,且service必须是 Headless Service
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx-container
          image: nginx:one
          imagePullPolicy: IfNotPresent
          volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates: # 定义持久化卷模板
    - metadata:
        name: www # PVC 名称,需与 volumeMounts 一致
      spec:
        accessModes: 
          - ReadWriteOnce
        storageClassName: nfs
        resources:
          requests:
            storage: 1Gi

#一般pvc是配合sfs使用,pod分配的pv也是一致,假设想pod分别用不同规格的pv可以用另种方式。先写pvc,但是pvc名称一定要和sfs产生的pod名称完全一致,pod会自动按序号挂载同名pvc,所以还是可以工作的。但这样失去了动态创建的便利性

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: www-pod-0
spec:
  accessModes: 
    - ReadWriteOnce
  storageClassName: nfs
  resources:
    requests:
      storage: 1Gi  # pod-0 用 1G
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: www-pod-1
spec:
  accessModes: 
    - ReadWriteOnce
  storageClassName: nfs
  resources:
    requests:
      storage: 2Gi  # pod-1 用 2G
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: www-pod-2
spec:
  accessModes: 
    - ReadWriteOnce
  storageClassName: nfs
  resources:
    requests:
      storage: 3Gi  # pod-2 用 3G
---
# 2. StatefulSet 中不使用 volumeClaimTemplates,改用 volumes 手动指定
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx-statefulset
spec:
  serviceName: nginx-svc
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx-container
        image: nginx:latest
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates: []   #volumeClaimTemplates留空

代码中要求控制器创建三个Pod,三个Pod都需要挂载存储卷,所以它会根据清单创建三个PVC,按照PVC声明自动匹配满足条件的PV,这就是整个清单的逻辑了。

从上图的的查询信息可以非常清晰的看到Pod、PVC和PV的状态及绑定情况。

稍微提一句,pod和PV的绑定并不是pod1绑定PV1这种按顺序来的这里涉及到了预选和优选算法

  • 预算算法:必须满足我的最低要求,有多个预选的情况下在预选的基础上优选
  • 优选算法:就是打分,海选之后晋级。如果预选全部一样,那就随机选择

PV的回收

实验结束后删除控制器,可以看到控制器和pod已经被删除了,但是pvc和pv还是有绑定关系的。因为它害怕当前pod的持久化文件被自动抹除,所以PVC不会进行自动删除。

​

看上图,前面提过pod死亡后pvc成为了孤立状态,并且和pv是绑定的。所以pvc是需要手动进行删除来释放PV的

如果不对PV进行释放,根据同一个清单重新创建pod会发生什么呢?试一下

​

看上图,再次创建pod时,然后访问web-0还是之前的页面,这说明pod又自动挂载了pvc。因为控制器名字没变,其次pv模板的名字没变,所以pvc会复用之前的配置

所以工作中如果不确定数据后期会不会再次使用的话,只删除StatefulSet控制器即可,保留pvc


上图,对nginx-0的这个pvc进行了删除,理论上绑定pv的声明都被删除了,那么pv应该被释放对吗?

但是nginx-0的pv删除后pv虽然处于一个Relaeased已释放的这么一个状态,但是数据仍然保留,pv尚未被重新分配或清理。如果想清理数据就需要手动删除nfs共享目录的数据,想真正释放pv让它被使用需要修改pv的这么一个地方

​

再来看pv就处于可用了的状态了​

再删除nginx-2,PV状态是回收失败。因为前面提到过nfs不支持delete策略,也可以用这种方式进行pv的回收​

storageClass(存储类)

它实现动态申请存储的机制,这种方式已经趋近于主流方式

StorageClass是一种资源对象,用于定义持久卷(PersistentVolume)的动态供给(DynamicProvisioning)策略。StorageClass允许管理员定义不同类型的存储,并指定如何动态创建持久卷以供应用程序使用。这使得k8s集群中的存储管理更加灵活和自动化.

  • 静态PV:静态 PV 是由集群管理员提前创建的,通常用于某些需要明确控制存储资源的场景。需要管理员手动创建 PV 资源,并指定存储细节(如存储大小、存储类型、访问模式等。虽然满足要求,但并不是最好的。假设申请1.98T的PV。存储部门不可能这样划分的,它会给你2T,这样就造成了资源的浪费
  • 动态PV:云存储服务会直接将他的服务抽象到集群内部形成一个PV,这个PV可以理解为一个通道。然后直接写PVC,在通过PVC清单要求的容量大小、类型自动划分

所以StorageClass更多是配合云厂商实现,毕竟只有大厂才有专业的存储部门,存储类就是指不同厂商的存储服务。

当然公司内部也可以自己搭建块存储的服务。有相应的软件提供支持,包括但不限于,比如lognhorn为分布式存储提供支持,nfs-client-provisioner为中心化存储提供支持

我这里就要用nfs-client-provisioner软件,nfs实在太老了,动态分配存储并不符合k8s的标准,所以需要这个第三方软件来实现,用它来模拟一个云厂商的StaaS(存储即服务)

这个软件镜像是开源的,可以再github上搜索或者用这个国内镜像站,很全面很好用渡渡鸟镜像同步站https://docker.aityp.com/

#搭建nfs服务器,每个节点都安装
yum -y install nfs-utils rpcbind
systemctl start rpcbind
systemctl start nfs-server.service

#回到master节点
mkdir /nfsdata
useradd nfsnobody
chmod 666 /nfsdata
chown nfsnobody /nfsdata
cd /nfsdata


vim /etc/exports
/nfsdata/share *(rw,no_root_squash,no_all_squash,sync)

#创建命名空间
kubectl create ns nfs-storageclass
#创建控制器,模拟k8s的动态存储分配器nfs-client-provisioner,创建一个存储类
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  namespace: nfs-storageclass
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs-client-provisioner
  strategy:
     type: Recreate
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/gmoney23/nfs-client-provisioner:latest
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: k8s-sigs.io/nfs-subdir-external-provisioner
            - name: NFS_SERVER
              value: 192.168.176.101
            - name: NFS_PATH
              value: /nfsdata/share
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.176.101
            path: /nfsdata/share
#创建sa,这个后面安全的章节会学到, 现在不用看懂,后面安全章节会学到,那时候再回来看就豁然开朗了,我这里先写出来是为了让同志们知道有这么个方法
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  namespace: nfs-storageclass
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: nfs-storageclass
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  namespace: nfs-storageclass
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  namespace: nfs-storageclass
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: nfs-storageclass
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  pathPattern: ${.PVC.namespace}/${.PVC.name}
  onDelete: delete
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-claim
  annotations:
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Mi
  storageClassName: nfs-client
---
apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
    - name: test-container
      image: nginx:one 
      imagePullPolicy: IfNotPresent
      volumeMounts:
        - name: nfs-pvc
          mountPath: /usr/share/nginx/html
  restartPolicy: Never
  volumes:
    - name: nfs-pvc
      persistentVolumeClaim:
        claimName: test-claim

访问pod被拒绝,因为原本的index.html文件被挂载的存储覆盖了,去目录自己随便写个就行

​删除pod后PV/PVC都会消失

6、StatefulSet和无头服务

在上面“PV/PVC创建”章节中,出现了之前提过的控制器和服务:StatefulSet和Headless Service。之前没进行实验是因为要结合PV/PVC进行。下面具体展开介绍

前面说了pod控制器,pod控制器分为两种类型

  • 批处理任务:Job、CronJob
  • 守护进程:ReplicationController、RepolicaSet、Deployment、DaemonSet、StatefulSet

守护进程类型控制器还分为两种:

  • 控制无状态服务:ReplicationController、RepolicaSet、Deployment、DaemonSet
    • nginx就是典型的无状态应用。实例之间相互独立,没有唯一身份或持久数据的需求。任意实例可以随时被替换,且数据可以丢失就。比如日志丢失不会导致服务出问题。
  • 控制有状态服务:StatefulSet
    • 而mysql就是典型的有状态应用。比如mysql集群,从两个高可用的mysql拿走一个,过段时间在放回进群内,它肯定不能使用了,因为数据没有同步。StatefulSet 提供了对有状态应用所需的特性支持,例如持久存储、固定网络标识和有序部署/删除等。

StatefulSet的特性

  1. 有序创建;有序回收
  2. 数据持久化Pod级别
  3. 稳定的网络访问方式

有序创建

#依旧是部署nfs服务
yum -y install nfs-utils rpcbind
systemctl start rpcbind
systemctl start nfs-server.service

#回到master节点,我选了此节点作为nfs服务器
mkdir /nfsdata
useradd nfsnobody
chmod 666 /nfsdata
chown nfsnobody /nfsdata
cd /nfsdata

for i in {1..10} ;do mkdir $i ;echo $i > $i/index.html ;done

vim /etc/exports
/nfsdata/1 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/2 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/3 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/4 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/5 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/6 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/7 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/8 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/9 *(rw,no_root_squash,no_all_squash,sync)
/nfsdata/10 *(rw,no_root_squash,no_all_squash,sync)

exportfs -r

showmount -e <hostIP>    #查看共享

#可以在集群中其他节点测试挂载nfs目录
mount -t nfs <nfsServerIP:/path> <localPath>
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfspv1
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /nfsdata/1
    server: 192.168.100.101
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfspv2
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadOnlyMany
  persistentVolumeReclaimPolicy: Delete
  storageClassName: nfs
  nfs:
    path: /nfsdata/2
    server: 192.168.100.101
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfspv3
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs
  nfs:
    path: /nfsdata/3
    server: 192.168.100.101
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfspv4
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteMany
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: nfs
  nfs:
    path: /nfsdata/4
    server: 192.168.100.101
---
apiVersion: v1
kind: Service
metadata:
  name: pvsv
spec:
  clusterIP: None
  ports:
  - name: web
    port: 6666
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  serviceName: pvsv
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx-container
        image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nginx:1.27.0
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - name: suibian
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata: 
      name: suibian
    spec:
      accessModes: 
      - ReadWriteOnce
      storageClassName: nfs
      resources:
        requests:
          storage: 1Gi

可是同志们,我现在有个问题,我想扩容一下nginx pod数量,是否可行呢?试一下

#调整控制器副本数量
kubectl scale <controllerKind> <controllerName> --replicas=<Number>

​从上图看,pod名字很奇怪哈,不像其他控制器一样后面跟的是哈希值,而是从0开始按序往下。到了处于pending状态的nginx-2停了。这就是有序创建的特性,等待上一个pod处于running状态后,停才会继续创建下一个pod,这就是有序创建;有序回收就是倒着来

再来看pod,出现了Pending待处理的这么一个状态,describe看下pod创建过程

​

从上图来看,有个告警报错,这个报错的来源是默认调度器,说的是有三个节点可用,但是pod没有办法调度到任何节点。因为系统找不到可用的pv来绑定pvc。

为什么PVC无法绑定到PV了?看下代码,PVC要求有三点,类型要是nfs、大小1GB、单节点读写

​

再看创建好的4个pv情况,只有两个单节点读写的pv,从ngxin-2开始的pod创建后pvc找不到合适的pv了,所以一直处于一个待处理状态

​

数据持久化Pod级别

随便访问一个pod,访问nginx-0好了,可以看到主页内容是1

 进入容器内给主页添加一些内容然后删除pod

等到pod重建后再去访问nginx-0看结果,就会发现访问内容还是一样的。通过ip和创建时间就知道这是完全的重建的新的pod

实现过程来分析下:pod绑定pvc,pvc绑定pv,pv绑定nfs实体存储。

虽然pod被删除了,但是pvc和pv的绑定是没有变化的,当同样一个pod出现,绑定同样一个pvc,那后端的数据肯定是一致的

从实验可以看出来这个数据的持久化是pod级别的,将pod杀死在创建一个新的数据也一致

稳定的网络访问方式

通过上面的数据持久化实验会发现,虽然数据一致了,但是IP进行了变更。通过IP访问Pod很显然这不是一个稳定的方式。但是有另个方式:域名(比如www.baidu.com)

做个实验,随便写个pod。

apiVersion: v1
kind: Pod
metadata:
  name: test
spec:
  containers:
  - name: test-c
    image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/busybox:1.28
    imagePullPolicy: IfNotPresent
    command:
    - "sleep"
    - "3600"

 启动对象,进入pod内部

wget http://nginx-0.pvsv.default.svc.cluster.local./index.html && cat index.html && rm -fr index.html

#之前访问域名都是从svc的名字开始,但这样不能准确访问到web-0的内容,因为svc会轮训负载。所以最前面加了web-0,用域名精准访问

 ok没问题,访问的是同一个主页,现在还不能说明问题

 删除nginx-0后,待nginx-0重建后访问看看,如下图所示,访问的是同个域名但是可以发现pod的ip是变了的,佐证了域名的稳定性访问

​

无头服务(Headless Services)

生产环境中有时并不需要负载均衡,也不需要ServiceIP。遇到这种情况,可以通过显式设置集群 IP(spec.clusterIP)的值为 "None" 来创建无头服务(Headless Service)。可以使用无头 Service 与其他服务发现机制交互,而不必绑定到 Kubernetes 的实现
 

它是一种特殊的Service类型,它的核心特点是:不分配 ClusterIP,也不做负载均衡。只告诉你所有Pod的真实地址。还能给每个Pod一个永久的DNS名字(稳定的网络标识),此服务虽说不是为了SFS控制器而生,但是其最核心、最经典的功能(稳定的网络标识),必须依赖StatefulSet才能实现

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: deploy-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: deploy
  template:
    metadata:
      labels:
        app: deploy
    spec:
      containers:
      - name: deploy-demo-container
        image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/nginx:1.27.0
        imagePullPolicy: IfNotPresent
apiVersion: v1
kind: Service
metadata:
  name: headless
spec:
  clusterIP: None
  ports:
  - name: web
    port: 6666
  selector:
    app: suibian

无头 Service 不会获得集群 IP,kube-proxy 不会处理这类 Service, 而且平台也不会为它们提供负载均衡或路由支持。但是和svc同名的endpoint却是有记录的

前面提到过SVC底层实现是将SVC的标签选择器匹配到的podIP记录到端点中,再由kube-proxy将规则维护至ipvs中,由每个节点的kube-proxy进程为SVC实现VIP的效果。但是现在clusterIP为空,没了VIP,endpoint还记录端点信息有什么用呢?

写DNS记录

之前说到过SVC的默认域名为svcName.namespace.svc.cluster.local.从这个规则的域名可以解析出svc的clusterIP,但是现在svc是无头服务,没有了clusterIP解析他有什么意义呢

先通过集群DNS Pod来解析下,看看结果

可以看到SVC关联的Pod的IP和域名都被解析了出来,这不是重点,重点是SFS用唯一的取名方式给了每个Pod独一无二的域名

Pod域名就是在SVC域名前面加上Pod名称,其他控制器用控制器名字加哈希值来给Pod取名 ,随着Pod被重建,Pod名称也会变化。但是SFS始终是名称加序数这种有序创建的方式进行取名,这就代表了一旦根据SFS控制器的Pod被创建,Pod的域名就已经被固定了,即使Pod被删除重建,名称依然不变。这种方式为数据库、消息队列等有状态应用提供稳定的网络身份

解析一下Pod域名就知道结果如何

域名和IP被解析出来了,把deploy-demo-2删掉,在进行解析看看

只有IP变了,域名依旧不变,侧面证明了稳定的网络身份

总结

StatefulSet 负责给每个 Pod 起固定的名字(比如 db-0),PVC 负责给每个 Pod 绑定专属的存储,无头服务负责让 Pod 之间能通过固定的域名(比如 db-0.db-svc)互相找到。

可以这样理解它们的分工:

  1. StatefulSet + PVC:解决 “我的数据不能丢”。即使 Pod 删了重建,它还能找到自己原来的那个存储(PVC),不会拿错别人的数据。

  2. StatefulSet + 无头服务:解决 “我的同伴在哪里”。数据库集群里(比如主从复制),Pod 需要通过一个永远不变的地址(域名)去找到其他 Pod,而不是靠可能会变的 IP。

如果去掉无头服务:
你只知道 db-0 这个名字,但找不到它的访问地址,其他 Pod 也就没法跟它通信了。

一个极简类比:

  • StatefulSet:固定的工号(员工-001)。

  • PVC:专属的带锁工位抽屉(重启回来还是打开自己的抽屉)。

  • 无头服务:内部通讯录(直接拨 员工-001 就能找到他,不管他换了多少次座位/IP)。

结论: 单机版有状态应用(比如一个 MySQL 不用主从)可以只用 StatefulSet + PVC。但只要涉及到集群、主从、互相发现(比如 Kafka、ZK、Redis 集群),就必须加上无头服务。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐