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 명령어입니다.
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 서버로부터 복제가 잘 되는지 확인해 봅니다.
🔖참고