MariaDB/Galera Cluster 기술 노트!!

Overview

MariaDB에서 MariaDB/Galera Cluster 제품군을 새롭게 출시하였습니다.MariaDB/Galera는 MariaDB의 Synchronous 방식으로 동작하는 다중 마스터 클러스터입니다.

MariaDB/Galera Cluster은 Galera 라이브러리를 사용하여 노드 간 데이터 복제를 수행합니다. 물론 아직은 Alpha 버전으로 발표되기는 했지만, 조만간 안정적인 버전이 릴리즈 되면 상당한 물건이 될만한 놈입니다.

오늘은 이에 관해 간단하게 리뷰를 하겠습니다.

Feature & Benefits

먼저 MariaDB/Galera Cluster의 특징은 다음과 같이 몇 가지로 나눠볼 수 있습니다.

  • Synchronous 방식으로 노드 간 데이터 복제
  • Active-Active 방식의 다중 마스터 구성 – 모든 노드에서 읽기/쓰기가 가능
  • 클러스터 내 노드 자동 컨트롤 – 특정 노드 장애 시 자동으로 해당 노드 제거
  • 자동으로 신규 노드 추가
  • 완벽한 병렬적으로 데이터를 행단위로 복제
  • 기존의 MySQL 클라이언트 방식으로 동작

cluster-diagram1
출처 : http://www.percona.com/doc/percona-xtradb-cluster/_images/cluster-diagram1.png

이와 같은 특징에서 전통적인 Asynchronous 방식의 리플리케이션이 가지는 한계점이 해결됩니다.

  • 마스터/슬레이브간 데이터 동기화 지연 없음
  • 노드 간 유실되는 트랜잭션이 없음
  • 읽기/쓰기 모두 확장이 가능
  • 클라이언트의 대기 시간이 줄어듬 – 데이터는 로컬 노드에 존재

하지만 Replication이 가지는 본연의 한계점은 여전히 내재합니다.

  • 신규 노드 추가 시 부하 발생 – 신규 노드 추가 시 모든 데이터를 복사해야 함
  • 효과적인 쓰기 확장 솔루션에는 한계 – 서버 간 Group Communication시 트래픽 발생
  • 모든 서버 노드에 동일한 데이터를 유지해야 함 – 저장 공간 낭비

MariaDB/Galera Cluster

MariaDB/Galera cluster는 Galera 라이브러리를 사용하여 리플리케이션을 수행한다고 하는데 Galera Replication은 어떤 방식으로 동작할까요?

Galera Replication은 wsrep API로 노드 간 통신을 하며, MariaDB 측에서는 wsrep API에 맞게 내부적인 개선하였다고 합니다. MySQL-wsrep는 https://launchpad.net/codership-mysql에서 시작한 오픈소스 프로젝트입니다.

MySQL-wsrep는 MySQL의 InnoDB스토리지 엔진 내부에서 Write Set(기록 집합 : 트랜잭션의 기록하는 모든 논리적인 데이터 집합)을 추출하고 적용하는 구현됩니다. 노드 간 Write Set을 전송 및 통신을 위해서는 별도의 리플리케이션 플러그인을 사용하며, 리플리케이션 엔진은 wsrep에 정의된 Call/Callback 함수에 따라 동작합니다.

1) Synchronous vs. Asynchronous

먼저 Synchronous와 Asynchronous 리플리케이션의 차이점에 대해서 설명하겠습니다.

리플리케이션의 두 가지 방식의 가장 기본적인 차이점은 클러스터 내 특정 노드에서 데이터 변경이 발생하였을 때 다른 노드들에 동시에 데이터 변경이 적용되는 것을 보장는지 여부에 있습니다.

Asynchronous 방식의 Replication은 마스터 노드에서 발생한 변화가 슬레이브 노드에 동시에 적용되는 것을 보장하지 않습니다. 마스터/슬레이브 간 데이터 동기화 지연은 언제든 발생할 수 있으며, 마스터 노드가 갑자기 다운되는 경우 가장 최근의 변경 사항이 슬레이브 노드에서는 일부 유실될 수도 있는 가능성도 있습니다.

Synchronous 방식의 Replication은 Asynchronous에 비해 다음과 같은 강점을 가집니다.

  • 노드 장애 시에도 데이터 유실 없이 높은 가용성 달성
  • 트랜잭션은 모든 노드에서 동시 다발적으로 발생
  • 클러스트 내 모든 노드 간 데이터 일관성을 보장

그러나 Synchronous Replication 수행을 위해서는 2단계 Commit 필요하거나 분산 잠금과 같은 상당히 느린 방식으로 동작합니다.

Synchronous 방식의 Replication은 성능 이슈와 및 복잡한 구현 내부적으로 요구되기 때문에 여전히 Asynchronous 방식의 Replication이 널리 사용되고 있습니다. 오픈 소스 대명사로 불리는 MySQL과 PostgreSQL이 Asynchronous 방식으로만 데이터 복제가 이루어지는 것 또한 그와 같은 이유에서입니다.

