2014년 2월 4일 화요일

Triggers - INSERT ON DUPLICATE KEY UPDATE & REPLACE INTO

Overview

MySQL에서 트리거를 사용하는 경우가 있습니다. 단순 INSERT/UPDATE/DELETE인 경우에는 큰 문제가 없겠지만, "INSERT .. ON DUPLICATE KEY" 또는 "REPLACE INTO"와 같은 특별한 SQL 경우 예상과는 트리거가 다른 동작을 보일 수 가 있습니다. 위 두 쿼리에 따른 트리거 동작에 대해 정리하도록 하겠습니다.

Trigger?

트리거는 데이터 변경 쿼리(INSERT/UPDATE/DELETE)가 들어온 경우 이후에 발동되는 이벤트를 의미하며, MySQL에서는 아직까지는 행 기반의 트리거만 동작합니다. 오라클 경우에는 특정 칼럼 변경도 감지하여 트리거를 발동시킬 수는 있습니다만.. ^^

일반적으로 SQL이 실행되기 전에 발동되는 "BEFORE"와 SQL 실행 후 발동되는 "AFTER" 트리거가 있습니다. 하단에서는 "INSERT .. ON DUPLICATE KEY" 또는 "REPLACE INTO" 호출 시 각 트리거에 따른 이벤트 발동 순서를 살펴보았습니다.

After Trigger

테스트를 위해 사용한 테이블 스키마와 트리거 생성 구문입니다. test 테이블에 After 트리거를 생성하고 "INSERT ON DUPLICATE KEY UPDATE"과 "REPLACE INTO"를 각각 두 번 수행한 이후 test_log에 기록된 사항을 확인해보겠습니다.

-------------------------------------------------------------
## Table Schema
drop table test;
CREATE TABLE `test` (
  `i` int(11) NOT NULL,
  `j` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`i`)
);

drop table test_log;
CREATE TABLE `test_log` (
  `d` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `t` varchar(10) DEFAULT NULL,
  `old` int null,
  `new` int null
);

## Triggers 
delimiter //
create trigger trg_test_after_insert
after insert on test
for each row
begin
  insert into test_log values (now(), 'INSERT', null, new.i);
end;
//
delimiter ;

delimiter //
create trigger trg_test_after_update
after update on test
for each row
begin
  insert into test_log values (now(), 'UPDATE', old.i, new.i);
end;
//
delimiter ;

delimiter //
create trigger trg_test_after_delete
after delete on test
for each row
begin
  insert into test_log values (now(), 'DELETE', old.i, null);
end;
//
delimiter ;


1) INSERT ON DUPLICATE KEY UPDATE - first time

중복키가 없으므로, 단순 Insert와 동일한 패턴을 보입니다.
-------------------------------------------------------------
kadba@vtom043:test 11:03:38>INSERT INTO test (i,j) VALUES(1,1) ON DUPLICATE KEY UPDATE j=j+1;
Query OK, 1 row affected (0.01 sec)
kadba@vtom043:test 11:03:47>select * from test;
+---+---+
| i | j |
+---+---+
| 1 | 1 |
+---+---+
1 row in set (0.00 sec)
kadba@vtom043:test 11:03:52>select * from test_log;
+---------------------+--------+------+------+
| d                   | t      | old  | new  |
+---------------------+--------+------+------+
| 2014-02-04 02:03:47 | INSERT | NULL |    1 |<==
+---------------------+--------+------+------+
1 row in set (0.00 sec)


2) INSERT ON DUPLICATE KEY UPDATE - second time
INSERT관련된 내용없이 업데이트 트리거만 발동되었습니다. 원하는 형태로 트리거가 동작합니다.
-------------------------------------------------------------
kadba@vtom043:test 11:04:00>INSERT INTO test (i,j) VALUES(1,1) ON DUPLICATE KEY UPDATE j=j+1;
Query OK, 2 rows affected (0.00 sec)
kadba@vtom043:test 11:04:57>select * from test;
+---+---+
| i | j |
+---+---+
| 1 | 2 |
+---+---+
1 row in set (0.00 sec)
kadba@vtom043:test 11:05:16>select * from test_log;
+---------------------+--------+------+------+
| d                   | t      | old  | new  |
+---------------------+--------+------+------+
| 2014-02-04 02:03:47 | INSERT | NULL |    1 |
| 2014-02-04 02:04:57 | UPDATE |    1 |    1 |<==
+---------------------+--------+------+------+
2 rows in set (0.00 sec)


