-
러스트로 만든 웹사이트를 Docker로 반복 빌드할 때 빌드 시간 문제를 겪음
- 기본 Docker 설정에서는 전체 의존성 재빌드가 매번 발생해 4분 이상 소요됨
-
cargo-chef와 캐싱 도구를 사용해도 최종 바이너리 빌드에 여전히 많은 시간이 걸림
- 프로파일링 결과, LTO(링크 타임 최적화)와 LLVM 모듈 최적화에 대부분 시간을 소모함
- 최적화 옵션, 디버그 정보, LTO 설정을 조절해 일부 개선 가능하지만, 최종 바이너리 컴파일에 최소 50초는 소요되는 현상 확인
문제 제기 및 배경
- Rust로 만든 개인 웹사이트를 수정할 때마다 정적 링크 바이너리를 빌드해 서버에 복사 후 재시작하는 번거로운 작업이 반복됨
- Docker 또는 Kubernetes 등 컨테이너 기반 배포로 전환하려 했으나, Rust의 Docker 빌드 속도가 큰 문제로 드러남
- Docker 내에서 작은 코드 변경에도 전체를 처음부터 재빌드해야 해서 비효율적인 상황 발생
Docker에서 Rust 빌드 – 기본 접근
- 일반적인 Dockerfile 접근은 모든 의존성과 소스코드를 복사한 뒤 cargo build 실행 방식임
- 이 경우 캐싱의 이점이 없어 전체 재빌드가 반복됨
- 본인의 웹사이트 기준, 전체 빌드는 약 4분 소요됨—의존성 다운로드에도 추가 시간 소요
Docker 빌드 캐싱 개선 – cargo-chef
-
cargo-chef 도구를 활용하면, 의존성만 별도의 레이어로 미리 캐시해둘 수 있음
- 이를 통해 코드 변경 시 의존성 빌드가 재사용되어 빌드 속도 개선 효과 기대
- 실제 적용 시, 전체 시간 중 25%만이 의존성 빌드에 집중되고, 최종 웹서비스 바이너리 빌드에 여전히 상당한 시간이 소요됨(2분 50초~3분)
- 주요 의존성(axum, reqwest, tokio-postgres 등)과 7,000여 줄의 자체 코드로 구성됨에도 rustc의 단일 실행에 3분 소요되는 구조
rustc 빌드 시간 분석: cargo --timings
-
cargo --timings를 사용하여 각 크레잇(compilation unit)별 빌드 시간 확인 가능
- 결과적으로 최종 바이너리 빌드가 전체 시간의 대부분을 차지하는 것을 확인
- 보다 세밀한 원인 분석에는 도움이 되나, 구체적 컴파일러 내부 동작 파악이 부족함
rustc 자체 프로파일링(-Zself-profile) 활용
- rustc의 자체 프로파일링 기능을 -Zself-profile 플래그로 활성화하여 세부 동작 시간 측정
- 이를 위해 환경변수를 통해 프로파일링 활성화
- 결과 요약(summarize) 툴로 분석 시, LLVM LTO(링크 타임 최적화), LLVM 모듈 코드 제너레이션이 전체 시간의 60% 이상 차지함을 발견
- flamegraph 시각화로도 codegen_module_perform_lto 단계에서 전체 시간의 80% 소모를 확인
LTO(링크 타임 최적화)와 빌드 최적화 옵션
- Rust 빌드는 기본적으로 codegen unit별로 분할된 후, LTO에 의해 전체 최적화가 비교적 후반에 적용됨
- LTO에는 off, thin, fat 등 여러 옵션이 존재: 각각 성능 및 최종 결과물에 영향
- 작성자의 프로젝트는 Cargo.toml에서 LTO를 thin으로, 디버그 심볼도 full로 설정된 상태였음
- 다양한 LTO/디버그 심볼 조합을 테스트한 결과:
- full 디버그 심볼의 빌드 시간 증가 효과와 fat LTO의 4배 가량 빌드 지연 확인
- LTO 및 디버그 심볼 제거 시에도 최소 50초 빌드 시간 필요
추가 최적화 및 단상
- 50초 정도면 실제 서비스 부하가 거의 없는 본인 사이트에는 큰 문제 없으나, 기술적 호기심으로 추가 분석 시도
-
증분 컴파일(incremental compilation)을 Docker로 잘 활용하면 더 빠른 빌드도 가능하나, 빌드 환경 청결성과 도커 캐시의 결합 필요
LLVM 단계 세부 프로파일링
- LTO와 디버그 심볼 제거 후에도 LLVM_module_optimize 단계에서 70% 가까운 시간 소모
- release 프로필에서 opt-level 기본값(3)으로 인한 최적화 비용이 큼을 인지, 바이너리에서만 opt-level을 낮추는 방법 테스트
- 각종 최적화 조합 실험 결과 최적화 미적용(opt-level=0) 시 15초 내외, 최적화 적용(1~3) 시 50초 내외로 소요됨
LLVM 추적 이벤트 심층 분석
- rustc의 추가 플래그(-Z time-llvm-passes, -Z llvm-time-trace)를 이용해 LLVM 단계별 실행 시간 상세 추적 가능
-
-Z time-llvm-passes는 출력이 방대해 Docker의 log 제한을 초과하는 경우가 많아 log 설정 조정 필요
- 로그를 파일로 저장해 분석하면 각 LLVM 최적화 패스별 실행 시간을 개별적으로 확인 가능
-
-Z llvm-time-trace 옵션은 chrome tracing 포맷의 방대한 JSON 출력을 생성하며, 파일이 매우 커 일반적인 텍스트 편집/분석 도구를 사용하기 어려움
- 이를 newline 단위로 분할 처리(jsonl)해 CLI/스크립트 환경에서 분석 가능
주요 insight 및 결론
- Rust로 복잡한 프로젝트를 Docker로 빌드할 때, 빌드 속도 병목은 주로 최종 바이너리 빌드 및 관련 LLVM 최적화 단계에서 발생함
- LTO와 디버그 심볼, opt-level을 조정할 때 빌드 시간과 바이너리 크기 간 트레이드오프 분명함
- 최적화 옵션을 적극적으로 조정할 경우 빌드 시간 대폭 단축 가능하나, 최적화 미사용 시 퍼포먼스 저하 가능성 존재
- 대규모 크레이트 의존성과 상용 환경에서 빌드 효율 중요시 된다면 프로파일링을 적극적으로 활용해 세부 병목을 구체적으로 파악하는 것이 좋은 전략임
- 러스트 빌드 파이프라인 설계 시 LTO, opt-level, 디버그 심볼, 캐시 전략에 대한 정교한 조합 설계가 필요함