线上golang grpc服务资源泄露问题排查

 更新时间:2023年12月28日 08:56:20   作者:liyunlong41  
这篇文章主要介绍了线上golang grpc服务资源泄露问题排查,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

前几天告警群里报出一个go服务grpc接口出现很多超时现象,排查发现是服务有内存泄露与cpu占用高的问题,在这里将排查的过程记录一下,给大家提供排查问题的方向与思路,同时借鉴教训,优化自己服务代码。

发现超时现象后,登录机器看了下top,该服务总共有两台机器,发现02机器的cpu与内存占用很高(如下图第一个进程),而01机器都很低。

正常情况下不会有这么高的资源占用,可能是服务有资源泄露的问题,资源一致得不到释放。

首先做的,是重启服务,优先解决问题,资源泄露的问题可以通过重启来快速解决,重启后接口超时现象不再出现,接口耗时恢复正常。

重启后,开始排查问题,超时的服务是B服务,上游有A服务调用B服务,从A服务中找了几个超时的请求,根据opentracing生成的tracer_id查询日志,发现A服务调用B服务超时5s就返回错误了,B服务收到了A服务的请求,发现有两种情况,一种是B立即收到了A的请求,但是处理了400+秒才返回;另一种是A发出请求400+秒后,B才开始处理请求。

另外发现grpc请求全部打到一台机器上,另一台机器没什么量。

然后去看了下历史cpu、内存曲线,发现cpu在15分钟内上升至很高,同时内存占用很高的现象。

拉长了内存统计时间,发现A服务的内存在缓慢增长,肯定是有内存泄露的问题:

总结下观察到的问题:

  • 请求为什么全部打到一台机器上,两台机器的前面是有slb的,难道slb没有发挥作用吗?
  • cpu占用在15分钟快速增长的原因是什么?
  • 请求处理时间慢的原因是什么?
  • 为什么会有内存泄露现象出现?

Q1 slb没有负载均衡

原因

针对Q1,特意去查了下slb的配置,由于slb是根据权重轮询的,可能权重配置错误导致的请求分配不均,但是看了配置,slb的配置并没有问题,两台后端服务器的权重相同。

然后去查了下,发现我们的slb是4层(tcp/udp层)的负载均衡,4层的slb是针对连接做负载均衡的,而不是针对请求,当连接的客户端很少时,负载也可能不均衡,4层的负载均衡是客户端和服务器tcp直连。

然后去两台机器上netstat看了下,发现01机器没有A服务的连接,02机器有A服务的连接。同时grpc维持的是长连接并且复用连接,对于新请求不会新建连接,这样在第一次经过slb的负载均衡创建连接后,grpc的请求会复用这个连接,请求会全部打到连接的机器上。

如何解决?

知道原因了,那么如何解决呢?可以通过etcd与grpc两者结合来实现服务注册与服务发现,grpc客户端根据所有server的ip来实现负载均衡。

Q2 cpu快速增长

原因

对于Q2,由于当时服务没有设置pprof,无法看到运行的状况……后面加了pprof,又不能马上复现,所以暂时是通过看代码的方式来猜测哪些地方可能出了问题- -

想到之前的请求处理了400+秒,并且当时内存占用很高,代码中又有worker类的任务,每秒从数据库中取出数据,对每条数据启动一个goroutine处理。

伪代码如下:

for {
	datas := Mysql.GetDatas()
	for _, v := range datas {
		cur := v
		go func() {
			handle(cur)
		}
	}
	time.Sleep(time.Second)
}

正常情况下,这是没问题的,但是当时机器的内存占用接近100%,那么goroutine的处理时间肯定变长,如果处理时间超过1秒甚至远超一秒,那么这个goroutine还没处理完,worker又新起了一个goroutine,goroutine的数量没有控制,多了以后又占用更多资源,旧的goroutine处理时间更长,worker还是每秒启动一个新的goroutine……后面就产生了goroutine泄露,这可能是导致cpu增长的主要原因。

所以初步猜测是内存泄露问题,导致内存占用很高,然后导致goroutine处理时间过长,又导致goroutine泄露,goroutine进一步导致cpu、内存增长。

后来在线上加了pprof,但是内存泄露比较缓慢,需要等一段时间才能捕获,到时候在这里补充。

如何解决?

对于goroutine泄露的解决,自然是控制goroutine的数量,我把伪代码改成了如下来控制goroutine(判断超过限制数量就sleep):

int32 runningG = 0
const maxRunningG = 200
for {
	if atomic.LoadInt32(&runningG) > maxRunningG {
		time.Sleep(time.Seconds * 3)
		continue
	}
	datas := Mysql.GetDatas()
	for _, v := range datas {
		cur := v
		atomic.AddInt32(&runningG, 1)
		go func() {
			handle(cur)
			atomic.AddInt32(&runningG, -1)
		}
	}
	time.Sleep(time.Second)
}

Q3 请求处理缓慢

原因

处理请求缓慢的原因可能是Q2 goroutine泄露问题导致的cpu占用过高,请求处理不过来了。

如何解决?

参考Q2解决方案

Q4 内存泄露问题

原因1

同样也是直接从代码的角度排查,借鉴了网上一些人的内存泄露经验,发现一个方法中对于http请求的处理方式可能有问题。

