首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

剖析与优化 Go的web 应用

首发于:https://studygolang.com/articles/12685

Go 语言有一个很强大的内置分析器(profiler),支持CPU、内存、协程 与 阻塞/抢占(block/contention)的分析。

开启分析器(profiler)

Go 提供了一个低级的分析 API runtime/pprof ,但如果你在开发一个长期运行的服务,使用更高级的 net/http/pprof 包会更加便利。

你只需要在代码中加入 import _ "net/http/pprof" ,它就会自动注册所需的 HTTP 处理器(Handler) 。

如果你的 web 应用使用自定义的 URL 路由,你需要手动注册一些 HTTP 端点(endpoints) 。

如上代码那样,开启 web 应用,然后使用 pprof 工具:

pprof 的最大的优点之一是它是的性能负载很小,可以在生产环境中使用,不会对 web 请求响应造成明显的性能消耗。

但是在深入挖掘 pprof 之前,我们需要一个真实案例来展示如何在 GO 应用中检查并解决性能问题。

案例: Left-pad 微服务

假设,你需要开发一个全新的微服务,为输入的字符串添加左填充

这个服务需要收集基本的指标(metric),如请求的数量与每个请求的响应时间。收集到的所有指标都应该发送到一个指标聚合器(metric aggregator)(例如 StatsD)除此之外,这个服务需要日志记录这个请求的详细信息,如 URL,IP 地址与 user-agent 。

你可以在 Github 上看到这个微服务的初步实现,tag 为 v1

编译并运行这个应用

性能分析

我们将要测试这个微服务每秒可以处理多少个请求,可以使用这个工具 Apache Benchmark tool :

测试结果不差,但可以做到更快

Requests per second: 22810.15 #/sec

Time per request: 0.042 [ms] (mean, across all concurrent requests)

注:上面的测试结果的执行环境:笔记本 MacBook Pro Late 2013 (2.6 GHz Intel Core i5, 8 GB 1600 MHz DDR3, macOS 10.12.3) , Go编译器版本是1.8 。

CPU 分析(CPU profile)

再次执行 Apache benchmark tool ,但这次使用更高的请求数量(1百万应该足够了),并同时执行 pprof :

这个 CPU profiler 默认执行30秒。它使用采样的方式来确定哪些函数花费了大多数的CPU时间。Go runtime 每10毫秒就停止执行过程并记录每一个运行中的协程的当前堆栈信息。

当 pprof 进入交互模式,输入 ,这条命令会展示收集样本中最常出现的函数列表。在我们的案例中,是所有 runtime 与标准库函数,这不是很有用。

有一个更好的方法来查看高级别的性能概况 —— 命令,它会生成一个热点(hot spots)的 SVG 图像,可以在浏览器中打开它:

从上图你可以看到这个应用花费了 CPU 大量的时间在 logging、测试报告(metric reporting )上,以及部分时间在垃圾回收上。

使用 list 命令可以 inspect 每个函数的详细代码,例如 :

函数堆栈分析(Heap profile)

执行堆栈分析器(heap profiler)

默认情况下,它显示当前正在使用的内存量:

但是我们更感兴趣的是分配的对象的数量,执行 pprof 时使用选项

几乎 70% 的对象仅由两个函数分配 —— leftpad 与 StatsD ,我们需要更仔细的查看它们:

还有一些非常有用的调试内存问题的选项, 可以显示正在使用的对象的数量,可以显示程序启动以来分配的多少内存。

自动内存分配很便利,但世上没有免费的午餐。动态内存分配不仅比堆栈分配要慢得多,还会间接地影响性能。你在堆上分配的每一块内存都会增加 GC 的负担,并且占用更多的 CPU 资源。要使垃圾回收花费更少的时间,唯一的方法是减少内存分配。

逃逸分析(Escape analysis)

无论何时使用 & 运算符来获取指向变量的指针或使用 make 或 new 分配新值,它并不一定意味着它被分配在堆上。

在上面的例子中, make([]string, 8) 是在栈上分配内存的。Go 通过 escape analysis 来判断使用堆而不是栈来分配内存是否安全。你可以添加选项 -gcflags=-m 来查看逃逸分析(escape analysis)的结果:

Go 编译器足够智能,可以将一些动态分配转换为栈分配。但你如果使用接口来处理变量,会导致糟糕的情况。

Dmitry Vyukov 的论文 Go Escape Analysis Flaws 讲述了更多的逃逸分析(escape analysis)无法处理的案例。

一般来说,对于你不需要再修改数据的小结构体,你应该使用值传参而不是指针传参。

注:对于大结构体,使用指针传参而不是值传参(复制整个结构体)的性能消耗更低。

