엔지니어링 노트설치·운영과 품질

업데이트가 실패하면 스키마 변경에 따라 코드만 되돌리거나 덤프로 복원한다

업데이트가 실패하면 운영자는 먼저 이전 이미지로 다시 배포하려고 합니다. 이미지 재배포는 이미지만 되돌리고 DB 스키마는 그대로 둡니다. 새 판의 마이그레이션이 컬럼 하나를 지웠다면 옛 코드는 기동은 하지만, 그 컬럼을 읽는 화면마다 오류를 냅니다. 그동안 다른 화면으로 들어온 쓰기는 계속 쌓이고, 결국 덤프로 DB를 되돌리는 순간 그 쓰기를 모두 잃습니다. 화면으로는 되돌린 것처럼 보여도 실패 직후보다 수습할 일이 늘어난 상태입니다.

OwlAI 구축형의 업데이트 실행기(updater)는 실패했을 때 두 가지를 보고 끝 상태를 고릅니다. 어느 단계에서 실패했는지, 그리고 그 판의 스키마 변경이 옛 코드와 함께 돌 수 있는 expand인지 그렇지 못한 contract인지입니다. 코드만 되돌려도 되는 경우는 실행기가 끝까지 되돌립니다. 그렇지 않으면 컨테이너를 다시 배포하지 않은 채 멈추고, 사람이 업데이트 직전 덤프로 DB를 복원하게 합니다. 이 글은 그 판정과 복원 절차를 설명합니다. 승인과 적용을 나눈 권한 구조와 expand·contract를 정하는 기준은 「업데이트 버튼은 승인만 하고, docker 소켓은 실행기만 쥔다」에서 다뤘습니다.

이미지 롤백은 expand 변경 뒤에만 성립한다

손 배포 경로에는 이미지 고정 파일에 남은 직전 다이제스트로 한 칸 되돌리는 절차가 있습니다(「배포와 폐쇄망 적재는 태그 대신 다이제스트로 확인한다」). 이 절차는 이미지만 바꾸고 설치 디렉터리의 설정과 DB 스키마는 건드리지 않습니다. 그래서 이것이 롤백이 되는지는 그사이 올라간 마이그레이션 리비전이 무엇이었는지로 정해집니다.

expand 리비전은 옛 코드가 읽고 쓰는 컬럼과 제약을 그대로 둡니다. 새로 생긴 컬럼은 옛 코드가 읽지 않으므로, 옛 코드는 새 스키마 위에서도 그대로 돕니다. 백엔드 기동도 이 경우를 압니다. DB의 Alembic 리비전이 코드가 모르는 값이면 더 새 코드가 올린 스키마로 보고, 마이그레이션을 건너뛴 채 경고 로그 한 줄을 남기고 뜹니다. 이 판정이 없으면 alembic upgrade head가 모르는 리비전에서 오류로 멈추고, 코드만 되돌리는 롤백이 기동 단계에서 막힙니다.

contract 리비전 뒤에는 이 롤백이 성립하지 않습니다. 옛 코드가 읽던 컬럼이 사라졌거나, 새로 더한 NOT NULL·CHECK 제약이 옛 코드의 쓰기를 거절합니다. 기동은 통과해도 그 컬럼을 읽거나 그 테이블에 쓰는 순간 실패합니다. 이때 돌아가는 길은 업데이트 전 덤프로 DB까지 되돌리는 것뿐입니다.

흔한 대안은 리비전마다 스키마를 되돌리는 스크립트(down 마이그레이션)를 함께 쓰는 것입니다. Alembic도 downgrade를 지원하고, 잘 쓰면 덤프 없이 스키마만 되돌릴 수 있습니다. 하지만 컬럼을 지운 리비전의 down 스크립트는 컬럼은 되살려도 지워진 값은 되살리지 못합니다. expand 리비전을 down 스크립트로 되돌리면 그사이 새 컬럼에 쌓인 값을 잃습니다. 이 스크립트가 고객 설치에서 처음 돌다 틀리면 복원 자체가 새 사고가 됩니다.

