공부용 블로그

Web Server 보충 본문

설계/WebServer

Web Server 보충

tomato212 2018. 10. 30. 13:56


I/O


성능에 결정적인 영향을 미치는 요인으로서의 I/O는 크게 두 가지 I/O가 존재한다. 디스크에서 데이터를 읽어오는 I/O와 네트워크 통신에서 발생하는 I/O이다. 이 두 I/O 작업이 처리되는 속도는 CPU의 작업 처리속도에 비해 매우 느리다. 그렇기 때문에 어플리케이션의 성능은 이 I/O 작업을 어떻게 처리하느냐에 따라 달려있다고 할 수 있다. 그 중 네트워크 통신에서 사용하는 socket을 중심으로 I/O 방식에 대해 알아본다.


Q. CPU안에 I/O가 포함된 개념이 아닌건가? nmonchart에서 CPU use를 확인했을 때 user 사용량과 kernel 사용량이 나눠져 있고 두 부분을 합쳐서 cpu 사용량이라고 하지 않았었나?



Socket 


소켓이란 네트워크로 연결되어 있는 서로 다른 두 컴퓨터가 데이터를 주고 받을 수 있도록 하는 네트워크 프로그램이다. 

IP 주소와 포트 번호를 가지고 있고, 양방향 네트워크 통신이 가능한 객체라 할 수 있다. 이 소켓에 데이터를 기록하고 읽으려면 소켓에 연결된 소켓채널 혹은 스트림을 통해 기술해야 한다. 소켓의 동작방식은 Blocking mod와 Non-Bloking mod가 있다.

블로킹은 요청한 작업이 성공하거나 에러가 발생하기 전까지는 응답을 돌려주지 않는 것을 말하며 넌블로킹은 요청한 작업의 성공여부와는 상관없이 바로 결과를 돌려주는 것을 말한다. 


Q. 넌블로킹 방식에서 작업의 성공여부와 상관없이 결과를 바로 돌려준다면 결과가 아직 없을때는 어떤 내용을 돌려주는걸까?


Bloking I/O


자바에서 블로킹 소켓은 ServerSocket, Socket 두 가지 클래스를 사용한다. 클라이언트가 서버로 연결 요청을 보내면 서버는 연결을 수락(accept)하고 클라이언트와 연결된 소켓을 새로 생성하는데 이 때 해당 메서드의 처리가 완료되기 전까지 스레드에 블로킹이 발생하게 된다. 또 클라이언트가 연결된 소켓을 통해서 서버로 데이터를 전송하면 서버는 클라이언트가 전송한 데이터를 읽기위해 read 메서드를 호출하고 이 메서드의 처리가 완료되기 전까지 스레드가 블로킹된다. 


Q.  소켓도 하나의 스레드인가?



병렬 처리의 문제


블로킹 소켓은 데이터 입출력에서 스레드의 블로킹이 발생하기 때문에 동시에 여러 클라이언트에 대한 처리가 불가능하게 된다.

이러한 문제를 해결하기 위한 모델은 클라이언트 별로 각각 스레드를 할당하는 모델이다. 

클라이언트가 서버에 연결을 요청하면 서버는 새로운 스레드를 하나 생성하고 그 스레드에게 클라이언트 소켓에 대한 I/O 처리를 넘겨주면 된다. 이로써 서버 소켓이 동작하는 스레드는 다음 클라이언트의 연결을 처리할 수 있게 된다. 



Q. 소켓은 I/O 처리 작업을 스레드에게 넘겨줌으로써 I/O 처리 작업에 걸리는 시간만큼 블로킹되는 시간이 줄었지만

클라이언트별로 스레드를 할당하는 시간동안은 여전히 블로킹되지 않을까?



블로킹 소켓으로 여러 클라이언트에 대한 요청을 해결할 수 있게 되었다. 그런데 여러 클라이언트가 동시에 접속 요청을 하는 상황에서는 어떨까?
접속을 요청한 클라이언트들은 각각에 해당하는 스레드를 생성하고 할당하고 제거하는 시간을 모두 기다려야하기 때문에 대기 시간이 길어진다. 또한 서버는 클라이언트의 요청이 올 때마다 계속해서 스레드를 생성할 것인데 이것은 스레드가 생성되는 공간인 힙 메모리의 부족을 야기할 수 있다. 계속해서 클라이언트의 요청에 따른 스레드를 생성하다가 Out Of Memory(OOM) 오류가 발생할 수 있는 것이다. 이것은 결국 서버가 다운되고 서비스 불가 상태로 이어지게 된다.


스레드 풀(Thread Pool)
이 문제를 해결하기 위해 스레드 풀이란 개념이 등장하였다. 스레드 수 증가에 따른 OOM 오류를 피하기 위해 일정 개수의 스레드를 스레드 풀에 미리 생성해두는 방법이다. 클라이언트의 요청이 들어오면 그 요청은 일단 작업 큐에 있고, 스레드 풀에서 가용 스레드를 할당받는 방식인 것이다. 스레드 풀을 사용하면 스레드 생성, 제거에 대한 오버헤드도 사라지고, 동시 접속 클라이언트에 대해 대처할 수 있다.

Java에서는 java.uitl.concurrent.Executors에서 스레드 풀을 제공한다.

Executors의 구조는 Producer-Consumer 패턴으로 만들어져 있다. 따라서 스레드가 생성하고 나서 같은 작업을 다시 하는 일이 없다면 스레드가 처리하고 나서 바로 종료 처리해도 되지만 반복적으로 어떠한 작업을 어떠한 시점에 해야 한다면 매번 스레드를 생성하고 종료하는 로직보다는 해당 스레드가 계속 대기 중인 상태가 되어 작업할 시점이 왔을 때 처리하는 것이 좋다. Executor에서는 각종 상황에 맞는 스레드 풀을 제공하고 있다.


newFixedThreadPool
parameter로 주어진 스레드 개수만큼 스레드를 생성하고, 애플리케이션이 종료될 때까지 그 수를 유지한다.
newCachedThreadPool
처리할 작업이 많아지면 그 만큼 스레드를 증가하여 생성한다. 만약 놀고 있는 스레드가 많다면 해당 스레드를 종료시킨다. 스레드의 개수를 유동적으로 조절해주기 때문에 유연한 대처가 가능하다는 장점이 있지만 처리할 작업이 무한정으로 많아지면 스레드의 개수도 계속해서 늘어나기 때문에 위에서 발생한 OOM 오류가 발생할 수 있다.
newSingleThreadExecutor
스레드를 하나만 생성하며 생성된 스레드가 비정상적으로 종료될 경우 하나의 스레드를 다시 생성한다.
newScheduledThreadPool

스레드와 관련된 작업을 특정 시간 이후에 실행되거나 주기적으로 작업을 실행할 수 있는 스레드 풀을 생성한다.


이 방법에는 문제가 없을까?
스레드 풀의 크기를 어떻게 잡아야 할까? 동시 접속 수를 늘리기 우해서 스레드 풀의 크기를 자바 힙 메모리가 허용하는 한도까지 키우는 것이 옳을까? 자바에는 메모리를 청소해주는 GC(Garbage Collection)이 존재한다. 애플리케이션 서버가 가동되고 시간이 흐름에 따라 GC 대상이 되는 객체 수가 늘어나게 된다. 결국 애플리케이션 서버가 작동하는 중간에 GC 가 발생할 것이고 이것은 서버가 작동하지 않는 것처럼 보이게 된다. (Stop the World!) 그리고 작동하지 않는 것처럼 보이는 이 시간은 힙 크기에 비례하게 된다. 만약 동시 접속 수를 늘리기 위해 스레드 풀의 크기를 최대로 키웠다면 이 대기 시간은 길어질 것이다. 힙에 할당된 메모리가 크면 클수록 GC가 발생하는 경우는 적어지겠지만 수행시간인 길어지게 되므로 trade-off가 존재하는 것이다.

Q. GC가 발생하는데 왜 서버가 작동하지 않는 것처럼 보이는거지? GC가 휴지통이라고 치면 휴지통이 꽉차면 서버가 다른일을 수행할 수 없게 되는건가?

Q. 수행시간이 길어진다는 것은 무슨 의미? 힙에 할당된 메모리가 크면 그 만큼 스레드 풀에 더 많은 스레드를 생성해 놓을 수 있고, 클라이언트들의 동시요청에 더 많이 대응할 수 있는거 아닌가? 왜 수행시간이 더 길어지지?

CPU의 입장에서는 어떨까? 스레드가 많으면 많을 수록 당연히 좋지 않다. 수많은 스레드가 CPU 자원을 획득하기 위해 경쟁하면서 CPU 자원을 소모하기 대문에 실제로 작업에 사용할 CPU 자원이 적어지게 되는 결과를 초래한다.

정말 온갖 문제를 모두 떠안고 있는 것처럼 블로킹 방식의 통신에 대해 이야기했지만 대부분의 서비스들은 이 방식으로도 잘 작동한다. Tomcat이 그 예가 되겠다.



출처 : http://asfirstalways.tistory.com/m/347?category=660807

'설계 > WebServer' 카테고리의 다른 글

Nginx에 관한 이해  (0) 2018.10.30
웹서버 벤치마크에서의 user mode와 kernel mode의 의미  (0) 2018.10.30
Jetty 설치  (0) 2018.10.27
ubuntu 16.04 $JAVA_HOME 설정  (0) 2018.10.25
Apache 마인드맵  (0) 2018.10.23