사전 워커
개념

워커

gabury1고친 사람 github-actions[bot]

워커는 다른 프로그램이 맡긴 일을 받아서 대신 처리합니다. 일을 맡긴 쪽은 끝나기를 기다리지 않고 제 할 일로 돌아갑니다. 백엔드에서 워커라고 하면 대개 큐에서 일감을 하나씩 꺼내 끝내는 프로그램을 말합니다. 스레드나 서버 한 대도 워커라고 부릅니다. 그때는 앞에 붙은 말을 보고 뜻을 가립니다.

쉽고 빠른 이해

워커는 오래 걸리는 일을 뒤에서 도맡아 처리하는 프로그램입니다. 주문을 받은 웹 서버는 확인 메일 보내기를 큐에 넣어 둡니다. 큐는 할 일을 차례로 세워 두는 줄입니다. 웹 서버는 사용자에게 곧바로 주문 완료를 알립니다.

이게 없으면 사용자는 메일이 나갈 때까지 화면 앞에서 기다립니다. 메일 서버가 느려지면 주문 응답도 같이 느려집니다.

어떻게 도나:

  1. 워커는 큐를 지켜보다가 일감이 오면 하나를 꺼냅니다
  2. 그 일을 끝낸 뒤 큐에 끝났다고 알립니다. 큐는 이 알림을 받아야 그 일감을 지웁니다
  3. 다시 큐로 돌아가 다음 일감을 기다립니다

일이 많아지면 워커를 더 띄웁니다. 대가는 결과를 바로 받지 못한다는 점입니다.

워커가 도중에 죽으면 같은 일이 두 번 돌 수 있습니다. 일을 끝내고 끝났다고 알리기 직전에 죽으면 큐는 안 끝난 줄 알고 그 일을 다시 내줍니다. 그래서 두 번 돌아도 괜찮게 짜야 합니다.

상세

이 절은 주문 확인 메일 한 통을 워커가 보내기까지를 따라갑니다. 그 길에서 워커가 도는 순서와 도중에 죽었을 때 일감이 살아남는 방법을 봅니다.

이어서 워커를 늘리는 두 방법과 워커를 짤 때 지키는 것을 봅니다. 끝으로 같은 이름을 쓰는 다른 워커들과, 워커에게 넘기지 않는 일을 가릅니다.

일을 맡기는 쪽과 하는 쪽

택배 접수 창구의 직원은 상자를 받고 영수증을 건넵니다. 손님은 영수증을 받자마자 가게를 나섭니다. 상자는 창구 뒤 선반에 쌓입니다. 배송 기사들이 선반에서 상자를 나눠 싣고 나갑니다. 이 배송 기사가 워커입니다.

백엔드의 워커도 같은 짜임으로 일합니다. 웹 서버는 사용자의 요청을 받아 응답을 돌려줍니다. 사용자가 그 응답을 기다리는 동안 서버에서 도는 코드를 요청 경로라고 부릅니다. 이 코드가 오래 걸리면 사용자도 그만큼 기다립니다.

주문을 처리하는 코드 안에서 확인 메일까지 보내면, 메일 서버가 답할 때까지 주문 응답이 안 나갑니다. 확인 메일은 몇 초 늦게 가도 곤란할 것이 없습니다. 그래서 웹 서버는 「이 주문에 메일을 보내라」는 메모만 남기고 바로 응답합니다. 메일은 워커가 따로 보냅니다.

웹 서버와 워커 사이에는 큐가 놓입니다. 큐는 먼저 넣은 것을 먼저 꺼내 주는 줄입니다. 웹 서버가 남긴 메모는 이 줄에 섭니다.

큐에 넣는 할 일 한 건이 일감입니다. 웹 서버가 남긴 메모가 곧 일감입니다.

일감을 넣는 쪽은 생산자입니다. 여기서는 웹 서버가 생산자입니다. 일감을 꺼내 가는 쪽은 소비자입니다. 워커가 곧 소비자입니다.