2) Certification Based Replication Method

성능 저하 없이 Synchronous하게 데이터베이스 리플리케이션을 구현하기 위해 “Group Communication and Transaction Ordering techniques”이라는 새로운 방식이 고안되었습니다. 이것은 많은 연구자들(Database State Machine Approach and Don’t Be Lazy, Be Consistent)이 제안했던 방식으로, 프로토타입 구현해본 결과 상당한 발전 가능성을 보여주었던 바가 있습니다.

Galera Replication은 높은 가용성과 성능이 필요한 어플리케이션에서는 상당히 쉽고 확장 가능한 Synchronous 방식의 리플리케이션을 제공하며 다음과 같은 특징이 있습니다.

  • 높은 가용성
  • 높은 투명성(알기 쉽다는 의미)
  • 높은 확장성(어플리케이션에 따라 거의 선형적인 확장까지도 가능)

Galera 리플리케이션은 분할된 라이브러리와 같이 동작하고, wsrep API로 동작하는 시스템이라면 어떠한 트랜잭션과도 연관되어 동작할 수 있는 구조입니다.

3) Galera Replication implementation

Galera Replication의 가장 큰 특징은 트랜잭션이 커밋되는 시점에 다른 노드에 유효한 트랜잭션인지 여부를 체크하는 방식으로 동작한다는 점입니다. 클러스트 내에서 트랜잭션은 모든 서버에 동시에 반영되거나 전부 반영되지 않는 경우 둘 중 하나입니다.

트랜잭션 커밋이 가능한 여부는 네트워크를 통해서 다른 노드와의 통신에서 결정합니다. 그렇기 때문에 커밋 시 커밋 요청에 대한 응답 시간이 존재하죠. 커밋 요청에 대한 응답 시간은 다음 요소에 영향을 받습니다.

  • 네트워크 왕복 시간
  • 타 노드에서 유효성 체크 시간
  • 각 노드에서 데이터 반영 시간

여기서 재미있는 사실은 트랜잭션을 시작하는 시점(BEGIN)에는 자신의 노드에서는 Pessimistic Locking으로 동작하나, 노드 사이에서는 Optimistic Locking Model로 동작한다는 점입니다. 먼저 트랜잭션을 자신의 노드에 수행을 하고, 커밋을 한 시점에 다른 노드로부터 해당 트랜잭션에 대한 유효성을 받는다는 것이죠. 보통 InnoDB와 같이 트랜잭션을 지원하는 시스템인 경우 SQL이 시작되는 시점에서 Lock 감지를 하나, 여기서는 커밋되는 시점에 노드 간 트랜잭션 유효성 체크를 합니다.

위 그림에서 커밋되는 시점에 마스터 노드에서 슬레이브 노드에 이벤트를 날립니다. 그리고 슬레이브 노드에서 유효성 체크 후 데이터를 정상적으로 반영하게되면 실제 마스터 노드에서 커밋 완료가 되는 것이죠. 그렇지 않은 경우는 롤백 처리됩니다. 이 모든 것은 클러스터 내부에서 동시에 이루어집니다.

4) Galera Replication VS MySQL Replication

CAP 모델 관점에서 본다면 MySQL Replication은 “Availability”과 “Partitioning tolerance”로 동작하지만 Galera Replication에서는  “Consistency”과 “Availability” 로 동작한다는 점에서 차이가 있습니다. MySQL Replication은 데이터 일관성을 보장하지 않음에 반해 Galera Replication은 데이터 일관성을 보장합니다.

C – Consistency (모든 노드의 데이터 일관성 유지)
A – Availability (모든 노드에서 항시 Read/Write이 가능해야 함)
P – Partitioning tolerance (내부 네트워크 단절 시에도 정상적으로 작동해야함)

5) Limitations

현재는 Alpha 버전으로 릴리즈되었고, 추후 안정적인 버전이 나오겠지만, 태생적으로 가지는 한계가 있습니다.

데이터 일관성 유지를 위해서 트랜잭션이 필요한 만큼, 트랜잭션이 기능이 있는 스토리지 엔진에서만 동작합니다. 현재까지는 오직 InnoDB에서만 가능하다고 하네요. MyISAM과 같이 커밋/롤백 개념이 없는 스토리지 엔진은 데이터 복제가 이뤄질 수 없다는 점이죠.

Row 기반으로 데이터 복제가 이루어지기 때문에 반드시 Primary Key가 있어야 합니다. Oracle RAC와 같이 공유 스토리지에서 동일한 데이터 파일을 사용한다면 Rowid가 같으므로 큰 문제가 없겠지만, 물리적으로 스토리지가 독립적인 구조이기 때문이죠. 이것은 기존 MySQL Replication에서도 주의하고 사용해야할 사항이기도 합니다.