3) REPLACE INTO - first time
이번에는 "REPLACE INTO" 관련 테스트입니다. 위와 마찬가지로 처음에는 충돌나는 키가 없으므로, Innsert와 동일한 패턴을 보입니다.
-------------------------------------------------------------
kadba@vtom043:test 11:07:08>REPLACE INTO test (i,j) VALUES(2,1);
Query OK, 1 row affected (0.00 sec)
kadba@vtom043:test 11:07:15>select * from test;
+---+---+
| i | j |
+---+---+
| 1 | 2 |
| 2 | 1 |
+---+---+
2 rows in set (0.00 sec)
kadba@vtom043:test 11:07:23>select * from test_log;
+---------------------+--------+------+------+
| d                   | t      | old  | new  |
+---------------------+--------+------+------+
| 2014-02-04 02:03:47 | INSERT | NULL |    1 |
| 2014-02-04 02:04:57 | UPDATE |    1 |    1 |
| 2014-02-04 02:07:15 | INSERT | NULL |    2 |<==
+---------------------+--------+------+------+
3 rows in set (0.00 sec)

4) REPLACE INTO - second time
"REPLACE INTO"는 DELETE->INSERT 하는 방식으로 동작합니다. 트리거 기록에서도 마찬가지로 DELETE->INSERT하는 흔적을 보여줍니다.
-------------------------------------------------------------
kadba@vtom043:test 11:07:27>REPLACE INTO test (i,j) VALUES(2,2);
Query OK, 2 rows affected (0.01 sec)
kadba@vtom043:test 11:08:07>select * from test;
+---+---+
| i | j |
+---+---+
| 1 | 2 |
| 2 | 2 |
+---+---+
2 rows in set (0.00 sec)
kadba@vtom043:test 11:08:10>select * from test_log;
+---------------------+--------+------+------+
| d                   | t      | old  | new  |
+---------------------+--------+------+------+
| 2014-02-04 02:03:47 | INSERT | NULL |    1 |
| 2014-02-04 02:04:57 | UPDATE |    1 |    1 |
| 2014-02-04 02:07:15 | INSERT | NULL |    2 |
| 2014-02-04 02:08:07 | DELETE |    2 | NULL |<==
| 2014-02-04 02:08:07 | INSERT | NULL |    2 |<==
+---------------------+--------+------+------+
5 rows in set (0.00 sec)

Before Trigger

AFTER 트리거에서는 큰 무리없이 원하는 형태로 트리거가 잘 동작하였습니다. 그런데, BEFORE 트리거에서는 위와는 다르게 동작하게 됩니다. 테이블과 트리거를 아래와 같이 다시 생성합니다.
-------------------------------------------------------------
## Table Schema
drop table test;
CREATE TABLE `test` (
  `i` int(11) NOT NULL,
  `j` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`i`)
);

drop table test_log;
CREATE TABLE `test_log` (
  `d` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `t` varchar(10) DEFAULT NULL,
  `old` int null,
  `new` int null
);

## Triggers 
delimiter //
create trigger trg_test_before_insert
before insert on test
for each row
begin
  insert into test_log values (now(), 'INSERT', null, new.i);
end;
//
delimiter ;

delimiter //
create trigger trg_test_before_update
before update on test
for each row
begin
  insert into test_log values (now(), 'UPDATE', old.i, new.i);
end;
//
delimiter ;

delimiter //
create trigger trg_test_before_delete
before delete on test
for each row
begin
  insert into test_log values (now(), 'DELETE', old.i, null);
end;
//
delimiter ;


1) INSERT ON DUPLICATE KEY UPDATE - first time
AFTER 트리거와 마찬가지로 중복된 키가 없으므로, 무리없이 INSERT트리거가 발동됩니다.
-------------------------------------------------------------
kadba@vtom043:test 11:12:18>INSERT INTO test (i,j) VALUES(1,1) ON DUPLICATE KEY UPDATE j=j+1;
Query OK, 1 row affected (0.00 sec)
kadba@vtom043:test 11:12:19>select * from test;
+---+---+
| i | j |
+---+---+
| 1 | 1 |
+---+---+
1 row in set (0.00 sec)
kadba@vtom043:test 11:12:24>select * from test_log;
+---------------------+--------+------+------+
| d                   | t      | old  | new  |
+---------------------+--------+------+------+
| 2014-02-04 02:12:19 | INSERT | NULL |    1 |<==
+---------------------+--------+------+------+
1 row in set (0.00 sec)

