백엔드 데이터 인프라 109편 — Kafka Network Layer (NIO · Selector · Thread Pool)

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 109편. Kafka Network Layer — Java NIO 기반 비동기 I/O 모델. Acceptor·Processor·RequestHandler 3-thread pool, Selector multiplex 로 수만 connection 처리, num.network.threads vs num.io.threads 튜닝까지 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 109편 — Kafka Network Layer (NIO · Selector · Thread Pool)

이 글은 백엔드 데이터 인프라 시리즈 130편 중 109편이에요. 108편에서 디스크 영역을 잡았다면, 이번 109편은 네트워크 영역 — Network Layer. Kafka가 수만 클라이언트와 동시 통신하는 비밀입니다.

Network Layer가 어렵게 느껴지는 이유

대부분 Kafka 내부 동작이라 일상 코드에서 만질 일이 없어요. 다만 성능을 튜닝할 때는 이해가 필수입니다.

첫째, Java NIO(Non-blocking I/O, 비차단 입출력)·Selector(이벤트 다중화 객체) 모델 자체가 낯설어요. 전통적인 thread-per-connection(연결 하나에 스레드 하나) 방식과 작동 원리가 다릅니다.

둘째, thread pool이 3가지로 나뉘어 있습니다. Acceptor(연결 수락)·Processor(네트워크 I/O)·RequestHandler(요청 처리)가 각자 역할을 분담해요.

이 글에서는 NIO 모델·thread 구조·튜닝까지 차례로 봅니다.

핵심 모델 — NIO + 3 Thread Pool

Client → TCP connection
    ↓
[Acceptor Thread] (한 개) — connection 받음
    ↓ 라운드 로빈
[Processor Threads] (num.network.threads) — read request, write response
    ↓ request queue
[Request Handler Threads] (num.io.threads) — 실제 비즈니스 로직
    ↓ response queue
[Processor Threads] — 응답 보냄
    ↓
Client

3단계로 thread를 나눠서 각 단계가 최적화된 작업만 합니다.

1. Acceptor Thread

역할은 TCP 연결 수락(accept)만 담당해요.

  • thread 한 개만 둡니다 (listener별)
  • ServerSocketChannel.accept()만 호출
  • 새 connection을 Processor들에 round-robin(돌아가며 균등 분배)으로 분배

작업 자체가 매우 가벼워서 bottleneck이 되는 경우는 거의 없어요.

2. Processor Threads — Network I/O

역할은 TCP read/write만 처리합니다. 애플리케이션 로직은 건드리지 않아요.

  • 개수 = num.network.threads (기본 3)
  • 각 Processor가 Java NIO Selector를 한 개씩 갖고 있음
  • 수많은 connection을 한 thread가 multiplex(여러 채널을 하나로 다중 처리)함

Selector 동작

Processor 1 의 Selector:
  connection A → 데이터 도착 (read 준비)
  connection B → 응답 전송 (write 준비)
  connection C → idle
  connection D → 데이터 도착
  ...

Selector는 이벤트가 발생한 connection만 골라서 처리해요. 그래서 thread 수보다 connection 수가 훨씬 많아도 견딥니다.

Processor 의 흐름

1. Selector 가 read-ready connection 알림
2. byte 수신 → request 만들기
3. RequestChannel 의 queue 에 enqueue
4. 다음 connection 으로 이동

Processor는 response도 같은 connection으로 돌려보내요.

3. RequestHandler Threads — 실제 처리

역할은 메시지를 저장하고 조회하는 등 실제 비즈니스 로직을 담당합니다.

  • 개수 = num.io.threads (기본 8)
  • Processor들이 enqueue한 request 처리
  • 응답을 만들어 response queue에 enqueue

RequestHandler 의 작업

  • Produce request → log segment에 append → fsync(디스크 강제 flush) 또는 OS pagecache(커널 페이지 캐시)에 맡김
  • Fetch request → log segment에서 read → response
  • Metadata request → cluster 정보 반환
  • Admin request → topic 생성·config 변경

