개발/CS

C10K부터 Event-Driven까지

728x90

 

  • 하드웨어 자원이 충분함에도 불구하고 IO 처리방식의 문제 때문에 프로세스가 제대로 처리하지 못하는 문제.
    • BSD 소켓을 처음 설계할 때 10K 클라이언트를 처리할 하드웨어 여력이 되지 않았다. 그래서 이를 고려하지 않고 설계함.
    • 10K가 붙으면 네트워크 대역만 해도 기가비트급이고 당시에 상상도 못할일. 이걸 감당할 하드웨어도 없었다.
  • 처음에 OS 커널을 수정하는 방식으로 해결하려고 했음. 그 이후에는 event-driven 서버(Nginx, Node.js)를 통해 해결하려는 시도들이 등장.
  • 무어의 법칙 :  18개월마다 반도체 직접회로의 성능이 2배로 증가 에 비해 소프트웨어의 한계로 인해 발생한 문제.
  • 스레드는 서버 CPU 코어수에 종속적이다. 
  • 하나의 Connection당 하나의 스레드가 처리하는 모델 사용
  • 소켓의 채널을 넓히는데 발생하는 문제.
  • 멀티스레드의 IO처리에서 task양이 코어 수에 종속적임
    • 동시에 10k가 와도 멀티스레드의 경우 동시에 처리할 수 있는 물리적인 갯수는 코어수에 종속될 수 밖에 없다는 것이다.
    • 또한 예전버전의 커널에서 사용하는 select()/poll()의 I/O Multiplexing 함수가 처리할 수 있는 FileDiscripor의 갯수가 설계 당시부터 10K이상을 고려하지 않음
  • 커널이 해결책이 아니라 문제다
    • 즉, 
  • 유닉스는 일반 서버 OS로 설계된 것이 아니라, 전화망을 위한 제어 시스템으로 설계되었다.
  • Apache의 문제
    • 많은 커넥션이 있을수록 성능이 저하됨
    • 예를 들어 두배 빠른 프로세서를 쓰더라도 2배 많은 커넥션을 얻을 수 없다.
      • 성능과 가용성이 선형관계가 아니다.
      • 하드웨어를 업그레이드 하는 것으로는 가성비가 떨어진다.
    •  
    • Apache는 CGI 프로세스를 fork하고 kill한다. 이 방식은 scalable하지 않음.
    • 서버는 10K를 동시에 처리할 수 없다. 왜냐하면 커널의 처리방식 시간복잡도가 O(N^2) 이기 때문이다
      • 패킷이 들어오면 커널에 있는 모든 10K개의 프로세스를 순회하여, 패킷을 다룰 스레드를 찾는다.
      • 각각의 패킷은 소켓들의 리스트를 순회한다.
    • 해결책 -> 커널을 수정하여 일정한 시간에 조회
      • 이제 스레드 수에 관계없이 스레드가 일정 시간 후에 컨텍스트 스위칭 됨
    • 스레드 스케줄링은 여전히 scalable하지 않음. 그래서 서버를 소켓을 epoll과 함께 사용하여 Scaleable하게 하려고 함.
      • 이러한 움직임은 Node, Nginx에 탑재된 비동기 프로그래밍쪽으로 옮겨감.
    • 더 많은 스레드를 생성할 수록, 데이터 복사(cache IO)
    • 그.래서 c10k의 해결책은 사용하는 컴퓨팅 리소스를 최소화 하는것이다. 
    • 하나의 커넥션당 하나의 스레드나 프로세스를 생성하게 되면 시스템 리소스를 소비하고 여러 프롯세ㅡ 스레드를 관리하면 시스템에 부담이 가서 확장성이 좋지 않다. 
  • 해결책 -> 각 프로세스와 스레드가 여러 연결을 동시에 처리하는 방식 - Io 멀티플렉싱
  • io멀티플렉싱을 구현하는 방법에는 여러가지가 있다.ㄱ

