54

如何在 Golang API 中避免内存泄漏?

 4 years ago
source link: https://www.tuicool.com/articles/fYjAzuB
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

关注公众号:JongSunShine 获取更多资料

建议你在将Golang API投入生成之前阅读此文,此文是基于真实的线上问题修复经历,如有巧合,纯属踩雷!

几个星期前,在修复我们的主服务器的漏洞时,我们尝试了很多方法来调试和修复它,因为它已经投入生产几个星期了。 但是我们总是需要通过我们的自动缩放机制来缓解,使其看起来似乎一切正常。直到后来我们才明白,这是coding中出现了问题。

01      架构

我们在整个系统架构中使用了微服务模式。 有一个网关 API (我们称之为主 API )为我们的用户(移动和网络)提供 API。 它的角色类似于 API 网关,所以它的任务只处理来自用户的请求,然后调用所需的服务,并向用户构建响应。 此AP服务完全由 Golang 来编写。

qEbqYr7.png!web

基础架构

01     Problem:

我们已经为我们的主API挣扎了很长一段时间,这些 API 总是被关闭且总是长时间处于无法响应的状态,有时导致我们的 API 无法访问,服务也处于无法使用状态。

API监控仪表盘显示红色警报,老实说,当我们的 API监控仪表盘变成红色时,是一件非常非常危险的事情,会给我们的工程师带来压力、恐慌和崩溃。

我们的 CPU 和内存使用率也正在变得越来越高。 如果发生这种情况,我们只需无助的手动重新启动服务,然后等待它再次重新运行。

E7NZf2z.jpg!web

对于单个请求,我们的 API 响应时间可达86秒

MvAjQrJ.png!web

这个 bug 真的让我们很沮丧,因为我们没有任何关于这个 bug 的日志。 我们只知道响应时间很长。 Cpu 和内存使用量不断增加。 这就像一场噩梦。

阶段1:     使用定制的 http.Client

在开发这个服务时,我们真正学到的一件事是:“不要相信默认配置,切记”。我们使用一个内置的 http客户端,而不是使用默认的一个从 http 的包

client:=http.Client{} //default

我们根据需要添加一些配置。 因为我们需要重新连接,所以我们在参数中进行了一些配置,并控制了最大空闲可重用连接。

func main() {
   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,
   }
}复制代码
80db5f42d3c0adbaf6a7d0c34c708bae.html

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

阶段2:     避免未关闭响应主体的内存泄漏

我们从这个阶段学到的是: 如果我们想重用连接池到另一个服务,我们必须读取响应体并关闭它。

因为我们的主 API 只是调用另一个服务,我们犯了一个致命的错误。 我们的主 API 应该重用来自 http 的可用连接,所以无论发生什么,我们必须读取响应体,即使我们不需要它。 我们也必须关闭响应体。 这两种方法都用于避免服务器中的内存泄漏。

假如我们忘记在代码中关闭响应主体。 这些东西会给我们的生产带来巨大的灾难

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

func Func()error {
   req, err:= http.NewRequest("GET","http://example.com?q=one",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
   }
}复制代码
80db5f42d3c0adbaf6a7d0c34c708bae.html

第一阶段和第二阶段,在自动缩放成功的帮助下,减少这个 bug。 好吧,说实话,从去年2017年开始,这种事情连三个月都没有发生过。

阶段3:      golang的超时控制

经过几个月稳定运行,这个错误没有再次发生。但 在2018年1月的第一个星期,我们的一个服务被我们的主要 API 调用, 宕机了。 由于某些原因,它不能被访问。

因此,当我们的内容服务关闭时,我们的主 API 将再次启动。 Api 仪表盘再次变红,API 响应时间变得越来越慢。 我们的 CPU 和内存使用率非常高,即使使用自动缩放。

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

对于这种情况,我们很好奇,为什么会发生这种情况。 因为我们认为,我们已经在 http 中设置了超时截止时间。 所以正常来说这种情况,不可能再次发生。

在我们在代码中check潜在的问题时,我们发现了一些非常危险的代码。

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 调用。

超时控制

为了改进这一点,我们在channel采用了超时控制的方法。 因为使用上述样式代码(使用 WaitGroup 将等待所有进程完成) ,我们必须等待所有 API 调用完成,这样我们才能处理并将响应返回给用户。

这是我们最大的错误之一。 当我们的一个服务器死亡时,这段代码可能会造成巨大的灾难。 因为要等很长时间才能恢复dead服务。 当然,有了5K qps/s,这就是一场灾难。

第一次尝试的解决方案:

我们通过添加超时来修改它。 所以我们的用户不会等这么长时间,他们只会得到一个内部服务器错误。

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: 使用上下文的超时控制

在第三阶段之后,我们的问题仍然没有完全解决。 我们的主 API 仍然消耗高 CPU 和内存。

这是因为,即使我们已经将 Internal Server Error 返回给 我们的用户,但是我们的 goroutine 仍然存在。 我们想要的是,如果我们已经返回响应,那么所有的资源也会被清除,没有例外。

我们发现了一些有趣的功能,我们还没有意识到在golang中可以使用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;
}复制代码

因此,我们为代码中的每个 goroutine 调用使用上下文。 这可以帮助我们释放内存并取消 goroutine 调用。 此外,为了获得更好的控制性和可靠性,我们还将上下文传递给 HTTP 请求。

func ( u *usecase) getDataFromFacebook(ctx context.Context, id int64, param string) sampleChanel {

   req, err := http.NewRequest("GET", "https://facebook.com", 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,
      }
   }
}复制代码

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

经验教训:

1,不要在在生产中使用默认选项.

2,不要在在生产中使用默认选项. 如果您正在构建一个大的并发 api,千万不要使用默认选项

3,大量阅读,大量尝试,大量失败,大量收获

4,我们从这个经验中学到了很多,这种经验只有在真实的案例和真实的用户中才能获得。 我很高兴能参与修复这个漏洞

关注微信公众号

yMVZreZ.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK