−파일을 읽는 쪽은 초당 100개를 내보내고, 읽어온 자료를 처리하는 쪽은 초당 60개를 끝낸다고 해보자. 차이가 계속되는 동안에는 초마다 40개가 쌓인다. 버퍼를 키우면 잠깐 버틸 수 있지만, 처리 속도의 차이가 유지되면 새 버퍼도 결국 찬다.
백프레셔(backpressure)는 이런 상황에서 받는 쪽의 여유가 보내는 쪽으로 전달돼, 상류의 생산이나 전송을 조절하는 방식이다. 데이터는 생산자에서 소비자로 흐르고, 더 받을 수 있는지에 관한 신호는 반대 방향으로 돌아간다. 유한한 자원을 가진 처리 단계들이 서로 속도를 맞추는 문제라고 볼 수 있다.
수신자의 여유를 발신자에게 알려주기
이 문제는 비동기 스트림 라이브러리보다 오래됐다. 1981년 TCP 명세인 RFC 793은 수신 측이 지금 받을 수 있는 바이트 수를 윈도(window)로 알려주고 송신 측이 그 범위에 맞춰 보내는 흐름 제어를 설명한다. 발신자에게 전송할 데이터가 더 있어도 수신자의 여유를 함께 고려하는 방식이다. 여기서 백프레셔라는 용어가 처음 생겼다고 단정할 수는 없지만, 받는 쪽의 제약을 보내는 쪽에 전달하는 오래된 프로토콜 사례는 확인할 수 있다. RFC 793 §2.6.
TCP가 신호를 주고받는 단위는 바이트이고, 애플리케이션의 큐는 항목 수를, 스트림 API는 요청한 원소 수를 다루기도 한다. 같은 문제를 서로 다른 계층에서 푸는 셈이다. 다만 TCP 수신 버퍼에 여유가 있다는 사실만으로 주문 처리나 파일 변환까지 끝났다고 볼 수는 없으니, 애플리케이션 내부에 무제한 큐를 두면 TCP 위에서도 작업은 계속 쌓일 수 있다.
흐름 제어와 혼잡 제어가 나뉘는 이유도 통제하려는 대상이 다르기 때문이다. 흐름 제어는 수신자의 수용 능력을 보고, 혼잡 제어는 네트워크 경로의 혼잡을 다룬다. 현재 TCP 기본 명세인 RFC 9293도 송신 윈도와 혼잡 윈도를 구별한다. RFC 9293 §3.8.2·§3.8.6.
비동기 처리에서 생기는 적체
같은 스레드에서 함수를 차례로 부르면 호출이 끝날 때까지 다음 일을 시작하지 않는 구조가 자연스럽게 속도를 묶는다. 처리를 큐나 별도 작업으로 넘기면 생산자는 소비자의 완료를 기다리지 않고 앞으로 갈 수 있는데, 그 사이의 적체를 어디에 얼마나 둘지가 새로 문제가 된다.
이 때문에 '비동기'와 '자원을 제한한다'는 것은 별개의 설계다. 소비자가 늦어졌을 때 생산자가 계속 넣을 수 있는 큐는 두 실행을 분리해주지만, 속도를 맞추는 역할까지 맡지는 않는다. 버퍼의 상한과 상류에 신호를 돌려주는 경로가 있어야 기다림이 생산 속도에 영향을 준다.
+파일을 읽는 생산자가 초당 100개를 내보내고, 다음 단계의 소비자가 초당 60개를 처리한다고 해보자. 차이가 계속되면 초마다 40개가 쌓인다. 버퍼를 키우면 잠깐 더 보관할 수 있지만, 소비자가 같은 속도로 처리하는 동안에는 그 버퍼도 결국 찬다.
백프레셔(backpressure)를 적용하면 받는 쪽의 여유에 맞춰 보내는 쪽이 생산이나 전송을 조절한다. 데이터는 생산자에서 소비자로 흐르고, 더 받을 수 있는지 알리는 신호는 상류로 돌아간다. TCP의 수신 윈도, 비동기 큐의 대기, 스트림의 요청량은 이 문제를 서로 다른 단위와 실행 방식으로 다룬다.
수신자가 받을 수 있는 양을 알려준다
1981년 TCP 명세인 RFC 793에서 수신 측은 지금 받을 수 있는 바이트 수를 윈도(window)로 알려준다. 송신 측은 보낼 데이터가 더 있어도 이 범위를 고려해 전송한다. 비동기 스트림 라이브러리가 나오기 전부터 수신자의 여유를 송신자에게 알려주는 프로토콜을 사용했던 것이다. RFC 793 §2.6.
수신 측 TCP가 데이터를 받아줬다고 해서 애플리케이션이 그 내용의 주문 처리나 파일 변환을 마친 것은 아니다. 애플리케이션이 소켓에서 계속 읽어 무제한 큐에 넣는다면 TCP 수신 버퍼를 비워주는 동안에도 내부 작업은 쌓인다. 그래서 소켓 바깥의 생산자와 소비자까지 어느 단계에서 더 기다리게 할지 연결해야 한다.
통신 분야에서는 흐름 제어와 혼잡 제어도 구분한다. 수신자는 자신의 수용 능력을 알리고, 송신 측의 혼잡 제어는 네트워크 경로에서 감당할 전송량을 다룬다. 현재 TCP 기본 명세인 RFC 9293도 송신 윈도와 혼잡 윈도를 구별한다. RFC 9293 §3.8.2·§3.8.6.
비동기로 나누면 어디에서 기다릴까
한 스레드에서 함수를 차례로 호출하면 앞선 호출이 끝날 때까지 다음 호출을 기다린다. 생산자가 작업을 큐나 별도 Task로 넘기도록 바꾸면, 소비자의 완료를 기다리지 않고 다음 일을 만들 수 있다. 그 사이의 데이터를 어디에 얼마나 보관할지도 정해야 한다.
생산자가 큐의 각 put 완료를 기다리면, 큐가 찼을 때 생산자도 다음 항목을 넣지 못하고 기다린다. 반대로 put을 수행할 Task를 계속 새로 만들면, 큐는 작게 유지해도 그 바깥에 대기하는 Task가 늘어날 수 있다.