
본 포스트는 Real MySQL 8.0 1권을 읽은 뒤 정리하는 글입니다.
페이지 압축은 Transparent Page Compression 이라고도 불리는데, MySQL 서버가 디스크에 저장하는 시점에서 데이터 페이지가 압축되어 저장되고, 반대로 MySQL 서버가 디스크에서 데이터 페이지를 읽어올 때 압축이 해제되기 때문이다. 즉 버퍼 풀에 데이터 페이지가 한 번 적재되면 InnoDB 스토리지 엔진은 압축이 해제된 상태로만 데이터 페이지를 관리한다. 그래서 MySQL 서버의 내부 코드에서는 압축 여부와 관계없이 투명하게 작동한다. 이 과정에서 어떤 데이터 페이지를 압축한 결과가 용량이 얼마나 될지 예측이 불가능한데 적어도 하나의 테이블은 동일한 크기의 페이지(블록)로 통일돼야 한다는 것이다.
그래서 페이지 압축 기능은 운영체제별로 특정 버전의 파일 시스템에서만 지원되는 펀치 홀(Punch hole)이라는 기능을 사용한다. 운영체제의 블록 사이즈가 512바이트인 경우, 페이지 압축이 작동하는 방식을 살펴보면 아래와 같다. MySQL 서버는 특정 테이블에 대해 16KB 크기의 페이지를 유지하면서도 압축된 다양한 크기의 데이터 페이지를 디스크에 저장하고 압축된 만큼의 공간을 절약할 수 있다.
- 16KB 페이지 압축 (압축 결과 7KB 가정)
- MySQL 서버는 압축 결과 7KB 및 9KB 빈 데이터 기록
- 디스크에 데이터 기록 후, 7KB 이후 공간 9KB에 대해 펀치 홀을 생성
- 파일 시스템은 7KB만 남기고 나머지 디스크의 9KB 공간은 운영체제로 반납

그러나 MySQL 서버의 페이지 압축이 가진 문제는 펀치 홀 기능은 운영체제뿐만 아니라 하드웨어 자체에서도 해당 기능을 지원해야 사용 가능하며, 파일 시스템 관련 명령어가 펀치 홀을 지원하지 못한다는 것이다. 이러한 이유로 실제 페이지 압축은 많이 사용되지 않는 상태이다.
테이블 압축은 운영체제나 하드웨어에 대한 제약 없이 사용할 수 있어 활용도가 더 높으며 디스크의 데이터 파일 크기를 줄일 수 있는 이점은 존재하나 아래와 같은 몇 가지 단점이 존재한다.
테이블 압축을 사용하기 위해서는 압축을 사용하려는 테이블이 별도의 테이블 스페이스를 사용해야 하며, 이는 innodb_file_per_table 시스템 변수가 ON으로 설정된 상태에서 테이블이 생성돼야 한다. 또한 테이블 압축을 사용하는 테이블은 테이블을 생성할 때 ROW_FORMAT=COMPRESSED 옵션을 명시해야 하며, 추가로 KEY_BLOCK_SIZE 옵션을 이용해 2n(n은 2 이상)이상의 압축된 페이지 타깃 크기를 명시할 수 있다. InnoDB 스토리지 엔진의 페이지 크기가 16KB라면 KEY_BLOCK_SIZE는 4KB 또는 8KB만 설정할 수 있으며, 페이지 크기가 32KB 또는 64KB인 경우 테이블 압축을 적용할 수 없다. 아래는 테이블 압축의 작동 방식 및 이를 나타낸 이미지이다. 테이블 압축에서는 InnoDB 스토리지 엔진의 InnoDB I/O 레이어에서는 아무런 역할을 하지 않는다는 것을 볼 수 있다.
- 16KB 데이터 페이지 압축
1.1 압축된 결과가 8KB 이하이면 그대로 디스크 저장 (압축 완료)
1.2 압축된 결과가 8KB 초과시 원본 페이지 스플릿, 2개의 페이지에 8KB씩 저장

