mariaDB-Optimization and Tuning(Table)

OPTIMIZE TABLE

OPTIMIZE TABLE 구문은 두 개의 함수로 이루어져 있다. 각각 테이블의 조각화 현상을 해결하거나 전문인덱스를 업데이트하는데 사용된다.

Defragmenting

OPTIMIZE TABLE 명령은 InnoDB, Aria, MyISAM, ARCHIVE 테이블에서 동작 가능하며 많은 양의 레코드를 삭제하였거나 가변길이 row(VARCHAR나 VARBINARY, BLOB, Text 컬럼등)를 대량 변경하였을 경우 수행해야 한다. (MariaDB 10.1.1 이전에는 innodb_file_per_table 파라미터가 ON 된 경우에만 가능했었음) 삭제된 row는 링크된채로 리스트에 보관되며 insert 동작이 오면 해당 row위치를 재사용한다.

이 구문은 테이블에 대해 SELETE와 INSERT 권한이 필요하다.

기본적으로 OPTIMIZE TABLE 명령은 바이너리 로그에 남고 복제본에 전달되는데 NO_WRITE_TO_BINLOG 키워드를 (혹은 별칭으로 그냥 LOCAL만 쳐도 됨) 사용하면 바이너리 로그를 남기지 않게 된다.

OPTIMIZE TABLE은 또한 파티셔닝 된 테이블에도 지원된다. ALTER TABLE … OPTIMIZE PARTITION 명령을 사용하면 다수의 파티셔닝도 최적화할 수 있다.

Updating an InnoDB fulltext index

전문 인덱스에 row가 추가되거나 삭제되어도 전문인덱스의 재구성 작업은 비용이 크기 때문에 즉시 수행되지 않는다. 변경된 통계정보는 다른 위치에 저장된다. 전문 인덱스는 오직 OPTIMIZE TABLE 명령을 통해서만이 재구성된다.

기본적으로 OPTIMIZE TABLE 명령은 테이블의 조각화 현상을 해결한다. 만약 전문 인덱스의 통계정보를 업데이트하려면 innodb_optimize_fulltext_only 시스템 변수를 1로 셋팅해야 한다. 이것은 반드시 임시적으로만 쓰여야 하며 전문인덱스의 재구성이 완료되면 0으로 돌려놔야 한다. 전문인덱스의 재구성 작업은 시간이 꽤 오래 걸리기 때문에 innodb_ft_num_word_optimize 변수는 재구성하려는 단어의 값을 제한하고 있다. (기본적으로 2000) 전문인덱스 재구성 명령을 동시에 여러 개 수행해도 된다.

Defragmenting InnoDB Tablespaces

mariaDB 10.1.1 에서 페이스북과 카카오의 조각모음 방식 패치를 받아들여 InnoDB 테이블스페이스에 대해서도 조각모음이 가능하게 되었다. 이 기능을 사용하려면 innodb_defragment 변수를 활성화시켜야 한다.

InnoDB 테이블에서 row가 삭제되면 물리적으로 제거하는 대신 delete maker가 새겨놓는다. 그리고 삭제된 영역의 재사용을 위해 OS로 반환하지 않는다. purge 쓰레드가 인덱스의 키와 row를 지우기 위해 수행되는데, 이렇게 지워진 free 여역도 OS에 반환되지 않는다. 이것은 page에 빈 공간을 유발하게 된다. 만약 당신이 가변길이 row를 사용하고 있을 때 새로운 row의 길이가 방금 지운 row의 길이보다 크다면 지운 영역을 재사용할 수 없게 된다.

OPTIMIZE TABLE이나 ALTER TABLE … ENGINE=InnoDB 명령은 테이블을 재구성하도록 해준다. 그런데 불행히도 공통 테이블스페이스 파일인 ibdata1에 대해 최적화를 수행하면 아래와 같은 현상이 일어난다.

  • 테이블의 데이터와 인덱스를 연속적으로 만든다.
  • 연속적인 데이터와 인덱스 페이지가 ibdata1에 이어써지기 때문에 ibdata1의 크기가 증가한다. (?)

MariaDB 10.1 에서는 페이스북의 Matt, Kakao의 이성욱에 의해 제공된 조각모음 패치를 포함하였다. 페이스북의 코드와 Matt코드의 차이점은 MariaDB는 새로운 literal을 SQL에 포함하지 않았고 서버 코드를 변경하지 않았다는 것이다. 대신 InnoDB/XtraDB 스토리지 엔진 내부에만 코 변경을 하였다.

OPTIMIZE TABLE의 동작 원리는 기본적으로 변하지 않았다. 새로 추가된 이 기능을 사용하려면 innodb_defragment 시스템 변수를 1로 설정해야 한다.

새로운 테이블을 만들어서 데이터를 이전 테이블에서 새로운 테이블로 옮기는 작업이 필요없어졌다. 대신 n개의 새로운 page를 만들어서 record를 새로운 page로 옮기게 된다. 새로운 page가 꽉 차면 기존 record가 있던 page는 빈 영역이 된다. 이와 관련하여 새로 소개된 시스템 변수는 아래와 같다. (이 변수들도 10.1.1부터 추가됨)

System variables

  • Innodb_defragment: InnoDB의 조각모음을 활성화한다.
  • innodb_defragment_n_pages: 조각모음시 한 타임에 몇 개의 page를 가지고 작업할 것인지를 결정하는 파라미터이다. 기본 값: 7
  • innodb_defragment_stats_accuracy:  조각모음 통계의 개수… (?)
  • innodb_defragment_fill_factor_n_recs: 조각모음 수행시 추후 사용을 위해 남겨놓을 페이지 여유분으로 record 건수를 기준으로 함. 기본 값: 20
  • innodb_defragment_fill_factor: 조각모음 수행시 페이지를 얼마나 채울 지 비율을 결정하는 변수. 기본 값: 0.9
  • innodb_defragment_frequency: 단일 인덱스에 대해 조각모음을 수행할 수 있는 초당 횟수의 최대값. 기본 값: 40

Status variables

  • innodb_defragment_compression_failures: 조각모음 재압축의 실패 횟수
  • innodb_defragment_failures: 조각모음 실패 횟수
  • innodb_defragment_count: 조각모음 수행 횟수

 

ANALYZE TABLE

ANALYZE TABLE은 테이블의 키 분포에 대해 분석하고 저장하는 구문이다. MyISAM, Aria, InnoDB 테이블에서 모두 수행 가능한데, MyISA 엔진에서 수행하면 테이블에 read 락이 걸리며 InnoDB 엔진에서 수행하면 write 락이 걸린다. MyISAM 테이블 같은 경우 myisamchk –analyze 명령을 수행한 것과 동일한 효과를 보인다.

Join 수행시 MariaDB는 키의 분포도를 보고 테이블의 조인 순서를 결정한다. 그리고 키 분포를 보고 특정 테이블에서 어떤 인덱스를 사용할 것인지를 결정하기도 한다. ANALYZE 문을 수행하기 위해선 SELECT와 INSERT 권한이 필요하다. 기본적으로 ANALYZE문은 바이너리 로그에 남고 replication에 전달되기 때문에 만약 바이너리 로그에 남기지 않으려면 NO_WRITE_TO_BINLOG 키워드를 추가하면 된다.

※ MariaDB 10.0.1에서 ANALYZE시 스토리지 엔진과 무관한 통합 통계정보를 수집하도록 되었으며 현재 이러한 통계정보는 풀 테이블 스캔이나 풀 인덱스 스캔시 수집된다. 물론 비용은 꽤 크다.

MariaDB 10.0.1부터 ANALYZE TABLE 명령은 PERSISTENT FOR 키워드를 이용하여 특정 컬럼이나 인덱스를 선택할 수 있도록 되었다. 아래는 사용법에 대한 예제이다. 그리고 BLOB이나 텍스트 컬럼은 통계정보 수집되지 않는다.

ANALYZE TABLE tbl PERSISTENT FOR ALL;
ANALYZE TABLE tbl PERSISTENT FOR COLUMNS (col1,col2,...) INDEXES (idx1,idx2,...);
ANALYZE TABLE tbl PERSISTENT FOR COLUMNS () INDEXES (idx1,idx2,...);
ANALYZE TABLE tbl PERSISTENT FOR COLUMNS () INDEXES ();
--> 컬럼과 인덱스에 대한 통계는 빼고 테이블의 통계 정보만 수집함.
use_stats_tables 변수가 never로 설정되어 있지 않은 한 단순 ANALYZE TABLE 명령은
통합 통계 테이블에 저장되게 된다.
SET SESSION use_stat_tables='COMPLEMENTARY';
ANALYZE TABLE tbl;

테이블 옵션에 STATS_PERSISTENT=1이 설정되어 있다 하더라도 통합 통계정보를 업데이트하도록 하려면 PERSISTENT FOR 키워드를 써야 한다.

 

Converting Tables from MyISAM to InnoDB

MyISAM 을 InnoDB로 옮기는 것은 ALTER TABLE … ENGINE=innoDB 명령으로 간단히 바꿀수는 있다. 그러나 스토리지 엔진을 변경하는 것은 미처 알아채기 힘든 여러 이슈들을 포함하고 있다.

인덱스가 어떻게 작동하는지 이해하는 것은 InnoDB에서 무엇이 더 빠르고 무엇이 더 느릴지 이해하는데에 도움을 줄 것이다.

INDEX Issues

모든 InnoDB 테이블은 PK를 갖는다. 만약 별도로 지정하지 않았다면, Not null & Unique 속성을 갖는 컬럼이 PK가 될 것이며, 그것도 없으면 6바이트의 숨겨진 컬럼이 내부적으로 생성된다.
별도로 PK컬럼을 지정하지 않은 테이블을 생각해보자. 비록 인위적인 AUTO_INCREMENT 속성이 포함된다 하더라도 PK를 명시적으로 지정하자. 이것은 강제적인 사항은 아니지만 MyISAM이 아닌 InnoDB를 사용할  강력한 권고사항이다. 아래는 PK에 대해 몇 가지 사실들을 이야기하고 있다.

Fact: 모든 세컨더리 인덱스의 키는 PK를 포함하고 있다.

PRIMARY KEY(id),
INDEX(b), -- effectively the same as INDEX(b, id)
INDEX(b, id) -- effectively the same as INDEX(b)

Index(b)를 만드나 Index(b, id)를 만드나 동일하기 때문에 하나는 삭제해도 좋다.

PRIMARY KEY(id),
UNIQUE(b), -- keep for uniqueness constraint
INDEX(b, id) -- DROP this one

이런 경우도 동일한 인덱스이지만 UNIQUE(b)는 b가 Unique하다는 것을 알려주기 때문에 좀 더 많은 정보를 담고 있다. 따라서 INDEX(b,id)를 삭제하는 것이 좋다.

PRIMARY KEY(id),
INDEX(id, b) -- DROP this one; it adds almost nothing

이 경우에도 인덱스는 삭제하는 것이 좋다.

Contrast: MyISAM의 특성이 InnoDB에서는 발현되지 않는 케이스도 있다. MyISAM에서 아래 케이스의 경우 ‘abc’의 값마다 별도의 ‘id’의 값을 가지며 1씩 증가하게 된다.

id INT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (abc, id)

이런 MyISAM의 특성을 흉내내기 위해 아마 당신은 아래와 같이 코딩할 것이다.

INSERT INTO foo
    (other, id, ...)
    VALUES
    (123, (SELECT MAX(id)+1 FROM foo WHERE other = 123), ...);

그러나 테이블을 두 번 언급하였기 때문에 아래와 같이 에러를 반환하게 된다.

ERROR 1093 (HY000): Table ‘foo’ is specified twice, both as a target for ‘INSERT’ and as a separate source for data

따라서 변수를 이용하여 아래와 같이 만들어야 할 것이다.

BEGIN;
SELECT @id := MAX(id)+1 FROM foo WHERE other = 123;
INSERT INTO foo
    (other, id, ...)
    VALUES
    (123, @id, ...);
COMMIT;

다른 쓰레드에서 중복된 id값을 가져가는 것을 방지하기 위해 반드시 트랜잭션을 형성해야 한다.

 

 

mariaDB-Optimization and Tuning(OS)

1. Configuring Swappiness

Why to avoid swapping

  • mariaDB는 메모리가 스왑되지 않았다고 가정한다. 만약 swap이 발생한 상태라면 단순히 최초 access로 disk에서 로딩하는 것보다 성능에 더 좋지 않은 영향을 미친다.

 

스와핑을 피할 수 있는 가장 좋은 방법은 충분한 RAM을 확보하는 것이다. 시스템 변수를 너무 높게 잡으면 서버의 메모리가 부족해지면서 스왑을 일으킬 수도 있게 된다. 따라서 어떤 셋팅 값을 사용할 것인지, 이것이 메모리에 어떤 영향을 미칠지 이해하는 것이 매우 중요하다.

Setting swappiness on Linux

리눅스는 swappiness 라는 셋팅값을 가지고 있는데 이것은 RAM에서 하드디스크에 존재하는 스왑영역으로 스왑하는 정도를 의미한다. 0부터 100까지 설정이 가능하며 낮은 값은 잘 스왑하지 않는 다는 의미이다. 기본 값은 60이며 아래 커맨드로 확인해 볼 수 있다.

sysctl vm.swappiness

이 기본값은 서버로 하여금 스왑하도록 권장하고 있다고 볼 수 있다. 왠만해선 MariaDB에서 스왑하는 상황을 원치 않을 것이므로 이 값을 0으로 하는 것이 좋다. sysctl.conf 파일을  (대부분 /etc/sysctl.conf에 존재) 아래와 같이 swappniess를 0으로 만드는 설정을 추가하면 된다.

vm.swappiness = 0

/etc/sysctl.conf 파일에 적용하는 것은 리부팅 이후에도 지속된다.

데이터베이스 워크로드에는 낮은 수준의 swappiness를 추천한다. mariaDB 데이터베이스에 대해서 이 값을 1로 설정하는 것을 추천한다.

vm.swappiness = 1

 

Disabling swap altogether

스와핑을 완전히 비활성화 함으로써 데이터베이스 프로세스가 스와핑되는 것을 원천봉쇄하고자 하는 유혹이 있을 것이다. 그러나 조금의 스왑 기능을 남겨놓아야 실행중인 프로세스가 kill 되는 상황을 방지할 수 있다.

 

2. Filesystem Optimizations

Which filesystem is best?

사실 MariaDB 성능에 있어서 파일시스템은 중요한 포인트가 아니다. RAM이나 Disk drive의 속도, 시스템 변수의 셋팅이 더 중요하다고 볼 수 있다.

그러나 파일시스템의 최적화에 따라 큰 차이를 보일 수도 있다. 현재 리눅스 파일시스템 중 가장 훌륭한 성능을 보이는 것은 ext4와 XFS, Btrfs 이다. 이들은 모두 리눅스 커널에 포함돼있고 대부분의 리눅스 배포판에서 사용 가능하다. (Redhat제품에서 Brtfs는 미리 보기만 가능하고 아직 운영환경에 사용할 수 있는 준비는 되지 않음)

 

 

 

 

MariadB-Backup and Restore Overview

Logical vs Physical backups

로지컬 백업은 복구에 필요한 SQL문을 이용하는 것이고 피지컬 백업은 데이터파일이나 디렉터리를 통째로 복사하는 것을 의미한다. 이 둘은 아래와 같은 차이점이 있다.

  • 로지컬 백업이 좀 더 유연성이 있다. 다른 하드웨어에서도 복구할 수 있고 다른 mariaDB버전, 심지어 다른 DBMS에서도 복구할 수도 있다.
  • 로지컬 백업은 데이터베이스와 테이블 단위에서 실행되며 피지컬 백업은 디렉터리와 파일 단위에서 실행된다.
  • 로지컬 백업의 사이즈가 피지컬 백업보다 더 크다.
  • 로지컬 백업이 피지컬 백업보다 백업/복구 시간이 더 오래 걸린다.
  • 로그 파일과 구성파일은 로지컬 백업의 대상이 아니다.

Backup tools

mysqldump

mysqldump는 로지컬 백업을 수행하는 도구이며 백업/복구 하는데 있어 가장 유연성 있다. 데이터 사이즈가 상대적으로 작으면 가장 좋은 선택이라 할 수 있다.

더 큰 데이터 사이즈에서는 백업 파일이 커질 수 있으며 복구시간도 더 길어지게 된다. mysqldump는 SQL 형식(CSV나 XML형식으로도 가능) 으로 data를 덤프하기 때문에 다른 데이터베이스에 쉽게 임포트할 수 있다. 따라서 특정 DBMS에 맞춘 SQL문법만 아니면 MySQL이나 다른 DBMS로도 임포트할 수 있다.

mysqldump로 덤프를 뜨면 트리거는 자동으로 받아지지만 스토어드 프로시저나 뷰, 이벤트들은 명시적으로 옵션을 써야한다. (–routins, –events). 프로시저와 펑션은 시스템 테이블의 일부에 속한다. (예: mysql.proc)

InnoDB logical backups

로지컬 백업을 수행하면 테이블 Full scan을 유발하게 된다. 이것은 버퍼풀의 효율성을 떨어뜨릴 수도 있는데 이 문제를 회피할 수 있는 방법 중 한가지는 innodb_old_blocks_time 변수 값을 늘리는 것이다. 이 파라미터는 새로운 페이지가 버퍼 풀에 로딩되었을 때 LRU 알고리즘에서 recent측으로 옮겨가기 전 대기해야 하는 시간을 의미한다. 단위는 밀리 세컨드이며 기본값은 1000이다. 만약 0이면 재사용되는 즉시 recent 측으로 옮겨가게 된다. 따라서 이 시간을 늘리면 그만큼 recent 측으로 옮겨갈 가능성이 줄어드므로 금방 flush 될 수 있다.

또 다른 방법 하나는 innodb_old_block_pct 변수 값을 더 작게 하는 것이다. 그럼 LRU에 insert 되는 위치가 좀 더 old 영역에 가깝게 되기 때문에 금방 flush될 수 있다.

※ 로지컬 백업을 하면 버퍼 풀의 페이지가 많이 바뀔 수 있으므로 작업 전 버퍼 풀의 내용을 dump 떠넣는 것도 좋은 방법이다. innodb_buffer_pool_dump_now 변수를 ON하는 즉시 버퍼 풀을 dump뜨며 innodb_buffer_pool_load_now 변수를 ON하는 즉시 버퍼 풀을 복구한다.

Examples

백업하기

shell> mysqldump db_name > backup-file.sql

복구하기

shell> mysql db_name < backup-file.sql

 

mysqlhotcopy

mysqlhotcopy는 물리적으로 복제를 하는 방식이며 MyISAM과 ARCHIVE 테이블에 대해서만 수행할 수 있으며 원격서버로는 불가하기 때문에 스크립트를 수행하는 서버에 존재하는 데이터베이스에 대해서만 백업할 수 있다.
작동 방식은 FLUSH TABLES 를 하고 LOCK을 건 후 cp 명령으로 데이터 파일을 복사하는 것으로 매우 간단하지만 번거로운 수작업을 덜어주는 정도라 보면 되겠다.

Examples

shell> mysqlhotcopy db_name_1 ... db_name_n /path/to/new_directory

 

XtraBackup

빠르게 핫백업할 수 있는 오픈소스 툴이며 XtraDB와 InnoDB 데이터베이스를 위해 특별히 제작되었다. 그러나 다른 스토리지 엔진에도 사용할수는 있으며 mariaDB에는 기본적으로 포함돼있지 않다.

Filesystem snapshots

Veritas 같은 파일시스템은 스냅샷 기능을 제공하고 있으며 이 때 테이블은 반드시 lock 되어 있어야 한다. 순서는 아래와 같다.

  • FLUSH TABLES WITH READ LOCK 명령으로 버퍼 풀을 플러쉬시키고 락을 건다.
  • mount vxfs snapshot  명령으로 스냅샷을 시작한다.
  • 이제 UNLOCK TABLES 으로 락을 풀어줄 수 있으며 필요하다면 수행한다.
  • 스냅샷 파일을 카피한다.
  • umount snapshot 명령으로 스냅샷을 umount한다.

LVM

생략

Percona TokuBackup

생략

MariaDB-바이너리로그

