2P by GN⁺ | ★ favorite | 댓글 1개
  • Knock은 알림 워크플로 엔진의 핵심 저장소인 Postgres를 AWS RDS Aurora 11.9에서 15.3으로 올리며 고객 영향 없이 전환하는 절차를 마련함
  • Amazon RDS의 Postgres 11.9 퇴역일인 2024년 2월 29일 전에 움직이지 않으면 강제 업그레이드와 다운타임을 감수해야 하는 상황이었음
  • 인플레이스 업그레이드와 pg_dump/pg_restore는 긴 중단 시간이 필요해 제외하고, 새 DB에 PUBLICATION/SUBSCRIPTION 기반 논리 복제를 구성하는 방식을 택함
  • 테이블 크기와 쓰기 패턴에 따라 복제 전략을 나눴으며, 작은 테이블은 직접 복제하고 큰 append-only 테이블은 copy_data = false와 스냅샷 백필을 조합함
  • 최종 전환은 두 DB 연결을 유지한 채 플래그를 바꾸고, 실행 중 쿼리에 500ms를 준 뒤 1초간 새 DB 요청을 멈춰 stale read 위험을 줄이는 방식으로 몇 초 만에 끝남

업그레이드 목표와 제약

  • Knock은 알림 워크플로 엔진을 Postgres에 의존하며, 워크플로 설정, 메시지 템플릿, 수백만 건의 로그 수집, 백그라운드 작업 큐잉에 Postgres를 사용함
  • Postgres는 관계형 데이터베이스 특성상 업그레이드 때 최소 재부팅이 필요하고, 메이저 버전 업그레이드는 디스크의 데이터·인덱스 저장 방식 변경 때문에 몇 분 이상 완전 종료가 필요할 수 있음
  • 회사 시작 이후 사용해 온 Postgres 11.9는 Amazon RDS에서 퇴역 예정이었고, 별도 조치가 없으면 강제 업그레이드와 강제 다운타임 가능성이 있었음
  • 업그레이드 조건은 운영 리스크를 줄이는 데 맞춰짐
    • 가능한 최신 버전인 Aurora용 Postgres 15.3까지 건너뛰기
    • 60초를 넘는 다운타임은 허용하지 않고, 이상적으로는 시스템 다운타임 0
    • Amazon의 2024년 2월 마감 전에 완료
    • 고객 영향 최소화, 예를 들어 API 오류 응답 0
    • 다음 업그레이드에 재사용할 수 있도록 절차를 런북화
  • 11.9에서 15.3까지는 4개 메이저 버전 업그레이드에 해당해, 인플레이스 업그레이드를 4번 반복하는 방식은 선택지에서 제외됨

사전 준비: 위험 줄이기와 관측성

  • Postgres 업그레이드는 먼저 위험 목록을 만들고, 영향이 크면서도 미리 제거하기 쉬운 위험부터 줄이는 방식으로 접근함
    • 긴 다운타임
    • 데이터 손실
    • 애플리케이션 워크로드의 DB 성능 변화
    • VACUUM 빈도나 동작 변화
    • 복제 슬롯 이전 필요 여부
  • Postgres 릴리스 노트로 버전 간 변경 사항을 확인하며, VACUUM 동작 변화나 특정 업그레이드에서의 재인덱싱 필요 같은 위험을 식별함
  • 업그레이드 중에는 시스템과 데이터베이스 지표를 계속 확인해야 함
    • 트랜잭션 wraparound 방지를 위한 Max TXN ID
    • DB CPU 사용률
    • writer 인스턴스의 대기 세션
    • 쿼리 지연 시간
    • 애플리케이션 API 응답 지연 시간
  • Knock은 API 요청이 알림으로 바뀌는 데 걸리는 시간처럼 애플리케이션 고유 지표도 함께 모니터링함
  • 제때 확인할 수 있는 지표가 없으면 업그레이드 과정에서 눈을 가린 상태가 됨

제외한 방식: 인플레이스 업그레이드와 dump/restore

  • AWS RDS의 인플레이스 업그레이드는 AWS 콘솔에서 실행되며, AWS가 DB를 중단하고 업그레이드 스크립트를 실행한 뒤 다시 온라인으로 올림
  • 이 과정은 데이터 양과 버전 간 변경 폭에 따라 몇 분에서 몇 시간 이상 걸릴 수 있음
  • DB가 다시 온라인이 된 뒤에도 VACUUM이나 REINDEX 같은 유지보수 작업이 필요해 곧바로 완전히 사용 가능한 상태가 아닐 수 있음
  • pg_dumppg_restore 방식은 신뢰할 수 있는 백업을 얻기 위해 모든 애플리케이션을 기존 DB에서 분리해야 하고, 큰 DB에서는 dump와 restore 자체가 오래 걸림
  • Knock의 다운타임 한도를 크게 넘길 가능성이 커 두 방식 모두 제외됨

선택한 방식: 논리 복제 기반 업그레이드

  • 최종 선택은 Postgres의 PUBLICATIONSUBSCRIPTION을 사용하는 논리 복제 방식이었음
  • 기본 흐름은 다음과 같음
    • 목표 Postgres 버전의 새 DB를 띄움
    • 설정, 확장, 테이블 구성, 사용자 등을 옮김
    • 기존 DB에 publication을 만들고 새 DB에 subscription을 구성함
    • 테이블을 publication에 추가함
    • 복제가 완료되면 남은 위험을 확인하는 테스트를 수행함
    • 새 DB 구성이 충분히 확인되면 애플리케이션을 새 DB로 전환함
    • 기존 DB를 제거함
  • 한 번에 큰 업그레이드를 실행하지 않고 점진적 단계로 진행할 수 있었고, 실제 데이터와 실제 워크로드로 새 DB를 테스트할 수 있었음
  • 새 DB가 준비된 뒤에는 전환 자체가 몇 초 만에 끝나, 전환 시점과 방식을 더 잘 통제할 수 있었음

복제 구성의 핵심

  • Postgres 논리 복제는 복제 슬롯 설정에 필요한 파라미터를 사용하며, 단순한 애플리케이션에서는 wal_levellogical로 설정하는 것이 주요 변경일 수 있음
  • 이미 읽기 복제본, DB 장애 조치, 데이터 웨어하우스 동기화 등에 복제 슬롯을 쓰고 있다면 max_replication_slots 등 관련 파라미터를 문서에 맞게 조정해야 함
  • 새 DB의 테이블 구조는 기존 DB와 동일해야 하지만 비어 있어야
  • 스키마 스냅샷은 pg_dumpall--schema-only, --no-role-passwords 옵션을 주어 생성하고, 새 DB용 SQL과 비교해 차이를 수정할 수 있음
  • 기존 DB에서 publication을 만들고 새 DB에서 subscription을 만들 때 주요 옵션을 설정함
    • enabled = false: 처음부터 동기화를 시작하지 않도록 함
    • create_slot = true: Postgres가 복제 슬롯을 관리하도록 함
    • copy_data = true: 기본적으로 테이블 내용을 복사함
    • disable_on_error = true: 예기치 않은 오류에서 subscription을 중단해 문제를 고친 뒤 재개할 수 있게 함
  • FOR ALL TABLES로 모든 테이블을 한 번에 publication에 넣으면 큰 DB에서 성능 문제가 생길 수 있어, Knock은 ALTER PUBLICATION ... ADD TABLE로 테이블을 하나씩 추가

테이블 분류와 복제 전략

  • Knock은 테이블을 디스크 크기와 튜플 수 기준으로 나눔
    • 몇 분 안에 동기화할 수 있는 작은 테이블
    • 크지만 append-only에 가까운 테이블
    • 크고 대부분의 row가 자주 업데이트되는 테이블
  • Knock 기준의 “작은” 테이블은 50GB 미만이고 1천만 튜플 미만인 테이블이었음
  • Postgres에서 튜플은 insert나 update가 저장되는 단위이며, row 수가 적어도 정리되지 않은 튜플이 많으면 복제 시간이 길어질 수 있음
  • 복제 전 VACUUM을 수행하면 소스 DB가 대상 DB로 복사해야 하는 튜플 수를 줄이는 데 도움이 될 수 있음
  • 테이블 동기화 시간은 디스크 크기와 튜플 수에 직접적으로 연관되며, 오래 걸리는 동기화는 primary DB의 VACUUM을 방해해 성능 저하와 트랜잭션 wraparound 위험으로 이어질 수 있음

작은 테이블 복제

  • 작은 테이블은 기존 DB에서 publication에 테이블을 추가하고, 새 DB에서 subscription을 refresh하는 방식으로 처리함
  • 테이블 복사, 동기화, 이후 변경 적용은 Postgres가 맡음
  • 아주 작은 테이블은 1초 미만에 동기화될 수 있음

큰 append-only 테이블 복제

  • 업데이트가 없거나 최근 row에만 업데이트가 발생하는 큰 테이블은 copy_data = false로 별도 publication/subscription을 만들 수 있음
  • Knock은 이름에 _nocopy 접미사를 붙여 일반 복제와 구분함
  • 새 변경분만 먼저 복제하고, 과거 데이터는 백업이나 스냅샷에서 별도로 백필
  • AWS RDS Aurora에서 사용한 절차는 다음과 같음
    • 프로덕션 DB 스냅샷 생성
    • 스냅샷을 새 DB 인스턴스로 복원
    • 복제할 스냅샷 DB 테이블 이름에 _snapshot 같은 접미사 추가
    • 대상 DB에도 같은 스키마의 스냅샷용 테이블 생성
    • 스냅샷 DB에서 대상 DB로 publication/subscription 구성
    • 복제 진행 상황 모니터링
    • 복제가 따라잡으면 INSERT ... ON CONFLICT DO NOTHING으로 실제 대상 테이블에 병합
  • 매우 큰 테이블은 이 과정이 며칠 걸릴 수 있지만, 백그라운드에서 진행되므로 프로덕션 환경에 영향을 주지 않아야 함
  • 병합 후 row 수를 비교해 일관성을 확인하고, 대상 DB의 스냅샷 테이블과 스냅샷 subscription, 스냅샷 DB 인스턴스를 제거함

