Maria 2탄 – 진화하는 Maria, 함께하는 MySQL!!

Overview

MySQL 오픈 소스 진영은 더이상 단순 데이터 처리에만 강한 DBMS이기를 거부합니다. 이제는 대용량 처리에 적합하도록 탈바꿈 중입니다.

지금까지 MySQL에서는 단일 쓰레드로 Nested Loop 방식으로 쿼리를 처리하였기 때문에, 조인 건 수가 대형화될 수록 성능이 급속도로 악화되었습니다.

MariaDB는 5.3버전부터 DB 엔진과 스토리지 엔진 간의 데이터 전송이 개선되었고, 조인 시 추가적인 블록 기반의 조인 알고리즘을 제공합니다. 물론 MySQL도 5.6버전부터는 관련 기능을 어느정도 지원합니다.

변화하는 MariaDB에 대해 몇 가지 소개하도록 하겠습니다.

Disk access optimization

1) Index Condition Pushdown

MySQL/MariaDB는 구조적으로 DB 엔진과 스토리지 엔진 역할이 명확하게 구분됩니다.

DB 엔진은 데이터를 처리하여 클라이언트에게 전달하고, 스토리지 엔진은 물리적 장치에서 읽어와 DB 엔진에 전달합니다.

이런 구조이기 때문에 다양한 스토리지 엔진을 가질 수 있다는 확장성이 있지만, 그에 따라 내부적인 비효율이 발생하기도 합니다.

다음과 같은 SQL이 호출된다고 가정합니다. tbl 테이블에는 (key_col1, key_col2)로 인덱스가 구성되어 있습니다.

select * from tbl 
where key_col1 between 10 and 11
and key_col2 like '%foo%';

여기서 데이터를 스토리지 엔진에 전달할 때는 key_col1에 해당하는 조건만 전달할 수 있습니다. key_col2는 문자열 패턴 검색이므로 인덱스 사용에서는 무의미하기 때문이죠.

만약 key_col1 의 between 조건 결과가 100만 건이라고 가정하면, 스토리지 엔진으로 부터 대상 데이터 100만 건을 모두 DB 엔진으로 가져와서 key_col2 유효성을 체크합니다. 그렇기에 스토리지 엔진에서 DB엔진으로 데이터를 전송하는 “Sending Data”에서 비효율이 발생하기도 합니다.

MariaDB None Index Condition Pushdown
MariaDB None Index Condition Pushdown

그러나 MariaDB 5.3.3부터 Index Condition Pushdown 기능이 추가되면서, 인덱스 데이터 구조를 활용하여 한번 더 필터링하여 필요한 데이터만 테이블 데이터에서 읽고 DB 엔진에 전달합니다.

MariaDB Index Condition Pushdown
MariaDB Index Condition Pushdown
위 그림에서는 앞선 그림과는 다르게 오직 한 건만 DB 엔진에 전달합니다. 불필요한 데이터를 DB엔진에 전달하지 않기 때문에 퍼포먼스가 크게 향상되겠죠. (간단한 테스트에서는, 1분이 넘던 쿼리가 1초 내로 처리되었습니다.^^)

단, Index Condition Pushdown 기능이 동작하기 위해서는 위와 같이 “조건을 포함하는 형식”으로 인덱스가 구성이 되어 있어야 합니다.

2) Multi-Record Read

디스크는 데이터를 읽어오는 구조 상 Random Access에 성능이 취약합니다. 데이터를 읽어들이기 위해서는 헤더를 끊임없이 움직여야 하기 때문이죠.

MariaDB에서는 효과적으로 데이터를 긁어오기 위해서 Multi-Record Read 기능을 제공합니다. 필요한 데이터를 Rowid를 기준으로 정렬하여 디스크에 데이터를 요청합니다. Rowid로 데이터가 정렬되었기 때문에 디스크는 Sequential하게 데이터를 읽어오죠. 즉 데이터를 읽기 위해 과도하게 헤더가 움직이지 않아도 된다는 것을 의미합니다.

