优化你的Go应用程序
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
这句话的意思是:“我们该如何减少呢?我们可以根据内存填充对字段进行排序。”
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
}
我们并不总是需要手动排序这些字段。你可以使用一些工具,例如 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设置为“关闭”。
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
如fasthttp和fiber等库也在其内部采用了这种结构。
注意:如果你的字节或字符串值可能稍后更改,请勿使用此功能。
5. 使用jsoniter代替encoding/json
我们通常在代码中使用Marshal和Unmarshal方法进行序列化或反序列化。
Jsoniter是encoding/json的100%兼容替代品。
以下是一些基准测试:
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
评论(0)