본문 바로가기

app/python

[python] 서버의 기본 동작 방식

- 해당 글은 python 3.6을 기준으로 작성하였습니다. 

- mac 환경에서 작성하였으며, 다른 운영체제에서는 다르게 동작할 수 있습니다. 

1. 유저 요청에서 서버까지

브라우저를 통해 사이트를 접속하면 위의 그림과 같이

(1) 유저는 서버에 요청을 보내고

(2) 서버에선 요청을 받아 처리한 후 

(3) 응답 메시지를 유저에게 전달하고

(4) 브라우저는 우리에게 화면을 보여준다

 

유저들의 간단한 url 입력만으로 위의 동작이 발생하며, 세부적으론 http의 통신 규약 , DNS탐색, 라우터 통신, 응답받은 데이터를 통한 브라우저의 디스패치, 랜더링의 많은 일들이 일어나게 됩니다. 이러한 많은 일들을 뒤로하고 우리의 백엔드 개발자들은  다양한 프레임워크를 통해 서버를 쉽게 구동하고, 웹서버를 실행하여 유저에게 제공합니다. 그렇다면 서버는 어떻게 유저의 요청을 받아들이고, 내부적으로 어떻게 처리하게 될까요?

 

2. python의 간단한 서버 구동

# !/usr/bin/python
from http.server import BaseHTTPRequestHandler, HTTPServer

PORT_NUMBER = 8080
IP_NUMBER = '127.0.0.1'

class myHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()
        self.wfile.write("Hello World !".encode())
        return


if __name__ == '__main__':
    try:
        server = HTTPServer((IP_NUMBER, PORT_NUMBER), myHandler)
        print('Started httpserver on port ', PORT_NUMBER)
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    
    print('^C received, shutting down the web server')
    server.socket.close()

 

여기 파이썬의 기본 클래스만으로 만든 서버가 있습니다. 

HTTPServer()를 통해 포트를 설정하고 BaseHTTPRequestHandler로 구현한 GET 메서드를 통해 유저에게 "Hello world!" 메시지를 출력하는  간단한 로직입니다.  

 

이제부터 위의 코드를 중심으로 작동하는 상위 클래스를 하나씩 살펴보겠습니다. 

 

3. http.server 에서 포트까지 

https://docs.python.org/3.6/library/http.server.html

 

예제 코드에서 사용한 HTTPServer 클래스는 http.server에 정의되어 있으며 공식 문서를 보면 HTTPServer는 "핸들러를 통해 server 인스턴스에 접근할 수 기능을 제공한다"라고 쓰여 있습니다. 위의 예제에서의 핸들러, server인스턴스는 아래의 소스에 해당합니다. 

 

server = HTTPServer((IP_NUMBER, PORT_NUMBER), myHandler)
server.serve_forever()

 

server의 인스턴스를 생성 시 사용하는 핸들러를 추가하였고, serve_forever()에서 계속 유저의 요청을 수신하다고 유추해볼 수 있습니다. 그렇다면 HTTPServer의 내부는 어떻게 동작하는 걸까요?

 

위의 코드를 pycallgraph로 실행해보면 다음과 같은 도식도를 볼 수 있습니다.

확대해서 보세요. 빨간색만 따라가시면 됩니다.

위의 그림에서 오른쪽 대부분은 유저의 요청 시 데이터를 처리하기 위한 파일 로더, 인코더 등으로 핵심 클래스인 http, socketserver, socket,  selector가 서버의 구성에 쓰인다고만 보셔도 무방합니다.  

 

위의 소스를 디버깅으로 따라가 보면 아래와 같은 순서로 동작합니다.. 

!! 글로 읽기 힘들 수 있습니다. 꼭 디버깅으로 따라가 보시는 것을 추천합니다. 

 

1)   HTTPServer   ->  TCPServer init() 호출

2)  TCPServer      ->  BaseServer init() 호출

