MySql 아키텍처

Jaeyoung·2023년 4월 13일
0
post-thumbnail

MySql 서버는 Mysql 엔진과 스토리지 엔진으로 크게 두가지로 나눌 수 있습니다. Mysql엔진과 스토리지 엔진이 어떻게 구성되어있고 어떤 역할을 하는지 한번 알아보도록 하겠습니다.

MySql 엔진과 스토리지 엔진

MySql엔진은 사람으로 치면 두뇌에 해당하는 작업들을 담당합니다. 클라이언트로부터의 접속 및 쿼리 요청을 처리하는 커넥션 핸들러와 SQL 파서 및 전처리기, 쿼리 최적화를 위한 옵티마이저로 구성되어있습니다. 전반적인 처리는 MySql 엔진에서 처리하는 것을 알 수 있었습니다. 그러면 스토리지 엔진은 어떤 역할을 할까요? 스토리지 엔진은 실제 데이터를 디스크에 저장하거나 디스크로부터 데이터를 읽어오는 역할을 담당합니다. 또한 스토리지 엔진은 여러 개를 동시에 사용할 수 있기 때문에 각각 테이블에 서로 다른 스토리지 엔진을 설정할 수 있습니다. MySql은 MyISAM 스토리지 엔진과 InnoDB 스토리지 엔진을 내장하고 있습니다. 스토리지 엔진이 읽기 쓰기 작업을 하려면 MySql엔진과 통신해야하는데 통신하기 위한 API를 핸들러 API라고 합니다.

스레딩 구조

MySql 서버는 프로세스 기반이 아니라 스레드 기반으로 동작합니다. 이러한 스레드는 크게 포그라운드(사용자) 스레드와 백그라운드 스레드로 나뉘어집니다. 포그라운드 스레드는 클라이언트 스레드라고도 불리며 최소 MySql서버에 접속된 클라이언트 수만큼 존재하고 클라이언트 사용자가 요청한 쿼리문을 처리합니다. 클라이언트가 커넥션을 끊게 되면 해당 커넥션에서 사용했던 스레드는 스레드 캐시로 돌아가거나 스레드 캐시안에 설정된 크기만큼 스레드가 존재한다면 스레드를 종료시킵니다. 그래서 포그라운드 스레드는 데이터 버퍼나 캐시로 부터 데이터를 가져오며, 버퍼나 캐시에 데이터가 없는경우 디스크나 인덱스 파일으로 부터 데이터를 읽어와서 처리합니다. MyISAM 스토리지 엔진 같은 경우는 디스크 쓰기 작업까지 처리하지만 InnoDB 스토리지 엔진은 쓰기 작업은 백그라운드 스레드에서 처리합니다. 아까 이야기했듯이 MyISAM에서는 포그라운드 스레드에서 읽기쓰기 요청이 모두 처리되기 때문에 InnoDB에서 백그라운드 스레드가 어떤 동작을 하는지 알아보겠습니다. 인서트 버퍼를 병합하는 작업, 로그를 디스크로 기록하는 작업, 버퍼 풀의 데이터를 디스크에 기록하는 작업, 잠금이나 데드락을 모니터링 하는 작업 등을 처리해줍니다. 이러한 스토리지 엔진의 차이로 MyISAM은 쓰기 작업을 포그라운드 스레드에서 처리되기 때문에 쓰기 작업에 대해 지연되어 처리될 수 없고 InnoDB 같은 경우는 데이터의 변경이 일어날 떄 버퍼링해서 일괄 처리할 수 있어서 디스크에 완전히 저장 될 때까지 기다리지 않아도 됩니다.

메모리 할당 및 사용 구조

MySql에서 사용되는 메모리 공간은 크게 글로벌 메모리 영역과 로컬 메모리 영역으로 구분할 수 있습니다. 글로벌 메모리 영역은 운영체제로 부터 서버가 실행되면서 할당 됩니다. 글로벌 메모리 영역은 클라이언트 스레드의 수와 무관하게 하나의 메모리 공간만 할당되고 필요에 의해 2개 이상의 메모리 공간을 할당받을 수도 있습니다. 이러한 영역은 테이블 캐시, InnoDb 버퍼 풀, InnoDB 어댑티브 해시 인덱스, InnoDB 리두 로그 버퍼로 되어있고 모든 스레드에 의해 공유됩니다. 다음은 로컬 메모리 영역인데요 세션 메모리 영역이라고도 불리며 클라이언트 스레드가 쿼리를 처리하는 데 사용하는 메모리 영역입니다. 이러한 특징 때문에 스레드별로 독립적으로 할당되어 절대 공유되지 않습니다. 로컬 메모리 영역은 정렬 버퍼, 조인 버퍼, 바이너리 로그 캐시, 네트워크 버퍼 이렇게 구성되어있습니다.

