파티션 제약 극복기! 유니크한 토큰 값을 만들어보자!

Overview

MySQL에는 날짜 별 데이터 관리를 위해 파티셔닝이라는 좋은 기능(?)을 5.1버전부터 무료(!)로 제공합니다. 일정 시간 지난 후 불필요한 데이터는 간단하게 해당 파티셔닝을 제거하면, 굳이 DELETE 쿼리로 인한 오버헤드를 방지할 수 있죠.

그러나, 파티셔닝 적용 시, “파티셔닝 키는 반드시 PK에 포함되어야 한다”, “추가 제약조건(유니크 속성)을 부여할 수 없다”라는 대표적인 제약 조건으로 인하여, 유니크 속성을 가지는 데이터를 파티셔닝 적용이 불가한 경우가 있는데.. 이것을 해결할 수 있는 간단한 트릭을 이 자리에서 설명하고자 합니다. ^^

Plan to..

토큰을 생성하는 경우를 간단하게 생각해볼까요? ^^

토큰, 특히 일정 시간 이후에는 절대로 활용이 되지 않는 Access Token을 생각해본다면.. 수많은 유저들이 어떤 권한을 위해 반드시 발급받아야하는 데이터죠. 기본적으로 이 값은 중복이 발생해서도 안되고, 일정 시간이 지나버리면 폐기해도 무관한 데이터입니다.

  • 반드시 유니크 속성을 보장해야한다.
  • 파티셔닝 관리가 가능해야 한다.

간단하게 스키마를 상상해보면, 아래와 같습니다. ^^

CREATE TABLE access_tokens (
  token         char(64) NOT NULL,
  expires_at    datetime NOT NULL,
  PRIMARY KEY (token)
);

자.. 이제 이 엄청난 데이터를 파티셔닝을 하고 싶은데.. 흠.. 가장 간단한 방법으로는 아래처럼 PK제약 조건을 충족하기 위해 PK 속에 파티셔닝 키를 포함시켜서 테이블을 파티셔닝 하는 방법이 있습니다.

CREATE TABLE access_tokens (
  token         char(64) NOT NULL,
  expires_at    datetime NOT NULL,
  PRIMARY KEY (token, expires_at)
)
PARTITION BY RANGE COLUMN (expires_at)
(PARTITION PF_20150513 VALUES LESS THAN ('2015-05-14') ,
 PARTITION PF_20150514 VALUES LESS THAN ('2015-05-15') ,
 PARTITION PF_20150515 VALUES LESS THAN ('2015-05-16'));

아.. 그런데 여기서 중요한 문제가.. 반드시 유니크성을 보장해야할 Token을 스키마 레벨에서 완벽하게 보장할 수 없다는 말이죠. 즉, 정말 유니크한지 확인을 하려면 발급할 Token값이 현재 테이블에 존재 여부를 사전에 조회해서 체크 해봐야 합니다. 물론, 랜덤한 Hash로 Token을 발급한다면.. 중복은 매우 희박하기는 하나.. 최악의 경우를 무시할 수는 없을테니요. ^^

How to?

이러한 상황에서 과연 어떤 트릭(?)을 써서 파티셔닝을 하면서 유니크한 Token 발급이 가능할까요? Token 특성 상 굉장히 많은 데이터가 적재될 것이기에.. 테이블 사이즈를 고려하여 파티셔닝을 위한 별도의 칼럼(daynum) 칼럼을 생각해봅시다. 이 값은 smallint 타입으로 2바이트로, PK에 8바이트짜리 datetime 타입이 들어가는 것 보다는 훨씬 유리하죠. 특히나 인덱스를 추가로 만들 경우!! Token에 트릭을 가미하기 위해 기존보다 4바이트가 늘어납니다. 즉, PK는 6바이트 증가!

아래와 같이 테이블을 만들어봅시다.

CREATE TABLE access_tokens (
  token         char(68) NOT NULL,
  daynum        smallint unsigned not null,
  expires_at    datetime NOT NULL,
  PRIMARY KEY (token, daynum)
)
PARTITION BY RANGE (daynum)
(PARTITION PF_20150513 VALUES LESS THAN (134),
 PARTITION PF_20150514 VALUES LESS THAN (135),
 PARTITION PF_20150515 VALUES LESS THAN (136));

