블로킹
- 블로킹이란 작업이 완료될때까지 스레드가 대기하는 것을 의미한다.
- 주로 데이터의 입출력 과정에서 블로킹이 발생하기 때문에 블로킹 IO 라고도 한다.
컨텍스트 스위칭
- 운영체제는 여러 스레드를 번갈아 가면서 CPU에 할당한다.
- CPU가 스레드를 전환하려면 현재 실행중인 스레드의 상태를 기록하고 다음에 실행할 스레드의 상태 정보를 불러와야 한다.
트래픽이 증가하면 다음 2가지 이유로 자원 효율이 떨어진다.
- IO대기와 컨텍스트 스위칭에 따른 CPU 낭비
- 요청마다 스레드를 할당함으로써 메모리 사용량이 높음
→ 톰캣과 같은 요청마다 스레드를 할당하는 서버를 사용하면 CPU나 메모리 낭비가 많을 수 있음.
하지만 이정도의 트래픽을 일으키는 서버는 많지 않이게 현재에도 많이 쓰인다.
서버 성능을 높이는 다른 방법은 자원 효율을 높이는 것이다. IO 대기로 인한 CPU의 자원 낭비를 줄이고 요청을 처리하는데 드는 메모리를 줄이는 것이다.
- 가상 스레드나 고루틴같은 경량 스레드 사용
- 논블로킹 또는 비동기 IO 사용
가상스레드 사용하기
- 자바의 가상 스레드나 GO 언어의 고루틴을 사용하면 입출력동안 스레드가 대기하지 않고 다른 일ㅇ르 할 수 있다.
- 경량 스레드를 사용하면 OS가 사용하는 스레드가 아니라 런타임이 관리하는 스레드를 사용하기 때문에 OS가 관리하는 스레드보다 더 적은 자원을 사용한다.
- JVM은 기본적으로 풀에 CPU 코어 개수만큼 플랫폼 스레드를 생성한다.
- 가상 스레드는 수백 바이트에서 수 KB, 수십 KB 까지 힙 메모리를 사용한다. 호출 스택의 깊이에 따라 사용하는데 메모리를 동적으로 늘렸다가 줄인다.
- 스레드를 생성하는 시간도 차이가 많이난다.
- 그렇기에 톰캣처럼 요청별 스레드를 생성하는 서버에서 가상 스레드를 사용하면 획기적으로 많은 메모리를 아낄수 있다.
- 한개의 캐리어 스레드가 여러 가상 스레드를 실행하게 된다. 특정 가상 스레드가 캐리어 연결되는 것을 마운트, 연결이 해제되는 것을 언마운트 라고 한다.
네트워크 IO 와 가상 스레드
- 가상 스레드는 실행하는 과정에서 블로킹되면 플랫폼 스레드와 언마운트되고 다른 실행 대기중인 플랫폼 스레드와 연결된 뒤 실행을 재개한다.
- 블로킹 연산에는 IO 기능, ReentrantLock, Thread.sleep() 등이 포함된다. 이들 연산을 사용해서 가상 스레드가 블로킹되면 플랫폼 스레드는 대기 중인 다른 가상 스레드를 실행한다.
- 반면에 자바 23 또는 이전 버전에서 synchronized 인해 블로킹되면 가상 스레드는 플랫폼 스레드로부터 언마운트 되지 않는다.
- 이렇게 가상 스레드가 플랫폼 스레드까지 블로킹할 때 이를 가상 스레드가 플랫폼 스레드에 고정됐다 라고 한다.
가상 스레드와 성능
- IO 중심 작업과 CPU 중심 작업이 있을 수 있으나 가상 스레드가 효과를 볼 수 있는 작업은 IO 중심 작업임.
- IO 중심 작업일 때 플랫폼 스레드가 CPU 낭비 없이 효율적으로 가상 스레드를 실행할 수 있음
- 반면에 CPU 중심 작업에 가상 스레드가 쓰이면 성능 개선 효과를 얻을 수 없음 오히려 나빠짐
- 또한 IO 중심 작업이라고 해서 무조건 성능에 이점을 가지는 것은 아님.
- 스케줄링에 사용되는 플랫폼 스레드 개수보다 가상 스레드의 개수가 많아야 효과를 기대할 수 있음.
- 가상 스레드가 이점을 얻을 수 있는 부분은 처리량임.
가상 스레드의 장점
- 가상스레드의 장점은 기존 코드를 크게 수정할 필요가 없다는 것임.
논블로킹 IO 로 성능 높이기
- 사용자가 폭발적으로 증가하면 어느 순간 경량 스레드로는 한계가 온다. 이때에는 서버의 IO 구현 방식을 논블로킹 IO 로 변경해야한다.
논블로킹 IO 의 동작과정
- 논블로킹 IO는 입출력이 끝날때까지 스레드가 대기하지 않는다.
- 논블로킹 IO 를 사용하는 코드는 대기하지 않고 다음 줄의 코드가 바로 실행된다.
- 그렇기에 논블로킹 IO 를 사용할 때에는 데이터 읽기를 바로 시도하기 보다 어떤 연산을 수행할 수 있는지 확인 후 해당 연산을 실행하는 방식을 사용한다.
- 실행 가능한 IO 연산 목록을 구한다.
- a에서 구한 IO 연산 목록을 차례로 순회한다.
- 각 IO 연산을 처리한다.
- 이 과정을 반복한다.
- 논블로킹 IO 를 스레드 1개로 구현하면 동시성이 떨어진다.
- 보통 1개 채널에 대한 읽기 처리가 끝나야 다음 채널에 대한 읽기 처리를 진행하기 ㄸ매ㅜㄴ이다.
리액터 패턴
리액터 패턴은 논블로킹 IO를 이용해서 구현할 때 사용하는 패턴 중 하나이다.
논블로킹 IO 로 구현된 네트워크 프레임워크의 문서를 읽다보면 보이는 리액터라는 단어가 이 패턴에서 말하는 리액터에 해당한다.
리액터는 이벤트를 대기하고 핸들러에 전달하는 과정을 반복하는데 이를 이벤트루프라고 한다.
논블로킹에 기반한 Nettty, Nginx, Node.js 등의 프레임워크나 서버는 이러한 리액터 패턴을 사용하고 있다.
리액터 패턴에서 이벤트 루프는 단일 스레드로 실행한다.
언제 어떤 방법을 사용할까?
논블로킹 IO 나 가상 스레드를 적용할 때는 먼저 다음을 검토해야한다.
- 문제가 있는가?
- 문제가 없는데 구현을 변경하는 것은 시간을 낭비하는 것이다.
- 게다가 논블로킹 IO/비동기IO 방식으로 구현하면 코드가 복잡해지고 유지보수난이도도 올라간다.
- 문제가 있다면 네트워크 IO 관련 성능 문제인가?
- 구현 변경이 가능한가?
우선 순위에 밀려 구현 변경이 불가능한 상황에는 구현 방식을 바꾸는 대신 서버 확장으로 문제를 해결해야한다.