대부분 디스크 I/O가 들어가서 CPU보다 I/O bound 성격이 강해요.

RequestChannel — Queue 구조

[Processor 1, 2, 3]
       │
       ↓ enqueue request
[Request Queue] (FIFO, queued.max.requests=500)
       │
       ↓ dequeue
[Request Handler 1, 2, ..., 8]
       │
       ↓ enqueue response
[Response Queue]
       │
       ↓
[Processor 1, 2, 3]

이 queue가 backpressure(역방향 부하 제어) 역할을 합니다.

  • Request queue가 가득 차면 Processor가 새 request를 받지 않고, client가 backoff
  • Response queue가 가득 차면 RequestHandler가 새 작업을 받지 않음

Java NIO 의 핵심

Selector

Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(9092));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select();      // 이벤트 발생까지 대기
    Set<SelectionKey> keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        if (key.isAcceptable()) handleAccept(key);
        else if (key.isReadable()) handleRead(key);
        else if (key.isWritable()) handleWrite(key);
    }
}

핵심은 한 thread가 수많은 channel을 multiplex 한다는 점이에요. C10K problem(1만 동시 연결 한계 문제)을 해결합니다.

Non-Blocking

기존 thread-per-connection 방식은 10,000 connection = 10,000 thread = OS 한계에 부딪혔어요.

NIO는 수십 thread로 수만 connection을 처리해요. Kafka가 대규모로 동작할 수 있는 이유입니다.

튜닝

num.network.threads

num.network.threads=3        # 기본
# 운영 환경
num.network.threads=8        # 8~16 권장

권장값은 broker의 CPU core 수 나누기 2 정도예요.

증상:

  • NetworkProcessorAvgIdlePercent < 0.3 = 부하 큼 → 늘림
  • 0.7 = 충분 (또는 과다)

num.io.threads

num.io.threads=8             # 기본
# 운영 환경
num.io.threads=16            # 8~16+ 권장

권장값은 broker의 CPU core 수 정도입니다.

증상:

  • RequestHandlerAvgIdlePercent < 0.3 = 부하 큼 → 늘림

Queue 크기

queued.max.requests=500      # 기본
queued.max.request.bytes=-1   # -1 = 무제한 (bytes 기반)

500이면 대부분 환경에 충분해요. 너무 크면 메모리 부담이 늘고, 너무 작으면 backpressure가 폭증합니다.

Multiple Listeners — 여러 endpoint

listeners=PLAINTEXT://internal:9092,SSL://external:9093,SASL_SSL://gateway:9094

각 listener는 별도 Acceptor를 갖고 Processor pool은 공유하거나 별도로 둡니다.

Listener 별 thread 분리

listener.name.ssl.num.network.threads=10
listener.name.plaintext.num.network.threads=3

특정 listener(예: external SSL)가 부하가 크다면 별도 thread pool로 분리해요.

Inter-Broker Communication

Broker끼리 통신(replication·controller)도 같은 network layer를 씁니다.

inter.broker.listener.name=PLAINTEXT

또는 보안 환경:

inter.broker.listener.name=SSL
security.inter.broker.protocol=SSL

별도 listener를 둬서 inter-broker 트래픽을 격리하는 게 권장 패턴이에요.

모니터링 — JMX

JMX(Java Management Extensions, 자바 관리 표준)로 broker 내부 지표를 노출합니다.

Idle Percent

kafka.network:type=SocketServer,name=NetworkProcessorAvgIdlePercent
kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent

0~1 사이 값. 0.3 미만이면 thread를 늘려야 합니다.

Request Queue

kafka.network:type=RequestChannel,name=RequestQueueSize
kafka.network:type=RequestChannel,name=ResponseQueueSize

값이 지속적으로 증가하면 bottleneck이에요.

Request Latency

kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce
kafka.network:type=RequestMetrics,name=TotalTimeMs,request=FetchConsumer

