운영 작업

기존 workspace 이전

원본을 보존하며 legacy workspace를 현재 quota로 옮긴다

무엇을 하게 되나

label 없는 legacy volume 또는 다른 byte quota의 workspace를 원본을 보존하며 현재 계약으로 옮긴다.

준비물

전제 조건:

  • 그 volume을 물고 있는 컨테이너가 없어야 한다. 정지된 컨테이너도 포함한다(다시 시작되면 복사 중인 원본에 쓰기 때문이다). 있으면 이름을 알려 주고 거절한다. 돌고 있는 세션이면 먼저 terminate로 멈추고(컨테이너가 지워진다) 옮긴 뒤 resume한다. 컨테이너를 docker rm으로 직접 지우면 진행 중이던 turn이 끊기고, 그 세션은 컨테이너를 잃은 세션으로 처리된다.
  • scheduler pass lock을 잡는다. pass가 도는 중이면 A scheduler pass holds the lock으로 끝난다. 끝나면 다시 실행한다. 복사하는 동안 pass는 돌지 않으므로 GC도 launch도 두 volume을 건드리지 않는다.
  • 그 세션에 끝나지 않은 회수 claim이 없어야 한다. 있으면 다음 pass가 처리한 뒤 다시 실행한다.
  • 세션 row가 있고 closed가 아니어야 한다. closed 세션의 legacy volume은 GC가 회수한다. row가 없는 세션의 legacy volume은 GC도 이 명령도 건드리지 않는다. 우리 것이라는 증거가 이름뿐이기 때문이다. 운영자가 확인하고 직접 지운다.
  • helper 이미지는 digest로 고정돼 있어야 하고 daemon에 이미 있어야 한다. 기본값은 busybox 1.36(DEFAULT_MIGRATION_HELPER_IMAGE)이다. docker pull busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662을 먼저 실행한다. 바꾸려면 --helper-image 또는 EXECUTION_WORKSPACE_MIGRATION_IMAGE를 쓴다.
  • quota preflight(verifyWorkspaceQuota)를 먼저 통과해야 한다. 새 volume은 지금 설정의 상한으로 만들어진다.

회수 claim·현재 quota·helper digest·Docker daemon·설치 id가 이번 설치와 맞아야 한다. 작업 중 scheduler pass는 lock으로 배제된다.

단계

이번 설치의 status와 project label로 실제 scheduler 컨테이너를 고른다. 이 컨테이너는 설치 DB·Docker socket·quota env를 이미 받는다. 개발용 기본 Compose project를 새로 띄우지 않는다.

SCHEDULER_CONTAINER='<this-installation-scheduler-container-id>'
docker exec "$SCHEDULER_CONTAINER" \
  bun run apps/control-host/src/scheduler/migrate-workspace.ts <session-id>

하는 일:

  1. 새 volume을 지금 계약대로 만든다. label과 size를 붙이고, 원본 이름은 werft.migration-source label로, 원본의 생성 시각은 werft.migration-source-created-at label로 남긴다. 이름은 다른 workspace와 같은 1회용이다. workspace는 label로 찾으므로 이름을 바꿀 필요가 없다.
  2. 시작하지 않는 pin 컨테이너(werft-ws-migrate-pin- 접두어)가 원본을 읽기 전용으로 붙잡는다. Docker는 컨테이너가 참조하는 volume을 지우지 않는다. 그래서 pass lock을 잃은 사이에 GC가 끼어들어도 이 실행이 끝날 때까지 원본은 남는다. pin 이름은 실행마다 다르고, 각 실행은 자기 pin만 지운다. 이어서 helper 컨테이너가 원본을 읽기 전용으로, 새 volume을 쓰기로 붙여 cp -a로 복사한다. helper는 root이지만 네트워크가 없고, 파일 복사에 필요한 capability(CHOWN·DAC_OVERRIDE·FOWNER·FSETID)만 가진다.
  3. 같은 helper가 양쪽의 manifest를 비교한다. 비교 대상은 경로 집합, 종류, mode, uid/gid, symlink 대상, 일반 파일의 크기·mtime·SHA-256이다. 디렉터리의 크기와 시각, xattr와 ACL은 비교하지 않는다. busybox cp -a는 hard link를 보존하지 않으므로 hard link는 내용이 같은 별개 파일이 된다. 그만큼 디스크를 더 쓰므로 quota에 걸리면 복사가 실패한다. mtime은 초 단위까지만 비교한다.
  4. 일치하면 원본을 지우기 전에 같은 원본의 다른 복사본이 있는지 다시 본다. 있으면 다른 실행이 아직 일하는 중이다. 그때는 자기 복사본을 버리고 실패한다. 원본이 다른 컨테이너에 붙잡혀 있을 때도 마찬가지다. 둘 다 아니면 helper와 pin을 지우고 원본을 지운다. 그 순간부터 새 volume이 그 세션의 workspace다.

