RealMySQL 8.0 을 읽고 정리했습니다.

InnoDB 스토리지 엔진 아키텍처

InnoDB → MySQL의 스토리지 엔진 가운데

  • 가장 많이 사용되고,
  • 거의 유일하게 레코드 기반의 잠금을 제공하여 동시성 처리가 가능하고, 안정적이며 뛰어난 성능
  • 구조

4.2.1 프라이머리 키에 의한 클러스터링

  • InnoDB의 모든 테이블은 기본적으로 프라이머리 키를 기준으로 클러스터링 되어 저장 → 즉, 프라이머리 키 값의 순서대로 디스크에 저장되어 → 프라이머리 키에 의한 스캔은 상당히 빨리 처리됨
  • 결과적으로 쿼리의 실행 계획에서 프라이머리 키는 다른 보조 인덱스에 비해 높은 비중으로 설정
    • InnoDB 테이블 구조 = 오라클의 IOT(Index Organized Table) 구조

4.2.2 외래 키 지원

  • Only InnoDB, not MyISAM과 MEMORY 테이블
  • 여러가지 제약사항이 있어 실무에서는 잘 사용하지 않음
  • InnoDB의 외래 키는 부모 테이블과 자식 테이블 모두 해당 칼럼에 인덱스 생성이 필요하고, 변경 시에는 반드시 부모 테이블이나 자식 테이블에 데이터가 있는지 체크하는 작업이 필요하므로 잠금이 여러 테이블로 전파되고, 그로 인해 데드락이 발생할 때가 많아서 실무에서 잘 사용하지 않는다.

4.2.3 MVCC(Multi Version Concurrency Control)

  • 일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능
  • MVCC의 가장 큰 목적은 잠금 없는 일관된 읽기를 제공하는 것 → InnoDB는 언두 로그를 이용해 이 기능을 구현함
  • ex) ‘유재석’에서 ‘홍길동’으로 UPDATE
mysql > UPDATE member SET name='홍길동' WHERE member_id='1';

이 때의 상황

  1. InnoDB 버퍼 풀 : 수정 후 ‘홍길동'이 반영됨 (레코드 전체)
  2. 언두 로그 : 수정 전 ‘유재석'이 반영됨 (PK, 메타정보 및 수정된 칼럼만 백업)

→ 이 때, 커밋이나 롤백이 일어나지 않은 상황에서 2번 사용자가 해당 데이터를 읽으려고 하면 어떻게 될까?

결론 : 격리 수준에 따라 다르다.

  • READ_UNCOMMITTED : InnoDB 버퍼 풀이나 데이터 파일로부터 변경된 ‘홍길동' 데이터를 읽어서 반환한다.
  • READ_COMMITTED 이상 : 아직 커밋되지 않았기 때문에 언두 영역의 '유재석' 데이터를 반환한다. 이러한 과정을 DBMS 에서는 MVCC 라고 표현한다.

4.2.4 잠금 없는 일관된 읽기(Non-Locking Consistent Read)

  • InnoDB 스토리지 엔진은 MVCC를 이용해 INSERT와 연결되지 않은 순수한 SELECT 작업은 락을 걸지 않고, 바로 수행
    • 락을 걸지 않기 때문에 다른 트랜잭션이 갖고 있는 락을 기다리지 않음
    • 읽기 작업 가능(serializable 격리 수준은 제외)
    • lock이 걸려있어도 읽을 때는 언두 영역에서 읽기 때문에 lock이 걸리든 말든 상관없이 이전 버전의 데이터를 읽을 수 있다.

4.2.5 자동 데드락 감지

  • InnoDB는 그래프 기반의 데드락 체크 방식을 사용 → 데드락이 발생함과 동시에 바로 감지되고, 감지된 데드락은 관련 트랜잭션 중에서 ROLLBACK이 가장 용이한 트랜잭션(ROLLBACK을 했을 때 복구 작업이 가장 작은 트랜잭션, 즉 레코드를 가장 적게 변경한 트랜잭션)을 자동적으로 강제 종료해 버린다. 따라서 데드락 때문에 쿼리가 제한시간(Timeout)에 도달하거나 슬로우 쿼리로 기록되는 경우는 많지 않다.

