导读:在Go中,如何有效提高性能,减少垃圾收集。本文有实践干货。
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=off。
在字符串到字节或字节到字符串之间的转换时,这表示正在复制变量。但在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或字符串值以后可能还会更改,请不要用此功能。
我们经常在代码中使用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.ConfigCompatibleWithStandardLibrary
json.Marshal(&data)
json.Unmarshal(input, &data)
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 应用程序中正在使用的优化实践,希望它也对各位有用。
欢迎所有的读者反馈,谢谢!
作者:老田
本文为 @ 万能的大雄 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。