1. 원인 분석
메시지 손실
- 생성자가 메시지를 영구적으로 저장하지 않아 시스템 다운 시 손실
- 메시지 큐가 영구화 설정이 되어 있지 않음
- 소비자가 메시지를 받았지만 처리 전에 다운되어 메시지 확인 후 손실
- 네트워크 오류, 타임아웃, 큐 가득 찰 때 등
중복 소비
- 소비자의 처리 시간 초과 또는 실패로 인해 메시지 재전송
- 메시지가 올바르게 확인되지 않아 큐에서 자동으로 재시도
- 메시지 큐의 "최소 한 번" 전달 정책
- 소비자 비즈니스 로직에서 멱등성 처리 미흡
2. 문제 해결 및 위치 파악
- 큐 모니터링: 메시지 누적, 미처리, 처리 실패, 재시도 횟수 확인
- 로그 분석: 생성, 소비, ack, nack, 예외 로그 분석
- 메시지 ID 추적: 각 메시지에 고유 ID 부여하여 손실/중복 확인
3. 완전한 해결 방안
메시지 손실 방지
생성자 측
- 메시지 영구화: RabbitMQ/Kafka 등의 메시지 영구화 지원
- 발송 확인 메커니즘: RabbitMQ의 publisher confirm, Kafka의 acks
- 실패 재시도: 발송 실패 시 자동 재시도
$msg = new AMQPMessage($data, ['delivery_mode' => 2]); // 2=영구화
$channel->basic_publish($msg, '', 'queue_name');
큐 측
- 큐 영구화: 큐를 영구화로 선언
- 고가용성 클러스터: 여러 노드, 마스터-슬레이브, 파티션, 복제본
$channel->queue_declare('queue_name', false, true, false, false); // 세 번째 매개변수 true=영구화
소비자 측
- 수동 확인: 성공 시 확인, 실패 시 미확인
- 소비 실패 nack/requeue: 실패 시 메시지를 다시 큐에 넣음
- 소비 시간 초과/다운 시 자동 재전송
$channel->basic_consume('queue_name', '', false, false, false, false, function($msg) use ($channel) {
try {
// 비즈니스 로직 처리
$channel->basic_ack($msg->delivery_info['delivery_tag']);
} catch (Exception $e) {
$channel->basic_nack($msg->delivery_info['delivery_tag'], false, true); // 재입력
}
});
중복 소비 방지
멱등성 설계
- 각 메시지에 고유 ID (예: 주문 번호, 메시지 ID, UUID)
- 소비 전 Redis나 DB에서 중복 확인, 이미 처리된 메시지는 건너뜀
- 비즈니스 로직 자체가 멱등해야 함 (예: 결제, 배송, 쿠폰 발행 등)
$msgId = $msg['id'];
if ($redis->setnx("processed_msg:$msgId", 1)) {
$redis->expire("processed_msg:$msgId", 86400);
// 비즈니스 로직 처리
} else {
// 이미 처리됨, 건너뜀
}
메시지 유일성 및 중복 제거
- 생성자가 전역적으로 고유한 메시지 ID 생성
- 소비자/비즈니스 계층에서 고유 ID를 사용하여 중복 제거
메시지 재시도 및 사망 메시지 큐
- 소비 실패 또는 시간 초과 메시지는 자동으로 재시도하며, 일정 횟수 이후 사망 메시지 큐(DLQ)로 이동
- RabbitMQ, Kafka 등은 DLQ를 지원함
모니터링 및 알림
- 메시지 누적, 소비 실패, 재시도 횟수, 사망 메시지 큐 모니터링
- 예외 발생 시 자동 알림으로 즉시 처리