分享
  1. 首页
  2. 文章

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

John1314 · · 2559 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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

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

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

01 架构

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



基础架构


01 Problem:

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

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

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

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



这个 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,
 }
}复制代码
这种配置可以帮助我们减少调用另一个服务的最长时间。


阶段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
 }
}复制代码
第一阶段和第二阶段,在自动缩放成功的帮助下,减少这个 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,我们从这个经验中学到了很多,这种经验只有在真实的案例和真实的用户中才能获得。 我很高兴能参与修复这个漏洞

关注微信公众号




有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:John1314

查看原文:如何在 Golang API 中避免内存泄漏?

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
2559 次点击
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