当前位置: 首页 > 科技观察

如何优化Go服务以将CPU使用率降低40%?

时间:2023-03-17 13:38:14 科技观察

Coralogix工程师通过优化Go服务成功地将CPU使用率降低了40%。10年前,Google遇到了C++编译时间长导致的严重瓶颈,他们需要一个新的解决方案。为了应对这一挑战,谷歌工程师创建了一种名为Go(又名Golang)的新编程语言。Go语言借鉴了C++的优点(如性能和安全特性),同时结合了Python的开发速度,使其能够快速使用多核实现并发计算。在Coralogix,我们解析客户日志,为他们提供相应的实时分析、警报、元数据等。为此,解析阶段必须非常快,但解析阶段非常复杂,需要为每一行日志服务大量规则。这是我们认为我们正在采用Golang的原因之一。这项新服务在我们的生产环境中全天候24/7运行。虽然结果很不错,但它仍然需要在高性能机器上运行。这个Go服务运行在AWSm4.2xlarge实例上,拥有8核CPU和36GB内存,每天解析超过数百亿条日志。在这个阶段,一切都在正常运行,我们可以凑合,但这不是我们的风格。我们希望使用更少的AWS实例来提供更多的功能,比如性能。为此,我们需要了解瓶颈的性质以及如何减少或完全消除瓶颈。1.问题分析我们决定对服务进行一些分析,检查是什么导致了高CPU消耗,看看我们是否可以做一些优化工作。升级版本首先,我们将GO升级到最新的稳定版本(软件生命周期中的关键步骤)。之前,我们使用的是1.12.4版,现在是1.13.8版。根据官方文档,1.13版本对运行时库和一些对内存使用有较大影响的组件进行了重大改进。无论如何,使用最新的稳定版本是有道理的,并且为我们节省了很多工作。https://golang.org/doc/devel/release.html因此内存消耗也从800MB左右优化到了180MB左右。分析开始接下来,为了更好地了解我们的工作流程并了解我们将时间和资源花在哪里,我们从分析开始。分析不同的服务和编程语言可能看起来非常复杂和令人生畏,但在Go中,它实际上非常简单,只需几条命令即可实现。Go有一个特殊的工具叫做“pprof”,可以在应用中通过监听路由(默认端口6060)来启用,使用Go包来管理HTTP连接import_"net/http/pprof"然后,在mainfunction或在路由包中启用以下操作:gofunc(){log.Println(http.ListenAndServe("localhost:6060",nil))}()现在我们可以启动服务并连接到Http://localhost:6060/debug/可以在此处找到pprof的完整Go文档。https://golang.org/pkg/net/http/pprofpprof默认配置为每30秒采样一次CPU使用率。我们可以调整一些配置来采样参数,例如CPU使用率和堆使用率。我们主要关心的是CPU使用率,因此在生产中我们每隔30秒进行一次性能采样,看看下图显示的内容(注意:这是在我们升级Go版本并在最小化组件后更改Go的内部结果之后):可以看到,我们发现了很多和runtimelibrary(runtimepackage)相关的activity,需要指出的是GC(garbagecollection):几乎29%的CPU都被GC占用了,而这只是top20个消耗最多的对象。由于Go的GC已经非常快并且经过了大量优化,因此最好不要更改或修改它。由于我们的内存消耗非常低(与之前的Go版本相比),主要问题变成了高对象分配率。如果是这种情况,我们可以做两件事:调整GoGC活动以适应我们的服务行为,即我们需要延迟GC的触发以降低运行频率。作为代价,我们将不得不消耗更多的内存。查找分配过多对象的函数、区域或代码行。查看实例类型,我们有很多空闲内存,CPU数量受机器类型限制。所以我们需要调整这个比例。从Golang的早期开始,就有一个被大多数开发者忽略的标志:GOGC。这个阈值的默认值为100,它的主要作用是告诉系统什么时候触发GC。当堆达到其初始大小的100%时,默认将触发GC过程。将默认值更改为更高的数字会延迟GC触发,相反会更快地触发GC。我们开始针对不同的值进行基准测试,最终发现我们在GOGC=2000时获得了最好的性能。这立即将我们的内存使用量从200MB增加到2.7GB(这是在我们更新Go版本以减少内存消耗之后)并将我们的CPU使用率降低了10%。下面的截图显示了这些基准测试的结果:Gogc=2000前4个CPU使用函数成为我们的服务函数,这是有道理的。总GC使用率现在约为13%,不到之前的一半。深入挖掘我们本可以就此打住,但我们决定继续调查分配这么多对象的位置和原因。很多时候分配对象是有充分理由的(例如,在流处理的情况下,我们为每条消息创建很多新对象,因为它与下一条消息无关,需要删除),但在某些情况下,有一种简单的方法可以优化并大大减少对象创建。首先,让我们运行与之前相同的命令,有一个小的变化,使用堆转储:http://localhost:6060/debug/pprof/heap要查询生成的文件,我们可以在代码文件目录中运行以下命令分析调试结果:gotoolpprof-allocobjects我们的屏幕截图如下所示:除了第三行外,一切似乎都合理。第三行是一个监控函数,它在每个Coralogix规则解析阶段结束时向我们的Promethes导出器输出报告。为了获取更多信息,我们运行以下命令:listeg:listreportRuleExecution然后,我们得到以下结果:这两个对WithLabelValues的调用实际上是Prometheus对指标的调用(我们让prod决定它是否真的需要它).此外,我们看到第一行创建了大量对象(函数分配的对象总数的10%)。进一步研究,我们发现这是将客户ID从int转换为string的过程。这个过程非常重要,但是鉴于数据库中的客户数量有限,我们不应该接受变量作为字符串来迎合Prometheus。因此,与其每次创建一个新字符串并在函数结束时将其丢弃(浪费分配时间和GC的更多工作),我们在对象初始化时定义一个映射,映射从1到100,000之间的所有数字和一个相应的“获取”操作。我们运行了一个新的分析会话来验证上述论点,事实证明它是正确的(我们可以看到这部分不再分配对象):这不是一个巨大的变化,但总的来说它为我们节省了另一个GC活动,更具体地说,大约是CPU使用率的1%。最终状态如下面的屏幕截图所示:2.最终结果内存使用:~1.3GB->~2.7GBCPU使用:~2.55平均值和~5.05峰值->~2.13平均值和~2.9峰值。Golang优化前的CPU使用率:Golang优化后的CPU使用率:总的来说,我们的提升主要体现在高峰时间段,每秒处理的日志数增加了。这意味着我们的基础设施不再需要针对异常值进行调整,并且变得更加稳定。3.总结通过分析Go解析服务,我们能够查明问题区域,更好地了解我们的服务,并决定在哪里(如果有的话)投入时间改进它。大多数性能分析工作最终都会根据用户使用情况调整阈值或配置,以实现更好的性能。