2) INSERT ON DUPLICATE KEY UPDATE - second time
문제는 두번째 "DEPLICATE KEY UPDATE"가 발동되는 순간입니다. 이 경우 실제 트리거는 INSERT->UPDATE하는 방식으로 동작합니다.키가 충돌나는 지는 쿼리를 실행하기 전에는 알 수 없기 때문에,INSERT를 시도한 후 중복을 감지하여 UPDATE하는 순서 그대로 트리거가 호출되는 것입니다.
-------------------------------------------------------------
AFTER 트리거와는 다르게 INSERT 이후 UPDATE를 체크합니다.
kadba@vtom043:test 11:12:29>INSERT INTO test (i,j) VALUES(1,1) ON DUPLICATE KEY UPDATE j=j+1;
Query OK, 2 rows affected (0.00 sec)
kadba@vtom043:test 11:12:45>select * from test;
+---+---+
| i | j |
+---+---+
| 1 | 2 |
+---+---+
1 row in set (0.00 sec)
kadba@vtom043:test 11:12:53>select * from test_log;
+---------------------+--------+------+------+
| d                   | t      | old  | new  |
+---------------------+--------+------+------+
| 2014-02-04 02:12:19 | INSERT | NULL |    1 |
| 2014-02-04 02:12:45 | INSERT | NULL |    1 |<==
| 2014-02-04 02:12:45 | UPDATE |    1 |    1 |<==
+---------------------+--------+------+------+
3 rows in set (0.00 sec)

이 경우 복제 테이블을 구성하는 경우 "ON DUPLICATE KEY UPDATE"가 복제 대상에 걸린 PK제약으로 인하여 정상적으로 동작하지 않습니다. 즉! INSERT 실패가 발생하겠죠.

3) REPLACE INTO - first time
REPLACE INTO 또한 중복된 키가 없으므로, 무리없이 데이터가 INSERT됩니다.
-------------------------------------------------------------
kadba@vtom043:test 11:12:55>REPLACE INTO test (i,j) VALUES(2,1);
Query OK, 1 row affected (0.01 sec)
kadba@vtom043:test 11:13:36>select * from test;
+---+---+
| i | j |
+---+---+
| 1 | 2 |
| 2 | 1 |
+---+---+
2 rows in set (0.00 sec)
kadba@vtom043:test 11:13:43>select * from test_log;
+---------------------+--------+------+------+
| d                   | t      | old  | new  |
+---------------------+--------+------+------+
| 2014-02-04 02:12:19 | INSERT | NULL |    1 |
| 2014-02-04 02:12:45 | INSERT | NULL |    1 |
| 2014-02-04 02:12:45 | UPDATE |    1 |    1 |
| 2014-02-04 02:13:36 | INSERT | NULL |    2 |<==
+---------------------+--------+------+------+
4 rows in set (0.00 sec)

4) REPLACE INTO - second time
REPLACE INTO 또한 중복된 키가 존재하는 경우 문제가 있습니다.마찬가지로 REPLACE INTO 구문이 실행되기 전에는 키가 중복되는 지 알 수 없기 때문에, 먼저 INSERT를 한 이후 DELETE를 하는 방식으로 동작합니다.
-------------------------------------------------------------
INSERT 트리거가 실행된 이후 DELETE가 실행됩니다.
AFTER 트리거와는 반대의 패턴을 보입니다.
kadba@vtom043:test 11:13:45>REPLACE INTO test (i,j) VALUES(2,2);
Query OK, 2 rows affected (0.00 sec)
kadba@vtom043:test 11:14:06>select * from test;
+---+---+
| i | j |
+---+---+
| 1 | 2 |
| 2 | 2 |
+---+---+
2 rows in set (0.00 sec)
kadba@vtom043:test 11:14:10>select * from test_log;
+---------------------+--------+------+------+
| d                   | t      | old  | new  |
+---------------------+--------+------+------+
| 2014-02-04 02:12:19 | INSERT | NULL |    1 |
| 2014-02-04 02:12:45 | INSERT | NULL |    1 |
| 2014-02-04 02:12:45 | UPDATE |    1 |    1 |
| 2014-02-04 02:13:36 | INSERT | NULL |    2 |
| 2014-02-04 02:14:06 | INSERT | NULL |    2 |<==
| 2014-02-04 02:14:06 | DELETE |    2 | NULL |<==
+---------------------+--------+------+------+
6 rows in set (0.00 sec)

