Python-企业级应用开发实用指南(全)

Python 企业级应用开发实用指南(全)

原文:zh.annas-archive.org/md5/B119EED158BCF8E2AB5D3F487D794BB2

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Python 是一种动态类型的解释语言,可以快速构建各种领域的应用程序,包括人工智能、桌面和 Web 应用程序。

随着 Python 生态系统的最新进展和支持高度可重用性并使模块化代码编译成为可能的大量库的可用性,Python 可以用于构建能够解决组织问题的应用程序。这些应用程序可以在短时间内开发,并且如果开发得当,可以以一种解决组织需求的方式进行扩展。

Python 3.7 版本带来了几项改进和新功能,使应用程序开发变得轻松。连同...

这本书适合谁

企业应用程序是旨在解决组织特定业务需求的关键应用程序。企业应用程序的要求与个人通常需要的要求大不相同。这些应用程序应提供高性能和可扩展性,以满足组织日益增长的需求。

考虑到这一点,本书适用于具有 Python 编程中级知识并愿意深入了解根据组织需求进行扩展的应用程序构建的开发人员。本书提供了几个示例,可以在运行在 Linux 发行版上的 Python 3.7 上执行,但也适用于其他操作系统。

为了充分利用本书,您必须对基本操作系统概念有基本的了解,例如进程管理和多线程。除此之外,对数据库系统的基本工作知识可能有益,但不是强制性的。

熟悉 Python 应用程序构建不同方面的开发人员可以学习有助于构建可扩展应用程序的工具和技术,并了解企业应用程序开发方法的想法。

充分利用本书

除了对编程有一般了解外,不需要特定的专业知识才能利用本书。

Odoo 是使用 Python 构建的,因此对该语言有扎实的了解是个好主意。我们还选择在 Ubuntu 主机上运行 Odoo(一种流行的云托管选项),并且将在命令行上进行一些工作,因此一些熟悉将是有益的。

为了充分利用本书,我们建议您找到关于 Python 编程语言、Ubuntu/Debian Linux 操作系统和 PostgreSQL 数据库的辅助阅读。

尽管我们将在 Ubuntu 主机上运行 Odoo,但我们还将提供关于如何在...设置开发环境的指导

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packt.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-Enterprise-Application-Development-with-Python。我们还有其他代码包来自我们丰富的书籍和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

使用的约定

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

CodeInText:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。以下是一个例子:“除了这三个包,读者还需要sqlalchemy包,它提供了我们将在整个章节中使用的 ORM,以及psycopg2,它提供了postgres数据库绑定,允许sqlalchemy连接到postgres。”

代码块设置如下:

username = request.args.get('username')email = request.args.get('email')password = request.args.get('password')user_record = User(username=username, email=email, password=password)

当我们希望绘制...

第一章:使用 Python 进行企业开发

Python 在编程世界中已经存在了二十多年,多年来,这种语言已经经历了许多改进,一个不断增长的社区,以及许多生产就绪和得到良好支持的库。但 Python 是否准备好在长期由 C++、Java 和.NET 等所谓的企业级语言主导的企业应用程序开发领域取得突破?

在本章中,我们将看到 Python 如何在多年来发展,并且准备成为企业应用程序开发领域的严肃竞争者。

本章将涵盖以下主题:

  • Python 的最新发展,以促进其在企业应用程序开发中的增长

  • Python 发光的特殊用例

  • 企业和通用软件之间的区别

  • 开发企业应用程序的要求

技术要求

本书中的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter01目录下找到

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

运行代码的说明可以在各个章节目录下的README文件中找到。

代码已经测试在运行 Fedora 28 和 Python 版本 3.6.5 的系统上运行,但它应该能够在运行 Python 3.6.5 的任何系统上运行。

Python 的最新发展

Python 是一种动态类型的解释型语言,最初非常适合于无聊和重复的日常脚本任务。但随着年龄的增长,语言获得了许多新功能和庞大的社区支持,推动了其发展,使其成为一种非常适合执行从简单的应用程序(如网络抓取)到分析大量数据以训练机器学习模型的任务的语言。这些模型本身是用 Python 编写的。让我们看看多年来发生了哪些重大变化,并了解 Python 的最新版本 Python 3 带来了什么。

放弃向后兼容性

Python 作为一种语言在多年来发生了很大变化,但尽管这一事实,用 Python 1.0 编写的程序仍然能够在 Python 2.7 中运行,这是在 Python 1.0 发布 19 年后发布的版本。

尽管对 Python 应用程序的开发人员来说是一个巨大的好处,但语言的这种向后兼容性也是语言规范的重大改进的增长和发展的主要障碍,因为如果对语言规范进行重大更改,大量旧代码库将会中断。

随着 Python 3 的发布,这种向后兼容性链被打破了。版本 3 的语言放弃了对早期版本编写的程序的支持...

这都是 Unicode

在 Python 2 时代,文本数据类型str用于支持 ASCII 数据,对于 Unicode 数据,语言提供了unicode数据类型。当有人想要处理特定编码时,他们会取一个字符串并将其编码为所需的编码方案。

此外,该语言天生支持将字符串类型隐式转换为unicode类型。如下代码片段所示:

str1 = 'Hello'
type(str1)        # type(str1) => 'str'
str2 = u'World'
type(str2)        # type(str2) => 'unicode'
str3 = str1 + str2
type(str3)        # type(str3) => 'unicode'

这曾经有效,因为在这里,Python 会隐式地使用默认编码将字节字符串str1解码为 Unicode,然后执行连接。这里需要注意的一点是,如果str1字符串包含任何非 ASCII 字符,那么这种连接在 Python 中将失败,引发UnicodeDecodeError

随着 Python 3 的到来,处理文本的数据类型发生了变化。现在,默认数据类型str用于存储文本并支持 Unicode。此外,Python 3 还引入了一个名为bytes的二进制数据类型,用于存储二进制数据。这两种类型strbytes是不兼容的,它们之间不会发生隐式转换,任何尝试这样做的行为都会引发TypeError,如下面的代码所示:

str1 = 'I am a unicode string'
type(str1) # type(str1) => 'str'
str2 = b"And I can't be concatenated to a byte string"
type(str2) # type(str2) => 'bytes'
str3 = str1 + str2
-----------------------------------------------------------
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't concat str to bytes

正如我们所看到的,尝试将unicode类型字符串与byte类型字符串连接失败,出现了TypeError。虽然无法将string隐式转换为byte,或者将byte隐式转换为string,但我们有一些方法可以将string编码为bytes类型,将bytes类型解码为string。看看以下代码:

str1 = '₹100'
str1.encode('utf-8')
#b'\xe2\x82\xb9100'
b'\xe2\x82\xb9100'.decode('utf-8')
# '₹100'

字符串类型和二进制类型之间的明显区别以及对隐式转换的限制可以实现更健壮的代码和更少的错误。但这些变化也意味着,任何在 Python 2 中处理 Unicode 的代码都需要在 Python 3 中进行重写,因为存在向后不兼容性。

在这里,你应该关注用于将string转换为bytes和反之的编码和解码格式。选择不同的格式进行编码和解码可能会导致重要信息的丢失,并可能导致数据损坏。

类型提示的支持

Python 是一种动态类型语言,因此变量的类型在赋值后由解释器在运行时评估,如下面的代码所示:

a = 10type(a)        # type(a) => 'int'a = "Joe"      type(a)        # type(a) => 'str'

尽管动态解释变量类型的功能在编写小型程序时可能很方便,因为代码库可以很容易地被跟踪,但当处理非常庞大的代码库时,这种语言特性也可能成为一个大问题,因为会产生大量模块,跟踪特定变量类型可能会成为一个挑战,与不兼容类型相关的愚蠢错误也很容易发生。看看以下代码:...

Python 的亮点

每种语言都是为了解决开发人员在构建特定领域软件时遇到的某种类型的问题而开发的。Python 作为一种动态类型、解释型语言,也有一系列擅长的用例。

这些用例涉及自动化重复和乏味的任务,快速原型设计应用程序,以及专注于实现特定目标的小型应用程序,比如安装软件、设置开发环境、执行清理等。

但这就是全部吗?Python 只适用于执行小任务吗?答案是否定的。作为一种语言,Python 更加强大,可以轻松完成大量越来越复杂的任务,比如运行一个网站,能够在很短的时间内扩展以满足数百万用户的需求,处理大量的传入文件,或者为图像识别系统训练机器学习模型。

我们正在讨论使用 Python 执行越来越复杂的任务,但与我们传统的编译时语言(如 C++、Java 和.NET)相比,Python 是否慢?嗯,这完全取决于一个人想要使用 Python 的上下文。如果你的目标是在处理能力有限的嵌入式设备上运行 Python 程序,那么是的,Python 可能不够,因为其解释器对处理环境的额外负载。但如果你计划在配置良好的现代硬件上运行 Web 应用程序,你可能永远不会在使用 Python 时遇到任何减速。相反,你可能会觉得在使用 Python 时更加高效,因为其语法非常简单,执行操作时无需编写数百行代码。

因此,让我们看看 Python 在企业环境中的表现。

企业 IT 的需求

企业 IT 是复杂的,为企业构建的应用程序与为普通消费者构建的应用程序有很大的不同。在为企业用户开发应用程序之前,需要考虑几个因素。让我们看看企业 IT 应用程序与普通消费者产品有何不同,如下列表所示:

  • 面向业务:与为解决个人用户问题而构建的应用程序不同,企业应用程序是为满足组织的特定需求而构建的。这要求应用程序符合组织的业务实践、规则和工作流程。

  • 健壮性...

Python 在企业生态系统中

Python 以多种形式存在于企业生态系统中;无论是自动化乏味和重复的任务,作为产品两层之间的粘合剂,还是用于构建快速易用的大型服务器后端客户端,该语言在各种用例中都看到了越来越多的采用。但是是什么让 Python 准备好开发大型企业应用程序呢?让我们来看一下:

  • 能够快速构建原型:Python 的语法非常简单,很多事情可以用很少的代码实现。这使开发人员能够快速开发和迭代应用程序的原型。除此之外,这些原型并不总是需要被丢弃,如果开发得当,它们可以作为构建最终应用程序的良好基础。

通过快速原型化应用程序的能力,企业软件开发人员可以准确地看到需求如何在应用程序中对齐以及应用程序的性能如何。有了这些信息,应用程序的利益相关者可以更准确地定义应用程序开发的路径,从而避免因为某些事情没有按预期的方式进行而导致中期架构更改。

  • 成熟的生态系统:成熟的生态系统是 Python 值得关注的特性之一。Python 的外部库数量正在迅速增长。对于大多数需要在应用程序中实现的任务,例如双因素身份验证、测试代码、运行生产 Web 服务器、与消息总线集成等,您可以轻松地寻找到一个具有相当不错支持的库。

这证明了它非常有帮助,因为它减少了代码重复量,并增加了组件的可重用性。借助诸如pip之类的工具,很容易将所需的库添加到项目中,并借助诸如virtualenv之类的工具,您可以轻松地在同一系统上对许多不同的项目进行分隔,而不会创建依赖混乱。

例如,如果有人想要构建一个简单的 Web 应用程序,他们可能只需使用 Flask,这是一个用于开发 Web 应用程序的微框架,并且可以继续开发 Web 应用程序,而无需担心处理套接字、操纵数据等底层复杂性。他们只需要几行代码就可以让一个简单的应用程序运行起来,如下面的代码所示:

from flask import Flask
app = Flask(__name__)

@app.route('/', methods=["GET"])
def hello():
    return "Hello, this is a simple Flask application"

if name == '__main__':
    app.run(host='127.0.0.1', port=5000)

现在,一旦有人调用前面的脚本,他们将拥有一个flask HTTP 应用程序正在运行。这里剩下要做的就是启动浏览器并导航到http://localhost:5000。然后我们将看到 Flask 在不费吹灰之力地提供 Web 应用程序。所有这些都可以在不到 10 行代码的情况下实现。

有许多外部库为许多任务提供支持,企业开发人员可以轻松地在应用程序中启用对新功能的支持,而无需从头开始编写所有内容,从而减少可能出现的错误和非标准化接口进入应用程序的机会。

  • 社区支持:Python 语言不归任何特定的公司实体所有,完全由庞大的社区支持,决定标准的未来。这确保了语言将继续得到长时间的支持,并且不会很快过时。这对组织来说非常重要,因为他们希望他们运行的应用程序能够得到长期的支持。

考虑到 Python 的所有优势,如果能够以经过精心规划的方式做出决策,那么使用该语言时开发人员的生产力将得到提升,同时还能够降低软件的总拥有成本。这些决策涉及应用程序架构的布局以及使用外部库或自行开发的决定。因此,是的,Python 现在确实已经准备好在企业应用程序开发的主流世界中使用。

介绍 BugZot - 一个 RESTful 错误跟踪器

随着我们在本书的章节中的进展,我们需要一种方法来实现我们所学到的知识。

想象一下,你在一家名为Omega Corporation的组织工作,这是一家向公司和个人销售软件产品的市场领导者。Omega Corporation 需要一个系统,通过该系统可以跟踪其产品中的错误。经过大量的头脑风暴,他们启动了一个名为 BugZot 的项目,这将是他们跟踪产品中错误的工具。

让我们看看 Omega Corporation 希望通过 BugZot 项目实现什么:

  • 用户能够报告产品中的错误:用户,无论是内部还是外部用户,都应该能够针对特定产品报告错误...

在开发之前收集需求

在开始开发企业应用程序之前收集软件需求可能是一项繁琐的任务,如果未能充分做到这一点,可能会导致严重后果,例如由于在应用程序开发周期后期识别需求而导致的延迟增加的成本。缺乏重要功能以改进业务流程工作流的应用程序将导致用户在最坏的情况下停止使用应用程序。

需求收集过程复杂而繁琐,在组织中可能需要数月才能完成。本书的范围超出了涉及该过程的所有步骤。本节试图简要描述需求收集软件需求过程中的一些重要步骤。

询问用户需求

对于组织内部的应用程序,可能会有各种利益相关者和用户,可以定义应用程序的需求。这些用户可以大致分为两类:

  • 劳动力:这些是通常使用应用程序来完成一定任务的用户。他们不关心应用程序提供的所有功能,而是关注应用程序如何适应他们的个人工作流程。这些用户可以提供特定于他们工作的需求,但可能无法提供关于他们将来可能需要什么或其他团队可能需要什么的想法。

  • 管理层:管理层由人员组成...

需求分类

一旦用户被调查了他们希望在应用程序中拥有什么,下一步就是对这些需求进行分类。广义上说,需求可以分为两部分:

  • 功能需求:这些是定义应用程序功能和功能的需求。例如,BugZot 具有以下功能需求:

  • 为内部和外部用户提供提交错误的功能

  • 提供角色和权限支持

  • 提供处理文件上传的功能

  • 与电子邮件系统集成,以便在错误更改状态时发送电子邮件,等等

  • 非功能需求:这些是不影响软件功能的一组要求,而是基于功能需求的隐式或显式特征。例如,在 BugZot 中,以下可能被定义为一些非功能需求:

  • 应用程序应提供针对常见 Web 攻击向量(如 XSS 和 CSRF)的安全性

  • 应用程序的运营成本不应超过总预算的N%

  • 应用程序应能够在崩溃后需要恢复时生成备份

优先考虑需求

一旦确定并将需求分类为功能和非功能需求,就需要根据其在应用程序中的重要性对其进行优先考虑。如果不进行这种优先考虑,将导致开发成本增加、截止日期延迟,并降低组织的生产力。广义上,我们可以将需求分类为以下类别:

  • 必须有:这些是对应用程序成功至关重要的要求,在应用程序发货时必须存在。

  • 应该有:这些是那些将增强应用程序功能的要求,但需要进一步讨论是否...

生成软件需求规格说明文档

一旦确定、分组和优先考虑了需求,就会生成一份名为软件需求规格说明的文档。该文档描述了需要开发的软件的预期目的、需求和性质。

软件需求规格说明SRS)将描述以下信息:

  • 应用程序的预期目的

  • 文档中使用的约定是特定于组织业务流程的

  • 应用程序的特性

  • 将使用应用程序的用户类

  • 应用程序将运行的环境

  • 应用程序的功能和非功能需求

一旦 SRS 生成,就会进行审查和进一步的谈判。一旦成功完成,应用程序就会进入设计阶段,其中会设计应用程序的模拟。

摘要

在本章中,我们简要介绍了不断变化的编程环境,并探讨了多年来 Python 生态系统的变化。我们看到 Python 允许快速原型设计,并且拥有大量得到良好支持的库和一个开放的社区,因此迅速成为需要长期支持和与现有系统轻松集成的企业大型应用程序开发的主要选择。

然后,我们介绍了演示应用程序 BugZot,这是我们将在本书的过程中构建的,并定义了应用程序所需的功能。

本章的最后一节涵盖了...

问题

  1. 在 Python 3 中是否可以对str类型和byte类型执行连接等操作?

  2. Python 3 中引入的类型提示支持是否是强制性的?

  3. 除了功能和非功能需求之外,还有其他类型的需求可能需要记录到软件需求规格说明中吗?

  4. 需求优先级可以在哪些主要类别中进行?

  5. 一旦生成了软件需求规格说明文档,接下来应该采取哪些步骤?

进一步阅读

如果您想在进入企业应用程序开发世界之前再次学习 Python 编程的基础知识,Packt 有一本非常好的书可以供您参考。您可以在以下链接获取:

第二章:设计模式-做出选择

当进行软件应用程序开发项目时,它本质上被视为需要解决的问题。当我们开始开发应用程序时,我们开始开发一个特定于给定问题的解决方案。最终,这个解决方案可能开始在类似问题中得到重复使用,并成为解决这类问题的标准解决方案。随着时间的推移,我们发现很多显示相同模式的问题。一旦我们修改我们的标准解决方案以适应这种观察到的模式,我们就提出了一个设计模式。设计模式不是闹着玩的;经过多年的尝试和测试,才能产生,用于解决大量具有相似模式的问题。

设计模式不仅定义了我们构建软件应用程序的方式,还提供了关于在尝试解决特定类型问题时什么有效和什么无效的知识。有时候,没有特定的设计模式可能适合特定应用程序的需求,开发人员别无选择,只能提出独特的解决方案。

是否有一些现有的标准设计模式可以用于特定类型的问题?我们如何决定在我们的问题中使用哪种设计模式?我们可以偏离特定的设计模式并在解决方案中使用它们吗?随着我们在本章的进展,我们将尝试回答这些问题。

在本章结束时,您将了解以下内容:

  • 设计模式及其分类

  • Python 的面向对象特性,以及我们如何使用它来实现一些常见的设计模式

  • 特定模式可能被使用的用例

技术要求

本书的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter02目录下找到。

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

运行代码示例的说明可以在章节目录内的README.md文件中找到。

设计模式

设计模式定义了我们如何组织解决给定问题的方式。它不定义可以用来解决问题的算法,而是提供了关于例如代码应该如何组织,需要定义哪些类,它们的粒度将是什么,以及如何创建不同对象的抽象。

设计模式已经获得了很多关注,1994 年出版的书籍《设计模式:可复用面向对象软件的元素》仍然是理解设计模式时的事实参考。

设计模式通常包括以下元素:

  • 问题陈述:问题陈述描述了我们想要解决的问题,因此也定义了我们可以使用的设计模式。问题陈述将告诉我们关于我们计划追求的设计范围,我们可能需要注意的约束,有时还会告诉我们应用程序中不同组件如何相互通信。

  • 解决方案:解决方案描述了弥补问题的设计。它详细说明了类层次结构应该如何形成,对象将如何形成,对象之间的关系以及不同组件之间的通信将如何进行。解决方案将是一个抽象设计,不指定实现的细节。这使得解决方案通用,可以应用于一类问题,而不用关心应该使用什么算法来解决特定问题。

  • 后果:在软件开发世界中,没有免费的东西。一切都有代价,我们为一件事情而牺牲另一件事情。重要的是权衡是否合理。同样适用于设计模式的选择,它们也有自己的后果。大多数情况下,这些后果是空间和时间的权衡,是评估替代选项的重要部分,如果特定的设计选择不能证明权衡成本的合理性。有时,后果也可能定义语言的实现障碍,并且通常会影响应用程序的可重用性和灵活性。

选择设计模式并不是每组问题都通用的事情。解决问题将基于多种因素,如开发人员对问题的解释,需要使用的编程语言的限制,与项目相关的截止日期等。

设计模式的分类

在书籍《设计模式:可重用面向对象软件的元素》中,设计模式被分类为三大类:

  • 创建模式:这些模式定义了如何创建对象,以便您的代码可以独立于存在哪些对象,并因此使其与可能发生的新对象引入代码库的影响分离。这需要将对象创建逻辑与代码库隔离开来。Singleton 和 Factory 等模式属于创建模式类别。

  • 结构模式:与创建模式不同,结构模式通常用于描述...

定义设计模式的选择

在选择设计模式时,我们可能希望设计模式具有一定的特征。让我们看看如果我们要使用 Python 来实现我们的设计模式,这些特征可能包括什么:

  • 最小惊讶原则:Python 之禅说应该遵循最小惊讶原则。这意味着使用的设计模式在行为方面不应该让用户感到惊讶。

  • 减少耦合:耦合被定义为软件内不同组件之间相互依赖的程度。具有高耦合度的软件可能很难维护,因为对一个组件的更改可能需要对许多其他组件进行更改。耦合作为一种影响无法完全从软件中移除,但应该选择设计模式,以便在开发过程中最小化耦合度。

  • 专注于简单性:开始开发一个软件时,过于泛化的设计原则可能会带来更多的害处。它可能会在代码库中引入许多不需要的功能,这些功能很少被使用或根本不被使用。设计模式的选择应该更多地专注于为所述问题提供简单的解决方案,而不是专注于特定设计模式可以解决多少常见类型的问题。

  • 避免重复:良好的设计模式选择将帮助开发人员避免重复代码逻辑,并将其保留在一个位置,系统的不同组件可以从该位置访问。逻辑重复的减少不仅可以节省开发时间,还可以使维护过程变得简单,其中逻辑的更改只需要在一个地方进行,而不是在代码库的多个部分进行。

面向对象的 Python

面向对象编程OOP)指的是以一种不关心方法组织的格式组织代码,而是关心对象、它们的属性和行为。

一个对象可以代表任何逻辑实体,比如动物、车辆和家具,并且会包含描述它们的属性和行为。

面向对象编程语言的基本构建块是,通常将逻辑相关的实体组合成一个单一的单元。当我们需要使用这个单元时,我们创建这个单元的一个新实例,称为类对象,并使用对象公开的接口来操作对象。

Python 中的面向对象编程...

基本的面向对象编程原则

一个语言不能仅仅因为它支持类和对象就被认为是面向对象的语言。该语言还需要支持一系列不同的功能,比如封装、多态、组合和继承,才能被认为是面向对象的语言。在这方面,Python 支持许多基于面向对象编程的概念,但由于其松散的类型特性,它的实现方式有些不同。让我们看看这些特性在 Python 中的区别。

封装

封装是一个术语,用来指代类限制对其成员的访问的能力,只能通过对象公开的接口来访问。封装的概念帮助我们只关注我们想要对对象做什么的细节,而不是对象如何处理内部的变化。

在 Python 中,封装并不是严格执行的,因为我们没有访问修饰符的支持,比如私有、公共和受保护,这些可以严格控制类内部特定成员的访问。

然而,Python 确实支持封装,借助名称修饰,可以用来限制对特定属性的直接访问...

组合

组合是用来表达不同对象之间关系的属性。在组合中表达这种关系的方式是将一个对象作为另一个对象的属性。

Python 通过允许程序员构建对象,然后将其作为其他对象的一部分来支持组合的概念。例如,让我们看下面的代码片段:

class MessageHandler:
  __message_type = ['Error', 'Information', 'Warning', 'Debug']

  def __init__(self, date_format):
    self.date_format = date_format

  def new_message(message, message_code, message_type='Information'):
    if message_type not in self.__message_type:
      raise Exception("Unable to handle the message type")
    msg = "[{}] {}: {}".format(message_type, message_code, message)
    return msg

class WatchDog:

  def __init__(self, message_handler, debug=False):
    self.message_handler = message_handler
    self.debug = debug

  def new_message(message, message_code, message_type):
    try:
      msg = self.message_handler.new_message(message, message_code, message_type)
    except Exception:
      print("Unable to handle the message type")
    return msg

message_handler = MessageHandler('%Y-%m-%d')
watchdog = WatchDog(message_handler)

从例子中我们可以看到,我们已经将message_handler对象作为watchdog对象的属性。这标志着我们可以在 Python 中实现组合的一种方式。

继承

继承是我们创建对象层次结构的一种方式,从最一般的到最具体的。通常作为另一个类的基础的类也被称为基类,而继承自基类的类被称为子类。例如,如果一个类B派生自类A,那么我们会说类B是类A的子类。

就像 C++一样,Python 支持多重和多层继承的概念,但不支持在继承类时使用访问修饰符的概念,而 C++支持。

让我们看看如何在 Python 中实现继承,尝试模拟 BugZot 应用程序中的新请求将是什么样子。以下代码片段给出...

Python 中的多重继承

让我们看一个抽象的例子,展示了我们如何在 Python 中实现多重继承,可以在接下来的代码片段中看到:

class A:
    def __init__(self):
        print("Class A")

class B:
    def __init__(self):
        print("Class B")

class C(A,B):
    def __init__(self):
        print("Class C")

这个例子展示了我们如何在 Python 中实现多重继承。一个有趣的地方是要理解当我们使用多重继承时,Python 中的方法解析顺序是如何工作的。让我们来看看。

多重继承中的方法解析顺序

那么,基于前面的例子,如果我们创建一个C类的对象会发生什么呢?

>>> Cobj = C()Class C

正如我们所看到的,这里只调用了派生类的构造函数。那么如果我们想要调用父类的构造函数呢?为此,我们需要在我们的类C构造函数内部使用super()调用。为了看到它的作用,让我们稍微修改一下C的实现:

>>> class C(A,B):...  def __init__(self):...    print("C")...    super().__init__()>>> Cobj = C()CA

一旦我们创建了派生类的对象,我们可以看到派生类的构造函数首先被调用,然后是第一个继承类的构造函数。super()调用自动...

利用 mixin

Mixin 是每种面向对象语言中都存在的概念,可以用来实现可以在代码的不同位置重复使用的对象类。诸如 Django Web 框架之类的项目提供了许多预构建的 mixin,可以用于在我们为应用程序实现的自定义类中实现一定的功能集(例如,对象操作、表单渲染等)。

那么,mixin 是语言的一些特殊特性吗?答案是否定的,它们不是一些特殊特性,而是一些不打算成为独立对象的小类。相反,它们被构建为通过多重继承支持为类提供一些指定的额外功能。

回到我们的示例应用 BugZot,我们需要一种以 JSON 格式返回多个对象数据的方法。现在,我们有两个选择;我们可以在单个方法的级别构建返回 JSON 数据的功能,或者我们可以构建一个可以在多个类中重复使用的 mixin:

Import json
class JSONMixin:
  def return_json(self, data):
    try:
      json_data = json.dumps(data)
    except TypeError:
      print("Unable to parse the data into JSON")
    return json_data

现在,让我们想象一下,如果我们想要我们在尝试理解继承时在示例中实现的 bug 类。我们所需要做的就是在Bug类中继承JSONMixin

class Bug(Request, JSONMixin):
  …

通过简单地继承该类,我们就得到了所需的功能。

抽象基类

在面向对象编程中,抽象基类是那些只包含方法声明而不包含实现的类。这些类不应该有独立的对象,而是被构建为基类。从抽象基类派生的类需要为抽象类中声明的方法提供实现。

在 Python 中,虽然你可以通过不提供已声明方法的实现来构建抽象类,但语言本身并不强制派生类为方法提供实现。因此,如果在 Python 中执行以下示例,它将完美运行:

class AbstractUser:  def return_data(self):    passclass ...

元类

Python 提供了许多特性,其中一些直接对我们可见,例如列表推导、动态类型评估等,而另一些则不那么直接。在 Python 中,许多事情都可以被认为是魔术,是在幕后发生的。其中之一就是元类的概念。

在 Python 中,一切都是对象,无论是方法还是类。即使在 Python 内部,类也被认为是可以传递给方法、分配给变量等的一等对象。

但是,正如面向对象编程的概念所述,每个对象都表示一个类的实例。因此,如果我们的类是对象,那么它们也应该是某个类的实例。那么这个类是什么?这个问题的答案是type类。Python 中的每个类都是type类的实例。

这可以很容易地验证,如下面的代码片段所示:

class A:
  def __init__(self):
    print("Hello there from class A")

>>>isinstance(A, type)
True

这些对象是类的对象,被称为元类。

在 Python 中,我们不经常直接使用元类,因为大多数时候,我们试图通过其他简单的解决方案来解决元类的问题。但是元类确实为我们提供了很多创建类的方法。让我们首先看一下如何通过设计LoggerMeta类来创建我们自己的元类,该类将强制实例类为不同以HANDLER_为前缀的日志方法提供有效的处理程序方法:

class LoggerMeta(type):
  def __init__(cls, name, base, dct):
    for k in dct.keys():
      if k.startswith('HANDLER_'):
        if not callable(dct[k]):
          raise AttributeError("{} is not callable".format(k))
    super().__init__(name, base, dct)

def error_handler():
  print("error")
def warning_handler():
  print("warning")

class Log(metaclass=LoggerMeta):
  HANDLER_ERROR = error_handler
  HANDLER_WARN = warning_handler
  HANDLER_INFO = 'info_handler'

  def __init__(self):
    print(“Logger class”)

在这个例子中,我们通过从 type 类继承来定义了一个名为LoggerMeta元类。(为了定义任何元类,我们需要从 type 类或任何其他元类继承。继承的概念在元类创建期间也适用。)一旦我们声明了我们的元类,我们在元类中提供了__init__魔术方法的定义。元类的__init__魔术方法接收类对象、要创建的新类的名称、新类将派生自的基类列表以及包含用于初始化新类的属性的字典。

__init__方法中,我们提供了一个实现,用于验证以HANDLER_开头的类属性是否有有效的处理程序分配给它们。如果属性分配的处理程序不可调用,我们会引发AttributeError并阻止类的创建。在__init__方法的最后,我们返回基类__init__方法的调用结果。

在下一个例子中,我们创建两个简单的方法,它们将充当我们处理错误类型消息和警告类型消息的处理程序。

在这个例子中,我们定义了一个元类为LoggerMeta的类日志。这个类包含一些属性,比如HANDLER_ERRORHANDLER_WARNHANDLER_INFO和魔术方法__init__

现在,让我们看看如果我们尝试执行提供的例子会发生什么:

python3 metaclass_example.py
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __init__
AttributeError: HANDLER_INFO is not callable

从输出中可以看出,一旦解释器解析了类日志的定义以创建类,元类__init__方法就会被调用,验证类的属性并引发AttributeError

Python 中的元类为我们提供了很多强大的功能,并使我们能够以神奇的方式做很多事情,例如基于方法名称生成类属性,并跟踪类的实例化数量。

通过学习 Python 中的面向对象编程和元类的所有内容,现在让我们继续使用它们来实现 Python 中的一些设计模式,并学习如何决定使用哪种设计模式。

单例模式

单例模式是《设计模式》一书中四人帮之一的模式,它可以在应用程序中有各种用途,我们希望一个类在整个应用程序中只有一个实例。

单例模式强制一个类只能有一个实例,该实例将被应用程序中的任何组件/模块使用。当我们想要控制只使用一个对象来访问资源时,这种强制可以很有用。这种类型的资源可以是日志文件、数据库、崩溃处理机制等。

在大多数基于面向对象的语言中,要实现单例模式,第一步是将类构造函数设为私有,然后在类内部使用静态方法...

call 魔术方法

__call__魔术方法在 Python 元类的上下文中是特殊的。与__init__方法不同,__init__方法在我们从元类创建新类时被调用,而__call__方法在初始化类的对象时被调用。为了更好地理解这一点,让我们尝试运行以下示例:

class ExampleMeta(type):
  def __init__(cls, name, bases, dct):
    print("__init__ called")
    return super().__init__(name, bases, dct)
  def __call__(cls, *args, **kwargs):
    print("__call__ called")
    return super().__call__(*args, **kwargs)
class Example(metaclass=Example):
  def __init__(self):
    print("Example class")
__init__ called
>>> obj = Example()
__call__ called

从这个例子中,可以清楚地看出__init__方法是在解释器完成基于元类的类初始化后被调用的,而__call__方法是在创建类的对象时被调用的。

现在,有了这个理解,让我们构建我们的数据库连接类,它将提供我们的数据库操作的支持。在这个例子中,我们只关注类的初始化部分,而将在后面的章节中提供完整的类实现细节。

现在,在bugzot目录下,让我们创建一个名为database.py的文件,其中将保存我们的数据库类:

from bugzot.meta import Singleton

class Database(metaclass=Singleton):
  def __init__(self, hostname, port, username, password, dbname, **kwargs):
    """Initialize the databases
    Initializes the database class, establishing a connection with the database and providing
    the functionality to call the database.
    :params hostname: The hostname on which the database server runs
    :parms port: The port on which database is listening
    :params username: The username to connect to database
    :params password: The password to connect to the database
    :params dbname: The name of the database to connect to
    """ 
    self.uri = build_uri(hostname, port, username, password, dbname)
    #self.db = connect_db()
    self.db_opts = kwargs
    #self.set_db_opts()

  def connect_db(self):
    """Establish a connection with the database."""
    pass
  def set_db_opts(self):
    """Setup the database connection options."""
    pass

在这个例子中,我们定义了一个数据库类,它将帮助我们建立与数据库的连接。这个类的不同之处在于,无论我们尝试创建这个类的新实例时,它总是返回相同的对象。例如,让我们看看如果我们创建这个相同类的两个不同对象会发生什么:

dbobj1 = Database("example.com", 5432, "joe", "changeme", "testdb")
dbobj2 = Database("example.com", 5432, "joe", "changeme", "testdb")
>>> dbobj1
<__main__.Database object at 0x7fb6d754a7b8>
>>> dbobj2
<__main__.Database object at 0x7fb6d754a7b8>

在这个例子中,我们可以看到,当我们尝试实例化该类的新对象时,返回的是数据库对象的相同实例。

现在,让我们来看看另一个有趣的模式,即工厂模式。

工厂模式

在开发大型应用程序时,有些情况下我们可能希望根据用户输入或其他动态因素动态初始化一个类。为了实现这一点,我们可以在类实例化期间初始化所有可能的对象,并根据环境输入返回所需的对象,或者可以完全推迟类对象的创建,直到收到输入为止。

工厂模式是后一种情况的解决方案,其中我们在类内部开发一个特殊的方法,负责根据环境输入动态初始化对象。

现在,让我们看看如何在 Python 中实现工厂模式...

模型-视图-控制器模式

让我们从一个图表开始讨论 MVC 模式:

该图表显示了使用 MVC 模式的应用程序中请求的流程。当用户发出新的请求时,应用程序拦截请求,然后将请求转发给适当的控制器处理该请求。一旦控制器接收到请求,它将与模型交互,根据其收到的请求执行一些业务逻辑。这可能涉及更新数据库或获取一些数据。一旦模型执行了业务逻辑,控制器执行视图并传递给视图需要显示请求的任何数据。

虽然我们将在本书的后面实现 MVC 模式,但在开发 BugZot 应用程序时,让我们来看看 MVC 模式中的不同组件以及它们扮演的角色。

控制器

控制器充当模型和视图之间的中介。当首次向应用程序发出请求时,控制器拦截请求,并根据此决定需要调用哪个模型和视图。一旦决定了这一点,控制器就执行模型来运行业务逻辑,从模型中检索数据。一旦检索到数据并且模型执行完成,控制器就执行视图,并使用从模型中收集的数据。一旦视图执行完成,用户就会看到视图的响应。

简而言之,控制器负责执行以下操作:

  • 拦截应用程序发出的请求,并执行所需的...

模型

模型是应用程序的业务逻辑所在的地方。许多时候,开发人员会将模型与数据库混淆,这对于一些 Web 应用程序可能是正确的,但在一般情况下并非如此。

模型的作用是处理数据,提供对数据的访问,并在请求时允许修改。这包括从数据库或文件系统检索数据,向其中添加新数据,并在需要更新时修改现有数据。

模型不关心存储的数据应该如何呈现给用户或应用程序的其他组件,因此将呈现逻辑与业务逻辑解耦。模型也不经常更改其模式,并且在应用程序生命周期中基本保持一致。

因此,简而言之,模型负责执行以下角色:

  • 提供访问应用程序中存储的数据的方法

  • 将呈现逻辑与业务逻辑解耦

  • 为存储在应用程序中的数据提供持久性

  • 提供一致的接口来处理数据

视图

视图负责向用户呈现数据,或通过向用户呈现界面来操作模型中存储的数据。MVC 中的视图通常是动态的,并根据模型中发生的更改频繁变化。视图也可以被认为仅包含应用程序的呈现逻辑,而不考虑应用程序将如何存储数据以及如何检索数据。通常,视图可以用于缓存呈现状态,以加速数据的显示。

因此,简而言之,以下是视图执行的功能:

  • 为应用程序提供呈现逻辑,以显示应用程序中存储的数据

  • 为用户提供...

摘要

在本章中,我们讨论了设计模式的概念以及它们如何帮助我们解决设计应用程序中常遇到的一些问题。然后,我们讨论了如何决定使用哪种设计模式,以及是否有必要选择已经定义的模式之一。在本章的进一步探讨中,我们探索了 Python 作为一种语言的一些面向对象的能力,并且还探讨了在 Python 中实现抽象类和元类的一些示例,以及我们如何使用它们来构建其他类并修改它们的行为。

在掌握了面向对象的 Python 知识后,我们继续在 Python 中实现一些常见的设计模式,如单例模式和工厂模式,并探索了 MVC 模式,了解它们试图解决的问题。

现在我们掌握了设计模式的知识,是时候了解如何使我们应用程序内部处理数据的过程更加高效了。下一章将带领我们探索不同的技术,帮助我们有效地处理应用程序内部将发生的数据库操作。

问题

  1. 我们如何在 Python 中实现责任链模式,以及它可以使用的一些可能用例是什么?

  2. __new__方法和__init__方法之间有什么区别?

  3. 我们如何使用 ABCMeta 类作为抽象类的元类来实现抽象类?

第三章:构建大规模数据库操作

在企业软件开发领域,开发人员一直在构建处理大量数据的应用程序。在计算机的早期,系统通常跨越比我们目前居住的房间还要大的空间,数据存储在平面文件格式中,而今天,系统已经缩小到以前存放单个系统的相同大小的房间中,我们现在可以运行成千上万个系统,每个系统都与其他系统协调,为我们提供可以以光速处理数据的机器。随着时间的推移,数据存储的方式也从使用平面文件发展到了复杂的数据库管理系统。

随着企业规模的增长和由新兴领域带来的不断扩大的业务,企业应用程序需要处理的数据量也在增长,这使得了解如何构建我们的应用程序以处理大规模数据库相关操作变得重要。虽然构建大规模数据库操作永远不可能是一种适合所有情况的解决方案,但我们将涵盖一些常见的构建应用程序的要点,这些应用程序可以轻松扩展以处理数据增加、模式修改的要求、应用程序复杂性的增加等。

尽管有多种类型的数据库,如 SQL、NoSQL 和图形数据库,可以用来存储应用程序数据,取决于企业所需的应用程序类型,本章重点关注使用 SQL 的关系数据库管理系统,因为它们非常流行,并且能够处理大量的用例。

通过本章结束时,您将学到以下内容:

  • 使用对象关系映射器ORMs)及其提供的好处

  • 为了提高效率和便于修改,构建数据库模型

  • 专注于维护数据库一致性

  • 急切加载和延迟加载之间的区别

  • 利用缓存加速查询

技术要求

本书中的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter03目录下找到

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

本章提供的代码示例需要您在系统上安装和配置以下系统包:

  • python-devel

  • PostgreSQL

  • Python - virtualenv

除了这三个包,您还需要sqlalchemy包,它提供了我们在整个章节中将使用的 ORM,以及psycopg2,它提供了postgres数据库绑定,以允许sqlalchemy ...

数据库和对象关系映射器

正如我们在前几章中讨论的,Python 为我们提供了许多面向对象的能力,并允许我们以类和对象的术语来映射我们的用例。现在,当我们可以将我们的问题集映射到一个类及其对象时,为什么我们不应该将我们的数据库表也映射为对象,其中一个特定的类代表一个表,它的对象代表表中的行。沿着这条路走,不仅有助于我们维护我们编写代码的一致性,还有助于我们建模我们的问题。

提供了通过它们可以将我们的数据库映射到对象的功能的框架被称为 ORMs,它们帮助我们将我们的数据库可视化为一组类和对象。

在 Python 领域中,看到 ORMs 是非常常见的。例如,流行的 Python Web 框架 Django 提供了自己的 ORM 解决方案。然后,还有 SQLAlchemy,它提供了一个完整的 ORM 解决方案和支持各种关系数据库的数据库工具包。

但是,要说服开发人员使用 ORM 框架,应该有比仅仅说它们能够将数据库映射到类和对象,并为您提供面向对象的接口来访问数据库更好的优势。让我们看看 ORM 的使用带来了哪些优势:

  • 抽象出特定供应商的 SQL:关系数据库领域充满了选择,有几家公司在推广他们的产品。这些产品中的每一个都可以在如何通过使用 SQL 实现某个功能上有所不同。有时,一些数据库可能实现了一些尚未在其他数据库中支持的 SQL 关键字。对于开发人员来说,如果他们需要支持具有不连贯功能集的多个数据库,这可能会成为一个问题。由于 ORM 已经知道如何处理这些数据库的差异,它们帮助开发人员减轻了支持多个数据库的问题。大多数情况下,使用 ORM 时,开发人员所需要做的就是修改数据库连接的统一资源标识符(URI),然后他们就可以准备在应用程序中使用新的数据库了。

  • 减少重复 SQL 的需求:在编写应用程序时,有很多地方需要使用类似的查询从相同的表中检索数据。这将导致很多重复的 SQL 代码被写入很多地方,不仅导致很多格式不佳的代码,还会因为 SQL 查询构造不当而导致错误的出现(人类在做重复工作时很容易失去注意力,开发人员也会这样吗?)。ORM 解决方案通过提供对 SQL 命令的抽象和根据我们调用不同方法动态生成 SQL 来减少编写 SQL 以实现相同结果的需求。

  • 增加应用程序的可维护性:由于 ORM 允许您一次定义数据库模型并通过实例化类在整个应用程序中重用它,它允许您在一个地方进行更改,然后在整个应用程序中反映出来。这使得维护应用程序的任务变得稍微不那么繁琐(至少与处理数据库相关的部分)。

  • 提高生产力:这本身不是一个特性,而是前面提到的点的副作用。使用 ORM 解决方案,开发人员现在不再那么担心始终考虑 SQL 查询,或者试图遵循特定的设计模式。他们现在可以专注于如何最好地设计他们的应用程序。这显著提高了开发人员的生产力,并允许他们完成更多工作并提高时间的利用率。

在这一章中,我们将专注于如何利用 ORM 来最好地开发我们的企业应用程序,以便它们可以轻松地与数据库交互并高效地处理大规模的数据库操作。为了保持本章简单,我们将坚持使用 SQLAlchemy,它将自己作为一个 SQL 工具包,并为 Python 提供了一个 ORM 解决方案,并为 Python 领域中的不同框架提供了许多绑定。它被一些相当大规模的项目使用,如 OpenStack,Fedora 项目和 Reddit。

设置 SQLAlchemy

在我们深入研究如何为应用程序创建最佳的数据库模型以促进高效的大规模数据库操作之前,我们首先需要设置我们的 ORM 解决方案。由于我们将在这里使用 SQLAlchemy,让我们看看如何在开发环境中设置它。

为了使 SQLAlchemy 工作,你应该有一个数据库管理系统设置,可以是在你的系统上或远程机器上,你可以连接到它。一个暴露端口的容器也可以为我们完成工作。为了保持示例简单,我们假设读者在这里使用 PostgreSQL 作为他们的数据库解决方案,并且了解 PostgreSQL 设置的工作原理。现在,让我们看看如何设置 SQLAlchemy:

mkdir ch3 && cd ch3 ...

构建最佳数据库模型

实现对数据库的任何有效访问的第一步是为数据库构建一个最佳模型。如果一个模型不是最佳的,那么加速对数据库的访问的其他技术将几乎没有什么区别。

但在我们深入研究如何为数据库构建最佳模型之前,让我们首先看看如何实际使用 SQLAlchemy 为我们的数据库构建任何模型。

举个例子,假设我们想要构建一个模型来代表我们的 BugZot 应用程序中的用户。在我们的 BugZot 应用程序中,用户将需要提供以下字段:

  • 名字和姓氏

  • 用户名

  • 电子邮件地址

  • 密码

此外,我们的 BugZot 应用程序还需要维护有关用户的一些其他信息,例如他们在系统中的会员级别,用户有权利的特权,用户帐户是否处于活动状态,以及发送给用户激活他们帐户的激活密钥。

现在,让我们看看如果我们尝试使用 SQLAlchemy 来满足这些要求建立用户表会发生什么。以下代码描述了我们如何在 SQLAlchemy 中构建用户模型:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Boolean, Date, Integer, String, Column
from datetime import datetime

# Initialize the declarative base model
Base = declarative_base()

# Construct our User model
class User(Base):
 __tablename__ = 'users'

 id = Column(Integer, primary_key=True, autoincrement=True)
 first_name = Column(String, nullable=False)
 last_name = Column(String, nullable=False)
 username = Column(String(length=25), unique=True, nullable=False)
 email = Column(String(length=255), unique=True, nullable=False)
 password = Column(String(length=255), nullable=False)
 date_joined = Column(Date, default=datetime.now())
 user_role = Column(String, nullable=False)
 user_role_permissions = Column(Integer, nullable=False)
 account_active = Column(Boolean, default=False)
 activation_key = Column(String(length=32))

 def __repr__(self):
 return "<User {}>".format(self.username)

这个例子展示了我们如何使用 SQLAlchemy 构建模型。现在,让我们看看我们在代码示例中做了什么。

在代码示例的开始部分,我们首先导入了declarative_base方法,该方法负责为我们的模型提供基类。

Base = declarative_base()行将基本模型分配给我们的基本变量。

接下来我们做的事情是包括来自 SQLAlchemy 的不同数据类型,这些数据类型将在我们的模型定义中使用。

最后的导入导入了我们将在数据库模型中使用的 Python datetime库。

现在,不考虑我们的代码将如何填充数据库模型的不同字段,让我们看看我们是如何设计我们的用户模型的。

设计模型的第一步是定义一个作为我们模型类的用户类。这个类派生自我们在代码中之前初始化的基本模型。

__tablename__ = 'users'行定义了当这个数据库模型在数据库中实现时应该给表的名称。

接着,我们开始定义表将包含的列。为了定义列,我们使用key=value的方式,其中 key 定义了列的名称,value 定义了列的属性。

例如,要定义 id 列,它应该是整数类型,并且应该作为用户表的主键,我们这样定义:

id = Column(Integer, primary_key=True, autoincrement=True)

我们现在可以看到它是多么简单。我们不需要编写任何 SQL 来定义我们的列。同样,通过只传递unique=Truenullable=False参数给列构造函数,就可以很容易地强制一个特定字段应该具有唯一值,并且不能有 null 值,可以从以下行作为例子:

username = Column(String(length=25), unique=True, nullable=False)

在我们定义了所有的列之后,我们提供了__repr__方法的定义。__repr__方法是一个魔术方法,它由内部的repr()Python 方法调用,以提供对象的表示,比如当用户发出print(userobj)时。

这样就完成了我们使用 SQLAlchemy 定义用户模型的定义。很简单,不是吗?我们不需要编写任何 SQL;我们只需快速地将列添加到一个类中,然后让 SQLAlchemy 处理其他所有事情。现在,虽然所有这些都很有趣且容易实现,但我们犯了一些错误,现在似乎没有造成任何伤害,但随着我们的应用规模扩大,这些错误将会变得代价高昂。让我们来看看这些错误。

我们模型定义的问题

虽然 SQLAlchemy 为我们提供了很多抽象来轻松定义用户模型,但它也让我们很容易犯一些错误,一旦应用规模扩大并且企业增长,这些错误就会变得代价高昂。让我们来看看我们在定义这个模型时犯了一些错误:

  • 易受变化影响:我们当前的用户模型定义使得一旦应用规模扩大,对模型进行更改变得非常困难。让我们举个例子,假设组织决定在错误报告上为用户提供更多权限。在 SQL 方面,为了实现这个效果,我们需要编写一个查询,遍历所有记录并具有user_role作为用户...

优化我们的模型

在讨论如何构建最佳模型之前,我们首先需要了解最佳模型应具备的特征。让我们来看看以下内容:

  • 易于调整:一个优化的模型应该根据应用程序不断增长的需求变化而容易调整。这意味着更改特定模型不应该需要在整个应用程序中进行更改,并且应该具有高内聚性。

  • 最大化主机吞吐量:每个主机都有不同的架构,数据模型应该能够利用底层主机资源,以最大化吞吐量。这可以通过使用特定架构和用例的正确数据存储引擎,或者在一组机器上运行数据库以增加并行执行能力来实现。

  • 高效存储:数据库模型还应考虑到随着存储在其中的数据增长,可能使用的存储空间。这可以通过仔细选择数据类型来实现。例如,仅表示一个只能有两个值(true 或 false)的列,使用整数类型会浪费大量磁盘空间,随着数据库中记录的数量增加。对于这样的列,名义数据类型可以是布尔型,它在内部不占用太多空间。

  • 易于调整:一个高效的模型将谨慎地为可以加速对特定表的查询处理的列建立索引。这将导致数据库的响应时间得到改善,并且用户不会因为应用程序从数据库返回 10,000 条记录需要长达 20 分钟而感到沮丧。

为了实现这些目标,我们现在需要简化我们的模型,并使用关系数据库提供的关系概念。现在让我们开始重构我们的用户模型,使其更加优化。

为了实现这一点,首先我们需要将一个大模型分解为多个小模型,在我们的代码库中独立存在,并且不要将所有东西耦合得太紧。让我们开始吧。

我们要移出模型的第一件事是如何处理角色和权限。由于角色及其权限不会在用户之间有太大的差异(肯定不是每个用户都会有一个唯一的角色,也不是每个角色都可以有不同的权限集),我们可以将这些字段移动到另一个模型,称为权限。以下代码说明了这一点:

class Role(Base):
 __tablename__ = 'roles'

 id = Column(Integer, primary_key=True, autoincrement=True)
 role_name = Column(String(length=25), nullable=False, unique=True)
 role_permissions = Column(Integer, nullable=False)

 def __repr__(self):
 return "<Role {}>".format(role_name)

现在,我们已经将角色与用户模型解耦。这使我们可以轻松地对提供的角色进行修改,而不会引起太多问题。这些修改可能包括重命名角色或更改现有角色的权限。我们只需要在一个地方进行修改,就可以反映到所有具有相同角色的用户身上。让我们看看如何在我们的用户模型中利用关系数据库管理系统RDBMS)中的关系来做到这一点。

以下代码示例显示了如何实现角色模型和用户模型之间的关系:

class User(Base):
  __tablename__ = 'users'

  id = Column(Integer, primary_key=True, autoincrement=True)
  first_name = Column(String, nullable=False)
  last_name = Column(String, nullable=False)
  username = Column(String(length=25), unique=True, nullable=False)
  email = Column(String(length=255), unique=True, nullable=False)
  password = Column(String(length=255), nullable=False)
  date_joined = Column(Date, default=datetime.now())
  user_role = Column(Integer, ForeignKey("roles.id"))
  account_active = Column(Boolean, default=False)
  activation_key = Column(String(length=32))

  def __repr__(self):
    return "<User {}>".format(self.username) 

在这个代码示例中,我们将user_role修改为整数,并存储在roles模型中存在的值。任何尝试向这个字段插入不在 roles 模型中的值的操作都会引发 SQL 异常,表示不允许该操作。

现在,继续使用同一个例子,让我们考虑用户模型的activation_key列。一旦用户激活了他们的账户,我们可能就不再需要激活密钥。这为我们提供了在用户模型中进行一次优化的机会。我们可以将这个激活密钥从用户模型中移出,并存储在一个单独的模型中。一旦用户成功激活了他们的账户,记录就可以被安全地删除,而不会有用户模型被修改的风险。因此,让我们开发激活密钥的模型。以下代码示例说明了我们想要做的事情:

class ActivationKey(Base):
  __tablename__ = 'activation_keys'

  id = Column(Integer, primary_key=True, autoincrement=True)
  user_id = Column(Integer, ForeignKey("users.id"))
  activation_key = Column(String(length=32), nullable=False)

  def __repr__(self):
    return "<ActivationKey {}>".format(self.id)

在这个例子中,我们实现了ActivationKey模型。由于每个激活密钥都属于唯一的用户,我们需要存储哪个用户拥有哪个激活密钥。我们通过向用户模型的id字段引入外键来实现这一点。

现在,我们可以安全地从用户模型中移除activation_key列,而不会引起任何麻烦。

利用索引

索引是一种可以在适合建立索引的字段上提供大量性能优势的东西。但是,如果被索引的列没有经过慎重选择,索引也可能毫无用处,甚至会损害数据库的性能。例如,在表中索引每一列可能不会带来任何优势,而且会不必要地占用磁盘空间,同时使数据库操作变慢。

因此,在我们以一个例子来介绍的 ORM 中如何对特定字段建立索引之前,让我们首先澄清在数据库上下文中索引到底是什么(不深入探讨它们的工作原理),这个数据结构...

保持数据库一致性

数据库通常在应用程序部署后的整个生命周期中并行进行大量操作。这些操作可以是从数据库中检索信息,也可以是修改数据库状态的操作,比如插入新记录、更新现有记录或删除其他记录。目前大型组织生产中使用的大多数数据库都具有相当多的弹性,可以处理环境中可能发生的错误和崩溃,以防止数据损坏和停机。

但这并不能完全解除应用程序开发人员对数据库内数据一致性的关注。让我们试着理解这种情况。

在企业级应用程序中,任何给定时间点都会有许多数据库查询并行运行。这些查询来自于许多用户使用的应用程序或内部应用程序维护作业。其中一个主要的事实是,并非所有的查询都能成功执行。这可能是由于多种原因,比如查询中的数据不符合模式,为列值提供了不正确的数据类型,以及违反约束。当这种情况发生时,数据库引擎会阻止查询执行并返回查询错误。这是完全可以接受的,因为我们不正确的查询没有对数据库进行任何不正确的更改。但是当这个查询是一系列操作的一部分,用于在数据库中创建一个新资源时,情况就变得棘手了。现在我们需要确保在失败的查询之前由其他查询所做的更改被恢复。

这种行为仍然可以通过应用程序的开发人员通过跟踪 SQL 查询并在事情变得混乱时手动恢复它们的更改来解决。

但是,如果数据库引擎由于执行查询时发生错误而崩溃。现在我们处于一个无法预测数据库状态的情况下,处理这种情况可能会变得非常繁琐,并且可能会成为一个长时间阻碍整个组织运营的任务,直到数据库一致性得到验证。那么,我们能做些什么?有没有办法可以防止这些问题的出现?答案是肯定的。让我们来看看。

利用事务来维护一致性

关系数据库中的事务为我们提供了解决刚才讨论的问题的能力。在关系数据库方面,事务可以被认为是由多个数据库查询组成的信封,这些查询要么作为一个任务执行,要么在任何一个失败时完全恢复。我们还可以将事务视为数据库操作的原子单位,在这里,即使一个失败也会恢复整个事务。但是,这难道不正是我们需要解决数据库一致性问题的吗?

现在,让我们看看我们的 ORM 解决方案如何帮助我们实现事务支持。

为了理解这一点,让我们举个例子。我们的 BugZot...

理解延迟加载与急切加载

当我们查询从数据库加载数据时,这个操作可能会定义我们构建的应用程序的响应时间。这主要发生在需要加载大量数据并且应用程序等待数据库将所有这些行和列返回给它时。

这样的操作可能需要一些时间,从几毫秒到超过 10 秒,这取决于从数据库查询多少数据。这里的问题是,我们能否优化这一点以改善我们应用程序的响应时间?

这个问题的答案在于使用 SQL 关系和 ORM 层加载技术。虽然关系可以帮助我们定义两个模型之间的关系,加载技术定义了 ORM 如何检索关系。当需要加载大量数据时,这可以证明是非常有帮助的,不仅提供了一个机制,通过这个机制我们可以推迟加载关系数据直到需要它们,而且还可以在应用程序的内存占用方面节省相当多的空间。所以,让我们来看看这些技术。

使用关系

有了关系数据库管理系统的支持,我们现在可以定义两个模型之间的关系。数据库支持对两个模型之间不同类型的关系进行建模,例如:

  • 一对一关系:这是一种关系,其中一个模型的记录只与另一个模型的一个记录相关联。例如,我们的用户模型中的用户只有一个激活密钥与我们的 ActivationKey 模型相关联。这是一种一对一关系。

  • 一对多关系:这是一种关系,其中一个模型的记录映射到另一个模型的多个记录。例如,如果我们有一个描述 bug 条目的 Bug 模型,那么我们可以说,一个用户…

延迟加载

许多 ORM 层以及 SQLAlchemy 都试图尽可能地延迟数据加载。通常情况下,只有在应用程序实际访问对象时才会加载数据。这种延迟加载数据直到尝试访问数据的技术被称为延迟加载。

这种技术对于减少应用程序的响应时间非常有帮助,因为整个数据不是一次性加载的,而是按需加载的。这种优化是以运行更多的 SQL 查询为代价的,这些查询将在请求时检索实际数据。但是有没有一种方法可以明确控制这种技术呢?

对于每个 ORM 解决方案,答案都会有所不同,但其中很多实际上允许您启用或禁用延迟加载行为。那么,在 SQLAlchemy 中如何控制这一点呢?

看一下我们在上一节中对用户模型的修改,我们可以通过在我们的角色字段中添加一个额外的属性来明确告诉 SQLAlchemy 从我们的角色模型中延迟加载数据,如下面的片段所示:

role = relationship("Role", lazy_load='select')

这个额外的lazy_load属性定义了 SQLAlchemy 用来从我们的角色模型加载数据的技术。下面的例子展示了在延迟加载期间请求的流程:

>>> Session = sessionmaker(bind=engine)
>>> db_session = Session()
>>> user_record = db_session.query(User).first()
INFO sqlalchemy.engine.base.Engine SELECT users.username AS users_username, users.id AS users_id, users.role_id AS users_role_id 
FROM users 
 LIMIT %(param_1)s
INFO sqlalchemy.engine.base.Engine {'param_1': 1}
>>> role = user_record.role
INFO sqlalchemy.engine.base.Engine SELECT roles.id AS roles_id, roles.role_name AS roles_role_name, roles.role_permissions AS roles_role_permissions 
FROM roles 
WHERE roles.id = %(param_1)s
INFO sqlalchemy.engine.base.Engine {'param_1': 1}

从这个例子中可以看出,SQLAlchemy 在我们尝试访问角色模型的数据之前并不尝试加载角色模型的数据。一旦我们尝试访问角色模型的数据,SQLAlchemy 就会向数据库发出SELECT查询,获取结果并返回填充的对象,然后我们现在可以使用它。

与按需加载数据的技术相反,我们也可以要求 SQLAlchemy 在第一次请求时加载所有数据。这可以节省我们等待应用程序等待 ORM 层按需从数据库获取数据的几毫秒时间。

这种技术被称为急切加载,我们将在接下来的部分中解释。

急切加载

有时我们希望加载我们想要的对象的数据以及我们的对象映射到的关系的数据。这是一个有效的用例,比如当开发人员确信他们将访问关系的数据时,无论情况如何。

在这些用例中,没有必要浪费时间,而 ORM 层会按需加载关系。这种加载对象数据以及与我们的主对象相关的关联对象的数据的技术被称为急切加载。

SQLAlchemy 提供了一种简单的方法来实现这种行为。还记得我们在上一节中指定的lazy_load属性吗?是的,这就是你需要从延迟加载行为切换到急切加载的全部内容…

优化数据加载

我们可以为应用程序的性能提供的一种提升是优化它从数据库加载数据的方式。这并不是一件复杂的事情,ORM 解决方案使得这一切变得更加简单。

优化数据加载只有几条规则。因此,让我们看看这些规则是什么,以及它们如何能够证明有利:

  • 推迟加载可以跳过的数据:当我们知道我们不需要从数据库中获取的所有数据时,我们可以安全地推迟加载该数据,利用延迟加载技术。例如,如果我们想要向我们的 BugZot 应用程序的所有用户发送邮件,这些用户有超过 10 个未解决的 bug,并且不是管理员,我们可以推迟加载角色的关系。考虑到一个有很多用户的大型数据库,这可以帮助显著减少应用程序的响应时间,以及整体内存占用,而只需付出一些额外的查询,这可能是一个可取的权衡。

  • 如果数据将被使用,则尽早加载:与第一点完全相反,如果我们知道应用程序将使用数据,无论情况如何,那么一次性加载它而不是发出额外的查询来按需加载数据是完全有道理的。例如,如果我们想要将所有管理员提升为超级管理员,我们知道我们将访问所有用户的角色字段。那么,让应用程序懒加载角色字段就没有意义。我们可以简单地要求应用程序急切地加载所需的数据,以便应用程序不必等待数据按需加载。这种优化会增加内存使用量和初始响应时间,但一旦所有数据加载完毕,就会提供快速执行的优势。

  • 不加载不需要的数据:有时对象映射的一些关系在处理过程中根本不需要。在这种情况下,我们可以通过简单地设置lazy_load='noload'来节省大量内存和时间,从而根本不加载这些关系对象。在 SQLAlchemy 中可以很容易地实现这一点。一个这样的用例是当我们只想要更新数据库中用户的last_active时间时,不需要加载关系。在这种情况下,我们知道我们不需要验证与用户角色相关的任何内容,因此我们可以完全跳过加载角色。

如果加载技术完全嵌入在模型定义中,显然无法实现这些效果。因此,SQLAlchemy 确实提供了另一种通过使用不同方法来实现这些效果的方式,这些方法根据它们从数据库加载数据的技术命名,例如,lazyload()用于延迟加载,joinedload()用于连接急切加载,subqueryload()用于子查询急切加载,noload()用于不加载,我们将在后面的章节中解释它们,包括它们如何在实际应用程序中使用。

现在我们熟悉了加载技术以及如何利用它们的优势,现在让我们来看看本章的最后一个主题之一,我们将看到如何利用缓存来加快应用程序的响应时间,以及节省一遍又一遍地查询数据库的工作,这在应用程序执行大量数据密集型操作时确实会帮助我们。

利用缓存

在大多数企业应用程序中,一旦访问过的数据就会被再次使用。这可能是在不同的请求中,也可能是因为请求正在操作相同的数据集。

在这些情况下,如果我们试图一遍又一遍地从数据库中再次访问相同的数据,这将是一种巨大的资源浪费,导致应用程序向数据库发出大量查询,导致数据库负载高,响应时间差。

我们使用的 ORM 层提供了一定程度的缓存以访问过的数据,但是,大部分控制权仍然掌握在应用程序开发人员手中,他可以通过分析哪些数据将一遍又一遍地使用来使应用程序性能良好。

在数据库级别进行缓存

数据库是相当复杂的软件。它们不仅能够高效地存储我们的数据,还能够以同样的效率提供检索数据的机制。这背后涉及了许多复杂的逻辑。

使用 ORM 的优势之一是数据库可以在查询级别执行缓存。由于数据库应该以最快的方式返回数据,数据库系统通常会缓存反复执行的查询。这种缓存发生在查询解析级别,因此当在数据库上执行相同的查询时,可以通过不再解析相同的查询来节省一些时间。

这种缓存可以提高响应时间,因为保存了大量解析查询的工作。

块级缓存

现在,让我们来看一下我们可以在应用程序级别使用的缓存类型,这可能会提供重要帮助。

要理解应用程序块级缓存的概念,让我们看一下以下简单的代码片段:

for name in ['super_admin', 'admin', 'user']:  if db_session.query(User).first().role.role_name == name:    print("True")

从我们可以假设的情况来看,这可能已经查询了一次,然后从数据库中检索了数据,然后将一遍又一遍地使用它来与名称变量进行比较。但让我们来看一下前面代码的输出:

INFO sqlalchemy.engine.base.Engine SELECT users.username AS users_username, users.id AS users_id, users.role_id AS ...

使用用户级缓存

用户级缓存是另一种可以证明非常有用的缓存级别。想象一下,每次用户从一个页面移动到另一个页面时都从数据库查询用户的个人详细信息。这不仅效率低下,而且在高负载情况下会受到惩罚,当数据库的响应时间非常高时,请求可能会超时,用户将无法登录到应用程序,直到整体负载减少。

那么,有什么可以在这里帮助的吗?

答案是用户级缓存。当我们知道某些数据是特定于用户且对安全性不重要时,我们可以简单地从数据库中加载一次并将其保存在用户端。这可以通过实现 cookie 或在客户端创建临时文件来实现。这些 cookie 或临时文件存储有关用户的非机密数据,例如用户 ID 或用户名,或其他不重要的数据,例如用户的姓名。

每当应用程序想要加载这些数据时,它首先检查用户是否在其端有这些数据可用。如果找到数据,则从那里加载数据。如果在用户端找不到数据,则向数据库发出请求,然后从那里加载数据,最后在客户端缓存。

这种技术在试图减少特定于用户的数据加载的影响时非常有帮助,并且不需要经常从数据库刷新。

通过使用键值缓存机制,还有更复杂的缓存数据的技术,我们将在后面的章节中看到,比如使用诸如 memcached 之类的工具来实现内存缓存,这在处理大量数据时可能会非常有帮助。然而,由于涉及的主题复杂性可能涵盖数百页,这超出了本书的范围。

总结

在本章中,我们学习了如何构建数据库模型,以帮助我们在处理大规模数据时使应用程序性能更高。我们看到优化模型可以是优化的第一阶段,它可以帮助我们使应用程序更易于维护,通过减少数据库模型之间的耦合。然后,我们继续讨论索引如何有助于通过对更频繁访问的列进行索引来加快访问数据库内部数据。

后来,我们讨论了通过使用事务来维护数据库一致性的重要方面之一。

本章的最后部分涵盖了数据加载技术,如延迟加载、急切加载和无加载,…

问题

  1. 数据库表规范化的好处是什么?

  2. 通过select和通过joined进行延迟加载有什么区别?

  3. 在运行数据库更新查询时,我们如何保持数据的完整性?

  4. 从数据库缓存数据的不同级别是什么?

第四章:处理并发

正如我们在上一章中看到的,当处理任何大型企业应用程序时,我们会处理大量数据。这些数据以同步方式处理,并且只有在特定进程的数据处理完成后才发送结果。当处理的数据不大时,这种模型是完全可以接受的。但是考虑一种情况,需要在生成响应之前处理大量数据。那么会发生什么?答案是,应用程序响应时间变慢。

我们需要一个更好的解决方案。一种允许我们并行处理数据,从而获得更快应用程序响应的解决方案。但是我们如何实现这一点呢?问题的答案是并发...

技术要求

本书中的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter04目录下找到。

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

本章中提到的代码示例需要运行 Python 3.6 及以上版本。虚拟环境是将依赖项与系统隔离的首选选项。

并发的需求

大多数情况下,当我们构建相当简单的应用程序时,我们不需要并发。简单的顺序编程就可以很好地工作,一个步骤在另一个步骤完成后执行。但随着应用程序用例变得越来越复杂,并且有越来越多的任务可以轻松地推入后台以改善应用程序的用户体验,我们最终围绕并发的概念展开。

并发本身就是一个不同的东西,并且使编程任务变得更加复杂。但是,尽管增加了复杂性,但并发也带来了许多功能,以改善应用程序的用户体验。

在我们深入讨论为什么我们...

GUI 应用程序中的并发

我们已经习惯使用的硬件每年都变得越来越强大。如今,即使是我们智能手机内部的 CPU 也具有四核或八核的配置。这些配置允许并行运行多个进程或线程。不利用并发的硬件改进将是对之前提到的硬件改进的浪费。如今,当我们在智能手机上打开应用程序时,大多数应用程序都有两个或更多个线程在运行,尽管我们大部分时间都不知道。

让我们考虑一个相当简单的例子,在我们的设备上打开一个照片库应用程序。当我们打开照片库时,一个应用程序进程就会启动。这个进程负责加载应用程序的 GUI。GUI 在主线程中运行,允许我们与应用程序进行交互。现在,这个应用程序还会生成另一个后台线程,负责遍历操作系统的文件系统并加载照片的缩略图。从文件系统加载缩略图可能是一个繁琐的任务,并且可能需要一些时间,这取决于需要加载多少缩略图。

尽管我们注意到缩略图正在慢慢加载,但在整个过程中,我们的应用程序 GUI 仍然保持响应,并且我们可以与之交互,查看进度等。所有这些都是通过并发编程实现的。

想象一下,如果这里没有使用并发。应用程序将在主线程中加载缩略图。这将导致 GUI 在主线程完成加载缩略图之前变得无响应。这不仅会非常不直观,而且还会导致糟糕的用户体验,而我们通过并发编程避免了这种情况。

现在我们已经对并发编程如何证明其巨大用处有了一个大致的了解,让我们看看它如何帮助我们设计和开发企业应用程序,以及它可以实现什么。

企业应用程序中的并发

企业应用程序通常很大,通常涉及大量用户发起的操作,如数据检索、更新等。现在,让我们以我们的 BugZot 应用程序为例,用户可能会在提交错误报告时附上图形附件。这在提交可能影响应用程序 UI 或在 UI 上显示错误的错误时是一个常见的过程。现在,每个用户可能会提交图像,这些图像可能在质量上有所不同,因此它们的大小可能会有所不同。这可能涉及到非常小的尺寸的图像和尺寸非常大且分辨率很高的图像。作为应用程序开发人员,您可能知道以 100%质量存储图像可能会...

使用 Python 进行并发编程

Python 提供了多种实现并行或并发的方法。所有这些方法都有各自的优缺点,在实现方式上有根本的不同,需要根据使用情况做出选择。

Python 提供的实现并发的方法之一是在线程级别上进行,允许应用程序启动多个线程,每个线程执行一个任务。这些线程提供了一种易于使用的并发机制,并在单个 Python 解释器进程内执行,因此非常轻量级。

另一种实现并行的机制是通过使用多个进程代替多个线程。通过这种方法,每个进程在其自己的独立 Python 解释器进程内执行一个单独的任务。这种方法为多线程 Python 程序在全局解释器锁GIL)存在的情况下可能面临的问题提供了一些解决方法,但也可能增加管理多个进程和增加内存使用量的额外开销。

因此,让我们首先看看如何使用线程实现并发,并讨论它们所附带的好处和缺点。

多线程的并发

在大多数现代处理器系统中,多线程的使用是司空见惯的。随着 CPU 配备多个核心和诸如超线程等技术的出现,允许单个核心同时运行多个线程,应用程序开发人员不会浪费任何一个利用这些技术提供的优势的机会。

作为一种编程语言,Python 通过使用线程模块支持多线程的实现,允许开发人员在应用程序中利用线程级别的并行性。

以下示例展示了如何使用 Python 中的线程模块构建一个简单的程序:

# simple_multithreading.pyimport threadingclass SimpleThread(threading.Thread): ...

线程同步

正如我们在前一节中探讨的,虽然在 Python 中可以很容易地实现线程,但它们也有自己的陷阱,需要在编写面向生产用例的应用程序时予以注意。如果在应用程序开发时不注意这些陷阱,它们将产生难以调试的行为,这是并发程序所以闻名的。

因此,让我们试着找出如何解决前一节讨论的问题。如果我们仔细思考,我们可以将问题归类为多个线程同步的问题。应用程序的最佳行为是同步对文件的写入,以便在任何给定时间只有一个线程能够写入文件。这将强制确保在已经执行的线程完成其写入之前,没有线程可以开始写入操作。

为了实现这种同步,我们可以利用锁的力量。锁提供了一种简单的实现同步的方法。例如,将要开始写操作的线程将首先获取锁。如果锁获取成功,线程就可以继续执行其写操作。现在,如果在中间发生上下文切换,并且另一个线程即将开始写操作,它将被阻塞,因为锁已经被获取。这将防止线程在已经运行的写操作之间写入数据。

在 Python 多线程中,我们可以通过使用threading.Lock类来实现锁。该类提供了两种方法来方便地获取和释放锁。当线程想要在执行操作之前获取锁时,会调用acquire()方法。一旦锁被获取,线程就会继续执行操作。一旦线程的操作完成,线程调用release()方法释放锁,以便其他可能正在等待它的线程可以获取锁。

让我们看看如何使用锁来同步我们的 JSON 到 YAML 转换器示例中的线程操作。以下代码示例展示了锁的使用:

import threading
import json
import yaml

class JSONConverter(threading.Thread):
        def __init__(self, json_file, yaml_file, lock):
                threading.Thread.__init__(self)
                self.json_file = json_file
                self.yaml_file = yaml_file
      self.lock = lock

        def run(self):
                print("Starting read for {}".format(self.json_file))
                self.json_reader = open(self.json_file, 'r')
                self.json = json.load(self.json_reader)
                self.json_reader.close()
                print("Read completed for {}".format(self.json_file))
                print("Writing {} to YAML".format(self.json_file))
      self.lock.acquire() # We acquire a lock before writing
                self.yaml_writer = open(self.yaml_file, 'a+')
                yaml.dump(self.json, self.yaml_writer)
                self.yaml_writer.close()
                self.lock.release() # Release the lock once our writes are done
                print("Conversion completed for {}".format(self.json_file))

files = ['file1.json', 'file2.json', 'file3.json']
write_lock = threading.Lock()
conversion_threads = []

for file in files:
        converter = JSONConverter(file, 'converted.yaml', write_lock)
        conversion_threads.append(converter)
        converter.start()

for cthread in conversion_threads:
        cthread.join()

print("Exiting")

在这个例子中,我们首先通过创建threading.Lock类的实例来创建一个lock变量。然后将这个实例传递给所有需要同步的线程。当一个线程需要进行写操作时,它首先通过获取锁来开始写操作。一旦这些写操作完成,线程释放锁,以便其他线程可以获取锁。

如果一个线程获取了锁但忘记释放它,程序可能会陷入死锁状态,因为没有其他线程能够继续。应该谨慎地确保一旦线程完成其操作,获取的锁就被释放,以避免死锁。

可重入锁

除了提供多线程的一般锁定机制的threading.Lock类之外,其中锁只能被获取一次直到释放,Python 还提供了另一种可能对实现递归操作的程序有用的锁定机制。这种锁,称为可重入锁,使用threading.RLock类实现,可以被递归函数使用。该类提供了与锁类提供的类似方法:acquire()release(),分别用于获取和释放已获取的锁。唯一的区别是当递归函数在调用堆栈中多次调用acquire()时发生。当相同的函数一遍又一遍地调用获取方法时,...

条件变量

让我们想象一下,不知何故,我们有一种方法可以告诉我们的Thread-1等待,直到Thread-2提供了一些数据可供使用。这正是条件变量允许我们做的。它们允许我们同步依赖于共享资源的两个线程。为了更好地理解这一点,让我们看一下以下代码示例,它创建了两个线程,一个用于输入电子邮件 ID,另一个负责发送电子邮件:

# condition_variable.py
import threading

class EmailQueue(threading.Thread):

    def __init__(self, email_queue, max_items, condition_var):
        threading.Thread.__init__(self)
        self.email_queue = email_queue
        self.max_items = max_items
        self.condition_var = condition_var
        self.email_recipients = []

    def add_recipient(self, email):
        self.email_recipients.append(email)

    def run(self):
        while True:
            self.condition_var.acquire()
            if len(self.email_queue) == self.max_items:
                print("E-mail queue is full. Entering wait state...")
                self.condition_var.wait()
                print("Received consume signal. Populating queue...")
            while len(self.email_queue) < self.max_items:
                if len(self.email_recipients) == 0:
                    break
                email = self.email_recipients.pop()
                self.email_queue.append(email)
                self.condition_var.notify()
            self.condition_var.release()

class EmailSender(threading.Thread):

    def __init__(self, email_queue, condition_var):
        threading.Thread.__init__(self)
        self.email_queue = email_queue
        self.condition_var = condition_var

    def run(self):
        while True:
            self.condition_var.acquire()
            if len(self.email_queue) == 0:
                print("E-mail queue is empty. Entering wait state...")
                self.condition_var.wait()
                print("E-mail queue populated. Resuming operations...")
            while len(self.email_queue) is not 0:
                email = self.email_queue.pop()
                print("Sending email to {}".format(email))
            self.condition_var.notify()
            self.condition_var.release()

queue = []
MAX_QUEUE_SIZE = 100
condition_var = threading.Condition()

email_queue = EmailQueue(queue, MAX_QUEUE_SIZE, condition_var)
email_sender = EmailSender(queue, condition_var)
email_queue.start()
email_sender.start()
email_queue.add_recipient("joe@example.com")

在这个代码示例中,我们定义了两个类,分别是EmailQueue,它扮演生产者的角色,并在电子邮件队列中填充需要发送电子邮件的电子邮件地址。然后还有另一个类EmailSender,它扮演消费者的角色,从电子邮件队列中获取电子邮件地址并发送邮件给它们。

现在,在EmailQueue__init__方法中,我们接收一个 Python 列表作为参数,这个列表将作为队列使用,一个定义列表最多应该容纳多少项的变量,以及一个条件变量。

接下来,我们有一个方法add_recipient,它将一个新的电子邮件 ID 附加到EmailQueue的内部数据结构中,以临时保存电子邮件地址,直到它们被添加到发送队列中。

现在,让我们进入run()方法,这里发生了真正的魔术。首先,我们启动一个无限循环,使线程始终处于运行模式。接下来,我们通过调用条件变量的acquire()方法来获取锁。我们这样做是为了防止线程在意外时间切换上下文时对我们的数据结构进行任何形式的破坏。

一旦我们获得了锁,我们就会检查我们的电子邮件队列是否已满。如果已满,我们会打印一条消息,并调用条件变量的wait()方法。对wait()方法的调用会释放条件变量获取的锁,并使线程进入阻塞状态。只有在条件变量上调用notify()方法时,这种阻塞状态才会结束。现在,当线程通过notify()接收到信号时,它会继续其操作,首先检查内部队列中是否有一些数据。如果它在内部队列中找到了一些数据,那么它会用这些数据填充电子邮件队列,并调用条件变量的notify()方法来通知EmailSender消费者线程。现在,让我们来看看EmailSender类。

在这里不需要逐行阅读,让我们把重点放在EmailSender类的run()方法上。由于这个线程需要始终运行,我们首先启动一个无限循环来做到这一点。然后,我们要做的下一件事是,在共享条件变量上获取锁。一旦我们获得了锁,我们现在可以操作共享的email_queue数据结构。因此,我们的消费者首先要做的事情是检查电子邮件队列是否为空。如果发现队列为空,我们的消费者将调用条件变量的wait()方法,有效地释放锁并进入阻塞状态,直到电子邮件队列中有一些数据为止。这会导致控制权转移到负责填充队列的EmailQueue类。

现在,一旦电子邮件队列中有一些电子邮件 ID,消费者将开始发送邮件。一旦队列耗尽,它通过调用条件变量的notify方法向EmailSender类发出信号。这将允许EmailSender继续其操作,填充电子邮件队列。

让我们看看当我们尝试执行前面的示例程序时会发生什么:

python condition_variable.py 
E-mail queue is empty. Entering wait state...
E-mail queue populated. Resuming operations...
Sending email to joe@example.com
E-mail queue is empty. Entering wait state...

通过这个例子,我们现在了解了在 Python 中如何使用条件变量来解决生产者-消费者问题。有了这些知识,现在让我们来看看在我们的应用程序中进行多线程时可能出现的一些问题。

多线程的常见陷阱

多线程提供了许多好处,但也伴随着一些陷阱。如果不加以避免,这些陷阱在应用程序投入生产时可能会带来痛苦的经历。这些陷阱通常会导致意外行为,可能只会偶尔发生,也可能在特定模块的每次执行时都会发生。这其中令人痛苦的是,当这些问题是由多个线程的执行引起时,很难调试这些问题,因为很难预测特定线程何时执行。因此,在开发阶段讨论这些常见陷阱发生的原因以及如何在开发阶段避免它们是值得的。

一些常见的原因是...

竞争条件

在多线程的上下文中,竞争条件是指两个或更多个线程尝试同时修改共享数据结构的情况,但由于线程的调度和执行方式,共享数据结构被修改成一种使其处于不一致状态的方式。

这个声明是否令人困惑?别担心,让我们通过一个例子来理解它:

考虑我们之前的 JSON 转 YAML 转换器问题的例子。现在,假设我们在将转换后的 YAML 输出写入文件时没有使用锁。现在假设我们有两个名为writer-1writer-2的线程,它们负责向共同的 YAML 文件写入。现在,想象一下,writer-1writer-2线程都开始了写入文件的操作,并且操作系统安排线程执行的方式是,writer-1开始写入文件。现在,当writer-1线程正在写入文件时,操作系统决定该线程完成了其时间配额,并将该线程与writer-2线程交换。现在,需要注意的一点是,当被交换时,writer-1线程尚未完成写入所有数据。现在,writer-2线程开始执行并完成了在 YAML 文件中的数据写入。在writer-2线程完成后,操作系统再次开始执行writer-1线程,它开始再次写入剩余的数据到 YAML 文件,然后完成。

现在,当我们打开 YAML 文件时,我们看到的是一个文件,其中包含了两个写入线程混合在一起的数据,因此,使我们的文件处于不一致的状态。writer-1writer-2线程之间发生的问题被称为竞争条件。

竞争条件属于非常难以调试的问题类别,因为线程执行的顺序取决于机器和操作系统。因此,在一个部署上可能出现的问题在另一个部署上可能不会出现。

那么,我们如何避免竞争条件?嗯,我们已经有了问题的答案,而且我们最近刚刚使用过它们。所以,让我们来看看一些可以预防竞争条件发生的方法:

  • 在关键区域使用锁:关键区域指的是代码中共享变量被线程修改的区域。为了防止竞争条件在关键区域发生,我们可以使用锁。锁本质上会导致除了持有锁的线程外,所有其他线程都会被阻塞。需要修改共享资源的所有其他线程只有在当前持有锁的线程释放锁时才能执行。可以使用的锁的类别包括互斥锁(一次只能由一个线程持有)、可重入锁(允许递归函数对同一共享资源进行多次锁定)和条件对象(可用于在生产者-消费者类型的环境中同步执行)。

  • 使用线程安全的数据结构:预防竞争条件的另一种方法是使用线程安全的数据结构。线程安全的数据结构是指能够自动管理多个线程对其所做修改并串行化其操作的数据结构。Python 提供的一个线程安全的共享数据结构是队列。当操作涉及多个线程时,可以轻松地使用队列。

现在,我们对竞争条件是什么,它是如何发生的,以及如何避免有了一个概念。有了这个想法,让我们来看看由于我们预防竞争条件而可能出现的其他问题之一。

死锁

死锁是指两个或更多个线程永远被阻塞,因为它们彼此依赖或者一个资源永远不会被释放。让我们通过一个简单的例子来理解死锁是如何发生的:

考虑我们之前的 JSON 转 YAML 转换器的例子。现在,假设我们在线程中使用了锁,这样当一个线程开始向文件写入时,它首先对文件进行互斥锁定。现在,在线程释放这个互斥锁之前,其他线程无法执行。

因此,让我们想象一下有两个线程writer-1writer-2,它们试图写入共同的输出文件。现在,当writer-1开始执行时,它首先在文件上获取锁并开始操作。...

GIL 的故事

如果有人告诉你,即使你创建了一个多线程程序,只有一个线程可以同时执行?这种情况在系统只包含一个一次只能执行一个线程的单核心时是真实的,多个运行线程的幻觉是由 CPU 频繁地在线程之间切换而产生的。

但这种情况在 Python 的一个实现中也是真实的。Python 的原始实现,也称为 CPython,包括一个全局互斥锁,也称为 GIL,它只允许一个线程同时执行 Python 字节码。这有效地限制了应用程序一次只能执行一个线程。

GIL 是在 CPython 中引入的,因为 CPython 解释器不是线程安全的。GIL 通过交换运行多个线程的属性来有效地解决了线程安全问题。

GIL 的存在在 Python 社区中一直是一个备受争议的话题,有很多提案旨在消除它,但由于各种原因,包括对单线程应用程序性能的影响、破坏对 GIL 存在依赖的功能的向后兼容性等,没有一个提案被纳入 Python 的生产版本。

那么,GIL 的存在对于你的多线程应用程序意味着什么呢?实际上,如果你的应用程序利用多线程来执行 I/O 工作负载,那么由于大部分 I/O 发生在 GIL 之外,你可能不会受到 GIL 的性能损失影响,因此多个线程可以被复用。只有当应用程序使用多个线程执行需要大量操作应用程序特定数据结构的 CPU 密集型任务时,GIL 的影响才会被感知到。由于所有数据结构操作都涉及 Python 字节码的执行,GIL 将通过不允许多个线程同时执行严重限制多线程应用程序的性能。

那么,GIL 引起的问题是否有解决方法?答案是肯定的,但应该采用哪种解决方案完全取决于应用程序的用例。以下选项可能有助于避免 GIL:

  • 切换 Python 实现:如果你的应用程序并不一定依赖于底层的 Python 实现,并且可以切换到另一个实现,那么有一些 Python 实现是没有 GIL 的。一些没有 GIL 的实现包括:Jython 和 IronPython,它们可以完全利用多处理器系统来执行多线程应用程序。

  • 利用多进程:Python 在构建考虑并发的程序时有很多选择。我们探讨了多线程,这是实现并发的选项之一,但受到 GIL 的限制。实现并发的另一个选项是使用 Python 的多进程能力,它允许启动多个进程并行执行任务。由于每个进程在自己的 Python 解释器实例中运行,因此 GIL 在这里不成问题,并允许充分利用多处理器系统。

了解了 GIL 对多线程应用程序的影响,现在让我们讨论多进程如何帮助你克服并发的限制。

多进程并发

Python 语言提供了一些非常简单的方法来实现应用程序的并发。我们在 Python 线程库中看到了这一点,对于 Python 的多进程能力也是如此。

如果您想要借助多进程在程序中构建并发,那么借助 Python 的多进程库和该库提供的 API,实现起来非常容易。

那么,当我们说我们将使用多进程来实现并发时,我们是什么意思呢?让我们试着回答这个问题。通常,当我们谈论并发时,有两种方法可以帮助我们实现它。其中一种方法是运行单个应用程序实例,并允许其使用多个线程。...

Python 多进程模块

Python 提供了一种简单的方法来实现多进程程序。这种实现的便利性得益于 Python 的多进程模块,该模块提供了重要的类,如 Process 类用于启动新进程;Queue 和 Pipe 类用于促进多个进程之间的通信;等等。

以下示例快速概述了如何使用 Python 的多进程库创建一个作为单独进程执行的 URL 加载器:

# url_loader.py
from multiprocessing import Process
import urllib.request

def load_url(url):
    url_handle = urllib.request.urlopen(url)
    url_data = url_handle.read()
    # The data returned by read() call is in the bytearray format. We need to
    # decode the data before we can print it.
    html_data = url_data.decode('utf-8')
    url_handle.close()
    print(html_data)

if __name__ == '__main__':
    url = 'http://www.w3c.org'
    loader_process = Process(target=load_url, args=(url,))
    print("Spawning a new process to load the url")
    loader_process.start()
    print("Waiting for the spawned process to exit")
    loader_process.join()
    print("Exiting…")

在这个例子中,我们使用 Python 的多进程库创建了一个简单的程序,它在后台加载一个 URL 并将其信息打印到 stdout。有趣的地方在于理解我们如何轻松地在程序中生成一个新的进程。所以,让我们来看看。为了实现多进程,我们首先从 Python 的多进程模块中导入 Process 类。下一步是创建一个函数,该函数以要加载的 URL 作为参数,然后使用 Python 的 urllib 模块加载该 URL。一旦 URL 加载完成,我们就将来自 URL 的数据打印到 stdout。

接下来,我们定义程序开始执行时运行的代码。在这里,我们首先定义了我们想要加载的 URL,并将其存储在 url 变量中。接下来的部分是我们通过创建 Process 类的对象在程序中引入多进程。对于这个对象,我们将目标参数提供为我们想要执行的函数。这类似于我们在使用 Python 线程库时已经习惯的目标方法。Process 构造函数的下一个参数是 args 参数,它接受在调用目标函数时需要传递给目标函数的参数。

要生成一个新的进程,我们调用 Process 对象的 start()方法。这将在一个新的进程中启动我们的目标函数并执行其操作。我们做的最后一件事是等待这个生成的进程退出,通过调用 Process 类的 join()方法。

这就是在 Python 中创建多进程应用程序的简单方法。

现在,我们知道如何在 Python 中创建多进程应用程序,但是如何在多个进程之间分配特定的任务呢?嗯,这很容易。以下代码示例修改了我们之前示例中的入口代码,以利用多进程模块中的 Pool 类的功能来实现这一点:

from multiprocessing import Pool
if __name__ == '__main__':
    url = ['http://www.w3c.org', 'http://www.microsoft.com', '[http://www.wikipedia.org', '[http://www.packt.com']
    with Pool(4) as loader_pool:
      loader_pool.map(load_url, url)

在这个例子中,我们使用多进程库中的 Pool 类创建了一个包含四个进程的进程池来执行我们的代码。然后使用 Pool 类的 map 方法,将输入数据映射到执行函数中的一个单独的进程中,以实现并发。

现在,我们有多个进程在处理我们的任务。但是,如果我们想让这些进程相互通信怎么办。例如,在之前的 URL 加载问题中,我们希望进程返回数据而不是在 stdout 上打印数据怎么办?答案在于使用管道,它为进程之间提供了双向通信的机制。

以下示例利用管道使 URL 加载器将从 URL 加载的数据发送回父进程:

# url_load_pipe.py
from multiprocessing import Process, Pipe
import urllib.request

def load_url(url, pipe):
    url_handle = urllib.request.urlopen(url)
    url_data = url_handle.read()
    # The data returned by read() call is in the bytearray format. We need to
    # decode the data before we can print it.
    html_data = url_data.decode('utf-8')
    url_handle.close()
    pipe.send(html_data)

if __name__ == '__main__':
    url = 'http://www.w3c.org'
    parent_pipe, child_pipe = Pipe()
    loader_process = Process(target=load_url, args=(url, child_pipe))
    print("Spawning a new process to load the url")
    loader_process.start()
    print("Waiting for the spawned process to exit")
    html_data = parent_pipe.recv()
    print(html_data)
    loader_process.join()
    print("Exiting…")

在这个例子中,我们使用管道为父进程和子进程提供双向通信机制。当我们在代码的__main__部分调用pipe构造函数时,构造函数返回一对连接对象。每个连接对象都包含一个send()和一个recv()方法,用于在两端之间进行通信。使用send()方法从child_pipe发送的数据可以通过parent_piperecv()方法读取,反之亦然。

如果两个进程同时从管道的同一端读取或写入数据,可能会导致管道中的数据损坏。尽管,如果进程使用两个不同的端口或两个不同的管道,这就不成问题了。只有可以通过 pickle 序列化的数据才能通过管道发送。这是 Python 多进程模块的一个限制。

同步进程

与同步线程的操作一样重要的是,多进程上下文中的操作同步也很重要。由于多个进程可能访问相同的共享资源,它们对共享资源的访问需要进行序列化。为了帮助实现这一点,我们在这里也有锁的支持。

以下示例展示了如何在多进程模块的上下文中使用锁来同步多个进程的操作,通过获取与 URL 相关联的 HTML 并将其写入一个共同的本地文件:

# url_loader_locks.pyfrom multiprocessing import Process, Lockimport urllib.requestdef load_url(url, lock): url_handle = urllib.request.urlopen(url) ...

总结

在这一章中,我们探讨了如何在 Python 应用程序中实现并发以及它的用途。在这个探索过程中,我们揭示了 Python 多线程模块的功能,以及如何使用它来生成多个线程来分配工作负载。然后,我们继续了解如何同步这些线程的操作,并了解了多线程应用程序可能出现的各种问题,如果不加以处理。然后,本章继续探讨了全局解释器锁(GIL)在某些 Python 实现中所施加的限制,以及它如何影响多线程工作负载。为了探索克服 GIL 所施加的限制的可能方法,我们继续了解了 Python 的多进程模块的使用,以及它如何帮助我们利用多处理器系统的全部潜力,通过使用多个进程而不是多个线程来实现并行处理。

问题

  1. Python 是通过哪些不同的方法实现并发应用程序的?

  2. 如果已经获得锁的线程突然终止会发生什么?

  3. 当应用程序接收到终止信号时,如何终止执行线程?

  4. 如何在多个进程之间共享状态?

  5. 有没有一种方法可以创建一个进程池,然后用于处理任务队列中的任务?

第五章:构建大规模请求处理

在企业环境中,随着用户数量的增长,同时尝试访问 Web 应用程序的用户数量也会增长是正常的。这给我们带来了一个有趣的问题,即如何扩展 Web 应用程序以处理大量用户的并发请求。

将 Web 应用程序扩展以处理大量用户是可以通过多种方式实现的任务,其中最简单的方式之一可以是增加更多基础设施并运行应用程序的更多实例。然而,尽管这种技术简单,但对应用程序可扩展性的经济影响很大,因为运行规模化应用程序的基础设施成本可能是巨大的。我们当然需要以这样的方式设计我们的应用程序,以便它能够轻松处理大量并发请求,而不需要频繁地扩展基础设施。

在前一章奠定的基础上,我们将看到如何应用这些技术来构建一个可扩展的应用程序,可以处理大量并发请求,同时学习一些其他技术,将帮助我们轻松地扩展应用程序。

在本章中,我们将看到以下技术来扩展我们的 Web 应用程序,以处理大规模的请求处理:

  • 在 Web 应用部署中利用反向代理

  • 使用线程池来扩展请求处理

  • 了解使用 Python AsyncIO 的单线程并发代码的概念

技术要求

本书中的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter05目录下找到。

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

为了成功执行代码示例,需要安装 python-virtualenv包。

容纳增加的并发问题

多年来,互联网存在的时间里,Web 应用架构师常常面临的最常见问题之一是如何处理不断增加的并发。随着越来越多的用户上线并使用 Web 应用程序,迫切需要扩展基础设施来管理所有这些请求。

即使对于我们的企业 Web 应用程序也是如此。尽管我们可以估计企业内可能有多少用户同时访问这些 Web 应用程序,但没有硬性规定适用于未来的时间。随着企业的发展,访问应用程序的客户数量也会增加,给基础设施增加更多压力,并增加扩展的需求。但是,在尝试扩展应用程序以适应不断增加的客户数量时,我们有哪些选择?让我们来看看。

多种扩展选项

技术世界提供了许多选项,以扩展应用程序以适应不断增长的用户群体;其中一些选项只是要求增加硬件资源,而其他选项则要求应用程序围绕处理内部的多个请求来构建。大多数情况下,扩展选项分为两大类,即垂直扩展和水平扩展:

让我们看看它们,找出它们的利弊:

  • 垂直扩展:垂直扩展的整个概念基于向现有资源添加更多资源的事实...

为可扩展性工程应用

在大多数企业项目在生产阶段通常使用一个框架或另一个框架来决定应用程序的服务方式的时候,仍然有必要深入了解如何在开发应用程序时保持应用程序的可扩展性。

在本节中,我们将看看不使用一些预先构建的框架,如何构建可扩展的应用程序的不同技术。在本节课程中,我们将看到如何使用线程/进程池来同时处理多个客户端,以及资源池化为什么是必要的,以及是什么阻止我们为处理每个其他传入请求启动单独的线程或进程。

但在我们深入探讨如何在应用程序开发中利用线程池或进程池之前,让我们首先看一下通过哪种简单的方式我们可以将传入请求的处理交给后台线程。

以下代码实现了一个简单的套接字服务器,首先接受传入的连接,然后将其交给后台线程进行读写,从而释放主线程以接受其他传入连接:

# simple_socket_thread.py
#!/usr/bin/python3
import socket
import threading

# Let's first create a TCP type Server for handling clients
class Server(object):
    """A simple TCP Server."""

    def __init__(self, hostname, port):
        """Server initializer

        Keyword arguments:
        hostname -- The hostname to use for the server
        port -- The port on which the server should bind
        """

        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.hostname = hostname
        self.port = port
        self.bind_connection()
        self.listen()

    def bind_connection(self):
        """Bind the server to the host."""

        self.server.bind((self.hostname, self.port))

    def listen(self):
        """Start listening for the incoming connections."""

        self.server.listen(10) # Queue a maximum of 10 clients
        # Enter the listening loop
        while True:
            client, client_addr = self.server.accept()
            print("Received a connection from %s" % str(client_addr))
            client_thread = threading.Thread(target=self.handle_client, args=(client,))
            client_thread.daemon = True
            client_thread.start()

    def handle_client(self, client):
        """Handle incoming client connection.

        Keyword arguments:
        client -- The client connection socket
        """

        print("Accepted a client connection")
        while True:
            buff = client.recv(1024).decode()
            if not buff:
                break
            print(buff)
        print("Client closed the connection")
        client.close() # We are done now, let's close the connection

if __name__ == '__main__':
    server = Server('localhost', 7000)

在这段代码中,我们实现了一个简单的Server类,它在机器上初始化了一个基于 TCP 的服务器,准备接受传入的连接。在不偏离太多的情况下,让我们试着专注于这段代码的重要方面,在这里我们在listen()方法下启动了服务器的监听循环。

listen()方法下,我们首先调用套接字的listen()方法,并告诉它最多可以排队 10 个尚未被接受的连接。一旦达到这个限制,服务器将拒绝任何进一步的客户端连接。接下来,我们开始一个无限循环,在循环中首先调用套接字的accept()方法。对accept()方法的调用将阻塞,直到客户端尝试建立连接。成功尝试后,accept()调用将返回客户端连接套接字和客户端地址。客户端连接套接字可用于与客户端执行 I/O 操作。

接下来发生的有趣部分是:一旦客户端连接被接受,我们就启动一个负责处理与客户端通信的守护线程,并将客户端连接套接字交给线程处理。这实质上使我们的主线程从处理客户端套接字的 I/O 中解放出来,因此,我们的主线程现在可以接受更多的客户端。这个过程对于连接到我们服务器的每个其他客户端都会继续进行。

到目前为止一切都很好;我们有了一个很好的方法来处理传入的客户端,我们的服务可以随着客户端数量的增加逐渐扩展。这是一个简单的解决方案,不是吗?嗯,在提出这个解决方案的过程中,显然我们忽略了一个主要缺陷。缺陷在于我们没有实现任何与应用程序可以启动多少个线程来处理传入客户端相关的控制。想象一下,如果一百万个客户端尝试连接到我们的服务器会发生什么?我们真的会同时运行一百万个线程吗?答案是否定的。

但为什么不可能呢?让我们来看看。

控制并发

在前面的例子中,我们遇到了一个问题,为什么我们不能有一百万个线程,每个线程处理一个单独的客户端?这应该为我们提供了大量的并发性和可扩展性。但是,有许多原因实际上阻止我们同时运行一百万个线程。让我们试着看看可能阻止我们无限扩展应用程序的原因:

  • 资源限制:服务器处理的每个客户端连接都不是免费的。随着每个新连接的客户端,我们都在消耗机器的一些资源。这些资源可能包括映射到套接字的文件描述符,用于保存信息的一些内存...

使用线程池处理传入的连接

正如我们在前一节中看到的,我们不需要无限数量的线程来处理传入的客户端。我们可以通过有限数量的线程来处理大量的客户端。但是,我们如何在我们的应用程序中实现这个线程池呢?事实证明,使用 Python 3 和concurrent.futures模块实现线程池功能是相当容易的。

以下代码示例修改了我们现有的 TCP 服务器示例,以使用线程池来处理传入的客户端连接,而不是任意启动无限数量的线程:

# simple_socket_threadpool.py
#!/usr/bin/python3
from concurrent.futures import ThreadPoolExecutor
import socket

# Let's first create a TCP type Server for handling clients
class Server(object):
    """A simple TCP Server."""

    def __init__(self, hostname, port, pool_size):
        """Server initializer

        Keyword arguments:
        hostname -- The hostname to use for the server
        port -- The port on which the server should bind
        pool_size -- The pool size to use for the threading executor
        """

        # Setup thread pool size
        self.executor_pool = ThreadPoolExecutor(max_workers=pool_size)

        # Setup the TCP socket server
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.hostname = hostname
        self.port = port
        self.bind_connection()
        self.listen()

    def bind_connection(self):
        """Bind the server to the host."""

        self.server.bind((self.hostname, self.port))

    def listen(self):
        """Start listening for the incoming connections."""

        self.server.listen(10) # Queue a maximum of 10 clients
        # Enter the listening loop
        while True:
            client, client_addr = self.server.accept()
            print("Received a connection from %s" % str(client_addr))
            self.executor_pool.submit(self.handle_client, client)

    def handle_client(self, client):
        """Handle incoming client connection.

        Keyword arguments:
        client -- The client connection socket
        """

        print("Accepted a client connection")
        while True:
            buff = client.recv(1024).decode()
            if not buff:
                break
            print(buff)
        print("Client closed the connection")
        client.close() # We are done now, let's close the connection

if __name__ == '__main__':
    server = Server('localhost', 7000, 20)

在这个例子中,我们修改了我们的 TCP 服务器代码,以利用线程池来处理客户端连接,而不是启动任意数量的线程。让我们看看我们是如何做到的。

首先,要使用线程池,我们需要初始化线程池执行器的实例。在Server类的__init__方法中,我们首先通过调用其构造函数来初始化线程池执行器:

self.executor_pool = ThreadPoolExecutor(max_workers=pool_size)

ThreadPoolExecutor构造函数接受一个max_workers参数,该参数定义了ThreadPool内可能有多少并发线程。但是,max_workers参数的最佳值是多少呢?

一个经验法则是将max_workers = (5 x CPU 核心总数)。这个公式背后的原因是,在 Web 应用程序中,大多数线程通常在等待 I/O 完成,而少数线程正在忙于执行 CPU 绑定的操作。

创建了ThreadPoolExecutor之后,下一步是提交作业,以便它们可以由执行器池内的线程处理。这可以通过Server类的listen() 方法来实现:

self.executor_pool.submit(self.handle_client, client)

ThreadPoolExecutorsubmit() 方法的第一个参数是要在线程内执行的方法的名称,第二个参数是要传递给执行方法的参数。

这样做非常简单,而且给我们带来了很多好处,比如:

  • 充分利用底层基础设施提供的资源

  • 处理多个请求的能力

  • 增加了可扩展性,减少了客户端的等待时间

这里需要注意的一点是,由于ThreadPoolExecutor利用了线程,CPython 实现可能由于 GIL 的存在而无法提供最大性能,GIL 不允许同时执行多个线程。因此,应用程序的性能可能会因所使用的底层 Python 实现而异。

现在出现的问题是,如果我们想要规避全局解释器锁,那该怎么办呢?在仍然使用 Python 的 CPython 实现的情况下,有没有一些机制?我们在上一章讨论了这种情况,并决定使用 Python 的多进程模块来代替线程库。

此外,事实证明,使用ProcessPoolExecutor是一件相当简单的事情。并发.futures 包中的底层实现处理了大部分必要性,并为程序员提供了一个简单易用的抽象。为了看到这一点,让我们修改我们之前的例子,将ProcessPoolExecutor替换为ThreadPoolExecutor。要做到这一点,我们只需要首先从 concurrent.futures 包中导入正确的实现,如下行所述:

from concurrent.futures import ProcessPoolExecutor

我们需要做的下一件事是修改我们的__init__方法,以创建一个进程池而不是线程池。下面的__init__方法的实现显示了我们如何做到这一点:

def __init__(self, hostname, port, pool_size):
        """Server initializer

        Keyword arguments:
        hostname -- The hostname to use for the server
        port -- The port on which the server should bind
        pool_size -- The size of the pool to use for the process based executor
        """

        # Setup process pool size
        self.executor_pool = ProcessPoolExecutor(max_workers=pool_size)

        # Setup the TCP socket server
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.hostname = hostname
        self.port = port
        self.bind_connection()
        self.listen()

确实,这是一个简单的过程,现在我们的应用程序可以使用多进程模型而不是多线程模型。

但是,我们可以保持池大小不变,还是它也需要改变?

每个进程都有自己的内存空间和需要维护的内部指针,这使得进程比使用线程实现并发更重。这提供了减少池大小的理由,以便允许更重的底层系统资源的使用。作为一个一般规则,对于ProcessPoolExecutormax_workers可以通过公式max_workers = (2 x CPU 核心数 + 1)来计算。

这个公式背后的推理可以归因于这样一个事实,即在任何给定时间,我们可以假设一半的进程将忙于执行网络 I/O,而另一半可能忙于执行 CPU 密集型任务。

所以,现在我们对如何使用资源池以及为什么这是一个比启动任意数量的线程更好的方法有了一个相当清楚的想法。但是,这种方法仍然需要大量的上下文切换,并且也高度依赖于所使用的底层 Python 实现。但肯定有比这更好的东西。

考虑到这一点,让我们尝试进入 Python 王国的另一个领域,即异步编程领域。

使用 AsyncIO 进行异步编程

在我们深入探讨异步编程这个未知领域之前,让我们首先回顾一下为什么我们使用线程或多个进程。

使用线程或多个进程的主要原因之一是增加并发性,从而使应用程序能够处理更多的并发请求。但这是以增加资源利用率为代价的,而且使用多线程或启动更重的进程来适应更高的并发性,需要复杂的实现在共享数据结构之间实现锁定。

现在,在构建可扩展的 Web 应用程序的背景下,我们还有一些与一般用途不同的主要区别...

AsyncIO 术语

正如我们最近讨论的,Python 中对异步编程的支持是通过事件循环和协程来实现的。但它们究竟是什么?让我们来看一下:

事件循环

事件循环,顾名思义,就是一个循环。这个循环的作用是,当一个新任务应该被执行时,事件循环会将这个任务排队。现在,控制权转移到了事件循环。当事件循环运行时,它会检查它的队列中是否有任务。如果有任务存在,控制权就会转移到这个任务上。

现在,在异步执行任务的上下文中,这是一个有趣的部分。假设事件循环的队列中有两个任务,即任务 A 和任务 B。当事件循环开始执行时,它会检查它的任务队列的状态。事件队列发现它的队列中有任务。因此,事件队列选择了任务 A。现在发生了上下文切换...

协程

Python AsyncIO 中的协程提供了执行多个同时操作的轻量级机制。协程在 Python 中作为生成器的特殊用例实现。因此,在我们深入理解协程之前,让我们花一点时间来理解生成器。

一般来说,生成器是那些生成一些值的函数。然而,这是每个其他函数所做的,那么生成器与常规函数有何不同。区别在于一般函数的生命周期与生成器的生命周期不同。当我们调用一个函数时,它产生一些值,返回它,一旦调用移出函数体,函数的作用域就被销毁。当我们再次调用函数时,会生成并执行一个新的作用域。

相比之下,当我们调用一个生成器时,生成器可以返回一个值,然后进入暂停状态,控制权转移到调用者。此时,生成器的作用域不会被销毁,它可以从之前离开的地方继续生成值。这基本上为我们提供了一个通过它可以拉取或产生一些值的函数。

以下代码示例显示了如何编写一个简单的生成器函数:

def get_number():
  i = 0
  while True:
    yield i 
    i = i + 1
num = get_number()
print(next(num))
>>> 0
print(next(num))
>>> 1

这里有趣的部分是,通过简单地一次又一次地调用生成器,生成器不会继续提供下一个结果。为了产生新的结果,我们需要在生成器上使用next()方法。这允许我们从生成器中产生新的结果。

现在,协程实现了生成器的一个特殊用例,在这个用例中,它不仅可以产生新的结果,还可以接收一些数据。这是通过 yield 和生成器的send()方法的组合实现的。

以下代码示例显示了一个简单协程的实现:

def say_hello():
  msg = yield "Hello"
  yield msg
greeting = say_hello()
next(greeting)
>>> Hello
greeting.send("Joe")
>>> Joe

由于协程允许函数暂停和恢复,因此可以懒惰地生成结果,这使得它成为异步编程的一个很好的选择,其中任务经常进入阻塞状态,一旦它们的操作完成,就会从那里恢复。

任务

Python AsyncIO 中的任务是包装协程的机制。每个任务都有与之关联的结果,可能会立即生成,也可能会延迟,这取决于任务的类型。这个结果被称为 Future。

在 AsyncIO 中,任务是 Future 的一个子类,它包装了一个协程。当协程完成生成值时,任务返回并被事件循环标记为完成,因此从事件队列的任务队列中移除。

现在,我们对与 Python AsyncIO 相关的术语有了相当清楚的概念。现在让我们深入一些行动,并编写一个简单的程序来了解 Python AsyncIO 的工作原理。

编写一个简单的 Python AsyncIO 程序

现在是时候做好准备,开始深入了解 Python 异步编程的世界,了解 AsyncIO 是如何工作的。

以下代码使用 Python 请求库和 AsyncIO 实现了一个简单的 URL 获取器:

# async_url_fetch.py
#!/usr/bin/python3
import asyncio
import requests

async def fetch_url(url):
   response = requests.get(url)
   return response.text

async def get_url(url):
    return await fetch_url(url)

def process_results(future):
    print("Got results")
    print(future.result())

loop = asyncio.get_event_loop()
task1 = loop.create_task(get_url('http://www.google.com'))
task2 = loop.create_task(get_url('http://www.microsoft.com'))
task1.add_done_callback(process_results)
task2.add_done_callback(process_results)
loop.run_forever()

这是一个小而不错的异步程序,实现了 Python AsyncIO 库。现在,让我们花一些时间来理解我们在这里做了什么。

从头开始,我们已经导入了 Python 请求库来从我们的 Python 代码中进行网络请求,并且还导入了 Python 的 AsyncIO 库。

接下来,我们定义了一个名为fetch_url的协程。在 AsyncIO 中定义协程的一般语法需要使用async关键字:

async def fetch_url(url)

接下来是另一个名为get_url的协程的定义。在get_url例程中,我们调用另一个协程fetch_url,它执行实际的 URL 获取。

由于fetch_url是一个阻塞协程,我们在调用fetch_url之前使用await关键字。这表示这个方法可以被暂停,直到结果被获取:

return await fetch_url(url)

程序中的下一个部分是process_results方法的定义。我们使用这个方法作为一个回调来处理get_url方法的结果一旦它们到达。这个方法接受一个参数,一个future对象,它将包含get_url函数调用的结果。

在方法内部,可以通过future对象的results()方法访问 future 的结果:

print(future.results())

有了这个,我们已经为执行 AsyncIO 事件循环设置了所有基本的机制。现在,是时候实现一个真正的事件循环并向其提交一些任务了。

我们首先通过调用get_event_loop()方法获取 AsyncIO 事件循环。get_event_loop()方法返回在代码运行的平台上的 AsyncIO 的最佳事件循环实现。

AsyncIO 实现了多个事件循环,程序员可以使用。通常,对get_event_loop()的简单调用将返回系统解释器正在运行的最佳事件循环实现。

一旦我们创建了循环,现在我们通过使用create_task()方法向事件循环提交了一些任务。这将任务添加到事件循环的队列中以执行。现在,由于这些任务是异步的,我们不知道哪个任务会首先产生结果,因此我们需要提供一个回调来处理任务的结果。为了实现这一点,我们使用任务的add_done_callback()方法向任务添加回调:

task1.add_done_callback(process_results)

一旦这里的一切都设置好了,我们就开始进入run_forever模式的事件循环,这样事件循环就会继续运行并处理新的任务。

有了这个,我们已经完成了一个简单的 AsyncIO 程序的实现。但是,嘿,我们正在尝试构建一个企业规模的应用程序。如果我想用 AsyncIO 构建一个企业级 Web 应用程序呢?

因此,现在让我们看看如何使用 AsyncIO 来实现一个简单的异步套接字服务器。

使用 AsyncIO 实现简单的套接字服务器

Python 实现提供的 AsyncIO 库提供了许多强大的功能。其中之一是能够接口和管理套接字通信。这为程序员提供了实现异步套接字处理的能力,因此允许更多的客户端连接到服务器。

以下代码示例构建了一个简单的套接字处理程序,使用基于回调的机制来处理与客户端的通信:

# async_socket_server.py#!/usr/bin/python3import asyncioclass MessageProtocol(asyncio.Protocol):    """An asyncio protocol implementation to handle the incoming messages.""" def connection_made(self, transport): ...

增加应用程序的并发性

当我们通过框架构建 Web 应用程序时,大多数情况下,框架通常会提供一个小巧易用的 Web 服务器。尽管这些服务器在开发环境中用于快速实现更改并在开发阶段内部调试应用程序中的问题,但这些服务器无法处理生产工作负载。

即使整个应用程序是从头开始开发的情况下,通常也是一个好主意通过使用反向代理来代理与 Web 应用程序的通信。但问题是,为什么我们需要这样做?为什么我们不直接运行 Web 应用程序并让它处理传入的请求。让我们快速浏览一下 Web 应用程序的所有职责:

  • 处理传入请求:当一个新的请求到达 Web 应用程序时,Web 应用程序可能需要决定如何处理该请求。如果 Web 应用程序有可以处理请求的工作进程,应用程序将接受请求,将其交给工作进程,并在工作进程完成处理后返回请求的响应。如果没有工作进程,那么 Web 应用程序必须将此请求排队以供以后处理。在最坏的情况下,当队列积压超过最大排队客户端数量的阈值时,Web 应用程序必须拒绝请求。

  • 提供静态资源:如果 Web 应用程序需要生成动态 HTML 页面,它也可以兼作服务器发送静态资源,如 CSS、Javascript、图像等,从而增加负载。

  • 处理加密:现在大多数网络应用程序都启用了加密。在这种情况下,我们的网络应用程序还需要我们来管理加密数据的解析并提供安全连接。

这些都是由一个简单的网络应用程序服务器处理的相当多的责任。我们实际上需要的是一种机制,通过这种机制,我们可以从网络应用程序服务器中卸载很多这些责任,让它只处理它应该处理的基本工作,并且真正发挥其作用。

在反向代理后运行

因此,我们改进网络应用程序处理大量客户端的第一步行动是,首先从它的肩上卸下一些责任。为了实现这一点,首先要做的简单选择是将网络应用程序放在反向代理后面运行:

那么,反向代理到底是做什么的呢?反向代理的工作方式是,当客户端请求到达网络应用程序服务器时,反向代理会拦截该请求。根据定义的规则将请求匹配到适当的后端应用程序,反向代理然后将该请求转发到...

提高安全性

考虑使用反向代理时首先想到的一个优势是提高安全性。这是因为现在我们可以在防火墙后面运行我们的网络应用程序,因此无法直接访问。反向代理拦截请求并将其转发到应用程序,而不让用户知道他们所发出的请求在幕后发生了什么。

对网络应用程序的受限访问有助于减少恶意用户可以利用的攻击面,从而破坏网络应用程序,并访问或修改关键记录。

改进连接处理

反向代理服务器还可以用于改进网络应用程序的连接处理能力。如今,为了加快获取远程内容的速度,Web 浏览器会打开多个连接到 Web 服务器,以增加资源的并行下载。反向代理可以排队并提供连接请求,同时网络应用程序正在处理待处理的请求,从而提高连接接受性并减少应用程序管理连接状态的负载。

资源缓存

当网络应用程序为特定客户端请求生成响应时,有可能会再次收到相同类型的请求,或者再次请求相同的资源。对于每个类似的请求,使用网络应用程序一遍又一遍地生成响应可能并不是一个优雅的解决方案。

反向代理有时可以帮助理解请求和响应模式,并为它们实施缓存。当启用缓存时,当再次收到类似的请求或再次请求相同的资源时,反向代理可以直接发送缓存的响应,而不是将请求转发到网络应用程序,从而卸载了网络应用程序的很多开销。这将提高网络应用程序的性能,并缩短客户端的响应时间。

提供静态资源

大多数网络应用程序提供两种资源。一种是根据外部输入生成的动态响应,另一种是保持不变的静态内容,例如 CSS 文件、Javascript 文件、图像等。

如果我们可以从网络应用程序中卸载这些责任中的任何一项,那么它将提供很大的性能增益和改进的可扩展性。

我们在这里最好的可能性是将静态资源的提供转移到客户端。反向代理也可以充当服务器,为客户端提供静态资源,而无需将这些请求转发到 Web 应用程序服务器,从而大大减少了等待的请求数量...

摘要

在本章的过程中,我们了解了构建我们的 Web 应用程序以处理大量并发请求的不同方式。我们首先了解并学习了不同的扩展技术,如垂直扩展和水平扩展,并了解了每种技术的不同优缺点。然后,我们进一步深入讨论了帮助我们提高 Web 应用程序本身处理更多请求的能力的主题。这使我们进入了使用资源池的旅程,以及为什么使用资源池而不是随意分配资源给每个到达 Web 应用程序的新请求是一个好主意。在旅程的进一步过程中,我们了解了处理传入请求的异步方式,以及为什么异步机制更适合于更 I/O 绑定的 Web 应用程序的更高可扩展性。我们通过研究反向代理的使用以及反向代理为我们扩展 Web 应用程序提供的优势来结束了我们关于为大量客户端扩展应用程序的讨论。

现在,通过了解我们如何使我们的应用程序处理大量并发请求,下一章将带领我们通过构建演示应用程序的过程,利用到目前为止在本书中学到的不同概念。

问题

  1. 我们如何使用同一应用程序的多个实例来处理传入请求?

  2. 我们如何实现进程池并将客户端请求分发到它们之上?

  3. 我们是否可以实现一个同时利用进程池和线程池的应用程序?在实施相同的过程中可能会遇到什么问题?

  4. 我们如何使用 AsyncIO 实现基本的 Web 服务器?

第六章:示例 - 构建 BugZot

在过去的几章中,我们已经讨论了许多构建企业级应用程序的技术。但是,如果我们不知道在哪里应用这些知识,那么这些知识有什么用呢?

在本章的过程中,我们将学习构建一个企业级 Web 应用程序的过程,该应用程序将用于跟踪 Omega 公司销售的各种产品的各种利益相关者报告的错误。从现在开始,我们将称之为BugZot的系统旨在提供此功能。

该应用程序将使用各种概念构建系统,以便在用户与系统交互的数量增加时能够轻松扩展。我们将看到如何利用各种优化的数据访问和存储技术、高度可扩展的部署和缓存技术来构建一个性能良好的应用程序,即使在高负载情况下也能表现良好。

在本章的过程中,我们将学习以下内容:

  • 利用现有的 Web 框架构建企业级 Web 应用程序

  • 实现优化数据库访问以加快应用程序速度

  • 实施缓存技术以减少应用程序后端的负载

  • 利用多线程技术增加应用程序的并发性

  • 以可扩展的方式部署应用程序以供生产使用

技术要求

本书中的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter06目录下找到。

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

本章旨在构建一个可扩展的错误跟踪 Web 应用程序。为了实现这一目标,我们使用了许多现有的库和工具,这些库和工具是公开可用的,并经过了长时间的测试,以适应各种用例。构建和运行演示应用程序需要以下一组工具:

  • PostgreSQL 9.6 或更高版本

  • Python 3.6 或更高版本

  • Flask—Python 中的 Web 开发微框架...

定义需求

构建任何企业级应用程序的第一步是定义应用程序的目标。到目前为止,我们知道我们的应用程序将跟踪 Omega 公司销售的各种产品的错误。但是,我们的应用程序需要什么功能来进行错误跟踪呢?让我们来看一看,并尝试定义我们将要构建的应用程序的需求。

  • 支持多个产品:我们的错误跟踪系统的一个基本要求是支持组织构建的多个产品的错误跟踪。考虑到组织的未来增长,这也是一个必需的功能。

  • 支持产品的多个组件:虽然我们可以在产品级别上报告错误,但这将会太笨重,特别是考虑到大多数组织都有一个专门负责产品正交特性的团队。为了更容易地跟踪基于已提交的组件的错误,错误跟踪系统应支持基于组件的错误报告。

  • 附件支持:很多时候,提交错误的用户,或者在错误生命周期中以任何方式参与的用户,可能希望附加显示错误效果的图像,或者可能希望附加错误的补丁,以便在合并到产品之前进行测试。这将需要错误跟踪系统提供支持,以将文件附加到错误报告中。

  • 支持评论:一旦提交了 bug,负责解决该 bug 的用户可能需要有关该 bug 的其他信息,或者可能需要一些协作。这使得缺陷跟踪系统必须支持评论成为必须。此外,并非每条评论都可以公开。例如,如果开发人员可能已经附加到 bug 报告中以供原始提交者测试但尚未纳入主产品的补丁,开发人员可能希望保持补丁私有,以便只有特权访问的人才能看到。这也使得私人评论的功能的包含成为必要。

  • 支持多个用户角色:组织中并非每个人对缺陷跟踪系统都具有相同级别的访问权限。例如,只有主管级别的人才能向产品添加新组件,只有员工才能看到 bug 的私人评论。这要求系统包含基于角色的访问权限作为要求。

这些是我们的缺陷跟踪系统特定的一些要求。然而,由于这些,显然还有一些其他要求需要包含在系统中。其中一些要求是:

  • 用户认证系统的要求:系统应该提供一种根据一些简单机制对用户进行认证的机制。例如,用户应该能够通过提供他们的用户名和密码,或者电子邮件和密码组合来登录系统。

  • 用于提交新 bug 的 Web 界面:应用程序应该提供一个简单易用的 Web 界面,用户可以用来提交新的 bug。

  • 支持 bug 生命周期:一旦 bug 被提交到系统中,它的生命周期就从 NEW 状态开始。从那里,它可能转移到 ASSIGNED 状态,当组织中的某人接手验证和重现 bug 时。从那里,bug 可以进入各种状态。这被称为我们跟踪系统内的 bug 生命周期。我们的缺陷跟踪系统应该支持这种生命周期,并且应该如何处理当 bug 从一个状态转移到另一个状态。

因此,我们终于把我们的需求摆在了这里。当我们开始设计和定义我们的缺陷跟踪网络应用程序的构建方式时,这些需求将发挥重要作用。因此,有了需求,现在是时候开始定义我们的代码基础是什么样子了。

进入开发阶段

随着我们的项目结构定义并就位,现在是时候站起来开始开发我们的应用程序了。开发阶段涉及各种步骤,包括设置开发环境,开发模型,创建与模型相对应的视图,并设置服务器。

建立开发环境

在我们开始开发之前的第一步是建立我们的开发环境。这涉及到准备好所需的软件包,并设置环境。

建立数据库

我们的 Web 应用程序在管理与用户和已提交的 bug 相关的个人记录方面严重依赖数据库。对于演示应用程序,我们将选择 PostgreSQL 作为我们的数据库。要在基于 RPM 的发行版上安装它,例如 Fedora,需要执行以下命令:

dnf install postgresql postgresql-server postgresql-devel

要在 Linux 的任何其他发行版或 Windows 或 Mac OS 等其他操作系统上安装postgresql,需要执行分发/操作系统的必需命令。

一旦我们安装了数据库,下一步就是初始化数据库,以便它可以用来存储我们的应用程序数据。用于设置...

建立虚拟环境

现在数据库已经就位,让我们设置虚拟环境,这将用于应用程序开发的目的。为了设置虚拟环境,让我们运行以下命令:

virtualenv –python=python3 

这个命令将在我们当前的目录中设置一个虚拟环境。设置虚拟环境之后的下一步是安装应用程序开发所需的框架和其他包。

然而,在继续安装所需的包之前,让我们首先通过执行以下命令激活我们的虚拟环境:

source bin/activate

作为一个设计决策,我们将基于 Python Flask 微框架进行 Web 应用程序开发。这个框架是一个开源框架,已经存在了相当多年,并且得到了各种插件的支持,这些插件可以很容易地与框架一起安装。该框架也是一个非常轻量级的框架,它只带有最基本的预打包模块,因此允许更小的占用空间。要安装flask,执行以下命令:

pip install flask

一旦我们安装了 Flask,让我们继续设置我们将在 Web 应用程序开发中使用的其他一些必需的包,通过执行以下命令:

pip install flask-sqlalchemy requests pytest flask-session

有了这个,我们现在已经完成了虚拟环境的设置。现在,让我们继续设置我们的代码库将是什么样子。

构建我们的项目

现在,我们处于一个需要决定我们的项目结构将是什么样子的阶段。项目结构非常重要,因为它决定了我们代码中不同组件之间的交互方式,以及什么地方将标志着我们应用程序的入口点。

一个结构良好的项目不仅有助于为项目提供更好的导航,而且还有助于提供代码不同部分之间的增强一致性。

所以,让我们来看看我们的代码结构将是什么样子,并理解特定目录或文件的意义:

$ tree --dirsfirst├── bugzot│   ├── helpers│   │   └── __init__.py│   ├── models│   │   └── __init__.py│   ├── static│ ├── templates ...

初始化 Flask 项目

所以,我们终于进入了项目的有趣阶段,我们将从头开始构建这个项目。所以,让我们不要等太久,我们就可以看到一些行动。我们要做的第一件事是使用 Flask 设置一个基本项目并让它运行起来。为了做到这一点,让我们启动我们的代码编辑器并设置我们的初始代码库。

让我们打开文件bugzot/application.py并初始化我们的应用程序代码库:

'''
File: application.py
Description: The file contains the application initialization
             logic that is used to serve the application.
'''
from flask import Flask, session
from flask_bcrypt import Bcrypt
from flask_session import Session
from flask_sqlalchemy import SQLAlchemy

# Initialize our Flask application
app = Flask(__name__, instance_relative_config=True)

# Let's read the configuration
app.config.from_object('config')
app.config.from_pyfile('config.py')

# Let's setup the database
db = SQLAlchemy(app)

# Initializing the security configuration
bcrypt = Bcrypt(app)

# We will require sessions to store user activity across the application
Session(app)

现在我们已经完成了应用程序的非常基本的设置。让我们花一些时间来理解我们在这里做了什么。

在文件的开头,我们首先导入了我们将要构建项目的所需包。我们从flask包中导入Flask应用程序类。类似地,我们导入了代码哈希库bcryptFlask会话类,以及用于 Flask 的 SQLAlchemy 支持包,它提供了与 Flask 的 SQLAlchemy 集成。

一旦我们导入了所有必需的包,下一步就是初始化我们的 Flask 应用程序。为此,我们创建一个Flask类的实例,并将其存储在一个名为app的对象中。

app = Flask(__name__, instance_relative_config=True)

在创建这个实例时,我们向类构造函数传递了两个参数。第一个参数用于表示 Flask 的应用程序名称。__name__提供了我们传递给构造函数的应用程序名称。第二个参数instance_relative_config允许我们从实例文件夹中覆盖应用程序配置。

有了这个,我们的 Flask 应用程序实例设置就完成了。接下来要做的是加载应用程序的配置,这将用于配置应用程序内部不同组件的行为,以及我们的应用程序将如何提供给用户。为了做到这一点,我们需要从配置文件中读取。以下两行实现了这一点:

app.config.from_object('config')
app.config.from_pyfile('config.py')

第一行加载了我们项目根目录下的config.py文件,将其视为一个对象,并加载了它的配置。第二行负责读取实例目录下的config.py文件,并加载可能存在的任何配置。

一旦这些配置加载完成,它们就可以在app.config对象下使用。大多数 Flask 插件都配置为从app.config读取配置,因此减少了可能发生的混乱,如果每个插件都有不同的配置处理机制。

在我们的应用程序中加载配置后,我们现在可以继续初始化我们可能需要的其余模块。特别是,我们需要一些额外的模块来建立我们的应用程序功能。这些模块包括 SQLAlchemy 引擎,我们将使用它来构建和与我们的数据库模型交互,一个会话模块,它将被用来管理应用程序中的用户会话,以及一个bcrypt模块,它将被用来在整个应用程序中提供加密支持。以下代码提供了这些功能:

db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
Session(app)

从这些代码行中可以看出,要配置这些模块,我们所需要做的就是将 Flask 应用程序对象作为参数传递给各自的类构造函数,它们的配置将从那里自动获取。

现在,我们已经将应用程序初始化代码放在了适当的位置,我们需要做的下一件事是从我们的 BugZot 模块中导出所需的组件,以便可以从项目根目录调用应用程序。

为了实现这一点,我们需要做的就是将这些模块包含在模块入口点中。所以,让我们打开代码编辑器,打开bugzot/__init__.py,我们需要在那里获取这些对象。

'''
File: __init__.py
Description: Bugzot application entrypoint file.
'''
from .application import app, bcrypt, db

好了,我们完成了。我们已经在 BugZot 模块中导出了所有必需的对象。现在,问题是如何启动我们的应用程序。因此,为了启动我们的应用程序并使其提供传入的请求,我们需要完成一些更多的步骤。所以,让我们打开项目根目录下的run.py文件,并添加以下行:

'''
File: run.py
Description: Bugzot application execution point.
'''
from bugzot import app

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

好了,是不是很简单?我们在这里所做的就是导入我们在 BugZot 模块中创建的flask应用对象,并调用app对象的run方法,将应用程序将要提供给用户的hostname值和应用程序服务器应该绑定以监听请求的端口值传递给它。

我们现在已经准备好启动我们的应用程序服务器,并使其监听传入的请求。但是,在我们这样做之前,我们只需要完成一个步骤,即创建应用程序的配置。所以,让我们开始并创建配置。

创建配置

在我们启动应用程序之前,我们需要配置我们将在应用程序中使用的模块。因此,让我们首先打开代码编辑器中的config.py,并向其中添加以下内容,以创建我们应用程序的全局配置:

'''File: config.pyDescription: Global configuration for Bugzot project'''DEBUG = FalseSECRET_KEY = 'your_application_secret_key'BCRYPT_LOG_ROUNDS = 5 # Increase this value as required for your applicationSQLALCHEMY_DATABASE_URI = "sqlite:///bugzot.db"SQLALCHEMY_ECHO = FalseSESSION_TYPE = 'filesystem'STATIC_PATH = 'bugzot/static'TEMPLATES_PATH = 'bugzot/templates'

有了这些,我们已经起草了全局应用程序配置。让我们尝试...

开发数据库模型

数据库模型构成了任何现实生活应用程序的重要部分。这是因为企业中的任何严肃应用程序肯定会处理需要在时间跨度内持久化的某种数据。

对于我们的 BugZot 也是一样的。BugZot 用于跟踪 Omega Corporation 产品中遇到的错误及其生命周期。此外,应用程序还需要记录在其上注册的用户。为了实现这一点,我们将需要多个模型,每个模型都有自己的用途。

为了开发这个应用程序,我们将所有相关的模型分组到它们自己的单独目录下,这样我们就可以清楚地知道每个模型的作用是什么。这也让我们能够保持代码库的整洁,避免开发人员在未来难以理解每个文件的作用。

因此,让我们首先开始开发管理用户账户相关信息所需的模型。

为了开始开发与用户账户相关的模型,我们首先创建一个名为users的目录,放在我们的模型目录下:

mkdir bugzot/models/users

然后将其初始化为模型模块的子模块。

一旦我们完成了这一点,我们就可以开始创建我们的用户模型,其定义如下所示:

'''
File: users.py
Description: The file contains the definition for the user data model
             that will be used to store the information related to the
             user accounts.
'''
from bugzot.application import db
from .roles import Role

class User(db.Model):
    """User data model for storing user account information.

    The model is responsible for storing the account information on a
    per user basis and providing access to it for authentication
    purposes.
    """

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(50), unique=True, index=True, nullable=False)
    password = db.Column(db.String(512), nullable=False)
    email = db.Column(db.String(255), unique=True, nullable=False, index=True)
    user_role = db.Column(db.Integer, db.ForeignKey(Role.id))
    role = db.relationship("Role", lazy=False)
    joining_date = db.Column(db.DateTime, nullable=False)
    last_login = db.Column(db.DateTime, nullable=False)
    account_status= db.Column(db.Boolean, nullable=False, default=False)

    def __repr__(self):
        """User model representation."""
        return "<User {}>".format(self.username)

有了这个,我们刚刚创建了我们的用户模型,可以用来存储与用户相关的信息。大多数列只是提供了我们期望存储在数据库中的数据的定义。然而,这里有一些有趣的地方,让我们来看看:

index=True

我们可以看到这个属性在用户名和电子邮件列的定义中被提及。我们将索引属性设置为 True,因为这两列经常被用来访问与特定用户相关的数据,因此可以从索引带来的优化中受益。

这里的下一个有趣的信息是与角色模型的关系映射。

role = db.relationship("Role", lazy=False)

由于我们数据库中的每个用户都有一个与之关联的角色,我们可以从我们的用户模型到角色模型添加一个一对一的关系映射。此外,如果我们仔细看,我们设置了lazy=False。我们之所以要避免懒加载,有一个小原因。角色模型通常很小,而且用户模型到角色模型只有一个一对一的映射。通过避免懒加载,我们节省了一些时间,因为我们的数据库访问层不再懒加载来自角色模型的数据。现在,问题是,角色模型在哪里?

角色模型的定义可以在bugzot/models/users/roles.py文件中找到,但我们明确地没有在书中提供该定义,以保持章节简洁。

此外,我们需要一种机制来验证用户的电子邮件地址。我们可以通过发送包含激活链接的小邮件给用户来实现这一点,他们需要点击该链接。为此,我们还需要为每个新用户生成并存储一个激活密钥。为此,我们利用了一个名为ActivationKey模型的新模型,其定义可以在bugzot/models/users/activation_key.py文件中找到。

一旦所有这些都完成了,我们现在可以准备将这些模型从用户模型子模块中导出。为了做到这一点,让我们打开代码编辑器中的模块入口文件,并通过向bugzot/models/users/__init__.py文件添加以下行来导出模型:

from .activation_key import ActivationKey
from .roles import Role
from .users import User

有了这个,我们完成了与存储用户信息相关的数据模型的定义。

我们应用程序中的下一件事是定义与产品分类相关的数据模型,用于对可以提交 bug 的产品进行分类。因此,让我们开始创建与产品分类相关的模型。

为了创建与产品相关的模型,我们首先在bugzot/models模块下创建一个新的子模块目录并进行初始化。接下来,我们在bugzot/models/products/products.py下提供产品模型的定义,如下所示:

'''
File: products.py
Description: The file contains the definition for the products
             that are supported for bug filing inside the bug tracker
'''
from bugzot.application import db
from .categories import Category

class Product(db.Model):
    """Product defintion model.

    The model is used to store the information related to the products
    for which the users can file a bug.
    """

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    product_name = db.Column(db.String(100), nullable=False, unique=True, index=True)
    category_id = db.Column(db.Integer, db.ForeignKey(Category.id))
    category = db.relationship("Category", lazy=True)

    def __repr__(self):
        """Product model representation."""
        return "<Product {}>".format(self.product_name)

有了这个,我们已经完成了产品模型的定义,该模型将用于跟踪产品,用户可以在我们的应用程序中提交 bug。

在我们的产品子模块中还有一些其他模型定义,如下所示:

  • 类别:类别模型负责存储关于特定产品所属的产品类别的信息

  • 组件:组件模型负责存储与产品组件相关的信息,其中一个错误可以被归类

  • 版本:版本模型负责存储与产品版本相关的信息,其中一个错误可以被分类

一旦所有这些模型被定义,它们就可以从产品的子模块中导出,以便在应用程序中使用。

以类似的方式,我们定义了与系统内错误跟踪相关的模型。我们将跳过在本章节中提及这些模型的定义,以保持章节长度合理,但是,对于好奇的人来说,这些模型的定义可以很容易地在代码库中的bugzot/models/bugs目录中找到。

迁移数据库模型

有了我们创建的数据库模型并准备好使用,下一步是将这些数据库模型迁移到我们用来运行应用程序的数据库服务器。这个过程非常简单。

要将模型迁移到数据库服务器,我们首先将它们暴露到应用程序根目录中。例如,要迁移与用户和产品相关的数据库模型,我们只需要在bugzot/__init__.py文件中添加以下行:

from bugzot.models import ActivationKey, Category, Component, Product, Role, User, Version

完成后,我们只需要调用我们创建的 SQLAlchemy 数据库对象的create_all()方法。这可以通过添加以下...来完成

构建视图

一旦模型生成并准备就绪,我们需要的下一步是拥有一种机制,通过该机制我们可以与这些模型进行交互,以便访问或修改它们。我们可以通过视图的使用来实现这种功能的一种方式。

使用 Flask,构建视图是相当容易的任务。Flask Web 框架提供了多种构建视图的方法。事实上,/ping端点也可以被称为使用过程式风格构建的视图之一。

在示例过程中,我们现在将尝试在定义应用程序中的任何资源时遵循面向对象的方法。因此,让我们继续并开始开发一些视图。

开发索引视图

每当用户访问我们的应用程序时,很可能用户会登陆到应用程序的主页上。因此,我们首先构建的是索引视图。这也是我们可以了解如何在 Flask 中构建简单视图的地方之一。

因此,作为第一步,让我们通过执行以下命令在项目工作空间的视图目录中创建一个新模块,用于索引模块:

mkdir bugzot/views/indextouch bugzot/views/index/__init__.py

有了这个,我们现在准备编写我们的第一个视图,其代码如下:

'''File: index.pyDescription: The file provides the definition for the index view             which is used to render the homepage of Bugzot.'''from bugzot.application ...

获取索引视图以渲染

现在,我们已经准备好了索引视图。但是,在此视图可以提供给用户之前,我们需要为 Flask 提供有关此视图将被渲染的端点的映射。为了实现这一点,让我们打开我们的代码编辑器并打开bugzot/__init__.py文件,并向文件中添加以下行:

from bugzot.views import IndexView
app.add_url_rule('/', view_func=IndexView.as_view('index_view'))

在这里,我们的重点是第二行,它负责将我们的视图与 URL 端点进行映射。我们的 Flask 应用程序的add_url_rule()负责提供这些映射。该方法的第一个参数是视图应该在其上呈现的 URL 路径。提供给该方法的view_func参数接受需要在提供的 URL 端点上呈现的视图。

完成后,我们现在准备好提供我们的索引页面。现在我们只需要运行以下命令:

python run.py

然后在浏览器上访问localhost:8000/

构建用户注册视图

现在,部署并准备好使用的索引视图,让我们继续构建一个更复杂的视图,在这个视图中,我们允许用户在 BugZot 上注册。

以下代码实现了一个名为UserRegisterView的视图,允许用户注册到 BugZot。

'''File: user_registration.pyDescription: The file contains the definition for the user registration             view allowing new users to register to the BugZot.'''from bugzot.application import app, brcypt, dbfrom bugzot.models import User, Rolefrom flask.views import MethodViewfrom datetime import datetimefrom flask import render_template, sessionclass UserRegistrationView(MethodView):    """User registration view to allow new user registration. The user ...

部署以处理并发访问

到目前为止,我们处于开发阶段,可以轻松使用 Flask 自带的开发服务器快速测试我们的更改。但是,如果您计划在生产环境中运行应用程序,这个开发服务器并不是一个好选择,我们需要更专门的东西。这是因为在生产环境中,我们将更关注应用程序的并发性,以及其安全方面,比如启用 SSL 并为一些端点提供更受限制的访问。

因此,我们需要根据我们的应用需要处理大量并发访问的事实,同时不断保持对用户的良好响应时间,来确定一些选择。

考虑到这一点,我们最终得到了以下一系列选择,它们的性质在许多生产环境中也是相当常见的:

  • 应用服务器:Gunicorn

  • 反向代理:Nginx

在这里,Gunicorn 将负责处理由我们的 Flask 应用程序提供的请求,而 Nginx 负责请求排队和处理静态资产的分发。

那么,首先,让我们设置 Gunicorn 以及我们将如何通过它提供应用程序。

设置 Gunicorn

设置 Gunicorn 的第一步是安装,这是一个相当简单的任务。我们只需要运行以下命令:

pip install gunicorn

一旦完成了这一步,我们就可以运行 Gunicorn 了。Gunicorn 通过WSGI运行应用程序,WSGI 代表 Web 服务器网关接口。为了让 Gunicorn 运行我们的应用程序,我们需要在项目工作空间中创建一个名为wsgi.py的额外文件,内容如下:

'''File: wsgi.pyDescription: WSGI interface file to run the application through WSGI interface'''from bugzot import appif __name__ == '__main__':    app.run()

一旦我们定义了接口文件,我们只需要运行以下命令来使 Gunicorn...

设置 Nginx 作为反向代理

要将 Nginx 用作我们的反向代理解决方案,我们首先需要在系统上安装它。对于基于 Fedora 的发行版,可以通过使用dnfyum软件包管理器轻松安装,只需运行以下命令:

$ sudo dnf install nginx

对于其他发行版,可以使用它们的软件包管理器来安装 Nginx 软件包。

安装 Nginx 软件包后,我们现在需要进行配置,以使其能够与我们的应用服务器进行通信。

要配置 Nginx 将通信代理到我们的应用服务器,创建一个名为bugzot.conf的文件,放在/etc/nginx/conf.d目录下,内容如下:

server {
    listen 80;
    server_name <your_domain> www.<your_domain>;

    location / {
        include proxy_params;
        proxy_pass http://unix:<path_to_project_folder>/bugzot.sock;
    }
}

现在 Nginx 配置完成后,我们需要建立我们的 Gunicorn 应用服务器和 Ngnix 之间的关系。所以,让我们来做吧。

建立 Nginx 和 Gunicorn 之间的通信

在我们刚刚完成的 Nginx 配置中需要注意的一点是proxy_pass行:

proxy_pass http://unix:<path_to_project_folder>/bugzot.sock

这行告诉 Nginx 查找一个套接字文件,通过它 Nginx 可以与应用服务器通信。我们可以告诉 Gunicorn 为我们创建这个代理文件。执行以下命令即可完成:

gunicorn –bind unix:bugzot.sock -m 007 wsgi:app

执行此命令后,我们的 Gunicorn Web 服务器将创建一个 Unix 套接字并绑定到它。现在,剩下的就是启动我们的 Nginx Web 服务器,只需执行以下命令即可轻松实现:

systemctl start nginx.service

一旦完成了这一步,...

总结

在本章中,我们获得了如何开发和托管企业级网络应用程序的实际经验。为了实现这一目标,我们首先做出了一些关于要使用哪些网络框架和数据库的技术决策。然后,我们继续定义我们的项目结构以及它在磁盘上的外观。主要目标是实现高度模块化和代码之间的耦合度较低。一旦项目结构被定义,我们就初始化了一个简单的 Flask 应用程序,并实现了一个路由来检查我们的服务器是否正常工作。然后,我们继续定义我们的模型和视图。一旦这些被定义,我们就修改了我们的应用程序,以启用提供对我们视图的访问的新路由。一旦我们的应用程序开发周期结束,我们就开始了解如何使用 Gunicorn 和 Nginx 部署应用程序以处理大量请求。

现在,当我们进入下一章时,我们将看看如何开发优化的前端,以适应我们正在开发的应用程序,并且前端如何影响用户与我们的应用程序交互时的体验。

问题

  • Flask 提供了哪些其他预构建的视图类?

  • 我们能否在不删除关系的情况下从用户表中删除到角色表的外键约束?

  • 除了 Gunicorn 之外,还有哪些用于提供应用程序的其他选项?

  • 我们如何增加 Gunicorn 工作进程的数量?

第七章:构建优化的前端

在本书中,我们已经在尝试了解如何在 Python 中为企业构建应用程序时走得很远。到目前为止,我们已经涵盖了如何为我们的企业应用程序构建一个可扩展和响应迅速的后端,以满足大量并发用户,以便我们的企业应用程序能够成功地为其用户提供服务。然而,在构建企业级应用程序时,有一个我们一直忽视的话题,通常在构建企业级应用程序时很少受到关注:应用程序的前端。

当用户与我们的应用程序交互时,他们很少关心后端发生了什么。用户的体验直接与应用程序的前端如何响应他们的输入相关。这使得应用程序的前端不仅是应用程序最重要的方面之一,也使其成为应用程序在用户中成功的主要决定因素之一。

在本章中,我们将看看如何构建应用程序前端,不仅提供易于使用的体验,还能快速响应他们的输入。

在阅读本章时,我们将学习以下主题:

  • 优化应用前端的需求

  • 优化前端所依赖的资源

  • 利用客户端缓存来简化页面加载

  • 利用 Web 存储持久化用户数据

技术要求

本书中的代码清单可以在chapter06中的bugzot应用程序目录下找到github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

代码的执行不需要任何特定的特殊工具或框架,这是一个非常简单的过程。README.md文件指向了如何运行本章的代码示例。

优化前端的需求

应用的用户界面是最重要的用户界面组件之一。它决定了用户如何感知应用程序。一个流畅的前端在定义用户体验方面起着很大的作用。

这种对流畅用户体验的需求带来了优化应用前端的需求,它提供了一个易于使用的界面,快速的响应时间和操作的流畅性。如果我们继续向 Web 2.0 公司(如 Google、Facebook、LinkedIn 等)看齐,他们会花费大量资源来优化他们的前端,以减少几毫秒的渲染时间。这就是优化前端的重要性。

优化前端的组件

我们正在讨论优化前端。但是优化的前端包括什么?我们如何决定一个前端是否被优化了?让我们来看一下。

优化的前端有几个组件,不是每个组件都需要从前端反映出来。这些组件如下:

  • 快速渲染时间:前端优化的首要重点之一是减少页面的渲染时间。虽然没有预定义的渲染时间可以被认为是好还是坏,但你可以认为一个好的渲染时间是用户在一个体面的互联网连接上不必等待太长时间页面加载的时间。另外,...

导致前端问题的原因

前端问题是一类问题,用户很容易察觉到,因为它们影响用户与应用程序的交互方式。在这里,为了清楚起见,当我们说企业 Web 应用的前端时,我们不仅谈论其用户界面,还谈论代码和模板,这些都是用来呈现所需用户界面的。现在,让我们继续了解前端特定问题的可能原因:

  • 过多的对象:在大多数负责呈现前端的动态填充模板中,第一个问题出现在呈现过多对象时。当大量对象传递给需要呈现的模板时,页面响应时间往往会增加,导致过程明显减慢。

  • 过多的包含:软件工程中关注的一个主要问题是如何增加代码库的模块化。模块化的增加有助于增加组件的可重用性。然而,过度的模块化可能是可能出现重大问题的信号。当前端模板被模块化到超出所需程度时,模板的呈现性能会降低。原因在于每个包含都需要从磁盘加载一个新文件,这是一个异常缓慢的操作。这里的一个反驳观点可能是,一旦模板加载了所有包含的内容,呈现引擎就可以缓存模板,并从缓存中提供后续请求。然而,大多数缓存引擎对它们可以缓存的包含深度有一个限制,超出这个限制,性能损失将是明显的。

  • 不必要的资源集:一些前端可能加载了大量不在特定页面上使用的资源。这包括包含仅在少数页面上执行的函数的 JavaScript 文件。每个额外加载的文件不仅增加了带宽的消耗,还影响了前端的加载性能。

  • 强制串行加载代码:现代大多数浏览器都经过优化,可以并行加载大量资源,以有效利用网络带宽并减少页面加载时间。然而,有时,我们用来减少代码量的一些技巧可能会强制页面按顺序加载,而不是并行加载。可能导致页面资源按顺序加载的最常见示例之一是使用 CSS 导入。尽管 CSS 导入提供了直接在另一个样式表中加载第三方 CSS 文件的灵活性,但它也减少了浏览器加载 CSS 文件内容的能力,因此增加了呈现页面所需的时间。

这一系列原因构成了可能导致页面呈现时间减慢的问题的非穷尽列表,因此给用户带来不愉快的体验。

现在,让我们看看如何优化我们的前端,使其具有响应性,并提供最佳的用户体验。

优化前端

到目前为止,我们了解了可能影响前端性能的各种问题。现在,是时候看看我们如何减少前端的性能影响,并使它们在企业级环境中快速响应。

优化资源

我们首先要看的优化是在请求特定页面时加载的资源。为此,请考虑管理面板中用户数据显示页面的以下代码片段,该页面负责显示数据库中的用户表:

<table>
{% for user in users %}
  <tr>
    <td class="user-data-column">{{ user.username }}</td>
    <td class="user-data-column">{{ user.email }}</td>
    <td class="user-data-column">{{ user.status }}</td>
  </tr>
{% endfor %}
</table>

到目前为止,一切顺利。正如我们所看到的,代码片段只是循环遍历用户对象,并根据用户表中存储的记录数量来渲染表格。这对于大多数情况下用户记录只有少量(例如 100 条左右)的情况来说是很好的。但随着应用程序中用户数量的增长,这段代码将开始出现问题。想象一下尝试从应用程序数据库中加载 100 万条记录并在 UI 上显示它们。这会带来一些问题:

  • 数据库查询缓慢:尝试同时从数据库加载 100 万条记录将会非常缓慢,并且可能需要相当长的时间,因此会阻塞视图很长时间。

  • 解码前端对象:在前端,为了渲染页面,模板引擎必须解码所有对象的数据,以便能够在页面上显示数据。这种操作不仅消耗 CPU,而且速度慢。

  • 页面大小过大:想象一下从服务器到客户端通过网络传输数百万条记录的页面。这个过程耗时且使页面不适合在慢速连接上加载。

那么,我们可以在这里做些什么呢?答案很简单:让我们优化将要加载的资源量。为了实现这一点,我们将利用一种称为分页的概念。

为了实现分页,我们需要对负责渲染前端模板的视图以及前端模板进行一些更改。以下代码描述了如果视图需要支持分页,它将会是什么样子:

From bugzot.application import app, db
from bugzot.models import User
from flask.views import MethodView
from flask import render_template, session, request

class UserListView(MethodView):
    """User list view for displaying user data in admin panel.

      The user list view is responsible for rendering the table of users that are registered
      in the application.
    """

    def get(self):
        """HTTP GET handler."""

        page = request.args.get('next_page', 1) # get the page number to be displayed
        users = User.query.paginate(page, 20, False)
        total_records = users.total
        user_records = users.items

        return render_template('admin/user_list.html', users=user_records, next_page=page+1)

我们现在已经完成了对视图的修改,它现在支持分页。通过使用 SQLAlchemy 提供的设施,实现这种分页是一项相当容易的任务,使用paginate()方法从数据库表中分页结果。这个paginate()方法需要三个参数,即页面编号(应从 1 开始),每页记录数,以及error_out,它负责设置该方法的错误报告。在这里设置为False会禁用在stdout上显示错误。

开发支持分页的视图后,下一步是定义模板,以便它可以利用分页。以下代码显示了修改后的模板代码,以利用分页:

<table>
{% for user in users %}
  <tr>
    <td class="user-data-column">{{ user.username }}</td>
    <td class="user-data-column">{{ user.email }}</td>
    <td class="user-data-column">{{ user.status }}</td>
  </tr>
{% endfor %}
</table>
<a href="{{ url_for('admin_user_list', next_page) }}">Next Page</a>

有了这个视图代码,我们的视图代码已经准备好了。这个视图代码非常简单,因为我们只是通过添加一个href来扩展之前的模板,该href加载下一页的数据。

现在我们已经优化了发送到页面的资源,接下来我们需要关注的是如何使我们的前端更快地加载更多资源。

通过避免 CSS 导入并行获取 CSS

CSS 是任何前端的重要组成部分,它帮助为浏览器提供样式信息,告诉浏览器如何对从服务器接收到的页面进行样式设置。通常,前端可能会有许多与之关联的 CSS 文件。我们可以通过使这些 CSS 文件并行获取来实现一些可能的优化。

所以,让我们想象一下我们有以下一组 CSS 文件,即main.cssreset.cssresponsive.cssgrid.css,我们的前端需要加载。我们允许浏览器并行加载所有这些文件的方式是通过使用 HTML 链接标签将它们链接到前端,而不是使用 CSS 导入,这会导致加载 CSS 文件...

打包 JavaScript

在当前时间和希望的未来,我们将不断看到网络带宽的增加,无论是宽带网络还是移动网络,都可以实现资源的并行更快下载。但是对于每个需要从远程服务器获取的资源,由于每个单独的资源都需要向服务器发出单独的请求,仍然涉及一些网络延迟。当需要加载大量资源并且用户在高延迟网络上时,这种延迟可能会影响。

通常,大多数现代 Web 应用程序广泛利用 JavaScript 来实现各种目的,包括输入验证、动态生成内容等。所有这些功能都分成多个文件,其中可能包括一些库、自定义代码等。虽然将所有这些拆分成不同的文件可以帮助并行加载,但有时 JavaScript 文件包含用于在网页上生成动态内容的代码,这可能会阻止网页的呈现,直到成功加载网页呈现所需的所有必要文件。

我们可以减少浏览器加载这些脚本资源所需的时间的一种可能的方法是将它们全部捆绑到一个单一文件中。这允许所有脚本组合成一个单一的大文件,浏览器可以在一个请求中获取。虽然这可能会导致用户在首次访问网站时体验有点慢,但一旦资源被获取和缓存,用户对网页的后续加载将会显著更快。

今天,有很多第三方库可用,可以让我们捆绑这些 JavaScript。让我们以一个名为 Browserify 的简单工具为例,它允许我们捆绑我们的 JavaScript 文件。例如,如果我们有多个 JavaScript 文件,如jquery.jsimage-loader.jsslideshow.jsinput-validator.js,并且我们想要使用 Browserify 将这些文件捆绑在一起,我们只需要运行以下命令:

browserify jquery.js image-loader.js slideshow.js input-validator.js > bundle.js

这个命令将把这些 JavaScript 文件创建成一个称为bundle.js的公共文件包,现在可以通过简单的脚本标签包含在我们的 Web 应用程序中,如下所示:

<script type="text/javascript" src="js/bundle.js"></script>

将 JavaScript 捆绑到一个请求中加载,我们可能会开始看到一些改进,以便页面在浏览器中快速获取和显示给用户。现在,让我们来看看另一个可能有用的有趣主题,它可能会在网站重复访问时对我们的 Web 应用程序加载速度产生真正的影响。

我们讨论的 JavaScript 捆绑技术也可以用于包含 CSS 文件的优化。

利用客户端缓存

缓存长期以来一直被用来加快频繁使用的资源的加载速度。例如,大多数现代操作系统利用缓存来提供对最常用应用程序的更快访问。Web 浏览器也利用缓存,在用户再次访问同一网站时,提供对资源的更快访问。这样做是为了避免如果文件没有更改就一遍又一遍地从远程服务器获取它们,从而减少可能需要的数据传输量,同时提高页面的呈现时间。

现在,在企业应用程序的世界中,像客户端缓存这样的东西可能会非常有用。这是因为...

设置应用程序范围的缓存控制

由于我们的应用程序基于 Flask,我们可以利用几种简单的机制来为我们的应用程序设置缓存控制。例如,将以下代码添加到我们的bugzot/application.py文件的末尾可以启用站点范围的缓存控制,如下所示:

@app.after_request
def cache_control(response):
  """Implement side wide cache control."""
  response.cache_control.max_age = 300
  response.cache_control.public = True
  return response

在这个例子中,我们利用 Flask 内置的after_request装饰器钩子来设置 HTTP 响应头,一旦请求到达 Flask 应用程序,装饰的函数需要一个参数,该参数接收一个响应类的对象,并返回一个修改后的响应对象。

对于我们的用例,在after_request钩子的方法代码中,我们设置了cache_control.max_age头,该头指定了内容在再次从服务器获取之前从缓存中提供的时间的上限,以及cache_control.public头,该头定义了缓存响应是否可以与多个请求共享。

现在,可能会有时候我们想为特定类型的请求设置不同的缓存控制。例如,我们可能不希望为用户个人资料页面设置cache_control.public,以避免向不同的用户显示相同的个人资料数据。我们的应用程序允许我们相当快速地实现这些类型的场景。让我们来看一下。

设置请求级别的缓存控制

在 Flask 中,我们可以在将响应发送回客户端之前修改响应头。这可以相当容易地完成。以下示例显示了一个实现响应特定头控制的简单视图:

from bugzot.application import app, dbfrom bugzot.models import Userfrom flask.views import MethodViewfrom flask import render_template, session, request, make_responseclass UserListView(MethodView):  """User list view for displaying user data in admin panel.  The user list view is responsible for rendering the table of users that are registered  in the application.  """  def get(self):    """HTTP GET handler."""        page = request.args.get('next_page', 1) # get the page number to be displayed users = User.query.paginate(page, ...

利用 Web 存储

任何曾经处理过即使是一点点用户管理的应用程序的 Web 应用程序开发人员肯定都听说过 Web cookies,它本质上提供了一种在客户端存储一些信息的机制。

利用 cookies 提供了一种简单的方式,通过它我们可以在客户端维护小量用户数据,并且可以多次读取,直到 cookies 过期。但是,尽管处理 cookies 很容易,但有一些限制限制了 cookies 的实用性,除了在客户端维护少量应用程序状态之外。其中一些限制如下:

  • cookies 随每个请求传输,因此增加了每个请求传输的数据量

  • Cookies 允许存储少量数据,最大限制为 4 KB

现在,出现的问题是,如果我们想存储更多的数据,或者我们想避免在每个请求中一遍又一遍地获取相同的存储数据,我们该怎么办?

为了处理这种情况,HTML 的最新版本 HTML 5 提供了各种功能,允许处理客户端 Web 存储。这种 Web 存储相对于基于 cookies 的机制提供了许多优点,例如:

  • 由于 Web 存储直接在客户端上可用,因此不需要服务器一遍又一遍地将信息发送到客户端

  • Web 存储 API 提供了最多 10 MB 的存储空间,这是比使用 cookies 存储的多次更大的存储空间

  • Web 存储提供了在本地存储中存储数据的灵活性,例如,即使用户关闭并重新打开浏览器,数据也是可访问的,或者基于每个会话的基础上存储数据,其中存储在 Web 存储中的数据将在会话失效时被清除,无论是当用户会话被应用程序处理用户注销的处理程序销毁,还是浏览器关闭

这使得 Web 存储成为一个吸引人的地方,可以存放数据,避免一遍又一遍地加载

对于我们的企业应用程序,这可以通过仅在用户浏览器中存储中间步骤的结果,然后仅在填写完所有必需的输入字段时将它们提交回服务器,从而提供很大的灵活性。

另一个可能更适用于 Bugzot 的用例是,我们可以将用户提交的错误报告存储到 Web 存储中,并在完成错误报告时将其发送到服务器。在这种情况下,用户可以灵活地随时回到处理其错误报告,而不必担心再次从头开始。

现在我们知道了 Web 存储提供的好处,让我们看看如何利用 Web 存储的使用。

使用本地 Web 存储

使用 HTML 5 的本地 Web 存储非常容易,因为它提供了许多 API 来与 Web 存储交互。因此,让我们不浪费时间,看一下我们如何使用本地 Web 存储的一个简单例子。为此,我们将创建一个名为localstore.js的简单 JavaScript 文件,内容如下:

// check if the localStorage is supported by the browser or notif(localStorage) {  // Put some contents inside the local storagelocalStorage.setItem("username", "joe_henry");  localStorage.setItem("uid", "28372");    // Retrieve some contents from the local storage  var user_email = localStorage.getItem("user_email");} else {  alert("The browser does not support local web storage");}

这是...

使用会话存储

使用本地存储同样简单,会话存储也不会增加任何复杂性。例如,让我们看看将我们的localStorage示例轻松转换为sessionStorage有多容易:

// check if the sessionStorage is supported by the browser or not
if(sessionStorage) {
  // Put some contents inside the local storage
sessionStorage.setItem("username", "joe_henry");
  sessionStorage.setItem("uid", "28372");

  // Retrieve some contents from the session storage
  var user_email = sessionStorage.getItem("user_email");
} else {
  alert("The browser does not support session web storage");
}

从这个例子可以明显看出,从本地存储转移到会话存储非常容易,因为这两种存储选项都提供了类似的存储 API,唯一的区别在于存储中的数据保留时间有多长。

通过了解如何优化前端以提供完全可扩展和响应的企业 Web 应用程序,现在是时候我们访问一些确保我们构建的内容安全并符合预期的企业应用程序开发方面的内容,而不会带来意外惊喜。

摘要

在本章的过程中,我们了解了为企业应用程序拥有优化的前端的重要性,以及前端如何影响我们在企业内部使用应用程序。然后,我们继续了解通常会影响 Web 前端性能的问题类型,以及我们可以采取哪些可能的解决方案来改进应用程序前端。这包括减少前端加载的资源量,允许 CSS 并行加载,捆绑 JavaScript 等。然后,我们继续了解缓存如何在考虑企业 Web 应用程序的使用情况下证明是有用的。一旦我们了解了缓存的概念,我们就进入了领域...

问题

  1. CDN 的使用如何提高前端性能?

  2. 我们能做些什么让浏览器利用现有的连接从服务器加载资源吗?

  3. 我们如何从 Web 存储中删除特定键或清除 Web 存储的内容?

第八章:编写可测试的代码

通过本章,我们已经进入了本书的第二部分,涵盖了使用 Python 开发企业级应用程序的过程。而本书的第一部分侧重于如何构建具有可扩展性和性能的企业级应用程序,本书的第二部分侧重于应用程序的内部开发方面,例如我们如何确保我们的应用程序是安全的,它的性能如何,以及如何在生产阶段进行更高质量的检查,以最大程度地减少意外行为的发生。

在本章中,我们想要把您的注意力集中在企业应用程序开发或者说...

技术要求

本书中的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter08目录下找到。

与第六章中开发的 bugzot 应用程序相关的代码示例可以在chapter06目录下找到。

可以通过运行以下命令来克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

这包括了关于如何运行代码的说明。除此之外,本章还需要安装一个 Python 库,它可以简化我们的测试代码编写。可以通过运行以下命令来安装该库和所有相关依赖项:

pip install -r requirements.txt

测试的重要性

作为开发人员,我们通常致力于解决具有挑战性的问题,试图穿越复杂的连接,并提出解决方案。但是我们有多少次关心过我们的代码可能失败以提供预期的结果的所有可能方式?尽管我们作为开发人员自己编写的东西很难去尝试破坏,但它构成了开发周期中最重要的方面之一。

这是测试成为开发生命周期中重要方面的时候。应用程序测试的目标可以通过回答以下问题来总结:

  • 代码中的个别组件是否按预期执行?

  • 代码的流程是否从...

不同种类的测试

当重点是交付质量应用程序时,无论是为一般客户还是企业,都需要执行多种测试。这些测试技术可能从应用程序开发生命周期的不同阶段开始,因此被相应地分类。

在本节中,我们不是专注于一些可以归为黑盒测试和白盒测试的测试方法,而是更加专注于理解与开发人员相关的术语。所以,让我们来看一下。

单元测试

当我们开始构建应用程序时,我们将应用程序划分为多个子模块。这些子模块包含了许多相互交互的类或方法,以实现特定的输出。

为了生成正确的输出,所有个别类和方法都需要正常工作,否则结果将有所不同。

现在,当我们的目标是检查代码库中个别组件的正确性时,我们通常编写针对这些个别组件的测试,独立于应用程序的其他组件。这种测试,其中一个个别组件独立于其他组件进行测试,被称为单元测试

简而言之,以下是一些...

集成测试

一个应用程序不仅仅是在所有单独的组件都编写完成后就完成了。为了产生任何有意义的输出,这些单独的组件需要根据提供的输入类型以不同的方式相互交互。为了完全检查应用程序代码库,组成应用程序的组件不仅需要在隔离状态下进行测试,而且在它们相互交互时也需要进行测试。

集成测试是在应用程序经过单元测试阶段后开始的。在集成测试中,通过接口使单个组件相互交互,然后测试这种交互是否符合预期的结果。

在集成测试阶段,不仅测试应用程序组件之间的交互,还测试组件与任何其他外部服务(如第三方 API 和数据库)之间的交互。

简而言之,以下是一些集成测试的特点:

  • 专注于测试接口:由于应用程序的不同组件通过组件提供的接口相互交互,集成测试的作用是验证这些接口是否按预期工作。

  • 通常在单元测试之后开始:一旦组件通过了单元测试,它们就会被集成在一起相互连接,然后进行集成测试

  • 代码流测试:与单元测试相反,单元测试通常侧重于从一个组件到另一个组件的数据流,因此还检查代码流的结果

正如我们所看到的,集成测试是应用测试过程的重要组成部分,其目的是验证应用程序的不同组件是否能够正确地相互交互。

一旦集成测试完成,测试过程的下一个阶段是进行系统测试,然后是最终的验收测试阶段。以下图片显示了从单元测试阶段到验收测试阶段的测试流程以及在应用程序开发过程中可能发生的不同类型的测试。

为了控制本书的长度,我们将跳过对这两种测试技术的解释,而是将本章的其余部分专注于实施一些实际的单元测试。

在本章的其余部分,我们将专注于单元测试实践以及如何在我们的演示应用程序中实现它们。

以测试为导向构建应用程序

因此,我们现在知道测试很重要,也了解了不同类型的测试。但是在构建应用程序时,我们需要做一些重要的事情,以便能够正确地测试它吗?

对这个问题的答案有点复杂。虽然我们可以轻松地按照任何我们想要的特定方式编写代码,并通过多种程序进行测试,例如单元测试,但最好还是遵循一般的一套指南,以便能够轻松高效地测试代码。因此,让我们继续看一下这些指南:

  • 每个组件都应该有一个责任:为了测试的高效性和覆盖率...

测试驱动开发

测试驱动开发是一种软件开发过程,其中软件开发的过程首先涉及为单个需求编写测试,然后构建或改进能够通过这些测试的方法。这种过程通常有利于生成具有比在组件开发后编写测试时更少缺陷的应用程序。

在测试驱动开发过程中,遵循以下步骤:

  1. 添加一个测试:一旦需求被指定,开发人员就会开始为先前组件的改进或新组件编写新的测试。这个测试设置了特定组件的预期结果。

  2. 运行测试以查看新测试是否失败:当新测试被添加后,对代码运行测试,以查看新测试是否因预期原因而失败。这可以确保测试按预期工作,并且在不利条件下不会通过。

  3. 编写/修改组件:一旦测试运行并且可以看到预期结果,我们就会继续编写新组件或修改现有组件,以使新添加的测试用例通过。

  4. 运行测试:一旦对测试进行了必要的修改以使测试通过,就会再次运行测试套件,以查看之前失败的测试现在是否通过。这可以确保修改按预期工作。

  5. 重构:随着应用程序开发生命周期的进展,会有时候会出现重复的测试或者可能承担相同责任的组件。为了消除这些问题,需要不断进行重构以减少重复。

现在,我们已经对测试在任何成功应用程序的开发中扮演重要角色有了相当的了解,也知道如何编写易于测试的代码。现在,是时候开始为我们在第六章中构建的应用程序编写一些测试了。

编写单元测试

因此,现在是时候开始编写我们的单元测试了。Python 库为我们提供了许多编写测试的选项,而且非常容易。我们通常会被选择所困扰。该库本身提供了一个单元测试模块,可用于编写单元测试,而且我们可以使用各种框架来更轻松地编写单元测试。

因此,让我们首先看一下如何使用 Python 的unittest模块编写一些简单的单元测试,然后我们将继续使用著名的 Python 测试框架为我们的应用程序编写单元测试。

使用 Python unittest 编写单元测试

Python 3 提供了一个非常好的、功能齐全的库,允许我们为应用程序编写单元测试。这个名为unittest的库用于编写单元测试,可以从非常简单的测试的复杂性到在运行单元测试之前进行适当设置的非常复杂的测试。

Python unittest库支持的一些功能包括:

  • 面向对象:该库以面向对象的方式简化了单元测试的编写。这意味着,通过类和方法以面向对象的形式编写对象。这绝不意味着只有面向对象的代码才能使用该库进行测试。该库支持测试面向对象和非面向对象的代码。

  • 测试夹具的能力:一些测试可能需要在运行测试之前以某种方式设置环境,并在测试完成执行后进行适当的清理。这称为测试夹具,Python 的unittest库完全支持这一特性。

  • 能够编写测试套件:该库提供了编写完整功能的测试套件的功能,由多个测试用例组成。测试套件的结果被汇总并一次性显示。

  • 内置测试运行器:测试运行器用于编排测试并编译执行测试的结果以生成报告。该库提供了一个内置的测试运行器来实现这个功能。

现在,让我们看一下以下代码,我们将使用它来编写我们的单元测试:

import hashlib
import secrets

def strip_password(password):
    """Strip the trailing and leading whitespace.

    Returns:
        String
    """
    return password.strip()

def generate_salt(num_bytes=8):
    """Generate a new salt

    Keyword arguments:
    num_bytes -- Number of bytes of random salt to generate

    Returns:
        Bytes
    """

    return secrets.token_bytes(num_bytes)

def encrypt_password(password, salt):
    """Encrypt a provided password and return a hash.

    Keyword arguments:
    password -- The plaintext password to be encrypted
    salt -- The salt to be used for padding

    Returns:
        String
    """

    passwd_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 10000).hex()
    return passwd_hash

在这段代码中,我们定义了一些函数,旨在帮助我们生成可以安全存储在数据库中的密码哈希。

现在,我们的目标是利用 Python 的unittest库为前面的代码编写一些单元测试。

以下代码旨在为密码助手模块实现一小组单元测试:

from helpers import strip_password, encrypt_password
import unittest

class TestPasswordHelpers(unittest.TestCase):
    """Unit tests for Password helpers."""

    def test_strip_password(self):
        """Test the strip password function."""

        self.assertEqual(strip_password(' saurabh '), 'saurabh')

    def test_encrypt_password(self):
        """Test the encrypt password function."""

        salt = b'\xf6\xb6(\xa1\xe8\x99r\xe5\xf6\xa5Q\xa9\xd5\xc1\xad\x08'
        encrypted_password = '2ba31a39ccd2fb7225d6b1ee564a6380713aa94625e275e59900ebb5e7b844f9'

        self.assertEqual(encrypt_password('saurabh', salt), encrypted_password)

if __name__ == '__main__':
    unittest.main()

我们创建了一个简单的文件来运行我们的单元测试。现在,让我们看看这个文件做了什么。

首先,我们从所需模块中导入我们要测试的函数。在本例中,我们将从名为helpers.py的文件中导入这些函数。接下来的导入让我们获得了 Python 的 unittest 库。

一旦我们导入了所需的内容,下一步就是开始编写单元测试。为此,我们首先定义一个名为TestPasswordHelpers的类,它继承自unittest.TestCase类。该类用于定义我们可能想要执行的一组测试用例,如下所示:

class TestPasswordHelpers(unittest.TestCase):

在类定义内部,我们继续为我们想要测试的方法定义单独的测试用例。定义测试用例的方法必须以单词test开头,以表明这个特定的方法是一个测试,并且需要由测试运行器执行。例如,负责测试我们的strip_password方法的方法被命名为test_strip_password()

def test_strip_password(self):

在方法定义内部,我们使用断言来验证特定方法的输出是否符合我们的预期。例如,assertEqual方法用于断言参数 1 是否与参数 2 匹配:

self.assertEqual(strip_password(' saurabh '), 'saurabh')

一旦这些测试被定义,下一步就是定义一个入口点,用于在终端运行我们的测试文件。这是通过从入口点调用unittest.main()方法来完成的。一旦调用完成,文件中提到的测试用例将被运行,并显示输出,如下所示:

python helpers_test.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.020s

OK

当你想要用 Python 编写单元测试时,这是最简单的方法。现在,是时候转向更重要的事情了。让我们为我们的演示应用程序编写一些单元测试。

使用 pytest 编写单元测试

正如我们讨论的那样,在 Python 中编写单元测试可以利用我们手头的许多选项。例如,在前一节中,我们利用了 Python 的unittest库来编写我们的单元测试。在本节中,我们将继续使用pytest来编写单元测试,这是一个用于编写应用程序单元测试的框架。

但是,pytest提供了哪些好处,使我们应该转向它呢?为什么我们不能坚持使用 Python 捆绑的unittest库呢?

尽管unittest库为我们提供了许多灵活性和易用性,但pytest仍然带来了一些改进,让我们来看看这些改进是什么:

设置 pytest

pytest框架是一个独立的框架,作为标准化 Python 发行版之外的一个单独库。在我们开始使用pytest编写测试之前,我们需要安装pytest。安装pytest并不是一件大事,可以通过运行以下命令轻松完成:

pip install pytest

现在,在我们开始为应用程序编写测试之前,让我们首先在应用程序目录下创建一个名为tests的新目录,并与run.py位于同一级别,以存储这些测试用例,通过运行以下命令来创建:

mkdir -p bugzot/tests

现在,是时候用pytest编写我们的第一个测试了。

使用 pytest 编写我们的第一个测试

在我们的演示应用程序中,我们定义了一些模型,用于在数据库中存储数据。作为我们的第一个测试,让我们着手编写一个针对我们模型的测试用例。

以下代码片段显示了我们的User模型的简单测试用例:

'''File: test_user_model.pyDescription: Tests the User database model'''import sysimport pytest# Setup the import path for our applicationsys.path.append('.') # Add the current rootdir as the module path# import our bugzot model we want to testfrom bugzot.models import User@pytest.fixture(scope='module')def create_user():  user = User(username='joe', email='joe@gmail.com', password='Hello123')  return userdef test_user_creation(create_user): assert create_user.email ...

使用 pytest 编写功能测试

pytest框架以及其独特的装置和flask的强大功能,使我们能够轻松地为我们的应用程序编写功能测试。这使我们能够相当轻松地测试我们构建的 API 端点。

让我们看一下我们的索引 API 端点的一个示例测试,然后我们将深入了解我们如何编写测试。

以下代码片段显示了使用pytest编写的简单测试用例,用于测试索引 API 端点:

'''
File: test_index_route.py
Description: Test the index API endpoint
'''
import os
import pytest
import sys
import tempfile

sys.path.append('.')
import bugzot

@pytest.fixture(scope='module')
def test_client():
  db, bugzot.app.config['DATABASE'] = tempfile.mkstemp()
  bugzot.app.config['TESTING'] = True
  test_client = bugzot.app.test_client()

  with bugzot.app.app_context():
    bugzot.db.create_all()

  yield test_client

  os.close(db)
  os.unlink(bugzot.app.config['DATABASE'])

def test_index_route(test_client):
  resp = test_client.get('/')
  assert resp.status_code == 200

这是我们为测试我们的索引 API 路由编写的一个非常简单的功能测试,以查看它是否正常工作。现在,让我们看看我们在这里做了什么来使这个功能测试起作用:

前几行代码或多或少是通用的,我们导入了一些构建测试所需的库。

有趣的工作从我们构建的test_client()装置开始。这个装置用于为我们获取一个基于 flask 的测试客户端,我们可以用它来测试我们的应用程序端点,以查看它们是否正常工作。

由于我们的应用程序是一个面向数据库的应用程序,需要数据库才能正常运行,我们需要为应用程序设置数据库配置。为了测试目的,我们可以使用在大多数操作系统中都可以轻松创建的 SQLite3 数据库。以下调用为我们提供了我们将用于测试目的的数据库:

db, bugzot.app.config['DATABASE'] = tempfile.mkstemp()

调用返回一个文件描述符到数据库和一个 URI,我们将其存储在应用程序配置中。

数据库创建完成后,下一步是告诉我们的应用程序它正在测试环境中运行,以便禁用应用程序内部的错误处理,以改善测试的输出。这很容易通过将应用程序配置中的TESTING标志设置为True来实现。

Flask 为我们提供了一个简单的测试客户端,我们可以使用它来运行应用程序测试。通过调用应用程序的test_client()方法可以获得此客户端,如下所示:

test_client = bugzot.app.test_client()

一旦获得了测试客户端,我们需要设置应用程序上下文,这是通过调用 Flask 应用程序的app_context()方法来实现的。

建立应用程序上下文后,我们通过调用db.create_all()方法创建我们的数据库。

一旦我们设置好应用程序上下文并创建了数据库,接下来要做的是开始测试。这是通过产生测试客户端来实现的:

yield test_client

完成后,测试现在执行,控制权转移到test_index_route()方法,我们只需尝试通过调用test_clientget方法加载索引路由,如下所示:

resp = test_client.get('/')

完成后,我们通过检查响应的 HTTP 状态码并验证其是否为200来检查 API 是否提供了有效的响应,如下所示:

assert resp.status_code == 200

一旦测试执行完成,控制权就会转移到测试装置,我们通过关闭数据库文件描述符和删除数据库文件来进行清理,如下所示:

os.close(db)
os.unlink(bugzot.app.config['DATABASE'])

相当简单,不是吗?这就是我们如何使用pytestFlask编写简单的功能测试。我们甚至可以编写处理用户身份验证和数据库修改的测试,但我们将把这留给您作为读者来练习。

总结

在本章中,我们看到测试如何成为应用开发项目中重要的一部分,以及为什么它是必要的。在这里,我们看了通常在开发生命周期中使用的不同类型的测试以及不同技术的用途。然后,我们继续看如何以一种使测试变得简单和有效的方式来编写我们的代码。接着,我们开始深入研究 Python 语言,看看它提供了哪些用于编写测试的工具。在这里,我们发现了如何使用 Python 的 unittest 库来编写单元测试,以及如何运行它们。然后,我们看了如何利用像 pytest 这样的测试框架来编写测试用例...

问题

  1. 单元测试和功能测试之间有什么区别?

  2. 我们如何使用 Python 的 unittest 编写单元测试套件?

  3. 在 pytest 中,固定装置的作用是什么?

  4. 在编写固定装置时,pytest 中的作用域是什么?

第九章:为性能分析应用程序

在本书的过程中,我们已经看到了应用程序的性能和可扩展性在企业环境中有多么重要;考虑到这一点,我们在本书的相当大部分内容中致力于理解如何构建一个不仅性能良好而且可扩展的应用程序。

到目前为止,我们只是看到了一些构建高性能和可扩展应用程序的最佳实践,但并不知道如何找出我们应用程序中特定代码的执行速度慢以及可能导致它的原因。

对于任何企业级应用程序,提高其性能和可扩展性是一个持续的过程,因为应用程序的用户群不断增长,应用程序的…

技术要求

本书中的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter09目录下找到。

与 bugzot 示例应用程序的性能分析和基准测试相关的代码示例可以在代码库的测试模块下的chapter06目录中找到。

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

这一章还依赖于第三方 Python 库,可以通过在开发系统上运行以下命令轻松安装:

pip install memory_profiler

性能瓶颈的幕后

在应用程序进入开发阶段之前,会对应用程序应该做什么、如何做以及应用程序需要与之交互的第三方组件的类型进行彻底讨论。一旦所有这些都确定下来,应用程序就进入了开发阶段,在这个阶段,开发人员负责以尽可能高效的方式构建应用程序,以便应用程序执行的任务可以以最有效的方式完成。这种效率通常是以应用程序完成提供的任务所需的时间和应用程序在执行该任务时使用的资源数量来衡量的。

当应用程序部署到生产环境时,…

查看性能瓶颈的原因

通常,性能瓶颈可能是由许多因素引起的,这些因素可能包括部署应用程序的环境中物理资源的短缺,或者在处理特定工作负载时选择了不好的算法,而实际上有更好的算法可用。让我们看看可能导致部署应用程序性能瓶颈的一些可能问题:

  • 没有足够的硬件资源: 最初,性能和可扩展性的大部分瓶颈是由于对运行应用程序所需的硬件资源的规划不足。这可能是由于估计不正确或应用程序用户群突然不经意地激增。当这种情况发生时,现有的硬件资源会受到压力,系统会变慢。

  • 不正确的设计选择: 在第二章中,设计模式-做出选择,我们看到了对于任何企业级应用程序来说,设计选择是多么重要。不断为某些事情分配新对象,而本可以通过分配一个共享对象来完成,这将影响应用程序的性能,不仅会给可用资源带来压力,还会因为重复分配对象而导致不必要的延迟。

  • 低效的算法: 在处理大量数据或进行大量计算以生成结果的系统中,由于选择了低效的算法,性能可能会下降。仔细研究可用的替代算法或现场算法优化的可用性可能有助于提高应用程序的性能。

  • 内存泄漏: 在大型应用程序中,可能会出现内存泄漏的地方。尽管在 Python 等垃圾收集语言中这很困难,但仍然有可能发生。有时候,尽管对象不再使用,但由于它们在应用程序中的映射方式,它们仍然没有被垃圾收集。随着运行时间的延长,这将导致可用内存减少,并最终使应用程序停止运行。

这些是系统中性能瓶颈发生的几个原因。对于我们作为软件开发人员来说,幸运的是,我们有许多工具可以帮助我们找出瓶颈,以及发现诸如内存泄漏之类的问题,甚至只是对个别部分的内存使用进行分析。

有了关于为什么会出现性能瓶颈的知识,现在是时候学习如何在应用程序中寻找这些性能瓶颈,然后尝试理解我们可以减少它们影响的一些方法了。

探测应用程序的性能问题

性能是任何企业级应用程序的关键组成部分,您不能容忍应用程序经常变慢并影响整个组织的业务流程。不幸的是,性能问题也是最难理解和调试的问题之一。这种复杂性是因为没有标准的方法来访问应用程序中特定代码片段的性能,而且一旦应用程序开发完成,就需要理解代码的完整流程,以便找出可能导致特定性能问题的可能区域。

作为开发人员,我们可以通过以这种方式构建应用程序来减少这些困难...

编写性能基准测试

让我们从讨论开始,讨论我们作为软件开发人员如何构建应用程序,以帮助我们在开发周期的早期阶段标记性能瓶颈,以及如何在调试这些瓶颈方面使我们的生活变得更加轻松。

在应用程序开发周期中,我们可以做的第一件最重要的事情是为应用程序的各个组件编写基准测试。

基准测试是简单的测试,旨在通过多次迭代执行代码并计算这些迭代中执行代码所需的时间的平均值来评估特定代码片段的性能。您还记得听说过一个名为 Pytest 的库吗?我们在第八章中用它来编写单元测试,编写可测试的代码吗?

我们将利用相同的库来帮助我们编写性能基准测试。但是,在我们可以让 Pytest 用于编写基准测试之前,我们需要让它理解基准测试的概念,这在 Python 中非常容易,特别是因为有一个庞大的 Python 生态系统可用。为了让 Pytest 理解基准测试的概念,我们将导入一个名为pytest-benchmark的新库,它为 Pytest 添加了基准测试固定装置,并允许我们为我们的应用程序编写基准测试。为此,我们需要运行以下命令:

pip install pytest-benchmark

一旦我们安装了库,我们就可以为我们的应用程序编写性能基准测试了。

编写我们的第一个基准测试

安装所需的库后,现在是时候为我们的第一个性能基准测试编写了。为此,我们将使用一个简单的示例,然后继续了解如何为我们的应用程序编写基准测试:

'''File: sample_benchmark_test.pyDescription: A simple benchmark test'''import pytestimport timedef sample_method():  time.sleep(0.0001)  return 0def test_sample_benchmark(benchmark):  result = benchmark(sample_method)  assert result == 0if __name__ == '__main__':  pytest.main()

我们已经编写了我们的第一个基准测试。确实是一个非常简单的测试,但有很多事情我们需要了解,以便看清我们在这里做什么:

首先,当我们开始编写基准测试时,我们导入...

编写 API 基准测试

有了这个,我们知道如何编写一个简单的基准测试。那么,我们如何为我们的 API 编写类似的东西呢?让我们看看如何修改我们用于验证索引 API 端点功能的 API 测试之一,并看看如何在其中运行基准测试。

以下代码修改了我们现有的索引 API 测试用例,包括 API 的基准测试:

'''
File: test_index_benchmark.py
Description: Benchmark the index API endpoint
'''
import os
import pytest
import sys
import tempfile

sys.path.append('.')
import bugzot

@pytest.fixture(scope='module')
def test_client():
  db, bugzot.app.config['DATABASE'] = tempfile.mkstemp()
  bugzot.app.config['TESTING'] = True
  test_client = bugzot.app.test_client()

  with bugzot.app.app_context():
    bugzot.db.create_all()

  yield test_client

  os.close(db)
  os.unlink(bugzot.app.config['DATABASE'])

def test_index_benchmark(test_client, benchmark):
  resp = benchmark(test_client.get, "/")
  assert resp.status_code == 200

在上述代码中,我们只需添加一个名为test_index_benchmark()的新方法,它接受两个 fixture 作为参数。其中一个 fixture 负责设置我们的应用程序实例,第二个 fixture——基准 fixture——用于在客户端 API 端点上运行基准测试并生成结果。

另一个重要的事情要注意的是,我们如何能够将单元测试代码与基准测试代码混合在一起,这样我们就不需要为每个测试类编写两种不同的方法;所有这些都是由 Pytest 实现的,它允许我们在方法上运行基准测试,并允许我们通过单个测试方法验证被测试的方法是否提供了正确的结果。

现在我们知道如何在应用程序中编写基准测试。但是,如果我们需要调试某些慢的东西,但基准操作并没有引发任何关注,我们该怎么办呢?幸运的是,Python 提供了许多选项,允许我们测试代码内部可能发生的任何性能异常。因此,让我们花一些时间来了解它们。

进行组件级性能分析

使用 Python,许多设施都是内置的,其他设施可以很容易地使用第三方库实现。因此,让我们看看 Python 为我们提供了哪些用于运行组件级性能分析的功能。

使用 timeit 测量慢操作

Python 提供了一个非常好的模块,称为timeit,我们可以使用它来对代码的小片段运行一些简单的时间分析任务,或者了解特定方法调用所花费的时间。

让我们来看一个简单的脚本,向我们展示了如何使用timeit来了解特定方法所花费的时间,然后我们将更多地了解如何使用timeit提供的功能来运行我们打算构建的应用程序的时间分析。

以下代码片段展示了在方法调用上运行timeit进行时间分析的简单用法:

import timeit

def calc_sum():
    sum = 0
    for i in range(0, 100):
        sum = sum + i
    return sum

if __name__ == '__main__':
    setup = "from __main__ import calc_sum"
    print(timeit.timeit("calc_sum()", setup=setup))

运行此文件后,我们得到的输出如下:

7.255408144999819

正如我们从上面的示例中看到的,我们可以使用timeit来对给定方法的执行进行简单的时间分析。

现在,这很方便,但是当我们需要对多个方法进行计时时,我们不能一直编写多个设置语句。在这里我们该怎么办呢?应该有一种简单的方法来实现这一点。

那么,我们可以创建一个简单的装饰器,用于对可能需要时间分析的方法进行计时。

让我们创建这个简单的装饰器方法。以下示例向我们展示了如何编写一个装饰器方法,以便以后在我们的方法上进行时间比较:

import time
def time_profile(func):
  """Decorator for timing the execution of a method."""
  def timer_func(*args, **kwargs):
    start = time.time()
    value = func(*args, **kwargs)
    end = time.time()
    total_time = end – start
    output_msg = "The method {func} took {total_time} to execute"
    print(output_msg.format(func=func, total_time=total_time))
    return value
  return timer_func

这是一个我们创建的装饰器。在装饰器内部,我们将要分析的函数作为参数传入,以及传递给它的任何参数。现在,我们初始化函数的开始时间,然后调用函数,然后在函数返回执行后存储调用的结束时间。基于此,我们计算函数执行所花费的总时间。

但是我们如何使用这个装饰器来分析我们的方法呢?以下示例展示了一个示例:

@time_profile
def calc_sum():
    sum = 0
    for i in range(100):
        sum = sum+i
    return sum

这非常简单,比一遍又一遍地导入单个方法进行时间分析要容易得多。

因此,我们的timeit方法是一个非常简单的方法,可以为我们提供有关特定方法执行所花费的时间的一些基本信息。我们甚至可以使用这些方法对单个语句进行分析。但是,如果我们想要更详细地了解特定方法内部单个语句花费了多少时间,或者了解是什么导致了给定方法变慢,我们的简单计时解决方案就不是一个理想的选择。我们需要更复杂的东西。

事实上,Python 为我们提供了一些内置的分析器,我们可以使用它们来对应用程序进行深入的性能分析。让我们看看如何做到这一点。

使用 cProfile 进行分析

Python 库为我们提供了一个应用程序分析器,可以通过它轻松地对整个应用程序以及应用程序的各个组件进行分析,从而简化开发人员的工作。

Profile 是一个内置的代码分析器,作为一些 Python 发行版的模块捆绑在一起。该模块能够收集有关已进行的单个方法调用的信息,以及对第三方函数的任何调用进行分析。

一旦收集了这些细节,该模块将为我们提供大量统计信息,可以帮助我们更好地了解组件内部发生了什么。在我们深入了解收集和表示的细节之前,...

使用 memory_profiler 进行内存使用分析

内存分析是应用程序性能分析的一个非常重要的方面。在构建应用程序时,有些地方我们可能会实现处理动态分配对象的不正确机制,因此可能会陷入这样一种情况:这些不再使用的对象仍然有一个指向它们的引用,从而阻止了垃圾收集器对它们的回收。

这导致应用程序内存使用随时间增长,导致应用程序在系统耗尽可分配给应用程序执行其常规活动所需的内存时停止运行。

现在,为了解决这些问题,我们不需要一个能帮助我们分析应用程序调用堆栈并提供有关单个调用花费了多少时间的分析器。相反,我们需要的是一个能告诉我们应用程序的内存趋势的分析器,比如单个方法可能消耗多少内存,以及随着应用程序继续运行,内存如何增长。

这就是memory_profiler发挥作用的地方,它是一个第三方模块,我们可以轻松地将其包含在我们的应用程序中以进行内存分析。但是,在深入了解如何使用memory_profiler之前,我们需要先将该模块引入我们的开发环境。以下代码行将所需的模块引入我们的开发环境:

pip install memory_profiler

一旦内存分析器被获取到开发环境中,我们现在可以开始使用它了。让我们看一个示例程序,并了解如何使用memory_profiler来了解我们应用程序的内存使用模式。

以下代码片段向我们展示了如何使用memory_profiler的示例:

from memory_profiler import profile

@profile
def calc_sum():
    sum = 0
    for i in range(100):
        sum = sum + i
    print(str(sum))

if __name__ == '__main__':
    calc_sum()

现在,代码已经就位,让我们试着理解我们在这里做了什么。

首先,我们导入了一个名为 profile 的装饰器,它是由memory_profiler库提供的。这个装饰器用于通知memory_profiler需要对内存使用情况进行分析的方法。

要为方法启用内存分析,我们只需要使用装饰器装饰该方法。例如,在我们的示例应用程序代码中,我们使用装饰器装饰了calc_sum()方法。

现在,让我们运行我们的示例代码,并通过运行以下命令查看输出结果:

python3 memory_profile_example.py

一旦执行了命令,我们会得到以下输出:

4950
Filename: memory_profile.py

Line # Mem usage Increment Line Contents
================================================
     3 11.6 MiB 11.6 MiB @profile
     4 def calc_sum():
     5 11.6 MiB 0.0 MiB sum = 0
     6 11.6 MiB 0.0 MiB for i in range(100):
     7 11.6 MiB 0.0 MiB sum = sum + i
     8 11.6 MiB 0.0 MiB print(str(sum))

从上述输出中可以看出,我们得到了有关该方法的内存分配的详细统计信息。输出为我们提供了有关应用程序使用了多少内存以及每个步骤导致应用程序增加了多少内存的信息。

现在,让我们举一个例子,看看当一个方法调用另一个方法时内存分配如何改变。以下代码展示了这一点:

from memory_profiler import profile

@profile
def calc_sum():
    sum = 0
    for i in range(100):
        sum = sum + i
    say_hello()
    print(str(sum))

def say_hello():
    lst = []
    for i in range(10000):
        lst.append(i)

if __name__ == '__main__':
    calc_sum()

执行上述代码后,我们得到以下输出:

Line # Mem usage Increment Line Contents
================================================
     3 11.6 MiB 11.6 MiB @profile
     4 def calc_sum():
     5 11.6 MiB 0.0 MiB sum = 0
     6 11.6 MiB 0.0 MiB for i in range(100):
     7 11.6 MiB 0.0 MiB sum = sum + i
     8 11.7 MiB 0.1 MiB say_hello()
     9 11.7 MiB 0.0 MiB print(str(sum))

正如我们所看到的,当调用say_hello()方法时,调用导致内存使用量增加了 0.1 MB。如果我们怀疑代码中可能存在内存泄漏,这个库就非常方便。

收集实时性能数据

到目前为止,我们已经看到了在需要时如何使用不同的性能分析工具来分析应用程序的性能,以帮助我们找出代码的哪一部分导致了性能瓶颈。但是,我们如何知道一个操作是否花费的时间比应该花费的时间长?

其中一个答案可能是用户报告的响应时间慢,但这可能有很多因素,可能只涉及用户端的减速。

我们可以使用一些其他机制来实时监控应用程序的性能问题。因此,让我们看看其中一种方法,它允许我们收集有关单个操作所需时间的信息...

记录性能指标

在应用程序中,可能有几个步骤。可以通过使用不同的工具来分析每个步骤的性能。其中最基本的工具之一是日志记录。在这种情况下,我们收集不同方法的执行时间,并将其记录在日志文件中。

以下代码片段展示了如何在我们在第六章中构建的演示应用程序中实现这一点,示例-构建 BugZot

@app.before_request
def before_request_handler():
    g.start_time = time.time()

@app.teardown_request
def teardown_request_handler(exception=None):
    execution_time = time.time() - g.start_time
    app.logger.info("Request URL: {} took {} seconds".format(request.url, str(execution_time)))

这是一个简单的代码,记录了请求中调用的每个 API 端点的执行时间。我们在这里做的非常简单。我们首先创建一个before_request处理程序,在 flask 全局命名空间中初始化一个属性start_time。一旦完成这一步,请求就被发送到处理程序。一旦请求被处理,它就会进入我们定义的teardown处理程序。

一旦请求到达这个teardown处理程序,我们计算处理请求所需的总时间,并将其记录在应用程序日志中。

这种方法允许我们查询或处理我们的日志文件,了解每个请求所需的时间以及哪些 API 端点花费了最长的时间。

避免性能瓶颈

在过去的几个部分中,我们看了一下我们可以对应用程序进行性能分析的不同方式,以便解决可能导致性能下降或内存泄漏的性能瓶颈。但是一旦我们意识到这些问题以及它们发生的原因,我们还有哪些其他选项可以防止它们再次发生呢?

幸运的是,我们有一些有用的准则可以帮助防止性能瓶颈,或者可以限制这些瓶颈可能产生的影响。因此,让我们看看其中一些准则:

  • 选择正确的设计模式:设计模式在应用程序中是一个重要的选择。例如,日志对象不需要在应用程序的每个子模块中重新初始化...

总结

在本章中,我们看到应用程序的性能是软件开发中的重要方面,通常会导致应用程序出现性能瓶颈的问题。接下来,我们看了一下我们可以对应用程序进行性能分析的不同方式。首先,这涉及编写单个组件以及单个 API 的基准测试,然后转向更具体的组件级分析,我们看了不同的组件分析方法。这些分析技术包括使用 Python 的timeit模块对方法进行简单的时间分析,然后我们转向使用更复杂的技术,使用 Python cProfile 并进行内存分析。在我们的旅程中,我们还看了一下使用日志技术来帮助我们评估慢请求的一些主题。最后,我们看了一些通用原则,可以帮助我们预防应用程序内的性能瓶颈。

在下一章中,我们将看一下保护应用程序的重要性。如果不这样做,不仅会为严重的数据窃取铺平道路,还会产生许多责任,并可能侵蚀用户的信任。

问题

  1. 应用部署时可能导致性能瓶颈的因素有哪些?

  2. 我们可以通过哪些不同的方式来对方法进行时间分析?

  3. 什么可能导致 Python 中的内存泄漏,Python 是一种垃圾收集语言?

  4. 我们如何对 API 响应进行分析,找出其减慢的原因?

  5. 选择错误的设计模式会导致性能瓶颈吗?

第十章:保护您的应用程序

在关于应用程序性能和可扩展性的讨论中,以及确保应用程序在企业环境中稳定的最佳实践中,我们已经涵盖了很多内容。我们了解到用户体验对于使应用程序在企业内部成功非常重要。但是你认为我们在这里漏掉了什么吗?

假设我们拥有构建成功企业应用程序的所有组件,并且能够使其扩展,同时为用户提供符合预期行为的响应时间。然而,任何人都可以轻易访问我们应用程序的记录。如果存在漏洞允许用户在不进行登录的情况下从应用程序中获取敏感数据怎么办?是的,这就是缺失的环节:应用程序安全。在企业内部,应用程序的安全性是一个非常重要的因素。一个不安全的应用程序可能会向未预期的方面泄露敏感和机密数据,并且还可能给组织带来法律上的麻烦。

应用程序安全是一个大课题,即使是一本 500 页的书也可能不足以深入涵盖这个主题。但在本章的过程中,我们将快速介绍如何处理应用程序安全,让我们的用户在使用我们的应用程序时感到安全。

作为读者,在本章结束时,您将学到以下内容:

  • 企业应用程序安全的重要性

  • 用于突破应用程序安全的不同类型攻击向量

  • 导致泄露的应用程序开发常见错误

  • 确保您的应用程序安全

技术要求

对于本章,我们期望用户具有基本的配置 Web 服务器和网络通信基础知识。

企业应用程序安全

应用程序安全是一个如此重要的课题,您可能会讨论如何防止机密数据泄露,以及使应用程序足够强大以应对篡改攻击。

在企业中,这个话题变得更加严肃。这是因为大多数企业处理大量个人数据,其中可能包括可用于识别个人用户或与其财务详情相关的信息,例如信用卡号码、CVV 码或支付记录。

大多数企业都会花费大量资金来提高业务安全性,因为他们无法承受链条中的薄弱环节可能导致机密信息泄露的风险。一旦发生泄露,对组织的影响将从对未能维护机密数据安全的组织处以罚款开始,一直延伸到失去信任可能导致组织破产。

安全性不是闹着玩的,也没有一种解决方案适用于所有情况。相反,为了使事情变得更加复杂,用于突破组织安全防线的攻击变得越来越复杂,更难以建立保护措施。如果我们回顾一下网络安全漏洞的历史,我们可以找到一些例子,展示了网络安全问题有多么严重。例如,近年来,我们看到了一些涉及主要组织的泄露事件,其中一家组织的用户账户泄露超过 30 亿个;在另一次攻击中,一个游戏网络遭受了安全漏洞,并且停机了大约一个月,给组织造成了巨大的财务损失。

网络安全领域清楚地表明了一件事:这是一个不断发展的领域,每天都会发现新的攻击类型,并且正在研究新的缓解措施以及及时地克服它们。

现在,让我们来了解为什么企业应用安全是一个重要的话题,不应该被 compromise。

企业安全的重要性

大多数企业,无论其规模大小,都处理大量用户数据。这些数据可能涉及用户的一些公开可用的信息,也可能涉及机密数据。一旦这些数据进入组织的存储系统,组织就有责任保护数据的机密性,以防止未经许可的任何未经授权的人访问。

为了实现这一点,大多数企业加强了他们的网络安全,并建立了多重屏障,以防止未经授权的访问其用户数据系统。因此,让我们来看看企业安全如此重要的一些原因:

  • 数据的机密性:许多组织...

系统安全面临的挑战

信息技术领域正在以快速的速度增长,每天都会出现新的技术。两方之间的通信方式也在不断发展,提供更有效的远程通信。但这种演变也带来了一系列关于系统安全的挑战。让我们来看看使得组织的系统安全变得困难的挑战。

  • 数据量的增加:大多数组织正在构建他们的系统,利用人工智能和机器学习为用户提供更个性化的体验,他们也正在收集大量关于用户的信息,以改进推荐。这大量的数据存储使得数据的安全性更难以维护,因为现在越来越多的机密信息被保留,使得系统对攻击者更具吸引力。

  • 数据分布在公共服务提供商之上:许多企业现在正在削减其存储基础设施,并且越来越依赖第三方公共存储提供商,这些提供商以更低的成本提供相同数量的存储空间,以及降低的维护成本。这也会使企业的安全性面临风险,因为现在数据受第三方服务提供商的安全策略管辖,数据所有者对数据的安全策略几乎没有控制权。存储服务提供商的一次违规行为可能会暴露不同组织的多个用户的数据。

  • 连接到互联网的设备数量不断增加:随着越来越多的设备加入互联网,攻击面也在增加。即使是单个设备内部存在弱点,无论是加密标准还是未实施适当的访问控制,整个系统的安全性都很容易被破坏。

  • 复杂的攻击:攻击变得越来越复杂,攻击者现在利用系统中的零日漏洞,甚至利用组织尚未发现的漏洞。这些攻击危害了大量数据,并对整个系统构成了巨大的安全风险。更加复杂的是,由于漏洞是新的,它们没有即时解决方案,导致延迟的响应,甚至有时延迟识别攻击发生。

  • 国家赞助攻击的增加:随着全球信息技术驱动的通信和流程不断增加,战争的背景也在改变。以往的战争是在地面上进行的,现在它们正在网络上进行,这导致了国家赞助的攻击。这些攻击通常针对企业,要么是为了收集情报,要么是为了造成重大破坏。国家赞助的攻击问题在于这些攻击本质上是高度复杂的,并利用了大量资源,这使得它们难以克服。

有了这些,我们现在知道了不同因素使企业难以提高其系统的安全性。这就是为什么网络安全总是在进行追赶,企业正在改进其安全性,以抵御攻击者利用不断变化的攻击向量攻击 IT 系统。

现在,有了这些知识,是时候让我们了解到底是什么影响了应用程序的安全性。只有了解了不同的攻击向量,我们才能继续前进,使我们的应用程序免受攻击。因此,让我们开始这段旅程。

看一下攻击向量

每次侵犯系统安全或使其崩溃的攻击,都会利用系统应用程序运行的一个或另一个漏洞。这些漏洞对每种类型的应用程序都是不同的。为系统本地构建的应用程序可能具有不同的攻击向量,而为网络开发的应用程序可能具有不同的攻击向量。

为了充分保护应用程序免受攻击,我们需要了解针对不同应用程序类型使用的不同攻击向量。

从现在开始,我们将简要介绍两种最常见的应用程序类型和可能用于针对这些应用程序的攻击向量。

本地应用程序的安全问题

本地应用程序是专门为其运行的平台构建的应用程序。这些应用程序利用所提供的库和功能,以充分利用平台功能。这些应用程序可能遇到的安全问题通常是影响这些应用程序运行的基础平台的安全问题,或者是由应用程序开发人员留下的漏洞造成的。因此,让我们来看看影响本地应用程序安全性的一些问题:

  • 基础平台的漏洞:当应用程序在平台上运行时,其功能受基础平台公开的内容所支配。如果基础平台容易受到安全问题的影响,那么在平台上运行的应用程序也会容易受到影响,除非它们在应用程序级别实施适当的措施来减轻这些漏洞。这类问题可能涉及硬件问题,比如最近影响 x86 平台的 Spectre 和 Meltdown 漏洞。

  • 使用第三方库:一些使用第三方库的应用程序,特别是用于在应用程序内部实现安全性的库,如果开发人员停止维护这些库,或者存在一些未修复的漏洞,确实会使应用程序更容易受到安全漏洞的影响。通常,更好的选择是至少针对在应用程序中实现安全性的用例使用平台本身提供的库,而不是使用未记录的平台 API,这可能对应用程序的使用具有未解释的安全影响。

  • 未加密的数据存储:如果一个应用程序涉及存储和检索数据,以未加密的格式存储数据可能导致数据被不受信任的来源访问,并使数据容易被滥用。应用程序应确保其存储的数据是以加密形式存储的。

  • 与第三方的未加密通信:如今,许多应用程序依赖于第三方服务来实现特定功能。即使在企业网络内部,应用程序可能也会调用网络内部的第三方身份验证服务器来验证用户的身份。如果这些应用程序之间的通信是未加密的,可能会导致攻击,如中间人攻击。

  • 避免边界检查:那些实施自己的内存管理技术的本地应用程序,如果应用程序的开发人员忽略了可能的边界检查,可能会变得容易受到攻击者访问应用程序边界之外的数据的攻击。这可能会导致系统安全性的严重破坏,不仅受影响的应用程序的数据暴露,其他应用程序的数据也会暴露。

这是一个非详尽的可能影响本地应用程序安全性的问题列表。其中一些问题可以很容易地修复,而其他问题则需要应用程序开发人员和平台提供商付出大量努力来减轻可能的安全漏洞。

现在,了解可能影响本地应用程序的攻击向量的知识后,是时候让我们了解可能影响 Web 应用程序的攻击向量了。

Web 应用程序的安全问题

Web 应用程序的使用量一直在不断增加。随着互联网的日益普及,越来越多的组织正在将日常办公工作转移到帮助不同地理位置的办公室之间建立联系的 Web 应用程序上。但是,这些优势也伴随着安全方面的成本。

由于 Web 应用程序可能发生的攻击方式之多,Web 应用程序的安全一直是一个具有挑战性的领域。因此,让我们来看看困扰 Web 应用程序安全的问题:

  • SQL 注入:由于使用 SQL 数据库支持的 Web 应用程序的常见攻击之一是使用 SQL 注入。

安全反模式

现在,我们需要了解通常会使应用程序处于安全漏洞区域的实践。可能有很多事情会导致应用程序遭受安全问题,当我们通过本节时,我们将看一些通常会使应用程序容易受到安全漏洞的错误。所以,让我们逐个看一下。

不过滤用户输入

作为应用程序开发人员,我们希望用户信任我们的应用程序。这是我们确保用户会使用我们的应用程序的唯一方式。但是,同样地,我们是否也应该信任我们的用户,并期望他们不会做任何错误的事情?具体来说,信任他们通过我们的应用程序向他们提供输入的输入机制。

以下代码片段展示了一个简单的例子,未过滤用户提供的输入:

username = request.args.get('username')email = request.args.get('email')password = request.args.get('password')user_record = User(username=username, email=email, password=password) #Let's create an object to store in database ...

未加密存储敏感数据

现在,作为应用程序开发人员,我们喜欢在应用程序代码库中保持简单,以便以后可以轻松维护应用程序。在考虑这种简单性的同时,我们认为我们的应用程序已经在一个良好的防火墙后面运行,并且每次访问都经过了彻底检查,那么为什么不只是以明文形式在数据库中存储用户的密码呢?这将帮助我们轻松匹配它们,也将帮助我们节省大量的 CPU 周期。

有一天,当应用程序在生产中运行时,攻击者能够破坏数据库的安全性,并不知何故能够从用户表中获取详细信息。现在,我们面临的情况是用户的登录凭据不仅泄露了,而且还以明文格式可用。根据一般心理学,许多人会在许多服务上重复使用相同的密码。在这种情况下,我们不仅危及了我们应用程序用户的凭据,还危及了用户可能正在使用的其他应用程序的凭据。

这种试图在没有任何强大加密的情况下存储安全敏感数据的做法不仅使应用程序面临可能随时发生的安全问题,而且还使其用户面临风险。

忽略边界检查

与缺少边界检查相关的安全问题在软件应用程序中是相当常见的情况。这种情况发生在开发人员意外地忘记在他们正在实现的数据结构中实施边界检查时。

当程序尝试访问分配给它的内存区域之外的内存区域时,会导致程序发生缓冲区溢出。

例如,考虑以下代码片段:

arr = [None] * 10for i in range(0,15):    arr[i] = i

当执行此程序时,程序会尝试更改实际上并非由其管理的内存内容。如果底层平台没有引发任何内存保护,此程序将成功地能够覆盖...

不保持库的更新

大多数生产应用程序依赖于第三方库来启用一些功能集。保持这些库过时可以节省一些额外的千字节的更新或维护软件,以便它继续使用更新的库。然而,这也可能导致应用程序存在未修复的安全漏洞,攻击者以后可能利用这些漏洞非法访问您的应用程序和应用程序管理的数据。

将数据库的完整权限赋予单个用户

许多应用程序实际上会给应用程序的单个用户完整的数据库权限。有时,这些权限足以让您的应用程序数据库用户具有与数据库的根用户相同的权限集。

现在,这种实现方式在解决验证某个用户是否具有执行数据库操作的特定权限的问题上有很大帮助,同时也为应用程序带来了巨大的漏洞。

想象一下,如果某个数据库用户的凭据不知何故泄露了。攻击者现在将完全访问您的数据库,这使他们...

改进应用程序的安全性

如果我们遵循软件安全的一些基本规则并且在应用程序的开发和生产周期中严格实施这些规则,就可以保持应用程序的安全性:

  • 永远不要相信用户输入: 作为应用程序的开发人员,我们应该确保不相信任何用户输入。在应用程序存储或任何其他可能导致提供的输入被执行的操作之前,用户端可能提供的一切都应该得到适当的过滤。

  • 加密敏感数据: 任何敏感数据都应具有强大的加密,以支持其存储和检索。在生成数据的加密版本时具有一定程度的随机性可以帮助防止攻击者从数据中获取有用信息,即使他们以某种方式获得了对数据的访问权限。

  • 妥善保护基础设施: 用于运行应用程序的基础设施应该得到妥善保护,防火墙配置应限制对内部网络或节点的任何未经授权的访问。

  • 实施端到端加密:两个服务之间发生的任何通信都应该进行端到端加密,以避免中间人攻击或信息被窃取。

  • 谨慎实施边界检查:如果您的应用程序使用任何类型的数据结构,请确保适当的边界检查已经就位,以避免漏洞,比如缓冲区溢出,这可能允许恶意代码被执行。

  • 限制用户权限:没有应用程序应该有一个拥有所有权限的单个用户。用户权限应该受到限制,以定义用户执行操作的边界。遵循这种建议可以帮助限制较低权限用户的凭据被泄露时可能造成的损害。

  • 保持依赖项更新:应用程序的依赖项应该保持更新,以确保依赖项没有已知的安全漏洞。

遵循这些指南可以在改善应用程序安全性方面起到很大作用,并确保应用程序和数据都得到保护,从而保持用户信任和数据安全。

总结

随着我们在本章的进展,我们了解了管理软件应用程序开发和运营的不同安全原则。我们谈到了需要在企业应用程序方面保持高安全标准,以及应用程序安全被破坏后会发生什么。然后,我们了解了系统安全面临的挑战。然后,我们转向了用于危害应用程序安全的常见攻击向量。

一旦我们了解了攻击向量,我们就看了一些常见的安全反模式,这些反模式会危及您的应用程序以及与应用程序相关的数据的安全性。一旦我们了解了这些反模式,...

问题

  1. 是什么不同的问题使应用程序安全变得困难?

  2. 什么是 XSS 攻击?

  3. 我们如何防止 DoS 攻击?

  4. 一些危害应用程序安全的错误是什么?

进一步阅读

如果您觉得应用程序安全是一个有趣的话题,并且想要了解如何使用 Python 来提高应用程序的安全性,请看一下这个由 Manish Saini 撰写、Packt 制作的视频系列《Python 持续交付和应用程序安全》。

第十一章:采用微服务方法

到目前为止,在本书中,我们已经了解了如何开发企业级应用程序以及如何成熟我们的流程,以便我们交付的应用程序符合我们的质量标准,并为其用户提供强大而有韧性的体验。在本章中,我们将看看一种新的应用程序开发范式,其中应用程序不是一个单一的产品,而是多个产品相互交互,以提供统一的体验。

近年来,开发场景发生了快速变化。应用程序开发已经从开发大型单体转变为开发小型服务,所有这些服务相互交互,为用户提供所需的结果。这种变化是为了满足更快地发布项目的需求,以增加添加新功能和提高应用程序可扩展性的能力。

在本章中,我们将看看这种新的应用程序开发范式,团队变得更小,能够以越来越低的成本在应用程序中发布新功能已经成为新的标准。这种被称为微服务开发方法的范式彻底改变了应用程序开发周期的工作方式,并且还导致了与 DevOps、持续集成和部署相关的技术的当前趋势。

随着本章的进行,您将了解以下内容:

  • 朝着微服务开发方法迈进

  • 服务之间基于 API 的通信

  • 构建健壮的微服务

  • 处理微服务中的用户-服务器交互

  • 微服务之间的异步通信

技术要求

本书中的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter11目录下找到。

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

设置和运行代码的步骤已包含在README.md文件中,以便更深入地了解代码示例。

向微服务开发方法转变

在过去几年里,开发人员一直在尝试用新的方式来开发应用程序。其目的是缩短开发生命周期,增加更快地将项目投入生产的能力,增加组件之间的解耦,使它们可以独立开发,并提高团队并行开发应用程序的能力。

随之而来的是使用微服务的开发技术,这有助于解决上述的用例。在这种方法中,应用程序不是一个单一的大型代码库,所有组件都放在一起,对任何组件的单一更改都需要再次部署整个应用程序。首先,让我们看看微服务模型与单体模型的不同之处,然后看看遵循微服务方法有哪些优势。

单体开发模型与微服务

我们都习惯于构建一个应用程序,其中单个代码库包含应用程序的所有功能组件,紧密地联系在一起,以实现特定的期望结果。这些应用程序遵循严格的开发方法,应用程序的功能和架构首先在初始需求收集和设计阶段进行思考,然后应用程序的严格开发开始。

只有在所有组件都经过开发和彻底测试后,应用程序才进入生产阶段,在那里它被部署在基础设施上供常规使用。这个模型在下图中显示:

这个过程...

微服务架构的优势

微服务架构为我们解决了许多问题,主要是因为我们开发和部署微服务的方式发生了变化。让我们来看看微服务架构为我们的开发过程带来的一些优势,如下列表所示:

  • 小团队:由于一个特定的微服务通常专注于做一件事并且做得很好,负责构建该微服务的团队通常可以很小。一个团队可以全面拥有多个微服务,他们不仅负责开发,还负责部署和管理,从而形成良好的 DevOps 文化。

  • 增强了独立性:在微服务架构中,负责开发一个微服务的团队不需要完全了解另一个微服务的内部工作方式。团队只需要关注微服务暴露的 API 端点,以便与其进行交互。这避免了团队在开展开发活动时对彼此的依赖。

  • 增强了对故障的韧性:在微服务架构中,由于一个微服务的故障不会影响整个应用程序,而是会逐渐降低服务的性能,因此故障韧性相当高。在此期间,可能会启动一个新的失败服务实例,或者可以轻松地将失败服务隔离以进行调试,以减少影响。

  • 增强了可扩展性:微服务架构为应用程序的可扩展性提供了很大的自由度。现在,随着负载的增加,可以独立地扩展各个微服务,而不是整体扩展应用程序。这种扩展可以以水平扩展的方式进行,根据应用程序所经历的负载,可以启动更多的特定微服务实例,或者可以使用垂直扩展的方式单独扩展这些服务,为特定服务分配更多资源,以便更好地处理不断增加的负载。

  • 简单集成:使用微服务,不需要了解其他微服务内部的知识,因此不需要了解其他微服务的内部情况,不需要了解其他微服务的内部情况。所有的集成都是在假设其他微服务是黑匣子的情况下进行的。

  • 增强了可重用性:一旦开发完成,一个微服务可以在不同的应用程序中被利用。例如,负责用户认证处理的微服务可以在多个应用程序中重复使用,而无需复制代码。

  • 轻松推出新功能的自由:使用微服务架构,新功能可以轻松推出。在大多数情况下,特定功能被转换为自己的微服务,然后在经过适当测试后部署到生产环境。一旦服务在生产环境中上线,其功能就可以使用。这与整体式方法不同,整个应用程序需要在新功能或改进需要部署到生产环境时重新部署。

从这个列表中,我们可以看到微服务架构向我们提供了许多好处。从工具的选择到快速推出新功能的便利性,微服务架构使开发人员有利可图,并迅速开始推出新的微服务。

但所有这些优势并非免费。尽管有优势,但在微服务架构中工作时也有可能创建基础设施的混乱,这不仅会增加成本。然而,这也可能影响团队的整体生产力,他们可能更专注于解决因架构实施不当而可能出现的问题,而不是专注于改进和开发对应用程序用户至关重要的功能。

这并不是什么大问题。我们可以遵循一些简单的建议,在微服务架构的旅程中会有很大帮助。因此,让我们花些时间了解这些简单的技巧,这些技巧可以帮助我们顺利进行微服务的旅程。

微服务开发指南

微服务的开发是具有挑战性的,而且很难做到完美。有没有什么方法可以让这个过程变得更容易?事实证明,有一些指南,如果遵循,可以在微服务的开发中提供很大帮助。因此,让我们看一下以下列表中所示的这些指南:

  • 开发前的设计:当进行微服务开发时,它们通常应该模拟特定的责任领域。但这也是最常出现最大错误的地方。通常情况下,服务的边界没有定义。在后期阶段,随着领域的发展,微服务也变得复杂,以处理增加的...

微服务中的服务发现

在应用程序开发的传统模型中,通常会以静态方式部署特定应用程序的服务,它们的网络位置不会自动更改。如果是这种情况,那么偶尔更新配置文件以反映服务的更改网络位置是完全可以的。

但在现代基于微服务的应用程序中,服务的数量可能会根据多种因素而上下波动,例如负载平衡、扩展、新功能的推出等,因此维护配置文件会变得有些困难。此外,如今大多数云环境都不提供这些服务的静态网络部署,这意味着服务的网络位置可能会不断变化,增加了维护配置文件的麻烦。

为了解决这些情况,我们需要有一些更加动态的东西,可以适应不断变化的环境。这就是服务发现的概念。服务发现允许动态解析所需服务的网络端点,并消除了手动更新配置文件的需要。

服务发现通常有以下两种方式:

  • 客户端服务发现

  • 服务器端服务发现

但在讨论这两种方法之前,我们需要了解服务发现系统的另一个重要组件。让我们看看这个重要组件是什么,以及它如何促进服务发现过程。

客户端服务发现

使用客户端服务发现方法,各个服务需要知道服务注册表。例如,在这种模型中,如果服务实例 A想要向服务实例 C发出请求,那么进行此请求的过程如下图所示:

请求的流程如下所示:

  • 服务实例 A查询服务注册表以获取服务实例 C的网络地址。

  • 服务注册表检查其数据库以获取服务实例 C的网络地址,并将其返回给服务实例 A。如果服务实例 C是负载平衡服务...

服务器端服务发现

使用服务器端服务发现模式,解析服务的网络地址的能力不在个体客户端内部——相反,这个逻辑被移动到负载均衡器中。在服务器端服务发现模式中,请求流程如下图所示:

这个图表显示了以下过程:

  1. 客户端发出对 API 端点的请求

  2. 负载均衡器拦截请求并查询服务注册表以解析适当服务的网络地址

  3. 负载均衡器然后将请求发送到适当的网络服务来处理请求

这种模式的优势在于通过从客户端中删除服务发现逻辑来减少代码重复,并且由于服务注册表不负担负载均衡算法的负载,因此负载均衡更好。

现在我们知道了微服务架构中服务发现是如何发生的,让我们把重点放在理解微服务中另一个有趣的概念上。

想象一下,你正在构建一个应用程序,该应用程序应该处理多个设备,并且每个设备提供的功能根据某些方面而有所不同,比如移动设备将不具备向其他用户发送直接消息的功能。在这种情况下,每个设备都需要一个不同的 API 端点,以便调用其特定的服务集。然而,在应用程序的维护阶段或某些 API 发生变化时,让客户端了解每个单独的 API 端点可能会成为一个问题。为了处理这种情况,我们需要有一些可以作为我们通信的中间层的东西。

幸运的是,在微服务架构中,我们有一些东西可以帮助我们解决这个问题。让我们看看我们可以利用什么。

微服务中的服务级别协议

在基于微服务架构的任何生产级应用程序的开发过程中,服务可能会在很大程度上依赖于生产环境中部署的其他服务的可用性。例如,为应用程序的管理面板提供功能的服务可能需要用户认证服务的可用性,以允许管理员登录和权限管理。如果用户管理服务出现故障,应用程序提供的操作的稳定性可能会受到严重影响。

为了保证这些要求,我们需要有作为团队之间特定微服务交付的合同的 SLA。这...

构建你的第一个微服务应用程序

我们现在准备使用微服务架构构建我们的第一个应用程序。在开发这个应用程序的过程中,我们将看到如何利用我们迄今为止所获得的知识来推出一个可工作的应用程序。

现在,关于我们的例子,为了保持这个应用程序简单,并且提供对微服务架构工作原理的简单理解,我们将构建一个简单的待办事项创建应用程序:让我们看看这个应用程序将会是什么样子,如下列表所规定的:

  • 该应用程序将由两个微服务组成——即待办事项管理服务和用户认证服务

  • 这些服务将使用 Python 开发

  • 为了这个练习,服务将利用它们自己的 SQLite 数据库

  • 待办事项服务将依赖用户服务来收集与用户操作相关的任何信息,包括用户认证、配置获取等

  • 服务将通过使用 RESTful API 进行通信,每个服务提供 JSON 编码的响应

具体要求已经指定,现在是时候开始编写我们的微服务了。

用户微服务

用户微服务负责处理与用户配置文件管理相关的任何事务。该服务提供以下功能:

  • 注册新用户

  • 用户配置文件管理

  • 现有用户的身份验证

  • 为用户生成唯一的身份验证令牌以登录

  • 为其他服务提供用户认证功能

为了使该服务运行,我们需要以下两个数据库模型:

  • 用户数据库模型: 用户数据库模型负责管理用户记录,如他们的用户名、哈希密码等。

  • 令牌数据库模型: 令牌数据库模型负责存储已生成的令牌的信息...

待办事项管理器服务

待办事项管理器服务是帮助用户管理其todo项目的服务。该服务提供了用户创建新列表并向列表添加项目的功能。为此,唯一的要求是用户应该经过身份验证。

为了正确工作,服务将需要存在一个列表数据库模型,用于存储用户创建的todo列表的信息,以及一个项目模型,其中将包含特定todo列表的项目列表。

以下代码片段实现了这些模型:

'''
File: models.py
Description: The models for the todo service.
'''
from todo_service.todo_service import db
import datetime

class List(db.Model):
    """The list database model.

    The list database model is used to create a new todo list
    based on the input provided by the user.
    """

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, nullable=False)
    list_name = db.Column(db.String(25), nullable=False)
    db.UniqueConstraint('user_id', 'list_name', name='list_name_uiq')

    def __repr__(self):
        """Provide a representation of model."""
        return "<List {}>".format(self.list_name)

class Item(db.Model):
    """The item database model.

    The model is used to store the information about the items
    in a particular list maintained by the user.
    """

    id = db.Column(db.Integer, primary_key=True)
    list_id = db.Column(db.Integer, db.ForeignKey(List.id))
    item_name = db.Column(db.String(50), nullable=False)
    db.UniqueConstraint('list_id', 'item_name', name='item_list_uiq')

    def __repr__(self):
        """Provide a representation of model."""
        return "<Item {}>".format(self.item_name)

一旦开发了这些模型,我们需要做的下一件事就是实现 API。

对于待办事项管理器服务,将设置以下 API,为服务提供交互端点:

  • /list/new:此 API 端点接受要创建的列表的名称并创建新列表。

  • /list/add_item:此 API 端点接受需要添加到列表中的项目列表以及应将项目添加到的列表的名称。一旦验证通过,项目将被添加到列表中。

  • /list/view:此 API 端点接受需要显示内容的列表的名称,并显示列表的内容。

以下代码片段显示了服务的端点实现:

def check_required_fields(req_fields, input_list):
    """Check if the required fields are present or not in a given list.

    Keyword arguments:
    req_fields -- The list of fields required
    input_list -- The input list to check for

    Returns:
        Boolean
    """

    if all(field in req_fields for field in input_list):
        return True
    return False

def validate_user(auth_token):
    """Validates a user and returns it user id.

    Keyword arguments:
    auth_token -- The authentication token to be used

    Returns:
        Integer
    """

    endpoint = user_service + '/auth/validate'
    resp = requests.post(endpoint, json={"auth_token": auth_token})
    if resp.status_code == 200:
        user = resp.json()
        user_id = user['user_id']
        return user_id
    else:
        return None

@app.route('/list/new', methods=['POST'])
def new_list():
    """Handle the creation of new list."""

    required_fields = ['auth_token', 'list_name']
    response = {}
    list_data = request.get_json()
    if not check_required_fields(required_fields, list_data.keys()):
        response['message'] = 'The required parameters are not provided'
        return jsonify(response), 400

    auth_token = list_data['auth_token']

    # Get the user id for the auth token provided
    user_id = validate_user(auth_token)

    # If the user is not valid, return an error
    if user_id is None:
        response['message'] = "Unable to login user. Please check the auth token"
        return jsonify(response), 400

    # User token is valid, let's create the list
    list_name = list_data['list_name']
    new_list = List(user_id=user_id, list_name=list_name)
    db.session.add(new_list)
    try:
        db.session.commit()
    except Exception:
        response['message'] = "Unable to create a new todo-list"
        return jsonify(response), 500
    response['message'] = "List created"
    return jsonify(response), 200

@app.route('/list/add_item', methods=['POST'])
def add_item():
    """Handle the addition of new items to the list."""

    ...
    # The complete code for the service can be found inside the assisting code repository for the book

有了上述代码,我们现在已经准备好使用我们的待办事项管理器服务,它将通过 RESTful API 帮助我们创建和管理待办事项列表。

但在我们执行待办事项管理器服务之前,我们需要记住一件重要的事情。该服务依赖于用户服务来执行任何类型的用户认证并获取有关用户配置文件的信息。为了实现这一点,我们的待办事项管理器需要知道用户服务在哪里运行,以便可以与用户服务进行交互。在这个例子中,我们通过在待办事项管理器服务配置文件中设置用户服务端点的配置键来实现这一点。以下代码片段显示了待办事项管理器服务配置文件的内容:

DEBUG = False
SECRET_KEY = 'du373r3uie3yf3@U#^$*EU9373^#'
BCRYPT_LOG_ROUNDS = 5
SQLALCHEMY_DATABASE_URI = 'sqlite:///todo_service.db'
SQLALCHEMY_ECHO = False
USER_SERVICE_ENDPOINT = 'http://localhost:5000'

要使待办事项管理器服务运行,需要从存储库的todo_service目录内执行以下命令:

python3 run.py

一旦命令成功执行,待办事项管理器服务将在http://localhost:5001/上可用。

一旦服务启动运行,我们可以利用其 API 来管理我们的清单。例如,如果我们想要创建一个新的待办事项列表,我们只需要向http://localhost:5001/list/new API 端点发送 HTTP POST 请求,传递以下键作为 JSON 格式的输入:

  • auth_token 这是用户在使用http://localhost:5000/auth/login API 端点成功登录用户服务后收到的身份验证令牌

  • list_name 这是要创建的新列表的名称

一旦 API 端点调用完成,待办事项管理器服务首先尝试通过与用户服务交互来验证 API 调用中提供的auth令牌。如果auth令牌验证通过,待办事项管理器服务将接收一个用于识别用户的用户 ID。完成这一步后,待办事项管理器服务会在其数据库中为新的待办事项列表创建一个条目,并针对检索到的用户 ID。

这是待办事项管理器服务的简单工作流程。

现在我们了解了如何构建一个简单的微服务,我们现在可以专注于有关微服务架构的一些有趣的主题。你是否注意到我们如何告知待办事项管理器服务用户服务的存在?我们利用了配置密钥来实现这一点。当你只有两个或三个服务,无论发生什么,它们总是在相同的端点上运行时,使用配置密钥绝不是一个坏选择。然而,当微服务的数量甚至比两个或三个服务稍微多一点时,这种方法会严重崩溃,因为它们可能在基础设施的任何地方运行。

除此之外,如果新服务频繁投入生产以为应用程序添加新功能,问题会进一步加剧。在这一点上,我们需要更好的解决方案,不仅应提供一种简单的方式来识别新服务,还应自动解析它们的端点。

微服务内的服务注册表

假设有一场魔术表演将在礼堂内举行。这场表演对所有人开放,任何人都可以来礼堂参加。在礼堂的门口,有一个登记处,你需要在进入礼堂之前先登记。当观众开始到来时,他们首先去登记处,提供他们的信息,比如他们的姓名、地址等,然后被给予一张入场券。

服务注册表就像这样。它是一种特殊类型的数据库,记录了基础设施上运行的服务以及它们的位置。每当新服务启动时,它都会注册...

微服务中的 API 网关

在构建微服务架构时,我们有很多选择,大多数情况下可以自由选择最适合实现微服务的技术栈。除此之外,我们始终可以通过推出针对特定设备的不同微服务来为不同设备提供不同的功能。但是当我们这样做时,我们也给客户端增加了复杂性,现在客户端必须处理所有这些不同的情况。

因此,让我们首先看一下客户端可能面临的挑战,如下图所示:

前面的图表显示了我们面临的挑战,如下列表所示:

  • 处理不同的 API: 当每个设备都有一个特定的微服务,为其提供所需的功能集时,该设备的客户端需要了解与该特定服务相关的 API 端点。这增加了复杂性,因为现在负责处理客户端开发的团队需要了解可能会减慢客户端开发过程的微服务特定端点。

  • 更改 API 端点: 随着时间的推移,我们可能会修改微服务内特定 API 端点的工作方式。这将要求我们更新所有利用微服务提供的服务的客户端,以反映这些更改。这是一个繁琐的过程,也可能引入错误或破坏现有功能。

  • 协议支持不足:使用微服务架构,我们有权控制用于构建微服务的技术栈。有时,微服务可能由通常不受其他平台支持或在其他平台上实现不佳的协议驱动。例如,客户端运行的大多数平台可能不支持像 AMQP 这样的东西,这将使得客户端的开发变得困难,因为现在开发人员必须在每个客户端内构建处理 AMQP 协议的逻辑。这种要求不仅可能具有挑战性,而且如果平台无法处理所需的过多处理负载,可能也无法完成。

  • 安全性:如果我们需要嵌入每个客户端支持的微服务的个别网络位置的细节,我们可能会在基础设施中打开安全漏洞,即使其中一个微服务未正确配置安全性。

这些只是在开发微服务应用程序过程中可能面临的一些挑战。但我们能做些什么来克服它们呢?

这个问题的答案在于使用 API 网关。

API 网关可以被视为客户端和应用程序通信之间的中介,处理客户端请求的路由以及将这些请求从客户端支持的协议转换为后端微服务支持的协议。它可以在不让客户端担心微服务可能运行的位置的情况下完成所有这些操作。

在使用 API 网关的基于微服务架构的应用程序中,从客户端到应用程序的请求流程可以描述如下:

  1. 客户端有一组共同的端点,用于访问一定的功能集。

  2. 客户端向 API 端点发送请求,以及需要传递的任何数据,以便完成请求。

  3. API 网关拦截客户端对 API 端点的请求。

  4. API 网关确定客户端类型和客户端支持的功能。

  5. 然后 API 网关确定需要调用哪些个别微服务来完成请求。

  6. 然后 API 网关将请求转发到后端运行的特定微服务。如果微服务接受的协议与客户端发出请求的协议不同,API 网关会将请求从客户端协议转换为微服务支持的协议,然后转发请求。

  7. 一旦微服务完成生成响应,API 网关收集响应并将集体响应发送回请求的客户端。

这种过程有几个优点;让我们来看看其中的一些:

  • 简单客户端:有了 API 网关,客户端无需知道它们可能需要调用的各个微服务。这里的客户端对特定功能调用一个共同的端点,然后 API 网关负责确定需要调用哪个服务来完成请求。这大大减少了正在开发的客户端的复杂性,并使其维护变得容易。

  • 更改 API 端点的便利性:当后端微服务的特定 API 实现发生变化时,API 网关可以处理未更新的旧客户端的兼容性。这可以通过使 API 网关返回降级响应或自动更新其接收到的请求以适应新的 API 兼容层来实现,如果可能的话。

  • 更简单的协议支持:有了 API 网关来处理微服务可能需要的任何协议转换,客户端就不需要担心如何处理它无法支持的协议,大大减少了引入不受平台支持的协议支持可能带来的复杂性和问题。

  • 改进的安全性:通过 API 网关,客户端不需要知道特定微服务运行的个别网络位置。他们只需要知道 API 网关监听请求的位置,以便成功调用 API。一旦调用完成,API 网关负责确定提供该 API 的各个微服务的运行位置,然后将请求转发给它们。

  • 改进的故障处理:如果特定的后端服务出现故障,API 网关也可以提供帮助。在这种情况下,如果后端微服务是非关键微服务,API 网关可以向客户端返回降级响应,而如果关键后端服务出现故障,API 网关可以立即返回错误响应,而不让请求排队,增加服务器的负载。

正如我们所看到的,使用 API 网关的好处是巨大的,并且极大地简化了微服务应用程序中客户端的开发。此外,通过利用 API 网关,可以轻松建立服务之间的通信。

为了使服务相互通信,它们只需调用 API 网关知道的适当端点,然后 API 网关负责确定适当的微服务及其网络地址,以完成对其发出的请求。

前面的方法确实很好,但有一个缺点:这里的一切都是串行和同步的。发出调用,然后调用客户端/服务等待直到生成响应。如果服务的负载很高,这些响应可能需要很长时间才能到达,这可能会导致大量请求在基础设施上排队,进一步增加基础设施的负载,或者可能导致大量请求超时。这可能会大大降低应用程序的吞吐量,甚至可能使整个基础设施崩溃,如果排队请求的数量变得非常大。

是否有一种服务之间可以相互交互的异步通信方法,而不需要一遍又一遍地进行 API 调用?让我们看看这样一种方法。

微服务中的异步通信

在微服务架构中,每个服务都有一个明确的职责,并且做得很好。为了实现业务应用的任何有意义的响应,这些服务需要相互通信。所有这些通信都发生在网络上。

在这里,一个服务向另一个服务发出请求,然后等待响应返回。但有一个问题。如果另一个服务花费很长时间来处理请求,或者服务宕机了呢?那时会发生什么?

大多数情况下,请求会超时。但如果这个服务是一个关键服务,那么可能会到达它的请求数量可能会很大,并且可能会不断排队。如果服务很慢,这将...

消息队列用于微服务通信

消息队列是一种相当古老的机制,用于在应用程序内的许多不同组件之间建立通信。这种古老的方法甚至适用于我们当前的微服务架构用例。但在我们深入研究如何使用消息队列使微服务通信异步之前,让我们首先看一下在处理这种通信方法时使用的一些行话:

  • 消息:消息是特定服务生成的一种包,用于与另一个服务交流其想要实现的目标。

  • 队列:队列是一种主题,特定消息可能会出现在其中。对于任何实际应用程序,可能会有许多队列,每个队列表示特定的通信主题。

  • 生产者:生产者是生成消息并将其发送到特定主题的服务。

  • 消费者:消费者是监听特定主题并处理可能到达的任何消息的服务。

  • 路由器:路由器是消息队列内的一个组件,负责将特定主题的消息路由到适当的队列。

现在我们知道了行话,我们可以继续看看消息队列如何帮助我们建立微服务之间的通信。

当微服务利用诸如消息队列之类的东西时,它们会使用异步协议进行交互。例如,AMQP 是更著名的异步通信协议之一。

通过异步通信,微服务之间的通信将如下进行:

  1. 建立一个消息代理,它将提供消息队列的管理功能,并将消息路由到适当的队列。

  2. 新服务启动并注册它想要监听或发送消息的主题。消息代理为该主题创建适当的队列,并将请求服务添加为该队列的消费者或生产者。这个过程也会继续进行其他服务。

  3. 现在,一个想要实现特定目标的服务将消息发送到主题,比如Topic Authenticate

  4. 监听Topic Authenticate的消费者收到有关新消息的通知并将其消耗掉。

  5. 消费者处理它已经消费的消息,并将响应放回另一个主题Topic Auth_Response

  6. 原始消息的生产者是Topic Auth_Response的消费者,并收到有关新消息的通知。

  7. 原始请求客户端然后读取此消息并完成请求-响应循环。

现在,我们知道了由异步消息队列驱动的微服务架构内部的通信是什么样子。但是除了异步通信之外,这种方法还有其他好处吗?

事实证明,我们可能会从这种通信模式中看到许多好处。以下列表显示了我们可能会体验到的一些好处:

  • 更好的请求分发:由于可能有许多消费者可能会监听特定主题,因此消息可以并行处理,并且负载平衡可以通过在消费者之间平均分配消息来自动处理。

  • 更好的错误韧性:在特定微服务宕机的情况下,需要由该微服务处理的消息可以在消息队列内排队一段时间,然后在服务恢复后进行处理,从而减少可能的数据丢失。

  • 减少重复响应:由于消息只传递一次给单个消费者,并且在被消费后立即出列,因此很少有可能为单个请求产生重复响应。

  • 增加的容忍度:在基础设施内部的不同微服务经历高负载时,消息队列系统提供了异步请求-响应循环,从而减少了请求排队的机会。

有了这个,我们现在知道了如何在微服务之间建立异步通信,并且使我们的基础设施随着时间的推移而发展,而不必担心如何处理新增的 API 端点以进行服务间通信。

总结

在本章中,我们看了一下如何使用微服务架构以及它与传统的单片式企业应用程序开发方式有何不同。然后,我们看了一下向微服务开发方式转变的优势,并了解了我们可以遵循的指南,使我们朝着微服务更顺利地前进。

一旦我们了解了微服务的基础知识,我们继续看了一下 SLA 如何保证我们在服务之间获得一定的期望功能集,并且它们作为合同来支持应用程序的顺畅服务。然后,我们进行了一个实践练习,编写了一个简单的待办事项管理应用程序...

问题

  1. 服务导向架构与微服务架构有何不同?

  2. 如何确保基于微服务的应用程序的高可用性?

  3. SLA 提供了什么样的保证?

  4. 我们可以让 API 网关直接与服务注册表通信吗?

  5. 我们可以使用哪些工具来实现微服务之间的异步通信?

进一步阅读

想了解更多关于微服务的知识吗?看看Packt PublishingUmesh Ram SharmaPractical Microservices

第十二章:微服务中的测试和跟踪

到目前为止,我们已经看到了微服务如何帮助我们改变构建和交付应用程序到生产环境的方式。无论是更快地推出新功能,还是保持团队的小规模,微服务都能为我们实现这一点。但是在这种架构中,每个组件都是自己的小服务,我们有一些挑战需要解决。这些挑战涉及我们如何在遵循微服务方法的同时将稳定且无错误的应用程序部署到生产环境中。

在单体架构中,我们只有少量需要测试的移动组件。我们可以编写单元测试来测试单体应用程序的各个方法,然后进行集成测试以验证这些组件是否与彼此正确运行。但是现在,随着微服务的出现,图片中有越来越多的移动组件。在微服务内,我们有不同的功能,其中每个功能都被描述为自己的微服务。

在本章的过程中,我们将看看微服务的测试与单体应用程序的测试有何不同,我们现在不仅需要考虑单个微服务的正确功能,还需要确保这些服务以明确定义的方式相互通信,以满足业务需求的正确结果。

此外,由于基于微服务的应用程序中的信息从一个服务流向另一个服务,我们需要了解当客户端发出请求时到生成响应时这些信息的流动。通过这样做,我们可以准确地找到并修复可能导致不正确响应或导致应用程序性能瓶颈的任何问题。

作为读者,在本章结束时,您可以期望了解以下内容:

  • 单体应用程序和基于微服务的应用程序的测试之间的差异

  • 微服务测试的方法

  • 在微服务内实施分布式跟踪

技术要求

本书中的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter12目录下找到。

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

可以通过在终端上执行以下命令来安装基于 Python 的应用程序的要求:

pip install -r requirements.txt

除了通常的基于 Python 的要求之外,本章中的代码示例需要具有以下附加依赖项才能正常工作:

  • Docker:需要 docker 客户端来运行我们将在其中使用的一些工具...

微服务世界中的测试

随着我们远离单体架构,我们需要了解在单体应用程序开发中为我们工作的过程也需要跟随。在开发单体应用程序期间,我们通常使用单元测试等测试策略,旨在覆盖应用程序内部各个方法的功能,然后进行集成测试,用于覆盖这些方法是否与彼此正确运行的事实。

在微服务架构中,情况变得有点复杂。现在我们有了小型服务,每个服务都应该执行特定的功能。这些服务确实需要通过网络相互交互,以产生任何可能存在的业务用例的有意义输出。但事情并不会在这里结束。每个微服务都由多个需要正确工作的个体方法和接口组成。这使得测试微服务的情况变得有趣,因为现在我们不仅需要对微服务的各个组件和它们之间的交互进行单元测试,还需要测试微服务是否能够正确地相互操作。

这需要对应用程序进行更详尽的测试,使用多种不同的技术。让我们看看这些技术,以便更好地理解它们。

微服务中的单元测试

微服务内的单元测试遵循与单体应用程序测试相同的原则。我们致力于为微服务内的各个方法编写单元测试,并手动或通过自动化运行这些测试,以验证这些组件是否产生了预期的结果。

微服务中的功能测试

一旦我们确定微服务内部的各个方法正常工作,我们需要确保微服务在完全独立的情况下能够在没有任何问题的情况下运行。这是因为大多数微服务本身就是一个完整的包。它们具有自己的一套依赖项,以及可以管理其数据的数据源。

作为开发人员,重要的是确保微服务能够与其依赖项正确交互。为此,我们致力于实现微服务的功能测试。

此外,在功能测试期间,我们需要注意一些事情。由于微服务内的每个 API 端点可能需要与其他微服务交互以产生正确的结果,我们可能需要模拟某些微服务的存在,以便功能测试能够成功完成。

微服务中的集成测试

一旦我们确定我们的微服务与其依赖项正确工作,就是时候确保它们在相互交互时也具有相同的复选框。这是重要的,因为这些服务无论如何都需要相互交互,以产生任何有意义的业务结果。

在集成测试期间,我们通常旨在通过向 API 端点引入正确和不正确的参数来测试请求-响应周期,以验证微服务能够处理两种用例并且不会失败。这确保了两个外部服务之间的通信接口足够健壮,能够处理各种输入...

微服务中的端到端测试

一旦我们确保不同的微服务能够无缝地相互操作以产生有意义的结果,就是时候验证整个系统,包括不同的微服务及其依赖项,是否能够在没有任何问题的情况下工作。这种测试旨在覆盖整个系统的请求-响应周期,验证中间阶段产生的输出以及最终阶段。这被称为端到端测试。

这种测试确保整个系统以明确定义的方式运行,并且在向系统提供超出系统域的输入时不会产生任何意外。

可扩展性测试

基础设施中的每个微服务都是为处理一定的请求而设计的。随着对应用程序的请求数量增加,一些微服务可能会比其他服务承受更大的负载。

想象一下,有一个基于微服务架构的电子商务网站。正在进行限时抢购,很多顾客同时尝试结账和付款。如果处理顾客结账和付款的服务在负载增加时没有扩展,顾客可能会面临增加的响应时间或超时,给电子商务公司带来混乱,其客户服务现在可能正忙于处理...

微服务测试中的挑战

当涉及测试时,微服务架构提出了一些挑战。这些挑战有时是架构的副作用,测试策略不佳,或者对微服务架构的经验不足。以下是使微服务测试成为复杂过程的一些挑战:

  • 对微服务的知识不完整:对于基于微服务架构构建的应用程序的集成测试和调试问题,负责编写应用程序测试的测试人员需要完全了解基础设施和各个微服务。没有这些知识,测试人员无法编写能够覆盖应用程序中所有可能的请求流程的测试,这可能导致一些错误在测试阶段逃脱。

  • 协调不足:在开发微服务时,有多个团队拥有自己的一组微服务,并且通常以自己的节奏工作。这可能会导致协调问题,并且可能会延迟应用程序的测试,如果某个微服务,其上有某些依赖,仍然没有完成开发阶段。

  • 增加的复杂性:对于只有少量微服务的应用程序,测试通常很容易。但随着支持应用程序的微服务数量的增加,这种测试变得越来越繁琐。这是因为现在测试人员需要为增加的请求流程编写测试,并确保不同的 API 端点按预期运行。

  • 高灵活性:微服务允许增加灵活性。作为开发人员,我们可以自由选择技术栈来支持特定的微服务。同样,这也增加了应用程序测试的问题,因为现在测试需要考虑用于支持特定微服务的不同类型的组件。

上述观点是使测试微服务工作成为一项具有挑战性的任务的一些挑战。然而,每个问题都有解决方案,这些挑战也不例外。让我们看看我们有哪些可能的解决方法来克服这些挑战,如下所述:

  • 实施发布计划:负责构建应用程序的团队可以承诺按里程碑发布应用程序的计划。在每个里程碑阶段,根据要部署的服务的优先级,一些服务将可用于测试。这有助于改善团队的协调。

  • 标准化 API 端点:每个服务都需要公开一组用于接收请求和生成响应的 API。标准化 API 并定义特定 API 端点可能需要的参数在测试阶段非常有帮助,测试人员现在可以轻松地模拟一个服务,即使该服务尚未可用于测试。

  • 标准化开发实践:尽管负责开发特定微服务的每个团队都可以自由选择用于开发微服务的任何一组工具,但通常最好将团队可能使用的一组工具和技术标准化,以避免基础设施内部的不必要复杂性。

  • 集成 DevOps 实践:随着微服务架构的转变,应该采用 DevOps 实践,旨在使团队对其正在开发的微服务的完整生命周期负责。这不仅有助于加快开发过程,还允许在部署到生产环境之前对微服务进行彻底测试。

现在,我们知道了测试微服务架构所需的变化。这使我们能够提前规划我们的策略,并确保服务在部署到生产环境之前经过了充分测试。

在测试知识的基础上,现在是时候了解微服务领域中一个非常重要的概念,它让我们能够了解个别服务在生产环境中的行为。这也让我们能够找出基于微服务的应用程序中特定请求失败的确切位置。因此,让我们深入了解这个概念。

微服务内部的请求追踪

在任何应用程序内,一个请求可能在生成请求的最终响应之前流经多个组件。所有这些组件可能会在将请求交给另一个组件之前进行一些处理,这些处理可能在进程之前是必需的。

请求追踪使我们能够可视化关于特定请求流的丰富细节。有了对请求流的完整了解,我们现在可以着手查找可能导致请求-响应周期性能瓶颈的地方,或者找出可能导致生成不正确结果的组件。

今天,在应用程序开发世界中,任何严肃的应用程序...

OpenTracing 标准

目前,市面上有许多解决方案提供了实现应用程序追踪功能。其中一些解决方案是专有的,而另一些是开源的。

作为开发人员,您可以根据应用程序的要求和所选择解决方案提供的功能自由选择任何解决方案。但问题是,如果您想要切换到不同的追踪解决方案,因为该解决方案提供了更好的功能和更多对环境的控制,会发生什么?现在您陷入困境,因为您可能需要更改基础设施和应用程序代码中的许多内容才能使新的追踪解决方案正常工作。这是很麻烦的。

OpenTracing 标准提供了一组通用的供应商中立 API 和仪器,用于在应用程序内实现分布式追踪。任何实现了这组标准 API 的追踪解决方案都与 OpenTracing 标准兼容,并且可以与遵循相同标准的其他工具进行互操作。

我们选择 Jaeger 作为演示应用程序的追踪工具,它也是符合 OpenTracing 标准的工具。现在,让我们不再浪费时间,在我们在上一章中构建的应用程序内实现追踪。

在待办事项管理器内实现追踪

在上一章中,我们致力于构建一个简单的应用程序,允许我们管理“待办事项”列表。现在是时候在这个应用程序内实现请求追踪了。作为第一个示例,我们将致力于在用户服务内实现请求追踪。

为了使追踪工作,我们需要满足一些要求。如果您遵循了本书的技术要求部分,您就可以开始使用本教程了。但在我们深入实施追踪之前,让我们看看我们将需要的以下组件:

  • Jaeger 全一体图像: Jaeger 全一体图像为我们提供了 Jaeger 服务器、代理和 UI...

分布式追踪

在微服务世界中,请求可能在生成最终响应之前从一个服务传输到另一个服务。即使是我们简单的todo列表管理应用程序的示例也展示了这种行为,todo管理器服务经常向用户服务发出请求以实现用户身份验证,并收集有关用户的详细信息,从而创建一个新的todo列表。

分布式追踪系统旨在在请求从一个微服务到另一个微服务的传输过程中实现追踪。

为了实现这一点,追踪系统利用了许多机制,其中最简单的机制是将唯一的追踪密钥嵌入到每个请求的 HTTP 标头中。然后,追踪系统能够通过读取 HTTP 标头中存在的请求标识符来区分和聚合特定请求,从而使请求从一个服务流向另一个服务。

现在,是时候让我们看看分布式追踪是如何运作的了。为此,我们将进行一些更改,以在我们的todo管理器服务内启用追踪。

以下代码片段展示了在todo管理器服务内启用分布式追踪所需的更改:

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from jaeger_client import Config
from flask_opentracing import FlaskTracer
from opentracing_instrumentation.client_hooks import install_all_patches
import datetime
import requests

app = Flask(__name__, instance_relative_config=True)
app.config.from_object('config')

def init_tracer():
    """Initialize the tracing system."""

    config = Config(
        config={ # usually read from some yaml config
            'enabled': True,
            'sampler': {
                'type': 'const',
                'param': 1,
            },
            'logging': True,
        },
        service_name='todo-service',
        validate=True,
    )
    return config.initialize_tracer()

install_all_patches()

flask_tracer = FlaskTracer(init_tracer, True, app)

todo_service.py文件中放置了上述代码后,我们已经启用了分布式追踪。但在看到它实际运行之前,有一些事情我们需要看一看。在上述代码片段中,我们从opentracing_instrumentation库中导入了一个额外的方法install_all_patches(),如下所示:

from opentracing_instrumentation.client_hooks import install_all_patches

这个方法负责启用在 SQL 库内或通过python_requests库进行的操作的追踪。

一旦这个库与jaeger_clientflask_opentracing一起导入,我们就继续配置和启用应用程序内的追踪,这是在init_tracer方法中完成的。

现在,追踪已配置好,让我们重新启动应用程序,然后通过向 API 端点传递适当的参数,向http://localhost:5001/list/new发出请求,以创建一个新的todo列表。

一旦这个操作成功,我们可以转到运行在http://localhost:16686上的 Jaeger UI,查看 Jaeger UI 显示我们刚刚进行的 API 调用的追踪。以下截图显示了屏幕可能的样子:

从上述截图中可以看出,Jaeger UI 不仅显示了 todo 管理器 API 服务端点的请求跟踪,还进一步显示了在用户服务内调用的端点,并提供了在响应返回给客户端应用程序之前在每个 API 端点上花费了多少时间的详细信息。

有了这个,我们现在知道了微服务内部的分布式跟踪是什么样子的。但是有哪些可能的用例可以从这种追踪中受益呢?让我们找出来。

分布式追踪的好处

在基于微服务架构的应用程序中实施分布式追踪后,我们已经能够处理许多用例,例如以下用例:

  • 理解应用程序的流程: 通过分布式追踪,我们现在可以可视化客户端发来的请求在我们的应用程序内从一个服务到另一个服务的流动。这种信息对于弄清楚应用程序的工作原理并实现更好的应用程序测试非常有用。

  • 缩小错误范围:通过了解请求是如何从一个服务传递到另一个服务的,我们可以通过分析每个步骤来快速确定可能导致请求产生错误响应的服务...

总结

在本章的过程中,我们了解了向微服务架构的转变如何影响应用程序开发生命周期内的流程。我们了解了微服务应用程序内的测试与单体应用程序的测试有何不同,以及在处理微服务架构时通常需要哪些测试阶段。然后,我们了解了在测试阶段出现的挑战,由于向微服务架构的转变,以及我们如何克服这些挑战。

本章的第二部分带领我们深入了解应用程序内的分布式跟踪,我们进行了实际操作,使我们能够跟踪我们在上一章中开发的 ToDo 管理器应用程序中的请求流程。在此过程中,我们了解了跟踪的工作原理,以及分布式跟踪与常规跟踪方法的区别。我们还了解了 OpenTracing 标准如何帮助提供一个供应商中立的 API,以实现微服务应用程序内的分布式跟踪。

现在,有了所有这些知识,让我们继续来看一下另一种开发企业应用程序的方法,其中我们不是构建服务或组件,而是构建在发生某个事件时执行的函数。下一章将带领我们了解这种无服务器的应用程序开发方法。

问题

  • 我们如何为微服务编写集成测试?

  • 跟踪单体应用程序与跟踪基于微服务的应用程序有何不同?

  • 除了 Jaeger 之外,还有哪些工具可用于实现分布式跟踪?

  • 我们如何使用 Jaeger 对代码的特定部分进行仪器化?

第十三章:无服务器

正如我们迄今所探讨的,微服务提供了一种很好的替代架构,我们可以通过它来处理应用程序开发场景。具有更快的发布周期、易于启动新功能和高可伸缩性的优势,微服务对开发人员来说是一个引人注目的选择。但所有这些微服务仍然在基于服务器的环境中运行。

在基于服务器的环境中运行对于应用程序的响应时间是有用的,因为总是有一个准备接受传入请求的服务。但有一个缺点:如果没有用户,应用程序将继续消耗系统资源。

最近,应用程序开发人员开始转向一种新的应用程序开发方法。这种开发方法侧重于应用程序是事件驱动的,并且根据某些事件的发生启动操作。这种类型的应用程序被称为无服务器应用程序,因为当没有用户时它们不会继续运行,它们的实例只有在发生了某些事件时才会启动。

随着我们在本章中的深入,我们将看看这种无服务器应用程序开发方法以及它如何改变开发场景。

作为本章的读者,您将学习以下内容:

  • 应用程序开发的无服务器方法

  • 驱动无服务器架构的流程

  • 构建无服务器应用程序

  • 无服务器方法的好处

技术要求

本书中的代码清单可以在github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Pythonchapter13目录下找到.

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

此外,为了成功执行代码,还需要一些额外的软件:

  • Docker:Docker 是运行 OpenWhisk 软件平台以部署无服务器应用程序所需的依赖项。要在您的平台上安装docker,请查看docs.docker.com/install/

  • Apache OpenWhisk:Apache OpenWhisk 提供了一个开源平台,用于...

无服务器应用程序开发方法

近年来,作为开发人员,我们已经习惯了以传统方式构建应用程序并在生产基础设施上处理它们的部署。在这种传统架构中,我们开发了应用程序,其中应用程序接收来自客户端的请求,检查客户端是否被授权执行该操作,然后继续执行该操作。

一旦应用程序开发完成,我们将其部署到与我们的应用程序兼容的平台上。这涉及选择操作系统、平台运行的基础设施类型,例如裸金属服务器、虚拟机或容器,然后通过处理其可伸缩性和解决可能出现的任何问题来维护基础设施。例如,一个管理组织内员工工资的简单系统将如下所示:

在这种情况下,应用程序在服务器上持续运行,等待请求到来,并在请求到达时执行操作。

这种方法虽然非常有用,但通常会使开发人员从编写实现系统特定结果的逻辑的主要任务中分心,并使他们专注于涉及基础设施管理和可伸缩性的许多领域。

现在想象一种架构,它允许开发人员专注于只编写特定业务流程背后的逻辑,而不必担心这些逻辑将在何处执行以及如何扩展。

构建应用程序的无服务器方法提供了这些功能。在无服务器中,这是通过引入两种新的应用程序开发技术来实现的:

  • 后端即服务BaaS):BaaS 是一种新的云计算服务,为应用程序开发人员提供了通过 API 将其应用程序与后端服务链接的功能,以提供一些常见的功能集,如用户认证和数据存储。它与应用程序开发的一般架构不同,因为后端提供的这些服务可能不需要由应用程序开发人员自己开发,而是通过这些服务提供的 API 来访问这些服务。

  • 函数即服务FaaS):FaaS 是云计算的另一类别,允许开发人员专注于编写应用程序逻辑,而不必担心这些逻辑将在何处执行。在 FaaS 中,应用程序以无状态和短暂的方式运行,它们可能在执行的基础设施仅适用于少数调用,甚至可能只有一个调用。

在应用程序开发的无服务器架构中,应用程序通常被开发为作为对某个事件的响应而执行的函数。这些函数在它们自己的无状态容器中执行,这些容器可能仅在基础设施中存在几次调用。我们将在本章的后面部分看一下无服务器应用程序是如何工作的。作为一个快速参考,如果我们必须将工资系统实现为无服务器应用程序;以下图表显示了系统架构的样子:

正如我们所看到的,我们的无服务器工资单应用程序包含了 BaaS 提供的功能,其中客户直接通过Auth DB暴露的 API 与Auth DB进行交互,并且工资单生成和员工搜索在 FaaS 提供的功能中运行,它们被存储为函数,并且只在特定事件发生时执行。

这两个功能都不维护任何状态,因此它们可以在短暂的容器中运行,这些容器可能只存在很短的时间。

现在,让我们看看驱动无服务器架构的组件以及无服务器架构如何工作,以更好地理解我们如何开发最好利用无服务器架构的应用程序。

无服务器架构的组件

正如我们所见,无服务器架构为我们提供了一种开发应用程序的方式,我们只需要负责编写应用程序背后的逻辑,而不用担心如何管理运行应用程序的基础设施,以及应用程序如何根据请求的数量进行扩展或缩减。

但是是什么驱动了这种架构?让我们花点时间来看看架构内部的不同组件是如何工作的,以提供一种无服务器开发方法来进行应用开发。

正如前面讨论的,应用程序开发的无服务器方法是通过使用两种技术来实现的...

后端即服务

我们开发的大多数应用程序共享一组常见的功能。这些功能可能包括实现用户认证数据库,提供存储和检索文件的方式,或者通过电子邮件或推送通知发送通知。

大多数情况下,这些功能是通过在应用程序中引入新组件来构建的,其他组件可以与这些组件进行交互。对于基于微服务的应用程序也是如此,这些功能被实现为不同的微服务,其他微服务与这些服务进行交互以实现特定的结果。

在 BaaS 方法中,我们通过使用第三方云提供商来解耦这些功能,通过使用第三方提供商提供的 API,我们的应用程序通常集成这些功能。

为了更好地理解这一点,让我们来看一下我们之前介绍的无服务器工资管理系统。在这个系统中,我们通过利用第三方提供的 BaaS 服务,将用户认证作为我们应用程序的一个不相关的部分。

在这种方法中,我们的用户认证系统及其相关的任何数据都由第三方提供商管理。该提供商公开了一些服务的 API,我们可以使用这些 API 将服务与我们的应用程序集成。

在我们的例子中,我们通过使用服务提供的 API 来向客户端公开部分用户认证服务。这允许客户端直接与服务进行用户认证,而无需通过整个应用程序的后端。我们使用 BaaS 服务的第二个地方是当我们将员工搜索功能与用户认证服务链接起来,根据某些标准检索特定的员工。

BaaS 的这一概念为我们提供了几个优势,例如:

  • 减少开发时间:通过 BaaS,应用程序的开发人员无需担心开发可以直接从第三方服务提供商那里获取的常见功能集,而是使用服务提供商提供的 API。

  • 操作的便利性:由于服务和与服务相关的基础设施仅由云计算提供商管理,这减少了管理服务和其提供的操作的复杂性,从而减少了操作上的麻烦。

  • 易于扩展性:云计算提供商提供的服务直接由他们管理,可以轻松扩展,现在只由提供商完成。

  • 集成的灵活性:提供商提供的服务通常通过 API 进行集成。如果提供的平台有所需的服务集成 API,平台可以轻松地与服务集成,而不必担心集成背后的复杂性,从而支持不同类型的应用程序。

函数即服务

FaaS 是一个有趣的概念,也是支持无服务器架构的主要技术之一。在这种方法中,我们开发后端代码,而不用担心代码将如何部署以及在哪里执行。

针对 FaaS 的应用程序就像任何其他不需要任何特殊框架进行开发和执行的应用程序一样。FaaS 应用程序与部署在服务器上的常规应用程序之间唯一的区别在于,FaaS 应用程序在维护其状态和执行的时间方面有严格的限制。因此,让我们深入探讨这两个主要方面...

国家管理的限制

在 FaaS 模型中,应用程序的不同部分被构建为单独的函数,每个函数在发生某个事件时被执行。当应用程序应该被部署时,云提供商会自动管理应用程序将在哪里运行以及应用程序如何扩展。

与传统应用程序相比,一旦部署,就会启动服务器进程并准备接受传入连接,基于 FaaS 的应用程序是动态启动作为对某个输入的响应。一旦事件发生,函数开始执行,等待一段时间,然后包含函数的实例被终止。现在,这使得这个过程变得有趣,因为函数在基础设施中存在的时间是有限的,而且不能保证同一个函数实例也会处理下一个调用。

这使得状态管理,也就是当前执行操作的本地数据管理,在基于 FaaS 的服务中成为一个具有挑战性的任务,严重限制了我们可以在函数实例中存储的本地数据。

为了处理这种情况,我们依赖于一个可以为我们存储状态数据的外部服务。这可能包括使用外部数据库或缓存服务器,其中数据可以被持久化以供将来参考。

执行时间限制

一旦函数在 FaaS 服务中开始执行,它只有有限的时间来完成执行。大多数知名的云服务提供商都对 FaaS 服务中的函数执行时间设置了限制。例如,如果我们选择 AWS 最著名的 FaaS 服务 AWS Lambda,函数的最大执行时间限制为五分钟。其他提供商的限制可能略有不同,但不会太高。

现在,这对我们作为应用程序开发人员来说是一个有趣的案例。如果我们试图将一个应用程序组件实现为一个函数,可能需要花费相当长的时间...

在 FaaS 中执行函数

一旦我们将应用程序开发为函数形式,我们需要一个地方来托管和运行它。这些函数的托管地点由云服务提供商提供。一旦我们成功托管了这些函数并实施了特定函数执行的规则,云服务提供商就有责任处理这些函数的正确执行。

现在,当这些函数需要执行时,云服务提供商确定执行特定函数所需的正确环境。一旦确定了这个环境,云服务提供商就会启动一个临时容器,函数代码就驻留在这个容器内。这个容器为函数提供了完全隔离,使其与可能在环境中执行的其他函数隔离开来。一旦容器成功启动,函数就会执行,并返回响应。

有趣的部分发生在函数完成执行后。一旦函数完成执行,云服务提供商可以终止包含函数的容器实例,也可以保持其活动以处理新的请求。大多数情况下,决定是基于到达的请求频率和用户设置的策略来做出的。

如果一个函数实例仍在运行并等待,新的请求可能会被重定向到该实例,而如果没有正在运行的函数实例,云服务提供商将启动一个新实例并将请求重定向到该实例。

通过这样,我们对 FaaS 在无服务器架构中的工作原理以及它如何使我们能够开发无服务器应用程序有了一个很好的了解。但是这些函数实际上是如何触发的呢?这就引出了构成无服务器服务的另一个重要组件。让我们看看它是什么。

无服务器架构中的 API 网关

在第十一章 采用微服务方法中,当我们了解了微服务的概念时,我们介绍了 API 网关以及它们如何帮助开发微服务。这些 API 网关在基于无服务器架构的应用程序开发中也起着重要作用。

API 网关只是嵌入有关应用程序的某些 API 端点的信息并将这些端点与某些处理程序相关联的 HTTP 服务器。一旦向某个 API 端点发出请求,就会调用与 API 端点相关联的处理程序来处理请求。

在无服务器架构中,与特定 API 端点相关联的处理程序...

理解无服务器应用程序的执行

到目前为止,我们已经了解到无服务器应用程序是以函数的形式构建的,这些函数基于某些事件的发生而执行。此外,这些函数并不永远保持活动状态。相反,这些函数在需要时被执行。那么,当请求到来时,提供者如何处理这些函数的执行呢?让我们来看一下。

冷启动函数

当应用程序刚刚部署时,很容易想象当前不会有任何正在执行的函数实例。当新请求到来并要求由我们刚刚部署在基础设施上的函数提供的功能时。现在,云提供者系统被通知说没有正在运行的函数实例可以处理传入的请求。

一旦提供者系统意识到情况,它就会生成一个包含函数代码的新实例。这个实例现在开始根据请求中提供的参数执行函数,并且函数生成响应并发送回请求的客户端。

热启动函数

与冷启动完全相反,热启动函数利用已经在提供者基础设施中运行的函数的现有实例。当这种情况发生时,传入的请求不必等待新实例生成才能处理请求。这允许请求快速处理。

这里需要注意一点:即使在函数的热启动情况下,也不会存储函数先前执行的状态。

现在我们知道了函数性能可能取决于的一个主要因素。现在让我们继续构建我们的第一个无服务器应用程序。

构建我们的第一个无服务器应用程序

有了我们对无服务器架构及其工作原理的基本了解,现在是时候开发我们的第一个无服务器应用程序了。在本教程中,我们将使用 Apache OpenWhisk 项目,在本地开发系统上运行我们的演示应用程序。因此,让我们看看 Apache OpenWhisk 为我们提供了什么,以及我们如何利用该平台来获益。

Apache OpenWhisk 的快速介绍

Apache OpenWhisk 平台为我们提供了功能和功能,使我们能够设置自己的平台来运行无服务器应用程序。该项目提供了根据环境中某些事件的触发执行函数的功能。

这些函数的执行发生在 docker 容器内,OpenWhisk 平台管理其中的函数的部署和扩展。

以下是平台提供的一些功能:

  • 易于使用的工具: 该平台提供了许多工具,使我们能够轻松打包和移植应用程序以在 OpenWhisk 平台上运行,除了应用程序遵循平台定义的一组约定。

  • 使用容器进行隔离: 该平台通过使用 Docker 容器来隔离不同的功能,使得每个功能都在自己独立的环境中运行,以避免任何环境依赖冲突。

  • 支持多种语言: OpenWhisk 平台为我们提供了许多支持的语言平台,我们可以使用这些平台来构建我们的无服务器应用程序。这还包括使用 Go、C++和 Rust 构建的二进制可执行文件。

  • 内置 API 网关: OpenWhisk 软件包配备了自己的内置 API 网关,使我们能够通过 RESTful API 端点轻松集成应用程序。

所有这些功能使 OpenWhisk 成为在云端或本地开发环境中运行无服务器应用程序的绝佳平台。

但在我们开始构建应用程序之前,我们需要在系统上部署 OpenWhisk。要部署项目,请按照本章开头的技术要求部分中的步骤进行操作。

对于演示,我们将构建一个应用程序,该应用程序会查询 GitHub API,并检索与我们的用户帐户关联的存储库。

设置开发环境

在我们开始编写应用程序的代码之前,我们需要先安装一些依赖项。因此,让我们先构建环境,然后开始编写将驱动我们应用程序的代码。

作为第一步,让我们创建一个目录,其中包含与我们项目相关的所有文件。让我们将这个文件夹命名为github_demo。以下命令可以帮助我们创建这个文件夹:

mkdir github_demo

一旦我们设置好目录,让我们进入目录并设置一些东西:

cd github_demo

完成这些步骤后,我们现在可以设置我们的项目了。在我们开始编写代码之前,让我们完成虚拟环境的设置,这将帮助我们保持项目依赖项的隔离。...

构建我们的配置文件

为了这个应用程序,我们将使用一个配置文件来存储与我们用户帐户相关的数据,这将允许我们对Github API 进行身份验证。为此,在我们的项目目录中,创建一个名为config.ini的新文件,其中包含以下内容:

[github_auth]
username = ‘<your github username>’
password = ‘<your github password>’

一旦我们完成了配置文件的设置,让我们继续编写我们的应用程序代码,这将与Github交互以获取我们的repos

与 GitHub API 集成

现在我们即将开始我们应用程序的实际部分,让我们开始编写代码。以下代码片段描述了我们用来查询Github API 的代码:

from github import Githubimport configparser# Provide the location of where the config file existsCONFIG_FILE = 'config.ini'def parse_config():    """Parse the configuration file and setup the required configuration."""    config = configparser.ConfigParser()    config.read(CONFIG_FILE)    if 'github_auth' not in config.sections():        return False    username = config['github_auth']['username']    password = config['github_auth']['password']    return (username, password)def get_repos():    """Retrieve the github repos associated with the user.    Returns: Dict ...

准备好与 OpenWhisk 一起运行的代码

代码就绪后,现在是时候将其转换为 OpenWhisk 可以执行的格式了。

要在 OpenWhisk 内执行任何功能,代码应该从__main__.py文件中调用。因此,让我们创建该文件并添加以下内容:

from github_demo import get_repos

def main(dict):
    repos = get_repos()
    return repos

代码就位后,让我们试着理解我们在这里做了什么。首先,我们导入了在github_demo.py文件中创建的get_repos函数,该函数有助于从Github API 中检索内容:

from github_demo import get_repos

然后,我们定义main()函数,OpenWhisk 将调用该函数来执行代码。任何存在于主函数中的代码都将由 OpenWhisk 直接执行。因此,我们使用这种方法来调用我们的get_repos()函数:

def main(dict):

一旦完成这一步,我们就快要准备好部署我们的应用程序了。

朝着部署的最后步骤迈进

在我们部署应用程序之前,我们还有一些步骤要完成。为了成功安装应用程序,让我们创建一个文件,用于存储运行我们项目所需的依赖项。以下命令可帮助我们安装所需的依赖项:

pip freeze > requirements.txt

有了这些要求打包好后,现在让我们打包我们的项目,以便可以部署到 OpenWhisk。为此,运行以下命令可以帮助我们创建不同项目组件的包:

tar -zcvf github_demo.tar.gz github_demo

有了这个,我们现在已经准备好将我们的应用程序部署到 OpenWhisk 了。

部署到 OpenWhisk

一旦我们准备好部署包,我们需要运行 OpenWhisk 提供的一些命令,以便在平台上启动并运行包。

作为第一步,我们必须执行以下命令,将包上传到 OpenWhisk:

wsk action create github_demo –kind python:3 github_demo.tar.gz

一旦执行了这个命令,包将被上传到 OpenWhisk 平台,并准备好运行。

现在,要调用应用程序,我们可以运行以下命令,以异步方式执行应用程序:

wsk action invoke github_demo

完成后,我们的应用程序开始以异步方式执行。通过异步运行,我们的意思是命令的执行不会等到函数执行结束,而是会提供一个可以用来跟踪调用结果的操作激活 ID。

现在,让我们看看应用程序部署后 OpenWhisk 如何处理这个应用程序的执行。

了解 Openwhisk 内应用程序的执行

有了演示应用程序,现在是时候了解这个应用程序在幕后是如何执行的了。

应用程序成功执行的背后,有几个步骤涉及,从我们运行wsk action invoke命令开始执行我们的应用程序。因此,让我们看看幕后发生的步骤:

  1. 发出 API 调用: 我们构建的每个要部署到 OpenWhisk 的操作都被映射为将调用该操作的 API 端点。当我们运行wsk action invoke时,该命令会调用为所提供的函数映射的 API 端点。然后,这个调用被 OpenWhisk 内的 Nginx 拦截,起到...

无服务器的优势

了解了无服务器应用程序的工作原理后,现在是时候看看这种开发方法提供的优势了:

  • 减少开发工作量: 通过使用第三方云提供商提供的服务,我们可以减少一些在应用程序中找到的常见功能的开发工作量,例如用户身份验证、通知和文件存储。所有这些功能都可以通过云提供商提供的 API 来实现。

  • 操作复杂性较低: 无服务器应用程序的执行和扩展由云服务提供商管理,这消除了管理我们自己的基础设施以处理应用程序执行的操作复杂性。

  • 高可用性: 以无服务器方式构建的应用程序由于基础设施由云提供商管理,因此可以在世界各地的不同数据中心运行应用程序,从而降低了应用程序的可用性受影响的机会。

  • 优化资源分配: 由于只有在发生某个事件时才执行函数,因此只有在执行特定函数时才分配资源,这优化了跨基础设施的资源使用。

  • 编程语言的选择: 大多数无服务器解决方案都支持各种类型的编程语言,这使我们能够使用最佳的技术栈来实现我们的解决方案。

有了这个,我们现在有足够的理由选择无服务器开发方法,以便我们的需求与构建无服务器应用程序所需遵循的开发方法论相一致。

总结

在我们阅读本章的过程中,我们看到了无服务器架构如何成为应用程序开发的新趋势,以及这种架构的工作原理。我们涵盖了无服务器架构的不同组件,并介绍了后端即服务和函数即服务的概念,它们支持无服务器架构。然后,我们看了一下 API 网关在架构中的作用,以及无服务器应用程序中的 API 网关与微服务中使用的 API 网关有何不同。

之后,我们开始构建我们的第一个无服务器应用程序,并通过 Apache OpenWhisk 运行它,该平台提供了一个运行无服务器应用程序的开源平台。在这里,我们也深入探讨了...

问题

  1. 无服务器架构提供了哪些优势?

  2. BaaS 如何帮助应用程序开发?

  3. API 网关如何帮助执行无服务器应用程序?

  4. 有哪些因素使将应用程序转换为无服务器格式变得困难?

进一步阅读

你觉得无服务器架构的理念有趣吗?看看Jalem Raj RohitPackt Publishing出版的使用 Python 构建无服务器应用程序,深入了解无服务器架构。

第十四章:部署到云端

到目前为止,我们的重点大部分都花在了应用程序的开发上,不管是以大型单体应用程序的形式还是以基于微服务的应用程序形式,其中存在许多服务。为了让这些应用程序对用户可用,应用程序需要部署到一定的地方,以便一般用户可以与应用程序进行交互。

在 DevOps 的现代世界中,部署策略以及应用程序的部署地点在定义应用程序如何工作和向用户提供访问权限方面起着重要作用。关于应用程序部署的决策可以影响基础设施中的许多事情,例如运行特定应用程序所需的基础设施的复杂性,以及应用程序内的新功能将如何推出。

在本章的过程中,我们将看看如何为单体应用程序和基于微服务的应用程序创建部署,以及如何实施部署策略,优先考虑应用程序在基础设施上部署后的稳定性。我们还将研究使用容器部署应用程序的现代方法,并在私有、公共和混合云部署之间做出选择。

作为本章的读者,您将学习以下内容:

  • 部署策略的需求

  • 将应用程序容器化以进行部署

  • 将测试集成为部署策略的一部分

  • 在私有云上部署

  • 在公共云上部署

  • 向混合云的转变

技术要求

为了理解本章,对使用 Docker 进行容器化和至少一个云提供商的 CLI 的知识将会有所帮助。

部署企业应用程序

在本书的过程中,我们已经看到了如何使用不同的原则开发企业应用程序,无论是通过单体应用程序开发的方式还是通过使用小型微服务来开发应用程序。但这些事情都汇聚在一个共同点。为了让我们的应用程序对一般用户可用,它们需要部署到开发环境之外的某个地方,以便一般用户可以访问。

为了成功部署,特定应用程序的基础设施和所选择的部署类型需要提供一定的功能:

  • 高可用性: 应用程序部署的任何基础设施都需要提供高可用性,以便为用户提供几乎无中断的应用程序服务。如果基础设施容易频繁宕机,那么可能会导致应用程序的可用性严重中断,并且可能会导致依赖于应用程序的流程停滞,直到应用程序运行的基础设施恢复在线。

  • 低延迟: 为应用程序提供服务的基础设施的延迟应该低,以便为用户提供足够的响应时间。如果基础设施的延迟很高,用户可能需要等待与应用程序进行交互,或者应用程序生成的响应可能严重影响他们的生产力。

  • 容错性: 部署基础设施应具有容错性,并且应能够从偶尔的几个节点故障中恢复。如果缺乏容错性,即使基础设施内出现单个问题,也足以使整个应用程序崩溃,给应用程序的用户造成严重的可靠性问题。

这只是需要满足的基本基础设施要求,才能考虑将应用程序部署到该基础设施上。可能会有其他要求,这些要求可能是由于选择特定的基础设施部署策略而施加的,但讨论这些要求超出了本书的范围。

到目前为止,我们经常听到部署策略这个词,但当我们说我们需要为应用程序选择适当的部署策略时,我们到底是什么意思呢?让我们花点时间来探讨一下。

选择部署策略

一旦我们确定我们现在准备将应用程序投入生产,我们现在的任务是找出我们将要使用的应用程序部署策略。

应用程序的部署策略通常会规定应用程序的推出方式,取决于我们所拥有的应用程序的类型。这些部署策略涵盖了有关在生产中使应用程序可用所需的步骤的信息,并且可能还涵盖有关如何在应用程序中推出新功能的其他重要领域。

因此,让我们花点时间讨论可用的不同部署策略...

不同的部署策略

在软件开发世界中,没有一种解决方案适用于所有情况,即使在选择我们将要遵循的部署策略类型时也是如此。

我们选择的每种部署策略都会有与之相关的优缺点。一些部署策略提供的灵活性不大,但实施起来简单,而其他部署策略非常灵活,但在实施过程中可能会变得麻烦。作为开发人员,选择取决于我们如何处理应用程序的部署。主要地,我们将在本章的过程中涵盖六种部署策略,即:

  • 重新创建部署

  • 滚动部署

  • 蓝绿部署

  • 金丝雀部署

  • A/B 部署

  • 影子部署

因此,让我们花点时间来熟悉每种部署策略。

重新创建部署

这是应用程序部署的最传统方法。在这种部署策略中,我们简单地销毁旧版本的应用程序,并引入新版本的应用程序,并将所有用户请求路由到新版本的应用程序。以下图表显示了重新创建部署策略的表示:

这种策略对于遵循单体开发方法的应用程序的部署非常有用,因为对于每个新功能或升级,整个应用程序都需要重新部署。

使用重新创建部署的优势...

滚动部署

在部署应用程序的滚动部署模型中,我们不会突然关闭所有旧版本应用程序的实例,以替换它们为新版本。相反,我们采取逐步推出新应用程序版本的方法,覆盖整个基础设施。

在这个过程中,我们首先启动升级后的应用程序的新实例,放在负载均衡器后面,一旦它准备好接受流量,我们就移除旧版本应用程序的等效实例。这个过程会持续进行,直到所有旧版本应用程序的实例都被新版本实例替换。以下图表显示了滚动部署策略的表示:

这种部署策略对于单片应用程序也是一个不错的选择,如果我们希望通过应用程序的逐步推出来实现低停机时间,因为应用程序在基础设施内逐渐推出。

滚动部署提供了几个好处,例如:

  • 易于恢复故障升级:如果应用程序的升级版本引入了一些错误或故障,我们可以在中间阶段轻松回滚升级。这是因为新版本在基础设施内逐渐推出。

  • 易于设置:具有应用程序运行基础设施知识的情况下,这种部署策略易于设置和自动化,基础设施的不同部分逐一更新。

蓝/绿部署

蓝/绿部署策略是一种有趣的策略。该策略实现了一系列用于测试应用程序并在生产环境中启动的技术的混合。

在蓝/绿部署方法中,更新的应用程序被引入基础设施,其实例数量与旧版本的应用程序相同。完成后,在基础设施内测试新版本的应用程序。一旦发现版本稳定,流量就会从旧版本切换到新版本的应用程序,然后旧版本的应用程序被废弃。以下图表显示了蓝/绿...

金丝雀部署

在这种部署方法中,我们遵循与蓝/绿部署相同的策略,但有一个小改变。在蓝/绿部署中,测试是在内部进行的,一旦应用程序的新版本被标记为稳定,所有请求都将立即切换到新版本。

在金丝雀部署方法中,测试是基于实际用户请求进行的。负载均衡器被配置为将一定百分比的请求重定向到已部署在基础设施中的金丝雀版本,以查看新版本在实际请求存在的情况下的性能。

以下图表显示了金丝雀部署策略的表示:

当应用程序的内部测试被认为不足以满足要求,并且对应用程序运行的基础设施的稳定性存在疑虑时,通常会使用这种部署方法。

这种测试方法提供了在生产用例中测试应用程序的优势,同时允许在应用程序未达到预期结果时轻松回滚。

使用这种部署方法的缺点是基础设施内部的复杂性增加,现在需要智能地将部分传入请求路由到应用程序的金丝雀版本。

A/B 部署

A/B 部署方法与金丝雀部署方法有很多相似之处,其中新版本的应用程序被引入生产基础设施,并且一定数量的传入请求被重定向到金丝雀版本。

在 A/B 部署中,应用程序的升级版本(版本 B)被引入生产基础设施,然后负载均衡器被配置为根据一些预定义的标准将一定数量的请求重定向到升级版本。

当我们不确定升级版本将如何影响某个用户子集时,就需要这种部署方法。例如,使用智能手机的用户将如何受到升级版本的影响...

阴影部署

在阴影部署方法中,我们引入了一种新方法。与金丝雀部署或 A/B 部署相比,在这些方法中,一定数量的请求由旧版本处理,一定数量的请求由新版本处理,我们在生产基础设施中有两个应用程序版本。这些是旧版本和包含最新更新的新版本。

在阴影部署中,应用程序的更新版本看到与旧稳定版本应用程序发送的完全相同的请求,但新版本应用程序实例的任何处理都不会影响仅由生产中的稳定实例处理的请求的响应。以下图表显示了阴影部署策略的表示:

这种部署方式通常适用于基于微服务的应用程序,并且在开发人员希望测试应用程序在负载变化时的行为时使用。

这种部署方式也被用来检查应用程序在真实使用情况下是否表现正常,这是在内部环境无法测试的。

采用这种方式的唯一缺点是这种部署方式会增加基础设施成本,因为我们需要同时以全面规模运行旧版本和新版本。

现在,通过这个,我们已经习惯了不同类型的部署策略,这些策略可以帮助我们决定如何在生产环境中部署应用程序。虽然其中一些部署策略侧重于流程的简单性,但其他部署策略侧重于确保部署的新版本足够稳定并提供最佳结果。

选择应用程序部署的部署策略在很大程度上取决于几个因素,包括您可以承担的基础设施成本、可以花费在基础设施维护上的时间以及您计划部署的应用程序类型。另一个限制可以使用的部署策略的重要因素是应用程序之间的 API 是否发生了变化。通常,这些变化受 SLA 的约束,如果发生了变化,可能需要更新部署策略以适应所做的更改。

在撰写本书时,许多组织正在将云作为他们首选的基础设施选择,用于在生产环境中部署他们的应用程序。因此,让我们花一些时间了解目前存在的各种云基础设施,以及我们如何决定使用哪种基础设施进行部署。

选择基础设施

应用程序需要一个可以运行的基础设施。根据存在的应用程序类型,所需的基础设施可能会发生变化。选择哪种基础设施用于部署应用程序的选择受到正在部署的应用程序类型、应用程序的复杂性以及应用程序将支持的用例类型的极大影响。

在选择应用程序部署的基础设施时,另一个重要因素是对应用程序可扩展性的关注,包括我们可以如何扩展应用程序以及可以采用的扩展类型的复杂性。

首先让我们来看一下...

传统基础设施

过去,当应用程序使用大型单块来执行多个业务流程时,开发人员和组织通常会采用由大型主机或虚拟机组成的基础设施,以提供运行应用程序所需的充足资源。

这些裸机或虚拟机都配备了运行应用程序所需的所有要求,然后将应用程序部署在这些机器上,并提供给用户供一般使用。

这种基础设施选择运行良好,甚至允许多个应用程序存在于同一台强大的裸机服务器上,通过使用虚拟机进行隔离,将服务器的硬件抽象化。

然而,这种方法存在许多问题,例如:

  • 基础设施成本高:对于使用裸机系统或虚拟机的部署,基础设施成本很高。组织要么需要购买能够运行这些应用程序的强大服务器,要么必须求助于专门的托管提供商,这通常成本很高。

  • 开销增加:对于任何运行在虚拟机内的应用程序,运行支持应用程序的完整虚拟化操作系统所产生的开销非常高,大大减少了可以共存在同一硬件上的应用程序数量。

  • 启动时间长:随着负载的增加,需要生成应用程序的新实例来处理增加的请求数量。然而,由于需要为虚拟机启动完成整个过程,启动完整的虚拟机及其中运行的应用程序实例是一个缓慢的过程。

  • 扩展困难:在传统基础设施中,可以进行的水平扩展量非常有限,通常唯一的选择是通过使用垂直扩展来扩展应用程序,即根据需求增加应用程序所需的资源。

这些缺点使开发人员开始考虑替代传统的应用程序部署方式。

推动远离传统基础设施的另一个主要原因是转向容器化的应用程序打包方式。让我们来看看这是什么。

容器化的应用程序打包方式

随着现代硬件的出现和软件工程的进步,一些操作系统推出了一种轻量级的替代方案,用以取代笨重的虚拟机。这种替代方案以容器的形式出现,承诺不仅可以更轻量地进行应用程序隔离,而且由于它们根本不会对底层硬件进行抽象,因此也可以快速启动。

随着应用程序开发向微服务架构的转变,容器化的应用程序打包方式变得越来越主流。在这种方法中,每个微服务都被打包为一个单独的容器,可以部署...

向云端转移

在过去的十年里,许多云服务提供商已经出现,以帮助支持应用程序部署。每个云服务提供商都提供一套独特的功能,以使他们的服务在吸引组织和开发人员使用其平台进行应用程序部署时脱颖而出。

云部署模型的转变为负责应用程序开发的开发人员/组织提供了各种优势,包括以下内容:

  • 降低基础设施维护成本:随着应用程序部署转移到云端,维护基础设施的成本正在降低。这是因为云服务提供商现在负责维护应用程序运行的硬件,个人开发人员和组织不需要购买这些硬件或处理可能发生的任何问题。

  • 高运行时间:大多数云提供商保证其基础设施的高运行时间,这是由于他们在端上进行的大量基础设施复制。受益者是在云中维护特定应用程序的开发人员,因为现在他们可以为应用程序用户提供高运行时间,而不必担心基础设施崩溃可能导致的生产损失。

  • 低延迟:在云部署方法中,开发人员可以为用户提供低延迟的应用程序。这是通过在云服务提供商的不同地理数据中心之间复制应用程序实例来实现的。一旦应用程序被复制,云服务提供商就会将请求重新路由到靠近客户端的应用程序服务器,以便实现低延迟响应。

  • 易扩展:随着应用程序的负载增加,可能需要生成新的应用程序实例来处理增加的负载。云服务提供商通常提供动态扩展应用程序的功能,随着负载的增加而扩展应用程序,并随着负载的减少而缩减实例。这提供了一种高吞吐量、低成本的解决方案,可以处理高峰负载,而不必担心传统基础设施通常需要的手动干预。此外,与传统基础设施相比,这种扩展的响应时间通常较低。

所有前述观点都为应用程序转向基于云的部署提供了有力的论据。但根据组织的需求,他们可能希望或不希望将其应用程序部署在组织几乎无法控制的第三方服务器上。为了处理这种情况,组织可能决定转向在其基础设施上运行并处理组织内所有应用程序部署的私有云。因此,让我们花一些时间了解当前存在的各种云部署模型。

不同类型的云部署

对于企业来说,他们非常关注他们的应用程序在哪里运行。这是因为企业处理各种可能包含大量敏感信息的数据,任何一种违规行为都可能威胁到他们的业务。作为构建企业应用程序的开发人员,我们有责任建议和决定应该使用哪种云部署来部署应用程序。目前存在的云类型主要分为两大类:

  • 公共云

  • 私有云

最近,还出现了第三种类别,称为混合云。因此,让我们来看看...

私有云

私有云是由企业严格管理的一组计算资源。这些云运行在企业的企业内网中,通常位于组织拥有的数据中心或由第三方维护。

这些云实施了非常严格的安全策略,定义了在其上运行的应用程序如何被访问以及谁可以访问它们。

通常,企业选择私有云是因为以下几点:

  • 企业已经拥有自己的数据中心,不想再投资于第三方云

  • 企业运行的应用程序非常重视安全性,并且公共云提供商实施的安全策略不能被信任或不足以满足所需的用例。

私有云提供了一定的优势:

  • 更灵活:由于组织控制决定私有云中将存在哪些计算资源,组织保持了灵活性,可以做出符合其最佳利益的决定

  • 提高安全性:组织可以自由地在企业防火墙或内部网络后运行其云基础设施,并实施严格的安全策略,这在使用公共云时可能是不可能的

对于处理安全敏感数据并且没有成本障碍的企业来说,私有云是部署和运行应用程序的不错选择。

公共云

在公共云中,计算资源由第三方云服务提供商拥有和管理。作为企业,您部署的应用与其他应用共享相同的硬件资源,这些应用可能是您开发的,也可能是其他组织开发的。

当组织的应用程序不涉及可能需要严格的安全策略来防止任何事件发生的安全敏感数据,或者运行常用的应用程序,例如他们的电子邮件服务器时,通常会使用公共云。

公共云提供的优势是巨大的。其中一些如下:

  • 降低成本:由于公共云提供商提供的基础设施...

混合云

混合云部署模型提供了私有云和公共云方法的最佳结合。在这里,来自私有云和公共云的计算资源被汇集,应用程序可以根据需要从私有云转移到公共云。

企业通常采用这种部署模型,在公共云上运行一些不太安全敏感的应用程序,同时在私有云中运行安全敏感的应用程序。

通常采取的另一种方法是首先在私有云中部署应用程序,然后当请求数量增加时,从公共云中汇集资源,通过在公共云中启动更多进程来扩展应用程序。

混合云方法的好处如下:

  • 控制:组织可以控制在私有云中运行安全敏感的应用程序,同时在公共云上运行不太安全敏感的应用程序

  • 灵活性:在需要时,组织可以从公共云中汇集资源来处理更高的负载

  • 成本效益:由于只有在应用程序需求高时才从公共云中汇集资源,组织可以通过仅在需要时使用公共云资源来节省公共云的成本

对于可以轻松从一个地方过渡到另一个地方的应用,或者可能需要动态扩展同时保持安全性的基础设施的应用,混合云部署方法为一个不错的选择。

总结

在我们对这一章的探索中,我们看了如何做出与企业应用程序部署相关的决策。我们探讨了不同的部署策略以及它们如何影响我们的应用程序在生产环境中的运行方式。接下来,我们了解了可用于部署单片和基于微服务的应用程序的六种不同部署策略,以及它们提供的优缺点。

一旦熟悉了部署策略,我们深入研究了应用程序部署的基础设施选择,并了解了从传统...过渡到基于微服务的开发方法如何推动了转变。

问题

  1. 采用蓝/绿部署方法有哪些好处?

  2. 在应用程序投入生产之前,金丝雀部署如何帮助测试应用程序?

  3. 如果我们使用虚拟机的方法来运行基于微服务的应用程序,可能会面临哪些问题?

  4. 我们如何在混合云模型中处理部署?

第十五章:企业应用集成及其模式

在本书的过程中,我们已经介绍了如何实现企业应用程序。这些应用程序要么是实现了大量组件以提供一定功能集的大型单体,要么是基于微服务的应用程序,其中应用程序由多个小型服务组成,所有这些服务都通过网络相互交互,以根据业务需求提供某种功能并提供输出。

但是,在任何企业中,我们很少会开发的应用程序是唯一存在的应用程序。相反,大多数情况下,企业基础设施将包括许多应用程序,这些应用程序具有...

技术要求

本章的后续不需要任何特殊工具或在开发系统上存在特定软件。但对中间件和企业服务总线解决方案功能的了解将有助于理解本章的上下文。

EAI 的需求

在任何大型企业中,可能存在一些应用程序来解决特定的问题领域。这些系统中的每一个都致力于解决一组特定的问题。通常,这种方法对于在企业内部构建应用程序是可取的,因为现在应用程序可以使用最佳的可用技术栈来解决其领域的问题。

但要使这些应用程序产生任何有用的业务影响,通常情况下这些应用程序需要以某种方式相互通信,以促进可能存在于一个应用程序中并且另一个应用程序需要的数据的交换。

但由于不同应用程序的集成是一项具有挑战性的任务,因为...

点对点集成

解决问题的方法之一是实现应用程序之间的点对点集成。这意味着每个应用程序都有一组连接器,允许它与另一个应用程序通信。对于需要彼此通信的每一对应用程序,都需要存在一个单独的连接器来促进应用程序之间的通信。

当需要相互通信的应用程序数量较少时,这种方法是完全可以接受的。但随着企业的发展,其需求也会增长,这意味着需要将新应用程序纳入基础设施中。现在,随着应用程序数量的增加,需要用于促进不同应用程序之间通信的连接器数量也将开始增长,达到一个基础设施变得过于复杂以至无法管理和维护的水平。

为了在企业内部集成大量应用程序,可能会随着时间的推移而增长,我们可能需要一些更灵活的东西,可以帮助我们标准化这些应用程序之间的通信方式。转向 EAI 正是我们正在考虑的架构类型,因为它旨在帮助我们实现这些目标。因此,现在让我们来看看 EAI 为我们提供了什么。

走向 EAI

当我们的目标是标准化基础设施中运行的不同应用程序之间的通信方式以及它们将如何存储数据时,EAI 方法确实为我们提供了一种选择,不仅具有灵活性,而且可扩展,而不会给基础设施引入不必要的复杂性。

EAI 模式为我们提供了一个框架,其中包含帮助我们标准化不同应用程序之间通信方式的工具和技术。EAI 框架通常配备了促进应用程序之间数据交换和数据从一种格式转换为另一种格式的组件,并充当不同应用程序之间的粘合层的组件。

传统的 EAI 方法

在 EAI 的早期,应用程序需要以各种格式相互交互,这可能包括通信一些信息或交换数据。为了促进这种交换,组织采用了 EAI 的中心枢纽模型。

在这种中心枢纽模型中,有一个基于路由器的中间件组件和事件的概念。每当一个应用程序的状态发生变化时,该应用程序会生成一个事件。其他应用程序订阅它们感兴趣的事件流。

现在,每当生成新事件时,路由器负责将事件传递给感兴趣的应用程序,并处理数据从一种格式转换为另一种格式,以便应用程序之间可以相互通信。在这种方法中,路由器成为促进不同应用程序之间集成的中心点。

路由器提供了许多功能,例如以下功能:

  • 适配器和 SDK: 应用程序需要与路由器通信以触发事件,它们需要一种促进应用程序和路由器之间连接的粘合剂。路由中间件提供的适配器用于提供必要的粘合层。如果某个应用程序没有受支持的适配器,路由中间件将提供 SDK 以促进适配器的开发。

  • 消息转换: 当生成新事件时,路由器根据一组预定义的规则,将与事件相关的消息转换为另一个应用程序可以消费的格式。这种功能对于促进两个不同应用程序之间的通信非常重要,每个应用程序都有自己的数据存储格式和通信风格。

  • 智能路由: 应用程序需要无缝地相互配合,必须保证正确的事件能够到达正确的目标受众。路由中间件用于实现基于应用程序生成的事件和对该事件感兴趣的应用程序的智能路由,以便将作为事件一部分生成的消息传递给正确的接收者。

这种方法提供了一种很好的机制,可以消除企业基础设施中不必要的复杂性,如果每个应用程序都必须直接与其他应用程序通信,管理自己的连接器并处理数据一致性,那么这种复杂性将会增加。而采用这种方法,路由器促进了不同应用程序之间的通信。

但是,尽管这种方法带来了许多好处,但它也存在一些严重的缺点:

  • 单点故障: EAI 的经纪模型被证明是一个单点故障。如果路由中间件出现故障,不同应用程序之间的所有通信将停止。

  • 集中逻辑: 数据转换的逻辑以及数据的路由都集中在单个路由器中。这使得经纪成为一个复杂的组件,使得经纪的运营和维护成为一项艰巨的任务。

  • 扩展性差: 当路由器的负载增加时,路由器处理消息的能力会受到影响。这会导致不同应用程序之间的数据状态不一致。此外,如果试图连接到彼此的应用程序位于世界各地的不同地理位置,那么单一的、集中位置的路由器将成为路由器地理扩展的障碍。

  • 专有解决方案:在早期,当存在基于路由器的集线器和分支集成企业应用程序的方法时,大多数解决方案通常是专有的,只支持供应商的子集。对于从不受支持的供应商集成的应用程序,这对开发人员来说是一个巨大的问题,然后他们需要基于提供的 SDK 编写和维护自己的适配器。

所有这些问题都需要实施更好的方法,不会遭受基于路由器的方法所遇到的问题。最终,企业开始转向面向服务的架构SOA)模型,并引入了企业服务总线ESB)来集成 SOA 内部的不同服务。因此,让我们看看 ESB 如何改变了 EAI 的发生方式。

ESB 的引入

随着时代的推移,企业转向了一种新的应用程序开发模式。这种模式用于将应用程序建模为服务,其中每个服务提供一定的业务能力。因此,例如,在一个企业中将有一个工资服务,该服务将提供与员工工资管理相关的所有必要功能,例如处理新员工的数据,记录他们获得的薪水金额并生成每月的工资单。

现在,这些服务需要相互集成,以便在这些服务之间促进数据交换。在这一点上,企业需要一些...

EAI 中的模式

EAI 是一种方法,与之相关的有几种模式,它们规定了应用程序如何集成。通常使用哪种模式取决于企业基础架构中存在的应用程序类型,以及集成中存在哪些挑战。

因此,让我们看看这些模式通常是如何实现的。

集成模式

在 EAI 期间,集成模式定义了应用程序如何相互集成。这可能会定义不同应用程序如何相互通信以及这些应用程序如何转换数据。因此,让我们看看应用程序如何相互集成的两种广泛方式。

调解模式

在 EAI 的调解模式中,有一个负责事件传播的中央组件。例如,在基于代理的中间件模型中,每当一个应用程序生成一个事件时,该事件由中间件代理处理,然后负责将事件传播到对该事件感兴趣的其他应用程序。

在这种集成模式中,通常应用程序直接相互交互,由中间件代理进行便利,中间件代理传递发生的事件,之后另一个应用程序的事件处理程序负责。

另一种通常实现调解模式的集成方法是消息总线方法,其中消息总线充当不同应用程序之间传递消息以促进它们之间通信的调解者。

联邦模式

联邦模式在功能上与调解模式完全相反。虽然调解模式侧重于应用程序之间的直接通信,而不提供任何障碍,联邦模式通常限制应用程序之间的自由通信。

在联合模式中,中间件公开一组标准的端点,通过这些端点,其他应用程序可以与其通信。一旦应用程序向联合中间件的 API 发出请求,联合中间件就负责翻译并将该请求传递给后端应用程序。一旦后端应用程序处理了请求,联合中间件...

访问模式

访问模式定义了企业基础设施内应用程序对数据的访问方式。

通常有两种访问模式:异步和同步访问模式。让我们看看这些模式的目标是什么。

异步访问模式

异步访问模式遵循“发出请求并忘记”的数据访问方式。在这种情况下,一旦中间件转发了请求,它就不会等待该请求的响应返回,而是继续处理它正在接收的新请求。

异步访问模式通常在调解方法中使用,其中路由器中间件一旦被通知发生某个事件,就会传播该事件并忘记该事件的回复而不等待。

消息总线模型也是如此;一旦消息总线传递了消息,它就不再关心消息生成的响应,因此使得该过程...

同步访问模式

同步访问模式与异步模式相反。同步访问模式不是转发请求然后忘记响应,而是发出请求,然后等待其他应用程序生成响应。

这种模式通常在联合集成的情况下使用,其中中间件充当中间人,处理对其管理的后端应用程序的访问。

例如,在基于网关的模式中,中间件通常接受请求,将请求转发给后端应用程序,然后等待响应到达,然后再处理下一个请求。

这些只是控制 EAI 过程的一些基本模式。目前仍在使用近 65 种 EAI 模式,以促进 EAI 的概念。

现在,让我们来看一下一些常见的问题,这些问题阻碍了不同企业应用之间成功集成。

EAI 中的问题

企业应用程序的成功集成通常受到许多因素的影响;让我们来看一下:

  • 专有数据格式:一些应用程序使用自己的专有数据格式,并且几乎没有关于如何与它们集成的文档,阻止了应用程序之间的集成,或导致应用程序集成质量不佳,从而导致一系列问题。

  • 数据一致性问题:维护数据一致性可能成为 EAI 的问题。当每个应用程序都维护自己的数据源时,跨不同数据源的数据一致性可能会成为问题,特别是如果中间件遇到重负载,导致...

总结

在本章中,我们看到了为什么 EAI 对企业业务流程的正常运行是必要的。一旦我们了解了 EAI 的必要性,我们就开始了解 EAI 的方法,我们探讨了应用程序点对点集成以及为什么点对点集成的过程存在问题。然后我们探讨了通过使用经纪人中间件模型来实现 EAI 的传统方式,然后继续讨论模型是如何随着 SOA 的出现而转变的,以及 ESB 如何取代基于经纪人的模型。

然后我们继续理解 EAI 中的不同模式,并了解连接不同应用程序的调解和联合集成模式,然后理解不同访问模式(如异步和同步访问)在信息从一个应用程序传输到另一个应用程序时是如何工作的。我们通过探讨一些困扰企业成功集成应用程序的问题来结束本章。

随着我们进入下一章,我们将了解微服务的引入如何改变了 EAI 的格局,并取代了 ESB 的使用,现在被分布式消息代理和 API 网关所取代。

问题

  1. 在点对点集成过程中通常会面临哪些问题?

  2. ESB 如何连接不同类型的应用程序?

  3. 存在哪些不同类型的 EAI 模式,促进了应用程序集成的方法?

第十六章:微服务和企业应用集成

微服务架构的引入彻底改变了企业应用程序的视角。这些应用程序不再是大型的单体或提供特定领域问题解决功能的大型服务。相反,现在我们有小型的微服务,每个提供特定的功能集。

这些小型微服务通过网络相互通信,以提供与组织业务需求相对应的特定输出。

随着我们在本章中的深入,我们将看到传统的企业应用集成(EAI)的传统方法正在被微服务的使用所淘汰,微服务引入了新的集成模式,由小型、无状态的消息代理组成,而不是一个庞大而复杂的企业服务总线。

客户端之间的通信现在已经被 API 网关取代,API 网关提供了客户端和后端微服务之间的联合。

作为本章的读者,您将学习以下内容:

  • 微服务和 EAI 景观的变化

  • 企业服务总线的转变

  • 以微服务架构思考 EAI

技术要求

这一章建立在第十一章 采用微服务方法 和 第十五章 企业应用集成及其模式 的内容之上。因此,阅读本章的内容并不需要特殊的硬件或软件,但对分布式消息代理和异步消息系统的一些了解将有助于在阅读本章时提供更广泛的背景。

微服务和不断变化的 EAI 景观

最近,组织开始转向一种新的应用程序开发方法。这种方法侧重于开发由多个提供单一功能并且提供良好的小型服务组成的应用程序。这些小型服务被称为微服务。

这些微服务模拟了企业领域的一个子集的功能。例如,基础设施中可能有一个负责处理用户凭据和身份验证的服务,另一个可能负责处理电子邮件的功能,还有另一个处理员工工资的服务。

所有这些服务通过消息传递的机制或通过使用服务暴露的 API 从一个服务向另一个服务发出 API 调用来进行网络通信,以实现特定的用例。

与传统应用程序相比,传统应用程序通常庞大,并且需要中间件来处理数据从一个应用程序支持的格式到另一个应用程序支持的格式的转换,然后安全地传输这些数据,微服务要求通过 API 直接与其他服务通信,或者通过小型消息代理将数据以消息的形式从一个微服务传输到另一个微服务。

这改变了企业应用集成的方式,因为现在基础设施中没有复杂的中间件解决方案,提供粘合层来连接基础设施内的不同应用程序。

因此,让我们看看为什么传统方法在微服务架构中不起作用,并尝试理解出现的新替代方案,以促进企业应用程序的集成。

微服务中传统 EAI 的挑战

在通过现代的小型微服务开发实践开发、将它们托管在企业基础设施上,然后将它们集成在一起进行通信的应用程序中,我们不能再使用我们在运行和维护大型单片应用程序或服务时熟悉的传统方法。让我们先花点时间了解为什么点对点集成在微服务的情况下可能行不通。

微服务的点对点集成

在微服务的点对点集成方法中,我们使微服务直接通过它们暴露的 API 相互交互。为了实现这一点,每个微服务都需要了解其他服务暴露的端点。这是完全可以的,但是如果微服务需要执行依赖于与其他五个微服务交互的操作会发生什么呢?

在这一点上,我们必须将五个不同微服务的端点嵌入到我们的微服务中。作为一项一次性任务,这是一个完全可以接受的解决方案。但是,由于微服务的性质,它们会随着时间不断发展。这现在导致我们不断更新我们的微服务,以反映更新的 API。

这只是其中一个挑战。通常,基于微服务架构的应用程序会随着时间的推移而增长,拥有超过 100 个在基础设施中运行的服务,这使得在不同的微服务之间实现点对点集成变得非常困难。

那么,现在我们知道了微服务不能通过点对点集成来集成,我们能否使用老式的企业服务总线?让我们来看看。

使用 ESB 集成微服务

企业服务总线通常提供一个中间总线,通过该总线,两个应用程序可以通过消息传递机制进行通信。这个 ESB 还有一个标准格式,可以在发送之前对消息进行编码。

现在,我们可以将我们的微服务连接到 ESB,然后这些服务可以通过传递消息进行通信。这种方法是完全可以的,也是有效的。但是,当微服务的数量开始在基础设施中增长时,真正的问题开始出现。一旦发生这种情况,ESB 就会因为传输的大量消息而开始承受沉重的负载。

ESB 无法扩展的另一个原因...

利用 API 网关集成微服务

在微服务架构中使用 API 网关提供了一种非常有趣的方法来解决微服务集成问题,同时也遵循了应用程序集成的模式之一,即通过使用联合网关。因此,让我们看看 API 网关如何帮助我们进行微服务集成的过程。

微服务架构中的 API 网关充当中心点,通过它,微服务可以与基础设施中的其他微服务进行交互。这个 API 网关提供以下特点:

  • API 的受限暴露: API 网关提供了仅从后端微服务中暴露一组受限 API 的功能,因此限制了暴露的功能。除此之外,API 网关还可以在基础设施中引入新的 API 端点,其中每个 API 端点可以映射到后端微服务的多个 API 端点。

  • 联合访问: API 网关实现了微服务的联合访问。这是因为,如果任何两个服务想要相互交互,需要向 API 网关发出调用,API 网关将确实向其他微服务发出请求,并从微服务中提供结果。

  • 请求的转换:API 网关还负责在微服务之间转换请求,如果它们都使用不同的数据表示机制。为了进行这种转换,API 网关通常实现了一个通用的数据格式,每个服务都可以使用它来处理与 API 网关的通信,这个概念通常由 ESB 实现。

通过使用 API 网关进行微服务集成,不同微服务之间的通信过程如下:

  • 想象一下有两个微服务,AB

  • 微服务A希望通知微服务B某个事件已经发生,这是某个调用或其他外部事件的结果

  • 微服务A调用 API 网关暴露的微服务B的端点

  • API 网关接收请求,对请求进行任何类型的转换,并将调用转发给微服务B

  • API 网关现在等待来自微服务B的响应返回

  • 一旦响应返回,API 网关将响应转换为微服务A支持的格式,并将响应返回,完成循环

其他服务通常也会遵循这种过程。

使用 API 网关集成微服务相比传统方法提供了许多优势,如以下示例所示:

  • 提高安全性:由于 API 网关限制了后端 API 的暴露,API 网关在不同微服务之间提供更好的安全性。通过在不同微服务和 API 网关之间的通信中实现简单的端到端加密,也可以增加安全性。

  • 更好的可扩展性:API 网关通过使用负载均衡器允许动态扩展,提供比传统基于中间件的方法更好的可扩展性。多个 API 网关进程可以在负载均衡器后运行,最终分发到它们的请求。

  • 更容易维护:与使用 API 网关集成的应用通常更容易维护,因为它们需要单独管理的 API 端点数量减少了。

这些都是一些很大的好处,看起来是集成基于微服务的应用的一个很好的方法。那么,我们不再需要 ESB 了吗?ESB 已经消失了吗?

答案是否定的。相反,随着微服务的出现,它已经发生了变化。让我们看看这种转变是什么样子的。

ESB 的转变

随着微服务革命的出现,企业服务总线也发生了变化,现在已经被一些类似的解决方案所取代,但具有更好的可扩展性和去除单点故障的优势。

应用集成中的 ESB 曾经扮演着中央总线的角色,作为希望相互通信的应用之间的中介。ESB 通过引入通用数据格式并提供适配器来促进这种通信,应用可以通过这些适配器与 ESB 进行通信。

但 ESB 仍然存在两个主要缺点:

  • 可扩展性: ESB 是一种沉重的中间件,需要专门化才能使用……

在微服务中重新思考 EAI

有了微服务的参与,他们拥有自己的工具和不同的要求,我们现在必须重新思考企业基础设施中的 EAI 方法。因此,让我们看看在考虑基于微服务的基础设施中的应用集成时需要注意的一些要点:

  • 扩展规划:微服务基础架构内的应用不断发展,它们的集成需要以相同的方式进行规划。在考虑集成策略时,我们需要确保它能够支持我们应用的未来规模和应用可能需要的通信类型。

  • 定义 API:微服务暴露的 API 在不同应用的集成中起着重要作用。在开始开发微服务之前,应该充分规划和记录其 API,以便与其他服务更顺畅地集成。

  • 保持数据格式标准:不同微服务管理数据的数据格式应该标准化,只有少量格式,以便实现简单的集成和减少基础架构的复杂性。

总结

在本章的过程中,我们看了一下微服务作为企业应用开发方法的引入是如何彻底改变企业内部应用集成方式的。

我们看了一下当传统的企业应用集成方法应用到微服务架构时会失败,然后我们看了一下在引入 API 网关和分布式消息路由器后,EAI 的转变是如何发生的。

在本章结束时,我们看了一下随着我们转向基于微服务的方法,企业应用集成的规划发生了什么变化。

从这里,我们现在对不同方面有了一个概念...

问题

  1. 微服务应用中点对点集成的瓶颈是什么?

  2. 企业服务总线在微服务出现后发生了什么变化?

  3. 微服务架构内的消息代理如何提供高可用性?

第十七章:评估

第一章

答案 1

在 Python 3 标准中,不允许将不可变类型的byte类型和str类型进行连接;任何尝试连接这两种类型的操作都会引发TypeError错误。

答案 2

Python 3 中引入的类型提示支持只旨在提供对方法和参数进行更清晰的文档记录,并不强制执行任何操作标准。

答案 3

除了功能性和非功能性需求外,软件需求规格说明文档还指定了其他要求,如 UI、性能、业务和市场需求。

答案 4

各种类型的需求被分类如下:

  • 必须要求:这些是必须存在于系统中的要求。如果缺少任何一个,其缺失将影响系统中的关键功能。

  • 应该要求:这些要求如果存在,将增强应用程序的功能。

  • 可选要求:这些要求在性质上是非关键的。如果缺少它们,不会对应用程序的功能产生任何影响。

  • 需求愿望清单:这些是利益相关者可能希望在应用程序的未来更新中看到的要求。

答案 5

一旦生成了软件需求规格说明文档,流程的下一步包括软件的设计阶段。在设计阶段,决定软件应用程序的结构,并就可能使用的技术栈做出决策。

第二章

答案 1

Python 中的责任链模式允许我们构建一个考虑松散耦合的应用程序。这是通过将接收到的请求通过软件内的一系列对象链传递实现的。

以下代码片段显示了在 Python 中实现责任链模式的方法:

import abcclass Handler(metaclass=abc.ABCMeta):    """Handler provides an interface to build handlers."""    def __init__(self, handler=None):        """Initialize the handler.        Keyword arguments:        handler -- The next handler object to be called        """        self._next_handler = handler    @abc.abstractmethod    def handler(self, data):        """The handler abstract method.        Keyword arguments:        data -- The data to be processed by the handler        """        passclass StringHandler(Handler): ...

答案 2

__new__方法是在需要创建对象的新实例时调用的第一个方法,而__init__方法仅在需要初始化对象的新创建实例时运行。在类实例创建的正常流程中,__new__方法将始终首先执行,并且只有在开发人员希望控制新实例的创建时才应该被重写。然后应调用__init__方法,该方法将在实例创建后调用,并且需要进行初始化。

答案 3

使用 ABC 元类很容易定义一个新的抽象类。以下代码片段展示了实现这种行为的示例:

import abcclass Handler(metaclass=abc.ABCMeta):    """Handler provides an interface to build handlers."""    def __init__(self, handler=None):        """Initialize the handler.        Keyword arguments:        handler -- The next handler object to be called        """        self._next_handler = handler    @abc.abstractmethod    def handler(self, data):        """The handler abstract method.        Keyword arguments:        data -- The data to be processed by the handler        """        pass

第三章

答案 1

DBMS 中模式的规范化提供了许多好处,例如:

  • 改进关系的整体组织

  • 减少冗余数据的存储

  • 改进数据库中数据的一致性

  • 更好地索引数据,提高对数据的访问

答案 2

SQLAlchemy 中的延迟加载为开发人员提供了使用selectjoined模式进行延迟加载的选项。当开发人员选择select模式加载数据时,数据集的加载通过发出 SQL SELECT语句进行,这样可以根据需求加载数据。

在使用joined时,相关数据集通过发出 SQL JOIN语句一次性加载。这种技术也被称为连接式急加载。

答案 3

在进行数据更新时,我们可以通过多种方式来保持数据的完整性。其中一种最简单的方法是通过使用事务来实现,这允许我们在原子事务中进行多个更新,其中要么应用所有更新,要么不应用任何更新。

在事务中的更新失败时,之前应用的事务中的更新也会被回滚,从而保持数据库中关系的一致状态。

答案 4

可以在数据库中实现的不同级别的缓存如下:

  • 数据库级缓存: 当我们在数据库级缓存时,通常利用数据库的内置功能,通过维护查询缓存来缓存频繁使用的数据集。

  • 块级缓存: 块级缓存发生在应用程序级别,我们将 ORM 层获取的数据缓存到基于内存的数据存储中,以避免每次请求特定结果时运行数据库查询。

  • 用户级缓存: 在用户级缓存时,非安全关键数据通过会话 cookie 或本地存储在客户端缓存。

第四章

答案 1

Python 有两种不同的方式允许我们构建能够并发处理请求的应用程序。具体如下:

  • 多进程: Python 的多进程模块允许开发人员启动多个进程以并行处理工作负载

  • 多线程: Python 多线程模块允许开发人员执行多个线程,可用于处理并发工作负载

答案 2

当获得锁的线程突然终止时,根据获得锁的方式,可能会出现多种情况。

如果锁是通过 Python 中的with语句获得的,那么一旦线程终止,锁就会被释放。

如果锁是在try-except-final方法中获取的,那么当异常传播到最终语句时,锁将被释放。

如果锁是在没有任何安全程序的情况下获得的,线程的突然终止将导致死锁,因为锁没有被释放。

答案 3

通常,当主程序接收到终止信号时,信号也会传播到其线程;否则,线程可以被标记为守护线程,以便其执行随主程序终止而终止。

另一种实现方式是通过标志,线程可以定期检查。如果标志被设置,线程就会开始终止。

答案 4

不同进程之间的状态共享可以通过使用管道来实现,这可以帮助进程彼此通信。

答案 5

在 Python 中,我们有多种方法来创建进程池以分发任务。我们可以手动创建这些池——就像本章进程同步部分中的示例所示——或者我们可以利用concurrent.futures库中提供的ProcessPoolExecutor

第五章

答案 1

为了通过多个应用程序实例处理请求,我们使用水平扩展的概念,其中我们在负载均衡器后启动多个相同应用程序的实例。负载均衡器负责在这些应用程序实例池中分发传入的请求。

答案 2

可以通过 Python 中的concurrent.futures库中的ProcessPoolExecutor来实现进程池。如何使用ProcessPoolExecutor在本章的使用线程池处理传入连接部分中有示例。

答案 3

完全可以编写一个同时使用多进程和多线程的程序。以下代码片段显示了这种实现方式:

import threading
import multiprocessing

def say_hello():
    print("Hello")

def start_threads():
    thread_pool = []
    for _ in range(5):
        thread = threading.Thread(target=say_hello)
        thread_pool.append(thread)
    for thread in thread_pool:
        thread.start()
    for thread in thread_pool:
        thread.join()

def start_process():
    process_pool = []
    for _ in range(3):
        process = multiprocessing.Process(target=start_threads)
        process_pool.append(process)
    for process in process_pool:
        process.start()
    for process in process_pool:
        process.join()

if __name__ == '__main__':
    start_process()

上述实现方式是有效的,并且可以轻松实现而不会出现任何问题,尽管您可能会发现它的使用案例有限,并且其使用将受到 GIL 实现的限制。

答案 4

本章的使用 AsyncIO 实现简单的套接字服务器部分展示了实现套接字服务器的简单示例。另一种通过使用aiohttp框架实现完全功能的 Web 服务器的方法是使用基于 AIO 的 HTTP 服务器。

第六章

答案 1

除了我们在本章中看到的通用View类之外,Flask 还提供了另一个预构建的可插拔视图类,称为MethodView

答案 2

是的,我们可以删除用户表中对角色表的外键约束,并保持关系。但是每当我们需要存储数据时,我们将需要在用户表中手动插入角色对象所需的对象。

答案 3

有许多替代方案可用于为基于 Flask 的 Python 应用程序提供服务,例如以下内容:

  • uWSGI

  • 扭曲的网络

  • mod_wsgi

  • Gevent

答案 4

增加 Gunicorn 工作进程的数量非常简单。我们只需要在命令中添加-w <worker count>参数来设置 Gunicorn 工作进程的数量,如下例所示:

gunicorn -w 8 --bind 0.0.0.0:8000 wsgi:app

第七章

答案 1

使用 CDN 确实可以提高网页的加载性能。这是因为浏览器缓存来自给定 URL 的内容的方式。有时,当我们使用现有的 CDN 来提供一些内容时,我们可以获得以下好处:

  • 对于一些常见的前端库,有可能用户的浏览器已经缓存了这些库,因为他们访问了其他网站,其中包括来自 CDN 的内容。这有助于我们避免重新下载这些库,减少带宽使用,并提高页面的加载速度。

  • CDN 还可以根据用户地理位置将请求路由到服务器,以便以最小的延迟下载内容,从而提高页面的加载速度。

答案 2

为了让浏览器使用现有的连接,我们可以利用一个叫做KeepAlive的概念。当请求中设置了KeepAlive头时,服务器会保持用于发出请求的连接打开一段固定的时间,希望可以在另一个请求中继续使用相同的连接,避免为每个请求进行初始连接设置的成本。

答案 3

JavaScript API 提供了一个非常方便的方法,称为removeKey(key),可以用来从浏览器的本地/会话存储中删除特定的键。

第八章

答案 1

单元测试和功能测试之间的主要区别是测试的范围,如下所述:

  • 单元测试:单元测试通常侧重于测试软件中的单个组件,这些组件可以被分解为单个函数或类的方法。

  • 功能测试:功能测试也称为集成测试,通常测试系统的特定功能,可能涉及多个组件之间的交互,以及它们与外部环境(如数据库系统)的交互。

答案 2

测试套件是需要在特定程序上运行的一系列测试用例。使用 Python 的unittest库编写测试套件非常容易实现。例如,如果您编写了一些测试用例,比如TestTextInputTestTextUppercaseTestTextEncode,我们可以使用以下代码片段将它们组合成一个测试套件:

import texttest # Module containing our text related test casesimport unittest# Create a test loaderloader = unittest.TestLoader()# Create a test suitesuite = unittest.TestSuite()# Add tests to a suitesuite.addTests(loader.loadTestsFromModule(texttests)

答案 3

Pytest 中 fixture 的目的是提供一个固定和稳定的环境,以便测试用例可以执行。这些 fixture 负责通过设置所需的变量或接口来初始化环境,以便测试执行。

使用 fixture 的另一个优点是它的可重用性,允许同一个 fixture 在多个测试中使用而没有任何问题。

答案 4

Pytest 中的 fixture 作用域描述了 fixture 将被调用的频率。fixture 有许多不同的作用域可以应用于它们,如下所示:

  • 函数作用域:Fixture 在每个测试中运行一次

  • 类作用域:Fixture 在每个类中运行一次

  • 模块作用域:Fixture 在每个模块中运行一次

  • 会话作用域:Fixture 在每个测试会话中运行一次

第九章

答案 1

应用程序内部可能导致性能瓶颈的多个因素,包括以下内容:

  • 不足的硬件资源规划,以运行应用程序

  • 在应用程序中实施功能的算法选择不当

  • 数据库关系实施不当,存在大量冗余

  • 未对频繁访问的数据实施适当的缓存

答案 2

Python 中方法的时间分析有助于我们了解方法执行所需的时间。根据要求,我们可以通过几种不同的方式对方法进行时间分析,如下所示:

  • 使用timeit模块:timeit模块提供了一个功能,可以用来找出脚本或方法执行所需的时间。

  • 使用time模块:我们还可以使用time模块来帮助我们测量 Python 中方法的运行时间。我们可以通过创建装饰器来实现这一点,这可以帮助我们对方法的运行时间进行分析。

  • 使用cProfile模块:cProfile模块允许我们对 Python 程序内部的不同步骤进行性能分析。

答案 3

尽管 Python 是一种具有垃圾回收功能且没有直接访问内存指针的语言,但通过非法指针操作可能导致的典型内存泄漏几乎不会发生。但还有另一种方式,即 Python 程序可以继续消耗更多内存而不释放它。当程序忘记在对象不再使用时取消引用这些对象时,可能会导致分配新对象而不进行不再使用对象的垃圾回收。

答案 4

应用程序的 API 响应可以通过测量 API 返回响应所需的平均时间来进行性能分析,这可以通过多种方式进行测量,可能涉及使用 Python 标准库中的timeittime模块。

答案 5

设计模式在应用程序性能中起着重要作用,不正确的设计模式可能会对应用程序性能造成影响。例如,考虑分配一个对象来实现应用程序中的日志记录。如果这个日志记录对象分配需要在每个单独的模块或类中进行,那么我们可能会浪费大量资源来分配一个对象,而这个对象本可以在不同模块之间共享。

第十章

答案 1

目前有许多问题使得应用程序的安全性变得困难。这些问题包括以下内容:

  • 难以应对的复杂攻击

  • 未被修补的 0-day 漏洞的增加

  • 越来越多的国家支持的攻击针对系统的多个漏洞,通常很难追踪

  • 越来越多的设备上线而没有适当的安全措施,使它们容易受到 DDoS 攻击的利用

答案 2

XSS(或跨站脚本)攻击是指攻击者在受信任的网站中注入恶意脚本。当加载带有恶意脚本的页面时,它会导致客户端系统受到攻击者的威胁。

答案 3

DoS(或拒绝服务)攻击是攻击者用来通过向系统发送多余的请求来使服务或资源对其用户不可用的一种方式,这会导致系统排队这些请求并造成服务中断。

可以通过在不同级别实施不同的技术来减轻攻击,例如:

  • 添加防火墙规则以拒绝来自给定不受信任来源的流量

  • 使用云安全提供商的服务,可以分析传入流量并在到达应用程序基础设施之前阻止它,有助于减轻 DoS 攻击

  • 配置基础设施以将流量引导到没有运行应用程序的节点,或者重新路由...

答案 4

有很多可能的错误可能会危及应用程序的安全性,例如:

  • 在应用程序内部使用不安全的第三方库,可能包含安全漏洞

  • 不过滤用户提供的应用程序输入

  • 在应用程序内部以未加密的方式存储安全敏感数据

  • 不实施适当的限制以控制对内部基础设施的访问

第十一章

答案 1

面向服务的架构和微服务架构之间的主要区别在于,在面向服务的架构中,应用程序由不同的服务组成,每个服务提供组织业务领域之一的功能。这些服务通过企业服务总线进行通信,该总线将消息从一个服务路由到另一个服务,同时提供消息交换的通用格式。

在微服务的情况下,应用程序将由许多小型微服务组成,每个微服务负责提供仅限于单一功能的功能,可能无法映射到组织的完整领域,可能只是更大问题领域的子集。这些微服务通过各自微服务公开的 API 或通过无状态消息路由器进行通信,允许消息从一个服务传递到另一个服务。

答案 2

为了确保基于微服务的应用程序具有高的正常运行时间,我们可以使用以下技术:

  • 不使用单一存储来存储所有微服务

  • 在负载均衡器后运行同一微服务的多个实例

  • 使用 API 网关在关键服务失败时提供优雅降级的服务,使客户端仍然在服务失败时收到响应

答案 3

使用服务级别协议(SLA)提供了许多保证,例如:

  • 对服务的 API 稳定性的保证

  • 对服务正常运行时间的保证

  • 对服务的预期响应时间的保证

  • 对服务实施的请求速率限制的保证

答案 4

API 网关可以通过服务注册表提供的 SDK 或通过服务注册表公开的 API 与服务注册表直接通信。这允许 API 网关自动从服务注册表中获取给定服务的正确位置。

答案 5

微服务内部的异步通信可以通过使用无状态消息代理来实现。要实现异步通信,一些微服务充当生产者,并将消息发送到消息代理队列。然后,其他微服务可能会消费该消息,处理它,并将响应发送回发送消息的微服务。然后,由请求微服务设置的回调函数来处理响应。这就是微服务之间的异步通信建立的方式。

第十二章

答案 1

微服务的集成测试基本上与单片应用程序的测试方式相同,只有以下几点不同:

  • 如果微服务需要与另一个外部微服务通信,则集成测试可能需要设置外部服务,以便正确执行测试用例

  • 组成单个服务的各个组件应该为基础设施中的所有微服务设置好,例如,伴随特定微服务的数据库需要用于测试目的

答案 2

单体应用程序的跟踪与基于微服务的应用程序的跟踪不同,单体应用程序的跟踪涉及理解请求从应用程序内的一个组件到另一个组件的流程。相反,跟踪基于微服务的应用程序涉及理解请求不仅在特定微服务内的流动,还涉及请求从一个微服务到另一个微服务的流动。

答案 3

有多个可用于微服务架构内跟踪的工具,如下列表所示:

  • Jaeger

  • Zipkin

  • Appdash

答案 4

为了跟踪微服务内的各个组件,我们可以利用 Jaeger 提供的功能之一,称为 spans。如何使用 spans 的示例可以在github.com/jaegertracing/jaeger-client-python中看到。

第十三章

答案 1

迁移到无服务器架构提供了许多优势,例如:

  • 通过集成第三方服务减少开发工作量

  • 操作复杂性减少,因为现在组织不需要关心基础设施

  • 改进的安全性,因为各个功能在它们自己独立的容器中执行,这有助于我们保持不同功能之间不会相互干扰

  • 应用程序的可扩展性得到改善

答案 2

使用后端即服务BaaS)有助于通过集成 API 提供的常见功能来创建应用程序。这些服务由第三方提供商托管,从而减少了应用程序开发人员在从头开始重建它们的应用程序中所需的工作量。

答案 3

无服务器架构中的 API 网关将 API 端点映射到后端的一个功能。当特定事件发生时,客户端可以调用这些 API 端点,从而调用后端功能。

答案 4

有一些原因导致应用无法成功迁移到无服务器架构。这些原因如下:

  • 使用无服务器基础设施提供商不支持的技术堆栈

  • 需要存储请求处理状态以生成正确结果的应用程序

  • 代码基础紧密耦合,很难定义个别方法

  • 应用程序的某些组件执行时间非常长

第十四章

答案 1

使用蓝绿部署为我们提供了以下一系列好处:

  • 能够立即将应用程序从一个版本切换到另一个版本

  • 在新版本遇到一些关键功能错误时,能够轻松将应用程序从新版本回滚到旧版本

  • 与应用程序升级相关的停机时间减少

答案 2

使用 Canary 部署可以帮助测试应用程序的以下方式:

  • 应用程序经过一小部分真实请求的测试,这可能有助于暴露应用程序中的任何未识别的错误

  • Canary 部署使我们能够同时运行应用程序的新版本和旧版本,以便比较 API 提供的响应

答案 3

使用虚拟机运行基于微服务的应用程序可能会导致微服务实例的开销增加,因为虚拟机的要求更高。此外,使用虚拟机会限制可以共存在同一基础设施上的服务数量,因为虚拟机比容器更重,容器利用操作系统功能来保持程序隔离。

答案 4

混合云模型中的部署可以以与公共或私有云中处理的方式进行处理。区别在于应用程序需要进行扩展时。在这种情况下,使用混合云方法时,组织可以根据扩展的需求从公共云中汇集资源,然后在公共云中运行其应用程序的某些部分,而在私有云中运行其他部分。

第十五章

答案 1

企业应用程序的点对点集成需要为需要集成的每对应用程序构建连接器。这将创建一个复杂的基础设施,如果引入新应用程序,可能难以管理和扩展。

答案 2

企业服务总线负责通过消息传递机制帮助基础设施内的不同服务相互连接。ESB 为应用程序提供连接器,应用程序可以通过这些连接器连接到 ESB 并向 ESB 发送消息。

然后,ESB 负责将这些消息路由到它们预期的正确服务,从而促进基础设施内两个服务之间的通信。

答案 3

EAI 的不同类型的模式如下:

  • 调解模式

  • 联邦模式

第十六章

答案 1

由于基础设施中可能使用特定微服务的不同技术堆栈,因此很难实现不同微服务的点对点集成。这可能导致为每对微服务构建单独的连接器,以将一个微服务的数据格式转换为另一个微服务的数据格式。

另一个瓶颈是由于这些服务的可伸缩性,现在连接器必须连接部署的每个单个微服务实例。

答案 2

随着微服务架构的出现,企业服务总线已被无状态消息路由器所取代,这些路由器可以单独扩展,并为可能在基础设施内运行的大量微服务实现消息路由。

答案 3

微服务架构中的消息代理通过在多个消息代理实例之间复制消息队列来提供高可用性。这允许路由器取代失败的路由器,并保持基础设施内的通信完整。

posted @ 2024-05-04 21:29  绝不原创的飞龙  阅读(133)  评论(0编辑  收藏  举报