K8S入门-概念篇(下)
一. volume
volume解决的是pod上不同容器之间共享文件的问题和容器文件持久化的问题;K8S提供了以下几类volume:
1. hostPath
hostPath是一种通过pod所在node上的文件系统指定的文件或者目录实现文件共享,如下所示,先在Pod内定义一个volume,类型定义为hostPath,并指定挂载到node上的指定路径下,然后pod内各个容器指定对应volume挂载到自己文件系统即可,如下所示:
apiVersion: v1 kind: Pod metadata: name: test-pod2 spec: containers: - image: busybox name: test-hostpath volumeMounts: - mountPath: /test-data name: test-volume volumes: - name: test-volume hostPath: path: /data type: Directory
在上面的示例中,我们创建了一个名字叫做test-volume且类型为hostPath的卷,在容器中volumeMounts的name字段指明使用这个test-volume这个卷,并挂载到容器的/test-data目录下。
hostPath类型卷有一个优点是,volume内文件的存在周期是脱离于pod的生命周期的,即Pod终止之后,hostPath卷下面的文件不会消失。但使用的时候需要注意,容器会在启动之初将挂载点下的目录清空,所以若是存在同一个容器共享一个挂载文件,需要在容器启动之后再去填充指定的目录。
2. emptyDir
hostPath因为是挂载到node本地路径下,所以对于被调度节点的目录结构是有要求的。还有一种node本地存储卷方式叫做emptyDir,这种映射机制不需要指定映射目录,不仅可以映射到磁盘甚至可以映射到内存,对于被调度节点目录结构无要求,配置示例如下:
apiVersion: v1 kind: Pod metadata: name: test-pd spec: containers: - image: k8s.gcr.io/test-webserver name: test-container volumeMounts: - mountPath: /cache name: cache-volume volumes: - name: cache-volume emptyDir: {} medium: Memory
如上图示例,我们创建了一个emptyDir类型的volume cache-volume,Pod上的容器test-container使用这个卷,并映射到容器内的/cache路径下,甚至愿意的话,我们可以指定存储介质为Memory,表示借助内存来构建卷。
需要注意的是,emptyDir里面的文件在Pod重新调度会丢失,且当选择内存作为挂载点时,需要关注内存的消耗(这部分内存会归为容器的内存消耗)。
3. nfs
无论是hostPath还是emptyDir,都面临一个问题,即Pod被重新调度后原有磁盘上的文件丢失了,有时我们希望无论Pod被如何调度,都可以恢复在磁盘上的数据,NFS volume提供了这样一种机制,NFS volume可以把网络文件系统映射到容器内部,以保证Pod调度不会导致磁盘数据丢失。配置格式如下:
apiVersion: apps/v1 kind: Pod metadata: name: redis spec: selector: matchLabels: app: redis spec: containers: # 应用的镜像 - image: redis name: redis # 持久化挂接位置,在docker中 volumeMounts: - name: redis-persistent-storage mountPath: /data volumes: # 宿主机上的目录 - name: redis-persistent-storage nfs: path: /k8s-nfs/redis/data server: 192.168.8.150
4. persistent volume claim
从上面通过NFS实现持久化存储,我们看到开发人员需要关注底层的存储介质是NFS,且对应文件系统的IP地址,不仅如此,如果我们在另外一个K8S集群部署服务的时候,对应K8S集群需要支持NFS的存储介质,这不仅增加了开发人员的负担也降低了部署的灵活性,而PVC(Persistent Volume Claim)就是用来实现底层存储技术和Pod的解耦,使得开发人员不需关注底层的存储技术,也不需要了解应该使用哪种类型服务器运行Pod。
那么具体是如何操作的呢?
首先系统管理员创建某类或者某几类网络存储,这种网络存储可能是NFS、也可能是mongoDB等,创建好之后系统管理员通过K8S的API传递PV声明(这个PV声明包含存储能力、访问模式、存储类型等信息)通过这个PV声明K8S集群将存储介质抽象成一个个预分配的持久卷;PV创建如下所示:
kind: PersistentVolume #指定为PV类型 apiVersion: v1 metadata: name: pv-statefulset #指定PV的名称 labels: #指定PV的标签 release: stable spec: capacity: storage: 0.1Gi #指定PV的容量 accessModes: - ReadWriteOnce #指定PV的访问模式,简写为RWO,只支持挂在1个Pod的读和写 persistentVolumeReclaimPolicy: Recycle #指定PV的回收策略,Recycle表示支持回收, hostPath: #指定PV的存储类型,本文是以hostpath为例 path: /data/pod/volume1 #指定PV对应后端存储hostpath的目录
当用户需要申请PV的时候,会申请一个PVC,一种用户存储的声明,里面含有存储容量大小、存储类型等,由K8S在系统中查找对应项进行匹配,实现PVC和PV的绑定如下所示:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mppvc-01 # 指定PVC的名称 namespace: default spec: accessModes: ["ReadWriteOnce"] # 指定PVC的访问模式 resources: requests: storage: 0.05Gi # PVC申请的容量
用户创建Pod通过volume的配置引用PVC,实现PV的使用。
通过PV和PVC的形式,底层存储介质的申请和分割由系统管理员完成,上层开发人员对底层存储介质和地址无感知,只需向系统申请一块指定大小和访问模式的存储空间即可。
但是,是否我们可以不用预先创建一系列的PV,这样既麻烦又不灵活,而storage class提供了一种可以动态创建PV的。
二. Deployment
Deployment是比Pod更高阶的资源,解决的是Pod声明式更新的问题;通过Replication controller,K8S集群可以将Pod的个数维持在期望的水平,而Deployment资源在创建时,也会创建一个Replica Set/Replication Controller借助它的功能达到节点数目维持在一定数目的能力。
那么既然Deployment是解决Pod声明式的更新问题,那么它的解决方案是怎样的呢?
第一,升级时机
当我们修改deployment的描述文件的镜像信息并提交K8S 的API Server,K8S的deployment controller发现Pod的image的版本更新了,则触发版本升级流程。
第二,升级策略
deployment支持两种版本升级策略:
一种是Recreate策略,所谓recreate策略,是先把现网Pod实例都停了,然后统一进行版本升级,这种升级策略相对比较简单,但是期间会导致短暂的服务不可用。
一种是RollingUpdate,这也是deployment默认的升级策略,滚动更新默认分批停止Pod实例,分批更新,分批重启;整个过程中,服务不会中断,但实践过程中,需要业务方关注版本兼容性的问题。
第三,升级步长
所谓升级步长即定义批次的大小,每一批次更新多少Pod实例,在deployment的配置过程中,通过maxSurge(决定了除期望副本数之外最多允许超过的Pod数目)和maxUnavailable(决定了版本更新期间最多多少Pod不可用)属性定义一次替换多少Pod;以maxSurge=1为例
第四,安全升级
在deployment升级过程中,我们可以通过就绪探针+minReadySeconds来避免升级过程中错误扩散,具体来说,就是通过就绪探针,我们可以确保Pod实例并稳定工作minReadySeconds之后,我们才会执行下一个节点的停止和升级更新操作,避免因为版本问题导致的更新错误扩散。
那么deployment进行版本更新的原理是什么呢?
简单来说,就是发现需要版本升级了,deployment controller会创建一个新版本的replicaSet,通过协调两个replicaSet的replicas属性,逐步实现老版本的替换
三. statefulset
Deployment用于解决无状态节点的部署问题,但是这类节点没有稳定的网络标识:节点无法预知自己的网络标识,重新拉起之后网络标识会发生改变;这对于有状态的现网服务来说十分的不友好,因此K8S提供了StatefulSet作为有状态节点重启的解决方案。
具体来说,StatefulSet提供以下几点保证:
1)stateful pod的每个Pod有一个从零开始的索引,Pod名称可预知
2)Pod重新调度之后保留其标识和状态不变
3)Stateful 提供at most one服务,即当现有Pod故障时,除非控制面能确定Pod已经停止服务了,否则不会重新调度
StatefulSet使用时需要注意的点:
1)扩容的时候,会按顺序使用下一个索引值,不能跳着用
2)缩容时,按照节点索引号从大到小的缩容,且缩容时是逐个进行,所以缩容比较慢
四. nodeport
NodePort解决的是集群服务对外暴露的问题;基本原理是给集群内的机器开一个端口,外网机器拿到这台机器的外网IP和对外暴露的端口号,给指定端口发包,集群内机器的指定端口的流量都会被据此转到后端对应的service上,再由service转发到各个Pod上;如下图所示:
这里多提一句,都是服务暴露的方式,Service和NodePort,以及后面说到的ingress有什么区别?
service是内网服务暴露的机制,本质上就是一条转发规则,其IP事实上是不存在的,既不存在于外网也不存在于内网,service请求端需要是集群内的节点,装有kube-proxy,并运行于集群内;而NodePort则是服务对外网暴露的,其IP和端口是事实存在并可以被外网访问的,请求NodePort暴露的服务的客户端,可以是任意机器,不需要运行于K8S集群内。
五. ingress
通过NodePort进行服务暴露是K8S比较原始的机制,且不灵活,为什么呢?
首先,一个端口只能暴露一个服务;其次,若是节点的外网IP变了,则对应客户端也需要感知到这种变化。所以,对于七层协议服务,K8S提供了ingress这种服务暴露方式,具体有哪些优势?
第一,是支持一个地址暴露多个服务,服务之间通过路径进行区分;
第二,提供名字服务,机器地址改变无需客户端做出变化。
如下图所示:
参考资料
https://www.qikqiak.com/k8strain/k8s-basic/pod/
http://docs.kubernetes.org.cn/683.html:kubectl命名表
https://www.cnblogs.com/gdut1425/p/13046507.html:PV和PVC原理
https://blog.csdn.net/qq_36807862/article/details/106068871:iptables vs ipvs
http://dockone.io/article/4884:nodeport、ingress、loadbalance