- Helm学习指南:Kubernetes上的应用程序管理
- (美)马特·布彻 马特·法里纳 乔什·多利茨基
- 5710字
- 2025-04-08 08:52:27
1.1 云原生生态系统
云技术的出现显然改变了业界对硬件、系统管理、物理网络等的看法。虚拟机取代了物理服务器,存储服务取代了硬盘驱动器,自动化工具的地位日益突出。这也许是业界对云的概念化方式的早期改变。但是,随着这种新方法的优点和缺点日益清晰,设计应用程序和服务的实践也开始发生变化。
开发人员和运营人员开始质疑在坚固的硬件上构建大型单体二进制应用程序的做法。他们认识到了在保持数据完整性的同时跨不同应用程序共享数据的困难性。分布式锁定、存储和缓存成为主流问题,而不再只是学术界关注的焦点。大型软件包被分解成更小的离散可执行文件。正如Kubernetes创始人Brendan Burns经常说的那样,“分布式计算从一个高级主题发展为计算机科学入门课程”。
云原生抓住了这种认知上的转变,我们可以称之为云的架构视角。当我们围绕云的功能和约束设计系统时,设计的就是云原生系统。
1.1.1 容器和微服务
云原生计算的核心是这样一种哲学观点,即较小的离散独立服务(smaller discrete standalone service)比处理一切事务的大型单体服务(large monolithic service)更可取。云原生方法不是编写一个大型应用程序来处理从生成用户界面到处理任务队列再到与数据库和缓存交互的所有事务,而是编写一系列较小的服务,每个服务都有相对特定的用途,然后将这些服务连接到一起以用于更高级别的用途。在这种模型中,其中一个服务可能是关系数据库的独占用户。希望访问数据的服务将(通常)通过表征状态转移(REST)API与该服务联系。而且,通过HTTP使用JavaScript对象表示法(JSON),这些其他的服务将查询和更新数据。
这种细分允许开发人员隐藏底层实现,提供针对更广泛应用程序的特定业务逻辑的一组特性。
微服务
如果一个应用程序由完成所有工作的单个可执行文件组成,则云原生应用程序就是分布式应用程序。虽然独立的程序各自负责一个或两个独立的任务,但这些程序一起构成了一个逻辑上单一的应用程序。
结合这些理论,下面我们举一个简单的例子来具体解释。想象一个电子商务网站。这类网站一般由几个任务共同组成。网站上有一个产品目录、用户账户和购物车、一个支付处理器(处理对安全敏感的货币交易过程),以及一个前端(客户可以查看商品并进行选择和购买)。还有一个管理界面,店主在这里管理库存和完成订单。
历史上,像这样的应用程序曾经作为一个单独的程序构建。负责每个工作单元的代码都被编译进一个大的可执行文件,通常在一台庞大的硬件设备上运行此文件。
然而,编写这种应用程序的云原生方法是将这个电子商务应用程序分成多个部分:一个处理支付交易,一个跟踪产品目录,还有一个提供管理功能,等等。这些服务使用定义良好的REST API通过网络相互通信。
极端地说,应用程序被分解成最小的基础构件,每个部分都是一个程序。这就是微服务架构。与单体应用程序相反,微服务仅负责处理整个应用程序的处理流程的一小部分。
微服务概念对云计算的发展产生了巨大的影响。这一点在容器计算(container computing)的出现中最为明显。
容器
容器常常被拿来和虚拟机进行比较。虚拟机在主机上的隔离环境中运行整个操作系统。相反,容器有自己的文件系统,但与主机在同一操作系统内核中执行。
还有第二种描绘容器的方法可能对目前的讨论更有利。顾名思义,容器为打包单个程序的运行时环境提供了一种有用的方法,从而确保可执行文件在从一个主机移动到另一个主机时满足其所有依赖项。
这是一种更具哲理的方法,也许是因为它对容器施加了一些非技术性的限制。例如,可以将十几个不同的程序打包在一个容器中,并同时执行它们。但容器,至少按Docker的要求设计的容器,是一个顶层程序的载体。
当我们在这里谈论程序时,实际上是在思考一个比“二进制文件”更高层次的抽象。大多数Docker容器至少有几个可执行文件,它们只是用来辅助主程序的。但这些可执行文件都是容器主要功能的辅助文件。例如,Web服务器可能需要一些其他本地实用程序来启动或执行低级任务(例如,Apache有用于模块的工具),但主程序是Web服务器本身。
容器和微服务在设计上是完美的搭配。小的离散程序可以连同它们的所有依赖项一起打包到小巧的容器中。这些容器可以在主机之间移动。在执行容器时,主机不必拥有执行程序所需的所有工具,因为所有工具都打包在容器中了。主机只需具备运行容器的能力即可。
例如,如果一个程序是用Python 3构建的,那么主机不需要安装和配置Python并安装该程序所需的所有库,因为所有这些都被打包在容器里。当主机执行此容器时,容器中已经存储了正确版本的Python 3和每个必需的库。
更进一步,主机可以自由地执行具有互相冲突的需求的容器。容器化的Python 2程序可以与容器化的Python 3需求在同一主机上运行,并且主机的管理员无须做任何特殊的工作来配置这些互相冲突的需求!
这些例子说明了云原生生态系统的一个特性:管理员、运营人员和站点可靠性工程师(SRE)不再从事管理程序依赖项的工作。相反,他们可以把精力集中在更高层次的资源分配上。运营人员不必担心Python、Ruby和Node的哪个版本在不同的服务器上运行,而可以关注是否为这些容器化的工作负载正确分配了网络、存储和CPU资源。
在完全隔离的环境中运行程序有时是有用的。但更多的时候,我们希望将这个容器的某些方面暴露给外部世界。我们想让它能够访问存储,能够回答网络连接,能够根据我们目前的需求向容器中注入一些配置。所有这些任务(甚至更多)都是由容器运行时提供的。当容器声明它有一个服务在端口8080上进行内部监听时,容器运行时可以授予它在主机端口8000上的访问权限。因此,当主机在端口8000上收到网络请求时,容器将此视为其端口8080上的请求。同样,主机可以将文件系统装载到容器中,或者在容器中设置特定的环境变量。通过这种方式,容器可以参与到它周围更广泛的环境中——不仅包括该主机上的其他容器,还包括本地网络甚至Internet上的远程服务。
容器镜像和登记站
容器技术本身就是一个复杂而迷人的空间。但就我们的目的而言,在进入云原生堆栈的下一层之前,我们只需要了解更多关于容器如何工作的内容就够了。
如前所述,容器是一个程序及其依赖项和环境。整个过程可以打包成一个可移植的表示形式,称为容器镜像(通常简称为镜像)。镜像不是被打包成一个大的二进制文件,而是被打包成离散的层,每个层都有自己的唯一标识符。当镜像四处移动时,它们作为层的集合移动,这提供了巨大的优势。如果一个主机具有五层镜像,而另一个主机需要相同的镜像,那么它只需要获取它还没有的层。例如,如果它已经有五层中的两个,则只需要获取另外三层就可以重建整个容器。
有一项关键的技术提供了移动容器镜像的能力,即镜像登记站(image registry),这是一种专门的块存储技术,它容纳容器,使其可供主机使用。主机可以将容器镜像推送(push)到登记站,该动作会将各层传输到登记站。然后另一台主机可以将镜像从登记站拉取(pull)到该主机的环境中,如此一来该主机就可以执行此容器了。
登记站负责管理各个层。当一个主机请求某个镜像时,登记站会让该主机知道哪些层构成了该镜像。然后,主机可以确定自己缺少哪些层(如果有缺少的话),然后从登记站下载这些层。
登记站最多使用三条信息来标识特定的镜像:
名称
镜像名称可以简单也可以复杂,具体取决于存储镜像的登记站:nginx、servers/nginx或example.com/servers/nginx都是镜像名称。
标签
标签通常是指安装的软件版本(v1.2.3),尽管标签实际上只是任意字符串。标签latest和stable通常分别用于表示“最新版本”和“最新生产就绪版本”。
摘要
有时必须拉取一个非常具体的镜像版本。由于标签是可变的,因此不能保证在任何给定的时间,标签都确切地引用软件的此特定版本。因此,登记站支持通过摘要(镜像层信息的SHA-256或SHA-512摘要)获取镜像。
在本书中,我们将看到使用前面三条信息引用的镜像。组合这些的标准格式是name:tag@digest,其中只有name是必需的。因此,example.com/servers/nginx:latest表示“给我名为example.com/servers/nginx,标签为latest的映像”,而

