안녕하세요. 우아한형제들에서 빌링시스템을 개발하고 있는 이주현입니다.

입사한 이래로 2년 가까이 일하며 정말 다양한 문제를 마주하고 해결하며 소중한 경험을 쌓고 있습니다. 그중 얼마 전 MariaDB 백업 방식으로 적용한 XtraBackup에 대하여 이야기해보려고 합니다. 그전에 잠시 2016년으로 돌아가 보겠습니다.

개발자가 언제 죽는다고 생각하나? 총알이 심장을 관통했을 때? 아니야..
불치병에 걸렸을 때? 아니지! 맹독 버섯스프를 마셨을때? 아니다!!
그건 바로 메인 DB를 날렸을 때다.

때는 바야흐로 2016년

12월 27일 오후 3시

결제시스템 모듈을 개발하던 중 저의 실수로 빌링 데이터베이스의 주요 테이블 9개가 DROP 되는 사고가 발생했습니다. 모니터링 시스템에 빨간불이 들어오고 각종 장애알림이 빗발치기 시작했습니다. 식은땀이 흐르고 머릿속이 새하얘지며 아무것도 생각나지 않습니다. 빨리 복구를 해야겠다는 생각은 가득한데 부끄럽게도 데이터베이스 시스템에 별다른 지식이 없던 저로서는 눈 앞이 캄캄해졌죠.

테이블 삭제 상태

결국 이 장애로 배달의민족 결제가 잠시 중단되었습니다. 그나마 다행인 건 애플리케이션에서 임시 데이터베이스를 바라보도록 수정하여 장애 시간이 길지는 않았다는 점입니다. ㅠㅠ

우선 급한대로 Full Backup 데이터부터 복구 하기 시작했습니다.

1. Full Backup 데이터 복구

당시 장애 상황을 재현한 데이터로 실제와는 많은 차이가 있을 수 있습니다.

빌링 데이터베이스는 매일 새벽 6시 mysqldump를 사용해 전체 데이터를 백업하고 있었습니다. 테이블별로 데이터를 SQL형식으로 생성한 뒤 압축한 형태였기 때문에 DROP 된 테이블을 쉽게 복구할 수 있었습니다.

$ cd backup
$ ls -lh
합계 xx
-rw-r--r-- 1 root root  500M 2017-12-27 06:00 card_receipt.sql.gz
-rw-r--r-- 1 root root    5G 2017-12-27 06:10 pay.sql.gz
-rw-r--r-- 1 root root    3G 2017-12-27 06:20 pay_card.sql.gz
-rw-r--r-- 1 root root    8G 2017-12-27 06:25 pay_detail.sql.gz
-rw-r--r-- 1 root root  800M 2017-12-27 06:55 receipt.sql.gz

많은 분들께서 아시겠지만 gzip으로 압축된 sql내용의 파일은 아래와 같은 명령어로 DB에 실행할 수 있습니다. 결제 데이터가 많이 쌓여있던 상황이라 시간이 조금 걸리기는 했지만 빌링 시스템 Open ~ 새벽 6시까지의 데이터는 복구할 수 있었습니다.

for file in `ls | grep ${DROPPED_TABLE}`; \
do \
echo "recover $file"; gunzip < $file | mysql -u [uname] -p[pass] [dbname]; \
done

fullbackup을 통한 테이블 복구 상태

2. Binary log로 복구

$ cd binarylog
$ ls -l
합계 xxx
-rw-rw---- 1 mysql mysql 1.1G 2016-12-23 11:42 mysql-bin.000531
-rw-rw---- 1 mysql mysql 1.1G 2016-12-24 12:12 mysql-bin.000532
-rw-rw---- 1 mysql mysql 1.1G 2016-12-25 10:53 mysql-bin.000533
-rw-rw---- 1 mysql mysql 1.1G 2016-12-26 10:59 mysql-bin.000541
-rw-rw---- 1 mysql mysql 841M 2016-12-27 10:42 mysql-bin.000542

