월 130만원 절감한 프로덕션 서버 무중단 이전: DigitalOcean → Hetzner 실전 가이드 (isayeter.com)
목차(4)
한줄 요약
월 $1,432 DigitalOcean 서버를 $233 Hetzner 전용 서버로 무중단 이전해 연간 $14,388을 절감한 실전 마이그레이션 사례다.
어떤 상황에서 필요한가?
클라우드 인프라 비용이 점점 감당하기 어려워지는 시점이 온다. 트래픽이 예측 가능하고, 오토스케일링이나 매니지드 서비스를 적극적으로 활용하지 않는 steady-state 워크로드라면, 퍼블릭 클라우드보다 전용 서버(dedicated server)가 훨씬 합리적인 선택일 수 있다.
이 사례의 출발점도 그랬다. DigitalOcean에서 192GB RAM, 32 vCPU, 600GB SSD 구성으로 월 $1,432를 지불하고 있었다. Hetzner의 AX162-R은 AMD EPYC 9454P(48코어/96스레드), 256GB DDR5, 1.92TB NVMe Gen4 RAID1 구성을 월 $233에 제공한다. 스펙은 모든 항목에서 우월하고 가격은 6분의 1 수준이다.
운영 중인 스택도 단순하지 않았다. MySQL 데이터베이스 30개(총 248GB), Nginx 가상 호스트 34개, GitLab EE, Neo4j 그래프 DB, Supervisor로 관리되는 다수의 백그라운드 워커, Gearman 잡 큐, 그리고 수십만 명이 사용하는 라이브 모바일 앱까지 포함된 스택이었다. 여기에 CentOS 7(EOL)에서 AlmaLinux 9.7로의 OS 전환까지 동시에 수행했다.
이 조건에서 무중단 이전을 설계했다는 점이 이 사례를 주목할 만하게 만든다.
핵심 구현 방법
전략의 핵심은 "어느 순간에도 트래픽이 끊기지 않는 구조를 만든 뒤 전환한다"는 원칙이다. 6단계로 설계됐다.
1단계: 새 서버에 풀 스택 사전 구성
DNS를 건드리기 전에 Nginx, PHP, MySQL 8.0, Neo4j, GitLab EE, Node.js, Supervisor, Gearman을 모두 설치하고 기존 서버와 동일하게 설정했다. Let's Encrypt 인증서는 /etc/letsencrypt/ 디렉토리 전체를 rsync로 복사했고, 전환 완료 후 certbot renew --force-renewal로 일괄 갱신했다.
2단계: 웹 파일 rsync 복제
약 65GB, 150만 개 파일을 --checksum 플래그와 함께 rsync로 복제했다. 전환 직전에 증분 동기화를 한 번 더 실행해 초기 복제 이후 변경된 파일을 반영했다.
3단계: MySQL 마스터-슬레이브 리플리케이션
248GB 데이터를 오프라인 dump/restore로 처리하면 수일이 걸린다. 대신 mydumper를 48코어 병렬 처리로 활용해 덤프와 로드를 수 시간 내에 완료했다. 덤프 메타데이터에 기록된 binlog 포지션을 시작점으로 리플리케이션을 설정해 두 서버의 데이터를 실시간으로 동기화했다.
여기서 발생한 실제 문제가 있었다. 덤프가 두 번에 나눠 진행되는 사이 일부 테이블에 쓰기가 발생해 1062(Duplicate Key) 에러로 리플리케이션이 멈췄다. 해결책은 SET GLOBAL slave_exec_mode = 'IDEMPOTENT'였다. 중복 키와 누락 행 에러를 자동으로 건너뛰어 리플리케이션이 정상 추적 상태에 도달했다.
또 하나의 함정은 SUPER 권한이었다. 애플리케이션 DB 사용자 24명 전원에게 SUPER 권한이 부여돼 있어서 read_only = 1이 무력화됐다. 전환 전에 모든 사용자에서 SUPER를 회수해야 슬레이브 보호가 제대로 작동한다.
4단계: DNS TTL 사전 축소
DigitalOcean DNS API로 A, AAAA 레코드의 TTL을 3600초에서 300초로 낮췄다. MX, TXT 레코드는 건드리지 않았다. 메일 레코드 TTL 변경은 전달성 문제를 유발할 수 있기 때문이다. 기존 TTL이 전 세계에서 만료되도록 1시간 대기한 뒤 다음 단계로 넘어갔다.
5단계: 기존 서버 Nginx를 리버스 프록시로 전환
34개 Nginx 설정 파일을 손으로 수정하는 대신, Python 스크립트로 모든 server {} 블록을 파싱해 새 서버로 프록시하는 설정으로 자동 교체했다. DNS 전파가 완료되기 전까지 구 IP로 들어오는 요청도 새 서버로 투명하게 포워딩됐다.
6단계: DNS 전환 및 순서 제어
전환 순서가 중요하다. 새 서버에서 슬레이브를 중단하고 read_only를 해제한 뒤, 구 서버의 Supervisor를 멈추고, Python 스크립트로 모든 A 레코드를 새 서버 IP로 일괄 변경했다. DNS 반영까지 약 5분, 전체 전환은 10초 내에 완료됐다.
전환 후 추가 작업도 있었다. GitLab 프로젝트 웹훅이 여전히 구 서버 IP를 가리키고 있어서, GitLab API를 통해 전체 프로젝트를 스캔하고 일괄 업데이트하는 스크립트를 별도로 작성했다.
실전에서 주의할 점
MySQL 5.7 → 8.0 업그레이드는 예상치 못한 문제를 낸다. 이 사례에서는 import 후 mysql.user 테이블의 컬럼 구조가 맞지 않아 mysql.infoschema가 누락됐고 사용자 인증이 깨졌다. mysqld --upgrade=FORCE를 시도했지만 sys 스키마가 뷰가 아닌 일반 테이블로 임포트된 탓에 첫 시도가 실패했다. DROP DATABASE sys 후 재실행으로 해결했다. 마이그레이션 전 mysqlcheck --check-upgrade로 호환성을 반드시 검증해야 한다.
로컬 /etc/hosts를 이용한 사전 검증은 필수다. 로컬 머신의 hosts 파일에 도메인들을 새 서버 IP로 임시 매핑해두면, 실제 사용자 트래픽에 영향을 주지 않고 모든 API 엔드포인트와 관리자 패널을 새 서버 기준으로 검증할 수 있다. DNS 전환 전 이 검증 단계를 건너뛰는 것은 위험하다.
전용 서버는 퍼블릭 클라우드의 대체재가 아닌 보완재다. 오토스케일링, 로드 밸런서, 매니지드 DB 같은 클라우드 생태계 기능을 적극적으로 쓰는 팀이라면 단순 가격 비교만으로 전환을 결정하면 안 된다. 반면 워크로드가 예측 가능하고 단일 대형 서버로 충분한 구조라면, 전용 서버 비용은 퍼블릭 클라우드 대비 압도적으로 낮다.
전체 마이그레이션 소요 시간은 약 24시간이었고, 사용자에게 미친 다운타임은 0분이었다.
자주 묻는 질문
Q.mydumper를 써야 하는 이유가 있나? mysqldump도 되지 않나?
mysqldump는 단일 스레드로 동작한다. 수백 GB 규모의 데이터베이스를 mysqldump로 처리하면 내보내기와 불러오기 모두 수일이 걸릴 수 있다. mydumper와 myloader는 멀티스레드 병렬 처리를 지원해, 코어가 많은 서버에서 같은 작업을 수 시간 내에 완료한다. 대규모 MySQL 마이그레이션에서 mydumper 사용은 선택이 아니라 필수에 가깝다.
Q.DNS TTL을 낮추기 전 1시간을 기다리는 이유는?
TTL 값을 낮춰도 기존 TTL이 만료되기 전까지 전 세계 DNS 리졸버는 이전 값을 캐싱하고 있다. 기존 TTL이 3600초(1시간)였다면, TTL 변경 후 최소 1시간은 기다려야 대부분의 리졸버가 새 TTL(300초)로 갱신된다. 이 대기 없이 DNS를 전환하면 일부 사용자가 최대 1시간 동안 구 서버로 접근할 수 있다.
Q.Hetzner 전용 서버는 퍼블릭 클라우드와 비교해 어떤 단점이 있나?
가장 큰 차이는 관리 부담과 유연성이다. 하드웨어 장애 시 Hetzner가 교체해주지만, 대응 시간이 퍼블릭 클라우드 SLA와 다를 수 있다. 오토스케일링이 없어서 트래픽 급증에 즉시 대응하기 어렵고, 스냅샷이나 클론 같은 클라우드 편의 기능도 직접 구성해야 한다. 또한 서버 1대에 모든 서비스를 올리면 단일 장애점이 생기므로 별도 장애 대응 계획이 필요하다.