Multi-Record Read를 간단하게 그림으로 표현하겠습니다.

MariaDB Multi-Record Read
MariaDB Multi-Record Read

인덱스 구조로부터 키가 1,2,4,6,7에 해당하는 결과를 가져와서, 이를 다시 Rowid 기준으로 정렬을 합니다.

그리고 Rowid 기준으로 실제 스토리지 엔진에 데이터를 요청하게 되는데, Rowid 순으로 접근하는 경우 디스크에서 Random Access가 최소화됩니다.

위 그림은 MyISAM 기준이며, InnoDB인 경우 Rowid 역할을 하는 Primary Key 순으로 재정렬하여 데이터를 효과적으로 가져오겠죠.^^

Join Buffer

MariaDB 5.3부터는 조인 버퍼를 기존보다 더욱 효율적으로 사용합니다.

가변형 데이터 타입(Varchar) 경우 최대 문자열보다 부족한 부분에 \0 문자로 채우지 않고, Null 필드 경우 조인 버퍼에 적재를 하지 않고 데이터를 처리합니다. 즉 조인 버퍼 사용 효율이 증대하는 것이죠

Inner Join에서만 사용하던 조인 버퍼를 이제는 Outer Join과 Semi Join에서도 사용할 수 있도록 기능이 개선되었습니다.

1) Incremental Join Buffer

조인 버퍼를 더욱 더 효율적으로 사용하기 위한 새로운 접근입니다.

테이블A, 테이블B, 테이블C 등 세 개의 테이블을 조인하는 경우에는 두 개의 조인 버퍼를 내부적으로 사용합니다.

첫번째 조인 버퍼(테이블A과 테이블B 사이의 조인 버퍼)은 테이블A의 레코드 값을 임시로 저장하고 테이블B와 비교하기 위한 용도로 사용됩니다.

두번째 조인버퍼(“테이블A과 테이블B 결과”와 테이블C 사이의 조인 버퍼)는 앞선 결과 값과 테이블 C 조인을 위해 임시로 데이터를 저장하는 용도로 사용됩니다. 기존까지는 “테이블A와 테이블B 결과”를 “Copy”하면서 두번째 조인 버퍼에 적재하였습니다. 여기서 메모리에는 이중으로 데이터가 적재되는 현상이 발생하고, 비효율 현상이 발생하는 것이죠.

MariaDB incremental join buffer
MariaDB incremental join buffer

그러나 Incremental join buffer 방식에서는 데이터를 복사하지 않고, 위 그림과 같이 테이블A와 테이블B 결과가 저장된 임시 공간에 접근할 수 있는 “포인터” 값만 조인 버퍼에 저장합니다.

즉, “불필요한 데이터 Copy를 제거”하면서 메모리 공간을 더욱 효율적으로 활용할 수 있는 것이죠.

2) Join Buffer with Outer-Join/Semi-Join

MariaDB5.3부터는 Inner-Join 뿐만 아니라 Outer-Join과 Semi-Join에서도 조인 버퍼를 활용합니다.

Outer-Join에서는 조인 버퍼 내부에 “매칭 플래그”, 즉 테이블A가 기준 테이블인 경우 관련 데이터와 매칭되는 여부를 체크하는 플래그가 내부적으로 포함됩니다.

기본적으로 매칭 플래그는 OFF 값으로 세팅되어 있고, 테이블B에서 일치하는 데이터를 찾으면 플래그를 ON으로 변경합니다.

조인 버퍼에서 테이블A와 테이블B 간 데이터 매칭 여부 수행 이후 여전히 OFF값을 플래그로 가지는 필드인 경우, 테이블B에 해당하는 칼럼들은 NULL로 채웁니다.

MariDB Join Buffer with Outer-Join/Semi-Join
MariDB Join Buffer with Outer-Join/Semi-Join

