作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!
某服务上线后,运行一段程序崩溃,一开始以为是panic,为所有的go出来的协程都加上了recover()处理,仍然未找到崩溃原因。
更奇怪的是,在 aws 云中,程序崩溃后,其对应的容器一直无法拉起。
最后在 SRE 的帮助下找到了最终的崩溃信息:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Fri, 06 Sep 2024 18:14:51 +0800
Finished: Fri, 06 Sep 2024 18:15:48 +0800
继续观察崩溃时候的golang runtime 上报:
容器最大内存为 16 gb,可见确实是某种原因导致了内存分配太多。
为了找到程序崩溃的原因,开启了程序的 pprof http 端口:
wget -O heap_profile.out "http://[xxxxx]:18888/debug/pprof/heap"
scp myserver:/home/ahfuzhang/temp/2024-09-09/heap_profile.out ./
go tool pprof -http=:8090 heap_profile.out
打开浏览器,看到如下信息:
通过 top 也发现 s3 sdk 里面内存分配得很离谱:
通过源码分析:
// vendor/github.com/aws/aws-sdk-go/service/s3/s3manager/pool.go
func (p *maxSlicePool) newSlice() *[]byte {
bs := make([]byte, p.sliceSize) // 这里的 sliceSize 太大了
return &bs
}
func newMaxSlicePool(sliceSize int64) *maxSlicePool {
p := &maxSlicePool{sliceSize: sliceSize} // sliceSize 是调用端传进来的
p.allocator = p.newSlice
return p
}
func newUploader(client s3iface.S3API, options ...func(*Uploader)) *Uploader {
u := &Uploader{
S3: client,
PartSize: DefaultUploadPartSize,
Concurrency: DefaultUploadConcurrency,
LeavePartsOnError: false,
MaxUploadParts: MaxUploadParts,
BufferProvider: defaultUploadBufferProvider(),
}
for _, option := range options {
option(u)
}
u.partPool = newByteSlicePool(u.PartSize) // 原来是 part size 决定了内部缓冲区的大小
return u
}
最后,修改 part size 后问题解决:
xx := s3manager.NewUploader(session.Must(sess, err), func(u *s3manager.Uploader) {
u.Concurrency = types.UploadConcurrency
u.PartSize = partSize // 默认 20mb,不分片
})
引入这个问题是因为某次讨论中认为分片没必要,直接将默认分片从 5mb 修改为 3GB
修改后未确认 s3 sdk 内部的逻辑。