表示“给我与此处的摘要完全一致的example.com/my/app”。
虽然关于镜像和容器还有很多知识需要学习,但是我们现在已经有足够的知识来继续下一个重要的主题了。下面我们将探索调度器和Kubernetes。
1.1.2 调度器和Kubernetes
在上一节中,我们看到了容器如何封装各个程序及其所需的环境。容器可以在工作站上本地执行,也可以在服务器上远程执行。
随着开发人员开始将应用程序打包到容器中,运营人员也开始使用容器作为部署的工件,于是出现了一组新的问题。如何最好地执行大量容器呢?如何最好地促进一个需要大量容器协同工作的微服务架构呢?如何明智地共享对网络连接存储、负载均衡器和网关等的访问呢?如何设法将配置信息注入许多容器中呢?也许最重要的是,如何管理内存、CPU、网络带宽和存储空间等资源呢?
甚至更进一步,人们(基于他们对虚拟机的经验)开始询问如何管理跨多个主机分发容器,以及在合理使用资源的同时公平地分配负载呢。即,如何在运行尽可能多的容器的同时运行尽可能少的主机呢?
2015年,时机已经成熟:Docker容器正在向企业进军。显然,此时需要一种工具来管理跨主机的容器调度和资源管理。多种技术相继登场:Mesos引入了Marathon;Docker创建了Swarm;Hashicorp发布了Nomad;Google创建了其内部Borg平台的开源兄弟,并将这项技术命名为Kubernetes(希腊语中的船长一词)。
所有这些项目都提供了一个集群容器管理系统的实现,该系统可以调度容器并将它们连接起来,以托管复杂的类似微服务的分布式应用程序。
每一个调度器都有其优点和缺点。但是Kubernetes引入了两个概念,使它与众不同:声明性基础设施(declarative infrastructure)和协调循环(reconciliation loop)。
声明性基础设施
考虑部署容器的情况。我们可以这样处理部署容器的过程:创建容器;打开一个端口让它监听,然后在文件系统的这个特定位置附加一些存储;等待所有的东西被初始化;测试它,看看容器是否准备好了;将其标记为可用。
在这种方法中,我们通过关注设置容器的流程来按过程进行思考。但Kubernetes的设计是我们以声明的方式思考。我们告诉调度器(Kubernetes)我们想要的状态是什么,Kubernetes负责将声明性语句转换成它自己的内部过程。
在Kubernetes上安装一个容器更像是在说,“我希望这个容器在这个端口上运行,使用一定量的CPU并在文件系统的这个位置上安装一些存储”。Kubernetes在后台工作,根据我们对所需内容的声明来连接所有内容。
协调循环
Kubernetes是如何在幕后完成这一切的?当我们按过程看问题时,会有一定的运作顺序。Kubernetes是怎么知道顺序的呢?这就是协调循环思想的用武之地。
在协调循环中,调度程序说:“这是用户所需的状态。以下是当前状态。它们不一样,所以我将采取步骤来协调它们。”用户希望为容器提供存储空间。当前没有附加存储。因此Kubernetes创建了一个存储单元并将其附加到容器中。容器需要公共网络地址。现在也不存在。所以一个新的地址被附加到容器上。Kubernetes中的不同子系统开展工作以实现用户对所需状态的整体声明的各个部分。
最终,Kubernetes要么成功地创建了用户想要的环境,要么得出了无法实现用户需求的结论。同时,用户只能被动地观察Kubernetes集群并等待它成功完成或将安装标记为失败。
从容器到pod、服务、部署等
前面的例子虽然简洁,但有点误导。Kubernetes不一定把容器当作工作单元。相反,Kubernetes引入了一个更高层次的抽象,称为pod。pod是描述离散工作单元的抽象信封。pod描述的不仅是一个容器,它还描述一个或多个容器(以及它们的配置和需求),这些容器结合在一起执行一个工作单元:

