외부 거래소 체결 통보 처리가 멈춰서 ActiveMQ 큐에는 메시지가 쌓이는데 소비가 전혀 안 되고 있었다. @JmsListener 프로세스는 살아 있었고 자원 사용량도 전부 정상이었다.
Table of contents
Open Table of contents
증상 — 지표가 전부 정상이던 정지
프로세스가 떠 있고 브로커에 연결돼 있고 JVM 힙 사용량도 정상이었다. 인프라 관점에서 보는 지표로는 아무 문제도 나타나지 않았다.
프로세스가 살아 있는데 메시지만 안 나가는 정지는 그 지표들로는 잡히지 않는다. 그래서 소비자 로그를 처음부터 시간순으로 훑어 내려갔다.
원인 — 하나가 뒤 전부를 막는 이유
정상 처리가 이어지다가 한 지점에서 UnrecognizedPropertyException 이 나고 그 뒤로 아무것도 없었다. JMSXGroupID 로 순서를 보장하는 큐에서는 한 메시지를 처리하지 못하면 진행 위치가 넘어가지 않는다.
다음 메시지로 넘어갈 수가 없으니 뒤에 쌓인 전부가 그 하나를 기다리게 된다. 하나가 전체를 마비시키는 구조라 처리 못 하는 메시지 한 건이 곧 전면 정지였다.
필드 하나가 늘었을 뿐이었다
원인은 상대 거래소가 체결 통보 JSON 에 liquidation 필드를 하나 추가한 것이었다. Jackson 의 FAIL_ON_UNKNOWN_PROPERTIES 가 기본값 참이라 우리 DTO 가 모르는 필드를 만나면 예외를 던지게 돼 있었다.
필드 추가는 보통 하위 호환으로 여겨져서 따로 알리지 않는 경우가 많다. 그 옵션을 꺼서 모르는 필드를 무시하게 하니 막힘이 풀렸다.
조치 — 공유 컨슈머가 공유하는 실패
이 소비자는 여러 외부 거래소에서 오는 메시지를 한꺼번에 처리하고 있었다. 문제를 일으킨 것은 한 곳인데 다른 곳의 메시지도 같은 줄 뒤에 서 있었다.
제보가 온 곳은 자체 대사로 먼저 알아챈 한 곳뿐이었으므로 제보 범위가 장애 범위가 아니었다. 두 가지 방법이 있었다. 거래소마다 소비자와 큐를 전부 분리하는 것과, RedeliveryPolicy 의 maximumRedeliveries 만큼 재시도한 뒤 ActiveMQ.DLQ 로 보내고 진행 위치를 넘기게 하는 것이다. 뒤쪽을 골랐다. 이유는 ActiveMQ 공식 문서가 그 흐름을 이미 기본으로 제공해서 코드 변경이 설정 한 줄이라서다. 대신 DLQ 에 들어간 메시지를 사람이 따로 봐야 하는 것을 감수했고 큰 거래소 하나만 소비자를 분리했다.
커밋 시점과 발효 시점
이 취약점을 만든 코드 변경은 몇 달 전이었는데 장애는 최근이었다. 상대가 필드를 추가한 시점이 촉발 조건이었다.
우리 코드는 그동안 계속 취약한 상태였고 촉발 조건만 나중에 온 것이다. git log 로 최근에 무엇을 바꿨는지로만 찾으면 안 나오므로 외부 변화도 함께 봐야 했다.
정리
- 역직렬화 실패 한 건이 뒤에 쌓인 전부를 막는다
- 순서를 보장하는 큐에서는 진행 위치가 안 넘어간다
- 프로세스가 살아 있고 지표가 정상인 정지가 있다
FAIL_ON_UNKNOWN_PROPERTIES를 끄면 외부의 필드 추가에 안 깨진다- 처리 못 하는 메시지를
ActiveMQ.DLQ로 격리하고 진행 위치를 넘긴다 - 공유 소비자는 실패도 공유하므로 제보 범위가 장애 범위가 아니다
- 전파 범위와 관리 비용을 견줘 분리 정도를 정한다
- 커밋 시점과 발효 시점이 다르므로 외부 변화도 함께 본다