위 결과대로라면, REPLACE INTO 구문을 사용하는 경우에서도 복제 테이블 구성 시 복제 테이블에 걸린 PK제약으로 인하여 INSERT 실패가 날 수 있습니다. 설혹, 실패가 나지 않는다고 하더라도, DELETE 이후 아무런 처리를 하지 않기 때문에, 결과적으로 오히려 복제 테이블에서 데이터를 지워버리는 어이없는 결과가 나오게 됩니다.

Conclusion

AFTER 트리거는 원하는 형태대로 트리거가 제대로 발동되었으나, BEFORE 트리거 경우에는 예상된 순서로 트리거가 발동되지 않습니다. 즉, BEFORE 트리거로 데이터를 처리하는 경우, 게다가 REPLACE 혹은 INSERT ON DUPLICATE UPDATE를 사용하는 경우 반드시 주의가 필요합니다.
pt-online-schema-change에서 복제 테이블 구성시 트리거를 사용하는데, 다행히도 AFTER 트리거 방식으로 동작하기 때문에, 결과적으로 스키마 변경에는 아무 문제는 없습니다. ^^


2014년 1월 22일 수요일

TokuDB Introduction

TokuDB는 높은 확장성과 zero-maintenance downtime 을 지향하는 MySQL Storage engine으로, 인덱스 기반의 쿼리 속도 향상, 복제 성능 향상, 병렬화되지 않은 압축 및 온라인 스키마 변경이 가능하다.

TokuDB는 drop-in stroage engine의 MySQL로 개발한 애플리케이션을 그대로 사용할 수 있으며,  ACID와 MVCC를 완벽하게 지원한다.
TokuDB는 2013년 초 Open source로 개방하였고, 2014년 1월 현재 TokuDB 7.1.0 Community for MySQL 5.5.30, TokuDB 7.1.0 for Community Edition MariaDB 5.5.30 버전을 제공하고 있다.
한편 Tokutek에서 MongoDB with Fractal Tree Indexes v0.0.2도 발표하여 Fractal Tree Index를 활용할 수 있는 영역을 넓히고 있어 주목된다.
그러나, 아직은 QPS가 높은 OLTP상에서는 여러 안정성에서 문제가 발생하고 있기 때문에, 데이터 size가 크고 QPS가 높지  않은 성격의 DB (Log DB, 분석 DB 등등)라면 권장 해 볼만 하며, 실제로 카카오에서는 이러면 성격의 DB에 도입하여 활용 중에 있다.

Toku DB 사용의 appeal point는 다음과 같다.
장점 : 
  • index scan의 경우 innodb 대비 read속도가 상당히 빠르다.
  • insert speed InnoDB 대비 10x (read가 많지 않은 경우)
  • 압축 size InnoDB 대비 : 1/10~1/20

단점 : 
  • full scan의 경우는 압축을 풀어야 하므로, InnoDB대비 3x이상 느리다.
  • 압축을 사용할 경우 innoDB보다 user CPU사용율이 약간 높다.
  • XtraBackup 등의 innoDB open source backup tool을 사용할 수 없다.

Fast insertions and deletions

  • TokuDB에서는 Fractal Tree기술로 빠른 인덱싱이 가능하다. B-tree의 경우 느린 인덱싱으로 인해 암묵적으로 인덱스 개수를 제한하고 있다는 점에서 주목할만하다. 인덱싱이 빠르다는 것은 인덱스를 많이 만들 수 있다는 것을 의미하고 그만큼 쿼리에서 활용할 수 있는 인덱스의 수가 늘어난다는 것을 의미한다. Fractal Tree는 B-tree의 인덱스 최적 포인트인(indexing sweet spot)인 sequential data에 대비해서, 높은 카디널리티를 가진 랜덤 데이터에서도 2배 이상 빠르다.
  • fast indexing means fast queries. -Tokuteck-

Hot index creation

  • TokuDB에서는 온라인으로 인덱스를 생성할 수 있다. MySQL 5.5 이하 및 MariaDB 5.5 이하에서 지원하지 않음
  • tokudb_create_index_online 세션 변수가 ON (Default) 일 경우 온라인으로 인덱스 생성을 할 수 있다.
  • 온라인 인덱스 생성은 CREATE INDEX 구문으로만 가능하다. ALTER TABLE 구문으로 인덱스를 생성하면 tokudb create index online 세션 변수가 on 으로 설정되어 있다 하더라도 인덱스는 오프라인(해당 테이블은 오퍼레이션이 불가, Locking)으로 생성된다.
  • 인덱스 생성 진행 상황은 다른 클라이언트에서 SHOW PROCESSLIST 명령어에서 확인할 수 있다. (CTAS, LOAD, INSERT...SELECT 등 포함)
