容器化入门(六)--k8s高级

文摘   2024-07-29 00:00   美国  

容器化入门(六)--k8s高级

本文内容主要来源于极客时间《Kubernetes 入门实战课》,夹杂了自己在学习过程中的思考和相关记录

1.Volume:在使用ConfigMap/Secret时已经接触过Volume了,它类似与一个虚拟盘。Volume是k8s对数据存储的抽象,具体的类型、容量大小都可以自由发挥,Pod无需关注复杂性,只要设置好VolumeMount就能使用

2.PersistentVolume:我们知道Pod里的容器是镜像产生的, 而镜像本身是只读的,Pod内的进程进行普通的读写磁盘操作实际使用的是一个临时空间,Pod销毁后临时存储中的数据就会丢失。真实的业务场景中,我们的业务数据肯定是要做持久化的。PersistentVolume对象用于表示持久化存储设备,简称PV。作为存储的抽象,PV实际上是一些存储设备、文件系统等,例如NFS、甚至是本地磁盘。需要注意, PV是集群的系统资源,跟Node平级,Pod对其没有管理权、只有使用权。

3.PersistentVolumeClaim/StorageClass:多种多样的存储设备,只有一个对象来管理不太方便,不符合单一职责原则,让Pod直接去选择PV也不灵活。为了更好的管理存储,也方便Pod使用,引入了中间层,两个新对象:PersistentVolumeClaim和StorageClass

  • PersistentVolumeClaim简称PVC,是用来向系统申请存储资源,是Pod的代理,代表Pod向系统申请PV。一旦申请成功,k8s就把PV和PVC联结在一起,这个动作叫绑定Bind
  • StorageClass有点IngressClass,它将特定类型存储资源抽象出来,便于资源的申请和管理。

4.PV的yaml描述:

  • k8s中有很多类型的PV,HostPath是最简单的本地存储。 跟docker的使用-v参数挂载本地路径非常像。
  • 无法根据create命令创建出来yaml模板,只能参考api描述自行完成。如下是一个样例,重点关注几个关键字段:
apiVersion: v1
kind: PersistentVolume
metadata:
  name: host-10m-pv

spec:
  storageClassName: host-test
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 10Mi
  hostPath:
    path: /tmp/host-10m-pv/
  • storageClassName:即storageClass名称
  • accessModes:定义访问模式,总共有三种:ReadWriteOnce可读可写、但是只能被一个节点挂载;ReadOnlyMany,只读不可写,可以被多个节点挂载;ReadWriteMany,可读可写,可以被多个节点挂载;
  • capacity:表示存储设备的容量。这里的单位一定是国际标准单位,我们日常习惯使用的 KB/MB/GB 的基数是 1024,要写成 Ki/Mi/Gi
  • hostPath:指明存储卷的本地路径

5.PVC的yaml描述:

  • 如下是一个PVC定义的yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: host-5m-pvc

spec:
  storageClassName: host-test
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Mi
  • storageClassName、accessModes的含义跟PV一致。通过resources.request表示希望申请的容量。

  • k8s 根据 PVC 里的描述,去找能够匹配 StorageClass 和容量的 PV,然后把 PV 和 PVC“绑定”在一起,实现存储的分配

6.PV、PVC的使用:

  • 使用kubectl apply -f创建PV和PVC对象。注意观察PVC和PV的状态,因为PVC能申请到合适的PV对象,因为PVC和PV的对象都是Bound
kubectl apply -f host-path-pv.yml 
kubectl apply -f host-path-pvc.yml 
  • 在Pod中挂载存储。我们在Pod的spec.volumes中定义好存储卷,特别的使用persistentVolumeClaim指明PVC的名字;然后containers.volumeMounts中挂载进容器。
apiVersion: v1
kind: Pod
metadata:
  name: host-pvc-pod

spec:
  volumes:
  - name: host-pvc-vol
    persistentVolumeClaim:
      claimName: host-5m-pvc

  containers:
    - name: ngx-pvc-pod
      image: nginx:alpine
      ports:
      - containerPort: 80
      volumeMounts:
      - name: host-pvc-vol
        mountPath: /tmp
kubectl apply -f host-path-pod.yaml
kubectl get pod host-pvc-pod -o wide
  • 进入到容器内部创建一个文件,发现在对应宿主机节点hostPath定义的本地路径下同步创建出该文件,Pod销毁后本地路径下该文件依旧存在。重新创建Pod,进入Pod后依旧能读取到之前创建的文件,达到了持久化的目的。
  • 如果Pod使用的存储超过申请的大小,会出现什么效果呢?

