본문 바로가기

Programming/Network/Server/DB

PHP+MySQL을 이용한 웹게임 개발

http://115.68.7.121/mediawiki/index.php/Engineering_php%2Bmysql_web_games


이 문서는 PHP + MySQL로 웹게임 제작시 고려해야할 기술적인 문제에 대해 정리한 것이다.

Contents

 [hide]

[edit]Concurrency control

여러 명이 서버에 여러명이 접근하게 되면 웹게임이든 온라인 게임이든 동시성(concurreny)을 고려하고 제작을 해야한다. 동시에 여러명의 사용자가 게임에 접근하게 되면 database의 쿼리나 PHP코드 실행 순서 동기화가 깨지면서개발자가 생각하지 못한 식으로 갱신이 이루어지게 될 수 있다.

예를 들어 PHP 스크립트를 통해 다음과 같은 MySQL쿼리문 두개를 요청하였다고 하자. 다음 쿼리는 uid가 1인 플레이어가 가진 돈을 가져온후 100증가시키는 일을 하고 있다.

$result = mysql_query("SELECT money FROM player WHERE uid=1");
$row = mysql_fetch_row($result);
mysql_query("UPDATE SET money=$row[0] + 100 FROM player WHERE uid=1");

위와 같은 코드가 PHP 스크립트를 실행하는 프로세스 여러개에서 실행된다면 무슨 일이 일어날까? 위 PHP 코드를 두번 실행하면 돈이 200상승 한다고 생각하는게 자연스럽다. 하지만 다음과 같은 상황이 발생하면 돈은 100만 상승하게 된다. Process #1,#2 모두 같은 값의 돈의 크기에 100을 더해주는 상황이 발생하는 것이다.

  • 1. process #1이 SELECT쿼리 요청
  • 2. process #2가 SELETT쿼리 요청
  • 3. process #1이 UPDATE쿼리 요청
  • 4. process #2가 UPDATE쿼리 요청

이와 같은 동시 접근에 따른 순서가 보장되지 않는 문제는 MySQL의 storage engine을 MyISAM을 쓴다면 table lock으로, InnoDB라면 row level lock을 통해 해결할 수 있다. InnDB라면 위의 두개의 쿼리를 하나의 Transaction으로 묶어주고 SELECT ... FOR UPDATE문으로 SELECT 쿼리를 변경해준다.

mysql_query("START TRANSACTION");
$result = mysql_query("SELECT money FROM player WHERE uid=1 FOR UPDATE");
$row = mysql_fetch_row($result);
mysql_query("UPDATE SET money=$row[0] + 100 FROM player WHERE uid=1");
mysql_query("COMMIT");

위와같이 수정하게 되면 SELECT FOR UPDATE에서 접근된 row들은 다른 클라이언트에서 정보를 변경하지 못하게 lock이 걸리게 되어 쿼리 순서 직렬화를 보장받을 수 있는 것이다. 그런데 MySQL의 lock 기법은 transaction isolation level, storage engine에 따라 동작 방법이 틀려진다. 내부에서 일어나는 복잡한 동기화 구현방식을 이해하지 못하게 되면 문제 발생시 해결이 어려워질 수 있다. 게다가 row level lock은 table lock에 비해 빠르게 동작하지만 dead lock의 발생 위험을 지니고 있다. MySQL의 복잡한 row level locking 동작방식을 신경쓰지 않고 다른 방법으로 쿼리 순서 동기화를 처리할 수 있는 방법이 없을까? 순서 동기화 문제는 MySQL만의 이슈가 아니다. 다른 시스템을 이용하여 동시성 문제를 해결할 수도 있다는 말이다. 그럼 웹게임 제작시 사용될 수 잇는 여러가지 동기화 기법에 대해 알아보자.

platformsolution
Game server (one thread)Not Required
Game server (multi thread)Critical Section
WebServer + PHPSemaphore, Custom lock (ex)memcached lock)
MySQLtable lock, row level lock, 하나의 쿼리문으로 합쳐서 요청, procedure로 요청

제일 먼저 보통 온라인 게임에서 쓰이는 socket방식의 game server부터 알아보자. 해당 서버가 원스레드라면 동기화 문제는 일어나지 않으므로 해결할 필요가 없다. 만약 멀티스레드라면 critical section과 같은 동기화 객체를 이용하여 실행 순서를 보장하면 된다.

