惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Proofpoint News Feed
Attack and Defense Labs
Attack and Defense Labs
Security Archives - TechRepublic
Security Archives - TechRepublic
Engineering at Meta
Engineering at Meta
WordPress大学
WordPress大学
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
F
Full Disclosure
云风的 BLOG
云风的 BLOG
爱范儿
爱范儿
V2EX - 技术
V2EX - 技术
B
Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
M
MIT News - Artificial intelligence
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
W
WeLiveSecurity
Stack Overflow Blog
Stack Overflow Blog
TaoSecurity Blog
TaoSecurity Blog
T
Threatpost
小众软件
小众软件
T
The Blog of Author Tim Ferriss
Google Online Security Blog
Google Online Security Blog
MongoDB | Blog
MongoDB | Blog
T
Tenable Blog
P
Privacy International News Feed
S
Security @ Cisco Blogs
H
Heimdal Security Blog
大猫的无限游戏
大猫的无限游戏
B
Blog RSS Feed
H
Help Net Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Cisco Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
P
Proofpoint News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
有赞技术团队
有赞技术团队
Application and Cybersecurity Blog
Application and Cybersecurity Blog
O
OpenAI News
Security Latest
Security Latest
S
Securelist
Cyberwarzone
Cyberwarzone
D
Docker
S
Schneier on Security
V
Vulnerabilities – Threatpost
The GitHub Blog
The GitHub Blog
P
Privacy & Cybersecurity Law Blog
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research

小松鼠的博客

记录一次线上k8s工作节点无法创建容器的问题排查思路与解决办法 从LastPass转向拥抱开源KeePass的心路历程 故障定位与 AI 结合前后端编码实践 FileBeat收集nginx-ingress-controller日志 K8s云原生环境下文件描述符占用过高查询思路 2024年最新关闭火绒安全工具的开机自启方法 Kubernetes任务调度实践-Go语言实现Job和CronJob对比分析 离线更新k8s环境下的trivy漏洞库方法 使用Go语言接入Choerodon实现基于OAuth2的统一身份认证登录 在Vue2中自定义Switch组件并实现父子组件双向数据绑定 关于docker jdk1.8镜像中的GB18030-2022标准支持及验证 Go框架gin中的session存储gin-contrib-sessions和go-session 关于修改node_module中的源码问题记录 docker-compose网络和内网服务IP冲突问题 慎用存储过程:一条语句引发的数据库存储100%占用 Spring Boot中4种文件下载方法的实现 避坑-不能将specific类型的gitlab-runner改变为share类型 Docker compose中的MySQL主从复制模式和percona-toolkit工具使用 在minio中开启https访问以及使用rclone备份minio桶 在多机Docker环境下部署Choerodon的解决方案 Prometheus中Monitor添加对SpringBoot Actuator的Basic认证 在Nginx的容器镜像中隐藏Nginx的Server响应头 K8s中的两种nginx-ingress-controller及其区别 两个docker工具:runlike和whaler Grafana中的邮件报警和截图插件grafana-image-enderer K8s中externalName-service和services-without-selectors maven配置文件settings.xml中的一些概念总结 K8s中flexvolume插件驱动的安装 K8s中的coredns无法解析svc问题排查 K8s中使用Ingress访问请求体过大问题解决 关于k8s中对于SpringBoot应用TCP类型的就绪探针不准确的问题发现 K8s中的环境变量与应用程序的对应关系与操作 SpringMVC4升级为SpringBoot2实战 在Vmware中Ubuntu22.04的vm-tools和网络问题 修改k8s节点主机名并重新加入集群 离线安装Grafana插件 Spring Data Jpa 中使用CriteriaBuilder动态拼接SQL 在SpringBoot项目配置Liquibase数据库版本管理 记录Vue中父子组件传值的实战应用 实现单例模式的8种方法 三种常用的生产者消费者模式实现 使用两个线程交替打印0-100的奇偶数 关于部署于JBoss5中的Spring应用获取项目真实部署路径的问题 获取下一个完全对称日 通过短信验证码验证修改密码的解决方案 在Win10中使用Win+R快速启动软件 使用RSA加解密时注意Cipher.getInstance(String var0,Provider var1)提供的Provider是否正确 在RestEasy2.x中解决接口重复提交问题 几道简单的CTF题目思路 重温Spring---Spring事务控制与基于XML和注解的配置方法 重温Spring---Spring AOP基于XML和注解的配置 重温Spring---AOP动态代理和Spring AOP及其基本原理 重温Spring---Spring IOC基于XML和注解的配置和比较 在Windows10中安装MySQL5.7 Zip版本及常用配置 重温Spring---使用Spring IOC解决程序耦合 策略模式与责任链模式实战应用 Linux上直接打开war包修改文件 在Windows上运行两个微信的简单脚本 ThreadPoolExecutor的使用方法与分页查询数据实例 IDEA中Shelve Changes 和 Git Stash 通过resteasy发布RESTful接口 解决前端请求后台接口,后台报错Can not deserialize instance of java.util.ArrayList out of START_OBJECT token 使用VBA脚本汇总Excel文档 使用Jenkins+GitLab实现自动部署vue项目 Kubernetes:使用hostPath挂载nginx集群的配置文件和html 彻底搞定VirtualBox虚拟机的网络设定 在Docker中安装MySQL5.7并开启远程访问(附授权和修改密码方式) 利用git命令和java文件流 获取自己改动过的文件 浅谈Spring定时任务的使用(Scheduled注解) 在Spring项目简单配置Flyway(V4.2版本)数据库版本管理 解决Spring单元测试中因外键关联导致的失败integrity constraint violation:foreign key no action Redis安装与哨兵模式配置入门 关于Vue中使用Element-UI样式row-class-name失效的问题 Element-UI中实现可动态增加行列和可编辑单元格的表格 Windows系统查看端口占用、结束进程方法和命令 层次分析法(AHP)分析步骤与计算方法 源码分析之解决layui框架重载表格时额外参数不清空的问题 Spring Data Jpa 返回自定义对象(实体部分属性、多表联查) 如何将一个jar放到本地maven仓库中 关于SSM项目停止Tomcat时Log4j出现java.lang.NoClassDefFoundError: 获取el-table单元格值并根据该值对元素自定义样式渲染 解决Git每次push都要重新输入账号密码和HttpRequestException encountered的问题 解决前后端分离项目中Vue不带cookies的问题 SSM集成Shiro自定义权限过滤器不执行解决方案 SSM集成Shiro不进入自定义Realm的doGetAuthorizationInfo的解决方案 Vue+SSM中使用Token验证登录 Git拉代码推送代码提示密码错误如何修改 Git配置SSH Key(Git配置多个账户) 安装Tomcat服务器以及错误汇总(tomcat8.0、jdk8) 关于我
记一次线上GoLang项目OOM排查过程
ycyin · 2024-12-05 · via 小松鼠的博客

