Study/Real-MySQL

4.2 InnoDB 스토리지 엔진 아키텍처

hongeeii 2023. 12. 13. 10:09
728x90
반응형

4.2 InnoDB 스토리지 엔진 아키텍처

  • MySQL 스토리지 엔진 중 거의 유일한 레코드 기반 잠금 제공 : 높은 동시성 처리가 가능하고 안정적

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

  • 모든 테이블은 기본적으로 프라이머리 키를 기준으로 클러스터링되어 저장된다. 프라이머리 키 값의 순서대로 디스크에 저장.

    클러스터링 : 유사한 데이터끼리 묶어주는 작업

  • 프라이머리 키는 다른 보조 인덱스에 비해 비중이 높게 설정된다.
  • MyISAM은 클러스터링 키를 지원하지 않는다. 이 말은 프라이머리 키와 인덱스가 구조적으로 동일하다.

4.2.2 외래키 지원

  • MyISAM이나 MEMORY 테이블에 존재하지 않음.
  • InnoDB에는 외래키로 부모와 자식테이블 모두 인덱스를 생성하고, 데이터가 있는지 체크한다. 이로 인하여 잠금이 전파되며 데드락이 발생할 수 있다.
  • 수동으로 데이터를 적재할 때 외래키가 있을 경우 데이터 처리가 어려움. 이 경우 foregin_key_checks=off로 하여 일시적으로 멈춘다.
    • on delete cascade, on update cascade 옵션을 무시한다.
    • 반드시 부모 - 자식 테이블 간 일관성을 맞춘 후 on으로 변경한다.
    • 적용 범위가 BOTH로서 반드시 세션에서만 적용해야 한다.
    • SET SESSION foregin_key_checks=off; 다만 SESSION은 생략가능하다.
  • 상용에서는 사용하지 않더라도 개발 환경에서는 가이드 역할을 수행하므로 가능한 설정.

4.2.3 MVCC(Multi Version Concurrency Control)

  • 잠금을 사용하지 않는 일관된 읽기 제공이 핵심 목표.
  • InnoDB는 언두 로그를 사용하여 구현하며 레코드에 대한 멀티 버전을 제공.
  • UPDATE로 하나의 레코드를 변경할 경우
    • 버퍼 풀에 해당 데이터가 갱신된 후, 변경 전 값은 언두 로그에 저장된다.
    • 버퍼 풀의 변경된 데이터는 버퍼의 쓰기작업 후 디스크에 저장된다(쓰기 스레드의 상태에 따라 아닐 수도 있으나 사실상 일치한다).
    • commit : 버퍼 풀 및 디스크의 데이터를 영구적인 데이터로 변경한다.
    • rollback : 언두 영역의 데이터를 버퍼 풀의 데이터로 복구
  • commit이 되지 않은 상태에서 해당 레코드를 조회하면 어떻게 될까?
    • READ_UNCOMMITTED : 버퍼 풀의 값을 반환
    • 그 이상의 격리 수준 : 언두 영역의 값을 반환
  • 언두 영역은 그 영역을 사용한 트랜잭션이 종료될 때 삭제 됨.

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

  • MVCC 기술을 사용해 잠금 없이 읽기 작업 수행.
  • SERIALIZABLE를 제외한 격리 수준은 SELECT을 잠그지 않아 다른 트랜잭션의 변경 작업과 관계 없이 바로 실행 가능.
  • READ_COMMITED와 REFATABLE_READ는 언두를 사용. 언두가 오랫동안 유지될 경우 성능 저하가 발생하므로 트랜잭션은 최소한의 시간동안 유지

4.2.5 자동 데드락 감지

  • 잠금으로 인한 교착 상태를 확인하기 위하여 백그라운드 스레드인 데드락 감지 스레드가 그래프(Wait-For List)를 관리.
  • 교착 상태 시 트랜잭션 중 하나를 강제로 종료하며 그 기준은 언두의 양. 언두 로그가 적은 트랜잭션이 롤백의 대상이 된다. 왜냐하면 언두가 적을 수록 롤백으로 인한 비용이 적기 때문이다.
  • 테이블 락은 MySQL 엔진이 관리하며 스토리지 엔진이 확인할 권한이 없다. innodb_table_locks를 활성화하여 테이블락을 감지하고 처리할 수 있도록 변경 권장.
  • 잠금과 트랜잭션 등 변경이 많은 서비스의 경우 데드락 감지가 성능의 저하를 불러일으킬 수 있다. 이런 경우
    • 데드락 감지를 종료innodb_deadlock_detect=off하고,
    • innodb_lock_wait_timeout=:sec의 형태로 잠금 소유까지 대기 시간을 설정하여 자체적으로 세션을 종료시킬 수 있다.

