사전 스로틀링
개념

스로틀링

gabury1고친 사람 github-actions[bot]

스로틀링은 일을 일부러 늦추거나 잠시 멈춰 세워서, 처리되는 양을 미리 정해 둔 상한 아래로 눌러 둡니다. 상한을 넘긴 일은 버리기보다 기다리게 하는 쪽을 먼저 고릅니다. 서버가 들어온 요청을 늦출 때도, 운영체제가 프로세스를 잠시 재울 때도 같은 이름으로 부릅니다.

쉽고 빠른 이해

스로틀링은 일이 몰릴 때 처리 속도를 일부러 늦춰서 정해 둔 상한을 못 넘게 하는 장치입니다. 한 계정의 호출을 1분에 60번까지만 흘려보내고 넘긴 일은 잠깐 붙잡아 둡니다.

이 장치가 없으면 한쪽이 몰아칠 때 나머지가 같이 늦어집니다. 처리 속도를 넘어선 일은 줄을 섭니다. 그 줄은 몰아친 쪽과 아무 상관 없는 일까지 뒤로 밉니다.

어떻게 도나:

  1. 일정 시간마다 쓸 수 있는 몫을 다시 채웁니다
  2. 일이 들어오면 남은 몫이 있는지 봅니다
  3. 있으면 통과시키고 깎습니다. 없으면 다음 채움 때까지 기다리게 합니다

대가는 둘입니다. 눌린 쪽은 평소보다 오래 기다립니다. 상한을 낮게 잡으면 감당할 수 있는 양까지 같이 묶입니다.

상세

출퇴근 시간에 사람이 몰리는 역에서는 승강장으로 내려가는 계단 앞에서 안전요원이 사람을 잠깐 세웁니다. 앞쪽이 열차를 타고 빠지면 세워 두었던 사람들을 그대로 내려보냅니다. 돌아가는 사람 없이 내려가는 차례만 뒤로 밀립니다.

이름은 자동차의 스로틀 밸브에서 왔습니다. 흘러 들어가는 양을 조절한다는 뜻이 그대로 남았습니다.

스로틀링은 단위 시간에 처리할 양의 상한을 둡니다. 그 상한에 닿으면 남은 일을 늦추거나 멈춰 세웁니다. 한 계정의 호출을 1분에 60번까지만 받는다면 예순한 번째는 다음 분까지 기다립니다.

상한이 없으면 시스템은 들어오는 대로 다 받으려 듭니다. 처리 속도보다 많이 들어온 일은 큐에 쌓입니다. 큐는 아직 처리하지 못한 일을 순서대로 세워 두는 줄입니다.

줄이 길어지면 나중에 온 일뿐 아니라 이미 들어와 있던 일까지 같이 늦어집니다. 스로틀링은 줄이 길어지기 전에 입구를 좁힙니다. 그래서 늦어지는 것은 상한을 넘긴 쪽에만 몰립니다.

노리는 것은 둘입니다. 하나는 시스템이 감당할 수 있는 만큼만 일하게 묶는 것입니다. 다른 하나는 몰아친 쪽만 늦춰서 나머지가 평소 속도를 지키게 하는 것입니다.

넘긴 일의 두 갈래

상한에 닿았을 때 넘긴 일을 어떻게 하느냐로 두 갈래가 갈립니다.

속도 제한은 넘긴 일을 거절하는 데 무게가 있습니다. 「1분에 60번까지」처럼 셈을 먼저 정해 둡니다. 넘으면 그 요청을 돌려보냅니다.

스로틀링은 넘긴 일을 붙잡아 두었다가 차례가 오면 흘려보내는 데 무게가 있습니다. 클라이언트는 거절을 받는 대신 답을 늦게 받습니다.

다만 두 낱말은 실무에서 자주 섞여 쓰입니다. 어느 이름이 붙었는지를 따지기보다 「넘긴 일을 버리나, 재우나」를 확인하는 편이 확실합니다. 그 서비스의 API(Application Programming Interface, 응용 프로그램 인터페이스) 문서가 둘 중 어느 쪽인지를 적어 둡니다.