바이너리로그를 활성화시키려면 mariaDB 시작시 –log-bin 옵션에 로그파일이름을 지정해주면 된다. 확장자를 포함하여 파일이름을 지정할 수 있지만 확장자는 그냥 무시된다. 만약 이름을 명시하지 않으면 기본 위치는 datadir/log-basename-bin 으로 되거나 data/mysql-bin으로 되거나 datadir/mariadb-bin 으로 된다. (앞의 두 개는 basename이 지정되지 않은 경우이며 mariadb의 버전에 따라 다르다). –log-basename 옵션을 통해 경로를 지정하거나 파일이름 지정시 전체 경로를 명시하는것을 추천한다. 그래야 호스트명이 바뀌거나 서버에 어떤 변경이 일어났을 때 replication(복제)가 중단되는 것을 방지할 수 있다.

바이너르 로그를 보관하는 디렉토리는 해당 로그 파일뿐만 아니라 로그 인덱스도 보유한다. 파일이름의 확장자로 연속적인 숫자가 오게 되는데, 바이너리 로그가 추가되면 숫자도 증가한다. mariaDB가 재기동되면 새로운 바이너리로그가 생성된다. 또는 최대사이즈에 도달했을 때도 새로운 바이너리 로그가 생성된다. (max_binlog_size에 의해 결정, 기본 1G) 인덱스 파일은 바이너리 로그에 대한 리스트를 가지고 있는 마스터 파일라고 할 수 있다. 바이너리 로그 디렉토리를 보면 아래와 같이 존재하는 것을 볼 수 있다.

shell> ls -l 
total 100
...
-rw-rw---- 1 mysql adm 2098 Apr 19 00:46 mariadb-bin.000079
-rw-rw---- 1 mysql adm  332 Apr 19 00:56 mariadb-bin.000080
-rw-rw---- 1 mysql adm  347 Apr 19 07:36 mariadb-bin.000081
-rw-rw---- 1 mysql adm  306 Apr 20 07:15 mariadb-bin.000082
-rw-rw---- 1 mysql adm  332 Apr 20 07:41 mariadb-bin.000083
-rw-rw---- 1 mysql adm  373 Apr 21 07:56 mariadb-bin.000084
-rw-rw---- 1 mysql adm  347 Apr 21 09:09 mariadb-bin.000085
-rw-rw---- 1 mysql adm  398 Apr 21 21:24 mariadb-bin.000086
-rw-rw---- 1 mysql adm  816 Apr 21 17:05 mariadb-bin.index

 

인덱스파일은 log-bin-index 파라미터를 이용해 이름을 명시할 수 있다. 바이너리 로그의 포맷은 statement기반, row기반, 두가지를 합친 것으로 총 3가지의 형식이 가능하다.

 

Using and Maintaining the Binary Log

Purging log files

모든 로그파일을 삭제하려면 RESET MASTER 명령을 사용해라. 특정 시간 이전이나 특정 넘버의 로그만 지우려면 PURGE BINARY LOGS 명령을 이용해라.

※ 슬레이브가 활성화되어 있고 삭제하려는 바이너르 로그를 읽고있다면 삭제할 수 없다. 그러나 슬레이브가 비활성화되어 있다면 삭제할 수 있다. 또한 삭제한 이후부터 슬레이브와 다시 연결하기 전까지는 replication이 이뤄지지 않는다.

또 expire_logs_days 파라미터에 의해 정해진 기간에 도달하면 삭제되기도 한다. 기본 값은 0이지만 day 단위로 값을 지정할 수 있으며 적어도 슬레이브 lag 시간 보다는 길게 설정해야 한다.

※ 인덱스 파일이 삭제되거나 인위적으로 잘못 수정되면, 위에 언급한 purge 작업은 실패하게 된다. 인덱스 파일은 텍스트 파일이기 때문에 직접 수정이 가능하며 현재 존재하는 로그파일에 대해 시간순으로 리스트를 담고있다.

Examples

PURGE BINARY LOGS TO 'mariadb-bin.000063';
PURGE BINARY LOGS BEFORE '2013-04-22 09:55:22';

 

Safely purging binary log files while replicating

바이너리 로그를 삭제하는 도중 replication이 깨지지 않도록 하려면 아래 절차를 잘 지켜라.

  1. SHOW BINARY LOGS 명령을 통해 현존하는 로그 파일의 리스트를 확인해라.(마스터에서)
  2. 각 슬레이브에서 SHOW SLAVE STATUS 명령을 통해 각각의 슬레이브가 현재 읽고 있는 로그 파일이 어떤 것인지 확인해라.
  3. 슬레이브들이 읽고 있는 것 중 가장 오래된 파일을 확인해라. 이 파일 이전의 로그파일은 필요없다.
  4. 필요하다면 로그 파일을 백업해둬라.
  5. 그리고 가장 오래된 파일로 확인했던 로그 파일 이전의 것들을 삭제해라.

 

Selectively logging to the binary log

기본적으로 모든 데이터, 데이터구조를 변화시키는 쿼리가 기록된다. –binlog-ignore-db=database_name 옵션이나 –binlog-do-db=database_name 옵션을 통해 로그를 남기지 않을 데이터베이스나 남길 데이터베이스를 선택할 수 있다.

※ 여러 DB를 명시할 때는 콤마를 쓰면 안된다(DB명에 콤마가 있을 수도 있기 때문). 대신 이 옵션을 여러번 반복해서 쓰면 된다.

–binlog-ignore-db 옵션이나 –binlog-d-db 옵션은 로그파일 포맷이 statement기반이냐, row기반이냐에 따라 조금씩 다르다. 로그파일 형식이 statement 기반일 경우엔 USE명령으로 사용하고 있는 기본 데이터베이스가 해당 옵션에 있는 경우에만 적용된다고 보면 된다. 가령 DB가 A,B,C,D 가 있고 파일포맷이 statement이며, ignore  (또는 do) 대상이 A,B 인 상황에서 내가 C 데이터베이스를 기본으로 사용하고 있다면 내가 입력하는 작업내용은 기록되지 않는 (또는 기록되는)것이다. 로그파일 형식이 row기반일 경우엔 기본 데이터베이스와 상관없이 적용된다.

 

Binary Log Formats

파일형식은 statement기반, row기반, 이 둘을 합친 것해서 총 3가지가 지원된다. statement기반이라고 해서 텍스트형식으로 저장되는 것이 아니라 기본적으로 항상 바이너리 형식으로 저장된다. 따라서 mysqlbinlog 라고 하는 CLI기반의 명령어를 통해 텍스트로 변환하여 볼 수 있다. 파일 형식은 binlog_format 시스템 변수를 통해 정할 수 있다.

Statement-based

파일 형식의 기본 설정이며 –binlog-format=STATEMENT 로 지정할 수 있다. 데이터나 테이블 구조에 변경을 가하는 모든 SQL구문을 기록한다. 어떤 경우엔 SQL구문이 확정적(Deterministic)이지 않은 경우가 존재하며 이 경우 replication이 안전하지 않게 된다. 이 경우 log_warning 시스템 변수에 정의된 곳에 경고문이 뜨게 된다.

Row-based

테이블의 개별적인 row가 영향받는 event를 기록한다. –binlog-format=ROW 로 지정한다.

Mixed

일단 statement-based가 기본적으로 사용되지만 replication에 안전하지 않은 statement라고 mariaDB가 판단하는 경우 row-based 방식으로 기록하는 방식이다. statement방식 대신 row방식으로 기록되는 경우는 아래와 같다.

  • INSERT-DELAYED 구문이 사용된 경우
  • AUTO_INCREMENT 컬럼을 가진 테이블이 업데이트, 트리거되거나 스토어드 함수가 사용된 경우
  • LOAD_FILE() 함수가 사용된 경우
  • ROW_COUNT() 함수나 FOUND_ROWS() 함수가 사용된 경우
  • USER()나 CURRENT_USER()함수가 사용된 경우
  • UUID() 함수가 사용된 경우
  • 구문에 포함된 테이블 중 하나가 mysql database의 로그 테이블일 경우
  • 구문이 시스템 변수를 참조하는 경우(세션단위에서 사용하는 것은 예외)
  • 유저정의 함수가 사용되는 경우

참고로 마스터와 슬레이브간 서로 다른 스토리지 엔진을 쓰는 것보다 같은 엔진을 사용할 때 에러가 줄어든다.

Changing the binary log format

로그포맷의 기본값은 statement기반이며 mariaDB 기동 중에 변경할 수 있다.

※ replication을 사용중에는 파일 포맷 변경하는 것을 조심해야 한다. 바이너리 로그 형식은 서버 자신에 의해서만 변경될 수 있다. 따라서 마스터에서 로그 포맷을 바꾸더라도 슬레이브의 로그 포맷에는 영향을 미치지 않으며 로그의 불일치를 가져올 수도 있다. 또한 Global 파라미터로 변경하더라도 현재 기동 중인 쓰레드를 보유한 세션에는 영향을 주지 않는다. 그리고 parallel 복제를 사용하는 환경에서 slave가 STOP SLAVE 명령 및 재기동되어도 slave_parallel_threads 값이 변경되기 전까지는 워커 쓰레드가 계속 기동하게 된다. 따라서 이때에는 slave_parallel_threads=0 으로 값을 변경해야 워커 쓰레드가 즉시 종료된다.

MariaDB 10.0.22 부터는 로그 형식과는 상관없이 마스터로에서 발생하는 어떤 이벤트도 적용할 수 있게 되었다.

비록 로그 포맷을 바꾸는 일은 거의 없겠지만 아래의 케이스는 알아두면 좋겠다.

  • 단일 구문 또는 적은 구문을 통한 update가 많은 수의 row를 업데이트할 때는 statement로깅기법이 더 효과적이다.
  • 많은 SQL문이 결과적으로 미미한 양의 row를 변경하는 경우 row로깅기법이 더 효과적이다.
  • 매우 오래 수행되지만 결과적으로 매우 적은 row가 변경되는 경우에도 row로깅기법이 더 효과적이다.

 

Binary Logging of Stored Routines

statement기반의 로깅을 사용하는 경우 마스터와 슬레이브간 서로 다른 결과를 만들어내는 SQL구문이 존재할 수도 있다. 스토어드 루틴같은 경우는 2가지 이유 때문에 이러한 경향이 더욱 짙다.

  1. 스토어드 루틴은 확정적(Deterministic)이지 않다. 다른 말로 repeatable하지 않다는 의미로 실행될 때마다 다른 결과 값을 만들어낸다.
  2. 슬레이브에서 스토어드 루틴을 실행하는 쓰레드는 전체 권한을 가지고 있는 반면, 마스터에서는 그렇지 않은 경우가 있다.

리플리케이션에서 발생하는 문제는 오직 statement기반의 로깅에서만 일어난다. 만약 row기반의 로깅이 사용되면, 마스터의 row를 기반으로 변경사항이 전달되기 때문에 sync가 어긋날 가능성이 완전히 사라지게 된다.

기본적으로 row기반의 리플리케이션을 사용하면, 마스터에서 트리거를 수행시켜 슬레이브로 해당 내용이 전달되도록 한다. 그러나 MariaDB 10.1.1부터는 슬레이브에서도 트리거를 수행시킬 수 있게 되었다.

How MariaDB handles statement-based binary logging of routines

이번 장에서 설명하는 내용은 statement기반의 로깅이 활성화된 케이스에만 해당한다.

  • 바이너리 로그가 활성화된 상태에서 스토어드 펑션이 만드려면 반드시 DETERMINISTIC이나 NO SQL 또는 READS SQL DATA 를 선언해주어야 한다. 안그러면 에러가 발생한다.
  • mariaDB는 펑션이 확정적(Deterministic)인지 판단하지 않으며 대신 올바른 정의가 사용되었는지를 본다.(?)
  • 스토어드 펑션을 만드려면 정규권한 외에도 SUPER 권한이 필요하다.
  • log_bin_trust_function_creators=1로 설정되면 위에서 언급한 조건은 필요없다.
  • 트리거…

 

PURGE logs

PURGE { BINARY | MASTER } LOGS { TO ‘log_name’ | BEFORE datetime }

명시한 로그파일명 혹은 날짜값 이전에 있는 로그파일을 삭제한다. 인덱스 파일내 목록에서도 삭제되므로 명시했던 파일명이 첫 번째로 존재하는 로그파일이 된다.

삭제하기 전 SHOW BINARY LOGS 명령을 통해 로그 리스트를 확인하고, 어떤 로그파일이 읽히고 있는지를 확인해보려면 SHOW SLAVE STATUS 명령을 이용하면 된다. 슬레이브가 읽고 있는 파일 중 가장 old한 것보다 더 이전에 생긴 로그파일부터 지울 수 있다.

전체 로그파일을 다 삭제하려면 RESET MASTER 명령을 날리면 된다. (여태까지의 로그파일이 모두 삭제되고 새로운 로그파일부터 시작함)

Examples

PURGE BINARY LOGS TO 'mariadb-bin.000063';
PURGE BINARY LOGS BEFORE '2013-04-21';
PURGE BINARY LOGS BEFORE '2013-04-22 09:55:22';

 

 

 

 

 

 

 

14.4 InnoDB Configuration

14.4.1 InnoDB Initialization and Startup Configuration

InnoDB의 구성할 때 가장 처음 결정했던 것은 데이터파일을 어떻게 표현할 것인가와 InnoDB 스토리지 엔진을 위한 메모리를 어떻게 할당할 거인가였다. 당신은 configuration 파일에 값을 넣어 MySQL이 시작하면서 읽도록 할 수 있으며 CLI 옵션으로도 지정할 수 있다.

Overview of InnoDB Tablespace and Log Files

InnoDB 스토리지 엔진에 의해 관리되는 것 중 가장 중요한 디스크 기반의 리소스를 꼽으라면 테이블스페이스 데이터 파일과 로그 파일이다. configuration 파일에서 따로 명시하지 않았다면 MySQL은 자동으로 증가하고 12MB보다 약간 큰 데이터 파일인 ibdata1과 로그파일 2개(ib_logfile0, ib_logfile1)을 데이터 디렉토리에 생성한다. 이 로그 사이즈의 크기는 innodb_log_file_size 파리미터에 의해 결정된다. 좋은 성능을 위해 파라미터의 값을 명시적으로 조절할 필요가 있다. 물론 당신의 하드웨어와 요구사항에 맞게 조절해야 한다.

Considerations for Storage Devices

어떤 케이스에서는 데이터파일이 서로 다른 물리적인 디스크에 위치해야 더 좋은 성능이 나온다. 로그 파일을 데이터 파일과 다른 디스크에 구성하는 것은 매우 자주 쓰이며 효과적인 방법이다. 다음 예제는 어떻게 하는지를 보여준다. 두 개의 데이터 파일을 서로 다른 디스크에 위치시키며 로그 파일 또한 세 번째 다른 디스크에 위치시킨다. InnoDB는 테이블스페이스를 채울 때 데이터파일의 첫 번째 데이터 파일을 사용한다. 그리고 I/O를 향상시키 위해 raw device를 InnoDB의 데이터 파일로 사용할 수도 있다.

Caution
InnoDB는 트랜잭션이 보장되는 엔진이다. 그러나 OS나 하드웨어가 그러한 요구사항을 받아주지 못한다면 무용지물이 된다. 많은 OS와 disk 시스템이 성능을 위해 write 동작을 delay시키거나 reorder 시키고 있다. 어떤 OS에서는 fsync() 시스템 콜이 모든 데이터가플러쉬 될 때까지 기다려야함에도 완료되기 전에 return하는 경우가 있다. 이 때문에 OS crash나 정전등으로 인해 최근 commit 된 데이터가 유실되는 경우나 write 동작이 reorder됨으로써 데이터베이스가 깨지는 경우도 있다. 만약 데이터의 정합성이 중요하다면 프로덕션에 적용하기 전 pull-the-plug 테스트를 해봐라. OS X 10.3 보다 높은 경우 InnoDB는 플러쉬를 위해 특별한 fcntl() 라는 함수를 사용한다. Linux에서는 write-back cache를 비활성화 하는 것을 고려해볼만 하다 (CPU 캐시에만 update하고 메모리에는 update하지 않아 데이터 일관성이 맞지 않을 수 있다)

ATA/SATA 디스크에서는 write-back cache를 비활성화하는 게 불가능할 수도 있다. 어떤 drive나 디스크 컨트롤러에서도 write-back cache를 비활성화하는게 불가능한 경우가 있으니 유의해라.

유저의 데이터를 보호함에 관련하여 복구 능력을 향상시켜주는 것이 존재하는데 InnoDB는 doublewrite buffer라고 불리는 파일 플러쉬 기술이 존재한다 (여러 개의 dirty page를 각각의 데이터파일에 플러쉬하려면 랜덤I/O가 발생하여 시간이 오래 걸리므로 해당 dirty page를 시스템 테이블스페이스 내 하나의 연속적인 extent에 때려넣음으로써 단일 I/O로 저장하는 방법, 이렇게 저장이 되면 정상적인 플러쉬 과정을 한다.) innodb_doublewrite파라미터는 기본적으로 ON인데 서버 crash나 정전 등으로 인한 사고에서 더 안전하게 복구할 수 있도록 해준다. 또한 대부분의 unix 장비에서 fsync() 시스템 콜의 요청을 줄여줌으로써 성능도 향상시키게 된다. 따라서 innodb_doublewrite 옵션을 활성화된 상태로 놔둘 것을 권고하는 바이다.

Specifying the Location and Size for InnoDB Tablespace Files

InnoDB의 테이블스페이스 파일을 셋업하려면 [mysqld] 세션에서 innodb_data_file_path 파라미터를 수정해라. 이 파라미터에는 하나 이상의 데이터 파일에 대한 리스트가 있어야 하며 세미콜론(;)으로 구분해주면 된다.

innodb_data_file_path=datafile_spec1[;datafile_spec2]...

예를 들어 아래와 같이 작성하면 된다.

[mysqld]
innodb_data_file_path=ibdata1:12M:autoextend

이것은 자동 증가하는 ibdata1 라는 하나의 12MB짜리 파일을 명시하는 것이다.위치에 대한 내용이 없으므로 자동으로 데이터 디렉토리에 생서하게 된다. 자동 증가하는 50M짜리의 두 개의 데이터 파일을 명시하려면 아래와 같이 쓰면 된다.

[mysqld]
innodb_data_file_path=ibdata1:50M;ibdata2:50M:autoextend

만약 자동증가 옵션을 명시했다면 InnoDB는 테이블스페이스가 꽉 차면 자동으로 증가시키며 한번에 8MB씩 증가시킨다.  (MariaDB에서는 64로 정의되어 있음) 만약 이 값을 수정하려면 innodb_autoextend_increment 파라미터를 수정하면 된다.

InnoDB는 파일시스템 내 최대 파일 사이즈를 알지 못한다. 따라서 파일시스템 내 최대 파일의 사이즈가 적당히 작은지 잘 모니터링 해야 한다. 자동 증가하는 데이터 파일의 최대 사이즈를 지정하기 위해서는 autoextend 속성 다음에 max 속성으로 지정할 수 있다. max 속성은 데이터 사용량이 매우 중요한 이유가 있는 경우에만 사용해라. 아래 예제는 ibdata1이 최대 500MB까지만 커질 수 있도록 제한을 두는 예제이다.

[mysqld]
innodb_data_file_path=ibdata1:12M:autoextend:max:500M

그리고 위치를 지정하려면 innodb_data_home_dir 파라미터를 작성해라. 아래 예제는 /ibdata 아래에 데이터파일을 생성하는 예제이다.

[mysqld]
innodb_data_home_dir = /ibdata
innodb_data_file_path=ibdata1:50M;ibdata2:50M:autoextend

Note
InnoDB는 디렉토리를 만들지 않기 때문에 /ibdata 디렉토리가 존재하는지 미리 확인해라. 이것은 로그 파일을 지정할 때도 마찬가지이다. 그리고 mysql이 해당 디렉토리에 적당한 권한이 있는지 잘 확인해라.

innodb_data_home_dir 파라미터를 빈 칸으로 놔두고 innodb_data_file_path 파라미터에서 데이터 파일을 절대 경로로 표시할 수 있다.

[mysqld]
innodb_data_home_dir =
innodb_data_file_path=/ibdata/ibdata1:50M;/ibdata/ibdata2:50M:autoextend

어떤 파일시스템에서는 데이터 파일의 크기가 2G보다 작아야 한다는 것을 명심해라.

 

14.4.3.1 Configuring InnoDB Buffer Pool Prefetching (Read-Ahead)