daynum에는 무슨 값이 들어가냐고요? to_days(now())에서to_days(‘2014-12-31’)을 뺀 값이 들어갑니다. 이렇게 함으로써 단순 2바이트 사이즈로 몇 십년 치(아마도 늙어 죽을 때까지)를 관리할 수 있는 테이블 파티셔닝 관리가 가능한 것이죠.

자, 그럼.. 데이터 처리를 위한 쿼리를 만들어볼까요? Token은 SHA2로 만든다고 가정해봅시다.

## => shar2함수 안의 스트링 값과 날짜 값만 인수로..
insert into access_tokens 
  ( token, daynum, expires_at )
 values 
 (
   concat(SHA2(?, 256), right(hex(TO_DAYS(?)-735963),4)),
   TO_DAYS(?)-735963,
   ?
 );

setString(1, authInfo + nanoTime)
setString(2, expires_at)
setString(3, expires_at)

ex)
insert into access_tokens
 ( token, daynum, expires_at )
values
 (
   concat(SHA2('user1', 256), right(hex(TO_DAYS('2015-05-17')-735963),4)),
   TO_DAYS('2015-05-17')-735963,
   '2015-05-17'
 );

복잡하쥬? 간단하게 말하자면.. SHA2로 64바이트 키를 만들고, 가장 마지막 4글자를 날짜가 가미된 문자열을 넣자는 것입니다. (4글자를 맞추기 위해.. )

결국 PK는 아래와 같은 형태로 관리가 되기에, Token은 유니크 보장이 된다고 할 수 있습니다. ^^ (슈퍼 울트라 캡숑 “꼼수”)

  • Primary Key : (SHA2+날짜, 날짜)
  • “SHA2+날짜”는 유니크 속성 보장

데이터를 넣었으니, 데이터를 끄집어 내는 방법도 고민해봐야겠죠? 매번 조회 시 전체 파티셔닝을 뒤지지 않기 위해서는 원하는 데이터가 위치한 곳의 적절한 파티셔닝 키도 같이 전달을 해야할텐데..  그렇다고, 어플리케이션에서 늘 daynum을 가지고 있을 수는 없는 노릇! Token에서 날짜 정보를 추출할 수 있는 방안을 고안해야 합니다. 아래처럼..

## => 토큰 값만 쿼리에 인수로..
select * from access_tokens 
where daynum = conv(trim((substr(?, 65))), 16, 10) and token = ?;
setString(1, token_string)
setString(2, token_string)

ex)
select *
from access_tokens
where daynum = conv(trim((substr('0a041b9462caa4a31bac3567e0b6e6fd9100787db2ab433d96f6d178cabfce9089', 65))), 16, 10)
and token = '0a041b9462caa4a31bac3567e0b6e6fd9100787db2ab433d96f6d178cabfce9089';

Token에서 65번째 부터 4글자를 가져와서, 그 데이터를 16진수 -> 10진수로 변환하여 최종적으로 daynum을 만들어보자는 것입니다. 실제 호출되는 쿼리는 바로 위 예제처럼 사용되겠죠. ^^

쿼리가 복잡해지기는 하나.. 나름 서버에 부담없이 데이터를 최적으로 추출할 수 있는 방안이라고 봅니다.

기존 토큰 길이가, 64->68로 길어진다는 단점이 있기는 하나.. 음.. 반드시 64글자여야 한다면, SHA2결과 값에서 마지막 4글자를 빼버리는 것도 나쁘지 않다고 생각합니다. 발급된 Token은 스키마 레벨에서 유니크를 보장하고, 날짜별로 파티셔닝도 가능하니.. 일석이조!!

Conclusion

약간(?)의 트릭으로 파티셔닝 제약을 극복하였습니다. 유니크한 Token을 파티셔닝 관리하는.. 유니크 보장을 위해 사전 SELECT 없이도 쿼리 레벨에서 해결할 수 있는 방안이죠.

