네트워크 메모리 튜닝 가이드

네트워크 메모리 튜닝 가이드 (Network memory tuning guide)

Flink의 네트워크 버퍼와 인플라이트(in-flight) 데이터 양을 튜닝하는 방법, 그리고 버퍼 디블로트(buffer debloating) 메커니즘을 설명하는 문서예요.

출처: 문서

본문

개요 (Overview)

Flink의 각 레코드는 다른 레코드들과 함께 *네트워크 버퍼(network buffer)*로 합쳐져 다음 서브태스크(subtask)로 전송돼요. 이 버퍼는 서브태스크 간 통신의 가장 작은 단위예요. 일관된 높은 처리량을 유지하기 위해 Flink는 전송 과정의 입력과 출력 측에서 네트워크 버퍼 큐(network buffer queues) (일명 인플라이트 데이터 in-flight data)를 사용해요.

각 서브태스크는 데이터를 소비하기 위해 대기하는 입력 큐와 다음 서브태스크로 데이터를 보내기 위해 대기하는 출력 큐를 가지고 있어요. 인플라이트 데이터의 양이 많을수록 Flink가 파이프라인에서 더 높고 탄력적인 처리량을 제공할 수 있어요. 하지만 이로 인해 체크포인트 시간이 길어질 수 있어요.

Flink의 체크포인트는 모든 서브태스크가 주입된 체크포인트 배리어(barrier)를 모두 받을 때에만 완료될 수 있어요. 얼라인드 체크포인트(aligned checkpoints)에서 이런 체크포인트 배리어들은 네트워크 버퍼와 함께 작업 그래프 전체를 이동해요. 인플라이트 데이터의 양이 많을수록 체크포인트 배리어 전파 시간이 길어져요. 언얼라인드 체크포인트(unaligned checkpoints)에서는 인플라이트 데이터의 양이 많을수록 체크포인트 크기가 커지는데, 캡처된 모든 인플라이트 데이터가 체크포인트의 일부로 영속화되어야 하기 때문이에요.

버퍼 디블로팅 메커니즘 (The Buffer Debloating Mechanism)

이전에는 인플라이트 데이터의 양을 설정하는 유일한 방법이 버퍼 개수와 버퍼 크기를 모두 지정하는 것이었어요. 그러나 배포마다 이상적인 값이 다르기 때문에 선택하기 어려울 수 있어요. Flink 1.14에서 추가된 버퍼 디블로팅 메커니즘은 인플라이트 데이터의 양을 자동으로 합리적인 값으로 조정해 이 문제를 해결하려고 해요.

버퍼 디블로팅 기능은 서브태스크에 대해 가능한 최대 처리량(항상 바쁜 시나리오에서)을 계산하고, 그 인플라이트 데이터의 소비 시간이 설정된 값과 같아지도록 인플라이트 데이터의 양을 조정해요.

버퍼 디블로트 메커니즘은 taskmanager.network.memory.buffer-debloat.enabled 속성을 true로 설정해 활성화할 수 있어요. 인플라이트 데이터를 소비하기 위한 목표 시간은 taskmanager.network.memory.buffer-debloat.targetduration으로 설정해 구성할 수 있어요. 디블로트 타깃의 기본값은 대부분의 경우 충분해요.

이 기능은 과거 처리량 데이터를 사용해 남은 인플라이트 데이터를 소비하는 데 필요한 시간을 예측해요. 예측이 틀리면 디블로팅 메커니즘은 두 가지 방식 중 하나로 실패할 수 있어요:

  • 완전한 처리량을 제공하기에 충분한 버퍼링된 데이터가 없음.
  • 버퍼링된 인플라이트 데이터가 너무 많아 얼라인드 체크포인트 배리어 전파 시간이나 언얼라인드 체크포인트 크기에 부정적 영향을 줌.