read-ahead 요청이란 페이지들이 곧 사용될 것이란 예측을 하고 비동기적으로 여러 개의 페이지를 버퍼 풀로 미리 올려놓는 것을 의미한다. 즉 하나의 익스텐트에 있는 모든 페이지를 한꺼번에 올려놓는 것이다. InnoDB는 I/O 성능을 향상시키기 위해 두 개의 read-ahead 알고리즘을 갖고 있다.

  • Linear: 버퍼 풀에서 순차적으로 접근하는 페이지를 보고 어떤 페이지가 곧 사용될 것인지를 판단하는 기법이다. 언제 read-ahead 가 작동될 것인지는 얼마나 많은 연속적인 페이지가 접근되는지를 기준으로 삼으며 innodb_read_ahead_threshold 파라미터로 조절할 수 있다. 이 파라미터가 있기 전에는 현 익스텐트의 마지막 파이지를 읽으면서 다음 익스텐트 전체를 미리 꺼내야 하는지만을 계산했었다.innodb_read_ahead_threshold 파라미터는 InnoDB가 순차적인 페이지 접근 패턴에 대해 얼마나 민감한지를 결정한다. 만약 이 파라미터에 정의된 값보다 더 많은 수의 순차적 페이지 접근이 발생한다면 InnoDB는 비동기 read-ahead 를 동작시켜 전체 익스텐트를 가져온다. 파라미터의 값은 0~64까지 올 수 있ㅇ며 기본 값은 56이다. 이 값을 더 높이면 그만큼 접근 패턴에 대해 엄격하다는 뜻이다. 에를 들어 이 값은 48개로 낮추면 현 익스텐트에서 48개의 page만큼만 순차적 접근이 발생하면 linear read-ahead를 동작시킨다.
  • Random: read-ahead 는 이미 버퍼 풀에 있는 페이지에 대해 페이지가 읽히는 순서에 상관없이 언제 페이지가 필요할 것인지를 예측하는 것이다. 만약 버퍼 풀에서 동일한 익스텐트 내 13개의 연속적인 페이지만 발견되어도 InnoDB는 익스텐트의 남은 페이지에 대해 미리 비동기적으로 가져오는 것이다. 이것을 활성화하려면 innodb_randmon_read_ahead 기능을 ON해라.SHOW ENGINE INNODB STATUS를 보면 read-ahead 알고리즘이 얼마나 효과적인지 확인할 수 있다.

 

14.4.3.2 Configuring the Rate of InnoDB Buffer Pool Flushing

InnoDB는 어떤 task들은 백그라운드로 동작시킨다. 가령 dirty page를 백그라운드로 내려보내는 것이 있다. InnoDB는 innodb_max_dirty_pages_pct 로 정의한 값보다 버퍼 풀 내에 더티 페이지의 비율이 커지면  플러쉬 시킨다.

InnoDB는 적당한 플러쉬 속도를 측정하는 알고리즘을 갖고 있는데 리두로그 발생량과 현재의 플러쉬 속도를 기반으로 측정한다. 이 방법의 목적은 버퍼 풀의 상태를 클린하게 유지하면서 적당한 플러쉬 속도를 유지하도록 만듦으로써 전반적으로 안정적인 성능을 만드는데에 있다. 플러쉬 속도를 자동적으로 조절하는 것은 과도한 버퍼 풀 플러쉬가 발생하여 정상적인 R/W을 방해하는 갑작스런 사고를 방지하는데 도움이 된다.

InnoDB는 로그파일을 원형 방식으로 사용한다. 로그 파일을 재사용하기 전에 InnoDB는 해당 로그파일이 가지고 있는 리두 엔트리에 대한 부분을 버퍼 풀에서 디스크로 플러쉬시킨다. 이 과정을 sharp 체크포인트라고 한다. 만약 워크로드가 write의 비중이 크다면 많은 로그를 발생시킬 것이며 모두 리두 로그에 쓰이게 될 것이다. 만약 리두 파일의 공간이 모두 사용되면 sharp 체크포인트가 발생하게 되어 가용한 성능이 잠깐 줄어들게 된다. 이 현상은 innodb_max_dirty_pages_pct에 도달하지 않더라도 발생한다.

InnoDB는 이 현상을 회피하기 위해 버퍼 풀 내에 더티 페이지의 비율과 리두 로그의 발생 속도를 측정한 값을 기반으로 휴리스틱 기반의 알고리즘을 사용한다. 이 값들을 가지고 버퍼 풀에서 초당 플러쉬해야 하는 더티 페이지의 개수를 결정한다. 이 자율적응 알고리즘은 워크로드가 급변하는 상황에서도 효과적으로 쓰일 수 있다.

이 알고리즘을 내부적으로 벤치마킹한 결과 성능을 일정하게 잘 유지할 뿐만 아니라 전반적인 성능을 매우 향상시켰다.

적응적 플러쉬 기법은 워크로드의 I/O 패턴에 매우 큰 효과를 미치기 때문에 innodb_adaptive_flushing 파라미터를 통해 이 기법을 비활성화 하는 방법도 존재한다.

 

14.4.3.3 Making the Buffer Pool Scan Resistant

엄격한 LRU 알고리즘을 사용하기 보다 InnoDB는 버퍼 풀로 로딩되었다가 두 번 다시는 쓰이지 않을 데이터의 양을 최소화하는 기술을 사용하고 있다. 이 기술의 목표는 비록 read-ahead나 full table 스캔으로 인해 어쩌면 재사용하지 않을 수도 있는 페이지라도 가급적 자주 억세스되는 hot 페이지를 선별하는 것이다.

새로 읽히는 블럭은 LRU의 중간에 insert된다. 새로 읽히는 모든 페이지들은 기본적으로 LRU 리스트의 3/8 위치에 insert된다. 버퍼 풀에서 페이지가 처음으로 읽히면 정면 방향(최근 사용된 것들 있는 방향)으로 이동한다. 그러므로 절대 사용되지 않은 페이지는 절대 LRU의 정면 방향으로 이동할 수가 없게 된다. 그리고는 엄격한 LRU 도달에 의해 age out 된다. 이 배열 방법은 LRU 리스트를 두 개의 세그먼트로 나누는데 insert 포인트보다 아래에 있는 페이지들을 old라 간주하고 LRU에서 방출 대상의 희생양으로 삼는다.

InnoDB의 버퍼 풀 작동 방식이나 LRU 알고리즘의 자세한 특성을 알고싶으면 8.10.1의 The InnoDB Buffer Pool 을 참조해라.

LRU list의 insert 포인트를 조절할 수 있는데 테이블 스캔이나 인덱스 스캔에도 동일하게 적용할 것인지도 결정할 수 있다. innodb_old_blocks_pct 파라미터는 old 블럭의 비중을 결정한다. 기본 값은 37로써 3/8에 100을 곱한 값이다. 5부터 95까지 설정할 수 있다. 5로 설정하면 버퍼 풀로 새로 insert된 페이지는 매우 빨리 age out 된다. 95로 설정하면 버퍼 풀의 5% 만이 hot 페이지로 설정되어 일반적인 LRU와 비슷한 구조를 갖게 된다.

read-ahead로 인해 버퍼 풀이 지저분해지는 것을 막아주는 이 최적화 방식 테이블 및 인덱스 스캔 페이지로 버퍼풀이 지저분해지는 것을 방지해주기도 한다. 이런 스캔에서 올라온 데이터 페이지의 경우 대부분 짧은 시간 내에 몇 번 억세스 되고 그 이후로는 접근되지 않는 경우가 많다. innodb_old_blocks_time 파라미터는 처음 페이지가 억세스 되고 ?? 이 파라미터의 기본 값은 1000이며 이 값을 증가시키면 버퍼 풀에서 더 빨리 age out 된다.

innodb_old_blocks_pct 파라미터와 innodb_old_blocks_time 파라미터는 모두 글로벌 변수이고 운영중에 변경할 수 있다.

아래는 이 파라미터들의 효과에 대해 추정하는데 도움을 줄 수 있는 자료이다. show engine innodb status 명령에서 나온 BUFFER POOL AND MEMORY 섹션이다. (MariaDB의 조회한 내용을 가져옴)

Total memory allocated 138412032; in additional pool allocated 0
Total memory allocated by read views 200
Internal hash tables (constant factor + variable factor)
 Adaptive hash index 2217568 (2213368 + 4200)
 Page hash 139112 (buffer pool 0 only)
 Dictionary cache 681082 (554768 + 126314)
 File system 822512 (812272 + 10240)
 Lock system 333232 (332872 + 360)
 Recovery system 0 (0 + 0)
Dictionary memory allocated 126314
Buffer pool size 8191
Buffer pool size, bytes 134201344
Free buffers 7901
Database pages 290
Old database pages 0
Modified db pages 0
Percent of dirty pages(LRU & free pages): 0.000
Max dirty pages percent: 75.000
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 186, created 109, written 351
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 290, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
  • Old database page: LRU 리스트의 old에 해당하는 페이지를 의미한다.
  • Pages made young, not young: young또는  young하지 않게 된 old 페이지의 총 개수
  • youngs/s and non young/s: old 페이지가 young 하게 된 초당 속도와 young하지 않게 된 초당 속도이며 InnoDB 상태를 마지막으로 확인해 본 시점간의 비교를 통해 계산한다.

하드웨어 조건, 데이터, 워크로드의 형태에 따라 이 파라미터가 가져오는 효과의 변동이 매우 크므로 운영 환경에 적용하기 전에 반드시 벤치마크를 통해 이 파라미터 변경이 가져오는 효과를 검증해야 한다.

버퍼 풀에 다 들어올 수도 없을 만큼 큰 규모의 테이블을 스캔할 때는 innodb_old_blocks_pct의 값을 작게 함으로써 한번 읽히고 말 페이지가 버퍼 풀의 비중을 많이 차지하지 않도록 해준다. 예를 들어 이 값을 5로 설정하면 한번 읽히고 말 페이지는 버퍼 풀의 5%만 사용할 수 있게 된다.

메모리에 딱 들어맞는 작은 규모의 테이블을 스캔할 때에는, 페이지를 버퍼 풀로 옮기는데 큰 비용이 들지 않기 때문에 innodb_old_blocks_pct의 값을 그냥 놔두던지 더 높게해도 된다.

innodb_old_blocks_time 파라미터의 효과는 innodb_old_blocks_pct 파라미터의 효과보다 예측하기가 어렵다. 그리고 상대적으로 효과도 미미하고 워크로드의 형태에 따라 효과의 격차도 크다. 따라서 innodb_old_blocks_pct 파라미터로 효과를 거두지 못한 경우에 벤치마크 테스트를 고려해 보아라.

 

14.4.3.4 Using Multiple Buffer Pool Instances

버퍼 풀이 수 기가 바이트에 해당하는 시스템의 경우 버퍼 풀을 몇 개의 인스턴스로 구분하는 것이 동시성을 향상시킬 수 있다. 캐시된 페이지에 대한 읽기/쓰기 시 분리된 쓰레드를 이용함으로써 경합을 감소시키게 된다. 이 특성은 버퍼 풀의 사이즈가 수 기가 바이트에 해당하는 시스템을 목표로 만들어진 것이다. 멀티플 버퍼 풀 인스턴스는 innodb_buffer_pool_instance 파라미터를 통해 조절할 수 있다.

만약 버퍼 풀의 사이즈가 크면 대부분의 데이터 요청이 메모리 안에서 해결됨으로써 만족하게 될 것이다. 그러나 여러 개의 쓰레드가 버퍼 풀을 한꺼번에 억세스하려고 시도함에 따라 병목구간에 빠지게 될 수도 있다. 멀티플 버퍼 풀을 활성화함으로써 이 경합을 최소화할 수 있는데 버퍼 풀에 저장되거나 read되는 페이지는 해시함수에 의해 버퍼 풀 중 하나에 임의적으로 할당된다. 각각의 버퍼 풀은 free리스트, flush리스트, LRU리스트등 버퍼 풀에 관련된 데이터 구조체들을 각자 관리한다.

멀티플 버퍼 풀을 활성화하려면 innodb_buffer_pool_instance 파라미터를 1에서 64 사이의 값을 주면 된다. 다만 이것이 효과를 발휘하려면 innodb_buffer_pool_size의 값이 1G 이상이 되어야 한다. 지정한 값은 total size로 이것은 각각의 버퍼 풀로 쪼개지게 된다. 최고의 성능을 위해서 각각의 버퍼 풀이 최소 1G 이상이 되도록 innodb_buffer_pool_instance와 innodb_buffer_pool_size 파라미터를 알맞게 명시하도록 하라.

 

14.4.3.5 Preloading the InnoDB Buffer Pool for Faster Restart

 

 

14.4.3.6 Tuning InnoDB Buffer Pool Flushing

innodb_flush_neighbors나 innodb_lru_scan_depth 파라미터는 버퍼 풀 플러쉬 과정을 좀 더 세밀하게 튜닝할 수 있는 방법을 제공해준다. 이 옵션은 주로 write 비율이 높은 워크로드에 큰 도움이 된다. 플러쉬의 속도가 충분치 않아 버퍼 풀에 과도한 메모리 사용율을 초래하거나, 플러쉬 속도고 너무 과도하여 I/O 용량을 모두 소모시키는 일이 발생할 수 있다. 이상적인 셋팅은 워크로드 환경, 데이터의 억세스 패턴, 스토리지 구성에 따라 달려있다 (HDD냐 SSD냐 등)

지속적으로 무거운 워크로드가 발생하는 환경이나 긴 간격으로 워크로드가 변동하는 시스템에서 아래의 파라미터는 플러쉬 방법을 정밀하게 튜닝할 수 있는 방법을 제공해준다.

innodb_adaptive_flushing_lwm
innodb_max_dirty_pages_pct_lwm
innodb_io_capacity_max
innodb_flushing_avg_loops

위의 파라미터들은 innodb_adaptive_flushing 옵션에 의해 사용되는 공식을 구성하는 파라미터이다. 파라미터간 서로를 제한하거나 확장하는 관계를 가진 파라미터는 아래와 같다.

innodb_adaptive_flushing   –> innodb_adaptive_flushing_lwm
innodb_io_capacity                –> innodb_io_capacity_max
innodb_max_dirty_pages_pct –> innodb_max_dirty_pages_pct_lwm

  • InnoDB의 adaptive flushing 메커니즘은 모든 케이스에 대해 잘 작동하진 않는다. 단지 리두 로그가 위험 수준에 도달했을 때 가장 큰 혜택을 준다. innodb_adaptive_flushing_lwm 파라미터는 리두 로그의 사용량에 대한 비율로써 한계값을 지정한다. 만약 리두로그의 사용량이 이 값을 넘어서면 innodb_adaptive_flushing 파라미터를 OFF해도 adaptive flushing 기법이 사용된다.
  • 만약 adaptive flushing 기법을 사용해도 충분치 않다면 InnoDB는 innodb_io_capacity 파라미터를 이용하여 좀 더 공격적으로 플러쉬시킨다. innodb_io_capacity_max 파라미터는 비상 상황에서 사용되는 사용할 수 있는 I/O의 한계를 의미한다. 이로써 비상 상황에서도 I/O를 모두 소비하지 않도록 제한을 둘 수 있다.

  • InnoDB는 버퍼 풀에서 더티 페이지를 플러쉬 시킴으로써 더티 페이지의 비율이 innodb_max_dirty_pages_pct를 초과하지 않도록 한다. 이 파라미터의 기본 값은 75이다.Note
    innodb_max_dirty_pages_pct 파라미터는 플러쉬 활동의 목표치만 제공할 뿐이지 플러쉬 속도에는 영향을 주지 않는다.innodb_max_dirty_pages_pct_lwm 옵션은 더티 페이지가 innodb_max_dirty_pages_pct 비율에 도달하지 않도록 미리미리 flushing 하도록 하는 최소한의 더티 페이지 비율을 의미한다. 기본 값은 0.001이며 0인 경우 미리 플러쉬하지 않는다.

innodb_flushing_avg_loops 파라미터는 InnoDB가 얼마나 많은 구 버전의 플러쉬 상태 계산 캡쳐본을 유지할 지를 결정하는 파라미터이다. 이것은 adaptive flush가 워크로드의 변화에 얼마나 빨리 반응하는지를 결정하는 파라미터이다. 이 값을 높게 설정하면 이전에 계산해놓은 스냅본이 더 오래 유지되므로 adaptive 응답속도가 좀 더 느려진다. 또한 백그라운드와 포그라운드의 양적 피드백 관계를 약화시키게 된다. 따라서 이 파라미터를 높게 잡았을 경우 InnoDB의 리두로그 사용율이 75%에 도달하지 않도록 하고 innodb_max_dirty_pages_pct의 세팅이 워크로드에 적당한 더티 페이지의 비율을 유지하고 있는지 반드시 확인하여야 한다.

innodb_log_file_size 의 값이 크고 리두 로그의 사용율이 75%에 도달하지 않는 일정한 워크로드가 있는 시스템의 경우 반드시innodb_flushing_avg_loops 의 값을 높게 잡아야 한다. 그래서 플러쉬가 가능한 한 부드럽게 일어나도록 해야 한다. 워크로드가 극단적으로 튀는 시스템이나 로그 파일의 크기가 충분치 않은 환경에서는 이 값을 작게 잡아야 한다. 그래야 부하에 따라 플러쉬 속도가 맞춰서 작동되며 리두 로그의 사용율이 75%에 도달하지 않도록 관리하는데 도움이 된다.

 

14.4.3.7 Configuring InnoDB Buffer Pool Size

버퍼 풀의 사이즈는 startup 시 조절할 수도 있고 운영 중에도 조절할 수도 있다. 이 장에서 설명하는 내용은 두 가지 방법에 모두 해당되는 내용이다.

만약 innodb_buffer_pool_size 파라미터를 통해 버퍼 풀의 사이즈를 줄이면 이 명령은 청크 상태로 동작한다. (MariaDB에는 청크 단위가 없음)

 

 

 

 

Ch14.3 InnoDB Transaction Model and Locking

InnoDB의 트랜잭션 모델의 목표는 다른 멀티버저닝 데이터베이스의 best 특성들을 두 가지 락킹으로 묶어 구현하는 것이다. InnoDB는 오라클처럼 row 레벨 락을 사용하며 쿼리 수행시 락을 유발하지 않는 consistent 읽기방식을 이용한다. 락 정보는 매우 공간 효율적으로 저장되기 때문에 lock 에스컬레이션이 필요없다. 몇몇의 유저가 매 row마다 락을 걸거나 어떤 임의의 집합 단위로 row 락을 걸어도 메모리 고갈이 유발되지 않는다.

InnoDB에서 모든 유저의 활동은 트랜잭션 안에 존재한다. 만약 자동커밋모드가 활성화되어 있으면 각각의 SQL문은 스스로 하나의 트랜잭션을 형성한다. 기본적으로 MySQL은 세션마다 자동커밋 모드를 활성화시킨 채 커넥션을 맺어주는데, 해당 세션에서 발생된 SQL문이 에러를 리턴하지 않는 한 항상 자동 커밋을 한다. 만약 에러를 리턴한다면 에러 종류에 따라 커밋을 할지, 롤백을 할지가 결정된다. 자세한 내용은 14.9.4 “InnoDB Error Handling”을 봐라.

자동커밋모드가 활성되어 있어도 여러 개의 SQL문을 묶어 하나의 트랜잭션으로 만들 수 있다. 명시적으로 START TRANSACTION 또는 BEGIN ~ COMMIT/ROLLBACK 문을 사용하면 된다. 자세한 내용은 13.3.1 “START TRANSACTION, COMMIT, and ROLLBACK Syntax”을 봐라.

만약 autocommit = 0 으로 설정하여 자동커밋 기능을 끈다면 해당 세션이 만든 트랜잭션은 항상 열려있게 된다. COMMIT이나 ROLLBACK 구문이 현 트랜잭션을 끄거나 시작할 수 있다.

COMMIT의 의미는 현 트랜잭션에 의해 수정된 내용이 영구적으로 저장되고 다른 세션에게도 보여지게 된다는 뜻이다. 반면 ROLLBACK은 현 트랜잭션에 의해 수정된 내용을 취소한다는 의미이다. COMMIT이나 ROLLBACK 모두 현 트랜잭션에 의해 잡혀있던 락을 놓아주게 된다.

