Kubernetes简介

Kubernetes,简称K8s,是用8代替名字中间的8个字符“ubernete”而成的缩写。是Google开源的一个容器编排引擎,它支持自动化部署、大规模可伸缩、应用容器化管理,用于管理云平台中多个主机上的容器化的应用。Kubernetes的目标是让部署容器化的应用简单并且高效,Kubernetes提供了应用部署,规划,更新,维护的一种机制。

在生产环境中部署一个应用程序时,通常要部署该应用的多个实例以便对应用请求进行负载均衡。在Kubernetes中,我们可以创建多个容器,每个容器里面运行一个应用实例,然后通过内置的负载均衡策略,实现对这一组应用实例的管理、发现、访问,而这些细节都不需要运维人员去进行复杂的手工配置和处理。

Kubernetes集群

Kubernetes 协调一个高可用计算机集群,每个计算机作为独立单元互相连接工作。Kubernetes 以更高效的方式跨集群自动分发和调度应用容器。一个Kubernetes集群包含两种类型的资源:Master(调度整个集群)和Nodes(负责运行应用)。

Master调度整个集群

Master协调集群中的所有活动,例如调度应用、维护应用的所需状态、应用扩容以及推出新的更新。

Nodes负责运行应用

Node是一个虚拟机或者物理机,它在Kubernetes集群中充当工作机器的角色。每个Node都有Kubelet,它管理Node,而且是Node与Master通信的代理。 Node还具有用于处理容器操作的工具,如Docker或rkt。

请注意生产级别的Kubernetes集群至少包含三个Node,因为如果一个Node出现故障,那么其对应的etcd成员和控制平面节点都会丢失,并且冗余会受到影响。当然了,开发者可以通过添加更多控制平面节点方式来降低这种风险 。

在Kubernetes上部署应用时,需要告诉Master来启动应用容器,之后Master就会编排容器在集群的Node上运行。 Node使用Master暴露的Kubernetes API与Master通信。当然了,终端用户也可以使用Kubernetes API与集群进行交互。

Kubernetes既可以部署在物理机上,也可以部署在虚拟机上。开发者可以使用Minikube开始部署Kubernetes集群。 Minikube是一种轻量级的Kubernetes实现,可在本地计算机上创建VM并部署仅包含一个节点的简单集群。Minikube可用于Linux,macOS和Windows系统。Minikube CLI提供了用于引导集群工作的多种操作,包括启动、停止、查看状态和删除。

Kubernetes应用部署

一旦运行了Kubernetes集群,就可以在其上部署容器化应用程序。 为此,你需要创建Kubernetes Deployment配置。Deployment指挥Kubernetes如何创建和更新应用程序的实例。创建Deployment后,Kubernetes master将应用程序实例调度到集群中的各个节点上。

创建应用程序实例后,Kubernetes Deployment控制器会持续监视这些实例。 如果托管实例的节点关闭或被删除,则Deployment控制器会将该实例替换为集群中另一个节点上的实例。 这提供了一种自我修复机制来解决机器故障维护问题。

在没有Kubernetes这种编排系统之前,启动应用程序通常会采取安装脚本这一方式,但它们不允许从机器故障中恢复。Kubernetes Deployments提供了一种与众不同的应用程序管理方法,通过创建应用程序实例并使它们在节点之间运行。

你可以使用Kubernetes命令行界面 Kubectl 创建和管理Deployment。Kubectl使用Kubernetes API 与集群进行交互。创建Deployment 时,你需要指定应用程序的容器镜像以及要运行的副本数。你可以稍后通过更新Deployment来更改该信息。

Kubernetes中的概念

Pod

Pod是Kubernetes处理的最基本单元,容器本身不会直接分配到主机上,而是会封装到名为Pod的对象中:

在上图中Pod1里面有一个应用容器,且该Pod有一个独立的IP地址。Pod2里面有一个应用容器,还挂载了一个磁盘,该Pod也有一个独立的IP地址。

Pod3里面有两个应用容器,还挂载了一个磁盘,该Pod也有一个独立的IP地址。也就是说这个Pod中的两个应用容器是可以共享IP与磁盘。

Pod4里面有多个应用容器和磁盘,该Pod也有一个独立的IP地址。也就是说同一个Pod中可以有任意多个应用容器(多个容器间网络共享)和磁盘(多个共享目录)。