크고 자주 업데이트되는 테이블

  • 크고 row 대부분이 자주 업데이트되는 테이블은 가장 어렵고, 오래 걸리는 복제가 AUTOVACUUM 실행을 방해할 수 있음
  • 고려할 수 있는 조치는 다음과 같음
    • housekeeping으로 테이블 크기를 줄일 수 있는지 확인
    • 최근 VACUUM 수행 여부 확인
    • 테이블을 더 작은 조각으로 파티셔닝할 수 있는지 검토
    • 일정 시간이 지나면 row 업데이트가 멈추는지 확인해 append-only처럼 다룰 수 있는지 판단
  • 소스 DB가 PG 15 미만이면 선택지가 제한되며, 작은 테이블 방식대로 복제하고 모니터링으로 서비스 저하 여부를 확인해야 함
  • 필요하면 publication에서 테이블을 제거하고 subscription을 refresh해 롤백할 수 있음
  • 너무 큰 테이블은 트래픽이 낮은 시간대에 복제를 시작해 부하와 쓰기 활동 영향을 줄일 수 있음

PG 15 이상에서 가능한 큰 테이블 분할 복제

  • 소스 DB가 PG 15 이상이면 여러 publication으로 복제를 나눠 큰 테이블을 작은 조각으로 옮길 수 있음
  • 이 방식은 파티셔닝이나 샤딩과 비슷하게 동작하며, 더 많은 복제 슬롯을 사용하는 대가가 있음
  • Knock은 11.9에서 15.3으로 옮겼기 때문에 이 방식을 사용할 수 없었고, 직접 테스트하지 않았음
  • 예시는 primary key 해시와 WHERE 절을 사용해 row를 여러 publication에 나누는 방식임
  • Knock이 생각한 관리 가능한 조각 크기는 인덱스를 제외한 데이터 기준 약 100GB였음

복제 상태 확인과 중단

  • subscription에 테이블이 추가되면 대상 DB의 pg_subscription_rel.srsubstate에서 상태를 확인할 수 있음
    • i: 초기화
    • d: 테이블 내용 복사
    • f: 복사 완료, 최종 동기화 대기
    • s: 초기 동기화 마무리
    • r: 정상 복제 실행
  • d 단계는 오래된 Postgres 트랜잭션 ID를 유지해야 하므로 VACUUM을 효과적으로 막을 수 있고, 성능 문제나 transaction ID wraparound로 이어질 수 있음
  • wraparound에 가까워지면 마이그레이션을 중단하고 더 작은 조각으로 나누는 편이 나음
  • 특정 테이블 복제를 중단하려면 기존 DB publication에서 테이블을 제거하고, 새 DB subscription을 refresh함
  • 단순히 subscription만 disable하면 소스 DB가 오래된 transaction ID를 계속 잡고 있어 성능 문제가 해결되지 않을 수 있음
  • 긴급 상황에서는 publication과 subscription 전체를 삭제하고 처음부터 다시 시작할 수 있으며, Postgres가 관련 복제 슬롯을 정리함

복제 슬롯 이전의 제약

  • Postgres 복제 슬롯은 다른 DB나 애플리케이션이 소비할 수 있는 DB 활동 로그를 저장함
  • 슬롯 진행 상황은 Log Sequence Number, 즉 LSN으로 추적되며, LSN은 primary Postgres DB에 고유함
  • 기존 DB의 복제 슬롯 LSN을 새 DB로 그대로 복사할 수 없음
  • 데이터 웨어하우스 도구처럼 복제 슬롯을 소비하는 애플리케이션은 각 도구 문서에 따라 이전 전략을 정해야 함
  • 자체 애플리케이션에서 복제 슬롯을 사용한다면 기존 DB와 새 DB의 중복 트랜잭션을 제거할 수 있는 멱등성 메커니즘이 도움이 됨

최종 검증

  • 모든 테이블을 publication에 추가하고 subscription이 따라잡으면, 테이블이 서로 맞는지 검증해야 함
  • 논리 복제의 지연 때문에 기존 DB와 새 DB가 같은 순간에 완벽히 일치하기는 어렵지만, row 수 비교로 충분히 가까운지 확인할 수 있음
  • Knock은 각 테이블에 대해 기존 DB와 새 DB의 row 수를 세는 스크립트를 작성함
  • inserted_at 컬럼이 있는 테이블은 10초보다 오래된 row만 비교해, 최근 10초분은 곧 복제될 것이라는 가정으로 검증함
  • 일부 테이블은 무작위 row 샘플을 비교해 테이블 내용이 일치하는지 추가로 확인함

