이번 프로젝트는 짧은 일정임에도 불구하고, 상당히 많은 도전과제가 있는 프로젝트였다. 가장 먼저 수행해야될 과제는 현재 40여시간 이상 소요되는 빌링 요금계산과 청구 작업을 30시간 이내로 단축하는것이다. 두번째로는 3억건 이상되는 청구테이블에 대한 파티션작업을 온라인 성능의 저하를 최소화하며, 최소한의 다운타임으로 처리하는것이며, 마지막으로, 주요 일별 작업에 대한 SQL 튜닝을 수행하는 것이다.
이번에 같이 투입된 황종필 과장과 스케줄링 작업한 결과, 현재 33시간 소요되는 요금계산 작업에 대한 튜닝을 가장 먼저 시작하기로 했다. 요금계산 작업은 크게 13개의 Pro*C 프로그램을 구성되어있으며, 세부 단계는 37단계로 구분된다. 이중에서 튜닝 대상이 아닌 17 단계를 제외한 20개의 요금계산 SQL에 대한 튜닝을 착수하였다.
요금계산 작업은 본 빌링 이전에 가빌링을 수차례 수행하게 된다. 지난 몇 개월간의 가빌링/본빌링의 스텝별 수행시간을 분석해본 결과, 아주 이상한 점을 발견할 수 있었다. 수행 시마다 아주 다른 수행 시간을 보인다는 점이다. 어떤 월에는 20시간 이내로 작업이 끝날때도 있고, 어떤 월에는 30시간 이상 작업이 지연되는 경우가 발생한다는 점이다. 즉, 속도에 대한 안정성 부재로 인해 항상 불안해 하면서 요금계산을 수행하는 문제점을 가지고 있다. 따라서 SQL 튜닝과 병행하여, 수행 시마다 수행 속도가 차이가 나는 문제의 원인을 분석하고 문제 원인을 제거하여야만 한다.
작업 시마다 수행시간이 차이가 난다고 할 때, 일반적으로 실행계획이 변경되어서 일것이라는 추측을 하게 된다. 하지만 해당 시스템은 지난 몇 개월간 통계정보를 생성한 이력이 없기 때문에 실행계획 변경에 따른 문제는 아닌것으로 판단된다. 그렇다면 어떤 방식과 전략으로 접근해야 할 것인가? 문제를 접근하기 위한 가장 좋은 방법은 해당 시스템의 구성과 특성을 분석하는 것이다. 따라서 해당 시스템의 기본적인 환경 구성과 특성을 살펴보기로 했다. 그 결과 해당 시스템은 다음과 같은 구성과 특성을 가지고 있다.
1. RAC 2 Node로 구성되어 있으며, INSTANCE_GROUPS, PARALLEL_INSTANCE_GROUP이 설정되어있지 않으며, PARALLEL_ADAPTIVE_MULTI_USER 파라미터는 TRUE이다.
2. 요금계산에서 수행되는 대부분의 SQL들은 PQ를 사용한다.
3. 요금계산이 시작되는 새벽00시부터 09시까지는 시스템의 사용률이 높지 않으나, 상담이 시작되는 09시부터 양 노드의 CPU 사용률은 모두 70~80%가까이 올라간다.
4. 월별 파티션으로 구성되어 있는 테이블도 있으나, 파티션 구성이 되어있지 않은 대용량 테이블이 존재한다.
5. 해당 시스템은 빌링 전용이 아닌, 빌링/Call Center/온라인 배치/온라인 리포트가 수행되는 Hybrid의 성격을 넘어서는 전사 서비스용 시스템이다.
2차례의 가빌링을 통해 아주 결정적인 사실을 확인할 수 있었다. 새벽에는 1시간이면 끝나는 5천여만건의 INSERT 작업(7번 스텝)이 오후 시간대에서는 6시간 넘게 소요된다는 것이다. 대부분의 수행 대기 시간은 “db file sequential read” 이며, file#, block#를 통해 확인해본 결과 INSERT 대상 인덱스임을 확인 할 수 있었다. 즉, 업무시간대에는 상담원들이 수행하는 SQL, 온라인 배치, 온라인 리포트등의 수행으로 인해 버퍼캐시의 액티비티가 높아짐에 따라, INSERT 대상 인덱스에 대한 반복적인 디스크 I/O가 발생하는 것이다. 즉, 새벽 시간대에는 버퍼캐시의 액티비티가 적으므로 한번 로딩된 인덱스 블록의 메모리 히트율이 상당히 높지만, 업무시간대에는 히트율이 너무나 떨어지는 현상이 발생하는 것이다. 아마 이것이 수행 성능 차이의 결정적인 원인이었을 것이다. 즉, 월별 요금계산 시간이 일정하게 시작하지 않음에 따라 7번 스텝이 업무시간을 넘기게 될 경우 성능 저하 문제가 발생하였을 것이다. 이러한 문제를 해결하기 위해서 1차적으로 고려한 것은, 요금계산의 전 스텝에 대한 튜닝을 통해 업무 시작 시간인 9시 이전에 모두 종료한다는 것이다. 하지만, 만일의 사태로 인해 요금계산 시작이 지연될 경우를 고려하여 업무시간에도 새벽 시간대와 유사한 성능을 내도록 할 필요가 있다.
업무시간대에도 새벽시간대와 유사한 성능을 내도록 하려면 어떻게 해야할것인가? 하는 문제에 봉착하게 되었다. 이 문제를 해결하기 위해서는 2가지 문제를 해결해야만 했다.
2. 업무시간대에서 인덱스 블록 캐시 히트율을 유지해야 한다.
1번 문제를 해결하기 위해서는 IBM에서 제공하는 WRM을 통해 프로세스의 우선순위를 유지시키는 것이 가능하다는 것을 확인하였지만, 레퍼런스가 많지 않고 적용이 어렵다는 이유로 일단 이 부분을 제외하기로 했다. 그렇다면 2번 문제는 어떻게 할 것인가? 캐시를 분리하자! 라는 단순하지만 효과적인 아이디어가 생각났다. 즉, 요금계산에 사용되는 22개의 테이블에 대해서는 KEEP 버퍼를 사용하도록 변경하는 것이다. 따라서 현재 14G로 설정된 버퍼캐시중 7G를 KEEP BUFFER로 할당하여 해당 버퍼는 요금계산 전용으로 사용하도록 DBA에게 의뢰하였다. 여기서 중대한 실수가 발생하게 된다.
주말에 DBA가 KEEP 버퍼를 설정한 것을 확인한 후 월요일 오전에 MaxGauge로 DB 모니터링을 해보니, 음. “db file sequential reads”가 지난 주보다 30%이상이나 많이 발생하는 것이다. 9월 말이라는 배치리포트/상담등이 몰리는 이유가 있을 것이다, 아무래도 KEEP 버퍼로 7G씩이나 할당을 한 탓에 요금계산 이외의 테이블/인덱스에서 기존보다 많은 Disk I/O가 발생하는 것으로 파악되었다. 음. 여기서 좀 더 신중하지 못했다는 생각이 뇌리를 스친다. 즉, 요금계산은 2번 노드에서만 수행되도록 설정하였으므로, 1번 노드의 KEEP 버퍼를 구지 7G까지 설정할 필요성이 없었던 것이다. 하지만 업무시간에 이를 변경할 수는 없는일.. 새벽시간까지 기다린 후에 DBA의 허락을 받은 후 1번 노드의 KEEP 버퍼를 1G로 줄이는 작업을 수행하기로 했다.
드디어 대부분의 업무 프로세스들이 종료되고 1번 노드의 DML 작업이 없는 시점에 SQL> alter system set db_keep_cache_size=1g scope=memory; 커맨드를 날린 직후.
MaxGauge에서 “DB DOWN” 이라는 메시지가 계속 뜬다. Alert log를 확인해본 결과, 역시 1번 노드의 DB가 다운되었다. 다행히 RAC인 탓에 1번 노드는 1분 30초만에 자동 리스타트가 되었다. 휴… 역시 RAC가 좋군.. 하면서 안도의 한숨을 내쉰후에, 양 노드의 접속 세션을 확인해보니.. 헉.. 1번 노드에 붙어있던 WAS 커넥션 풀들이 모두 2번 노드에 붙어있는 것이 아닌가!!. 혼자서는 어찌할 방도가 없는일. 일단 DBA에게 급히 연락하여 사고 경위를 설명한 후에 양쪽 AP서버의 WAS를 다시 리스타트함으로써 급한 문제는 해결되었다.
3번째 가빌링에서 또하나의 이상한 현상이 발생하였다. 기존에 16분 소요된 INSERT..SELECT가 2시간 가까이 소요되는 것이다. 후.. 이제 앞으로 본빌링까지 2일밖에 안남았는데 과연 이러한 문제가 모두 해결될까하는 의구심이 증폭되는 순간이다. 이상 현상에 대해 MaxGauge 로그로 면밀히 분석해본 결과, 아무래도 블록 클린아웃에 의한 문제로 결론을 냈다. 즉, 1번 또는 2번 노드에서 해당 테이블/인덱스 블록에 대한 DML이 발생한 후에 SELECT 작업을 수행함에 따라 블록 클린아웃 작업이 대량으로 발생하는 것 같았다. 따라서 이 문제를 해결하기 위해 요금계산적 사전 작업용으로 요금계산에서 사용되는 테이블/인덱스에 대한 FULL SCAN을 수행하는 스크립터를 생성하였다.
이제 3차레의 가빌링 수행 끝에, 본 빌링 요금계산을 수행하는 날이다. 가빌링 시마다 밤새 모니터링을 한 끝에 피곤이 누적되었지만, 오늘이 가장 결정적인 날인탓에, 식탁에 노트북을 올려두고 VPN으로 접속을 시도하였다. 지금 시간 00:01 분!! 요금계산이 수행될 시간임에도 불구하고 MaxGauge에 요금계산용 세션이 눈에 띄지 않는다. 어라..? MSN으로 요금계산 담당 과장님에게 이유를 물어보니, 요금 계산 추가 반영 사항이 있어서 작업중이라고 한다. 음. 이러면 안되는데.. 결국 00시30분에 요금계산 1차 작업이 시작되었다. 튜닝 성과를 기대하며 차분하지만 들뜬 마음으로 모니터링을 시작하였다. INSTANCE_GROUPS, PARALLEL_INSTANCE_GROUP 설정의 효과로 인해 1번 노드에서 수행되는 PQ들은 1번 노드에서만 수행되고 2번 노드를 침범하지 못한다. OK!! 블록 클린아웃 사전 작업의 효과로 인해 5천여만건 INSERT 작업도 원활히 수행. OK!! 음 그런데 또다른 5천여만건의 INSERT 작업이 약간의 지연이 발생한다 (나중에 확인한 일이지만 해당 인덱스는 파티션이 되어있지 않아서 Blevel이 4인 것이 원인이다. 파티션 필요!) 하지만 원만히 수행완료. OK~! 아침 7시. 이제 잘하면 모든 요금 계산이 9시 이전에 끝날 것 같은 기분좋은 상상을 하며, 잠시 눈을 붙이고 떳더니 8시20분. 현재 13번 스텝중에 12번 스텝을 수행중이다. 12번 스텝은 10분정도면 완료되고 14번 스텝은 튜닝을 통해 40분정도면 완료될 것이므로, 기분좋게 샤워를 한 후 9시30분에 고객사 도착~!
기분좋게 출근하여, 황과장에게 “다 끝났지?” 하고 물어본 결과, 아직도 수행중이라고 한다. 헉.. 13번 스텝에서 튜닝 대상이 아니라고 선정한 15분 수행되는 INSERT..SELECT가 1시간 넘게 수행중인것이다. 이럴수가!! 10월초에 특히나 온라인 업무와 SO별 리포트 업무가 오전 9시부터 집중되는 탓에 INSERT 작업이 거의 수행되지 않고 있는 것이다. 아.!!!! 요금계산을 20분만 일찍 시작했더라면, 9시 이전에 모든 요금계산이 끝났을 것이라는 생각을 해보지만, 이미 지나간일. 생각하면 마음만 아프다. 일단 MaxGauge를 통해 작업 시간을 예측해본 결과, 헉. 앞으로 10시간 이상 수행되야한다는 계산이 나왔다. 이럴수가… 하나의 희망은 점심시간에 상담원들의 사용률이 낮아지므로, 그 사이에 해당 작업이 빨리 끝나지 않을까? 하는 생각을 하면서 1시까지 모니터링을 해보지만, 역시 오늘은 하루종일 CPU 사용률이 높다. 음. 일단 마음을 비우고 좀 쉬어야겠다는 판단이 들었다. 황과장에게 “나 사우나 갈 테니 좋은일 있으면 메모보내”라는 말과 함께 졸린 눈을 비비며 사우나로 가서 잠을 청했다. 하지만 생각이 계속 요금계산에 있으니 잠을 자는둥 마는둥 하고 3시에 다시 고객사로 들어갔다. 음. 아직도 돌고 있겠지 하는 생각과 함께.
자리에 들어가면서 황과장에게 “아직도 돌지?” 물어보니 황과장이 빙긋 웃으면서 “부장님 죽으라는 법은 없다봅니다”라고 한다. 뭐지? 어라? 3시경에 모든 작업이 완료된것이다. 황과장말로는 부장님 나가시고 작업이 미친듯이 수행되었다고 한다. 휴…. 본 빌링 전체 작업 시간을 엑셀에 정리해보니 14시간 30분이 소요되었다. 전월 요금계산이 33시간이므로 14시간30분도 상당한 개선효과를 낸것이다.
요금계산이 완료된 2일 후에 청구 작업이 수행된다. 청구작업은 기존에 11시간이 소요된 작업이며, 모든 SQL이 FULL, HASH, PARALLEL 처리를 함에 따라 뾰족한 튜닝 포인트가 도출되지 않는 작업이다. 그리고 청구작업은 가청구 작업이 없음에 따라 튜닝한 내용을 테스트해볼 기회도 없었다는 점이 맘에 걸린다. 드디어 청구작업 시작. 청구팀에서 “어라 왜 이리 느려졌어?” 라는 말들이 들린다. 요금계산을 위해 수행해둔 PQ관련/Keep버퍼 작업들이 청구작업에 약간의 악영향이 미치는것이다. 음. 하지만 앞단의 작업들은 1분, 10분정도 소요되는 작업이므로 느려져도 대세에는 지장이 없다. 청구작업에서 가장 오래 소요되는 작업은 20개의 프로세스가 동시에 SELECT한후에 INSERT를 하는 2시간 30분 소요되는 작업과 1300만건 INSERT..SELECT하는 작업이기 때문이다.
이제 2시간 30분 걸리는 작업이 시작되었다. 헉.. 기존에 2시간 걸리던 SELECT 작업이 PQ 수행으로 인해 5분만에 처리가 끝났다. 그런데!!! 기존에 30분 걸리던 INSERT 작업이 너무 느린것이다. 그 이유는 기존에는 20개의 프로세스가 동시에 INSERT를 수행하였으나, 이번에는 1개의 프로세스만이 INSERT를 하고 있다. MaxGauge를 통해 작업 시간을 예측해보니, 2일 걸린다고 나온다. 헉헉!!
튜닝 적용 시 SELECT 에만 부하가 있다고 판단하여 SELECT만 PQ로 변경하고, INSERT 작업은 싱글 프로세스로 처리하도록 한것이다. 일단 작업 중지!! 다시 데이터를 원복하고 기존의 방식대로 작업을 수행하였다. 작업 완료 시작 01:30분. 이제 마지막 작업이 수행되었다. 기존에 7시간 30분 걸리던 작업이 얼마나 빨라졌을것인가? 믿는 구석이 있다면 테이블을 NOLOGGING으로 변경한것이다. 물론 INSERT는 APPEND, PARALLEL INSERT 이다. 작업 성능을 위해 INDEX UNUSABLE & APPEND INSERT & INDEX REBUILD를 제안했지만 온라인에서 SELECT하는 것이 있다는 이유로 일단 테이블만 NOLOGGIN으로 변경하였다. 모두 퇴근한 후에 황과장과 둘이서 MaxGauge로 모니터링을 하는데, 아무리해도 작업 속도가 안나온다. Undo Record와 Redo Entries로 작업 시간을 산정해보니, 이 역시 7시간 이상 소요된다. 음. 망했군..
청구작업 개선은 물건너 갔다고 생각하고, “다음달의 효과적인 청구작업을 위해서
5년간의 데이터가 저장된 3억건짜리 테이블을 월별 파티션을 하기 위해서는 어떤 방법이 있을까? 고려되어야 하는 사항은
2. 파티션 변경으로 인한 서비스 다운타임이 최소화되여야 한다.
3. 파티션 변경으로 인한 추가적인 디스크 소요가 최소화되여야 한다.
해당 테이블은 6개의 인덱스를 가지고 있으며, 테이블/인덱스 세그먼트 크기의 총합은 200G 정도이다.
단순하게 생각하면, 온라인 성능 저하를 유발하지 않기 위해서는 Global Index를 생성하면 되고, 서비스 다운타임은 PQ 처리로 빠르게 파티션 생성/인덱스 생성을 하고, 추가적인 디스크 소요를 최소화하기 위해서는 작업 순서를 잘 조정하는 정도일 것이다. 이러한 접근은 모든 데이터를 월별 단위의 파티션으로 만들어야 한다는 고정관념에서 비롯된다.
즉, 반드시 월별단위일 필요가 있을까 하는 의문을 제기해봐야한다. 즉, 현재 3억건짜리 테이블을 하나의 파티션으로 구성하고, 200811월부터의 파티션들을 월별 파티션으로 구성하는 것이다. 이럴 경우 EXCHANGE PARTITION INCLUDING INDEXES WITHOUT VALIDATION 명령어를 통해 기존의 3억건짜리 테이블을 특정 파티션으로 변경함과 동시에 INDEX 또한 USABLE 상태로 인덱스 파티션으로 변경이 가능하다. 즉, 파티션 변경으로 인한 서비스 다운타임은 0에 가깝고, 추가적인 디스크도 전혀 필요없으며, 2008년 11월까지는 2개의 인덱스 파티션에 대한 액세스만 필요하므로 온라인 성능 저하도 최소화되는 장점이 있다. 또한 청구 작업의 마지막 스텝 (1300만건 INSERT..SELECT – 튜닝 후 2시간30분소요)을 5분 이내로 획기적으로 단축할 수 있게 된다. 왜냐면 마지막 스텝은 3억건짜리 테이블에 INSERT.. SELECT 하는 작업이기 때문이다. 즉, 이 작업 또한 EXCHANGE PARTITION으로 변경이 가능한 것이다. 만일 이것이 적용되면 기존에 11시간(튜닝후 6시간 50분) 수행되던 청구작업이 1~2시간이내면 끝날수 있게된다.
일요일 저녁.. 파티션 전략 문서를 메일링한 후, 그동안 미뤄두던 UPDATE문을 튜닝하고 있다. 하루종일 머리속에 담아두고 처리 방식을 고민해보았지만, 쉽지 않다. 그러던 중. 참신한 솔루션을 발견했다. 기존의 24시간 이상 수행되는 UPDATE를 5분에 끝낼수 있는……
UPDATE 테이블 A
SET ( 미납월수)
= ( SELECT COUNT(DISTINCT( CASE WHEN 청구월< TO_CHAR (SYSDATE, 'YYYYMM')
THEN 청구월
ELSE NULL END ))
FROM 테이블
WHERE 청구ID = A.청구ID
AND 청구월 >= A.청구월
AND 서비스구분 = A.서비스구분
)
원문을 보면 미납개월수를 구해서 UPDATE하는 문장인데, 처리 조건이 까다롭다. 즉, 청구ID와 서비스 구분이 동일한 고객에 대해서 distinct한 미납개월수를 구하는것이다.
즉 아래와 같은 결과가 나와야 한다.
청구ID 서비스구분 청구월 미납개월수
1000176628 B 200806 1
1000176628 B 200805 2
1000176628 B 200804 3
1000176628 B 200804 3
1000176628 B 200803 4
1000176628 B 200803 4
1000176628 B 200802 5
1000176628 B 200802 5
1000176628 B 200801 6
1000176628 B 200801 6
1000176628 B 200712 7
1000176628 B 200712 7
DITINCT만 없다면 아래와 같은 방식으로 미납월 추출이 가능하다.
SELECT 청구ID,
서비스구분 ,
청구월 ,
upym_mth_cnt,
COUNT(
CASE
WHEN 청구월 < TO_CHAR( SYSDATE , 'YYYYMM' )
THEN 청구월
ELSE NULL
END
) over( PARTITION BY 청구ID, 서비스구분
ORDER BY 청구월 RANGE BETWEEN CURRENT ROW
AND unbounded following) mth_cnt
FROM 테이블
하지만 Disintct가 반드시 필요하며, over() 절안에 order by가 있을 경우 DISTINCT는 사용할 수 없으므로, 위의 SQL로는 요건을 만족하지 못한다. 어떻게 할것인가??
해결방안은 DENSE_RANK를 사용하는 것이다. 즉, 아래의 SQL처럼 수행하면 된다.
즉, 청구ID, 서비스구분별로 윈도우 파티션을 만들고 청구월로 decending order by를 한후 dense_rank()를 구하면 요건과 동일한 set이 만들어지는것이다.
이로써, 마지막 미결과제를 해결했다.
INTO 테이블 b USING (
SELECT /*+ full(a) parallel(a 8) */
DENSE_RANK( ) over( PARTITION BY 청구ID, 서비스 구분 ORDER BY 청구월 DESC ) cnt1,
a.rowid rid
FROM 테이블a
) v ON ( b.rowid=v.rid )
WHEN matched
THEN
UPDATE
SET b.미납개월수 = v.cnt1
이번 컨설팅을 통해 배운점은 단순히 새로운 처리기법이나 적용사례가 아니라, 컨설팅에 임하는자세를 다시금 되새긴점이라고 할 수 있다. 또한 몰입과 성취감을 통해 얻는 것이 얼마나 큰것인지도……
'엑셈 기업문화 > 엑셈 사람들' 카테고리의 다른 글
[이명진]아침공부편: "대용량 데이터베이스를 위한 오라클 SQL 튜닝" 저자특강 세미나 (3) | 2008.11.27 |
---|---|
[신용범]경험은 곧 힘이다 (0) | 2008.11.21 |
[오수영]덧셈뺄셈도 제일 처음엔 어려웠었다 (2) | 2008.11.14 |
[이은경]Function을 사용할 때 이것을 꼭 고려하세요! (0) | 2008.11.07 |
[박준연]“인덱스에 대한 구라” 누가 그랬을까~? (1) | 2008.10.31 |
댓글