show processlist;
+...+---------+------+------------------------------------+----------------------------------------------------+----------+
|...| Command | Time | State                              | Info                                               | Progress |
+...+---------+------+------------------------------------+----------------------------------------------------+----------+
|...| Query   | 1312 | Adding of indexes about 79.5% done | CREATE INDEX ix1_toku_default ON toku_default (c2) | 79.000   |
|...| Sleep   | 30   |                                    | NULL                                               | 0.000    |
|...| Query   | 0    | NULL                               | show processlist                                   | 0.000    |
+...+---------+------+------------------------------------+----------------------------------------------------+----------+

Hot column addition, expansion and rename(HCADER)

  • TokuDB에서는 일부 데이터 타입에 대해 Locking 없이 온라인으로 컬럼 추가, 확인 및 이름 변경을 할 수 있다. MySQL 5.5 이하 및 MariaDB 5.5 이하에서 지원하지 않음
  • 지원 가능한 데이터 타입은 CHAR, VARCHAR, VARBINARY, INTEGER 이다. TIME, ENUM, BLOB, TINYBLOB, MEDIUMBLOB, LONGBLOB 타입은 지원하지 않는다.
  • HCADER를 실행하면 meta 정보를 갱신하기 위해 아주 짧은 시간 동안 테이블 락을 잡고, 락이 풀린 후에는 row가 디스크에서 메모리로 올라올 때 각 row를 수정한다 - 이를 후행 작업이라고 정의하자 -. HCADER은 PK 및 secondary index를 포함한 Fractal Tree의 일부를 건드리는 후행 작업으로 수행된다. 추가된 컬럼, 삭제된 컬럼 및 확장된 컬럼의 각각의 후행 작업을 OPTIMIZE TABLE 구문으로 수동으로 한 번에 실행할 수 있다. OPTIMIZE TABLE 구문은 온라인으로 실행이 가능하다. 즉, OPTIMIZE TABLE 명령어가 실행되는 동안 아무런 블럭킹 없이 쿼리를 실행할 수 있다. 이는 TokuDB v7.0.1 에서 중요한 부분으로 소개되고 있다. 또한 온라인으로 OPTIMIZE TABLE을 수행하더라도 TokuDB 인덱스가 age되지 않은 한 인덱스를 리빌드하지 않는다. 즉, HCADER의 모든 플러시 작업은 백그라운드로 실행된다.
  • 온라인 컬럼 추가, 컬럼 삭제, 컬럼 확장 작업은 각각의 SQL 문장으로 실행해야 한다.
  • 인덱스를 추가 또는 삭제하는 동안에는 HCADER을 하지 않도록 한다.
  • HCADER을 수행하는 동안에 발생하는 테이블 락 시간은 dirty page flush time에 의해 결정된다. 만일 체크포인트가 최근에 발생했다면 이 작업은 길어야 수 초 이내에 끝나고 테이블에 dirty page가 많으면 flush로 인해 수 분이 걸릴 수도 있다.
  • 인덱스의 일부로 사용되고 있는 컬럼의 삭제는 피하는 것이 좋다. 인덱스 컬럼을 삭제 작업은 느리다.  인덱스 컬럼을 삭제하고자 한다면 먼저 해당 인덱스를 삭제한 후 컬럼을 삭제하도록 한다.
  • PK나 Secondary Key의 일부로 사용되는 컬럼에 대해서는 HCADER을 지원하지 않는다.
  • 문법은 아래와 같다.
ALTER TABLE <table-name> CHANGE <old-column-name> <new-column-name> <data-type> <required-ness> <default>
  • 임시 테이블의 경우 HCADER을 이용하는 것보다는 일반적인 방법으로 작업하는 것이 더 빠르다.

Compression

  • TokuDB는 3가지 레벨(tokudb_default, tokudb_fast, tokudb_small)의 압축을 제공한다.
  • 당연한 이야기이지만 압축과 CPU 사용율은 상반 관계를 가진다. 즉, 기본 레벨 (tokudb_default)을 사용하면 CPU 사용율은 낮아지지만, tokudb_small 압축 레벨을 사용하면 CPU 사용율은 높아진다.
  • TokuDB에서의 압축은 백그라운드 스레드로 동작하기 때문에 고레벨 압축을 사용하더라도 데이터베이스는 성능은 나빠지지 않는다. TokuTek왈, 오히려 압축율이 높은 데이터베이스가 좋을 성능을 보이는 것을  보았다.
  • 6 Core 이하에서는 tokudb_default, 그 이상에서는 tokudb_small를 권장한다.
  • 압축은 테이블 단위(row_format option)로 지정한다. 압축 옵션을 지정하지 않으면 기본 값으로 지정된다.
  • 압축 레벨은 crate table 또는 alter table 문장으로 변경할 수 있다. 바뀐 압축 옵션은 새롭게 유입된 데이터에 대해서만 유효하다. OPTIMIZE TABLE 구문을 실행하면 테이블의 모든 데이터 및 인덱스를 재작성할 수 있다.
