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 为例介绍了容器引擎如何工作的。

View File

@ -0,0 +1,101 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 让我们把 DDD 的思想真正落地
你好,我是范钢,曾任航天信息首席架构师,《大话重构》一书的作者。
作为互联网及大数据转型的实践者和倡导者,我先后参与过数十个国内大型软件项目,涉及国家财政、军工、税务、医疗等领域,多次参与大型遗留系统改造、系统重构等重大项目,对系统优化与改造方面有丰富的实践经验。同时,在提供架构设计、软件重构、微服务、大数据方面的培训与企业咨询的这十余年,我服务过的企业包括中国银行、中国人民银行、华为、中兴、贝尔/诺基亚、西门子、富士康等众多知名企业。
不管是做研发,还是做培训与咨询的过程中,我发现大部分公司都面临着一系列软件研发、架构转型的难题,而很大一部分难题,我认为是可能通过 DDD 来解决的。但是在实际工作中,我听到关于 DDD 的说法往往是DDD 是很抽象难以学习无从下手DDD 听着很厉害但不能落地DDD 真的适用微服务吗? 不妨现在就请暂时放下成见和疑问,先听听我这些年关于 DDD 的故事。
我和我的 DDD
第 1 幕:我与 DDD 的美好邂逅
记得 2006 年,我怀着无比激动的心情开始研读 Eric Evans 写的《领域驱动设计》一书。这的确是本重量级的巨著,我从中学到了太多软件开发的真谛,随后也开始积极地运用在实践中。
但是,多年以后,当经历了无数软件项目的磨炼以后,我扪心自问 DDD 真正用起来了吗?没有,只学到了它的思想,但却没有按照它的方法去实践,这是为什么呢?
DDD 是软件核心复杂性的应对之道但当时都在忙着开发新项目如何快速编码开发系统、快速上线才是王道领域驱动对于客户来说太慢了。并且那个时代业务也并没有那么复杂DDD 远远发挥不出应有的优势。但是,最近几年,事情却慢慢发生了变化。
第 2 幕:令人心塞的遗留系统
2012 年,我接手了一个遗留系统改造的任务,该系统是 2002 年开发的,其间经历了大大小小数十次变更,程序已经凌乱不堪了,维护的成本也越来越高。此时,需要通过重构好好优化改造一下,但我发现有许多动辄数千行的大函数与大对象,是软件退化的重灾区,为什么会这样?
深刻思考后,很快意识到问题的根源:这是软件的业务由简单向复杂转变的必然结果。软件会随着变更而越来越复杂、代码也越来越多,这样就不能在原有的简单程序结构里塞代码了,而是要调整程序结构,该解耦的解耦,该拆分的拆分,再实现新的功能,才能保持设计质量。
但是,怎样调整呢?也许第 1 次、第 2 次、第 3 次变更,我们能想得清楚,但第 10 次、第 20 次、第 30 次变更时,我们就想不清楚了,设计开始迷失方向。怎么办? 我再次陷入了沉思……
经过几番苦苦的思索与探寻,我终于找到了阻止软件退化的钥匙,那就是 DDD。当系统经过数十次变更设计迷失方向的根源还是复杂性即业务逻辑与代码变得越来越复杂而难于理解这不是个例而是当今所有软件都必须得面对的难题。
运用 DDD当系统业务变得越来越复杂时将我们对业务的理解绘制成领域模型就可以正确地指导软件开发。当系统变更时将变更业务透过领域模型还原到真实世界再根据真实世界去变更领域模型根据领域模型的变更指导程序变更就能做出正确的设计从而低成本地持续维护一个系统。这对于如今生命周期越来越长的软件系统来说显得尤为重要。
第 3 幕:谁来拯救微服务
2015 年,互联网技术的飞速发展带给了我们无限发展的空间。越来越多的行业在思考:如何转型互联网?如何开展互联网业务?这时,一个互联网转型的利器——微服务,它恰恰能够帮助很多行业很好地应对互联网业务。于是乎,我们加入了微服务转型的滚滚洪流之中。
但是,微服务也不是银弹,它也有很多的“坑”。
当按照模块拆分微服务以后才发现,每次变更都需要修改多个微服务,不但多个团队都要变更,还要同时打包、同时升级,不仅没有降低维护成本,还使得系统的发布比过去更麻烦,真不如不用微服务。是微服务不好吗?我又陷入了沉思。
这时我才注意到 Martin Flower 在定义微服务时提到的“小而专”,很多人理解了“小”却忽略了“专”,就会带来微服务系统难于维护的糟糕境地。这里的“专”,就是要“小团队独立维护”,也就是尽量让每次的需求变更交给某个小团队独立完成,让需求变更落到某个微服务上进行变更,唯有这样才能发挥微服务的优势。
通过这样的一番解析,才发现微服务的设计真的不仅仅是一个技术架构更迭的事情,而是对原有的设计提出了更高的要求,即“微服务内高内聚,微服务间低耦合”。如何才能更好地做到这一点呢?答案还是 DDD。
我们转型微服务的重要根源之一就是系统的复杂性,即系统规模越来越大,维护越来越困难,才需要拆分微服务。然而,拆分成微服务以后,并不意味着每个微服务都是各自独立地运行,而是彼此协作地组织在一起。这就好像一个团队,规模越大越需要一些方法来组织,而 DDD 恰恰就是那个帮助我们组织微服务的实践方法。
第 4 幕DDD想说爱你不容易
2018 年,经过一番挣扎,我终于说服了开发团队开始使用 DDD在这个过程中发现要让 DDD 在团队中用得好,还需要一个支持 DDD 与微服务的技术中台。
有了这个技术中台的支持,开发团队就可以把更多的精力放到对用户业务的理解,对业务痛点的理解,快速开发用户满意的功能并快速交付上。这样,不仅编写代码减少了,技术门槛降低了,还使得日后的变更更加容易,技术更迭也更加方便。因此,我又开始苦苦求索。
很快Bob 大叔的整洁架构Clean Architecture给了我全新的思路。整洁架构最核心的是业务图中的黄色与红色部分即我们通过领域模型分析最后形成的那些 Service、Entity 与 Value Object。
然而,整洁架构最关键的设计思想是通过一系列的适配器(图中的绿色部分),将业务代码与技术框架解耦。通过这样的解耦,上层业务开发人员更专注地去开发他们的业务代码,技术门槛得到降低;底层平台架构师则更低成本地进行架构演化,不断地跟上市场与技术的更迭。唯有这样,才能跟上日益激烈的市场竞争。
图片来自 Robert C. Martin 的《架构整洁之道》
不仅如此,我在实践摸索过程中,还创新性地提出了单 Controller、通用仓库、通用工厂以及完美支持 DDD + 微服务的技术中台架构设计。通过这些设计,开发团队能够更好地将 DDD 落地到项目开发中,真正地打造出一支支理解业务、高质量开发与快速交付的团队。
这门课能让你学到什么?
在我的故事里,你有没有看到你自己的影子呢?或者是否会想起在开发中的种种困境呢?
比如:
作为微服务开发人员的你,是不是不知道如何拆分和设计?从而导致微服务的拆分不合理,使得软件维护与发布很是困难。
作为业务开发人员的你,当业务越来越复杂,特别是需要应对不断出现的新需求时,是不是会发现自己改不动了,从而导致开发的质量越来越差,交付速度也越来越慢?
作为系统架构师的你,不知道该如何通过领域建模确认和规划系统边界,抑或不知道如何搭建技术中台,支持业务的快速更迭与架构的快速演进?
所以为了带你走出目前的“窘境”,我和我的团队在使用 DDD 的过程中的实战经验,以及那些踩过的“坑”,共同构成了这个专栏的主体内容。
本专栏综合了重构、高质量软件设计与微服务,将 DDD 的实践最终落实到如何指导开发团队从现有状态逐步转型为领域驱动设计与微服务架构,并通过重构打造支持微服务、支持领域驱动的技术中台,进而实现软件开发交付速度的提升。
模块一软件复杂性的应对之道01 ~ 06通过一系列真实的案例讲解了如何通过 DDD 去应对越来越复杂的业务系统,并始终保持低成本的维护与高质量的设计。学完这一模块,你将会了解 DDD 的基本概念,以及 DDD 在实际工作中该如何应用。
模块二领域建模过程演练07 ~ 09通过一个在线订餐系统演练了 DDD 从业务建模、事件风暴,到微服务拆分、技术实现的完整开发过程,深入 DDD 的底层原理与具体实现。学完这一模块,你将可以掌握 DDD 的建模过程,以及在微服务系统中的实战运用。
模块三,支持 DDD 的微服务技术中台10 ~ 13主要讲解了如何运用整洁架构构建一个既支持 DDD又支持微服务的技术中台以及如何通过该中台实现业务的快速交付与技术的架构演化。学完这一模块你将可以以架构师的角度去架构技术中台并支持开发团队的快速交付与架构演化。
模块四项目实战演练14 ~ 15用两个实战案例去演练在微服务+人工智能、嵌入式+物联网的项目中,如何运用 DDD 进行业务建模、系统规划与设计实践的过程。这部分可以更好地指导你如何将 DDD 应用到未来的实际项目中。
模块五代码落地演示16 ~ 17为你带来 2 个落地实战的代码演示,帮助你将知识与理论落地到开发实战中。这 2 个演示代码简洁流畅,并且复用性也比较高,你可以直接拿你需要的部分来用。
讲师寄语
最后我想说的是,虽然 DDD 的学习和应用都没有那么容易,但是在这门课里,我将像你身边的一位导师一样,用我这些年的经历、经验、实践与思考,帮你尽可能在轻松高效的状态下学好这门课,并且能够更快、更有效地将 DDD 应用到你的工作中。
相信我无论你是高级架构师还是初级开发人员DDD 整体的架构方法以及其中的设计思路,都会给你的工作带来能量。