사실 중요한 점은 새벽 6시 ~ 15시까지의 데이터를 어떻게 복구하냐 였습니다.
어렴풋 MariaDB 서버에서 보았던 binary log가 도움이 되지 않을까 지푸라기라도 잡는 심정으로 구글링을 해보았습니다. 그 결과는 다행히 ‘가능하다’였습니다. binary log는 데이터 수정과 관련된 모든 정보가 담겨 있는 파일인데 크게 두 가지 중요한 목적이 있다고 합니다.

  • Replication 구성하는 데 있어서 Slave서버로 binary log에 포함되어있는 이벤트를 전송
  • 데이터 복구 작업

2진 형식으로 기록된 binary log를 텍스트 파일로 복구하는 데는 mysqlbinlog 유틸을 이용합니다. mysqlbinlog에는 다양한 옵션이 존재합니다. 그 중에서도 --(start|stop)-position, --(start|stop)-datetime은 데이터 복구시 아주 유용합니다. 특정 position이나 시간에 대한 데이터를 뽑아낼 수 있기때문입니다.

$ mysqlbinlog \
    --start-datetime="2016-12-27 06:00:00" \
    --stop-datetime="2016-12-27 15:00:00" \
    mysql-bin.000541 > restore.log
$ mysqlbinlog \
    --start-datetime="2016-12-27 06:00:00" \
    --stop-datetime="2016-12-27 15:00:00" \
    mysql-bin.000542 >> restore.log
$ restore.log에 대한 수정 작업..생략
$ mysql -u root -p... [dbname] < restore.log

$ mysqlbinlog --start-datetime=.. | mysql -u root .. 와 같은 명령어로 복구된 이벤트 내용을 mysql에 직접 실행할 수 있습니다. 하지만 테이블 전반적인 내용들이 담겨있기 때문에 9개 테이블에 대한 필터링 작업이 필요하여 restore.log로 저장 후 수정 작업을 진행했습니다.

주의할 점은 --stop-datetime에 대한 시간을 잘못 지정하여 ‘DROP TABLE..’쿼리가 다시 실행되면 안됩니다.

테이블 복구 상태

이렇게 데이터가 모두 복구되었습니다.

부끄럽지만 binary log존재의 필요성이나 사용법 등을 처음 알게 된 계기가 되었습니다. 복구 방법에 대해 확신이 없는 상태에서 끝까지 믿고 맡겨주신 팀원들에게 아직도 고맙습니다.

위의 이야기는 하루에도 몇 번씩 회자(놀림)되고 있으며 제가 이 회사를 퇴사하는 순간까지 아니 퇴사 후에도 길이길이 남을 것 같습니다.

그리고 시간이 흘러

빌링 데이터베이스 대규모 개편 작업에 있었습니다. 신규 DB서버를 구매하고 파티셔닝도 진행하는 큰 규모의 작업이었습니다. 저도 모르는 사이 저희 팀 공식 DBA가 되어있던 저는 기존 데이터베이스의 데이터를 신규 서버로 이관하는 작업을 맡았습니다. 진행 방식은 위에서 언급한 장애 복구 방법과 비슷했습니다. 테스트도 할 겸 테이블 백업 데이터를 신규 서버에 INSERT 하기 시작했습니다.

# gunzip < pay.gz | mysql -u .. billing

그런데! 퇴근 시간이 지나도 끝날 기미가 보이지 않습니다. 주요 결제 테이블 한 개만 복구하는데도 엄청난 시간이 필요했습니다 (core가 32개인데 왜 사용하지를 못하니..). 그 동안 배달의민족 주문수가 급증하며 데이터가 많이 축적되었고 더이상 mysqldump를 통한 백업, 복구를 할 수 없다고 판단했습니다. 비슷한 장애가 발생했을 때 몇 십 시간씩 데이터 복구에 시간을 낭비 할 수는 없기 때문입니다. 그래서 이번 기회에 새로운 백업 방식을 알아보았습니다.

