Development··5 min read

무중단 DB 마이그레이션, 말은 쉽지

서비스 안 끄고 DB 스키마를 바꿔야 했던 실전 경험. 계획은 완벽했는데 현실은 달랐다.

테이블 컬럼 하나 바꾸는 게 왜 이렇게 무섭냐

users 테이블의 phone 컬럼을 varchar(20)에서 varchar(15)로 바꾸고, 포맷 검증 로직을 추가해야 했다. 사용자 184,720명의 데이터가 들어있는 테이블이었다.

"ALTER TABLE 한 줄이면 되지 않나?" 맞다. 근데 그 ALTER TABLE이 실행되는 동안 테이블에 락이 걸린다. 우리 서비스는 24시간 운영이고, 점검 시간을 잡으면 CS팀에서 난리가 난다.

처음 세운 계획

1단계: 새 컬럼 phone_new를 추가한다. 2단계: 코드에서 두 컬럼 모두에 쓴다 (dual write). 3단계: 기존 데이터를 배치로 마이그레이션한다. 4단계: 코드에서 새 컬럼만 읽도록 전환한다. 5단계: 옛날 컬럼을 삭제한다.

교과서적인 방법이다. 계획은 완벽했다.

(실제로 2단계에서 이미 일이 꼬이기 시작했다.)

dual write에서 벌어진 일

코드를 수정해서 phonephone_new 모두에 쓰도록 배포했다. 근데 배포 후 5분 만에 에러가 터졌다. ORM에서 phone_new 컬럼에 NOT NULL 제약 조건을 걸어놨는데, 기존 데이터에는 phone_new가 비어 있었기 때문이다.

NOT NULL을 빼고 다시 배포했다. 이번엔 됐다. 근데 이 사이에 3건의 주문이 불완전한 상태로 저장됐다. 수동으로 복구했다. 총 27분간의 소소한 장애.

배치 마이그레이션의 함정

184,720건을 한 번에 UPDATE하면 테이블 락이 걸린다. 그래서 1,000건씩 배치로 나눠서 처리했다. 각 배치 사이에 100ms 딜레이를 넣었다.

총 소요 시간: 43분 12초. 그 사이에 들어온 신규 데이터는 dual write 덕분에 괜찮았다. 근데 배치 실행 중에 DB CPU가 68%까지 올라갔다. 평소 15%였는데. 슬로우 쿼리 알림이 3번 울렸다.

딜레이를 500ms로 늘렸더니 CPU는 안정됐지만 총 시간이 2시간으로 늘어났다. 이런 트레이드오프가 제일 짜증난다.

코드 전환, 여기서도 실수

새 컬럼만 읽도록 코드를 전환했는데, API 응답에서 phone 필드 포맷이 바뀌어 있었다. 프론트엔드에서 하이픈 포함 포맷을 기대했는데 새 컬럼에는 숫자만 들어가 있었다. 프론트 수정 배포까지 18분 걸렸다.

솔직히 이건 마이그레이션 전에 확인했어야 했다. 프론트엔드 코드를 안 본 내 잘못이다.

옛날 컬럼 삭제

DROP COLUMN도 신중하게 해야 한다. 혹시 어딘가에서 하드코딩으로 참조하고 있을 수 있으니까. grep으로 전체 코드베이스를 뒤진 다음에 삭제했다. 1주일 동안 모니터링하고, 문제없는 걸 확인한 후에야 DROP을 실행했다.

다음부터는

테스트 환경에서 실제 데이터 양과 비슷한 규모로 먼저 돌려봐야 한다. 이번에는 테스트 DB에 데이터가 500건밖에 없어서 배치 처리 시간을 과소평가했다. 그리고 프론트엔드 영향도를 같이 확인하는 체크리스트가 필요하다.

무중단 마이그레이션은 기술적으로 어려운 게 아니라, 빠뜨리기 쉬운 게 많아서 어렵다.

관련 글