4.2.6 자동화된 장애 복구

  • InnoDB는 손실이나 장애로부터 데이터를 보호하기 위한 여러 가지 매커니즘이 탑재!
    • MySQL 서버가 시작될 때, 완료되지 못한 트랜잭션이나 디스크에 일부만 기록된 데이터 페이지(Partial write) 등에 대한 일련의 복구 작업이 자동으로 진행된다.

4.2.7 InnoDB 버퍼 풀

  • InnoDB에서 가장 핵심적인 부분
  • innodb_buffer_pool_size 로 설정, 전체 물리 메모리의 50~80% 수준으로 설정
  • 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해 두는 공간 + 쓰기 작업을 지연시켜 일괄 작업으로 처리할 수 있게 해주는 버퍼 역할 → INSERT나 UPDATE 그리고 DELETE와 같이 데이터를 변경하는 쿼리는 디스크 작업을 발생시킴 → 버퍼 풀이 있다면 이러한 변경된 데이터를 모아서 처리하게 되면 랜덤한 디스크 작업의 횟수를 줄일 수 있다!
  • 아직 디스크에 기록되지 않은 변경된 데이터를 가지고 있다. → 이러한 데이터를 가지고 있는 페이지를 더티 페이지(Dirty page)
  • 더티 페이지는 InnoDB에서 주기적으로 또는 어떤 조건이 되면 체크포인트 이벤트가 발생하는데, 이때 Write 스레드가 필요한 만큼의 더티 페이지만 디스크로 기록한다. 체크포인트가 발생한다고 해서 버퍼 풀의 모든 더티 페이지를 디스크로 기록하는 것은 아니다.

4.2.8 Double Write Buffer

  • 파셜 페이지(Partial-page) or 톤 페이지(Ton-page) : 페이지가 일부만 기록되는 현상, 하드웨어 오작동이나 시스템 비정상 종료 등으로 발생함
  • InnoDB의 리두 로그는 공간의 낭비를 막기 위해 페이지의 변경된 내용만을 기록함 → 이로 인해 InnoDB에서 더티 페이지를 디스크 파일로 플러시할 때 일부만 기록되는 문제가 발생하면 해당 페이지는 복구가 어려울 수도.. → 이를 막기 위해 Double Write 기법을 이용!

4.2.9 언두(Undo) 로그

  • ex) ‘유재석’에서 ‘홍길동’으로 UPDATE
mysql > UPDATE member SET name='홍길동' WHERE member_id='1';
  • 언두 영역 : when? UPDATE나 DELETE 문장으로 데이터를 변경했을 때 why? 변경되기 전의 데이터를 보관하고자 사용
  • 언두 데이터 : ex) 언두 데이터 ‘유재석’ why?
  1. 트랜잭션의 rollback 대비용
  2. 트랜잭션의 격리 수준을 유지하면서 높은 동시성 제공해줘서
    • 트랜잭션의 격리 수준 : 동시에 여러 트랜잭션이 데이터를 변경하거나 조회할 때, 한 트랜잭션의 작업 내용이 다른 트랜잭션에 어떻게 보여질지를 결정하는 기준

4.2.10 체인지 버퍼

  • 인덱스 변경에 따른 자원 소모를 줄여주는 임시 메모리 공간

탄생 배경

  • 🧐 문제 : INSERT나 UPDATE 될 때 데이터 파일을 변경하는 작업 + 해당 테이블에 포함된 인덱스를 변경하는 작업 이 필요 →해당 테이블에 포함된 인덱스를 변경하는 작업 은 랜덤하게 디스크를 읽는 작업이 필요하므로 테이블에 인덱스가 많다면 상당히 많은 자원을 소모
  • 😃 해결 : 인덱스를 변경해야 한다면, 즉시 실행하지 않고 임시 공간에 저장해 두고 바로 사용자에게 반환하는 형태로 성능을 향상! → 이 때 사용하는 임시 메모리 공간 = 체인지 버퍼

