DynamoDB: Building NoSQL Database-Driven Applications 노트
발생일: 2021.02.16
키워드: DynamoDB, 다이나모디비
문제:
DynamoDB에 코세라 강좌가 있어 들으면서 정리했던 노트다.
https://www.coursera.org/learn/dynamodb-nosql-database-driven-apps
지금보면 너무 기초적인 것 같은데, 당시 흐릿하게 개념이 잡혔던 터라 전체 컨셉을 이해하기에 좋았다.
지금 보더라도 유심히 기억해야할 만한 특징인 볼드로 표시해뒀다.
내용:
Relational Databases and the Problem that Need Solving
- RDS(Relational Data System)의 특징
- 정형 데이터를 분석하기 좋음
- 스토리지를 많이 차지하고
- 컴퓨팅 비용이 큼
- CAP 정리: 다음과 같은 세 가지 조건을 모두 만족하는 분산 컴퓨터 시스템이 존재하지 않음을 증명한 정리
- 일관성(Consistency): 모든 노드가 같은 순간에 같은 데이터를 볼 수 있다.
- 가용성(Availability): 모든 요청이 성공 또는 실패 결과를 반환할 수 있다.
- 분할내성(Partition tolerance): 메시지 전달이 실패하거나 시스템 일부가 망가져도 시스템이 계속 동작할 수 있다.
- Partiton Tolerance 는 시스템에서 보장해야 함. 그래서 C 와 A 중 둘 중 하나를 선택해야 함.
- NoSQL 이 유리하지 않은 경우
- 피크를 예상할 수 있는 self-managed 환경. 수평 확장이 항상 옵션은 아님
- 정형 데이터로만 구성된 경우
- 여러 테이블의 조인이 필요한 경우. NoSQL은 cross-table 관계가 없음. 조인을 할 수가 없음
- NoSQL 디비 타입
- Key-Value
- key-value 기반으로 키 조합으로 데이터를 가져올 수 있음
- DynamoDB
- 예: 유저 세션 데이터 저장하기에 적합
- Column-based
- 많은 데이터를 빨리 가져오는데 유리함
- Cassandra, HBase
- Document-based
- tagged elements로 저장
- MongoDB
- DynamoDB
- Managed System
- read/write capacity units 을 조정해서 성능 레벨을 관리할 수 있음
- 스트림, 트랜잭션, 액셀러레이터, 글로벌 테이블 등의 기능 제공
- low latency, redundant storage, managed durability, fault tolerance
- 1일 10조 콜, 초당 2000만 콜 처리 가능
- TTL(Time To Live) 설정 가능
DynamoDB
- 테이블
- primary key = partition key + (sort key)
- primary key가 중복되면 안됨
- attirbutes
- read request 의 consistent
- strongly: 무거움
- eventually
- 제한
- 한 레코드는 400KB 제한
- 더 큰 건 S3에 저장해야 함
- key 길이
- Partition Key: 1byte 이상, 2048 byte 이하
- Sort key: 1byte ~ 1024byte
테이블 생성
- Partition Key와 Sort Key 조합이 Unique 하면 됨
- 테이블 목록
- $ aws dynamodb list-tables
- 아이템 추가
- $ aws dynamodb put-item --table-name Music --item file://song1.json
- 조회
- $ aws dynamodb query --table-name Music --key-condition-expression "Artist = :v1 AND SongTiItle = :v2" --expression-attribute-values file://values1.json
- v1, v2 = 변수
- "S": "Rock" 의 의미 = 타입: 값
- pk 와 sk 를 제외한 다른 속성은 쿼리(query)할 수 없음
- 삭제
- $ aws dynamodb delete-item --table-name Music --key file://key.json
- { "Artist": { "S": "David Bowie" }, "SongTitle": { "S": "Changes" } }
Query and Scan
- Query: PK 또는 PK + SK로 조회
- 정렬키만 있으면 조회
- Scan: 테이블의 전체 테이블 조회
- $ aws dynamodb scan --table-name Music
- 필터: 속성으로 조회해야 하는데, 키가 아니라서 쿼리를 할 수 없는 경우
- key-condition 과 동일하게 --filter-expression 을 쓰면 됨
- Scan + Filter 로 키가 아닌 걸 조회할 수 있으나,
- scan 은 전체 테이블을 읽는 것이므로 주의해야 함
Secondary Index
- 다른 속성을 키로 갖는 테이블 복제본을 만드는 것
- Local secondary index: 다른 정렬키를 생성
- Global secondary index: 다른 파티션키와 정렬키를 생성
백업
- on-demand backup
- 람다나,
- AWS Backup 으로 스케쥴링 가능
- point-in-time recovery
- 자동 백업
- 35일 이내 복원 가능
- 비동기, 성능 저하 없음
- 복원할 땐 read/write capacity가 동일해야 함
- 복원 시간은 상황에 따라 다름
- $ aws dynamodb create-backup --table-name Music --backup-name MusicBackup
- $ aws dynamodb describe-backup --backup-arn [BackupArn]
- $ aws dynamodb restore-table-from-backup --target-table-name MusicRestored --backup-arn [BackupArn]
스캔
- $ aws dynamodb scan --table-name Music --max-items 3
- 최대 1MB 까지 가져옴
- last process key 로 다음 배치를 가져올 수 있음
- 페이지네이션처럼 동작함
- BatchWriteItem
- 한 번에 여러 테이블에 여러 레코드를 쓸 수 있음
- 추가하거나 삭제할 수 있음. 업데이트는 안됨
- 요청 중에 하나가 실패하더라도 나머지 항목은 실행됨
- 실패된 아이템은 unprocessed items 응답으로 내려옴
- { 테이블명: [ { 커맨드: { ... }, 커맨드: { ... }] }
- $ aws dynamodb batch-write-item --request-items file://batch.json
- BatchGetItem
- 한 번에 여러 테이블에서 여러 레코드를 가져올 수 있음
- 기본적으로 eventually consistent 임
- 필요하다면 strongly consistent 를 테이블 단위로 설정할 수 있음
- $ aws dynamodb batch-get-items --request-items file://...
- { 테이블명: { Keys: [ ... ] } }
- 특정 테이블이 실패했다면 unprocessed keys 응답에 있음
- BatchWriteItem 과 BatchGetItem 실패 시,
- unprocessed 응답을 보고 retry 하면 됨
- scan 시 주의
- 전체 데이터를 훑음
- 1메가 단위로 끊어서 읽음. LastEvalueatedKey 로 나머지 이어서 가져올 수 있음
- 필터가 적용되어 있으면, 읽어온 데이터에 값이 없을 수도 있음 (마지막이 아니더라도)
- 옵션
- ExpressionAttributeNames
- 속성 키가 예약어와 겹치는 등의 이유로 다른 이름으로 매핑하려는 경우
- # 캐릭터를 씀
- "#family": "family"
- ProjectionExpression
- 필요한 속성만 가져오려는 경우
- 예: "dragon_name, #family, protection, damage, description"
- Segment 나 TotalSegments 로 병렬 처리 가능 (가이드)
모니터링
- CloudTrail
- 테이블 액션, role, service 모니터링
- 이벤트 로그를 볼 수 있음
- CloudWatch Logs
- 알람 설정 가능
- API에 따라 처리할 수 있음
- X-Ray
- 바틀넥이나 에러 처리할 때 쓸 수 있음
- CloudWatch Metric
- 각종 지표
- RCU, WCU 볼 수 있음
- 1 RCU = 1 strongly consistent read = 2 eventually consistent read (4KB 이내, 초당 처리 기준)
- 1 WCU = 1 write (1KB 이내, 초당 처리 기준)
- dynamodb provisioned capacity 테이블을 보면 정보가 있음
- 메트릭스 상세: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/metrics-dimensions.html
- RCU, WCU 도 파티션에 따라 분산됨
- 예: 5000RCU 가 있고 파티션이 5개이면, 한 파티션에 1000RCU 씩 분산됨
RCU, WCU가 적절한 것 같은데도 퍼포먼스가 제대로 안나올 땐? (프로비전인 경우)
- CloudWatch에서 capacity 관련 메트릭스 확인
- 테이블에서 시간당 쓰인 RCU, WCU를 볼 수 있음 (글로벌 인덱스나 세컨더리 인덱스도)
- 메트릭스에서 설정한 것보다 더 많이 쓰였다면 올려줘야 함
- 알람 맞춰두면 좋음
- RCU와 WCU도 오토스케일링 정책을 적용해서 처리할 수 있음
- 그래도 병목이 있으면?
- 키 스키마 관련 이슈일 수 있음 (파티션 키 최적화가 필요함)
- large scan 하지 않게 쿼리 최적화가 필요함
파티션키와 최적화
- 테이블을 생성하면 데이터는 파티션에 저장됨
- 파티션은 스토리지 단위이고 물리적 공간(SSD)에 저장됨
- 멀티 리전에 자동으로 복제됨
- 테이블 생성하면 처리량(RCU와 WCU)에 맞춰 적당한 개수의 파티션을 생성함
- 테이블이 커지면 파티션이 추가됨
파티션키
- 모든 테이블은 파티션키가 있음
- 파티션키만 있으면
- 파티션키에 해시함수를 적용한 값으로 파티션 구분
- 정렬키가 같이 있으면,
- 파티션키에 해시함수를 적용한 값으로 파티션을 구분하고
- 여기에서 sort key 로 데이터를 정렬함
- 키 스키마 디자인할 때
- 파티션키가 저장 위치를 결정함
- 모든 파티션에 골고루 분산되게 파티션키를 디자인해야 함
- RCU, WCU를 직접 설정한 경우 (on-demand 는 해당 안됨)
- 400RCU가 있고 파티션이 4개라면, 각 파티션은 100RCU를 가짐
- 200RCU 만큼의 요청이 들어왔을 때, 파티션키가 골고루 분산되어 있다면 각 파티션은 50RCU 씩 처리함
- 만약 한 파티션에 200RCU만큼의 요청이 몰린다면(hot-partition), 처리량을 넘어서게 됨 (provision throughput exceeded exception 발생)
- 즉, 전체 RCU를 초과하지 않았는데도 에러가 발생하게 됨
- 핫파티션이 생기지 않게 파티션키를 디자인해야 함
- adaptive capacity 기능
- 핫파티션이 생겼을 때, 사용되지 않은 쓰루풋을 핫파티션이 사용하게 하는 옵션
- 기본 적용되어 있음 (아마도 2019년부터?)
- burst capacity 기능
- 5분 동안 사용하지 않는 용량을 남겨두었다가 초과분에 사용함
베스트 프랙티스. 파티션 키 디자인
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-uniform-load.html
- 골고루 분산되는 속성을 파티션키로 설정해야 함
- 좋은 예
- 사용자가 많은 경우, 사용자 아이디
- 디바이스가 비슷한 빈도로 접속하는 경우, 디바이스 아이디
- 나쁜 예
- HTTP 상태 코드: 200이 대부분을 차지하기 때문
- 날짜 단위의 아이템 생성 날짜: 요청이 한 곳에 몰림
- 예를 들어, 파티션키를 생성 날짜, 정렬키를 아이템 아이디로 만들면,
- 오늘 온 요청(파티션키 = '2021-02-18')은 한 파티션(물리적 공간)에 몰리게 됨
- 마찬가지로, 파티션키 종류가 적은 경우엔 분산되는 속성을 파티션키로 하는 게 나음
다른 우회 방법
- 날짜를 꼭 파티션키로 써야하는 경우, 날짜 뒤에 랜덤한 값을 붙여서 골고루 분산되게 할 수 있음
- 2021-02-18.123 처럼, 0~200 사이의 값을 넣는 것
- 근데 이렇게 하면 쿼리로 해당 날짜의 데이터를 조회할 수 없음
- 0~200까지 조합해서 다 합해야 함
- 조회할 때, 날짜 + 주문 아이디 조합 같은 경우라면, 주문 아이디 기반의 난수를 생성해서 골고루 분산되게 하는 방법도 있음
- 2021-02-18 에 주문번호 10423번이면, 10 % 100 을 적용해서, 2021-02-18.423 처럼 하는 방법
- 시계열 처리가 필요한 경우,
- 하루에 한 개의 테이블을 생성하고, 해당 테이블의 RCU/WCU을 고정함
- 파티션키는 날짜, 정렬키를 시간으로 함
- 하루에 한 테이블에만 처리가 몰리도록 해서 초과되지 않게 관리함
보안
- 암호화가 필요한 데이터(개인정보)
- 기본적으로 암호화됨
- 암호화 키로 별도로 암호화할 수 있음 (암호화 적용해도 성능 영향 거의 없음)
- EncryptionClient
- 아이템별로 보안 적용할 수 있음 (사이닝)
- 예: 정부의 개인정보에 맞추기 위해
- 전화번호 같은 것들이 암호화돼서 저장됨
- client 수준의 암호화도 있음
TTL
- 성능에 영향 없음
- Manage TTL 에서 켜주면 됨
- 아이템에서 TTL 대상 속성을 정해주면 됨
- 세션 구현 예제
- { PK: username, SK: sessionId, TTL: expireTime }
- TTL 적용 권한에 IAM을 설정할 수 있음
- 백업할 때에도 적용됨
- 만료된 아이템은 48시간 이내에 삭제됨
- 만료되었는데 삭제되지 않은 경우, 쿼리, 스캔 결과에 나올 수 있음?
글로벌 테이블
- 여러 리전 간 리플리케이션 제공
- 다이나모디비 스트림으로 리얼 타임으로 전달
- 각 리전 간 동일한 스킴의 테이블을 만들고, 글로벌 테이블로 묶으면 됨
- eventually consistent 로 동기화됨
스트림
- time-ordered sequence
- 24시간 동안 저장됨
- 스트림 이벤트를 받아서 처리할 수 있음
- 스트림 데이터는 별도로 저장됨
- 다이나모디비 스트림에 액세스가 필요함
- 프라이머리키 정보가 전달됨
- ES 인덱스 갱신 예
Optimistic Locking
- 동시 업데이트 이슈
- 다이나모디비는 락킹을 지원하지 않음
- Optimistic Locking
- 작업 중인 항목이 마지막으로 읽은 이후 덮어씌워지지 않은지 확인하는 전략
- conditional write 로 구현
- PutItem, UpdateItem, DeleteItem 메서드의 옵션으로 지원
- ConditionExpression 파라미터
- 예:
- 키가 없을 때에만 허용
- 속성이 특정 값인 경우에만 허용
- 버전 넘버로 관리하는 예
- 아이템에 버전을 명시하는 속성을 별도로 둠
- 업데이트 전에 아이템의 버전을 가져옴
- 업데이트할 때, 동일한 버전일 때만 업데이트
- 버전이 바뀌었다면 실패. 성공할 때까지 다시 수행
LSI
- 파티션 내에서 새로운 인덱스를 만듬
- LSI는 파티션에 대해서 고유할 필요가 없음
- consistent 를 선택해서 조회할 수 있음
- 테이블 생성할 때만 추가됨
GSI
- 파티션 키로 새로 정렬해야 할 때 사용
- 비동기로 업데이트됨 (eventually consistent)
- GSI 를 위한 RCU/WCU를 설정해야 함
보조키
- 사용하되 최소로 유지할 것
- 인덱스 사이즈를 최대한 작게 유지할 것
- project item
- 자주 사용되는 쿼리인 경우, 모든 속성을 project 할 것
- LSI의 projected attribute 에 없는 경우, 다이나모디비가 전체 테이블을 스캔함
싱글 테이블
- 연관된 데이터를 하나의 테이블에 넣는 것을 추천
- 단, 싱글테이블로 만들 땐, 테이블을 만들기 전에 쿼리를 설계해야 함
- 특히, LSI 는 테이블 생성할 때만 할 수 있기 때문이기도 함
- 먼저 관계 모델을 생성한 후에 싱글 테이블로 설계
- 성능과 쿼리를 위한 것임
- 쿼리를 먼저 생성
음원 테이블 설계 예:
- 작가, 노래, 앨범 관계 모델을 설계
- 쿼리를 설계
- 아티스트별 곡 찾기
- 장르별 앨범 찾기
- 연도별 아티스트의 앨범 찾기
- 노래 이름으로 찾기
- 쿼리를 설계
- 아티스트별 곡 찾기: 'PK = 아티스트'로 조회
- 장르별 앨범 찾기: 장르를 GSI로 만들어서 조회
- 연도별 아티스트의 앨범 찾기: LSI로 출시연도를 설정해서 조회
- 노래 이름으로 찾기: 노래 이름을 GSI로 만들어서 조회
- 아티스트 이름을 변경하려고 한다면?
- 데이터가 여러 번 중복되게 됨. 데이터를 비정규화하기 때문.
- 한 번에 업데이트하기 위해선 트랜잭션이 필요함
- 한 번의 오퍼레이션으로 처리할 수 있음
시계열 데이터
- 한 시간 단위에 하나의 테이블을 두는 게 나음
- 스캐닝이 자주 필요한 경우가 특히 그럼 (예: 분기별 집계)
- 아니면 AWS TimeStream 을 써도 됨
싱글 테이블을 만들려고 너무 무리하진 말 것
- 드래곤 카드 게임 서비스라면, 아래 두 테이블은 분리하는 것이 좋음
- 드래곤 정보 테이블
- 게임 정보 테이블
DAX
- 한자리수 밀리세컨드
- 마이크로세컨드로 원하는 경우. in-memory 캐시
- DAX 액셀러레이터
- 인메모리 캐시임
- API 호환됨
- 테이블 대신 클러스터만 보면 됨
- 쓰기보다 리드가 훨씬 큰 경우 유리함
- 작은 아이템이 다른 아이템보다 많이 조회되는 경우
- 예: 쇼핑몰의 오늘의 인기 아이템
- 별도 비용 있음
논의:
이후에 다이나모디비를 적용하면서 헷갈렸던 개념들을 정리해둠