优化您的 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% 的响应时间。
2. 对结构体字段进行排序
结构体中字段的顺序直接影响您的内存使用情况。
例如:
type testStruct struct {testBool1 bool // 1 bytetestFloat1 float64 // 8 bytestestBool2 bool // 1 bytetestFloat2 float64 // 8 bytes
}
您可能会认为这个结构体将占用 18 字节,但实际上不会。
func main() {a := testStruct{}fmt.Println(unsafe.Sizeof(a)) // 32 bytes
}
这是因为在 64 位架构中内部内存对齐的工作方式。有关更多信息,您可以阅读这篇文章。
我们如何降低内存使用?我们可以根据内存填充来对字段进行排序。
type testStruct struct {testFloat1 float64 // 8 bytestestFloat2 float64 // 8 bytestestBool1 bool // 1 bytetestBool2 bool // 1 byte
}func main() {a := testStruct{}fmt.Println(unsafe.Sizeof(a)) // 24 bytes
}
我们并不总是需要手动排序这些字段。您可以使用诸如 fieldalignment
等工具来自动对结构体进行排序。
fieldalignment -fix ./...
3. 垃圾回收调优
在 Go 1.19 之前,我们只能使用 GOGC(runtime/debug.SetGCPercent)
来配置垃圾回收周期;然而,在某些情况下,我们可能会超出内存限制。随着 Go 1.19 的到来,我们现在拥有了 GOMEMLIMIT
。GOMEMLIMIT
是一个新的环境变量,允许用户限制 Go 进程可以使用的内存量。这个功能提供了更好的控制 Go 应用程序内存使用的方式,防止它们使用过多的内存导致性能问题或崩溃。通过设置 GOMEMLIMIT
变量,用户可以确保其 Go 程序在系统上平稳高效地运行,而不会对系统造成不必要的压力。
它并不替代 GOGC
,而是与之配合使用。您还可以禁用 GOGC
百分比配置,只使用 GOMEMLIMIT
来触发垃圾回收。
GOGC
设为 100 和内存限制为 100MB
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% 兼容的替代品。
以下是一些性能基准:
将其替换为 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%。
希望对您有所帮助,欢迎提供任何反馈。谢谢。