https://github.com/onesound71/mysql-replication-test
*"갑자기 사용자가 몰려와서 서버가 다운됐어요!"* - 성공하는 스타트업이라면 한 번쯤 겪는 달콤한 비명이다. 하지만 이 순간을 제대로 준비하지 못한다면, 기회를 놓칠 수 있다.
스타트업의 흔한 실수: 모든 걸 한 대에
대부분의 초기 스타트업은 비용을 절약하기 위해 하나의 서버에 모든 것을 몰아넣는다.
단일 서버
🌐 웹 서버 (Nginx/Apache)
⚙️ WAS (Node.js/Django/Spring)
🗄️ 데이터베이스 (MySQL)
📦 Redis/파일시스템
처음엔 괜찮다. 사용자도 적고, 트래픽도 많지 않으니까. 하지만 서비스가 성장하면서 문제가 시작된다.
실제로 겪은 위기 상황들
사례 1: 새벽 2시의 알림
"DB CPU 100%, 서버 응답 안 됨"
갑작스러운 viral 마케팅으로 동시 접속자가 10배 증가. WAS와 DB가 서로 리소스를 놓고 경쟁하며 둘 다 다운.
사례 2: 한 번의 장애가 모든 것을 무너뜨림
디스크 풀로 인한 MySQL 크래시 → WAS도 함께 다운 → 전체 서비스 중단
사례 3: 백업 복구의 악몽
단일 서버 장애 시 백업에서 복구하는 동안 8시간 서비스 중단 → 사용자 이탈
DB 리플리케이션: 작은 투자, 큰 효과
MySQL 마스터-슬레이브 리플리케이션을 도입하면 이런 문제들을 대부분 해결할 수 있다.
리플리케이션의 핵심 이점
1. 읽기 성능 대폭 향상
Before: 모든 요청 → 단일 DB
After: 쓰기 요청 → Master DB
읽기 요청 → Slave DB (또는 여러 대)
대부분의 웹 서비스에서 읽기:쓰기 비율은 80:20 또는 90:10이다. 읽기 전용 슬레이브 DB를 추가하는 것만으로도 전체 DB 부하를 절반으로 줄일 수 있다.
2. 장애 대응력 강화
- Master DB 장애 시 → Slave를 Master로 승격 (몇 분 내 복구)
- 데이터 백업 → Slave에서 진행 (Master 성능에 영향 없음)
- 점검 작업 → 무중단 롤링 업데이트 가능
3. 비용 효율성
- 읽기 전용 슬레이브는 저사양 서버도 충분
- 클라우드에서는 Read Replica가 Master보다 저렴
- 스케일 아웃으로 점진적 확장 가능
실전! Docker로 MySQL 리플리케이션 구성하기
준비 단계
가장 간단한 방법은 Docker Compose를 활용하는 것이다. 로컬 개발 환경에서 먼저 테스트해보자.
# docker-compose.yml
version: '3.8'
services:
mysql-master:
image: mysql:8.0
container_name: mysql-master
ports:
- "13306:3306"
environment:
MYSQL_ROOT_PASSWORD: masterpass
MYSQL_DATABASE: testdb
volumes:
- ./master/my.cnf:/etc/mysql/my.cnf
- ./master/init.sql:/docker-entrypoint-initdb.d/init.sql
- master_data:/var/lib/mysql
mysql-slave:
image: mysql:8.0
container_name: mysql-slave
ports:
- "13307:3306"
environment:
MYSQL_ROOT_PASSWORD: slavepass
volumes:
- ./slave/my.cnf:/etc/mysql/my.cnf
- ./slave/init.sql:/docker-entrypoint-initdb.d/init.sql
- slave_data:/var/lib/mysql
depends_on:
- mysql-master
volumes:
master_data:
slave_data:
핵심 설정 포인트
Master 설정 (master/my.cnf)
[mysqld]
# 서버 식별 (클러스터 내 유니크)
server-id = 1
# 바이너리 로깅 (리플리케이션의 핵심)
log-bin = mysql-bin
binlog_format = ROW
# GTID 활성화 (MySQL 8.0 권장)
gtid_mode = ON
enforce_gtid_consistency = ON
# 성능 최적화
innodb_buffer_pool_size = 256M
sync_binlog = 1
# 모든 IP에서 접근 허용
bind-address = 0.0.0.0
Slave 설정 (slave/my.cnf)
[mysqld]
server-id = 2
# 읽기 전용 모드
read_only = ON
# GTID 설정
gtid_mode = ON
enforce_gtid_consistency = ON
# 리플리케이션 최적화
replica_parallel_workers = 4
replica_parallel_type = LOGICAL_CLOCK
bind-address = 0.0.0.0
리플리케이션 사용자 생성
Master에서 복제 전용 사용자를 생성한다.
-- master/init.sql
CREATE USER 'replication_user'@'%' IDENTIFIED BY 'replication_pass';
GRANT REPLICATION SLAVE ON *.* TO 'replication_user'@'%';
FLUSH PRIVILEGES;
Slave에서 Master에 연결한다.
-- slave/init.sql
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='mysql-master',
SOURCE_USER='replication_user',
SOURCE_PASSWORD='replication_pass',
SOURCE_AUTO_POSITION=1;
START REPLICA;
한 줄로 실행하기
# 환경 시작
docker-compose up -d
# 상태 확인
docker exec mysql-master mysql -uroot -pmasterpass -e "SHOW MASTER STATUS;"
docker exec mysql-slave mysql -uroot -pslavepass -e "SHOW REPLICA STATUS\G"
검증: 정말 동작하는지 확인해보자
Python 테스트 스크립트
import mysql.connector
import time
import random
# 연결 설정
MASTER_CONFIG = {
'host': 'localhost', 'port': 13306,
'user': 'root', 'password': 'masterpass',
'database': 'testdb'
}
SLAVE_CONFIG = {
'host': 'localhost', 'port': 13307,
'user': 'root', 'password': 'slavepass',
'database': 'testdb'
}
def test_replication():
# 1. Master에 데이터 INSERT
master_conn = mysql.connector.connect(**MASTER_CONFIG)
master_cursor = master_conn.cursor()
test_id = random.randint(10000, 99999)
test_name = f"test_user_{test_id}"
test_email = f"test_{test_id}@example.com"
master_cursor.execute(
"INSERT INTO users (name, email) VALUES (%s, %s)",
(test_name, test_email)
)
master_conn.commit()
print(f"✅ Master에 삽입: {test_name}")
# 2. 리플리케이션 대기
time.sleep(3)
# 3. Slave에서 데이터 확인
slave_conn = mysql.connector.connect(**SLAVE_CONFIG)
slave_cursor = slave_conn.cursor()
slave_cursor.execute(
"SELECT name, email FROM users WHERE name = %s",
(test_name,)
)
result = slave_cursor.fetchone()
if result:
print(f"✅ Slave에서 확인: {result[0]} - {result[1]}")
print("🎉 리플리케이션 성공!")
else:
print("❌ 리플리케이션 실패")
master_conn.close()
slave_conn.close()
if __name__ == "__main__":
test_replication()
실행 결과
✅ Master에 삽입: test_user_58582
✅ Slave에서 확인: test_user_58582 - test_58582@example.com
🎉 리플리케이션 성공!
실무 적용 시 주의사항
1. MySQL 버전별 차이점
MySQL 5.7 vs 8.0의 주요 차이
기능 | MySQL 5.7 | MySQL 8.0 |
---|---|---|
기본 인증 | mysql_native_password | caching_sha2_password |
GTID | 수동 설정 필요 | 기본 권장 |
그룹 리플리케이션 | 실험적 | 안정화 |
성능 | 기준점 | 10-20% 향상 |
⚠️ 주의: 기존에 5.7을 사용 중이라면 8.0으로 업그레이드 시 호환성 검토가 필수다.
2. 애플리케이션 코드 수정
리플리케이션을 도입하면 읽기/쓰기를 분리해야 한다.
# Before: 모든 작업을 단일 DB에
db = get_database_connection()
# After: 읽기/쓰기 분리
def get_db_connection(read_only=False):
if read_only:
return connect_to_slave()
else:
return connect_to_master()
# 사용 예시
def get_user(user_id):
db = get_db_connection(read_only=True) # Slave 사용
return db.query("SELECT * FROM users WHERE id = %s", user_id)
def create_user(name, email):
db = get_db_connection(read_only=False) # Master 사용
return db.execute("INSERT INTO users (name, email) VALUES (%s, %s)", name, email)
3. 리플리케이션 지연 처리
문제 상황: 사용자가 게시글을 작성한 직후 목록 페이지로 이동했는데 방금 쓴 글이 안 보임
해결 방법:
- 읽기 후 일관성 - 쓰기 직후에는 Master에서 읽기
- 세션 기반 라우팅 - 사용자별로 일정 시간 Master 사용
- 애플리케이션 캐시 - Redis 등으로 최신 데이터 캐싱
def create_post_and_redirect(title, content, user_id):
# 1. Master에 쓰기
master_db.execute("INSERT INTO posts (title, content, user_id) VALUES (%s, %s, %s)",
title, content, user_id)
# 2. 세션에 '방금 쓰기 작업 함' 표시
session['last_write_time'] = time.time()
# 3. 리다이렉트
redirect('/posts')
def get_posts_list(user_id):
# 방금 쓰기 작업을 했다면 Master에서 읽기
if session.get('last_write_time', 0) > time.time() - 5: # 5초 이내
db = get_master_db()
else:
db = get_slave_db()
return db.query("SELECT * FROM posts WHERE user_id = %s ORDER BY created_at DESC", user_id)
성능 모니터링과 알림 설정
핵심 지표들
-- 리플리케이션 지연 확인
SELECT
CHANNEL_NAME,
SERVICE_STATE,
LAST_ERROR_MESSAGE,
LAST_ERROR_TIMESTAMP
FROM performance_schema.replication_connection_status;
-- 슬레이브 상태 확인
SHOW REPLICA STATUS\G
간단한 모니터링 스크립트
#!/bin/bash
# monitor_replication.sh
SLAVE_LAG=$(docker exec mysql-slave mysql -uroot -pslavepass -e "
SELECT TIMESTAMPDIFF(SECOND,
STR_TO_DATE(Master_Log_Time, '%Y-%m-%d %H:%i:%s'),
NOW()) as lag_seconds
FROM (SHOW REPLICA STATUS) AS replica_status;" 2>/dev/null | tail -1)
if [ "$SLAVE_LAG" -gt 10 ]; then
echo "⚠️ 리플리케이션 지연: ${SLAVE_LAG}초"
# Slack 알림 등 추가
fi
다음 단계: 확장 로드맵
단계별 확장 계획
1단계: 기본 Master-Slave (현재)
Master (쓰기) → Slave (읽기)
2단계: 다중 읽기 슬레이브
┌─ Slave 1 (읽기)
Master (쓰기) ──┼─ Slave 2 (읽기)
└─ Slave 3 (백업)
3단계: 지역별 분산
Seoul Master ──→ Tokyo Slave
│
└──→ Singapore Slave
4단계: 샤딩 도입
App Layer
│
├─ Shard 1 (User A-M)
├─ Shard 2 (User N-Z)
└─ Shard 3 (Analytics)
클라우드 서비스 활용
AWS RDS Read Replica
- 자동 백업, 모니터링
- 장애 시 자동 승격
- 다중 AZ 배포
Google Cloud SQL
- 고가용성 설정
- 자동 스케일링
- 통합 모니터링
마무리: 미리 준비하는 것의 가치
*"서비스가 성공하면 사용자가 몰릴 텐데, 그때 대응하면 되지 않을까?"*
이런 생각이 위험한 이유는 기회는 한 번만 온다는 것이다. 바이럴이 터졌을 때, 언론에 소개됐을 때, 그 순간에 서버가 다운되면 다음 기회는 언제 올지 모른다.
리플리케이션 도입의 ROI
비용:
- 개발 시간: 2-3일
- 추가 서버 비용: 월 50-100달러
- 운영 복잡성: 약간 증가
수익:
- 서비스 안정성: 99.9% → 99.99%
- 성능 향상: 2-5배
- 기회 손실 방지: 무한대
오늘부터 시작하기
- 로컬에서 먼저 테스트 - Docker Compose로 30분이면 구성 가능
- 작은 기능부터 적용 - 조회가 많은 API부터 읽기/쓰기 분리
- 모니터링 도구 설정 - 지연 시간, 에러율 추적
- 팀 내 공유 - 동료들과 함께 운영 노하우 축적
기억하자: 스타트업의 성공은 준비된 자의 몫이다. 사용자가 몰려오기 전에 미리 준비하는 것, 그것이 성장하는 스타트업과 사라지는 스타트업의 차이다.
추가 리소스
- GitHub 저장소: [완전한 예제 코드와 설정 파일들]
- MySQL 공식 문서: [리플리케이션 가이드]
- 모니터링 도구: Prometheus + Grafana 설정 예제
- 클라우드 마이그레이션: AWS RDS, GCP Cloud SQL 가이드
성공하는 스타트업의 인프라는 하루아침에 만들어지지 않는다. 지금 시작하자.
'나의 IT 기억' 카테고리의 다른 글
💼 업무 자동화 전쟁: n8n vs Langflow vs Make - 당신의 팀에겐 어떤 무기가 필요할까? (6) | 2025.05.29 |
---|---|
🤖 그냥해보세요! Qwen2.5VL 멀티모달 모델로 이미지 분석하기 (0) | 2025.05.28 |
🤷♂️ 신입 개발자들이 가장 궁금해하는 질문들 - 현직 개발자의 솔직한 답변 (0) | 2025.05.27 |
🚀 MCP Server - 나의 동료에게 설명하는 MCP (7) | 2025.05.27 |
SaaS 개발하면서 **멀티테넌시** 구성이 생각보다 복잡하더라고.. (1) | 2025.05.25 |