Go服务器性能调优:避坑选型,QPS翻倍实战
|
老张昨天在工位上拍桌子,说「这接口又超时了!」——其实监控大盘上那条绿色的线一直在抖,我盯着看了三分钟才敢确认是QPS涨了,不是系统要崩。后来查日志,发现是某个灰度环境的限流阈值没调,新上线的查询接口把旧接口的流量全吃光了。这事儿最离谱的是,我原以为只是简单的缓存击穿,结果翻堆栈发现是某个定时任务在偷偷扫全表,扫的还是加了索引但数据倾斜的字段,扫完还把脏数据写回了缓存。 你说Go的性能调优难吗?其实就那几个坑——goroutine泄漏、channel阻塞、内存分配、GC压力。但难的是怎么在业务代码里把这些坑全避开。记不清是哪次了,我们有个服务QPS卡在3000上不去,排查了两天发现是某个接口用了`sync.Map`,而它的读写比例是1:100,直接换成`map[string]struct{}`加`sync.RWMutex`,QPS直接飙到6000。后来复盘的时候,前端小妹还吐槽:「你们后端是不是偷偷换了服务器?」——她哪知道,我们连`runtime.GOMAXPROCS`都没调,就改了三行代码。 咖啡机坏了那天,我正盯着pprof看火焰图,突然闻到一股焦味——隔壁工位的老王把机箱盖打开了,风扇转得跟直升机似的。他说「这机器嗡嗡响了一上午」,我凑过去一看,CPU温度85℃,内存占用98%,而日志还在疯狂刷「malloc cache miss」。我原以为是他开了什么吃内存的监控工具,结果一查,是某个定时任务没做幂等,每分钟都在重复执行,每次执行都要加载10万条数据到内存里。后来我们把定时任务的频率从1分钟改成5分钟,又加了缓存,温度直接降到60℃,内存占用也掉到30%。 但最坑的还是选型。去年我们想用某个「高性能」的Go框架,宣传说QPS能到10万+,结果压测的时候发现,它的路由匹配是用的正则表达式,而我们的接口路径里有大量动态参数,比如`/user/{id}/order/{order_id}`,每次匹配都要重新编译正则,压到5000QPS就开始丢包。后来换回标准库的`http.ServeMux`,自己写了个简单的路由匹配,QPS反而冲到了8000。这事儿告诉我们,别信那些「高性能」的宣传,得自己测——就像老王说的:「框架再快,也快不过标准库。」
文章配图,仅供参考 说到压测,我想起上个月的一次事故。我们灰度了一个新版本,压测的时候QPS从8000掉到4000,排查了半天发现是某个接口的SQL没加索引——而这个SQL是三年前写的,当时数据量小,根本没考虑性能问题。现在数据量涨了100倍,全表扫描直接把数据库打挂了。后来我们连夜加了索引,又把慢查询日志的阈值从1秒调到100毫秒,结果发现还有十几个接口的SQL也需要优化。这事儿最讽刺的是,那个没加索引的字段,是产品经理三个月前要求加的「非必要字段」,说「以后可能有用」——结果现在真的「有用」了,但有用的是拖垮系统。外卖筷子掉在地上的时候,我正跟前端小妹争执。她说「你们后端能不能别老改接口字段?」——她哪知道,我们改字段是为了优化性能。比如之前有个接口返回的是`[]string`,后来发现客户端其实只需要`map[string]bool`,我们改成返回`map`后,传输数据量少了30%,接口耗时从200ms降到100ms。但前端小妹不理解,她说「你们改字段要改我的代码,还要重新测试,很麻烦的!」——我直接把压测报告甩给她:「你看,QPS翻倍了,你少测几个接口,值不值?」 运维老李最近总抱怨,说「你们后端老发版,我都快累死了」。其实我们也不想发版,但性能优化有时候就得靠发版。比如之前我们优化GC压力,把`new()`改成`sync.Pool`,结果发现`sync.Pool`的`Get()`和`Put()`也有开销,最后改成对象池+复用,GC停顿时间从50ms降到10ms。但这个改动得改代码,得发版——而发版就得跟运维撕逼,因为他们总担心「发版会出问题」。我原以为他们只是谨慎,结果有次偷听到老李打电话谈房租,才明白他其实是怕发版后加班,影响他晚上去健身。 工位隔断上贴的便签卷了边,我伸手去抚平,发现是去年写的「Go性能优化checklist」——第一条是「避免goroutine泄漏」,第二条是「减少内存分配」,第三条是「优化GC」。现在看,这些条目都没错,但真正坑人的往往是那些「没想到」的地方。比如我们之前有个服务,QPS卡在5000上不去,排查了半个月发现是`context.WithTimeout`的默认值设得太短,导致大量请求被提前终止,而终止的请求又占着连接不放,最后把连接池打满了。后来我们把超时时间从1秒改成5秒,QPS直接冲到1万——这事儿告诉我们,性能优化有时候就是「调个参数」这么简单,但你得先知道哪个参数该调。 现在回头看,Go的性能调优其实没什么秘诀,就是「优胜劣汰」——把慢的代码淘汰掉,把快的代码留下来。但难的在于,你得知道哪些代码是慢的,哪些是快的。比如我们之前用`fmt.Sprintf`拼接字符串,后来改成`strings.Builder`,QPS涨了20%;又比如我们之前用`json.Marshal`序列化,后来改成`easyjson`,QPS又涨了30%。这些优化看似简单,但你得先测出来,才能知道值不值得做。而测的时候,又得避开那些「假性能」——比如压测时把日志关了,结果上线后日志开起来,QPS又掉了一半。 最近我们在灰度一个新的优化方案,是把部分热点数据存到Redis里,用Lua脚本保证原子性。但测试的时候发现,Lua脚本的执行时间比预期长,导致QPS没涨反而降了。后来我们改成用`MULTI/EXEC`事务,执行时间从10ms降到2ms,QPS终于涨了。这事儿还没完全搞定,因为Redis的集群模式有点问题,有时候会跨节点执行事务,导致延迟变高。老王说「要不咱们换回单机版?」——我摇摇头:「单机版容量不够,还是得解决集群的问题。」他叹了口气:「行吧,我再看看。」 (编辑:站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |




