[JAVA] Blocking I/O, Non-Blocking I/O(NIO)와 대용량 트레픽
I/O작업이 blocking 방식으로 구현되면 하나의 클라이언트가 I/O작업을 진행하면 해당 쓰레드가 진행하는 작업을 중지하게 된다. 영향을 미치지 않게 하기 위해서 클라이언트 별로 Thread를 만들어 연결시켜주어야 한다. Thread가 많아지면 시간 할당량은 작아진다. 시간할당량이 작으면 동시에 수행되는 느낌을 가질 수 있다.
하지만 문제가 있다. 이 문제는 java 1.4이후로 NIO가 나오기 전 blocking I/O의 큰 단점이였다.
이 경우에 Thread 수는 접속자 수가 많아질 수록 Thread 수도 많아지게 된다. Thread가 많으면 CPU의 Context Switching 및 interrupt 횟수와 오버헤드 증가하게 된다. 이러한 것들이 발생할 때 cpu는 일하지 못한다. 때문에, 실제 작업하는 양에 비하여 훨씬 비효율적으로 동작하게 될 것이다. 즉, 성능에 악영향을 줄 수 있다.
그래서 NIO는 Non-Blocking방식으로 이 문제를 해결하였다. 어떻게 해결했는지 좀 깊이있게 알기위해서 현재 새벽 3시 임에도 불구하고 공부한 것을 포스팅 하고 있다.
혹시나 interrupt, Context Switching 및 PCB 등에 관하여 설명이 필요하다면, 여기서 확인 할 수 있다.
Java NIO는 어떻게 해결했을까?
우선, 간단하게 요약해보자면
Non-Blocking I/O은 I/O작업을 진행하는 동안 쓰레드의 작업을 중단시키지 않는다. 쓰레드가 커널에게 I/O를 요청하는 함수를 호출하면, 함수는 I/O를 요청한 다음 진행상황과 상관없이 바로 결과를 반환한다.
I/O는 스트림으로 단 반향으로만 가능하지만, NIO는 Channels과 Buffers를 이용해 양방향으로 가능하다. 또 Selectors가 있다.
Channels
일반적인 NIO의 I/O는 채널에서 시작된다.
Java NIO 채널은 몇 가지 차이점을 제외하고 스트림과 유사하다.
채널은 항상 데이터를 버퍼로 읽고 버퍼는 데이터를 채널에 쓴다.
- 채널을 읽고 쓸 수 있다. 스트림은 일반적으로 단방향 (읽기 또는 쓰기)이다.
- 채널은 비동기 적으로 읽고 쓸 수 있다. ( ServerSocketChannel이나 SocketChannel의 경우는 Selector를 활용해 Non-Blocking 프로그래밍이 가능하다.)
- 채널은 항상 버퍼에서 읽거나 버퍼에서 쓴다.
위에서 언급했듯이 채널에서 버퍼로 데이터를 읽고 버퍼에서 채널로 데이터를 쓴다.
Buffers
Java NIO 버퍼는 NIO 채널과 상호 작용할 때 사용된다. 버퍼는 기본적으로 데이터를 쓸 수있는 메모리 블록이며 나중에 다시 읽을 수 있다. 이 메모리 블록은 NIO Buffer 객체로 래핑되어 메모리 블록으로 작업하기 쉽게하는 일련의 메서드를 제공한다.
위의 사진을 참조하면 된다.
Channels과 Buffers를 이용한 Non-Blocking I/O - NIO
정리하자면,
자바 NIO에서는 non-blocking IO를 사용할 수 있다. 예를 들면,
하나의 스레드는 버퍼에 데이터를 읽도록 채널에 요청할 수 있다.
채널이 버퍼로 데이터를 읽는 동안 스레드는 다른 작업을 수행할 수 있다.
데이터가 채널에서 버퍼로 읽어지면, 스레드는 해당 버퍼를 이용한 processing(처리)를 계속 할 수 있다.
데이터를 채널에 쓰는 경우도 non-blocking이 가능하다.
Selectors
셀렉터를 사용하면 하나의 스레드가 여러 채널을 처리(handle)할 수 있다.
셀렉터는 사용을 위해 하나 이상의 채널을 셀렉터에 등록하고 select() 메서드를 호출해 등록 된 채널 중 이벤트 준비가 완료된 하나 이상의 채널이 생길 때까지 봉쇄(block)된다.
메서드가 반환(return)되면 스레드는 채널에 준비 완료된 이벤트를 처리할 수 있다. 즉, 하나의 스레드에서 여러 채널을 관리할 수 있으므로 여러 네트워크 연결을 관리할 수 있다. (SocketChannel, ServerSocketChannel)
커널 수준의 쓰레드 vs 사용자 수준의 쓰레드
커널 수준의 쓰레드
- 커널 수준 스레드는 커널 레벨에서 생성되는 스레드이다.
- 운영체제 시스템 내에서 생성되어 동작하는 스레드로, 커널이 직접 관리한다.
그런데 커널 수준에서는 프로세스가 주기억 장치에 여러 개가 적재되어 CPU 할당을 기다리며 동작한다.
CPU에서 인터럽트 발생으로 현재 작업 중인 프로세스가 Block 되고 다른 프로세스로 변경할 때, CPU 내 재배치 레지스터에 다음에 실행할 프로세스 정보들로 교체를 하고 캐시를 비운다. 이 것을 컨텍스트 스위칭이라고 한다.
이 컨텍스트 스위칭이 일어날 때는 CPU가 일을 못한다. 그래서 이게 자주 일어나면 성능에 영향이 발생하게 되는 단점이 있다.
하지만 커널이 직접 관리하므로 특정 스레드가 Block이 되어도 다른 스레드들은 독립적으로 일을 할 수 있다.
사용자 수준의 쓰레드
- 쓰레드 패키지를 사용자 영역에 두고 운영체제 커널은 단일 프로세스만을 관리한다.
- 쓰레드 패키지를 런타임 시스템에서 사용한다.
- 운영체제를 사용하는 입장에서는 런타임 시스템도 하나의 프로세스로 인식한다.
- 쓰레드를 운영하지 않는 운영체제제에서 실행할 수 있으므로 이식성이 뛰어나다.
- 즉, 입출력 인터럽트가 발생하면 커널은 '사용자 모드'가 되어서 사용자 수준 스레드의 응답을 기다린다. 사용자 수준 스레드의 응답이 오면 다시 '커널 모드'로 변환되어 이어서 커널 스레드가 일 처리를 하게 되는 것이다.
- 컨텍스트 스위칭이 발생하지 않는다.
Bloking I/O가 커널 수준의 쓰레드, Non-Bloking NIO가 사용자 수준의 쓰레드라고 보면 될 것 같다.
정리
Blocking I/O는 하나의 호출마다 Thread를 생성한다. 그에 따른 컨텍스트 스위칭이 발생하기 때문에 성능상 단점이 있다.
Non-Blocking IO는 요청을 받는 Thread는 오직 하나다. Thread 내부에서 채널과 버퍼를 이용하여 Non-Blocking 방식으로 진행한다. 그래서 컨텍스트 스위칭이 발생하지 않는다.
위에서 말한 사용자 수준 스레드 관점에서 봤을 때,
Context-switching은 OS 단에서 처리하는데 이것을 사용자(개발자)가 직접 처리한다는 개념에서 최적화 시킬 수 있다는 장점이 있다. 직접 처리한다는 것은 단일 쓰레드의 내부로 NIO의 채널과 버퍼를 이용해서
하지만 요청이 적다면, Blocking I/O가 더 좋다.
호출마다 thread를 생성하니 요청이 적은 서비스에는 최적의 성능을 낼 수 있다. cpu 코어 갯수많큼 쓰레드를 생성하는게 최적이다. (병렬 작업의 장점)
I/O의 요청이 많아질 때 생기는 성능상의 이유로, 많은 요청을 해결하기 위해 자바 1.4에서 NIO가 나왔고,
NIO는 대용량 트레픽 처리를 위해 꼭 알아야 하는 개념이라고 생각한다.
참고 문헌