2014년 7월 4일 금요일

MariaDB 10.0 멀티 쓰레드 복제

개요

MariaDB 10.0과 MySQL 5.6은 모두 Multi-threaded replication feature를 가지고 있지만,
두 Main-stream의 Multi-threaded replication의 구현 기능은 상당한 차이를 가지고 있다.

MySQL 5.6은 자동화된 DB 단위의 Parallel replication을 지원하지만, MariaDB 10.0은 반 자동화된 GTID의 domain_id 기반의 Parallel replication 기능을 지원한다.
여기서 MySQL의 자동화는 사용자가 Parallel replication apply에 어떻게 관여할 수 있는 방법이 없다는 것을, 그리고 MariaDB의 반자동화는 응용 프로그램이나 개발자가 Parallel replication을 컨트롤 할 수 있다는 의미이기도 하다.

하지만 MariaDB 10.0에는 많이 알려져 있지 않은 자동화된 Parallel replication feature도 가지고 있는데, 간단히 그 기능을 소개하고자 한다.
MariaDB의 자동화된 Parallel replication을 이해하기 위해서는 먼저 MariaDB의 Binary log "GROUP-COMMIT"을 이해해야 하므로, 간단히 살펴보고 본론으로 넘어가도록 하자.

바이너리 로그 그룹 커밋

바이너리 로그의 그룹 커밋 기능은 MySQL 5.0 시절부터 Binary log 쓰기 성능 향상을 위해서 계속 이슈가 되어 왔던 기능이다.
물론 MySQL 5.6 버전도 가지고 있는 기능이지만, 이 기능이 MySQL 5.6이나 MariaDB 10.0에서 처음 구현된 기능은 아니며, 예전부터 추가되었다가 다른 제약때문에 제거되었다가 다시 구현된 기능이다.

그룹 커밋은 여러 개의 아이템 (바이너리 로그 이벤트나 트랜잭션 등..)을 한번에 모아서 처리하는 기능을 의미할 때 자주 선택되는 용어이기도 한데,
MariaDB의 바이너리 로그의 그룹 커밋 기능은 단순히 이벤트를 바이너리 로그 파일에 모아서 기록하는 수준이 아니라, 실제 트랜잭션의 커밋 처리까지 모아서 수행하는 것을 의미한다.
즉 바이너리 로그 이벤트의 그룹 커밋을 효율적으로 모아서 처리하기 위해서는 트랜잭션의 Commit 성능을 조금 포기해야 할 수도 있다는 것을 의미한다.

MariaDB 10.0에서는 바이너리 이벤트의 그룹 커밋을 위해서는 아래와 같은 2개의 시스템 변수를 지원하고 있다.
  • binlog_commit_wait_usec
    짧은 간격을 두고 여러 클라이언트에서 커밋되는 트랜잭션의 바이너리 로그 이벤트를 모아서 한번에 바이너리 로그 파일로 기록하게 된다. 이 때 순수하게 동시점에 수행된 트랜잭션만 그룹 커밋 대상으로 선정할 수도 있지만, MariaDB 서버가 그룹 커밋의 효율을 높이기 위해서 일정 시간을 기다리면서 커밋되는 트랜잭션을 모아서  그룹 커밋으로 처리하도록 할 수도 있다. 이때 MariaDB 서버가 최대 얼마만큼의 시간을 기다리도록 할지를 설정하는 시스템 변수이다. binlog_commit_wait_usec 시스템 변수는 Milli-seconds 단위로 설정하므로, 만약 최대 10 밀리 초 이내에는 커밋이 수행되도록 하고자 한다면 binlog_commit_wait_usec을 10000로 설정하면 된다.
  • binlog_commit_wait_count
    MariaDB 서버가 그룹 커밋의 효율을 높이기 위해서 조금은 기다리면서 일정 개수 이상의 이벤트가 커밋되면 이들을 모아서 그룹 커밋으로 처리하게 된다. 이때 MariaDB 서버가 최대 몇 개까지의 커밋을 기다릴지를 설정하는 시스템 변수이다. 이때 개수의 대상의 바이너리 로그의 이벤트 개수나 쿼리의 수가 아니라 트랜잭션의 개수를 의미한다.

