Zero-Growth Stack, Real Gains: How Stack Allocation Can Save 10% CPU in Go
스택 확장 없이 CPU 절감하기 — Go에서 스택 할당으로 CPU 사용량 10% 줄이기
Uber는 Go 런타임의 스택 확장으로 CPU를 많이 쓰던 서비스에 프로파일 기반 스택 크기 조정을 적용했습니다. 고루틴 시작 스택을 2KB에서 32KB로 바꾼 뒤 스택 확장으로 발생하던 CPU 사용량을 10% 미만에서 1% 미만으로 낮췄습니다.
- 주제
AI 요약
Uber는 서비스 약 65%를 Go로 운영하며, Go 서비스에 200만 개가 넘는 코어를 사용합니다. 이런 규모에서는 전체 효율을 1%만 높여도 수백만 달러의 비용 절감으로 이어집니다. 엔지니어들은 한 서비스에서 스택 확장에 CPU의 약 10%가 쓰이는 문제를 찾아내고, 고루틴의 시작 스택 크기를 조정해 CPU 사용량을 낮췄습니다.
고루틴 스택 확장과 CPU 비용
운영체제 스레드는 보통 2MB 스택을 사용하지만, Go 고루틴은 기본 2KB의 작은 스택으로 시작합니다. 고루틴의 함수 호출이 이 공간을 넘으면 Go 런타임은 스택 크기를 두 배로 늘리고 기존 내용을 새 스택에 복사합니다. 메모리를 아끼고 더 많은 고루틴을 실행하는 데 유리하지만, 스택 확장과 복사가 반복되면 CPU 비용이 커집니다.
go1.19부터 런타임은 평균 스택 사용량을 추적해 시작 크기를 조정하는 적응형 기능을 도입했습니다. 하지만 Uber의 일부 서비스에서는 이 방식으로도 스택 확장 비용이 컸습니다. 다른 선택지로 고루틴 풀을 고려할 수도 있습니다. 다만 작업자 풀에 맞춰 코드를 바꿔야 하고, 채널 통신 비용과 고정된 풀 크기를 다뤄야 합니다.
런타임 시작 스택 크기 조정
Uber는 Go 런타임의 비공개 값인 startingStackSize를 수정하는 방식을 택했습니다. Go 소스 코드에 패치를 적용해 linkname으로 이 값과 디버그 전역 변수에 접근하고, 내부 모듈에 값을 바꾸는 공개 메서드를 만들었습니다. 적응형 시작 크기를 끄고 스택 축소도 비활성화해 런타임이 설정한 값을 덮어쓰지 않도록 했습니다.
이 방법은 런타임 내부 구조에 의존하므로 Go 버전이 바뀌면 코드가 작동하지 않을 위험이 있습니다. Uber는 빌드 태그를 사용해 버전별 구현을 관리하고, 새 Go 버전이 나올 때마다 관련 구조와 변수가 유지되는지 확인합니다. 스택 크기는 애플리케이션 시작 시 설정하며, 구성 시스템을 이용해 실행 환경, 존, 리전별로 값을 다르게 적용합니다.
프로파일로 적정 크기 찾기
처음에는 4KB부터 배포한 뒤 프로파일에서 copystack이 계속 보이는지 확인하고, 필요하면 8KB로 늘리는 수동 조정을 검토했습니다. 평균 시작 스택 크기를 계측하는 방법도 있었지만, 배포를 기다리며 반복해야 했습니다. 실행 중 직접 값을 얻는 방식은 오버헤드가 있고, 어느 지점에서 호출할지 정하기도 어려웠습니다.
대신 이미 수집하는 프로파일에서 필요한 정보를 얻었습니다. pprof로 copystack에 도달하는 스택 트레이스를 추린 다음, 각 함수가 사용하는 스택 공간을 계산합니다. 구현에는 go tool objdump를 호출하는 방법과 자체 어셈블리 분석기를 만드는 방법이 있습니다. Uber는 최종 운영 환경에 맞춰 바이너리를 직접 분석하는 방식을 사용했습니다.
분석기는 디버그 심볼을 읽고 각 함수의 어셈블리 명령을 살핍니다. 함수가 스택 공간을 확보할 때 나오는 sub 명령에서 할당량을 얻어 스택 트레이스별 사용량을 합산합니다. 분석 결과 가장 큰 사용량은 약 19KB였습니다. 시작 스택 크기를 2의 거듭제곱으로 맞춰야 하므로 32KB를 선택했습니다. 분석 과정에서 요청마다 호출되는 go.uber.org/yarpc/internal/observability.(*Middleware).Call이 2.6KB를 사용하는 점도 발견했습니다. 해당 함수의 스택 사용량은 코드 최적화로 600바이트까지 줄였습니다.
운영 결과와 다음 단계
변경 전에는 서비스 CPU 프로파일의 약 10%가 스택 확장에서 나왔습니다. 시작 스택 크기를 2KB에서 32KB로 바꾼 뒤 이 비율은 1% 미만으로 줄었습니다. 전체 CPU 사용량은 180에서 150으로 감소해 약 16% 개선됐습니다. 인스턴스 대부분의 스택 메모리 사용량은 약 50MB였고, 일부는 200MB까지 늘었습니다. 인스턴스 메모리가 16GB라 최대 증가분도 2% 미만이었습니다.
Uber는 스택 확장으로 CPU를 많이 쓰면서 스택 메모리 사용량은 낮은 서비스나, 메모리 여유가 있는 서비스를 우선 찾아 적용할 계획입니다. 스택 크기를 정하는 작업을 자동화하고, 분석기 결과를 활용해 스택 사용량이 큰 핫 함수도 계속 최적화하려 합니다.
원문: Uber Engineering / 번역·요약: Trawling