前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go Performance

Go Performance

作者头像
gopher云原生
发布2022-11-22 14:59:00
5780
发布2022-11-22 14:59:00
举报
文章被收录于专栏:gopher云原生gopher云原生

计算机系统自下而上可分为:硬件、操作系统和应用程序,其中操作系统管理着各种计算机硬件,并控制和协调应用程序对硬件的分配与使用。

硬件

一切软件包括操作系统都是在硬件上运行的,一个高性能程序,也不能脱离一个好的硬件环境的支撑。

受益于「 摩尔定律 」,计算机会越来越快,我们可能根本不需要关心代码的性能,硬件制造商会为我们解决性能问题。

但事实真是如此吗。

2005 年,C++ 委员会负责人 Herb Sutter 发布了一篇 《免费午餐结束》[1]的文章,断言未来的程序员将无法通过依赖更快的硬件来修复程序的性能问题。

放到今日来看,确实如此:内存没有跟上 CPU 速度的增长,高速缓存大小受到限制,CPU 时钟速度停滞不前。

时钟速度在 2005-2015 十年来没有增加

免费午餐的时代已经结束。

在性能优化的过程中,我们应该更多地去考虑软件层面,而不是等待更快的硬件来挽救局面。(当然,要是拿个 1c1g 的服务器来部署 GitLab ,那就说不过去了)

操作系统

我们大部分软件部署都是使用 Linux 操作系统。

在 Linux 中有很多命令可以用于分析服务器的性能,国际知名的计算性能专家 Brendan Gregg[2] 将这些工具称为:Linux Performance Observability Tools :

Linux 性能可观测性工具[3]

摘取一些平时常用的命令:

uptime-系统平均负载

top-进程资源占用状况

iostat-系统设备的IO负载情况

strace-跟踪进程执行时的系统调用和所接收的信号

这些工具我们可以直接通过 man 手册获取其相关用法,这里就不再一一介绍了。

性能优化不能全靠凭空想象,只有合理利用相应的工具,才能更快地分析出我们需要优化的地方。

应用程序

性能优化离工作所执行的地方越近越好:最好在应用程序里。应用程序包括数据库、Web 服务器、应用服务器、负载均衡器、文件服务器,等等。

一般来说,应用程序的性能指标主要关注以下三个方面:

  • 延时:应用程序响应时间
  • 吞吐量:应用程序操作率或者数据传输率
  • 资源使用率:对于给定应用程序工作负载,高效地使用资源

一个能有效提高应用程序性能的方法是找到对应生产环境工作负载的公用代码路径,并开始对其做优化。如果应用程序是 CPU 密集型的,那么意味着代码路径会频繁占用 CPU。如果应用程序是 I/O 密集型的,则应该查看导致频繁 I/O 的代码路径。这些都能借助应用程序的观测工具来分析和剖析进而确定优化点。

得益于 Go 生态系统的强大,我们有大量的 API 和工具可以用来诊断 Go 程序中的逻辑和性能问题。

大致上可以分成 4 种:

1、Profiling

Profiling 分析,包括 CPU 分析、内存分析、goroutine 分析等。

Go 在运行时可以通过 runtime/pprof 写入文件的方式 或 net/http/pprof 标准的 HTTP 接口的方式来 提供 分析数据。

之后就可以借助 go tool pprof 工具来 读取 数据以进行可视化分析。

简单示例:

代码语言:javascript
复制
package main

import (
 "bytes"
 "net/http"
 _ "net/http/pprof"
 "time"
)

var buf = bytes.NewBuffer(nil)

func main() {
 go http.ListenAndServe(":6060", nil)

 t1 := time.NewTicker(1 * time.Second)
 t2 := time.NewTicker(10 * time.Second)
 for {
  select {
  case <-t1.C:
   mem()
   cpu()
  case <-t2.C:
   buf.Reset()
  }
 }
}

func mem() {
 buf.Write(make([]byte, 100*1024*1024))
}

func cpu() {
 for i := 0; i < 10000000000; i++ {
 }
}

运行该程序,等待一段时间后,开始进行采样分析。

首先执行 go tool pprof -http=:9090 http://localhost:6060/debug/pprof/profile 可以查看 CPU 占用情况:

