사전 백프레셔
개념

백프레셔

gabury1

백프레셔는 받는 쪽이 감당할 만큼만 들어오도록 보내는 쪽의 속도를 늦춥니다. 받는 쪽이 밀리고 있다는 것이 보내는 쪽까지 거슬러 올라가, 보내는 쪽이 스스로 덜 내보내게 만듭니다. 이름은 관에 갇힌 유체가 되밀어 내는 압력에서 왔습니다.

쉽고 빠른 이해

백프레셔는 받는 쪽이 밀리고 있다는 것을 보내는 쪽에 알려서 보내는 속도를 낮추게 하는 장치입니다. 초당 천 건이 들어오는 서버가 백 건씩만 처리할 수 있다면, 「더는 못 받는다」를 보내는 쪽에 돌려줍니다. 그러면 보내는 쪽이 밀어 넣는 양을 스스로 줄입니다.

이 장치가 없으면 보내는 쪽은 받는 쪽 형편을 모른 채 계속 밀어 넣습니다. 처리 못 한 일이 중간에 쌓이다가 메모리를 다 먹거나, 오래 기다린 요청이 끊깁니다.

어떻게 도나:

  1. 받는 쪽이 지금 더 받을 수 있는 양을 셉니다
  2. 그 양이 바닥나면 보내는 쪽으로 신호를 되돌립니다
  3. 보내는 쪽은 그만큼 보내는 속도를 낮추거나 잠시 멈춥니다

대가는 둘입니다. 보내는 쪽이 멈춰 있는 동안은 처리되는 양이 줄어듭니다. 그리고 신호가 거슬러 올라가는 데 시간이 걸립니다. 늦으면 이미 쌓인 뒤입니다.

상세

주유기 손잡이를 쥐고 있으면 기름이 들어가다가, 탱크가 가득 차는 순간 손잡이가 저절로 튕겨 나옵니다. 탱크 쪽에서 되밀려 온 압력이 손잡이를 밀어 올린 것입니다. 넣는 사람이 탱크 안을 들여다본 것이 아니라, 꽉 찼다는 것이 관을 거슬러 손끝까지 전해졌습니다.

백프레셔는 이 되밀림을 시스템에 옮겨 놓은 것입니다. 일을 받는 쪽이 「지금은 이만큼밖에 못 받는다」를 보내는 쪽에 알립니다. 보내는 쪽은 그에 맞춰 속도를 낮춥니다.

파일을 읽어 데이터베이스에 넣는 작업이 그런 예입니다. 데이터베이스가 밀리기 시작하면 파일 읽기도 같이 늦어집니다.

일을 만들어 내보내는 쪽을 생산자, 받아서 처리하는 쪽을 소비자라고 부릅니다. 곧 보내는 쪽이 생산자이고 받는 쪽이 소비자입니다. 아래에서는 이 두 이름으로만 씁니다.

둘의 속도는 대개 다릅니다. 생산자가 앞서면 그 차이만큼이 어딘가에 쌓입니다. 쌓이는 곳이 버퍼입니다. 버퍼는 속도가 다른 두 쪽 사이에 두는 임시 저장 공간입니다.

잠깐의 차이는 버퍼가 받아 주지만, 차이가 계속되면 언젠가 찹니다. 버퍼가 찬 뒤에 갈 길은 둘뿐입니다. 하나는 넘치는 일을 버리는 것이고, 다른 하나는 생산자를 멈춰 세우는 것입니다. 백프레셔는 멈춰 세우는 길입니다.

신호가 거슬러 올라가는 길

일은 앞에서 뒤로 흐릅니다. 백프레셔 신호는 그 반대 방향으로, 뒤에서 앞으로 갑니다. 그래서 이름에 「백」이 붙었습니다.

큐는 아직 처리하지 못한 일을 순서대로 세워 두는 줄입니다. 앞에서 본 버퍼에 순서를 붙인 것입니다. 먼저 들어온 일이 먼저 나갑니다.

flowchart TD
    A[생산자] -->|일을 보낸다| B[큐]
    B -->|꺼내서 처리한다| C[소비자]
    C -.->|지금은 더 못 받는다| B
    B -.->|빈 곳이 없다| A

소비자가 처지면 큐가 먼저 찹니다. 큐에 빈 곳이 없으면 생산자의 넣기가 그 앞에서 멈춥니다. 그 멈춤이 곧 신호입니다.

신호는 한 단계에서 끝나지 않습니다. 생산자 자신도 누군가의 소비자라면, 멈춰 있는 동안 자기 앞에 선 생산자에게서 일을 못 받습니다. 그래서 되밀림이 사슬을 따라 입구까지 번집니다.

flowchart TD
    I[입구] -->|일| S1[단계 1]
    S1 -->|일| S2[단계 2]
    S2 -->|일| S3[마지막 단계]
    S3 -.->|더 못 받는다| S2
    S2 -.->|더 못 받는다| S1
    S1 -.->|더 못 받는다| I

실선은 일이 내려가는 길이고 점선은 신호가 거슬러 올라가는 길입니다. 점선이 한 단계에서 끊기면 되밀림은 거기까지만 갑니다. 입구는 아무 신호도 못 받은 채 계속 밀어 넣습니다.