CREATE TABLE <table_name> (c1 INT NOT NULL PRIMARY KEY, c2 INT NOT NULL) ENGINE=TOKUDB ROW_FORMAT=<row_format>;
ALTER TABLE <table_name> row_format=<row-format>;
  • 또한 세션 변수로 tokudb_row_format을 사용해도 된다.
SET tokudb_row_format = <row_format>;
  • row_format
    • tokudb_default : 기본값으로 TokuDB 7.0.1에서는 (=tokudb_fast)로 동작한다. 앞으로 이 값은 바뀔 예정이다.
    • tokudb_fast : quicklz 라이브러리를 사용하여 기본 압축을 한다.
    • tokudb_small : lzma 라이브러리를 사용하여 고압축을 한다.
  • 참고

Elimiates slave lag

  • MySQL의 복제는 단일 스레드로 디자인되어 있기 때문에 slave log가 종종 발생한다. 하지만 TokuDB에서는 high insertion rates로 인해 slave lag가 거의 발생하지 않는다. 
  • 참고

No Aging/Fragmentation

  • TokuDB Fractal Tree Index에서는 단편화가 절대 발생하지 않기 때문에 Reorg가 필요하지 않다.

Clustering keys and other indexing improvements

  • TokuDB 테이블들은 PK로 clustered된다.
  • secondary key도 clustering key로 생성할 수 있다. 하지만 clustering key의 leaf level에는 모든 컬럼이 포함되므로 문자열 데이터의 경우 데이터 사이즈의 최소 2배 이상의 공간이 필요하다 (단, 압축율에 따라 달라짐) . 
  • 따라서 clustering key를 이용하여 범위 검색을 할 경우 clustering factor가 높은 검색을 할 수 있어 쿼리 성능을 상당히 향상시킬 수 있다.
  • auto-increment 컬럼을 모든 인덱스의 모든 포지션에서도 사용할 수 있다.
  • 최대 32 컬럼까지 지원한다.

Bulk loader

  • 오프라인 테이블 및 인덱스 생성 작업은 parallel loader로 작업하기 때문에 multiple core에서 빠르게 작업할 수 있다.

Fast recovery

  • TokuDB 에서의 recovery는 100% 자동으로 실행된다. 
  • 빠른 복구를 지원한다. (일반적으로 1분이내)

참고

Linux Hot Copy(hcp) – Snapshot Tool


Overview

Linux Hot Copy(HCP) 유틸리티가 무료로 풀리면서, 고가의 스냅샷 유틸리티를 구입 없이도 얼마든지 사용할 수 있게 되었습니다. 스냅샷을 멋지게 활용할 수 있다면, 단순히 데이터 백업 뿐만 아니라 DB 시스템과 같은 매번 디스크에 데이터 변화가 많은 시스템에서도 특정 시점의 스냅샷 데이터를 추출할 수 있습니다.

Feature

HCP는 다음과 같은 특성을 가집니다.
  1. Point-in-Time 디스크 볼륨 스냅샷
  2. Copy on Write 방식의 Snapshot
  3. 서비스 영향없이 스냅샷을 생성
"Copy on Write"을 통한 스냅샷과 다른 방식의 차이를 비교해보도록 하겠습니다.

<Snapshot 비교>

  1. Copy-on-WriteWrite 시 원본 데이터 Block을 Snapshot 스토리지로 복제하는 방식으로 Snapshot 데이터 Read 시 변경되지 않은 데이터는 원본 블록에서,변경된 데이터는 Snapshot 블록에서 처리데이터 변경 분만 저장하므로 공간을 효율적으로 활용하나 블록 변경 시 원본 데이터와 스냅샷 데이터 양쪽 모두에서 Write이 발생
  2. Redirect-on-WriteCopy-on-Write와 유사하나, 원본 볼륨에 대한 Write을 별도의 공간에 저장하는 방식으로 Copy-on Write(원본 Read, 원본 Write, 스냅샷 Write)에 비해 Write이 1회만 발생하나, 스냅샷 해제 시 변경된 블록들을 원본 데이터 블록으로 Merge시켜야함
  3. Split mirror원본 볼륨과 동일한 사이즈의 별도 복제 볼륨 생성하는 방식으로 데이터를 Full Copy하므로 즉시 생성이 어렵고 용량 또한 많이 필요