최대 가능한 트랜잭션 사이즈는 wsrep_max_ws_rows와 wsrep_max_ws_size에 정의된 설정 값에 제약을 받으며, LOAD DATA INFILE 처리 시 매 1만 건 시 커밋이 자동으로 이루어집니다.

트랜잭션 유효성이 커밋되는 시점에서 이루어지며, 동일한 행에 두 개의 노드에서 데이터 변경을 시도한다면 오직 하나의 노드에서만 데이터 변경이 가능합니다.

또한 원거리 리플리케이션 경우 커밋에 대한 응답 요청으로 인하여 전반적인 시스템 성능 저하가 발생합니다.

Conclusion

MariaDB/Galera Cluster은 전통적인 MySQL Replication이 가지는 가장 큰 문제점이었던 데이터 동기화 지연과 노드 간 트랜잭션 유실 가능에 대한 해결책을 제시합니다.

또한 노드 내부에서는 InnoDB 고유의 트랜잭션으로 동작하고, 실제 커밋이 발생하는 시점에 다른 노드에게 유효성을 체크 및 동시 커밋한다는 점에서 재미있는 방식으로 동작하죠. 결국 기존 MySQL 아키텍트는 그대로 유지하고, Replication 동작에 관한 방법만 수정하여 RDBMS 기반의 분산 DBMS 를 내놨다는 점에서 상당히 흥미로운 제품입니다.

그러나, 동일한 데이터 변경 이슈가 많은 서비스 경우 노드 간 데이터 충돌이 자주 발생 가능성이 있을 것으로 판단됩니다. 데이터 충돌이 발생하여 자주 트랜잭션 롤백이 발생하면 사용자 별로 원활한 서비스 사용이 불가하니, 이에 대한 대책을 어플리케이션 레벨에서 적절하게 고려하여 서비스 설계를 해야 하겠죠. 예를 들어 노드 단위로 주로 변경할 데이터를 나눠서 처리하는 방식으로 서비스 설계가 이뤄져야하지 않을까 생각합니다.

Synchronous 방식으로 노드 간 데이터 복제가 이루어진다는 점에서 아주 반가운 소식이기는 하지만, 기존과 같이 데이터를 설계하면 오히려 서비스 안정성이 크게 떨어질 수도 있다는 점에서 새로운 변화가 예상됩니다.

관련 벤치마크와 안정성 검토가 반드시 필요합니다. 데이터는 거짓말을 하지 않으니..^^

감사합니다.

메모리 기반 RDBMS, MemSQL을 알아봅시다

MemSQL이란?

MemSQL은 디스크 병목을 최소화하기 위해 만들어진 메모리 기반의 관계형 DBMS입니다. 메모리 기반으로 데이터 처리가 이루어지기 때문에, 엄청난 속도로 Read/Write이 가능하며, 기존의 NoSQL 또는 캐시로만 가능했던 퍼포먼스 향상이 있습니다. 실제로 디스크 기반 DBMS 대비 약 30배 이상의 성능 향상이 있다고 하니, 놀라울 따름입니다.
memsql logo

최근 들어 메모리 가격이 하루가 다르게 저렴해지고 있기 때문에 메모리 사이즈를 최대한 늘려서 가격 대비 성능 비를 최대로 이끌어 내는 DB입니다.

MemSQL 특징

1) 강력한 SQL기반의 통신

SQL 기반으로 통신하며, MySQL API를 따릅니다. 그렇기 때문에 기존 MySQL을 사용하는 서비스 경우 로직 변경이 불필요하다고 합니다. 하다못해 라이브러리 또한 기존 MySQL에서 사용하던 그대로 재사용해도 상관 없습니다.

memsql protocol

SQL기반이기 때문에 복잡한 데이터 처리에 유연하며, DB 레이어가 구분되므로 어플리케이션에 영향을 의존적이지 않습니다.

사실 NoSQL이 강력한 성능을 발휘하기는 하지만, DB Schema와 같은 기반 요소가 어플리케이션에 녹아있고 사용이 어렵습니다. 그리고 데이터 처리 시 개발자 개인 역량에 따라 전체적인 성능에 엄청난 영향을 주기도 하죠.

2) 내구성이 좋은 구조

MemSQL은 서버가 예기치 않게 종료가 된다고 하더라도 데이터 유실이 거의 없습니다. 서버 장애가 발생한다고 하더라도 Snapshot으로 특정 시점의 풀 백업 데이터와 WAL(Write Ahead to Log)로 쌓인 데이터 변경 이력을 조합하여 “장애 시점 이전”으로 데이터 버전을 되돌릴 수 있습니다.

memsql data recovery

물론 데이터 영속성이 불필요한 프로세스 경우에는 “durability = off” 옵션으로 비활성할 수 있습니다. 하지만, 이 경우 서버 재시작 후 메모리에 있는 모든 데이터는 소멸되므로 사용 시 주의를 해야겠죠. ^^;;

3) 쉬운 설치 및 사용

