Se ha denunciado esta presentación.
Utilizamos tu perfil de LinkedIn y tus datos de actividad para personalizar los anuncios y mostrarte publicidad más relevante. Puedes cambiar tus preferencias de publicidad en cualquier momento.

NoSQL 위에서 MMORPG 개발하기

2.274 visualizaciones

Publicado el

〈야생의 땅: 듀랑고〉는 메인 DB로 Couchbase를 쓰고 있습니다. Couchbase에서 게임을 개발하면서 있었던 제약과 그 제약을 해결하기 위한 방법들을 정리해 보았습니다.

Publicado en: Software
  • Sex in your area is here: www.bit.ly/sexinarea
       Responder 
    ¿Estás seguro?    No
    Tu mensaje aparecerá aquí
  • Dating for everyone is here: www.bit.ly/2AJerkH
       Responder 
    ¿Estás seguro?    No
    Tu mensaje aparecerá aquí
  • DOWNLOAD FULL MOVIE, INTO AVAILABLE FORMAT ......................................................................................................................... ......................................................................................................................... ,DOWNLOAD FULL. MOVIE 4K,FHD,HD,480P here { https://tinyurl.com/yybdfxwh }
       Responder 
    ¿Estás seguro?    No
    Tu mensaje aparecerá aquí

NoSQL 위에서 MMORPG 개발하기

  1. 1. NoSQL 위에서 MMORPG 개발하기 〈야생의 땅: 듀랑고〉의 사례를 바탕으로 What! Studio 최호영
  2. 2. 듀랑고 서버유닛 게임플레이파트 루니아 전기 2009-2012 야생의 땅: 듀랑고 2012- 가죽 장화를 먹게 해달라니 NDC2014 〈야생의 땅: 듀랑고〉 중앙 서버 없는 게임 로직 NDC2016
  3. 3. 발표에서 다루는 것 • NoSQL 중 Couchbase의 특징 • Couchbase 위에서 게임 플레이 로직을 만든 경험 • Couchbase가 가지는 장점과 단점
  4. 4. 발표에서 다루지 않는 것 • NoSQL의 역사 • NoSQL의 종류와 성능 비교 • Couchbase를 선택한 이유
  5. 5. 시작하기 전 스키마 데이터의 저장 두개 이상의 문서에 대한 저장과 변경 쿼리하기 정리
  6. 6. NoSQL? NoSQL은 이런 4가지 특징을 공유한다고들 하지만 시류를 가리키는 말에 가깝지도 하고 이미 유행한지도 10년이 가까워 오기 때문에, 요즘엔 딱히 지켜지는 것 같지 않습니다.
  7. 7. 오늘 주로 다룰 Couchbase는 NoSQL의 특징들을 충실하게 지킨 데이터베이스입니다.
  8. 8. 분산형 구조를 사용하고 수평 확장이 용이합니다. 덕분에 부하 분산이 잘 되고, 메모리를 적극적으로 활용기도 하여 캐시급 성능 내는 것이 장점입니다.
  9. 9. {key:value} Couchbase는 key-value store이기 때문에 데이터의 저장/조회가 key를 통해서만 이루어집니다.
  10. 10. JSON Value로는 다양한 포맷을 저장할 수 있지만 JSON을 권장하고 있습니다. Views, N1QL 등을 JSON을 사용할 때만 지원하기 때문입니다. 요 부분은 나중에 다시 자세히 다루겠습니다.
  11. 11. JSON Document-oriented Couchbase는 Key-Value Store의 한 종류인 Document-oriented로도 분류되는데 구조화된 데이터인 JSON을 이용해서 저장/동작하기 때문입니다.
  12. 12. 시작하기 전 스키마 데이터의 저장 두개 이상의 문서에 대한 저장과 변경 쿼리하기 정리
  13. 13. RDB name job level 김농부 농부 34 김군인 군인 27 김학생 학생 29 item level owner 곡괭이 20 김농부 밀짚모자 15 김농부 군번 줄 1 김군인 PlayersItems 스키마란 데이터베이스에 저장되는 자료의 구조나 타입을 얘기합니다. 흔히 RDB에 저장되는 데이터들은 이렇게 테이블의 형태를 취하는데요
  14. 14. Couchbase { name: 김농부 job: 농부 level: 34 items: [ {item:곡괭이, level:20}, {item:밀짚모자, level:15} ] } { name: 김군인 job: 군인 level: 27 items: [ {item:군번줄, level:1} ] } { name: 김학생 job: 학생 level: 29 items: [] } 반면 Couchbase는 JSON 기반의 document 데이터베이스이기 때문에 연관된 데이터를 묶어 하나의 문서에 같이 보관하는 경우가 많습니다.
  15. 15. Couchbase { name: 김농부 job: 농부 level: 34 items: [ {item:곡괭이, level:20}, {item:밀짚모자, level:15} ] } 1 2 3 4 5 6 7 8 class Item: item = declare(unicode) level = declare(int) class Entity: job = declare(unicode) level = declare(int) items = declare(list(Item)) JSON으로 저장할 때의 가장 큰 특징은 로직의 데이터 구조와 유사하다는 점 입니다. JSON이기 때문에 데이터베이스 사용자가 임의로 구조를 정할 수 있기 때문이죠.
  16. 16. Couchbase { name: 김농부 job: 농부 level: 34 items: [ {item:곡괭이, level:20}, {item:밀짚모자, level:15} ] } 1 2 3 4 5 6 7 8 class Item: item = declare(unicode) level = declare(int) class Entity: job = declare(unicode) level = declare(int) items = declare(list(Item)) 때문에 RDB와 다르게 메모리 상의 데이터와 DB상의 데이터의 구조를 일치시킬 수 있어 게임 플레이 로직 개발이 굉장히 수월합니다.
  17. 17. Schemaless 이렇듯 DB에 딱딱한 스키마가 정의되는 것이 아니기 때문에 Schemaless라고 불립니다.
  18. 18. 말랑말랑한 스키마 한국말로 표현하면 말랑말랑한 스키마가 되겠네요.
  19. 19. 말랑말랑한 스키마 수월한 로직 구현 게임 플레이 프로그래머의 책임 증가 한국말로 표현하면 말랑말랑한 스키마가 되겠네요. 하지만 당연히 장점만 있는 것은 아니죠.
  20. 20. 스키마의 변경 스키마를 변경할 때가 대표적이라고 할 수 있습니다.
  21. 21. RDB name job level 김농부 농부 34 김군인 군인 27 김학생 학생 29 Players 게임 디자이너가 “감정 상태"를 추가해 달라는 요청을 해왔습니다.
  22. 22. RDB ALTER TABLE Players ADD emotion VARCHAR NOT NULL DEFAULT(”분노”) 대충 이런 쿼리문을 짜서 돌리면 되겠네요.
  23. 23. RDB A C I D tomicity onsistency solation urability RDB는 여러분이 잘 아시는 대로 ACID 트랜잭션을 지원하기 때문에
  24. 24. RDB name job level emotion 김농부 농부 34 분노 김군인 군인 27 분노 김학생 학생 29 분노 Players 아무리 행이 많아도 단 한번에 초기화 됩니다. 작업이 되다 말거나 중간에 멈추는 일은 없죠. 되거나 실패하거나 입니다.
  25. 25. Couchbase { name: 김농부 job: 농부 level: 34 emotion: 분노 items: [ {item:곡괭이, level:20}, {item:밀짚모자, level:15} ] } Couchbase에서도 하나의 문서를 변경할 땐 ACID 트랜잭션이라고 할 수 있습니다. 읽고, 쓰고, 저장하면 되니까요. 문서 단위로 동작하기 때문에 문서가 저장이 되다가 말거나 하는 일은 없습니다.
  26. 26. Couchbase 하지만 Couchbase를 특별하게 만드는 분산형 구조와 높은 확장성은 다문서를 저장할 때 ACID 트랜잭션을 지원하기 힘들게 만듭니다.
  27. 27. Couchbase JSON 하나의 문서를 저장할 때는 괜찮지만
  28. 28. Couchbase JSONJSON JSONJSON JSON 여러 개의 문서를 저장할 때는 동시에 저장하지 못하고 애플리케이션에서 순서대로 저장할 수 있을 뿐 입니다.
  29. 29. Couchbase { name: 김농부 job: 농부 level: 34 emotion: 분노 items: [ {item:곡괭이, level:20}, {item:밀짚모자, level:15} ] }
  30. 30. Couchbase { name: 김농부 job: 농부 level: 34 emotion: 분노 items: [ {item:곡괭이, level:20}, {item:밀짚모자, level:15} ] } { name: 김군인 job: 군인 level: 27 emotion: 분노 items: [ {item:군번줄, level:1} ] } { name: 김학생 job: 학생 level: 29 items: [] } 때문에 여러 개의 문서를 저장하다 보면 몇 개는 실패할 수도 있죠.
  31. 31. Couchbase { name: 김농부 job: 농부 level: 34 emotion: 분노 items: [ {item:곡괭이, level:20}, {item:밀짚모자, level:15} ] } JSON 수억개 더군다나 문서가 수억개 쯤 된다면 실패한 것들을 재시도해서 저장하는 것도 쉽지 않은 일이 됩니다.
  32. 32. on-demand migration 그래서 저희는 on-demand migration이라고 부르는 기법을 사용하고 있습니다.
  33. 33. on-demand migration { version: 0 name: 김농부 job: 농부 level: 34 items: [ {item:곡괭이, level:20}, {item:밀짚모자, level:15} ] } 1 2 3 4 5 6 7 8 9 class Item: item = declare(unicode) level = declare(int) class Entity: __version__ = 0 job = declare(unicode) level = declare(int) items = declare(list(Item)) on-demand migratio은 문서의 데이터와 메모리 상의 데이터가 동일하다는 부분에서 출발합니다.
  34. 34. on-demand migration { version: 0 name: 김농부 job: 농부 level: 34 items: [ {item:곡괭이, level:20}, {item:밀짚모자, level:15} ] } 1 2 3 4 5 6 7 8 9 class Item: item = declare(unicode) level = declare(int) class Entity: __version__ = 0 job = declare(unicode) level = declare(int) items = declare(list(Item)) 저장된 데이터와 애플리케이션에서 선언한 스키마에 버전을 부여하고 둘이 같아지도록 애플리케이션에서 관리하는 겁니다.
  35. 35. on-demand migration 1 2 3 4 5 6 7 8 9 10 class Item: item = declare(unicode) level = declare(int) class Entity: __version__ = 1 job = declare(unicode) level = declare(int) emotion = declare(unicode) items = declare(list(Item)) 이렇게 Entity의 버전을 0에서 1로 올리고
  36. 36. on-demand migration 1 2 3 4 5 6 7 8 9 10 @migrate(Entity, 0, 1) def migration(document): document[‘emotion’] = ‘분노’ def load(cls, key): doc = db.load(key) if doc[‘version’] != cls.version: doc = migrate(doc[‘version’], cls.version, doc) db.update(key, doc) return unpack(doc) 버전이 0에서 1로 올라갈 때 문서의 마이그레이션을 어떻게 할지 함수로 만들어 둡니다. 그리고 문서를 애플리케이션에서 읽을 때 저장된 버전이 현재와 다르면 해당 함수를 실행시킵니다.
  37. 37. on-demand migration { version: 0 name: 김농부 job: 농부 level: 34 items: [ {item:곡괭이, level:20}, {item:밀짚모자, level:15} ] } { version: 1 name: 김군인 job: 군인 level: 27 emotion: 분노 items: [ {item:군번줄, level:1} ] } { version: 1 name: 김학생 job: 학생 level: 29 emotion: 분노 items: [] } 이렇게 되면 다른 버전의 데이터가 공존할 수 있지만, 어차피 읽어서 쓸 때는 버전이 맞춰지니 괜찮습니다.
  38. 38. on-demand migration 분산된 마이그레이션 수행 부담이 적은 점검 이 방식은 필요할 때만 실행되기 때문에 잘 분산되어 실행되고 점검 시간에 할 일이 줄어들어 점섬 시간 자체가 줄어듭니다.
  39. 39. on-demand migration 분산된 마이그레이션 수행 부담이 적은 점검 점검이 끝나고 버그 발견 하지만 점검이 끝나야 실제 마이그레이션이 산발적으로 진행되기 때문에 버그가 있을 경우 뒷수습이 힘듭니다. 그래서 저희는 마이그레이션 작업이 있을 경우 빼먹지 않도록 관련된 테스트 프로세스를 따로 운영중입니다.
  40. 40. 시작하기 전 스키마 데이터의 저장 두개 이상의 문서에 대한 저장과 변경 쿼리하기 정리
  41. 41. 저희는 플레이어들에게 게임 디자인적으로 허용하는 한 최대한 큰 섬을 유저들에게 주고 싶어합니다.
  42. 42. 때문에 하나의 섬을 하나의 물리적인 서버에 바인딩 시키고 싶어하지 않습니다. 서버 성능이 섬 크기의 하드캡이 되기 때문이죠
  43. 43. 그렇다고 하나의 섬을 심리스하지 않게 만드는 것도 하고 싶지 않았죠. 그럼 섬을 두개 만드는 것만 못하죠.
  44. 44. A BA B 그렇기 때문에 저희는 물리적인 서버에 지역을 묶지 않고 물리적인 서버에서는 적당히 배정된 플레이어 캐릭터만 담당하게 하였습니다.
  45. 45. A BA B 그렇기 때문에 바로 옆에 있는 것 같았던 다른 캐릭터가 사실은 다른 물리적인 노드에 있을 수 있는 겁니다. 하지만 이 상황에서 캐릭터의 데이터를 변경하려고 하면 문제가 생길 수 있습니다.
  46. 46. A BA B 둘이 싸우는 과정을 가정해 보죠. A는 B를 공격하려고 하고, B는 물약을 마시려고 합니다.
  47. 47. A BA B -20 +30 두가지 액션은 다른 캐릭터가 다른 서버에서 진행했지만 결과는 하나의 문서에 반영되어야 합니다.
  48. 48. -20 +30 { name: B job: 방어자 life: 50 } { name: B job: 방어자 life: 50 } 각기 서버에서 한 문서를 읽어 변경하려고 하겠죠
  49. 49. { name: B job: 방어자 life: 80 } { name: B job: 방어자 life: 30 } 이렇게 바뀐 데이터를 동시에 저장하려고 하면 문제가 생깁니다. 어느 쪽인가가 덮어씌워지겠죠.
  50. 50. CAS 이런 일을 방지하기 위해서 Couchbase는 CAS 시스템을 제공합니다. Check And Set 또는 Compare And Swap이라고 하죠.
  51. 51. { name: B job: 방어자 life: 50 } 1111 { name: B job: 방어자 life: 50 } 1111 B:1111 CAS key Couchbase에서는 읽을 때 언제나 문서당 발급된 유니크한 CAS키를 같이 읽어옵니다.
  52. 52. { name: B job: 방어자 life: 30 } 1111 { name: B job: 방어자 life: 80 } 1111 B:1111 문서를 새로 저장하려고할 때 변경된 문서의 CAS키와 데이터베이스의 CAS키가 같은것을 확인해야 저장을 받아주죠.
  53. 53. { name: B job: 방어자 life: 30 } 1111 { name: B job: 방어자 life: 80 } 2222 B:2222 그리고는 새로운 CAS키를 발급해서 저장을 요청한 서버에게 알려줍니다.
  54. 54. { name: B job: 방어자 life: 30 } 1111 { name: B job: 방어자 life: 80 } 2222 B:2222 하지만 다른 서버는 이 상황을 모르기 때문에 저장을 시도하다가 틀린 CAS키 때문에 저장이 거절되죠
  55. 55. { name: B job: 방어자 life: 80 } 2222 { name: B job: 방어자 life: 80 } 2222 B:2222 거절 당하면 데이터베이스에서 새 버전을 읽어와서 다시 변경한 뒤 저장해야 합니다.
  56. 56. 낙관적 동시성 제어 이런 방식을 낙관적 동시성 제어라고 부릅니다. 아마 성공할 것이라고 낙관하고 일단 저장을 시도하는 거죠. 물론 실패할 수도 있지만 그건 나중에 생각합니다.
  57. 57. 낙관적 동시성 제어 루프의 함정 • 경합이 잦은 곳에서는 급격하게 퍼포먼스가 나빠진다 • 루프 안에서는 사이드 이펙트가 없어야 한다 하지만 일단 저장하고 실패하면 다시 읽어와서 변경한 뒤 저장을 시도하는 방식은 단점이 있습니다. 경합이 잦은 곳에서의 문제도 문제지만, 데이터를 변경하는 곳이 참조 투명성을 갖춰야 한다는 점이죠.
  58. 58. 낙관적 동시성 제어 루프의 함정 • 경합이 잦은 곳에서는 급격하게 퍼포먼스가 나빠진다 • 루프 안에서는 사이드 이펙트가 없어야 한다 1 2 3 4 def eat_medicine(self, item): for doc in self.saving(): doc.life += item.life broad_cast(‘I ate medicine’) 아마도 개발자는 물약을 먹었을 때 주변에 나 먹었다고 알리고 싶었을 뿐 입니다. 하지만 부주의하게 낙관적 동시성 제어 루프 안에 넣는 바람에 저장을 재시도할 때마다 날리게 되었죠
  59. 59. 낙관적 동시성 제어 이런 특징 때문에 캐릭터 간 인터랙션 같은 복잡한 로직에서 낙관적 동시성 제어를 믿고 프로그래밍 하는 것은 매우 어려운 일입니다.
  60. 60. 서버 노드가 문서의 소유권을 독점하기 그래서 저희는 좀 다른 방식을 사용하고 있습니다. 어떤 문서의 소유권을 명확히 하는 방식이죠.
  61. 61. BA Ghost RPC 캐릭터가 특정 서버에게 완전히 소유되어 있기 때문에 그 캐릭터를 보고 싶은 다른 서버에서는 Ghost라는 캐릭터의 현재 상태가 업데이트 되는 더미가 존재하게 됩니다.
  62. 62. BA Ghost는 스스로 행동할 수 없기 때문에 아까처럼 공격을 받을 경우 직접 자신의 데이터를 변경하는 게 아니고 본체에게 RPC를 통해서 연락을 보냅니다.
  63. 63. BA -20 +30 그럼 본체에서 모든 처리를 줄 세워서 안전하게 하죠.
  64. 64. BA 이 정보는 DB에 저장됨과 동시에 Ghost에도 동기화가 되어서 A가 자기 공격의 결과를 볼 수 있습니다. 또한 B 문서를 저장하는 서버 노드는 한 곳이기 때문에 데이터를 저장할 때 경합을 고려하지 않아도 되죠.
  65. 65. B A 하지만 이 방법에도 하나의 단점이 있으니 동기화 비용 때문에라도 B가 A의 시야에서 멀어지면 B의 Ghost도 사라진다는 점입니다.
  66. 66. ? 본체와의 연락 통로인 Ghost 없이는 다른 캐릭터에게 영향력을 전혀 행사할 수 없죠. 처음에는 이래도 괜찮게 생각했지만, 짜잔 절대라는 건 없더군요.
  67. 67. B A A와 B가 공룡을 사냥하고 있는데
  68. 68. A B가 갑자기 사라졌습니다. 그냥 멀어진 걸수도 있고, 접속을 종료했을 수도 있죠.
  69. 69. A A 혼자서 사냥을 마무리 했는데, 획득한 경험치를 기여도가 있는 B에게 나눠주고 싶어도 Ghost가 없어서 나눠줄 수가 없습니다.
  70. 70. Promise 이럴 때 저희는 promise라고 부르는 방법을 씁니다.
  71. 71. { name: B job: 방랑자 exp: 50 } { promises: [ (add_exp, 30) (add_exp, 20) (add_exp, 40) ] } 하나의 캐릭터에게는 언제나 하나의 promise 큐를 만들어 두고 시야에 없는 캐릭터에게는 약속만 걸어두면
  72. 72. { name: B job: 방랑자 exp: 140 } { promises: [] } 해당 캐릭터가 접속했을 때 알아서 해당 큐의 작업들을 수행하는 방식이죠. 이미 접속 중이었다면 바로 실행할 것 입니다.
  73. 73. Promise 일방적 요청 정확한 실행 타이밍 예측 불가 Promise는 유용하지만 요청이 일방적이어서 인터랙션 용도로 적절하지 못하고 접속할 때 처리될 수 있기 때문에 정확한 실행 타이밍을 예측하기 힘들다는 문제가 있습니다.
  74. 74. 시작하기 전 스키마 데이터의 저장 두개 이상의 문서에 대한 저장과 변경 쿼리하기 정리
  75. 75. ACID transactionACID 초반에 설명했듯이 Couchbase는 여러 개 문서에 대한 저장을 ACID하게 처리하지 못합니다.
  76. 76. { name: 가방 items: [] } { name: 상자 items: [ 가죽 장화 ] } 하지만 두개의 문서를 동시에 저장할 일은 필요하기 마련입니다. 상자에서 가방으로 아이템을 옮기는 것이 대표적인 예이죠.
  77. 77. { name: 가방 items: [ 가죽 장화 ] } { name: 상자 items: [] } 이렇게 뿅 하고 옮겨지면 좋겠지만, Couchbase에서는 이 과정이 쉽지 않습니다.
  78. 78. 1 2 3 4 5 def transfer_item(source, destination, item_name): item = source.pop(item_name) destination.add(item) source.save() destination.save() 그냥 로직에서 옮기고 순서대로 저장을 하게 하면 저장 중간에 문제가 생겼을 경우
  79. 79. { name: 가방 items: [] } { name: 상자 items: [] } 아이템이 소실되는 문제가 생깁니다.
  80. 80. 1 2 3 4 5 def transfer_item(source, destination, item_name): item = source.pop(item_name) destination.add(item) destination.save() source.save() 목적지를 출발지보다 먼저 저장하게 했다면 더 끔찍한 일이 일어나겠죠.
  81. 81. { name: 가방 items: [ 가죽 장화 ] } { name: 상자 items: [ 가죽 장화 ] } 아이템 복사가 일어나죠.
  82. 82. B A S E asically vailable oft state ventual consistency 이런 경우에 BASE 트랜잭션을 써야 합니다. ACID의 반대 표현인 BASE에 맞추기 위해서 애를 쓴 것이 보이는 용어입니다.
  83. 83. { name: 가방 items: [] } { name: 상자 items: [ 가죽 장화 ] } BASE 트랜잭션의 용어를 일일히 먼저 이해하는 것 보다 예시를 보고 용어를 다시 보면 이해가 쉬울 것 같습니다.
  84. 84. 1. 이동 명세 문서 생성 { name: 가방 items: [] } { name: 상자 items: [ 가죽 장화 ] } { id: transfer_001 source: 상자 destination: 가방 state: pending items: [ 가죽 장화 ] } 아이템을 옮기려고 하면 일단 아이템 이동 자체에 대한 명세 문서를 만들어 DB에 저장합니다. 출발지와 목적지, 아이템 그리고 이동에 대한 현재 상태도 넣어둬야겠죠.
  85. 85. 2. 상자와 명세 문서를 연결하기 { name: 가방 items: [] } { name: 상자 items: [] transferring: transfer_001 } { id: transfer_001 source: 상자 destination: 가방 state: pending items: [ 가죽 장화 ] } 그 다음엔 먼저 출발지에서 아이템을 빼고 명세 문서와의 연결 고리를 만듭니다. 혹시 실패한다면 이 명세를 참고해서 원래 상태로 복원할 수도 있고, 다시 이동을 이어서 진행할 수도 있습니다.
  86. 86. 3. 가방과 명세 문서를 연결하기 { name: 가방 items: [] transferring: transfer_001 } { name: 상자 items: [] transferring: transfer_001 } { id: transfer_001 source: 상자 destination: 가방 state: pending items: [ 가죽 장화 ] } 도착지에도 이동을 반영합니다. 아이템 부터 넣으면 아직 프로세스가 끝나지 않았는데도 아이템을 사용해 버리는 일이 있을 수 있으니 명세 문서만 링크로 걸어 둡니다.
  87. 87. 4. 명세 상태 변경 { name: 가방 items: [] transferring: transfer_001 } { name: 상자 items: [] transferring: transfer_001 } { id: transfer_001 source: 상자 destination: 가방 state: committed items: [ 가죽 장화 ] } 양쪽 인벤토리에 명세 문서가 다 링크 된 것을 확인하면 상태를 committed로 바꿉니다. 사실상 이동이 완료된 것이기 때문이죠.
  88. 88. 5. 아이템 이동 완료 { name: 가방 items: [ 가죽 장화 ] } { name: 상자 items: [] } { id: transfer_001 source: 상자 destination: 가방 state: committed items: [ 가죽 장화 ] } 명세 문서에 committed가 기록되었으니 각 인벤토리는 이동 명세를 참고해서 결과를 스스로 반영할 수 있습니다.
  89. 89. 아이템 이동의 핵심 • 하나의 단계에선 하나의 문서만 변경 • 아이템 이동에 대한 명세를 문서화 하여 어느 단계에서 멈춰도 다시 아이템 이동을 재개할 수 있음 각 단계에선 ACID 트랜잭션을 보장하는 하나의 문서에 대해서만 동작하기 때문에 아토믹함이 보장되고 명세 문서를 통해서 관리하기 때문에 언제 진행이 멈춰도 다시 진행할 수 있다는 점이 핵심입니다.
  90. 90. B A S E asically vailable oft state ventual consistency 이제 BASE 트랜잭션의 용어를 다시 한번 볼까요?
  91. 91. BA S E 아이템 이동이 진행되는 중에도 상자와 가방은 정상 동작함 아이템 이동에 대한 상태가 확정되지 않은 순간이 존재함 순간적으로 아이템이 양쪽에서 사라진 것처럼 보일 수 있지만, 언젠가는 이동이 완료됨 BASE 트랜잭션은 ACID 트랜잭션을 지원하지 않을 때에 애플리케이션 레벨에서 ACID와 비슷한 동작을 위해서 만족해야 하는 특성들을 모아둔 것이라 하겠습니다.
  92. 92. 시작하기 전 스키마 데이터의 저장 두개 이상의 문서에 대한 저장과 변경 쿼리하기 정리
  93. 93. 저희 게임에는 사유지라고 하는 기능이 있습니다. 특정 유저가 땅을 독점하는 기능이죠.
  94. 94. Grid 땅의 기록 이런 사유지에 대한 정보는 위치 기반으로 동작하기 때문에 땅에 기록되어야 하는데요. 저희는 이 땅을 내부적으로 Grid라고 부르고 있습니다.
  95. 95. Chunk 문서 단위 땅을 통째로 하나의 문서로 저장하기에는 데이터가 너무 많기 때문에 이렇게 Chunk라는 단위로 쪼개서 저장을 합니다.
  96. 96. Cell 사유지 단위 하나의 Chunk 안에는 사유지의 단위가 되는 Cell이 여러 개 들어있죠.
  97. 97. { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } Grid 문서를 살펴보면 대충 이와 같은 모습을 하고 있습니다.
  98. 98. { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } 이런 것이 여러 개 모여 하나의 땅 즉 Grid를 이루죠. 더 크게 보면 섬 여러 개로 이루어진 듀랑고는 결국 이 Chunk 단위 문서들의 모음이라고 하겠습니다.
  99. 99. 사유지는 위치 기반 시스템이라 땅에 기록된 데이터지만 가끔은 사유지 주인의 이름으로 사유지가 어디어디 있는지 찾아야 할 필요가 있습니다.
  100. 100. 게다가 주인도 내 사유지의 권한을 멀리서도 바꾸고 싶기 때문에 내 사유지가 어디에 있는지 알아야 변경된 권한을 멀리서도 바로 적용시킬 수 있겠죠.
  101. 101. Views MapReduce 여러 개의 문서에 흩어진 정보를 찾기 위해서 Couchbase는 Views라는 기능을 지원합니다. MapReduce라는 기술이 기반이 된 부가기능이죠.
  102. 102. MapReduce map(f, [i1, i2, i3, ...]) = [f(i1), f(i2), f(i3) ...] reduce(g, [i1, i2, i3, ...]) = g(i1, g(i2, g(i3, ...))) map과 reduce는 여러분이 아시는 그것이 맞습니다. 이걸 어떻게 활용한다는 것일까요?
  103. 103. { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김부자 [3, 3] 김부자 [1, 2] 김서민 [1, 1] … … 우리는 이런 여러 개의 문서로부터 오른쪽 같은 테이블을 얻기를 원하죠
  104. 104. { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } 우선 하나의 문서만 대상으로 놓고 해볼까요?
  105. 105. 1 2 3 4 5 6 function(doc) for (var cell in doc[‘estates’]) { var owner = doc[‘estates’][cell] emit(owner, cell) } } 이런 함수면 충분할 것 같네요! 편의상 요 함수를 F라고 표기하겠습니다.
  106. 106. 1 2 3 4 5 { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } ( ) 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김서민 [1, 1] F 하나의 문서를 F에 넣으면 오늘쪽 처럼 데이터가 나오겠네요.
  107. 107. F { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 [1, 1]: 김서민 ] } map( , ) 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김평범 [1, 1] 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김평범 [1, 1] 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김평범 [1, 1] 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김서민 [1, 1] map을 여기서 사용하면 되겠어요. 여러 개의 데이터를 F와 함께 map에 넣으면 각기 데이터를 병렬 처리해서 작은 테이블들을 주겠네요.
  108. 108. 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김평범 [1, 1] 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김평범 [1, 1] 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김평범 [1, 1] 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김서민 [1, 1] reduce(add, ) 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김부자 [3, 3] 김부자 [1, 2] 김서민 [1, 1] … … 이렇게 받은 여러 개의 테이블은 reduce를 이용해서 모으기만 하면 되겠네요. (다양한 함수를 이용하면 이 단계에서 더 많은 가공을 할 수 있지만 지금의 예시는 단순히 합치는 데만 씁니다)
  109. 109. 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김부자 [3, 3] 김부자 [1, 2] 김평범 [1, 1] … … 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김부자 [3, 3] 김부자 [1, 2] 김평범 [1, 1] … … 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김부자 [3, 3] 김부자 [1, 2] 김평범 [1, 1] … … 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김부자 [3, 3] 김부자 [1, 2] 김평범 [1, 1] … … 이름 위치 김부자 [3, 2] 김부자 [2, 1] 김부자 [2, 2] 김부자 [3, 3] 김부자 [1, 2] 김서민 [1, 1] … … Couchbase는 분산 구조를 택하고 있기 때문에 각 데이터서비스에서 MapReduce를 통해 수집한 데이터를 한번 더 모으면 우리가 원하는 결과가 나옵니다.
  110. 110. map/reduce map/reduce map/reduce map/reduce reduce view 간단히 보면 이런 모습이 되겠죠. map → reduce → reduce → view
  111. 111. Views 분산 형 구조를 가진 Couchbase에서 MapReduce를 활용한 View는 꽤 괜찮은 전략입니다.
  112. 112. Views 하지만 꽤 괜찮을 뿐이었죠. 저희 사유지 시스템은 부하가 너무 심해서 검색을 위한 Views 때문에 데이터 서비스에 지장이 있을 정도였습니다.
  113. 113. N1QL JSON을 위한 유사 SQL 그래서 N1QL을 활용해 보기로 했습니다. Couchbase에서 제공하는 유사 SQL입니다.
  114. 114. 데이터서비스 인덱스서비스쿼리서비스 Couchbase 4.0 부터 분리 Views를 쓸 때도 있었지만 성능이 쓸 수 없는 수준이었지만 4.0 버전부터 쿼리와 인덱스 서비스가 분리되어서 성능면에서 많이 좋아졌습니다.
  115. 115. 데이터서비스 인덱스서비스 쿼리서비스 변경점 검색 결과 간략하게 설명하면 이런 방식으로 동작하는 구조입니다.
  116. 116. 데이터서비스 제일 중요한 점은 Views는 데이터 서비스 혼자서 고생하는 시스템이라면
  117. 117. 데이터서비스 인덱스서비스쿼리서비스 N1QL은 그 부하가 분산된다는 점 입니다. 그리고 더 중요한 점은 검색 시스템에 부하가 걸려도 데이터 서비스는 영향을 받지 않는 다는 점 이죠.
  118. 118. 오픈
  119. 119. 인덱스서비스쿼리서비스데이터서비스 하지만 저희의 기대와 달리 Views 보단 덜하지만 여전히 N1QL도 데이터 서비스를 괴롭힌다는 사실이 오픈 후에 발견되었습니다.
  120. 120. 데이터서비스 인덱스서비스 쿼리서비스 변경점 검색 결과 데이터 서비스가 변경점을 인덱스 서비스로 보낼 때 부하가 꽤 많다는 사실을 알게 되었습니다. 이 과정도 좀 무겁지만 보내는 코드를 살펴보니 코드 자체도 꽤나 비 효율적으로 되어있었습니다.
  121. 121. 데이터서비스 • 저장이 빈번할 수록 • 저장한 문서 크기가 클 수록 • N1QL 인덱스가 많을 수록 다양한 실험과 연구를 통해서 이런 특징들을 알아내고
  122. 122. 데이터서비스 • 라이브에 필요 없는 인덱스 분리 • 문서의 크기 줄이기 내부적인 튜닝을 거쳐서 어느 정도 감당이 가능한 정도로 부하를 줄일 수있었습니다.
  123. 123. 관성의 함정 • Couchbase는 Key-Value Store • Views, N1QL은 부가 기능 • RDB를 사용하던 느낌 그대로 로직을 디자인함 하지만 그와 별개로 저희는 쿼리 서비스에 의존하지 않아야 한다는 결론을 내리고 우리가 너무 RDB 스러운 DB 사용법에 익숙해져 있는 것은 아닌가에 대한 반성을 했습니다.
  124. 124. { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 ] } 그래서 쿼리를 사용하지 않는 방식으로 재 구현하기로 결정하였습니다. Grid와 별개로 사유지 하나 별로 땅문서를 만드는 방식이었습니다.
  125. 125. { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 ] } 이전에도 땅문서에 주인이 링크되어 있는 것은 같았지만 새 시스템의 가장 중요한 차이점은 데이터의 근원이 땅이 아니라 땅문서라는 점 이었습니다.
  126. 126. { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 ] } 예전엔 Grid에 이 땅의 주인이 누구인지 적혀있으면 그것을 믿었지만 이제는 땅문서를 확인해서 그 땅이 기록되어 있지 않다면
  127. 127. { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 ] } 그 땅은 Grid를 믿지 않고 주인이 없는 땅으로 간주하는 것입니다.
  128. 128. { estates: [ [3, 2]: 김부자 [2, 1]: 김부자 [2, 2]: 김부자 ] } 이 방식은 Grid는 단순히 땅문서를 확인하기 위한 연결에 불과하기 때문에 모든 땅에서 이 땅의 주인을 확인하려면 지속적으로 땅문서를 읽어야 한다는 점이 부담이 됩니다.
  129. 129. 하지만 Couchbase는 자신의 장점을 십분 발휘하여 Key-Value 읽기는 캐시급 성능으로 충분히 극복하였습니다. 저흰 DB의 최고 장점을 두고 불완전한 부가기능으로 열심히 씨름하고 있던 셈이었죠.
  130. 130. 시작하기 전 스키마 데이터의 저장 두개 이상의 문서에 대한 저장과 변경 쿼리하기 정리
  131. 131. NoSQL • RDB가 달성할 수 없다고 생각되는 목표를 위해 잘 다져진 환경을 버리고 나온 것들 • 아직도 발전하고 있고 계속해서 바뀌고 있음 • 그래서 분명히 단단하지 못한 부분들이 존재함 • 단단하지 못한 부분을 애플리케이션에서 커버하고 있는 느낌을 받음 • 시간이 흐르면 RDB처럼 될까?
  132. 132. 게임 플레이 프로그래머로써 • RDB를 사용하던 시절에는 모든걸 DBA와 상담함 • 하지만 Couchbase를 사용하니 게임 플레이 로직과 데이터베이스가 적응하기 힘들 정도로 급격하게 가까워짐 • 데이터베이스를 깊게 이해 해야지만 게임 플레이 로직을 구현할 수 있었다.
  133. 133. 앞으로 • 여러가지의 데이터베이스를 필요에 의해서 조합해서 사용하는 시대 • 앞으로도 NoSQL을 계속 사용해야 할 것 같은데 DBA와 게임 플레이 프로그래머의 중간 어디쯤 있는 역할이 새롭게 필요해진 것은 아닌가 하는 생각

×