Installation

설치 버전을 다운로드 하기 위해서는 하단 페이지에서 등록해야하는데, 등록하게 되면 설치 바이너리를 다운로드 받을 수 있는 별도의 링크를 메일로 보내줍니다. 무료로 배포가 되었을 뿐 오픈 소스가 아닌지라, 설치하기 위한 절차가 까다롭습니다. 

http://www.idera.com/productssolutions/freetools/sblinuxhotcopy

설치 파일을 받고  압축을 풀면 아래와 같이 OS 별로 설치 바이너리가 있습니다.

$ unzip Idera-hotcopy.zip
$ cd Idera-hotcopy
$ ls
Idera-hotcopy.zip Installing+Hot+Copy.html idera-hotcopy-5.2.2.i386.rpm idera-hotcopy-5.2.2.x86_64.rpm idera-hotcopy-amd64-5.2.2.deb idera-hotcopy-i386-5.2.2.deb idera-hotcopy-i386-5.2.2.tar.gz idera-hotcopy-x86_64-5.2.2.tar.gz

서버에 사용하고자 하는 설치 바이너리를 설치 한 후 리눅스 커널에 맞는 모듈을 업그레이드해야 합니다. hcp-setup 명령어로 쉽게 가능하며, 업그레이드 시 https접근(443포트)이 필요합니다.

$ rpm -i idera-hotcopy-5.2.2.x86_64.rpm
$ hcp-setup --get-module

Usage

사용법은 다음과 같이 “hcp –help” 명령어를 통해 확인해볼 수 있습니다.

$ hcp --help
Usage: hcp -h | -m <MOUNT POINT> <DEVICE> | -l | -r <DEVICE>
Options:
  -h, --help             Show this help message.
  -l, --list             List active Hot Copy sessions.
  -r, --remove           Remove Hot Copy session.
  -m, --mount-point      Specify mount point.
  -o, --read-only        Mount hcp fs read only.
  -c, --changed-blocks   Specify changed blocks storage device.
  -q, --quota            Sets quota for changed blocks storage.
  -s, --show-hcp-device  Show the Hot Copy device path for a given 
                         device.
  -v, --version          Show the Hot Copy driver version.
Examples:
    Start session:
        hcp /dev/sdb1
        hcp -m /mnt/tmp /dev/sdb1
    Remove session:
        hcp /dev/hcp1
    List sessions:
        hcp -l

변경된 블록들이 저장한 디바이스가 별도로 존재한다면 -c 옵션으로 스토리지를 분리할 수 있습니다. 그렇지 않으면, 스냅샷을 생성한 디스크에 기본적으로 변경 블록이 기록됩니다.

Example

1) 스냅샷 생성 – 변경 블록이 저장될 스토리지(/dev/sdb1) 

$ hcp -c /dev/sdb1 /dev/sdc1
Idera Hot Copy     5.2.2 build 19218 (http://www.r1soft.com)
Documentation      http://wiki.r1soft.com
Forums             http://forum.r1soft.com
Thank you for using Hot Copy!
Idera makes the only Continuous Data Protection software for Linux.
Starting Hot Copy: /dev/sdc1.
Changed blocks stored: /backup/.r1soft_hcp_sdc1
Snapshot completed: 0.000 seconds
File system frozen: 0.019 seconds
Hot Copy created: Tue Jul 18:31:02 KST 2013
Creating hotcopy snaphost device: /dev/hcp1, Please wait...
Hot Copy created at: /dev/hcp1
making new path: /var/idera_hotcopy/sdc1_hcp1
Mounting /dev/hcp1 read-write
Hot Copy mounted at: /var/idera_hotcopy/sdc1_hcp1

2) 스냅샷 현황