Token에 날짜가 가미된 스트링을 넣되, PK를 날짜와 같이 엮어서 Token만으로 유니크를 보장하자는 것이 목적입니다.

  • Primary Key : (SHA2+날짜, 날짜) => “SHA2+날짜”는 유니크

정말 간단한 팁같지 않은 팁이기는 하나.. 대규모 Token 관리를 계획하고 있으신 분에게는 꽤 좋은 솔루션이 될 수 있다고 생각해요. 설명장애가 있어서.. 매끄럽지 않았지만.. 의미 전달이 잘 되었기를..

다음에는 또다른 재미난 팁을 소개하도록 할께요. ^^

MariaDB의 FederatedX 엔진으로 데이터를 주물러보자.

Overview

FederatedX는 MariaDB에서 제공하는 확장된 기능의 Federated이며, 기본적으로 MariaDB에서는 별다른 옵션 없이 사용할 수 있습니다.

바로 이전 포스팅(http://j.mp/16VA8x6)에서는 이 FederatedX 엔진을 활용하여 대용량 테이블을 서비스에 큰 지장없이 이관을 했던 사례에 대해서 정리를 했었는데요. 이 경험을 바탕으로 서비스에서 조금 더 유용하게 활용할 수 있을 방안에 대해서 상상(?)을 해보았습니다.

즉, 지금부터는 FederatedX 엔진 관련된 테스트를 바탕으로 정리하는 내용이오니, 만약 실 서비스에 적용하고자 한다면 반드시 검증 후 진행하셔야 합니다. ^^

Why FederatedX?

단일 데이터베이스의 성능을 따지자면, 굉장한 퍼포먼스를 발휘하는 MySQL 이기는 합니다만.. SNS 서비스 환경에서는 결코 단일 서버로는 커버 불가능합니다. 이 상황에서는 반드시 샤딩(데이터를 분산 처리) 구조로 설계를 해야하는데, 한번 구성된 샤딩은 온라인 중에 변경하기가 참으로 어렵습니다.

서비스 순단을 발생할 수 있지만, 온라인으로 데이터를 재배치를 할 수 있는 방법은 없을까? 하는 생각을 하던 와중에, 저는 FederatedX 엔진에서 해답을 찾았습니다.

FederatedX-Shard-Reorg1

 

MariaDB에서 제공하는 FederatedX엔진은 MySQL에서 제공하는 기능 외에 트랜잭션 및 파티셔닝 기능이 확장되어 있는데, 이 두가지 특성을 활용한다면 앞선 그림처럼 기존 데이터 구조를 원하는 형태로 재배치할 수 있다는 생각이 번뜩 났습니다. ㅎㅎ

단, 아래에서 설명할 모든 상황들은 아래 4 가지 상황이어야한다는 가정이 필요합니다.

  1. 샤드 키 업데이트는 없어야 한다.
    업데이터 시 기존 샤드 데이터가 DELETE되지 않음
  2. ALTER 작업은 없어야 한다.
    FederatedX 스키마 변경 불가
  3. 샤드 키는 문자열 타입이어서는 안된다.
    문자열로는 Range 파티셔닝 사용 불가
  4. 원본 서버 Binary Log은 ROW 포멧이어야 한다.
    FederatedX 에서 일부 쿼리 미지원
    ex) 
    INSERT INTO .. ON DUPLICATE UPDATE

자, 각 상황에 맞게 이쁜(?) 그림으로 간단하게 살펴보도록 할까요?

1) 샤드 재배치

기존 샤드를 재배치하는 경우인데, 만약 일정 범위(1~100, 101~200)로 데이터가 분산 저장되어 있는 경우를 해시 기준으로 변경하고 싶을 때 유용하게 적용가능할 것이라고 생각되는데요.

FederatedX-Shard-Reorg2

위 상황에서 FederatedX1과 FederatedX2을 다음과 같은 스키마 구성합니다.

