df_0010
Dragonflydb Architecture
Architecture
Dragonfly는 Redis 및 Memcached와 같은 메모리 저장소를 현대적으로 대체합니다.
싱글 인스턴스에서 수직으로 확장되어 초당 수백만 개의 요청을 처리합니다.
메모리 효율성이 더 높고 안정성을 염두에 두고 설계되었으며 더 나은 캐싱 설계가 포함되어 있습니다.
⬜️
Dragonflydb Architecture 원문(영어)
📦 Threading model - 스레드 모델
⬜️ Dragonfly는 싱글 프로세스 / 다중 스레드 아키텍처의를 사용합니다.
각 Dragonfly 스레드에는 파이버(fiber)를 통해 여러 가지 책임이 간접적으로 할당됩니다.
⬜️ 그러한 책임 중 하나는 들어오는 연결을 처리하는 것입니다.
소켓 수신기가 클라이언트 연결을 수락하면 연결은 파이버(fiber) 내부의 단일 스레드에 바인딩된 전체 수명을 보냅니다.
Dragonfly는 100% 비차단(non-blocking)으로 작성되었습니다.
파이버(fiber)를 사용하여 각 스레드에 비동기성을 제공합니다.
비동기성의 필수 속성 중 하나는 보류 중인 CPU 작업이 있는 한 스레드를 차단할 수 없다는 것입니다.
Dragonfly는 각 실행 컨텍스트 단위를 파이버로 래핑하여 이 속성을 유지합니다.
I/O에서 잠재적으로 차단될 수 있는 실행 단위를 래핑합니다.
예를 들어, 연결 루프는 파이버 내에서 실행됩니다.
스냅샷을 작성하는 함수는 파이버 내부에서 실행됩니다.
⬜️ 부차적인 설명으로 비동기성과 병렬성은 다른 용어입니다.
예를 들어 Nodejs는 비동기 실행을 제공하지만 싱글 스레드입니다.
마찬가지로 각 Dragonfly 스레드는 자체적으로 비동기적입니다.
따라서 Dragonfly는 디스크에 저장하거나 Lua 스크립트를 실행하는 등 장기 실행(long-running) 명령을 처리하는 경우에도
들어오는 이벤트에 응답합니다.
◎ Thread actors in Dragonflydb
⬜️ Dragonflydb 인메모리 데이터베이스는 N개의 부분으로 분할됩니다(sharded).
여기서 N은 시스템의 스레드 수보다 작거나 같습니다.
각 데이터베이스 샤드는 싱글 스레드에 의해 소유되고 액세스됩니다.
동일한 스레드는 TCP 연결을 처리하는 동시에 데이터베이스 샤드를 호스팅할 수 있습니다. 아래 다이어그램을 참조하세요.
⬜️ 여기서 Dragonflydb 프로세스는 4개의 스레드를 생성합니다.
여기서 스레드 1~3은 I/O(즉, 클라이언트 연결 관리)를 처리하고 스레드 2~4는 DB 샤드를 관리합니다.
예를 들어 스레드 2는 들어오는 요청을 처리하고 자신이 소유한 샤드에서 DB 작업을 처리하는 데 CPU 시간을 나눕니다.
⬜️ 따라서 스레드 1이 I/O 스레드라고 말하면 Dragonfly가 스레드 1에 대한 클라이언트 연결을 관리하는 파이버를
고정할 수 있다는 의미입니다.
일반적으로 모든 스레드에는 CPU 시간이 필요한 많은 책임이 있을 수 있습니다.
데이터베이스 관리와 연결 처리는 이러한 책임 중 두 가지일 뿐입니다.
📦 Fibers - 파이버
파이버(fibers)에 대해 더 자세히 알아보려면 소개 게시물을 읽어 보시기 바랍니다.
• Introduction to fibers in c++ (Boost.Fibers)(영문)
• Boost.Fiber(한글) - redisgate
⬜️ Boost.Fibers 라이브러리는 매우 잘 설계되었습니다.
방해가 되지 않고(unintrusive), 가볍고(lightweight), 효율적(efficient)입니다.
또한 기본 스케줄러를 재정의(overridden)할 수 있습니다.
Dragonfly를 구동하는 I/O 라이브러리인 helio의 경우, 우리는 Boost.Fibers 스케줄러를 재정의하여
비공유(shared-nothing) 아키텍처를 지원하고 이를 I/O 폴링 루프와 통합했습니다.
⬜️ 중요한 것은 파이버의 비동기성을 유지하려면 애플리케이션 레이어에서 상향식(bottom-up) 지원이 필요하다는 것입니다.
예를 들어, 아래 요약 코드(스니펫 snippet)에서 fd에 대한 쓰기 차단은 파이버가 선점하여 다른 파이버로 전환하는 것을
허용하지 않습니다. 전체 스레드가 차단됩니다.
⬜️ 마찬가지로, pthread_mutex_lock 호출을 사용하면 전체 스레드가 차단되어 귀중한 CPU 시간이 낭비될 수 있습니다.
따라서 Dragonfly 코드는 I/O, 통신 및 조정(coordination)을 위해 파이버 친화적인(fiber-friendly) 기본 요소를 사용합니다.
이러한 기본 요소는 helio 및 Boost.Fibers 라이브러리에서 제공됩니다.
📦 Life of a command request - 명령 요청의 수명
⬜️ 이 섹션에서는 비공유 아키텍처의 맥락에서 Dragonfly가 명령을 처리하는 방법을 설명합니다.
오늘날 사용되는 대부분의 아키텍처에서 다중 스레드 서버는 데이터 구조를 보호하기 위해 뮤텍스 잠금을 사용하지만
Dragonfly는 그렇지 않습니다.
⬜️ Dragonfly의 스레드 간 상호 작용(Inter-thread interactions)은 스레드에서 스레드로 메시지를 전달하는 경우에만 발생합니다.
예를 들어, SET 요청을 처리하는 다음 시퀀스 다이어그램을 고려해보세요.
⬜️ 여기서 연결 파이버는 KEY 엔터티를 처리하는 스레드와 다른 스레드에 있습니다.
우리는 해싱을 사용하여 어떤 샤드가 어떤 키를 소유하는지 결정합니다.
이 흐름을 생각하는 또 다른 방법은 연결 파이버가 다른 스레드에 트랜잭션 명령을 발행하기(issuing) 위한
조정자(coordinator) 역할을 한다는 것입니다.
이 간단한 예에서 외부 "SET" 명령에는 코디네이터에서 대상 샤드 스레드로 전달되는 단일 메시지(a single message)가 필요합니다.
단일 명령(a single command) 요청의 맥락에서 Dragonfly 모델을 생각할 때 위의 다이어그램 대신
다음 다이어그램을 사용하는 것을 선호합니다.
⬜️ 여기서 코디네이터(또는 연결 파이버)는 우연히(coincidently) 샤드 중 하나를 소유하는 스레드 중 하나에 상주할 수도 있습니다.
그러나 샤드 데이터에 직접 액세스하지 않는 별도의 엔터티로 생각하는 것이 더 쉽습니다.
⬜️ 코디네이터(coordinator)는 여러 샤드와 통신하는 모든 복잡성을 숨기는 가상화 계층 역할을 합니다.
이는 "mset, mget 및 blpop"과 같은 다중 키 명령에 대한 원자성(엄격한 직렬화) 의미 체계를 제공하기 위해
최첨단 알고리즘을 사용합니다.
또한 Lua 스크립트 및 다중 명령 트랜잭션(multi-command transaction)에 대한 엄격한 직렬성을 제공합니다.
⬜️ 이러한 복잡성을 숨기는 것은 최종 고객에게 가치가 있지만, 일부 CPU 및 대기 시간(latency) 비용이 발생합니다.
우리는 Dragonfly가 제공하는 가치를 고려할 때 절충(trade-off)은 가치 있다고 믿습니다.
⬜️ 트랜잭션 코드의 복잡성 없이 Dragonfly 아키텍처에 대해 자세히 알아보고 싶다면 ,
PING, SET 및 GET 명령을 지원하는 작은 백엔드를 구현하는 Midi Redis를 확인해 볼 가치가 있습니다.
Midi Redis
Midi Redis main_service.cc
⬜️ 사실 Dragonfly는 그 프로젝트에서 성장했습니다.
⬜️ midi-redis 보다 훨씬 간단한 TCP 백엔드를 구축하는 방법을 배우려면,
helio 라이브러리는 echo_server 및 ping_iouring_server.cc와 같은 샘플 백엔드를 제공합니다.
이러한 백엔드는 Dragonfly 및 midi-redis와 마찬가지로 멀티 코어 서버에서 수백만 QPS에 도달합니다.
✅ Fiber 파이버
⬜️
위키(한글)
⬜️ 파이버(Fiber)는 컴퓨터 공학에서 특히 가벼운 실행 스레드이다.
⬜️ 스레드와 마찬가지로 파이버는 주소 공간을 공유한다.
그러나 파이버는 협력적 멀티 태스킹을 사용하는 반면 스레드는 선점형(preemptive) 멀티 태스킹을 사용한다.
일반적인 스레드는 커널의 스레드 스케줄러에 의존해 실행 권한이 양도되지만, 파이버는 스스로 실행 권한을 양도한다.
선점형 스케줄링(preemptive scheduling)은 시분할 시스템에서 타임 슬라이스가 소진되었거나,
인터럽트나 시스템 호출 종료 시에 더 높은 우선 순위 프로세스가 발생 되었음을 알았을 때,
현 실행 프로세스로부터 강제로 CPU를 회수하는 것을 말한다.
⬜️ 장점과 단점
장점: 파이버가 협력적으로 멀티 태스킹하기 때문에 스레드 안전성은 선점형 스레드보다 문제가 적으며,
스핀락 및 원자성 작업을 포함한 동기화 구성은 파이버 단위가 자체적으로 양도를 하는 특성 덕분에 동기화되므로 불필요하다.
그러나 많은 라이브러리는 비동기 I/O를 수행하기 위해 암시적으로 파이버를 생성한다.
단점: 파이버가 선점형 스레드를 사용하지 않고는 다중 프로세서 시스템을 사용할 수 없다는 것이다.
그러나 CPU 코어보다 더 많은 선점 스레드가 없는 M : N 스레딩 모델은 순수 파이버 또는 순수 선점 스레딩보다 더 효율적일 수 있다.
일부 서버 프로그램에서 파이버는 자체적으로 소프트 블록(soft block)을 사용하여 단일 스레드로 작동하는
상위 프로그램이 계속 작동할 수 있도록 한다. 이런 설계에서 파이버는 CPU 처리가 필요없는 I/O 액세스에 주로 사용된다.
그래서 메인 프로그램이 기존에 수행중인 작업을 계속할 수 있다.
파이버는 단일 스레드 메인 프로그램에 대한 제어를 양보하며 I/O 작업이 완료되면 파이버가 중단 된 지점에서 계속된다.
⬜️ 운영 체제 지원
파이버는 스레드에 비해 운영 체제의 지원이 상대적으로 덜 필요하다.
모던 유닉스(Unix) 시스템에서는 GNU Portable Threads처럼 ucontext.h 라이브러리에서 제공되는
getcontext, setcontext와 swapcontext 함수를 이용해 구현하거나,
boost.fiber 처럼 C++를 사용하여 구현할 수 있다.
