性能分析: 一个 Go 编写的简单 HTTP Web 服务器的优化方法

性能分析和调优是一种很强大的技术,用来验证是否满足客户关注的性能要求。性能分析常常被用来分析一个程序将大部分时间花在哪里了,并通过一个科学的方法来测试调优实践的效果。这个帖子使用一个 Go 语言编写的 HTTP 服务作为一个例子来定义一种性能分析和调优的普遍方法。go 特别适合性能分析和调优,因为它在它的标准库中提供了 pprof 剖析工具链。

策略

我们先尝试建立一个简单的框架来构建对程序的分析。我们将要尝试做的是使用数据引导我们得出结论,而不是基于直觉或者预感做出决定。为此我们将要:

  • 确定我们要优化的维度(要求)
  • 创建一个测试代码(harness)将事务负载到这个系统上
  • 执行一个测试——(生成数据)
  • 观察
  • 分析——是否满足要求?
  • 调优——科学方法——形成一个假说
  • 执行实验代码来测试这个假说

简单的 HTTP 服务器架构

关于这个贴,我们将使用一个 Golang 编写的小型 HTTP 服务器。这个贴的所有代码都可以在 这里 找到。

我们将要分析的这个应用是一个每次请求都查询 PostgreSQL 的 HTTP 服务器。此外,通过 Prometheus, node_exporter, 和 Grafana 来收集和可视化应用和系统级的指标:

为简单起见,本文假设为了横向扩展(并简化我们的计算),每个 HTTP 服务和 Postgres 数据库将一起部署:

确定目标(维度)

这一步概述了特定的目标。我们将尝试分析什么?我们如何知道我们的努力已完成?

本文中,我们将假设客户端均衡在我们的服务上,每秒为 10000 请求量。

Google SRE Book 在如何选择和建模上有更深入的探讨。在 SRE 的精髓上,我们将建立我们的模型如下:

  • 延迟—— 99% 的请求应该在 60 ms 内完成。
  • 费用——这个服务应该在我们认为尽可能合理的最小费用内完成。为了达到这个目标,吞吐量应该最大化。
  • 容量规划——对要求启动多少个实例和记录通常情况下的缩放能力的理解。我们需要满足预期初始负载要求并实现 n + 1 redundancy 的实例数量是多少?

延迟可能需要除了分析之外的优化,而吞吐量就只需要分析了。使用 SRE SLO 处理的延迟需求可能来自客户端或者产品拥有者所代表的事务。真正值得一说的是,我们的服务能够在一开始就满足这种承诺而不需要任何调整!

设置测试代码(test harness)

这个测试代码将应用一个固定总数的负载到我们的系统。为了分析 HTTP 服务的性能,数据将需要它来生成。

交互负载(transactional load)

这个测试代码只使用了 Vegeta 以可配置的速率来产生 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)的指标外,这时将通过剖析应用来理解它将时间花费在哪里了。

剖析(profiling)

profiling 是度量中的一类,让我们了解应用将时间花费到哪里了。它能够报告应用将时间花费在哪。Profileing 能够用来确定哪个函数正在被调用,并且应用在每个函数上花费了多少时间:

这个数据可以用来可视化分析程序将时间花在哪些不必要的工作上。Go(pprof)可以用来生成 profiles,并使用 标准工具链 将它们可视化为 火焰图 。在本文后面,我们将通过使用它们来引导调优的结论。

执行、观察、分析

我们开始执行这些实践。我们将执行,观察和分析直到我们的性能要求失效。先选择任意一个低的负载量来生成第一份观察报告和分析。如果每次的性能要求能够 hold 得住,我们就通过一个随机缩放因子(random-ish scaling factor)来增加负载。每次负载测试通过调整速率来执行:

make load-test LOAD_TEST_RATE=X

50 个请求 / 秒

观察上面两张图。左上角的是我们的应用正在处理 50 个请求每秒,而右上角报告每个请求的延迟时间。将它们结合在一起来帮助我们观察和分析我们的性能要求是否满足。HTTP Request Latency 上的红线在 SLO 的 60ms 上。这个表示我们最大响应时间远远低于它。

在成本方面:

10k 请求量 / 秒 / 50 请求量 / 机器 = 200 台机器 + 1

我们就可以很好的支持了。

500 请求量 / 秒

当我们的请求数达到 500 请求量每秒时,事情开始变得有趣:

左上图再一次展示了应用的预期负载。如果它不是这样,它可能会在主机上判定是负载测试命令的问题或者是应用服务器的问题。右上角的延迟图展示了应对 500 请求量 / 秒时,每个 HTTP 请求的延迟时间在 25-40 ms 之间。99% 的请求仍然保持在 60 ms SLO 以下。

在成本方面:

10k 请求量 / 秒 / 500 请求量 / 机器 = 20 台机器 + 1

就可以很好的支持!

1000 请求量 / 秒

这个太大了!应用正在处理 1000 请求量 / 秒,但是延迟时间已经超过 SLO 的延迟量。这个可以看右上角(原文是左上角,可能打错了?)的图中的 P99 线。而尾部的 p100 max 远大于最大限制量的 60ms,P99 线也在 60ms 以上。是时候查看和剖析应用实际上正在做的事情了。

剖析(profile)

为了剖析,我们将使用 1000 请求量每秒的负载然后使用 pprof 来采样这些栈获得我们的程序将它的时间花费在哪些地方。这个可以在负载被使用时,通过 pprof 的 HTTP 端点,并用 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