SQL: 1992 트랜잭션 고립화 수준에 따르면 InnoDB는 REPEATABLE READ를 따르고 있다. InnoDB는 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE으로 설명되는 4가지 트랜잭션 고립화 수준을 모두 지원하고 있다.

유저는 SET TRANSACTION 구문을 통해 고립화 수준을 변경할 수 있다. 서버의 기본 고립화 수준을 설정하려면서버 시작시 –transaction-isolation 옵션을 이용하거나 옵션파일에서 설정할 수 있다.

행 단위 락킹에서 InnoDB는 next-key 락킹을 사용한다. InnoDB는 gap 락도 사용하는데 이것은 gap 사이에 인덱스화된 값을 insert할 수도 있는 구간 사이를 다른 세션이 insert하지 못하도록 구간을 락킹하는 것이다. next-key 락은 해당 레코드와 이전 레코드 구간에 대해 락을 거는 것이다.?? gap 락은 구간만 락을 거는 것이다.

row-level 락에 대해 궁금하다면, 그리고 gap locking이 무효화되는 환경이 궁금하다면 14.3.2 “InnoDB Record, Gap, and Next-Key Locks”을 봐라.

공간 인덱스와 관련된 동작에서 락을 통제하기 위해, repeatable read, resializable 트랜잭션 고립화 수준에서 next-key 락은 작동하지 않는다. 이 경우 예측 lock을 사용한다. 14.3.6의 “Predicate Locking for Spatial Indexes”을 봐라.

 

14.3.1 InnoDB Lock Modes

InnoDB는 두 가지 표준 row-level 락을 구현하고 있는데 shared lock(S)과 exclusive lock(X) 을 사용하고 있다. record락, gap락, next-key락등에 대해선 다음 절을 봐라.

  • Shared lock(S)은 트랜잭션이 row를 읽는 것을 허용한다.
  • Exclusive lock(X)은 트랜잭션이 update하거나 delete 하는 것을 허용한다.

만약 트랜잭션 T1이 row r에 대해서 S락을 갖고 있다면 동일한 row r에 대한 다른 트랜잭션 T2의 요청은 아래와 같이 처리된다.

  • T2가 S lock을 요청한다면 해당 락을 즉시 얻을 수 있다. 그 결과 T1와 T2는 모두 row r에 대해 S락을 얻게 된다.
  • T2가 X lock을 요청한다면 락을 즉시 얻을 수 없다.

만약 트랜잭션 T1이 row r에 대해 X락을 얻었다면 T2는 S락이나 X락 모두 즉시 얻을 수 없다. T2는 T1이 row r에 대해 락을 놓을 때까지 기다려야 한다.

Intention Locks

덧붙여서 InnoDB는 레코드에 대한 락과 테이블에 대한 락이 공존하는 ‘multiple granularity lock”을 지원한다. 이것을 실제적으로 지원하기 위해 intention 락이라는 타입을 사용한다. intention락은 일종의 테이블 락인데 트랜잭션이 테이블 내부 row에 대해 어떤 종류의 락(S 혹은 X)을 걸고 작업할 것인지를 알려주는 역할을 한다. InnoDB에는 두 가지의 intention 락이 존재한다.

  • Intention shared(IS): 트랜잭션 T가 테이블 row에 대해 S락을 걸 것임을 알림
  • Intention Exclusive(IX): 트랜잭션 T가 row에 대해 X락을 걸 것임을 알림

SELECT … LOCK IN SHARE MORE는 IS 락을 거는 것이며
SELECT … FOR UPDATE는 IX 락을 거는 것이다.

intention 락 프로토콜은 아래와 같다.

  • 트랜잭션이 테이블t 내부 row에 대해 S락을 걸려면, 먼저 테이블 t에 대해 IS락이나 더 강한 락을 획득해야 한다.
  • row에 대해 X락을 걸려면 테이블 t에 대해 반드시 IX락을 먼저 획득해야 한다.

이상 설명한 rule은 lock type 호환표로 정리할 수 있다.

X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

현재 걸려있는 락과 호환 가능하면 트랜잭션은 락을 얻을 수 있다. 그러나 현재 걸려있는 락과 충돌이 발생하면 얻을 수 없다. 그렇게 되면 락을 얻으려는 트랜잭션은 락이 풀릴 때까지 대기해야 한다. 만약 락 충돌이 발생하고 데드락 때문에 즉시 락을 얻을 수 없는 것이라면 에러가 발생한다.

그러므로 intention 락은 Full 테이블 요청(LOCK TABLES … WRITE)을 제외하고는 어떠한 것도 막지 않는다. IX와 IS 락의 가장 큰 목적은 누군가가 row 락을 걸어놨거나 락을 걸으려 한다는 것을 보여주려고 함에 있다.

Deadlock Example

아래 예제는 lock 요청이 어떻게 데드락을 발생시키는지 보여준다. 클라이언트 A와 B가 있다고 해보자. 먼저 A는 1개의 row를 갖는 테이블을 만들고 트랜잭션을 시작하였다. 트랜잭션에서 A는 row에 S락을 설정하였다.

mysql> CREATE TABLE t (i INT) ENGINE = InnoDB;
Query OK, 0 rows affected (1.07 sec)

mysql> INSERT INTO t (i) VALUES(1);
Query OK, 1 row affected (0.09 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM t WHERE i = 1 LOCK IN SHARE MODE;
+------+
| i    |
+------+
|    1 |
+------+

그리고 클라이언트 B는 트랜잭션을 시작하면서 이 값을 지우려고 하고 있다.

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> DELETE FROM t WHERE i = 1;

삭제 작업은 X 락이 필요하다. 이 락은 A가 쥐고 있는 S 락과 호환되지 않기 때문에 대기하게 된다. 따라서 해당 요청은 row과 block과 함께 lock request 큐로 옮겨가게 된다.

그 후 A가 delete를 시도하려고 할 때 아래와 같은 메시지를 보게 된다.

mysql> DELETE FROM t WHERE i = 1;
ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction

A가 삭제를 하려면 X 락이 필요하게 되므로 데드락이 발생한다. B는 A가 잡고 있는 S락이 풀리기를 기다리며 A는 B가 잡은 X락이 풀리기만을 기다리고 있으므로 충돌이 발생하는 것이다. 결과적으로 InnoDB는 클라이언트 중 한 명에게 에러를 뿌리며 락을 놓아주게 된다. 해당 클라이언트는 아래와 같은 메시지를 받게 된다.

ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction

그러면 다른 한 명은 락을 획득하여 row를 삭제할 수 있게 된다.

 

14.3.2 InnoDB Record, Gap, and Next-Key Locks

InnoDB는 레코드락 외에도 gap 락, next-key 락등 여러 락을 가지고 있다.

  • 레코드 락: 인덱스 레코드를 잠그는 것(인덱스란 clustered index)
  • Gap 락: 인덱스 레코드 사이를 잠그는 것
  • 레코드 락과 gap 락을 함쳐놓은 것

Record Locks

테이블을 인덱스 없이 생성했어도 레코드 락은 항상 인덱스 레코드에 대해서 잠금을 한다. 왜냐하면 InnoDB는 클러스터드 인덱스를 보이지 않게 가지고 있기 때문에 이 인덱스에 레크드 락킹을 구현하는 것이다.

Next-key Locks

기본적으로 InnoDB는 REPEATABLE READ 트랜잭션 고립화 수준을 유지한다. 이 경우 InnoDB 유저는 인덱스를 스캔하는데 next-key 락을 사용하여 팬텀 row 를 방지할 수 있게 된다.

next-key 락킹은 인덱스 row 락과 gap 락을 합친 것이다. InnoDB는 테이블을 스캔할 때 이런 방식으로 row 수준 락킹을 적용한다. 따라서 쿼리에 따라 마주치는 레코드에 대해 shared 또는 exclusive 락을 걸어둔다. 결국 row 수준 락은 인덱스 레코드 락이라 할 수 있다. 덧붙여서 인덱스 레코드 에 대한 next-key 락은 해당 레코드에 대한 gap에도 영향을 미친다. 왜냐하면 next-key 락은 레코드 락과 레코드 앞에 있는 대상에 락을 거는 gap락을 합친 것이기 때문이다. 만약 어떤 세션이 레코드 R 에다가 shared, 또는 exclusive 락을 걸었다면 다른 세션은 레코드 R보다 인덱스 순서상 앞에 있는 레코드에 대해 새로운 값을 insert 할 수 없다.

인덱스가 값 10, 11, 13, 20을 가지고 있다고 해보자. 이 인덱스에 대해 next-key를 설정할 수 있는 구간은 아래와 같다. ] 표시는 해당 숫자까지 포함됨을 의미한다.

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

마지막 구간에서는 인덱스에서 가장 큰 값(20)과 어떤 값보다도 큰 가상의 “supremum”이라는 가상 레코드 구간 사이를 잠근다. 이 “supremum”은 사실 인덱스에 존재하지 않는 값이기 때문에 ??

Gap Locks

위에서 본 next-key 예제은 gap의 구간이 하나의 인덱스 값이 될 수도 있으며, 여러 개의 인덱스 값 구간이 될 수도 있고, 심지어 비어있을 수도 있다는 것을 보여준다.

Gap 락킹은 유니크 인덱스로 단일의 유니크 row를 검색하는 SQL구문에는 필요없다. 예를 들어 id 컬럼이 유니크 인덱스를 가질 때, 아래와 같은 값이 100인 row에 인덱스 레코드 락이 사용되며 다른 세션에서 이 값보다 작은 값을 insert하는 것을 허용한다.

SELECT * FROM child WHERE id = 100;

만약 id 가 인덱스를 갖고 있지 않거나 유니크하지 않다면 SQL은 앞선 gap구간에 락을 건다. 여기서 호환되지 않는 락이 다른 트랜잭션에 의해 얻어질 수도 있음을 유념해야 한다. 예를 들어 트랜잭션 B가 exclusive gap 락을 걸어놓은 구간에 트랜잭션 A가 shared gap 락을 걸수도 있다는 것이다. gap 락이 동시에 걸릴 수 있는 이유는 만약 어떤 레코드가 인덱스에서 삭제되면, 다른 트랜잭션에 의해 걸려있던 gap 락과 합쳐져야 하기 때문이다..?????

InnoDB에서 gap 락은  “순전히 억제하는” 기능을 갖고 있다. 즉 gap 사이에 다른 트랜잭션이 insert 하는 것을 막는 역할을 하는 것이다. 다른 트랜잭션이 같은 구간에 또 다른 gap 락을 거는 것을 막지 않는다. 그러므로 gap X-lock은 gap S-lock과 똑같은 효과를 보인다.

gap 락의 한 종류 중 insert intention gap 락이라고 불리는 gap 락은 insert 구문에서 row에 값을 삽입하기 전에 설정된다. 이 락은 insert시에 여러 개의 트랜잭션이 동일한 구간 내에서 똑같은 위치에 insert하는 것만 아니라면 서로가 대기하는 현상이 필요없음을 알려준다. insert 레코드가 4부터 7까지 있는 것을 생각해보자. 각각의 트랜잭션이 값 5와 6을 insert하려고 한다고 해보면, 각각은 insert하려는 row에 대한 exclusive 락을 얻기 전에 4부터 7 구간에 대해 insert intention 락을 건다. 그러나 서로가 insert 하려는 row가 다르기 때문에 충돌을 일으키지 않는다.

아래 예제는 값을 insert하기 위한 exclusive 락을 얻기 이전에 insert intention 락을 얻는 과정을 보여준다.

클라이언트 A가 두 개의 레코드(90과 102)가 있는 테이블을 만든 후 id가 100보다 큰 레코드에 대해 exclusive 락을 얻으려고 하고 있다. 이 exclusive 락은 레코드 102 이전에 대해 gap lock을 포함한다.

mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);

mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
+-----+
| id  |
+-----+
| 102 |
+-----+

클라이언트 B가 이 구간 사이로 값을 insert하려고 한다고 해보자. 이 트랜잭션은 insert intention lock을 얻지만 exclusive lock을 얻기 위해 대기하게 된다.

mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);

insert intention lock을 보기 위해 SHOW ENGINE INNODB STATUS 명령을 쳐봐라. TRANSACTIONS 부분에 아래와 같은 내용을 볼 수 있을 것이다. (생략)

Disabling Gap Locking

gap 락은 명시적으로 해제시킬 수 있다. 트랜잭션 고립화 수준을 READ COMMITTED로 변경하던지innodb_locks_unsafe_for_binlog 파라미터(지금은 비권장하는)를 활성화시켜라. 이런 환경에서는 검색이나 인덱스 탐색에서는 gap 락이 쓰이지 않게 되며, FK 제약조건 체크나 중복키 를 체크하는 환경에서만 쓰이게 된다.

그리고 트랜잭션 고립화 수준을 READ COMMITTED로 변경하는 것이나 innodb_locks_unsafe_for_binlog 파라미터를 활성화 시켰을 경우 발생하는 또 다른 특이사항이 있다. 바로 SQL의 where 조건에 매칭되지 않는 row는 즉각 레코드 lock이 풀린다는 것이다.  update 구문에서 InnoDB는 “반정합성” read를 한다. 즉 마지막으로 커밋된 버전을 리턴하기 때문에 update절에서 where 조건에 부합되는지를 판단할 수 있는 것이다.

 

14.3.3 Avoiding the Phantom Problem Using Next-Key Locking

phantom 읽기라고 일컫는 문제는 똑같은 쿼리를 다른 시간(T1과 T2)에 수행했을 때 결과 집합이달라지는 것을 말한다. 예를 들어 SELECT문이 두 번 수행됐을 때 첫 번째 읽어을 때는 없던 값이 두 번째 읽을 때는 보이는 것을 의미하며 이것을 “유령” row라고 한다.

child 테이블의 id 컬럼에 PK가 구성되어 있고 아래와 같이 100보다 큰 값 이상에 대해 차후 업데이트할 의향으로 조회하고 락을 걸고 싶다고 가정해보자.

SELECT * FROM child WHERE id > 100 FOR UPDATE;

이 쿼리는 id가 100보다 큰 첫 번째 값을 시작점으로 인덱스를 스캔하기 시작할 것이다. 여기서 테이블에 id 값이 90과 102가 있다고 가정해보자. 만약 스캔한 인덱스 레코드 중 gap 구간(여기서는 90부터 102까지를 의미함)에 대한 insert를 막지 않는다면, 다른 세션이 id=101을 가지고 insert를 할 수 있을 것이다. 그래서 당신이 이 트랜잭션 안에서 똑같은 SELECT를 다시 한다면 id=101이라는 새로운 row(“phantom”)을 보게 될 것이다. 이 row 결과값을 데이터 항목이라고 한다면 새로운 phantom row로 인해, 트랜잭션은 항상 실행 가능해야 하고 트랜잭션 내에서 읽은 값은 변하면 안된다는  원칙을 위반하는 것이다.

phantom 현상을 막기 위해서 InnoDB는 next-key 락킹이라고 부르는 매커니즘을 사용하는데 이것은 index-row 락킹과 gap 락킹을 합쳐 놓은 것이다. InnoDB는 테이블 인덱스를 검색하거나 스캔할 때, 마주치는 레코드에 shared 락이나 exclusive 락을 걸어놓는 방식으로 row레벨 락킹을 구현하고 있다. 그러므로 row level 락킹은 사실상 인덱스 레코드 락이라고 할 수 있다. 이 말은 next-key 락은 index-record 락 더하기 인덱스 앞 레코드와의 gap에 대한 gap 락이라고 할 수 있다.만약 어떤 세션이 index 레코드 R에 대해서 shared나 exclusive 락을 걸었을 때, 다른 세션은 인덱스 순서상 R보다 앞에 있는 구간에 대해 새로운 값을 즉시 insert할 수 없다.

InnoDB가 인덱스를 스캔하면서 인덱스의 마지막 값 다음 구간(gap)에 대해 락을 걸 수도 있다. 아까 보았던 예제에서 이런 현상이 발생할 수 있다: id가 100보다 큰 값으로 insert 하려는 시도를 막으려면 id=102 부터의 구간에 대해 락을 걸어야 한다.

어플리케이션에서 유일성 체크를 구현하기 위해 next-key 락킹을 사용할 수 있다:  만약 당신이 share 모드에서 데이터를 읽고 insert하려고 할 때 중복된 값을 발견하지 않아 안전하게 해당 row를 insert할 수 있고 내가 insert 하려는 값 이후의 구간에 대해 next-key 락이 걸려있기 때문에 아무도 insert하거나 중복시킬 수 없다는 것을 알고 있다. 그러므로 next-key 락킹은 테이블에 존재하지 않는 값에 대해 락을 거는 셈이다.

gap 락킹을 비활성화 하는 것을 앞장에서 배웠다. 이것을 비활성화하면 다른 세션이 새로운 row를 insert할 수 있기 때문에 phantom 문제가 발생할 수 있다는 것을 명심해라.

 

14.3.4 Consistent Nonlocking Reads

일관성 읽기란 InnoDB가 멀티 버저닝을 이용하여 쿼리가 발행된 시점에서의 데이터베이스의 스냅샷을 구성하여 쿼리해오는 것을 의미한다. 쿼리는 해당 시점 전에 commit 된 트랜잭션으로 인해 변경된 값을 본다. 그리고 쿼리가 날라온 시간 이후에 변경된 데이터나 아직 커밋되지 않은 데이터는 읽지 않는다. 이 법칙의 예외사항이 하나 있는데 동일 트랜잭션 안에서 먼저 수행된 SQL과 다른 변경사항을 보는 것이다. 이 예외사항은 아래와 같은 이유 때문에 발생한다: 만약 테이블에서 어떤 rows를 update하면 SELECT 구문은 update된 마지막 버전의 row를 보게된다. 그러나 다른 예전 버전을 보게 될 수도 있다. (?? 이게 몬소리?) 만약 다른 세션이 동시에 해당 테이블을 업데이트하였다면, 데이터베이스에서 존재하지 않았던 테이블의 상태를 보게 되는 것이다.

만약 트랜잭션 고립화 수준이 REPEATABLE READ로 되어 있다면, 해당 트랜잭션에서 발행된 첫 번째 read 에 대해 snapshot을 만들고 이 snapshot을 읽음으로써 트랜잭션 안에서의 일관성 읽기가 가능하다. 만약 새로운 버전의 스냅샷을 만들고 싶다면 현 트랜잭션을 commit하고 다시 새로운 쿼리를 발행하면 된다.

READ COMMITTED 고립화 수준에서는 트랜잭션 내부의 일관적 읽기는 각각의 새로운 스냅샷을 생성하고 참조한다.

일관성 읽기는 InnoDB의 기본 모드이고 READ COMMITTED나 REPEATABLE READ 고립화 수준에서 SELECT 구문을 사용할 때 항상 사용되는 모드이다. 일관성 읽기는 테이블에서 억세스되는부분에 대해 락을 걸지 않으며 일관성 읽기가 수행되는 시점에서 테이블을 수정하는 것이 가능하다.

REPEATABLE READ 읽기 모드에서 일반적인 SELECT 구문을 날렸다고 가정해보자. InnoDB는 당신이 데이터베이스를 쿼리하고 있는 그 시점에 따른 timepoint를 가지고 트랜잭션을 생성한다. 만약 다른 트랜잭션이 row를 삭제하고 commit해도 당신은 row가 삭제된 것을 보지 못한다. insert나 update도 비슷하게 작용한다.

Note
데이터베이스의 스냅샷은 트랜잭션 내부에서 SELECT 구문에서 생성되며 DML 구문에서는 생성되지 않는다. 만약 당신이 트랜잭션에서 어떤 row를 insert하거나 update하고 commit 했는데 다른 트랜잭션이 DELETE나 UPDATE를 발행한다면 방금 당신이 commit한 그 row가 영향 받을 수도 있다. 어떤 트랜잭션이 다른 트랜잭션에 의해 이미 commit된 row를 update하거나 insert 했을 때 이 변경사항을 바로 눈으로 확인할 수 있다.

현재의 트래잭션을 commit하고 다른 SELECT나 START TRANSACTION WITH CONSISTENT SNAPSHOT 문을 명시함으로써 timepoint를 갱신할 수 있다.

이것을 MVCC(멀티버전 동시성 제어)라고 한다.

