优化您的 Go 应用程序
img1. 如果您的应用程序在 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% 的响应时间。
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 位架构中内部内存对齐的工作方式。有关更多信息,您可以阅读这篇文章。
many boxes showing 8 bytes of testbool1, testFIoat1, testbool2, testFIoat2
我们如何降低内存使用?我们可以根据内存填充来对字段进行排序。
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
}
img
我们并不总是需要手动排序这些字段。您可以使用诸如fieldalignment等工具来自动对结构体进行排序。
fieldalignment?-fix?./...3. 垃圾回收调优
在 Go 1.19 之前,我们只能使用GOGC(runtime/debug.SetGCPercent)来配置垃圾回收周期;然而,在某些情况下,我们可能会超出内存限制。随着 Go 1.19 的到来,我们现在拥有了GOMEMLIMIT。GOMEMLIMIT是一个新的环境变量,允许用户限制 Go 进程可以使用的内存量。这个功能提供了更好的控制 Go 应用程序内存使用的方式,防止它们使用过多的内存导致性能问题或崩溃。通过设置GOMEMLIMIT变量,用户可以确保其 Go 程序在系统上平稳高效地运行,而不会对系统造成不必要的压力。
它并不替代GOGC,而是与之配合使用。您还可以禁用GOGC百分比配置,只使用GOMEMLIMIT来触发垃圾回收。
img
GOGC设为 100 和内存限制为 100MB
img
GOGC设为关闭(off)并且内存限制为 100。
在减少垃圾回收的运行量方面有明显的效果,但在应用此设置时需要小心。如果您不了解应用程序的极限,请不要将GOGC=off。
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% 兼容的替代品。
以下是一些性能基准:
chart with four columns, seven rows. first column is blank, ns/op, allocation bytes, allocation times std decode, 35510 ns/op, 1960 B/op, 99 allocs/op, easyjson decode, 8449 ns/op, 160 B/op, 4 allocs/op, jsoniter decode, 5623 ns/op, 160 B/op, 3 allocs/op, std encode, 2213 ns/op, 712 B/op, 5 allocs/op, easyjson encode, 883 ns/op, 576 B/op, 3 allocs/op, jsoniter encode, 837 ns/op, 384 B/op, 4 allocs/op
将其替换为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)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 实例,从而大大减少了堆使用,并因此减少了系统的垃圾回收次数。这个改进大约将我们应用程序的 CPU 使用率降低了约 40%,内存使用率降低了约 22%。
领取专属 10元无门槛券
私享最新 技术干货