Skip to content

Latest commit

 

History

History
357 lines (236 loc) · 20.6 KB

File metadata and controls

357 lines (236 loc) · 20.6 KB

九、分析应用的性能

在本书的整个过程中,我们已经了解了应用的性能和可伸缩性在企业环境中的重要性;考虑到这一点,我们在书中花了很大一部分时间来理解如何构建一个不仅具有性能而且具有可扩展性的应用。

到目前为止,我们刚刚看到了一些最佳实践,说明了我们可以如何构建一个高性能、可伸缩的应用,但没有说明如何确定应用中的某个特定代码段是否慢,以及是什么原因导致了它。

对于任何企业级应用,提高其性能和可伸缩性都是一个持续的过程,因为应用的用户群不断增长,应用的。。。

技术要求

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

bugzot 示例应用的评测和基准测试相关的代码示例可以在代码库测试模块下的chapter06目录下找到。

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

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

本章还依赖于第三方 Python 库,可以通过在开发系统上运行以下命令轻松安装这些库:

pip install memory_profiler

性能瓶颈的背后

在应用进入开发阶段之前,会对应用应该做什么、它将如何做以及应用需要与什么样的第三方组件进行彻底的讨论。一旦所有这些都完成了,应用就进入了开发阶段,在开发阶段,开发人员负责构建应用,使应用要执行的任务能够以最有效的方式实现。这种效率通常是根据一个应用完成一项提供的任务所需的时间以及在执行该任务时所使用的资源来衡量的。

当应用部署到生产环境中时。。。

查看性能瓶颈的原因

通常,性能瓶颈可能是由许多因素造成的,这些因素可能包括部署应用的环境中缺少物理资源,或者在有更好的算法可用时选择错误的算法来处理特定的工作负载。让我们来看看一些可能导致部署应用性能瓶颈的可能问题:

  • **没有足够的硬件资源:**最初,性能和可扩展性方面的大多数瓶颈都是由于运行应用所需的硬件资源规划不当造成的。这可能是由于不正确的估计或应用的用户群突然出现意外激增造成的。当这种情况发生时,现有的硬件资源会受到压力,系统会变慢。
  • **错误的设计选择:**在第 2 章设计模式–做出选择中,我们了解了设计选择对任何企业级应用的重要性。不断地为本可以通过分配单个共享对象来完成的事情分配新对象将影响应用的性能,这不仅会增加可用资源的压力,还会由于对象的重复分配而导致不必要的延迟。
  • **低效算法:**处理大量数据的地方或执行大量计算以生成结果的系统往往会因为选择低效算法而导致性能下降。仔细研究替代算法或就地算法优化的可用性可能有助于提高应用的性能。
  • **内存泄漏:**在大型应用中,内存泄漏可能会以意外的方式发生。虽然这在诸如 Python 之类的垃圾收集语言中很难实现,但仍然有可能实现。有时,对象虽然不再使用,但由于它们在应用中的映射方式,仍然不会被垃圾收集。在较长的运行时间内,这将导致可用内存减少,并最终导致应用停止。

以下是系统中出现性能瓶颈的几个原因。幸运的是,作为软件开发人员,我们有许多工具可以帮助我们找出瓶颈,以及发现诸如内存泄漏之类的问题,甚至只是分析各个部分的内存使用情况。

有了这些关于为什么会出现某些性能瓶颈的知识,现在是我们学习如何在应用中查找这些性能瓶颈的时候了,然后尝试了解我们可以减少其影响的一些方法。

探索应用的性能问题

性能是任何企业级应用的关键组成部分,您不能让应用经常减慢速度并影响整个组织的业务流程。不幸的是,性能问题也是需要理解和调试的最复杂的问题之一。之所以会出现这种复杂性,是因为没有标准的方法来访问应用中特定代码段的性能,并且因为一旦开发了应用,就需要了解完整的代码流,以便确定可能导致特定性能问题的可能区域。

作为开发人员,我们可以通过以这样的方式构建应用来减少这些困难。。。

编写性能基准

让我们首先讨论一下,作为软件开发人员,我们如何以一种帮助我们在开发周期早期标记性能瓶颈的方式构建应用,以及如何在调试这些瓶颈方面使我们的生活变得轻松。

在应用开发周期中,我们可以做的第一件也是最重要的事情是为应用的各个组件编写基准测试。

基准测试是一种简单的测试,旨在通过多次迭代执行特定代码段并平均这些迭代执行代码所需的时间来评估其性能。您还记得我们在第 8 章编写可测试代码中听说过一个名为 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 实现的,Pytest 允许我们在方法上运行基准测试,并允许我们通过使用单个测试方法验证被测试的方法是否提供了正确的结果。

现在我们知道了如何在应用内部编写基准测试。但是如果我们不得不调试一些速度很慢但基准测试操作没有标记任何问题的东西呢。我们能在这里做什么?幸运的是,Python 提供了许多选项,允许我们测试代码中可能发生的任何类型的性能异常。所以,让我们花些时间来看看它们。

进行组件级性能分析

有了 Python,许多功能都是内置的,其他功能可以通过第三方库轻松实现。所以,让我们看看 Python 在运行组件级别的性能分析方面对我们有什么帮助。