3)  BaseServer     ->  init()에서 종료 인터럽트 설정 마치고 리턴

4)  TCPServer      ->  socket.socket()에서 객체 생성

5)  TCPServer      ->  server_bind()에서 서버 기본값 설정 및 socket에 정보 설정

6)  TCPServer      ->  server_activate()에서 socket.listen() 실행

------------ 여기까지 서버가 응답을 받기 위한 기본 세팅  ---------------------

7)  HTTPServer   ->  BaseServer server_forever() 호출

8)  BaseServer    ->  selectors.PollSelector 호출

9)  Selectors       ->  select.poll의 기본 세팅 

10) BaseServer  ->  3번에서 종료 인터럽트가 오기 전까지 while True:

11)  BaseServer   ->  selector.select() 호출

12)  selectors      ->  fd_event_list = self._poll.poll()를 통해 이벤트가 발행했다면 해당 이벤트 반환

13)  BaseServer   ->  12번의 이벤트가 있다면 _handle_request_noblock() 호출 

14)  10번으로 돌아감 무한반복

 

- selectors에서 -> _BaseSelectorImpl -> BaseSelector로  동작하는 함수들이 있지만 여기에선 제외하였습니다.  

- 13번의 _handle_request_noblock() 부분은 요청을 처리하는 구현 부분으로 앞서 작성했던 핸들러에 속합니다. 자세한 내용은 다음 편에 계속됩니다. 

 

 

우리가 사용했던  HTTPServer는 TCPServer의 wrapper이며, 대부분의 동작은 TCPServer의 상속과 최상위 클래스인 BaseServer에서 담당하는 것을 볼 수 있습니다. ( python 서버 구현에 있어서 아래 그림이 핵심 클래스입니다.)

 

https://docs.python.org/3.4/library/socketserver.html 부분을 꼭 읽어보길 바랍니다. 

C나 JAVA로 서버를 구성했다면 위의 흐름에서 몇몇은 함수들은 익숙한 부분들이 보일 겁니다. 대부분의 서버 동작은 초기 세팅, 소켓 생성, 소켓 바인딩, 소켓 리슨 순서로 동작하기 때문입니다.

 

여기에서 소켓에 유저의 요청 이벤트가 왔는지 확인하는 부분이 fd_event_list = selfself._poll.poll()입니다. 그렇다면  fd, poll은 무엇이길래 이벤트를 감지하는 것일까요?

 

 

4. select, poll, fd의 세계

I/O Multiplexing

input() 중에는 동작이 block 되고, 진행이 되지 않는 현상들을 볼 수 있습니다. 

즉, I/O가 병행적으로 진행되지 못하여 (I/O Blocking 발생) 그런 것인데, 이러한 방법을 해결하기 위해 select(), poll(), epoll(), kqueue() 등의 구현되게 됩니다. 

 

예제 코드에서 사용한 모듈인 selector는 "고수준의 효율적인 I/O 다중화"로 select를 더욱 효율적인 I/O 멀티 플렉싱을 수행할 수 있도록 만들어졌습니다. 그리고 select는 운영체제에서 사용할 수 있는 select(), poll(), epoll(), kqueue() 등의 기능에 엑섹스를 제공하는 클래스입니다. 

 

select

 

초기 운영체제에선 사용자의 입력을 기다리기 위해 스레드가 블록 되기 때문에 여러 개의 input을 동시에 받을 수가 없었습니다. 따라서 멀티플 레싱 해주는 메커니즘의 해결을 위해 select가 만들어집니다. 지정된  ready descriptor의 번호를 리턴하는데 이를 이용하여 fdset을 만들어 그 set에 속한 fd 중 하나라도 입력이 들어오면 블록 상태가 해제되고 원하는 루틴을 수행하도록 알려줍니다. 

 

fd라는 파일을 만들고 해당 파일이 변경되었다면 이벤트가 발행했다고 알려줍니다. 

 

poll