View File

@ -0,0 +1,141 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 DDD :杜绝软件退化的利器
2004 年,软件大师 Eric Evans 的不朽著作《领域驱动设计软件核心复杂性应对之道》面世从书名可以看出这是一本应对软件系统越来越复杂的方法论的图书。然而在当时中国的软件业才刚刚起步软件系统还没有那么复杂即使维护了几年软件退化了不好维护了推倒重新开发就好了。因此在过去的那么多年里真正运用领域驱动设计开发DDD的团队并不多。一套优秀的方法论因为现实阶段原因而一直不温不火。
不过,这些年随着中国软件业的快速发展,软件规模越来越大,生命周期也越来越长,推倒重新开发的风险越来越大。这时,软件团队急切需要在较低成本的状态下持续维护一个系统很多年。然而,事与愿违。随着时间的推移,程序越来越乱,维护成本越来越高,软件退化成了无数软件团队的噩梦。
这时,微服务架构成了规模化软件的解决之道。不过,微服务对设计提出了很高的要求,强调“小而专、高内聚”,否则就不能发挥出微服务的优势,甚至可能令问题更糟糕。
因此,微服务的设计,微服务的拆分都需要领域驱动设计的指导。那么,领域驱动为什么能解决软件规模化的问题呢? 我们先从问题的根源谈起,即软件退化。
软件退化的根源
最近 10 年的互联网发展,从电子商务到移动互联,再到“互联网+”与传统行业的互联网转型,是一个非常痛苦的转型过程。而近几年的人工智能与 5G 技术的发展,又会带动整个产业向着大数据与物联网发展,另一轮的技术转型已经拉开帷幕。
那么,在这个过程中,一方面会给我们带来诸多的挑战,另一方面又会给我们带来无尽的机会,它会带来更多的新兴市场、新兴产业与全新业务,给我们带来全新的发展机遇。
然而,在面对全新业务、全新增长点的时候,我们能不能把握住这样的机遇呢?我们期望能把握住,但每次回到现实,回到正在维护的系统时,却令人沮丧。我们的软件总是经历着这样的轮回,软件设计质量最高的时候是第一次设计的那个版本,当第一个版本设计上线以后就开始各种需求变更,这常常又会打乱原有的设计。
因此,需求变更一次,软件就修改一次,软件修改一次,质量就下降一次。不论第一次的设计质量有多高,软件经历不了几次变更,就进入一种低质量、难以维护的状态。进而,团队就不得不在这样的状态下,以高成本的方式不断地维护下去,维护很多年。
这时候,维护好原有的业务都非常不易,又如何再去期望未来更多的全新业务呢?比如,这是一段电商网站支付功能的设计,最初的版本设计质量还是不错的:
当第一个版本上线以后,很快就迎来了第一次变更,变更的需求是增加商品折扣功能,并且这个折扣功能还要分为限时折扣、限量折扣、某类商品的折扣、某个商品的折扣。当我们拿到这个需求时怎么做呢?很简单,增加一个 if 语句if 限时折扣就怎么怎么样if 限量折扣就怎么怎么样……代码开始膨胀了。
接着,第二次变更需要增加 VIP 会员,除了增加各种金卡、银卡的折扣,还要为会员发放各种福利,让会员享受各种特权。为了实现这些需求,我们又要在 payoff() 方法中加入更多的代码。
第三次变更增加的是支付方式,除了支付宝支付,还要增加微信支付、各种银行卡支付、各种支付平台支付,此时又要塞入一大堆代码。经过这三次变更,你可以想象现在的 payoff() 方法是什么样子了吧,变更是不是就可以结束了呢?其实不能,接着还要增加更多的秒杀、预订、闪购、众筹,以及各种返券。程序变得越来越乱而难以阅读,每次变更也变得越来越困难。
问题来了:为什么软件会退化,会随着变更而设计质量下降呢?在这个问题上,我们必须寻找到问题的根源,才能对症下药、解决问题。
要探寻软件退化的根源,先要从探寻软件的本质及其规律开始,软件的本质就是对真实世界的模拟,每个软件都能在真实世界中找到它的影子。因此,软件中业务逻辑正确与否的唯一标准就是是否与真实世界一致。如果一致,则软件是 OK 的;不一致,则用户会提 Bug、提新需求。
在这里发现了一个非常重要的线索,那就是,软件要做成什么样,既不由我们来决定,也不由用户来决定,而是由客观世界决定。用户为什么总在改需求,是因为他们也不确定客观世界的规则,只有遇到问题了他们才能想得起来。因此,对于我们来说,与其唯唯诺诺地按照用户的要求去做软件,不如主动地理解业务的基础上去分析软件,而后者会更有利于我们减少变更的成本。
那么,真实世界是怎样,我们就怎样开发软件,不就简单了吗?其实并非如此,因为真实世界是非常复杂的,要深刻理解真实世界中的这些业务逻辑是需要一个过程的。因此,我们最初只能认识真实世界中那些简单、清晰、易于理解的业务逻辑,把它们做到我们的软件里,即每个软件的第一个版本的需求总是那么清晰明了、易于设计。
然而,当我们把第一个版本的软件交付用户使用的时候,用户却会发现,还有很多不简单、不明了、不易于理解的业务逻辑没做到软件里。这在使用软件的过程中很不方便,和真实业务不一致,因此用户就会提 Bug、提新需求。
在我们不断地修复 Bug实现新需求的过程中软件的业务逻辑也会越来越接近真实世界使得我们的软件越来越专业让用户感觉越来越好用。但是在软件越来越接近真实世界的过程中业务逻辑就会变得越来越复杂软件规模也越来越庞大。
你一定有这样一个认识:简单软件有简单软件的设计,复杂软件有复杂软件的设计。
比如,现在的需求就是将用户订单按照“单价 × 数量”公式来计算应付金额,那么在一个 PaymentBus 类中增加一个 payoff() 方法即可,这样的设计没有问题。不过,如果现在的需求需要在付款的过程中计算各种折扣、各种优惠、各种返券,那么我们必然会做成一个复杂的程序结构。
但是,真实情况却不是这样的。真实情况是,起初我们拿到的需求是那个简单需求,然后在简单需求的基础上进行了设计开发。但随着软件的不断变更,软件业务逻辑变得越来越复杂,软件规模不断扩大,逐渐由一个简单软件转变成一个复杂软件。
这时,如果要保持软件设计质量不退化,就应当逐步调整软件的程序结构,逐渐由简单的程序结构转变为复杂的程序结构。如果我们总是这样做,就能始终保持软件的设计质量,不过非常遗憾的是,我们以往在维护软件的过程中却不是这样做的,而是不断地在原有简单软件的程序结构下,往 payoff() 方法中塞代码,这样做必然会造成软件的退化。
也就是说,软件退化的根源不是软件变更,软件变更只是一个诱因。如果每次软件变更时,适时地进行解耦,进行功能扩展,再实现新的功能,就能保持高质量的软件设计。但如果在每次软件变更时没有调整程序结构,而是在原有的程序结构上不断地塞代码,软件就会退化。这就是软件发展的规律,软件退化的根源。
杜绝软件退化:两顶帽子
前面谈到,要保持软件设计质量不退化,必须在每次需求变更的时候,对原有的程序结构适当地进行调整。那么应当怎样进行调整呢?还是回到前面电商网站付款功能的那个案例,看看每次需求变更应当怎样设计。
在交付第一个版本的基础上,很快第一次需求变更就到来了。第一次需求变更的内容如下。
增加商品折扣功能,该功能分为以下几种类型:
限时折扣
限量折扣
对某类商品进行折扣
对某个商品进行折扣
不折扣
以往我们拿到这个需求,就很不冷静地开始改代码,修改成了如下一段代码:
这里增加了一段 if 语句,并不是一种好的变更方式。如果每次都这样变更,那么软件必然就会退化,进入难以维护的状态。这种变更为什么就不好呢?因为它违反了“开放-封闭原则”。
开放-封闭原则OCP 分为开放原则与封闭原则两部分。
开放原则我们开发的软件系统对于功能扩展是开放的Open for Extension即当系统需求发生变更时可以对软件功能进行扩展使其满足用户新的需求。
封闭原则对软件代码的修改应当是封闭的Close for Modification即在修改软件的同时不要影响到系统原有的功能所以应当在不修改原有代码的基础上实现新的功能。也就是说在增加新功能的时候新代码与老代码应当隔离不能在同一个类、同一个方法中。
前面的设计,在实现新功能的同时,新代码与老代码在同一个类、同一个方法中了,违反了“开放-封闭原则”。怎样才能既满足“开放-封闭原则”,又能够实现新功能呢?在原有的代码上你发现什么都做不了!难道“开放-封闭原则”错了吗?
问题的关键就在于,当我们在实现新需求时,应当采用“两顶帽子”的方式进行设计,这种方式就要求在每次变更时,将变更分为两个步骤。
两顶帽子:
在不添加新功能的前提下,重构代码,调整原有程序结构,以适应新功能;
实现新的功能。
按以上案例为例,为了实现新的功能,我们在原有代码的基础上,在不添加新功能的前提下调整原有程序结构,我们抽取出了 Strategy 这样一个接口和“不折扣”这个实现类。这时,原有程序变了吗?没有。但是程序结构却变了,增加了这样一个接口,称为“可扩展点”。在这个可扩展点的基础上再实现各种折扣,既能满足“开放-封闭原则”来保证程序质量,又能够满足新的需求。当日后发生新的变更时,什么类型的折扣就修改哪个实现类,添加新的折扣类型就增加新的实现类,维护成本得到降低。
“两顶帽子”的设计方式意义重大。过去,我们每次在设计软件时总是担心日后的变更,就很不冷静地设计了很多所谓的“灵活设计”。然而,每一种“灵活设计”只能应对一种需求变更,而我们又不是先知,不知道日后会发生什么样的变更。最后的结果就是,我们期望的变更并没有发生,所做的设计都变成了摆设,它既不起什么作用,还增加了程序复杂度;我们没有期望的变更发生了,原有的程序依然不能解决新的需求,程序又被打回了原形。因此,这样的设计不能真正解决未来变更的问题,被称为“过度设计”。
有了“两顶帽子”,我们不再需要焦虑,不再需要过度设计,正确的思路应当是“活在今天的格子里做今天的事儿”,也就是为当前的需求进行设计,使其刚刚满足当前的需求。所谓的“高质量的软件设计”就是要掌握一个平衡,一方面要满足当前的需求,另一方面要让设计刚刚满足需求,从而使设计最简化、代码最少。这样做,不仅软件设计质量提高了,设计难点也得到了大幅度降低。
简而言之,保持软件设计不退化的关键在于每次需求变更的设计,只有保证每次需求变更时做出正确的设计,才能保证软件以一种良性循环的方式不断维护下去。这种正确的设计方式就是“两顶帽子”。
但是,在实践“两顶帽子”的过程中,比较困难的是第一步。在不添加新功能的前提下,如何重构代码,如何调整原有程序结构,以适应新功能,这是有难度的。很多时候,第一次变更、第二次变更、第三次变更,这些事情还能想清楚;但经历了第十次变更、第二十次变更、第三十次变更,这些事情就想不清楚了,设计开始迷失方向。
那么,有没有一种方法,让我们在第十次变更、第二十次变更、第三十次变更时,依然能够找到正确的设计呢?有,那就是“领域驱动设计”。
保持软件质量:领域驱动
前面谈到,软件的本质就是对真实世界的模拟。因此,我们会有一种想法,能不能将软件设计与真实世界对应起来,真实世界是什么样子,那么软件世界就怎么设计。如果是这样的话,那么在每次需求变更时,将变更还原到真实世界中,看看真实世界是什么样子的,根据真实世界进行变更。这样,日后不论怎么变更,经过多少轮变更,都按照这样的方法进行设计,就不会迷失方向,设计质量就可以得到保证,这就是“领域驱动设计”的思想。
那么,如何将真实世界与软件世界对应起来呢?这样的对应就包括以下三个方面的内容:
真实世界有什么事物,软件世界就有什么对象;
真实世界中这些事物都有哪些行为,软件世界中这些对象就有哪些方法;
真实世界中这些事物间都有哪些关系,软件世界中这些对象间就有什么关联。
真实世界与软件世界的对应图
在领域驱动设计中,就将以上三个对应,先做成一个领域模型,然后通过这个领域模型指导程序设计;在每次需求变更时,先将需求还原到领域模型中分析,根据领域模型背后的真实世界进行变更,然后根据领域模型的变更指导软件的变更,设计质量就可以得到提高。
总结
总之,软件发展的规律就是逐步由简单软件向复杂软件转变。简单软件有简单软件的设计,复杂软件有复杂软件的设计。因此,当软件由简单软件向复杂软件转变时,就需要通过两顶帽子适时地对程序结构进行调整,再实现新需求,只有这样才能保证软件不退化。然而,在变更的时候,如何调整代码以适应新的需求呢?
DDD 给了我们思路:在每次变更的时候,先回到领域模型,基于业务进行领域模型的变更。然后,再基于领域模型的变更,指导程序的变更。这样,不论经历多少次需求变更,始终能够保持设计质量不退化。这样的设计,才能保障系统始终在低成本的状态下,可持续地不断维护下去。