애플리케이션 전환 방식

  • 최종 cutover를 위해 애플리케이션이 두 DB에 연결되도록 변경할 수 있음
  • 트래픽이 낮은 DB는 설정을 새 DB로 바꾸고 애플리케이션을 재시작하는 단순한 방식으로 이전함
  • 동시 활동이 많은 애플리케이션에서는 기존 DB와 새 DB 사이의 충돌 쓰기를 피해야 했음
  • Knock의 cutover 스크립트는 다음 순서로 동작함
    • 모든 애플리케이션 인스턴스에 새 쿼리를 새 DB로 보내도록 지시
    • 실행 중 DB 쿼리에 500ms 완료 시간을 주고 이후 강제 취소
    • 플래그 전환 후 첫 1초 동안 새 DB 요청을 인위적으로 일시 정지해 pending transaction이 새 DB로 복제될 시간을 확보
    • 이후 DB 활동을 정상화하되 새 DB를 바라보게 함
    • 일부 특수 DB 워크로드는 중단 후 새 DB에 다시 연결되도록 재시작
  • Knock은 500ms가 대부분의 DB 쿼리보다 훨씬 길었고, 강제 연결 해제로 인한 오류는 없었다고 확인함

시퀀스 처리

  • Postgres 논리 복제는 sequence를 동기화하지 않음
  • 기존 DB에서 sequence 값이 사용돼도 새 DB의 sequence 값은 증가하지 않음
  • Knock은 feature flag 전환 직전에 두 DB에 연결하는 스크립트를 실행함
    • 기존 DB의 모든 sequence에 대해 SELECT nextval('sequence_name')로 다음 값을 가져옴
    • 새 DB에서 SELECT setval('sequence_name', value::int4 + 100000)로 sequence를 앞당김
  • 이 방식은 sequence에 gap을 만들지만, Knock의 sequence는 bigint라 10만 개 값 건너뛰기는 사용 가능한 sequence 공간에서 사실상 0%에 가까웠음
  • 실제 cutover 중 사용할 sequence 값 규모에 맞춰 gap 크기를 조정해야 함

cutover 전 확인할 것들

  • 최종 전환 전 확인 항목은 운영 준비 상태를 폭넓게 다룸
    • 모든 테이블 row 수가 기대대로 맞는지
    • 모든 subscription이 enable 상태이고 오류 없이 실행 중인지
    • 스키마가 일치하는지, 마이그레이션 릴리스를 동결할 수 있는지
    • 새 DB가 워크로드에 맞게 sizing됐는지
    • 기존 DB와 새 DB의 클러스터 토폴로지를 맞추기 위해 read replica가 필요한지
    • 새 DB에서 REINDEX와 기본 VACUUM 유지보수를 수행했는지
    • Postgres 릴리스 노트에서 애플리케이션 회귀 가능성을 다시 확인했는지
    • 새 버전의 staging DB에서 자동·수동 테스트를 수행했는지
    • 가장 부담이 큰 쿼리를 pg_bench로 부하 테스트했는지
    • 아직 줄일 수 있는 위험이 남아 있는지
    • staging 또는 test 환경에서 cutover 절차를 여러 번 연습했는지
    • cutover 직전 DB 백업을 생성했는지

실제 전환 결과

  • Knock은 몇 주에 걸쳐 테이블을 하나씩 복제했고, 주로 업무 시간 이후와 트래픽이 가장 낮은 시간대에 진행함
  • staging 환경에서 cutover를 여러 번 연습해 운영자 개입이 많지 않아도 동작하도록 절차를 다듬음
  • PG 15 replica와 애플리케이션 전환 코드가 준비된 뒤 최종 점검을 수행하고 플래그를 전환함
  • 실제 cutover는 몇 초 안에 끝났고, 복제를 기다리기 위한 의도적인 짧은 latency blip 외에 애플리케이션은 계속 동작함
  • 이후 임시 애플리케이션 변경을 되돌리고, 모든 연결을 새 DB로 영구 전환하고, 새 DB의 subscription과 기존 DB를 제거함
  • Knock은 Postgres 11.9에서 15.3으로 무중단 이전을 완료함

