容器化入门(六)--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:需要重点关注 storageClassName
、accessModes
字段;同时,针对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
... ...