Kubernetes核心组件

Tips: 安装 Kubernetes 最小需要 2 核处理器、2 GB 内存,且为 x86 架构(暂不支持 ARM 架构),可前往系统的/proc/cpuinf/proc/meminfo文件进行配置查阅

1. 概述

Kubernetes是2014年由Google公司开源的内部系统,是基于Borg、Omega以及其他谷歌内部系统实践组成的开源系统,用于管理分布式系统的应用程序部署,Kubernetes是希腊单词,对应意思是舵手 / 领航员,由于这个单词起始字母与结束中间包含了8个字母,因此也被称为K8s,这套解决方案集合了Google公司在大规模部署生产应用程序的15年经验而来

在Kubernetes之前,大部分使用的还是Docker Compose或是Docker Swarm技术,甚至还是传统的直接部署形式,但到了微服务架构日益剧增的情境下,需要一套成熟的体系来统一化管理应用程序的状态,这就是K8s所擅长的事,如下是部署模式的趋势变化:

Deployment evolution

  • 传统部署:在早期的传统部署模式下,应用程序部署都是基于物理机器上进行的,而这会引发一个问题在于无法指定分配给每个服务固定的资源,而这种资源不设限的情况下,单个服务可能会由于某些缺陷请求物理机器的大量资源,从而导致这台物理机器上的资源耗尽,其他服务受到波及无法正常运作,对应解决这个问题的方式其实比较简单,即是单台物理机器上只部署一个特有服务,然而这种方式在于物理机器的资源(CPU、内存等)动态伸缩成本较高,资源利用率不高,同时在机器数量多的情况下,
  • 虚拟化部署:为了解决传统部署方式的痛点,引出了虚拟化部署,虚拟化旨在一台物理机器上建立多套虚拟化的操作系统,而这多套虚拟化系统互相不可达,有高度的隔离化,从而使得每一个应用程序可以运行在各自的虚拟环境中,同时享有其独立的存储卷、CPU和内存等资源,但需要注意的是所有的虚拟机都共享着宿主机的资源总量,这种部署方式带来了更高的资源利用率,减少了大量建立物理服务器的成本,相当于建立了一整套的虚拟化资源
  • 容器化部署:容器化部署相比于虚拟机来说有较为宽松的隔离特性(比如容器与容器之间可以通过特定网络进行相互访问),使得容器之间可以共享操作系统,每个容器都有自己独立的一整套系统环境(文件系统、进程空间、CPU和内存等),而重要的是容器化部署提供一些关键的特性:
    • 相对于虚拟机部署来说,容器的创建依赖于镜像,而正是基于此,应用程序部署和创建相比于虚拟机来说要快得多
    • CI / CD能够更好地通过容器化进行支持,也可以快速地进行回滚(这一点是得益于镜像的不变性),将部署的聚焦点转移到了制作镜像上
    • 得益于容器化部署的虚拟化特性,使得它的可移植性非常高,可以游走于多种不同类型的操作系统
    • 可以做到动态资源管理、部署,同时应用程序可以划分为不同的服务(微服务)进行解耦,而不局限于只能部署在一台物理机器上
    • 通过镜像化可以做到各个环境之间的一致性(开发环境、测试环境和生产环境)

容器的管理需要一个好帮手,这个好帮手需要在容器故障的时候,确保故障节点所接收的请求转移到正常容器节点上,同时需要确保重新启动一个健康的容器,保证服务不会由于容器故障而不可用,同时在请求量剧增的情况下,这个好帮手同样能够帮助进行创建更多的容器节点以便处理剧增的业务请求,以上就是Kubernetes所能够做到的事情,简而言之,Kubernetes帮助提供了一个可弹性运行分布式系统的框架,有关更多的Kubernetes功能特性介绍,可前往Kubernetes官网文档进行查看

Kubernetes的设计特点总结起来有:声明式、显式接口、无侵入性和可移植性

  • 声明式

在Kubernetes中,可以通过YAML文件直接声明定义好期望的服务结构以及状态,随后Kubernetes将会按照YAML文件中的定义来运行指定的服务并将其迁移至对应的目标状态,如下所示:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mycontainer
image: nginx:latest