1. blocjing, nonblokcing, asynchoronous io (io 전략) io자체를 효율적으로 바꿔보자

2. 1 to 1 or 1 to more tsak, tread poll 

(스레드 자체가 오버헤드가 크니한 스레드에서 여러개의 작업을 처리하는 방향을 생각해보자) 이벤트 드리븐 방식.

  • UNIX에서는 File descriptor를 만드는데 드는 비용과 그 많은 소켓을 동기화시키는 문제가 있다.
    • *File Descriptor란 ?
      • 리눅스 혹은 유닉스 계열의 시스템에서 프로세스가 파일을 다룰 때 사용하는 개념. 프로세스에서 특정 파일에 접근할 때 사용하는 추상적인 값. 일반적으로 0이 아닌 정수값.
      • 흔히 유닉스 시스템에서 모든 것을 파일이라고 한다. 일반적인 정규파일부터 디렉토리, 소켓, 파이프, 블록 디바이스, 캐릭터 디바이스 등 모든 객체들을 파일들로 관리한다.
      • 유닉스 시스템에서 이 파일들을 접근할 대 디스크립터라는 개념을 사용한다.
      • 프로세스가 실행 중에 파일을 Open하면 커널은 해당 프로세스의 파일 디스크립터 숫자 중 사용하지 않는 가장 작은 값을 할당해준다.
      • 그 다음 프로세스가 열려이쓴 파일에 시스템 콜을 이용해서 접근할 때, 파일 디스크립터 값을 이용해서 파일을 지칭할 수 있다.
    • 문제 :
      • 클라이언트 접속마다 프로세스를 생성하면 OS 파일 디스크립터나 프로세스 수가 최대치에 도달
      • 프로세스가 만 단위 이상이되어 컨텍스트 스위칭 등에 사용되는 CPU사용률 문제
      • 프로세스 수가 많으면 프로세스를 관리하는 OS 커널 내의 관리용 데이터 크기 제
  • UNIX 계열 OS의 IO Model이 2K 이상의 소켓을 열어서 정상적인 I/O를 할 수 있느냐는 문제가 제기되었다.
    • Unix계열에서 File Descripto를 만드는 비용과 그 많은 소켓을 동기화시키는 문제가 있다.
    • 즉 10K 만큼의 소켓을 열게 되면, 하드웨어가 충분함에도 불구하고, OS에서 제공하는 IO처리방식의 문제때문에 프로세스가 제대로 처리되지 못한다는 것이다.
    • 2048은 select() 함수의 파라미터로 들어가는 File Desciptor 배열의 최대값이다.

 

커널에서 사용되는 O(N^2) 알고리즘으로 인해 서버는 10K 동시 연결을 처리할 수 없었다.

커널의 두가지 기본적인 문제

1. Connection = thread/process :

패킷이 들어오면, 10K의 프로세스를 거쳐 어떤 스레드가 패킷을 다룰것인지 정해진다.

 

2. Connection = select/poll (single thread) : 같은 가용성 문제, 각각의 패킷은 모든 소켓들을 거쳐야 한다. 

 

해결책 : 커널이 상수 시간을 동안 

 

 

*epoll : 리눅스에서 Select의  단점을 보완하여 사용할 수 있도록 만든 IO 통지 모델. 파일 디스크립터를 사용자가 아닌 커널이 관리하고, 그만큼 CPU는 계속해서 파일 디스크립터의 상태 변화를 감시할 필요가 없다.

즉 select처럼 어떤 파일 디스크립터에 의해 이벤트를 찾기 위해 전체를 순차검색을 하기 위핸 FD_ISSET 루프를 돌려야 하지만,

Epoll의 경우 이벤트가 발생한 파일 디스크립터만 구조체 배열을 통해 넘겨주므로 메모리 카피에 대한 비용이 줆

 

 

기본적으로 소켓은 블러킹, 넌 블럭킹, 비동기 3가지 동작모드가 있음.

