这是一篇关于 k8s 基础入门的文章。如果你想要入门k8s但又苦于找不到合适的教材,那这篇文章可能会适合你;如果你是个编程老手,对 k8s 已经熟的不行,也欢迎进来批评指正,或者给我发个红包鼓励鼓励。

因为是入门,当然是越简单越好。k8s的内容太多了,很多人想要学但又被厚重的书籍劝退,动不懂就几百页,人都傻了,线上的教程也是十几小时起步。在这篇文章里,我不会去同你讨论 k8s 的复杂架构,也不会要求你去背诵k8s各种指令,从始至终我的目的就只有一个:带你去了解 k8s 中最常用的几个概念。但后续 k8s 的其他知识,你可能需要从其他的途径去补充。

另外,在这里也先说明一下在理想情况下,你应该将本文掌握到什么程度:

  1. 能够大概了解文中 kubernetes 对象是做什么的;
  2. 能够大概看懂文中介绍的 kubernetes 对象 yaml 文件;

一、Pod(突破容器的限制)

如果你之前接触过 docker ,你可能知道,一个容器中一般只跑一个进程,即一个容器只会运行一个应用。

那些无法通过网络连接的容器组

拿招采系统举例子,可能是由【ebidding】、【mysql】、【mq】组成的,那么,如果用 docker 的思想,我们会是创建三个容器,每个容器之间的资源隔离的:分别是【ebidding容器】、【mysql容器】、【mq容器】。这三个软件(ebidding、mysql、mq)分开到三个容器中并不会有什么问题,因为他们之间是可以通过如果通过 tcp 等协议去相互访问的。

但如果存在一个容器需要访问另外一个容器里面的文件,这种要如何处理呢?

假设:存在【ebidding】和【探针】,其中,【探针】是用于监控【招采】的运行环境,【探针jar】的一个功能就是要去搜集【ebidding】运行时产生的日志文件。也就是说,【ebidding】和【探针】存在强关联关系。

这其实是两个不同的进程,按照 docker 的习惯,需要把他们放在两个不同的容器中,但因为容器之间无法直接磁盘上的文件。当然,我们也可以给这两个容器挂载同一个数据盘,但是这样操作就显得有些不够灵活了,因为理论上所有容器都有无限的横向拓展的可能性,你得准备一个足够大容量的硬盘,并且还要保证不同容器中的应用不要使用相同的配置文件。

所以,对于强关联关系的两个应用,docker 的隔离特性就无法满足了。

k8s的解决方案

k8s 对👆上面的问题提出了一个解决方案:既然 docker 是基于单个进程资源的隔离,那如果把这种隔理性再往上提一个层次,同时对多个进程做隔离,问题不就解决了吗?

k8s 提出了 Pod 对象的概念,本质上,它其实是一个容器组,但一个 Pod 中不同的容器组共享了IP 地址、共享网络、存储卷等资源。

Pod 是 Kubernetes 管理应用的最小单位,其他的所有概念都是从 Pod 衍生出来的。Kubernetes 让 Pod 去编排处理容器,然后把 Pod 作为应用调度部署的最小单位,Pod 也因此成为了 Kubernetes 世界里的“原子”。

这里需要强调一下的是,我们在这里以及后续概念的介绍,都指得是对象(有不是的我会单独强调),比如这里谈的是 Pod 对象,k8s 是通过 yaml 文件来定义不同对象,通过这些对象来完成对资源的调度。

Pod 的使用场景

由此我们可以得出结论:一组强关联的容器,一般是放在一个 Pod 中。但目前对于我们大部分的场景来说,一个 Pod 就是放一个应用,对应着一个镜像。

如何定义Pod

了解了 Pod 的概念之后,我们还要看一下 kubernetes 中如何定义 Pod 。

ebidding-dev-pod.yaml
image.png300

其中,

  • kind声明了对象类型,这里是 Pod 类型;
  • metadata 声明了元数据,主要是对Pod进行命名、打打标签,便于后续筛选等等。
  • labels:标签,labels是可以自定义的,定义多少层级都可以,便于后续筛选。
  • spec:声明了对象的主要配置。
  • containers:在这个对象中,容器的相关配置。

目前了解这些就足够了,更多的标签在后续会慢慢完善。

除此之外,要执行 k8s 调度任务,k8s 提供了很多指令,我们在这里只需要记住一个指令就行了:

kubectl apply -f ebidding-dev-pod.yaml

以上指令表示运行一个 yaml 文件,也就是根据 yaml 的配置去指定调度任务。
执行上面代码后,k8s 就会启动一个以harbor.gxbtxc.com:8443/ebidding-xxx为基础镜像的招采服务了。

二、Deployment(管理多个pod)

从上一节我们知道,Pod是 k8s 的最小调度单位。

多个Pod的管理需求

思考一下 Pod 的问题:Pod只能管理容器组,但是并不能够管理 Pod 自身
所以,需要一个更高级的对象来管理 Pod,这个对象就是Deployment对象