查看 top 耗时

定位源码

火焰图宽度也很明显

继续执行 go tool pprof -http=:9091 http://localhost:6060/debug/pprof/allocs 查看内存分配情况:

程序启动以来总的内存分配情况

定位源码

当前堆上内存使用情况

结合源码来看,每隔 1s 会申请 100M 内存,10s 粗算 1G ,由于每隔 10s 会清空 Buffer ,所以程序实时堆内存占用稳定在 1.5G 左右( Buffer Grow 过程会增加容量)也是合理的。

通过 pprof 可以很容易地定位到代码中的热点位置,此时再结合我们的业务逻辑来考虑性能优化就可以事半功倍了。

2、Tracing

微服务架构下的服务治理中有一个 Tracing 分布式链路追踪 模块,其做法是在程序代码中使用 OpenTracing/OpenTelemetry API 进行埋点,采集调用的相关信息后发送到追踪服务器(Jaeger 等)进行分析处理。

对于单体应用,所有模块都存在于一个进程中,可以考虑使用 Go 提供的 golang.org/x/net/trace 包,作用包括:

  • 在 Go 进程中检测和分析应用程序的延迟
  • 统计一连串调用链中特定调用的耗时
  • 方便计算利用率进而改进性能,因为很多瓶颈在收集追踪数据之前并不明显

其中 trace.Trace 可以为短期对象(通常是 HTTP 请求)提供追踪,trace.EventLog 可以为长期存在的对象(例如 RPC 连接)提供追踪。

后续两者的追踪数据分别可以从 /debug/requests/debug/events HTTP 端点中查看。

简单示例:

代码语言:javascript
复制
package main

import (
 "math/rand"
 "net/http"
 "time"

 "golang.org/x/net/trace"
)

func main() {
 http.HandleFunc("/hello", hello)
 http.ListenAndServe(":8080", nil)
}

func hello(w http.ResponseWriter, req *http.Request) {
 tr := trace.New("SuperGopher", req.URL.Path)
 defer tr.Finish()
 time.Sleep(time.Duration(rand.Intn(10)+1) * time.Second)

 output := "hello go"
 tr.LazyPrintf("输出:%s", output)
 w.Write([]byte(output))
}

多次请求 http://localhost:8080/hello 接口后,可以访问 http://localhost:8080/debug/requests?fam=SuperGopher&b=0&exp=1 页面查看追踪得到的数据:

使用 trace.Trace 追踪的数据

3、Debugging

Debugging 调试是识别程序异常行为的过程,能够让我们了解程序的执行流程和当前状态。

想调试代码就得使用调试器,Go 主要使用到两种调试器:

  • Delve :专门为 Go 语言设计开发的调试工具,支持 Go runtime 和内置类型,Delve 正试图成为 Go 程序的全功能可靠调试器
  • GDB :最早支持的调试工具,但并不是 Go 程序的可靠调试器,适合用来调试 Cgo 代码或调试 Go runtime 本身

以 Delve 为例,安装可以直接使用 go install :

代码语言:javascript
复制
go install github.com/go-delve/delve/cmd/dlv@latest

简单示例:

代码语言:javascript
复制
package main

import "fmt"

func main() {
 fmt.Println("hello")
 fmt.Println("go")
}

使用 dlv 的调试过程

除了 dlv 这种 CLI 客户端,Delve 还有各种 Editor plugins 或 GUI ,就比如我们平时开发使用的 Goland 或 VS Code 的调试器就是使用了 Delve Editor plugins 。

所以哪个顺手哪个来就行。

4、Runtime statistics and events

Go Runtime 提供了内部事件(调度、系统调用、垃圾回收、堆大小等)的统计信息和报告,用户可以主动监控这些统计数据,以更好地了解 Go 程序的整体运行状况和性能,一些经常监控的统计数据有:

  • runtime.ReadMemStats :报告与堆分配和垃圾收集相关的指标
  • debug.ReadGCStats :读取有关垃圾收集的统计信息
  • debug.Stack :返回当前堆栈跟踪信息
  • debug.WriteHeapDump :暂停所有 goroutine 的执行,将堆转储( Go 进程内存的快照)到文件中
  • runtime.NumGoroutine :返回当前 goroutine 的数量

