테마
12화 · 아무도 안 건드렸는데 전체가 멈췄다
5월 20일 월요일, 3막의 첫 화다. 오늘은 고칠 조각을 고르기도 전에 장애가 먼저 왔다. 처음으로 장애를 맞고, 처음으로 포스트모템을 쓴다. 그리고 세 번 틀린다.
오전 9시 10분
매주 하던 대로 리비전 폴더부터 열었다.
Q1_rev03.sql Q2_rev01.sql Q3_rev01.sql Q4_rev03.sql여섯 조각 중 넷은 판이 있었고, 둘은 없었다. P1과 N1이다. N1은 01화에 "N1: 1번 고친다 (12화)"라고 적어 두기만 한 조각이다.
선배가 커피를 들고 왔다. "지난주에 내가 마지막에 한 말 기억나?"
"하드 파싱이 줄면 그 뒤에 줄 서 있던 게 앞으로 나온다고요. 이번엔 데이터 쪽에서."
"N1은 그 얘기에 딱 맞는 조각이야. 열어 봐."
PKG_ORD_STLM에서 N1_NEXT_NO를 꺼내 N1_rev00.sql을 만들었다. 본문은 한 줄이었다.
sql
SELECT NVL(MAX(STLM_SEQ), 0) + 1 FROM T_ORD_STLM;그 위의 주석도 한 줄이었다. "시퀀스는 구멍이 생겨서 안 씀 (2019.03 강도현)". 그리고 01화에 내가 적어 둔 메모가 있었다. "채번 테이블 T_STLM_NUM이 있는데 안 쓴다. LAST_NO는 0."
"이건 쉽네요. 시퀀스로 바꾸면 끝이잖아요."
"강도현 씨가 시퀀스를 안 쓴 이유가 바로 위에 있는데?"
"구멍이요. 그런데 구멍이 좀 생기는 게 뭐 어때서…"
"그게 어때서인지는 네가 정하는 게 아니야." 선배가 의자를 끌어왔다. "자, 그럼 오늘은 N1…"
전화가 울렸다.
오전 10시 04분 — 첫 장애
박팀장이었다. 메신저가 아니라 전화로 왔다.
"지금 정산 화면이 안 떠요. 정산팀 전체가요. 등록 버튼을 누르면 모래시계만 돌고요."
"배포가 있었나요?"
"없대요. 오늘 아무도 아무것도 안 건드렸답니다. 그런데 전체가 멈췄어요."
나는 수화기를 든 채 선배를 봤다. 선배는 이미 일어나 있었다.
"첫 장애네." 선배가 말했다. 목소리가 평소와 똑같았다. "순서 지켜. 하나, 지금 누가 누구를 막고 있는지 찍는다. 둘, 그걸 찍기 전에는 아무것도 고치지 않고 아무도 죽이지 않는다. 셋, 시각을 적는다. 분 단위로."
"운영 DB에 들어가요? T0에 운영에서는…"
"실험하지 말라고 했지. 보는 건 실험이 아니야. V$SESSION을 읽기만 해." 선배가 내 키보드를 당겨 쿼리 하나를 쳤다. START WITH blocking_session IS NULL CONNECT BY PRIOR sid = blocking_session. 처음 보는 모양이었다. 막힌 세션을 막은 세션 밑에 들여 써서 나무처럼 보여 주는 쿼리라고 했다.
화면에는 세션 수십 개가 한 줄짜리 뿌리 하나에 매달려 있었다. 아래는 그날 오후 연습 DB에서 같은 장면을 다시 만든 것이다. 줄은 셋으로 줄였고, 테이블은 복사본(_C12)을 썼다. 역할 이름(WHO)은 재현 스크립트가 붙였다.
SID_TREE WHO EVENT SECS BLOCKER WAITING_SQL
--------- ------ ------------------------------- ---- -------- ----------------------------------------------------------------
22 BATCH PL/SQL lock timer 0
38 SCR2 enq: TX - row lock contention 3 22 SELECT LAST_NO + 1 FROM T_STLM_NUM_C12 WHERE NUM_TP = 'STLM' FOR
221 SCR1 enq: TX - row lock contention 3 22 SELECT LAST_NO + 1 FROM T_STLM_NUM_C12 WHERE NUM_TP = 'STLM' FOR막는 쪽(BATCH)의 이벤트가
PL/SQL lock timer인 것은 재현 방식 때문이다. 배치가 일하는 10분을 다음 지시를 기다리며 자는 시간으로 흉내 냈다. 운영에서는 그 자리에 배치의 실제 일(CPU, 읽기)이 찍힌다. 막는 쪽이 무엇을 하든 막히는 쪽의 줄은 똑같다.
뿌리는 하나였다. 프로그램 이름을 보니 주간 정산 보정 배치였다. 매달린 것은 전부 정산 등록 화면이었고, 이벤트는 enq: TX - row lock contention이었다. 기다리는 SQL은 셋 다 같았다.
"뿌리를 죽이면 다 풀리잖아요." 내가 말했다.
"풀리지." 선배가 화면에서 눈을 떼지 않았다. "누가 누구를 막는지는 봤어. 그게 정당한지는 아직 몰라. 그리고 저 배치를 죽이면 지금까지 고친 지난주 정산이 전부 되감겨. 되감는 동안에도 락은 그대로고. 몇 분이 걸릴지 알아?"
몰랐다.
"오과장님한테 전화해. 저 배치가 뭔지, 보통 얼마나 걸리는지."
오과장은 바로 받았다. "월요일 10시 보정이요? 강도현 씨 때부터 매주 도는 거예요. 지난주 정산 금액을 맞추는 거고, 보통 10분 안에 끝나요. 오늘 바꾼 거 없어요."
선배가 정했다. 죽이지 않는다. 박팀장에게는 "원인 세션 확인. 10분 안에 풀릴 것으로 예상. 분석 중"이라고 보낸다. 그리고 나는 시각을 적는다.
10:04 박팀장 전화 — 정산 등록 화면 전체 무응답
10:06 블로킹 트리 — 뿌리 1 (주간 정산 보정 배치) · 대기 다수 (정산 등록 화면) · enq: TX - row lock contention
10:07 오과장 확인 — 매주 도는 배치, 오늘 변경 없음, 평소 10분 이내
10:08 조민우 메신저 — "등록하다가 ORA-00060 이 뜨고 고치던 게 날아갔어요"
10:11 블로킹 트리 비어 있음 — 배치 COMMIT. 화면 정상10시 8분의 메신저를 읽고 선배가 한마디 했다. "그 에러 화면 지우지 말고 캡처해 두시라고 해. 그게 이따 제일 값진 증거가 될 거야."
10시 11분, 트리가 비었다. 화면도 돌아왔다. 멈췄다가 풀렸다. 아무도 아무것도 하지 않았는데.
오전 10시 40분 — 후보 넷
화이트보드에 후보 넷을 적었다. 어젯밤 3막 예습으로 읽은 장애 사례집에서 "아무도 안 건드렸는데 전체가 멈추는" 장애의 단골로 꼽은 것들이었다.
1. 무인덱스 FK
2. MAX+1 채번 (N1)
3. 긴 트랜잭션
4. 배치 중간의 DDL"하나씩 지워." 선배가 말했다. "증거로."
나는 1번에 동그라미를 쳤다. "이게 제일 유력해요. 사례집 첫 장이 이거였어요. 부모 테이블에서 한 줄을 지우면, 자식 쪽 FK 컬럼에 인덱스가 없을 때 자식 테이블 전체가 잠긴다고…"
"한빛유통 주문 시스템에 FK가 몇 개였지?"
손이 멈췄다. 01화에 내가 직접 셌다. 0개였다. 계정 전체에 하나 있는 EMP → DEPT는 SCOTT 견본이다.
"FK가 없으면 그 가설은 설 자리가 없어." 선배가 1번에 줄을 그었다. "대신 지웠다고 잊지는 마. 다음 달에 붙는 신규 이커머스 플랫폼은 FK가 일곱 개야. 오후에 거기서 재 볼 거야."
다음은 4번. 오과장에게 다시 물었더니 보정 배치에 DDL은 없다고 했다. 선배는 아까 화면의 이벤트를 짚었다. "DDL이 줄 맨 앞에 서면 이벤트가 enq: TM으로 찍혀. 테이블 단위 락이니까. 아까 건 TX였어." 4번에도 줄이 그어졌다(그 TM 줄은 오후에 직접 쟀다. 실습/05 참고).
3번. 보정 배치는 지난주 정산 수천 건을 고치고 맨 끝에 한 번 COMMIT한다. 오과장 말로는 "전부 아니면 전무로 짰어요. 반만 고쳐진 정산은 없는 게 낫잖아요." 3번은 남겼다.
그리고 2번. 거기에는 별을 쳤다.
"범인은 이거예요." 나는 01화의 그 한 줄을 가리켰다. "다들 MAX를 읽고 같은 번호를 받으려고 줄을 서는 거죠. 같은 번호를 두 사람이 받으면 안 되니까."
"MAX+1이 줄을 세워?" 선배가 물었다. "무엇으로?"
"…락으로요?"
"SELECT가 락을 걸어?"
대답을 못 했다.
"재 봐."
오전 11시 10분 — 줄은 서지 않았다
동시에 일어나는 일은 혼자서는 잴 수 없다. 선배가 연습 DB에 판을 하나 깔아 줬다. 세션 셋을 뒤에 띄워 두고, 내 세션이 지휘자가 되어 수첩에 지시를 적으면 셋이 그대로 따르는 방식이다. 하나는 배치, 둘은 정산 등록 화면이다. 원본은 건드리지 않는다. 정산 결과 T_ORD_STLM을 복사본으로 떠서 그 위에서만 한다.
첫 판은 강도현 원본 그대로다. 배치가 MAX+1로 번호를 받아 한 줄 넣는다. 아직 COMMIT하지 않는다. 그 사이 화면 둘이 번호를 받고, 넣고, COMMIT한다. 마지막에 배치가 COMMIT한다.
-- 블로킹 트리
SID_TREE WHO EVENT SECS BLOCKER
22 BATCH PL/SQL lock timer 0
38 SCR2 PL/SQL lock timer 0
221 SCR1 PL/SQL lock timer 0
-- 받은 번호와 걸린 시간 (1/100초)
LABEL STEP GOT_NO CS
BATCH TAKE 100001 0
SCR1 TAKE 100001 1
SCR2 TAKE 100001 0
-- 정산 결과
STLM_SEQ CNT
100001 3아무도 기다리지 않았다. 트리에 들여 쓴 줄이 하나도 없었다.
그리고 셋 다 100001을 받았다. 정산 결과에 같은 번호가 세 줄 들어갔다. 에러는 한 번도 나지 않았다.
"배치가 넣은 100001은 COMMIT 전이라 화면들 눈에 안 보여." 선배가 말했다. "그러니 화면들이 읽은 MAX는 여전히 100000이고, 그래서 100001. SELECT는 아무도 막지 않아. 막지 않으니까 겹치는 거야."
별표를 친 2번을 봤다. 범인이 아니었다. 아니, 범인이긴 한데 다른 사건의 범인이었다. 줄을 세우는 쪽이 아니라 번호를 조용히 겹치게 하는 쪽이었다.
혼자서 하나 더 해 봤다. 100001을 넣고 COMMIT한 뒤, 그 정산을 취소해서 지우고 COMMIT하고, 다시 번호를 받았다.
rev00 : first 100001 (cancelled) / next 100001 -> REUSED취소된 정산의 번호가 다음 정산에 다시 붙었다. 장부로 치면 같은 번호로 두 번 발행된 전표다.
"그럼 아까 그 줄은 뭐였어요?"
"아까 운영 트리에서 기다리던 SQL을 다시 읽어 봐."
SELECT LAST_NO + 1 FROM T_STLM_NUM WHERE NUM_TP = 'STLM' FOR UPDATEMAX가 아니라 T_STLM_NUM 테이블이었다. 강도현이 만들어 두고 쓰지 않은, LAST_NO가 0인 그 채번 테이블이다. 그리고 FOR UPDATE가 붙어 있었다.
10시 6분에 분명히 이 줄을 봤다. 보고도 읽지 않았다. 뿌리 세션과 이벤트 이름만 눈에 들어왔고, 그다음엔 머릿속에 이미 있던 범인을 찾고 있었다.
"이 SQL은 강도현 씨 코드에 없어요. 아침에 rev00을 만들 때 분명히…"
"그럼 매주 하던 걸 하자." 선배가 말했다. "diff."
운영의 PKG_ORD_STLM 소스를 받아 아침에 만든 N1_rev00.sql과 나란히 놓았다. N1_NEXT_NO가 달랐다.
sql
-- 운영 · 5월 17일 (금) 18:40 반영 · "정산번호 중복 버그 수정"
FUNCTION N1_NEXT_NO RETURN NUMBER IS
v NUMBER;
BEGIN
SELECT LAST_NO + 1 INTO v FROM T_STLM_NUM WHERE NUM_TP = 'STLM' FOR UPDATE;
UPDATE T_STLM_NUM SET LAST_NO = v WHERE NUM_TP = 'STLM';
RETURN v;
END;개발팀의 핫픽스였다. 반영 기록을 거슬러 올라가니 지난주 목요일에 조민우 씨가 보낸 요청이 있었다. "정산번호가 같은 게 두 건씩 찍힌 게 몇 쌍 나왔습니다." 방금 내가 연습 DB에서 만든 바로 그 겹침이다. 개발팀은 강도현이 만들어 둔 채번 테이블을 찾아 LAST_NO를 현재 최댓값으로 맞추고, FOR UPDATE로 고쳤다. 혼자 돌려 본 테스트는 통과했다. 금요일 저녁에 반영했고, 버그 수정이라 변경 공지 대상도 아니었다.
"아무도 안 건드린 게 아니었네요."
"건드린 걸 아무도 장애와 이어 붙이지 않은 거야." 선배가 말했다. "그리고 개발팀은 틀린 걸 고친 게 아니야. 중복은 진짜 버그였어. 네가 방금 쟀잖아."
오전 11시 40분 — 문서 발번 대장
"따라와." 선배가 일어섰다.
선배가 향한 곳은 총무팀이었다. 11화의 결재 서식함이 있는 복합기 옆에서 책상 하나를 더 지나면 된다. 총무팀 데스크 위에는 늘 두꺼운 바인더가 하나 있다. 문서 발번 대장이다. 회사 밖으로 나가는 공문과 계약서에는 전부 번호가 붙는데, 그 번호는 이 대장에 손으로 적어서 받는다. 마지막 줄 다음 번호를 쓰고, 문서 제목을 쓰고, 이름을 쓴다.
그런데 데스크에 대장이 없었다. 앞에 두 사람이 서 있었다.
"김대리님이 계약서 서른 건 한꺼번에 번호 따신다고 대장 들고 회의실 들어가셨어요." 총무팀 막내가 말했다. "계약서 검토 다 끝나면 가지고 나오신대요."
나는 협력사 공문 한 장에 번호를 받아야 했다. 선배가 받아 오라고 시킨 일이었다. 그래서 세 번째로 줄을 섰다.
7분을 서 있었다. 김대리가 대장을 들고 나왔고, 앞의 두 사람이 차례로 번호를 적었고, 내 차례가 왔다. 번호를 적는 데는 10초가 걸렸다.
"어땠어?" 자리로 돌아오며 선배가 물었다.
"10초짜리 일에 7분을 기다렸어요."
"그게 핫픽스야." 선배가 말했다. "번호를 받으려면 대장을 가져가야 해. 그리고 대장은 그 사람이 하던 일을 다 끝낼 때까지 돌아오지 않아. 김대리님한테 번호 서른 개 적는 데 걸리는 시간은 몇 분 안 돼. 그런데 계약서를 다 읽을 때까지 들고 있었지. 그동안 너는 서 있었고."
"그럼 강도현 씨 방식은요?"
선배가 총무팀 앞 발송함을 가리켰다. 오늘 나갈 공문이 쌓이는 트레이였다. "대장 대신 발송함 맨 위 공문의 번호를 보고 하나 더해서 쓰는 거야. 아무도 안 기다려. 그런데 너하고 옆 사람이 동시에 발송함을 보면?"
"같은 번호를 쓰겠네요. 아까 100001 세 장처럼."
"그리고 맨 위 공문이 반송돼서 빠지면, 그 번호를 다음 사람이 또 써." 선배가 걸으며 말했다. "1층 은행 지점 번호표 기계는 어때? 뽑으면 끝이야. 줄도 안 서고, 겹치지도 않아. 그런데 뽑고 그냥 나가 버리면?"
"그 번호는… 아무도 안 부르겠죠. 비는 번호요."
"그게 시퀀스야." 선배가 엘리베이터 버튼을 눌렀다. "겹치지 않을 것, 비지 않을 것, 기다리지 않을 것. 셋 다 주는 방법은 없어. 대장은 앞의 둘을 주고 셋째를 뺏고, 발송함은 셋째를 주고 첫째를 뺏고, 번호표는 첫째와 셋째를 주고 둘째를 뺏어."
"그럼 뭘 골라요?"
"뭘 포기해도 되는지는 번호를 쓰는 사람이 알아. 정산번호면 조민우 씨고." 엘리베이터 문이 열렸다. "하나만 더. 아까 대장 앞에 서 있을 때 앞사람 순서를 건너뛸 수 있었어?"
"아뇨. 왔던 순서대로요."
"그게 enqueue야. 이름 그대로 큐야. 줄을 세우고 순서를 지켜. 07화의 탕비실 집게 기억나? 집게는 줄을 안 세웠어. 못 잡으면 손을 뻗어 다시 잡아 보고, 또 잡아 보고."
"래치요. spin."
"그래. 래치는 아주 짧은 일을 위해 손을 계속 뻗는 거고, enqueue는 길어질 수 있는 일을 위해 이름을 적고 줄을 서는 거야. 오늘 정산팀이 선 건 줄이야."
오후 1시 00분 — 줄은 트랜잭션에 선다
점심을 먹고 연습 DB에 핫픽스를 그대로 깔았다. 배치가 번호를 받고, COMMIT은 맨 끝에 한다. 그 사이 화면 둘이 번호를 받으려 한다.
이번엔 내가 먼저 예상을 적었다. "채번 테이블 전체가 잠긴다. V$LOCK에 TM 락이 크게 걸려 있을 것." 테이블 하나에 한 줄뿐인 표니까, 그 표 전체가 잠긴 거라고 생각했다.
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
SCR2 TX 0 6 0 usn 5 / slot 17 / wrap 824 TX-00050011-00000338
SCR2 TM 3 0 0 T_STLM_NUM_C12TM은 셋 다 3이었다. 배치도, 기다리는 화면 둘도 마찬가지였다. TM을 기다리는 세션은 없었다(REQUEST 0).
기다리는 건 TX였다. 화면 둘이 REQUEST 6으로 원하는 TX는 usn 5 / slot 17 / wrap 824로, 배치가 LMODE 6으로 쥔 바로 그 TX였다. BLOCK 1은 배치 줄에만 있었다.
"TM 3은 이 테이블을 고치는 중이니 테이블째 바꾸지는 마라는 정도의 표시야. 셋 다 고치는 중이라도 서로 안 막아." 선배가 말했다. "그런데 TX는 테이블 이름이 아니라 트랜잭션 이름이야. 화면들은 채번 테이블을 기다리는 게 아니라, 배치라는 트랜잭션이 끝나기를 기다리고 있어."
"트랜잭션 이름이 usn, slot, wrap이에요?"
"V$LOCK의 ID1을 65,536으로 나눈 몫이 USN, 나머지가 SLOT, ID2가 WRAP이야. 트랜잭션이 시작되면 언두 세그먼트 하나의 머리에 있는 칸 하나를 받아. USN은 그 세그먼트 번호, SLOT은 칸 번호, WRAP은 그 칸을 몇 번째 쓰는지야. 그 셋이 트랜잭션의 이름표야." 선배가 배치 세션의 V$TRANSACTION을 찍었다.
WHO USN SLOT WRAP
BATCH 5 17 824화면들이 기다리는 이름표와 배치의 이름표가 같았다.
"그 칸 안에 뭐가 적혀 있는지, 언두가 어떻게 생겼는지는 다음 주에 해." 선배가 말했다. "오늘은 하나만 가져가. 줄은 행이나 테이블에 서는 게 아니라 앞사람의 트랜잭션에 선다. 그러니까 기다리는 시간은 앞사람이 무엇을 잠갔느냐가 아니라 언제 COMMIT하느냐로 정해져."
화면 둘이 기다린 시간은 316cs, 321cs였다. 지휘자인 내가 배치에게 COMMIT을 지시할 때까지 기다린 것이다. 운영에서는 그 자리에 보정 배치의 10분이 있었다. 번호를 받는 일 자체는 연습 DB에서 5블록짜리 일이었다.
배치가 COMMIT하자 화면 둘이 100002, 100003을 받았다. 겹치지 않았다. 정합성은 완벽했다. 대신 모두가 줄을 섰다.
강도현의 주석을 다시 봤다. 시퀀스는 구멍이 생겨서 안 씀. 그리고 LAST_NO가 0인 채로 남은 채번 테이블이 있었다. 형상관리에서 패키지 이력을 거슬러 올라갔다.
2019-03-14 hd.kang
N1 채번: 채번 테이블(T_STLM_NUM) 방식 시험 — 보정 배치가 도는 동안 정산 등록 화면이 대기함. 철회.
번호는 주간 보정 배치에서만 매긴다 (등록 화면은 번호 없이 저장, 배치가 매김).
MAX+1 로 변경. 매기는 곳이 하나뿐이라 겹칠 일이 없음.
시퀀스는 쓰지 않음 — 결번이 생긴다 (2018 감사 지적).나는 그 커밋을 한참 봤다.
강도현은 오늘 이 장애를 이미 한 번 겪었다. 채번 테이블로 시험하다 정산 등록이 줄을 서는 걸 보고, 철회하고, MAX+1로 바꿨다. 그때 번호를 받는 곳은 보정 배치 하나뿐이었다. 받는 사람이 하나면 MAX+1은 겹치지도, 기다리지도, 비지도 않는다. 그때는 옳았다. LAST_NO가 0인 이유도 그거였다. 만들어 놓고 시험하다 접은 것이다.
"그 뒤에 정산 등록 화면이 N1을 부르기 시작했어요. 그게 언제인지, 누가 그랬는지는 아무 데도 안 적혀 있고요."
"받는 사람이 둘이 된 순간 강도현 씨의 근거는 유효기간이 끝났어." 선배가 말했다. "그런데 그걸 아무도 몰랐지. 그리고 개발팀은 그 사람이 왜 채번 테이블을 접었는지 모르고 다시 폈고."
⛳ 오늘은 여기까지(중단 지점 1) 여기까지가 오늘의 절반이다. 누가 누구를 막았는지 찍었고, 후보 넷 중 둘을 증거로 지웠다. MAX+1은 막지 않고 겹친다는 것과, 핫픽스의 줄이 앞사람의 트랜잭션에 선다는 것도 쟀다. 이어서 읽으면 조민우 씨의 ORA-00060이 남긴 트레이스를 읽고, 그 줄이 정당했는지를 따진다.
오후 2시 00분 — 장애가 남긴 유일한 흔적
조민우 씨가 10시 8분에 찍은 캡처를 노트북에 띄워 들고 왔다.
ORA-00060: deadlock detected while waiting for resource"지난주 정산 한 건 금액이 틀려서 고치고 있었어요. 금액 고치고, 보정 번호 받으려고 등록을 눌렀는데 이게 뜨면서 화면이 처음으로 돌아갔어요. 고친 금액도 날아갔고요."
"고마워요. 이게 오늘 제일 중요한 증거예요." 선배가 말했다. 그러고는 나를 보며 말을 이었다. "지나간 장애는 V$에 안 남아. 10시 11분에 다 풀렸으니까. 그런데 데드락은 흔적을 남겨. alert 로그에 한 줄, 그리고 트레이스 파일 하나."
인프라팀에 그 시각의 운영 트레이스를 요청해 두고, 기다리는 동안 연습 DB에서 같은 장면을 만들었다. 핫픽스를 깐 상태에서 조민우 씨 화면이 정산 50000번의 금액을 고친다(COMMIT 전). 배치가 번호를 받는다. 조민우 씨 화면이 번호를 받으려다 배치를 기다린다. 배치는 지난주 정산을 고쳐 가다 50000번에 닿아 조민우 씨를 기다린다.
서로가 서로를 기다리는 원이 생겼다. 3초쯤 지나자 한쪽이 에러를 받았다.
LABEL STEP CS MSG
SCR1 EXEC 0 UPDATE T_ORD_STLM_C12 SET STLM_AMT = STLM_AMT WHERE STLM_SEQ = 50000
BATCH TAKE 0 N1_HOTFIX_C12
SCR1 ERR 328 ORA-00060: deadlock detected while waiting for resource에러를 받은 쪽은 먼저 기다리기 시작한 조민우 씨 자리의 화면이었다. 오라클은 기다린 지 3초가 지난 세션이 원을 발견하면 그 세션의 문장 하나를 되돌리고 에러를 준다.
"이제 풀렸겠네요."
"트리 다시 찍어 봐."
SID_TREE WHO EVENT SECS BLOCKER WAITING_SQL
38 SCR2 PL/SQL lock timer 0
221 SCR1 PL/SQL lock timer 0
22 BATCH enq: TX - row lock contention 3 221 UPDATE T_ORD_STLM_C12 SET STLM_AMT = ... = 50000배치가 여전히 막혀 있었다. 이번에는 조민우 씨 화면이 막고 있었다.
"ORA-00060은 문장 하나만 되돌려." 선배가 말했다. "번호를 받으려던 그 SELECT ... FOR UPDATE만. 그 앞에 고친 50000번 행의 락은 트랜잭션이 쥐고 있어. 화면이 에러를 받고 ROLLBACK해야 비로소 풀려. 운영에서는 화면 프레임워크가 에러를 받자마자 ROLLBACK했으니까 배치가 계속 갔고, 조민우 씨의 금액은 날아갔지."
"데드락이 줄을 풀어 준 게 아니네요."
"원 하나를 끊어 줬을 뿐이야. 줄은 배치가 COMMIT한 10시 11분에 풀렸어."
연습 DB의 트레이스를 꺼냈다. 파일은 1.6MB에 2만 9천 줄 가까이 됐다. 프로세스 상태를 통째로 떠 놓은 덤프다. 필요한 건 그중 스무 줄, Deadlock graph였다.
Deadlock graph:
------------Blocker(s)----------- ------------Waiter(s)------------
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
Session 221:
current SQL: SELECT LAST_NO + 1 FROM T_STLM_NUM_C12 WHERE NUM_TP = 'STLM' FOR UPDATE
Session 22:
current SQL: UPDATE T_ORD_STLM_C12 SET STLM_AMT = STLM_AMT WHERE STLM_SEQ = 50000TX-0002000F-00000350은 아까 배운 이름표를 16진수로 적었을 뿐이다. 0002000F의 앞 넷이 USN 2, 뒤 넷이 SLOT 15, 00000350이 WRAP 848이다. 몇 분 전 V$LOCK에서 본 조민우 씨 화면(SCR1)의 TX(usn 2 / slot 15 / wrap 848)와 같았다. 첫 줄은 "221(SCR1)이 X로 쥐고, 22(BATCH)가 X로 기다린다", 둘째 줄은 그 반대였다. 원이 그대로 적혀 있었다. 뒤에 붙은 469D09C8은 이 PDB의 CON_UID를 16진수로 쓴 것이었다. 맞춰 보니 같았다.
인프라팀이 보내 준 운영 트레이스도 같은 모양이었다. 줄이 둘, SQL이 둘이었다. 하나는 N1의 FOR UPDATE 문장, 다른 하나는 보정 배치의 UPDATE T_ORD_STLM 문장이었다.
"이 두 줄이 오늘 사건 전체야." 선배가 말했다. "번호 락이 배치의 트랜잭션에 묶여 있었고, 그 트랜잭션이 지난주 정산 수천 건을 잡은 채로 10분을 갔어. 운이 좋았던 건, 원을 끊을 때 희생된 게 조민우 씨의 문장이었다는 거야. 배치 쪽 문장이 희생됐으면?"
오과장의 배치 코드를 열었다. 예외 처리부가 ROLLBACK하고 끝나게 돼 있었다. "…보정 전체가 되감기고, 오늘 밤에 다시 돌려야 했겠네요."
"그것도 포스트모템에 적어. 운이 좋았던 점 칸에."
오후 3시 00분 — 세 칸
이제 고칠 차례였다. 아침의 세 마디를 표로 만들었다. 겹치지 않을 것, 비지 않을 것, 기다리지 않을 것.
"핫픽스를 되돌리면 겹치고, 그대로 두면 멈춰요." 내가 말했다. "그럼 MAX+1으로 돌아가되 정산번호에 PK를 걸면 되잖아요. 겹치는 건 DB가 막아 주고, MAX+1은 줄을 안 세우니까."
"재 봐."
복사본에 PK를 걸고 첫 판을 다시 했다. 배치가 100001을 넣고 아직 COMMIT하지 않은 상태에서, 화면 둘이 MAX+1로 100001을 받아 넣으려 한다.
SID_TREE WHO EVENT SECS BLOCKER WAITING_SQL
22 BATCH PL/SQL lock timer 0
38 SCR2 enq: TX - row lock contention 2 22 INSERT INTO T_ORD_STLM_PK_C12 ...
221 SCR1 enq: TX - row lock contention 2 22 INSERT INTO T_ORD_STLM_PK_C12 ...
WHO TYPE LMODE REQUEST BLOCK WHAT
BATCH TX 6 0 1 usn 7 / slot 30 / wrap 751
SCR1 TX 0 4 0 usn 7 / slot 30 / wrap 751
SCR2 TX 0 4 0 usn 7 / slot 30 / wrap 751줄을 섰다. 이번에는 INSERT에서였다. 같은 키를 넣으려는데 앞사람이 넣은 같은 키가 아직 COMMIT 전이니, 그 트랜잭션이 COMMIT할지 ROLLBACK할지 알 때까지 기다린다. REQUEST는 행 락을 기다릴 때의 6이 아니라 4였다.
그리고 배치가 COMMIT하자 이렇게 됐다.
SCR2 ERR 221 ORA-00001: unique constraint (SQLP.PK_ORD_STLM_PK_C12) violated ...
SCR1 ERR 221 ORA-00001: unique constraint (SQLP.PK_ORD_STLM_PK_C12) violated ...2.2초를 기다린 끝에 둘 다 에러를 받았다.
"정합성은 지켰는데… 줄도 서고 에러도 나요."
"셋 중 하나를 지키려고 둘을 내준 거야." 선배가 말했다. "PK는 마지막 방어선으로는 맞아. 번호를 주는 방법은 아니고."
그다음은 시퀀스였다. 같은 순서로 하되, 배치는 마지막에 ROLLBACK했다. 보정을 취소한 셈이다.
LABEL STEP GOT_NO CS
BATCH TAKE 100001 0
SCR2 TAKE 100002 0
SCR1 TAKE 100003 0
STLM_SEQ CNT
100002 1
100003 1기다리지도 않았고 겹치지도 않았다. 그리고 100001이 비었다. 배치가 취소한 번호는 돌아오지 않는다. 연습 DB의 SYS로 하나 더 쟀다. 시퀀스는 번호 20개를 메모리에 미리 꺼내 둔다. 두 개를 쓰고 공유 풀을 비우자 다음 번호가 3이 아니라 21이었다. 18개가 그냥 사라졌다.
혼자 잰 것까지 모아 표 하나로 만들었다.
| 겹침 | 빈 번호 | 기다림 | 번호 하나 Buffers | |
|---|---|---|---|---|
rev00 MAX+1 | 있다 (100001 × 3) · 취소된 번호를 다시 준다 | 없다 | 없다 | 568 |
| rev00 + PK | 없다 | 없다 | 있다(그리고 줄 끝에서 ORA-00001) | 568 |
핫픽스 FOR UPDATE | 없다 | 없다 | 있다(앞사람이 COMMIT할 때까지) | 5 |
| 채번 테이블 + 자율 트랜잭션 | 없다 | 있다 (ROLLBACK해도 LAST_NO는 올라가 있다) | 번호 받는 순간뿐 | — |
| 시퀀스 | 없다 | 있다 (ROLLBACK · 캐시 소실) | 없다 | 0 (20번에 한 번 4) |
568이라는 숫자에서 잠깐 멈췄다. 강도현 원본은 번호 하나를 받으려고 정산 10만 건을 처음부터 끝까지 읽고 있었다. 장애와는 상관없는 숫자였지만, T0 제1조대로라면 이것도 적어야 했다.
"셋 다 되는 게 없네요."
"없다고 했잖아. 그러니까 고르는 거야." 선배가 말했다. "핫픽스의 줄은 빈 번호가 절대 없어야 한다는 요구가 있을 때만 정당해. 그 요구가 진짜인지는?"
"강도현 씨가 감사 지적이라고…"
"원문 봤어?"
조민우 씨에게 전화해 강도현의 커밋에 적힌 바로 그 감사 지적서를 찾아 달라고 했다. 30분 뒤 사진 한 장이 왔다.
지적 사항 7. 정산번호 결번 발생 시 결번 사유를 확인할 수 있는 기록이 없음나는 그 문장을 두 번 읽었다. 결번 금지가 아니었다. 결번의 사유를 댈 수 있으라는 것이었다.
조민우 씨가 회계팀과 통화하고 다시 전화를 줬다. "정산번호는 순서에 의미가 없는 번호라서요. 취소·장애로 쓰이지 않은 번호는 결번으로 남는다를 정산 업무 규정에 한 줄 넣으면 된대요. 감사팀도 그거면 된다고 했고요. 그런데 번호가 겹치는 건 안 돼요. 그건 지난주에 진짜로 사고였어요."
겹침은 안 된다. 빈 번호는 사유를 적으면 된다. 기다림은 어떤가? 오늘 아침이 답이었다.
"그럼 빈 번호 하나를 막으려고 정산팀 전체를 배치 뒤에 세울 이유는 없어요."
"그게 오늘 질문의 답이야." 선배가 말했다. "누가 누구를 막았는지는 V$를 보면 나와. 그 줄이 정당한지는 업무가 정해. 그걸 묻지 않고 락만 들여다보면, 강도현 씨처럼 근거는 있는데 유효기간이 지난 결정을 또 하게 돼."
N1_rev01을 썼다.
sql
-- N1_rev01 : 번호 받기를 업무 트랜잭션에서 떼어 낸다
FUNCTION N1_NEXT_NO RETURN NUMBER IS
BEGIN
RETURN SQ_STLM_SEQ.NEXTVAL; -- 시작값 = 지금까지 나간 번호 중 최댓값 + 1 · CACHE 20
END;번호 하나에 Buffers 568 → 0. 세 명이 동시에 받을 때 기다림 0. 겹침 0. 빈 번호는 생기지만, 그 사유가 취소나 장애라는 것을 정산 업무 규정에 적는다. 리비전 파일의 「채택하지 않은 대안」 칸에는 핫픽스, PK, 자율 트랜잭션, 채번을 트랜잭션 맨 끝으로 미루기, 이렇게 넷을 적었다.
⛳ 오늘은 여기까지(중단 지점 2)
N1_rev01이 나왔다. 번호 하나 Buffers 568 → 0 · 동시에 받을 때 기다림 0 · 겹침 0 · 빈 번호는 규정에. 오늘 몫은 여기까지만 해도 다 했다. 이어지는 오후에는 아침에 지운 가설 하나를 FK가 일곱 개인 시스템에서 다시 꺼내 문서와 다른 숫자를 보고, 저녁에는 첫 포스트모템을 쓴다.
오후 4시 30분 — FK를 건 쪽
"아침에 지운 1번." 선배가 말했다. "신규 이커머스 플랫폼 컨테이너, ora-newshop 열어."
『SQL BOOSTER』 원본 스키마 ORA_SQL_TEST에는 테이블 아홉 개에 FK가 일곱 개 있다. 먼저 자식 쪽 FK 컬럼이 어느 인덱스의 맨 앞 컬럼인지 셌다.
CHILD FK FK_COL PARENT COVERING_INDEX
M_CUS FK_CUS_1 RGN_ID M_RGN
T_ITM_EVL FK_T_ITM_EVL_1 CUS_ID M_CUS
T_ORD FK_T_ORD_1 CUS_ID M_CUS
C_BAS_CD FK_C_BAS_CD_DV_1 BAS_CD_DV C_BAS_CD_DV PK_C_BAS_CD
M_ITM_PRC_HIS FK_M_ITM_PRC_HIS ITM_ID M_ITM PK_M_ITM_PRC_HIS
T_ITM_EVL FK_T_ITM_EVL_2 ITM_ID M_ITM PK_T_ITM_EVL
T_ORD_DET FK_T_ORD_DET_1 ORD_SEQ T_ORD PK_T_ORD_DET일곱 중 셋이 비어 있었다. 주문(T_ORD)의 고객번호도 그중 하나였다. 넷은 PK의 맨 앞 컬럼이 FK 컬럼이라 저절로 덮여 있었다.
고객과 주문을 복사본으로 떠서 FK를 똑같이 걸었다. 그리고 판을 깔았다. 주문 화면 둘이 주문을 넣는 중(COMMIT 전)이고, 관리자가 주문이 하나도 없는 탈퇴 고객 한 명을 지운다.
예상은 자신 있게 적었다. 사례집 첫 장 그대로였다. "관리자의 DELETE가 자식 테이블 T_ORD에 TM 모드 4(S)를 요청하고, 주문 화면들의 TM 3과 부딪쳐 기다린다. 그 뒤로 들어오는 주문 INSERT는 전부 관리자 뒤에 줄을 선다. 전체가 멈춘다."
SID_TREE WHO EVENT SECS BLOCKER WAITING_SQL
54 ORD2 PL/SQL lock timer 0
51 ADMIN enq: TX - row lock contention 1 54 DELETE FROM M_CUS_C12 WHERE CUS_ID = 'CUS_C12'
178 ORD1 PL/SQL lock timer 0
WHO TYPE LMODE REQUEST BLOCK WHAT
ADMIN TX 6 0 0 usn 10 / slot 10 / wrap 627
ADMIN TX 0 4 0 usn 3 / slot 21 / wrap 598
ADMIN TM 3 0 0 M_CUS_C12
ORD2 TX 6 0 1 usn 3 / slot 21 / wrap 598관리자는 기다렸다. 그런데 TM이 아니었다. 관리자가 자식 테이블 T_ORD_C12에 쥐거나 요청한 TM은 한 줄도 없었다. 기다린 것은 주문 화면(ORD2)의 TX였고, REQUEST 4였다. 그 사이 ORD1이 새 주문을 하나 더 넣었는데 0cs. 막히지 않았다.
ORD1이 COMMIT했을 때도 관리자는 여전히 ORD2를 기다렸고, ORD2가 COMMIT하자 그제야 끝났다(관리자의 DELETE 260cs). 주문을 넣는 게 아니라 다른 고객의 주문 금액 하나를 고치는 중이어도 마찬가지였다. 관리자는 그 화면이 끝날 때까지 기다렸다.
"사례집이랑 달라요."
"사례집만이 아니야. 공식문서에도 자식 테이블에 테이블 락을 건다고 써 있어." 선배는 놀라지 않았다. "그래서 재고 쓰는 거야. 적어도 이 버전에서는 무인덱스 FK의 부모를 지우면, 자식 테이블을 통째로 훑으면서 진행 중인 자식 트랜잭션을 만나는 대로 하나씩 기다려. 자식 테이블에 테이블 락을 거는 게 아니라. 그래서 주문 화면은 안 멈췄고."
"그럼 무인덱스 FK는 이제 괜찮은 거예요?"
"관리자가 기다리는 동안 무엇을 쥐고 있는지 봐."
관리자가 고객을 먼저 지워 두고(COMMIT 전) 주문이 들어오게 했다. 다른 고객의 주문은 그냥 지나갔다. 그런데 바로 그 고객의 주문을 넣으려던 화면은 관리자에게 막혔다(TX REQUEST 4). 이건 FK의 정상 동작이다. 지워지는 중인 부모를 가리키는 자식은 결과를 기다려야 한다. 하지만 관리자가 한 명이 아니라 고객 수천 명을 정리하는 배치라면 어떨까? 자식 트랜잭션을 하나씩 기다리는 동안 이미 지운 고객 수천 명의 행 락을 쥐고 있게 된다.
마지막으로 자식 FK 컬럼에 인덱스를 만들고 같은 판을 다시 했다.
SID_TREE WHO EVENT SECS BLOCKER
51 ADMIN PL/SQL lock timer 0
54 ORD2 PL/SQL lock timer 0
178 ORD1 PL/SQL lock timer 0
WHO TYPE LMODE REQUEST BLOCK WHAT
ADMIN TM 3 0 0 M_CUS_C12
ADMIN TM 3 0 0 T_ORD_C12아무도 기다리지 않았다. 관리자는 인덱스에서 그 고객을 찾아보고 없음을 확인하고 끝났다. 혼자 재 보니 부모 한 줄을 지우는 데 읽는 블록이 31에서 8로 줄었다. 재미있게도 인덱스가 있을 때 관리자는 자식 테이블에 TM 3을 쥐었다. 서로 막지 않는 모드다.
"한빛유통 쪽은요? FK가 없으니까 이 장애는 없는 거죠."
"그 대신 이거." 선배가 같은 복사본에서 FK를 잠시 끄고, 주문이 있는 고객 한 명을 지웠다. 에러도 대기도 없이 지워졌고, 주인 없는 주문 33건이 남았다. FK가 켜져 있을 때는 ORA-02292로 막혔던 그 삭제다. "FK를 안 걸면 줄은 안 서. 대신 고아가 조용히 생겨. 01화에 적은 그거야. 공짜는 없어."
신규 플랫폼 팀에 보낼 메모를 썼다. FK 일곱 중 셋의 자식 컬럼에 인덱스가 없다. 이 버전에서 부모 삭제는 자식 테이블의 진행 중 트랜잭션을 전부 기다린다(실측). 인덱스 셋을 권고한다. 대가는 인덱스 셋의 적재 비용.
오후 5시 30분 — 10만 행을 잠가도
포스트모템을 쓰기 전에 걸리는 게 하나 있었다.
"보정 배치가 지난주 정산 수천 건을 잡고 있었잖아요. 락이 그렇게 많으면 메모리가 모자라서 테이블 락으로 바뀌지 않아요? SQL Server에는 그런 게 있다던데요. 락 에스컬레이션."
"재 봐."
복사본에서 한 행을 고치고 V$LOCK을 셌다. TM 3 하나, TX 6 하나로 2줄이었다. 같은 트랜잭션에서 나머지 10만 행을 전부 고치고 다시 셌다.
2줄. 똑같았다.
"오라클은 행 락을 락 관리자의 메모리에 하나씩 적지 않아. 그 행이 사는 블록 안에 적어." 선배가 말했다. "행 머리에 이 행은 몇 번 칸의 트랜잭션이 잡고 있음이라는 표시가 있고, 블록 머리의 ITL 칸에 그 트랜잭션의 이름표가 있어. 10만 행을 잡아도 늘어나는 건 블록 안의 표시뿐이고, 락 관리자가 아는 건 트랜잭션 하나야. 그러니까 올릴 이유가 없어. 에스컬레이션이 없어."
"05화에서 블록 머리에 ITL 칸이 두 개 있다고 했던 거요. 그 칸이 모자라면요?"
05화가 넘긴 질문이었다. 꽉 찬 블록(PCTFREE 0) 하나에 들어 있는 272행 중 1, 2, 3번 행을 세 사람이 동시에 고쳤다. 행이 다르니 행 락끼리는 부딪치지 않는다.
SID_TREE WHO EVENT SECS BLOCKER WAITING_SQL
52 TX1 PL/SQL lock timer 0
216 TX2 PL/SQL lock timer 0
220 TX3 enq: TX - allocate ITL entry 0 216 UPDATE T_ITL_C12 SET V = V WHERE ID = 3세 번째 사람이 막혔다. 행이 아니라 칸을 기다렸다(enq: TX - allocate ITL entry, REQUEST 4). 블록에 칸이 둘뿐이고 새 칸을 만들 빈자리가 없었다. 테이블을 INITRANS 3으로 다시 만들자 아무도 막히지 않았다. 대가는 블록마다 칸 하나만큼 데이터를 덜 담는다는 것이다.
내친김에 07화가 넘긴 것도 쟀다. 세 사람이 순번 키로 2만 건씩 동시에 넣으면, 인덱스의 맨 오른쪽 리프 블록 하나에 셋이 몰린다. 한 사람당 buffer busy waits가 4천~6천 번, 블록이 나뉘는 동안 기다리는 enq: TX - index contention이 80~110번이었다. 시간으로는 한 사람당 20cs를 넘지 않았다. CPU 두 개짜리 연습 DB에 세 명이니 그 정도다. 수백 세션이 몰리는 운영 규모의 경합은 이 컨테이너로 만들 수 없었다. 그건 별권 몫으로 적어 두었다.
"오늘 본 줄을 세 칸으로 정리해 봐." 선배가 말했다.
이렇게 썼다. 07화의 래치는 집게다. 아주 짧은 일이고, 줄 없이 손을 계속 뻗는다. 11화의 뮤텍스는 서식함 칸 하나에 붙은 작은 자물쇠다. 역시 짧고 줄이 없다. 오늘의 enqueue는 발번 대장 앞의 줄이다. 길어질 수 있는 일이라 이름을 적고 순서대로 기다린다. TX도 TM도 이 줄이다.
오후 6시 10분 — 포스트모템 1호
선배가 빈 양식을 하나 열어 줬다. T4 장애 포스트모템.
"규칙은 하나야. 사람 이름은 타임라인에만 쓰고, 원인 칸에는 구조만 써. '개발팀이 핫픽스를 했다'가 원인이면 다음엔 아무도 핫픽스를 안 해. 그럼 지난주 중복은 누가 고쳐?"
타임라인은 아침에 적은 그대로 옮기고, 그 앞에 두 줄을 더했다. 목요일 조민우 씨 중복 보고 · 금요일 18:40 핫픽스 반영. 탐지 칸에는 "사용자 전화(10:04). 락 대기를 보는 감시 지표가 없었다"라고 썼다.
근본 원인을 찾으려고 '왜'를 다섯 번 물었다.
왜 화면이 멈췄나 N1 의 SELECT ... FOR UPDATE 에서 줄을 섰다 (enq: TX - row lock contention)
왜 줄이 10분이었나 맨 앞의 주간 보정 배치가 번호를 받은 뒤 트랜잭션을 10분 동안 열어 두었다
왜 번호 락이 그 10분을 번호 받기가 업무 트랜잭션 안의 행 락이었다 — COMMIT 할 때까지 놓지 않는다
같이 갔나
왜 그 구조가 들어왔나 중복(MAX+1)을 고치는 "버그 수정" 이라 동시 시험과 변경 공지 없이 반영됐다
왜 MAX+1 이었나 "번호를 받는 곳은 하나뿐" 이라는 그때의 전제 — 둘이 된 사실이 어디에도 기록되지 않았다그다음은 재발방지 표였다. 칸은 조치, 구분, 담당, 기한, 검증 방법, 리스크, 이렇게 여섯이었다.
1번은 쉬웠다. N1 → rev01 (시퀀스). 리스크 칸에는 "결번: ROLLBACK · 캐시 소실 시 생긴다. 사유를 정산 업무 규정에 적는다 (조민우 · 회계팀 확인)"이라고 썼다. 02화부터 이 칸을 비웠다가 반려당한 게 한두 번이 아니었다.
2번을 쓰려다 멈췄다. 번호 락은 rev01로 사라진다. 그런데 보정 배치는 여전히 지난주 정산 수천 건을 10분 동안 쥔다. 조민우 씨의 데드락은 번호가 아니라 그 행들 위에서 났다. 그 10분이 짧았다면 원이 생길 틈도 없었을 것이다. 그러다 P1이 생각났다. 야간 정산 적재 배치다. 01화에서 본 강도현의 주석이 있었다. 1,000건마다 COMMIT하는 줄 옆에 "트랜잭션은 짧게 (2020.07 강도현)"라고 적혀 있었다. 강도현은 이미 거기까지 가 있었다.
나는 이렇게 썼다.
2. 주간 보정 배치 — 트랜잭션을 짧게: 배치는 N 건마다 COMMIT 한다
구분: 항구조치 담당: 오과장 기한: 5/27 검증: 다음 주 보정 실행 중 블로킹 트리 1분 간격 확인
리스크:리스크 칸에서 커서가 깜빡였다. 짧게 하는 데 무슨 리스크가 있지? 락을 덜 쥐는 것뿐인데. 나는 3번으로 넘어갔다.
3번은 탐지다. 블로킹 트리를 1분마다 찍고, 30초 넘게 막힌 세션이 있으면 알림. 4번은 변경관리다. 채번 · 락에 닿는 변경은 버그 수정이라도 두 세션 동시 시험을 붙인다. 5번은 신규 플랫폼의 FK 인덱스 셋이다.
「운이 좋았던 점」 칸에는 둘을 적었다. 데드락의 희생자가 배치가 아니라 화면의 문장 하나였다. 배치가 평소처럼 10분 안에 끝났다. 「잘 된 점」에는 하나를 적었다. 뿌리 세션을 찾고도 죽이지 않았다. 되감기 비용을 먼저 물었다. 그건 내가 한 일이 아니라 선배가 한 일이었지만, 포스트모템은 사람이 아니라 동작을 적는 거라고 했으니까.
정합성 칸은 08화부터 늘 붙이는 체크리스트로 채웠다. 겹침 3 → 0, 다시 씀 → 없음, 시작값은 지금까지의 최댓값 + 1, N1을 부르는 곳은 정산 등록 화면과 보정 배치 둘뿐이다(P1은 N1을 부르지 않는다).
선배가 의자를 끌어와 처음부터 읽었다. 타임라인에서 10:06을 짚었다. "트리를 찍은 시각이 증거의 시각이야. 잘 적었어." 5 Whys의 마지막 줄에서 한참 멈췄다. "이 줄이 제일 좋다. 사람이 아니라 기록되지 않은 전제로 끝났어." 재발방지 표로 내려와 1번의 리스크 칸을 짚었다.
"이번엔 리스크 칸 채웠네."
"02화부터 이 칸 때문에 반려를 몇 번이나 당했는데요."
선배가 웃었다. "좋아. 오늘은 첫 장애였어. 숫자로 말했고, 아무도 탓하지 않았고, 아무도 죽이지 않았어." 그리고 맨 아래 서명란에 이름을 적었다.
포스트모템을 박팀장과 오과장, 개발팀, 정산팀에 보냈다. 오과장의 답이 제일 먼저 왔다.
오과장 18:52
2번 좋네요. 저도 그 배치 10분씩 잡고 있는 게 늘 마음에 걸렸어요.
P1 처럼 1,000건 단위로 끊겠습니다. 다음 주 월요일 보정부터 적용할게요.오후 7시 00분
퇴근하며 리비전 폴더를 다시 열었다. 판이 하나 늘어 있었다.
N1_rev00.sql N1_rev01.sql아침에는 시퀀스로 바꾸면 끝이라고 했다. 결론은 같았다. 그런데 아침의 나는 강도현이 시퀀스를 피한 이유를 몰랐고, 그 이유의 원문을 몰랐고, 그 원문이 무엇을 요구하는지 몰랐다. 저녁의 rev01에는 그 세 가지가 주석으로 붙어 있었다.
같은 결론이어도 근거가 다르면 다른 판이다. 18화에서 rev00과 rev01을 나란히 놓으면, 바뀐 건 SQL 한 줄이 아니라 그 위의 주석일 것이다.
오늘 남은 것
| 무엇 | 값 |
|---|---|
N1_rev01 | 시퀀스. 번호 하나 Buffers 568 → 0 (20번에 한 번 4) · 세 명 동시 기다림 0 · 겹침 3 → 0 · 빈 번호는 생긴다 → 사유를 업무 규정에 |
| T4 장애 포스트모템 ★ 새 양식 · 1호 | 타임라인 · 블로킹 체인 증거 · 5 Whys(구조로) · 즉시/항구 분리 · 운이 좋았던 점 · 재발방지(리스크 칸) |
| 핫픽스 (리비전 아님) | 정합성 완벽 · 기다림 = 앞사람의 트랜잭션 길이 (연습 DB 316~321cs · 운영 약 10분) |
| 무인덱스 FK (신규 플랫폼) | 일곱 중 셋. 이 버전에서는 자식 TM 락이 아니라 진행 중 자식 트랜잭션을 TX REQUEST 4로 하나씩 기다린다. 인덱스가 있으면 아무도 기다리지 않는다 |
| 원인 후보 넷 | 무인덱스 FK: FK 0개라 해당 없음 · 배치 중간 DDL: 해당 없음 (이벤트가 TX) · 긴 트랜잭션: 증폭 · N1: 핫픽스가 원인 · rev00은 겹침의 원인 |
오늘 배운 한 문장
줄은 행이 아니라 앞사람의 트랜잭션에 선다. 그래서 기다리는 시간은 무엇을 잠갔느냐가 아니라 언제 COMMIT하느냐로 정해진다. 누가 누구를 막는지는
V$를 보면 알 수 있다. 그 줄이 정당한지는 업무가 정한다.
다음으로 넘기는 것
- TX의 이름표가 가리키는 그 칸: 언두 세그먼트 헤더의 트랜잭션 테이블, 블록의 ITL과 행의 lock byte → 13화
- 재발방지 2번: 주간 보정 배치를 N건마다 COMMIT → 오과장이 다음 주 월요일부터 적용한다 → 13화
- 신규 플랫폼 FK 인덱스 셋의 적재 비용 · 인덱스 맨 오른쪽 블록 경합을 나누는 방법 → 14화
- 수백 세션이 한 줄에 서는 규모의 경합: Free의 CPU 2개로는 만들 수 없다 → 별권
다음 →
3_실습과_기록.md에서 세 사람을 동시에 세워 직접 줄을 만들고, 데드락 트레이스를 꺼내 읽고, 포스트모템 1호를 쓴다. 개념이 막히면 →2_파고들기.md