중간에 실패하면 원본은 그대로다. 복사 실패(quota 초과, helper가 만들 수 없는 device 파일 등), 불일치, 시간 초과(--deadline-sec, 기본 3600), lock 유실이 모두 그렇다. 실패한 helper는 docker logs로 볼 수 있게 남겨 둔다. pin도 남아 원본을 계속 붙잡는다. 다시 실행하면 먼저 새 pin으로 원본을 붙잡는다. 그다음 남은 helper, 이전 pin, 미완성 복사본(migration-source이 같은 원본을 가리키는 것)을 지우고 처음부터 복사한다. 원본과 복사본이 함께 있는 동안에는 backend가 그 세션의 기동을 거절하고, GC는 둘 다 후보에서 뺀다. 그래서 반쯤 복사된 트리 위에서 worker가 뜨는 일도, 원본만 먼저 회수되는 일도 없다. 끝내지 않은 채 두면 두 volume이 그대로 남으므로, 다시 실행하거나 직접 정리한다. 원본 이름의 volume이 복사본을 만든 뒤에 다시 만들어졌을 수도 있다. Docker는 없는 이름을 mount하면 빈 volume을 만들기 때문이다. 이때 생성 시각이 맞지 않으므로, 명령은 그 복사본을 미완성으로 보고 지우지 않는다. 대신 거절한다. 복사본이 유일하게 온전한 workspace일 수 있으니 둘을 직접 비교해 정리한다. 원본이 지워지면 새 volume 위에서 worker를 다시 띄운다. 띄우는 것은 다음 pass이고, 그 전에 실패한 launch가 재시도 대기 중이면 대기가 끝난 뒤의 pass다. legacy volume 때문에 실패하는 launch도 다른 실패처럼 횟수가 쌓인다. 상한에 닿으면 세션이 failed가 되므로 되도록 빨리 옮긴다.

확인

helper가 비교한 경로 집합·종류·mode·uid/gid·symlink 대상·일반 파일 크기/mtime/SHA-256이 일치해야 한다. xattr·ACL·directory 크기/시각은 비교하지 않고 hard link는 별개 파일이 된다. 마이그레이션 뒤 새 worker에서 실제 작업 트리와 재개 상태를 확인한다.

자주 막히는 곳

컨테이너가 원본을 물고 있거나 pass lock이 바쁘거나 회수 claim이 남으면 거부한다. 원본 이름이 재생성돼 생성 시각이 달라진 경우, 복사본이 유일한 정상 데이터일 수 있으므로 직접 비교한다.

되돌리기

실패하면 원본·pin·helper를 보존하고 로그를 조사한다. 같은 명령 재실행은 새 pin으로 원본을 보호한 뒤 미완성 복사본을 처리한다. 원본 삭제가 완료된 성공 뒤에는 무조건 이전 이름으로 되돌리지 말고 별도 백업에서 복원한다.

다음에 읽을 곳

저장소 계약, 세션 복구.

이 페이지