작업에 변동하는 부하가 있으면(예: 갑작스러운 입력 레코드 급증, 주기적으로 발화하는 윈도우 집계 또는 조인), 다음 설정을 조정해야 할 수 있어요:

  • taskmanager.network.memory.buffer-debloat.period - 버퍼 크기 재계산 사이의 최소 시간 기간이에요. 기간이 짧을수록 디블로팅 메커니즘의 반응 시간은 빨라지지만 필요한 계산을 위한 CPU 오버헤드는 높아져요.
  • taskmanager.network.memory.buffer-debloat.samples - 처리량 측정이 평균화되는 샘플 수를 조정해요. 수집되는 샘플의 빈도는 taskmanager.network.memory.buffer-debloat.period로 조정할 수 있어요. 샘플이 적을수록 디블로팅 메커니즘의 반응 시간은 빨라지지만, 처리량의 갑작스러운 급증이나 하락이 발생해 버퍼 디블로팅 메커니즘이 최적의 인플라이트 데이터 양을 잘못 계산할 가능성이 높아져요.
  • taskmanager.network.memory.buffer-debloat.threshold-percentages - 잦은 버퍼 크기 변경을 방지하기 위한 최적화예요(예: 새 크기가 이전 크기와 크게 다르지 않은 경우).

자세한 내용과 추가 파라미터는 configuration 문서를 참고하세요.

현재 버퍼 크기를 모니터링하는 데 사용할 수 있는 메트릭은 다음과 같아요:

  • estimatedTimeToConsumeBuffersMs - 모든 입력 채널에서 데이터를 소비하는 총 시간
  • debloatedBufferSize - 현재 버퍼 크기

제한 사항 (Limitations)

현재 버퍼 디블로팅 메커니즘으로 자동 처리되지 않는 몇 가지 경우가 있어요.

여러 입력과 유니온 (Multiple inputs and unions)

현재 처리량 계산과 버퍼 디블로팅은 서브태스크 수준에서 일어나요.

서브태스크에 여러 개의 서로 다른 입력이 있거나, 단일하지만 유니온된 입력이 있으면, 버퍼 디블로팅은 낮은 처리량의 입력이 버퍼링된 인플라이트 데이터를 너무 많이 가지는 반면 높은 처리량의 입력은 그 처리량을 유지하기에 너무 작은 버퍼를 가질 수 있게 할 수 있어요. 이는 서로 다른 입력들의 처리량이 매우 다를 때 특히 두드러질 수 있어요. 이 기능을 테스트할 때 이런 서브태스크에 특별한 주의를 기울일 것을 권장해요.

버퍼 크기와 버퍼 개수 (Buffer size and number of buffers)

현재 버퍼 디블로팅은 최대 사용 버퍼 크기에서만 상한을 설정해요. 실제 버퍼 크기와 버퍼 개수는 그대로 남아요. 즉 디블로팅 메커니즘은 작업의 메모리 사용량을 줄일 수 없어요. 버퍼의 개수나 크기를 직접 줄여야 해요.

또한 버퍼 디블로팅이 현재 허용하는 것보다 낮게 버퍼링된 인플라이트 데이터의 양을 줄이고 싶다면, 버퍼 개수를 직접 구성할 수 있어요.

높은 병렬도 (High parallelism)

현재 버퍼 디블로팅 메커니즘은 기본 구성을 사용할 때 높은 병렬도(약 200 이상)에서 올바르게 동작하지 않을 수 있어요. 처리량 감소나 예상보다 긴 체크포인트 시간이 관찰되면, 플로팅 버퍼(taskmanager.network.memory.floating-buffers-per-gate)의 수를 기본값에서 병렬도와 같은 수 이상으로 늘릴 것을 제안해요.

문제가 발생하기 시작하는 실제 병렬도 값은 작업마다 다르지만, 보통 수백을 넘어야 해요.

네트워크 버퍼 수명 주기 (Network buffer lifecycle)

Flink는 여러 로컬 버퍼 풀을 가지고 있어요 - 출력 스트림용 하나와 각 입력 게이트용 하나. 각 버퍼 풀의 목표 크기는 다음 공식으로 계산돼요.

#channels * taskmanager.network.memory.buffers-per-channel + taskmanager.network.memory.floating-buffers-per-gate

버퍼의 크기는 taskmanager.memory.segment-size를 설정해 구성할 수 있어요.

입력 네트워크 버퍼 (Input network buffers)