특징

  • 반드시 중복 여부를 체크해야 하는 유니크 인덱스는 체인지 버퍼를 사용할 수 없음
  • 체인지 버퍼에 임시로 저장된 인덱스 레코드 조각은 이후 백그라운드 스레드에 의해 병합됨 → 이를 체인지 버퍼 머지 스레드 라고 함

4.2.11 리두(Redo) 로그 및 로그 버퍼

데이터 파일을 변경하는 것은 랜덤하게 디스크에 기록해야 하므로 비용이 비싼 작업이다. → 이 작업을 모아서 처리하여 성능을 향상하기 위해 InnoDB 버퍼 풀 같은 장치가 있음

BUT! InnoDB 버퍼 풀 만으로는 ACID를 보장할 수 없어서 → 순차적으로 디스크에 기록하는 로그 파일을 생성 = 리두 로그 (일반적으로 로그를 지칭함) 너무 많은 변경이 일어나면 리두 로그에 기록하는 것도 큰 문제가 되어서 보완하고자 = 로그 버퍼 로그 버퍼의 크기 = 일반적으로 1~8MB 수준, BLOB나 TEXT와 같이 큰 데이터를 자주 변경하는 경우는 더 크게 설정

4.2.12 어댑티브 해시 인덱스

  • InnoDB에서 사용자가 자주 요청하는 데이터에 대해 자동으로 생성하는 인덱스
  • innodb_adaptive_hash_index : 시스템 변수를 이용해서 활성화 및 비활성화

도입 목적

  • B-Tree의 검색 시간을 줄여주기 위해

작동 방식

  • 자주 읽히는 데이터 페이지의 키 값을 이용해 변수 생성
  • 필요할 때마다 어댑티브 해시 인덱스를 검색해서 레코드가 저장된 데이터 페이지로 가기
  • → 장점! B-Tree를 루트 노드부터 리프 노드까지 찾아가는 비용이 없어지고, CPU는 적은 일을 하지만 쿼리 성능은 빨라져서 동시에 더 많은 쿼리를 처리함

특징

  • (key, value) = (’인덱스 키 값’, 해당 인덱스 키 값이 저장된 ‘데이터 페이지 주소’)
  • 인덱스 키 값 = ‘B-Tree 인덱스 고유번호(ID)와 B-Tree 인덱스 실제 키 값’ 조합
  • 인덱스 키 값에 ‘B-Tree 인덱스의 고유번호’가 포함되는 이유 → InnoDB에서 어댑티브 해시 인덱스는 하나만 존재하기 때문이다. 즉, 특정 키 값이 어느 인덱스에 속한 것인지 구분하기 위해서!

데이터 유형과 해당 데이터에 대한 액세스 패턴을 잘 고려해서 선택해야 한다.

읽기 전략

Look-Aside(Lazy Loading)

데이터를 읽는 작업이 많을 때 사용 (가장 일반적으로 사용하는 방법)

Cache Hit

  1. 데이터를 요청한다.
  2. 먼저 cache 서버(redis)를 확인한다.
  3. cache에 데이터가 있으면 DB를 조회하지 않고 cache에 있는 데이터를 바로 반환한다. (Cache Hit)

Cache Miss

 

  1. 데이터를 요청한다.
  2. 먼저 cache 서버(redis)를 확인한다.
  3. cache 서버에 데이터가 없으면 DB를 조회하여 cache 서버에 저장하고 데이터를 반환한다. (Cache Miss)

 

Lazy Loading

cache는 찾는 데이터가 없을 때에만 입력되기 때문에 lazy loading이라고도 부름

이 구조는 redis가 다운되더라도 바로 장애로 이어지지 않고 DB에서 데이터를 가지고 올 수 있음

 

Cache Warming

대신 cache에 붙어있던 커넥션이 많이 있었다면 그 커넥션이 다 DB에 붙게 돼서 DB에 갑자기 많은 부하가 몰릴 수 있음

이런 경우 cache miss가 많이 발생해서 성능에 저하가 올 수 있음

→ 미리 DB에서 cache로 데이터를 밀어넣어 주는 작업

Cache Warming🔥

 

쓰기 전략

Write-Around

일단 모든 데이터는 DB에 저장되고 cache miss가 발생했을 때만 cache(redis)에서 데이터를 끌어오는 방법