어떤 트랜잭션들이 얼마나 모아서 그룹 커밋 처리되었는지는 바이너리 로그 파일의 내용을 살펴보면 알 수 있는데, 간단한 예제로 그룹 커밋 비율을 살펴보도록 하자.
먼저 컬럼을 3개 가지는 test라는 테이블을 생성하고, 4개의 Client connection을 통해서 동시에 각각 INSERT문장을 실행해 보았다. (실제 바이너리 로그의 그룹 커밋은 sync_binlog 옵션과 innodb_flush_log_at_trx_commit 옵션의 값이 1일 때에만 활성화되는 것은 아니다.)
MariaDB [test]> set global binlog_commit_wait_usec=5000; /* 5 milli-seconds */
Query OK, 0 rows affected (0.00 sec)
 
MariaDB [test]> set global binlog_commit_wait_count=50;
Query OK, 0 rows affected (0.00 sec)
 
MariaDB [test]> set global sync_binlog=1;
Query OK, 0 rows affected (0.00 sec)
 
MariaDB [test]> set global innodb_flush_log_at_trx_commit=1;
Query OK, 0 rows affected (0.00 sec)
 
Client1 > insert into test values (1,1,1);
Client2 > insert into test values (2,2,2);
Client3 > insert into test values (3,3,3);
Client4 > insert into test values (4,4,4);

위와 같이 쿼리가 처리되면, 각 트랜잭션들은 최대 5 milli-seconds를 기다렸다가 커밋을 수행하게 된다. 이때 같이 커밋된 바이너리 로그 이벤트(바이너리 로그 파일에 기록된 트랜잭션)들은 동일한 "COMMIT ID"를 가지게 된다.
mysqlbinlog나 show binlog events 명령의 결과로 decoding된 바이너리 로그 파일에서 "cid"라는 값으로 표시된다.
위의 INSERT 명령 실행 결과로 생성된 바이너리 로그 파일의 내용은 아래와 같았다.
MariaDB [test]> show binlog events in 'binary-log.000015';
+-------------------+-----+-------------------+-----------+-------------+------------------------------------------------+
| Log_name          | Pos | Event_type        | Server_id | End_log_pos | Info                                           |
+-------------------+-----+-------------------+-----------+-------------+------------------------------------------------+
| binary-log.000015 |   4 | Format_desc       |         1 |         248 | Server ver: 10.0.11-MariaDB-log, Binlog ver: 4 |
| binary-log.000015 | 248 | Gtid_list         |         1 |         287 | [0-1-1213]                                     |
| binary-log.000015 | 287 | Binlog_checkpoint |         1 |         327 | binary-log.000014                              |
| binary-log.000015 | 327 | Binlog_checkpoint |         1 |         367 | binary-log.000015                              |
| binary-log.000015 | 367 | Gtid              |         1 |         407 | BEGIN GTID 0-1-1214 cid=1786                   |
| binary-log.000015 | 407 | Query             |         1 |         501 | use `test`; insert into test values (3,3,3)    |
| binary-log.000015 | 501 | Xid               |         1 |         528 | COMMIT /* xid=1786 */                          |
| binary-log.000015 | 528 | Gtid              |         1 |         568 | BEGIN GTID 0-1-1215 cid=1786                   |
| binary-log.000015 | 568 | Query             |         1 |         662 | use `test`; insert into test values (1,1,1)    |
| binary-log.000015 | 662 | Xid               |         1 |         689 | COMMIT /* xid=1788 */                          |
| binary-log.000015 | 689 | Gtid              |         1 |         729 | BEGIN GTID 0-1-1216 cid=1787                   |
| binary-log.000015 | 729 | Query             |         1 |         823 | use `test`; insert into test values (4,4,4)    |
| binary-log.000015 | 823 | Xid               |         1 |         850 | COMMIT /* xid=1787 */                          |
| binary-log.000015 | 850 | Gtid              |         1 |         890 | BEGIN GTID 0-1-1217 cid=1787                   |
| binary-log.000015 | 890 | Query             |         1 |         984 | use `test`; insert into test values (2,2,2)    |
| binary-log.000015 | 984 | Xid               |         1 |        1011 | COMMIT /* xid=1789 */                          |
+-------------------+-----+-------------------+-----------+-------------+------------------------------------------------+
 
16 rows in set (0.00 sec)