PHP는 코드 실행 동기화를 위해 Semaphore 함수를 제공한다. web serve가 여러개의 PHP프로세스를 실행하더라도 Sempaphore객체가 순서를 보장해주기 때문에 동시성 문제가 일어나지 않지만 Semaphore 함수는 해당 서버 머신에서만 동기화를 보장하기 때문에 확장성이 떨어진다. 여러 머신간의 코드 실행 동기화는 Memcached를 이용한 lock기법을 사용할 수 있다(이는 PHP가 여러 머신간의 세션 공유를 위해 Memcached에 세션정보를 저장하고 있는 방식과 유사한 개념이다)

MySQL은 쿼리의 실행 동기화를 위해 transaction을 통한 lock 기법을 사용한다.

정리하면 웹게임 제작시에 추천하는 쿼리 순서 동기화 방법은 다음과 같다.

  • 1. Gamer server의 critical section을 통한 쿼리 동기화
  • 2. MySQL의 Transaciotn을 통한 lock 기법을 통한 쿼리 동기화(InnoDB)
  • 3. Memcached로 구현된 custom lock을 구현해 쿼리 동기화

Critical section을 통해 특정 코드 영역을 동기화 하면 쿼리문 요청 순서도 같이 동기화 된다. 타 머신간의 쿼리 동기화가 불가능하므로 확장성면에서는 불리하다. MySQL의 transction을 통한 lock은 숙련자가 사용하면 동시성 보장이 되면서 성능도 우수하게 만들 수 있다. 하지만 isolation level, transacion, storage engine에 따라 같은 쿼리문도 lock의 동작이 틀려져 transaction이 적은 웹게임에서는 과한 해결방법이 될 수 있다.

과거 바이시티 개발시에 MySQL의 쿼리 순서 동기화를 위해 특정 키를 memcached에 저장하고, 처리가 끝나면 키를 삭제해서 한번에 명령을 하나만 처리하는 식으로 동시성문제를 해결했다. lock되어 있는 상태에서 다른 유저가 해당 처리를 요청하면 "처리에 실패했습니다" 라는 메시지를 발생시키고 유저의 요청처리를 취소했기 때문에 매끄럽지 못했다. 이를 해결하기 위해서는 php의 while문을 이용하여 spin-lock형태로 memcached를 이용하면 된다. lock wait time을 너무 길게 잡으면 웹 브라우저가 웹서버 프로세스를 계속 물고 있게 되어 다른 유저들이 접속하지 못하는 상황이 발생할 있는 점은 주의해서 만들어야 한다.

[edit]Development design

웹게임 개발을 위한 시스템 구성은 다음 세가지가 대표적이다.

  • 1. Web server and game server(one thread)
  • 2. Web server and game server(multi thread)
  • 3. Only web server

첫번째 구성은 Web server는 게임을 보여주는 역할을 하고 게임 로직처리는 게임서버에서 수행하는 모델. 원스레드로 서버 하나로 유저 요청을 모두 처리하게 되면 MySQL 쿼리에 대한 동시성 문제가 일어 나지 않는다. 이 모델은 서버는 한가지 처리밖에 진행 못하기 때문에 조금이라도 처리가 길어지는 요청이 들어오게 되면 다른 요청을 처리하지 못하고 지연되는 현상이 일어나게 된다. 이런 병목현상이 일어나게 되면 필경 원스레드 서버를 자원갱신 서버, 유저요청처리 서버등의 역할별로 분리하게 될 것이다. 그렇게 되면 다시 외부 기능을 이용하여 동시성 문제를 해결해야 한다.

두번째 구성은 Web server는 게임을 보여주는 역할을 하고 게임 로직처리는 게임서버에서 수행하는 모델. Game server내에서 critical secion을 사용하여 외부 기능을 이용하지 않고 동기화 수행이 가능. Game server code 순서가 보장되면 MySQL 쿼리의문의 순서도 동기화 된다. 멀티스레드 방식이기 때문에 1번에서 제기 되었던 성능문제가 없다. 이 모델 역시 하나의 월드에 대해 게임서버를 여러대 운영할 경우 외부 기능을 이용하여 동시성 문제를 해결해야 한다.