在Go语言中使用了gin web框架,我们有一个接口是文件下载服务,用户下载超过100M文件就会OOM导致重启服务,本文主要记录通过Golang内置的pprof服务来排查OOM问题的过程。

问题现象

网站一功能包含文件下载功能,用户通过接口下载超过100M的文件就下载不全下载到30M~50M左右浏览器就显示下载完成。发现测试环境并没有问题,通过对比K8S部署文件,发现只有Memory Limit测试环境比较高,当时猜测Golang本身不像Java应该不是很消耗内存才对。

开始还怀疑是NGINX连接中断的问题,因为我们网站的前端和K8S网关(Ingress Controller)都使用了Nginx了,是哪个环节出问题了吗?通过添加一些timeout超时参数和使用NodePort直接访问后端服务确定就是后端服务问题。

因为测试环境代码迭代比生产快很多,难道是代码Bug生产上未修复?排查了一遍代码也没有看出啥问题,最后尝试在测试环境将内存降低也重现了这个问题,排除测试和生产代码不一致问题。

通过Prometheus监控也没看出内存泄漏问题。(最终判定应该是监控不灵敏未检测到程序就重启完成了,我们监控系统的采集周期是15s)

通过Kubectl describe 查看Pod状态发现确实有重启,Last Stated 就是OOMKilled。

于是尝试将生产服务的内存加大,确实能暂时缓解。于是开始了下面的排查流程。

代码定位,尝试修复失败

核心代码:

func DownloadPodLogs(c *gin.Context) {
	host := c.Query("host")
	if host == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "没有Host参数"})
		return
	}
	u, _ := url.ParseRequestURI(fmt.Sprintf("http://%s:%s/api/logs/download", host, daemonServicePort))
	data := url.Values{}
	data.Set("namespace", c.Param("namespace"))
	data.Set("app", c.Param("app"))
	data.Set("pod", c.Param("pod"))
	data.Set("path", c.Param("filename"))
	fmt.Println(data.Encode())
	u.RawQuery = data.Encode()
	res, err := http.Get(u.String())
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	defer res.Body.Close()

	c.Status(res.StatusCode)
	c.Header("Content-Type", "application/force-download")
	c.Header("Content-Disposition", fmt.Sprintf("attachment;filename*=utf-8''%s", c.Param("filename")))
	_, err = io.Copy(c.Writer, res.Body)

	if err != nil {
		log.Panic(err)
	}
	c.Writer.Flush()
}

其实这块代码并没有问题,根据io.Copy的实现,此Copy函数实现将src拷贝到dst,直到在src上读取EOF或错误发生。

func Copy(dst Writer, src Reader) (written int64, err error) {
	return copyBuffer(dst, src, nil)
}