这种结果导向的声明式相比于传统的命令式可以大量地减少运维人员的关注面,无需再关注如何使得服务到达目标状态,只需关注服务的终态即可,剩余的工作由K8s来完成

如果Kubernetes采用命令式编程进行服务搭建,会带来极大的自定义优势,但相反的,运维人员也需要关注服务在过程中应该如何达到期望的状态,需要在过程中耗费不必要的精力

  • 显式接口

第二个Kubernetes的设计特点在于它的接口均是公有的,不存在内部的私有接口,所有的API都能够自由进行串联,提供了极大的自定义能力

  • 无侵入性

无侵入性指的是从传统的Docker容器化部署迁移至K8s时,无需针对镜像做额外的处理,因为本身这一类镜像就可以在Kubernetes中无缝使用,不需要变更应用程序中的代码


Docker是容器技术的标准,而Kubernetes与Docker也息息相关,Docker的实现主要是依赖的是Linux下的namespace、cgroup等相关核心技术(FLAG:后续再针对Docker原理补充一篇笔记)

docker-logo

Docker的意图是通过虚拟化的特性来实现网络层、存储层和进程等运行环境的隔离,并且能够对宿主机的资源进行分配,正常来说,Docker镜像仅仅需要关注自身环境的依赖,而无需关注宿主机的依赖,仅需在镜像构建时进行依赖的安装和编译工作,得益于以上所述的特性,Docker成为了Kubernetes的一层重要依赖

Tips: 这篇笔记只会总结记录下Kubernetes的核心组件含义、作用等等信息,涉及到有关的K8s环境不会出现在这篇笔记中,如果希望搭建本地的Kubernetes集群进行练习,或许可以参考MacOS搭建K3s集群

2. 集群架构

Kubernetes遵循的是传统的C/S架构(即客户端 - 服务器架构),客户端通过RESTful接口与或者直接使用kubectl工具与Kubernetes集群进行通信(kubectl工具本质上也是通过RESTful接口进行封装),Kubernetes集群由多个节点构成,这些节点一般被区分为2种类型:

  • 主节点(Master Node):作为Kubernetes控制和管理整个集群系统的控制枢纽
  • 工作节点(Worker Node):运行着用户实际部署的应用

Kubernetes的master节点会被分配运行Pod吗?

标准的Kubernetes集群配置下,master节点不会被分配运行Pod,主要还是用于控制枢纽

常见的Kubernetes集群架构图如下所示:

Kubernetes 组件

主节点(Master Node)作为Kubernetes集群中的控制枢纽,主要负责接收客户端的请求,安排容器的执行并将集群状态向目标状态进行迁移,主节点包含了多个组件,这些组件可以运行在单个或者多个主节点上来确保高可用性,如下:

  • kube-api-server: 作为和其他节点的通讯桥梁,同时处理来自用户的请求,对外提供RESTful的接口,包括用于查看集群状态的读请求和改变集群状态的写请求,也是唯一一个集群中与etcd进行通信的组件
  • etcd: 可靠的分布式数据存储,用于持久化存储集群配置,存储结构一般是键值对的方式,很注重数据存储的一致性
  • scheduler: 用于调度应用(为每一个可部署组件分配一个工作节点),会根据用户的需求来选择最能满足请求的工作节点来运行Pod
  • Controller Manager: 执行集群级别的功能,比如复制组件、持续跟踪工作节点、处理节点失败等情况,底层运行了一系列的控制器进程,这些进程会按照用户的期望不断地调整集群中的对象,打个比方:当控制器发现了某个服务的状态变为了不健康,那它会开始将这个服务从不健康的状态调整为健康状态(其中这个健康状态是一个目标状态)

工作节点(Worker Node)则是运行容器化应用的机器,运行、监控和管理应用服务的任务是由以下2个组件构成:

  • kubelet: 用于与API服务器通信(会周期性地从API Server获取最新的Pod规格从而进行调整),并管理所在的节点的容器
  • kube-proxy: 负责子网管理,以及组件之间的负载均衡网络流量,同时能将服务暴露给外部,原理就是在多个隔离的网络中把请求转发给正确的Pod或者容器

3. Pod

a. 概述

