strace로 8μs 차이의 동시성 버그 찾기, shaka-packager 디버깅

헤드라인

들어가며

인프랩에서는 shaka-packager로 영상을 DASH·HLS 등 스트리밍 포맷으로 패키징합니다. 해상도별 입력을 한꺼번에 넘기면 패키저가 스레드별로 병렬 처리하는데, 이 과정에서 일부 스레드가 간헐적으로 실패하는 현상이 있었습니다. 대부분은 정상인데 마지막 파일에서만 실패하고, 재시도하면 성공하는 패턴이어서 로그만으로는 원인을 좁히기 어려웠습니다. 이번에는 strace로 시스템콜을 추적해 원인을 찾고 해결한 과정을 정리했습니다.

문제 정리와 가설

에러 메시지는 `Packaging Error: 5 (FILE_FAILURE) - Cannot open file to write` 한 줄뿐이었습니다. 디스크·메모리·fd 부족, 특정 영상·버전 문제 등 가능한 가설을 차례로 검증했고, 입력이 여러 개일 때만 실패한다는 점에서 동시성 문제를 의심하게 됐습니다. 다만 애플리케이션 로그만으로는 어떤 시스템콜이 실패하는지 알 수 없어, 시스템콜 수준에서 추적하기로 했습니다.

strace로 시스템콜 추적

strace는 프로세스가 호출하는 시스템콜을 추적하는 리눅스 도구입니다. `-ff`로 fork·clone된 스레드마다 별도 로그를 남기고, `-tt`로 마이크로초 단위 타임스탬프를 붙여, 성공·실패 케이스를 시간순으로 비교할 수 있게 했습니다. 에러가 파일 쓰기와 관련돼 있었으므로 `openat`을 검색했고, 실패 케이스에서는 실패한 출력 파일에 대한 `openat` 호출 자체가 없었습니다. 즉 파일을 열려는 시도 전에 이미 실패한 다른 시스템콜이 있다는 뜻이었습니다.

mkdirat과 8μs 차이

실패한 스레드 로그에서 반환값 `= -1`인 시스템콜을 찾아보니, mkdirat으로 출력 디렉토리를 만들 때 실패가 발생하고 있었습니다. 성공 케이스에서는 한 스레드만 `mkdirat`으로 디렉토리를 만들었지만, 실패 케이스에서는 두 스레드가 거의 동시에 `mkdirat`을 호출했습니다. 한 스레드는 성공하고, 다른 스레드는 8μs 뒤에 같은 경로로 `mkdirat`을 시도했다가 EEXIST(이미 존재함)를 받았습니다. 디렉토리는 정상적으로 만들어져 있었는데, EEXIST를 받은 스레드는 그 다음 `openat` 없이 곧바로 종료되고 있었습니다.

소스 코드에서의 원인

strace로 동작을 파악한 뒤, shaka-packager 소스에서 `Cannot open file to write` 메시지를 사용하는 위치를 찾았습니다. 출력 파일을 열기 전에 부모 디렉토리가 없으면 `std::filesystem::create_directories`로 만드는 코드가 있었는데, `create_directories`는 '새로 만들었을 때만 true'를 반환하고, '이미 있어서 만들지 않았을 때'는 false를 반환합니다. 이때 에러가 난 것이 아니라면 error_code는 비어 있습니다. 그런데 코드는 반환값만 보고 false이면 무조건 실패로 처리하고, error_code를 보지 않고 있었습니다. 그래서 두 스레드가 동시에 디렉토리 생성에 들어갔을 때, 먼저 만든 스레드는 true로 통과하고, 8μs 뒤에 도착한 스레드는 EEXIST로 false를 받아 '실패'로 처리된 뒤 파일 열기까지 진행하지 않고 종료하는 동시성 버그가 재현된 것입니다.

두 스레드의 실행 흐름

정리와 해결

간헐적 실패, 단일 파일일 때는 정상, 재시도 시 성공하는 현상은 위 로직으로 모두 설명됩니다. 근본 해결은 shaka-packager에서 `create_directories` 호출 후 error_code를 확인해, EEXIST처럼 '이미 존재'인 경우는 성공으로 처리하도록 수정하는 것입니다. 당장은 패키저 실행 전에 출력 디렉토리를 미리 만들어 두는 우회책을 적용하면, `is_directory`가 true를 반환해 `create_directories`를 호출하지 않으므로 오류를 피할 수 있습니다. 우회 적용 후 해당 패키징이 안정적으로 성공하는 것을 확인했습니다.

맺음말

애플리케이션 로그의 FILE_FAILURE만으로는 원인 규명이 어려웠고, strace로 시스템콜 단위까지 내려가 보니 실패 지점이 mkdirat으로 좁혀졌고, 소스 코드의 반환값 처리 문제까지 이어졌습니다. 로그 정보가 부족한 오류를 만날 때는 strace 같은 저수준 추적을 함께 고려해 보는 것이 도움이 됩니다.