아래 예제에서 세션 A는 B가 row를 insert하고 커밋함과 더불어 A 세션 역시 커밋을 해야 B가 insert한 값을 볼 수 있음을 나타낸다. 따라서 timepoint는 B가 이전에 발행한 commit 시점으로 갱신된 것이다.

             Session A              Session B

           SET autocommit=0;      SET autocommit=0;
time
|          SELECT * FROM t;
|          empty set
|                                 INSERT INTO t VALUES (1, 2);
|
v          SELECT * FROM t;
           empty set
                                  COMMIT;

           SELECT * FROM t;
           empty set

           COMMIT;

           SELECT * FROM t;
           ---------------------
           |    1    |    2    |
           ---------------------

만약 가장 최근 상태의 데이터베이스를 보려면 READ COMMITTED 고립화 수준을 사용하던지 locking read를 사용해라.

SELECT * FROM t LOCK IN SHARE MODE;

READ COMMITTED 고립화 수준에서 트랜잭션 내부의 일관성 읽기 쿼리는 각각 최신본의 스냅샷을 셋팅하고 읽는다. LOCK IN SHARE MODE에서 락 읽기는 이렇게 된다: SELECT 하면서 만든 가장 최신본의 row의 트랜잭션이 끝날 때까지 다른 세션들을 block한다.

일관적 읽기는 명확한 DDL 구문에서는 동작하지 않는다.

  • 일관성 읽기는 drop table에서 작동하지 않는다. 왜냐하면 MySQL이 테이블을 drop하고 InnoDB가 테이블을 파괴하기 때문(destroy)
  • ALTER TABLE에서도 작동하지 않는다. 왜냐면 ALTER문은 임시 테이블을 구성하고 작업이 완료되면 원본 테이블을 지워버리기 때문이다. 만약 당신이 트랜잭션 안에서 일관성 읽기를 재발행해도 스냅샷에 row가 존재하지 않기 때문에 새로운 테이블의 row가 보이지 않게 된다. 이러한 경우 트랜잭션은 에러를 보낸다: ER_TABLE_DEF_CHANGED “Table definition has changed, please retry transaction”.

FOR UPDATE나 LOCK IN SHARE MODE를 명시하지 않은 SELECT를 이용 다양한 구문들, 가령
INSERT INTO .. SELECT
UPDATE … (SELECT)
CREATE TABLE … SELECT

  • 기본적으로 InnoDB는 강한 락을 사용하며 SELECT 부분을 READ COMMITTED로 수행한다. 비록 같은 트랜잭션 내에서라도 해당 일관성 읽기는 최신본의 스냅샷을 생성하고 참조한다.
  • 이런 경우에 일관성 읽기를 사용하려면 innodb_locks_unsafe_for_binlog 파라미터를 활성화하고 트랜잭션 고립화 수준을 SERIALIZABLE 외에 3가지 중 아무거나 하나로 설정해라. 그러면 select한 테이블에서 어떤 row에도 락을 걸지 않는다.

 

14.3.5 Locking Reads (SELECT … FOR UPDATE and SELECT … LOCK IN SHARE MODE)

만약 당신이 동일 트랜잭션에서 데이터를 조회하고 관련된 값을 업데이트하고자 한다면, 단순히 SELECT 구문은 안정성을 보장하기에는 충분치 않다. 다른 트랜잭션이 당신이 쿼리했던 똑같은 row를 update하거나 delete할 수 있다. 그래서 InnoDB는 추가적인 안정장치를 위해 두 가지의 locking read를 제공하고 있다.

  • SELECT … LOCK IN SHARE MORE
    읽은 row에 shared 모드의 락을 걸어둔다. 다른 세션은 읽을 수는 있으나 당신이 commit하기 전까지는 데이터를 수정할 수 없다. 만약 다른 트랜잭션이 commit하지 않은 채 데이터를 수정했다면 이 트랜잭션이 끝날 때까지 당신이 기다려야 한다.
  • SELECT … FOR UPDATE 는 인덱스 레코드에 락을 걸어 update한 것과 마찬가지의 효과를 준다. 다른 트랜잭션은 해당 row를 업데이트할 수 없으며 SELECT … LOCK IN SHARE MODE로 읽거나 특정 고립화 수준에서 읽는 것도 막히게 된다. 그러나 일관성 읽기는 레코드의 락을 무시한다 (레코드의 구버전 데이터는 락에 걸릴 수 없다. 구버전 데이터는 메모리 상에 존재하는 레코드의 복사본인 undo log로부터 재구성된 것이다)

이 장은 tree 구조나 graph 구조의 데이터, 혹은 단일 테이블이나 데이터가 여러 개의 테이블에 걸쳐 있는 경우 특히 유용할 것이다. ??

LOCK IN SHARE MODE나 FOR UPDADE로 인해 생긴 락 세트는 commit하거나 롤백할 때 해제된다.

Note
SELECT FOR UPDATE는 autocommit이 비활성화 되었을 때만 적용된다 (혹은 START TRANSACTION으로 시작했을 때) 만약 autocommit이 활성화되면 해당 row에 락이 걸리지 않는다.

Usage Examples

테이블 child에 새로운 값을 insert 한다고 가정해보자. 그리고 child의 부모key에 해당하는 parent 테이블이 있다고 해보자. 당신의 어플리케이션 코드는 참조 완결성이라는 것을 지켜야 한다.

첫 번째로, PARENT  테이블을 읽관성 읽기를 하고 row 세트가 존재함을 확인한다. 이때 당신은 안심하고 child 테이블로 insert 할 수 있겠는가? 아니다. 왜냐하면 당신의 select와 insert 동작 사이에 누군가 parent 테이블의 값을 삭제했을 수도 있기 때문이다.

이러한 잠재적인 문제를 방지하기 위해, LOCK IN SHARE MODE로 select를 수행해라.

SELECT * FROM parent WHERE NAME = 'Jones' LOCK IN SHARE MODE;

LOCK IN SHARE MODE로 parent 테이블의 ‘Jone’을 쿼리한 후에 당신은 child 테이블에 안전하게 insert하고 commit할 수 있다. 당신이 수정하려고 하는 parent 테이블의 row를 읽거나 write하려고 하는 어떤 트랜잭션도 당신이 완료할 때까지 기다려야 한다. 즉 모든 테이블의 데이터가 일관적인 상태에 놓인 것이다.

다른 예를 들어보자. CHILD_CODES라는 테이블에 정수를 카운트하는 필드가 존재하며 이것이 CHILD 테이블에 존재하는 각각의 row에 대해 유니크한 식별자를 부여하는 역할을 하고 있다고 해보자. 이때 일관적 읽기나 SHARE MODE 읽기로 카운터의 현재 값을 읽으면 안된다. 왜냐면 두 유저가 카운터의 같은 값을 볼 가능성이 있으며 child 테이블의 row에 대해 똑같은 식별자를 중복생성할 수 있기 때문에 duplicate-key 에러가 발생하게 된다.

여기서는 LOCK IN SHARE MODE는 좋은 선택이 아니다. 왜냐면 두 유저가 카운터를 동시에 읽었을 경우 적어도 한 명은 카운터를 업데이트하려고 시도하는 데드락 상황에 빠지게 된다.

카운터를 읽고 증가시키는 것을 구현하기 위해서는 FOR UPDATE로 일단 읽고 카운터를 증가시켜야 한다.

SELECT counter_field FROM child_codes FOR UPDATE;
UPDATE child_codes SET counter_field = counter_field + 1;

SELECT … FOR UPDATE는 가장 최신의 데이터를 읽고 읽은 row에 X락을 걸어둔다. 그러므로 이것은 UPDATE가 row에 걸어둘 락과 동일한 락을 걸어두는 것이다.

위 예제는 다만 어떻게 SELECT … FOR UPDATE가 이용되는지를 보여준 것이다. 사실 MySQL에서 유일 식별자를 생성하는 것은 테이블의 단일 접근을 통해 구현할 수 있다.

UPDATE child_codes SET counter_field = LAST_INSERT_ID(counter_field + 1);
SELECT LAST_INSERT_ID();

이 SELECT 부분은 단지 식별자 정보를 받아올 뿐이며 (현재의 커넥션에 한해서) 테이블에 접근하지는 않는다.

 

14.3.7 Locks Set by Different SQL Statements in InnoDB

락모드로 읽기나 update, delete 같은 구문들은 인덱스 스캔 중에 만나는 모든 row에 index 레코드 락을 건다. 심지어 where 조건에 해당하지 않는 레코드에도 락을 건다. InnoDB는 where 조건을 기억하지 못하며 스캔한 인덱스 범위만을 알고있다. 락은 보통 next-key 락을 의미하며 이것은 레코드보다 앞에 있는 구간에 대해 insert를 하지 못하도록 즉각적으로 막는 역할을 한다. gap 락킹은 명시적으로 비활성화할 수 있으며 이 경우 next-key 락킹도 사용하지 못하게 된다. 트랜잭션 고립화 수준 역시 어떤 락이 세팅될지 영향을 준다.

만약 세컨더리 인덱스를 탐색하고 인덱스 레코드 락에 X락이 걸린다면 InnoDB는 관련된 클러스터드 인덱스 레코드를 복구하고 락을 건다.

만약 SQL 구문에 적당한 인덱스가 없어서 전체 테이블을 스캔해야 한다면 테이블의 모든 row에 락이 걸리게 되며, 다른 유저가 테이블에 insert 하는 것을 막게 된다. 따라서 테이블에 적당한 인덱스를 잘 만들어놔야 불필요하게 많은 row를 스캔하지 않게 된다.

SELECT … FOR UPDATE나 SELECT … LOCK IN SHARE MODE 같은 쿼리는 scan한 row에 대해 락을 걸며 결과 집합에서 조건에 해당하지 않는 row에 대해선 락을 해제한다. 그러나 몇몇의 케이스에서 보았듯이 쿼리 실행동안에 원본 소스와 결과 집합간의 관계가 실종되기 때문에 락이 바로 해제되지 않을 수 있다. 예를 들어, UNION 동작은 테이블을 스캔하고 (그리고 락을 건다) row를 임시 테이블에 넣어 결과세트를 평가하는 작업을 한다. 이런 환경에서 임시  테이블에 있는 row와 원본 테이블에 있는 관계가 끊어지게 되며 앞단의 row들은 쿼리 실행이 끝날 때까지 lock이 풀리지 않을 수 있다.

InnoDB는 락의 타입을 아래와 같이 정의하고 있다.

  • SELECT … FROM
    일관성 읽기이며 데이터베이스로부터 스냅샷을 만든다. 트랜잭션 고립화 수준이 SERIALIZABLE이 아닌 이상 락을 걸진 않는다. SERIALIZABLE인 경우 row에 shared next-key 락을 건다.
  • SELECT … FROM …LOCK IN SHARE MODE
    검색하는 과정에서 마주친 모든 레코드에 shared next-key 락을 건다.
  • SELECT … FROM … FOR UPDATE 구문은 LOCK IN SHARE MODE나 특정 트랜잭션 고립화 수준에서의 읽기와 호환되지 않는다. 일관성 읽기는 락 셋팅을 무시한다.
  • UPDATE … WHERE … 는 exclusive next-key 락을 건다.
  • DELETE FROM … WHERE … 도 마찬가지로 exclusive next-key 락을 건다.
  • INSERT는 insert된 row에 exclusive 인덱스레코드 락을 건다. (next-key 락이 아니며 이말은 gap lock도 없다는 것을 의미) 다른 세션이 insert된 row 보다 앞에 insert하는 것을 막지 않는다.insert 하기에 앞서 insertion intention gap 락이라 불리는 gap 락이 셋팅된다. 이것은 다른 여러 트랜잭션이 구간내에 똑같은 값으로 insert 하지만 않는다면 대기할 필요없이 진행할 수 있다는 것을 알려주는 일종의 시그널이다. 인덱스 레코드가 4와 7이 있다고 생각해보자. 서로 다른 트랜잭션이 5와 6일 insert 하려고 하며 insert 하기 위한 X락을 얻기 전에 각각 4와 7 구간을 lnsert intention lock을 걸게 된다. 이 경우 서로 insert 하려는 값이 다르기 때문에 서로 막지 않게 된다.

    만약 중복키 에러가 발생한다면 해당 중복된 인덱스 레코드에 S락이 걸린다. S락은 건 후 여러 개의 세션이 똑같은 값을 insert하려고 시도하면서 서로 X락을 대기하는 현상 때문에 S락은 데드락을 유발할 수도 있다. 데드락은 어떤 세션이 값을 삭제하면서도 생길 수 있다. 테이블 T1이 아래와 같은 구조를 가지고 있다고 생각해보자.

    CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;

    3개의 세션이 차례대로 아래와 같은 작업을 하고 있다.

    session 1:

    START TRANSACTION;
    INSERT INTO t1 VALUES(1);

    session 2:

    START TRANSACTION;
    INSERT INTO t1 VALUES(1);
    session 3:
    START TRANSACTION;
    INSERT INTO t1 VALUES(1);

    session 1:

    ROLLBACK;

    세션 1에 의한 첫 번째 동작에 의해 해당 row에 대해 X락을 걸린다. 세션 2와 세션 3에 의한 동작은 중복키 에러가 발생하게 되며 두 세션 모두 row에 대해 shared 락을 요구하게 된다. 세션 1이 rollback했을 때 row에 대해 X락을 풀게 되어 세션2와 3이 S락을 얻을 수 있게 된다. 이제 세션 2와 3이 데드락 상황에 빠지게 된다. 둘 다 S락을 얻은 상태이기 때문에 X락을 얻을 수 없다.

    비슷한 상황으로 테이블이 이미 1을 가지고 있고 세 개의 세션이 아래와 같은 동작으로 수행하려 한다고 가정해보자.

    session 1:

    START TRANSACTION;
    DELETE FROM t1 WHERE i = 1;
    
    session 2:
    START TRANSACTION;
    INSERT INTO t1 VALUES(1);
    
    session 3:

    START TRANSACTION; INSERT INTO t1 VALUES(1);

    session 1:
    COMMIT;

    세션 1은 row에 대해 X락을 얻게된다. 세션 2와 3의 동작은 중복키 에러를 발생하면서 row 대해 S락을 기다리게 된다. 세션1이 commit을 하면서 X락이 풀리고 세션 2와 3은 큐에 대기하고 있던 S락을 획득하게 된다. 이 시점에서 세션 2와 3에 데드락이 걸리게 된다. 두 세션 모두 서로가 갖고 있는 S락으로 인해 X락을 획득할 수 없다.

 

  • INSERT … ON DUPLICATE KEY UPDATE
    X락을 거는 단순한 INSERT와는 좀 다르다. 중복키로 인해 update가 되는 부분에서는 S락이걸린다.
  • REPLACE
    유일키에 대해 충돌되지 않으면 INSERT와 동일하며 중복키에 대해 X락이 걸린다.
  • INSERT INTO T SELECT … FROM S WHERE …
    테이블 T에 대해 insert 되는 row에 대해서는 exclusive 인덱스 레코드 락이 걸린다(gap lock이 아님) 만약 트랜잭션 고립화 수준이 READ COMMITTED이거나 innodb_locks_unsafe_for_binlog가 활성화되고 고립화 수준이 SERIALIZABLE이 아니면 S에 대해서는 일관성 읽기를 한다(락을 걸지 않음) 그렇지 않으면 S에 대해 S next-key 락을 건다.

 

14.3.8 Implicit Transaction Commit and Rollback

기본적으로 MySQL은 autocommit 이 활성화된 상태로 커넥션을 맺어준다. 그래서 쿼리 결과에 오류만 없으면 항상 커밋된 결과를 돌려받게 된다. 만약 에러가 있다면 커밋 또는 롤백하게 된다.

그리고 만약 autocommit 이 비활성화된 상태에서 명시적인 commit없이 종료된 세션에 대해서자동 롤백된다.

 

14.3.9 Deadlock Detection and Rollback

InnoDB는 데드락을 자동적으로 감지하여 한개 혹은 다수의 트랜잭션을 롤백시킬 수도 있다. 먼저 규모가 작은 트랜잭션을 골라 롤백시키는데 규모의 기준은 insert혹은 update 혹은 delete 하는 row의 숫자를 보고 판단한다.

InnoDB는 innodb_table_locks = 1 / autocommit = 0 이 설정된 경우 테이블의 락을 감지하게 되며 MySQL 레이어 윗단에서는 row-lelvel의 락 여부도 알 수 있다. 반면에 InnoDB는 MySQL이 설정한 LOCK TABLES구문이나 InnoDB가 아닌 다른 스토리지 엔진에 거린 데드락의 현황에 대해서는 알지 못한다. 이 문제를 해결하기 위해선 innodb_lock_wait_timeout 파라미터를 조절해야 한다.

InnoDB가 트랜잭션에서 롤백을 완료하면 트랜잭션에 의해 잡혔던 모든 락이 해제된다. 그러나 단일 SQL구문이 에러에 의해 롤백되었다면 이 구문에 의한 몇몇의 락은 여전히 걸려있을 수도 있다. 이 현상은 락이 어떤 구문에 의해 걸렸는지 나중에 알 수 없는 형식으로 InnoDB가 락 정보를 저장하기 때문이다.

만약 SELECT 구문이 스토어드 펑션을 call하였는데 해당 펑션 내부에 있는 SQL구문에서 fail이 발생했다면 해당 SQL 구문은 롤백된다. 게다가 만약 롤백이 그 이후에 수행된다면 전체 트랜잭션이 롤백된다.

 

14.3.10 How to Cope with Deadlocks

 

 

 

 

 

Ch14.2 InnoDB Table and Index Structure

14.1.1 InnoDB as the Default MySQL Storage Engine

InnoDB는 아래와 같은 특징이 있다.
– 트랜잭션이 보장된다(ACID)
– row-level lock이 가능하다.
– 오라클처럼 여러 유저가 동시에 읽는 것이 가능하다(Undo를 이용하여)
– PK에 기반한 공통 쿼리에 대해서는 데이터를 클러스터화된 인덱스에 저장하여 I/O를 절감
– 기본 엔진이기 때문에 테이블 생성시 ENGINE 절을 별도로 명시하지 않으면 기본엔진으로 InnoDB가 선택된다.

InnoDB의 장점
– 서버 Crash 후 기동시 자동으로 복구해준다(redo이용)
– 버퍼 풀을 이용하여 자주 access되는 테이블/인덱스를 캐시할 수 있다.
– PK/FK 등을 이용한 엄격한 스키마 구성이 가능하다.
– 데이터 충돌이 발생하면 checksum 메커니즘에 의해 사용자에게 인폼해준다.
– 테이블에 대해 PK를 구성하면 자동적으로 해당 컬럼은 최적화된다. where, order by, group by, join에서 매우 뛰어난 성능을 발휘할 수 있다.
– change buffering 이라는 메카니즘으로 인해 DML구문이 최적화된다.
– 자주 access되는 row는 adaptive hash index라고 불리는 방식으로 변환되어 마치 hash table에 접근하는 것처럼 접근속도가 더욱 빨라진다.
– 테이블과 인덱스 압축이 가능하다.
– 성능과 가용성에 영향이 덜 가는 인덱스 삭제가 가능하다.
– file_per_table 테이블 스페이스의 truncate가 가능하며 OS에서도 자동 삭제해준다.
– DYNAMIC row형식의 사용으로, BLOB과 Long 텍스트 필드의 저장방식이 더 효과적이다.
– INFORMATION_SCHEMA 테이블을 쿼리함으로써 내부 상황을 모니터링할 수 있다.

최근에 추가된 InnoDB의 개선사항
 – FULLTEXT 인덱스 타입을 이용하여 전문검색이 가능하다.
– read-only나 대부분의 read성 워크로드에 더욱 성능이 향상된다. 최적화 자동화는 오토커밋모드의 InnoDB 쿼리에도 적용되며, read-only 트랜잭션은 START TRANSACTION READ ONLY문을 추가함으로써 명시할 수 있다.
– read-only 미디어 기반의 분산 어플리케이션은 이제 InnoDB 테이블을 사용할 수 있다.