View File

@ -0,0 +1,153 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 以电商支付功能为例演练 DDD
上一讲我们花了不少篇幅讲解了软件退化的根源,以及 DDD 如何解决软件退化的问题。现在,我们以电商网站的支付功能为例,来重新演练一下基于 DDD 的软件设计及其变更的过程。
运用 DDD 进行软件设计
开发人员在最开始收到的关于用户付款功能的需求描述是这样的:
在用户下单以后,经过下单流程进入付款功能;
通过用户档案获得用户名称、地址等信息;
记录商品及其数量,并汇总付款金额;
保存订单;
通过远程调用支付接口进行支付。
以往当拿到这个需求时,开发人员往往草草设计以后就开始编码,设计质量也就不高。
而采用领域驱动的方式,在拿到新需求以后,应当先进行需求分析,设计领域模型。 按照以上业务场景,可以分析出:
该场景中有“订单”,每个订单都对应一个用户;
一个用户可以有多个用户地址,但每个订单只能有一个用户地址;
此外,一个订单对应多个订单明细,每个订单明细对应一个商品,每个商品对应一个供应商。
最后,我们对订单可以进行“下单”“付款”“查看订单状态”等操作。因此形成了以下领域模型图:
有了这样的领域模型,就可以通过该模型进行以下程序设计:
通过领域模型的指导,将“订单”分为订单 Service 与值对象,将“用户”分为用户 Service 与值对象,将“商品”分为商品 Service 与值对象……然后,在此基础上实现各自的方法。
商品折扣的需求变更
当电商网站的付款功能按照领域模型完成了第一个版本的设计后,很快就迎来了第一次需求变更,即增加折扣功能,并且该折扣功能分为限时折扣、限量折扣、某类商品的折扣、某个商品的折扣与不折扣。当我们拿到这个需求时应当怎样设计呢?很显然,在 payoff() 方法中去插入 if 语句是不 OK 的。这时,按照领域驱动设计的思想,应当将需求变更还原到领域模型中进行分析,进而根据领域模型背后的真实世界进行变更。
这是上一个版本的领域模型,现在我们要在这个模型的基础上增加折扣功能,并且还要分为限时折扣、限量折扣、某类商品的折扣等不同类型。这时,我们应当怎么分析设计呢?
首先要分析付款与折扣的关系。
付款与折扣是什么关系呢?你可能会认为折扣是在付款的过程中进行的折扣,因此就应当将折扣写到付款中。这样思考对吗?我们应当基于什么样的思想与原则来设计呢?这时,另外一个重量级的设计原则应该出场了,那就是“单一职责原则”。
单一职责原则:软件系统中的每个元素只完成自己职责范围内的事,而将其他的事交给别人去做,我只是去调用。
单一职责原则是软件设计中一个非常重要的原则,但如何正确地理解它成为一个非常关键的问题。在这句话中,准确理解的关键就在于“职责”二字,即自己职责的范围到底在哪里。以往,我们错误地理解这个“职责”就是做某一个事,与这个事情相关的所有事情都是它的职责,正因为这个错误的理解,带来了许多错误的设计,而将折扣写到付款功能中。那么,怎样才是对“职责”正确的理解呢?
“一个职责就是软件变化的一个原因”是著名的软件大师 Bob 大叔在他的《敏捷软件开发:原则、模式与实践》中的表述。但这个表述过于精简,很难深刻地理解其中的内涵,从而不能有效地提高我们的设计质量。这里我好好解读一下这句话。
先思考一下什么是高质量的代码。你可能立即会想到“低耦合、高内聚”,以及各种设计原则,但这些评价标准都太“虚”。最直接、最落地的评价标准就是,当用户提出一个需求变更时,为了实现这个变更而修改软件的成本越低,那么软件的设计质量就越高。 当来了一个需求变更时,怎样才能让修改软件的成本降低呢?如果为了实现这个需求,需要修改 3 个模块的代码,完后这 3 个模块都需要测试,其维护成本必然是“高”。那么怎样才能降到最低呢?维护 0 个模块的代码?那显然是不可能的,因此最现实的方案就是只修改 1 个模块,维护成本最低。
那么,怎样才能在每次变更的时候都只修改一个模块就能实现新需求呢?那就需要我们在平时就不断地整理代码,将那些因同一个原因而变更的代码都放在一起,而将因不同原因而变更的代码分开放,放在不同的模块、不同的类中。这样,当因为这个原因而需要修改代码时,需要修改的代码都在这个模块、这个类中,修改范围就缩小了,维护成本降低了,自然设计质量就提高了。
总之,单一职责原则要求我们在维护软件的过程中需要不断地进行整理,将软件变化同一个原因的代码放在一起,将软件变化不同原因的代码分开放。 按照这样的设计原则,回到前面那个案例中,那么应当怎样去分析“付款”与“折扣”之间的关系呢?只需要回答两个问题:
当“付款”发生变更时,“折扣”是不是一定要变?
当“折扣”发生变更时,“付款”是不是一定要变?
当这两个问题的答案是否定时,就说明“付款”与“折扣”是软件变化的两个不同的原因,那么把它们放在一起,放在同一个类、同一个方法中,合适吗?不合适,就应当将“折扣”从“付款”中提取出来,单独放在一个类中。
同样的道理:
当“限时折扣”发生变更的时候,“限量折扣”是不是一定要变?
当“限量折扣”发生变更的时候,“某类商品的折扣”是不是一定要变?
……
最后发现,不同类型的折扣也是软件变化不同的原因。将它们放在同一个类、同一个方法中,合适吗?通过以上分析,我们做出了如下设计:
在该设计中,将折扣功能从付款功能中独立出去,做出了一个接口,然后以此为基础设计了各种类型的折扣实现类。这样的设计,当付款功能发生变更时不会影响折扣,而折扣发生变更的时候不会影响付款。同样,当“限时折扣”发生变更时只与“限时折扣”有关,“限量折扣”发生变更时也只与“限量折扣”有关,与其他折扣类型无关。变更的范围缩小了,维护成本就降低了,设计质量提高了。这样的设计就是“单一职责原则”的真谛。
接着,在这个版本的领域模型的基础上进行程序设计,在设计时还可以加入一些设计模式的内容,因此我们进行了如下的设计:
显然,在该设计中加入了“策略模式”的内容,将折扣功能做成了一个折扣策略接口与各种折扣策略的实现类。当哪个折扣类型发生变更时就修改哪个折扣策略实现类;当要增加新的类型的折扣时就再写一个折扣策略实现类,设计质量得到了提高。
VIP 会员的需求变更
在第一次变更的基础上,很快迎来了第二次变更,这次是要增加 VIP 会员,业务需求如下。
增加 VIP 会员功能:
对不同类型的 VIP 会员(金卡会员、银卡会员)进行不同的折扣;
在支付时,为 VIP 会员发放福利(积分、返券等);
VIP 会员可以享受某些特权。
我们拿到这样的需求又应当怎样设计呢同样先回到领域模型分析“用户”与“VIP 会员”的关系“付款”与“VIP 会员”的关系。在分析的时候,还是回答那两个问题。
“用户”发生变更时“VIP 会员”是否要变?
“VIP 会员”发生变更时,“用户”是否要变?
通过分析发现“用户”与“VIP 会员”是两个完全不同的事物。
“用户”要做的是用户的注册、变更、注销等操作;
“VIP 会员”要做的是会员折扣、会员福利与会员特权;
而“付款”与“VIP 会员”的关系是在付款的过程中去调用会员折扣、会员福利与会员特权。
通过以上的分析,我们做出了以下版本的领域模型:
有了这些领域模型的变更,然后就可以以此作为基础,指导后面程序代码的变更了。
支付方式的需求变更
同样,第三次变更是增加更多的支付方式,我们在领域模型中分析“付款”与“支付方式”之间的关系,发现它们也是软件变化不同的原因。因此,我们果断做出了这样的设计:
而在设计实现时,因为要与各个第三方的支付系统对接,也就是要与外部系统对接。为了使第三方的外部系统的变更对我们的影响最小化,在它们中间果断加入了“适配器模式”,设计如下:
通过加入适配器模式,订单 Service 在进行支付时调用的不再是外部的支付接口,而是“支付方式”接口,与外部系统解耦。只要保证“支付方式”接口是稳定的,那么订单 Service 就是稳定的。比如:
当支付宝支付接口发生变更时,影响的只限于支付宝 Adapter
当微信支付接口发生变更时,影响的只限于微信支付 Adapter
当要增加一个新的支付方式时,只需要再写一个新的 Adapter。
日后不论哪种变更,要修改的代码范围缩小了,维护成本自然降低了,代码质量就提高了。
总结
这一讲通过以上的过程,我们演练了如何运用 DDD 进行软件的设计与变更,以及在设计与变更的过程中如何分析思考、如何评估代码、如何实现高质量。后面,我们将演练如何将领域模型的设计进一步落实到软件系统的微服务设计与数据库设计。