작업 큐는 큐 하나를 가리키는 이름이 아닙니다. 일감을 넣는 쪽과 줄과 꺼내 가는 쪽을 통틀은 이름입니다. 생산자와 큐와 워커가 모두 들어갑니다.

sequenceDiagram
    participant U as 사용자
    participant S as 웹 서버
    participant Q as 큐
    participant W as 워커
    U->>S: 주문 요청
    S->>Q: 확인 메일 일감을 넣는다
    S-->>U: 주문 완료
    W->>Q: 다음 일감을 달라
    Q-->>W: 확인 메일 일감
    Note over W: 메일을 보낸다

그림에서 사용자는 세 번째 화살표에서 이미 응답을 받았습니다. 워커가 일감을 가져가는 것은 그 뒤입니다. 메일이 늦게 나가도 주문 응답은 늦어지지 않습니다.

워커를 따로 두면 얻는 것이 둘 더 있습니다. 하나는 실패가 번지지 않는다는 것입니다. 메일 서버가 잠깐 죽어도 주문은 성공합니다. 일감은 큐에 남아 있다가 메일 서버가 살아나면 다시 처리됩니다.

다른 하나는 따로 늘릴 수 있다는 것입니다. 주문이 몰려도 웹 서버는 일감만 넣으면 되므로 금방 응답합니다. 밀린 메일은 워커를 더 띄워 처리합니다. 웹 서버와 워커의 대수를 각자 정할 수 있습니다.

워크플로는 여러 단계를 정해진 순서로 잇는 일의 흐름입니다. 주문 접수, 결제, 배송 요청이 차례로 이어지는 과정이 한 예입니다. 이런 흐름을 돌리는 프로그램에도 워커가 들어갑니다.

워크플로의 단계 하나하나가 태스크입니다. 워크플로를 돌리는 프로그램은 어느 태스크를 언제 돌릴지만 정합니다. 태스크를 받아 실행하는 것은 워커입니다.

워커 하나가 도는 순서

워커는 켜진 뒤 꺼질 때까지 같은 순서를 되풀이합니다. 이 소절은 그 한 바퀴를 따라갑니다.

먼저 큐에 일감이 있는지 봅니다. 없으면 올 때까지 기다립니다. 잠깐 쉬었다가 다시 묻는 방식이 폴링입니다.

큐와 연결을 열어 둔 채 일감이 들어올 때까지 답을 기다리는 방식도 있습니다. 이 방식이 롱 폴링입니다. 빈 큐에 묻는 횟수가 줄어듭니다.

일감이 있으면 하나를 꺼내 처리합니다. 처리가 끝나면 워커는 큐에 끝났다고 알립니다. 이 알림이 확인 응답(ack, acknowledgment)입니다. 큐는 확인 응답을 받아야 그 일감을 지웁니다.

처리하다 실패하면 워커는 실패했다고 알리거나 확인 응답을 보내지 않습니다. 어느 쪽이든 일감은 큐에 남습니다. 그 일감은 나중에 다시 시도됩니다.

flowchart TD
    A["큐에 일감이 있나"]
    A -- 없다 --> B["올 때까지 기다린다"]
    B --> A
    A -- 있다 --> C["일감 하나를 꺼내 처리한다"]
    C --> D["성공했나"]
    D -- 예 --> E["확인 응답을 보낸다"]
    D -- 아니오 --> F["일감이 큐에 남는다"]
    E --> A
    F --> A

그림의 화살표는 어느 길로 가도 맨 위로 돌아옵니다. 워커는 꺼질 때까지 이 고리를 돕니다.

워커가 도중에 죽을 때

워커는 일을 하다 언제든 죽을 수 있습니다. 서버가 재시작되거나 프로세스가 메모리를 다 써서 강제로 끝나는 경우입니다. 이 소절은 그때 하던 일감이 어떻게 살아남는지 봅니다.