用 timeit 测量慢速操作

Python 提供了一个非常好的模块,称为timeit,我们可以使用它在小代码片段上运行一些简单的时间分析任务,或者了解一个特定的方法调用需要多少时间。

让我们来看看一个简单的脚本,它告诉我们如何使用 AutoT0TAL 来理解特定方法所花费的时间,然后我们将了解更多关于如何使用由 OutT1AER 提供的功能来运行我们要构建的应用的时间剖析。

下面的代码片段显示了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对给定方法的执行进行简单的时间分析。

现在,这很方便,但是当我们需要花费超过两个方法的时间时,我们不能继续编写多个 setup 语句。我们在这里该怎么办?应该有一个简单的方法来实现这一点。

那么,我们创建一个简单的装饰器,可以用来为可能需要时间分析的方法计时。

让我们创建这个简单的 decorator 方法。下面的示例向我们展示了如何编写一个 decorator 方法,稍后我们可以使用该方法对方法进行时间比较:

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 = endstart
    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

这是我们创造的一个装饰师。在 decorator 内部,我们将要分析的函数以及传递给它的任何参数作为参数。现在,我们初始化函数的开始时间,然后调用函数,然后在函数返回执行时存储调用的结束时间。基于此,我们计算函数执行所需的总时间。

但是我们如何使用这个装饰器来分析我们的方法呢?以下示例显示了其中的一个示例:

@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之前,我们需要先将模块引入我们的开发环境。以下代码行将所需的模块提取到我们的开发环境中:

pip install memory_profiler

一旦内存分析器被引入到开发人员环境中,我们现在就可以开始运行它了。让我们来看一个示例程序,看看如何使用 AutoT0A.来理解我们的应用的内存使用模式。

下面的代码片段向我们展示了如何使用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哪些方法需要对内存使用情况进行分析。

要为方法启用内存分析,我们需要做的就是用 decorator 修饰该方法。例如,在我们的示例应用代码中,我们用 decorator 修饰了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.1MB。这是一个非常方便的库,以防我们怀疑代码中可能存在内存泄漏。

收集实时性能数据

到目前为止,我们已经了解了如何在需要时使用不同的分析工具来分析应用的性能,从而帮助我们找出代码的哪一部分导致了性能瓶颈。但是,我们如何知道一项手术是否花费了比它应该花费的时间更长的时间呢?

其中一个答案可能是用户报告的响应时间慢,但这背后可能有很多因素,可能只涉及用户端的响应速度慢。

我们还可以使用其他一些机制来实时监控应用中的性能问题。那么,让我们看看这些方法中的一种,它可以让我们收集关于个体时间的信息。

日志记录性能度量

在应用内部,可能有几个步骤。通过使用不同的工具,可以分析这些步骤的性能。最基本的工具之一是日志记录。在本文中,我们收集不同方法的执行时间,并将其条目保存在日志文件中。

下面的代码片段展示了一个小示例,说明了如何在第 6 章示例——构建 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。完成此操作后,请求将发送到 processing。一旦请求被处理,它就会转到我们定义的teardown处理程序。

一旦请求到达这个teardown处理程序,我们计算处理请求所花费的总时间,并将其记录在应用日志中。

这种方法允许我们查询或处理日志文件,以了解每个请求花费的时间以及哪些 API 端点花费的时间最长。

避免性能瓶颈

在过去的几节中,我们研究了针对不同类型的性能瓶颈(可能会导致内存泄漏)分析应用的不同方法。但一旦我们意识到这些问题以及它们为什么会发生,我们还有什么其他选择来防止它们再次发生?

幸运的是,我们有两个有用的指导原则,可以帮助防止性能瓶颈或限制这些瓶颈可能产生的影响。那么,让我们来看看这些指南中的一些:

  • **选择正确的设计模式:**设计模式是应用中的重要选择。例如,日志对象不需要在应用的每个子模块中重新初始化。。。

总结

在本章中,我们了解了应用的性能是如何成为软件开发的一个重要方面的,以及什么样的问题通常会导致应用中出现性能瓶颈。接下来,我们研究了针对性能问题分析应用的不同方法。这涉及到,首先为单个组件和单个 API 编写基准测试,然后转向更具体的组件级分析,在这里我们研究了分析组件的不同方法。这些评测技术包括使用 Pythontimeit模块的方法的简单计时评测,然后我们继续使用 Python cProfile 和内存评测的更复杂技术。在我们的旅程中,我们看到的另一个主题是使用日志记录技术来帮助我们在需要时评估缓慢的请求。最后,我们看了一些可以帮助我们防止应用内部性能瓶颈的一般原则。

在下一章中,我们将了解保护我们的应用的重要性。如果不这样做,它不仅会为严重的数据盗窃铺平道路,而且会造成大量的责任,并可能侵蚀用户的信任。

问题

  1. 部署应用时,哪些因素会导致性能瓶颈?
  2. 在方法上运行时间配置文件的不同方式有哪些?
  3. Python 是一种垃圾收集语言,是什么导致了内存泄漏?
  4. 我们如何分析 API 响应,并找出其速度减慢的原因?
  5. 选择错误的设计模式会导致性能瓶颈吗?