All Articles

innodb_ruby 빠르게 훑어보기

원문: A quick introduction to innodb_ruby

이 포스트는 innodb_ruby 0.8.8 버전(2014/02/03)을 기준으로 작성되었습니다.

InnoDB 핵심을 이해하기 위한 여행에서 innodb_ruby 프로젝트의 라이브러리와 커맨드 라인 툴을 소개했습니다. 이제 이 툴이 어떤 일을 할 수 있는지 직접 보여드리겠습니다. 이 포스트에서는 데모에 집중할 예정이라, 이 포스트에 등장하는 InnoDB 구조에 대해 일일이 설명하지는 않을 겁니다. 추후 다른 포스트에서 자세히 설명할 예정입니다!

innodb_ruby 설치

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 인스턴스와의 조정은 따로 필요 없습니다.)

Tablespace 파일 검사

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_BITMAPINODE 페이지), 실제 테이블 데이터 (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 페이지의 경우 다음 정보가 덤프 됩니다:

  • page header: index 페이지에 대한 정보
  • fseg header: index에서 사용되는 파일 세그먼트 (extent의 그룹)의 space management와 관련된 정보
  • size: 페이지의 다양한 구성 요소들 (free space, data space, record size 등)의 크기를 byte 단위로 요약
  • system records, infimum and supremum
  • record 검색을 보다 효율적으로 수행하는 데 사용되는 page directory의 내용
  • 사용자가 저장한 실제 데이터인 user record (record “describer”가 로드되지 않은 경우, 해당 필드는 파싱되지 않음)

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-flot

Free Space Plot - Y축은 각 페이지의 여유 공간을 나타내며, X축은 페이지 번호이자 파일 오프셋을 의미합니다.

Row 데이터의 의미 만들기

실제 테이블을 검사할 때 이 툴을 유용하게 쓰려면, 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 페이지)와 같이 전체 레코드가 출력되도록 페이지를 덤프하는 것도 가능합니다.

Index를 재귀 방문하기 (recursion)

레코드 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 한글 문서

더 자세한 내용은 innodb_ruby wiki에서 확인하실 수 있고, 한글 문서는 innodb_ruby 한글 문서에서 확인하실 수 있습니다.

Published Aug 9, 2019

Mijin An
Working with two cats 🐱🦁