4.2.6 자동화된 장애 복구

  • 서버가 시작될 때 완료되지 못한 트랜잭션이나 디스크의 일부를 자동으로 복구하는 기능. 손실이나 장애로부터 데이터를 보호하기 위한 메커니즘.
  • innoDB는 매우 견고하여 이런 상황이 거의 발생하지 않는다. 보통 서버와 무관한 하드웨어 이슈로 인해 발생하며, 이 문제가 발생하면 보통 쉽게 해결하기 어렵다.
  • 자동 복구를 실패할 경우 서버는 실행되지 않는다.
  • 자동 복구에 실패하였으나 서버 실행이 필요한 경우 innodb_force_recovery를 설정하여 강제 실행 가능하다.
    • 기본값인 0을 제외하고 1부터 6까지 설정 가능하다. 낮은 숫자에서 재실행이 가능하다는 의미는 문제의 수준이 낮다는 의미와 같다.
  • 서버가 실행조차 안된다면, 백업 데이터와 바이너리 로그를 사용하여 복구하는 방법 밖에 없다.

4.2.7 InnoDB 버퍼 풀

  • InnoDB의 가장 핵심적인 부분. 주요 역할은
    • 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시.
    • 쓰기 작업을 지연시켜 일괄 작업을 처리하는 버퍼.

4.2.7.1 버퍼 풀의 크기 설정

  • 동적 시스템 변수로서, innodb_buffer_pool_size로 변경
    • 8GB 미만은 50퍼센트
    • 8GB 초과는 50퍼센트로 시작하여 필요에 따라 점차 추가
    • 50DB 이상이면 15GB-30GB 사이
    • 버퍼 크기를 줄이는 작업은 위험할 수 있음
  • 전통적인 버퍼 풀은 하나였으며 이로 인한 내부 잠금 경합이 있었음. 현재는 여러 개의 버퍼 풀로 분리 및 분산 처리 중.
  • 각 버퍼 풀을 버퍼 풀 인스턴스라 표현하며 그 갯수는 innodb_buffer_pool_instances로 설정함.
    • 기본값 : 8
    • 1GB 미만 : 1
    • 40GB 초과 : 하나의 풀 인스턴스가 5GB를 점유하도록 갯수를 설정

4.2.7.2 버퍼풀 구조

  • 버퍼 풀은 페이지 크기(innodb_page_size)로 나누어 관리.
  • 페이지의 관리는 아래 3개의 자료구조가 관리
    • LRU(Least Recently Used) 리스트
    • 플러시(Flush) 리스트
    • 프리(Free) 리스트: 비어 있는 페이지의 목록

LRU

  • 디스크로부터 한 번 읽어온 페이지는 가능한 오랫동안 버퍼풀 메모리에 유지하여 디스크 읽기를 최소화하는 것에 목표
  • 오랫동안 보관할 페이지와 그렇지 않은 페이지를 구분해야 함.
  • 동작 순서 :
    • 필요한 데이터가 버퍼 풀에 있는지 조회한다.
      • 어댑티브 해시 -> 테이블의 인덱스를 순서로 버퍼 풀에 해당 데이터가 있는지를 우선적으로 파악한다.
      • 버퍼 풀에 존재할 경우 해당 페이지는 MRU(NEW) 방향으로 승급한다.
    • 없을 경우 해당 데이터를 버퍼 풀에 적재하고, 적재한 페이지의 포인터는 LRU(OLD) 헤더 부분에 추가
    • 각 페이지는 최근 사용자의 접근에 따라 나이가 부여되며,
      • 오래된 페이지는 버퍼 풀에서 제거되며
      • 쿼리에 의해 사용되면 나이가 초기화된다.
    • 자주 접근한 페이지의 인덱스 키는 어댑티브 해시 인덱스에 추가한다.
  • MRU에 조각이 늘어날 수록 LRU로 밀려나는 조각이 늘어나며 LRU를 벗어난 조각은 버퍼 풀에서 제거된다.

플러시 리스트

  • 데이터 변경이 가해진 페이지(터티 페이지)를 관리
  • 데이터가 변경되면
    • 리두 로그에 기록
    • 버퍼 풀의 페이지 데이터에 반영
    • 그러므로 리두 로그와 페이지는 연결 되어 있다.
  • 리두 로그와 더티 페이지는 동기화 및 디스크 보관을 목적으로 하나 이를 항상 보장하지 않는다.
  • 체크포인트는 MySQL이 재실행 할 때 복구 순서를 결정할 때 사용한다.

