도커를 공부해보고자 실습 시나리오를 구상해봤습니다.

실습 시나리오는 과거 대학교 졸업작품을 만들 때

Docker 없이 웹 서비스를 배포했던 기억을 토대로 구상했습니다.

 

당시 저희 팀은 첫 배포로 마이크로소프트의 클라우드 컴퓨팅 플랫폼인 Azure 를 사용했었는데요.

처음 얼마간 무료로 쓸 수 있는 것을 활용했습니다.

서버 구조는 대충 아래와 같았습니다.

(서버 한대에 DB 까지 때려박은 것을 보실 수 있습니다. 그냥 졸업작품 프로젝트였어서...)

React.js 로 개발한 프론트엔드도 있었지만.. 이번 실습에서 쓰지 않을 생각이어서 생략했습니다.

 

그런데 Azure 의 무료 기간이 생각보다 너무 짧았습니다.

마침 저는 어디선가 라즈베리파이를 하나 얻어서 가지고있었기 때문에, 라즈베리파이로 서버를 옮기기로 했습니다.

문제는 번거로운 서버 셋팅을 처음부터 다시 해야한다는 것이었습니다.

SSH 설정, JDK 설치 및 환경변수 설정, MySQL 설치 및 계정 설정, Nginx 설치, NodeJS 설치 등..

또한 라즈베리파이 OS 에는 MySQL을 설치할 수 없었기 때문에 MariaDB를 설치해야 했습니다. (물론 MySQL과 MariaDB는 거의 차이가 없었던 것 같긴 합니다.)

 

그런데 만약 Azure 서버를 셋팅했을 당시

Nginx, WAS, MySQL 이 셋팅된 OS를 통째로 Docker 이미지화 해두었다면?

그랬다면 라즈베리 파이 서버를 셋팅하는 과정은

Docker 설치, 이미지 다운받아서 실행

이것으로 끝났을 것입니다.

 

만약 어떤 회사에서 한 직원이 서버를 셋팅하고나서 퇴사했다고 해보겠습니다.

또한 서버는 저희 프로젝트에서 처럼 한 대에 프론트, 백엔드, reverse proxy 등을 전부 때려박아둔 상태라고 해볼게요.

(DB는.. DB는 빼고 ㅎㅎ ㅜ)

이후 새로운 직원이 입사했고, 회사는 저희 프로젝트에서 처럼 서버 비용 문제로 인해 서버를 이관하기로 결정했습니다.

이 때 퇴사한 직원이 서버 셋팅 기록을 엄청나게 신경써서 꼼꼼하고 정확하게 남겨두지 않았다면, B는높은 확률로 삽질을 해야할 것입니다.

 

그런데 서버가 Docker image 로 관리되고 있었다면?

새로운 직원은 새 서버에 Docker 를 설치하고 이미지를 실행하기만 하면 되는것이죠.

이것이 제가 지금까지 이해한, 서버 구축에서 Docker 의 장점입니다.

깃허브로 프로젝트 코드를 관리하는 것처럼, 서버 셋팅 자체를 이미지로 관리함으로써 이식성을 개선해주는 것이죠.

물론 장점이 이것 뿐만은 아니겠지만, 현재로서 저에게는 이게 가장 와닿는 것 같네요.

혹시 잘못 알고있는 부분이 있다면 알려주세요 ㅜ

 

그래서 아무튼 구상한 1차 실습 시나리오는 아래 그림과 같습니다.

 

일단은 도커를 써보면서 익숙해지는 것을 목적으로 아주 많이 단순화해봤습니다.

 

도커의 사용에 익숙해지는 것 이상으로

도커가 어떤 문제를 해결해주기 때문에 필요한지를 이해하는 것은 중요할 것입니다.

 

(이해한 대로 써보긴 했지만 확실하게 검증된 이해는 아님. 다른 개발자들 통해 검증 필요)

문제상황

1.

예제 테이블 구조

 

2.

select * from team join member on team.id = member.id;

 

3.