큐는 일감을 워커에게 내준 뒤에도 바로 지우지 않습니다. 처리 중이라고 표시만 해 둡니다. 확인 응답 없이 워커와의 연결이 끊기거나 정해 둔 시간이 지나면, 큐는 그 워커가 죽었다고 봅니다. 그리고 일감을 다른 워커에게 다시 내줍니다.

이 방식에도 빈틈이 하나 남습니다. 워커가 메일을 보낸 직후, 확인 응답을 보내기 직전에 죽는 경우입니다.

sequenceDiagram
    participant Q as 큐
    participant W1 as 워커 1
    participant W2 as 워커 2
    participant M as 메일 서버
    Q->>W1: 확인 메일 일감
    W1->>M: 메일을 보낸다
    Note over W1: 확인 응답 전에 죽는다
    Note over Q: 일이 안 끝났다고 본다
    Q->>W2: 같은 일감
    W2->>M: 메일을 또 보낸다

큐는 일이 안 끝났다고 보고 같은 일감을 워커 2 에게 줍니다. 고객은 확인 메일을 두 통 받습니다.

이처럼 큐는 일감을 빠뜨리지 않는 대신 두 번 내줄 수 있습니다. 이 방식을 적어도 한 번 전달이라고 부릅니다. 워커가 하는 일은 이것을 전제로 짭니다.

그래서 워커의 일은 두 번 돌아도 결과가 한 번 돈 것과 같게 만듭니다. 이 성질이 멱등성입니다. 메일이라면 보낸 주문 번호를 적어 둡니다. 이미 보낸 주문은 건너뜁니다.

오래 걸리는 일감은 하나 더 챙깁니다. 일이 정해 둔 시간보다 길어지면 큐는 멀쩡히 일하는 워커를 죽었다고 봅니다. 그러면 같은 일감이 다른 워커에게 또 갑니다.

이를 막으려고 워커는 일하는 도중에 「아직 하고 있다」는 신호를 주기적으로 보냅니다. 이렇게 살아 있음을 알리는 신호가 하트비트입니다. 큐는 신호가 올 때마다 기다리는 시간을 늘려 줍니다.

워커를 늘리는 두 방법

일감이 들어오는 속도를 워커가 못 따라가면 큐에 일감이 쌓입니다. 큐에 쌓인 일감의 건수가 큐 길이입니다. 이 소절은 워커 쪽 처리량을 올리는 두 방법을 봅니다.

첫째는 워커 프로그램을 더 띄우는 것입니다. 여러 워커가 한 큐를 같이 봅니다. 일감 하나는 그중 한 워커에게만 갑니다. 소비자, 곧 워커 여럿이 한 큐의 일감을 나눠 가져가는 이 방식이 경쟁 소비자입니다.

워커를 더 띄울 때 생산자는 손대지 않아도 됩니다. 같은 일을 하는 프로그램이나 서버의 수를 늘려 처리량을 올리는 것이 수평 확장입니다.

둘째는 워커 하나가 여러 일감을 한꺼번에 붙잡게 하는 것입니다. 그러려면 워커 안에 일을 따로 돌리는 실행 흐름이 여럿 있어야 합니다. 실행 흐름을 여럿 두는 방법은 둘입니다.

프로세스는 운영체제가 실행 중인 프로그램 하나에 붙이는 단위입니다. 워커는 프로세스를 여럿 띄울 수 있습니다. 프로세스마다 일감을 하나씩 맡깁니다.

스레드는 한 프로세스 안에서 따로 도는 실행 흐름입니다. 워커는 프로세스 하나 안에 스레드를 여럿 둘 수도 있습니다. 스레드마다 일감을 하나씩 맡깁니다.

워커 하나가 한꺼번에 붙잡는 일감의 수를 흔히 동시 실행 수라고 합니다. 실행 흐름을 셋 두면 동시 실행 수는 3입니다. 아래 그림은 동시 실행 수가 3인 워커 둘이 한 큐를 나눠 보는 모습입니다.

