테마
커밋 주기 기준 (템플릿)
13화에서 제정했다. 배치나 루프에서 COMMIT을 언제, 얼마나 자주 할지 정할 때 쓰는 결정표다. 12화 포스트모템 1호 재발방지 2번 "배치는 N건마다 COMMIT"은 「리스크」 칸이 비어 있었다. 그 칸을 채운 것이 이 문서다.
한 줄 요약: 커밋 주기를 바꿔서 피하려는 두 장애는 서로 반대쪽에 있다. 커밋이 너무 드물면 ORA-30036(언두가 꽉 참)이, 너무 잦으면(긴 읽기와 섞이면) ORA-01555(스냅샷이 사라짐)가 난다. 주기만 바꿔서는 둘 다 피할 수 없다.
왜 이 문서가 있는가
2020-07-22 hd.kang "야간 배치 중 UNDO 부족 장애. 트랜잭션 분할."강도현은 P1을 한 번에 COMMIT 했다가 ORA-30036(언두를 늘릴 수 없음)을 맞고 1,000건마다 끊었다. 그때는 옳았다. 12화에서 '나'는 "트랜잭션을 짧게"를 포스트모템 재발방지책으로 냈고, 오과장이 그것을 주간 보정 배치에 그대로 적용했다. 그 주에 보정 배치가 ORA-01555(snapshot too old)로 죽었다. 같은 처방이 반대쪽 장애를 불렀다.
13화 실측 (ora-hanorder · 작은 언두 재현) | 커밋 주기 | 결과 |
|---|---|---|
| 큰 트랜잭션이 언두를 다 쥔다 | 한 번만 COMMIT | ORA-30036 (약 42,000행째) |
| 커밋이 '고치기 전 값'을 재사용할 수 있게 풀어 버린다 | 1,000건마다 COMMIT (긴 읽기와 동시) | ORA-01555. 게다가 반쯤 반영된 상태가 남는다 |
커밋을 자주 하면 언두를 덜 쥐므로 ORA-30036은 멀어진다. 그런데 언두를 덜 쥔다는 바로 그 점이 ORA-01555를 부른다.
두 장애의 정체 (← 13화에서 본 것)
| ORA-30036 | ORA-01555 | |
|---|---|---|
| 무엇이 모자라나 | 지금 쓸 언두 공간(활성 트랜잭션이 다 쥠) | 과거 시점의 언두(이미 재사용됨) |
| 누가 피해를 보나 | 지금 쓰는 트랜잭션 | 오래 읽는 커서/쿼리 |
| 커밋을 덜 하면 | — | 멀어진다(언두를 풀어 주지 않으니 과거가 남는다) |
| 커밋을 더 하면 | 멀어진다(덜 쥐니까) | 가까워진다(과거를 빨리 풀어 재사용되니까) |
| 한 줄 | "되돌릴 공간이 없다" | "되돌릴 과거가 없어졌다" |
결정표 — 이 배치는 어떻게 커밋하나
순서대로 묻는다. 위에서 답이 나오면 멈춘다.
| # | 묻는 것 | 예 | 아니오 |
|---|---|---|---|
| 1 | 집합 연산 한 문장으로 쓸 수 있나? (INSERT…SELECT · MERGE · UPDATE…) | → 그렇게 쓴다. 커밋은 끝에 한 번만 한다. 언두가 걱정이면 14화(APPEND·파티션·병렬)를 본다 | 2 |
| 2 | 루프의 구동 커서가 읽는 테이블을 그 루프가 고치면서 COMMIT 하나? (= fetch across commit) | → 위험. 읽기와 쓰기를 분리한다(아래 「분리」) | 3 |
| 3 | 한 트랜잭션의 언두가 언두 테이블스페이스에 비해 큰가? (대량 DML) | → 끊는다. 단, 너무 잘게 끊지는 않는다(아래 「주기」). 중간에 실패했을 때의 정합성은 아래 「원자성」을 본다 | 4 |
| 4 | 그 밖의 경우 | 한 번에 COMMIT한다(원자성은 덤으로 따라온다). 언두가 부족하면 2·3으로 간다 |
「분리」 — fetch across commit을 푸는 법
읽는 커서가 도는 동안 그 테이블을 고치고 COMMIT 하면, 커서의 스냅샷이 써야 할 과거 언두를 같은 루프의 커밋이 풀어 버린다. 그래서 ORA-01555가 난다(제 발등 찍기). 다음 셋 중 하나로 끊어 낸다.
| 방법 | 어떻게 | 대가 |
|---|---|---|
| 읽고 → 닫고 → 쓴다 | 고칠 키/ROWID를 먼저 모아 담고(짧은 읽기), 커서를 닫은 뒤 그 목록으로 수정 | 목록을 보관할 메모리(컬렉션·임시테이블) |
| 키 범위로 쪼갠다 | WHERE key BETWEEN a AND b 같은 독립된 짧은 쿼리를 범위마다 돌린다. 조각마다 자기 스냅샷을 쓴다 | 범위 경계 설계 |
| 집합 연산 | 애초에 루프를 없앤다(#1) | 14화 |
「주기」 — 끊어야 할 때, 얼마나
- 한 건마다 COMMIT 하는 것은 과하다. 13화 실측(P1 10만 건)에서 한 건마다 커밋하면 한 번 커밋할 때보다 redo 약 1.9배 · undo 약 2배 · LGWR 기록 약 12배였다.
- 1,000건마다 커밋해도 한 번 커밋할 때와 redo·undo가 거의 같다. '덩어리'가 너무 작지만 않으면 끊는 비용은 거의 보이지 않는다.
- 기준은 "활성 언두가 테이블스페이스를 위협하지 않을 만큼 크게, 하지만 fetch across commit은 되지 않게"다. 보통 수천~수만 건이다.
「원자성」 — 중간에 죽으면
- 끊어서 커밋한 배치가 중간에 죽으면 반쯤 반영된 데이터가 남는다(13화 실측: ORA-01555가 난 시점까지만 보정됨). 한 번에 커밋하는 배치는 전부 롤백된다(전부 아니면 전무).
- 끊을 거라면 재시작 지점을 설계한다. 어디까지 했는지 적어 두고(체크포인트 테이블), 다시 돌릴 때는 이미 처리한 건을 건너뛰거나 멱등하게 처리한다. 「정합성 회귀 체크리스트」의 E(경계)·H(영향)로 반쯤 반영된 데이터가 없는지 반드시 점검한다.
「log file sync」 — 커밋을 자주 하면 비싸지는 자리
- PL/SQL 루프 안에서 COMMIT 하면 세션이 매번 기다리지는 않는다(13화 실측: 10만 번 커밋해도
log file sync는 2회). 대신 LGWR의log file parallel write가 늘어난다(한 번 50 → 한 건마다 약 600). - 클라이언트가 한 건을 처리할 때마다 커밋하거나
COMMIT WRITE WAIT를 쓰면 그제야 커밋마다log file sync를 기다린다(13화 실측: 커밋 수만큼). 행 단위로 커밋하는 애플리케이션이 느린 진짜 이유가 여기에 있다.
쓰는 법
리포트(T1·T4)에서 배치나 루프를 다룰 때는 「리스크」/「롤백 절차」 칸에 이 결정표의 몇 번 경로로 정했는지와 중간에 실패했을 때의 정합성을 적는다.
| 칸 | 적는 것 |
|---|---|
| 커밋 주기 | 결정표 경로(#1~#4) · 최종 주기(한 번 / N건) · 그 N의 근거 |
| 리스크 | 이 주기가 멀리하는 장애와 가까이하는 장애(30036 ↔ 01555) · fetch across commit인가 |
| 중간 실패 | 반쯤 반영이 생기나 · 재시작 지점이 있나 · 멱등한가 |
개정 이력: 13화에 제정했다(12화 포스트모템 1호 재발방지 2번에서 비어 있던 「리스크」 칸을 채웠다). 14화에는 결정표 #1의 "언두가 걱정이면"에 실측을 붙였다. P1 10만 건에서 루프+1,000건 커밋 undo 6.50MB → 한 문장
INSERT … SELECT0.26MB →+APPEND0.02MB였다. 대가는 배타 락(TM LMODE 6)이다(T3 7번 칸). 15화에는 결정표 #1 "한 문장으로 쓸 수 있나"에 "쓰면 무엇을 잃나"를 붙였다(주간 보정 배치 · T3 2호). 규칙(함수)이 두 곳으로 나뉘는가, 성능이 힌트 하나에 걸리는가, 한 행의 실패를 행마다 다룰 수 있는가를 본다. 셋 중 하나라도 잃는다면 PL/SQL로 제대로 쓰고(BULKLIMIT· FORALL · 반송함), 한 문장과 같은 값이 나오는지 잰다(보정: 한 문장 최선 23~34cs · PL/SQL 27~28cs). 커밋은 #4대로 끝에 한 번 한다.