首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

提升您的 Go 应用性能的 6 种方法

优化您的 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%。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OWY5MXpuwVyLHundSJ1p3aeA0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券
http://www.vxiaotou.com