经过测试,实际并没有什么限制。有点意外。如下是chatgpt的答复, 可信度存疑

7.网络存储:在k8s中只有网络存储才有意义

  • HostPath的存储面对Pod偏移的场景会出现严重问题,但是在真正的集群环境中,Pod漂移是非常常见的
  • 要想存储卷真正能被Pod任意挂载,需要改成网络存储。网络存储有非常多的产品,k8s还专门定义了CSI规范。本节我们使用linux上比较常用的NFS系统来介绍网络存储的使用

8.安装和部署NFS的服务端:选一个linux服务器,安装服务端,即数据真正被存储的地方:

  • 安装服务端,并创建数据存储目录
sudo apt -y install nfs-kernel-server
mkdir -p /tmp/nfs
  • 配置/etc/exports,指定目录名、允许访问的网段、权限等参数。新加如下配置
/tmp/nfs 192.168.56.0/24(rw,sync,no_subtree_check,no_root_squash,insecure)

并让配置生效

sudo exportfs -ra
sudo exportfs -v
sudo systemctl start  nfs-server
sudo systemctl enable nfs-server
sudo systemctl status nfs-server

查看挂载情况

showmount -e 127.0.0.1

9.安装NFS的服务端:在需要使用NFS的节点安装客户端

  • 安装包:mkdir -p /tmp/test
  • 挂载到本地并测试:
sudo mount -t nfs 192.168.56.110:/tmp/nfs /tmp/test
touch /tmp/test/x.yml

10.NFS存储卷实战:

  • 创建PV:需要重点关注storageClassNameaccessModes字段;同时,针对NFS系统,需要在YAML中添加nfs字段指定NFS服务器的IP地址和共享目录,需要提前创建该目录1g-ppv,否则对应的Pod无法创建成功
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-1g-pv

spec:
  storageClassName: nfs
  accessModes:
    - ReadWriteMany
  capacity:
    storage: 1Gi

  nfs:
    path: /tmp/nfs/1g-pv
    server: 192.168.56.110
  • 创建PVC:内容跟PV差不多,只不过不需要描述nfs的细节,需要使用resources.request声明要申请的存储大小
  • 在Pod中挂载:跟之前一样,先在spec.volums中定义卷,然后在spec.containers.volumeMount挂载到对应路径
apiVersion: v1
kind: Pod
metadata:
  name: nfs-static-pod

spec:
  volumes:
  - name: nfs-pvc-vol
    persistentVolumeClaim:
      claimName: nfs-static-pvc

  containers:
    - name: nfs-pvc-test
      image: nginx:alpine
      ports:
      - containerPort: 80

      volumeMounts:
        - name: nfs-pvc-vol
          mountPath: /tmp
  • 本案例相关的几个对象之间的关系如下

11.动态存储之NFS Provisioner:

  • 我们目前实现的持久化存储方案太low,PV需要手动创建、人工管理。因为存储是系统资源,实际的集群环境中,必须由系统管理员创建PV、然后由开发人员再创建PV,并且PV的大小很难精确控制;再者实际的集群环境中,分配存储的诉求会很多,由人工管理分配这么多存储完全不现实。必须让PV的创建更加自动化一点,最好让工具完成

  • 在k8s中有动态存储卷的概念,就是解决动态地、自动地根据PVC创建PV的诉求。通过用StorageClass绑定一个Provisioner对象来实现,这个Provisioner提供自动管理存储、创建PV的功能。

  • 跟Ingress Controller类似,NFS Provisioner是通过Pod形式提供服务的,部署它需要三个Yaml,参考https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner以及https://github.com/chronolaw/k8s_study/tree/master/nfs安装,注意为了方便安装同时也为了匹配我们实际的业务逻辑、对原始的yaml做了一些调整(包括命名空间、镜像、NFS服务器、NFS共享路径):

12.使用动态存储:

  • StorageClass:NFS有一份自定义的StorageClass,名称为nfs-client。provisioner指明了动态存储的提供方,archiveOnDelete参数指明在删除PVC时是否要归档,我们也可以修改参数,重新定义一个StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client

provisioner: k8s-sigs.io/nfs-subdir-external-provisioner 
parameters:
  archiveOnDelete: "false"
  • 再定义PVC,像系统申请10MB空间,没什么特别注意的, 把storageClassName写对即可,其余跟静态存储保持一致
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-dyn-10m-pvc

spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteMany

  resources:
    requests:
      storage: 10Mi
  • 在Pod中使用PVC
apiVersion: v1
kind: Pod
metadata:
  name: nfs-dyn-pod

spec:
  volumes:
  - name: nfs-dyn-10m-vol
    persistentVolumeClaim:
      claimName: nfs-dyn-10m-pvc

  containers:
    - name: nfs-dyn-test
      image: nginx:alpine
      ports:
      - containerPort: 80

      volumeMounts:
        - name: nfs-dyn-10m-vol
          mountPath: /tmp

创建好PVC和Pod后我们可以看到,PV被自动创建出来了,大小刚好时10M。可以进入Pod在被挂载的文件夹下创建文件,然后去NFS服务器对应目录下查看文件确实生成了:

13.动态存储下PV、PVC、Pod的关系:

  • 删除Pod,不会影响PV和PVC的状态
  • 针对accessMode为ReadWriteMany的PVC,支持多个Pod同时使用该PVC挂载存储
  • 在保证PVC没有被任何Pod使用时,可以删除PVC,此时PV的状态取决与StorageClass定义的参数

14.StatefulSet

  • 有状态应用和无状态应用的概念:无状态应用是指自己不需要存储任何业务数据、微服务重启就重启了; 现实生活中,最典型的无状态应用是Nginx,而Nginx、Redis等数据库应用是有状态应用
  • Deployment加上PV,能满足有状态应用的基本需要,但是不不够:对于有状态应用,大部分情况下还需要考虑启动顺序依赖、Pod的名称、IP地址以及域名等问题。 需要一个新的API对象来管理,StatefulSet应运而生。
  • StatefulSet的YAML无法根据之前的方法创建样板,但是它跟Deployment对象差不多,适当修改Deployment对象的YAML就可以获得StatefulSet的YAML(唯一的不不同是kind不同,以及spec多出了一个serviceName字段)
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-sts

spec:
  serviceName: redis-svc
  replicas: 2
  selector:
    matchLabels:
      app: redis-sts

  template:
    metadata:
      labels:
        app: redis-sts
    spec:
      containers:
      - image: redis:5-alpine
        name: redis
        ports:
        - containerPort: 6379

15.StatefulSet的使用

  • StatefulSet所管理的Pod不再是随机的名字了,分别是从YAML定义的name再加一个序号生成的名字,该序号代表启动顺序
  • Pod的hostname被设置为Pod名,这样Pod内部的应用可以获取到自己的身份以及启动顺序,从而据此处理一些服务启动依赖的问题
  • 网络标识问题:我们给sts对象创建以恶搞Service对象,注意:给sts创建svc对象必须通过yaml的方式、而不能通过kubectl expose命令的方式直接生成;同时svc的name必须和sts对象的serviceName一致
apiVersion: v1
kind: Service
metadata:
  name: redis-svc

spec:
  selector:
    app: redis-sts

  ports:
  - port: 6379
    protocol: TCP
    targetPort: 6379

创建成功之后就可以验证一个非常重要的特性,针对svc对象应用于sts对象时、会针对它的Pod生成一个稳定的域名(不像一般的Pod那样,域名是由IP地址构成的,我们都知道IP地址是不稳定的),格式为“Pod名.服务名.名字空间.svc.cluster.local”,可以简写为“Pod名.服务名

  • 针对sts的svc对象,如果我们仅仅是为了产生pod稳定的域名,而不会使用svc本身,那么我们可以在svc的定义中加一个字段clusterIP: None告诉k8s不用产生一个IP地址。

  • sts对象和svc对象的关系图如下

16.StatefulSet的数据持久化

  • 可以跟之前的方法类似,单独定义PVC,然后在StatefulSet中挂载
  • 但是,为了强调Pod对象跟pvc的一对一关系,k8s为sts对象新增了一个字段volumeClaimTemplates,直接把PVC的定义嵌入到sts中, 能保证创建StatefulSet的同时,就会为每个Pod自动创建PVC,让StatefulSet的可用性更高。例如,下面定义了一个sts对象,使用动态存储分配100M空间
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-pv-sts