Pod是Kubernetes中最基本的概念,是Kubernetes对象模型中可以创建的最小的单元,一个Pod中可以运行多个容器,但这并不意味着它总是需要运行多个容器,个人觉得,一个Pod运行一个容器会更加的合适(一定程度上对各个容器之间进行解耦,同时可以提高基础架构的利用率),如果一个Pod中包含了多个容器,它也不会运行在多个工作节点中(一个Pod不会跨多个工作节点存在)

有了容器为什么还需要Pod?

一个大的前提是:容器被设计为每个容器只运行一个进程,除非进程本身产生子进程,如果要在容器中起多个进程,那么将会需要我们来负责维系这几个进程的日志输出、崩溃时的处理方式等等行为,这无疑是为自身增加了额外的工作量(这都是因为一开始提到的大前提)

那么有了容器的原始设计限制,意味着如果应用程序由多个进程组成时,则需要部署对应数量的容器,而正是基于此,就需要一个更高级的数据结构来将这些容器绑定在一起,并将它们作为一个单元进行管理,这就是Pod的作用,也是它存在的意义

Pod中的容器还有一个特性在于,它们并不是完全隔离的,其原理是Kubernetes通过配置Docker让Pod内的所有容器共享相同的Linux命名空间,而不是每一个容器都有自己的一组命名空间,同时它们还共享相同的主机名和网络接口(可以直接理解成共享IP地址和端口号,容器可以通过localhost与同一Pod内的其他容器进行通信),这里需要注意的是,这些容器之间的文件系统是相互完全隔离的(在默认情况下),但可以通过Volume组件来做存储卷空间共享

Kubernetes集群中的所有Pod都在同一个共享网络地址空间中,如下所示,意味着每个Pod都可以通过目标Pod的IP地址进行请求(不存在NAT地址转换)

pod-network

b. 组成

Pod的定义由Kubernetes API版本(apiVersion)、资源类型(kind)和metadata & spec & status等部分组成:

  • metadata:包括了名称、命名空间、标签和关于该容器的其他信息
  • spec:包含了Pod内容的实际说明,比如Pod的容器、存储卷等其他数据
  • satus:包含了运行中的Pod的当前信息(这个信息只有当查询Pod时才会显示,用于知晓当前Pod的最新状态,如果是创建新的Pod,这个status是不需要提供的)

如下定义了一个简单的Pod结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 表示该结构遵循v1版本的Kubernetes API
apiVersion: v1
# 定义了一个Pod类型的资源
kind: Pod
# 通过metadata.name定义Pod的名称
metadata:
name: kubia-manual
spec:
containers:
# 指定该容器所使用的镜像
- image: luksa/kubia
# 容器的名称
name: kubia
# 应用监听的端口
ports:
- containerPort: 8080
protocol: TCP

将以上Pod定义写入到YAML文件后,通过kubectl apply -f <file_name>来创建Pod,声明中定义了这是一个Pod资源,名称为kibia-manual,这个Pod由基于luksa/kubia镜像的单个容器组成,同这个应用对外暴露了8080端口

如何了解Kubernetes定义中的字段含义?

