Skip to content

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; wait

PowerShell

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.sql

N1은 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 · pkg100000 · 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$TRANSACTIONmatch = 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 ~ E61 · 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]핫픽스: 내가 쥔 동안 옆 사람 NOWAITORA-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$TRANSACTIONSCR 둘이 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 = 30DDL이 SCR 밑에 TM REQUEST 6 · SCR2의 INSERT가 DDL 밑에 TM REQUEST 3☐
[D3] LOCK TABLE ... EXCLUSIVED2와 같은 줄 · 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 / WRAP0002000F → 2 / 15 · 00000350 → 848
그 자원을 쥔 세션 · 기다린 세션221(SCR1)이 X로 쥠 · 22(BATCH)가 X로 기다림
둘째 줄22가 쥐고 221이 기다린다 → 원
두 세션의 current SQLSELECT ... 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_정리.sql

PowerShell에서는 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건☐
19C12 객체 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.sql
powershell
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 contentionV$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만 건과의 충돌: 시퀀스 시작값 = 지금까지의 최댓값 + 1rev01 첫 번호 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. 재발방지

#조치구분담당기한검증 방법리스크
1N1 → 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문 (오늘 버전) ​

  1. 조건 반전: "번호를 받는 곳이 다시 하나뿐이라면(정산 등록 화면이 N1을 안 부른다면), rev00의 세 칸은 어떻게 되나?"
  2. 반례 요구: "빈 번호가 절대 생기면 안 되는 번호를 하나 든다면, 그때 줄을 짧게 하는 방법은 어떻게 설계하나?"
  3. 측정 지목: "'무인덱스 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$LOCK 2줄, 실패한 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-newshopORA_SQL_TEST 9 테이블 · FK 7 그대로 · _C12 객체 0 · ECOMMERCE는 아직 없다
N1 현재 리비전rev01: 시퀀스. 리비전 파일로만 있다 (연습 DB의 패키지는 바꾸지 않았다)
새 양식T4 장애 포스트모템 (1호 제출)
열어 둔 것포스트모템 재발방지 2번: 주간 보정 배치 N건마다 COMMIT (오과장 · 다음 주 월요일부터) → 13화 · TX 이름표가 가리키는 칸 → 13화 · FK 인덱스 · 맨 오른쪽 블록의 적재 비용 → 14화

다음 → 13화 「안전 조치의 대가」