Node

Pod是运行在Node上的,kubelet负责调度Pod的运行。只要资源足够,一个Node上可以有任意多个Pod。

Service

粗蓝线框的节点是Master节点,其他的都是Node节点。看一下左下角的Node节点,里面运行了一个Pod,然后这个Pod的外面有一层黄色的虚线,这层虚线就是Service。这个Pod有一个IP,地址是10.10.10.1,这个Service也有一个IP,地址是10.10.9.1,可以看到Service和Pod的IP是不一样的。现在有个问题,为什么Pod有了IP,而外面还包一层Service,同时它也有独立的IP呢?

原因在于Pod是运行在具体的一个Node上,可能在某些场景下这个Pod出了问题(不稳定挂掉了或者服务容器异常退出,甚至整个Node挂掉了),这样的话这个Node就挂掉了,服务就无法访问了。那可能会去其他的位置重新启动一个Node,运行Pod,此时Pod的IP地址肯定变化了,因此就需要一个Service。当我们的Pod出了问题,它的IP地址发生了变化,此时我们还可以通过Service的IP来找到这个Pod。注意Service的IP与其生命周期是对应的,只要这个Service没有被删除,那么Service的IP就会一直存在,并且永远不会发生变化。

再来看上面两个Node,左侧Node中只有一个Pod,而右侧Node中有两个Pod,一共三个Pod。在多个Pod的情况下,一般都是同一个应用的多个副本,也就是对同一个应用进行扩容,由1个实例变成3个实例,然后它们对外会提供相同的服务。此时这个Service的IP除了可以向上面那样,定位到Pod的地址之外,还有另外一个作用,即对多个Pod的地址进行负载均衡。举个例子,如轮询访问每个Pod。当然了这个Pod不一定是一模一样的,也可以是同一应用的不同版本。比如我在进行灰度发布的时候,一个可能是旧版本,一个可能是新版本,这都是可以的。所以说Kubernetes并没有对Pod本身做任何限制,那怕是完全不同的应用,你也可以将其作为一个Service去管理。只是一般情况下,我们都不会这么做。

Service的概念我们已经知道了,那么Kubernetes是如何确定哪些Pod属于同一个Service呢?Kubernetes使用Label Selector来进行确定。当我们定义一个Service的时候,可以给它指定一个标签,如s:app=B,有这样一对标签的时候,就可以认为它属于ServiceB。之后它会去搜索所有的Pod,发现上面三个Pod都携带有app=B标签,那么这三个Pod就会自动归属于这个ServiceB。这就让Service和Pod之间的耦合变得很松,非常灵活。

接下来梳理几个概念,首先是Pod,Pod里面可以运行多个容器,Service里面可以包含多个Pod,然后Development就比较宽泛,可以是部署Pod,也可以是部署Service。

接下来看一下通过Development来完成应用的扩容过程:首先从Master节点发起了一个Development,向给这个Service里面的Pod扩容成4个实例,而目前就只有一个实例。

扩容的目标对象是Pod,Pod才是一个真实存在的东西,才有扩容和缩容的概念,而Service是没有的,Service只是一个逻辑上存在的东西。它只是通过Label Selector将一组Pod划分为一个逻辑组,从而实现它们的负载均衡而已。

然后就扩容完成了,在其他的两个节点上,也有了运行的Pod,一共是4个实例。然后这个Service就自动将新的Pod包含进来,那么问题来了,它是如何知道这些新的Pod呢?肯定是通过标签,既然是扩容,那么新Pod的标签和原来的Pod的标签是一样的,然后它通过标签选择,将新的Pod都选择上,接着Service就可以对外进行负载均衡,将流量转发给每个Pod。当某个Pod出问题的时候,Service会通过某种机制,不会将流量转发给出现问题的Pod上。

接下来看一下滚动更新的过程:首先停掉了一个旧的Pod,然后又新启动了一个Pod,此时这个Service中就同时有旧的版本和新的版本:

接着又停掉了最下面那个Pod,又启动了一个新的Pod:

如此进行,直到所有旧的Pod都更新完成,滚动更新就结束了。

在所有的更新和扩容的过程,这个Service的IP都是始终保持不变的,以上就是Kubernetes中的核心概念。

