Skip to content
isdnetworks
Go back

큐 테이블에서 워커끼리 서로 기다렸다

처리할 것을 테이블에 쌓고 워커 여러 대가 꺼내 가게 만들었다. 워커를 늘려도 처리량이 안 늘었다.

Table of contents

Open Table of contents

증상 — 워커를 늘려도 그대로였다

꺼내는 방식은 이랬다.

SELECT * FROM job_queue WHERE state = 'READY' ORDER BY no LIMIT 10 FOR UPDATE;

FOR UPDATE 로 잠그고 가져와서 처리한 뒤 state 를 바꾼다.

워커를 두 대로 늘렸는데 처리량이 그대로였고 세 대로 늘려도 같아서 늘린 것이 일을 안 하는 것처럼 보였다.

원인 — 서로 기다리고 있었다

모든 워커가 state = 'READY' 로 찾으니 같은 행을 먼저 만난다.

워커1   10건 잠금 → 처리(30초)
워커2   같은 10건 요청 → 30초 기다림 → 그다음 10건
워커3   ... 계속 밀림

FOR UPDATE 는 잠긴 행을 만나면 기다리므로 두 번째 워커는 첫 워커가 끝날 때까지 아무것도 못 한다.

늘어난 것은 대기하는 워커 수였고 동시에 처리되는 건수는 그대로라 셋을 띄우나 하나를 띄우나 순서대로 도는 것과 같았다.

조치 — 잠긴 행을 건너뛰게

SKIP LOCKED 를 붙였다.

SELECT * FROM job_queue WHERE state = 'READY'
ORDER BY no LIMIT 10
FOR UPDATE SKIP LOCKED;

SKIP LOCKED 가 잠긴 행을 건너뛰고 다음 것을 가져온다.

워커1   1~10 가져감
워커2   11~20 가져감
워커3   21~30 가져감

SKIP LOCKED 를 붙인 뒤로는 워커를 늘린 만큼 처리량이 함께 늘었다.

이 구문을 쓸 수 있는 판인지 먼저 확인해야 했고 옛 판에서는 안 되므로 그 환경에는 다른 방법이 필요했다.

대안 — 판이 낮을 때

다른 환경에 옛 판이 있어서 대안도 준비했다.

UPDATE job_queue SET state = 'TAKEN', worker = ?, taken_at = NOW()
WHERE state = 'READY' ORDER BY no LIMIT 10;

SELECT * FROM job_queue WHERE state = 'TAKEN' AND worker = ?;

TAKEN 으로 먼저 표시하고 그 worker 것만 가져오는 방식이다.

UPDATE 가 짧게 잠그므로 서로 오래 안 기다린다. 잠그고 처리하는 대신 TAKEN 이라는 표시를 남기고 잠금을 바로 푸는 셈이다.

정렬이 붙은 갱신은 판에 따라 동작이 다를 수 있어서 쓰기 전에 실제로 확인했다.

대응 — 죽은 워커와 중복 처리

워커가 처리 중에 죽으면 그 행이 TAKEN 인 채로 남는다.

UPDATE job_queue SET state = 'READY', worker = NULL
WHERE state = 'TAKEN' AND taken_at < NOW() - INTERVAL 10 MINUTE;

taken_at 이 10분을 넘으면 READY 로 되돌린다.

기준 시간은 실제 처리 시간을 보고 정했는데 평소 30초에 가끔 5분 걸리는 것이 있어 10분이면 살아 있는 것을 안 뺏는다.

READY 로 되돌린 횟수도 셌는데 그 수가 늘면 워커가 자주 죽는다는 뜻이다.

되돌리는 장치가 있으니 같은 건이 두 번 처리될 수 있다.

$done = $db->count("SELECT COUNT(*) FROM job_result WHERE job_no = ?", [$job->no]);
if ($done > 0) { return; }

job_result 에 결과를 남기고 이미 있으면 넘어간다.

job_result 를 남기는 것과 처리를 한 트랜잭션에 넣어야 이 확인이 성립하고 되돌리는 구조를 넣으면 여러 번 해도 되는 처리가 따라온다.

검증 — 쌓이는 속도와 처리 속도

state 별로 세어 봤다.

SELECT state, COUNT(*) FROM job_queue GROUP BY state;
READY   12,441
TAKEN      30
DONE   882,104

READY 가 12,441이라는 것만으로는 밀리고 있는지 알 수 없다.

14:00  READY 8,200
15:00  READY 10,100
16:00  READY 12,441

READY 를 시간별로 남기니 시간당 2,000씩 늘고 있다는 것이 보였다.

지금 값은 상태이고 시간별 값은 방향이라 방향을 봐야 워커를 늘려야 하는지가 정해진다.

DONE 이 88만 건이라 READY 조회가 느려진 것도 함께 걸렸다.

DELETE FROM job_queue WHERE state = 'DONE' AND done_at < NOW() - INTERVAL 7 DAY LIMIT 5000;

LIMIT 5000 없이 지우면 오래 잠그므로 나눠서 지웠다.

ALTER TABLE job_queue ADD INDEX ix_state_no (state, no);

인덱스도 상태와 번호로 다시 걸었다. 큐로 쓰는 표는 넣는 것만큼 지우는 것이 중요했다.

정리


Share this post on:

Previous Post
규칙이 시점에 따라 달랐다
Next Post
상태에 따라 부를 API가 다르다