003 Transactional Outbox Pattern

기술 블로그 정리 #

Transactional Outbox 패턴이 등장하게된 상황 #

  • Event Driven Architecture > 메시지 발행의 신뢰성 보장 패턴
  • Event driven ARchitecture : Message Broker를 이용해 다양한 메시지(이벤트)를 publish(발행) 하고, 그에 연관된 작업을 비동기적으로 처리하여 시스템을 통합
    • DB 트랜잭션을 실행한 뒤, 연관 메시지를 Message Broker에 publish 하게 되는데, 메시지 publish가 반드시 완료되어야하는 경우가 있다.
    • 예시) 리디 주문 기능;
      • 주문이 발생하면 사용한 캐시&포인트 금액을 차감하고 상품을 지급하며 주문 완료로 상태를 바꾸는 DB 트랜잭션이 발생 -> Message Broker에 주문 완료 메시지를 publish
      • 이 경우, DB와 Message Broker의 트랜잭션 원자성 보장이 어렵다.
      • DB상 주문완료 처리가 되었떠라도 Message Broker에 메시지를 publish 하는데 실패할 수 있고, DB의 주문 완료 처리를 rollback 하기도 어렵다. img.png

Transactional Outbox 패턴을 도입한 배경 #

예시) 리디 서비스 리디 서비스는 Kafka를 중심으로 통합되고있다. 그리고 데이터 영속화를 위해서 MySQL을 사용한다. 주로 MySQL을 이용해 비즈니스 로직을 처리하고, 비동기적인 처리를 위해 메시지를 Kafka에 publish 한다.

[Kafka 도입 초기] DB 트랜잭션 완료 -> kafka 메시지 publish -> 실패한 메시지를 dead-letter-queue DB 테이블에 저장 -> 별도 batch process에서 retry 수행

  • 한계상황
    • DB 트랜잭션과 메시지를 publish를 원자적으로 처리할 수 없기 때문에, DB 트랜잭션이 완료되어도 메시지 publish를 보장할 수 없다.
    • 일정 시간 간격으로 batch process가 실행되면서 retry 하므로 실시간성이 없고, 신속하지 못하다.
    • 메시지간 publish 순서가 중요한 경우, 토픽의 경우 retry 되는 메시지가 늦게 publish 되면서 기대했던 순서가 뒤바뀔 수 있다.

Transactional Outbobx 패턴 구현 #

Polling Publisher #

  • Outbox DB 테이블에 polling 하는 것만으로 publish 할 메시지를 가져올 수 있다.
    • DB 트랜잭션이 실행되는 시점부터 publish 할 메시지 정보를 각 비즈니스 로직에서 생성하여 Outbox DB 테이블에 저장하기 때문
  • 장점
    • 전반적인 구조가 단순해서 구현하기 간편
  • 단점
    • 높은 비용의 polling이 DB 부하로 이어질 수 있음

Transaction Log Tailing #

img_1.png

  • DB 트랜잭션 로그(커밋 로그)를 테일링(tailing)하는 방법

  • 애플리케이션에서 커밋된 업데이트는 각 DB 트랜잭션 로그 항목(log entry)으로 남는다.

  • 트랜잭션 로그 마이너로 트랜잭션 로그를 읽어 변경분을 하나씩 메시지로 메시지 브로커에 발행하는 방법

    • 해당 log에 대한 CDC(Change Data Capture)를 구현
  • 단점

    • MySQL binlog에 대해 CDC를 구현하고 그 결과를 바탕으로 kafka consumer에서 사용하는 메시지 포맷으로 데이터를 생성하는 작업이 필요

구현 방법 선택 #

  • Polling Publisher
    • Message Relay 구현

실제 구현 #

  • 테이블 : message
    • Message Broker로 publish할 메시지 정보를 저장
  • 테이블 : processed_message
    • publish 되거나 publish가 불가능해서 skip하려는 message id를 저장
    • 처리 완료된 row를 임시로 저장해두었다가 삭제하는 방식 사용 용도
    • publish 완료된 메시지를 바로 삭제하지 않고 processed_message 테이블에 저장해두었다가 나중에 삭제하도록 수정
  • publish할 데이터를 가져올때 message 테이블에는 있지만, processed_message 테이블에는 없는 데이터를 가져와야 하기 때문에 LEFT JOIN을 해야한다.
    • row가 적을수록 SELECT 쿼리 성능이 좋아지므로 processed_message 테이블의 row 개수를 일정한 수준을 유지해야한다. (삭제 주기를 그에 맞게 정해야할 것 같다)
CREATE TABLE `message` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `topic` varchar NOT NULL,
  `type` varchar NOT NULL,
  `key` varchar DEFAULT NULL,
  `payload` longtext NOT NULL,
  `source` varchar NOT NULL,
  `created_at` datetime NOT NULL DEFAULT current_timestamp(),
  `updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `processed_message` (
  `id` bigint NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;