MySQL을 중심으로
The post Transactional Outbox message relay 개선하기 appeared first on 리디주식회사 RIDI Corporation.
||
안녕하세요. 리디 서비스백엔드팀 강규입니다.
지난 글에서 Transactional Outbox 패턴을 사용해 메시지 발행을 보장하는 message-relay를 리디에서 어떻게 운영하고 있는지 소개했습니다.
오늘은 message-relay를 운영하면서 겪은 다음 이슈를 개선한 내용을 소개하겠습니다.
많은 양의 메시지가 message 테이블에 입력되는 상황에서 처리된 메시지의 삭제가 지연되었을 때 select 쿼리의 latency가 저하되는 문제
message 테이블에 대한 select for update 쿼리와 delete 쿼리의 latency가 간헐적으로 치솟는 문제
불필요한 JOIN 제거하기
기존에 message-relay는 처리할 메시지가 기록된 message 테이블로부터 메시지를 읽어서 kafka에 발행하고 processed_message 테이블에 처리된 메시지 id를 기록하는 방식으로 동작했습니다. 이러한 구조에서는 message-relay가 처리할 메시지를 가져올 때, 아래의 쿼리가 사용됩니다.
select * from message
left join processed_message on processed_message.id = message.id
where processed_message.id is null
order by message.id asc
limit 500;
explain analyze를 이용해서 쿼리 실행 계획을 확인해 봤습니다.
-> Limit: 500 row(s) (cost=74826 rows=500) (actual time=72.9..73.9 rows=500 loops=1)
-> Filter: (processed_message.id is null) (cost=74826 rows=500) (actual time=72.9..73.9 rows=500 loops=1)
-> Nested loop antijoin (cost=74826 rows=500) (actual time=72.9..73.8 rows=500 loops=1)
-> Index scan on message using PRIMARY (cost=0.928 rows=500) (actual time=0.0188..28 rows=100499 loops=1)
-> Filter: (processed_message.id = message.id) (cost=0.25 rows=1) (actual time=399e-6..399e-6 rows=0.995 loops=100499)
-> Single-row covering index lookup on processed_message using PRIMARY (id=message.id) (cost=0.25 rows=1) (actual time=323e-6..323e-6 rows=0.995 loops=100499)
nested loop anti join 방식으로 조회하면서, driving 테이블은 message, driven 테이블은 processed_message 테이블로 결정된 것을 알 수 있습니다. message 테이블 row를 하나씩 읽어가면서 processed_message.id = message.id 조건으로 processed_message 테이블 row를 찾고 processed_message.id IS NULL 조건에 해당하는지 확인합니다.
anti join이기 때문에 해당 조건을 만족하지 않는 row가 많을수록 실행 시간이 늘어날 것을 짐작할 수 있습니다. processed_message 테이블에 있는 처리된 메시지 삭제가 비동기로 동작했기 때문에 처리된 메시지의 삭제 속도가 메시지가 새로 쌓이는 속도보다 느려질 수 있었습니다.
즉, 많은 양의 메시지가 processed_message 테이블에 쌓여서 processed_message.id = message.id AND pr

