14. ORM Part 4: DB 처리

14.1. 트랜잭션

아이펀 엔진의 ORM 은 이벤트 쓰레드로 처리되며, 한 이벤트 핸들러 안에서의 DB 작업을 묶어서 하나의 트랜잭션으로 처리합니다.

void OnMyEvent(const Ptr<const MyEvent> &event) {
// transaction 시작

  MyFunction();

// transaction 끝
}

void MyFunction() {
// transaction 안
  ...
}

그리고 아이펀 엔진은 DB 처리에 대하여 non-blocking 으로 작동합니다. 즉, DB I/O 가 발생하더라도 이벤트 쓰레드를 block 시키지 않습니다. 만일 DB 처리를 위해서 쓰레드가 멈출 것 같은 상황에서, 아이펀 엔진은 처리하고 있던 트랜잭션을 롤백 하고 DB 처리가 바로 시작될 수 있는 상황이 되었을 때 트랜잭션을 재시작 합니다.

롤백Create(...) / Fetch(...) / Refresh() 처리 시 오브젝트를 바로 접근할 수 없을 때 발생합니다. 이런 예로는 오브젝트를 DB 에서 로딩하거나, 다른 이벤트 쓰레드에서 같은 오브젝트를 이미 사용하고 있는 경우가 해당됩니다.

Important

DB 서버 Sharding 에 언급된 방식으로 DB 샤딩을 할 때, 디비 서버 크래시나 객체를 소유한 게임 서버 크래시 등으로 일부 오브젝트를 쓰기 실패하는 경우가 발생할 수 있습니다. 이런 경우에 대비해서 게임서버에서는 오브젝트가 null인지 검사하는 과정이 꼭 필요합니다.

14.1.1. 트랜잭션 롤백

아래 함수들은 롤백을 발생시켜 트랜잭션을 강제 중단 시켰다가 이후 다시 실행할 준비가 되면 트랜잭션을 다시 실행합니다. 트랜잭션이 중단되면 아이펀 엔진은 트랜잭션 안에서 변경한 오브젝트를 이전 상태로 되돌립니다.

  • 인터페이스 클래스의 Fetch() 메소드
  • 인터페이스 클래스의 Create() 메소드 중 Key 속성을 갖는 메소드
  • 인터페이스 클래스의 Refresh() 메소드

Note

롤백을 일으키는 아이펀 엔진 함수들은 함수의 signature 에 ROLLBACK 이라는 키워드가 붙습니다.

Important

롤백을 발생시키는 함수 이전의 코드는 언제나 재실행될 수 있다는 것을 기억하세요. 가장 좋은 방법은 롤백을 일으킬 수 있는 함수들을 이벤트 핸들러 가장 앞쪽에 위치시키는 것 입니다.

14.1.1.1. 예제: 롤백 가능한 함수 뒤로 코드 옮기기

아래 코드에서 FetchById(...)Create(...) 은 롤백을 발생시키고 트랜잭션을 재시작시킬 수 있습니다. 따라서 g_character_create_count 실제 생성된 캐릭터 숫자보다 큰 값이 되므로 버그입니다.

이를 막기 위해서는 전역 변수에 쓰는 작업을 하는 코드를 롤백 가능한 함수들 뒤로 위치하는 것이 좋습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int g_character_create_count;