❶ 前两行定义了Kubernetes类型(v1 Pod)。
❷ 一个pod可以有一个或多个容器。
一般一个pod只有一个容器。但有时有一些用于为主容器做预配置并在主容器联机之前退出的容器,这些被称为初始化容器(init container)。另外有一些与主容器同时运行并提供辅助服务的容器,这些被称为边车容器(sidecar container)。这些都被认为是同一个pod的一部分。
在前面的代码中,我们编写了Kubernetes Pod资源的定义。当用YAML或JSON表示时,这些定义称为清单(manifest)。清单可以包含一个或多个Kubernetes资源(资源也称为对象或资源定义)。每个资源都与一种Kubernetes类型相关联,例如Pod或Deployment。在本书中,我们通常使用“资源”一词,因为“对象”这个词还有其他含义:YAML将对象定义为一个命名的键/值结构。
Pod描述容器所需的配置(如网络端口或文件系统装载点)。Kubernetes中的配置信息可以存储在ConfigMap中,对于敏感信息,可以存储在Secret中。Pod的定义可以将这些ConfigMap和Secret与每个容器中的环境变量或文件联系起来。当Kubernetes看到这些关系时,它将尝试附加和配置Pod定义中描述的配置数据:

❶ 在本例中,我们声明了一个v1 ConfigMap对象。
❷ 在data内部,我们声明了一些任意的名/值对。
Secret在结构上类似于ConfigMap,只是data部分中的值必须是Base64编码的。
Pod使用卷(volume)链接到配置对象(如ConfigMap或Secret)。在本例中,我们采用前面的Pod示例并附加上述Secret:

