Go 1.15에서 작은 객체 할당 최적화 경험
문제 상황
운영 중인 Go API 서버의 메모리 사용량이 시간이 지날수록 증가하는 현상이 발견됐다. 트래픽이 일정한데도 RSS 메모리가 계속 늘어나 재배포 주기가 짧아지고 있었다.
프로파일링
pprof로 힙 프로파일을 확인했다.
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
go tool pprof http://localhost:6060/debug/pprof/heap 결과, JSON 응답을 만들 때 사용하는 임시 구조체가 문제였다. 초당 수천 개의 작은 객체가 생성되고 있었다.
해결: sync.Pool 적용
자주 사용되는 응답 객체를 풀링했다.
var responsePool = sync.Pool{
New: func() interface{} {
return &Response{
Data: make([]Item, 0, 100),
}
},
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
resp := responsePool.Get().(*Response)
defer func() {
resp.Data = resp.Data[:0]
responsePool.Put(resp)
}()
// 비즈니스 로직
resp.Data = append(resp.Data, items...)
json.NewEncoder(w).Encode(resp)
}
결과
- 메모리 사용량 30% 감소
- GC 빈도 40% 감소
- P99 레이턴시 15ms → 11ms
풀 사용 시 객체 초기화를 제대로 하지 않으면 이전 요청 데이터가 남을 수 있어 주의가 필요했다. defer에서 슬라이스를 명시적으로 비우는 게 중요했다.
참고
Go 1.15에서 링커 개선으로 바이너리 크기가 줄었다는 릴리즈 노트를 봤는데, 실제로 빌드 결과물이 5% 정도 작아졌다. 사소하지만 반가운 변화였다.