记一次golang项目context引发的OOM故障
之前写过一篇一种基于etcd实践节点自动故障转移的思路, 程序经历过一次线上进程内存持续上涨终OOOM的小事故, 本次技术复盘导致本次内存泄露的完整起因。
提炼代码:
业务函数etcdWatchLoop: 基于etcd的Watch机制持续监听/foo前缀键值对的变更; 收到Watch信道的变更消息,就去查询当前键值对。
func etcdWatchLoop() error {
ctx, cancle := context.WithTimeout(context.Background(), time.Second*5)
defer cancle()
wchan := eClient.Watch(ctx, "/foo", clientv3.WithPrefix())
var tick = time.NewTicker(time.Minute * 1)
defer tick.Stop()
for {
select {
case <-tick.C: // 1min 探测一次,防止假死
fmt.Println("watch tick")
case resp := <-wchan:
fmt.Printf("watch result: %v \n", resp)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
if r, err := eClient.Get(ctx, "/foo"); err != nil {
fmt.Println(err)
} else {
// todo logic
}
}
}
}
程序日志显示: 程序进入死循环。
watch result: {{0 0 0 0 {} [] 0} [] 0 false false <nil> }
watch result: {{0 0 0 0 {} [] 0} [] 0 false false <nil> }
watch result: {{0 0 0 0 {} [] 0} [] 0 false false <nil> }
.....
当时etcd底层正在压缩或者发生网络问题,watch方法产生的信道resp := <-wchan被cancle了,信道被关闭,程序进入了死循环。
故障产生的第一点: 没有关注到从closed的信道中能持续读取到零值,导致进入无限循环。
无限循环(持续发送到etcd的get请求) 导致了OOM, 那具体是哪块内存泄露呢,高频grpc请求还是其他?
事后重现的示例进程。
ps -p <PID> -o etime=显示程序执行了20:33:12, 内存从7M上涨到184M,持续进行中。
执行go tool pprof -http=:8090 http://localhost:6060/debug/pprof/heap) 显示调用grpc请求时与context相关的2处堆内存占用较大且持续增长。
故障点二: 代码中的defer cancel()函数并不会执行,因为是无限循环,函数不会返回,defer压栈的cancel函数无法出栈执行。
godoc:
Calling the CancelFunc cancels the child and its children, removes the parent's reference to the child, and stops any associated timers. Failing to call the CancelFunc leaks the child and its children until the parent is "canceled" or the "timer fires"
修复代码如下:
func etcdWatchLoop1() error {
ctx, cancle := context.WithTimeout(context.Background(), time.Second*5)
defer cancle()
wchan := eClient.Watch(ctx, "/foo", clientv3.WithPrefix()) // 使用超时机制模拟 信道关闭
var tick = time.NewTicker(time.Minute * 1)
defer tick.Stop()
for {
select {
case <-tick.C:
fmt.Println("watch tick")
case resp, ok := <-wchan: // 从cancled信道或者超时信道中,信道会关闭,从closed信道会读取到零值,导致死循环
if ok {
fmt.Printf("watch result: %v \n", resp)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
fmt.Printf("watch ptr: %p, %p \n", &ctx, cancel)
if _, err := eClient.Get(ctx, "/foo"); err != nil {
fmt.Println(err)
} else {
// todo logic
}
cancel()
} else {
wchan = eClient.Watch(ctx, "/foo", clientv3.WithPrefix())
}
}
}
}
- 利用读信道的参数2, 来判断信道是否关闭,如果关闭了,重新初始化监听信道。
- context.WithTimeout 产生的cancel,在业务逻辑结束后迅速主动执行。
在本例中, 与context相关的内存泄露有两处,且有关联。
<1> 业务函数context.WithTimeout无限循环,未能执行cancel(), 导致高频产生的timerCtx堆内存迟迟无法释放。
<2> grpc请求底层源码以第一处产生的timerCtx为父级, 产生的子级cancelCtx接收父级取消传播,此处为父级timerCtx填充了取消信道。
第<1>处:未能调用cancel 导致的内存泄露。
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
产生了timerCtx对象, 因函数返回逃逸到堆上(由栈区返回值ctx引用)。
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{ // withTmeout实际产生了timerCtx对象
deadline: d,
}
c.cancelCtx.propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() { // 异步启动goroutine执行定时器触发逻辑
c.cancel(true, DeadlineExceeded, cause)
})
}
return c, func() { c.cancel(true, Canceled, nil) }
}
context.WithTimeout返回的cancel函数和timer触发函数做了相同的动作:
- 形成了闭包,捕获了timerCtx对象
- 与父级context解绑, 停止timer资源
区别在于释放的时机: 定时器触发函数捕获的timerCtx,要在定时器触发之后才能释放,也就是说timerCtx堆内存被硬生生持有了timeout=10s(连带上timerCtx附加的timer资源)。
于是在本例中, 理想情况下, 高频产生的timerCtx虽然在10s之后被GC清理,但是架不住无限循环导致的随地分配啊。
有如下简化实验:
for {
context.WithTimeout(context.Background(), time.Second*10)
}
GODEBUG = gotrace=1 ./sample 执行程序并打印gc日志:
有关gotrace=1 的输出解释,godoc https://pkg.go.dev/runtime 有详细介绍。
#->#-># MB heap size at GC mark start, at GC Mark end, and live heap
当第三列值持续上升,说明发生了内存泄露 (每次GC之后 live heap在持续上升)。
第<2>处的内存泄露:
在grpc一元请求堆栈函数newClientStreamWithParams内会产生子context: cancelCtx, 也会逃逸到堆上(由另一个栈区变量ctx引用)。
WithCancel returns a copy of parent with a new Done channel. The returned
context's Done channel is closed when the returned cancel function is called
or when the parent context's Done channel is closed, whichever happens first.
newClientStreamWithParams
--- ctx, cancel = context.WithCancel(ctx)
--- defer func() {
if err != nil {
cancel()
}
}()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}
根据火焰图,此处产生内存泄露的地方是 propagateCancel函数:
设置接受父级的取消传播, 此处是通过懒加载的方式为父级timerCtx填充取消信道,
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
c.Context = parent
done := parent.Done() // 此函数为timerCtx填充信道, 懒加载
if done == nil {
return // parent is never canceled
}
}
......
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
总结
本文复盘了golang项目生产环境某次OOM的现场,记录了本人未能强化的golang的知识点。
从closed信道能持续读取零值
defer 函数压栈,在函数返回之前出栈。
在业务逻辑结束后尽早 执行cancel() 解绑子级关系和释放timer资源,避免内存泄露。
强化了pprof的使用方式、理解了火焰图的指标意义
GODEBUG=gotrace=1 输出了gc日志,观察每次gc的堆内存变动。
https://go.dev/src/context/context.go?s=9162:9288
记一次golang项目context引发的OOM故障的更多相关文章
- Golang项目目录结构组织
其实golang的工程管理还是挺简单的,完全使用目录结构还有package名来推导工程结构和构建顺序. 当然,首先要说的是环境变量$GOPATH,项目构建全靠它.这么说吧,想要构建一个项目,就要将这个 ...
- golang项目中使用条件编译
golang项目中使用条件编译 C语言中的条件编译 golang中没有类似C语言中条件编译的写法,比如在C代码中可以使用如下语法做一些条件编译,结合宏定义来使用可以实现诸如按需编译release和de ...
- golang中Context的使用场景
golang中Context的使用场景 context在Go1.7之后就进入标准库中了.它主要的用处如果用一句话来说,是在于控制goroutine的生命周期.当一个计算任务被goroutine承接了之 ...
- 记一次SSM项目小结(一)
记一次SSM项目小结(一) ssm框架 环境配置 服务器配置 解决方法 拦截器重定向到localhost nginx和tomcat中session失效 mybatis的xml文件不生效 数据库用户创 ...
- Emacs中多个golang项目的配置方法
概述 最近使用golang开发项目时, 发现有时需要同时进行多个golang项目. 在这种情况下, 如果把所有的项目都放在 GOPATH 之下, 不仅管理麻烦(因为各个项目需要提交到不同的代码库), ...
- 关于go get安装git golang项目时报错的处理办法
关于go get安装git golang项目时报错的处理办法 使用go get安装github上的项目时一般来说,不可避免会出错.各种错误的处理办法: 必须条件: 1.安装git并配置环境变量.下载地 ...
- Golang项目的测试实践
Golang项目的测试实践 最近有一个项目,链路涉及了4个服务.最核心的是一个配时服务.要如何对这个项目进行测试,保证输出质量,是最近思考和实践的重点.这篇就说下最近这个实践的过程总结. 测试金字塔 ...
- 性能测试——记XX银行保全项目性能问题分析优化
记XX银行保全项目性能问题分析优化 数据库问题也许是大部分性能问题的关注点,但是JAVA应用与数据库交互的关节,JDBC 就像是我们人体的上半身跟下半身的腰椎,支持上半身,协调下半身运动的重要支撑点. ...
- Golang的Context介绍及其源码分析
简介 在Go服务中,对于每个请求,都会起一个协程去处理.在处理协程中,也会起很多协程去访问资源,比如数据库,比如RPC,这些协程还需要访问请求维度的一些信息比如说请求方的身份,授权信息等等.当一个请求 ...
- 深入理解golang:Context
一.背景 在golang中,最主要的一个概念就是并发协程 goroutine,它只需用一个关键字 go 就可以开起一个协程,并运行. 一个单独的 goroutine运行,倒也没什么问题.如果是一个go ...
随机推荐
- Python 调整Excel行高、列宽
在Excel中,默认的行高和列宽可能不足以完全显示某些单元格中的内容,特别是当内容较长时.通过调整行高和列宽,可以确保所有数据都能完整显示,避免内容被截断.合理的行高和列宽可以使表格看起来更加整洁和专 ...
- 使用ProWindow时,控制按钮状态的说明
在Pro SDK中,提供了一个默认的窗口基类,ProWindow Class,提供了基础的窗体样式,可供扩展和调用. 有网友问我,在使用时,会发现窗体右上角的控制按钮,有时会没有按照自己的预期显示. ...
- Vue CLI中views和components文件夹的区别
首先,src/components和文件夹src/views都包含Vue组件. 关键区别在于某些Vue组件充当路由视图. 在Vue中(通常是Vue Router)处理路由时,将定义路由以切换组件中使用 ...
- COSBrowser 文件夹分享——多端文件实时共享
您还在为临时分享某个文件夹而烦恼吗? 您是否对授权的复杂度感到震惊? 关于存储桶 Policy 权限,您是否很迷茫,不知如何设置? 不用着急,用 COSBrowser 开始文件夹分享,一键简化分享 ...
- 【前端】【Vue】Vue3自适应瀑布流解决方案
Vue3自适应瀑布流解决方案 效果如上图所示. 说明:Vue3.[vue-masonry插件](vue-masonry - npm (npmjs.com)) 建议查看官方文档vue-masonry ( ...
- Mysql数据库连接失败SSLException: Unsupported record version Unknown-0.0
问题描述:mysql版本:5.7.27jdk版本:1.8.0_201 tomcat日志中报错,显示连接数据库失败,报错信息如下: The last packet successfully receiv ...
- 开源即时通讯IM框架MobileIMSDK的鸿蒙NEXT端开发快速入门
相关链接: ① MobileIMSDK-鸿蒙端的详细介绍 ② MobileIMSDK-鸿蒙端的开发手册new(* 精编PDF版) 一.理论知识准备 您需要对鸿蒙Next和ArkTS开发有所了解: 1 ...
- Verilog6_串行通信协议
一.通信协议概述 1.串行通信与并行通信 串行通信(serial communication):数据通过单根数据线一位一位地传输:成本低但速度慢:适用于远距离传输,用于计算机与外设之间,如UART.\ ...
- Swagger UI、RESTful简介
Swagger UI 简介 Swagger UI允许任何人(无论您是开发团队还是最终用户)都可以可视化API资源并与之交互,而无需任何实现逻辑.它是根据您的OpenAPI(以前称为Swagger)规范 ...
- 「V 曲收集」幸运
标题原为"絮语",但终究是别人的絮语.不过是在他们的演唱下,我有这"幸运"拾起它们吧. 啊,还是加一个维护日志好了. [2022/08/31] 开启日志 ...