协程分析(Goroutine profile)

Goroutine profile 会转储协程的调用堆栈与运行中的协程数量

上图只有18个活跃中的协程,这是非常小的数字。拥有数千个运行中的协程的情况并不少见,但并不会显著降低性能。

阻塞分析(Block profile)

阻塞分析会显示导致阻塞的函数调用,它们使用了同步原语(synchronization primitives),如互斥锁(mutexes)和 channels 。

在执行 block contention profile 之前,你必须设置使用runtime.SetBlockProfileRate 设置 profiling rate 。你可以在函数或者 函数中添加这个调用。

与 花费了大量的时间来等待 中的互斥锁。导致这个结果的原因是 package 的实现使用了互斥锁来对多个协程共享的文件进行同步访问(synchronize access)。

指标(Benchmarking)

正如我们之前注意的,在这个案例的最大的几个性能杀手是 log package ,leftpad 与 StatsD.Send 函数。现在我们找到了性能瓶颈,但是在优化代码之前,我们需要一个可重复的方法来对我们关注的代码进行性能测试。Go 的 testing package 包含了这样的一个机制。你需要在测试文件中创建一个函数,以 func BenchmarkXxx(*testing.B) 的格式。

也可以使用 对这整个 HTTP 程序进行基准测试:

执行基准测试:

它会显示每次迭代需要的时间量,以及 内存/分配数量 (amount of memory/number of allocations):

优化性能

Logging

让应用运行更快,一个很好又不是经常管用的方法是,让它执行更少的工作。除了 debug 的目的之外,这行代码在 web service 中不需要。所有非必要的 logs 应该在生产环境中被移除代码或者关闭功能。可以使用分级日志(a leveled logger)来解决这个问题,比如这些很棒的 日志工具库(logging libraries)

关于打日志或者其他一般的 I/O 操作,另一个重要的事情是尽可能使用有缓冲的输入输出(buffered input/output),这样可以减少系统调用的次数。通常,并不是每个 logger 调用都需要立即写入文件 —— 使用 bufio package 来实现 buffered I/O 。我们可以使用 或者 来简单地封装 对象,再传递给 logger :

左填充(leftpad)

再看一遍 函数

在每一个循环中连接字符串的做法并不高效,因为每一次循环迭代都会分配一个新的字符串(反复分配内存空间)。有一种更好的方法来构建字符串,使用 bytes.Buffer :

另外,我们还可以使用 string.Repeat ,使代码更加简洁:

StatsD client

接下来需要优化的代码是 函数:

以下是有一些可能的值得改进的地方:

对字符串格式化非常便利,它性能表现很好,除非你每秒调用它几千次。不过,它把输入参数进行字符串格式化的时候会消耗 CPU 时间,而且每次调用都会分配一个新的字符串。为了更好的性能优化,我们可以使用 + 来替换它。

这个函数不需要每一次都创建一个新的 实例,它可以声明为全局变量,或者作为结构体的一部分。

用 替换 ,并且使用堆栈上分配的 来传递变量,防止额外的堆分配。

这样做,将分配数量(number of allocations)从14减少到1个,并且使 运行快了4倍。

测试优化结果

做了所有优化之后,基准测试显示出非常好的性能提升:

注: 作者使用 benchcmp 来对比结果:

再一次运行

这个 web 服务现在可以每秒多处理10000个请求!

优化技巧

避免不必要的 heap 内存分配。

对于不大的结构体,值传参比指针传参更好。

如果你事先知道长度,最好提前分配 maps 或者 slice 的内存。

生产环境下,非必要情况不打日志。

如果你要频繁进行连续的读写,请使用缓冲读写(buffered I/O)

如果你的应用广泛使用 JSON,请考虑使用解析器/序列化器(parser/serializer generators)(作者个人更喜欢 easyjson)

在主要路径上的每一个操作都很关键(Every operation matters in a hot path)

结论

有时候,性能瓶颈可能不是你预想那样,理解应用程序真实性能的最好途径是认真分析它。

你可以在 Github 上找到本案例的完整的源代码,初始版本 tag 为 v1,优化版本 tag 为 v2 。比较这两个版本的传送门 。

作者并非以英语为母语,并且他在努力提高英语水平,如果原文有表达问题或者语法错误,请纠正他。

via: http://artem.krylysov.com/blog/2017/03/13/profiling-and-optimizing-go-web-applications/

作者:Artem Krylsov

译者:lightfish-zhang

校对:wang_zheng_zhi

本文由 GCTT 原创编译,Go 中文网 荣誉推出

喜欢本文的朋友们,欢迎长按下图关注订阅哦!

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181010B0YGMW00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券