4.2.7.3 버퍼 풀과 리두 로그

  • 버퍼 풀은 캐싱의 기능을 담당한다. 버퍼링으로서 리두 로그를 관리하고 두 개의 관계를 파악해야 한다.
    • 버퍼와 캐시의 차이?
      • 캐시는 자주 사용하는 데이터를 보관. 반복되는, 오래걸리는 작업을 보관하여 빠르게 처리할 때 사용.
      • 버퍼는 데이터를 이동하기 위한 임시 보관소.

더티 페이지와 리두 로그

  • 더티 페이지는 언젠가는 디스크로 기록되어야 하며 동시에 버퍼 풀에 계속 머무를 수는 없다.
  • 리두 로그는 순환 고리와 같은 모습이며, 리두 로그가 정리되지 않으면 새로운 로그가 이전 로그를 덮어 쓸 수도 있다.
  • 활성 리드 로그 공간이 설정한 최대 값(체크포인트 에이지)를 넘어설 경우 그 값보다 작은 값은 모두 디스크에 동기화 한다. (정확하게 이해했는지 모르겠음)
  • 리두 로그의 버퍼 공간과 버퍼 풀의 캐시 공간이 중간 지점을 찾아야 한다. 버퍼 풀이 클 경우 리두 로그의 버퍼를 잘 활용하지 못하고, 리두 로그가 클 경우 갑작스러운 쓰기가 발생할 수 있다.
  • 버퍼 풀의 크기가 100GB일 경우 리두 로그 파일의 전체크기는 5-10GB 정도로 설정한다.

4.2.7.4 버퍼 풀 플러시

  • 8.0 버전 이후로 더티 페이지가 디스크로 동기화하는 과정에서 에러가 거의 발생하지 않음. 특별한 성능 문제가 없을 경우 수정 불필요.
  • 플러시는 두 개가 백그라운드로 실행 : 플러시 리스트 플러시, LRU 리스트 플러시

4.2.7.4.1 플러시 리스트 플러시

  • 리두 공간을 효과적으로 사용하기 위한 플러시 함수 목록
  • innodb_page_clearners - 더티 페이지를 디스크로 동기화는 클리너 스레드. 기본적으로 버퍼 풀 인스턴스 마다 하나씩 가진다.
  • innodb_max_dirty_pages_pct - 버퍼 풀 중 더티 페이지의 비율을 결정. 기본적으로 90퍼센트를 점유 가능.
    • 높을 수록 디스크 쓰기를 최소화 할 수 있음.
    • 다만, 과도한 쓰기로 인한 디스크 폭발(Disk IO Brust)가 발생할 수 있음.
    • innodb_max_tirty_pages_pct_lwm - 폭발을 방지하기 위해 일정 수준 이상 터티 페이지가 차지하면 조금씩 플러시를 수행:
  • innodb_io_capacity, innodb_io_capacity_max : 디스크의 읽고 쓰기의 수준을 설정
    • 다만 이 튜닝보다는 어댑티브 플러시(innodb_adaptive_flushing)을 사용하여 내부 알고리즘으로 처리하도록 함. 더티 페이지의 규모는 리두 로그의 증감을 분석하는 것과 같음. 해당 비율에 따른 적절한 알고리즘을 선택하여 디스크 쓰기의 수준을 결정
  • innodb_flush_neighbors를 사용하여 물리 디스크의 근접 수준에 따라 함께 기록하는 기능. SSD가 보편화된 현 상태에서 비활성화.

4.2.7.4.2 LSU 리스트 플러시

  • 사용 빈도가 낮은 페이지를 제거를 목적으로 함.
  • 설정한 값(innodb_lru_scan_depth)만큼 LRU부터 시작하여 스캔한다.
  • 더티 페이지는 동기화하고 클린 페이지는 프리 리스트 페이지로 옮긴다.

4.2.7.5 버퍼 풀 상태 백업 및 복구

  • 서버를 종료한 후 다시 실행할 경우, 버퍼 풀의 데이터가 없으면, 쿼리 처리 성능이 평상시보다 1/10이 안된다.
  • 워밍업을 필요로 하며 전통적으로는 주요 서비스의 테이블과 인덱스를 풀 스캔하였다.
  • 5.6부터는 innodb_buffer_pool_dumb_now를 통해 버퍼 풀을 백업 및 복원 가능하다.
  • 백업은 빠르지만 복원은 느리다. 백업된 내용의 리스트를 디스크로부터 읽어야 하기 때문이다.

4.2.7.6 버퍼 풀의 적재 내용 확인

  • 8.0 이후 테이블의 인덱스 별 버퍼 풀에 데이터 페이지의 적재 수준을 확인할 수 있다.

