[IT 심층 분석]
레거시 C++ TCP 서버의 Epoll 파일 디스크립터(FD) 누수 현상과 쓰라린 좀비 소켓 추적기
김민준 · IT 시스템 엔지니어|
회사에 입사하고 처음 맡은 레거시 프로젝트는 C++로 작성된 낡은 자체 TCP 푸시 서버였습니다. 이 서버는 언제나 정확히 가동 7일 차가 되면 "Too many open files"라는 끔찍한 로깅을 토해내며 새로운 클라이언트의 접속을 전면 거부하는 기이한 현상이 있었습니다. 선임 개발자들은 이를 버그로 여기기보다는 주 1회 서버를 정기 재부팅하는 식의 '반창고 처방'으로 버티고 있었습니다. 하지만 제대로 된 원인 분석 없이 크론탭으로 서버를 끄고 켜는 타협은 제 엔지니어링 자존심이 도저히 허락하지 않았습니다. 저는 곧장 서버의 런타임 환경에 lsof 명령어와 strace를 던져 넣고 며칠간 그 거대한 트래픽 소켓들의 생로병사를 추적하기 시작했습니다.
관찰 결과 놀랍게도 클라이언트가 비정상적으로 네트워크 연결을 끊어버릴 때 수거 불가능한(CLOSE_WAIT 상태) 좀비 소켓들이 기하급수적으로 누적되고 있었습니다. 리눅스 epoll 멀티플렉싱 이벤트 루프 안에서 소켓의 끊어짐(EPOLLERR | EPOLLHUP) 이벤트가 올라왔을 때, 이를 예외 처리하는 catch 블록 내에서 막상 파일 디스크립터(FD)를 close() 시켜주는 단 한 줄의 코드가 누락된 것이 원인이었습니다. 개발자가 예외를 로깅하고 객체는 파괴했지만, 정작 운영체제에게 "이 네트워크 포트 번호를 반납할게"라고 말하는 걸 잊어버린 아주 고전적인 휴먼 에러였습니다. 누군가의 실수 단 한 줄이 7일마다 서버를 질식시키고 있었던 겁니다. 원인을 파악한 순간 실소가 터져 나오더군요.
저는 즉각 RAII(Resource Acquisition Is Initialization) 패턴을 도입해, 소켓을 감싸는 전용 스마트 포인터 래퍼 클래스를 기초부터 다시 짰습니다. 개발자가 실수로 close()를 잊더라도 객체의 소멸자가 불리는 순간 무조건 운영체제에 FD 반환 인터럽트를 날려버리도록 아키텍처를 원천 방어적으로 재설계했죠. 코드를 수정하고 단위 테스트를 거쳐 프로덕션 서버에 포팅한 날, 저는 일부러 재부팅 크론 탭을 삭제해버렸습니다. 그 후 서버는 한 달이 지나고 두 달이 지나도 파일 디스크립터 수치를 1,000개 이내로 완벽하고 평온하게 유지하며 미친 듯한 롱런을 보여주었습니다. 가장 기본적인 리소스 커널 원리를 무시한 코드가 어떤 파국을 맞는지, 그리고 아키텍처적 통제가 얼마나 중요한지 뼈에 새긴 날이었습니다.