2023 · Nudge Healthcare

19억 건의 테이블을 점검 시간 1시간 내에 마이그레이션

배경

타임스프레드에는 랜덤 상자라는 기능이 있습니다. 광고를 보면 캐시가 무작위로 담긴 상자가 지급되고, 그 상자를 열면 캐시가 들어오는 단순한 기능이었습니다. 상자 하나는 luckybox 테이블의 한 행이었습니다. 사용자가 늘면서 이 테이블에 19억 건이 쌓였는데, 기본 키가 INT 로 선언되어 있어 상한인 21억까지 두 달 정도밖에 남지 않은 상태였습니다. 이 테이블을 BIGINT 로 선언한 새 테이블로 옮겨야 했습니다.

첫 번째 시도

먼저 서비스의 특징을 조사했습니다. 상자는 24시간이 지나면 만료되고, 랭킹을 처리하는 배치 프로그램은 최근 1개월 데이터만 읽고 있었습니다. 점검 시간 안에 이 데이터만 옮기면 서비스를 다시 띄울 수 있다는 뜻이었습니다.

19억 건 가운데 실제로 읽히는 것은 최근 한 달치뿐

팀장님과 논의해 평일 새벽에 서비스를 1시간 멈추고, 미리 준비한 마이그레이션 스크립트를 실행하기로 했습니다. 스크립트는 아래 작업을 순서대로 수행합니다.

  • 기존 테이블의 이름을 RENAME 해 연결을 끊습니다.
  • 새 테이블을 BIGINT 로 다시 만듭니다.
  • 혹시 모를 중복을 막기 위해 PK 가 22억부터 시작하게 합니다.
  • 1개월 전 데이터가 몇 번 ID 인지 알아냅니다.
  • MySQL 프로시저로 100건씩 배치로 데이터를 복사합니다.

스크립트를 짜면서 가장 중요하게 본 것은 '롤백이 가능한가'였습니다. 작업 도중 문제가 생기면 즉시 멈출 수 있어야 했고, 진행 상황도 눈으로 볼 수 있어야 했습니다. 데이터 삽입은 쿼리 한 줄로도 작성할 수 있었지만, 진행 상황을 알 수 없고 중간에 멈출 수도 없어 작업을 잘게 쪼개기로 했습니다.

마이그레이션은 무사히 진행됐습니다. 1개월치 데이터가 들어갔고, 새 상자는 22억부터 만들어지기 시작했습니다. 그런데 어째서인지 안드로이드 앱에서 크래시가 나기 시작했습니다. 남은 점검 시간이 많지 않아 준비해둔 순서대로 롤백했고, 서비스는 이전 모습으로 돌아갔습니다.

원인

이유는 단순했습니다. 앱에서도 ID 를 INT 로 다루고 있어 21억이 넘는 값을 처리하지 못한 것입니다. 서버만 고쳐서 될 문제가 아니었습니다. 다른 방법을 찾아야 했습니다.

두 번째 시도

팀장님과 해결책을 논의하다가 문득 INT 에는 음수 영역도 있다는 사실이 떠올랐습니다. 다행히 unsigned 로 선언되어 있지 않아, PK 를 음수로 저장하는 방법을 두고 이야기를 나눴습니다. 팀장님께서도 좋은 방법이라고 하셔서 곧바로 작업에 들어갔습니다.

트리거가 PK 를 음수로 바꿔 끼우는 흐름

BEFORE INSERT 트리거를 걸어, 값이 들어올 때 시퀀스 테이블에서 다음 값을 읽어 PK 를 음수로 바꿔 넣도록 했습니다. 테스트 서버에서 서버와 앱이 음수 PK 를 모두 문제없이 처리하는 것을 확인하고, 새벽에 트리거를 설치했습니다.

그날 이후 삽입되는 모든 데이터의 PK 가 음수로 들어갔고, 앱에서도 문제가 생기지 않았습니다. 이 조치로 당장의 장애를 막고 약 2년의 리팩토링 시간을 벌었습니다.