데이터 시스템에서는 발생 가능한 문제가 있고, 이 결함들을 해결하기 위해 트랜잭션이 메커니즘으로 채택돼왔다.
※ 트랜잭션: 앺에서 몇개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법. 전체가 성공(커밋)하거나 실패(abort, 롤백)
※ 발생 가능한 문제
- DB hw, sw 은 언제라도 실패할 수 있다.
- 앺은 연산 도중 언제라도 죽을 수 있다.
- 네트워크 끊기면 노드 사이 통신 안 될 수 있다.
- 여러 클라가 동시에 쓰기를 해서 내용을 덮어쓸 수 있다.
- 부분적으로만 갱신돼서 클라가 비정상 데이터를 읽을 수 있다.
- 클라 사이의 경쟁 조건이 버그 유발할 수 있다.
이번 장에서는 특히 동시성 관련해서 생길 수 있는 여러 문제들과 이를 방지하기 위한 DB에서 사용하는 알고리즘을 알려주겠다.
ACID의 의미
트랜잭션은 4가지 안전성을 제공한다: A(Atomic,원자성) / C(Consistency, 일관성) / I(Isolation, 격리성) / D(Durability, 영속성)
원자성(Atomic, abortability)
트랜잭션은 한 번에 하려는 연산이 여러개일 때 중간에 실패시 이미 처리된 작업은 abort 해야한다. 즉, 하나로 묶인 작업은 모두 성공하거나 실패해야 한다.
왜? 원자성이 없으면 중간에 실패했을 때 어떤 변경이 일어났고 어떤 변경은 안 일어났는지 알기 어렵다. 그럼 재시도를 하더라도 중복으로 실행돼서 잘못된 데이터가 만들어질 수 있는 사이드 이펙트가 발생한다.
원자성은 여러 프로세스가 동시에 같은 데이터에 접근할 때 발생하는 문제에 대해서는 논외이다. 이건 격리성에서 다룰 문제이다.
일관성(Consistency)
항상 진실이어야하는 어떤 데이터에 관한 선언이다. ex. 대변과 차변은 항상 같아야 한다.
이런 일관성에 대해 올바른지 아닌지 체크하는 것은 앺의 책임이다.
그래서 일관성은 ACID 에 속하지 않는다. (약어 만들기 위해 낀 것이지 일관성이 중요하게 고려되진 않았다고 한다.)
격리성(Isolation)
여러 클라가 동시에 동일한 레코드에 접근하면 동시성 문제(경쟁 조건)에 맞닥뜨리게 된다.
여기서 격리성이랑 동시에 실행되는 트랜잭션은 서로 격리되어 다른 트랜잭션을 방해할 수 없는 걸 말한다.
DB는 동시에 실행되도 트랜잭션이 순차적으로 실행됐을 때 결과와 동일하도록 보장한다.
지속성(Durability)
트랜잭션이 성공적으로 커밋됐다면 하뒈어 결함 발생하거나 DB죽더라더 모든 데이터는 손실되지 않는다는 보장
그래서 디스크 오염됐을 때 복구할 수 있도록 쓰기 전 로그 같은 수단을 동반한다.
다른 노드에 데이터를 복제하는 방법도 있다.
다중 객체 연산 vs. 단일 객체 연산
일단 다중 객체든 단일 객체든 한 트랜잭션 내에서 여러 번 쓰기할 때 원자성과 격리성은 보장해야 한다.
- 원자성: 쓰기 도중 오류 발생하면 트랜잭션은 abort 되고 롤백돼야 한다. all or not 이므로 부분 실패를 걱정할 필요가 없어야한다.
- 격리성: 한 트랜잭션이 여러 번 쓰고 있을 때 (여러 테이블에 쓰고있다든가) 다른 트랜잭션은 그 내용이 전부 반영됐을 때 봐야한다. 쓰고 있는 도중에 보면 동기화가 깨질 수 있어서.
ex. 격리성이 깨지는 예
한 트랜잭션(user1): email 에 넣고, mailBoxes 업데이트함 - 다중 객체 연산
다른 트랜잭션(user2): user1 트랜잭션 진행 도중에 읽음. 그래서 mailBoxes 업데이트 하기 전에 조회해 버려서 새로 온 메일은 있는데 unread 카운트는 0이라 뜸.
→ 격리성 깨짐 (일부분만 봤으므로)
단일 객체에서 원자성, 격리성 보장하기
- 원자성: 장애 복구용 로그 (Write Ahead Log) 로 복구
- 격리성: 각 객체에 잠금 사용
오류, abort 처리하는 방법
재시도를 한다.
근데 완벽하지는 않다. 왜?
- 서버가 클라한테 커밋 성공 날리다가 네트워크 끊겼을 때 재시도하면 트랜잭션 두번 실행됨
- 오류가 과부하가 원인이라면 재시도는 문제를 악화시킬 수 있다.
- 일시적인 오류(데드락, 격리성 위반, 일시 네트워크 단전, 장애 복구)영구적인 오류(ex. 제약 조건 위반) 은 재시도해도 아무 소용 없다.
이제, 발생할 수 있는 격리성 위반 사례와 트랜잭션 격리하는 법을 알려줄게
커밋 후 읽기 (read commited)
: 가장 기본적인 수준의 트랜잭션 격리
- 더티 읽기가 없음을 보장 - 커밋된 데이터만 읽음
- 더티 쓰기가 없음을 보장 - 커밋된 데이터만 덮어씀
더티 읽기 방지
더티 읽기란? commit 이나 abort 안된 데이터 읽는거
어케 구현?
커밋 완료 전에는 과거 데이터 읽고 커밋되면 그 데이터 읽음
장점
- 사용자 중 일부는 갱신된, 일부는 갱신 안 된 데이터를 보면 사용자가 혼란스러워한다.
- 만약 트랜잭션이 abort 되면 모두 롤백돼야 하는데 커밋 전에 읽어버리면 커밋되지 않을 데이터를 보게 된다.
더티 쓰기 방지
더티 쓰기란? 커밋될 데이터를 나중에 쓴 데이터가 덮어써버림
어케 구현?
먼저 쓴 트랜잭션이 commit이나 abort 될때까지 두 번째 쓰기를 지연시킨다.
장점
- 구매는 밥이, 송장은 앨리스가 받는 사고를 방지할 수 있다.
커밋 후 읽기 구현
사용처: 오라클11g, POSTGRESQL, SQL 서버 2012, MemSQL
- 더티 쓰기 방지: 쓰려는 객체의 잠금을 획득해야지만 쓸 수 있게 한다.
- 더티 읽기 방지: 트랜잭션 실행중이면 과거 값 읽고, 커밋되면 새 값을 읽게 한다. 쓰기 처럼 잠금을 걸려면 응답시간이 안 좋아져 잘 안 쓴다.
스냅숏 격리와 반복 읽기
스냅숏 격리가 필요한 이유
커밋 후 읽기로 격리성을 보장해도 읽기할 땐 잠그지 않으므로 일관성이 깨지는 시나리오가 있다.
읽기가 진행중(계좌 500, 500)인데 그 찰나에 쓰기 트랜잭션(계좌 600, 400)이 실행돼버려서 계좌1은 예전값인 500, 계좌2는 새 값인 400이 찍힘
읽기 스큐(read skew = 비반복 읽기, nonrepeatable read): 이렇게 읽기 일관성이 깨지는 현상
※ skew: 비뚤어짐, 왜곡하다. 여기선 시간적인 이상 현상을 뜻함
위 사례는 새로고침하면 커밋된 데이터 계좌 600, 400 으로 찍히겠지만 이런 일시적인 비일관성을 감내할 수 없는 경우도 있다.
- 백업할 때. 백업의 일부는 과거버전, 일부는 최신 버전을 가져버리면 복원시 비일관성이 영속적이게 된다.
- 대용량 데이터 질의할 때. 불합리한 결과를 반환할 수 있다.
해결책 > 스냅숏 격리
스냅숏 격리란
- 각 트랜잭션은 DB의 일관된 스냅숏을 읽는다. 시작할 때 부터 커밋된 상태의 모든 데이터를 보기 때문에 중간에 바뀌어버린 데이터를 안 봐도 된다.
- 트랜잭션마다 고유한 버전의 데이터(스냅숏)을 갖고 있다.
- 읽기 전용 트랜잭션에 유용한다.
사용처: POSTGRESQL, MySQL(InnoDB), 오라클, SQL서버
용어 정리 (스냅숏 격리)
오라클 - 직렬성
POSTGREQL, mySQL - 반복 읽기(repeatble read)
스냅숏 격리 구현하기
더티 쓰기 방지 위해 쓰기 잠금 사용한다. 그리고 읽을 때는 잠금 획득할 필요없이 읽을 수 있다.
(elsboo) 마지막에 왜 400 이 아니라 삭제된 500 을 봄?
다중 버전 동시성 제어(MVCC, Multi-Version Concurrency Control): 트랜잭션마다 ID를 부여하여 DB 객체가 여러 버전을 함께 유지한다.
커밋 후 읽기는 두 개버전(과거, 최신 각 1개)만 있으면 되지만 스냅숏 격리는 트랜잭션 수만큼 버전을 갖게 된다.
트랜잭션이 볼 수 있는 데이터 (가시성 규칙)
- 트랜잭션 시작 시 그 시점에 진행 중인 다른 트랜잭션이 쓴 데이터는 무시된다(나중에 커밋될지라도)
- abort된 트랜잭션이 쓴 데이터는 모두 무시
- 트랜잭션 시작 후에 시작된 다른 트랜잭션이 쓴 데이터는 모두 무시
- 이외 모든 데이터는 앺 질의로 볼 수 있다.
→ 이미 커밋된 트랜잭션의 데이터만 볼 수 있다.
지금까지의 커밋 후 읽기와 스냅숏 격리 수준은 동시에 쓸 때 읽기 전용 트랜잭션이 무엇을 봐야 하는가에 대한 문제였다.
두 트랜잭션이 동시에 쓸 때 한 쓰기는 무시되버리는 갱신 손실이 있는데 이게 뭔지, 어케 해결하는지를 보겠다.
갱신 손실 (lost update)
앺에서 DB 의 어떤 값을 읽고 변경한 후 변경된 값을 다시 쓸 때 (read-modfiy-write), 이걸 동시에 하면 한 쪽은 갱신한 값을 포함하지 않으므로 손실되는 경우를 말한다.
해결책
1.원자적 쓰기 연산
객체를 읽을 때 잠금. 변경되는 부분을 잠가서 변경 후 갱신 완료될 때까지 읽지 못하게 한다(=커서 안전성, cursor stability)
이러면 read-modify-write 주기를 구현할 필요가 없다. 읽기 시작할 때 부터 잠가버리니깐
ex. 몽고DB: json 일부를 지역적으로 변경하는 원자적 연산 제공 / redis: 우선순위 큐같은 데이터 구조를 변경하는 원자적 연산 제공
2.명시적 잠금
객체를 명시적으로 잠그로 read-modify-write 를 실행한다. 다른 트랜잭션이 동시에 읽으려고 하면 첫번째 read-modify-write 주기가 완료될 때까지 기다리게 한다.
- (elsboo) 읽을 때도 잠금을 획득해야 하는건 1번 원자적 쓰기 랑 비슷한거 같은데 둘의 차이점은 뭐지?
3.자동 감지
read-modify-write 가 순차적으로 실행되게 해서 갱신 손실을 방지한다.
아니면 병렬로 실행시키고 충돌 발생하는 트랜잭션은 abort 하고 재시도하도록 강제한다.
※ 사용처: POSTGRESQL, 오라클, SQL (mySQL 은 갱신 손실 감지하지 않음)
4.compare-and-set (CAS)
값을 마지막 읽은 후로 변경되지 않았을 때만 갱신을 허용함으로써 갱신 손실을 회피한다. 방금 읽은 값과 이전 읽은 값이 일치하지 않으면 갱신은 반영 안되고 read-modify-write 주기 재시도한다.
복제 서버가 있을 때 충돌 해소는?
잠금이나 CAS 는 복사본이 하나만 있다고 가정하기 때문에 복제가 여러 개 있으면 사용할 수 없다.
그러면? 쓰기가 동시에 실행될 때 여러 충돌된 버전을 생성하는 걸 허용하고 사후에 앺 단에서 충동을 해소하고 버전을 병합한다. 흔히 쓰는 방법.
쓰기 스큐와 팬텀
더티 쓰기와 갱신 손실을 동시에 같은 객체에 쓰려할 때 발생하는 문제이다. 동시에 다른 객체를 변경했을 때 문제가 생기는 경우를 쓰기 스큐라고 한다.
그날 온콜이 0명이면 안된다. 그대신 날 온콜이 2명 이상이면 한명은 취소할 수 있다.
앨리스와 밥이 동시에 온콜 몇명인지 확인해서 2인걸 확인하고 동시에 바꿔서 그날 온콜 0명 되버리는 문제 발생
→ 두 트랜잭션이 같은 값 읽고 각자 다른 객체를 변경했을 때 문제가 발생하면 쓰기 스큐라고 한다.
(더티쓰기, 갱신손실은 두 트랜잭션이 같은 값 변경하려 했을 때임)
- (elsboo) 내가 생각한 해결법: 트랜잭션 마지막에 온콜수 count 쿼리 날려서 0이면 롤백하는 로직을 추가하면 되지 않을까!
해결책: 변경 여부 판단할 값을 읽을 때 잠근다.
FOR UPDATE: 이 질의가 반환하는 모든 row 잠그라고 지시한다.
팬텀(phantom)
트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 질의 결과를 바꾸는 효과
ex. 위 의사 온콜 경우에서 서로 false 로 바꾸고 다시 온콜 카운트 쿼리 날리면 0으로 돼서 온콜 못 내리는 경우
또 하나의 해결책, 직렬성
쓰기 스큐나 팬턴 같은 문제를 해결하기 위해 직렬성 격리로 방지!
직렬성 구현 방법
- 진짜로 트랜잭션을 순차적으로 실행한다.
- 2단계 잠금(2 Phased Locking) 을 사용한다. (잠금 종류: 공유모드, 독점모드)
- 읽을 때: 공유 모드 잠금을 획득해야 한다. 동시에 공유 모드 잠금을 얻는 건 허용되나 독점 모드 잠금 획득한 트랜잭션 있으면 걔가 끝날 때 까지 기다려야 한다.
- 쓸 때: 독점 모드 잠금을 획득해야 한다. 독점 모드 잠금 획득하면 공유든 독점이든 동시에 획득할 수 없다. 끝날 때까지 기다려야 한다.
- → 쓰기 요청이 없으면 누구든지 동시에 읽을 수 있다. 하지만 쓰려면 그 누구도 쓰거나 읽을 수 없다.
- 낙관적 동시성 제어(ex. 직렬성 스냅숏 격리) 를 사용한다.
- 일단 쓰고 직렬성 충돌 감지하면 abort 시킬 트랜잭션 결정해서 abort 시킨다.
레퍼런스
데이터 중심 애플리케이션 설계 7장
'SearchDeveloper > 데이터 중심 애플리케이션 설계' 카테고리의 다른 글
[9] 일관성과 합의 (0) | 2023.08.10 |
---|---|
[8] 분산 시스템의 골칫거리 3가지 (0) | 2023.08.06 |
[6] 파티셔닝 하는 방법, 리밸런싱 전략, 요청 라우팅 전략 (0) | 2023.07.01 |
[5] 복제를 구성하는 방법과 동기화를 위한 노력 (0) | 2023.06.25 |
[4] 데이터를 다른 시스템에 전송하기 위한 부호화 (인코딩) 와 호환성 (0) | 2023.06.15 |