K8S学习之当我们部署应用的时候都发生了什么?

时间:2022-12-23 12:06:41

   前言

  最近在学习K8S,基础的知识参考于《Kubernetes in Action》。看完整本书基本上用了2~3个月的时间,进度比较慢;主要都是每天早晨到公司后,在正常工作时间之前的1个小时里完成的。由于时间拉的很长,各章的知识在我脑袋里是散状的,所以在我整理这篇笔记的时候,就想通过一个主题,把相关的内容串起来。第一篇笔记定的主题是“调度-当我们部署应用的时候都发生了什么?”,先从大的框架上记录一下K8S的架构与原理;对于卷、网络、configmap等组件会放在第二篇。初学者级别的学习笔记,有问题的地方大佬们及时勘误。

  我在这里先给大家简单描述一下,当我们在aone里手动点击了升级之后,k8s里都发生了什么?

  当我们在aone里手动点击了重新部署,aone会通过k8s的api接口通知master节点创建一个Deployment,Deployment会按照配置里的声明要求创建一个新的RepliactionSet,RepliactionSet会按照配置的创建一个或者若干个Pod,pod会调度到相应的工作节点上,通过docker拉取镜像,启动应用。

  如果你能完全看懂我的描述,并且知道这些英文的工作原理,那你可以关掉网页了;如果你看完这段描述并不知道我在说什么,并且还想了解”当我们部署应用的时候都发生了什么?“的话,那请继续往下看。

   经常混在一起出现的名词:Docker&虚拟机&容器

  我在看之前经常会把一些名词”搞混“,至少是没太明确的了解其概念和关系-Docker&虚拟机&容器。

  Docker&虚拟机

  应用容器引擎:让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化;如Docker、RKT等。

  虚拟机(Virtual Machine):指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统;如vmware、virtualBox。

  其实严格意义上来讲,Docker和虚拟机不是同一个层级的定义。应用容器引擎和虚拟机是俩种虚拟技术方案,而Docker只是应用容器引擎中最出名的,RKT是后起之秀。  

K8S学习之当我们部署应用的时候都发生了什么?

  左图为Docker容器,右图为虚拟机系统,我们从下往上对比大概有如下四个区别

  Docker共享宿主操作系统的内核,虚拟机独立操作系统内核

  Docker不同容器之间资源隔离,虚拟机在同一个VM上的app共享资源

  Docker镜像分层,相同的层可以共享,减少网络传输压力;虚拟机镜像不能共享

  Docker可移植性没有虚拟机好,需要考虑内核版本等;虚拟机完全独立可以移植

  其实这跟我们很多中台系统建设的俩个极端是很像的,灵活性越高,通用性就越差;通用性好那就难免要做一些定制化的需求。

  容器

  容器技术:同一台机器上运行多个服务,提供不同的环境给服务,服务之间相互隔离。

  容器是通过linux内核上提供的cgroups和 Namespace来实现隔离的。cgroups将进程分组管理的内核功能通过cgroups可以隔离进程,同时还可以控制进程的资源占用(CPU、内存等等)情况在操作系统底层限制物理资源;Namespace用来隔离IP地址、用户空间、进程ID。

  值得提前一提的是容器的文件系统隔离,App的写入完全隔离,如果修改底层文件,则复制一份到容器所挂载的文件系统内,不直接修改底层文件。

   K8S简介-What Why How

  What&Why

  从上一节的介绍应该可以看出来,一个应用的部署使用Docker已经完全可以运行起来,那K8S是什么,在整个应用声明周期中的作用是什么?先看一下用户在使用docker部署应用的过程。  

K8S学习之当我们部署应用的时候都发生了什么?

  Docker部署的时候,需要开发者来做运维操作的有俩步,一是用Docker构建和推送镜像到镜像库,二是在生产机器上拉取镜像并运行。这俩步操作看起来没什么工作量,毕竟Docker在里面做了大量的工作;但如果一个App需要部署多台服务,并且还要做升级更新以及后续的运维操作;如果一个人要负责多个系统成百上千个服务的部署运维,那工作量对他来说就是个灾难。所以我们需要一个软件系统,帮我们更简单的部署和管理应用,这就是K8S存在的价值。

  开发者需要做的是:1、将镜像构建并推送到镜像库(在我们常使用的Devops平台,比如Aone,甚至已经将打包镜像通过平台实现),2、定义好每个应用需要数据的节点数量,其他的运维工作都由K8S来代理;这里还是需要运维同学的,但由于K8S出色的运维能力支持,使运维工作也很轻松,物理资源利用率会更高。

  How  