JPQL select t from Team t join fetch t.members

 

실행된 쿼리 (요약)

select * 
    from Team team
    inner join Member members
    on team.id=members.team_id

조회 결과 (List<Team>)

team{id=1, name=청팀, 멤버:{id=1, name=김예림},{id=2, name=홍금비}}
team{id=1, name=청팀, 멤버:{id=1, name=김예림},{id=2, name=홍금비}}
team{id=2, name=홍팀, 멤버:{id=3, name=이종성},{id=4, name=백승연}}
team{id=2, name=홍팀, 멤버:{id=3, name=이종성},{id=4, name=백승연}}
team{id=3, name=백팀, 멤버:{id=5, name=이하빈},{id=6, name=오혜령}}
team{id=3, name=백팀, 멤버:{id=5, name=이하빈},{id=6, name=오혜령}}
team{id=4, name=흑팀, 멤버:{id=7, name=최진영},{id=8, name=진완선}}
team{id=4, name=흑팀, 멤버:{id=7, name=최진영},{id=8, name=진완선}}

 

JPQL 사용시, 2번에서와 쿼리는 똑같이 나갔음에도 조회 결과는 뻥튀기되었음.

 

이 문제는 OneToMany  join & fetch join 에서만 발생한다.

 

distinct를 이용하여 해결한다.

select distinct t from Team t join fetch t.members

 

실행된 쿼리 (요약)

select distinct * 
    from Team team
    inner join Member members
    on team.id=members.team_id

조회 결과 (List<Team>)

team{id=1, name=청팀, 멤버:{id=1, name=김예림}, {id=2, name=홍금비}}
team{id=2, name=홍팀, 멤버:{id=3, name=이종성}, {id=4, name=백승연}}
team{id=3, name=백팀, 멤버:{id=5, name=이하빈}, {id=6, name=오혜령}}
team{id=4, name=흑팀, 멤버:{id=7, name=최진영}, {id=8, name=진완선}}

 

이러면 실제 쿼리에도 distinct가 붙는데 얘는 딱히 의미는 없고

쿼리 실행 이후에 JPA에서 뻥튀기되지 않는 효과가 있다.

(B Tree 자료구조에 대해서는 설명하지 않습니다)

 

MySQL의 InnoDB는 Primary Key 클러스터링 인덱싱이 기본이다.

Primary Key 클러스터링 인덱싱이 있을 때와 없을 때를 비교해보고, 클러스터링 인덱싱이 어떤 효과를 발휘하는지 이해해보자.

 

1. Primary Key 클러스터링 인덱스 없는 경우 (세컨터리 인덱스도 X)

아래 그림을 보면 Primary Key인 ID 필드가 정렬되어있지 않다.

ID=1인 레코드를 저장한 다음, ID=4인 레코드를 저장한 다음, ID=3인 레코드를 저장한 모습이다.

 

 

이렇게 하나씩 레코드가 늘어나다보니

1억개가 저장되었다. (~라고 해보자.)

 

그리고 다음과 같은 쿼리를 실행해보자.

SELECT * FROM user WHERE id = 890;

 

만약 맨 위에서부터 내려가면서 레코드를 찾는다면

해당 데이터는 1억번 째에 있다.

 

MySQL의 한 페이지에 user 레코드가 500개까지 저장될 수 있다고 하자.

그러면 최소 1억 / 500 = 20만개의 페이지를 찾아봐야 해당 레코드를 발견할 수 있다.

따라서 디스크 I/O는 약 20만 회 일어나게 된다.

 

(버퍼 풀로 I/O 횟수를 줄이는 경우에 대해서는 이 글에서 고려하지 않을 것이다.

어차피 인덱스도 레코드도 버퍼풀에 올라갔다 내려갔다 하는거고... 다같이 생각하면 복잡하니 생각 안하겠음)

 

2. Primary Key 클러스터링 인덱스 있는 경우

이번에도 앞에서와 같이

