
모든 시스템에서 성능 최적화와 운영의 핵심은 내부 동작을 깊이 이해하는 것이다. MySQL을 단순히 사용할 수도 있지만, 내부 아키텍처를 파악한다면 더 큰 이점을 얻을 수 있을 것이다.
그렇기에, MySQL의 엔진 아키텍처를 상세히 살펴보고, MySQL이라는 DBMS가 어떻게 데이터를 처리하는지 알아보도록 하자.
MySQL은 크게 MySQL Engine, Storage Engine 두 가지로 구분할 수 있다.
MySQL 엔진은 클라이언트로부터의 접속 및 쿼리를 분석하고 실행하며 요청을 처리하는 핵심 계층이다. 주요 컴포넌트는 다음과 같다.
앞서 말했던 MySQL 엔진은 요청된 SQL 문장을 분석하거나 최적화하는 등 DBMS의 두뇌에 해당하는 처리를 수행한다.
실제 데이터를 디스크 스토리지에 저장하고 관리하는 계층은 스토리지 엔진이 전담하며, MySQL은 후술할 플러그인 스토리지 엔진 구조를 채택하기에 사용자가 목적에 맞는 스토리지 엔진을 선택할 수 있다.
CREATE TABLE table (fd INT) ENGINE=INNODB;
핸들러 API는 간단히 말하자면 MySQL 엔진과 스토리지 엔진을 연결하는 인터페이스이다. MySQL 엔진의 쿼리 실행기에서 데이터를 쓰거나 읽어야 할 때에는 앞서 말했듯 각 스토리지 엔진에 쓰기 또는 읽기를 요청하는데, 이러한 요청을 Handler Request라고 하고, 여기서 사용되는 API를 Handler API라고 한다.
마치 OS에서 커널 계층에 접근하기 위해 사용자 계층에서 사용하는 System Call과 비슷한 개념이다.
각 Handler에 대해서 얼마나 많은 작업이 있었는지는 아래 명령을 통해 확인 가능하다.
SHOW GLOBAL STATUS LIKE 'Handler%';


MySQL 서버는 스레드 기반으로 작동하는데, 크게 Foreground thread와 Background thread로 나눌 수 있다.
Foreground thread는 클라이언트의 요청을 처리하는 스레드로, 각 연결 마다 개별 스레드가 생성되며, 사용자가 요청하는 쿼리 문장을 처리한다.
만약 클라이언트 사용자가 작업을 끝내고 커넥션을 종료하게 되면, 해당 커넥션을 담당하던 스레드는 다시 Thread Cache로 돌아간다. 이때, 이미 Cache에 일정 수준의 스레드 (thread_cashe_size 시스템 변수 값)가 존재한다면, 스레드를 종료시켜 일정 개수만 캐시에 유지시킨다.
Foreground Thread는 데이터를 MySQL의 데이터 버퍼나 캐시로부터 가져오며, 버퍼나 캐시에 없는 경우에는 직접 디스크의 데이터나 인덱스 파일로부터 데이터를 읽어와서 작업을 처리한다. MyISAM 테이블은 디스크 쓰기 작업까지 해당 스레드가 처리하지만, InnoDB 테이블은 데이터 버퍼나 캐시까지만 처리하고, 나머지 디스크까지 기록하는 작업은 Background Thread에서 처리한다.
앞서 말했듯이 MyISAM과는 별로 해당이 없지만, InnoDB에서는 Background Thread를 통해서 내부적으로 버퍼 풀 관리, 로그 플러시, 트랜잭션 관리등의 역할을 수행한다.
Background Thread에서 가장 중요한 작업은 아무래도 로그 관리 및 쓰기가 가장 중요하며, 사용자의 요청을 처리하는 도중 데이터 쓰기 작업은 버퍼링되어 성능을 최적화 할 수 있지만, 데이터의 읽기 작업은 절대 지연될 수 없다.
SELECT 쿼리를 실행했는데, 나중에 답을 주겠다는 DBMS는 있어서는 안된다.
그래서 대부분의 DBMS는 쓰기 작업을 버퍼링해서 Batch처리 하는 기능이 탑재되어 있으며, InnoDB도 해당 스레드를 통해 이러한 방식으로 처리한다. (MyISAM에서 일반적인 쿼리는 쓰기 버퍼링 기능을 수행하지 않고, 사용자 쓰레드로 쓰기 작업 까지 같이 수행한다.)
MySQL에서 사용되는 메모리 공간은 크게 글로벌 메모리 영역과 로컬 메모리 영역으로 구분 된다.
이름대로 각 영역은 MySQL 서버 내에 존재하는 많은 스레드가 공유 하는 공간인지 아닌지에 따라 구분되는 것이다.
모든 스레드가 공유하는 메모리로, MySQL 서버 전체에서 사용된다. 그렇기에, 클라이언트 스레드 수와 무관하게 하나의 메모리 공간만 할당 된다.
대표적으로 존재하는 메모리 영역은 다음과 같다.
각 클라이언트 스레드가 개별적으로 사용하는 메모리 영역이다.
대표적으로, 커넥션 버퍼와, 정렬 버퍼, 조인 버퍼 등이 존재한다. 이전에 봤던 것처럼, 클라이언트가 MySQL 서버에 접속하면 MySQL 서버에서는 클라이언트 커넥션으로부터 요청을 처리하기 위해 스레드를 하나씩 할당하게 되는데 (Foreground Threads), 이 스레드들이 사용하는 메모리 공간이다.
메모리 사용량이 동적으로 증가 가능하며, 기본적으로 Sort Buffer나 Join Buffer 같은 쿼리에 사용되는 공간은 쿼리 실행중에만 유지되지만, Connection Buffer같은 경우는 커넥션이 계속 열려있으면 할당되어 있는 상태로 남아있다.
그렇기에, 동시 연결이 많거나, 비효율적인 쿼리가 많다면 메모리 사용량이 급증할 수 있어 적절한 조정과 사용량을 관리하는 것이 중요하다.
MySQL은 독특하게 플러그인 방식의 스토리지 엔진을 지원하여, 데이터베이스 사용 목적에 맞게 최적화할 수 있다.
단순하게 아무 엔진이나 사용하면 되지 않나? 라고 생각 할 수 있지만, 생각보다 각 엔진 마다 차이점이 명확하다. 대표적인 스토리지 엔진은 InnoDB, MyISAM, Memory가 존재한다. 이는 이후 포스팅을 통해 InnoDB와 MyISAM에 대해서는 더 깊이 알아보고자 한다.
물론, 스토리지 말고도 다양한 기능을 플러그인 형태로 지원한다. 만약 인증이나 전문 검색, 쿼리 재작성, 커넥션 제어 등 다양한 플러그인을 제공하고 커스텀하게 확장할 수 있다.