덤프는 판의 성격과 상관없이 업데이트마다 뜬다

contract 판에서 돌아갈 자리를 남기려면 덤프가 있어야 합니다. 실행기는 이 덤프를 contract 판에서만 뜨지 않고 업데이트마다 뜹니다. 서명된 매니페스트에는 이번 판에 백업이 필요한지 알리는 값이 따로 있지만, 그 값이 거짓이어도 실행기는 백업을 건너뛰지 않습니다. expand·contract는 릴리스마다 사람이 판단해 적는 값이라(「업데이트 버튼은 승인만 하고, docker 소켓은 실행기만 쥔다」), 그 판단이 틀렸을 때도 돌아갈 자리가 있어야 하기 때문입니다.

덤프에는 다음 규칙이 걸립니다.

  • 대상은 메타스토어(사용자·대화·설정을 담는 OwlAI 자체 PostgreSQL)이고, pg_dump의 custom 형식으로 뜹니다.
  • 최근 2개만 남깁니다. 더 오래된 덤프가 필요하면 다음 업데이트를 승인하기 전에 다른 곳으로 옮겨 둬야 합니다.
  • 덤프에는 사용자 계정과 저장된 자격 증명까지 들어 있어서, 파일은 0600, 디렉터리는 0700으로 만듭니다. 만든 뒤에 권한을 좁히면 그 사이에 다른 계정이 파일을 열 수 있으므로, 파일은 처음부터 이 권한으로 엽니다.
  • 쓰는 동안은 이름 끝에 .partial이 붙고, 다 쓴 뒤 덤프 형식 머리(PGDMP)를 확인한 다음에야 정식 이름을 받습니다. 쓰다 끊긴 파일은 백업 목록에 남지 않습니다.
  • 설치를 바꾸기 전에 백업 자리에 DB 크기의 1.2배가 비어 있는지 잽니다. 모자라면 그 자리에서 멈추고, 지금 판이 그대로 돕니다.
  • 앱이 쓰는 DB가 같은 compose 프로젝트의 postgres 컨테이너에 있지 않으면 백업하지 않고 멈춥니다. 외부 DB를 쓰는 설치에서 그 컨테이너를 덤프하면, 앱이 쓰지 않는 DB를 백업한 채 업데이트가 계속되기 때문입니다. 이런 설치는 DB를 직접 백업한 뒤 손으로 업데이트합니다.

덤프에는 DB만 들어갑니다. 문서·회의 음성·샌드박스 파일이 든 객체 저장소(「재배포 뒤에도 남아야 할 파일은 객체 저장소에만 둔다」)는 업데이트가 바꾸지 않으므로, 직전 판으로 돌아가는 데는 DB만 있으면 됩니다. 업데이트 뒤에 올라온 파일은 복원한 DB가 모르는 파일로 저장소에 남습니다. 확인 창은 승인 전에 「문서·생성 파일 등 파일 저장소는 백업 범위 밖입니다」로 이 범위를 알립니다.

실패한 단계와 스키마 변경이 끝 상태를 정한다

실행기는 적용을 서명 확인부터 기동 확인까지 여러 단계로 나눠 돌립니다(단계 목록은 「업데이트 버튼은 승인만 하고, docker 소켓은 실행기만 쥔다」). 끝 상태는 어느 단계에서 실패했는지와 그 판의 스키마 변경에 따라 넷으로 갈립니다.

실패한 단계와 스키마 변경에 따라 갈리는 업데이트 끝 상태
그림 1. 실패한 단계와 스키마 변경에 따라 갈리는 업데이트 끝 상태

이미지를 적재하기 전에 실패하면 설치는 바뀐 것이 없습니다. 서명, 업그레이드 조건, 파일 사본, 디스크, 펼치기, 백업 단계가 여기에 듭니다. 지금 판이 그대로 돌고, 원인을 고친 뒤 다시 승인하면 됩니다.