XtraBackup

XtraBackup은 Percona에서 개발된 오픈소스로 백업 도구입니다. mysqldump가 테이블 생성, 데이터 쿼리에 대한 SQL 생성문을 갖는 논리적 백업이라면 XtraBackup은 엔진 데이터를 그대로 복사하는 물리적 백업 방식입니다. MySQL 엔터프라이즈 라이센스에 포함된 백업 도구의 기능을 모두 제공할 뿐만 아니라 더 유용한 기능들도 제공합니다. XtraBackup의 백업 방식은 크게 전체 백업, 증분 백업, 개별(db, table) 백업, 압축(qpress) 백업, Encrypted 백업이 있습니다. 또한 stream을 지원하기 때문에 파이프(|)를 통하여 다른 프로그램의 표준 입력으로 리다이렉션이 가능합니다.
이 내용에서는 전체 백업 + stream을 이용한 백업과 복구 방법에 대하여 공유해보려고 합니다.

설치

Centos 6.x에 MariaDB는 10.2.x를 설치했습니다. 현재 최신버젼인 XtraBackup 2.4을 기준으로 합니다.

# yum install http://www.percona.com/downloads/percona-release/redhat/0.1-4/percona-release-0.1-4.noarch.rpm
# yum install yum install percona-xtrabackup-24
# yum install qpress #압축백업을 하고싶으시다면 설치

xtrabackup vs innobackupex

XtraBackup은 xtrabackup, innobackupex두 가지 유틸을 지원합니다. 백업을 해준다는 점에서는 같지만 각각 기능과 사용할 수 있는 옵션값에 차이점이 존재했습니다.

  • 2.2 버젼까지 innobackupex는 xtrabackup의 기능을 perl로 wrapping 한 스크립트였으나 2.3 버젼부터는 xtrabackup의 symlink로 변경되었며 현재는 deprecated상태입니다. (호출된 명령어를 기반으로 동작 방식이 결정됩니다.)
  • 2.2 버젼까지 xtrabackup은 MyISAM을 지원하지 않았지만 지금은 innobackupex와 동일한 기능을 제공하고 있습니다.

XtraBackup에서 innobackupex는 next major부터 삭제된다고 했으나 아직까지도 유지되고 있습니다. 관련 도서나 커뮤니티에서 대부분 innobackupex기준으로 설명하며 다양한 편의 기능이 포함되어 있기 때문에 innobackupex를 사용하여 설명 드리겠습니다.

전체 백업

빌링 데이터베이스는 장애 시 복구 과정을 단순하게 하기 위해 증분 백업을 사용하지 않고 전체 백업을 하고 있습니다. 말 그대로 운영중인 데이터베이스를 통짜로 복사합니다.

$ innobackupex --defaults-file=/etc/my.cnf \
--no-lock \
--user=${USER} \
--password=${PASSWORD} /home/backup/xtrabackup/

$ cd /home/backup/xtrabackup/2018-05-24_21-14-00
$ ls -l
-rw-r----- 1 root root      438  5월 22 15:08 backup-my.cnf
drwxr-x--- 2 root root     4096  5월 22 15:08 billing
-rw-r----- 1 root root 12582912  5월 22 15:08 ibdata1
drwxr-x--- 2 root root     4096  5월 22 15:08 mysql
drwxr-x--- 2 root root     4096  5월 22 15:08 performance_schema
-rw-r----- 1 root root      113  5월 22 15:08 xtrabackup_checkpoints
-rw-r----- 1 root root      464  5월 22 15:08 xtrabackup_info
-rw-r----- 1 root root  1556480  5월 22 15:08 xtrabackup_logfile

