기술스택을 쌓아보자/데이터 엔지니어링
데이터 중심 애플리케이션 설계 - 트랜잭션
소리331
2023. 4. 26. 08:30
반응형
트랜잭션
- 트랜잭션은 수십년동안 여러 내결함성 결여로 인해 발생되는 문제를 해결하는 메커니즘으로 채택되어 왔다. 데이터베이스에 접속하는 애플리케이션에서 프로그래밍모델을 단순화 하려는 목적으로 만든 것이다.
- 안전성보장: safety guarantee: db에서 트랜잭션 사용을 통해 어플의 잠재적 오류와 동시성 문제를 무시할 수 있다.
- 항상 트랜잭션이 필요한 것은 아니다.‘
- 이번장의 중요 질문: 트랜잭션이 필요한지 아닌지 어떻게 알 수 있을까?
애매모호한 트랜잭션의 개념
- 관계형 데이터베이스는 거의 모두 트랜잭션을 채택하는 경우가 많고, 비관계형 베이스는 채택하는 경우도, 아닌 경우도 있다. ⇒ 이 과정에서 트랜잭션의 의미가 약화되었다.
ACID의 의미
- 원자성(atomicity), 일관성 (consistency), 격리성(isolation), 지속성(durability)
- 현실에서는 acid의 구현 의미가 제각각이다. , 거의 마케팅 용어가 되어 버렸다.
- BASE: ACID 표준을 따르지 않는 시스템
- Basically available, soft state, eventual consistency( 가용성을 제공하고, 유연하며, 최종적 일관성을 지님),
원자성 atomicity
- 원자적 연산의 의미: 클라이언트가 쓰기 작업 몇개를 실행하려고 하는데, 그중 일부만 처리된 후결함이 생기면 무슨일이 생기는지를 설명한다. 완료되지 않는다면 여태까지 쓰인 연산을 abort 해야한다.
- 오류가 생겼을 때 롤백하고 기록한 모든 내용을 취소하는 능력.
- 나현공유: 속성을 가지고 있으면서 속성을 띄는 가장 작은 단위, tr은 하나의 tr 보다 작게 쪼갤 수 없기 때문에 원자성( 커밋되던가 어보트되던가)
일관성 consistency
- 일관성을 뜻하는 여러단어들이 있다. ( 최종적 일관성, 복제일관성, 일관성 해싱, 선형성)
- Invariant: Acid 맥락에서는 데이터베이스가 좋은 상태에 있어야 한다는 것의 어플에 특화된 개념을 말한다. 데이터에 관한 어떤 선언이 있다는 것.
- TR이 올바르게 작성되도록 하는 것은 어플리케이션의 책임이다. ⇒ c는 db 보다는 어플의 성격
격리성 isolation
- 여러 클라이언트들이 동시에 동일한 레코드에 접근하면 경쟁조건에 맞닥뜨리게 된다.
- 격리성은 동시에 실행되는 트랜잭션은 서로 격리된다는 것을 의미한다. (직렬성)
- 여러 트랜잭션이 동시에 실행되었더라도 트랜젝션이 커미소디었을 때의 결과각 순차적으로 실행되었을 때의 결과와 동일하도록 보장한다.
- 직렬성 격리는 성능 손실이 있으므로 보통 이보다 약한 스냅숏격리를 한다.
- 트랜잭션들은 서로를 방해하지 말아야한다.
지속성 durability
- TR이 성공적으로 커밋되었다면 하드웨어 결함이 발생하거나 db가 죽더라도 기록된 모든 TR은 손실되지 않는다는 보장이다.
단일 객체 연산과 다중 객체 연산
- 원자성과 격리성은 클라이언트가 한 트랜잭션 내에서 여러번 쓰기를 하면 db가 어떻게 해야하는지를 설명한다.
- 다중 객체 트랜잭션: 데이터의 여러 조각이 동기화된 상태로 유지돼야 할 때 필요하다. 한번에 여러 로우를 변경할 수 있다고 가정한다.
- 다중 객체 트랜잭션은 읽기연선과 쓰기연산이 동일한 트랜잭션에 속하는지 알아낼 수단이 있어야 한다. Tcp 연결을 기반으로 한다.
- Tcp 연결이란? https://velog.io/@ragnarok_code/Network-TCP-프로토콜-연결종료-과정
- Begin transaction 문과 commit 문 사이의 모든 것은 같은 tr 에 속하는 것으로 여겨진다.
- 그런데, 비관계형은 앞서 말한 것고 ㅏ같은 연산을 묶는 방법이 없는 경우가 많다. 다중 객체 api가 있어도 이게 반드시 tr 시맨틱을 뜻하지는 않는다. 부분적으로만 갱신될 수 있다.
단일 객체 쓰기
- 저장소 ㅔㅇㄴ진들은 거의 보편적으로 한 노드에 존재하는 단일 객체 수준에서 원자성과 격리성을 제공하는 것을 목표로 한다.
- 원자성은 자앵복구용로그를 써서 구현할 수 잇고, 격리성은 각 객체에 잠금을 통해 구현할 수 있다(flock처럼)
다중 객체 트랜잭션의 필요성
- 보통 다중 객체 트랜잭션을 구현하지 않는 경우가 있다. 왜냐하면 파티션 등 여러 시나리오에서 방해가 되기 때문이다.
- 단일객체 만으로도 충분한 사용사례가 ㅣㅇㅆ다. 하지만 많은 경우에는 코디네이션 되어야 한다.
오류와 어보트 처리
- 트랜잭션의 핵심기능은 오류가 생기면 기존 작업이 초기화 되어 안전하게 재시도 할 수 잇다는 것이다. 그런데 모든 시스템이 이철학을 따르지 않는다.
- 가령 리더없는 복제는 best effort 원칙을 기반으로 더 많은 일을 하는 것을 우선으로 둔다.
- 오류를 해결하는 것은 어플리케이션의 역할이다.
완화된 격리 수준
- 동시성버그는 운이 없을 때만 촉발되기 때문에 테스트로 발견하기 어렵다. 때문에 트랜잭션 격리를 제공하므로서 동시성 문제를 감추려고 했다. (직렬성 격리를 통해)
- 그런데 현실에서 격리는 간단하지 않다. 모든 이슈로부터 보호할 수 없다.
- 맹목적으로 도구에 의존하기 보다는 방지하는 방법을 배울 필요가 잇다.
- 비직렬성 격리수준을 몇가지 살펴본다.
커밋 후 읽기
- 이 수준에서는 두가지가 보장된다.
- 더티읽기와 쓰기가 없음: 커밋된 데이터만 보고 쓰이게 된다.
더티 읽기 방지
- 더티읽기: 커밋되지 않은 트랜잭션을 보게 되는 것
- 더티읽기를 막아야 한다. (사용자마다 보는 것의 일관성을 보장 할 수 없음)
더티 쓰기를 방지
- 커밋되지 않은 트랜잭션들이 겹쳐 덮어씌워지게 되는 것.
커밋 후 읽기 구현
- 로우 수준 잠금을 사용해 더티 쓰기를 방지한다.
- 커밋되거나 어보트 될때까지 잠금을 보유하고 잇어야 한다.
- 동일한 잠금을 써서 객체를 읽기 원하는 드랝개션이 잠시 잠금을 획ㄷ그한 후 읽기가 끝난 후 바로 해제하게 하는 것이다. ⇒ 잘 작동하기 어려움
- 때문에 로우 수준으로 잠금을 진행하는 것이다.
스냅숏 격리와 반복 읽기 (263~)
- ㅋㅓ밋 후 읽기 구현도 여전히 동시성 버그가 생길 수 있다.
- 비반복 읽기, 읽기 스큐: Nonrepeatable read, read skew, ⇒ 앨리스 잔고 사례
- 스냅숏격리: 각 트랜잭션은 데이터 베이스의 일관된 스냅숏으로 부터 읽는다. Tr이 시작될 때, db에 커밋된 모든 데이터를 본다는 걳이다.
- 읽기만 실행하는 질의에 요긴하다.
스냅숏 격리 구현
- 더티 쓰기, 읽기를 방지하기 위해 쓰기 잠금을 사용한다. ⇒ 한번에 하나씩만 쓰인다는 것.
- 읽는 쪽과 쓰는 쪽이 서로를 결코 차단하지 않는다는 것.
- 다중 버전 동시성 제어: db가 객체의 여러버전을 함께 유지하는 것.
- 전체 트랜잭션에 대해 동일한 스냅숏을 사용한다.
일관된 스냅숏을 보는 가시성 규칙
- 아래 두 조건이 참이면 객체를 볼 수 있다.
- 읽기를 실행하는 트랜잭션이 시작한 시점에 ㅇ릭기 대상 객체를 생성한 트랜잭션이 이미 커밋된 상태였다.
- 읽기 대상 객체가 삭제된 것으로 표시되지 않았다. 또는 삭제된 것으로 표시했지만 읽기를 실행한 트랜잭션이 시작한 시점에 삭제 요청 트랜잭션이 아직 커밋되지 않았다.
색인과 스냅숏 격리
- 다중 데이터베이스에스 색인이 동작하는 방법
- 색인이 객체의 보든 버전을 가리키게 하고, 볼수 없는 버전을 걸러내는 것
반복 읽기와 혼란스러운 이름
- 스냅숏격리는 오라클에서는 직렬성 마이sql에서는 반복읽기 라고 한다.
- Sql 표준에 스냅숏 격리의 개념이 없기 때문이다.
- /??? 반복읽기 아는 사람 아무도 없다.
갱신 손실 방지
- Tr이동시에 시랭되면 발새앟ㄹ 수 있는 충돌
- 갱신손실(lost update) : 데이터가 Clobber;
원자적 쓰기 연산
- Cursor stability: 원자적 연산은 보통 독점적인 잠금을 획득해서 구현한다.
명시적인 잠금
- 충돌을 막기 위해 갱신시 명시적으로 잠그는 것
- 가령 두명으ㅢ 플레이어가 같은 객체를 동시에 잠그지 못하게 하는 것
갱신 손실 자동 감지
- 원자적 연산과 잠금은 read0modify0wirte 주기가 순차적으로 실행되도록 강제함으로 손실을 방지하는 방법
- 자동감지는 db에서 스냅숏 격리와 결합해 효율적으로 시행할 수 있다. 누락이 발견되면 tr을 어보트 시키는 것이다.
Compare-and-set
- 값을 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 허용함. 현재 값이 이전에 읽은 값과 일치하지 않으면 갱신은 반영되지않음
- 앞과 비교해서 맞아야 갱신이 가능하다는 뜻인듯~
- 최신 복사본이 하나만 있다고 가정한다.
쓰기 스큐와 팬텀
- 경쟁조건이 발생할 경우 발생할 수 있는 좀 더 미묘한 충돌을 보자!
- 데이터 오염을 방지하려면 경쟁조건을 방지해야한다.
쓰기 스큐를 특징짓기
- 한번에 여러명이 동시에 쓰기를 진행하여 조건을 맞추지 못할 때( 서비스에서 정의한 조건)
- 경쟁조건이 명백히 발생하였을 때
- 쿼리문을 통해 잠글 수 ㅣㅇㅆ다.
직렬성
- 다루기 까다로운 예시
- 격리수준은 이해하기 어렵고 데이터베이스마다 구현에 일관성이 없다.
- 거대한 앱에서 특정 격리수준에서 해당코드를 실행하는게 안전한지 알기 어렵다
- 경쟁조건을 감지하는데 도움이 되는 좋은 도구가 없다.
- 이럴 때, 직렬성 격리를 사용하라!
- 보통 가장 강력한 격리 수준이다.
- 동시성 없이 한번에 하나씩 직렬로 실행될 때와 같도록 보장한다.
- 모든 경쟁조건을 막는 것이다.
- 직렬성 격리의 기법
- 말그대로 순차적으로 실행하기
- 수십년동안 유일한 수단이었던 2단계 잠김
- 직렬성 스냅숏 격리 같은 낙관적 동시성제어 기법
실제적인 직렬 실행
- 동시성을 피하는법: 동시성을 완전히 제거한다.
- 비교적 최근에야 단일 스레드 실행이 가능하게 되었다.
- 두가지 발전이 생각을 바꿨다. (원래 동시 스레드 선호)
- 램 가격이 저렴해져서 많은 사용 사례에서 활성화된 데이터셋 전체를 메모리에 유지할 수 있을 정도가 됐다.
- Oltp 트랜젝션이 보통 짧고 실행하는 읽기와 쓰기의 개수가 적다는 것을 깨달았다.
- 이 방법은 볼트db/h-스토어, 레디스, 데이토믹에서 구현되어 있다.(단일스레드)
- 잠금을 코디네이션하는 오버헤드를 피할 수 있지만 cpu 코어 하나의 처리량으로 제한된다.
트랜잭션을 스토어드 프로시저 안에 캡슐화하기
- 초기에는 사용자 활동의 전체 흐름을 포함할 수 있게하려는 의도가 있었다.
- 사람을 기다리는 것은 비효율적이다(잠재적으로 많은 트랜잭션이 있음)
- 사람이 주요경로에서 제외되고 한번에 구문하나씩 실행하는 방식으로 되어왔다. 이것은 처리량이 끔찍하다.
- 때문에 단일 ㄹ스레드에서 트랜잭션을 순차적으로 처리하는 시스템들은 상호작용하는 다중구문을 허용하지 않는다. 대신 앱은 코드 전체를 스토어드 프로시저 형태로 db에 미리 제출해야한다.
스토어드 프로시저의 장단점
- 단점
- Db에서 실행되는 코드는 관리하기 어렵다.
- Db 벤더마다 제각각 스토어드 프로시저용 언어가 있다.
- Db는 성능에 민감해서, 잘못 작성된 스토어드 프로시저는 곤란한 상황을 만들 수 있다.
- 극복방법
- 범용 프로그래밍 언어를 사용한다.
- 스토어드 프로시저가 있고 데이터가 메모리에 저장된다면 모든 트랜잭션을 단일 스레드에 실행하는게 현실성이 있다. (좋은 처리량을 얻을 수 있다)
- 복제에도 사용되는 경우도 있다. (볼트db- 결정적이어야함(동일한결과))
파티셔닝
파티셔닝은 순서, 멀티스레드는 노순서
- 직렬성은 단일 cpu 코어의 속도로 제한된다.
- 특히 쓰기에서 성능이 병목이 될 수 있다.
- 여러 cpu 코어를 쓰기위해 데이터를 파티셔닝할 수도 있다.
- 코어별로 파티션을 할당해서 cpu 코어 개수에 맞춰 선형적으로 확장할 수 있다.
- 그런데 여러 파티션에 접근해야하는 tr이 있다면 모든 파티션에 걸쳐 코디네이션을 해야한다. 이는 느리다.
직렬 실행 요약
- 모든 트랜잭션은 작고 빨라야한다.
- 활성화된 데이터셋이 메모리에 적재될 수 있는 경우로 사용이 제한된다.
- 쓰기처리량이 단일 cpu로 처리할 수 있을 정도로 충분히 낮아야한다.
- 여러파티션을 쓸 수 있지만 제한이 있다.
2단계 잠금(2pl)
- 2pc 랑 다르다.
- 2단계잠금은 앞에 나왔던 더티쓰기를 막는 것과 유사하지만 훨씬 더 강하다.
- 잠금: 다른 쓰기가 완료될 때까지 대기(여전히 접근 가능)
- 쓰이는 동안 과거 데이터 읽기 가능
- 2pl: 쓰기 시 독점적인 접근 필요
- 쓰이는 동안 과거 데이터 읽기 불가능
- 잠금: 다른 쓰기가 완료될 때까지 대기(여전히 접근 가능)
- 2pl은 쓰기 뿐만 아니라 읽기도 막는다. 이는 스냅숏격리와 반대된다
2단계 잠금 구현
- 보통 2pl은 이노db에서 직렬성 격리 수준을 구현하는데 사용되고, db2에서는 반복읽기격리수준을 구현하는데 사용된다.
- 잠금은 공유모드나 독점모드로 사용될 수 있다.
- Tr이 읽기를원함 ⇒ 공유모드로 잠금 획득 ⇒ 여러명이 공유모드로 잠금하고 읽기는 허용되지만 독점모드로 잠금되어 있다면 읽을 수 없다.
- 쓰기를 원한다면 독점모드로 잠금을 획득해야한다.
- 읽다가 쓰면 독점으로 업그레이드
- 잠금을 로득한 후에는 종료될 때까지 잠금을 가지고 있어야한다
- 2단계의 의미: 1단계 ⇒ 잠금 획득 , 2단계 ⇒ 잠금 해제
- 잠금 수요가 겹치면 멈춘 상황이 쉽게 발생할 수 있다. 이런 상황을 교착상태라고 한다.(데드락)
2단계 잠금의 성능
- 성능이 약점이다. 완화된 격리 수준보다 처리량과 질의응답시간이 크게 나빠진다.
- 잠금 획득의 오버헤드 뿐만 아니라 동시성이 줄어들기 때문이다.
- 교착상태가 성능 저하를 부른다.
- (아래) 2단계로 팬텀 문제를 해결하는 방법
서술 잠금 predicate lock
- 직렬성 격리를 쓰는db는 팬텀을 막아야한다.
- 쿼리문의 where을 통해 겹치지 않는 조건을 찾아 팬텀을 막는다.
- 핵심아이디어는 서술잠금은 db에 아직 존재하지 않지만 미래에 추가될 수 있는 객체에도 적용이 가능하다.
색인 범위 잠금 index range locking, next-key-locking
- 서술 잠금은 잘 작동하지 않는다. 잠금이 많으면 조건에 부합하는 잠금을 확인하는데 시간이 걸린다. 그래서 색인범위 잠금을 쓴다.
- 검색 조건에 인덱싱이 붙은 칼럼이 붙는 것 , 팬텀과 쓰기스큐로부터 보호해주는 효과가 있다.
- 정밀하지 않지만 오버헤드가 훨씬 낮다.
- 적합한 색인이 없다면 테이블 전체에 공유색인을 잡는 방법도 있다.
직렬성 스냅숏 격리(ssi)
- 직렬성과 좋은 성능의 공존 방법!
- Serializable snapshot isolation
- 완전한 직렬성을 제공하지만 스냅숏 격리에 비해 약간의 성능손해만 있을 뿐.
비관적 동시성 제어 대 낙관적 동시성 제어
- 2pl: 비관적 동시성 제어 메커니즘
- 뭔가 잘못될 가능성이 있으면 뭔가를 하기 전에 상황이 다시 안전해질 때까지 기다리는게 낫다는 원칙을 기반으로 한다.
- 상호배제와 유사하다. Mutual exclusion
- 직렬 실행은 극단적으로 비관적이다.(무조건 독점잠금과 같다)
- 직렬성 스냅숏 격리: 낙관적 동시성 제어 기법
- 낙관적이란? 뭔가 위험한 상황이 발생할 가능성이 있을 때 트랜잭션을 막는 대신 모든 것 이 괜찮아질것이라는 희망을 갖고 계속 진행한다는 뜻이다
- 나쁘면 어보트
- 예비용량이 충분하고 경쟁이 너무 심하지 않으면 낙관적 동시성 제어 기법은 비관적 동시성 제어보다 성능이 좋다.
- 스냅숏 격리를 기반으로 한다.
뒤처진 전제에 기반한 결정
- 트랜잭션은 전제를 기반으로 동작한다. 커밋할 때 전ㄴ제가 변경되어 더이상 참이 아닐 수 있다.(가령 where문)
- 이런 쓰기가 유효하지 않을 수 있다고 가정한다.
오래된 mvcc 읽기 감지하기, 과거의 읽기에 영향을 미치는 쓰기 감지하기
- 스냅숏은 보통 다중 버전 동시성제어로 구현한다.
- 전제가 변경되는 것을 막으려면 db는 트랜잭션이 mvcc 가시성 규칙에 따라 다른 tr의 쓰기를 무시하는 경우를 추적해야한다. 커밋될 때까지 기다려야한다.
직렬성 스냅숏 격리의 성능
- 상충: 성능과 트랜잭션의 읽기 쓰기를 추적하는 세밀함의 정도
출처
반응형