Semi-Join(IN 안의 서브쿼리와 같은 조건)에서도 매칭 플래그가 비슷하게 사용됩니다.

다만 매칭 플래그가 On이 되는 시점에서 관련 데이터를 테이블B에서 더이상 탐색하지 않는다는 점에서 차이점이 있습니다.

Block Based Join Algorithm

1) Block Nested Loop Join

블록 기반의 조인 알고리즘을 소개하기에 앞서, Block Nested Loop Join에 대해 설명하도록 하겠습니다.

테이블A와 테이블B이 있는 상태에서 다음 SQL이 호출된다고 가정합니다.

Select a.r1, b.r2
From TABEL_A a
Inner Join TABLE_B On a.r1 = b.r2
MariaDB Block Nested Loop Join
MariaDB Block Nested Loop Join

테이블A로부터 읽어오면서 조인버퍼가 가득 찰 때까지 채웁니다. 여기서는 연두색 사각형이 조인 버퍼를 가득 채우는 데이터라고 보면 되겠습니다.

조인 버퍼가 가득 채워지면, 테이블B를 스캔하면서 조인 버퍼에 있는 데이터와 매칭되는지 하나하나 체크하고, 매칭되면 조인 결과로 내보냅니다.

조인 버퍼 안의 모든 데이터를 비교하는 과정이 끝나면, 조인 버퍼를 비우고 다시 앞선 과정을 수행합니다. 여기서는 노란 색 사각형 부분입니다.

이러한 과정을 테이블A에서 조인 버퍼에 더이상 데이터를 채울 수 없는 시점, 즉 테이블A 조건에 해당하는 데이터를 모두 처리할 때까지 반복 수행합니다. 여기서 테이블B를 스캔하는 횟수는 조인 버퍼에 데이터가 적재되는 횟수와 동일합니다. 그리고 테이블B 데이터를 스캔할 때는 Full table scan, Full index scan, Range index scan 등으로 데이터에 접근합니다.

2) Block Hash Join

Block Hash Join은 MariaDB 5.3부터 제공하는 새로운 조인 알고리즘입니다.

이 알고리즘은 테이블 간 조인을 동등 비교 시에서 사용됩니다.

다른 조인 알고리즘과 마찬가지로, Block Hash Join에서도 조인 버퍼를 사용하여 테이블 간의 연관성을 체크하지만, 조인 버퍼를 사용하는 방식에서는 약간 다릅니다.

MariaDB Block Hash Join
MariaDB Block Hash Join

테이블A에서 데이터를 읽어와 조인 버퍼에 밀어 넣을 때, 테이블A 조건에 해당하는 해시 값을 내부적으로 생성하고 조인 버퍼에 저장 합니다.

그리고 테이블B에서 조건을 해시값을 통하여 직접 데이터 매칭 여부를 결정하고 결과셋을 생성합니다. 즉 Nested Loop 조인 방식에서는 데이터에 순차적으로 접근해야 하는 것과는 커다란 차이가 있습니다.

조인 버퍼에 별도로 해시 값을 추가 저장하기 때문에, 기존 Block Nested Loop 방식보다는 조인 버퍼에 저장되는 데이터 양이 적으나, 테이블A가 작을수록 혹은 조인 버퍼에 저장되는 데이터 가지 수가 작을 수록 상당한 퍼포먼스를 발휘합니다.

3) Batched Key Access Join

기존의 Block Nested Join에서는 대용량 테이블과의 조인에서는 성능이 크게 떨어질 수밖에 없습니다.

테이블 조인 시 랜덤 Access가 발생하기 때문이죠. 그나마 인덱스를 생성하여 차선책으로 해결할 수는 있겠지만, 완벽한 대안은 아닐 것입니다.

Batched Key Access 조인은 랜덤 Access를 최대한 줄이려는 목적으로 고안된 알고리즘으로, 조인 대상이 되는 데이터를 “미리 예측”함과 동시에 디스크에 저장된 순서대로 데이터를 가져와서 “디스크 접근 효율”을 최대로 늘리자는 데 있습니다.

MariaDB Batched Key Access Join

기본적인 Batched Key Access 조인은 다음과 같습니다.

다른 Block Based Join 알고리즘처럼, Batched Key Access 조인도 첫번 째 피연산자의 레코드 값을 조인 버퍼에 채웁니다.

그리고 조인 버퍼가 다시 채워지면 조인 버퍼 안에 있는 레코드와 매칭이 될 수 있는 값을 조인 테이블로부터 “미리” 찾아냅니다.

MariaDB Batched Key Access Join
MariaDB Batched Key Access Join

조인 버퍼 안에 있는 레코드와 매칭이 될 수 있는 값을 미리 찾아내기 위해서 Multi-Record Read 인터페이스를 호출합니다.

Multi-Record Read는 조인 버퍼 안의 모든 레코드로 구성된 키 값들로 테이블B와 연관된 인덱스 룩업을 수행하고, 테이블B의 레코드를 빠르게 가져오기 위해 Rowid 순으로 데이터를 검색 합니다. 자세한 내용은 상단에 설명되어 있습니다. ^^

그리고 조인 버퍼의 레코드와 “미리 가져온” 테이블B의 데이터를 비교하여 조인 조건이 맞는지를 체크하고 최종적으로 결과값으로 출력하는 것이죠.

Conclusion

물론 위에서 소개한 기능은 대부분 상용 DBMS에서 구현되어 있습니다. 그리고 그동안은 MySQL DB 엔진 태생적인 문제로 단순 데이터 처리 혹은 작은 데이터 조각만을 취급하는 소규모 DBMS로 인식되어 왔던 것이죠. 또한 옵티마이저 기능이 여전히 좋지 않기 때문에, 쿼리 작성 시에도 상당한 노력을 기울여야 최상의 퍼포먼스가 나옵니다.

하지만, 점차적으로 기능이 개선됨에 따라 MariaDB혹은 MySQL을 통해서도 얼마든지 어느정도의 대용량 데이터를 처리할 수 있는 모습으로 변모하고 있습니다.

더이상 DB 태생적인 한계점이 사라진다는 점에서 앞으로 MySQL 오픈소스 진영의 다음 행보가 상당히 기대됩니다.

<참고자료>
http://kb.askmonty.org/en/what-is-mariadb-53/
http://kb.askmonty.org/en/index-condition-pushdown/
http://kb.askmonty.org/en/block-based-join-algorithms/#batch-key-access-join
http://kb.askmonty.org/en/multi-range-read-optimization/
http://assets.en.oreilly.com/1/event/2/Batched%20Key%20Access_%20a%20Significant%20Speed-up%20for%20Join%20Queries%20Presentation.ppt

MySQL에서 Replication Driver 사용 시 장애 취약점 리포트

Overview

MySQL에서 슬레이브 부하 분산을 하는 방안으로 Replication Driver 기능을 제공하는 jdbc 내부적으로 지원합니다. Replication Driver를 사용하면 상당히 간단하게 마스터/슬레이브 활용을 할 수 있고 어느정도의 Failover는 가능합니다.

하지만 서비스 적용을 위해 Failover테스트 도중 치명적인 문제점이 발생하였습니다. 관련 포스팅을 하도록 하겠습니다. ^^

사용 방법

Replication Driver 사용 시 ReadOnly 옵션을 True/False 상태에 따라 마스터/슬레이브 장비를 선택합니다.

아래 그림처럼 ReadOnly이 False이면 마스터 장비에 쿼리를 날리고, True이면 슬레이브에 쿼리를 날리는 구조입니다. 그리고 로드발란싱 기능을 사용하면, 슬레이브 서버 부하 분산할 수 있습니다.

MySQL Replication Driver

Oracle에서 제시한 Replication Driver 사용 방법입니다.

import java.sql.Connection;
import java.sql.ResultSet;
import java.util.Properties;

