운영 안정화 변경을 최신 main과 통합한다
This commit is contained in:
commit
087517821e
47 files changed
+1558
-123
No files matched your search
@@ -2,6 +2,9 @@
|
||||
|
||||
## 문서 상태와 사용법
|
||||
|
||||
2026-11-09 저장 최적화: [물리 압축·정리 처리량·ZFS 운영 경계](../play-audit-storage.md).
|
||||
판단 원문과 현재 기수 보존 계약을 유지하며 저장 표현과 이전 기수 정리 구현을 변경한다.
|
||||
|
||||
이 문서는 플레이 감사의 구조·현재 구현 경계·남은 요구사항을 관리하는 기준 문서다.
|
||||
사용 방법과 DB 적용은 [운영 안내](../play-audit-operations.md)를 따른다.
|
||||
날짜별 구현·측정 기록은 Git 이력과 상위 작업공간의 `report/`에서 확인한다.
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# Hyper-V 저장·복원 이후 게임 복구
|
||||
|
||||
Windows 재부팅 때 Linux VM이 저장·복원되면 게스트의 shutdown/resume hook이
|
||||
실행되지 않을 수 있다. 게임은 종료 이벤트 없이도 기존 lease 만료와 DB snapshot
|
||||
재적재로 복구한다. Windows 식별자 확인은 이 경로에 추가하는 선택 기능이다.
|
||||
|
||||
## 예약턴 임대 복구
|
||||
|
||||
- 실행 루프가 종료되면 DB에서 자기 owner의 장수·국가 예약턴 임대를 해제한다.
|
||||
In-memory rollback으로 leased set에서 빠진 row도 정리한다. 큐 내용과 revision,
|
||||
다른 실행자/API의 임대는 변경하지 않는다.
|
||||
- 계산 전 임대 획득 충돌은 전체 메모리 savepoint를 복원한 뒤 1초 단위로 재시도한다.
|
||||
한 충돌 구간의 상한은 301초다(기본 queue lease 5분 + 여유 1초). 그동안 다음
|
||||
loop마다 운영자 pause/stop과 daemon 권위를 확인한다. 성공한 batch에서 상한을
|
||||
초기화하며 큐가 바뀌어도 같은 실패 구간의 상한을 연장하지 않는다.
|
||||
- 저장·finalize 단계의 소유권 상실, 메모리 복원 실패와 다른 게임 오류는 자동
|
||||
재시도하지 않는다. 상한을 넘은 획득 충돌도 기존 PAUSED/장애 이력 경로로 간다.
|
||||
- daemon의 30초 lease와 fencing epoch, 기존 10분 이하 catch-up 및 장시간 복구
|
||||
정책은 유지한다. 운영자가 정지한 profile을 자동 START하지 않는다.
|
||||
|
||||
## Windows Node.js 에이전트
|
||||
|
||||
필요 파일은 `windows-boot-agent.mjs` 하나이며 외부 npm package가 필요 없다.
|
||||
Node.js 24.22.1 이상을 사용한다. 설정 예시는
|
||||
`tools/host-recovery/windows-boot-agent.example.json`이다.
|
||||
|
||||
Node.js가 Windows 내장 PowerShell의 CIM 조회로 `Win32_OperatingSystem.LastBootUpTime`
|
||||
UTC ticks를 읽고 hostname과 함께 hash한다. 별도의 PowerShell 스크립트 설치나
|
||||
Linux에서 Windows 관리자 명령 실행은 필요 없다. Node 에이전트를 재시작해도
|
||||
같은 Windows boot의 식별자는 같고, Windows를 재부팅하면 달라진다.
|
||||
실제 Windows에서 CIM 조회가 실패하면 가짜 식별자를 만들지 않고 시작을 거부한다.
|
||||
|
||||
1. 파일들을 `C:\ProgramData\Sammo` 같은 고정 경로에 복사한다.
|
||||
2. 디렉터리를 만든 뒤 Windows 터미널에서 private key를 생성한다. 기존 파일을
|
||||
덮어쓰지 않으며 key 내용은 출력하지 않는다.
|
||||
|
||||
```text
|
||||
node C:\ProgramData\Sammo\windows-boot-agent.mjs --create-key C:\ProgramData\Sammo\host-boot.key
|
||||
```
|
||||
|
||||
3. 설정 예시를 `C:\ProgramData\Sammo\host-boot.json`으로 복사한다. `keyFile`은
|
||||
위 경로, `bindAddress`는 **Linux VM에서 접근할 수 있는 Windows의 사설 주소**로
|
||||
설정한다. 예시의 `127.0.0.1`은 안전한 기본값이며 원격 Linux에서 접근할 수 없다.
|
||||
`port` 기본값은 17781이다. Windows 방화벽은 해당 사설 listener에 Linux VM
|
||||
주소에서 오는 TCP 연결만 허용하도록 설정한다. 기존 공개 Caddy는 변경하지 않는다.
|
||||
4. key 파일은 실행 계정만 읽도록 NTFS 권한을 제한하고, 같은 내용을 Linux의
|
||||
`/run/secrets/host_boot_key` 같은 private file로 비공개 전달한다. Git·메신저·
|
||||
CLI 인자에 key 내용을 넣지 않는다. 에이전트는 key를 시작할 때 읽으므로 key
|
||||
교체 시 양쪽 프로세스를 함께 재시작한다.
|
||||
5. 실행한다.
|
||||
|
||||
```text
|
||||
node C:\ProgramData\Sammo\windows-boot-agent.mjs --config C:\ProgramData\Sammo\host-boot.json
|
||||
```
|
||||
|
||||
6. Windows 작업 스케줄러에 **시스템 시작 시** task를 등록한다. 프로그램은 Node의
|
||||
절대 `node.exe` 경로, 인자는 위 `.mjs --config ...` 부분을 사용한다. 로그인 없이
|
||||
실행되고 실패 시 재시작하도록 구성한다. 설정 파일을 사용하므로 터미널 세션의
|
||||
임시 환경변수에 의존하지 않는다. 실제 Windows 설치·방화벽 변경·task 등록은
|
||||
대상 host에서 별도 적용하고 확인해야 한다.
|
||||
|
||||
원한다면 설정 파일 대신 `HOST_BOOT_KEY_FILE`, `HOST_BOOT_BIND_ADDRESS`,
|
||||
`HOST_BOOT_PORT` 환경변수로도 실행할 수 있다.
|
||||
|
||||
## Linux 연결
|
||||
|
||||
기존 game runtime/PM2가 상속하는 서버 환경에 아래 두 값을 함께 설정한다.
|
||||
설정이 없으면 기능을 사용하지 않는다. 설정이 불완전하거나 key 파일이 없거나 잘못돼도
|
||||
고정 경고와 함께 보조 관측만 비활성화하며 게임 기동은 계속한다.
|
||||
|
||||
```dotenv
|
||||
TURN_HOST_BOOT_URL=http://windows-private-address:17781/v1/boot-id
|
||||
TURN_HOST_BOOT_KEY_FILE=/run/secrets/host_boot_key
|
||||
```
|
||||
|
||||
Docker에서는 private key가 들어 있는 전용 디렉터리를 runtime에 read-only mount하고 두 환경변수를
|
||||
**runtime container environment**에 전달해야 한다. 호스트의 `.env`에만 추가하면
|
||||
자동 전달되지 않는다. 기존 Compose와 host overlay를 함께 적용하는 전용 wrapper를
|
||||
사용한다. 실제 secret 파일의 host 경로와 Windows 사설 주소를 확인하기 전에는
|
||||
운영 stack 설정을 추측해 바꾸지 않는다.
|
||||
|
||||
예를 들어 host overlay의 runtime 설정에는 다음 형태로 연결할 수 있다. host 경로는
|
||||
placeholder이며 실제 운영 경로로 교체해야 한다.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
runtime:
|
||||
environment:
|
||||
TURN_HOST_BOOT_URL: ${TURN_HOST_BOOT_URL:-}
|
||||
TURN_HOST_BOOT_KEY_FILE: /run/sammo-host-recovery/host_boot.key
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /absolute/private/path/host-recovery
|
||||
target: /run/sammo-host-recovery
|
||||
read_only: true
|
||||
bind:
|
||||
create_host_path: true
|
||||
```
|
||||
|
||||
위 디렉터리는 보조 기능 전용이므로 없으면 빈 디렉터리를 만들 수 있다. 파일 부재는
|
||||
관측만 비활성화한다. DB·backup 같은 필수 저장소에는 이 fallback을 적용하지 않는다.
|
||||
운영에는 이 전용 디렉터리의 `host_boot.key` 파일을 전달한다.
|
||||
|
||||
처음 인증에 성공한 식별자를 프로세스의 기준으로 삼고 고정된 성공 로그를 남긴다. 이후 다른 식별자를
|
||||
확인하면 `TurnDaemonLeaseLostError`로 기존 실행자를 중단하고 DB를 다시 읽는다.
|
||||
종료 시 owner/epoch가 여전히 자기 것인 경우에만 DB lease와 `clock_ready`를
|
||||
해제하므로 다른 실행자를 중지시키지 않는다. 재시작은 기존 profile 상태를
|
||||
보존하며 PAUSED/STOPPED를 임의로 RUNNING으로 바꾸지 않는다.
|
||||
|
||||
보통 최대 2초 간격으로 확인한다. 벽시계와 monotonic 경과가 1초 이상 어긋나면
|
||||
cache를 무시하고 다시 조회한다. 조회 timeout은 1초다. 네트워크·인증·응답 오류는
|
||||
분당 한 번의 고정 경고만 남기고 기존 DB lease 복구를 유지한다. 처음 인증받기 전
|
||||
발생한 reboot는 이 신호만으로 확정할 수 없으며 기존 복구 경로가 담당한다.
|
||||
|
||||
## 인증 프로토콜과 검증
|
||||
|
||||
key 파일은 32바이트 난수를 lowercase hex 64자로 저장한다. 매 요청마다 새 32바이트
|
||||
challenge를 만들며, key 자체를 네트워크에 보내지 않는다.
|
||||
|
||||
- GET `/v1/boot-id`, query string 없음.
|
||||
- `x-sammo-challenge`: lowercase hex 64자.
|
||||
- `x-sammo-signature`: `HMAC-SHA256(key, "sammo-host-boot-request-v1\n" + challenge)`.
|
||||
- JSON: `version=1`, `challenge`, `bootId`(hex 64자), `signature`.
|
||||
- 응답 signature:
|
||||
`HMAC-SHA256(key, "sammo-host-boot-response-v1\n" + challenge + "\n" + bootId)`.
|
||||
|
||||
위 `\n`은 실제 LF다. Linux는 challenge와 응답 signature를 검증하고, redirect와
|
||||
2048바이트 초과 응답을 거부한다. 서명된 옛 응답도 새 challenge에는 유효하지 않다.
|
||||
시각 정확도나 요청 시각 만료에 의존하지 않는다. 이 프로토콜은 인증과 무결성을
|
||||
제공하며 암호화는 하지 않는다. 사설 망에서만 사용하며 공개 무인증 재개 API를
|
||||
만들지 않는다.
|
||||
|
||||
```sh
|
||||
node --test tools/host-recovery/windows-boot-agent.test.mjs
|
||||
pnpm --filter @sammo-ts/game-engine test hostBootMonitor.test.ts runtimePauseGate.test.ts reservedTurnLeaseRetry.test.ts
|
||||
```
|
||||
|
||||
CI에서는 Windows boot를 모사한 HTTP 응답, 위조·재사용·timeout, 실제 PostgreSQL
|
||||
lease fencing과 queue cleanup을 검증한다. 실제 Windows CIM·Task Scheduler와
|
||||
Hyper-V 재부팅 검증을 대체하지 않는다. 배포 후 기존 장애 진단 화면의 lease/
|
||||
epoch와 public lobby `turnEngineRunning`, 실제 tick/queue revision 진행을 확인한다.
|
||||
|
||||
기능을 해제하려면 Linux의 두 환경변수를 함께 제거하고 해당 runtime을 정상
|
||||
재기동한다. Windows task도 중지할 수 있다. DB migration이나 게임 초기화는 없다.
|
||||
|
||||
근거: [Microsoft LastBootUpTime](https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-operatingsystem),
|
||||
[Node.js HMAC와 timingSafeEqual](https://nodejs.org/api/crypto.html),
|
||||
[게임 시계 복구 계약](../architecture/game-clock-reconciliation.md),
|
||||
[릴리스 운영](../release-operations.md).
|
||||
@@ -5,6 +5,9 @@
|
||||
관리한다. 아래 적용 절차는 각 프로필의 DB 보존 업데이트를 위한 것이며,
|
||||
문서에 기능이 기재되어 있다고 운영 DB에 적용된 것은 아니다.
|
||||
|
||||
저장량 최적화, 중지된 프로필 정리, ZFS 이전과 rollback은
|
||||
[저장량과 운영 정리](play-audit-storage.md)를 따른다.
|
||||
|
||||
## 지금 사용할 수 있는 기능
|
||||
|
||||
| 화면 | 사용할 수 있는 정보 | 읽을 때 주의할 점 |
|
||||
@@ -55,7 +58,7 @@
|
||||
## DB migration과 적용 순서
|
||||
|
||||
정식 game migration에 감사 테이블과 인덱스가 포함되어 있다. `prisma db push`나
|
||||
수동 CREATE TABLE로 대신 적용하지 않는다. 현재 game chain은58개이며 다음 감사
|
||||
수동 CREATE TABLE로 대신 적용하지 않는다. 현재 game chain은59개이며 다음 감사
|
||||
migration들을 포함한다. 기존 기록을 삭제하거나 지난달 상세를 역산하지 않는다.
|
||||
|
||||
| migration | 준비되는 저장소/제약 |
|
||||
@@ -69,6 +72,7 @@ migration들을 포함한다. 기존 기록을 삭제하거나 지난달 상세
|
||||
| `20260916060000_widen_play_audit_ticks` | 월 표본/정책 tick을 BIGINT로 확장하여 약60개월 이후 INTEGER 초과 방지 |
|
||||
| `20260916070000_add_play_audit_decision` | NPC/자동턴 결정 요약과 순서별 상세 chunk, bounded 정리용 FK/index |
|
||||
| `20260916080000_index_play_audit_decision_month` | 기존 장수 인덱스를 월 조건 포함 인덱스로 교체하여 기수 전체 조회 방지 |
|
||||
| `20261109000000_compact_play_audit_storage` | 무손실 압축 chunk 형식과 작은 JSON inline 압축 설정 |
|
||||
|
||||
운영은 [릴리스 절차](release-operations.md)의 **DB 보존 버전 업데이트**로 해당 고정
|
||||
commit을 적용한다. 이 기능을 켜기 위해 시나리오를 초기화할 필요는 없다. 수동 환경의
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# 플레이 감사 저장량과 운영 정리
|
||||
|
||||
2026-11-09 저장 최적화는 Core 전용 감사의 물리 표현과 정리 구현을 바꾼다.
|
||||
현재 기수 전체 기록·판단 순서·정책 참조·hash·API 응답·권한은 유지한다.
|
||||
원문 샘플링이나 보존기간 축소를 하지 않는다.
|
||||
|
||||
## 물리 표현
|
||||
|
||||
- 결정 chunk는 기존 `steps` JSONB 배열 또는 `{encoding:"gzip-json-v1",data:"base64"}`로 저장한다.
|
||||
128 step 단위는 그대로이며 gzip level 3을 사용한다. 압축 envelope가 더 크면 원래 배열을 쓴다.
|
||||
- replay hash는 압축 전 원문으로 계산한다. API는 공용 decoder 뒤 기존 step schema를 검증한다.
|
||||
손상·알 수 없는 버전은 조회 오류이며 빈 자료로 바꾸지 않는다.
|
||||
- 압축 해제는 chunk당 16MiB로 제한한다. 더 큰 원문은 기존 배열로 저장해 잘라내지 않는다.
|
||||
- 장수 snapshot `data`와 결정 `summary`는 PostgreSQL `STORAGE MAIN`,
|
||||
`toast_tuple_target=512`로 작은 반복 JSON도 inline 압축한다. JSON 값은 바뀌지 않는다.
|
||||
- migration `20261109000000_compact_play_audit_storage`는 기존 테이블을 재작성하지 않는다.
|
||||
chunk CHECK는 새 쓰기에 즉시 적용하고 대형 과거 자료 검사는 `NOT VALID`로 미룬다.
|
||||
정리 후 `ALTER TABLE ... VALIDATE CONSTRAINT play_audit_decision_chunk_steps_check`를 실행한다.
|
||||
기존 자료의 압축 변경은 별도 bounded backfill 또는 검증된 export/restore가 필요하다.
|
||||
|
||||
## 정리 처리량과 실행 수명주기
|
||||
|
||||
- 이전 기수 결정 부모를 기존 `(server_id,nation_id,tick,id)` 인덱스로 최대 200개 잠근다.
|
||||
한 transaction의 chunk+header 삭제 합도 200행 이하이며 큰 결정 하나가 있어도 상한을 지킨다.
|
||||
- 다음 성공 batch까지 100ms를 기다린다. RESET과 동일 advisory lock, identity 재검사,
|
||||
2초 statement/250ms lock 제한, rollback을 유지한다. 현재 기수의 결정과 표본은 삭제하지 않는다.
|
||||
- API outbox는 전달 후 24시간을 보존한다. 기존 인덱스 순서로 최대 500행씩 정리하고,
|
||||
backlog가 있으면 다음 dispatch에서도 이어간다. 한 호출은 최대 10 batch/50ms이며
|
||||
미전달 자료는 삭제하지 않는다. 빈 상태에서는 60초 주기로 돌아간다.
|
||||
- game API/daemon이 중지된 프로필도 host scheduler가 다음 CLI로 정리한다.
|
||||
CLI는 접속 URL이나 SQL 원문을 출력하지 않는다. 기본은 dry-run이다.
|
||||
|
||||
```sh
|
||||
# 먼저 common/logic/infra/game-engine을 빌드한다. 접속 정보는 기존 process 환경으로 전달한다.
|
||||
node tools/play-audit-maintenance.mjs --schemas=che,kwe,pwe,twe,nya,pya,hwe
|
||||
node tools/play-audit-maintenance.mjs --schemas=che,kwe,pwe,twe,nya,pya,hwe --budget-ms=3000 --apply
|
||||
```
|
||||
|
||||
schema마다 최신 identity를 읽고 각 삭제 transaction에서 다시 확인한다. 예상 identity가 바뀌면
|
||||
그 schema의 처리를 중단한다. host timer는 다음 실행에서 새 identity를 읽는다.
|
||||
장수/국가/명령/input_event 같은 gameplay 테이블은 이 CLI의 삭제 대상이 아니다.
|
||||
|
||||
## ZFS 운영 경계
|
||||
|
||||
호스트는 감사 전용 tablespace와 backup 전용 dataset을 별도로 제공한다.
|
||||
hot gameplay, WAL, legacy archive를 임의로 이동하지 않는다. DB와 tablespace는 한 cluster이므로
|
||||
tablespace 파일만 백업하는 것으로 복구를 보장하지 않는다.
|
||||
|
||||
1. 전체 DB의 일관된 압축 backup을 ZFS에 만들고 checksum, archive 읽기, 실제 복원을 확인한다.
|
||||
2. DB 보존 DEPLOY로 writer/reader/migration을 함께 적용한다.
|
||||
3. profile 상태를 기록하고 공식 STOP으로 쓰기를 중단한다. 원래 STOPPED인 profile은 유지한다.
|
||||
가오픈까지 1시간 이상 남은 RESERVED는 runtimeExpected=false이므로 예약을 유지할 수 있다.
|
||||
실제 process 비기동과 identity/hash 안정성을 확인하고 export의 예상 상태를 RESERVED로 고정한다.
|
||||
유지보수 중 RESET 등으로 identity가 바뀌면 해당 profile을 새로 export/검증하며 이전 자료를 덮어쓰지 않는다.
|
||||
4. 각 schema에서 현재 serverId의 8개 감사 테이블을 일관되게 export한다.
|
||||
월 child는 현재 월 header와 join하고 결정 chunk는 현재 결정 header와 join한다.
|
||||
5. 격리 DB에서 복원·건수·내용 hash를 검증한다. 이전 기수는 전체 ZFS backup에 남는다.
|
||||
6. 하나의 transaction에서 감사 테이블만 TRUNCATE하고 ZFS tablespace로 옮긴 뒤
|
||||
현재 기수 export를 다시 넣는다. 부모를 먼저 넣고 모든 건수/hash와 identity를 확인한 뒤 commit한다.
|
||||
index도 별도로 ZFS로 옮긴다. 실패하면 전체 transaction을 rollback한다.
|
||||
7. CHECK 검증과 ANALYZE를 수행하고 원래 RUNNING이었던 profile만 공식 START한다.
|
||||
현재 기록, 새 기록의 decode/hash, lease/clock, 공개 HTTPS와 루트/ZFS 실제 사용량을 확인한다.
|
||||
|
||||
기존 데이터가 적을 때는 bounded DELETE 뒤 별도 재작성으로 공간을 회수할 수도 있다.
|
||||
수천만 건 backlog에서 export/TRUNCATE/restore는 현재 자료를 보존하며 물리 공간을 즉시
|
||||
회수하기 위한 계획된 유지보수 절차다. backup 검증이나 profile 정지가 빠진 자동 작업으로 쓰지 않는다.
|
||||
ZFS dataset이 없을 때 루트 디렉터리에 쓰지 않도록 host mount guard와 재현 가능한 Compose 설정이 필요하다.
|
||||
|
||||
## rollback
|
||||
|
||||
이전 reader는 압축 envelope를 이해하지 못한다. 구형 버전으로 돌아가기 전 해당 기록을
|
||||
새 decoder로 원래 JSON 배열로 변환하고 같은 hash/개수를 검증한다. 물리 압축 설정은
|
||||
이전 애플리케이션과 호환되며 원래 TOAST 설정으로 돌릴 필요가 없다.
|
||||
현재 기수 복원 중 실패하면 transaction rollback으로 기존 table/data/tablespace를 보존한다.
|
||||
기수 전체 과거 자료가 필요하면 ZFS의 변경 전 backup을 별도 DB에 복원한다.
|
||||
운영 DB 전체를 과거 dump로 덮어써 새 gameplay를 잃게 하지 않는다.
|
||||
@@ -376,6 +376,18 @@ Hyper-V의 VM 정지/재개, 체크포인트 또는 host 시간 보정은 게스
|
||||
NTP 로그와 Windows Hyper-V/백업 작업 이력을 대조합니다. lease 기간을 늘리거나
|
||||
시간 동기화를 임의로 끄는 것으로 해결하지 않습니다.
|
||||
|
||||
예약턴의 실행 전 임대 충돌은 메모리 rollback 뒤 1초 간격, 한 구간 최대 301초 동안
|
||||
해제·만료를 기다립니다. flush/finalize의 ownership 상실은 재시도하지 않습니다.
|
||||
종료 시에는 rollback으로 journal에서 사라진 자기 owner의 queue 임대도 DB에서
|
||||
해제합니다. 다른 owner와 queue 내용/revision은 보존합니다. 대기 중에도 매 loop의
|
||||
운영자 pause/stop과 daemon gate를 통과하며 상한을 넘으면 기존 오류 정지를 유지합니다.
|
||||
|
||||
Windows의 저장·복원으로 guest 이벤트가 없을 때를 위한 선택적 boot identity 확인은
|
||||
[Windows Node.js 에이전트 설치와 Linux 연결](./developer/host-boot-recovery.md)을 따릅니다.
|
||||
`TURN_HOST_BOOT_URL`과 private `TURN_HOST_BOOT_KEY_FILE`을 함께 설정하면 인증된
|
||||
식별자 변경을 기존 lease 상실/재기동으로 처리합니다. 통신 실패는 기존 복구를
|
||||
막지 않으며 운영자의 PAUSED/STOPPED를 자동 해제하지 않습니다.
|
||||
|
||||
Turn daemon의 DB persistence interactive transaction은 연결 획득 대기(`maxWait`)
|
||||
10초와 callback 실행(`timeout`) 30초를 각각 제한합니다. Prisma 기본 연결 대기
|
||||
2초는 일시적인 DB 지연에도 callback 시작 전 `P2028`로 서버를 PAUSED로 만들 수
|
||||
|
||||
Reference in new issue
Block a user