Azure MySQL 무중단 업그레이드: Blue/Green 전략 구현 (AKS + Node.js mysql2 PoolCluster)¶
AKS와 Node.js mysql2 PoolCluster를 이용한 Azure MySQL Blue/Green 전환을 설명합니다.
개요¶
AWS RDS는 Blue/Green Deployments를 통해 별도의 Green 환경을 자동 프로비저닝하고 DNS 전환으로 무중단 MySQL 버전 업그레이드를 지원합니다.
Azure MySQL Flexible Server는 MySQL binlog 복제(SQL 스레드 기반) 를 활용하며, Custom Private DNS Zone의 CNAME 교체 + Session Kill 패턴을 결합하여 무중단 버전 업그레이드를 달성할 수 있습니다.
| AWS RDS | Azure (이 문서) | |
|---|---|---|
| Green 환경 | Blue/Green 자동 프로비저닝 | MySQL Flexible Server (VNet Integration) 수동 구성 |
| 데이터 동기화 | 자동 복제 | MySQL binlog 복제 (CHANGE REPLICATION SOURCE TO, IO/SQL 스레드) |
| 트래픽 전환 | RDS Endpoint DNS 자동 전환 | Custom DNS Zone CNAME 교체 (TTL=5s) |
| 기존 커넥션 처리 | 자동 drain | Session Kill → PoolCluster 자동 재연결 |
| 앱 재배포 | 불필요 | 불필요 (DB_HOST 불변) |
| Rollback | Switchback | CNAME 원복 + super_read_only 해제 |
이 문서는 해당 아키텍처의 설계 제안과 AKS + Node.js(mysql2 PoolCluster) 환경에서의 실제 동작 검증 결과를 담고 있습니다.
핵심 원리¶
커트오버 흐름¶
[old-db] super_read_only = ON
│
└─> [new-db] SHOW REPLICA STATUS: Seconds_Behind_Source = 0 대기
│
└─> STOP REPLICA; RESET REPLICA ALL (standalone 스스로 실행, instant)
│
└─> CNAME 교체: primary → new-db FQDN (TTL=5s)
│
└─> old-db 세션 KILL
│
└─> PoolCluster 재연결
│
└─> new-db 연결 성공
포인트:
- super_read_only = ON 이후 new-db의 Seconds_Behind_Source = 0 이 되는 시점이 커트오버에 안전한 시점
- STOP REPLICA 전에 CNAME을 바꾸면 new-db가 아직 복제 중이었다가 old-db의 write를 받을 수 있음 — 순서 중요
- STOP REPLICA; RESET REPLICA ALL 은 SQL 명령 1개, instant (제관형 API 호출 보다 ~64s 단우)
DNS 커넥션 재연결 동작¶
Session Kill 수신
└─> mysql2 pool: 해당 커넥션 폐기
└─> 다음 쿼리: 신규 커넥션 획득 시도
└─> FQDN 재조회 (CNAME TTL=5s → new-db FQDN → VNet IP)
└─> new-db 연결 성공
mysql2 poolCluster의 재연결은 반드시 새 커넥션 생성을 통해서만 발생합니다.
이미 열린 커넥션은 DNS가 바뀌어도 기존 IP를 유지하므로, Session Kill이 필수 단계입니다.
DNS 체인 구조¶
primary.db.{prefix}.internal (앱 접속 FQDN — 코드/Secret 불변)
↓ CNAME TTL=5s ← 컷오버 시 이 값만 변경
{prefix}-old-db.mysql.database.azure.com
↓ Azure DNS → Private DNS Zone mysql.database.azure.com
A record: {prefix}-old-db → VNet Integration IP (10.2.0.x)
(VNet Integration: 서버 NIC가 VNet 내 mysql-subnet에 직접 위치)
장점:
- 앱 코드/Secret에서 DB_HOST 불변 (primary.db.{prefix}.internal)
- VNet Integration으로 old-db과 new-db가 동일 VNet 안에 위치 → SQL binlog 복제 네트워크 접근 가능
- TLS: rejectUnauthorized:true 만 유지 — mysql2는 hostname 검증 미적용 (실증). custom FQDN 사용 시에도 접속 영향 없음. servername 불필요
코드 변경 사항 (2건)¶
프로덕션 코드(urecaDbHandler.js)에 실제로 추가한 변경입니다.
1. makeConfig() — 커넥션 안정성 옵션 추가¶
const rtn = {
...props,
ssl: sslConfig,
+ connectTimeout: 10000, // 기본값 무한대 → 신규 DB 연결 hang 방지
+ enableKeepAlive: true, // TCP keepalive로 idle 커넥션 조기 감지
+ keepAliveInitialDelay: 10000, // keepalive probe 시작 시간 (ms)
};
2. tsAbWrapper — rollback 에러 분리¶
Session Kill 발생 시 rollbackAsync()도 동일 커넥션에서 실행되므로 2차 에러가 원래 에러를 덮어씁니다.
DB는 세션 종료 시 서버 측에서 자동 rollback하므로 클라이언트 rollback 실패는 warn 로깅만 하고 무시합니다.
} catch (err) {
- await conn.rollbackAsync();
- throw err;
+ try {
+ await conn.rollbackAsync();
+ } catch (rollbackErr) {
+ logger.w('tsAbWrapper.rollback', { code: rollbackErr.code, message: rollbackErr.message });
+ }
+ throw err;
}
인프라/설정 변경 사항 (1건)¶
DB_HOST — Custom Internal FQDN 사용¶
앱 Secret의 DB_HOST를 Azure MySQL FQDN 대신 Custom Private DNS Zone의 CNAME 으로 설정합니다.
커트오버 후에도 DB_HOST는 변경하지 않습니다.
실증 확인 사항¶
코드 변경은 아니지만, 이 시나리오의 전제가 되는 동작을 시뮬레이션에서 실증 확인했습니다.
ssl.servername 불필요 — Custom FQDN 접속 가능¶
원본 코드에 servername 설정은 원래 없습니다. 시뮬레이션에서 mysql2가 TLS hostname 검증을 수행하지 않음을 실증했습니다.
servername: 'wrong.example.com'으로 설정해도 접속 성공 → hostname 검증 미적용rejectUnauthorized: true는 CA 체인 검증만 수행- 따라서
DB_HOST를 Custom FQDN(primary.db.{prefix}.internal)으로 설정해도 TLS 접속에 영향 없음 - old/new DB 모두 동일 wildcard cert(
*.mysql.database.azure.com) → 인증서 교체 불필요
removeNodeErrorCount — 프로덕션 미설정 유지¶
원본 코드에 removeNodeErrorCount는 원래 없습니다 (PoolCluster 기본값 사용). 시뮬레이션에서 이 설정의 위험성을 확인했습니다.
| 값 | 동작 | 시뮬레이션 결과 |
|---|---|---|
| 1 | 에러 1회 시 노드 즉시 제거 | 50-pod 동시 connection storm에서 노드 영구 사망, 재기동 불가 |
| 5 (시뮬레이션 사용) | 5회 연속 에러 시 노드 제거 | 일시적 ECONNREFUSED 허용, storm 이후 정상 복구 |
| 미설정 (프로덕션 현행) | PoolCluster 기본 동작 | 일시 장애에도 노드 유지 — 그대로 유지 권장 |
DNS Cutover 절차 (상세)¶
Phase 0. 사전 확인¶
명령어 보기
Phase 1. 사전 준비¶
명령어 보기
# 1. old-db write 차단
# 방법 A: super_read_only (자체 관리형 MySQL)
mysql -h <old-db> -e "SET GLOBAL super_read_only = ON;"
# 방법 B: REVOKE + ACCOUNT LOCK (Azure MySQL Flexible Server)
# ⚠ Azure MySQL은 SUPER 권한을 부여하지 않아 super_read_only 설정 불가
# 대안: 앱 유저의 write 권한을 제거하고 계정을 잠금
mysql -h <old-db> -e "
REVOKE INSERT, UPDATE, DELETE ON simdb.* FROM 'appuser'@'%';
ALTER USER 'appuser'@'%' ACCOUNT LOCK;
FLUSH PRIVILEGES;"
# 2. new-db 복제 지연 = 0 확인 (SQL 스레드 기반)
mysql -h <new-db> -e "SHOW REPLICA STATUS\G"
# → Replica_IO_Running: Yes
# → Replica_SQL_Running: Yes
# → Seconds_Behind_Source: 0 ← 이 값이 0 이면 커트오버 안전
# (write 차단 이후 old-db 에 추가 write 없으므로 수초 내 0 수렴)
# 3. new-db → standalone 승격 (SQL 명령, instant)
mysql -h <new-db> -e "STOP REPLICA; RESET REPLICA ALL;"
# → 복제 IO/SQL 스레드 즉시 중단. new-db 는 read/write standalone 서버가 됨
# ⚠ CNAME 변경 전 실행 — 변경 후에는 already standalone
Phase 2. DNS Cutover (CNAME 변경)¶
명령어 보기
# Custom DNS Zone CNAME: old-db FQDN → new-db FQDN
az network private-dns record-set cname set-record \
--resource-group <rg> \
--zone-name db.<prefix>.internal \
--record-set-name primary \
--cname <prefix>-new-db.mysql.database.azure.com
# Pod에서 DNS 전파 확인 (CNAME TTL=5s이므로 최대 5초 대기)
kubectl exec -it <pod> -- nslookup primary.db.<prefix>.internal
# → canonical name = <prefix>-new-db.mysql.database.azure.com ← 변경 확인
Phase 3. 기존 DB 세션 Kill¶
Azure MySQL Flexible Server에서는
KILL CONNECTION대신 내장 프로시저mysql.az_kill(id)를 사용합니다. 일반KILL문은 SUPER 권한이 필요하여 Azure 에서 실행 불가합니다.
SQL 보기
-- Azure MySQL: mysql.az_kill() 사용
-- 앱 세션 목록 조회 후 루프로 kill
SELECT id FROM information_schema.processlist WHERE user = '<app-user>';
-- → 각 id에 대해: CALL mysql.az_kill(<id>);
-- 자체 관리형 MySQL: KILL CONNECTION 사용
-- 기존 DB에서 실행
-- 1. 진행 중인 쿼리 확인 (완료 대기)
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE command != 'Sleep' AND user = '<app-user>';
-- 2. app 세션 일괄 kill (Sleep 포함)
SELECT CONCAT('KILL ', id, ';')
FROM information_schema.processlist
WHERE user = '<app-user>';
-- 출력된 KILL 문을 복사하여 실행
-- 또는 프로시저로 일괄 처리
DELIMITER $$
CREATE PROCEDURE kill_app_sessions()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_id BIGINT;
DECLARE cur CURSOR FOR
SELECT id FROM information_schema.processlist
WHERE user = '<app-user>' AND command = 'Sleep';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
loop_label: LOOP
FETCH cur INTO v_id;
IF done THEN LEAVE loop_label; END IF;
KILL CONNECTION v_id;
END LOOP;
CLOSE cur;
END$$
DELIMITER ;
CALL kill_app_sessions();
Phase 4. 안정화 및 확인¶
명령어 보기
# 신규 DB에서 app 커넥션 수렴 확인
mysql -h <new-db> -e "SHOW PROCESSLIST;" | grep '<app-user>'
# 에러 로그 확인 (PROTOCOL_CONNECTION_LOST / ER_CONNECTION_KILLED 후 정상 재연결 여부)
kubectl logs -l app=<app-label> --since=2m | grep -E "reconnected|DOWNTIME|RECOVERED|CONNECTION_LOST"
# write 발생 여부 확인 (rollback 대비)
mysql -h <new-db> -e "
SELECT COUNT(*) as active_writes
FROM information_schema.innodb_trx
WHERE trx_started > NOW() - INTERVAL 5 MINUTE;"
Phase 5 (재배포) 불필요 —
DB_HOST=primary.db.<prefix>.internal은 CNAME 교체 후에도 동일.
Rollback 절차 보기
Rollback 가능 조건: new-db에 write가 발생하지 않은 시점(= super_read_only ON 오류로만 발생). new-db에 이미 데이터가 작성된 경우는 데이터 동기화 후 교체해야 하며, 데이터 팀 개입이 필요합니다.
# Custom DNS Zone CNAME 원복
az network private-dns record-set cname set-record \
--resource-group <rg> \
--zone-name db.<prefix>.internal \
--record-set-name primary \
--cname <prefix>-old-db.mysql.database.azure.com
# old-db super_read_only 해제
mysql -h <old-db> -e "SET GLOBAL super_read_only = OFF;"
| write 발생 여부 | Rollback 방법 |
|---|---|
| 없음 | CNAME 원복 + SET GLOBAL super_read_only = OFF → 안전 |
| 있음 | 신규 DB → 기존 DB 데이터 동기화 필요 → 위험, 데이터 팀 개입 필요 |
시뮬레이션 (simulation/)¶
시뮬레이션 구성, 설계 결정, 실행 순서 보기
목적¶
AKS Pod + MySQL Flexible Server × 2 환경에서 Custom DNS Zone CNAME cutover 시 안정화 시간 및 시나리오 무결성 검증.
구성¶
Custom Private DNS Zone: db.{prefix}.internal (VNet linked)
앱 접속 FQDN: primary.db.{prefix}.internal (DB_HOST, 코드/Secret 불변)
↓ CNAME TTL=5s ← Cutover 시 이 값만 변경
{prefix}-old-db.mysql.database.azure.com
↓ Private DNS Zone mysql.database.azure.com (VNet Integration)
A record: {prefix}-old-db → VNet IP (10.2.0.x)
Cutover:
CNAME 변경: primary → {prefix}-new-db.mysql.database.azure.com
기존 열린 커넥션: Session Kill → 재연결 시 CNAME 재조회 → new-db 로 수렴
검증 시나리오: MySQL binlog 복제 (v8.0→v8.4) +
STOP REPLICA; RESET REPLICA ALL실시간 승격 + Custom DNS CNAME 커트오버 + Session Kill
핵심 검증: 버전 업그레이드 포함 무중단 전환 — 인프라 타이밍 및 app 재연결 시간
주요 설계 결정¶
| 항목 | 내용 | 이유 |
|---|---|---|
| DNS Zone (앱 접속) | db.{prefix}.internal Custom Private DNS Zone |
DB_HOST 불변, CNAME 교체만으로 컷오버 |
| DNS Zone (DB 해석) | mysql.database.azure.com |
VNet Integration 자동 A record 등록 → VNet IP 해석 |
| DNS Zone Group | 미사용 (VNet Integration 방식) | VNet Integration으로 DNS 자동 관리 — PE/Zone Group 불필요 |
| CNAME TTL | 5s | 커트오버 후 DNS 전파 최대 5초 이내 |
| DB SKU | GeneralPurpose 이상 권장 | 빠른 응답속도 필요 시 |
| 데이터 동기화 | MySQL binlog 복제 (v8.0→v8.4) | CHANGE REPLICATION SOURCE TO + START REPLICA. IO/SQL 스레드 별도 실행 — SHOW REPLICA STATUS 으로 lag 실시간 모니터링 가능 |
| new-db 승격 | STOP REPLICA; RESET REPLICA ALL |
SQL 명령 1개, instant (제관형 API ~64s 마이그레이션 없음) |
| TLS | rejectUnauthorized: true |
CA 체인 검증만 수행. hostname 검증 미적용 (실증) — servername 불필요 |
removeNodeErrorCount |
시뮬레이션: 1 / 프로덕션: 미적용 | 시뮬레이션에서 빠른 재연결 측정. 프로덕션은 일시 장애에도 노드 제거 리스크 |
server_name 컬럼 |
@@hostname INSERT |
cutover 후 어느 DB 에 write 됐는지 확인 |
실행 순서¶
# 터미널 A: 인프라 배포 (~15분, old-db만 생성)
.\scripts\01-deploy-infra.ps1 -ResourceGroup "rg-dbsim" -Password "YourP@ss1"
# 터미널 A: old-db 초기화 + K8s 배포
.\scripts\02-setup-db.ps1
# 터미널 A: new-db 생성(standalone v8.4, VNet Integration) + SQL binlog 복제 설정
.\scripts\02b-setup-replication.ps1
# 터미널 A: 이미지 빌드/푸시 + Deployment rollout
.\scripts\03-build-push.ps1
# 터미널 B: CRUD 로그 실시간 스트리밍 (cutover 전 켜두기)
.\scripts\04-watch-logs.ps1
# 터미널 A: Cutover 실행 (SHOW REPLICA STATUS lag=0 → STOP REPLICA → CNAME → session kill)
.\scripts\05-cutover.ps1
기대 로그 패턴 (04-watch-logs)¶
실험 결과¶
동일 환경에서 5회 반복 커트오버 측정 후 평균치를 기재합니다.
환경¶
| 항목 | 값 |
|---|---|
| Resource Group | rg-dbsim-v3 (Korea Central) |
| AKS | 50 Pods (crud-worker), 5 workers/pod, ~250 sessions |
| old-db sessions | 250 |
| old-db | MySQL 8.0.21, GeneralPurpose (VNet Integration) |
| new-db | MySQL 8.4.x-azure, Standalone (VNet Integration) |
| CNAME TTL | 5s |
| Write 차단 방식 | REVOKE + ACCOUNT LOCK |
| Session Kill | mysql.az_kill(id) |
removeNodeErrorCount |
5 |
개별 실행 결과 (5회)¶
| 항목 | Run 1 | Run 2 | Run 3 | Run 4 | Run 5 |
|---|---|---|---|---|---|
| 총 소요시간 | 37s | 32s | 28.2s | 31.1s | 30.0s |
| Write 차단 → Lag=0 | 10s | 5s | 1.2s | 5.5s | 1.8s |
| CNAME 전파 | <1s | <1s | 1.6s | 1.4s | 1.6s |
| Session Kill | 250/R1 | 250/R1 | 250/R1 | 250/R1 | 250/R1 |
| 앱 에러 | 3 | 4 | 4 | 3 | 5 |
| 전환 후 ops/s | ~37 | ~45 | 49 | 43 | 23 |
측정 요약 (5회 평균)¶
| 측정값 | 결과 |
|---|---|
STOP REPLICA; RESET REPLICA ALL |
instant (< 1s) |
Write 차단 → Lag=0s 수렴 |
~5s (1~10s 범위) |
| CNAME 전파 (Pod 내 CoreDNS) | ~1s (TTL=5s, 캐시 상태에 따라 편차 가능) |
| Session Kill → new-db 수렴 | ~12s |
| 총 커트오버 소요시간 | ~32s (28~37s 범위) |
| 앱 에러 | ~4건 (즉시 복구, 이후 증가 없음) |
| 전환 후 ops/s | ~39/s (전환 전과 동일 수준으로 복구) |
| write split (old-db에 INSERT) | 없음 |
| Phase 5 재배포 | 불필요 |
설계 고찰: Session Kill vs. Graceful Drain¶
| 방식 | 장점 | 단점 |
|---|---|---|
| Session Kill (현재 방식) | 앱 코드 변경 없이 강제 재연결. DB_HOST 불변 |
Kill 시점 진행 중 트랜잭션 롤백 |
| 앱 측 graceful drain | 진행 중 트랜잭션 완료 후 재연결. 에러 0건 가능 | drain 로직 추가 필요. write 차단 후 대기 시간 증가 |
| K8s Rolling Update | 배포 프로세스와 통합 가능 | 재배포 필요. Pod 롤링 수십 초~수분 추가 |
Session Kill은 앱 코드 변경 없이 수 ms 재연결을 달성하는 가장 단순한 방법입니다.
핵심 결론¶
- Session Kill 1라운드에 250개 전체 kill 성공 —
mysql.az_kill()활용, 50-pod 규모에서도 추가 라운드 불필요 (5회 전수 동일) - 앱 에러 평균 ~4건 (즉시 복구) —
PROTOCOL_CONNECTION_LOST→ PoolCluster 자동 재연결, 이후 err 증가 없음 - Replication lag 수렴 평균 ~5s — write 차단 후 잔여 binlog 소화 (1~10s 범위). 프로덕션 피크에서는 수십 초 가능
- CNAME 전파 평균 ~1s — CoreDNS 캐시 잔여 TTL에 의존 (캐시 상태에 따라 편차 가능)
- 총 커트오버 소요시간 평균 ~32s — 28~37s 범위 (5회 측정)
STOP REPLICA; RESET REPLICA ALLinstant — 관리형 API ~64s 없음- 앱 재배포 불필요 —
DB_HOST=primary.db.{prefix}.internal불변, CNAME만 교체 - VNet Integration — old/new DB가 동일 VNet → SQL binlog 복제 직접 통신 (Private Endpoint 불필요)
⚠️ 프로덕션 적용 시 유의사항¶
시뮬레이션에서 발견한 실제 운영 환경 적용 시 주의점입니다.
1. Azure MySQL Flexible Server 제약사항¶
Azure MySQL은 SUPER 권한을 부여하지 않습니다. 아래 명령은 직접 실행 불가하며, Azure 전용 대안을 사용해야 합니다.
| 일반 MySQL | Azure MySQL 대안 |
|---|---|
SET GLOBAL super_read_only = ON |
REVOKE + ACCOUNT LOCK |
KILL CONNECTION <id> |
CALL mysql.az_kill(<id>) |
CHANGE REPLICATION SOURCE TO ... |
CALL mysql.az_replication_change_master(host, user, pass, port, file, pos, '') |
START REPLICA |
CALL mysql.az_replication_start |
STOP REPLICA |
CALL mysql.az_replication_stop |
Write 차단 (Phase 1) — Azure 방식:
-- super_read_only 대신: 앱 유저의 write 권한 제거 + 계정 잠금
REVOKE INSERT, UPDATE, DELETE ON <db>.* FROM '<app-user>'@'%';
ALTER USER '<app-user>'@'%' ACCOUNT LOCK;
FLUSH PRIVILEGES;
-- 확인
SELECT account_locked FROM mysql.user WHERE user = '<app-user>'; -- Y
SHOW GRANTS FOR '<app-user>'@'%'; -- SELECT only
Session Kill (Phase 3) — Azure 방식:
-- 세션 목록 조회
SELECT id FROM information_schema.processlist WHERE user = '<app-user>';
-- 각 id에 대해 kill (루프 또는 스크립트)
CALL mysql.az_kill(<id>);
-- kill 후 확인: 0이어야 함
SELECT COUNT(*) FROM information_schema.processlist WHERE user = '<app-user>';
Replication 설정 — Azure 방식:
-- old-db의 binlog position 확보
SHOW MASTER STATUS; -- File, Position 기록
-- new-db에서 replication 설정
CALL mysql.az_replication_change_master(
'<old-db-fqdn>', -- host
'<repl-user>', -- user
'<repl-password>', -- password
3306, -- port
'<binlog-file>', -- master_log_file
<binlog-pos>, -- master_log_pos
'' -- ssl (빈 문자열 = 기본)
);
CALL mysql.az_replication_start;
-- 확인
SHOW REPLICA STATUS\G
-- Replica_IO_Running: Yes, Replica_SQL_Running: Yes, Seconds_Behind_Source: 0
2. require_secure_transport 설정¶
복제 IO 스레드가 old-db에 연결할 때 SSL 없이 접근하므로, old-db에서 반드시 비활성화해야 합니다.
# 복제 전 old-db에서
az mysql flexible-server parameter set \
--resource-group <rg> --server-name <old-db> \
--name require_secure_transport --value OFF
# 복제 완료 + 커트오버 후 복원
az mysql flexible-server parameter set \
--resource-group <rg> --server-name <old-db> \
--name require_secure_transport --value ON
3. mysqldump + --master-data=2 주의¶
--master-data=2는 내부적으로 FLUSH TABLES WITH READ LOCK (FTWRL)을 실행합니다.
활성 write 트래픽이 있는 상태에서 FTWRL은 모든 write를 블로킹하며, 기존 트랜잭션이 완료될 때까지 대기합니다.
| 상황 | 결과 |
|---|---|
| write 없음 (maintenance window) | 정상 완료 |
| 활성 write 중 (250+ sessions) | 무한 대기 — long-running 트랜잭션이 FTWRL 획득 차단 |
권장 방법:
# 방법 1: 앱 트래픽 없는 시점에 dump
kubectl scale deployment <app> --replicas=0
mysqldump --single-transaction --master-data=2 --no-tablespaces <db> > dump.sql
kubectl scale deployment <app> --replicas=<N>
# 방법 2: binlog position 별도 확보 후 dump (write 중에도 가능)
mysql -e "SHOW MASTER STATUS;" # → File, Position 기록
mysqldump --single-transaction --no-tablespaces <db> > dump.sql
# 기록해둔 File/Position으로 az_replication_change_master 호출
4. MySQL 버전 업그레이드 쿼리 호환성¶
8.0 → 8.4 업그레이드 시 기존에 정상 동작하던 쿼리가 실패할 수 있습니다.
-- ❌ MySQL 8.4에서 ER_UPDATE_TABLE_USED
UPDATE orders SET qty = qty + 1
WHERE id = (SELECT id FROM orders ORDER BY id DESC LIMIT 1);
-- ✅ derived table로 래핑
UPDATE orders SET qty = qty + 1
WHERE id = (SELECT t.id FROM (SELECT id FROM orders ORDER BY id DESC LIMIT 1) t);
-- ❌ ORDER BY RAND() — full table scan + gap lock → INSERT 블로킹
SELECT id FROM orders ORDER BY RAND() LIMIT 1;
-- ✅ PK 범위 기반 랜덤 선택 (gap lock 최소화)
SELECT id FROM orders
WHERE id >= (SELECT FLOOR(MIN(id) + RAND() * (MAX(id) - MIN(id))) FROM orders)
LIMIT 1;
→ 사전에 new-db에서 전체 쿼리 regression test 수행 필수
5. Rollback 절차
커트오버 후 문제 발생 시 아래 순서로 원복합니다.
-- 1. CNAME 원복
-- az network private-dns record-set cname set-record \
-- --resource-group <rg> --zone-name db.<prefix>.internal \
-- --record-set-name primary --cname <old-db-fqdn>
-- 2. appuser 복원 (old-db에서)
ALTER USER '<app-user>'@'%' ACCOUNT UNLOCK;
GRANT SELECT, INSERT, UPDATE, DELETE ON <db>.* TO '<app-user>'@'%';
FLUSH PRIVILEGES;
-- 3. Replication 정리 (new-db에서)
CALL mysql.az_replication_stop;
RESET REPLICA ALL;
-- 4. Pod 재시작 → old-db로 세션 수렴
-- kubectl rollout restart deployment <app>
| 확인 항목 | 기대값 |
|---|---|
nslookup primary.db.<prefix>.internal |
<old-db-fqdn> |
SELECT account_locked FROM mysql.user WHERE user='<app-user>' |
N |
old-db SHOW PROCESSLIST |
appuser 세션 수렴 |
new-db SHOW REPLICA STATUS |
빈 결과 |