2014년 7월 25일 금요일

TokuDB Fast Bulk Loader - INSERT INTO ... SELECT 문에 대한 이해

TokuDB 에서는 LOAD DATA FILE, CTAS, INSERT INTO ...SELECT문과 오프라인 인덱스 생성 작업시, 멀티 코어를 사용하여 병렬로 loader가 동작하는 Fast Bulk Loader 기능을 제공한다.
이 중에서 INSERT INTO ... SELECT문을 사용할 때 Fast Bulk Loader가 어떻게 움직이는지, 그리고 주의 사항에 대해서 소개하고자 한다.

Bulk Loader와 관련된 파라미터

tokudb_tmp_dir

이 파라미터에 지정된 디렉토리에 TokuDB bulk loader가 임시 파일을 저장한다. 이 파라미터에 지정된 디렉토리의 디스크 공간이 부족할 경우 bulk loader가 실패하고 에러가 발생한다.

tokudb_loader_memory_size

Bulk loader 가 사용하게 될 메모리를 제한하는 세션 변수이다. 기본값은 100M이며, 초 대형 테이블인 경우 이 값을 증가시켜 bluk loader의 성능을 향상 시킬 수 있다. Bulk loader에서 사용할 메모리는 tokudb_cache_size 파라미터에 지정된 TokuDB cache에서 할당받으므로 tokudb_cache_size 파라미터에 지정된 값보다 작아야 한다.

tokudb_read_buf_size

TokuDB의 읽기 버퍼 사이즈로써 큰 범위를 스캔하여 bulk fetch를 할 경우 이 값을 크게 조정하도록 한다. 기본값은 131,072(128KB) 이다.

tokudb_load_save_space

이 파라미터는 TokuDB bulk loader의 동작 방식을 결정하는 세션 변수이다. 만일 off로 지정되어 있을 경우 중간 생성 파일을 압축하지 않는다. 따라서 TokuDB bulk loader는 on 상태일 때보다 cpu를 보다 적게 사용한다. 하지만 보다 많은 디스크 공간을 사용하므로 주의해야 한다. 기본값은 on이다.

tokudb_prelock_empty

TokuDB에서 비어있는 테이블에 대해 첫 조작이 일어날 경우 전체 테이블에 대해 LOCK을 선점한다. 만일 비어있는 테이블에 대해 하나의 트랜잭션이 loading을 시작할 경우, loading을 빨리 끝내야겠구나라고 간주하고 TABLE LOCK을 걸고 loading job을 실행한다. 따라서 이후에 들어오는 같은 테이블에 대한 트랜잭션을 브로킹된다. 이를 preemptive prelocking 이라고 한다. tokudb_prelock_empty = off 로 설정하면 비어있는 테이블에 대해 TABLE LOCK을 선점하는 것을 막을 수 있다. 하지만 loading을 fast bulk loader로 동작하지 않는다. 기본값은 on이다.



TokuDB Fast Bulk Loader는 크게 Fetch와 Load 단계로 나누어진다.


Fetch 단계

tokudb_tmp_dir 파라미터에 지정된 디렉토리에 loding할 입력 데이터를 임시 저장한다. 이 단계는 tokudb_load_save_space 세션 변수에 의해 임시 데이터의 압축 여부를 결정한다.
임시 파일은 아래와 같이 fetch 단위로 각각 생성된다.

$ ls -al
-rw------- 1 mysql dba 11335514 7월 25 09:20 tokuld04pZJY
-rw------- 1 mysql dba 24249800 7월 25 07:43 tokuld08nQkb
-rw------- 1 mysql dba 21165661 7월 25 07:47 tokuld08sBgG
-rw------- 1 mysql dba 10838587 7월 25 09:37 tokuld0CKDyC
-rw------- 1 mysql dba 11055343 7월 25 09:36 tokuld0EkBvj
...

이 때 show processlist 명령어로 현재 bluk loader가 진행 중인 사항을 확인할 수 있는데, 아래와 같이 Fetched about xxxx rows, loading data still remains 라고 표시된다.
만일 Fetched about xxxx rows, INSERTS .... xxxx ROWS 라고 표시되면 INSERT INTO ... SELECT 문이 bulk loader로 동작하는 하는 것이 아니므로 주의하기 바란다.
또한 INSERT INTO ... SELECT 문으로 테이블에 첫 입력을 할 때 bulk loader로 동작하지만, 그 후에 실행하는 INSERT INTO ... SELECT 문은 bulk loader로 동작하지 않는다. 이 문제는 TokuDB 7.1.8 에서 bulk loader로 동작할 수 있도록 수정될 예정이라고 한다.

> show processlist
Queried about 2040287006 rows, Fetched about 2040287000 rows, loading data still remains
...
...

TokuDB에서 저장 파일 및 Fractal Tree Index의 물리적 위치는 information_schema.tokudb_file_map 에서 조회할 수 있다.