MySQL은 동일한 쿼리를 반복적으로 실행할 경우 결과를 메모리 캐싱하여 성능을 높일 수 있다. (있었다.) 하지만, 모든 캐싱이 그렇듯이 쿼리 캐시의 문제점은 테이블의 데이터가 변경되면 캐시에 저장된 결과 중 변경된 테이블과 관련된 모든 것들을 Invalidate처리 해주어야 했다. 이는 성능상의 문제가 되었고, 게다가 수많은 버그의 원인으로 지목되어 결국 MySQL 8.0에서는 제거 되었다.

SQL 문법을 해석하고 파싱 트리를 생성한다. 쿼리 문장의 문법적인 오류는 여기서 발견되고, 사용자에게 오류를 전달한다.
사용자가 입력한 쿼리를 분석하여 테이블 및 컬럼이 존재하는지 확인한다. 실제 존재하지 않거나, 권한상 접근이 불가하다면 이 단계에서 걸러진다.
DBMS의 두뇌같은 역할이다. 가능한 실행 계획을 생성하고, 비용을 분석하여 최적의 실행 계획을 생성한다. 보통 쿼리를 최적화 한다고 하면, 어떻게 하면 옵티마이저가 더 나은 선택을 할 수 있게 유도하는 것이고 그만큼 중요하고 영향력이 크다.
선택된 실행 계획을 바탕으로 스토리지 엔진을 통해 데이터를 가져오거나 변경한다. 앞서 언급했듯이, MyISAM 테이블을 조작하면, MyISAM Storage Engine으로, InnoDB 테이블을 조작하면 InnoDB Storage Engine으로 조작한다.
기본적으로 MySQL은 클라이언트 연결마다 스레드를 생성하는데, 다수의 연결이 존재하면 성능이 저하될 수 있다. 그래서 다수의 클라이언트 요청을 보다 효율하게 처리하기 위해 미리 생성된 스레드 풀 을 유지하면서 클라이언트 요청이 들어오면 기존 스레드를 재사용한다.
기본적으로 CPU 코어의 개수만큼 스레드 그룹을 생성하지만, thread_pool_size 시스템 변수를 통해 조정할 수 있다. CPU와의 협응력을 높이기 위해서는 그룹 개수를 코어 개수와 맞추는 편이 좋다.
또한, thread_pool_oversubscribe 변수를 통해서, Thread Group이 동시 실행 가능한 스레드 개수를 늘려 더 많은 요청을 병렬로 처리할 수 있도록 할 수 있다. 당연히도 과도하게 설정할 경우 오히려 컨텍스트 스위칭 비용이 증가하여 성능 저하가 발생할 수 있다.
🏗 스레드 풀의 내부 동작 방식
- MySQL이 시작되면,
thread_pool_size만큼의 Thread Group이 생성된다.- 클라이언트 요청이 들어오면, Thread Pool은 적절한 Thread Group을 선택하여 요청을 할당한다.
- Thread Group 내부에서 사용 가능한 스레드가 있으면 즉시 실행하고, 없으면 대기 큐에 추가된다.
- 일정 수준 이상 요청이 몰릴 경우, Thread Group 내부에서 새로운 스레드를 동적으로 생성하여 처리한다.
- 처리된 요청은 다시 Thread Pool로 반환되며, 사용되지 않는 스레드는 일정 시간이 지나면 종료될 수 있다.
Real MySQL 8.0 (1권)
https://dev.mysql.com/doc/refman/8.4/en/pluggable-storage-overview.html
https://blog.ex-em.com/1679
https://www.geeksforgeeks.org/architecture-of-mysql/
https://shashwat-creator.medium.com/mysqls-logical-architecture-1-eaaa1f63ec2f