SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

性能分析和调优是验证客户性能合规性的强大工具。

通过应用科学方法来测试调优实验,性能分析可用于检查程序中的瓶颈。 本文以 Go Web 服务器为例,定义了性能分析和调优的通用方法。

Go 在这里特别好,因为它有分析工具 pprof 在标准库中。

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

战略

让我们为结构分析创建一个摘要列表。 我们会尝试使用一些数据来做出决策,而不是根据直觉或猜测进行更改。 为此,我们将这样做:

  • 我们确定优化边界(要求​​);
  • 我们计算系统的交易负载;
  • 我们执行测试(创建数据);
  • 我们观察;
  • 我们分析 - 是否满足所有要求?
  • 我们科学地设定,做出假设;
  • 我们进行了一项实验来检验这个假设。

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

简单的 HTTP 服务器架构

在本文中,我们将使用 Golang 中的小型 HTTP 服务器。 本文所有代码均可找到 这里.

正在分析的应用程序是一个 HTTP 服务器,它为每个请求轮询 Postgresql。 此外,还有 Prometheus、node_exporter 和 Grafana 用于收集和显示应用程序和系统指标。

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

为了简化,我们认为为了水平扩展(并简化计算),每个服务和数据库都部署在一起:

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

定义目标

在这一步,我们决定目标。 我们想分析什么? 我们怎么知道什么时候结束? 在本文中,我们将假设我们有客户端,并且我们的服务每秒将处理 10 个请求。

В 谷歌 SRE 书籍 详细讨论了选择和建模的方法。 让我们做同样的事情并构建模型:

  • 延迟:99%的请求应在60ms以内完成;
  • 成本:服务应该消耗我们认为合理可能的最低金额。 为此,我们最大化吞吐量;
  • 容量规划:需要了解并记录需要运行多少个应用程序实例,包括总体扩展功能,以及需要多少个实例来满足初始负载和配置要求 冗余n+1.

除了分析之外,延迟可能还需要优化,但吞吐量显然需要分析。 使用 SRE SLO 流程时,延迟请求来自以产品负责人为代表的客户或企业。 而我们的服务从一开始就履行这个义务,无需任何设置!

设置测试环境

在测试环境的帮助下,我们将能够在系统上放置测量的负载。 为了进行分析,将生成有关 Web 服务性能的数据。

交易负载

该环境使用 过单调生活 创建自定义 HTTP 请求速率直至停止:

$ make load-test LOAD_TEST_RATE=50
echo "POST http://localhost:8080" | vegeta attack -body tests/fixtures/age_no_match.json -rate=50 -duration=0 | tee results.bin | vegeta report

看着

事务负载将在运行时应用。 除了应用程序(请求数量、响应延迟)和操作系统(内存、CPU、IOPS)指标之外,还将运行应用程序分析来了解问题所在以及 CPU 时间的消耗情况。

分析

分析是一种测量方法,可让您了解应用程序运行时 CPU 时间的去向。 它允许您准确确定处理器时间花费在何处以及花费了多少:

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

这些数据可在分析过程中使用,以深入了解浪费的 CPU 时间和正在执行的不必要的工作。 Go (pprof) 可以使用一组标准工具生成配置文件并将其可视化为火焰图。 我将在本文后面讨论它们的使用和设置指南。

执行、观察、分析。

我们来做一个实验。 我们将进行表演、观察和分析,直到我们对表演感到满意为止。 让我们选择一个任意低的负载值来应用它以获得第一次观察的结果。 在后续的每个步骤中,我们都会以一定的比例因子增加负载,并通过一些变化来选择。 每次负载测试运行都会调整请求数量: make load-test LOAD_TEST_RATE=X.

每秒 50 个请求

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

注意上面的两张图。 左上角显示我们的应用程序每秒处理 50 个请求(它认为),右上角显示每个请求的持续时间。 这两个参数都可以帮助我们查看和分析我们是否在性能范围内。 图表上的红线 HTTP 请求延迟 显示 SLO 为 60 毫秒。 该线显示我们远低于最大响应时间。

我们再看看成本方面:

每秒 10000 个请求/每台服务器 50 个请求 = 200 个服务器 + 1

我们仍然可以改进这个数字。

每秒 500 个请求

当负载达到每秒 500 个请求时,更有趣的事情开始发生:

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

同样,在左上图中,您可以看到应用程序正在记录正常负载。 如果不是这种情况,则运行应用程序的服务器存在问题。 响应延迟图位于右上角,显示每秒 500 个请求导致响应延迟为 25-40ms。 第 99 个百分位仍然非常适合上面选择的 60 毫秒 SLO。

在成本方面:

每秒 10000 个请求/每台服务器 500 个请求 = 20 个服务器 + 1

一切还有待改进。

每秒 1000 个请求

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