ID=1인 레코드를 저장한 다음, ID=4인 레코드를 저장한 다음, ID=3인 레코드를 저장한 모습이다.

4번 먼저 저장하고나서 3번을 저장했는데, 디스크(=파일)에 쓰여진 데이터가 ID 순서대로 유지되었다.

(이 과정에서 정렬을 유지하는 데에 비용이 들었으리라.)

 

 

앞에서와 마찬가지로, MySQL의 한 페이지 단위에 user 레코드가 최대 500개 들어갈 수 있다고 하자.

 

위 그림의 경우 데이터가 3개밖에 없으므로 한 페이지에 들어가있다.

따라서 이 페이지가 인덱스의 루트노드이자 리프노드인 상태.

 

이제 또 다시 레코드가 늘어나서 1억개가 저장되었다고 해보자.

 

데이터파일을 보면 1억개의 데이터가 ID 순서대로 디스크에 쓰여있음을 볼 수 있다.

그리고 MySQL의 한 페이지에 500개의 레코드가 들어갈 수 있다고 하자.

그러면 디스크의 페이지 갯수는 1억 / 500 = 20만 개 이다.

 

인덱스에서 브랜치노드, 루트노드는

한 페이지에 1000개의 레코드를 가질 수 있다고 가정하겠다.

(리프노드와 달리, 브랜치노드와 루트노드는 NAME, AGE 필드가 없고 자식노드 필드가 있다.

따라서 row 한개의 크기가 더 작고, 이로인해 한 페이지에 더 많은 row가 들어갈 것이다.)

 

 

트리 높이 = 2

이제 PK로 데이터 한 개를 검색해보자.

그러면

루트 노드, 브랜치노드 중 1개, 리프노드 중 1개

이렇게 세 개의 페이지밖에 읽지 않는다.

이러면 디스크 I/O는 겨우 3번밖에 일어나지 않으므로 디스크 IO가 획기적으로 줄어들었다!

 

PK 클러스터링 인덱스의 장점과 단점

아래 표는 Real MySQL 8.0 교재의 275 페이지에 있는 표(를 거의 배낀 것)이다.

(쓰다보니 세컨더리 인덱스 얘기가 많아서

이후에 세컨더리 인덱스까지 쓴 다음에 그 뒤로 옮겨야겠다..!)

장점   PK 검색 매우 빠름
 세컨더리 인덱스에서 PK를 가지고있기 때문에,
 (테이블 읽기까지 안가고) 인덱스 읽기만으로 검색작업이 끝나는 경우가 많음
 (커버링 인덱스)
단점  세컨더리 인덱스들이 PK를 갖기 때문에
 PK 크기가 크면 전체적으로 인덱스의 크기가 커짐
 세컨더리 인덱스를 통해 검색할 때 PK로 다시한번 검색해야 하므로 느림
  INSERT 할 때 PK에 의해 레코드의 저장 위치가 결정되기 때문에 느림
  PK 를 UPDATE할 경우, 레코드를 DELETE 하고 INSERT 함 -> 느림

 

앞이 이해가 되었다면, 대강

read는 빨라지고 write가 느려지는 느낌이라는걸 알것이다.

 

일반적으로 웹 서비스와 같은 온라인 트랜잭션 환경에서는 쓰기:읽기 비율이 2:8 ~ 1:9 정도로, read 작업이 훨씬 많이 발생한다. 따라서 조금 느린 write를 감수하고 읽기를 빠르게 유지하는 것은 매우 중요하다.

 


참고

1

Real MySQL 8.0 (백은빈, 이성욱)

2

[Database/MySQL] Tablespace (테이블 스페이스와 데이터 파일에 대해서 이해할 수 있었던 자료)

https://pangtrue.tistory.com/190

 

 

 

 

 

 

 

1. 잠금(Lock)의 종류

1.1 Shared Lock ( = Read Lock )

보통 데이터를 읽을 때 사용한다. (InnoDB 에서는 잠금 없이 읽으니까 의미 없는듯?)

데이터에 락을 걸었지만 다른 세션에서 읽을 수 있다.