InnoDB 테이블의 BP사례
– 모든 테이블에 하나씩 PK를 만들어라. 자주 사용되는 컬럼(들)을 선정하면 좋고, 명확한 PK가 없을 경우에는 자동 증가값을 이용하라.
– 조인의 성능을 향상시키기 위해선 FK 설정을 해라.
– 자동커밋 기능을 꺼버려라. 초당 수백번의 커밋을 찍는 것은 성능에 영향을 미친다.
– DML문을 하나의 트랜잭션으로 묶어라. START TRASACTION AND COMMIT문이 도움이 된다.
– LOCK TABLE 구문을 사용하지 마라. 필요하면 SELECT FOR UPDATE를 써라.
– innodb_file_per_table 옵션을 ON하라.
– InnoDB 테이블을 압축해서 사용하고자 한다면 테스트를 진행해봐라. CREATE TABLE 문에서 ROW_FORMAT=COMPRESSED 옵션을 사용하면 되는데 R/W 성능을 떨어뜨리지 않고도 압축이 가능하다.
– 다른 스토리지 엔진으로 테이블이 생성되는 것을 방지하려면 –sql_mode= NO_ENGINE_SUBSTITUTION 옵션을 사용하라.

14.1.2 Checking InnoDB Availability

현재 서버가 InnoDB를 지원하는지 알아보려면 SHOW ENGINES 명령을 사용하면 된다.

14.1.3 Turning Off InnoDB

InnoDB를 끄는 법은 없다. –skip-innodb, –innodb=OFF 등의 옵션은 무시되고 있다. 반드시 사용해야 하는 스토리지 엔진이다.

 

14.2 InnoDB Concepts and Architecture

14.2.1 MySQL and the ACID Model

MYSQL은 어떻게 ACID 모델을 구현하고 있는가?
A: 자동커밋도 있고 COMMIT/ROLLBACK 구문도 있다.
C: InnoDB의 doublewrite 버퍼와 충돌 복구기능이 있다.
I: 자동커밋 기능과 ISOLATION LEVEL 구문을 지원한다.
D: innodb_doublewrite 파라미터로 doublewrite 기능 지원
innodb_flush_log_at_trx_commit 파라미터
sync_inlog 파라미터
innodb_file_per_table 파라미터
OS의 fsync() 시스템 콜을 지원

14.2.2 InnoDB Multi-Versioning

InnoDB는 오라클처럼 UNDO 세그먼트를 지원하기 때문에 old version을 볼 수 있다. 내부적으로 InnoDB는 row당 3개의 필드를 추가한다. 6 byte의  DB_TRX_ID 필드는 마지막으로 UPDATE나 INSERT 되었던 트랜잭션 ID를 의미한다. DELETE도 내부적으로는 업데이트된 것으로 인식하며 삭제되었다는 것을 의미하는 별도의 bit를 새겨둔다. 각 row는 roll 포인터라 불리는 7byte의 DB_ROLL_PTR 필드를 가지고 있다. 이 roll 포인터는 사용된 UNDO세그먼트의 포인터를 가리킨다. 6byte의 DB_ROW_ID 필드는 row의 ID를 의미하며 새로운 row가 insert될 때마다 1씩 자동적으로 증가한다. 만약 innoDB가 클러스터된 인덱스를 자동으로 생성했다면 해당 인덱스는 row ID를 가지지만, 이 경우를 제외하고는 어떤 인덱스도 DB_ROW_ID컬럼을 가지지 않는다.

undo 세그먼트는 insert용과 update용으로 나뉜다. insert undo는 오로지 트랜잭션 롤백에서만 의미가 있으며 트랜잭션이 커밋되면 바로 사라진다. update undo는 일관성 읽기에 사용되는데, snapshot이 완료된 직후에 트래잭션이 없는 상황에서만이 폐기될 수 있다.

주기적으로 커밋해야 하며, 그렇지 않으면 InnoDB가 undo log를 폐기할 수 없기 때문에 파일이 비대해질 수 있다. undo 세그먼트에서 undo 레코드의 물리적인 사이즈는 보통 실제 발생된 insert나 update row 보다 작다. SQL 구문에서 delete을 한다고 해서 즉시 값이 물리적으로 제거되는 것은 아니다. delete 내용을 담은 undo 로그 레코드가 폐기 되었을 때 비로소 물리적으로 row와 index의 값을 수정하는 것이다. 이 과정을 purge라고 부르며 꽤 빠르게 진행된다. 거의 delete SQL 구문이 발행되자마자 수행된 것처럼 느낄 정도로 빠르다고 볼 수 있다.

만약 한 테이블에서 insert와 delete를 반복적으로 수행하는 배치를 돌린다면 purge 쓰레드는 점점 뒤쳐질 수 밖에 없고 테이블은 점점 더 불어나게 된다. 이것은 모두 disk 작업으로 향하게 되고 시스템이 매우 느려지는 결과를 초래할 수 있다. 이러한 경우 purge 쓰레드에 좀 더 많은 자원을 투입하도록 파라미터를 수정할 수 있다. innodb_max_purge_lag 변수를 조절하면 된다.

멀티 버저닝과 세컨더리 인덱스

InnoDB의 MVCC는 세컨더리 인덱스를 클러스터된 인덱스와는 다르게 취급한다. 클러스터링된 인덱스는 그 자리에 업데이트되고, 숨겨진 시스템 컬럼이 undo 로그 값을 가리켜서 이전 버전의 레코드 값이 복원될 수 있다. 이러한 클러스터드 인덱스와는 달리 세컨더리 인덱스는 숨겨진 시스템 컬럼이 없기 때문에 적소에 업데이트 되지 않는다.

세컨더리 인덱스의 컬럼이 업데이트되면, old 버전의 레코드 값은 delete 마크가 새겨지고 새로운 값이 insert 되며 delete 마크가 새겨진 레코드는 최종적으로 purge된다. 세컨더리 인덱스 레코드에 delete 마크가 새겨졌거나 새 트랜잭션에 의해 update되었을 경우 InnoDB는 클러스터드 인덱스의 레코드를 살펴본다. 클러스터드 인덱스에서 레코드의 DB_TRX_ID 가 체크되었을 경우 만약 레코드의 값이 읽기 트랜잭션 이후에 수정되었다면 정확한 레코드의 버전을 undo 로그를 통해 복구한다.

세컨더리 인덱스의 레코드에 delete마크가 새겨졌거나 새로운 트랜잭션에 의해 업데이트 되었다면 covering 인덱스 기술(쿼리에 필요한 컬럼이 인덱스에 모두 존재하는 경우 테이블 엑세스 없이 인덱스 lookup만으로 값을 리턴하는 기술)은 사용되지 않는다. 인덱스 구조로부터 값을 돌려주는 대신 InnoDB는클러스터드 인덱스에서 값을 찾아본다.

그러나 만약 인덱스 조건 푸시다운(ICP) 최적화가 적용되었다면, 그리고 where 조건의 일부가 인덱스의 필드에 의해 평가될 수 있다면 MySQL은 where 조건의 일부를 스토리지 엔진으로 푸시 다운한다. 만약 매칭되는 레코드가 없다면 클러스터드 인덱스의 룩업과정은 생략된다. 만약 매치되는 레코드가 발견되면 delete 마크가 새겨져 있다 하더라도 InnoDB는 클러스터드 인덱스의 레코드를 뒤져본다.

14.2.3 InnoDB Redo Log

14.2.3.1 Group Commit For Redo Log Flushing

리두로그는 디스크 상에 존재하는 데이터 구조이며 장애 복구상황에서 완료되지 않은 트랜잭션에 의해 쓰여졌던 데이터를 복구하는데 사용된다. 정상 상황에서 리두 로그는 테이블 구조를 변경하라는 SQL구문이나 Low 레벨의 API로 표현되는 요구사항를 인코딩해둔다. 데이터 파일을 업데이트하려는 수정사항이 완료되지 않은 채 셧다운 되었을 때는 DB가 재시작되고 커넥션을 받기 전에 해당 트랜잭션이 재현된다.

기본적으로 리두로그는 ib_logfile0, ib_logfile1이라는 이름으로 디스크에 존재한다. MySQL은 리두 로그파일을 순환 방식으로 데이터를 write한다. 리두 로그는 영향받는 레코드에 대해 정보를 인코딩하는데 이 데이터를 총괄하여 리두라고 한다. 리두로그를 통한 데이터의 이동은 항상 증가하는 방향의 LSN 값으로 표현된다.

디스크에 존재하는 리두 로그는 아래 옵션을 이용하여 표현된다.

  • innodb_log_file_size: 로그 파일 사이즈를 byte 단위로 지정한다. 기본적으로 48MB이다. 이 파라미터와 innodb_log_files_in_group 파라미터 값을 곱한 값이 통합 로그 파일의 사이즈가 되며 이 값은 512GB보다 조금 작다.
  • innodb_log_files_in_group: 로그 그룹에 포함될 로그 파일의 개수이다. 기본은 2개.
  • innodb_log_group_home_dir: 로그파일의 위치를 설정한다. 기본은 datadir이다.

 

14.2.3.1 Group Commit for Redo Log Flushing

다른 ACID 지원 데이터베이스 엔진과 같이 InnoDB도 트랜잭션이 커밋되기 전에 redo log를 플러쉬한다. innoDB는 여러 개의 플러쉬 요청을 그룹화함으로써 각각의 커밋에 대한 개별 flush를 회피하는 방식을 사용하고 있으며 이를 그룹 커밋이라고 한다. 그룹 커밋을 이용하면 거의 비슷한 시간대에 발행한 여러 유저의 트랜잭션을 하나의 로그파일 쓰기 요청으로 만들어 수행하며 이것은 성능을 대단히 향상시킨다.

 

14.2.4 InnoDB Undo Logs

언두 로그는 활성 트랜잭션에 의해 수정된 데이터의 이전 버전을 가지고 있는 스토리지 영역이다. 만약 다른 트랜잭션이 원래의 데이터를 보고자 한다면(일관성 읽기 포함) 이 영역을 통해 수정되지 않은 데이터를 복구할 수 있다. 기본적으로 이 영역은 시스템 테이블스페이스의 일부로써 포함된다. 그러나 MySQL 5.6.3부터 언두로그는 별도의 언두 테이블스페이스에 존재할 수 있다.

InnoDB는 128개의 언두 로그를 지원한다. MySQL5.7.2부터 128개 중 32개의 언두 로그는 임시 테이블 트랜잭션을 위한 non-redo 언두영역으로 예약된 상태로 존재한다. 임시 테이블을 update하는 각 트랜잭션(읽기 전용 트랜잭션은 제외)은 2개의 언두 로그를 할당받는다. 하나는 redo-enabled 된 언두로그이며 하나는 non-redo 언두 로그이다. 읽기 전용 트랜잭션은 오직 임시 테이블을 수정할 수 있는 권한만을 받기 때문에 non-redo 언두 로그만 할당받는다.

남은 96개의 언두 로그들은 각각 데이터를 수정하는 동시 트랜잭션을 1023개 까지 지원할 수 있다. 따라서 대략적으로 96K개 만큼의 데이터 수정 트랜잭션을 동시에 지원할 수 있다고 볼 수 있다. 이 96K 라는 것에 대한 가정은 임시 테이블을 수정하지 않는 트랜잭션에 대한 것이다. 만약 임시 테이블까지도 수정하는 모든 데이터를 수정하는 트랜잭션을 동시에 지원해야 한다면 가능한 트랜잭션은 약 32K 정도가 될 것이다.

 

14.2.5 InnoDB Temporary Table Undo Logs

MySQL 5.7.2에서 일반 임시테이블, 압축된 임시테이블, 관련된 오브젝트에 대해 새로운 언두로그 타입을 소개했다. 이 새로운 언두로그 타입은 리두 로그가 아닌데 왜냐하면 임시 테이블은 장애 복구가 되지 않기 때문에 리두 로그가 필요 없기 때문이다. 대신 임시테이블 언두로그는 서버가 기동 중인 상황에서 롤백하는데 사용될 수는 있다. 이러한 non-redo 언두로그는 임시테이블과 관련된 오브젝트에 대해 불필요한 redo logging I/O를 줄일 수 있다는 것이 장점이다. 임시 테이블 언두로그는 임시 테이블스페이스에 존재한다. 임시 테이블스페이스는 기본적으로 ibtmp1이며 data 디렉토리에 존재한다. 그리고 서버가 재기동될 때 재생성된다. innodb_temp_data_file_path 파라미터를 통해 임시테이블스페이스 파일의 위치를 결정할 수 있다.

 

14.2.6 InnoDB Table and Index Structures

14.2.6.1 Role of the .frm File for InnoDB Tables

MySQL은 테이블에 대한 data dictionary를 .frm 파일에 저장한다. 다른 스토리지 엔진과는 달리 InnoDB는 테이블에 대한 정보를 테이블 스페이스 안의 내부적인 데이터 딕셔너리에 저장한다. 만약 테이블이나 database를 드랍할 경우 데이터 딕셔너리에 존재하는 관련된 엔트리들 뿐만 아니라 .frm 파일도 삭제한다. .frm 파일을 옮긴다고 InnoDB의 테이블을 데이터베이스간 옮길 수 있는 것은 아니다.

14.2.6.2 Clustered and Secondary Indexes

모든 InnoDB테이블은 row 데이터를 저장하는 클러스터드 인덱스라 불리는 특별한 인덱스를 가지고 있다. 일반적으로 클러스터드 인덱스는 Primary Key와 동기화된다. insert나 기타 쿼리에 대한 최적의 성능을 발휘하려면 InnoDB가 DML이나 lookup동작을 최적화하기 위해 InnoDB가 클러스터드 인덱스를 어떻게 사용하는지를 잘 파악해야 한다.

  • Primary Key를 지정하면 InnoDB는 그것을 클러스터드 인덱스로 사용한다.
    테이블 생성시 PK를 지정하면 좋고, 만약 논리적으로 unique하거나 null이 없는 컬럼을 만들기가 힘들다면 값이 자동으로 채워지는 auto-increment 컬럼을 붙여라.
  • 만약 Primary Key를 명시적으로 지정하지 않는다면 MySQL은 NOT Null인 첫 번째 Unique 컬럼을 찍어서 클러스터드 인덱스로 지정한다.
  • 만약 Primary Key도 없고 적당한 Unique 인덱스도 없다면, InnoDB는 row ID 값을 포함하여 합성된 컬럼을 기반으로 hidden 클러스터드 인덱스를 내부적으로 만들어낸다. InnoDB가 테이블의 각 row에 부여한 ID를 기반으로 row는 정렬된다. 이 row ID는 6byte의 필드로 구성되어 있으며 새로운 row가 insert 될 때마다값이 하나씩 증가한다. 따라서 row ID에 따라 정렬된 row는 물리적으로도 정렬된다.
How the Clustered Index Speeds Up Queries

클러스터드 인덱스에서 인덱스 탐색은 row data가 있는 page로 직접적으로 이어주기 때문에 row 접근이 매우 빠르다. 테이블이 커진 경우에 대해 row data와 인덱스 레코드간 서로 다른 페이지에 저장하는 저장방식과 비교하자면  클러스터드 인덱스의 디스크 I/O가 더 효율적이다. (예를 들어 MyISAM은 데이터 row마다 하나의 file을 사용한다)

How Secondary Indexes Relate to the Clustered Index

클러스터드 인덱스를 제외한 모든 인덱스는 세컨더리 인덱스이다. InnoDB에서 세컨더리 인덱스의 각 레코드는 명시한 컬럼 외에 row에 대한 PK 컬럼을 가지고 있다. InnoDB는 이 PK 값을 이용하여 클러스터드 인덱스에 있는 row 값을 찾는다.
PK 값이 길다면 세컨더리 인덱스는 저장 공간을 더 많이 차지하기 때문에 가급적 PK 길이를 줄이는 것이 좋다.

14.2.6.3 InnoDB FULLTEXT Indexes

전문 인덱스는 텍스트 기반의 컬럼들을(CHAR, VARCHAR, TEXT 컬럼) 기반으로 생성되며 이러한 컬럼에 저장된 데이터에 대한 쿼리와 DML 문의 처리를 빨리 할 수 있도록 한다. ?? stopwords?

전문 인덱스는 CREATE TABLE 문에 추가하여 만들 수도 있고 나중에 ALTER TABLE이나 CREATE INDEX 문으로도 만들 수 있다. 전문 검색은 MATCH()… AGAINST 구문으로 수행된다.

Full-Text Index Design

전문 인덱스는 inverted 인덱스 디자인을 갖고 있다. inverted 인덱스는 단어들의 리스트와 각 단어들이 나타나는 문서의 리스트를 저장한다. 신속한 탐색을 위해 각 단어들의 위치 정보도 함께 1byte로 저장된다.

Full-text Index Tables

InnoDB의 각 전문 인덱스마다 인덱스 테이블 묶음이 생성된다. 아래 예제를 보자.

CREATE TABLE opening_lines (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
opening_line TEXT(500),
author VARCHAR(200),
title VARCHAR(200),
FULLTEXT idx (opening_line)
) ENGINE=InnoDB;

mysql> SELECT table_id, name, space from INFORMATION_SCHEMA.INNODB_SYS_TABLES 
WHERE name LIKE 'test/%';
+----------+----------------------------------------------------+-------+
| table_id | name                                               | space |
+----------+----------------------------------------------------+-------+
|      333 | test/FTS_0000000000000147_00000000000001c9_INDEX_1 |   289 |
|      334 | test/FTS_0000000000000147_00000000000001c9_INDEX_2 |   290 |
|      335 | test/FTS_0000000000000147_00000000000001c9_INDEX_3 |   291 |
|      336 | test/FTS_0000000000000147_00000000000001c9_INDEX_4 |   292 |
|      337 | test/FTS_0000000000000147_00000000000001c9_INDEX_5 |   293 |
|      338 | test/FTS_0000000000000147_00000000000001c9_INDEX_6 |   294 |
|      330 | test/FTS_0000000000000147_BEING_DELETED            |   286 |
|      331 | test/FTS_0000000000000147_BEING_DELETED_CACHE      |   287 |
|      332 | test/FTS_0000000000000147_CONFIG                   |   288 |
|      328 | test/FTS_0000000000000147_DELETED                  |   284 |
|      329 | test/FTS_0000000000000147_DELETED_CACHE            |   285 |
|      327 | test/opening_lines                                 |   283 |
+----------+----------------------------------------------------+-------+

테이블 생성시 opening_line 컬럼에 대해 전문 인덱스를 만들었다.
그리고 테이블 현황을 보면 처음 6개의 테이블은 inverted 된 인덱스를 의미하며 이것들을 보조 인덱스 테이블이라 한다. 만약 들어오는 문서가 토큰화되면 각 단어들(토큰들이라 부름)은 위치 정보, 그리고 연관된 문서ID(DOC_ID)를 따라 인덱스 테이블로 insert된다.이 단어들은 완전히 정렬되며 character set 정렬인수(weight) 나 단어의 첫 글자에 따라서 여섯 테이블 중 하나로 파티션되어 들어가게 된다.

inverted 인덱스는 병렬 인덱스 생성을 지원하기 위해 여섯개의 보조 인덱스  테이블로 파티션된다. 기본적으로 2개의 쓰레드가 토큰화, 정렬, 단어 및 연관된 데이터에 대한 insert 작업을 담당한다.  쓰레드의 개수는 innodb_ft_sort_pll_degree 파라미터로 지정할 수 있다. 큰 사이즈의 테이블에 대해 전문인덱스를 생성할 경우 쓰레드의 개수를 늘리는 것을 고려해 보아라.

보조 인덱스 테이블은 FTS_로 시작하여 INDEX_로 끝난다. 원본 테이블의 table_id 값을 16진수 값으로 변환한 값을 각 인덱스 테이블의 이름에서 찾아볼 수 있다. 예를 들면 test/opening_lines의 table_id 가 327인데 이것은 16진수로 147이며 이 값은 인덱스 테이블의 이름에서 찾아볼 수 있다.

전문 인덱스에서 16진수로 표현한 index_id 또한 보조 인덱스 테이블에서 찾아볼 수 있다. 예를 들어 test/FTS_0000000000000147_00000000000001c9_INDEX_1  에서 1c9는 10진수로 457이다. INFORMATION_SCHEMA.INNODB_SYS_INDEXES에서 index_id=457로 값을 조회해보면 인덱스를 확인할 수 있다.

mysql> SELECT index_id, name, table_id, space from INFORMATION_SCHEMA.INNODB_SYS_INDEXES 
  WHERE index_id=457;
+----------+------+----------+-------+
| index_id | name | table_id | space |
+----------+------+----------+-------+
|      457 | idx  |      327 |   283 |
+----------+------+----------+-------+

