File Description Passing with Python

요새 Python 에 대해서 공부를 하고 있습니다. 정확하게는 Python 을 이용한 서버인데

서버의 대세는 Multi-Core, Multi-Threading 입니다. 그런데

cPython 의 경우에 GIL(Global Interpreter Lock) 이라는 무서운 녀석이 이 Multi-Threading 을

막고 있습니다.( JPython 이나 IronPython 의 경우는 Lock 구현이 달라서 이런 문제가 없다고 합니다.

그러나 많은 모듈들이 cPython 으로 존재하고 있어서 T.T )

GIL은 다음 사이트에서 http://www.dabeaz.com/python/UnderstandingGIL.pdf 자세한 정보를 얻을 수

있습니다.

그래서 cPython 을 쓸 때 Multi-Core 를 활용하기 좋은 모델은 Multi-Thread 가 아니라 Multi-Process

라고 할 수 있습니다. 그렇다면 Multi-Process를 활용한 서버 모델에는 어떤 것들이 있을까요?

일반적으로 서버 프로그래밍을 조금 공부하셨던 분들은(절대로 하셨던 분들이 아니라 공부하셨던 분입니다. -__- )

Preforked 방식이나, Process Per Client 방식에 대해서 아실껍니다.

일반적으로 적은 규모에서는 Client Per Process 의 방식이 구현이 편하고, 적당한 성능을 발휘해 주는 데,

처리량이 많거나 동시 접속 수가 많다면, Client Per Process 방식은 좋은 방식은 아닙니다. 그래서 선택하게

되는 것이 Preforked 방식입니다.( Thread 방식에서는 Thread Pool 을 많이 사용합니다. 똑같이 Thread Per Client

나 PreThread 방식도 사용가능합니다. )

그런데 이럴 때, Accept 를 어떻게 할 것인지에 대해서 고민을 하게 됩니다.

보통 일반적으로 쉽게 사용할 수 있는 모델은 Accept Socket 을 생성한 후 Fork 해서 Child 에서 직접적으로

Accept를 하게 되는 모델입니다. Unix Network Programming 의 서버 모델을 참고하세요.

그런데 위의 모델의 경우 Thundering herd problem 이라는 것에 걸리게 됩니다. 같은 Descriptor 에 대해서

Acceptor 나 Select 를 하게 되어서 Event 를 대기하게 된 Process 들이 하나의 Event 가 발생했을 때,

모두 wake up 하게 되어서 서로 경쟁하고 그 중 하나만 Accept 가 성공한 다음 다시 나머지 Process 들이 Sleep

해야 되는 이슈가 발생하게 되는 겁니다. 그래서 Unix Network Programming 의 책을 보게 되면, Parent 에서만

Accept 를 하고 실제 accept 된 socket 은 CHILD 에게 passing 하므로써 해당 Process 가 해당 Client 를 처리하게

하는 모델이 가장 좋다고 제안합니다.(첫번 째 모델로, Lock 을 이용해서 전체 중에 Lock을 획득한 하나만 Accept 를 하게

하는 방법도 있습니다.)

Thread 라면 굉장히 쉽게 File Descriptor 전달이 쉽습니다. 그런데 Process 간의 전달은 조금 힘이듭니다.

Advanced Progamming in Unix Environment 에 보면 해당 방법이 나와있는데, 예제 코드만 보면 이해가 어려워서

특히 Python 에서 어떻게 하는지 어렵기 때문에 간단한 샘플을 만들어보았습니다. 결과만 보면 쉬운걸 고생했습니다.

먼저 Python 에서 직접전으로 FD 전달이 안되고 linux 에서는 UnixDomianSocket 을 이용한 sendmsg, recvmsg 를

사용해야 합니다. 검색을 해보면 이것외에, fcntl.ioctl 을 이용하는 방법들이 있는데 linux 에서는 안되더군요. T.T
(왜 2.6에서도 안되는 T.T)

단 이미, 선지자들이 sendmsg, recvmsg binding 을 만들어 두어서 쉽게 사용할 수 있습니다.

python-passfd 를 설치하면 됩니다.

#!/usr/bin/env python

import os
import sys
import subprocess
import fcntl
import SocketServer
import socket, time
from passfd import sendfd, recvfd
import _passfd

m_max_process_cnt = 10
m_cur_process_cnt = 0
os_pipe_list = []

count = 0
class SocketHandler(SocketServer.StreamRequestHandler):
    """
    """

    def handle(self):
        global count
        sock = self.request
        if count == 10:
            count = 0

        pout = (os_pipe_list[count])
        print "pipe out" , str(pout), sock.fileno()
        try:
            sendfd( pout, sock )
            sock.close()
        except Exception, e:
            print e

        count += 1

def child_do(index, pin):
    'child_do()'

    try:
        while True:
            'do'
            fd, m = recvfd(pin);
            print index, fd, m
            sock = socket.fromfd(fd,1,1)
            sock.send("1234")
            sock.close()
            return

            while True:
                data = sock.recv(1024)
                if not data:
                    print "closesocket: ", sock.fileno()
                    sock.close()
                    break
                else:
                    sock.send(data)
    except:
        sys.exit(1)

def parent_do():
    'parent_do()'
    try:
        server = SocketServer.TCPServer( ("localhost", 8000), SocketHandler )
        server.serve_forever()
    except:
        sys.exit(1)

if __name__ == "__main__":

    for i in range(m_max_process_cnt):
        (pin, pout) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0)
        print pin, pout

        pid = os.fork()

        if pid == 0:
            pin.close()
            child_do(i, pout)
        else:
            pout.close()
            os_pipe_list.append(pin)

    parent_do()

    sys.exit(0)