[uv] macOS에서 uv 캐시 정리가 3.8배 빨라진 비결: getattrlistbulk를 활용한 일괄 메타데이터 조회
PR 링크: astral-sh/uv#21344 상태: Merged | 변경: +409 / -15
들어가며
Python 패키지 매니저인 uv는 극강의 속도를 자랑합니다. 하지만 수만 개의 파일이 쌓인 캐시를 정리하는 작업(uv cache clean 또는 prune)은 파일 시스템의 한계로 인해 병목이 발생하곤 했습니다. 특히 macOS 환경에서 각 파일의 hardlink count를 하나씩 확인하는 작업은 시스템 콜 오버헤드로 인해 성능 저하의 주범이었습니다.
이번 PR(#21327의 후속)은 macOS 전용 시스템 콜인 getattrlistbulk를 도입하여, 수만 개의 파일 메타데이터를 개별적으로 조회하던 기존 방식을 배치(Batch) 처리 방식으로 전환했습니다. 이를 통해 캐시 스캔 성능을 약 3.8배(386ms -> 101ms) 향상시켰습니다. 시니어 엔지니어의 관점에서 이 최적화가 왜 훌륭한지, 그리고 실제 구현에서 어떤 저수준(low-level) 기법들이 사용되었는지 분석해 보겠습니다.
코드 분석: 기존의 순차적 접근 vs 새로운 일괄 처리
1. crates/uv-cache/src/lib.rs: 순회 로직의 변화
기존에는 walkdir을 사용하여 모든 파일을 하나씩 방문하며 hardlink_count를 조회했습니다. 이 방식은 구현이 간단하지만, 파일마다 독립적인 stat 계열 시스템 콜을 발생시킵니다.
Before:
// 모든 엔트리를 하나씩 순회하며 개별적으로 hardlink_count 호출
for entry in walkdir::WalkDir::new(&root)
.min_depth(1)
.contents_first(true)
{
let entry = entry?;
if entry.file_type().is_file() {
match uv_fs::hardlink_count(entry.path()) {
Ok(1) => summary += self.remove_path(entry.path())?,
Ok(_) => {}
Err(err) => return Err(err),
}
} // ... 디렉토리 삭제 로직
}
After:
새로운 로직은 macOS에서 지원하는 경우 files_with_one_hardlink라는 함수를 통해 디렉토리 내의 파일들을 한 번에 읽어옵니다. 만약 이 최적화 경로를 사용할 수 있다면 entries.skip_current_dir()를 호출하여 중복 스캔을 방지합니다.
let mut entries = walkdir::WalkDir::new(&root).min_depth(1).into_iter();
while let Some(entry) = entries.next() {
let entry = entry?;
if entry.file_type().is_file() {
// 기존 방식 유지
} else if entry.file_type().is_dir() {
// macOS 최적화 경로 시도
if let Some(files) = uv_fs::files_with_one_hardlink(entry.path())? {
entries.skip_current_dir(); // 하위 파일들을 이미 읽었으므로 스킵
for file in files {
summary += self.remove_path(file)?;
}
}
directories.push(entry.into_path());
}
}
2. crates/uv-fs/src/hardlink_macos.rs: getattrlistbulk 구현
이 PR의 핵심은 macOS 전용 getattrlistbulk 시스템 콜을 직접 호출하는 부분입니다. nix나 rustix 같은 표준 라이브러리에서 이 API를 제공하지 않기 때문에, 직접 libc를 사용하여 unsafe 블록 내에서 구현되었습니다.
핵심 데이터 구조:
getattrlistbulk는 가변 길이 레코드를 8바이트 정렬된 버퍼에 담아 반환합니다. 이를 위해 #[repr(align(8))]를 사용한 버퍼 구조체를 정의했습니다.
#[repr(align(8))]
struct AttributeBuffer([u8; 64 * 1024]);
#[repr(C)]
struct FileAttributes {
length: u32,
returned: libc::attribute_set_t,
error: u32,
name: libc::attrreference_t,
object_type: u32,
hardlink_count: u32,
}
시스템 콜 호출 부분:
한 번의 호출로 여러 파일의 이름, 타입, 하드링크 수를 가져옵니다. 이는 수천 번의 lstat 호출을 단 몇 번의 getattrlistbulk 호출로 줄여줍니다.
let count = unsafe {
libc::getattrlistbulk(
directory.as_raw_fd(),
(&raw mut attributes).cast(),
buffer.0.as_mut_ptr().cast(),
buffer.0.len(),
u64::from(libc::FSOPT_PACK_INVAL_ATTRS),
)
};
왜 이게 좋은 최적화인가?
1. 시스템 콜 오버헤드 감소 (Batching)
현대 운영체제에서 시스템 콜은 비싼 작업입니다. 유저 모드에서 커널 모드로의 컨텍스트 스위칭이 발생하기 때문입니다. 87,000개의 파일을 처리할 때 기존 방식은 최소 87,000번의 시스템 콜을 발생시키지만, 64KB 버퍼를 사용하는 배치 방식은 이를 수백 번 수준으로 줄여줍니다.
2. IOPS 및 파일 시스템 탐색 최적화
getattrlistbulk는 파일 시스템 내부적으로 디렉토리 엔트리를 순회할 때 메타데이터를 함께 추출하도록 설계되었습니다. 이는 디렉토리 엔트리를 읽은 후 다시 각 파일의 inode를 찾아가는 추가적인 디스크 탐색(seek)을 줄여줍니다.
3. 견고한 폴백(Fallback) 전략
이 최적화는 macOS에 특화되어 있습니다. 코드에서는 ENOTSUP, ENOSYS 등의 에러를 체크하여, 해당 시스템 콜을 지원하지 않는 환경이나 파일 시스템(예: 네트워크 드라이브)에서는 자동으로 기존의 안전한 WalkDir 방식으로 돌아가도록 설계되었습니다.
Some(libc::ENOTSUP | libc::ENOSYS | libc::EINVAL | libc::EACCES) => {
debug!("Falling back to individual hardlink counts: {error}");
Ok(None) // None을 반환하여 상위 로직에서 기본 walk를 수행하게 함
}
리뷰 피드백 반영: 안전성과 유지보수
리뷰 과정에서 konstin과 charliermarsh는 몇 가지 중요한 지적을 했습니다.
- 안전한 래퍼의 부재:
nix나rustix에 해당 기능이 없음을 확인하고 직접 구현하는 정당성을 확보했습니다. - 에러 핸들링:
EACCES(권한 없음) 상황에서도 전체 프로세스가 중단되지 않고 폴백을 통해 개별 파일 단위로 시도하여, 접근 가능한 파일이라도 삭제할 수 있도록 개선되었습니다. - 디렉토리 삭제 순서:
uv-cache수정 시directories.push(...)후.rev()를 통해 역순으로 삭제하는 로직을 추가했습니다. 이는 자식 디렉토리가 먼저 삭제되어야 부모 디렉토리가 비워져 삭제 가능해지는 파일 시스템의 특성을 고려한 것입니다.
결론
이 PR은 "성능은 디테일에 있다"는 것을 잘 보여줍니다. 단순히 라이브러리를 사용하는 수준을 넘어, 타겟 플랫폼의 저수준 API를 깊이 있게 이해하고 활용했을 때 얻을 수 있는 성능 이득은 막대합니다. 특히 uv처럼 대규모 파일 처리가 빈번한 도구에서 이러한 OS 특화 최적화는 사용자 경험을 결정짓는 핵심 요소가 됩니다.
참고 자료
- https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/getattrlistbulk.2.html
- https://docs.rs/libc/latest/libc/fn.getattrlistbulk.html
- https://github.com/astral-sh/uv/pull/21327
⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
PR Analysis 의 다른글
- 이전글 [sglang] FLUX.2 모델 성능 최적화: Token Concatenation과 NVFP4 양자화의 커널 융합
- 현재글 : [uv] macOS에서 uv 캐시 정리가 3.8배 빨라진 비결: getattrlistbulk를 활용한 일괄 메타데이터 조회
- 다음글 [uv] uv의 잠금 파일 의존성 그래프 순회 최적화: 해싱에서 정수 인덱싱으로
댓글