对于每个http请求,该方法每次都会新建一个http.Client与transport, 伪代码如下。

...
tr := &http.Transport{
   TLSClientConfig: &tls.Config{
      ... // 证书相关
   },
}
client := &http.Client{Transport: tr}
response, err := client.Post(url, contentType, body)
if err != nil {
   return
}
responseByte, err := ioutil.ReadAll(response.Body)
if err != nil {
   return
}
...

而通过http.Client与transport的注释我们可以看出这两个是可以复用的。

http.Client:

// The Client's Transport typically has internal state (cached TCP
// connections), so Clients should be reused instead of created as
// needed. Clients are safe for concurrent use by multiple goroutines.
//

http.Transport:

// Transports should be reused instead of created as needed.
// Transports are safe for concurrent use by multiple goroutines.

且该方法对于http.Response.Body没有调用**Close()**方法,这可能导致潜在的资源泄露。

如何解决?

创建全局的http Client和Transport并且设置好超时时间等参数,复用这个client。http请求返回的response需要调用http.Response.Body.Close()释放连接使其可以被其他协程复用。

原因2

同时发现对于mysql查询结果的处理,也可能有些问题,伪代码如下:

rows, err := db.Query(sql, case)
if err != nil {
	return
}
for rows.Next() {
	if err = rows.Scan(...); err != nil{
		return
	}
	...
}

对于sql.Query的结果rows,如果没有rows.Close(),但是rows.Scan()读取了所有的数据,那么rows的资源会自动得到释放;

但是如果Scan发生错误,rows没有读取完,又没有rows.Close(),就可能导致潜在的内存泄露。

如何解决?

每次都调用rows.Close()方法来释放资源。

rows, err := db.Query(sql, case)
if err != nil {
	return
}
defer rows.Close() //
for rows.Next() {
	if err = rows.Scan(...); err != nil{
		return
	}
	...
}

其他原因

通过pprof正在分析中…… 待补充。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 解析Go 标准库 http.FileServer 实现静态文件服务

    解析Go 标准库 http.FileServer 实现静态文件服务

    http.FileServer 方法属于标准库 net/http,返回一个使用 FileSystem 接口 root 提供文件访问服务的 HTTP 处理器。下面通过本文给大家介绍Go 标准库 http.FileServer 实现静态文件服务的相关知识,感兴趣的朋友一起看看吧
    2018-08-08
  • 纯Go语言实现百万级数据高效导出Excel和CSV实战指南

    纯Go语言实现百万级数据高效导出Excel和CSV实战指南

    这篇文章主要为大家详细介绍了如何通过纯Go语言实现百万级数据高效导出Excel和CSV的相关知识,文中的示例代码讲解详细,需要的小伙伴可以了解下
    2025-09-09
  • Golang利用compress/flate包来压缩和解压数据

    Golang利用compress/flate包来压缩和解压数据

    在处理需要高效存储和快速传输的数据时,数据压缩成为了一项不可或缺的技术,Go语言的compress/flate包为我们提供了对DEFLATE压缩格式的原生支持,本文将深入探讨compress/flate包的使用方法,揭示如何利用它来压缩和解压数据,并提供实际的代码示例,需要的朋友可以参考下
    2024-08-08
  • Go语言极简入门之语法、并发及避坑全攻略

    Go语言极简入门之语法、并发及避坑全攻略

    Go是由谷歌支持的开源编程语言,属于编译型语言,对并发编程有较好的支持,这篇文章主要介绍了Go语言极简入门之语法、并发及避坑的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2026-06-06
  • Golang中基于HTTP协议的网络服务

    Golang中基于HTTP协议的网络服务

    HTTP协议是基于TCP/IP协议栈的,并且它也是一个面向普通文本的协议。这篇文章主要详细介绍了Golang中基于HTTP协议的网络服务,感兴趣的小伙伴可以借鉴一下
    2023-04-04
  • Go接口的用法详解

    Go接口的用法详解

    本文主要介绍了Go接口的用法详解,包括定义接口、实现接口、使用接口、空接口等,通过接口,可以实现多态性,即一个对象可以实现多个接口,从而实现不同接口的行为,感兴趣的可以了解一下
    2023-11-11
  • golang debug调试的实现

    golang debug调试的实现

    本文主要介绍了使用Go语言进行本地调试和远程调试,文中通过图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-12-12
  • linux中用shell快速安装配置Go语言的开发环境

    linux中用shell快速安装配置Go语言的开发环境

    相信每位开发者都知道选择一门开发语言,免不了需要安装配置开发环境,所以这篇文章给大家分享了linux下使用shell一键安装配置GO语言开发环境的方法,有需要的朋友们可以参考借鉴,下面来一起看看吧。
    2016-10-10
  • GoLang的sync.WaitGroup与sync.Once简单使用讲解

    GoLang的sync.WaitGroup与sync.Once简单使用讲解

    sync.WaitGroup类型,它比通道更加适合实现这种一对多的goroutine协作流程。WaitGroup是开箱即用的,也是并发安全的。同时,与之前提到的同步工具一样,它一旦被真正的使用就不能被复制了
    2023-01-01
  • golang操作mongodb的方法

    golang操作mongodb的方法

    这篇文章主要介绍了golang操作mongodb的方法,涉及Go语言操作mongodb的连接、读取及显示的方法,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02

最新评论