Lance fragment 무한 누적 + 파일당 삭제 커밋 — 대량 삭제 시 ingest 32시간 stall #230

Closed
opened 2026-08-05 02:12:15 +00:00 by altair823 · 1 comment
Owner

증상

대량 삭제가 발생한 워크스페이스에서 kebab ingest 가 진행바 0/12115 상태로 약 32시간 동안 ingest 루프에 진입하지 못한다. 문서 1건 purge 에 20초가 걸리고, 그 시간 전부가 삭제와 무관한 오버헤드다.

kebab reset --vector-only 로 Lance 를 비우면 같은 작업이 문서 1건당 0.17초로 떨어진다 — 약 120배 차이. 즉 비용은 삭제할 벡터의 개수가 아니라 Lance 데이터셋의 누적 상태에서 나온다.

원인 (세 겹)

1. 삭제가 파일 1건당 Lance 커밋 1회

sweep_deleted_files 는 루프 안에서 파일마다 delete_by_chunk_ids 를 호출한다 (crates/kebab-app/src/lib.rs:2101-2113). execute_orphans_only 도 동일 구조 (crates/kebab-app/src/reset.rs). delete_by_chunk_ids 는 200개 배치로 table.delete(predicate) 를 실행하므로 (crates/kebab-store-vector/src/store.rs:288-347), 평균 5 chunk 짜리 문서에서는 결국 문서 1건 = Lance 커밋 1회가 된다. 삭제 대상 5,834건 → 커밋 5,834회.

빈 테이블에서도 이 커밋은 그대로 발생한다. reset --vector-only 직후 재실행한 sweep 이 10분 동안 _versions / _transactions 에 각각 3,915개 파일을 만들었고, 같은 구간에서 purge 된 문서 수는 3,965건이었다 (1:1 대응).

2. fragment 가 무한 누적 — compaction 호출이 없다

upsert 는 asset 1건당 merge_insert_batch 를 1회 호출한다 (crates/kebab-store-vector/src/store.rs:256). Lance 는 이때 fragment 를 하나 만든다. 코드베이스 전체에 optimize / compact_files / cleanup_old_versions 호출이 없다.

도그푸딩 인스턴스 실측 (문서 11,229건):

항목
fragment 수 (data/) 11,892
manifest 1개 크기 1.1 MB (fragment 전체를 나열하므로)
_versions/ 총 크기 870 MB
실제 벡터 데이터 (data/) 347 MB
최신 버전 번호 11,971

메타데이터가 실데이터의 2.5배다. manifest 크기가 코퍼스에 비례해 커지므로 모든 쓰기 경로가 함께 느려진다.

3. lance 가 fragment 마다 DataFusion planner 를 새로 만든다

table.delete(predicate) 는 fragment 전체를 스캔하는데, FilteredReadStream::read_fragment 가 fragment 마다 Planner::new 를 호출하고 그 안에서 LanceContextProvider::defaultregister_functionsgeodatafusion::register 가 실행된다. geo(지리정보) UDF 와 Arrow geometry 스키마 전체를 fragment 11,892개마다 처음부터 재구성한다.

sample <pid> 4초, 메인 스레드 100%:

kebab_app::ingest_with_config_opts
  → kebab_app::sweep_deleted_files
    → LanceVectorStore::delete_by_chunk_ids
      → tokio Runtime::block_on
        → lance::io::exec::filtered_read::FilteredReadStream::read_fragment
          → lance_datafusion::planner::Planner::new
            → LanceContextProvider::default
              → lance_datafusion::udf::register_functions
                → geodatafusion::register            ← 샘플의 최대 비중
                  → geoarrow_schema::type::GeometryType::data_type
                    → arrow_schema::field::Field::new / malloc / free

kebab 은 geo 기능을 쓰지 않는다. lancedb = { version = "0.23", default-features = false } 인데도 lance-datafusiongeodatafusion 0.1.1 을 무조건 끌어온다 (lancedb 0.23.1 / lance 1.0.1).

실측 요약

조건 문서 1건 purge 5,834건 총 소요
fragment 11,892개 20.0 초 약 32시간
Lance 비어 있음 (reset --vector-only 후) 0.17 초 약 16분

