모벤MOVE BEYOND

웹소켓 서버가 며칠에 한 번 OOM으로 죽는다면 브로드캐스트 목록부터 본다

간헐 OOM은 전송 로직이 아니라 끊긴 연결이 남아 있는 브로드캐스트 대상 목록에서 시작하는 경우가 많습니다. 확인 순서와, 동시성 결함을 재현으로 잡는 방법.

웹소켓 · 메모리 누수 · 동시성3

실시간 기능을 붙인 서버가 배포 직후에는 멀쩡하다가 며칠 뒤 워커가 메모리 부족으로 죽는다면, 브로드캐스트를 보내는 코드보다 보내는 대상 목록을 먼저 보는 편이 빠릅니다.

끊긴 연결이 목록에 남으면 팬아웃이 누적된다

저희가 만든 전사 협업 도구(일정·칸반·문서 공동 편집·채팅 등 업무 도메인 20개)에서 실시간 협업 서버가 간헐적으로 OOM을 내며 워커가 죽었습니다. 원인은 전송 로직이 아니라 목록이었습니다. 연결이 끊긴 클라이언트가 브로드캐스트 대상 목록에서 제거되지 않아, 편집 이벤트 하나가 만드는 전송 수가 실제 접속자 수보다 계속 커졌고 팬아웃이 누적돼 폭발했습니다.

확인은 두 가지면 됩니다. ① 현재 접속 수와 브로드캐스트 대상 목록의 길이를 각각 남겨 두 값이 벌어지는지 본다. ② 정상 종료가 아니라 비정상 종료(네트워크 단절, 탭 강제 종료, 절전)에서도 제거 경로가 도는지 확인한다. 대개 두 번째에서 갈립니다.

같은 코드베이스에서 조용히 커지던 것이 하나 더 있었습니다. 즉시 로딩(joinedload)이 대용량 블롭을 부모 행마다 끌어오던 지점 24곳을 selectinload로 바꿨더니 프로젝트 히스토리 저장 용량이 8.6GB에서 0.9GB가 됐고, 바꾸기 전후 반환 결과가 byte 단위로 같은지 확인한 뒤 수치를 남겼습니다.

조건문은 동시성 방어선이 아니다

"자리가 남았는지 확인하고 넣는다"는 코드는 두 요청이 확인과 삽입 사이에 끼어들면 그대로 통과합니다. 개인 프로젝트로 만든 서버에서 저희가 잡은 결함 3건도 같은 모양이었습니다. 점수 갱신의 lost update는 연결 단위 직렬화로, 라운드 이중 종료는 식별자 기반 NX 잠금으로, 정원 초과(TOCTOU)는 데이터베이스 자문 잠금(pg_advisory_xact_lock)으로 경계를 옮겨 막았습니다. 판정을 데이터베이스 쪽으로 내리면 조건문은 사용자에게 보여 줄 메시지를 고르는 역할만 남습니다.

고치기 전에 실패를 먼저 재현한다

위 3건은 수정 전에 실패를 재현하는 테스트를 먼저 만들고, 그 테스트가 실제로 깨지는 것을 확인한 뒤에 고쳤습니다. 간헐 장애는 고친 다음 "안 나더라"로는 검증되지 않습니다. 재현 없이 처음부터 통과하는 테스트는 수정을 기록하는 게 아니라 우연을 기록합니다.

다른 노트

비슷한 문제를 앞두고 계시면, 저희가 만들어 둔 화면을 먼저 열어 보시는 편이 빠릅니다.