적재 뒤 서비스 교체 전에 실패하면 실행기는 이미지 고정 파일과 설치 키트, 설정 파일을 업데이트 전 사본으로 되돌립니다. 컨테이너는 아직 옛 판입니다. 워커를 이미 내린 뒤였다면(「워커를 재시작해도 돌던 턴은 답을 내거나 실패를 남긴다」) 옛 고정으로 다시 배포해 워커를 되살립니다. 스키마가 아직 그대로라 옛 코드가 문제없이 섭니다.

서비스 교체나 기동 확인에서 실패하면 그 판의 스키마 변경을 봅니다. 서명된 매니페스트에는 「코드만 되돌려도 되는 가장 옛 판」이 들어 있습니다. 이번 판이 expand이고 지금 판이 그 범위 안이면, 실행기는 파일을 되돌리고 옛 고정으로 다시 배포한 뒤 기동 확인을 한 번 더 합니다. 끝 상태는 되돌림입니다. 스키마는 새 판 그대로이고, 옛 코드가 그 위에서 돕니다.

contract 판이면 매니페스트의 그 값이 이번 판 자신이라, 어느 옛 판도 범위에 들지 않습니다. 실행기는 파일만 되돌리고 컨테이너는 다시 배포하지 않은 채 멈춥니다. 옛 코드를 새 스키마 위에 띄우면 첫머리에서 본 오류가 나기 때문입니다. 상태에는 덤프 경로와 덤프를 뜬 앱 DB 이름이 남습니다. 관리 화면 맨 위에는 「자동으로 되돌릴 수 없어 멈췄습니다. 업데이트 직전 데이터베이스 백업으로 복원해야 합니다.」가 닫을 수 없는 배너로 뜨고, 다음 업데이트를 승인해야 사라집니다.

여기에는 예외가 하나 있습니다. 배포 스크립트가 설치 전 점검이나 이미지 받기처럼 컨테이너를 하나도 바꾸기 전에 멈췄다고 종료 코드로 알리면, 스키마도 그대로입니다. 이때는 contract 판이라도 옛 고정으로 다시 배포해 워커를 되살립니다.

기동 확인이 무엇을 보고 실패를 가르는지는 「배포가 성공했는지는 헬스 체크가 아니라 배포 뒤 실패한 턴으로 판정한다」에서 다룹니다. 컨테이너가 멀쩡히 섰어도 그 판정에 걸리면 이 단계의 실패로 다룹니다.

재부팅이나 docker 엔진 재시작으로 배포 도우미가 끊기면 곧바로 실패로 치지 않습니다. 배포 스크립트는 다시 돌려도 같은 결과가 나오므로, 실행기가 한 번 더 돌려 이어 갑니다. 다시 돈 배포도 끊기거나 실패하면 위 규칙을 따릅니다.

덤프 복원은 서버에서 하고, 배포보다 DB를 먼저 되돌린다

자동으로 되돌릴 수 없어 멈췄으면 사람이 덤프로 복원합니다. 관리 화면은 이 복원을 버튼 대신 서버 명령으로 안내합니다. 서비스가 반쯤 바뀐 상태라 화면이 뜬다고 기대할 수 없기 때문입니다. 관리 화면이 열리지 않아도 설치 디렉터리의 tools/ops/update.sh로 같은 실행기를 부를 수 있습니다. 상태 조회, 요청별 진행 기록, 되돌리기가 화면과 같은 상태 파일을 씁니다.

복원은 다음 순서로 합니다.

  1. tools/ops/update.sh status로 덤프 경로와 덤프를 뜬 앱 DB 이름을 읽습니다. 앱 DB 이름은 설치마다 다를 수 있습니다.
  2. 덤프를 실행기 볼륨에서 꺼냅니다.
  3. backend, worker, frontend를 멈춥니다.
  4. 앱 DB를 지우고 다시 만든 뒤, 스키마 소유자 계정으로 pg_restore합니다.
  5. tools/ops/restart_stack.sh deploy로 배포합니다. 실행기가 고정 파일을 옛 판으로 되돌려 두었으므로 옛 판이 뜨고, 내려가 있던 워커도 이때 함께 뜹니다.
  6. tools/ops/restart_stack.sh updater로 실행기를 다시 띄웁니다.

이 순서에서 꼭 지킬 것은 4번보다 5번을 먼저 돌리지 않는 것입니다. 고정 파일이 이미 옛 판을 가리키므로, 배포를 먼저 하면 옛 코드가 새 스키마 위에서 뜹니다. 업데이트 뒤의 쓰기를 잃고 싶지 않으면, 덤프로 되돌리는 대신 실패 원인을 고친 뒤 같은 판을 다시 승인할 수도 있습니다.

tools/ops/update.sh rollback은 이 복원과 쓰임이 다릅니다. 업데이트가 성공한 뒤 새 판에서 문제를 발견했을 때 코드만 이전 판으로 되돌리는 명령입니다. 마지막 업데이트가 성공했고, 그 판이 지금 판이며, 코드만 되돌려도 되는 범위일 때만 동작합니다. 조건이 맞지 않으면 거절하고 덤프 경로를 알려 줍니다. 되돌리기에는 그 업데이트가 남긴 고정·키트 사본을 쓰는데, 이 사본은 다음 업데이트 요청이 들어오면 지워집니다. 실행기 자신은 새 판으로 남습니다. 실행기는 옛 매니페스트도 판정할 수 있으므로, 실행기의 결함 수정까지 되돌릴 까닭이 없습니다.

확인 창은 이 판정을 승인 전에 미리 보여 줍니다. 코드만 되돌릴 수 있는 판이면 「문제가 생기면 이전 버전으로 바로 되돌릴 수 있습니다.」, 그렇지 않으면 「되돌리려면 업데이트 직전 백업으로 복원해야 합니다.」입니다.

설계에서 고른 것

down 마이그레이션 대신 업데이트 직전 한 시점의 덤프로 돌아가는 쪽을 골랐습니다. down 마이그레이션은 업데이트 뒤의 쓰기를 살릴 수 있다는 장점이 있습니다. 대신 판마다 역방향 스크립트를 쓰고 시험해야 하고, 지운 컬럼의 값은 그래도 되살리지 못합니다. 덤프 복원은 업데이트 뒤에 들어온 쓰기를 버리므로, 기동 확인이 적용 직후에 실패를 잡게 해 버려지는 쓰기가 그 짧은 구간에 그치게 했습니다.

덤프는 최근 2개만 둡니다. 업데이트 덤프는 직전 판으로 돌아갈 길을 남기는 데까지만 맡고, 장기 보관과 파일 백업은 고객 운영의 정기 백업이 맡습니다. 판을 거듭해도 덤프가 디스크를 계속 차지하지 않는 대신, 오래된 덤프가 필요하면 운영자가 승인 전에 옮겨 둬야 합니다.

배포 도우미를 기다리다 시간이 넘으면 실행기는 아무것도 되돌리지 않습니다. 도우미가 아직 컨테이너를 바꾸는 중일 수 있어서, 이때 되돌리기를 시작하면 바꾸는 쪽과 되돌리는 쪽이 같은 설치를 동시에 고칩니다. 화면에는 「앞선 작업이 끝나지 않아 되돌리기를 시작하지 못했습니다.」가 뜨고, 운영자는 도우미가 끝나기를 기다려 그 로그를 보고 판단합니다. 자동 복구를 한 번 덜 하는 대신, 반쯤 바뀐 설치를 다시 반쯤 덮어쓰는 일을 막았습니다.

고객 호스트에서 스스로 업데이트하는 제품이라면 이 갈림을 그대로 옮길 수 있습니다. 릴리스마다 스키마 변경이 옛 코드와 함께 돌 수 있는지 적고, 덤프는 그 값과 상관없이 뜨고, 자동으로 코드를 되돌릴지는 그 값으로 정합니다.

어떤 문제를 해결하나요?

설치·운영과 품질의 다른 글