import com.mysql.jdbc.ReplicationDriver;

public class ReplicationDriverDemo {

  public static void main(String[] args) throws Exception {
    ReplicationDriver driver = new ReplicationDriver();

    Properties props = new Properties();

    // 로드발란싱 옵션 설정
    props.put("autoReconnect", "true");
    props.put("roundRobinLoadBalance", "true");

    // 접속 정보 설정
    props.put("user", "foo");
    props.put("password", "bar");

    Connection conn = driver.connect("jdbc:mysql:replication://master,slave1,slave2,slave3/test", props);
    conn.setAutoCommit(false);

    // 마스터 접근
    conn.setReadOnly(false);
    conn.createStatement().executeUpdate("UPDATE alt_table SET a = 1;");
    conn.commit();

    // 슬레이브 접근
    conn.setReadOnly(true);
    ResultSet rs = conn.createStatement().executeQuery("SELECT a, b FROM alt_table");
  }
}

참고자료: http://dev.mysql.com/doc/refman/5.5/en/connector-j-reference-replication-connection.html

기능 테스트

실제 상용 서비스 투입을 위해 여러 장애 상황을 구현해보았으며, Replication Driver 동작 상태에 관해 살펴보았습니다.

결론적으로 말하면, 상용 서비스에서는 사용해서는 안될 취약점을 있었습니다.

특히 Connection Pool 환경에서는 말도 안되는 현상이죠.

Case 1 – 슬레이브장비가 죽은 경우

슬레이브 장비 중 하나 가 죽은 경우에는 Replication Driver에서 자동으로 감지하고 다른 슬레이브로 데이터 요청 쿼리를 보냅니다. DB 접속을 공인 아이피로 통신할 지라도 접속 지연 현상은 발생하지 않습니다. 아마도 내부적으로 ICMP 프로토콜 통신을 하며 주기적으로 슬레이브 장비 구동 유무를 체크하는 듯 합니다.^^;;

MySQL Replication Driver Failover1

Case 2 – 슬레이브가 모두 죽은 경우

Master와만 통신이 가능할 뿐 슬레이브와는 통신이 불가(당연한 이야기겠지만)합니다. 하지만 슬레이브 DB가 정상적으로 돌아오게 되면 자동으로 상태가 복구됩니다.

MySQL Replication Driver Failover2

Case 3 – 마스터가 죽은 경우

말도 안되는 장애 현상이 발생했습니다. 마스터가 죽은 경우에는 아래와 같이 슬레이브가 멀쩡하게 살아있는데도 데이터를 읽을 수가 없습니다. 일반적인 서버 데이터 작업이라면, 마스터가 죽어도 슬레이브 서버에서 읽기가 가능해야 한 상태, 즉 트랜잭션이 불필요한 READ 로직은 멀쩡해야 합니다. 그러나, 마스터가 죽으면 슬레이브에서도 데이터를 읽어올 수 없기 때문에 결과적으로 전체 서비스 마비가 발생합니다.

MySQL Replication Driver Failover3

하지만, 더욱 더 황당한 상황이 벌어졌습니다.

Connection을 공유(Connection Pool 사용 시)하는 경우 장애가 발생했던 마스터 서버를 복구하였다 할 지라도, 서비스가 정상적으로 돌아오지 않는다는 것입니다. Reconnect 옵션이 활성화되어 있어도 Connection은 정상적으로 돌아오지 않았으며, Connection을 다시 맺어줘야 정상적으로 돌아옵니다. Connection을 공유하는 상황에서 Connection을 다시 맺는 것은 곧 어플리케이션을 재시작하는 것과 거의 비슷한 상황이라고 볼 수 있겠죠. (물론 프로그램적으로 풀 수도 있겠지만, jdbc driver 외적인 요소로 여기서 언급할 필요는 없다고 생각합니다.)

MySQL Replication Driver Failover4

아래는 Connection Pool 환경을 구현하기 위해 부끄럽지만, 간단하게 작성한 JAVA 소스입니다.

import java.sql.*;
import java.util.Properties;
import com.mysql.jdbc.ReplicationDriver;
public class MysqlReplicationConnection  extends Thread{
    public static void main(String[] args) throws Exception {
        long currentMiliSecond = System.currentTimeMillis();

        ReplicationDriver driver = new ReplicationDriver();
        Connection conn = null;
        ResultSet rs = null;
        Statement stmt = null;
        Properties props = new Properties();
        props.put("autoReconnect", "yes");
        props.put("maxReconnects", "1");
        props.put("autoReconnectForPools", "true");
        props.put("roundRobinLoadBalance", "true");
        props.put("user", "dba");
        props.put("password", "");
        conn = driver.connect("jdbc:mysql:replication://master:3306,slave1:3306,slave2:3306/dbatest", props);

        while(true){
            try{                
                /*************************************
                * Read Test - Slave
                * *************************************/
                conn.setAutoCommit(false);
                conn.setReadOnly(true);
                stmt = conn.createStatement();

                // Print Server Hostname
                rs = stmt.executeQuery("show variables like 'hostname';");
                while(rs.next()){
                    System.out.print("[Select]"+rs.getString(1)+" : "+rs.getString(2));
                }
                rs.close();

                rs = stmt.executeQuery("select count(*) from test;");
                while(rs.next()){
                    System.out.println(" : "+rs.getInt(1));
                }
                rs.close();

                System.out.println("[Time]=========["+(System.currentTimeMillis()-currentMiliSecond-1000)+"]=========");
                currentMiliSecond = System.currentTimeMillis();

                conn.commit();
                stmt.close();

                /*************************************
                * Write Test
                * *************************************/
                conn.setReadOnly(false);

                stmt = conn.createStatement();
                stmt.executeUpdate("insert into test (j) values ('1');");
                rs = stmt.executeQuery("show variables like 'hostname';");
                while(rs.next()){
                    System.out.println("[Insert]"+rs.getString(1)+" : "+rs.getString(2));
                }
                conn.commit();
                rs.close();
                stmt.close();
            }catch(Exception e){
                System.out.println(e);
                //try{conn.close();} catch (Exception sqlEx) {}
                //try{conn = driver.connect("jdbc:mysql:replication://kthdba02:3306,kthdba03:3306,kthdba04:3306/dbatest", props);} catch (Exception sqlEx) {}
            }

            // Sleep 1 Second
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

위와 같은 문제는 Replication Driver를 사용 시에만 발생할 뿐, jdbc로 직접적인 Connection을 맺는 경우에는 발생하지 않습니다. 물론 슬레이브 접속을 다음과 같이 별도 로드발란싱하는 형식으로 Connection으로 수행해도 관련 문제는 발생하지 않습니다.

jdbc:mysql:loadbalance://slave1-pri,slave2-pri/dbatest?loadBalanceConnectionGroup=conn&loadBalanceEnableJMX=true

Commit/Rollback이 명시적으로 호출되면 다른 서버와 번갈아가며 쿼리가 질의되며, 슬레이브 중 한 대가 죽어도 정상적으로 동작합니다. 쿼리 결과 예외 상황 발생 시 해당 쿼리를 다른 쪽으로 다시 질의하는 방식으로 이루어집니다. 단, 공인아이피로 하는 경우 장애 발생 서버로 쿼리 질의 후 예외 상황 인지 시까지 상당한 시간이 걸릴 수 있으므로 반드시 사설 아이피 대역으로 통신하시기 바랍니다.

기술 지원 그 후..

주요 서비스 몇 개에 Oracle로부터 기술 지원을 받기 위해 유지보수 계약을 체결하였기에, 관련 문제 상황에 대해서 SR을 진행하였습니다. 2주 정도 관련 문제로 시달렸으며, 개인적인 생각으로는 불필요한 로그도 상당히 요청하고 엉뚱한 부분만 자꾸 지적하였습니다.

기술 지원 요청

아래와 같이 Oracle로 기술지원 요청을 하였습니다. 그리고 전화 상으로 Connection을 공유하는 상황이라고 설명을 하였습니다.

문제 설명: jdbc에서 ReplicationDriver 사용 중 Master 장비에서 장애가 난 경우 Master 복구 이후에 Reconnect이 되지 않는 현상이 있습니다.
ReplicationDriver에 전달하는 Properties 정보는 하단과 같습니다.

Properties props = new Properties();
props.put("autoReconnect", "yes");
props.put("failOverReadOnly", "yes");
props.put("roundRobinLoadBalance", "true");

Slave 특정 노드 장애 시 정상적으로 다른 노드에서 select가 일어나나, 문제는 마스터 장애 시에는 "com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure" 메시지와 함께 장애 현상이 발생합니다.
Slave에서 Select도 불가할 뿐만 아니라, Master가 정상적으로 재 구동되어도 기능은 정상적으로 돌아오지 않습니다. 즉 Application을 재시작해야 비로서 서비스가 정상화됩니다.

관련 무슨 문제가 있는지, 정상적인 현상인지 혹은 버그 문제인지 확인 부탁 드립니다.

기술 지원 결과

Replication Driver 사용 시 발생했던 모~든 문제는 의도된 결과로 소스 수정이 불가하다는 답변을 받았습니다. 관련 샘플 JAVA 프로그램을 받았으나, Connection을 공유하는 상황이 아닌, 매번 직접 DB로부터 Connection을 맺는 방식이었습니다. (관련 첨부를 다시 다운받으려고 들어갔으나, 현재 받을 수가 없네요)

앞서 드렸던 답변에서와 같이, replication driver 의 이같은 동작은 의도된 동작입니다.

2012-06-27 09:34 AM
> I consulted the developers and they said that this behavior is made for purpose.

2012-06-28 08:43 AM
> The autoReconnect will restore connections to the slaves, but not to the master.
=> 고객님께서 생각하고 계신 autoReconnect 는 master 가 down 된 이후 복구되었을 때 다시 master로 reconnect 하는 것이 아니라, connection 을 slave 로 restore 하기 위한 것입니다.

replication driver 의 이와 같은 동작은 의도된 것이며, autoReconnect 는 master 로 reconnect 하기 위한 paratmeter 가 아닙니다.
고객님께서 driver 문제로 판단하시는 이와 같은 구현방식에 불편함이 있으실 수 있습니다만, driver 수정은 어려울 것으로 보입니다.

Review 후 SR update 바랍니다.

감사합니다.

Conclusion

MySQL에서 Replication Driver를 사용하면 쉽게 마스터/슬레이브 서버 접속이 가능하나, 마스터 장애시 엄청난 장애가 발생할 수 있는 잠재적인 포인트가 있습니다.

MySQL이 SUN을 인수하면서 부록(?)으로 따라온 오픈소스일지라도, 일단은 기술 지원을 하는 만큼은 제대로 응대를 해줘야하는 것이 아닐까요? 고객 계약에만 혈안되어 제재 가하기 보다는.. -_-;; 다시는 오라클로부터 어떠한 답변도 기대하지 않을 것을 다짐했습니다.

서비스 부하 분산을 위해 슬레이브에서 로드발란싱이 필요한 경우에는 다음과 같이 “jdbc:mysql:loadbalance://db1,db2..” 형식으로 사용하시기 바랍니다. 단 “사설 아이피”로 접속을 해야 장애 발생 시에도 유연하게 서비스가 동작합니다.

혹은 개인적으로 jdbc를 일부 수정하는 것도 좋은 방안입니다. (내부 소스를 까보니 크게 수정이 어려울 것 같지는 않습니다. ^^)

이미 Replication Driver를 사용하고 있다면, 관련 문제점을 반드시 진단하시기 바랍니다. ^^

메모리 기반 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/