云原生
2021-11-20 10:16:16 48 举报
AI智能生成
云原生是一种新兴的软件开发和部署方法,它利用云计算技术的优势来构建、交付和管理应用程序。云原生应用程序通常采用容器化、微服务架构和自动化运维等技术,以提高可伸缩性、弹性和可靠性。这些应用程序可以在多个云平台之间无缝迁移,从而降低了成本并提高了灵活性。此外,云原生还强调开发者与运维人员之间的紧密合作,以确保应用程序能够快速响应不断变化的需求。总之,云原生是一种面向未来的软件开发方法,它将为企业和开发者带来巨大的价值。
作者其他创作
大纲/内容
云原生
简介
云原生的定义
实际上,云原生是一条最佳路径或者最佳实践。更详细的说,云原生为用户指定了一条低心智负担的、敏捷的、能够以可扩展、可复制的方式最大化地利用云的能力、发挥云的价值的最佳路径
云原生的核心底盘
容器技术
容器技术使得应用具有了一种“自包含”的定义方式。所以,这样的应用才能以敏捷的、以可扩展可复制的方式发布在云上,发挥出云的能力。这也就是容器技术对云发挥出的革命性影响所在。所以说,容器技术是云原生技术的核心地盘。
云原生的技术范畴
第一部分:是云应用定义与开发流程。这包括应用定义与镜像制作、配置CI/CD、消息和Streaming以及数据库等。
第二部分:是云应用的编排与管理流程。这也是Kubernetes比较关注的一部分,包括了应用编排与调度、服务发现治理、远程调用、API网关以及Service Mesh。
第三部分:是监控与可观测性。这部分所强调的是云上应用如何进行监控、日志收集、Tracing以及在云上如何实现破坏性测试,也就是混沌工程的概念。
第四部分:就是云原生的底层技术,比如容器运行时、云原生存储技术、云原生网络技术等。
第五部分:是云原生工具集,在前面的这些核心技术点之上,还有很多配套的生态或者周边的工具需要使用,比如流程自动化与配置管理、容器镜像仓库、云原生安全技术以及云端密码管理等。
最后则是Serverless。Serverless是一种PaaS的特殊形态,它定义了一种更为“极端抽象”的应用编写方式,包含了FaaS和BaaS这样的概念。而无论是FaaS还是BaaS,其最为典型的特点就是按实际使用计费(Pay as you go),因此Serverless计费也是重要的知识和概念。
云原生思想的两个理论
不可变基础设施
这一点目前通过容器镜像来实现的,其含义就是应用的基础设施应该是不可变的,是一个自包含、自描述可以完全在不同环境中迁移的东西;
云应用编排理论
当前的实现方式就是Google所提出来的“容器设计模式”
容器与镜像
什么是容器?
回顾一下操作系统是如何管理进程的
首先,当我们登陆到操作系统之后,可以通过ps等操作看到各式各样的的进程,这些进程包括系统自带的服务和用户的应用进程。那么,这些进程都有什么样的特点?
第一、这些进程可以相互看到、相互通信;
第二、它们使用的是同一个文件系统,可以对同一个文件进行读写操作;
第三、这些进程会使用相同的系统资源。
这三个特点会带来什么问题呢?
因为这些进程能够好像看到并且进行通信,高级权限的进程可以攻击其它进程;
因为它们使用的是同一个文件系统,因此会带来两个问题:这些进程可以对于已有数据进行增删改查,具有高级权限的进程可能会将其它进程的数据删除掉,破坏其它进程的正常运行;此外,进程与进程之间的依赖可能会存在冲突,如此一来就会给运维带来很大压力;
因为这些进程使用的是同一个宿主机的资源,应用之间会存在资源抢占的问题,当一个应用需要消耗大量的CPU和内存资源的时候,就可能会破坏其它应用的运行,导致其它应用无法正常地提供服务。
针对上述三个问题,如何为进程提供一个独立的运行环境呢?
针对不同进程使用同一个文件系统所造成的问题而言,Linux和Unix操作系统可以通过chroot系统调用将子目录变成根目录,达到视图级别的隔离;进程在chroot的帮助下可以具有独立的文件系统,对于这样的文件系统进行增删改查不会影响到其它进程;
因为进程之间相互可见并且可以相互通信,使用Namespace技术来实现进程在资源的视图上进行隔离。在chroot和Namespace的帮助下,进程就能够运行在一个独立的环境下了。
但在独立的环境下,进程所使用的还是同一个操作系统的资源,一些进程可能会侵蚀掉整个操作系统的资源。为了减少进程彼此之间的影响,可以通过Cgroup来限制其资源的使用率,设置其能够使用的CPU以及内存量。
那么,应该如何定义这样的进程集合呢?
其实,容器就是一个试图隔离、资源可限制、独立文件系统的进程集合。
所谓“视图隔离”就是能够看到部分进程以及具有独立的主机名等;控制资源使用率则是可以对于内存大小以及CPU使用个数等进行限制。容器就是一个进程集合,它将系统的其它资源隔离开来,具有自己独立的资源视图。
容器具有一个独立的文件系统,因为是使用的是系统资源,所以在独立的文件系统内不需要具备内核相关代码或者工具,我们只需要提供容器所需的二进制文件、配置文件以及依赖即可。只要容器运行时所需的文件集合都能够具备,那么这个容器就能够运行起来。
什么是镜像?
我们将这些容器运行时所需要的所有文件集合称之为镜像
采用什么样的方式来构建镜像呢?
通常情况下,我们采用Dockerfile来构建镜像,这是因为Dockerfile提供了非常便利的语法糖,能够帮助我们很好的描述构建的每个步骤。当然,每个构建步骤都会对已有的文件系统进行操作,这样就会带来文件系统内容的变化,我们将这些变化称之为changeset。当我们把构建步骤所产生的变化依次作用到一个空文件夹上,就能够得到一个完整的镜像。
changeset的分层以及复用特点能够带来几点优势:
第一、能够提高分发效率,简单试想一下,对于大的镜像而言,如果将其拆分成各个小块就能够提高镜像的分发效率,这是因为镜像拆分之后就可以并行下载这些数据;
第二、因为这些数据是相互共享的,也就意味着当本地存储上包含了一些数据的时候,只需要下载本地没有的数据即可,举个简单点例子就是golang镜像是基于alpine镜像进行构建的,当本地已经具有alpine镜像之后,再下载golang镜像的时候只需要下载本地alpine镜像中没有的部分即可;
第三、因为镜像数据是共享的,因此可以节约大量的磁盘空间,简单设想一下,当本地存储具有了alpine镜像和golang镜像,在没有复用的能力之前,alpine镜像具有5M大小,golang镜像具有300M大小,因此就会占用305M空间;而当具有了复用能力之后,只需要300M空间即可。
那如何构建镜像呢?
编写Dockerfile文件
FROM golang:1.12-alpineWORKDIR /go/src/appCOPY . .RUN go get -d -v ./..RUN go install -v ./..CMD [\"app\"]
FROM行表示一下的关键步骤基于什么镜像进行的构建,正如前面所说的,镜像是可以复用的;
WORKDIR行表示会把接下来的关键步骤都在哪一个相应的具体目录下进行,相当于cd到哪个文件夹一样;
COPY行表示的是可以将宿主机上的文件拷贝到容器镜像内,还有一个ADD也可以,有一点区别的哦;
RUN行表示在具体的文件系统内执行相应的动作。当我们运行完毕之后就可以得到一个应用了;
CMD行表示使用镜像时的默认程序名字。
当有了Dockerfile之后,就可以提供docker build -t .命令构建所需要的应用。构建的结果存储在本地,一般情况下,镜像构建会在打包机或者其它隔离环境下完成。
那么,这些镜像如何运行在生产环境或者测试环境上呢?这时候就需要一个中转站或者中心存储,我们称之为docker registry,也就是镜像仓库,其负责存储所有产生的镜像数据。我们只需要通过docker push就能够将本地的镜像推到镜像仓库中,这样一来,就能够在生产环境或者测试环境上将相应的数据下载下来并运行了。
如何运行容器?
第一步、从镜像仓库中将相应的镜像下载下来;
第二步、当镜像下载完成之后就可以通过docker images查看本地镜像;
第三步、当选中镜像之后,就可以通过docker run来运行这个镜像得到想要的容器,当然可以通过多次运行得到多个容器。一个镜像就相当于是一个模板,一个容器就像是一个具体的运行实例,因此镜像就具有了一次构建、到处运行的特点。
容器的生命周期
容器是一组具有隔离特性的进程集合,在使用docker run的时候会选择一个镜像来提供独立的文件系统并指定相应的运行程序。这里指定的运行程序称之为initial进程,这个initial进程启动的时候,容器也会随之启动,当initial进程退出的时候,容器也会随之退出。
因此,可以认为容器的生命周期和initial进程的生命周期是一致的。当然,因为容器内不只有这样一个这样的一个initial进程,initial进程本身也可以产生其它的子进程或者通过docker exec产生出来的运维操作,也属于initial进程管理的范围内。当initial进程退出的时候,所有的子进程也会随之退出,这样也是为了防止资源的泄漏。
但是这样的做法也会存在一些问题,首先应用里面的程序往往是有状态的,其可能会产生一些重要数据,当一个容器退出被删除之后,数据也就会丢失了,这对于应用方而言是不能接受的,所以需要将容器所产生出来的重要数据持久化下来。容器能够直接将数据持久化到指定的目录上,这个目录就称之为数据卷。
数据卷有一些特点,其中非常明显的就是数据卷的生命周期是独立于容器的生命周期,也就是说容器的创建、运行、停止、删除等操作都和数据卷没有任何关系,因为它是一个特殊的目录,是用于帮助容器进行持久化的,简单而言,我们会将数据卷挂载到容器内,这样一来容器就能够将数据写入到相应的目录里面了,而容器的退出并不会导致数据的丢失。
通常情况下,数据卷管理主要有两种方式:
第一种是通过binf的方式,直接将宿主机的目录直接挂载到容器内;这种方式比较简单,但是会带来运维成本,因为其依赖宿主机的目录,需要对于所有的宿主机进行统一的管理。
第二种是将目录管理交给运行引擎。
容器项目架构
moby容器引擎架构
moby daemon会对上提供有关于容器、镜像、网络以及Volume的管理。
moby daemon所依赖的最重要的组件就是containerd,containerd是一个容器运行时管理引擎,其独立于moby daemon,可以对上提供容器、镜像的相关管理。
containerd底层有containerd shim模块,其类似于一个守护进程,这样设计的原因有几点:
首先,containerd需要管理容器的生命周期,而容器可能是由不同的容器运行时所创建出来的,因此需要提供一个灵活的插件化管理。而shim就是针对于不同的容器运行时所开发的,这样就能够从containerd中脱离出来,通过插件的形式进行管理。
其次,因为shim插件化的实现,使其能够被containerd动态接管。如果不具备这样的能力,当moby deamon或者containerd deamon意外退出的时候,容器就没人管理了,那么它它也会随之消失、退出,这样就会影响到应用的运行。
最后,因为随时可能会对moby或者containerd进行升级,如果不提供shim机制,那么就无法做到原地升级,也无法做到不影响业务的升级,因此containerd shim非常重要,它实现了动态接管的能力。
容器 vs VM
VM利用Hypervisor虚拟化技术来模拟CPU、内存等硬件资源,这样就可以在宿主机上建立一个Guest OS,这是常说的安装一个虚拟机。
每一个Guest OS都有一个独立的内核,比如Ubuntu、CentOS甚至是Windows等,在这样的Guest OS之下,每一个应用都是相互独立的,VM可以提供一个更好的隔离效果。但这样的隔离效果需要付出一定的代价,因为需要把一部分的计算资源交给虚拟化,这样就很难充分的利用现有的计算资源,并且每个Guest OS都需要占用大量的磁盘空间,比如Windows操作系统的安装需要占用10-30G的磁盘空间,Ubuntu也需要5-6G,同时这样的方式启动很慢。正是因为虚拟机技术的缺点,催生出了容器技术。
容器是针对与进程而言的,因此无需Guest OS,只需要一个独立的文件系统提供所需要文件集合即可。所有的文件隔离都是进程级别的,因此启动时间快于VM,并且所需的磁盘空间也小于VM。当然了,进程级别的隔离并没有想象中的那么好,隔离效果比VM要差很多。
总体而言,容器和VM相比,各有优劣,因此容器技术也在向着强隔离方向发展。
Kubernetes
什么是Kubernetes
Kubernetes,从官方网站上可以看到,它是一个工业级的容器编排平台。Kubernetes 这个单词是希腊语,它的中文翻译是“舵手”或者“飞行员”。也叫做“k8s”,它是通过将8个字母“ubernete ”替换为“8”而导致的一个缩写。我们之前其实介绍过一个概念叫做 container,container 这个英文单词也有另外的一个意思就是“集装箱”。Kubernetes 也就借着这个寓意,希望成为运送集装箱的一个轮船,来帮助我们管理这些集装箱,也就是管理这些容器。这个就是为什么会选用 Kubernetes 这个词来代表这个项目的原因。
更具体一点滴来说:Kubernetes是一个自动化的容器编排平台,它负责应用的部署、应用的弹性以及应用的管理,这些都是基于容器的。
Kubernetes有以下几个核心功能:
介绍
1. 服务的发现与负载均衡;
2. 容器的自动装箱,我们也会把它叫做scheduling,就是“调度”,把一个容器放到一个集群的某一个机器上,Kubernetes会帮助我们去做存储的编排,让存储的声明周期与容器的生命周期能有一个连接;
3. Kubernetes会帮助我们去做自动化的容器的恢复。在一个集群中,经常会出现宿主机的问题或者说OS的问题,导致容器本身的不可用,Kubernetes会自动地对这些不可用的容器进行恢复;
4. Kubernetes会帮助我们去做应用的自动发布与应用的回滚,以及与应用相关的配置密文的管理;
5. 对于job类型任务,Kubernetes可以去做批量的执行;
6. 为了让这个集群、这个应用更富有弹性,Kubernetes也支持水平的伸缩。
1. 调度
Kubernetes可以把用户提交的容器放到Kubernetes管理的集群的某一台节点上去。Kubernetes的调度器是执行这项能力的组件,它会观察正在被调度的这个容器的大小、规格。
比如说它所需要的CPU以及它所需要的memory,然后在集群中找一台相对比较空闲的机器来进行一次placement,也就是一次放置的操作。在下面图片中,它可能会把红颜色的这个容器放置到第二台空闲的机器上,来完成一次调度的工作。
2. 自动修复
Kubernetes有一个节点健康检查的功能,它会检测这个集群中所有的宿主机,当宿主机本身出现故障,或者软件出现故障的时候,这个节点健康检查会自动对它进行发现。
下面Kubernetes会把运行在这些失败节点上的容器进行自动迁移,迁移到一个正在健康运行的宿主机上,来完成集群容器的一个自动恢复。
迁移
3. 水平伸缩
Kubernetes有业务负载检查的能力,它会监测业务上所承担的负载,如果这个业务本身的CPU利用率过高,或者响应时间过长,它可能对这个业务进行一次扩容。
比如说在下面这个图片中,黄颜色的过度忙碌,Kubernetes就可以把黄颜色负载从一份变为三份。接下来,它就可以通过负载均衡把原来打到第一个黄颜色上的负载平均分到三个黄颜色的负载上去,以此来提高响应的时间
水平扩容
Kubernetes的架构
Kubernetes架构是一个比较典型的二层架构和server-client架构。Master作为中央的管控节点,会去与Node进行一次连接。
所有UI的、clients、这些user侧的组件,只会和Master进行连接,把希望的状态或者想执行的命令下发给Master,Master会把这些命令或者状态下发给相应的节点,进行最终的执行。
Kubernetes的Master包含四个主要的组件:API Server、Controller、Scheduler以及etcd。如右图所示:
API Server:顾名思义是用来处理 API 操作的,Kubernetes中所有的组件都会和 API Server 进行连接,组件与组件之间一般不进行独立的连接,都依赖于API Server 进行消息的传送;
Controller:是控制器,它用来完成对集群状态的一些管理。比如刚刚我们提到的两个例子之中,第一个自动对容器进行修复、第二个自动进行水平扩张,都是由Kubernetes中的Controller来进行完成的;
Scheduler:是调度器,“调度器”顾名思义就是完成调度的操作,就是我们刚才介绍的第一个例子中,把一个用户提交的Container,依据它对CPU、对memory请求大小,找一台合适的节点,进行放置;
etcd:是一个分布式的一个存储系统,API Server 中所需要的这些原信息都被放置在etcd中,etcd本身是一个高可用系统,通过etcd保证整个Kubernetes的Master组件的高可用性。
刚刚提到的API Server,它本身在部署结构上是一个可以水平扩展的一个部署组件;Controller是一个可以进行热备的一个部署组件,它只有一个active,它的调度器也是相应的,虽然只有一个active,但是可以进行热备。
Node
Kubernetes的Node是真正运行业务负载的,每个业务负载会以Pod的形式运行。下面会介绍Pod的。一个Pod中运行的一个或者多个容器,真正去运行这些Pod的组件的是叫做Kubelet,也就是Node上最为关键的组件,它通过API Server接收到所需要Pod运行的状态,然后提交到我们下面画的这个Controller Runtime组件中。
在OS上去创建容器所需要运行的环境,最终把容器或者Pod运行起来,也需要对存储跟网络进行管理。Kubernetes并不会去直接进行网络存储的操作,它们会靠Storage Plugin或者是网络的Pligin来进行操作。用户自己或者云厂商都会去写相应的Storage Plugin或者Network Plugin,去完成存储操作与网络操作。
在Kubernetes自己的环境中,也会有Kubernetes的Network,它是为了提供Service network来进行搭网组网的。(等一下会介绍“service”这个概念。)真正完成service组网的组件就是Kube-proxy,它是利用了iptable的能力来进行组建Kubernetes的Network,就是cluster network,以上就是Node的四个组件。
Kubernetes的Node并不会直接和user进行interaction,它的interaction只会通过Master。而user是通过Master向节点下发这些信息的。Kubernetes每个Node上,都会运行上面的几个组件。
下面我们以一个例子再去看一下Kubernetes架构中的这些组件,是如何互相进行interaction的。
用户可以通过UI或者CLI提交一个Pod给Kubernetes进行部署,这个Pod请求会首先通过CLI或者UI提交给Kubernetes API Server,下一步API Server会把这个信息写入到它的存储系统etcd,之后Scheduler会通过API Server的watch或者叫做notification机制得到这个信息:有一个Pod需要被调度。
这个时候Scheduler会根据它的内存状态进行一次调度决策,在完成这次调度之后,它会向API Server report说“OK!这个Pod需要被调度到某一个节点上。”
这个时候API Server接收到这次操作之后,会把这次的结果再次写到etcd中,然后API Server会通知相应的节点进行这次Pod真正的执行启动。相应节点的Kubelet会得到这个通知,Kubelet就会去调Container runtime来真正去启动配置这个容器和这个容器的运行环境,去调度Storage Plugin去配置存储,network Plugin去配置网络。
这个例子我们可以看到:这些组件之间是如何相互沟通相互通信,协调来完成一次Pod的调度执行操作的。
Kubernetes的核心概念与它的API
核心概念
Pod
Pod是Kubernetes的一个最小调度以及资源单元。用户可以通过Kubernetes的Pod API 生产一个Pod,让Kubernetes对这个Pod进行调度,也就是把它放在某一个Kubernetes管理的节点上运行起来。一个Pod简单来说是对一组容器的抽象,它里面会包含一个或者多个容器。
比如像右边这副图里面,它包含了两个容器,每个容器可以指定它所需要的资源大小。比如说,一核一G,或者0.5核0.5G。
当然在这个Pod中也可以包含一些其它所需要的资源:比如说我们所看到的Volume卷这个存储资源;比如说我们需要100GB的存储或者20GB的另外一个存储。
Volume
Volume就是卷的概念,它是用来管理Kubernetes存储的,是用来声明在Pod中容器可以访问文件目录的,一个卷可以被挂载在Pod中一个或者多个容器的指定路径下面。
而Volume本身是一个抽象的概念,一个Volume可以去支持多种的后端的存储。比如说Kubernetes的Volume就支出了很多存储插件,它可以支持本地的存储,可以支持分布式的存储,比如说像ceph,GlusterFS;它也可以支出云存储,比如说阿里云上的云盘、AWS上的云盘、Google上的云盘等等。
Deployment
Deployment是在Pod这个抽象上更为上层的一个抽象,它可以定义一组Pod的副本数目、以及这个Pod的版本。一般大家用Deployment这个抽象来做应用的真正管理,而Pod是组成Deployment的最小的单元。
Kubernetes是通过Controller,也就是我们刚才提到的控制器去维护Deployment中Pod的数目,它也会去帮助Deployment自动恢复失败的Pod。
比如说我可以定义一个Deployment,这个Deployment里面需要两个Pod,当一个Pod失败的时候,控制器就会检测到,它重新把Deployment中的Pod数目从一个恢复到两个,通过再去新生成一个Pod。通过控制器,我们也会帮助完成发布的策略。比如说进行滚动升级,进行重新生成的升级,或者进行版本的回滚。
Service
Service提供了一个或者多个Pod实例的稳定访问地址。
比如在上面的例子中,我们看到:一个Deployment可能有两个甚至更多个完全相同的Pod。对于一个外部的用户来讲,访问哪个Pod其实都是一样的,所以它希望做一次负载均衡,在做负载均衡的同时,我只想访问一个固定的VIP,也就是Virtual IP地址,而不希望得知每一个具体的Pod的IP地址。
刚才提到,这个Pod本身可能terminal go(终止),如果一个Pod失败了,可能会换成另外一个新的。
对一个外部用户来讲,提供了多个具体的Pod地址,这个用户要不停地去更新Pod地址,当这个Pod再失败重启之后,我们希望有一个抽象,把所有Pod的访问能力抽象成一个第三方的一个IP地址,实现这个的Kubernetes的抽象就叫做Service。
实现Service的方式有多种,Kubernetes支出Cluster IP,上面我们讲过的Kuber-proxy的组网,它也支持nodePort、LoadBalancer等其它的一些访问的能力。
NamesPace
Namespace是用来做一个集群内部的逻辑隔离的,它包括鉴权、资源管理等。Kubernetes的每个资源,比如刚才讲的Pod、Depolyment、Service都属于一个Namespace,同一个Namespace中的资源需要命名的唯一性,不同的Namespace中的资源可以重名。
Namespace一个用例,比如像在阿里巴巴,我们内部会有很多个business units,在每一个business units之间,希望有一个视图上的隔离,并且在鉴权上也一样,在cuda上面也不一样,我们就会用Namespace来去给每一个BU提供一个他所看到的这么一个看到的隔离的机制。
API
从high-level上看,Kubernetes API是由HTTP+JSON组成的:用户访问的方式是HTTP,访问的API中content的内容是JSON格式的。
Kubernetes的kubectl,也就是command tool,Kubernetes UI,或者有时候用curl,直接与Kubernetes进行沟通,都是使用HTTP+JSON这种格式。
下面这个例子:比如说,对于这个Pod资源,它的HTTP访问的路径,就是API,然后是apiversion: V1,之后是相应的Namespace,以及Pods资源,最终是Podname,也就是Pod的名字。
如果我们去提交一个Pod,或者get一个Pod的时候,它的content内容都是用JSON或者是YAML表达的。上图中有个yaml的例子,在这个yaml file中,对Pod资源的描述也分为几个部分。
第一个部分,一般来讲会是 API 的 version。比如在这个例子中是V1,它也会描述我在操作哪个资源;比如说我们的kind如果是Pod,在Metadata中,就写上这个Pod的名字;比如说nginx,我们也会给它打一些label,我们等下会讲label的概念。在Metadata中,有时候也会去写annotation,也就是对资源的额外的一些用户层次的描述。
比较重要的一个部分叫做Spec,Spec也就是我们希望Pod达到一个预期的状态。比如说它内部需要有哪些container被运行;比如说这里面有一个nginx的container,它的image是什么?它暴露的port是什么?
当我们从Kubernetes API中去获取这个资源的时候,一般来讲在Spec下面会有一个项目叫status,它表达了这个资源当前的状态;比如说一个Pod的状态可能是正在被调度、或者是已经running、或者是已经被terminates,就是被执行完毕了。
在刚刚API之中,我们讲了一个比较有意思的metadata叫做“label”,这个label可以是一组KeyValuePair。
比如下图的第一个Pod中,label就可能是一个color等于red,即它的颜色是红颜色。当然你也可以加其它label,比如说size: big就是大小,定义为大的,它可能是一组label。
这些label是可以被selector,也就是选择器所查询的。这个能力实际上跟我们的sql类型的select语句是非常相似的,比如下图中的三个Pod资源中,我们就可以进行select。name color等于red,就是它的颜色是红色的,我们也可以看到,只有两个被选中了,因为只有他们的label是红色的,另外一个label中写的color等于yellow,也就是它的颜色是黄色,是不会被选中的。
通过label ,kubernetes的API层就可以对这些资源进行一个统一的筛选,那这些筛选也是kubernetes对资源的集合所表达默认的一种方式。
例如说,我们刚刚介绍的Deployment,它可能是代表一组Pod,它是一组Pod的抽象,一组Pod就是通过label selector来表达的。当然我们刚才讲到说service对应的一组Pod,就是一个service要对应一个或者多个的Pod,来对它们进行统一的访问,这个描述也是通过label selector来进行select选取一组Pod。
所以可以看到label是一个非常核心的kubernetes API的概念。
以一个demo结尾
安装一个 Kubernetes 沙箱环境
首先需要安装一个虚拟机,来在虚拟机中启动 Kubernetes。我们会推荐大家利用 virtualbox 来作为虚拟机的运行环境;
安装 VirtualBox: https://www.virtualbox.org/wiki/Downloads
其次我们需要在虚拟机中启动 Kubernetes,Kubernetes 有一个非常有意思的项目,叫 minikube,也就是启动一个最小的 local 的 Kubernetes 的一个环境。
minikube 我们推荐使用下面写到的阿里云的版本,它和官方 minikube 的主要区别就是把 minikube 中所需要的 Google 上的依赖换成国内访问比较快的一些镜像,这样就方便了大家的安装工作;
安装 MiniKube(中国版): https://yq.aliyun.com/articles/221687
最后在安装完 virtualbox 和 minikube 之后,大家可以对 minikube 进行启动,也就是下面这个命令。
启动命令:minikube start —vm-driver virtualbox
如果大家不是 Mac 系统,其他操作系统请访问下面这个链接,查看其它操作系统如何安装 minikube 沙箱环境。https://kubernetes.io/docs/tasks/tools/install-minikube/
做一个例子,来做三件事情:
1. 提交一个 nginx deployment;
kubectl apply -f https://k8s.io/examples/application/deployment.yaml
2. 升级 nginx deployment;
kubectl apply -f https://k8s.io/examples/application/deployment-update.yaml
3. 扩容 nginx deployment。
kubectl apply -f https://k8s.io/examples/application/deployment-scale.yaml
Pod和容器设计模式
为什么需要Pod?
容器的基本概念
我们知道Pod是Kubernetes项目里面非常重要的概念,也是非常重要的一个原子调度单位,但是为什么我们会需要这样一个概念呢?我们在使用容器Docker的时候,也没有这个说法。其实要理解Pod,我们首先要理解容器。
容器的本质实际上是一个进程,是一个视图被隔离,资源受限的进程。
容器里面 PID=1 的进程就是应用本身,这意味着管理虚拟机等于管理基础设施,因为我们是在管理机器,但管理容器却等于直接管理应用本身。这也是之前说过的不可变基础设施的一个最佳体现,这个时候,你的应用就等于你的基础设施,它一定是不可变的。
那Kubernetes又是什么呢?
Kubernetes就是云时代的操作系统!
以此类推,容器镜像其实就是:这个操作系统的软件安装包
真实操作系统里的例子
如果说Kubernetes就是操作系统的话,我们看一下真实操作系统的例子
例子里面有一个程序叫做Helloword,这个Helloword程序实际上是由一组进程组成的,需要注意一下,这里说的进程实际上等同于Linux中的线程。
因为Linux中的线程是轻量级进程,所有如果从Linux系统中去查看helloword中的pstree,将会看到这个Helloword实际上是由四个线程组成的;分别是{api、main、log、compute}。也就是说,四个这样的线程共同协作,共享Helloword程序的资源,组成了Helloword程序的真实工作情况。
思考一下,在真实的操作系统里面,一个程序往往是根据进程组来进行管理的。Kubernetes把它类比为一个操作系统,比如说Linux。针对于容器我们前面提到可以类比为进程,就是前面的Linux线程。那么Pod又是什么呢?实际上Pod就是我们刚刚提到的进程组,也就是Linux里的线程组。
进程组概念
还是前面那个例子:Helloword程序由四个进程组成,这些进程之间会共享一些资源和文件。那么现在有一个问题:假如说现在把Halloword程序用容器跑起来,你会怎么去做?
当然,最自然的一个解法就是,我现在就启动一个Docker容器,里面运行四个进程。可是这样会有一个问题,这种情况下容器里面PID=1的进程该是谁?比如说,它应该是我的main进程,那么问题来了,“谁”又负责去管理剩余的三个进程呢?
这个核心问题在于,容器的设计本身是一种“单进程”模型,不是说容器里只能起一个进程,由于容器的应用等于进程,所以只能去管理PID=1的这个进程,其它再起来的进程其实是一个托管状态。所以说服务应用进程本身就具有“进程管理”的能力。
比如说Helloword的程序有system的能力,或者直接把容器里PID=1的进程直接改为systemd,否则这个应用,或者是容器是没有办法去管理很多个进程的。因为PID=1进程是应用本身,如果现在把这个PID=1的进程kill了,或者它自己运行过程中死掉了,那么剩下的三个进程的资源就没有人回收了,这个是非常严重的一个问题。
而反过来真的把应用本身改成了systemd,或者在容器里面运行了一个systemd,将会导致另外一个问题:使得管理容器,不再是管理应用本身了,而等于管理systemd,这里的问题就非常明显了。比如说我这个容器里面run的程序或者进程是systemd,那么接下来,这个应用是不是退出了?是不是fail了?是不是出现异常失败了?实际上是没办法直接知道的,因为容器管理的是systemd。这就是为什么在容器里面运行复杂程序往往比较困难的一个原因。
这里再梳理一下:由于容器实际上是一个“单进程”模型,所以如果你在容器里启动多个进程,只有一个可以作为PID=1的进程,而这时候,如果PID=1的进程挂了,或者说失败退出了,那么其它三个进程就会自然而然的成为孤儿,没有人能够管理它们,没有人能够回收它们的资源,这是一个非常不好的情况。
注意:Linux容器的“单进程”模型,指的是容器的生命周期等同于PID=1的进程(容器应用进程)的生命周期,而不是说容器里不能创建多进程。当然,一般情况下,容器应用进程并不具备进程管理能力,所以你通过exec或者ssh在容器里创建的其他进程,一旦异常退出(比如ssh终止)是很容易变成孤儿进程的。
反过来,其实可以在容器里面run 一个systemd,用它来管理其他所有的进程。这样会产生第二个问题:实际上没有办法直接管理我的应用了,因为我的应用被system给接管了,那么这个时候应用状态的生命周期就不等于容器的生命周期。这个管理模型实际上是非常非常复杂的。
Pod = \"进程组\"
在Kubernetes里面,Pod实际上正是Kubernetes项目为你抽象出来的一个可以类比为进程组的概念。
前面提到的,由四个进程共同组成的一个应用Helloword,在Kubernetes里面,实际上会被定义为一个拥有四个容器的Pod,这个一定要非常仔细的理解。
就是说现在有四个职责不同、相互协作的进程,需要放在容器里去运行,在Kubernetes里面并不会把它们放到一个容器里面,因为这里会遇到两个问题。那么在Kubernetes里会怎么去做呢?它会把四个独立的进程分别用四个独立的容器启动起来,然后把它们定义在一个Pod里面。
所以说当Kubernetes把Helloword给拉起来的时候,你实际上会看到四个容器,它们共享了某些资源,这些资源都属于Pod,所以说Pod在Kubernetes里面只有一个逻辑单位,没有一个真实的东西对应说这个就是Pod,不会有的。真正起来在物理上存在的东西,就是四个容器。这四个容器,或者说是多个容器的组合就叫做Pod。并且还有一个概念一定要非常明确,Pod是Kubernetes分配资源的一个单位,因为里面的容器要共享某些资源,所以Pod也是Kubernetes的原子调度单位。
这些应用之前往往有着密切的协作关系,使得它们必须部署在同一台机器上并且共享某些信息。
为什么Pod必须是原子调度单位?
虽然了解了这个东西是一个进程组,但是为什么要把Pod本身作为一个概念抽象出来呢?或者说能不能通过调度把Pod这个事情给解决掉呢?为什么Pod必须是Kubernetes里面的原子调度单位?
我们通过一个例子来解释:
假如现在有两个容器,它们是紧密协作的,所以它们应该被部署在一个Pod里面。具体来说,第一个容器叫做APP,就是业务容器,它会写日志文件;第二个容器叫做LogCollector;它会把刚刚App容器写的日志文件转发到后端的ElasticSearch中。
两个容器的资源需求是这样的:App容器需要1G内存,LogCollector需要0.5G内存,而当前集群环境的可用内存是这样一个情况:Node_A:1.25G内存,Node_B:2G内存。
假如说现在没有Pod概念,就只有两个容器,这两个容器要紧密协作、运行在一台机器上。可是,如果调度器先把App调度到了Node_A上面,接下来会怎么样呢?这时你会发现:LogCollector实际上是没办法调度到Node_A上的,因为资源不够。其实此时整个应用本身就已经出问题了,调度已经失败了,必须去重新调度。
这是一个非常典型的成组调度失败的例子。英文名字叫做:Task co-scheduling问题,这个问题不是不能解,在很多项目里面,这样的问题都有解法。
比如说在Mesos里面,它会这一件事情,叫做资源囤积(resource hoarding):即当所以设置了Affinity约束的任务都达到时,才开始统一调度,这是一个典型的成组调度的解法。
所以上面提到的“App”和“LogCollector”这两个容器,在Mesos里面,它们不会说立即调度,而是等两个容器都提交完成,才开始统一调度。这样也会带来新的问题,首先调度效率会损失,因为需要等待。由于需要等还会有另外一个情况出现,就是产生死锁,就是互相等待的一个情况。这些机制在Mesos里都是需要解决的,也带来了额外的复杂度。
另外一种解法是Google的解法。它在Omega系统(就是Borg下一代)里面,做了一个非常复杂且非常厉害的解法,叫做乐观调度。比如说:不管这些冲突的异常情况,先调度,同时设置一个非常精妙的回滚机制,这样经过冲突后,通过回滚来解决问题。这个方式相对来说要更加优雅,也更加高效,但是它的实现机制是非常复杂的。这个很多人也能理解,就是悲观锁的设置一定比乐观锁要简单。
而像这样的一个Task co-scheduling问题,在kubernetes里,就直接通过Pod这样一个概念去解决了。因为在Kubernetes里,这样的一个App容器和LogCollector容器一定是属于一个Pod的,它们在调度时必然是以一个Pod为单位进行调度,所以这个问题根本不存在。
再次理解Pod
首先Pod里面的容器是“超亲密关系”。
这个“超”字需要理解,正常来说,有一种关系叫做亲密关系,这个亲密关系是一定可以通过调度来解决的。
比如说现在有两个Pod,他需要运行在同一个宿主机上,那就属于亲密关系,调度器一个是可以帮助去做的。但是对于超亲密关系来说,有一个问题,即它必须通过Pod来解决。因为如果超亲密关系赋予不了,那么整个Pod或者说是整个应用都无法启动。
什么叫超亲密关系呢?
比如说两个进程之间会发生文件交换,前面提到的例子就是这样,一个写日志,一个读日志;
两个进程之间需要通过localhost或者说是本地的Socket去进行通信,这种本地通信也是超亲密关系;
这两个容器或者是微服务之间,需要发生非常频繁的RPC调用,出于性能的考虑,也希望它们是超亲密关系;
两个容器或者应用,它们需要共享某些Linux Namespace。最简单常见的一个例子,就是我有一个容器需要加入另一个容器的Network Namespace。这样我就能看到另一个容器的网络设备,和它的网络信息。
像以上几种关系都属于超亲密关系,它们都是在 Kubernetes 中会通过 Pod 的概念去解决的。
现在我们理解了 Pod 这样的概念设计,理解了为什么需要 Pod。它解决了两个问题:
1. 我们怎么去描述超亲密关系;
2. 我们怎么去对超亲密关系的容器或者说是业务去做统一调度,这是Pod最主要的一个诉求。
Pod的实现机制
Pod要解决的问题
像Pod这样一个东西,本身就是一个逻辑概念。那在机器上,它究竟是怎么实现的呢?
既然说Pod要解决这个问题,核心就在于如何让一个Pod里的多个容器之间最高效的共享某些资源和数据。
因为容器之间原本是被Linux Namespace和cgroups隔开的,所以现在实际要解决的是怎么去打破这个隔离,然后共享某些事情和某些信息。这就是Pod的设计要解决的核心问题所在。
所以说具体的解法分为两部分:网络和存储
共享网络
看一个例子:比如说现在有一个Pod,其中包含了一个容器A和容器B,它们两个就要共享Network Namespace。在Kubernetes里的解法是这样的:它会在每个Pod里,额外起一个Infra container小容器来共享整个Pod的Network Namespace。
Infra container是一个非常小的镜像,大概100~200KB左右,是一个汇编语言写的、永远处于“暂停”状态的容器。由于有了这样一个Infra container之后,其他所有容器都会通过Join Namespace的方式加入到Infra container的Network Namespace中。
所以说一个Pod里面的所有容器,它们看到的网络视图是完全一样的。即:它们看到的网络设备、IP地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于Pod第一次创建的这个Infra container。这就是Pod解决网络共享的一个解法。
在Pod里面,一定有一个IP地址,是这个Pod的Network Namespace对应的地址,也是这个Infra container的IP地址。所以大家看到的都是一份,而其他所有网络资源,都是一个Pod一份,并且被Pod中的所有容器共享。这就是Pod的网络实现方式。
由于需要一个相当于说中间的容器存在,所以整个Pod里面,必然是Infra container第一个启动。并且整个Pod的生命周期是等同于Infra container的生命周期的,与容器A和B是无关的。这也是为什么在Kubernetes里面,它是允许去单独更新Pod里的某一个镜像的,即:做这个操作,整个Pod不会重建,也不会重启,这是非常重要的一个设计。
共享存储
比如说现在有两个容器,一个是Nginx,另外一个是非常普通的容器,在Nginx里面放一些文件,让我能通过Nginx访问到。所以它需要share这个目录。我share文件或者是share目录在Pod里面是非常简单的,实际上就是把volume变成Pod level。然后所有容器,就是所有同属于一个Pod的容器,他们共享所有的volume。
比如说上图这个例子,这个volume叫做shared-data,它是属于Pod level的,所以在每一个容器里可以直接声明:要挂载shared-data这个volume,只要你声明了你挂载这个volume,你在容器里去看这个目录,实际上大家看到的的就是同一份。这个就是Kubernetes通过Pod来给容器共享存储的一个做法。
所以在之前的例子中,应用容器App写了日志,只要这个日志是写在一个volume中,只要声明挂载了体样的volume,这个volume就可以立即被另外一个LogCollector容器给看到。以上就是Pod实现存储的方式。
详解容器设计模式
举例
比如说我现在有一个非常常见的一个诉求:我现在要发布一个应用,这个应用是JAVA写的,有一个WAR包需要把它放到Tomcat的web APP目录下面,这样就可以把它启动起来了。可是像这样一个WAR包或者Tomcat这样的容器的话,怎么去做,怎么去发布?
第一种方式:可以把WAR包和Tomcat打包放进一个镜像里面。但是这样带来一个问题,就是现在这个镜像实际上揉进了两个东西。那么接下来,无论是我要更新WAR包还是说我要更新Tomcat,都要重新做一个新的镜像,这是比较麻烦的;
第二种:就是镜像里面只打包Tomcat。它就是一个Tomcat,但是需要使用数据卷的方式,比如说hostPath,从宿主机上把WAR包挂载进我们Tomcat容器中,挂到我的web APP目录下面,这样把这个容器启用起来之后,里面就能用了。
但是这时会发现一个问题:这种做法一定需要维护一套分布式存储系统。因为这个容器可能第一次启动是在宿主机A上面,第二次重新启动就可能跑到B上去了,容器它是一个可迁移的东西,它的状态是不保持的。所以必须维护一套分布式存储系统,使容器不管是在A还是在B上,都可以找到这个WAR包,找到这个数据。
注意,即使有了分布式存储系统做volume,你还需要负责维护Volume里的WAR包。比如:你需要单独写一套Kubernetes Volume插件,用来在每次Pod启动之前,把应用启动所需的WAR包下载到这个Volume里,然后才能被应用挂载使用到。
这样操作带来的复杂程度还是比较高的,且这个容器本身必须依赖于一套持久化的存储插件(用来管理Volume里的WAR包内容)。
InitContainer
像这样的组合方式,有没有更加通用的方法?哪怕在本地Kubernetes上,没有分布式存储的情况下也能玩、能用、能发布。
在Kubernetes里面,像这样的组合方式,叫做Init Container。
还是同一个例子:在上图的yaml里,首先定义一个Init Container,它只做一件事情,就是把WAR包从镜像里拷贝到一个Volume里面,它做完这个操作就退出了,所以Init Container会比用户容器先启动,并且严格按照定义顺序来依次执行。
然后,这个关键在于刚刚拷贝到的这样一个目的目录:APP目录,实际上是一个Volume。而我们前面提到,一个Pod里面的多个容器,它们是可以共享Volume的,所以现在这个Tomcat容器,只是打包了一个Tomcat镜像。但在启动的时候,要声明使用APP目录作为我的Volume,并且要把它们挂载在Web APP目录下面。
而这个时候,由于前面已经运行过了一个Init Container,已经执行完拷贝操作了,所以这个Volume里面已经存在了应用的WAR包:就是sample.war,绝对已经存在这个Volume里面了。等到第二步执行启动这个Tomcat容器的时候,去挂这个Volume,一定能在里面找到前面拷贝来的sample.war。
所以可以这样去描述:这个Pod就是一个自包含的,可以把这一个Pod在全世界任何一个Kubernetes上面都顺利启动起来。不用担心没有分布式存储、Volume不是持久化的,它一定是可以公布的。
所以这是一个通过组合两个不同角色的容器,并且按照这样一些像Init Container这样一种编排方式,统一的去打包这样一个应用,把它用Pod来去做的非常典型的一个例子。像这样的一个概念,在Kubernetes里面就是一个非常经典的容器设计模式,叫做:“Sidecar”。
Sidecar
什么是Sidecar?就是说其实在Pod里面,可以定义一些专门的容器,来执行主业务容器所需要的一些辅助工作,比如我们前面举的例子,其实就干了一件事,这个Init Container,它其实就是一个Sidecar,它只负责把镜像里的WAR包拷贝到共享目录里面,以便被Tomcat能够用起来。
其他有哪些操作呢?比如说:
1. 原本需要在容器里面执行SSH需要干的一些事情,可以写脚本、一些前置条件,其实都可以通过像Init Container或者另外像Sidecar的方式去解决;
2.当然还有一个典型的例子就是我的日志收集,日志收集本身是一个进程,是一个容器,那么就可以把它打包进Pod里面去做这个收集工作;
3. 还有一个非常重要的东西就是Debug应用,实际上现在Debug整个应用都可以在应用Pod里面再定义一个额外的小的Container,它可以去exec应用Pod的namespace;
4. 查看其它容器的工作状态,这也是它可以做的。不再需要SSH登陆到容器里面去看,只要把监控组件装到额外的小容器里面就可以了,然后把它作为一个Sidecar启动起来,跟业务容器进行协作,所以同样业务监控也都可以通过Sidecar方式去做。
这种做法一个非常明显的优势就是在于其实将辅助功能从我的业务容器解耦了,所以我就能够独立发布Sidecar容器,并且更重要的是这个能力是可以重用的,即同样的一个监控Sidecar或者日志Sidecar,可以被全公司的人共用的。
应用与日志收集
前面提到的应用日志收集,业务容器将日志写在一个Volume里面,而由于Volume在Pod里面是被共享的,所以日志容器---即Sidecar容器一定可以通过共享改Volume,直接把日志文件读出来,然后存到远程存储里面,或者转发到另外一个例子。现在业界常用的Fluentd日志进程或者日志组件,基本上都是这样的工作方式。
代理容器(Proxy)
假如现在有个Pod需要访问一个外部系统,或者一些外部服务,但是这些外部系统是一个集群,那么这个时候如何通过一个统一的、简单的方式,用一个IP地址,就把这些集群都访问到呢?有一种方法就是:修改代码。因为代码里记录了这些集群的地址;另外还有一种解耦方法,即通过Sidecar代理容器。
简单来说,单独写一个这么小的Proxy,用来处理对接外部的服务集群,它对外暴露出来的只有一个IP地址就可以了。所以接下来,业务容器主要访问Proxy,然后由Proxy去连接这些服务集群,这里的关键在于Pod里面多个容器是通过localhost直接通信的,因为它们它属于一个network Namespace,网络视图都一样,所以它们俩通信localhost,并没有性能损耗。
所以说代理容器除了做了解耦之外,并不会降低性能,更重要的是,像这样一个代理容器的代码就又可以被全公司重用了。
适配器容器(Adapter)
现在业务暴露出来的API,比如说有个API的一个格式是A,但是现在有一个外部系统要去访问我的业务容器,它只知道的一种格式是API B,所以要做一个工作,就是把业务容器怎么想办法改掉,要去改业务代码。但实际上,你可以通过一个Adapter帮你来做这层转换。
现在有个例子:现在业务容器暴露出来的监控接口是/metrics,访问这个容器的metrics的这个URL就可以拿到了。可是现在,这个监控系统升级了,它访问的URL是/health,我只认得暴露出health健康检测的URL,才能去做监控,metrics不认识。那这个怎么办?那就需要改代码了,但可以不去改代码,而是额外写一个Adapter,用来把所有对health的这个请求转发给metrics就可以了,所以这个Adapter对外暴露的是health这样一个监控的URL,这就可以了,你的业务就又可以工作了。
这样的关键还在于Pod之中的容器是通过localhost直接通信的,所以没有性能损耗,并且这样一个Adapter容器可以被全公司重用起来。
应用编排与管理:核心原理
资源元信息
Kubernetes资源对象
首先,我们来回顾一下Kubernetes的资源对象组成:注要包括了Spec、Status两部分。其中Spec部分用来描述期望的状态,Status部分用来描述观测到的状态。
现在我们来了解一下K8s的另外一个部分,即元数据部分。该部分主要包括了用来识别资源的标签:Labels,用来描述资源的注解:Annotation,用来描述多个资源之间的相互关系的OwnerReference。这些元数据在K8s运行中有非常重要的重要作用。
Labels
第一个元数据,也是非常重要的一个元数据是:资源标签。资源标签是一种具有标识型的Key:Value元数据,这里展示了几个常见的标签。
前三个标签都打在了Pod对象上,分别标识了对应的应用环境、发布的成熟度和应用的版本。从应用标签的例子可以看到,标签的名字包括了一个域名的前缀,用来描述打标签的系统和工具,最后一个标签打在了Node对象上,还在域名前增加了版本的标识beta字符串。
标签主要用来筛选资源和组合资源,可以使用类似于SQL查询select,来根据Label查询相关资源。
Selector
最常见的Selector就是相等型Selector。
假设系统中有四个Pod,每个Pod都有标识系统层级和环境的标签,我们通过Tie:front这个标签,可以匹配左边栏的Pod,相等型Selector还可以包括多个相等条件,多个相等条件之间是逻辑“与”的关系。
在刚才的例子中,通过Tie=front,Env=dev的Selector,我们可以筛选出所有的Tie=front,而且Env=dev的Pod,也就是上图中左下角的Pod。另外一种Selector是集合型Selector,在例子中,Selector筛选所有环境是test或者是gray的Pod。
Annotations
一般是系统或者工具用来存储资源的非标示性信息,可以用来扩展资源的spec/status的描述。
第一个例子,存储了阿里云负载器的证书ID,我们可以看到annotations一样可以拥有域名的前缀,标注中也可以包含版本信息。
第三个annotations一般可以在Kubectl apply命令行操作后的资源中看到,annotation值是一个结构化的数据,实际上是一个json串,标记了上一次kubectl操作的资源的json的描述。
OwnerReference
所谓所有者,一般就是指集合类的资源,比如说Pod集合,就有replication、statefulset。
集合类资源的控制器会创建对应的归属资源。比如:replicaset控制器在操作中会创建Pod,被创建Pod的Ownerreference就指向了创建Pod的replicaset,Ownerreference使得用户可以方便地查找一个创建资源的对象,另外,还可以用来实现级联删除的效果。
操作演示
去网页查看:https://edu.aliyun.com/lesson_1651_18353?spm=5176.10731542.0.0.598120bebTC4KI#_18353
控制器模式
控制循环
控制模式最核心的就是控制循环的概念。在控制循环中包括了控制器,被控制的系统,以及能够观测系统的传感器,三个逻辑组件。
当然这些组件都是逻辑的,外界通过修改资源spec来控制资源,控制器比较资源spec和status,从而计算一个diff,diff最后会用来决定执行对系统进行什么样的控制操作,控制操作会使得系统产生新的输出,并被传感器以资源status形式上报,控制器的各个组件将都会是独立自主地运行,不断使系统向spec表示终态趋近。
Sensor
控制循环中逻辑的传感器主要由Reflector、Informer、Indexer三个组件构成。
Reflector通过List和Watch K8s server来获取资源的数据。List用来在Controller重启以及Watch中断的情况下,进行系统资源的全量更新;而Watch则在多次List之间进行增量的资源更新;Reflector在获取新的资源数据后,会在Delta队列中塞入一个包括资源对象信息本身以及资源对象事件类型的Delta记录,Delta队列中可以保证同一个对象在队列中仅有一条记录,从而避免Reflector重新List和Watch的时候产生产生重复的记录。
Informer组件不断地从Delta队列中弹出delta记录,然后把资源对象交给indexer,让indexer把资源记录在一个缓存中,缓存在默认设置下是用资源的命名空间来做索引的,并且可以被Controller Manager或多个Controller所共享。之后,再把这个事件交给事件的回调函数。
控制循环中的控制器组件主要由事件处理函数以及worker组成,事件处理函数之间会互相关注资源的新增、更新、删除的事件,并根据控制器的逻辑去决定是否需要处理。对需要处理的事件,会把事件关联资源的命名空间以及名字塞入一个工作队列中,并且由后续的worker池中的一个worker来处理,工作队列会对存储的对象进行去重,从而避免多个Worker处理同一个资源的情况。
Worker在处理资源对象时,一般需要用资源的名字来重新获得最新的资源数据,用来创建或者更新资源对象,或者调用其它的外部服务,Worker如果处理失败的时候,一般情况下会把资源的名字重新加入到工作队列中,从而方便之后进行重试。
控制循环例子-扩容
ReplicaSet是一个用来描述无状态应用的扩缩容行为的资源,ReplicaSet controller通过监听ReplicaSet资源来维持应用希望的状态数量,ReplicaSet中通过selector来匹配所关联的Pod,在这里考虑ReplicaSet reA的,replicas从2被改到3的场景。
首先,Refletor会watch到ReplicaSet和Pod两种资源的变化,为什么我们还会watch pod资源的变化稍后讲解。发现ReplicaSet发生变化后,在delta队列中塞入了对象是rsA,而且类型是更新的记录。
Informer一方面把新的ReplicaSet更新到缓存中,并与Namespace nsA作为索引。另一方面,调用Update的回调函数,ReplicaSet控制器发现ReplicaSet发生变化后会把字符串的nsA/rsA字符串塞入到工作队列中,工作队列后的一个Worker从工作队列中取到了nsA/rsA这个字符串的key,并且从缓存中取到了最新的ReplicaSet数据。
Worker通过比较ReplicaSet中spec和status里的数值,发现需要对这个ReplicaSet进行扩容,因此ReplicaSet的Worker创建了一个Pod,这个Pod中的Ownereference取向了ReplicaSet rsA。
然后Reflector Watch到的Pod新增事件,在delta队列中额外加入了Add类型的data记录,一方面把新的Pod记录通过Indexer存储到了缓存中,另一方面调用了ReplicaSet控制器的Add回调函数,Add回调函数通过检查pod ownerReferences找到了对应的ReplicaSet,并把包括ReplicaSet命名空间和字符串塞入到了工作队列中。
ReplicaSet的Worker在得到新的工作项之后,从缓存中取到了新的ReplicaSet记录,并得到了其所有创建的Pod,因为ReplicaSet的状态不是最新的,也就是所有创建Pod的数量不是最新的。因此在此时ReplicaSet更新status使得spec和status达成一致。
控制器模式总结
两种API设计方法
Kubernetes控制器模式依赖声明式的API。另外一种常见的API类型是命令式API。为什么Kubernetes采用声明式API,而不是命令式API来设计整个控制器呢?
首先,比较两种API在交互行为上的差别。在生活中,常见的命令式的交互方式是家长和孩子交流方式,因为孩子欠缺目标意识,无法理解家长期望,家长往往通过一些命令,教孩子一些明确的动作,比如:吃饭、睡觉类似的命令。我们在容器编排体系中,命令式API就是通过向系统发出明确的操作来执行的。
而常见的声明式交互方式,就是老板对自己员工的交流方式。老板一般不会给自己的员工下很明确的决定,实际上可能老板对于要操作的事情本身,还不如员工清楚。因此,老板通过给员工设置可量化的业务目标的方式,来发挥员工自身的主观能动性。比如说,老板会要求某个产品的市场占有率达到80%,而不会指出要达到这个市场占有率,要做的具体操作细节。
类似的,在容器编排体系中,我们可以执行一个应用实例副本数保持在3个,而不用明确的去扩容Pod或是删除已有的Pod,来保证副本数在三个。
命令式API的问题
命令式API最大的一个问题在于错误处理
在大规模的分布式系统中,错误是无处不在的。一旦发出的命令没有响应,调用方只能通过反复重试的方式来试图恢复错误,然而盲目的重试可能会带来更大的问题。
假设原来的命令,后台实际上已经执行完成了,重试后又多执行了一个重试的命令操作。为了避免重试的问题,系统往往需要在执行命令前,先记录一下需要执行的命令,并且在重启等场景下,重做待执行的命令,而且在执行的过程中,还需要考虑多个命令的先后顺序、覆盖关系等等一些复杂的逻辑情况。
实际上许多命令式的交互系统后台往往还会做一个巡检的系统,用来修正命令处理超时、重试等一些场景造成数据不一致的问题;
然而,因为巡检逻辑和日常操作逻辑是不一样的,往往在测试上覆盖不够,在错误处理上不够严谨,具有很大的操作风险,因此往往很多巡检系统都是人工来触发的。
最后,命令式API在处理多并发访问时,也很容易出现问题;
假如有多方并发的对一个资源请求进行操作,并且一旦其中有操作出现了错误,就需要重试。那么最后哪一个操作失效了,就很难确认,也无法保证。很多命令式系统往往在操作前会对系统进行加锁,从而保证整个系统最后失效行为的可预见性,但是加锁行为会降低整个系统的操作执行效率。
相对的,声明式API系统里天然地记录了系统现在和最终的状态。
不需要额外的操作数据。另外因为状态的幂等性,可以在任意时刻反复操作。在声明式系统运行的方式里,正常的操作实际上就是对资源状态的巡检,不需要额外开发巡检系统,系统的运行逻辑也能够在日常的运行中得到测试和锤炼,因此整个操作的稳定性能够得到保证。
最后,因为资源的最终状态是明确的,我们可以合并多次对状态的修改。可以不需要加锁,就支持多方的并发访问。
1. Kubernetes所采用的控制器模式,是由声明式API驱动的。确切来说,是基于对Kubernetes资源对象的修改来驱动的;
2. Kubernetes资源之后,是关注该资源的控制器。这些控制器将异步的控制系统向设置的终态趋近;
3. 这些控制器是自主运行的,使得系统的自动化和无人值守成为可能;
4. 因为Kubernetes的控制器和资源都是可以自定义的,因此可以方便的扩展控制器模式。特别是对于有状态应用,我们往往通过自定义资源和控制器的方式,来自动化运维操作。这个也就是后续会介绍的operator的场景。
章节总结:
Kubernetes资源对象中的元数据部分,主要包括了用来识别资源的标签:Labels,用来描述资源的注解;Annotations,用来描述多个资源之间相互关系的OwnerReference。这些元数据在K8s运行中有非常重要的作用;
控制型模式中最核心的就是控制循环的概念;
两种API设计方法:声明式API和命令式API;Kubernetes所采用的控制器模式,是由声明式API驱动的;
应用编排与管理: Deployment
需求来源
背景问题
如图所示:如果我们直接管理集群中所有的Pod,应用A、B、C的Pod,其实是散乱地分布在集群中。
现在有以下的问题:
首先,如何保证集群内可用Pod的数量?也就是说我们应用A四个Pod如果出现了一些宿主机故障,或者一些网络问题,如何能保证它可用的数量?
如何为所有Pod更新镜像版本?我们是否要某一个Pod去重建新版本的Pod?
然后在更新过程中,如何保证服务的可用性?
以及更新过程中,如果发现了问题,如何快速回滚到上一个版本?
Deployment:管理部署发布的控制器
可以看到我们通过Deployment将应用A、B、C分别规划到不同的Deployment中,每个Deployment其实是管理的一组相同的应用Pod,这组Pod我们认为它是相同的一个副本,那么Deployment能帮我们做什么事情呢?
1. 首先,Deployment定义了一种Pod期望数量,比如说应用A,我们期望Pod数量是四个,那么这样的话,controller就会持续维持Pod数量为期望的数量。当我们与Pod出现了网络问题或者宿主机问题的话,controller能帮我们恢复,也就是新扩出来对应的Pod,来保证可用的Pod数量与期望数量一致;
2. 配置Pod发布方式,也就是说controller会按照用户给定的策略来更新Pod,而且更新过程中,也可以设定不可用Pod数量在多少范围内;
3. 如果更新过程中发生问题的话,即所谓“一键回滚”,也就是说你通过一条命令或者一行修改能够将Deployment下面所有Pod更新为某一个旧版本。
用例解读
Deployment语法
上图可以看到一个最简单的Deployment的yaml文件。
\"apiVersion : apps/v1\",也就是说Deployment当前所属的组是apps,版本是v1。\"metadata\"是我们看到的Deployment元信息,也就是往期回顾中的Labels、Selector、Pod.image,这些都是往期提到的知识点。
Deployment作为K8s资源,它有自己的metadata元信息,这里我们定义的Deployment.name是nginx.Deployment。Deployment.spec中首先要有一个核心的字段,即replicas,这里定义期望的Pod数量为三个;selector其实是Pod选择器,那么所有扩容出来的Pod,它的Labels必须匹配selector层上的image.lebels,也就是app.nginx。
就如上面的Pod模板template中所述,这个template它其实包含了两部分内容:
一部分是我们期望Pod的metadata,其中包含了labels,即跟selector.matchLabels相匹配的一个Labels;
第二部分是template包含的一个Pod.spec。这里Pod.spec其实是Deployment最终创建出来Pod的时候,它所用的Pod.spec,这里定义了一个container.nginx,它的镜像版本是nginx:1.7.9。
下面是遇到的新知识点:
第一个是replicas,就是Deployment中期望的或者终态数量;
第二个是template,也就是Pod相关的一个模板。
查看Deployment状态
当我们创建出一个Deployment的时候,可以通过Kubectl get deployment,看到Deployment总体的一个状态。如图所示:
重点讲解一下AVAILABLE:这个其实是运行过程中可用的Pod数量。后面会提到,这里AVAILABLE并不简单是可用的,也就是Ready状态的,它其实包含了一些可用超过一定时间长度的Pod;
查看Pod
上图中有三个Pod,Pod名字格式我们不难看到。
最前面一段Lnginx-deployment,其实是Pod所属Deployment.name;之间一段:template-hash,这里三个Pod是一样的,因为这三个Pod其实都是同一个template中创建出来的。
最后一段,是一个random的字符串,我们通过get.pod可以看到,Pod的ownerReference即Pod所属的controller资源,并不是Deployment,而是一个ReplicaSet。这个ReplicaSet的name,其实是nginx-deployment加上pod.template-hash,后面会提到。所有的Pod都是ReplicaSer创建出来的,而ReplicaSet它对应某一个具体的Deployment.template版本。
更新镜像
接下来我们可以看一下,如何对一个给定的Deployment更新它所有Pod的镜像版本呢?这里我们可以执行一个kubectl命令:
kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.9.1
首先kubectl后面有一个set image固定写法,这里指的是设定镜像;其次是一个deployment.v1.apps,这里也是一个固定写法,写的是我们要操作的资源类型,deployment是资源名、v1是资源版本、apps是资源组,这里也可以简写为deployment或者deployment.apps,比如说写为deployment的时候,默认将使用apps组v1版本。
第三部分是要更新的deployment的name,也就是我们的nginx-deployment;再往后的nginx其实指的是template,也就是Pod中的container.name;这里我们可以注意到:一个Pod中,其实可能存在多个container,而我们指定想要更新的镜像的container.name,就是nginx。
最后,指定我们这个容器期望更新的镜像版本,这里指的是nginx: 1.9.1。如下图所示:当执行完这条命令之后,可以看到deployment中的template.spec已经更新为nginx: 1.9.1。
快速回滚
如果我们在发布过程中遇到了问题,也支持快速回滚。通过kubectl执行的话,其实是\"kubectl rollout undo\"这个命令,可以回滚到Deployment上一个版本;通过\"rollout undo\"加上\"to-revision\"来指定可以回滚到某一个具体的版本。
DeploymentStatus
最后我们来看一下DeploymentStatus。前面我们学习到,每一个资源都有它的spec.Status。这里可以看一下,deploymentStatus中描述的三个其实是它的conversion状态,也就是Processing、Complete以及Failed。
以Processing为例:Processing指的是Deployment正在处于扩容和发布中。比如说Processing状态的deployment,它所有的replicas及Pod副本全部达到最新版本,而且是available,这样的话,就可以进入complete状态。而complete状态如果发生了一些扩缩容的话,也会进入Processing这个处理工作状态。
如果在处理过程中遇到一些问题:比如说拉取镜像失败了,或者说readiness probe检查失败了,就会进入failed状态;如果在运行过程中即complete状态,中间运行时发生了一些pod readiness probe检查失败,这个时候deployment也会进入failed状态。进入failed状态之后,除非所有点replicas均变成available,而且是update最新版本,deployment才会重新进入complete状态。
Deployment创建及状态
这里连接一个阿里云服务集群。我们可以看到当前集群已经有几个可用的node
首先创建对应的deployment。可以看到deployment中的desired(渴望的)、current(当前的)、up-to-date(到达最新的期望版本的Pod数量)以及available已经都达到了可用的期望状态。
Deployment的结果
这里看到spec中replicas是三个,selector以及template labels中定义的标签都是app: nginx,spec中的image是我们期望的nginx: 1.7.9;status中的available.replicas,readReplicas以及updatedReplicas都是3个。
Pod状态
可以看到:Pod中ownerReferences的功能是ReplicaSet;pod.spec.container里的镜像是1.7.9。这个Pod已经是Running状态,而且它的condition.status是“true”,表示它的服务已经可用了。
更新升级
当前只有最新版本的replicaset,那么现在尝试对deployment做一次升级。
“kubectl set image”这个操作命令,后面接“deployment”,加deployment.name,最后指定容器名,以及我们期望升级的镜像版本。
接下来我们看下deployment中的template中的image已经更新为1.9.1。
这个时候我们再来get pod看一下状态,三个pod已经升级为新版本,pod名字中的pod-template-hash也已更新。
可以看到:旧版本replicaset的spec数量以及pod数量都是0,新版本的pod数量是3个。
假设又做了一次更新,这个时候get pod其实可以看到:当前的pod其实是有两个旧版本的处于running,另一个旧版本是在删除中;而两个新版本的pod,一个已经进入running,一个还在creating中。
这时我们可用的pod数量即非删除状态的pod数量,其实是4个,已经超过了replica原先在deployment设置的数量3个。这个原因是我们在deployment中有maxavailable和maxsugar两个操作,这两个配置可以限制我们在发布过程中的一些策略。
历史版本保留 revisionHistoryLimit
上图我们可以看到最新版本的replicaset是3个pod,另外还有两个历史版本的replicaset,那么会不会存在一种情况:就是随着deployment持续更新,这个旧版本的replicaset会越积越多呢?其实deployment提供了一个机制来避免这个问题:在deployment spec中,有一个revisionHistoryLimit,它默认值是10,它其实保证了保留历史版本的replicaset的数量,我们尝试把它改为1.
由上面两张图,可以看到两个replicaset,也就是说,除了当前版本的replicaset之外,旧版本的replicaset其实只保留了一个。
回滚
最后,我们尝试做一下回滚。首先再来看replicaset,这时发现旧版本的replicaset数量从0增加到了2个,而新版本的replicaset数量从3个削减为1个,表示它已经开始在做回滚的操作。然后再观察一下,旧版本的数量已经是3个,即已经回滚成功,而新版本的pod数量变为0个。
我们最后再get pod看一下:
这时,3个pod.template-hash已经更新为旧版本的hash,但其实这3个pod都是重新创建出来的,而并非我们在前一个版本中创建的3个pod。换句话说,也就是我们回滚的时候,其实是创建了3个旧版本的pod,而并非把先前的3个pod找回来。
架构设计
管理模式
我们来看一下架构设计。首先简单看一下管理模式:Deployment只负责管理管理不同版本的ReplicaSet,由ReplicaSet来管理具体的Pod副本数,每个ReplicaSet对应Deployment template的一个版本。在上文的例子中可以看到,每一次修改template,都会生成新的ReplicaSet,这个ReplicaSet底下的Pod其实都是相同的版本。
如上图所示:Deployment创建ReplicaSet,而ReplicaSet创建Pod。他们的OwnerRef其实都对应了其控制器的资源。
Deployment控制器
首先,我们所有的控制器都是通过Informer中的Event做一些Handler和Watch。这个地方Deployment控制器,其实是关注Deployment和ReplicaSet 中的Event,收到事件后会加入到队列中。而Deployment controller从队列中取出来之后,它的逻辑会判断Check Paused,这个Paused其实是Deployment是否需要新的发布,如果Paused设置为true的话,就表示这个Deployment只会做一个数量上的维持,不会做新的发布。
如上图,可以看到如果Check Paused为Yes也就是true的话,那么只会做Sync replicas。也就是说把replicas sync同步到对应的ReplicaSet中,最后再update Deployment status,那么controller这一次的ReplicaSet就结束了。
那么如果paused为false的话,它就会做Rollout,也就是通过Create或者是Rolling的方式来做更新,更新的方式其实也是通过Create/Update/Delete这种ReplicaSet来做实现的。
ReplicaSet控制器
当Deployment分配ReplicaSet之后,ReplicaSet控制器本身也是从Informer中watch一些事件,这些事件包含了ReplicaSet和Pod的事件。从队列中取出之后,ReplicaSet controller的逻辑很简单,就只管理副本数。也就是说如果controller发现replicas比Pod数量大的话,就会扩容,而如果发现实际数量超过期望数量的话,就会删除Pod。
上面Deployment控制器的图中可以看到,Deployment控制器其实做了更复杂的事情,包含了版本控制,而它把每一个版本下的数量维持工作交给了ReplicaSet来做。
扩/缩容模拟
下面看一些操作模拟,比如说扩容模拟。这里有一个Deployment,它的副本数是2,对应的ReplicaSet有pod1、pod2。这时如果我们修改Deployment replicas,controller就会把replicas同步到当前版本的ReplicaSet中,这个ReplicaSet发现当前有2个Pod,不满足当前期望的3个,就会创建一个新的Pod3。
发布模拟
发布的情况会稍微复杂一点。这里可以看到Deployment当前初始的template,比如说template1这个版本。template1这个ReplicaSet对应的版本下有三个Pod:Pod1,Pod2,Pod3。
这时修改template中一个容器的image,Deployment controller就会新建一个对应template2的ReplicaSet。创建出来之后ReplicaSet会逐渐修改两个ReplicaSet的数量,比如它会逐渐增加ReplicaSet2中的replicas的期望数量,而逐渐减少ReplicaSet1中的Pod数量。
那么最终达到的效果是:新版本的Pod为Pod4、Pod5和Pod6,旧版本的Pod已经被删除了,这里就完成了一次发布。
回滚模拟
根据上面的发布模拟可以知道Pod4、Pod5和Pod6已经发布完成。这时发现当前的业务版本是有问题的,如果做回滚的话,不管是通过rollout命令还是通过回滚修改template,它其实都是把template回滚为旧版本的template1。
这个时候Deployment会重新修改ReplicaSet1中的Pod的期望数量,把期望数量修改为3个,且会逐渐减少新版本也就是ReplicaSet2中的replicas数量,最终的效果就是把Pod从旧版本重新创建出来。
发布模拟的图中可以看到,其实初始版本中Pod1、Pod2、Pod3是旧版本,而回滚之后其实是Pod7、Pod8、Pod9。就是说它回滚并不是把之前的Pod重新找回来,而是说重新创建出符合旧版本template的Pod.
spec字段解析
首先看一下Deployment中其它的spec字段:
MinReadySeconds
Deployment会根据Pod ready来看Pod是否可用,但是如果我们设置了MinReadySeconds之后,比如设置为30秒,那么Deployment就一定会等到Pod ready超过30秒之后才会认为Pod是available的。Pod available的前提条件是Pod ready,但是ready的Pod不一定是available的,它一定要超过MinReadySeconds之后,才会判断为available;
revisionHistoryLimit
保留revision,即保留历史ReplicaSet的数量,默认值是10个。这里可以设置为一个或者两个,如果回滚可能性比较大的话,可以设置数量超过10;
paused
paused是标识,Deployment只做数量维持,不做新的发布,这里在Debug场景可能会用到;
progressDeadineSeconds
前面提到当Deployment处于扩容或者发布状态时,它的condition会处于一个processing的状态,processing可以设置一个超时时间。如果超过超时时间还处于processing,那么controller将认为这个Pod会进入failed的状态。
升级策略字段解析
Deployment在RollingUpdate中主要提供了两个策略,一个是MaxUnavalable,另一个是MaxSurge。这两个字段解析的意思,可以看下图中详细的comment,或者简单解释一下:
MaxUnavailable
滚动过程中最多有多少个Pod不可用;
MaxSurge
滚动过程中最多存在多少个Pod超过预期replicas数量。
上文中提到,ReplicaSet为3的Deployment在发布的时候可能存在一种情况:新版本的ReplicaSet和旧版本的ReplicaSet都可能有两个replicas,加在一起就是4个,超过了我们期望的数量三个。这是因为我们默认的MaxUnavailable和MaxSurge都是25%,默认Deployment在发布的过程中,可能有25%的replica是不可用的,也可能超过replica数量25%是可用的,最高可以达到125%的replica数量。
这里其实可以根据用户实际场景来做设置。比如当用户的资源足够,且更注意发布过程中的可用性,可设置MaxUnavailable较小、MaxSurge较大。但如果用户的资源比较紧张,可以设置MaxSurge较小,甚至设置为0,这里要注意的是MaxSurge和MaxUnavailable不能同时设置为0。
理由不难理解,当MaxSurge为0的时候,必须要删除Pod,才能扩容Pod;如果不删除Pod是不能新扩Pod的,因为新扩出来的话,总共的Pod数量就会超过期望数量。而两者同时为0的话,MaxSurge保证不能新扩Pod,而MaxUnavailable不能保证ReplicaSet中有Pod是available的,这样就会产生问题。属于说这两个值不能同时为0.用户可以根据自己的实际场景来设置对应的、合适的值。
简单总结
Deployment是Kubernetes中常见的一种Workload,支出部署管理多版本的Pod;
Deployment管理多版本的方式,是针对每个版本的template创建一个ReplicaSet,由ReplicaSet维护一定数量的Pod副本,而Deployment只需要关心不同版本的ReplicaSet里要指定多少数量的Pod;
因此,Deployment发布部署的根本原理,就是Deployment调整不同版本ReplicaSet里的终态副本数,以此来达到多版本Pod的升级和回滚。
0 条评论
回复 删除
下一页