백업하는 동안에 table lock을 없애려면 --no-lock옵션을 추가해야 합니다. 하지만 InnoDB table이 아닌 상황에서는 경우에 따라서 일관성 없는 백업 결과가 나올 수 있으므로 사전에 확인이 필요합니다. 참고: cmdoption-innobackupex-no-lock

위와 같은 명령어를 실행하면 /home/backup/xtrabackup/yyyy-MM-dd_HH-mm-ss경로에 테이블, 리두로그, XtraBackup관련 데이터들이 함께 백업된 것을 확인할 수 있습니다.

XtraBackup은 qpress를 이용한 compress 백업을 지원합니다. 하지만 압축 효율성과 속도에 아쉬움이 있다면 stream + pigz를 사용할 수 있습니다.

$ yum install pigz

$ innobackupex --defaults-file=/etc/my.cnf \
--no-lock \
--user=${USER} \
--password=${PASSWORD} \
--stream=tar ${BACKUP_DIR} | pigz -p ${PROCESS_COUNT} ${BACKUP_DIR}/${$FILENAME}

tar를 해제할 때는 -i 옵션을 추가하셔야 합니다. eg) $ tar -xizf backup.tar.gz 관련문서

복원 준비

백업된 데이터를 그대로 사용하면 좋겠지만 항상 생각대로 되는 게 없습니다.. 백업이 특정 시점에 완벽하게 이루어지면 좋겠지만 데이터 크기와 서버의 성능에 따라 백업하는 시간도 수십 분 ~ 수 시간 걸릴 수 있습니다. 이때 INSERT, UPDATE, DELETE쿼리가 유입된다면 백업된 데이터와 일관성이 없어지게 됩니다. 복원 준비 단계는 백업 중 수행 된 트랜잭션 로그파일(xtrabackup_logfile)을 적용하여 데이터를 일관성 있게 만들어줍니다.

준비 단계는 백업한 뒤 즉시 실행할 필요가 없습니다. 데이터 복구 전 실행하셔도 됩니다.

$ innobackupex --defaults-file=/etc/my.cnf \
--apply-log /home/backup/xtrabackup/2018-05-22_06-00-01/

복원

복원 원리는 간단합니다. 준비된 백업 파일을 MariaDB의 datadir로 옮겨주면 끝입니다. 이와 관련된 옵션을 innobackupex에서 지원해줍니다.
그전에 주의할 점이 있다면 mysql 서비스를 종료하고 datadir에 내용이 남아있다면 다른 폴더에 백업을 한 뒤 비워줘야 합니다.

$ service mysql stop