spec:
  serviceName: redis-pv-svc

  volumeClaimTemplates:
  - metadata:
      name: redis-100m-pvc
    spec:
      storageClassName: nfs-client
      accessModes:
        - ReadWriteMany
      resources:
        requests:
          storage: 100Mi

  replicas: 2
  selector:
    matchLabels:
      app: redis-pv-sts

  template:
    metadata:
      labels:
        app: redis-pv-sts
    spec:
      containers:
      - image: redis:5-alpine
        name: redis
        ports:
        - containerPort: 6379

        volumeMounts:
        - name: redis-100m-pvc
          mountPath: /data
  • 可以进入Pod内部,在redis中设置几个值,然后退出Pod、删除重建后重新进入,检查redis里是否还有原先设置的值,验证持久化的目标是否达成:
  • 几个对象之间的关系如下:

17.滚动更新

  • 在实际的工程处理中,应用版本是快速迭代的,在工程上需要有优雅的方案解决版本的升级更新问题, 要考虑升级更新不中断业务、可回滚、可追随等问题,在k8s中可以使用kubectl rollout完成用户无感知的滚动更新,实现给“空中的飞机换引擎”
  • 应用的版本实际上体现在容器镜像的标签中,在k8s中容器运行在Pod中,因为版本更新实际上在更新Pod。k8s中应用的版本变化就是template里Pod的变化,k8s使用摘要算法对template生产hash值、并用该hash值作为Pod的“版本号”。任何template中的变化都会形成一个新的版本

18.k8s滚动更新的实现过程

改写之前的ngx-dep对象,yaml如下所示,使用kubectl apply -f创建之。 几个关键点

  • 创建一个cm对象,管理nginx的配置,让nginx返回nginx的版本号等信息

  • deploy对象增加一个minReadySeconds参数,该参数用于标识在设置Pod为可用之前的最小等待时间,用于观察滚动更新过程(实际业务场景中,该字段可以让滚动更新过程更加稳定,有助于确保Pod在加入svc之前已经运行了一段时间; 该字段不属于templete部分)

  • Pod的镜像使用1.21-alpine,replicas设置为4

  • 创建一个svc对象

---

# this cm will be mounted to /etc/nginx/conf.d
apiVersion: v1
kind: ConfigMap
metadata:
  name: ngx-conf

data:
  default.conf: |
    server {
      listen 80;
      location / {
        default_type text/plain;
        return 200
          'ver : $nginx_version\nsrv : $server_addr:$server_port\nhost: $hostname\n';
      }
    }

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep
  annotations:
    kubernetes.io/change-cause: v1, ngx=1.21

spec:
  minReadySeconds: 15

  replicas: 4
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      volumes:
      - name: ngx-conf-vol
        configMap:
          name: ngx-conf

      containers:
      - image: nginx:1.21-alpine
        name: nginx
        ports:
        - containerPort: 80

        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: ngx-conf-vol

---

apiVersion: v1
kind: Service
metadata:
  name: ngx-svc

spec:
  selector:
    app: ngx-dep

  ports:
  - port: 80
    protocol: TCP
    targetPort: 80

---

我们更新Pod使用的镜像为nginx:1.22-alpine,并使用kubectl apply -f应用,并使用kubectl rollout status查看更新状态

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep
  annotations:
    kubernetes.io/change-cause: update to v2, ngx=1.22

spec:
  minReadySeconds: 15

  replicas: 4
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      volumes:
      - name: ngx-conf-vol
        configMap:
          name: ngx-conf

      containers:
      - image: nginx:1.22-alpine
        name: nginx
        ports:
        - containerPort: 80

        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: ngx-conf-vol

使用kubectl describe deploy ngx-dep能更详细的看到滚动更新的过程:所谓的滚动更新,就是由Deployment控制的两个同步的应用伸缩过程,老版本缩容到0,新版本扩容到指定值,此消彼长的过程

19.如何管理滚动更新

  • 查看更新的历史kubectl rollout history
  • 可以追加--revision输出每个更新版本的更多信息
  • 回滚版本:使用kubectl rollout undo回滚版本,也可以加上--to-revision回退到任意一个历史版本;回滚实际上跟正向的滚动升级的过程是一样的,仍然是滚动的

20.如何追加更新的描述:Deployment 的metadata中添加一个annotations字段来追加更新的描述。annotations是注解的意思,相关的信息给k8s内部对象使用,你可以理解为扩展属性。借助annotations字段,可以给API对象添加任意的附加信息。编写更新说明需要使用特定的字段kubernetes.io/change-cause。如下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep
  annotations:
    kubernetes.io/change-cause: v1, ngx=1.21
... ...


编程废柴
努力Coding