MemSQL은 설치가 정말로 쉬우며, 관련 Configure 파일도 단순합니다. 그리고 MySQL 진영에서 개발을 했었다면, 쿼리 사용에도 큰 무리가 없습니다.

4) Transaction 기능

MemSQL은 트랜잭션을 단일 쿼리 레벨에서 Read Committed 레벨로 데이터를 처리합니다. 사용자 레벨로는 Transaction을 제공하지 않기 때문에 복잡한 Transaction이 요구되는 부분에서는 분명 한계가 있습니다.

만약 being, commit, rollback과 같은 구문을 어플리케이션에서 사용한다고 하더라도, Warning 메세지와 함께 DB 내부에서 무시됩니다. 즉, 데이터 변경 질의가 완료되는 바로 그 순간 다른 세션에서도 변경된 데이터를 바로 읽을 수 있습니다.

Repeatable Read(Snapshot Read) Isolation Level은 내부적으로 Database Snapshot을 생성하기 위해 사용되는데, snapshot-trigger-size 임계치에 도달하여 새로운 Snapshot을 생성할 때 사용합니다.

5) 동시성과 퍼포먼스를 위한 인덱스 기능

Hash Index

Hash 인덱스는 1:1로 매칭되는 경우 최상의 성능을 발휘하며, Hash 인덱스를 사용하기 위해서는 인덱스 필드에 Unique 옵션이 있어야 가능합니다. 그리고 Hash 인덱스 생성 시 BUCKET_COUNT 옵션을 별도로 줄 수 있고, 512에서 2^30까지 설정할 수 있는데, 데이터 사이즈에 따라 적절하게 파라메터 값을 지정해야 합니다. 파라메터 값이 너무 작으면, Hash 효율이 크게 저하되고, 반대로 너무 크면 Hash Bucket을 위해 메모리가 불필요하게 많이 사용되기 때문이죠.

예를 들어 파라메터 값이 너무 작으면 빨간 색 그림처럼 한 개의 Hash 타겟에 여러 개의 데이터가 포함되어 있으므로, 원하는 타겟 데이터를 선별하는 부분에서 로드가 발생합니다. 그렇다고 파라메터를 너무 크게 잡으면 파란 그림처럼 메모리 공간만 잡고 아무 것도 포인팅하고 있지 않는 결과가 발생하죠.

memsql hash index

MemSQL측에서는 누적될 수 있는 데이터 최대치 Row 수의 50%로 설정하는 것이 바람직하다고 합니다. 즉, 하나의 Hash키에서는 평균 2개까지 중복되는 것이 성능 및 공간 효율적으로 최적이라는 것을 의미하는 것이죠.

Skip List

B-Tree는 일반적으로 RDBMS에서 제공하는 인덱스 구조이며, MySQL에서도 관련 기능을 제공합니다.

둘의 차이점은 MySQL은 디스크 기반으로 B-Tree를 수행하나, MemSQL은 메모리 기반의 Skip List 구조로 데이터를 찾아냅니다. 데이터 접근 속도가 더욱 빠른 것이죠.

memsql skip list

하단과 같이 인덱스를 ASC 옵션으로 생성된 경우, “Order By r ASC”로 Skip List로 빠르게 데이터에 접근할 수 있습니다. 그러나 “Order By r DESC” 경우에는 Skip List를 사용하지 않기 때문에 상대적으로 속도가 느리다고는 합니다.

물론 DESC 옵션으로 추가적인 인덱스를 생성하면 동일하게 Skip List의 강점을 이용할 수도 있겠지만, 인덱스 또한 공간을 잡아먹는 데이터이기 때문에 사용 상 주의가 필요합니다.

memsql> CREATE TABLE tbl (
     ->   name VARCHAR(20) PRIMARY KEY,
     ->   r INT,
     ->   INDEX i USING BTREE (r ASC)
     -> );
memsql> EXPLAIN SELECT name FROM tbl ORDER BY r ASC;
+-------------+------+------+-------+
| select_type | type | key  | Extra |
+-------------+------+------+-------+
| SIMPLE      | ALL  | i1   |       |
+-------------+------+------+-------+
memsql> EXPLAIN SELECT name FROM tbl ORDER BY r DESC;
+-------------+------+------+-----------------------+
| select_type | type | key  | Extra                 |
+-------------+------+------+-----------------------+
| SIMPLE      | ALL  | i1   | memsql: requires sort |
+-------------+------+------+-----------------------+

6) SQL을 선행 컴파일하여 처리

SQL 실행 순서는 Oracle과 거의 흡사합니다.

Parse → Code Generate → Plan Cache → Execute 단계를 거칩니다. Plan Cache에 적재된 SQL 실행 속도는 빠르지만, 처음 실행되는 SQL은 상당히 느립니다.

