1. MySQL 서버의 전체 구조
MySQL은 크게 2가지의 구조로 나뉩니다. MySQL 엔진과 스토리지 엔진 두 부분으로 구성되어 있으며, 각각은 데이터베이스 처리의 다양한 측면을 담당합니다. MySQL 엔진은 사람의 머리와 같은 역할을 하여, 데이터베이스 서버와의 커넥션 관리, SQL 파싱, 쿼리 최적화 등을 책임집니다. 이를 통해 클라이언트로부터의 요청을 효과적으로 처리하고, 데이터 처리 과정을 최적화합니다.
한편, 스토리지 엔진은 데이터의 저장과 검색, 즉 MySQL의 ‘손과 발’ 역할을 수행합니다. 이 엔진들은 각각의 고유한 특성을 가지고 있어 다양한 데이터 저장 요구사항을 충족시킬 수 있으며, 키 캐시나 InnoDB 버퍼 풀과 같은 기능을 통해 데이터 액세스의 성능을 크게 향상시킵니다. MySQL 서버는 이러한 구성 요소들을 통해 데이터의 일관성을 유지하고, 효율적인 데이터 관리를 가능하게 합니다. 이 서론에서는 MySQL의 내부 구조와 주요 구성 요소의 역할에 대해 소개합니다.
MySQL 엔진: 사람의 머리와 같은 역할을 하며, 주요 구성 요소는 다음과 같습니다.
- 커넥션 핸들러: 클라이언트로부터의 접속 및 쿼리 요청을 처리합니다.
- SQL 파서 및 전처리기: SQL 명령을 해석하고 검증합니다.
- 옵티마이저: 쿼리를 최적화하여 실행합니다.
스토리지 엔진: 손과 발의 역할을 하며, 핸들러 API를 통해 MySQL 서버와 통신합니다. 이 엔진은 각기 다른 특성(예: 키 캐시, InnoDB 버퍼 풀 등)을 가지며 성능 향상에 기여합니다.
핸들러 API : 스토리지 엔진에 쓰기 또는 읽기를 요청하는 API입니다.
- 핸들러 요청 : MySQL 엔진의 쿼리 실행기에서 데이터를 쓰거나 읽어야 할 대 각 스토리지 엔진에 쓰기 또는 읽기를 요청합니다.
- 얼마나 많은 데이터(레코드) 작업이 있었는지
SHOW GLOBAL STATUS LIKE Handler%
명령으로 확인할 수 있습니다.
2. MySQL 스레딩 구조
MySQL 서버는 스레드 기반 구조로, 포그라운드(Foregroud) 스레드와 백그라운드(Backgroud) 스레드로 구분됩니다.
대부분이 백그라운드 스레드이며 thread/sql/one_connection 이라는 스레드 하나가 실제 사용자의 요청을 처리하는 포그라운드 스레드입니다.
포그라운드 스레드 (클라이언트 스레드):
- 각 클라이언트 연결마다 최소 하나의 스레드가 할당됩니다. 이 스레드는 각 클라이언트 사용자가 요청하는 쿼리를 처리합니다.
- 따라서 포그라운드 스레드는 MySQL 서버에 접속 된 클라이언트의 수 보다 많이 존재합니다.
- 연결이 종료되면 스레드는 스레드 캐시(Thread cache)로 반환되며, 스레드 캐시에 일정 개수 이상의 대기 중인 스레드가 잇으면 스레드를 종료시켜 일정 개수의 스레드만 유지하도록 합니다.
- 포그라운드 스레드는 데이터 버퍼나 캐시로부터 데이터를 읽어오며, 데이터가 캐시에 없는 경우 디스크에서 직접 읽어옵니다.
백그라운드 스레드:
- 인서트 버퍼(Insert Buffer)를 병합하는 스레드
- 로그를 디스크로 기록하는 스레드
- InnoDB 버퍼 풀의 데이터를 디스크에 기록하는 스레드
- 데이터를 버퍼로 읽어 오는 스레드
- 잠금이나 데드락을 모니터링하는 스레드
InnoDB 에서도 데이터를 읽는 작업은 주로 클라이언트 스레드에서 처리되기 때문에 읽기 스레드는 많이 설정할 필요가 없지만 쓰기 스레드는 아주 많은 작업을 백그라운드로 처리하기 때문에 디스크를 최적으로 사용할 수 있을 만큼 설정하는 것이 좋습니다.
InnoDB 에서는 쓰기 작업을 버퍼링해서 일괄 처리하는 기능이 탑재되어 있어 INSER, UPDATE, DELETE 쿼리로 데이터가 변경되는 경우 데이터가 디스크의 데이터 파일로 완전히 저장 될 때까지 기다리지 않아도 됩니다.
3. 메모리 할당 및 사용 구조
MySQL은 글로벌 메모리와 로컬 메모리 두 가지 유형으로 메모리를 관리합니다.
3-1. 글로벌 메모리
글로벌 메모리는 클라이언트 스레드 수와 무관하게 하나의 메모리 공간만 할당 됩니다.(필요에 따라 2개도 가능) 크기는 MySQL 서버가 시작되면서 운영체제로부터 MySQL 시스템 변수로 설정해 둔 만큼 할당됩니다.
글로벌 메모리 영역
- 테이블 캐시
- InnoDB 버퍼 풀
- InnoDB 어댑티브 해시 인덱스
- InnoDB 리두 로그 버퍼
3-2. 로컬 메모리
- 클라이언트 스레드가 쿼리를 처리하는데 사용하는 메모리 영역입니다.
- 클라이언트 메모리 영역, 세션 메모리 영역이라고도 합니다.
- 로컬 메모리는 각 클라이언트 스레드 별로 독립적으로 메모리를 할당받아, 공유 되지 않고 사용됩니다.
- 쿼리를 실행하는 순간에만 할당하고 해제하는 공간(소트 버퍼나 조인 버퍼)와 커넥션이 열려 있는 동안 계속 할당된 상태로 남아 있는 공간인 (커넥션 버퍼나 결과 버퍼)가 있다.
로컬메모리 영역
- 정렬 버퍼(Sort buffer)
- 조인 버퍼
- 바이너리 로그 캐시
- 네트워크 버퍼
4. 플러그인 스토리지 엔진 모델
MySQL의 다양한 스토리지 엔진은 핸들러 API를 통해 데이터베이스 엔진과 상호 작용합니다. 이 구조는 MySQL의 유연성과 확장 가능성을 크게 향상시킵니다.
핸들러 : MySQL 엔진이 스토리지 엔진을 조정하기 위한 것 입니다. => MySQL 엔진이 각 스토리지 엔진에게 데이터를 읽어오거나 저장하도록 명령할 때 핸들러를 통해 명령합니다.
5. 컴포넌트
컴포넌트 아키텍처는 플러그인 아키텍처의 단점을 보완해서 구현되었습니다.
- 플러그인은 오직 MySQL 서버와 인터페이스 할 수 있고, 플러그인끼리는 통신할 수 없습니다.
- 플러그인은 MySQL 서버의 변수나 함수를 직접 호추랗기 떄문에 안전하지 않습니다.
- 플러그인은 상호 의존 관계를 설정할 수 없어서 초기화가 어렵습니다.
6. 쿼리 실행 구조
쿼리 파서
- 사용자 요청으로 들어온 쿼리 문장을 토큰(어휘나 기호의 최소 단위로 MySQL이 인식 가능)으로 분리하여 트리 형태의 구조로 만듭니다. 이 과정에서 쿼리 문장의 기본 문법 오류가 발견됩니다.
전처리기
- 파서를 통해 생성된 파서 트리를 사용하여 쿼리 문장에 구조적인 문제점이 있는지 검토합니다. 이 과정에서 토큰을 테이블 이름, 칼럼 이름, 내장 함수 등의 개체에 매핑하며, 개체의 존재 여부와 접근 권한을 확인합니다. 존재하지 않거나 접근할 수 없는 개체는 이 단계에서 제거됩니다.
옵티마이저
- 사용자의 쿼리를 최소 비용으로 가장 빠르게 처리할 수 있는 방법을 결정합니다. 여러 실행 경로 중 최적의 경로를 선택하는 역할을 합니다.
실행 엔진
- 옵티마이저가 선택한 계획을 기반으로, 실행 엔진은 각 핸들러에게 데이터 요청을 보내고, 받은 결과를 다른 핸들러 요청의 입력으로 사용합니다. 이 과정에서 실행 엔진은 쿼리의 논리적 순서대로 작업을 조정하고 처리합니다.
핸들러
- 실행 엔진의 요청을 받아 데이터를 디스크에서 읽거나 디스크로 쓰는 역할을 담당합니다. 데이터의 물리적 입출력 작업을 처리하며, 쿼리의 실제 실행에 필요한 데이터를 제공합니다.
7. 쿼리 캐시
SQL 의 실행 결과를 메모리에 캐시하고, 동일 SQL 쿼리가 실행되면 즉시 결과를 반환하기 때문에 빠른 성능을 보였지만,테이블의 데이터가 변경되면 캐시에 저장된 결과 중에서 변경된 테이블과 관련된 것들은 모두 삭제(Invalidate) 해야 했습니다. 이는 동시 처리 성능 저하를 유발해서 MySQL 8.0 에서 쿼리 캐시는 삭제되었습니다.
8. 스레드 풀
MySQL 서버 엔터프라이즈 에디션은 스레드 풀 기능을 제공하지만, 커뮤니티 에디션은 이 기능을 제공하지 않습니다. 대신 Percona Server에서는 스레드 풀을 지원합니다.
- 스레드 풀의 주요 목적 : 사용자 요청을 처리하는 스레드의 수를 줄여 서버의 CPU가 제한된 수의 스레드만 처리하도록 하여 자원 소모를 최소화합니다.
Percona Server의 스레드 풀은 기본적으로 CPU 코어의 개수만큼 스레드 그룹을 생성합니다. MySQL 서버가 처리해야 할 요청이 생기면 스레드 풀로 처리를 이관합니다. 만약 이미 스레드 풀이 처리 중인 작업이 있는 경우에는 thread_pool_oversubscribe 시스템 변수에 설정된 개수만큼 추가로 더 받아들여서 처리합니다. 이 값이 너무 크면 스케줄링해야 할 스레드가 많아져서 스레드 풀이 비효율적으로 작동할 수도 있습다.
스레드 그룹의 모든 스레드가 일을 처리하고 있을 때 스레드 풀은 스레드 그룹에 새로운 작업 스레드를 추가할지, 아니면 기존 작업 스레드가 처리를 완료할 때까지 기다릴지 여부를 판단해야 합니다.
만약 thread_pool_stall_limit 정의된 밀리초만큼 스레드가 처리 중인 작업을 끝내지 못하면 새로운 스레드를 생성해 스레드 그룹에 추가합니다.
이때 전체 스레드 풀에 있는 스레드의 개수는 thread_pool_max_threads 시스템 변수에 설정된 개수를 넘어설 수 없습다.