NDC/Dev

[NDC 2014] 파이썬과 친구들

MAKGA 2021. 12. 6. 00:21
320x100

웹 프레임 워크

- 종류는 꽤 많다.

    Django, web.py, CherryPy, web2py, Bottle, Flask, ...

- 일반적인 웹 프레임워크에서 제공하는 많은 기능이 게임 서버에선 대부분 불필요 하므로 마이크로 프레임워크가 좋다.

    Flask, Bottle등


Flask

$ pip install Flask
from flask import Flask
app = Flask()

@app.rout('/')
def hello():
	return 'Hello World!'

app.run(debug=True)

# http://localhost:8080/
from flask import request, make_response

@app.route('/login')
def login():
	user_id = request.args.get('user_id', None)
    password = request.args.get('password', None)

    if user_id is None or password is None:
    	return 'Login failed!'

    resp = make_response('Login successed!')
    resp.set_cookie('login_user', user_id)
    return resp

# http://localhost:8080/login?user_id=foo&password&bar
from functools import wraps

def requires_login(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        if request.cookies.get('login_user', None) is None:
            return 'Not signed in!'
        else:
            return func(*args, **kwargs)
        return wrapper

@app.route('/am_i_logged_in')
@requires_login
def login_check():
    return 'Yes, you are.'

# http://localhost:8080/am_i_logged_in

데이터 입출력

되도록 Json을 사용하고 보인이 필요하면 https를 사용하자

from flask import jsonify

@app.route('/leaderboard')
@requires_login
def get_leaderboard():
    return jsonify(names=['John Doe', 'Alan Smithee', 'Hong Gildong'], scores=[1500, 1200, 800], your_rank=7)

# http://localhost:8080/leaderboard
# >>> {'names': ['John Doe', 'Alan Smithee', 'Hong Gildong'], ...

예외처리

# 기본
from flask import abort

@app.route('/fail'):
def epic_fail():
    try:
        v = 1 / 0
        return 'divide one by zero is: ' + v
    except Exception, e:
        handle_exception(e)
        abort(500)

# 미들웨어 방법
class UserDefinedException(Exception): pass

class MyMiddleware(object):
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        try:
            return self.app(environ, start_response)
        except UserDefindException, e:
            handle_exception(e)
        except Exception, e:
            lets_die()

app.wsgi_app = MyMiddleware(app.wsgi_app)

웹 서버 배포하기

app.run()은 테스트용 웹 서버라 실제 환경에 돌리기 부적합

WSGI 인터페이스를 지원해야 한다

Apache에 mod_wsgi 모듈이 있다

개인적으로는 gunicorn 추천

$ pip install gunicorn

파이썬 네트워킹 및 I/O

파이썬은 GIL(Global Interpreter Lock)때문에 스레딩 효율이 떨어진다. Single threaded asynchronous I/O를 추천

중앙 이벤트 루프(I/O 허브 역할)에 콜백을 붙이는 형태로 컨트롤 플로우가 구성된다.

var http = require('http');
var fs = require('fs');

http.createServer(function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    fs.readFile('file.text', 'utf8', function(err, data) {
        res.end(data);
    });
}).listen(1337, '127.0.0.1');

 

Gevent

코루틴 기반 네트워킹 라이브러리

동기 I/O 쓰듯 비동기 I/O 프로그래밍을 할 수 있다.

 - 협력 스레딩 Cooperative threading

 - 스케줄러가 없다

 - 문맥 전환의 주체가 각 스레드다(작업이 없으면 리소스 반환)

 - gevent는 I/O idle이 발생할 시 제어권을 넘긴다

기존 스레딩 방식
코루틴 방식 

$ pip install gevent
import gevent

def func(id):
    number = 0
    while True:
        print 'event id %d: %d' % (id, number)
        number += 1
        gevent.sleep(1)

jobs = [gevent.spawn(func, 1), gevent.spawn(func, 2)]
gevent.joinall(jobs)

 

Monkey patching

파이썬의 표준 라이브러리 함수들의 동작을 변경한다

Blocking I/O를 자동으로 Nonblocking으로 바꾼다

대부분의 파이썬 라이브러리와 호환된다

from gevent.monkey import patch_all
patch_all()

#표준 라이브러리
import urllib2

def read_url_contents(url):
    data = urllib2.urlopen(url).read()
    print 'URL contents of %s is bytes long' % (url, len(data))

job1 = gevent.spawn(read_url_contents, 'http://www.example.com')
job2 = gevent.spawn(read_url_contents, 'http://ndc.nexon.com')

gevent.joinall([job1, job2])

URL contents of http://ndc.nexon.com is 17827 bytes long
URL contents of http://www.example.com is 1270 bytes long

서버 확장하기

싱글 스레드 & 멀티 프로세스

인스턴스 확장을 통해 Scale out 구현

웹 서버의 경우 cpu 개수에 맞춰 프로세스 띄우기 가능한 gunicorn이 좋다

 

클라우드 서비스를 사용한다면 Automatic scaling

On-demand로 서버를 추가/제거

클라우드 API를 통해 전 과정 자동화 가능