처리 단계를 조목조목 설명하겠습니다.

  1. Parse
    쿼리가 유입되면 SQL에 포함된 정수 혹은 문자열 파라메터를 특정 문자로 변환하는 과정입니다.
    예를 들어 SELECT * FROM users WHERE id = ‘gywndi’;라는 쿼리는 SELECT * FROM users WHERE id = @; 와 같이 변환됩니다.
    물론 이 SQL Parsing 단계는 DB에 오버헤드가 거의 없습니다.
  2. Code Generate
    Parsing 단계에서 생성된 SQL을 DB가 쉽게 처리할 수 있는 C++ 기반으로 변환하는 단계입니다.
    DB에서는 원본 SQL이 아닌 데이터를 조회하기에 최적화된 코드에 따라서 데이터를 질의합니다.
    결과는 바로 다음 단계인 Plan Cache 영역에 저장됩니다.
  3. Plan Cache
    전 단계에서 컴파일된 실행 계획을 Plan Cache 메모리 영역에 저장하며, 이후 동일한 타입의 SQL 유입 시 SQL 컴파일 단계 없이 Plan Cache에 있는 정보로 바로 처리합니다.
  4.  Execute
    데이터 추출은 오직 선행 컴파일된 Plan정보로만 수행됩니다.
    이제 SQL을 더이상 순차적으로 읽으며 분석하지 않아도 되기 때문에 마치 NoSQL Solution처럼 혹은 그 이상의 속도를 낼 수 있는 것입니다.

memsql query execute

처음 SQL 질의 요청이 들어오면 Parse → Code Generate → Plan Cache → Execute 단계를 거쳐서 데이터를 뽑아냅니다.
기존에 SQL을 실행하여 이미 Plan Cache에 관련 실행 계획이 저장된 경우에는 Parse → Plan Cache → Execute 단계로 수행을 합니다. 실행 계획 생성 없이 Plan Cache에 저장된 정보로 바로 데이터를 조회할 수 있습니다.

7) Concepts

메모리 기반의 RDBMS인 MemSQL은 두 가지 컨셉으로 만들어졌습니다. Lock이 없는 Hash인덱스와 Skip List를 활용한 B-Tree인덱스를 사용함으로써 일반 CPU에서도 강력한 데이터 처리가 가능합니다. 그리고 MVCC를 제공하기 때문에 엄청난 Writing이 발생함에도 절대 Reading 시 장애가 발생하지 않습니다.

두 가지 특징에 초점을 맞추어 개발을 하였고, 기존 디스크 기반 데이터 처리 속도 대비 30배 이상 빠르다고 합니다.

MemSQL 테스트 후기

Sysbench 툴을 사용하여 벤치마킹을 시도하려 하였으나, DB 특성이 다르다보니 테스트를 할 수 없었습니다. 대신 다른 몇 가지 부분만 짚어서 단일 쿼리 테스트로 진행을 하였고, 테스트 기간 동안 데이터 현황을 한번 살펴보았습니다.

MemSQL Requirement가 일단은 CentOS 6.0 이상이었고, 해당 OS는 제 개인 블로그 서버에만 설치되어 있었기 때문에 어쩔 수 없이 저사양 PC에서 테스트하였습니다. ^^;;

테스트 환경

  • CPU : E-350 (1.6GHz)
  • Memory : 8G
  • Disk : 3.5′ SATAII 5400 rpm Single
  • Data : 100만 건

Sysbench 툴로 100만 건 데이터를 생성하고, 해당 데이터로 간단한 질의 몇개와 기본적인 차이점을 위주로 살펴보았습니다. MemSQL은 물론 메모리 안에 모두 데이터가 존재하며, MySQL 또한 데이터 사이즈가 크지 않기 때문에 메모리에 전체 데이터가 들어있다고 봐도 무관합니다.

memsql performance result

테스트 결과 100만 건 일괄 업데이트에서는 MySQL이 더 빠릅니다. InnoDB Buffer Pool도 결국에는 메모리에 존재하기 때문이 아닐까 합니다. 그리고 업데이트 도중 다른 세션에서는 현재 변경되고 있는 데이터 현황을 확인할 수 없는 것에서 쿼리 단위로 트랜잭션이 보장되는 것을 추측할 수 있었습니다.

MemSQL에서 SQL이 처음 실행되는 순간, 즉 컴파일이 필요한 시점에는 상당히 느립니다. 그러나 동일 타입의 쿼리가 다시 호출되는 경우 MySQL대비 10배 이상 빠른 결과를 보였습니다.

MemSQL에는 Plan Cache가 있는데 이것에 관한 현황은 다음과 같이 조회할 수 있습니다.

memsql> SHOW PLANCACHE\G
************** 1. row **************
     Database: sysbench
    QueryText: select * from sbtest where id = @
      Commits: 113
    Rollbacks: 0
     RowCount: 2606
ExecutionTime: 5
LogBufferTime: NULL
 LogFlushTime: NULL
  RowLockTime: NULL
************** 2. row **************
     Database: sysbench
    QueryText: select k, count(*) from sbtest group by k
      Commits: 7
    Rollbacks: 0
     RowCount: 53