K8S学习之当我们部署应用的时候都发生了什么?

  先简单抛一下K8S的架构简图,可以先有个大概的了解。架构可以分为俩部分,控制面板(也就是master节点)用于控制和管理整个集群;工作节点是运行容器化应用的机器,通过一些组件来完成运行、监控、管理。

  工作节点

  容器:docker、rtk等

  kubelet:与API服务器通信,管理所在节点容器

  kube-proxy:组件之间的负载均衡

  控制面板(master)

  KubernetesAPI:与其他组件通信

  Scheculer:调度应用

  ControllerManager:执行集群级别的功能,复制组件、跟踪工作节点、处理节点失败

  etcd:持久化存储集群配置

  看到这些概念肯定会更头疼,后面的笔记会尽量用平时我们工作接触到的东西来帮大家逐步理解。

  Aone 、EAS、ASI、K8S、Docker的关系?

  一张图简单表示一下,我们工作中经常出现的一些平台的关系  

K8S学习之当我们部署应用的时候都发生了什么?

  k8s和docker的关系我们已然了解;ASI是阿里云基于K8S之上封装的一个容器管理平台,支持几乎所有阿里上云的中间件,集成了阿里的安全组件,使集团内部使用更方便和安全;Aone和EAS都是依赖ASI+各种Devops功能,以此应对各自垂直场景上的运维易用性需求。

   应用都跑在哪里?Pod

  在K8S架构简图里有这样一个细节,有一些应用是独立容器部署的,有一些则是多个容器组合放在一起部署,这是符合实际需求的。比如俩个应用需要通过ipc(进程之间通信)、或者需要共享本地文件系统,但在linux容器之间是相互隔离的。所以在K8S中,并不是以容器为最基础的部署单位来运行的,而是抽象出一个概念叫Pod。

  概念:Pod是一个并置容器,作为K8S最基础的构建模块。一个pod中可以有多个容器,但一个容器是不会跨工作节点的。我们平时部署的应用就是在pod中运行的,一个容器中原则上只有一个应用进程(除非有守护进程),这样做的好处也是为了可以减少应用之间的依赖方便扩缩容等运维动作。  

K8S学习之当我们部署应用的时候都发生了什么?

  实现原理

  K8S会将同一个pod中的容器,放在同一个linux的namespace下。

  对于容器和pod之间如何合理的组合,个人理解一个最基础的判断方法,就是这俩个容器是否是同生命周期的;其实我们遇到的大多数的情况都是一个pod中只有一个容器。

  标签(label)

  标签是一种简单却功能强大的Kubernetes特性,不仅可以组织pod,也可以组织所有其他的Kubernetes资源。

  之所以在这里提出来标签的概念,是因为后续介绍的K8S的运行机制里,大量的使用标签筛选器(Selector),是个非常重要的属性。后面聊到的各种资源里圈定控制的范围就是通过标签选择器来搞定的。如下图所示,通过标签的筛选分类,可以区分出各种环境,以此来做灵活的批量运维管理。

   如何保证启动的数量和版本?ReplicationController

  因为这篇笔记主要想记录K8S中部署应用时是如何调度的,所以我们先略去用Pod内部涉及到的一些其他组件,比如volume、网络、configMap等。我们直接进入到K8S的副本机制,也就是K8S是通过什么机制来保证Pod符合配置要求(镜像版本,部署个数等)。

  ReplicationController

  是一种kubernetes资源,确保运行的pod符合配置要求(声明式),简称RC。之所以把”声明式“标亮,是因为这跟K8S实现机理有关,所有的资源定义的API都是声明式api,这个在后续会详细聊。  