이 경우 cache 데이터와 DB 데이터가 다를 수 있다는 단점이 있다.

 

Write-Through

DB에 데이터를 저장할 때 cache에도 함께 저장하는 방법

cache는 항상 최신 정보를 가지고 있다는 장점이 있지만 

저장할 때마다 두 단계 스텝을 거쳐야 하기 때문에 상대적으로 느리다.

 

무조건 cache에 데이터를 저장하기 때문에 일종의 리소스 낭비를 가지고 올 수 있어

expire time을 설정하는 것이 좋다.

 

redis란?

공식문서에서는 다음과 같이 정의되어 있습니다.

Redis는 데이터베이스, 캐시, 메시지 브로커 및 스트리밍 엔진으로 사용되는 오픈 소스(BSD 라이센스), 메모리 내 데이터 구조 저장소입니다. Redis는 strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams과 같은 데이터 구조를 제공합니다. Redis는 기본 제공 복제, Lua 스크립팅, LRU 제거, 트랜잭션 및 다양한 수준의 온디스크 지속성을 갖추고 있으며 Redis Sentinel을 통한 고가용성 및 Redis 클러스터를 통한 자동 파티셔닝을 제공합니다.

 

redis는 remote dictionary server의 줄임말입니다.

사전 같은 구조를 통해 데이터를 저장, 관리할 수 있다는 의미입니다. 즉, key-value 구조를 이용한다는 것입니다.

 

redis는 db-engines.com에서 key, value 저장소 중 가장 높은 순위입니다.

2023년 1월 18일 기준

 

특징

  • key-value 구조의 NoSQL
    • 쿼리를 사용할 필요가 없음
  • In-Memory 데이터베이스
    • 매우 빠른 속도
    • 서버 재시작 시 모든 데이터가 사라지기 때문에 유의 필요
    • 복제와 백업은 다른 것으로 복제로는 복원이 불가, 중요한 데이터는 주기적 백업 필요
  • 다양한 자료구조 지원
    • sorted sets 타입을 활용하면 데이터가 저장됨과 동시에 정렬되기 때문에 실시간 랭킹 시스템 개발에 유용함
    • application 단에서 작성할 로직을 redis로 간단하게 처리할 수 있음
    • 자료구조 별로 다른 커맨드 제공
  • Single Thread 방식 동작
    • 한 번에 하나의 명령만 처리할 수 있음
    • 처리 시간이 긴 명령어가 들어오면 뒤 명령어들은 앞에 있는 명령어가 처리될 때까지 대기
    • 하지만 get, set 명령어의 경우 초당 10만 개 이상 처리할 수 있을 만큼 빠름
    • atomic을 보장하고, race condition을 회피
  • 버전 6.x부터 아래와 같은 부분에만 Multi Thread 도입
    • 클라이언트에서 전송한 명령을 읽고 파싱하는 부분
    • 명령어 처리 결과를 클라이언트에게 전송하는 부분
    • (주의) 명령어 실행 자체는 위의 메인스레드에서 수행하므로 여전히 싱글스레드라고 봐야함

 

왜 쓸까?

DB가 있는데도 redis라는 인메모리 데이터 저장소를 사용하는 이유는 무엇일까요?

 

서비스의 유저가 증가했을 때 모든 유저의 요청을 DB 접근으로만 처리한다면 DB 서버에 부하가 증가할 수 밖에 없습니다. 또한 스키마가 정해져 있어 큰 저장공간을 차지하는 테이블에는 컬럼 하나만 추가하는 것도 쉽지 않습니다.

 

이를 개선하고자 DB를 스케일 인, 스케일 아웃 하는 방식 외에 캐시 서버로 이용하는 것이 redis 입니다.

 

캐시는 한번 읽은 데이터를 미리 저장해두었다가 다음에 읽을 때는 빠르게 반환할 수 있도록 동작하는 공간입니다.

같은 요청이 여러 번 들어오는 경우 매번 데이터베이스를 거치는 것이 아니라 캐시 서버에서 결과값을 바로 내려주기 때문에 DB의 부하를 줄일 수 있고, 서비스 속도도 느려지지 않는 장점이 있습니다.

 

 

 

+ Recent posts