플러그인과 컴포넌트

MySQL은 스토리지 엔진, 전문 검색 엔진을 위한 검색어 파서 혹은 사용자의 인증을 위한 Native Authentication과 Caching SHA-2 Authentication 등을 댜양한 기능들을 플러그인으로 구현되어 제공됩니다. 그래서 사용자는 Custom한 스토리지 엔진을 직접 개발해서 적용 시킬 수도 있습니다. MySql 8.0 부터는 기존의 플러그인 아키텍처를 대체하기 위해 컴포넌트 아키텍처가 지원됩니다. 플러그인 방식은 몇 가지 단점이 존재하는데 다음과 같습니다. 플러그인은 오직 MySql 서버와 인터페이스할 수 있고 플러그인 끼리는 통신할 수 없습니다. 플러그인은 MySql 서버의 변수나 함수를 직접 호출하기 때문에 캡슐화가 되지않아 안전하지 않습니다. 플러그인은 상호 의존 관계를 설정할 수 없어서 초기화가 어렵습니다. 컴포넌트는 이러한 단점들을 보완하였습니다.

쿼리 실행 구조

쿼리를 실행하는 과정은 크게 5가지로 나눌 수 있습니다. 먼저 쿼리 파서 작업입니다. 쿼리 파서 작업은 사용자 요청으로 들어온 쿼리 문장을 토큰(MySql이 인식할 수 있는 최소 단위의 어휘나 기호)로 분리해 트리 형태의 구조로 만들어 내는 작업을 이야기합니다. 이 과정에서 쿼리의 기본 문법 오류가 발견되고 사용자에게 오류 메세지를 전달하게 됩니다. 그 다음으로는 전처리기 과정인데요 파서 과정에서 만들어진 파서 트리를 기반으로 쿼리 문장에 구조적인 문제가 있는지 확인합니다. 이러한 과정에서 각 토큰을 테이블 이름이나 칼럼 이름, 내장 함수와 같은 개체를 매핑해서 해당 객체의 존재 여부와 접근 권한 등을 확인하고 사용할 수 없는 토큰인 경우에는 이 단계에서 걸러집니다. 다음은 옵티마이저인데요 쿼리를 저렴한 비용으로 빠르게 처리할지를 결정하는 역할을 담당합니다. 다음은 실행 엔진 입니다. 실행 엔진은 만들어진 계획대로 각 핸들러에게 요청해서 받은 결과를 또 다른 핸들러 요청의 입력으로 연결하는 역할을 수행합니다. 그 다음은 앞에서 이야기 했던 핸들러 입니다. 실행 엔진의 요청에 따라 데이터를 디스크에 저장하고 읽어오는 역할을 담당합니다. 결국 이 핸들러는 스토리지 엔진을 이야기합니다.

트랜잭션 지원 메타데이터

데이터베이스 서버에서 테이블의 구조 정보와 스토어드 프로그램(트리거, 사용자 정의 함수, 스토어드 프로시저, 이벤트 스케줄러) 등의 정보를 데이터 딕셔너리 또는 메타데이터라고 하는데 5.7 버전까지 테이블의 구조를 FRM 파일에 저장하고 일부 스토어드 프로그램 또한 파일 기반으로 관리했습니다. 이러한 파일 기반의 메타데이터는 생성 및 변경 작업이 트랜잭션을 지원하지 않기 때문에 테이블의 생성 도는 변경 도중 MySql 서버가 비정상적으로 종료되면 일관되지 않은 상태로 남는 문제가 있었습니다. 그래서 이러한 문제를 해결하기 위해 관련 정보를 모두 InnoDB의 테이블에 저장하도록 개선했습니다. 그래서 스키마 변경 작업 중간에 MySql 서버가 비정상적으로 종료된다고 하더라도 작업이 완전하게 성공하거나 실패하게 됩니다.

InnoDB 스토리지 엔진

InnoDB는 MySql에서 사용할 수 있는 스토리지 엔진 중 거의 유일하게 레코드 기반의 잠금을 제공합니다. 그 대문에 높은 동시성 처리가 가능하고 안정적이며 성능이 좋습니다. InnoDB의 모든 테이블은 기본적으로 프라이머리키를 기준으로 클러스터링되어 저장됩니다. 모든 세컨더리 인덱스는 레코드의 주소 대신 프라이머리 키의 값을 논리적인 주소로 사용합니다. 그렇기 때문에 프라이머리 키를 이용한 레인지 스캔은 상당히 빨리 처리될 수 있습니다. 결과적으로 쿼리의 실행 계획에서 프라이머리 키는 기본적으로 다른 보조 인덱스에 비해 비중이 높게 설정됩니다.