ExecutionTime: 3611
LogBufferTime: NULL
 LogFlushTime: NULL
  RowLockTime: NULL
************** 3. row **************
     Database: sysbench
    QueryText: INSERT INTO sbtest(k, c, pad) VALUES (?,?,?)
      Commits: 100
    Rollbacks: 0
     RowCount: 1000000
ExecutionTime: 19503
LogBufferTime: 0
 LogFlushTime: 0
  RowLockTime: 0

각 정보는 누적된 결과이며, Plan Cache에 들어있으면, 그 이후로는 SQL 실행 시간이 엄청하게 빨라집니다. (1번 쿼리는 평균 0.00005초 미만입니다.)

그러나! 질의를 만드는 도중 몇 가지 제약 사항을 알게 되었는데.. 기존에서 큰 무리가 없던 쿼리 사용이 불가하다는 것입니다. 

간단하게 두 개 정도 쿼리 예를 들겠습니다.

memsql> update sbtest
     -> set k = cast(rand()*100000 as unsigned);
ERROR 1707 (HY000): This query contains constructs not currently supported by MemSQL. The query cannot be executed.

memsql> select count(*)
     -> from (
     ->     select id from sbtest
     ->     where id between 1 and 100
     -> ) t;
ERROR 1707 (HY000): This query contains constructs not currently supported by MemSQL. The query cannot be executed.

Update쿼리에는 함수가 아닌 상수 값이 지정되어야 하며, 위와 같은 타입의 서브쿼리는 지원하지 않습니다. 즉 MySQL에서 MemSQL로 데이터를 이관한다고 하더라도 상당 부분 SQL 변경을 해야할 것 같네요^^;;

위 두 가지 외에도 더욱 많은 제약 사항들이 있을 것 같네요.

Conclusion

트랜잭션을 지원하는 메모리 기반 DBMS이기 때문에 많은 기대를 하였으나, 서비스에 직접적인 투입을 하기에는 아직까지는 한계가 있어 보입니다.풍부한 기능을 가진 MySQL과 테스트에서는 기능적인 요소 뿐만 아니라 편의성 부분에서도 부족합니다.

MySQL 프로토콜을 사용기 때문에 접근성은 타 NoSQL보다는 수훨하나, 기존 MySQL에서 데이터 이관 시 쿼리 또한 상당 부분 변경이 필요합니다. 그리고 Replicaion 기능과 Sharding에 관한 내용을 MemSQL에서는 소개를 하였으나, 활용할 수 있는 메뉴얼을 현재까지 찾을 수 없었습니다. (현재 개발되고 있는 버전이 아닐까 생각이 드네요.)

하지만 메모리 기반이고, NoSQL보다는 접근성이 뛰어나며 단순 Select 테스트에서도 MySQL대비 10배 이상 빠른 성능을 보인 것으로 보아 지속적으로 눈여겨 볼 필요가 있는 제품일 것 같네요. ^^; 

DB 목적을 정확하게 정의하고 데이터 그룹 메타 정보 보관 혹은 해시 성격의 데이터를 관리하는 시스템에서라면.. (SQL 종류가 다양하지 않은 단순 질의) 꽤나 좋은 성능을 보이지 않을까요?

물론 서비스에 투입하기 이전에 DB 안정성 및 데이터 견고성에 관한 보장이 선행되어야겠죠.^^

참고자료 : http://developers.memsql.com/

Tumblr에서는 MySQL로 어떻게 대용량 데이터를 관리하였을까?

Overview

트위터의 새로운 분산 관리 라이브러리 Gizzard를 소개합니다.“를 알아보던 당시 부사수 “임창선”님과 진행했던 또 다른 해외 사례 “Tumblr”를 정리해보았습니다.

Tumblr는 국내에서는 사용자가 많지는 않지만, Twitter 정도의 트래픽을 자랑하는 Micro Blog 서비스입니다. 하루 평규 5억 이상의 PV가 나오고, 초당 4만 건 이상 Request가 나오며, 하루 평균 3TB 이상의 데이터를 쌓는다고 하니 엄청난 서비스인 것은 틀림 없습니다.

이정도 데이터를 관리하기 위해서 수 천대 이상의 서비스를 운영한다고 하는데, 데이터 관리를 MySQL을 활용하여 제공한다고 합니다. 그렇다면 MySQL로 어떻게 대용량 데이터를 멋지게 다뤘을까요?

Tumblr?

데이터 저장에 사용하고 있는 MySQL머신 수는 약 175대 이상(지금은 더욱 많아졌겠죠^^)이며, Master 데이터 용량만 약 11TB 이상(단일 데이터 건수 250억 건 이상)이라고 합니다. 어마어마한 데이터 용량이죠.

1년 동안 Tumblr 상황을 비교한 표입니다. 엄청난 데이터를 약 20명 정도의 인력으로 관리하고 있다니, 감탄을 금치 않을 수 없네요.^^
(정확히 언제 1년 전/후 인지는 파악은 안됩니다. ㅋ)