적절한 metrics 전략 필요 - e.g 최근 5분간 모든 인스턴스 평균 CPU 사용량이 50% 초과시 Scale out


데이터 베이스

Nosql

Document-oriented Database

스키마가 없어서 저장되는 데이터의 형식 및 구조는 자유

일반적으로 하나의 Primary key로 구성되고 릴레이션이 없다

 

MongoDB

Ad hoc querying(쿼리 조건을 자유자재로)

JavaScript 지원

$ pip install pymongo
from pymongo import MongoClient
client = MongoClient('localhost', 27017)
db = client.test.db
collection = db.test_collection

collection.insert({'name':'Honggildong', 'phone':'+82-10-0000-0000'});
collection.insert({'name':'Kimchunhyang', 'phone':'+82-10-1111-1111'});

print collection.find_one({'name':'Honggildong'})
# {'name':'Honggildong', 'phone':'+82-10-0000-0000'}

 

NoSQL이 능사는 아니다

쌓이는 데이터가 관계성이 큰 경우

Query의 성능 이슈

레코드가 커질수록 생기는 Serialization/Deserialization 오버헤드

트랜잭션의 부재

 

RDBMS

파이썬에서는 표준화된 인터페이스(DBAPI) 제공

 

- SQLAlchemy - 파이썬 DB 라이브러리

- ORM(Object Relational Mapper)

    언어와 DBMS 사이의 인터페이스

- SQL을 직접 짤 필요가 없다

# 데이터 정의
from sqlalchemy import *
form sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Record(Base):
    __tablename__ = 'phonebook'

    id = Column(Integer, primary_key=True)
    name = Column(String(255), nullable=False, index=True)
    number = Column(String(30), nullable=False)

    def __init(self, name, number):
        self.name = name
        self.number = number
# DB 접속 및 스키마 생성
from sqlalchemy.orm import sessionmaker

engine = create_engine('sqlite://test.db')
Session = sessionmaker(bind=engine)

# Create metadata (schema, ...)
Base.metadata.create_all(engine)
# 데이터 삽입
r1 = Record('A', '123')
r2 = Record('B', '456')

# Insert into database
session = Session()
try:
    session.add(r1)
    session.add(r1)
    session.commit()
except:
    session.rollback()
# 데이터 쿼리
# Selecting
for data in session.query(Record).all():
    print data.name, data.number

# Selecting with where clause
for data in session.query(Record).filter(Record.name == 'A'):
    print data.number

 

캐싱

실질적으로는 거대한 해시 테이블

데이터의 영속성은 보장되지 않음

Temporal locality가 큰 데이터의 경우 큰 효과

Memcached, Redis등

$ pip install redis
import redis

con = redis.StrictRedis(host='localhost', port=6379, db=0)
con.set('foo', 'bar')
print con.get('foo')

# bar

워커

게임 서버에서는 특정 기능을 위한 별도의 워커가 필요

싱글 스레드 서버의 경우 CPU가 작업하는 동안 다른 작업이 Block 되기 때문에 CPU intensive task를 구현할 수 없다

작업을 위임하기 위한 dedicated worker를 구현하자

 

Celery

분산 작업 큐이고, 작업은 비동기로 수행한다

Celery와 게임서버간 통신을 위해 RabitMQ와 Redis를 지원한다

# my_tasks.py

from celery import Celery
app = Celery('tasks', broker='redis://localhost:6379/0')

@app.task
def a_very_huge_task(x, y):
    # Assume this is a huge task
    return x + y

$ celery -A my_tasks worker #run the worker
from my_tasks import a_very_huge_task

>>> result = a_very_huge_task.delay(1, 2)
>>> result.ready()
False
>>> result.ready() #After the task's done
True
>>> result.get()
3

# 워커에게 명령내리고 결과값 받기

로깅

logging: 파이썬의 기본 로깅 라이브러리

import logging

logging.basicConfig()
logger = logging.getLogger('foo')

logger.debug('This is debug message')
logger.info('This is informational message')
logger.warning('You should take a look at this')
logger.error('Fix this RIGHT NOW')
logger.critical('Your revenue dropped by half')

 

Logbook

$ pip install logbook

핸들러를 통한 확장 구현이 쉽다

(ciritical log는 메일 전송, 로그 서버로 로그 전송, 에러 로그를 Trello에 게시등)


배치(Deployment) 자동화

DVCS(Distributed Version Control System)를 쓰면 편해진다 (Git, Mercurial...)

프로덕션 환경에 소스 서버를 셋팅해야함

Hook 기능을 통해 반자동 배치 구현 가능

    이벤트 발생시 임의의 명령 실행

    원격 소스 서버에 push 발생시 서버가 알아서 업데이트 후 서버 재시작

 

Fabric: 시스템 관리 명령 및 배치 자동화

$ pip install fabric

ssh를 통한 자동화(소스 업데이트, 서버 재시작, 로그 삭제등)


출처: 박준규 / 넥슨 신규개발본부

https://ndcreplay.nexon.com/NDC2014/sessions/NDC2014_0041.htm

320x100