View File

@ -0,0 +1,246 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 DDD 是如何落地到数据库设计的?
过去,系统的软件设计是以数据库设计为核心,当需求确定下来以后,团队首先开始进行数据库设计。因为数据库是各个模块唯一的接口,当整个团队将数据库设计确定下来以后,就可以按照模块各自独立地进行开发了,如下图所示。
在上面的过程中,为了提高团队开发速度,尽量让各个模块不要交互,从而达到各自独立开发的效果。但是,随着系统规模越来越大,业务逻辑越来越复杂,我们越来越难于保证各个模块独立不交互了。
随着软件业的不断发展,软件系统变得越来越复杂,各个模块间的交互也越来越频繁,这时,原有的设计过程已经不能满足我们的需要了。 因为如果要先进行数据库设计,但数据库设计只能描述数据结构,而不能描述系统对这些数据结构的处理。因此,在第一次对整个系统的梳理过程中,只能梳理系统的所有数据结构,形成数据库设计;接着,又要再次梳理整个系统,分析系统对这些数据结构的处理过程,形成程序设计。为什么不能一次性地把整个系统的设计梳理到位呢?
现如今,我们已经按照面向对象的软件设计过程来分析设计系统了。当开始需求分析时,首先进行用例模型的设计,分析整个系统要实现哪些功能;接着进行领域模型的设计,分析系统的业务实体。在领域模型分析中,采用类图的形式,每个类可以通过它的属性来表述数据结构,又可以通过添加方法来描述对这个数据结构的处理。因此,在领域模型的设计过程中,既完成了对数据结构的梳理,又确定了系统对这些数据结构的处理,这样就把两项工作一次性地完成了。
在这个设计过程中,其核心是领域模型的设计。以领域模型作为核心,可以指导系统的数据库设计与程序设计,此时,数据库设计就弱化为了领域对象持久化设计的一种实现方式。
领域对象持久化的思想
什么叫领域对象的持久化呢?在当今软件架构设计的主流思想中,面向对象设计成了主流思想,在整个系统运行的过程中,所有的数据都是以领域对象的形式存在的。譬如:
要插入一条记录就是创建一个领域对象;
要更新一条记录就是根据 key 值去修改相应的领域对象;
删除数据则是摧毁这个领域对象。
假如我们的服务器是一台超级强大的服务器,那实际上不需要任何数据库,直接操作这些领域对象就可以了,但在现实世界中没有那么强大的服务器。因此,必须将暂时不用的领域对象持久化存储到磁盘中,而数据库只是这种持久化存储的一种实现方式。
按照这种设计思想,我们将暂时不使用的领域对象从内存中持久化存储到磁盘中。当日后需要再次使用这个领域对象时,根据 key 值到数据库查找到这条记录,然后将其恢复成领域对象,应用程序就可以继续使用它了,这就是领域对象持久化存储的设计思想。
所以,今天的数据库设计,实际上就是将领域对象的设计按照某种对应关系,转换成数据库的设计。同时,随着整个产业的大数据转型,今后的数据库设计思想也将发生巨大的转变,有可能数据库就不一定是关系型数据库了,也许是 NoSQL 数据库或者大数据平台。数据库的设计也不一定遵循 3NF第三范式可能会增加更多的冗余甚至是宽表。
数据库设计在发生剧烈的变化但唯一不变的是领域对象。这样当系统在大数据转型时可以保证业务代码不变变化的是数据访问层DAO。这将使得日后大数据转型的成本更低让我们更快地跟上技术快速发展的脚步。
领域模型的设计
此外,这里有个有趣的问题值得探讨:领域模型的设计到底是谁的职责,是需求分析人员还是设计开发人员?我认为,它是两个角色相互协作的产物。而未来敏捷开发的组织形成,团队将更加扁平化。过去是需求分析人员做需求分析,然后交给设计人员设计开发,这种方式就使得软件设计质量低下而结构臃肿。未来“大前端”的思想将支持更多设计开发人员直接参与需求分析,实现从需求分析到设计开发的一体化组织形式。这样,领域模型就成为了设计开发人员快速理解需求的利器。
总之DDD 的数据库设计实际上已经变成了:以领域模型为核心,如何将领域模型转换成数据库设计的过程。那么怎样进行转换呢?在领域模型中是一个一个的类,而在数据库设计中是一个一个的表,因此就是将类转换成表的过程。
上图是一个绩效考核系统的领域模型图,该绩效考核系统首先进行自动考核,发现一批过错,然后再给一个机会,让过错责任人对自己的过错进行申辩。这时,过错责任人可以填写一张申辩申请单,在申辩申请单中有多个明细,每个明细对应一个过错行为,每个过错行为都对应了一个过错类型,这样就形成了一个领域模型。
接着,要将这个领域模型转换成数据库设计,怎么做呢?很显然,领域模型中的一个类可以转换成数据库中的一个表,类中的属性可以转换成表中的字段。但这里的关键是如何处理类与类之间的关系,如何转换成表与表之间的关系。这时候,就有 5 种类型的关系需要转换,即传统的 4 种关系 + 继承关系。
传统的 4 种关系
传统的关系包含一对一、多对一、一对多、多对多这 4 种,它们既存在于类与类之间,又存在于表与表之间,所以可以直接进行转换。
1. 一对一关系
在以上案例中,“申辩申请单明细”与“过错行为”就是一对“一对一”关系。在该关系中,一个“申辩申请单明细”必须要对应一个“过错行为”,没有一个“过错行为”的对应就不能成为一个“申辩申请单明细”。这种约束在数据库设计时,可以通过外键来实现。但是,一对一关系还有另外一个约束,那就是一个“过错行为”最多只能有一个“申辩申请单明细”与之对应。
也就是说,一个“过错行为”可以没有“申辩申请单明细”与之对应,但如果有,最多只能有一个“申辩申请单明细”与之对应,这个约束暗含的是一种唯一性的约束。因此,将过错行为表中的主键,作为申辩申请单明细表的外键,并将该字段升级为申辩申请单明细表的主键。
2. 多对一关系
是日常的分析设计中最常见的一种关系。在以上案例中,一个过错行为对应一个税务人员、一个纳税人与一个过错类型;同时,一个税务人员,或纳税人,或过错类型,都可以对应多个过错行为。它们就形成了“多对一”关系。在数据库设计时,通过外键就可以建立这种“多对一”关系。因此,我们进行了如下数据库的设计:
多对一关系在数据库设计上比较简单,然而落实到程序设计时,需要好好探讨一下。比如,以上案例,在按照这样的方式设计以后,在查询时往往需要在查询过错行为的同时,显示它们对应的税务人员、纳税人与过错类型。这时,以往的设计是增加一个 join 语句。然而,这样的设计在随着数据量不断增大时,查询性能将受到极大的影响。
也就是说join 操作往往是关系型数据库在面对大数据时最大的瓶颈之一。因此,一个更好的方案就是先查询过错行为表,分页,然后再补填当前页的其他关联信息。这时,就需要在“过错行为”这个值对象中通过属性变量,增加对税务人员、纳税人与过错类型等信息的引用。
3. 一对多关系
该关系往往表达的是一种主-子表的关系。譬如,以上案例中的“申辩申请单”与“申辩申请单明细”就是一对“一对多”关系。除此之外,订单与订单明细、表单与表单明细,都是一对多关系。一对多关系在数据库设计上比较简单,就是在子表中增加一个外键去引用主表中的主键。比如本案例中,申辩申请单明细表通过一个外键去引用申辩申请单表中的主键,如下图所示。
除此之外,在程序的值对象设计时,主对象中也应当有一个集合的属性变量去引用子对象。如本例中,在“申辩申请单”值对象中有一个集合属性去引用“申辩申请单明细”。这样,当通过申辩申请单号查找到某个申辩申请单时,同时就可以获得它的所有申辩申请单明细,如下代码所示:
public class Sbsqd {
private Set<SbsqdMx> sbsqdMxes;
public void setSbsqdMxes(Set<SbsqdMx> sbsqdMxes){
this.sbsqdMxes = sbsqdMxes;
}
public Set<SbsqdMx> getSbsqdMxes(){
return this.sbsqdMxes;
}
}
4. 多对多关系
比较典型的例子就是“用户角色”与“功能权限”。一个“用户角色”可以申请多个“功能权限”;而一个“功能权限”又可以分配给多个“用户角色”使用,这样就形成了一个“多对多”关系。这种多对多关系在对象设计时,可以通过一个“功能-角色关联类”来详细描述。因此,在数据库设计时就可以添加一个“角色功能关联表”,而该表的主键就是关系双方的主键进行的组合,形成的联合主键,如下图所示:
以上是领域模型和数据库都有的 4 种关系。因此,在数据库设计时,直接将相应的关系转换成数据库设计就可以了。同时,在数据库设计时还要将它们进一步细化。如在领域模型中,不论对象还是属性,在命名时都采用中文,这样有利于沟通与理解。但到了数据库设计时,就要将它们细化为英文命名,或者汉语拼音首字母,同时还要确定它们的字段类型与是否为空等其他属性。
继承关系的 3 种设计
第 5 种关系就不太一样了:继承关系是在领域模型设计中有,但在数据库设计中却没有。 如何将领域模型中的继承关系转换成数据库设计呢?有 3 种方案可以选择。
1. 继承关系的第一种方案
首先,看看以上案例。“执法行为”通过继承分为“正确行为”和“过错行为”。如果这种继承关系的子类不多(一般就 2 ~ 3 个并且每个子类的个性化字段也不多3 个以内)的话,则可以使用一个表来记录整个继承关系。在这个表的中间有一个标识字段,标识表中的每条记录到底是哪个子类,这个字段的前面部分罗列的是父类的字段,后面依次罗列各个子类的个性化字段。
采用这个方案的优点是简单,整个继承关系的数据全部都保存在这个表里。但是,它会造成“表稀疏”。在该案例中,如果是一条“正确行为”的记录,则字段“过错类型”与“扣分”永远为空;如果是一条“过错行为”的记录,则字段“加分”永远为空。假如这个继承关系中各子类的个性化字段很多,就会造成该表中出现大量字段为空,称为“表稀疏”。在关系型数据库中,为空的字段是要占用空间的。因此,这种“表稀疏”既会浪费大量存储空间,又会影响查询速度,是需要极力避免的。所以,当子类比较多,或者子类个性化字段多的情况是不适合该方案(第一种方案)的。
2. 继承关系的第二种方案
如果执法行为按照考核指标的类型进行继承分为“考核指标1”“考核指标2”“考核指标3”……如下图所示
并且每个子类都有很多的个性化字段,则采用前面那个方案就不合适了。这时,用另外两个方案进行数据库设计。其中一个方案是将每个子类都对应到一个表,有几个子类就有几个表,这些表共用一个主键,即这几个表的主键生成器是一个,某个主键值只能存在于某一个表中,不能存在于多个表中。每个表的前面是父类的字段,后面罗列各个子类的字段,如下图所示:
如果业务需求是在前端查询时,每次只能查询某一个指标,那么采用这种方案就能将每次查询落到某一个表中,方案就最合适。但如果业务需求是要查询某个过错责任人涉及的所有指标,则采用这种方案就必须要在所有的表中进行扫描,那么查询效率就比较低,并不适用。
3. 继承关系的第三种方案
如果业务需求是要查询某个过错责任人涉及的所有指标,则更适合采用以下方案,将父类做成一个表,各个子类分别对应各自的表(如图所示)。这样,当需要查询某个过错责任人涉及的所有指标时,只需要查询父类的表就可以了。如果要查看某条记录的详细信息,再根据主键与类型字段,查询相应子类的个性化字段。这样,这种方案就可以完美实现该业务需求。
综上所述,将领域模型中的继承关系转换成数据库设计有 3 种方案,并且每个方案都有各自的优缺点。因此,需要根据业务场景的特点与需求去评估,选择哪个方案更适用。
NoSQL 数据库的设计
前面我们讲的数据库设计还是基于传统的关系型数据库、基于第三范式的数据库设计。但是随着互联网高并发与分布式技术的发展另一种全新的数据库类型孕育而生那就是NoSQL 数据库。正是由于互联网应用带来的高并发压力,采用关系型数据库进行集中式部署不能满足这种高并发的压力,才使得分布式 NoSQL 数据库得到快速发展。
也正因为如此NoSQL 数据库与关系型数据库的设计套路是完全不同的。关系型数据库的设计是遵循第三范式进行的,它使得数据库能够大幅度降低冗余,但又从另一个角度使得数据库查询需要频繁使用 join 操作,在高并发场景下性能低下。
所以NoSQL 数据库的设计思想就是尽量干掉 join 操作,即将需要 join 的查询在写入数据库表前先进行 join 操作,然后直接写到一张单表中进行分布式存储,这张表称为“宽表”。这样,在面对海量数据进行查询时,就不需要再进行 join 操作,直接在这个单表中查询。同时,因为 NoSQL 数据库自身的特点,使得它在存储为空的字段时不占用空间,不担心“表稀疏”,不影响查询性能。
因此NoSQL 数据库在设计时的套路就是,尽量在单表中存储更多的字段,只要避免数据查询中的 join 操作,即使出现大量为空的字段也无所谓了。
增值税发票票样图
正因为 NoSQL 数据库在设计上有以上特点,因此将领域模型转换成 NoSQL 数据库时,设计就完全不一样了。比如,这样一张增值税发票,如上图所示,在数据库设计时就需要分为发票信息表、发票明细表与纳税人表,而在查询时需要进行 4 次 join 才能完成查询。但在 NoSQL 数据库设计时,将其设计成这样一张表:
{ _id: ObjectId(7df78ad8902c)
fpdm: '3700134140', fphm: '02309723,
kprq: '2016-1-25 9:22:45',
je: 70451.28, se: 11976.72,
gfnsr: {
nsrsbh: '370112582247803',
nsrmc:'联通华盛通信有限公司济南分公司',…
},
xfnsr: {
nsrsbh: '370112575587500',
nsrmc:'联通华盛通信有限公司济南分公司',…
},
spmx: [
{ qdbz:'00', wp_mc:'蓝牙耳机 车语者S1 蓝牙耳机', sl:2, dj:68.37,… },
{ qdbz:'00', wp_mc:'车载充电器 新在线', sl:1, dj:11.11,… },
{ qdbz:'00', wp_mc:'保护壳 非尼膜属 iPhone6 电镀壳', sl:1, dj:24,… }
]
}
在该案例中对于“一对一”和“多对一”关系在发票信息表中通过一个类型为“对象”的字段来存储比如“购方纳税人gfnsr”与“销方纳税人xfnsr”字段。对于“一对多”和“多对多”关系通过一个类型为“对象数组”的字段来存储如“商品明细spmx”字段。在这样一个发票信息表中就可以完成对所有发票的查询无须再进行任何 join 操作。
同样,采用 NoSQL 数据库怎样实现继承关系的设计呢?由于 NoSQL 数据库自身的特点决定了不用担心“表稀疏”,同时要避免 join 操作所以比较适合采用第一个方案即将整个继承关系放到同一张表中进行设计。这时NoSQL 数据库的每一条记录可以有不一定完全相同的字段,可以设计成这样:
{ _id: ObjectId(79878ad8902c),
name: Jack,
type: parent,
partner: Elizabeth,
children: [
{ name: Tom, gender: male },
{ name: Mary, gender: female}
]
},
{ _id: ObjectId(79878ad8903d),
name: Bob,
type: kid,
mother: Anna,
father: David
}
以上案例是一个用户档案表有两条记录Jack 与 Bob。但是Jack 的类型是“家长”,因此其个性化字段是“伴侣”与“孩子”;而 Bob 的类型是“孩子”,因此他的个性化字段是“父亲”与“母亲”。显然,在 NoSQL 数据库设计时就会变得更加灵活。
总结
将领域模型落地到系统设计包含 2 部分内容,本讲演练了第一部分内容——从 DDD 落实到数据库设计的整个过程:
传统的 4 种关系可以直接转换;
继承关系有 3 种设计方案;
转换成 NoSQL 数据库则是完全不同的思路。
有了 DDD 的指导,可以帮助我们理清数据间的关系,以及对数据的操作。 不仅如此,在未来面对大数据转型时更加从容。