在云原生应用中有一个特征是“无限的拓展性”,对于Pod来说,有时候一个 Pod 并不能够满足线上业务的需求,为了维持系统的高可用要求,我们通常需要部署多个 Pod。

所以,Deployment对象必须要能够指定 Pod 的数量。

组合模式

在 k8s 的设计理念上,还体现除了“组合模式”的思想:往已有的对象上,添加一些新的元素,然后组成新的对象。

既然已经存在 Pod 了,Deployment只是管理 Pod,当然是直接往 Pod 上添加一些新的元素最简单了。

让我们来看一下 Deployment 对象和 Pod 对象的 yaml 文件对比图。
image.png300

从上图可以发现,除了 kind 从 Pod 变成 Deployment 类型之外。我们还可以发现一些有意思的东西:

  • 右侧 Deployment 有两个spec标签:spec 和 template.spec。
  • 左侧 Pod 中声明的 spec 内容,和右侧 Deployment 中 template.spec 中声明的内容是一样的。

下面来解释一下 Deployment 的 yaml 文件内容:

  • template 字段定义了一个“应用模板”,里面嵌入了一个 Pod,这样 Deployment 就可以从这个模板中创建出 Pod,这个 Pod 因为是被 Deployment 直接控制的。
  • template.metadata.labels 标签,用于给template.spec 中定义的 Pod 打标签,在 k8s 其他的对象中需要根据这个标签来过滤出需要的 Pod。
  • spec.replicas 用于控制 Pod 的副本数。若根据图中的配置启动 yaml ,会启动2个 pod 来运行。
  • spec.selector.matchLabels 表示了当前 Deployment 对象中需要管理的 Pod 对象是哪些。这个字段一般来说是和在模板中(即template.metadata.labels)定义的标签保持一致。因为在 k8s 中,【定义 Pod 模板】,和要【管理 Pod】 ,已经完全解耦了。也就是说,在一个 Deployment 定义的模板 Pod ,可以在另外的 Deployment 文件中被管理。但是在实际使用的时候我们并不建议这样使用。

定义了deployment.yaml还需要额外去定义 Pod 的yaml吗?

答案是不需要。因为在 Deployment 的 yaml 中已经有足够的来生成一个 Pod了,对于 k8s 来说,会通过 Deployment 来生成调度所需要的 Pod。

执行 Deployment

如何执行?当然是使用 kubectl apply -f 指令啦

kubectl apply -f ebidding-dev-pod.yaml

三、Service(Pod的负载均衡)

在软件的生命周期中,Pod 的生命周期比较短暂。主要原因有:

  • 我们会有部署新的软件版本的需求,会重启 pod ;
  • pod 的底层上仍是基于容器,容器的运行情况会受到宿主机资源的影响。k8s 会根据 pod 所在宿主机的资源使用情况,适当启动新的 pod 来替代旧的 pod (当然,k8s 会保证副本的总数还是spec.replicas)。

此外,k8s 全程接管了 pod 的启动,以及 Pod 的 IP 地址分配。所以 Pod 的反腐销毁和新建,会导致 Pod 地址的不断变化。这对微服务而言是个坏消息,因为微服务需要一个稳定的 IP 来进行通信,在多个微服务中,一般是多个服务。

为了解决 Pod 的 IP 变化的问题,k8s 专门定义了一个新的对象:Service。它是集群内部的负载均衡机制,用来解决服务发现的关键问题。

Service 是怎么做的

Service 对象类似于一个云原生技术中的 Nginx。Kubernetes 会给它分配一个静态 IP 地址,然后它再去自动管理、维护后面动态变化的 Pod 集合,当客户端访问 Service,它就根据某种策略,把流量转发给后面的某个 Pod。如下图所示:
image.png300

Service 会维持一个固定的地址(可以是 IP,也可以是域名),然后把流量分配到后端的 pod 中去。

此外,Service 对象使用与 Deployment 相同的“selector”字段,用于选择要代理哪些 pod 的请求,这是一种松耦合关系。

Service 和 Deployment 的关系

为了方便大家看清二者的关系,我画了一张图:

image.png300

可以看到:

  • Service 中要代理的 pod 是通过 spec.selectors 标签来指定的,这个标签和在 Deployment 的 template 中定义的 pod 的标签、以及在 Deployment 中 管理的 Pod 标签是一致的。
  • Service 对象中的 ports ,定义了这个 Service 需要开放哪些端口以及与 Pod 的端口映射关系,一般来说和 Pod 的保持一致,可以根据实际情况指定。

一般来说,我们通过 deployment 来保证 pod 能够正常运行(表现为可以通过 replicas 为设置 pod 足够的副本数),再通过 service 来让这组 pods(通过标签筛选)能够屏蔽内部部署情况,支持负载均衡。

如何让 Service 对外暴露服务

service 默认是集群内部 pod 之间的负载均衡。可以通过修改 type 来调整负载的类型。

除了“ClusterIP”,Service 还支持其他三种类型,分别是“ExternalName”“LoadBalancer”“NodePort”。不过前两种类型一般由云服务商提供,我们的实验环境用不到,所以接下来就重点看“NodePort”这个类型。