4.2.8 Double Write Buffer

  • 터티 페이지와 리두 로그를 통한 디스크 플러시 과정에서 일부분만 저장될 수 있다. 이를 보완하기 위한 방법
  • 더티 페이지를 모와 일괄적으로 플러시하기 전, 해당 더티 페이지를 묶어 DoubleWrite 버퍼에 기록한다.
  • 데이터 무결성이 중요한 경우 사용한다.
  • 다만, 리두 로그 동기화 설정이 1이 아닌 경우 비활성화하는 것이 좋다.

4.2.9 언두로그

  • 트랜잭션과 격리 수준을 보장하기 위해 DML로 변경되기 이전 버전의 데이터를 별도로 백업한다. 이를 언두 로그라 한다.
    • 트랜잭션 보장: 롤백 할 경우 언두의 데이터를 백업한다.
    • 격리수준 보장: 특정 커넥션이 데이터를 변경하더라도 다른 커넥션이 조회를 할 때 격리 수준에 맞게 언두 로그의 백업을 읽는다.
  • 다만, 매우 중요한 역할을 수행하나 반대로 많은 관리 비용을 사용한다.
    • 10만건의 delete 쿼리가 있을 경우, 10만건의 레코드의 값이 언두 로그에 있어야 한다.
    • 어떤 트랜잭션으로 select을 하였고, 사용자가 커넥션을 하루 동안 유지한다면, 다른 커넥션을 위하여 계속 언두 로그를 유지해야 한다.
  • 그러므로 언두 로그의 갯수는 모니터링이 필요하며, SHOW NGINE INNODB STATUS \G 등의 명령어로 확인 가능하다.

4.2.9.2 언두 테이블스페이스 관리

  • 언두 로그가 저장되는 공간을 언두 테이블스페이스라고 한다.
  • MySQL 5에서는 테이블 스페이스에 언두 로그를 저장하였으나 8 이후에는 별도의 로그파일에 저장.
  • 언두 테이블스페이스는 최소 1개가 생성되고 테이블스페이스 안에는 128개의 롤백 세그먼트를 가지며 세그먼트는 1개 이상의 언두 슬롯을 가진다.
  • 부족할 경우 트랜잭션 문제가 발생할 수 있기 때문에 기본 값(innodb_undo_tablespace=2, innodb_rollback_segments=128)을 유지하며 필요시 확장한다.
undo tablespace truncate
  • 언두 테이블 스페이스의 불필요한 공간을 제거하는 기능을 제공
  • innodb_undo_log_truncate=on|off
  • 자동모드 : 트랜잭션이 커밋된 불필요한 언두 로그는, 퍼지 스레드(purge thread)가 주기적으로 깨어나 제거하는 작업을 수행(undo purge)
  • 수동모드 : innodb_undo_log_truncate이 off이거나, on이더라도 잘 동작하지 않은 경우,
  • 사용자는 테이블 스페이스를 비활성화(SET INACTIVE) 할 수 있다. 이 경우 퍼지 스레드가 비활성 상태의 언두 테이블 스페이스를 찾아 작업한다. 이후 다시 활성화 한다. 최소 3개 이상의 테이블 스페이스가 존재해야 작동한다.

4.2.10 체인지 버퍼 change buffer

  • INSERT 및 UPDATE 할 때, 해당 테이블의 인덱스를 업데이트 해야 하며, 이는 디스크에서 인덱스를 버퍼 풀로 호출한 후 업데이트를 해야 한다.
  • 인덱스가 버퍼풀에 없을 경우 즉시 실행하지 않고 사용자가 호출할 때까지 변경 사항을 임시 공간에 두는 방식을 채택했다. 임시 메모리 공간을 체인지 버퍼라 한다.
  • 중복 여부를 체크하는 유니크 인덱스는 사용할 수 없다.
  • 백그라운드 스레드인 버퍼 머지 스레드가 병합 작업을 수행하며, 8.0 이후부터는 INSERT, DELETE, UPDATE 등 모든 상황에 사용 가능하다. -> 병합은 무엇을 의미하지?