이 결과에서 4개의 트랜잭션이 저장된 것을 알 수 있다. (각 트랜잭션은 BEGIN GTID라는 문자열로 시작됨)
4개의 트랜잭션이 모두 다른 GTID 값을 가지고 있지만, 그 뒤에 명시된  cid는 동일한 값이라는 것을 알 수 있다.
즉 여기에서는 4개의 트랜잭션이 모두 같은 cid 1786을 가지고 있는데, 이것이 4개의 트랜잭션이 모두 한번의 그룹 커밋으로 저장된 이벤트들이라는 것을 의미한다.

MariaDB 10.0의 Multi-threaded Replication

MariaDB 10.0의 Multi-threaded replication은 크게 In-Ordered와 Out-of-Ordered로 구분될 수 있는데, Out-of-Ordered 복제는 GTID의 Domain_id 기반으로 처리되는 Multi-threaded replication을 의미한다.
이는 이미 잘 알고 있는 부분이므로 생략하도록 하겠다.

나머지 In-Ordered 방식은 위에서 잠깐 살펴본 "GROUP-COMMIT"을 기반으로 작동하는 Multi-threaded Replication을 의미한다.
Replication Slave의 Coordinator는 바이너리 로그에서 그룹 커밋된 트랜잭션들을 모두 모아서(cid 기반), SQL Thread들에게 동시에 나눠서 처리하도록 한다.
이렇게 Parallel 처리가 가능한 이유는, 하나의 그룹 커밋내에 있던 모든 트랜잭션들은 서로 충돌 (동일 Row에 대한 Exclusive lock을 필요로 하는)이 없다는 것이 이미 Master에서 증명된 것들이기 때문이다.

결론적으로 마스터에서 하나의 그룹 커밋내에 포함된 트랜잭션이 많으면 많을수록 Multi-threaded slave의 parallelism이 높아지게 되는 것이다.

그룹 커밋이 아닌 트랜잭션들에 대한 Multi-threaded Replication

또한 MariaDB 10.0의 Multi-threaded slave에서는 그룹 커밋에 소속되지 않은 트랜잭션이라 하더라도, 각 트랜잭션의 일부분에 대해서는 Parallel하게 처리할 수 있도록 지원하는 것으로 기술(메뉴얼)되어 있다.
즉 2개의 트랜잭션이 슬레이브에서 실행되어야 하는 상태에서, Slave의 Coordinator는 첫번째 트랜잭션을 읽어서 1번 SQL thread에게 넘겨주고 1번 쓰레드가 Commit을 시작하는 시점까지 기다리다가 
1번 쓰레드가 Commit을 실행하는 시점에 Coordinator는 두번째 트랜잭션을 2번 SQL Thread에게 넘겨주게 된다.

즉 그룹 커밋에 포함되지 않은 트랜잭션이라 하더라도 최소한 COMMIT 작업은 Multi-threading이 가능하도록 설계한 것이다.
COMMIT만 Multi-threading하는 것이 무슨 효과가 있을까라고 생각하겠지만, sync_binlog=1이거나 log-slave-updates가 활성화된 경우 그리고 innodb_flush_log_at_trx_commit이 1로 설정된 경우(COMMIT이 고비용으로 처리되는 경우)에는 이것만으로도 많은 도움이 될 것으로 보인다.

Multi-threaded slave의 성능 테스트 (Test env)

Hardware spec
  • 3.40GHz * 4 with HyperThreading
  • 32G memory
  • 2 SAS + Raid controller (R-1) with 512MB cache

MySQL configuration
  • innodb_buffer_pool_size = 20G
  • binlog_commit_wait_usec = 1000
  • binlog_commit_wait_count = 50 ## only for MTS env.
  • slave_parallel_threads = 10       ## only for MTS env.
  • slave_parallel_max_queued = 524288

Sysbench
  • 20 ~ 50 clients
  • table rows : 10 million
  • run only update_nokey (sysbench) update statement

Multi-threaded slave의 성능 테스트 (Replication performance)

15k Update statement / second env




25k Update statement / second env





