운영 DB 옮기기: 되돌릴 수 없는 버튼 누르기

aws rds delete-db-instance --skip-final-snapshot ...

엔터를 누르려다 멈췄다.

코드였으면 그냥 누르고 푸시 했다. 잘못 배포하면 롤백하면 되고, 버그는 핫픽스로 덮으면 된다. 우리가 하는 일은 대부분 되돌릴 수 있어서 "일단 하고 깨지면 고친다"가 먹힌다. 근데 이 명령어는 그게 안 된다. 누르면 인스턴스가 영영 사라져서 되돌릴 수가 없다.

운영 DB 마이그레이션이 어려운 건 옮기는 기술이 어려워서가 아니다. 실제로 데이터 옮긴 시간은 덤프 26초, 복원 0.75초가 전부였다. 어려운 건 이 마지막 엔터처럼 못 되돌리는 순간이 중간중간 박혀 있다는 거다. 거기선 "깨지면 고친다"가 안 통하니까, 누르기 전에 맞다는 걸 알고 있어야 한다. 1


Region이라는 비용

앱은 서울인데 RDS는 시드니(이런저런 이유로..)에 있었다. "멀어서 느리겠지" 하고 넘기기 전에 얼마나 느린지부터 쟀다. 서울에서 시드니 RDS까지 TCP 왕복이 139ms, 같은 리전이면 0.2ms다. 쿼리를 던질 때마다 139ms씩 깔고 가는 셈이다.

덤프가 26초나 걸린 것도 같은 이유였다. pg_dump가 스키마 알아내느라 카탈로그를 184번 물어보는데, 한 번 묻고 답 받아야 다음을 묻는 식이라 전부 직렬이다. 184번 × 139ms 하면 25.7초. 12MB라서 느린 게 아니라 시드니라서 느렸던 거다. 옮기고 나니 같은 쿼리 왕복이 0.2~0.9ms로 떨어졌다.

down time의 비용

무중단으로 옮기려면 앱이랑 DB 사이에 PgBouncer를 두고 앱을 여러 대로 띄워야 한다. 우리는 서버 한 대에 앱이 DB로 바로 붙는 구조라, 이걸 하려면 구성을 새로 짜야 했다. 너무 큰 공사이다.

그 공서로 얻는 것이 결국엔 다운타임 1분을 0으로 만드는 거다. 그 1분이 얼마인지는 리허설에서 나왔다. 덤프 26초 + 복원 0.75초 + 앱 재시작 30초. 새벽에 1분 멈추는 걸 0으로 만들겠다고 몇 주를 쓰진 않는다. 데이터가 12GB였으면 덤프만 수십 분 걸려서 얘기가 달랐겠지만, 우리는 12MB였다. 그래서 그냥 멈추고 복사하기로 했다.

메모리 예산

RAM 1.9GB 거지 서버에 JVM 앱이 이미 도는데 DB까지 올리면 안 터지나? 리허설을 통해 실험해봤다.
앱 490MB, PostgreSQL 컨테이너 33MB, 남는 메모리 836MB. 둘 다 올려도 836MB가 비니까 인스턴스는 안 건드렸다.

PG가 33MB밖에 안 쓴 건 데이터가 12MB라 작업셋이 작아서다. shared_buffers를 128MB 잡아둬도 실제로 건드리는 페이지가 그뿐이다. 데이터가 작으니 통째로 메모리에 올라가서, 디스크도 거의 안 읽는다(옮긴 뒤 캐시 히트율 99.5%). 사실 이 서버에서 메모리 걱정할 놈은 PG가 아니라 JVM이었다.

스왑과 디스크

서버에 스왑이 아예 없었다. 스왑이 없으면 메모리가 잠깐 튀어도 바로 OOM이라, 안전망으로 깔기로 했다. 크기는 디스크가 정해줬다. 그때 디스크 여유가 1.7GB라 2GB 스왑파일은 애초에 안 들어간다. 1GB로 만들었고, 그 1GB가 또 디스크를 먹어서 여유는 1.4GB(80%)가 됐다. 작은 서버에선 안전망 하나 거는 것도 디스크를 갉아먹는다.


전환

새벽이 되었다! 여기서부터 못 되돌리는 게 하나씩 생기니까, 단계마다 통과 조건을 걸었다.

앱이 이 DB에 쓰는 유일한 놈이라고 보고, 앱을 멈춰서 RDS를 얼렸다. 양쪽이 둘 다 움직이면 비교가 안 되니 일단 멈춰야 한다. (사실 엄밀히는 pg_stat_activity로 active 연결이 0인지 봤어야 했다. 앱만 끄고 "딴 데서 쓰는 건 없겠지" 하고 넘어간 게 이번 작업에서 제일 찜찜한 부분이다.) 멈추고 덤프 26초, 복원 0.75초, 경고 0줄.

