-
MySQL은 다른 DBMS에 비해 독특한 구조
- 이로 인해 장/단점이 존재
-
MySQL 서버는 MySQL 엔진과 스토리지 엔진으로 구분할 수 있음
MySQL Server = MySQL Engine(쿼리 분석, 최적화) + Storage Engine(실제 데이터 접근)
- MySQL Engine: SQL interface, SQL parser, SQL optimizer, Cache & Buffer, Connection Handler
- Storage Engine: InnoDB, MyISAM, Memory...
- 클라이언트로부터의 접속 및 쿼리 요청을 처리(커넥션 핸들러)
- SQL 파싱 및 전처리
- 쿼리의 최적화된 실행
- 실제 데이터를 디스크 스토리지에 저장하거나 디스크 스토리지로부터 데이터를 읽어오는 부분을 담당
- MySQL 서버에서 MySQL 엔진은 하나지만 Storage 엔진은 여러 개를 사용할 수 있음
- 아래 쿼리처럼 테이블이 사용할 스토리지 엔진을 지정하면 이후 해당 테이블의 읽기/변경 작업은 해당 스토리지 엔진이 처리
mysql> CREATE TABLE users (col1 INT, col2 INT) ENGINE=INNODB
- MySQL의 스토리지 엔진은 플러그인 방식이며, 기본적으로 8가지의 스토리지 엔진이 탑재
mysql> SHOW ENGINES;
Ref. Storage 엔진 설명
- storage 엔진 관련 공식 문서: https://dev.mysql.com/doc/refman/8.0/en/storage-engines.html
- 엔진 설명 포스트 1: https://nomadlee.com/mysql-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80-%EC%97%94%EC%A7%84-%EC%A2%85%EB%A5%98-%EB%B0%8F-%ED%8A%B9%EC%A7%95/
- 엔진 설명 포스트 2: http://asuraiv.blogspot.com/2017/07/mysql-storage-engine.html
-
MySQL 서버는 프로세스 기반이 아닌 스레드 기반으로 작동
-
포그라운드 스레드와 백그라운드 스레드로 구분
mysql> SELECT thread_id, name, type, proccesslist_user, processlist_host FROM performance_schema.threads ORDER BY type, thread_id;
- 포그라운드 스레드는 최소 MySQL 서버에 접속된 클라이언트 수만큼 존재, 각 클라이언트 사용자가 요청하는 쿼리 문장을 처리
- 클라이언트 사용자가 작업을 마치고 커넥션을 종료하면 해당 커넥션을 담당하는 스레드는 다시 스레드 캐시로 되돌아감
- 이미 스레드 캐시에 일정 개수 이상의 대기 중인 스레드가 있으면 스레드를 종료 -> 일정 개수의 스레드 캐시 수(
thread_chach_size
)를 유지
- 이미 스레드 캐시에 일정 개수 이상의 대기 중인 스레드가 있으면 스레드를 종료 -> 일정 개수의 스레드 캐시 수(
- 포그라운드 스레드는 데이터를 MySQL의 데이터 버퍼나 캐시로부터 가져오며, 버퍼나 캐시에 없는 경우에는 직접 디스크의 데이터나 인덱스 파일로부터 데이터를 읽어옴
- 데이터 버퍼나 캐시까지만 포그라운드 스레드가 처리하고, 나머지 버퍼로부터 디스크까지 기록하는 작업은 백그라운드 스레드가 처리
- 포그라운드 스레드가 디스크 쓰기 작업까지 처리
- MyISAM도 지연 쓰기를 활용할 수 있지만 일반적이지 않음
- MyISAM의 경우 별로 해당 사항 없음(디스크 쓰기 작업까지 처리하기 때문)
- InnoDB는 아래 작업을 백그라운드로 처리
- 인서트 버퍼(Insert buffer)를 병합하는 스레드
- 로그를 디스크로 기록하는 스레드 (로그 스레드)
- InnoDB 버퍼 풀의 데이터를 디스크에 기록하는 스레드 (쓰기 스레드)
- 데이터를 버퍼로 읽어 오는 스레드
- 잠금이나 데드락을 모니터링하는 스레드
- MySQL 5.5 버전부터 데이터 쓰기 스레드와 읽기 스레드의 개수를 2개 이상 지정할 수 있게 됨
innodb_write_io_threads
,innodb_read_io_threads
로 스레드 개수 설정
- InnoDB에서도 데이터를 읽는 작업은 주로 클라이언트 스레드에서 실행되기 때문에 읽기 스레드는 많이 설정할 필요 없음
- 쓰기 스레드는 대부분의 많은 작업을 백그라운드로 처리하기 때문에 일반적으로 2~4 정도 설정
글로벌 메모리 영역 | 로컬 메모리 영역 |
---|---|
InnoDB 버퍼 풀 MyISAM 키 캐시 바이너리 로그 버퍼 리두 로그 버퍼 테이블 캐시 |
조인 버퍼 정렬 버퍼 네트워크 버퍼 리드 버퍼 |
- MySQL 메모리 공간은 글로벌 메모리 영역과 로컬 메모리 영역으로 구분
- 글로벌 메모리 영역의 모든 메모리 공간은 MySQL 서버가 시작되면서 OS로부터 할당
- OS에 따라 한 번에 100%를 할당할 수도 있고, 조금씩 할당해주는 경우도 존재
- 일반적으로 클라이언트 스레드의 수와 무관하게 하나의 메모리 공간만 할당
- MySQL 서버상에 존재하는 클라이언트 스레드가 쿼리를 처리하는 데 사용하는 메모리 영역
- 로컬 메모리는 각 클라이언트 스레드 별로 독립적으로 할당되며 절대 공유하지 않음
- 플러그인 모델이기 때문에 원하는 플러그인을 갈아끼워서 적용할 수 있음
- 플러그인 아키텍처에는 아래와 같은 단점이 존재
- 플러그인은 오직 MySQL 서버와 인터페이스할 수 있고, 플러그인끼리 통신할 수 없음
- 플러그인은 MySQL 서버의 변수나 함수를 직접 호출하기 때문에 안전한지 않음(캡슐화 X)
- 플러그인은 상호 의존 관계를 설정할 수 없어서 초기화에 어려움 존재
- 이와 같은 단점들을 보완하기 위해 컴포넌트 아키텍처를 지원
0. 사용자의 SQL 요청
1. 쿼리 파서 // 토큰 분리, 트리 생성, 문법 오류 파악
2. 전처리기 // 토큰 매핑, 접근 권한 확인
3. 옵티마이저 // 쿼리 변환, 비용 최적화, 실행 계획 수립
4. 쿼리 실행기
5. 스토리지 엔진 접근
6. SQL 결과
- 사용자 요청으로 들어온 쿼리 문장을 토큰(MySQL이 인식할 수 있는 최소 단위의 기호)으로 분리해서 트리 구조를 생성
- 이 과정에서 기본 문법 오류를 파악
- 트리 구조의 쿼리 문장에 구조적인 문제점이 있는지 파악
- 테이블이나 컬럼 이름, 내장 함수같은 개체를 매핑해서 해당 객체의 존재 여부와 객체의 접근 권한을 확인
- 사용자의 요청으로 들어온 쿼리 문장을 저렴한 비용으로 가장 빠르게 처리할지 결정하는 역할
- 쿼리 변환 비용 최적화, 실행 계획 수립
- 옵티마이저에 의해 만들어진 계획대로 각 핸들러에게 요청해서 받은 결과를 또 다른 핸드렁의 요청의 입력으로 연결
- 데이터를 디스크에 저장, 읽어오는 역할 담당
- 16장에서 자세히
- SQL 실행 결과를 메모리에 캐시하고 동일 SQL 쿼리가 실행되면 테이블을 읽지 않고 결과를 반환
- 테이블의 데이터가 변경되면 캐시에 저장된 결과 중 관련된 것들은 모두 삭제
- 성능 저하 및 버그의 원인이기 때문에 사용 권장 X
- 8.0으로 오면서 관련 기능은 Deprecated
- MySQL 서버 엔터프라이즈 에디션은 스레드 풀 기능을 제공하지만, 커뮤니티 에디션은 스레드 풀 기능을 지원하지 않음
- 결국 목적은 제한된 개수의 스레드 처리에만 집중하고, 반복해서 생성.삭제하는 코스트를 줄이는 역할
- InnoDB는 MySQL에서 사용할 수 있는 스토리지 엔진 중 거의 유일하게 레코드 기반 잠금을 제공
- InnoDB의 모든 테이블은 PK를 기준으로 클러스터링되어 저장
- PK 값의 순서대로 디스크에 저장
- PK가 클러스터링 인덱스이기 때문에 PK를 이용한 range scan 효율이 좋음
- 다른 보조 인덱스에 비해 비중이 높게 설정(쿼리 실행 계획에서 다른 보조 인덱스보다 PK가 선택될 확률이 높음)
- 8장에서 클러스터링 인덱스에 대한 설명 보충
- MyISAM에서는 InnoDB와 달리 클러스티링 키를 지원하지 않음
- MyISAM에서는 PK와 보조 인덱스는 구조적으로 차이가 없음(PK는 단순히 unique 제약을 가진 인덱스)
- 외래 키 지원은 InnoDB 스토리지 엔진 레벨에서 지원하는 기능
- MyISAM이나 Memory 테이블에서 사용할 수 없음
- 외래 키는 DB 서버 운영의 불편함 때문에 서비스용 데이터베이스에서 생성하지 않은 경우가 존재
- 부모, 자식 테이블의 해당 컬럼에 인덱스 생성이 필요하고, 테이블 변경 시 양쪽 테이블에 데이터가 있는지 체크
- 데이터가 있는지 체크할 때 잠금이 여러 테이블로 전파되고, 이로 인해 데드락이 발생할 수도 있음
-
레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능
-
가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공
- InnoDB에서는 Undo log를 이용하여 해당 기능을 구현
언두(Undo) 로그
- 언두 영역은 UPDATE 문장이나 DELETE와 같은 문장으로 데이터를 변경했을 때 변경되기 전의 데이터(이전 데이터)를 보관하는 공간
mysql > UPDATE member SET name = '김희동' WHERE member_id = '2';
- 위 쿼리가 실행되면 트랜잭션을 커밋하지 않아도 실제 데이터 파일 내용은 "김희동"으로 변경
- 그리고 변경되기 전의 값이 "홍길동"이라면 언두 영역에는 "홍길동"이라는 값이 백업
- 이 상태에서 만약 사용자가 커밋하게 되면 현재 상태(김희동)가 그대로 유지되고, 롤백하게 된다면 언두 영역의 백업된 데이터(홍길동)를 다시 데이터 파일로 복구
언두의 데이터 사용 용도
- 트랜잭션의 롤백 대비용
- 트랜잭션의 격리 수준을 유지하면서 높은 동시성을 제공
- InnoDB에서는 Undo log를 이용하여 해당 기능을 구현
-
MVCC의 MV(Multi version)은 하나의 레코드에 대해 여러 개의 버전이 동시에 관리됨을 의미
트랜잭션 격리 수준
- READ UNCOMMITTED(커밋되지 않은 읽기)
- READ COMMITTED(커밋된 읽기)
- REPEATABLE READ(반복 가능한 읽기)
- SERIALIZABLE(직렬화 가능)
트랜잭션 격리 수준과 관련하여 읽어볼만한 글
DB별 Default 격리 수준
- MySQL(InnoDB): REPEATABLE READ
- Oracle: READ COMMITTED
- Postgre: READ COMMITTED
- InnoDB 스토리지 엔진은 MVCC를 이용해서 잠금을 걸지 않고 읽기 작업을 수행
SERIALIZABLE
가 아닌 경우 읽기(select) 작업은 다른 트랜잭션의 변경 작업과 관계없이 바로 실행
- InnoDB는 내부적으로 잠금이 교착 상태에 빠지지 않았는지 체크하기 위해 잠금 대기 목록을 그래프(wait-for list) 형태로 관리
- 데드락 감지 스레드를 통해 주기적으로 잠금 대기 그래프를 검사
- InnoDB는 견고한 편이어서 데이터 파일이 손상되거나 서버가 시작되지 못 하는 경우는 거의 발생하지 않음
- 하지만 서버와 무관하게 디스크, 서버 HW로 InnoDB 엔진이 복구를 못 하는 경우 발생할 수 있음
- 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해두는 공간
- 쓰기 작업을 지연시켜 일괄 작업으로 처리할 수 있게 해주는 버퍼 역할도 수행
- insert, update, delete처럼 데이터를 변경하는 쿼리는 데이터 파일의 레코드를 변경하기 때문에 랜덤한 디스크 작업을 발생
- 버퍼 풀은 이런 변경된 데이터를 모아서 처리하기 때문에 랜덤한 디스크 작업의 횟수를 감소
- 일반적으로 전체 물리 메모리의 80%를 버퍼 풀로 사용하는 내용이 있는데 그렇게 하면 안 됨
- os와 client thread가 사용할 메모리도 고려해서 함께 설정
- 5.7버전부터 버퍼 풀 크기를 동적으로 조절할 수 있게 개선
- 처음에 50% 정도로 설정했다가 조금씩 올려가면서 최적점을 찾아야 함
innodb_buffer_pool_size
시스템 변수로 크기 설정 가능
- InnoDB 버퍼 풀은 서버의 메모리가 허용하는 만큼 크게 설정하면 할수록 쿼리의 성능이 빨라짐
- 이미 디스크의 모든 데이터 파일이 버퍼 풀에 적재될 정도의 버퍼 풀 공간이면 성능에 도움이 되지 않음
- InnoDB 버퍼 풀은 데이터베이스 성능 향상을 위해 캐시와 쓰기 버퍼링 두 가지 용도로 활용되는데, 버퍼 풀 메모리 공간을 증가시키느 것은 캐시 기능만 향상시키는 것
용어
- 클린 페이지(clean page): 디스크에서 읽은 상태로 전혀 변경되지 않은 페이지
- 더티 페이지(dirty page): 변경(insert, update, delete)된 데이터를 가진 페이지, 언제간 디스크에 기록될 페이지
- LSN(Log Sequence Number): 리두 로그 파일 공간은 계속 순환되어 재사용되지만, 매번 기록될 때마다 로그 포지션은 계속 증가된 값을 갖음. 이 값이 LSN
- 활성 리두 로그(Active Redo Log): 재사용 불가능한 공간
- 체크포인트 에이지(checkpoint age): 활성 리두 공간의 크기
- InnoDB 엔진에서 리두 로그는 1개 이상의 고정 크기 파일을 연결해서 순환고리처럼 사용
- 데이터 변경이 계속해서 발생하면 리두 로그 파일에 기록됐던 로그 엔트리는 다시 새로운 로그 엔트리로 덮어 쓰임
- 따라서 전체 리두 로그 파일에서 재사용 가능한 공간과 당장 재사용 불가능한 공간을 구분해서 관리해야 함
- 재사용 불가능한 공간은 활성 리두 로그(Active Redo log)라고 일컫음
- InnoDB 엔진은 주기적으로 체크포인트 이벤트를 발생시켜 리두 로그와 버퍼 풀의 더티 페이지를 디스크로 동기화
- 이렇게 발생한 체크포인트 중 가장 최근 체크포인트 지점의 LSN이 활성 리두 로그 공간(재사용 불가능한 공간)의 시작점이 됨
- 버퍼풀 크기 100GB면 리두 로그 파일은 5~10GB 수준이 적정
- 버퍼 풀 크기가 너무 크고, 리두 로그 파일 크기가 너무 작으면 쓰기 버퍼링 효과를 거의 보지 못함
- 버퍼 풀 크기가 너무 작고, 리두 로그 파일 크기가 너무 크면, 버퍼 풀 크기가 작아서 더티 페이지를 버퍼 풀 크기 밖에 가지지 못 해서 버퍼링 효과를 거의 보지 못함
- 추가로 더티 페이지 크기가 너무 크면 갑자기 버퍼 풀이 필요해지는 상황에 더티 페이지를 한 번에 기록해야 되는 상황이 발생할 수 있음
- 굳이 조정할 필요 없음
- 트랜잭션 격리 수준을 보장하기 위한 데이터를 백업
- 레코드가 변경(insert, update)되는 작업뿐만 아니라 테이블에 포함된 인데스를 수정하는 작업도 존재
- 인덱스를 업데이트하는 작업은 랜덤하게 디스크를 읽는 작업이 필요하므로 테이블에 인덱스가 많다면 이 작업에 많은 자원을 소모
- 그래서 InnoDB는 변경해야될 할 인덱스 페이지가 버퍼 풀에 있으면 바로 업데이트를 수행하지만, 디스크로부터 읽어와서 업데이트해야 한다면 즉시 실행하지 않고, 임시 공간에 저장해두고 사용자에게 결과를 반환하는 형태로 성능을 향상시킴
- 이 때 사용하는 임시 메모리 공간이 체인지 버퍼
- 일반적으로 인덱스라고 하면 테이블에 사용자가 생성해둔 B-Tree 인덱스를 의미함
- 어뎁티브 해시 인덱스는 사용자가 수동응로 생성하는 인덱스가 아니라 InnoDB 엔진에서 사용자가 자주 요청하는 데이터에 대해 자동으로 생성하는 인덱스
innodb_adaptive_hash_index
시스템 변수를 통해 어뎁티브 해시 인덱스 기능을 활성화 또는 비성활할 수 있음
- 어뎁티브 해시 인덱스는 B-Tree 검색 시간을 줄여주기 위해 도입된 기능
- 자주 읽히는 데이터 페이지의 키 값을 이용해서 해시 인덱스를 만들고, 필요할 때마다 어뎁티브 해시 인덱스를 검색
- B-Tree 검색 시간을 줄여줄 수 있기 때문에 항상 활성화해야 된다는 생각을 할 수도 있지만 의도적으로 비활성화하는 경우도 많음
- 중요한 것은 어뎁티브 해시 인덱스는 데이터 페이지를 메모리(버퍼 풀) 내에서 접근하는 것을 더욱 빠르게 하는 기능
- 데이터 페이지를 많이 읽는 경우네는 도움이 되지 않음
어뎁티브 해시 인덱스가 성능 향상에 도움을 주는 경우
- 디스크의 데이터가 InnoDB 버퍼 풀 크기와 비슷한 경우(디스크 읽기가 많지 않은 경우)
- 동등 조건 검객(동등 비교와 IN 연산자)이 많은 경우
- 쿼리가 데이터 중에서 일부 데이터에만 집중되는 경우
어뎁티브 해시 인덱스가 성능 향상에 도움을 주지 않는 경우
- 디스크 읽기가 많은 경우
- 특정 패턴의 쿼리가 많은 경우(JOIN이나 LIKE 패턴 검색)
- 매우 큰 데이터를 가진 테이블의 레코드를 폭넓게 읽는 경우
- 8.0 이전에는 MyISAM에서만 전문 검색, 공간 좌표 검색을 지원했지만, 8.0부터는 InnoDB에서도 지원
- MyISAM의 기능을 InnoDB에서 지원하기 때문에 곧 deprecate 될 듯
- 동시 처리 성능에 있어서 Memory 엔진은 InnoDB 엔진을 못 따라옴
- 일시적인 테이블 생성의 경우에 TempTable엔진을 사용할 수 있기 때문에 Memory 엔진도 deprecate 될 듯