Kubernetes整体架构

接下来我们看一下Kubernetes的整体架构,下面这张图初看可能觉得非常乱:

首先上面这一块是Master节点,然后下面两块分别都是Node节点。Master节点中部署的都是Kubernetes的核心模块,中间的虚线框表示APIServer,它提供了资源操作的唯一入口,并且提供认证、授权和Kubernetes的访问控制。开发者可以通过kuberctl或者自己开发的客户端通过HTTP请求以RESTful API的形式来访问这个APIServer,进而实现对整个集群的控制。

ControllerManager负责维护集群的状态,如故障检测、扩缩容、滚动更新等。Scheduler负责资源的调度,它会按照预定的调度策略,将Pod调度给相应的Node节点。前面学习的Mesos和Docker Swarm也都有调度模块,也有非常多的调度策略可以选择,而Kubernetes则有更加丰富的调度策略,关于这一块后面会详细进行介绍。

右上角还有一个ETCD,主要用作于一致性存储,保存了Pod,Service,还有集群的一些状态等信息,其实就相当于所有的这些Kubernetes集群需要持久化的数据都会存储到这个ETCD中。

在这个Master节点中还运行了一个kube-dns组件,它负责整个集群的DNS服务,注意这个组件不是必须的。不过一般我们都选择安装它,因为通过名字访问是一个非常重要的功能。右侧还有一个名为dashboard的组件,它提供了集群数据管理的可视化界面。

再来看一下Node节点。从上图中可以看到,每个Node节点中都有一个Kubelet,Kubelet负责维护当前节点上的容器的生命周期,也负责维护当前节点的网络等。在每个Node节点上都运行一个kube-proxy,kube-proxy负责Service,负责提供内部的服务发现和负载均衡,相当于为Service这个概念提供了一个落地的方法。

接下来看一下一个Pod Developed全过程:首先用户执行kubectl,向APIServer发起一个命令,然后请求经过认证之后,再由Schedluer的各种策略来评分计算,得到一个目标的Node,然后告诉APIServer。

APIServer就会请求相关Node的kubelet,然后通过这个kubelet将这个Pod运行起来,同时APIServer会将这个Pod的信息存储到ETCD中。当Pod运行起来之后,这个ControllerManager就会负责管理这个Pod的状态。举个例子,假设这个Pod不小心挂了,那么ControllerManager就会重新创建一个一样的Pod,Pod的扩缩容也是由这个ControllerManager来管理的。此时这个Pod有一个独立的IP地址,我们可以在整个集群内使用这个IP来访问它。但是这个Pod的IP是容易发生变化,比如异常重启、服务升级时IP就变化了。再有一个就是当存在多个实例的时候,我们也不可能实时的去关注这些Pod的IP,并访问它们,于是就有了Service。

图中绿色虚线内有三个Pod,它们同属一一个Service,不再此绿色虚线内的Pod则是单独存在的,并没有提供Service的入口。而具体完成Service工作的则是kube-proxy模块,可以看到在每个Node上都有一个kube-proxy。在任何一个节点上访问这个Service的虚拟IP(即给Service分配的IP)都可以访问到这三个Pod,因此这两个节点对这一个Service都会有一个IP的指向,负载均衡的访问它们。Service的IP虽然在集群内部是可以访问得到,但是在这个集群之外呢?我们是想要在集群外的服务,访问集群内的一个服务,此时应该怎么办?这一点kube-proxy也考虑到了,它可以将服务端口直接暴露在当前的Node上,然后外面的请求直接访问Node上的一个IP就可以关联到这个Service了。

我们在学习Docker Swarm的时候,知道在容器中可以通过名字来访问其他的容器,Kubernetes也是可以的,这就是kube-dns所做的工作,它提供了整个集群的dns服务,让每个Pod都可以通过名字来访问对方。也就是说,任何一个Pod都可以通过名字来访问另一个Pod。

Kubernetes Scheduler调度策略

Scheduler用于决定每个Pod应该调度到哪个节点上,Kubernetes的分配方式有两种:预选规则(preselect)和优选规则(optimize-select)。