K8S学习之当我们部署应用的时候都发生了什么?

  用途

  1、pod或者节点发生故障时,可以自动恢复;2、实现水平伸缩

  组成

  RC的声明主要分为三个部分,pod selector、replicas、pod template。通过这些我们就可以告诉K8S一个应用在部署在容器中时,用哪个版本的镜像,运行多少个。RC也会通过pod selector进行管理和运维动作。

  Pod selector:用于确认作用域内有哪些pod

  Replicas:指定应运行的副本数量

  Podtemplate:用于创建新的pod副本模板

  How it works

  场景1:当一个Pod出现故障,我们将其手动触发删除时;RC会拉取template中配置版本的镜像,创建一个相同标签的Pod。在实际工作中,如果遇到某一个节点无法work,我们在aone里操作销毁的时候,最底层的调度就是这样的。

  场景2:当一个标签为kubia的pod,被重新打上新标签的时候,RC会发现数量不符合要求,拉取template中配置版本的镜像,创建标签为kubia的Pod。在实际工作中,当我们在重新部署的时候,第一个要重新部署的pod可能会被打上”待销毁“的标签,等新的pod启动之后,才正式的销毁。

  场景3:当RC感知到 Replicas声明减少。实际工作中,在缩容的时候,K8S会按照一定的优先级规则,对已有容器进行销毁。优先级规则的底层逻辑总结起来就是:状态越稳定,销毁优先级越低。

  优先级如下:

  1 如果pod没分配到节点,先被删除 。

  2 如果pod的状态是 Pending>PodUnknown>PodRunning,则Pending优先被删除,PodUnknown次之,PodRunning最后被删除。

  3 不是Ready状态的Pod先被删除。

  4 如果Pod都是Ready状态,则最后一个变成Ready状态的Pod先被删除(Ready时间最短的)。

  5 重启次数大的Pod先被删除。

  6 创建时间最新的Pod先被删除。

  场景4:当RC感知到Replicas声明增加。实际工作中的扩容场景,过程与场景1类似,不赘述。

  场景5:当镜像版本升级的时候。

  注意,修改模板的动作,并不会触发容器的重新部署,但当场景1、2出现的时候,RC会控制用新的镜像版本创建Pod。

  ReplicationSet和StatefulSet

  ReplicationSet与ReplicationController类似,但拥有较RC更强大的标签筛选器,是RC的上位替代品,简称RS。

  StatefulSet:从rc和rs的副本机制可以看出来,当新创建一个pod的时候,和原有pod是完全没有关系的,也就是说他们是用来管理无状态Pod的。而StatefulSet是用于管理有状态Pod的,应对新的pod需要与原pod具有相同的网络标识,可以访问同一份持久化数据等需求。由于涉及到的其他资源很多,我会放到第二篇笔记中来记录,顺便引出volume和网络等资源。

   如何触发应用升级?

  副本机制和其他控制器-手动升级

  上一节我们了解了K8S内部是如何用rc、rs等资源来保证pod的是符合配置要求的。也提到了镜像版本升级是不会触发更新的,所以在实际运维的时候,用户不可能手动的来触发。我们先来看下手动操作带来的问题:

  手动升级的弊端  

K8S学习之当我们部署应用的时候都发生了什么?

  手动操作通常有俩种模式。

  第一种是recreate:升级完rc之后把所有的pod全部删除等rc自动创建;或者创建俩个rc,等新rc下的pod全部可用,再把服务重定向到新的pod上。这样会导致服务的短暂(也不一定短暂)不可用,或者造成资源的浪费。

  第二种是rolling-update,也是创建俩个rc,销毁一个旧的,然后创建一个新的,直到全部切换。这样的问题是手工操作很复杂,通过kubctl客户端访问api的方式进行,很有可能中间中断鲁棒性很低。而且很大一个问题是由于是先销毁,所以无法回滚。

   手工操作太麻烦了怎么办?Deployment

  Deployment声明式升级应用-代理了手工操作

  为了解决上述手动操作的弊端,K8S使用了计算机接经典解决方案-套一层,一层解决不了就俩层。K8S提供了一个更高阶的资源Deployment,用于部署应用并以声明的方式升级应用,代理了上一节提到的手工操作。

  Deployment的优点在于,只需要做声明式的升级,仅改动一个字段,k8s就会接管后续所有的升级动作,稳定可靠。并且Deployment会默认保存俩版replicationSet(只保留就的rs,不保留旧pod),所以可以做到回滚。

  控制滚动升级速率

  k8s发布的时候,可能经常遇到发布“太快不稳定”或“太慢体验差”的情况。Deployment是通过maxSurge 和 maxUnava工lable俩个参数来控制滚动升级的速率:

  maxUnavailable:和期望ready的副本数比,不可用副本数最大比例(或最大值),这个值越小,越能保证服务稳定,更新越平滑;

  maxSurge:和期望ready的副本数比,超过期望副本数最大比例(或最大值),这个值调的越大,副本更新速度越快。

   机理:声明式+监听

  到这里其实我们已经算是了解了手动触发升级的时候,k8s里会涉及到的大部分的资源组件,以及他们各自的工作原理,接下来我们把他们整个流程串起来,还原一下现场。

  简单回顾一下k8s的架构简图,架构分为控制平面(master)与工作节点。API服务器用于所有内部外部的组件通信;etcd是个分布式持久化引擎,用于存储pod、rs等等的配置;控制器执行集群级别的功能,复制组件、跟踪工作节点、处理节点失败;调度器负责判断pod应该调度到哪个工作节点上;kubelet用于工作节点与api服务器通信和控制本工作节点上几乎所有事情。

  为了保证高可用状态,集群里有多个主节点,默认配置下会有三个主节点,其他俩个的控制器和调度器就是非活跃状态,但是API服务器是都会接受通信请求,etcd之间也会同步配置信息。

  现场还原

  当主节点通过API服务器收到部署的命令时

  1、API服务器会通过接口传过来的声明配置,创建Deployment资源。

  2、Deployment控制器会监听到Deployment资源创建的消息,调用API服务器根据声明来创建ReplicasSet资源。

  3、ReplicaSet控制器会监听到ReplicaSet资源创建的消息,调用API服务器根据声明来创建Pod资源。

  4、调度器会监听到Pod资源创建的消息,然后根据规则给Pod分配一个工作节点,调用API服务器写入到Pod的配置中(如何决定给pod分配哪个工作节点,后面会说)。

  5、工作节点的Kubelet会监听到Pod分配工作节点的消息,根据Pod的声明来来通知Docker拉取镜像,运行容器。  