copyBuffer代码中可以发现,buf==nil时,会申请一个3M的缓冲区,然后for循环从src中读取内容,每次读取完成后写入到dst。

// copyBuffer is the actual implementation of Copy and CopyBuffer.
// if buf is nil, one is allocated.
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
	// If the reader has a WriteTo method, use it to do the copy.
	// Avoids an allocation and a copy.
	if wt, ok := src.(WriterTo); ok {
		return wt.WriteTo(dst)
	}
	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
	if rt, ok := dst.(ReaderFrom); ok {
		return rt.ReadFrom(src)
	}
	if buf == nil {
		size := 32 * 1024
		if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
			if l.N < 1 {
				size = 1
			} else {
				size = int(l.N)
			}
		}
		buf = make([]byte, size)
	}
	for {
		nr, er := src.Read(buf)
		if nr > 0 {
			nw, ew := dst.Write(buf[0:nr])
			if nw < 0 || nr < nw {
				nw = 0
				if ew == nil {
					ew = errInvalidWrite
				}
			}
			written += int64(nw)
			if ew != nil {
				err = ew
				break
			}
			if nr != nw {
				err = ErrShortWrite
				break
			}
		}
		if er != nil {
			if er != EOF {
				err = er
			}
			break
		}
	}
	return written, err
}

所以这块应该是没有问题,但是我还是尝试修改了两个版本:
第一个版本:不用io.Copy改用类似Copy函数底层实现

	buf := make([]byte, 32*1024) // 32 KB buffer
	for {
		n, err := res.Body.Read(buf)
		if n > 0 {
			if _, writeErr := c.Writer.Write(buf[:n]); writeErr != nil {
				log.Printf("Write error: %v", writeErr)
				return
			}
			c.Writer.Flush() // 强制立即发送
		}
		if err != nil {
			if err != io.EOF {
				log.Printf("Read error: %v", err)
			}
			break
		}
	}

第二个版本:改用gin的Stream流

	buf := make([]byte, 32*1024) // 32 KB buffer
	c.Stream(func(w io.Writer) bool {
		n, err := res.Body.Read(buf)
		if n > 0 {
			_, writeErr := w.Write(buf[:n])
			if writeErr != nil {
				log.Printf("Write error: %v", writeErr)
				return false
			}
		}
		if err != nil {
			if err != io.EOF {
				log.Printf("Read error: %v", err)
			}
			return false
		}
		return n > 0
	})

以上三个版本的代码都能下载文件,但是都不能解决OOM问题。

问题依旧,只能依靠pprof来排查了。

pprof使用,找出真凶

pprof介绍

是什么:
pprof 是用于可视化和分析性能分析数据的工具

pprof 以 profile.proto 读取分析样本的集合,并生成报告以可视化并帮助分析数据(支持文本和图形报告)

profile.proto 是一个 Protocol Buffer v3 的描述文件,它描述了一组 callstack 和 symbolization 信息, 作用是表示统计分析的一组采样的调用栈,是很常见的 stacktrace 配置文件格式

支持什么使用模式:
Report generation:报告生成
Interactive terminal use:交互式终端使用
Web interface:Web 界面
可以做什么:
CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置
Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏
Block Profiling:阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置
Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况

代码注入pprof

在与main.go文件相同位置写一个pprof.go文件,在打包时带上tags(go build --tags pprof ....)程序启动后就可以通过http://127.0.0.1:6060/debug/pprof/端口直接查看运行时资源情况。参考:https://pkg.go.dev/runtime/pprof

//go:build pprof
// +build pprof

package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
)

func init() {
	go pprofServer()
}

func pprofServer() {
	ip := "0.0.0.0:6060"
	if err := http.ListenAndServe(ip, nil); err != nil {
		fmt.Printf("start pprof failed on %s\n", ip)
	}
}

通过Web界面分析

直接在浏览器访问http://127.0.0.1:6060/debug/pprof/ 界面
查看当前总览:

/debug/pprof/
profiles:
0    block
5    goroutine
3    heap
0    mutex
9    threadcreate
full goroutine stack dump

这个页面中有许多子页面,咱们继续深究下去,看看可以得到什么?

cpu(CPU Profiling): HOST/debug/pprof/profile,默认进行30s的CPUProfiling,得到一个分析用的profile文件block(BlockProfiling):HOST/debug/pprof/block,查看导致阻塞同步的堆栈跟踪
goroutine:$HOST/debug/pprof/goroutine,查看当前所有运行的 goroutines 堆栈跟踪
heap(Memory Profiling): HOST/debug/pprof/heap,查看活动对象的内存分配情况mutex(MutexProfiling):HOST/debug/pprof/mutex,查看导致互斥锁的竞争持有者的堆栈跟踪
threadcreate:$HOST/debug/pprof/threadcreate,查看创建新 OS 线程的堆栈跟踪