신호를 전하는 세 방법

막아 세우기가 가장 단순합니다. 큐에 빈 곳이 없으면 넣으려는 생산자를 거기서 멈춰 세웁니다. 따로 신호를 만들 필요가 없습니다.

남은 몫을 미리 알려 주는 방법도 있습니다. 소비자가 「지금 이만큼 더 받을 수 있다」를 숫자로 내려보냅니다. 생산자는 그 숫자만큼만 보냅니다. 소비자가 처리한 만큼 숫자를 다시 올려 줍니다.

sequenceDiagram
    participant 생산자
    participant 소비자
    소비자->>생산자: 4개까지 더 받을 수 있다
    생산자->>소비자: 일 4개를 보낸다
    Note over 생산자,소비자: 남은 몫이 0 이라 더 못 보낸다
    소비자->>생산자: 2개를 처리했다. 2개 더 받을 수 있다
    생산자->>소비자: 일 2개를 보낸다

거절과 함께 다시 올 때를 알려 주는 방법이 셋째입니다. 지금은 못 받는다고 곧바로 돌려보내되 언제 다시 오라고 덧붙입니다.

웹에서는 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 규약)가 이 신호를 상태 코드로 돌려줍니다. 상태 코드는 요청을 어떻게 처리했는지 알리는 번호입니다. 「지금은 못 받는다」에 해당하는 번호가 429입니다. 다시 올 시각은 Retry-After 헤더에 적어 보냅니다.

막아 세우는 방법과 거절하는 방법은 같은 큐에서도 어느 것을 부르느냐로 갈립니다.

큐.넣기(일)        // 빈 곳이 날 때까지 안 돌아온다
큐.넣기_시도(일)    // 빈 곳이 없으면 곧바로 false

위의 넣기는 생산자를 멈춰 세워서 백프레셔를 겁니다. 아래의 넣기_시도는 멈추지 않는 대신 거절을 돌려줍니다. 생산자가 그 답을 보고 무엇을 할지 스스로 정해야 합니다.

신호를 받은 생산자의 세 갈래

되밀림을 받은 생산자가 할 수 있는 일은 셋입니다. 빈 곳이 날 때까지 기다리거나, 들고 있던 일을 버리거나, 자기 앞 단계로 신호를 한 칸 더 넘깁니다.

flowchart TD
    A[되미는 신호를 받았다] --> B{기다려도 되나}
    B -->|된다| C[빈 곳이 날 때까지 멈춘다]
    B -->|안 된다| D{버려도 되나}
    D -->|된다| E[덜 중요한 일부터 버린다]
    D -->|안 된다| F[앞 단계로 신호를 넘긴다]

기다리기가 기본입니다. 일이 사라지지 않고 늦어지기만 하므로 치르는 값이 시간뿐입니다.

버리기는 늦으면 값이 없어지는 일에 씁니다. 몇 초 지난 시세나 화면에 잠깐 그리는 지표가 그렇습니다. 밀린 것을 다 처리하는 것보다 최신 것 하나를 보여주는 편이 맞습니다.

넘기기가 백프레셔를 시스템 전체의 성질로 만듭니다. 사슬의 어느 한 단계라도 넘기지 않고 혼자 쌓아 두면 되밀림이 거기서 끊깁니다. 그 단계가 대신 터집니다.

백프레셔가 없을 때

생산자를 안 멈추면 밀린 일은 버퍼와 큐에 쌓입니다. 병목은 전체 처리 속도를 혼자 정하는 단계입니다. 아무도 안 멈추면 그 앞의 줄이 끝없이 길어집니다.

타임아웃은 정해 둔 시간 안에 답이 안 오면 기다리기를 그만두는 것입니다. 줄이 그 시간을 넘길 만큼 길어지면 아래 고리가 돌기 시작합니다.

flowchart TD
    A[소비자가 들어오는 속도를 못 따라간다] --> B[큐가 길어진다]
    B --> C[기다리는 시간이 늘어난다]
    C --> D[기다리던 요청이 타임아웃으로 끊긴다]
    D --> E[끊긴 요청이 재시도로 다시 들어온다]
    E --> A

이 고리가 위험한 것은 한 바퀴 돌 때마다 들어오는 양이 늘기 때문입니다. 끊긴 요청은 대개 재시도로 다시 들어옵니다.

그런데 앞서 보낸 일은 큐에 그대로 남아 있습니다. 같은 일이 두 벌이 됩니다. 그만큼 줄이 더 빨리 자랍니다.

줄이 어디에 쌓이느냐에 따라 터지는 모습이 갈립니다. 메모리에 쌓이면 프로세스가 메모리를 다 먹고 죽습니다. 디스크에 쌓이면 디스크가 찹니다. 어느 쪽이든 생산자는 그때까지 아무 신호도 못 받은 채 계속 밀어 넣고 있었습니다.

이웃한 말과 가르는 선

백프레셔와 헷갈리는 이름이 넷 있습니다. 가르는 잣대는 하나입니다. 무엇을 근거로 속도를 줄이느냐입니다.