void OnCreateCharacter(const Ptr<Session> &session, const Json &message) {
  // 아래 코드는 재실행될 수 있는데, 전역 변수 값을 변경하므로 버그입니다.
  ++g_character_create_count;

  // 아래 코드는 재실행될 수 있습니다.
  // 하지만 로컬변수를 업데이트하거나, 읽기만 하기 때문에 기능에 문제가 되지 않습니다.
  std::string id = message["id"].GetString();
  std::string name = message["name"].GetString();

  // 아래 Fetch/Create 호출이 롤백을 발생시킬 수 있습니다.
  Ptr<User> user = User::FetchById(id);
  Ptr<Character> new_character;
  if (user) {
    Ptr<Character> old_character = user->GetMyCharacter();
    if (old_character) {
      old_character->SetHp(0);
      old_character->SetLevel(0);
    }
    new_character = Character::Create(name);
    user->SetMyCharacter(new_character);
  }

  Json response;
  if (new_character)
    response["result"] = true;
  else
    response["result"] = false;

  session->SendMessage("create_character", response);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static int the_character_create_count;

void OnCreateCharacter(Session session, JObject message)
{
  // 아래 코드는 재실행될 수 있는데, 전역 변수 값을 변경하므로 버그입니다.
  ++the_character_create_count;

  // 아래 코드는 재실행될 수 있습니다.
  // 하지만 로컬변수를 업데이트하거나, 읽기만 하기 때문에 기능에 문제가 되지 않습니다.
  string id = (string) message ["id"];
  string name = (string) message ["name"];

  // 아래 Fetch/Create 호출이 롤백을 발생시킬 수 있습니다.
  User user = User.FetchById (id);
  Character new_character = null;

  if (user)
  {
    Character old_character = user.GetMyCharacter ();
    if (old_character)
    {
      old_character.SetHp (0);
      old_character.SetLevel (0);
    }
    new_character = Character.Create (name);
    user.SetMyCharacter (new_character);
  }

  JObject response = new JObject();
  if (new_character)
    response ["result"] = true;
  else
    response ["result"] = false;

  session.SendMessage ("create_character", response);
}

아래는 위의 잘못된 예를 수정한 것입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int g_character_create_count;

void OnCreateCharacter(const Ptr<Session> , const Json ) {
  // 중복으로 실행될 수 있지만 문제 없습니다.
  std::string id = message["id"].GetString();
  std::string name = message["name"].GetString();

  Ptr<User> user = User::FetchById(id);
  Ptr<Character> new_character;
  if (user) {
    Ptr<Character> old_character = user->GetMyCharacter();
    if (old_character) {
      old_character->SetHp(0);
      old_character->SetLevel(0);
    }
    new_character = Character::Create(name);
    user->SetMyCharacter(new_character);
  }

  ++g_character_create_count;

  Json response;
  if (new_character)
    response["result"] = true;
  else
    response["result"] = false;

  session->SendMessage("create_character", response);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int the_character_create_count;

void OnCreateCharacter(Session session, JObject message)
{
  // 중복으로 실행될 수 있지만 문제 없습니다.
  string id = (string) message ["id"];
  string name = (string) message ["name"];

  User user = User.FetchById (id);
  Character new_character = null;
  if (user)
  {
    Character old_character = user.GetMyCharacter ();
    if (old_character)
    {
      old_character.SetHp (0);
      old_character.SetLevel (0);
    }
    new_character = Character.Create (name);
    user.SetMyCharacter (new_character);
  }

  ++the_character_create_count;

  JObject response = new JObject ();
  if (new_character)
    response["result"] = true;
  else
    response["result"] = false;

  session.SendMessage ("create_character", response);
}

14.1.1.2. 예제: 롤백을 피해 이벤트 쪼개기

ROLLBACK 을 피해 코드를 뒤로 옮기는 것이 언제나 가능하지 않을 수 있습니다. 이런 경우에는 ROLLBACK 이 이벤트 단위로 발생한다는 점에 착안해, 이벤트를 둘로 나누는 방법을 쓸 수 있습니다.

아래는 앞의 예를, 코드를 옮기는 대신 이벤트를 둘로 쪼갠 경우입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void OnCreateCharacter(const Ptr<Session> &session, const Json &message) {
  ++g_character_create_count;

  std::string id = message["id"].GetString();
  std::string name = message["name"].GetString();

  function next_step =
      bind(&OnCreateCharacter2, session, id, name);

  Event::Invoke(next_step, session->id());
}


void OnCreateCharacter2(
    const Ptr<Session> &session, const std::string &id, const string &name) {
  Ptr<User> user = User::FetchById(id);
  Ptr<Character> new_character;
  if (user) {
    Ptr<Character> old_character = user->GetMyCharacter();
    if (old_character) {
      old_character->SetHp(0);
      old_character->SetLevel(0);
    }
    new_character = Character::Create(name);
    user->SetMyCharacter(new_character);
  }

  Json response;
  if (new_character)
    response["result"] = true;
  else
    response["result"] = false;

  session->SendMessage("create_character", response);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void OnCreateCharacter(Session session, JObject message)
{
  // 아래 코드는 재실행될 수 있는데, 전역 변수 값을 변경하므로 버그입니다.
  ++the_character_create_count;

  // 아래 코드는 재실행될 수 있습니다.
  // 하지만 로컬변수를 업데이트하거나, 읽기만 하기 때문에 기능에 문제가 되지 않습니다.
  string id = (string) message ["id"];
  string name = (string) message ["name"];

  Event.Invoke(() => {
    OnCreateCharacter2 (session, id, name);
  }, session.Id);
}


void OnCreateCharacter2(Session session, string id, string name)
{
  User user = User.FetchById (id);
  Character new_character = null;

  if (user)
  {
    Character old_character = user.GetMyCharacter ();
    if (old_character)
    {
      old_character.SetHp (0);
      old_character.SetLevel (0);
    }
    new_character = Character.Create (name);
    user.SetMyCharacter (new_character);
  }

  JObject response = new JObject();
  if (new_character)
    response ["result"] = true;
  else
    response ["result"] = false;

  session.SendMessage ("create_character", response);
}

14.1.2. 원하지 않는 롤백 감지

예상치 못한 롤백이 발생하면 오작동에 따른 알 수 없는 결과가 발생할 수 있고, 원인 파악에 상당한 시간을 허비할 수 있습니다. 아이펀 엔진은 디버깅 목적으로 아래 롤백 관련 유틸리티 함수를 제공합니다.

void AssertNoRollback()

이 함수가 호출된 후 롤백이 발생되면 로그와 함께 강제종료합니다. 이 때 다음과 같은 로그가 발생합니다.

transaction rollback raised after 'AssertNoRollback()': event_name=on_create, model_name=User

Tip

DebugSetEventName() 을 이용하면 위처럼 이벤트의 이름도 표시됩니다. 이름이 지정되지 않은 이벤트의 경우 event_name=(unnamed) 로 표시됩니다. 자세한 내용은 이벤트에 디버깅용 이름 부여 을 참고하세요.

Tip

AssertNoRollback() 은 MANIFEST.json 에서 전체 활성화/비활성화를 설정할 수 있습니다. 자세한 내용은 ORM 기능 설정 파라미터enable_assert_no_rollback 를 참고하세요.

Tip

아이펀 엔진의 함수들 중에 함수 이름 끝에 ASSERT_NO_ROLLBACK 이라고 태그 되어있는 함수들은 내부 구현에 AssertNoRollback() 을 포함하고 있습니다. 해당 함수들은 롤백이 발생할 수 있는 곳에서 불리면 안됩니다. 아래는 그런 함수들의 예시입니다.

  • AccountManager::CheckAndSetLoggedIn()
  • AccountManager::CheckAndSetLoggedInAsync()
  • AccountManager::SetLoggedOut()
  • AccountManager::SetLoggedOutAsync()
  • AccountManager::SetLoggedOutGlobal()
  • AccountManager::SetLoggedOutGlobalAsync()
  • AccountManager::Locate()
  • AccountManager::LocateAsync()
  • AccountManager::SendMessage()
  • AccountManager::BroadcastLocally()
  • AccountManager::BroadcastGlobally()
  • MatchmakingClient::StartMatchmaking()
  • MatchmakingClient::CancelMatchmaking()
  • Session::SendMessage()
  • Session::BroadcastLocally()
  • Session::BroadcastGlobally()
  • Rpc::Call()
  • Rpc::ReadyBack 타입의 핸들러
  • ApiService::ResponseWriter 타입의 핸들러
  • Timer::ExpireAt()
  • Timer::ExpireAfter()
  • Timer::ExpireRepeatedly()
  • Timer::Cancel()

14.1.2.1. 예제: AssertNoRollback() 을 이용한 능동적 롤백 감지

아래 예는 AssertNoRollback() 을 활용하여 ++g_character_create_count 코드가 Character::Create(name) 에 의하여 재실행되는 것을 감지하는 코드입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int g_character_create_count;

void OnMyHandler(const Ptr<Session> &session, const Json &message) {
  string id = message["id"].GetString();
  string name = message["name"].GetString();

  Ptr<User> user = User::FetchById(id);

  AssertNoRollback();
  ++g_character_create_count;

  if (not user->GetMyCharacter()) {
    Ptr<Character> character = Character::Create(name);
  }
  ...
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int the_character_create_count;

void OnMyHandler(Session session, JObject message)
{
  string id = (string) message["id"];
  string name = (string) message["name"];

  User user = User.FetchById(id);

  AssertNoRollback();
  ++the_character_create_count;

  if (user.GetMyCharacter() == null) {
    Character character = Character.Create (name);
  }
  ...
}

14.1.2.2. 예제: AssertNoRollback() 을 포함하는 아이펀 엔진 함수들

아래 예는 AssertNoRollback() 을 포함하는 Session::SendMessage()Item::Create(item_id) 에 의한 롤백을 감지하여 메세지가 중복 전송되는 것을 방지하는 예제입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void OnLogin(const Ptr<Session> &session, const Json &message) {
  string id = message["id"].GetString();
  Ptr<User> user = User::FetchById(id);
  Ptr<Character> character = user->GetMyCharacter();

  Json response;
  response["result"] = true;
  response["character_name"] = character->GetName();
  session->SendMessage("login_reply", response);

  // 출석 보상을 지급합니다.
  Uuid item_id = RandomGenerator::GenerateUuid();
  Ptr<Item> gift = Item::Create(item_id);

  ArrayRef<Ptr<Inventory> > inventory = character->GetInventory();
  inventory.PushBack(gift);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void OnLogin(Session session, JObject message)
{
  string id = (string) message ["id"];
  User user = User.FetchById(id);
  Character character = user.GetMyCharacter();

  JObject response = new JObject();
  response ["result"] = true;
  response ["character_name"] = character.GetName ();
  session.SendMessage ("login_reply", response);

  // 출석 보상을 지급합니다.
  System.Guid item_id = RandomGenerator.GenerateUuid();
  Item gift = Item::Create(item_id);

  ArrayRef<Inventory> inventory = character.GetInventory();
  inventory.PushBack(gift);
}

14.2. DB 서버 관리

14.2.1. DB 캐싱

Fetch 시 오브젝트 cache 에 설명된대로 아이펀 엔진 ORM 은 오브젝트를 Create(...) 하거나 Fetch(...) 로 DB 에서 오브젝트를 읽어 오는 경우 이를 자동으로 캐싱합니다.

캐시된 오브젝트는 다음의 경우가 발생하면 캐시에서 제거됩니다.

  • ORM 기능 설정 파라미터 에서 cache_expiration_in_ms 로 지정된 시간동안 다시 Fetch(...) 되지 않는 경우.
  • 오브젝트가 Delete() 메소드로 삭제된 경우.

14.2.2. DB 서버 Sharding

DB 샤딩을 위해서는 MySQL Cluster 같은 솔루션을 쓰실 수도 있고, 아니면 아이펀 엔진 ORM 의 샤딩 기능을 쓰실 수도 있습니다.

MANIFEST.json 에 key_database 부분을 설정하고, object_databases 부분에서는 shard 에 따라 range_end 부분을 조정하면 아이펀 엔진 ORM 이 오브젝트 ID 기준으로 자동으로 sharding 을 처리합니다. 아래는 2개의 shard 서버를 두는 경우의 예시입니다.

"Object": {
  "cache_expiration_in_ms" : 3000,
  "enable_database" : true,

  // 아래 key_database와 object_databases 는 enable_database 가 true 인
  // 경우에만 동작합니다.

  // object 의 key 가 저장되는 database 를 설정합니다.
  "key_database" : {
    // key database 가 존재하는 mysql server의 주소를 입력합니다. (기본값: tcp://127.0.0.1:3306)
    "address": "tcp://127.0.0.1:3306",
    "id": "funapi",                     // key database 가 존재하는 mysql server의 id 를 입력합니다.
    "pw": "funapi",                     // key database 가 존재하는 mysql server의 password 를 입력합니다.
    "database": "funapi_key"            // key database 가 존재하는 mysql server의 database 를 입력합니다.
  },
  // object 가 저장되는 database 들을 설정합니다.
  // range_end 값에 따라 여러 mysql server 에 나누어 object 를 저장합니다.
  "object_databases": [
    {
      // object 를 sharding 하기 위한 object id 의 범위값입니다.
      // object id 값이 range_end 이하면 이 database server 에 object 가 저장됩니다.
      "range_end": "80000000000000000000000000000000",
      "address": "tcp://127.0.0.1:3306",  // object database 가 존재하는 mysql server 의 주소를 입력합니다.
      "id": "funapi",                     // object database 가 존재하는 mysql server 의 id 를 입력합니다.
      "pw": "funapi",                     // object database 가 존재하는 mysql server 의 pw 를 입력합니다.
      "database": "funapi1"               // object 가 저장될 database 를 입력합니다.
    },
    {
      "range_end": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
      "address": "tcp://127.0.0.1:3306",
      "id": "funapi",
      "pw": "funapi",
      "database": "funapi2"
    }
  ],
  "db_read_threads_size" : 8,
  "db_write_threads_size" : 16,
  "enable_assert_no_rollback" : true
}

14.2.2.1. Sharding 시 DB 에 생성되는 table/procedure

Sharding 시 object_databaseskey_database 에 생성되는 DB table 과 procedure 는 다음과 같습니다.

14.2.2.1.1. object_databases 에 생성되는 table/procedure
  • Table
    • tb_Object_{{ObjectName}}
    • tb_Object_{{ObjectName}}_ArrayAttr_{{AttributeName}}
    • tb_Object_{{ObjectName}}_MapAttr_{{AttributeName}}
  • Procedure
    • sp_Object_Get_{{ObjectName}}
    • sp_Object_Insert_{{ObjectName}}
    • sp_Object_Update_{{ObjectName}}
    • sp_Object_Delete_{{ObjectName}}
    • sp_Object_Array_{{ObjectName}}_{{AttributeName}}
    • sp_Object_Map_{{ObjectName}}_{{AttributeName}}
14.2.2.1.2. key_database 에 생성되는 table/procedure
  • Table
    • tb_Key_{{ObjectName}}_{{KeyAttributeName}}
  • Procedure
    • sp_Object_Get_Object_Id_{{ObjectName}}By{{KeyAttributeName}}
    • sp_Object_Key_Insert_{{ObjectName}}_{{KeyAttributeName}}
    • sp_Object_Delete_Key_{{ObjectName}}

14.2.2.2. Sharding 시 데이터 마이그레이션

DB 를 늘리거나 줄이는 등의 DB 추가, 삭제 작업이 발생하게 되면 DB 샤딩을 다시 해야 하며, 이때 새로운 샤딩 규칙에 따라 데이터를 이전 해줘야 합니다.

아이펀 엔진은 이 과정을 단순화 시켜주는 object_db_migrate.py 라는 스크립트를 제공합니다. 다음과 같이 설치합니다.

Ubuntu

$ sudo apt-get install python-funapi1-dev

CentOS

$ sudo yum install python-funapi1-devel

다음과 같이 object_db_migrator.py 를 실행합니다.

$ cd /usr/lib/python2.7/dist-packages/funapi/object/
$ object_db_migrator.py --old_manifest='/home/test/OLD_MANIFEST.Json' --new_manifest='/home/test/NEW_MANIFEST.json'

실행하면 다음과 같은 로그가 출력되면서 /tmp 경로에 임시 디렉터리가 생성되고 해당 디렉터리에 데이터를 이전시키기 위한 SQL script 파일들이 생성됩니다.

$ cd /usr/lib/python2.7/dist-packages/funapi/object/
$ object_db_migrator.py --old_manifest='/home/test/OLD_MANIFEST.Json' --new_manifest='/home/test/NEW_MANIFEST.json'

Checks model fingerprint
Makes migration data
Creates a migration file: /tmp/tmp9eqI2T/insert_into_funapi2_from_funapi1_40000000000000000000000000000000_80000000000000000000000000000000.sql
Creates a migration file: /tmp/tmp9eqI2T/insert_into_funapi3_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql
Creates a migration file: /tmp/tmp9eqI2T/delete_from_funapi1_00000000000000000000000000000000_80000000000000000000000000000000.sql
Creates a migration file: /tmp/tmp9eqI2T/delete_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql
Creates migration files to /tmp/tmp9eqI2T
Done

SQL script 파일의 이름은 다음과 같은 방식으로 생성됩니다.

  • insert_into_{{shard1}}_from_{{shard2}}_{{range_start}}_{range_end}}.sql: {{shard2}} 에서 {{range_start}} 부터 {{range_end}} 까지 해당되는 오브젝트를 {{shard1}} 으로 복사한다는 의미입니다.

    예를 들어 insert_into_funapi2_from_funapi1_4000_8000.sqlfunapi1 이라는 shard 에서 오브젝트 ID 4000 부터 8000 까지의 오브젝트를 shard2 로 복사하는 파일입니다.

  • delete_from_{{shard}}_{{range_start}}_{{range_end}}.sql: {{shard}} 서버에서 {{range_start}} 부터 {{range_end}} 까지 해당되는 오브젝트를 지운다는 의미입니다.

    예를 들어 delete_from_funapi1_0000_8000.sqlfunapi1 이라는 shard 에서 오브젝트 ID 0000 부터 8000 까지의 오브젝트 중 migration 이 된 레코드들을 지운다는 의미입니다.

생성된 delete script 를 먼저 실행하고 이어 insert script 를 실행합니다. 다음 두 가지 방법 모두 가능합니다.

Note

디비 서버들이 모두 다른 서버 머신인 경우 적용 순서는 상관이 없습니다. 그러나 만약 동일한 서버머신에서 서로 다른 DB 를 만들어서 sharding 을 한 경우, delete 파일부터 적용한 후에 insert 파일을 적용해야 합니다. 그렇지 않으면 insert 후에 delete 를 실행하게 되어 앞에서 insert 된 것까지 같이 지워질 수 있습니다.

방법1: DB 서버에 접속해서 source 명령어를 이용해서 실행

mysql> source /tmp/tmp9eqI2T/insert_into_funapi2_from_funapi1_4000_8000.sql

방법2: Shell 에서 SQL script 를 바로 실행

$ mysql -h localhost -u root -p < /tmp/tmp9eqI2T/insert_into_funapi2_from_funapi1_4000_8000.sql

Warning

insert 파일의 경우 중복 실행하면 다음과 같은 Error 가 발생되니 주의하시기 바랍니다.

ERROR 1062 (23000): Duplicate entry '\x81b\xA7\x98z4E9\x8A\xE8\x9Fp\xF9\xEB\xEF\x99' for key 'PRIMARY'

Important

object_db_migrator.py 는 데이터만 이동할 뿐, 테이블이나 프로시저를 생성하지 않습니다. 테이블과 프로시져 생성을 위해서는 필요한 DB 권한 에서 설명된 export_db_schema 옵션을 사용하여 schema script 를 추출하고 insert/delete script 실행 전에 먼저 적용해줘야됩니다.

예제: 2개의 shard 에서 3개로 변경하기

예를 들어 샤딩하는 DB 개수가 2 개에서 3 개로 증가한다고 가정하면 각 MANIFEST.json 중 key_databaseobject_databases 는 다음과 같을 겁니다.

OLD_MANIFEST.json (shard 2개일 때 파일)

"Object": {
  "key_database" : {
    "address": "tcp://127.0.0.1:3306",
    "id": "funapi",
    "pw": "funapi",
    "database": "funapi_key"
  },
  "object_databases": [
    {
      "range_end": "80000000000000000000000000000000",
      "address": "tcp://127.0.0.1:3306",
      "id": "funapi",
      "pw": "funapi",
      "database": "funapi1"
    },
    {
      "range_end": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
      "address": "tcp://127.0.0.1:3306",
      "id": "funapi",
      "pw": "funapi",
      "database": "funapi2"
    }
  ],
}

NEW_MANIFEST.json (shard 3개일 때 파일)

"Object": {
  "key_database" : {
    "address": "tcp://127.0.0.1:3306",
    "id": "funapi",
    "pw": "funapi",
    "database": "funapi_key"
  },
  "object_databases": [
    {
      "range_end": "40000000000000000000000000000000",
      "address": "tcp://127.0.0.1:3306",
      "id": "funapi",
      "pw": "funapi",
      "database": "funapi1"
    },
    {
      "range_end": "80000000000000000000000000000000",
      "address": "tcp://127.0.0.1:3306",
      "id": "funapi",
      "pw": "funapi",
      "database": "funapi2"
    },
    {
      "range_end": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
      "address": "tcp://127.0.0.1:3306",
      "id": "funapi",
      "pw": "funapi",
      "database": "funapi3"
    }
  ],
}

변경된 내용을 정리하면 다음 표와 같습니다.

DB OLD_MANIFEST.json NEW_MANIFEST.json
funapi1 0000... ~ 8000... 0000... ~ 4000...
funapi2 8000... ~ FFFF... 4000... ~ 8000...
funapi3   8000... ~ FFFF...

위 내용대로 object_db_migrator.py 를 실행합니다.

$ cd /usr/lib/python2.7/dist-packages/funapi/object/
$ object_db_migrator.py --old_manifest='/home/test/OLD_MANIFEST.Json' --new_manifest='/home/test/NEW_MANIFEST.json'

Checks model fingerprint
Makes migration data
Creates a migration file: /tmp/tmp9eqI2T/insert_into_funapi2_from_funapi1_40000000000000000000000000000000_80000000000000000000000000000000.sql
Creates a migration file: /tmp/tmp9eqI2T/insert_into_funapi3_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql
Creates a migration file: /tmp/tmp9eqI2T/delete_from_funapi1_00000000000000000000000000000000_80000000000000000000000000000000.sql
Creates a migration file: /tmp/tmp9eqI2T/delete_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql
Creates migration files to /tmp/tmp9eqI2T
Done

위 결과를 보면 위의 변경 표를 반영하여 다음의 4 가지 스크립트가 생성된 것을 확인할 수 있습니다.

  • 4000... 부터 8000... 까지를 funapi1 에서 funapi2 로 복사하는 스크립트
  • 8000... 부터 ffff... 까지를 funapi2 에서 funapi3 로 복사하는 스크립트
  • funapi1 에서 기존 영역 0000... 부터 8000... 까지 중 이동 대상을 지우는 스크립트
  • funapi2 에서 기존 영역 8000... 부터 ffff... 까지 중 이동 대상을 지우는 스크립트

이제 생성된 sql 파일을 실행합니다.

$ mysql -h localhost -u root -p < /tmp/tmp9eqI2T/delete_from_funapi1_00000000000000000000000000000000_80000000000000000000000000000000.sql
$ mysql -h localhost -u root -p < /tmp/tmp9eqI2T/delete_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql

$ mysql -h localhost -u root -p < /tmp/tmp9eqI2T/insert_into_funapi2_from_funapi1_40000000000000000000000000000000_80000000000000000000000000000000.sql
$ mysql -h localhost -u root -p < /tmp/tmp9eqI2T/insert_into_funapi3_from_funapi2_80000000000000000000000000000000_ffffffffffffffffffffffffffffffff.sql

14.2.3. 필요한 DB 권한

14.2.3.1. 최소 권한

아이펀 엔진 ORM 이 오브젝트를 자동으로 추가/삭제/갱신 하기 위해서는 다음과 같은 최소 권한이 필요합니다.

DB 권한
information_schema SELECT
각 object DB 와 key DB SELECT, INSERT, UPDATE, DELETE, EXECUTE

14.2.3.2. DB 스키마 자동 관리시 추가 권한

ORM 은 게임 서버가 뜰 때 DB table 이나 procedure 를 자동으로 생성해줄 수 있습니다. 이를 위해서는 다음의 권한이 필요합니다.

대상 권한
TABLE, INDEX, PROCEDURE CREATE, ALTER, DROP

14.2.3.3. DB 스키마 자동 관리를 사용하지 않을 때 필요한 schema script 추출

DB 스키마 자동 관리시 추가 권한 에 언급된 권한을 열어두는 것이 부담스러운 경우, 필요한 스키마를 추출하고 관리자 권한으로 해당 스키마를 손으로 생성해줄 수 있습니다.

DB 스키마 script 를 추출하기 위해서는 ORM 기능 설정 파라미터 에서 export_db_schematrue 로 설정한 상태에서 게임 서버를 실행합니다. script 파일이 생성되면 서버는 종료됩니다. script 파일은 /tmp/{{project_name}}_schema/{{project_name}}_schema.{{range_begin}}-{{range_end}}.sql 형태로 생성됩니다.

Important

export_db_schema 옵션은 MANIFEST.json 에 기술된 object DB 를 기준으로 SQL script 를 생성합니다. 따라서 MANIFEST.json 에 기술된 object DB 와 추출한 SQL script 를 적용시킬 DB 간에는 DB schema 가 동일한 상태여야 됩니다.

Note

export_db_schema 로 생성되는 script 에는 스키마의 버전 데이터가 함께 입력되어있습니다. 따라서 export_db_schema 대신 mysqldump 를 이용해 schema script 를 추출한 경우, 아이펀 엔진이 게임 서버를 실행할 때 스키마 재 생성을 시도하게 되며 이때 DB 스키마 자동 관리시 추가 권한 에 필요한 권한이 없다고 오류를 내게 됩니다.

Tip

만약 shell 에서 서버를 실행하고 있다면, MANIFEST.jsonexport_db_schema 를 추가하는 대신 서버의 실행 스크립트 뒤에 --export_db_schema 옵션을 추가할 수도 있습니다.

$ ./my_project-local.sh --export_db_schema

14.2.4. DB 서버 Failover 구현

아이펀 엔진 ORM 은 DB 서버와의 연결이 끊기면 자동으로 해당 서버에 재접속을 시도합니다.

이 특성을 이용하면, MySQL 에 master-slave replication 이 설정 되어 있는 경우 MySQL master 서버 장애시, master 의 IP 를 slave 에 부여함으로써 DB 서버 fail-over 를 구현할 수 있습니다.

정리하면, 다음과 같은 순서로 처리합니다.

  1. MySQL master 서버 장애 발생 (게임 서버와 DB 서버 연결 단절)
  2. DBA 가 Slave 를 새로운 master 로 전환
  3. DBA 가 새로운 master 에 이전 master 의 IP 주소 부여
  4. 아이펀 엔진 ORM에 의해 게임서버와 DB 서버 자동 재연결

Tip

DBA 는 2, 3 번 작업을 스크립트 또는 Master HA 등의 솔루션을 이용하여 자동화할 수 있습니다.

14.2.5. 게임 서버 외부에서 DB 접근시 유의 사항

게임을 서비스하면서 고객지원이나 게임 이벤트 진행에 따른 당첨자 추출 등을 위해 아이펀 엔진의 오브젝트 인터페이스를 사용하는 대신 데이터베이스에 직접 접속하여 SQL Script 를 실행해야하는 경우 다음의 가이드라인을 따라주세요.

아래 설명은 ORM 이 생성하는 스키마와 겹칠 때에 대한 설명이며, ORM 과 관계없는 테이블이나 컬럼은 외부에서 SQL query 를 사용하는데 어떤 제약도 없습니다.

Tip

만약 ORM 이 생성하지 않은 스키마를 아이펀 엔진 게임 서버에서 다뤄야 한다면 DB 접근 Part 1: MySQL 를 참고하세요.

14.2.5.1. 서버가 구동중이지 않은 경우

점검 등과 같이 아이펀 엔진으로 만든 서버가 모두 구동중이지 않은 상황에서는 데이터 조회나 수정 등에 제약이 없습니다.

14.2.5.2. 서버 구동 중 데이터를 조회하는 경우

서버가 구동중이라도 자유롭게 조회할 수 있습니다. 단, 아이펀 엔진에서의 오브젝트 cache 처리 및 오브젝트의 분산 처리 등으로 인하여 엔진에서 수정한 오브젝트 데이터 와 데이터베이스에서 조회한 데이터간에 순간적인 불일치가 있을 수 있습니다.

14.2.5.3. 서버 구동 중 데이터를 추가/변경/삭제하는 경우

서버 구동 중 임의로 데이터를 변경할 경우 데이터 유실 및 오동작의 원인이 될 수 있으며 복구 불가능한 상황이 발생할 수 있습니다. 따라서 서버가 구동중인 상태에서 MySQL Connector 또는 별도 SQL Script 를 통한 데이터 변경을 해서는 안되며, 반드시 점검 등을 통해 서버가 구동중 이지 않은 상태에서 진행해야 합니다.

만일 서버가 구동 중에 데이터를 변경해야된다면, 안전한 처리를 위해 서버 관리 Part 1: RESTful APIs 추가 를 이용하여 관리 기능을 통해 오브젝트를 변경하는 것을 권장합니다.

14.3. ORM 성능 개선

14.3.1. 오브젝트 접근 가이드라인

아이펀 엔진은 게임 오브젝트에 접근할 때마다 내부적으로 자동으로 락을 잡거나 푸는 작업을 합니다. 그렇기 때문에 락의 범위를 최소화하고 접근하는 작업을 배치로 처리하는 것이 바람직합니다. 다음과 같은 방식을 따르시면 됩니다.

  • Object 는 명확한 소유관계가 있고 그 안에서만 가져올 수록 좋다.
  • 한꺼번에 접근하는 Object 수를 적게 유지한다.
  • 매우 빈도 높게 접근되는 Object 는 별도의 시스템으로 구현한다. (월드 수준에 존재하는 Object)
  • 같은 타입의 오브젝트 여러 개에 접근해야하는 경우, 루프를 돌며 하나씩 가져오는게 아니라 object UUID 를 벡터에 넣고 벡터형 Fetch 로 전체를 동시에 얻어온다.
  • 여러개의 업데이트가 상호관계가 없는 경우, 즉 트랜잭션이 아닌 경우, 별도로 쪼개서 Event::Invoke() 에서 처리한다.

14.3.2. 빈번하게 사용되는 Object 처리

게임 내에 유일하게 또는 매우 적은 수만 존재하며 매우 빈번하게 사용되는 Object 는 ORM 이 아닌 Redis 등의 별도의 시스템으로 구현하거나, ORM 으로 구현하더라도 Redis 에 저장하여 expire 를 지정하는게 더 좋습니다.

14.3.3. 여러 오브젝트 한번에 Fetch 하기

다수의 오브젝트를 Fetch 할 때는 loop 를 돌면서 하나씩 처리하는 것보다 벡터형 Fetch 함수를 쓰는 것이 효율적입니다.

다음 코드는 두 유저의 Inventory 에 있는 Item 을 모두 불러옵니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void ArrayToVector(const ArrayRef<Object::Id> &array,
                   std::vector<Object::Id> *vector) {
  for (size_t i = 0; i < array.Size(); ++i) {
    vector->push_back(array.GetAt(i));
  }
}

void FetchTwoUsersItems() {
  Ptr<User> user1 = User::Fetch(user1_uuid);
  Ptr<User> user2 = User::Fetch(user2_uuid);

  std::vector<Object::Id> id_list;

  ArrayToVector(user1->GetInventory(), &id_list);
  ArrayToVector(user2->GetInventory(), &id_list);

  std::vector<std::pair<Object::Id, Ptr<Item>>> items;
  Item::Fetch(id_list, &items);
  ...
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public static void ArrayToSortedSet(ArrayRef<System.Guid> array,
                                    SortedSet<System.Guid> sorted_set)
{
  foreach (System.Guid guid in array)
  {
    sorted_set.Add (guid);
  }
}

void FetchTwoUsersItems()
{
  User user1 = User.Fetch (user_guid);
  User user2 = User.Fetch (user_guid);

  SortedSet<Guid> id_set = new SortedSet<Guid> ();
  ArrayToSortedSet (user1.GetInventory(), id_set);
  ArrayToSortedSet (user2.GetInventory(), id_set);

  Dictionary<System.Guid, Item> items = Item.Fetch (id_set);
  ...
}

14.3.4. Foreign 을 이용한 오브젝트 fetch 최소화

Foreign 으로 지정된 속성은 해당 객체를 가져올 때 자동으로 가져오지 않기 때문에, 꼭 필요하지 않은 다른 객체를 로드하거나 / lock을 걸지 않아서 성능 향상에 도움이 됩니다.

문제가 있는 모델:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "User": {
    "Name": "String KEY",
    "MailBox": "Mail[]"
  },

  "Mail": {
    ...
  }
}

위 ORM 에 따르면 엔진에서 User 오브젝트를 불러오면 MailBox 배열도 항상 같이 가져옵니다. 만일 유저가 평균 100개의 Mail 을 가지고 있다면 이는 상당한 부하가 됩니다.

그런데 MailBox 배열은 우편함 작업을 할 때 빼고는 접근할 필요가 없고, 그 때문에 User 를 읽을 때 같이 읽을 필요가 없습니다. 이런 경우 MailBoxForeign 을 적용하면 필요할 때만 MailBox 를 가져올 수 있습니다.

문제를 해결한 모델:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "User": {
    "Name": "String KEY",
    "MailBox": "Mail[] Foreign"
  },

  "Mail": {
    ...
  }
}

14.3.4.1. 예제: 친구 목록 구현

문제가 있는 모델:

{
  "User": {
    "Name": "String KEY",
    "Inventory": "Item[]",
    "Friends": "User[]"
  },
  "Item": {
    ...
  }
}

위 JSON 모델에서 User 객체를 DB 에서 읽으면, Friends 에 지정된 User 들도 가져오게 됩니다. 그리고 다시 친구 UserFriends 도 연쇄적으로 가져오게 됩니다. 이는 유저를 로딩할 때마다 친구의 친구까지 모두 로딩하게 되기 때문에 잘못된 구현이며, FriendsForeign 으로 정의해야 한다.

문제를 해결한 모델:

{
  "User": {
    "Name": "String KEY",
    "Inventory": "Item[]",
    "Friends": "User[] Foreign"
  },
  "Item": {
    ...
  }
}

이제 FriendsForeign 으로 정의된 배열이기 때문에, Foreign 으로 정의된 배열이나 Map 의 설명대로 다음 두 종류의 getter 를 갖게 됩니다.

  • User::GetFriends() : 친구 User 의 오브젝트 ID 를 포함하는 배열을 반환합니다.
  • User::FetchFriends() : 친구 User 를 모두 Fetch 한 후, 이를 포함하는 배열을 반환합니다.

이를 이용하면 친구 목록에 대해서 다음 두 경우가 가능합니다.

DB 에서 모든 친구 오브젝트를 읽어오기

Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Ptr<User> >  friends = user->FetchFriends();
User user = User.Fetch(user_guid);
ArrayRef<User> friends = user.FetchFriends();

친구 데이터는 읽어오지 않고 친구의 Object Id 만을 반환

Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Object::Id>  friends = user->GetFriends();
User user = User.Fetch(user_guid);
ArrayRef<System.Guid> friends = user.GetFriends ();

14.3.4.2. 예제: 친구 목록에서 불필요하게 인벤토리 읽는 것 피하기

친구 목록에 있는 이름만 읽어오고 싶다고 했을 때, 위의 JSON 모델을 이용하면 다음과 같은 코드를 작성하게 됩니다.

 Ptr<User> user = User::Fetch(user_uuid);
 ArrayRef<Ptr<User> > friends = user->FetchFriends();
 std::vector<std::string> names;
 for (size_t i = 0; i < friends.Size(); ++i) {
   if (friends.GetAt(i)) {
    names.push_back(friends.GetAt(i)->GetName());
   }
 }
User user = User.Fetch (user_guid);
ArrayRef<User> friends = user.FetchFriends();
SortedSet<string> names = new SortedSet<string>();
foreach (User friend in friends)
{
  names.Add (friend.GetName ());
}

위 코드는 user->FetchFriends() 를 했을 때 친구 목록에 해당하는 User 객체를 가져오는데, 이 때 Inventory attribute도 가져오기 때문에 상당한 부하를 주게 됩니다. 이런 경우를 막으려면 InventoryForeign 으로 지정하면 됩니다.

인벤토리를 자동으로 읽어오지 않게 수정된 모델:

{
  "User": {
    "Name": "String KEY",
    "Inventory": "Item[] Foreign",
    "Friends": "User[] Foreign"
  },
  "Item": {
    ...
  }
}

이렇게 수정한 뒤에 Inventory 의 모든 Item 을 읽어올 때는 다음과 같이 처리합니다.

Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Ptr<Item> > inventory = user->FetchInventory();

for (size_t i = 0; i < inventory.Size(); ++i) {
  Ptr<Item> item = inventory.GetAt(i);
  ...
}
User user = User.Fetch(user_guid);
ArrayRef<Item> inventory = user.FetchInventory ();
foreach (Item item in inventory)
{
  ...
}

만일 전체 오브젝트를 가져오는게 아니라 Inventory 의 특정 Item 만 읽어온다면 다음처럼 처리합니다.

Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Object::Id> inventory = user->GetInventory();
Ptr<Item> item = Item::Fetch(inventory.GetAt(3));
User user = User.Fetch (user_guid);
ArrayRef<Guid> inventories = user.GetInventory();
Item my_item = Item.Fetch(inventories.GetAt(3));

14.3.5. 올바른 락 타입 사용

Fetch 시 LockType 에 언급된 대로, 아이펀 엔진은 오브젝트 접근 시 다음과 같은 락 타입을 제공하고 있습니다.

  • kReadLock
  • kWriteLock
  • kReadCopyNoLock

kReadLock 의 경우 다른 이벤트가 이미 kReadLock 으로 오브젝트를 불러와 사용중이라 도 해당 오브젝트를 가져와 읽기 모드로 사용할 수 있습니다.

kWriteLock 의 경우 다른 이벤트가 이미 kWriteLock 으로 오브젝트를 불러와 사용중이면 그 사용이 끝날 때까지는 모든 락 타입에 대해서 배타적입니다. 따라서 오브젝트를 읽기만 하는 경우에는 kReadLock 으로 처리하는 것이 성능 향상에 도움이 될 수 있습니다.

Tip

개발 편의를 위하여 Fetch(...)kWriteLock 을 기본 인자값으로 사용하고 있으나, 이를 적절하게 kReadLock 로 바꾸는 것이 좋습니다.

그런데 kReadLock 으로 오브젝트를 사용중이라 하더라도 그 시간이 길어지는 경우, 해당 오브젝트 수정을 위해 kWriteLock 으로 가져오려는 다른 이벤트는 kReadLock 사용이 완료될 때까지 처리가 지연될 수 있습니다.

kReadCopyNoLock 은 락을 잡지 않고 단순히 오브젝트의 사본을 가져옴으로써 이와 같은 읽기에 의한 쓰기의 병목 을 방지합니다. 그러나 kReadCopyNoLock 은 사본으로 작업하기 때문에 순간적으로 데이터가 불일치 할 수 있습니다.

그러나 친구 목록, 랭킹 조회와 같이 단순히 데이터를 보여주기만 하는 컨텐츠의 경우 순간적으로 데이터가 불일치하더라도 문제가 없을 것입니다. 보통 데이터 갱신이 필요할 경우 목록창을 닫고 다시 열거나 갱신 버튼을 통해서 최신 목록을 재요청할 것이기 때문입니다. 이처럼 단순하게 보여주기만 하는 데이터이며 약간의 불일치를 감수할 수 있는 경우 kReadCopyNoLock 사용을 권장합니다.

다음은 kReadCopyNoLock 으로 친구 목록을 만드는 코드입니다.

Json response;

Ptr<User> user = User::Fetch(user_uuid);
ArrayRef<Ptr<User> >  friends = user->FetchFriends(kReadCopyNoLock);

response["friends"].SetArray();
for (size_t i = 0; i < friends.Size(); ++i) {
  Ptr<User> friend = friends.GetAt(i);
  if (not friend) {
    continue;
  }

  Json friend_json;
  friend_json.SetObject();
  friend_json["name"] = friend->GetName();
  friend_json["level"] = friend->GetLevel();  // Level 이란 Attribute 가 있다고 가정

  response["friends"].PushBack(friend_json);
}
JObject response = new JObject ();
JArray friends_json = new JArray ();

User user = User.Fetch (user_guid);
ArrayRef<User> friends = user.FetchFriends (funapi.LockType.kReadCopyNoLock);

foreach (User friend in friends)
{
  JObject obj = new JObject ();
  obj ["name"] = friend.GetName ();
  obj ["level"] = friend.GetLevel (); // Level 이란 Attribute 가 있다고 가정
  friends_json.Add(obj);
}

response ["friends"] = friends_json;

14.3.6. DB index 추가

Table index 는 자유롭게 추가 가능하며 (고급) ORM 에 의한 DB 스키마 에서 설명한 procedure 의 경우 signature 를 제외하고 변경 가능합니다. 만일 index 를 추가하거나 procedure 를 수정해서 성능 향상을 얻을 수 있는 경우 이 특성을 활용할 수 있습니다.

14.4. (고급) DB에서 오브젝트 검색

ORM 의 Fetch...(...) 만으로 충분하지 않을 경우, SQL의 SELECT 와 유사하게 지정된 검색 조건에 맞는 오브젝트 ID를 검색하는 기능을 쓸 수 있습니다. 이를 위해 ORM 은 각 오브젝트 모델별, 속성별로 다음과 같은 메소드를 생성해줍니다.

void ObjectName::SelectBy{{AttributeName}}(cond_type, cond_value, callback)
  • cond_type: Object::kEqualTo, Object::kLessThan, 또는 Object::kGreaterThan

  • cond_value: 기준이 되는 값.

  • callback: void(const Ptr<std::set<Object::Id> > &object_ids) 타입의 함수.

    object_ids 로 검색된 object id 가 전달되며 만약 오류가 발생하면 object_ids 로 NULL 이 전달됩니다.

검색된 오브젝트 ID 를 이용하여 Fetch(...) 를 통해 오브젝트를 접근합니다. 그러나 그 사이에 다른 곳에서 Object 가 변경되거나 삭제 되었을 수 있으니 아래 예제와 같이 검색 조건에 해당하는지를 다시 확인해야됩니다.

Important

검색 조건에 따라 너무 많은 Object ID 가 검색되지 않도록 해야 합니다.

Important

검색 조건에 해당하는 column 에 직접 index 를 추가해야 합니다. 엔진 입장에서는 어떤 column 이 검색 조건으로 사용될지 알 수 없기 때문입니다.

Note

이 기능은 많은 경우 게임 콘텐츠 구현을 위해서가 아니라 운영 기능 구현을 위해서 사용하게 됩니다.

Note

만약 이 기능으로도 충분하지 않다면 아이펀 엔진의 DB 접근 Part 1: MySQL 로 직접 SQL query 를 만들어 검색할 수도 있습니다. 그러나, SQL 을 이용하여 직접 접근하는 경우 ORM 과의 충돌을 방지 하기 위해 읽기 전용으로 사용하셔야 합니다.

14.4.1. 예제: 레벨 100 이상의 유저 검색

운영툴 용도로 제작된 이 예제 서버는 레벨 100 이상의 유저를 찾아 99 로 낮춘다고 가정 하겠습니다. Character::SelectByLevel(...)Character::Fetch(...) 사이에 다른 곳에서 오브젝트를 변경하거나 삭제할 수 있기 때문에, 오브젝트가 존재하는지 여부와 검색 조건을 다시 한 번 확인해야됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Select() {
  Character::SelectByLevel(Object::kGreaterThan, 99, OnSelected);
}

void OnSelected(const Ptr<std::set<Object::Id> &object_ids) {
  if (not object_ids) {
    LOG(ERROR) << "error";
    return;
  }

  auto level_down = [](const Object::Id &object_id) {
    Ptr<Character> character = Character::Fetch(object_id);
    if (not character || character->GetLevel() < 100) {
      return;
    }
    character->SetLevel(99);
  };

  for (const Object::Id &object_id: *object_ids) {
    Event::Invoke(bind(level_down, object_id));
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void Select()
{
  Character.SelectByLevel(Object.kGreaterThan, 99, OnSelected);
}

void OnSelected(SortedSet<Object.Id> object_ids)
{
  if (object_ids = null)
  {
    Log.Error ("error");
    return;
  }

  auto level_down = [] (Object.Id &object_id) {
    Character character = Character.Fetch (object_id);
    if (character == null || character.GetLevel () < 100)
    {
      return;
    }
    character.SetLevel (99);
  };

  for (Object.Id &object_id in object_ids)
  {
    Event.Invoke (() => { level_down(object_id); });
  }
}

14.5. (고급) ORM 에 의한 DB 스키마

14.5.1. ORM 에 의한 DB 스키마 자동 변경

아이펀 엔진은 사용자가 정의한 오브젝트 모델에 따라 DB 스키마를 유지합니다. 오브젝트 모델을 변경하면 서버 구동 시 아이펀 엔진이 스키마 변경 사항을 비교하여 다음과 같이 자동으로 DB 스키마를 수정합니다.

  • 새로 추가된 오브젝트 모델에 대한 테이블과 DB 프로시져 생성
  • 자동 수정이 가능한 경우 테이블 컬럼 수정 (DB 프로시져도 같이 수정)

Tip

아이펀 엔진 ORM 은 테이블 수정시 컬럼이 삭제되더라도 삭제 작업은 하지 않습니다. 이는 자칫 실수로 컬럼을 지운 경우 치명적인 결과를 낼 수 있기 때문입니다.

아이펀 엔진 ORM 은 다음의 경우 자동 수정이 불가능하다고 판단합니다.

  • 모델의 속성을 정의할 때 사용한 String(n) 에서 n 이 이전에 비해 줄어든 경우.
  • ORM 기능 설정 파라미터 에서 설명하는 db_string_length 또는 db_key_string_length 가 이전에 비해 줄어든 경우.
  • Primitive 타입 속성이 다른 타입으로 변경된 경우.

자동 수정이 불가능한 경우 다음과 같은 로그를 출력하고 구동을 중지됩니다. 이 경우에는 로그를 보고 수동으로 스키마를 수정하셔야 합니다.

예시: Character 오브젝트의 Level 을 Integer 에서 String(24) 로 변경한 경우

F1028 15:56:50.045100  3951 object_database.cc:996] Mismatches object_model=User, table_name=tb_Object_User, column_name=col_Cash, expected_model_column(type=VARCHAR(24), character_set=utf8), mismatched_db_column(type=bigint(8), character_set=)

14.5.2. String 길이 축소

String 길이가 늘어난 경우에는 아이펀 엔진이 자동으로 해당 길이로 수정하지만, 줄어든 경우라면 수동으로 수정해줘야 합니다. 수동으로 스키마를 수정하는 방법은 두 가지가 있습니다.

방법 1: 만약 DB 를 제거해도 된다면 DROP {{dbname}}CREATE {{dbname}} 으로 DB 를 새로 생성하고 서버를 다시 실행합니다.

방법 2: DB 데이터 유지에 따른 DB 를 제거할 수 없다면 다음과 같이 ALTER 스크립트를 만들고 DB 에서 실행합니다.

-- key 테이블 변경
ALTER TABLE tb_Key_{{Object_Name}}_{{KeyColumnName}} MODIFY col_{{KeyColumnName}} CHAR({{NewLength}});

-- object 테이블 변경
ALTER TABLE tb_Object_{{ObjectName}} MODIFY col_{{KeyColumnName}} CHAR({{NewLength}});

다음은 key string 의 길이를 20 에서 12 로 변경하는 경우의 예입니다.

ALTER TABLE tb_Key_User_Name MODIFY col_Name CHAR(12);
ALTER TABLE tb_Object_User MODIFY col_Name CHAR(12);

만들어진 sql script 를 실행하고 서버를 다시 시작합니다. 스키마를 변경한 후 서버를 시작하면 DB 프로시져는 아이펀 엔진이 자동으로 재생성합니다.

Note

만약 DB 서버 Sharding 에 따른 key db 와 object db 가 나뉘어져 있는 경우에는 위 sql script 를 각 key db 와 object db 에서 실행해야합니다.

Tip

만약 변경해야할 컬럼들이 많다면 다음 sql script 실행을 통해 변경해야 할 컬럼들의 ALTER ... script 를 만들 수 있습니다.

key string 의 길이를 20 에서 12로, string 의 길이를 4096 에서 100로 변경하겠다고 가정해보겠습니다.

-- MANIFEST.json 에 입력한 database 를 입력합니다.
USE [database];

-- 변경 대상인 key string 컬럼의 현재 타입을 입력합니다.
SET @org_key_string_length = 'CHAR(20)';

-- key string 컬럼을 변경하려는 type 으로 입력합니다.
SET @key_string_length = 'CHAR(12)';

-- 변경 대상인 string 컬럼의 현재 타입을 입력합니다.
SET @org_string_length = 'VARCHAR(4096)';

-- string 컬럼의 변경하려는 타입으로 입력합니다.
SET @string_length = 'VARCHAR(100)';

-- ALTER ... sql script 를 만듭니다.
SELECT CONCAT(GROUP_CONCAT(sql_script separator '; '), ';') AS sql_script
FROM
(
  SELECT GROUP_CONCAT('ALTER TABLE ', table_name, ' MODIFY ', column_name, ' ',
         @key_string_length separator '; ') AS 'sql_script'
  FROM information_schema.columns
  WHERE table_schema = DATABASE() AND column_type = @org_key_string_length
  UNION ALL
  SELECT GROUP_CONCAT('ALTER TABLE ', table_name, ' MODIFY ', column_name, ' ',
         @string_length separator '; ') AS 'sql_script'
  FROM information_schema.columns
  WHERE table_schema = DATABASE() AND column_type = @org_string_length
) AS A;

위 sql script 를 실행하면 다음과 같이 ALTER ... sql script 가 만들어 집니다.

ALTER TABLE tb_Key_Character_Name MODIFY col_Name CHAR(12); ALTER TABLE tb_Key_User_Id MODIFY col_Id CHAR(12); ALTER TABLE tb_Object_Character MODIFY col_Name CHAR(12); ALTER TABLE tb_Object_User MODIFY col_Id CHAR(12); ALTER TABLE tb_Object_Character MODIFY col_Name2 VARCHAR(100); ALTER TABLE tb_Object_Character MODIFY col__tag VARCHAR(100); ALTER TABLE tb_Object_User MODIFY col__tag VARCHAR(100);

14.5.3. ORM 의 타입이 SQL 타입으로 변환되는 규칙

ORM 의 타입은 다음처럼 SQL 타입으로 변환됩니다.

iFun Engine Type SQL Type
Bool tinyint(1)
Integer bigint(8)
Double double
String varchar(4096). Key 일 경우 char(12)
Object binary(16)
User Defined Object binary(16)

Note

위 String 의 SQL Type 은 기본값을 나타내며 ORM 기능 설정 파라미터 에서 db_string_lengthdb_key_string_length 를 수정하면 길이를 변경할 수 있습니다.

14.5.4. ORM 의 table naming 규칙

아이펀 엔진 ORM 은 아래와 같은 table 을 생성합니다.

Tip

아래 설명에서 col__ObjectId_ 는 binary 로 저장되어 있으므로 hex() 함수로 읽을 수 있습니다.

14.5.4.1. tb_Object_{{ObjectName}}

JSON 으로 정의한 오브젝트의 table 입니다. 모든 shard 에 생성됩니다. 속성들을 컬럼으로 저장합니다.

Columns 설명
col__ObjectId_ 아이펀 엔진이 내부적으로 object 를 식별하기 위하여 부여한 Uuid 타입의 object id.
col_{{AttributeName}}  
...  

14.5.4.2. tb_Key_{{ObjectName}}_{{KeyAttributeName}}

JSON 으로 정의한 오브젝트의 Key 속성값이 저장되는 table 입니다. Key Database 에 생성됩니다. Key 속성들을 컬럼으로 저장합니다.

Columns 설명
col__ObjectId_ 아이펀 엔진이 내부적으로 object 를 식별하기 위하여 부여한 Uuid 타입의 object id.
col_{{KeyAttributeName}} 오브젝트의 key 속성입니다. Primary Key 로 지정됩니다.

14.5.4.3. tb_Object_{{ObjectName}}_ArrayAttr_{{AttributeName}}

배열 타입의 속성을 갖는 경우 생성됩니다. 모든 shard 에 생성됩니다. col__ObjectId_ 컬럼과 col__Index_ 컬럼은 복합키로 구성됩니다.

Columns 설명
col__ObjectId_ 아이펀 엔진이 내부적으로 object 를 식별하기 위하여 부여한 Uuid 타입의 object id.
col__Index_ Array 의 index 입니다. SQL type 은 bigint 입니다.
col__Value_ 해당 index 의 value 값입니다.

14.5.4.4. tb_Object_{{ObjectName}}_MapAttr_{{AttributeName}}

Map 타입의 속성을 갖는 경우 생성됩니다. 모든 shard 에 생성됩니다. col__ObjectId_ 컬럼과 col__Key_ 컬럼은 복합키로 구성됩니다.

Columns 설명
col__ObjectId_ 아이펀 엔진이 내부적으로 object 를 식별하기 위하여 부여한 Uuid 타입의 object id.
col__Key_ Map 의 key 입니다.
col__Value_ 해당 key 의 value 값입니다.

14.5.5. ORM 이 생성하는 DB Index 와 Constraint

아이펀 엔진은 table 생성시 col__ObjectId_ column 을 Primary Key*로 설정하고, key 로 지정된 속성에 *Non-Clustred IndexUnique Constraint 를 설정합니다.

Tip

아이펀 엔진이 생성한 index 와 constraint 외에 다른 index 나 constraint 를 추가하실 수 있습니다.

14.5.6. ORM 이 생성하는 DB Procedure

아이펀 엔진은 object table 을 생성 후 아래와 같은 procedure 도 함께 생성하고 이 procedure 를 통해서만 database 의 데이터에 접근합니다.

  • sp_Object_Get_{{ObjectName}} : 지정된 object id 로 object 를 DB 에서 불러오기 위하여 사용.
  • sp_Object_Get_Object_Id_{{ObjectName}}By{{KeyAttributeName}} : 지정된 object 의 key 로 object id 를 DB 에서 불러오기 위하여 사용.
  • sp_Object_Key_Insert_{{ObjectName}}_{{KeyAttributeName}} : 새로운 object 의 key 를 DB 에 저장하기 위하여 사용.
  • sp_Object_Insert_{{ObjectName}} : 새로운 object 를 DB 에 저장하기 위하여 사용.
  • sp_Object_Update_{{ObjectName}} : object 의 변경사항을 저장하기 위하여 사용.
  • sp_Object_Delete_{{ObjectName}} : object 를 삭제하기 위하여 사용.
  • sp_Object_Delete_Key_{{ObjectName}} : object 의 key 를 삭제하기 위하여 사용.
  • sp_Object_Array_{{ObjectName}}_{{ArrayAttributeName}} : array attribute 의 element 를 추가/삭제할 때 사용.

Tip

index/constraint 처럼 procedure 도 signature 만 변경하지 않는다면 자유롭게 수정가능합니다. 단, procedure 에서 반환되는 rowset 의 column 도 일치하여야 합니다.

14.6. (고급) ORM 프로파일링

ORM 기능 설정 파라미터 에 입력한 각 데이터베이스 별로 쿼리 처리시간 통계를 측정하여 제공합니다. 이 기능을 사용하려면 다음 옵션들의 활성화 및 port 가 설정되어야 합니다.

통계를 보기 위해서는 다음과 같이 제공되는 API 를 호출합니다.

GET http://{ip}:{api-service-port}/v1/counters/funapi/object_database_stat/

결과는 각 데이터베이스의 range_end 값으로 구분하며 통계 결과의 각 항목의 의미는 다음과 같습니다.

항목  
all_time 누적된 통계
last1min 1분전 통계
write_count 쓰기 쿼리가 처리된 횟수
write_mean_in_sec 평균 쓰기 처리 시간
write_stdev_in_sec 쓰기 처리 시간 표준편차
write_max_in_sec 최대 쓰기 처리 시간
read_count 읽기 쿼리가 처리된 횟수
read_mean_in_sec 평균 읽기 처리 시간
read_stdev_in_sec 읽기 처리 시간 표준편차
read_max_in_sec 최대 읽기 처리 시간

Note

range_end 값이 00000000-0000-0000-0000-000000000000 인 결과는 key_database 를 의미합니다. (샤딩을 하지 않았어도 동일한 의미를 가집니다.) 이 결과에는 Key 를 가지는 오브젝트를 가져오기 / 생성하기 관련 쿼리 처리 시간 통계가 측정됩니다.

결과 예

{
  "00000000-0000-0000-0000-000000000000": {
      "database": "funapi",
      "address": "tcp://127.0.0.1:3306",
      "all_time": {
          "write_count": 4,
          "write_mean_in_sec": 0.000141,
          "write_stdev_in_sec": 0.000097,
          "write_max_in_sec": 0.000286,
          "read_count": 1000,
          "read_mean_in_sec": 0.031476,
          "read_stdev_in_sec": 0.033169,
          "read_max_in_sec": 0.104138
      },
      "last1min": {
          "write_count": 0,
          "write_mean_in_sec": 0.0,
          "write_stdev_in_sec": 0.0,
          "write_max_in_sec": 0.0,
          "read_count": 0,
          "read_mean_in_sec": 0.0,
          "read_stdev_in_sec": 0.0,
          "read_max_in_sec": 0.0
      }
  },
  "ffffffff-ffff-ffff-ffff-ffffffffffff": {
      "database": "funapi",
      "address": "tcp://127.0.0.1:3306",
      "all_time": {
          "write_count": 4,
          "write_mean_in_sec": 0.000086,
          "write_stdev_in_sec": 0.00006,
          "write_max_in_sec": 0.000176,
          "read_count": 19989,
          "read_mean_in_sec": 0.057533,
          "read_stdev_in_sec": 0.045418,
          "read_max_in_sec": 0.198318
      },
      "last1min": {
          "write_count": 0,
          "write_mean_in_sec": 0.0,
          "write_stdev_in_sec": 0.0,
          "write_max_in_sec": 0.0,
          "read_count": 0,
          "read_mean_in_sec": 0.0,
          "read_stdev_in_sec": 0.0,
          "read_max_in_sec": 0.0
      }
  }
}

Note

데이터베이스 샤딩 을 적용하지 않아도 프로파일링 결과는 위와 동일한 형태로 출력됩니다. 아이펀엔진에서는 샤딩한 것과 같이 처리하기 때문입니다.

만약 다음과 같이 데이터베이스 샤딩을 적용하였다면 결과는 그 다음과 같습니다.

샤딩을 적용한 MANIFEST.json

"Object": {
  "enable_database" : true,
  "key_database" : {
    "address": "tcp://127.0.0.1:3306",
    "id": "funapi",
    "pw": "funapi",
    "database": "funapi_key"
  },
  "object_databases": [
    {
      "range_end": "80000000000000000000000000000000",
      "address": "tcp://127.0.0.1:3306",
      "id": "funapi",
      "pw": "funapi",
      "database": "funapi_obj1"
    },
    {
      "range_end": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
      "address": "tcp://127.0.0.1:3306",
      "id": "funapi",
      "pw": "funapi",
      "database": "funapi_obj2"
    }
  ]
}

결과 예

{
  "00000000-0000-0000-0000-000000000000": {
      "database": "funapi_key",
      "address": "tcp://127.0.0.1:3306",
      "all_time": {
          "write_count": 4,
          "write_mean_in_sec": 0.000141,
          "write_stdev_in_sec": 0.000097,
          "write_max_in_sec": 0.000286,
          "read_count": 1000,
          "read_mean_in_sec": 0.031476,
          "read_stdev_in_sec": 0.033169,
          "read_max_in_sec": 0.104138
      },
      "last1min": {
          "write_count": 0,
          "write_mean_in_sec": 0.0,
          "write_stdev_in_sec": 0.0,
          "write_max_in_sec": 0.0,
          "read_count": 0,
          "read_mean_in_sec": 0.0,
          "read_stdev_in_sec": 0.0,
          "read_max_in_sec": 0.0
      }
  },
  "80000000-0000-0000-0000-000000000000": {
      "database": "funapi_obj1",
      "address": "tcp://127.0.0.1:3306",
      "all_time": {
          "write_count": 4,
          "write_mean_in_sec": 0.0001,
          "write_stdev_in_sec": 0.000061,
          "write_max_in_sec": 0.000191,
          "read_count": 20011,
          "read_mean_in_sec": 0.055629,
          "read_stdev_in_sec": 0.046967,
          "read_max_in_sec": 0.224221
      },
      "last1min": {
          "write_count": 0,
          "write_mean_in_sec": 0.0,
          "write_stdev_in_sec": 0.0,
          "write_max_in_sec": 0.0,
          "read_count": 0,
          "read_mean_in_sec": 0.0,
          "read_stdev_in_sec": 0.0,
          "read_max_in_sec": 0.0
      }
  },
  "ffffffff-ffff-ffff-ffff-ffffffffffff": {
      "database": "funapi_obj2",
      "address": "tcp://127.0.0.1:3306",
      "all_time": {
          "write_count": 4,
          "write_mean_in_sec": 0.000086,
          "write_stdev_in_sec": 0.00006,
          "write_max_in_sec": 0.000176,
          "read_count": 19989,
          "read_mean_in_sec": 0.057533,
          "read_stdev_in_sec": 0.045418,
          "read_max_in_sec": 0.198318
      },
      "last1min": {
          "write_count": 0,
          "write_mean_in_sec": 0.0,
          "write_stdev_in_sec": 0.0,
          "write_max_in_sec": 0.0,
          "read_count": 0,
          "read_mean_in_sec": 0.0,
          "read_stdev_in_sec": 0.0,
          "read_max_in_sec": 0.0
      }
  }
}

14.7. ORM 기능 설정 파라미터

  • enable_database: 실제 데이터베이스와의 연동을 활성화합니다. 간단한 테스트 또는 개발 단계에서는 false 로 지정하면 DB 를 준비하지 않아도 됩니다. (type=bool, default=false)
  • db_mysql_server_address: ORM 이 사용할 DB 주소. (type=string, default=”tcp://127.0.0.1:3306”)
  • db_mysql_id: ORM 이 사용할 MySQL user id (type=string, default=””)
  • db_mysql_pw: ORM 이 사용할 MySQL password (type=string, default=””)
  • db_mysql_database: ORM 이 사용할 MySQL database 이름 (type=string, default=””)
  • cache_expiration_in_ms: DB 에서 읽어들여서 캐싱하고 있는 오브젝트를 캐시에서 내릴 때까지 밀리초 (type=int64, default=300000)
  • copy_cache_expiration_in_ms: 원격 서버로부터 오브젝트를 복사해온 경우 이 오브젝트를 캐시에서 내릴때까지 밀리 초 (type=int64, default=700)
  • enable_delayed_db_update: DB 업데이트를 그때마다 하지 않고 딜레이 했다가 batch 처리할지 여부 (type=bool, default=false)
  • db_update_delay_in_second: DB 업데이트를 바로 하지 않고 batch 하는 경우, 작업을 딜레이 하는 초단위 시간 (type=int64, default=10)
  • db_read_connection_count: 읽기 작업을 위한 DB 연결 갯수 (type=int64, default=8)
  • db_write_connection_count: 쓰기 작업을 위한 DB 연결 갯수 (type=int64, default=16)
  • db_key_shard_read_connection_count: object_subsystem_sharding 사용시 key database 에 읽기 작업을 위한 DB 연결 갯수 (type=int64, default=8)
  • db_key_shard_write_connection_count: object_subsystem_sharding 사용시 key database 에 쓰기 작업을 위한 DB 연결 갯수 (type=int64, default=16)
  • db_character_set: DB 의 character set. 아이펀 엔진은 DB 의 설정이 아닌 이 설정 값을 따름. (type=string, default=”utf8”)
  • export_db_schema: 만일 true 인 경우 DB 스키마 생성 스크립트를 출력하고 종료. 필요한 DB 권한 참고. (type=bool, default=false)

직접 설정을 바꿀 일이 거의 없는 설정들

  • db_string_length: 문자열 속성의 경우 대응되는 SQL VARCHAR 길이 (type=int32, default=4096)
  • db_key_string_length: Key 인 문자열 속성의 경우 대응되는 SQL CHAR 의 길이 (type=int32, default=12)
  • use_db_stored_procedure: RAW SQL 문장 대신 stored procedure 를 사용할지 여부 (type=bool, default=true)
  • use_db_stored_procedure_full_name: 축약된 이름 대신 긴 이름의 stored procedure 를 사용할지 여부 (type=bool, default=true)
  • use_db_char_type_for_object_id: Object ID 를 표시하기 위해서 SQL DB 상에 BINARY(16) 대신 CHAR(32) 를 사용함 (type=bool, default=false)
  • enable_assert_no_rollback: 코드 상에 AssertNoRollback() 체크를 활성화 시킴. 원하지 않는 롤백 감지 참고. (type=bool, default=true)
  • use_db_select_transaction_isolation_level_read_uncommitted: Select 쿼리를 실행할 때 TRANSACTION ISOLATION LEVELREAD UNCOMMITTED 로 설정. false 일 경우에는 mysql 의 기본값이 사용됨. (type=bool, default=true)

14.8. MySQL(MariaDB) 서버 설정 및 관리

이 가이드는 MySQL 또는 MariaDB에 익숙하지 않은 사용자를 위한 설정 방법을 제공합니다. 그 외에 성능 개선에 필요한 설정들도 설명합니다.

설정을 변경하려면 서비스를 종료한 후 설정 파일을 변경한 뒤 재시작 하는 일련의 과정이 필요합니다. 설정 파일은 운영체제마다 조금씩 다른 위치에 있습니다.

  • 우분투의 경우는 다음 둘 중 한 곳에 위치해 있습니다.
/etc/mysql/my.cnf
/etc/mysql/conf.d/mysql.cnf
  • CentOS의 경우에는 아래 경로에 위치해 있습니다.
/etc/my.cnf

설정은 설정=값 형태로 기재해야 하며 반드시 [mysqld] 항목에 포함되어야만 적용이 됩니다.

  • 잘못된 예
[mysqldump]
...
max_connections = 300
  • 올바른 예
[mysqld]
...
max_connections = 300

14.8.1. 기본 설정 가이드

아래 설정들은 단일 머신에서 개발할 때는 특별히 확인할 필요가 없습니다. 하지만 테스트 또는 상용 환경과 같이 게임 서버와 DB 서버가 각각 다른 머신에서 실행되는 경우라면 확인해볼 필요가 있습니다.

  • bind-address:

MySQL이 연결을 주고 받을 네트워크 주소를 지정합니다. 우분투의 경우 이 값이 127.0.0.1 로 설정되어 로컬 상에서만의 접근을 허용하고 있습니다.

이 값을 0.0.0.0 으로 설정하거나 다음과 같이 주석 처리(#)할 경우 MySQL은 모든 곳으로부터 연결을 허용합니다.

[mysqld]
...
# bind-address = 127.0.0.1
  • max_connections:

MySQL이 연결을 주고 받는 최대 커넥션 개수를 지정합니다. 기본 값은 151로 상용 환경에서 사용하기엔 부족합니다. 권장되는 값은 8GB 램을 기준으로 300에서 500 정도이며 아래에서 설명할 open_files_limit 값과 함께 보는 것이 좋습니다.

아이펀 엔진에서 사용하는 커넥션 개수는 다음과 같은 공식에 따라 증가 합니다.

  • DB에 접속하는 서버 개수 x MANIFEST.json 에 설정된 object_databases 개수 x MANIFEST.json 에 설정에 따른 커넥션 개수

MANIFEST.json 설정 중 커넥션 개수를 증가시키는 항목들은 다음과 같습니다.

  • db_read_connection_count
  • db_write_connection_count
  • db_key_shard_read_connection_count
  • db_key_shard_write_connection_count
[mysqld]
...
max_connections = 300
  • open_files_limit:

운영체제에서 MySQL이 사용하는 파일 디스크립터를 몇 개까지 허용할 것인지 결정합니다.

이 값은 max_connections 값의 5배로 설정되므로 일반적으로 값을 변경할 필요는 없지만 보다 많은 테이블을 사용할 때는 더 높은 값을 할당해야 합니다. 가장 적절한 설정 방법은 10000으로 지정한 후 필요에 따라 늘리는 것입니다. 더 높은 값을 사용할 수록 많은 메모리가 사용되므로 메모리를 충분히 두는 것이 좋습니다.

[mysqld]
...
open_files_limit = 10000

Ubuntu 16.04 또는 CentOS 7에서는 MySQL 설정 파일이 아닌 systemd 설정 파일에 지정해야만 정상적으로 값이 적용됩니다. 적용 방법은 다음과 같습니다.

$ sudo vi /lib/systemd/system/mysql.service

[Service]
...
LimitNOFILE=10000

14.8.2. (고급)성능 개선 가이드

MySQL에서 제공하는 일부 설정들은 서버 자원을 보다 효율적으로 사용할 수 있도록 합니다. 그러나 항상 성능을 개선한다는 보장은 없기 때문에 하나씩 바꾸는 것이 좋습니다.

  • innodb_buffer_pool_size:

    테이블 및 인덱스 데이터의 캐시가 저장되는 버퍼의 크기를 지정합니다. 이 값이 클수록 캐시 적중률이 높아져 디스크의 부하를 줄일 수 있습니다. 하지만 너무 큰 값은 메모리 swap 을 발생시켜 큰 성능 저하가 발생하므로 주의해야 합니다.

    가장 적절한 값을 찾기 위해서는 시스템 메모리의 80%를 innodb_buffer_pool_size 사이즈로 지정한 후 iostat 이나 vmstat 과 같은 툴로 메모리 swap 이 발생하는지 모니터링하는 것입니다. 만약 DB 서버에 다른 프로세스가 실행 중이거나 앞서 설명한 max_connections 값이 충분히 높은 상태라면 80%보다 낮은 값으로 시작하는 것이 좋습니다.

    [mysqld]
    ...
    innodb_buffer_pool_size = 8GB # MySQL 서버가 10GB을 사용할 경우
    
  • innodb_log_file_size:

    redo 로그 파일 크기를 지정합니다. 높은 값을 지정할 경우 디스크 쓰기 빈도가 낮아져 부하를 줄여주지만 MySQL에 장애가 발생했을 때 복구에 걸리는 시간이 길어질 수 있습니다.

    상용 환경에서는 서버의 규모에 따라 64MB에서 512MB 사이의 값을 지정하는 것이 좋습니다. 특히 1초 이내의 순간동안 많은 양의 UPDATE가 발생할 수 있는 상황에서는 innodb_log_file_size 값을 높게 지정함으로써 디스크 부하를 크게 줄일 수 있습니다.

    Note

    이 값을 변경하기 위해서는 MySQL을 종료한 후

    redo 파일(/var/lib/mysql/lib_logfile*)을 삭제하거나 다른 곳으로 옮겨야 합니다.

    [mysqld]
    ...
    innodb_log_file_size = 256MB
    
  • innodb_flush_method:

    데이터를 디스크에 플러시 하는 방법을 변경합니다. 기본값은 O_DSYNC 로 OS에서 제공하는 페이지 캐시를 이용합니다.

    innodb_buffer_pool_size 값이 충분히 크다면 이 값을 O_DIRECT 로 변경함으로써 운영체제에서 제공하는 페이지 캐시를 무시할 수 있습니다. 이는 MySQL과 OS가 캐시를 중복 저장하지 않도록 하므로 약간의 성능 개선을 기대할 수 있습니다.

    innodb_buffer_pool_size 값이 충분하지 않은 상태에서 O_DIRECT 를 사용하는 경우에는 오히려 캐시 적중률이 낮아짐으로써 디스크 부하가 커질 수 있으니 값을 변경하기 전에는 충분한 테스트가 필요합니다.

    [mysqld]
    ...
    innodb_flush_method=O_DIRECT
    
  • innodb_flush_log_at_trx_commit:

    redo 로그 파일 기록 방식을 변경합니다. 지정할 수 있는 값은 0,1,2 로 3가지가 있으며 기본값은 1입니다.

    innodb_flush_log_at_trx_commit=0 : 트랜잭션과 관계없이 1초마다 로그 버퍼에 있는 내용을 로그 파일에 저장한 후 디스크로 플러시 합니다. MySQL이 멈출 경우 최대 1초 이내의 로그 버퍼 데이터가 유실될 수 있습니다. 디스크 부하가 가장 적지만 가장 불안전한 파일 기록 방식입니다.

    innodb_flush_log_at_trx_commit=1 : 하나의 트랜잭션이 완료될 때마다 로그 버퍼에 있는 내용을 로그 파일에 저장한 후 디스크로 플러시 합니다. 기록 방식 중 가장 느린 속도를 가지고 있으며 디스크에 많은 부하를 주지만 데이터의 안정성을 보장받을 수 있습니다.

    innodb_flush_log_at_trx_commit=2 : 하나의 트랜잭션이 완료될 때마다 로그 버퍼에 있는 내용이 로그 파일로 쓰여집니다. 이와 동시에 1초마다 로그 파일에 있는 내용이 디스크로 플러시 됩니다. MySQL이 멈추더라도 데이터의 안정성을 보장받지만 OS가 멈출 경우 최대 1초 동안 플러시 되지 않은 로그 버퍼 데이터가 유실될 수 있습니다.

    기본 값(1)을 제외한 두 옵션 모두 트랜잭션이 유실될 수 있으므로 이 값을 변경하는 것 보다는 다른 값을 우선적으로 보는 것이 좋습니다. 1의 경우에도 하드웨어 레벨의 wrtie-back 캐시가 켜져 있다면 데이터 손실의 위험이 있으므로 캐시 옵션을 체크해보는 것이 좋습니다. 손실을 감안할 수 있는 데이터를 저장하는 경우라면 2를 지정하는 것이 좋습니다.

    [mysqld]
    ...
    innodb_flush_log_at_trx_commit=2