Python-Docker-微服务实用指南-全-

Python Docker 微服务实用指南(全)

原文:zh.annas-archive.org/md5/50389059E7B6623191724DBC60F2DDF3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

软件的演进意味着系统变得越来越庞大和复杂,使得一些传统的处理技术变得无效。近年来,微服务架构作为一种有效的处理复杂 Web 服务的技术,获得了广泛的认可,使更多的人能够在同一个系统上工作而不会相互干扰。简而言之,它创建了小型的 Web 服务,每个服务解决一个特定的问题,并通过明确定义的 API 进行协调。

在本书中,我们将详细解释微服务架构以及如何成功运行它,使您能够在技术层面上理解架构以及理解架构对团队和工作负载的影响。

对于技术方面,我们将使用精心设计的工具,包括以下内容:

  • Python,用于实现 RESTful Web 服务

  • Git源代码控制,跟踪实现中的更改,以及GitHub,共享这些更改

  • Docker容器,以标准化每个微服务的操作

  • Kubernetes,用于协调多个服务的执行

  • 云服务,如 Travis CI 或 AWS,利用现有的商业解决方案来解决问题

我们还将涵盖在微服务导向环境中有效工作的实践和技术,其中最突出的是以下内容:

  • 持续集成,以确保服务具有高质量并且可以部署

  • GitOps,用于处理基础设施的变更

  • 可观察性实践,以正确理解实时系统中发生的情况

  • 旨在改善团队合作的实践和技术,无论是在单个团队内还是跨多个团队之间

本书围绕一个传统的单体架构需要转移到微服务架构的示例场景展开。这个示例在《第一章》进行迁移-设计、计划、执行中有描述,并贯穿整本书。

本书适合对象

本书面向与复杂系统打交道并希望能够扩展其系统开发的开发人员或软件架构师。

它还面向通常处理已经发展到难以添加新功能并且难以扩展开发的单体架构的开发人员。本书概述了传统单体系统向微服务架构的迁移,提供了覆盖所有不同阶段的路线图。

本书涵盖的内容

《第一部分》微服务简介介绍了微服务架构和本书中将使用的概念。它还介绍了一个示例场景,贯穿全书。

《第一章》进行迁移-设计、计划、执行,探讨了单体架构和微服务之间的差异,以及如何设计和规划从前者到后者的迁移。

《第二部分》设计和操作单个服务-创建 Docker 容器,讨论了构建和操作微服务,涵盖了从设计和编码到遵循良好实践以确保其始终高质量的完整生命周期。

《第二章》使用 Python 创建 REST 服务,介绍了使用 Python 和高质量模块实现单个 Web RESTful 微服务。

《第三章》使用 Docker 构建、运行和测试您的服务,向您展示如何使用 Docker 封装微服务,以创建标准的、不可变的容器。

第四章《创建管道和工作流程》教你如何自动运行测试和其他操作,以确保容器始终具有高质量并且可以立即使用。

第三部分《使用多个服务:通过 Kubernetes 操作系统》转向下一个阶段,即协调每个单独的微服务,使它们作为一个整体在一致的 Kubernetes 集群中运行。

第五章《使用 Kubernetes 协调微服务》介绍了 Kubernetes 的概念和对象,包括如何安装本地集群。

第六章《使用 Kubernetes 进行本地开发》让您在本地 Kubernetes 集群中部署和操作您的微服务。

第七章《配置和保护生产系统》深入探讨了在 AWS 云中部署的生产 Kubernetes 集群的设置和操作。

第八章《使用 GitOps 原则》详细描述了如何使用 Git 源代码控制来控制 Kubernetes 基础设施定义。

第九章《管理工作流程》解释了如何在微服务中实现新功能,从设计和实施到部署到向世界开放的现有 Kubernetes 集群系统。

第四部分《生产就绪系统:使其在现实环境中运行》讨论了在现实集群中成功操作的技术和工具。

第十章《监控日志和指标》是关于监控活动集群的行为,以主动检测问题和改进。

第十一章《处理系统中的变更、依赖和秘密》关注如何有效地处理在集群中共享的配置,包括正确管理秘密值和依赖关系。

第十二章《跨团队协作和沟通》关注独立团队之间的团队合作挑战以及如何改善协作。

为了充分利用本书

本书使用 Python 进行编码,并假定读者能够熟练阅读这种编程语言,尽管不需要专家级水平。

本书中始终使用 Git 和 GitHub 进行源代码控制和跟踪更改。假定读者熟悉使用它们。

熟悉 Web 服务和 RESTful API 对于理解所呈现的不同概念是有用的。

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packt.com

  2. 选择“支持”选项卡。

  3. 单击“代码下载”。

  4. 在搜索框中输入书名并按照屏幕上的说明操作。

下载文件后,请确保使用以下最新版本解压或提取文件夹:

  • Windows 系统使用 WinRAR/7-Zip

  • Mac 系统使用 Zipeg/iZip/UnRarX

  • Linux 系统使用 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/ 上找到。去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:static.packt-cdn.com/downloads/9781838823818_ColorImages.pdf

代码实例

您可以在此处查看本书的代码实例视频:bit.ly/34dP0Fm

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:“这将生成两个文件:key.pemkey.pub,带有私钥/公钥对。”

代码块设置如下:

class ThoughtModel(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50))
    text = db.Column(db.String(250))
    timestamp = db.Column(db.DateTime, server_default=func.now())

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:

# Create a new thought
new_thought = ThoughtModel(username=username, text=text, timestamp=datetime.utcnow())
db.session.add(new_thought)

任何命令行输入或输出都以以下方式书写:

$ openssl rsa -in key.pem -outform PEM -pubout -out key.pub

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“从管理面板中选择系统信息。”

警告或重要说明会以这种方式出现。

提示和技巧会以这种方式出现。

第一部分:微服务简介

本节涵盖了书的第一章。它介绍了微服务架构以及它旨在解决的经典单体系统的问题。

第一章,迁移-设计、计划、执行,描述了单体系统的典型情况,它的问题,以及如何将其迁移到微服务可以提高开发速度和独立功能实现。它还提出了一项分步计划,以便从最初的独特单体迁移到多个 RESTful 微服务。它还介绍了使用 Docker 来将不同的微服务实现为容器。

在本节中,我们描述了本书中将用作示例系统的示例系统,以便真实地演示从单体应用到微服务架构的过程。

本节包括以下章节:

  • 第一章,迁移-设计、计划和执行

第一章:迁移 - 设计、规划和执行

随着 Web 服务变得越来越复杂,软件服务公司的规模也在增长,我们需要新的工作方式来适应并提高变化的速度,同时确保高质量标准。微服务架构已经成为控制大型软件系统的最佳工具之一,得益于容器和编排器等新工具的支持。我们将首先介绍传统单体架构和微服务架构之间的区别,以及迁移到后者的优势。我们将介绍如何构建架构迁移以及如何计划成功完成这一困难的过程。

在本书中,我们将处理 Web 服务器服务,尽管一些想法也可以用于其他类型的软件应用程序,显然需要进行调整。单体/微服务架构与操作系统设计中的单体/微内核讨论有一些相似之处,包括 1992 年 Linus Torvalds 和 Andrew S. Tanenbaum 之间的著名辩论(www.oreilly.com/openbook/opensources/book/appa.html)。本章对工具相对中立,而接下来的章节将介绍具体的工具。

本章将涵盖以下主题:

  • 传统的单体方法及其问题

  • 微服务方法的特点

  • 并行部署和开发速度

  • 挑战和警示信号

  • 分析当前系统

  • 通过测量使用情况进行准备和调整

  • 分解单体的战略规划

  • 执行迁移

在本章的结尾,您将熟悉我们将在整本书中使用的基本概念,不同的策略,以及在迁移到微服务期间如何进行和构建工作的实际示例。

技术要求

本章不专注于特定技术,而采用更中立的方法。我们将讨论一个 Python Django 应用程序作为我们单体示例。

单体示例可以在以下位置找到:github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter01/Monolith。安装和运行说明可以在其README.md文件中找到。

传统的单体方法及其问题

在开发系统时,传统的软件方法是创建一个单体。这是一个花哨的词,意思是“包含一切的单一元素”,这几乎是每个项目开始的方式。在 Web 应用程序的上下文中,这意味着创建可部署的代码,可以复制,以便请求可以指向任何已部署的副本:

毕竟,每个项目都会从小处开始。在早期进行严格的划分是不方便的,甚至没有意义。新创建的项目很小,可能由单个开发人员处理。虽然设计可以适合几个人的头脑,但在系统的各个部分之间进行严格的界限是适得其反的。

有很多运行 Web 服务的选项,但通常会包括一个或多个服务器(物理服务器、虚拟机和云实例,如 EC2 等),运行 Web 服务器应用程序(如 NGINX 或 Apache)来将请求指向 HTTP 端口80或 HTTPS 端口443,指向一个或多个 Python 工作进程(通常通过 WSGI 协议),由mod_wsgi运行 - github.com/GrahamDumpleton/mod_wsgi(仅限 Apache)、uWSGI、GNUnicorn 等。

如果使用多台服务器,将会有一个负载均衡器来在它们之间分配负载。我们将在本章后面讨论它们。服务器(或负载均衡器)需要在互联网上可访问,因此它将拥有专用的 DNS 和公共 IP 地址。

在其他编程语言中,结构将是类似的:一个前端 web 服务器公开端口以进行 HTTP/HTTPS 通信,以及在专用 web 工作人员中运行单体代码的后端。

但事情会改变,成功的软件会增长,经过一段时间,拥有一大堆代码可能不是构建大型项目的最佳方式。

单体应用在任何情况下都可以有内部结构,这意味着它们不一定会变成意大利面代码。它可能是完全结构化的代码。定义单体应用的是需要将系统作为一个整体部署,而不能进行部分部署。

意大利面代码是指缺乏任何结构且难以阅读和理解的代码的常见方式。

随着单体应用的增长,一些限制将开始显现:

  • 代码将增加:没有模块之间的严格边界,开发人员将开始遇到理解整个代码库的问题。尽管良好的实践可以帮助,但复杂性自然倾向于增加,使得在某些方面更改代码变得更加困难,并增加微妙的 bug。运行所有测试将变得缓慢,降低任何持续集成系统的速度。

  • 资源利用效率低下:每个部署的 web 工作人员都需要整个系统工作所需的所有资源,例如,任何类型的请求所需的最大内存,即使需要大量内存的请求很少,只需要几个工作人员就足够了。CPU 也可能出现相同的情况。如果单体应用连接到数据库,每个工作人员都需要连接到它,无论是否经常使用等等。

  • 开发可扩展性问题:即使系统被设计成可以水平扩展(可以添加无限数量的新工作人员),随着系统和开发团队的增长,开发将变得越来越困难,而不会相互干扰。一个小团队可以轻松协调,但一旦有几个团队在同一个代码库上工作,冲突的可能性就会增加。除非严格执行纪律,否则对团队的所有权和责任进行界定也可能变得模糊。无论如何,团队都需要积极协调,这会降低他们的独立性和速度。

  • 部署限制:部署方法需要在团队之间共享,并且团队不能分别对每个部署负责,因为部署可能涉及多个团队的工作。部署问题将导致整个系统崩溃。

  • 技术的相互依赖:任何新技术都需要与单体应用中使用的技术相匹配。例如,一个对特定问题非常适合的工具可能很难添加到单体应用中,因为技术不匹配。更新依赖项也可能会导致问题。例如,更新到 Python 的新版本(或子模块)需要与整个代码库一起运行。一些必要的维护任务,如安全补丁,可能会导致问题,因为单体应用已经使用了特定版本的库,如果更改将会破坏。适应这些变化也需要额外的工作。

  • 系统的一小部分出现 bug 可能导致整个服务崩溃:由于服务是一个整体,任何影响稳定性的关键问题都会影响到一切,使得难以制定高质量的服务策略或导致结果下降。

正如您在示例中所看到的,大多数单体问题都是逐渐增长的问题。除非系统有相当大的代码库,否则它们并不真正重要。在单体系统中有一些非常有效的东西,比如,由于代码中没有边界,代码可以被迅速高效地改变。但随着团队的壮大,越来越多的开发人员在系统中工作,边界有助于定义目标和责任。过度的灵活性在长期内会成为问题。

微服务方法的特点

单体方法适用,直到它不适用为止。但是,替代方案是什么?这就是微服务架构进入场景的地方。

遵循微服务架构的系统是一组松散耦合的专门化服务,它们协同工作以提供全面的服务。让我们稍微分解一下这个定义,更具体地说:

  1. 一组专门的服务,意味着有不同的、明确定义的模块。

  2. 松散耦合,意味着每个微服务都可以独立部署。

  3. 它们协同工作——每个微服务都能够与其他微服务通信。

  4. 提供全面的服务,因为我们的微服务系统将需要复制使用单体方法可用的相同功能。这是其设计背后的意图。

与之前的图表相比,微服务架构将如下所示:

每个外部请求将被引导到微服务 A微服务 B,每个微服务专门处理一种特定类型的请求。在某些情况下,微服务 B微服务 C通信,而不是直接对外可用。请注意,每个微服务可能有多个工作人员。

这种架构有几个优势和含义:

  1. 如果微服务之间的通信是通过标准协议进行的,那么每个微服务可以用不同的语言编程。

在整本书中,我们将使用 HTTP 请求,并使用 JSON 编码的数据在微服务之间进行通信。虽然还有更多的选择,但这绝对是最标准和广泛使用的选项,因为几乎每种广泛使用的编程语言都对其有很好的支持。

这在某些情况下非常有用,比如专门的问题需要专门的语言,但限制其使用,使其受控,不需要公司进行 drastical 的变化。

  1. 更好的资源利用——如果微服务 A需要更多的内存,我们可以减少工作人员的副本数量。而在单体系统中,每个工作人员都需要最大的资源分配,现在每个微服务只使用其所需的整个系统部分的资源。也许其中一些不需要连接到数据库,例如。每个单独的元素都可以进行微调,甚至在硬件级别。

  2. 每个单独的服务都更小,可以独立处理。这意味着更少的代码需要维护,更快的构建,更简单的设计,更少的技术债务需要维护。服务之间没有依赖问题,因为每个服务都可以自己定义和移动它们的步伐。进行重构可以以更受控的方式进行,因为它们不会影响整个系统的完整性。此外,每个微服务都可以更改其编程语言,而不会影响其他微服务。

从某种角度来看,微服务架构类似于 UNIX 哲学,应用于 Web 服务:编写每个程序(服务)来做一件事,并且做得很好,编写程序(服务)来协同工作,编写程序(服务)来处理文本流(HTTP 调用),因为这是一个通用接口。

  1. 一些服务可以隐藏不对外部访问。例如,微服务 C只能被其他服务调用,而不能被外部访问。在某些情况下,这可以提高安全性,减少对敏感数据或服务的攻击面积。

  2. 由于系统是独立的,一个系统中的稳定问题不会完全停止整个系统。这减少了关键响应并限制了灾难性故障的范围。

  3. 每个服务可以由不同的开发人员独立维护。这允许并行开发和部署,增加了公司可以完成的工作量。这要求暴露的 API 是向后兼容的,我们将在后面描述。

Docker 容器

微服务架构对支持它的平台非常不可知。它可以部署在专用数据中心中的旧物理盒子上,也可以在公共云中或以容器化形式部署。

然而,有一种倾向是使用容器来部署微服务。容器是一种软件的打包捆绑,封装了运行所需的一切,包括所有依赖项。它只需要兼容的操作系统内核来自主运行。

Docker 是 Web 应用程序容器的主角。它有一个非常充满活力的社区支持,以及用于处理各种操作的出色工具。我们将学习如何使用 Docker 进行工作和操作。

第一次使用 Docker 容器时,它们对我来说看起来像一种轻量级虚拟机;一个不需要模拟硬件即可运行的小型操作系统。但过了一段时间,我意识到这不是正确的方法。

描述容器的最佳方式是将其视为被自己的文件系统包围的进程。您运行一个进程(或几个相关的进程),它们看到一个完整的文件系统,不被任何人共享。

这使得容器非常易于移植,因为它们与运行它们的底层硬件和平台分离;它们非常轻量级,因为只需要包含最少量的数据,它们是安全的,因为容器的暴露攻击面非常小。您不需要像在传统服务器上那样管理它们的应用程序,比如sshd服务器,或者像 Puppet 这样的配置工具。它们是专门设计成小而单一用途的。

特别是,尽量保持您的容器小而单一用途。如果最终添加了几个守护程序和大量配置,那么很可能您试图包含太多;也许您需要将其拆分成几个容器。

使用 Docker 容器有两个步骤。首先,我们构建容器,对文件系统执行一层又一层的更改,比如添加将要执行的软件和配置文件。然后,我们执行它,启动它的主要命令。我们将在第三章中看到如何做到这一点,将服务 Docker 化

微服务架构与 Docker 容器的一些特征非常契合——小型、单一用途的元素,通过 HTTP 调用进行通信。这就是为什么尽管这不是一个硬性要求,但这些天它们通常一起呈现的原因。

十二要素应用原则是一系列在开发 Web 应用程序中被证明成功的实践,它们也与 Docker 容器和微服务架构非常契合。其中一些原则在 Docker 中非常容易遵循,我们将在本书后面深入讨论它们。

处理容器的一个重要因素是容器应该是无状态的(Factor VI—12factor.net/processes)。任何状态都需要存储在数据库中,每个容器都不存储持久数据。这是可扩展的 Web 服务器的关键元素之一,当涉及到几台服务器时,可能无法完成。请务必记住这一点。

Docker 的另一个优势是有大量现成的容器可用。Docker Hub(hub.docker.com/)是一个充满有趣容器的公共注册表,可以继承或直接使用,无论是在开发还是生产中。这有助于您为自己的服务提供示例,并快速创建需要很少配置的小型服务。

容器编排和 Kubernetes

尽管 Docker 提供了如何处理每个单独微服务的方法,但我们需要一个编排器来处理整个服务集群。为此,我们将在整本书中使用 Kubernetes(kubernetes.io/)。这是主要的编排项目,并且得到了主要云供应商的大力支持。我们将在第五章中详细讨论它,使用 Kubernetes 协调微服务

并行部署和开发速度

最重要的元素是能够独立部署。创建成功的微服务系统的第一条规则是确保每个微服务尽可能独立地运行。这包括开发、测试和部署。

这是允许不同团队并行开发的关键因素,使它们能够扩展工作。这增加了复杂系统变更的速度。

负责特定微服务的团队需要能够在不中断其他团队或服务的情况下部署微服务的新版本。目标是增加部署次数和每次部署的速度。

微服务架构与持续集成和持续部署原则密切相关。小型服务易于保持最新状态和持续构建,以及在不中断的情况下部署。在这方面,CI/CD 系统倾向于采用微服务,因为它增加了并行化和交付速度。

由于微服务的部署对于依赖服务应该是透明的,因此应特别注意向后兼容性。一些更改需要逐步升级并与其他团队协调,以删除旧的、不正确的功能,而不会中断系统。

尽管在理论上,完全断开的服务是可能的,但在实践中并不现实。一些服务之间会存在依赖关系。微服务系统将迫使您在服务之间定义明确的边界,并且任何需要跨服务通信的功能都将带来一些额外的开销,甚至可能需要协调不同团队的工作。

转向微服务架构时,这一举措不仅仅是技术上的改变,还意味着公司工作方式的重大变革。微服务的开发将需要自治和结构化的沟通,这需要在系统的总体架构规划中提前付出额外的努力。在单体系统中,这可能是临时的,并且可能已经演变成一个内部结构不太分离的结构,增加了纠缠的代码和技术债务的风险。

清晰沟通和定义所有者的需求不言而喻。目标是允许每个团队就其代码做出自己的决定,并规范和维护其他服务依赖的外部 API。

这种额外的规划增加了长期交付带宽,因为团队被授权做出更多自主决策,包括诸如使用哪种操作系统或编程语言等重大决策,以及使用第三方软件包、框架或模块结构等许多较小的决策。这增加了日常开发速度。

微服务架构也可能影响组织中团队的结构。一般规则是要尊重现有的团队。他们会有非常有用的专业知识,而彻底革命会破坏这一点。但可能需要进行一些微调。一些概念,比如理解 Web 服务和 RESTful 接口,需要在每个微服务中出现,以及如何部署自己的服务的知识。

传统的团队划分方式是创建一个负责基础设施和任何新部署的运维团队,因为他们是唯一被允许访问生产服务器的人。微服务方法会干扰这一点,因为它需要团队能够控制自己的部署。在第五章中,使用 Kubernetes 协调微服务,我们将看到使用 Kubernetes 如何在这种情况下有所帮助,将基础设施的维护与服务的部署分离开来。

这也允许创建一种强烈的所有权感,因为团队被鼓励以自己喜欢的方式在自己的领域内工作,同时与其他团队一起在明确定义和结构化的边界内进行游戏。微服务架构可以允许在系统的小部分进行实验和创新,一旦证明有效,就可以在整个系统中传播。

挑战和红旗

我们已经讨论了微服务架构相对于单体应用的许多优势,但迁移是一项庞大的工程,不应该被低估。

系统开始时是单体应用,因为这样更简单,可以在小的代码库中进行更快的迭代。在任何新公司中,转变和改变代码,寻找成功的商业模式至关重要。这比清晰的结构和架构分离更重要——这就是应该的方式。

然而,一旦系统成熟,公司发展起来。随着越来越多的开发人员参与进来,单体应用的优势开始变得不那么明显,长期战略和结构的需求变得更加重要。更多的结构并不一定意味着向微服务架构迈进。一个良好架构的单体应用可以实现很多。

转向微服务也有自己的问题。其中一些是:

  1. 迁移到微服务需要大量的努力,积极改变组织的运作方式,并且需要大量的投资,直到开始见效。过渡可能会很痛苦,因为需要采取务实的方法,并需要做出妥协。这还将涉及大量的设计文件和会议来规划迁移,而业务仍在继续运营。这需要全面的承诺和对所涉及内容的理解。

  2. 不要低估文化变革——组织是由人组成的,人们不喜欢变化。微服务中的许多变化与不同的运营方式和不同的做事方式有关。虽然这赋予了不同的团队权力,但也迫使他们澄清他们的接口和 API,并形式化沟通和边界。这可能导致团队成员的挫折和抵制。

有一句叫康威定律的格言(www.melconway.com/Home/Conways_Law.html),它指出设计系统的组织受限于产生与这些组织的沟通结构相同的设计。对于微服务来说,这意味着团队之间的分工应该反映不同的服务。让多个团队在同一个微服务中工作会模糊界面。我们将在第十二章《跨团队协作和沟通》中详细讨论康威定律。

  1. 学习工具和程序也需要一定的学习曲线。管理集群与单体的方式不同,开发人员需要了解如何在本地进行测试时相互操作不同的服务。同样,部署也与传统的本地开发不同。特别是,学习 Docker 需要一些时间来适应。因此,要做好计划,并为所有参与者提供支持和培训。

  2. 调试跨服务的请求比单体系统更困难。监控请求的生命周期很重要,一些微妙的错误在开发中可能很难复制和修复。

  3. 将单体拆分为不同的服务需要仔细考虑。糟糕的划分线会使两个服务紧密耦合,不允许独立部署。这意味着几乎任何对一个服务的更改都需要对另一个服务进行更改,即使通常情况下可以独立完成。这会导致工作的重复,因为通常需要对单个功能进行更改和部署多个微服务。微服务以后可以进行变异,边界可以重新定义,但这是有成本的。在添加新服务时也应该采取同样的谨慎。

  4. 创建微服务存在一些额外开销,因为一些工作会在每个服务上复制。这种额外开销通过允许独立和并行开发来进行补偿。但是,要充分利用这一点,你需要数量。一个最多有 10 人的小型开发团队可以高效地协调和处理单体。只有当规模扩大并形成独立团队时,迁移到微服务才开始变得有意义。公司规模越大,这种做法就越有意义。

  5. 自由和允许每个团队做出自己的决定以及标准化一些共同的元素和决定之间需要保持平衡。如果团队缺乏方向,他们会一遍又一遍地重新发明轮子。他们也会最终创建知识孤岛,其中公司的某一部分的知识完全无法转移到另一个团队,这使得共同学习变得困难。团队之间需要良好的沟通,以便达成共识并重复使用共同的解决方案。允许受控实验,将其标记为实验,并让所有团队从中汲取教训,以使其他团队受益。共享和可重复使用的想法与独立的、多重实施的想法之间会产生紧张关系。

在引入跨服务共享代码时要小心。如果代码增长,它将使服务相互依赖。这可能会减少微服务的独立性。

  1. 遵循敏捷原则,我们知道,工作软件比广泛的文档更重要。然而,在微服务中,最大限度地提高每个单独微服务的可用性以减少团队之间的支持量是很重要的。这涉及一定程度的文档编制。最好的方法是创建自我记录的服务。我们将在本书的后面看一些例子,介绍如何使用工具来允许以最小的努力记录如何使用服务。

  2. 每次调用另一个服务,比如内部微服务相互调用,都会增加响应的延迟,因为将涉及多个层。这可能会产生延迟问题,外部响应时间更长。它们也会受到内部网络连接微服务的性能和容量的影响。

迁移到微服务应该谨慎进行,并仔细分析其利弊。在成熟的系统中,完成迁移可能需要数年的时间。但对于一个大型系统来说,结果系统将更加灵活和易于更改,使您能够有效地处理技术债务,并赋予开发人员充分的所有权和创新能力,构建沟通并提供高质量、可靠的服务。

分析当前系统

正如我们之前定义的,从单体迁移到一组微服务的第一步是了解当前系统。这个阶段不应该被低估。很可能没有一个人对单体的不同组件有很好的理解,特别是如果一些部分是遗留的。

这个阶段的目标是确定迁移到微服务是否真的有益,并初步了解迁移的结果将是什么样的微服务。正如我们所讨论的,迁移是一个巨大的投资,不应该轻率对待。在这个阶段无法对所需的工作量进行详细估计;此时的不确定性将会很大,但千里之行始于足下。

所涉及的工作将大大取决于单体的结构化程度。这可能从一团没有太多方向的有机生长的意大利面代码混乱到一个结构良好、模块化的代码库。

我们将在本书中使用一个示例应用程序——一个名为 MyThoughts 的微博网站,这是一个简单的服务,允许我们发布和阅读短消息或想法。该网站允许我们登录、发布新想法、查看我们的想法,并在系统中搜索想法。

作为第一步,我们将绘制单体的架构图。将当前系统简化为相互交互的块列表。

我们示例的代码在这里可用:github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter01/Monolith。这是一个使用 Bootstrap 作为其 HTML 界面的 Django 应用程序。查看README以获取运行说明。

在我们的示例中,MyThoughts 模型在以下图表中描述:

正如你所看到的,单体似乎遵循了模型视图控制器结构(www.codecademy.com/articles/mvc):

Django 使用一种称为模型模板视图的结构,它遵循与 MVC 类似的模式。阅读medium.com/shecodeafrica/understanding-the-mvc-pattern-in-django-edda05b9f43f上的文章以获取更多信息。它是否完全符合 MVC 是值得商榷的。让我们不要陷入语义,而是将定义作为描述系统的起点。

  • 数据库中存储并通过模型访问的三个实体:用户、想法和会话模型。会话用于跟踪登录。

  • 用户可以通过login.py中的代码登录和退出以访问网站。如果用户登录,将创建一个会话,允许用户查看网站的其余部分。

请注意,此示例中身份验证和密码的处理仅用于演示目的。请使用 Django 中的默认机制以获得更安全的访问。会话也是一样,原生会话管理未被使用。

  • 用户可以看到他们自己的想法。在同一页上,有一个新的表单可以创建一个新的想法。这由thoughts.py文件处理,通过ThoughtModel检索和存储想法。

  • 要搜索其他用户的想法,有一个搜索栏连接到search.py模块并返回获取的值。

  • HTML 通过login.htmlsearch.htmllist_thoughts.htmlbase.html模板呈现。

  • 此外,还有样式网站的静态资产。

这个例子非常简单,但我们能够看到一些相互依赖:

  • 静态数据非常隔离。它可以在任何时候更改,而无需在其他任何地方进行任何更改(只要模板与 Bootstrap 兼容)。

  • 搜索功能与列出想法密切相关。模板相似,信息以相同的方式显示。

  • 登录和注销不与ThoughtModel交互。它们编辑会话,但应用程序的其余部分只读取那里的信息。

  • base.html模板生成顶部栏,并用于所有页面。

在进行这项分析之后,一些关于如何继续的想法浮现在脑海中:

  1. 保持现状,投资于结构化,但不将其拆分为多个服务。它已经有一定的结构,尽管有些部分可以改进。例如,处理用户是否已登录的方式可能会更好。这显然是一个小例子,在现实生活中,将其拆分为微服务将会产生很大的开销。请记住,坚持使用单体架构可能是一种可行的策略,但如果这样做,请投入时间来清理代码和偿还技术债务。

  2. 搜索想法非常基本。目前,我们直接搜索数据库。如果有数百万个想法,这将不是一个可行的选择。search.py中的代码可以调用一个特定的搜索微服务,由 Solr(lucene.apache.org/solr/)或 Elasticsearch(www.elastic.co/products/elasticsearch)支持的搜索引擎。这将扩展搜索,并可以添加诸如在日期之间搜索或显示文本匹配等功能。搜索也是只读的,因此将创建新想法的调用与搜索它们的调用分离可能是一个好主意。

  3. 身份验证也是与阅读和编写想法不同的问题。拆分它将使我们能够跟踪新的安全问题,并有一个专门处理这些问题的团队。从应用程序的角度来看,它只需要有一个可用于检查用户是否已登录的东西,这可以委托给一个模块或包。

  4. 前端目前非常静态。也许我们想创建一个单页面应用程序,调用后端 API 在客户端渲染前端。为此,需要创建一个能够返回想法和搜索元素的 RESTful API 微服务。前端可以使用 JavaScript 框架编码,例如 Angular(angular.io)或 React(reactjs.org/)。在这种情况下,新的微服务将成为前端,将作为静态的预编译代码提供,并将从后端拉取。

  5. RESTful API 后端也将可用于允许外部开发人员在 MyThoughts 数据之上创建自己的工具,例如创建原生手机应用程序。

这只是一些想法,需要讨论和评估。对于您的单片应用程序来说,具体的痛点是什么?路线图和战略未来是什么?现在或未来最重要的点和功能是什么?也许对于某家公司来说,拥有强大的安全性是优先考虑的,第 3 点至关重要,但对于另一家公司来说,第 5 点可能是与合作伙伴合作的扩展模型的一部分。

团队的结构也很重要。第 4 点将需要具有良好的前端和 JavaScript 技能的团队,而第 2 点可能涉及后端优化和数据库工作,以允许对数百万条记录进行高效搜索。

在这里不要过快地得出结论;考虑一下什么样的能力是可行的,您的团队可以实现什么。正如我们之前讨论过的,转变为微服务需要一定的工作方式。与相关人员核实他们的反馈和建议。

经过一些考虑,对于我们的示例,我们提出以下潜在架构:

系统将分为以下模块:

  1. Users backend: 这将负责所有身份验证任务,并保留有关用户的信息。它将在数据库中存储其数据。

  2. Thoughts backend: 这将创建和存储thoughts

  3. Search backend: 这将允许搜索thoughts

  4. 一个代理将任何请求路由到适当的后端。这需要是外部可访问的。

  5. HTML frontend: 这将复制当前的功能。这将确保我们以向后兼容的方式工作,并且过渡可以顺利进行。

  6. 允许客户端访问后端将允许创建除我们的 HTML 前端之外的其他客户端。将创建一个动态前端服务器,并且正在与外部公司讨论创建移动应用程序的事宜。

  7. Static assets: 能够处理静态文件的 Web 服务器。这将为 HTML 前端提供样式和动态前端的索引文件和 JavaScript 文件。

这种架构需要适应实际使用;为了验证它,我们需要测量现有的使用情况。

通过测量来准备和适应。

显然,任何真实世界的系统都会比我们的示例更复杂。通过仔细观察,代码分析能够发现的内容是有限的,而计划往往在接触真实世界时无法生存。

任何划分都需要经过验证,以确保它将产生预期的结果,并且付出的努力是值得的。因此,请仔细检查系统是否按您认为的方式运行。

了解实时系统运行情况的能力被称为可观测性。它的主要工具是指标和日志。您将发现的问题是,它们通常会配置为反映外部请求,并且不提供有关内部模块的信息。我们将在第十章中深入讨论系统的可观测性,监控日志和指标。您可以参考它以获取更多信息,并在此阶段应用那里描述的技术。

如果您的系统是一个网络服务,默认情况下,它将已激活其访问日志。这将记录系统中进入的每个 HTTP 请求,并存储 URL、结果和发生时间。与您的团队核实这些日志的位置,因为它们将提供关于调用哪些 URL 的良好信息。

尽管这种分析可能只会提供有关被调用的外部端点的信息,但对于根据我们的计划将被分割为不同微服务的内部模块,它不会提供太多信息。请记住,对于微服务长期成功的最重要因素是允许团队独立。如果您跨模块进行分割,而这些模块需要不断协同变更,部署将不会真正独立,并且在过渡后,您将被迫使用两个紧密耦合的服务。

特别要小心的是,不要创建一个对每个其他服务都是依赖的微服务。除非该服务非常稳定,否则当任何其他服务需要新功能时,可能会频繁更新。

为了验证新的微服务不会紧密耦合,让团队了解这些分割以及他们周围的接口需要多久改变一次。在接下来的几周内监控这些变化,确保分割线是稳定的,不需要不断变化。如果微服务之间的接口被频繁更改,任何功能都将需要在多个服务中进行多次更改,这将减缓交付新功能的速度。

在我们的示例中,经过分析提出的架构后,我们决定简化设计,如图所示:

在监控和与团队交流之后,已经做出了一些决定:

  1. 团队对 JavaScript 动态编程的了解不够。在同时进行前端变更和转向微服务的情况下,被视为过于冒险。

  2. 另一方面,外部移动应用被视为公司的战略举措,使外部可访问的 API 成为一个可取的举措。

  3. 分析日志,似乎搜索功能并不经常使用。搜索次数的增长很小,将搜索拆分为独立的服务将需要与 Thoughts 后端进行协调,因为这是一个积极开发的领域,正在添加新字段。决定将搜索保留在 Thoughts 后端,因为它们都与相同的 Thoughts 一起工作。

  4. 用户后端已经得到了良好的接受。这将通过明确负责修补安全漏洞和改进服务的所有权来提高身份验证的安全性。其余的微服务将需要独立工作,并由用户后端进行验证,这意味着负责这个微服务的团队将需要创建和维护一个包,其中包含验证请求的信息。

一旦我们决定了最终状态,我们仍然需要决定如何从一个状态转移到另一个状态。

分解单体的战略规划

正如我们之前讨论过的,从初始状态到期望状态的转变将是一个缓慢的过程。不仅因为它涉及到新的工作方式,而且还因为它将与其他“业务如常”的功能和发展并行进行。实际上,公司的业务活动不会停止。因此,应该制定一个计划,以便在一个状态和另一个状态之间实现平稳过渡。

这被称为窒息模式docs.microsoft.com/en-us/azure/architecture/patterns/strangler)-逐渐替换系统的部分,直到旧系统被“窒息”,可以安全地移除。

有几种技术方法可以选择,以进行转变并将每个元素分割以迁移到新系统:

  • 替换方法,将旧代码替换为全新编写的新服务

  • 分割方法,挑选现有代码并将其移入全新的服务

  • 两者的结合

让我们更仔细地看一看。

替换方法

大块地替换服务,只考虑它们的外部接口或影响。这种黑盒方法完全用从头开始的替代功能编码替换现有功能。一旦新代码准备就绪,它就会被激活,旧系统中的功能就会被弃用。

请注意,这并不是指替换整个系统的单个部署。这可以部分地、一块一块地完成。这种方法的基础是创建一个新的外部服务,旨在取代旧系统。

这种方法的优点在于它极大地有助于构建新服务,因为它不会继承技术债务,并且可以以新的视角审视旧问题。

新服务还可以使用新工具,并且不需要继续使用与公司技术未来方向战略观点不一致的旧技术栈。

这种方法的问题在于成本可能很高,而且可能需要很长时间。对于未经记录的旧服务,替换它们可能需要大量的工作。此外,这种方法只能应用于稳定的模块;如果它们在积极开发中,试图用其他东西替换它们就会不断改变目标。

这种方法对于小型的旧遗留系统或者至少有一小部分执行有限功能的系统来说是最合理的,而且这些系统是使用难以维护的旧技术栈开发的,或者已不再被认为是可取的。

分割的方法

如果系统结构良好,也许它的一些部分可以干净地分割成自己的系统,保持相同的代码。

在这种情况下,创建一个新服务更多地是一个复制粘贴的练习,并用最少量的代码包装它,以使其能够独立执行并与其他系统进行交互,换句话说,以 HTTP 请求为基础来构建其 API 以获得标准接口。

如果可以使用这种方法,这意味着代码已经相当结构化,这是个好消息。

被调用到这一部分的系统也必须进行调整,不是调用内部代码,而是通过 HTTP 调用。好处是这可以分几步完成:

  1. 将代码复制到自己的微服务中并部署它。

  2. 旧的调用系统正在使用旧的嵌入式代码。

  3. 迁移一个调用并检查系统是否正常工作。

  4. 迭代,直到所有旧的调用都迁移到新系统。

  5. 从旧系统中删除分割的代码。

如果代码结构不太干净,我们需要先进行更改。

更改和结构化方法

如果单体系统是有机增长的,那么它的所有模块都不太可能是干净的结构化。可能存在一些结构,但也许它们并不是我们期望的微服务划分的正确结构。

为了适应服务,我们需要进行一些内部更改。这些内部更改可以进行迭代,直到服务可以被干净地分割。

这三种方法可以结合起来进行完整的迁移。每种方法所涉及的工作量并不相同,因为一个易于分割的服务将能够比替换文档不完整的遗留代码更快地进行迁移。

在项目的这个阶段,目标是拥有一个清晰的路线图,应该分析以下元素:

  • 一个有序的计划,确定哪些微服务将首先可用,考虑如何处理依赖关系。

  • 了解最大的痛点是什么,以及是否解决它们是一个优先事项。痛点是经常处理的元素,而目前处理单体系统的方式使它们变得困难。

  • 有哪些困难点和棘手的问题?很可能会有一些。承认它们的存在,并将它们对其他服务的影响最小化。请注意,它们可能与痛点相同,也可能不同。困难点可能是非常稳定的旧系统。

  • 一些快速的成功案例将保持项目的动力。快速向您的团队和利益相关者展示优势!这也将使每个人都能够理解您想要转移到的新操作模式并开始以这种方式工作。

  • 团队需要的培训和您想要引入的新元素的想法。此外,您的团队是否缺乏任何技能——您可能计划招聘。

  • 任何团队变化和对新服务的所有权。重要的是要考虑团队的反馈,这样他们就可以表达他们对计划制定过程中任何疏忽的担忧。

对于我们的具体示例,结果计划如下:

  • 作为先决条件,负载均衡器需要位于操作的前面。这将负责将请求引导到适当的微服务。然后,更改这个元素的配置,我们将能够将请求路由到旧的单体或任何新的微服务。

  • 之后,静态文件将通过它们自己独立的服务提供,这是一个简单的更改。一个静态的 Web 服务器就足够了,尽管它将部署为一个独立的微服务。这个项目将有助于理解转移到 Docker。

  • 身份验证的代码将被复制到一个新的服务中。它将使用 RESTful API 进行登录和生成会话,以及注销。该服务将负责检查用户是否存在,以及添加和删除他们:

  • 最初的想法是针对每个检索到的会话进行检查,但是,鉴于检查会话是一个非常常见的操作,我们决定生成一个包,在外部面向的微服务之间共享,这将允许检查会话是否已经使用我们自己的服务生成。这将通过对会话进行加密签名并在我们的服务之间共享密钥来实现。预计这个模块不会经常更改,因为它是所有微服务的依赖项。这使得会话不需要存储。

  • 用户后端需要能够使用 OAuth 2.0 模式进行身份验证,这将允许其他不基于 Web 浏览器的外部服务进行身份验证和操作,例如移动应用程序。

  • Thoughts 后端也将作为 RESTful API 进行复制。这个后端目前非常简单,它将包括搜索功能。

  • 在两个后端都可用之后,当前的单体将被更改,从直接调用数据库到使用后端的 RESTful API。成功完成后,旧的部署将被 Docker 构建替换,并添加到负载均衡器。

  • 新的 API 将被添加到负载均衡器并作为外部可访问的推广。制作移动应用程序的公司将开始集成他们的客户端。

我们的新架构图如下:

请注意,HTML 前端将使用与外部可用的相同的 API。这将验证调用是否有用,因为我们将首先为我们自己的客户使用它们。

这个行动计划可以有可衡量的时间和日程安排。还可以采取一些技术选项——在我们的情况下,如下:

团队们同意继续使用这些技术栈,并期待学习一些新技巧!

执行移动

最后一步是执行精心设计的计划,开始从过时的单体架构向新的微服务乐土迁移!

但这个阶段可能是最长和最困难的,特别是如果我们希望保持服务运行而不会出现中断业务的情况。

在这个阶段最重要的想法是向后兼容。这意味着系统在外部看来仍然像旧系统一样运行。如果我们能够做到这一点,我们就可以在客户继续无中断操作的情况下透明地改变我们的内部操作。

这显然更容易说而不易做,有时被称为用福特 T 型车开始比赛,最后用法拉利结束,而不停下来更换每一个零件。好消息是,软件是如此灵活和可塑的,实际上是可能的。

Web 服务的好朋友 - 负载均衡器

负载均衡器是一种工具,允许将 HTTP 请求(或其他类型的网络请求)分配给多个后端资源。

负载均衡器的主要操作是允许将流量定向到单个地址,然后分发到几个相同的后端服务器,以分担负载并实现更好的吞吐量。通常,流量将通过轮询方式分发,即依次分配到所有服务器上:

首先一个工作进程,然后是另一个,依次类推:

这是正常的操作。但它也可以用来替换服务。负载均衡器确保每个请求都干净地发送到一个工作进程或另一个。工作进程池中的服务可以是不同的,因此我们可以使用它来干净地在 Web 服务的一个版本和另一个版本之间进行过渡。

对于我们的目的,一个老的 Web 服务组在负载均衡器后面可以添加一个或多个向后兼容的替代服务,而不会中断操作。替换旧服务的新服务将以较小的数量(也许是一个或两个工作进程)添加到合理的配置中,确保一切都按预期工作。验证后,通过停止向旧服务发送新请求,排空它们,只留下新服务器来完全替换它。

如果以快速的方式进行,就像部署服务的新版本一样,这被称为滚动更新,因此工作进程逐个替换。

但是对于从旧的单体架构迁移到新的微服务,更慢的步伐更明智。一个服务可以在 5%/95%的分裂中生存几天,因此任何意外错误只会出现五分之一的时间,然后转移到 33/66,然后 50/50,然后 100%迁移。

一个高负载的系统具有良好的可观测性,将能够非常快速地检测到问题,并且可能只需要等待几分钟就可以继续。但大多数传统系统可能不会属于这一类。

任何能够以反向代理模式运行的 Web 服务器,如 NGINX,都可以作为负载均衡器工作,但是,对于这项任务,可能最完整的选择是 HAProxy (www.haproxy.org/)。

HAProxy 专门用于在高可用性和高需求的情况下充当负载均衡器。它非常灵活,并且在必要时接受从 HTTP 到更低级别的 TCP 连接的流量。它还有一个出色的状态页面,将帮助监视通过它的流量,并采取快速行动,如禁用失败的工作进程。

云提供商如 AWS 或 Google 也提供集成的负载均衡器产品。它们非常适合从网络边缘工作,因为它们的稳定性使它们非常出色,但它们不会像 HAProxy 那样易于配置和集成到您的操作系统中。例如,亚马逊网络服务提供的产品称为弹性负载均衡ELB)-aws.amazon.com/elasticloadbalancing/

要从具有由 DNS 引用的外部 IP 的传统服务器迁移到前端放置负载均衡器,您需要遵循以下程序:

  1. 创建一个新的 DNS 来访问当前系统。这将允许您在过渡完成后独立地引用旧系统。

  2. 部署负载均衡器,配置为为旧 DNS 上的旧系统提供流量。这样,无论是访问负载均衡器还是旧系统,请求最终都将在同一位置交付。为负载均衡器创建一个专门的 DNS,以允许特别引用它。

  3. 测试向负载均衡器发送请求,指向旧 DNS 的主机是否按预期工作。您可以使用以下curl命令发送请求:

$ curl --header "Host:old-dns.com" http://loadbalancer/path/
  1. 更改 DNS 指向负载均衡器 IP。更改 DNS 注册表需要时间,因为会涉及缓存。在此期间,无论请求从何处接收,都将以相同的方式处理。保持这种状态一两天,以确保每个可能的缓存都已过时并使用新的 IP 值。

  2. 旧 IP 不再使用。服务器可以(也应该)从外部网络中删除,只留下负载均衡器进行连接。需要访问旧服务器的任何请求都可以使用其特定的新 DNS。

请注意,像 HAProxy 这样的负载均衡器可以使用 URL 路径工作,这意味着它可以将不同的路径指向不同的微服务,这在从单体架构迁移中非常有用。

由于负载均衡器是单点故障,您需要对负载均衡器进行负载平衡。最简单的方法是创建几个相同的 HAProxy 副本,就像您对任何其他网络服务所做的那样,并在顶部添加一个云提供商负载均衡器。

因为 HAProxy 如此多才多艺和快速,当正确配置时,您可以将其用作重定向请求的中心点-真正的微服务风格!

保持新旧之间的平衡

计划只是计划,而转移到微服务是为了内部利益而做的事情,因为它需要投资,直到外部改进可以以更好的创新速度的形式显示出来。

这意味着开发团队将面临外部压力,要求在公司正常运营的基础上增加新功能和要求。即使我们进行这种迁移以加快速度,也会有一个初始阶段,您将移动得更慢。毕竟,改变事物是困难的,您需要克服最初的惯性。

迁移将经历三个大致阶段。

试点阶段-设置前两个微服务

在看到第一个部署之前可能需要很多基础设施。这个阶段可能很难克服,也是需要最大努力的阶段。一个好的策略是组建一个专门的新微服务架构团队,并允许他们领导开发。他们可以是参与设计的人,或者可能喜欢新技术或在副业项目中使用过 Docker 和 Kubernetes 的人。并不是你团队中的每个开发人员都会对改变运营方式感到兴奋,但其中一些人会。利用他们的热情开始项目,并在其初步阶段加以照顾:

  1. 从小开始 - 将有足够的工作来建立基础设施。这个阶段的目标是学习工具,建立平台,并调整如何使用新系统。团队合作和协调的方面很重要,从一个小团队开始可以让我们测试一些方法,并迭代以确保它们有效。

  2. 选择非关键服务。在这个阶段,有很多事情可能会出错。确保问题不会对运营或收入产生巨大影响。

  3. 确保保持向后兼容性。用新服务替换单体架构的部分,但不要试图同时改变行为,除非它们显然是错误。

如果有一个新功能可以作为新的微服务实现,那就抓住机会采用新方法,但要确保额外花费的时间或错误的风险是值得的。

巩固阶段 - 稳定迁移至微服务

在初始设置之后,其他团队开始采用微服务方式工作。这扩大了处理容器和新部署的人数,因此最初的团队需要给予他们支持和培训。

培训将是迁移项目的关键部分 - 确保分配足够的时间。虽然培训活动如研讨会和课程对于启动流程非常有用,但经验丰富的开发人员的持续支持是无价的。指定开发人员作为问题的联系点,并明确告诉他们,他们的工作是确保他们回答问题并帮助其他开发人员。让支持团队定期会面,分享对知识转移的关注和改进。

传播知识是这个阶段的主要重点之一,但还有另外两个:澄清和规范流程,以及保持迁移微服务的适当速度。

文档化标准将有助于提供清晰和方向。创建检查点,明确要求,以便非常清楚地知道何时一个微服务准备投入生产。创建适当的反馈渠道,以确保流程可以得到改进。

在这段时间里,迁移的速度可以加快,因为很多不确定性和问题已经得到解决;并且开发将同时进行。尽管可能需要做出妥协,但一定要保持动力并遵循计划。

最终阶段 - 微服务商店

单体架构已经拆分,架构现在是微服务。可能会有被认为优先级较低的单体架构残留部分。任何新功能都是以微服务方式实现的。

虽然理想情况下,从单体架构迁移绝对所有东西可能并不现实。有些部分可能需要很长时间才能迁移,因为它们特别难以迁移,或者涉及公司的奇怪角落。如果是这种情况,至少要清晰地定义边界并限制它们的行动范围。

这个阶段是团队可以完全拥有他们的微服务并开始进行测试和创新,比如改变编程语言。架构也可以改变,微服务可以分割或合并。明确界定微服务的约定要求,但在其中允许自由。

团队将会成熟稳定,流程会运行顺利。密切关注来自不同团队的好主意,并确保传播开来。

恭喜!你做到了!

总结

在本章中,我们看到了传统单体架构方法和微服务架构之间的区别,以及微服务如何使我们能够跨多个团队扩展开发,并改善高质量软件的交付。

我们讨论了从单体架构到微服务架构过渡中所面临的主要挑战,以及如何在不同阶段执行变更的方法:分析当前系统,测量以验证我们的假设,制定分割单体架构的计划,并成功执行迁移的策略。

尽管本章是以技术中立的方式编写的,但我们了解了为什么 Docker 容器是实现微服务的一种好方法,这将在接下来的章节中进行探讨。您现在也知道使用负载均衡器如何帮助保持向后兼容性并以不间断的方式部署新服务。

您学会了如何制定将单体架构分割为更小的微服务的计划。我们描述了这样一个过程的示例以及单体架构的示例以及如何分割它。我们将在接下来的章节中详细了解如何做到这一点。

问题

  1. 单体架构是什么?

  2. 单体架构的一些问题是什么?

  3. 描述微服务架构。

  4. 微服务的最重要特性是什么?

  5. 从单体架构迁移到微服务架构的主要挑战是什么?

  6. 做这样迁移的基本步骤是什么?

  7. 描述如何使用负载均衡器从旧服务器迁移到新服务器而不中断系统。

进一步阅读

您可以在书籍《架构模式》(www.packtpub.com/application-development/architectural-patterns)和《软件架构师手册》(www.packtpub.com/application-development/software-architects-handbook)中了解更多关于系统架构以及如何划分和构建复杂系统的知识。

第二部分:设计和操作单个服务-创建 Docker 容器

本节跨越三章,跟踪了单个微服务的创建过程。它从介绍在 Python 中实现的单个 REST 服务开始,继续完成将服务实现为一个独立的 Docker 容器的所有必要步骤,并创建管道以确保服务始终符合高质量标准。

本节的第一章描述了实现单个服务的过程,按照第一节中提出的示例进行。它描述了要实现的 API 接口,并使用 Python 生成了一个成熟的微服务,使用 Flask 和 SQLAlchemy 等工具来提高开发的便利性。该服务包括一个测试策略。

本节的第二章展示了如何将微服务封装在 Docker 容器中,以便代码可以在软件生命周期中以不可变的方式执行。介绍了基本的 Docker 使用方法,如构建和运行容器,使用环境变量以及如何执行测试。还描述了将容器共享到公共注册表的过程。

本节的第三章深入探讨了自动检查容器中引入的任何新代码是否符合基本质量准则,包括通过所有测试。它介绍了持续集成实践,并演示了如何在 Travis CI 中在云中创建一个管道,并将其集成到 GitHub 存储库中。本章还涵盖了如何自动将生成的容器推送到注册表中。

本节包括以下章节:

  • 第二章,使用 Python 创建 REST 服务

  • 第三章,使用 Docker 构建、运行和测试您的服务

  • 第四章,创建管道和工作流程

第二章:使用 Python 创建 REST 服务

按照上一章的示例,我们将设计为单体的系统拆分为更小的服务。在本章中,我们将详细分析上一章中提到的一个微服务(Thoughts 后端)。

我们将讨论如何使用 Python 开发这个微服务作为一个应用程序。这个微服务将准备好通过标准的 Web RESTful 接口与其他微服务进行交互,这使得它成为我们全局微服务架构系统的基础。

我们将讨论不同的元素,如 API 设计,支持它的数据库模式,以及如何实现和如何实现微服务。最后,我们将看到如何测试应用程序,以确保它正常工作。

本章将涵盖以下主题:

  • 分析 Thoughts 后端微服务

  • 设计 RESTful API

  • 定义数据库模式

  • 实施服务

  • 测试代码

在本章结束时,您将知道如何成功开发一个微服务应用程序,包括从设计到测试的不同阶段。

技术要求

Thoughts 后端示例可以在这里找到(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter02/ThoughtsBackend)。安装和运行说明可以在其README.md文件中找到。

分析 Thoughts 后端微服务

让我们回顾一下我们在上一章中创建的微服务图表:

图表显示了我们示例系统的不同元素:两个后端,用户和想法,以及 HTML 前端。

Thoughts 后端将负责存储新的想法,检索现有的想法,并搜索数据库。

理解安全层

由于 Thoughts 后端将会对外开放,我们需要实现一个安全层。这意味着我们需要识别产生操作的用户并验证其有效性。在这个服务示例中,我们将从已登录的用户创建一个新的想法,并且我们将检索我的想法,以及当前已登录用户创建的想法。

请注意用户已登录也验证了用户的存在。

这个安全层将以一个头部的形式出现。这个头部将包含由用户后端签名的信息,验证其来源。它将采用 JSON Web Token (JWT),jwt.io/introduction/,这是一个标准的用途。

JWT 本身是加密的,但这里包含的信息大多只与检查已登录的用户相关。

JWT 并不是令牌的唯一可能性,还有其他替代方案,比如将等效数据存储在会话 cookie 中,或者在更安全的环境中使用类似的模块,比如 PASETO (github.com/paragonie/paseto)。确保您审查系统的安全影响,这超出了本书的范围。

这个方法应该由用户后端团队处理,并打包,以便其他微服务可以使用它。在本章中,我们将把代码包含在这个微服务中,但稍后我们将看到如何创建它,使其与用户后端相关联。

如果请求没有有效的头部,API 将返回 401 未经授权的状态码。

请注意,并非所有 API 端点都需要身份验证。特别是search不需要登录。

理解了认证系统的工作原理,我们可以开始设计 API 接口。

设计 RESTful API

我们将遵循 RESTful 设计原则来设计我们的 API。这意味着我们将使用构建的 URI 来表示资源,然后使用 HTTP 方法来对这些资源执行操作。

在这个示例中,我们将只使用GET(检索)、POST(创建)和DELETE(删除)方法,因为思想是不可编辑的。请记住,PUT(完全覆盖)和PATCH(执行部分更新)也是可用的。

RESTful API 的主要特性之一是请求需要是无状态的,这意味着每个请求都是完全独立的,可以由任何服务器提供。所有必需的数据应该在客户端(将其附加到请求中发送)或数据库中(因此服务器将完全检索它)。当处理 Docker 容器时,这个属性是一个硬性要求,因为它们可以在没有警告的情况下被销毁和重建。

虽然通常资源直接映射到数据库中的行,但这并非必需。资源可以是不同表的组合,其中的一部分,甚至完全代表不同的东西,例如满足某些条件的数据聚合,或者基于当前数据分析的预测。

分析服务的需求,不要受现有数据库设计的限制。迁移微服务是重新审视旧设计决策并尝试改进整个系统的好机会。还要记住十二要素应用原则(12factor.net/)来改进设计。

在设计 API 之前,最好先简要回顾一下 REST,这样您可以查看restfulapi.net/进行复习。

指定 API 端点

我们的 API 接口将如下:

端点 需要身份验证 返回
GET /api/me/thoughts/ 用户的思想列表
POST /api/me/thoughts/ 新创建的思想
GET /api/thoughts/ 所有思想的列表
GET /api/thoughts/X/ ID 为X的思想
GET /api/thoughts/?search=X 搜索包含X的所有思想
DELETE /admin/thoughts/X/ 删除 ID 为X的思想

请注意 API 有两个元素:

  • 一个公共 API,以/api开头:

  • 一个经过身份验证的公共 API,以/api/me开头。用户需要经过身份验证才能执行这些操作。未经身份验证的请求将返回 401 未经授权状态码。

  • 一个非经过身份验证的公共 API,以/api开头。任何用户,即使没有经过身份验证,也可以执行这些操作。

  • 一个管理员 API(以/admin开头)。这不会公开。它省去了身份验证,并允许您执行不是为客户设计的操作。明确地使用前缀标记有助于审计操作,并清楚地表明它们不应该在数据中心之外可用。

思想的格式如下:

thought
{
    id integer
    username string
    text string
    timestamp string($date-time)
}

要创建一个,只需要发送文本。时间戳会自动设置,ID 会自动创建,用户名会被身份验证数据检测到。

由于这只是一个示例,这个 API 被设计为最小化。特别是,可以创建更多的管理员端点来有效地模拟用户并允许管理员操作。DELETE操作是第一个包括的操作,用于清理测试。

最后一个细节:关于是否最好以斜杠结尾 URI 资源存在一些争论。然而,在使用 Flask 时,用斜杠定义它们将返回一个重定向状态码,308 PERMANENT_REDIRECT,对于没有正确结尾的请求。无论如何,尽量保持一致以避免混淆。

定义数据库模式

数据库模式简单,继承自单体。我们只关心存储在thought_model表中的想法,因此数据库结构如下:

字段 类型 注释
id INTEGER NOT NULL 主键
username VARCHAR(50)
text VARCHAR(250)
timestamp DATETIME 创建时间

thought_model 表

这个表在thoughts_backend/models.py文件中以 SQLAlchemy 格式表示,代码如下:

class ThoughtModel(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50))
    text = db.Column(db.String(250))
    timestamp = db.Column(db.DateTime, server_default=func.now())

SQLAlchemy 能够为测试目的或开发模式创建表。在本章中,我们将数据库定义为 SQLite,它将数据存储在db.sqlite3文件中。

使用 SQLAlchemy

SQLAlchemy (www.sqlalchemy.org/)是一个强大的 Python 模块,用于处理 SQL 数据库。处理高级语言(如 Python)的数据库有两种方法。一种是保持低级方法,使用原始 SQL 语句,检索数据库中的数据。另一种是使用对象关系映射器ORM)来抽象数据库,并在不涉及实现细节的情况下使用接口。

第一种方法由 Python 数据库 API 规范(PEP 249—www.python.org/dev/peps/pep-0249/)很好地代表,所有主要数据库都遵循这一规范,比如psycopg2 (initd.org/psycopg/)用于 PostgreSQL。这主要创建 SQL 字符串命令,执行它们,然后解析结果。这使我们能够定制每个查询,但对于重复的常见操作来说并不是很高效。PonyORM (ponyorm.org/)是另一个例子,它不那么低级,但仍旨在复制 SQL 语法和结构。

对于第二种方法,最著名的例子可能是 Django ORM (docs.djangoproject.com/en/2.2/topics/db/)。它使用定义的模型 Python 对象来抽象数据库访问。对于常见操作,它的工作效果非常好,但它的模型假设数据库的定义是在我们的 Python 代码中完成的,映射遗留数据库可能非常痛苦。ORM 创建的一些复杂 SQL 操作可能需要很长时间,而定制的查询可以节省大量时间。工具使我们与最终结果的关系如此之远,甚至可能不自觉地执行缓慢的查询。

SQLAlchemy (www.sqlalchemy.org/)非常灵活,可以在两端工作。它不像 Django ORM 那样直截了当或易于使用,但它允许我们将现有的数据库映射到 ORM。这就是为什么我们会在我们的示例中使用它:它可以接受现有的、复杂的遗留数据库并进行映射,使您可以轻松执行简单的操作和以您想要的方式执行复杂的操作。

请记住,我们将在本书中使用的操作非常简单,SQLAlchemy 在这些任务中不会特别突出。但是,如果您计划从通过手动编写 SQL 语句访问数据库的旧单体迁移到新创建的微服务,那么 SQLAlchemy 是一个无价的工具。如果您已经处理了一个复杂的数据库,花一些时间学习如何使用 SQLAlchemy 将是非常宝贵的。一个精心设计的 SQLAlchemy 定义可以非常高效地执行一些抽象任务,但它需要对工具有很好的了解。

Flask-SQLAlchemy 的文档(flask-sqlalchemy.palletsprojects.com/en/2.x/)是一个很好的起点,因为它总结了主要操作,而主要的 SQLAlchemy 文档一开始可能会让人感到不知所措。

在我们定义模型之后,我们可以通过模型中的query属性执行查询,并相应地进行过滤:

# Retrieve a single thought by its primary key
thought = ThoughtModel.query.get(thought_id)
# Retrieve all thoughts filtered by a username
thoughts = ThoughtModel.query.filter_by(username=username)
.order_by('id').all()

存储和删除一行需要使用会话,然后提交它:

# Create a new thought
new_thought = ThoughtModel(username=username, text=text, timestamp=datetime.utcnow())
db.session.add(new_thought)
db.session.commit()

# Retrieve and delete a thought
thought = ThoughtModel.query.get(thought_id)
db.session.delete(thought)
db.session.commit()

要查看如何配置数据库访问,请查看thoughts_backend/db.py文件。

实施服务

为了实现这个微服务,我们将使用 Flask-RESTPlus(flask-restplus.readthedocs.io/en/stable/)。这是一个 Flask(palletsprojects.com/p/flask/)的扩展。Flask 是一个著名的 Python 微框架,特别擅长实现微服务,因为它小巧、易于使用,并且与 Web 应用程序的常规技术栈兼容,因为它使用Web 服务器网关接口WSGI)协议。

介绍 Flask-RESTPlus

Flask 能够实现 RESTful 接口,但 Flask-RESTPlus 添加了一些非常有趣的功能,可以支持良好的开发实践和快速开发:

  • 它定义了命名空间,这是创建前缀和结构化代码的一种方式。这有助于长期维护,并在创建新的端点时有助于设计。

如果在单个命名空间中有超过 10 个端点,那么现在可能是考虑分割它的好时机。使用一个文件一个命名空间,并允许文件大小提示何时是一个尝试进行分割的好时机。

  • 它有一个完整的解决方案来解析输入参数。这意味着我们有一种简单的方法来处理需要多个参数并验证它们的端点。使用请求解析flask-restplus.readthedocs.io/en/stable/parsing.html)模块类似于使用 Python 标准库中包含的argparse命令行模块(docs.python.org/3/library/argparse.html)。它允许在请求体、标头、查询字符串甚至 cookie 的参数中定义参数。

  • 同样,它还有一个用于生成对象的序列化框架。Flask-RESTful 称之为响应编组flask-restplus.readthedocs.io/en/stable/marshalling.html)。这有助于定义可以重复使用的对象,澄清接口并简化开发。如果启用,它还允许字段掩码,返回部分对象。

  • 它具有完整的 Swagger API 文档支持。Swagger(swagger.io/)是一个开源项目,用于帮助设计、实现、文档化和测试 RESTful API Web 服务,遵循标准的 OpenAPI 规范。Flask-RESTPlus 自动生成了 Swagger 规范和自我记录页面:

Thoughts Backend API 的主要 Swagger 文档页面,自动生成

Flask 的其他好元素源自它是一个受欢迎的项目,并且有很多支持的工具:

  • 我们将使用 SQLAlchemy 的连接器 Flask-SQLAlchemy(flask-sqlalchemy.palletsprojects.com/en/2.x/)。它的文档涵盖了大多数常见情况,而 SQLAlchemy 的文档更详细,可能有点令人不知所措。

  • 要运行测试,pytest-flask模块(pytest-flask.readthedocs.io/en/latest/)创建了一些准备与 Flask 应用程序一起工作的固定装置。我们将在测试代码部分更多地谈论这个。

处理资源

典型的 RESTful 应用程序具有以下一般结构:

  1. 一个由 URL 定义的资源。这个资源允许通过 HTTP 方法(GETPOST等)执行一个或多个操作。

  2. 每次调用这些操作时,框架都会路由请求,直到定义的代码执行操作。

  3. 如果有任何输入参数,它们将首先需要进行验证。

  4. 执行操作并获得结果值。此操作通常涉及对数据库的一个或多个调用,这将以模型的形式完成。

  5. 准备结果值并以客户端理解的方式进行编码,通常是 JSON 格式。

  6. 将编码值返回给客户端,并附上适当的状态码。

大多数这些操作都是由框架完成的。需要进行一些配置工作,但这就是我们的 Web 框架,例如在这个例子中的 Flask-RESTPlus,将提供最大的帮助。特别是除了步骤 4之外,其他都将大大简化。

让我们来看一个简单的代码示例(在 GitHub 上可用)来描述它:

api_namespace = Namespace('api', description='API operations')

@api_namespace.route('/thoughts/<int:thought_id>/')
class ThoughtsRetrieve(Resource):

    @api_namespace.doc('retrieve_thought')
    @api_namespace.marshal_with(thought_model)
    def get(self, thought_id):
        '''
        Retrieve a thought
        '''
        thought = ThoughtModel.query.get(thought_id)
        if not thought:
            # The thought is not present
            return '', http.client.NOT_FOUND

        return thought

这实现了GET /api/thoughts/X/操作,通过 ID 检索单个想法。

让我们分析每个元素。请注意,行是按主题分组的。

  1. 首先,我们通过其 URL 定义资源。请注意,api_namespace设置了 URL 的api前缀,这将验证参数X是一个整数:
api_namespace = Namespace('api', description='API operations')

@api_namespace.route('/thoughts/<int:thought_id>/')
class ThoughtsRetrieve(Resource):
    ...
  1. 该类允许您对同一资源执行多个操作。在这种情况下,我们只执行一个:GET操作。

  2. 请注意,编码在 URL 中的thought_id参数作为参数传递给该方法:

class ThoughtsRetrieve(Resource):

    def get(self, thought_id):
        ...
  1. 现在我们可以执行该操作,这是在数据库中搜索以检索单个对象。调用ThoughModel来搜索指定的想法。如果找到,将以http.client.OK (200)状态代码返回。如果未找到,则返回空结果和http.client.NOT_FOUND 404状态代码:
def get(self, thought_id):
    thought = ThoughtModel.query.get(thought_id)
    if not thought:
        # The thought is not present
        return '', http.client.NOT_FOUND

    return thought
  1. 返回thought对象。marshal_with装饰器描述了 Python 对象应如何序列化为 JSON 结构。稍后我们将看到如何配置它:
@api_namespace.marshal_with(thought_model)
def get(self, thought_id):
    ...
    return thought
  1. 最后,我们有一些文档,包括由自动生成的 Swagger API 呈现的文档字符串:
class ThoughtsRetrieve(Resource):

    @api_namespace.doc('retrieve_thought')
    def get(self, thought_id):
        '''
        Retrieve a thought
        '''
        ...

正如您所看到的,大多数操作都是通过 Flask-RESTPlus 配置和执行的,作为开发人员的主要工作是肉体的步骤 4。但是还有一些工作要做,例如配置预期的输入参数并验证它们,以及如何将返回的对象序列化为适当的 JSON。我们将看到 Flask-RESTPlus 如何帮助我们。

解析输入参数

输入参数可以采用不同的形式。当我们谈论输入参数时,主要谈论两种类型:

  • 字符串查询参数编码到 URL 中。这些通常用于GET请求,看起来像下面这样:
http://test.com/some/path?param1=X&param2=Y

它们是 URL 的一部分,并将存储在沿途的任何日志中。参数被编码为它们自己的格式,称为URL 编码www.urlencoder.io/learn/)。您可能已经注意到,例如,空格会被转换为%20

通常,我们不需要手动解码查询参数,因为诸如 Flask 之类的框架会为我们完成,但是 Python 标准库具有用于执行此操作的实用程序(docs.python.org/3/library/urllib.parse.html)。

  • 让我们来看一下 HTTP 请求的主体。这通常用于POSTPUT请求。可以使用Content-Type头指定特定格式。默认情况下,Content-Type头被定义为application/x-www-form-urlencoded,它以 URL 编码的方式进行编码。在现代应用程序中,这被替换为application/json以将其编码为 JSON。

请求的主体不会存储在日志中。期望是GET请求多次调用时产生相同的结果,这意味着它们是幂等的。因此,它可以被一些代理或其他元素缓存。这就是为什么在再次发送POST请求之前,您的浏览器会要求确认,因为此操作可能会产生不同的结果。

但还有另外两个地方可以传递参数:

  • 作为 URL 的一部分:像thought id这样的东西是参数。尽量遵循 RESTful 原则,并将 URL 定义为资源,以避免混淆。查询参数最好留作可选项。

  • 标头:通常,标头提供有关元数据的信息,例如请求的格式、预期的格式或身份验证数据。但它们也需要被视为输入参数。

所有这些元素都会被 Flask-RESTPlus 自动解码,因此我们不需要处理编码和低级访问。

让我们看看这在我们的例子中是如何工作的。这段代码是从 GitHub 中提取的,并缩短以描述解析参数:

authentication_parser = api_namespace.parser()
authentication_parser.add_argument('Authorization', 
location='headers', type=str, help='Bearer Access 
Token')

thought_parser = authentication_parser.copy()
thought_parser.add_argument('text', type=str, required=True, help='Text of the thought')

@api_namespace.route('/me/thoughts/')
class MeThoughtListCreate(Resource):

    @api_namespace.expect(thought_parser)
    def post(self):
        args = thought_parser.parse_args()
        username = authentication_header_parser(args['Authorization'])
        text=args['text']
        ...

我们在下面的行中定义了一个解析器:

authentication_parser = api_namespace.parser()
authentication_parser.add_argument('Authorization', 
location='headers', type=str, help='Bearer Access Token')

thought_parser = authentication_parser.copy()
thought_parser.add_argument('text', type=str, required=True, help='Text of the thought')

authentication_parserthought_parser继承,以扩展功能并结合两者。每个参数都根据类型和是否需要来定义。如果缺少必需的参数或其他元素不正确,Flask-RESTPlus 将引发400 BAD_REQUEST错误,并提供有关出了什么问题的反馈。

因为我们想以稍微不同的方式处理身份验证,我们将其标记为不需要,并允许它使用默认值(由框架创建)None。请注意,我们指定Authorization参数应该在标头中。

post方法得到一个装饰器,表明它期望thought_parser参数,并且我们用parse_args解析它:

@api_namespace.route('/me/thoughts/')
class MeThoughtListCreate(Resource):

    @api_namespace.expect(thought_parser)
    def post(self):
        args = thought_parser.parse_args()
        ...

此外,args现在是一个带有所有参数正确解析并在下一行中使用的字典。

在身份验证标头的特定情况下,有一个特定的函数来处理它,并且通过使用abort返回401 UNAUTHORIZED状态码。这个调用立即停止了一个请求:

def authentication_header_parser(value):
    username = validate_token_header(value, config.PUBLIC_KEY)
    if username is None:
        abort(401)
    return username

class MeThoughtListCreate(Resource):

    @api_namespace.expect(thought_parser)
    def post(self):
       args = thought_parser.parse_args()
       username = authentication_header_parser(args['Authentication'])
       ...

我们暂时不考虑要执行的操作(将新的想法存储在数据库中),而是专注于其他框架配置,将结果序列化为 JSON 对象。

序列化结果

我们需要返回我们的结果。最简单的方法是通过定义 JSON 结果的形状来实现,通过一个序列化器或编组模型(flask-restplus.readthedocs.io/en/stable/marshalling.html)。

序列化器模型被定义为一个带有预期字段和字段类型的字典:

from flask_restplus import fields

model = {
    'id': fields.Integer(),
    'username': fields.String(),
    'text': fields.String(),
    'timestamp': fields.DateTime(),
}
thought_model = api_namespace.model('Thought', model)

该模型将接受一个 Python 对象,并将每个属性转换为相应的 JSON 元素,如字段中所定义的那样:

@api_namespace.route('/me/thoughts/')
class MeThoughtListCreate(Resource):

    @api_namespace.marshal_with(thought_model)
    def post(self):
        ...
        new_thought = ThoughtModel(...)
        return new_thought

请注意,new_thought是一个ThoughtModel对象,由 SQLAlchemy 检索到。我们将在下面详细介绍它,但现在,可以说它具有模型中定义的所有属性:idusernametexttimestamp

内存对象中不存在的任何属性默认值为None。您可以将此默认值更改为将返回的值。您可以指定一个函数,因此在生成响应时将调用它来检索值。这是向对象添加动态信息的一种方式:

model = {
    'timestamp': fields.DateTime(default=datetime.utcnow),
}

您还可以添加要序列化的属性的名称,以防它与预期的结果不同,或者添加一个将被调用以检索值的lambda函数:

model = {
    'thought_text': fields.String(attribute='text'),
    'thought_username': fields.String(attribute=lambda x: x.username),
 }

对于更复杂的对象,你可以像这样嵌套值。请注意,这从文档的角度定义了两个模型,并且每个Nested元素都创建了一个新的作用域。你也可以使用List来添加多个相同类型的实例:

extra = {
   'info': fields.String(),
}
extra_info = api_namespace.model('ExtraInfo', extra)

model = {
    'extra': fields.Nested(extra),
    'extra_list': fields.List(fields.Nested(extra)),
 }

一些可用字段有更多的选项,比如DateTime字段的日期格式。查看完整的字段文档(flask-restplus.readthedocs.io/en/stable/api.html#models)以获取更多详细信息。

如果返回一个元素列表,在marshal_with装饰器中添加as_list=True参数:

@api_namespace.route('/me/thoughts/')
class MeThoughtListCreate(Resource):

    @api_namespace.marshal_with(thought_model, as_list=True)
    def get(self):
        ...
        thoughts = (
            ThoughtModel.query.filter(
                ThoughtModel.username == username
            )
            .order_by('id').all()
        )
        return thoughts

marshal_with装饰器将把result对象从 Python 对象转换为相应的 JSON 数据对象。

默认情况下,它将返回http.client.OK (200)状态码,但我们可以返回不同的状态码,返回两个值:第一个是要marshal的对象,第二个是状态码。marshal_with装饰器中的代码参数用于文档目的。请注意,在这种情况下,我们需要添加特定的marshal调用:

@api_namespace.route('/me/thoughts/')
class MeThoughtListCreate(Resource):

    @api_namespace.marshal_with(thought_model, 
         code=http.client.CREATED)
    def post(self):
        ...
        result = api_namespace.marshal(new_thought, thought_model)
        return result, http.client.CREATED

Swagger 文档将显示所有您定义的marshal对象:

Swagger 页面的末尾

Flask-RESTPlus 的一个不便之处是,为了输入和输出相同的对象,它们需要定义两次,因为输入和输出的模块是不同的。这在一些其他 RESTful 框架中并非如此,例如在 Django REST 框架中(www.django-rest-framework.org/)。Flask-RESTPlus 的维护者们意识到了这一点,并且根据他们的说法,他们将集成一个外部模块,可能是marshmallow(marshmallow.readthedocs.io/en/stable/)。如果您愿意,您可以手动集成它,因为 Flask 足够灵活,可以这样做,看看这个示例(marshmallow.readthedocs.io/en/stable/examples.html#quotes-api-flask-sqlalchemy)。

有关更多详细信息,您可以在 Flask-RESTPlus 的完整编组文档中查看flask-restplus.readthedocs.io/en/stable/marshalling.html

执行操作

最后,我们来到了输入数据已经清洁并准备好使用的具体部分,我们知道如何返回结果。这部分可能涉及执行一些数据库查询和组合结果。让我们以以下内容作为示例:

@api_namespace.route('/thoughts/')
class ThoughtList(Resource):

    @api_namespace.doc('list_thoughts')
    @api_namespace.marshal_with(thought_model, as_list=True)
    @api_namespace.expect(search_parser)
    def get(self):
        '''
        Retrieves all the thoughts
        '''
        args = search_parser.parse_args()
        search_param = args['search']
        # Action
        query = ThoughtModel.query
        if search_param:
            query =(query.filter(
                ThoughtModel.text.contains(search_param)))

        query = query.order_by('id')
        thoughts = query.all()
        # Return the result
        return thoughts

您可以在此处看到,在解析参数后,我们使用 SQLAlchemy 检索查询,如果search参数存在,将应用过滤器。我们使用all()获取所有ThoughtModel对象的结果。

返回对象编组(自动将它们编码为 JSON),如我们在marshal_with装饰器中指定的那样。

验证请求

身份验证逻辑封装在thoughts_backend/token_validation.py文件中。其中包含头部的生成和验证。

以下函数生成Bearer令牌:

def encode_token(payload, private_key):
    return jwt.encode(payload, private_key, algorithm='RS256')

def generate_token_header(username, private_key):
    '''
    Generate a token header base on the username. 
    Sign using the private key.
    '''
    payload = {
        'username': username,
        'iat': datetime.utcnow(),
        'exp': datetime.utcnow() + timedelta(days=2),
    }
    token = encode_token(payload, private_key)
    token = token.decode('utf8')
    return f'Bearer {token}'

这将生成一个 JWT 有效负载。它包括username作为自定义值使用,但它还添加了两个标准字段,即exp到期日期和iat令牌生成时间。

然后使用私钥使用 RS256 算法对令牌进行编码,并以正确的格式返回:Bearer <token>

反向操作是从编码的头部获取用户名。这里的代码较长,因为我们应该考虑我们可能收到Authentication头部的不同选项。这个头部直接来自我们的公共 API,所以我们应该期望任何值并编写程序来做好防御准备。

令牌本身的解码很简单,因为jwt.decode操作将执行此操作:

def decode_token(token, public_key):
    return jwt.decode(token, public_key, algoritms='RS256')

但在到达该步骤之前,我们需要获取令牌并验证多种情况下的头部是否有效,因此我们首先检查头部是否为空,以及是否具有正确的格式,提取令牌:

def validate_token_header(header, public_key):
    if not header:
        logger.info('No header')
        return None

    # Retrieve the Bearer token
    parse_result = parse('Bearer {}', header)
    if not parse_result:
        logger.info(f'Wrong format for header "{header}"')
        return None
    token = parse_result[0]

然后,我们解码令牌。如果无法使用公钥解码令牌,则会引发DecodeError。令牌也可能已过期:

    try:
        decoded_token = decode_token(token.encode('utf8'), public_key)
    except jwt.exceptions.DecodeError:
        logger.warning(f'Error decoding header "{header}". '
        'This may be key missmatch or wrong key')
        return None
    except jwt.exceptions.ExpiredSignatureError:
        logger.info(f'Authentication header has expired')
        return None

然后,检查它是否具有预期的expusername参数。如果其中任何一个参数缺失,这意味着令牌在解码后的格式不正确。这可能发生在不同版本中更改代码时。

    # Check expiry is in the token
    if 'exp' not in decoded_token:
        logger.warning('Token does not have expiry (exp)')
        return None

    # Check username is in the token
    if 'username' not in decoded_token:
        logger.warning('Token does not have username')
        return None

    logger.info('Header successfully validated')
    return decoded_token['username']

如果一切顺利,最后返回用户名。

每个可能的问题都以不同的严重程度记录。最常见的情况以信息级别的安全性记录,因为它们并不严重。例如,在令牌解码后出现格式错误可能表明我们的编码过程存在问题。

请注意,我们使用的是私钥/公钥架构,而不是对称密钥架构,用于编码和解码令牌。这意味着解码和编码密钥是不同的。

从技术上讲,这是一个签名/验证,因为它用于生成签名,而不是编码/解码,但这是 JWT 中使用的命名约定。

在我们的微服务结构中,只有签名机构需要私钥。这增加了安全性,因为其他服务中的任何密钥泄漏都无法检索到能够签署 bearer tokens 的密钥。但是,我们需要生成适当的私钥和公钥。

要生成私钥/公钥,请运行以下命令:

$ openssl genrsa -out key.pem 2048
Generating RSA private key, 2048 bit long modulus
.....................+++
.............................+++

然后,要提取公钥,请使用以下命令:

$ openssl rsa -in key.pem -outform PEM -pubout -out key.pub

这将生成两个文件:key.pemkey.pub,其中包含私钥/公钥对。以文本格式读取它们就足以将它们用作编码/解码 JWT 令牌的密钥:

>> with open('private.pem') as fp:
>> ..  private_key = fp.read()

>> generate_token_header('peter', private_key)
'Bearer <token>'

请注意,对于测试,我们生成了一个样本密钥对,作为字符串附加。这些密钥是专门为此用途创建的,不会在其他任何地方使用。请不要在任何地方使用它们,因为它们在 GitHub 上是公开可用的。

请注意,您需要一个非加密的私钥,不受密码保护,因为 JWT 模块不允许您添加密码。不要将生产秘钥存储在未受保护的文件中。在第三章中,使用 Docker 构建、运行和测试您的服务,我们将看到如何使用环境变量注入这个秘钥,在第十一章中,处理系统中的更改、依赖和秘钥,我们将看到如何在生产环境中正确处理秘钥。

测试代码

为了测试我们的应用程序,我们使用了优秀的pytest框架,这是 Python 应用程序的测试运行器的黄金标准。

基本上,pytest有许多插件和附加组件,可用于处理许多情况。我们将使用pytest-flask,它有助于运行 Flask 应用程序的测试。

运行所有测试,只需在命令行中调用pytest

$ pytest
============== test session starts ==============
....
==== 17 passed, 177 warnings in 1.50 seconds =====

请注意,pytest具有许多可用于处理许多测试情况的功能。在处理测试时,运行匹配测试的子集(-k选项)、运行上次失败的测试(--lf)或在第一个失败后停止(-x)等功能非常有用。我强烈建议查看其完整文档(docs.pytest.org/en/latest/)并发现其所有可能性。

还有许多用于使用数据库或框架、报告代码覆盖率、分析、BDD 等的插件和扩展。值得了解一下。

我们配置了基本用法,包括在pytest.ini文件中始终启用标志和在conftest.py中的 fixtures。

定义 pytest fixtures

pytest中使用 fixture 来准备测试应该执行的上下文,准备并在结束时清理它。pytest-flask需要应用 fixture,如文档中所示。该插件生成一个client fixture,我们可以用它来在测试模式下发送请求。我们在thoughts_fixture fixture 中看到了这个 fixture 的使用,它通过 API 生成三个 thoughts,并在我们的测试运行后删除所有内容。

简化后的结构如下:

  1. 生成三个 thoughts。存储其thought_id
@pytest.fixture
def thought_fixture(client):

    thought_ids = []
    for _ in range(3):
        thought = {
            'text': fake.text(240),
        }
        header = token_validation.generate_token_header(fake.name(),
                                                        PRIVATE_KEY)
        headers = {
            'Authorization': header,
        }
        response = client.post('/api/me/thoughts/', data=thought,
                               headers=headers)
        assert http.client.CREATED == response.status_code
        result = response.json
        thought_ids.append(result['id'])
  1. 然后,在测试中添加yield thought_ids
yield thought_ids
  1. 检索所有 thoughts 并逐个删除它们:
# Clean up all thoughts
response = client.get('/api/thoughts/')
thoughts = response.json
for thought in thoughts:
    thought_id = thought['id']
    url = f'/admin/thoughts/{thought_id}/'
    response = client.delete(url)
    assert http.client.NO_CONTENT == response.status_code

请注意,我们使用faker模块生成假姓名和文本。您可以在faker.readthedocs.io/en/stable/查看其完整文档。这是一个生成测试随机值的好方法,避免反复使用test_usertest_text。它还有助于塑造您的测试,通过独立检查输入而不是盲目地复制占位符。

Fixture 也可以测试您的 API。您可以选择更低级的方法,比如在数据库中编写原始信息,但使用您自己定义的 API 是确保您拥有完整和有用接口的好方法。在我们的例子中,我们添加了一个用于删除想法的管理员界面。这在整个 fixture 中都得到了运用,以及为整个和完整的接口创建想法。

这样,我们还使用测试来验证我们可以将我们的微服务作为一个完整的服务使用,而不是欺骗自己以执行常见操作。

还要注意client fixture 的使用,这是由pytest-flask提供的。

理解 test_token_validation.py

这个测试文件测试了token_validation模块的行为。该模块涵盖了认证头的生成和验证,因此对其进行彻底测试非常重要。

这些测试检查了头部是否可以使用正确的密钥进行编码和解码。它还检查了在无效输入方面的所有不同可能性:不同形状的不正确格式,无效的解码密钥或过期的令牌。

为了检查过期的令牌,我们使用了两个模块:freezegun,使测试检索特定的测试时间(github.com/spulec/freezegun),以及delorean,以便轻松解析日期(尽管该模块能够做更多;请查看delorean.readthedocs.io/en/latest/的文档)。这两个模块非常易于使用,非常适合测试目的。

例如,这个测试检查了一个过期的令牌:

@freeze_time('2018-05-17 13:47:34')
def test_invalid_token_header_expired():
    expiry = delorean.parse('2018-05-17 13:47:33').datetime
    payload = {
        'username': 'tonystark',
        'exp': expiry,
    }
    token = token_validation.encode_token(payload, PRIVATE_KEY)
    token = token.decode('utf8')
    header = f'Bearer {token}'
    result = token_validation.validate_token_header(header, PUBLIC_KEY)
    assert None is result

请注意,冻结时间恰好是令牌到期时间后的 1 秒。

用于测试的公钥和私钥在constants.py文件中定义。还有一个额外的独立公钥用于检查如果使用无效的公钥解码令牌会发生什么。

值得再次强调:请不要使用这些密钥。这些密钥仅用于运行测试,并且可以被任何有权访问本书的人使用。

test_thoughts.py

这个文件检查了定义的 API 接口。每个 API 都经过测试,以正确执行操作(创建新的想法,返回用户的想法,检索所有想法,搜索想法,按 ID 检索想法),以及一些错误测试(未经授权的请求来创建和检索用户的想法,或检索不存在的想法)。

在这里,我们再次使用freezegun来确定思想的创建时间,而不是根据测试运行时的时间戳创建它们。

总结

在这一章中,我们看到了如何开发一个 Web 微服务。我们首先按照 REST 原则设计了其 API。然后,我们描述了如何访问数据库的模式,并使用 SQLAlchemy 进行操作。

然后,我们学习了如何使用 Flask-RESTPlus 来实现它。我们学习了如何定义资源映射到 API 端点,如何解析输入值,如何处理操作,然后如何使用序列化模型返回结果。我们描述了认证层的工作原理。

我们包括了测试,并描述了如何使用pytest fixture 来为我们的测试创建初始条件。在下一章中,我们将学习如何将服务容器化,并通过 Docker 运行。

问题

  1. 你能说出 RESTful 应用程序的特点吗?

  2. 使用 Flask-RESTPlus 的优势是什么?

  3. 你知道除了 Flask-RESTPlus 之外的替代框架吗?

  4. 在测试中使用的 Python 软件包名称来修复时间。

  5. 您能描述一下认证流程吗?

  6. 为什么我们选择 SQLAlchemy 作为示例项目的数据库接口?

进一步阅读

关于 RESTful 设计的深入描述,不仅限于 Python,您可以在Hands-On RESTful API Design Patterns and Best Practices中找到更多信息(www.packtpub.com/gb/application-development/hands-restful-api-design-patterns-and-best-practices)。您可以在书籍Flask: Building Python Web Services中了解如何使用 Flask 框架(www.packtpub.com/gb/web-development/flask-building-python-web-services)。

第三章:使用 Docker 构建、运行和测试您的服务

在上一章中设计了一个工作的 RESTful 微服务,本章将介绍如何以Docker 方式使用它,将服务封装到一个自包含的容器中,使其不可变,并且可以独立部署。本章非常明确地描述了服务的依赖关系和使用方式。运行服务的主要方式是作为 Web 服务器运行,但也可以进行其他操作,比如运行单元测试,生成报告等。我们还将看到如何在本地计算机上部署服务进行测试,以及如何通过镜像仓库共享服务。

本章将涵盖以下主题:

  • 使用 Dockerfile 构建您的服务

  • 操作不可变的容器

  • 配置您的服务

  • 在本地部署 Docker 服务

  • 将您的 Docker 镜像推送到远程注册表

在本章结束时,您将了解如何使用 Docker 操作,创建基本服务,构建镜像并运行它。您还将了解如何共享镜像以在另一台计算机上运行。

技术要求

对于本章,您需要安装 Docker,版本为 18.09 或更高版本。请参阅官方文档(docs.docker.com/install/),了解如何在您的平台上进行安装。

如果您在 Linux 上安装 Docker,可能需要配置服务器以允许非 root 访问。请查看文档:docs.docker.com/install/linux/linux-postinstall/

使用以下命令检查版本:

$ docker version
Client: Docker Engine - Community
 Version: 18.09.2
 API version: 1.39
 Go version: go1.10.8
 Git commit: 6247962
 Built: Sun Feb 10 04:12:39 2019
 OS/Arch: darwin/amd64
 Experimental: false

您还需要安装 Docker Compose 版本 1.24.0 或更高版本。请注意,在某些安装中,如 macOS,这是自动为您安装的。请查看 Docker 文档中的安装说明:docs.docker.com/compose/install/

$ docker-compose version
docker-compose version 1.24.0, build 0aa5906
docker-py version: 3.7.2
CPython version: 3.7.3
OpenSSL version: OpenSSL 1.0.2r 26 Feb 2019

代码可以在 GitHub 上找到,位于此目录:github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter03。在第二章中介绍了ThoughtsBackend的副本,使用 Python 创建 REST 服务,但代码略有不同。我们将在本章中看到这些差异。

使用 Dockerfile 构建您的服务

一切都始于一个容器。正如我们在第一章中所说的,迁移-设计、计划和执行,容器是一种标准化的软件包,以标准方式封装的软件包。它们是可以独立运行的软件单元,因为它们是完全自包含的。要创建一个容器,我们需要构建它。

记得我们描述容器为一个被其自己的文件系统包围的进程吗?构建容器会构建这个文件系统。

要使用 Docker 构建容器,我们需要定义其内容。文件系统是通过逐层应用来创建的。每个 Dockerfile,即生成容器的配方,都包含了生成容器的步骤的定义。

例如,让我们创建一个非常简单的 Dockerfile。创建一个名为example.txt的文件,其中包含一些示例文本,另一个名为Dockerfile.simple,内容如下:

# scratch is a special container that is totally empty
FROM scratch
COPY example.txt /example.txt

现在使用以下命令构建它:

$ # docker build -f <dockerfile> --tag <tag> <context>
$   docker build -f Dockerfile.simple --tag simple .
Sending build context to Docker daemon 3.072kB
Step 1/2 : FROM scratch
 --->
Step 2/2 : COPY example.txt /example.txt
 ---> Using cache
 ---> f961aef9f15c
Successfully built f961aef9f15c
Successfully tagged simple:latest

$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
simple latest f961aef9f15c 4 minutes ago 11B

这将创建一个只包含example.txt文件的 Docker 镜像。这并不是很有用,但非常小-只有 11 个字节。这是因为它继承自空容器scratch,然后将example.txt文件复制到容器中的/example.txt位置。

让我们来看看docker build命令。使用-f参数定义 Dockerfile,使用--tag定义结果镜像的标签,使用context参数定义为点(.)。context参数是指在 Dockerfile 中的步骤中查找文件的引用。

该镜像还具有自动分配的镜像 IDf961aef9f15c。这是文件系统内容的哈希值。稍后我们将看到这为什么是相关的。

执行命令

之前的容器并不是很令人兴奋。完全可以从头开始创建自己的容器,但通常情况下,您会寻找一个包含某种 Linux 发行版的基线,以便您可以在容器中执行一些有用的操作。

正如我们在FROM命令中看到的,我们可以从以前的容器开始。我们将在整本书中使用 Alpine Linux(alpinelinux.org/)发行版,尽管还有其他发行版可用,如 Ubuntu 和 CentOS。查看这篇文章sweetcode.io/linux-distributions-optimized-hosting-docker/,了解针对 Docker 容器的发行版。

为什么选择 Alpine Linux?它可以说是 Docker 系统中最受欢迎的发行版,因为它的占用空间非常小,旨在提高安全性。它得到了很好的维护,并定期更新和修补。它还具有完整的软件包管理系统,可以轻松安装大多数常见的 Web 服务工具。基础镜像的大小只有大约 5MB,并包含一个可用的 Linux 操作系统。

在使用时,它有一些怪癖,比如使用自己的包管理,称为apk,但它很容易使用,几乎可以直接替代常见的 Linux 发行版。

以下 Dockerfile 将继承自基础alpine容器,并添加example.txt文件:

FROM alpine

RUN mkdir -p /opt/
COPY example.txt /opt/example.txt

这个容器允许我们运行命令,因为通常的命令行实用程序都包括在内:

$ docker build -f Dockerfile.run --tag container-run .
Sending build context to Docker daemon 4.096kB
Step 1/3 : FROM alpine
 ---> 055936d39205
Step 2/3 : RUN mkdir -p /opt/
 ---> Using cache
 ---> 4f565debb941
Step 3/3 : COPY example.txt /opt/example.txt
 ---> Using cache
 ---> d67a72454d75
Successfully built d67a72454d75
Successfully tagged container-run:latest

$ # docker run <image name> <command> 
$   docker run container-run cat /opt/example.txt
An example file

注意cat /opt/example.txt命令行的执行。这实际上是在容器内部发生的。我们在stdout控制台中打印结果。但是,如果有文件被创建,当容器停止时,该文件不会保存在我们的本地文件系统中,而只保存在容器内部:

$ ls
Dockerfile.run example.txt
$ docker run container-run /bin/sh -c 'cat /opt/example.txt > out.txt'
$ ls
Dockerfile.run example.txt

文件实际上是保存在一个已停止的容器中。一旦容器完成运行,它将被 Docker 保持停止状态,直到被移除。您可以使用docker ps -a命令查看已停止的容器。尽管已停止的容器并不是很有趣,但它的文件系统已保存在磁盘上。

运行 Web 服务时,正在运行的命令不会停止;它将一直运行,直到停止。记住我们之前说过的,容器是一个附加了文件系统的进程。正在运行的命令是容器的关键。

您可以通过添加以下内容来添加默认命令,当没有给出命令时将执行该命令:

CMD cat /opt/example.txt

使用以下命令使其自动运行:

$ docker run container-run
An example file

定义标准命令使容器变得非常简单。只需运行它,它将执行其配置的任何操作。记得在您的容器中包含一个默认命令。

我们还可以在容器中执行 shell 并与其交互。记得添加-it标志以保持连接正常打开,-i保持stdin打开,-t创建伪终端,您可以将其记住为交互式终端:

$ docker run -it container-run /bin/sh
/ # cd opt/
/opt # ls
example.txt
/opt # cat example.txt
An example file
/opt # exit
$

在发现问题或执行探索性测试时非常有用。

了解 Docker 缓存

构建镜像时,构建图层的工作原理是构建镜像时的一个主要困惑点。

Dockerfile 上的每个命令都是按顺序执行的,并在前一个图层的基础上执行。如果您熟悉 Git,您会注意到这个过程是类似的。每个图层只存储对上一步的更改:

这使得 Docker 可以进行非常积极的缓存,因为任何更改之前的层已经计算过了。例如,在这个例子中,我们使用apk update更新可用的软件包,然后安装python3软件包,然后复制example.txt文件。对example.txt文件的任何更改只会在层be086a75fe23上执行最后两个步骤。这加快了镜像的重建速度。

这也意味着您需要仔细构建您的 Dockerfile,以免使缓存无效。从很少更改的操作开始,比如安装项目依赖,然后进行更频繁更改的操作,比如添加您的代码。我们的示例的带注释的 Dockerfile 有关于缓存使用的指示。

这也意味着,即使层删除了数据,图像的大小也永远不会变小,因为前一个层仍然存储在磁盘上。如果要从一个步骤中删除不需要的数据,需要在同一个步骤中进行。

保持容器的小是非常重要的。在任何 Docker 系统中,倾向于有大量的容器和大量的镜像。没有理由的大图像会很快填满仓库。它们下载和推送都会很慢,并且在您的基础设施中复制容器时也会很慢。

还有另一个实际的考虑。容器是简化和减少服务到最低程度的好工具。通过一点投资,您将获得很好的结果,并保持小而简洁的容器。

有几种保持图像小的做法。除了小心不安装额外的元素之外,主要的做法是创建一个单一的、复杂的层,安装和卸载,以及多阶段图像。多阶段 Dockerfile 是一种引用先前中间层并从中复制数据的方式。查看 Docker 文档(docs.docker.com/develop/develop-images/multistage-build/)。

编译器,特别是倾向于占用大量空间。如果可能的话,尽量使用预编译的二进制文件。您可以使用多阶段 Dockerfile 在一个容器中进行编译,然后将二进制文件复制到正在运行的容器中。

您可以在这篇文章中了解两种策略之间的区别:pythonspeed.com/articles/smaller-python-docker-images/

分析特定图像及其组成层的好工具是divegithub.com/wagoodman/dive)。它还会发现图像可以缩小的方法。

我们将在下一步创建一个多阶段容器。

构建 web 服务容器

我们有一个具体的目标,那就是创建一个能够运行我们的微服务ThoughtsBackend的容器。为此,我们有一些要求:

  • 我们需要将我们的代码复制到容器中。

  • 代码需要通过 web 服务器提供。

因此,大致上,我们需要创建一个带有 web 服务器的容器,添加我们的代码,配置它以运行我们的代码,并在启动容器时提供结果。

我们将把大部分配置文件存储在./docker目录的子目录中。

作为一个 web 服务器,我们将使用 uWSGI(uwsgi-docs.readthedocs.io/en/latest/)。uWSGI 是一个能够通过 WSGI 协议为我们的 Flask 应用提供服务的 web 服务器。uWSGI 非常灵活,有很多选项,并且能够直接提供 HTTP 服务。

一个非常常见的配置是在 uWSGI 前面放置 NGINX 来提供静态文件,因为对于这一点来说更有效率。在我们特定的用例中,我们不提供太多静态文件,因为我们正在运行一个 RESTful API,并且在我们的主要架构中,如第一章中所述,进行移动-设计,计划和执行,前端已经有一个负载均衡器和一个专用的静态文件服务器。这意味着我们不会为了简单起见添加额外的组件。NGINX 通常使用uwsgi协议与 uWSGI 通信,这是专门为 uWSGI 服务器设计的协议,但也可以通过 HTTP 进行通信。请查看 NGINX 和 uWSGI 文档。

让我们来看一下docker/app/Dockerfile文件。它有两个阶段;第一个是编译依赖项:

########
# This image will compile the dependencies
# It will install compilers and other packages, that won't be carried
# over to the runtime image
########
FROM alpine:3.9 AS compile-image

# Add requirements for python and pip
RUN apk add --update python3

RUN mkdir -p /opt/code
WORKDIR /opt/code

# Install dependencies
RUN apk add python3-dev build-base gcc linux-headers postgresql-dev libffi-dev

# Create a virtual environment for all the Python dependencies
RUN python3 -m venv /opt/venv
# Make sure we use the virtualenv:
ENV PATH="/opt/venv/bin:$PATH"
RUN pip3 install --upgrade pip

# Install and compile uwsgi
RUN pip3 install uwsgi==2.0.18
# Install other dependencies
COPY ThoughtsBackend/requirements.txt /opt/
RUN pip3 install -r /opt/requirements.txt

这个阶段执行以下步骤:

  1. 将阶段命名为compile-image,继承自 Alpine。

  2. 安装python3

  3. 安装构建依赖项,包括gcc编译器和 Python 头文件(python3-dev)。

  4. 创建一个新的虚拟环境。我们将在这里安装所有的 Python 依赖项。

  5. 激活虚拟环境。

  6. 安装 uWSGI。这一步从代码中编译它。

您还可以在 Alpine 发行版中安装包含的 uWSGI 包,但我发现编译的包更完整,更容易配置,因为 Alpine 的uwsgi包需要您安装其他包,如uwsgi-python3uwsgi-http等,然后在 uWSGI 配置中启用插件。大小差异很小。这还允许您使用最新的 uWSGI 版本,而不依赖于 Alpine 发行版中的版本。

  1. 复制requirements.txt文件并安装所有依赖项。这将编译并复制依赖项到虚拟环境中。

第二阶段是准备运行容器。让我们来看一下:

########
# This image is the runtime, will copy the dependencies from the other
########
FROM alpine:3.9 AS runtime-image

# Install python
RUN apk add --update python3 curl libffi postgresql-libs

# Copy uWSGI configuration
RUN mkdir -p /opt/uwsgi
ADD docker/app/uwsgi.ini /opt/uwsgi/
ADD docker/app/start_server.sh /opt/uwsgi/

# Create a user to run the service
RUN addgroup -S uwsgi
RUN adduser -H -D -S uwsgi
USER uwsgi

# Copy the venv with compile dependencies from the compile-image
COPY --chown=uwsgi:uwsgi --from=compile-image /opt/venv /opt/venv
# Be sure to activate the venv
ENV PATH="/opt/venv/bin:$PATH"

# Copy the code
COPY --chown=uwsgi:uwsgi ThoughtsBackend/ /opt/code/

# Run parameters
WORKDIR /opt/code
EXPOSE 8000
CMD ["/bin/sh", "/opt/uwsgi/start_server.sh"]

执行以下操作:

  1. 将镜像标记为runtime-image,并像之前一样继承自 Alpine。

  2. 安装 Python 和运行时的其他要求。

请注意,需要安装用于编译的任何运行时。例如,我们在运行时安装了libffilibffi-dev来编译,这是cryptography包所需的。如果不匹配,尝试访问(不存在的)库时会引发运行时错误。dev库通常包含运行时库。

  1. 复制 uWSGI 配置和启动服务的脚本。我们稍后会看一下。

  2. 创建一个用户来运行服务,并使用USER命令将其设置为默认用户。

这一步并不是严格必要的,因为默认情况下会使用 root 用户。由于我们的容器是隔离的,在其中获得 root 访问权限比在真实服务器中更安全。无论如何,最好的做法是不要将我们的面向公众的服务配置为 root 用户,并且这样做会消除一些可以理解的警告。

  1. compile-image镜像中复制虚拟环境。这将安装所有编译的 Python 包。请注意,它们是与运行服务的用户一起复制的,以便访问它们。虚拟环境已激活。

  2. 复制应用程序代码。

  3. 定义运行参数。请注意,端口8000已暴露。这将是我们将在其上提供应用程序的端口。

如果以 root 身份运行,可以定义端口80。在 Docker 中路由端口是微不足道的,除了前端负载均衡器之外,没有真正需要使用默认的 HTTP 端口的理由。不过,可以在所有系统中使用相同的端口,这样可以消除不确定性。

请注意,应用程序代码是在文件末尾复制的。应用程序代码可能是最经常更改的代码,因此这种结构利用了 Docker 缓存,并且只重新创建了最后的几个层,而不是从头开始。在设计 Dockerfile 时,请考虑这一点。

另外,请记住,在开发过程中没有什么能阻止您改变顺序。如果您试图找到依赖关系的问题等,您可以注释掉不相关的层,或者在代码稳定后添加后续步骤。

现在让我们构建我们的容器。请注意,已创建了两个镜像,尽管只有一个被命名。另一个是编译镜像,它更大,因为它包含了编译器等。

$ docker build -f docker/app/Dockerfile --tag thoughts-backend .
...
 ---> 027569681620
Step 12/26 : FROM alpine:3.9 AS runtime-image
...
Successfully built 50efd3830a90
Successfully tagged thoughts-backend:latest
$ docker images | head
REPOSITORY TAG IMAGE ID CREATED SIZE
thoughts-backend latest 50efd3830a90 10 minutes ago 144MB
<none>           <none> 027569681620 12 minutes ago 409MB

现在我们可以运行容器了。为了能够访问内部端口8000,我们需要使用-p选项进行路由:

$ docker run -it  -p 127.0.0.1:8000:8000/tcp thoughts-backend

访问我们的本地浏览器127.0.0.1会显示我们的应用程序。您可以在标准输出中看到访问日志:

您可以使用docker exec从不同的终端访问正在运行的容器,并执行一个新的 shell。记得添加-it以保持终端开启。使用docker ps检查当前正在运行的容器以找到容器 ID:

$ docker ps
CONTAINER ID IMAGE            COMMAND ... PORTS ...
ac2659958a68 thoughts-backend ... ...     127.0.0.1:8000->8000/tcp 
$ docker exec -it ac2659958a68 /bin/sh
/opt/code $ ls
README.md __pycache__ db.sqlite3 init_db.py pytest.ini requirements.txt tests thoughts_backend wsgi.py
/opt/code $ exit
$ 

您可以使用Ctrl + C停止容器,或者更优雅地,从另一个终端停止它:

$ docker ps
CONTAINER ID IMAGE            COMMAND ... PORTS ...
ac2659958a68 thoughts-backend ... ...     127.0.0.1:8000->8000/tcp 
$ docker stop ac2659958a68
ac2659958a68

日志将显示graceful stop

...
spawned uWSGI master process (pid: 6)
spawned uWSGI worker 1 (pid: 7, cores: 1)
spawned uWSGI http 1 (pid: 8)
Caught SIGTERM signal! Sending graceful stop to uWSGI through the master-fifo
Fri May 31 10:29:47 2019 - graceful shutdown triggered...
$ 

正确捕获SIGTERM并优雅地停止我们的服务对于避免服务突然终止很重要。我们将看到如何在 uWSGI 中配置这一点,以及其他元素。

配置 uWSGI

uwsgi.ini文件包含了 uWSGI 的配置:

[uwsgi]
uid=uwsgi
chdir=/opt/code
wsgi-file=wsgi.py
master=True
pidfile=/tmp/uwsgi.pid
http=:8000
vacuum=True
processes=1
max-requests=5000
# Used to send commands to uWSGI
master-fifo=/tmp/uwsgi-fifo

其中大部分信息都是我们从 Dockerfile 中获取的,尽管它需要匹配,以便 uWSGI 知道在哪里找到应用程序代码、启动 WSGI 文件的名称、以及从哪个用户开始等。

其他参数是特定于 uWSGI 行为的:

  • master:创建一个控制其他进程的主进程。作为 uWSGI 操作的推荐选项,因为它创建了更平稳的操作。

  • http:在指定端口提供服务。HTTP 模式创建一个进程,负载均衡 HTTP 请求到工作进程,并建议在容器外提供 HTTP 服务。

  • processes:应用程序工作进程的数量。请注意,在我们的配置中,这实际上意味着三个进程:一个主进程,一个 HTTP 进程和一个工作进程。更多的工作进程可以处理更多的请求,但会使用更多的内存。在生产中,您需要找到适合您的数量,将其与容器的数量平衡。

  • max-requests:在工作进程处理此数量的请求后,回收工作进程(停止并启动新的)。这减少了内存泄漏的可能性。

  • vacuum:在退出时清理环境。

  • master-fifo:创建一个 Unix 管道以向 uWSGI 发送命令。我们将使用这个来处理优雅的停止。

uWSGI 文档(uwsgi-docs.readthedocs.io/en/latest/)非常全面和详尽。它包含了很多有价值的信息,既可以操作 uWSGI 本身,也可以理解关于 Web 服务器操作的细节。我每次阅读它时都会学到一些新东西,但一开始可能会有点压倒性。

值得投入一些时间来运行测试,以发现您的服务在超时、工作进程数量等方面的最佳参数是什么。但是,请记住,uWSGI 的一些选项可能更适合您的容器配置,这简化了事情。

为了允许优雅的停止,我们将 uWSGI 的执行包装在我们的start_server.sh脚本中:

#!/bin/sh

_term() {
  echo "Caught SIGTERM signal! Sending graceful stop to uWSGI through the master-fifo"
  # See details in the uwsgi.ini file and
  # in http://uwsgi-docs.readthedocs.io/en/latest/MasterFIFO.html
  # q means "graceful stop"
  echo q > /tmp/uwsgi-fifo
}

trap _term SIGTERM

uwsgi --ini /opt/uwsgi/uwsgi.ini &

# We need to wait to properly catch the signal, that's why uWSGI is started
# in the background. $! is the PID of uWSGI
wait $!
# The container exits with code 143, which means "exited because SIGTERM"
# 128 + 15 (SIGTERM)
# http://www.tldp.org/LDP/abs/html/exitcodes.html
# http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html

脚本的核心是调用uwsgi来启动服务。然后它会等待直到服务停止。

SIGTERM信号将被捕获,并通过向master-fifo管道发送q命令来优雅地停止 uWSGI。

优雅的停止意味着当有新的容器版本可用时,请求不会被中断。我们稍后会看到如何进行滚动部署,但其中一个关键元素是在现有服务器不提供请求时中断它们,以避免在请求中间停止并留下不一致的状态。

Docker 使用SIGTERM信号来停止容器的执行。超时后,它将使用SIGKILL来杀死它们。

刷新 Docker 命令

我们已经了解了一些重要的 Docker 命令:

  • docker build:构建镜像

  • docker run:运行镜像

  • docker exec:在运行的容器中执行命令

  • docker ps:显示当前正在运行的容器

  • docker images:显示现有的镜像

虽然这些是基本命令,但了解大多数可用的 Docker 命令对于调试问题和执行操作(如监视、复制和标记镜像、创建网络等)非常有用。这些命令还会向您展示 Docker 内部工作的很多内容。

一个重要的命令:一定要定期使用docker system prune清理旧的容器和镜像。在使用几周后,Docker 占用的空间相当大。

Docker 文档(docs.docker.com/v17.12/engine/reference/commandline/docker/)非常完整。一定要熟悉它。

使用不可变容器进行操作

像本章前面看到的 Docker 命令一样,这些命令是一切的基础。但是,当处理多个命令时,开始变得复杂。您已经看到一些命令可能会变得相当长。

要在集群操作中操作容器,我们将使用docker-compose。这是 Docker 自己的编排工具,用于定义多容器操作。它通过一个 YAML 文件定义所有不同的任务和服务,每个都有足够的上下文来构建和运行它。

它允许您在默认情况下的配置文件docker-compose.yaml中存储不同服务和每个服务的参数。这允许您协调它们并生成可复制的服务集群。

测试容器

我们将首先创建一个服务来运行单元测试。请记住,测试需要在容器内部运行。这将标准化它们的执行并确保依赖关系是恒定的。

请注意,在创建容器时,我们包括执行测试所需的所有要求。有选项创建运行容器并从中继承以添加测试和测试依赖项。

这确实创建了一个较小的运行容器,但也创建了一个情况,即测试容器与生产中的容器并不完全相同。如果大小很重要并且存在很大差异,这可能是一个选择,但要注意如果存在细微错误。

我们需要在docker-compose.yaml文件中定义一个服务,如下所示:

version: '3.7'

services:
    # Development related
    test-sqlite:
        environment:
            - PYTHONDONTWRITEBYTECODE=1
        build:
            dockerfile: docker/app/Dockerfile
            context: .
        entrypoint: pytest
        volumes:
            - ./ThoughtsBackend:/opt/code

此部分定义了一个名为test-sqlite的服务。构建定义了要使用的 Dockerfile 和上下文,方式与docker build命令相同。docker-compose会自动设置名称。

我们可以使用以下命令构建容器:

$ docker-compose build test-sqlite
Building test-sqlite
...
Successfully built 8751a4a870d9
Successfully tagged ch3_test-sqlite:latest

entrypoint指定要运行的命令,在本例中通过pytest命令运行测试。

命令和entrypoint之间有一些差异,它们都执行命令。最重要的差异是command更容易被覆盖,而entrypoint会在最后附加任何额外的参数。

要运行容器,请调用run命令:

$ docker-compose run test-sqlite
=================== test session starts ===================
platform linux -- Python 3.6.8, pytest-4.5.0, py-1.8.0, pluggy-0.12.0 -- /opt/venv/bin/python3
cachedir: .pytest_cache
rootdir: /opt/code, inifile: pytest.ini
plugins: flask-0.14.0
collected 17 items

tests/test_thoughts.py::test_create_me_thought PASSED [ 5%]
...
tests/test_token_validation.py::test_valid_token_header PASSED [100%]

========== 17 passed, 177 warnings in 1.25 seconds ============
$ 

您可以附加要传递给内部entrypointpytest参数。例如,要运行与validation字符串匹配的测试,请运行以下命令:

$ docker-compose run test-sqlite -k validation
...
===== 9 passed, 8 deselected, 13 warnings in 0.30 seconds =======
$

还有两个额外的细节:当前代码通过卷挂载,并覆盖容器中的代码。看看如何将./ThoughtsBackend中的当前代码挂载到容器中的代码位置/opt/code。这对开发非常方便,因为它将避免每次更改时都需要重新构建容器。

这也意味着在挂载目录层次结构中的任何写入都将保存在您的本地文件系统中。例如,./ThoughtsBackend/db.sqlite3数据库文件允许您用于测试。它还将存储生成的pyc文件。

db.sqlite3文件的生成可能会在某些操作系统中创建权限问题。如果是这种情况,请删除它以重新生成和/或允许所有用户读写chmod 666 ./ThoughtsBackend/db.sqlite3

这就是为什么我们使用environment选项传递一个PYTHONDONTWRITEBYTECODE=1环境变量。这可以阻止 Python 创建pyc文件。

虽然 SQLite 适用于测试,但我们需要创建一个更好的反映部署的结构,并配置对数据库的访问以能够部署服务器。

创建一个 PostgreSQL 数据库容器

我们需要针对 PostgreSQL 数据库测试我们的代码。这是我们将在生产中部署代码的数据库。

虽然 SQLAlchemy 中的抽象层旨在减少差异,但数据库的行为仍然存在一些差异。

例如,在/thoughts_backend/api_namespace.py中,以下行是不区分大小写的,这是我们想要的行为:

query = (query.filter(ThoughtModel.text.contains(search_param)))

将其翻译成 PostgreSQL,它是区分大小写的,这需要你进行检查。如果在 SQLite 中进行测试并在 PostgreSQL 中运行,这将是一个生产中的错误。

使用ilike替换的代码,以获得预期的行为,如下所示:

param = f'%{search_param}%'

query = (query.filter(ThoughtModel.text.ilike(param)))

我们将旧代码保留在注释中以显示这个问题。

要创建一个数据库容器,我们需要定义相应的 Dockerfile。我们将所有文件存储在docker/db/子目录中。让我们来看看 Dockerfile 及其不同的部分。整个文件可以在 GitHub 上找到(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter03/docker/db/Dockerfile)。

  1. 使用ARG关键字,定义基本的 PostgreSQL 配置,如数据库名称、用户和密码。它们被设置为环境变量,以便 PostgreSQL 命令可以使用它们。

这些命令仅用于本地开发。它们需要与环境设置匹配。ARG关键字在构建时为 Dockerfile 定义了一个参数。我们将看到它们如何在docker-compose.yaml文件中设置为输入参数。

ARG元素也被定义为ENV变量,因此我们将它们定义为环境变量:

# This Dockerfile is for localdev purposes only, so it won't be
# optimised for size
FROM alpine:3.9

# Add the proper env variables for init the db
ARG POSTGRES_DB
ENV POSTGRES_DB $POSTGRES_DB
ARG POSTGRES_USER
ENV POSTGRES_USER $POSTGRES_USER
ARG POSTGRES_PASSWORD
ENV POSTGRES_PASSWORD $POSTGRES_PASSWORD
ARG POSTGRES_PORT
ENV LANG en_US.utf8
EXPOSE $POSTGRES_PORT

# For usage in startup
ENV POSTGRES_HOST localhost
ENV DATABASE_ENGINE POSTGRESQL
# Store the data inside the container, as we don't care for
# persistence
RUN mkdir -p /opt/data
ENV PGDATA /opt/data
  1. 安装postgresql包及其所有依赖项,如 Python 3 及其编译器。我们需要它们来运行应用程序代码:
RUN apk update
RUN apk add bash curl su-exec python3
RUN apk add postgresql postgresql-contrib postgresql-dev
RUN apk add python3-dev build-base linux-headers gcc libffi-dev
  1. 安装并运行postgres-setup.sh脚本:
# Adding our code
WORKDIR /opt/code

RUN mkdir -p /opt/code/db
# Add postgres setup
ADD ./docker/db/postgres-setup.sh /opt/code/db/
RUN /opt/code/db/postgres-setup.sh

这初始化了数据库,设置了正确的用户、密码等。请注意,这并没有为我们的应用程序创建特定的表。

作为我们初始化的一部分,我们在容器内创建数据文件。这意味着数据在容器停止后不会持久保存。这对于测试来说是件好事,但是如果你想要访问数据进行调试,请记住保持容器运行。

  1. 安装我们应用程序的要求和在数据库容器中运行的特定命令:
## Install our code to prepare the DB
ADD ./ThoughtsBackend/requirements.txt /opt/code

RUN pip3 install -r requirements.txt
  1. 复制存储在docker/db中的应用程序代码和数据库命令。运行prepare_db.sh脚本,该脚本创建应用程序数据库结构。在我们的情况下,它设置了thoughts表:
## Need to import all the code, due dependencies to initialize the DB
ADD ./ThoughtsBackend/ /opt/code/
# Add all DB commands
ADD ./docker/db/* /opt/code/db/

## get the db ready
RUN /opt/code/db/prepare_db.sh

该脚本首先在后台启动运行 PostgreSQL 数据库,然后调用init_db.py,最后优雅地停止数据库。

请记住,在 Dockerfile 的每个步骤中,为了访问数据库,它需要在运行,但也会在每个步骤结束时停止。为了避免数据损坏或进程突然终止,确保在最后使用stop_postgres.sh脚本。尽管 PostgreSQL 通常会恢复突然停止的数据库,但这会减慢启动时间。

  1. 要启动数据库运行,CMD 只是postgres命令。它需要以postgres用户身份运行:
# Start the database in normal operation
USER postgres
CMD ["postgres"]

运行数据库服务,我们需要将其设置为docker-compose文件的一部分:

    db:
        build:
            context: .
            dockerfile: ./docker/db/Dockerfile
            args:
                # These values should be in sync with environment
                # for development. If you change them, you'll 
                # need to rebuild the container
                - POSTGRES_DB=thoughts
                - POSTGRES_USER=postgres
                - POSTGRES_PASSWORD=somepassword
                - POSTGRES_PORT=5432
        ports:
            - "5432:5432"

请注意,args参数将在构建期间设置ARG值。我们还将路由 PostgreSQL 端口以允许访问数据库。

现在,您可以构建和启动服务器:

$ docker-compose up build
$ docker-compose up db
Creating ch3_db_1 ... done
Attaching to ch3_db_1
...
db_1 | 2019-06-02 13:55:38.934 UTC [1] LOG: database system is ready to accept connections

在另一个终端中,您可以使用 PostgreSQL 客户端访问数据库。我建议使用 fantastic pgcli。您可以查看其文档(www.pgcli.com/)。

您还可以使用官方的psql客户端或您喜欢的任何其他 PostgreSQL 客户端。默认客户端的文档可以在此处找到:www.postgresql.org/docs/current/app-psql.html

在这里,我们使用PGPASSWORD环境变量来显示密码是先前配置的密码:

$ PGPASSWORD=somepassword pgcli -h localhost -U postgres thoughts
Server: PostgreSQL 11.3
Version: 2.0.2
Chat: https://gitter.im/dbcli/pgcli
Mail: https://groups.google.com/forum/#!forum/pgcli
Home: http://pgcli.com
postgres@localhost:thoughts> select * from thought_model
+------+------------+--------+-------------+
|  id  |  username  |  text  |  timestamp  |
|------+------------+--------+-------------|
+------+------------+--------+-------------+
SELECT 0
Time: 0.016s

能够访问数据库对于调试目的很有用。

配置您的服务

我们可以配置服务使用环境变量来更改行为。对于容器来说,这是使用配置文件的绝佳替代方案,因为它允许不可变的容器注入其配置。这符合十二要素应用程序(12factor.net/config)原则,并允许良好地分离代码和配置,并设置代码可能用于的不同部署。

我们稍后将在使用 Kubernetes 时看到的一个优势是根据需要创建新环境,这些环境可以根据测试目的进行调整,或者专门用于开发或演示。通过注入适当的环境,能够快速更改所有配置,使此操作非常简单和直接。它还允许您根据需要启用或禁用功能,如果正确配置,这有助于在启动日启用功能,而无需代码发布。

这允许配置数据库以连接,因此我们可以在 SQLite 后端或 PostgreSQL 之间进行选择。

系统配置不仅限于开放变量。环境变量将在本书后面用于存储秘密。请注意,秘密需要在容器内可用。

我们将配置测试以访问我们新创建的数据库容器。为此,我们首先需要通过配置选择 SQLite 或 PostgreSQL 的能力。查看./ThoughtsBackend/thoughts_backend/db.py文件:

import os
from pathlib import Path
from flask_sqlalchemy import SQLAlchemy

DATABASE_ENGINE = os.environ.get('DATABASE_ENGINE', 'SQLITE')

if DATABASE_ENGINE == 'SQLITE':
    dir_path = Path(os.path.dirname(os.path.realpath(__file__)))
    path = dir_path / '..'

    # Database initialisation
    FILE_PATH = f'{path}/db.sqlite3'
    DB_URI = 'sqlite+pysqlite:///{file_path}'
    db_config = {
        'SQLALCHEMY_DATABASE_URI': DB_URI.format(file_path=FILE_PATH),
        'SQLALCHEMY_TRACK_MODIFICATIONS': False,
    }

elif DATABASE_ENGINE == 'POSTGRESQL':
    db_params = {
        'host': os.environ['POSTGRES_HOST'],
        'database': os.environ['POSTGRES_DB'],
        'user': os.environ['POSTGRES_USER'],
        'pwd': os.environ['POSTGRES_PASSWORD'],
        'port': os.environ['POSTGRES_PORT'],
    }
    DB_URI = 'postgresql://{user}:{pwd}@{host}:{port}/{database}'
    db_config = {
        'SQLALCHEMY_DATABASE_URI': DB_URI.format(**db_params),
        'SQLALCHEMY_TRACK_MODIFICATIONS': False,
    }

else:
    raise Exception('Incorrect DATABASE_ENGINE')

db = SQLAlchemy()

当使用DATABASE_ENGINE环境变量设置为POSTGRESQL时,它将正确配置。其他环境变量需要正确设置;也就是说,如果数据库引擎设置为 PostgreSQL,则需要设置POSTGRES_HOST变量。

环境变量可以单独存储在docker-compose.yaml文件中,但更方便的是将多个环境变量存储在一个文件中。让我们看一下environment.env

DATABASE_ENGINE=POSTGRESQL
POSTGRES_DB=thoughts
POSTGRES_USER=postgres
POSTGRES_PASSWORD=somepassword
POSTGRES_PORT=5432
POSTGRES_HOST=db

请注意,用户的定义等与为测试创建 Dockerfile 的参数一致。POSTGRES_HOST被定义为db,这是服务的名称。

在为docker-compose创建的 Docker 集群中,您可以通过它们的名称引用服务。这将由内部 DNS 指向适当的容器,作为快捷方式。这允许服务之间轻松通信,因为它们可以通过名称非常容易地配置其访问。请注意,此连接仅在集群内有效,用于容器之间的通信。

我们使用 PostgreSQL 容器的测试服务定义如下:

    test-postgresql:
        env_file: environment.env
        environment:
            - PYTHONDONTWRITEBYTECODE=1
        build:
            dockerfile: docker/app/Dockerfile
            context: .
        entrypoint: pytest
        depends_on:
            - db
        volumes:
            - ./ThoughtsBackend:/opt/code

这与test-sqlite服务非常相似,但它在environment.env中添加了环境配置,并添加了对db的依赖。这意味着如果不存在db服务,docker-compose将启动db服务。

现在可以针对 PostgreSQL 数据库运行测试:

$ docker-compose run test-postgresql
Starting ch3_db_1 ... done
============== test session starts ====================
platform linux -- Python 3.6.8, pytest-4.6.0, py-1.8.0, pluggy-0.12.0 -- /opt/venv/bin/python3
cachedir: .pytest_cache
rootdir: /opt/code, inifile: pytest.ini
plugins: flask-0.14.0
collected 17 items

tests/test_thoughts.py::test_create_me_thought PASSED [ 5%]
...
tests/test_token_validation.py::test_valid_token_header PASSED [100%]

===== 17 passed, 177 warnings in 2.14 seconds ===
$

这个环境文件对于任何需要连接到数据库的服务都很有用,比如在本地部署服务。

本地部署 Docker 服务

有了所有这些元素,我们可以创建服务来本地部署 Thoughts 服务:

     server:
        env_file: environment.env
        image: thoughts_server
        build:
            context: .
            dockerfile: docker/app/Dockerfile
        ports:
            - "8000:8000"
        depends_on:
            - db

我们需要确保添加db数据库服务的依赖关系。我们还绑定了内部端口,以便可以在本地访问它。

我们使用up命令启动服务。uprun命令之间有一些区别,但主要区别在于run用于启动和停止的单个命令,而up设计用于服务。例如,run创建一个交互式终端,显示颜色,up显示标准输出作为日志,包括生成时间,接受-d标志以在后台运行等。通常使用其中一个而不是另一个是可以的,但是up会暴露端口并允许其他容器和服务连接,而run则不会。

现在我们可以使用以下命令启动服务:

$ docker-compose up server
Creating network "ch3_default" with the default driver
Creating ch3_db_1 ... done
Creating ch3_server_1 ... done
Attaching to ch3_server_1
server_1 | [uWSGI] getting INI configuration from /opt/uwsgi/uwsgi.ini
server_1 | *** Starting uWSGI 2.0.18 (64bit) on Sun Jun 2 
...
server_1 | spawned uWSGI master process (pid: 6)
server_1 | spawned uWSGI worker 1 (pid: 7, cores: 1)
server_1 | spawned uWSGI http 1 (pid: 8)

现在在浏览器中访问localhost:8000中的服务:

![

您可以在终端中查看日志。按下Ctrl + C将停止服务器。该服务也可以使用-d标志启动,以分离终端并以守护程序模式运行:

$ docker-compose up -d server
Creating network "ch3_default" with the default driver
Creating ch3_db_1 ... done
Creating ch3_server_1 ... done
$

使用docker-compose ps检查运行的服务、它们的当前状态和打开的端口:

$ docker-compose ps
 Name Command State Ports
------------------------------------------------------------------------------
ch3_db_1 postgres Up 0.0.0.0:5432->5432/tcp
ch3_server_1 /bin/sh /opt/uwsgi/start_s ... Up 0.0.0.0:8000->8000/tcp

正如我们之前所见,我们可以直接访问数据库并在其中运行原始的 SQL 命令。这对于调试问题或进行实验非常有用:

$ PGPASSWORD=somepassword pgcli -h localhost -U postgres thoughts
Server: PostgreSQL 11.3
Version: 2.0.2

postgres@localhost:thoughts> 
INSERT INTO thought_model (username, text, timestamp) 
VALUES ('peterparker', 'A great power carries a great
 responsability', now());

INSERT 0 1
Time: 0.014s
postgres@localhost:thoughts>

现在 Thoughts 通过以下 API 可用:

$ curl http://localhost:8000/api/thoughts/
[{"id": 1, "username": "peterparker", "text": "A great power carries a great responsability", "timestamp": "2019-06-02T19:44:34.384178"}]

如果需要以分离模式查看日志,可以使用docker-compose logs <optional: service>命令:

$ docker-compose logs server
Attaching to ch3_server_1
server_1 | [uWSGI] getting INI configuration from /opt/uwsgi/uwsgi.ini
server_1 | *** Starting uWSGI 2.0.18 (64bit) on [Sun Jun 2 19:44:15 2019] ***
server_1 | compiled with version: 8.3.0 on 02 June 2019 11:00:48
...
server_1 | [pid: 7|app: 0|req: 2/2] 172.27.0.1 () {28 vars in 321 bytes} [Sun Jun 2 19:44:41 2019] GET /api/thoughts/ => generated 138 bytes in 4 msecs (HTTP/1.1 200) 2 headers in 72 bytes (1 switches on core 0)

要完全停止集群,请调用docker-compose down

$ docker-compose down
Stopping ch3_server_1 ... done
Stopping ch3_db_1 ... done
Removing ch3_server_1 ... done
Removing ch3_db_1 ... done
Removing network ch3_default

这将停止所有容器。

将 Docker 镜像推送到远程注册表

我们所见的所有操作都适用于我们的本地 Docker 存储库。鉴于 Docker 镜像的结构以及每个层可以独立工作,它们很容易上传和共享。为此,我们需要使用一个远程存储库或 Docker 术语中的注册表,它将接受推送到它的镜像,并允许从中拉取镜像。

Docker 镜像的结构由每个层组成。只要注册表包含它所依赖的层,每个层都可以独立推送。如果先前的层已经存在,这将节省空间,因为它们只会被存储一次。

从 Docker Hub 获取公共镜像

默认注册表是 Docker Hub。这是默认配置的,它作为公共镜像的主要来源。您可以在hub.docker.com/上自由访问它,并搜索可用的镜像来基于您的镜像:

每个镜像都有关于如何使用它以及可用标签的信息。您不需要单独下载镜像,只需使用镜像的名称或运行docker pull命令。如果没有指定其他注册表,Docker 将自动从 Docker Hub 拉取:

镜像的名称也是我们在 Dockerfiles 中使用的FROM命令。

Docker 是一种分发工具的绝佳方式。现在很常见的是,一个开源工具在 Docker Hub 中有一个官方镜像,可以下载并以独立模式启动,从而标准化访问。

这可以用于快速演示,比如 Ghost(https://hub.docker.com//ghost)(一个博客平台),或者 Redis(https://hub.docker.com//redis)实例作为最小工作量的缓存。尝试在本地运行 Ghost 示例。

使用标签

标签是用来标记同一镜像的不同版本的描述符。有一个镜像alpine:3.9,另一个是alpine:3.8。还有 Python 的官方镜像用于不同的解释器(3.6、3.7、2.7 等),但除了版本之外,解释器可能指的是创建镜像的方式。

例如,这些镜像具有相同的效果。第一个是包含 Python 3.7 解释器的完整镜像:

$ docker run -it python:3.7
Python 3.7.3 (default, May 8 2019, 05:28:42)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

第二个也有一个 Python 3.7 解释器。请注意名称中的slim变化:

$ docker run -it python:3.7-slim
Python 3.7.3 (default, May 8 2019, 05:31:59)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

然而,镜像的大小相当不同:

$ docker images | grep python
python 3.7-slim ca7f9e245002 4 weeks ago 143MB
python 3.7      a4cc999cf2aa 4 weeks ago 929MB

如果没有指定其他标签,任何构建都会自动使用latest标签。

请记住,标签可以被覆盖。这可能会让人感到困惑,因为 Docker 和 Git 的工作方式之间存在一些相似之处,例如 Git 中的“标签”意味着不可更改。Docker 中的标签类似于 Git 中的分支。

单个镜像可以多次打标签,使用不同的标签。例如,latest标签也可以是版本v1.5

$ docker tag thoughts-backend:latest thoughts-backend:v1.5
$ docker images
REPOSITORY       TAG    IMAGE ID     CREATED    SIZE
thoughts-backend latest c7a8499623e7 5 min ago 144MB
thoughts-backend v1.5   c7a8499623e7 5 min ago 144MB

请注意image id是相同的。使用标签允许您标记特定的镜像,以便我们知道它们已经准备部署或赋予它们某种意义。

推送到注册表

一旦我们给我们的镜像打了标签,我们就可以将它推送到共享注册表中,以便其他服务可以使用它。

可以部署自己的 Docker 注册表,但是,除非绝对必要,最好避免使用它。有一些云服务提供商允许您创建自己的注册表,无论是公共的还是私有的,甚至在您自己的私有云网络中。如果您想使您的镜像可用,最好的选择是 Docker Hub,因为它是标准的,而且最容易访问。在本章中,我们将在这里创建一个,但是我们将在本书的后面探索其他选项。

值得再次强调的是:维护自己的 Docker 注册表比使用提供者的注册表要昂贵得多。商业注册表的价格,除非您需要大量的仓库,将在每月几十美元的范围内,而且有来自知名云服务提供商如 AWS、Azure 和 Google Cloud 的选项。

除非您确实需要,否则避免使用自己的注册表。

我们将在 Docker Hub 注册表中创建一个新的仓库。您可以免费创建一个私有仓库,以及任意数量的公共仓库。您需要创建一个新用户,这可能是在下载 Docker 时的情况。

在 Docker 术语中,仓库是一组具有不同标签的镜像;例如,所有thoughts-backend的标签。这与注册表不同,注册表是一个包含多个仓库的服务器。

更不正式地说,通常将注册表称为仓库,将仓库称为镜像,尽管从纯粹的角度来说,镜像是唯一的,可能是一个标签(或者不是)。

然后,您可以按以下方式创建一个新的仓库:

创建仓库后,我们需要相应地给我们的镜像打标签。这意味着它应该包括 Docker Hub 中的用户名以标识仓库。另一种选择是直接使用包含用户名的镜像名称:

$ docker tag thoughts-backend:latest jaimebuelta/thoughts-backend:latest

为了能够访问仓库,我们需要使用我们在 Docker Hub 中的用户名和密码登录 Docker:

$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: jaimebuelta
Password:
Login Succeeded

一旦登录,您就可以推送您的镜像:

$ docker push jaimebuelta/thoughts-backend:latest
The push refers to repository [docker.io/jaimebuelta/thoughts-backend]
1ebb4000a299: Pushed
669047e32cec: Pushed
6f7246363f55: Pushed
ac1d27280799: Pushed
c43bb774a4bb: Pushed
992e49acee35: Pushed
11c1b6dd59b3: Pushed
7113f6aae2a4: Pushed
5275897866cf: Pushed
bcf2f368fe23: Mounted from library/alpine
latest: digest: sha256:f1463646b5a8dec3531842354d643f3d5d62a15cc658ac4a2bdbc2ecaf6bb145 size: 2404

现在你可以分享镜像并从任何地方拉取它,只要本地的 Docker 已经正确登录。当我们部署生产集群时,我们需要确保执行它的 Docker 服务器能够访问注册表并且已经正确登录。

总结

在本章中,我们学习了如何使用 Docker 命令来创建和操作容器。我们学习了大多数常用的 Docker 命令,比如buildrunexecpsimagestagpush

我们看到了如何构建一个 Web 服务容器,包括准备配置文件,如何构建 Dockerfile 的结构,以及如何尽可能地减小我们的镜像。我们还介绍了如何使用docker-compose在本地操作,并通过docker-compose.yaml文件连接运行在集群配置中的不同容器。这包括创建一个允许更接近生产部署的数据库容器,使用相同的工具进行测试。

我们看到了如何使用环境变量来配置我们的服务,并通过docker-compose配置来注入它们,以允许不同的模式,比如测试。

最后,我们分析了如何使用注册表来分享我们的镜像,以及如何充分标记它们并允许将它们从本地开发中移出,准备在部署中使用。

在下一章中,我们将看到如何利用创建的容器和操作来自动运行测试,并让自动化工具为我们做繁重的工作,以确保我们的代码始终是高质量的!

问题

  1. 在 Dockerfile 中,FROM关键字是做什么的?

  2. 你会如何启动一个带有预定义命令的容器?

  3. 为什么在 Dockerfile 中创建一个步骤来删除文件不会使镜像变得更小?

  4. 你能描述一下多阶段 Docker 构建是如何工作的吗?

  5. runexec命令有什么区别?

  6. 在使用runexec命令时,什么时候应该使用-it标志?

  7. 你知道除了 uWSGI 之外还有什么替代方案来提供 Python Web 应用程序吗?

  8. docker-compose用于什么?

  9. 你能描述一下 Docker 标签是什么吗?

  10. 为什么有必要将镜像推送到远程注册表?

进一步阅读

为了进一步了解 Docker 和容器,你可以查看Mastering Docker – Third Edition一书(www.packtpub.com/eu/virtualization-and-cloud/mastering-docker-third-edition)。要调整容器并学习如何使你的应用程序更高效,可以查看* Docker High Performance - Second Edition*一书(www.packtpub.com/eu/networking-and-servers/docker-high-performance-second-edition),其中涵盖了许多分析和发现性能问题的技术。

第四章:创建流水线和工作流程

自动运行的流水线工作流程,通过不同阶段,将及早发现问题,并帮助您的团队以最有效的方式进行协作。

在本章中,我们将遵循持续集成实践,自动运行流水线并在每次更改时确保我们所有的代码都符合高质量标准,并且运行并通过所有测试。我们还将准备一个容器以便投入生产。

我们将看到如何利用 GitHub 和 Travis CI 等工具来创建最小干预的镜像。

在本章中,我们将涵盖以下主题:

  • 理解持续集成实践

  • 配置 Travis CI

  • 配置 GitHub

  • 从 Travis CI 推送 Docker 镜像

在本章结束时,您将了解如何在每次代码更改时自动运行测试,以及如何创建一个安全网,使您能够更快,更高效地开发。

技术要求

您需要一个 GitHub 帐户,并且需要是您为持续集成设置的项目的所有者。我们将在本章中创建一个 Travis CI 帐户。

您可以在 GitHub 的Chapter04子目录中查看本章中提到的完整代码(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter04)。以.travis.yml结尾的文件位于根目录中(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/.travis.yml)。

理解持续集成实践

持续集成(通常缩写为CI)是一系列软件工程实践,确保代码始终处于工作状态。

持续集成这个术语的由来是历史上经常需要频繁集成软件,通常一天多次。这是因为开发人员使用的是本地代码,不一定会自动与其他人的代码结合。如今,使用 Git 等源代码控制版本软件使一些元素自动可用。

持续集成强调始终具有潜在可发布的代码。这使得以非常小的代码增量非常频繁地进行发布成为可能。

更频繁地发布更多的版本实际上会增加每个版本的质量。更多的部署也意味着每个部署都更小,减少了出现大问题的可能性。即使听起来违反直觉,更快的部署与部署质量更高以及更少的生产问题之间存在高度相关性。

这里的目标是能够提高部署速度。但为此,我们需要确保建立一个良好的安全网,自动检查我们正在做的是否安全发布。这就是所有 CI 实践发挥作用的地方。

在设置所有流程和基础设施之后,完全有可能一天多次实施发布(假设代码生成速度足够快)。可能需要一段时间才能达到这一点,但一定要花时间了解流程,并生成所有必要的工具,以确保您在不牺牲稳定性的情况下获得速度。相信我,这是完全可以实现的!

生成自动化构建

CI 的核心要素是生成与源代码控制系统集成的自动化构建。软件构建是一个过程(从源代码开始),执行一系列操作并产生输出。如果项目是用编译语言编写的,输出通常将是编译后的程序。

如果我们想要高质量的软件,那么构建的一部分就是检查生成的代码是否符合代码标准。如果代码不符合这些标准,那么构建将返回一个错误。

描述构建错误的一种常见方式是说构建已损坏。构建可以以不同的方式中断,某些类型的错误可能会在早期停止它(例如在运行测试之前的编译错误),或者我们可以继续检测更多问题(例如运行所有测试以返回所有可能的错误)。

构建中可能包括的一些步骤示例如下:

  • 编译代码。

Python 通常不需要编译,但如果使用 C 扩展(用 C 编写并从 Python 导入的模块:docs.python.org/3/extending/)或诸如 Cython (cython.org/) 这样的工具可能需要编译。

  • 运行单元测试

  • 运行静态代码分析工具

  • 构建一个或多个容器

  • 使用诸如 Safety (pyup.io/safety/)这样的工具检查已知漏洞的依赖项。

  • 生成用于分发的二进制或源代码包。例如,RPM (rpm.org/),Debian 软件包 (www.debian.org/doc/manuals/debian-faq/ch-pkg_basics),等等

  • 运行其他类型的测试

  • 从代码生成报告、图表或其他资产

任何可以自动运行的东西都可以成为构建的一部分。可以随时生成本地构建,即使代码仍在进行中。这对于调试和解决问题非常重要。但自动构建将针对每个单独的提交运行,并不会在任何中间阶段运行。这使得明确检查预期在生产中运行的代码以及仍在进行中的代码非常重要。

请注意,单个提交可能仍然是正在进行的工作,但无论如何都值得提交。也许这是朝着一个功能迈出的一小步,有多人在同一部分代码上工作,或者工作分布在几天之间,代码在一天结束时被推送。无论如何,每个提交都是一个可重现的步骤,可以构建并检查构建是否成功。

对每个提交运行构建可以非常快速地检测问题。如果提交很小,那么很容易找出破坏性的更改。它还可以轻松地撤销破坏构建的更改并返回到已知的工作代码。

了解使用 Docker 进行构建的优势

构建的一个主要传统问题是拥有一个适当的构建环境,其中包含运行完整构建所需的所有依赖项。这可能包括编译器、运行测试的测试框架、任何静态分析工具和软件包管理器。版本不一致也可能导致错误。

正如我们之前所看到的,Docker 是封装软件的绝佳方式。它允许我们创建一个包含我们的代码和所有能够执行所有步骤的工具的镜像。

在上一章中,我们看到了如何在一个构建镜像上运行单元测试的单个命令。镜像本身可以运行自己的单元测试。这样可以抽象测试环境并明确定义它。这里唯一必要的依赖是安装了 Docker。

请记住,单个构建可能会生成多个镜像并使它们协调工作。我们在上一章中看到了如何运行单元测试——通过生成服务镜像和数据库镜像——但还有更多可能的用途。例如,您可以在两个不同的操作系统上运行测试,从每个操作系统创建两个镜像或不同的 Python 解释器版本,并检查测试是否在所有这些版本中都通过。

使用 Docker 镜像可以在所有环境中实现标准化。我们可以在开发环境中本地运行镜像,使用与自动化环境中相同的命令。这样可以简化查找错误和问题,因为它在每次运行构建的地方都创建了相同的环境,包括封装的操作系统。

不要低估这一点。在此之前,一个在运行 Ubuntu 的笔记本电脑上工作的开发人员,想要运行在 CentOS 中部署的代码,需要安装一个虚拟机并按照步骤来创建一个与生产环境类似的环境。但是,本地虚拟机往往会偏离,因为很难保持每个开发人员的本地虚拟机与生产环境中的虚拟机同步;此外,任何自动构建工具也可能有要求,比如不支持在生产中运行的旧版本 CentOS。

更糟糕的是,有时不同的项目安装在同一个虚拟机上,以避免每个项目都有一个虚拟机,这可能会导致兼容性问题。

Docker 大大简化了这个问题,部分原因是它迫使你明确声明依赖关系,并减少了实际运行我们的代码所需的表面。

请注意,我们不一定需要创建一个运行整个构建的单个步骤;它可以是几个 Docker 命令,甚至可以使用不同的镜像。但要求是它们都包含在 Docker 中,这是运行它所需的唯一软件。

使用 Docker 构建的主要产品是 Docker 镜像。我们需要正确地给它们打标签,但只有在构建成功的情况下才需要。

利用流水线的概念

CI 工具有助于澄清构建应该如何进行,并围绕流水线的概念进行工作。流水线是一系列阶段。如果其中任何一个不成功,流水线就会停止。

流水线中的每个阶段都可以产生可以在后续阶段使用的元素,或者作为完整构建的最终产品可用。这些最终元素被称为构件。

让我们看一个流水线的例子:

第一个阶段从源代码控制系统中拉取最新的提交。然后,我们构建所有的容器并运行测试和静态分析。如果一切顺利,我们就会给生成的server容器打标签,并将其推送到注册表中。

这些阶段运行的顺序应该是为了尽快检测问题,以便快速反馈。例如,如果static-analysis阶段比test阶段快得多,将分析阶段放在第一位将使失败的构建更快完成。要注意哪些部分可以更早执行以减少反馈时间。

CI 工具通常允许在流水线中进行大量配置,包括并行运行不同阶段的可能性。为了能够并行运行阶段,它们需要能够并行化,这意味着它们不应该改变相同的元素。

如果所选的 CI 工具允许并行运行阶段,可以将流水线定义如下:

请注意,我们同时构建数据库和测试图像。下一个阶段构建其余的元素,这些元素已经在缓存中可用,因此速度会非常快。测试和静态分析都可以在两个不同的容器中并行运行。

这可能加快复杂的构建速度。

一定要验证所花费的时间是否减少。有些情况下,所花费的时间会非常相似。例如,静态分析可能非常快,或者你运行它的硬件可能不够强大,无法并行构建,使得并行构建和顺序构建所花费的时间非常相似。因此,一定要验证你的假设。

流水线是特定于 Travis CI 工具的脚本描述的。我们稍后会看一个 Travis CI 的例子。

分支、合并和确保清晰的主要构建

何时运行构建?每次推送提交时。但每个结果并不相同。在处理 Git 等源代码控制系统时,我们通常有两种类型的分支:

  • 一个主分支

  • 功能分支

它们实现了特定的功能或错误修复,当准备好时将合并到主分支中,如下图所示:

在这个例子中,我们看到主分支(master)分支到开发feature AFeature A随后被简要介绍。还有一个feature B,因为它还没有准备好,所以尚未合并。有了额外的信息,我们可以知道何时安全地将一个功能分支合并到主分支中:

尚未合并的功能分支出现故障并不是很好,但在进行中的工作中,这是可以预料的。与此同时,主分支的破坏是一个应该尽快修复的事件。如果主分支状态良好,那意味着它有可能发布。

GitHub 有一个模型:拉取请求。我们将配置拉取请求自动检查构建是否通过并避免合并。如果我们强制任何功能分支在合并回来之前也与主分支保持最新,那么主分支最终会非常稳定。

对于在 Git 中处理分支以定义发布,最流行的模型是 Git-flow,在这篇有影响力的博文中定义(nvie.com/posts/a-successful-git-branching-model/)。以下的 CI 实践可以简化一些事情,不涉及诸如发布分支之类的元素。这篇博文是强烈推荐阅读的。

在主分支上有一系列连续成功的构建也对项目的稳定性和质量有很大帮助。如果主分支的破坏非常罕见,那么使用最新的主分支创建新版本的信心就会非常高。

配置 Travis CI

Travis CI (travis-ci.com/) 是一个流行的持续集成服务,可免费用于公共 GitHub 项目。与 GitHub 的集成非常简单,它允许您配置它运行的平台,如 macOS、Linux,甚至 iOS。

Travis CI 与 GitHub 紧密集成,因此您只需要登录 GitHub 即可访问它。我们将看看如何将我们的项目连接到它。

为了清晰起见,本章中的代码将只与 Travis 连接起来。

Travis 的工作方式与其他 CI 工具有些不同,它通过启动一个新的虚拟机创建独立的任务。这意味着任何为上一个阶段创建的构件都需要复制到其他地方,以便在下一个阶段开始时下载。

有时这会让事情变得有点不切实际,一个简单的解决方案是为每个单独的任务构建多次。

配置远程系统,如 Travis CI,有时可能会有点令人沮丧,因为它要求您推送一个提交以进行构建,以查看配置是否正确。此外,它使用一个 YAML 文件进行配置,在语法方面可能有点暴躁。您可能需要尝试几次才能得到稳定的东西,但不要担心。一旦设置好,您只能通过特定的拉取请求来更改它,因为配置文件也受源代码控制。

您还可以检查 Travis CI 配置中的请求,看看.yml文件是否创建了解析错误。

您可以在这里查看完整的 Travis CI 文档:docs.travis-ci.com/

要配置 Travis CI,让我们首先从 GitHub 添加一个存储库。

将存储库添加到 Travis CI

要将存储库添加到 Travis CI,我们需要采取以下步骤:

  1. 第一阶段是转到 Travis CI 网页并使用您的 GitHub 凭据登录。

  2. 然后,您需要授权 Travis 访问 GitHub,通过激活它。

  3. 然后,选择要构建的存储库。

最简单的起点是在github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python中 fork 此书中的示例的存储库。随意操作!

但请记住更改用户名、凭据和注册信息以匹配您自己的信息。

您需要对 GitHub 存储库拥有者权限,然后您就可以开始了!

创建.travis.yml 文件

Travis CI 中的主要元素是创建.travis.yml文件。

请确保将其命名为这样(包括初始点和.yml扩展名),并将其包含在 GitHub 存储库的根目录中。如果不这样做,Travis CI 构建将不会启动。请注意,在示例存储库中,该文件位于根目录中,而不是Chapter04子目录下。

.travis.yml描述了构建及其不同的步骤。构建在一个或多个虚拟机中执行。可以通过指定一般操作系统和具体版本来配置这些虚拟机。默认情况下,它们在 Ubuntu Linux 14.04 Trusty 中运行。您可以在此处找到有关可用操作系统的更多信息:docs.travis-ci.com/user/reference/overview/

使用 Docker 允许我们抽象出大部分操作系统的差异,但我们需要确保我们使用的特定dockerdocker-compose版本是正确的。

我们将开始.travis.yml,确保存在有效的docker-compose版本(1.23.2),使用以下代码:

services:
  - docker

env:
  - DOCKER_COMPOSE_VERSION=1.23.2

before_install:
  - sudo rm /usr/local/bin/docker-compose
  - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
  - chmod +x docker-compose
  - sudo mv docker-compose /usr/local/bin
  - docker --version
  - docker-compose version

before_install块将在所有虚拟机中执行。现在,为了运行测试,我们添加一个script块:

script:
- cd ch4
- docker-compose build db
- docker-compose build static-analysis
- docker-compose build test-postgresql
- docker-compose run test-postgresql
- docker-compose run static-analysis

我们构建所有要使用的镜像,然后运行测试。请注意,使用 PostgreSQL 数据库运行测试需要构建db容器。

关于db容器有一个小细节:Travis 虚拟机不允许我们打开端口5432。因此我们在docker-compose中删除了ports。请注意,这仅仅是为了调试目的而使得 PostgreSQL 在外部可用;在内部,容器可以通过其内部网络相互通信。

我们创建了一个名为db-debug的服务,它是db的副本,但它公开了本地开发的端口。您可以在docker-compose.yaml文件中查看它,网址为github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter04/docker-compose.yaml

这将运行所有测试。将代码推送到存储库后,我们可以看到构建在 Travis CI 中开始:

一旦完成,我们可以通过标记为绿色来确认构建成功。然后可以检查日志以获取更多信息:

现在您可以在日志的末尾看到测试:

这对于检测问题和构建中断非常有用。现在,让我们看看 Travis 中作业的工作方式。

使用 Travis 作业

Travis 将整个构建划分为一系列将依次运行的阶段。在每个阶段,可以有多个作业。同一构建中的所有作业将并行运行。

正如我们之前所见,可以通过用jobs部分替换script部分来配置测试和静态分析并行运行:

jobs:
  include:
    - stage: tests
      name: "Unit Tests"
      script:
      - cd ch4
      - docker-compose build db
      - docker-compose build test-postgresql
      - docker-compose run test-postgresql
    - stage: tests
      name: "Static Analysis"
      script:
      - cd ch4
      - docker-compose build static-analysis
      - docker-compose run static-analysis

这在一个阶段隐式地创建了两个作业。该阶段命名为tests,作业分别称为“单元测试”和“静态分析”。

结果显示在 Travis 页面上:

请注意,在这两种情况下,由于作业是独立的,它们需要构建所需的映像。由于单元测试作业需要构建db映像,这需要几分钟的时间,所以比静态分析慢。

您可以检查每个作业的详细日志。请注意,环境设置和before_install操作在所有作业中都会执行。

这个分工不仅可以极大地加快构建速度,还可以澄清问题所在。一眼就可以看出破坏因素是单元测试还是静态分析。这样可以减少混乱。

发送通知

默认情况下,Travis CI 会发送电子邮件通知构建的结果,但只有在构建失败或修复了破损的构建时才会发送。这样可以避免不断发送“成功”电子邮件,并且只在需要采取行动时才会发送。默认情况下,电子邮件只发送给提交者(如果不同,则发送给提交作者)。

请注意,“失败”构建和“错误”构建之间存在差异。后者是作业设置中的失败,这意味着before_installinstallbefore_script部分存在问题,而失败的构建是因为脚本部分返回了非零结果。在更改 Travis 配置时,错误构建很常见。

Travis 允许我们配置通知电子邮件并连接更多通知系统,包括 Slack、IRC,甚至 OpsGenie,它可以根据值班计划发送短信。在此处查看更多信息的完整文档:docs.travis-ci.com/user/notifications/

配置 GitHub

为了充分利用我们配置的 CI 系统,我们需要确保在将其合并到主分支之前检查构建。为此,我们可以在 GitHub 中将master配置为主分支,并在合并到它之前添加要求:

确保.travis.yaml文件包含适当的凭据,如果您 fork 了存储库。您需要使用自己的更新它们。

  1. 转到我们的 GitHub 存储库中的设置和分支,然后单击添加规则。

  2. 然后,我们启用了要求状态检查通过才能合并选项,并使用来自travis-ci的状态检查:

  1. 我们还选择了在合并之前要求分支是最新的选项。这确保了没有合并到master的分支在之前没有运行过。

看看 GitHub 提供的其他可能性。特别是,强制执行代码审查是明智的,可以在合并之前对代码进行审查并传播知识。

  1. 创建新分支和新的拉取请求,旨在失败静态测试,我们可以看到测试是如何添加到 GitHub 的:

详细链接将带您到 Travis CI 和特定的构建。您还可以查看构建的历史记录:

当构建完成时,GitHub 不会让您合并拉取请求:

有关 Travis CI 中构建页面的详细信息可以在此处找到:

修复问题并推送代码将触发另一个构建。这一次,它将成功,并且拉取请求将成功合并。您可以看到每个提交都有自己的构建信息,无论是正确还是错误:

现在我们可以放心地合并到主分支,确保master分支在运行测试时不会中断。

请注意,拉取请求中有两个构建:一个用于分支,另一个用于拉取请求。默认情况下,Travis CI 有这种配置。如果您强制它在合并之前始终创建拉取请求,那么请求将是多余的,尽管在某些情况下,当分支在创建拉取请求之前被推送时,它可能有所帮助。您可以在 Travis 项目配置中启用或禁用它。

可以配置的另一个有趣的特性是,如果推送了更新的提交,可以自动取消构建。这有助于减少系统中的总构建数量。

在 GitHub 的 Commits 视图中也可以检查构建结果。

从 Travis CI 推送 Docker 图像

在我们的构建创建了一个 Docker 镜像之后,我们需要能够与团队的其他成员共享或部署它。我们将使用 Docker Hub 中的 Docker 注册表,如前一章所述,来推送镜像。

让我们从设置安全变量开始。

设置安全变量

为了能够推送到 Docker 存储库,我们首先需要在 Travis CI 的秘密配置中配置密码,以避免在 GitHub 存储库中提交敏感信息:

值得重申:不要在 GitHub 存储库中提交机密信息。这些技术可以用于任何其他所需的机密。

  1. 使用gem安装travis命令行。这假设你的系统上已经安装了gem(Ruby 1.93 或更高版本)。如果没有,请查看安装说明(github.com/travis-ci/travis.rb#installation):
$ gem install travis
  1. 登录到 Travis:
travis login --pro
  1. 使用 Docker Hub 用户名创建一个安全变量:
$ travis encrypt --com DOCKER_USERNAME="<your user name>"
  1. 你会看到类似以下的输出:
secure: ".... encrypted data ...."
  1. 然后,您需要将加密数据添加到环境变量中,如下所示:
env:
  global:
    - DOCKER_COMPOSE_VERSION=1.23.2
    - secure: ".... encrypted data ...."
  1. 现在,请注意新的global部分,并重复第 3 步,使用 Docker Hub 密码:
$ travis encrypt --com DOCKER_PASSWORD="<password>"
  1. 在第一个之后添加另一个安全变量:
env:
  global:
    - DOCKER_COMPOSE_VERSION=1.23.2
    - secure: ".... encrypted data ...."
    - secure: ".... encrypted data ...."

此操作创建了两个环境变量,在构建期间可用。不用担心——它们不会显示在日志中:

Setting environment variables from .travis.yml
$ export DOCKER_COMPOSE_VERSION=1.23.2
$ export DOCKER_PASSWORD=[secure]
$ export DOCKER_USERNAME=[secure]

现在,我们可以在before_install部分添加适当的登录命令,以便 Docker 服务可以连接并推送图像:

before_install:
  ...
  - echo "Login into Docker Hub"
  - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin

下一阶段是构建和标记结果图像。

标记和推送构建

以下代码将添加一个新的阶段,用于构建、标记和最终将结果推送到 Docker 注册表:

jobs:
  include:
    ...
    - stage: push
      script:
      - cd Chapter04
      - docker-compose build server
      - docker tag thoughts_server:latest <registry>/thoughts-backend:$TRAVIS_BRANCH

这第一部分构建了服务器的最终镜像,并使用分支的名称进行标记。为了部署它,我们将添加一个deploy部分:

- stage: push
  script:
  ...
  - docker tag thoughts_server:latest <registry>/thoughts-backend:$TRAVIS_BRANCH
  deploy:
  - provider: script
    script: docker push <registry>/thoughts-backend:$TRAVIS_BRANCH
    on:
      branch: master 

当分支是master时,deploy部分将执行一个script命令。现在,我们的构建还将生成一个最终镜像并推送它。这将确保我们的注册表中有主分支的最新版本。

我们可以添加更多的deploy条件来推送标签;例如,如果我们创建了一个新的 Git 标签,我们可以推送带有适当标签的结果图像。

请记住,如前一章所述,标签是标记图像为重要的一种方式。通常,这意味着它已准备好在自动测试之外的某些地方使用,例如在部署中。

我们可以在deploy部分添加标签:

      deploy:
      - provider: script
        script: docker push <registry>/thoughts-backend:$TRAVIS_BRANCH
        on:
          branch: master 
      - provider: script
        script: docker push <registry>/thoughts-backend:$TRAVIS_TAG
        on:
          tags: True

请注意,这里我们推送的是主分支或有定义标签的情况,因为这两种情况都不会匹配。

您可以在此处查看完整的部署文档:docs.travis-ci.com/user/deployment。我们已经介绍了script提供程序,这是一种创建自己的命令的方式,但也支持提供程序,如 Heroku、PyPI(用于创建 Python 包的情况)和 AWS S3。

对每次提交进行标记和推送

可以将每个构建的图像推送到注册表,由其 Git SHA 标识。当工作正在进行中可以共享用于演示目的、测试等时,这可能很有用。

为此,我们需要在before_install部分创建一个包含 Git SHA 的环境变量:

before_install:
  ...
  - export GIT_SHA=`git rev-parse --short HEAD`
  - echo "Building commit $GIT_SHA"

然后,push部分添加了图像的标记和推送:

- stage: push
  script:
  - cd Chapter04
  - docker-compose build server
  - docker tag thoughts_server:latest <registry>/thoughts-backend:$GIT_SHA
  - docker push <registry>/thoughts-backend:$GIT_SHA
  - docker tag thoughts_server:latest <registry>/thoughts-backend:$TRAVIS_BRANCH

由于此操作发生在deploy部分之前,因此它将在达到此部分的每次构建中产生。

这种方法将产生大量的标签。根据您的注册表如何管理它们,这可能是昂贵的。请确保这是一个明智的做法。

请记住,这种方法也可以用于其他条件推送。

请注意,注册表需要根据您自己的注册表详细信息进行调整。如果您克隆示例存储库,则后者需要更改。

总结

在本章中,我们介绍了持续集成的实践,并探讨了 Docker 如何帮助实现这些实践。我们还研究了如何设计一个管道,确保我们的代码始终符合高标准,并尽快检测到偏差。在 GitHub 中使用 Git 分支和拉取请求与此相一致,因为我们可以确定代码何时准备合并到主分支并部署。

然后,我们介绍了 Travis CI 作为一个与 GitHub 一起使用的优秀工具,以实现持续集成,并讨论了它的特点。我们学习了如何在 Travis CI 中创建一个管道,从创建.travis.yml文件,配置作业,使构建推送经过验证的 Docker 镜像到我们的 Docker 注册表,以及如何收到通知。

我们描述了如何加快并行运行部分的速度,以及如何将值设置为秘密。我们还配置了 GitHub,以确保 Travis CI 管道在将新代码合并到我们的主分支之前已成功运行。

在下一章中,我们将学习基本的 Kubernetes 操作和概念。

问题

  1. 增加部署数量是否会降低它们的质量?

  2. 描述管道是什么。

  3. 我们如何知道我们的主分支是否可以部署?

  4. Travis CI 的主要配置来源是什么?

  5. Travis CI 何时会默认发送通知电子邮件?

  6. 我们如何避免将一个损坏的分支合并到我们的主分支中?

  7. 为什么我们应该避免在 Git 存储库中存储秘密?

进一步阅读

要了解更多关于持续集成和其他工具的信息,您可以查看《实践持续集成和交付》一书(www.packtpub.com/eu/virtualization-and-cloud/hands-continuous-integration-and-delivery),该书不仅涵盖了 Travis CI,还包括 Jenkins 和 CircleCI 等其他工具。如果您想深入了解 GitHub 及其所有可能性,包括如何有效地协作以及它所支持的不同工作流程,请在《GitHub Essentials》中了解更多信息(www.packtpub.com/eu/web-development/github-essentials-second-edition)。

第三部分:使用多个服务-通过 Kubernetes 操作系统

在上一节中,我们介绍了如何开发和容器化单个微服务,本节介绍编排概念,使多个服务协同工作。本节深入解释了作为 Docker 容器编排器的 Kubernetes,以及最大化其使用的实践方法以及如何将其部署到实际运营中。

本节的第一章介绍了 Kubernetes,并解释了这个工具背后的基本概念,这将贯穿本节的使用。Kubernetes 有其自己的特定术语,最初可能有点令人生畏,所以当有不清楚的地方时,不要害怕回到这一章。它还涵盖了如何安装和操作本地集群。

本节的第二章展示了如何在本地 Kubernetes 集群中安装开发的微服务,使用上一章介绍的概念进行具体操作。它配置了一个完整的集群,其中服务正在运行和协作,同时演示了如何在这种环境中进行开发。

本节的第三章涉及实际操作:如何使用商业云服务(本书中使用 AWS 服务)创建云集群,旨在为公开互联网上的外部客户提供服务。它还涵盖了如何在适当的 HTTPS 端点下保护服务,使用私有 Docker 注册表以及自动扩展集群和确保容器平稳运行的实践方法等高级主题。

本节的第四章介绍了 GitOps 的概念,即使用 Git 存储库来控制集群基础设施,将任何基础设施更改保持在源代码控制下,并允许使用 Git 的常见元素,如拉取请求,来控制和验证基础设施更改是否正确。

本节的第五章描述了单个服务内的软件生命周期,以及如何添加新功能的工作原理,从定义功能到在现有 Kubernetes 集群中实时运行。本章展示了测试和验证新功能的实践方法,以便以自信和高效的方式将其引入实时系统。

本节包括以下章节:

  • 第五章,使用 Kubernetes 协调微服务

  • 第六章,使用 Kubernetes 进行本地开发

  • 第七章,配置和保护生产系统

  • 第八章,使用 GitOps 原则

  • 第九章,管理工作流

第五章:使用 Kubernetes 协调微服务

在本章中,我们将讨论 Kubernetes 背后的基本概念,这是一个允许您管理多个容器并协调它们的工具,从而使已部署在每个容器上的微服务协同工作。

本章将涵盖容器编排器的概念以及特定的 Kubernetes 术语,例如 pod、service、deployment 等之间的区别。我们还将学习如何分析运行中的集群并执行其他常见操作,以便您可以将它们应用于我们的微服务示例。

在本章中,我们将涵盖以下主题:

  • 定义 Kubernetes 编排器

  • 理解不同的 Kubernetes 元素

  • 使用 kubectl 执行基本操作

  • 故障排除运行中的集群

在本章结束时,您将了解 Kubernetes 的基本元素,并能够执行基本操作。您还将学习基本的故障排除技能,以便您可以检测可能的问题。

技术要求

如果您使用的是 macOS 或 Windows,默认的 Docker 桌面安装可以启动本地 Kubernetes 集群。只需确保在 Kubernetes 的首选项中启用了此功能:

对于 Linux,本地安装 Kubernetes 的最简单方法是使用 k3s(k3s.io/)。

k3s 是对 Kubernetes(即 k8s)的一种致敬,但是它是其简化版本。

k3s 是 Kubernetes 的一个简化安装,您可以使用它来运行包含在单个二进制文件中的集群。如果您希望下载并运行它,请查看安装页面(github.com/rancher/k3s/blob/master/README.md)。

为了能够使用运行在 k3s 集群内的 Docker 版本,我们需要使用以下代码:

$ # Install k3s
$ curl -sfL https://get.k3s.io | sh -
$ # Restart k3s in docker mode
$ sudo systemctl edit --full k3s.service
# Replace `ExecStart=/usr/local/bin/k3s` with `ExecStart=/usr/local/bin/k3s server --docker`
$ sudo systemctl daemon-reload
$ sudo systemctl restart k3s
$ sudo systemctl enable k3s
$ # Allow access outside of root to KUBECTL config
$ sudo chmod 644 /etc/rancher/k3s/k3s.yaml
$ # Add your user to the docker group, to be able to run docker commands
$ # You may need to log out and log in again for the group to take effect
$ sudo usermod -a -G docker $USER

确保安装kubectl(k3s 默认安装了一个单独的版本)。安装kubectl的步骤可以在kubernetes.io/docs/tasks/tools/install-kubectl/找到。kubectl命令控制 Kubernetes 操作。

检查上述页面上的说明以添加 Bash 完成,这将允许我们按Tab键完成一些命令。

如果一切安装正确,您应该能够使用以下命令检查运行中的 pod:

$ kubectl get pods --all-namespaces
NAMESPACE NAME                                         READY STATUS  RESTARTS AGE
docker    compose-89fb656cf-cw7bb                      1/1   Running 0        1m
docker    compose-api-64d7d9c945-p98r2                 1/1   Running 0        1m
kube-system etcd-docker-for-desktop                    1/1   Running 0        260d
kube-system kube-apiserver-docker-for-desktop          1/1   Running 0        2m
kube-system kube-controller-manager-docker-for-desktop 1/1   Running 0        2m
kube-system kube-dns-86f4d74b45-cgpsj                  3/3   Running 1        260d
kube-system kube-proxy-rm82n                           1/1   Running 0        2m
kube-system kube-scheduler-docker-for-desktop          1/1   Running 0        2m
kube-system kubernetes-dashboard-7b9c7bc8c9-hzpkj      1/1   Running 1        260d

注意不同的命名空间。它们都是 Kubernetes 自己创建的默认命名空间。

转到以下页面安装 Ingress 控制器:github.com/kubernetes/ingress-nginx/blob/master/docs/deploy/index.md。在 Docker 桌面上,您需要运行以下两个命令:

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/cloud-generic.yaml

这将创建一个带有控制器 pod 的ingress-nginx命名空间。Kubernetes 将使用该 pod 来设置 Ingress 配置。

现在,让我们来看看使用 Kubernetes 的优势。

定义 Kubernetes 编排器

Kubernetes 是一种流行的容器编排工具。它允许我们以协调的方式管理和部署多个相互交互的容器。由于每个微服务都存在于单独的容器中,正如我们在第一章中提到的那样,进行迁移-设计、计划和执行,它们可以协同工作。

要了解更多关于 Kubernetes 的深入介绍,您可以查看由 Scott McCloud 发布的以下漫画:cloud.google.com/kubernetes-engine/kubernetes-comic/

Kubernetes 旨在用于生产系统。它旨在能够控制大规模部署并抽象出大部分基础设施的细节。Kubernetes 集群中的每个元素都是以编程方式配置的,Kubernetes 本身根据可用的容量来管理集群的部署位置。

Kubernetes 可以完全使用配置文件进行配置。这使得在出现完全瘫痪导致所有物理服务器宕机的情况下,可以复制集群成为可能。甚至可以在不同的硬件上进行这样的操作,而传统的部署可能会非常困难。

这个例子假设数据被存储和检索;例如,在备份设备中。显然,这可能很困难——灾难恢复总是如此。然而,它简化了许多如果你希望复制一个集群所需的步骤。

鉴于 Kubernetes 使用容器并且很容易安装它们,有一个庞大的容器生态系统可以为 Kubernetes 本身添加功能。最好的例子可能是 Kubernetes 仪表板(kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/),一个显示 Kubernetes 操作概述的用户界面。它不是默认安装的,但可以以与安装服务相同的方式安装。其他用例的例子包括监控和日志记录。这使得 Kubernetes 非常可扩展。

将 Kubernetes 与 Docker Swarm 进行比较

Kubernetes 并不是唯一可用的编排器。正如我们在第三章中提到的,还有docker-compose。Docker Compose 也可以编排不同的容器并协调它们,但不涉及多台服务器。

Docker 有一个名为 Docker Swarm 的本机扩展,它允许我们使用一组机器来运行docker-compose,同时重用相同的 YAML 文件,但增加了一些细节来描述你希望它们如何运行。

您可以在官方文档中了解更多关于 Docker Swarm 的信息(docs.docker.com/engine/swarm/)。

与 Kubernetes 相比,Docker Swarm 更容易设置,假设您必须管理服务器。随着您扩展 Docker Compose 的功能,您会发现它的学习曲线很低。

另一方面,Kubernetes 更强大和可定制。它有一个更大的社区和更快的创新步伐。它在处理问题方面也更好。最大的问题是设置一个集群,但正如我们将在第七章中看到的,配置和保护生产系统,现在有易于商业部署的方式,我们可以用几分钟的时间创建一个集群,这降低了 Kubernetes 的准入门槛。

这使得 Kubernetes(可以说)在处理从旧系统迁移和展望未来时是更好的解决方案。对于小规模部署,或者如果您需要部署和管理自己的服务器,Docker Swarm 可能是一个有趣的选择。

为了帮助您从使用docker-compose.yaml文件转移到使用等效的 Kubernetes YAML 文件,您可以使用komposegithub.com/kubernetes/kompose)。它可能有助于快速启动一个 Kubernetes 集群,并将docker-compose.yaml文件中描述的服务转换为它们等效的 Kubernetes 元素,但两个系统之间总是存在差异,可能需要进行调整。

让我们从描述 Kubernetes 的特定元素和命名方式开始。

理解不同的 Kubernetes 元素

Kubernetes 有自己的不同元素的命名方式。我们在本书中经常会使用这些命名方式,Kubernetes 文档也在使用它们。了解它们之间的区别很重要,因为其中一些可能是微妙的。

节点

Kubernetes 的主要基础设施元素称为节点。Kubernetes 集群由一个或多个节点组成,这些节点是支持其他元素抽象化的物理机器(或虚拟机器)。

每个节点需要能够与其他节点通信,并且它们都在一个容器运行时中运行 - 通常是 Docker,但它们也可以使用其他系统,比如rktletgithub.com/kubernetes-incubator/rktlet)。

节点之间创建了一个网络,将所有发送到集群的请求路由到适当的节点,因此发送到集群中任何节点的任何请求都将得到充分的回答。Kubernetes 将处理哪个可部署元素应该部署到哪个节点,甚至在节点出现问题或资源问题时重新部署节点或将它们从一个节点移动到另一个节点。

节点不一定需要完全相同,当部署特定元素到特定节点时需要一定程度的控制,但通常情况下它们是相同的。

虽然节点是支持集群的支柱,但 Kubernetes 通过定义期望的结果并让 Kubernetes 决定何处放置元素,并确保内部网络通道的请求被发送到适当的服务,帮助抽象化特定节点。

Kubernetes 控制平面

Kubernetes 控制平面是 Kubernetes 用来正确配置作为 Kubernetes 集群中节点的一部分的一组服务器的所有过程的地方。服务器允许节点相互连接,允许我们监视它们的当前状态,并允许我们根据部署、规模等方面进行必要的更改。

负责注册和进行这些更改的节点称为主节点。可以有多个主节点。

所有这些控制通常在幕后顺利运行。它的网络与其他部分分离,这意味着在这个级别出现问题不会影响集群的当前操作,除了我们无法进行更改。

Kubernetes 对象

Kubernetes 对象是表示部署在集群中的服务状态的抽象。主要涉及运行容器和这些容器的路由,以及持久存储。

让我们从最小到最大来看不同的元素。这个列表并不详尽;查看 Kubernetes 文档以获取更多细节:

  • 容器:一个单独的 Docker 容器。这些是 Kubernetes 的构建块,但它们永远不会单独存在。

  • Pod:在 Kubernetes 中可以部署的基本单元。Pod 是一个或多个容器的集合,通常来自不同的镜像。通常,一个 Pod 只有一个容器,但有时可能有更多的容器是有用的。同一 Pod 中的所有容器共享相同的 IP 地址(Pod IP),这意味着访问localhost端口的容器可能实际上在访问另一个容器。这实际上是与它们通信的推荐方式。

这对你来说一开始可能有点奇怪,但通常,多容器 Pod 将有一个主要容器和执行辅助任务的其他内容,比如导出指标。

  • ConfigMap:这定义了一组可以注入到 Pod 中的键值对,通常作为环境变量或文件。这允许我们在不同定义的 Pod 之间共享配置,例如,使所有容器记录调试信息。请注意,Pod 可以有自己的配置,但 ConfigMaps 是一种方便的方式来共享相同的值,以便它们可用于不同的 Pod。

  • :容器内的文件是临时的,如果容器停止执行,这些文件将丢失。卷是一种持久存储形式,可用于在启动之间保持数据信息并在 pod 中的容器之间共享信息。

作为一个一般原则,尽量减少卷的数量。大多数应用程序本来就应该是无状态的,任何可变数据都应该存储在数据库中。如果同一 pod 中的容器需要通信,最好通过 HTTP 请求进行。请记住,任何不可变数据,例如静态文件,都可以存储在容器镜像中。

  • 部署:这是一个或多个相同 pod 的分组。部署的定义将说明所需的数量,Kubernetes 将根据定义的策略努力实现这一点。单个部署中的 pod 可以部署到不同的节点,并且通常会这样做。如果任何 pod 被删除、完成或出现任何问题,部署将启动另一个,直到达到定义的数量。

  • 作业:作业创建一个或多个预期完成的 pod。虽然部署会假设任何完成的 pod 都是问题,并且会启动另一个,但作业会重试,直到满足适当数量的成功。完成的 pod 不会被删除,这意味着我们可以检查它们的日志。作业是一次性执行。还有定时作业,将在特定时间运行。

  • 服务。由于 pod 被创建和重新创建,并且具有不同的 IP,为了允许服务访问它们,服务需要定义其他元素可以使用的名称来发现它。换句话说,它将请求路由到适当的 pod。通常,服务和部署将相关联,服务使部署可访问,并在所有定义的 pod 之间进行轮询。服务还可以用于为外部服务创建内部名称。

Kubernetes 中的服务解决了分布式系统中的一个旧问题,即服务发现。当集群中的节点需要知道服务的位置时,即使节点发生变化,也会出现这个问题;也就是说,当我们添加或删除节点时,不会改变所有节点的配置设置。

如果创建一个服务,Kubernetes 将自动执行此操作。

  • 入口: 虽然服务是内部的,但入口是外部的。它将任何外部请求路由到适当的服务,以便它们可以提供服务。您可以通过主机名定义不同的入口,这样可以确保集群通过请求的目标主机路由到不同的服务,或者根据其路径托管单个入口。在内部,入口被实现为实现入口控制器的容器,默认情况下是nginx

根据您的 Kubernetes 安装,您可能需要安装默认控制器。要安装默认控制器,请按照github.com/kubernetes/ingress-nginx/blob/master/docs/deploy/index.md上的说明操作。

  • 命名空间:这是虚拟集群的定义。您可以在同一个物理 Kubernetes 集群中定义多个命名空间。在命名空间下定义的每个名称都需要是唯一的,但另一个命名空间可以使用相同的定义。不同命名空间中的对象无法在内部进行通信,但可以在外部进行通信。

使用非常相似的定义生成不同的命名空间可能是有用的,如果您希望为测试、开发或演示概念等目的创建不同的环境。 Kubernetes 的主要优势在于您可以复制整个系统,并利用这一点创建具有细节上的小改变的类似环境,例如环境的新版本。

对象可以在.yaml文件中找到,这些文件可以加载到系统中。单个.yaml文件可以定义多个对象,例如,定义包含容器的 pod 的部署。

以下图表总结了可用的不同对象:

作业和卷不存在,但有两个服务可用:一个指向部署,另一个指向外部服务。外部服务针对内部元素,并且不会向外部公开。

使用 kubectl 执行基本操作

通过使用kubectl,我们可以对所有不同的元素执行操作。我们已经偷偷看了一眼get,以了解可用的元素。

有关更多信息和kubectl中可用的最常见操作的快速概述,请查看kubernetes.io/docs/reference/kubectl/cheatsheet/上的kubectl备忘单。

我们可以使用kubectlcreate一个新元素。例如,要创建和列出命名空间,我们可以使用以下代码:

$ kubectl create namespace example
namespace/example created
$ kubectl get namespaces
NAME        STATUS AGE
default     Active 260d
docker      Active 260d
example     Active 9s
kube-public Active 260d
kube-system Active 260d

我们可以创建各种元素,其中一些我们将在本书中介绍。

定义元素

命名空间是一个特殊情况,因为它不需要任何配置。要创建新元素,需要创建一个描述该元素的 YAML 文件。例如,我们可以使用 Docker Hub 中的官方 NGINX 镜像创建一个新的 pod:

---
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  namespace: example
spec:
  containers:
    - name: nginx
      image: library/nginx:latest

作为最低要求,元素应包含以下内容:

  • 元素的 API 版本。

  • 元素的类型。

  • 元素的名称,以及其命名空间。

  • 包括配置详细信息的spec部分。对于 pod,我们需要添加必要的容器。

YAML 文件有时可能有点反复无常,特别是涉及缩进和语法时。您可以使用诸如 Kubeval(kubeval.instrumenta.dev/)之类的工具来检查文件是否正确,并且在使用文件之前遵循 Kubernetes 良好实践。

我们将此文件保存为example_pod.yml。我们将使用apply命令创建它,并使用以下命令监视其运行情况:

$ kubectl apply -f example_pod.yml
pod/nginx created
$ kubectl get pods -n example
NAME  READY STATUS            RESTARTS AGE
nginx 0/1   ContainerCreating 0        2s
$ kubectl get pods -n example
NAME  READY STATUS  RESTARTS AGE
nginx 1/1   Running 0        51s

注意使用-n参数来确定命名空间。

现在我们可以exec进入容器并在其中运行命令。例如,要检查 NGINX 服务器是否正在运行并提供文件,我们可以使用以下代码:

$ kubectl exec -it nginx -n example /bin/bash
root@nginx:/# apt-get update
...
root@nginx:/# apt-get install -y curl
...
root@nginx:/# curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</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>

pod 可以以两种方式更改。第一种方法是手动运行edit,它会打开您预定义的终端编辑器,以便您可以编辑文件:

$ kubectl edit pod nginx -n example

您将看到带有所有默认参数的 pod。这种更改 pod 的方式对于小型测试很有用,但一般来说,最好更改原始的 YAML 文件,以便您可以跟踪发生的更改。例如,我们可以更改 NGINX,以便我们使用其以前的版本:

---
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  namespace: example
spec:
  containers:
    - name: nginx
      image: library/nginx:1.16

然后,我们可以再次apply这些更改,这将重新启动 pod:

$ kubectl apply -f example_pod.yml
pod/nginx configured
$ kubectl get pods -n example
NAME  READY STATUS  RESTARTS AGE
nginx 1/1   Running 1        36s

获取更多信息

get命令接受更多配置。您可以使用wide输出选项检索更多信息:

$ kubectl get pods -n example -o wide
NAME  READY STATUS  RESTARTS AGE IP        NODE
nginx 1/1   Running 1        30m 10.1.0.11 docker-for-desktop

如果您进行更改并对此更改产生兴趣,可以使用-w参数来监视任何更改。例如,以下代码显示了 pod 的重启结果。此重启是由于对容器图像进行更改而产生的:

$ kubectl get pods -n example -w
NAME  READY STATUS  RESTARTS AGE
nginx 1/1   Running 2        32m
nginx 1/1   Running 3        32m

如果您需要有关特定元素的更多信息,可以使用describe

$ kubectl describe pod nginx -n example
Name: nginx
Namespace: example
Node: docker-for-desktop/192.168.65.3
Start Time: Sun, 23 Jun 2019 20:56:23 +0100
Labels: <none>
Annotations: ...
Status: Running
IP: 10.1.0.11
...
Events:
 Type Reason Age From Message
 ---- ------ ---- ---- -------
 Normal Scheduled 40m default-scheduler Successfully assigned nginx to docker-for-desktop
 ...
 Normal Created 4m43s (x5 over 40m) kubelet, docker-for-desktop Created container
 Normal Started 4m43s (x5 over 40m) kubelet, docker-for-desktop Started container

这返回了大量信息。最有用的信息通常是关于事件的信息,它将返回有关元素的生命周期的信息。

删除元素

delete命令删除一个元素及其下的所有内容:

$ kubectl delete namespace example
namespace "example" deleted
$ kubectl get pods -n example
No resources found.

请注意,有时删除元素将导致其重新创建。这在通过部署创建 pod 时很常见,因为部署将努力使 pod 的数量达到配置的数量。

运行集群故障排除

我们可以用getdescribe命令来排查 Kubernetes 中的问题。

根据我的经验,Kubernetes 运行中最常见的问题是,有时某些 Pod 无法启动。排查步骤如下:

  1. 容器镜像是否正确?下载镜像出现问题将显示ErrImagePull。这可能是由于无法从注册表下载镜像导致的身份验证问题。

  2. CrashLoopBackOff状态表示容器的进程已中断。Pod 将尝试一遍又一遍地重新启动。这通常是由于容器的潜在问题引起的。检查配置是否正确。您可以使用以下命令检查容器的stdout日志:

$ kubectl logs <pod> -n <namespace> -c <container>

确保容器可运行。尝试使用以下命令手动运行它:

$ docker run <image>
  1. Pod 通常不会被外部暴露。这通常是由于暴露它们的服务和/或 Ingress 存在问题。您可以通过使用exec进入另一个容器,然后尝试访问服务和 Pod 的内部 IP,通常使用curl来检测 Pod 在集群内是否响应。

正如我们之前所看到的,curl通常不会默认安装在容器中,因为它们通常只安装了一组最小的工具。不用担心,您可以使用操作系统的软件包管理器安装它,优点是,一旦容器被回收(在正常的 Kubernetes 操作中很快就会发生),它就不会占用任何空间!出于同样的原因,每次需要调试问题时可能都需要安装它。

记住我们讨论过的 Ingress、服务、部署和 Pod 的链条,并从内部向外部查找配置错误的位置。

在排查问题时,请记住,可以通过exec命令访问 Pod 和容器,这将允许我们检查运行中的进程、文件等。这类似于访问物理服务器的终端。您可以使用以下代码来执行此操作:

$ kubectl exec -it <pod> -n <namespace> /bin/sh

要小心,因为 Kubernetes 集群的性质可能需要您检查一个 Pod 中是否有多个容器运行,如果是这样,您可能需要检查特定的容器。

总结

在本章中,我们了解了 Kubernetes 的基本概念,以及如何管理和协调包含我们的微服务的多个容器。

首先,我们介绍了 Kubernetes 的概念以及一些高级优势。然后,我们描述了 Kubernetes 术语中定义集群的不同元素。这既包括物理方面,其中节点是主要的定义元素,也包括抽象方面,如 Pod、部署、服务和 Ingress,这些是我们生成工作集群所需的构建块。

我们描述了kubectl以及我们可以使用的常见操作来定义元素和通过 YAML 文件检索信息。我们还描述了在处理 Kubernetes 集群时可能出现的一些常见问题。

在下一章中,我们将定义在 YAML 文件中可以使用的不同选项,以便生成集群,并学习如何为我们的微服务示例生成 Kubernetes 集群。

问题

  1. 什么是容器编排器?

  2. 在 Kubernetes 中,什么是节点?

  3. Pod 和容器之间有什么区别?

  4. 工作和 Pod 之间有什么区别?

  5. 何时应该添加 Ingress?

  6. 什么是命名空间?

  7. 我们如何在文件中定义一个 Kubernetes 元素?

  8. kubectlgetdescribe命令有什么区别?

  9. CrashLoopBackOff错误表示什么?

进一步阅读

您可以通过阅读《Kubernetes 入门指南-第三版》(www.packtpub.com/eu/virtualization-and-cloud/getting-started-kubernetes-third-edition)和《完整的 Kubernetes 指南》(www.packtpub.com/eu/virtualization-and-cloud/complete-kubernetes-guide)来了解更多关于 Kubernetes 的信息。

第六章:使用 Kubernetes 进行本地开发

在本章中,您将学习如何定义一个集群,部署所有交互式微服务,以及如何在本地进行开发。我们将在前一章介绍的概念基础上进行构建,并描述如何在 Kubernetes 中以实际方式配置整个系统,部署多个微服务,并使其在您自己的本地计算机上作为一个整体运行。

在这里,我们将介绍另外两个微服务:前端和用户后端。它们在第一章中讨论过,在战略规划以打破单体部分。我们将在本章中看到它们需要如何配置才能在 Kubernetes 中工作。这是除了第二章中介绍的 Thoughts 后端,使用 Python 创建 REST 服务,第三章,使用 Docker 构建、运行和测试您的服务,和第四章,创建管道和工作流。我们将讨论如何正确配置它们三个,并添加一些其他选项,以确保它们在部署到生产环境后能够顺利运行。

本章将涵盖以下主题:

  • 实施多个服务

  • 配置服务

  • 在本地部署完整系统

到本章结束时,您将拥有一个在本地工作的 Kubernetes 系统,其中三个微服务已部署并作为一个整体运行。您将了解不同元素的工作原理以及如何配置和调整它们。

技术要求

对于本章,您需要像前一章中描述的那样运行本地 Kubernetes 实例。记得安装 Ingress 控制器。

您可以在 GitHub 存储库中检查我们将在其中使用的完整代码(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter06)。

实施多个服务

在 GitHub 存储库中,您可以找到本章中将使用的三个微服务。它们基于第一章中介绍的单体,并分为三个元素:

  • Thoughts 后端:如前一章所述,它处理了 Thoughts 的存储和搜索。

  • 用户后端:存储用户并允许他们登录。根据身份验证方法的描述,它创建一个可用于对其他系统进行身份验证的令牌。

  • 前端:这来自单体应用,但是不直接访问数据库,而是向用户和 Thoughts 后端发出请求以复制功能。

请注意,尽管我们描述了集群独立提供静态文件的最终阶段,但静态文件仍由前端提供。这是为了简单起见,以避免多余的服务。

上述服务与 Thoughts 后端在第三章中的方式类似进行了 Docker 化,使用 Docker 构建、运行和测试您的服务。让我们看看其他微服务的一些细节。

描述用户后端微服务

用户后端的代码可以在github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter06/users_backend找到。其结构与 Thoughts 后端非常相似,是一个与 PostgreSQL 数据库通信的 Flask-RESTPlus 应用程序。

它有两个端点,如其 Swagger 接口中所示:

端点如下:

端点 输入 返回
POST /api/login {username: <username>, password: <password>} {Authorized: <token header>}
POST /admin/users {username: <username>, password: <password>} <new_user>

admin端点允许您创建新用户,登录 API 返回一个有效的标头,可用于 Thoughts Backend。

用户存储在数据库中,具有以下架构:

字段 格式 注释
id Integer 主键
username String (50) 用户名
password String(50) 密码以明文存储,这是一个坏主意,但简化了示例
creation Datetime 用户创建时间

使用以下代码描述了 SQLAlchemy 模型定义中的此模式:

class UserModel(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50))
    # DO NOT EVER STORE PLAIN PASSWORDS IN DATABASES
    # THIS IS AN EXAMPLE!!!!!
    password = db.Column(db.String(50))
    creation = db.Column(db.DateTime, server_default=func.now())

请注意,创建日期会自动存储。还要注意,我们以明文形式存储密码。这是在生产服务中一个可怕的主意。您可以查看一篇名为如何在数据库中存储密码?的文章(www.geeksforgeeks.org/store-password-database/)以获取有关使用盐种加密密码的一般想法。您可以使用pyscryptgithub.com/ricmoo/pyscrypt)等软件包在 Python 中实现此类结构。

用户brucestephen被添加到db示例中,作为示例数据。

描述前端微服务

前端代码可以在 GitHub 存储库中找到。它基于 Django 单体应用程序(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter01/Monolith)介绍于第一章,进行移动-设计,计划和执行

与单体应用程序的主要区别在于不访问数据库。因此,Django ORM 没有用处。它们被替换为对其他后端的 HTTP 请求。为了发出请求,我们使用了 fantastic requests库。

例如,search.py文件被转换为以下代码,该代码将搜索委托给 Thoughts Backend 微服务。请注意,客户的请求被转换为对GET /api/thoughts端点的内部 API 调用。结果以 JSON 格式解码并呈现在模板中:

import requests

def search(request):
    username = get_username_from_session(request)
    search_param = request.GET.get('search')

    url = settings.THOUGHTS_BACKEND + '/api/thoughts/'
    params = {
        'search': search_param,
    }
    result = requests.get(url, params=params)
    results = result.json()

    context = {
        'thoughts': results,
        'username': username,
    }
    return render(request, 'search.html', context)

单体等效代码可以在存储库的Chapter01子目录中进行比较(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter01/Monolith/mythoughts/thoughts/search.py)。

请注意,我们通过requests库向定义的搜索端点发出get请求,结果以json格式返回并呈现。

THOUGTHS_BACKEND根 URL 来自设置,通常是 Django 的风格。

这个例子很简单,因为没有涉及身份验证。参数从用户界面捕获,然后路由到后端。请求在发送到后端和获取结果后都得到了正确格式化,然后呈现。这是两个微服务共同工作的核心。

一个更有趣的案例是list_thoughtgithub.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter06/frontend/mythoughts/thoughts/thoughts.py#L18)视图。以下代码列出了已登录用户的想法:

def list_thoughts(request):
    username = get_username_from_session(request)
    if not username:
        return redirect('login')

    url = settings.THOUGHTS_BACKEND + '/api/me/thoughts/'
    headers = {
        'Authorization': request.COOKIES.get('session'),
    }
    result = requests.get(url, headers=headers)
    if result.status_code != http.client.OK:
        return redirect('login')

    context = {
        'thoughts': result.json(),
        'username': username,
    }
    return render(request, 'list_thoughts.html', context)

在这里,在做任何事情之前,我们需要检查用户是否已登录。这是在 get_username_from_session 调用中完成的,它返回 usernameNone(如果他们未登录)。如果他们未登录,则返回将被重定向到登录屏幕。

由于此端点需要身份验证,因此我们需要将用户的会话添加到请求的 Authorization 标头中。用户的会话可以从 request.COOKIES 字典中获取。

作为保障,我们需要检查后端返回的状态代码是否正确。对于此调用,任何不是 200(HTTP 调用正确)的结果状态代码都将导致重定向到登录页面。

为了简单和清晰起见,我们的示例服务不处理不同的错误情况。在生产系统中,应该区分错误,其中问题是用户未登录或存在其他类型的用户错误(400 错误),或者后端服务不可用(500 状态码)。

错误处理,如果做得当,是困难的,但值得做好,特别是如果错误帮助用户理解发生了什么。

get_username_from_session 函数封装了对 validate_token_header 的调用,与上一章介绍的相同:

def get_username_from_session(request):
    cookie_session = request.COOKIES.get('session')
    username = validate_token_header(cookie_session,
                                     settings.TOKENS_PUBLIC_KEY)
    if not username:
        return None

    return username

settings 文件包含解码令牌所需的公钥。

在本章中,为简单起见,我们直接将密钥复制到 settings 文件中。这不适用于生产环境。任何秘密都应通过 Kubernetes 环境配置获取。我们将在接下来的章节中看到如何做到这一点。

环境文件需要指定用户后端和 Thoughts 后端的基本 URL,以便能够连接到它们。

连接服务

只有使用 docker-compose 才能测试服务是否协同工作。检查 Users 后端和 Thoughts 后端的 docker-compose.yaml 文件是否在外部公开了不同的端口。

Thoughts 后端公开端口8000,用户后端公开端口8001。这允许前端连接到它们(并公开端口8002)。此图显示了此系统的工作原理:

您可以看到三个服务是如何隔离的,因为 docker-compose 将为它们创建自己的网络以进行连接。两个后端都有自己的容器,充当数据库。

前端服务需要连接到其他服务。服务的 URL 应该添加到 environment.env 文件中,并且应该指示具有计算机 IP 的服务。

内部 IP,如 localhost 或127.0.0.1,不起作用,因为它在容器内部被解释。您可以通过运行 ifconfig 来获取本地 IP。

例如,如果您的本地 IP 是 10.0.10.3,则 environment.env 文件应包含以下内容:

THOUGHTS_BACKEND_URL=http://10.0.10.3:8000
USER_BACKEND_URL=http://10.0.10.3:8001

如果您在浏览器中访问前端服务,它应该连接到其他服务。

一种可能性是生成一个更大的 docker-compose 文件,其中包括所有内容。如果所有微服务都在同一个 Git 存储库中,这可能是有意义的,这种技术被称为monorepo (gomonorepo.org/)。可能的问题包括保持内部的 docker-compose 与单个系统一起工作,并使通用的 docker-compose 保持同步,以便自动化测试应该检测到任何问题。

这种结构有点累赘,因此我们可以将其转换为一个适当的 Kubernetes 集群,以便进行本地开发。

配置服务

要在 Kubernetes 中配置应用程序,我们需要为每个应用程序定义以下 Kubernetes 对象:

  • 部署:部署将控制 pod 的创建,因此它们将始终可用。它还将根据镜像创建它们,并在需要时添加配置。Pod 运行应用程序。

  • Service:该服务将使 RESTful 请求在集群内部可用,具有简短的名称。这将路由请求到任何可用的 pod。

  • Ingress:这使得服务在集群外部可用,因此我们可以从集群外部访问该应用。

在本节中,我们将详细查看 Thoughts Backend 配置作为示例。稍后,我们将看到不同部分是如何连接的。我们创建了一个 Kubernetes 子目录 (github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter06/thoughts_backend/kubernetes) 来存储每个定义的 .yaml 文件。

我们将使用 example 命名空间,因此请确保它已创建:

$ kubectl create namespace example

让我们从第一个 Kubernetes 对象开始。

配置部署

对于 Thoughts Backend 部署,我们将部署一个具有两个容器的 pod,一个带有数据库,另一个带有应用程序。这种配置使得在本地工作变得容易,但请记住,重新创建 pod 将重新启动两个容器。

配置文件完全在这里可用 (github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter06/thoughts_backend/kubernetes/deployment.yaml),让我们来看看它的不同部分。第一个元素描述了它是什么以及它的名称,以及它所在的命名空间:

---
apiVersion: apps/v1
kind: Deployment
metadata:
    name: thoughts-backend
    labels:
        app: thoughts-backend
    namespace: example

然后,我们生成 spec。它包含我们应该保留多少个 pod 以及每个 pod 的模板。selector 定义了要监视的标签,它应该与模板中的 labels 匹配:

spec:
    replicas: 1
    selector:
        matchLabels:
            app: thoughts-backend

template 部分在其自己的 spec 部分中定义了容器:


    template:
        metadata:
            labels:
                app: thoughts-backend
        spec:
            containers:
                - name: thoughts-backend-service
                  ...
                - name: thoughts-backend-db
                  ...

thoughts-backend-db 更简单。唯一需要的元素是定义容器的名称和镜像。我们需要将拉取策略定义为 Never,以指示镜像在本地 Docker 仓库中可用,并且不需要从远程注册表中拉取它:

- name: thoughts-backend-db
  image: thoughts_backend_db:latest
  imagePullPolicy: Never

thoughts-backend-service 需要定义服务的暴露端口以及环境变量。变量的值是我们在创建数据库时使用的值,除了 POSTGRES_HOST,在这里我们有一个优势,即同一 pod 中的所有容器共享相同的 IP:

 - name: thoughts-backend-service
   image: thoughts_server:latest
   imagePullPolicy: Never
   ports:
   - containerPort: 8000
   env:
   - name: DATABASE_ENGINE
     value: POSTGRESQL
   - name: POSTGRES_DB
     value: thoughts
   - name: POSTGRES_USER
     value: postgres
   - name: POSTGRES_PASSWORD
     value: somepassword
   - name: POSTGRES_PORT
     value: "5432"
   - name: POSTGRES_HOST
     value: "127.0.0.1"

要在 Kubernetes 中获取部署,需要应用该文件,如下所示:

$ kubectl apply -f thoughts_backend/kubernetes/deployment.yaml
deployment "thoughts-backend" created

部署现在已在集群中创建:

$ kubectl get deployments -n example
NAME             DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
thoughts-backend 1       1       1          1         20s

这将自动创建 pods。如果 pod 被删除或崩溃,部署将使用不同的名称重新启动它:

$ kubectl get pods -n example
NAME                              READY STATUS  RESTARTS AGE
thoughts-backend-6dd57f5486-l9tgg 2/2   Running 0        1m

部署正在跟踪最新的镜像,但除非删除,否则不会创建新的 pod。要进行更改,请确保手动删除 pod,之后它将被重新创建:

$ kubectl delete pod thoughts-backend-6dd57f5486-l9tgg -n example
pod "thoughts-backend-6dd57f5486-l9tgg" deleted
$ kubectl get pods -n example
NAME                              READY STATUS  RESTARTS AGE
thoughts-backend-6dd57f5486-nf2ds 2/2   Running 0        28s

该应用程序在集群内部仍然无法被发现,除非通过其特定的 pod 名称引用它,而这个名称可能会改变,因此我们需要为此创建一个服务。

配置服务

我们创建了一个 Kubernetes 服务来为创建的部署公开的应用程序创建一个名称。服务可以在 service.yaml 文件中进行检查。让我们来看一下:

---
apiVersion: v1
kind: Service
metadata:
    namespace: example
    labels:
        app: thoughts-service
    name: thoughts-service
spec:
    ports:
        - name: thoughts-backend
          port: 80
          targetPort: 8000
    selector:
        app: thoughts-backend
    type: NodePort

初始数据类似于部署。spec 部分定义了开放端口,将对 thoughts-backend 中的容器中的服务的端口 80 的访问路由到端口 8000,部署的名称。selector 部分将所有请求路由到与之匹配的任何 pod。

类型为 NodePort,以允许从集群外部访问。这使我们能够检查它是否正常工作,一旦找到外部暴露的 IP:

$ kubectl apply -f kubernetes/service.yaml
service "thoughts-service" configured
$ kubectl get service -n example
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
thoughts-service 10.100.252.250 <nodes> 80:31600/TCP 1m

我们可以通过访问所描述的 pod 的本地主机来访问 Thoughts Backend。在这种情况下,http://127.0.0.1:31600

服务为我们提供了内部名称,但是如果我们想要控制它如何在外部暴露,我们需要配置 Ingress。

配置 Ingress

最后,我们在ingress.yaml中描述 Ingress(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter06/thoughts_backend/kubernetes/ingress.yaml)。文件在此处复制。注意我们如何设置元数据以存储在正确的命名空间中:

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
    name: thoughts-backend-ingress
    namespace: example
spec:
    rules:
        - host: thoughts.example.local
          http:
            paths:
              - backend:
                  serviceName: thoughts-service
                  servicePort: 80
                path: /

此 Ingress 将使服务在端口80上暴露给节点。由于多个服务可以在同一节点上暴露,它们通过主机名进行区分,在本例中为thoughts.example.local

我们使用的 Ingress 控制器只允许在servicePort中暴露端口80(HTTP)和443(HTTPS)。

应用服务后,我们可以尝试访问页面,但是,除非我们将调用指向正确的主机,否则我们将收到 404 错误:

$ kubectl apply -f kubernetes/ingress.yaml
ingress "thoughts-backend-ingress" created
$ kubectl get ingress -n example
NAME                     HOSTS                  ADDRESS  PORTS  AGE
thoughts-backend-ingress thoughts.example.local localhost 80 1m
$ curl http://localhost
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.15.8</center>
</body>
</html>

我们需要能够将任何请求指向thoughts.example.local到我们的本地主机。在 Linux 和 macOS 中,最简单的方法是更改您的/etc/hosts文件,包括以下行:

127.0.0.1 thoughts.example.local

然后,我们可以使用浏览器检查我们的应用程序,这次是在http://thoughts.example.local(端口80):

定义不同的主机条目允许我们外部访问所有服务,以便能够调整它们并调试问题。我们将以相同的方式定义其余的 Ingresses。

如果在运行kubectl get ingress -n example时出现Connection refused错误,并且单词localhost没有出现,那么您的 Kubernetes 安装没有安装 Ingress 控制器。请仔细检查安装文档github.com/kubernetes/ingress-nginx/blob/master/docs/deploy/index.md.

所以现在我们在 Kubernetes 中本地部署了一个可工作的应用程序!

在本地部署完整系统

我们的每个微服务都可以独立运行,但是要使整个系统工作,我们需要部署这三个(Thoughts 后端、用户后端和前端)并将它们连接在一起。特别是前端需要其他两个微服务正在运行。使用 Kubernetes,我们可以在本地部署它。

要部署完整系统,我们需要先部署用户后端,然后是前端。我们将描述这些系统的每一个,将它们与已部署的 Thoughts 后端相关联,我们之前看到如何部署它。

部署用户后端

用户后端文件与 Thoughts 后端非常相似。您可以在 GitHub 存储库中检查它们(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter06/users_backend/kubernetes)。确保deployment.yaml中的环境设置值是正确的:

$ kubectl apply -f users_backend/kubernetes/deployment.yaml
deployment "users-backend" created
$ kubectl apply -f users_backend/kubernetes/service.yaml
service "users-service" created
$ kubectl apply -f users_backend/kubernetes/ingress.yaml
ingress "users-backend-ingress" created

记得确保在/etc/hosts中包含新的主机名:

127.0.0.1 users.example.local

您可以在http://users.example.local访问用户后端。

添加前端

前端服务和 Ingress 与先前的非常相似。部署略有不同。让我们分三组查看配置:

  1. 首先,我们添加关于namespacenamekind(deployment)的元数据,如下面的代码所示:
---
apiVersion: apps/v1
kind: Deployment
metadata:
    name: frontend
    labels:
        app: frontend
    namespace: example
  1. 然后,我们使用模板和replicas的数量定义spec。对于本地系统来说,一个副本就可以了:
spec:
    replicas: 1
    selector:
        matchLabels:
            app: frontend
    template:
        metadata:
            labels:
                app: frontend
  1. 最后,我们使用容器定义spec模板:
        spec:
            containers:
                - name: frontend-service
                  image: thoughts_frontend:latest
                  imagePullPolicy: Never
                  ports:
                     - containerPort: 8000
                  env:
                      - name: THOUGHTS_BACKEND_URL
                        value: http://thoughts-service
                      - name: USER_BACKEND_URL
                        value: http://users-service

与先前定义的 Thoughts 后端部署的主要区别在于只有一个容器,而且它上面的环境更简单。

我们将后端 URL 环境定义为服务端点。这些端点在集群内可用,因此它们将被定向到适当的容器。

请记住,*.example.local地址仅在您的计算机上可用,因为它们只存在于/etc/hosts中。在容器内,它们将不可用。

这适用于本地开发,但另一种选择是拥有一个可以重定向到127.0.0.1或类似地址的 DNS 域。

我们应该在/etc/hosts文件中添加一个新的域名:

127.0.0.1 frontend.example.local

Django 要求您设置ALLOWED_HOSTS设置的值,以允许它接受主机名,因为默认情况下它只允许从 localhost 进行连接。有关更多信息,请参阅 Django 文档(docs.djangoproject.com/en/2.2/ref/settings/#allowed-hosts)。为了简化事情,我们可以使用'*'来允许任何主机。在 GitHub 上查看代码(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter06/frontend/mythoughts/mythoughts/settings.py#L28)。

在生产中,将主机限制为完全限定域名FQDN),主机的完整 DNS 名称是一个良好的做法,但 Kubernetes Ingress 将检查主机头并在不正确时拒绝它。

前端应用程序将像以前一样部署:

$ kubectl apply -f frontend/kubernetes/deployment.yaml
deployment "frontend" created
$ kubectl apply -f frontend/kubernetes/service.yaml
service "frontend-service" created
$ kubectl apply -f frontend/kubernetes/ingress.yaml
ingress "frontend-ingress" created

然后我们可以访问整个系统,登录,搜索等。

记住有两个用户,brucestephen。他们的密码与他们的用户名相同。您无需登录即可搜索。

在浏览器中,转到http://frontend.example.local/

恭喜!您拥有一个工作的 Kubernetes 系统,包括不同的部署的微服务。您可以独立访问每个微服务以进行调试或执行操作,例如创建新用户等。

如果需要部署新版本,请使用docker-compose构建适当的容器并删除 pod 以强制重新创建它。

总结

在本章中,我们看到了如何在 Kubernetes 本地集群中部署我们的微服务,以允许本地开发和测试。在本地计算机上部署整个系统大大简化了开发新功能或调试系统行为的过程。生产环境将非常相似,因此这也为其奠定了基础。

我们首先描述了两个缺失的微服务。用户后端处理用户的身份验证,前端是第一章中介绍的单体的修改版本,进行移动-设计,计划和执行,它连接到两个后端。我们展示了如何以docker-compose的方式构建和运行它们。

之后,我们描述了如何设置一组.yaml文件来在 Kubernetes 中正确配置应用程序。每个微服务都有自己的部署来定义可用的 pod,一个服务来定义一个稳定的访问点,以及一个 Ingress 来允许外部访问。我们对它们进行了详细描述,然后将它们应用到所有的微服务上。

在下一章中,我们将看到如何从本地部署转移到部署准备好生产的 Kubernetes 集群。

问题

  1. 我们正在部署的三个微服务是什么?

  2. 哪个微服务需要其他两个可用?

  3. 为什么我们需要在运行docker-compose时使用外部 IP 来连接微服务?

  4. 每个应用程序所需的主要 Kubernetes 对象是什么?

  5. 有哪些对象是不必要的?

  6. 如果我们将任何微服务扩展到多个 pod,您能看到任何问题吗?

  7. 为什么我们要使用/etc/hosts文件?

进一步阅读

您可以在书籍《Kubernetes for Developers》(www.packtpub.com/eu/virtualization-and-cloud/kubernetes-developers)和《Kubernetes Cookbook - Second Edition》(www.packtpub.com/in/virtualization-and-cloud/kubernetes-cookbook-second-edition)中了解更多关于 Kubernetes 的信息。

第七章:配置和保护生产系统

生产(来自生产环境)是描述主要系统的常用名称-为真实客户提供服务的系统。这是公司中可用的主要环境。它也可以被称为live。该系统需要在互联网上公开可用,这也使得安全性和可靠性成为重要的优先事项。在本章中,我们将看到如何为生产部署 Kubernetes 集群。

我们将看到如何使用第三方提供商 Amazon Web Services(AWS)来设置一个,以及为什么自己创建是一个坏主意。我们将在这个新部署中部署我们的系统,并将查看如何设置负载均衡器以有序地将流量从旧的单体系统转移到新系统。

我们还将看到如何自动扩展 Kubernetes 集群内的 Pod 和节点,以使资源适应需求。

本章将涵盖以下主题:

  • 在野外使用 Kubernetes

  • 设置 Docker 注册表

  • 创建集群

  • 使用 HTTPS 和 TLS 保护外部访问

  • 为迁移到微服务做好准备

  • 自动扩展集群

  • 顺利部署新的 Docker 镜像

我们还将介绍一些良好的实践方法,以确保我们的部署尽可能顺利和可靠地部署。到本章结束时,您将在一个公开可用的 Kubernetes 集群中部署系统。

技术要求

我们将在本书中的示例中使用 AWS 作为我们的云供应商。我们需要安装一些实用程序以从命令行进行交互。查看如何在此文档中安装 AWS CLI 实用程序(aws.amazon.com/cli/)。此实用程序允许从命令行执行 AWS 任务。

为了操作 Kubernetes 集群,我们将使用eksctl。查看此文档(eksctl.io/introduction/installation/)以获取安装说明。

您还需要安装aws-iam-authenticator。您可以在此处查看安装说明(docs.aws.amazon.com/eks/latest/userguide/install-aws-iam-authenticator.html)。

本章的代码可以在 GitHub 的此链接找到:github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter07

确保您的计算机上安装了ab(Apache Bench)。它与 Apache 捆绑在一起,并且在 macOS 和一些 Linux 发行版中默认安装。您可以查看这篇文章:www.petefreitag.com/item/689.cfm

在野外使用 Kubernetes

在部署用于生产的集群时,最好的建议是使用商业服务。所有主要的云提供商(AWS EKS,Google Kubernetes Engine(GKE)和 Azure Kubernetes Service(AKS))都允许您创建托管的 Kubernetes 集群,这意味着唯一需要的参数是选择物理节点的数量和类型,然后通过kubectl访问它。

在本书的示例中,我们将使用 AWS,但请查看其他提供商的文档,以确定它们是否更适合您的用例。

Kubernetes 是一个抽象层,因此这种操作方式非常方便。定价类似于支付原始实例以充当节点服务器,并且无需安装和管理 Kubernetes 控制平面,因此实例充当 Kubernetes 节点。

值得再次强调:除非您有非常充分的理由,不要部署自己的 Kubernetes 集群;而是使用云提供商的服务。这样做会更容易,并且可以节省大量的维护成本。配置 Kubernetes 节点以实现高性能并实施良好的实践以避免安全问题并不是一件简单的事情。

如果您拥有自己的内部数据中心,则可能无法避免创建自己的 Kubernetes 集群,但在其他任何情况下,直接使用已知云提供商管理的集群更有意义。可能您当前的提供商已经为托管的 Kubernetes 提供了服务!

创建 IAM 用户

AWS 使用不同的用户来授予它们多个角色。它们具有不同的权限,使用户能够执行操作。在 AWS 的命名约定中,这个系统称为身份和访问管理IAM)。

根据您的设置以及 AWS 在您的组织中的使用方式,创建适当的 IAM 用户可能会相当复杂。查阅文档(docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html),并找到负责处理 AWS 的人员,并与他们核实所需的步骤。

让我们看看创建 IAM 用户的步骤:

  1. 如果尚未创建具有适当权限的 AWS 用户,则需要创建。确保它能够通过激活程序化访问来访问 API,如下面的屏幕截图所示:

这将显示其访问密钥、秘密密钥和密码。请务必将它们安全地存储起来。

  1. 要通过命令行访问,您需要使用 AWS CLI。使用 AWS CLI 和访问信息,配置您的命令行以使用aws
$ aws configure
AWS Access Key ID [None]: <your Access Key>
AWS Secret Access Key [None]: <your Secret Key>
Default region name [us-west-2]: <EKS region>
Default output format [None]:

您应该能够通过以下命令获取身份以检查配置是否成功:

$ aws sts get-caller-identity
{
 "UserId": "<Access Key>",
 "Account": "<account ID>",
 "Arn": "arn:aws:iam::XXXXXXXXXXXX:user/jaime"
}

现在您可以访问命令行 AWS 操作。

请记住,IAM 用户可以根据需要创建更多密钥,撤销现有密钥等。这通常由负责 AWS 安全的管理员用户处理。您可以在亚马逊文档中阅读更多信息(docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey_API)。密钥轮换是一个不错的主意,以确保旧密钥被废弃。您可以通过aws客户端界面执行此操作。

我们将使用 Web 控制台进行一些操作,但其他操作需要使用aws

设置 Docker 注册表

我们需要能够访问存储要部署的图像的 Docker 注册表。确保 Docker 注册表可访问的最简单方法是使用相同服务中的 Docker 注册表。

您仍然可以使用 Docker Hub 注册表,但是在同一云提供商中使用注册表通常更容易,因为它集成得更好。这也有助于身份验证方面。

我们需要使用以下步骤配置弹性容器注册表ECR):

  1. 登录 AWS 控制台并搜索 Kubernetes 或 ECR:

  1. 创建名为frontend的新注册表。它将创建一个完整的 URL,您需要复制:

  1. 我们需要使本地docker登录注册表。请注意,aws ecr get-login将返回一个docker命令,该命令将使您登录,因此请复制并粘贴:
$ aws ecr get-login --no-include-email
<command>
$ docker login -u AWS -p <token>
Login Succeeded
  1. 现在我们可以使用完整的注册表名称标记要推送的图像,并将其推送:
$ docker tag thoughts_frontend 033870383707.dkr.ecr.us-west-2.amazonaws.com/frontend
$ docker push 033870383707.dkr.ecr.us-west-2.amazonaws.com/frontend
The push refers to repository [033870383707.dkr.ecr.us-west-2.amazonaws.com/frontend]
...
latest: digest: sha256:21d5f25d59c235fe09633ba764a0a40c87bb2d8d47c7c095d254e20f7b437026 size: 2404
  1. 镜像已推送!您可以通过在浏览器中打开 AWS 控制台来检查:

  1. 我们需要重复这个过程,以推送用户后端和思想后端。

我们使用两个容器的设置来部署用户后端和想法后端,其中包括一个用于服务,另一个用于易失性数据库。这是为了演示目的而做的,但不会是生产系统的配置,因为数据需要是持久的。

在本章的最后,有一个关于如何处理这种情况的问题。一定要检查一下!

所有不同的注册表都将被添加。您可以在浏览器的 AWS 控制台中查看它们:

我们的流水线需要适应推送到这些存储库。

在部署中的一个良好的做法是进行一个称为推广的特定步骤,其中准备用于生产的镜像被复制到一个特定的注册表,降低了错误地在生产中部署坏镜像的机会。

这个过程可能需要多次进行,以在不同的环境中推广镜像。例如,在一个暂存环境中部署一个版本。运行一些测试,如果它们正确,推广版本,将其复制到生产注册表并标记为在生产环境中部署的好版本。

这个过程可以在不同的提供商中使用不同的注册表进行。

我们需要在我们的部署中使用完整 URL 的名称。

创建集群

为了使我们的代码在云中可用并且可以公开访问,我们需要设置一个工作的生产集群,这需要两个步骤:

  1. 在 AWS 云中创建 EKS 集群(这使您能够运行在此云集群中操作的kubectl命令)。

  2. 部署您的服务,使用一组.yaml文件,就像我们在之前的章节中看到的那样。这些文件需要进行最小的更改以适应云环境。

让我们来检查第一步。

创建 Kubernetes 集群

创建集群的最佳方式是使用eksctl实用程序。这将为我们自动化大部分工作,并且允许我们以后进行扩展。

请注意,EKS 只在一些地区可用,而不是所有地区。检查 AWS 区域表(aws.amazon.com/about-aws/global-infrastructure/regional-product-services/)以查看可用的区域。我们将使用俄勒冈(us-west-2)地区。

要创建 Kubernetes 集群,让我们采取以下步骤:

  1. 首先,检查eksctl是否正确安装:
$ eksctl get clusters
No clusters found
  1. 创建一个新的集群。这将需要大约 10 分钟:
$ eksctl create cluster -n Example
[i] using region us-west-2
[i] setting availability zones to [us-west-2d us-west-2b us-west-2c]
...
[✔]  EKS cluster "Example" in "us-west-2" region is ready

  1. 这将创建集群。检查 AWS web 界面将显示新配置的元素。

需要添加--arg-access选项以创建一个能够自动扩展的集群。这将在自动扩展集群部分中进行更详细的描述。

  1. eksctl create命令还会添加一个包含有关远程 Kubernetes 集群信息的新上下文,并激活它,因此kubectl现在将指向这个新集群。

请注意,kubectl有上下文的概念,作为它可以连接的不同集群。您可以通过运行kubectl config get-contextskubectl config use-context <context-name>来查看所有可用的上下文,以更改它们。请查看 Kubernetes 文档(kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/)以了解如何手动创建新的上下文。

  1. 这个命令设置了kubectl以正确的上下文来运行命令。默认情况下,它生成一个具有两个节点的集群:
$ kubectl get nodes
NAME                    STATUS ROLES AGE VERSION
ip-X.us-west-2.internal Ready <none> 11m v1.13.7-eks-c57ff8
ip-Y.us-west-2.internal Ready <none> 11m v1.13.7-eks-c57ff8
  1. 我们可以扩展节点的数量。为了减少资源使用和节省金钱。我们需要检索节点组的名称,它控制节点的数量,然后缩减它:
$ eksctl get nodegroups --cluster Example
CLUSTER NODEGROUP CREATED MIN SIZE MAX SIZE DESIRED CAPACITY INSTANCE TYPE IMAGE ID
Example ng-fa5e0fc5 2019-07-16T13:39:07Z 2 2 0 m5.large ami-03a55127c613349a7
$ eksctl scale nodegroup --cluster Example --name ng-fa5e0fc5 -N 1
[i] scaling nodegroup stack "eksctl-Example-nodegroup-ng-fa5e0fc5" in cluster eksctl-Example-cluster
[i] scaling nodegroup, desired capacity from to 1, min size from 2 to 1
  1. 您可以通过kubectl联系集群并正常进行操作:
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 7m31s

集群已经设置好了,我们可以从命令行上对其进行操作。

创建 EKS 集群可以以许多方式进行调整,但是 AWS 在访问、用户和权限方面可能会变化无常。例如,集群喜欢有一个 CloudFormation 规则来处理集群,并且所有元素应该由相同的 IAM 用户创建。与您组织中负责基础架构定义的任何人核对,以确定正确的配置是什么。不要害怕进行测试,集群可以通过eksctl配置或 AWS 控制台快速删除。

此外,eksctl会在不同的可用区(AWS 同一地理区域内的隔离位置)中创建集群节点,以尽量减少因 AWS 数据中心出现问题而导致整个集群宕机的风险。

配置云 Kubernetes 集群

下一阶段是在 EKS 集群上运行我们的服务,以便在云中可用。我们将使用.yaml文件作为基础,但需要进行一些更改。

查看 GitHub Chapter07github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter07)子目录中的文件。

我们将看到与上一章中的 Kubernetes 配置文件的不同,然后在部署系统部分部署它们。

配置 AWS 镜像注册表

第一个区别是我们需要将镜像更改为完整的注册表,以便集群使用 ECS 注册表中可用的镜像。

请记住,您需要在 AWS 内部指定注册表,以便 AWS 集群可以正确访问它。

例如,在frontend/deployment.yaml文件中,我们需要以这种方式定义它们:

containers:
- name: frontend-service
  image: XXX.dkr.ecr.us-west-2.amazonaws.com/frontend:latest
  imagePullPolicy: Always

镜像应该从 AWS 注册表中拉取。拉取策略应更改为强制从集群中拉取。

在创建example命名空间后,您可以通过应用文件在远程服务器上部署:

$ kubectl create namespace example
namespace/example created
$ kubectl apply -f frontend/deployment.yaml
deployment.apps/frontend created

过一会儿,部署会创建 pod:

$ kubectl get pods -n example
NAME                      READY STATUS  RESTARTS AGE
frontend-58898587d9-4hj8q 1/1   Running 0        13s

现在我们需要更改其余的元素。所有部署都需要适应包括正确注册表。

在 GitHub 上检查所有deployment.yaml文件的代码。

配置使用外部可访问负载均衡器

第二个区别是使前端服务可以在外部访问,以便互联网流量可以访问集群。

这很容易通过将服务从NodePort更改为LoadBalancer来完成。检查frontend/service.yaml文件:

apiVersion: v1
kind: Service
metadata:
    namespace: example
    labels:
        app: frontend-service
    name: frontend-service
spec:
    ports:
        - name: frontend
          port: 80
          targetPort: 8000
    selector:
        app: frontend
    type: LoadBalancer

这将创建一个可以外部访问的新弹性负载均衡器ELB)。现在,让我们开始部署。

部署系统

整个系统可以从Chapter07子目录中部署,使用以下代码:

$ kubectl apply --recursive -f .
deployment.apps/frontend unchanged
ingress.extensions/frontend created
service/frontend-service created
deployment.apps/thoughts-backend created
ingress.extensions/thoughts-backend-ingress created
service/thoughts-service created
deployment.apps/users-backend created
ingress.extensions/users-backend-ingress created
service/users-service created

这些命令会迭代地通过子目录并应用任何.yaml文件。

几分钟后,您应该看到一切都正常运行:

$ kubectl get pods -n example
NAME                              READY STATUS  RESTARTS AGE
frontend-58898587d9-dqc97         1/1   Running 0        3m
thoughts-backend-79f5594448-6vpf4 2/2   Running 0        3m
users-backend-794ff46b8-s424k     2/2   Running 0        3m

要获取公共访问点,您需要检查服务:

$ kubectl get svc -n example
NAME             TYPE         CLUSTER-IP EXTERNAL-IP AGE
frontend-service LoadBalancer 10.100.152.177 a28320efca9e011e9969b0ae3722320e-357987887.us-west-2.elb.amazonaws.com 3m
thoughts-service NodePort 10.100.52.188 <none> 3m
users-service    NodePort 10.100.174.60 <none> 3m

请注意,前端服务有一个外部 ELB DNS 可用。

如果您在浏览器中输入该 DNS,可以访问服务如下:

恭喜,您拥有自己的云 Kubernetes 服务。服务可访问的 DNS 名称不太好,因此我们将看到如何添加注册的 DNS 名称并在 HTTPS 端点下公开它。

使用 HTTPS 和 TLS 保护外部访问

为了向客户提供良好的服务,您的外部端点应通过 HTTPS 提供。这意味着您和客户之间的通信是私密的,不能在网络路由中被窃听。

HTTPS 的工作原理是服务器和客户端加密通信。为了确保服务器是他们所说的那样,需要有一个由授予 DNS 已验证的权威颁发的 SSL 证书。

请记住,HTTPS 的目的不是服务器本身是可信的,而是客户端和服务器之间的通信是私密的。服务器仍然可能是恶意的。这就是验证特定 DNS 不包含拼写错误的重要性。

您可以在这本奇妙的漫画中获取有关 HTTPS 如何运作的更多信息:howhttps.works/

获取外部端点的证书需要两个阶段:

  • 您拥有特定的 DNS 名称,通常是通过从域名注册商购买获得的。

  • 您通过认可的证书颁发机构CA)获得 DNS 名称的唯一证书。 CA 必须验证您控制 DNS 名称。

为了促进 HTTPS 的使用,非营利性组织Let's Encryptletsencrypt.org)提供有效期为 60 天的免费证书。这将比通过云服务提供商获得证书更费力,但如果资金紧张,这可能是一个选择。

这些天,这个过程非常容易通过云服务提供商来完成,因为它们可以同时充当两者,简化流程。

需要通过 HTTPS 进行通信的重要元素是我们网络的边缘。我们自己的微服务在内部网络中进行通信时不需要使用 HTTPS,HTTP 就足够了。但它需要是一个不受公共干扰的私有网络。

按照我们的例子,AWS 允许我们创建并将证书与 ELB 关联,以 HTTP 提供流量。

让 AWS 提供 HTTPS 流量可以确保我们使用最新和最安全的安全协议,例如传输层安全性TLS)v1.3(撰写时的最新版本),但也保持与旧协议的向后兼容性,例如 SSL。

换句话说,默认情况下使用最安全的环境是最佳选择。

设置 HTTPS 的第一步是直接从 AWS 购买 DNS 域名,或将控制权转移到 AWS。这可以通过他们的 Route 53 服务完成。您可以在aws.amazon.com/route53/上查看文档。

严格来说,不需要将您的 DNS 转移到亚马逊,只要您可以将其指向外部 ELB,但这有助于集成和获取证书。在创建证书时,您需要证明自己拥有 DNS 记录,使用 AWS 可以简化此过程,因为他们会为他们控制的 DNS 记录创建证书。请查看docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html上的文档。

要在 ELB 上启用 HTTPS 支持,请查看以下步骤:

  1. 转到 AWS 控制台中的监听器:

  1. 单击“编辑”并添加 HTTPS 支持的新规则:

  1. 如您所见,它将需要 SSL 证书。单击“更改”以进行管理:

  1. 从这里,您可以添加现有证书或从亚马逊购买证书。

务必查看亚马逊负载均衡器的文档。有几种类型的 ELB 可供使用,根据您的用例,一些 ELB 具有与其他 ELB 不同的功能。例如,一些新的 ELB 能够在客户端请求 HTTP 数据时重定向到 HTTPS。请查看aws.amazon.com/elasticloadbalancing/上的文档。

恭喜,现在您的外部端点支持 HTTPS,确保您与客户的通信是私密的。

准备好迁移到微服务

为了在进行迁移时顺利运行,您需要部署一个负载均衡器,它可以让您快速在后端之间切换并保持服务运行。

正如我们在第一章中讨论的那样,进行移动-设计、计划和执行,HAProxy 是一个很好的选择,因为它非常灵活,并且有一个很好的 UI,可以让您通过单击网页上的按钮快速进行操作。它还有一个出色的统计页面,可以让您监视服务的状态。

AWS 有一个名为应用负载均衡器ALB)的 HAProxy 替代方案。这是 ELB 的功能丰富更新,允许您将不同的 HTTP 路径路由到不同的后端服务。

HAProxy 具有更丰富的功能集和更好的仪表板与之交互。它也可以通过配置文件进行更改,这有助于控制更改,正如我们将在第八章中看到的那样,使用 GitOps 原则

显然,只有在所有服务都在 AWS 上可用时才能使用,但在这种情况下,它可能是一个很好的解决方案,因为它将更简单并且更符合技术堆栈的其余部分。查看文档:aws.amazon.com/blogs/aws/new-aws-application-load-balancer/

要在服务前部署负载均衡器,我建议不要在 Kubernetes 上部署它,而是以与传统服务相同的方式运行它。这种类型的负载均衡器将是系统的关键部分,消除不确定性对于成功运行是很重要的。它也是一个相对简单的服务。

请记住,负载均衡器需要正确复制,否则它将成为单点故障。亚马逊和其他云提供商允许您设置 ELB 或其他类型的负载均衡器,以便将流量平衡在它们之间。

举例来说,我们创建了一个示例配置和docker-compose文件来快速运行它,但配置可以按照团队最舒适的方式进行设置。

运行示例

代码可在 GitHub 上找到(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter07/haproxy)。我们从 Docker Hub 中的 HAProxy Docker 镜像继承(hub.docker.com/_/haproxy/),添加我们自己的配置文件。

让我们来看看配置文件haproxy.cfg中的主要元素:

frontend haproxynode
    bind *:80
    mode http
    default_backend backendnodes

backend backendnodes
    balance roundrobin
    option forwardfor
    server aws a28320efca9e011e9969b0ae3722320e-357987887
               .us-west-2.elb.amazonaws.com:80 check
    server example www.example.com:80 check

listen stats
    bind *:8001
    stats enable
    stats uri /
    stats admin if TRUE

我们定义了一个前端,接受任何端口80的请求,并将请求发送到后端。后端将请求平衡到两个服务器,exampleaws。基本上,example指向www.example.com(您的旧服务的占位符),aws指向先前创建的负载均衡器。

我们在端口8001上启用统计服务器,并允许管理员访问。

docker-compose配置启动服务器,并将本地端口转发到容器端口8000(负载均衡器)和8001(统计)。使用以下命令启动它:

$ docker-compose up --build proxy
...

现在我们可以访问localhost:8000,它将在thoughts服务和 404 错误之间交替。

通过这种方式调用example.com时,我们正在转发主机请求。这意味着我们发送一个请求,请求Host:localhostexample.com,它返回一个 404 错误。请确保检查您的服务,所有后端都接受相同的主机信息。

打开统计页面查看设置:

检查后端节点中的awsexample条目。还有很多有趣的信息,比如请求数量、最后连接、数据等等。

您可以在检查example后端时执行操作,然后在下拉菜单中将状态设置为 MAINT。应用后,example后端将处于维护模式,并从负载均衡器中移除。统计页面如下:

现在在localhost:8000中访问负载均衡器只会返回thoughts前端。您可以重新启用后端,将其设置为 READY 状态。

有一种称为 DRAIN 的状态,它将停止新会话进入所选服务器,但现有会话将继续。这在某些配置中可能很有趣,但如果后端真正是无状态的,直接转移到 MAINT 状态就足够了。

HAProxy 也可以配置使用检查来确保后端可用。在示例中,我们添加了一个被注释的检查,它发送一个 HTTP 命令来检查返回。

option httpchk HEAD / HTTP/1.1\r\nHost:\ example.com

检查将对两个后端相同,因此需要成功返回。默认情况下,它将每隔几秒运行一次。

您可以在www.haproxy.org/上查看完整的 HAProxy 文档。有很多可以配置的细节。与您的团队跟进,确保像超时、转发标头等区域的配置是正确的。

健康检查的概念也用于 Kubernetes,以确保 Pod 和容器准备好接受请求并保持稳定。我们将在下一节中看到如何确保正确部署新镜像。

平稳部署新的 Docker 镜像

在生产环境中部署服务时,确保其能够平稳运行以避免中断服务至关重要。

Kubernetes 和 HAProxy 能够检测服务是否正常运行,并在出现问题时采取行动,但我们需要提供一个充当健康检查的端点,并配置它以定期被 ping,以便及早发现问题。

为简单起见,我们将使用根 URL 作为健康检查,但我们可以设计特定的端点进行测试。一个良好的健康检查应该检查服务是否按预期工作,但是轻便快速。避免过度测试或执行外部验证,这可能会使端点花费很长时间。

返回空响应的 API 端点是一个很好的例子,因为它检查整个管道系统是否正常工作,但回答非常快。

在 Kubernetes 中,有两个测试来确保 Pod 正常工作,即就绪探针和活动探针。

活动探针

活动探针检查容器是否正常工作。它是在容器中启动的返回正确的进程。如果返回错误(或更多,取决于配置),Kubernetes 将终止容器并重新启动。

活动探针将在容器内执行,因此需要有效。对于 Web 服务,添加curl命令是一个好主意:

spec:
  containers:
  - name: frontend-service
    livenessProbe:
      exec:
        command:
        - curl
        - http://localhost:8000/
        initialDelaySeconds: 5
        periodSeconds: 30

虽然有一些选项,比如检查 TCP 端口是否打开或发送 HTTP 请求,但运行命令是最通用的选项。它也可以用于调试目的。请参阅文档以获取更多选项。

要小心对活动探针过于激进。每次检查都会给容器增加一些负载,因此根据负载情况,多个探针可能会导致杀死更多的容器。

如果您的服务经常被活动探针重新启动,要么探针太过激进,要么容器数量负载过高,或者两者兼而有之。

该探针配置为等待五秒,然后每 30 秒运行一次。默认情况下,连续三次失败的检查后,将重新启动容器。

就绪探针

就绪探针检查容器是否准备好接受更多请求。这是一个不那么激进的版本。如果测试返回错误或超时,容器不会重新启动,而只会被标记为不可用。

就绪探针通常用于避免过早接受请求,但它会在启动后运行。一个智能的就绪探针可以标记容器何时达到最大容量,无法接受更多请求,但通常配置类似于活跃探针的探针就足够了。

就绪探针在部署配置中定义,方式与活跃探针相同。让我们来看一下:

spec:
  containers:
  - name: frontend-service
    readinessProbe:
      exec:
        command:
        - curl
        - http://localhost:8000/
        initialDelaySeconds: 5
        periodSeconds: 10

就绪探针应该比活跃探针更积极,因为结果更安全。这就是为什么periodSeconds更短。根据您的特定用例,您可能需要两者或者不需要,但就绪探针是启用滚动更新所必需的,接下来我们将看到。

示例代码中的frontend/deployment.yaml部署包括了两个探针。查看 Kubernetes 文档(kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)以获取更多详细信息和选项。

请注意,这两个探针用于不同的目标。就绪探针延迟请求的输入,直到 Pod 准备就绪,而活跃探针有助于处理卡住的容器。

延迟的活跃探针返回将重新启动 Pod,因此负载的增加可能会产生重新启动 Pod 的级联效应。相应地进行调整,并记住两个探针不需要重复相同的命令。

就绪探针和活跃探针都帮助 Kubernetes 控制 Pod 的创建方式,这影响了部署的更新。

滚动更新

默认情况下,每次我们更新部署的镜像时,Kubernetes 部署将重新创建容器。

通知 Kubernetes 新版本可用并不足以将新镜像推送到注册表,即使标签相同。您需要更改部署.yaml文件中image字段中描述的标签。

我们需要控制图像的变化方式。为了不中断服务,我们需要执行滚动更新。这种更新方式会添加新的容器,等待它们就绪,将它们添加到池中,并移除旧的容器。这种部署比移除所有容器并重新启动它们要慢一些,但它允许服务不中断。

如何执行这个过程可以通过调整部署中的strategy部分来配置:

spec:
    replicas: 4
    strategy:
      type: RollingUpdate
      rollingUpdate:
        maxUnavailable: 25%
        maxSurge: 1

让我们了解这段代码:

  • strategytype可以是RollingUpdate(默认)或Recreate,后者会停止现有的 Pod 并创建新的 Pod。

  • maxUnavailable定义了更改期间不可用的最大 Pod 数量。这定义了新容器将被添加和旧容器将被移除的速度。它可以被描述为一个百分比,就像我们的例子,或者是一个固定的数字。

  • maxSurge定义了可以在期望 Pod 的限制之上创建的额外 Pod 的数量。这可以是一个特定的数字或者是总数的百分比。

  • 当我们将replicas设置为4时,在两种情况下的结果都是一个 Pod。这意味着在更改期间,最多可以有一个 Pod 不可用,并且我们将逐个创建新的 Pod。

更高的数字将使更新速度更快,但会消耗更多资源(maxSurge)或在更新期间更积极地减少可用资源(maxUnavailable)。

对于少量的副本,要保守并在您对流程更加熟悉并且有更多资源时增加数量。

最初,手动扩展 Pod 将是最简单和最好的选择。如果流量变化很大,有高峰和低谷,那么自动扩展集群可能是值得的。

自动扩展集群

我们之前已经看到了如何为服务更改 Pod 的数量,以及如何添加和移除节点。这可以自动化地描述一些规则,允许集群弹性地改变其资源。

请记住,自动缩放需要调整以适应您的特定用例。如果资源利用率随时间发生很大变化,例如,如果某些小时的活动比其他小时多得多,或者如果有一种病毒元素意味着服务意外地将请求增加了 10 倍,那么这是一种使用技术。

如果您对服务器的使用量很小,并且利用率相对恒定,可能没有必要添加自动缩放。

集群可以在两个不同的方面自动扩展或缩小:

  • 在 Kubernetes 配置中,pod 的数量可以自动增加或减少。

  • 节点的数量可以在 AWS 中自动增加或减少。

pod 的数量和节点的数量都需要保持一致,以允许自然增长。

如果 pod 的数量增加而没有添加更多的硬件(节点),Kubernetes 集群将没有更多的容量,只是在不同分布中分配了相同的资源。

如果节点数量增加而没有创建更多的 pod,那么在某个时候,额外的节点将没有 pod 可分配,导致资源利用不足。另一方面,任何新添加的节点都会有相关成本,因此我们希望能够正确地使用它。

要能够自动缩放 pod,请确保它是可扩展的。要确保 pod 是可扩展的,请检查它是否是无状态的 Web 服务,并从外部源获取所有信息。

请注意,在我们的代码示例中,前端 pod 是可扩展的,而 Thoughts 和 Users Backend 不可扩展,因为它们包括自己的数据库容器,应用程序连接到该容器。

创建一个新的 pod 会创建一个新的空数据库,这不是预期的行为。这是有意为之的,以简化示例代码。预期的生产部署是,如前所述,连接到外部数据库。

Kubernetes 配置和 EKS 都具有根据规则更改 pod 和节点数量的功能。

创建 Kubernetes 水平 Pod 自动缩放器

在 Kubernetes 术语中,用于增加和减少 pod 的服务称为水平 Pod 自动缩放器HPA)。

这是因为它需要一种检查测量以进行缩放的方法。要启用这些指标,我们需要部署 Kubernetes 度量服务器。

部署 Kubernetes 度量服务器

Kubernetes 度量服务器捕获内部低级别的指标,如 CPU 使用率,内存等。HPA 将捕获这些指标并使用它们来调整资源。

Kubernetes 度量服务器不是向 HPA 提供指标的唯一可用服务器,还可以定义其他度量系统。当前可用适配器的列表可在 Kubernetes 度量项目中找到(github.com/kubernetes/metrics/blob/master/IMPLEMENTATIONS.md#custom-metrics-api)。

这允许定义自定义指标作为目标。首先从默认指标开始,只有在特定部署存在真正限制时才转移到自定义指标。

要部署 Kubernetes 度量服务器,请从官方项目页面下载最新版本(github.com/kubernetes-incubator/metrics-server/releases)。写作时,版本为0.3.3

下载tar.gz文件,写作时为metrics-server-0.3.3.tar.gz。解压缩并将版本应用到集群:

$ tar -xzf metrics-server-0.3.3.tar.gz
$ cd metrics-server-0.3.3/deploy/1.8+/
$ kubectl apply -f .
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created
serviceaccount/metrics-server created
deployment.extensions/metrics-server created
service/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created

您将在kube-system命名空间中看到新的 pod:

$ kubectl get pods -n kube-system
NAME                            READY STATUS  RESTARTS AGE
...
metrics-server-56ff868bbf-cchzp 1/1   Running 0        42s

您可以使用kubectl top命令获取有关节点和 pod 的基本信息:

$ kubectl top node
NAME                    CPU(cores) CPU% MEM(bytes) MEMORY%
ip-X.us-west-2.internal 57m        2%   547Mi      7%
ip-Y.us-west-2.internal 44m        2%   534Mi      7%
$ kubectl top pods -n example
$ kubectl top pods -n example
NAME                              CPU(cores) MEMORY(bytes)
frontend-5474c7c4ff-d4v77         2m         51Mi
frontend-5474c7c4ff-dlq6t         1m         50Mi
frontend-5474c7c4ff-km2sj         1m         51Mi
frontend-5474c7c4ff-rlvcc         2m         51Mi
thoughts-backend-79f5594448-cvdvm 1m         54Mi
users-backend-794ff46b8-m2c6w     1m         54Mi

为了正确控制使用量的限制,我们需要在部署中配置分配和限制资源。

在部署中配置资源

在容器的配置中,我们可以指定所请求的资源以及它们的最大资源。

它们都向 Kubernetes 提供有关容器的预期内存和 CPU 使用情况的信息。在创建新容器时,Kubernetes 将自动将其部署到具有足够资源覆盖的节点上。

frontend/deployment.yaml文件中,我们包括以下resources实例:

spec:
    containers:
    - name: frontend-service
      image: 033870383707.dkr.ecr.us-west-2
                 .amazonaws.com/frontend:latest
      imagePullPolicy: Always
      ...
      resources:
          requests:
              memory: "64M"
              cpu: "60m"
          limits:
              memory: "128M"
              cpu: "70m"

最初请求的内存为 64 MB,0.06 个 CPU 核心。

内存资源也可以使用 Mi 的平方,相当于兆字节(1000²字节),称为 mebibyte(2²⁰字节)。在任何情况下,差异都很小。您也可以使用 G 或 T 来表示更大的数量。

CPU 资源是以分数形式衡量的,其中 1 表示节点运行的任何系统中的一个核心(例如,AWS vCPU)。请注意,1000m,表示 1000 毫核心,相当于一个完整的核心。

限制为 128 MB 和 0.07 个 CPU 核心。容器将无法使用超过限制的内存或 CPU。

目标是获得简单的整数以了解限制和所请求的资源。不要期望第一次就完美无缺;应用程序将改变它们的消耗。

以聚合方式测量指标,正如我们将在第十一章中讨论的那样,处理系统中的变化、依赖关系和机密,将帮助您看到系统的演变并相应地进行调整。

限制为自动缩放器创建了基准,因为它将以资源的百分比来衡量。

创建 HPA

要创建一个新的 HPA,我们可以使用kubectl autoscale命令:

$ kubectl autoscale deployment frontend --cpu-percent=10 --min=2 --max=8 -n example
horizontalpodautoscaler.autoscaling/frontend autoscaled

这将创建一个新的 HPA,它以example命名空间中的frontend部署为目标,并设置要在28之间的 Pod 数量。要缩放的参数是 CPU,我们将其设置为可用 CPU 的 10%,并在所有 Pod 中平均。如果超过了,它将创建新的 Pod,如果低于,它将减少它们。

10%的限制用于触发自动缩放器并进行演示。

自动缩放器作为一种特殊类型的 Kubernetes 对象工作,可以查询它:

$ kubectl get hpa -n example
NAME     REFERENCE           TARGETS  MIN MAX REPLICAS AGE
frontend Deployment/frontend 2%/10%   2   8   4        80s

请注意,目标显示当前约为 2%,接近限制。这是为了小型可用 CPU 而设计的,将具有相对较高的基线。

几分钟后,副本的数量将减少,直到达到最小值2

缩容可能需要几分钟。这通常是预期的行为,扩容比缩容更积极。

为了创建一些负载,让我们使用应用程序 Apache Bench(ab),并与前端中专门创建的端点结合使用大量 CPU:

$ ab -n 100 http://<LOADBALANCER>.elb.amazonaws.com/load
Benchmarking <LOADBALANCER>.elb.amazonaws.com (be patient)....

请注意,ab是一个方便的测试应用程序,可以同时生成 HTTP 请求。如果愿意,您也可以在浏览器中多次快速点击 URL。

请记住添加负载均衡器 DNS,如在创建集群部分中检索到的。

这将在集群中生成额外的 CPU 负载,并使部署扩展:

NAME     REFERENCE           TARGETS MIN MAX REPLICAS AGE
frontend Deployment/frontend 47%/10% 2   8   8        15m

请求完成后,几分钟后,Pod 的数量将缓慢缩减,直到再次达到两个 Pod。

但是我们需要一种方式来扩展节点,否则我们将无法增加系统中的资源总数。

扩展集群中节点的数量

EKS 集群中作为节点工作的 AWS 实例的数量也可以增加。这为集群增加了额外的资源,并使其能够启动更多的 Pod。

支持这一功能的底层 AWS 服务是自动扩展组。这是一组共享相同镜像并具有定义大小的 EC2 实例,包括最小和最大实例。

在任何 EKS 集群的核心,都有一个控制集群节点的自动扩展组。请注意,eksctl将自动扩展组创建并公开为节点组:

$ eksctl get nodegroup --cluster Example
CLUSTER NODEGROUP   MIN  MAX  DESIRED INSTANCE IMAGE ID
Example ng-74a0ead4 2    2    2       m5.large ami-X

使用eksctl,我们可以手动扩展或缩小集群,就像我们创建集群时描述的那样。

$ eksctl scale nodegroup --cluster Example --name ng-74a0ead4 --nodes 4
[i] scaling nodegroup stack "eksctl-Example-nodegroup-ng-74a0ead4" in cluster eksctl-Example-cluster
[i] scaling nodegroup, desired capacity from to 4, max size from 2 to 4

这个节点组也可以在 AWS 控制台中看到,在 EC2 | 自动缩放组下:

在 Web 界面中,我们可以收集有关自动缩放组的一些有趣信息。活动历史选项卡允许您查看任何扩展或缩小事件,监控选项卡允许您检查指标。

大部分处理都是由eksctl自动创建的,比如实例类型和 AMI-ID(实例上的初始软件,包含操作系统)。它们应该主要由eksctl控制。

如果需要更改实例类型,eksctl要求您创建一个新的节点组,移动所有的 pod,然后删除旧的。您可以在eksctl文档中了解更多关于这个过程的信息。

但是从 Web 界面,很容易编辑缩放参数并为自动缩放添加策略。

通过 Web 界面更改参数可能会使eksctl中检索的数据混乱,因为它是独立设置的。

可以为 AWS 安装 Kubernetes 自动缩放器,但需要一个secrets配置文件,其中包括在自动缩放器 pod 中添加适当的 AMI 的 AWS 权限。

在代码中以 AWS 术语描述自动缩放策略也可能会令人困惑。Web 界面使这变得更容易一些。好处是你可以在配置文件中描述一切,这些文件可以在源代码控制下。

在这里,我们将使用 Web 界面配置,但您可以按照eksctl.io/usage/autoscaling/上的说明进行操作。

对于缩放策略,有两个主要的组件可以创建:

  • 定时操作:这些是在定义的时间发生的扩展和缩小事件。该操作可以通过所需数量和最小和最大数量的组合来改变节点的数量,例如,在周末增加集群。操作可以定期重复,例如每天或每小时。操作还可以有一个结束时间,这将使值恢复到先前定义的值。这可以在系统中预期额外负载时提供几个小时的提升,或者在夜间减少成本。

  • 缩放策略:这些策略是在特定时间查找需求并在所描述的数字之间扩展或缩小实例的策略。有三种类型的策略:目标跟踪、阶梯缩放和简单缩放。目标跟踪是最简单的,因为它监视目标(通常是 CPU 使用率)并根据需要扩展和缩小以保持接近该数字。另外两种策略需要您使用 AWS CloudWatch 指标系统生成警报,这更强大,但也需要使用 CloudWatch 和更复杂的配置。

节点的数量不仅可以增加,还可以减少,这意味着删除节点。

删除节点

删除节点时,正在运行的 pod 需要移动到另一个节点。Kubernetes 会自动处理这个操作,EKS 会以安全的方式执行该操作。

如果节点由于任何原因关闭,例如意外的硬件问题,也会发生这种情况。正如我们之前所看到的,集群是在多个可用区创建的,以最小化风险,但如果 Amazon 的一个可用区出现问题,一些节点可能会出现问题。

Kubernetes 是为这种问题而设计的,因此在意外情况下很擅长将 pod 从一个节点移动到另一个节点。

将一个 pod 从一个节点移动到另一个节点是通过销毁该 pod 并在新节点上重新启动来完成的。由于 pod 受部署控制,它们将保持副本或自动缩放值所描述的适当数量的 pod。

请记住,Pod 本质上是不稳定的,应设计成可以被销毁和重新创建。

扩展还可以导致现有的 Pod 移动到其他节点以更好地利用资源,尽管这种情况较少。增加节点数量通常是在增加 Pod 数量的同时进行的。

控制节点的数量需要考虑要遵循的策略,以实现最佳结果,具体取决于要求。

设计一个成功的自动缩放策略

正如我们所看到的,Pod 和节点两种自动缩放需要相互关联。保持节点数量减少可以降低成本,但会限制可用于增加 Pod 数量的资源。

请记住,自动缩放是一个大量数字的游戏。除非您有足够的负载变化来证明其必要性,否则调整它将产生成本节省,这与开发和维护过程的成本不可比。对预期收益和维护成本进行成本分析。

在处理集群大小变化时,优先考虑简单性。在夜间和周末缩减规模可以节省大量资金,而且比生成复杂的 CPU 算法来检测高低要容易得多。

请记住,自动缩放并不是与云服务提供商降低成本的唯一方法,可以与其他策略结合使用。

例如,在 AWS 中,预订 EC2 实例一年或更长时间可以大大减少账单。它们可以用于集群基线,并与更昂贵的按需实例结合使用进行自动缩放,从而额外降低成本:aws.amazon.com/ec2/pricing/reserved-instances/

通常情况下,您应该有额外的硬件可用于扩展 Pod,因为这样更快。这在不同的 Pod 以不同的速度扩展的情况下是允许的。根据应用程序的不同,当一个服务的使用量增加时,另一个服务的使用量可能会减少,这将保持利用率在相似的数字。

这可能不是您首先想到的用例,但例如,在夜间安排的任务可能会利用白天被外部请求使用的可用资源。

它们可以在不同的服务中工作,随着负载从一个服务转移到另一个服务而自动平衡。

一旦头部空间减少,就开始扩展节点。始终留出安全余地,以避免陷入节点扩展不够快,由于资源不足而无法启动更多的 Pod 的情况。

Pod 自动缩放器可以尝试创建新的 Pod,如果没有可用资源,它们将不会启动。同样,如果删除了一个节点,任何未删除的 Pod 可能由于资源不足而无法启动。

请记住,我们在部署的resources部分向 Kubernetes 描述了新 Pod 的要求。确保那里的数字表明了 Pod 所需的数字。

为了确保 Pod 在不同节点上充分分布,您可以使用 Kubernetes 的亲和性和反亲和性规则。这些规则允许定义某种类型的 Pod 是否应在同一节点上运行。

例如,这对于确保各种 Pod 均匀分布在区域中,或者确保两个服务始终部署在同一节点以减少延迟非常有用。

您可以在这篇博客文章中了解有关亲和性和如何进行配置的更多信息:supergiant.io/blog/learn-how-to-assign-pods-to-nodes-in-kubernetes-using-nodeselector-and-affinity-features/,以及在 Kubernetes 官方配置中(kubernetes.io/docs/concepts/configuration/assign-pod-node/)。

总的来说,Kubernetes 和eksctl默认情况下对大多数应用程序都能很好地工作。仅在高级配置时使用此建议。

总结

在本章中,我们看到了如何将 Kubernetes 集群应用到生产环境中,并在云提供商(在本例中是 AWS)中创建 Kubernetes 集群。我们看到了如何设置我们的 Docker 注册表,使用 EKS 创建集群,并调整现有的 YAML 文件,使其适用于该环境。

请记住,尽管我们以 AWS 为例,但我们讨论的所有元素都可以在其他云提供商中使用。查看它们的文档,看看它们是否更适合您。

我们还看到了如何部署 ELB,以便集群对公共接口可用,并如何在其上启用 HTTPS 支持。

我们讨论了部署的不同元素,以使集群更具弹性,并顺利部署新版本,不中断服务——既可以通过使用 HAProxy 快速启用或禁用服务,也可以确保以有序方式更改容器映像。

我们还介绍了自动缩放如何帮助合理利用资源,并允许您覆盖系统中的负载峰值,既可以通过创建更多的 pod,也可以通过在需要时向集群添加更多的 AWS 实例来增加资源,并在不需要时将其删除以避免不必要的成本。

在下一章中,我们将看到如何使用 GitOps 原则控制 Kubernetes 集群的状态,以确保对其进行的任何更改都经过适当审查和捕获。

问题

  1. 管理自己的 Kubernetes 集群的主要缺点是什么?

  2. 您能否列举一些具有托管 Kubernetes 解决方案的商业云提供商的名称?

  3. 有没有什么操作需要您执行才能推送到 AWS Docker 注册表?

  4. 我们使用什么工具来设置 EKS 集群?

  5. 在本章中,我们对先前章节的 YAML 文件进行了哪些主要更改?

  6. 在本章中,有哪些 Kubernetes 元素在集群中是不需要的?

  7. 为什么我们需要控制与 SSL 证书相关的 DNS?

  8. 活跃探针和就绪探针之间有什么区别?

  9. 为什么在生产环境中滚动更新很重要?

  10. 自动缩放 pod 和节点有什么区别?

  11. 在本章中,我们部署了自己的数据库容器。在生产中,这将发生变化,因为需要连接到已经存在的外部数据库。您将如何更改配置以实现这一点?

进一步阅读

要了解更多关于如何使用 AWS 的网络能力的信息,您可以查看书籍AWS Networking Cookbook (www.packtpub.com/eu/virtualization-and-cloud/aws-networking-cookbook)。要了解如何确保在 AWS 中设置安全系统,请阅读AWS: Security Best Practices on AWS (www.packtpub.com/eu/virtualization-and-cloud/aws-security-best-practices-aws)。

第八章:使用 GitOps 原则

在了解如何配置 Kubernetes 集群之后,我们将学习如何使用 GitOps 实践来进行配置,而不是应用手动命令和文件。GitOps 意味着使用 Git 存储库来管理集群配置,以存储和跟踪包含配置的 YAML 文件。我们将看到如何将 GitHub 存储库与集群链接,以便使用 Flux 定期更新。

这种方法允许我们以确定性的方式存储配置,以代码描述基础设施的更改。更改可以进行审查,并且集群可以从头开始恢复或复制,正如我们将在第九章 管理工作流中看到的那样。

本章将涵盖以下主题:

  • 理解 GitOps 的描述

  • 设置 Flux 以控制 Kubernetes 集群

  • 配置 GitHub

  • 通过 GitHub 进行 Kubernetes 集群更改

  • 在生产环境中工作

在本章结束时,您将了解如何将 Kubernetes 配置存储在 Git 存储库中,并自动应用合并到主分支的任何更改。

技术要求

本章示例的代码可在 GitHub 上找到:github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter08

您需要安装fluxctl工具。然后,我们将使用此工具手动同步并获取 SSH 密钥,以允许 Flux 与 Git 存储库进行交互。请参阅其文档中的安装方法:docs.fluxcd.io/en/stable/tutorials/get-started.html

理解 GitOps 的描述

运维中的一个传统大问题是确保不同服务器保持适当的配置。当您拥有一大批服务器时,部署服务并保持它们正确配置并不是一项简单的任务。

在本章中,我们将使用配置来描述服务以及在生产环境中运行所需的所有配置。这包括服务的特定版本,以及基础设施(操作系统版本,服务器数量等)或依赖服务的软件包和配置(负载均衡器,第三方库等)。

因此,配置管理将是进行更改的方式。

随着基础设施的增长,保持所有服务器上的配置跟踪是具有挑战性的。最常见的更改是部署服务的新版本,但还有其他可能性。例如,需要添加到负载均衡器的新服务器,用于修复安全漏洞的 NGINX 的新配置调整,或者用于启用功能的服务的新环境变量。

初始阶段是手动配置,但随着时间的推移,这变得难以做到。

管理配置

手动配置意味着团队中的某人跟踪少量服务器,并且在需要进行更改时,单独登录到每台服务器并进行所需的更改。

这种操作方式在多个服务器上需要大量工作,并且容易出错,因为它们可能很容易发散。

因此,一段时间后,可以通过使用 Fabric (www.fabfile.org/)或 Capistrano (capistranorb.com/)的一些脚本来改进。基本模型是将配置和新代码推送到服务器,并执行一些自动化任务,在最后重新启动服务。通常,这是直接从团队的计算机上作为手动步骤完成的。

代码和配置通常存在于 Git 上,但手动过程使得可以更改这一点,因为它是分离的。如果以这种方式工作,请确保只部署存储在源代码控制下的文件。

一些服务器维护的元素,如操作系统升级或更新库,可能仍然需要手动完成。

以下图表显示了代码是如何从进行配置更改的团队成员的计算机上推送的:

在这个阶段,可以通过手动添加新的基础设施,也可以使用诸如 Terraform(www.terraform.io/)这样的工具与云服务进行交互。

一个更复杂的选择是使用 Puppet(puppet.com/)或 Chef(www.chef.io/)等工具。它们采用客户端-服务器架构。它们允许我们使用自己的声明性语言描述服务器的状态,当服务器中的状态发生变化时,所有客户端都会更新以遵循定义。服务器将报告任何问题或偏差,并将集中配置定义。

这个过程总结在下面的图表中:

在某些情况下,这些工具可以在云服务中分配资源;例如,在 AWS 中添加一个新的 EC2 实例。

配置管理工具还有助于监控并执行一些纠正任务。例如,它可以重新启动应该运行的服务,或者在更改配置时出现问题时重试。

它也更适合于更多服务器的情况。

所有这些策略都需要专门的工具,通常由特定的运维团队处理。这使得开发人员无法配置,需要他们之间的协调才能进行配置更新。

这种工作分工的划分会产生一些摩擦,随着时间的推移,DevOps 运动提出了其他组织这项工作的方式。

理解 DevOps

传统的工作划分方式是创建一个控制基础设施和部署的运维团队,以及一个创建服务的开发团队。

这种方法的问题在于开发人员通常不会真正了解他们的代码在生产环境中是如何工作的,同时,运维人员也不会确切地知道部署包含什么。这可能导致“我不知道它是什么”/“我不知道它在哪里”的情况,两个团队之间存在鸿沟。DevOps 最终被创建为填补这一差距的方法。

一个典型的问题是一个服务在生产环境中经常失败,并被运维发现,运维会执行纠正策略(例如,重新启动服务)。

然而,开发团队并不确切知道是什么导致了失败,他们还有其他紧迫的任务,所以他们不会解决问题。

随着时间的推移,这可能会危及系统的稳定性。

DevOps 是一套旨在改善运营方面和开发方面之间协作的技术。它旨在通过使开发人员了解整个运营方面来实现快速部署,并尽可能地使用自动化来简化运营。

它的核心是赋予团队控制自己的基础设施和部署的能力,加快部署速度并了解基础设施以帮助及早识别问题。团队应该在部署和支持基础设施方面是自治的。

为了实现 DevOps 实践,您需要一些工具来以受控的方式控制不同的操作。GitOps 是一个有趣的选择,特别是如果您使用 Kubernetes。

定义 GitOps

GitOps 的想法很简单——我们使用 Git 来描述我们的基础设施和配置管理。对定义分支的任何更改都将触发相关的更改。

如果您能够通过代码定义整个系统,Git 会给您带来很多优势:

  • 对基础设施或配置管理的任何更改都是有版本的。它们是明确的,如果有问题可以回滚。版本之间的变化可以通过差异来观察,这是正常的 Git 操作。

  • Git 仓库可以作为备份,可以在底层硬件发生灾难性故障时实现从头恢复。

  • 这是最常见的源代码控制工具。公司里的每个人可能都知道它的工作原理并且可以使用它。它也很容易与现有的工作流程集成,比如审查。

GitOps 概念是由 Weaveworks 在一篇博客文章中引入并命名的(www.weave.works/blog/gitops-operations-by-pull-request)。从那时起,它在公司中被越来越多地使用。

虽然 GitOps 也可以应用于其他类型的部署(当然也已经应用了),但它与 Kubernetes 有很好的协同作用,这实际上是 Weaveworks 博客文章中的描述。

可以使用 YAML 文件完全配置 Kubernetes 集群,这几乎包含了整个系统的定义。正如我们在上一章中看到的,这可能包括诸如负载均衡器之类的元素的定义。Kubernetes 集群外的元素,比如外部 DNS,这些不包含在 YAML 文件中的元素,很少发生变化。

服务器和基础设施可以使用其他工具自动化,比如 Terraform,或者使用第七章中描述的自动化程序,配置和保护生产系统

出于实际原因,一些基础设施操作完全可以是手动的。例如,升级 EKS 集群的 Kubernetes 版本是一个可以通过 AWS 控制台完成的操作,而且很少发生,所以手动操作也是可以的。

这些操作保持手动也是可以的,因为自动化它们可能不会带来回报。

正如我们在第六章中看到的,Kubernetes 的 YAML 文件包含可以使用kubectl apply -f <file>命令应用的元素定义。Kubernetes 非常灵活,因为一个文件可以包含多个元素或一个元素。

将所有的 YAML 文件分组到一个目录结构下,并将它们纳入 Git 控制,这是一种非常明确的应用变更的方式。这是我们将要操作的方式。

这个操作并不复杂,但我们将使用一个现有的工具,由 Weaveworks 创建,叫做Flux

设置 Flux 来控制 Kubernetes 集群

Flux(github.com/fluxcd/flux)是一个工具,确保 Kubernetes 集群的状态与存储在 Git 仓库中的文件匹配。

它被部署在 Kubernetes 集群内部,作为另一个部署。它每 5 分钟运行一次,并与 Git 仓库和 Docker 注册表进行检查。然后,它应用任何变更。这有助于访问 Git 仓库,因为不需要在 CI 系统内部创建任何推送机制。

我们将看到如何在 Kubernetes 内部启动一个从 GitHub 仓库拉取的 Flux 容器。

启动系统

为了简单起见,我们将使用本地 Kubernetes。我们将使用第六章中描述的镜像,所以确保运行以下命令:

$ cd Chapter06
$ cd frontend
$ docker-compose build server
...
Successfully tagged thoughts_frontend:latest
$ cd ..
$ cd thoughts_backend/
$ docker-compose build server db
...
Successfully tagged thoughts_frontend:latest
$ cd ..
$ cd users_backend
$ docker-compose build server db
...
Successfully tagged users_server:latest

基本的 Kubernetes 配置存储在示例文件夹(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter08/example)子目录中。

您可以使用以下命令部署整个系统:

$ cd Chapter08/example
$ kubectl apply -f namespace.yaml
namespace/example created
$ kubectl apply -f . --recursive
deployment.apps/frontend created
ingress.extensions/frontend-ingress created
service/frontend-service created
namespace/example unchanged
deployment.apps/thoughts-backend created
ingress.extensions/thoughts-backend-ingress created
service/thoughts-service created
deployment.apps/users-backend created
ingress.extensions/users-backend-ingress created
service/users-service created

这创建了整个系统。

应用namespace.yaml文件以避免无法部署元素,因为命名空间不存在,但您可以两次运行kubectl apply -f . --recursive命令。

如果您检查系统,应该已经部署了,通过运行kubectl get pods命令显示:

$ kubectl get pods -n example
NAME                   READY STATUS  RESTARTS AGE
frontend-j75fp         1/1   Running 0        4m
frontend-n85fk         1/1   Running 0        4m
frontend-nqndl         1/1   Running 0        4m
frontend-xnljj         1/1   Running 0        4m
thoughts-backend-f7tq7 2/2   Running 0        4m
users-backend-7wzts    2/2   Running 0        4m

请注意,有四个frontend的副本。我们将在本章中更改 Pod 的数量,作为如何更改部署的示例。

现在,删除部署以从头开始:

$ kubectl delete namespace example
namespace "example" deleted

有关此设置的更多详细信息,请查看第六章中的在本地部署完整系统部分,使用 Kubernetes 进行本地开发

配置 Flux

我们将准备一个 Flux 系统,它将帮助我们跟踪我们的 Git 配置。我们根据这个存储库中的 Flux 示例准备了一个(github.com/fluxcd/flux/tree/master/deploy),它在Chapter08/flux子目录中可用。

主文件是flux-deployment.yaml。其中大部分是注释的样板文件,但请查看要从中提取的存储库的定义:

# Replace the following URL to change the Git repository used by Flux.
- --git-url=git@github.com:PacktPublishing/Hands-On-Docker-for-Microservices-with-Python.git
- --git-branch=master
# Include this if you want to restrict the manifests considered by flux
# to those under the following relative paths in the git repository
- --git-path=Chapter08/example

这些行告诉 Flux 要使用的存储库,分支和任何路径。如果路径被注释了,在您的情况下可能是这样,它将使用整个存储库。在下一节中,我们需要更改要使用的存储库为您自己的存储库。

请注意,我们使用flux命名空间来部署所有这些元素。您可以重用您的主要命名空间,或者如果对您更有效,可以使用默认命名空间。

要使用 Flux,请创建命名空间,然后应用完整的flux目录:

$ kubectl apply -f flux/namespace.yaml
namespace/flux created
$ kubectl apply -f flux/
serviceaccount/flux created
clusterrole.rbac.authorization.k8s.io/flux created
clusterrolebinding.rbac.authorization.k8s.io/flux created
deployment.apps/flux created
secret/flux-git-deploy created
deployment.apps/memcached created
service/memcached created
namespace/flux unchanged

使用以下代码,您可以检查一切是否按预期运行:

$ kubectl get pods -n flux
NAME                       READY STATUS  RESTARTS AGE
flux-75fff6bbf7-bfnq6      1/1   Running 0        34s
memcached-84f9f4d566-jv6gp 1/1   Running 0        34s

但是,要能够从 Git 存储库部署,我们需要对其进行配置。

配置 GitHub

虽然我们可以配置任何 Git 存储库,但通常,我们将使用 GitHub 进行设置。我们需要设置一个有效的密钥来访问 Git 存储库。

这样做的最简单方法是允许 Flux 生成自己的密钥,并将其添加到 GitHub 存储库。但是,为了能够这样做,我们需要创建自己的 GitHub 存储库。

分叉 GitHub 存储库

配置存储库的第一步是分叉。让我们查看更多详细信息的以下步骤:

  1. 转到 GitHub 代码的页面(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/),然后单击右上角的 Fork 以生成您自己的副本。

  2. 一旦您拥有自己的副本,它将具有类似以下的 URL:

https://github.com/<YOUR GITHUB USER>/Hands-On-Docker-for-Microservices-with-Python/
  1. 现在,您需要在Chapter08/flux/flux-deployment.yaml文件中替换它为--git-url参数。

  2. 更改后,使用以下命令重新应用 Flux 配置:

$ kubectl apply -f flux/flux-deployment.yaml
deployment.apps/flux changed

现在,Flux 正在跟踪您完全控制的自己的存储库,并且您可以对其进行更改。首先,我们需要允许 Flux 访问 GitHub 存储库,可以通过部署密钥实现。

添加部署密钥

为了允许 Flux 访问 GitHub,我们需要将其秘钥添加为有效的部署密钥。使用fluxctl,很容易获取当前的ssh秘钥;只需运行以下命令:

$ fluxctl identity --k8s-fwd-ns flux
ssh-rsa <secret key>

有了这些信息,转到您分叉的 GitHub 项目的“设置|部署密钥”部分。使用描述性名称填写标题,使用之前获取的秘钥填写密钥部分,然后选择“添加密钥”:

一定要选择“允许写入访问”的复选框。现在,Flux 将能够联系 GitHub。

下一步是在 GitHub 和集群上同步状态。

同步 Flux

我们可以与 Flux 同步,因此 GitHub 中的描述将应用于集群,使用以下命令:

$ fluxctl sync --k8s-fwd-ns flux
Synchronizing with git@github.com:<repo>.git
Revision of master to apply is daf1b12
Waiting for daf1b12 to be applied ...
Done.
Macbook Pro:Chapter08 $ kubectl get pods -n example
NAME                   READY STATUS  RESTARTS AGE
frontend-8srpc         1/1   Running 0        24s
frontend-cfrvk         1/1   Running 0        24s
frontend-kk4hj         1/1   Running 0        24s
frontend-vq4vf         1/1   Running 0        24s
thoughts-backend-zz8jw 2/2   Running 0        24s
users-backend-jrvcr    2/2   Running 0        24s

同步需要一点时间,可能会出现错误,指出正在克隆存储库:

$ fluxctl sync --k8s-fwd-ns flux
Error: git repository git@github.com:<repo>.git is not ready to sync (status: cloned)
Run 'fluxctl sync --help' for usage

等待几分钟,然后重试:

$ fluxctl sync --k8s-fwd-ns flux
Synchronizing with git@github.com:<repo>.git
Revision of master to apply is daf1b12
Waiting for daf1b12 to be applied ...
Done.
$

您的 Flux 部署,因此本地 Kubernetes 集群现在与 Git 中的配置同步,并将随任何更改更新。

通过 GitHub 进行 Kubernetes 集群更改

通过 Flux,您的本地 Kubernetes 集群将更新以反映 Git 存储库中的更改。几分钟后,Git 中的任何更改都将传播到集群。

让我们通过测试来查看这一点,更新前端部署中的 Pod 数量:

  1. 按照以下描述更改您分叉的存储库中的Chapter08/example/frontend/deployment.yaml文件:
---
apiVersion: apps/v1
kind: Deployment
metadata:
    name: frontend
    labels:
        app: frontend
    namespace: example
spec:
    replicas: 2

这将将副本的数量从4更改为2

  1. 将更改提交到master分支并推送到 GitHub 仓库。

  2. 使用以下命令监视集群:

$ kubectl get pods -n example -w

几分钟后,您将看到前端 Pod 的数量减少。您可以通过手动同步 Flux 来加快速度。

  1. 撤消更改并查看它们将如何被添加。

Flux 不会删除元素以避免问题。这意味着删除部署或服务文件不会从存储库中删除它。要这样做,您需要手动删除它。

您可以通过将副本的数量设置为零来禁用由部署控制的 Pod。

恭喜!您现在拥有一个由 GitHub 存储库控制的集群。

让我们看看如何在生产环境中有效地使用这种方法。

在生产中工作

GitOps 主要针对生产环境,这些环境比本章中使用的示例本地集群更大更复杂。在本节中,我们将描述如何利用 Git 的优势来提高部署和更改的清晰度,以及如何确保我们在源代码控制下结构化不同文件以避免混乱。

创建结构

对于大型部署来说,结构化 YAML 文件至关重要。从技术上讲,您可以将所有内容合并到一个文件中,但当它增长时,这并不是处理它的最佳方式。Kubernetes 允许极大的灵活性,因此请尝试找到适合您的结构。

一个简单的方法是按命名空间和微服务创建子目录。这是我们在本示例中的结构方式。这种结构将相关元素放在一起,并为任何涉及微服务的人提供了清晰的路径。如果部署仅影响一个微服务(正如我们在第一章中讨论的那样,进行移动-设计、计划和执行,在并行部署和开发速度部分),这将使更改保持在同一个子目录中。

但不要感到受限于这种结构。如果对您有意义,您可以尝试一些不同的东西;例如,按元素进行划分,即将所有部署放在一个目录下,所有服务放在另一个目录下,依此类推。不要害怕尝试和移动元素,寻找项目的最佳结构。

所有这些文件都在 GitHub 中受源代码控制,这使我们能够利用它们的功能。

使用 GitHub 功能

考虑到任何对主分支的合并都会触发集群的变化,这在上线之前应该进行审查。

您可以通过要求需要批准的拉取请求来进行。批准可以来自专门跟踪集群的 Ops 团队,也可以来自微服务的所有者;例如,团队领导或经理。

您可以在 GitHub 中本地强制执行代码所有者。这意味着特定文件或目录的更改需要某个用户或团队批准。查看 GitHub 文档以获取更多信息(help.github.com/en/articles/about-code-owners)。

单个 GitHub 存储库也可以跟踪多个环境,例如,用于运行测试的暂存环境和向客户提供的生产环境。您可以通过分支或子目录来划分它们。

但 GitHub 功能并不是唯一可用的,常规的 Git 标签非常灵活,可以让我们定义要部署的特定容器。

使用标签

在本例中,我们使用了图像的latest标签。这使用了最近构建的容器,每次构建图像时都可能会发生变化。对于生产环境,我们应该使用与不可变容器相关联的特定标签,正如我们在第三章中讨论的那样,在使用远程注册表部分,以及在第四章中的创建流水线和工作流程部分中讨论的那样。

这意味着替换以下行:

spec:
  containers:
  - name: frontend-service
    image: thoughts_frontend:latest

我们用以下行替换它们:

spec:
  containers:
  - name: frontend-service
    image: <registry>/thoughts_frontend:v1.5

这就是能够以受控方式更新图像的优势所在。您将使用流水线(如第四章中所述的创建流水线和工作流程)构建和推送带标记的图像到远程注册表,然后您可以控制在集群中部署哪个特定版本。

在某些情况下,可能需要停止同步。Flux 使用工作负载的概念,这些工作负载是可更新的元素,与部署的方式相同。

你可以停止它们的自动更新或控制它们的更新方式。有关更多信息,请参阅文档:github.com/fluxcd/flux/blob/master/docs/using/fluxctl.md#workloads

将此版本置于 Git 控制之下,使开发人员能够轻松地恢复到以前的版本。

为了遵循持续集成原则,尝试进行小的更改并快速应用。Git 将帮助您撤消不良更改,但小的增量更改易于测试,并减少了破坏系统的风险。

大多数操作将是简单的更改,要么更改要部署的图像的版本,要么调整参数,例如副本的数量或环境变量。

总结

我们从回顾最常见的不同类型的配置管理策略开始本章,并讨论了它们在项目增长时的应用方式。我们讨论了 DevOps 方法如何使团队承担起部署的责任,并有助于填补开发和运维之间的传统差距。

我们看到了最新的 GitOps 方法在 Kubernetes 集群中运行得非常好,因为配置被紧密描述为一组文件。我们讨论了使用 Git 跟踪配置的优势。

我们介绍了 Flux,这是一个部署在集群内并从 Git 存储库分支中拉取更改的工具。我们提供了一个示例配置,在本地 Kubernetes 集群中部署了它,并配置了 GitHub 以便与其一起工作。这样一来,GitHub 中对 Git 分支的任何推送都会在本地集群中反映出来。

我们在本章结束时介绍了一些在生产环境中工作的策略。我们研究了确保 Kubernetes YAML 文件结构正确,利用 GitHub 功能的方法,并学习了如何发布和回滚带标记的图像。

在下一章中,我们将描述集群的完整开发周期的过程,从引入新功能到在生产环境中部署。我们将描述在实时系统中工作时的一些有用策略,以确保部署的代码运行顺畅且质量高。

问题

  1. 使用脚本将新代码推送到服务器和使用 Puppet 等配置管理工具有何区别?

  2. DevOps 的核心理念是什么?

  3. 使用 GitOps 的优势是什么?

  4. GitOps 只能在 Kubernetes 集群中使用吗?

  5. Flux 部署位于何处?

  6. 为了允许 Flux 访问 GitHub,您需要在 GitHub 中配置什么?

  7. 在生产环境中工作时,GitHub 提供了哪些功能可以帮助确保对部署的控制?

进一步阅读

您可以在以下书籍中了解更多关于 DevOps 实践和理念:实用 DevOps-第二版 (www.packtpub.com/virtualization-and-cloud/practical-devops-second-edition),以及DevOps 悖论 (www.packtpub.com/web-development/devops-paradox)。

第九章:管理工作流程

在本章中,我们将把前几章描述的不同流程汇总到一般工作流程中,以便对单个微服务进行更改。我们将从获取新功能请求的过程转移到本地开发、审查、在演示环境中测试,并批准更改并将其发布到实时集群。

这与我们在第四章中介绍的流水线概念有关,创建流水线和工作流。然而,在本章中,我们将讨论任务的过程。流水线和构建结构旨在确保任何提议的更改都符合质量标准。在本章中,我们将重点关注技术的团队合作方面,以及如何在跟踪不同更改的同时实现顺畅的互动。

在本章中,我们将涵盖以下主题:

  • 理解功能的生命周期

  • 审查和批准新功能

  • 设置多个环境

  • 扩展工作流程并使其正常运行

本章结束时,我们将清楚地了解设置新功能所涉及的不同步骤,以及如何使用多个环境来测试和确保发布成功。

理解功能的生命周期

遵循敏捷原则,任何团队的主要目标是能够快速实现新功能,而不会影响系统的质量或稳定性。变化的第一个元素是功能请求

功能请求是以非技术术语描述系统变更的请求。功能请求通常由非工程师(产品所有者、经理和 CEO)生成,他们希望出于业务原因改进系统,比如打造更好的产品或增加收入。

功能请求可能很简单,比如更新公司主页的标志,也可能很大且复杂,比如添加对新的 5G 网络的支持。功能请求可能包括错误报告。虽然通常不会,但在本章中会有。

复杂的功能请求可能需要分解为更小的独立功能请求,以便我们可以逐步迭代。

我们的重点是微服务方法和实践,而不是敏捷实践。这些实践涉及如何将功能请求结构化为任务和估算,但并不特定于基础技术。

请查看本章末尾的进一步阅读部分,以了解更多关于敏捷实践和方法论的信息。

在单体架构中,所有元素都在同一个代码库下。因此,无论特定功能请求有多复杂,只会影响一个系统。在单体架构中只有一个系统。然而,一旦我们迁移到微服务,情况就不同了。

在微服务架构中,我们需要分析任何新功能请求涉及的微服务。如果我们正确设计了微服务,大多数请求只会影响单个微服务。然而,最终,一些功能请求将太大,无法完全适应单个微服务,需要分成两个或更多步骤,每个步骤都改变不同的微服务。

例如,如果我们有一个新的功能请求,允许我们在一条想法的文本中提及用户(类似于 Twitter 上的提及方式),那么这个提及将需要存储在 Thoughts 后端,并在前端显示。这个功能影响了两个微服务:前端和 Thoughts 后端。

在本节中,我们引用了前几章介绍的概念,并从全局角度将它们结合起来。

在下一小节中,我们将看看影响多个微服务的特性。

影响多个微服务的特性

对于多个微服务的特性请求,您需要将特性分成几个技术特性,每个特性影响一个单独的微服务。

每个技术特性应涵盖与其影响的微服务相关的方面。如果每个微服务都有明确的目的和目标,那么特性将被完成和概括,以便以后的请求可以使用。

成功的微服务架构的基础是有松散耦合的服务。确保每个微服务的 API 本身是有意义的,这一点很重要,如果我们希望避免模糊服务之间的界限。不这样做可能意味着独立的工作和部署是不被允许的。

还应考虑请求和微服务之间的依赖关系,以便工作可以从后往前安排。这意味着准备添加额外数据或功能的新特性,但默认情况下保留旧行为。在这样做之后,可以部署使用这些额外数据的新特性。这种工作方式确保了任何给定时间的向后兼容性。

我们将在第十一章中更详细地查看影响多个微服务的特性,处理系统中的变更、依赖和秘密。我们还将学习如何更详细地协调工作和依赖关系。

回到我们之前的例子,要将用户的提及添加到他们的想法中,我们需要使 Thoughts Backend 能够处理对用户的可选引用。这是一个独立的任务,不会影响现有的功能。它可以被部署和测试。

然后,我们可以在前端进行相应的更改,以允许外部用户通过 HTML 界面与其进行交互。

正如我们在第一章中讨论的,迁移-设计、计划和执行,对于任何微服务架构来说,独立部署服务是至关重要的。这使我们能够独立测试服务,并避免任何需要复杂部署的开销,这使得我们难以调试和回滚错误。

如果不同的团队独立地在不同的微服务上工作,那么它们也将需要协调。

在下一节中,我们将学习如何在单个微服务中实施特性。

实施特性

一旦我们有了独立技术特性的定义,就可以实施它。

清晰地定义技术特性可能是具有挑战性的。请记住,一个特性可能需要进一步细分为更小的任务。然而,正如我们之前提到的,这里的目标不是构建我们的任务结构。

通过创建一个新的 Git 分支来开始您的任务。代码可以被更改以反映这个分支中的新特性。正如我们在第二章和第三章中所看到的,使用 Python 创建 REST 服务使用 Docker 构建、运行和测试您的服务,可以运行单元测试来确保这项工作不会破坏构建。

正如我们在第三章中所描述的,使用 Docker 构建、运行和测试您的服务,在使用不可变容器进行操作部分,我们可以使用pytest参数来运行测试的子集,以加快开发速度,从而在运行测试时获得快速反馈。确保您使用它。

这个功能在整个系统中的工作方式可以通过部署本地集群来检查。这会启动其他可能受到这个分支工作影响的微服务,但它有助于确保当前的工作不会破坏影响其他微服务的现有调用。

根据流水线,推送到 Git 的任何提交都将运行所有测试。 这将及早发现问题,并确保在与主分支合并之前构建正确。

在此过程中,我们可以使用拉取请求来审查主分支和新功能之间的更改。 我们可以检查我们的 GitHub 配置,以确保代码在合并之前处于良好状态。

一旦功能准备就绪并已与主分支合并,应创建一个新标签以允许其部署。 作为配置的一部分,此标签将触发生成注册表中的图像的构建,并使用相同的标签标记图像。 标签和图像是不可变的,因此我们可以确保代码在不同环境之间不会更改。 您可以放心地前进和后退,确保代码与标签中定义的完全相同的代码。

正如我们在第八章中看到的,使用 GitOps 原则,可以通过遵循 GitOps 原则部署标签。 部署在 Kubernetes 配置文件中,受 Git 控制,并在需要获得批准的拉取请求中进行审查。 一旦拉取请求已与主分支合并,Flux 将自动部署,正如我们在第八章中描述的那样,在设置 Flux 控制 Kubernetes 集群部分。 此时,功能在集群中可用。

让我们回顾一下这个生命周期,从技术请求的描述到部署到集群为止:

这是我们在第四章中介绍的流程的更完整版本。

  1. 技术请求已准备好实施到单个微服务中。

  2. 创建一个新的功能分支。

  3. 在此分支中更改微服务的代码,直到功能准备就绪。

  4. 创建了一个拉取请求,用于将功能分支合并到主分支中。 正如我们在第四章中描述的那样,在理解持续集成实践部分,运行 CI 流程以确保其质量。

  5. 拉取请求已经审查,批准并合并到主分支。

  6. 创建了一个新标签。

  7. 在 GitOps 存储库中创建一个部署分支,将微服务的版本更改为新标签。

  8. 创建用于合并此部署分支的拉取请求。 然后进行审查和合并。

  9. 一旦代码已合并,集群将自动发布微服务的新版本。

  10. 最后,新功能在集群中可用!

这是生命周期的简化版本; 实际上,可能更复杂。 在本章后面,我们将看到需要将生命周期部署到多个集群的情况。

在接下来的部分中,我们将看一些关于审查和批准拉取请求的建议。

审查和批准新功能

根据我们在第四章中描述的流水线模型,候选代码通过一系列阶段,如果出现问题就停止。

正如我们之前提到的,使用 GitHub 拉取请求进行审查适用于我们希望向微服务代码引入新功能,以及希望通过 GitOps 实践将这些更改部署到集群中。

在这两种情况下,我们可以通过自动化测试和流程自动检查。 但是,还有最后一步需要手动干预:知识转移和额外的眼睛。 一旦审阅者认为新功能已准备就绪,他们可以批准它。

工具是一样的,尽管审查过程有些不同。这是因为目标不同。对于功能代码,审查更加开放,直到获得批准并合并到主分支。另一方面,审查和批准发布通常更加直接和快速。

让我们从学习如何审查功能代码开始。

审查功能代码

代码审查可以在开发功能并打开合并请求时启动。正如我们已经看到的,在 GitHub 中,代码可以在拉取请求阶段进行审查。

代码审查基本上是关于代码和新功能的讨论;也就是说,在将代码引入主分支之前,我们会对代码进行检查。这为我们提供了在开发过程中改进功能的机会,以及在其成为系统组件之前进行改进。

在这里,团队的成员可以阅读尚未合并的代码,并给作者一些反馈。这可能来回进行,直到审阅者认为代码已经准备好合并并批准它。实质上,除了功能的作者之外,其他人需要同意新代码符合所需的标准。

代码库随着时间的推移而增长,它们的组件可以相互帮助。将代码合并到主分支表示您完全接受新代码将作为代码库的一部分由团队维护。

代码可能需要得到一个或多个人的批准,或者特定人员的批准。

在 GitHub 中,你可以启用代码所有者。这些是负责批准存储库或存储库部分的工程师。查看 GitHub 文档以获取更多信息:help.github.com/en/articles/about-code-owners

代码审查是一个非常常见的过程,而在 GitHub 中使用拉取请求的流行度和便利性已经传播开来。大多数开发人员都熟悉这个过程。

实施良好的反馈文化比看起来更加困难。编写代码是一种深层次的个人体验;没有两个人会写出相同的代码。对开发人员来说,让他人批评自己的代码可能是一种困难的经历,除非有明确的规则。

以下是一些建议:

  • 告诉你的审阅者他们应该寻找什么。坚持使用检查表。这有助于在团队内部培养关心共享核心价值观的文化。这也有助于初级开发人员知道要寻找什么。这可能会因团队而异,但以下是一些示例:

  • 有新的测试。

  • 错误条件要经过测试。

  • 文档要得到适当的更新。

  • 任何新的端点都要符合标准。

  • 架构图已更新。

  • 审查代码并不等同于编写代码。总会有差异(例如,这个变量名可以更改),但需要审查的是是否需要实施这样的更改。挑剔将会侵蚀团队成员之间的信任。

  • 要审查的代码越大,就越难以完成。最好是以小的增量工作,这与持续集成的原则相符。

  • 所有的代码都应该在同等的基础上进行审查。这包括高级开发人员的代码,应鼓励初级开发人员留下诚实的反馈。这有助于代码的所有权和公平性增长。

  • 代码审查是一种对话。评论并不一定意味着审阅者的反馈必须在你质疑之前立即实施。它开启了关于改进代码的对话,进行澄清和反驳是完全可以的。有时,处理请求的正确方式,也就是更改代码的一部分,是留下一条评论解释为什么以这种方式进行。

  • 审查有助于传播关于代码库的知识。然而,这并不是万能的。代码审查往往会陷入隧道视野,只关注诸如拼写错误和局部代码片段等小问题,而不关注更大的元素。这就是为什么以小的增量实现功能很重要的原因:以帮助周围的人消化变化。

  • 留下赞赏的评论很重要。营造一个欣赏写得好的代码的文化。只强调问题会让作者对审查过程感到痛苦。

  • 批评应该针对代码,而不是针对编码人员。确保您的审查是文明的。在这一步中,我们要确保代码质量高;作为审查人,您不希望让自己显得更优越。

对于那些不习惯代码审查的人来说,代码审查可能会带来压力。一些公司正在制定原则和想法,以减轻这一过程的痛苦。一个很好的例子可以在www.recurse.com/social-rules找到。不要害怕制定并分享您自己的原则。

  • 重要的是,代码可以随时获得批准,即使团队中的某人正在度假或生病。确保您授予团队多名成员批准,以便批准过程本身不成为瓶颈。

当您开始进行代码审查时,请确保团队领导牢记这些考虑,并强调为什么所有代码都要经过审查。

强调代码审查并不是技术解决方案,而是与人相关的解决方案。因此,它们可能会受到与人相关的问题的影响,比如自负、对立的讨论或无效的辩论。

微服务架构适用于有多人共同开发的大型系统。团队合作至关重要。其中一部分是确保代码不属于单个人,而是整个团队的。代码审查是实现这一目标的好工具,但一定要积极寻找健康的审查。

随着时间的推移,将形成共识,并且会一致地开发大量代码。在一个健康的团队中,花在审查上的时间应该减少。

随着时间的推移,团队将定期进行代码审查,但在开始阶段建立这些基础可能会很复杂。确保您留出时间来介绍它们。正如我们之前提到的,一旦功能准备就绪,我们需要继续批准它。批准新功能的代码并将其合并到主分支是功能审查的最后阶段,但仍然需要发布。发布受代码控制,也需要进行审查。

批准发布

使用 GitOps 原则使我们能够启用相同的审查和批准方法,以便我们可以对 Kubernetes 基础架构进行更改。正如我们之前提到的,基础架构是由 Kubernetes 中的 YAML 文件定义的,这使我们能够控制这些更改。

对 Kubernetes 集群进行的任何更改都可以经过拉取请求和审查方法。这使得批准将发布到集群变得简单。

这有助于最小化问题,因为团队的成员参与了更改,并且他们对基础架构的了解更加深入。这与 DevOps 原则很好地契合,允许团队掌控自己的部署和基础架构。

然而,GitOps 中的基础架构更改往往比常规代码审查更容易审查。一般来说,它们是以非常小的增量进行的,大多数更改都是如此直截了当,几乎不会引发辩论的可能性很小。

一般原则是,尽量使基础架构更改尽可能小。基础架构更改存在风险,因为错误可能导致其中的重要部分崩溃。更改越小,风险越小,诊断任何问题也就越容易。

我们之前提到的关于代码审查的所有建议也都有一定作用。其中最重要的一个是包括一些参考基础设施关键部分的指南。

基础设施的某些部分可能受到 GitHub 代码所有者的保护。这使得某些工程师必须批准对基础设施关键部分的更改。查看更多信息,请参阅文档:help.github.com/en/articles/about-code-owners

由于基础设施被定义为存储在 GitHub 中的代码,这也使得复制基础设施变得容易,从而极大地简化了生成多个环境的过程。

设置多个环境

在 Kubernetes 下创建、复制和删除命名空间的便利大大减轻了以前保持多个环境副本以复制基础设施的负担。您可以利用这一点。

根据我们之前提到的 GitOps 原则,我们可以定义新的命名空间来生成新的集群。我们可以使用另一个分支(例如,使用master分支用于生产集群,demo用于演示集群),或者复制包含集群定义的文件并更改命名空间。

可以为不同的目的使用不同的物理 Kubernetes 集群。最好将生产集群保持独立,不与任何其他环境共享,以减少风险。然而,其他每个环境可以存在于同一个集群中,这不会影响外部客户。

一些功能请求足以证明开发团队将确切知道该做什么,比如在处理错误报告时。然而,其他可能需要更多的测试和沟通,以确保它们在开发过程中满足要求。当我们检查新功能是否确实对预期的外部用户有用时,或者可能是更具探索性的功能时,就可能出现这种情况。在这种情况下,我们需要联系外部方,也就是功能的最终批准者:利益相关者

利益相关者是项目管理中的一个术语,指定了第三方,也就是产品的最终用户或受其影响的用户。在这里,我们使用这个术语来指定对功能感兴趣但不属于团队外部的人,因此他们无法从内部定义功能要求。利益相关者可以是例如经理、客户、公司的 CEO 或内部工具的用户。

任何曾经不得不处理模糊定义的利益相关者请求的开发人员,比如允许按名称搜索,都不得不对其进行微调:不是按名字,而是按姓氏

确保您为这类任务定义适当的结束。如果允许其无限制地运行,利益相关者的反馈可能是无穷无尽的。事先定义其中包含和不包含的内容,以及任何截止日期。

为了运行测试并确保正在开发的功能朝着正确的方向发展,您可以创建一个或多个演示环境,在这些环境中,您将部署尚未合并到主分支中的工作。这将帮助我们与利益相关者分享这项工作,以便他们在功能完成之前向我们提供反馈,而无需我们在生产环境中发布它。

正如我们在前几章中看到的,在 Kubernetes 中生成新环境很容易。我们需要创建一个新的命名空间,然后复制集群的生产定义,同时更改命名空间。这将创建一个环境的副本。

更改正在开发的微服务的特定版本将允许我们创建其工作版本。新版本可以像往常一样部署在这个演示环境中。

这是一个简化版本。您可能需要在生产环境和演示环境之间进行更改,例如副本数量和数据库设置。在这种情况下,可以使用模板环境作为参考,以便随时可以复制。

其他环境,如暂存,可以以类似的方式创建,旨在创建确保已部署到生产环境的代码将按预期工作的测试。这些测试可以是自动的,但如果我们想要检查用户体验是否合适,也可以是手动的。

暂存环境是一个尽可能忠实于生产环境的副本设置,这意味着我们可以运行测试,以确保在生产环境中部署将正常工作。暂存通常帮助我们验证部署过程是否正确,以及任何最终测试。

暂存环境通常非常昂贵。毕竟,它们是生产环境的副本。使用 Kubernetes,您可以轻松复制生产环境并减少所需的物理基础设施。甚至可以在不使用时启动和停止它以减少成本。

您可以使用多个环境以类似的方式创建部署的级联结构。这意味着需要将标签部署到暂存环境并获得批准,然后才能部署到生产环境。

现在让我们从开发人员的角度来看如何处理这个结构。

扩展工作流并使其正常工作

实施这种工作方式的一些挑战包括创建提供充分反馈循环的文化,并在快速审查新代码时仔细检查它。等待审查是一种阻塞状态,会阻止开发人员实施正在审查的功能。

虽然这段等待时间可以用于其他目的,但无法取得进展会迅速降低生产率。开发人员要么会同时保留几个功能,这在上下文切换的角度来看是非常有问题的,要么他们需要等待并无所事事,直到审查完成。

上下文切换可能是生产力的最严重杀手。保持团队的生产力高的关键之一是能够开始并完成任务。如果任务足够小,它将很快完成,因此在项目之间切换更容易。然而,同时处理两个或更多任务是非常不好的做法。

如果这经常发生,请尝试将任务分解为较小的块。

为了能够彻底审查代码并减少阻塞时间,有一些要点需要牢记。

审查和批准由整个团队完成

必须随时有足够的审阅者可用。如果只有开发人员有经验,审查可能最终只由团队中最资深的人员完成,例如团队负责人。尽管这个人原则上可能是更好的审阅者,但从长远来看,这种结构将损害团队,因为审阅者将无法做其他事情。如果审阅者因病或度假等原因不可用,开发和发布阶段的进展也将受阻。

相反,使整个团队都能够审查其同行的代码。即使资深贡献者在教导团队其他成员如何审查方面扮演更积极的角色,但一段时间后,大多数审查不应需要他们的帮助。

尽管最初实施这个流程需要积极的指导,但这通常由团队的资深成员来领导。审查代码是一种可培训的能力,其目标是在一段时间后,每个人都能够进行审查并获准批准拉取请求。

部署拉取请求也遵循相同的流程。最终,团队中的每个人,或者至少是相当数量的成员,都应该能够部署一个发布。不过,最初的主要审阅者可能会是不同的人。

最适合审查发布的候选人可能是对 Kubernetes 基础设施配置非常了解,但对微服务代码不是专家。

理解并不是每个批准都是一样的

记住,一个功能的不同阶段并不同样关键。代码审查的早期过程是为了确保代码可读,并且保持质量标准。在早期阶段,代码将有相对较多的注释,并且会有更多需要讨论的地方,因为需要调整的元素更多。

审查的一个重要部分是创建足够易懂的代码,以便团队的其他成员能够理解。尽管有些人声称代码审查可以让每个人都意识到团队其他成员正在实施的更改,但根据我的经验,审阅者并不那么了解特定功能。

然而,一个良好的审查将确保没有令人费解的东西被引入到代码库中,并且核心元素得到尊重(例如引入测试,保持文档更新,保持代码可读)。正如我们在本章前面建议的那样,尝试创建一个明确的检查事项列表。这将有助于使审查和代码更加一致。

新功能的部署阶段只需要检查微服务版本的更改以及基础设施的其余部分是否完好。这些通常会非常小;大多数情况下会再次检查是否有拼写错误,以及要更改的微服务是否正确。

定义发布的明确路径

拥有一个简单明了的流程可以帮助所有参与者清楚地了解一个功能是如何从开发到发布到生产环境的。例如,基于我们讨论过的想法,我们可能会得到一个类似于以下图表所示的部署路径:

对于这些步骤中的每一步,我们需要验证该步骤是否正确。正如我们在第四章中所看到的,创建流水线和工作流,自动化测试确保合并到主分支的任何内容都不会破坏现有的构建。这涵盖了前面的图表直到创建标签步骤。

同样,可能有一种方法可以在部署后验证部署是否成功。以下是关于此的一些想法:

  • 手动测试,以检查部署的微服务是否按预期工作

  • 自动化测试,比如第四章中描述的那些,创建流水线和工作流

  • 检查要部署的图像是否已经使用 Kubernetes 工具或版本 API 正确部署

一旦一个部署阶段成功完成,就可以开始下一个阶段。

在非生产环境中进行部署可以最大程度地减少破坏生产环境的风险,因为这将确保部署过程是正确的。流程需要足够快,以便允许快速部署,从而使它们尽可能小。

从合并到主分支到新版本发布到生产环境,整个过程应该不超过几个小时,但最好是更短。

如果需要更多时间,那么这个流程可能太繁重了。

小而频繁的部署将最大程度地减少破坏生产环境的风险。在一些特殊情况下,常规流程可能会很慢,需要使用紧急程序。

紧急发布

让我们假设在生产中有一个关键错误,需要尽快解决。对于这些特殊情况,事先定义一个紧急流程是可以的。

这种紧急流程可能涉及加快审查甚至完全跳过审查。这可能包括跳过中间发布(例如在事先不部署到演示环境)。确保明确定义何时需要使用此流程,并确保仅在紧急情况下使用。

如果您的常规部署流程足够快,那么就不需要紧急流程。这是尝试提高部署时间的一个很好的理由。

回滚是一个很好的例子。要撤销微服务的部署,因为上一个版本引入了关键错误,只需在生产环境中回滚并返回到上一个版本,而不影响其他任何东西,这是一个合理的流程。

请注意,这里我们减少了进行快速更改的风险,并确保已经回滚的版本已经在之前部署过。这是紧急程序可能起作用并减少风险的一个很好的例子。

在发现特殊情况时要运用常识,并与团队事先讨论如何处理。我们将在第十二章 跨团队协作和沟通 中讨论回顾。

频繁发布和添加功能标志

虽然回滚是可能的,正如我们刚才看到的,但一般共识应该是每次新部署都是向前推进的。新版本的代码包含了上一个版本的代码,再加上一些小的更改。按照 Git 的操作方式,我们在一个分支上工作(主分支),并将其推进。

这意味着要避免几个长期存在的活跃分支。这种模式被称为基于主干的开发,是持续集成的推荐工作方式。在基于主干的开发中,功能分支是短暂存在的,并且始终与主分支(或主干)合并,通常在 Git 中称为master

基于主干的开发避免了当我们有长期存在且与主分支分歧的分支时出现的问题,从而使多个组件的集成变得复杂。持续集成的基础是始终具有可以以小的增量发布的代码。这种模式以“主干”作为发布的参考。

在下图中,我们可以看到功能 A是如何合并到主分支中的,以及功能 B仍在进行中。任何发布都将来自主分支

如果功能 A引入了一个错误,一个新的 bug 修复分支将从主分支分支出来,并将被合并回去。请注意结构是继续向前推进的。

为了使这个系统工作,功能分支需要短暂存在 - 通常只有几天。这样做可以使合并变得容易,并允许进行小的增量更改,这是持续集成的关键。

使用功能标志

有时,有一些功能,由设计需要一次性进行大规模/重大更改,比如新的 UI 界面。持续集成倡导者提倡的短小迭代周期,逐渐添加小功能的方式在这些频繁发布的情况下行不通。新界面需要一次性包含所有元素,否则会显得奇怪。

当您希望以小的增量方式继续工作,并同时延迟功能的激活时,可以使用功能标志。

功能标志是启用或禁用特定功能的配置元素。这使您可以通过配置更改改变微服务的行为,起到开关的作用。

在 Kubernetes 中,我们使用deployment.yaml文件来描述环境变量,以及 ConfigMaps。我们将在第十一章《处理系统中的变更、依赖关系和机密信息》中讨论 ConfigMaps。

配置与每个单独的环境相关联。这使我们能够在特定环境中展示一个功能,而在另一个环境中不展示,同时代码库保持不变。

例如,可以慢慢开发并在功能标志下保护一个新接口。一些环境,比如演示环境,仍然可以处于活动状态,以便收集内部反馈,但这不会显示在生产环境中。

一旦新接口准备就绪,就可以进行小的更改;例如,我们可以更改配置参数以启用它。这在外部看起来可能是一个很大的变化,但如果我们切换回参数,它可以很容易地恢复。

功能标志在处理外部可访问服务时非常有用。内部服务可以添加更多功能而不会出现任何问题,因为它们只会被系统中的其他微服务调用。

内部微服务通常可以添加新功能。在这里,会尊重向后兼容性。外部可访问的功能有时需要我们出于各种原因(包括接口更改或产品弃用)用另一个功能替换一个功能。

一个相关的方法是将功能推送给一部分用户。这可以是预定义的用户集,例如已经加入测试计划以获得早期功能访问权限的用户,或者是一个随机样本,以便他们可以在全球发布之前及早发现问题。

一些大公司也使用区域访问,其中一些功能首先在特定国家/地区启用。

一旦功能标志被激活,任何已弃用的功能都可以被移除和清理,这样就不会有不会被使用的旧代码了。

处理数据库迁移

数据库迁移是对存储在特定环境中的持久数据进行的更改(通常是在一个或多个数据库中)。大多数情况下,这意味着改变数据库模式,但也有其他情况。

生产环境中的数据是运行系统中最重要的资产。对数据库迁移需要特别小心。

在某些情况下,迁移可能会锁定表一段时间,从而使系统无法使用。确保您适当地测试您的迁移,以避免或至少为这些情况做好准备。

尽管数据库迁移在技术上可能是可逆的,但从开发时间的角度来看,这样做是非常昂贵的。例如,添加和删除列可能很简单,但一旦列投入使用,它将包含不应删除的数据。

为了能够在数据迁移事件中无缝工作,您需要将其与将调用它的代码分离,并按照以下步骤进行操作:

  1. 设计数据库迁移时,要以不干扰当前代码为目标。例如,向数据库添加表或列是安全的,因为旧代码会忽略它。

  2. 执行数据库迁移。这样就可以在现有代码继续运行而不中断的情况下进行所需的更改。

  3. 现在,代码可以部署。一旦部署完成,它将开始使用新数据库定义的优势。如果出现问题,代码可以回滚到先前的版本。

这意味着我们需要创建两个部署:

  • 一个用于迁移

  • 另一个用于使用此迁移的代码

迁移部署可能类似于代码部署。也许有一个运行迁移的微服务,或者可能是一个执行所有工作的脚本。大多数框架都会有一种迁移的方法,以确保迁移不会被应用两次。

例如,对于 SQLAlchemy,有一个名为 Alembic 的工具(alembic.sqlalchemy.org/en/latest/),我们可以使用它来生成和运行迁移。

然而,还有一种替代操作:尝试将迁移应用于将使用它们的微服务。在处理生产环境时,这是一个坏主意,因为这将在所有情况下减慢启动时间,而不管是否正在进行迁移。此外,它不会检查代码是否可以安全回滚,并且是否与数据库的先前版本兼容。

与两个独立的部署一起工作显然比自由更改数据库更加受限,但它确保每一步都是稳固的,服务不会中断。这更加故意。例如,要重命名列,我们将按照以下步骤进行:

  1. 首先,我们将部署一个创建具有新列名称的新列的迁移,从而复制旧列中的数据。代码从旧列读取和写入。

  2. 然后,我们将部署从旧列读取并向两者写入的新代码。在发布过程中,从旧代码到旧列的任何写入都将被正确读取。

  3. 之后,我们将创建另一个迁移,将数据从旧迁移复制到新迁移。这样可以确保任何瞬态复制都被正确应用。此时,任何新数据仍然会同时写入两列。

  4. 然后,我们将部署代码,从新列读取和写入,忽略旧列。

  5. 最后,我们将实施一个迁移来删除旧列。此时,旧列不包含相关数据,可以安全地删除。这不会影响代码。

这是一个故意的长流程示例,但在大多数情况下,不需要这样的长流程。然而,在这些步骤中的任何时候都没有任何不一致。如果某个阶段出现问题,我们可以回滚到上一个阶段 - 直到修复为止,它仍然可以工作。

主要目标是避免数据库与当前部署的代码不兼容的瞬态状态。

总结

在本章中,我们讨论了团队的流程,从开始一个新功能到将其部署到生产环境中。

我们首先讨论了在微服务架构中工作时功能请求的关键点。我们介绍了影响多个微服务的请求,并学习了如何构建工作,以便服务不会中断。

我们讨论了构成良好审查和批准流程的要素,以及 GitHub 拉取请求如何帮助我们做到这一点。使用 GitOps 实践来控制基础设施使得部署可以轻松地进行审查。

然后,我们讨论了如何使用 Kubernetes 和 GitOps 帮助我们创建多个环境,以及在处理演示和分段环境时如何利用它们的优势,以测试部署并在进入生产之前在受控环境中展示功能。

之后,我们讨论了如何使团队能够全面了解整个生命周期,从功能请求到部署,并能够快速跟踪整个路径。我们学会了如何澄清这些步骤,以及如何使团队负责审查和批准自己的代码,这使开发人员可以完全拥有开发周期。

我们还讨论了在处理数据库迁移时可能出现的问题,并解释了如何进行这种特殊类型的部署,这不容易回滚。

在下一章中,我们将讨论实时系统以及如何启用诸如指标和日志之类的元素,以便我们可以检测在生产环境中发生的问题和错误,并获得足够的信息尽快主动地进行修复。

问题

  1. 当接收到一个新的业务功能时,在微服务架构下,我们需要进行怎样的分析?

  2. 如果一个功能需要修改两个或更多微服务,我们如何决定首先修改哪一个?

  3. Kubernetes 如何帮助我们建立多个环境?

  4. 代码审查是如何工作的?

  5. 代码审查的主要瓶颈是什么?

  6. 根据 GitOps 原则,部署审查与代码审查有何不同?

  7. 为什么一旦一个功能准备合并到主分支时,有一个清晰的部署路径是很重要的?

  8. 为什么数据库迁移与常规代码部署不同?

进一步阅读

想要了解更多关于敏捷实践并将其引入团队的信息,请查阅以下书籍:

  • 《The Agile Developer's Handbook》

  • 《Agile Technical Practices Distilled》

如果你的组织在使用 JIRA,阅读《Hands-On Agile Software Development with JIRA》可以帮助你更好地利用这个工具来进行敏捷实践。

第四部分:生产就绪系统-使其在实际环境中运行

该书的最后一部分关注一些使实际生活中的系统长期运行的要素,从系统的可观察性,这对于快速检测和解决问题至关重要,到处理影响整个系统的配置,并包括确保不同团队协作和协调开发系统的技术。

本部分的第一章介绍了如何在实时集群中发现操作以检测使用情况和相关问题。本章介绍了可观察性的概念以及支持它的两个主要工具:日志和指标。它涵盖了如何在 Kubernetes 集群中正确地包含它们。

本部分的第二章涉及跨不同微服务共享的配置以及如何处理服务之间的依赖关系。它还展示了如何在现实生活中处理机密信息:包含敏感信息的配置参数,如安全密钥和证书。

本部分的第三章描述了在微服务架构中工作时团队间沟通的常见问题以及如何处理这些问题,包括如何在整个组织中创建共享愿景,团队划分如何影响不同的 API,以及如何跨团队发布新功能。

本部分包括以下章节:

  • [第十章],监控日志和指标

  • [第十一章],处理变化、依赖和系统中的机密信息

  • [第十二章],团队间的协作与沟通

第十章:监控日志和指标

在实际运营中,快速检测和调试问题的能力至关重要。在本章中,我们将讨论我们可以用来发现在处理大量请求的生产集群中发生了什么的两个最重要的工具。第一个工具是日志,它帮助我们了解单个请求中发生了什么,而另一个工具是指标,它对系统的聚合性能进行分类。

本章将涵盖以下主题:

  • 实时系统的可观测性

  • 设置日志

  • 通过日志检测问题

  • 设置指标

  • 积极主动

在本章结束时,您将了解如何添加日志以便检测问题,以及如何添加和绘制指标,并了解它们之间的区别。

技术要求

我们将使用示例系统,并对其进行调整,包括集中式日志记录和指标。本章的代码可以在本书的 GitHub 存储库中找到:github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter10

要安装集群,您需要构建每个单独的微服务:

$ cd Chapter10/microservices/
$ cd frontend
$ docker-compose build
...
$ cd thoughts_backend
$ docker-compose build
...
$ cd users_backend
$ docker-compose build
...

本章中的微服务与之前介绍的相同,但它们增加了额外的日志和指标配置。

现在,我们需要创建示例命名空间,并使用Chapter10/kubernetes子目录中的find配置启动 Kubernetes 集群:

$ cd Chapter10/kubernetes
$ kubectl create namespace example
$ kubectl apply --recursive -f .
...

要能够访问不同的服务,您需要更新您的/etc/hosts文件,以便包含以下代码行:

127.0.0.1 thoughts.example.local
127.0.0.1 users.example.local
127.0.0.1 frontend.example.local
127.0.0.1 syslog.example.local
127.0.0.1 prometheus.example.local
127.0.0.1 grafana.example.local

有了这些,您将能够访问本章的日志和指标。

实时系统的可观测性

可观测性是了解实时系统发生情况的能力。我们可能会遇到低可观测性系统,我们无法了解其中发生了什么,或者高可观测性系统,我们可以通过工具从外部推断事件和内部状态。

可观测性是系统本身的属性。通常,监控是获取有关系统当前或过去状态的信息的行为。这有点命名上的争议,但你要监控系统以收集其中可观测的部分。

在大多数情况下,监控是很容易的。有很多出色的工具可以帮助我们捕获和分析信息,并以各种方式呈现。但是,系统需要暴露相关信息,以便可以收集。

暴露正确数量的信息是困难的。太多信息会产生很多噪音,会掩盖相关信号。信息太少将不足以检测问题。在本章中,我们将探讨不同的策略来解决这个问题,但每个系统都必须自行探索和发现。期望在自己的系统中进行实验和更改!

分布式系统,例如遵循微服务架构的系统,也会出现问题,因为系统的复杂性可能会使其内部状态难以理解。在某些情况下,行为也可能是不可预测的。这种规模的系统本质上永远不会完全健康;总会有一些小问题。您需要制定一个优先级系统,以确定哪些问题需要立即解决,哪些可以在以后解决。

微服务可观测性的主要工具是日志指标。它们为社区所熟知,并且有许多工具大大简化了它们的使用,既可以作为可以在本地安装的软件包,也可以作为云服务,有助于数据保留和降低维护成本。

使用云服务进行监控将节省您的维护成本。我们将在设置日志设置指标部分稍后讨论这一点。

在可观察性方面的另一种选择是诸如 Data Dog(www.datadoghq.com/)和 New Relic(newrelic.com/)等服务。它们接收事件——通常是日志——并能够从中推导出指标。

集群状态的最重要细节可以通过kubectl进行检查,就像我们在之前的章节中看到的那样。这将包括已部署的版本、重启、拉取镜像等详细信息。

对于生产环境,部署一个基于 Web 的工具来显示这种信息可能是一个好主意。查看 Weave Scope,这是一个开源工具,可以在网页上显示数据,类似于可以使用kubectl获得的数据,但以更美观和更图形化的方式。您可以在这里了解更多关于这个工具的信息:www.weave.works/oss/scope/

日志和指标有不同的目标,两者都可能很复杂。我们将在本书中看一些它们的常见用法。

理解日志

日志跟踪系统中发生的唯一事件。每个日志都存储一个消息,当代码的特定部分被执行时产生。日志可以是完全通用的(调用函数 X)或包含特定细节(使用参数 A 调用函数 X)。

日志的最常见格式是将它们生成为纯文本。这非常灵活,通常与与日志相关的工具一起使用文本搜索。

每个日志都包含一些关于谁产生了日志、创建时间等元数据。这通常也被编码为文本,出现在日志的开头。标准格式有助于排序和过滤。

日志还包括严重级别。这允许对消息的重要性进行分类。严重级别可以按重要性顺序为DEBUGINFOWARNINGERROR。这种严重性允许我们过滤掉不重要的日志,并确定我们应该采取的行动。日志记录设施可以配置为设置阈值;较不严重的日志将被忽略。

有许多严重级别,如果您愿意,可以定义自定义中间级别。然而,除非在非常特定的情况下,否则这并不是非常有用。在本章后面的通过日志检测问题部分,我们将描述如何针对每个级别设置策略;太多级别会增加混乱。

在 Web 服务环境中,大多数日志将作为对 Web 请求的响应的一部分生成。这意味着请求将到达系统,被处理,并返回一个值。沿途将生成多个日志。请记住,在负载下的系统中,多个请求将同时发生,因此多个请求的日志也将同时生成。例如,注意第二个日志来自不同的 IP:

Aug 15 00:15:15.100 10.1.0.90 INFO app: REQUEST GET /endpoint
Aug 15 00:15:15.153 10.1.0.92 INFO api: REQUEST GET /api/endpoint
Aug 15 00:15:15.175 10.1.0.90 INFO app: RESPONSE TIME 4 ms
Aug 15 00:15:15.210 10.1.0.90 INFO app: RESPONSE STATUS 200

常见的请求 ID 可以添加到所有与单个请求相关的日志中。我们将在本章后面看到如何做到这一点。

每个单独的日志可能相对较大,并且在聚合时会占用大量磁盘空间。在负载下的系统中,日志可能会迅速膨胀。不同的日志系统允许我们调整其保留时间,这意味着我们只保留它们一段时间。在保留日志以查看过去发生的事情和使用合理的空间之间找到平衡是很重要的。

在启用任何新的日志服务时,请务必检查保留策略,无论是本地还是基于云的。您将无法分析发生在时间窗口之前的情况。仔细检查进度是否符合预期——您不希望在跟踪错误时意外超出配额。

一些工具允许我们使用原始日志生成聚合结果。它们可以计算特定日志出现的次数,并生成每分钟的平均时间或其他统计信息。但这很昂贵,因为每个日志都占用空间。要观察这种聚合行为,最好使用特定的指标系统。

理解指标

指标处理聚合信息。它们显示与单个事件无关的信息,而是一组事件的信息。这使我们能够以比使用日志更好的方式检查集群的一般状态。

我们将使用与网络服务相关的典型示例,主要涉及请求指标,但不要感到受限。您可以生成特定于您的服务的指标!

日志记录每个单独事件的信息,而指标将信息减少到事件发生的次数,或将其减少到可以进行平均或以某种方式聚合的值。

这使得指标比日志更轻量,并且可以根据时间绘制它们。指标呈现的信息包括每分钟的请求次数,每分钟请求的平均时间,排队请求的数量,每分钟的错误数量等。

指标的分辨率可能取决于用于聚合它们的工具。请记住,更高的分辨率将需要更多的资源。典型的分辨率是 1 分钟,这足够小以呈现详细信息,除非您的系统非常活跃,每秒接收 10 次或更多请求。

捕获和分析与性能相关的信息,如平均请求时间,使我们能够快速检测可能的瓶颈并迅速采取行动以改善系统的性能。这通常更容易处理,因为单个请求可能无法捕获足够的信息让我们看到整体情况。它还有助于我们预测未来的瓶颈。

根据所使用的工具,有许多不同类型的指标。最常见的支持包括以下内容:

  • 计数器: 每次发生事件时都会生成一个触发器。这将被计数和聚合。这的一个例子是请求的数量和错误的数量。

  • 量规: 一个唯一的单一数字。它可以增加或减少,但最后一个值会覆盖之前的值。这的一个例子是队列中的请求数量和可用工作者的数量。

  • 度量: 与之相关的事件具有数字。这些数字可以被平均、求和或以某种方式聚合。与量规相比,前面的度量仍然是独立的;例如,当我们以毫秒为单位请求时间和以字节为单位请求大小时。度量也可以作为计数器工作,因为它们的数量可能很重要;例如,跟踪请求时间还计算请求的数量。

指标有两种主要工作方式:

  • 每次发生事件时,事件都会被推送到指标收集器。

  • 每个系统都维护自己的指标,然后定期从指标系统中拉取它们。

每种方式都有其优缺点。推送事件会产生更高的流量,因为每个事件都需要发送;这可能会导致瓶颈和延迟。拉取事件只会对信息进行抽样,并且会错过样本之间发生的确切情况,但它本质上更具可扩展性。

虽然两种方法都在使用,但趋势是向拉取系统转移。它们减少了推送系统所需的维护工作,并且更容易扩展。

我们将设置使用第二种方法的 Prometheus。第一种方法最常用的指标系统是 Graphite。

指标也可以组合以生成其他指标;例如,我们可以将返回错误的请求次数除以生成错误请求的总请求数。这样的派生指标可以帮助我们以有意义的方式呈现信息。

多个指标可以显示在仪表板上,这样我们就可以了解服务或集群的状态。通过这些图形工具,我们可以一目了然地检测系统的一般状态。我们将设置 Grafana,以显示图形信息:

与日志相比,指标占用的空间要少得多,可以捕获更长的时间窗口。甚至可以保留系统的生命周期内的指标。这与日志不同,日志永远无法存储那么长时间。

设置日志

我们将把系统生成的所有日志集中到一个单独的 pod 中。在本地开发中,这个 pod 将通过 Web 界面公开所有接收到的日志。

日志将通过syslog协议发送,这是传输日志的最标准方式。Python 中有syslog的原生支持,几乎任何处理日志并具有 Unix 支持的系统都有。

使用单个容器可以轻松聚合日志。在生产环境中,应该用一个容器来替换这个系统,将接收到的日志传送到 Loggly 或 Splunk 等云服务。

有多个syslog服务器可以接收日志并进行聚合;syslog-ng (www.syslog-ng.com/)和rsyslog (www.rsyslog.com/)是最常见的。最简单的方法是接收日志并将其存储在文件中。让我们启动一个带有rsyslog服务器的容器,它将存储接收到的日志。

设置 rsyslog 容器

在这一部分,我们将创建自己的rsyslog服务器。这是一个非常简单的容器,您可以在 GitHub 上查看有关日志的docker-composeDockerfile的更多信息(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter10/kubernetes/logs)。

我们将使用 UDP 协议设置日志。这是syslog的标准协议,但比用于 Web 开发的通常的 TCP 上的 HTTP 要少见。

主要区别在于 UDP 是无连接的,因此日志被发送后不会收到已传递的确认。这使得 UDP 更轻更快,但也更不可靠。如果网络出现问题,一些日志可能会无预警地消失。

这通常是一个合理的权衡,因为日志数量很大,丢失一些日志的影响并不大。syslog也可以通过 TCP 工作,从而增加可靠性,但也降低了系统的性能。

Dockerfile 安装了rsyslog并复制了其配置文件:

FROM alpine:3.9

RUN apk add --update rsyslog

COPY rsyslog.conf /etc/rsyslog.d/rsyslog.conf

配置文件主要是在端口5140启动服务器,并将接收到的文件存储在/var/log/syslog中:

# Start a UDP listen port at 5140
module(load="imudp")
input(type="imudp" port="5140")
...
# Store the received files in /var/log/syslog, and enable rotation
$outchannel log_rotation,/var/log/syslog, 5000000,/bin/rm /var/log/syslog

通过日志轮换,我们设置了/var/log/syslog文件的大小限制,以防止其无限增长。

我们可以使用通常的docker-compose命令构建容器:

$ docker-compose build
Building rsyslog
...
Successfully built 560bf048c48a
Successfully tagged rsyslog:latest

这将创建一个 pod、一个服务和一个 Ingress 的组合,就像我们对其他微服务所做的那样,以收集日志并允许从浏览器进行外部访问。

定义 syslog pod

syslog pod 将包含rsyslog容器和另一个用于显示日志的容器。

为了显示日志,我们将使用 front rail,这是一个将日志文件流式传输到 Web 服务器的应用程序。我们需要在同一个 pod 中的两个容器之间共享文件,最简单的方法是通过卷。

我们使用部署来控制 pod。您可以在github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter10/kubernetes/logs/deployment.yaml中检查部署配置文件。让我们在以下小节中看一下它最有趣的部分。

log-volume

log-volume创建了一个空目录,该目录在两个容器之间共享:

  volumes:
  - emptyDir: {}
    name: log-volume

这允许容器在存储信息的同时进行通信。syslog容器将向其中写入,而前端容器将从其中读取。

syslog 容器

syslog容器启动了一个rsyslogd进程:

spec:
  containers:
  - name: syslog
    command:
      - rsyslogd
      - -n
      - -f
      - /etc/rsyslog.d/rsyslog.conf
    image: rsyslog:latest
    imagePullPolicy: Never
    ports:
      - containerPort: 5140
        protocol: UDP
    volumeMounts:
      - mountPath: /var/log
        name: log-volume

rsyslogd -n -f /etc/rsyslog.d/rsyslog.conf命令使用我们之前描述的配置文件启动服务器。-n参数将进程保持在前台,从而保持容器运行。

指定了 UDP 端口5140,这是接收日志的定义端口,并且将log-volume挂载到/var/log。文件的后面将定义log-volume

前端容器

前端容器是从官方容器镜像启动的:

  - name: frontrail
    args:
    - --ui-highlight
    - /var/log/syslog
    - -n
    - "1000"
    image: mthenw/frontail:4.6.0
    imagePullPolicy: Always
    ports:
    - containerPort: 9001
      protocol: TCP
    resources: {}
    volumeMounts:
    - mountPath: /var/log
      name: log-volume

我们使用frontrail /var/log/syslog命令启动它,指定端口9001(这是我们用来访问frontrail的端口),并挂载/var/log,就像我们用syslog容器一样,以共享日志文件。

允许外部访问

与其他微服务一样,我们将创建一个服务和一个 Ingress。服务将被其他微服务使用,以便它们可以发送它们的日志。Ingress 将用于访问 Web 界面,以便我们可以在日志到达时查看日志。

YAML 文件位于 GitHub 上(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter10/kubernetes/logs),分别是service.yamlingress.yaml文件。

服务非常简单;唯一的特殊之处在于它有两个端口 - 一个 TCP 端口和一个 UDP 端口 - 每个端口连接到不同的容器:

spec:
  ports:
  - name: fronttail
    port: 9001
    protocol: TCP
    targetPort: 9001
  - name: syslog
    port: 5140
    protocol: UDP
    targetPort: 5140

Ingress 只暴露了前端端口,这意味着我们可以通过浏览器访问它。请记住,DNS 需要添加到您的/etc/host文件中,就像本章开头所描述的那样:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: syslog-ingress
  namespace: example
spec:
  rules:
  - host: syslog.example.local
    http:
      paths:
      - backend:
          serviceName: syslog
          servicePort: 9001
        path: /

在浏览器中输入http://syslog.example.local将允许您访问前端界面:

您可以使用右上角的框来过滤日志。

请记住,大多数时候,日志反映了就绪和存活探针,如前面的屏幕截图所示。您的系统中有更多的健康检查,您将会得到更多的噪音。

您可以通过配置rsyslog.conf文件在syslog级别上将其过滤掉,但要小心不要遗漏任何相关信息。

现在,我们需要看看其他微服务如何配置并将它们的日志发送到这里。

发送日志

我们需要在 uWSGI 中配置微服务,以便我们可以将日志转发到日志服务。我们将使用 Thoughts Backend 作为示例,即使 Frontend 和 Users Backend 也有这个配置,可以在Chapter10/microservices目录下找到。

打开uwsgi.ini配置文件(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter10/microservices/thoughts_backend/docker/app/uwsgi.ini)。您将看到以下行:

# Log to the logger container
logger = rsyslog:syslog:5140,thoughts_backend

这将以rsyslog格式发送日志到端口5140syslog服务。我们还添加了facility,这是日志来源的地方。这将为来自此服务的所有日志添加字符串,有助于排序和过滤。每个uwsgi.ini文件应该有自己的 facility 以帮助过滤。

在支持syslog协议的旧系统中,facility 需要符合预定值,例如KERNLOCAL_7等。但在大多数现代系统中,这是一个任意的字符串,可以取任何值。

uWSGI 自动记录很有趣,但我们还需要为自定义跟踪设置自己的日志。让我们看看如何做。

生成应用程序日志

Flask 自动为应用程序配置了一个记录器。我们需要以以下方式添加日志,如api_namespace.py文件中所示(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter10/microservices/thoughts_backend/ThoughtsBackend/thoughts_backend/api_namespace.py#L102):

from flask import current_app as app

...
if search_param:
    param = f'%{search_param}%'
    app.logger.info(f'Searching with params {param}')
    query = (query.filter(ThoughtModel.text.ilike(param)))

app.logger可以调用.debug.info.warning.error来生成日志。请注意,可以通过导入current_app来检索app

记录器遵循 Python 中的标准logging模块。它可以以不同的方式进行配置。查看app.py文件(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter10/microservices/thoughts_backend/ThoughtsBackend/thoughts_backend/app.py)以查看我们将在以下子部分中进行的不同配置。

字典配置

第一级别的日志记录通过默认的dictConfig变量。这个变量由 Flask 自动定义,并允许我们按照 Python 文档中定义的方式配置日志(docs.python.org/3.7/library/logging.config.html)。您可以在app.py文件中查看日志的定义:

from logging.config import dictConfig

dictConfig({
    'version': 1,
    'formatters': {
        'default': {
            'format': '[%(asctime)s] %(levelname)s in 
                        %(module)s: %(message)s',
        }
    },
    'handlers': {
        'wsgi': {
            'class': 'logging.StreamHandler',
            'stream': 'ext://flask.logging.wsgi_errors_stream',
            'formatter': 'default'
        }
    },
    'root': {
        'level': 'INFO',
        'handlers': ['wsgi']
    }
})

dictConfig字典有三个主要级别:

  • formatters:这检查日志的格式。要定义格式,可以使用 Python 文档中提供的自动值(docs.python.org/3/library/logging.html#logrecord-attributes)。这收集每个日志的信息。

  • handlers:这检查日志的去向。您可以将一个或多个分配给记录器。我们定义了一个名为wsgi的处理程序,并对其进行了配置,以便将其发送到 uWSGI。

  • root:这是日志的顶层,因此以前未记录的任何内容都将参考此级别。我们在这里配置INFO日志级别。

这将设置默认配置,以便我们不会错过任何日志。但是,我们可以创建更复杂的日志处理程序。

记录请求 ID

在分析大量日志时的一个问题是对其进行关联。我们需要看到哪些日志彼此相关。一种可能性是通过生成它们的 pod 来过滤日志,该 pod 存储在日志的开头(例如,10-1-0-27.frontend-service.example.svc.cluster.local)。这类似于生成日志的主机。然而,这个过程很繁琐,并且在某些情况下,单个容器可以同时处理两个请求。我们需要为每个请求添加一个唯一标识符,该标识符将添加到单个请求的所有日志中。

为此,我们将使用flask-request-id-header包(pypi.org/project/flask-request-id-header/)。这将添加一个X-Request-ID头(如果不存在),我们可以用它来记录每个单独的请求。

为什么我们设置一个头部而不是将随机生成的值存储在内存中以供请求使用?这是一种常见的模式,允许我们将请求 ID 注入到后端。请求 ID 允许我们在不同微服务的请求生命周期中传递相同的请求标识符。例如,我们可以在前端生成它并将其传递到 Thoughts 后端,以便我们可以跟踪具有相同来源的多个内部请求。

尽管出于简单起见,我们不会在示例中包含这一点,但是随着微服务系统的增长,这对于确定流程和来源变得至关重要。生成一个模块,以便我们可以自动传递内部调用,这是一个很好的投资。

以下图表显示了前端和两个服务之间的流程。请注意,前端服务在到达时未设置X-Request-ID头,并且需要转发到任何调用:

我们还需要将日志直接发送到syslog服务,以便我们可以创建一个为我们执行此操作的处理程序。

当从脚本执行代码时,与在 web 服务器中运行代码相比,我们不使用此处理程序。直接运行脚本时,我们希望日志记录到我们之前定义的默认记录器。在create_app中,我们将设置一个参数来区分它们。

Python 日志模块具有许多有趣的功能。查看 Python 文档以获取更多信息(docs.python.org/3/library/logging.html)。

正确设置日志比看起来更加棘手。不要灰心,继续调整它们直到它们起作用。

我们将在app.py文件中设置所有日志配置。让我们分解配置的每个部分:

  1. 首先,我们将生成一个格式化程序,以便在生成日志时附加request_id,使其在生成日志时可用:
class RequestFormatter(logging.Formatter):
    ''' Inject the HTTP_X_REQUEST_ID to format logs '''

    def format(self, record):
        record.request_id = 'NA'

        if has_request_context():
            record.request_id = request.environ.get("HTTP_X_REQUEST_ID")

        return super().format(record)

如您所见,HTTP_X_REQUEST_ID头在request.environ变量中可用。

  1. 稍后,在create_app中,我们将设置附加到application记录器的处理程序:
# Enable RequestId
application.config['REQUEST_ID_UNIQUE_VALUE_PREFIX'] = ''
RequestID(application)

if not script:
    # For scripts, it should not connect to Syslog
    handler = logging.handlers.SysLogHandler(('syslog', 5140))
    req_format = ('[%(asctime)s] %(levelname)s [%(request_id)s] '
                    %(module)s: %(message)s')
    handler.setFormatter(RequestFormatter(req_format))
    handler.setLevel(logging.INFO)
    application.logger.addHandler(handler)
    # Do not propagate to avoid log duplication
    application.logger.propagate = False

只有在脚本外运行时才设置处理程序。SysLogHandler包含在 Python 中。之后,我们设置格式,其中包括request_id。格式化程序使用我们之前定义的RequestFormatter

在这里,我们将记录器级别的值硬编码为INFOsyslog主机为syslog,这对应于服务。Kubernetes 将正确解析此 DNS。这两个值都可以通过环境变量传递,但出于简单起见,我们没有在这里这样做。

记录器尚未传播,因此避免将其发送到root记录器,这将重复记录。

记录每个请求

每个请求中都有一些常见元素需要捕获。Flask 允许我们在请求之前和之后执行代码,因此我们可以使用它来记录每个请求的常见元素。让我们学习如何做到这一点。

app.py文件中,我们将定义logging_before函数:

from flask import current_app, g

def logging_before():
    msg = 'REQUEST {REQUEST_METHOD} {REQUEST_URI}'.format(**request.environ)
    current_app.logger.info(msg)

    # Store the start time for the request
    g.start_time = time()

这将创建一个带有单词REQUEST和每个请求的两个基本部分(方法和 URI)的日志,这些部分来自request.environ。然后,它们将添加到应用程序记录器的INFO日志中。

我们还使用g对象来存储请求开始时的时间。

g对象允许我们通过请求存储值。我们将使用它来计算请求将花费的时间。

还有相应的logging_after函数。它在请求结束时收集时间并计算毫秒数的差异:

def logging_after(response):
    # Get total time in milliseconds
    total_time = time() - g.start_time
    time_in_ms = int(total_time * 1000)
    msg = f'RESPONSE TIME {time_in_ms} ms'
    current_app.logger.info(msg)

    msg = f'RESPONSE STATUS {response.status_code.value}'
    current_app.logger.info(msg)

    # Store metrics
    ...

    return response

这将使我们能够检测到需要更长时间的请求,并将其存储在指标中,我们将在下一节中看到。

然后,在create_app函数中启用了这些功能:

def create_app(script=False):
    ...
    application = Flask(__name__)
    application.before_request(logging_before)
    application.after_request(logging_after)

每次生成请求时都会创建一组日志。

有了生成的日志,我们可以在frontrail界面中搜索它们。

搜索所有日志

来自不同应用程序的所有不同日志将被集中并可在http://syslog.example.local上搜索。

如果您调用http://frontend.example.local/search?search=speak来搜索想法,您将在日志中看到相应的 Thoughts Backend,如下图所示:

我们可以按请求 ID 进行过滤,即63517c17-5a40-4856-9f3b-904b180688f6,以获取 Thoughts Backend 请求日志。紧接着是thoughts_backend_uwsgifrontend_uwsgi请求日志,显示了请求的流程。

在这里,您可以看到我们之前谈到的所有元素:

  • 请求之前的REQUEST日志

  • 包含应用数据的api_namespace请求

  • 包含结果和时间的RESPONSE日志

在 Thoughts Backend 的代码中,我们故意留下了一个错误。如果用户尝试分享新的想法,它将被触发。我们将使用这个来学习如何通过日志调试问题。

通过日志检测问题

在您运行的系统中,可能会出现两种类型的错误:预期错误和意外错误。

检测预期错误

预期错误是通过在代码中显式创建ERROR日志而引发的错误。如果生成了错误日志,这意味着它反映了事先计划的情况;例如,无法连接到数据库,或者某些数据存储在旧的废弃格式中。我们不希望这种情况发生,但我们看到了它发生的可能性,并准备好了代码来处理它。它们通常描述得足够清楚,以至于问题是显而易见的,即使解决方案不明显。

它们相对容易处理,因为它们描述了预见的问题。

捕获意外错误

意外错误是可能发生的其他类型的错误。事情以意想不到的方式出错。意外错误通常是由于代码中某些地方引发了 Python 异常而未被捕获。

如果日志已经正确配置,任何未被捕获的异常或错误都会触发一个ERROR日志,其中包括堆栈跟踪。这些错误可能不会立即显而易见,需要进一步调查。

为了帮助解释这些错误,我们在Chapter10代码的 Thoughts Backend 中引入了一个异常。您可以在 GitHub 上检查代码(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter10/microservices/thoughts_backend/ThoughtsBackend/thoughts_backend)。这模拟了一个意外的异常。

尝试为已登录用户发布新想法时,我们会遇到奇怪的行为,并在日志中看到以下错误。如下图右上角所示,我们正在按ERROR进行过滤以查找问题:

如您所见,堆栈跟踪显示在单行中。这可能取决于您如何捕获和显示日志。Flask 将自动生成一个状态码为 500 的 HTTP 响应。如果调用者没有准备好接收 500 响应,这可能会在路径上触发更多错误。

然后,堆栈跟踪将让您知道出了什么问题。在这种情况下,我们可以看到在第 80 行的api_namespace.py文件中有一个raise Exception命令。这使我们能够定位异常。

由于这是一个特意生成的合成错误示例,实际上很容易找到根本原因。在示例代码中,我们明确引发了一个异常,这会产生一个错误。在实际用例中可能不是这种情况,异常可能在与实际错误不同的地方生成。异常也可能来自同一集群中的不同微服务。

在检测到错误后,目标应该是在微服务中使用单元测试复制错误以生成异常。这将使我们能够在受控环境中复制条件。

如果我们运行 Chapter10 中可用的 Thoughts Backend 代码的测试,我们将看到由于此原因而出现错误。请注意,日志将显示在失败的测试中。

$ docker-compose run test
...
___ ERROR at setup of test_get_non_existing_thought ___
-------- Captured log setup ---------
INFO flask.app:app.py:46 REQUEST POST /api/me/thoughts/
INFO flask.app:token_validation.py:66 Header successfully validated
ERROR flask.app:app.py:1761 Exception on /api/me/thoughts/ [POST]
Traceback (most recent call last):
 File "/opt/venv/lib/python3.6/site-packages/flask/app.py", line 1813, in full_dispatch_request
 rv = self.dispatch_request()
 File "/opt/venv/lib/python3.6/site-packages/flask/app.py", line 1799, in dispatch_request
 return self.view_functionsrule.endpoint
 File "/opt/venv/lib/python3.6/site-packages/flask_restplus/api.py", line 325, in wrapper
 resp = resource(*args, **kwargs)
 File "/opt/venv/lib/python3.6/site-packages/flask/views.py", line 88, in view
 return self.dispatch_request(*args, **kwargs)
 File "/opt/venv/lib/python3.6/site-packages/flask_restplus/resource.py", line 44, in dispatch_request
 resp = meth(*args, **kwargs)
 File "/opt/venv/lib/python3.6/site-packages/flask_restplus/marshalling.py", line 136, in wrapper
 resp = f(*args, **kwargs)
 File "/opt/code/thoughts_backend/api_namespace.py", line 80, in post
 raise Exception('Unexpected error!')
Exception: Unexpected error!
INFO flask.app:app.py:57 RESPONSE TIME 3 ms
INFO flask.app:app.py:60 RESPONSE STATUS 500 

一旦在单元测试中重现了错误,修复它通常会很简单。添加一个单元测试来捕获触发错误的条件,然后修复它。新的单元测试将检测每次自动构建中是否重新引入了错误。

要修复示例代码,请删除 raise 代码行。然后,事情将再次正常工作。

有时,问题无法解决,因为可能是外部问题。也许我们的数据库中的某些行存在问题,或者另一个服务返回的数据格式不正确。在这些情况下,我们无法完全避免错误的根本原因。但是,可以捕获问题,进行一些补救,并从意外错误转变为预期错误。

请注意,并非每个检测到的意外错误都值得花时间处理。有时,未捕获的错误提供了足够的信息,超出了 Web 服务应该处理的范围;例如,可能存在网络问题,Web 服务无法连接到数据库。在开发时,要根据自己的判断来决定是否要花时间处理。

记录策略

处理日志时存在问题。对于特定消息,什么是适当的级别?这是 WARNING 还是 ERROR?这应该是一个 INFO 语句吗?

大多数日志级别描述使用定义,例如“程序显示潜在的有害情况”或“程序突出显示请求的进展”。这些定义模糊且在实际环境中并不是很有用。相反,尝试通过将每个日志级别与预期的后续操作联系起来来定义每个日志级别。这有助于明确发现特定级别的日志时应该采取的行动。

以下表格显示了不同级别的一些示例以及应该采取的行动:

日志级别 采取的行动 评论
DEBUG 无。 不跟踪。
INFO 无。 INFO 日志显示有关请求流程的通用信息,以帮助跟踪问题。
WARNING 跟踪数量。在提高级别时发出警报。 WARNING 日志跟踪已自动修复的错误,例如重试连接(但最终连接成功)或数据库数据中可修复的格式错误。突然增加可能需要调查。
ERROR 跟踪数量。在提高级别时发出警报。审查所有。 ERROR 日志跟踪无法修复的错误。突然增加可能需要立即采取行动以进行补救。
CRITICAL 立即响应。 CRITICAL 日志表示系统发生了灾难性故障。即使一个 CRITICAL 日志也表明系统无法正常工作且无法恢复。

这只是一个建议,但它为如何做出响应设定了明确的期望。根据团队和期望的服务水平的工作方式,可以将其调整为自己的用例。

在这里,层次结构非常清晰,并且人们接受一定数量的 ERROR 日志将被生成。并非所有问题都需要立即修复,但应该记录并进行审查。

在现实生活中,ERROR日志通常被归类为“我们注定要失败”或“无所谓”。开发团队应该积极修复或删除“无所谓”的错误,以尽量减少它们。这可能包括降低日志级别,如果它们没有涵盖实际错误的话。您希望尽可能少的ERROR日志,但所有这些日志都需要有意义。

然而,务实一点。有时,错误无法立即修复,时间最好用在其他任务上。然而,团队应该保留时间来减少发生的错误数量。不这样做将会损害系统的中期可靠性。

WARNING日志表明某些事情可能不像我们预期的那样顺利,但除非数字增长,否则无需惊慌。INFO只是在出现问题时为我们提供上下文,但在其他情况下应该被忽略。

避免在请求返回 400 BAD REQUEST 状态代码时产生ERROR日志的诱惑。一些开发人员会认为,如果客户发送了格式不正确的请求,那实际上就是一个错误。但是,如果请求已经被正确检测并返回,这并不是你应该关心的事情。这是业务惯例。如果这种行为可能表明其他问题,比如重复尝试发送不正确的密码,您可以设置WARNING日志。当系统表现如预期时,生成ERROR日志是没有意义的。

作为一个经验法则,如果一个请求没有返回某种 500 错误(500、502、504 等),它不应该生成ERROR日志。记住将 400 错误归类为您(客户)有问题,而将 500 错误归类为我有问题

然而,这并非绝对。例如,通常为 4XX 错误的认证错误激增可能表明用户由于真正的内部问题而无法创建日志。

有了这些定义,您的开发和运维团队将有一个共同的理解,这将帮助他们采取有意义的行动。

随着系统的成熟,预计需要调整系统并更改日志级别。

在开发过程中添加日志

正如我们已经看到的,正确配置pytest将使测试中的任何错误显示捕获的日志。

这是一个机会,可以在开发功能时检查是否生成了预期的日志。检查错误条件的任何测试也应该添加相应的日志,并在开发功能期间检查它们是否生成。

您可以检查日志作为测试的一部分,使用诸如pytest-catchlogpypi.org/project/pytest-catchlog/)这样的工具来强制执行正确的日志生成。

通常情况下,在开发过程中,只需稍加注意并检查是否生成了日志就足够了。但是,确保开发人员了解在开发过程中拥有日志的用处。

在开发过程中,DEBUG日志可用于显示关于流程的额外信息,这些信息对于生产环境来说可能过多。这可以填补INFO日志之间的空白,并帮助我们养成添加日志的习惯。如果在测试期间发现DEBUG日志对于在生产环境中跟踪问题有用,可以将其提升为INFO

在受控情况下,可能会在生产环境中启用DEBUG日志以跟踪一些困难的问题,但要注意拥有大量日志的影响。

INFO日志中呈现的信息要明智。在显示的信息方面,避免敏感数据,如密码、密钥、信用卡号或个人信息。日志数量也是如此。

注意任何大小限制以及日志生成的速度。随着新功能的添加、更多请求通过系统流动以及新的工作人员的加入,不断增长的系统可能会导致日志爆炸。

此外,还要仔细检查日志是否被正确生成和捕获,并且它们在所有不同级别和环境中是否起作用。所有这些配置可能需要一些时间,但您需要非常确定您能够在生产环境中捕获意外错误,并且所有的管道都设置正确。

让我们来看看可观察性的另一个关键要素:指标。

设置指标

要使用 Prometheus 设置指标,我们需要了解该过程的工作原理。其关键组件是,每个受测量的服务都有自己的 Prometheus 客户端,用于跟踪指标。Prometheus 服务器中的数据将可供 Grafana 服务绘制指标。

以下图表显示了一般架构:

Prometheus 服务器定期拉取信息。这种操作方法非常轻量级,因为注册指标只是更新服务的本地内存并且能够很好地扩展。另一方面,它在特定时间显示采样数据,并且不会注册每个单独的事件。这在存储和表示数据方面有一定的影响,并且对数据的分辨率施加了限制,特别是对于非常低的速率。

有许多可用的指标导出器,它们将在不同系统中公开标准指标,如数据库、硬件、HTTP 服务器或存储。查看 Prometheus 文档以获取更多信息:prometheus.io/docs/instrumenting/exporters/

这意味着我们的每个服务都需要安装一个 Prometheus 客户端,并以某种方式公开其收集的指标。我们将使用 Flask 和 Django 的标准客户端。

思想后端的指标定义

对于 Flask 应用程序,我们将使用prometheus-flask-exporter包(github.com/rycus86/prometheus_flask_exporter),已添加到requirements.txt中。

当应用程序创建时,它会在app.py文件中激活(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter10/microservices/thoughts_backend/ThoughtsBackend/thoughts_backend/app.py#L95)。

metrics对象没有设置应用程序,然后在created_app函数中实例化:

from prometheus_flask_exporter import PrometheusMetrics

metrics = PrometheusMetrics(app=None)

def create_app(script=False):
    ...
    # Initialise metrics
    metrics.init_app(application)

这将生成/metrics服务端点中的一个端点,即http://thoughts.example.local/metrics,它以 Prometheus 格式返回数据。Prometheus 格式是纯文本,如下截图所示:

prometheus-flask-exporter捕获的默认指标是基于端点和方法的请求调用(flask_http_request_total),以及它们所花费的时间(flask_http_request_duration_seconds)。

添加自定义指标

当涉及应用程序细节时,我们可能希望添加更具体的指标。我们还在请求结束时添加了一些额外的代码,以便我们可以存储与prometheus-flask-exporter允许我们存储的类似信息。

特别是,我们在logging_after函数中添加了此代码(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter10/microservices/thoughts_backend/ThoughtsBackend/thoughts_backend/app.py#L72),使用较低级别的prometheus_client

此代码创建了CounterHistogram

from prometheus_client import Histogram, Counter

METRIC_REQUESTS = Counter('requests', 'Requests',
                          ['endpoint', 'method', 'status_code'])
METRIC_REQ_TIME = Histogram('req_time', 'Req time in ms',
                            ['endpoint', 'method', 'status_code']) 

def logging_after(response):
    ...
    # Store metrics
    endpoint = request.endpoint
    method = request.method.lower()
    status_code = response.status_code
    METRIC_REQUESTS.labels(endpoint, method, status_code).inc()
    METRIC_REQ_TIME.labels(endpoint, method, status_code).observe(time_in_ms)

在这里,我们创建了两个指标:一个名为requests的计数器和一个名为req_time的直方图。直方图是 Prometheus 对具有特定值的度量和事件的实现,例如请求时间(在我们的情况下)。

直方图将值存储在桶中,从而使我们能够计算分位数。分位数对于确定诸如时间的 95%值非常有用,例如聚合时间,其中 95%低于它。这比平均值更有用,因为异常值不会影响平均值。

还有一个类似的指标叫做摘要。差异是微妙的,但通常,我们应该使用直方图。查看 Prometheus 文档以获取更多详细信息(prometheus.io/docs/practices/histograms/)。

指标由它们的名称、测量和它们定义的标签METRIC_REQUESTSMETRIC_REQ_TIME定义。每个标签都是指标的额外维度,因此您将能够通过它们进行过滤和聚合。在这里,我们定义了端点、HTTP 方法和生成的 HTTP 状态码。

对于每个请求,指标都会更新。我们需要设置标签、计数器调用,即.inc(),以及直方图调用,即.observe(time)

您可以在github.com/prometheus/client_python找到 Prometheus 客户端的文档。

我们可以在指标页面上看到requestreq_time指标。

为用户后端设置指标遵循类似的模式。用户后端是一个类似的 Flask 应用程序,因此我们也安装了prometheus-flask-exporter,但没有自定义指标。您可以在http://users.example.local/metrics上访问这些指标。

下一阶段是设置一个 Prometheus 服务器,以便我们可以正确地收集和聚合指标。

收集指标。

为此,我们需要使用 Kubernetes 部署指标。我们已经在Chapter10/kubernetes/prometheus.yaml文件中准备好了一切。

这个 YAML 文件包含一个部署、一个包含配置文件的ConfigMap、一个服务和一个 Ingress。服务和 Ingress 都是非常标准的,所以我们在这里不会对它们进行评论。

ConfigMap允许我们定义一个文件:

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: example
data:
  prometheus.yaml: |
    scrape_configs:
    - job_name: 'example'

      static_configs:
        - targets: ['thoughts-service', 'users-service', 
                    'frontend-service']

请注意prometheus.yaml文件是在|符号之后生成的。这是一个最小的 Prometheus 配置,从thoughts-serviceusers-servicefrontend-service服务器中抓取。正如我们从前面的章节中所知,这些名称访问服务,并将连接到提供应用程序的 pod。它们将自动搜索/metrics路径。

这里有一个小注意事项。从 Prometheus 的角度来看,服务后面的一切都是相同的服务器。如果有多个正在提供服务的 pod,那么 Prometheus 访问的指标将被负载平衡,指标将不正确。

这可以通过更复杂的 Prometheus 设置来解决,其中我们安装 Prometheus 操作员,但这超出了本书的范围。但是,这对于生产系统非常推荐。实质上,它允许我们注释每个不同的部署,以便动态更改 Prometheus 配置。这意味着一旦设置完成,我们就可以自动访问由 pod 公开的所有指标端点。Prometheus 操作员注释使我们非常容易向指标系统添加新元素。

如果您想了解如何执行此操作,请查看以下文章:sysdig.com/blog/kubernetes-monitoring-prometheus-operator-part3

部署将从prom/prometheus中的公共 Prometheus 镜像创建一个容器,如下所示:

spec:
  containers:
  - name: prometheus
    image: prom/prometheus
    volumeMounts:
    - mountPath: /etc/prometheus/prometheus.yml
      subPath: prometheus.yaml
      name: volume-config
    ports:
    - containerPort: 9090
    volumes:
    - name: volume-config
      configMap:
        name: prometheus-config

它还将ConfigMap挂载为卷,然后作为文件挂载到/etc/prometheus/prometheus.yml中。这将使用该配置启动 Prometheus 服务器。容器打开端口9090,这是 Prometheus 的默认端口。

在这一点上,请注意我们委托了 Prometheus 容器。这是使用 Kubernetes 的优势之一:我们可以使用标准可用的容器,以最小的配置为我们的集群添加功能。我们甚至不必担心操作系统或 Prometheus 容器的打包。这简化了操作,并允许我们标准化我们使用的工具。

部署的 Prometheus 服务器可以通过http://prometheus.example.local/访问,如 Ingress 和 service 中所述。

这显示了一个图形界面,可用于绘制图形,如下面的屏幕截图所示:

表达式搜索框还将自动完成指标,有助于发现过程。

该界面还显示了来自 Prometheus 的其他有趣元素,例如配置或目标的状态:

此界面中的图形可用,但我们可以通过 Grafana 设置更复杂和有用的仪表板。让我们看看这个设置是如何工作的。

绘制图形和仪表板

所需的 Kubernetes 配置grafana.yaml可在本书的 GitHub 存储库的Chapter10/kubernetes/metrics目录中找到。就像我们使用单个文件配置 Prometheus 一样,我们也使用单个文件配置 Grafana。

出于与之前解释的相同原因,我们不会显示 Ingress 和 service。部署很简单,但我们挂载了两个卷而不是一个,如下面的代码所示:

spec:
  containers:
    - name: grafana
      image: grafana/grafana
      volumeMounts:
        - mountPath: /etc/grafana/provisioning
                     /datasources/prometheus.yaml
          subPath: prometheus.yaml
          name: volume-config
        - mountPath: /etc/grafana/provisioning/dashboards
          name: volume-dashboard
      ports:
        - containerPort: 3000
  volumes:
    - name: volume-config
      configMap:
        name: grafana-config
    - name: volume-dashboard
      configMap:
        name: grafana-dashboard

volume-config卷共享一个配置 Grafana 的单个文件。volume-dashboard卷添加了一个仪表板。后者挂载了一个包含两个文件的目录。这两个挂载点都在 Grafana 期望的配置文件的默认位置。

volume-config卷设置了 Grafana 将接收数据以绘制的数据源的位置:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-config
  namespace: example
data:
  prometheus.yaml: |
      apiVersion: 1

      datasources:
      - name: Prometheus
        type: prometheus
        url: http://prometheus-service
        access: proxy
        isDefault: true

数据来自http://prometheus-service,指向我们之前配置的 Prometheus 服务。

volume-dashboard定义了两个文件,dashboard.yamldashboard.json

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-dashboard
  namespace: example
data:
  dashboard.yaml: |
    apiVersion: 1

    providers:
    - name: 'Example'
      orgId: 1
      folder: ''
      type: file
      editable: true
      options:
        path: /etc/grafana/provisioning/dashboards
  dashboard.json: |-
    <JSON FILE>

dashboard.yaml是一个简单的文件,指向我们可以找到描述系统可用仪表板的 JSON 文件的目录。我们指向相同的目录以挂载所有内容到单个卷。

dashboard.json在此处被编辑以节省空间;查看本书的 GitHub 存储库以获取数据。

dashboard.json以 JSON 格式描述了一个仪表板。通过 Grafana 用户界面可以自动生成此文件。添加更多.json文件将创建新的仪表板。

Grafana 用户界面

通过访问http://grafana.example.local并使用您的登录/密码详细信息,即admin/admin(默认值),您可以访问 Grafana 用户界面:

从那里,您可以检查仪表板,该仪表板可以在左侧中央列中找到:

这捕捉了对 Flask 的调用,无论是数量还是95^(th)百分位时间。每个单独的图形都可以进行编辑,以便我们可以看到生成它的配方:

左侧的图标允许我们更改系统中运行的查询,更改可视化(单位、颜色、条形或线条、绘图的类型等),添加名称等一般信息,并创建警报。

Grafana 用户界面允许我们进行实验,因此非常互动。花些时间尝试不同的选项,并学习如何呈现数据。

查询部分允许我们从 Prometheus 添加和显示指标。请注意默认附近的 Prometheus 徽标,这是数据源。

每个查询都有一个从 Prometheus 中提取数据的指标部分。

查询 Prometheus

Prometheus 有自己的查询语言称为 PromQL。这种语言非常强大,但它也有一些特殊之处。

Grafana UI 通过自动完成查询来帮助我们,这使我们可以轻松搜索指标名称。您可以直接在仪表板中进行实验,但是 Grafana 上有一个名为 Explore 的页面,允许您从任何仪表板进行查询,并提供一些不错的提示,包括基本元素。这在左侧边栏中用一个指南针图标表示。

首先要记住的是了解 Prometheus 指标。鉴于其采样方法,大多数指标是单调递增的。这意味着绘制指标将显示一条不断上升的线。

要获得值在一段时间内变化的速率,需要使用rate

rate(flask_http_request_duration_seconds_count[5m])

这将生成每秒的请求率,平均使用5分钟的移动窗口。速率可以进一步使用sumby进行聚合:

sum(rate(flask_http_request_duration_seconds_count[5m])) by (path)

要计算时间,可以使用avg。您还可以按多个标签进行分组:

avg(rate(flask_http_request_duration_seconds_bucket[5m])) by (method, path)

但是,您也可以设置分位数,就像我们在图表中可以做的那样。我们乘以 100 以获得以毫秒为单位的时间,而不是秒,并按methodpath进行分组。现在,le是一个特殊的标签,会自动创建并将数据分成多个桶。histogram_quantile函数使用这个来计算分位数:

histogram_quantile(0.95, sum(rate(flask_http_request_duration_seconds_bucket[5m])) by (method, path, le)) * 1000

可以对指标进行过滤,以便仅显示特定的标签。它们还可以用于不同的功能,例如除法,乘法等。

当我们试图显示几个指标的结果时,例如成功请求占总数的百分比时,Prometheus 查询可能会有点长而复杂。一定要测试结果是否符合您的预期,并留出时间来调整请求。

如果您想了解更多,请务必查看 Prometheus 文档:prometheus.io/docs/prometheus/latest/querying/basics/

更新仪表板

仪表板可以进行交互式更改和保存,但在我们的 Kubernetes 配置中,我们设置了包含文件的卷为非持久性。因此,重新启动 Grafana 将丢弃任何更改,并重新应用Chapter10/kubernetes/metrics/grafana.yaml文件中volume-dashboard中定义的配置。

这实际上是一件好事,因为我们将相同的 GitOps 原则应用于将完整配置存储在 Git 存储库中。

但是,正如您所看到的,包含在grafana.yaml文件中的仪表板的完整 JSON 描述非常长,因为参数的数量以及手动更改它们的困难。

最好的方法是交互式地更改仪表板,然后使用菜单顶部的共享文件按钮将其导出为 JSON 文件。然后,可以将 JSON 文件添加到配置中:

然后可以重新部署 Grafana pod,并且仪表板中的保存更改将包含在内。然后可以通过常规流程在 Git 中更新 Kubernetes 配置。

一定要探索仪表板的所有可能性,包括设置变量的选项,以便您可以使用相同的仪表板监视不同的应用程序或环境以及不同类型的可视化工具。有关更多信息,请参阅完整的 Grafana 文档:grafana.com/docs/reference/

有了可用的指标,我们可以利用它们来积极主动地了解系统并预测任何问题。

积极主动

指标显示了整个集群状态的聚合视图。它们使我们能够检测趋势问题,但很难找到单个的偶发错误。

不要低估它们。它们对于成功的监控至关重要,因为它们告诉我们系统是否健康。在一些公司,最关键的指标会在墙上的屏幕上显著显示,以便运维团队可以随时看到并迅速做出反应。

在系统中找到指标的适当平衡并不是一项简单的任务,需要时间和反复试验。然而,对于在线服务来说,总有四个重要的指标。它们分别是:

  • 延迟:系统响应请求所需的毫秒数。

根据不同的时间,可以使用不同的时间单位,比如秒或微秒。根据我的经验,毫秒是足够的,因为在 Web 应用系统中,大多数请求的响应时间应该在 50 毫秒到 1 秒之间。在这里,花费 50 毫秒的系统速度太慢,而花费 1 秒的系统则是非常高效的。

  • 流量:单位时间内通过系统的请求数,即每秒或每分钟的请求数。

  • 错误:收到的返回错误的请求的百分比。

  • 饱和度:集群的容量是否有足够的余地。这包括诸如硬盘空间、内存等元素。例如,有 20%的可用 RAM 内存。

要测量饱和度,请记住安装可用的导出器,它们将自动收集大部分硬件信息(内存、硬盘空间等)。如果您使用云提供商,通常他们也会公开一套相关的指标,例如 AWS 的 CloudWatch。

这些指标可以在 Google SRE Book 中找到,被称为四个黄金信号,被认为是成功监控的最重要的高级元素。

警报

当指标出现问题时,应该生成自动警报。Prometheus 有一个包含的警报系统,当定义的指标满足定义的条件时会触发警报。

查看有关警报的 Prometheus 文档以获取更多信息:prometheus.io/docs/alerting/overview/

Prometheus 的 Alertmanager 可以执行某些操作,比如根据规则发送电子邮件进行通知。该系统可以连接到集成的事件解决方案,如 OpsGenie(www.opsgenie.com),以生成各种警报和通知,如电子邮件、短信、电话等。

日志也可以用来创建警报。有一些工具允许我们在引发ERROR时创建一个条目,比如Sentry。这使我们能够检测问题并积极地进行补救,即使集群的健康状态没有受到影响。

一些商业工具可以处理日志,比如 Loggly,允许我们从日志中派生指标,根据日志的类型绘制图表,或者从日志中提取值并将其用作数值。虽然不如 Prometheus 这样的系统完整,但它们可以监视一些数值。它们还允许我们在达到阈值时发出通知。

监控领域充满了各种产品,有免费的也有付费的,可以帮助我们处理这些问题。虽然可以创建一个完全内部的监控系统,但能够分析商业云工具是否有帮助是至关重要的。功能的水平以及它们与有用工具的集成,比如外部警报系统,将很难复制和维护。

警报也是一个持续的过程。一些元素将在后续发现,新的警报将不得不被创建。务必投入时间,以确保一切都按预期工作。在系统不健康的时候,日志和指标将被使用,而在那些时刻,时间至关重要。您不希望因为主机参数配置不正确而猜测日志。

做好准备

备份如果没有经过测试和工作的恢复过程是没有用的,当检查监控系统是否产生有用信息时要采取主动措施。

特别是,尝试标准化日志,以便对包含什么信息以及其结构有一个良好的期望。不同的系统可能产生不同的日志,但最好让所有微服务以相同的格式记录日志。仔细检查任何参数,例如客户端引用或主机,是否被正确记录。

同样适用于指标。拥有一组所有人都理解的指标和仪表板将在跟踪问题时节省大量时间。

摘要

在本章中,我们学习了如何处理日志和指标,以及如何设置日志并使用syslog协议将其发送到集中式容器。我们描述了如何向不同的应用程序添加日志,如何包含请求 ID,以及如何从不同的微服务中生成自定义日志。然后,我们学习了如何制定策略,以确保日志在生产中是有用的。

我们还描述了如何在所有微服务中设置标准和自定义的 Prometheus 指标。我们启动了一个 Prometheus 服务器,并对其进行配置,以便从我们的服务收集指标。我们启动了一个 Grafana 服务,以便我们可以绘制指标,并创建了仪表板,以便我们可以显示集群的状态和正在运行的不同服务。

然后,我们向您介绍了 Prometheus 中的警报系统以及如何使用它来通知我们问题。请记住,有商业服务可以帮助您处理日志、指标和警报。分析您的选择,因为它们可以在维护成本方面为您节省大量时间和金钱。

在下一章中,我们将学习如何管理影响多个微服务的更改和依赖关系,以及如何处理配置和秘密。

问题

  1. 系统的可观察性是什么?

  2. 日志中有哪些不同的严重级别可用?

  3. 指标用于什么?

  4. 为什么需要向日志中添加请求 ID?

  5. Prometheus 有哪些可用的指标类型?

  6. 指标中的第 75 百分位是什么,它与平均值有何不同?

  7. 四个黄金信号是什么?

进一步阅读

您可以通过阅读监控 Dockerwww.packtpub.com/virtualization-and-cloud/monitoring-docker)来了解如何使用 Docker 使用不同工具和技术进行监控。要了解有关 Prometheus 和 Grafana 的更多信息,包括如何设置警报,请阅读使用 Prometheus 进行基础设施监控www.packtpub.com/virtualization-and-cloud/hands-infrastructure-monitoring-prometheus)。

监控只是成功运行服务的起点。要了解如何成功改进您的运营,请查看真实世界 SREwww.packtpub.com/web-development/real-world-sre)。

第十一章:处理系统中的变更、依赖和秘密

在本章中,我们将描述与多个微服务交互的不同元素。

我们将研究如何制定服务描述其版本的策略,以便依赖的微服务可以发现它们,并确保它们已经部署了正确的依赖关系。这将允许我们在依赖服务中定义部署顺序,并且如果不是所有依赖关系都准备好,将停止服务的部署。

本章描述了如何定义集群范围的配置参数,以便它们可以在多个微服务之间共享,并在单个位置进行管理,使用 Kubernetes ConfigMap。我们还将学习如何处理那些属于秘密的配置参数,比如加密密钥,这些密钥不应该对团队中的大多数人可见。

本章将涵盖以下主题:

  • 理解微服务之间的共享配置

  • 处理 Kubernetes 秘密

  • 定义影响多个服务的新功能

  • 处理服务依赖关系

在本章结束时,您将了解如何为安全部署准备依赖服务,以及如何在微服务中包含不会在其预期部署之外可访问的秘密。

技术要求

代码可在 GitHub 上的以下 URL 找到:github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11。请注意,该代码是Chapter10代码的扩展,其中包含本章描述的额外元素。结构相同,有一个名为microservices的子目录,其中包含代码,另一个名为kubernetes的子目录,其中包含 Kubernetes 配置文件。

要安装集群,您需要使用以下命令构建每个单独的微服务:

$ cd Chapter11/microservices/
$ cd rsyslog
$ docker-compose build
...
$ cd frontend
$ ./build-test.sh
...
$ cd thoughts_backend
$./build-test.sh
...
$ cd users_backend
$ ./build-test.sh
... 

这将构建所需的服务。

请注意,我们使用build-test.sh脚本。我们将在本章中解释它的工作原理。

然后,创建namespace示例,并使用Chapter11/kubernetes子目录中的配置启动 Kubernetes 集群:

$ cd Chapter11/kubernetes
$ kubectl create namespace example
$ kubectl apply --recursive -f .
...

这将在集群中部署微服务。

Chapter11中包含的代码存在一些问题,在修复之前将无法正确部署。这是预期的行为。在本章中,我们将解释两个问题:无法配置秘密,以及无法满足前端的依赖关系,导致无法启动。

继续阅读本章以找到所描述的问题。解决方案将作为评估提出。

要能够访问不同的服务,您需要更新您的/etc/hosts文件,包括以下行:

127.0.0.1 thoughts.example.local
127.0.0.1 users.example.local
127.0.0.1 frontend.example.local

有了这些,您就可以访问本章的服务了。

理解微服务之间的共享配置

某些配置可能适用于多个微服务。在我们的示例中,我们正在为数据库连接重复相同的值。我们可以使用 ConfigMap 并在不同的部署中共享它,而不是在每个部署文件中重复这些值。

我们已经看到如何在第十章 监控日志和指标设置指标部分中添加 ConfigMap 以包含文件。尽管它只用于单个服务。

ConfigMap 是一组键/值元素。它们可以作为环境变量或文件添加。在下一节中,我们将添加一个包含集群中所有共享变量的通用配置文件。

添加 ConfigMap 文件

configuration.yaml文件包含系统的公共配置。它位于Chapter11/kubernetes子目录中:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: shared-config
  namespace: example
data:
  DATABASE_ENGINE: POSTGRES
  POSTGRES_USER: postgres
  POSTGRES_HOST: "127.0.0.1"
  POSTGRES_PORT: "5432"
  THOUGHTS_BACKEND_URL: http://thoughts-service
  USER_BACKEND_URL: http://users-service

与数据库相关的变量,如DATABASE_ENGINEPOSTGRES_USERPOSTGRES_HOSTPOSTGRES_PORT,在 Thoughts Backend 和 Users Backend 之间共享。

POSTGRES_PASSWORD变量是一个密钥。我们将在本章的处理 Kubernetes 密钥部分中描述这一点。

THOUGHTS_BACKEND_URLUSER_BACKEND_URL变量在前端服务中使用。尽管它们在集群中是通用的。任何想要连接到 Thoughts Backend 的服务都应该使用与THOUGHTS_BACKEND_URL中描述的相同 URL。

尽管它目前只在单个服务 Frontend 中使用,但它符合系统范围的变量的描述,并应包含在通用配置中。

拥有共享变量存储库的一个优点是将它们合并。

在创建多个服务并独立开发它们的同时,很常见的情况是最终以两种略有不同的方式使用相同的信息。独立开发的团队无法完美共享信息,这种不匹配会发生。

例如,一个服务可以将一个端点描述为URL=http://service/api,另一个使用相同端点的服务将其描述为HOST=service PATH=/api。每个服务的代码处理配置方式不同,尽管它们连接到相同的端点。这使得以统一方式更改端点更加困难,因为需要在两个或更多位置以两种方式进行更改。

共享位置是首先检测这些问题的好方法,因为如果每个服务保留自己独立的配置,这些问题通常会被忽略,然后调整服务以使用相同的变量,减少配置的复杂性。

在我们的示例中,ConfigMap 的名称是shared-config,如元数据中所定义的,像任何其他 Kubernetes 对象一样,可以通过kubectl命令进行管理。

使用 kubectl 命令

可以使用通常的一组kubectl命令来检查 ConfigMap 信息。这使我们能够发现集群中定义的 ConfigMap 实例:

$ kubectl get configmap -n example shared-config
NAME               DATA AGE
shared-config      6    46m

请注意,ConfigMap 包含的键或变量的数量是显示的;在这里,它是6。要查看 ConfigMap 的内容,请使用describe

$ kubectl describe configmap -n example shared-config
Name: shared-config
Namespace: example
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
 {"apiVersion":"v1","data":{"DATABASE_ENGINE":"POSTGRES","POSTGRES_HOST":"127.0.0.1","POSTGRES_PORT":"5432","POSTGRES_USER":"postgres","THO...

Data
====
POSTGRES_HOST:
----
127.0.0.1
POSTGRES_PORT:
----
5432
POSTGRES_USER:
----
postgres
THOUGHTS_BACKEND_URL:
----
http://thoughts-service
USER_BACKEND_URL:
----
http://users-service
DATABASE_ENGINE:
----
POSTGRES

如果需要更改 ConfigMap,可以使用kubectl edit命令,或者更好的是更改configuration.yaml文件,并使用以下命令重新应用它:

$ kubectl apply -f kubernetes/configuration.yaml

这将覆盖所有的值。

配置不会自动应用到 Kubernetes 集群。您需要重新部署受更改影响的 pod。最简单的方法是删除受影响的 pod,并允许部署重新创建它们。

另一方面,如果配置了 Flux,它将自动重新部署依赖的 pod。请记住,更改 ConfigMap(在所有 pod 中引用)将触发在该情况下所有 pod 的重新部署。

我们现在将看到如何将 ConfigMap 添加到部署中。

将 ConfigMap 添加到部署

一旦 ConfigMap 就位,它可以用于与不同部署共享其变量,保持一个中央位置来更改变量并避免重复。

让我们看看微服务(Thoughts Backend、Users Backend 和 Frontend)的每个部署如何使用shared-config ConfigMap。

Thoughts Backend ConfigMap 配置

Thoughts Backend 部署定义如下:

spec:
    containers:
        - name: thoughts-backend-service
          image: thoughts_server:v1.5
          imagePullPolicy: Never
          ports:
              - containerPort: 8000
          envFrom:
              - configMapRef:
                    name: shared-config
          env:
              - name: POSTGRES_DB
                value: thoughts
          ...

完整的shared-config ConfigMap 将被注入到 pod 中。请注意,这包括以前在 pod 中不可用的THOUGHTS_BACKEND_URLUSER_BACKEND_URL环境变量。可以添加更多环境变量。在这里,我们保留了POSTGRES_DB,而没有将其添加到 ConfigMap 中。

我们可以在 pod 中使用exec来确认它。

请注意,为了能够连接到密钥,它应该被正确配置。请参阅处理 Kubernetes 密钥部分。

要在容器内部检查,请检索 pod 名称并在其中使用exec,如下面的命令所示:

$ kubectl get pods -n example
NAME                              READY STATUS  RESTARTS AGE
thoughts-backend-5c8484d74d-ql8hv 2/2   Running 0        17m
...
$ kubectl exec -it thoughts-backend-5c8484d74d-ql8hv -n example /bin/sh
Defaulting container name to thoughts-backend-service.
/opt/code $ env | grep POSTGRES
DATABASE_ENGINE=POSTGRESQL
POSTGRES_HOST=127.0.0.1
POSTGRES_USER=postgres
POSTGRES_PORT=5432
POSTGRES_DB=thoughts
/opt/code $ env | grep URL
THOUGHTS_BACKEND_URL=http://thoughts-service
USER_BACKEND_URL=http://users-service

env命令返回所有环境变量,但 Kubernetes 会自动添加很多环境变量。

用户后端 ConfigMap 配置

用户后端配置与我们刚刚看到的前一种类型的配置类似:

spec:
    containers:
        - name: users-backend-service
          image: users_server:v2.3
          imagePullPolicy: Never
          ports:
              - containerPort: 8000
          envFrom:
              - configMapRef:
                    name: shared-config
          env:
              - name: POSTGRES_DB
                value: thoughts
          ...

POSTGRES_DB的值与 Thoughts 后端中的相同,但我们将其留在这里以展示如何添加更多环境变量。

前端 ConfigMap 配置

前端配置仅使用 ConfigMap,因为不需要额外的环境变量:

spec:
    containers:
        - name: frontend-service
          image: thoughts_frontend:v3.7
          imagePullPolicy: Never
          ports:
              - containerPort: 8000
          envFrom:
              - configMapRef:
                    name: shared-config

前端 pod 现在还将包括连接到数据库的信息,尽管它不需要。对于大多数配置参数来说,这是可以的。

如果需要,您还可以使用多个 ConfigMaps 来描述不同的配置组。不过,将它们放在一个大桶中处理会更简单。这将有助于捕获重复的参数,并确保所有微服务中都有所需的参数。

然而,一些配置参数必须更加小心处理,因为它们将是敏感的。例如,我们从shared-config ConfigMap 中省略了POSTGRES_PASSWORD变量。这允许我们登录到数据库,并且不应该存储在任何带有其他参数的文件中,以避免意外暴露。

为了处理这种信息,我们可以使用 Kubernetes 秘密。

处理 Kubernetes 秘密

秘密是一种特殊的配置。它们需要受到保护,以免被其他使用它们的微服务读取。它们通常是敏感数据,如私钥、加密密钥和密码。

记住,读取秘密是有效的操作。毕竟,它们需要被使用。秘密与其他配置参数的区别在于它们需要受到保护,因此只有授权的来源才能读取它们。

秘密应该由环境注入。这要求代码能够检索配置秘密并在当前环境中使用适当的秘密。它还避免了在代码中存储秘密。

记住永远不要在 Git 存储库中提交生产秘密。即使删除了 Git 树,秘密也是可检索的。这包括 GitOps 环境。

还要为不同的环境使用不同的秘密。生产秘密需要比测试环境中的秘密更加小心。

在我们的 Kubernetes 配置中,授权的来源是使用它们的微服务以及通过kubectl访问的系统管理员。

让我们看看如何管理这些秘密。

在 Kubernetes 中存储秘密

Kubernetes 将秘密视为一种特殊类型的 ConfigMap 值。它们可以在系统中定义,然后以与 ConfigMap 相同的方式应用。与一般的 ConfigMap 的区别在于信息在内部受到保护。虽然它们可以通过kubectl访问,但它们受到意外暴露的保护。

可以通过kubectl命令在集群中创建秘密。它们不应该通过文件和 GitOps 或 Flux 创建,而应该手动创建。这样可以避免将秘密存储在 GitOps 存储库下。

需要秘密来操作的 pod 将在其部署文件中指示。这是安全的存储在 GitOps 源代码控制下,因为它不存储秘密,而只存储对秘密的引用。当 pod 被部署时,它将使用适当的引用并解码秘密。

登录到 pod 将授予您对秘密的访问权限。这是正常的,因为在 pod 内部,应用程序需要读取其值。授予对 pod 中执行命令的访问权限将授予他们对内部秘密的访问权限,因此请记住这一点。您可以阅读 Kubernetes 文档了解秘密的最佳实践,并根据您的要求进行调整(kubernetes.io/docs/concepts/configuration/secret/#best-practices)。

既然我们知道如何处理它们,让我们看看如何创建这些秘密。

创建秘密

让我们在 Kubernetes 中创建这些秘密。我们将存储以下秘密:

  • PostgreSQL 密码

  • 用于签署和验证请求的公钥和私钥

我们将它们存储在同一个 Kubernetes 秘密中,该秘密可以有多个密钥。以下命令显示了如何生成一对密钥:

$ openssl genrsa -out private_key.pem 2048
Generating RSA private key, 2048 bit long modulus
........+++
.................+++
e is 65537 (0x10001)
$ openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pub
writing RSA key
$ ls 
private_key.pem public_key.pub

这些密钥是唯一的。我们将使用它们来替换前几章中存储的示例密钥。

在集群中存储秘密

将秘密存储在集群中,在thoughts-secrets秘密下。请记住将其存储在example命名空间中:

$ kubectl create secret generic thoughts-secrets --from-literal=postgres-password=somepassword --from-file=private_key.pem --from-file=public_key.pub -n example

您可以列出命名空间中的秘密:

$ kubectl get secrets -n example
NAME             TYPE   DATA AGE
thoughts-secrets Opaque 3    41s

您还可以描述更多信息的秘密:

$ kubectl describe secret thoughts-secrets -n example
Name: thoughts-secrets
Namespace: default
Labels: <none>
Annotations: <none>

Type: Opaque

Data
====
postgres-password: 12 bytes
private_key.pem: 1831 bytes
public_key.pub: 408 bytes

您可以获取秘密的内容,但数据以 Base64 编码检索。

Base64 是一种编码方案,允许您将二进制数据转换为文本,反之亦然。它被广泛使用。这使您可以存储任何二进制秘密,而不仅仅是文本。这也意味着在检索时秘密不会以明文显示,从而在意外显示在屏幕上等情况下增加了一层保护。

要获取秘密,请使用如下所示的常规kubectl get命令。我们使用base64命令对其进行解码:

$ kubectl get secret thoughts-secrets -o yaml -n example
apiVersion: v1
data:
 postgres-password: c29tZXBhc3N3b3Jk
 private_key.pem: ...
 public_key.pub: ...
$ echo c29tZXBhc3N3b3Jk | base64 --decode
somepassword

同样,如果要编辑秘密以更新它,输入应该以 Base64 编码。

秘密部署配置

我们需要在部署配置中配置秘密的使用,以便在所需的 pod 中可用。例如,在用户后端的deployment.yaml配置文件中,我们有以下代码:

spec:
    containers:
    - name: users-backend-service
      ...
      env:
      ...
      - name: POSTGRES_PASSWORD
        valueFrom:
          secretKeyRef:
            name: thoughts-secrets
            key: postgres-password
        volumeMounts:
        - name: sign-keys
          mountPath: "/opt/keys/"

    volumes:
    - name: sign-keys
      secret:
        secretName: thoughts-secrets
        items:
        - key: public_key.pub
          path: public_key.pub
        - key: private_key.pem
          path: private_key.pem

我们创建了来自秘密的POSTGRES_PASSWORD环境变量。我们还创建了一个名为sign-keys的卷,其中包含两个密钥文件,public_key.pubprivate_key.pem。它挂载在/opt/keys/路径中。

类似地,Thoughts 后端的deployment.yaml文件包括秘密,但只包括 PostgreSQL 密码和public_key.pub。请注意,私钥没有添加,因为 Thoughts 后端不需要它,也不可用。

对于前端,只需要公钥。现在,让我们来建立如何检索这些秘密。

应用程序检索秘密

对于POSTGRES_PASSWORD环境变量,我们不需要更改任何内容。它已经是一个环境变量,代码已经从中提取它。

但是对于存储为文件的秘密,我们需要从适当的位置检索它们。存储为文件的秘密是签署身份验证标头的关键。公共文件在所有微服务中都是必需的,而私钥仅在用户后端中使用。

现在,让我们来看一下用户后端的config.py文件:

import os
PRIVATE_KEY = ...
PUBLIC_KEY = ...

PUBLIC_KEY_PATH = '/opt/keys/public_key.pub'
PRIVATE_KEY_PATH = '/opt/keys/private_key.pem'

if os.path.isfile(PUBLIC_KEY_PATH):
    with open(PUBLIC_KEY_PATH) as fp:
        PUBLIC_KEY = fp.read()

if os.path.isfile(PRIVATE_KEY_PATH):
    with open(PRIVATE_KEY_PATH) as fp:
        PRIVATE_KEY = fp.read()

当前密钥仍然作为默认值存在。当秘密文件没有挂载时,它们将用于单元测试。

再次强调,请不要使用这些密钥。这些仅用于运行测试,并且对于任何可以访问本书的人都是可用的。

如果/opt/keys/路径中存在文件,它们将被读取,并且内容将被存储在适当的常量中。用户后端需要公钥和私钥。

在 Thoughts 后端的config.py文件中,我们只检索公钥,如下所示:

import os
PUBLIC_KEY = ...

PUBLIC_KEY_PATH = '/opt/keys/public_key.pub'

if os.path.isfile(PUBLIC_KEY_PATH):
    with open(PUBLIC_KEY_PATH) as fp:
        PUBLIC_KEY = fp.read()

前端服务将公钥添加到settings.py文件中:

TOKENS_PUBLIC_KEY = ...

PUBLIC_KEY_PATH = '/opt/keys/public_key.pub'

if os.path.isfile(PUBLIC_KEY_PATH):
    with open(PUBLIC_KEY_PATH) as fp:
        TOKENS_PUBLIC_KEY = fp.read()

此配置使秘密对应用程序可用,并为秘密值关闭了循环。现在,微服务集群使用来自秘密值的签名密钥,这是一种安全存储敏感数据的方式。

定义影响多个服务的新功能

我们谈到了单个微服务领域内的更改请求。但是,如果我们需要部署在两个或多个微服务中运行的功能,该怎么办呢?

这种类型的功能应该相对罕见,并且是与单体应用程序相比微服务中的开销的主要原因之一。在单体应用程序中,这种情况根本不可能发生,因为一切都包含在单体应用程序的墙内。

与此同时,在微服务架构中,这是一个复杂的更改。这至少涉及到每个相关微服务中的两个独立功能,这些功能位于两个不同的存储库中。很可能这些存储库将由两个不同的团队开发,或者至少负责每个功能的人将不同。

逐个更改

为了确保功能可以顺利部署,一次一个,它们需要保持向后兼容。这意味着您需要能够在服务 A 已部署但服务 B 尚未部署的中间阶段生存。微服务中的每个更改都需要尽可能小,以最小化风险,并且应逐个引入更改。

为什么不同时部署它们?因为同时发布两个微服务是危险的。首先,部署不是瞬时的,因此会有时刻,过时的服务将发送或接收系统尚未准备处理的调用。这将导致可能影响您的客户的错误。

但是存在一种情况,其中一个微服务不正确并且需要回滚。然后,系统将处于不一致状态。依赖的微服务也需要回滚。这本身就是有问题的,但是当在调试此问题期间,两个微服务都卡住并且在问题得到解决之前无法更新时,情况会变得更糟。

在健康的微服务环境中,部署会经常发生。因为另一个服务需要工作而不得不停止微服务的流水线是一个糟糕的处境,它只会增加压力和紧迫感。

记住我们谈到了部署和变更的速度。经常部署小的增量通常是确保每次部署都具有高质量的最佳方式。增量工作的持续流非常重要。

由于错误而中断此流程是不好的,但是如果无法部署影响了多个微服务的速度,影响会迅速扩大。

同时部署多个服务也可能导致死锁,其中两个服务都需要进行修复工作。这会使开发和解决问题的时间变得更加复杂。

需要进行分析以确定哪个微服务依赖于另一个而不是同时部署。大多数情况下,这是显而易见的。在我们的例子中,前端依赖于 Thoughts 后端,因此任何涉及它们两者的更改都需要从 Thoughts 后端开始,然后转移到前端。

实际上,用户后端是两者的依赖项,因此假设有一个影响它们三者的更改,您需要首先更改用户后端,然后是 Thoughts 后端,最后是前端。

请记住,有时部署可能需要跨多个服务进行多次移动。例如,让我们想象一下,我们对身份验证标头的签名机制进行了更改。然后,流程应该如下:

  1. 在用户后端实施新的身份验证系统,但通过配置更改继续使用旧系统生成令牌。到目前为止,集群仍在使用旧的身份验证流程。

  2. 更改 Thoughts 后端以允许与旧系统和新的身份验证系统一起工作。请注意,它尚未激活。

  3. 更改前端以使其与两种身份验证系统一起工作。但是,此时新系统尚未被使用。

  4. 在用户后端更改配置以生成新的身份验证令牌。现在是新系统开始使用的时候。在部署过程中,可能会生成一些旧系统令牌。

  5. 用户后端和前端将使用系统中的任何令牌,无论是新的还是旧的。旧令牌将随着时间的推移而消失,因为它们会过期。只有新令牌才会被创建。

  6. 作为可选阶段,可以从系统中删除旧的身份验证系统。三个系统可以在没有任何依赖关系的情况下删除它们,因为此时系统不再使用。

在整个过程的任何步骤中,服务都不会中断。每个单独的更改都是安全的。该过程正在慢慢使整个系统发展,但如果出现问题,每个单独的步骤都是可逆的,并且服务不会中断。

系统往往通过添加新功能来发展,清理阶段并不常见。通常,即使功能在任何地方都没有使用,系统也会长时间保留已弃用的功能。

我们将在《第十二章》跨团队协作和沟通中更详细地讨论清理工作。

此过程也可能需要进行配置更改。例如,更改用于签署身份验证标头的私钥将需要以下步骤:

  1. 使 Thoughts 后端和前端能够处理多个公钥。这是一个先决条件和一个新功能。

  2. 更改 Thoughts 后端中处理的密钥,使其同时具有旧公钥和新公钥。到目前为止,系统中没有使用新密钥签名的标头。

  3. 更改前端中处理的密钥,使其同时具有旧密钥和新密钥。但是,系统中仍没有使用新密钥签名的标头。

  4. 更改用户后端的配置以使用新的私钥。从现在开始,系统中有用新私钥签名的标头。其他微服务能够处理它们。

  5. 系统仍然接受用旧密钥签名的标头。等待一个安全期以确保所有旧标头都已过期。

  6. 删除用户后端的旧密钥配置。

步骤 2 至 6 可以每隔几个月重复使用新密钥。

这个过程被称为密钥轮换,被认为是一种良好的安全实践,因为它减少了密钥有效的时间,缩短了系统暴露于泄露密钥的时间窗口。为简单起见,我们没有在示例系统中实施它,但建议您这样做。尝试更改示例代码以实现此密钥轮换示例!

完整的系统功能可能涉及多个服务和团队。为了帮助协调系统的依赖关系,我们需要知道某个服务的特定依赖项何时部署并准备就绪。我们将在《第十二章》跨团队协作和沟通中讨论团队间的沟通,但我们可以通过使服务 API 明确描述已部署的服务版本来通过编程方式进行帮助,正如我们将在处理服务依赖关系部分中讨论的那样。

如果新版本出现问题,刚刚部署的版本可以通过回滚快速恢复。

回滚微服务

回滚是将微服务之一迅速退回到先前版本的过程。

当新版本出现灾难性错误时,可以触发此过程,以便快速解决问题。鉴于该版本已经兼容,可以在非常短的反应时间内放心地进行此操作。通过 GitOps 原则,可以执行revert提交以恢复旧版本。

git revert命令允许您创建一个撤消另一个提交的提交,以相反的方式应用相同的更改。

这是撤消特定更改的快速方法,并允许稍后撤消撤消并重新引入更改。您可以查看 Git 文档以获取更多详细信息(git-scm.com/docs/git-revert)。

鉴于保持前进的战略性方法,回滚是一种临时措施,一旦实施,将停止微服务中的新部署。应尽快创建一个解决导致灾难性部署的错误的新版本,以保持正常的发布流程。

随着部署次数的增加,并且在适当的位置进行更好的检查,回滚将变得越来越少。

处理服务依赖关系

为了让服务检查它们的依赖项是否具有正确的版本,我们将使服务通过 RESTful 端点公开它们的版本。

我们将遵循 GitHub 上的 Thoughts Backend 示例,网址为:github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11/microservices/thoughts_backend

在前端检查版本是否可用(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11/microservices/frontend)。

该过程的第一步是为每个服务正确定义版本。

服务版本控制

为了清晰地了解软件的进展,我们需要命名要部署的不同版本。由于我们使用git来跟踪更改,系统中的每个提交都有一个独立的提交 ID,但它没有遵循任何特定的模式。

为了赋予其意义并对其进行排序,我们需要开发一个版本模式。有多种制定版本模式的方法,包括按发布日期(Ubuntu 使用此方法)或按major.minor.patch

在所有地方使用相同的版本控制方案有助于在团队之间发展共同的语言和理解。它还有助于管理了解变化,无论是在发布时的变化还是变化的速度。与您的团队商定一个在您的组织中有意义的版本控制方案,并在所有服务中遵循它。

在此示例中,我们将使用vMajor.Minor模式,并将用户后端的版本设置为v2.3

软件版本控制中最常见的模式是语义版本控制。这种版本控制模式对于软件包和面向客户的 API 非常有用,但对于内部微服务 API 则不太有用。让我们看看它的特点是什么。

语义版本控制

语义版本控制对不同版本号的每个更改赋予了含义。这使得很容易理解各个版本之间的变化范围,以及更新是否对依赖系统有风险。

语义版本控制使用三个数字定义每个版本:主要版本、次要版本和补丁版本,通常描述为major.minor.patch

增加这些数字中的任何一个都具有特定的含义,如下所示:

  • 增加主要版本号会产生不兼容的变化。

  • 增加次要版本号会添加新功能,但保持向后兼容。

  • 增加补丁号修复错误,但不添加任何新功能。

例如,Python 按照以下模式工作:

  • Python 3 与 Python 2 包含了兼容性变化。

  • Python 3.7 版本与 Python 3.6 相比引入了新功能。

  • Python 3.7.4 版本相对于 Python 3.7.3 增加了安全性和错误修复。

这种版本控制方案在与外部合作伙伴的沟通中非常有用,并且非常适用于大型发布和标准软件包。但对于微服务中的小型增量变化,它并不是非常有用。

正如我们在前面的章节中讨论的那样,持续集成的关键是进行非常小的更改。它们不应该破坏向后兼容性,但随着时间的推移,旧功能将被删除。每个微服务都以受控的方式与其他服务协同工作。与外部包相比,没有必要具有如此强烈的功能标签。服务的消费者是集群中受严格控制的其他微服务。

一些项目由于操作方式的改变而放弃了语义版本。例如,Linux 内核停止使用语义版本来生成没有特定含义的新版本(lkml.iu.edu/hypermail/linux/kernel/1804.1/06654.html),因为从一个版本到下一个版本的更改相对较小。

Python 也将版本 4.0 视为在 3.9 之后的版本,并且不像 Python 3 那样有重大变化(www.curiousefficiency.org/posts/2014/08/python-4000.html)。

这就是为什么在内部建议使用语义版本。保持类似的版本方案可能是有用的,但不要强制它进行兼容性更改,只需增加数字,而不对何时更改次要或主要版本做出具体要求。

然而,从外部来看,版本号可能仍然具有营销意义。对于外部可访问的端点,使用语义版本可能是有趣的。

一旦确定了服务的版本,我们就可以着手创建一个公开此信息的端点。

添加版本端点

要部署的版本可以从 Kubernetes 部署或 GitOps 配置中读取。但是存在一个问题。一些配置可能会误导或不唯一地指向单个镜像。例如,latest标签可能在不同时间代表不同的容器,因为它会被覆盖。

此外,还存在访问 Kubernetes 配置或 GitOps 存储库的问题。对于开发人员来说,也许这些配置是可用的,但对于微服务来说不会(也不应该)。

为了让集群中的其他微服务发现服务的版本,最好的方法是在 RESTful API 中明确创建一个版本端点。服务版本的发现是被授予的,因为它使用与任何其他请求中将使用的相同接口。让我们看看如何实现它。

获取版本

为了提供版本,我们首先需要将其记录到服务中。

正如我们之前讨论过的,版本是存储为 Git 标签的。这将是我们版本的标准。我们还将添加提交的 Git SHA-1,以避免任何差异。

SHA-1 是一个唯一的标识符,用于标识每个提交。它是通过对 Git 树进行哈希处理而生成的,因此能够捕获任何更改——无论是内容还是树历史。我们将使用 40 个字符的完整 SHA-1,尽管有时它会被缩写为八个或更少。

提交的 SHA-1 可以通过以下命令获得:

$ git log --format=format:%H -n 1

这将打印出最后一次提交的信息,以及带有%H描述符的 SHA。

要获取此提交所指的标签,我们将使用git-describe命令:

$ git describe --tags

基本上,git-describe会找到最接近当前提交的标签。如果此提交由标签标记,正如我们的部署应该做的那样,它将返回标签本身。如果没有,它将在标签后缀中添加有关提交的额外信息,直到达到当前提交。以下代码显示了如何使用git describe,具体取决于代码的提交版本。请注意,与标签不相关的代码将返回最接近的标签和额外的数字:

$ # in master branch, 17 commits from the tag v2.3
$ git describe
v2.3-17-g2257f9c
$ # go to the tag
$ git checkout v2.3
$ git describe
v2.3

这将始终返回一个版本,并允许我们一目了然地检查当前提交的代码是否在git中标记。

将部署到环境中的任何内容都应该被标记。本地开发是另一回事,因为它包括尚未准备好的代码。

我们可以以编程方式存储这两个值,从而使我们能够自动地进行操作,并将它们包含在 Docker 镜像中。

将版本存储在镜像中

我们希望在镜像内部有版本可用。由于镜像是不可变的,所以在构建过程中实现这一目标是我们的目标。我们需要克服的限制是 Dockerfile 过程不允许我们在主机上执行命令,只能在容器内部执行。我们需要在构建时向 Docker 镜像中注入这些值。

一个可能的替代方案是在容器内安装 Git,复制整个 Git 树,并获取值。通常不鼓励这样做,因为安装 Git 和完整的源代码树会给容器增加很多空间,这是不好的。在构建过程中,我们已经有了 Git 可用,所以我们只需要确保外部注入它,这在构建脚本中很容易做到。

通过ARG参数传递值的最简单方法。作为构建过程的一部分,我们将把它们转换为环境变量,这样它们将像配置的任何其他部分一样容易获取。让我们来看看以下代码中的 Dockerfile:

# Prepare the version
ARG VERSION_SHA="BAD VERSION"
ARG VERSION_NAME="BAD VERSION"
ENV VERSION_SHA $VERSION_SHA
ENV VERSION_NAME $VERSION_NAME

我们接受一个ARG参数,然后通过ENV参数将其转换为环境变量。为了简单起见,两者都具有相同的名称。ARG参数对于特殊情况有一个默认值。

使用build.sh脚本构建后,这使得版本在构建后(在容器内部)可用,该脚本获取值并调用docker-compose进行构建,使用版本作为参数,具体步骤如下:

# Obtain the SHA and VERSION
VERSION_SHA=`git log --format=format:%H -n 1`
VERSION_NAME=`git describe --tags`
# Build using docker-compose with arguments
docker-compose build --build-arg VERSION_NAME=${VERSION_NAME} --build-arg VERSION_SHA=${VERSION_SHA}
# Tag the resulting image with the version
docker tag thoughts_server:latest throughs_server:${VERSION_NAME}

在构建过程之后,版本作为标准环境变量在容器内部可用。

在本章的每个微服务中都包含了一个脚本(例如,github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter11/microservices/thoughts_backend/build-test.sh)。这个脚本模拟 SHA-1 和版本名称,以创建一个用于测试的合成版本。它为用户后端设置了v2.3版本,为思想后端设置了v1.5版本。这些将被用作我们代码中的示例。

检查 Kubernetes 部署是否包含这些版本(例如,github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter11/microservices/thoughts_backend/docker-compose.yaml#L21镜像是v1.5版本)。

此外,VERSION_NAME也可以作为 CI 管道的参数传递给脚本。为此,您需要替换脚本以接受外部参数,就像在build-ci.sh脚本中看到的那样:

#!/bin/bash
if [ -z "$1" ]
  then
    # Error, not version name
    echo "No VERSION_NAME supplied"
    exit -1
fi

VERSION_SHA=`git log --format=format:%H -n 1`
VERSION_NAME=$1

docker-compose build --build-arg VERSION_NAME=${VERSION_NAME} --build-arg VERSION_SHA=${VERSION_SHA}
docker tag thoughts_server:latest throughs_server:${VERSION_NAME}

所有这些脚本的版本都包括使用VERSION_NAME作为标签对镜像进行标记。

我们可以在 Python 代码中在容器内检索包含版本的环境变量,并在端点中返回它们,使版本通过外部 API 轻松访问。

实现版本端点

admin_namespace.py文件中,我们将使用以下代码创建一个新的Version端点:

import os

@admin_namespace.route('/version/')
class Version(Resource):

    @admin_namespace.doc('get_version')
    def get(self):
        '''
        Return the version of the application
        '''
        data = {
            'commit': os.environ['VERSION_SHA'],
            'version': os.environ['VERSION_NAME'],
        }

        return data

现在,这段代码非常简单。它使用os.environ来检索在构建过程中注入的环境变量作为配置参数,并返回一个包含提交 SHA-1 和标签(描述为版本)的字典。

可以使用docker-compose在本地构建和运行服务。要测试对/admin/version端点的访问并进行检查,请按照以下步骤进行:

$ cd Chapter11/microservices/thoughts_backend
$ ./build.sh
...
Successfully tagged thoughts_server:latest
$ docker-compose up -d server
Creating network "thoughts_backend_default" with the default driver
Creating thoughts_backend_db_1 ... done
Creating thoughts_backend_server_1 ... done
$ curl http://localhost:8000/admin/version/
{"commit": "2257f9c5a5a3d877f5f22e5416c27e486f507946", "version": "tag-17-g2257f9c"}

由于版本可用,我们可以更新自动生成的文档以显示正确的值,如app.py中所示:

import os
...
VERSION = os.environ['VERSION_NAME']
...

def create_app(script=False):
    ...
    api = Api(application, version=VERSION, 
              title='Thoughts Backend API',
              description='A Simple CRUD API')

因此,版本将在自动生成的 Swagger 文档中正确显示。一旦微服务的版本通过 API 中的端点可访问,其他外部服务就可以访问它以发现版本并加以利用。

检查版本

通过 API 能够检查版本使我们能够以编程方式轻松访问版本。这可以用于多种目的,比如生成一个仪表板,显示不同环境中部署的不同版本。但我们将探讨引入服务依赖的可能性。

当微服务启动时,可以检查其所依赖的服务,并检查它们是否高于预期版本。如果不是,它将不会启动。这可以避免在依赖服务更新之前部署依赖服务时出现配置问题。这可能发生在部署协调不佳的复杂系统中。

start_server.sh中启动服务器时,要检查版本,我们将首先调用一个检查依赖项的小脚本。如果不可用,它将产生错误并停止。我们将检查前端是否具有 Thought 后端的可用版本,甚至更高版本。

我们将在我们的示例中调用的脚本称为check_dependencies_services.py,并且在前端的start_server.sh中调用它。

check_dependencies_services脚本可以分为三个部分:所需依赖项列表;一个依赖项的检查;以及一个主要部分,其中检查每个依赖项。让我们来看看这三个部分。

所需版本

第一部分描述了每个依赖项和所需的最低版本。在我们的示例中,我们规定thoughts_backend需要是版本v1.6或更高:

import os

VERSIONS = {
    'thoughts_backend': 
        (f'{os.environ["THOUGHTS_BACKEND_URL"]}/admin/version',
         'v1.6'),
}

这里重用环境变量THOUGHTS_BACKEND_URL,并使用特定版本路径完成 URL。

主要部分遍历了所有描述的依赖项以进行检查。

主要函数

主要函数遍历VERSIONS字典,并对每个版本执行以下操作:

  • 调用端点

  • 解析结果并获取版本

  • 调用check_version来查看是否正确

如果失败,它以-1状态结束,因此脚本报告为失败。这些步骤通过以下代码执行:

import requests

def main():
    for service, (url, min_version) in VERSIONS.items():
        print(f'Checking minimum version for {service}')
        resp = requests.get(url)
        if resp.status_code != 200:
            print(f'Error connecting to {url}: {resp}')
            exit(-1)

        result = resp.json()
        version = result['version']
        print(f'Minimum {min_version}, found {version}')
        if not check_version(min_version, version):
            msg = (f'Version {version} is '
                    'incorrect (min {min_version})')
            print(msg)
            exit(-1)

if __name__ == '__main__':
    main()

主要函数还打印一些消息,以帮助理解不同的阶段。为了调用版本端点,它使用requests包,并期望200状态代码和可解析的 JSON 结果。

请注意,此代码会遍历VERSION字典。到目前为止,我们只添加了一个依赖项,但用户后端是另一个依赖项,可以添加。这留作练习。

版本字段将在check_version函数中进行检查,我们将在下一节中看到。

检查版本

check_version函数检查当前返回的版本是否高于或等于最低版本。为了简化,我们将使用natsort包对版本进行排序,然后检查最低版本。

您可以查看natsort的完整文档(github.com/SethMMorton/natsort)。它可以对许多自然字符串进行排序,并且可以在许多情况下使用。

基本上,natsort支持常见的版本排序模式,其中包括我们之前描述的标准版本模式(v1.6高于v1.5)。以下代码使用该库对版本进行排序,并验证最低版本是否为较低版本:

from natsort import natsorted

def check_version(min_version, version):
    versions = natsorted([min_version, version])
    # Return the lower is the minimum version
    return versions[0] == min_version

有了这个脚本,我们现在可以启动服务,并检查 Thoughts 后端是否具有正确的版本。如果您按照技术要求部分中描述的方式启动了服务,您会发现前端无法正确启动,并产生CrashLoopBackOff状态,如下所示:

$ kubectl get pods -n example
NAME READY STATUS RESTARTS AGE
frontend-54fdfd565b-gcgtt 0/1 CrashLoopBackOff 1 12s
frontend-7489cccfcc-v2cz7 0/1 CrashLoopBackOff 3 72s
grafana-546f55d48c-wgwt5 1/1 Running 2 80s
prometheus-6dd4d5c74f-g9d47 1/1 Running 2 81s
syslog-76fcd6bdcc-zrx65 2/2 Running 4 80s
thoughts-backend-6dc47f5cd8-2xxdp 2/2 Running 0 80s
users-backend-7c64564765-dkfww 2/2 Running 0 81s

检查一个前端 pod 的日志,以查看原因,使用kubectl logs命令,如下所示:

$ kubectl logs frontend-54fdfd565b-kzn99 -n example
Checking minimum version for thoughts_backend
Minimum v1.6, found v1.5
Version v1.5 is incorrect (min v1.6)

要解决问题,您需要构建一个具有更高版本的 Thoughts 后端版本,或者减少依赖要求。这将作为本章结束时的评估留下。

总结

在本章中,我们学习了如何处理同时与多个微服务一起工作的元素。

首先,我们讨论了在新功能需要更改多个微服务时要遵循的策略,包括如何以有序的方式部署小的增量,并且能够在出现灾难性问题时回滚。

然后,我们讨论了定义清晰的版本模式,并向 RESTful 接口添加了一个版本端点,以允许微服务自我发现版本。这种自我发现可以用来确保依赖于另一个微服务的微服务在没有依赖项的情况下不会被部署,这有助于协调发布。

本章中的前端 GitHub 代码(github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter11/microservices/frontend)包含对 Thoughts 后端的依赖,这将阻止部署。请注意,原样的代码无法工作。修复留作练习。

我们还学习了如何使用 ConfigMap 来描述在 Kubernetes 集群中共享的配置信息。我们随后描述了如何使用 Kubernetes secrets 来处理敏感且需要额外注意的配置。

在下一章中,我们将看到协调不同团队与不同微服务高效工作的各种技术。

问题

  1. 在微服务架构系统和单体架构中发布更改的区别是什么?

  2. 在微服务架构中,为什么发布的更改应该很小?

  3. 语义版本化是如何工作的?

  4. 微服务架构系统中内部接口的语义版本化存在哪些问题?

  5. 添加版本端点的优势是什么?

  6. 我们如何修复本章代码中的依赖问题?

  7. 我们应该在共享的 ConfigMap 中存储哪些配置变量?

  8. 您能描述将所有配置变量放在单个共享的 ConfigMap 中的优缺点吗?

  9. Kubernetes ConfigMap 和 Kubernetes secret 之间有什么区别?

  10. 我们如何更改 Kubernetes secret?

  11. 假设根据配置,我们决定将public_key.pub文件从秘密更改为 ConfigMap。我们需要实施哪些更改?

进一步阅读

要处理 AWS 上的秘密,您可以与一个名为 CredStash 的工具交互(github.com/fugue/credstash)。您可以在书籍AWS SysOps Cookbook – Second Edition (www.packtpub.com/cloud-networking/aws-administration-cookbook-second-edition)中了解更多信息。

第十二章:跨团队合作和沟通

正如我们之前讨论的,微服务的主要特点是能够并行开发。为了确保最大效率,我们需要成功协调我们的团队,以避免冲突。在本章中,我们将讨论确保不同团队成功合作所需了解的不同元素。

首先,我们将介绍如何在不同的微服务中获得一致的视野,不同的沟通结构如何塑造软件元素中的沟通,以及如何确保我们不会在软件中积累垃圾。然后,我们将讨论如何确保团队在发布和完善其流程和工具方面协调自己,使它们变得越来越可靠。

本章将涵盖以下主题:

  • 保持一致的架构视野

  • 分工和康威定律

  • 平衡新功能和维护

  • 设计更广泛的发布流程

在本章结束时,我们将知道如何构建和协调不同独立工作的团队,以便我们能够充分利用它们。

保持一致的架构视野

在基于微服务的系统中,每个团队能够独立完成大部分任务,独立于其他团队。设计服务,使其尽可能独立并且具有最小的依赖性,对于实现良好的开发速度至关重要。

因此,微服务的分离允许团队独立并行工作,而在单体系统中,大多数人都在关注发生的事情,甚至分散了特定开发人员关注领域之外的工作。他们会知道何时发布新版本,并看到新代码添加到他们正在工作的同一代码库中。然而,在微服务架构中并非如此。在这里,团队专注于他们的服务,不会被其他功能分散注意力。这带来了清晰和高效。

然而,仍然需要一个全局的系统视野。需要对系统的架构如何随时间变化而改变有一个长期的观点,以便能够适应。这种视野(在单片系统中)是隐含的。微服务需要更好地理解这些变化,以便能够有效地工作,因此一个能统一这种全局视野的领先架构师非常重要。

软件行业中的架构师角色并没有一致的定义。

在本书中,我们将其定义为处理 API 和服务整体结构的角色。他们的主要目标是在技术问题上协调团队,而不是直接处理代码。

明确指定一个负责系统全局架构的人有助于我们保持对系统如何发展的长期视野。

在小公司中,首席技术官可能会担任架构师的角色,尽管他们还忙于处理与管理流程和成本相关的元素。

领先架构师的主要责任是确保微服务划分在演变中仍然有意义,并且服务之间通信的 API 是一致的。他们还应该努力促进跨团队的标准生成,并在整个组织中分享知识。

架构师在涉及哪个功能与哪个微服务相关的任何问题以及可能涉及多个团队的其他冲突时,也应该是最终的决策者。这个角色在从单体架构过渡到微服务架构时非常有帮助,但在这个过程完成后,他们也可以确保组织能够适应新的挑战,并控制技术债务。微服务架构系统旨在创建独立的团队,但他们都会从一个外部人员创造的共享全局愿景中受益。

为了更好地协调,团队如何分工是非常重要的。让我们了解一下当我们将系统开发分成不同团队时会出现的一些挑战。

分工和康威定律

微服务架构系统适用于大型软件系统,尽管公司往往从单体应用程序开始。这对于任何有小团队的系统都是有意义的。随着系统的探索和转变,它会随着时间的推移而增长。

但当单体系统增长到一定规模时,它们变得难以处理和开发。由于历史原因,内部变得交织在一起,随着复杂性的增加,系统的可靠性可能会受到影响。在灵活性和冗余之间找到平衡可能很困难。

记住,当开发团队很大时,微服务是有用的。对于小团队来说,单体架构更容易开发和维护。只有当许多开发人员在同一个系统上工作时,分工和接受微服务架构的额外开销才是有意义的。

扩展开发团队可能会变得困难,因为那里会有太多的旧代码,学习如何在其中导航是困难且需要很长时间。开发人员(那些在团队中待了很长时间的人)知道哪些注意事项可以帮助,但他们成为了瓶颈。增加团队的规模并不能帮助,因为任何改变都可能变得复杂。因此,每个新的开发人员在能够成功地进行错误修复和新功能开发之前都需要接受大量的培训。

团队也有一个自然的规模限制。超过这个限制可能意味着必须将其分成更小的团队。

团队的规模是非常灵活的,但通常来说,7±2 个成员被认为是团队中理想人数的经验法则。

更大的团体往往会自行生成更小的团体,但这意味着管理的任务会变得繁重,有些团队可能没有明确的焦点。很难知道其他团队在做什么。

较小的团队在管理和团队间沟通方面往往会产生额外的开销。他们会因为成员更多而开发更快。

在一个大型单体系统中,多个独立团队往往会在没有明确的长期视野的情况下胡乱操作。通过设计一个健壮的内部结构可以缓解这种情况,但这需要巨大的前期规划和严格的监管来确保其得到遵守。

微服务架构是一种解决这些问题的设计,因为它在系统的各个部分之间建立了非常严格的边界。然而,这样做需要开发团队达到一定规模,以便他们可以像几个小团队一样独立工作。这是微服务架构系统的主要特点。构成它的每个微服务都是一个独立的服务,可以独立开发和发布。

这种工作方式允许团队并行工作,没有任何干扰。他们的行动范围是明确的,任何依赖关系都是明确设定的。因此,微服务之间的边界是强大的。

仅仅因为一个微服务可以独立发布并不意味着单个发布就足以发布一个完整的功能。正如我们已经看到的,有时,一个微服务中的一个功能需要在部署之前对另一个微服务进行处理。在这种情况下,需要处理多个微服务。

在规划如何划分团队时,要牢记的最重要的想法是团队结构如何在软件中得到体现。这是由康威定律描述的。

描述康威定律

康威定律是一个软件格言。换句话说,在任何生产软件的组织中,软件将复制组织的通信结构。例如,以一种非常简化的方式,一个组织被分为两个部门:采购和销售。这将产生两个软件模块:一个专注于购买,另一个专注于销售。它们将在需要时进行通信。

在这一部分,我们将讨论软件单元。这是一个通用术语,用来描述任何被视为单一凝聚元素的软件。它可以是一个模块、一个包,或者一个微服务。

在微服务架构中,这些软件单元主要是微服务,但在某些情况下,也可能是其他类型。我们将在将软件划分为不同类型的软件单元部分看到这方面的例子。

这可能并不奇怪。不同团队之间的沟通水平以及同一个团队内部的沟通水平是不同的,这是很自然的。然而,团队合作的影响是巨大的,其中一些如下:

  • 团队间的 API 比团队内的 API 更昂贵,无论是在操作上还是在开发上,因为它们的通信更加复杂。将它们设计成通用和灵活的是有意义的,这样它们就可以被重复使用。

  • 如果通信结构复制了人类组织,那么明确是有意义的。团队间的 API 应该比团队内的 API 更加可见、公开和有文档记录。

  • 在设计系统时,将它们划分到分层团队结构的界线上是最不费力的路径。以其他方式对其进行工程设计将需要组织变革。

  • 另一方面,改变组织结构是一个困难和痛苦的过程。任何经历过组织重组的人都知道这一点。这种变化将反映在软件中,因此要做好计划。

  • 让两个团队共同处理同一个软件单元会产生问题,因为每个团队都会试图将其引向自己的目标。

软件单元的所有者应该是一个团队。这向每个人展示了谁对任何变化负责并有最终决定权,并帮助我们专注于我们的愿景并减少技术债务。

  • 不同的物理位置会施加通信限制,比如时差,这将在我们跨地域开发软件时产生障碍。通常会根据地理位置划分团队,这就需要构建这些团队之间的通信(因此 API)结构。

请注意,DevOps 运动与康威定律有关。传统的工作分工方式是将正在开发的软件与其运行方式分开。这在康威定律所描述的两个团队之间产生了鸿沟,从而产生了与两个团队之间缺乏理解相关的问题。

对这个问题的反应是创建能够开发和运行自己软件的团队,并部署它。这就是所谓的 DevOps。它将运营问题转移到开发团队,旨在创建一个反馈循环来激励、理解和解决问题。

康威定律并不是一件需要克服的坏事。它反映了任何组织结构对软件结构的影响。

记住这一点可能有助于我们设计系统,使得组织和现有软件的通信流程合理。

DevOps 运动的一个关键组成部分是推进构建系统的技术,以简化生产环境的操作,使部署过程更简单。这使我们能够以新的方式组织团队,从而使多个团队能够控制发布。

现在,让我们谈谈软件如何被结构化为不同的部门。

将软件划分为不同类型的软件单元

虽然本书的主要目标是讨论微服务中软件的划分,但这并不是唯一可能的划分。其他划分可以包括微服务内的模块或共享包。

微服务的主要特点是在开发和部署方面是独立的,因此可以实现完全的并行化。其他划分可能会减少这一点并引入依赖关系。

确保你能够证明这些改变。

在我们在本书中介绍的示例系统中,我们引入了一个模块,用于验证请求是否由用户签名。用户后端生成一个签名头,思想后端和前端通过token_validation.py模块独立验证它。

这个模块应该由拥有用户后端的同一个团队拥有,因为它是它的自然延伸。我们需要验证它是否生成与用户后端生成的相同的令牌。

避免重复并始终保持同步的最佳方法是生成一个 Python 软件包,可以安装在依赖的微服务上。然后,这些软件包可以像requirements.txt文件中的任何其他外部依赖一样对待。

要在 Python 中打包一个库,我们可以使用几种工具,包括官方的Python Packaging User Guidepackaging.python.org/)中的工具,以及较新的工具,如 Poetry(poetry.eustace.io),这些工具对于新项目来说更容易使用。

如果我们希望将软件包公开,可以将其上传到 PyPI。或者,如果需要,我们可以将其上传到私有存储库,使用诸如 Gemfury 之类的工具,或者自己托管存储库。这将明确划分软件包及其维护者,以及使用它作为依赖项的团队之间的关系。

将软件单元划分成团队划分方面有着重要的影响。现在,让我们来看看如何组织团队。

设计工作结构

考虑到康威定律,划分软件应该反映组织的结构。当我们从单体架构迁移到微服务架构时,这一点非常重要。

记住,从单体架构迁移到微服务在我们操作方式上是一个重大的改变。这既是组织上的改变,也是技术上的改变。主要的风险在于人的因素,包括培训人员使用新技术以及让开发人员满意他们将要工作的新领域。

对组织结构进行根本性的改变可能非常困难,但将需要进行一些小的调整。当从单体架构迁移到微服务时,团队将需要重新调整。

请记住,大规模的重组可能会激怒人们并引发政治问题。人类不喜欢改变,任何决定都需要有意义。需要准备好解释和澄清这一举措。明确新结构的目标将有助于赋予其目的。

让我们来看一些团队划分的例子,以及它们的优缺点。

围绕技术构建团队

在某些情况下,与技术相关的不同技能可能是相关的。系统的某些部分可能涉及与其他任何东西完全不同的技术。

一个很好的例子是移动应用程序,因为它们在使用的语言方面是受限制的(Android 使用 Java,iOS 使用 Objective-C 或 Swift)。一个具有网站和移动应用程序的应用可能需要一个专门的团队来处理移动应用程序的代码。

一个更传统的例子是围绕数据库管理员DBA)构建的数据库团队。他们将控制对数据库的访问并操作它们以保持良好状态。然而,这种结构正在消失,因为数据库操作现在更容易,通常由大多数开发人员处理,并且近年来数据库的基础设施管理已大大简化。

这可能使我们能够为特定领域创建特定团队。技术的障碍确保系统之间的通信是有结构的。

下图是我们将遇到的团队类型的示例。它们按技术和沟通方式分组。数据库团队将与创建 Web 服务后端的团队进行通信,他们将与 Web 和移动团队进行通信:

这种模式的主要缺点是新功能可能需要多个团队共同努力。对客户端代码的任何更改,以便我们可以在数据库中存储新值,都需要每个团队的工作投入。这些功能需要额外的协调,这可能会限制开发速度。

围绕领域构建团队

另一种结构是围绕不同的知识领域,通常与公司的业务领域相关。每个知识领域都有自己独立的系统,但它们彼此通信。一些部分可能具有外部可访问的接口,而其他部分可能没有。

这种结构通常在已经成功运作多年的成熟组织中找到。

例如,一个在线零售商可能分为三个领域:

  • 销售:负责外部网站和营销。

  • 库存:购买商品以便销售,并处理库存。

  • 运输:将产品送到客户手中。跟踪信息显示在网站上。

在这种情况下,每个领域都有自己的数据库,以便存储相关数据和服务。它们通过定义的 API 相互通信,最频繁的变化发生在特定领域内。这允许在领域内快速发布和开发。

跨领域拥有新功能也是可能的。例如,对运输跟踪信息的更改可能需要我们匹配销售产生的更改。然而,这些变化应该发生得更少。

在这个例子中,每个团队将像下图所示相互通信:

这种结构的主要不便之处在于可能会创建孤立的团队和独立思维。每个系统都有自己的做事方式,因此它们可能会分歧到不共享相同的基本语言的程度。当需要跨领域功能时,可能会导致讨论和摩擦。

围绕客户构建团队

在一些组织中,主要目标是为客户创建定制工作。也许客户需要以定制的 B2B 方式与产品集成。在这种情况下,能够开发和运行定制代码至关重要。

结构侧重于客户。三个团队(称为红色、金色和蓝色)分配给客户,并为每个客户维护特殊服务,包括他们的自定义代码。每个客户团队处理几个客户。另一个团队处理产品的后端,其中包含系统的通用代码和基础设施。这个团队与客户分开工作,但在共享时从客户团队添加功能,以便将其包含在产品中。一般的改进也是共享的。

这在组织中形成了两种速度。客户团队专注于客户的短期需求,而产品团队专注于客户的长期需求。

在这里,产品团队将与客户团队交谈,但客户团队之间的交流不会太多。这在下图中有所体现:

这种结构对于高度定制的服务非常有效,以便它们可以包含为单个客户生成的代码,这可能会使它们失去对一般产品的关注。这里的主要问题是客户团队可能面临的高压力,因为他们面对苛刻的客户,这可能对开发人员造成负担。产品团队需要确保他们为产品做出有用的添加,并尽量减少他们的长期问题。

围绕混合结构团队

前面的三个例子是合成用例。现实生活更加复杂,可能需要混合使用所有这些例子,或者完全新的结构。

如果组织足够大,可能会有数十个不同的团队和软件单元。请记住,如果团队足够大,一个团队可以处理多个软件单元。但是,为了避免所有权和缺乏重点,两个团队不应拥有相同的软件单元。

分析组织中的沟通流程,以便了解在转向微服务时需要解决的痛点,并确保人员结构考虑了微服务和软件单元的设计。

团队的另一个重要元素是找到在添加新功能和维护现有代码之间花费的时间之间的适当平衡。

平衡新功能和维护

每个软件服务都需要维护,以保持良好状态,但不会增加明显的外部价值。维护任务对于良好的运行至关重要,可以分为两类:定期维护和管理技术债务。

技术债务是将使用大部分时间并需要进一步讨论的部分,但在此之前,让我们先看看定期维护。

定期维护

这种维护以与软件服务的性质固有的任务形式出现。通过运行依赖于其他组件的服务,例如底层操作系统或 Python 解释器,我们需要使它们保持最新并升级到新版本。

在使用容器和 Kubernetes 的情况下,有两个充当操作系统的系统需要考虑。一个是容器中的操作系统;在这里,我们使用了 Alpine。另一个是处理 Kubernetes 节点的操作系统,在这里 AWS EKS 会自动处理,但需要升级到 Kubernetes 版本。

保持依赖项最新的主要原因如下,按重要性排序:

  • 新版本修复安全问题。

  • 一般性能改进。

  • 可以添加新功能以实现新功能。

如果我们计划进行这些任务,这些任务可以得到缓解。例如,使用标记为长期支持LTS)的操作系统版本可以减少在更新系统时出现的问题。

操作系统的 LTS 版本是在长周期内接收支持和关键更新的版本。例如,常规的 Ubuntu 版本每 6 个月发布一次,并在 9 个月内接收更新(包括关键安全更新)。LTS 版本每 2 年发布一次,并获得 5 年的支持。

运行服务时,建议使用 LTS 版本,以最小化所需的维护工作。

所有这些软件包和依赖关系都需要更新,以确保操作系统正常工作。另一种选择是存在安全漏洞或者使用过时的系统。

更新依赖关系可能需要我们调整代码,这取决于它的部分是否已被弃用或移除。在某些情况下,这可能是昂贵的。在撰写本文时,Python 社区最著名的迁移是从 Python 2 升级到 Python 3,这是一个需要多年时间的任务。

大多数升级通常相当常规,需要很少的工作。尝试生成一个跟得上的升级计划,并制定明确的指导方针;例如,规则如当新的操作系统 LTS 版本发布时所有系统应在接下来的 3 个月内迁移。这样可以产生可预测性,并给每个人一个明确的目标,可以跟进和执行。

持续集成工具可以帮助这个过程。例如,GitHub 自动检测文件中的依赖关系,如requirements.txt,并在检测到漏洞时通知我们。甚至可以在更新模块时自动生成拉取请求。查看更多信息的文档:help.github.com/en/github/managing-security-vulnerabilities/configuring-automated-security-fixes

升级依赖关系可能是最常见的定期维护任务,但还有其他可能性:

  • 清理或归档旧数据。这些操作通常可以自动化,节省大量时间并减少问题。

  • 修复依赖于业务流程的操作,比如生成月度报告等等。这些应该在可能的情况下自动化,或者设计工具,使用户可以自动产生它们,而不是依赖技术人员进行定制操作。

  • 修复由错误或其他错误产生的永久问题。错误有时会使系统处于糟糕的状态;例如,数据库中可能有损坏的条目。在修复错误时,我们可能需要通过解除进程或用户来解决这种情况。

这些过程可能很烦人,特别是如果它们是重复的,但通常是被理解的。

另一种维护形式,涉及技术债务,更加复杂,因为它更逐渐地引入,并且更难以清晰地检测到。妥善解决技术债务是最具挑战性的维护任务,但在我们做任何事情之前,我们需要理解它。

理解技术债务

技术债务是软件开发中使用的一个概念,用来描述当实施非最佳解决方案时将在未来增加的额外成本。换句话说,选择快速或简单的选择意味着以后的功能需要更长时间和更难开发。

作为一个隐喻,技术债务自上世纪 90 年代初就存在,但在此之前就已经描述了这个概念。

像任何隐喻一样,它是有用的,但也有局限性。特别是,非技术人员倾向于将其与财务债务联系起来,尽管它们有不同的含义。例如,大多数技术债务是在我们甚至没有注意到的情况下创建的。确保不要过分使用这个隐喻。

技术债务在一定程度上是不可避免的。在实施功能之前,没有无限的时间来研究所有可能性,也没有完美的信息。这也是任何复杂系统中熵增长的结果。

除了是不可避免的,技术债务也可以是一个有意识的选择。由于时间的限制,开发受到限制,因此对市场的不完美快速解决方案可能比错过截止日期更可取。

技术债务的另一个迹象是专注于某些知识。无论如何,技术债务会随着时间的推移不断积累,并给新功能带来摩擦。复杂性的增加也可能会导致可靠性问题,因为错误会变得越来越难以理解和修复。

简单是可靠系统的最好朋友。简单的代码易于理解和纠正,使错误要么显而易见,要么很容易检测到。微服务架构旨在通过创建独立的、更小的服务,并为它们分配明确的责任,并在它们之间创建明确的接口,来减少单体架构的固有复杂性。

技术债务可能会增长到需要一个大型架构的程度。我们已经看到,从单体架构转移到微服务架构可能是其中一个时刻。

这样的架构迁移是一项艰巨的工作,需要时间来完成。在单体架构中已经存在的功能,新微服务可能会与引入的新功能发生冲突。

这会产生一个移动目标效应,可能会带来很大的破坏性。确保你识别出这些冲突点,并尽量在迁移计划中将其最小化。例如,一些新功能可能可以推迟到新的微服务准备就绪的时候。

然而,我们不应该等到技术债务变得如此庞大,以至于只有激进的改变才足以解决它,我们需要能够更早地解决技术债务。

持续解决技术债务

减少技术债务需要是一个持续的过程,并且需要引入到日常运营中。专注于持续改进的敏捷技术试图引入这种思维方式。

检测技术债务通常来自开发团队内部,因为他们更接近代码。团队应该考虑哪些地方的操作可以更顺畅,并留出时间进行改进。

允许我们检测技术债务的一个很好的信息来源是指标,比如我们在第十章中设置的监控日志和指标。

忽视解决这些问题的风险是陷入软件腐烂,已经存在的功能变得越来越慢和不太可靠。随着时间的推移,它们将对客户和外部合作伙伴变得越来越明显。在那之前,工作在这种环境中将使开发人员的生活变得困难,并存在燃尽的风险。新开发的延迟也会很常见,因为代码本身就很难处理。

为了避免陷入这种情况,需要不断地分配时间来持续减少技术债务,交替进行新功能和其他工作。应该在维护和技术债务减少以及新功能之间找到平衡。

我们在本书中讨论的许多技术手段都有助于我们以持续的方式改进系统,从我们在第四章中描述的持续集成技术,到我们在第八章中描述的代码审查和批准,再到我们在第十章中设置的监控日志和指标。

分布可能高度依赖于系统的当前形状,但明确并强制执行这一点确实有帮助。例如,花费在技术债务减少上的特定时间百分比可能会有所帮助。

减少技术债务是昂贵且困难的,因此尽量减少引入技术债务是有意义的。

避免技术债务

处理技术债务的最佳方法是首先不引入技术债务。然而,这说起来容易做起来难。有多个因素可能影响导致技术债务的决策的质量。

最常见的原因如下:

  • 缺乏战略性的高层计划来指导:这会产生不一致的结果,因为每次发现相同的问题时,都会以不同的方式解决。我们谈到了跨团队协调需要解决组织内的标准,并确保它们得到遵守。有人担任软件架构师,寻求在整个组织中创建一致的指导方针,应该会极大地改善这种情况。

  • 没有足够的知识来选择正确的选项:这是相当常见的。有时,需要做决定的人由于沟通不畅或简单缺乏经验而没有所有相关信息。这个问题是缺乏处理当前问题经验的结构的典型问题。确保团队经过高度培训,并且正在创造一个更有经验的成员帮助和指导初级成员的文化,将减少这些情况。跟踪以前的决定并简化如何使用其他微服务的文档将帮助我们协调团队,使他们拥有所有相关的拼图部分。这有助于他们避免由于错误的假设而犯错。另一个重要因素是确保团队对他们使用的工具进行适当的培训,以便他们充分了解自己的能力。这对于外部工具来说应该是这样,比如熟练掌握 Python 或 SQL,以及任何需要培训材料、文档和指定联系点的内部工具。

  • 没有花足够的时间调查不同的选项或进行规划:这个问题是由压力和迫切需要取得快速进展所造成的。这可能已经根深蒂固在组织文化中,当组织增长时,减缓决策过程可能是一项具有挑战性的任务,因为较小的组织往往需要更快的流程。记录决策过程并要求同行审查或批准可以帮助减缓这一过程,并确保工作是彻底的。在决定哪些决策需要更多审查和哪些不需要方面找到平衡是很重要的。例如,所有适合在一个微服务内的东西可以在团队内部进行审查,但需要多个微服务和团队的功能应该在外部进行审查和批准。在这种情况下,找到收集信息和做决定之间的适当平衡是重要的。记录决策和输入,以便了解得出这些决策的过程并完善你的流程。

避免这些问题的最佳方法是反思以前的错误并从中吸取教训。

设计更广泛的发布流程

虽然能够独立部署每个微服务确实是系统的关键要素,但这并不意味着不需要协调。

首先,仍然有一些功能需要在多个微服务中部署。我们已经看过如何可以在开发过程中处理这些细节,包括处理版本和明确检查依赖关系。那么现在呢?

在这些情况下,需要团队之间的协调,以确保依赖关系得到实施,并且不同的部署按适当的顺序执行。

虽然一些协调可以由主要架构师来协助,但架构角色应该专注于长期目标,而不是短期发布。允许团队自行协调的好工具是在会议上通知其他团队有关发布的情况。

在每周发布会议中的规划

当发布流程是新的,并且从单体系统迁移仍在进行时,向每个团队提供他们正在做什么的见解是一个好主意。每周的发布会应该由每个团队的代表参加,这样可以很好地传播关于其他团队正在进行的工作的知识。

发布会的目标应该是:

  • 下一个 7 天的计划发布和大致时间;例如,我们计划在周三发布新版本的用户后端。

  • 对于任何重要的新功能,尤其是其他团队可以使用的功能,你应该提前通知。例如,如果新版本改进了身份验证,请确保将你的团队重定向到新的 API,以便他们也可以获得这些改进。

  • 说明任何阻碍因素。例如,我们无法发布这个版本,直到 Thoughts 后端发布带有功能 A 的版本。

  • 如果有关键维护或可能影响发布的任何更改,请提出警告。例如,周四早上,我们需要进行数据库维护,所以请不要在 12 点之前发布任何东西。工作完成后,我们会发送电子邮件通知。

  • 回顾上周发生的发布问题。我们稍后会更详细地讨论这个问题。

这类似于许多敏捷实践中常见的站立会议,比如 SCRUM,但专注于发布。为了能够做到这一点,我们需要提前指定发布时间。

考虑到微服务发布的异步性质,以及持续集成实践的实施和加速这一过程,将会有很多例行发布不会提前计划那么长时间。这是可以接受的,也意味着发布流程正在得到完善。

在涉及风险较高的发布时,尽量提前计划,并利用发布会有效地与其他团队沟通。这个会议是保持对话开放的工具。

随着持续集成实践的不断确立和发布速度的不断加快,每周的发布会将逐渐变得越来越不重要,甚至可能不再需要定期举行。这是对持续改进实践的反思的一部分,也是通过识别发布问题来实现的。

反思发布问题

并不是每次发布都会顺利进行。有些可能会因为工具或基础设施的问题而失败,或者可能是因为流程中存在易犯的错误。事实上,有些发布会出现问题。不幸的是,无法避免这些情况。

随着时间的推移,减少和最小化发布问题,每次发现问题时,都需要将其记录并在每周的发布会或等价的论坛上提出。

一些问题可能很小,只需要额外的一点工作,就可以成功发布;例如,一个错误的配置会导致新版本无法启动,直到修复,或者一个协调问题,导致一个服务在其依赖之前部署。

其他问题可能更大,甚至可能导致故障。在这种情况下,回滚将非常有用,这样我们就可以快速返回到已知状态并重新规划。

无论如何,它们都应该被适当地记录,即使只是简要地记录,然后共享,以便流程得以完善。分析出了什么问题是关键,以便不断改进发布,使其更快速、更简单。

对这些问题要坦诚。如果希望检测到每一个问题并快速评估解决方案,那么创建一个公开讨论和承认问题的文化是很重要的。

捕捉问题并不是,也永远不应该是,归咎于谁的责任。检测和纠正问题是组织的责任。

如果发生这种情况,环境不仅会变得不那么吸引人,而且团队会隐藏问题,以免受到指责。

未解决的问题往往会成倍增加,因此可靠性将大大降低。

能够顺利发布对于快速部署和提高速度至关重要。当处理这类问题时,通常只需要轻量级文档,因为它们通常是轻微的,最坏的情况下可能会延迟一两天的发布。

对于更大的问题,当外部服务中断时,最好有一个更正式的流程来确保问题得到适当解决。

我们可以改进的另一种方式是正确理解中断现场系统服务的问题的原因。这方面最有效的工具是事后总结会议。

进行事后总结会议

不仅限于发布,有时会发生中断服务并需要大量工作才能修复的重大事件。在这些紧急情况下,第一个目标是尽快恢复服务。

在服务恢复稳定后,为了从这次经历中吸取教训并避免再次发生,应该由参与事件的所有人参加事后总结会议。事后总结会议的目标是从紧急情况中学到的教训中创建一系列后续任务。

为了记录这一点,您需要创建一个模板。这将在事后总结会议期间填写。模板应该包括以下信息:

  • 检测到了什么问题? 如果这不明显,包括如何检测到的;例如,网站宕机并返回 500 错误。这表明错误增加了。

  • 它是什么时候开始和结束的? 事件的时间轴;例如,周四下午 3 点到 5 点。

  • 谁参与了解决这次事件? 无论是检测问题还是解决问题。这有助于我们收集关于发生了什么的信息。

  • 为什么会失败? 找到根本原因和导致这一结果的一系列事件;例如,网站宕机是因为应用程序无法连接到数据库。数据库无响应是因为硬盘已满。硬盘已满是因为本地日志填满了磁盘。

  • 它是如何修复的? 采取了解决事件的步骤;例如,删除了一周前的日志。

  • 从这次事件中应该采取哪些行动? 应该采取纠正或修复不同问题的行动。理想情况下,它们应该包括谁将执行这些行动;例如,不应该存储本地日志,而应该将其发送到集中日志。应该监视硬盘空间的使用情况,并在空间少于 80%时发出警报。

其中一些元素可以在紧急情况后立即填写,例如谁参与了。然而,最好是在事件发生后一到三天安排事后总结会议,以便每个人都有时间消化和处理这些数据。根本原因可能与我们最初的想法不同,花一些时间思考发生了什么有助于我们提出更好的解决方案。

正如我们在反思发布问题部分讨论的那样,在处理服务中断事件时,一定要鼓励开放和坦率的讨论。

事后总结会议并不是为了责怪任何人,而是为了改进服务并在团队合作时减少风险。

应该在会议中决定后续行动,并相应地进行优先排序。

尽管检测根本原因非常重要,但请注意应该采取针对其他原因的行动。即使根本原因只有一个,也有其他预防性行动可以最小化其再次发生时的影响。

事后总结会议产生的行动通常具有很高的优先级,并应尽快完成。

总结

在本章中,我们看了团队之间协调的不同方面,以便成功管理运行微服务架构的组织。

我们首先讨论了保持全局视野和各部分之间协调的好处。我们谈到了明确指定的领先架构师监督系统,并具有高层视图,使他们能够确保团队之间不会发生冲突。

我们描述了康威定律以及沟通结构最终塑造了软件结构,因此对软件所做的任何更改都应在组织中得到反映,反之亦然。然后,我们学习了如何划分责任领域,并提供了一些可能的划分示例,基于不同的组织。

接下来,我们介绍了技术债务如何减缓持续开发过程,以及引入持续解决技术债务的思维方式对于避免降低内部团队和客户体验的重要性。

最后,我们解决了发布可能引起的一些问题,无论是在团队之间进行充分协调方面,特别是在使用 GitOps 的早期阶段,还是在发布失败或服务中断时进行回顾分析。

问题

  1. 为什么领先的架构师对微服务架构系统很方便?

  2. 康威定律是什么?

  3. 为什么会引入技术债务?

  4. 为什么重要创造一种文化,可以持续努力减少技术债务?

  5. 为什么重要记录发布中的问题并与每个团队分享?

  6. 事后总结会议的主要目标是什么?

进一步阅读

要了解更多有关架构师角色的信息,请阅读《软件架构师手册》(www.packtpub.com/application-development/software-architects-handbook),其中包括专门讨论软技能和架构演变的章节。您可以在《新工程游戏》(www.packtpub.com/data/the-new-engineering-game)中了解更多有关康威定律和构建数字化业务的信息。

第十三章:评估

第一章

  1. 什么是单体?

单体应用是指以单个块创建的软件应用程序。该应用程序作为单个进程运行。它只能一起部署,尽管可以创建多个相同的副本。

  1. 单体可能会遇到什么问题?

随着发展,单体可能会遇到以下问题:

  • 代码变得太大且难以阅读。

  • 可扩展性问题。

  • 需要协调部署。

  • 资源的不良使用。

  • 不可能在不同情况下使用冲突的技术(例如,相同库的不同版本,或两种编程语言)。

  • 一个错误和部署可能会影响整个系统。

  1. 你能描述微服务架构吗?

微服务架构是一组松散耦合的专业化服务的集合,它们协同工作以提供全面的服务。

  1. 微服务最重要的特性是什么?

微服务最重要的特性是它们可以独立部署,以便可以独立开发。

  1. 从单体架构迁移到微服务时,我们需要克服的主要挑战是什么?

可能的挑战包括以下内容:

    • 需要进行大的变更,需要我们改变服务的运行方式,包括团队的文化。这可能导致成本高昂的培训。
  • 调试分布式系统更加复杂。

  • 我们需要计划变更,以便不中断服务。

  • 每个开发的微服务都会产生大量开销。

  • 我们需要在允许每个团队决定如何工作和标准化以避免重复造轮之间找到平衡。

  • 我们需要记录服务,以便与另一个团队进行交互。

  1. 我们如何进行这样的迁移?

我们需要分析系统,测量,相应地计划并执行计划。

  1. 描述我们如何使用负载均衡器从旧服务器迁移到新服务器,而不中断系统。

首先,我们必须配置负载均衡器,使其指向旧的 Web 服务器,这将使流量通过 Web 服务器。然后,我们必须更改 DNS 记录,使其指向负载均衡器。流量经过负载均衡器后,我们需要为新服务创建一个新条目,以便负载均衡器在两者之间分配流量。确认一切按预期工作后,我们需要从旧服务中删除条目。现在,所有流量将路由到新服务。

第二章

  1. RESTful 应用程序的特征是什么?

虽然 RESTful 应用程序被理解为将 URI 转换为对象表示并通过 HTTP 方法操纵它们的 Web 界面(通常使用 JSON 格式化请求),但 REST 架构的典型特征如下:

  • 统一接口

  • 客户端-服务器

  • 无状态

  • 可缓存

  • 分层系统

  • 按需代码(可选)

您可以在restfulapi.net/了解有关 REST 架构的更多信息。

  1. 使用 Flask-RESTPlus 的优势是什么?

使用 Flask-RESTPlus 的一些优势包括:

  • 自动生成 Swagger。

  • 可以定义和解析输入并整理输出的框架。

  • 它允许我们在命名空间中组织代码。

  1. Flask-RESTPlus 的一些替代方案是什么?

其他选择包括 Flask-RESTful(这类似于 Flask-RESTPlus,但它不支持 Swagger)和 Django REST 框架,它拥有丰富的生态系统,充满了第三方扩展。

  1. 命名用于测试以修复时间的 Python 软件包。

freezegun

  1. 描述认证流程。

认证系统(用户后端)生成编码的令牌。此令牌使用只有用户后端拥有的私钥进行编码。此令牌以 JWT 编码,并包含用户 ID 以及其他参数,例如告诉我们令牌有效的时间。此令牌包含在Authentication标头中。

令牌从标头中获取,并使用相应的公钥进行解码,该公钥存储在 Thoughts 后端中。这使我们能够独立获取用户 ID,并确信它已被用户后端验证。

  1. 为什么我们选择 SQLAlchemy 作为数据库接口?

SQLAlchemy 在 Flask 中得到很好的支持,并允许我们定义已经存在的数据库。它高度可配置,并允许我们在低级别(即接近底层 SQL)和高级别上工作,从而消除了任何样板代码的需求。在我们的用例中,我们从遗留系统继承了一个数据库,因此需要与现有模式无缝工作。

第三章

  1. Dockerfile 中的 FROM 关键字是做什么的?

它从现有的镜像开始,向其添加更多的层。

  1. 如何使用预定义命令启动容器?

您将运行以下命令:

docker run image
  1. 为什么在 Dockerfile 中创建一个删除文件的步骤不会创建一个更小的镜像?

由于 Docker 使用的文件系统具有分层结构,Docker 文件中的每个步骤都会创建一个新的层。文件系统是所有操作协同工作的结果。最终镜像包括所有现有的层;添加一个层永远不会减小镜像的大小。删除的新步骤将不会出现在最终镜像中,但它将始终作为前一个层的一部分可用。

  1. 多阶段 Dockerfile 是如何工作的?

多阶段 Dockerfile 包含多个阶段,每个阶段都将以FROM命令开始,该命令指定作为起点的镜像。数据可以在一个阶段生成,然后复制到另一个阶段。

多阶段构建在我们希望减小最终镜像大小时非常有用;只有生成的数据将被复制到最终阶段。

  1. 运行和执行命令之间有什么区别?

run命令从镜像启动一个新的容器,而exec命令连接到已经存在的运行中的容器。请注意,如果在执行时容器停止,会话将被关闭。

exec会话中停止容器可能会发生。保持容器运行的主要进程是run命令。如果您终止命令或以其他方式停止它,容器将停止,会话将被关闭。

  1. 何时应该使用-it 标志?

当您需要保持终端打开时,例如交互式运行bash命令。请记住这个助记符交互式终端

  1. 除了使用 uWSGI 来提供 Web Python 应用程序之外,还有哪些替代方案?

任何支持 WSGI 网络协议的 Web 服务器都可以作为替代方案。最受欢迎的替代方案是 Gunicorn,旨在易于使用和配置,mod_wsgi是流行的 Apache Web 服务器的扩展,支持 WSGI Python 模块,以及 CherryPy,它包括自己的 Web 框架。

  1. docker-compose 用于什么?

docker-compose允许轻松编排,也就是说,我们可以协调多个相互连接的 Docker 容器,使它们协同工作。它还帮助我们配置 Docker 命令,因为我们可以使用docker-compose.yaml文件来存储所有受影响容器的配置参数。

  1. 你能描述一下 Docker 标签是什么吗?

Docker 标签是一种在保持其根名称的同时标记图像的方法。它通常标记相同应用程序或软件的不同版本。默认情况下,latest标签将应用于图像构建。

  1. 为什么我们需要将镜像推送到远程注册表?

我们将镜像推送到远程注册表,以便与其他系统和开发人员共享镜像。除非需要将镜像推送到另一个存储库,否则 Docker 会在本地构建镜像,以便其他 Docker 服务可以使用它们。

第四章

  1. 增加部署数量是否会降低部署的质量?

不会;已经证明增加部署数量与其质量增加有很强的相关性。一个能够快速部署的系统必须依赖于强大的自动化测试,这会增加系统的稳定性和整体质量。

  1. 什么是管道?

管道是用于执行构建的有序步骤或阶段的连续顺序。如果其中一个步骤失败,构建将停止。步骤的顺序应该旨在最大程度地早期检测问题。

  1. 我们如何知道我们的主分支是否可以部署?

如果我们自动运行我们的管道以在每次提交时生成构建,我们应该在提交时尽快检测主分支上的问题。构建应该让我们确信主分支的顶部提交可以部署。主分支的中断应该尽快修复。

  1. Travis CI 的主要配置来源是什么?

.travis.yml文件,可以在存储库的根目录中找到。

  1. Travis CI 默认在何时发送通知邮件?

Travis CI 在构建中断时发送通知邮件,以及先前中断的分支成功通过时发送通知邮件。成功的构建发生在先前的提交成功但未报告的情况下。

  1. 我们如何避免将中断的分支合并到主分支?

我们可以通过在 GitHub 中进行配置来避免这种情况,这可以确保分支在合并到受保护的分支之前通过构建。为了确保功能分支没有偏离主分支,我们需要强制它与构建合并。为了实现这一点,它需要与主分支保持最新。

  1. 为什么我们应该避免将机密存储在 Git 存储库中?

由于 Git 的工作方式,任何引入的机密都可以通过查看提交历史来检索,即使它已被删除。由于提交历史可以在任何克隆的存储库中复制,这使得我们无法验证它是否正确 - 我们无法将提交历史重写到克隆的存储库中。除非正确加密,机密不应存储在 Git 存储库中。任何错误存储的机密都应该被删除。

第五章

  1. 什么是容器编排器?

容器编排器是一个系统,我们可以在其中部署多个容器,这些容器可以协同工作,并以有序的方式管理供应和部署。

  1. 在 Kubernetes 中,什么是节点?

节点是集群中的物理服务器或虚拟机。节点可以被添加或从集群中移除,Kubernetes 会相应地迁移或重新启动正在运行的容器。

  1. Pod 和容器之间有什么区别?

一个 Pod 可以包含多个共享相同 IP 的容器。要在 Kubernetes 中部署容器,我们需要将其与一个 Pod 关联起来。

  1. 作业和 Pod 之间有什么区别?

一个 Pod 预期会持续运行。一个作业或定时作业执行单个操作,然后所有 Pod 容器完成它们的执行。

  1. 我们何时应该添加 Ingress?

当我们需要能够从集群外部访问服务时,我们应该添加 Ingress。

  1. 命名空间是什么?

命名空间是一个虚拟集群。集群中的所有定义都需要具有唯一的名称。

  1. 我们如何在文件中定义 Kubernetes 元素?

我们需要以 YAML 格式指定它,并提供关于其 API 版本、元素类型、具有名称和命名空间的元数据部分,以及spec部分中的元素定义。

  1. kubectl get 和 describe 命令有什么区别?

kubectl get获取多个元素,如服务或 pod,并显示它们的基本信息。另一方面,describe访问单个元素并呈现更多关于它的信息。

  1. CrashLoopBackOff 错误表示什么?

这个错误表明一个容器已经执行了定义的启动命令。这个错误只与 pod 有关,因为它们永远不应该停止执行。

第六章

  1. 我们正在部署的三个微服务是什么?

以下是我们正在部署的三个微服务:

  • 用户后端用于控制身份验证和用户处理方式。

  • Thoughts Backend 用于存储思想并允许我们创建和搜索它们。

  • 前端为我们提供了一个用户界面,以便我们可以与系统进行交互。它通过 RESTful 调用调用其他两个微服务。

  1. 这三个微服务中哪一个需要其他两个微服务可用?

前端调用其他两个微服务,因此它们需要对前端可用。

  1. 为什么我们需要在运行 docker-compose 时使用外部 IP 连接到微服务?

docker-compose为每个微服务创建一个内部网络,因此它们需要使用外部 IP 进行通信,以便正确路由。虽然我们在主机计算机上暴露端口,但可以使用外部主机 IP。

  1. 每个应用程序所需的主要 Kubernetes 对象是什么?

对于每个微服务,我们提供一个部署(自动生成一个 pod)、一个服务和一个 Ingress。

  1. 有没有任何不需要的对象?

用户后端和 Thoughts 后端的 Ingress 并不是绝对必需的,因为它们可以通过节点端口访问,但这样做可以更轻松地访问它们。

  1. 如果我们扩展到多个 pod 或任何其他微服务,我们能检测到问题吗?

用户后端和 Thoughts 后端创建了一个包含两个容器的 pod,其中包括数据库。如果我们创建多个 pod,将创建多个数据库,并在它们之间交替可能会导致问题。

例如,如果我们在一个 pod 中创建一个新的想法,如果请求是在另一个 pod 中进行的,我们将无法搜索到它。

另一方面,前端可以轻松扩展。

  1. 我们为什么要使用/etc/hosts文件?

我们正在使用这个文件来定义一个host,将其路由到我们的本地 Kubernetes 集群。这样我们就不必定义 FQDN 并配置 DNS 服务器。

第七章

  1. 为什么我们不应该管理自己的 Kubernetes 集群?

由于 Kubernetes 是一个抽象层,让云提供商负责维护、管理和安全最佳实践更加方便。将集群委托给现有的商业云提供商也非常便宜。

  1. 你能说出一些具有托管 Kubernetes 解决方案的商业云提供商吗?

亚马逊网络服务、谷歌云服务、微软 Azure、Digital Ocean 和 IBM Cloud 都是商业云提供商,提供了托管的 Kubernetes 解决方案。

  1. 你需要执行什么操作才能推送到 AWS Docker 注册表?

您需要登录到 Docker 守护程序。您可以使用以下代码获取登录命令:

$ aws ecr get-login --no-include-email
  1. 我们用什么工具来设置 EKS 集群?

eksctl允许我们从命令行创建整个集群,并根据需要进行扩展或缩减。

  1. 我们在本章中做了哪些主要更改,以便我们可以使用之前章节中的 YAML 文件?

我们必须更改图像定义才能使用 AWS 注册表。我们包括了活跃性和就绪性探针,以及部署策略。

这些只添加到frontend部署中。将其余部署添加留给你作为练习。

  1. 在这个集群中有没有不需要的 Kubernetes 元素?

Ingress 元素并不是严格要求的,因为 Thoughts Backend 和 Users Backend 无法从外部访问。前端服务能够创建一个面向外部的 ELB。

不要觉得我们的配置限制了你。你可以手动配置 ELB,这样你就可以以不同的方式访问集群,如果你愿意,你可以使用 Ingress 配置。

  1. 我们为什么需要控制与 SSL 证书关联的 DNS?

我们需要证明我们拥有 DNS,以便 SSL 证书可以验证只有 DNS 地址的合法所有者才能访问该 DNS 的证书。这是 HTTPS 的根本要素,并且表明您正在与特定 DNS 的所有者进行私人通信。

  1. 存活探针和就绪探针之间有什么区别?

如果就绪探针失败,Pod 将不接受请求,直到再次通过。如果存活探针失败,容器将被重新启动。

  1. 滚动更新在生产环境中为什么重要?

它们很重要,因为它们避免了服务中断。它们一次添加一个工作进程,同时删除旧的工作进程,确保任何时候可用的工作进程数量保持不变。

  1. 自动缩放 Pod 和节点之间有什么区别?

由于节点反映在物理实例中,对它们进行扩展会影响系统中的资源。与此同时,扩展 Pod 使用它们可用的资源,但不会修改它们。

换句话说,增加节点数量会增加需要在系统上运行的硬件。这是有成本的,因为我们需要从云提供商那里租用更多的硬件。增加 Pod 数量在硬件方面没有成本,这就是为什么应该有一些额外的开销来允许增加。

两种策略应该协调,以便我们可以迅速对负载增加做出反应,并同时减少正在使用的硬件数量,以便降低成本。

  1. 在本章中,我们部署了自己的数据库容器。在生产中,这是不需要的。但是,如果您连接到已经存在的数据库,您将如何做到这一点?

第一步是更改thoughts_backend/deployment.yamlusers_backend/deployment.yaml文件中的环境变量。主要连接的是POSTGRES_HOST,但用户和密码可能也需要更改。

我们可以创建一个名为postgres-db的内部 Kubernetes 服务,它指向外部地址,而不是直接连接到POSTGRES_HOST作为 IP 或 DNS 地址。这可以帮助我们抽象外部数据库的地址。

这将一次性部署,以确保我们可以连接到外部数据库。

然后,我们可以删除部署中描述的数据库容器,即thoughts-backend-dbusers-backend-db。这些容器的映像仅用于测试和开发。

第八章

  1. 使用脚本将新代码推送到服务器和使用 Puppet 等配置管理工具之间有什么区别?

当使用脚本将新代码推送到服务器时,每台服务器都需要单独推送代码。Puppet和其他配置管理工具有一个集中的服务器,接收新数据并适当分发。它们还监视服务器是否按预期运行,并可以执行补救任务。

配置管理工具用于大型集群,因为它们减少了需要在自定义脚本中处理的工作量。

  1. DevOps 的核心理念是什么?

DevOps 的核心理念是赋予团队控制权,使他们能够自行部署和管理基础设施。这需要一套自动化程序作为安全网络,以确保这些操作易于进行、安全且快速。

  1. 使用 GitOps 的优势是什么?

使用 GitOps 的主要优势如下:

  • Git 是大多数团队已经知道如何使用的常见工具。

  • 它保留了基础设施定义的副本,这意味着我们可以将其用作备份,并从灾难性故障中恢复,或者轻松地基于先前的定义创建新的集群。

  • 基础设施更改是有版本的,这意味着我们可以逐个进行小的离散更改,并在出现问题时撤消其中任何一个。

  1. Kubernetes 集群只能使用 GitOps 吗?

尽管 GitOps 与 Kubernetes 具有协同作用,因为 Kubernetes 可以通过 YAML 文件进行控制,但没有什么能阻止我们使用 Git 存储库来控制集群。

  1. Flux 部署位于哪里?

它位于自己的 Kubernetes 集群中,以便可以从 Git 中提取数据。

  1. 您需要在 GitHub 中配置什么,以便 Flux 可以访问它?

您需要将 SSH 密钥添加到 GitHub 存储库的部署密钥中。您可以通过调用fluxctl identity来获取 SSH 密钥。

  1. 在生产环境中工作时,GitHub 提供的哪些功能确保我们对部署有控制?

在我们可以合并到主分支之前,需要进行审查和批准,这会触发部署。强制从特定用户那里获得批准的代码所有者的包含可以帮助我们控制敏感区域。

第九章

  1. 在微服务架构下运行的系统中,当收到新的业务功能时,需要进行哪些分析?

我们需要确定新业务功能影响哪个微服务或多个微服务。影响多个微服务的功能使其实施更加困难。

  1. 如果一个功能需要更改两个或更多微服务,我们如何决定首先更改哪个?

这应该以向后兼容的方式进行,以保持向后兼容性。在考虑向后兼容性的情况下添加新功能,因此可能性有限。一旦后端准备就绪,前端可以相应地进行更改,以便我们可以利用新功能。

  1. Kubernetes 如何帮助我们设置多个环境?

在 Kubernetes 中创建新的命名空间非常容易。由于系统的定义封装在 YAML 文件中,它们可以被复制和修改以创建一个重复的环境。这可以用作基线,然后进行演变。

  1. 代码审查是如何工作的?

在一个分支中的代码与主分支进行比较。另一个开发人员可以查看它们之间的差异并进行评论,要求澄清或更改。然后可以讨论这些评论,如果审阅者认为代码足够好,那么代码就可以被批准。合并可以被阻止,直到它获得一个或多个批准。

  1. 代码审查的主要瓶颈是什么?

主要瓶颈是没有审阅者提供反馈并批准代码。这就是为什么有足够的人可以担任审阅者角色非常重要。

  1. 根据 GitOps 原则,部署的审查与代码审查不同吗?

不;在 GitOps 下,部署被视为代码,因此它们可以像任何其他代码审查一样进行审查。

  1. 一旦功能准备合并到主分支中,为什么有一个清晰的部署路径是重要的?

重要的是要有一个清晰的部署路径,以便每个人都在同一页面上。它还提供了部署速度的明确期望。通过这样做,我们可以指定何时需要审查。

  1. 为什么数据库迁移与常规代码部署不同?

它们不同,因为无法轻松回滚。虽然代码部署可以回滚以便重新部署以前的镜像,但数据库迁移会对数据库或数据的模式进行更改,如果它们被回滚可能会导致数据丢失。通常,数据库迁移只能向前进行,任何出现的问题都需要通过新的部署进行更正。

这就是为什么我们必须特别小心数据库迁移,并确保它们不向后兼容的主要原因。

第十章

  1. 系统的可观察性是什么?

这是系统的容量。它让您知道其内部状态是什么。

  1. 默认情况下日志中有哪些不同的严重级别?

按严重性递增的不同严重级别是DEBUGINFOWARNINGERRORCRITICAL

  1. 指标用于什么?

指标允许您了解系统上发生的事件的聚合状态,并了解系统的一般状态。

  1. 为什么需要在日志中添加请求 ID?

您需要向日志中添加请求 ID,以便您可以将与同一请求对应的所有日志分组在一起。

  1. Prometheus 中有哪些类型的指标?

计数器,用于计算特定事件;仪表,用于跟踪可以上升或下降的值;以及直方图(或摘要),用于跟踪与值相关联的事件,例如事件发生的时间或请求返回的状态代码。

  1. 度量中的 75th 百分位数是什么,它与平均值有何不同?

对于直方图,75^(th)百分位数是平均值高于25%的事件的位置,而低于它的事件占75%。平均值是通过将所有值相加并将该值除以最初相加在一起的值的数量来找到的。通常,平均值将接近 50th 百分位数,尽管这取决于值的分布方式。

90(th)*-*95(th)百分位数对于确定延迟很有用,因为它提供了请求的上限时间,不包括异常值。平均值可能会被异常值所偏离,因此不能为绝大多数请求提供真实的数字。

  1. 四个黄金信号是什么?

四个黄金信号是收集系统健康描述的四个测量值。它们是请求的延迟、流量量、返回错误的百分比和资源的饱和度。

第十一章

  1. 在微服务架构系统和单体架构中发布更改有哪些不同?

在单体架构中发布更改只涉及一个存储库,因为单体只是一个代码库。在微服务架构中进行的一些更改将需要我们更改两个或更多微服务,以便我们可以分配它们。这需要更多的规划和关注,因为我们需要确保这是正确协调的。在正确架构的微服务系统中,这种多存储库更改应该相对罕见,因为它们会产生额外的开销。

  1. 为什么在微服务架构中发布更改应该小?

微服务的优势在于我们可以并行发布微服务,这比单体发布更快。然而,鉴于微服务中的发布可能会影响其他微服务,它们应该以迭代方式工作,减少更改的规模并增加部署速度。

小的更改风险较小,如果需要,可以更容易地回滚。

  1. 语义版本如何工作?

在语义版本中,版本有三个数字:Major版本号,Minor版本号和Patch版本号。它们之间都用点分隔:

  • 补丁版本的增加只修复错误和安全问题。

  • 次要版本的增加会增加更多功能,但不会引入向后不兼容的更改。

  • 主要版本的增加会产生不向后兼容的更改。

  1. 微服务架构系统中内部接口的语义版本控制存在哪些问题?

由于微服务中的部署非常常见,向后兼容性非常重要,因此主要发布的含义变得淡化。此外,大多数微服务的消费者是内部的,因此版本之间的隐式通信变得不那么重要。

当发布变得常见时,语义版本控制失去了意义,因为目标是不断完善和改进产品,而不是标记大的发布。

  1. 添加版本端点的优势是什么?

任何使用微服务的消费者都可以以与进行任何其他请求相同的方式请求其版本:通过使用 RESTful 调用。

  1. 我们如何解决本章代码中的依赖问题?

本章的代码存在依赖问题。

  1. 我们应该在共享 ConfigMap 中存储哪些配置变量?

我们应该存储被多个微服务访问的配置变量。我们应该预先存储大部分配置变量,以便它们可以被重复使用。

  1. 描述将所有配置变量放入单个共享 ConfigMap 的优缺点。

一个共享的 ConfigMap 使配置变量非常明确。它鼓励每个人重复使用它们,并告诉其他人配置用于其他微服务的用途。

更改微服务的依赖将触发重新启动,因此更改作为一切依赖的 ConfigMap 将导致集群中的所有微服务重新启动,这是耗时的。

此外,单个 ConfigMap 文件可能会变得相当大,将其拆分为几个较小的文件可以帮助我们更有效地组织数据。

  1. Kubernetes ConfigMap 和 Kubernetes Secret 有什么区别?

Kubernetes Secrets 更好地防止意外访问。直接访问工具不会以明文显示 Secret。对 Secret 的访问也需要以更明确的方式进行配置。另一方面,ConfigMaps 可以进行批量配置,因此 pod 将能够访问 ConfigMap 中存储的所有值。

  1. 我们如何更改 Kubernetes Secret?

我们可以使用kubectl edit更改 Secret,但它需要以 Base64 格式进行编码。

例如,要用以下代码替换postgres-password秘密为someotherpassword值:

$ echo someotherpassword | base64
c29tZW90aGVycGFzc3dvcmQK
$ kubectl edit secrets -n example thoughts-secrets
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
 postgres-password: c29tZW90aGVycGFzc3dvcmQK
...
secret/thoughts-secrets edited

一旦重新启动,我们的 pod 将能够使用新的 Secret。

  1. 假设基于我们的配置,我们决定将 public_key.pub 从 Secret 更改为 ConfigMap。我们需要做哪些更改?

我们需要更改 ConfigMap,以便它包含configuration.yaml中的文件:

THOUGHTS_BACKEND_URL: http://thoughts-service
public_key.pub: |
  -----BEGIN PUBLIC KEY-----
  <public key>
  -----END PUBLIC KEY-----
USER_BACKEND_URL: http://users-service

注意缩进以界定文件。 |字符标记多行字符串。

然后,在deployment.yaml文件中,我们需要将挂载的来源从 Secret 更改为 ConfigMap:

volumes:
    - name: public-key
      configMap:
          name: shared-config
          items:
              - key: public_key.pub
                path: public_key.pub

记得先将这些更改应用到 ConfigMap 中,以便在应用部署文件时它们是可用的。

请注意,此方法创建了一个名为public_key.pub的环境变量,以及文件的内容,因为它作为shared-config ConfigMap 的一部分应用。另一种方法是创建一个独立的 ConfigMap。

在所有 pod 重新启动后,可以删除 Secret。

第十二章

  1. 为什么对于微服务架构系统来说,有一个领先的架构师很方便?

在微服务架构中构建系统允许我们创建可以并行处理的独立服务。这些服务仍然需要相互通信和合作。

独立团队通常无法把握全局,倾向于专注于自己的项目。为了协调和整体系统的发展,独立团队需要一个高层次概览系统的首席架构师。

  1. 康威定律是什么?

康威定律是一句谚语,它说软件结构复制了编写它的组织的沟通结构。

这意味着,要改变软件的结构,组织需要改变,这是一项更加困难的任务。

为了成功设计和发展大型系统,组织需要考虑并相应地进行规划。

  1. 技术债务是如何引入的?

技术债务可以以许多方式产生。

通常,技术债务可以分为以下四类或它们的混合:

  • 通过过快地发展而没有花时间分析其他选项

  • 通过妥协缩短开发时间,同时知道这种妥协以后需要修复

  • 通过对当前系统或工具的了解不够充分,或者缺乏培训或专业知识

  • 通过对外部问题做出错误假设,从而为不一定需要修复的东西设计

  1. 为什么重要建立一种文化,以便我们可以持续减少技术债务?

重要的是建立一种文化,以避免软件腐烂,即由于向现有软件添加复杂性而导致性能和可靠性持续下降。除非解决技术债务成为一个持续的过程,否则日常发布的压力意味着我们将无法进行维护。

  1. 为什么重要记录发布中的问题并与团队其他成员分享?

这很重要,因为每个团队都可以从其他人的经验和解决方案中学习,并改进他们的流程。这也可以创造一种开放的文化,人们不会害怕对自己的错误负责。

  1. 事后总结会议的主要目标是什么?

事后总结会议的主要目标是创建解决事故原因的后续任务。为此,我们需要尽可能确信已成功检测到根本原因(这也是次要目标)。

posted @ 2024-05-20 16:51  绝不原创的飞龙  阅读(12)  评论(0编辑  收藏  举报