MVCC(Multi Version Concurrency Control)

일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능이며, MVCC의 가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공하는 것 입니다. InnoDB는 언두 로그를 이용해 이 기능을 구현합니다. MVCC의 멀티 버전의 의미는 하나의 레코드에 여러 개의 버전이 동시에 관리된다는 의미입니다. 그러면 어떻게 구현되는지 알아보도록 하겠습니다. 어떤 한 테이블이 있고 그 테이블에 어떤 데이터를 insert시켜주게 되면 InnoDB 버퍼 풀하고 디스크에 에 insert한 데이터가 존재하게 됩니다. 이때 update로 해당 레코드의 값을 변경해주게 된다면 InnoDB 버퍼 풀에는 변경된 값이 남아있게되고 언두 로그에 변경 전의 값을 복사해주게 됩니다. 버퍼 풀의 변경 내용은 InnoDB 엔진의 백그라운드 스레드에 의해 기록됩니다. 그렇기 때문에 디스크에 기록되었는지는 시점에 따라 달라집니다. 다시 돌아가서 만약 이때 다른 트랜잭션에서 쿼리로 작업중인 레코드를 조회하면 어디에 있는 데이터를 조회할까요? 이는 트랜잭션의 격리 수준에 따라 달라집니다. 만약 격리 수준이 READ_UNCOMMITTED로 되어있다면 버퍼 풀에 있는 변경된 데이터를 조회하게 됩니다 하지만 READ_COMMITTED나 그 이상의 격리 수준에서는 변경전 데이터인 언두 로그의 데이터를 반환합니다. 이러한 과정을 DBMS에서는 MVCC라고 합니다. 그리고 이런 상태에서 COMMIT과 ROLLBACK을 하게 되면 COMMIT 명령인 경우 현재 상태를 영구적인 데이터로 만들어 버립니다. 하지만 ROLLBACK 명령인 경우 언두 영역에 있는 백업된 데이터를 InnoDB 버퍼 풀로 다시 복구하고 언두 영역의 내용을 삭제합니다. 삭제는 하지만 언두 영역의 백업 데이터는 항상 바로 삭제되는게 아닌 해당 언두 영역을 필요로 하는 트랜잭션이 없는 경우 삭제됩니다.

자동화된 장애 복구

InnoDB에는 손실이나 장애로부터 데이터를 보호하기 위한 여러가지 메커니즘이 탑재돼 있습니다. 이러한 메커니즘을 이용해서 서버가 시작될 때 완료되지 못한 트랜잭션이나 디스크에 일부만 기록된 데이터 페이지 등에 대한 일련의 복구 작업이 자동으로 진행됩니다. 드문 상황이지만 만약 이 단계에서 자동으로 복구될 수 없는 손상이 있다면 자동 복구를 멈추고 MySQL 서버는 종료됩니다. 이러한 경우에는 mysqldump를 이용해 데이터를 가능한 만큼 백업하고 MySql 서버의 DB와 테이블을 다시 생성하는 것이 좋습니다. 어떤 상황에서는 풀 백업과 바이너리 로그로 복구하는 편이 데이터 손실이 더 적을 수도 있습니다.

InnoDB 버퍼 풀

InnoDB 스토리지 엔진에서 가장 핵심적인 부분으로 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해 두는 공간입니다. 쓰기 작업을 지연시켜 일괄 작업으로 처리할 수 있게 해주는 버퍼 역할도 같이 합니다. 이러한 버퍼 역할로 랜덤한 디스크 작업의 횟수를 줄일 수 있습니다. InnoDB 스토리지 엔진은 버퍼 풀이라는 거대한 메모리 공간을 페이지 크기의 조각으로 쪼개어 InnoDB 스토리지 엔진이 데이터를 필요로 할 떄 데이터 페이지를 읽어서 각 조각에 저장합니다. 버퍼 풀의 페이지 크기 조각을 관리하기 위해 LRU 리스트와 Flush 리스트 그리고프리 리스트라는 3개의 자료 구조를 관리합니다. 프리 리스트는 버퍼 풀에서 실제 사용자 데이터로 채워지지 않은 비어 있는 페이지들의 모록이고 사용자의 쿼리가 새롭게 디스크의 데이터 페이지를 읽어와야 하는 경우 사용됩니다. LRU 리스트는 디스크로부터 한 번 읽어온 페이지를 최대한 오랫동안 버퍼 풀의 메모리에 유지해서 디스크 읽기를 최소화 시키기 위해 사용합니다. 플러시 리스트는 디스크로 동기화되지 않은 데이터를 가진 데이터 페이지의 변경 시점 기준의 페이지 목록을 관리합니다.

