[Python] ThreadingMixIn 이용 Non-blocking XMLRPC Develop Tip

파이썬에서 어떤 API를 제공하는 다양한 방법이 있습니다.
(일전에 Flask를 이용한 Restful API 를 소개한 적이 있습니다)

하지만 더 기본적인 API는 XML-RPC 로 제공하는 경우도 많습니다.

아래와 같은 간단한 XML-RPC 서버를 살펴보겠습니다.
(파일명 b_xmlrpc_server.py)

#!/usr/bin/env python
#coding=utf8
"""
====================================
:mod: 테스트용 blocking XML-RPC 서버
====================================
.. moduleauthor:: 채문창 <mcchae@gmail.com>
.. note:: GNU

설명
=====

테스트용 blocking XML-RPC 서버
"""
 
##########################################################################################
import time
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
 
##########################################################################################
class TestSvc(object):
def ping(self, pid, _sleep=0):
for i in xrange(_sleep):
print "[%d] %d" % (pid, i)
time.sleep(1)
return True
 
##########################################################################################
def doSvc():
mgr = TestSvc()
class RequestHandler(SimpleXMLRPCRequestHandler):
rpc_paths = ('/TestSvc')
server = SimpleXMLRPCServer(('0.0.0.0', 9000),
                           requestHandler=RequestHandler,
                           logRequests=False,
                           allow_none=True,
)
server.register_introspection_functions()
server.register_instance(mgr)
server.serve_forever()
 
##########################################################################################
if __name__=='__main__':
doSvc()

TestSvc가 서비스 클래스이며 ping 이라는 함수가 하나 있습니다.
_sleep에 5가 오면 1초씩 sleep을 하면서 어느 pid에서 몇번째 (0-based) sleep을 하는가를 출력합니다.

이를 간단히 호출하는 클라이언트 프로그램은
(파일명 xmlrpc_client.py)

#!/usr/bin/env python
#coding=utf8
import os
import xmlrpclib
proxy = xmlrpclib.ServerProxy("http://localhost:9000/TestSvc")
print "[%d] TestSvc.ping(5)=%s" % (os.getpid(), proxy.ping(os.getpid(), 5))

와 같이 작성할 수 있습니다.

이제 테스트를 위하여 간단한 bash 스크립트를 만들어 보았습니다.
(파일명 xmlrpc_client.sh)

#!/bin/bash
 
python xmlrpc_client.py &
python xmlrpc_client.py &
python xmlrpc_client.py &
python xmlrpc_client.py &
python xmlrpc_client.py &
 
FAIL=0
sleep 2
for job in `jobs -p`; do
wait $job || let "FAIL+=1"
done
 
if [ "$FAIL" == "0" ]; then
echo "Done!"
else
echo "FAIL! ($FAIL)"
fi

클라이언트 호출을 백그라운드로 5개를 돌리고, 끝날 때 까지 wait 하는
간단한 스크립트 입니다.

$ python b_xmlrpc_server.py
와 같이 서버를 동작시키고

$ time ./xmlrpc_client.sh
라고 실행시키면

[4038] TestSvc.ping(5)=True
[4041] TestSvc.ping(5)=True
[4042] TestSvc.ping(5)=True
[4039] TestSvc.ping(5)=True
[4040] TestSvc.ping(5)=True
Done!

real 0m25.141s
user 0m0.125s
sys 0m0.115s

와 같이 백그라운드로 병행해서 호출하였는데도 시리얼한 결과로 5x5 = 25초의 결과가 나왔습니다.

이 결과는 XML-RPC 호출을 하면 그 함수가 blocking 함수로 동작을 하여
서버는 순차적으로 돌기 때문에 발생한 결과 입니다.

서버의 결과를 확인하여도,

[4038] 0
[4038] 1
[4038] 2
[4038] 3
[4038] 4
[4041] 0
[4041] 1
[4041] 2
[4041] 3
[4041] 4
[4042] 0
[4042] 1
[4042] 2
[4042] 3
[4042] 4
[4039] 0
[4039] 1
[4039] 2
[4039] 3
[4039] 4
[4040] 0
[4040] 1
[4040] 2
[4040] 3
[4040] 4

와 같이 순차적으로 실행되었음을 알 수 있습니다.

