线上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正在分析中…… 待补充。

总结

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

相关文章

  • Golang AGScheduler动态持久化任务调度的强大库使用实例

    Golang AGScheduler动态持久化任务调度的强大库使用实例

    这篇文章主要为大家介绍了Golang AGScheduler动态持久化任务调度的强大库使用实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • 使用Go语言创建静态文件服务器问题

    使用Go语言创建静态文件服务器问题

    这篇文章主要介绍了使用Go语言创建静态文件服务器,本文通过试了代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • golang中为什么不存在三元运算符详解

    golang中为什么不存在三元运算符详解

    这篇文章主要给大家介绍了关于golang中为什么不存在三元运算符的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • docker中部署golang项目的步骤详解

    docker中部署golang项目的步骤详解

    这篇文章主要给大家介绍了关于在docker中部署golang项目的步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2017-11-11
  • go RWMutex的实现示例

    go RWMutex的实现示例

    本文主要来介绍读写锁的一种Go语言的实现方式RWMutex,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • Golang汇编之控制流深入分析讲解

    Golang汇编之控制流深入分析讲解

    这篇文章主要介绍了Golang汇编之控制流,程序执行的流程主要有顺序、分支和循环几种执行流程,本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序,或者说如何以汇编思维来编写Go语言代码,感兴趣的同学可以参考下文
    2023-05-05
  • Golang 断言与闭包使用解析

    Golang 断言与闭包使用解析

    这篇文章主要介绍了Golang 断言与闭包使用解析,Go中的断言用于判断变量的类型,更多相关内容需要的朋友可以参考一下
    2022-07-07
  • Go语言中slice作为参数传递时遇到的一些“坑”

    Go语言中slice作为参数传递时遇到的一些“坑”

    这篇文章主要给大家介绍了关于Go语言中slice作为参数传递时遇到的一些“坑”,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2018-03-03
  • Go语言string,int,int64 ,float之间类型转换方法

    Go语言string,int,int64 ,float之间类型转换方法

    Go语言中int类型和string类型都是属于基本数据类型,两种类型的转化都非常简单。下面通过本文给大家分享Go语言string,int,int64 ,float之间类型转换方法,感兴趣的朋友一起看看吧
    2017-07-07
  • Go语言字符串常见操作的使用汇总

    Go语言字符串常见操作的使用汇总

    这篇文章主要为大家总结了Go语言中常见的几种字符串操作,例如:位置索引、替换、统计次数等,文中的示例代码讲解详细,感兴趣的可以了解一下
    2022-04-04

最新评论