伟大的发射! 该应用程序显示它每秒处理 1000 个请求,但 SLO 违反了延迟限制。 这可以在右上图中的第 p99 行中看到。 尽管 p100 线要高得多,但实际延迟高于 60ms 的最大值。 让我们深入分析一下该应用程序的实际用途。

分析

对于分析,我们将负载设置为每秒 1000 个请求,然后使用 pprof 捕获数据以找出应用程序在何处花费了 CPU 时间。 这可以通过激活 HTTP 端点来完成 pprof,然后在负载下,使用curl保存结果:

$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.prof

结果可以这样显示:

$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

该图显示了应用程序在何处花费了 CPU 时间以及花费了多少时间。 从描述来看 布伦丹·格雷格(Brendan Gregg):

X 轴是堆栈配置文件总体,按字母顺序排序(这不是时间),Y 轴显示堆栈的深度,从 [top] 的零开始计数。 每个矩形都是一个堆栈帧。 框架越宽,它出现在堆栈中的频率就越高。 上面的是在CPU上运行的,下面的是子元素。 颜色通常没有任何意义,只是随机选择以区分框架。

分析-假设

对于调优,我们将重点尝试找到浪费的 CPU 时间。 我们将寻找无用支出的最大来源并将其消除。 好吧,鉴于分析非常准确地揭示了应用程序在何处花费了处理器时间,您可能需要执行多次,并且您还需要更改应用程序源代码,重新运行测试并查看性能是否接近目标。

按照 Brendan Gregg 的建议,我们将从上到下阅读图表。 每行显示一个堆栈帧(函数调用)。 第一行是程序的入口点,是所有其他调用的父级(换句话说,所有其他调用都将其放在堆栈中)。 下一行已经不同了:

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

如果将光标悬停在图表上的函数名称上,将显示调试期间该函数在堆栈上的总时间。 HTTPServe 函数有 65% 的时间存在,其他运行时函数 runtime.mcall, mstart и gc,占据了剩下的时间。 有趣的事实:总时间的 5% 花费在 DNS 查询上:

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

该程序查找的地址属于 Postgresql。 点击 FindByAge:

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

有趣的是,该程序表明,原则上,增加延迟的主要来源有三个:打开和关闭连接、请求数据以及连接到数据库。 该图显示,DNS 请求、打开和关闭连接约占总执行时间的 13%。

假设: 使用池化重用连接可以减少单个 HTTP 请求的时间,从而实现更高的吞吐量和更低的延迟.

设置应用程序 - 实验

我们更新源代码,尝试删除每个请求与 Postgresql 的连接。 第一个选项是使用 连接池 在应用程序级别。 在这个实验中我们 让我们设置一下 使用 go 的 sql 驱动程序进行连接池:

db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)

if err != nil {
   return nil, err
}

执行、观察、分析

以每秒 1000 个请求重新启动测试后,很明显 p99 的延迟水平已恢复正常,SLO 为 60 毫秒!

费用是多少?

每秒 10000 个请求/每台服务器 1000 个请求 = 10 个服务器 + 1

让我们做得更好!

每秒 2000 个请求

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

将负载加倍也显示出同样的情况,左上图显示应用程序每秒设法处理 2000 个请求,p100 低于 60ms,p99 满足 SLO。

在成本方面:

每秒 10000 个请求/每台服务器 2000 个请求 = 5 个服务器 + 1

每秒 3000 个请求

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

这里应用程序可以处理 3000 个请求,p99 延迟小于 60 毫秒。 不违反SLO,费用接受如下:

每秒 10000 个请求/每台服务器每 3000 个请求 = 4 个服务器 + 1 (作者已汇总, 约译者)

让我们尝试另一轮分析。

分析-假设

我们以每秒 3000 个请求的速度收集并显示应用程序的调试结果:

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

还有 6% 的时间花在建立连接上。 设置池可以提高性能,但您仍然可以看到应用程序继续创建与数据库的新连接。

假设: 尽管存在池,连接仍然会被丢弃并清理,因此应用程序需要重置它们。 将待处理连接的数量设置为池大小应该有助于通过最小化应用程序创建连接所花费的时间来减少延迟.

设置应用程序 - 实验

正在尝试安装 最大空闲连接数 等于池大小(也有描述 这里):

db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
   return nil, err
}

执行、观察、分析

每秒 3000 个请求

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

p99 小于 60ms,p100 明显更少!

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

检查火焰图显示连接不再明显! 让我们更详细地检查一下 pg(*conn).query ——我们也没有注意到这里建立了连接。

SRE:性能分析。 在 Go 中使用简单 Web 服务器的配置方法

结论

性能分析对于了解客户期望和非功能性需求是否得到满足至关重要。 通过将观察结果与客户期望进行比较进行分析可以帮助确定什么是可以接受的,什么是不能接受的。 Go 提供了内置于标准库中的强大工具,使分析变得简单且易于访问。

来源: habr.com

添加评论