Skip to content

Latest commit

 

History

History
392 lines (287 loc) · 17.5 KB

File metadata and controls

392 lines (287 loc) · 17.5 KB

1.4.5–1.4.6 → latest In-Place Upgrade (shard_map PK Refactor + Rename to item_shard_map) — Deployment Runbook

  • 마이그레이션 파일: 1.4.5-1.4.6_to_latest.sql
  • 대상 버전: stable 태그 1.4.51.4.6 (동일 스키마 — #1888 PK 리팩토링 이전이라 변환 동일)
  • 대상 변경:
    • <idx>_shard_map 테이블을 <idx>_item_shard_map 로 RENAME (의미 명확화: 주어가 item)
    • PK 를 item_id(shard_id, item_id) 로 변경
    • id_in_shard 컬럼 제거, row_idx 컬럼 신설 (sort-by-item_id 위치를 0-기반으로 저장)
    • <idx>_shard_map_legacy 테이블 제거

2026-06-04 개정 (initSchema-extraction): 간소화 이후 binary는 부팅 시 self-heal이 사라지므로 본 스크립트가 그 몫을 떠안습니다. (1) PART A 글로벌 델타 추가 — keys.status, shards.owner_merge_task_id(+ idx_keys_status_transient); (2) idx_<idx>_item_shard_map_row_idx_alive partial 생성 제거 + DROP INDEX IF EXISTS 보강 (미사용); (3) idx_<idx>_ctxt_map_deprecated partial 추가. 자세한 사유는 .sql 상단 AMENDMENT 주석을 참조하십시오. 1.4.0–1.4.4 배포는 자매 스크립트 1.4.0-1.4.4_to_latest.sql을 사용하십시오.


0. 배경

race 분석과 리팩토링 정당화 요약:

  • 현재 <idx>_shard_mapitem_id PK + id_in_shard 컬럼 구조.
  • 머지 cutover 가 UPSERT-on-item_id 로 동작하면서 옛 (shard_id, id_in_shard) 좌표가 영구 소실.
  • 이 소실이 검색 race ("Item ID count mismatch") 의 직접 원인.
  • <idx>_shard_map_legacy 는 이 race 의 임시 처방.

본 마이그레이션은 PK 와 컬럼 구조를 바꿔 race 와 legacy 테이블을 함께 제거하고, 의미가 더 명확한 <idx>_item_shard_map 이름으로 동시 RENAME 합니다. 이름 변경은 "shard 의 map" 이라는 오독을 막고 "item 이 어느 shard 의 어느 위치에 있는지" 라는 주어를 분명히 합니다.

⚠️ 본 SQL 만으로는 race 가 다 막히지 않습니다. Application 측에서 §5 의 변경 사항 (특히 §5.2 cutover INSERT 패턴) 이 함께 배포되어야 cutover ↔ DeleteData race 까지 차단됩니다. SQL 과 application 변경은 한 배포 단위로 묶여야 합니다.


1. 사전 요구사항

다음이 충족된 상태에서만 진행:

  • 신 application binary 가 빌드되어 배포 준비 완료. 신 binary 는:
    • id_in_shard 컬럼을 안 보고 sort-by-item_id 로 slot 위치 계산
    • <idx>_shard_map_legacy 테이블을 안 봄
    • Cutover INSERT 패턴 (§5.2) 적용 — INSERT ... SELECT FROM ctxt_map FOR UPDATE OF cm
    • RawShard 생성 path 제거 (§5.3)
    • is_raw 검색 라우팅 사이트 제거 (§5.4)
    • GetMetadata 응답에 item_id 포함 (§5.5)
  • SDK 신 버전이 응답에서 item_id 기반 dedup 처리
  • 마이그레이션 윈도우 확보 (인덱스 크기에 따라 수 분 ~ 수십 분)
  • DB backup 확보 (재해 시 복원 경로)

⚠️ 구 binary 와 새 schema 는 호환 불가. 마이그레이션 전후 binary 와 schema 가 짝을 이뤄야 합니다.

⚠️ §5 의 application 변경은 SQL 적용과 한 배포 단위. 부분 적용 (예: 스키마만 바뀌고 cutover 패턴 안 바뀜) 은 race 를 다른 형태로 잔존시킬 수 있습니다.


2. 진행 순서

단계 1: 트래픽 일시 정지

# 옵션 A: 완전 down (가장 단순)
docker compose stop envector-backend

# 옵션 B: read-only freeze (write 만 막음)
# Set SDK / gateway flag to reject InsertData / DeleteData / merge-by-request-ids
# Search 는 허용 (구 schema 에서도 안전, race 외엔 작동)

단계 2: 인덱스 목록 확인

SELECT lower(index_name) AS index_name
FROM indexes
ORDER BY index_name;

각 인덱스에 대해 1.4.5-1.4.6_to_latest.sql 의 "Per-index block template" 을 복사하고 <index_name> 을 치환해서 실행.

대량 인덱스가 있다면 스크립트로 자동화:

psql "$DATABASE_URL" -tAc "SELECT lower(index_name) FROM indexes" \
  | while read idx; do
      sed "s/<index_name>/$idx/g" 1.4.5-1.4.6_to_latest.sql \
        | psql "$DATABASE_URL"
    done

단계 3: 검증 (각 인덱스마다)

-- (a) 신 테이블 스키마 확인
\d <index_name>_item_shard_map
-- 기대값:
--   Primary key: (shard_id, item_id)
--   Columns:    shard_id, item_id, row_idx, deprecated, created_at, updated_at
--   NO column: id_in_shard

-- (b) 옛 테이블이 _old 로 보존되었는지 확인 (24h 롤백 윈도우용)
SELECT EXISTS (
    SELECT 1 FROM information_schema.tables
    WHERE table_name = '<index_name>_shard_map_old'
);
-- expect: true (24h 안정 운영 후 drop)

-- (c) legacy 테이블 부재
SELECT EXISTS (
    SELECT 1 FROM information_schema.tables
    WHERE table_name = '<index_name>_shard_map_legacy'
);
-- expect: false

-- (d) row_count invariant
SELECT
    (SELECT COUNT(DISTINCT item_id) FROM <index_name>_item_shard_map WHERE deprecated = FALSE) AS distinct_alive,
    (SELECT row_count FROM indexes WHERE index_name = '<index_name>') AS index_row_count;
-- expect: distinct_alive = index_row_count

-- (e) merge-lock 테이블 생성 확인 (Step 8 — pre-existing main gap 백필)
SELECT EXISTS (
    SELECT 1 FROM information_schema.tables
    WHERE table_name = '<index_name>_item_merge_lock'
);
-- expect: true

-- (f) per-index 인덱스 집합 확인 (2026-06-04 개정: row_idx_alive 미생성, ctxt_map_deprecated 추가)
SELECT indexname FROM pg_indexes
WHERE tablename = '<index_name>_item_shard_map' ORDER BY indexname;
-- expect 포함: idx_<index_name>_item_shard_map_item_id / _alive / _deprecated
-- expect 불포함: idx_<index_name>_item_shard_map_row_idx_alive
SELECT indexname FROM pg_indexes
WHERE tablename = '<index_name>_ctxt_map' AND indexname = 'idx_<index_name>_ctxt_map_deprecated';
-- expect: 1 row

모든 인덱스가 (a)(b)(c)(e)(f) 통과해야 다음 단계.

글로벌 델타 (PART A — 1회 확인):

SELECT column_name FROM information_schema.columns
WHERE (table_name = 'keys'   AND column_name = 'status')
   OR (table_name = 'shards' AND column_name = 'owner_merge_task_id');
-- expect: 2 rows

PART A는 .sql 상단에 포함되어 sed 루프에서 인덱스마다 재실행되지만 전부 IF NOT EXISTS라 첫 인덱스 이후 no-op입니다. 1회만 적용됨을 위 쿼리로 확인하십시오.

단계 4: 신 binary 배포

⚠️ 반드시 단계 1~3(SQL 마이그레이션)을 먼저 완료한 뒤 신 binary 를 배포하십시오. 신 binary 의 startup 마이그레이션(migrateAllensureDeprecatedColumnsOnPerIndexTables)은 새 이름 <idx>_item_shard_mapALTER TABLE ... ADD COLUMN deprecated 을 실행합니다. 테이블이 아직 옛 이름 <idx>_shard_map 이면 relation does not existstartup 이 실패합니다 (IF NOT EXISTS 는 컬럼만 가드, 테이블은 가드하지 않음).

docker compose up -d envector-backend

단계 5: 트래픽 재개

SDK / gateway 의 freeze flag 해제.

단계 6: 모니터링 (최소 24 시간)

다음 메트릭 / 로그 패턴 감시:

  • ❌ "Item ID count mismatch" 에러 (있어선 안 됨)
  • ❌ "shard_map_legacy" 참조 에러 (마이그레이션 누락 시 발생)
  • indexes.row_countCOUNT(DISTINCT item_id) 일치
  • ✅ 검색 latency 정상 범위
  • ✅ 머지 cutover 정상 진행

단계 7: 옛 테이블 정리 (24 시간 이상 안정 운영 후)

-- 인덱스별 _old 테이블 영구 삭제
DO $$
DECLARE
    idx TEXT;
BEGIN
    FOR idx IN SELECT lower(index_name) FROM indexes LOOP
        EXECUTE format('DROP TABLE IF EXISTS %I_shard_map_old', idx);
    END LOOP;
END $$;

3. 롤백 절차

⚠️ 24 시간 안에 문제 발견 시 (옛 _old 테이블이 살아 있는 동안만):

단계 R1: 트래픽 정지

docker compose stop envector-backend

단계 R2: 테이블 swap-back

-- 각 인덱스마다
BEGIN;
ALTER TABLE <index_name>_item_shard_map RENAME TO <index_name>_item_shard_map_failed;
ALTER TABLE <index_name>_shard_map_old  RENAME TO <index_name>_shard_map;
COMMIT;

이후 구 binary 가 다시 <idx>_shard_map 이름으로 접근하므로 의도대로 복귀.

단계 R3: 구 binary 재배포

docker compose up -d envector-backend:<previous-tag>

단계 R4: 트래픽 재개

⚠️ 24 시간 이후_old 테이블이 삭제되면 자동 롤백 불가. DB backup 으로부터의 시점 복구만 가능.


4. Known Issues / 주의사항

4.1 마이그레이션 도중 INSERT SELECT 의 메모리 사용

큰 인덱스 (> 10M shard_map rows) 의 경우 INSERT INTO ... SELECT 가 메모리를 많이 씁니다. 필요시 shard_id 단위로 나눠 실행하십시오. row_idx 는 shard 별 ROW_NUMBER 이므로 item_id 로 자르면 순번이 깨집니다 — 한 shard 의 전체 행이 같은 청크에 들어가야 합니다.

-- shard_id 부분집합 단위 청크 (한 shard 는 통째로 한 청크에 — row_idx 정합 보존)
INSERT INTO <index_name>_item_shard_map (shard_id, item_id, row_idx, deprecated, created_at, updated_at)
SELECT shard_id, item_id,
       ROW_NUMBER() OVER (PARTITION BY shard_id ORDER BY item_id) - 1,
       deprecated, created_at, updated_at
FROM <index_name>_shard_map
WHERE shard_id IN ( /* 이번 청크의 shard_id 목록 */ )
ON CONFLICT (shard_id, item_id) DO NOTHING;

4.2 인덱스 재생성 시 통계 갱신

신 PK 와 새 인덱스가 생긴 직후 PostgreSQL 의 query planner 가 옛 통계를 사용할 수 있음. 마이그레이션 직후:

-- 신 binary 가 읽는 새 테이블 대상 (옛 _shard_map 은 _shard_map_old 로 rename 됨)
ANALYZE <index_name>_item_shard_map;

각 인덱스마다 실행 권장.

4.3 다중 인덱스 환경에서의 부분 실패

여러 인덱스 마이그레이션 중간에 한 인덱스가 실패하면 부분적으로 신 schema 와 구 schema 가 혼재. 신 binary 는 모두 신 schema 가정. 따라서:

  • 마이그레이션 스크립트는 각 인덱스마다 BEGIN ... COMMIT 으로 묶여 있음
  • 한 인덱스 실패해도 다른 인덱스는 영향 없음
  • 모든 인덱스가 성공해야 신 binary 배포 가능
  • 일부만 성공한 상태에서 신 binary 를 띄우면 실패한 인덱스 검색이 깨짐 → 실패한 인덱스 만 골라 재시도하거나, 해당 인덱스를 일시적으로 비활성화 후 후속 처리

4.4 SDK 호환성

새 schema 자체는 SDK 가 직접 보지 않으므로 SDK 변경 불필요. 다만 새 binary 의 응답 형식이 바뀐다면 SDK 호환 매트릭스 별도 확인 필요.


5. Application 측 변경 (마이그레이션과 한 묶음으로 배포)

⚠️ 마이그레이션 SQL 적용 직후 배포되어야 하는 application 변경 사항. 다섯 변경이 한 PR/배포 단위로 묶여야 정합성 유지됩니다.

5.1 item_shard_map 스키마 대응 (옛 shard_map)

본 PR 에서 <idx>_shard_map 테이블은 <idx>_item_shard_map 로 RENAME 됩니다. 아래 코드 변경은 이 rename 과 함께 적용됩니다. 함수명 dbtable.ShardMap() 는 유지하고 suffix 상수만 item_shard_map 으로 변경했습니다 — 호출처 일괄 치환은 불필요하며, 전 호출처가 자동으로 새 테이블명을 해석합니다.

  • services/internal/dbtable/dbtable.go: suffixShardMap 상수를 item_shard_map 로 변경 (함수명 ShardMap() 유지), ShardMapLegacy() / suffixShardMapLegacy 제거
  • services/internal/backend/database/models.go::IndexShardMap 의 PK 를 (shard_id, item_id) 로, IdInShard 필드 제거. autoIncrement 도 제거.
  • services/internal/backend/database/models.go::IndexShardMapLegacy struct 통째로 제거
  • suffix 상수 변경으로 전 dbtable.ShardMap( 호출처가 자동으로 <idx>_item_shard_map 해석 (별도 호출처 치환 작업 없음)
  • services/internal/backend/database/init.go 의 alive partial index 컬럼 (id_in_shard → item_id)
  • services/internal/backend/database/init.goensureShardMapLegacyTable / legacy schema 관련 코드 제거 (현재 init.go 에는 직접 호출이 없으나 grep 재확인 필요)
  • services/internal/backend/database/index_shardmap.goshard_map_legacy 관련 코드 전부 제거 (getItemIdFromIdMap 의 legacy CTE 포함). dbtable 호출처 컨텍스트 검증 + IdInShard 필드 사용처 제거.
  • services/internal/backend/item/cleanup-worker/cleanup-worker.go::scanShardMapLegacy 제거
  • PK 변경에 따른 INSERT ... ON CONFLICT 절 조정 (item_id PK → (shard_id, item_id) PK)
  • IndexShardMap{...} instantiate 사이트에서 IdInShard: 필드 사용 제거 (grep -rn "IdInShard:")
  • InsertShardMap 시그니처 변경: (ctx, db, indexName, shardID, itemID) — idInShard 파라미터 제거. 호출처 일괄 갱신.

5.2 PR #1870 §2 의 cutover 패턴 채택 (race 차단의 핵심)

신 모델에서도 cutover ↔ DeleteData 일관성은 PR #1870 §2 (commit bf39b2b9) 의 이미 검증된 패턴을 그대로 채택 합니다. 새 lock 패턴 도입 없이.

§2 의 두 가지 핵심:

  1. effectiveItemIDs 전파: forwardMergeTaskToShaper 가 실제 처리한 item set 을 반환 → apply TX 의 shard_map row 수와 blob slot 수 일치
  2. 같은 TX 안 MarkShardMapDeprecated: cutover INSERT 직후, 같은 TX 에서 ctxt_map.deprecated=TRUE 인 item 의 shard_map 행도 deprecated=TRUE 로 flip

신 모델 적용 시 SQL 형태:

-- INSERT (effectiveItemIDs 기반) — 신 PK (shard_id, item_id) 라 충돌 없음
INSERT INTO <idx>_item_shard_map (shard_id, item_id, deprecated) VALUES
  ($1, eff_item_1, FALSE), ($1, eff_item_2, FALSE), ...;

-- 같은 TX: 옛 행 deprecated 처리. cutover 가 INSERT-only 이므로 옛 (shard_id, item_id)
-- 행이 그대로 살아 있음 → UPDATE 로 deprecated=TRUE flip.
UPDATE <idx>_item_shard_map SET deprecated = TRUE
WHERE item_id = ANY($2::bigint[])
  AND shard_id <> $1
  AND deprecated = FALSE;

체크리스트:

  • PR #1870 §2 의 effectiveItemIDs 반환 로직 차용 (shaper interface 변경 포함)
  • MarkShardMapDeprecated 호출이 모든 cutover 경로 (raw cutover, non-raw apply) 에 포함되어 있는지 확인
  • 신 모델 PK 변경에 따른 MarkShardMapDeprecated 의 WHERE 절을 shard_id=<new> AND item_id IN ... 형태로 조정
  • 기존 InsertShardMapList 의 ON CONFLICT (item_id) 분기 제거 — 신 PK 에선 conflict 없음

5.3 RawShard 생성 path 제거

  • services/internal/backend/item/item-core.go:226-268 createSearchableRawShardMetadata 함수 제거
  • services/internal/backend/item/item-core.go:348-367 의 single-row insert 의 raw shard 생성 분기 제거
  • services/internal/backend/rawshard/ 패키지 전체 삭제 (publish.go)
  • services/internal/backend/load_index_raw_fallback.go 파일 전체 삭제
  • services/internal/backend/index.goloadIndexRaw* 관련 호출/import 정리
  • PR #1870 §1, §4, §14b (manual raw cutover 관련) 코드 함께 제거

5.4 is_raw 검색 라우팅 사이트 제거

다음 6 사이트에서 is_raw 필터 제거 (is_raw 컬럼 자체는 유지):

  • services/internal/backend/database/index_shardmap.go:969 getNonRawShardIDsByItemIDsAndStatuses
  • services/internal/backend/database/index_shardmap.go:1011 getSearchableRawShardIDByItemIDs
  • services/internal/backend/database/index_shardmap.go:1158 countItemsInNonRawShardStatuses
  • services/internal/backend/database/index_shardmap.go:1348 getDeprecatedShardIDs
  • services/internal/backend/database/shard.go:244 listSearchableNonRawShardsByIndex
  • services/internal/backend/database/shard.go:358 filterRawShardIDs

나머지 26 사이트 (lifecycle / storage path / RPC 분기 / cleanup) 는 그대로 유지.

5.5 검색 응답 / dedup

  • backend GetMetadata 응답에 item_id 명시 포함 (현재는 좌표만)
  • SDK 측: index.search top-k 응답 처리 시 item_id 기준 dedup
  • SDK 호환성: 응답 protobuf 변경 시 backward compatibility 확인

5.6 검색 라우팅 단순화 (선택, 별도 PR 가능)

  • orchestrator 의 GetShardListByIndexName / GetShardListByClusterId / GetShardListByNodeIds 는 그대로 유지
  • R3 fix 와 함께 진행 가능: JOIN indexes i ON i.index_name = s.index_name WHERE i.is_loaded = true 추가

5.7 PR #1870 obsolete 코드 정리

Track B 적용 후 obsolete:

  • §1 (manual merge requeue) — rawShard 자체가 없음
  • §4 (AreItemsInNonRawSearchable) — manual cutover 가 사라짐
  • §5 (DeleteData soft-delete placeholder) — legacy 없으니 race 없음
  • §6 (GetMetadata 1× retry) — race 자체가 없으니 retry 불필요
  • §13a (phantom-slot placeholder), §13b (missing-shard placeholder) — 같음
  • §14b (AreItemsAllTrackedInShardMap) — manual path 없음

유지 (신 모델에서도 필요):

  • §2 (Shaper-blob effectiveItemIDs + MarkShardMapDeprecated) — §5.2 에서 그대로 채택
  • §3a (intra-batch race), §7 (orchestrator concurrent-delete), §11 (slot-count tripwire)

5.8 fix/legacy-prune-by-shard-status 브랜치 폐기

  • 해당 브랜치는 Track B 적용 후 완전 obsolete. PR 화하지 않고 폐기.

6. 연락처 / 참고

  • 관련 PR: #1870 (fix/cutover-shard-race-mitigation) — 본 리팩토링이 §5/§6/§13 을 obsolete
  • 별도 brunch: fix/legacy-prune-by-shard-status — 본 리팩토링이 완전히 obsolete