결론

  • Postgres 메이저 버전 4개를 한 번에 건너뛰는 작업은 고되지만 가능함
  • 논리 복제 방식은 실제 cutover 전에 여러 번 연습, 테스트, 재작업할 수 있어 예정된 다운타임보다 더 안전할 수 있음
  • 진행 중 문제가 생기면 기존 DB의 publication을 삭제하고 다시 시작할 수 있어 서비스 저하 없이 절차를 되돌릴 수 있었음
  • 완벽한 100% 가용성은 기술적으로 가능하지 않지만, 무중단 마이그레이션은 큰 서비스 중단 없이 시스템을 계속 운영하는 데 도움이 됨

댓글과 토론

Hacker News 의견들
  • 테이블 내용을 하나씩 전부 복사하는 방식은 입출력 부하가 너무 크고, 아주 큰 테이블에서는 통하지 않음
    더 나은 방법은 복제 슬롯을 만들고, 스냅샷을 뜬 뒤 새 인스턴스에 복원하고, LSN을 전진시킨 다음 거기서부터 복제하는 것임. 그러면 모든 데이터가 있는 논리 복제본이 생기고, 그 복제본을 업그레이드하면 됨
    Instacart 글에 방법이 나와 있음: https://archive.ph/K5ZuJ
    기억이 맞다면 글에 작은 오류가 몇 개 있었지만, 전반적인 절차는 동작했고 TB급 인스턴스를 여러 번 이렇게 업그레이드해 봤음

    • 이 방법은 좋은 레시피지만, pg_upgrade를 끼워 넣는 순서에 작지만 중요한 수정이 필요함
      먼저 논리 복제를 시작한 뒤 pg_upgrade를 실행하면 손상 위험이 있음. 관련 논의는 pgsql-hackers에 있음: https://www.postgresql.org/message-id/flat/20230217075433.u5...
      해결하려면 먼저 논리 슬롯을 만들고, 새 클러스터를 슬롯의 LSN 위치까지 전진시키되 논리 복제는 아직 시작하지 않은 다음, pg_upgrade를 실행하고, 새 PostgreSQL 버전에서 클러스터가 올라온 뒤에 논리 복제를 시작해야 함
      Postgres.ai가 최근 GitLab의 여러 multi-TiB 클러스터를 높은 부하 상태에서 무중단 업그레이드할 때 정확히 이 방식을 썼고, PgBouncer의 PAUSE/RESUME도 함께 사용했음. 이번 주 후반 Alexander Sosna 발표가 예정되어 있음: https://www.postgresql.eu/events/pgconfeu2023/schedule/sessi...
    • OP로서 이 방법도 검토했지만, 제안된 방식처럼 LSN을 수동으로 전진시키는 데 확신이 없었고 복제를 놓쳤을 때 불일치를 감지할 자신도 없었음
      테이블별 진행은 훨씬 번거로웠지만 더 신뢰할 만해 보였음
    • 글이 업데이트됨: https://tech.instacart.com/zero-downtime-postgresql-cutovers...
    • 그 글은 Instacart의 업그레이드 방식의 기초를 다루지만 꽤 오래됐고, 아래 글이 현재 절차를 더 잘 보여줌
      이 방식으로 매우 크고 활발한 데이터베이스를 많이 성공적으로 업그레이드했음
      https://www.instacart.com/company/how-its-made/zero-downtime...
  • 접근 방식이 흥미롭고 문서화도 잘 되어 있지만, “현대 고객은 100% 가용성을 기대한다”는 문장은 걸림
    고객으로서의 선호도 아니고, 공급자로서의 경험도 아님. 많은 워크로드에서는 가용성보다 일관성이 훨씬 중요함
    공급사가 다운타임 창을 공지하면 오히려 내 데이터를 신중하게 다루고 있다는 신호처럼 보여 안심될 때가 많음

    • OP로서 좋은 피드백임
      제품의 신뢰성과 워크로드의 일관성 모두에 대한 신뢰를 만들고 싶었음. 물론 일관성이 있는 척하면서 불안정한 것보다는, 고객 기대치를 관리하고 장기적으로 더 나은 가동 시간을 위해 의도적으로 다운타임을 갖는 편이 훨씬 나음
      주기적 유지보수 창을 미리 예상하게 하는 것이 전체적으로 더 견고한 아키텍처로 이어질 수도 있음. 고객이 다운타임을 견딜 안전장치를 만들면 복원력이 높아지고, 팀도 고객을 그렇게 신뢰할 수 있을 때 더 나은 제품을 위한 투자를 할 시간을 얻음
      다음 메이저 버전 업그레이드 뒤에는 “다운타임에 대한 기대치 설정이 매우 높은 가동 시간으로 가는 길”이라는 글을 쓸지도 모르겠음
    • 고객이 누구냐에 따라 다름
      AWS의 고객으로서는 100% 가용성을 기대함. 내 고객들이 전 세계에 있고 다운타임을 둘 수 있는 시간이 없기 때문임
  • AWS가 이제 블루/그린 배포를 지원함: https://aws.amazon.com/about-aws/whats-new/2023/10/amazon-rd...

    • 몇 주 전에 직접 해봤는데, 아직 PostgreSQL에서는 믿지 않는 편이 좋음
      AWS와 몇 차례 주고받은 뒤 실험이 몇 시간 동안 멈췄고, 나중에야 AWS UI가 전환이 적용되지 않았다고 인정했음. 다행히 안전하게 실패했지만, GB 이상 데이터셋에서 실제 전환 시점을 맞출 수 있다는 믿음은 없음
    • 맞는 말임. OP로서 당시 우리는 Aurora 11.9였고 블루/그린 배포 지원 대상이 아니었음
      다음번에는 가능할지도 모름
  • 이건 훌륭함
    겪은 일 대부분을 자동화하는 도구를 만들었고, 유용하거나 피드백/아이디어로 확장하고 싶다면 언제든 환영함: https://github.com/shayonj/pg_easy_replicate

    • 멋진 도구임
      큰 테이블에서 얻은 발견들이 이런 도구에 흥미로울 수 있음. 테이블별로 맞는 전략을 더 쉽게 적용하게 해주면, 앞으로 이런 마이그레이션을 하는 팀에게 필수 도구가 될 수 있음
  • “Knock 같은 서비스에는 예정 여부와 상관없이 어떤 다운타임도 허용되지 않는다”는 건 의심스러움
    복잡한 시스템이면 장애도 있고 다운타임도 있음. 사전에 공지된 15분 다운타임은 거의 모든 SaaS 비즈니스에서 괜찮음. 병원도 아니고 발전소도 아님
    서비스가 실제보다 더 중요하다고 생각해서 가짜 일이 많이 생김. 여기에 들인 엔지니어링 시간을 제품이나 개발팀 생산성 개선에 썼다면 사용자가 더 행복했을 가능성이 큼. 특히 알림을 큐에 넣고 다운타임 뒤 따라잡을 수 있다면 더 그렇음
    15분 다운타임에 대한 배상 조건이 있는 엔터프라이즈 SLA가 있다면 정당화할 수 있겠지만, 대부분은 그렇지 않음. 실제로는 이미 비슷하거나 더 긴 장애가 몇 번 있었을 가능성도 큼
    데이터베이스 마이그레이션에서는 “짧은 다운타임”과 “무중단” 사이의 작업량 차이가 보통 상당해서 더 중요함. 이번처럼 일회성이고, RDS의 최신 PostgreSQL 버전은 기본으로 지원하는 경우라면 특히 정당화하기 어렵다고 봄

    • OP로서 모든 서비스에 어떤 이유로든 다운타임이 있다는 건 맞음
      장애 창을 잡는 것도 논의했지만, 계속 고민했던 건 운영 데이터로 업그레이드를 어떻게 예행연습할 수 있느냐였음. 운영 데이터와 동기화된 PG 15 복제본은 워크로드가 예상대로 동작하는지 검증하는 데 매우 중요했음
      실시간 복제본을 쓰면 운영 환경에서 영향을 최소화하며 예행연습이 가능함
      이번 마이그레이션에서 크게 배운 점은 이런 프로젝트에서 생각할 수 있는 모든 위험을 추적하고 완화하는 일이 얼마나 유용한가였음. 결국 제자리 업그레이드의 위험이 선택한 경로의 위험보다 더 커 보였고, 장애 창 여부와는 별개의 판단이었음
      덤으로 앞으로 이 접근이 필요하면 이 블로그 글이 출발점이 되어 몇 주를 절약해 줄 것임. 비슷한 상황의 다른 팀에도 도움이 되길 바람
    • 의사 입장에서 “병원도 아니잖아”가 다운타임을 견딜 수 없는 시스템의 예로 나오는 게 재미있음
      미국 최대급 전자의무기록 제공자인 Epic도 업그레이드를 위해 최소 월 1회, 매번 30~60분 정도 예정 다운타임이 있음
    • RDS에서 PostgreSQL 인스턴스를 예정된 15분 다운타임으로 업그레이드할 방법이 없다는 점이 문제임
      재부팅 시점을 제어할 수 없음. 프로세스를 시작하면 전환이 한 시간 뒤, 두 시간 뒤, 세 시간 뒤에 시작될 수도 있고 언제 재부팅되는지 알 수도 제어할 수도 없음
      복제본이 있으면 병렬로 업그레이드되며 임의의 시점에 재부팅되어 더 골치 아픔
      따라서 데이터베이스 크기에 따라 몇 시간까지 이어질 수 있는 시간대에 임의의 비가용성을 감당할 수 없다면, RDS 업그레이드에는 논리 복제 방식이 사실상 유일함
      인스턴스가 클수록 문제가 더 어려워짐
    • 다운타임의 진짜 문제는 모든 시스템이 동시에 내려갈 때임
      Jira가 하루 15분 내려가면 보통은 큰 영향이 없음. 작업 큐에 다른 일이 있고, 최악의 경우 여러 장애가 겹쳐도 누군가에게 약속한 문서 작업이 있음
      하지만 Atlassian 제품군 전체가 동시에 죽으면 일을 이어갈 완충 작업을 유지하기가 훨씬 어려워짐. 기업의 모든 앱이 같은 스토리지 배열을 쓰게 만들면 생산성 손실이 5%에서 95%로 튈 수 있음
    • “사전에 공지된 15분 다운타임은 거의 모든 SaaS 비즈니스에서 괜찮다”는 말과 달리, 매달 다운타임이 없는 경쟁자가 있을 수 있음
      그런 경쟁자는 내 요구를 자기 편의보다 앞에 두는 셈임
      당신의 장애는 곧 내 장애이기도 함
  • hava.io에서 지금 이 과정을 겪는 중임
    AWS RDS PostgreSQL 11.13에서 15.5로 올리고 있음
    최종적으로 pglogical을 사용한 단방향 복제라는 비교적 단순한 접근을 택했음. Google Cloud SQL에서 AWS RDS로 무중단 이전을 같은 방식으로 해본 경험이 있어서, 고객에게 보이는 영향 없이 동작할 것이라는 확신이 있었음
    pglogical은 이런 마이그레이션을 꽤 단순하게 해줌. 항상 빠르지는 않지만, 전체 데이터베이스가 새 인스턴스로 점진 복제되는 며칠을 기다릴 수 있다면 괜찮음
    이 방식은 스토리지 유형과 크기를 바꾸는 자유도도 더 줬음. IOPS를 얻기 위해 스토리지를 과하게 잡아둔 상태라, 스토리지 유형을 바꾸고 크기도 줄이고 싶었음. 그래서 단순 스냅샷 복원으로는 안 됐음

  • “세일즈 엔지니어링” 단계에서 AWS가 약속했던 그 기능을 말하는 건가 싶음
    실제로는 메이저 버전 업그레이드를 강제로 해야 했을 때 제공하지 못했음

  • 백업에서 복제본을 초기화할 수 없다는 게 놀라움
    가능했다면 안정적인 기존 데이터베이스 내용을 새 서버로 스트리밍하느라 고생하는 일을 줄였을 것임
    그리고 이건 “무중단”이 아니라 새 서버로 서비스 전환하는 몇 초의 다운타임이 있음
    글은 일관성을 어떻게 보존했는지 빠뜨렸음. 예를 들어 애플리케이션을 일정 기간 양쪽 서버에 그냥 붙일 수는 없음. 읽기는 둘 다에서 제공할 수도 있겠지만 그것도 완전하지 않고, 쓰기는 반드시 한 서버로만 가야 함
    마지막으로 롤백 옵션도 없음. 이런 대규모 데이터를 한 번에 들어 옮기는 작업은 늦은 밤에 일이 틀어질 때가 있음. 그래서 이전 단계로 되돌아가고 아침에도 서비스가 살아 있을 것이라는 확신을 갖고 잘 수 있는 계획이 항상 필요함
    특히 이미 새 서버에 쓰기 트랜잭션을 보낸 뒤 어떤 이유로든 예전 서버로 돌아가야 하면 어렵고, 데이터는 이미 불일치해져 있음

    • OP로서 백업에서 복제본을 초기화할 수는 있지만, 백업 중 계속 발생하는 쓰기는 얻지 못함
      어떤 복제 수단이 없거나 애플리케이션 계층으로 올리지 않는 한, 복원된 시스템에는 누락된 쓰기가 생김
      예를 들어 앱을 수정해서 이중 쓰기를 적용할 수 있음. RDBMS에서 Apache Cassandra처럼 완전히 다른 데이터베이스로 전체 애플리케이션을 재플랫폼한 팀들도 그렇게 한 것으로 알고 있음
      우리 상황에서는 이중 쓰기가 PostgreSQL 기본 기능으로 스트리밍 복제를 설정하는 것보다 더 위험해 보였음. 하지만 어떤 팀에는 더 나은 선택일 수 있음
      “무중단이 아니다”와 “일관성 보존 세부가 빠졌다”는 부분에 대해, 글에서는 일관성을 지키고 API 다운타임을 피한 방법을 자세히 다뤘음. 요지는 앱이 두 데이터베이스에 모두 연결되어 있었지만 새 데이터베이스를 기본으로 쓰지는 않았다는 것임
      그런 다음 LaunchDarkly로 모든 앱 인스턴스에 전환 신호를 보냈고, LaunchDarkly는 모든 인스턴스와 저지연 연결을 유지함
      신호 후 첫 1초 동안 서버는 복제가 따라잡을 수 있도록 데이터베이스 요청을 큐에 넣었음. 이 때문에 짧은 지연 시간 스파이크가 있었지만 의도적으로 계산한 허용 범위 안이었음. 그 일시 정지 뒤에는 요청이 평소처럼 흐르되 새 데이터베이스를 대상으로 했고 전환이 완료됨
      기존 데이터베이스로 남아 있는 트래픽에 대해서는 500ms 타임아웃으로 강제 연결 해제도 넣었음. 이 값은 p99 쿼리 시간보다 훨씬 컸기 때문에 실행 중 쿼리가 강제 종료되지는 않았음. 이를 통해 기존 데이터베이스 트래픽이 멈췄고, 복제가 따라잡을 충분한 시간이 생김
      롤백 옵션은 블로그 글에는 빠졌지만, PG 11.9의 대체 데이터베이스를 만들고 15.3 데이터베이스를 그 세 번째 데이터베이스로 복제하는 방안도 검토했음. 중단해야 하면 같은 버전의 이 데이터베이스로 롤포워드할 수 있었음
      스테이징에서 업그레이드 절차를 여러 번 연습해 성공 가능성을 확인한 뒤, 이 방안은 쓰지 않기로 했음. 여러 번 리허설했기 때문에 실제 전환 때 자신감이 있었음. 운영에서도 카나리아 배포로 일부 읽기 전용 워크로드를 15.3 인스턴스에 대해 검증했고, 이를 읽기 복제본처럼 다뤘음
      늦은 밤 문제를 피하려고 일부러 주말 이른 저녁에 진행했음. 전환은 꼼꼼히 스크립트화하고 리허설해 사람 실수 위험을 줄였음
      치명적 실패가 발생하면 시스템은 예전 데이터베이스로 되돌릴 준비도 되어 있었음. 이 경우 새 데이터베이스에 들어간 일부 데이터 손실이 생겼을 것이고, 핵심 부분은 조정할 준비를 해두었음. 데이터 손실 위험을 줄이려고 전환 중 일부 백그라운드 작업을 잠시 멈춰 쓰기 수를 줄였음
      이런 세부사항은 Knock 특화 고려보다 PostgreSQL 관련 세부에 집중하려 했기 때문에 블로그에는 넣지 않았음. 이 플레이북을 적용하려는 팀은 항상 자기 맥락에서 위험 목록을 만들고 완화해야 함
  • 시퀀스 관련 부분이 확실히 흥미로움
    한동안 시퀀스를 거의 쓰지 않고, 주로 순차 UUID나 UUID v7, 또는 HiLo 같은 방식을 사용하고 있음
    https://en.wikipedia.org/wiki/Hi/Lo_algorithm

    • PostgreSQL이 네이티브로 지원하기 전까지 데이터베이스 안에서 UUID v7 생성 책임을 유지하려는 사람에게는 PL/pgSQL 함수가 도움이 될 수 있음
      IETF 초안 규격을 기준으로 12비트 시퀀스를 만들고, 현재 UNIX epoch 밀리초와 난수 62비트를 조합해 UUID를 구성하는 방식임
      핵심은 uuidv7_seq를 두고 generate_uuidv7() 함수에서 clock_timestamp(), NEXTVAL, RANDOM()을 사용해 UUID v7 형태의 값을 반환하게 하는 것임
    • OP로서 의존성 때문에 애플리케이션 한 군데를 제외하고는 시퀀스를 피하고 있음
      여러 곳에서 KSUID와 UUID v4를 사용함. 이 “함정”은 모든 시퀀스에 적용되므로, 이런 마이그레이션을 할 때 일반적인 조언으로 짚을 가치가 있음
      [1]: https://segment.com/blog/a-brief-history-of-the-uuid/
  • 성공적으로 해낸 엄청난 작업을 깎아내리려는 건 아니지만, 새 버전이 나올 때마다 작게 업그레이드하지 않은 이유가 궁금함
    읽을거리로는 훌륭하지만, 큰 폭풍을 돌아가지 않고 비극으로 끝날 수 있음을 알면서도 정면으로 통과하기로 한 선원들 이야기처럼 느껴짐
    이 경우 작은 업그레이드는 선택지 밖이었나? “작은 업그레이드 하나도 큰 업그레이드만큼 다운타임 비용이 들어서 최대한 미뤘다”는 식인지 궁금함. 도입부에서 그런 힌트가 보이긴 하지만 과하게 읽은 것일 수도 있음

    • OP로서 마이너 업그레이드에도 같은 접근을 썼을 것임
      “미루다가 코너에 몰렸다”기보다는, 언젠가 점프해야 한다는 건 알면서도 “고장 나지 않았으면 고치지 않는다”에 가까웠음
    • N개 버전을 올리는 것은 N이 1이든 3이든 가용성 위협 측면에서는 거의 같음
    • 업그레이드마다 다운타임이 듦
      실제 답이 60초 미만이라 해도, 15까지 가는 길에서 그 다운타임을 여러 번 겪었을 것임