세번째 구성은 Web server가 게임을 보여주는 역할과 로직처리를 모두 수행하는 모델. Web server가 설치된 머신에서는 PHP의 Semaphore를 이용하여 코드를 직렬화 할 수 있지만 여러 머신간의 동시성 문제는 외부 기능을 이용하여 동기화 해야 한다.

부족전쟁류 웹게임이라면 첫번째 모델인 Web server and game server(one thread)를 이용하는 것이 적절하겠다. game server를 운영하게 되면 memory에 게임상태를 저장할 수 있어 좀더 유연한 컨텐츠 기획이 가능해진다. 그리고 원스레드 서버이기 때문에 스레드를 썻을때 생기는 여러가지 개발 이슈에서 자유롭게 된다. 역할별로 분리하여 분산처리하면 확장성도 가능해지게 된다. 하지만 점점 더 웹게임은 온라인 게임과 같은 게임 콘텐츠를 원할 것이므로 일반적인 온라인 게임의 시스템 구성방식인 두번째 모델 사용이 많아 질 것으로 예상된다.

[edit]Performance

웹게임 성능을 좌우하는 요소는 무엇인가? 스크립트만의 처리는 빠르지만 문제는 다른 모듈혹은 서버간의 통신속도이다.( MySQL, Memcached) 특히 다음 요소에 주의를 기울여야 한다.

  • 페이지당 쿼리 개수 10개 이하로 유지
  • 최대한 이미지 개수 줄이기(이미지 하나하나가 HTTP 요청이다). 캐싱이 된다고 하지만 갑자기 최초방자가 몰리는 상황이라면?
  • CDN으로 분리하자
  • 필요한 모듈만 include한다. include명령어 자체는 eaccelerator로 캐싱하면 i/o부하는 일어나지 않으므로 큰 문제는 아니다. 하지만 스크립트 파싱도 의외로 속도가 들어갈 수 있다.

성능을 체크하기 위해서 JMeter등의 툴을 사용한다.

  • 초당 몇 개의 요청을 처리할 수 있는가

많은 수의 js파일이 있으면 성능저하가 일어나는지도 확인이 필요한다. Nginx+php_cgi와 같이 여러클라이언트를 하나의 프로세스에서 처리하는 형태를 가진 웹서버에서 웹스크립트 에러는 hang을 일으켜 성능을 저하시키는 원인이 된다. 바이시티 테스트시에 Nginx의 경우 hang으로 인한 BadGateway 에러등을 내보내기도 했었다. Apache와 같이 클라이언트당 하나의 프로세스를 실행시키면 그 프로세스만 문제가 생기지만 nginx의 경우 여러 클라이언트를 하나의 프로세스에서 처리하기 때문에 오류가 발생하면 잠시 동안 프로세스가 제일을 처리 못하는 것으로 보인다.