인덱스 테이블은 원본 테이블이 file-per-table일 경우 해당 테이블스페이스에 저장된다.
※ Mysql 5.6.5에서 발견된 버그 때문에 원본 테이블이 file-per-table 에 의해 생성되었더라도 인덱스 테이블은 InnoDB 시스템 테이블스페이스에 저장되었다. 이 버그는 MySQL5.6.20과 5.7.5버전에서 수정되었다. 5.7.8 버전부터는 보조 인덱스 테이블은 항상 원본테이블과 같은 테이블스페이스에 저장되며 원본 테이블과 같은 row 포맷을 같는다.

위 예제에서 본 다른 인덱스 테이블들은 삭제처리나 전문 인덱스의 내부적인 상태를 저장하기 위해 쓰인다.

  • FTS_DELETED 와 FTS_DELETED_CACHE: 문서를 삭제하더라도 아직 전문인덱스에서 제거되지 않은 문서들에 대한 ID(DOC_ID)가 저장되는 테이블이다. _CACHE테이블은 _DELETED 테이블의  in-memory 버전이다.
  • FTS_*_BEING_DELETED와 FTS_*_BEING_DELETED_CACHE: 전문인덱스에서 현재 삭제가 진행중인 문서의 ID를 저장하는 테이블이다.
  • FTS_*_CONFIG: 전문 인덱스의 내부적인 정보를 저장하는 테이블이다. FTS_SYNCED_DOC_ID는 문서를 파싱하고 디스크로 플러쉬한 문서를 식별하는 역할을 하는데 가장 중요한 정보라 할 수 있다. 장애 복구상황에서 이 값을 이용해 디스크로 아직 플러쉬되지 못한 문서들을 식별하고 해당 문서들을 다시 파싱하여 전문 인덱스 캐시로 다시 돌려보낸다. 이 테이블에 있는 데이터들을 보려면 INFORMATION_SCHEMA.INNODB_FT_CONFIG 테이블을 보면 된다.
Full-Text Index Cache

문서가 insert되면 이것이 토큰화되어서 각각의 단어들과 연관된 데이터가 전문 인덱스로 insert된다. 이 과정에서 작은 크기의 문서라도 보조 인덱스 테이블로 많은 insert가 발생하게 되어 경합이 발생하게 된다. 이 문제를 피하기 위해, innoDB는 전문 인덱스를 캐싱하여 최근 insert된 row에 대해 캐싱된 인덱스 테이블을 이용한다. 이런 인메모리 캐시 구조는 캐시가 꽉 차거나 주기적으로 disk로 플러쉬시킬 때까지 insert 트랜잭션을 유지하게 된다. INFORMATION_SCHEMA.INNODB_FT_INDEX_CACHE 테이블이 최근 insert된 row에 대해 토큰화된 데이터를 볼 수 있게 해준다.

캐싱과 배치 플러쉬 방식은 insert나 update가 빈번할 때 경합을 발생시킬 수도 있을 만한 보조 인덱스 테이블로의 빈번한 업데이트 작업을 줄여준다. 또한 이 방식은 동일한 단어에 대한 반복적인 insert 작업을 줄여주게 되고 중복된 엔트리 생성을 최소화해준다. 각각의 단어마다 플러쉬하는 대신 동일한 word에 대한 insert 는 하나로 합해져 단일의 엔트리로써 디스크로 플러쉬된다. 따라서 임시테이블을 가능한 한 작게 유지하는 반면 효율을 높을 수도 있다.

innodb_ft_cache_size 변수는 전문 인덱스의 캐시 사이즈(기본적으로 테이블 당)를 조절할 수 있는데 이는 전문 인덱스 캐시가 얼마나 자주 플러쉬 되는지를 결정하게 된다. 또한 innodb_ft_total_cache_size 파라미터를 통해 전체 테이블에 걸쳐 사용되는 글로벌 전문인덱스의 캐시 사이즈를 정할 수 있다.

전문 인덱스 캐시는 임시 테이블과 동일한 정보를 저장한다. 그런데 전문 인덱스  캐시는 최근 insert된 row에 대해 토큰화된 데이터만 저장한다. 즉 이미 디스크(임시 전문테이블)로 플러쉬된 데이터는 나중에 쿼리된다 하더라도 캐시로 올라오지 않는다. 보조 인덱스 테이블에 있는 데이터는 직접적으로 조회되고 보조 인덱스 테이블에서의 결과물은 전문 인덱스 캐시에 있는 값과 합쳐져서 반환된다.

InnoDB Full-Text Document ID and FTS_DOC_ID Column

InnoDB는 전문 인덱스에 있는 단어와 문서에서 실제 나타나는 구간을 매핑하기 위해 유일한 값을 갖는 ID (DOC_ID) 라고 불리는 식별자를 이용한다. 매핑을 위해선 원본 테이블의 FTS_DOC_ID 컬럼이 필요하다. 만약 FTS_DOC_ID 컬럼이 정의되지 않았다면 InnoDB는 전문 인덱스 생성시 숨겨진 상태로 이 컬럼을 자동 생성한다. 아래 예제는 이런 특징을 보여준다.

CREATE TABLE opening_lines (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
opening_line TEXT(500),
author VARCHAR(200),
title VARCHAR(200)
) ENGINE=InnoDB;

만약 CREATE FULLTEXT INDEX 키워드를 통해 전문 인덱스를 생성한다면, FTS_DOC_ID 컬럼을 추가하기 위해 테이블을 리빌딩했다는 경고 메시지를 받게 된다.

mysql> CREATE FULLTEXT INDEX idx ON opening_lines(opening_line);
Query OK, 0 rows affected, 1 warning (0.19 sec)
Records: 0  Duplicates: 0  Warnings: 1

mysql> SHOW WARNINGS;
+---------+------+--------------------------------------------------+
| Level   | Code | Message                                          |
+---------+------+--------------------------------------------------+
| Warning |  124 | InnoDB rebuilding table to add column FTS_DOC_ID |
+---------+------+--------------------------------------------------+

ALTER TABLE 문을 이용하여 전문 인덱스를 생성하려고 했을 때도 마찬가지의 경고를 받게 된다. 만약 별도의 FTS_DOC_ID 컬럼을 만들지 않았으나 CREATE TABLE 문을 이용하여 전문 인덱스를 생성하면 경고 없이 숨겨진 FTS_DOC_ID 컬럼을 만들어낸다.

CREATE TABLE시 FTS_DOC_ID 컬럼을 정의하는 것은 이미 데이터가 있는 테이블에서 전문 인덱스를 생성하는 것보다 시간이 더 적게 걸린다. 당연하겠지만 데이터를 로드하기 전에 테이블에 FTS_DOC_ID 컬럼이 정의되어 있다면 테이블과 인덱스는 리빌드될 필요가 없다. 만약 CREATE FULLTEXT INDEX 의 성능이 걱정된다면 InnoDB가 알아서 하도록 그냥 냅둬라.

InnoDB가 FTS_DOC_ID 컬럼과 그에 기반한 unique 인덱스를 생성한다.(FTS_DOC_ID_INDEX) 만약 FTS_DOC_ID를 직접 정의하고 싶으면 아래와 같이 반드시 BIGINT UNSIGNED NOT NULL 속성과 함께 FTS_DOC_ID로 네이밍해야 한다.

CREATE TABLE opening_lines (
FTS_DOC_ID BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
opening_line TEXT(500),
author VARCHAR(200),
title VARCHAR(200)
) ENGINE=InnoDB;

※FTS_DOC_ID 컬럼을 꼭 AUTO_INCREMENT 속성으로 만들 필요는 없지만 데이터를 로딩을 더욱 쉽게 해준다.

FTS_DOC_ID 컬럼을 직접 생성했다면 null 값이나 중복된 값을 회피하기 위한 관리를 직접 해주어야 한다. FTS_DOC_ID 값은 재사용될 수 없으며 항상 증가하는 방향이어야 한다. 추가적으로 FTS_DOC_ID_INDEX 인덱스도 직접 만들 수 있으며 직접 만들지 않으면 InnoDB가 알아서 만들어준다.

InnoDB Full-Text Index Deletion Handling

전문 인덱스 컬럼에서 값을 삭제하면 보조 인덱스 테이블에서 몇 번의 작은 삭제작업이 발생하게 되며 이것 또한 경합을 일으킬 소지가 있다. 이 문제를 피하고자 원본 테이블의 레코드가 삭제되면 삭제된 문서의 DOC_ID가 FTS_*_DELETED 테이블에 기록되는 반면 전문 인덱스에는 남아있게 된다. 쿼리 결과가 반환되기 FTS_*_DELETED 테이블을 찾아 삭제된 문서를 필터링한다. 이 방식의 장점은 삭제 작업이 빠르고 비용이 적게 든다는 것이다. 이 방식의 결점은 삭제작업 후 인덱스가 바로 삭제되지 않는 다는 것이다. 전문 인덱스의 엔트리에서 값을 삭제하기 위해선 innodb_optimize_fulltext_only=ON 으로 설정한 상태에서 OPTIMIZE TABLE을 돌려줘야 전문인덱스가 리빌딩되면서 값이 삭제된다.

InnoDB Full-Text Index Transaction Handling

전문인덱스가 가진 캐싱과 배치 플러쉬 특성 때문에 트랜잭션이 매우 특이하게 처리된다. 특히 전문 인덱스에 대한 update와 insert는 트랜잭션의 commit단계에서 발생하며 이것은 전문인덱스에 대한 정보는 오직 committed 데이터만 볼 수 있다는 것을 의미한다.

14.2.6.4 Physical Structure of an InnoDB Index

공간인덱스를 제외하면 InnoDB의 인덱스는 모두 B-tree 구조이다. 공간 인덱스는 R-tree 구조를 사용하며 이것은 다차원 구조로 인덱싱된 데이터 구조를 가지고 있다.B-tree, R-tree구조 모두 인덱스 레코드는 leaf 페이지에 저장되며 기본적인 페이지의 크기는 16KB이다.

클러스터드 인덱스에 새로운 레코드가 insert되면 innoDB는 차후에 insert되거나 update 될 때를 대비하여 페이지의 1/16은 남겨놓는다. 만약 연속적인 값이 insert된다면 인덱스 페이지는 15/16이 사용될 것이며, 랜덤하게 값이 들어온다면 인덱스 페이즈는 1/2 에서 15/16 사이를 사용하게 될 것이다.

Mysql 5.7.5부터 B-tree 인덱스를 생성하거나 리빌드할 때 벌크 load를 수행한다. 이런 인덱스 생성 방식은 정렬된 인덱스 생성이라고 알려져 있다. innodb_fill_factor 파라미터는 정렬된 인덱스 생성을 수행하는 동안 인덱스 페이지가 얼마나 채워질지를 결정한다. (MariaDB에 이 파라미터는 없음) 남은 공간은 차후 인덱스가 늘어날 때 사용된다. 정렬된 인덱스 생성방식은 공간 인덱스에는 지원되지 않는다.

만약 인덱스 페이지에 대한 fill factor가 MERGE_THRESHOLD(기본적으로 50%) 이하로 떨어지면 InnoDB는 인덱스 크기를 줄이게 된다. MERGE_THRESHOLD 값은 B-tree와 R-tree에 모두 영향을 준다.

인스턴스를 생성하기 전에 innodb_page_size 파라미터를 셋팅함으로써 인스턴스의 모든 InnoDB 테이블스페이스 페이지 사이즈를 정할 수 있다. 인스턴스에서 한번 셋팅되면, 다시 바꿀 수는 없다. 지원되는 사이즈는 64KB, 32KB, 16KB(기본 값), 8KB, 4KB이다.

14.2.6.5 Change Buffer

변경 버퍼는 세컨더리 인덱스 페이지의 변경사항이 발생했을 때 해당 페이지가 버퍼 풀에 존재하지 않으면 변경 내용을 캐싱하는 특별한 데이터 구조이다.
DML 구문으로 인해 버퍼된 변경사항은 나중에 읽기 동작으로 인해 페이지가 버퍼 풀로 로딩되면서 합쳐지게 된다.

클러스터드 인덱스와는 다르게 세컨더리 인덱스는 보통 non-unique하며 세컨더리 인덱스로의 insert는 상대적으로 랜덤한 순서를 가진다. 마찬가지로 delete와 update 가 수행되면 인덱스 트리의 인접한 위치의 인덱스 페이지에 영향을 미치는 것이 아니다. 다른 읽기 트랜잭션에 의해 해당페이지가 버퍼 풀로 로딩되었을 때 변경사항을 취합하여 저장하게 된다. 이것은 세컨더리 인덱스 페이즈를 디스크로부터 읽어들이기 위한 랜덤 엑세스를 줄여주게 된다.

시스템이 거의 idle 상태이거나 slow shutdown 상태가 되면 주기적으로 업데이트된 인덱스 페이지를 디스크로 내려쓰는 purge 동작을 수행한다. 이런 purge 동작은 연속적인 인덱스 값을 따라 디스크 블록에 저장하므로 수정사항이 발생할 때마다 디스크에 바로바로 쓰는 것보다 효율적으로 움직이게 된다. 세컨더리 인덱스의 개수가 많은 경우나 영향받은 row의 수가 많을 경우에 변경 버퍼를 취합하는 것은 몇 시간이 걸릴 수도 있다. 이 동안 디스크 사용율이 늘어나 disk작업이 수반되는 쿼리의 성능을 저하시킬 수 있다. 변경 버퍼의 취합과정은 트랜잭션이 커밋된 이후에도 지속되는데 사실 변경 버퍼의 취합은 서버가 셧다운되거나 리스타트 된 이후에도 지속된다.

메모리안에서 변경 버퍼는 InnoDB 버퍼 풀의 일부를 복사한다. 디스크에서 변경 버퍼는 시스템 테이블스페이스의 일부이기 때문에 DB가 재시작 된 이후에도 인덱스 변경 사항이 버퍼링될 수 있는 것이다. 변경 버퍼에 캐시되는 데이터의 유형은 innodb_change_buffering 파라미터에 의해 결정된다.

Monitoring the Change Buffer
(1) mysql> SHOW ENGINE INNODB STATUS\G

위 명령을 수행했을 때 나타나는 정보 중 INSERT BUFFER AND ADAPTIVE HASH INDEX 항목에서 관련 내용을 찾아볼 수 있다.

(2) INFORMATION_SCHEMA.INNODB_METRICS 테이블에서도 찾아볼 수 있다.

mysql> SELECT NAME, COMMENT FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME LIKE '%ibuf%'\G

(3) INFORMATION_SCHEMA.INNODB_BUFFER_PAGE 테이블에서도 관련 정보를 찾아볼 수 있다. 이 테이블은 버퍼 풀에 존재하는 페이지에 대한 메타 정보를 담고있는데 변경 버퍼 인덱스와 변경 버퍼 비트맵 페이지도 포함하고 있다. 인덱스 페이지에 대한 변경 버퍼 페이지는 PAGE_TYPE 컬럼이 IBUF_INDEX 라고 되어 있다. IBUF_BITMAP은 변경 버퍼 비트맵에 대한 페이지를 나타낸다.

변경 버퍼 페이지가 버퍼 풀의 몇 %를 차지하고 있는지를 확인하려면 아래 쿼리를 이용한다.

SELECT  
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE 
WHERE PAGE_TYPE LIKE 'IBUF%'
) AS change_buffer_pages,
(
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
) AS total_pages,
(
SELECT ((change_buffer_pages/total_pages)*100)
) AS change_buffer_page_percentage;

(4) PERFORMANCE_SCHEMA.SETUP_INSTRUMENTS

아래와 같이 쿼리하여 mutex wait 정보를 볼 수 있다.
mysql> SELECT * FROM performance_schema.setup_instruments WHERE NAME LIKE ‘%wait/synch/mutex/innodb/ibuf%’;

 

14.2.6.6 Adaptive Hash Indexes

Adaptive Hash Indexes(이하 AHI)는 매우 큰 버퍼 풀 공간과 다양한 워크로드의 조합을 기반으로 in-memory 데이터베이스처럼 동작하는데 트랜잭션의 특성이나 안정성을 훼손하지 않고도 동작 가능하다. innodb_adaptive_hash_index 파라미터를 통해 설정 가능하다.

검색 패턴을 관찰한 내용을 기반으로 하여 MySQL은 인덱스 key의 prefix를 이용하여 해쉬 인덱스를 만든다. key의 prefix의 길이에 대한 제한은 없으며, B-tree 값 중 몇 개의 값만 대상으로 해쉬 인덱스를 만들게 된다. 해쉬 인덱스는 자주 엑세스되는 인덱스 페이지에 대한 요청에 의해 생성된다.

만약 테이블 전체가 메모리 안에 상주해 있다면, 해쉬 인덱스는 어떤 값이든지 직접적으로 빠르게 값을 찾아보게 된다. InnoDB는 인덱스 탐색과정을 모니터링하는 메커니즘을 갖고 있으며 만약 해쉬인덱스를 만드는 것이 이득이라고 판단하면 자동적으로 만들어낸다.

어떤 특정한 워크로드에서는, 인덱스 모니터링이나 해쉬인덱스를 유지하는 등의 추가적인 부담으로 인해 해쉬인덱스를 사용하는 것이 시스템 부담이 더 커질수도 있다. 다차원 join이 동시다발적으로 일어나는 무거운 워크로드 하에서는 해쉬인덱스로의 접근을 통제하는 read/write lock이 경합을 일으키는 원인이 될 수 있다. LIKE 구문이나 % 와일드 카드를 사용하는 쿼리 또한 AHI를 제대로 사용하지 못하므로 AHI가 필요없는 워크로드에서는 이 기능을 꺼버리는 것이 불필요한 성능 오버헤드를 줄여주는 요인이 된다. 특정 시스템에서 이 기능을 쓰는 것이 좋은지 아닌지 판단하기가 힘들기 때문에 이 기능을 ON/OFF 하고 벤치마크 테스트 하는 것을 고려해보아라. MySQL 5.6이나 그 이상 버전에서는 대부분의 워크로드가 이 기능을 끄는 것이 오히려 더 좋게 변화되었다. 그러나 아직 디폴트로 이용하도록 되어있다.

MySQL 5.7.8 부터 AHI는 파티셔닝이 가능하다. 각 인덱스는 특정한 파티션에 소속되며 각 파티션은 별도의 latch에 의해 관리된다. innodb_adaptive_hash_index_partitions 파라미터에 의해 파티셔닝 개수를 조절할 수 있다. 이전 버전에서는 단 하나의 latch에 의해 AHI를 관리하였으므로 무거운 워크로드 하에서는 경합의 주 원인이 되었었다.

SHOW ENGINE INNODB STATUS 명령으로 AHI의 사용현황과 경합 발생 현황까지 볼 수 있다.(SEMAPHORES 섹션) 만약 btr0sea.c 에서 RW 래치를 대기하는 쓰레드의 수가 많다고 판단되면 AHI 기능을 끄는 것이 좋다.

AHI
< INNODB STATUS 의 AHI 섹션>

semaphore.jpg
< INNODB STATUS 의 SEMAPHORE 섹션>

14.2.6.7 Physical Row Structure

테이블의 물리적인 구성방식은 테이블 생성시 어떤 row 포맷으로 지정했느냐에 따라 다르다. 별도로 지정하지 않았을 경우 디폴트 row 포맷이 적용되는데 MySQL 5.7.6 버전 이전에서는 Antelope 라는 파일포맷과 이것의 COMPACT row 포맷이 기본적으로 적용되었다. MySQL 5.7.7 에서는 innodb_file_format 파타미터에 의해 기본적으로 Barracuda가 적용되었고 5.7.9 부터는 innodb_default_row_format 파라미터에 의해 결정되고 있다. 이 파라미터의 기본 값은 DYNAMIC이다. 현재 MariaDB는 innodb_file_format 파라미터에 의해 결정되고 있으며 기본 값은 antelope이다. (버전 10.1.12)

compact.jpg
<COMPACT row 포맷>

COMPACT row 포맷은 CPU성능을 좀 더 필요로 하는 대신 저장 공간을 20% 이상 절감한다. 만약 워크로드가 캐시 hit율과 disk speed에 의해 병목이 생기는 시스템이라면 COMPACT row 포맷은 좀 더 빠르게 작동한다. 그러나 CPU speed가 병목을 유발하는 시스템이라면 COMPACT row 포맷은 좀 더 느리게 작동한다.