목표 버퍼 풀 크기가 항상 도달되는 것은 아니에요. 버퍼를 얻지 못할 때 Flink가 실패해야 하는지 여부를 제어하는 임계값이 있어요. 이 임계값 아래에 있는 목표 버퍼 수의 일부는 필수(required)로 간주돼요. 나머지는 있다면 선택적(optional)이에요. 필수 버퍼를 얻지 못하면 태스크 실패로 이어져요. 선택적 버퍼를 얻지 못하면 태스크가 실패하지는 않지만 성능 저하가 발생할 수 있어요.

이 임계값의 기본값은 스트리밍 워크로드의 경우 Integer.MAX_VALUE, 배치 워크로드의 경우 1000이에요. 사용자가 충분한 이유가 있고 무엇을 하고 있는지 확실히 안다면이 아니면 이 임계값을 변경하지 않는 것을 권장해요. 관련 설정 옵션은 taskmanager.network.memory.read-buffer.required-per-gate.max예요. 일반적으로 임계값이 작을수록 "네트워크 버퍼 수 부족(insufficient number of network buffers)" 예외가 발생할 가능성은 줄지만, 워크로드가 조용히 성능 저하를 겪을 수 있고, 그 반대도 마찬가지예요.

출력 네트워크 버퍼 (Output network buffers)

입력 버퍼 풀과 달리, 출력 버퍼 풀은 모든 서브파티션 중에서 공유하는 한 가지 유형의 버퍼만 가져요.

과도한 데이터 스큐(skew)를 피하기 위해 각 서브파티션의 버퍼 수는 taskmanager.network.memory.max-buffers-per-channel 설정으로 제한돼요.

입력 버퍼 풀과 달리, 구성된 전용(exclusive) 버퍼와 플로팅 버퍼의 개수는 권장 값으로만 취급돼요. 사용 가능한 버퍼가 충분하지 않으면, Flink는 출력 서브파티션당 단일 전용 버퍼와 제로 플로팅 버퍼로도 진행할 수 있어요.

오버드래프트 버퍼 (Overdraft buffers)

각 출력 서브태스크는 추가로 taskmanager.network.memory.max-overdraft-buffers-per-gate(기본값 5)까지의 오버드래프트 버퍼를 요청할 수도 있어요. 이 버퍼들은 서브태스크가 다운스트림 서브태스크에 의해 백프레셔(backpressure)를 받고, 현재 하고 있는 일을 끝내기 위해 단일 네트워크 버퍼보다 더 필요한 경우에만 사용돼요. 이런 상황은:

  • 단일 네트워크 버퍼에 맞지 않는 매우 큰 레코드를 직렬화하는 경우
  • 단일 입력 레코드당 많은 출력 레코드를 생성하는 Flat Map 같은 연산자
  • 주기적으로 또는 일부 이벤트에 대한 반응으로 많은 레코드를 출력하는 연산자(예: WindowOperator의 트리거)

이런 상황에서 오버드래프트 버퍼가 없으면 Flink 서브태스크 스레드가 백프레셔에서 블록되어, 예를 들어 언얼라인드 체크포인트가 완료되지 못하게 돼요. 이를 완화하기 위해 오버드래프트 버퍼 개념이 추가됐어요. 이런 오버드래프트 버퍼는 엄밀히 선택적이며, Flink는 일반 버퍼만 사용해서도 점진적으로 진행할 수 있어요. 즉 0taskmanager.network.memory.max-overdraft-buffers-per-gate에 허용되는 구성이에요.

이 기능은 Pipelined Shuffle에만 적용돼요.

인플라이트 버퍼의 수 (The number of in-flight buffers)

전용 버퍼와 플로팅 버퍼의 기본 설정은 최대 처리량에 충분해야 해요. 인플라이트 데이터의 최소를 설정해야 한다면, 전용 버퍼를 0으로 설정하고 메모리 세그먼트 크기를 줄일 수 있어요.

버퍼 크기 선택하기 (Selecting the buffer size)

버퍼는 데이터 부분을 다음 서브태스크로 보낼 때 네트워크 오버헤드를 최적화하기 위해 레코드를 수집해요. 다음 서브태스크는 레코드를 소비하기 전에 레코드의 모든 부분을 받아야 해요.

버퍼 크기가 너무 작거나, 버퍼가 너무 자주 플러시되면(execution.buffer-timeout 설정 파라미터), 버퍼당 오버헤드가 Flink 런타임의 레코드당 오버헤드보다 상당히 높기 때문에 처리량이 감소할 수 있어요.