CREATE TABLE `tab` (
  `p_id` int(11) NOT NULL,
  `col01` bigint(20) NOT NULL,
  `col02` bigint(20) NOT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=FEDERATED
PARTITION BY LIST (mod(p_id,3))
(PARTITION p0 VALUES IN (0) ENGINE = FEDERATED connection='mysql://feduser:fedpass@Nshard1_host:3306/db/tab',
 PARTITION p1 VALUES IN (1) ENGINE = FEDERATED connection='mysql://feduser:fedpass@Nshard2_host:3306/db/tab',
 PARTITION p1 VALUES IN (1) ENGINE = FEDERATED connection='mysql://feduser:fedpass@Nshard3_host:3306/db/tab');

원본 샤드 데이터를 각 FederatedX에 Import 후 리플리케이션 구성하면, 파티셔닝 정의에 맞게 데이터샤드들이 저장되겠죠? ^^

단, 데이터 동기화에 가장 큰 요소는 바로.. Network Latency입니다. 동일 IDC, 다른 스위치 장비에 물려있는 DB 경우에는 약 초당 500 ~ 700 rows정도 데이터 변경이 이루어졌습니다. 물론, 동일 스위치 경우에는 이보다 훨씬 더 좋은 데이터 변경 처리가 이루어질 것이라고 예측됩니다만.. 흠.. 이것은 기회가 된다면, 테스트하고 싶네요. ^^;;

2) 로컬 데이터 재구성

아래와 같이 현재의 DB 혹은 테이블을 여러 개의 객체로 찢는 경우를 말할 수 있는데요, 앞서 치명적인 요소가 되었던 Network Latency는 이 상황에서는 별다른 문제가 되지 않습니다. 모든 통신이 메모리 상에서 이루어지기 때문에, 본연의 속도가 나올 수 있는 것이죠. ^^

FederatedX-Shard-Reorg3

FederatedX 전용 서버를 다음과 같은 설정(/etc/my_fed.cnf)으로 구동하도록 합니다. 포트는 13306으로 띄운다는 가정으로..

[mysqld_safe]
timezone = UTC
 
[mysqld]
port     = 13306
socket   = /tmp/mysql_fed.sock
character-set-server = utf8
skip-innodb
server-id = xxx
binlog_format = row
 
[client]
port    = 13306
socket    = /tmp/mysql_fed.sock
 
[mysql]
no-auto-rehash
default-character-set = utf8

FederatedX 전용 서버를 간단하게 아래와 같이 띄우면 되겠죠? ㅎ

/usr/local/mysql/bin/mysqld_safe --defaults-file=/etc/my_fed.cnf 

그후 아래와 같이 FederatedX 테이블을 생성 후 원본 테이블 데이터 Import 후 리플리케이션을 맺어주면, DB별로 데이터가 재배치된 샤드 형태가 완성됩니다. ^^

CREATE TABLE `tab` (
  `p_id` int(11) NOT NULL,
  `col01` bigint(20) NOT NULL,
  `col02` bigint(20) NOT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=FEDERATED
PARTITION BY LIST (mod(p_id,3))
(PARTITION p0 VALUES IN (0) ENGINE = FEDERATED connection='mysql://feduser:fedpass@127.0.0.1:3306/db1/tab',
 PARTITION p1 VALUES IN (1) ENGINE = FEDERATED connection='mysql://feduser:fedpass@127.0.0.1:3306/db2/tab',
 PARTITION p2 VALUES IN (2) ENGINE = FEDERATED connection='mysql://feduser:fedpass@127.0.0.1:3306/db3/tab');

테스트를 해보니 초당 수천 건 데이터 변경이 가능하며, 단일 서버 데이터 재배치에 가장 유용하게 활용될 수 있다고 생각되네요.

3) 데이터 통합

늘 서비스가 잘되면 참 좋겠지만.. 때로는 망해가는 서비스의 DB를 한곳으로 몰아야할 경우도 있습니다. ㅜㅜ  앞에서 설명한 것들을 이 상황에 맞게 조금만 수정을 하면 되니, 굳이 길게 설명은 하지 않도록 하겠습니다.

 

FederatedX-Shard-Merge1

단, 기존 샤드의 데이터가 샤드키에 맞게 엄격하게 분리가 되어 있어야 합니다. (리플리케이션에서 충돌날 수 있어요!!)

4) 아카이빙