Shared Lock이 걸린 레코드에 Shared Lock 을 추가로 걸 수 있다.

Shared Lock이 걸린 레코드에 Exclusive Lock 을 추가로 걸 수 없다.

 

1.2 Exclusive Lock ( = Write Lock )

보통 데이터를 변경할 때 사용한다.

해당 락이 해제되기 전에 어떠한 락도 걸 수 없다.

즉 읽기/쓰기가 불가능하다.

(InnoDB 에서는 락이 걸린 데이터에 대해서

언두 로그를 통해 읽기가 이루어짐)

 

MySQL에서 Read 는 undo 로그를 통해 Lock과 관계없이 동작하므로

Write Lock 만 생각하면 된다.

 

2. MySQL의 Lock

2.1 MySQL 엔진의 잠금

글로벌 락

테이블 락

네임드 락

메타데이터 락

 

2.2 InnoDB 스토리지 엔진의 잠금

레코드 락

갭 락

넥스트 키 락

 


출처

1

[DB] Lock 이란?

https://chrisjune-13837.medium.com/db-lock-락이란-무엇인가-d908296d0279

2

Real MySQL 8.0 (백은빈, 이성욱)

 

1. Dirty Read

커밋되지 않은 트랜잭션이 변경한 데이터를

다른 트랜잭션 조회쿼리에서 읽는 것.

 

만약 변경을 실행했던 트랜잭션이 롤백되기라도 하면

조회 쿼리가 말도안되는 데이터를 읽은 셈이 된다.

 

♣ InnoDB read committed 격리수준에서는 dirty read 문제를 어떻게 해결하는가?

커밋되지 않은 트랙잭션에 의해 변경된 데이터를 읽을 때

undo 로그를 이용해서 이전 상태를 읽는다.

 

 

2. Non-Repeatable Read

한 트랜잭션 내에서 똑같은 조회 쿼리가 두번 실행되었을 때

결과가 서로 다른 것.

 

 InnoDB repeatable read 격리수준에서는 non-repeatable read 문제를 어떻게 해결하는가?

트랜잭션이 커밋되었다고 무조건 undo 로그를 지워버리는 대신,

커밋된 트랜잭션보다 먼저 시작된 트랜잭션이 아직 실행중이라면

해당 undo 로그를 유지한다.

그럼으로써

실행중인 트랜잭션은 항상

자신이 시작되기 전의 데이터를 읽게된다.

 

3. Phantom Read

한 트랜잭션 내에서 똑같은 조회 쿼리가 두번 실행되었을 때

있던 레코드가 없어졌거나

없었던 레코드가 생겨있는 것.

 

InnoDB의 경우 repeatable read 격리수준에서도 이러한 문제가 발생하지 않는다.

(select ~ for update 같은 경우 제외)

 

 InnoDB의 repeatable read 격리수준에서는 phantom read 문제를 어떻게 해결하는가?

데이터 생성 & 삭제 역시 undo 로그로 유지되기 때문에

조회 트랜잭션보다 이후에 일어난 데이터 생성/삭제가 조회 쿼리에 반영되지 않는다.

 

select ~ for update 의 예외

select ~ for update 문장의 경우, 읽기 쿼리지만 변경 쿼리와 같이 레코드에 exclusive lock 을 건다.

이친구는 조회는 조횐데 undo 로그도 사용하지 않는다.

 

잔고가 1000원 이상인 사용자 목록을 select ~ for update 로 두번 조회한다고 해보자.

exclusive lock 덕분에 repeatable read 현상은 발생하지 않는다.

하지만 만약 잔고가 1000원 이상인 새로운 데이터가 생성된다면?

그러면 두번째 select ~ for update 문에서 이 레코드도 조회 될 것이다.

 

이 문제까지 완벽하게 막고싶다면?

InnoDB 스토리지 엔진에서는 넥스트 키 락(을 통해 사용하는 갭 락) 을 이용해서 막을 수 있다.