poll은 select와 마찬가지로 다중입출력을 구현하기 위한 방법으로 사용되며, select보다 더 많은 수의 클라이언트를 다루는 서버를 다루기 위해 만들어졌습니다. 물론 내부적으로 select로 구현이 되었지만 select의 한계인 최대 fd개수 1024를 무제한으로 수정했으며,  전달하는 데이터의 용량에도 제한이 있어 poll을 구현하게 됩니다. 

 

 

epoll

https://en.wikipedia.org/wiki/Epoll

 

앞써 poll과 select는 생성한 fd 배열을 몽땅 뒤지기 때문에 수행 시간이 O(n)가 됩니다. 따라서 대규모 서버에서 부하를 많이 일으킬 수밖에 없는 구조입니다. (물론 selelct / poll을 만들 당시엔 대규모의 요청이 없던 시절이었습니다.)리눅스 2.6 커널에서는 이를 해결하기 위해 epoll system call을 제공하였으며, fd를 모두 확인하는 방식이 아닌 이벤트가 있는 fd만을 선별하게 되어 효율적으로 작동하게 됩니다. 

( 위의 예제 코드에서는 epoll이 아닌 poll로 구현되었습니다, 우리가 흔히 사용하는 django, flask 등은 내부적으로 epoll를 사용합니다. )

 

 

fd ( File Descriptor )

https://en.wikipedia.org/wiki/File_descriptor

 

유닉스 시스템에서 모든 것은 파일로 관리됩니다. 우리가 쓰는 일반 파일부터 소켓, 파이프, 블록 디바이스까지 모든 객체들은 파일로써 관리됩니다. 그리고 이 파일에 접근할 때마다 fd(파일 디스크립터)가 사용됩니다. 

유저가 요청을 보내면 서버 측의 fd하나에 read이벤트가 발생하게 됩니다. 그 이벤트는 poll로 구현되었을 경우 poll fd중의 하나에 도착해서 수천 개의 poll fd 중에 read이벤트가 발생한 것들을 일일이 찾아서 처리해주고 다시 write 할 것들을 세팅해서 넘겨주게 됩니다. 

 

종합해보면 (유닉스 계열) I/O는 fd를 통해 관리되며, 이 fd를 효율적으로 사용하는 몇몇의 기술 중 poll()를 이용해서 소켓의 이벤트를 감지하는 서버를 만들었습니다

 

 

 

마무리

예제 소스를 간략히 설명해보면 

HTTPServer 실행  -> 소켓 연결 -> polling() -> fd 이벤트 감지로 요약할 수 있습니다. 

 

여기까지 python에서 제공하는 기초적인 서버의 구성 및 동작 방식을 확인해보며 그 과정에서 어떻게 이벤트를 감지할 수 있는지 알 수 있게 되었습니다. 

 

다음 편에서는 유저가 서버에 이벤트를 요청했을때 어떻게 동작하는지에 대하여 알아보도록 하겠습니다. 

 

- 다음편  [python] 서버의 기본 동작 방식 2

 

 

 

참고 및 같이 보면 좋은 글들

- https://soooprmx.com/archives/10919

- https://kldp.org/node/46542

- https://docs.python.org/ko/3.6/library/select.html#module-select

- https://itholic.github.io/python-select/

- https://hamait.tistory.com/834

- https://unabated.tistory.com/entry/%ED%8C%8C%EC%9D%BC-%EB%94%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%84%B0

- https://ozt88.tistory.com/22?category=123069

 

'app > python' 카테고리의 다른 글

[python] 서버를 만들어보자(1) echo-server  (0) 2019.12.10
[python] 서버의 기본 동작 방식 2  (0) 2019.11.16
[python] 서버의 기본 동작 방식  (0) 2019.11.14
python heap 구현 소스  (0) 2019.10.14
python zen (계속 갱신중)  (0) 2019.08.12
python datetime / date  (0) 2019.06.04