通过交互式终端使用

由于我们是像观察内存,所以重点看第一个

(1)go tool pprof http://localhost:6060/debug/pprof/heap?seconds=60

执行该命令后,需等待 60 秒(可调整 seconds 的值),pprof 会进行 内存记录。结束后将默认进入 pprof 的交互式命令模式,可以对分析的结果进行查看或导出。
比如使用top命令查看前10。
具体可执行 pprof help 查看命令说明

$ go tool pprof http://10.5.x.x:30596/debug/pprof/heap?seconds=60
Fetching profile over HTTP from http://10.5.x.x:30596/debug/pprof/heap?seconds=60
Saved profile in /home/ycyin/pprof/pprof.main.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
File: main
Type: inuse_space
Time: Dec 4, 2024 at 2:27pm (CST)
Duration: 60s, Total samples = 288.28MB 
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 280.73MB, 97.38% of 288.28MB total
Dropped 3 nodes (cum <= 1.44MB)
Showing top 10 nodes out of 26
      flat  flat%   sum%        cum   cum%
  284.26MB 98.60% 98.60%   284.26MB 98.60%  github.com/gin-contrib/cache.(*cachedWriter).Write
   -3.53MB  1.22% 97.38%    -3.53MB  1.22%  compress/flate.NewWriter
         0     0% 97.38%    -3.53MB  1.22%  bufio.(*Writer).Flush
         0     0% 97.38%    -3.53MB  1.22%  compress/gzip.(*Writer).Write
         0     0% 97.38%   284.76MB 98.78%  github.com/gin-contrib/cache.CachePage.func1
         0     0% 97.38%   281.23MB 97.55%  github.com/gin-gonic/gin.(*Context).Next (inline)
         0     0% 97.38%   284.26MB 98.60%  github.com/gin-gonic/gin.(*Context).Stream
         0     0% 97.38%   281.23MB 97.55%  github.com/gin-gonic/gin.(*Engine).ServeHTTP
         0     0% 97.38%   281.23MB 97.55%  github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
         0     0% 97.38%   281.23MB 97.55%  github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1

-inuse_space:分析应用程序的常驻内存占用情况
-alloc_objects:分析应用程序的内存临时分配情况

(2)go tool pprof http://localhost:6060/debug/pprof/profile

(3) go tool pprof http://localhost:6060/debug/pprof/block

(4) go tool pprof http://localhost:6060/debug/pprof/mutex

pprof可视化界面

上面通过命令行比较麻烦,可以使用pprof结合graphviz带来的可视化服务,是问题定位能够更加清晰。(首先需要下载安装graphviz

先通过Curl下载pprof文件到本地,然后进入web界面

$ curl -s http://172.26.29.58:30596/debug/pprof/heap?seconds=60 > heap.out

第一种方式使用go tool pprof heap.out然后输入web会打开浏览器展示调用图

$ go tool pprof heap.out
File: main
Type: inuse_space
Time: Dec 4, 2024 at 3:02pm (CST)
Duration: 60s, Total samples = 288.28MB 
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) web

第二种方式使用直接本地启动一个web服务

$ go tool pprof -http=:8080 cpu.prof
Serving web UI on http://localhost:8080

第二种方式可以以多个角度来分析:
VIEW:
Top:和top命令相同,将函数按资源使用进行排名

Graph:如图的函数调用逻辑图以及节点使用

Flame Graph:火焰图,资源使用按从大到小排列,点击可看详细信息

Peek:打印每个调用栈的信息

Source:显示具体函数的资源消耗信息,类似list命令

Disassemble:显示样本总量

SAMPLE:
如果是内存信息SAMOLE这一栏有四个选项

alloc_objects:已分配的对象总量(不管是否已释放)

alloc_space:已分配的内存总量(不管是否已释放)

inuse_objects:已分配但尚未释放的对象数量

inuse_sapce:已分配但尚未释放的内存数量

定位到问题

通过图形和命令行相结合,发现就是github.com/gin-contrib/cache.(*cachedWriter).Write这个地方使用了大量的内存,看到这个包名就恍然大悟了,我们在代码中使用了gin的一个缓存中间件:Cache gin's middleware,对每个接口进行了缓存,包括这个文件下载接口,所以每次请求下载文件就会缓存到内存中。
解决很简单,把这个接口的缓存删除就可以了。

总结

不要在文件下载接口添加缓存,防止OOM;相信自己的代码和分析,少走排查弯路;pprof很强大,以后Go程序性能分析可以用起来。

参考

  1. https://pkg.go.dev/runtime/pprof
  2. https://github.com/gin-contrib/cache
  3. https://www.jishuchi.com/read/gin-practice/3832
  4. https://blog.csdn.net/weixin_43710037/article/details/117718665
  5. https://blog.csdn.net/zhangkaixuan456/article/details/132775868