MySQL Replication with Docker
[#🐬Board] Replication으로 DB 부하 분산하기

Board 서버는 대부분의 애플리케이션 서버에서 그렇듯이 쓰기 연산(음식점 등록하기, 주문하기 등)에 비해 읽기 연산(음식점 리스트 조회, 주문 리스트 조회 등) 비중이 훨씬 큽니다. 따라서 향후 많은 TPS/QPS를 처리하기 위해 Board DB 서버의 다중화가 필요했습니다.
그런데 인터넷에서는 예상보다 docker-compose를 이용한 단방향 복제에 대한 예제가 부족했고, Github에 있는 소스들은 제가 원하는 방식이 아니거나 마운트 과정에서의 버그가 종종 발생했습니다.
그래서 이 글에서는 기록 겸 공유 목적으로 Board DB 서버에 Replication을 어떻게 적용시켰는지에 대해 Docker를 중심으로 다룹니다. Spring Boot 서버의 코드는 생략합니다.
💡docker-compose.yml 구성
version: "3.9"
services:
mysql_master:
image: mysql:latest
container_name: mysql_master
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_PORT: 3306
MYSQL_USER: master
MYSQL_PASSWORD: password
MYSQL_DATABASE: board
ports:
- "3307:3306"
volumes:
- ./mysql/master/conf/mysql.conf.cnf:/etc/mysql/conf.d/mysql.conf.cnf
- ./mysql/master/data:/var/lib/mysql
- ./mysql/master/initdb.d:/docker-entrypoint-initdb.d
command: ["mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_general_ci"]
networks:
- mysql-server
mysql_slave:
image: mysql:latest
container_name: mysql_slave
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_PORT: 3306
MYSQL_USER: slave
MYSQL_PASSWORD: password
MYSQL_DATABASE: board
ports:
- "3308:3306"
depends_on:
- mysql_master
volumes:
- ./mysql/slave/conf/mysql.conf.cnf:/etc/mysql/conf.d/mysql.conf.cnf
- ./mysql/slave/data:/var/lib/mysql
- ./mysql/slave/initdb.d:/docker-entrypoint-initdb.d
command: ["mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_general_ci"]
networks:
- mysql-server
networks:
mysql-server:
mysql_master의 인바운드를 3307 포트로, mysql_slave의 인바운드를 3308 포트로 잡았습니다.
볼륨은 따로 설정할 값이 없으면 무시하여도 됩니다.
mysql-server 컨테이너와 mysql-master 컨테이너를 같은 네트워크(mysql-server)에 묶었습니다.
💡build.sh 설정
#!/bin/bash
docker-compose down -v
rm -rf ./mysql/master/data/*
rm -rf ./mysql/slave/data/*
docker-compose build
docker-compose up -d
until docker exec mysql_master sh -c 'export MYSQL_PWD=root; mysql -u root -e ";"'
do
echo "Waiting for mysql_master database connection..."
sleep 4
done
priv_stmt='CREATE USER "slave"@"%" IDENTIFIED WITH mysql_native_password BY "password"; GRANT REPLICATION SLAVE ON *.* TO "slave"@"%"; FLUSH PRIVILEGES;'
docker exec mysql_master sh -c "export MYSQL_PWD=root; mysql -u root -e '$priv_stmt'"
until docker-compose exec mysql_slave sh -c 'export MYSQL_PWD=root; mysql -u root -e ";"'
do
echo "Waiting for mysql_slave database connection..."
sleep 4
done
MS_STATUS=`docker exec mysql_master sh -c 'export MYSQL_PWD=root; mysql -u root -e "SHOW MASTER STATUS"'`
CURRENT_LOG=`echo $MS_STATUS | awk '{print $6}'`
CURRENT_POS=`echo $MS_STATUS | awk '{print $7}'`
start_slave_stmt="SET GLOBAL server_id=2; CHANGE MASTER TO MASTER_HOST='mysql_master',MASTER_USER='slave',MASTER_PASSWORD='password',MASTER_LOG_FILE='$CURRENT_LOG',MASTER_LOG_POS=$CURRENT_POS; START SLAVE;"
start_slave_cmd='export MYSQL_PWD=root; mysql -u root -e "'
start_slave_cmd+="$start_slave_stmt"
start_slave_cmd+='"'
docker exec mysql_slave sh -c "$start_slave_cmd"
docker exec mysql_slave sh -c "export MYSQL_PWD=root; mysql -u root -e 'SHOW SLAVE STATUS \G'#!/bin/bash
docker-compose down -v
rm -rf ./mysql/master/data/*
rm -rf ./mysql/slave/data/*
docker-compose build
docker-compose up -d
until docker exec mysql_master sh -c 'export MYSQL_PWD=root; mysql -u root -e ";"'
do
echo "Waiting for mysql_master database connection..."
sleep 4
done
priv_stmt='CREATE USER "slave"@"%" IDENTIFIED WITH mysql_native_password BY "password"; GRANT REPLICATION SLAVE ON *.* TO "slave"@"%"; FLUSH PRIVILEGES;'
docker exec mysql_master sh -c "export MYSQL_PWD=root; mysql -u root -e '$priv_stmt'"
until docker-compose exec mysql_slave sh -c 'export MYSQL_PWD=root; mysql -u root -e ";"'
do
echo "Waiting for mysql_slave database connection..."
sleep 4
done
MS_STATUS=`docker exec mysql_master sh -c 'export MYSQL_PWD=root; mysql -u root -e "SHOW MASTER STATUS"'`
CURRENT_LOG=`echo $MS_STATUS | awk '{print $6}'`
CURRENT_POS=`echo $MS_STATUS | awk '{print $7}'`
start_slave_stmt="SET GLOBAL server_id=2; CHANGE MASTER TO MASTER_HOST='mysql_master',MASTER_USER='slave',MASTER_PASSWORD='password',MASTER_LOG_FILE='$CURRENT_LOG',MASTER_LOG_POS=$CURRENT_POS; START SLAVE;"
start_slave_cmd='export MYSQL_PWD=root; mysql -u root -e "'
start_slave_cmd+="$start_slave_stmt"
start_slave_cmd+='"'
docker exec mysql_slave sh -c "$start_slave_cmd"
docker exec mysql_slave sh -c "export MYSQL_PWD=root; mysql -u root -e 'SHOW SLAVE STATUS \G'"
https://github.com/vbabak/docker-mysql-master-slave
위 링크의 build.sh를 참고하였는데, 그대로 실행했을 때 master 서버와 slave 서버 둘다 server-id가 1로 설정이 되어 실제로 replication 도중 에러를 냅니다. 그래서 slave 서버에서 server-id를 2로 설정하는 명령을 추가했습니다.
쉘 스크립트 동작은 Docker 명령어에 대한 이해도가 있다면 쉽게 이해할 수 있을 것 같네요.
주의깊게 보아야 할 건 다음 MySQL 명령어입니다.
CREATE USER "slave"@"%" IDENTIFIED WITH mysql_native_password BY "password";GRANT REPLICATION SLAVE ON *.* TO "slave"@"%"; FLUSH PRIVILEGES;
Master 서버에서 데이터를 읽을 slave 유저를 생성하고, replication 권한을 부여합니다. 권한이 부여된 slave 유저는 Master 서버에서 변경이 일어나면 Slave 서버로 데이터를 갖고가서 복제하는 역할을 수행합니다.
SET GLOBAL server_id=2;CHANGE MASTER TO ...START SLAVE;
Slave 서버의 시스템 변수 server_id를 2로 설정한 후, Slave 서버가 Master 서버로부터 복제를 시작하기 위한 설정을 하고 복제 프로세스를 실행합니다.
💡Master 서버 확인
build.sh을 실행하고 다음 명령어로 Master 서버에 접속해봅시다.
docker exec -it mysql_master mysql -uroot -proot --database=board
접속 이후 slave 유저가 생성됐는지 확인하고, 복제 프로세스가 돌아가고 있는지 확인합니다.
mysql> SELECT user, host FROM mysql.user;
mysql> SHOW PROCESSLIST;


💡Slave 서버 확인
Slave 서버에서 replication 상태를 확인해봅니다. replication 도중 문제가 생길 경우 이 곳에서 에러 로그를 확인할 수 있습니다.
docker exec -it mysql_slave mysql -uroot -proot --database=board

💡Master -> Slave 단방향 복제 테스트
이제 Master 서버에서 일어난 생성, 수정, 삭제 행위는 Slave 서버에서도 모두 적용돼야 합니다. 실제로 데이터를 생성해보고 잘 복제되는지 확인해봅시다.
Master 서버에서 데이터베이스 eden을 생성합니다.
mysql> CREATE DATABASE eden;
Slave 서버에서 데이터베이스 조회해보면 eden이 생성돼있는 것을 확인할 수 있습니다.

현재는 Master DB 서버 전체를 복제하고 있는데, 우리가 필요한 것은 board 데이터베이스 복제이기 때문에 향후 시간이 될 때 설정을 변경할 필요가 있겠습니다. 또한 Replication을 구성한 김에, 향후 Fail-over가 되도록 구성하여 DB 서버 장애에 대한 유연한 처리를 할 계획입니다.
🎯정리
docker compose를 이용하여 Master 서버와 Slave 서버를 띄웁니다.
Master 서버에서 복제를 담당할 계정을 생성하고, 권한을 부여합니다.
Slave 서버에서
CHANGE MASTER TO ...명령으로 복제 설정을 합니다.Slave 서버에서
START SLAVE;로 복제를 시작한 후, 실제로 Master 서버로부터 복제가 잘 되는지 확인해 봅니다.