Breakdown 가능:

  • RequestQueueTimeMs — request queue 에서 대기
  • LocalTimeMs — handler 처리
  • RemoteTimeMs — replication wait
  • ResponseQueueTimeMs — response queue 대기
  • ResponseSendTimeMs — 전송

어느 단계가 bottleneck인지 단계별로 끊어서 봅니다.

Connection 관련 설정

socket.send.buffer.bytes · socket.receive.buffer.bytes

socket.send.buffer.bytes=102400      # 100KB
socket.receive.buffer.bytes=102400

TCP buffer 크기예요. WAN 환경에서는 늘려줍니다 (102편 참고).

max.connections.per.ip

max.connections.per.ip=2147483647    # 무제한 (기본)
max.connections.per.ip=1000

같은 IP에서 맺을 수 있는 최대 connection 수입니다. DoS(Denial of Service, 서비스 거부 공격) 방어 용도예요.

connections.max.idle.ms

connections.max.idle.ms=600000       # 10분

Idle connection을 자동으로 끊어줍니다.

한계·실무 함정

1. Network thread 부족

NetworkProcessorAvgIdlePercent < 0.3이 지속되면 processor에 bottleneck이 걸린 상태예요. 늘려줍니다.

2. IO thread 부족

RequestHandlerAvgIdlePercent < 0.3이면 디스크·CPU 부담이 큰 거예요.

3. Request queue 폭증

producer/consumer가 너무 빠르게 요청을 쏘면 broker가 못 따라갑니다. backpressure가 걸려요.

4. SSL 활성 시 zero-copy 손실

85편 efficiency를 참고하세요. SSL은 user space CPU를 거치기 때문에 zero-copy(커널이 buffer 복사 없이 바로 전송하는 최적화)를 우회할 수 없어요. CPU 부담이 올라갑니다.

5. Multiple Listener 의 함정

listener마다 별도 port와 thread를 두면 총 thread 수가 폭증할 수 있어요. 모니터링이 필수입니다.

시험 직전 한 번 더 — Kafka Network Layer 함정 압축 노트

  • 모델 = Java NIO + 3 Thread Pool
  • Acceptor (한 개) — TCP accept 만, round-robin 분배
  • Processor (num.network.threads, 기본 3) — TCP read/write·NIO Selector
  • RequestHandler (num.io.threads, 기본 8) — 실제 비즈니스 처리
  • Selector = 한 thread 가 수많은 connection multiplex (C10K 해결)
  • RequestChannel = request/response queue (backpressure)
  • 권장 — num.network.threads = CPU core / 2 (8~16)
  • 권장 — num.io.threads = CPU core (8~16+)
  • Queue 크기 = queued.max.requests=500
  • Multiple Listeners = listener 별 endpoint (PLAINTEXT·SSL·SASL_SSL)
  • listener 별 thread pool 분리 가능
  • inter.broker.listener.name = inter-broker 트래픽 격리
  • 모니터링 핵심NetworkProcessorAvgIdlePercent·RequestHandlerAvgIdlePercent (0.3 이상)
  • Queue 모니터링 = RequestQueueSize·ResponseQueueSize
  • Request Latency Breakdown = RequestQueueTimeMs·LocalTimeMs·RemoteTimeMs·ResponseQueueTimeMs·ResponseSendTimeMs
  • Connection 설정 — socket.*.buffer.bytes (WAN 환경 늘림) · max.connections.per.ip (DoS 방어) · connections.max.idle.ms
  • 함정 — Network thread 부족 → idle percent 모니터링
  • 함정 — IO thread 부족 → 디스크·CPU 부담
  • 함정 — Request queue 폭증 → backpressure
  • 함정 — SSL 활성 시 zero-copy 손실 → CPU 부담
  • 함정 — Multiple Listener 의 thread 폭증

공식 문서: Kafka Network Layer 에서 자세한 사양을 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!