对于这些事件,Go 可以使用 Trace 执行跟踪器捕获后进行可视化分析。

和基于毫秒(ms)频度的 pprof 定时采样相比,基于 event 的 tracer 精确到了纳秒(nanosecond)级精度,对系统的性能影响还是比较大的。在一般情况下我们不需要使用 trace 来定位性能问题,除非需要深入到 runtime 级别,比如想要:

  • 了解 goroutines 是如何执行的
  • 了解一些核心 runtime 事件,例如 GC
  • 识别并行性较差的执行等

Go 应用开启 Trace ,主要有三种方式:

  • 手动使用 runtime/trace 包,将收集的数据转存到文件
  • 通过 net/http/pprof 包的 /debug/pprof/trace HTTP 端点导出,适合生产环境
  • 测试时使用 go test -trace 参数

简单示例:

代码语言:javascript
复制
package main

import (
 "context"
 "os"
 "runtime/trace"
 "sync"
)

func main() {
 f, err := os.Create("trace.out")
 if err != nil {
  panic(err)
 }
 defer f.Close()

 err = trace.Start(f)
 if err != nil {
  panic(err)
 }
 defer trace.Stop()

 wg := sync.WaitGroup{}
 wg.Add(2)

 go func() {
  defer wg.Done()

  trace.Log(context.Background(), "goroutine", "SuperGopher1")
 }()
 go func() {
  defer wg.Done()

  trace.Log(context.Background(), "goroutine", "SuperGopher2")
 }()

 wg.Wait()
}
代码语言:javascript
复制
$ go run main.go
$ go tool trace trace.out
2022/08/10 14:52:52 Parsing trace...
2022/08/10 14:52:52 Splitting trace...
2022/08/10 14:52:52 Opening browser. Trace viewer is listening on http://127.0.0.1:55706

trace 支持功能

trace 查看跟踪界面

另外如果设置了 GODEBUG 环境变量,也可以通过日志的方式打印出相应的事件:

  • GODEBUG=gctrace=1 :在每次垃圾收集时打印垃圾收集器事件,汇总收集的内存量和暂停时间
  • GODEBUG=inittrace=1 :打印完成包初始化工作的执行时间和内存分配信息的摘要
  • GODEBUG=schedtrace=X :每 X 毫秒打印一次调度事件

简单示例:

代码语言:javascript
复制
package main

import (
 "runtime"
 "time"
)

func main() {
 n := 50000000
 m := make(map[int]*int, n)
 for i := 0; i < n; i++ {
  v := i
  m[i] = &v
 }
 for i := 0; i < 10; i++ {
  runtime.GC()
 }
 time.Sleep(time.Second)
}
代码语言:javascript
复制
$ GODEBUG=gctrace=1 go run main.go

(forced) 结尾的是 runtime.GC() 调用所触发的

尾声

  • 优化越靠近应用层效果越好
  • 尽可能使用最新发布的 Go 版本,享受官方技术红利
  • 保持简单,Go 编译器会针对简单代码进行优化
  • 观测而不是猜测代码瓶颈
  • 不是每部分都需要高性能
  • 注意内存分配的使用,尽量避免不必要的分配,复用可以复用的一切
  • 尽量避免堆内存的分配,优先使用栈内存
  • 不要过早优化

Readable means reliable -- Rob Pike

推荐继续阅读:

  • 《Systems Performance》中文版:《性能之巅》
  • High Performance Go Workshop[4]
  • Go Diagnostics[5]

参考资料

[1]

《免费午餐结束》: http://www.gotw.ca/publications/concurrency-ddj.htm

[2]

Brendan Gregg: https://www.brendangregg.com/index.html

[3]

Linux 性能可观测性工具: https://www.brendangregg.com/linuxperf.html

[4]

High Performance Go Workshop: https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html

[5]

Go Diagnostics: https://go.dev/doc/diagnostics

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-08-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 gopher云原生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 硬件
  • 操作系统
  • 应用程序
    • 1、Profiling
      • 2、Tracing
        • 3、Debugging
          • 4、Runtime statistics and events
          • 尾声
            • 参考资料
            相关产品与服务
            负载均衡
            负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档