
JuiceFS 1.4: Batch Unlink, Batch Clone 및 Redis Client-Side Caching을 통한 메타데이터 작업
요약
JuiceFS 1.4 버전에서 대규모 파일 처리 성능을 높이기 위한 세 가지 메타데이터 최적화 기능을 도입했습니다. Batch Unlink, Batch Clone, Redis Client-Side Caching을 통해 트랜잭션 오버헤드와 네트워크 왕복을 줄여 성능을 극대화했습니다.
핵심 포인트
- Batch Unlink 도입으로 대규모 파일 삭제 성능을 최대 93배 향상
- Batch Clone을 통해 메타데이터 복제 속도를 최대 24배 개선
- Redis 클라이언트 측 캐싱으로 빈번한 메타데이터 조회 오버헤드 감소
- 개별 트랜잭션을 배치 트랜잭션으로 병합하여 네트워크 및 시스템 호출 최적화
AI 학습 및 데이터셋 관리와 같은 대규모 파일 액세스 시나리오에서는 파일 수와 동시성(Concurrency)이 증가함에 따라 메타데이터가 종종 첫 번째 성능 병목 지점이 됩니다. 수백만 개의 작은 파일을 삭제하든, 대규모 데이터셋을 복제(Cloning)하든, 혹은 높은 동시성 환경에서 디렉토리를 탐색하든, 메타데이터 성능은 애플리케이션 효율성에 직접적인 영향을 미칩니다.
JuiceFS Community Edition 1.4는 세 가지 주요 메타데이터 최적화 기능을 도입했습니다:
- 대규모 파일 삭제를 위한 Batch unlink (일괄 삭제)
- 메타데이터 복제를 위한 Batch clone (일괄 복제)
- 빈번한 메타데이터 읽기를 위한 Redis client-side caching (Redis 클라이언트 측 캐싱)
이러한 개선 사항은 트랜잭션 커밋(Transaction commits), 네트워크 왕복(Network round trips), 그리고 불필요한 메타데이터 조회(Redundant metadata lookups)를 줄여줍니다. 100,000개의 파일이 포함된 플랫 디렉토리(Flat directory)에 대한 테스트 결과, batch unlink는 성능을 최대 93배 향상시켰으며, batch clone은 최대 24배의 속도 향상을 달성했습니다.
이 글에서는 이러한 최적화의 배경이 되는 동기, 설계 및 성능 이점에 대해 설명하겠습니다.
삭제: 개별 처리에서 일괄 트랜잭션으로
JuiceFS의 메타데이터-데이터 분리 아키텍처 (metadata-data separation architecture)] 하에서 파일을 삭제하는 것은 단순히 디렉토리 엔트리를 제거하는 것 이상의 작업을 포함합니다. 시스템은 또한 다음 작업을 수행해야 합니다:
- inode 참조 횟수(Reference counts) 업데이트
- inode 및 공간 리소스 회수
- 휴지통(Trash) 항목 처리
- 할당량(Quota) 통계 업데이트
이러한 작업들은 일반적으로 동일한 트랜잭션 내에서 완료되어야 합니다.
디렉토리에 수십만 개 또는 수백만 개의 파일이 포함되어 있는 경우, rm -rf에서 사용하는 전통적인 파일 단위 삭제 방식은 빠르게 병목 현상이 됩니다. 각 unlink 요청은 FUSE 프로토콜 (FUSE protocol)을 거치고, 커널(Kernel)과 유저 공간(User space) 사이를 전환하며, 별도의 메타데이터 트랜잭션을 트리거합니다.
파일 수가 증가함에 따라 시스템 호출(System calls), 컨텍스트 스위칭(Context switches), 네트워크 왕복, 그리고 트랜잭션 커밋으로 인한 오버헤드가 급격히 누적됩니다.
이 문제를 완화하기 위해 JuiceFS는 이전에 juicefs rmr 명령어를 도입했습니다. rm -rf와 달리, rmr은 FUSE 계층을 우회하여 클라이언트에게 삭제 요청을 직접 보냅니다. 또한 멀티스레드 삭제(기본값 50개 스레드)를 지원하여 처리량(Throughput)을 크게 향상시킵니다.
하지만 각 파일 삭제는 여전히 개별적인 메타데이터 트랜잭션(Metadata transaction)을 필요로 합니다. 즉, 100,000개의 파일을 삭제한다는 것은 여전히 100,000개의 트랜잭션을 실행해야 함을 의미합니다.
배치 언링크(Batch unlink)는 동일한 디렉토리 내의 많은 독립적인 삭제 작업을 단일 배치 트랜잭션(Batch transaction)으로 병합함으로써 최적화를 한 단계 더 발전시켰으며, 네트워크 오버헤드를 더욱 제거합니다.
핵심 설계
핵심은 많은 작은 트랜잭션을 더 적은 수의 큰 트랜잭션으로 전환하는 것입니다. JuiceFS는 메타데이터 엔진 계층에 배치 언링크(Batch unlink) 인터페이스를 추가했습니다. 이를 통해 클라이언트는 동일한 디렉토리 아래에 있는 여러 비디렉토리(Non-directory) 파일들을 한 번의 호출로 삭제할 수 있습니다.
디렉토리를 재귀적으로 비울 때, JuiceFS는 두 가지 방식으로 삭제 오버헤드를 줄입니다:
- 서로 다른 하위 디렉토리들은 멀티스레드 삭제를 통해 병렬로 처리됩니다.
- 각 디렉토리 내부의 일반 파일과 심볼릭 링크(Symlinks)는 배치(Batch)로 그룹화되어
BatchUnlink로 전송됩니다.
이는 메타데이터 수준에서 많은 언링크(Unlink) 작업을 더 적은 수의 배치 트랜잭션으로 병합합니다.
주의할 점은 BatchUnlink가 디렉토리를 직접 삭제하지는 않는다는 것입니다. 디렉토리 제거는 여전히 표준 재귀 워크플로우를 따릅니다: 먼저 하위 디렉토리를 비운 다음, 해당 하위 디렉토리 자체를 삭제합니다. 따라서 BatchUnlink는 동일한 디렉토리 내의 일반 파일과 심볼릭 링크에만 적용됩니다.
이러한 제한 사항은 디렉토리 트리 구조의 일관성 위험을 피하면서도 올바른 재귀적 삭제 의미론(Recursive deletion semantics)을 유지합니다.
메타데이터 엔진별 구현
JuiceFS는 트랜잭션 커밋 (transaction commits)과 네트워크 왕복 (network round trips)을 최소화하기 위해 메타데이터 백엔드 (metadata backend)에 따라 서로 다른 배치 (batching) 전략을 사용합니다.
SQL 백엔드 (MySQL, PostgreSQL 등): 이전에는 각 파일 삭제마다 개별적인 INSERT, DELETE, UPDATE 문 시퀀스가 필요했습니다. BatchUnlink를 통해 시스템은 다음과 같이 동작합니다:
- 단일 배치 쿼리로 대상 엔트리에 대한 모든 엣지 (edge) 레코드를 가져옵니다.
- 단일 잠금 배치 쿼리 (locked batch query)로 관련 아이노드 (inode) 속성을 검색합니다.
- 엣지 삭제, 아이노드 상태 업데이트 (nlink 감소 또는 정리 대상으로 표시), 그리고 delfile 엔트리 삽입을 모두 하나의 트랜잭션 내에서 실행합니다.
파일당 하나의 트랜잭션을 실행하는 대신, 이제 전체 배치를 단일 트랜잭션으로 완료할 수 있습니다.
Redis 백엔드: 최적화에는 Redis 파이프라인 (pipelines)과 트랜잭션 (transactions)이 사용됩니다. 이전에는 개별 삭제 시 별도의 명령 왕복이 필요했으나, BatchUnlink는 여러 파일에 대한 모든 HDEL (dentry 제거), ZADD (정리를 위한 인큐), SET (inode 속성 업데이트), INCRBY (카운터 업데이트) 명령을 하나의 파이프라인으로 수집하여 하나의 MULTI/EXEC 트랜잭션 내에서 원자적 (atomically)으로 실행합니다. Redis의 싱글 스레드 이벤트 루프 (single-threaded event loop)가 너무 오래 차단되는 것을 방지하기 위해 배치 크기는 250개 엔트리로 제한됩니다.
TiKV 백엔드: BatchUnlink는 TiKV의 배치 쓰기 (batch write) 기능을 사용하여 여러 삭제 작업을 하나의 트랜잭션으로 통합함으로써 네트워크 왕복과 트랜잭션 오버헤드를 줄입니다. 분산 키-값 (distributed key-value) 백엔드의 경우, 이러한 방식의 배치를 통해 백엔드의 동시 쓰기 용량을 더욱 충분히 활용할 수 있습니다.
아래 그림은 juicefs rmr --threads 16을 사용하여 100,000개의 파일이 있는 플랫 디렉토리 (flat directory)에서 수행한 벤치마크 결과를 보여줍니다. BatchUnlink는 모든 메타데이터 백엔드에서 유의미한 개선을 제공하며, 특히 TiKV와 Redis에서 가장 큰 이득을 보여줍니다.
Clone: 개별 복사에서 배치 참조로
juicefs clone은 훈련 데이터셋 버전 관리, 실험 스냅샷, 대규모 디렉토리 복제를 위해 파일 또는 디렉토리의 빠른 복사본을 생성합니다. 이 효율성은 클로닝(cloning)이 기반 데이터 블록을 즉시 복사하지 않는다는 사실에서 비롯됩니다. 대신, 메타데이터 계층에서 새로운 파일 레코드를 생성하고 소스 파일의 기존 블록 참조를 재사용합니다. 새로운 데이터 블록은 클론에 실제로 데이터가 쓰여질 때만 할당됩니다. 이를 통해 전체 복사 시 발생하는 시간 및 저장 공간 오버헤드를 방지합니다.
대규모 디렉토리 클론의 경우, 삭제와 동일한 문제가 발생합니다. 파일을 하나씩 처리하면 수많은 짧은 트랜잭션(transaction)과 네트워크 라운드 트립(network round trips)이 발생합니다. 배치 클론(batch clone)의 핵심 아이디어는 동일한 디렉토리 내 여러 파일에 대한 클론 작업을 하나의 배치 트랜잭션으로 병합하는 것입니다. 디렉토리를 재귀적으로 클론할 때, 시스템은 디렉토리 엔트리를 스트림(stream) 형태로 배치 단위로 읽습니다. 각 배치에 대해 모든 비-디렉토리(non-directory) 엔트리를 수집하여 하나의 작업으로 함께 클론합니다.
주요 구현 세부 사항 중 하나는 **inode 사전 할당(inode pre-allocation)**입니다. 트랜잭션에 진입하기 전에, 시스템은 nextInode를 사용하여 클론될 모든 엔트리에 대한 대상 inode를 미리 할당합니다. 이는 트랜잭션 내부에서 반복적으로 inode를 요청할 때 발생하는 잠금 경합(lock contention)을 방지합니다. 트랜잭션에 진입하면, 시스템은 모든 소스 파일 속성을 배치 쿼리(batch-query)하고(행 잠금(row locks) 사용), 대상 노드(nodes), 엣지(edges), 청크(chunks), 심볼릭 링크(symlinks), 확장 속성(xattrs)에 대한 모든 삽입 데이터를 구축한 다음, 모든 것을 단일 배치로 삽입합니다.
배치 클론은 배치 언링크(batch unlink)와 유사한 방식으로 각 백엔드의 네이티브 배치 쓰기(batch write) 기능을 사용합니다. 백엔드별 구현 세부 사항은 여기서 반복하지 않겠습니다.
성능 향상 폭은 다음 요소에 따라 백엔드별로 다르게 나타납니다:
- 트랜잭션 모델 (Transaction models)
- 네트워크 통신 오버헤드 (Network communication overhead)
- 노드(nodes), 엣지(edges), 청크 참조(chunk references)와 같은 메타데이터 레코드의 배치 삽입 효율성 (Batch insertion efficiency)
100,000개의 파일이 포함된 플랫 디렉토리(flat directory)에서의 결과는 다음과 같습니다. MySQL은 약 24배로 가장 큰 개선을 보였으며, Redis는 약 5배, TiKV는 약 2배의 개선을 보였습니다.
Redis 클라이언트 측 캐싱 (client-side caching): 핫 메타데이터를 로컬에 유지하기
AI 학습 데이터셋 액세스나 대규모 컨테이너 시작과 같이 동시성(concurrency)이 높은 메타데이터 워크로드에서는 JuiceFS 클라이언트와 Redis 사이의 네트워크 왕복 시간(network round trips)이 종종 주요 성능 병목 현상이 됩니다.
다음 작업을 예로 들어보겠습니다:
open("/mnt/jfs/dataset/images/cat.jpg")
파일을 열기 전에 Linux 가상 파일 시스템 (VFS)은 경로의 모든 구성 요소를 해석(resolve)해야 합니다:
dataset조회images조회cat.jpg조회
만약 images 디렉토리에 수십만 개의 파일이 들어 있고 학습 작업이 데이터셋 전체에 대해 무작위 액세스(random access)를 수행한다면, 각 조회마다 Redis에 GET 요청을 보내야 합니다.
높은 동시성 환경에서는 이로 인해 방대한 양의 네트워크 왕복이 발생하고 Redis의 CPU 사용률이 증가합니다. 단일 Redis 쿼리는 불과 수십 마이크로초(microseconds)밖에 걸리지 않더라도, 네트워크 지연 시간(latency)으로 인해 각 조회는 수백 마이크로초 또는 심지어 밀리초(milliseconds)까지 늘어납니다. 수천 개의 학습 프로세스가 동시에 파일에 액세스할 때, 이러한 오버헤드는 매우 심각해집니다.
작동 원리: Redis 6.0 클라이언트 측 캐싱 (client-side caching)
Redis 6.0은 클라이언트 측 캐싱 (client-side caching)을 도입하여, 클라이언트가 자주 사용되는 키(hot keys)를 로컬에 캐싱하고 해당 키가 수정될 때마다 무효화 알림(invalidation notifications)을 받을 수 있도록 지원합니다.
이 기능을 기반으로 JuiceFS는 두 가지 범주의 메타데이터를 클라이언트 메모리에 캐싱합니다:
- Inode 속성 캐시 (Inode attribute cache). Inode 번호를 키로 사용하며, 파일의 유형, 크기, 권한, 타임스탬프와 같은 전체 속성 데이터를 저장합니다. 이 캐싱은 Redis 드라이버 계층의 훅(hook) 메커니즘을 통해 투명하게 구현됩니다. 쿼리 시 먼저 로컬 캐시를 확인하며, 캐시 히트(hit) 시 네트워크 요청 없이 즉시 반환합니다. 수정 시에는 해당 캐시를 자동으로 무효화합니다. 애플리케이션 로직은 캐시의 존재를 인지할 필요가 없습니다.
- 디렉토리 엔트리 캐시 (Directory entry cache). "부모 Inode + 경로 구분자 + 파일명"을 키로 사용하여 디렉토리 조회(lookup) 결과를 캐싱합니다. Inode 속성 캐시와 달리, 엔트리 캐시의 조회 로직은 드라이버 계층에서 투명하게 가로채는 방식이 아니라 디렉토리 조회 경로에 직접 내장되어 있습니다. 디렉토리의 엔트리가 무효화되면, 접두사 매칭(prefix matching)을 사용하여 해당 디렉토리 아래의 모든 관련 캐시 엔트리를 삭제합니다. 이를 통해 경로 해석(path resolution)과 동일 디렉토리 내 자주 사용되는 엔트리에 대한 반복적인 접근을 로컬 메모리에서 처리할 수 있습니다.
클라이언트 측 캐싱을 도입하면 멀티 마운트(multi-mount) 시나리오에서 일관성 문제(consistency challenge)가 발생합니다. 여러 클라이언트가 동일한 JuiceFS 파일 시스템을 공유할 때, 한 클라이언트에서 수행되는 작업(파일 또는 디렉토리의 생성, 삭제, 이름 변경 또는 속성 업데이트)은 다른 클라이언트의 캐시된 Inode 속성이나 디렉토리 엔트리를 무효화할 수 있습니다. 효과적인 무효화 메커니즘이 없다면, 이후의 읽기 작업이 오래된(stale) 메타데이터를 참조하게 되어, 한 클라이언트에서 보이는 디렉토리 엔트리나 파일 속성이 백엔드의 실제 상태와 달라질 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