제안

  1. 삭제를 배치로. sweep_deleted_files / execute_orphans_only 에서 chunk_id 를 전부 모아 루프 종료 후 delete_by_chunk_ids 를 1회 호출. 커밋 5,834회 → 30회(200개 배치). SQLite purge 는 건별 커밋을 유지해야 하므로(중단 안전성) 벡터 삭제만 뒤로 미루고, 실패 시 orphan 벡터가 남는 현재 정책은 그대로 둘 수 있다.
  2. compaction 정책 추가. ingest 종료 시 또는 fragment 수 임계 초과 시 Lance optimize() / compact_files() + cleanup_old_versions() 호출. 지금은 색인을 오래 쓸수록 단조적으로 나빠지고, 사용자가 회복할 수단이 reset --vector-only (전량 재임베딩) 밖에 없다.
  3. geodatafusion 등록 비용 확인. lance 쪽에 feature flag 로 끌 수 있는 경로가 있는지 확인하고, 없으면 upstream 이슈로 올린다. planner 를 delete 호출당 1회 재사용하는 것만으로도 fragment 수만큼 곱해지는 비용이 사라진다.
  4. doctor 에 fragment 수 / manifest 크기 / 버전 수 지표를 노출. 지금은 사용자가 이 상태를 알 방법이 없다.

관련

  • sweep 구간의 무표시 문제 (별도 이슈)
  • chunks_fts 삭제 전체 스캔 (별도 이슈) — Lance 를 비운 뒤 남은 6건/초 상한의 원인
## 증상 대량 삭제가 발생한 워크스페이스에서 `kebab ingest` 가 진행바 `0/12115` 상태로 **약 32시간** 동안 ingest 루프에 진입하지 못한다. 문서 1건 purge 에 20초가 걸리고, 그 시간 전부가 삭제와 무관한 오버헤드다. `kebab reset --vector-only` 로 Lance 를 비우면 같은 작업이 문서 1건당 0.17초로 떨어진다 — **약 120배 차이**. 즉 비용은 삭제할 벡터의 개수가 아니라 Lance 데이터셋의 누적 상태에서 나온다. ## 원인 (세 겹) ### 1. 삭제가 파일 1건당 Lance 커밋 1회 `sweep_deleted_files` 는 루프 안에서 파일마다 `delete_by_chunk_ids` 를 호출한다 (`crates/kebab-app/src/lib.rs:2101-2113`). `execute_orphans_only` 도 동일 구조 (`crates/kebab-app/src/reset.rs`). `delete_by_chunk_ids` 는 200개 배치로 `table.delete(predicate)` 를 실행하므로 (`crates/kebab-store-vector/src/store.rs:288-347`), 평균 5 chunk 짜리 문서에서는 결국 **문서 1건 = Lance 커밋 1회**가 된다. 삭제 대상 5,834건 → 커밋 5,834회. 빈 테이블에서도 이 커밋은 그대로 발생한다. `reset --vector-only` 직후 재실행한 sweep 이 10분 동안 `_versions` / `_transactions` 에 각각 **3,915개** 파일을 만들었고, 같은 구간에서 purge 된 문서 수는 3,965건이었다 (1:1 대응). ### 2. fragment 가 무한 누적 — compaction 호출이 없다 `upsert` 는 asset 1건당 `merge_insert_batch` 를 1회 호출한다 (`crates/kebab-store-vector/src/store.rs:256`). Lance 는 이때 fragment 를 하나 만든다. 코드베이스 전체에 `optimize` / `compact_files` / `cleanup_old_versions` 호출이 **없다**. 도그푸딩 인스턴스 실측 (문서 11,229건): | 항목 | 값 | |---|---| | fragment 수 (`data/`) | 11,892 | | manifest 1개 크기 | 1.1 MB (fragment 전체를 나열하므로) | | `_versions/` 총 크기 | 870 MB | | 실제 벡터 데이터 (`data/`) | 347 MB | | 최신 버전 번호 | 11,971 | 메타데이터가 실데이터의 2.5배다. manifest 크기가 코퍼스에 비례해 커지므로 **모든 쓰기 경로**가 함께 느려진다. ### 3. lance 가 fragment 마다 DataFusion planner 를 새로 만든다 `table.delete(predicate)` 는 fragment 전체를 스캔하는데, `FilteredReadStream::read_fragment` 가 fragment 마다 `Planner::new` 를 호출하고 그 안에서 `LanceContextProvider::default` → `register_functions` → `geodatafusion::register` 가 실행된다. geo(지리정보) UDF 와 Arrow geometry 스키마 전체를 fragment 11,892개마다 처음부터 재구성한다. `sample <pid>` 4초, 메인 스레드 100%: ``` kebab_app::ingest_with_config_opts → kebab_app::sweep_deleted_files → LanceVectorStore::delete_by_chunk_ids → tokio Runtime::block_on → lance::io::exec::filtered_read::FilteredReadStream::read_fragment → lance_datafusion::planner::Planner::new → LanceContextProvider::default → lance_datafusion::udf::register_functions → geodatafusion::register ← 샘플의 최대 비중 → geoarrow_schema::type::GeometryType::data_type → arrow_schema::field::Field::new / malloc / free ``` kebab 은 geo 기능을 쓰지 않는다. `lancedb = { version = "0.23", default-features = false }` 인데도 `lance-datafusion` 이 `geodatafusion 0.1.1` 을 무조건 끌어온다 (lancedb 0.23.1 / lance 1.0.1). ## 실측 요약 | 조건 | 문서 1건 purge | 5,834건 총 소요 | |---|---|---| | fragment 11,892개 | 20.0 초 | **약 32시간** | | Lance 비어 있음 (`reset --vector-only` 후) | 0.17 초 | 약 16분 | ## 제안 1. **삭제를 배치로.** `sweep_deleted_files` / `execute_orphans_only` 에서 chunk_id 를 전부 모아 루프 종료 후 `delete_by_chunk_ids` 를 1회 호출. 커밋 5,834회 → 30회(200개 배치). SQLite purge 는 건별 커밋을 유지해야 하므로(중단 안전성) 벡터 삭제만 뒤로 미루고, 실패 시 orphan 벡터가 남는 현재 정책은 그대로 둘 수 있다. 2. **compaction 정책 추가.** ingest 종료 시 또는 fragment 수 임계 초과 시 Lance `optimize()` / `compact_files()` + `cleanup_old_versions()` 호출. 지금은 색인을 오래 쓸수록 단조적으로 나빠지고, 사용자가 회복할 수단이 `reset --vector-only` (전량 재임베딩) 밖에 없다. 3. **geodatafusion 등록 비용 확인.** lance 쪽에 feature flag 로 끌 수 있는 경로가 있는지 확인하고, 없으면 upstream 이슈로 올린다. planner 를 delete 호출당 1회 재사용하는 것만으로도 fragment 수만큼 곱해지는 비용이 사라진다. 4. `doctor` 에 fragment 수 / manifest 크기 / 버전 수 지표를 노출. 지금은 사용자가 이 상태를 알 방법이 없다. ## 관련 - sweep 구간의 무표시 문제 (별도 이슈) - `chunks_fts` 삭제 전체 스캔 (별도 이슈) — Lance 를 비운 뒤 남은 6건/초 상한의 원인
Author
Owner

