## Go 성능 최적화 팁
박재완
---
## Airbridge 와 Luft
- [Airbridge](https://www.airbridge.io/)
- 합리적인 비용의 올인원 모바일 마케팅 솔루션
- [Luft](https://engineering.ab180.co/stories/introducing-luft)
- Airbridge 에서 유저행동 분석을 위해 사용하는 OLAP 데이터베이스
- [Ziegel](https://engineering.ab180.co/stories/traildb-to-ziegel) 을 스토리지 엔진으로 사용
- Go 로 작성
<!--more-->
---
## Luft 사용자의 페르소나
- 장기간 데이터에 대해 복잡한 집계 쿼리를 한다.
- 반응속도에 민감하지 않다.
- 정확한 예를 들면:
- 지난 6개월간 우리 앱에서 한 달에 10만 원 이상 소비한 30대 여성 사용자의 1주일간의 리텐션
---
## 최적화 가설
- 매우 느린 쿼리는 사용자도 납득할만큼 터무니없음
- 매우 빠른 쿼리는 개선할 필요 없음
- 게임처럼 매우빠른 응답속도를 요구하지 않음
- 적당히 느린 쿼리를 개선대상으로 설정
---
## 적당히 느린 쿼리?
- 처음에는 그냥 눈에 띄는 긴 쿼리
- 좀 더 정확한 기준을 말하면 P90 에서 P95 사이의 쿼리
- 3개월 데이터에서 퍼널을 조회하는 9초 쿼리
- 1개월 데이터에서 리텐션을 조회하는 13초 걸리는 쿼리
---
## 또 있다!
- 잠자는 동료를 깨우는 쿼리
- 매우 느린 쿼리 중 일부는 새벽에 batch 로 실행
- 임계점을 넘어갈 정도로 느리면 사람이 수동으로 복구해야 했음
---
## 이런 쿼리들을 개선하며 얻은!
## 실전 Go 성능 최적화 팁!
---
"net/http/pprof" 패키지를 사용하면
프로파일 정보를 얻을 수 있음
```go
import _ "net/http/pprof"
main() {
http.ListenAndServe("localhost:6060", nil)
}
```
---
여러 기능이 있는데 오늘 다룰 내용은
- profile
- heap
- trace
---
`go tool pprof` 명령어로 데이터 분석
```zsh
go tool pprof http://localhost:6060/debug/pprof/profile
```
`http` flag 를 추가하면 분석용 UI 실행
```zsh
go tool pprof -http=localhost:8080 http://localhost:6060/debug/pprof/profile
```
`go tool trace` 로 trace 분석
```zsh
curl -o trace.log http://localhost:7400/debug/pprof/trace?seconds=10
go tool trace trace.log
```
---
## 이제 개별 팁으로 들어갑니다!
---
## Exists filter 가 느림(CPU profile)
![](/go/go-performance-optimization-tips/exists-filter-flame-graph.png)
---
데이터 존재를 확인하기 위해 호출되는
[string to interface 변환](https://github.com/golang/go/blob/e827d41c0a2ea392c117a790cdfed0022e419424/src/runtime/iface.go#L388) 이 느리기 때문
![](/go/go-performance-optimization-tips/runtime-convtstring.png)
---
- 쿼리 계획 시점에 filter 최적화
- 가능한 경우 Exists -> NotEqualTo fliter 로 최적화
- value != "" 비교로 동작
- NotEqualTo filter 타입 최적화가 이미 되어 있기 때문에 string to interface 변환 이 없음
---
- Exists 구현을 GetValue 없이 방법도 고려했지만
- 역사적인 이유로 시도하지 않음
- 다른 모듈에 큰 복잡도 증가를 가져올 것 같았음
- 왜 Exists 에서 value 를 가져와야 하나?
- Luft 는 스키마가 약하고, Null 개념이 없기 때문에 Exists 개념이 일반적인 경우와 약간 다름
- 값이 있지만 빈 문자열이 아님을 의미
---
Exists filter 개선 결과
- 약 22% 쿼리 속도 개선(13.5s -> 10.5s)
---
## 메모리 풀
일부 함수가 납득하기 어려운 지분을 차지함
- sync.(*Pool).Get
- runtime.makeslice
- runtime.gcBgMarkWorker
![](/go/go-performance-optimization-tips/pprof-old-memory-pool-cpu.png)
---
메모리가 pool 에 반납되지 않는 경우가 있는 것으로 추정
---
memory profile 도 해당 부분의 할당이 큰 지분을 차지
![](/go/go-performance-optimization-tips/pprof-old-memory-pool-mem.png)
---
### Cursor 의 Next 함수
여러 파이프라인을 거치며 데이터가 사용될 때
효율적으로 동작하게 개선
---
기존:
```go
for cur.Next() {
buffer := cur.Current()
var data []byte
copy(data, buffer)
doSomething(buffer)
}
```
변경 후:
```go
for cur.Next() {
current := cur.Current()
doSomething(current)
}
```
---
- 보통의 경우 Cursor 가 내부 buffer 를 가지는 것이 유리
- Luft 는 다음 파이프라인으로 데이터를 전달 하는 경우가 대부분이기 때문에 내부 buffer 를 사용하지 않게 수정
---
개선 후 profile
![](/go/go-performance-optimization-tips/pprof-new-memory-pool-cpu.png)
---
메모리 풀 개선 결과
- 약 51% 쿼리 속도 개선(8.5s -> 4.2s)
---
## gRPC stream 수 줄이기
---
프로파일 결과에서 특이점을 발견하지 못했음
- 오래 걸리는 함수 없음
- 과도하게 할당하는 메모리 없음
---
[gotraceui](https://github.com/dominikh/gotraceui) 에게 감사하는 시간
- 오픈소스 go trace frontend
- go tool trace 에 비해 압도적으로 빠름
---
쿼리시간 14.7초 중 3~6초만 processor 유의미하게 사용
![](/go/go-performance-optimization-tips/trace-old.png)
---
trace 의 synchronization blocking profile
- gRPC stream 이 과도한 blocking 을 발생시키고 있음
![](/go/go-performance-optimization-tips/trace-old-sync-block-prof.png)
---
gRPC stream 을 너무 많이 만들지 않게 개선
---
gRPC stream 수 줄이기 결과
- 약 56% 쿼리 속도 개선(14.7s -> 6.4s)
---
## Global lock 제거
---
## gRPC stream 수 줄이기 의 역습!
1. Stream 을 적게 만듬
2. 여러 쿼리가 동시에 실행될 가능성이 높아짐
3. 잠재된 위험이었던 메타데이터 접근 시 global lock 의 경합이 심화됨
---
Production 에서 얻어맞은 후
global lock 을 제거하는 방식으로 수정함
---
- 단일 쿼리에서 재현되지 않아 release 전에 검증하지 못함
- 이 문제를 해결하기 위해 대량 쿼리 프로파일 도입 중
---
Global lock 제거 결과
- Pxx: 13.2s -> 19.9s -> 7.6s 으로 개선
---
## 샘플링 개선
샘플링: 실제 대규모 쿼리를 하지 않고 결과값을 추정
---
프로파일이 아니라
시스템에 대한 이해가 있었기에 가능했던 작업
---
- 샘플링을 위해 실제 데이터를 읽어서 검증했음
---
- 데이터가 정렬되어 저장되게 수정하고
- 미리 샘플링할 데이터 범위를 정해 데이터를 읽는 시도조차 하지 않게 수정
---
샘플링 개선 결과
- 약 22% 쿼리 속도 개선(13.5s -> 10.5s) 성능개선
---
## 정리
- Exists filter 개선: 22%(13.5s -> 10.5s)
- 메모리 풀 개선: 51%(8.5s -> 4.2s)
- gRPC stream 수 줄이기: 56%(14.7s -> 6.4s)
- Global lock 제거: Pxx: 13.2s -> 19.9s -> 7.6s
- 샘플링 개선: 22%(13.5s -> 10.5s)
---
## 교훈
- profile 과 trace 를 사용하면 Go 최적화를 효율적으로 할 수 있다.
- 대상 시스템에 대한 이해는 최적화에 큰 도움을 준다.