[IT 심층 분석]
고속 연사 촬영 시 플래시 스토리지 I/O 병목 돌파 및 파일 시스템 버퍼 최적화
김민준 · IT 시스템 엔지니어|
순간을 놓치지 않기 위한 짐벌 촬영이나 스포츠 고속 연사 모드는 초당 수십 장의 거대한 이미지를 메모리에서 파일 시스템으로 거침없이 쏟아붓는 가장 가혹한 스트레스 조건입니다. 저희 애플리케이션 사용자 포럼에서 가장 많이 제기된 아킬레스건은 고해상도로 세팅 후 3초 이상 연사 셔터를 누르면 카메라 시스템 전체가 일시적으로 프리징되거나 앱이 죽어버린다는 불만이었습니다. 사실 초기 디버깅 로그에서는 단순한 OOM 즉 메모리 고갈로 앱이 사망한 흔적만이 찍혀 있어서 이미지 압축 알고리즘 위주로 자원을 경량화하려고 애를 썼습니다만 그것은 거대한 빙산의 지극히 일부에 불과했다는 것을 나중에서야 뼈저리게 알게 되었습니다.
가장 명확한 증명은 시스템 전체를 장악하고 I/O 상태를 모니터링하는 iostat 로그를 분석했을 때 드러났습니다. 플래시 메모리 스토리지 계층으로 내려가는 블록 디바이스의 I/O Wait 수치가 특정 시점을 기점으로 100% 한계치에 머물러 있는 것을 목도했습니다. 연사 초반부에서는 메모리에 적재된 페이지 캐시가 여유로워 버퍼에 곧바로 데이터를 내리꽂으며 앱의 응답성을 보장해주었으나 문제는 OS가 지연된 쓰기(Dirty Page Writeback) 동작을 수행하며 실제 낸드 플래시에 커밋을 일으키는 순간 발생했습니다. 쓰기 파이프라인의 폭주로 더 이상 더티 페이지를 할당받지 못한 앱단의 POSIX write 시스템 콜 루틴이 하염없이 블로킹 되어버렸고 앱의 메인 스레드마저 그 늪에 빠져 ANR(Application Not Responding)을 발생시키고 결국 강제 종료에 이른 것입니다.
해결의 실마리는 I/O 인터페이스의 전반적인 아키텍처 재구축이었습니다. 기존의 동기식 버퍼링 방식은 결코 이 속도를 감당할 수 없었기에 안드로이드 및 리눅스 환경의 코어 기법인 비동기 직접 입출력 즉 Direct I/O와 AIO(Asynchronous I/O)를 접목하는 하이브리드 엔진을 백엔드 파트 깊숙한 곳에 적용했습니다. 우선 연사로 떨어지는 이미지 페이로드를 사용자 공간의 대형 메모리 매핑 기법인 락-페이지(Pinned Memory) 저장소에 수용했습니다. 이후 I/O 전담 스레드 풀이 운영체제의 페이지 캐시를 아예 건너뛰어 스토리지 블록 장치 컨트롤러로 데이터를 직접 덤프시키는 형태를 유도했습니다. 운영체제 커널의 재매핑이나 버퍼 복사 과정을 완전히 생략해버림으로써 중간에서 증발하는 시간 지연 오버헤드를 제로화 시킨 셈입니다.
뿐만 아니라 파일 시스템의 인덱스 기록 오버헤드를 막기 위해 프레임 저널링의 부담감도 덜어냈습니다. 연사 모드가 트리거되면 다수의 낱장 파일을 생성하는 대신에 임시로 블록 단위로 예약 거대 연속 파일(Pre-allocated Blob)을 미리 할당해놓고 그 공간에 무조건 바이너리 바이트 스트림을 순차적으로 쑤어넣는 어펜드 로직을 실행했습니다. 연사가 종료된 틈을 타서 백그라운드 태스크가 이 덩어리진 블롭을 낱장 JPEG나 RAW 파일로 분할시키는 팩토리 패턴을 차용한 것이지요. 이 끔찍할 만큼 고된 아키텍처 변화 덕분에 고성능 연사 모드의 한계치는 3초에서 무제한에 가까운 수준으로 도약할 수 있었으며 I/O 장벽에 막혀 메모리 낭떠러지로 추락하던 악몽의 디버깅 시절은 화려한 벤치마크 결과만을 남긴 채 역사 속으로 자취를 감추게 되었습니다.