【预选规则】预选规则(preselect)这里介绍五个比较重要的规则:NodiskConflict、CheckNodeMemoryPressure、NodeSelector、FitResource和Affinity。
(1)NodiskConflict,即看看有没有挂载的冲突。如果一个Pod需要一个挂载,然后在这台机器上,这个挂载已经被其他Pod占用的话,这就是挂载冲突,它不会让挂载冲突这件事情发生。
(2)CheckNodeMemoryPressure,即检查当前节点的内存压力,所有内存压力为0的节点可以调度,当然了还存在对磁盘的检查等等。
(3)NodeSelector,即选择指定hostname或者具有某些标签的节点,这是非常基础的,所有的服务编排框架都有这样的功能。
(4)FitResource,这也是非常基础的,Node需要满足Pod的CPU、内存、GPU等资源的要求。
(5)Affinity,亲和性,这个逻辑比较复杂,它可以满足很多需求,如一个Pod必须与另一个Pod运行在一起,一个Pod最好与另一个Pod运行在一起,这是两种描述,它都可以实现。既然有亲和性,那么就有反亲和性,即一个Pod不能与另一个Pod运行在一起,一个Pod最好不要与另一个Pod运行在一起。

【优选规则】优选规则(optimize-select)是指通过预选规则选出来一系列的Node之后,对这些Node进行打分。Kubernetes会使用一组优先级函数去处理每一个待选的Node,每一个优先级函数会返回一个0-10的分数,分数越高表示这台机器越好,越适合,同时每个函数也会对应一个权重,最终选择一个分值最高的机器来部署Pod。

这里介绍三个比较重要的规则:SelectorSpreadPriority、LeastRequestedPriority和AffinityPriority。

(1)SelectorSpreadPriority。对于同一个Service或者Controller的Pod,尽量会分布在不同的机器上,如果指定了区域,则会尽量将Pod分散在不同区域的不同主机上。
(2)LeastRequestedPriority。如果一个新的Pod需要分配节点,那么这个节点的优先级就由节点空闲的那部分容量的比值来决定。其实就是使用节点上Pod的总容量减去新的Pod的容量得到的值再除以总容量。也就是说当这个Pod运行在这个节点上之后,当前节点还存在多少空闲,如果空闲越高则分数越大。
(3)AffinityPriority,调度综合亲和性机制。Node Selector在调度的时候将Pod限定在某些节点上的时候,它是支持多种操作符的,如in、not in、exists、gt/lt等,它不会去限定对Label的精确匹配,然后会根据这些匹配条件和匹配结果进行一个算法级别的打分。

Kubernetes Pod通讯

接下来学习Kubernetes Pod与Pod之间的通讯方式有三种,下面分别进行介绍。

第一种,Pod的内部通讯,同一个Pod中的容器之间的相互通信。这就是近的两个Pod之间的通讯,同一个Pod肯定是运行在同一个主机上,然后它有共享的网络,同一个IP,所以它们之间的访问可以使用localhost加上端口号就能进行访问,这是最简单的一种通信方式:

第二种,同一个Node上,不同Pod间的通讯。同一个Node上的Pod,它的默认路由都是docker0,由于它们都关联在同一个网桥docker0上,地址网段是相通的,所以它们之间可以直接通过该网桥来进行通讯。然后访问的方式就是可通过Pod的IP来直接访问:

第三种,不同Node上,不同Pod间的通讯。它们要想实现访问,需要满足几个条件,一个是Pod的IP不能冲突(不能有相同的IP),其次就是Pod的IP要和Node所在IP关联起来,通过这个关联让Pod之间可以相互访问:

Kubernetes服务发现

Kubernetes服务发现主要有两个组件,一个是kube-proxy,另一个则是kube-dns。

kube-proxy其实分为两种类型,一种是ClusterIP,这种对于每个服务都按照一个IPtable规则,会给相关的Pod做一个虚拟IP,会将虚拟IP的流量直接重定向到后端服务的一个集合。这个虚拟IP只能在集群内访问,并且是固定的,前提是Service不删除。另一种则是NodePort,NodePort就是在每一个Node上都起一个监听端口,相当于将服务暴露在节点上,这样就可以让集群外部的服务,通过NodeIP和NodePort去访问到我们集群内部的服务。

kube-dns是Kubernetes的一个插件,负责集群内部的DNS解析,目的是在集群内部可以让Pod之间通过名字来进行访问。