경험칙으로, 실제 워크로드에서 네트워크 병목을 관찰할 수 없다면 버퍼 크기나 버퍼 타임아웃을 늘리는 것에 대해 생각하지 않는 것을 권장해요(다운스트림 연산자 유휴, 업스트림 백프레셔, 출력 버퍼 큐가 가득 참, 다운스트림 입력 큐가 비어 있음).

버퍼 크기가 너무 크면 다음이 발생할 수 있어요:

  • 높은 메모리 사용
  • (언얼라인드 체크포인트의) 거대한 체크포인트 데이터
  • (얼라인드 체크포인트의) 긴 체크포인트 시간
  • 작은 execution.buffer-timeout에서 할당된 메모리를 비효율적으로 사용 (플러시된 버퍼가 부분적으로만 채워져 전송되므로)

버퍼 개수 선택하기 (Selecting the buffer count)

버퍼의 수는 taskmanager.network.memory.buffers-per-channeltaskmanager.network.memory.floating-buffers-per-gate 설정으로 구성돼요.

최상의 처리량을 위해 전용 버퍼와 플로팅 버퍼의 수에 기본값을 사용하는 것을 권장해요(제한 사항 중 하나가 아니라면). 인플라이트 데이터 양이 문제를 일으키면 버퍼 디블로팅을 활성화하는 것이 권장돼요.

네트워크 버퍼 수를 수동으로 튜닝할 수 있지만, 다음을 고려해야 해요:

  1. 예상 처리량(bytes/second)에 따라 버퍼 수를 조정해야 해요. 크레딧을 할당하고 버퍼를 보내는 데는 시간이 걸려요(두 노드 사이 약 두 번의 왕복 가량). 지연 시간은 네트워크에 따라 다르기도 해요.

버퍼 왕복 시간(건강한 로컬 네트워크에서 약 1ms), 버퍼 크기, 예상 처리량을 사용해 다음 공식으로 처리량을 유지하는 데 필요한 버퍼 수를 계산할 수 있어요:

number_of_buffers = expected_throughput * buffer_roundtrip / buffer_size

예를 들어, 예상 처리량이 320MB/s, 왕복 지연이 1ms, 기본 메모리 세그먼트 크기일 때, 예상 처리량을 달성하는 데 필요한 활성 사용 버퍼 수는 10이에요:

number_of_buffers = 320MB/s * 1ms / 32KB = 10
  1. 플로팅 버퍼의 목적은 데이터 스큐 시나리오를 처리하는 것이에요. 이상적으로는 해당 채널에 속한 플로팅 버퍼(기본값: 8)와 전용 버퍼(기본값: 2)가 네트워크 처리량을 포화시킬 수 있어야 해요. 하지만 이것이 항상 가능하거나 필요한 것은 아니에요. 태스크 매니저의 모든 서브태스크 중 단일 채널만 사용되는 경우는 매우 드물어요.
  2. 전용 버퍼의 목적은 원활한 처리량을 제공하는 것이에요. 하나의 버퍼가 전송되는 동안 다른 버퍼가 채워져요. 높은 처리량 설정에서 전용 버퍼의 수는 Flink가 사용하는 인플라이트 데이터의 양을 정의하는 주요 요소예요.

낮은 처리량 설정에서 백프레셔가 발생하면 전용 버퍼의 수를 줄이는 것을 고려해야 해요.

요약 (Summary)

Flink에서 네트워크 메모리 구성 튜닝은 버퍼 디블로팅 메커니즘을 활성화함으로써 단순화할 수 있어요. 조정이 필요할 수도 있어요.

이것이 작동하지 않으면, 버퍼 디블로팅 메커니즘을 비활성화하고 메모리 세그먼트 크기와 버퍼 수를 수동으로 구성할 수 있어요. 두 번째 시나리오에 대해 권장하는 것은:

  • 최대 처리량을 위해 기본값 사용
  • 체크포인트 속도를 높이고 네트워크 스택의 메모리 소비를 줄이기 위해 메모리 세그먼트 크기와/또는 전용 버퍼 수 줄이기

더 알아보기 (Learn more)