수년을 보관해야하는 당장 정리가 불가한 콜드데이터 성격의 데이터를 한 곳에 모은다는 컨셉인데요.. 이에 대한 것은 사전에 안정성 테스트 반드시 필요합니다. (어디까지나 테스트를 바탕으로 활용 가능한 상황인지라.. ^^;;)

FederatedX-Shard-Archieving

하루에 한번씩 돌아가면서 리플리케이션 IO_THREAD를 ON/OFF를 하게되면, 다수의 데이터베이스로부터 오는 데이터를 쉽게 한 곳으로 모을 수 있겠죠. 만약, 원본 테이블에서 파티셔닝 관리를 한다면, 이에 대한 에러 스킵 설정을 FederatedX에 미리 정의해놓으면 참 좋겠네요. ^^

Conclusion

처음에 버리다시피 전혀 검토를 하지 않았던 FederatedX를 다른 시각으로 발상을 전환해보니, 매우 활용할 수 있는 분야가 많았습니다. 물론 아직 실 서비스에서 직접 해보지는 않았지만, 이에 대한 각 테스트를 해보니 충분히 활용해볼 만한 여지는 있었습니다.

참고로, 위에서 설명하지는 않았지만, FederatedX +Blackhole을 활용하게 된다면, Network Latency 극복을 어느정도 할 수 있다고 생각합니다.

부족한 설명을 읽어주셔셔 감사합니다. ^^

MariaDB의 FederatedX 엔진을 활용한 9억 데이터 이관기

Overview

대용량 로그 테이블은 때로는 서비스에 지대한 영향을 미치기도 합니다. 게다가 이 테이블을 파티셔닝 구성을 해야하는데, 이를 서비스 운영 중인 상태에서 마스터 장비에서 Import하는 것은 사실 대단히 위험한 시도이기도 하죠.

이런 상황에서 얼마 전 FederatedX엔진을 활용하여 9억 데이터를 이관한 사례가 있는데, 이에 대해 공유하도록 하겠습니다. ^^

Goal

9억 건의 데이터를 Import하는 동안 서비스에는 어떠한 영향도 없어야 하며, 구성 후 어플리케이션 적용 전까지 데이터가 정상적으로 동기화되어야 합니다.

  1. 데이터 이동하는 동안 기존 서비스 영향 최소화 및 문제 발생 시 빠른 원복
  2. 데이터 구성 후 어플리케이션 코드 배포 전까지 데이터 동기화
  3. 데이터 보관 주기 정책에 따른 유연한 대처
    현재는 삭제 주기가 없으나, 추후 정책에 따라 변경 가능

Let me SEE..

가야할 골이 정해졌으니.. 현재 상황에 대해서 분석을 해봐야겠죠. ㅎㅎ 다음은 DB 사용 현황에 대한 내용입니다.

1) 소스 서버

  • Engine : InnoDB
  • QPS(Except SELECT) : 200qps, (MAX)500qps
  • 대상 테이블
    • download_log – 76G / 3.8억
    • open_log – 96G / 5.5억

2) 타겟 서버

  • Engine : TokuDB
  • QPS(Except SELECT) : 150qps, (MAX)300qps

서버 트래픽이 크지는 않지만, 9억 건 이상의 데이터를 타 서비스 DB로 마이그레이션을 해야하는 상황이었습니다. 그렇기에 서비스에 어떠한 영향도 없어야하며, 문제 발생 시에도 빠르게 롤백할 수 있는 방법이 되어야만 합니다.

How to Migrate?

이런 상황에서라면, 타겟 마스터 장비를 소스 마스터 혹은 슬레이브 장비와 리플리케이션을 걸어서 특정 테이블만 데이터 싱크를 맞추는 방법이 있겠습니다.

그러나, 9억 건 이상의 데이터이고, 서비스 영향없이 마스터 장비로 데이터를 넣어야 하기 때문에.. 목적에 적합한 방법은 아닌 듯 하네요. 슬레이브를 활용하고자 해도, 멀티 소스 리플리케이션을 활용할 수 있는 상황도 아니었으며, 기존 서비스 리플리케이션을 건들여야하기 때문에 깔끔해 보이지는 않았고요.