yaml 如下所示:

apiVersion: v1
...
spec:
  ...
  type: NodePort

NodePort 类型 Service 除了会对后端的 Pod 做负载均衡之外,还会在集群里的每个节点上创建一个独立的端口,用这个端口对外提供服务,这也正是“NodePort”这个名字的由来。

Service 对象的 IP 地址还有一个特点,它是一个“虚地址”,不存在实体,只能用来转发流量。也不能 ping。

NodePort 类型的 Service 的缺点

  • 第一个缺点是它的端口数量很有限。Kubernetes 为了避免端口冲突,默认只在“30000~32767”这个范围内随机分配,只有 2000 多个,而且都不是标准端口号,这对于具有大量业务应用的系统来说根本不够用。
  • 第二个缺点是它会在每个节点上都开端口,然后路由到真正的后端 Service,这对于有很多计算节点的大集群来说就带来了一些网络通信成本,不是特别经济。
  • 第三个缺点,它要求向外界暴露节点的 IP 地址,这在很多时候是不可行的,为了安全还需要在集群外再搭一个反向代理,增加了方案的复杂度。

所以,如果一组服务,只需要在内部使用,可以使用 NodePort 来暴露服务。如果对外暴露服务,则需要考虑其他的方式。

然后,如何执行?当然是使用 kubectl apply -f 指令啦。

四、Ingress(对外暴露服务)

上一章也介绍了,Service 主要是给 pod 做负载均衡,转发k8s集群内部的流量。要想处理外部的请求,还需要其他的机制。

Service 存在的问题

我们可以通过 Service的 yaml 文件看得出来,Service 只能够配置 端口、Pod 这些内容来做流量的转发,本质上他是四层的负载均衡。而在我们复杂的业务架构中,这肯定是不够的。我们其实更喜欢像 nginx 那种七层上的负载均衡做文章:能够根据 URL、域名等参数做流量的转发。

Ingress的出现

👇下面即将出现 3 个概念,大家要做好心理准备。

Ingress 是一组流量转发的规则。你可以暂时理解为像是 Nginx.conf 那样的一组配置。

光有 Ingress 配置规则还不够,还需要另外一个对象根据 Ingress 规则去执行,去做实际的流量转发。这个去执行 Ingress 规则的对象叫做 Ingress Controller。而这个Ingress Controller实际上是作为一个 Pod 运行在 k8s 集群中。

如此说来,Ingress岂不是要同Ingress Controller强关联了。这可不行,因为就跟 Nginx 和 Apache 一样,k8s 中会存在多种不同的Ingress Controller(其中就有 Nginx Ingress Controller)。所以 Ingress 需要同 Ingress Controller 解耦。这时候就需要通过一个新的对象来关联 Ingress 和 Ingress Controller。这个对象叫做 Ingress Class.

看到这里的时候是不是有些懵:明明学的是 Ingress,一下子蹦出了 3 个概念。确实如此,并且毫无办法。

Ingress、Ingress Controller、Ingress Class三者的关系

赶紧来看一下这三者的关系。

image.png300

可以看到:

  • 同一个 Ingress Class作为桥梁,把 Ingress和 Ingress Controller 关联上了;
  • Ingress 将含有 /backend 的 URL 请求转发到了集群内部的Service;

外部流量到底转发到哪里

到这里的时候,我们发现,集群外部的流量只要能够到达 Ingress Controller,那么就能够被转发到集群内部的 Service 上了。你可能会想,为什么外部流量是到Ingress Controller而不是Ingress?

还记得上面说过的两个细节吗?

  • Ingress 是一组规则,而不是执行规则的对象;
  • Ingress Controller 才是以 Pod 的方式,实际执行Ingress规则的执行者;

所以,外部流量应该被转发到Ingress Controller上。

回到原点

那么问题又来了,既然 Ingress Controller 这上面被定义成了一个 Deployment,那不还是要设计一个 Service 对象去对Ingress Controller的 Pod 做负载吗?这样子的话,不又变成了要如何把 Service 暴露到集群外部吗??这怎么还回到原点了呢???

确实就是这样。但是至少在两个方面上得到了改善:

  1. 原来后端的 Service 对象只能通过 Port 做转发,使用 Ingress 以后可以在七层上做转发了,能适配更更复杂多变的业务需求了;
  2. 现在只需要通过将 Ingress Controller 封装一个 Service,仅需要暴露一个 NodePort 端口,就可以满足所有Service 对集群外暴露服务的需求,小钱干大事,简单灵活。

虽然,但是

Ingress Controller 的部署可能比我们预想的要复杂一些,但也不用太担心一般云平台的 k8s 都内置了 Ingress 相关服务,我们通常只需要编写Ingress规则就可以了。

架构图

image.png300

五、尾声

除了上面所说的 Pod、Deployment、Service、Ingress 之外,k8s还有许多其他的对象适用于其他更多的场景。

如果你感兴趣的话,可以继续去了解一下 ConfigMap、Secret、PersistentVolume。这三个也是使用频率很高的对象。

在这里我就不再赘述了。