(완벽하게 막을 수 있나? 인덱스에 따라서 다를것같기도 하고.. 갭 락에 대해 정확히 알지를 못해서...)

innodb_locks_unsafe_for_binlog 시스템변수가 비활성화되면(0으로 설정되면)

변경을 위해 검색하는 레코드에는 넥스트 키 락 방식으로 잠금이 걸린다.

 

4. Serializable

한 트랜잭션에서 읽고 쓰는 레코드를

다른 트랜잭션에서 절대 접근할 수 없는 것.

대신 동시처리 성능도 다른 격리 수준들에 비해 떨어진다.

 

innoDB 스토리지 엔진에서는 갭 락과 넥스트 키 락 덕분에 repeatable read 격리수준에서도 이미 phantom read 가 발생하지 않는다고 한다. 따라서 굳이 serializable을 사용할 필요성은 없어보인다고 한다.

 

 


참고

1

Real MySQL 8.0 (백은빈, 이성욱)

2

[DATABASE] SELECT ~ FOR UPDATE 란?

https://dololak.tistory.com/446

3

[MySQL] 트랜잭션, 잠금(Lock)

https://velog.io/@fortice/MySQL-트랜잭션-잠금Lock

 

1. 버퍼 풀이란?

메모리 상의 공간으로, 아래 용도로 사용됨

  • 데이터 캐시 : 디스크의 데이터나 인덱스 정보를 메모리에 캐시해두는 용도
  • 쓰기 지연 : 쓰기 작업을 지연시켜 일괄 작업으로 처리할 수 있게 해주는 버퍼 역할

 

2. 버퍼 풀의 크기 설정

  • 운영체제 전체 메모리 공간이 8GB 미만인 경우
    50% 정도만 InnoDB 버퍼 풀로 설정하고 나머지 메모리 공간은 MySQL 서버와 운영체제, 그리고 다른 프로그램들이 사용할 수 있는 공간으로 확보해주는 것이 좋다.
  • 운영체제 전체 메모리 공간이 8GB 이상 50G 미만인 경우
    버퍼 풀의 크기를 전체 메모리의 50%에서 시작해서
    조금씩 올려가면서 최적점을 찾는다.
  • 운영체제 전체 메모리 공간이 50GB 이상인 경우
    15GB ~ 30GB 정도만 남겨두고 나머지를 버퍼 풀로 할당하자.

 

-ppt-

3. 버퍼 풀의 구조

InnoDB 스토리지 엔진은 버퍼 풀이라는 메모리 공간을

페이지 크기 (innodb_page_size 시스템 변수에 설정된) 조각으로 쪼개어,

InnoDB 스토리지 엔진이 데이터를 필요로 할 때

해당 데이터 페이지를 디스크로부터 읽어와서 각 조각에 저장한다.

 

InnoDB 스토리지 엔진은 버퍼풀의 페이지들을 관리하기 위해 다음 세개의 자료구조를 관리한다.

  • Free list
    데이터로 채워지지 않은, 비어있는 페이지들의 목록
  • LRU list
    데이터로 채워져있는 페이지들의 목록
  • Flush list
    변경이 가해진 페이지들의 목록

 

 

4. LRU List

데이터가 채워져있는 페이지들의 목록이다.

 

버퍼 풀이라는 캐시 공간은 한정되어있기 때문에

다시 쓰일 가능성이 높은 데이터 페이지들을 위주로 캐싱하는 것이 효과적이다.

그래서 자주 안쓰이는 페이지들을 제거하고 자주 쓰이는 페이지를 메모리에 상주시키기 위해 다음과 같은 알고리즘을 사용한다.

  • 처음으로 디스크에서 읽힌 페이지는 일단 Old 서브리스트의 head 부분에 붙는다.
  • 버퍼 풀에 있던 데이터가 재사용되면 해당 페이지는 위쪽 방향으로 승급한다.
  • 캐시 공간 마련을 위해 페이지들을 지울 때, 리스트 아래(Old 서브리스트 tail 부분)부터 차례대로 지운다.

 

 