이름 무엇을 근거로 줄이나
스로틀링 · 속도 제한 자기가 미리 정한 상한
백프레셔 소비자가 지금 밀린다는 신호
흐름 제어 소비자가 지금 받을 수 있는 양
혼잡 제어 중간 길이 막혔다는 신호

흐름 제어가 넷 중 가장 넓은 이름입니다. 네트워크에서 그 일을 하는 것이 TCP(Transmission Control Protocol, 전송 제어 규약)의 수신 윈도입니다. 수신 윈도는 소비자가 지금 더 받을 수 있는 양을 숫자로 알려 주는 칸입니다. 앞에서 본 「남은 몫을 미리 알려 주는 방법」이 이것입니다.

백프레셔는 같은 생각을 프로그램과 프로그램 사이에 옮겨 놓은 말로 씁니다. 스로틀링과 속도 제한은 상대가 밀리는지 몰라도 걸 수 있다는 것이 다릅니다.

되밀지 말아야 할 때

되미는 것이 늘 답은 아닙니다. 생산자를 멈춰 세울 수 없거나, 멈춰 세워도 소용이 없는 곳이 있습니다.

생산자가 우리 것이 아닐 때가 그렇습니다. 센서가 올려 보내는 측정값이나 다른 회사가 밀어 넣는 이벤트는 멈춰 달라고 해도 안 멈춥니다. 이때는 무엇을 버릴지 규칙을 정해 두는 편이 맞습니다.

늦은 답이 틀린 답이 되는 일도 그렇습니다. 지금 이 순간의 시세를 묻는 호출을 몇 초 재웠다가 답하면 이미 맞는 답이 아닙니다. 재우는 대신 곧바로 거절하고 다시 묻게 합니다.

들어오는 양이 이미 감당할 수 있는 크기를 넘었을 때도 그렇습니다. 입구를 좁혀도 안에서 도는 일이 안 끝나면 줄은 계속 자랍니다. 그때는 소비자를 늘리거나 받는 양 자체를 줄여야 합니다.

대가

백프레셔는 일을 없애 주지 않습니다. 늦출 뿐입니다. 그래서 값을 치릅니다.

첫째, 처리량이 떨어집니다. 생산자가 멈춰 있는 동안은 아무 일도 안 만듭니다. 소비자가 이미 한가해졌어도 신호가 아직 안 풀렸으면 생산자는 멈춘 채로 있습니다.

둘째, 신호가 늦으면 이미 쌓인 뒤입니다. 되밀림이 사슬을 거슬러 입구까지 닿는 데 시간이 걸립니다. 그동안 들어온 일은 그대로 쌓입니다. 버퍼를 크게 잡을수록 이 늦음이 커집니다.

셋째, 입구가 막히면 그 앞에 서 있던 요청이 대신 기다립니다. 사람이 보는 화면이라면 화면이 멈춘 것처럼 보입니다. 그래서 어디까지 되밀고 어디서 끊어 거절할지를 미리 정해 두어야 합니다.

넷째, 멈춰 세우는 구조는 데드락을 부를 수 있습니다. 데드락은 서로가 서로를 기다리느라 둘 다 안 움직이는 상태입니다. 두 단계가 서로의 큐에 넣으려고 마주 보고 멈추면 아무도 못 빠져나옵니다.

flowchart TD
    A[단계 A] -->|B의 큐에 넣으려고 멈춘다| QB[단계 B의 큐]
    QB --> B[단계 B]
    B -->|A의 큐에 넣으려고 멈춘다| QA[단계 A의 큐]
    QA --> A

관련 항목

백프레셔와 같은 뿌리에서 갈라진 억제 수단

스로틀링 · 속도 제한 · 흐름 제어 · 혼잡 제어 · 부하 제한 · 동시성 제한 · 우아한 저하 · 벌크헤드

백프레셔가 걸리는 구간에 놓이는 저장 구조

큐 · 버퍼 · 유한 큐 · 메시지 큐 · 커넥션 풀 · 세마포어 · 링 버퍼

백프레셔를 생략했을 때 나는 장애

병목 · 포화 · 큐 적체 · 연쇄 장애 · 타임아웃 · 버퍼 오버플로 · 버퍼 블로트 · 헤드 오브 라인 블로킹 · 메모리 부족

되미는 신호를 실어 나르는 규약과 헤더

TCP · 슬라이딩 윈도 · 수신 윈도 · 제로 윈도 · 확인 응답 · HTTP 429 · Retry-After

백프레셔를 설계에 넣어 둔 처리 모델

리액티브 스트림 · 스트림 처리 · 생산자와 소비자 · 논블로킹 입출력 · 이벤트 루프 · 배치 처리

신호를 받은 생산자가 쓰는 대응 수단

재시도 · 지수 백오프 · 지터 · 서킷 브레이커 · 부하 흘리기 · 오토스케일링

백프레셔가 걸린 정도를 재는 지표

큐 길이 · 큐 대기 · 처리량 · 응답 시간 · 지연 시간 · 리틀의 법칙 · 큐잉 이론

다른 이름: backpressure · back pressure · 배압 · 역압