首页
Preview

六个提升Golang应用性能的方法

优化你的Go应用程序

image.png

1. 如果你的应用程序在Kubernetes中运行,自动设置GOMAXPROCS以匹配Linux容器的CPU配额

Go调度程序可以有与其运行设备的核心数相同数量的线程。由于我们的应用程序在Kubernetes环境中的节点上运行,当我们的Go应用程序开始运行时,它可以有与节点上核心数相同数量的线程。由于许多不同的应用程序在这些节点上运行,这些节点可能包含相当多的核心。

通过使用 https://github.com/uber-go/automaxprocs ,Go调度程序使用的线程数将与你在k8s yaml中定义的CPU限制一样多。

例如:

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

1 core

Node core counts: 64

通常,Go调度程序将尝试使用64个线程,但是如果我们使用automaxprocs,它将仅使用一个线程。

我在实现这个的应用程序中观察到了相当高的性能改进。 ~60%的CPU使用率,30%的内存使用率以及30%的响应时间。

2. 对结构体字段进行排序

结构体中使用的字段的顺序直接影响你的内存使用情况。

例如:

type testStruct struct {
 testBool1  bool    // 1 byte
 testFloat1 float64 // 8 bytes
 testBool2  bool    // 1 byte
 testFloat2 float64 // 8 bytes
}

你可能认为这个结构体会占用18个字节,但实际上不会。

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

这是因为在64位架构中,内存对齐的内部工作方式。欲了解更多信息,请阅读这篇文章。https://en.wikipedia.org/wiki/Data_structure_alignment

image.png

这句话的意思是:“我们该如何减少呢?我们可以根据内存填充对字段进行排序。”

type testStruct struct {
 testFloat1 float64 // 8 bytes
 testFloat2 float64 // 8 bytes
 testBool1  bool    // 1 byte
 testBool2  bool    // 1 byte
}

func main() {
 a := testStruct{}
 fmt.Println(unsafe.Sizeof(a)) // 24 bytes
}

image.png

我们并不总是需要手动排序这些字段。你可以使用一些工具,例如 fieldalignment,自动对你的结构体进行排序。

go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest

fieldalignment -fix ./... 

3. 垃圾回收调整

在 Go 1.19 之前,我们只能使用 GOGC(runtime/debug.SetGCPercent)配置 GC 周期;然而,在某些情况下,我们超出了内存限制。在 Go 1.19 中,我们现在有了 GOMEMLIMIT。GOMEMLIMIT 是一种新的环境变量,允许用户限制 Go 进程可以使用的内存量。这个功能提供了更好的控制 Go 应用程序内存使用的方式,防止它们使用过多的内存,导致性能问题或崩溃。通过设置 GOMEMLIMIT 变量,用户可以确保他们的 Go 程序在不给系统造成不必要负担的情况下平稳高效地运行。

它并不替代 GOGC,而是与其配合使用。

你也可以禁用 GOGC 百分比配置,只使用 GOMEMLIMIT 触发垃圾回收。

GOGC 100 and Memory Limit 100MB

GOGC off and Memory Limit 100

垃圾回收运行量在显著减少,但在使用时需要小心。如果你不知道应用程序的限制,请勿将GOGC设置为“关闭”。

4. 使用unsafe包进行字符串 <-> 字节转换而无需复制

在字符串转换为字节或字节转换为字符串时,我们需要复制变量。但是,在Go内部,这两种类型通常使用StringHeader和SliceHeader值。我们可以在不进行额外分配的情况下在这两种类型之间进行转换。

// For Go 1.20 and higher
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))
}

// For lower versions
// Check the example here
// https://github.com/bcmills/unsafeslice/blob/master/unsafeslice.go#L116

fasthttpfiber等库也在其内部采用了这种结构。

注意:如果你的字节或字符串值可能稍后更改,请勿使用此功能。

5. 使用jsoniter代替encoding/json

我们通常在代码中使用Marshal和Unmarshal方法进行序列化或反序列化。

Jsoniter是encoding/json的100%兼容替代品。

以下是一些基准测试:

image.png

import "encoding/json"

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

var json = jsoniter.ConfigCompatibleWithStandardLibrary
json.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 中的一个内存泄漏问题。以前,每个请求都会创建一个新的 gzip writer。我创建了一个池,这样代理就可以使用其中的 writer,并且不会为每个请求创建新的 gzip writer 实例,而是使用已创建的实例。这样可以大大减少堆使用量,从而使系统运行更少的GC。这个开发大约减少了我们应用程序的 CPU 使用量约40%和内存使用量约22%。

译自:https://betterprogramming.pub/6-ways-to-boost-the-performance-of-your-go-applications-5382bb7532d7

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
Golang社区
欢迎关注微信公众号:Golang社区

评论(0)

添加评论