Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

request context pool 回收机制影响到了 bytebuffer pool 的分配计算,导致系统容易发生OOM #1026

Open
784909593 opened this issue Dec 13, 2023 · 5 comments
Assignees
Labels
bug Something isn't working

Comments

@784909593
Copy link

784909593 commented Dec 13, 2023

Describe the bug

request context pool 回收机制影响到了 bytebuffer pool 的分配机制,导致系统弹性变低,系统在遇到偶发异常的情况下,会更容易OOM。

代码分析:

image

这里是用 response body 的 len 进行判断的,而不是 cap。如果 body 的 cap 较大,但是 len 较小时,这个 body 对象是不会被 put 回 responseBody pool 中的。也就是说,如果最初 Hertz 从 reponseBody pool 中取出的是一个较大的对象时,而这次请求返回的 response 数据又较小时,这个 body 对象是不会被重新 put 回 responseBody pool 中的,而是继续被 requestContext 持有,并一起被 put 回 requestContext pool 中。

如果 response body 的 len,小于 MaxKeepBodySize(默认 4M),那他会被放在 requestContext 内。而大于 MaxKeepBodySize 会被放回 reponseBody pool 中。因此这个过程相当于起到了一层过滤的作用,只有大的 body 会被 Put 回 reponseBody pool 中,也就是说在 Put 方法中统计到的 body 大小,都是大于 MaxKeepBodySize 的。这就导致了 reponseBody pool 在触发 Calibirate 方法对 defaultSize 进行重计算时,会将 defaultSize 调大到一个大于 MaxKeepBodySize 的值。

image

线上流量源源不断,Hertz 框架总会调用到 responseBody pool 中的 Get 方法创建新的 body 使用,而新分配到的 body 大小都将是大于 MaxKeepBodySize 的 defaultSize(我们场景下为 16M),这些新分配的 body 如果遇到了 response 数据较小的接口,那又会因为缓存机制分析一节中提到的 len 和 cap 的判断差异问题,这些大 cap 小 len 的 body 无法被放回到 responseBody pool 中,最终导致 requestContext pool 中所有 requestContext 的 response body 的大小都会变为 defaultSize(即 16 M)。
这就会使得在突发流量到来,或者旧的的请求在异常情况下无法结束使得 requestContext 无法释放时,框架会为新来的请求创建大量 defaultSize (16M)大小的对象,导致系统弹性很低,进而发生 OOM。

Expected behavior

框架不要做超出正常流量的内存分配

Screenshots

image

Hertz version:

v0.6.7

Environment:
go 1.21

@welkeyever
Copy link
Member

可以提一个修改默认 maxKeepBodySize 的 PR,以及可能的话,添加一个测试来保障修改效果

@li-jin-gou li-jin-gou added the bug Something isn't working label Dec 13, 2023
@welkeyever
Copy link
Member

@784909593 If there is anything different to what has been concluded here, please provide more context for others to catch up.

@784909593 784909593 changed the title request context pool 和 bytebuffer pool 的配合设计不合理,导致系统容易发生OOM request context pool 回收机制影响到了 bytebuffer pool 的分配机制 ,导致系统容易发生OOM Jan 27, 2024
@784909593 784909593 changed the title request context pool 回收机制影响到了 bytebuffer pool 的分配机制 ,导致系统容易发生OOM request context pool 回收机制影响到了 bytebuffer pool 的分配机制,导致系统容易发生OOM Jan 27, 2024
@784909593
Copy link
Author

784909593 commented Jan 27, 2024

这里我想到了两种修复方式

  1. 将 MaxKeepBodySize 参数默认值设置为 0,将两个 pool 从设计上进行解耦。在每个请求完成后都将 response body 对象塞回到 responseBody Pool 中,使 responseBody Pool 的 put 方法可以统计到线上流量的真实情况,计算出更加合适的 defaultSize。但是这种方法有个弊端,即由于bytebuffer pool的分配算法较为简单,算出的defaultSize仍然不是最合适的值。仍然会存在浪费内存和分配较大的问题。
  2. 使用 mcache 这个分级 bufferpool,在每次 put 时根据 body 的大小 put 进不同的池子里,在每次 get 时传入所需 body 的大小,从对应的池子里取出对象,这样就能很好的隔离大小不同的 body。

@784909593 784909593 changed the title request context pool 回收机制影响到了 bytebuffer pool 的分配机制,导致系统容易发生OOM request context pool 回收机制影响到了 bytebuffer pool 的分配计算,导致系统容易发生OOM Jan 28, 2024
@784909593
Copy link
Author

784909593 commented Jan 29, 2024

To Reproduce

单测:

func TestBigBodyBug(t *testing.T) {

runtime.GOMAXPROCS(3)
hertz := New(WithHostPorts("127.0.0.1:8888"))
hertz.GET("/test1", func(c context.Context, ctx *app.RequestContext) {
	body := make([]byte, 1024*1024*9)
	ctx.SetBodyString(string(body))
})
hertz.GET("/test2", func(c context.Context, ctx *app.RequestContext) {
	body := make([]byte, 1024)
	ctx.SetBodyString(string(body))
})
hertz.GET("/test3", func(c context.Context, ctx *app.RequestContext) {
	body := make([]byte, 1024*2)
	ctx.SetBodyString(string(body))
})
go hertz.Run()
go func() {
	for i := 0; i < 2; i++ {
		go func() {
			for true {
				http.Get("http://127.0.0.1:8888/test1")
			}
		}()
	}
}()
go func() {
	for i := 0; i < 5; i++ {
		go func() {
			for true {
				http.Get("http://127.0.0.1:8888/test2")
			}
		}()
	}
}()
go func() {
	for i := 0; i < 5; i++ {
		go func() {
			for true {
				http.Get("http://127.0.0.1:8888/test3")
			}
		}()
	}
}()
<-make(chan struct{})

}

在这里增加一行日志,用来观测body大小
`
func (resp *Response) BodyBuffer() *bytebufferpool.ByteBuffer {

if resp.body == nil {
	resp.body = responseBodyPool.Get()
}
resp.bodyRaw = nil
fmt.Printf("test url=%s resp Body cap=%d\n", url, cap(resp.body.B))
return resp.body

}
`
运行一段时单测之后会发现,resp body的大小增大到了较大的程度。

@Duslia
Copy link
Member

Duslia commented Feb 2, 2024

可以加一个单测看一下修复后的效果吗?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Development

No branches or pull requests

4 participants