테마
12화 · 실습과 기록
여기서 하는 일: 세 사람을 동시에 세워 줄을 만들고, 누가 누구를 막는지
V$로 찍고, 데드락 트레이스를 꺼내 읽고, 첫 포스트모템을 쓴다.1_사건.md를 읽은 것은 "선배가 하는 걸 지켜본 것"이고, 이 문서를 끝내야 직접 해 본 것이 된다. 개념이 막히면2_파고들기.md를 펼친다.시간이 없는 주에는 STEP 0 → 1 → 4 → 7 → 10만 한다(약 60분).
이 화의 실습은 "세 사람"이 필요하다
동시에 일어나는 일은 세션 하나로는 잴 수 없다. 그래서 03, 04, 05, 11은 이렇게 돌린다.
[일꾼_1] [일꾼_2] [일꾼_3] ← 뒤에 먼저 띄워 둔다. 혼자서는 아무것도 안 한다
↑ ↑ ↑ 수첩(T_C12_CTL)에 적힌 지시를 순서대로 읽어 그대로 한다
└────────┴────────┘
[지휘자] ← 앞에서 돌린다. 지시를 적고, 줄이 생기면 V$ 를 찍는다- 일꾼 파일은 세 개가 전부다(
일꾼_1.sql,일꾼_2.sql,일꾼_3.sql). 한빛유통과 신규 플랫폼에서 같은 파일을 쓴다. - 지휘자가 일꾼에게 역할 이름을 붙인다(03은 BATCH · SCR1 · SCR2, 11은 ORD1 · ADMIN · ORD2).
- ★ 안전장치: 일꾼은 지시가 60초 동안 없으면 스스로 ROLLBACK하고 끝난다. 지휘자는 끝에서 일꾼을 퇴근시키고 남은 세션이 0인지 확인한다. 락을 쥔 세션이 남는 일은 없다.
- 지휘자가 "workers W1 W2 W3 are not running"을 찍고 멈추면 일꾼을 안 띄운 것이다. "more than 3 other sessions"로 멈추면 지난번 일꾼이 아직 살아 있는 것이니 1분 기다렸다가 다시 돌린다.
- 이 컨테이너는
job_queue_processes = 1이라 잡(job)으로는 동시에 못 띄운다. 그래서docker exec를 여러 개 띄운다(07화, 11화와 같다).
띄우는 법 (Git Bash · 이 화의 실습/ 폴더에서)
bash
H="docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1"
for w in 1 2 3; do $H < 일꾼_$w.sql > /dev/null & done; $H < 03_N1_지휘자.sql; waitPowerShell
powershell
$OutputEncoding = New-Object System.Text.UTF8Encoding $false
1..3 | ForEach-Object { Start-Job -ArgumentList (Resolve-Path "일꾼_$_.sql") -ScriptBlock { param($p)
Get-Content $p -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 } } | Out-Null
Get-Content 03_N1_지휘자.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1
Get-Job | Wait-Job | Remove-Job시간(cs)은 지휘자가 다음 지시를 내리는 간격과 CPU 2개라는 사정에 따라 실행마다 조금씩 다르다. 판정은 누가 누구를 막는가, LMODE / REQUEST, 받은 번호로 한다. USN, SLOT, WRAP 값도 실행마다 다르므로 같은 판 안에서 같은 값인가만 본다.
STEP 0 — 직전 리비전을 연다 · 전제를 확인한다 (5분) ★ 매주 처음 하는 일
bash
docker start ora-hanorder
cat ../01화_인수인계_없는_인수인계/실습/00_관통코드_생성.sql # PKG_ORD_STLM 원본 — N1_NEXT_NO 를 찾는다
cat 리비전/N1_rev00.sqlN1은 01화부터 판이 없던 조각이다. 오늘 N1_rev00이 처음 생긴다. 함수 본문 한 줄과 그 위의 주석 한 줄을 적어 둔다.
본문 : SELECT NVL(MAX(STLM_SEQ),0)+1 FROM T_ORD_STLM
주석 : 시퀀스는 구멍이 생겨서 안 씀 (2019.03 강도현)
메모 : 채번 테이블 T_STLM_NUM 이 있는데 안 쓴다. LAST_NO 는 0그다음은 점검과 준비다.
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 00_준비와_점검.sql| 확인 | 기대 |
|---|---|
| [점검 1] [점검 2] 내 계정의 다른 세션 · 락 | 0행 (남아 있으면 1분 뒤 다시) |
[전제] stlm_rows · stlm_max · last_no · fk_mt · fk_all · pkg | 100000 · 100000 · 0 · 0 · 1 · VALID |
| 만든 것 | 100000 · 100000 · 100000 · 무효 0 |
fk_all의 1개는 SCOTT 견본EMP → DEPT다. 주문 시스템(M_·T_테이블)의 FK는 0개다. 1_사건에서 후보 1번을 지운 근거가 이것이다. 00은 원본T_ORD_STLM,T_STLM_NUM,PKG_ORD_STLM에 아무것도 쓰지 않는다. 모두_C12복사본을 만들어 쓴다.
STEP 1 — 예측을 봉인한다 (5분)
Q1. 세 세션이 동시에 MAX+1 로 번호를 받는다 (첫 사람은 COMMIT 전). 뒤의 둘은 기다리는가? ______
받은 번호는? ______
Q2. 핫픽스(채번 테이블 FOR UPDATE) 아래에서 화면들이 막혔다. V$LOCK 에서 화면이 기다리는 것은
TM 인가 TX 인가? 모드(REQUEST)는? ______
Q3. MAX+1 그대로, 정산번호에 PK 만 건다. 뒤의 둘은 어떻게 되나? ______
Q4. 데드락(ORA-00060)이 나면, 에러를 받은 쪽의 트랜잭션은 어떻게 되나? ______
Q5. 10만 행을 UPDATE 한 트랜잭션의 V$LOCK 은 몇 줄인가? ______
Q6. FK 자식 컬럼에 인덱스가 없을 때 부모 한 행을 지우면, 자식 테이블에 무슨 락이 걸리나? ______▶ 적고 나서 펼치기
| 대부분의 답 | 실측 | |
|---|---|---|
| Q1 | "줄을 선다" | 기다리지 않는다(0~1cs). 셋 다 100001. 막지 않으니까 겹친다 |
| Q2 | "채번 테이블에 TM 락" | TX · REQUEST 6. TM은 셋 다 3 (서로 막지 않는다). 화면은 테이블이 아니라 배치의 트랜잭션을 기다린다 |
| Q3 | "겹침이 막힌다" | 막히긴 한다. TX REQUEST 4로 기다렸다가, 배치가 COMMIT하자 ORA-00001 |
| Q4 | "롤백된다" | 문장 하나만 되돌려진다. 트랜잭션과 그 앞의 락은 그대로라서 상대는 계속 기다린다 |
| Q5 | "수천 줄" · "테이블 락으로 바뀐다" | 2줄(TM 3 · TX 6). 1행일 때와 같다. 락 에스컬레이션은 없다 |
| Q6 | "자식 테이블 전체에 S 락(4)" | 이 컨테이너에서는 자식 TM 락이 보이지 않았다. 부모 DELETE가 자식 쪽 진행 중 트랜잭션을 TX REQUEST 4로 하나씩 기다렸다 |
Q6은 공식문서의 설명(자식 테이블 full table lock)과도 다르다. 그래서 이 화의 규칙은 "모드는 재서 적는다"이다.
STEP 2 — 락의 모양을 혼자 잰다 (10분)
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 01_락의_모양.sql| 블록 | 확인 | 기대 | ☐ |
|---|---|---|---|
| [A] | INSERT · UPDATE · DELETE · SELECT FOR UPDATE · SELECT의 V$LOCK | 앞의 넷은 TM 3 + TX 6 · SELECT는 없다 | ☐ |
| [B] | 1행 UPDATE 뒤 · 10만 행 UPDATE 뒤 V$LOCK 줄 수 | 2 · 2 | ☐ |
| [C] | TX의 ID1 · ID2 → USN · SLOT · WRAP = V$TRANSACTION | match = Y · TRACE_NAME이 TX-xxxxxxxx-xxxxxxxx 꼴 | ☐ |
| [D] | TM 5 x 5 매트릭스 | RX(3)끼리 O · RX–S X · X는 전부 X (2_파고들기 §3) | ☐ |
| [E] | INSERT → DDL → ROLLBACK 후 남은 행 수 E1 ~ E6 | 1 · 2 · 3 · 4 · 5 · 5. 실패한 DDL(ORA-00955 · ORA-00902)도 COMMIT했다 | ☐ |
[E]를 적어 둔다. 배치 중간에 DDL이 한 줄이라도 있으면 그 배치는 한 트랜잭션이 아니다. DDL 앞까지의 일은 이미 확정됐다.
STEP 3 — N1을 혼자 잰다 (5분)
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 02_N1_혼자_재기.sql| 블록 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [A1] | rev00 번호 하나 (원본 T_ORD_STLM을 읽기만) | TABLE ACCESS FULL · Buffers 568 | ☐ |
| [A2] | 핫픽스 번호 하나 | INDEX UNIQUE SCAN · 5 | ☐ |
| [A3] | rev01 번호 둘 | 첫 번째 4 (캐시 20개를 채운다) · 두 번째 Buffers 칸 없음 (0) | ☐ |
| [B] | 옆 사람(자율 트랜잭션)이 내 COMMIT 전에 받으면 | rev00 SAME NUMBER · rev01 different | ☐ |
| [C] | 맨 끝 정산을 취소하고 다시 받으면 | rev00 REUSED | ☐ |
| [D] | ROLLBACK한 번호 | rev01 100003 is never stored (gap) · rev00 no gap (same number again) | ☐ |
| [E] | 핫픽스: 내가 쥔 동안 옆 사람 NOWAIT | ORA-00054 | ☐ |
| [F] | 채번 테이블 + 자율 트랜잭션 | 옆 사람 ok 100002 · 내 ROLLBACK 뒤 100001 is never stored (gap) | ☐ |
[A1]의 568은 장애와 상관없는 숫자다. 그래도 T0 제1조에 따라 적는다. 번호 하나를 받으려고 정산 10만 건을 다 읽는다.
STEP 4 — 세 사람이 동시에 번호를 받는다 (10분) ★ 오늘의 본체
맨 위의 "띄우는 법"대로 일꾼 셋을 띄우고 03_N1_지휘자.sql을 돌린다. 약 30초 걸린다.
| 판 | 무엇 | 기대 | ☐ |
|---|---|---|---|
| [R1] rev00 | 트리 · 받은 번호 · 정산 결과 | 들여쓰기 없음 (아무도 안 막힘) · 셋 다 100001 · 100001 × 3 | ☐ |
| [R2] rev00 + PK | 트리 · V$LOCK · 결과 | SCR 둘이 BATCH 밑 · INSERT에서 · TX REQUEST 4 · BATCH COMMIT 뒤 ORA-00001 × 2 | ☐ |
| [R3] 핫픽스 | 트리 · V$LOCK · V$TRANSACTION | SCR 둘이 BATCH 밑 · SELECT ... FOR UPDATE에서 · 셋 다 TM 3 · SCR TX REQUEST 6 · 기다리는 TX의 USN/SLOT/WRAP = BATCH의 V$TRANSACTION | ☐ |
| [R4] rev01 | 받은 번호 · 결과 | 100001 · 100002 · 100003 · 기다림 0 · BATCH ROLLBACK 뒤 100001이 빈다 | ☐ |
| [R5] 데드락 | 로그 · 트리 | SCR1이 약 3초 뒤 ORA-00060 · 그 뒤에도 BATCH는 SCR1에게 막혀 있다 | ☐ |
| [트레이스] | 가장 최근 ORA-00060의 파일 | /opt/oracle/diag/rdbms/free/FREE/trace/FREE_ora_NNN.trc · STEP 7에서 쓰니 적어 둔다 | ☐ |
[R3]에서 눈으로 짚을 것
WHO TYPE LMODE REQUEST BLOCK WHAT TRACE_NAME
BATCH TX 6 0 1 usn 5 / slot 17 / wrap 824 TX-00050011-00000338 ← 쥔 쪽 · 막고 있다
BATCH TM 3 0 0 T_STLM_NUM_C12
SCR1 TX 0 6 0 usn 5 / slot 17 / wrap 824 TX-00050011-00000338 ← 같은 이름을 원한다
SCR1 TM 3 0 0 T_STLM_NUM_C12 ← TM 은 서로 안 막는다(값은 측정 예다. 직접 돌린 USN, SLOT, WRAP은 다르게 나온다)
- ☐ 화면이 원하는 TX의 이름 = 배치가 쥔 TX의 이름인가?
- ☐ 화면은 채번 테이블을 기다리는가, 배치의 트랜잭션을 기다리는가? 한 문장으로 적는다.
- ☐ 화면이 기다린 시간(
[R3-4]의 cs)은 무엇으로 정해졌나? (힌트: 지휘자가 배치에게 COMMIT을 지시한 시각)
[R2]와 [R3]의 REQUEST가 왜 다른가
같은 enq: TX - row lock contention인데 [R2]는 4, [R3]는 6이다. [R3]는 남이 잠근 행 그 자체를 원한다. [R2]는 남이 COMMIT 전에 넣은 같은 키가 결국 들어갈지를 알아야 하므로, 그 트랜잭션이 끝나기를 기다릴 뿐이다. → 2_파고들기 §4 REQUEST 지도
STEP 5 — DDL이 줄 맨 앞에 서면 (10분)
일꾼 셋을 띄우고 05_DDL_의_줄_지휘자.sql을 돌린다. 약 15초 걸린다.
| 판 | 기대 | ☐ |
|---|---|---|
[D1] TRUNCATE · 기본값 | 기다리지 않고 ORA-00054 (0cs) | ☐ |
[D2] TRUNCATE · ddl_lock_timeout = 30 | DDL이 SCR 밑에 TM REQUEST 6 · SCR2의 INSERT가 DDL 밑에 TM REQUEST 3 | ☐ |
[D3] LOCK TABLE ... EXCLUSIVE | D2와 같은 줄 · SCR이 끝나도 SCR2는 계속 기다린다 (DDL이 X를 쥐었다) | ☐ |
[D4] CREATE INDEX (ONLINE 아님) | DDL TM REQUEST 4 · SCR2는 그 뒤 | ☐ |
SCR2는 SCR과는 부딪치지 않는 모드(3)인데도 기다렸다. enqueue는 끼어들기를 허락하지 않는다. 바쁜 테이블에 DDL을 걸며
ddl_lock_timeout을 길게 주면, 기다리는 동안 그 테이블의 DML이 전부 뒤에 선다. 1_사건에서 후보 4번(배치 중간 DDL)을 지운 근거: 장애 때 이벤트가enq: TM이 아니라enq: TX였다.
STEP 6 — (선택) 블록 안의 줄 (10분)
일꾼 셋을 띄우고 04_블록_안의_줄_지휘자.sql을 돌린다. 약 15초 걸린다. 05화(ITL)와 07화(buffer busy waits)가 넘긴 질문이다.
| 판 | 기대 | ☐ |
|---|---|---|
| [I1] 꽉 찬 블록(272행)의 행 1 · 2 · 3을 셋이 고친다 | 셋째만 enq: TX - allocate ITL entry · TX REQUEST 4 | ☐ |
[I2] INITRANS 3으로 다시 만든 뒤 | 아무도 막히지 않는다 | ☐ |
| [X1] 셋이 순번 키로 2만 건씩 | 셋 모두 buffer busy waits (수천 번) · enq: TX - index contention (백 번 안팎) | ☐ |
[X1]의 횟수와 시간은 실행마다 다르다. CPU 2개에 세 명이라 시간은 작다. 이 컨테이너로 잴 수 있는 것은 이벤트가 생긴다는 것까지다. 수백 세션 규모는 별권에서 다룬다.
STEP 7 — 데드락 트레이스를 꺼내 읽는다 (10분) ★
STEP 4의 [R5]가 데드락을 하나 만들었다. 지나간 장애에서 남는 거의 유일한 흔적이 이것이다.
① 파일 이름: STEP 4 끝의 [트레이스] 출력을 보거나, 아래 쿼리로 찾는다.
sql
SELECT TO_CHAR(originating_timestamp, 'HH24:MI:SS') AS alert_time,
REGEXP_SUBSTR(message_text, '/opt/oracle/[^ ]+\.trc') AS trace_file
FROM v$diag_alert_ext WHERE message_text LIKE '%ORA-00060%'
ORDER BY originating_timestamp DESC FETCH FIRST 1 ROWS ONLY;② Deadlock graph만 보기 · 파일째 꺼내기: 트레이스는 컨테이너 안에 있다.
Git Bash: 컨테이너 안의 /opt/...를 윈도 경로로 바꾸지 않게 MSYS_NO_PATHCONV=1 환경 변수를 붙인다.
bash
T=/opt/oracle/diag/rdbms/free/FREE/trace/FREE_ora_NNN.trc # ①의 이름으로 바꾼다
MSYS_NO_PATHCONV=1 docker exec ora-hanorder bash -c "ls -l $T; wc -l $T"
MSYS_NO_PATHCONV=1 docker exec ora-hanorder bash -c "sed -n '/Deadlock graph/,/End of information for waiting sessions/p' $T"
MSYS_NO_PATHCONV=1 docker cp ora-hanorder:$T .PowerShell: 경로를 바꾸지 않으므로 그대로 쓴다.
powershell
$T = "/opt/oracle/diag/rdbms/free/FREE/trace/FREE_ora_NNN.trc"
docker exec ora-hanorder bash -c "sed -n '/Deadlock graph/,/End of information for waiting sessions/p' $T"
docker cp "ora-hanorder:$T" .③ 읽는다(측정 예)
Resource Name process session holds waits serial process session holds waits serial
TX-0002000F-00000350-469D09C8-00000000 87 221 X 46523 84 22 X 9825
TX-0005001F-00000338-469D09C8-00000000 84 22 X 9825 87 221 X 46523| 채울 것 | 나의 트레이스 | 측정 예 |
|---|---|---|
| 파일 크기 · 줄 수 | 1,660,663바이트 · 28,925줄 (거의 전부 프로세스 상태 덤프) | |
| 첫 줄 자원 → USN / SLOT / WRAP | 0002000F → 2 / 15 · 00000350 → 848 | |
| 그 자원을 쥔 세션 · 기다린 세션 | 221(SCR1)이 X로 쥠 · 22(BATCH)가 X로 기다림 | |
| 둘째 줄 | 22가 쥐고 221이 기다린다 → 원 | |
두 세션의 current SQL | SELECT ... FOR UPDATE (N1) · UPDATE ... STLM_SEQ = 50000 | |
세 번째 덩어리 (469D09C8) | PDB의 CON_UID. SELECT con_uid, TO_CHAR(con_uid,'FMXXXXXXXX') FROM v$pdbs;로 맞춰 본다 (설치마다 다르다) |
- ☐ 트레이스의 USN/SLOT/WRAP이 STEP 4 [R5-2]의
V$LOCK(TRACE_NAME열)과 같은가? - ☐ 누가 에러를 받았나? 먼저 기다리기 시작한 쪽인가?
STEP 8 — FK를 건 쪽 (15분) · ora-newshop
전제: 00_자료준비/고객사/L4_ora-newshop/README.md의 2단계(ORA_SQL_TEST + FK 7개)까지 마친 상태. ECOMMERCE 스키마는 쓰지 않는다. 18화에서 만난다.
bash
docker start ora-newshop
N="docker exec -i ora-newshop sqlplus -s ORA_SQL_TEST/1qaz2wsx@localhost/FREEPDB1"
$N < 10_FK_준비.sql
for w in 1 2 3; do $N < 일꾼_$w.sql > /dev/null & done; $N < 11_FK_지휘자.sql; wait
$N < 19_FK_정리.sqlPowerShell에서는 STEP 4의 방법에서 접속 문자열만 ORA_SQL_TEST/1qaz2wsx로 바꾼다(11의 머리말에 있다).
| 단계 | 기대 | ☐ |
|---|---|---|
10 [지도] | FK 7 · 자식 FK 컬럼 무인덱스 3 (M_CUS.RGN_ID · T_ITM_EVL.CUS_ID · T_ORD.CUS_ID) | ☐ |
10 [혼자] | 부모 한 행 DELETE의 logical reads: 인덱스 없음 31 · 있음 8 (컨테이너를 새로 지은 뒤 첫 실행은 17 안팎, 다시 돌리면 8~9) · 주문 있는 고객은 ORA-02292 | ☐ |
11 [K1] 무인덱스 · 주문 둘 진행 중 → 관리자가 고객 삭제 | 관리자 TX REQUEST 4 (주문 화면의 TX) · 자식 T_ORD_C12에 TM 없음 · 그 사이 새 주문 INSERT는 0cs | ☐ |
11 [K1b] 주문 금액만 고치는 중 | 관리자가 그 화면에게도 막힌다 | ☐ |
11 [K2] 관리자가 먼저 지움 | 다른 고객 주문은 통과 · 그 고객의 주문만 관리자에게 막힌다 (TX REQUEST 4) | ☐ |
11 [K3] 인덱스 있음 | 아무도 막히지 않는다 · 관리자가 자식에 TM 3 | ☐ |
11 [K4] FK 없음 | 주인 없는 주문 33건 | ☐ |
19 | C12 객체 0 · 테이블 9 · FK 7 · 원본 주문 3047 | ☐ |
공식문서(
cncpt)는 "자식 테이블에 full table lock"이라고 쓴다. 직접 돌린V$LOCK에는 무엇이 찍혔나? 그대로 적는다. 다른 버전이라면 다르게 찍힐 수 있다. 그래서 재서 적는다. → 2_파고들기 §8
STEP 9 — (선택 · SYSDBA) 시퀀스의 빈 번호, 두 번째 이유 (3분)
bash
MSYS_NO_PATHCONV=1 docker exec -i ora-hanorder sqlplus -s / as sysdba < 91_시퀀스_캐시_SYSDBA.sqlpowershell
Get-Content 91_시퀀스_캐시_SYSDBA.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s / as sysdba기대: 1 · 2 → LAST_NUMBER 21 → 공유 풀을 비운 뒤 21. 3~20의 18개는 영영 쓰이지 않는다. ⚠️ FLUSH SHARED_POOL은 연습 DB에서만 한다. 운영에서는 하지 않는다.
STEP 10 — T4 장애 포스트모템 1호 ★ 오늘의 산출물
양식은 T4_장애_포스트모템.md를 따른다(이 화에서 새로 정했다).
규칙: 사람 이름은 타임라인에만 쓰고, 원인 칸에는 구조만 쓴다.
┌──────────────────────────────────────────────────────────────────────────────┐
│ T4 장애 포스트모템 — 1호 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 1. 사건 요약 : ______________________________________________________________ │
│ 2. 타임라인 : (원인이 들어온 시각부터) │
│ 3. 영향 : ______________________________________________________________ │
│ 4. 탐지 : ______________________________________________________________ │
│ 5. 블로킹 체인 증거 : 트리 · V$LOCK · TX 이름표 · Deadlock graph │
│ 6. 근본 원인 (5 Whys) : │
│ 7. 조치 : 즉시 ___________________ | 항구 ___________________ │
│ 8. 잘 된 점 / 운이 좋았던 점 : │
│ 9. 정합성 : 「정합성 회귀 체크리스트」 A ~ H │
│10. 재발방지 : 조치 · 구분 · 담당 · 기한 · 검증 방법 · 리스크 │
└──────────────────────────────────────────────────────────────────────────────┘▶ 1_사건의 '나'가 제출한 1호 (쓰고 나서 대조)
1. 사건 요약: 5월 20일(월) 10:0x ~ 10:11 정산 등록 화면 전체 무응답. 주간 정산 보정 배치가 번호(N1)를 받은 뒤 트랜잭션을 약 10분 열어 두었고, N1 핫픽스의 채번 테이블 행 락이 그 시간 동안 풀리지 않았다.
2. 타임라인
| 시각 | 일어난 일 | 근거 |
|---|---|---|
| 5/16 (목) 오후 | 정산팀: 정산번호 중복 보고 | 요청 이력 |
| 5/17 (금) 18:40 | 개발팀: N1 핫픽스 반영 (채번 테이블 FOR UPDATE). 버그 수정이라 변경 공지 대상 아님 | 반영 기록 · 운영 소스 diff |
| 5/20 (월) 10:00 | 주간 정산 보정 배치 시작 (매주 · 변경 없음) | 오과장 확인 |
| 10:0x (추정) | 정산 등록 화면 첫 대기. 배치가 번호를 받은 직후 | 블로킹 트리의 대기 SQL |
| 10:04 | 탐지: 박팀장 전화 (사용자 신고) | 통화 |
| 10:06 | 블로킹 트리: 뿌리 1 (보정 배치) · 대기 다수 (정산 등록) · enq: TX - row lock contention | V$SESSION 캡처 |
| 10:07 | 강제 종료 보류 결정: 되감기 비용 · 평소 10분 이내 확인 | 오과장 통화 |
| 10:08 | 정산팀 화면 ORA-00060: 보정 중이던 1건 롤백 | 사용자 캡처 · alert 로그 · 트레이스 |
| 10:11 | 복구: 배치 COMMIT · 트리 비어 있음 | V$SESSION |
3. 영향: 정산 등록 화면 약 10분 무응답(정산팀 전체). 데드락으로 보정 입력 1건 유실(재입력). 데이터 손상 없음. (별건: 핫픽스 이전 MAX+1이 만든 중복 번호. 정산팀이 정정 중)
4. 탐지: 사용자 전화로 알았다. 락 대기를 보는 감시 지표가 없었다.
5. 블로킹 체인 증거: 연습 DB 재현 실습/03 [R3]에서 화면 둘이 배치의 TX(usn 5 / slot 17 / wrap 824)를 REQUEST 6으로 대기 · TM은 셋 다 3. 데드락 트레이스(운영 · 재현 모두): TX-… 두 줄이 서로를 가리킴 · SQL = N1의 SELECT ... FOR UPDATE와 보정 배치의 UPDATE T_ORD_STLM.
6. 근본 원인 (5 Whys)
| 왜 | 답 |
|---|---|
| 왜 화면이 멈췄나 | N1의 SELECT ... FOR UPDATE에서 줄을 섰다 |
| 왜 줄이 10분이었나 | 맨 앞의 주간 보정 배치가 번호를 받은 뒤 트랜잭션을 10분 열어 두었다 |
| 왜 번호 락이 그 10분을 같이 갔나 | 번호 받기가 업무 트랜잭션 안의 행 락이었다. COMMIT할 때까지 놓지 않는다 |
| 왜 그 구조가 들어왔나 | 중복(MAX+1)을 고치는 "버그 수정"이라 동시 시험과 변경 공지 없이 반영됐다 |
왜 MAX+1이었나 | "번호를 받는 곳은 하나뿐"이라는 그때의 전제. 둘이 된 사실이 어디에도 기록되지 않았다 |
7. 조치: 즉시 조치는 대기(배치 강제 종료 보류)와 사용자 공지. 항구 조치는 N1_rev01(시퀀스).
8. 잘 된 점: 뿌리 세션을 찾고도 죽이지 않았다. 되감기 비용을 먼저 물었다. 운이 좋았던 점: 데드락의 희생자가 배치가 아니라 화면의 문장 하나였다(배치였다면 예외 처리가 보정 전체를 롤백). 배치가 평소처럼 10분 안에 끝났다.
9. 정합성: 「정합성 회귀 체크리스트」 (양식)
| # | 항목 | 대상 / 방법 | 결과 | 판정 |
|---|---|---|---|---|
| A | 기준 | 번호 규칙 = 겹치지 않을 것 (빈 번호는 사유 기록으로 허용. 정산팀 · 회계팀 확인) | — | 정함 |
| B | 건수 · 겹침 | 실습/03 [R1] vs [R4], 세 명 동시 | rev00 100001 × 3 → rev01 100001 · 100002 · 100003 (겹침 0) | 통과 |
| C | 잘라 내기 전 전체 | 기존 10만 건과의 충돌: 시퀀스 시작값 = 지금까지의 최댓값 + 1 | rev01 첫 번호 100001 (기존 최댓값 100000) | 통과 |
| D | 조인마다 1 쪽 | 채번에는 조인이 없다 | — | 해당 없음 (조인 없음) |
| E | 경계 데이터 | ROLLBACK (03 [R4] · 02 [D]) · 취소 후 재발급 (02 [C]) · 캐시 소실 (91) | 빈 번호 생김 (100001 · 18개) · 재발급 없음 (rev00은 REUSED) | 통과 (빈 번호는 A의 기준대로 허용) |
| F | 계획 해시와 무관하게 | 번호 값으로 대조 | — | 통과 |
| G | 변환에 기대지 않음 | — | — | 해당 없음 |
| H | 영향 분석 | N1을 부르는 곳 = 정산 등록 화면 · 주간 보정 배치. P1_LOAD는 N1을 부르지 않는다 (자기 카운터) | 2곳 | 통과 |
10. 재발방지
| # | 조치 | 구분 | 담당 | 기한 | 검증 방법 | 리스크 |
|---|---|---|---|---|---|---|
| 1 | N1 → N1_rev01 (시퀀스) | 항구 | 나 · 개발팀 | 5/24 | 실습/03 [R4] 재현 · 반영 뒤 V$ENQUEUE_STATISTICS TX row lock contention 증가분 | 결번: ROLLBACK · 캐시 소실 시 생긴다. 사유를 정산 업무 규정에 적는다 (조민우 · 회계팀 확인) |
| 2 | 주간 보정 배치의 트랜잭션을 짧게: 배치는 N건마다 COMMIT한다 | 항구 | 오과장 | 5/27 | 다음 주 보정 실행 중 블로킹 트리 1분 간격 확인 | |
| 3 | 블로킹 트리를 1분마다 찍고, 30초 넘게 막힌 세션이 있으면 알림 | 탐지 | 인프라팀 | 5/31 | 연습 DB 실습/03으로 알림이 울리는지 | 1분 주기 V$SESSION 조회의 부하 (작다) |
| 4 | 채번 · 락에 닿는 변경은 버그 수정이라도 두 세션 동시 시험을 붙인다 | 절차 | 박팀장 | 5/31 | 다음 반영 건의 시험 기록 | 반영이 하루 늦어질 수 있다 |
| 5 | 신규 플랫폼: 무인덱스 FK 셋에 인덱스 | 예방 | 신규 플랫폼팀 | 개시 전 | 실습/11 [K3] 재현 | 인덱스 셋의 적재 비용 |
STEP 11 — 서술 답안 (400~600자)
정산팀 전체에 보내는 장애 공지를 쓴다. 박팀장이 "정산팀이 이해할 말로" 써 달라고 부탁했다.
4단계 프레임(현상/진단 → 원인 → 개선안 → 근거/효과)은 01화와 같다.
▶ 선배의 첨삭 포인트 (쓰고 나서)
① "DB 락 때문에"로 끝내지 않는다. 락은 원인이 아니라 증상이다. 정산팀이 알아듣게 옮기면 "번호를 받는 창구가 하나였고, 그 창구를 배치가 10분 동안 붙잡고 있었다"가 된다. 발번 대장 비유를 그대로 써도 좋다.
② 누구도 탓하지 않는다. 핫픽스는 지난주 정산팀이 보고한 진짜 버그(번호 중복)를 고친 것이었다. 그 사실을 공지에 적고, 정산팀의 보고가 헛되지 않았다는 것까지 밝힌다.
③ 결번을 미리 말한다. rev01이 반영되면 정산번호에 빈 번호가 생긴다. 정산팀이 "번호가 빠졌어요" 하고 다시 장애 신고를 하지 않도록, 빈 번호가 왜 생기고 규정에 어떻게 적혔는지 한 줄로 적는다.
④ 숫자는 하나만 쓴다. "번호 한 개를 받는 일은 1초도 안 걸리는데 10분을 기다렸다"처럼, 화면을 쓰는 사람이 몸으로 겪은 숫자로 쓴다.
STEP 12 — 파인만 자가설명
절차는 01화와 같다. 1단 백지 설명 → 2단 막힘 태그(최소 1개) → 3단 원천 복귀 → 4단 재설명 순서로 진행한다.
오늘 자주 나오는 태그 세 가지
| 태그 | 전형적인 문장 |
|---|---|
#인과 | "MAX+1은 왜 줄을 안 세우는데 FOR UPDATE는 세우는지, 그리고 왜 기다리는 시간이 '앞사람의 트랜잭션 길이'인지 설명하다가 막혔다" → 2_파고들기.md §2 · §6 |
#용어 | "LMODE와 REQUEST, 그리고 같은 row lock contention인데 REQUEST가 4와 6으로 갈리는 이유를 말로 못 하겠다" → §4 |
#수치 | "트레이스의 TX-0002000F-00000350을 USN · SLOT · WRAP으로 못 풀겠다" → §7 |
4단 되묻기 3문 (오늘 버전)
- 조건 반전: "번호를 받는 곳이 다시 하나뿐이라면(정산 등록 화면이 N1을 안 부른다면), rev00의 세 칸은 어떻게 되나?"
- 반례 요구: "빈 번호가 절대 생기면 안 되는 번호를 하나 든다면, 그때 줄을 짧게 하는 방법은 어떻게 설계하나?"
- 측정 지목: "'무인덱스 FK 때문에 멈췄다'를 증명하려면 장애 중에 무엇을 찍어야 하나? 지나간 뒤라면?"
태그는 _막힘로그.md로 옮긴다.
정리 — 한빛유통을 11화 종료 상태로
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 99_정리.sql기대: c12_left 0 · stlm_rows 100000 · last_no 0 · pkg VALID · big_idx 3 · fk_mt 0
완료 체크리스트
- [ ] STEP 0:
N1_rev00의 본문 · 주석 · LAST_NO 0을 적었다. 주문 시스템 FK 0을 확인했다 - [ ] STEP 1: 보기 전에 예측 6개를 적었다
- [ ] STEP 2: DML의 TM 3 · TX 6, 10만 행에도
V$LOCK2줄, 실패한 DDL도 COMMIT되는 것을 봤다 - [ ] STEP 3: 번호 하나 Buffers 568 · 5 · 0과 겹침 · 다시 씀 · 빈 번호를 봤다
- [ ] STEP 4: 세 사람을 세워 [R1] 겹침 · [R2] REQUEST 4 + ORA-00001 · [R3] TX REQUEST 6 · [R5] 데드락 뒤에도 남은 줄을 봤다
- [ ] STEP 4: [R3]에서 화면이 기다리는 TX의 이름이 배치의
V$TRANSACTION값과 같은지 맞춰 봤다 - [ ] STEP 5: DDL 뒤에 TM 3이 줄을 서는 것을 봤다
- [ ] STEP 7: 트레이스를 꺼내서 Deadlock graph의 TX 이름을 USN, SLOT, WRAP으로 풀었다
- [ ] STEP 8: 신규 플랫폼에서 무인덱스 FK의 부모 DELETE가 무엇을 기다리는지를 내
V$LOCK으로 적었다 - [ ] STEP 10: T4 포스트모템 1호를 썼다(타임라인 · 증거 · 5 Whys · 정합성 체크리스트 · 재발방지)
- [ ] STEP 11: 정산팀 공지를 산문으로 썼다
- [ ] STEP 12: 막힘 태그 최소 1개를
_막힘로그.md에 옮겼다 - [ ]
99_정리.sql·19_FK_정리.sql로 두 컨테이너를 정리했다
완결성 게이트: STEP 4의 [R3]
V$LOCK해석, STEP 7의 트레이스 해독, STEP 10의 5 Whys가 비어 있으면 이 화는 미완성이다.
다음 화로 넘기는 상태
| 무엇 | 값 |
|---|---|
ora-hanorder | 변화 없음: T_ORD_STLM 100,000 · T_STLM_NUM LAST_NO 0 · PKG_ORD_STLM 원본 그대로 · T_ORD_BIG 인덱스 3 · ORD_ST 히스토그램 (11화 상태) · _C12 객체 0 |
ora-newshop | ORA_SQL_TEST 9 테이블 · FK 7 그대로 · _C12 객체 0 · ECOMMERCE는 아직 없다 |
N1 현재 리비전 | rev01: 시퀀스. 리비전 파일로만 있다 (연습 DB의 패키지는 바꾸지 않았다) |
| 새 양식 | T4 장애 포스트모템 (1호 제출) |
| 열어 둔 것 | 포스트모템 재발방지 2번: 주간 보정 배치 N건마다 COMMIT (오과장 · 다음 주 월요일부터) → 13화 · TX 이름표가 가리키는 칸 → 13화 · FK 인덱스 · 맨 오른쪽 블록의 적재 비용 → 14화 |
다음 → 13화 「안전 조치의 대가」