넘긴 일이 어디로 가는지를 한 장으로 보면 이렇습니다.

flowchart TD
    A[일이 들어온다] --> B{남은 몫이 있나}
    B -->|있다| C[바로 처리한다]
    B -->|없다| D{기다리게 해도 되나}
    D -->|된다| E[다음 채움까지 재운다]
    D -->|안 된다| F[거절하고 다시 오라고 알린다]

세 갈래를 다 갖춘 구현이 흔합니다. 잠깐 재워서 될 만하면 재우고, 줄이 이미 길면 거절합니다.

요청에 거는 스로틀링

웹 API는 바깥에서 아무나 두드릴 수 있는 입구라 스로틀링을 거는 대표적인 곳입니다.

먼저 누구 몫으로 셀지를 정합니다. 계정마다 셀 수도 있고, 접속해 온 주소마다 셀 수도 있습니다. 이 기준이 없으면 한 사람이 몰아친 것과 여럿이 고르게 쓴 것을 가릴 수 없습니다.

상한에 닿으면 서버는 둘 중 하나를 합니다. 잠깐 붙잡아 두었다가 늦게 답하거나, 지금은 못 받는다고 곧바로 돌려보냅니다.

돌려보낼 때 쓰는 신호는 웹에 이미 정해져 있습니다. HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 규약)는 요청이 너무 잦다는 뜻으로 상태 코드 429를 둡니다. 다시 시도할 시각은 Retry-After 헤더로 알립니다. 이 답을 받으면 다시 보내는 책임이 재시도를 맡은 클라이언트로 넘어갑니다.

이 검사는 대개 API 게이트웨이처럼 앞단에 둡니다. 뒤에 있는 서비스마다 같은 규칙을 따로 만들면 규칙이 갈립니다. 안쪽까지 들여보낸 요청을 되돌리는 셈도 됩니다.

셋이 주고받는 순서는 이렇습니다.

sequenceDiagram
    participant C as 클라이언트
    participant GW as API 게이트웨이
    participant S as 서비스
    C->>GW: 요청
    GW->>S: 남은 몫이 있어 통과시킨다
    S-->>GW: 결과
    GW-->>C: 응답
    C->>GW: 상한을 넘긴 요청
    Note over GW: 재우기로 했으면 여기서 붙잡아 두었다가 늦게 답한다
    GW-->>C: 429 · Retry-After 로 다시 올 시각
    C->>GW: 알려 준 시각 뒤에 다시 보낸다

자원에 거는 스로틀링

운영체제도 같은 방식을 씁니다. 다만 세는 것이 요청 건수가 아니라 쓴 시간이나 오간 바이트입니다. 컨테이너 하나에 「1초 가운데 0.2초만 프로세서를 써라」라고 걸어 두는 것이 이 갈래입니다.

리눅스는 cgroup(control group, 제어 그룹)으로 프로세스 묶음마다 자원 상한을 겁니다. 프로세스 묶음은 한 컨테이너 안에서 도는 프로세스들처럼 함께 세고 함께 누를 대상입니다.

CPU(Central Processing Unit, 중앙처리장치)에 거는 상한은 시간으로 셉니다. 커널은 시간을 일정한 토막으로 자르고 그 토막마다 쓸 수 있는 시간을 나눠 줍니다. 이 토막을 주기라고 부릅니다. 주기가 새로 시작될 때마다 몫이 다시 채워집니다.

한 묶음이 그 주기의 몫을 다 쓰면 커널은 주기가 끝날 때까지 그 묶음을 실행에서 내립니다. 다음 주기가 오면 몫이 채워지고 다시 돕니다.

주기 둘을 이어 놓고 보면 실행과 멈춤이 이만큼씩 차지합니다.

block-beta
  columns 10
  a["실행 0.2초"]:1
  b["멈춤 0.8초"]:4
  c["실행 0.2초"]:1
  d["멈춤 0.8초"]:4
  e["주기 1 · 몫을 다 쓰면 주기 끝까지 멈춘다"]:5
  f["주기 2 · 새 주기가 오면 몫이 다시 채워진다"]:5

