backlink: https://loghub.me/series/ESukmean/스프링으로-간단한-웹-프로젝트-돌려보기/2
backlink에서 파생된 글이며, 조금 난이도가 있습니다.
본인이 여러 리전에 DB를 두는 "분산 DB"가 필요한지를 봐야 한다. 안정성이 요구 조건이라면 분산 DB를 가자.
분산 DB중에서 Strong Consistency를 지원하는 DB는 몇 없다. Cockroach DB의 경우 분산 DB임에도 Strong Consistency를 보장하는 대신 꽤 느리다. 체감으로 1 commit에 수백ms가 걸리는 느낌이 들었다. Strong Consistency가 필요 없다면 좋아보이는 것을 하나 골라 잡으면 된다.
분산 DB + 강한 일관성 보장: Cockroach DB
분산 DB면 됨: 시중에 있는 여러 많은 DB
굳이 분산 DB를 쓸 필요 없다면 적당한 RDB를 사용해도 된다. 10ms 미만의 지역 내에서 여러 IDC를 두고 Replication을 구성하면 상당한 안정성을 보장 받을 수 있다. 부산-서울도 10ms보다 덜 걸린다. 10ms 정도에서는 Replication로만 구성해도 무방하다.
대부분 MariaDB 또는 PostgreSQL를 선택 할 것이다. 필자는 또이또이 하다고 생각한다. 다만, PostgreSQL이 조금 더 Enterprise 느낌이다. 그래서, 작은것을 만들때는 MySQL(=MariaDB)가 편하다. 대신, 복잡한 권한 제어가 필요하다거나, 하나의 인스턴스에 여러 DB 넣는등의 일을 해야한다면 PostgreSQL가 더 나을 것이다.
중/소 규모: MySQL
복잡한 시스템(ORDBMS): PostgreSQL
Multi-Writer가 필요하면 Galera Cluster (MySQL)
그냥 replication이 필요하면 Patroni
필자 경험상 Galera쪽은 운영하다가 터지면 복구하는데 한세월 걸렸다. Patroni는 그래도 알아서 잘 된다.
만약 내가 쓸 데이터가 JSON 또는 그러한 동적 데이터면 NoSQL을 쓸 수 있다. 단, NoSQL 계열은 Transaction이 빈약하다는 말이 많다. 또한, schema (형태)를 잘 관리하지 않으면 오히려 나중에 필드 관리에 시간을 쓸 수도 있다. 필드명이 중구난방이면 나중에 mapping 할 때 힘들 수 있기 때문이다. 이 점을 유의해서 DB를 선택하자.
비정형화 된 데이터: NoSQL (MongoDB 같은것들)
DB도 필요없이 Key-Value 스토리지만으로 된다면 etcd도 괜찮다. 다만, no space alarm에 대비해서 space quota와 defrag, history retention 을 반드시 알고 가자
K-V로도 충분하면 etcd
국내 기준으로 수년전만 해도 중소 규모의 서비스에서는 MySQL, 윗동네에서는 Oracle만을 사용했다. 그러다가 NoSQL (MongoDB 등) 열풍이 한번 불었다. 요즈음에는 Supabase 및 해외파들을 중심으로 PostgreSQL이 퍼지더니, 올해 필자도 PostgreSQL을 사용해봤다.
각 DBMS는 지향하는 목표와 지원하는 기능이 조금씩 다르다. 모든 기능을 개발자가 직접 다룰 수 있으면 숨겨진 성능을 다 쓸 수 있을 것이다. 하지만 현실적으로 그건 힘들지 않은가.
이 글에서도 큰 개념만 다뤄보고자 한다. 다행히도 우리는 Spring 관점에서 쓸 것이므로 큰 문제가 되진 않을 것이다.
우선 DB를 선택하기 전에 "어떻게 사용할 것인지"를 정해야 한다.
"전자"를 고르면 고를수록 필요한 금액이 올라간다. 예를 들어, 장애를 줄이려면 서버를 3개 이상 배치하는것이 필요하다.
만약 모두가 "후자"라면 적당히 유명한 DB를 사용하면 된다. 본인의 숙련도에 따라 PostgreSQL, MySQL, MongoDB, influxDB, clickhouse 중 편한 것을 사용하면 된다.
하지만, 그렇지 않다면 목적에 따라서 CAP 이론과 Eventual Consistency, Strong Consistency를 동시에 고려해서 가장 알맞는 것을 선택하면 된다.
두가지 개념이 동시에 나오므로 돌려깎기 스타일로 글을 서술한다.
아래의 글은 분산 DB 개념까지 들어간다. 그러므로, 단일 서버로만 운영할 경우에는 적당히 본인이 편한걸 골라 쓰자.
내장애성을 가지려면 여러대의 서버를 운영해야 한다. 운영중인 서버가 3대면 하나가 죽어도 2대로 이어나갈 수 있다.
전세계에 100개의 DB가 있다면, 49대 정도가 고장나도 "이론적으로는" 서비스를 이어나갈 수 있다.
처리량 증가를 위해서도 서버 추가가 필요할 수 있다. Disk I/O, 네트워크 대역폭, CPU 사용량이 어느 수준에 이르게 되면 서버를 확장해야 한다.
처음에는 수직 확장이 편하다. 하지만 그것이 계속되면 끝끝내 수평 확장을 해야하는 시점이 온다. 데이터 읽기의 문제라면 Replication을 통해 어떻게 분산을 할 수도 있지만, 데이터 업데이트·쓰기 단에서 병목이 걸리면 답이 없다.
여러 노드를 운영하다 보면 몇가지 문제가 발생한다. 그때 무엇을 포기하고 무엇을 챙겨갈 것인지를 정해야 한다.
조금 자세하게는 https://blog.esukmean.com/2024/분산-시스템의-기초-개론-1/ 를 참고하자
우선 CAP란, C(Consistency) / A(Availability) / P(Partition Tolerance) 의 첫글자를 하나씩 따온 이론이다. 그리고 아래 3가지를 모두 챙기는것은 불가능하며, 오직 2개 만을 고를 수 있다고 한다.
Consistency: 모든 노드가 늘 항상 최신의 데이터를 읽을 수 있다는 것을 의미함Availability: 일부 노드에 장애가 발생해도 데이터를 처리(읽고·쓰기) 할 수 있다는 것을 의미함. (ᅟ완전 최신 자료가 아닐지라도 아는 선에서 데이터를 보내줌)Partition Tolerance: 네트워크가 분할돼도 계속해서 처리할 수 있는 것을 의미함Oracle Cloud 리전의 서버 자체는 켜져있는데, 외부 네트워크가 불안정하다고 하자. 그러면 해당 리전에 있는 서버들은 자체적으로라도 DB가 운영 돼야 하는가?
Partition Tolerance + Availability: Oracle Cloud 내의 DB끼리 알아서 움직여도 된다. 나중에 망이 연결됐을때 업데이트를 전파한다. (K-V 스토리지 급에서만 가능한 전략이다. hybrid-lamport clock 참고)Partition Tolerance + Consistency : Oracle Cloud쪽의 DB는 오프라인 처리한다. 그리고 Oracle Cloud쪽 스스로도 문제가 있다고 응답을 거부한다. 대신 나머지 노드들에서 Quorum을 구성해서 서비스를 이어나간다.Consistency + Availability: 애초에 멀티리전을 운영하지 않는다. (하나의 IDC 내에서만 운영해서 split-brain 같은것은 애초에 고려치 않는다.)그러면 다들 CP를 쓰면 되는거 아닌가? 묻는다면... 조금 복잡하다. Partition Tolerance에 Consistency를 끼워넣기 위해서는 성능, 특히 쓰기 latency가 꽤 많이 깎여 나간다. Quorum에 쓰기를 올리고, 각 노드들에 paxos, raft를 써야한다. 일관성 보장을 더 강하게 하려면 Clock-Bound wait 까지도 들어와야 한다.
개인적 경험으로 raft 쓴다는곳 치고 빠른곳을 못봤다. 요청 하나에 수십ms가 걸리더라.
Strong Consistency는 일반적으로 생각하는 DB의 모습이다. DB에 데이터를 넣거나 수정하면 모든 곳에서 동시에 데이터가 바뀐다.
하지만 안정성을 보장하기 위해서는 상술했던 일부 서버가 죽었을때의 문제를 해결해야 한다. 아마 왠만한 상황에서 CP를 쓰게 될 것이다. 그리고 CP가 합리적인것 처럼 보인다. 그런데 CP면 왜 성능이 느릴까? 이것을 위해서 Eventual Consistency와 Strong Consistency을 알아야 한다.
Eventual Consistency는 commit한 데이터가 옆 노드에서 "바로 보일것"이라는 보장이 없다. commit 단계에 동기화가 없으므로 쓰기 속도가 빠르다. Strong Consistency은 commit 한 데이터가 옆 노드에서 바로 보일것이라고 보장한다. 즉, commit 단계에 동기화가 포함된다.
이해하기 쉽게 각 이론·알고리즘이 나온 순서를 서술한다.
편의를 위해 서버가 5대 있다고 하자. 그리고 2대가 고장날 예정이다.
사용자는 원래 통장에 1억원이 있다. 그리고 1억을 방금 출금한 상태이다.
update 요청이 들어왔다. 해당 서버 내에서 commit이 된 순간 서버가 죽어버렸다.
수정된 데이터가 전파되지 못한채 죽어버렸기 때문에, 서버 복구 시점까지 해당 데이터가 보이지 않는다.
-> 사용자가 통장에서 1억을 출금했지만, 다른 서버 입장에선 통장에는 1억이 아직 남아있다. 여기서 사용자가 15만원을 더 쓴다면? 추후 정합성 문제가 발생할 것이다.uncommited log entries)여기서 async는 Eventual Consistency이다. 대게 쓰기만 Leader에서 하고, 읽기는 Follower에서 한다. 이때, Leader에서 Follower로의 데이터 전파가 async이기에 실질적으로 Eventual Consistency이다. 일반적인 patroni나 async 기반 분산 DB가 이 방법을 사용한다.
Eventual Consistency에서는 read-after-write 문제가 발생할 수도 있다. 옆노드에 반영이 되지 않은 찰나에 읽기 요청이 들어갈 수 있기 때문이다. 일부 서버가 죽어도 원할히 동작한다. Async Replication은 본인의 WAL에 넣어지기만 하면 되기 때문에 비교적 반응 속도도 빠르다. 트레이드 오프로 commit을 전파 하기 전에 죽으면 답이 없다.
Patroni의 기본값이 async streaming이다.
Strong Consistency는 commit 단계에 Follower들에게 데이터를 전파까지 포함된다. Two-phase commit에 의해 Accept(prepare phase)된 데이터는 commit 데이터에 준해진다. 이 상황에서 Leader DB 서버가 죽으면 새로운 leader가 선출될 때 까지 서비스 응답 자체가 불가하다. 멀티 Writer이면 조금 더 복잡하지만, 개념 자체는 그렇다.
Strong Consistency에서도 Read-After-write 문제를 해결하기 위해 모든 follower에게 데이터를 전파 할 수도 있다. 이 경우 추가적인 동기화 비용이 발생한다. 돈이 오가는 쪽이나 강한 트랜잭션이 필요한 곳에서는 모든 노드에 전파가 돼야 commit 처리를 해주는 경우도 종종 있다.
위의 내용은 어디까지나 Partition Tolerance이 없는 상황이다. 다시 돌려깎기 위해 Partition Tolerance를 고려해 보자. 이 경우에는 Quorum이 필요하다.
한국에서는 위원회로 번역을 하던데, 필자는 늬앙스로 합의체가 맞다고 생각한다.
각 리전에서 Write를 시도한다. (multi-writer) 그러면 각 DB가 Quorum에 쓰기 정보를 보낸다.(prepare phase) 그러면 Quorum이 합의를 해서 괜찮으면 승인을 한다.
이제 진짜 Multi-writer이기에 순서를 지키는것과 정합성을 유지하는 것이 필요하다. 아래 상황을 생각해 보자
이때 4번 또는 5번에서 문제가 생기면 어떻게 될까? Quorum 내부의 uncommitted entry에 데이터가 남을 것이다. 그리고 지정한 row에 접근하려 하면 lock에 의해 접근이 제한 될 것이다. 해당 서버가 복구 되거나 Quorum이 서버가 죽었다고 생각하고 uncommitted entry를 복원할 때 까지 락이 걸릴 것이다. 그래서 서버가 죽었는지를 계속해서 파악해야 한다.
최근의 분산 DB는 shard 데이터 자체를 분할해서 보관하는 경우도 많다. 이 경우에는 사실상 여러 DB 인스턴스간의 트랜잭션 연계가 필요하다.
분산·분할의 정도가 높아질 수록 성능 비용과 Latency가 많이 발생하게 된다.
그러면 Patroni는 어떤 방식으로 동기화를 할까? patroni의 사례를 참고해서 실제 예시를 보자. patorni는 비동기(async), 동기(sync), 강한 동기 (strict sync)를 지원한다.
각 트랜잭션은 접속된 PSQL 서버에서 Commit 된다.
Commit 된 데이터가(WAL) 비동기적으로 다른 node들에게 전파됨. 그러므로, Commit된 데이터가 다른 서버에 복사가 됐다는것은 보장할 수 없음. 그러므로 완전한 자료 보존은 보장되지 않음.
예시: 1) Commit이 되자마자 네트워크가 단절됐다면 다른 서버에 데이터가 동기화 되지 못함 2) 네트워크가 단절된 순간부터 들어오는 모든 트랜잭션은 동기화 되지 않음 (forked timeline)
그러나, 비동기적 특성 때문에 장애 후 붙는것 자체는 자동으로 이루어짐.
각 트랜잭션은 다른 node들에게 까지 전파 된 후 commit 됨
그렇기 때문에, 네트워크 망이 좋지 않다면 쓰기 속도가 떨어질 수 있다고 명시되어 있음. 읽기의 경우에는 속도 지연없이 자기자신에게서 읽음.
내부적으로 Leader가 있고 Follower가 있는 구조로서, Leader의 연결이 다 끊기면 Read-Only 모드로 진입함. 코딩시 Write Fail Exception을 고려해야 함. (망 단절시 Read-Only라도 수행하는)
약한 Synchronous 모드에서는 데이터 손실 가능성이 있긴 함. A (Leader), B (Follower), C (Follower) 서버가 있을때 성능을 위해 A가 B에게만 데이터를 복제할 수도 있음. 이때, A와 B가 다 죽는다면 C가 Leader로 동작 할 수도 있음. 이때, A와 B가 끊긴 지점부터의 트랜잭션은 손상됨.
약한 Sync모드에서는 Leader만 살아있다고 판단될 경우, Leader에만 쓰기를 할 수도 있음. 이 경우, Follower에 복제는 지원되지 않음 (연결이 복구될 때 까지 연기됨)
강한 Sync모드(Strict 모드) 에서는 최소 2개 노드 이상에 복제가 돼야 commit이 처리됨. 그러므로, 기준 수 이상의 Standby 서버가 죽으면 Write가 block처리 됨 (Application 단에서 Timeout 설정해야 함)
강한 동기모드를 설정 하더라도, 일부 서버는 Async mode Standby로 설정 할 수 있음.