您的位置 首页 golang

避免Golang API中的内存泄漏

在将Golang API投入生产之前,您必须阅读此内容。 根据我们在Kurio的真实故事,我们会为每个版本而苦苦挣扎,因为我们没有以正确的方式来做。

laptop on fire taken from google image search

几周前,我们在Kurio只是在主要服务中修复了我们怪异而未发现的错误。 我们尝试了许多调试和修复它的方法。 该错误与业务逻辑无关。 因为它已经投产了几个星期。 但是我们总是通过自动缩放机制来节省费用,因此就像运行良好一样。

直到后来,我们才弄清楚了,这是因为我们的代码做得不好。

架构

Fyi,我们在架构中使用了微服务模式。 我们有一个网关 API (我们称之为主API),可为用户(移动和网络)提供API。 由于其作用类似于API网关,因此它的任务仅处理来自用户的请求,然后调用所需的服务,并建立对用户的响应。 这个主要的API,完全用Golang编写。 选择golang的原因是另一个我不会在这里讲的故事。

如果绘制在图片中,我们的系统将更像这样。

Kurio architecture

问题

我们与主要API的斗争已经很长时间了,主要API一直崩溃,并且对我们的移动应用程序的响应很长,有时甚至导致无法访问我们的API。 我们的API仪表板监视器只是变成红色了-老实说,当我们的API仪表板监视器变成红色时,这是一件危险的事,并给我们带来压力,恐慌和疯狂,使工程师。

其他是,我们的CPU和内存使用率越来越高。 如果发生这种情况,我们只需手动重新启动它,然后等待它再次运行即可。

Our API response time up-to 86 seconds for a single request.

graph our API response time, and doing restart manually for safety.

这个错误确实使我们感到沮丧,因为我们没有任何日志专门说明此错误。 我们只是有这么长的响应时间。 CPU和内存使用率增加。 就像一场噩梦。

阶段1:使用自定义的http.Client

开发此服务时,我们了解到并且真正了解到的一件事是,不要相信默认配置。

我们使用自定义的http.Client,而不使用http包中的默认值,

 client:=http.Client{} //default  

我们根据需要添加一些配置。 因为我们需要重用连接,所以我们在传输和控制max-idle可重用连接中进行一些配置。

 keepAliveTimeout:= 600 * time.Second
timeout:= 2 * time.Second
defaultTransport := &http.Transport{
    Dial: (&net.Dialer{
                     KeepAlive: keepAliveTimeout,}
           ).Dial,
    MaxIdleConns: 100,
    MaxIdleConnsPerHost: 100,
}
client:= &http.Client{
           Transport: defaultTransport,
           Timeout:   timeout,
}  

此配置可以帮助我们减少用于调用另一个服务的最长时间。

阶段2:避免未公开回复的内容引起 内存泄漏

我们从该阶段中学到的是:如果要重用连接池到另一个服务,则必须读取响应主体并将其关闭。

因为我们的主要API只是调用另一个服务,所以我们犯了一个致命错误。 我们的主要API假设要重用来自http.Client的可用连接,因此无论发生什么情况,我们都必须阅读响应正文,即使我们不需要它。 而且,我们必须关闭响应主体。 两者都用于避免服务器中的内存泄漏。

我们忘记在代码中关闭响应主体。 这些事情可能给我们的生产造成巨大的灾难。