그래서 스로틀링에 걸린 프로세스는 죽지 않습니다. 메모리 상한을 넘겼을 때 커널이 프로세스를 골라 죽이는 것과 갈리는 대목입니다. 스로틀링은 멈춰 세웠다가 다시 돌려줍니다.

디스크 입출력에도 같은 방식이 걸립니다. 한 묶음이 디스크를 붙잡고 있으면 옆 묶음의 읽기가 통째로 밀립니다. 그래서 초당 넘길 수 있는 양을 미리 나눠 둡니다.

프로세서가 뜨거워졌을 때 스스로 동작 속도를 낮추는 것도 스로틀링입니다. 이쪽은 누가 걸어 준 규칙이 아니라 하드웨어가 자기를 태우지 않으려고 하는 일입니다.

남은 몫을 세는 방법

지금 얼마나 남았는지를 세는 방법은 몇 가지입니다. 어느 것을 고르느냐가 짧은 몰림을 얼마나 받아 주는지를 정합니다.

토큰 버킷은 일정 속도로 토큰이 채워지는 통을 둡니다. 일 하나가 토큰 하나를 가져갑니다. 통이 비면 채워질 때까지 기다립니다. 한가한 동안 토큰이 쌓이므로 갑작스러운 몰림을 어느 정도 받아 줍니다.

누출 버킷은 반대로 나가는 속도를 고정합니다. 들어오는 속도가 들쭉날쭉해도 나가는 흐름은 고르게 유지됩니다. 뒤쪽이 일정한 속도만 감당할 때 맞습니다.

두 통은 속도를 고정하는 곳이 서로 다릅니다.

flowchart TD
    subgraph TB["토큰 버킷 · 채우는 속도가 고정이다"]
        T1[토큰을 채운다] -->|속도 고정| T2[통에 토큰이 쌓인다]
        T2 --> T3[일이 토큰을 하나 가져간다]
        T3 --> T4[나가는 속도는 들쭉날쭉하다]
    end
    subgraph LB["누출 버킷 · 나가는 속도가 고정이다"]
        L1[들쭉날쭉하게 들어온다] --> L2[통에 담긴다]
        L2 -->|속도 고정| L3[고르게 흘러 나간다]
    end

시간을 토막 내 세는 방법도 있습니다. 1분씩 잘라 낸 구간을 창이라고 부릅니다. 한 창 동안 몇 번인지를 세고 다음 창에서 다시 0부터 셉니다.

셈이 간단한 대신 창이 바뀌는 순간이 헐겁습니다. 0시 0분 59초에 60번, 0시 1분 0초에 다시 60번을 보내면 1초 사이에 120번이 지나갑니다.

block-beta
  columns 4
  a["창 1 앞부분 · 0번"] b["창 1 끝 · 60번"] c["창 2 시작 · 60번"] d["창 2 뒷부분 · 0번"]
  space:1
  e["이 두 칸 사이는 1초 · 그 사이에 120번"]:2
  space:1

스로틀링을 거는 지점

어디에 두느냐에 따라 막을 수 있는 것이 달라집니다.

받는 쪽에 두면 자기를 지킵니다. 감당할 수 있는 양만 안으로 들입니다. 나머지는 입구에 세웁니다.

보내는 쪽에 두면 상대를 지킵니다. 남의 서비스를 호출하는 배치 작업이 초당 20건까지만 나가도록 스스로 조입니다. 상대가 거절하기 전에 우리가 먼저 속도를 맞추는 셈입니다.

받는 서버가 지금 이만큼만 보내라고 알려 주고 호출하는 쪽이 그에 맞춰 속도를 줄이면 그건 백프레셔입니다. 스로틀링은 혼자 정한 상한을 지키는 일입니다. 백프레셔는 뒷단의 사정을 앞단으로 되돌려 알리는 일입니다.

상한을 걸 수 있는 두 곳과 백프레셔가 도는 방향은 이렇습니다.

flowchart TD
    A["보내는 쪽 · 여기 걸면 상대를 지킨다"] -->|일을 보낸다| B["받는 쪽 · 여기 걸면 자기를 지킨다"]
    B -.->|지금은 이만큼만 보내라 · 백프레셔| A

