시작하기 앞서
DynamoDB와 스패너, 두 서비스 다 꽤 오래전부터 기반이 있었고 상당히 성격이 다릅니다. Amazon DynamoDB의 최초 출시일은 2012년으로 꽤 오래된 서비스이고, NoSQL에 기반한 서비스로 유명한 반면, Google Spanner는 릴리즈는 비교적 최근이지만(2020년) 이론은 2012년에 출시된 NewSQL 기반의 서비스입니다. NewSQL이 뭐냐면, 기존의 RDB와 다르게 NoSQL과 같이 scalable 하면서, 동시에 RDB의 이점인 ACID transaction을 수행한다는 의미에서 가지고 온 용어입니다.
어쨌든, 이제와서 다루기에는 꽤 오래되었음과 동시에 성격이 전혀 다른 두 서비스를 붙여놓고 무슨 이야기를 하고 싶은가? 냐고 묻는다면, 먼저 여전히 두 서비스는 업계 선두를 달리고 있는 서비스임과 동시에, 두 서비스가 가지고 있는 아키텍쳐의 디테일을 서로 대조하면서 보다 깊이있게 알아보기 위해서입니다.
...라고 썼지만, 사실 깊이있게 잘 정리한 글은 도처에 많아서, 여기서는 서비스의 가장 큰 특징들과 두 서비스의 차이에 대해서, NoSQL과 RDB의 기본적인 성격
DynamoDB의 특징
클라우드 기술과 NoSQL이 성행하는 이제는 흔히 볼 수 있는 기술이지만, 당시에는 혁신적인 기술들이 많았습니다. 사실 일부 기술은 어느 정도의 자본도 필요한 걸 생각하면 흔히 볼 수 있다는 말도 애매할지도...
사실 이제 와서는 겹치는 기술들이 많아서 크게 쓸 부분은 없어 간단하게만 짚고 넘어갑니다. 아마 핵심 기술이라면 아래의 요소일 겁니다. 분산 시스템에서의 확장성과 속도를 최대화하면서 동시에 consistency를 최대한으로 챙기고자 하는 기술을 엿볼 수 있는 굉장히 좋은 내용들이 많습니다.
Eventual Consistency
CAP이론이 있습니다. 극도로 아주 간단히 말하면, 분산 시스템에서는 서버를 절대 죽지 않도록 하던가(P), 아니면 못 하겠다고 하거나(A)! 둘 중 하나 골라라.
라는 딜레마가 있습니다. DynamoDB는 과감하게 일단 하겠다고 말할 테니 돌아가~(C)
를 포기해서 이 문제를 해결합니다. 일단 결과를 뱉고 내부적으로는 어찌어찌 해결해 보겠다는 방식인거죠. 물론 그 동안 결과는 다르게 나올 수 있지만 궁극적으로는(eventually) 일관성이 지켜지게 됩니다.
덕분에 아주 강력한 성능을 보여줄 수 있습니다.
Vector Clock
이러한 궁극적 일관성을 위해서 “Vector Clock” 이라는 것을 사용합니다. 아래와 같이 여러 노드들에 값이 분할되어 쓰여져도(partitioned), 궁극적으로 가장 마지막에 쓰여진 값으로 통합(reconcile) 됩니다.
N, R, W
그리고 consistency를 너무 희생하는 건 좋은 생각이 아닐 수 있습니다. 제대로 된 값을 저장하고 얻으려고 DB 쓰는 건데 아무 값이나 되는 대로 뱉으면 안 되겠죠?
그래서 avability를 적당히 희생할만한, 내부적으로 적당한 기준을 주기로 합니다. 그게 바로 N, R, W 파라미터입니다. 이를 테면 (N=3, R=2, W=2) 라면, 노드 3개중 2개의 쓰기가 성공하면 일단 눈감아주는 거죠.
엣지 컴퓨팅
실제 데이터 서버의 리전을 기반으로, 샤딩/리플리케이션의 이점을 살리면서도 동시에 최소한의 응답시간을 보장할 수 있도록 하고 있습니다. 이건 아무래도 기존 NoSQL에서 미지원했다기보다는, 자본을 때려박을 수 있는 Amazon이라 가능했던 점이 클 듯.
가십 프로토콜
수천개의 아주 거대한 시스템을 지탱하기 위해서, 모든 시스템이 소통하는 방식이 아니라 일부 랜덤한 경로로 커뮤니케이션을 하도록 하여 커뮤니케이션으로 인한 부담을 훨씬 줄였습니다. 주로 membership과 outage 확인에 쓰였는데, 아주 빠르게 반응할 필요는 없는 정보지만 주기적으로 체크가 필수인 정보인 점에서 효율이 좋습니다.
확장성, 파티셔닝, 그리고 Consistent Hashing
기존의 기본적인 파티셔닝을 넘어서, 테이블 단위의 파티셔닝(보조 인덱스; GSI) 또한 지원하고 있습니다. 동시에 scability 또한 지원하고 있는데, 이 배경에는 consistent hashing이 있습니다.
consistent hashing은 일종의 Ring hashing으로 이루어집니다. 거대한 해시 ring으로 되어 있고, 그 안에 node들이 있는 모습을 상상할 수 있습니다.
하지만 처음에는 ring을 실제로 구성할 instance가 많지 않을 것이기 때문에, ring 내 node들은 중복된 instance 값들로 채워져 있게 됩니다. 이것을 “virtual node”라고 부르게 됩니다. 이후에 스케일을 키울 때는, 새로 instance를 만든 후 virtual node에 이를 채워 넣는 방식으로 새 instance를 추가할 수 있겠죠.
그리고 이렇게 구성된 hash ring에 값을 저장할 때는, 값(PK)을 hash 하여 그 뒤에 있는 몇 개의 node들에 replicate하는 방식으로 진행한다고 합니다. 여기에선 값 K의 해시값이 A~B 사이일 때, B/C/D가 해당 키를 복제하여 저장하는 역할을 하게 되고, 이 때 노드 B가 중재자가 됩니다. 따라서 각 노드가 모두 중재자가 될 수 있는 경우가 있고, 이는 SPOF(single point of failure)를 방지하고자 하는 설계이기도 합니다.
참고로, Consistent Hash Ring은 최대한 균등하게 보장하는 것이 (Strategy 3) 성능 상 제일 좋다고 합니다...
Hinted handoff 극복을 위한 복사 (Replication)
아무래도 새로 노드가 생기면 통째로 복사를 하면 끝이겠지만, 가끔은 일시적인 장애가 생겨서 잠깐 시스템이 outage 했다가 돌아올 수 있습니다. 이 경우 모든 데이터를 굳이 복사할 필요는 없을 테지만, 그러려면 어떤 데이터가 동기화되어야 하는지 알 방법이 있어야 합니다.
Dynamo는 Merkle tree 형태로 데이터를 관리하여 이러한 방법을 구현했습니다. 이는 leave들의 hash값을 묶어서 다시 hash하는 형태로 구성되어 있는데, 아주 간단히 말해서 해당 node와 그 아래 leave들의 데이터가 같으면 hash 값이 같을 것이기 때문에 이미 동기화 된 것으로 간주하고 복사할 필요가 없게 됩니다. 이를 통해서 dynamoDB는 복구에 드는 비용을 최소화한다고 합니다.
Spanner의 특징, 그리고 RDB와 어떻게 다른가
Spanner 구조는 정말로 복잡했습니다. 존 나누기는 물론이요, 똑똑하고 능동적인 샤딩을 위한 placement driver에, 스팬서버간에 걸친 트랜잭션을 위한 리더 선정까지...
그래도 개인적으로 Spanner을 읽으면서 꽤 재미있던 점이 바로 Zone과 Paxos, 그리고 TrueTime 개념이었습니다.
Zone과 Paxos
최근의 클라우드 컴퓨팅은 지리적 이점을 살리기 위한 엣지 컴퓨팅은 기본으로 들어가 있는 편이고, 스패너 또한 예외도 아닙니다. Zone은 물리적인 구성단위로서 그러한 이점을 살리기 위함이라고 볼 수 있을 것입니다.
반면 Paxos는 논리적 단위로서, 각종 데이터 센터에 산재해 있습니다. 이들의 replica를 일컫어서 Paxos group이라 부르고, 이 Paxos group에 대해서 sharding이 됩니다. 그리고 이를 기반으로 2PC commit 등의 작업을 수행하게 됩니다. 어떻게 보면 일종의 가십 프로토콜의 한 단위가 아닐까요?
이게 꽤 재밌는게, consistency를 위해 으레 leader을 선정해야 하고, 이 때문에 SPOF(single point of failure)이 발생하면 전체 시스템에 영향이 큰 경우가 많은데, 여기서는 소규모의 Paxos 단위에서 능동적으로 리더를 선출하여 SPOF이 가지는 위험성을 줄임과 동시에 2PC 커밋 같은 걸 수행하도록 하고 있습니다.
또 하나 재미있는 점은, 특정 Paxos의 부하가 심할 경우에는 능동적으로 directory(대충 데이터)를 다른 Paxos로 옮긴다는 것입니다.
논리적 단위와 물리적 단위를 최소한의 단위로 나눠서, 이를 기반으로 최적의 효용성을 발휘할 수 있는 설계를 했다는 점이 꽤 흥미로웠습니다.
TrueTime
보통 분산 시스템에서의 제약사항으로 CAP가 있다고들 이야기를 합니다. 특히 RDB에서는 P를 희생하여 CA만 보장하는 것이 최선으로 알려져 있지만, 분산 시스템에서는 사실상 이는 불가능합니다. 결국 어느 정도 Availability와 협상을 하는 수밖에 없습니다.
특이하게도 Spanner은 “대놓고” CP를 택했습니다. 동시에 Availability 희생을 최소화 하는 설계를 하여 최대한으로 CAP를 보장하였다고 이야기합니다.
이 비결은 구글의 일관적인 하드웨어 세팅에 달려 있다고 이야기 하지만, 제가 생각에는 이 트루타임이 핵심이 아닐까 싶습니다. 트루타임을 통한 병적일 정도의 시간 동기화를 통해서 트랜잭션이 동일한 타임 마킹을 할 수 있도록 하고, 동시에 이를 통해 트랜잭션의 버전매김 및 일관성, 그리고 커밋 순서또한 단일 시스템처럼 유지할 수 있게 됩니다. 그런데 이를 밑받침하기 위해서는 timestamp의 정확도가 굉장히 중요한 요소인데, 몇천개의 분산 시스템에서 이를 균일하게 유지하기란 솔직히 굉장히 어려운 일입니다. 이에 대해 높은 신뢰도(high availability)를 보장하지 않으면 위의 전제 자체가 모두 깨지니, consistency 자체를 보장하기 어렵겠죠.
이를 통해 극강의 분산시스템에서의 Consistency를 구현하고, 거기에 마구 스케일을 늘려서(P) availability를 떼우는 전략으로 간게 아닐까 조심스레 생각합니다! 덕분에 확장성 측면에서도 자유로운 모습입니다. 간단히 말하면, NoSQL 위에 RDB의 요소들을 얹어 기존에 없었던 Consistency를 보장하는 설계를 했다고도 볼 수 있을 것 같네요.
둘을 비교할 수 있을까?
좀 이상한 질문일 수 있지만, Spanner은 나름 NoSQL의 확장성과 기존 RDB의 기능성 및 ACID, 즉 두 마리 토끼를 잡고자 나온 제품입니다. 두 토끼를 잡기 위해 나왔다면, 어느 정도 비교도 가능하겠죠.
정량적 비교는 어렵지만, 몇 가지 비교해 볼 수 있는 요소들이 있습니다.
Query의 Latency 및 QPS (queries per second)
Latency의 경우 DynamoDB가 압도적으로 빠를 수밖에 없습니다. Eventually consistent 하기 때문에, 쿼리 수행이 끝날 때까지 기다려야 하는 Spanner이 도저히 이길 수가 없습니다. 그럼에도 불구하고 Spanner의 30ms 가량의 순수 latency는 그렇게 큰 요소는 아닙니다.
그리고 Spanner의 QPS는 read 기준 10000, write 기준 2000 으로 아주 강력한 수준입니다 #. 이는 transaction Lock 단위를 아주 세밀하게 걸음으로서, write 되는 행이 영향을 받는 경우가 아니라면 다른 write를 방해하지 않도록 되어 있기 때문입니다 #.
물론 이는 별도의 락 검사, 쿼리가 끝날 때까지 기다려야 하는 작업이 필요 없는 DynamoDB와 같은 NoSQL 에 비하면 또 높은 수치라고 보기는 어려울 수 있지만... 보통은 저 정도면 일반적인 트래픽을 감당하기에는 충분할 것이고, 언제나 그렇듯이 더 높은 극한의 성능이 필요할 경우 스키마와 파티셔닝을 등을 통한 쿼리 최적화를 하면 되니까요... RDB가 항상 그렇죠 뭐 ... 😭
Extensibility
사실 둘 다 완벽한 분산 시스템 기반이기 때문에, 둘을 비교하는 상황에서는 확장성에서는 큰 차이가 없습니다.
다만 Spanner의 경우, RDB 대비해서는 이게 가장 큰 장점일 것입니다. 기존의 RDB도 clustering이 가능은 했지만, 상당히 많은 제약사항이 있었습니다. MySQL의 경우 replication 방식만 지원하기도 했고 동기되는 동안 데이터 불일치가 발생하는 문제가 있고, RDB 분야의 최강자 Oracle RAC도 파티션 기반 해싱만 지원하고 있어서 확장성이 자유롭지는 않습니다. ACID를 충족하면서 실시간으로 확장하기란 사실상 불가능하다고 볼 수 있습니다. RDB의 실시간 확장성에 대한 한계점을 해소했다는 것 자체가 독보적인 강점일 것입니다.
Consistency와 Availability
NoSQL 기반의 DynamoDB는 Consistency를 희생했다고 잘 알려져 있지만, N-R-W와 같이 어느 정도의 일관성에 대한 기준을 가지고는 있습니다. Spanner의 경우는 여기에서 더 나아가 매우 강력한 Consistency를 기반으로 Availability를 희생하는 방식인 것을 확인했고요. 이런 걸 보면 이 둘은 어느 정도 연속적인 상호 교환 관계에 있다는 생각이 듭니다.
다만, 어느 한 쪽이 완벽해지기 위해선 다른 점에서 그만큼 희생을 꼭 해야 하는 것은 아닐 것입니다. 비록 CPA를 완벽히 충족할 순 없더라도, 어떻게 설계하느냐에 따라서 완벽에 가까운 모습이 될 수 있음은 확실해 보입니다. Spanner의 경우에서처럼 요.
참고
'개발 > Engineering' 카테고리의 다른 글
AWS가 이야기하는 Well-Architected Framework (0) | 2022.05.06 |
---|---|
리눅스 커널 바로 위에 executable 얹기 (0) | 2022.05.04 |
왜 ElasticSearch 인가? (0) | 2022.04.30 |
문자열에 대한 철학 — Go, Rust (0) | 2022.04.28 |
Go의 메모리 과다 사용 문제 해결해보기 (0) | 2022.04.26 |