Skip to content

[CBRD-27369] Serialize concurrent UPDATE STATISTICS per class (gate + piggyback) - #7900

Open
soheejung-cs wants to merge 5 commits into
CUBRID:developfrom
soheejung-cs:CBRD-27369
Open

[CBRD-27369] Serialize concurrent UPDATE STATISTICS per class (gate + piggyback)#7900
soheejung-cs wants to merge 5 commits into
CUBRID:developfrom
soheejung-cs:CBRD-27369

Conversation

@soheejung-cs

@soheejung-cs soheejung-cs commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

http://jira.cubrid.org/browse/CBRD-27369

Purpose

동일 테이블에 여러 세션이 UPDATE STATISTICS ... WITH FULLSCAN 을 동시에 반복 호출하면 처리율이 붕괴합니다(제보: 단일 순차 대비 약 1/386, shell_heavy cbrd_21362 가 10~15분에서 약 10시간으로 늘어 QA timeout). 코어 덤프 없이 진행이 수 초간 정체됐다가 소수만 통과하는 패턴이 반복됩니다.

원인은 CBRD-26959(#7476) 이후 UPDATE STATISTICS 가 히스토그램을 기본 수집하게 된 것입니다. 수집 경로가 히스토그램 가능 컬럼마다 _db_histogram 카탈로그 인스턴스 행을 S(존재 확인 읽기) → X(blob 쓰기) 로 잠그고 클라이언트 트랜잭션 커밋까지 유지합니다. 다수 세션이 같은 소수의 행 집합에 S/X 를 교차 요구하면서 락 컨보이와 데드락이 발생하고, 데드락 감지기가 주기적으로 피해자를 abort → 소수만 통과 → 재경합이 반복됩니다. #7476 직전 develop 에서는 update_statistics_update_histogram 파라미터(기본값 false)가 이를 gate 했으므로 구버전 UPDATE STATISTICS ... WITH FULLSCAN_db_histogram 을 건드리지 않아 이 경합이 없었습니다.

타 DBMS 도 같은 테이블에 대한 다수의 동시 전수 통계 갱신을 그대로 쌓지 않습니다: PostgreSQL ANALYZE 는 자기충돌하는 ShareUpdateExclusiveLock 으로 직렬화하고 autovacuum 은 이미 처리 중인 테이블을 skip 하며, InnoDB 는 recalc pool 로 중복을 제거하고, Oracle DBMS_STATS 는 기본 직렬입니다.

Implementation

UPDATE STATISTICS 를 클래스 단위로 직렬화합니다. 수집 시작 지점(do_update_stats 의 클래스별 루프, 첫 카탈로그 접근 이전)에서 클래스별 게이트 락을 트랜잭션 스코프로 잡아(커밋 시 해제) 한 번에 한 세션만 그 클래스의 통계·히스토그램을 쓰게 합니다.

  • 게이트 자원(OID_GET_UPDATE_STATS_GATE_OID): 클래스 OID 의 슬롯에 예약 비트(bit 14)를 세운 전용 OID 입니다. 실제 슬롯 번호(작음)·카탈로그 가상 디렉터리 OID(bit 15, VIRTUAL_CLASS_DIR_OID_MASK)·그 밖의 어떤 락 사용처와도 겹치지 않습니다. 따라서 이 OID 에 대한 X 는 자기충돌해 통계 수집을 직렬화하지만, 테이블의 실제 클래스 OID·행·카탈로그 접근 락과는 다른 자원이라 그 테이블의 조회(IS)·DML(IX) 을 막지 않습니다(PostgreSQL ANALYZEShareUpdateExclusiveLock 과 같은 의미). bit 15 를 비워 두어 락 매니저가 특별 취급하는 OID_IS_VIRTUAL_CLASS_OF_DIR_OID() 가 이 OID 에 대해 항상 false 가 되게 했습니다.
  • piggyback: 게이트 획득 전/후로 클래스 통계 timestamp(ci_time_stamp)를 읽어, 대기하는 동안 다른 세션이 이미 갱신했으면 재스캔·히스토그램 빌드·store 를 건너뜁니다(동시 N 개 요청의 전수 스캔이 1 회로 합쳐집니다). 명시적 UPDATE STATISTICS 의 동기 계약(반환 시 최신 통계)은 진행 중 갱신의 완료를 기다린 뒤 반환하는 방식으로 유지됩니다. DROP HISTOGRAM / NO HISTOGRAM 요청은 skip 대상에서 제외합니다.
  • 신규: xstats_enter_update_gate / sqst_enter_update_gate / stats_enter_update_gate 와 요청 NET_SERVER_QST_ENTER_UPDATE_GATE, 그리고 OID 매크로 OID_GET_UPDATE_STATS_GATE_OID.
  • 옵티마이저 히스토그램 읽기의 무잠금화(348e93909): 게이트만으로는 부족한 두 번째 경합이 있었습니다. 질의 컴파일 시 stats_get_histogramdb_get_histogramdb_find_multi_unique(DB_FETCH_READ) 가 객체 계층의 잠금 조회(sbtree_find_uniqueS_SELECT_WITH_LOCK 고정 → 행 S 락, 이어 LC_FETCH_DIRTY_VERSION fetch 도 S 락)를 타므로, 오토커밋 OFF 세션이 UPDATE STATISTICS 를 커밋하지 않은 동안 그 테이블의 모든 SELECT 컴파일이 _db_histogram 행 S 락 대기로 커밋까지 블록됐습니다(실측: 15초 timeout). 반대로 컴파일만 한 세션의 S 락도 커밋까지 남아 UPDATE STATISTICS 의 X 를 막았습니다. 옵티마이저는 마지막 커밋 히스토그램만 필요하므로 잠금도, 트랜잭션 스냅샷도 만들지 않는 읽기 경로를 추가했습니다:
    • mvcc_satisfies_committed(): mvcc_satisfies_dirty 에서 타 트랜잭션의 미커밋 버전만 제외한 "최신 커밋 버전" 가시성 규칙. 미커밋 in-place 갱신은 TOO_NEW 로 판정돼 heap 이 prev_version_lsa 로 직전 커밋 버전을 돌려줍니다.
    • xbtree_find_unique(): _db_histogram 에 대한 S_SELECT 조회는 위 규칙 + NULL_LOCK (카탈로그 클래스 취급과 같음). 쓰기 쪽 S_SELECT_WITH_LOCK / S_UPDATE 는 그대로라 writer 의미론은 변하지 않습니다.
    • LC_FETCH_COMMITTED_VERSION (xlocator_fetch): 같은 규칙으로 객체 fetch.
    • obj_find_multi_attr_committed() / db_get_histogram_committed(): 위 둘을 묶은 읽기 전용 조회. stats_get_histogram 만 사용하고, writer·DDL·;info histogramdb_get_histogram 호출은 그대로 둡니다.
    • 트랜잭션 스냅샷을 만들지 않는 이유: need_skip_mvcc_snapshot_db_histogram 이 들어간 것([CBRD-26202] Add Optimizer Histogram Support #7180)은 컴파일 시점 내부 조회가 RR 스냅샷을 고정시키지 않게 하려는 것이고, 기존 경로(dirty 조회 + dirty fetch)도 스냅샷을 만들지 않았으므로 그 성질을 유지했습니다.

Remarks

  • 실측(80 워커 × 20 회, release): 수정 전 62/1600 완료·0.67 건/초·데드락 다수 → 수정 후 1600/1600 완료·데드락 0·워커 전건 성공, 처리율은 piggyback 으로 회복(단일 세션 대비 대기자 대부분이 재스캔을 건너뜀). cubrid lockdb 상 게이트 자원이 유일한 직렬화점(X 홀더 1 + X 대기, 변환 없는 FIFO)이고 _db_histogram·클래스 락 변환 경합은 사라졌습니다. debug 빌드 동시성 실행에서 assert·코어 없음.
  • 즉시 회피책으로 WITH FULLSCAN, NO HISTOGRAM_db_histogram 을 건드리지 않아 붕괴를 피합니다(실측 200 건/초).
  • 오토커밋 OFF 실측(release, demodb): A 가 UPDATE STATISTICS ON t1 WITH FULLSCAN 을 커밋하지 않은 상태에서 B 의 SELECT ... FROM t1 — 수정 전 _db_histogram 인스턴스 S 락 대기로 15초 timeout, 수정 후 0.039초 정상(대기자 0). 이때 B 의 /*+ RECOMPILE */ 컴파일은 커밋된 구 히스토그램을 사용하고(미커밋 통계 비관찰), A 커밋 뒤 재컴파일하면 새 통계로 플랜이 바뀝니다(iscan card 100 → sscan card 99004). 같은 SQL 을 재컴파일 없이 던지면 XASL 캐시의 구 플랜이 유지되는데, 이는 UPDATE STATISTICS 가 XASL 캐시를 무효화하지 않는 기존 동작으로 본 PR 범위 밖입니다.
  • 범위: 본 PR 은 명시적 UPDATE STATISTICS ON <table> 경로를 직렬화합니다. ON ALL CLASSES 경로는 게이트를 거치지 않습니다(동시 all-classes 는 드문 관리 시나리오이며, 본 변경으로 기존 대비 악화되는 동작은 없습니다) — 필요 시 후속으로 확장합니다.
  • 회귀 재현·검증은 shell_heavy cbrd_21362 및 첨부 재현 스크립트 기준입니다. CTP sql·medium 회귀는 별도 실행합니다.

… piggyback)

동일 테이블에 여러 세션이 UPDATE STATISTICS ... WITH FULLSCAN 을 동시 호출하면 처리율이
붕괴한다(제보: 순차 대비 ~1/386, shell_heavy cbrd_21362 가 ~10시간으로 늘어 QA timeout).

원인: CBRD-26959(CUBRID#7476) 이후 UPDATE STATISTICS 는 히스토그램을 기본 수집한다. 컬럼마다
_db_histogram 카탈로그 행을 S(존재확인)→X(blob 쓰기) 로 잠그고 커밋까지 유지하는데, 다수 세션이
같은 소수 행 집합에 S/X 를 교차 요구하면서 락 컨보이와 데드락 폭풍이 발생한다. 구버전은
update_statistics_update_histogram 기본값이 false 라 _db_histogram 을 건드리지 않아 경합이 없었다.

수정: UPDATE STATISTICS 를 클래스 단위로 직렬화한다. 수집 시작에 클래스별 게이트 락을
트랜잭션 스코프로 잡아(커밋 시 해제) 한 번에 한 세션만 통계·히스토그램을 쓰게 한다.

- 게이트 자원은 통계 수집 전용 OID다: 클래스 OID 에 예약 슬롯 비트(bit 14,
  UPDATE_STATS_GATE_OID_MASK)를 세운 값으로, 실제 슬롯·카탈로그 가상 디렉터리 OID(bit 15)·기타
  어떤 락 사용처와도 겹치지 않는다. 따라서 이 OID 의 X 는 자기충돌해 통계 수집을 직렬화하지만
  테이블의 실제 클래스 OID·행·카탈로그 접근 락과 무관해 조회·DML 을 막지 않는다(PostgreSQL
  ANALYZE 의 ShareUpdateExclusiveLock 과 같은 의미). bit 15 를 비워 두어
  OID_IS_VIRTUAL_CLASS_OF_DIR_OID() 가 이 OID 에 false 가 되게 한다(락 매니저가 그 플래그를
  특별 취급하므로).
- piggyback: 게이트 진입 전/후로 클래스 통계 timestamp 를 읽어, 대기 중 다른 세션이 갱신했으면
  do_update_stats 가 재스캔·히스토그램 빌드·store 를 건너뛴다(N 회 전수 스캔 → 1 회).
  drop histogram / no histogram 요청은 skip 대상에서 제외한다.

신규: xstats_enter_update_gate / sqst_enter_update_gate / stats_enter_update_gate +
NET_SERVER_QST_ENTER_UPDATE_GATE + OID_GET_UPDATE_STATS_GATE_OID.

http://jira.cubrid.org/browse/CBRD-27369

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbvVtggcaYxe1uFvUiahig

# 제목: 변경 요약 (50자 이내)  (refactor, hotfix, feature)

# 본문: 변경 사항에 대한 자세한 설명
# - 주요 변경 사항 1
# - 주요 변경 사항 2

# 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

❌ TC Merge Gate — Merge Blocked

One or more TC PRs are still open. Please merge or close them before merging this PR.

TC Repositories & Branches:

  • cubrid-testcases: TC PR tc/pr-7900 is open (draft) — must be merged or closed first
  • cubrid-testcases-private-ex: TC PR tc/pr-7900 is open (draft) — must be merged or closed first

Steps to unblock:

  1. Merge or close all TC PRs listed above.
  2. Re-run this check: Actions tab → TC Merge Gate → Re-run failed jobs

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

🧪 TC Test Environment Ready

CircleCI Testing:

  • CircleCI will automatically test using the branches below.

TC Repositories & Branches:

Next Steps:

  1. Wait for CircleCI tests to complete
  2. If CircleCI tests failed, please check the test results and fix the issues.
  3. When ready to merge this PR, please merge the TC PR first, then merge this PR.

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "[CBRD-27369] Serialize concurrent UPDATE..." | Re-trigger Greptile

Comment thread src/query/execute_statement.c
Comment thread src/storage/statistics_sr.c Outdated
@soheejung-cs

Copy link
Copy Markdown
Contributor Author

/run all

…on, judge freshness by the _db_class row chn

Greptile review of PR CUBRID#7900 (two findings):

1. The piggyback skipped a waiting request whenever the class statistics had been
   refreshed meanwhile, regardless of that request's collection options: a WITH FULLSCAN
   request waiting behind a default (sampling) collection, or one with explicit BUCKETS /
   RANDOM SEED, returned success without producing what it asked for.  The gate now also
   reports the stored statistics_strategy, and do_update_stats () skips only when the
   committed collection satisfies the request -- never for explicit BUCKETS / RANDOM SEED,
   a WITH FULLSCAN request only when the stored collection was itself a full scan, and a
   plain (sampling) request on any collection.

2. Freshness was judged by the second-granular ci_time_stamp behind a ts_before > 0
   guard, so a class without statistics (every waiter read 0) never piggybacked on the
   first collection, and two collections within one second were indistinguishable.  It is
   now judged by the _db_class row's cache coherency number, which
   catcls_update_class_stats () bumps on every write (old_chn + 1): a changed chn -- or
   bookkeeping appearing where there was none -- means a concurrent collection committed
   while we waited, at any granularity.  The row is read through a scan cache without an
   MVCC snapshot (catcls_get_class_stats ()) so the latest committed version is seen
   regardless of the waiter's own snapshot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbvVtggcaYxe1uFvUiahig

# 제목: 변경 요약 (50자 이내)  (refactor, hotfix, feature)

# 본문: 변경 사항에 대한 자세한 설명
# - 주요 변경 사항 1
# - 주요 변경 사항 2

# 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.
@soheejung-cs

Copy link
Copy Markdown
Contributor Author

리뷰 1차 대응 (879e26ac7)

Greptile P1 2건을 반영했습니다.

  1. piggyback 옵션 대조 — 게이트가 저장된 statistics_strategy도 반환하고, do_update_stats()는 선행 수집이 요청과 동등·상위일 때만 skip 합니다: BUCKETS/RANDOM SEED 명시 요청은 항상 직접 수집, WITH FULLSCAN 요청은 저장 수집이 fullscan일 때만, 기본(샘플링) 요청은 어떤 수집에도 skip.
  2. 신선도 판정 — 초 단위 ci_time_stamp 대신 _db_class 행의 chn(매 통계 기록마다 old_chn + 1)으로 판정해 동일 초 갱신도 감지하고, 통계가 없던 클래스는 found 플래그로 분리했습니다. 행은 스냅샷 없는 scan cache(catcls_get_class_stats())로 읽어 대기자 자신의 스냅샷과 무관하게 최신 커밋을 봅니다.

재검증(debug 빌드, 80워커 × 20회, 데드락·abort·assert 0):

완료 처리율 실패
WITH FULLSCAN (동일 옵션 → piggyback) 1600/1600 41건/초 (단일 세션 0.6건/초) 0
WITH FULLSCAN, 200 BUCKETS (명시 옵션 → 직렬화만) 74/1600 (120초 예산) 0.61건/초 ≈ 단일 세션 0

명시 옵션 요청은 설계대로 piggyback 없이 직렬화되어 각자 수집합니다(처리율 = 단일 세션 수준).

CI 의 indent 가 catalog_class.c / statistics_sr.c 에서 120칸 경계에 수동 개행한 두 줄을
한 줄로 합쳤다. CI 잡이 낸 gitdiff 를 그대로 적용한 것으로 동작 변화는 없다.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbvVtggcaYxe1uFvUiahig

# 제목: 변경 요약 (50자 이내)  (refactor, hotfix, feature)

# 본문: 변경 사항에 대한 자세한 설명
# - 주요 변경 사항 1
# - 주요 변경 사항 2

# 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.
@soheejung-cs

Copy link
Copy Markdown
Contributor Author

/run all

…stogram rows

A query compile on a table reads the table's _db_histogram rows through the
object layer's locking lookup (db_find_multi_unique: S_SELECT_WITH_LOCK unique
search + dirty-version fetch, both S locks). An UPDATE STATISTICS holds those
rows X-locked until its transaction commits, so with autocommit off every
SELECT on that table blocked in stats_get_histogram until the statistics
session committed (reproduced: SELECT waited on the _db_histogram instance
S_LOCK for the whole transaction).

The optimizer only needs the last committed histogram, so give it a read path
that takes no lock and does not build the transaction snapshot:

- mvcc_satisfies_committed: visibility rule for "latest committed version"
  (mvcc_satisfies_dirty minus other transactions' uncommitted versions).
- xbtree_find_unique: an S_SELECT lookup on _db_histogram uses that rule with
  NULL_LOCK, like the catalog classes; S_SELECT_WITH_LOCK / S_UPDATE (writers)
  are unchanged.
- LC_FETCH_COMMITTED_VERSION: xlocator_fetch reads the object under the same
  rule (previous versions from the log when the current one is uncommitted).
- obj_find_multi_attr_committed / db_get_histogram_committed: the unlocked
  lookup + fetch, used by stats_get_histogram. Writers keep db_get_histogram.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uq9B87BEyUD6WGbWe6vfxu
@soheejung-cs soheejung-cs self-assigned this Sep 10, 2026
@soheejung-cs

Copy link
Copy Markdown
Contributor Author

/run all

@soheejung-cs

Copy link
Copy Markdown
Contributor Author

/run all

@soheejung-cs

Copy link
Copy Markdown
Contributor Author

/run rerun 34562280940

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant