pgsql, mysql, ldap, pgsql+slony-I을 지나 도저히 RDBMS로는 해결할 수 없을 것 같아서 파일 기반 DB엔진들을 알아보았습니다.
Tokyo Cabinet(이하 TC)이라는 일본의 mixi에서 쓰는 엔진의 성능이 좋아 보여서 바로 설치해서 포함된 테스팅 툴을 돌렸습니다.
1억건 채우는데 400초가 걸리더군요. 옳다쿠나 싶어서 이걸 기반으로 미들웨어를 만들었습니다.
필요한 구조는
ID, URL 두가지로 조회가 일어나고 두가지 값 중 하나로 검색을 하면 입력 시 넘겨준 데이터 내용을 주기만 하면 됩니다.
그래서 ID-DATA 해시 테이블, URL-ID 해시 테이블. 이렇게 두개를 만들어서 하기로 했습죠..
처음에는 엄청난 속도로 입력이 되기 시작했습니다. 드디어 고생끝이다 싶었는데 천만건을 넘어가면서 속도가 1/10으로 줄어들었습니다.
그래서 여기저기 튜닝도 해보고 테스트 셋인 1억건에 맞춰서 hash bucket도 늘리고 이짓 저짓을 다 했습니다.
근데 중요한게 이 서버의 메모리가 15기가 뿐이라서 이 영역을 넘어서는 데이터 크기가 되면 무조건 느려졌습니다.
하나의 파일을 mmap으로 처리하는 놈이라 디스크 IO에 영향을 많이 받겠구나 싶어서 중간에 메모리 해시 테이블을 두고 일단 메모리에 넣은 다음에 background writer가 파일에 싱크하는 방식도 해봤는데 메모리와 디스크의 속도 차가 너무 커서 결국 포기
이거 이틀만에 GG
그다음 솔루션은 Berkeley DB였습니다. 오라클에서 지원하고 있고 문서도 풍부하고 튜닝 포인트도 많고 해서 선택했습니다.
저장 레이어를 TC에서 BDB로 바꾸고 다시 테스트를 돌리는데 캐쉬를 12기가를 주고 page size 조절하고 DB_ENV가지고 뭐도 해보고 다 해봐도 TC만큼 나오지 않았습니다. 입력, 조회 속도가 느린데다 메모리도 많이 먹더군요..
필요한 성능은 초당 5천~1만건 입력입니다... 턱도 안되는 성능을 보여줘서 GG..
그래서 어쩔수 없이 low level을 직접 만들기로 했습니다.
요구치는 10억건의 데이터를 store한다. 인데 목표치를 낮췄습니다;; 3억건으로.
3억건의 데이터 중 필요한 데이터를 찾아내는데 필요한 ID, URL인덱스에 메모리 9기가를 주고 나머지 메모리는 파일 캐싱과 OS, 미들웨어가 사용한다고 가정하고 데이터 구조를 만들기 시작했습니다.
1억건의 데이터가 100바이트만 사용해도 10기가 입니다.. 3억건이 9기가의 메모리 내에서 처리하려면 하나의 인덱스가 30바이트를 넘어서는 안됩니다.. =_=; 데이터 구조를 만들다 보니 임베디드 환경도 아니고 엄청나게 제약적인 환경이 되더군요..
ID인덱스 (10바이트)
ID: unsigned int32(4바이트)
데이터 파일 ID: unsigned int16(2바이트)
데이터 파일 내의 데이터 위치 offset: unsigned int32(4바이트)
URL인덱스 (20바이트)
URL hash: char[16] (16바이트 md5)
ID: unsigned int32(4바이트)
사실 ID인덱스는 9바이트 였습니다; 마지막 offset의 경우 2의 24승으로 충분히 표현할 수 있어서 int24를 구현했는데 개발 환경이랑 실제 서비스랑 바이트 오더가 달라서 문제가 자꾸 생겨서 그냥 4바이트로 감.. 메모리 300메가 낭비 ㅠ
URL로 검색이 들어오면 ID를 찾아서 ID인덱스에서 뒤지는 구조입니다.
두 종류의 인덱스는 각각 3억건에 해당하는 내용을 담을 파일을 수만개로 나눠서 mmap으로 열고 있습니다. 처음에는 무심코 65,536개씩으로 했다가 max open file에 걸려서 부랴부랴 수정함;; 두 인덱스 합쳐서 약 4만개의 수백KB의 파일로 나눔
ID인덱스는 어차피 정수형이고 3억건이 limit이기 때문에 들어오면 2차원 배열처럼 된 메모리에 바로 매핑됩니다. 수치가 어떻든 찾아서 데이터 얻는데 걸리는 시간이 매우 빠르고 같습니다.
URL쪽 인덱스가 문제인데 어떤 URL이 들어올 지 몰라서; ID인덱스 처럼 공간을 미리 할당해 놓을 수가 없습니다. 그리고 MD5로 URL을 해싱해서 하기 때문에 데이터 충돌이 날 수 있습니다.. 통계 상 3억건의 MD5에서 충돌이 발생할 확율이 매우(0.000...0???) 낮기 때문에 무시하기로 함 -_);;;
새로운 URL이 들어오면 mmap으로 매핑된 URL인덱스 파일에서 20바이트씩 seek 하면서 데이터를 찾습니다... 이놈이 성능이 쥐약임 ㅠ 이거 개선해야 함.... 그래서 ID를 찾으면 ID인덱스에서 데이터 파일에 대한 정보 얻음
그래서 파일 ID와 파일 offset을 찾아내면 4메가마다 블럭단위로 증가하는 데이터 파일을 열어서 해당 offset에서 [내용길이,내용..] 으로 되어 있는 블럭을 읽어서 sendfile로 클라이언트에게 전송.
데이터 파일은 한 번 쓰여지면 삭제 마킹되거나 수정하지 않고 새로운 내용이나 갱신된 내용이 들어오면 현재 열려있는 마지막 데이터 파일 맨 밑에 붙입니다.
요 며칠 삽질이고 아직 얼마나 잘 돌아가는지는 확인해보지 못 했습니다. 아직도 테스트 중;
신경 쓴 부분입니다.
1. 데이터를 찾아가는 복잡도를 0에 가깝게 한다 -> ID 인덱스
2. 다른 엔진처럼 디스크를 아끼기 위한 삽질을 하지 않는다 -> 무조건 마지막 데이터 파일에 append
3. 메모리를 아끼기 위해 가능하면 zero copy -> 전송은 sendfile
4. 인덱스만 메모리에 올리고 데이터 파일은 가능하면 캐싱되지 않도록 한다. -> 쓰기 위한 마지막 데이터 파일을 제외하고는 요청시 읽어서 sendfile하고 끝
가장 힘든건 뭐 바꾸고 나면 성능 잘 나오는지 알아보기 위해서 하염없는 데이터 입력 테스트를 기다려야 한다는 것 ㅠ
이걸로 끝이 났으면 좋겠네요 ㅠㅠㅠㅠㅠ;
|