MySQL 서버는 MySQL 엔진과 스토리지 엔진으로 구성된다. 각각의 엔진이 어떤 역할을 하는지 살펴보자.
MySQL 엔진은 클라이언트의 접속, 쿼리 요청을 처리하는 커넥션 핸들러와 SQL 파서, 옵티마이저 등 실행에 필요한 다양한 구성을 포함하고 있으며, 표준 ANSI SQL 문법을 지원하기 때문에 다른 DBMS와 호환되어 실행할 수 있다.
스토리지 엔진은 실제 데이터를 디스크에 저장하거나 읽어오는 부분에서 사용되며 MySQL 엔진은 하나만 사용할 수 있지만, 스토리지 엔진은 특정 테이블에 어떤 엔진을 사용할 지 정하는 등 여러 개를 같이 사용할 수 있다.
MySQL 엔진이 디스크 작업이 필요하면 스토리지 엔진에게 디스크 쓰기, 읽기를 요청하는데 이러한 요청을 핸들러 요청이라고 하며, 여기서 사용하는 API 를 핸들러 API라고 한다. 핸들러 API만 구현하면 스토리지 엔진으로 사용할 수 있다.
MySQL 서버는 프로세스를 사용하지 않고 스레드 기반으로 동작한다. 스레드는 크게 포그라운드 쓰레드와 백그라운드 쓰레드로 구분할 수 있다.
포그라운드 쓰레드(클라이언트 쓰레드)
최소한 MySQL 서버의 커넥션 수만큼 존재하며, 사용자가 요청하는 쿼리 문장을 처리한다.
커넥션이 종료되면 해당 쓰레드는 쓰레드 캐시에 저장되며, 최대 쓰레드 캐시를 넘어가면 해당 쓰레드는 종료된다. 이는 thread_cache_size 시스템 변수로 설정할 수 있다.
포그라운드 스레드는 버퍼나 캐시에 있는 데이터를 읽어오며 존재하지 않을 경우 직접 인덱스나 파일 데이터를 읽어와서 작업을 처리한다. MyISAM 스토리지 엔진은 디스크 쓰기 작업까지 포그라운드 쓰레드가 처리하지만, InnoDB 엔진은 버퍼와 캐시까지만 포그라운드 쓰레드가 처리한다.
백그라운드 쓰레드
MyISAM 의 경우 포그라운드 쓰레드가 대부분 처리하지만, InnoDB 의 경우 로그를 디스크로 기록하거나, 데이터를 버퍼로 읽어오거나, 버퍼의 데이터를 쓰는 작업을 하는 백그라운드 쓰레드가 존재한다.
기본적으로 쓰기 쓰레드는 하나만 지원했으나, 5.5 버전 이후로는 2개 이상의 디스크 쓰기 쓰레드, 로그 쓰기 쓰레드를 지원한다.
읽기는 바로 진행되어야 하지만, 쓰기는 어느 정도 지연이 있어도 괜찮다. 따라서 쓰기 작업은 버퍼링해서 일괄적으로 디스크에 쓴다. 이 기능은 대부분의 상용 DBMS에 탑재되어 있다. 하지만, MyISAM 은 그렇지 않고 InnoDB 엔진에서만 쓰기 버퍼링을 지원한다.
MySQL 에서 사용하는 메모리 공간은 글로벌 메모리 영역과 로컬 메모리 영역으로 구분되어 사용하며, 글로벌 메모리 공간은 MySQL 서버 시작 시에 바로 할당된다.
글로벌 메모리 영역
클라이언트 쓰레드 수와는 무관하게 하나의 메모리 공간이 할당되며, 모든 쓰레드에 의해 공유된다. 필요에 따라 하나 이상의 메모리 공간을 할당할 수도 있다.
대표적인 글로벌 메모리 영역
테이블 캐시
InnoDB 버퍼 풀
InnoDB 어댑티브 해시 인덱스
InnoDB 리두 로그 버퍼
로컬 메모리 영역
세션 메모리 영역이라고도 불리며, 클라이언트가 쿼리를 처리할 때 사용하는 메모리 영역이다.
클라이언트 별로 할당되며, 다른 스레드와 절대 공유하지 않는다.
필요에 의해 메모리 할당과 해제가 계속해서 이루어진다.
대표적인 로컬 메모리 영역
정렬 버퍼
조인 버퍼
바이너리 로그 캐시
네트워크 버퍼
MySQL 의 독특한 구조 중 하나로, 스토리지 엔진을 플러그인 형태로 사용할 수 있다. 전문 검색 엔진을 위한 검색어 파서나, 인증을 위한 플러그인등을 추가하여 사용할 수 있다.
디스크 쓰기/읽기 작업만 처리하므로 MySQL의 쿼리 처리 로직 등은 변화하지 않는다.
MySQL 8.0 부터는 기존의 플러그인 아키텍처를 대체하기 위한 컴포넌트 아키텍처가 지원된다.
아래와 같은 플러그인의 단점을 보완한다.
플러그인은 오직 MySQL 서버 인터페이스와만 통신할 수 있고, 플러그인간 통신은 할 수 없다.
MySQL 서버의 변수나 함수를 직접 호출하기 때문에 보안상 좋지 않다.
플러그인은 상호 의존 관계를 설정할 수 없어 초기화가 어렵다.
SQL 요청이 들어올 경우 MySQL의 실행순서는 다음과 같다. 쿼리 파서 -> 전처리기 -> 옵티마이저 -> 실행 엔진 -> 스토리지 엔진
쿼리 파서
사용자의 쿼리 문장을 MySQL이 인식할 수 있는 최소 단위의 토큰으로 분리하여 트리 형태의 구조로 만드는 작업을 한다.
쿼리 문장의 기본적인 문법 오류는 이 과정에서 오류 메시지를 전달한다.
전처리기
쿼리 파서에서 만들어진 파서 트리를 받아 구조적인 문제점이 있는지 점검한다.
테이블 이름, 칼럼 이름, 내장 함수 등을 토큰와 매핑한다. 이 과정에서 존재하지 않는 객체이거나, 접근 권한이 없는 객체라면 오류 메시지를 전달한다.
옵티마이저
사용자의 쿼리 문장을 저렴한 비용으로 처리할 수 있도록 최적화하는 작업을 진행한다.
실행 엔진
옵티마이저에서 만들어진 계획대로 쿼리를 수행하기 위해 핸들러에게 필요한 작업을 요청한다.
빠른 응답이 필요한 애플리케이션에서 한 번 사용한 쿼리에 대한 쿼리 결과를 메모리에 캐시하고, 이후 동일한 쿼리 요청이 들어오면 바로 캐시를 리턴하여 매우 빠른 성능을 보여준다.
그러나, 테이블 데이터가 바뀌면 모든 관련된 캐시를 삭제해야해서 심각한 성능 저하가 발생했고, 이후의 MySQL 서버의 발전에서도 이 기능 때문에 많은 성능 저하와 버그의 원인이 되었다.
MySQL 8.0 부터는 쿼리 캐시는 MySQL 서버의 기능에서 완전히 제거되었다.
쓰레드 풀은 MySQL 엔터프라이즈 버전에서 MySQL 서버 프로그램에 내장되어 있다. 이 장에서는 플러그인 형태로 커뮤니티 에디션에서도 사용 가능한 Percona Server 에서 제공하는 쓰레드 풀에 대해 살펴본다.
쓰레드 풀의 특징
사용자의 요청을 처리하는 쓰레드 개수를 줄여서 동시 처리되는 요청이 많아도 제한된 개수의 쓰레드만 동작하여 서버의 자원 소모를 줄인다.
기본적으로 CPU 코어 수와 동일한 수의 쓰레드를 가진 쓰레드 풀을 사용하며, thread_pool_size 시스템 변수를 통해 설정할 수 있다.
쓰레드 풀의 쓰레드가 이미 전부 사용중인데 계속해서 요청이 들어올 경우, thread_pool_oversubscribe(기본값 3) 시스템 변수에 설정된 개수만큼 추가로 쓰레드를 받아서 처리하며, 너무 크게 값을 설정할 경우 컨텍스트 스위칭 비용이 커질 수 있다.
쓰레드 풀이 꽉찼을 경우 새로운 워커 쓰레드를 추가하거나, 수행중인 쓰레드가 끝나길 기다리는 방법이 있다. thread_pool_stall_limit 시스템 변수만큼 요청은 쓰레드를 기다리며, 시간이 넘을 경우 새로운 워커 쓰레드를 추가하는 방식으로 동작한다. 워커 쓰레드를 아무리 늘려도 thread_pool_max_threads 설정값을 넘길 수 없다.
Percona 쓰레드 풀은 선순위 큐와 후순위 큐를 두어 특정 트랜잭션이나 SQL을 우선적으로 처리할 수 있는 기능이 존재한다. 이 기능을 통해서 빨리 트랜잭션을 끝내 lock을 해제하여 성능을 향상시킬 수 있다.
테이블의 구조나 스토어드 프로그램의 정보를 딕셔너리 또는 메타데이터라 부르는데, 5.7 버전까지는 파일 형태로 관리했기 때문에 트랜잭션을 걸 수가 없어, 테이블의 변경, 생성 도중에 MySQL 서버가 비정상적으로 종료되면 테이블이 일관되지 않은 상태로 남는 문제가 있었다.
MySQL 8.0 버전부터는 이러한 정보들을 InnoDB 테이블에 저장하도록 개선하였다. 이 외에도 MySQL 서버 작동에 필요한 데이터들을 InnoDB 테이블에 저장하는 방식으로 개선했다. 시스템 테이블과 딕셔너리 들은 mysql.ibd 테이블스페이스에서 확인할 수 있다.
MyISAM 이나 CSV 스토리지 엔진을 사용하는 테이블은 여전히 메타데이터를 .sdi 파일 형태로 저장하고 있다.