이 포스트는 innodb_ruby
0.8.8 버전(2014/02/03)을 기준으로 작성되었습니다.
InnoDB 핵심을 이해하기 위한 여행에서 innodb_ruby 프로젝트의 라이브러리와 커맨드 라인 툴을 소개했습니다. 이제 이 툴이 어떤 일을 할 수 있는지 직접 보여드리겠습니다. 이 포스트에서는 데모에 집중할 예정이라, 이 포스트에 등장하는 InnoDB 구조에 대해 일일이 설명하지는 않을 겁니다. 추후 다른 포스트에서 자세히 설명할 예정입니다!
Ruby와 gem에 익숙한 경우 (또는 Ruby 설치가 잘 된 경우), 저는 정기적으로 innodb_ruby
gem을 RubyGems에 push하기 때문에 아래과 같이 설치하면 됩니다:
$ gem install innodb_ruby
위의 명령어로 설치가 안된다면, RubyGems 메뉴얼을 참고하여 설치가 제대로 됐는지 확인하세요. 아니면.. 포기하세요. :-D
설치가 완료되면, 경로에 innodb_space
커맨드가 있어야 합니다:
$ innodb_space
Error: File must be provided with -f argument
Usage: innodb_space -f <file> [-p <page>] [-l <level>] <mode> [<mode>, ...]
이 예제에서는 다른 데이터 구조를 제대로 검토하기 위해 여러 줄의 코드가 필요합니다. Barracuda 테이블을 갖는 새로운 서버 (MySQL 5.5+)를 실행하고 innodb_file_per_table
옵션을 활성화했는지 확인하세요. Ruby를 사용해 아주 간단한 테이블을 만들고 채웁니다:
#!/usr/bin/env ruby
require "mysql"
m = Mysql.new("127.0.0.1", "root", "", "test")
m.query("DROP TABLE IF EXISTS t")
m.query("CREATE TABLE t (i INT UNSIGNED NOT NULL, PRIMARY KEY(i)) ENGINE=InnoDB")
(1..1000000).to_a.shuffle.each_with_index do |i, index|
m.query("INSERT INTO t (i) VALUES (#{i})")
puts "Inserted #{index} rows..." if index % 10000 == 0
end
위 코드는 약 48MiB 또는 16KiB 페이지 3,071개에 해당하는 백만 행 (random 순서로 삽입됨)을 갖는 테이블을 생성합니다.
(집에서 위 코드를 사용하려는 경우, 이 툴이 디스크의 tablespace 파일에 접근할 것이므로, 진행하기 전에 모든 dirty 페이지가 flush 될 때까지 기다려야 합니다. 이 때, SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty'
의 결과가 도움이 되실 겁니다. 실행 중인 InnoDB 인스턴스와의 조정은 따로 필요 없습니다.)
innodb_space
로 볼 수 있는 가장 하이 레벨의 개요 중 하나는 space-page-type-regions
입니다. 이 기능은 주어진 페이지 유형에 대해 인접한 블록끼리 묶어 한 줄씩 출력합니다:
$ innodb_space -f test/t.ibd space-page-type-regions
start end count type
0 0 1 FSP_HDR
1 1 1 IBUF_BITMAP
2 2 1 INODE
3 37 35 INDEX
38 63 26 FREE (ALLOCATED)
64 2188 2125 INDEX
2189 2239 51 FREE (ALLOCATED)
2240 2240 1 INDEX
2241 2303 63 FREE (ALLOCATED)
2304 2304 1 INDEX
2305 2367 63 FREE (ALLOCATED)
2368 2368 1 INDEX
2369 2431 63 FREE (ALLOCATED)
2432 2432 1 INDEX
2433 2495 63 FREE (ALLOCATED)
2496 2496 1 INDEX
2497 2687 191 FREE (ALLOCATED)
InnoDB 내부 구현의 세부 사항까지 들어가지 않아도, InnoDB의 부기 (bookkeeping) 구조 (FSP_HDR, IBUF_BITMAP 및 INODE 페이지), 실제 테이블 데이터 (INDEX 페이지) 및 여유 공간 (FREE (ALLOCATED) 페이지)을 확인할 수 있습니다.
각 index (실제로는 각 “파일 세그먼트” 또는 각 index에 대한 FSEG)별로 페이지의 공간 사용률을 리스트로 뽑아보면 꽤 흥미롭습니다:
$ innodb_space -f test/t.ibd space-indexes
id root fseg used allocated fill_factor
15 3 internal 3 3 100.00%
15 3 leaf 2162 2528 85.52%
모든 index는 non-leaf 페이지에 사용되는 internal
파일 세그먼트 (file segment)와 leaf 페이지에 사용되는 leaf
파일 세그멘트를 갖습니다. 페이지는 하나의 파일 세그먼트에 할당될 수 있지만, 현재 미사용 중일 수 있습니다 (type FREE (ALLOCATED)). 따라서 fill_factor
는 사용 중인 페이지 / 할당된 전체 페이지
의 비율을 나타냅니다 (index 페이지가 얼마나 꽉 찼는지 와는 관련이 없으며, 이는 다른 문제).
page-dump
모드는 단일 페이지에 대해 알고 있는 모든 것을 덤프합니다. 구조를 출력하기 위해 전형적인 Ruby pretty-printer 모듈인 pp를 현재 사용하고 있습니다 — 이 부분은 추후 정리가 필요합니다. innodb_ruby
라이브러리는 처음에는 최소한의 Innodb::Page 클래스를 사용하여 페이지를 파싱한 다음, 추가 파싱을 위해 공통 헤더에 있는 type 필드를 사용하여 선택적으로 다른 페이지 타입을 특수 클래스 (예: INDEX 타입인 Innodb::Page::Index)로 넘깁니다.
검사를 시작하기 좋은 페이지는 위에서 만든 테스트 테이블의 index tree root 노드인 첫 번째 INDEX 페이지이며, 페이지 3에 해당합니다:
$ innodb_space -f test/t.ibd -p 3 page-dump
첫 번째 행은 이 페이지를 핸들링하는 클래스를 나타냅니다:
#<Innodb::Page::Index:0x007fe304855360>:
그 다음에 바로 FIL 헤더가 출력됩니다:
fil header:
{:checksum=>621772966,
:offset=>3,
:prev=>nil,
:next=>nil,
:lsn=>102947976,
:type=>:INDEX,
:flush_lsn=>0,
:space_id=>1}
FIL 헤더 (그리고 footer)는 모든 페이지가 공통적으로 갖는 필드이며, 페이지 자체에 대한 정보를 주로 포함합니다.
추가 정보는 페이지 유형에 따라 다릅니다; INDEX 페이지의 경우 다음 정보가 덤프 됩니다:
space-index-pages-summary
모드를 사용하면 모든 index 페이지의 공간 사용 관련 데이터를 볼 수 있습니다:
$ innodb_space -f test/t.ibd space-index-pages-summary | head -n 10
page index level data free records
3 15 2 26 16226 2
4 15 0 9812 6286 446
5 15 0 15158 860 689
6 15 0 10912 5170 496
7 15 0 10670 5412 485
8 15 0 12980 3066 590
9 15 0 11264 4808 512
10 15 0 4488 11690 204
11 15 0 9680 6418 440
위와 같이 데이터 양과 여유 공간을 볼 수 있고, 테이블에 대한 레코드 수를 최소한으로 줄일 수 있습니다.
gnuplot이 있고 Ruby gnuplot gem이 설치되어 있다면, 이 정보를 이용해 scatter plot을 그리는 게 (비록 그리 예쁘지는 않지만) 매우 쉽습니다:
$ innodb_space -f test/t.ibd space-index-pages-free-plot
Wrote t_free.png
space-index-pages-free-plot
으로 그린 scatter plot은 아래와 같습니다:
Free Space Plot - Y축은 각 페이지의 여유 공간을 나타내며, X축은 페이지 번호이자 파일 오프셋을 의미합니다.
실제 테이블을 검사할 때 이 툴을 유용하게 쓰려면, innodb_ruby
가 테이블 스키마를 이해할 수 있는 수단을 제공 받아야 합니다. 이는 동적으로 로드될 수 있는 “describer” 클래스의 형태로 수행됩니다. 이 부분은 innodb_ruby
라이브러리의 한 특징으로, 문서화가 잘 되어 있지는 않습니다 (아직 잘 설계되어 있지 않음). 위 테이블 (i INT UNSIGNED NOT NULL, PRIMARY KEY (i)
및 다른 열이나 index가 없는)에 대한 간단한 describer 클래스는 다음과 같습니다:
class SimpleTDescriber < Innodb::RecordDescriber
type :clustered
key "i", :INT, :UNSIGNED, :NOT_NULL
end
이 클래스가 simple_t_describer.rb
파일에 저장되면, innodb_space
에서 -r <file>
을 사용하여 로드되고 -d <class>
인수로 활성화될 수 있습니다:
$ innodb_space -f test/t.ibd -r /path/to/simple_t_describer.rb -d SimpleTDescriber <mode>
로드된 레코드 describer는 주로 다음 두 가지 작업을 수행합니다:
page-dump
모드에서 레코드 파싱 및 덤프를 활성화합니다. 이를 통해 덤프된 레코드에 :key
와 :row
키가 채워질뿐만 아니라, 트랜잭션 ID와 롤 포인터 키가 사용 가능하게 됩니다 (이 정보는 키와 키가 아닌 필드 사이에 저장되므로, 키 필드를 파싱하는 방법을 모르는 한 바로 접근할 수 없습니다).index-recurse
모드를 포함한 모든 index recursion 함수의 사용을 허용합니다. B+tree 페이지들을 서로 연결하는 InnoDB의 “node pointer records”를 파싱하려면 레코드를 파싱할 수 있는 능력이 필요합니다.test_t_page_3_page_dump.txt (index root 페이지) 및 test_t_page_4_page_dump.txt (index leaf 페이지)와 같이 전체 레코드가 출력되도록 페이지를 덤프하는 것도 가능합니다.
레코드 describer가 사용 가능하면, index-recurse
를 사용해 index를 재귀 방문할 수 있습니다:
$ innodb_space -f test/t.ibd -r /path/to/simple_t_describer.rb -d SimpleTDescriber -p 3 index-recurse
ROOT NODE #3: 2 records, 26 bytes
NODE POINTER RECORD >= (i=252) -> #36
INTERNAL NODE #36: 1117 records, 14521 bytes
NODE POINTER RECORD >= (i=252) -> #4
LEAF NODE #4: 446 records, 9812 bytes
RECORD: (i=1) -> ()
RECORD: (i=2) -> ()
RECORD: (i=3) -> ()
RECORD: (i=4) -> ()
RECORD: (i=5) -> ()
발견된 각 노드 (페이지)에 대한 정보를 출력하고, leaf 페이지의 사용자 레코드를 덤프하는 동안 B+tree를 오름차순 (기본적으로 full-table scan)으로 방문합니다. test_t_page_3_index_recurse.txt에서 위 결과의 샘플 (10k 라인)을 확인할 수 있습니다.
더 자세한 내용은 innodb_ruby wiki에서 확인하실 수 있고, 한글 문서는 innodb_ruby 한글 문서에서 확인하실 수 있습니다.