* 이글은 RealMysql 책을 참고하여 작성한 내용입니다.
db 성능은 디스크 접근횟수에 달려있다.
일반적으로 메모리는 디스크보다 1000~1만 배 빠르다.
최대한 메모리에서 많은 작업을 하고 디스크 접근을 최소화해야한다.
mysql db를 공부하며 위의 전제조건이 중요하다.
mysql 아키텍쳐 설계는 쓰기 작업의 안정성도 보장하지만 무엇보다 읽기 성능 향상을 위해 설계되어있다.
위 내용을 중점에 두고 글을 이해해보자.
mysql 아키텍쳐
[mysql 엔진]
- 커넥션 핸들러
- 클라이언트 접속 허용
- 쿼리 실행 요청 - sql 파서
- 쿼리를 토큰(mysql이 인식할 수 있는 최소 단위의 어휘나 기호)으로 분리해 트리 형태의 구조를 만들어 내는 작업
- 기본 sql 문법 오류 검사 - sql 전처리기
- 쿼리 파서에 의해 만들어진 트리를 기반으로 테이블, 칼럼, 내장 함수 등이 실제 존재하는지 검증
- 접근 권한 검증 - 옵티마이저
- 쿼리 변환
- 비용 최적화 실행 계획 수립 - 캐시 & 버퍼
- 캐시 : SELECT 쿼리의 결과를 저장하여 이후 요청시 해당 결과를 반환(8.0 부터 제거)
- 버퍼 : 디스크에 존재하는 데이터 파일을 메모리 공간에 두어 읽기와 쓰기 작업의 안정성과 효율을 위한 공간
- 위 그림에서 버퍼 공간이 mysql 엔진 영역으로 포함되어있지만 innoDB의 버퍼도 포함되어있다.
[스토리지 엔진]
실제 데이터를 디스크 스토리지에 저장하거나 읽어오는 역할을 담당한다.
mysql 엔진은 명령을 통해 수행되는데 핸들러 api를 통해 이루어진다.
스토리지 엔진은 여러 플러그인으로 이루어진 아키텍쳐를 갖추고 있다.
플러그인을 통해 mysql 함수, 전문 검색, 사용자 인증과 같은 기능을 제공해준다.
- innoDB (5.5 이후 기본 설정 엔진)
- 트랜잭션 지원, 외래키 지원
- mvcc 지원(락 미사용으로 동시 처리 성능 효율)
- 클러스터 인덱스 사용 - MyIsam
- 트랜잭션 미지원(빠른 읽기 성능)
- 테이블 단위의 락 동작 방식(동시 처리 성능 비효율)
mysql 스레딩 구조
mysql 스레드는 mysql 엔진에서 주로 활동하는 스레드와 스토리지 엔진에서 주로 활동하는 스레드가 존재한다.
- 포그라운드 스레드
- 클라이언트 스레드 : 커넥션 연결시 사용자에게 할당되는 스레드
mysql 엔진 영역에서만 활동(디스크 접근 X) - 백그라운드 스레드
- 로그 스레드 : 로그를 디스크에 기록
- 버퍼 스레드 : 버퍼 풀의 데이터를 디스크에 저장
- 기타 등등 디스크의 데이터 파일을 메모리로 가져오거나 모니터링을 수행하는 수많은 백그라운드 스레드가 존재함
사용자의 요청 처리를 빠르게 진행하기 위해 자주 사용되는 데이터는 메모리 공간에 적재되어있고 포그라운드 스레드는 메모리 공간에 주로 작업을 수행한다.
백그라운드 스레드는 메모리 데이터와 디스크 데이터를 동기화하는데 주로 사용된다.
만일, 메모리 공간에 적재된 데이터가 없다면 백그라운드 스레드를 통해 디스크에 접근해서 데이터를 가져오는데 이러한 처리는 성능 저하의 원인이 된다.
mysql의 내부 알고리즘에 의해 이러한 접근이 자주 발생하지는 않지만 서버 재시작을 하는 경우 메모리가 비워져 있을 수 있다. 허나, 최신 mysql에서는 메모리 공간의 데이터 페이지도 백업하여 재시작시 적재하도록 하여 성능 저하를 막는다.
버퍼풀
버퍼풀의 역할
- 디스크의 데이터 파일이나 인덱스 정보를 저장함 (읽기 성능 향상 목적)
- 변경 사항이 발생한 데이터의 정보를 저장하고 한번에 디스크에 저장하는 버퍼 역할 (쓰기 성능 향상 목적)
버퍼의 구조
- innoDB 버퍼풀
- **언두 로그** : 트랜잭션 시작 전 여러 버전의 데이터
- 데이터 페이지
- 메모리에 존재하는 데이터를 페이지 형태로 저장
- 클린 페이지 : 변경사항이 없음
- 더티 페이지 : 변경사항이 있음
- **로그 버퍼**
- 리두 로그 : 트랜잭션 커밋 후 리두 로그가 기록되고 즉시 디스크의 리두 로그 파일에 기록됨
* 시스템 테이블 스페이스(디스크 영역)
- 데이터 파일, 인덱스 정보 등 디스크에 영구 저장되는 정보
* 리두 로그 (디스크 영역)
- 로그 버퍼의 리두 로그가 디스크에 영구 저장되는 정보
InnoDB의 특징
1. pk에 의한 클러스터링
InnoDB의 모든 테이블은 기본적으로 pk를 기준으로 클러스터링되어 저장된다.
즉, pk 순서대로 저장된다.
모든 세컨더리 인덱스는 레코드 주소가 아닌 pk의 값을 논리적인 주소로 사용한다.
따라서 pk를 통한 조회가 세컨더리 인덱스를 통한 조회보다 항상 빠르다.
2. 외래키 지원
외래키는 부모-자식 테이블간의 관계를 설정해주는 장점이 있지만, 단점 또한 존재한다.
변경시 부모 테이블과 자식 테이블 모두 데이터가 존재하는지 체크를 해야하므로 잠금이 전파되고, 인덱스 또한 부모-자식 테이블 모두에 필요로 한다.
3. MVCC(Multi Version Concurrency Control)
잠금을 사용하지 않더라도 일관된 읽기를 가능하도록 하는 기능을 제공해주는 것이 MVCC이다.
InnoDB의 메모리 영역에는 버퍼풀과 로그 영역이 존재하고 디스크에 데이터 파일이 존재한다.
만약, 트랜잭션을 시작하고 특정 레코드를 update하면 버퍼풀과 디스크 영역의 레코드는 update 된다.
허나, 로그 영역의 언두 로그는 변경되기 이전에 데이터를 보관하고 있다.
트랜잭션이 커밋되기 전에 다른 사용자가 해당 레코드를 조회하면 버퍼나 디스크가 아닌 언두 로그의 데이터를 조회하여 커밋(영구 반영)되기 이전의 데이터를 읽을수 있다.
4. 자동 데드락 감지
잠금으로 인한 교착상태에 빠지지 않기 위해 데드락 감지 스레드를 통해 주기적으로 교착상태에 빠진 트랜잭션을 찾아 강제 종료한다.
언두 레코드를 적게 가진 트랜잭션이 우선순위를 갖는다. 언두 레코드 양이 적다는 것은 롤백시 제거해야할 언두 레코드가 적다는 것이므로 롤백으로 인한 부하를 줄일 수 있다.
데드락 감지 스레드는 잠금목록을 검사하기 위해 잠금 목록을 가진 테이블을 새롭게 잠금하는데 이로인해 잠금목록에 존재하는 레코드를 변경하는 트랜잭션이 작업을 수행하지 못하여 성능 저하를 유발할 수 있다. 만약 동시 처리 스레드가 많거나, 트랜잭션에 의한 잠금 레코드가 많다면 이로인한 부하는 더욱 더 커지게 된다.
-> mysql은 데드락 감지 스레드로 인한 새로운 부하를 해결하기 위해 특정 시간이 지나면 데드락 상황이 발생한 트랜잭션을 자동으로 실패하게 할 수 있다.(innodb_lock_wait_timeout)
언두 로그 : 고립성과 MVCC의 핵심
서로 다른 트랜잭션이 레코드를 변경하고 데이터를 읽었을 때, 두 트랜잭션의 작업은 서로 영향을 미치지 않기에 select를 했을 때 다른 트랜잭션이 변경한 내용은 나의 트랜잭션 내에서 보이지 않는다. 이는 실제 데이터 파일을 읽어들이는게 아니라 언두 로그의 데이터를 읽었기에 가능한 것이다. 트랜잭션이 시작되고 데이터를 변경하면 데이터 페이지에 변경사항을 기록하고 변경되기 이전 데이터를 언두로그에 저장한다. 다른 트랜잭션은 현재 변경 중인 데이터는 언두로그를 통해 읽어온다. 따라서 트랜잭션이 데이터를 변경하더라도 읽어오는 공간이 언두로그이므로 영향을 미치지 않는 것이다.(고립성)
또한 트랜잭션 수행 중 롤백을 하기 위해 트랜잭션 수행 이전 데이터를 읽어 오는 공간도 언두 로그이다.
언두 로그는 여러 버전의 데이터가 존재한다.(Mutli Version Conccurency Control : 다중 버전 동시제어)
이 버전은 트랜잭션 id로 구분한다. 특정 트랜잭션이 커밋하여 변경한 데이터 상태를 버전별로 기록하고 있다.
모든 트랜잭션은 순차적으로 증가하는 시퀀스 넘버를 id로 할당 받는다.
트랜잭션은 select 작업시 자신의 id보다 작은 언두로그를 참조한다.
만약, 언두로그를 사용하지 않는 방식에서는 트랜잭션 고립성을 지키기 위해 잠금 처리를 수행한다.
허나, 잠금 처리를 수행하면 동시 처리 성능이 매우 떨어진다.
언두로그를 이용하면 잠금처리 없이 수많은 트랜잭션이 같이 시작될 수 있다.
이를 '잠금없는 일관된 읽기'라고 표현하며 이를 통해 높은 동시성 처리가 가능해진 것이다.
예시를 통해 자세히 알아보자.
트랜잭션 10, 11, 12에서 변경하거나 조회하는 레코드는 모두 동일하고 9번 트랜잭션에서 최종 변경했다고 하자.
10번 트랜잭션이 9번 트랜잭션에 의해 커밋된 레코드를 변경하면 트랜잭션 id가 9번인 언두로그가 생성된다.
11, 12는 해당 레코드를 조회할 때, 트랜잭션 수행 이전에 커밋된 데이터가 저장된 언두로그에서 데이터를 가져온다.
따라서 각 트랜잭션에서 자신이 변경한 레코드를 제외한 나머지 조회결과는 모두 같다.
transaction 10 : |----------start------------------------------commit--------------|
transaction 11 : |-----------------------start----------------------------commit--------------|
transaction 12 : |-------------------------------start----------------------------------------commit--------------|
언두로그 삭제는 어느 시점에 이루어질까
모두 9를 사용하기에 9를 사용하는 12까지 커밋이 이루어져야 언두로그 9가 삭제된다.
만일 트랜잭션이 아래와 같이 사용되었다면 트랜잭션 11과 12에서 언두로그 10을 사용함으로써 언두 로그 9는 트랜잭션 10이 커밋되고 삭제된다.
transaction 10 : |----start------commit--------------|
transaction 11 : |------------------------------start----------------------------commit--------------|
transaction 12 : |-------------------------------------start----------------------------------------commit--------------|
여러 버전의 데이터를 언두로그에 저장하면 동시성 처리에도 좋고 디스크에 접근하는 횟수가 줄어드므로 읽기 성능은 당연히 좋아질 것이다.
허나, 사용하지 않는 언두로그를 메모리에 저장하고 있는 것은 좋지 못하다.
만일 트랜잭션이 길어지면 해당 언두로그를 사용하는 트랜잭션이 많아지고 다른 트랜잭션의 언두로그들도 모두 저장되야하므로 메모리에 부하를 줄 수 있다.
따라서 개발자의 실수나 잘못으로 트랜잭션 처리를 길게하는 상황은 좋지 못하다.
[참고1] 언두로그도 별도의 로그파일(디스크)에 기록함
비정상적 종료 이후 재시작 될 때, 로그파일을 통해 언두로그를 메모리에 복구하여 mvcc를 지원하여 높은 동시성 처리와 롤백을 지원함
[참고2] 5.7 이후부터 언두로그 삭제 자동화
5.7 버전 부터 언두로그 삭제 기능이 자동화되었다.
롤백되거나 오래전에 커밋되어 사용되지 않은 언두로그는 즉시 삭제되도록 지원되었다.
이전에는 수동으로 관리자가 관리했었다고 함
리두 로그 : 안정적인 쓰기 작업의 핵심
쓰기 작업이 어떻게 이루어지는 먼저 살펴보자.
- 트랜잭션에서 데이터 변경 사항 발생
데이터의 변경이 발생하면 데이터 페이지의 레코드에 변경사항이 기록되고,
리두로그 또한 로그버퍼에 작성된다. - 트랜잭션 커밋
- 리두 로그 버퍼에 변경사항을 기록한다.
리두 로그 공간은 여러 영역으로 나누어져 있으며 순차적으로 증가하는 시퀀스 넘버(Log Sequence Nuber)를 갖는다.
현재 진행중인 트랜잭션에 의해 변경사항이 기록되는 활성 리두 공간과 이미 변경사항이 기록된 비활성 리두 공간(장애시 재사용 가능)이 존재한다. - 체크 포인트 이벤트를 발행한다.
체크 포인트 이벤트 발생시 현재 활성 리두 로그 번호 중 가장 작은 값보다 작은 비활성 리두 로그들을 디스크의 로그파일에 기록한다.
이때 리두 로그들은 각각 더티페이지들과 매핑되어있기에 더티 페이지들이 데이터 파일에 반영된다.
(즉, 체크 이벤트를 통한 리두 로그 반영이 데이터 파일에 쓰기 작업을 끌고 가는 구조)
이때 더티 페이지는 즉시 디스크에 반영되지 않을 수 있으며 버퍼 공간에 저장된 이후 write 스레드에 의해 한번에 일괄적으로 반영(플러시) 된다.
* 리두 로그 파일은 고정된 크기의 여러개 파일로 구성되며 모든 파일이 가득차면 첫번째 로그 파일이 새로운 리두로그로 덮어씌워지는 순환구조를 갖춘 로그파일 관리 규칙을 갖는다.
[잠깐] 리두로그와 데이터 페이지로 이원화하여 사용하는 이유
디스크에 기록하는 작업은 리두로그 부터 로그파일에 기록하고 이후에 더티 페이지를 데이터 파일에 기록한다.
mysql default 설정에서는 트랜잭션 커밋시 체크포인트 이벤트가 즉시 발생하여 리두로그가 로그 파일에 즉시 기록된다.
리두 로그는 데이터 페이지보다 작은 자료 구조를 갖추고 있어 데이터 페이지에 비해 IO 작업의 부하가 적다.
데이터 페이지는 상대적으로 큰 자료구조로 인해 버퍼 공간에 적재되고 버퍼 공간이 차거나 일정시간이 지나면 한번에 일괄적으로 반영하도록 동작한다.
실패하는 건수가 있다면 리두로그를 통해 복구할 수 있다.
즉, 리두로그는 장애시 복구 목적을 위한 안정장치이며 안정장치가 있기 데이터페이지의 쓰기 작업을 뒤로 미루고
버퍼를 통해 한번에 일괄적으로 반영하여 IO 작업의 부하를 줄일 수 있다.
[참고] 로그 버퍼
리두 로그가 저장되는 로그 버퍼는 버퍼라는 용어 처럼 버퍼 공간을 제공하지만
리두 로그는 쓰기 작업의 안정성을 위해 트랜잭션 커밋시 즉시 로그파일에 기록하는 것이 중요하다.
mysql default 설정에서 이를 지원하니 이를 변경하는 작업은 추천하지 않는다.
(innodb_flush_log_at_trx_commit 옵션)
'DB' 카테고리의 다른 글
mysql 잠금 - 락의 종류 (0) | 2025.02.09 |
---|---|
mysql 아키텍쳐[2] - 버퍼풀 (0) | 2025.02.07 |
B-tree와 인덱스 (feat. mysql) (0) | 2021.11.07 |
데이터베이스 정규화(1차, 2차, 3차, BCNF 정규화) (0) | 2020.09.30 |
[Database] 키의 개념과 구분(슈퍼키, 후보키, 기본키, 대체키, 외래키) (0) | 2020.09.29 |