MySQL의 구조(아키텍처)를 파헤쳐보자

반응형

MySQL 엔진 구조

MySQL 서버는 크게 커넥션 핸들러, MySQL 엔진, 스토리지 엔진으로 구분할 수 있다.

 

실행 흐름을 보면 다음과 같은 순서로 요청을 처리한다.

1. 프로그래밍 API (클라이언트 단계 )

프로그래밍 언어로 작성된 애플리케이션에서 MySQL 서버로 쿼리 요청한다.

이 때 각 프로그래밍 언어별 MySQL 커넥터를 통해 통신한다.

 

2. MySQL 엔진의 커넥션 핸들러

클라이언트의 접속 요청을 받아들인다.

이 때 사용자 인증 및 권한 검사 수행과 쿼리 요청에 대한 커넥션 생성 및 관리를 담당한다.

 

3. MySQL 엔진의 쿼리 파서

사용자 요청으로 들어온 쿼리 문장을 MySQL이 인식할 수 있는 토큰으로 분리해 트리 형태의 구조로 파싱한다.

쿼리 문장의 기본 문법 오류는 이 과정에서 발견되고 사용에게 오류 메시지를 전달한다.

 

4. MySQL 엔진의 전처리기

쿼리 파서가 만든 파서 트리를 기반으로 쿼리 문장에 구조적인 문제점이 있는지 확인한다.

각 토큰을 테이블 이름이나 칼럼 이름, 또는 프로시저와 같은 객체를 매핑해

해당 객체의 존재 여부와 객체의 접근 권한 등을 확인하는 과정을 수행한다.

 

5. MySQL 엔진의 옵티마이저

옵티마이저는 검증을 마친 파서 트리를 받아 실행 계획을 만드는 역할로 DBMS의 두뇌와 같은 역할을 한다.

실행 계획을 만들 때는 '규칙 기반', '비용 기반' 최적화 방법이 있는데

대부분의 RDBMS는 기본적으로 통계 정보를 바탕으로 비용 기반 최적화 수행한다.

옵티마이저는 최적화된 쿼리로 재작성, 테이블 스캔 순서 결정, 사용할 인덱스 선택하는 등의 계획을 만든다

 

만약 옵티마이저가 만든 실행 계획을 보고 싶다면 아래와 같이 쿼리 앞에 EXPLAIN 키워드를 붙여 확인할 수 있다.

// 옵티마이저의 실행 계획 보기
EXPLAIN SELECT * FROM table;

 

항상 옵티마이저가 최적의 실행 계획을 만들어주진 않는다는 점을 주의해야한다.

실행 계획을 확인했을 때 옵티마이저가 좋지 않은 선택을 했다면

쿼리 힌트를 사용하여 더 나은 선택을 할 수 있게 유도해 더 좋은 성능을 발휘할 수 있도록 할 수도 있다.

 

6. MySQL 엔진의 실행 엔진

옵티마이저로 부터 만들어진 실행 계획대로 스토리지 엔진을 호출해 받은 결과를 읽고 쓴다.

 

7. 스토리지 엔진

실제 데이터를 디스크 스토리지에 저장하거나 디스크 스토리지로부터 데이터를 읽어오는 부분을 전담한다.

MySQL 서버에서 MySQL 엔진은 하나지만 스토리지 엔진은 여러 개를 동시에 사용도 가능하다.

다음과 같이 스토리지 엔진을 지정해 테이블을 생성할 수도 있다. (생략하면 기본 스토리지 엔진인 InnoDB 선택)

CREATE TABLE member (id BIGINT, name VARCAHR(255)) ENGINE=INNODB;

MySQL 엔진은 옵티마이저가 작성한 실행 계획에 따라서 스토리지 엔진을 적절히 호출해 쿼리를 실행한다.

 

MySQL 엔진과 스토리지 엔진의 통신 방식 - 핸들러 API

MySQL 엔진의 실행 엔진에서 데이터를 쓰거나 읽을 때

각 스토리지 엔진에 쓰기/읽기를 요청하는데 이러한 요청을 핸들러(Handler) 요청이라 한다.

그리고 여기서 사용되는 API를 핸들러 API라고 한다.

스토리지 엔진 또한 이 핸들러 API를 이용해 MySQL 엔진과 데이터를 주고 받는다.

 

다음과 같이 핸들러 API를 통해 얼마나 많은 데이터 작업이 있었는지 확인할 수 있다.

SHOW GLOBAL STATUS LIKE 'Handler%';

 

MySQL 스레딩 구조

MySQL 서버는 멀티 스레드 기반으로 작동하며, 크게 포그라운드와 백그라운드 스레드로 구분된다.