4.2.11 리두 로그 및 로그 버퍼

  • 리두 로그는
    • 데이터 변경 내용은 로그에 가장 먼저 기록된다.
    • MySQL에 비정상적으로 종료됐을 때, 데이터 파일에 기록되지 못한 데이터를 잃지 않게 해주는 안전장치이다.
    • DBMS는 읽기 성능을 고려한 자료 구조를 가지고 있기 때문에 파일 쓰기는 디스크의 랜덤 엑세스가 필요하다. 쓰기 비용이 낮은 별도의 자료 구조를 가진 리두 로그를 가진다.
  • MySQL 서버가 비정상적으로 종료 되며 일관성이 깨지면 다음과 같은 상태로 예상 가능하다.
    • 커밋됐지만 데이터 파일에 기록되지 않은 데이터
    • 롤백됐지만 데이터 파일에 이미 기록된 데이터
    • 전자는 리두 로그를 복사하면 해결. 후자는 트랜잭션 간 관계를 파악해야 하므로 언두와 리두를 모두 필요.
  • innodb_flush_log_at_trx_commit으로 리두 로그의 디스크 동기화 방침을 설정할 수 있다.
    • 1 - 트랜잭션 커밋 직후 디스크에 기록한다. 부하가 가장 크지만 가장 안전하다. 권장.
    • 0 - 매초마다 리두 로그를 기록하고 동기화 한다. 완료 전에 서버가 비정상적으로 종료되면 데이터가 정상적으로 기록되지 않을 수도 있다.
    • 2 - 매초마다 트랜잭션의 커밋 직후 운영체제 메모리 버퍼에 기록됨을 보장한다. 완료 전에 서버와 운영체제가 비정상적으로 종료되면 정상적으로 기록되지 않을 수도 있다.
  • 리두 파일의 크기와 갯수는 innodb_log_file_sizeinnodb_log_files_in_group으로 결정한다. BLOB이나 TEXT가 많은 경우 기본 값인 16MB보다 많도록 설정한다.

4.2.11.1 리두 로그 아카이빙

  • 8.0 이후 아카이빙 기능이 추가하여 리두 로그 백업을 실패하지 않게 해준다.
  • 아카이빙은 이런 문제가 없음을 보장.
  • innodb_redo_log_archive_dirs로 디렉토리를 설정하고 UDF(사용자 정의 함수)를 실행 mysql> DO innodb_redo_log_archive_start('backup');
  • 아카이빙을 수행하는 세션이 비정상적으로 종료되면, 기존 파일을 자동으로 삭제. UDF를 통해 아카이빙을 종료 후 해당 세션을 종료해야 한다.

4.2.11.2 리두 로그 활성화 및 비활성화

  • 데이터의 수동 복구를 수행할 경우, 리두 로그가 발생하면 불필요한 부하가 발생할 수 있다. 이 경우 리두를 비활성화 할 수 있다.
  • 작업 후 반드시 활성화해야 한다.

4.2.12 어댑티브 해시 인덱스 Adaptive Hash Index

  • 사용자가 자주 요청하는 데이터를 InnoDB가 자동으로 생성하는 인덱스.

  • B-Tree 인덱스의 검색 과정에 앞서서 키 해시 인덱스를 통해 더 빠른 검색을 할수 있도록 도와준다.

  • innodb_adaptive_hash_index를 통해 활성화 여부를 결정한다.

  • innodb_adaptive_hash_index_parts를 통해 파티션의 갯수를 늘려 내부 잠금 경합을 줄일 수 있다. 8.0 이전에는 하나의 인덱스만 존재했다.

  • 속도를 효과적으로 높혀주지만 언제나 장점으로 작용하지는 않는다. 아래의 상황과 같이 제한적인 상태에서 어댑티브 해시 인덱스가 효과를 발휘한다.

  • 어댑티브 해시 인덱스가 도움 안 될 때

    • 디스크 읽기가 많은 경우
    • 특정 패턴의 쿼리가 많은 경우(조인이나 LIKE 패턴)
    • 매우 큰 데이터를 가진 테이블의 레코드를 폭넓게 읽는 경우
  • 도움 될 때

    • 디스킈 데이터가 innoDB 버퍼 풀 크기와 비슷한 경우(디스크 읽기 많지 않은 경우)
    • 동등 조건 검색(동등 비교와 IN 연산자)이 많을 경우
    • 쿼리가 데이터 중에서 일부 데이터에만 집중되는 경우
  • 어댑티브 해시 인덱스는 불필요한 오버헤드를 발생시킬 수 있다.

    • 상당히 큰 메모리 공간을 요구할 수도 있다.
    • 테이블 삭제 및 변경 작업은 해쉬 인덱스를 제거를 포함하여 많은 부하가 발생한다.
  • 어댑티브 해시 인덱스의 효과적인 사용 여부를 판단하기 위하여 모니터링이 필요하다.

    • mysql> SHOW ENGINE INNODB STATUS\G을 통해 hash searches와 non-hash searches의 비율을 통해 판단 가능하다.
728x90
반응형