首先来看一下Pod Volumes的使用场景:
以上两个场景,其实都可以借助Volumes来很好地解决,接下来首先看一下Pod Volumes的常见类型:
既然已经有了Pod Volumes,为什么又要引入PV呢?Pod中声明的volume生命周期与Pod相同的,以下有几种常见的场景:
以上场景中,通过Pod Volumes很难准确地表达它的复用/共享语义,对它的扩展也比较困难。因此K8s中又引入了Persistent Volumes概念,它可以将存储和计算分离,通过不同的组件来管理存储资源和计算资源,然后解耦pod和Volume之间生命周期的关联。这样,当把pod删除之后,它使用的PV仍然存在,还可以被新建的pod复用
用户在使用PV时其实是通过PVC,为什么有了PV又设计了PVC呢?主要原因是为了简化K8s用户对存储的使用方式,做到职责分离。通常用户在使用存储的时候,只用声明所需的存储大小以及访问模式
访问模式是什么?其实就是:我要使用的存储是可以被多个node共享还是只能单node独占访问(注意是node level而不是pod level)?只读还是读写访问?用户只用关心这些东西,与存储相关的实现细节是不需要关心的
通过PVC和PV的概念,将用户需求和实现细节解耦开,用户只用通过PVC声明自己的存储需求。PV是有集群管理员和存储相关团队来统一运维和管控,这样的话,就简化了用户使用存储的方式
既然PV是由集群管理员统一管控的,接下来就看一下PV这个对象是怎么产生的
静态产生方式(静态Provisioning):由集群管理员事先去规划这个集群中的用户会怎样使用存储,它会先预分配一些存储,也就是预先创建一些PV;然后用户在提交自己的存储需求(也就是PVC)的时候,K8s内部相关组件会帮助它把PVC和PV做绑定;之后用户再通过pod去使用存储的时候,就可以通过PVC找到相应的PV,它就可以使用了
静态产生方式有什么不足呢?可以看到,首先需要集群管理员预分配,预分配其实是很难预测用户真实需求的。举一个最简单的例子:如果用户需要的是20G,然而集群管理员在分配的时候可能有80G 、100G的,但没有20G的,这样就很难满足用户的真实需求,也会造成资源浪费
动态供给:就是说现在集群管理员不预分配PV,他写了一个模板文件,这个模板文件是用来表示创建某一类型存储(块存储、文件存储等)所需的一些参数,这些参数是用户不关心的,给存储本身实现有关的参数。用户只需要提交自身的存储需求,也就是PVC文件,并在PVC中指定使用的存储模板(StorageClass)
K8s集群中的管控组件,会结合PVC和StorageClass的信息动态,生成用户所需要的存储(PV),将PVC和PV进行绑定后,pod就可以使用PV了。通过StorageClass配置生成存储所需要的存储模板,再结合用户的需求动态创建PV对象,做到按需分配,在没有增加用户使用难度的同时也解放了集群管理员的运维工作
在pod yaml文件中的Volumes字段中,声明我们卷的名字以及卷的类型。声明的两个卷,一个是用的是emptyDir,另外一个用的是hostPath,这两种都是本地卷
在容器中应该怎么去使用这个卷呢?它其实可以通过volumeMounts这个字段,volumeMounts字段里面指定的name其实就是它使用的哪个卷,mountPath就是容器中的挂载路径
这里还有个subPath,subPath是什么?先看一下,这两个容器都指定使用了同一个卷,就是这个cache-volume。那么,在多个容器共享同一个卷的时候,为了隔离数据,我们可以通过subPath来完成这个操作。它会在卷里面建立两个子目录,然后容器1往cache下面写的数据其实都写在子目录cache1了,容器2往cache写的目录,其数据最终会落在这个卷里子目录下面的cache2下
还有一个readOnly字段,readOnly的意思其实就是只读挂载,这个挂载你往挂载点下面实际上是没有办法去写数据的
另外emptyDir、hostPath都是本地存储,它们之间有什么细微的差别呢?emptyDir其实是在pod创建的过程中会临时创建的一个目录,这个目录随着pod删除也会被删除,里面的数据会被清空掉;hostPath顾名思义,其实就是宿主机上的一个路径,在pod删除之后,这个目录还是存在的,它的数据也不会被丢失。这就是它们两者之间一个细微的差别
静态PV首先是由管理员来创建的,管理员我们这里以NAS,就是阿里云文件存储为例。我需要先在阿里云的文件存储控制台上去创建NAS存储,然后把NAS存储的相关信息要填到PV对象中,这个PV对象预创建出来后,用户可以通过PVC来声明自己的存储需求,然后再去创建pod。创建pod还是通过我们刚才讲解的字段把存储挂载到某一个容器中的某一个挂载点下面
刚刚创建的阿里云NAS文件存储对应的PV,有个比较重要的字段:capacity,即创建的这个存储的大小,accessModes,创建出来的这个存储它的访问方式
然后有个ReclaimPolicy(PV的回收策略):这块存储在被使用后,等它的使用方pod以及PVC被删除之后,这个PV是应该被删掉还是被保留呢?
接下来看看用户怎么去使用该PV对象。用户在使用存储的时候,需要先创建一个PVC对象。PVC对象里面,只需要指定存储需求,不用关心存储本身的具体实现细节。存储需求包括哪些呢?首先是需要的大小,也就是resources.requests.storage;然后是它的访问方式,即需要这个存储的访问方式,这里声明为ReadWriteMany,也即支持多node读写访问,这也是文件存储的典型特性
上图中左侧,可以看到这个声明:它的size和access mode,跟我们刚才静态创建这块PV其实是匹配的。这样的话,当用户在提交PVC的时候,K8s集群相关的组件就会把PV的PVC bound到一起。之后,用户在提交pod yaml的时候,可以在卷里面写上PVC声明,在PVC声明里面可以通过claimName来声明要用哪个PVC。这时,挂载方式其实跟前面讲的一样,当提交完yaml的时候,它可以通过PVC找到bound着的那个PV,然后就可以用那块存储了。这是静态Provisioning到被pod使用的一个过程
这个模板文件叫StorageClass,在StorageClass里面,我们需要填的重要信息:第一个是provisioner,provisioner其实就是说创建PV和对应的存储的时候,应该用哪个存储插件来去创建
这些参数是通过K8s创建存储的时候,需要指定的一些细节参数。对于这些参数,用户是不需要关心的,像这里regionld、zoneld、fsType和它的类型。ReclaimPolicy就是动态创建出来的这块PV,当使用方使用结束、Pod及PVC被删除后,这块PV应该怎么处理,我们这个地方写的是delete,意思就是说当使用方pod和PVC被删除之后,这个PV也会被删除掉
接下来看一下,集群管理员提交完 StorageClass,也就是提交创建PV的模板之后,用户怎么用,首先还是需要写一个PVC的文件
PVC的文件里存储的大小、访问模式是不变的。现在需要新加一个字段,叫StorageClassName,它的意思是指定动态创建PV的模板文件的名字,这里StorageClassName填的就是上面声明的csi-disk
在提交完PVC之后,K8s集群中的相关组件就会根据PVC以及对应的StorageClass动态生成这块PV给这个PVC做一个绑定,之后用户在提交自己的yaml时,用法和接下来的流程和前面的静态使用方式是一样的,通过PVC找到我们动态创建的PV,然后把它挂载到相应的容器中就可以使用了
Capacity:存储对象的大小
AccessModes使用这个PV的方式。它有三种使用方式:
用户在提交PVC的时候,最重要的两个字段:Capacity和AccessModes。在提交PVC后,K8s集群中的相关组件是如何去找到合适的PV呢?首先它是通过为PV建立的AccessModes索引找到所有能够满足用户的PVC里面的AccessModes要求的PV list,然后根据PVC的Capacity、StorageClassName、Label Selector进一步筛选PV,如果满足条件的PV有多个,选择PV的size最小的,accessmodes列表最短的PV,也即最小适合原则
ReclaimPolicy:用户方PV的PVC在删除之后,PV应该做如何处理?常见的有两种方式
StorageClassName:动态Provisioning时必须指定的一个字段,就是说要指定到底用哪一个模板文件来生成PV
NodeAffinity:就是说创建出来的PV,它能被哪些node去挂载使用,其实是有限制的。然后通过NodeAffinity来声明对node的限制,这样其实对使用该PV的pod调度也有限制,就是说pod必须要调度到这些能访问PV的node上,才能使用这块PV
首先在创建PV对象后,它会处在短暂的pending状态;等真正的PV创建好之后,它就处在available状态
available状态意思就是可以使用的状态,用户在提交PVC之后,被K8s相关组件做完bound(即:找到相应的PV),这个时候PV和PVC就结合到一起了,此时两者都处在bound状态。当用户在使用完PVC,将其删除后,这个PV就处在released状态,之后它应该被删除还是被保留呢?这个就会依赖ReclaimPolicy
这里有一个点需要特别说明一下:当PV已经处在released状态下,它是没有办法直接回到available状态,也就是说接下来无法被一个新的PVC去做绑定
如果我们想把已经released的PV复用,我们这个时候通常应该怎么去做呢?第一种方式:我们可以新建一个PV对象,然后把之前的released的PV的相关字段的信息填到新的PV对象里面,这样的话,这个PV就可以结合新的PVC了;第二种是在删除pod之后,不要去删除PVC对象,这样给PV绑定的PVC还是存在的,下次pod使用的时候,就可以直接通过PVC去复用。K8s中的StatefulSet管理的Pod带存储的迁移就是通过这种方式
Readiness probe也叫就绪探针,用来判断一个pod是否处在就绪状态。当一个pod处在就绪状态的时候,它才能够对外提供相应的服务,也就是说接入层的流量才能打到相应的pod。当这个pod不处在就绪状态的时候,接入层会把相应的流量从这个pod上面进行摘除
如下图其实就是一个Readiness就绪的一个例子:
当这个pod指针判断一直处在失败状态的时候,其实接入层的流量不会打到现在这个pod上
当这个pod的状态从FAIL的状态转换成success的状态时,它才能够真实地承载这个流量
Liveness指针也是类似的,它是存活探针,用来判断一个pod是否处在存活状态
当一个pod处在不存活状态时会由上层的判断机制来判断这个pod是否需要被重新拉起。那如果上层配置的重启策略是restart always的话,那么此时这个pod会直接被重新拉起
Liveness指针和Readiness指针支持三种不同的探测方式:
从探测结果来讲主要分为三种:
1)exec
如上图所示,这是一个Liveness probe,它里面配置了一个exec的一个诊断。接下来,它又配置了一个command的字段,这个command字段里面通过cat一个具体的文件来判断当前Liveness probe的状态,当这个文件里面返回的结果是0时,或者说这个命令返回是0时,它会认为此时这个pod是处在健康的一个状态
2)httpGet
httpGet里面有一个字段是路径,第二个字段是port,第三个是headers。这个地方有时需要通过类似像header头的一个机制做health的一个判断时,需要配置这个header,通常情况下,可能只需要通过health和port的方式就可以了
3)tcpSocket
tcpSocket只需要设置一个检测的端口,像这个例子里面使用的是8080端口,当这个8080端口tcp connect连接正常被建立的时候,那tecSocket Probe会认为是健康的一个状态
4)此外还有如下的五个参数,是Global的参数
第一个参数叫initialDelaySeconds,它表示的是说这个pod启动延迟多久进行一次检查,比如说现在有一个Java的应用,它启动的时间可能会比较长。所以前期,可能有一段时间是没有办法被检测的,而这个时间又是可预期的,那这时可能要设置一下initialDelaySeconds
第二个是periodSeconds,它表示的是检测的时间间隔,正常默认的这个值是10秒
第三个字段是timeoutSeconds,它表示的是检测的超时时间,当超时时间之内没有检测成功,那它会认为是失败的一个状态
第四个是successThreshold,它表示的是:当这个pod从探测失败到再一次判断探测成功,所需要的阈值次数,默认情况下是1次,表示原本是失败的,那接下来探测这一次成功了,就会认为这个pod是处在一个探针状态正常的一个状态
最后一个参数是failureThreshold,它表示的是探测失败的重试次数,默认值是3,表示的是当从一个健康的状态连续探测3次失败,那此时会判断当前这个pod的状态处在一个失败的状态
实际上是一个Pod的一个生命周期。刚开始它处在一个pending的状态,那接下来可能会转换到类似像running,也可能转换到Unknown,甚至可以转换到failed。然后,当running执行了一段时间之后,它可以转换到类似像successded或者是failed,然后当出现在unknown这个状态时,可能由于一些状态的恢复,它会重新恢复到running或者successded或者是failed
其实K8s整体的一个状态就是基于这种类似像状态机的一个机制进行转换的,而不同状态之间的转化都会在相应的K8s对象上面留下来类似像Status或者像Conditions的一些字段来进行表示
在早期,也就是1.10以前的K8s版本。大家都会使用类似像Heapster这样的组件来去进行监控的采集,Heapster的设计原理其实也比较简单
首先,在每一个Kubernetes上面有一个包裹好的cadvisor,这个cadvisor是负责数据采集的组件。当cadvisor把数据采集完成,Kubernetes会把cadvisor采集到的数据进行包裹,暴露成相应的API。在早期的时候,实际上是有三种不同的API:
这三种接口,其实对应的数据源都是cadvisor,只是数据格式有所不同。而在Heapster里面,其实支持了summary接口和kubelet两种数据采集接口,Heapster会定期去每一个节点拉取数据,在自己的内存里面进行聚合,然后再暴露相应的service,供上层的消费者进行使用。在K8s中比较常见的消费者,类似像dashboard,或者是HPA-Controller,它通过调用service获取相应的监控数据,来实现相应的弹性伸缩,以及监控数据的一个展示
上图是Heapster内部的一个架构。分为几个部分,第一个部分是core部分,然后上层是有一个通过标准的http或者https暴露的这个API。然后中间是source的部分,source部分相当于是采集数据暴露的不同的接口,然后processor的部分是进行数据转换以及数据聚合的部分。最后是sink部分,sink部分是负责数据离线的,这个是早期的Heapster的一个应用的架构。那到后期的时候呢,K8s做了这个监控接口的一个标准化,逐渐就把Heapster进行了裁剪,转化成了metrics-server
目前0.3.1版本的metrics-server大致的一个结构就变成了上图这样,是非常简单的:有一个core层、中间的source层,以及简单的API层,额外增加了API Registration这层。这层的作用就是它可以把相应的数据接口注册到K8s的API server之上,以后客户不再需要通过这个API层去访问metrics-server,而是可以通过这个API注册层,通过API server访问API注册层,再到metrics-server。这样的话,真正的数据消费方可能感知到的并不是一个metrics-server,而是说感知到的是实现了这样一个API的具体的实现,而这个实现是metrics-server。这个就是metrics-server改动最大的一个地方
日志采集从采集位置是哪个划分,需要支持如下三种:
首先是宿主机文件,这种场景比较常见的是说我的这个容器里面,通过类似像volume,把日志文件写到了宿主机之上。通过宿主机的日志轮转的策略进行日志的轮转,然后再通过我的宿主机上的这个agent进行采集
第二种是容器内有日志文件,那这种常见方式怎么处理呢,比较常见的一个方式是说我通过一个Sidecar的streaming的container,转写到stdout,通过stdout写到相应的log-file,然后再通过本地的一个日志轮转,然后以及外部的一个agent采集
第三种我们直接写到stdout,这种比较常见的一个策略,第一种就是直接我拿这个agent去采集到远端,第二种我直接通过类似像一些sls的标准API采集到远端
课程地址:https://edu.aliyun.com/roadmap/cloudnative?spm=5176.11399608.aliyun-edu-index-014.4.dc2c4679O3eIId#suit