그래서 아이디어를 낸 방법이, 슬레이브 장비에 FederatedX 스토리지 엔진 전용의 MySQL 데몬을 하나 더 띄워서 데이터 이관을 하는 것이었습니다. 서비스 투입 직전 데이터 흐름을 간단하게 그림으로 표현해 보았습니다.

Migrate-With-FerderatedX-1

그렇다면, FederatedX? 뭐냐고요?

바로 이전 포스팅에서 이럴줄 알고 슬쩍 정리를 해봤습니다. ^^ 아래 링크를 쿡~!!
>> https://gywn.net/2014/12/let-me-introduce-federatedx/

자~! 그렇다면.. 데이터 이관에 대해 차근차근 단계적으로 설명하도록 하겠습니다.

  1. Data Dump Backup
  2. Table Creation
  3. Data Import & Sync
  4. Master/Slave Switching
  5. Slave Restore

1) Data Dump Backup

첫 번째 단계입니다. 옮길 대용량 테이블은 두 개이고, 조금이라도 빠르게 데이터를 이관하기 위해서 각 테이블 별로 덤프 파일을 생성합니다. 두 개의 덤프 파일이 모두 동일한 바이너리 로그 포지션을 가져야하기 때문에, UserDB 쪽 슬레이브의 SQL_Thread를 일시적으로 중지 후 포지션을 기록합니다.

>> UserDB Slave (상단 이미지 왼쪽 아래 서버)

슬레이브 시작 시 영향도를 최소화하기 위해, 슬레이브의 SQL Thread만 중지합니다. 그리고 현재 바이너리 로그 포지션을 기록해놓습니다.

mysql> stop slave sql_thread;
Query OK, 0 rows affected(0.00 sec)
 
mysql> show master status;
+------------------+----------+
| File             | Position |
+------------------+----------+
| mysql-bin.006027 | 18752723 |
+------------------+----------+

>> ContentsDB Slave (상단 이미지 오른쪽 아래 서버)

UserDB 슬레이브 장비에 3306으로 붙어서 직접 데이터를 받아옵니다. 사실 UserDB에서 로컬로 데이터를 내릴 수 있겠지만, 파일로 전송하는 단계를 생략하기 위함입니다.

$ mysqldump -udumpuser -pdumppass \
  --single-transaction --no-create-info \
  --add-locks=false -h user-slave \
  --databases user_db --tables open_log \
  > /backup/open_log.sql &

$ mysqldump -udumpuser -pdumppass \
  --single-transaction --no-create-info \
  --add-locks=false -h user-slave \
  --databases user_db --tables download_log \
  > /backup/download_log.sql &

아참, 덤프하기 전에 dumpuser를 사전에 생성을 해놔야한다는 사실을 잊으면 안되겠죠. ^^

>> UserDB Slave (상단 이미지 왼쪽 아래 서버)

자~ 이제 덤프가 시작되었으니.. UserDB 쪽 슬레이브를 재계합니다.

mysql> start slave sql_thread;
Query OK, 0 rows affected(0.00 sec)

2) Table Creation

FederatedX는 원격의 테이블을 연결시켜주는 실체가 없는 브릿지 역할을 합니다. 즉, “원격 테이블”“형상 테이블” 모두 생성을 해줘야하는 것이죠.테이블 스키마 생성 작업은 모~두 서비스와는 전혀 연관이 없는 ContentsDB 슬레이브(상단 이미지 오른쪽 하단)에서 이루어집니다.

>> 원격 테이블 – 3306포트

먼저, 원격 테이블을 생성합니다. 추후 효과적인 데이터 관리를 위해 파티셔닝 설정도 이 기회에 같이 합니다. ㅎㅎ

CREATE TABLE download_log (
  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  user_id int(11) unsigned NOT NULL,
  pid bigint(20) unsigned NOT NULL DEFAULT '0',
  sid bigint(20) NOT NULL DEFAULT '0',
  download_dt datetime NOT NULL,
  val1 varchar(45) NOT NULL DEFAULT '',
  val2 varchar(45) NOT NULL DEFAULT '',
  PRIMARY KEY (id, download_dt),
  KEY ix_userid (user_id, download_dt)
) ENGINE=TokuDB AUTO_INCREMENT=400000000
/*!50500 PARTITION BY RANGE COLUMNS(download_dt)
(PARTITION PF_201306 VALUES LESS THAN ('2013-07-01'),
 PARTITION PF_201312 VALUES LESS THAN ('2014-01-01'),
 PARTITION PF_201406 VALUES LESS THAN ('2014-07-01'),
 PARTITION PF_201412 VALUES LESS THAN ('2015-01-01')) */;