그러면 이런 blocking XML-RPC를 해결할 좋은 방법은 없을까요?

위에 그 해답이 있었습니다. 
TCP 소켓 서버는 자체로 synchronous (Blocking) 이기 때문에 ForingMixIn 또는 ThreadingMixIn 을 사용하라는 것이지요.
클래스를 사용하면 ThreadingMixIn 방식이 편리하지만 바로 GIL (Global Interpreter Lock) 문제에 걸릴 수 있습니다.

여기서 잠깐 Python의 Thread를 확인해 볼까요?

파이썬으로 클라우드 하고 싶어요 에서 너무나 쉽게 잘 설명해 놓으셔서 그 그림을 이용해 보았습니다.


GIL 이라는 하나의 자물쇠를 이용하므로,

이렇게 병렬로 쓰레드가 구동되기를 바라지만,

위와 같이 병렬로 되지 않고 시리얼 처럼 작업할 수 있습니다.

하지만... 실제로는,

위와 같이 I/O 작업을 하게되면 GIL일 풀리는 효과를 가져와서 병렬 이득이 실제로 생깁니다.

따라서 다음과 같이 서버 프로그램을 수정해 보았습니다.
(파일명 n_xmlrpc_server.py)

#!/usr/bin/env python
#coding=utf8
"""
====================================
:mod: 테스트용 non-blocking XML-RPC 서버
====================================
.. moduleauthor:: 채문창 <mcchae@gmail.com>
.. note:: GNU

설명
=====

테스트용 non-blocking XML-RPC 서버
"""

##########################################################################################
import time
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler

##########################################################################################
class TestSvc(object):
def ping(self, pid, _sleep=0):
for i in xrange(_sleep):
print "[%d] %d" % (pid, i)
time.sleep(1)
return True

##########################################################################################
def doSvc():

mgr = TestSvc()
from SocketServer import ThreadingMixIn
class SimpleThreadXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer):
pass
class RequestHandler(SimpleXMLRPCRequestHandler):
rpc_paths = ('/TestSvc')
server = SimpleThreadXMLRPCServer(('0.0.0.0', 9000),
                           requestHandler=RequestHandler,
                           logRequests=False,
                           allow_none=True,
)
server.register_introspection_functions()
server.register_instance(mgr)
server.serve_forever()

##########################################################################################
if __name__=='__main__':
doSvc()


자, 이제는 

$ python n_xmlrpc_server.py 
를 실행시키고

$ time ./xmlrpc_client.sh 
를 실행시켰더니...

[4060] TestSvc.ping(5)=True
[4058] TestSvc.ping(5)=True
[4059] TestSvc.ping(5)=True
[4062] TestSvc.ping(5)=True
[4061] TestSvc.ping(5)=True
Done!

real 0m5.126s
user 0m0.122s
sys 0m0.113s


왓! 평행하게 돌았던 결과 25초 대신 5초 언저리의 결과가 나왔네요...

서버 결과도 확인해 보면,

[4060] 0
[4058] 0
[4059] 0
[4062] 0
[4061] 0
[4060] 1
[4058] 1
[4059] 1
[4062] 1
[4061] 1
[4060] 2
[4058] 2
[4059] 2
[4062] 2
[4061] 2
[4060] 3
[4058] 3
[4059] 3
[4062] 3
[4061] 3
[4060] 4
[4058] 4
[4059] 4
[4062] 4
[4061] 4

와 같이 시리얼하지 않음을 알 수 있습니다.




어느 분께는 도움이 되셨기를...


핑백

  • 지훈현서 : [Ubuntu] Supervisor 를 이용한 서비스 동작 및 모니터링 2015-10-02 08:54:16 #

    ... 와 같은 파일을 특정 폴더에 옮기고build.sh 를 수행하면 됩니다. (우분투 12.04 LTS에서 수행 및 테스트) xmlrpc 소스는 예전에 non-blocking XMLRPC 구현 방법 내용을 수정한 것입니다. 테스트는 간단한 xmlrpc 서버를 돌립니다.특정 포트를 받아 그 포트로 서비스를 열고 있습니다. xt_8997.c ... more

덧글

댓글 입력 영역

구글애드텍스트