flowchart TD
    Q["큐"]
    subgraph W1["워커 1 · 동시 실행 수 3"]
        A1["일감"]
        A2["일감"]
        A3["일감"]
    end
    subgraph W2["워커 2 · 동시 실행 수 3"]
        B1["일감"]
        B2["일감"]
        B3["일감"]
    end
    Q --> W1
    Q --> W2

그림에서는 워커 둘이 세 건씩 붙잡아 한꺼번에 여섯 건을 처리합니다. 워커를 셋으로 늘리면 아홉 건이 됩니다. 워커는 둘 그대로 두고 동시 실행 수를 넷으로 올리면 여덟 건이 됩니다.

코어는 CPU(Central Processing Unit, 중앙 처리 장치) 안에서 명령을 따로 실행하는 단위입니다. 서버 한 대가 한꺼번에 해낼 수 있는 계산의 양은 코어 수에 묶입니다. 워커를 어떻게 늘릴지 정할 때 이 수를 봅니다.

워커 수와 동시 실행 수 중 무엇을 늘릴지는 일의 성격이 정합니다. 계산이 많은 일은 코어를 붙잡고 놓지 않습니다. 이런 일은 동시 실행 수를 코어 수보다 크게 잡아도 더 빨라지지 않습니다. 그래서 서버를 늘려 그 위에 워커를 더 띄웁니다.

기다림이 많은 일은 다릅니다. 메일 서버나 다른 서비스의 답을 기다리는 동안 코어는 놉니다. 이런 일은 워커 하나의 동시 실행 수를 코어 수보다 크게 잡아도 됩니다. 그래서 워커 수보다 동시 실행 수를 먼저 올립니다.

큐 길이를 보고 워커 수를 자동으로 늘리고 줄이기도 합니다. 이렇게 부하에 맞춰 대수를 스스로 조절하는 것이 오토스케일링입니다. 큐 길이를 기준으로 삼으면 일감이 밀렸을 때 바로 늘어납니다.

워커를 늘린다고 끝없이 빨라지지는 않습니다. 워커들이 함께 쓰는 데이터베이스나 메일 서버가 먼저 못 버팁니다. 그때부터는 워커를 더 띄워도 처리량이 안 오릅니다.

워커를 짤 때 지키는 것

워커는 여럿이 같은 큐를 나눠 봅니다. 그리고 언제든 새로 뜨고 내려갑니다. 이 소절은 그래서 생기는 두 약속을 봅니다.

첫째, 워커는 일감 사이에 기억을 남기지 않습니다. 앞 일감에서 읽은 값을 다음 일감에 쓰면, 그 일감이 다른 워커에게 갔을 때 결과가 달라집니다. 일과 일 사이에 상태를 남기지 않는 이 성질이 무상태입니다.

그래서 일감에는 무슨 일인지와 필요한 값 몇 개만 담습니다. 확인 메일 일감이라면 주문 번호 하나면 됩니다. 워커는 그 번호로 주문 내용을 데이터베이스에서 다시 읽어 옵니다. 일감이 큐에서 기다리는 사이 주문이 바뀌었어도 워커는 최신 내용으로 메일을 씁니다.

둘째, 워커는 하던 일을 마치고 내려갑니다. 새 버전을 배포하려고 워커를 바꾸는 일은 자주 있습니다. 이때 워커는 종료 신호를 받으면 새 일감을 더 받지 않습니다. 붙잡고 있던 일감을 끝내고 확인 응답까지 보낸 뒤 꺼집니다.

하던 일을 정리하고 끝내는 이 방식이 그레이스풀 셧다운(graceful shutdown)입니다. 정해 둔 시간 안에 마무리를 못 하면 워커는 강제로 꺼집니다. 그 일감은 확인 응답이 없으므로 큐에 남아 다른 워커가 다시 합니다.

stateDiagram-v2
    state "일감 기다림" as 대기
    state "처리 중" as 처리
    state "마무리 중" as 마무리
    [*] --> 대기: 워커가 뜬다
    대기 --> 처리: 일감을 꺼냄
    처리 --> 대기: 확인 응답
    대기 --> [*]: 종료 신호
    처리 --> 마무리: 종료 신호
    마무리 --> [*]: 하던 일감을 끝냄

