웹 서버의 성능을 최적화 하는 방법으로 멀티 스레드를 활용하면 가능하다고 포스팅을 올렸는데
사실 멀티태스킹이 반드시 효울적이지만은 않습니다.
오늘은 멀티스레드 개념에서 어떤 상황에서 멀티 스레드를 활용하면 오히려 성능이 떨어지는 케이스를 살펴보고
멀티스레드를 올바르게 활용할 수 있는 판단력을 키워보고자 합니다.
개요
프로세스, 스레드 개념이 익숙하지 않는 독자들은 컨텍스트 스위칭 (문맥 교환) 개념이 익숙하지가 않을 것 같아
사람의 멀티 태스킹을 비유로 어떤 케이스에 오히려 비효율적인 성능이 나오는지 확인해보겠습니다.
1. 사람의 멀티태스킹
만약 어떤 개발자가 프로그램 A를 개발하고 있다고 가정해보겠습니다.
이때 기획자가 갑자기 프로그램 B에 급한 이슈 상황이 발생해 바로 수정해달라고 합니다.
프로그램 A의 개발을 잠시 멈추고, 프로그램B를 수정한다고 가정해보겠습니다.
그리고 프로그램B를 이슈 상황을 잘 마무리 하고 다시 기존의 작업인 프로그램A의 작업을 진행할때
내가 작성하고 있던 코드의 위치를 찾아야 하고, 개발할때 선언한 변수에 무슨 값이 들어왔는지 다시 기억해내야 합니다
만약 프로그램A의 개발을 완료하고 프로그램B를 수정한다면 전체 시간으로 보면 더 효율적으로 개발할 수 있을것입니다.
2. 컴퓨터의 멀티태스킹
운영체제의 멀티태스킹을 생각해보면 cpu 코어가 1개 있다고 가정해봅시다.
스레드A, 스레드B가 존재하고
운영체제는 먼저 스레드A를 실행하고, 멀티태스킹을 해야하기 때문에 스레드A를 계속 실행할 순 없습니다.
스레드A를 잠시 멈추고 스레드B를 실행합니다. 이후 스레드A에 그냥 돌아갈 순 없습니다, cpu에서 스레드를 실행하는데, 스레드A가 어디까지 실행했는지 위치를 찾아야하고, 계산하던 변수들의 값을 cpu에서 다시 불러들어야 합니다.
따라서 스레드A를 멈추는 시점에서 cpu에서 사용하던 값들을 메모리에 저장해두어야 합니다. 그리고 이후에 스레드A를 다시 실행할 때 이 값들을 다시 cpu에 다시 불러와야합니다.
이런 과정을 컨텍스트 스위칭이라고 합니다.
결과적으로는 컨텍스트 스위칭 과정에는 약간의 비용이 발생합니다 (오버헤드 간접비용)
컨텍스트 스위칭이 필요한 이유
- 여러 프로세스와 스레드을 동시에 실행시키기 위해 (cpu가 매우 빠르게 처리하기 때문에 동시에 처리하는 것 처럼 보임)
- 여러 프로세스와 스레드들이 공정하게 cpu 시간을 나눠 갖기 위해 (운영체제 알고리즘 마다 다름)
- 높은 우선순위의 작업이 빠르게 처리될 수 있게
발생 시점
- 주어진 Time Slice(Time Quantum)를 다 사용함
- I/O 작업을 해야 함
- 다른 리소스를 기다려야 함
- 인터럽트(Interrupt)
누구에 의해서 실행 되는가?
os의 커널에 의해서 수행이 됩니다.
커널이란, 운영체제에서도 핵심적인 기능을 담당하는 존재인데, 각종 리소스를 관리/감독하는 역활을 하는 존재입니다.
컨텍스트 스위칭 일어나는 과정
컨텍스트 스위칭이 구체적으로 일어나는 과정은, 다른 프로세스끼리의 스위칭(Process Context Switching)인지 같은 프로세스의 스레드들끼리의 스위칭(Thread Context Switching)인지에 따라 다릅니다. 이 두 가지의 공통점과 차이점을 바탕으로 설명해 보겠습니다.
둘의 공통점
1. 커널 모드에서 실행된다
두 스위칭은 모두 커널 모드에서 실행됩니다.
커널 모드라는 말은, 어느 프로세스가 실행되다가 하드웨어와 밀접한 일들 혹은 컴퓨터에 있는 여러 리소스들을 다뤄야 하는 상황이 발생하면 프로세스가 직접 컴퓨터의 리소스에 접근하는 것이 아니라 운영체제를 통해 접근하게 되는데, 특히 운영체제에서도 커널을 통해서 접근하게 됩니다. 이때, 이 프로세스에서 커널로 통제권이 넘어가서 커널에 의해서 실행되는 것을 '커널 모드'라고 합니다.
이 컨텍스트 스위칭은 커널 모드에서 실행되기 때문에 커널로 통제권이 넘어갑니다.
2. CPU의 레지스터 상태를 교체
두 스위칭은 또한 모두 CPU의 레지스터 상태를 교체합니다.
레지스터에 대해서 간단하게 설명하고 가자면, 레지스터란 CPU에서 각종 명령어들을 수행하기 위해 필요한 여러 데이터들을 저장하는 곳입니다. 많은 레지스터들이 있지만, 대표적으로 PC(Program Counter), Stack Pointer 등이 있습니다.
어떤 프로세스가 CPU에서 실행되고 있을 동안에, 레지스터에 있는 여러 값들이 계속 바뀌면서 실행되고 있었을 겁니다. 이때, 다른 프로세스가 실행되면 실행되고 있던 프로세스의 레지스터 상태들을 어딘가에 저장을 하고 다른 프로세스가 실행되는 것입니다. 그래야, 다시 다른 프로세스가 실행됐을 때 그때의 상태 정보들을 알고 있어야 이어서 실행할 수 있기 때문이겠죠?
둘의 차이점
1. 다른 프로세스끼리의 스위칭인 프로세스 컨텍스트 스위칭은 가상 메모리 주소 관련 처리를 추가로 수행함
지난 글에서 같은 프로세스에 속한 스레드들끼리는 공유하는 메모리 지역이 있다고 했었습니다. 조금 설명을 덧붙이자면, 스레드들끼리는
- 코드(Code) 영역 : 프로세스가 실행할 코드
- 데이터(Data) 영역 : 전역 변수가 정적 변수들이 저장되는 영역
- 힙(Heap) 영역 : 동적으로 할당된 메모리
의 영역을 공유합니다. 따라서, 스레드들끼리의 컨텍스트 스위칭은 같은 프로세스에 속하기 때문에 스위칭이 일어나도 메모리와 관련해서는 챙겨줘야 할 부분이 없습니다. 하지만, 프로세스간의 컨텍스트 스위칭이 발생했을 때에는 메모리 주소 체계가 다르기 때문에 이 때는 메모리 주소 관련된 처리를 추가로 수행해야 합니다.
그렇기 때문에, MMU(Memory Management Unit)와 TLB(Translation Lookaside Buffer)도 관리를 해줘야 합니다.
MMU는 가상 메모리와 물리 메모리 사이의 주소 변환을 담당합니다. 프로세스가 물리 메모리에서 할당되는 위치를 추상화하여 각 프로세스가 독립적인 주소 공간을 가지고 있는 것처럼 만듭니다.
TLB는 MMU 내에 존재하는 캐시 메모리입니다. 가상 주소를 물리 주소로 변환하는 과정을 상대적으로 느릴 수 있기 때문에, 이러한 변환의 결과를 TLB에 저장해 두고 빠르게 사용하는 데에 목적이 있습니다.
스레드 간의 스위칭에서는 실행되고 있던 스레드의 상태를 저장하고, 새로 실행될 스레드의 상태를 로딩하는 것으로 해결이 되지만 프로세스간의 스위칭에서는, 위의 작업에 더불어서 MMU가 실행 될 작업의 메모리를 보도록 해야 하고 캐시 역할을 하는 TLB를 완전히 비워줘야 합니다.
요약하자면 '다른 프로세스끼리의 스위칭은 주소 체계와 관련된 일들도 추가적으로 처리를 해야 한다'가 되겠고요, 이러한 추가적인 작업이 필요하기 때문에 프로세스 컨텍스트 스위칭보다 스레드 컨텍스트 스위칭이 더 빠릅니다.
3. 컨텍스트 스위칭이 미치는 영향
컨텍스트 스위칭이 발생하면서, 캐시 오염(Cache Pollution)이라는 간접적인 영향이 발생합니다.
캐시란 데이터나 값을 임시 저장해 두는 임시 장소입니다. CPU가 자주 사용하는 것들에 대해서 매번 메모리에 접근하는 것이 아니라, 캐시에 저장해 두고 빠르게 데이터를 가져오기 위해 사용합니다.
컨텍스트 스위칭이 일어나면, 또 사용할 것이라고 예상을 하고 캐시에 데이터나 값을 저장한 캐시가 의미가 없는 데이터가 되어버립니다. 컨텍스트 스위칭이 일어난 직후에 캐시에 가봤자, 이전에 프로세스에서 실행되었던 정보들을 저장해놓고 있을 가능성이 매우 높기 때문에, 내가 필요한 정보는 없을 가능성이 매우 높아지기 때문입니다. 이러하다는 것은 성능적으로 손해를 보는 부분이 발생한다는 거겠죠?
캐시 메모리는 메인 메모리에 비해 매우 작기 때문에 프로세스끼리 나눠서 쓰지 않고 다 같이 사용하는 공간이라고 합니다. 그래서 위와 같은 문제가 발생합니다!
어플리케이션 관점에서는, 컨텍스트 스위칭은 순수한 오버헤드(Overhead, 간접비용)입니다.
즉, 내가 실행한 이 프로그램의 동작과는 아무 상관없는 순수한 CPU 작업을 필요로 하는 간접비용입니다.
CPU 바운드 작업 vs I/O 바운드 작업
스레드가 하는 작업은 크게 2가지로 나눌 수 있는데요
Cpu - 바운드 작업
- CPU에 연산 능력을 많이 요구하는 작업
- 계산, 데이터 처리, 알고리즘 실행등 cpu의 처리 속도가 작업 완료 시간을 결정하는 경우입니다
- 예시: 복잡한 수학 연산, 데이터 분석, 비디오 인코딩, 과학적 시뮬레이션 등
I/O - 바운드 작업
- 디스크, 네트워크, 파일 시스템 등과 같은 입출력 작업을 많이 요구하는 작업
- 이러한 경우 I/O 작업이 완료될 때까지 대기 시간이 많이 발생하며, cpu는 상대적으로 대기상태에 있는 경우가 많습니다
쉽게 이야기 해서 스레드가 cpu를 사용하지 않고 I/O 작업이 완료될 때 까지 대기하는것을 뜻합니다 - 예시: 데이터베이스 쿼리 처리, 파일 읽기/쓰기, 네트워크 통신, 사용자 입력 처리 등
웹 애플리케이션 서버
사실 대부분 웹 서버의 경우 I/O 바운드 작업이 많은 경우가 많은데
스레드가 1 ~ 100000까지 더하는 cpu연산이 필요하는 작업보다 (복잡한 계산) 대부분 사용자의 입력을 기다리거나
데이터베이스를 호출하고 그 결과를 기다리는 등, 기다리는 일이 많습니다
일반적으로 자바 웹 애플리케이션 서버의 경우 사용자 요청을 처리하는데 1개의 스레드가 필요한데
사용자 4명이 동시에 요청하면 4개의 스레드가 동작하는 것입니다. 사용자의 요청을 처리하는데 cpu의 1% 정도 사용하고
나머지는 서버에 어떤 결과를 기다린다고 가정하는 경우 cpu 코어가 4개 있다고 해서. 스레드 숫자고 4개로 설정하면 안됩니다.
그러면 동시에 4명의 요청자 밖에 처리할 수 없고 cpu는 4%만 사용하고 놀고 있는 사태가 벌어집니다
결국 스레드 숫자만 늘리면 되는데, 이런 부분을 잘 캐치하지 못하고 서버장비탓을 하며 장비를 늘리는 불필요한 비용을 지불하는 경우가
종종 있다고 합니다.
정리하자면 cpu 바운드 작업이 많은가 I/O 작업이 많은지 판단하고 해당 상황에 맞는 적절한 코어 갯수와 스레드 갯수 설정이 중요하겠습니다.
cpu - 바운드 작업
- cpu 코어 수 + 1개
- cpu를 거의 100% 사용하는 작업이므로 스레드를 cpu 숫자에 최적화
I/O - 바운드 작업
- cpu 코어수 보다. 많은 스레드를 생성 cpu를 최대한 사용할 수 있는 숫자까지 스레드 생성
- CPU를 많이 사용하지 않으므로 성능 테스트를 통해 CPU를 최대한 활용하는 숫자까지 스레드 생성
- 너무 많이 생성하면 컨텍스트 스위칭 비용이 발생하니 적절한 성능 테스트 필요합니다
'OS' 카테고리의 다른 글
동시성 문제 해결 synchronized 메서드 (0) | 2024.09.11 |
---|---|
동기화 - synchronized (동시성 이슈) (0) | 2024.09.10 |
자바 volatile, 메모리 가시성 문제 해결 (0) | 2024.09.09 |
OS 스레드의 예외 처리 (0) | 2024.09.03 |
Java 스레드 Deep Dive (4) | 2024.09.02 |