언두 로그

InnoDB 스토리지 엔진은 트랜잭션과 격리 수준을 보장하기 위해 DML로 변경되기 이전 버전의 데이터를 별도로 백업합니다. 이러한 데이터를 언두 로그라고 합니다. 언두 로그는 트랜잭션 보장과 격리 수준 보장에 사용됩니다. 트랜잭션이 롤백되면 트랜잭션 도중 변경된 데이터를 변경 전 데이터로 복구하기 위해 언두 로그에 백업해둔 이전 버전의 데이터를 이용해 복구합니다. 또한 특정 커넥션에서 데이터를 변경하는 도중에 다른 커넥션에서 데이터를 조회하면 트랜잭션 격리 수준에 맞게 변경중인 레코드를 읽는게 아닌 언두 로그에 백업해둔 데이터를 읽어서 반환하기도 합니다.

체인지 버퍼

RDBMS에서 레코드에 변경작업이 일어나면 해당 테이블에 포함된 인덱스를 업데이트하는 작업도 필요합니다. 하지만 인덱스를 업데이트하는 작업은 랜덤하게 디스크를 읽어야 하기 때문에 테이블에 인덱스가 많을수록 상당히 많은 자원을 소모하게 됩니다. 그렇기 때문에 InnoDB는 변경해야 할 인덱스 페이지가 버퍼 풀에 있으면 바로 업데이트를 수행하지만 디스크로부터 읽어와야한다면 즉시 실행하는게 아닌 임시 공간에 저장해두고 바로 결과를 반환하는 형태로 성능을 향상시키게 되는데 이러한 임시 공간을 체인지 버퍼라고 합니다. 중복 여뷰를 체크해야 하는 유니크 인덱스 같은 경우는 체인지 버퍼를 사용할 수 없습니다.

리두 로그

리두 로그는 트랜잭션의 요소인 영속성과 가장 연관되어있습니다. 리두 로그는 하드웨어나 소프트웨어 등 여러 가지 문제점으로 인해 MySql 서버가 비정상적으로 종료되었을때 디시크에 기록되지 못한 데이터를 잃지 않게 해주는 안전장치 입니다. 대부분의 데이터베이스 서버는 데이터 변경 내용을 로그로 먼저 기록합니다. 데이터 파일은 쓰기보다 읽기 성능을 고려한 자료 구조의 특성을 가지고 있기 때문에 쓰기 작업은 디스크의 랜덤 액세스가 필요합니다. 그렇기 때문에 쓰기 작업은 상대적으로 큰 비용이 발생합니다. 이러한 성능 저하를 막기 위해 쓰기 비용이 낮은 자료 구조를 가진 리두 로그를 가지고 있으며 비정상 종료가 되면 리두 로그의 내용을 이용해서 종료되기 전 상태로 복구합니다.

어댑티브 해시 인덱스

어댑티브 해시 인덱스는 사용자가 자주 요청하는 데이터에 대해 자동으로 생성하는 인덱스입니다. 일반적으로 알고 있는 인덱스는 B-Tree 인덱스인데요 B-Tree 인덱스는 루트 노드를 거쳐서 브랜치 노드 그리고 리프 노드까지 찾아가야 원하는 레코드를 읽을 수 있습니다. 어댑티브 해시 인덱스는 이러한 B-Tree 검색 시간을 줄여주기 위해 도입된 기능입니다. InnoDB 스토리지 엔진은 자주 읽히는 데이터 페이지의 키 값을 이용해 해시 인덱스를 만들고 필요할 때마다 어댑티브 해시 인덱스를 검색해서 레코드가 저장된 데이터 페이지를 즉시 찾아갈 수 있습니다. 그렇기 때문에 쿼리의 성능이 빨라져서 동시에 많은 쿼리를 처리할 수 있습니다. 해시 인덱스는 인덱스 키 값과 데이터 페이지 주소의 쌍으로 관리되는데 B-Tree 인덱스의 고유번호와 B-Tree 인덱스의 실제 키 값으로 생성됩니다. 이렇게 고유번호를 키 값으로 두는 이유는 어댑티브 해시 인덱스는 InnoDB 스토리지 엔진에서 하나만 존재하기 때문입니다. 어댑티브 해시 인덱스가 성능 향상에 도움이 되지 않는 경우는 다음과 같습니다. 디스크 읽기가 많은 경우, 특정 패턴의 쿼리가 많은 경우(조인이나 Like 검색), 매우 큰 데이터를 가진 테이블의 레코드를 폭넓게 읽는 경우

참고 도서

http://www.yes24.com/Product/Goods/103415627

profile
Programmer

0개의 댓글