큐와 데이터베이스
원문은 Salvatore Sanfilippo님이 에 게재했습니다. 이 블로그 구독하기
큐는 현대 컴퓨팅에서 매우 유용한 도구이며, 웹 애플리케이션에서 다소 느릴 수 있는 연산을 나중에 처리하기 위해 자주 사용된다. 기본적으로 큐는 연산을 두 시점으로 나누는 것을 가능하게 한다. 연산이 예약되는 시점과 연산이 실행되는 시점이다. “프로듀서”는 실행할 작업을 큐에 넣고, “컨슈머” 혹은 “워커”가 큐에서 작업을 꺼내 실행한다. 예를 들어 웹 애플리케이션에서 새로운 사용자가 회원가입 과정을 완료하면, 웹 애플리케이션은 활성화 링크가 담긴 이메일을 보내기 위한 새로운 작업을 큐에 추가한다. 일시적인 네트워크 장애나 다른 오류로 인해 재시도가 필요할 수 있는 실제 이메일 전송 과정은 워커가 담당한다.
기술적으로 말하면 큐를 일종의 프로세스 간 메시징 프리미티브로 볼 수 있는데, 이때 수신 프로세스는 메시지 수신을 확인(acknowledge)해야 한다. 메시지를 보내고 잊어버리는(fire-and-forget) 방식일 수는 없다. 큐가 메시지를 큐에서 제거해도 되는지 판단해야 하므로 어떤 형태의 확인이든 반드시 필요하기 때문이다.
우리가 이야기하는 종류의 큐처럼 메시지 수신이 작업 실행을 촉발하는 경우, 메시지 수신을 언제 확인하는지에 따라 큐의 의미가 달라진다. 워커 프로세스가 메시지를 처리하기 전에 메시지 수신을 확인하면, 워커에 장애가 발생했을 때 작업이 전혀 수행되기도 전에 메시지가 유실될 수 있다. 확인을 메시지가 처리된 후에만 보낸다면, 워커 장애나 네트워크 파티션으로 인해 큐가 메시지를 다시 전달할 수도 있다. 이는 큐의 일관성 속성과 관계없이 발생한다. 따라서 강력한 일관성을 제공하는 시스템으로 큐를 모델링하더라도 이러한 불확정성은 그대로 남는다:
- 메시지를 처리하기 전에 확인하면 큐는 최대 한 번(at-most-once) 전달 속성을 갖는다. 즉, 메시지는 0번 또는 1번 처리될 수 있다.
- 메시지를 처리한 후에 확인하면 큐는 최소 한 번(at-least-once) 전달 속성을 갖는다. 즉, 메시지는 1번부터 무한대까지 처리될 수 있다.
두 경우 모두 완벽하지는 않지만, 현실에서는 두 번째 방식이 더 선호되는 경우가 많다. 가끔 작업이 전혀 실행되지 않는 시스템을 다루는 것보다 메시지 중복 전달(작업의 중복 실행)을 다루는 편이 보통 훨씬 간단하기 때문이다. 최소 한 번 전달 시스템의 예로는 Amazon SQS(Simple Queue Service)가 있다.
최소 한 번 전달 시스템이 선호되는 데에는 분산 시스템과 관련된 근본적인 이유도 있다. 다른 의미 체계인 최대 한 번 전달은 큐가 강한 일관성을 가질 것을 요구한다. 메시지가 한 번 확인되면 다른 어떤 워커도 같은 메시지를 다시 확인할 수 없어야 하는데, 이는 매우 강한 속성이기 때문이다.
관심을 최소 한 번 전달 시스템으로 옮기면, 큐를 CP 시스템으로 모델링하는 것은 낭비일 뿐 아니라 오히려 불리하다는 것을 알 수 있다:
- 어차피 최소 한 번 전달 이상은 보장할 수 없다.
- 큐는 네트워크 파티션 시 소수 파티션 쪽에서 동작할 수 있는 능력을 잃는다.
- 일관성 요구사항 때문에 큐에는 합의가 필요하므로, 정당한 이유 없이 성능을 소모하고 지연을 추가하게 된다.
메시지가 여러 번 전달될 수 있으므로, 개념적으로 우리가 원하는 것은 교환 가능한(commutative) 자료구조와 최종적 일관성을 가진 시스템이다. 메시지는 N개의 노드에 복제된 집합(set) 자료구조에 저장될 수 있으며, 이때 병합 함수는 집합 간의 합집합이 된다. 메시지 실행 후 워커가 보내는 확인 역시 개념적으로는 특정 원소가 처리되었음을 표시하는 집합의 원소이다. 이는 실제 시스템에 바로 적용하기에는 그다지 실용적이지 않은 단순한 예시이지만, 특정 종류의 큐가 분산 시스템의 특정 속성 집합으로 얼마나 잘 모델링될 수 있는지를 보여준다.
실용적인 관점에서 우리 큐가 제공하면 유용한 다른 기능들도 있다:
- 적어도 일정 시간 동안은 단일 워커에게만 전달을 보장한다. 중복 전달이 허용되더라도 가능한 한 피하고 싶다.
- 이미 처리된 메시지를 타임아웃 이후에 다시 전달하지 않도록 최선을 다해 확인한다. 다시 말하지만 이 속성을 보장할 수는 없지만, 실제로 이미 처리된 메시지를 재발급하는 일을 줄이기 위해 최대한 노력할 수 있다.
- 정상 동작 중에는 메시지를 FIFO 방식으로 처리할 수 있을 만큼 충분한 내부 상태를 유지하여, 먼저 도착한 메시지가 먼저 처리되도록 한다.
- 내부 자료구조의 자동 정리.
여기에 더해 네트워크 파티션 동안에도 메시지를 유지해야 하므로, 개념적으로는(실제로는 다른 자료구조를 사용할 수 있더라도) 전달해야 할 메시지 집합은 모든 노드의 모든 메시지들의 합집합이 된다.
안타깝게도 Redis 기반 큐 구현은 많이 존재하지만, N개의 독립적인 Redis 노드와 Redis가 제공하는 프리미티브를 이러한 특성을 가진 분산 시스템의 빌딩 블록으로 활용하려는 시도는 없었다. Redis의 자료구조와 성능, 그리고 특정한 유용한 보장을 제공하는 알고리즘을 활용한다면 사용하기 매우 실용적이고 관리와 확장이 쉬우면서도 노드당 뛰어난 성능(초당 메시지 수)을 제공하는 큐 시스템을 만들 수 있을 것이다.
이 주제가 흥미롭고 Redis를 활용하기에 아주 좋은 사례라고 생각하기 때문에, 이러한 Redis 기반 큐 시스템의 설계를 아주 천천히 진행하고 있다. 시간이 허락한다면 앞으로 몇 주 안에 무언가를 보여줄 수 있기를 바란다.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기