따라서

버퍼 풀에 올라온 어떤 데이터 페이지가

자주 사용되면 New 서브리스트 영역에서 계속 살아남고,

반대로 거의 사용되지 않는다면

새롭게 디스크에서 읽히는 데이터 페이지들에 밀려서

Old 서브리스트의 끝으로 밀려나 결국은 버퍼풀에서 제거된다.

 

5. 리두 로그 파일

리두 로그 파일은 고정 크기의 파일이다.

 

InnoDB 스토리지 엔진은 데이터 변경 시 다음 세가지 작업을 한다.

1. 버퍼풀의 페이지에 변경 내용을 반영

2. 리두 로그 파일에도 변경 내용을 저장

3. 변경된 페이지를 flush list가 참조함

 

리두 로그는 왜 필요할까?

만약 MySQL 서버가 쓰기 지연을 하고있었는데 다운되었다고 해보자.

메모리에 있는 버퍼풀 쓰기지연 데이터가 날아가버려도, 리두로그는 디스크에 남아있으므로 복구할 수 있다. 2)

 

-ppt-

6. 버퍼 풀과 리두 로그

리두로그는 1개 이상의 고정 크기 파일을 연결해서 순환 고리처럼 사용한다.

즉, 데이터 변경이 계속 발생하면 덮어쓰인다.

그래서 재사용 가능한 공간과 당장 재사용 불가능한 공간을 구분해서 관리해야한다.

재사용 불가능한 공간을 활성 리두 로그라 한다.

 

리두 로그 파일의 공간은 계속 순환되어 재사용되지만