K8S学习之当我们部署应用的时候都发生了什么?

  在整个实现流程中,我highlight了俩个词,一个是”监听“、一个是”声明“,这是K8S架构实现中最重要的俩个机理。

  首先“监听”,大家应该都理解,通过相关组件之间的消息监听,事件链的方式完成整个部署工作,可以解耦各个资源之间的依赖。

  另一个是“声明“,主节点中提供的所有API都是声明式的,只定义期望的状态,系统来负责向指定的状态来进行工作;而命令式api需要调用者直接下达执行命令,并监控状态,再进行下一步命令的下达。

  声明式 vs. 命令式

  1、可以有效的避免单点故障,系统会想统一的状态工作,不会发生某个命令没有收到而带来状态的不统一。

  2、Master节点更简单,简单在大多数情况下就意味着稳定和效率。

  3、K8s的组件可以更具备组合性和扩展性,可以升级到达某个状态的资源和组件,不用考虑历史命令式API的兼容问题。

  声明式API也分为俩种,eedge triggering(边缘触发) 和 level triggering(水平触发)

  水平触发(level-triggered,也被称为条件触发)LT: 只要满足条件,就触发一个事件(只要有数据没有被获取,就不断通知你)

  边缘触发(edge-triggered)ET: 每当状态变化时,触发一个事件

  这俩种方式各有各的优势与应用场景,K8S使用的水平触发(LT)方式,因为虽然在正常情况下ET比LT更节省资源,但如果在状态变化的时候,使用ET的方式并且正好丢掉了消息(在实际复杂网络环境下很有可能发生),就会使状态与实际预想的不一致。而K8S架构中最重要的就是状态的声明,如果消息丢失,则会引发各种不受控的问题出现。

  做了一次分享,会觉得这部分比较难理解,举个例子:

  团队有100个bug,Leader在群里发消息:100个bug明天下班之前改完(谁改Leader不管),这就是声明式。

  100个bug,A处理60个,B处理40个;第二天Leader看了下aone,还有超过50个没改,@C一起来处理,这就是命令式。

  声明式的好处是:1、Leader只要结果不关注细节,可以做其他事情。2、如果A居家隔离,没有收到消息,为了达到目标,团队里会有人自动补位一起改bug。

  不管哪种方式的消息,如果Leader一直在确认是否所有人都收到,不是就不断的发,这是水平触发;如果Leader发进群里就不再管了,这是边缘触发。

   如何自动扩缩容?HPA&VPA

  我们可以通过调高 ReplicationController、 ReplicaSet、 Deployment等可伸缩资源的 replicas 字段,来手动实现 pod中应用的横向扩容 。我们也可以通过增加pod容器的资源请求和限制来纵向扩容pod (尽管目前该操作只能在pod创建时,而非运行时进行)。虽然如果你能预先知道负载何时会飙升,或者如果负载的变化是较长时间内逐渐发生的,手动扩容也是可以接受的,但指望靠 人工干预来处理突发而不可预测的流量增长,仍然不够理想 。

  K8S提供了HorizontalpodAutoscaler(HPA),就是英文字面意思。横向伸缩的意思是,通过增加或者减少同样配置的pod的数量,来动态满足业务需求量。  

