[CBRD-27369] Serialize concurrent UPDATE STATISTICS per class (gate + piggyback) - #7900
Open
soheejung-cs wants to merge 5 commits into
Open
[CBRD-27369] Serialize concurrent UPDATE STATISTICS per class (gate + piggyback)#7900soheejung-cs wants to merge 5 commits into
soheejung-cs wants to merge 5 commits into
Conversation
… 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 # 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.
soheejung-cs
requested review from
beyondykk9,
hornetmj and
shparkcubrid
as code owners
September 8, 2026 10:48
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
❌ TC Merge Gate — Merge BlockedOne or more TC PRs are still open. Please merge or close them before merging this PR. TC Repositories & Branches:
Steps to unblock:
|
🧪 TC Test Environment ReadyCircleCI Testing:
TC Repositories & Branches:
Next Steps:
|
Contributor
|
Reviews (1): Last reviewed commit: "[CBRD-27369] Serialize concurrent UPDATE..." | Re-trigger Greptile |
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 # 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.
Contributor
Author
리뷰 1차 대응 (
|
| 팔 | 완료 | 처리율 | 실패 |
|---|---|---|---|
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 # 참고: 이 라인 아래의 내용은 커밋 메시지에서 제거됩니다.
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
Contributor
Author
|
/run all |
Contributor
Author
|
/run all |
Contributor
Author
|
/run rerun 34562280940 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
http://jira.cubrid.org/browse/CBRD-27369
Purpose
동일 테이블에 여러 세션이
UPDATE STATISTICS ... WITH FULLSCAN을 동시에 반복 호출하면 처리율이 붕괴합니다(제보: 단일 순차 대비 약 1/386, shell_heavycbrd_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 로 중복을 제거하고, OracleDBMS_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) 을 막지 않습니다(PostgreSQLANALYZE의ShareUpdateExclusiveLock과 같은 의미). bit 15 를 비워 두어 락 매니저가 특별 취급하는OID_IS_VIRTUAL_CLASS_OF_DIR_OID()가 이 OID 에 대해 항상 false 가 되게 했습니다.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_histogram→db_get_histogram→db_find_multi_unique(DB_FETCH_READ)가 객체 계층의 잠금 조회(sbtree_find_unique는S_SELECT_WITH_LOCK고정 → 행 S 락, 이어LC_FETCH_DIRTY_VERSIONfetch 도 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 histogram의db_get_histogram호출은 그대로 둡니다.need_skip_mvcc_snapshot에_db_histogram이 들어간 것([CBRD-26202] Add Optimizer Histogram Support #7180)은 컴파일 시점 내부 조회가 RR 스냅샷을 고정시키지 않게 하려는 것이고, 기존 경로(dirty 조회 + dirty fetch)도 스냅샷을 만들지 않았으므로 그 성질을 유지했습니다.Remarks
cubrid lockdb상 게이트 자원이 유일한 직렬화점(X 홀더 1 + X 대기, 변환 없는 FIFO)이고_db_histogram·클래스 락 변환 경합은 사라졌습니다. debug 빌드 동시성 실행에서 assert·코어 없음.WITH FULLSCAN, NO HISTOGRAM은_db_histogram을 건드리지 않아 붕괴를 피합니다(실측 200 건/초).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 범위 밖입니다.UPDATE STATISTICS ON <table>경로를 직렬화합니다.ON ALL CLASSES경로는 게이트를 거치지 않습니다(동시 all-classes 는 드문 관리 시나리오이며, 본 변경으로 기존 대비 악화되는 동작은 없습니다) — 필요 시 후속으로 확장합니다.cbrd_21362및 첨부 재현 스크립트 기준입니다. CTPsql·medium회귀는 별도 실행합니다.