+------------------------------------------+------------------------------------------------------------+--------------+----------------+-----------------------+
| dictionary_name                          | internal_file_name                                         | table_schema | table_name     | table_dictionary_name |
+------------------------------------------+------------------------------------------------------------+--------------+----------------+-----------------------+
| ...t1#P#PF_201401-key-cl1_page_accesslog | /.../_db1_t1_P_PF_201401_key_cl1_t1_ad97db_1_19_B_0.tokudb | db1          | t1#P#PF_201401 | key-cl1_t1            |
| ...t1#P#PF_201401-key-ix1_page_accesslog | /.../_db1_t1_P_PF_201401_key_ix1_t1_ad97db_1_19_B_1.tokudb | db1          | t1#P#PF_201401 | key-ix1_t1            |
| ...t1#P#PF_201401-main                   | /.../_db1_t1_P_PF_201401_main_ad97db_1_19_B_2.tokudb       | db1          | t1#P#PF_201401 | main                  |
| ...t1#P#PF_201401-status                 | /.../_db1_t1_P_PF_201401_status_ad97c0_1_19.tokudb         | db1          | t1#P#PF_201401 | status                |
...
+------------------------------------------+------------------------------------------------------------+--------------+----------------+-----------------------+

tokudb_data_dir에서 지정된 데이터 디렉터리에서 ls 명령어로 조회하면 해당 파일을 확인할 수 있다.

-rwxrwx--x 1 mysql mysql 32K 7 25 06:31 _db1_t1_P_PF_201401_key_cl1_t1_ad97c0_3_19.tokudb
-rwxrwx--x 1 mysql mysql 32K 7 25 06:31 _db1_t1_P_PF_201401_key_ix1_t1_ad97c0_4_19.tokudb
-rwxrwx--x 1 mysql mysql 32K 7 25 06:31 _db1_t1_P_PF_201401_main_ad97c0_2_19.tokudb
-rwxrwx--x 1 mysql mysql 64K 7 25 07:12 _db1_t1_P_PF_201401_status_ad97c0_1_19.tokudb

load 단계

임시 파일을 테이블 및 인덱스에 적재하는 단계이다. 이 때 멀티 코어를 사용하여 병렬로 loder를 동작시킨다. 이 사실은 top 명령어로 쉽게 확인할 수 있다.

이 때 show processlist 명령어로 확인해 보면 Loading of data about xxx.x% done 이라고 표시된다.
여기서 나타나는 백분율은
  1. 파티션되지 않은 테이블일 때는 테이블과 테이블에 포함된 인덱스(n) 전체의 진행 사항에 에 대한 백분율을 나타낸다.
  2. 파티션된 테이블일 때는 각 파티션 별로 테이블 파티션과 파티션에 포함된 인덱스(n) 전체의 진행 사항에 대한 백분율을 나타낸다. 따라서 하나의 테이블에 2개의 파티션이 있을 경우 1~100% 까지 두 번 표시된다.

load는 인덱스 번호 순(n) > 힙 테이블 순으로 진행된다.
각 파일에 대해 위의 순서대로 B_0, B_1... 순(_db1_t1_P_PF_201401_key_cl1_t1_ad97db_1_19_B_0.tokudb, _db1_t1_P_PF_201401_key_ix1_t1_ad97db_1_19_B_1.tokudb, _db1_t1_P_PF_201401_main_ad97db_1_19_B_2.tokudb)으로 임시 파일을 만들어 loading을 한 다음 전체 loading 작업이 완료되면 원래 파일과 바꿔치기를 하고 원래 파일을 삭제한다.

-rwxrwx--x 1 mysql dba 32K 7 25 06:31 _db1_t1_P_PF_201401_key_cl1_t1_ad97c0_3_19.tokudb
-rwxrwx--x 1 mysql dba 32K 7 25 06:31 _db1_t1_P_PF_201401_key_cl1_t1_ad97db_1_19_B_0.tokudb
-rwxrwx--x 1 mysql dba 32K 7 25 06:31 _db1_t1_P_PF_201401_key_ix1_t1_ad97c0_4_19.tokudb
-rwxrwx--x 1 mysql dba 32K 7 25 06:31 _db1_t1_P_PF_201401_key_ix1_t1_ad97db_1_19_B_1.tokudb
-rwxrwx--x 1 mysql dba 32K 7 25 06:31 _db1_t1_P_PF_201401_main_ad97c0_2_19.tokudb
-rwxrwx--x 1 mysql dba 32K 7 25 06:31 _db1_t1_P_PF_201401_main_ad97db_1_19_B_2.tokudb
-rwxrwx--x 1 mysql dba 64K 7 25 07:12 _db1_t1_P_PF_201401_status_ad97c0_1_19.tokudb

참고로 파일 이름은 TokuDB 7.1.6에서는 통상적으로
  1. 메타 데이터 파일: _<데이터베이스명>_<테이블명>_[파티션명]_status_<xxx>_1_19.tokudb
  2. 힙 테이블: _<데이터베이스명>_<테이블명>_[파티션명]_main_<xxx>_2_19.tokudb
  3. 인덱스 파일: _<데이터베이스명>_<테이블명>_[파티션명]_<인덱스명>_<xxx>_[3~n]_19.tokudb