눌린 쪽이 치르는 대가

스로틀링은 일을 없애 주지 않습니다. 늦출 뿐입니다. 그래서 값을 치릅니다.

첫째, 눌린 쪽의 응답이 늘어집니다. 평균은 멀쩡해 보여도 상한에 걸린 요청만 유독 오래 걸려서 꼬리 지연이 늘어납니다.

둘째, 상한을 낮게 잡으면 감당할 수 있는 몰림까지 같이 묶입니다. 시스템은 놀고 있는데 사용자는 기다리는 그림이 나옵니다.

셋째, 거절로 답하면 보낸 쪽이 곧바로 다시 보냅니다. 여럿이 같은 순간에 다시 보내면 눌러 둔 부하가 한 덩어리로 되돌아옵니다. 그래서 다시 보내는 간격을 점점 늘리는 지수 백오프를 같이 씁니다. 여럿이 같은 순간에 몰리지 않도록 그 간격에 조금씩 다른 값을 섞는 지터도 함께 씁니다.

넷째, 서버 여러 대가 한 몫을 나눠 세려면 남은 몫을 한군데 모아 두어야 합니다. 일마다 그 저장소를 한 번 더 다녀오는 값이 붙습니다.

스로틀링이 맞지 않는 경우

늦게 처리해도 답이 그대로인 일에만 맞습니다. 늦추는 사이에 답이 틀려지는 일에는 못 씁니다.

지금 이 순간의 시세를 묻는 호출을 몇 초 재웠다가 답하면 이미 맞는 답이 아닙니다. 이런 호출은 재우는 대신 곧바로 거절하고 다시 물어보게 하는 쪽이 맞습니다.

부하가 이미 감당할 수 없는 크기면 스로틀링만으로는 못 버팁니다. 입구를 좁혀도 안에서 도는 일이 안 끝나면 줄은 계속 자랍니다. 그때는 받는 양을 줄이면서 처리하는 쪽을 늘리거나 기능을 덜어 내야 합니다.

고장 난 상대를 향해서도 답이 아닙니다. 상대가 아예 죽어 있으면 늦춰서 보내 봐야 전부 실패합니다. 이럴 때는 한동안 아예 끊었다가 살아났는지 살펴보는 서킷 브레이커가 맞습니다.

관련 항목

스로틀링과 같은 뿌리에서 갈라진 억제 수단

속도 제한 · 백프레셔 · 흐름 제어 · 혼잡 제어 · 부하 제한 · 동시성 제한 · 벌크헤드 · 우아한 저하

남은 몫을 세는 데 쓰는 알고리즘

토큰 버킷 · 누출 버킷 · 고정 윈도 카운터 · 슬라이딩 윈도 카운터 · 세마포어 · 쿼터 정책

상한에 걸린 쪽에 돌아가는 신호와 그 처리

HTTP 429 · Retry-After · RateLimit 헤더 필드 · 재시도 · 지수 백오프 · 지터 · 재시도 폭풍

커널이 자원을 눌러 둘 때 같이 쓰는 기능

cgroup · 스케줄링 · 메모리 부족 킬러 · 메모리 회수 · 디스크 입출력 · CPU · 컨테이너

스로틀링을 걸어 주는 앞단 장비

API 게이트웨이 · 로드 밸런서 · 리버스 프록시 · 서비스 메시 · 웹 애플리케이션 방화벽 · nginx

스로틀링이 지키거나 깎는 성능 지표

처리량 · 응답 시간 · 꼬리 지연 · 초당 요청 수 · 대역폭 · 큐 길이

스로틀링을 잘못 걸었을 때 나는 장애

썬더링 허드 · 기아 · 타임아웃 · 캐스케이딩 장애 · 큐 적체

스로틀링의 상한을 정할 때 보는 문서

용량 계획 · 서비스 수준 목표 · 서비스 한도 · 할당량 · 서비스 수준 계약

다른 이름: throttling · 스로틀 · 요청 스로틀링 · 자원 스로틀링