400만 건짜리 테이블에 컬럼을 추가해야 했고 그동안 서비스가 멈춘다고 보고했다. 예상 시간이 20분이었으므로 점검 시간을 잡으려던 참이었다.
Table of contents
Open Table of contents
잠기는지 확인
점검 공지를 내기 전에 정말 잠기는지부터 확인했는데 DB 판을 보니 그 판에서는 끝에 컬럼을 추가하는 것이 온라인으로 처리돼 잠기지 않았다.
ALTER TABLE orders ADD COLUMN memo TEXT, ALGORITHM=INPLACE, LOCK=NONE;
알고리즘과 잠금 수준을 명시하면 그 방식으로 못 할 때 오류가 나므로 조용히 잠기는 일이 없다. 시험 환경에서 먼저 걸어 보니 실제로 잠기지 않고 그대로 통과했다. 잠긴다는 것은 이 작업의 성질이 아니라 판과 변경 종류의 조합이 정하는 것이었다.
잠기는 변경과 안 잠기는 변경
이 참에 무엇이 잠기는지 정리했다. 끝에 컬럼을 추가하는 것과 인덱스를 추가하는 것과 컬럼 이름을 바꾸는 것은 안 잠기고 컬럼 타입을 바꾸는 것과 순서를 바꾸는 것과 기본 키를 바꾸는 것은 잠긴다.
잠기는 변경은 다른 방법이 필요했는데 새 컬럼을 만들고 값을 나눠서 옮긴 뒤 읽는 쪽을 바꾸고 마지막에 옛 컬럼을 지우는 순서로 한다. 단계가 여럿이지만 각 단계가 짧아서 안 멈춘다.
값 채우기의 분할
컬럼을 추가한 뒤 값을 채우는 것도 한 번에 하면 오래 잠근다.
do {
$n = DB::update("UPDATE orders SET memo = '' WHERE memo IS NULL LIMIT 5000");
usleep(100000);
} while ($n > 0);
5,000건씩 나누면 각각은 짧게 잠근다. 사이에 쉬는 것을 넣은 것은 계속 돌면 다른 요청이 밀리기 때문이다. 부하가 높으면 더 쉬게 하고 정산 배치가 도는 시간에는 아예 안 돌게 했다.
진행 표시와 중단 지점
채워야 할 전체 건수를 미리 세어 두고 진행률을 화면에 찍게 했다. 20분 걸리는 것을 아무 표시 없이 기다리는 것과 몇 퍼센트인지 보면서 기다리는 것은 다르다.
if (file_exists('/tmp/stop-migration')) {
echo "\n중단 요청. 여기까지 처리됨.\n";
break;
}
파일 하나로 멈출 수 있게도 했다. 중간에 멈춰도 다시 시작하면 남은 것부터 하므로 멈추는 비용이 낮아야 이상하다 싶을 때 실제로 멈춘다.
되돌릴 수 있는 구간
각 단계가 되돌릴 수 있는 것인지도 함께 따져 봤다. 컬럼을 추가한 것은 지우면 되지만 자료가 들어간 뒤에는 그 자료를 잃고, 값을 채운 것은 원래 비어 있었으므로 되돌릴 수 있다.
읽는 쪽을 바꾸고 나면 되돌리기 어려워지므로 그 단계 전에 확인을 충분히 했다. 어느 지점까지가 되돌릴 수 있는 구간인지 알고 있으면 그 앞에서 검증을 몰아서 한다.
사본에서 재 본 시간과 공간
시험 환경은 자료가 적어 시간이 안 맞았다. 4만 건에 12초가 나왔는데 운영 자료를 복원한 사본에서는 400만 건에 18분이 걸렸고 예상한 20분과 비슷했다.
사본에서 재 보니 시간만 알게 된 것이 아니었다. 작업 중에 테이블 크기만큼 임시 공간이 필요하다는 것도 나왔다. 테이블이 12기가라 12기가가 더 필요한데 남은 공간이 8기가였다. 미리 재 보지 않았으면 운영에서 중간에 실패했을 것이다. 공간을 늘리고 나서 실행했다.
작업하는 동안 실제로 요청을 계속 보내면서 응답 시간도 함께 봤다. 한 번 느려진 구간이 있었지만 실패한 요청은 없었다. 안 멈춘다고 말하기 전에 요청을 보내 확인한 것이다.
정리
- 잠기는지는 판과 변경 종류에 따라 다르므로 확인한다
- 알고리즘과 잠금 수준을 명시하면 조용히 잠기는 일이 없다
- 잠기는 변경은 새 컬럼을 만들고 옮기는 방식으로 한다
- 값 채우기는 나눠서 하고 사이에 쉰다
- 진행을 보이고 멈출 수 있게 한다
- 되돌릴 수 있는 구간이 어디까지인지 알고 그 앞에서 검증한다
- 사본에서 미리 해 보면 시간과 공간을 둘 다 알 수 있다
- 안 멈춘다고 말하기 전에 요청을 보내 확인한다