복원이 제대로 됐는지는 행 수로 안 봤다. 35개가 35개여도 값이 틀어질 수 있으니까. 그냥 전체 행을 이어붙여 해시를 떠서 양쪽을 맞췄다.

            신 컨테이너        RDS
users       8479836e179a   =   8479836e179a
blog_posts  5b2b243ef8ac   =   5b2b243ef8ac

확장도 pg_trgm 1.6으로 양쪽 같은지 봤고, 빅뱅이라 시퀀스는 덤프에 같이 딸려와서 users_id_seq=1649 그대로 들어왔다(논리 복제였으면 이걸 손으로 맞춰야 한다). 해시가 맞고 나서 DB_HOST를 컨테이너로 바꾸고 앱을 재시작했다. DB_HOST는 코드가 아니라 설정이라 재시작만 하면 되는데, 한 가지 — Secrets Manager 값도 같이 안 바꾸면 다음 배포 때 .env가 옛날 값으로 다시 덮여서 지운 DB를 보러 간다. 복원 직후라 통계가 비어 있어서 ANALYZE 한 번 돌리고, 헬스 UP에 연결 11개, 조회 정상. 앱 멈춘 시점부터 1분 정도 만에 끝났다.

이때부터 신 DB가 진짜다. 여기가 돌아올 수 없는 지점이었다.


삭제, 그리고 망설임

다시 그 삭제 명령 앞. 누를 수 있었던 건 앞 단계가 다 통과했기 때문이다. 해시 맞고, 앱 헬스 UP, 조회 정상. 하나라도 빨간불이었으면 안 누르고 DB_HOST를 RDS로 되돌렸을 거다. 셋 다 초록이라, 스냅샷 하나 떠서 돌아갈 길 남겨두고(그래서 --skip-final-snapshot) 눌렀다.

status: deleting

게이트도 통과했고 스냅샷도 있었다. 잃을 것도 딱히 없었다. 근데도 손가락이 한 번 멈췄다. 데이터값 때문이 아니라 delete-db-instance라는 명령이 한 방향이라서다. 누르면 그 인스턴스는 안 돌아온다(스냅샷 복원은 새 인스턴스를 또 세우는 별개 일이다). 솔직히 잃을 게 없는 삭제라 이번 멈칫함은 거의 반사에 가까웠다. 근데 작은 비가역에도 손 한 번 멈추고 게이트를 보는 게 버릇이 되면, 진짜 잃을 게 큰 삭제 앞에서 그 버릇이 남는다. 멈춘 손을 다시 움직인 건 그 게이트들이었다.


옮기고 나서

RDS 요금이 뭐였는지는 떠나고 나서 알았다. 자동 백업, 모니터링, 장애 복구를 사던 돈이었다. 떠둔 스냅샷은 옛날 RDS를 찍은 사진 한 장이지 새 컨테이너 DB의 백업이 아니다. 이제 그걸 다 직접 해야 한다. 메모리가 100MB 밑으로 가면 디스코드로 알림 오게는 해뒀는데, pg_dump를 S3로 넘기는 백업은 아직 안 했다. 그것도 복원까지 한 번 해봐야 백업이라고 할 수 있다. managed를 떠난 게 돈을 아낀 거였냐면, 아니다. 돈을 노동이랑 리스크로 바꾼 거다.

남은 부채를 정리하면 이렇다. 백업 직접, 모니터링 직접, 서버 한 대뿐이라 이게 죽으면 앱이랑 DB가 같이 죽고, 8GB 디스크는 벌써 90%다.


데이터 옮긴 건 27초였고, 나머지는 전부 못 되돌리는 순간을 넘기는 일이었다. 옮길 이유는 139ms가, 빅뱅은 1분이, 인스턴스 유지는 836MB가, 스왑 크기는 디스크 1.7GB가, 엔터는 해시 일치가 정했다. 이 숫자들을 빼면 결정이 그냥 느낌으로 돌아간다. 못 되돌리는 작업일수록 더 그렇다. 어차피 되돌릴 수가 없으니, 누르기 전에 숫자로 직접 확인하는 것 말고 기댈데가 없다.

git revert가 없는 작업은 생각보다 많다. 운영 중인 테이블 삭제, 데이터 이전, 한번 나가면 끝인 결제 기능. 그 되돌릴 수 없는 버튼 앞에서 손가락 멈추는 건 겁먹은게 아니다. 멈춘 김에, 누를 근거가 다 green인지 보면 된다