기본적으로 블럭킹 모드로 동작함.

fcntl() 함수를 통해 O_NONBLOCK으로 변경가능하.

동기 비동기는 OS가 지원하는 IO모델의 분류임. 이는 커널과 애플리케이션 사이의 문제이다.

동기는 IO를 API를 통해서 커널에 의뢰했을 때 커널이 디바이스든 내부버퍼든 어딘가에 데이터가 다 전송시키고 어플리케이션에 그 결과를 알려주는 것이다.

 

블럭킹 모드는 소켓의 함수를 통해 네트워크에 관련된 작업(IO) 수행시 작업이 완료될때까지 리턴하지 않으므로 프로세스가 멈춰 있는 것이다.

 

 

  • 즉, 다음의 5가지를 주요 OS 별로 사용할 수 있는 시스템 콜들과 제안된 사항, 라이브러리들을 기반으로 풀어놓고 있다.
    1. Serve many clients with each thread, and use nonblocking I/O and level-triggered readiness notification — select, poll, kqueue(2도 됨) 같은 시스템 콜을 쓰는 것
    2. Serve many clients with each thread, and use nonblocking I/O and readiness change notification – 글에서도 설명하는 것 처럼 edge-triggered logic이고(1과 상반되게), epoll, kqueue, linux 2.6 kernel의 NAIO …
    3. Serve many clients with each server thread, and use asynchronous I/O — AIO함수들
    4. Serve one client with each server thread, and use blocking I/O
    5. Build the server code into the kernel

 

C10K는 

JVM 기반 진영에서는 아래와 같고

Java NIO, Netty, Akka, Vert.x에서 

 

JS 기반에서는 V8 엔진을 기반으로 둔다

 

 

그리고 웹서버는 TCP 로드 밸래

Nginx : TCP Load balanceer, FastCGI

 

 

원칙1 애플리케이션을 C10K 문제에 적합하게 만들기

 

CPU를 최대한 활용해야할 때, 프로

 

 

 

해결책은 아래의 두가지 관점에서 논의되었다.

 

  • 애플리케이션은 어떤식으로 OS와 렵혁해야 하는가. (Blocking? , non-blocking? asynchronous IO?)
    • Blocking IO의 read()를 통해 만약 동시성을 보장하려면 멀티 스레딩을 이용해야함
    • Non-Blocking IO (select in IO Multiplexing, poll 등) : 커널은 애플리케이션에게 notifty 해야한다.  IO가 준비됐을 때 읽고 쓰라고 알려야 한다. 그 다음 epoll
  • 애플리케이션은 어떤식으로 task를 thread/process에 할당해야하는가?
  •  

해결책 

  • dev/poll 모델을 출발로 윈도우의 Overapped/I/O, IOCP이다.
  • 노드와 같은 IO 다중화처리를 위한 event-driven 모델의 철학은 동시 10k를 처리하기 위한 IO에 대한 처리를 위함.
    • event loop와 IO를 분리하여 Context Switching 비용을 줄이고, 비동기 IOf를 처리하여 병목을 줄이는데 초점을 둠.
    • select, poll에서 모든 fd_set을 조회하는 비효율적인 구조를 epoll을 이용하여 발생한 이벤트만 조회 가능하도록 바꾸고
    • IO를 aio library를 이용하여 요청을 비동기 callback을 이용하여 처리함으로서 최대한의 IO 병목을 줄여 동시에 많은 IO처리가 가능하도록 설계한 오픈소스

 

NGINX : 

Apache의 C10K를 해결하기 위해 Event Driven 구조로 만든 웹서버.

고정된 프로세스만 생성하여, 그 프로세스 내부에서 비동기방식으로 task를 처리함.

io 멀티플렉싱 모델(epoll, kqueu가 핵심)

 

Apachedhk 같이 프로세스/스레드 단위로 커넥션을 처리하는 서버 모델에는 한계가 있음.