❶ volumes部分告诉Kubernetes这个pod需要哪些存储源。
❷ configuration-data是我们在前面的示例中创建的ConfigMap的名称。
❸ env部分将环境变量注入容器中。
❹ 环境变量将在容器内命名为BACKGROUND_COLOR。
❺ 这是它将使用的ConfigMap的名称。如果要将此映射用作文件系统卷,该映射必须在volumes中。
❻ 这是ConfigMap的data部分中的键的名称。
pod是可运行工作单元的“原始”描述,容器是pod的一部分。但是Kubernetes引入了更高阶的概念。
考虑一个Web应用程序。我们可能不想只运行此Web应用程序的一个实例。如果我们只运行了一个实例,且它失败了,那么网站就会崩溃。如果我们想升级它,必须想办法在不破坏整个网站的情况下进行升级。因此,Kubernetes引入了部署(Deployment)的概念。Deployment将应用程序描述为相同pod的集合。Deployment由一些顶级配置数据以及构建副本pod的模板组成。
通过Deployment,我们可以告诉Kubernetes使用单个pod创建应用程序。然后我们可以扩大到使用5个pod。再减少到3个pod。我们可以附加一个Horizontal PodAutoscaler(另一种Kubernetes类型)并配置它来根据资源使用情况扩展pod。当升级应用程序时,Deployment可以采用各种策略来增量升级单个pod,而不必关闭整个应用程序:

❶ 这是一个apps/v1 Deployment对象。
❷ 在spec中,我们要求提供以下template的三个副本。
❸ template指定每个副本pod的规格。
当涉及将Kubernetes应用程序附加到网络上的其他东西时,Kubernetes提供了服务(Service)的定义。Service是一种持久的网络资源(有点像静态IP),即使连接到它的一个或多个pod消失,它也会持久存在。这样,Kubernetes Pod可以来去自如,而网络层可以继续将流量路由到同一Service端点。虽然Service是一个抽象的Kubernetes概念,但在幕后它可以实现为从路由规则到外部负载均衡器的任何东西:

❶ 类型是v1 Service。
❷ 此Service将通过app:my-deployment标签路由到pod。
❸ 此Service的80端口的TCP流量将路由到与app:my-deployment标签匹配的pod上的8080端口。
上述Service将把流量路由到我们之前创建的Deployment。
我们已经介绍了几种类型的Kubernetes。还有另外几十种,但到目前为止最常用的是Pod、Deployment、ConfigMap、Secret和Service。在下一章中,我们将更直接地介绍这些概念。但现在,我们可以介绍Helm了。