실행 중인 스레드 목록은 다음 명령어를 통해 확인 가능하다.

SELECT thread_id, name, type, processlist_user, processlist_host
FROM performance_schema.threads ORDER BY type, thread_id;

 

포그라운드 스레드 (사용자 스레드)

조회해보면 대부분 접속된 클라이언트 수 만큼 존재하는 것을 볼 수 있다.

포그라운드 스레드는 주로 클라이언트가 요청하는 쿼리를 처리해 사용자 스레드라고도 한다.

클라이언트와 연결이 종료되면 해당 커넥션을 담당하던 스레드는 다시 스레드 캐시로 되돌아간다.

만약 이미 캐시에 일정 개수 이상의 대기 중인 스레드가 있다면 스레드 캐시에 넣지 않고 종료시켜 일정 개수의 스레드 캐시 크기를 유지한다.

이 스레드 캐시 크기는 thread_cache_size 시스템 변수로 설정가능하다.

 

프그라운드 스레드는 데이터를 MySQL의 데이터 버퍼나 캐시로부터 가져오며

없는 경우에는 직접 디스크의 데이터나 인덱스 파일로부터 데이터를 읽어와 작업을 처리한다.

InnoDB는 데이터 버퍼나 캐시까지만 포그라운드 스레드가 처리하고

버퍼로부터 디스크까지 기록하는 나머지 작업은 백그라운드 스레드가 처리한다.

 

백그라운드 스레드

사용자 스레드는 클라이언트의 요청을 직접 처리하는 반면, 백그라운드 스레드는 시스템 전반의 백그라운드 작업을 담당한다.

InnoDB의 경우 다음과 같은 여러 가지 작업이 백그라운드로 처리된다.

  • 인서트 버퍼(Insert Buffer)를 병합하는 스레드
  • 로그를 디스크로 기록하는 스레드
  • InnoDB 버퍼 풀의 데이터를 디스크에 기록하는 스레드
  • 데이터를 버퍼로 읽어 오는 스레드
  • 잠금이나 데드락을 모니터링하는 스레드

MySQL 5.5 버전부터 데이터 쓰기, 읽기 스레드 개수를 2개 이상 지정할 수 있게 됐으며

innodb_write_io_threads와 innodb_read_io_threads 시스템 변수로 스레드의 개수를 설정할 수 있다.

쓰기 스레드는 아주 많은 작업을 백그라운드로 처리하기 때문에 충분히 설정하는 것이 좋다고 한다.

 

사용자의 요청을 처리하는 도중에 데이터 쓰기 작업은 지연(버퍼링)되어 처리될 수 있지만 읽기 작업은 지연될 수 없다.

대부분의 DBMS들은 쓰기 작업을 버퍼 풀에 먼저 기록하고 실제 디스크 쓰기는 나중에 일괄 처리하는 기능이 탑재되어 있다.

InnoDB 또한 이러한 '쓰기 지연' 방식으로 처리한다.

 

포그라운드 스레드와 백그라운드 스레드를 간단히 정리하면

포그라운드 스레드는 사용자의 요청을 받아 요청에 따라 읽기, 쓰기 작업을 처리하고

백그라운드 쓰레드는 주로 시스템 유지보수와 관련된 버퍼 풀에 모여있는 데이터를 디스크에 실제로 기록하는 등의 작업들을 수행한다.

 

MySQL 메모리 할당 및 사용 구조

MySQL에서 사용되는 메모리 공간은 글로벌 메모리 영역, 세션(로컬) 메모리 영역으로 구분한다.

 

글로벌 메모리 영역

MySQL 서버가 시작되면서 운영체제로부터 할당되는 영역이다.

일반적으로 클라이언트 스레드의 수와 무관하게 하나의 메모리 공간만 할당된다.

필요에 따라 2개 이상의 메모리 공간을 할당될 수도 있고, N개라 하더라도 모든 스레드에 의해 공유된다.

 

대표적인 글로벌 메모리 영역은 다음과 같다.

  • 테이블 캐시
  • InnoDB 버퍼 풀
  • InnoDB 어댑티브 해시 인덱스
  • InnoDB 리두 로그 버퍼

 

세션(로컬) 메모리 영역

MySQL 서버상에 존재하는 클라이언트 스레드가 쿼리를 처리하는데 사용되는 메모리 영역이다.

클라이언트 요청당 하나씩 할당되기 때문에 독립적이며 절대 공유되어 사용되지 않는다는 특징이 있다.

 

대표적인 세션 메모리 영역은 다음과 같다.

  • 정렬 버퍼
  • 조인 버퍼
  • 바이너리 로그 캐시
  • 네트워크 버퍼