17611538698
webmaster@21cto.com

提升 Go 应用程序性能的 6 种方法

编程语言 0 606 2023-03-31 02:00:45

图片

导读:在Go中,如何有效提高性能,减少垃圾收集。本文有实践干货。

1.当应用运行在Kubernetes中,设置GOMAXPROCS匹配Linux容器CPU配额


Go 的调度程序可以有与其运行设备的内核数量一样多的线程。

如果应用程序部署在 Kubernetes 环境中的节点上,当这些 Go 应用程序开始运行时,它可以拥有与节点中内核数一样多的线程。

这些节点上运行着许多不同的应用程序,因此这些节点包含相当多的内核。

我们可以使用https://github.com/uber-go/automaxprocs,这样Go 调度程序使用的线程数将与在 k8s yaml 中定义的 CPU 数量一样多。

例子:


应用程序 CPU 限制(在 k8s.yaml 中定义):

1 个内核

节点内核数:64


通常情况下, Go 调度器会尝试使用 64 个线程。但是,如果开发者使用 automaxprocs 的参数,它将只用到一个线程。

我们在实践中观察到,在实现它的应用程序中有着相当大的性能改进:

  • ~60% 的 CPU 使用率

  • ~%30 的内存使用率

  • ~%30 的响应时间


对结构字段进行排序


结构化字段的顺序,可以直接影响你的内存使用。

例如:

type testStruct struct {     testBool1   bool     // 1 字节    testFloat1 float64  // 8 字节    testBool2   bool     // 1 字节    testFloat2 float64  // 8 字节}

你可能认为这个结构将占用 18 个字节,但它不会如此。

func  main () {    a := testStruct{}    fmt.Println(unsafe.Sizeof(a)) // 32 字节}

这是因为内存对齐在 64 位体系结构中的内部工作方式。

图片


我们怎样才能减少内存占用率?可以按以下方式,根据内存填充对字段进行排序。

type testStruct struct { testFloat1 float64  // 8 字节testFloat2 float64  // 8 字节testBool1   bool     // 1 字节testBool2   bool     // 1 字节} 
func main () { a := testStruct{} fmt.Println(unsafe.Sizeof(a) ) // 24 字节}


图片

因此,我们不必总是手动对这些字段进行排序。此外,开发者还可以使用诸如fieldalignment。

地址:

https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment

3.垃圾收集调整


在 Go 1.19 之前,只需要GOGC(runtime/debug.SetGCPercent)配置 GC 周期;

但是,在某些运行情况下也会超出内存限制。

在 Go 1.19 中,开发者将拥有称为 GOMEMLIMIT. GOMEMLIMIT 的一个新的环境变量,它允许用户限制 Go 进程可以使用的内存量。

此功能可以更好地控制 Go 应用程序的内存使用,防止使用过多内存导致性能问题或崩溃。

设置 GOMEMLIMIT 变量后,开发者可以确保自己的 Go 程序平稳高效地运行,进而不会对系统造成过度的压力。

但是它不会取代 GOGC,而是与其结合使用。

开发者可以禁用 GOGC 的百分比配置,仅使用 GOMEMLIMIT 来触发垃圾收集。

图片

GOGC 100 与内存限制 100MB

图片


GOGC 关闭与内存限制 100


通过以上调校,运行时垃圾收集量显着减少。但在运行应用时需要小心从事,如果你并不知道应用程序的限制,请不要设置 GOGC=off。

4.使用unsafe包对string<->byte进行无拷贝转换


在字符串到字节或字节到字符串之间的转换时,这表示正在复制变量。但在Go 的内部,这两种类型通常使用StringHeader和SliceHeader值。我们可以在这两种类型之间进行转,而无需再进行额外分配。

// 对于 Go 1.20 与更高版本func  StringToBytes (s string ) [] byte {  return unsafe.Slice(unsafe.StringData(s), len (s)) } 
func BytesToString (b [] byte ) string { return unsafe.String( unsafe.SliceData(b), len (b)) }
// 对于低版本// 示例地址// https://github.com/bcmills/unsafeslice/blob/master/unsafeslice.go#L116

fasthttp(地址:https://github.com/valyala/fasthttp)以及fiber(地址:https://github.com/gofiber/fiber)等知名的外部库在内部也使用这种结构。

注:如果byte或字符串值以后可能还会更改,请不要用此功能。

5. 使用 jsoniter 而不是 encoding/json


我们经常在代码中使用Marshaland方法来进行序列化或者反序列化。

Jsoniter是 100% 兼容 encoding/jsonUnmarshal 的替代品。它的地址:

https://github.com/json-iterator/go

以下是一些基准测试数据:

图片


替换encoding/json 相当简单:

import "encoding/json"
json.Marshal(&data)json.Unmarshal(input, &data)import jsoniter "github.com/json-iterator/go"
var json = jsoniter.ConfigCompatibleWithStandardLibraryjson.Marshal(&data)json.Unmarshal(input, &data)

6.使用 sync.Pool 减少堆分配


对象池背后的主要概念,是避免重复创建和销毁对象的开销,但是会对性能产生负面影响。


项目中缓存提前分配但不使用,有助于减少垃圾收集器的负载,并可以在以后启用它们。


以下是一个代码例子:

type Person struct { Name string}
var pool = sync.Pool{ New: func() any { fmt.Println("Creating a new instance") return &Person{} },}
func main() { person := pool.Get().(*Person) fmt.Println("Get object from sync.Pool for the first time:", person) person.Name = "Mehmet"
fmt.Println("Put the object back in the pool") pool.Put(person)
fmt.Println("Get object from pool again:", pool.Get().(*Person))
fmt.Println("Get object from pool again (new one will be created):", pool.Get().(*Person))}
//Creating a new instance//Get object from sync.Pool for the first time: &{}//Put the object back in the pool//Get object from pool again: &{Mehmet}//Creating a new instance//Get object from pool again (new one will be created): &{}

通过使用sync.Pool,解决了New Relic Go Agent 中的内存泄漏问题。

地址:https://github.com/newrelic/go-agent/pull/620

以前它为每个请求创建一个新的 gzip 写入器。这里,我们没有为每个请求创建一个新的写入器,而是创建了一个池,以方便代理使用该池中的写入器。并且,不会为每个请求创建新的 gzip 写入器实例。

这样的处理将大大减少堆的使用,系统运行将使用更少的 GC。这一种开发使我们的应用程序的 CPU 使用率降低了约 40%,内存使用率降低了约 22%。

以上是分享我在 Go 应用程序中正在使用的优化实践,希望它也对各位有用。

欢迎所有的读者反馈,谢谢!


作者:老田

评论