可以通过kubectl expalin pods.spec.ports.containerPort的方式来了解一个组件中的定义(可以理解为是Kubernetes的命令行帮助文档),或是一个组件(如kubectl expalin pod

c. 命令

  • 创建Pod:kubectl create -f <file_name>
  • 获取Pod定义:kubectl get po <pod_name> -o yaml(用过参数-o可以指定输出为YAML或JSON格式)
  • 查看Pod列表:kubectl get pods
  • 查看Pod日志:kubectl logs <pod_name>,如果Pod中包含多个容器,可通过追加参数-c <container_name>的方式来指定查看哪个容器内的日志
  • 本地端口转发至Pod:kubectl port-forward <pod_name> <local_port>:<remote_port>,通过以上命令可以将本地端口转发到Pod的目的端口中,以方便进行调试

Tips: Pod中的日志默认只会存储最近10MB大小的内容,kubectl logs默认显示的是最近10MB的日志信息

d. 健康检查

Kubernetes可以通过存活探针(liveness probe)来检查是容器是否还在正常运行,可以为Pod中的每个容器都单独设置存活探针,如果探测失败,Kubernetes将重启容器,以确保

Kubernetes的存活探针的类型有3种:

  • HTTP GET探针:通过间断发送HTTP GET请求至目标接口,通过返回的响应状态码来进行判断,如果HTTP响应状态码为2XX或3XX,则认为探测成功,而如果出现未响应或响应错误则会被认定为探测失败,对应容器会被重启
  • TCP套接字探针:这种探针方式会尝试与容器指定端口建立TCP连接,如果连接建立成功,则探测成功,否则对应容器将会进行重启
  • Exec探针:主要原理在于进入容器中执行任意命令,并检查命令的退出状态码,如果状态码是0,则认为探测成功,而所有其他的状态码都会被认为是失败

以下是一个HTTP GET探针示例,该Pod定义中描述了一个HTTP GET存活探针,指定了对应的探测端口(spec.containers[0].livenessProbe.httpGet.port)与请求路径(spec.containers[0].livenessProbe.httpGet.path)

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: pod
metadata:
name: kubia-liveness
spec:
containers:
- image: luksa/kubia-unhealthy
name: kubia
livenessProbe:
httpGet:
path: /
port: 8080

经由健康检查失败后,如果需要查看上一个崩溃的Pod的日志,可以通过kubectl logs <pod_name> --previous进行查看

Tips: 请注意对应接口是否无需认证,以防止探测持续失败,容器持续重启

e. 控制器

当下流行管理Pod的组件是Deployment,它是Kubernetes中的一个控制器,主要的功能是用于Pod的部署、滚动更新和回滚,而这个控制组件并不是一开始就存在的,而是由初代控制器慢慢演变而来,演变路径为:ReplicationController -> ReplicaSet -> DaemonSet,几代控制器的目的都是为了集中管理Pod,这种方式在当Pod所在的节点被整个删除时,Pod归属的控制器会将其快速分配至新的Node节点,而如果是手动创建的孤立Pod,则会由于所在节点被销毁而移除,并且不会自动恢复

Tips: 截止至2024年01月,推荐使用的Pod控制器为Deployment

ReplicationController

ReplicationController目的在于创建和管理Pod的多个副本(Replicas),并且时刻关注其Pod的状态,ReplicationController控制器则是通过标签管理器来识别出需要管理的Pod,以下是ReplicationController的工作流

replication-controller-workflow

ReplciationController的主要组成部分为:

  • label selector: 用于确定ReplicationController的作用域中有哪些Pod
  • replica count: 所管辖的Pod对应的副本数量
  • pod template: 创建新的Pod时所使用的模板

以下是一个ReplicationController的样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: ReplicationController
metadata:
name: kubia
spec:
replicas: 3
selector:
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
ports:
- containerPort: 8080

在应用以上配置时,Kubernetes会创建一个名称为kubiaReplicationController控制器,同时确保标签为app=kubia的Pod数量为3

Tips: 如果在定义ReplicationController时不指定Pod标签选择器,则会自动根据Pod标签进行配置(所以定义时可以直接省略标签选择器)

这里需要注意的是,如果后续在ReplicationController中对Pod模板进行调整,则会直接创建新的Pod,而现存的Pod则会陷入无控制器监督状态,简单的来说就是现有的变更都不会影响旧的Pod

如果需要手动调整ReplicationController下的Pod的副本数量调整为10,则可以以下命令进行调整:

1
$ kubectl scale rc kubia --replicas=10

上述操作也可通过kubectl edit re kubia修正其中的spec.replicas字段值

ReplicaSet

ReplicaSet也是Pod管理的组件之一,是ReplicationController的改进版本,主要改进的点在于它的标签选择器,打个比方,ReplicationController能管理带有标签为env=dev的Pod副本,但是无法同时管理带有标签为env=prod的Pod副本,而ReplicaSet可以通过以下方式来匹配多个Pod副本集

1
2
3
4
5
6
7
selector:
matchExpressions:
- key: env
operator: In
values:
- dev
- prod

如上所示,通过表达式的方式融合了多个标签,每个表达式必须包含一个key,一个运算符(operator)和一组待匹配的标签值values列表,其中运算符有以下取值:

  • In: 表示标签值必须与其中一个值匹配
  • NotIn: 表示标签值必须与指定的值都不匹配
  • Exists: 表示标签必须存在,至于值是啥不重要
  • DoesNotExist: 表示Pod的标签必须不存在值才能被匹配

简单的说,即是ReplicationController只支持一个标签匹配,而ReplicaSet支持表达式进行匹配

如果通过kubectl delete rs <replica-set-name>删除ReplicaSet,默认情况下会级联删除关联的Pod

DaemonSet

如果期望在所有Node节点中各运行一个Pod,则需要使用DaemonSet进行Pod的定义与控制,需要注意的是,DaemonSet没有副本数的概念,如果Node节点下线,对应Pod并不会重新创建

如果需要在指定的Node节点中运行Pod,可以通过节点选择器nodeSelector属性进行指定,比方说,需要在打了disk=ssd标签的节点才部署Pod,则可以通过以下进行DaemonSet的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: DaemonSet
metadata:
name: ssd-monitor
spec:
selector:
matchLabels:
app: ssd-monitor
template:
metadata:
labels:
app: ssd-monitor
spec:
nodeSelector:
disk: ssd
containers:
- name: main
image: luksa/ssd-monitor

根据以上定义,在当Pod所在的Node节点标签值发生改变时(标签disk的值不再是ssd时),对应节点中名称为ssd-monitor的Pod被移除,同时删除DaemonSet的操作也会导致Pod被移除

4. Job

Job是一种资源对象,用于管理一次性或者短期任务的执行,当确保任务成功完成后即可终止,它有一些典型的应用场景:比如一次性的垃圾清理,备份等任务

以下是一个例子,这个例子定义了一个名为myJob的Job,创建一个Pod来执行一个简单的任务,任务的内容是输出一段文字,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
parallelism: 1
completions: 1
template:
spec:
containers:
- name: myjob-container
image: busybox
command: ["echo", "Hello from my job"]
restartPolicy: Never

例子中的parallelism用于指定Job的同时运行数量,completions用于指定当Pod运行成功达到目标数量时,则认为该Job执行完成,其中apiVersion指定的batch/v1batch是Kubernetes中的一个API分组,这个组包含了一些作业和任务相关的资源

5. CronJob

如果涉及到Cron任务,则可以通过Kubernetes中的CronJob组件来实现,一个每隔1小时执行curl指令的例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: batch/v1
kind: CronJob
metadata:
name: curl-job
spec:
schedule: "0 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: curl-container
image: appropriate/curl
args: ["curl", "http://example.com"]
restartPolicy: OnFailure

其中jobTemplate 中定义了要运行的任务,这里使用了一个容器镜像 appropriate/curl,并传递了 CURL 命令来访问 http://example.comrestartPolicy: OnFailure 指定了当任务失败时重新启动任务

CronJobJob的主要区别在于CronJob组件支持定时循环重复执行任务,而Job组件则会在组件任务执行完成后进入终止态

可以通过kubectl get cronjobs指令来查看集群中的定时任务列表,通过kubectl describe cronjob my-cronjob来查看具体的定时任务

6. Service

a. 概述

通常来说,Pod需要提供给不同的集群、甚至是外部客户端对应的HTTP服务,同时,也需要一种能够发现其他Pod,将其他Pod作为服务调用,相同的Pod虽然有不同的IP地址,但是仍旧希望能够通过一个固定IP来访问到关联的一组相同Pod

为了解决以上问题,Kubernetes提供了一个服务(Service)组件,说到底就是其主要提供的是为一组功能相同的Pod提供单一不变的接入点(也就是同一个IP),基于此,客户端就不需要记住每个Pod对应的IP地址,仅需记住Service对应的IP地址即可接入背后关联的Pod

b. 例子

服务(Service)通过类似于Pod控制器的标签选择器方式来确定自身管理的Pod,以下是Service组件的一个简单例子,其中包含两部分,分别是由Deployment组成的Pod控制器与Service组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
ports:
# 连接该服务的端口
- port: 80
# 服务将连接转发到容器的端口
targetPort: 80
selector:
# 表示具有app=nginx标签的Pod都属于该服务
app: nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2 # 告知 Deployment 运行 2 个与该模板匹配的 Pod
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

通过kubectl apply -f -创建后,通过以下进行确认Service组件已成功创建:

1
2
3
4
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 31d
nginx-service ClusterIP 10.43.167.133 <none> 80/TCP 34m

并且通过CLUSTER-IP列可以看到nginx-service服务已经被分配了一个内部集群IP地址:10.43.167.133,而因为是内部集群地址,因此只能在集群内部进行访问,由此也可以得知,服务的主要目的是使得集群的其他Pod可以访问当前服务下的这组Pod

验证服务IP的连通性则可以通过kubectl exec命令远程地执行CURL命令在任意一个已经存在的Pod上,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ k exec mysql-58c97fb64c-tpk6r -n nacos -- curl -s 10.43.167.133
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Tips:在kubectl命令中,双横杆--表示命令行的结束,而对于上述例子,在双横杆后,紧跟着的是需要在Pod中执行的命令,其中该curl命令中包含了-s选项,如果没有使用双横杆,这个-s的选项将会被识别为exec命令的选项

如果需要进入Pod中执行Shell命令,可以通过kubectl exec -it <pod_name> bash实现

整个集群内调用的转发过程如下所示:

kubectl-service

在默认情况下,如果多次在集群内调用以上curl命令,会由Service转发至随机进行处理,如果希望来自某个Client IP的请求固定落在某个Pod内进行处理,则可以通过配置服务中的会话亲和性来达到这个效果,具体配置方式为:

1
2
3
4
5
apiVersion: v1
kind: Service
spec:
sessionAffinity: ClientIP
...

Kubernetes仅仅支持该字段(sessionAffinity)为两种取值:ClientIPNone

如果希望通过一个服务连接多个端口,则可以通过以下进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- name: http
port: 80
# 此处假设Pod的端口为8080,则映射为Service的80端口
targetPort: 8080
- name: https
# 此处假设Pod同时开放了8443端口,映射为Service的443端口
port: 443
targetPort: 8443
selector:
app: kubia

通过以上配置,请求Service IP地址 + 端口号,则会转发至Pod的对应的端口号,也就是配置中的targetPort字段,targetPort字段对应的端口号也可以通过名称来代替,使用名称代替的好处在于后续如果Pod暴露的端口变更,无需对应变更Service的端口,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Pod
spec:
containers:
- name: kubia
ports:
- name: http
containerPort: 8080
- name: https
containerPort: 8443
---
apiVersion: v1
kind: Service
spec:
ports:
- name: http
port: 80
targetPort: http
- name: https
port: 443
targetPort: https

c. 服务发现

通过创建Service组件,可以得到一个固定不变的IP地址访问其背后的Pod,但其他的调用方Pod需要提前得知服务的IP地址和端口,获取的方式也就是这一节需要讨论的重点

最为简单的来说,可以直接通过kubectl get svc并查看CLUSTER-IP列来确定IP地址,但Kubernetes为调用方Pod提供了发现服务IP和端口的方式,可以通过环境变量的方式来发现服务,使用环境变量来实现服务发现有个前提在于Service组件的创建:

1
2
3
4
5
$ kubectl exec nginx-deployment-86dcfdf4c6-4sgs5 -- env | grep NGINX
...
NGINX_SERVICE_SERVICE_HOST=10.43.167.133
NGINX_SERVICE_PORT=tcp://10.43.167.133:80
NGINX_SERVICE_SERVICE_PORT=80

如上所示,对应nginx-service的环境变量为NGINX_SERVICE_SERVICE_HOSTNGINX_SERVICE_SERVICE_PORT,分别代表着IP地址和端口号

Tips: 环境变量的命名规则为:[service_name]_SERVICE_HOST,其中前缀服务名称中的横线被转化为了下划线,同时所有字母均为大写

另一种方式则是DNS服务发现,Kubernetes在kube-system命名空间下提供了一个名称为kube-dns的Pod用于运行DNS服务,在集群中的其他Pod都会默认将该Pod其配置为DNS,本质是通过修改每个容器的/etc/resolv.conf文件实现

如果不希望使用Kubernetes集群内的DNS服务,则可以通过Pod中的spec.dnsPolicy字段来实现,该字段的使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ kubectl explain pod.spec.dnsPolicy
KIND: Pod
VERSION: v1

FIELD: dnsPolicy <string>

DESCRIPTION:
Set DNS policy for the pod. Defaults to "ClusterFirst". Valid values are
'ClusterFirstWithHostNet', 'ClusterFirst', 'Default' or 'None'. DNS
parameters given in DNSConfig will be merged with the policy selected with
DNSPolicy. To have DNS options set along with hostNetwork, you have to
specify DNS policy explicitly to 'ClusterFirstWithHostNet'.

Possible enum values:
- `"ClusterFirst"` indicates that the pod should use cluster DNS first
unless hostNetwork is true, if it is available, then fall back on the
default (as determined by kubelet) DNS settings.
- `"ClusterFirstWithHostNet"` indicates that the pod should use cluster DNS
first, if it is available, then fall back on the default (as determined by
kubelet) DNS settings.
- `"Default"` indicates that the pod should use the default (as determined
by kubelet) DNS settings.
- `"None"` indicates that the pod should use empty DNS settings. DNS
parameters such as nameservers and search paths should be defined via
DNSConfig.

现如今流行的服务发现方式一般是通过**全限定域名(FQDN)**来访问,而不是通过环境变量,打个比方,假设需要访问上述创建的nginx-service,则可以通过<component-name>.<namespace>.svc.cluster.local来访问,即它的FQDN为nginx-service.default.svc.cluster.local,此处如果涉及到动态的端口号,则可以通过环境变量进行获取

Tips: 如果连接的服务与当前Pod位于同一命名空间下,则可以省略default.svc.cluster.local后缀,直接通过nginx-service来指代服务

d. Endpoint

服务(Service)并不是直接连接的Pod,而是通过Endpoint资源,起到节点列表的作用,基于前述创建的nginx-service服务,可以通过kubectl describe查看对应的Endpoint资源,其中对应为2个Pod的IP地址 + 暴露的端口号,即体现为2个Endpoint资源,Endpoint资源的目的就是暴露一个服务的端口和IP地址,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ kubectl describe svc nginx-service
Name: nginx-service
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=nginx
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.43.167.133
IPs: 10.43.167.133
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.42.1.10:80,10.42.2.14:80
Session Affinity: None
Events: <none>

如果涉及到需要创建集群外的服务,则也可以通过手动创建Endpoint资源的方式来达到这个效果,同时需要确保Service对应的标签选择器为空(未选择任何Pod),手动创建的Endpoint资源名称需要与Service名称一致才能够对应上,同时在创建的Endpoint资源中需要指定对应的IP地址和端口号,如下所示:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Endpoints
metadata:
name: external-service
subsets:
- addresses:
- ip: 11.11.11.11
- ip: 22.22.22.22
ports:
- port: 80

如此,对应external-service服务对应的请求则会转发至11.11.11.11:8022.22.22.22:80两个节点

e. 服务暴露

目前的Service例子仅支持服务的IP地址在集群内部访问,如果涉及到向外部公开服务,则需要通过以下两个方式来进行(还有Ingress的服务暴露方式将在后续内容中深度展开阐述)

NodePort服务

每个集群会在节点(可以将节点想象成是一台机器)上打开一个端口,并将端口上接收到的流量转发到对应服务,因此叫NodePort

它最终效果与常规服务类似,同样是通过CLUSTER-IP在内部集群进行访问,但是不同的是,在外部服务进行访问的时候,是通过任意一个节点IP + 设置的服务端口号进行访问的

NodePort服务的配置如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 30080
selector:
app: nginx

如上所示,主要将spec.type字段设置为NodePort,默认情况下,这个字段将是ClusterIP,便可以通过任意集群节点IP加设置的30080端口号在外部网络来访问该服务,需要注意的是nodePort字段并不是需要强制设置的,如果NodePort服务未设置该字段,将由Kubernetes随机选择一个端口

通过以下PORT列可以得知集群内部80端口正在被映射为外部端口30080:

1
2
3
$ kubectl get svc nginx-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-service NodePort 10.43.167.133 <none> 80:30080/TCP 2d2h
LoadBalance服务

Tips: 由于负载均衡类型涉及到与云厂商结合,因此本节仅会简单介绍,不会过多深入展开与云厂商结合的内容

使用LoadBalance类型的服务一般是将Kubernetes将各大云厂商进行结合,其中负载均衡器由云厂商提供,需要做的设置是将类型字段设置为LoadBalancer,如下所示:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 80
selector:
app: nginx

如果成功创建,在EXTERNAL-IP列会有一个外部IP地址,通过该外部IP地址可以访问对应的服务

7. Ingress

Tips: Ingress是一个名词,意思是:进入; 进入的权利;

a. 概述

通过Ingress配合着一个公网IP流量入口就能为许多的服务提供访问,当外部客户端向Ingress发送HTTP请求时,Ingress会根据请求的主机名和路径决定请求转发到的服务,如下所示:

ingress-diagram

为什么有了Service还要Ingress?

Service实现外部访问只能依赖NodePort,同时LoadBalance则需要云厂商环境的支持,而此时如果仅依赖NodePort则可能会在服务存在成百上千的时候需要大量地维护端口(NodePort)对应关系,Service也不支持基于URL对HTTP/HTTPS协议进行高级路由、超时重试和灰度发布等功能

使用Ingress则可以很好地解决以上情况,能够实现域名代理指向性转发

b. 例子

一个Ingress的例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
spec:
rules:
- host: k3s-test.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-service
port:
number: 80

上述配置了将来自k3s-test.cn域名同时是80端口的流量转发至nginx-service服务中,同时pathType路径匹配类型为前缀匹配,转发的目标端口为80端口,完成以上Ingress的创建后,通过kubectl get ingress来查看对应Ingress的配置信息

1
2
3
$ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
nginx-ingress traefik k3s-test.cn 192.168.64.10,192.168.64.11,192.168.64.12 80 14m

而有关对应入口域名流量的转发,主要依赖于配置中的host字段,这个字段仅支持域名,默认为空的情况下,将会转发所有的入口流量

Tips: K3s自带traefik实现ingress,与使用nginx实现ingress不同,traefik无需额外部署ingress-controller,自己就可以做到服务发现

其中通过ADDRESS列即可看到对应Ingress的入口IP地址,通过修改系统域名映射文件/ect/hostk3s-test.cn指向192.168.64.10,随即通过以下CURL命令验证Ingress的有效性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ curl k3s-test.cn
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

c. 路径类型

Ingress的路径类型(pathType)字段对应有三种取值:PrefixExactImplementationSpecific

  • ImplementationSpecific: 对于这种路径类型,匹配方法取决于IngressClass
  • Exact: 精确匹配URL路径,并且区分大小写
  • Prefix: 基于以/分隔的URL路径前缀匹配,同时区分大小写,比如设置为/foo/bar,则/foo/bar/baz能够匹配,而/foo/barbaz无法匹配

d. 原理

Ingress底层实现主要是由Ingress API + Ingress Controller两大部分组成,Ingress API主要提供的是配置入口,而实际的路由规则匹配与请求转发这件事情则是由Ingress Controller来实现

Ingress API

Kubernetes上的标准API资源类型之一,仅仅用于定义路由配置信息,只是元数据,需要由对应的Ingress控制器动态加载,将代理配置抽象成一个Ingress对象,每个服务对应一个YAML配置文件,负责以K8s的标准资源格式定义流量调度和路由

Ingress Controller

Ingress Controller是反向代理服务程序,主要负责监视API Server上有关Ingress资源的变更,并生成具体应用的自身的配置文件格式,动态加载新加入Ingress的路由配置,并使其生效,完成流量的转发,Ingress Controller一般来说都需要额外部署,它不是一个Kubernetes内置的组件

Ingress Controller支持由任何具有反向代理的程序实现,如:Nginx、Traefik、Envoy、HAProxy、Vulcand等等,完整的清单可以通过Kubernetes - Ingress控制器进行了解


需要注意的一点是Ingress Controller本身就是一个Pod的形式存在,同时通过一个Service进行暴露,而它实际上并不会将流量直接转发给Service,而是会获取Service对应的Endpoint资源列表,通过Endpoint获取到对应的目标Pod IP地址,而直接将对应的请求转发进目标Pod中,整个过程如下所示:

ingress