火焰图展示了应用在哪些地方花费时间和在那里花费了多少时间!来自 Brendan Gregg 的描述

x 轴展示了栈的横截数量(profile population),按照字典序排列(注意,它不是通过调用时间长短排序的),y 轴表示栈的深度,从顶层以 0 开始计数。每个矩形表示一个栈帧。栈帧的宽度越宽,则它在栈中出现的次数越多。最底层显示的是正在 CPU 中运行的,在它上面的就是它的父函数。颜色通常没有意义,随机选择来区分不同的栈帧。

分析——假说

为了引导优化,我们将重点放在查找那些”无用功“。我们将尝试查找产生这些”无用功“的大部分源码,并删除它。因为剖析可以揭露出这个服务把时间花费在哪里了,这就需要从中找出潜在的重复工作,修改代码来改进它, 重新运行测试,并观察性能是否接近目标值。

根据 Bredan Gregg 的描述,go pprof 的火焰图是从上往下读的。每一行代表一个栈帧(函数调用)。第一行是这个程序的入口点,它是所有其他调用的父亲(即,所有其他调用的栈中都有第一行这个函数地址)。后面的行从这里分支出去:

在火焰图中的函数名上面停留会显示在跟踪期间,这个函数在栈中的时间总数。HTTPServe 在栈中占时为剖析时间的 65%,而各种 Go 运行时方法 runtime.mcall, mstart, gc 构成了剩下的剖析时间。一个有趣的事情是程序总运行时间的 5% 被花费在 DNS 的查询中:

唯一的 IP 地址需要程序解析的是 Postgres 的地址。点击 FindByAge 显示:

有趣的是,这个图显示了 main 源码有 3 点造成了这个延迟:关闭 / 释放连接,查询数据,和连接。基于这个火焰图, DNS 查询和连接的关闭、打开数量大概占了总的服务时间的 13%。

假说:使用连接池来重用连接可以减少 HTTP 交互时间,从而有更高的吞吐量和更低的延迟。

应用优化——实践

更新这个应用,避免每次 postgres 请求都重建连接。一个解决方法是使用应用级的 连接池 。这个实践将使用 Go sql 驱动的池配置选项来配置一个连接池:

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

if err != nil {
   return nil, err
}

执行、观察、分析

重新运行 1000 测试负载,显示 99% 的 HTTP 请求延迟都在 60ms SLO 以下!

而成本方面:

10k 请求量 / 秒 / 1000 请求量 / 机器 = 10 台机器 + 1

我们继续尝试,看能不能更好!

2000 请求量 / 秒

双倍请求显示也一样。左上角的图显示应用正在接收每秒 2000 的请求量,而 p100 max 客户端延迟在 60 ms 以上,p99 线却一直在 SLO 以内。

这次成本为:

10k 请求量 / 秒 / 2000 请求量 / 机器 = 5 台机器 + 1

3000 请求量 / 秒

此时,这个服务能够在 3000 请求量 / 秒 的速率下,p99 延迟 < 60ms,而 p100 从 2000 请求量 / 秒 时的 100-250 ms 到此时的 250-1000 ms。这个 SLO 没有被违背,可被接受的成本为:

10k 请求量 / 秒 / 3000 请求量 / 机器 = 4 台机器 + 1

尝试更进一步的分析。

分析——假说

生成并可视化 3000 请求量 / 秒 下应用的剖析情况如下:

可以看出,FindByAge 6% 的交互时间是由 Dialing 连接造成的!!建立一个连接池提高了性能,但是可以观察到应用还是继续做创建新的数据库连接的重复工作!

假说:即使连接被放到池里了,但是他们一直被回收并清理导致应用必须重新连接。调整空闲连接数等于池的大小应该可以帮助减少延迟时间,最小化应用花在创建数据库连接的总时间。

应用优化——实践

我们尝试设置 MaxIdleConns 等于池的大小(或者在 这里 查看):

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

执行、观察、分析

3000 请求量 / 秒

p99 总是 < 60ms !而 3000 请求量每秒也有更低的 p100 了!

仔细观察下面的火焰图,连接的 dial 不再出现了!仔细看 pg(*conn).query 那行,整个 dialing 不再存在了:

结论

性能分析是理解是否满足客户期望和非功能需求的至关重要的手段。通过符合客户期望的审查分析性能能够帮助我们决定哪些是性能可接受的,哪些是不可接受的。Go 在标准库中提供了强大的组件,让这个分析的一系列方法变得简单易用。

我对你阅读本文表示感谢,并希望你能反馈!


作者:dm03514 译者:daliny 校对:polaris1119
本文由 GCTT 原创编译,Go语言中文网 荣誉推出

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • 转 # https://www.cnblogs.com/easypass/archive/2010/12/ 08/...
    吕品㗊阅读 9,698评论 0 44
  • 当 app 和服务器进行通信的时候,大多数情况下,都是采用 HTTP 协议。HTTP 最初是为 web 浏览器而定...
    Flysss1219阅读 1,252评论 0 4
  • 性能既是客观指标,诸如响应时间、吞吐量等技术指标;又是实际参与者的主观感受。 1 性能测试 性能测试是性能优化的前...
    deniro阅读 1,339评论 2 15
  • 蔚县鼓楼位于蔚州古城中心,是蔚州古城著名景点之一。鼓楼始建于明代洪武十四年(公元1381年),落成于洪武十...
    2018栏杆拍遍阅读 483评论 0 0