#title Disk 분산 [[TableOfContents]] 일단 디스크 분산에 대한 이해를 하려면 디스크와 메모리에 대한 기본적인 지식과 SQL문의 처리과정을 알아야 한다. 먼저 디스크에 대한 이야기를 한 후 구체적으로 디스크 분산에 대한 이야기를 해보자. 첫 부분은 기초적인 부분이므로 만화책 읽듯이 한번 읽어보거나 초심자가 아니면 그냥 건너뛰어 SQL문의 처리과정을 봐도 무관할 것이라는 생각이 든다. ==== 성능에 관한 물리적 팩터 ==== 디스크의 구조를 이야기 하기 전에 성능에 관한 물리적 요소 중 어떤 것이 가장 중요할 지를 DB의 관점에서 생각해 보도록 하자. DB에서 가장 중요한 물리적 요소는 무엇일까? 5가지로 말 할 수 있다. CPU, 실제 메모리, 디스크, 네트웍 트래픽, SQL문.. DB의 관점에서는 이렇게 5가지가 가장 중요하다. 물론 컴퓨터로 하는 것은 SQL문만 빼면 다 중요한 요소임에 틀림없다. 그러나 DBMS는 서버사이드에서 가장 중요한 소프트웨어임에 틀림없기 때문에 거의 대부분의 서비스에 관련된 성능은 DB를 어떻게 다루느냐에 따라서 프로젝트 성공여부가 결정된다. 프로그램을 짠다라고 하는 것은 무엇일까? 다음과 같은 것이 프로그램인가? {{{#!geshi cpp #include using namespace std; int main() { int nPoint; cout << "점수를 입력하시오 : " cin >> nPoint; if(nPoint <= 100 && nPoint >=90) cout << "A"; else if(nPoint < 90 && nPoint >= 80) cout << "B"; else if(nPoint < 80 && nPoint >= 70) cout << "C"; else if(nPoint < 70 && nPoint >= 60) cout << "D"; else cout << "F"; cin >> nPoint; return 0; } }}} 이것은 어떤 일에 대한 순서를 기록하는 것이 아니라 ‘데이터를 어떻게 처리하여 사용자에게 정보를 줄 것인가?’에 초점이 맞춰진다. 여기서 중요한 물리적 요소가 CPU, 메모리이다. 물론 서버/클라이언트 환경에서 네트웍 트래픽에 관한 고려도 해야 한다. 현재는 어느 것도 포기해서는 안 된다. 조금 벗어나는 이야기이지만 위 프로그램은 잘 짜여진 프로그램인가? 필자는 아니라고 본다. 이유는 A ~ F까지 정확이 6등분된 데이터가 현실적으로 존재하는지가 의심스럽기 때문이다. 대부분의 경우 C또는 B,D가 더 많으므로 데이터의 양에 따라서 if ~ else의 순서가 바뀌어야 한다. 만약 C가 가장 많다면 가장 먼저 C에 대한 처리가 이루어져야 할 것이다. 예전에 친구가 이런 질문을 한 적이 있다. ‘이런~ 데이터 몇 십 건 집어 넣는데 모래시계냐?’ 라고 말이다. 이런 경우 사용자에게 서비스 중이라면 Lock의 문제 아니면 CPU Busy이다. 친구의 경우는 CPU가 100%로 사용되고 있어서 문제가 되었다. 소프트웨어가 최적화 되었는데도 이런 현상이 자주 발생한다면 당연히 CPU를 한 개 더 추가해야 한다. 만약 CPU가 80%이상의 사용률을 보인다고 해서 하드웨어의 증설을 성급히 고려한다면 그것도 문제다. 왜냐하면 사실 CPU가 100% 사용된다고 해서 문제되는 것이 아니기 때문이다. 더 큰 문제는 큐(Queue)에 작업이 쌓일 때가 문제가 되는 것이다. 큐에 작업이 밀리면서 계속적으로 CPU 사용률이 많아지는 것이 문제이지 CPU가 100% 사용률을 보인다고 해서 문제가 되는 것은 아니다. 그러므로 개발자들은 가장 CPU를 덜 써먹을 수 있게 개발을 해야 한다. 위의 경우처럼 가장 적은 처리로 같은 출력을 낼 수 있는 것이 좋은 것이다. 대부분의 CPU사용은 알고리즘의 복잡도에 대한 차이이다. DBMS의 성능은 감히 I/O를 얼마나 효율적으로 하느냐와 메모리를 얼마나 효율적으로 사용하느냐에 따라 틀려진다. 물론 메모리의 사용도 I/O와 밀접한 관련을 맺고 있다. 이제 I/O에 관심을 집중하고 I/O를 중심으로 내용을 전개해 나가보도록 하자. ==== 디스크 구조 ==== 디스크는 아래의 그림과 같이 실린더들과 디스크 암등으로 구성되어 있다. 디스크는 회전을 하면서 데이터를 메모리로 읽어 들인다. 뾰족한 것이 디스크암(전기자)이고 끝에 붙은 것은 헤드이다. 헤드는 실린더에서 회전을 하여 디스크가 있는 곳으로 이동하게 되어 데이터를 읽는 것이다. 그러므로 데이터가 디스크에 여기저기 산재해 있으면 그만큼 데이터의 탐색시간은 길어진다. 윈도우 조각모음을 생각해보면 왜 순차적으로 디스크에 기록되어 있는 데이터를 읽기 좋은지 알 수 있을 것이다. 그러므로 디스크의 탐색시간은 헤드가 얼마만큼 움직이느냐에 따라서 틀려지는 것이다. attachment:disk02.jpg 우리가 5400rpm과 7200rpm(rpm : Revolution Per Minute)과 같이 이야기 하는 것은 분당 회전 수를 의미하는 것이다. 7200rpm의 경우 평균 탐색시간은 4ms이다. 인간 세상에는 꽤 짧은 시간이지만 컴퓨터의 세계에서는 무진장 긴 시간이다. ==== 순차적인 데이터 ==== 위에서 디스크의 구조를 살펴본 결과 디스크의 평균 탐색시간과 데이터가 디스크에 어떻게(순차적으로 기록되어 있는지) 저장되어 있는가 디스크라는 하드웨어의 성능을 좌우한다. 평균 탐색 시간은 디스크 제조회사의 문제지만 데이터를 순차적으로 디스크에 기록하는 것은 사용자에 달려 있는 문제이다. 그러므로 어떻게 하면 데이터를 순차적으로 기록할 수 있는지를 고려해봐야 한다. DBMS 제품들은 이런 디스크의 특성을 고려하여 순차적인 데이터의 기록을 하려고 애를 쓴다. 오라클에서는 클러스터라는 것이 있고, MSSQL Server는 클러스터드 인덱스가 있다. MSSQL Server의 경우는 PK에 클러스터드 인덱스를 생성하면 데이터가 정렬된 상태로 들어가게 된다. 그러나 순차적인 데이터와 비순차적인 데이터를 서로 다른 테이블에 저장한다고 해도 별 소용이 없다. 순차 + 비순차 이면 당연히 비순차적인 데이터가 되기 때문이다. 그러므로 순차적인 데이터를 저장할 때는 순차적인 데이터만 기록하는 디스크를 따로 두어야 효과를 볼 수 있다. ==== 메모리와 디스크의 검색 시간차 ==== 현재의 컴퓨터들은 폰 노이만의 “프로그램 내장 방식”이라는 형태를 벗어나지 못하고 있다. 프로그램이 내장되어 있다는 소리는 우리가 소프트웨어를 컴퓨터에 설치하여 저장하고 있다는 것이 아니라 디스크나 다른 물리적인 저장 장치를 RAM에 올리고, CPU는 메모리에 접근해서 어떤 짓거리를 한다는 의미이다. 그러므로 디스크등의 저장장치(앞으로 이 문서에서는 디스크만을 이야기 하겠다.)에서 메모리(RAM으로 한정짓는다.)로 처리하고자 하는 데이터를 전송하는 것이 상당히 큰일 중에 하나이다. 물론 컴퓨터에서 입/출력과 연산, CPU도 중요하지만 DB서버에서는 사용자가 직접 DB서버의 모니터로 와서 확인할 일은 없을 테니 요청한 정보만 전송해주면 그만이다. 메인 메모리는 디스크와 같은 저장 장치에 비해서 너무나도 적은 양이다. 그러므로 운영체제나 DBMS 제품들은 버퍼캐시라는 것을 두어서 자주 쓰이는 데이터를 메모리에 저장하고 있다가 사용자의 요청이 들어오면 디스크 I/O(이후로는 물리적 읽기라고 하겠다.)를 하지 않고, 메모리 I/O(이후로는 논리적 읽기라고 하겠다.)를 하여 성능을 향상 시킨다. 성능이 향상된다? 당연히 여러분이 알고 있는 데로 물리적 읽기보다는 논리적 읽기가 훨씬 빠르기 때문이다. 물리적 읽기를 한다는 것은 이미 논리적 읽기까지 포함되어 있는 일이다. 그러므로 더 많은 일을 하게 되는 것이다. 그러나 더 큰 문제는 바로 디스크와 메모리의 입/출력의 성능차이다. 메모리가 얼마나 빠를까? 일반적으로 알려진 바로는 4 * 10의 6승배 만큼(실제 디스크와 메모리의 성능차이는 수 천배 정도다)이다. 즉, 4백만배더 빠른 것이다. 보통 7200rpm 디스크의 경우는 평균 탐색 시간이 4ms이다. 만약 메모리에서 처리하면 될 것을 디스크로 내려가서 처리한다고 생각을 해보자. 1초도 안돼서 끝날 일을 훨씬 많은 시간을 들여서 행해야 결과를 받아 볼 수 있을 것이다. 그러므로 우리는 버퍼캐쉬의 히트율을 높이는 것이 중요한 것임을 인식해야 한다. 또한 디스크 경합을 최대한 줄여주는 것도 상당히 중요하다. 왜냐하면 디스크는 하나의 명령당 하나의 작업만을 수행하기 때문이다. 그러므로 여러 작업이 복합적으로 일어난다면 명령어 들은 디스크를 사용하기 위해서 쟁탈전을 벌일 것이다. 그러므로 디스크의 적절한 분산이 필요한 것이다. 이제 SQL문의 처리과정을 보고, 어떤 일이 복합적으로 일어나는 지를 알아보고, 이에 따른 적절한 디스크 분산에 대한 이야기와 메모리 사용에 대한 이야기를 간단하게 해보도록 하자. ==== SQL문의 처리과정 ==== 현재 존재하는 DBMS의 입/출력 구조는 비슷하다. 모두 최대한 메모리를 효율적으로 사용하고, 디스크의 입/출력을 줄이려는 비슷한 메커니즘을 가지고 있다. 어떤 정보를 요청하는 쪽은 클라이언트이다. 클라이언트에서는 서버의 프로시저를 호출하거나 SQL을 서버에게 던진다. 이것은 사용자가 서버에게 ‘나는 거시기 거시기 정보가 필요하니까 줘봐봐’라고 요청을 하는 것이다. 그러면 서버는 의무적으로 그 요청에 응답을 한다. 설령 클라이언트가 원하는 정보이든 쓰레기 같은 정보이든 아무것도 없던지 간에 아무튼 응답을 한다. 이렇게 보면 간단하지만 이제 여러분은 마우스 한번 클릭에 서버에서 어떤 일이 일어나는 지를 살펴보고, 디스크 분산에 대한 이유를 생각해 보아야 한다. 상황을 설정해 보자. attachment:disk03.jpg 위 그림은 클라이언트가 갱신을 서버에게 요청한 것이다. 사용자는 empno가 1453인 사원을 부서번호가 40번인 부서에 배치한 것이다. 아마도 [수정]이란 버튼을 눌렀을 것이다. 자~ UPDATE문이 네트웍을 통해서 서버로 날아간다. 그 전에 아마도 클라이언트는 이미 연결되어서 시스템의 자원을 할당 받았을 것이다. 그럼 서버에는 먼저 이 SQL문이 전에 사용한 적이 있는지 없는지를 체크할 것이다. 이미 사용된 적이 있다면 파싱(Parsing: 구문분석, 의미분석)을 하지 않고, 이미 파싱된 문장을 사용할 것이다. 그러면 부수적으로 사용권한등의 문제도 해결된다. MSSQL Server의 경우는 자동 매개 변수화라는 것이 있어서 Where절의 조건에 명시되는 값을 매개변수화 하여 메모리에 있는 파싱된 문장을 재사용한다. 오라클은 필자가 알기로는 그렇지는 않고, 무조건 같아야 한다. 물론 대/소문자도 정확히 구별되어야 한다. 아무튼 메모리에서 재사용된다면 그리 큰 문제가 없다. 다만 그 잘못된 실행계획을 재사용하는 것이 문제가 된다. 만약 메모리에 이미 사용되어지거나 매개변수화된 문장이 없다면 어떻게 될까? 메모리를 뒤지고 없으니 파싱도 하고, 그런 테이블이 있는지등도 확인하고, 사용권한도 체크하고, 디스크로부터 데이터를 메모리로 퍼올리는 짓을 하게 된다. 비록 이렇게 간단하게 이야기 했지만 실제로는 더 복잡한 처리과정을 가지고 있다. 실행계획을 세우는 일만해도 시스템의 통계정보와 분포도, 비용 산정을 하고, 다양한 실행계획 중 가장 비용이 적게 드는 실행계획을 선택하여 디스크에서 메모리로 퍼 올린다. 만약 인덱스가 있다면 인덱스를 이용해서 데이터를 퍼 올릴 것이다. 물론 비용기반의 옵티마이저에서 말이다. 또한 갱신 이전에 값과 갱신 후의 값을 메모리에서 가지고 있거나 디스크에 기록해야 일관성이 유지될 수 있다. 로그도 기록한다. 여기서 만약 ‘왜 디스크에서 메모리로 퍼올리지?’ 라고 의문을 가지는 사람은 반성해야 한다. CPU는 디스크에 직접 접근하지 못한다. 오로지 메모리로 접근하기 때문에 메모리가 많을수록 유리하다고 하는 것이다. 그럼 ‘디스크로 접근하게 만들면 되지’ 라고 생각하는 사람도 있을 것이다. 만약 디스크의 입/출력 속도가 메모리 만큼 된다면 모를까 아마도 디스크에 CPU가 접근하는 방식이라면 느려서 울화통 터져서 정신병원에 갈지도 모른다. 왜냐하면 디스크에서의 입/출력 속도는 메모리보다 일반적으로 4 * 10의 6승배 만큼 느리기 때문이다. 실로 엄청난 속도차이다. 그러므로 여러분은 최대한 메모리를 활용할 수 있는 방안을 마련해야 한다. 그래서 메모리 기반의 DBMS에 대한 연구도 많이 진행 되었으며, 실제로 상용화된 DBMS도 있다. 그러나 필자가 보기엔 아직까지는 메모리가 비싸기 때문에 별로라고 생각이 든다. 실제로 접해보지는 않았기 때문에 그냥 생각이지 실제로 그렇다는 것은 아니니 오해는 말기를… ==== 물리적 설계 ==== 실제로 얼마나 많은 일이 일어나는지 이제 알았을 것이다. 그러나 메모리를 잘 활용하는 것이 중요하지만 메모리는 디스크에 비해서 데이터를 많이 저장하지 못하는 것이 사실이다. 그러므로 디스크로의 접근은 불가피하다. 그리고 수시로 데이터는 디스크에 내려졌다 올려 졌다를 반복해야 한다. 이제 초점을 디스크에 대한 특성과 디스크 입/출력에 대해서 살펴보자. 디스크는 특성상 하나의 오퍼레이션에 하나의 결과를 리턴한다. 그러므로 한번의 요청에 시스템 카탈로그, 인덱스, 데이터, 로그, 만약 정렬까지 한다면 임시 저장공간도 필요하다. 이 많은 일을 하나의 디스크에서 한다고 생각해보면 끔찍한데 실제로 사용자는 별로 못 느낀다. 대부분 사용자가 몇 명 안되고, 시스템의 자원도 펑펑 남아돌기 때문이다. 만약 사용자가 많다면 상당히 바빠질 것이다. 그러므로 필자는 시스템DB, 트랜잭션 로그, 데이터, 인덱스, 임시저장을 위한 공간을 서로 다른 디스크에 분리하는 것을 원칙으로 생각하고 있다. 돈이 없다면 할 수 없지만 어쨌든 서로 다른 디스크에 분리하는 것을 권장한다. 적어도 디스크의 경합을 이 정도면 상당히 줄일 수 있을 것이다. 물론 디스크 입/출력의 양도 디스크가 한 개 일 때 보다 5배 정도 향상되었으니 디스크의 분산의 이점을 상당하다. 이런 분산의 개념은 나중에 이야기할 RAID와는 혼동하지 말기를.. 여담이지만 예전에 필자가 면접보러 갔다가 이러한 이야기를 해줬더니 RAID와 혼동하였다. 그래서 열심히 침튀겨가며 이해를 시켜주고 왔는데 씁쓸했다. 이 글을 보는 사람들은 필자를 씁쓸하게 하지 않았으면 좋겠다. 각 DBMS제품에서는 이러한 분산에 대한 방안을 많이 강구하고 있다. 대표적으로 오라클의 분할기법이다. 인덱스와 테이블을 분할 할 수 있는 것이데, 참으로 감동먹이는 것이다. 이와 비슷하게 MSSQL Server에서는 어떤 특정한 기준으로 테이블을 여러 개로 수평분할 한 다음 뷰를 이용하는 방법을 선보이고 있다. 이에 대해서는 나중에 언급하기로 하자. 다음은 MSSQL Server에서 파일그룹을 이용한 디스크 분산된 데이터베이스를 생성하는 모습니다. {{{ USE master GO /*각각의 파일그룹은 다른 디스크에 만든다(경로 다시 지정해야함)*/ /*데이터양을 예상하여 데이터베이스 크기를 결정한다*/ CREATE DATABASE testdb ON PRIMARY ( NAME = PData1, --부모테이블 저장 FILENAME = 'C\PData1.mdf', SIZE = 10, MAXSIZE = 50, FILEGROWTH = 15% ), FILEGROUP CData --자식테이블 저장 ( NAME = CData1, FILENAME = 'D:\CData1.ndf', SIZE = 10, MAXSIZE = 50, FILEGROWTH = 5 ), FILEGROUP IndexData --인덱스 저장 ( NAME = IndexData1, FILENAME = 'E:\IndexData1.ndf', SIZE = 10, MAXSIZE = 50, FILEGROWTH = 5 ), FILEGROUP Image_Data --파일등의 바이너리 포멧의 이미지 저장 ( NAME = Image_Data1, FILENAME = 'F:\Image_Data1.ndf', SIZE = 10, MAXSIZE = 50, FILEGROWTH = 5 ) LOG ON --로그저장 ( NAME = EAS_log, FILENAME = 'G:\EAS_Log.ldf', SIZE = 5MB, MAXSIZE = 25MB, FILEGROWTH = 5MB ) }}} ==== 조인과 디스크 분산 ==== 위의 예제를 잘 보라. 필자가 왜 부모 테이블과 자식 테이블을 저장할 공간을 따로 두었는지 말이다. 필자가 저렇게 부모테이블과 자식테이블을 분리한 이유는 조인시 발생하는 디스크 경합문제를 최소화해보자는 의도에서 최소한의 디스크 분산을 시킨 것이다. 조인이란 것은 두 개 이상의 테이블에 접근하는 것을 말한다. 물론 셀프조인의 경우는 실제로 테이블 한 개에 접근하는 것이지만 기본적으로는 별칭을 두어 두 개의 테이블로 인식하게 하기 때문에 어쨌든 두 개 이상의 테이블에 접근한다. 그럼 왜 부모테이블과 자식테이블로 디스크를 분산시켰을까? 이것은 두 개의 테이블에서 1측은 부모, 다측은 자식의 기준이 아니다. 그것은 바로 탄생의 순서에 따른 기준이다. 즉, 엔티티 도출과정을 거쳐 도출된 엔티티를 분류하는 과정에서 나타나는 중요한 엔티티들을 부모 테이블로 보고, 업무를 실제로 발생시키는 엔티티를 자식이라고 본 것이다. 이렇게 부모/자식 테이블을 나눈 이유는 대부분 조인이 일어나면 부모/자식 테이블간 조인이 일어나기 때문이다. 또한 사용 패턴에 맞추어서 테이블을 잘 분리해야 할 것이다. 물론 이러한 분류기준은 디스크의 개수에 따라서 달라지는 것은 당연하다. 실제로 두 개의 테이블을 조인할 때 Nested Loop 조인은 먼저 한 테이블을 읽어 들인 후 나머지 테이블을 읽어서 조건에 맞는 행을 리턴하는 형태이다. 그러므로 Nested Loop 조인의 경우에는 많은 디스크 경합을 벌이지 않는다. Nested Loop 조인의 경우는 데이터가 정렬된 상태인가가 더 문제가 된다. 그러나 Merge 조인의 경우는 디스크 경합 문제가 심각할 수도 있다. 왜냐하면 Merge 조인은 두 개의 테이블을 조인할 때 두 개의 테이블을 동시에 읽어서 필터링 후 정렬된 결과를 가지고 짝짓기를 하기 때문이다. 두 개의 테이블에 동시에 접근해야 하는데 디스크는 하나의 명령에 하나의 읽기 또는 쓰기를 수행하므로 디스크 경합이 발생한다. 이러한 경우는 테이블을 서로 분리하면 성능에 도움이 된다. 그렇다고 Nested Loop 조인에는 테이블들을 각각의 디스크에 놓아도 별 소용이 없다는 것은 아니다. 앞에서 SQL문의 처리과정에 보았듯이 순차적으로 일을 해야 하기는 하지만 이것은 거의 동시에 일어나므로 각각의 임무를 수행하려는 프로세스들은 디스크라는 자원을 얻기 위해 쟁탈전을 벌이므로 역시 자주 조인될 테이블들을 각각의 디스크에 분산 시키는 것은 많은 이득이 있다. ==== 순차데이터와 비 순차데이터의 분리 ==== 전산학에서 데이터를 정렬시켜서 저장하면 검색속도가 빠르다는 것은 일반화된 사실이다. 여러 개의 테이블에 저장되는 데이터는 순차적일 수도 있고, 비순차적일 수도 있다. 만약 여러 개의 테이블이 하나의 디스크에 순차적으로 저장되어 있다면 아주 좋은 상태라고 할 수 있다. 그러나 중간에 미꾸라지 한마리가 있다면 물은 흐려진다. 아무리 많은 수가 있어도 거기에 곱하기 0을 하면 0이 되듯이 순차 데이터가 있는 디스크에 비순차 데이터가 낀다면 그것은 비순차 데이터가 된다. 그러므로 순차 데이터를 담을 디스크와 비순차 데이터를 담을 디스크를 분리하는 것이 좋다. ==== RAID 적합성 판단 ==== RAID레벨이 적합한 지도 판단해야 한다. RAID에 관련된 사항은 [Disk와 RAID] 부분을 참고하기 바란다. ==== 참고자료 ==== * [http://msdn.microsoft.com/en-us/library/dd758814.aspx Disk Partition Alignment Best Practices for SQL Server] * [http://blogs.msdn.com/jimmymay/archive/2009/05/08/disk-partition-alignment-sector-alignment-make-the-case-with-this-template.aspx Disk Partition Alignment (Sector Alignment): Make the Case: Save Hundreds of Thousands of Dollars] * [http://kendalvandyke.blogspot.com/2009/02/disk-performance-hands-on-series.html Disk Performance Hands On: Series Introduction]