REDUNDANT row 포맷을 사용하는 InnoDB의 row는 아래와 같은 특징을 가진다.

  • 각 인덱스 레코드는 6byte의 헤더를 갖는다. 이 헤더는 연속적인 레코드를 서로 이어주는데 사용되며 저단계 locking에 사용된다.
  • 클러스터드 인덱스는 유저가 정의한 모든 컬럼을 갖는다. 추가적으로 6 byte의 트랜잭션ID 필드와 7 byte의 roll pointer 필드를 갖는다.
  • 테이블에 대해 PK가 정의되지 않는다면 각 클러스터드 인덱스는 6byte의 ROW ID 필드를 갖는다.
  • 모든 세컨더리 인덱스는 클러스터드 인덱스에서 정의되었지만 세컨더리 인덱스에서는 정의되지 않은 PK컬럼을 내부적으로 포함하고 있다.
  • 각 레코드는 서로를 가리키는 포인터를 가지고 있다. 만약 레코드의 전체 길이가 128byte 이내라면 포인터는 1byte가 되며 그렇지 않으면 2byte가 된다. 이 pointer의 배열을 레코드 디렉토리라고 부른다. 이 포인터가 가리키는 영역을 레코드의 데이터 파트라고 한다.
  • Null 값에는 1byte나 2byte의 공간을 레코드 디렉토리에 예약해둔다. 게다가 Null 값은 해당 필드가 가변길이 컬럼으로 정의되어 있으면 데이터 파트에는 공간을 예약해두지 않는다. 고정 길이 컬럼에는 데이터 파트에는 해당 길이만큼 공간을 예약해둔다. 이 예약방식 덕분에 non-null 값으로 차후 업데이트 되어도 페이지의 조각화를 유발하지 않게 된다.

COMPACT row format을 갖는 InnoDB의 row는 아래와 같은 특징을 가진다.

  • 각 인덱스 레코드는 5byte의 헤더를 가지며 가변길이 헤더와 별도로 미리 생성된다. 또한 이 헤더는 연속한 레코드들 간 서로 연결하기 위해 쓰이며 저레벨 locking에 사용된다.
  • 레코드 헤더의 가변길이 파트는 Null 컬럼을 가리키기 위한 bit 벡터를 가진다. 인덱스에서 null 값을 가질 수 있는 컬럼의 개수가 N일 때 9<= N <=15 라면 bit 벡터는 2byte의 크기가 된다. null인 컬럼은 bit vector 외엔 공간을 차지하지 않는다. 헤더의 가변길이 파트는 가변길이 컬럼에 대한 length를 가진다.  각 length는 가변길이 컬럼의 최대 값에 따라 1byte 또는 2byte를 갖는다. 만약 인덱서의 모든 컬럼이 Not null 이고 고정된 길이를 가진다면 레코드 헤더는 가변길이 파트를 가지지 않는다.
  • not null인 가변길이의 각 컬럼에 대해, 레코드 헤더는 1byte 혹은 2byte의 컬럼에 대한 ??을 갖는다. ???
  • null값이 아닌 데이터 뒤에 레코드 헤더가 뒤따라온다.
  • 클러스터드 인덱스는 유저가 정의한 모든 컬럼을 갖는다. 추가적으로 6 byte의 트랜잭션ID 필드와 7 byte의 roll pointer 필드를 갖는다. (REDUNDANT와 동일)
  • 테이블에 대해 PK가 정의되지 않는다면 각 클러스터드 인덱스는 6byte의 ROW ID 필드를 갖는다. (REDUNDANT와 동일)
  • 세컨더리 인덱스의 각 레코드는 해당 인덱스에선 정의되진 않았지만 클러스터드 인덱스에 정의되어 있는 PK값을 가지고 있다. 만약 PK 컬럼이 가변길이 특성을 갖고 있다면, 세컨더리 인덱스가 고정길이 컬럼에 기반하여 생성되었더라도 이 레코드 헤더 또한 length를 기록하기 위한 가변길이 파트를 보유한다.
  • 내부적으로 InnoDB는 CHAR(10)같은 고정길이의 컬럼에 대해 고정 길이만큼 저장한다. VARCHAR 컬럼에 대해선 trailing한 공간은 truncate하지 않는다.
  • SQL null 값에 대해 레코드 디렉토리에 1byte나 2byte의 공간을 남겨둔다. 게다가 null 값은 가변길이 컬럼에 대해선 데이터 파트에 공간을 하나도 남겨두지 않는다. 고정길이 컬럼에 대해서는 데이터 파트에 고정 길이만큼 공간을 남겨둔다. 차후 null이 아닌 값이 update되는 상황을 위해 특정 길이의 공간을 남겨두는 것은 페이지의 조각화를 방지해준다.
  • 내부적으로 InnoDB는 utf8의 CHAR 컬럼과 uft8mb4의 CHAR(N) 컬럼의 데이터는 N byte만큼 지저분한 공간들을 정리하여 저장한다. 만약 CHAR(N)의 바이트 길이가 N 바이트를 초과하면 InnoDB는 바이트 길이를 최소화하기 위해 지저분한 공간들을 정리한다. CHAR(N)의 최대 길이는 character 바이트 길이 * N 과 동일하게 되며 INFORMATION_SCHEMA.COLUMNS 테이블의 CHARACTER_OCTET_LENGTH 컬럼의 값에서 내용을 확인할 수 있다.
    InnoDB는 CHAR(N)에 대해 최소 N 바이트는 예약해둔다. N 바이트만큼 예약함으로써 차후 update가 될 때 페이지의 조각화가 되는 것을 방지하게 된다.
    대조적으로 ROW_FORMAT 이 REDUMDANT인 경우 utf8과 utf8mb4 컬럼은 character 바이트 길이*N 길이만큼 미리 할당해두며 ROW_FORMAT이 DYNAMIC인 경우와 COMPRESSED인 경우는 COMPACT인 경우와 동일하게 작동한다.
    DYNAMIC과 COMPRESSED row 포맷은 COMPACT의 변형된 포맷이다.

14.2.6.8 Sorted Index Builds

MySQL 5.7.5부터 인덱스를 생성하거나 리빌드할 때 한번에 레코드 하나씩 insert하는 것이 아니라 대량으로 로딩하는 방법을 사용한다. 이러한 방식의 인덱스 생성기법을 정렬된 인덱스 생성이라고 부른다. 정렬된 인덱스 생성기법은 공간 인덱스에서는 지원되지 않는다.

인덱스를 생성하는 과정은 크게 3가지로 나뉜다. 첫 번째 단계에서는 클러스터드 인덱스를 스캔한 후 인덱스 엔트리를 생성하여 sort 버퍼에 넣어둔다. sort 버퍼가 꽉 차면 엔트리들을 정렬시킨 후 임시 파일에 내려쓴다. 이 과정을 “run”이라고 부른다. 두 번째 단계에서는 첫 번째 단계에서 한번 이상 임시 파일에 내려 쓴 것들을 모두 모아 전체 엔트리를 대상으로 전체 정렬을 수행한다. 세 번째 단계에서는 정렬된 엔트리들이 B-tree로 insert된다.

정렬된 인덱스 생성기법이 소개되기 전에는 insert API를 이용하여 한번에 레코드 하나씩 B-tree로 삽입하는 방법을 사용했었다. 이 방법은 insert할 위치를 찾고 낙관적 insert 방식으로 엔트리를 B-tree 페이지 안으로 넣기 위해 B-tree의 커서를 오픈해야 했다. 만약 page가 full로 인해 insert가 실패한다면 비관적 insert 방식이 수행되었는데 이 방식은 엔트리의 저장 공간을 찾기 위해 B-tree의 커서를 오픈하고 B-tree의 노드를 분할, 합병하는 방식으로 진행되었다. 이러한 Top down  방식의 단점은 insert 위치를 찾는데 소모되는 비용과 지속적인 B-tree 노드의 분할, 합병이 발생한다는 점이다.

정렬된 인덱스 생성 기법은 bottom up 방식을 채택하고 있다. 이 방식에서는 B-tree의 모든 레벨에서 가장 우측에 있는 leaf 페이지를 기준으로 삼아, ???

Reserving B-tree Page Space for Future Index Growth

향후 인덱스의 증가에 대비해서, innodb_fill_factor 파라미터를 통해 B-tree 페이지의 여분 공간을 정할 수 있다. 예를 들어 이것을 80으로 한다면 정렬된 인덱스 생성 중 페이지의 20%는 여유공간으로 남겨둔다.  이 방식은 B-tree leaf와 non leaf 페이지에 모두 적용된다. 그러나 이것은 TEXT나 BLOB엔트리를 위해 저장하는 외부 페이지에 대해서는 적용되지 않는다. 그리고 여분 공간의 사이즈는 정확하게 지켜지지 않는다. innodb_fill_factor 파라미터는 명확한 규율이라기 보다는 힌트에 가깝다.

Sorted Index Builds and Fulltext Index Support

정렬된 인덱스 생성기법은 전문 인덱스에서도 지원된다. 엔트리를 전문 인덱스로 insert 하도록 SQL이 미리 이용된다.

Sorted Index Builds and Compressed Tables

압축된 테이블에서 이전의 인덱스 생성 방식은 압축되거나 혹은 압축되지 않은 페이지에 엔트리를 덧붙였었다. 만약 수정 로그(압축된 페이지에서 여유 공간을 나타냄)가 꽉 차게 되면 압축된 페이지는 다시 압축되었다. 만약 공간 부족을 이유로 압축이 실패하게 되면 페이지는 쪼개진다. 정렬된 인덱스 생성방식에서 엔트리는 압축되지 않은 페이지에만 덧붙여지게 된다. 압축되지 않은 페이지가 꽉 차게 되면 이 페이지는 압축된다. 압축이 대부분의 케이스에서 성공적으로 끝났다는 것을 확실하게 하기 위해 적응적 덧붙임 방식이 사용되지만, 압축이 실패로 끝난 경우 페이지는 쪼개지며 압축이 재실행된다. 이 과정은 압축이 정상적으로 끝날 때까지 지속된다.

Sorted Index Builds and Redo Logging

정렬된 인덱스 생성중에는 리두 로깅이 중단된다. 대신에 인덱스 생성과정이 장애 상황에 대비할 수 있도록 체크포인트가 존재한다. 체크포인트는 모든 dirty 페이지를 디스크에 내리도록 강제한다. 정렬된 인덱스 생성중에 체크포인트 동작이 빨리 수행될 수 있도록 page cleaner 쓰레드가 주기적으로 신호를 받아 dirty 페이지를 플러시한다. 보통 page cleaner 쓰레드는 clean 페이지의 개수가 특정 임계치 값 이하로 내려가면 작동되는데 정렬된 인덱스 생성중에는 병렬I/O와 CPU그리고 체크포인트의 오버헤드를 줄이기 위해 dirty 페이지들이 신속히 플러시된다.

Sorted Index Builds and Optimizer Statistics

정렬된 인덱스 생성방식은 이전의 생성방식과는 다른 옵티마이저 통계정보를 생성해낼 수 있다. 인덱스를 생성하는 알고리즘이 다르기 때문에 이런 일이 발생하는데 워크로드에 어떤 영향을 미칠지 예상할 수는 없다.

 

14.2.7 InnoDB Mutex and Read/Write Lock Implementation

MySQL과 InnoDB에서 여러 개의 쓰레드가 공유 구조체에 접근한다. InnoDB는 읽기/쓰기 락과 자체구현한 뮤텍스로 이러한 접근들을 동기화한다. 전통적으로 InnoDB는 읽기/쓰기 락의 내부 상태를 뮤텍스로 보호했었다. 그리고 이 뮤텍스는 P쓰레드 뮤텍스라고 하는 것에 의해 보호되었다.

많은 플랫폼에서 P쓰레드보다는 원자적 수행으로 여러 개의 쓰레드를 동기화하는 방안이 더 효율적으로 사용되곤 하였다. 락을 얻거나 해제하려는 각각의 시도들은 CPU를 더 적게 소모하였고 쓰레드가 공유 데이터 구조체에 접근하려고 경쟁하는 시간도 더 단축하게 되었다. 이것은 결국 멀티 코어플랫폼에서는 더욱 큰 확장성을 가져다 준다는 것을 의미한다.

원자적 수행을 지원하는 플랫폼에서 InnoDB는 빌트인된 GNU 컴파일러 컬렉션(GCC)로 만들어진 뮤텍스와 읽기/쓰기 락을 구현하였으며 원자적 메모리 접근 상황하에서 P쓰레드 방식 대신 사용되고 있다. 더군다나 GCC 버전 4.1.2 이상에서 컴파일된 InnoDB의 경우 뮤텍스와 읽기/쓰기 락을 구현하기 위해 pthread_mutex_t 대신 내장된 빌트인을 사용하고 있다.

32비트의 윈도우즈의 경우, InnoDB는 뮤텍스(읽기/쓰기 락은 아닌)를 직접쓰기 어셈블러 방식으로 구현하였다. 윈도우즈 2000이 시작될 때 interlocked variable access 함수가 가능했었는데 이것은 GCC에 의해 제공되던 내장함수와 비슷하였다. 윈도우 2000 이후부터는 InnoDB는 읽기/쓰기 락과 64비트 플랫폼을 지원하는 interlocked 함수를 사용하였다.

솔라리스10은 원자적 동작에 대한 라이브러리 함수를 제공하였으며 InnoDB는 이 방식을 디폴트로 사용하였다. MySQL이 솔라리스10과 그 이후 버전에서 GCC에서 제공하는 내장 함수를 지원하지 않는 컴파일러로 컴파일 되었을 때 InnoDB는 이 라이브러리 함수를 사용하였다.

GCC, 윈도우, 솔라리스 함수와 같이 원자적 메모리 접근이 가능하지 않은 플랫폼의 경우 InnoDB는 뮤텍스와 읽기/쓰기 락을 구현하기 위해 P쓰레드 방식을 사용하고 있다.

MySQL이 시작될 때 InnoDB는 원자적 메모리 접근시 뮤텍스를 사용하는지, 혹은 뮤텍스와 읽기/쓰기 락을 사용하였는지, 혹은 둘 다 사용하지 않았는지에 대한 정보를 로그 파일에 남기고 있다. 만약 InnoDB를 구축하기 위해 어떤 툴을 사용하고 있던가, 원자적 동작을 지원하는 CPU를 대상으로 개발하고 있다면 InnoDB는 내장된 뮤텍스 함수를 사용할 것이다. 또는 비교&스왑 동작을 하는 쓰레드 식별자(pthread_t) 가 사용될 수도 있는데 이 경우 InnoDB는 읽기/쓰기 락을 구현할 때도 이 방식을 사용하게 된다.

 

시스템 파라미터(통계정보, 히스토그램)

  1. 통계정보 관련

innodb_stats_auto_recalc=0 or 1 (기본 값: 1)
innoDB에서 생성된 테이블은 통계정보를 영구적으로 저장할 수 있다. 해당 통계정보는 mysql 데이터베이스의 innodb_index_stats 테이블과(인덱스의 경우) innodb_table_stats 테이블에 저장되는데(테이블의 경우) 이 통계정보도 오라클처럼 자동으로 갱신되도록 할 수 있다. 이 파라미터 값이 1(ON)이면 오라클과 마찬가지로 10%의 row가 변경될 때 통계정보를 갱신한다.
다만 테이블의 옵션인 STATS_PERSISTENT 값이 1이거나 innodb_stats_persistent 파라미터가 enable 되어있어야 한다.

use_stat_tables=’never’ | ‘complementary’ | ‘preferably’

통합 통계정보의 저장여부 및 참조 순위를 결정하는 파라미터.
never: 통합 통계정보를 수집하지 않으며 참조하지도 않음.
complementary: 통합 통계정보를 수집하지만 스토리지 엔진의 통계정보를 우선 참조하고 필요한 경우 통합 통계정보를 참조한다.
preferably: 통합 통계정보를 수집하고 우선적으로 참조함. 없는 경우 스토리지 엔진의 통계정보 참조

 

     2. 히스토그램 관련

히스토그램은 10.0.2 버전부터 제공되고 있으며 스토리지 엔진과 독립적으로 mysql.column_stat 테이블에 저장된다.

histogram_size=0~255 (기본 값: 0)
히스토그램을 저장할 공간의 크기를 지정하는 파라미터로 단위는 byte가 된다. mariaDB에서는 Height-Balanced Histogram 알고리즘을 사용하고 버킷에 저장된 최대 값을 1byte를 할당하여 저장한다. 그러나 정확도를 높이고자 한다면 2byte를 할당하여 저장한다.
따라서 histogram_size 값이 0인 경우 히스토그램을 생성하지 않으며 생성되는 버킷의 개수는 히스토그램 정확도를 의미하는 histogram_type 파라미터의 값에 따라 달라지게 된다.
histogram_type 값이 ‘single_prec_hb’이면 histogram_size 값 만큼 버킷이 생성되며
histogram_type값이 ‘double_prec_hb’이면 histogram_size/2 만큼 버킷이 생성된다. 버킷당 2byte를 사용하는 대신 정확성을 늘렸기 때문이다. 히스토그램 알고리즘이나 버킷 개수까지 모두 자동으로 관리해주던 오라클과는 대조되는 부분이다. 

histogram_type=’single_prec_hb’ | ‘double_prec_hb’ (기본 값: single_prec_hb)
히스토그램의 정확도를 나타내며 double_prec_hb로 할 경우 버킷의 개수는 histogram_size/2 가 된다.

optimizer_use_condition_selectivity=1~5
옵티마이저가 최적의 실행계획을 생성하기 위해 어떤 통계정보를 선택할 지 고르는 기준에 대한 파라미터이다.
1: MariaDB 5.5 버전에서 사용하던 선택도 예측 방식을 유지(디폴트 값)
2: 인덱스가 생성된 칼럼의 조건에 대해서만 선택도 판단
3: 모든 칼럼의 조건에 대해서 선택도 판단(히스토그램 사용 안함)
4: 모든 칼럼의 조건에 대해서 선택도 판단(히스토그램 사용)
5: 4에 추가적으로 범위 검색이 아닌 조건에 대해서는 샘플링 정보를 이용해 선택도를 판단

권고 값(필자 생각): histogram_size=255, optimizer_use_condition_selectivity=5

   3. 옵티마이저 관련

Optimizer_search_depth=0~62 (기본 값: 62)
MariaDB에서 테이블 조인시 조인 순서를 결정하는 알고리즘에는 두 가지가 있다. 하나는 Exhaustive 라고 하여 무식한 방법을 이용하는 것(예를 들어 테이블 10개에 대해 조인 경우의 수인 10! 를 모두 계산하는 방식)과 Greedy검색이라고 하는 방법이 있다. Greedy 방식은 좀 더 복잡한 방식을 이용하는데 알고리즘은 아래와 같다.
테이블 N개가 존재할 때 테이블 p개 만큼 선택하여 테이블 p개의 조인에 대한 최적의 순서를 찾아낸다. N개의 테이블에서 P개를 고르는 경우의 수만nCp가 (C: 콤비네이션) 나올 것이므로 nCp개의 테이블 조합마다 각각 최적의 비용이 나올 것이다. 그 중에서 가장 적은 비용을 가지고 있는 조합의 첫 번째 조인 테이블이었던 것을 1번으로 선정한다. 이제 1번을 빼고 남은 N-1개의 테이블에 대해 동일한 작업을 반복한다. 이렇게 선정된 1번부터 N번까지의 테이블이 조인의 순서로 정해지게 된다. 이 방식을 Greedy방식이라고 하는데 여기서 P가 Optimizer_search_depth 파라미터에 의해 결정된다.
기본적으로 이 값은 62인데,  62도 매우 큰 수 일뿐더러 62개의 테이블을 조인할 경우도 드물기 때문에 이 값을 낮추는 것이 좋다. 반면 이 값을 너무 낮추면 테이블 조인 순서가 최적화되지 않을 수도 있기 때문에 적당한 값을 주는 것이 좋다.

Optimizer_prune_level=0 or 1 (기본 값: 1)
1일 경우 위에서 설명한 Greedy 방식으로, 0일 경우 Exhaustive 방식으로 결정한다.

권고 값(필자 생각): Optimizer_search_depth=10, Optimizer_prune_level=1
아니면 세션별로도 설정 가능하기 때문에 테이블 조인이 많은 SQL문이라면 세션에서 파라미터를 적당히 수정한 후 SQL을 수행하는 방법도 있다.