$ hcp -l
Idera Hot Copy     5.2.2 build 19218 (http://www.r1soft.com)
Documentation      http://wiki.r1soft.com
Forums             http://forum.r1soft.com
Thank you for using Hot Copy!
Idera makes the only Continuous Data Protection software for Linux.
****** hcp1 ******
 Real Device:           /dev/sdc1
 Virtual Device:        /dev/hcp1
 Changed Blocks Stored: /backup/.r1soft_hcp_sdc1.cow_hcp1
 Mounted:               /var/idera_hotcopy/sdc1_hcp1
 Time Created:          Tue Jul 18:31:02 KST 2013
 Changed Blocks:        0.25 MiB (262144 bytes)

백업 및 복구 방안

InnoDB 백업 시에는 Xtrabackup을 사용하면 시점 백업이 가능하나, 문제는 InnoDB가 아니거나, 스토리지 엔진이 섞여 있는 경우(예를들면 Mroonga, MyISAM, TokuDB) 특정 시점의 백업을 만들어 내기 난해합니다. 이 경우 HCP를 통해 백업을 활용한다면 간단하게 해결할 수 있습니다.

위와 같이, Global Read Lock을 걸고 스냅샷을 걸고 바로 Global Read Lock을 해제하면, 그 시점의 테이블 데이터에 대한 스냅샷 데이터를 추출할 수 있습니다. 물론 스냅샷을 거는 순간 바이너리 로그 포지션을 기록해놔야 하겠지요.

간단하게 Perl로 예제를 적어보겠습니다.

먼저 백업할 DB 커넥션을 맺고 Global Read Lock을 획득합니다.
$dbh = DBI->connect('DBI:mysql:mysql;host=127.0.0.1', 'root', 'pass')
           or die $DBI::errstr;

# Flush and Lock tables
$dbh->do("flush tables with read lock");

이 순간부터는 더 이상 데이터 변경은 없습니다. 즉, 이 상태에서 바이너리 포지션을 기록하면, 그 이후부터의 변경 사항은 바이너리 로그를 활용하여 복구할 수 있습니다.

$sth = $dbh->prepare("show master status");
$sth->execute();
$sth->bind_columns( \( @row{ @{$sth->{NAME_lc} } } ));
while ( @row = $sth->fetchrow_array ) {
   $status_info .= "[MASTER STATUS]\n";
   $status_info .= @row{file}."\t".@row{position}."\n";
}

백업은 슬레이브에서 하는 경우가 대부분이므로, 슬레이브 상태를 필히 기록해야겠죠.

$sth = $dbh->prepare("show slave status");
$sth->execute();
$sth->bind_columns( \( @row{ @{$sth->{NAME_lc} } } ));
while ( @row = $sth->fetchrow_array ) {
 $status_info .= "[SLAVE STATUS]\n";
 $status_info .= "CHANGE MASTER TO MASTER_HOST='@row{master_host}', MASTER_LOG_FILE='@row{relay_master_log_file}', MASTER_LOG_POS=@row{exec_master_log_pos};\n";
}

아직 로그로 Flush되지 않은 Commit된 트랜잭션을 유실하지 않기 위해 1초 대기합니다. InnoDB를 예를 들자면, innodb_flush_log_at_trx_commit이 0으로 설정된 경우를 들 수 있겠네요.

sleep(1);

이제 스냅샷을 만들고, 앞서 획득했던 Global Read Lock을 해제합니다. 여기서 명시적으로 마운트 포인트를 지정한 이유는 추후 hcp 언마운트를 위해서입니다.

# create snapshot
system "hcp -m /var/hcp/snapshot1 /dev/sdc1;
system "hcp -m /var/hcp/snapshot2 /dev/sdc1;

# unlock tables and disconnect 
$dbh->do("unlock tables");
$dbh->disconnect();

이제, 원하는 데이터를 다른 스토리지로 옮기면 됩니다. 로컬 디스크라면 cp로, 원격으로 옮긴다면 scp 또는 rsync를 생각해볼 수 있겠네요.

cp -R /var/hcp/snapshot1/mysql-data /backup/20140122/
cp -R /var/hcp/snapshot2/mysql-iblog /backup/20140122/

백업이 모두 마무리되었다면, 이제 스냅샷을 해제합니다.

system "hcp -r /var/hcp/snapshot1";
system "hcp -r /var/hcp/snapshot2";

만약 /var/hcp 밑에 마운트된 스냅샷을 한번에 지운다면, 아래로 한방에 처리해도 됩니다.

system "df | grep '/var/hcp/' | awk '{print \$1}' | xargs -i hcp -r {}";

간단하지만, HCP를 활용하여 백업할 수 있는 방안에 대해 설명하였습니다. ^^

Conclusion

Linux Hot Copy(hcp)는 설치 및 사용이 편리할 뿐만 아니라 무료로 사용할 수 있기에, 조금만 활용하면 멋진 백업 솔루션도 만들어낼 수 있습니다. 물론, 디스크에 두번 적는 추가 I/O가 발생하나, 스냅샷으로 백업을 할 수밖에 없는 스토리지 엔진 경우 매우 유용합니다.