Nixpkgs 로딩 속도, 10년 묵은 'stat storm' 해결책은?
Nixpkgs의 동적 로더(Dynamic Loader)는 패키지 탐색 시 수백 번의 실패한 `openat` 호출로 인해 심각한 성능 저하를 유발함
70ms 이상의 쉘 프롬프트 지연은 devenv뿐 아니라 Nixpkgs 전반의 문제로 지적됨
절대 경로 링크(Absolute Path Linking), 심볼릭 링크 팜(Symlink Farm) 등 다양한 해결책이 시도되었으나, 기존 시스템과의 호환성 문제로 완전한 해결책이 되지 못함
ELF 노트 캐시(ELF Note Cache) 방식이 모든 요구사항을 충족하는 가장 유망한 해결책으로 제시됨
Nixpkgs의 'Stat Storm' 근본 원인 분석
Nixpkgs는 각 패키지를 고유한 `/nix/store` 경로에 격리하는 데이터 격리 아키텍처(Data Isolation Architecture)를 사용합니다. 이로 인해 동적 로더는 공유 라이브러리를 찾기 위해 수백 개의 디렉토리를 순차적으로 탐색해야 하며, 각 탐색마다 실패하는 `openat` 시스템 호출이 발생합니다. 이는 `DT_RUNPATH` 메타데이터와 `glibc-hwcaps` 서브디렉토리 탐색으로 인해 더욱 악화되어, 수백에서 수천 번의 불필요한 파일 열기 시도를 유발합니다. 이로 인해 devenv와 같은 도구는 쉘 프롬프트마다 수십 밀리초의 지연을 경험하게 됩니다.
기존 해결책들의 트레이드오프 (Trade-offs)
커뮤니티에서 논의된 절대 경로 링크(Absolute Path Linking) 방식은 `DT_NEEDED` 항목을 라이브러리의 절대 경로로 직접 수정하여 검색을 제거하지만, `LD_LIBRARY_PATH` 오버라이드와 같은 중요한 기능을 깨뜨립니다. 심볼릭 링크 팜(Symlink Farm) 방식은 각 실행 파일마다 필요한 라이브러리를 가리키는 심볼릭 링크 디렉토리를 생성하여 검색 경로를 단일화하지만, 동일한 `soname`을 가진 라이브러리 충돌 문제를 해결하지 못하고 스토어 공간을 추가로 차지하는 단점이 있습니다. 두 방식 모두 Nix의 핵심 모델인 동일 `soname`에 대한 다른 빌드 버전 관리를 유지하기 어렵습니다.
ELF 노트 캐시(ELF Note Cache) 접근 방식의 장점
가장 유망한 해결책으로 제시된 ELF 노트 캐시(ELF Note Cache)는 `patchelf` 도구를 사용하여 각 라이브러리에 `PT_NOTE` 섹션을 추가합니다. 이 노트에는 각 `DT_NEEDED` `soname`에 대한 라이브러리 위치 정보가 기록되며, 수정된 `glibc` 로더는 이를 활용하여 직접 라이브러리를 로드합니다. 이 방식은 `LD_LIBRARY_PATH` 오버라이드, `LD_PRELOAD`, `glvnd` 드라이버 스왑 등 기존의 모든 기능을 보존하면서도 `glibc-hwcaps` 탐색을 포함한 모든 실패한 검색을 제거합니다. 또한, 스토어 크기 증가나 `glibc` 패치 유지보수 부담이 적다는 장점이 있습니다.
정적 링크(Static Linking)의 가능성과 한계
devenv와 같이 자체 포함적인 CLI 도구의 경우, 정적 링크(Static Linking)를 통해 동적 로더 자체를 제거하는 것이 성능 향상에 효과적입니다. 글에서는 `musl` 기반으로 정적 링크 시 devenvn의 시작 시간을 약 70ms에서 16ms로 단축했다고 보고합니다. 하지만 이 방식은 플러그인 로딩, 드라이버 오버라이드 등 동적 로더의 기능에 의존하는 Nixpkgs의 많은 부분과 호환되지 않아 일반적인 해결책으로는 적용하기 어렵습니다.
macOS의 dyld와 Nix on Darwin의 접근 방식
macOS의 동적 로더인 `dyld`는 Nixpkgs의 `glibc`와 달리 절대 경로 기반의 라이브러리 참조(`LC_LOAD_DYLIB`)를 사용하며, `LC_RPATH`가 없어 `stat storm` 문제가 발생하지 않습니다. Nix on Darwin은 이미 이 방식을 채택하여 성능을 확보했습니다. 다만, Nix on Darwin에서 발생하는 특정 시작 비용(`~13ms`)은 `libstore`가 `arch -arch x86_64 /usr/bin/true`를 실행하는 데서 기인하며, 이는 `stat` 호출로 대체되어 크게 개선되었습니다.