으로 생성된다.

2014년 7월 4일 금요일

TokuDB Fast Upserts and Update

Upsert(INSERT INTO .. ON DUPLICATE KEY UPDATE...)나 UPDATE문은 UPDATE에 영향을 받는 로우가 얼마나 되는지를 판단해야 하기 때문에 느리고, 시스템의 read I/O 성능에 의해 영향을 많이 받는다. MySQL에서 update문은 read-modify-write 알고리즘에 의해 수행되어 스토리지 엔진에서 로우를 읽고, 갱신한 후 스토리지 엔진에 갱신된 로우를 쓴다.
TokuDB의 update 알고리즘은 이와 다르다.  Update 문이 실행되면 먼저, Fractal Tree Message라는 작은 공간에 update 표현식을 암호화한 형태로 저장한다. update message는 Fractal Tree의 root level에 전달되고, background에서 Fractal Tree Buffer flush down 이벤트에 의해 message down을 수행한다. 결국 Fractal Tree의 leaf node까지 전달되고 update program이 해당 로우를 쓰게 된다.
아래와 같은 테이블이 있다고 가정하자.
CREATE TABLE t (
id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
count BIGINT UNSIGNED NOT NULL
);

우리는 이 테이블의 id 컬럼에 어떤 값이 저장되어 있는지 알 수 없으므로 아래와 같은 Upsert문을 실행한다. 즉 해당 :id에 바인드된 id가 있으면 update를 없으면 insert를 수행한다.
INSERT NOAR INTO t values (:id, 1)
ON DUPLICATE KEY UPDATE count=count+1;

여기서 INSERT INTO 사이의 "NOAR"(TokuDB에서는 NOAR modifier라고 말하고 있다.) 예약어를 주목하자. NOAR modifier는 TokuDB로 하여금 PK 존재 여부를 Fractal Tree Index 안으로 "upsert" message를 삽입하는 형태로 체크할 것으로 대체하도록 명령한다. "upsert" message는 row의 복사본과 업데이트할 count 컬럼의 값을 들고 있다. 그리고 앞에서 언급한 것처럼 Fractal Tree의 노드의 Buffer가 채워지게 되면, 이 "upsert" message를 하위 트리의 노드에 flush하고, message가 leaf node에 도달하면 update를 실행하여 count컬럼의 값을 갱신하거나 새로운 로우를 leaf node에 삽입한다. 따라서, "upsert" message가 어떤 레벨에 있든지 관계없이 같은 PK값을 가지는 메시지는 상위 노드에 있는 메시지가 최신이기 때문에, PK의 존재 여부를 마지막 leaf level까지 내려가서 찾을 필요가 없이 가장 먼저 찾아진 message에서 그 로우를 취득하면 된다.

단, Fast Upsert 및 Update의 제약 사항은 아래와 같다. 
  • 테이블에 반드시 PK가 정의되어 있어야 한다.
  • 테이블에 보조키가 있어서는 안된다.
  • 테이블에 트리거가 있어서는 안된다.
  • binlog_format은 STATEMENT 이어야 한다.
  • Update할 컬럼의 데이터 타입은 int, char, varchar 타입이여만 한다.
  • 쿼리가 실행되는 세션의 SQL_MODE는 STRICT_ALL_TABLES 또는 STRICT_TRNAS_TABLE가 아니어야 한다.
아래의 링크를 Tokutek 내부의 벤치마크 결과이다.
http://www.tokutek.com/2013/03/tokudb-fast-update-benchmark/

아래는 sysbench 0.5에서 InnoDB, TokuDB normal update, TokuDB fast update(NOAR)를 수행했을 때의 벤치마크 결과이다. 혹시나해서 테이블에 보조키를 생성하여 돌려보았으나 fast update가 동작하지 않아서 해당 결과는 제외시켰다.
위와 같은 제한된 환경이 얼마나 있을지 모르겠지만 혹시 존재한다면 Fast Upsert 기능은 상당히 강력한 기능이 될 수도 있겠다.


MariaDB for TokuDB 7.1.6 CTAS 및 INSERT ... SELECT 문에서의 Bulk Fetch 미동작 문제

 GROUP BY절을 포함한 CTAS 또는 INSERT ... SELECT 문에서 TokuDB Bulk Fetch가 미동작하는 이슈이다.
이는 TokuDB 7.1.8에서 패치될 예정이다.
Example 1
CREATE TABLE t1 (
  s VARCHAR(2) PRIMARY KEY,
  cnt INT)
AS
( SELECT s, COUNT(*)
    FROM t
   GROUP BY s )

Example 2
INSERT INTO t1
SELECT '20140601', no, MAX(price), COUNT(*)
FROM tab01_temp_log
GROUP BY dates, no
ORDER BY dates, no DESC;