解决方案是:我们关闭响应主体并读取它,即使我们不需要数据也是如此。

 req, err:= http.NewRequest("GET","#34;,nil)
if err != nil {
  return err
}
resp, err:= client.Do(req)
//=================================================
// CLOSE THE RESPONSE BODY
//=================================================
if resp != nil {
    defer resp.Body.Close() // MUST CLOSED THIS 
}
if err != nil {
  return err
}
//=================================================
// READ THE BODY EVEN THE DATA IS NOT IMPORTANT
// THIS MUST TO DO, TO AVOID MEMORY LEAK WHEN REUSING HTTP 
// CONNECTION
//=================================================
_, err = io.Copy(ioutil.Discard, resp.Body) // WE READ THE BODY
if err != nil { 
   return err
}  

我们在这里阅读了一篇很棒的文章后,对此进行了修复: / 11/21 /调整http客户端库以进行负载测试/

第1阶段和第2阶段,并在自动扩展成功的帮助下减少了此错误。 好吧,老实说,自去年以来三个月都没有发生这种情况:2017年。

阶段3:Golang Channel 中的超时控制

在运行几个月后,此错误再次发生。 在2018年1月的第一周,主要API调用的一项服务让我们说:已关闭。 由于某些原因,无法访问它。

因此,当我们的内容服务关闭时,我们的主要API会再次触发。 API仪表板再次变红,API响应时间越来越长。 即使使用自动缩放,我们的CPU和内存使用率也会很高。

同样,我们试图再次找到根本问题。 好了,在重新运行内容服务之后,我们再次运行良好。

对于这种情况,我们很好奇,为什么会这样。 因为我们认为,我们已经在http.Client中设置了超时期限,所以在这种情况下,这永远不会发生。

在我们的代码中搜索潜在的问题,然后我们找到了一些危险的代码。

为了更简单,代码看起来更像这样* ps:此函数只是一个示例,但与我们的模式相似

 type sampleChannel struct{
  Data *Sample
  Err error
}

func (u *usecase) GetSample(id int64, someparam string, anotherParam string) ([]*Sample, error) {

	chanSample := make(chan sampleChannel, 3)
	wg := sync.WaitGroup{}

	wg.Add(1)
	go func() {
		defer wg.Done()
       		chanSample <- u.getDataFromGoogle(id, anotherParam) // just example of function
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
    		chanSample <- u.getDataFromFacebook(id, anotherParam)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
    		chanSample <- u.getDataFromTwitter(id,anotherParam)
	}()

	wg.Wait()
	close(chanSample)

	result := make([]*Sample, 0)

	for sampleItem := range chanSample {
		if sampleItem.Error != nil {
			logrus.Error(sampleItem.Err)
		}
		if sampleItem.Data == nil { 
			continue
		}
         result = append(result, sampleItem.Data)
	}


	return result
}  

如果我们看上面的代码,那没有错。 但是此函数是访问最多的函数,并且在我们的主要API中具有最重的调用。 因为此函数将执行3个具有巨大处理能力的API调用。

为了改善这一点,我们使用通道上的超时控制进行了新的处理。 因为使用上述样式代码(使用WaitGroup的代码将等待直到所有过程完成),我们必须等待所有API调用都必须完成,这样我们才能处理并将响应返回给用户。

这是我们的重大错误之一。 当我们的一项服务死亡时,此代码可能会造成巨大的灾难。 因为将要等待很长的时间才能恢复服务。 当然,使用5K通话,这是一场灾难。

首次尝试解决方案:

我们通过添加超时来对其进行修改。 因此,我们的用户不会等待那么长时间,他们只会收到内部服务器错误。

 func (u *usecase) GetSample(id int64, someparam string, anotherParam string) ([]*Sample, error) {
	chanSample := make(chan sampleChannel, 3)
	defer close(chanSample)
 	go func() {
       		chanSample <- u.getDataFromGoogle(id, anotherParam) // just example of function
	}()

 
	go func() {
    		chanSample <- u.getDataFromFacebook(id, anotherParam)
	}()

   
	go func() {
   		chanSample <- u.getDataFromTwitter(id,anotherParam)
	}()
 

	result := make([]*feed.Feed, 0)
	timeout := time.After(time.Second * 2)
		for loop := 0; loop < 3; loop++ {
			select {
			case sampleItem := <-chanSample:
				if sampleItem.Err != nil {
					logrus.Error(sampleItem.Err)
					continue
				}
				if feedItem.Data == nil {
					continue
				}
				 result = append(result,sampleItem.Data)
			case <-timeout:
			      err := fmt.Errorf("Timeout to get sample id: %d. ", id)
			      result = make([]*sample, 0)
			      return result, err
			}
		}

	return result, nil;
}  

阶段4:使用上下文进行超时控制

完成第3阶段后,我们的问题仍然没有完全解决。 我们的主要API仍然消耗大量CPU和内存。

发生这种情况是因为,即使我们已经将内部服务器错误返回给我们的用户,但goroutine仍然存在。 我们想要的是,如果我们已经返回了响应,那么所有资源也将被清除,没有异常,包括在后台运行的goroutine和API调用。

稍后阅读本文后:http://dahernan.github.io/2015/02/04/context-and-cancellation-of-goroutines/

我们在golang中发现了一些我们尚未意识到的有趣功能。 那是使用上下文来帮助取消例程。

而不是使用时间。在使用超时之后,我们转到context.Context。 有了这种新方法,我们的服务将更加可靠。

然后,我们通过向相关功能添加上下文来再次更改代码结构。

 func (u *usecase) GetSample(c context.Context, id int64, someparam string, anotherParam string) ([]*Sample, error) {
	
  if c== nil {
    c= context.Background()
  }
  
  ctx, cancel := context.WithTimeout(c, time.Second * 2)
  defer cancel()
  
  chanSample := make(chan sampleChannel, 3)
  defer close(chanSample)
 	go func() {
       		chanSample <- u.getDataFromGoogle(ctx, id, anotherParam) // just example of function
	}()

 
	go func() {
    		chanSample <- u.getDataFromFacebook(ctx, id, anotherParam)
	}()

   
	go func() {
   		chanSample <- u.getDataFromTwitter(ctx, id,anotherParam)
	}()
 

	result := make([]*feed.Feed, 0)
	for loop := 0; loop < 3; loop++ {
	  select {
		case sampleItem := <-chanSample:
			if sampleItem.Err != nil {
				continue
			}
			if feedItem.Data == nil {
				continue
			}
			 result = append(result,sampleItem.Data)
		  // ============================================================
		  // CATCH IF THE CONTEXT ALREADY EXCEEDED THE TIMEOUT
		  // FOR AVOID INCONSISTENT DATA, WE JUST SENT EMPTY ARRAY TO 
		  // USER AND ERROR MESSAGE
		  // ============================================================
      		case <-ctx.Done(): // To get the notify signal that the context already exceeded the timeout
		      err := fmt.Errorf("Timeout to get sample id: %d. ", id)
		      result = make([]*sample, 0)
		      return result, err
		}
	}

	return result, nil;
}  

因此,我们将Context用于代码中的每个goroutine调用。 这有助于我们释放内存并取消goroutine调用。

此外,为了获得更多控制和可靠性,我们还将上下文传递给我们的HTTP请求。

 func ( u *usecase) getDataFromFacebook(ctx context.Context, id int64, param string) sampleChanel{
 
  req,err := http.NewRequest("GET","#34;,nil)
  if err != nil {
    return sampleChannel{
      Err: err,
    }
  }
  // ============================================================
  // THEN WE PASS THE CONTEXT TO OUR REQUEST.
  // THIS FEATURE CAN BE USED FROM GO 1.7
  // ============================================================
  if ctx != nil {
    req = req.WithContext(ctx) // NOTICE THIS. WE ARE USING CONTEXT TO OUR HTTP CALL REQUEST
  }
  resp, err:= u.httpClient.Do(req)
  if err != nil {
    return sampleChannel{
      Err: err,
    }
  }
  body,err:= ioutils.ReadAll(resp.Body)
  if err!= nil {
    return sampleChannel{
      Err:err,
    }
    sample:= new(Sample)
    err:= json.Unmarshall(body,&sample)
    if err != nil {
      return sampleChannle{
        Err:err,
      }
    }
    return sampleChannel{
      Err:nil,
      Data:sample,
    }
  }  

通过所有这些设置和超时控制,我们的系统更加安全和可控。

学过的知识:

· 从未在生产中使用默认选项,也从未使用过默认选项。 如果您要构建较大的并发A,请不要使用默认选项。

· 阅读很多,尝试很多,失败很多,收获很多。我们从这种经验中学到了很多,这种经验只有在实际案例和实际用户中才能获得。 在修复此错误时,我很高兴能参与其中。

*最后更新时间:2018年1月18日:修复了一些错字

(本文翻译自Iman Tumorang的文章《Avoiding Memory Leak in Golang API》,参考:

文章来源:智云一二三科技

文章标题:避免Golang API中的内存泄漏

文章地址:https://www.zhihuclub.com/86485.shtml

关于作者: 智云科技

热门文章

发表评论

您的电子邮箱地址不会被公开。

网站地图