CREATE TABLE open_log (
  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  user_id int(11) unsigned NOT NULL,
  pid bigint(20) unsigned NOT NULL,
  sid bigint(20) NOT NULL DEFAULT '0',
  product_type char(4) NOT NULL DEFAULT '',
  open_dt datetime NOT NULL,
  PRIMARY KEY (id, open_dt),
  KEY ix_userid_productid (user_id, open_dt)
) ENGINE=TokuDB AUTO_INCREMENT=600000000
/*!50500 PARTITION BY RANGE COLUMNS(open_dt)
(PARTITION PF_201306 VALUES LESS THAN ('2013-07-01'),
 PARTITION PF_201312 VALUES LESS THAN ('2014-01-01'),
 PARTITION PF_201406 VALUES LESS THAN ('2014-07-01'),
 PARTITION PF_201412 VALUES LESS THAN ('2015-01-01')) */;

>> 형상 테이블(FederatedX) – 13306포트

UserDB로부터 받아온 데이터를 ContentsDB로 전달하는 FederatedX 테이블 스키마입니다. 원본 서버와는 완벽하게 같을 필요는 없으나, 만약 바이너리 로그가 SQL기반으로 기록된다면, 관련 인덱스를 어느정도 맞춰놓는 것이 좋습니다. 여기서는 생략~!

CREATE TABLE download_log (
  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  user_id int(11) unsigned NOT NULL,
  pid bigint(20) unsigned NOT NULL DEFAULT '0',
  sid bigint(20) NOT NULL DEFAULT '0',
  download_dt datetime NOT NULL,
  val1 varchar(45) NOT NULL DEFAULT '',
  val2 varchar(45) NOT NULL DEFAULT '',
  PRIMARY KEY (id)
) ENGINE = FEDERATED connection='mysql://feduser:fedpass@127.0.0.1:3306/contents_db/download_log';
 
CREATE TABLE open_log (
  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  user_id int(11) unsigned NOT NULL,
  pid bigint(20) unsigned NOT NULL,
  sid bigint(20) NOT NULL DEFAULT '0',
  product_type char(4) NOT NULL DEFAULT '',
  open_dt datetime NOT NULL,
  PRIMARY KEY (id)
) ENGINE = FEDERATED connection='mysql://feduser:fedpass@127.0.0.1:3306/contents_db/open_log';

아참~! 3306 포트로 올라와있는 서버에는 feduser/fedpass로 생성된 유저가 있어야 합니다. 슬레이브가 READ_ONLY일테니 SUPER권한과 함께.. (슬레이브 READ_ONLY를 풀 수도 있겠지만.. 이보다는 전용 DB 접속 유저에 권한을 주는 것이 더 좋을 듯 하네요. ^^)

지금까지 단계가 완료되면, ContentsDB에는 다음과 같이 서버가 구성이 되어 있겠네요.

Migrate-With-FerderatedX-2

3) Data Import & Sync

옮겨야할 데이터도 있고 옮길 그릇도 있으니, 이제 실제 데이터 이관 작업 후 데이터 동기화를 하는 작업만 남았겠네요. ^^

mysql -ufeduser -pfedpass contents_db < /backup/open_log.sql &
mysql -ufeduser -pfedpass contents_db < /backup/download_log.sql &

시간이 꽤 걸리는 이 작업이 완료가 되면, 이제 FederatedX가 떠있는 DB에 접속하여 슬레이브 구성을 합니다.

## replicate_do_table 설정
mysql> set global replicate_do_table = 'user_db.download_log,user_db.open_log ';
 
