first commit

This commit is contained in:
张乾
2024-10-15 23:19:16 +08:00
parent 1093d24039
commit 9327d43695
12 changed files with 2601 additions and 0 deletions

View File

@ -0,0 +1,335 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 应用存储和持久化数据卷:核心知识
Volumes 介绍
Pod Volumes
首先来看一下 Pod Volumes 的使用场景:
场景一:如果 pod 中的某一个容器在运行时异常退出,被 kubelet 重新拉起之后,如何保证之前容器产生的重要数据没有丢失?
场景二:如果同一个 pod 中的多个容器想要共享数据,应该如何去做?
以上两个场景,其实都可以借助 Volumes 来很好地解决,接下来首先看一下 Pod Volumes 的常见类型:
本地存储,常用的有 emptydir/hostpath
网络存储:网络存储当前的实现方式有两种,一种是 in-tree它的实现的代码是放在 K8s 代码仓库中的随着k8s对存储类型支持的增多这种方式会给k8s本身的维护和发展带来很大的负担而第二种实现方式是 out-of-tree它的实现其实是给 K8s 本身解耦的通过抽象接口将不同存储的driver实现从k8s代码仓库中剥离因此out-of-tree 是后面社区主推的一种实现网络存储插件的方式;
Projected Volumes它其实是将一些配置信息如 secret/configmap 用卷的形式挂载在容器中让容器中的程序可以通过POSIX接口来访问配置数据
PV 与 PVC 就是今天要重点介绍的内容。
Persistent Volumes
接下来看一下 PVPersistent Volumes。既然已经有了 Pod Volumes为什么又要引入 PV 呢?我们知道 pod 中声明的 volume 生命周期与 pod 是相同的,以下有几种常见的场景:
场景一pod 重建销毁,如用 Deployment 管理的 pod在做镜像升级的过程中会产生新的 pod并且删除旧的 pod ,那新旧 pod 之间如何复用数据?
场景二:宿主机宕机的时候,要把上面的 pod 迁移,这个时候 StatefulSet 管理的 pod其实已经实现了带卷迁移的语义。这时通过 Pod Volumes 显然是做不到的;
场景三:多个 pod 之间,如果想要共享数据,应该如何去声明呢?我们知道,同一个 pod 中多个容器想共享数据,可以借助 Pod Volumes 来解决;当多个 pod 想共享数据时Pod Volumes 就很难去表达这种语义;
场景四如果要想对数据卷做一些功能扩展性snapshot、resize 这些功能,又应该如何去做呢?
以上场景中,通过 Pod Volumes 很难准确地表达它的复用/共享语义,对它的扩展也比较困难。因此 K8s 中又引入了 **Persistent Volumes **概念,它可以将存储和计算分离,通过不同的组件来管理存储资源和计算资源,然后解耦 pod 和 Volume 之间生命周期的关联。这样,当把 pod 删除之后它使用的PV仍然存在还可以被新建的 pod 复用。
PVC 设计意图
了解 PV 后,应该如何使用它呢?
用户在使用 PV 时其实是通过 PVC为什么有了 PV 又设计了 PVC 呢主要原因是为了简化K8s用户对存储的使用方式做到职责分离。通常用户在使用存储的时候只用声明所需的存储大小以及访问模式。
访问模式是什么其实就是我要使用的存储是可以被多个node共享还是只能单node独占访问(注意是node level而不是pod level)?只读还是读写访问?用户只用关心这些东西,与存储相关的实现细节是不需要关心的。
通过 PVC 和 PV 的概念,将用户需求和实现细节解耦开,用户只用通过 PVC 声明自己的存储需求。PV是有集群管理员和存储相关团队来统一运维和管控这样的话就简化了用户使用存储的方式。可以看到PV 和 PVC 的设计其实有点像面向对象的接口与实现的关系。用户在使用功能时,只需关心用户接口,不需关心它内部复杂的实现细节。
既然 PV 是由集群管理员统一管控的,接下来就看一下 PV 这个对象是怎么产生的。
Static Volume Provisioning
第一种产生方式:静态产生方式 - 静态 Provisioning。
静态 Provisioning由集群管理员事先去规划这个集群中的用户会怎样使用存储它会先预分配一些存储也就是预先创建一些 PV然后用户在提交自己的存储需求也就是 PVC的时候K8s 内部相关组件会帮助它把 PVC 和 PV 做绑定;之后用户再通过 pod 去使用存储的时候,就可以通过 PVC 找到相应的 PV它就可以使用了。
静态产生方式有什么不足呢?可以看到,首先需要集群管理员预分配,预分配其实是很难预测用户真实需求的。举一个最简单的例子:如果用户需要的是 20G然而集群管理员在分配的时候可能有 80G 、100G 的,但没有 20G 的,这样就很难满足用户的真实需求,也会造成资源浪费。有没有更好的方式呢?
Dynamic Volume Provisioning
第二种访问方式:动态 Dynamic Provisioning。
动态供给是什么意思呢?就是说现在集群管理员不预分配 PV他写了一个模板文件这个模板文件是用来表示创建某一类型存储块存储文件存储等所需的一些参数这些参数是用户不关心的给存储本身实现有关的参数。用户只需要提交自身的存储需求也就是PVC文件并在 PVC 中指定使用的存储模板StorageClass
K8s 集群中的管控组件,会结合 PVC 和 StorageClass 的信息动态生成用户所需要的存储PV将 PVC 和 PV 进行绑定后pod 就可以使用 PV 了。通过 StorageClass 配置生成存储所需要的存储模板,再结合用户的需求动态创建 PV 对象,做到按需分配,在没有增加用户使用难度的同时也解放了集群管理员的运维工作。
用例解读
接下来看一下 Pod Volumes、PV、PVC 及 StorageClass 具体是如何使用的。
Pod Volumes 的使用
首先来看一下 Pod Volumes 的使用。如上图左侧所示,我们可以在 pod yaml 文件中的 Volumes 字段中,声明我们卷的名字以及卷的类型。声明的两个卷,一个是用的是 emptyDir另外一个用的是 hostPath这两种都是本地卷。在容器中应该怎么去使用这个卷呢它其实可以通过 volumeMounts 这个字段volumeMounts 字段里面指定的 name 其实就是它使用的哪个卷mountPath 就是容器中的挂载路径。
这里还有个 subPathsubPath 是什么?
先看一下,这两个容器都指定使用了同一个卷,就是这个 cache-volume。那么在多个容器共享同一个卷的时候为了隔离数据我们可以通过 subPath 来完成这个操作。它会在卷里面建立两个子目录,然后容器 1 往 cache 下面写的数据其实都写在子目录 cache1 了,容器 2 往 cache 写的目录,其数据最终会落在这个卷里子目录下面的 cache2 下。
还有一个 readOnly 字段readOnly 的意思其实就是只读挂载,这个挂载你往挂载点下面实际上是没有办法去写数据的。
另外emptyDir、hostPath 都是本地存储它们之间有什么细微的差别呢emptyDir 其实是在 pod 创建的过程中会临时创建的一个目录,这个目录随着 pod 删除也会被删除里面的数据会被清空掉hostPath 顾名思义,其实就是宿主机上的一个路径,在 pod 删除之后,这个目录还是存在的,它的数据也不会被丢失。这就是它们两者之间一个细微的差别。
静态 PV 使用
接下来再看一下PV 和 PVC 是怎么使用的。
先看一个静态 PV 创建方式。静态 PV 的话,首先是由管理员来创建的,管理员我们这里以 NAS就是阿里云文件存储为例。我需要先在阿里云的文件存储控制台上去创建 NAS 存储,然后把 NAS 存储的相关信息要填到 PV 对象中,这个 PV 对象预创建出来后,用户可以通过 PVC 来声明自己的存储需求,然后再去创建 pod。创建 pod 还是通过我们刚才讲解的字段把存储挂载到某一个容器中的某一个挂载点下面。
那么接下来看一下 yaml 怎么写。集群管理员首先是在云存储厂商那边先去把存储创建出来,然后把相应的信息填写到 PV 对象中。
刚刚创建的阿里云 NAS 文件存储对应的PV有个比较重要的字段capacity即创建的这个存储的大小accessModes创建出来的这个存储它的访问方式我们后面会讲解总共有几种访问方式。
然后有个 ReclaimPolicyReclaimPolicy 的意思就是:这块存储在被使用后,等它的使用方 pod 以及 PVC 被删除之后,这个 PV 是应该被删掉还是被保留呢其实就是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使用的一个过程。
动态 PV 使用
然后再看一下动态 Provisioning。动态 Provisioning 上面提到过,系统管理员不再预分配 PV而只是创建一个模板文件。
这个模板文件叫 StorageClass在StorageClass里面我们需要填的重要信息第一个是 provisionerprovisioner 是什么?它其实就是说我当时创建 PV 和对应的存储的时候,应该用哪个存储插件来去创建。
这些参数是通过k8s创建存储的时候需要指定的一些细节参数。对于这些参数用户是不需要关心的像这里 regionld、zoneld、fsType 和它的类型。ReclaimPolicy跟我们刚才讲解的 PV 里的意思是一样的,就是说动态创建出来的这块 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然后把它挂载到相应的容器中就可以使用了。
PV Spec 重要字段解析
接下来,我们讲解一下 PV 的一些重要字段:
Capacity这个很好理解就是存储对象的大小
AccessModes也是用户需要关心的就是说我使用这个 PV 的方式。它有三种使用方式。
一种是单 node 读写访问;
第二种是多个 node 只读访问,是常见的一种数据的共享方式;
第三种是多个 node 上读写访问。
用户在提交 PVC 的时候,最重要的两个字段 —— Capacity 和 AccessModes。在提交 PVC后k8s集群中的相关组件是如何去找到合适的PV呢首先它是通过为PV建立的AccessModes索引找到所有能够满足用户的 PVC 里面的 AccessModes 要求的PV list然后根据PVC的 CapacityStorageClassName, Label Selector 进一步筛选PV如果满足条件的PV有多个选择PV的size最小的accessmodes列表最短的PV也即最小适合原则。
ReclaimPolicy这个就是刚才提到的我的用户方 PV 的 PVC 在删除之后,我的 PV 应该做如何处理?常见的有三种方式。
第一种方式我们就不说了,现在 K8s 中已经不推荐使用了;
第二种方式 delete也就是说 PVC 被删除之后PV 也会被删除;
第三种方式 Retain就是保留保留之后后面这个 PV 需要管理员来手动处理。
StorageClassNameStorageClassName 这个我们刚才说了,我们动态 Provisioning 时必须指定的一个字段,就是说我们要指定到底用哪一个模板文件来生成 PV
NodeAffinity就是说我创建出来的 PV它能被哪些 node 去挂载使用,其实是有限制的。然后通过 NodeAffinity 来声明对node的限制这样其实对 使用该PV的pod调度也有限制就是说 pod 必须要调度到这些能访问 PV 的 node 上,才能使用这块 PV这个字段在我们下一讲讲解存储拓扑调度时在细说。
PV 状态流转
接下来我们看一下 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带存储的迁移就是通过这种方式。
操作演示
接下来,我会在实际的环境中给大家演示一下,静态 Provisioning 以及动态 Provisioning 具体操作方式。
静态 Provisioning 例子
静态 Provisioning 主要用的是阿里云的 NAS 文件存储;动态 Provisioning 主要用了阿里云的云盘。它们需要相应存储插件,插件我已经提前部署在我的 K8s 集群中了(csi-nasplugin*是为了在 k8s 中使用阿里云 NAS 所需的插件csi-disk*是为了在 k8s 中使用阿里云云盘所需要的插件)。
我们接下来先看一下静态 Provisioning 的 PV 的 yaml 文件。
volumeAttributes是我在阿里云nas控制台预先创建的 NAS 文件系统的相关信息,我们主要需要关心的有 capacity 为5Gi; accessModes 为多node读写访问; reclaimPolicyRetain也就是当我使用方的 PVC 被删除之后,我这个 PV 是要保留下来的以及在使用这个卷的过程中使用的driver。
然后我们把对应的 PV 创建出来:
我们看一下上图 PV 的状态,已经处在 Available也就是说它已经可以被使用了。
再创建出来 nas-pvc
我们看这个时候 PVC 已经新创建出来了而且也已经和我们上面创建的PV绑定到一起了。我们看一下 PVC 的 yaml 里面写的什么。
其实很简单 ,就是我需要的大小以及我需要的 accessModes。提交完之后它就与我们集群中已经存在的 PV 做匹配,匹配成功之后,它就会做 bound。
接下来我们去创建使用 nas-fs 的 pod
上图看到,这两个 Pod 都已经处在 running 状态了。
我们先看一下这个 pod yaml
pod yaml 里面声明了刚才我们创建出来的PVC对象然后把它挂载到nas-container容器中的 /data 下面。我们这个 pod 是通过前面课程中讲解 deployment 创建两个副本,通过反亲和性,将两个副本调度在不同的 node 上面。
上图我们可以看一下两个Pod所在的宿主机是不一样的。
如下图所示我们登陆到第一个上面findmnt 看一下它的挂载信息,这个其实就挂载在我声明的 nas-fs 上,那我们再在下面 touch 个 test.test.test 文件,我们也会登陆到另外一个容器看一下,它有没有被共享。
我们退出再登陆另外一个 pod刚才登陆的是第一个现在登陆第二个
如下图所示:我们也 findmnt 一下,可以看到,这两个 pod 的远程挂载路径一样,也就是说我们用的是同一个 NAS PV我们再看一下刚才创建出来的那个是否存在。
可以看到这个也是存在的就说明这两个运行在不同node上的 pod 共享了同一个 nas 存储。
接下来我们看一下把两个 pod 删掉之后的情况。先删Pod接着再删一下对应的 PVC(K8s内部对pvc对象由保护机制在删除pvc对象时如果发现有pod在使用pvcpvc是删除不掉的),这个可能要稍等一下。
看一下下图对应的 PVC 是不是已经被删掉了。
上图显示,它已经被删掉了。再看一下,刚才的 nas PV 还是在的,它的状态是处在 Released 状态,也就是说刚才使用它的 PVC 已经被删掉了,然后它被 released 了。又因为我们 RECLAIN POLICY 是 Retain所以它这个 PV 是被保留下来的。
动态 Provisioning 例子
接下来我们来看第二个例子,动态 Provisioning 的例子。我们先把保留下来的 PV 手动删掉,可以看到集群中没有 PV了。接下来演示一下动态 Provisioning。
首先,先去创建一个生成 PV 的模板文件,也就是 storageclass。看一下 storageclass 里面的内容,其实很简单。
如上图所示,我事先指定的是我要创建存储的卷插件(阿里云云盘插件,由阿里云团队开发)这个我们已经提前部署好了我们可以看到parameters部分是创建存储所需要的一些参数但是用户不需要关心这些信息然后是 reclaimPolicy也就是说通过这个 storageclass 创建出来的 PV 在给绑定到一起的 PVC 删除之后,它是要保留还是要删除。
如上图所示:现在这个集群中是没有 PV 的,我们动态提交一个 PVC 文件,先看一下它的 PVC 文件。它的 accessModes-ReadWriteOnce(因为阿里云云盘其实只能是单node读写的所以我们声明这样的方式它的存储大小需求是 30G它的 storageClassName 是 csi-disk就是我们刚才创建的 storageclass也就是说它指定要通过这个模板去生成 PV。
这个 PVC 此时正处在 pending 状态,这就说明它对应的 PV 还在创建过程中。
稍过一会,我们看到已经有一个新的 PV 生成,这个 PV 其实就是根据我们提交的 PVC 以及 PVC 里面指定的storageclass 动态生成的。之后k8s会将生成的 PV 以及我们提交的 PVC就是这个 disk PVC 做绑定,之后我们就可以通过创建 pod 来使用了。
再看一下 pod yaml
pod yaml 很简单,也是通过 PVC 声明,表明使用这个 PVC。然后是挂载点下面我们可以创建看一下。
如下图所示:我们可以大概看一下 Events首先被调度器调度调度完之后接下来会有个 attachdetach controller它会去做 disk的attach操作就是把我们对应的 PV 挂载到调度器调度的 node 上然后Pod对应的容器才能启动启动容器才能使用对应的盘。
接下来我会把 PVC 删掉看一下PV 会不会根据我们的 reclaimPolicy 随之删掉呢?我们先看一下,这个时候 PVC 还是存在的,对应的 PV 也是存在的。
然后删一下 PVC删完之后再看一下我们的 PV 也被删了,也就是说根据 reclaimPolicy我们在删除 PVC 的同时PV 也会被删除掉。
我们的演示部分就到这里了。
架构设计
PV 和 PVC 的处理流程
我们接下来看一下 K8s 中的 PV 和 PVC 体系的完整处理流程。我首先看一下这张图的右下部分里面提到的 csi。
csi 是什么csi 的全称是 container storage interface它是K8s社区后面对存储插件实现(out of tree)的官方推荐方式。csi 的实现大体可以分为两部分:
第一部分是由k8s社区驱动实现的通用的部分像我们这张图中的 csi-provisioner和 csi-attacher controller
另外一种是由云存储厂商实践的,对接云存储厂商的 OpenApi主要是实现真正的 create/delete/mount/unmount 存储的相关操作对应到上图中的csi-controller-server和csi-node-server。
接下来看一下,当用户提交 yaml 之后k8s内部的处理流程。用户在提交 PVCyaml 的时候,首先会在集群中生成一个 PVC 对象,然后 PVC 对象会被 csi-provisioner controller watch到csi-provisioner 会结合 PVC 对象以及 PVC 对象中声明的 storageClass通过 GRPC 调用 csi-controller-server然后到云存储服务这边去创建真正的存储并最终创建出来 PV 对象。最后,由集群中的 PV controller 将 PVC 和 PV 对象做 bound 之后,这个 PV 就可以被使用了。
用户在提交 pod 之后首先会被调度器调度选中某一个合适的node之后该 node 上面的 kubelet 在创建 pod 流程中会通过首先 csi-node-server 将我们之前创建的 PV 挂载到我们 pod 可以使用的路径然后kubelet开始 create && start pod中的所有container。
PV、PVC 以及通过 csi 使用存储流程
我们接下来通过另一张图来更加详细看一下我们 PV、PVC 以及通过 CSI 使用存储的完整流程。
主要分为三个阶段:
第一个阶段(Create阶段)是用户提交完 PVC由 csi-provisioner 创建存储,并生成 PV 对象,之后 PV controller 将 PVC 及生成的 PV 对象做 boundbound 之后create 阶段就完成了;
之后用户在提交 pod yaml 的时候,首先会被调度选中某一个 合适的node等 pod 的运行 node 被选出来之后,会被 AD Controller watch 到 pod 选中的 node它会去查找 pod 中使用了哪些 PV。然后它会生成一个内部的对象叫 VolumeAttachment 对象,从而去触发 csi-attacher去调用csi-controller-server 去做真正的 attache 操作attach操作调到云存储厂商OpenAPI。这个 attach 操作就是将存储 attach到 pod 将会运行的 node 上面。第二个阶段 —— attach阶段完成
然后我们接下来看第三个阶段。第三个阶段 发生在kubelet 创建 pod的过程中它在创建 pod 的过程中,首先要去做一个 mount这里的 mount 操作是为了将已经attach到这个 node 上面那块盘,进一步 mount 到 pod 可以使用的一个具体路径,之后 kubelet 才开始创建并启动容器。这就是 PV 加 PVC 创建存储以及使用存储的第三个阶段 —— mount 阶段。
总的来说,有三个阶段:第一个 create 阶段,主要是创建存储;第二个 attach 阶段,就是将那块存储挂载到 node 上面(通常为将存储load到node的/dev下面);第三个 mount 阶段,将对应的存储进一步挂载到 pod 可以使用的路径。这就是我们的 PVC、PV、已经通过CSI实现的卷从创建到使用的完整流程。
结束语
我们今天的内容大概就到这里,下一节我将为大家来分享 Volume Snapshot 以及 Volume Topology-aware Scheduling 相关的知识以及具体处理流程,谢谢大家~
本节总结(需补充)
本节课的主要内容就到此为止了,这里为大家简单总结一下。
K8s Volume是用户Pod保存业务数据的重要接口和手段
PVC和PV体系增强了K8s Volumes在多Pod共享/迁移/存储扩展等场景下的能力
PV存储的不同供给模式(static and dynamic)可以通过多种方式为集群中的Pod供给所需的存储

View File

@ -0,0 +1,292 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 应用存储和持久化数据卷:存储快照与拓扑调度(至天)
本文将主要分享以下两方面的内容:
存储快照概念、使用与工作原理;
存储拓扑调度背景、概念、使用与工作原理。
基本知识
存储快照产生背景
在使用存储时为了提高数据操作的容错性我们通常有需要对线上数据进行snapshot以及能快速restore的能力。另外当需要对线上数据进行快速的复制以及迁移等动作如进行环境的复制、数据开发等功能时都可以通过存储快照来满足需求而 K8s 中通过 CSI Snapshotter controller 来实现存储快照的功能。
存储快照用户接口-Snapshot
我们知道K8s 中通过 pvc 以及 pv 的设计体系来简化用户对存储的使用,而存储快照的设计其实是仿照 pvc & pv 体系的设计思想。当用户需要存储快照的功能时,可以通过 VolumeSnapshot 对象来声明,并指定相应的 VolumeSnapshotClass 对象,之后由集群中的相关组件动态生成存储快照以及存储快照对应的对象 VolumeSnapshotContent。如下对比图所示动态生成 VolumeSnapshotContent 和动态生成 pv 的流程是非常相似的。
存储快照用户接口-Restore
有了存储快照之后,如何将快照数据快速恢复过来呢?如下图所示:
如上所示的流程,可以借助 PVC 对象将其的 dataSource 字段指定为 VolumeSnapshot 对象。这样当 PVC 提交之后,会由集群中的相关组件找到 dataSource 所指向的存储快照数据,然后新创建对应的存储以及 pv 对象,将存储快照数据恢复到新的 pv 中这样数据就恢复回来了这就是存储快照的restore用法。
Topolopy-含义
首先了解一下拓扑是什么意思:这里所说的拓扑是 K8s 集群中为管理的 nodes 划分的一种“位置”关系,意思为:可以通过在 node 的 labels 信息里面填写某一个 node 属于某一个拓扑。
常见的有三种,这三种在使用时经常会遇到的:
第一种,在使用云存储服务的时候,经常会遇到 region也就是地区的概念在 K8s 中常通过 label failure-domain.beta.kubernetes.io/region 来标识。这个是为了标识单个 K8s 集群管理的跨 region 的 nodes 到底属于哪个地区;
第二种,比较常用的是可用区,也就是 available zone在 K8s 中常通过 label failure-domain.beta.kubernetes.io/zone 来标识。这个是为了标识单个 K8s 集群管理的跨 zone 的 nodes 到底属于哪个可用区;
第三种,是 hostname就是单机维度是拓扑域为 node 范围,在 K8s 中常通过 label kubernetes.io/hostname 来标识,这个在文章的最后讲 local pv 的时候,会再详细描述。
上面讲到的三个拓扑是比较常用的,而拓扑其实是可以自己定义的。可以定义一个字符串来表示一个拓扑域,这个 key 所对应的值其实就是拓扑域下不同的拓扑位置。
举个例子:可以用 rack 也就是机房中的机架这个纬度来做一个拓扑域。这样就可以将不同机架 (rack) 上面的机器标记为不同的拓扑位置,也就是说可以将不同机架上机器的位置关系通过 rack 这个纬度来标识。属于 rack1 上的机器node label 中都添加 rack 的标识,它的 value 就标识成 rack1即 rack=rack1另外一组机架上的机器可以标识为 rack=rack2这样就可以通过机架的纬度就来区分来 K8s 中的 node 所处的位置。
接下来就一起来看看拓扑在 K8s 存储中的使用。
存储拓扑调度产生背景
上一节课我们说过K8s 中通过 PV 的 PVC 体系将存储资源和计算资源分开管理了。如果创建出来的 PV有”访问位置”的限制也就是说它通过 nodeAffinity 来指定哪些 node 可以访问这个 PV。为什么会有这个访问位置的限制
因为在 K8s 中创建 pod 的流程和创建 PV 的流程,其实可以认为是并行进行的,这样的话,就没有办法来保证 pod 最终运行的 node 是能访问到 有位置限制的 PV 对应的存储,最终导致 pod 没法正常运行。这里来举两个经典的例子:
首先来看一下 Local PV 的例子Local PV 是将一个 node 上的本地存储封装为 PV通过使用 PV 的方式来访问本地存储。为什么会有 Local PV 的需求呢?简单来说,刚开始使用 PV 或 PVC 体系的时候,主要是用来针对分布式存储的,分布式存储依赖于网络,如果某些业务对 I/O 的性能要求非常高,可能通过网络访问分布式存储没办法满足它的性能需求。这个时候需要使用本地存储,刨除了网络的 overhead性能往往会比较高。但是用本地存储也是有坏处的分布式存储可以通过多副本来保证高可用但本地存储就需要业务自己用类似 Raft 协议来实现多副本高可用。
接下来看一下 Local PV 场景可能如果没有对PV做“访问位置”的限制会遇到什么问题
当用户在提交完 PVC 的时候K8s PV controller可能绑定的是 node2 上面的 PV。但是真正使用这个 PV 的 pod在被调度的时候有可能调度在 node1 上,最终导致这个 pod 在起来的时候没办法去使用这块存储,因为 pod 真实情况是要使用 node2 上面的存储。
第二个(如果不对 PV 做“访问位置”的限制会出问题的)场景:
如果搭建的 K8s 集群管理的 nodes 分布在单个区域多个可用区内。在创建动态存储的时候,创建出来的存储属于可用区 2但之后在提交使用该存储的 pod它可能会被调度到可用区 1 了,那就可能没办法使用这块存储。因此像阿里云的云盘,也就是块存储,当前不能跨可用区使用,如果创建的存储其实属于可用区 2但是 pod 运行在可用区 1就没办法使用这块存储这是第二个常见的问题场景。
接下来我们来看看 K8s 中如何通过存储拓扑调度来解决上面的问题的。
存储拓扑调度
首先总结一下之前的两个问题,它们都是 PV 在给 PVC 绑定或者动态生成 PV 的时候,我并不知道后面将使用它的 pod 将调度在哪些 node 上。但 PV 本身的使用,是对 pod 所在的 node 有拓扑位置的限制的,如 Local PV 场景是我要调度在指定的 node 上我才能使用那块 PV而对第二个问题场景就是说跨可用区的话必须要在将使用该 PV 的 pod 调度到同一个可用区的 node 上才能使用阿里云云盘服务,那 K8s 中怎样去解决这个问题呢?
简单来说,在 K8s 中将 PV 和 PVC 的 binding 操作和动态创建 PV 的操作做了 delaydelay 到 pod 调度结果出来之后,再去做这两个操作。这样的话有什么好处?
首先,如果要是所要使用的 PV 是预分配的,如 Local PV其实使用这块 PV 的 pod 它对应的 PVC 其实还没有做绑定,就可以通过调度器在调度的过程中,结合 pod 的计算资源需求(如 cpu/mem) 以及 pod 的 PVC 需求,选择的 node 既要满足计算资源的需求又要 pod 使用的 pvc 要能 binding 的 pv 的 nodeaffinity 限制;
其次对动态生成 PV 的场景其实就相当于是如果知道 pod 运行的 node 之后,就可以根据 node 上记录的拓扑信息来动态的创建这个 PV也就是保证新创建出来的 PV 的拓扑位置与运行的 node 所在的拓扑位置是一致的,如上面所述的阿里云云盘的例子,既然知道 pod 要运行到可用区 1那之后创建存储时指定在可用区 1 创建即可。
为了实现上面所说的延迟绑定和延迟创建 PV需要在 K8s 中的改动涉及到的相关组件有三个:
PV Controller 也就是 persistent volume controller它需要支持延迟 Binding 这个操作。
另一个是动态生成 PV 的组件,如果 pod 调度结果出来之后,它要根据 pod 的拓扑信息来去动态的创建 PV。
第三组件,也是最重要的一个改动点就是 kube-scheduler。在为 pod 选择 node 节点的时候,它不仅要考虑 pod 对 CPU/MEM 的计算资源的需求,它还要考虑这个 pod 对存储的需求,也就是根据它的 PVC它要先去看一下当前要选择的 node能否满足能和这个 PVC 能匹配的 PV 的 nodeAffinity或者是动态生成 PV 的过程,它要根据 StorageClass 中指定的拓扑限制来 check 当前的 node 是不是满足这个拓扑限制,这样就能保证调度器最终选择出来的 node 就能满足存储本身对拓扑的限制。
这就是存储拓扑调度的相关知识。
用例解读
接下来通过 yaml 用例来解读一下第一部分的基本知识。
Volume Snapshot/Restore示例
下面来看一下存储快照如何使用:首先需要集群管理员,在集群中创建 VolumeSnapshotClass 对象VolumeSnapshotClass 中一个重要字段就是 Snapshot它是指定真正创建存储快照所使用的卷插件这个卷插件是需要提前部署的稍后再说这个卷插件。
接下来用户他如果要做真正的存储快照,需要声明一个 VolumeSnapshotClassVolumeSnapshotClass 首先它要指定的是 VolumeSnapshotClassName接着它要指定的一个非常重要的字段就是 source这个 source 其实就是指定快照的数据源是啥。这个地方指定 name 为 disk-pvc也就是说通过这个 pvc 对象来创建存储快照。提交这个 VolumeSnapshot 对象之后,集群中的相关组件它会找到这个 PVC 对应的 PV 存储,对这个 PV 存储做一次快照。
有了存储快照之后,那接下来怎么去用存储快照恢复数据呢?这个其实也很简单,通过声明一个新的 PVC 对象并在它的 spec 下面的 DataSource 中来声明我的数据源来自于哪个 VolumeSnapshot这里指定的是 disk-snapshot 对象,当我这个 PVC 提交之后,集群中的相关组件,它会动态生成新的 PV 存储,这个新的 PV 存储中的数据就来源于这个 Snapshot 之前做的存储快照。
Local PV 的示例
如下图看一下 Local PV 的 yaml 示例:
Local PV 大部分使用的时候都是通过静态创建的方式,也就是要先去声明 PV 对象,既然 Local PV 只能是本地访问,就需要在声明 PV 对象的,在 PV 对象中通过 nodeAffinity 来限制我这个 PV 只能在单 node 上访问,也就是给这个 PV 加上拓扑限制。如上图拓扑的 key 用 kubernetes.io/hostname 来做标记,也就是只能在 node1 访问。如果想用这个 PV你的 pod 必须要调度到 node1 上。
既然是静态创建 PV 的方式,这里为什么还需要 storageClassname 呢?前面也说了,在 Local PV 中,如果要想让它正常工作,需要用到延迟绑定特性才行,那既然是延迟绑定,当用户在写完 PVC 提交之后,即使集群中有相关的 PV 能跟它匹配,它也暂时不能做匹配,也就是说 PV controller 不能马上去做 binding这个时候你就要通过一种手段来告诉 PV controller什么情况下是不能立即做 binding。这里的 storageClass 就是为了起到这个副作用,我们可以看到 storageClass 里面的 provisioner 指定的是 no-provisioner其实就是相当于告诉 K8s 它不会去动态创建 PV它主要用到 storageclass 的 VolumeBindingMode 字段,叫 WaitForFirstConsumer可以先简单地认为它是延迟绑定。
当用户开始提交 PVC 的时候pv controller 在看到这个 pvc 的时候,它会找到相应的 storageClass发现这个 BindingMode 是延迟绑定,它就不会做任何事情。
之后当真正使用这个 pvc 的 pod在调度的时候当它恰好调度在符合 pv nodeaffinity 的 node 的上面后,这个 pod 里面所使用的 PVC 才会真正地与 PV 做绑定,这样就保证我 pod 调度到这台 node 上之后,这个 PVC 才与这个 PV 绑定,最终保证的是创建出来的 pod 能访问这块 Local PV也就是静态 Provisioning 场景下怎么去满足 PV 的拓扑限制。
限制 Dynamic Provisioning PV 拓扑示例
再看一下动态 Provisioning PV 的时候,怎么去做拓扑限制的?
动态就是指动态创建 PV 就有拓扑位置的限制,那怎么去指定?
首先在 storageclass 还是需要指定 BindingMode就是 WaitForFirstConsumer就是延迟绑定。
其次特别重要的一个字段就是 allowedTopologies限制就在这个地方。上图中可以看到拓扑限制是可用区的级别这里其实有两层意思
第一层意思就是说我在动态创建 PV 的时候,创建出来的 PV 必须是在这个可用区可以访问的;
第二层含义是因为声明的是延迟绑定,那调度器在发现使用它的 PVC 正好对应的是该 storageclass 的时候,调度 pod 就要选择位于该可用区的 nodes。
总之,就是要从两方面保证,一是动态创建出来的存储时要能被这个可用区访问的,二是我调度器在选择 node 的时候,要落在这个可用区内的,这样的话就保证我的存储和我要使用存储的这个 pod 它所对应的 node它们之间的拓扑域是在同一个拓扑域用户在写 PVC 文件的时候,写法是跟以前的写法是一样的,主要是在 storageclass 中要做一些拓扑限制。
操作演示
本节将在线上环境来演示一下前面讲解的内容。
首先来看一下我的阿里云服务器上搭建的 K8s 服务。总共有 3 个 node 节点。一个 master 节点,两个 node。其中 master 节点是不能调度 pod 的。
再看一下,我已经提前把我需要的插件已经布好了,一个是 snapshot 插件 (csi-external-snapshot),一个是动态云盘的插件 (csi-disk)。
现在开始 snapshot 的演示。首先去动态创建云盘,然后才能做 snapshot。动态创建云盘需要先创建 storageclass然后去根据 PVC 动态创建 PV然后再创建一个使用它的 pod 了。
有个以上对象,现在就可以做 snapshot 了,首先看一下做 snapshot 需要的第一个配置文件snapshotclass.yaml。
其实里面就是指定了在做存储快照的时候需要使用的插件,这个插件刚才演示了已经部署好了,就是 csi-external-snapshot-0 这个插件。
接下来创建 volume-snapshotclass 文件,创建完之后就开始了 snapshot。
然后看 snapshot.yamlVolumesnapshot 声明创建存储快照了,这个地方就指定刚才创建的那个 PVC 来做的数据源来做 snapshot那我们开始创建。
我们看一下 Snapshot 有没有创建好如下图所示content 已经在 11 秒之前创建好了。
可以看一下它里面的内容,主要看 volumesnapshotcontent 记录的一些信息,这个是我 snapshot 出来之后,它记录的就是云存储厂商那边返回给我的 snapshot 的 ID。然后是这个 snapshot 数据源,也就是刚才指定的 PVC可以通过它会找到对应的 PV。
snapshot 的演示大概就是这样,把刚才创建的 snapshot 删掉,还是通过 volumesnapshot 来删掉。然后看一下,动态创建的这个 volumesnapshotcontent 也被删掉。
接下来看一下动态 PV 创建的过程加上一些拓扑限制,首先将的 storageclass 创建出来,然后再看一下 storageclass 里面做的限制storageclass 首先还是指定它的 BindingMode 为 WaitForFirstConsumer也就是做延迟绑定然后是对它的拓扑限制我这里面在 allowedTopologies 字段中配置了一个可用区级别的限制。
来尝试创建一下的 PVCPVC 创建出来之后,理论上它应该处在 pending 状态。看一下,它现在因为它要做延迟绑定,由于现在没有使用它的 pod暂时没办法去做绑定也没办法去动态创建新的 PV。
接下来创建使用该 pvc 的 pod 看会有什么效果,看一下 podpod 也处在 pending了。
那来看一下 pod 为啥处在 pending 状态,可以看一下是调度失败了,调度失败原因:一个 node 由于 taint 不能调度,这个其实是 master另外两个 node 也是没有说是可绑定的 PV。
为什么会有两个 node 出现没有可绑定的 pv 的错误?不是动态创建么?
我们来仔细看看 storageclass 中的拓扑限制,通过上面的讲解我们知道,这里限制使用该 storageclass 创建的 PV 存储必须在可用区 cn-hangzhou-d 可访问的,而使用该存储的 pod 也必须调度到 cn-hangzhou-d 的 node 上。
那就来看一下 node 节点上有没有这个拓扑信息,如果它没有当然是不行了。
看一下第一个 node 的全量信息吧,主要找它的 labels 里面的信息,看 lables 里面的确有一个这样的 key。也就是说有一个这样的拓扑但是这指定是 cn-hangzhou-b刚才 storageclass 里面指定的是 cn-hangzhou-d。
那看一下另外的一个 node 上的这个拓扑信息写的也是 hangzhou-b但是我们那个 storageclass 里面限制是 d。
这就导致最终没办法将 pod 调度在这两个 node 上。现在我们修改一下 storageclass 中的拓扑限制,将 cn-hangzhou-d 改为 cn-hangzhou-b。
改完之后再看一下,其实就是说我动态创建出来的 PV 要能被 hangzhou-b 这个可用区访问的,使用该存储的 pod 要调度到该可用区的 node 上。把之前的 pod 删掉,让它重新被调度看一下有什么结果,好,现在这个已经调度成功了,就是已经在启动容器阶段。
说明刚才把 storageclass 它里面的对可用区的限制从 hangzhou-d 改为 hangzhou-b 之后,集群中就有两个 node它的拓扑关系是和 storageclass 里要求的拓扑关系是相匹配的,这样的话它就能保证它的 pod 是有 node 节点可调度的。上图中最后一点 Pod 已经 Running 了,说明刚才的拓扑限制改动之后可以 work 了。
处理流程
kubernetes 对 Volume Snapshot/Restore 处理流程
接下来看一下 K8s 中对存储快照与拓扑调度的具体处理流程。如下图所示:
首先来看一下存储快照的处理流程,这里来首先解释一下 csi 部分。K8s 中对存储的扩展功能都是推荐通过 csi out-of-tree 的方式来实现的。
csi 实现存储扩展主要包含两部分:
第一部分是由 K8s 社区推动实现的 csi controller 部分,也就是这里的 csi-snapshottor controller 以及 csi-provisioner controller这些主要是通用的 controller 部分;
另外一部分是由特定的云存储厂商用自身 OpenAPI 实现的不同的 csi-plugin 部分,也叫存储的 driver 部分。
两部分部件通过 unix domain socket 通信连接到一起。有这两部分,才能形成一个真正的存储扩展功能。
如上图所示,当用户提交 VolumeSnapshot 对象之后,会被 csi-snapshottor controller watch 到。之后它会通过 GPPC 调用到 csi-plugincsi-plugin 通过 OpenAPI 来真正实现存储快照的动作,等存储快照已经生成之后,会返回到 csi-snapshottor controller 中csi-snapshottor controller 会将存储快照生成的相关信息放到 VolumeSnapshotContent 对象中并将用户提交的 VolumeSnapshot 做 bound。这个 bound 其实就有点类似 PV 和 PVC 的 bound 一样。
有了存储快照,如何去使用存储快照恢复之前的数据呢?前面也说过,通过声明一个新的 PVC 对象,并且指定他的 dataSource 为 Snapshot 对象,当提交 PVC 的时候会被 csi-provisioner watch 到,之后会通过 GRPC 去创建存储。这里创建存储跟之前讲解的 csi-provisioner 有一个不太一样的地方,就是它里面还指定了 Snapshot 的 ID当去云厂商创建存储时需要多做一步操作即将之前的快照数据恢复到新创建的存储中。之后流程返回到 csi-provisioner它会将新创建的存储的相关信息写到一个新的 PV 对象中,新的 PV 对象被 PV controller watch 到它会将用户提交的 PVC 与 PV 做一个 bound之后 pod 就可以通过 PVC 来使用 Restore 出来的数据了。这是 K8s 中对存储快照的处理流程。
kubernetes 对 Volume Topology-aware Scheduling 处理流程
接下来看一下存储拓扑调度的处理流程:
第一个步骤其实就是要去声明延迟绑定,这个通过 StorageClass 来做的,上面已经阐述过,这里就不做详细描述了。
接下来看一下调度器,上图中红色部分就是调度器新加的存储拓扑调度逻辑,我们先来看一下不加红色部分时调度器的为一个 pod 选择 node 时,它的大概流程:
首先用户提交完 pod 之后,会被调度器 watch 到,它就会去首先做预选,预选就是说它会将集群中的所有 node 都来与这个 pod 它需要的资源做匹配;
如果匹配上,就相当于这个 node 可以使用,当然可能不止一个 node 可以使用,最终会选出来一批 node
然后再经过第二个阶段优选,优选就相当于我要对这些 node 做一个打分的过程,通过打分找到最匹配的一个 node
之后调度器将调度结果写到 pod 里面的 spec.nodeName 字段里面,然后会被相应的 node 上面的 kubelet watch 到,最后就开始创建 pod 的整个流程。
那现在看一下加上卷相关的调度的时候,筛选 node(第二个步骤)又是怎么做的?
先就要找到 pod 中使用的所有 PVC找到已经 bound 的 PVC以及需要延迟绑定的这些 PVC
对于已经 bound 的 PVC要 check 一下它对应的 PV 里面的 nodeAffinity 与当前 node 的拓扑是否匹配 。如果不匹配, 就说明这个 node 不能被调度。如果匹配,继续往下走,就要去看一下需要延迟绑定的 PVC
对于需要延迟绑定的 PVC。先去获取集群中存量的 PV满足 PVC 需求的,先把它全部捞出来,然后再将它们一一与当前的 node labels 上的拓扑做匹配,如果它们(存量的 PV)都不匹配,那就说明当前的存量的 PV 不能满足需求,就要进一步去看一下如果要动态创建 PV 当前 node 是否满足拓扑限制,也就是还要进一步去 check StorageClass 中的拓扑限制,如果 StorageClass 中声明的拓扑限制与当前的 node 上面已经有的 labels 里面的拓扑是相匹配的,那其实这个 node 就可以使用,如果不匹配,说明该 node 就不能被调度。
经过这上面步骤之后,就找到了所有即满足 pod 计算资源需求又满足 pod 存储资源需求的所有 nodes。
当 node 选出来之后第三个步骤就是调度器内部做的一个优化。这里简单过一下就是更新经过预选和优选之后pod 的 node 信息,以及 PV 和 PVC 在 scheduler 中做的一些 cache 信息。
第四个步骤也是重要的一步,已经选择出来 node 的 Pod不管其使用的 PVC 是要 binding 已经存在的 PV还是要做动态创建 PV这时就可以开始做。由调度器来触发调度器它就会去更新 PVC 对象和 PV 对象里面的相关信息,然后去触发 PV controller 去做 binding 操作,或者是由 csi-provisioner 去做动态创建流程。
总结
通过对比 PVC&PV 体系讲解了存储快照的相关 K8s 资源对象以及使用方法;
通过两个实际场景遇到的问题引出存储拓扑调度功能必要性,以及 K8s 中如何通过拓扑调度来解决这些问题;
通过剖析 K8s 中存储快照和存储拓扑调度内部运行机制,深入理解该部分功能的工作原理。

View File

@ -0,0 +1,270 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 可观测性:你的应用健康吗?(莫源)
本次课程的分享主要围绕以下五个部分:
介绍一些整体需求的来源;
介绍在 K8s 中 Liveness 和 Readiness 的使用方式;
介绍在 K8s 中常见问题的诊断;
应用的远程调试的方式;
课程的总结与实践;
需求来源
首先来看一下,整个需求的来源:当把应用迁移到 Kubernetes 之后,要如何去保障应用的健康与稳定呢?其实很简单,可以从两个方面来进行增强:
首先是提高应用的可观测性;
第二是提高应用的可恢复能力。
从可观测性上来讲,可以在三个方面来去做增强:
首先是应用的健康状态上面,可以实时地进行观测;
第二个是可以获取应用的资源使用情况;
第三个是可以拿到应用的实时日志,进行问题的诊断与分析。
当出现了问题之后,首先要做的事情是要降低影响的范围,进行问题的调试与诊断。最后当出现问题的时候,理想的状况是:可以通过和 K8s 集成的自愈机制进行完整的恢复。
Liveness 与 Readiness
本小节为大家介绍 Liveness probe 和 eadiness probe。
应用健康状态-初识 Liveness 与 Readiness
Liveness probe 也叫就绪指针,用来判断一个 pod 是否处在就绪状态。当一个 pod 处在就绪状态的时候,它才能够对外提供相应的服务,也就是说接入层的流量才能打到相应的 pod。当这个 pod 不处在就绪状态的时候,接入层会把相应的流量从这个 pod 上面进行摘除。
来看一下简单的一个例子:
如下图其实就是一个 Readiness 就绪的一个例子:
当这个 pod 指针判断一直处在失败状态的时候,其实接入层的流量不会打到现在这个 pod 上。
当这个 pod 的状态从 FAIL 的状态转换成 success 的状态时,它才能够真实地承载这个流量。
Liveness 指针也是类似的,它是存活指针,用来判断一个 pod 是否处在存活状态。当一个 pod 处在不存活状态的时候,会出现什么事情呢?
这个时候会由上层的判断机制来判断这个 pod 是否需要被重新拉起。那如果上层配置的重启策略是 restart always 的话,那么此时这个 pod 会直接被重新拉起。
应用健康状态-使用方式
接下来看一下 Liveness 指针和 Readiness 指针的具体的用法。
探测方式
Liveness 指针和 Readiness 指针支持三种不同的探测方式:
第一种是 httpGet。它是通过发送 http Get 请求来进行判断的,当返回码是 200-399 之间的状态码时,标识这个应用是健康的;
第二种探测方式是 Exec。它是通过执行容器中的一个命令来判断当前的服务是否是正常的当命令行的返回结果是 0则标识容器是健康的
第三种探测方式是 tcpSocket。它是通过探测容器的 IP 和 Port 进行 TCP 健康检查,如果这个 TCP 的链接能够正常被建立,那么标识当前这个容器是健康的。
探测结果
从探测结果来讲主要分为三种:
第一种是 success当状态是 success 的时候,表示 container 通过了健康检查,也就是 Liveness probe 或 Readiness probe 是正常的一个状态;
第二种是 FailureFailure 表示的是这个 container 没有通过健康检查,如果没有通过健康检查的话,那么此时就会进行相应的一个处理,那在 Readiness 处理的一个方式就是通过 service。service 层将没有通过 Readiness 的 pod 进行摘除,而 Liveness 就是将这个 pod 进行重新拉起,或者是删除。
第三种状态是 UnknownUnknown 是表示说当前的执行的机制没有进行完整的一个执行,可能是因为类似像超时或者像一些脚本没有及时返回,那么此时 Readiness-probe 或 Liveness-probe 会不做任何的一个操作,会等待下一次的机制来进行检验。
那在 kubelet 里面有一个叫 ProbeManager 的组件,这个组件里面会包含 Liveness-probe 或 Readiness-probe这两个 probe 会将相应的 Liveness 诊断和 Readiness 诊断作用在 pod 之上,来实现一个具体的判断。
应用健康状态-Pod Probe Spec
下面介绍这三种方式不同的检测方式的一个 yaml 文件的使用。
首先先看一下 execexec 的使用其实非常简单。如下图所示,大家可以看到这是一个 Liveness probe它里面配置了一个 exec 的一个诊断。接下来,它又配置了一个 command 的字段,这个 command 字段里面通过 cat 一个具体的文件来判断当前 Liveness probe 的状态,当这个文件里面返回的结果是 0 时,或者说这个命令返回是 0 时,它会认为此时这个 pod 是处在健康的一个状态。
那再来看一下这个 httpGethttpGet 里面有一个字段是路径,第二个字段是 port第三个是 headers。这个地方有时需要通过类似像 header 头的一个机制做 health 的一个判断时,需要配置这个 header通常情况下可能只需要通过 health 和 port 的方式就可以了。
第三种是 tcpSockettcpSocket 的使用方式其实也比较简单,你只需要设置一个检测的端口,像这个例子里面使用的是 8080 端口,当这个 8080 端口 tcp connect 审核正常被建立的时候,那 tecSocketProbe 会认为是健康的一个状态。
此外还有如下的五个参数,是 Global 的参数。
第一个参数叫 initialDelaySeconds它表示的是说这个 pod 启动延迟多久进行一次检查,比如说现在有一个 Java 的应用,它启动的时间可能会比较长,因为涉及到 jvm 的启动,包括 Java 自身 jar 的加载。所以前期,可能有一段时间是没有办法被检测的,而这个时间又是可预期的,那这时可能要设置一下 initialDelaySeconds
第二个是 periodSeconds它表示的是检测的时间间隔正常默认的这个值是 10 秒;
第三个字段是 timeoutSeconds它表示的是检测的超时时间当超时时间之内没有检测成功那它会认为是失败的一个状态
第四个是 successThreshold它表示的是当这个 pod 从探测失败到再一次判断探测成功,所需要的阈值次数,默认情况下是 1 次,表示原本是失败的,那接下来探测这一次成功了,就会认为这个 pod 是处在一个探针状态正常的一个状态;
最后一个参数是 failureThreshold它表示的是探测失败的重试次数默认值是 3表示的是当从一个健康的状态连续探测 3 次失败那此时会判断当前这个pod的状态处在一个失败的状态。
应用健康状态-Liveness 与 Readiness 总结
接下来对 Liveness 指针和 Readiness 指针进行一个简单的总结。
介绍
Liveness 指针是存活指针,它用来判断容器是否存活、判断 pod 是否 running。如果 Liveness 指针判断容器不健康,此时会通过 kubelet 杀掉相应的 pod并根据重启策略来判断是否重启这个容器。如果默认不配置 Liveness 指针,则默认情况下认为它这个探测默认返回是成功的。
Readiness 指针用来判断这个容器是否启动完成,即 pod 的 condition 是否 ready。如果探测的一个结果是不成功那么此时它会从 pod 上 Endpoint 上移除,也就是说从接入层上面会把前一个 pod 进行摘除,直到下一次判断成功,这个 pod 才会再次挂到相应的 endpoint 之上。
检测失败
对于检测失败上面来讲 Liveness 指针是直接杀掉这个 pod而 Readiness 指针是切掉 endpoint 到这个 pod 之间的关联关系,也就是说它把这个流量从这个 pod 上面进行切掉。
适用场景
Liveness 指针适用场景是支持那些可以重新拉起的应用,而 Readiness 指针主要应对的是启动之后无法立即对外提供服务的这些应用。
注意事项
在使用 Liveness 指针和 Readiness 指针的时候有一些注意事项。因为不论是 Liveness 指针还是 Readiness 指针都需要配置合适的探测方式,以免被误操作。
第一个是调大超时的阈值,因为在容器里面执行一个 shell 脚本,它的执行时长是非常长的,平时在一台 ecs 或者在一台 vm 上执行,可能 3 秒钟返回的一个脚本在容器里面需要 30 秒钟。所以这个时间是需要在容器里面事先进行一个判断的,那如果可以调大超时阈值的方式,来防止由于容器压力比较大的时候出现偶发的超时;
第二个是调整判断的一个次数3 次的默认值其实在比较短周期的判断周期之下,不一定是最佳实践,适当调整一下判断的次数也是一个比较好的方式;
第三个是 exec如果是使用 shell 脚本的这个判断,调用时间会比较长,比较建议大家可以使用类似像一些编译性的脚本 Golang 或者一些 C 语言、C++ 编译出来的这个二进制的 binary 进行判断,那这种通常会比 shell 脚本的执行效率高 30% 到 50%
第四个是如果使用 tcpSocket 方式进行判断的时候,如果遇到了 TLS 的服务,那可能会造成后边 TLS 里面有很多这种未健全的 tcp connection那这个时候需要自己对业务场景上来判断这种的链接是否会对业务造成影响。
问题诊断
接下来给大家讲解一下在 K8s 中常见的问题诊断。
应用故障排查-了解状态机制
首先要了解一下 K8s 中的一个设计理念,就是这个状态机制。因为 K8s 是整个的一个设计是面向状态机的,它里面通过 yaml 的方式来定义的是一个期望到达的一个状态,而真正这个 yaml 在执行过程中会由各种各样的 controller来负责整体的状态之间的一个转换。
比如说上面的图,实际上是一个 Pod 的一个生命周期。刚开始它处在一个 pending 的状态,那接下来可能会转换到类似像 running也可能转换到 Unknown甚至可以转换到 failed。然后当 running 执行了一段时间之后,它可以转换到类似像 successded 或者是 failed然后当出现在 unknown 这个状态时,可能由于一些状态的恢复,它会重新恢复到 running 或者 successded 或者是 failed。
其实 K8s 整体的一个状态就是基于这种类似像状态机的一个机制进行转换的,而不同状态之间的转化都会在相应的 K8s对象上面留下来类似像 Status 或者像 Conditions 的一些字段来进行表示。
像下面这张图其实表示的就是说在一个 Pod 上面一些状态位的一些展现。
比如说在 Pod 上面有一个字段叫 Status这个 Status 表示的是 Pod 的一个聚合状态,在这个里面,这个聚合状态处在一个 pending 状态。
然后再往下看,因为一个 pod 里面有多个 container每个 container 上面又会有一个字段叫 State然后 State 的状态表示当前这个 container 的一个聚合状态。那在这个例子里面,这个聚合状态处在的是 waiting 的状态,那具体的原因是因为什么呢?是因为它的镜像没有拉下来,所以处在 waiting 的状态,是在等待这个镜像拉取。然后这个 ready 的部分呢,目前是 false因为它这个进行目前没有拉取下来所以这个 pod 不能够正常对外服务,所以此时 ready 的状态是未知的,定义为 false。如果上层的 endpoint 发现底层这个 ready 不是 true 的话,那么此时这个服务是没有办法对外服务的。
再往下是 conditioncondition 这个机制表示是说:在 K8s 里面有很多这种比较小的这个状态,而这个状态之间的聚合会变成上层的这个 Status。那在这个例子里面有几个状态第一个是 Initialized表示是不是已经初始化完成那在这个例子里面已经是初始化完成的那它走的是第二个阶段是在这个 ready 的状态。因为上面几个 container 没有拉取下来相应的镜像,所以 ready 的状态是 false。
然后再往下可以看到这个 container 是否 ready这里可以看到是 false而这个状态是 PodScheduled表示说当前这个 pod 是否是处在一个已经被调度的状态,它已经 bound 在现在这个 node 之上了,所以这个状态也是 true。
那可以通过相应的 condition 是 true 还是 false 来判断整体上方的这个状态是否是正常的一个状态。而在 K8s 里面不同的状态之间的这个转换都会发生相应的事件,而事件分为两种: 一种叫做 normal 的事件,一种是 warning 事件。大家可以看见在这第一条的事件是有个 normal 事件,然后它相应的 reason 是 scheduler表示说这个 pod 已经被默认的调度器调度到相应的一个节点之上,然后这个节点是 cn-beijing192.168.3.167 这个节点之上。
再接下来,又是一个 normal 的事件,表示说当前的这个镜像在 pull 相应的这个 image。然后再往下是一个 warning 事件,这个 warning 事件表示说 pull 这个镜像失败了。
以此类推,这个地方表示的一个状态就是说在 K8s 里面这个状态机制之间这个状态转换会产生相应的事件,而这个事件又通过类似像 normal 或者是 warning 的方式进行暴露。开发者可以通过类似像通过这个事件的机制,可以通过上层 condition Status 相应的一系列的这个字段来判断当前这个应用的具体的状态以及进行一系列的诊断。
应用故障排查-常见应用异常
本小节介绍一下常见应用的一些异常。首先是 pod 上面pod 上面可能会停留几个常见的状态。
Pod 停留在 Pending
第一个就是 pending 状态pending 表示调度器没有进行介入。此时可以通过 kubectl describe pod 来查看相应的事件,如果由于资源或者说端口占用,或者是由于 node selector 造成 pod 无法调度的时候,可以在相应的事件里面看到相应的结果,这个结果里面会表示说有多少个不满足的 node有多少是因为 CPU 不满足,有多少是由于 node 不满足,有多少是由于 tag 打标造成的不满足。
Pod 停留在 waiting
那第二个状态就是 pod 可能会停留在 waiting 的状态pod 的 states 处在 waiting 的时候,通常表示说这个 pod 的镜像没有正常拉取,原因可能是由于这个镜像是私有镜像,但是没有配置 Pod secret那第二种是说可能由于这个镜像地址是不存在的造成这个镜像拉取不下来还有一个是说这个镜像可能是一个公网的镜像造成镜像的拉取失败。
Pod 不断被拉取并且可以看到 crashing
第三种是 pod 不断被拉起,而且可以看到类似像 backoff。这个通常表示说 pod 已经被调度完成了,但是启动失败,那这个时候通常要关注的应该是这个应用自身的一个状态,并不是说配置是否正确、权限是否正确,此时需要查看的应该是 pod 的具体日志。
Pod 处在 Runing 但是没有正常工作
第四种 pod 处在 running 状态,但是没有正常对外服务。那此时比较常见的一个点就可能是由于一些非常细碎的配置,类似像有一些字段可能拼写错误,造成了 yaml 下发下去了,但是有一段没有正常地生效,从而使得这个 pod 处在 running 的状态没有对外服务,那此时可以通过 apply-validate-f pod.yaml 的方式来进行判断当前 yaml 是否是正常的,如果 yaml 没有问题,那么接下来可能要诊断配置的端口是否是正常的,以及 Liveness 或 Readiness 是否已经配置正确。
Service 无法正常的工作
最后一种就是 service 无法正常工作的时候,该怎么去判断呢?那比较常见的 service 出现问题的时候,是自己的使用上面出现了问题。因为 service 和底层的 pod 之间的关联关系是通过 selector 的方式来匹配的,也就是说 pod 上面配置了一些 label然后 service 通过 match label 的方式和这个 pod 进行相互关联。如果这个 label 配置的有问题,可能会造成这个 service 无法找到后面的 endpoint从而造成相应的 service 没有办法对外提供服务,那如果 service 出现异常的时候,第一个要看的是这个 service 后面是不是有一个真正的 endpoint其次来看这个 endpoint 是否可以对外提供正常的服务。
应用远程调试
本节讲解的是在 K8s 里面如何进行应用的远程调试,远程调试主要分为 pod 的远程调试以及 service 的远程调试。还有就是针对一些性能优化的远程调试。
应用远程调试 - Pod 远程调试
首先把一个应用部署到集群里面的时候,发现问题的时候,需要进行快速验证,或者说修改的时候,可能需要类似像登陆进这个容器来进行一些诊断。
比如说可以通过 exec 的方式进入一个 pod。像这条命令里面通过 kubectl exec-it pod-name 后面再填写一个相应的命令,比如说 /bin/bash表示希望到这个 pod 里面进入一个交互式的一个 bash。然后在 bash 里面可以做一些相应的命令,比如说修改一些配置,通过 supervisor 去重新拉起这个应用,都是可以的。
那如果指定这一个 pod 里面可能包含着多个 container这个时候该怎么办呢怎么通过 pod 来指定 container 呢?其实这个时候有一个参数叫做 -c如上图下方的命令所示。-c 后面是一个 container-name可以通过 pod 在指定 -c 到这个 container-name具体指定要进入哪个 container后面再跟上相应的具体的命令通过这种方式来实现一个多容器的命令的一个进入从而实现多容器的一个远程调试。
应用远程调试 - Servic 远程调试
那么 service 的远程调试该怎么做呢service 的远程调试其实分为两个部分:
第一个部分是说我想将一个服务暴露到远程的一个集群之内,让远程集群内的一些应用来去调用本地的一个服务,这是一条反向的一个链路;
还有一种方式是我想让这个本地服务能够去调远程的服务,那么这是一条正向的链路。
在反向列入上面有这样一个开源组件,叫做 Telepresence它可以将本地的应用代理到远程集群中的一个 service 上面,使用它的方式非常简单。
首先先将 Telepresence 的一个 Proxy 应用部署到远程的 K8s 集群里面。然后将远程单一个 deployment swap 到本地的一个 application使用的命令就是 Telepresence-swap-deployment 然后以及远程的 DEPLOYMENT_NAME。通过这种方式就可以将本地一个 application 代理到远程的 service 之上、可以将应用在远程集群里面进行本地调试,这个有兴趣的同学可以到 GitHub 上面来看一下这个插件的使用的方式。
第二个是如果本地应用需要调用远程集群的服务时候,可以通过 port-forward 的方式将远程的应用调用到本地的端口之上。比如说现在远程的里面有一个 API server这个 API server 提供了一些端口,本地在调试 Code 时候,想要直接调用这个 API server那么这时比较简单的一个方式就是通过 port-forward 的方式。
它的使用方式是 kubectl port-forward然后 service 加上远程的 service name再加上相应的 namespace后面还可以加上一些额外的参数比如说端口的一个映射通过这种机制就可以把远程的一个应用代理到本地的端口之上此时通过访问本地端口就可以访问远程的服务。
开源的调试工具 - kubectl-debug
最后再给大家介绍一个开源的调试工具,它也是 kubectl 的一个插件,叫 kubectl-debug。我们知道在 K8s 里面,底层的容器 runtime 比较常见的就是类似像 docker 或者是 containerd不论是 docker 还是 containerd它们使用的一个机制都是基于 Linux namespace 的一个方式进行虚拟化和隔离的。
通常情况下 ,并不会在镜像里面带特别多的调试工具,类似像 netstat telnet 等等这些 ,因为这个会造成应用整体非常冗余。那么如果想要调试的时候该怎么做呢?其实这个时候就可以依赖类似于像 kubectl-debug 这样一个工具。
kubectl-debug 这个工具是依赖于 Linux namespace 的方式来去做的,它可以 datash 一个 Linux namespace 到一个额外的 container然后在这个 container 里面执行任何的 debug 动作,其实和直接去 debug 这个 Linux namespace 是一致的。这里有一个简单的操作,给大家来介绍一下:
这个地方其实已经安装好了 kubectl-debug它是 kubectl 的一个插件。所以这个时候,你可以直接通过 kubectl-debug 这条命令来去诊断远程的一个 pod。像这个例子里面当执行 debug 的时候,实际上它首先会先拉取一些镜像,这个镜像里面实际上会默认带一些诊断的工具。当这个镜像启用的时候,它会把这个 debug container 进行启动。与此同时会把这个 container 和相应的你要诊断的这个 container 的 namespace 进行挂靠,也就说此时这个 container 和你是同 namespace 的,类似像网络站,或者是类似像内核的一些参数,其实都可以在这个 debug container 里面实时地进行查看。
像这个例子里面,去查看类似像 hostname、进程、netstat 等等,这些其实都是和这个需要 debug 的 pod 是在同一个环境里面的,所以你之前这三条命令可以看到里面相关的信息。
如果此时进行 logout 的话,相当于会把相应的这个 debug pod 杀掉,然后进行退出,此时对应用实际上是没有任何的影响的。那么通过这种方式可以不介入到容器里面,就可以实现相应的一个诊断。
此外它还支持额外的一些机制,比如说我给设定一些 image然后类似像这里面安装了的是 htop然后开发者可以通过这个机制来定义自己需要的这个命令行的工具并且通过这种 image 的方式设置进来。那么这个时候就可以通过这种机制来调试远程的一个 pod。
本节总结
关于 Liveness 和 Readiness 的指针。Liveness probe 就是保活指针,它是用来看 pod 是否存活的,而 Readiness probe 是就绪指针,它是判断这个 pod 是否就绪的,如果就绪了,就可以对外提供服务。这个就是 Liveness 和 Readiness 需要记住的部分;
应用诊断的三个步骤:首先 describe 相应的一个状态;然后提供状态来排查具体的一个诊断方向;最后来查看相应对象的一个 event 获取更详细的一个信息;
提供 pod 一个日志来定位应用的自身的一个状态;
远程调试的一个策略,如果想把本地的应用代理到远程集群,此时可以通过 Telepresence 这样的工具来实现,如果想把远程的应用代理到本地,然后在本地进行调用或者是调试,可以用类似像 port-forward 这种机制来实现。

View File

@ -0,0 +1,257 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 可观测性-监控与日志(莫源)
本文主要分为四个部分:
在 K8s 中监控和日志的背景信息;
在 K8s 中监控方案的演进,以及常见的监控方案的提供;
日志采集的一些细节以及常见的日志的开源系统;
课程总结,介绍一下阿里云容器服务上面监控和日志的最佳实践。
背景
监控和日志是大型分布式系统的重要基础设施,监控可以帮助开发者查看系统的运行状态,而日志可以协助问题的排查和诊断。
在 Kubernetes 中监控和日志属于生态的一部分它并不是核心组件因此大部分的能力依赖上层的云厂商的适配。Kubernetes 定义了介入的接口标准和规范,任何符合接口标准的组件都可以快速集成。
监控
监控类型
先看一下监控,从监控类型上划分,在 K8s 中可以分成四个不同的类型:
资源监控
比较常见的像 CPU、内存、网络这种资源类的一个指标通常这些指标会以数值、百分比的单位进行统计是最常见的一个监控方式。这种监控方式在常规的监控里面类似项目 zabbix telegraph这些系统都是可以做到的。
性能监控
性能监控指的就是 APM 监控,也就是说常见的一些应用性能类的监控指标的检查。通常是通过一些 Hook 的机制在虚拟机层、字节码执行层通过隐式调用,或者是在应用层显示注入,获取更深层次的一个监控指标,一般是用来应用的调优和诊断的。比较常见的类似像 jvm 或者 php 的 Zend Engine通过一些常见的 Hook 机制,拿到类似像 jvm 里面的 GC 的次数,各种内存代的一个分布以及网络连接数的一些指标,通过这种方式来进行应用的性能诊断和调优。
安全监控
安全监控主要是对安全进行的一系列的监控策略,类似像越权管理、安全漏洞扫描等等。
事件监控
事件监控是 K8s 中比较另类的一种监控方式。在上一节课中给大家介绍了在 K8s 中的一个设计理念,就是基于状态机的一个状态转换。从正常的状态转换成另一个正常的状态的时候,会发生一个 normal 的事件,而从一个正常状态转换成一个异常状态的时候,会发生一个 warning 的事件。通常情况下warning 的事件是我们比较关心的,而事件监控就是可以把 normal 的事件或者是 warning 事件离线到一个数据中心,然后通过数据中心的分析以及报警,把相应的一些异常通过像钉钉或者是短信、邮件的方式进行暴露,弥补常规监控的一些缺陷和弊端。
Kubernetes 的监控演进
在早期,也就是 1.10 以前的 K8s 版本。大家都会使用类似像 Heapster 这样的组件来去进行监控的采集Heapster 的设计原理其实也比较简单。
首先,我们在每一个 Kubernetes 上面有一个包裹好的 cadvisor这个 cadvisor 是负责数据采集的组件。当 cadvisor 把数据采集完成Kubernetes 会把 cadvisor 采集到的数据进行包裹,暴露成相应的 API。在早期的时候实际上是有三种不同的 API
第一种是 summary 接口;
第二种是 kubelet 接口;
第三种是 Prometheus 接口。
这三种接口,其实对应的数据源都是 cadvisor只是数据格式有所不同。而在 Heapster 里面,其实支持了 summary 接口和 kubelet 两种数据采集接口Heapster 会定期去每一个节点拉取数据,在自己的内存里面进行聚合,然后再暴露相应的 service供上层的消费者进行使用。在 K8s 中比较常见的消费者,类似像 dashboard或者是 HPA-Controller它通过调用 service 获取相应的监控数据,来实现相应的弹性伸缩,以及监控数据的一个展示。
这个是以前的一个数据消费链路,这条消费链路看上去很清晰,也没有太多的一个问题,那为什么 Kubernetes 会将 Heapster 放弃掉而转换到 metrics-service 呢?其实这个主要的一个动力来源是由于 Heapster 在做监控数据接口的标准化。为什么要做监控数据接口标准化呢?
第一点在于客户的需求是千变万化的,比如说今天用 Heapster 进行了基础数据的一个资源采集,那明天的时候,我想在应用里面暴露在线人数的一个数据接口,放到自己的接口系统里进行数据的一个展现,以及类似像 HPA 的一个数据消费。那这个场景在 Heapster 下能不能做呢?答案是不可以的,所以这就是 Heapster 自身扩展性的弊端;
第二点是 Heapster 里面为了保证数据的离线能力,提供了很多的 sink而这个 sink 包含了类似像 influxdb、sls、钉钉等等一系列 sink。这个 sink 主要做的是把数据采集下来,并且把这个数据离线走,然后很多客户会用 influxdb 做这个数据离线,在 influxdb 上去接入类似像 grafana 监控数据的一个可视化的软件,来实践监控数据的可视化。
但是后来社区发现,这些 sink 很多时候都是没有人来维护的。这也导致整个 Heapster 的项目有很多的 bug这个 bug 一直存留在社区里面,是没有人修复的,这个也是会给社区的项目的活跃度包括项目的稳定性带来了很多的挑战。
基于这两点原因K8s 把 Heapster 进行了 break 掉,然后做了一个精简版的监控采集组件,叫做 metrics-server。
上图是 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 改动最大的一个地方。
Kubernetes 的监控接口标准
在 K8s 里面针对于监控,有三种不同的接口标准。它将监控的数据消费能力进行了标准化和解耦,实现了一个与社区的融合,社区里面主要分为三类。
第一类 Resource Metrice
对应的接口是 metrics.k8s.io主要的实现就是 metrics-server它提供的是资源的监控比较常见的是节点级别、pod 级别、namespace 级别、class 级别。这类的监控指标都可以通过 metrics.k8s.io 这个接口获取到。
第二类 Custom Metrics
对应的 API 是 custom.metrics.k8s.io主要的实现是 Prometheus。它提供的是资源监控和自定义监控资源监控和上面的资源监控其实是有覆盖关系的而这个自定义监控指的是比如应用上面想暴露一个类似像在线人数或者说调用后面的这个数据库的 MySQL 的慢查询。这些其实都是可以在应用层做自己的定义的,然后并通过标准的 Prometheus 的 client暴露出相应的 metrics然后再被 Prometheus 进行采集。
而这类的接口一旦采集上来也是可以通过类似像 custom.metrics.k8s.io 这样一个接口的标准来进行数据消费的,也就是说现在如果以这种方式接入的 Prometheus那你就可以通过 custom.metrics.k8s.io 这个接口来进行 HPA进行数据消费。
第三类 External Metrics
External Metrics 其实是比较特殊的一类,因为我们知道 K8s 现在已经成为了云原生接口的一个实现标准。很多时候在云上打交道的是云服务,比如说在一个应用里面用到了前面的是消息队列,后面的是 RBS 数据库。那有时在进行数据消费的时候,同时需要去消费一些云产品的监控指标,类似像消息队列中消息的数目,或者是接入层 SLB 的 connection 数目SLB 上层的 200 个请求数目等等,这些监控指标。
那怎么去消费呢?也是在 K8s 里面实现了一个标准,就是 external.metrics.k8s.io。主要的实现厂商就是各个云厂商的 provider通过这个 provider 可以通过云资源的监控指标。在阿里云上面也实现了阿里巴巴 cloud metrics adapter 用来提供这个标准的 external.metrics.k8s.io 的一个实现。
Promethues - 开源社区的监控“标准”
接下来我们来看一个比较常见的开源社区里面的监控方案,就是 Prometheus。Prometheus 为什么说是开源社区的监控标准呢?
一是因为首先 Prometheus 是 CNCF 云原生社区的一个毕业项目。然后第二个是现在有越来越多的开源项目都以 Prometheus 作为监控标准,类似说我们比较常见的 Spark、Tensorflow、Flink 这些项目,其实它都有标准的 Prometheus 的采集接口。
第二个是对于类似像比较常见的一些数据库、中间件这类的项目,它都有相应的 Prometheus 采集客户端。类似像 ETCD、zookeeper、MySQL 或者说 PostgreSQL这些其实都有相应的这个 Prometheus 的接口,如果没有的,社区里面也会有相应的 exporter 进行接口的一个实现。
那我们先来看一下 Prometheus 整个的大致一个结构。
上图是 Prometheus 采集的数据链路,它主要可以分为三种不同的数据采集链路。
第一种,是这个 push 的方式,就是通过 pushgateway 进行数据采集,然后数据线到 pushgateway然后 Prometheus 再通过 pull 的方式去 pushgateway 去拉数据。这种采集方式主要应对的场景就是你的这个任务可能是比较短暂的,比如说我们知道 Prometheus最常见的采集方式是拉模式那带来一个问题就是一旦你的数据声明周期短于数据的采集周期比如我采集周期是 30s而我这个任务可能运行 15s 就完了。这种场景之下,可能会造成有些数据漏采。对于这种场景最简单的一个做法就是先通过 pushgateway先把你的 metrics push下来然后再通过 pull 的方式从 pushgateway 去拉数据,通过这种方式可以做到,短时间的不丢作业任务。
第二种是标准的 pull 模式,它是直接通过拉模式去对应的数据的任务上面去拉取数据。
第三种是 Prometheus on Prometheus就是可以通过另一个 Prometheus 来去同步数据到这个 Prometheus。
这是三种 Prometheus 中的采集方式。那从数据源上面除了标准的静态配置Prometheus 也支持 service discovery。也就是说可以通过一些服务发现的机制动态地去发现一些采集对象。在 K8s 里面比较常见的是可以有 Kubernetes 的这种动态发现机制,只需要配置一些 annotation它就可以自动地来配置采集任务来进行数据采集是非常方便的。
在报警上面Prometheus 提供了一个外置组件叫 Alentmanager它可以将相应的报警信息通过邮件或者短信的方式进行数据的一个告警。在数据消费上面可以通过上层的 API clients可以通过 web UI可以通过 Grafana 进行数据的展现和数据的消费。
总结起来 Prometheus 有如下五个特点:
第一个特点就是简介强大的接入标准,开发者只需要实现 Prometheus Client 这样一个接口标准,就可以直接实现数据的一个采集;
第二种就是多种的数据采集、离线的方式。可以通过 push 的方式、 pull 的方式、Prometheus on Prometheus的方式来进行数据的采集和离线
第三种就是和 K8s 的兼容;
第四种就是丰富的插件机制与生态;
第五个是 Prometheus Operator 的一个助力Prometheus Operator 可能是目前我们见到的所有 Operator 里面做的最复杂的,但是它里面也是把 Prometheus 这种动态能力做到淋漓尽致的一个 Operator如果在 K8s 里面使用 Prometheus比较推荐大家使用 Prometheus Operator 的方式来去进行部署和运维。
kube-eventer - Kubernetes 事件离线工具
最后,我们给大家介绍一个 K8s 中的事件离线工具叫做 kube-eventer。kube-eventer 是阿里云容器服务开源出的一个组件,它可以将 K8s 里面,类似像 pod eventer、node eventer、核心组件的 eventer、crd 的 eventer 等等一系列的 eventer通过 API sever 的这个 watch 机制离线到类似像 SLS、Dingtalk、kafka、InfluxDB然后通过这种离线的机制进行一个时间的审计、监控和告警我们现在已经把这个项目开源到 GitHub 上了,大家有兴趣的话可以来看一下这个项目。
那上面这张图其实就是 Dingtalk 的一个报警图。可以看见里面有一个 warning 的事件,这个事件是在 kube-system namespace 之下,具体的这个 pod大致的一个原因是这个 pod 重启失败了,然后大致 reason 就是 backoff然后具体发生事件是什么时间。可以通过这个信息来做到一个 Checkups。
日志
日志的场景
接下来给大家来介绍一下在 K8s 里面日志的一个部分。首先我们来看一下日志的场景,日志在 K8s 里面主要分为四个大的场景:
主机内核的日志
第一个是主机内核的日志,主机内核日志可以协助开发者进行一些常见的问题与诊断,比如说网栈的异常,类似像我们的 iptables mark它可以看到有 controller table 这样的一些 message
第二个是驱动异常,比较常见的是一些网络方案里面有的时候可能会出现驱动异常,或者说是类似 GPU 的一些场景,驱动异常可能是比较常见的一些错误;
第三个就是文件系统异常,在早期 docker 还不是很成熟的场景之下overlayfs 或者是 AUFS实际上是会经常出现问题的。在这些出现问题后开发者是没有太好的办法来去进行监控和诊断的。这一部分其实是可以主机内核日志里面来查看到一些异常
再往下是影响节点的一些异常,比如说内核里面的一些 kernel panic或者是一些 OOM这些也会在主机日志里面有相应的一些反映。
Runtime 的日志
第二个是 runtime 的日志,比较常见的是 Docker 的一些日志,我们可以通过 docker 的日志来排查类似像删除一些 Pod Hang 这一系列的问题。
核心组件的日志
第三个是核心组件的日志,在 K8s 里面核心组件包含了类似像一些外置的中间件,类似像 etcd或者像一些内置的组件类似像 API server、kube-scheduler、controller-manger、kubelet 等等这一系列的组件。而这些组件的日志可以帮我们来看到整个 K8s 集群里面管控面的一个资源的使用量,然后以及目前运行的一个状态是否有一些异常。
还有的就是类似像一些核心的中间件,如 Ingress 这种网络中间件,它可以帮我们来看到整个的一个接入层的一个流量,通过 Ingress 的日志,可以做到一个很好的接入层的一个应用分析。
部署应用的日志
最后是部署应用的日志,可以通过应用的日志来查看业务层的一个状态。比如说可以看业务层有没有 500 的请求?有没有一些 panic有没有一些异常的错误的访问那这些其实都可以通过应用日志来进行查看的。
日志的采集
首先我们来看一下日志采集,从采集位置是哪个划分,需要支持如下三种:
首先是宿主机文件,这种场景比较常见的是说我的这个容器里面,通过类似像 volume把日志文件写到了宿主机之上。通过宿主机的日志轮转的策略进行日志的轮转然后再通过我的宿主机上的这个 agent 进行采集;
第二种是容器内有日志文件,那这种常见方式怎么处理呢,比较常见的一个方式是说我通过一个 Sidecar 的 streaming 的 container转写到 stdout通过 stdout 写到相应的 log-file然后再通过本地的一个日志轮转然后以及外部的一个 agent 采集;
第三种我们直接写到 stdout这种比较常见的一个策略第一种就是直接我拿这个 agent 去采集到远端,第二种我直接通过类似像一些 sls 的标准 API 采集到远端。
那社区里面其实比较推荐的是使用 **Fluentd **的一个采集方案Fluentd 是在每一个节点上面都会起相应的 agent然后这个 agent 会把数据汇集到一个 Fluentd 的一个 server这个 server 里面可以将数据离线到相应的类似像 elasticsearch然后再通过 kibana 做展现;或者是离线到 influxdb然后通过 Grafana 做展现。这个其实是社区里目前比较推荐的一个做法。
总结
最后给大家做一下今天课程的总结,以及给大家介绍一下在阿里云上面监控和日志的最佳实践。在课程开始的时候,给大家介绍了监控和日志并不属于 K8s 里面的核心组件,而大部分是定义了一个标准的一个接口方式,然后通过上层的这个云厂商进行各自的一个适配。
阿里云容器服务监控体系
监控体系组件介绍
首先,我先给大家来介绍一下在阿里云容器服务里面的监控体系,这张图实际上是监控的一个大图。
右侧的四个产品是和监控日志相关比较紧密的四个产品:
sls
第一个是 SLS就是日志服务那刚才我们已经提到了在 K8s 里面日志分为很多种不同的采集,比如说有核心组件的日志、接入层的日志、还有应用的日志等等。在阿里云容器服务里面,可以通过 API server 采集到审计的日志,然后可以通过类似像 service mesh 或者 ingress controller 采集到接入层的日志,然后以及相应的应用层采集到应用的日志。
有了这条数据链路之后,其实还不够。因为数据链路只是帮我们做到了一个数据的离线,我们还需要做上层的数据的展现和分析。比如说像审计,可以通过审计日志来看到今天有多少操作、有多少变更、有没有攻击、系统有没有异常。这些都可以通过审计的 Dashboard 来查看。
ARMS
第二个就是应用的一个性能监控。性能监控上面,可以通过这个 ARMS 这样的产品来去进行查看。ARMS 目前支持的 JAVA、PHP 两种语言,可以通过 ARMS 来做应用的一个性能诊断和问题的一个调优。
AHAS
第三个是比较特殊的叫 AHAS。AHAS 是一个架构感知的监控,我们知道在 K8s 里面,很多时候都是通过一些微服的架构进行部署的。微服带来的问题就是组件会变的非常多,组件的副本处也会变的很多。这会带来一个在拓扑管理上面的一个复杂性。
如果我们想要看一个应用在 K8s 中流量的一个走向或者是针对流量异常的一个排查其实没有一个很好的可视化是很复杂的。AHAS 的一个作用就是通过网络栈的一个监控,可以绘制出整个 K8s 中应用的一个拓扑关系,然后以及相应的资源监控和网络的带宽监控、流量的监控,以及异常事件的一个诊断。任何如果有架构拓扑感知的一个层面,来实现另一种的监控解决方案。
Cloud Monitor
最后是 Cloud Monitor也就是基础的云监控。它可以采集标准的 Resource Metrics Monitoring来进行监控数据的一个展现可以实现 node、pod 等等监控指标的一个展现和告警。
阿里云增强的功能
这一部分是阿里云在开源上做的增强。首先是 metrics-server文章开始提到了 metrics-server 做了很多的一个精简。但是从客户的角度来讲,这个精简实际上是把一些功能做了一个裁剪,这将会带来很多不便。比如说有很多客户希望将监控数据离线到类似像 SLS 或者是 influxdb这种能力实际上用社区的版本是没有办法继续来做的这个地方阿里云继续保留了常见的维护率比较高的 sink这是第一个增强。
然后是第二个增强,因为在 K8s 里面整合的一个生态的发展并不是以同样的节奏进行演进的。比如说 Dashboard 的发布,并不是和 K8s 的大版本进行匹配的。比如 K8s 发了 1.12Dashboard 并不会也发 1.12 的版本,而是说它会根据自己的节奏来去发布,这样会造成一个结果就是说以前依赖于 Heapster 的很多的组件在升级到 metrics-server 之后就直接 break 掉,阿里云在 metrics-server 上面做了完整的 Heapster 兼容,也就是说从目前 K8s 1.7 版本一直到 K8s 1.14 版本,都可以使用阿里云的 metrics-server来做到完整的监控组件的消费的一个兼容。
还有就是 eventer 和 npd上面提到了 kube-eventer 这个组件。然后在 npd 上面,我们也做了很多额外的增强,类似像增加了很多监控和检测项,类似像 kernel Hang、npd 的一个检测、出入网的监控、snat 的一个检测。然后还有类似像 fd 的 check这些其实都是在 npd 里面的一些监控项,阿里云做了很多的增强。然后开发者可以直接部署 npd 的一个 check就可以实现节点诊断的一个告警然后并通过 eventer 离线上的 kafka 或者是 Dingtalk。
再往上是 Prometheus 生态Prometheus 生态里面,在存储层可以让开发者对接,阿里云的 HiTSDB 以及 InfluxDB然后在采集层提供了优化的 node-exporter以及一些场景化监控的 exporter类似像 Spark、TensorFlow、Argo 这类场景化的 exporter。还有就是针对于 GPU阿里云做了很多额外的增强类似于像支持 GPU 的单卡监控以及 GPU share 的监控。
然后在 Prometheus 上面,我们连同 ARMS 团队推出了托管版的 Prometheus开发者可以使用开箱即用的 helm chats不需要部署 Prometheus server就可以直接体验到 Prometheus 的一个监控采集能力。
阿里云容器服务日志体系
在日志上面,阿里云做了哪些增强呢?首先是采集方式上,做到了完整的一个兼容。可以采集 pod log 日志、核心组件日志、docker engine 日志、kernel 日志,以及类似像一些中间件的日志,都收集到 SLS。收集到 SLS 之后,我们可以通过数据离线到 OSS离线到 Max Compute做一个数据的离线和归档以及离线预算。
然后还有是对于一些数据的实时消费,我们可以到 Opensearch、可以到 E-Map、可以到 Flink来做到一个日志的搜索和上层的一个消费。在日志展现上面我们可以既对接开源的 Grafana也可以对接类似像 DataV去做数据展示实现一个完整的数据链路的采集和消费。
本文总结
今天的课程到这里就结束了,下面为大家进行要点总结:
首先主要为大家介绍了监控其中包括四种容器场景下的常见的监控方式Kubernetes 的监控演进和接口标准;两种常用的来源的监控方案;
在日志上我们主要介绍了四种不同的场景,介绍了 Fluentd 的一个采集方案;
最后向大家介绍了一下阿里云日志和监控的一个最佳实践。

View File

@ -0,0 +1,170 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 Kubernetes 网络概念及策略控制(叶磊)
本文将主要分享以下 5 方面的内容:
Kubernetes 基本网络模型;
Netns 探秘;
主流网络方案简介;
Network Policy 的用处;
思考时间。
Kubernetes 基本网络模型
本节来介绍一下 Kubernetes 对网络模型的一些想法。大家知道 Kubernetes 对于网络具体实现方案没有什么限制也没有给出特别好的参考案例。Kubernetes 对一个容器网络是否合格做出了限制,也就是 Kubernetes 的容器网络模型。可以把它归结为约法三章和四大目标。
约法三章的意思是:在评价一个容器网络或者设计容器网络的时候,它的准入条件。它需要满足哪三条? 才能认为它是一个合格的网络方案。
四大目标意思是在设计这个网络的拓扑,设计网络的具体功能的实现的时候,要去想清楚,能不能达成连通性等这几大指标。
约法三章
先来看下约法三章:
第一条:任意两个 pod 之间其实是可以直接通信的,无需经过显式地使用 NAT 来接收数据和地址的转换;
第二条node 与 pod 之间是可以直接通信的,无需使用明显的地址转换;
第三条pod 看到自己的 IP 跟别人看见它所用的IP是一样的中间不能经过转换。
后文中会讲一下我个人的理解,为什么 Kubernetes 对容器网络会有一些看起来武断的模型和要求。
四大目标
四大目标其实是在设计一个 K8s 的系统为外部世界提供服务的时候,从网络的角度要想清楚,外部世界如何一步一步连接到容器内部的应用?
外部世界和 service 之间是怎么通信的?就是有一个互联网或者是公司外部的一个用户,怎么用到 serviceservice 特指 K8s 里面的服务概念。
service 如何与它后端的 pod 通讯?
pod 和 pod 之间调用是怎么做到通信的?
最后就是 pod 内部容器与容器之间的通信?
最终要达到目标,就是外部世界可以连接到最里面,对容器提供服务。
对基本约束的解释
对基本约束,可以做出这样一些解读:因为容器的网络发展复杂性就在于它其实是寄生在 Host 网络之上的。从这个角度讲,可以把容器网络方案大体分为 Underlay/Overlay 两大派别:
Underlay 的标准是它与 Host 网络是同层的,从外在可见的一个特征就是它是不是使用了 Host 网络同样的网段、输入输出基础设备、容器的 IP 地址是不是需要与 Host 网络取得协同(来自同一个中心分配或统一划分)。这就是 Underlay
Overlay 不一样的地方就在于它并不需要从 Host 网络的 IPM 的管理的组件去申请IP一般来说它只需要跟 Host 网络不冲突,这个 IP 可以自由分配的。
为什么社区会提出 perPodperIP 这种简单武断的模型呢?我个人是觉得这样为后面的 service 管理一些服务的跟踪性能监控,带来了非常多的好处。因为一个 IP 一贯到底,对 case 或者各种不大的事情都会有很大的好处。
Netns 探秘
Netns 究竟实现了什么
下面简单讲一下Network Namespace 里面能网络实现的内核基础。狭义上来说 runC 容器技术是不依赖于任何硬件的,它的执行基础就是它的内核里面,进程的内核代表就是 task它如果不需要隔离那么用的是主机的空间 namespace并不需要特别设置的空间隔离数据结构 nsproxy-namespace proxy
相反,如果一个独立的网络 proxy或者 mount proxy里面就要填上真正的私有数据。它可以看到的数据结构如上图所示。
从感官上来看一个隔离的网络空间,它会拥有自己的网卡或者说是网络设备。网卡可能是虚拟的,也可能是物理网卡,它会拥有自己的 IP 地址、IP 表和路由表、拥有自己的协议栈状态。这里面特指就是 TCP/Ip协议栈它会有自己的status会有自己的 iptables、ipvs。
从整个感官上来讲,这就相当于拥有了一个完全独立的网络,它与主机网络是隔离的。当然协议栈的代码还是公用的,只是数据结构不相同。
Pod 与 Netns 的关系
这张图可以清晰表明 pod 里 Netns 的关系,每个 pod 都有着独立的网络空间pod net container 会共享这个网络空间。一般 K8s 会推荐选用 Loopback 接口,在 pod net container 之间进行通信,而所有的 container 通过 pod 的 IP 对外提供服务。另外对于宿主机上的 Root Netns可以把它看做一个特殊的网络空间只不过它的 Pid 是1。
主流网络方案简介
典型的容器网络实现方案
接下来简单介绍一下典型的容器网络实现方案。容器网络方案可能是 K8s 里最为百花齐放的一个领域,它有着各种各样的实现。容器网络的复杂性,其实在于它需要跟底层 Iass 层的网络做协调、需要在性能跟 IP 分配的灵活性上做一些选择,这个方案是多种多样的。
下面简单介绍几个比较主要的方案:分别是 Flannel、Calico、Canal ,最后是 WeaveNet中间的大多数方案都是采用了跟 Calico 类似的策略路由的方法。
Flannel 是一个比较大一统的方案,它提供了多种的网络 backend。不同的 backend 实现了不同的拓扑,它可以覆盖多种场景;
Calico 主要是采用了策略路由,节点之间采用 BGP 的协议,去进行路由的同步。它的特点是功能比较丰富,尤其是对 Network Point 支持比较好,大家都知道 Calico 对底层网络的要求,一般是需要 mac 地址能够直通,不能跨二层域;
当然也有一些社区的同学会把 Flannel 的优点和 Calico 的优点做一些集成。我们称之为嫁接型的创新项目 Cilium
最后讲一下 WeaveNet如果大家在使用中需要对数据做一些加密可以选择用 WeaveNet它的动态方案可以实现比较好的加密。
Flannel 方案
Flannel 方案是目前使用最为普遍的。如上图所示,可以看到一个典型的容器网方案。它首先要解决的是 container 的包如何到达 Host这里采用的是加一个 Bridge 的方式。它的 backend 其实是独立的,也就是说这个包如何离开 Host是采用哪种封装方式还是不需要封装都是可选择的。
现在来介绍三种主要的 backend
一种是用户态的 udp这种是最早期的实现
然后是内核的 Vxlan这两种都算是 overlay 的方案。Vxlan 的性能会比较好一点,但是它对内核的版本是有要求的,需要内核支持 Vxlan 的特性功能;
如果你的集群规模不够大,又处于同一个二层域,也可以选择采用 host-gw 的方式。这种方式的 backend 基本上是由一段广播路由规则来启动的,性能比较高。
Network Policy 的用处
Network Policy 基本概念
下面介绍一下 Network Policy 的概念。
刚才提到了 Kubernetes 网络的基本模型是需要 pod 之间全互联,这个将带来一些问题:可能在一个 K8s 集群里,有一些调用链之间是不会直接调用的。比如说两个部门之间,那么我希望 A 部门不要去探视到 B 部门的服务,这个时候就可以用到策略的概念。
基本上它的想法是这样的:它采用各种选择器(标签或 namespace找到一组 pod或者找到相当于通讯的两端然后通过流的特征描述来决定它们之间是不是可以联通可以理解为一个白名单的机制。
在使用 Network Policy 之前,如上图所示要注意 apiserver 需要打开一下这几个开关。另一个更重要的是我们选用的网络插件需要支持 Network Policy 的落地。大家要知道Network Policy 只是 K8s 提供的一种对象,并没有内置组件做落地实施,需要取决于你选择的容器网络方案对这个标准的支持与否及完备程度,如果你选择 Flannel 之类,它并没有真正去落地这个 Policy那么你试了这个也没有什么用。
配置实例
接下来讲一个配置的实例,或者说在设计一个 Network Policy 的时候要做哪些事情?我个人觉得需要决定三件事:
第一件事是控制对象,就像这个实例里面 spec 的部分。spec 里面通过 podSelector 或者 namespace 的 selector可以选择做特定的一组 pod 来接受我们的控制;
第二个就是对流向考虑清楚,需要控制入方向还是出方向?还是两个方向都要控制?
最重要的就是第三部分,如果要对选择出来的方向加上控制对象来对它流进行描述,具体哪一些 stream 可以放进来,或者放出去?类比这个流特征的五元组,可以通过一些选择器来决定哪一些可以作为我的远端,这是对象的选择;也可以通过 IPBlock 这种机制来得到对哪些 IP 是可以放行的;最后就是哪些协议或哪些端口。其实流特征综合起来就是一个五元组,会把特定的能够接受的流选择出来 。
本节课总结
本节内容到这里就结束了,我们简单总结一下:
在 pod 的容器网络中核心概念就是 IPIP 就是每个 pod 对外通讯的地址基础,必须内外一致,符合 K8s 的模型特征;
那么在介绍网络方案的时候,影响容器网络性能最关键的就是拓扑。要能够理解你的包端到端是怎么联通的,中间怎么从 container 到达 HostHost 出了 container 是要封装还是解封装?还是通过策略路由?最终到达对端是怎么解出来的?
容器网络选择和设计选择。如果你并不清楚你的外部网络,或者你需要一个普适性最强的方案,假设说你对 mac 是否直连不太清楚、对外部路由器的路由表能否控制也不太清楚,那么你可以选择 Flannel 利用 Vxlan 作为 backend 的这种方案。如果你确信你的网络是 2 层可直连的,你可以进行选用 Calico 或者 Flannel-Hostgw 作为一个 backend
最后就是对 Network Policy在运维和使用的时候它是一个很强大的工具可以实现对进出流的精确控制。实现的方法我们也介绍了要想清楚你要控制谁然后你的流要怎么去定义。
思考时间
最后留一些思考,大家可以想一想:
为什么接口标准化 CNI 化了,但是容器网络却没有一个很标准的实现,内置在 K8s 里面?
Network Policy 为什么没有一个标准的 controller 或者一个标准的实现,而是交给这个容器网络的 owner 来提供?
有没有可能完全不用网络设备来实现容器网络呢?考虑到现在有 RDMA 等有别于 TCP/IP 的这种方案。
在运维过程中网络问题比较多、也比较难排查,那么值不值得做一个开源工具,让它可以友好的展示从 container 到 Host 之间、Host 到 Host 之间,或者说封装及解封装之间,各个阶段的网络情况,有没有出现问题,能够快速的定位。据我所知应该现在是没有这样的工具的。
以上就是我对 K8s 容器网络的基本概念、以及 Network Policy 的一些介绍,谢谢大家的观看。

View File

@ -0,0 +1,250 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 Kubernetes Service溪恒
需求来源
为什么需要服务发现
在 K8s 集群里面会通过 pod 去部署应用,与传统的应用部署不同,传统应用部署在给定的机器上面去部署,我们知道怎么去调用别的机器的 IP 地址。但是在 K8s 集群里面应用是通过 pod 去部署的, 而 pod 生命周期是短暂的。在 pod 的生命周期过程中,比如它创建或销毁,它的 IP 地址都会发生变化,这样就不能使用传统的部署方式,不能指定 IP 去访问指定的应用。
另外在 K8s 的应用部署里,之前虽然学习了 deployment 的应用部署模式,但还是需要创建一个 pod 组,然后这些 pod 组需要提供一个统一的访问入口,以及怎么去控制流量负载均衡到这个组里面。比如说测试环境、预发环境和线上环境,其实在部署的过程中需要保持同样的一个部署模板以及访问方式。因为这样就可以用同一套应用的模板在不同的环境中直接发布。
ServiceKubernetes 中的服务返现与负载均衡
最后应用服务需要暴露到外部去访问,需要提供给外部的用户去调用的。我们上节了解到 pod 的网络跟机器不是同一个段的网络,那怎么让 pod 网络暴露到去给外部访问呢?这时就需要服务发现。
在 K8s 里面,服务发现与负载均衡就是 K8s Service。上图就是在 K8s 里 Service 的架构K8s Service 向上提供了外部网络以及 pod 网络的访问,即外部网络可以通过 service 去访问pod 网络也可以通过 K8s Service 去访问。
向下K8s 对接了另外一组 pod即可以通过 K8s Service 的方式去负载均衡到一组 pod 上面去,这样相当于解决了前面所说的复发性问题,或者提供了统一的访问入口去做服务发现,然后又可以给外部网络访问,解决不同的 pod 之间的访问,提供统一的访问地址。
用例解读
下面进行实际的一个用例解读,看 pod K8s 的 service 要怎么去声明、怎么去使用?
Service 语法
首先来看 K8s Service 的一个语法,上图实际就是 K8s 的一个声明结构。这个结构里有很多语法,跟之前所介绍的 K8s 的一些标准对象有很多相似之处。比如说标签 label 去做一些选择、selector 去做一些选择、label 去声明它的一些 label 标签等。
这里有一个新的知识点,就是定义了用于 K8s Service 服务发现的一个协议以及端口。继续来看这个模板,声明了一个名叫 my-service 的一个 K8s Service它有一个 app:my-service 的 label它选择了 app:MyApp 这样一个 label 的 pod 作为它的后端。
最后是定义的服务发现的协议以及端口,这个示例中我们定义的是 TCP 协议,端口是 80目的端口是 9376效果是访问到这个 service 80 端口会被路由到后端的 targetPort就是只要访问到这个 service 80 端口的都会负载均衡到后端 appMyApp 这种 label 的 pod 的 9376 端口。
创建和查看 Service
如何去创建刚才声明的这个 service 对象,以及它创建之后是什么样的效果呢?通过简单的命令:
kubectl apply -f service.yaml
或者是
kubectl created -f service.yaml
上面的命令可以简单地去创建这样一个 service。创建好之后可以通过
kubectl discribe service
去查看 service 创建之后的一个结果。
service 创建好之后,你可以看到它的名字是 my-service。Namespace、Label、Selector 这些都跟我们之前声明的一样,这里声明完之后会生成一个 IP 地址,这个 IP 地址就是 service 的 IP 地址,这个 IP 地址在集群里面可以被其它 pod 所访问,相当于通过这个 IP 地址提供了统一的一个 pod 的访问入口,以及服务发现。
这里还有一个 Endpoints 的属性,就是我们通过 Endpoints 可以看到:通过前面所声明的 selector 去选择了哪些 pod以及这些 pod 都是什么样一个状态?比如说通过 selector我们看到它选择了这些 pod 的一个 IP以及这些 pod 所声明的 targetPort 的一个端口。
实际的架构如上图所示。在 service 创建之后,它会在集群里面创建一个虚拟的 IP 地址以及端口,在集群里,所有的 pod 和 node 都可以通过这样一个 IP 地址和端口去访问到这个 service。这个 service 会把它选择的 pod 及其 IP 地址都挂载到后端。这样通过 service 的 IP 地址访问时,就可以负载均衡到后端这些 pod 上面去。
当 pod 的生命周期有变化时,比如说其中一个 pod 销毁service 就会自动从后端摘除这个 pod。这样实现了就算 pod 的生命周期有变化,它访问的端点是不会发生变化的。
集群内访问 Service
在集群里面,其他 pod 要怎么访问到我们所创建的这个 service 呢?有三种方式:
首先我们可以通过 service 的虚拟 IP 去访问,比如说刚创建的 my-service 这个服务,通过 kubectl get svc 或者 kubectl discribe service 都可以看到它的虚拟 IP 地址是 172.29.3.27,端口是 80然后就可以通过这个虚拟 IP 及端口在 pod 里面直接访问到这个 service 的地址。
第二种方式直接访问服务名,依靠 DNS 解析,就是同一个 namespace 里 pod 可以直接通过 service 的名字去访问到刚才所声明的这个 service。不同的 namespace 里面,我们可以通过 service 名字加“.”,然后加 service 所在的哪个 namespace 去访问这个 service例如我们直接用 curl 去访问,就是 my-service:80 就可以访问到这个 service。
第三种是通过环境变量访问,在同一个 namespace 里的 pod 启动时K8s 会把 service 的一些 IP 地址、端口,以及一些简单的配置,通过环境变量的方式放到 K8s 的 pod 里面。在 K8s pod 的容器启动之后,通过读取系统的环境变量比读取到 namespace 里面其他 service 配置的一个地址,或者是它的端口号等等。比如在集群的某一个 pod 里面,可以直接通过 curl $ 取到一个环境变量的值,比如取到 MY*SERVICE*SERVICE*HOST 就是它的一个 IP 地址MY*SERVICE 就是刚才我们声明的 MY*SERVICESERVICE*PORT 就是它的端口号,这样也可以请求到集群里面的 MY_SERVICE 这个 service。
Headless Service
service 有一个特别的形态就是 Headless Service。service 创建的时候可以指定 clusterIP:None告诉 K8s 说我不需要 clusterIP就是刚才所说的集群里面的一个虚拟 IP然后 K8s 就不会分配给这个 service 一个虚拟 IP 地址,它没有虚拟 IP 地址怎么做到负载均衡以及统一的访问入口呢?
它是这样来操作的pod 可以直接通过 service_name 用 DNS 的方式解析到所有后端 pod 的 IP 地址,通过 DNS 的 A 记录的方式会解析到所有后端的 Pod 的地址,由客户端选择一个后端的 IP 地址,这个 A 记录会随着 pod 的生命周期变化,返回的 A 记录列表也发生变化,这样就要求客户端应用要从 A 记录把所有 DNS 返回到 A 记录的列表里面 IP 地址中,客户端自己去选择一个合适的地址去访问 pod。
可以从上图看一下跟刚才我们声明的模板的区别,就是在中间加了一个 clusterIP:None即表明不需要虚拟 IP。实际效果就是集群的 pod 访问 my-service 时,会直接解析到所有的 service 对应 pod 的 IP 地址,返回给 pod然后 pod 里面自己去选择一个 IP 地址去直接访问。
向集群外暴露 Service
前面介绍的都是在集群里面 node 或者 pod 去访问 serviceservice 怎么去向外暴露呢?怎么把应用实际暴露给公网去访问呢?这里 service 也有两种类型去解决这个问题,一个是 NodePort一个是 LoadBalancer。
NodePort 的方式就是在集群的 node 上面(即集群的节点的宿主机上面)去暴露节点上的一个端口,这样相当于在节点的一个端口上面访问到之后就会再去做一层转发,转发到虚拟的 IP 地址上面,就是刚刚宿主机上面 service 虚拟 IP 地址。
LoadBalancer 类型就是在 NodePort 上面又做了一层转换,刚才所说的 NodePort 其实是集群里面每个节点上面一个端口LoadBalancer 是在所有的节点前又挂一个负载均衡。比如在阿里云上挂一个 SLB这个负载均衡会提供一个统一的入口并把所有它接触到的流量负载均衡到每一个集群节点的 node pod 上面去。然后 node pod 再转化成 ClusterIP去访问到实际的 pod 上面。
操作演示
下面进行实际操作演示,在阿里云的容器服务上面进去体验一下如何使用 K8s Service。
创建 Service
我们已经创建好了一个阿里云的容器集群,然后并且配置好本地终端到阿里云容器集群的一个连接。
首先可以通过 kubectl get cs ,可以看到我们已经正常连接到了阿里云容器服务的集群上面去。
今天将通过这些模板实际去体验阿里云服务上面去使用 K8s Service。有三个模板首先是 client就是用来模拟通过 service 去访问 K8s 的 service然后负载均衡到我们的 service 里面去声明的一组 pod 上。
K8s Service 的上面,跟刚才介绍一样,我们创建了一个 K8s Service 模板,里面 podK8s Service 会通过前端指定的 80 端口负载均衡到后端 pod 的 80 端口上面,然后 selector 选择到 run:nginx 这样标签的一些 pod 去作为它的后端。
然后去创建带有这样标签的一组 pod通过什么去创建 pod 呢?就是之前所介绍的 K8s deployment通过 deployment 我们可以轻松创建出一组 pod然后上面声明 run:nginx 这样一个label并且它有两个副本会同时跑出来两个 pod。
先创建一组 pod就是创建这个 K8s deployment通过 kubectl create -f service.yaml。这个 deployment 也创建好了,再看一下 pod 有没有创建出来。如下图看到这个 deployment 所创建的两个 pod 都已经在 running 了。通过 kubectl get pod -o wide 可以看到 IP 地址。通过 -l即 label 去做筛选run=nginx。如下图所示可以看到这两个 pod 分别是 10.0.0.135 和 10.0.0.12 这样一个 IP 地址,并且都是带 run=nginx 这个 label 的。
下面我们去创建 K8s service就是刚才介绍的通过 service 去选择这两个 pod。这个 service 已经创建好了。
根据刚才介绍,通过 kubectl describe svc 可以看到这个 service 实际的一个状态。如下图所示,刚才创建的 nginx service它的选择器是 run=nginx通过 run=nginx 这个选择器选择到后端的 pod 地址,就是刚才所看到那两个 pod 的地址10.0.0.12 和 10.0.0.135。这里可以看到 K8s 为它生成了集群里面一个虚拟 IP 地址,通过这个虚拟 IP 地址,它就可以负载均衡到后面的两个 pod 上面去。
现在去创建一个客户端的 pod 实际去感受一下如何去访问这个 K8s Service我们通过 client.yaml 去创建客户端的 podkubectl get pod 可以看到客户端 pod 已经创建好并且已经在运行中了。
通过 kubectl exec 到这个 pod 里面,进入这个 pod 去感受一下刚才所说的三种访问方式,首先可以直接去访问这个 K8s 为它生成的这个 ClusterIP就是虚拟 IP 地址,通过 curl 访问这个 IP 地址,这个 pod 里面没有装 curl。通过 wget 这个 IP 地址,输入进去测试一下。可以看到通过这个去访问到实际的 IP 地址是可以访问到后端的 nginx 上面的,这个虚拟是一个统一的入口。
第二种方式,可以通过直接 service 名字的方式去访问到这个 service。同样通过 wget访问我们刚才所创建的 service 名 nginx可以发现跟刚才看到的结果是一样的。
在不同的 namespace 时,也可以通过加上 namespace 的一个名字去访问到 service比如这里的 namespace 为 default。
最后我们介绍的访问方式里面还可以通过环境变量去访问,在这个 pod 里面直接通过执行 env 命令看一下它实际注入的环境变量的情况。看一下 nginx 的 service 的各种配置已经注册进来了。
可以通过 wget 同样去访问这样一个环境变量,然后可以访问到我们的一个 service。
介绍完这三种访问方式,再看一下如何通过 service 外部的网络去访问。我们 vim 直接修改一些刚才所创建的 service。
最后我们添加一个 type就是 LoadBalancer就是我们前面所介绍的外部访问的方式。
然后通过 kubectl apply这样就把刚刚修改的内容直接生效在所创建的 service 里面。
现在看一下 service 会有哪些变化呢?通过 kubectl get svc -o wide我们发现刚刚创建的 nginx service 多了一个 EXTERNAL-IP就是外部访问的一个 IP 地址,刚才我们所访问的都是 CLUSTER-IP就是在集群里面的一个虚拟 IP 地址。
然后现在实际去访问一下这个外部 IP 地址 39.98.21.187,感受一下如何通过 service 去暴露我们的应用服务,直接在终端里面点一下,这里可以看到我们直接通过这个应用的外部访问端点,可以访问到这个 service是不是很简单
我们最后再看一下用 service 去实现了 K8s 的服务发现,就是 service 的访问地址跟 pod 的生命周期没有关系。我们先看一下现在的 service 后面选择的是这两个 pod IP 地址。
我们现在把其中的一个 pod 删掉,通过 kubectl delete 的方式把前面一个 pod 删掉。
我们知道 deployment 会让它自动生成一个新的 pod现在看 IP 地址已经变成 137。
现在再去 describe 一下刚才的 service如下图,看到前面访问端点就是集群的 IP 地址没有发生变化,对外的 LoadBalancer 的 IP 地址也没有发生变化。在所有不影响客户端的访问情况下,后端的一个 pod IP 已经自动放到了 service 后端里面。
这样就相当于在应用的组件调用的时候可以不用关心 pod 在生命周期的一个变化。
以上就是所有演示。
架构设计
** **最后是对 K8s 设计的一个简单的分析以及实现的一些原理。
Kubernetes 服务发现架构
如上图所示K8s 服务发现以及 K8s Service 是这样整体的一个架构。
K8s 分为 master 节点和 worker 节点:
master 里面主要是 K8s 管控的内容;
worker 节点里面是实际跑用户应用的一个地方。
在 K8s master 节点里面有 APIServer就是统一管理 K8s 所有对象的地方,所有的组件都会注册到 APIServer 上面去监听这个对象的变化,比如说我们刚才的组件 pod 生命周期发生变化,这些事件。
这里面最关键的有三个组件:
一个是 Cloud Controller Manager负责去配置 LoadBalancer 的一个负载均衡器给外部去访问;
另外一个就是 Coredns就是通过 Coredns 去观测 APIServer 里面的 service 后端 pod 的一个变化,去配置 service 的 DNS 解析,实现可以通过 service 的名字直接访问到 service 的虚拟 IP或者是 Headless 类型的 Service 中的 IP 列表的解析;
然后在每个 node 里面会有 kube-proxy 这个组件,它通过监听 service 以及 pod 变化,然后实际去配置集群里面的 node pod 或者是虚拟 IP 地址的一个访问。
实际访问链路是什么样的呢?比如说从集群内部的一个 Client Pod3 去访问 Service就类似于刚才所演示的一个效果。Client Pod3 首先通过 Coredns 这里去解析出 ServiceIPCoredns 会返回给它 ServiceName 所对应的 service IP 是什么,这个 Client Pod3 就会拿这个 Service IP 去做请求,它的请求到宿主机的网络之后,就会被 kube-proxy 所配置的 iptables 或者 IPVS 去做一层拦截处理,之后去负载均衡到每一个实际的后端 pod 上面去,这样就实现了一个负载均衡以及服务发现。
对于外部的流量,比如说刚才通过公网访问的一个请求。它是通过外部的一个负载均衡器 Cloud Controller Manager 去监听 service 的变化之后,去配置的一个负载均衡器,然后转发到节点上的一个 NodePort 上面去NodePort 也会经过 kube-proxy 的一个配置的一个 iptables把 NodePort 的流量转换成 ClusterIP紧接着转换成后端的一个 pod 的 IP 地址,去做负载均衡以及服务发现。这就是整个 K8s 服务发现以及 K8s Service 整体的结构。
后续进阶
后续再进阶部分我们还会更加深入地去讲解 K8s Service 的实现原理,以及在 service 网络出问题之后,如何去诊断以及去修复的技巧。
本节总结
本节课的主要内容就到此为止了,这里为大家简单总结一下:
为什么云原生的场景需要服务发现和负载均衡,
在 Kubernetes 中如何使用 Kubernetes 的 Service 做服务发现和负载均衡
Kubernetes 集群中 Service 涉及到的组件和大概实现原理
相信经过这一节的学习大家能够通过 Kubernetes Service 将复杂的企业级应用快速并标准的编排起来。

View File

@ -0,0 +1,158 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 从 0 开始创作云原生应用(殷达)
一、 云原生应用是什么?
首先我们来思考一个问题:云原生应用是什么?
在生活中我们会和各种各样的应用打交道,有时候会在移动端上使用淘宝购物、使用高德导航,在 PC 端使用 word 编辑文稿、使用 Photoshop 处理相片······这些在各类平台上的应用程序对用户而言,大多数都只需要用户点击安装就可以使用了。那么对于云上的应用,或者说在我们今天的云上、在 Kubernetes 上的应用,是什么样子的呢?
想象一下,如果我们要把一个应用程序部署到云上都需要做什么呢?
首先我们要准备好它所需的环境,打包成一个 docker 镜像,把这个镜像放到 deployment 中。部署服务、应用所需要的账户、权限、匿名空间、秘钥信息,还有可持久化存储,这些 Kubernetes 的资源,简而言之就是要把一堆 yaml 配置文件布置在 Kubernetes 上。
虽然应用的开发者可以把这些镜像存放在公共的仓库中,然后把部署所需要的 yaml 的资源文件提供给用户,当然用户仍然需要自己去寻找这些资源文件在哪里,并把它们一一部署起来。倘若用户希望修改开发者提供的默认资源,比如说想使用更多的副本数,或者是修改服务端口,那他还需要自己去查:在这些资源文件中,哪些地方需要相应的修改。同时版本更替和维护,也会给开发者和用户造成很大的麻烦,所以可以看到最原始的这种 Kubernetes 的应用形态并不是非常的便利。
二、Helm 与 Helm Chart
Helm 是什么?
我们今天的主角Helm就在这样的环境下应用而生。开发者安装 Helm Chart 的格式,将应用所需要的资源文件都包装起来,通过模板化的方法,将一些可变的字段,比如说我们之前提到的要暴露哪一个端口、使用多少副本数量,把这些信息都暴露给用户,最后将封装好的应用包,也就是我们所说的 Helm Chart 集中存放在统一的仓库里面供用户浏览下载。
那么对于用户而言,使用 Helm 一条简单的命令就可以完成应用的安装、卸载和升级,我们可以在安装完成之后使用 kubectl 来查看一下应用安装后的 pod 的运行状态。需要注意的是,我们证明使用的是 Helm v3 的一个命令,与目前相对较为成熟的 Helm v2 是有一定的区别的。我们推荐大家在进行学习尝鲜的过程中使用这个最新的 v3 版本。
如何去创作一个 Helm 应用
站在开发者的角度上,我们应该如何去创作一个 Helm 应用呢?首先我们需要一个准备部署的镜像,这个镜像可以是一个 JAVA 程序、一个 Python 脚本,甚至是一个空的 Linux 镜像,跑几条命令。
编写 Golang 程序
如上图所示,这里我们是用 Golang 编写一个非常简单的 Hello World 的 http 服务,并且使用 docker 进行一个打包。Golang 的程序大致是长这个样子的,包括从环境变量中读取 pod、username 两个参数,在指定的端口上提取 http 服务,并返回相应的响应信息。
构建 Docker 镜像
打包用的 Dockerfile 是长这个样子的。在这里面,我们首先对 Golang 代码进行编译,然后将编译后的程序放到 0353 的一个镜像中来缩小镜像的体积。我们看到上文所说的两个环境变量只有 port 在这里面进行一个设置username 将会在后续作为应用的一个参数报告给用户,所以在这里面我们先不做设置,在 docker 构建好镜像之后,我们把这个镜像上传到仓库中,比如说我们可以上传到 Docker Helm或者是阿里云镜像仓库中。
创建空白应用
准备工作都做完之后,我们可以开始今天的重头戏,也就是构建这个 Helm Chart 了。首先我们先运行 helm create 的命令,创建一个空白的应用,那么在 create 命令运行完之后,可以看到在这个 Charts 的文件夹下出现了一系列文件和文件夹。
Charts.yaml 的文件包含了 Helm Chart 的一些基本信息;
templates 文件夹内则是存放了这个应用所需要的各种 Kubernetes 的资源;
values.yaml 则是提供了一个默认的参数配置。
Chart 配置
接下来一个一个看:
在根目录下这个 Charts.yaml 文件内声明了当前 Chart 的名称和版本和一些基本信息,那么这些信息会在 chart 被放入仓库之后,供用户浏览和检索,比如我们在这里面可以把 chart 的 description 改成 My first hello world helm chart。
在 Charts.yaml 里面,有两个和版本相关的字段,其中 version 指的是我们当前这个 chart 应用包的版本,而另外一个 appVersion 则指的是我们内部所使用的,比如说在这里面就是我们内部所使用的 Golang 这个程序,我们给它打一个 tag 这个版本。
template 文件夹
在 templates 这个文件夹下,则是存放各种部署应用所需要的 yaml 文件,比如说我们看到的 deployment 和 service。
我们当前的应用实际上只需要一个 deployment但是有的应用可能包含多个组件此时就需要在这个 templates 文件夹下放 deploymentA、deploymentB 等多个 yaml 文件。有时候我们还需要去配置一些 service account secret volume 的内容,也是在这里面去添加相应的内容。
在 templates 文件夹下,这个配置文件可以只是单纯的 Kubernetes.yaml 配置文件,也可以是配置文件的模板,比如说在这看到 deployment.yaml 文件里面,有很多以 {{ }} 包裹起来的变量,这些以 values 或者是 chart 开头的这些变量,都是从根目录下的 chart.yaml 以及 values.yaml 中获取出来的。
如上图所示,看到 replicaCount 实际上就是我们所要部署的副本数量,而 repository 则是指定了镜像的位置。我们之前在 docker 镜像构建中并没有设置 username 的环境变量,这里也是通过类似的方式暴露在了 values.yaml 里面。
Helm 在安装应用的时候,实际上会先去渲染 templates 这个文件夹下的模板文件,将所需要的变量都填入进去,然后再使用渲染后的 kubernetes.yaml 文件进行一个部署,而我们在创建这个 Helm Chart 的过程中,实际上并不需要考虑太多如何去渲染,因为 Helm 已经在客户端安装应用的时候帮我们把这些事情都完成了。
校验与打包
在我们准备好这些应用后,就可以使用 helm lint 命令,来粗略检查一下我们制作的这个 chart 有没有语法上的错误,如果没有问题的话,就可以使用 Helm package 命令,对我们的 chart 文件包进行一个打包。打包之后,我们就可以得到一个 tar 的应用包了,这个就是我们所要发布的一个应用。
安装测试
我们可以使用 Helm install 这个命令来尝试安装一下刚刚做好的应用包,然后使用 kubectl 来查看一下 pod 的运行状态,同样可以通过 port-forward 命令来把这个 pod 的端口映射到本地的端口上,这样就可以通过本地的这个 localhost 来访问到刚刚部署好的这个应用了。
参数覆盖
有的同学可能会有疑惑:虽然我们应用开发者把这些可配置的信息都暴露在了 values.yanl 里面,用户使用应用的时候,如果想要修改该怎么办呢?这个答案其实也很简单,用户只需要在 install 的时候使用这个 set 参数设置,把想要设置的参数覆盖掉就行了。
同样,如果用户编写自己的 my-values.yaml 文件,也可以把这个文件在 install 的时候设置起来,这样的话,文件中的参数会覆盖掉原有的一些参数。如果用户不想重新去 install 一个新的 app而是想要升级原来的 app他也只需要用这个 helm upgrade 的命令把这个 Helm install 这个命令替换掉就可以了。
修改 NOTES.txt
细心的同学可能会注意到,之前在执行 Helm install 的命令后,这个提示信息其实是有一些问题的,我们看一下之前所写的 deployment.yaml 这个文件,里面可以看到,两个 label 其实是有一定出入的,这个提示信息其实就是在 templates 的 notes 文件下,所以我们只需要到这个文件里面去把这里面的相应信息修改一下就可以了。
升级版本
接下来我们回到 chart.yaml 的文件中,更新一下这个 version 字段,重新做一个打包,这样我们就可以对原来部署好的应用做这样一个版本升级了。
应用上传
制作完成的这个应用应该如何和其他人做分享呢?
Helm 官方是通过了 CHARTMUSEUM 这样一个工具,用这个工具,大家可以来构建自己的 chart 仓库,但是自己维护一个 chart 成本会比较高,另外对于使用户而言,如果它使用每一个应用的开发者都有自己的一个仓库的话,那这个用户他就需要去把这些所有的仓库都加入到自己的检索列表里面,这个非常麻烦,非常不利于应用的传播和分享。
三、开放云原生应用中心
应用来源
我们团队最近推出了一个开放云原生应用中心Cloud Native App Hub。在这里面我们同步了各种非常流行的应用同时还提供了一个开发者上传自己应用的一个渠道。
提交应用
在开放云原生应用中心,应用主要是来自两个渠道:
一方面我们会定期从一些国外知名的 Helm 仓库同步 chart 资源,同时将其内部使用的一些 docker 镜像也一并做这样的替换。
另一方面,我们和 Helm 官方库一样,在 GitHub 上,也接受开发者通过 push request 的形式提交自己的应用。
感兴趣的同学可以移步我们的云原生应用中心位于 GitHub 上的 chart 仓库,仿照刚才所讲的 chart 制作流程创作自己的 chart然后提交 push request。
结束语
最后欢迎大家使用 Helm 和阿里云来进行云原生应用的开发。如果有问题或者希望深入交流讨论的同学,可以扫码加入我们的 Kubernetes 钉钉技术大群,和大牛们一起探索技术。今天的介绍就到这里,谢谢大家。

View File

@ -0,0 +1,228 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 深入解析 Linux 容器(华敏)
前两个部分就是资源隔离和限制还有容器镜像的构成,第三部分会以一个业界比较成熟的容器引擎为例去讲解一下容器引擎的构成。
容器
容器是一种轻量级的虚拟化技术,因为它跟虚拟机比起来,它少了一层 hypervisor 层。先看一下下面这张图,这张图简单描述了一个容器的启动过程。
最下面是一个磁盘,容器的镜像是存储在磁盘上面的。上层是一个容器引擎,容器引擎可以是 docker也可以是其它的容器引擎。引擎向下发一个请求比如说创建容器然后这时候它就把磁盘上面的容器镜像运行成在宿主机上的一个进程。
对于容器来说,最重要的是怎么保证这个进程所用到的资源是被隔离和被限制住的,在 Linux 内核上面是由 cgroup 和 namespace 这两个技术来保证的。接下来以 docker 为例,来详细介绍一下资源隔离和容器镜像这两部分内容。
一、资源隔离和限制
namespace
namespace 是用来做资源隔离的,在 Linux 内核上有七种 namespacedocker 中用到了前六种。第七种 cgroup namespace 在 docker 本身并没有用到,但是在 runC 实现中实现了 cgroup namespace。
我们先从头看一下:
第一个是 mout namespace。mout namespace 就是保证容器看到的文件系统的视图,是容器镜像提供的一个文件系统,也就是说它看不见宿主机上的其他文件,除了通过 -v 参数 bound 的那种模式,是可以把宿主机上面的一些目录和文件,让它在容器里面可见的。
第二个是 uts namespace这个 namespace 主要是隔离了 hostname 和 domain。
第三个是 pid namespace这个 namespace 是保证了容器的 init 进程是以 1 号进程来启动的。
第四个是网络 namespace除了容器用 host 网络这种模式之外,其他所有的网络模式都有一个自己的 network namespace 的文件。
第五个是 user namespace这个 namespace 是控制用户 UID 和 GID 在容器内部和宿主机上的一个映射,不过这个 namespace 用的比较少。
第六个是 IPC namespace这个 namespace 是控制了进程兼通信的一些东西,比方说信号量。
第七个是 cgroup namespace上图右边有两张示意图分别是表示开启和关闭 cgroup namespace。用 cgroup namespace 带来的一个好处是容器中看到的 cgroup 视图是以根的形式来呈现的,这样的话就和宿主机上面进程看到的 cgroup namespace 的一个视图方式是相同的。另外一个好处是让容器内部使用 cgroup 会变得更安全。
这里我们简单用 unshare 示例一下 namespace 创立的过程。容器中 namespace 的创建其实都是用 unshare 这个系统调用来创建的。
上图上半部分是 unshare 使用的一个例子,下半部分是我实际用 unshare 这个命令去创建的一个 pid namespace。可以看到这个 bash 进程已经是在一个新的 pid namespace 里面,然后 ps 看到这个 bash 的 pid 现在是 1说明它是一个新的 pid namespace。
cgroup
两种 cgroup 驱动
cgroup 主要是做资源限制的docker 容器有两种 cgroup 驱动:一种是 systemd 的,另外一种是 cgroupfs 的。
cgroupfs 比较好理解。比如说要限制内存是多少,要用 CPU share 为多少,其实直接把 pid 写入对应的一个 cgroup 文件,然后把对应需要限制的资源也写入相应的 memory cgroup 文件和 CPU 的 cgroup 文件就可以了。
另外一个是 systemd 的一个 cgroup 驱动。这个驱动是因为 systemd 本身可以提供一个 cgroup 管理方式。所以如果用 systemd 做 cgroup 驱动的话,所有的写 cgroup 操作都必须通过 systemd 的接口来完成,不能手动更改 cgroup 的文件。
容器中常用的 cgroup
接下来看一下容器中常用的 cgroup。Linux 内核本身是提供了很多种 cgroup但是 docker 容器用到的大概只有下面六种:
第一个是 CPUCPU 一般会去设置 cpu share 和 cupset控制 CPU 的使用率。
第二个是 memory是控制进程内存的使用量。
第三个 device device 控制了你可以在容器中看到的 device 设备。
第四个 freezer。它和第三个 cgroupdevice都是为了安全的。当你停止容器的时候freezer 会把当前的进程全部都写入 cgroup然后把所有的进程都冻结掉这样做的目的是防止你在停止的时候有进程会去做 fork。这样的话就相当于防止进程逃逸到宿主机上面去是为安全考虑。
第五个是 blkioblkio 主要是限制容器用到的磁盘的一些 IOPS 还有 bps 的速率限制。因为 cgroup 不唯一的话blkio 只能限制同步 iodocker io 是没办法限制的。
第六个是 pid cgrouppid cgroup 限制的是容器里面可以用到的最大进程数量。
不常用的 cgroup
也有一部分是 docker 容器没有用到的 cgroup。容器中常用的和不常用的这个区别是对 docker 来说的,因为对于 runC 来说,除了最下面的 rdma所有的 cgroup 其实都是在 runC 里面支持的,但是 docker 并没有开启这部分支持,所以说 docker 容器是不支持下图这些 cgroup 的。
二、容器镜像
docker images
接下来我们讲一下容器镜像,以 docker 镜像为例去讲一下容器镜像的构成。
docker 镜像是基于联合文件系统的。简单描述一下联合文件系统:大概的意思就是说,它允许文件是存放在不同的层级上面的,但是最终是可以通过一个统一的视图,看到这些层级上面的所有文件。
如上图所示,右边是从 docker 官网拿过来的容器存储的一个结构图。这张图非常形象的表明了 docker 的存储docker 存储也就是基于联合文件系统,是分层的。每一层是一个 Layer这些 Layer 由不同的文件组成,它是可以被其他镜像所复用的。可以看一下,当镜像被运行成一个容器的时候,最上层就会是一个容器的读写层。这个容器的读写层也可以通过 commit 把它变成一个镜像顶层最新的一层。
docker 镜像的存储,它的底层是基于不同的文件系统的,所以它的存储驱动也是针对不同的文件系统作为定制的,比如 AUFS、btrfs、devicemapper 还有 overlay。docker 对这些文件系统做了一些相对应的一个 graph driver 的驱动,也就是通过这些驱动把镜像存在磁盘上面。
以 overlay 为例
存储流程
接下来我们以 overlay 这个文件系统为例,看一下 docker 镜像是怎么在磁盘上进行存储的。先看一下下面这张图,简单地描述了 overlay 文件系统的工作原理 。
最下层是一个 lower 层,也就是镜像层,它是一个只读层。右上层是一个 upper 层upper 是容器的读写层upper 层采用了写实复制的机制,也就是说只有对某些文件需要进行修改的时候才会从 lower 层把这个文件拷贝上来,之后所有的修改操作都会对 upper 层的副本进行修改。
upper 并列的有一个 workdir它的作用是充当一个中间层的作用。也就是说当对 upper 层里面的副本进行修改时,会先放到 workdir然后再从 workdir 移到 upper 里面去,这个是 overlay 的工作机制。
最上面的是 mergedir是一个统一视图层。从 mergedir 里面可以看到 upper 和 lower 中所有数据的整合,然后我们 docker exec 到容器里面,看到一个文件系统其实就是 mergedir 统一视图层。
文件操作
接下来我们讲一下基于 overlay 这种存储,怎么对容器里面的文件进行操作?
先看一下读操作容器刚创建出来的时候upper 其实是空的。这个时候如果去读的话,所有数据都是从 lower 层读来的。
写操作如刚才所提到的overlay 的 upper 层有一个写实数据的机制对一些文件需要进行操作的时候overlay 会去做一个 copy up 的动作,然后会把文件从 lower 层拷贝上来,之后的一些写修改都会对这个部分进行操作。
然后看一下删除操作overlay 里面其实是没有真正的删除操作的。它所谓的删除其实是通过对文件进行标记,然后从最上层的统一视图层去看,看到这个文件如果做标记,就会让这个文件显示出来,然后就认为这个文件是被删掉的。这个标记有两种方式:
一种是 whiteout 的方式。
第二个就是通过设置目录的一个扩展权限,通过设置扩展参数来做到目录的删除。
操作步骤
接下来看一下实际用 docker run 去启动 busybox 的容器,它的 overlay 的挂载点是什么样子的?
第二张图是 mount可以看到这个容器 rootfs 的一个挂载,它是一个 overlay 的 type 作为挂载的。里面包括了 upper、lower 还有 workdir 这三个层级。
接下来看一下容器里面新文件的写入。docker exec 去创建一个新文件diff 这个从上面可以看到,是它的一个 upperdir。再看 upperdir 里面有这个文件,文件里面的内容也是 docker exec 写入的。
最后看一下最下面的是 mergedirmergedir 里面整合的 upperdir 和 lowerdir 的内容,也可以看到我们写入的数据。
三、容器引擎
containerd 容器架构详解
接下来讲一下容器引擎,我们基于 CNCF 的一个容器引擎上的 containerd来讲一下容器引擎大致的构成。下图是从 containerd 官网拿过来的一张架构图,基于这张架构图先简单介绍一下 containerd 的架构。
上图如果把它分成左右两边的话,可以认为 containerd 提供了两大功能。
第一个是对于 runtime也就是对于容器生命周期的管理左边 storage 的部分其实是对一个镜像存储的管理。containerd 会负责进行的拉取、镜像的存储。
按照水平层次来看的话:
第一层是 GRPCcontainerd 对于上层来说是通过 GRPC serve 的形式来对上层提供服务的。Metrics 这个部分主要是提供 cgroup Metrics 的一些内容。
下面这层的左边是容器镜像的一个存储,中线 images、containers 下面是 Metadata这部分 Matadata 是通过 **bootfs **存储在磁盘上面的。右边的 Tasks 是管理容器的容器结构Events 是对容器的一些操作都会有一个 Event 向上层发出,然后上层可以去订阅这个 Event由此知道容器状态发生什么变化。
最下层是 Runtimes 层,这个 Runtimes 可以从类型区分,比如说 runC 或者是安全容器之类的。
shim v1/v2 是什么
接下来讲一下 containerd 在 runtime 这边的大致架构。下面这张图是从 kata 官网拿过来的,上半部分是原图,下半部分加了一些扩展示例,基于这张图我们来看一下 containerd 在 runtime 这层的架构。
如图所示:按照从左往右的一个顺序,从上层到最终 runtime 运行起来的一个流程。
我们先看一下最左边,最左边是一个 CRI Client。一般就是 kubelet 通过 CRI 请求,向 containerd 发送请求。containerd 接收到容器的请求之后,会经过一个 containerd shim。containerd shim 是管理容器生命周期的,它主要负责两方面:
第一个是它会对 io 进行转发。
第二是它会对信号进行传递。
图的上半部分画的是安全容器,也就是 kata 的一个流程,这个就不具体展开了。下半部分,可以看到有各种各样不同的 shim。下面介绍一下 containerd shim 的架构。
一开始在 containerd 中只有一个 shim也就是蓝色框框起来的 containerd-shim。这个进程的意思是不管是 kata 容器也好、runc 容器也好、gvisor 容器也好,上面用的 shim 都是 containerd。
后面针对不同类型的 runtimecontainerd 去做了一个扩展。这个扩展是通过 shim-v2 这个 interface 去做的,也就是说只要去实现了这个 shim-v2 的 interface不同的 runtime 就可以定制不同的自己的一个 shim。比如runC 可以自己做一个 shim叫 shim-runcgvisor 可以自己做一个 shim 叫 shim-gvisor像上面 kata 也可以自己去做一个 shim-kata 的 shim。这些 shim 可以替换掉上面蓝色框的 containerd-shim。
这样做的好处有很多,举一个比较形象的例子。可以看一下 kata 这张图,它上面原先如果用 shim-v1 的话其实有三个组件,之所以有三个组件的原因是因为 kata 自身的一个限制,但是用了 shim-v2 这个架构后,三个组件可以做成一个二进制,也就是原先三个组件,现在可以变成一个 shim-kata 组件,这个可以体现出 shim-v2 的一个好处。
containerd 容器架构详解 - 容器流程示例
接下来我们以两个示例来详细解释一下容器的流程是怎么工作的,下面的两张图是基于 containerd 的架构画的一个容器的工作流程。
start 流程
先看一下容器 start 的流程:
这张图由三个部分组成:
第一个部分是容器引擎部分,容器引擎可以是 docker也可以是其它的。
两个虚线框框起来的 containerd 和 containerd-shim它们两个是属于 containerd 架构的部分。
最下面就是 container 的部分,这个部分是通过一个 runtime 去拉起的,可以认为是 shim 去操作 runC 命令创建的一个容器。
先看一下这个流程是怎么工作的,图里面也标明了 1、2、3、4。这个 1、2、3、4 就是 containerd 怎么去创建一个容器的流程。
首先它会去创建一个 matadata然后会去发请求给 task service 说要去创建容器。通过中间一系列的组件,最终把请求下发到一个 shim。containerd 和 shim 的交互其实也是通过 GRPC 来做交互的containerd 把创建请求发给 shim 之后shim 会去调用 runtime 创建一个容器出来,以上就是容器 start 的一个示例。
exec 流程
接下来看下面这张图,是怎么去 exec 一个容器的。和 start 流程非常相似,结构也大概相同,不同的部分其实就是 containerd 怎么去处理这部分流程。和上面的图一样,我也在图中标明了 1、2、3、4这些步骤就代表了 containerd 去做 exec 的一个先后顺序。
由上图可以看到exec 的操作还是发给 containerd-shim 的。对容器来说,去 start 一个容器和去 exec 一个容器,其实并没有本质的区别。
最终的一个区别无非就是,是否对容器中跑的进程做一个 namespace 的创建:
exec 的时候,需要把这个进程加入到一个已有的 namespace 里面;
start 的时候,容器进程的 namespace 是需要去专门创建。
本节总结
最后希望各位同学看完本节后,能够对 Linux 容器有更深刻的了解。这里为大家简单总结一下:
容器如何用 namespace 做资源隔离以及 cgroup 做资源限制;
简单介绍了基于 overlay 文件系统的容器镜像存储;
以 docker+containerd 为例介绍了容器引擎如何工作的。