Tumblr 현황 비교

Shard Automation

Tumblr에서는 Shard를 자동화 구성을 위해 다음같은 것들을 구현하였습니다.

  1. 모든 Shard에 관한 위상 정보 수집
  2. 서버 설정을 조작하기 위한 Unix 명령 및 DB 쿼리 실행
  3. 여러 서버로 대용량 파일을 복사
  4. 임의의 데이터셋에 대한 Import/Export
  5. 단일 Shard를 대중 Shard로 분리할 수 있는 기능

Shard 목표는 다음과 같습니다.

  1. 지나치게 큰 Shard 조각을 새로운 N개의 Shard로 재배치한다.
  2. Lock이 없어야하며 어플리케이션 로직도 필요 없어야 한다.
  3. 5시간 안에 800GB 데이터 파일을 2개로 나누어야 한다.
  4. 전체적으로 서비스에 전혀 지장이 없어야 한다.

Shard 자동화는 다음 원칙하에 이루어집니다.

  1. 모든 테이블은 InnoDB  Storage Engine을 사용한다.
  2. 모든 테이블은 Shard Key로 시작하는 Index를 가진다.
  3. Shard Schema는 Range-based로 한다.
  4. Shard 과정 중 스키마 변경은 없다
  5. 디스크 사용률이 2/3 미만이거나, 별도 디스크가 있어야 한다.
  6. Shard Pool 마다 2 대의 대기 Slave 서버가 존재해야 한다.
  7. Master와 Slave 사이의 균일한  MySQL 설정 한다.
    (log-slave-updates, unique server-id, generic logbin and relay-log names, replication user/grants)
  8. Master Slave 사이에 데이터 동기화에 지연이 있으면 안된다.
  9. 잘못된 Shard에 일시적인 중복된 행은 서비스에 문제가 안된다.

Shard Process

Shard는 다음과 같은 프로세스 대로 자동화 구현합니다.

  1. 기존 Shard Pool에서 새로운 Slave N개 생성
  2. 신규 Slave에 기존 Master 데이터 분할 저장
  3. READ를 신규 분할 Master(신규 Slave)로 이동
  4. Write를 신규 분할 Master(신규 Slave)로 이동
  5. 기존 Master와 신규 Master 서버 간 Replication 끊기
  6. 신규 Master 데이터 보정

1) 기존 Shard Pool에서 새로운 Slave N개 생성

커다란 데이터 파일을 N개의 Slave 서버로 빠르게 재비치 하는 것을 목표로 합니다.

Tumblr Slave Data Clone
Tumblr Slave Data Clone

pigz(Parallel gzip)을 사용 빠르게 압축하고, 그와 동시에 유닉스 명령어인 nc로 신규 Slave에 파일을 바로 전송합니다.

Slave4(신규 장비)에 nc로 포트 개방 및 압축 해제하도록 구성합니다.

[mysql@Slave4~]$ nc -l 10000 | pigz -d | tar xvf -

Slave3(신규 장비)에 tee와 fifo를 활용하여 받음과 동시에 데이터를 Slave4로 보내도록 구성합니다.

[mysql@Slave3~]$ mkfifo myfifo
[mysql@Slave3~]$ nc Slave4 1234 <myfifo &
[mysql@Slave3~]$ nc -l 10000 | tee myfifo | pigz -d | tar xvf -

Slave2(기존 장비)에서 압축 후 nc 로 바로 신규 데이터를 전송하도록 합니다.