siege,ab등의 툴을 이용해 초당 Transaction수를 파악하도록 한다.( Redorf rasmus의 문서참고: http://hardworker.tistory.com/102 ,http://talks.php.net/show/flux) yslow를 이용해서 페이지를 분석한다.

[edit]Web server

웹서버는 nginx 사용(Nginx + fastcgi)한다. apache같은 웹서버보다 사용자 폭주시 안정적으로 버텨준다

[edit]Database

하드웨어 개선을 통해 6000쿼리까지도 처리가능하다고 하지만 기본적으로 초당 3000이상 요청하면 안된다고 생각해야 한다. 웹게임의 성능은 Database최적화에 달려 있다고 해도 과언이 아닌다. 이때 Web server와 database사이에 game server나 memcached등의 미들웨어를 통해 database 접근을 줄여서 빠르게 처리할 수도 있다. GDC강연을 보면 FarmVill도 MySQL의 바로 접근하지 않고 memcached pool을 통해 성능 개선을 한것을 알 수 있다.

데이타베이스 설계시 중요한 것은 테이블 설계를 적절히 하는 것이다. 너무 많게 테이블을 나눠 구성하면 Transaction 코드가 많아질 수밖에 없다. 그러면 실행 순서 동기화 문제가 발생하면 lock을 통해 동기화 해야 되므로 어려움이 예상되기 때문이다. 기본적으로 웹게임이든 온라인 게임이든 단일문의 짧은 쿼리로 데이타 갱신을 처리하고 필요한 부분에만 조심스럽게 transaction lock을 걸어주는 것이 성능과 안정성 면에서 모두 좋은 전략이다.

[edit]Deadlock

데드락은 트랙잭션이 무한히 기다리는 교착상태에 빠진것을 말한다. 혹은 lock wait time이 너무 길어져서 설정된 시간을 넘겨서 transaction이 실패나는 경우에도 데드락이라고 한다

예를 들면 다음과 같은 쿼리가 호출되는 상황에서는 dead lock이 발생한다.

  • 1. client #1> BEGIN; SELECT name FROM table LOCK IN SHADE MODE;
  • 2. client #2> BEGIN; SELECT name FROM table LOCK IN SHADE MODE;
  • 3. client #1> UPDATE SET name= “test1”table; (lock wait .. )
  • 4. client #2> UPDATE SET name= “test2”table; (lock wait .. dead lock!)
  • 5. client #1> COMMIT;

1번과 2번 쿼리가 UPDATE를 제한하는 읽기락의 종류인 LOCK IN SHADE MODE를 사용하였다. 하지만 같은 row을 두고 두개의 클라이언트 모두 lock wait상황이 빠졌기 때문에 어느 클라이언트도 COMMIT을 통해 트랜잭션을 끝낼수가 없다. 그래서 client #2에서 dead lock이란 에러가 뜨고 트랜잭션이 종료된다. 그외 lockwait timeout이 있는데 이는 다른 트랜잭션에 의한 락이 오랫동안 풀리지않는 경우 발생하는 에러이다. 보통 10초가량 세팅해 놓는다. 이것도 dead lock상황의 하나라고 볼수 있다. 게임에서는 추천하지 않지만 일반 웹어플리케이션의 경우 dead lock 상황이 나오면 다시 요청해주는 코드를 추가해주는 것도 고려할만 한다.

게임 개발시 transaction 쿼리는 많지 않다. 많이 사용하는 것 자체가 문제지 많은 transaction에 의한 dead lock을 해결하고자 lock 코드를 변경하거나 외부 custom lock객체등을 이용해 쿼리 순서를 동기화 하는 것은 일을 더 크게 만드는 것이다. 테이블 구조와 게임 기획 변경을 통해 단일 쿼리로 처리할 수 있게 만들어 lock걸리는 상황이 아예 사라지도록 변경하는 것이 옳은 전략이다

[edit]Authentication

쿠키인증은 빠르다. 세션인증은 상대적으로 느리다.(memcached를 이용해서 저장하여 최적화 가능) 쿠키인증이든 세션인증이든 결국 쿠키를 사용하기 때문에 XSS를 통해 쿠키를 훔쳐오는게 가능해진다. 로그인시마다 유효키를 발급받아 셰션에 저장하고 이를 모든 요청에 넣어서 보내주는 방식이 가장 무난할 것으로 보인다.

[edit]Security

웹게임의 보안이슈는 다음과 같다.

  • 입력값는 모두 XSS, SQL Injection을 대비해서체크
  • Parameter Query방식을 통해 Injection방어와 효율성 개선 -> 메모리사용률도 줄어든다.
  • URL입력에 ../을 통해 상위 path로 이동이 가능한 경우도 있따. 이를 통해 /etc/passwd파일을 열어 볼 수 있는 경우도 있었다
  • 사용안하는 port는 블로킹 처리
  • DDOS 방어는? Nginx용 모듈을 찾아보자
  • Nginx는 기본적으로 많은요청에 대해 잘 방어한다. 처리 못하는 유저가 발생한지 아파치처럼 완전히 멈추지는 않는다
  • 하드웨어적으로 상위에서 방어해줘야 한다. DDOS는 어쩔수가 없다. 특정 아이피차단 정도가 할 수 있는 방어의 전부
  • 로그인시 POST 전송 패스워드 암호화 . 네이버 인증시스템을 보면 공개키암호화 시스템이 도입되어 있는 것을 볼 수 있다. 즉 자바스크립트를 딴에서 아이디/암호 문자열 자체를 공개키로 암호화해서 POST전송에 들어가는 내용까지 보호해준다. 이 처리까지 수행하기에는 현실적인 어려움이 따른다. 네트워크 스누핑에만 노출안된다면 별 문제가 없을 것으로 본다.

그외 BurpSuite등의 툴로 취약성 파악이 필수다. rasmuas의 security관련 문서도 읽어보자( http://talks.php.net/show/flux )