백엔드 데이터 인프라 109편. Kafka Network Layer — Java NIO 기반 비동기 I/O 모델. Acceptor·Processor·RequestHandler 3-thread pool, Selector multiplex 로 수만 connection 처리, num.network.threads vs num.io.threads 튜닝까지 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 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 waitResponseQueueTimeMs— 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 에서 자세한 사양을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 104편 — Kafka Hardware · OS (CPU·메모리·디스크·튜닝)
- 105편 — Kafka KRaft (Zookeeper 의 후속 · Quorum 운영)
- 106편 — Kafka Tiered Storage (S3 · 무한 Retention)
- 107편 — Kafka Log Compaction (Key 별 최신만 유지)
- 108편 — Kafka Log 파일 구조 (Segment · Index · Offset)
다음 글: