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를 사용하고 있다면, 관련 문제점을 반드시 진단하시기 바랍니다. ^^

MySQL Replication 이해(3) – 활용

Overview

MySQL Replication 시리즈 마지막 3탄, “활용”에 관한 포스트입니다. 앞 선 시리즈 MySQL Replication 이해(1) – 개념MySQL Replication 이해(2) – 구성)에서 기본적인 개념과 구성을 다뤘다면, 이 자리에서는 실제적으로 어떤 분야에 활용할 수 있는지 설명드리겠습니다.

  1. Scale Out
  2. High Availability
  3. Data Partitioning
자, 그럼 시작해볼까요?

Scale out

MySQL Replication이 가장 많이 활용되는 분야입니다.
MySQL Replication은 READ관련 Scale out만 가능합니다. 만약 WRITE 이슈가 있다면, MySQL 레벨에서는 Scale out이 불가합니다. 특히나 Replication 운영 시 마스터 트래픽이 과도하게 발생하면, Master와 Slave 간 데이터 동기화 지연 현상이 발생합니다. “반드시 알아야할 MySQL 특징 세 가지” 내용을 읽어보시면 이해가 조금더 수훨하겠네요.^^

MySQL Replication Scale Out
MySQL Replication Scale Out

WRITE 관련 Scale out이 불가하다고 했었는데, 전혀 불가능한 것일까요? 그렇지 않아요~! 서버 구성을 적절하게 재배치한다면 WRITE 분산도 일부 가능합니다.

  • 다중 마스터 구성 (하단 High Availability 참고)
    기본적으로 MySQL에서는 다중으로 마스터를 구성할 수 없습니다. 각 슬레이브들은 오직 하나의 슬레이브만 가질 수 있습니다.
  • 피라미드형  구성 (하단 Data Partitioning 참고)
    역할에 따라서 서버를 재배치하는 방식입니다. 모든 서버가 가져야할 데이터 공유는 최상위 마스터에서 담당하고, 중간 슬레이브는 자신이 맡은 역할에 맞는 마스터 역할을 하는 것이죠.

High Availability

MySQL Replication 을 높은 가용성 구현을 위해서 사용할 수 있습니다.  아래와 같이 가상 아이피(Virtual IP)를 통해서 App서버들이 서비스를 제공하고 있다면 마스터 장비 장애 발생 시자동으로 Virtual IP가 유휴 슬레이브 장비로 Virtual IP를 넘겨서 장애를 빠르게 대비할 수 있습니다.

High Availability
High Availability

일단 슬레이브로 마스터 역할이 넘어간 시점부터는 “현재 데이터의 기준”은 “신규 마스터”이어야 합니다. 데이터가 비동기적으로 복제되는 구조이기 때문에, 장애 후 IP가 넘어가는 일시적인 시점 동안 트랜잭션 유실은 발생할 수 있다는 점 있지 마세요^^

서버 한 대 효율을 조금 더 올리고자 한다면, 아래와 같이 구성해보는 것은 어떨까요?

High Availability : Multi-Master
High Availability : Multi-Master

문제는 동일 데이터 변경에 관한 이슈인데, 이것은 Service1 과 Service2 데이터베이스를 물리적으로 분리하시면 됩니다.

Data Partitioning

분명 MySQL Replication에서 Slave는 하나의 Thread로만 SQL을 실행하기 때문에, 서버 간 동기화 지연 현상이 발생합니다. 하지만 서버 구성을 조금만 변경한다면 어느정도는 해결할 수 있습니다.

Master Scale-out
Master Scale-out

Replicate_Do_DB 혹은 Replicate_Do_Table 옵션을 사용하여, 실제로 적용할 객체들만 선별적으로 동기화하는 것입니다. 서비스 단위로 기능을 나눌 수도 있고, 역할 별로 기능을 나눌 수 있습니다.

$ vi /etc/my.cnf
replicate-do-db=common