매번 기록될 때마다 로그 포지션은 계속 증가된 값을 갖게되는데, 이를 LSN(Log Sequence Number 라고 한다.

 

 

 

체크 포인트

리두 로그와 버퍼 풀의 더티 페이지를 디스크로 동기화하는 이벤트.

InnoDB 스토리지 엔진은 주기적으로 체크포인트 이벤트를 발생시킨다.

 

체크 포인트 에이지

활성 리두 로그 공간의 크기

 

리두 로그 크기 정하기

* 리두 로그 크기가 너무 작을 경우

모든 더티 페이지는 리두 로그로 관리되어야 한다.

따라서 버퍼 풀의 크기가 크더라도 리두 로그 크기가 너무 작으면 쓰기 버퍼링 효과는 거의 보지 못한다.

 

* 리두 로그 크기가 너무 클 경우

이 경우 이론적으로는 문제가 없어보이지만

서비스를 운영하다 보면 급작스러운 디스크 쓰기가 발생할 가능성이 높다.

버퍼 풀에 더티 페이지의 비율이 너무 높은 상태에서 갑자기 버퍼 풀이 필요해지는 상황이 오면

InnoDB 스토리지 엔진이 매우 많은 더티 페이지를 한번에 기록해야 하는 상황이 온다.

 

* 책에서 제안하는 방법

버퍼 풀의 크기가 100GB 이하인 MySQL 서버에서는

리두 로그 파일의 전체 크기를 대략 5~10GB 수준으로 선택하고

필요할 때마다 조금씩 늘려가면서 최적값을 선택하는 것이 좋다.

 

리두 로그는 변경분만 가지고 있고 버퍼풀을 변경된적 없는 페이지들도 가지고 있기 때문에

리두 로그는 버퍼풀의 크기보다 훨씬 작은 공간만 있으면 된다.

 

7. 버퍼 풀 플러시

MySQL 8.0 버전 이후부터는 더티 페이지를 디스크로 동기화할 때 디스크 폭증 현상이 발생하지 않는다.

따라서 특별히 성능 문제가 발생하지 않는한 디스크 쓰기 동기화 시스템 변수들을 조정할 필요는 없다.

 

InnoDB 스토리지 엔진은 더티 페이지들을 성능상의 악영향 없이 디스크에 동기화하기 위해 다음 2개의 플러시 기능을 백그라운드로 실행한다.

  • Flush_List 플러시
  • LRU_List 플러시

7.1 Flush_List 플러시

InnoDB는 Flush_List 플러시 함수를 주기적으로 호출해서 더티페이지들을 디스크에 동기화한다.

변경된지 오래된 더티 페이지부터 순서대로 작업한다.

 

7.2 LRU_List 플러시

InnoDB 스토리지 엔진은 LRU 리스트에서 사용 빈도가 낮은 데이터 페이지들을 제거해서

새로운 페이지들을 읽어올 공간을 만들어야 하는데, 이를 위해 LRU_list 플러시 함수가 사용된다.

  • 클린 페이지 : 즉시 Free 리스트로 옮김
  • 더티 페이지 : 디스크에 동기화 한 다음 Free 리스트로 옮김

 

8. 버퍼 풀 상태 백업 및 복구

디스크의 데이터가 버퍼 풀에 적재돼 있는 상태를 워밍업(Warming UP)이라고 표현한다.

버퍼 풀이 잘 워밍업 된 상태에서는 그렇지 않은 경우보다 몇십 배의 쿼리 처리 속도를 보이는 것이 일반적이다.

 

그래서 서버 점검 등의 이유로 서버를 재시작할 때 버퍼풀을 복구할 수 있도록, MySQL 에서는 버퍼풀 백업 및 복구 기능을 제공한다.

-- MySQL 서버 셧다운 전에 버퍼 풀의 상태 백업
mysql> SET GLOBAL innodb_buffer_pool_dump_now=ON

-- MySQL 서버 재시작 후, 백업된 버퍼 풀의 상태 복구
mysql> SET GLOBAL innodb_buffer_pool_load_now=ON

 

 

버퍼 풀 백업은 메타 정보를 백업하는 것이기 때문에 백업은 매우 빠르고 백업 파일 크기도 매우 작다.

버퍼 풀 복구는 백업해둔 메타정보를 바탕으로 디스크에서 읽어와서 버퍼풀에 적재하기 때문에 시간이 많이 걸릴 수도 있다.

 

 


참고

1

Real MySQL 8.0 (백은빈, 이성욱)

2

Managing the Redo Log

일부 인용 : "Redo entries record data that you can use to reconstruct all changes made to the database, including the undo segments. Therefore, the redo log also protects rollback data. When you recover the database using redo data, the database reads the change vectors in the redo records and applies the changes to the relevant blocks."

https://docs.oracle.com/cd/B19306_01/server.102/b14231/onlineredo.htm

 

 

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

InnoDB의 모든 테이블들은 primary key 값 순서대로 디스크에 저장된다.

이를 통해 primary key를 이용한 레인지 스캔이 상당히 빨리 처리될 수 있다.

  • 레인지 스캔 : 데이터의 일부 레코드에만 엑세스하는 것
-- 레인지 스캔 예
select * from user where age between 20 and 24;
-- primary key를 이용한 레인지 스캔 예
select * from user where id=1;

 

2. 외래 키 지원

foreign key 를 사용할 수 있다.

(MyISAM엔진이나 MEMORY엔진에서는 foreign key를 갖는 테이블을 만들 수 없음)

 

 

3. MVCC & 잠금 없는 일관된 읽기

InnoDB 스토리지 엔진은 읽기 작업시 잠금을 대기하지 않고 바로 실행된다. (격리 수준 serializable 제외)

잠금을 대기하지 않으면서 일관성을 보장하기 위해 MVCC 기술을 이용한다.

3.1 MVCC(Multi Version Concurrency Control)

데이터 변경 시,

변경 전의 데이터를 복사하여

트랜잭션이 끝나기 전까지

Undo 로그라는 메모리 공간에 보관하고 있는 것

 

-ppt-

3.2 잠금 없는 일관된 읽기 with MVCC

InnoDB는

끝나지 않은 트랜잭션들이 존재할 때, 조회 기능으로 하여금 항상 그 이전 상태를 읽어오도록 보장한다.

아래 예시를 통해 원리를 이해해보자.

 

<update 트랜잭션 실행 도중 select 쿼리 실행 예>

 

step 1

트랜젝션이 시작되면서 '김예림' 을 'KimYerim' 으로 바꾼다.

이 때 undo로그 영역에 이전버전을 복사해둔다. (MVCC)

또한 트랜잭션이 끝나기 전까지 'KimYerim', '100' 레코드는 잠긴다.

 

step 2

이 와중에 조회 쿼리가 실행된다.

이 때 InnoDB는 undo 로그의 데이터를 읽는다.

잠금 없는 일관된 읽기 with MVCC 예시

* 장점

조회시 대기하지 않으므로 빠르면서도, 일관성이 보장됨

 

* 주의사항

트랜잭션이 오래 걸리면

undo 로그도 오래 유지되어야 하므로

메모리 공간을 계속 차지함.

따라서 트랜잭션 하나가 오래 걸리는 것을 지양하자. 최대한 빨리 rollback/commit 하게 하자.

 

 

4. 자동 데드락 감지

데드락 감지 스레드가

잠금 대기 그래프를 주기적으로 검사해

교착상태에 빠진 트랜잭션들을 찾아서 그중 하나를 강제종료한다.

 

innodb_table_locks 시스템 변수

이 변수를 활성화하면

데드락 감지 스레드가 레코드 잠금 뿐만 아니라 테이블 레벨의 잠금까지 감지한다.

특별한 이유가 없다면 활성화하자.

 

데드락 감지 스레드 단점

왠만하면 괜찮지만

동시 처리 스레드가 매우 많아지거나, 각 트랜잭션이 가진 잠금의 갯수가 많아지면

데드락 감지 스레드가 느려진다.

 

데드락 감지 스레드가 잠금 목록을 검사하는 동안에는

트랜잭션들이 잠금 상태를 변경할 수 없기 때문에 대기한다.

→ 서비스 느려짐

해결방법

step 1. 데드락 감지 스레드 끄기

   → 데드락 발생시 무한정 대기

step 2. 요청에 시간 제한 걸기

   → 일정 시간이 지나면 트랜잭션이 강제 종료되므로, 데드락 발생시 무한정 대기하는 문제가 해결됨

 

5. 자동화된 장애 복구

Real MySQL 8.0  p105 ~ 107

 

 

다음 글 : InnoDB 스토리지 엔진이 제공하는 기능 (2) - 버퍼 풀


참고

1

Real MySQL 8.0 (백은빈, 이성욱)

2

[DB] Lock 이란?

https://chrisjune-13837.medium.com/db-lock-락이란-무엇인가-d908296d0279

 

1. 데이터 딕셔너리 (=메타데이터) 란?

데이터베이스 서버에서 테이블 구조 정보, 스토어드 프로그램 등의 정보. (그야말로 메타데이터!)

 

2. MySQL의 데이터 딕셔너리 관리

MySQL 8.0 버전부터는 메타 데이터를 InnoDB의 테이블에 저장한다.

시스템 테이블과 데이터 딕셔너리 정보를 모두 모아서 mysql DB에 저장한다.

 

mysql DB는 사용자 접근을 막아놨기 때문에 다음과 같이 조회를 해볼 수는 없다.

mysql> SELECT * FROM mysql.tables;
ERROR 3554 (HY000) : Access to data dictionary table 'mysql.tables' is rejected.

대신 MySQL 서버는 데이터 딕셔너리 정보를 information_schema DB의 TABLES와 COLUMNS 등과 같은 뷰를 통해 조회할 수 있게 하고있다.

-- information_schema DB의 tables 테이블에 대한
-- create table 구문 출력
SHOW CREATE information_schema.tables;

그러면 대충 아래와 비슷한 결과를 볼 수 있다.

CREATE VIEW 'TABLES'AS
	select 어쩌구 from 'mysql'.'tables' 'tbl' join 'mysql'.'schemata' 'sch'

 

+ Recent posts