K8S学习之当我们部署应用的时候都发生了什么?

  HPA是用来声明横向自动扩缩容的高级资源,它需要监控到相关pod的资源使用情况。K8S在每个工作节点的kubelet中都有一个cAdvisor,用于收集本工作节点的资源使用情况,然后在集群中会启动一个Heapster来从所有节点采集度量数据。这样HPA就可以通过Heapster拿到它想要的数据。Heapster经常与influxdb、grafana一起使用,被称为监控三件套,从图中可以看出Heapster也是以pod的方式运行在某个工作节点上。

  How it works?

  1、首先HPA会从Heapster中获取到所管理的pod的资源度量。

  2、计算当前状态下,所需要的pod数量。

  如下图,假设Hpa中设定的cpu使用率为50%,hpa会将各个pod的cpu使用率加和,再除以50%,向上取整。

  3、如果发现所需pod数与现有副本数有差别,则会更新deployment中的replicas字段,就会触发自动的扩缩容。

  当然扩缩容太过于频繁会导致服务的不稳定,所以hpa中支持最快伸缩的间隔,默认扩容最快3分钟,缩容最快5分钟。  

K8S学习之当我们部署应用的时候都发生了什么?

  上面介绍的是个简单的场景,用户只通过cpu使用率来控制伸缩,如果用户希望通过多硬件指标监控的话, K8S会取多个度量的最大值来进行扩容。HPA也同时支持多种类型的度量方式,Resource度量就是上面提到的,根据硬件指标来做度量;Pod度量类型可以用来监控QPS或者消息队列中的数量等;Object度量类型可以用来监控外部资源,比如依赖外部服务的个数来动态调整pod的个数。

  VPA 全称 Vertical Pod Autoscaler,即垂直 Pod 自动扩缩容,它根据容器资源使用率自动设置 CPU 和 内存 的requests,从而允许在节点上进行适当的调度,以便为每个 Pod 提供适当的资源。垂直自动扩容是在k8s1.9版本之后才提供的。

  VPA vs. HPA

  优点:可以节省资源,比如一个这批pod的内容使用量特别高,但cpu使用效率很低,HPA会直接扩N个相同配置的pod,来满足内存使用需求;VPA会启动一个内存量更大,cpu量更小的pod来运行应用。HPA会明显浪费更多的资源。

  缺点:VPA的扩容需要重启pod,这样可能会造成服务的不稳定。虽然在社区中已经有一些优化方案(),但重建Pod环境无论怎样都会有一些风险。

  所以HPA和VPA各有各的使用场景,我们团队是做toB的业务,我大概看了一下,基本上都是VPA的伸缩方式。但如果是2C的应用,对稳定性的要求非常高,建议使用HPA,在应用发布之前应该做好每个pod的各项资源的合适配比。

  上面一直在聊的是Pod的伸缩,但如果集群中工作节点的资源不能满足需求了怎么办?K8S提供了Cluster Autoscaler(CA),它的伸缩方式与HPA大体一致,如果有感兴趣的可以自行了解一下细节。

   结尾

  本篇文章最后,把前言中的”当我们在aone里手动点击了升级之后,k8s里都发生了什么?“再贴在这里看一下↓↓↓

  当我们在aone里手动点击了重新部署,aone会通过k8s的api接口通知master节点创建一个Deployment,Deployment会按照配置里的声明要求创建一个新的RepliactionSet,RepliactionSet会按照配置的创建一个或者若干个Pod,pod会调度到相应的工作节点上,通过docker拉取镜像,启动应用。

  如果大家看完文章能看懂这段文字,说明已经了解了整个过程,要是还能想起来一些运行机理或者其中资源的实现原理就更好了。

  整篇文章到这里已经快9K字了,其实为了更简单易懂的把整个知识点串起来,已经隐藏掉了很多实现细节,文章中的内容也大概输出了学习笔记中不到50%的章节。希望后续还有时间把后面没有分享出来的内容码下来,毕竟整个过程对自己是个很大的巩固和提升。  

K8S学习之当我们部署应用的时候都发生了什么?