각 커넥션의 IO작업중에 스레드/프로세스가 Blocked되어 

이렇게 스레드 기반은 하나의 커넥션 당 하나의 스레드를 점유한다. 즉 불필요한 자원까지 점유하게 된다.  하지만 이벤트 드라이븐 방식은 여러개의 커넥션을 몽땅 다 Event Handler를 통해 비동기 방식으로 처리해, 먼저 처리되는 것부터 로직이 진행되도록 함. 

 

Nodejs 에서는 모든 IO 메서드에서 논블록킹 방식인 비동기로 콜백을 받음

 

이벤트 드리븐 방식 : 

IO처리는 커널 비동기 라이브러리 epoll kqeue를 사용하여 callback 받음

이벤트와 IO처리를 분리

JS의 비동기 특성으로 인해 모든 nodejs시스템 라이브러리 파일 읽기 또는 HTTP파일 전송과 같은 IO작업을 위한 이벤트 및 비동기 API를 제공함.

 

 

node js의 이벤트 루프가 scale 측면에서 우수하다 라는 말을 알기 위해서는 기존에 요청을 어떻게 처리했는지를 이애하면 도움이 된다.

 

기존에는 server 소켓을 생성하고 포트 바인드를 한다

그리고 listen 상태로서 요청이 오는지 확인하며 대기한다.

요청이 오면 요청을 처리한다_

TCP Connection 요청을 받고 상태는 accept connection이 된다. accpt connection 은 시스템콜이고, 시스템 콜은 프로그램을 block할 수 있다.

 

멀티스레드

만약 새로운 커넥션 마다 새로운 스레드를 생성해서 할당하고, 그 스레드에게 요청을 맡기고 요청이 끝나면 다시 스레드를 회수한다고 생각해보자.

메인 스레드에서는 커넥션을 기다리고 커넥션 요청이 오면 새로운 스레드를 생성하고 요청을 할당한 후 다시 커넥션을 기다린다면 요청이 끝나기 전 다른 요청ㅇ르 받ㅇ르 수 있다.

 

하지만 스레드를 생성하는 과정에서 오버헤드가 발생한다. 아무리 간단한 요청을 하더라도 스레드를 생성해야한다.

Epoll은 IO 통지 모델로서 커널 수준에서 file descriptor를 관리하게 된다.

epoll;

은 콜백을 통해 정 이벤

epoll은 리눅스에서 select의 단점을 보완하여 사용할 수 있도록 만든 IO통지 모델. 

CPU는 fd의 상태 변화를 감시할 필요가 없다. 즉 select처럼 어느 파일 디스크립터에 이벤트가 발생하였는지 순차검색하지 않고, 이벤트가 발생한 특정 파일 디스크립터만 구조체 배열을 통해 넘겨주므로 메모리 카피에 대한 비용이 줄어든다.

 

이벤트 루프란

계속해서 도는 while loop이다. eqoll (kqeue을 wait혹은 poll이라 부르고. callback 이벤트 등 이 발생했을 때 노드로 전달되고 epoll에서 기다릴 것이 없을 때 종료된다. 

https://dzone.com/articles/thousands-of-socket-connections-in-java-practical

http://www.kegel.com/c10k.html

https://openwiki.kr/tech/c10k_problem

 

http://highscalability.com/blog/2013/5/13/the-secret-to-10-million-concurrent-connections-the-kernel-i.html

 

https://titanwolf.org/Network/Articles/Article?AID=62a43a26-7dcf-4719-84bc-d2ace9c1de25#gsc.tab=0 

 

'개발 > CS' 카테고리의 다른 글

리눅스 select, poll, epoll  (0) 2021.05.16
컨텍스트 스위칭시 일어나는일  (0) 2021.05.14
부동소수점이란  (0) 2021.05.08
상속과 조합  (0) 2021.05.08
Blocking Queue 블락킹 큐  (0) 2021.04.26