$ /bin/cp -r /var/lib/mysql/* /path/to/backup

$ rm -rf /var/lib/mysql/*

$ innobackupex --defaults-file=/etc/my.cnf \
--copy-back /home/backup/xtrabackup/2018-05-28_13-59-23/

$ ls -la /var/lib/mysql
합계 xxxxxx
drwxr-x---  2 root  root      4096  2018-05-22 15:22 billing
-rw-r-----  1 root  root      2016  2018-05-22 15:22 ib_buffer_pool
-rw-r-----  1 root  root  79691776  2018-05-22 15:22 ibdata1
drwxr-x---  2 root  root      4096  2018-05-22 15:22 mysql
drwxr-x---  2 root  root      4096  2018-05-22 15:22 performance_schema
drwxr-x---  2 root  root      4096  2018-05-22 15:22 test
-rw-r-----  1 root  root       464  2018-05-22 15:22 xtrabackup_info

위와 같이--copy-back명령어를 사용하면 백업된 내용을 원본 디렉토리(/var/lib/mysql)에 이동시켜줍니다.

추가적으로 데이터 파일의 권한을 MariaDB 서비스 계정으로 수정이 필요할 수 있습니다.

$ chown -R mysql:mysql *
$ chmod ...
$ service mysql start

Point-In-Time 복원

지난번과 같이 오후 3시에 관리자의 실수로 데이터베이스를 삭제했고 새벽 6시에 진행한 전체 백업 덕분에 데이터를 어느 정도 복구했다고 가정합니다. 이제 우리는 새벽 6시 ~ 오후 3시 데이터를 binary log를 통해 복구해야 합니다.

$ cd /path/to/backup/binarylog
$ ls -la 
-rw-rw---- 1 mysql mysql 1.1G 2018-05-19 11:42 mysql-bin.000001
-rw-rw---- 1 mysql mysql 1.1G 2018-05-20 12:12 mysql-bin.000002
-rw-rw---- 1 mysql mysql 1.1G 2018-05-21 10:53 mysql-bin.000003
-rw-rw---- 1 mysql mysql 1.1G 2018-05-22 10:59 mysql-bin.000004

그런데 binary log파일은 어떤 걸 사용해야 하고 백업을 종료지점은 어떻게 알아낼 수 있을까요? 그것은 바로.. 전체 백업 후 데이터를 준비하는 과정(--apply-log)에서 생성되는 xtrabackup_binlog_info파일에 있습니다. 해당 파일에는 백업에 사용된 binary log과 position값이 적혀있습니다.

테이블 복구 상태

$ cat /home/backup/xtrabackup/2018-05-22_06-00-01/xtrabackup_binlog_info
/var/log/mysql/binary/mysql-bin.000004  551

mysql-bin.000004파일의 551 position에서 오후 3시까지의 데이터를 복구하면 됩니다. 이 정보는 데이터 복구할 때뿐만 아니라 Slave서버를 추가적으로 구성하는데도 유용하겠죠.

위에서 말씀드렸지만 테이블이 정확히 DROP 된 시간을 알아내는 건 중요합니다. 그렇지 않으면 같은 내용의 장애 쿼리(DROP..)가 다시 실행될 수 있습니다. 반드시 mysqlbinlog 결과 값을 새로운 파일로 리다이렉션 하시고 데이터를 확인하시기 바랍니다.

$ mysqlbinlog /path/to/backup/mysql-bin.000003 \
    --start-position=551 \
    --stop-datetime="2018-05-22 15:00:08" | mysql -u root.. 생략

이렇게 복원이 모두 완료되었습니다.

mysqldump vs XtraBackup

데이터 사이즈가 크지 않다면 mysqldump를 사용하는 게 간단하며 복원 시에도 신경 써줘야 할 포인트가 적습니다 하지만 데이터 사이즈가 수십 ~ 수백 GB에 이르면 이야기가 달라집니다. 실제 빌링 데이터베이스에 적용한 결과 아래와 같은 차이를 얻어낼 수 있었습니다.

테이블 복구 상태

이 글에서 직접 다루지는 않았지만 증분 백업, 테이블별 백업 등 여러분 들에게 좋은 선택이 될 수 있는 기능들을 제공하고 있으니 XtraBackup 공식 메뉴얼을 둘러보시는것도 좋을것 같습니다.

미워도 다시 한번

데이터베이스의 서비스, 관리용 계정 분리하기. 쿼리 날리기 전 한번 더 확인하기. rm -rf막기 등 장애를 관리할 수 있는 포인트는 많이 있습니다. 아직까지도 전체 백업이나 binary log가 존재하지 않았으면 어떻게 됐을지 상상이 안됩니다. 혹시나 이 글을 보시는 다른 개발자분들도 장애 시 전체 복구 포인트가 존재하는지 점검하는 계기가 되었으면 좋겠습니다.

회사가 빠르게 성장하고 있습니다. 데이터가 급격하게 늘어나니 개발에 있어서도 고민하고 해결해야 할 부분들이 많이 생깁니다. 이런 일을 제가 언제 또 맡아서 해볼 수 있을까요.

성장할 수 있도록 좋은 서비스와 환경을 만들어주신.. 그리고 저를 믿고 맡겨주신 회사와 팀원들에게 너무 고맙습니다.

(이력서 한 줄 추가욧!)

참고 자료