今天,大型单体应用正被逐渐拆分成小的、可独立运行的组件,我们称之为微服务。微服务彼此之间解耦,所以它们可以被独立开发、部署、升级、伸缩。这使得我们可以对每一个微服务实现快速迭代,并且迭代的速度可以和市场需求变化的速度保持一致。
但是,随着部署组件的增多和数据中心的增长,配置、管理并保持系统的正常运行变得越来越困难。如果我们想要获得足够高的资源利用率并降低硬件成本,把组件部署在什么地方变得越来越难以决策。手动做所有的事情,显然不太可行。我们需要一些自动化的措施,包括自动调度、配置、监管和故障处理。这正是K8S的用武之地。
K8S抽象了数据中心的硬件基础设施,使得对外暴露的只是一个巨大的资源池。它让我们在部署和运行组件时,不用关注底层的服务器。使用K8S部署多组件应用时,它会为每个组件都选择一台合适的服务器,部署之后它能够保证每个组件可以轻易地发现其他组件,并彼此之间实现通信。
通过K8S部署应用程序时,你的集群包含多少节点都是一样的。集群规模不会造成什么差异性,额外的集群节点只是代表一些额外的可用来部署应用的资源。
一个K8S集群由多个节点组成,节点可分为两种类型:
主节点:负责管理和调度工作节点
工作节点:负责运行部署的应用程序,每台服务器都是一个工作节点(主节点除外)
API服务器:主节点和工作节点的通信媒介
Scheduler:负责调度应用(为应用的每个可部署组件分配一个工作节点)
Controller Manager:它执行集群级别的功能,如复制组件、持续跟踪工作节点、处理失败节点等。
etcd:持久化存储集群配置
容器运行时:Docker、Containerd、RTK或其他容器类型。
Kubelet:管理所在的节点的容器
Kube-Proxy:负责组件之间的负载均衡网络流量
一个容器里运行的进程实际上运行在宿主机的操作系统上,就像所有其他进程一样(不像虚拟机,进程是运行在不同的操作系统上的)。但在容器里的进程仍然是和其他进程隔离的。对于容器内进程本身而言,就好像是在机器和操作系统上运行的唯一一个进程。
容器的隔离机制主要依靠 Linux 命名空间 namespace 和 Linux 控制组 cgroups。namespace 提供隔离视图(文件、进程、网络接口、主机名等),cgroups 限制进程可使用的资源(CPU、内存、网络带宽等)。
默认情况下,每个 Linux 系统最初仅有一个命名空间,所有系统资源都属于这一个命名空间,但是你能创建额外的命名空间,以及在它们之间组织资源。这就是容器的隔离机制。
一个 pod 由一个或一组紧密相关的容器组成,它们总是一起运行在同一个工作节点上。
每个 pod 就像是一个独立的逻辑机器,拥有自己的 IP,即使不同的 pod 运行在同一个工作节点,它们也是独立的。
前面我们讲到容器的隔离机制是通过 namespace 和 cgroup 来实现的,一个 pod 内的容器,或者说是容器组通常需要共享某些资源,K8S 通过配置 Docker 来让一个 pod 内的所有容器共享相同的 network 和 UTS 命名空间,这样它们都有相同的主机名和网络接口。但这也意味着在同一 pod 中的容器运行的多个进程不能绑定到相同的端口号,否则会导致端口冲突,这也表示着一个道理有共享就有冲突。
K8S 集群中的所有 pod 都在同一个共享网络地址空间中,每个 pod 都可以通过其他 pod 的 IP 地址来实现相互访问。
pod 的定义
已部署 pod 的 yaml 通常包含三个部分:
metadata:包括名称、命名空间、标签和关于该容器的其他信息
spec:包含 pod 内容的实际说明,例如 pod 的容器、卷和其他数据
status:包含运行中的 pod 的当前信息(创建新的 pod 不需要这一部分)
一个简单的 pod 定义 yaml:
pod 并不是直接创建出来的,它是通过 ReplicatiionController、ReplicaSet 或 Deployment......等方式创建出来的。
ReplicaSet 是 ReplicationController 的升级版,ReplicaSet 在标签选择器上拥有更强大的选择功能。
但是 ReplicaSet 也很少使用,我们通常用更高级的资源对象 Deployment,它会创建 ReplicaSet 来创建和管理 pod。
当然还有一些其他资源对象如:DaemonSet、Job、CronJob.....等等。
由于 pod 是动态的,为了高可用并且通常不是单节点,没有一个固定访问的 IP。前面提到过K8S拥有负载均衡和弹性伸缩的能力,它提供了服务这个资源对象。如果想要从集群外部访问 pod 的话,就需要借助 LoadBalancer 服务来访问,LoadBalancer 服务拥有一个不变的 IP,这样 pod 就可以自由的伸缩了。
服务有多种,LoadBalancer 服务相当于是对集群外部的,Service 是对集群内部的,主要目的就是使集群内部的其他 pod 可以访问当前这组 pod,集群外部是没法访问这个 IP的。
另外,服务并不是和 pod 直接相连的,相反,有一种资源介于两者之间,它就是 Endpoint 资源,Endpoint 资源就是暴露一个服务的 IP 地址和端口的列表。
如果创建不包含 pod 选择器的服务,K8S 将不会创建 Endpoint 资源(毕竟缺少选择器,不知道服务中包含哪些 pod),你可能会想为什么要用 Endpoint 资源,直接用选择器不行嘛。尽管在 spec 服务中定义了 pod 选择器,但在重定向传入连接时不会直接使用它。相反,选择器用于构建 IP 和端口列表,然后存储在 Endpoint 资源中。当客户端连接到服务时,服务代理选择这些 IP 和端口中的一个,并将连接重定向到该位置监听的服务器。另外,Endpoint 对象需要与服务具有相同的名称,并包含该服务的目标 IP 地址和端口列表。如果不是手动创建 Endpoint,Endpoint 对于开发者来说几乎是无感的。
前面说到的都是将服务暴露给集群内部(LoadBalancer除外),如果想将服务暴露给集群外部,可以将服务的类型设置为 NodePort,这样每个集群节点都会在节点上打开一个端口,然后将端口上收到的流量重定向到基础服务。
如图所示:
当然也可以将服务的类型设置为 LoadBalancer,它是 NodePort 类型的一种扩展,这使得服务可以通过一个专用的负载均衡器来访问,这是有 K8S 中正在运行的云基础设施提供的。负载均衡器将流量重定向到跨所有节点的节点端口,客户端通过负载均衡器的 IP 连接到服务。
如图所示:
还可以通过 Ingress 资源,这是一种完全不同的机制,通过一个 IP 地址公开多个服务。
标签可用于组织 pod,也可组织所有其他的 K8S 资源。一个资源可以拥有多个标签,通常我们在创建资源的时候就会将标签附件到资源上,但也可以之后添加标签或修改标签。
标签是一个键值对,上面就是 creation_method 和 env 两个标签。另外,上面说了标签不仅用于组织 pod,还可以组织其他 K8S 资源,比如你的 pod 对硬件有特定要求,比如需要 GPU 等特殊资源,你还可以对节点打标签。让 pod 调度到拥有该标签的节点上。
除标签外,pod 和其它对象还可以包含注解。注解也是键值对,所以它们非常相似。但是注解不是用于保存标识信息而存在的,因为已经有标签了,注解主要用于工具使用。
标签是用于组织资源的,由于每个对象都可以拥有多个标签,因此这些对象组是可以重叠的,另外当在集群中工作时,如果没有明确指定标签选择器,我们总能看到所有对象。
但是,当你想将对象分割成完全独立且不重叠的组时,就需要用到命名空间了。这个命名空间和 Linux 命名空间不一样,这个仅为对象名称提供一个作用域。
K8S 可以通过存活探针检查容器是否还在运行。可以为 pod 中的每个容器指定存活探针。如果探测失败,K8S 将定期执行探针并重新启动容器。
HTTP GET 探针:对给定的URL执行 GET 请求,如果探测器收到相应,并且状态码不代表错误,则探测成功。
TCP 套接字探针:尝试与容器指定端口建立 TCP 连接,如果连接建立成功,则探测成功。
Exec 探针:在容器内执行任意命令,并检查命令的退出状态码。如果状态码是 0,则探测成功。
ReplicationController 是一种 K8S 资源,可确保它的 pod 始终保持运行状态,如果 pod 因任何原因消失,则 ReplicationController 会注意到缺少了的 pod 并创建替代 pod。
ReplicationController 会持续监控正在运行的 pod 列表,并保 pod 的数目与期望相符,如果运行的 pod 太少则会创建新副本,否则会删除多余副本。
一个 ReplicationController 有三个主要部分:
Label Selector(标签选择器):确定 ReplicationController 作用域中有哪些 pod
Replica Count(副本个数):指定应运行的 pod 数量
Pod Template(pod 模板):用于创建新的 pod 副本
ReplicaSet 的行为和 ReplicationController 完全相同,但 pod 选择器的表达能力更强,可完全替代 ReplicationController。
DaemonSet 和 ReplicaSet 的区别是,它会在每个节点上都运行一个 pod,比如日志收集器和资源监控器就有这样的需求。
前面介绍的都是需要持续运行的 pod,如果你想要运行完成工作后就终止任务的情况就需要用到 Job。
Job 资源在创建时会立即运行 pod,但是更多的需求是定时执行,这就要用到 CronJob。
为什么需要 Ingress,一个重要的原因是每个 LoadBalancer 服务都需要自己的负载均衡器,以及独有的公有 IP 地址,而 Ingress 只需要一个公网 IP 就能为许多服务提供访问。当客户端向 Ingress 发生 HTTP 请求时,Ingress 会根据请求的主机名和路径决定请求转发到的服务,如图所示:
不知道你有没有考虑到这么一种情况,如果 pod 的标签与服务的 pod 选择器相匹配,那么 pod 就将作为服务的后端,请求就会被重定向到 pod 上,但是如果 pod 没有准备好,如何处理服务请求呢?
该 pod 可能需要时间来加载配置或数据,或者可能需要执行预热过程以防止第一个用请求时间太长影响用户体验。在这种情况下,不希望该 pod 立即开始接受请求,尤其是在运行的实例可以正确快速地处理请求的情况下。不要将请求转发到正在启动的 pod 中,直到完全准备就绪。
与前面介绍的存活探针类似,K8S 还允许为容器定义准备就绪探针,就绪探针也有三种类型:Exec 探针、HTTP GET 探针、TCP Socket 探针。
上面我们已经知道服务可提供负载均衡的能力,将请求转发到随机的一个 pod 上,但是如果你想要让请求连接到所有的 pod,那该怎么办呢?
幸运的是,K8S 允许客户端通过 DNS 查找发现 pod IP。通常,当执行服务的 DNS 查找时,DNS 服务器会返回单个 IP---服务的集群 IP。但是如果告诉K8S 不需要为服务提供集群 IP(通过在服务 spec 中将 clusterIP 字段设置为 None 来完成此操作),则 DNS 服务器将返回 pod IP 而不是单个服务 IP。DNS 服务器不会返回单个 DNS 记录,而是会为该服务返回多个记录,每个记录指向当时支持该服务的单个 pod 的 IP。
将服务的 spec 中的 clusterIP 字段设置为 None 会使服务成为 headless 服务:
我们之前说过,pod 类似逻辑主机,在逻辑主机中运行的进程共享诸如 CPU、RAM、网络接口等资源,或许你会认为进程也能共享磁盘,那你就错了。pod 中的每个容器都有自己的独立文件系统,因为文件系统来自容器镜像。
K8S 中的卷是 pod 的一个组成部分,因此像容器一样在 pod 的规范中就定义了。它们不是独立的 K8S 对象,也不能单独创建或删除。 pod 中的所有容器都可以使用卷,但必须先将它挂载在每个需要访问它的容器中。在每个容器中,都可以在其文件系统的任意位置挂载卷。
假设有一个带有三个容器的 pod,一个容器运行了一个 web 服务器,该 web 服务器的 HTML 页面目录位于 /var/htdocs,并将站点访问日志存储到 /var/logs 目录中。第二个容器运行了一个代理来创建 HTML 文件,并将它们存放在 /var/html 中,第三个容器处理在 /var/logs 目录中找到的日志。每个容器都有一个明确的用途,但是每个容器单独使用就没多大用处了。但是如果将两个卷添加到 pod 中,并在三个容器的适当路径上挂载它们,就会创建一个完善的系统。
常见的卷有如下几种:
1、emptyDir
2、gitRepo
3、hostPath
hostPath 虽然可用于持久化存储卷,但是使用的很少,通常使用其他云存储卷,例如 gce、aws 存储卷。
几乎所有的应用程序都需要配置信息,并且这些配置数据不应该被嵌入应用本身。通常传递配置选项给容器化应用程序的方法是环境变量、命令行参数、亦或者是通过 gitRepo 存储卷,但其实还有一种更好的方式,那就是 ConfigMap。
尽管绝大多数配置选项并未包含敏感信息,少量配置可能含有证书、私钥,以及其他需要保持安全的相似数据,这类型数据需要被特殊对待,这就需要用到 Secret。
应用配置的关键在于能够在多个环境中区分配置选项,将配置从应用程序源码中分离,可频繁变更配置值,而不用重新部署 pod。如果将 pod 定义描述看作是应用程序源码,显然需要将配置移除 pod 定义。
1、ConfigMap 定义
2、使用 ConfigMap
前面通过环境变量、或者 ConfigMap、Secret 卷向应用传递配置数据都是预先知道的。但是对于那些不能预先知道的数据,比如 pod 的 IP、主机名或者是 pod 自身的名称该怎么获取呢?此外对于那些已经在 pod 中定义的数据,比如 pod 的标签和注解又该如何获取呢?这些问题都可以使用 Downward API 解决。Downward API 允许我们通过环境变量或文件传递 pod 的元数据。Downward API 主要是将在 pod 的定义和状态中取得的数据作为环境变量和文件的值。
Deployment 是一种更高价资源,用于部署应用程序并以声明的方式升级应用,而不是通过 ReplicationController 或 ReplicaSet 进行部署,它们都被认为是更底层的概念。当创建一个 Deployment 时,ReplicatSet 资源也会随之创建。使用 Deployment 可以更容易的更新应用程序,因为可以直接定义单个 Deployment 资源所需达到的状态,并让 K8S 处理中间的状态。总之,部署应用程序使用 Deployment 就行了。
我们之前使用 ReplicaSet 创建的 pod 都是共享一个持久卷:
那能不能通过一个 ReplicaSet 让多个 pod 副本都指定独立的持久卷声明呢,很明显这是做不到的,除非你先手动创建 pod,之后在创建 ReplicaSet 来管理它们,但是如果发生节点故障后,你还需要手动管理它们,所以这是不合适的。
由于每个副本都是独立有状态的,所以你先得创建多个持久卷声明,当创建 Statefulset 的时候,你就需要指定 pod 对应的持久卷。
K8S 的一些资源对象书中介绍的大概就这些了,上面只是一些简单的概念,具体还是需要看书和实操深入理解。