단, 서버 확장을 고려하여, 서비스 설계 단계부터 Database  또는 테이블을 최대한 물리적으로 분리하여 설계하는 것이 가장 중요합니다.

Replicat_Do* 옵션 참조)
Replication Slave Options and Variables

Conclusion

위에서 설명드린 것은 극히 일부일 뿐 더욱 다양한 케이스에 Replication을 활용할 수 잇습니다. 예를 들어 DB Major 버전 업그레이드(ex: 5.1.x -> 5.5.x), 테이블 구조 변경, 테스트 환경 구성 등이 바로 그것들입니다. MySQL Replication은 물리적으로 저장소가 분리된 영역에 데이터를 비동기적으로 복제하는 원리만 꼭 기억하세요. ^^

위 설명에서는 추상적으로 언급 드렸으나, 추후 실제 구성 사례를 정리해서 꼭 공유 드릴께요^^

MySQL Replication 이해(2) – 구성

Overview

MySQL Replication 개념에 이어, 이번에는 실 구성에 관한 내용입니다.
각 서버 구성 방법은 “리눅스에 MySQL 설치하기” 편을 참고하시기 바랍니다.

시작에 앞서서 Server_id는 다른 숫자로 설정하세요^^.

Replication 구성은 다음 세 단계를 거쳐서 수행됩니다.

  1. DB 유저 생성
  2. DB 데이터 동기화(셋 중 택 1)
    – DB Data File Copy
    – MySQL Dump (All Lock)
    – Export/Import (Single Transaction)
  3. 리플리케이션 시작

1. DB 유저 생성

복제 데이터 전송을 위한 리플리케이션 권한의 DB 유저를 마스터에 생성합니다. 각 슬레이브 IO 쓰레드들은 추가된 DB 유저를 통해 데이터를 받습니다.

[mysql@master] $ mysql -uroot -p비밀번호
 mysql> GRANT REPLICATION SLAVE ON *.*
     -> TO repl IDENTIFIED BY 'repl';

2. DB 데이터 동기화

  • DB Data File Copy
    DB 서버 데몬을 내린 상태에서 데이터 파일 자체를 복사하는 방식입니다.
    데이터 파일 복사 과정만 수행하면 되기 때문에 대용량 서버에서 슬레이브 추가 시 유용한 방식입니다.

    ## MySQL 데몬 중지
    [mysql@master] $ /etc/init.d/mysqld stop
    [mysql@slave] $ /etc/init.d/mysqld stop
    
    ## 슬레이브에 데이터 복사
    [mysql@slave] $ scp -r mysql@master:/data/mysql/mysql-data /data/mysql
    
    ## 마스터 바이너리 로그 파일 확인
    [mysql@master]$ cd /data/mysql/mysql-binlog
    [mysql@master ]$ ls -alh
    합계 21M
    drwxr-xr-x. 2 mysql DBA 4.0K 13:34 .
    drwxrwx---. 7 mysql DBA 4.0K 16:34 ..
    -rw-rw----. 1 mysql DBA 21M  13:34 mysql-bin.000006
    -rw-rw----. 1 mysql DBA 126 13:34 mysql-bin.000007
    -rw-rw----. 1 mysql DBA 126  13:34 mysql-bin.index

    DB가 재시작되면 기본적으로 새로운 바이너리 로그를 생성됩니다.
    * 로그 파일명 :  mysql-bin.000007
    * 로그 포지션:  106

  •  MySQL Dump (All Lock)
    DB 전체에 READ LOCK을 걸고 데이터를 Export하는 방식입니다. 트랜잭션이 지원 안되는 스토리지 엔진이 섞여 있고, 데이터량이 작은 경우 사용하면 되겠습니다. “MySQL Table Lock에 관한 이해”와 같이 뜻하지 않은 Dead Lock이 발생할 수 있습니다.
    Lock을 걸고 데이터를 Export 후 슬레이브 장비에서 다시 Import하는 방식입니다. 백업하는 도중에는 데이터 변경 작업은 수행 불가하며, MyISAM의 경우 백업 수행 시간 동안 Dead Lock이 발생할 수 있습니다.
    데이터량과 트랜잭션이 작은 경우 사용할 수 있는 방식입니다.

    MySQL Full Backup
    MySQL Full Backup
    <마스터 세션1> 
    ## READ LOCK을 걸어서 데이터 변경을 방지합니다.
    mysql> FLUSH TABLES WITH READ LOCK;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> show master status\G
    *********** 1. row ***********
    File: mysql-bin.000008
    Position: 456730
    1 row in set (0.00 sec)
    
    <마스터 세션2> 
    ## 다른 세션에서 전체 데이터를 백업합니다.
    [mysql@master]$ export -uroot -p비밀번호 --all-databases > /data/mysql/mysql-dump/dump.sql
    
    <마스터 세션1> 
    ## 원래 세션에서 READ LOCK을 해제합니다.
    mysql> UNLOCK TABLES;
    Query OK, 0 rows affected (0.00 sec)
    
    ## 슬레이브에 데이터 이관
    [mysql@slave] $ scp -r mysql@master:/data/mysql/mysql-dump/dump.sql /data/mysql/mysql-dump
    [mysql@slave] $ mysql -uroot -p비밀번호 --force < /data/mysql/mysql-dump/dump.sql
  • MySQL Dump (Single Transaction)
    서비스 중지가 불가하고, 테이블이 트랜잭션을 지원하는 경우에만 사용할 수 있는  방법으로, 트랜잭션 고립 (Isolation)을 특성을 활용하는 방식입니다. 즉, Database 가 InnoDB로만 이루어진 경우 많이 쓰이는 방식이죠.^^

    ## 시점 데이터 생성
    [mysql@master ~]$ export -uroot -p비밀번호 --single-transaction --master-data=2 --all-databases > /data/mysql/mysql-dump/dump.sql
    
    ## 슬레이브에 데이터 이관
    [mysql@slave] $ scp -r mysql@master:/data/mysql/mysql-dump/dump.sql /data/mysql/mysql-dump
    [mysql@slave] $ head -n 22 full_backup.sql | tail -n 1
    -- CHANGE MASTER TO MASTER_LOG_FILE='mysql_bin.000008', MASTER_LOG_POS=456730;
    [mysql@slave] $ mysql -uroot -p비밀번호 --force < /data/mysql/mysql-dump/dump.sql

3. 리플리케이션 시작

데이터 통신 용도로는 별도 네트워크에 구성해야 NIC 간섭을 최소화할 수 있습니다. 그리고 앞서서 기록을 해놓은 마스터 Binlog 파일과 포지션을 세팅 후 슬레이브 서버를 구동하면 되겠습니다.

## 슬레이브에서 실행
mysql> CHANGE MASTER TO
    -> MASTER_HOST='master-pri',
    -> MASTER_USER='repl',
    -> MASTER_PASSWORD='repl',
    -> MASTER_PORT=3306,
    -> MASTER_LOG_FILE='mysql-bin.000008',
    -> MASTER_LOG_POS=456730,
    -> MASTER_CONNECT_RETRY=5;
## 슬레이브 시작
mysql> START SLAVE;

## Slave_IO_Running, Slave_SQL_Running 상태 확인
mysql> show slave status\G
*********************** 1. row ***********************
               Slave_IO_State: Waiting for master to..
                  Master_Host: gisselldb01-pri
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 5
              Master_Log_File: mysql-bin.000008
          Read_Master_Log_Pos: 456730
               Relay_Log_File: mysql-relay.000001
                Relay_Log_Pos: 251
        Relay_Master_Log_File: mysql-bin.000008
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
            ..중략..
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 456730
              Relay_Log_Space: 547
              Until_Condition: None
            ..중략..
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
1 row in set (0.00 sec)

다음 편에는 실제 활용할 수 있는 분야에 관해서 정리하도록 하겠습니다.
긴 글 읽으시느라 수고하셨어요^^