그림에서 종료 신호는 두 곳에서 받습니다. 기다리던 워커는 바로 꺼집니다. 일하던 워커는 마무리 중을 거쳐 하던 일감을 끝낸 뒤 꺼집니다.

같은 이름을 쓰는 다른 워커

워커라는 말은 백엔드 여러 곳에서 쓰입니다. 관리하는 쪽과 일하는 쪽을 가를 때 일하는 쪽을 워커라고 부릅니다. 앞에 붙은 말이 무엇을 일꾼으로 삼는지 알려 줍니다.

이름 관리하는 쪽 워커가 하는 일
워커 프로세스 웹 서버의 주 프로세스 들어온 요청을 받아 처리한다
워커 스레드 스레드 풀 풀의 큐에서 작업을 꺼내 실행한다
워커 노드 서버 여럿을 묶은 클러스터의 관리 서버 배정받은 프로그램을 돌린다
웹 워커 브라우저에서 화면을 그리는 흐름 화면이 멈추지 않게 오래 걸리는 스크립트를 따로 돌린다

표의 워커들은 각자 항목에서 다룹니다. 이 편은 큐에서 일감을 꺼내는 워커를 다룹니다.

워커에게 넘기지 않는 일

워커에게 넘긴 일은 결과를 바로 받지 못합니다. 웹 서버는 일감을 넣은 순간 응답했으므로 그 일이 성공했는지 모릅니다. 결과를 보여 줘야 하면 워커가 결과를 데이터베이스에 적어 둡니다. 사용자 쪽은 끝났는지 다시 물어야 합니다.

그래서 사용자가 결과를 보고 다음으로 넘어가야 하는 일에는 안 맞습니다. 로그인 확인이나 잔액 조회를 워커에게 넘기면 결과를 다시 받아 오는 길만 복잡해집니다.

금방 끝나는 일도 워커에게 안 맞습니다. 큐에 넣고 꺼내는 비용이 일 자체보다 커집니다.

워커를 두면 굴릴 것도 늘어납니다. 큐를 담는 서버와 워커 프로그램을 따로 띄우고 지켜봐야 합니다. 일이 어디서 멈췄는지 찾을 때 웹 서버 기록만으로는 부족합니다.

관련 항목

워커에게 일감을 건네는 구성 요소

큐 · 작업 큐 · 생산자 · 소비자 · 메시지 · 메시지 큐 · 메시지 브로커

워커가 일감을 받고 끝내는 방식

폴링 · 롱 폴링 · 확인 응답 · 경쟁 소비자 · 재시도 · 데드 레터 큐 · 하트비트 · 타임아웃

워커가 일감을 다룰 때 지키는 성질

멱등성 · 적어도 한 번 전달 · 무상태 · 그레이스풀 셧다운

워커 처리량을 늘리는 방법

수평 확장 · 동시성 · 병렬성 · 오토스케일링 · 스레드 풀

워커가 밀리는 정도를 재는 지표

큐 길이 · 처리량 · 사용률 · 서비스율

워커가 못 따라갈 때 벌어지는 현상과 대응

큐 적체 · 헤드 오브 라인 블로킹 · 백프레셔 · 로드 셰딩

워커에게 일을 나눠 주는 상위 시스템

워크플로 · 워크플로 엔진 · 태스크 · 스케줄러 · 배치 처리 · 크론

워커가 딛고 도는 실행 단위

프로세스 · 스레드 · 코어 · CPU

워커라는 이름을 나눠 쓰는 다른 실행 단위

워커 프로세스 · 워커 스레드 · 워커 노드 · 웹 워커 · 서비스 워커

워커를 띄워 쓰는 작업 큐 도구

Celery · Sidekiq · BullMQ · Temporal

다른 이름: worker · 백그라운드 워커 · 잡 워커