TL;DR
- zip 파일은 구조상 원하는 파일 하나만 읽기 위해선 random access가 필요하진 않다.
- s3는 byte-range 헤더를 지원한다.
- 적절한 파싱을 하면 압축파일에서 필요한 파일의 데이터만 받아올 수 있다.
- Node.js 라이브러리 unzipper 를 사용하면 다 해준다.
상황
현재 운영중인 서비스에는 약 800MB 크기의 zip 파일에서 특정 폴더만 추출하는 로직이 있다.
서비스의 주요기능으로 운영자가 작성한 컨텐츠를 유저에게 보여주는것이 있는데, 이 컨텐츠가 외부시스템에서 관리되며 실시간 쿼리가 불가능한 상황이라 컨텐츠의 변경이 발생할 때마다 전체 폴더를 하나의 zip 파일로 압축해 받아오고 있다.
그리고 관리자툴에서 컨텐츠의 정보를 등록하며 zip 파일 내의 폴더 하나를 선택하면 해당 폴더만 따로 작은 zip 파일로 압축해 유저들에게 제공되는 형태이다.
기존엔 위 로직을 컨텐츠가 업데이트 될 때마다 hook 을 받아 전체 zip 파일을 s3에 저장해두고 필요할 때 마다 서버가 s3에서 받아 압축을 풀어 특정 폴더만 재 압축하는 형태로 구현했었다. 이게 초창기엔 압축파일이 5-60MB 정도여서 큰 문제가 안됐는데, 시간이 지나며 800MB가 넘는 크기가 되어 관리자가 자료 하나를 등록하는데 1-20 초 정도의 시간이 걸리는 문제가 발생하였다.
이게 단지 로직에 필요한 데이터 용량이 크기에 시간이 걸리는 문제가 아니었던게, 800MB 가 넘는 파일을 받아왔지만 실제로 로직에 사용되는 데이터는 5MB도 안되는 경우가 허다했다. 다운받은 데이터의 99% 는 사용하지도 않고 날리고 있었다.
그리고, 단지 시간이 오래걸리는 것 뿐만이 아니라 아래와 같은 문제들도 같이 발생하고 있었다.
- s3에서 다운로드 시간이 오래걸림 (10~20초)
- 서버가 gcp에 있어서 s3 egress network 요금이 불필요하게 사용됨
- 압축해제된 내용을 메모리에 담아둬서 서버 인스턴스의 RAM 사용량 급증
- 요청 처리 시간 증가 + RAM 사용량 증가로 cloud run 비용 증가
- 큰 파일 압축해제에 따른 CPU 사용량 증가로 동시 요청 처리 능력 저하
Zip 파일 구조
Zip파일 구조를 보면, 각 파일 데이터는 serial 하게 붙어있고, 맨 마지막에 central directory 라는 부분에 모든 file entry의 lookup table이 존재한다.
S3 byte-range header
S3는 byte-range 헤더를 지원한다. 특정 object 의 fetch 를 할 때 byte-range 를 지정해 해당 데이터만 받아올 수 있다.
해결
필요한건 특정 폴더 내부의 모든 파일들이기에 원본 zip 파일의 맨 뒷부분(Central directory)을 읽어 (약 3-4MB 정도) lookup table 을 파싱하고 필요한 파일들의 byte address 를 이용해 해당 부분의 데이터만 읽어오는 형태로 해결 할 수 있었다.
그리고 다행히 이러한 로직을 구현해 둔 라이브러리가 있었다. unzipper.js