안녕하세요 . DB 팀 임현수입니다 . 본 세션에서는 성능 향상을 위한 데이터베이스 아키텍쳐와 개발에 대한 소개를 드리고자 합니다 .
DB 성능에 관심을 가져야 하는 이유와 요소에 대해서 알아보고 , 장애 사례와 게임 시스템 구성 방식 , 개발 가이드에 대해서 이야기드리겠습니다 . 제가 1, 2 장을 발표하고 , 박숙봉님께서 3 장 개발 가이드를 발표하도록 하겠습니다 .
그럼 시작해보도록 하겠습니다 .
성능 문제 발생 시점이 게임 중요한 터닝 포인트가 되어지는 시점입니다 . 종종 DB 문제가 발생하여 게임서비스 사용자 증가의 발목을 잡는 경우가 있었습니다 . 그래서 문제가 발생하기 전에 성능에 대한 고려나 검토가 필요합니다 .
4 가지 요소로 구분할 수 있음 .
4 가지 요소로 구분할 수 있음 .
이 장에서는 하드웨어 성능 이슈 해결했던 사례를 같이 살펴보고 이러한 문제 발생을 줄이기 위해서는 DB 시스템 구성을 어떻게 하는 것이 좋을지 보도록 하겠습니다 .
피크타임에는 문제가 없는데 , 동시접속자가 적은 새벽에만 문제가 발생함 .
클러스터링 환경 , Raid 5 로 구성 ( 디스크 4 장 ) 클러스터링 환경에서 리소스 지연 현상 발생 시 , 안정적인 서비스 제공을 위해서 대기 장비로 서비스가 이전됨 . 고가용 솔루션인 클러스터링은 DB 서버스가 내려갔다가 올라오기 때문에 해당 게임서버도 이에 맞추어 개발이 되어져 있지 않으면 자동 이중화의 의미가 없다 . 문제 인지가 늦어질 수 있어 문제가 커질 수 있으니 주의 필요함 .
장비 성능 개선을 위해서 구매가 필요하여 시간이 오래 소요됨
체크 포인트에 대해서 간단한 소개 메모리에 있는 더티 페이지를 디스크에 동기화하는 프로세스를 체크포인트라고 하는데 해당 시점에는 많은 량의 페이지를 디스크에 쓰게 됩니다 . 디스크 장비 성능이 안 좋을 경우 서비스에 영향을 줄 수 있습니다 . Recovery Interval 파라미터 설정에 따라서 동작주기는 DBMS 가 알아서 결정함 .
체크포인트 시점에 300 배 이상 느려짐을 볼 수 있음
디스크 장비의 성능 개선이 필요함 . SQL 서버 최신 버전이 되어지면서 checkpoint 프로세스가 개선되어 , 서비스 IO 가 지연되어지는 경우 체크포인트 프로세스가 천천히 움직입니다 . 임시 방편으로는 Recovery Interval 을 줄여서 대응 가능함 . 해외의 경우 문제 해결에 긴 시간이 요소되어짐 .
Scale up 은 한계가 있기 때문에 글로벌 서비스를 목표로 한다면 Scale out 전략으로 접근해야 한다 . 필요시에 확장이 가능하다 .
Middle 에서 비동기 적으로 내려주는 양이 db io 처리를 해주는 양만큼이 되기 때문에 해결되는 것도 있음
Io 분산이 gamedb 와 logdb 를 물리적으로 DB 만 분리해서 해결 한 것인지 db 분리를 통해서 해당 파일을 드라이브 분리 및 서버 분리를 통해서 가능한것인지에 대해서 명료하게 전달 필요
얻을 수 있는 장점은 > DB 크기가 작아져서 장애시 복구 시간 단축 > 게임 사용자 분석을 위해서 점점 로그를 많이 기록하는 추세인데 분리하여 관리함으로써 요구사항을 수용하기 용이하다 .
T-SQL 개발 가이드 부분을 담당한 DB 팀 박숙봉 입니다 . 그동 안 저희 팀에서 DB 개발과 운영을 하면서 겪었던 사례를 통해서 여러분들이 앞으로 개발하는데 도움이 될만한 내용을 알려드리겠습니다 .
앞에서 설명하신 io 병목은 SQL 서버 성능에 많은 영향을 줍니다 ., 또 메모리는 IO 병목을 줄이는데 매우 중요하구요 그래서 잠시 sql 서버가 메모리를 어떻게 사용하는지에 대해 간단하게 설명해드리겠습니다 . SQL 서버는 데이터를 접근 할때 먼저 메모리를 살펴보고 , 메모리에 없을 경우에는 디스크에서 해당 데이터를 메모리로 올린 뒤 원하는 데이터를 가져옵니다 .. 만약 메모리가 부족하다면 , 기존 메모리에 있던 데이터를 디스크에 페이징해서 공간을 확보합니다 . 디스크를 접근하면 IO 비용이 소모가 되므로 메모리 효율적으로 사용하는 건 아주 중요합니다 .
제 세션에서는 메모리 활용하는데 있어서 필요없는것까지 많이 읽어서 문제가 되었던 ,, 페이징과 복권이벤트 , 잘못된 사용으로 메모리 낭비를 초래한 플랜캐쉬폴루션 사례을 말씀드리고 , 마지막으로 메모리랑은 좀 관련이 없지만 oltp 환경에서 종종 발견되어 성능 저하를 끼치는 데드락에 대해 이야기 하겠습니다 ..
일단 페이징
지금 보고 계시는 화면은 A 모 게임의 거래소 스샷입니다 . 보다시피 저 안에서도 DB 의 페이징 사용이 되고 있는데요 ,, 저 수많은 상품들을 리스팅하고 페이징을 어떻게 하나냐에 따라 db 서버의 성능에 많은 영향을 줍니다 . 어떻게 하면 좀더 효율적으로 페이징을 할 수 있는가 ,, 저희 팀에서도 많은 고민을 해 보았는데요
일단 단순한 방법을 생각해본다면 , 단순하게 처음부터 필요한 부분까지 모두다 읽고 ,, 필요한 부분만 넘겨주는 방법이 있는데요 즉 600 페이지를 읽는다고 하면 처음부터 몽땅 다 읽어버리는거죠 ,, 그리고 600 번째 페이지만 넘겨주게 됩니다 . 참 쉬운 방법입니다 .,,, 하지만 ,,
음…좀 그렇죠 ..?
조금 더 머리를 굴려봤는데요 ,, 데이터를 절반으로 나눠서 생각해 봤습니다 . 즉 300 페이지를 읽는다면 첫 페이지부터 300 페이지를 읽어서 300 페이지를 넘겨주는 거고 만약 301 페이지를 읽는다면 ,, 600 페이지부터 읽는 거죠 . 이렇게 해서 두 배나 성능이 개선되었습니다 . 하지만 ,, 중간으로 갈수록 ,, 여전히 느리네요 ..
n 개의 페이지를 하나의 블록으로 묶어 데이터를 읽는 방법입니다 . 1pAGE 는 10 개의 글을 포함하고 1 개의 블록은 10 의 패이지를 포함 이라고 가정을 했습니다 . 내가 10 페이지를 읽는다고 하면 10 페이지가 속한 블록 1 번의 시작점 즉 110 번부터 블록 사이즈 만큼을 읽은 뒤 , 10 페이지만 넘겨주는 방식입니다 . 블록 단위로 나무면 늘 동일한 성능을 보장할 수 있습니다 . 하지만 페이지를 이동할때 내가 처음 발급받은 블록 시작 번호를 기점으로 데이터를 읽기 때문에 최신 글을 읽을 수가 없는 문제가 있습니다 .
해서 ,, 저희팀에서 사용하는 방법은 최근페이지의 경우 최근글이 항상 반영이 되어야 하므로 ,, 첫번째 블록은 최신글부터 ,, 그다음 블록은 블록 시작점을 기준으로 읽어 , 원하는 페이지를 얻어냅니다 .
이해를 돕기 위해 리스트 출력 sp 를 수도코드로 표현해보았는데요 , GETLIST 파라메터로 , 읽을 페이지 번호화 , 블록시작번호를 받고 , 이게 첫블럭일경우 , 최근 글부터 top N 건 ,, 아닐경우는 블럭시작번호보다 작은 top n 건을 SELECT 합니다 .
부하를 최소화 하기 위해 비즈니스 적으로 제한하는 다른 방법들은 첫번째는 게시물의 변경을 반영하지 않는 방법으로 삭제된 게시물도 그냥 노출해버리는겁니다 ,, 이렇게 되면 내가 읽을 페이지의 번호와 사이즈로도 가능합니다 . 두번째는 이 건 W 모 게임의 거래소 스샷인데요 ,, 원하는 페이지 따위는 없다 ! 이전페이지와 다음페이지만 읽을 뿐 .. 잠깐 페이징의 여러 기법에 대해 알려드렸는데요 ,, 각자 자기의 비즈니스 방법에 알맞은 방법을 선택하시면 되겠습니다 . 저희팀에서는 데이터가 많은 프로잭트의 경우 블록 방식을 제안하고 있고 ,, 대부분의 서비스에서 실제 사용하고 있습니다 . 다음 주제로 넘어가겠습니다 .
자 다음은 복권 아이템 발급에 관련된 사례입니다 .
라이브 운영하시는 분들 한번쯤 경험하셨지요 ?
이번 주말에 a 게임 복권 이벤트가 시작되었습니다 . 복권 이벤트에서 랜덤 ID 를 발급하는 쿼리입니다 . 이 쿼리 문은 NEWID 함수를 이용해 ,, lotto 테이블 만큼 랜덤 ID 를 발급해서 탑 1 건을 추출하는 쿼리입니다 . LOTTO 테이블을 전체 스캔한다는 것도 문제이고 , 발급된 랜덤 ID 를 정렬하기때문에 , CPU 를 많이 쓴다건도 문제 입니다 .
개선해보았습니다 . 로또 테이블에는 유니크한 시퀀셜 키가 존재해야합니다 . 이 키의 MAX 값을 @A 변수에 넣고 이 변수를 랜던하게 돌려 ,, 생성된 값을 이용하는 방법입니다 . 이렇게 생성된 값을 이용해서 랜덤을 발번 하면 Full Scan 피하고 필요한 범위만 검색 비용을 훨씬 절감할수 있다
백만건의 테이블로 방금 쿼리들을 테스트 해보았습니다 . 첫번째 쿼리에 비해 두번째 쿼리의 cpu 성능이 2000 배 향상되었습니다 . 물론 일기수도 ,, 900 배 향상되었습니다 . 첫번째 쿼리는 구글링에서 찾아봤는데 ,, MSSQL 랜덤 발급 쉽게 찾을 수 잇는 쿼리입니다 . 하지만 이번 경우처럼 성능을 고려하지 않고 , 서비스에 반영을 하면 ,, 운영하는데 장애가 발생하기도 합니다 . 한번정도는 어떤 영향을 미치는지 생각하는게 좋을 것 같습니다 . 다음 주제로 넘어가겠습니다 .
플랜캐쉬가 뭔지 .. 잠깐 설명드리겠습니다 . Sql 서버가 쿼리를 실행하면 ,, 그림과 같이 플랜이 생성되고 ,, 이를 메모리의 특정 영역에 저장을 하게됩니다 바로 이 메모리의 특정 영역에 캐싱하는걸 플랜케시입니다 . 동일 쿼리가 실행되면 ,, sql 서버는 플랜을 다시 생성하지 않고 ,, 메모리에 이미 저장된 플랜을 사용해서 결과를 반환합니다 . 저희가 프로시져를 사용하는 가장 큰 이유는 이미 생성된 플랜을 재사용하기 위해서지요 ,,
이번 사례는 플랜캐시를 잘 못 사용하여 메모리를 낭비하는 경우입니ㅏㄷ .
현재 중국에서 서비스 중인 ,, b 모 게임에서 발생한 성능 이슈 사례입니다 . 보고 내용은 콜 수에 비해 높은 CPU 를 사용 중이며 ,, ADHOC 쿼리가 거의 없는데도 전체 6G 메모리 중 2.4G 나 플랜 캐쉬로 사용하고 있다는 겁니다 . 정말 확인해보니 매일 초기화를 하는데도 , 초기화 직전까지 40% 를 점유하고 있었습니다 .
어떤 종류의 쿼리인지 dmv 를 통해 알아보았습니다 . 위 표를 보면 PREPARED 쿼리 가 눈에 띄게 높은 수치를 기록한 다는 것을 알수 있습니다 .
플랜캐쉬 내용입니다 . 지금 자세히 보시면 ,, 같은 SP 가 호출되고 있지만 ,, 파라메터의 사이즈가 각각 다른것을 알수 있습니다 . sqL 서버가 , 이 sp 가 같은 SP 인줄 모르고 , 전부 다 캐싱하고 있는겁니다 .
이 한 SP 에 얼마나 쌓였는지 ,, 확인해보았습니다 . 이렇게 많이 짤방 ㅋ
왜 이런 일이 일어났는지 설명해드릴께요 . 이 표는 각각의 메모리 사이즈당 플랜캐시의 할당 제한 수치를 버전별로 나타낸 것입니다 . 지금 이상황은 PLAN CACHE POLLOTION 이라고 해서 ,, 이렇게 되면 메모리에는 데이터가 많이 저장되어야 하는데 ,, 플랜캐시가 과다 점유를 해버리게 된경우입니다 . 데이터가 메모리를 모두 활용하지 못하니까 IO 도 많이 쓰게 되고 플랜도 자꾸 재생성되어 cpU 도 높아지게 됩니다 . 요즘 장비를 추세가 메모리도 사이즈도 크고 , 64bit 부터 할당 범위까지 커져서 , 이런 이슈가 최근에 발견된 거였고 , 2000 를 사용하던 시절에는 4g 메모리에서 OS, 데이터 , 실행계획이 서로 나누어서 사용했기때문에 ,, 과다 점유가 해서 문제가 되는 경우는 거의 없었습니다 .
이 현상을 방지하기 위해서는 어플단에서 SP 를 호출할때 파라메터의 타입과 , 사이즈를 FIX 해야 합니다 . 운영담에서는 매일 플랜캐시를 초기화 하거나 , 각 테이블 별로 플랜 캐시를 강제 매개변수화 하는 방법이 있습니다 . alter table db_baz_charge set parameterization forced Ad-hoc 쿼리가 많이 들어오는 경우 2008 부터 추가된 optimize for ad-hoc query 옵션 사용 sp_CONFIGURE ‘show advanced options’,1 RECONFIGURE 이 주제를 처음 접하시는 분들은 혹시 자기가 관리하는 서버가 ,, 메모리 스케일업을 했는데도 ,, 성능이 생각만큼 향상되지 않았을때 , 한번 확인해 보시면 좋을 것 같습니다 . 자 이제 메모리와 관련된 이야기는 ,, 여기서 마치고 다음 주제로 넘어가겠습니다 .
SQL 에서 발생하는 가장 당혹스런 오류 메시지 중 하나입니다 . 뭐가 잘못되었는지에 대한 오류 메시지는 트랜잭션을 다시 실행하라는 정도입니다 . 지금 소개해 드릴 사례는 가장 일반적인 데드락 상황이랑 ,, 조금은 특이한 경우인 인덱스 커버링이 제대로 안되서 발생한 상황 두가지를 소개해드리겠습니다 .
캐시 충전 복구 작업 중에 , 데드락이 발생하였습니다 .
데드락이 발생한 캐시 테이블에는 USERID 가 넌클로 잡혀있습니다 . SPID 1 은 캐시테이블데이터 전부를 TEMP 테이블에 BULK INSERT 하고 있고 SPID 2 는 , 유저가 캐시를 구입한 순간 캐시테이블에데이터가 입력되는프로세스 입니다 . S1 이 인덱스 페이지를 먼저 읽어 S LOCK 을 획득했고 , S2 가 데이터 페이지 영역에 데이터를 입력하기 위해 X 잠금을 획득하였습니다 . 다음 S2 가 인덱스 페이지까지 입력하기 위해 X 잠금을 요청했고 , S1 이 데이터 페이지를 읽기 위해 S 잠금을 요청했습니다 . 이 상황이 데드락 상황입니다 . 제 3 의 어플이 간섭하기 전까지 이 교착상황은 계속 유지되어 , 아래 두개의 리소스는 잠금 상태에 빠지게 됩니다 .
이 상황은 S1 작업이 바로 변경데이터를 반영해도 되지 않는 경우였습니다 . 해서 격리수준을 조정하는 방법을 선택하였습니다 . 벌크 인서트 부분에 WITH(NOLOCK) 힌트를 줘서 X 락 도중에도 변경전 데이터를 읽어가도록 ,, 즉 더티페이지를 읽도록 설정해서 교착상황을 해결하였습니다 .
S 모 게임에서 간헐적으로 응답 지연 현상이 있다고 해서 모니터링 로그를 보다 보니 ,, 같은 시간 내에 데드락이 발생하고 있었습니다 . 윗그림은 Perfmon 에서의 avgwaittime 수치이고 , deadlock 발생 카운트입니다 . 그림을 보시면 같은 시간내에 수치가 올라와있는것을 확인할수있습니다 .
프로파일러를 사용하면 , 아래 그래프와 같이 ,, 교착상태에 빠진 두 개의 프로세스에 대한 정보를 비쥬얼하게 확인하실수 있습니다 . 쿼리를 찾았고 ,, 재현해보았습니다 . SELECT 구문인 spid 53 번과 update 을 실행중이 spid 54 번 중에 상대적으로 비용이 적은 53 번이 희생되었습니다 . 데드락의 희생자 선정은 얼마나 많은 롤백작업이 필요한가 ,, 사용자 프로세스인가 ? 시스템프로세스인지에 따라 다릅니다 .
Deadlocktest 테이블에는 idx 컬럼이 키이고 , 포괄열로 name 과 nickname 이 잡혀있는 인클루드 인덱스였습니다 . 이 교착상황을 잠깐 설명해드리겠습니다 . 53 번이 idx , name, nickname 이 포함되어있는 인덱스 영역을 읽기 위해 S lock 을 먼저 획득하고 데이터 영역인 registdate 를 마저 읽기 위해 Slock 을 요청한 상태입니다 . 54 번은 idx =1 에 해당하는 로우를 update 하기 위해 registdate 가 포함된 rid 에 x 잠금을 획득했고 ,, 마저 인덱스 영역을 수정하기 위해 ,, 인덱스 키로우에 x 잠금금을 요청했습니다 .
53 번과 54 번은 자주 쓰이는 프로세스이고 , 변경이 바로 반영이 되어야 하는 상황이었기때문에 ,, 격리 수준 조정보다는 엑세스 패턴을 조정하였습니다 . 포괄열에 나머지 registdate 를 추가해서 하나의 리소스만 커버링 하도록 수정하였습니다 . 같은 리소스를 참조하기때문에 53 번이 먼저 잠금을 획득을 하면 54 번은 53 번이 끝날때까지 대기하게 됩니다 . 대기상태는 많이 일어나겠지만 교착상황은 빠지지 않습니다 .
데드락을 최소화 하는 방법을 몇가지 적어보았습니다 . With(nolock) 으로 읽기 허용 수준을 조정하면 왠만한 잠금과 관련된 상황은 해결이 됩니다 . 4 번째 엑세스 패턴의 경우는 ,, 마지막 데드락상황과 관련이 있는데요 , 인클루드 인덱스나 커버링 인덱스를 사용하는 이유가 ,, 룩업을 하지 않기 위해 사용하는데 ,, 이번 건은 시간이 지나면서 이미 생성된 인덱스를 고려하지 않고 select 컬럼을 추가해버려서 , 룩업이 발생했습니다 . 현재 엑세스 패턴을 파악을 해서 적절한 인덱스를 선정하는 것이 바람직하다고 생각합니다 .