[mysql@Slave2~]$ tar cv ./* | pigz | nc Slave3 10000

Slave2 -> Slave3 -> Slave4 로 동시에 순차적으로 데이터가 전송됩니다. 결과적으로 세 대 Slave 장비는 CPU, Memory, Disk, Network 등을 효율적으로 사용하게 되죠.^^ 순차적으로 복사하거나 단일 소스에서 병렬로 복사하는 것보다 훨씬 성능이 좋습니다.

위 그림처럼 텀블러가 Slave 장비를 서비스에 투입하지 않고 Standby 상태로 구성하는 이유는 pigz의 사용시 발생되는 리소스 부하로 인한 서비스의 영향도 때문으로 추측됩니다. 실제 테스트를 해보니 서버 리소스 영향이 있었습니다.

17:16:54  CPU  %user  %nice  %system  %iowait  %idle
17:16:55  all  53.80   0.00     3.55     3.05  39.60
17:16:56  all  67.00   0.00     4.25     2.38  26.38
17:16:57  all  40.11   0.00     2.68     5.24  51.97
17:16:58  all  70.75   0.00     4.36     2.12  22.78
17:16:59  all  63.27   0.00     3.81     3.69  29.23
17:17:00  all  64.57   0.00     4.30     2.06  29.07
17:17:01  all  51.00   0.00     3.62     4.50  40.88
17:17:02  all  57.96   0.00     4.37     2.87  34.79

2) 신규 Slave에 기존 Master 데이터 분할 저장

신규 Slave에 기존 데이터를 N개로 나눠서 저장합니다.

신규 Slave 데이터 분할 저장

그런데 데이터를 분할하는 방식이 참으로 재미 있습니다.

먼저 “Select .. Into Outfile”로 데이터를 추출하고, 테이블을 Drop 및 Create 한 후 추출한 데이터를 “Load Data Infile”로 다시 넣는 것입니다.

과연 무엇이 더 빠른 것인지 판단이 정확하게 서지는 않지만, Tumblr에서는 기존 데이터를 날리고 Bulk Insert 하는 것이 훨씬 빠르다고 판단한 것 같습니다.

Load Data가 정상적으로 마무리되면, 신규 Slave 밑에 각각 두 개 Slave를 붙입니다. 아래 그림과 같은 형상이 되겠죠.^^

Slave 최종 형상

아래는 데이터를 Export/Import 시 주의할 사항이라고 합니다.

  1. import 속도를 위해 바이너리 로그를 비활성화 한다.
  2. 쿼리를 Kill하는 스크립트는 사용하지 않는다.
    (SELECT … INTO OUTFILE 과 LOAD DATA INFILE는 KILL하지 않도록 함)
  3. 어느 정도 import/export가 가능하지를 벤치마크하여 파악한다.
  4. 속도가 다른 디스크 여러 개를 동시 사용하는 경우 디스크 I/O 스케줄러 선택을 주의한다.

한 가지 의문이 가는 사항은 정상적인 Replication 상태라면 분명 신규 Slave에서 Replication Fail이 발생했을 것 같다는 것입니다. 아무래도 Slave 서버에서 모든 Error를 무시하는 설정을 해놨다는 생각이 드네요.

또한 Binlog Log 는 아무래도 Row 포멧이 위와 같은 경우에서는 조금 더 유리하지 않을까요? 그냥 추측 두 가지를 해봅니다.^^

3) READ를 신규 분할 Master(신규 Slave)로 이동

어플리케이션에서 READ를 기존  Master에서 신규 Slave(앞으로 Master로 구성될 서버)로 이동을 하여 서비스를 진행합니다.

Move Read to New Slave

만약 어플리케이션 서버에서 업데이트가 동시에 완료되지 않은 상태에서 갑자기 Read/Write 포인트가 이동한다면, 데이터 일관성 문제가 발생할 것입니다.

동시에 Write까지 이동되었다고 가정해봅시다. A가 새로운 DB 형상을 받고 200 게시물을 작성하였으나, B는 여전히 예전 형상을 바라보고 기존 Master 서버에서 데이터를 읽어오면 게시물을 찾을 수 없겠죠.

그렇기 때문에 기존 Master와 데이터 복제를 유지하면서 Read 포인트만 먼저 이동하는 것입니다.

4) Write를 신규 분할 Master(신규 Slave)로 이동

모든 DB 구성에 관한 형상이 업데이트 된 후에 Write 포인트를 변경합니다. 옮긴 후에도 기존 Master DB의 바이너리 로그 기록이 정지하기 전까지는 절대 기존 Master/Slave 구성에 손을 대면 안되겠죠.^^

Move Write to New Slave

5) 기존 Master와 신규 Master 서버 간 Replication 끊기

기존 Master의 바이너리 로그 기록이 중지되었다면, 이제 필요없는 DB들을 제거하도록 합니다.

Remove Previous DB Servers

이제 모든 데이터는 새로운 Slave 아니 새로운 Master에서 Read/Write 서비스되겠죠. ^^

6) 신규 Master 데이터 보정

분할된 Shard된 조각에서 잘못 복제된 데이터를 제거하는 작업이 필요합니다.

그러나 절대적으로 하나의 커다란 단위의 Delete 작업은 피하도록 합니다. Master/Slave 간 데이터 동기화 지연을 최소화하기 위한 방안이죠.

그러므로 Delete 작업도 조금씩 끊어서 수행하도록 유도합니다.

Conclusion

정리를 하자면, “서비스에 투입하지 않은 Slave에서 빠르게 복제 서버를 두 대 추가하고, Master 데이터를 각각 반으로 나눠서 분리 저장한다” 고 보면 되겠습니다. Tumblr에서는 내부적으로 조금더 상세한 내용을 공유하지 않았지만, 적어도 데이터를 Shard하는 동안에는 서비스 Downtime이 없다는 것입니다.

Scale-Out 방안이 다양하지 않은 MySQL에서 이러한 방식으로 신속하게 Scale Out할 수 있었던 기법에서 많은 점을 배운 것 같네요.^^

대용량 데이터 관리를 위한 어플리케이션 개발도 필요하겠지만, 전체적인 데이터 흐름을 아는 것이 무엇보다 중요하다고 생각합니다.

<참고 사이트>
Tumblr – Massively Sharded MySQL
Efficiently copying files to multiple destinations
Tumblr Architecture – 15 Billion Page Views A Month And Harder To Scale Than Twitter