[IT 심층 분석]
멀티 스레드 환경에서 백그라운드 이미지 썸네일 생성 시 발생한 데드락 타파 경험
김민준 · IT 시스템 엔지니어|
사용자 사진 갤러리 앱은 매끄러운 뷰어 경험을 위해 수천 장의 초고화질 사진 목록을 빠르게 스와이프할 수 있어야 하며 이를 뒷받침하는 핵심 기술은 백그라운드에서의 지능적인 비동기 썸네일 캐싱 생성기에 의존합니다. 하지만 내부 스트레스 테스트 서버에서 무작위로 만 장의 이미지를 순식간에 임포트하는 시나리오를 가동하자 갑자기 갤러리가 완전히 멈춰버리고 스레드가 꼬여 단 한 장의 사진도 띄우지 못하는 대참사가 발생했습니다. 운영체제 크래시 덤프나 JNI 레벨의 폴백 로그도 나타나지 않은 채 CPU 점유율이 0%로 추락하며 모든 자원이 얼어붙는 전형적인 교착 상태 즉 데드락(Deadlock)에 직면한 순간이었습니다. 멀티 코어를 과신한 무분별한 스레드 풀 생성과 동기화 락 관리가 초래한 고질적인 참사였죠.
원인 규명을 위해 스레드 덤프를 추출해 시각화 도구로 호출 스택 트리를 그려 추적해보았더니 그 참혹한 동기화 거미줄이 민낯을 드러냈습니다. 한쪽의 워커 스레드는 파일 경로를 기준으로 이미지 디코딩을 수행하기 위해 공용 메모리 버퍼 풀 오브젝트의 뮤텍스(Mutex) 락을 획득한 상태에서 파일 I/O 스레드의 종료를 기다리고 있었습니다. 그러나 정작 그 파일 I/O 스레드는 캐시 디렉토리의 데이터베이스 인덱스를 업데이트하는 메서드에 갇힌 채 또 다른 스레드가 점유하고 있는 버퍼 풀의 응답 콜백 락이 해제되기만을 기다리는 전형적인 환형 대기(Circular Wait) 블로킹 상태를 창조해버린 것이었습니다. 하나의 객체를 향해 얽히고설킨 락의 순서 불일치가 거대한 썸네일 생성기를 하나의 거대한 정지 화면으로 박제해버리고 말았습니다.
이 사악한 데드락의 고리를 끊기 위한 첫 번째 아키텍처 개편안은 자원 할당의 위상 정렬이었습니다. 동기화 블록으로 감싸야 할 공유 객체들의 다이어그램을 그려놓고 어떠한 상황에서도 락을 점유하는 순서가 단 하나의 방향 우상향 트리 구조만을 갖도록 코드 상의 획득 순서를 강제로 뜯어고쳤습니다. 데이터베이스 인덱스 락을 먼저 취득해야만 버퍼 풀 뮤텍스에 접근할 수 있게 획득 계층에 우선순위 룰을 씌우자 교착의 씨앗이 구조적으로 소거되었습니다. 하지만 진정한 성능을 위해 저는 여기서 한 발 더 나아가 전통적인 락 기반의 동기화 모델을 아예 내던져버리고 락 프리(Lock-Free) 컨커런트 자료구조 디자인을 도입하는 모험을 강행했습니다. 공유 버퍼를 참조하는 카운트를 원자성(Atomic) CAS(Compare-And-Swap) 연산 명령어로 완전히 치환시켜 뮤텍스 호출에 따르는 문맥 교환 비용을 제거했습니다.
추가로 스레드 풀 과다 생성 폭주를 방어하기 위해 운영체제 코어 개수에 맞추어 고정된 수의 워커 그룹을 생성하고 코루틴과 유사한 형태의 논블로킹 채널 모델로 작업 큐를 전달하여 스레드들 간의 가학적인 경쟁을 소멸시켰습니다. 이러한 설계 변경 후 무시무시한 부하 테스트 스크립트를 재구동시켰습니다. 만 장의 고해상도 이미지가 쏟아지는 혹독한 워크로드 속에서도 데드락의 공포는 완전히 소멸했고 CPU는 100% 한계치까지 자원을 끌어다 쓰며 무지막지한 속도로 썸네일 격자를 뱉어내기 시작했습니다. 다중 스레드 환경에서 무분별한 락의 남발은 결국 성능 최적화가 아니라 시스템 자살 행위에 불과하다는 굳건한 진리와 그 해결책으로서 원자적 자료구조 아키텍처 도입의 아름다움을 마주하게 된 통쾌한 디버깅이었습니다.