PR #233(압축 정책) + PR #234(삭제 배치화 / 삭제 경로 압축 / doctor 지표)로 네 제안을 모두 반영해 닫는다.

실측 근거는 두 PR 본문과 tasks/HOTFIXES.md 2026-08-16 항목에 있다. 요약하면 2.8만 문서 색인 중 속도가 분당 32~36건으로 평평하게 유지됐고(수정 전 30.7 → 4.3건 단조 하락), LanceDB 크기는 문서가 1.7배 늘었는데 15 GB에서 2.8 GB로 줄었다. 문서 364건 삭제가 만드는 Lance 커밋은 33회에서 1회가 됐다.

이슈 본문 중 두 가지는 실측과 달랐다. 회복 수단이 reset --vector-only 전량 재임베딩뿐이라고 적혀 있었으나, 기존 테이블 압축 한 번이 153초에 15 GB를 1.7 GB로 되돌린다(행 수 불변). 그리고 메타데이터 대 실데이터 비율은 11,229 문서에서 2.5배, 16,828 문서에서 7.2배로, 제곱으로 증가한다는 것이 두 지점에서 확인됐다.

PR #233(압축 정책) + PR #234(삭제 배치화 / 삭제 경로 압축 / doctor 지표)로 네 제안을 모두 반영해 닫는다. 실측 근거는 두 PR 본문과 `tasks/HOTFIXES.md` 2026-08-16 항목에 있다. 요약하면 2.8만 문서 색인 중 속도가 분당 32~36건으로 평평하게 유지됐고(수정 전 30.7 → 4.3건 단조 하락), LanceDB 크기는 문서가 1.7배 늘었는데 15 GB에서 2.8 GB로 줄었다. 문서 364건 삭제가 만드는 Lance 커밋은 33회에서 1회가 됐다. 이슈 본문 중 두 가지는 실측과 달랐다. 회복 수단이 `reset --vector-only` 전량 재임베딩뿐이라고 적혀 있었으나, 기존 테이블 압축 한 번이 153초에 15 GB를 1.7 GB로 되돌린다(행 수 불변). 그리고 메타데이터 대 실데이터 비율은 11,229 문서에서 2.5배, 16,828 문서에서 7.2배로, 제곱으로 증가한다는 것이 두 지점에서 확인됐다.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#230