Kubernetes实战:构建生产级应用平台
上QQ阅读APP看书,第一时间看更新

1.1 Kubernetes的定位

Kubernetes是一个平台、基础设施,还是一种应用?不乏“精神领袖”可以为你提供他们对Kubernetes的精确定义。与其在这堆定义中添油加醋,不如把精力放在Kubernetes解决的问题上。一旦撇开这个烦琐的定位问题,接下来我们便要探索如何基于这个“工具集合”来构建我们的生产环境。“生产型Kubernetes”的理想状态是我们已经将其用于完全承载生产环境流量。

Kubernetes这个名字可能有点像一个概括性术语。在GitHub上快速浏览一下,发现kubernetes组织(在撰写本书时)包含69个仓库。然后是kubernetes-sigs,它拥有大约107个项目。在本书中,我们不会讨论这一领域中的数百个云原生计算基金会(CNCF)项目,而是将专门介绍Kubernetes的核心项目。

那么,什么是核心?核心项目包含在kubernetes/kubernetes(https://github.com/kuberne-tes/kubernetes)仓库中。这是Kubernetes集群中一些核心组件的位置。当在集群中使用这些组件时可以提供如下功能:

•分布式的集群,实现跨多个主机进行调度。

•提供一个声明性、可扩展的、用于与系统交互的API接口。

•提供一个CLI(kubectl),用于开发人员与系统交互。

•从对象的当前状态到期望状态的调度。

•提供基本的服务抽象来帮助在工作负载之间路由请求。

•暴露多个接口,用于支持可插拔的网络和存储插件。

这些功能集合创造了一个生产级容器编排服务。更简单地说,Kubernetes为我们提供了一种在多个主机上运行和调度容器的工作方法。在我们进行深入研究时,请记住这一能力。随着时间的推移,我们希望证明这种能力虽然是基础性的,但却是我们构建生产环境的一部分。

1.1.1 核心组件

那么,是哪些组件提供了我们之前所说的功能呢?我们在前面提到,核心组件在kuber-netes/kubernetes仓库中。我们中的许多人以不同的方式使用这些组件。例如,那些运行托管服务(如Google Kubernetes Engine(GKE))的人可能会从该托管主机上获得每个组件。而不使用托管服务的人可能会从仓库中下载二进制文件或从供应商那里获得签名版本。无论如何,任何人都可以从kubernetes/kubernetes仓库中选择一个版本并下载Kubernetes。在下载并解压后,可以使用cluster/get-kube-binaries.sh命令来获取二进制文件。这将自动检测你的目标架构并下载服务器和客户端组件。让我们看一看下面的示例代码,然后开始探索关键的组件。

在下载的服务器组件中(它可能保存在server/kubernetes-server-${ARCH}.tar.gz),你会发现构成Kubernetes集群的关键项目,如下所示。

API服务器(API Server)

所有Kubernetes组件和用户的主要交互点。这是我们获取、添加、删除和改变对象的地方。API服务器会将状态委托给一个后端存储服务,最常见的是etcd。

kubelet

主机上的代理服务。它与API服务器进行通信以报告节点状态,并分析应该在该节点安排哪些工作负载。它与主机内部的容器运行时(container runtime)(如Docker)通信,以确保为节点安排的工作负载被正确启动并且处于健康状态。

控制管理器(Controller Manager)

一组打包在一个二进制文件中的控制器,用于调度处理Kubernetes中许多核心对象。当所需的状态被声明时(例如,需要部署某个服务的三个副本),内部的控制器将会创建新的Pod来满足这个状态。

调度器(Scheduler)

根据它认为的最佳节点(内存占用或服务大小)来决定工作负载的运行位置。它使用过滤和评分来做出这个决定。

Kube代理(Kube Proxy)

为Kubernetes服务提供虚拟IP,可以使用该IP路由到后端Pod。这是通过主机上的数据包过滤机制完成的,如iptables或ipvs。

虽然上述不是一个非常详细的列表,但这些是构成我们所要讨论的核心功能的主要组件。图1-1展示了这些组件在整体架构中是如何共同发挥作用的。

Kubernetes有很多不同的架构。例如,许多集群运行kube-apiserver、kube-scheduler和kube-controller-manager作为容器。这意味着控制面板(control-plane)也可能运行着容器运行时、kubelet和kube-proxy。这些有关部署方面的问题将在下一章介绍。

1.1.2 服务编排之外的扩展功能

在有些领域,Kubernetes不仅仅用于调度工作负载。例如,组件kube-proxy对主机进行内部代理,为工作负载提供了一个虚拟IP(VIP)。这种通过建立内部IP地址,并将流量路由到一个或多个底层Pod的行为已经超出了运行和调度容器化工作负载的范围。从理论上讲,将其作为Kubernetes核心的部分来实现,不如将它定义为一系列API接口,并利用插件模式来实现该接口。这种方法可以使用户在生态系统中的各种插件中进行选择,而不是将其作为Kubernetes的核心功能。

这也是许多Kubernetes API(如Ingress和NetworkPolicy)所采取的模式。例如,在Kubernetes集群中创建一个Ingress对象并不会发生什么。换句话说,虽然API存在,但它并没有被实现。我们的平台开发团队必须考虑想引入什么技术来实现这个API。对于Ingress,一些平台开发团队使用一个控制器(如ingress-nginx(https://kubernetes.github.io/ingress-nginx))在集群中运行,通过读取Ingress对象并为指向Pod的NGINX实例创建NGINX配置来实现该API。ingress-nginx只是众多选项中的一个。Project Contour(https://projectcontour.io)实现了相同的Ingress API,但却是envoy的实例程序,envoy是Contour的基础代理。这种可插拔的模式使我们的平台开发团队拥有了多种选择。

图1-1:组成Kubernetes集群的主要组件。虚线代表不属于核心Kubernetes的组件

1.1.3 Kubernetes接口

基于这种可扩展的插件化思想,我们现在应该探索更多的接口来使得我们能够在核心功能上进行定制。我们认为Kubernetes接口只是一个定义,或者说是一个关于如何与某些东西进行交互的合同。这与在软件开发中定义接口抽象的想法类似——类或结构需要实现这个接口所要求的功能。在像Kubernetes这样的系统中,我们部署了许多满足这些接口的插件,用于提供诸如网络等功能。

一个例子是容器运行时接口(CRI)(https://github.com/kubernetes/cri-api)。在早期,Kubernetes只支持一个容器运行时Docker。虽然Docker今天仍然存在于许多集群中,但人们对使用替代方案的兴趣越来越大,如containerd(https://containerd.io)或CRI-O(https://github.com/cri-o/cri-o)。图1-2展示了这两个容器运行时。

图1-2:两个工作负载节点运行两个不同的容器运行时。kubelet发送CRI中定义的命令(如CreateContainer),期望运行时满足请求并做出响应

在许多接口中,命令(如CreateContainerRequest或PortForwardRequest)都是通过远程过程调用(RPC)的形式发布的。在使用CRI的情况下,通信是通过GRPC进行的,kubelet期望得到如CreateContainerResponse或PortForwardResponse这样的响应。在图1-2中,你还会注意到实现CRI的两种不同模式,而CRI-O是CRI的初始实现。因此,kubelet可以直接向它发出这些命令。而containerd需要一个插件支持,作为kubelet和它自己接口之间的一个中间件。总而言之,无论其具体的系统架构如何,关键步骤是让容器运行时执行,而kubelet不需要关心每个容器运行时内部具体是怎么运行的。这使得不论如何架构、构建和部署Kubernetes集群,它都拥有强大的功能。

随着时间的推移,我们甚至看到一些功能从核心项目kubernetes/kubernetes中移除,用来支持这种插件模式。例如,用于整合云服务商接口的项目(CPI)(https://github.com/kubernetes/cloud-provider)。在传统方式中,大多数CPI被直接打包进核心项目,例如kube-controller-manager和kubelet。但是这种方式通常需要配置负载均衡器或需要暴露云服务所需的元数据等。特别是在创建容器存储接口(CSI)(https://kubernetes-csi.github.io/docs/introduction.html)之前,这些云服务需要配置块存储,并将其提供给在Kubernetes中运行的工作负载。仅以这种方式对接一个云服务就需要执行很多的功能和步骤,更不用说它需要为每一个可能的云服务重新实现一遍了。将这些逻辑抽离,封装到一个自己的接口模型中,则是一个更好的解决方案。例如,kubernetes/cloud-provider(https://github.com/kubernetes/cloud-provider)可以由多个项目或云服务商进行实现,除了最大限度地减少Kubernetes代码库的冗余之外,还使得CPI功能可以在核心Kubernetes集群的范围外进行升级或修复。

现在Kubernetes为我们提供了几个接口用以实现定制和扩展的功能。下面是一个高层抽象的接口列表,我们将在本书的各章中讲解如何对其进行扩展:

•容器网络接口(CNI)使网络服务提供商能够定义从IPAM到真实数据包的路由。

•容器存储接口使存储服务提供商能够满足集群内的工作负载存储请求。常用于ceph、vSAN和EBS等技术的实施。

•容器运行时接口支持各种运行时,包括Docker、containerd和CRI-O。它还使得一些不那么常见的运行时得到推广,如firecracker(它利用KVM来提供一个最小的虚拟机)。

•服务网格接口(SMI)是Kubernetes生态系统中较新的接口之一。它希望在定义流量策略、服务发现和服务管理时保证一致性。

•云服务商接口(Could Provider Interface,CPI)使VMware、AWS、Azure等云服务商能够为其云服务与Kubernetes集群进行整合。

•开放容器倡议(OCI)对镜像格式进行了标准化,确保由一个工具构建的容器镜像在符合要求的情况下可以在任何符合OCI的容器运行时运行。这与Kubernetes没有直接关系,但对推动插件化容器运行时发展起到了辅助作用。

1.1.4 小结

现在我们已经了解了Kubernetes的作用范围。它是一个用于容器编排的服务,并在各方面都提供了一些额外的功能。它也可以利用插件化的接口进行扩展和功能定制。但是,Kubernetes对于许多寻找如何优雅地运行应用程序的组织来说是基础性的。让我们换个思路来思考这个问题。如果我们用Kubernetes取代目前组织中的应用平台系统,它能够满足组织的要求吗?答案是不能,因为构成当前应用平台的组件和机器所涉及的内容还有很多。

根据我们的Kubernetes实践经验,当某组织认为Kubernetes将成为其实现软件构建和运行方式现代化的强制功能时,它将会遇到许多问题。当然,Kubernetes是一项伟大的技术,但它真的不应该成为你在现代基础设施、平台或软件发展方向的焦点。你会惊讶地发现,有许多行政人员或高级架构师认为Kubernetes是解决问题的银弹,但是实际上他们的问题是应用交付、软件开发、组织或人员方面的问题。Kubernetes最好被理解为整个系统架构中能为应用程序提供平台的一块拼图。我们将会一直围绕这个问题进行探讨。