목차
대용량 데이터, 트래픽 처리에 대해
서버 개발자의 핵심은 데이터다. 대용량 시스템이 어려운 이유는 많은 양의 데이터에서 시작된다. 어떻게 많은 양의 데이터를 안정적으로 삽입, 갱신, 조회 할 것인가? 이 글에서는 대용량 시스템에 대한 전반적인 이해와 RDBMS 관점에서 대용량 데이터 처리를 위한 정규화, 인덱스, 트랜잭션, 동시성 제어 를 알아볼 것이다.
대용량 데이터, 트래픽 처리는 왜 어려울까?
여러 이유가 있겠지만, 몇 가지를 들어보면
- 핵심은 하나의 서버 또는 데이터베이스로 감당하기 힘든 부하 때문이다.
이로 인해 다수의 서버와 데이터베이스를 활용하게 되는데, 이를 마치 하나인 것처럼 동작하도록 하기 위해 여러 최적화 기법이나 기술들이 활용된다. - 여러개의 서버에서 유입되는 데이터의 일관성을 보장할 수 있어야 한다.
- 대부분의 웹 서비스들은 24시간 무중단의 특성을 가지고 있어 잘못된 코드 한 줄이 미치는 영향의 범위가 크다.
- 시스템이 발전함에 따라 팀이 커지고 도메인별로 여러 개의 마이크로 서비스들이 복잡한 의존관계를 가지게 된다.
대용량 시스템은 어떠해야 하는가?
- 고가용성
언제든 서비스를 이용할 수 있어야한다. - 확장성
시스템이 비대해짐에 따라 증가하는 데이터와 트래픽에 대응할 수 있어야한다. - 관측가능성
문제가 생겼을 때 빠르게 인지할 수 있어야하고, 문제의 범위를 최소화할 수 있어야한다.
스케일업과 스케일아웃
서버를 스케일업 한다는 것은 서버의 성능을 증가시킨다는 의미,
서버를 스케일아웃 한다는 것은 서버의 대수를 증가시킨다는 의미이다.
스케일 업 | 스케일 아웃 | |
---|---|---|
유지보수 및 관리 | 쉬움 | 여러 노드에 적절히 부하분산 필요 |
확장성 | 제약이 있음 | 스케일업에 비해 자유로움 |
장애복구 | 서버가 1대, 다운타임이 있음 | 장애 탄력성이 있음 |
스케일아웃에 대해 조금 더 얘기를 해보면, 스케일아웃은 실제로 서버가 여러개 존재하더라도, 클라이언트에게는 마치 하나의 서버인 것처럼 동작해야 한다. 그렇기 때문에 여러 서버들은 대부분 같은 데이터를 바라볼 수 있도록 데이터베이스를 공유하게 된다.
그렇다면 데이터베이스를 스케일아웃 하기는 어려울까? 라는 생각이 들 수 있다. 데이터베이스는 데이터라는 상태를 관리하고 있어, 서버보다 스케일아웃을 하기 위해서는 훨씬 많은 비용이 필요하다.
현대 서버 아키텍처는 상태관리를 데이터베이스에 위임하고, 서버는 상태관리를 하지 않는 방향으로 가고 있다. 이로 인해 서버는 자유롭게 스케일아웃을 할 수 있는 구조가 되고, 주로 서버는 메모리에서 데이터를 관리하며, 데이터베이스는 디스크의 데이터를 관리한다.
대용량 시스템의 아키텍처
- 시작
- 시작은 간단하게 클라이언트와 서버, 데이터베이스가 하나씩 있다.
- 이 때는 서버나 데이터베이스가 중단될 경우 시스템 이용이 어려운 상황이다.
- 트래픽이 점점 증가하면 서버의 응답속도도 느려진다.
- 서버 확장
- 서버가 다수 추가되고, 부하분산을 위해 로드밸런서가 추가된다.
- 서버를 추가할 때 고려할 점은 부하분산인데, 부하분산이 잘 될 수 있도록 앞단의 Nginx와 같은 로드밸런스를 추가한다.
- 서버도 추가했지만 서비스가 계속 커져 데이터베이스에도 병목이 생긴다.
- 데이터베이스 병목 해결
- 데이터베이스 부하를 최소화 할 수 있는 메모리 캐시를 추가한다.
- 캐시에 먼저 데이터를 질의하고 없으면 데이터베이스에 질의한 다음 캐시를 갱신하는 방식이다.
- 대외기관 연동
- 서비스의 규모가 커짐에 따라 이메일, 푸시 알림 등 대외기관과의 연동이 필요한 요구사항들이 추가된다.
- 이에 따라 점점 대외기관이 우리 트래픽을 받지 못해 서버의 응답속도가 느려진다.
- 비동기큐 도입
- 응답속도에 영향을 미치지 않도록 대외기관 통신을 카프카나 래빗엠큐와 같은 비동기큐를 이용한다.
- 확장한 아키텍처
- 점점 느려지는 시스템들을 개선하고, 이런 시스템들이 모여 하나의 거대한 서비스로 발전한다. 이러한 예는 극히 일부이며, 서비스의 특성과 팀에 따라 다양한 아키텍처들이 나타날 수 있다.
- 중요한 것은 각각의 기술들이 유기적으로 연결되어 어떠한 역할을 수행하며, 어떤 문제를 해결하고, 동작하는지 이해하는 것이다.
Mysql 아키텍처
서버에서 요청하는 SQL 은 Mysql 엔진, 스토리지 엔진을 거쳐 운영체제를 통해 디스크에 접근하여 데이터를 서버에게 전달하는데, Mysql 엔진은 사람으로 비유하면 판단과 명령을 내리는 두뇌, 스토리지 엔진은 동작을 수행하는 팔과 다리라고 생각하면 된다.
쿼리파서와 전처리기는 컴파일 과정과 매우 유사하다. 하지만 SQL 은 프로그래밍 언어처럼 컴파일 타임 때 검증할 수 없어 매번 구문 평가를 진행한다.
Mysql 캐시
Mysql 5 버전대까지는 쿼리캐시(SQL에 해당하는 데이터를 저장하는 것) 기능이 있었다. 하지만 8.0 버전대에 들어와서 쿼리캐시는 폐기되었다.
쿼리캐시는 데이터를 캐시하기 때문에 테이블의 데이터가 변경되면 캐시의 데이터도 함께 갱신시켜줘야 한다.
Mysql 에는 소프트 파싱이 없고, Oracle 에는 소프트 파싱이 존재한다. 하지만 모든 SQL 과 맵핑해 데이터까지 캐싱하지는 않는다. (힌트나 설정으로 가능하기도 하다)
Mysql 에서는 쿼리캐시, Oracle 에서는 소프트 파싱 모두 성능 최적화를 위해 캐시라는 기술을 도입한 것이다. 그러나 두 DB에서의 캐시를 사용하는 범위는 다르다. 쿼리캐시는 소프트 파싱에 비해 조회성능은 더 높지만 캐시 데이터 관리에 더 높은 비용이 들어간다.
소프트 파싱
SQL, 실행계획을 캐시에서 찾아 옵티마이저 과정을 생략하는 것, 이후 실행 단계로 넘어간다.
하드 파싱
SQL, 실행계획을 캐시에서 찾지못해 옵티마이저 과정을 거치는 것, 이후 실행단계로 넘어간다.
Mysql 엔진
- Mysql 쿼리파서
SQL 을 파싱하여 Syntax Tree 를 만든다.
이 과정에서 문법 오류 검사가 이루어진다.
- Mysql 전처리기
쿼리파서에서 만든 Tree 를 바탕으로 전처리를 시작한다.
테이블이나 컬럼 존재 여부, 접근권한 등 Semantic 오류를 검사한다.
- Mysql 옵티마이저
SQL 을 최적화를 담당하여 SQL 을 가장 빠르고 효율적으로 수행할 최적(최저비용)의 처리경로를 선택하는 것이 핵심이다.
쿼리를 처리하기 위한 여러 방법들을 만들고, 각 방법들의 비용정보와 테이블의 틍계정보를 이용해 비용을 산정한다.
테이블 순서, 불필요한 조건 제거, 통계정보를 바탕으로 전략을 결정한다. (실행계획 수립)
옵티마이저가 어떤 전략을 결정하느냐에 따라 성능이 많이 달라진다.
- 쿼리 실행기
옵티마이저가 결정한 계획대로 Handler API 를 통해 스토리지 엔진에 요청하는 역할을 수행한다.
Mysql 스토리지 엔진
디스크에서 데이터를 가져오거나 저장하는 역할을 수행한다.
스토리지 엔진은 InnoDB, MyIsam 등 여러개의 스토리지 엔진이 존재하며, Mysql 8.0 대 부터는 InnoDB 엔진을 기본으로 사용한다.
스토리지 엔진 특성에 따라 데이터 접근이 얼마나 빠른지, 안정적인지, 트랜잭션 기능의 제공여부 등이 달라진다.
InnoDB 와 MyIsam 차이점
두 스토리지 엔진의 핵심 차이점이라면 Locking 하는 방식과 트랜잭션을 제공하느냐이다.
Locking 은 트랜잭션 처리의 순차성을 보장하기 위한 방법이다.
트랜잭션 순차성을 보장하기 위해 InnoDB 는 특정한 로우를 Locking 하는 반면, MyIsam 은 테이블 전체를 Locking 한다.
또한 InnoDB 는 트랜잭션을 제공하며, MyIsam 은 제공하지 않는다.
정규화, 비정규화
위키백과에 데이터베이스 정규화에 대한 설명을 보면, 관계형 데이터베이스의 설계에서 중복을 최소화하게 데이터를 구조화 하는 프로세스라고 한다...* 데이터베이스 디자인 표준 가이드는 데이터베이스가 완전히 정규화되게 디자인되어야 한다. 하지만 그 뒤에 일부가 성능상의 이유로 비정규화될 수 있다.
이 내용에서 결국 읽기와 쓰기 사이의 트레이드 오프라는 것을 알 수 있다. 읽기와 쓰기를 분리해서 바라보고, 둘 중 어떤 것에 중점을 두고 최적화할지에 따라 설계가 달라지기 때문이다.
즉, 데이터의 중복을 최소화 한다는 것은 여러 곳에 있는 동일한 데이터를 한 곳에서만 관리한다는 것이다. 이렇게 되면 데이터의 불일치가 생기지 않게 된다. 하지만 중복을 최소화하게 되면 읽을 때는 항상 원본 데이터를 찾아가서 참조해야한다.
테이블 설계 관점에서 정규화는 읽기의 성능을 희생하고, 데이터 관리를 용이하게 하는 것이다.
정리하면 다음과 같다.
정규화
- 중복을 제거하고 한 곳에서 데이터를 관리
- 데이터 정합성 유지가 쉬움
- 읽기시 참조가 발생
정규화 고려사항
얼마나 빠르게 데이터의 최신성을 보장해야 하는가?
데이터 변경 주기와 조회 주기는 어떻게 되는가? (히스토리성 데이터는 오히려 정규화를 하지 않은 것이 좋은편이다.)
읽기시에 객체(테이블)의 탐색 깊이는 얼마나 깊은가? (즉, 몇 단계 조인을 하여 탐색해야 하는가?)
정규화를 하기로 했다면 읽기 시 데이터를 어떻게 가져올 것인가?
- 테이블 조인을 많이 활용하는데, 고민해볼 문제다.
테이블 조인이 쉽게 데이터를 조회해올 수는 있지만, 서로 다른 테이블의 결합도를 높여 리팩토링이 힘들어지고 아키텍쳐 성능을 풀어가는데 어려움을 겪을 수 있으며, 조인이 성능면에서도 좋은편이 아니기 때문이다. 또한 빈번하게 테이블 조인이 일어나면 영향도 파악이 힘들어진다. 이러한 문제는 추후에 캐싱으로 확장할 여지마저 줄어들기에 최후의 수단으로 보류하는 것이 좋을 수 있다. - 조회시에는 성능이 좋은 별도 데이터베이스나 캐싱 등 다양한 최적화 기법을 이용할 수 있다.
- 조인을 사용하게 되면 이러한 기법들을 사용하는데 제한이 있거나 더 많은 리소스가 들 수 있다.
- 읽기 쿼리 한 번 더 발생되는 것은 그렇게 큰 부담이 아닐 수도 있다.
정규화 예시
- 주문 테이블에 제조사 정보가 있다면, 제조사 이름이 바뀌었을 경우 바뀐 이름이 들어가는게 맞는지 혹은 기존 이름이 들어가는 것이 맞는지 이런 경우에는 비즈니스, 요구사항에 따라 다르기에 PM 혹은 기획자 등에게 확인하는 것이 좋다.
- SNS 팔로잉을 예로 들어 A 가 B 를 팔로우를 한다면, 팔로잉 테이블에 A 와 B 의 닉네임이 들어갈텐데, 만약 B 의 팔로워가 100만명쯤 되는 인플루언서이고, 닉네임을 변경한다면 100만 라인을 update 할 것인가? 아니면 과거 닉네임은 update 하지 않고 그대로 보여줄 것인가? SNS 특성상 팔로잉은 최신 닉네임을 보여주는게 맞을것이다. 그렇다면 100만 라인을 update 하는 것 보다는 정규화를 하는게 맞을 가능성이 높다.
비(반)정규화
- 중복을 허용
- 데이터 정합성 유지가 어려움
- 참조없이 읽기가 가능
데이터베이스 성능 핵심
데이터베이스의 데이터는 메모리에 먼저 쓰고, 결국 디스크에 저장된다. 즉, 데이터베이스 성능에 핵심은 디스크 접근(I/O)을 최소화 하는 것이다. 그렇다면 디스크 접근을 줄이려면 메모리에 올라온 데이터로 최대한 요청을 처리하는 것인데 데이터베이스는 메모리에 데이터 유실을 고려해 WAL(Write Ahead Log)를 사용한다.
WAL (Write Ahead Log, 로그 선행 기입)
쓰기가 발생할 때 마다 디스크에 가서 데이터를 저장하는 것은 비효율적이기에 메모리에 쌓아뒀다가 한 번에 디스크로 보내어 쓴다.
일단 파일의 끝부분부터 순차적으로 쿼리의 로그가 남아서 메모리에 쌓여있던 데이터가 디스크에 가지 않고 유실되더라도 이 파일에 히스토리가 있어 서버가 다시 실행되면 파일에 있던 로그를 순차적으로 재실행시켜 디스크에 있는 원본 데이터와 같아져 정합성을 유지한다. redo 및 undo 정보를 모두 로그에 기록하며, buffer 를 비우기 전에 로그 파일에 기록한다.
쿼리 프로파일링
쿼리 프로파일링은 쿼리 수행 시 여러 성능 지표나 통계를 확인할 수 있는 기능이다.
Mysql에서 쿼리가 처리되는 동안 각 단계별 작업에 시간이 얼마나 걸렸는지 확인할 수 있으며, Mysql 5.1 버전 이상에서 지원한다.
인덱스
인덱스는 정렬된 자료구조로 핵심은 인덱스를 통해 읽기 시에 탐색(검색) 범위를 최소화 하는 것이다. 읽기의 성능은 높이지만, 쓰기나 갱신, 삭제의 성능은 낮아진다. 테이블의 쓰기, 갱신, 삭제가 일어나면 인덱스 테이블에서도 동일한 과정이 일어나기 때문이다. 그래서 인덱스의 성질을 잘 이해하고 활용하는 것이 중요하다.
특정 컬럼으로 인덱스를 설정해두면 해당 컬럼과 id 값에 대한 인덱스 테이블이 생성된다. 그리고 쿼리가 들어오면 인덱스를 먼저 조회하고, 다음으로 원본 데이터를 찾아간다. 그래서 데이터베이스가 데이터를 스캔하는 방식에 대해 알고, 그에 따라 인덱스를 잡는것이 좋다. 예를 들어 식별자가 적은 성별로 인덱스를 잡으면 탐색 시 데이터를 절반밖에 걸러낼 수가 없어 탐색범위가 많이 좁혀지지 않는다.
Mysql 인덱스는 PK 사이즈가 커지게 되면 하나의 노드가 가질 수 있는 데이터의 개수가 적어지기에 삽입, 삭제 시에 노드의 리밸런싱이 빈번하게 일어날 수 있다. 즉 PK 사이즈가 인덱스의 사이즈를 결정한다. 반면 오라클은 PK 대신 인덱싱 테이블에 데이터의 주소를 가지고 있다.
우리의 의도대로 인덱스가 동작하지 않을 수 있는데, explain 으로 확인해보자.
그리고 인덱스도 비용이다. 쓰기의 성능을 희생하고 읽기의 성능을 얻는 것이다. 그렇다면 꼭 인덱스로만 해결할 수 있는 문제인가를 생각해보아야 한다.
Mysql 에서 탐색에 사용되는 자료구조는 B+Tree 이다. 아래 그림에서 Cherry 인덱스 키를 찾아가는 방법을 보여준다.
인덱스를 다룰 때 주의할점
- 인덱스 필드 가공 (필드 가공이 생기면 인덱스를 사용할 수 없다)
예시 1 필드의 데이터 타입을 변형하는 경우
// age 는 int 타입
SELECT * FROM Member WHERE age = '1'; - 예시 2 필드를 연산하는 경우 (인덱스에 저장되어 있는 데이터로 인덱스를 찾아가기에 연산을 하게 되면 인덱스를 활용할 수 없다)
// age 는 int 타입
SELECT * FROM Member WHERE age * 10 = 1; - 복합 인덱스
예시 1
두 번째 컬럼인 원산지는 첫 번째 컬럼인 과일을 정렬한 후에 동일한 과일에 대해 원산지가 정렬되기에 원산지로만 where 조건을 주면 인덱스를 타도 훨씬 느려진다. - 하나의 쿼리에는 하나의 인덱스만 사용
기본적으로 하나의 쿼리에는 하나의 인덱스만 적용되기에 여러 인덱스 테이블을 동시에 탐색하지 않는다. (index merge hint 를 사용하면 가능) 또한 WHERE, ORDER BY, GROUP BY 를 혼합해서 사용할 때에는 인덱스를 잘 고려해서 사용하여야 한다.
클러스터형 인덱스(Clustered Index)
클러스터형 인덱스는 테이블 전체가 정렬된 인덱스가 되는 방식의 인덱스 종류이다. 실제 데이터와 무리(cluster)를 지어 인덱싱 되므로 클러스터형 인덱스라고 부른다.
또한, 데이터와 함께 전체 테이블이 물리적으로 정렬된다.
클러스터형 인덱스는 테이블당 하나만 생성할 수 있고, 어떤 컬럼을 선택하여 클러스터형 인덱스를 만들지에 따라 성능이 크게 달라질 수 있다. 특정 컬럼을 PK 로 지정하면 클러스터형 인덱스를 생성한다. 혹은 Unique + Not null 로 지정해도 클러스터형 인덱스를 생성한다. 이 두가지가 모두 없는 경우 InnoDB 는 내부적으로 GEN_CLUST_INDEX 라는 컬럼을 생성하여 클러스터형 인덱스를 생성한다. GEN_CLUST_INDEX 는 행이 생성된 순서대로 값이 부여된다. 즉, Mysql 의 PK 는 클러스터 인덱스이며, PK 를 제외한 모든 인덱스는 PK 를 가지고 있다.
그래서 클러스터형 인덱스는 PK 를 활용한 검색, 특히 범위 검색이 빠르다. 또한, 보조 인덱스들이 PK 를 가지고 있어 커버링에 유리하다.
또한 클러스터형 인덱스는 데이터 위치를 결정하는 키 값이다. 즉, 클러스터 키가 4를 제외한 1~5 까지 정렬되어 있는데, 4 를 추가하게 되면 5 부터는 뒤로 밀리고, 3과 5 사이에 4를 넣어주게 된다. 그래서 클러스터 키 삽입 및 갱신시에 성능 이슈가 발생한다.
비클러스터형 인덱스 (Non-Clustered Index)
비클러스터형 인덱스는 보조 인덱스(Secondary Index)라고도 불리며, 클러스터형 인덱스와 다르게 물리적으로 테이블을 정렬하지 않는다. 대신 정렬된 별도의 인덱스 페이지를 생성하고 관리한다. 즉, 실제 데이터를 함께 가지고 있지 않는다.
비클러스터형 인덱스는 테이블 당 여러개 생성이 가능하다.
커버링 인덱스
인덱스에서 바로 데이터를 조회할 수 있는 인덱스로 쿼리의 성능을 향상시키기 위한 인덱스의 형태이다.
커버링 인덱스는 인덱스에 필요한 모든 컬럼을 포함하여 쿼리를 처리할 수 있도록 하고, 인덱스 자체에 검색 조건에 필요한 컬럼 이외의 데이터도 함께 포함시킨다. 이를 통해 쿼리 실행에 필요한 추가적인 디스크 I/O 나 메모리 로딩을 줄일 수 있다.
하지만 커버링 인덱스를 사용하면 인덱스 크기가 커질 수 있으며, 업데이트 작업에 따른 추가적인 비용이 발생할 수 있어 이를 고려해야 한다.
트랜잭션
트랜잭션 ACID
ACID (원자성, 일관성, 고립성, 지속성) 는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어이다.
- Atomicity 원자성
데이터베이스 트랜잭션은 All or Nothing 이다. - Consistency 무결성, 일관성
트랜잭션이 종료되었을 때 데이터의 무결성이 보장된다. 제약조건을 통해 무결성을 유지한다. (유니크, 외래키 제약 등) - Isolation 독립성
트랜잭션은 서로 간섭하지 않고 독립적으로 동작한다. 하지만 많은 성능을 포기해야함으로 트랜잭션 격리레벨을 통해 제어를 한다. - Durability 지속성
완료된 트랜잭션은 유실되지 않는다.
트랜잭션 사용 시 유의사항
트랜잭션도 비용이 든다. 그래서 트랜잭션의 범위를 작게 가져가는 것이 좋다. 트랜잭션이 길어지면 DB의 커넥션을 오래 유지하기 때문에 동시다발적으로 일어나면 커넥션 풀 고갈로 이어질 수 있다.
선언적 트랜잭션 사용 시 유의사항
자신에게 @Transactional 을 선언하고, 자신을 호출하는 상위 메소드가 있을 경우 트랜잭션이 정상적으로 작동하지 않는다. (프록시 패턴으로 인해서)
SQL 로 트랜잭션을 확인하는 방법
START TRANSACTION; SQL... COMMIT; <- SQL 부분에는 원하는 트랜잭션을 확인하기 위한 본인의 SQL 을 작성한다. 이 SQL 을 실행하면 트랜잭션이 수행된다.
트랜잭션 격리 레벨 (Isolation)
Dirty Read: 커밋되지 않은 데이터를 읽는 것
Non Repeatable Read: 하나의 트랜잭션에서 같은 데이터를 여러 번 읽었을 때 결과가 다른 경우
Phantom Read: 같은 조건으로 데이터를 읽었을 때 없던 데이터가 생긴 경우
Dirty Read | Non Repeatable Read | Phantom Read | |
---|---|---|---|
Read Uncommitted | O | O | O |
Read Committed | O | O | |
Repeatable Read | O | ||
Serializable Read |
위 표를 보면 Read Uncommitted -> Serializable Read 로 갈수록 이상현상이 없어진다. 대신 아래로 갈수록 동시 처리량은 낮아진다.
DBMS 마다 트랜잭션 격리레벨이 다르기에 확인해보고 사용하자. DB Lock 의 범위가 길어질수록 커넥션 풀 점유시간이 길어지고, 커넥션 풀 고갈로 이어질 수 있기에 필요에 맞게 최소화하는 것이 중요하다. 그래서 보통 Read Committed, Repeatable Read 를 많이 사용한다.
Propagation 레벨 (전파 레벨)
분산시스템에서 트랜잭션을 여러 서비스 간에 전달하고 동기화하는 과정의 수준을 나타내는 것이다. 이를 통해 다음과 같은 결과를 만들어 낼 수 있다.
- 트랜잭션이 여러 단계로 나누어지거나 여러 서비스에 걸쳐 실행될 수 있다.
- 트랜잭션 시작점부터 끝점까지 안전하게 전달하고 동기화한다.
- 데이터의 일관성과 동기화를 보장하는 역할을 한다.
스프링부트에서 선언적 트랜잭션에 대해 트랜잭션이 어떻게 동작하는지 방법을 정의할 수 있다. 선언적 트랜잭션은 트랜잭션 템플릿을 사용하는 것보다 자유롭게 사용하지 못하기 때문에 Propagation 레벨을 통해 정의한다.
Mysql Lock
Mysql Lock 종류
- 테이블 락
- 레코드 락 (Mysql 에서는 Row 가 아닌 인덱스를 잠금 -> 인덱스가 없는 조건으로 Locking Read 시 불필요한 데이터들이 잠길 수 있음)
- gap 락
Mysql Lock 특징
트랜잭션의 커밋 혹은 롤백 시점에서 잠금이 풀린다. 트랜잭션이 곧 락의 범위이다. 락의 범위를 줄인다는 것은 트랜잭션의 범위를 줄인다는 것이다.
매번 잠금이 발생할 경우 성능저하를 피할 수가 없다.
예를 들어 트랜잭션 범위 내에서 AWS S3 에 파일 업로드를 한다면 S3 에 파일 업로드하는 시간, 네트워크 지연시간 등에 따라 트랜잭션 범위도 커지기에 가능하면 트랜잭션 범위가 늘어나는 것을 방지하기 위해 트랜잭션 밖에서 수행하는 것이 좋다.
Mysql Lock 발생할 수 있는 경우
테이블에 데이터 양이 많을 경우 컬럼을 추가할 때 default 값을 넣으면 테이블 락이 발생할 수도 있다. 그래서 24시간 운영하는 시스템이라면 이러한 방법을 사용하는 것은 위험하다. 이런경우 컬럼을 추가할 때 별도의 마이그레이션 배치를 만들어서 조금씩 데이터를 채워넣거나 아니면 조회 시점에 null 이면 값을 채워주는 방법을 사용하기도 한다.
Mysql Lock 확인 방법
락 상태를 확인
SELECT *
FROM performance_sechema.data_locks;
트랜잭션 상태를 확인
SELECT *
FROM information_schema.innodb_trx;
Shared Lock (읽기 락)
SELECT ... FOR SHARE 를 통해 읽기 락을 획득할 수 있다.
데이터 수정은 잠그고, 읽을 수 있도록 허용한다. 동시에 여러 트랜잭션이 데이터를 읽을 수 있다.
Exclusive Lock (쓰기 락)
SELECT ... FOR UPDATE 또는 UPDATE, DELETE 쿼리를 통해 쓰기 락을 획득할 수 있다.
동시에 여러 트랜잭션이 데이터를 수정하는 것을 방지하기 위해 사용한다. 읽기도 불가하게 잠근다.
동시성
데이터베이스에서 동시성 이슈가 발생하는 일반적인 패턴은 공유자원을 조회, 갱신할 때이다. 동시성 제어를 위한 가장 보편적인 방법은 락을 통한 트랜잭션들을 줄 세우는 것이다. 락을 통해 동시성을 제어할 때는 락의 범위를 최소화 하는것이 중요하다. Mysql 에서는 트랜잭션의 커밋 혹은 롤백 시점에 잠금이 풀리는데, 이는 트랜잭션이 곧 락의 범위이다.
동시성 이슈가 어려운 이유
- 로컬에서는 대부분 하나의 스레드로 테스트한다.
- 이슈가 발생하더라도 오류가 발생하지 않는다.
- 코드에서 잘 보이지 않는다.
- 항상 발생하지 않고 비결정적으로 발생한다.
낙관적 락, 비관적 락
락을 통한 줄 세우기는 비관적 락 이라고 하며, 락을 통한 동시성 제어는 불필요한 대기 상태를 만든다. 그럼 동시성이 빈번하지 않은 쿼리로 인해 다른 쿼리가 대기한다면 동시성 이슈가 빈번하지 않길 기대하고, 어플리케이션에서 제어하는 낙관적 락이 있다. 낙관적 락은 CAS (Compare and set) 을 통해 제어한다. 간단히 설명해서 데이터베이스 테이블에 로우마다 데이터의 버전을 나타내고, 쿼리의 조건으로 버전을 체크하는 것이다. 그럼 동시성 이슈가 발생하더라도 버전이 맞지 않으면 해당 쿼리는 실패하게 될 것이고, 실패에 대한 처리를 직접 구현하는 방법이다.
인텔리제이에서 간단한 자바 API 동시성 테스트
브레이크 포인트를 잡고 디버깅 모드로 실행한다. 그리고 브레이크 포인트에 suspend 를 All -> Thread 로 변경한다. 여기에서 All 은 요청을 하나밖에 처리를 못하고, 두 개를 동시에 보내도 나머지 한 개는 기다려야 한다.
이렇게 디버깅 모드가 실행되고 나서 API 를 두 번 호출해본다. 그러면 다른 스레드로 두 개가 브레이크 포인트에 잡히게 된다.
마치며
이 글은 업데이트 중이며, 관련해서 공부하면 좋은 주제에 대해 남기고 마무리한다.
TODO
- PK 설정 Auto increment, UUID 비교
- Null 허용에 대해서
- Mysql 랜덤io 순차io
- Mysql 데이터 스캔방식
- Mysql 성능 개선 메모리 활용방법
- Mysql 의 넥스트 키 락이 등장한 배경
- Mysql 외래키로 인한 잠금
- Mysql 데드락
- Mysql master / slave
- Mysql 파티셔닝 (초대량 데이터를 관리하는 방법)
- InnoDB redo, undo
- InnoDB buffer pool
- Java 에서의 동시성 이슈 제어방법
- 분산환경에서의 동시성 이슈 제어방법
- ngrinder, jmeter(부하테스트 툴), easy random
'IT > 대용량 데이터&트래픽 처리' 카테고리의 다른 글
Kafka 개념, 구조 (0) | 2023.06.06 |
---|---|
Redis 에 대해서 (세션, 캐시, 클러스터, 쿼리튜닝) (1) | 2023.06.06 |
외부 서비스 연동 시 비동기 처리에 대해서 (0) | 2023.01.27 |