## 슬레이브 설정
mysql> CHANGE MASTER TO
MASTER_HOST='use-slave',
MASTER_PORT= 3306,
MASTER_USER='repl',
MASTER_PASSWORD='xxxxxx',
MASTER_LOG_FILE='mysql-bin.006027',
MASTER_LOG_POS=18752723;
 
## 슬레이브 시작
start slave;

이 과정까지 되면, 제일 처음 표현했던 이미지대로 데이터가 흐르게 됩니다.

4) Master/Slave Switching

가장 트래픽이 적은 시점에 ContentsDB 쪽 마스터/슬레이브 장비를 스위칭합니다.

MHA같은 솔루션을 사용하고 있다면, 더욱 쉽게 마스터/슬레이브 스위칭이 이루어질 것이고, 스위칭 직후에는 아래와 같은 형태로 데이터가 흐르게 되겠죠. 아참, 스위칭 전에 반드시 다음 단계에서 진행할 슬레이브 복구를 위해, 백업을 해주는 것이 좋겠네요.

Migrate-With-FerderatedX-3

당연한 이야기겠지만, 로그 테이블들이 신규 슬레이브(구 마스터)에는 없기 때문에 ContentsDB 쪽 마스터/슬레이브는 끊어지겠죠.

5) Slave Restore

스위칭 직전 백업 데이터로, 끊어진 슬레이브를 연결합니다. 그리고, 어플리케이션에서 UserDB에 위치한 로그 테이블을 더이상 참조하지 않는 시점에 로그 테이블을 제거하도록 합니다. 이 시점에서는 더이상 FederatedX 테이블도 필요 없기 때문에, 13306포트로 구동 중인 DB서버를 셧다운하도록 합니다.

최종적으로는 대형 로그 테이블이 타 DB로 이관이된 아래와 같은 모습이 됩니다. (휴~! 끝)

Migrate-With-FerderatedX-4

 

마스터/슬레이브 스위칭 시 순단 현상은 있었겠지만, 적어도 9억 데이터를 이관하는 동안 어떠한 영향도 없었습니다.

Result

InnoDB를 TokuDB로 이관을 하면서 디스크 사용률이 30% 이하로 줄었습니다. TokuDB에 대한 설명은 굳이 여기에서 하지 않아도 될 것 같네요. ^^ 만약 궁금하시다면, 아래 포스팅 내용을 참고하세요.
https://gywn.net/2014/05/fractal-index-in-tokudb/

이관 후 데이터 사이즈는 다음과 같습니다.

  • download_log
    – 76G -> 21G
  • open_log
    – 96G -> 23G

파티셔닝 구조로 변경하면서, 데이터 보관 주기에 정책에 따라 유연하게 데이터를 유지할 수 있게 되었습니다.

참고로, FederatedX에서 슬레이브를 연결한 직후 동기화 되는 최대 속도는 다음과 같습니다. TokuDB Small 포멧임에도 단일 쓰레드로 3000 query 이상을 발휘합니다.

| Com_insert     | 3268    |
| Com_insert     | 3255    |
| Com_insert     | 3223    |
| Com_insert     | 3200    |
| Com_insert     | 3233    |

Conclusion

지금까지 FederatedX를 사용하여 9억건 데이터를 타 DB로 이관한 사례를 정리하였습니다.

물론, 굳이 이 방법이 아니라고 하더라도 대형 테이블을 타 DB로 이관할 수 있는 방법은 있겠죠. Tungsten Replicator와 같은 솔루션을 활용하거나, 혹은 개발 부서의 적극적인 지원을 받거나..^^

잘 활용되지 않는 FederatedX 엔진이라고 할 지라도, 이러한 용도로 활용을 한다면 꽤 난감한 상황(이를테면 DB 혹은 테이블 명이 변경되는)에서도 유연하게 대처할 수 있겠습니다. 때마침 좋은 사례가 있어서 공유 드립니다.

간만의 포스팅이라 내용이 매끄럽지 않네요. 게다가 하나하나 모두 설명하기에는 무리가 있어서.. 많은 부분을 생략하기도 했고요. ㅠㅠ 기회가 된다면, 이에 대해 조금 더 자세하게 정리할 수 있는 자리가 있으면 좋겠습니다.ㅎㅎ