테이블 압축 방식에서 가장 중요한 것은 원본 데이터 페이지의 압축 결과가 목표 크기(KEY_BLOCK_SIZE)보다 작거나 같을 때까지 페이지를 스플릿하는 것이다. 그래서 목표 크기가 잘못 설정되면 MySQL 서버의 처리 성능이 급격히 떨어질 수 있다.
테이블 압축에서 가장 중요한 부분은 압축된 결과가 어느 정도가 될지를 예측해서 KEY_BLOCK_SIZE를 결정하는 것으로, 테이블 압축 적용 전에 KEY_BLOCK_SIZE를 4KB 또는 8KB로 테이블을 생성해 샘플 데이터를 저장한 뒤 적절한지 판단하는 것이 좋다.
KEY_BLOCK_SIZE를 결정하며 주의해야 할 점은 압축 실패율이 높다고 해서 압축을 사용하지 말아야 한다는 것을 의미하지는 않는다. 예를 들어, INSERT만 되는 로그 테이블의 경우 한 번 INSERT되면 이후 다시는 변경되지 않으므로 한 번 정도는 압축 시도가 실패해 페이지 스플릿 후 재압축한다고 하더라도 전체적으로 데이터 파일의 크기가 큰 폭으로 줄어든다면 큰 손해는 아닐 것이다. 물론 반대로 테이블의 데이터가 매우 빈번하게 조회되고 변경된다면 압축을 고려하지 않는 것이 좋다. 압축 알고리즘은 많은 CPU 자원을 소모한다.
InnoDB 스토리지 엔진은 압축된 테이블의 데이터 페이지를 버퍼풀에 적재하면 압축된 상태와 압축이 해제된 상태 2개 버전을 관리한다. 따라서 디스크에 읽은 상태 그대로의 데이터 페이지 목록을 관리하는 LRU 리스트와 압축된 페이지들의 압축 해제 버전인 Unzip_LRU 리스트를 별도로 관리하게 된다.
LRU 리스트는 압축이 적용되지 않은 테이블의 데이터 페이지와 압축이 적용된 테이블의 압축된 데이터 페이지를 모두 가질 수 있으며, Unzip_LRU 리스트는 압축이 적용된 테이블에서 읽은 압축을 해제한 상태의 데이터 페이지 목록을 관리한다.
결국 InnoDB 스토리지 엔진은 압축된 테이블에 대해서는 버퍼 풀의 공간을 이중으로 사용함으로써 메모리를 낭비하며, 압축된 페이지에서 데이터를 읽거나 변경하기 위해서 압축을 해제하는 과정에서 CPU를 상대적으로 많이 소모하게 된다. 이러한 두 가지 단점을 보완하기 위해 Unzip_LRU 리스트를 별도로 관리하고 있다가 MySQL 서버로 유입되는 요청 패턴에 따라서 적절히 다음과 같은 처리를 수행한다.
InnoDB 스토리지 엔진은 버퍼 풀에서 압축 해제된 버전의 데이터 페이지를 적절한 수준으로 유지하기 위해 아래와 같은 어댑티브 알고리즘을 사용한다.
테이블 압축 관련 시스템 변수들로, 모두 페이지 압축 실패율을 낮추기 위해 필요한 튜닝 포인트를 제공한다.
innodb_cmp_per_index_enabled
테이블 압축이 사용된 테이블의 모든 인덱스별로 압축 성공 및 압축 실행 횟수를 수집하도록 설정한다.
innodb_compression_level
InnoDB의 테이블이 지원하는 zlib 압축 알고리즘의 압축률을 0~9까지의 값으로 설정할 수 있으며 값이 작을수록 압축 속도는 빨라지며 저장 공간은 커질 수 있다. 기본값은 6이다.
innodb_compression_failure_threshold_pct, innodb_compression_pad_pct_max
테이블 단위 압축 실패율이 innodb_compression_failure_threshold_pct시스템 설정 값보다 커지면 압축을 실행하기 전 원본 데이터 페이지의 끝에 의도적으로 일정 크기의 빈 공간을 추가해 압축률을 높여 압축 결과가 KEY_BLOCK_SIZE보다 작아지게 한다. 이때 추가는 빈 공간을 패딩(Padding)이라고 하며, 최대 크기는 innodb_compression_pad_pct_max 이상을 넘을 수 없다.
innodb_log_compressed_pages
MySQL 서버가 비정상적으로 종료됐다가 다시 시작되는 경우 압축 알고리즘의 버전 차이가 있더라도 복구 과정이 실패하지 않도록 InnoDB 스토리지 엔진은 압축된 데이터 페이지를 그대로 리두 로그에 기록한다. 기본값은 ON이다.