扫码订阅《 》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go学习
  • Go学院

    • Go小课
    • Go小考
    • Go实战
    • 精品课
  • Go宝典

    • 在线宝典
    • B站精选
    • 推荐图书
    • 精品博文
  • Go开源

    • Go仓库
    • Go月刊
  • Go下载

    • 视频资源
    • 文档资源
Go求职
  • 求职服务

    • 内推互助
    • 求职助力
  • 求职刷题

    • 企业题库
    • 面试宝典
    • 求职面经
Go友会
  • 城市
  • 校园
推广返利 🤑
实验区
  • Go周边
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
author-avatar

GOLANG ROADMAP


首页
Go学习
  • Go学院

    • Go小课
    • Go小考
    • Go实战
    • 精品课
  • Go宝典

    • 在线宝典
    • B站精选
    • 推荐图书
    • 精品博文
  • Go开源

    • Go仓库
    • Go月刊
  • Go下载

    • 视频资源
    • 文档资源
Go求职
  • 求职服务

    • 内推互助
    • 求职助力
  • 求职刷题

    • 企业题库
    • 面试宝典
    • 求职面经
Go友会
  • 城市
  • 校园
推广返利 🤑
实验区
  • Go周边
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
  • 课程介绍

    • Go 专家编程
  • 常见数据结构实现原理

  • 常见控制结构实现原理

  • 协程

  • 内存管理

  • 并发控制

  • 反射机制

  • 定时器

    • 定时器
    • Timer
    • Ticker
    • timer
    • 案例
  • 语法糖

  • 编程陷阱

扫码订阅《 》或入驻星球,即可阅读文章!

案例


Hongcai Ren

前面我们讨论了如果定时器使用不当会有资源泄露的风险,使用时需要格外注意。

实际项目中发生Ticker资源泄露的场景有如下几种:

  1. 创建了Ticker,忘记在使用结束后Stop;
  2. 从别处拷贝代码未拷贝Stop语句;
  3. 开源或第三方库中发生泄露;

对于前两种,推荐创建Ticker后立即使用defer语句将Ticker停掉,比如类似下面的代码:

    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
1
2

使用defer是安全的,因为只有当函数退出时才会执行,上面两行代码甚至可以写到一行中。

经常使用开源或第三方库时难免会遇到资源泄露的问题,这时我们就需要掌握一些基本的定位手段, 接下来分享一个自己在做项目遇到的开源库资源泄露的案例。案例中使用的pprof工具,本处只做简单说明,详细的使用方法 请参考相关章节。

# 开源库资源泄露

前面我们研究了Ticker的实现原理,已经知道Ticker如果不主动停止会有资源泄露的问题。

本节介绍一个真实的案例,重点分析产生资源泄露的现象以及排查思路。

# 应用背景

曾经做过一个产品,不经意间出现了CPU使用率缓慢升高,最后CPU使用率竟然达到了100%,严重影响了业务。经过排查,问题出在Ticker的使用方式上,创建了Ticker,在使用结束后没有释放导致的。

该产品需要监控其他服务器的健康状态,其中很常见的一种做法是心跳检测。简单的说,周期性的ping这些服务器,能在指定时间内收到ack说明与该服务器之间的网络没问题。

当时使用了一个小众的开源组件tatsushid/go-fastping来做ping。 该组件介绍如下图所示:

# 问题现象

在做性能测试时,管理了1000台服务器,差不多4天后发现系统越来越慢,查看CPU使用情况,结果发现CPU使用率已经达到100%。

排查性能问题主要使用pprof,关于pprof的使用方法及原理介绍在请参照相关章节。

使用pprof查看CPU使用情况,主要是查看CPU都在忙什么:

从上图可以看出,CPU主要是被runtime包占用了,其中第二行runtime.siftdownTimer正是timerproc中的一个动作。

再使用pprof查看函数调用栈,主要看是哪些函数在使用CPU:

从上图可以看出,CPU主要是被ping模块占用,其中ping.(*Pinger).Run正是开源组件的一个接口。

经过pprof分析可以很清晰的指出问题出在go-fastping组件的Run()接口中,而且是与timer相关的。问题定位到这里,解决就很简单了。

此处,可以先总结一下Ticker资源泄露的现象:

  • CPU使用率持续升高
  • CPU使用率缓慢升高

# 源码分析

出问题的源码在ping.go的run()方法中。为叙述方便,对代码做了适当简化:

func (p *Pinger) run() {
    timeout := time.NewTicker(p.Timeout)    // 创建Ticker timeout
    interval := time.NewTicker(p.Interval)  // 创建Ticker

    for {
        select {
        case <-p.done:       // 正常退出,未关闭Ticker
            wg.Wait()
            return
        case <-timeout.C:    // 超时退出,未关闭Ticker
            close(p.done)
            wg.Wait()
            return
        case <-interval.C:
            if p.Count > 0 && p.PacketsSent >= p.Count {
                continue
            }
            err = p.sendICMP(conn)
            if err != nil {
                fmt.Println("FATAL: ", err.Error())
            }
        case r := <-recv:
            err := p.processPacket(r)
            if err != nil {
                fmt.Println("FATAL: ", err.Error())
            }
        }
        if p.Count > 0 && p.PacketsRecv >= p.Count {  // 退出,未关闭Ticker
            close(p.done)
            wg.Wait()
            return
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

该段代码可以看出,这个函数是有出口的,但在出口处没有关闭Ticker,导致资源泄露。

这个问题已经被修复了,可以看到修复后的局部代码如下:

    timeout := time.NewTicker(p.Timeout)
    defer timeout.Stop()  // 使用defer保证Ticker最后被关闭
    interval := time.NewTicker(p.Interval)
    defer interval.Stop() // 使用defer保证Ticker最后被关闭
1
2
3
4

# 总结

有一种情况使用Ticker不主动关闭也不会造成资源泄露,比如,函数创建Ticker后就不会退出,直到进程结束。这种情况下不会持续的创建Ticker,也就不会造成资源泄露。

但是,不管哪种情况,创建一个Ticker后,紧跟着使用defer语句关闭Ticker总是好的习惯。因为,有可能别人无意间拷贝了你的部分代码,而忽略了关闭Ticker的动作。

  • 开源库资源泄露
  • 应用背景
  • 问题现象
  • 源码分析
  • 总结