이 테스트에서는 최대 초당 25k Update statements / sec 부하를 발생했었는데, MTS 환경에서는 Replication 지연이 발생하지 않았다.
MTS 환경이 최대 얼마만큼의 Update를 처리할 수 있을지는 테스트해보진 않았지만, 사실 이는 서비스 환경이나 특성에 따라서 가변적인 부분이므로 테스트 결과가 운영에 도움이 되지 않을 것으로 보여서 생략한다. 
하지만 MTS환경에서는 Slave sql thread간의 동기화를 위해서 많은 CPU 자원을 사용한다는 것을 알 수 있다. (이는 일반적으로 Stand-by 용도로 사용되는 Slave 환경에서는 특별히 문제되지는 않을 것으로 고려된다.)

Multi-threaded slave의 성능 테스트 (Group commit performance regression)



Multi-threaded replication을 위해서 Binary log의 Group commit 을 활성화한 경우에는 동일한 클라이언트 부하에서도 위와 같은 서버의 성능 차이를 보였다.
이는 Marster의 client thread가 Group commit을 대기하는 1ms 시간때문에 발생하는 현상이며, 이 테스트에서는 50개의 client thread만 사용했는데 10% 정도의 성능 저하가 발생했다.
하지만, 이는 그다지 큰 이슈가 될 것으로 보이지는 않는다. 일반적인 서비스 환경에서는 5000여개의 client thread가 동시에 기동되어서 Transaction을 발생시키는데, 이 트래잭션들이 최대 1ms를 더 기다리는 것이 되므로 MySQL  서버의 전체적인 Throughput에는 크게 영향을 미치지 않을 것으로 보인다.

MTS로 처리되는 슬레이브에서 프로세스 목록을 조회해보면, 각 SQL Thread가 서로 동기화되면서 병렬로 처리되는 과정을 볼 수 있다.
MariaDB [(none)]> show processlist;
+-----+-------------+-----------------+----------+---------+------+--------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+----------+
| Id  | User        | Host            | db       | Command | Time | State                                                                          | Info                                                                                                 | Progress |
+-----+-------------+-----------------+----------+---------+------+--------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+----------+
|   3 | system user |                 | sysbench | Connect |    1 | Waiting for prior transaction to start commit before starting next transaction | COMMIT                                                                                               |    0.000 |
|   4 | system user |                 | sysbench | Connect |    1 | init                                                                           | COMMIT                                                                                               |    0.000 |
|   5 | system user |                 | sysbench | Connect |    1 | init                                                                           | UPDATE sbtest set c='491483753-416518378-451160866-183205132-1026097669-61695471-1011294415-12704074 |    0.000 |
|   6 | system user |                 | sysbench | Connect |    1 | init                                                                           | UPDATE sbtest set c='909812426-309814605-1019264342-836244958-228197628-970942614-755265063-45250027 |    0.000 |
|   7 | system user |                 | sysbench | Connect |    1 | Waiting for prior transaction to start commit before starting next transaction | COMMIT                                                                                               |    0.000 |
|   8 | system user |                 | sysbench | Connect |    1 | Waiting for prior transaction to start commit before starting next transaction | COMMIT                                                                                               |    0.000 |
|   9 | system user |                 | sysbench | Connect |    1 | init                                                                           | UPDATE sbtest set c='112858395-197504108-673909257-770681497-1023414038-895922925-320043291-26580038 |    0.000 |
10 | system user |                 | sysbench | Connect |    1 | freeing items                                                                  | NULL                                                                                                 |    0.000 |
11 | system user |                 | sysbench | Connect |    1 | init                                                                           | UPDATE sbtest set c='55183839-759991643-1044340954-640849878-599408370-12160985-325658104-762744694- |    0.000 |
12 | system user |                 | sysbench | Connect |    1 | Waiting for prior transaction to start commit before starting next transaction | COMMIT                                                                                               |    0.000 |
14 | root        | 127.0.0.1:39423 | NULL     | Sleep   |    8 |                                                                                | NULL                                                                                                 |    0.000 |
18 | system user |                 | NULL     | Connect | 2327 | Waiting for master to send event                                               | NULL                                                                                                 |    0.000 |
19 | system user |                 | NULL     | Connect |   27 | Slave has read all relay log; waiting for the slave I/O thread to update it    | NULL                                                                                                 |    0.000 |
| 155 | root        | localhost       | NULL     | Query   |    0 | init                                                                           | show processlist                                                                                     |    0.000 |
+-----+-------------+-----------------+----------+---------+------+--------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+----------+



댓글 없음:

댓글 쓰기