etcd 대 PostgreSQL
Jinhua Luo
March 17, 2023
역사적 배경
PostgreSQL
PostgreSQL은 1986년 캘리포니아 대학교 버클리 캠퍼스의 Michael Stonebraker 교수의 지도 하에 처음 개발되었습니다. 수십 년에 걸친 개발 과정을 통해 PostgreSQL은 현재 가장 선도적인 오픈 소스 관계형 데이터베이스 관리 시스템으로 자리 잡았습니다. 허용적인 라이선스 덕분에 누구나 PostgreSQL을 자유롭게 사용, 수정 및 배포할 수 있으며, 이는 개인, 상업 또는 학술 연구 목적에 상관없이 적용됩니다.
PostgreSQL은 온라인 분석 처리(OLAP)와 온라인 트랜잭션 처리(OLTP) 모두에 강력한 지원을 제공하며, 강력한 SQL 쿼리 기능과 광범위한 확장 기능을 통해 거의 모든 상업적 요구를 충족할 수 있습니다. 결과적으로 최근 몇 년 동안 점점 더 많은 관심을 받고 있습니다. 사실, PostgreSQL의 확장성과 높은 성능은 거의 모든 다른 유형의 데이터베이스 기능을 복제할 수 있게 해줍니다.
이미지 출처 (CC 3.0 BY-SA 라이선스 준수): https://en.wikibooks.org/wiki/PostgreSQL/Architecture
etcd
etcd는 어떻게 탄생했으며, 어떤 문제를 해결하기 위해 만들어졌을까요?
2013년, 스타트업 팀 CoreOS는 Container Linux라는 제품을 개발했습니다. 이는 오픈 소스 경량 운영 체제로, 애플리케이션 서비스의 자동화와 빠른 배포를 우선시합니다. Container Linux는 애플리케이션이 컨테이너 내에서 실행되도록 요구하며, 클러스터 관리 솔루션을 제공하여 사용자가 단일 머신에서 서비스를 관리하는 것처럼 편리하게 관리할 수 있게 합니다.
사용자 서비스가 노드 재시작으로 인해 다운타임을 겪지 않도록 하기 위해 CoreOS는 여러 복제본을 실행해야 했습니다. 하지만 여러 복제본 간의 조정과 변경 중에 모든 복제본이 사용 불가능해지는 것을 어떻게 피할 수 있을까요?
이 문제를 해결하기 위해 CoreOS 팀은 서비스 구성 정보를 저장하고 분산 락 기능 등을 제공할 수 있는 조정 서비스가 필요했습니다. 그렇다면 그들은 어떻게 접근했을까요? 먼저 비즈니스 시나리오, 문제점 및 핵심 목표를 분석했습니다. 그런 다음 목표에 부합하는 솔루션을 선택했는데, 오픈 소스 커뮤니티 솔루션을 선택할지 아니면 자체적으로 도구를 개발할지 평가했습니다. 이 접근 방식은 어려운 문제에 직면했을 때 종종 사용되는 보편적인 문제 해결 방법이며, CoreOS 팀도 동일한 원칙을 따랐습니다.
조정 서비스는 이상적으로 다음과 같은 다섯 가지 목표를 충족해야 합니다:
- 여러 데이터 복제본을 통한 고가용성
- 복제본 간의 버전 검사를 통한 데이터 일관성
- 최소한의 저장 용량: 조정 서비스는 사용자 데이터가 아닌 서비스 및 노드에 대한 제어 평면 구성의 중요한 메타데이터 구성 정보만 저장해야 합니다. 이 접근 방식은 저장을 위한 데이터 샤딩 필요성을 최소화하고 과도한 설계를 방지합니다.
- CRUD(생성, 읽기, 업데이트, 삭제) 기능 및 데이터 변경을 감지하는 메커니즘. 서비스의 상태 정보를 저장하고, 서비스에 변경 사항이나 이상이 발생할 경우 변경 이벤트를 제어 평면에 빠르게 전달해야 합니다. 이는 서비스 가용성을 높이고 조정 서비스의 불필요한 성능 오버헤드를 줄이는 데 도움이 됩니다.
- 운영의 간편성: 조정 서비스는 운영, 유지보수 및 문제 해결이 쉬워야 합니다. 사용하기 쉬운 인터페이스는 오류 위험을 줄이고 유지보수 비용을 낮추며 다운타임을 최소화할 수 있습니다.
CAP 정리의 관점에서 etcd는 CP(일관성 & 분할 허용) 시스템에 속합니다.
Kubernetes 클러스터의 핵심 구성 요소인 kube-apiserver는 etcd를 기본 저장소로 사용합니다.
한편으로, etcd는 k8s 클러스터에서 리소스 객체 생성 시 지속성을 위해 사용됩니다. 다른 한편으로, etcd의 데이터 감시 메커니즘이 전체 클러스터의 Informer 작업을 주도하여 지속적인 컨테이너 오케스트레이션을 가능하게 합니다.
따라서 기술적 관점에서 Kubernetes가 etcd를 사용하는 핵심 이유는 다음과 같습니다:
- etcd는 Go 언어로 작성되었으며, k8s 기술 스택과 일치하고 리소스 소비가 낮으며 배포가 매우 쉽습니다.
- etcd의 강력한 일관성, 감시, 리스 등의 기능은 k8s의 핵심 의존성입니다.
요약하자면, etcd는 구성 관리 및 배포를 위해 특별히 설계된 분산 키-값 데이터베이스입니다. 클라우드 네이티브 소프트웨어로서, 즉시 사용 가능한 유용성과 높은 성능을 제공하여 이 특정 영역에서 전통적인 데이터베이스보다 우수합니다.
etcd와 PostgreSQL이라는 두 가지 다른 유형의 데이터베이스를 객관적으로 비교하려면 동일한 요구 사항을 기준으로 평가하는 것이 중요합니다. 따라서 이 글에서는 구성 관리 요구 사항을 충족하는 능력 측면에서 두 데이터베이스의 차이점만 논의할 것입니다.
데이터 모델
다른 데이터베이스는 사용자에게 제공하는 데이터 모델이 다르며, 이는 데이터베이스가 다양한 시나리오에 적합한지 여부를 결정합니다.
키-값 vs SQL
키-값 데이터 모델은 NoSQL에서 널리 사용되는 모델로, etcd도 이를 채택하고 있습니다. 이 모델은 SQL과 비교했을 때 어떤 장점이 있을까요?
먼저 SQL을 살펴보겠습니다.
관계형 데이터베이스는 데이터를 테이블로 유지하며, 구조화된 정보를 저장하고 접근하는 효율적이고 직관적이며 유연한 방법을 제공합니다.
테이블은 관계라고도 하며, 하나 이상의 데이터 범주를 포함하는 열과 해당 범주를 정의하는 데이터 집합을 포함하는 행(또는 테이블 레코드)으로 구성됩니다. 애플리케이션은 "프로젝트"로 속성을 식별하고, "선택"으로 튜플을 식별하고, "조인"으로 관계를 결합하는 등의 작업을 사용하여 데이터를 검색합니다. 데이터베이스 관리를 위한 관계형 모델은 1970년 IBM의 컴퓨터 과학자 Edgar Codd에 의해 개발되었습니다.
이미지 출처 (CC 3.0 BY-SA 라이선스 준수): https://en.wikipedia.org/wiki/Associative_entity
테이블의 레코드는 고유 식별자를 가지고 있지 않습니다. 테이블은 여러 중복 행을 수용하도록 설계되었기 때문입니다. 키-값 쿼리를 가능하게 하려면 테이블에서 키 역할을 하는 필드에 고유 인덱스를 추가해야 합니다. PostgreSQL의 기본 인덱스는 btree로, etcd와 마찬가지로 키에 대한 범위 쿼리를 수행할 수 있습니다.
구조화된 쿼리 언어(SQL)는 관계형 데이터베이스에서 정보를 저장하고 처리하기 위한 프로그래밍 언어입니다. 관계형 데이터베이스는 정보를 행과 열로 표현된 테이블 형태로 저장하며, 행과 열은 다양한 데이터 속성과 데이터 값 간의 다양한 관계를 나타냅니다. SQL 문을 사용하여 데이터베이스에서 정보를 저장, 업데이트, 제거, 검색 및 검색할 수 있습니다. 또한 SQL을 사용하여 데이터베이스 성능을 유지하고 최적화할 수 있습니다.
PostgreSQL은 SQL을 수많은 확장 기능으로 확장하여 튜링 완전 언어로 만들었습니다. 이는 SQL이 모든 복잡한 작업을 수행할 수 있음을 의미하며, 데이터 처리 로직을 완전히 서버 측에서 실행할 수 있게 합니다.
반면, etcd는 구성 관리 도구로 설계되었으며, 구성 데이터는 일반적으로 해시 테이블로 표현됩니다. 이 때문에 데이터 모델이 키-값 형식으로 구조화되어 하나의 큰 글로벌 테이블을 효과적으로 만듭니다. 이 테이블에 대해 CRUD 작업을 수행할 수 있으며, 이 테이블에는 버전 정보가 포함된 고유 키와 타입이 없는 값이라는 두 개의 필드만 있습니다. 결과적으로 클라이언트는 추가 처리를 위해 전체 값을 검색해야 합니다.
전반적으로 etcd의 키-값 구조는 SQL을 단순화하며, 구성 관리라는 특정 작업에 대해 더 편리하고 직관적입니다.
MVCC (다중 버전 동시성 제어)
MVCC는 구성 관리에서 데이터 버전 관리를 위한 필수 기능입니다. 이를 통해 다음이 가능합니다:
- 이전 데이터 조회
- 버전 비교를 통해 데이터의 연령 확인
- 데이터 감시, 이는 증분 알림을 가능하게 하기 위해 버전 관리가 필요
etcd와 PostgreSQL 모두 MVCC를 가지고 있지만, 둘 사이의 차이점은 무엇일까요?
etcd는 전역적으로 증가하는 64비트 버전 카운터를 사용하여 MVCC 시스템을 관리합니다. 오버플로우에 대해 걱정할 필요가 없습니다. 이 카운터는 초당 수백만 번의 업데이트가 발생하더라도 처리할 수 있도록 설계되었습니다. 키-값 쌍이 생성되거나 업데이트될 때마다 버전 번호가 할당됩니다. 키-값 쌍이 삭제되면 버전 번호가 0으로 재설정된 툼스톤이 생성됩니다. 이는 모든 변경 사항이 이전 버전을 덮어쓰는 대신 새로운 버전을 생성함을 의미합니다.
또한 etcd는 키-값 쌍의 모든 버전을 유지하고 사용자에게 표시합니다. 키-값 데이터는 덮어쓰지 않으며, 새로운 버전은 기존 버전과 함께 저장됩니다. etcd의 MVCC 구현은 읽기-쓰기 분리도 제공하므로 사용자가 잠금 없이 데이터를 읽을 수 있어 읽기 집약적인 사용 사례에 적합합니다.
PostgreSQL의 MVCC 구현은 etcd와 달리 증가하는 버전 번호를 제공하는 데 초점을 맞추지 않고, 사용자에게 투명하게 트랜잭션과 다양한 격리 수준을 구현하는 데 초점을 맞춥니다. MVCC는 동시 업데이트를 가능하게 하는 낙관적 잠금 메커니즘입니다. 테이블의 각 행에는 트랜잭션 ID 기록이 있으며, xmin
은 행 생성 시의 트랜잭션 ID를 나타내고 xmax
는 행 업데이트 시의 트랜잭션 ID를 나타냅니다.
- 트랜잭션은 이미 커밋된 데이터만 읽을 수 있습니다.
- 데이터를 업데이트할 때 버전 충돌이 발생하면 PostgreSQL은 업데이트를 진행할지 여부를 결정하기 위해 매칭 메커니즘으로 재시도합니다.
예제를 보려면 다음 링크를 참조하세요: https://devcenter.heroku.com/articles/postgresql-concurrency
안타깝게도 PostgreSQL에서 구성 데이터의 버전 관리를 위해 트랜잭션 ID를 사용하는 것은 여러 가지 이유로 불가능합니다:
- 트랜잭션 ID는 동일한 트랜잭션에 포함된 모든 행에 할당되므로 행 수준에서 버전 관리를 적용할 수 없습니다.
- 이전 데이터를 조회할 수 없으며, 최신 버전의 행만 접근할 수 있습니다.
- 32비트 카운터 특성상 트랜잭션 ID는 오버플로우가 발생하기 쉽고, vacuum 중에 재설정될 수 있습니다.
- 트랜잭션 ID를 기반으로 감시 기능을 구현할 수 없습니다.
결과적으로 PostgreSQL은 구성 데이터의 버전 관리를 위해 내장 지원이 없기 때문에 대체 방법이 필요합니다.
클라이언트 인터페이스
클라이언트 인터페이스 설계는 사용과 관련된 비용 및 리소스 소비를 결정하는 데 중요한 측면입니다. 인터페이스 간의 차이점을 분석함으로써 가장 적합한 옵션을 선택할 때 정보에 입각한 결정을 내릴 수 있습니다.
etcd의 kv/watch/lease API는 구성을 관리하는 데 특히 능숙한 것으로 입증되었습니다. 그러나 PostgreSQL에서 이러한 API를 어떻게 구현할 수 있을까요?
안타깝게도 PostgreSQL은 이러한 API에 대한 내장 지원을 제공하지 않으며, 이를 구현하려면 캡슐화가 필요합니다. 이를 구현하기 위해 제가 개발한 pg_watch_demo 프로젝트를 분석해 보겠습니다: pg_watch_demo.
gRPC/HTTP vs TCP
PostgreSQL은 다중 프로세스 아키텍처를 따르며, 각 프로세스는 한 번에 하나의 TCP 연결만 처리합니다. SQL 쿼리를 통해 기능을 제공하기 위해 사용자 정의 프로토콜을 사용하며, 요청-응답 상호 작용 모델(HTTP/1.1과 유사하게 한 번에 하나의 요청만 처리하며, 여러 요청을 동시에 처리하려면 파이프라이닝이 필요함)을 따릅니다. 그러나 높은 리소스 소비와 상대적으로 낮은 효율성을 고려할 때, 특히 높은 QPS 시나리오에서 성능을 향상시키기 위해 연결 풀 프록시(예: pgbouncer)가 중요합니다.
반면, etcd는 Golang의 다중 코루틴 아키텍처로 설계되었으며, gRPC와 RESTful이라는 두 가지 사용자 친화적인 인터페이스를 제공합니다. 이러한 인터페이스는 통합하기 쉽고 리소스 소비 측면에서 효율적입니다. 또한 각 gRPC 연결은 여러 동시 쿼리를 처리할 수 있어 최적의 성능을 보장합니다.
데이터 정의
etcd
message KeyValue {
bytes key = 1;
// 키가 생성된 리비전 번호
int64 create_revision = 2;
// 키가 마지막으로 수정된 리비전 번호
int64 mod_revision = 3;
// 키가 업데이트될 때마다 증가하는 카운터.
// 이 카운터는 키가 삭제되면 0으로 재설정되며, 툼스톤으로 사용됩니다.
int64 version = 4;
bytes value = 5;
// TTL을 위해 키가 사용하는 리스 객체. 값이 0이면 TTL이 없음을 의미합니다.
int64 lease = 6;
}
PostgreSQL
PostgreSQL은 etcd의 전역 데이터 공간을 시뮬레이트하기 위해 테이블을 사용해야 합니다:
CREATE TABLE IF NOT EXISTS config (
key text,
value text,
-- `create_revision` 및 `mod_revision`과 동일
-- 여기서는 리비전을 시뮬레이트하기 위해 큰 정수 증가 시퀀스 유형을 사용합니다.
revision bigserial,
-- 툼스톤
tombstone boolean NOT NULL DEFAULT false,
-- 복합 인덱스, 키로 먼저 검색한 후 리비전으로 검색
primary key(key, revision)
);
get
etcd
etcd의 get
API는 다양한 매개변수를 가지고 있습니다:
- 범위 쿼리, 예를 들어
key
를/abc
로 설정하고range_end
를/abd
로 설정하면/abc
를 접두사로 하는 모든 키-값 쌍을 검색합니다. - 이전 데이터 조회,
revision
또는mod_revision
범위를 지정합니다. - 정렬 및 반환 결과 수 제한.
message RangeRequest {
...
bytes key = 1;
// 범위 쿼리
bytes range_end = 2;
int64 limit = 3;
// 이전 데이터 조회
int64 revision = 4;
// 정렬
SortOrder sort_order = 5;
SortTarget sort_target = 6;
bool serializable = 7;
bool keys_only = 8;
bool count_only = 9;
// 이전 데이터 조회
int64 min_mod_revision = 10;
int64 max_mod_revision = 11;
int64 min_create_revision = 12;
int64 max_create_revision = 13;
}
PostgreSQL
PostgreSQL은 SQL을 통해 etcd의 get 기능을 수행할 수 있으며, 더 복잡한 기능도 제공할 수 있습니다. SQL 자체가 고정된 매개변수 인터페이스가 아닌 언어이기 때문에 매우 다재다능합니다. 여기서는 최신 키-값 쌍을 검색하는 간단한 예를 보여줍니다. 기본 키가 복합 인덱스이므로 범위로 빠르게 검색할 수 있어 고속 검색이 가능합니다.
CREATE FUNCTION get1(kk text)
RETURNS table(r bigint, k text, v text, c bigint) AS $$
SELECT revision, key, value, create_time
FROM config
where key = kk and tombstone = false
ORDER BY key, revision desc
limit 1
$$ LANGUAGE sql;
put
etcd
message PutRequest {
bytes key = 1;
bytes value = 2;
int64 lease = 3;
// 이 `Put` 요청으로 업데이트되기 전의 키-값 쌍 데이터로 응답할지 여부.
bool prev_kv = 4;
bool ignore_value = 5;
bool ignore_lease = 6;
}
PostgreSQL
etcd와 마찬가지로 PostgreSQL도 변경 사항을 즉시 실행하지 않습니다. 대신 새로운 행을 삽입하고 새로운 리비전을 할당합니다.
CREATE FUNCTION set(k text, v text) RETURNS bigint AS $$
insert into config(key, value) values(k, v) returning revision;
$$ LANGUAGE SQL;
delete
etcd
message DeleteRangeRequest {
bytes key = 1;
bytes range_end = 2;
bool prev_kv = 3;
}
PostgreSQL
etcd와 마찬가지로 PostgreSQL에서도 삭제는 데이터를 즉시 수정하지 않습니다. 대신 툼스톤 필드가 true로 설정된 새로운 행을 삽입하여 툼스톤임을 나타냅니다.
CREATE FUNCTION del(k text) RETURNS bigint AS $$
insert into config(key, tombstone) values(k, true) returning revision;
$$ LANGUAGE SQL;
watch
etcd
message WatchCreateRequest {
bytes key = 1;
// 감시할 키의 범위 지정
bytes range_end = 2;
// 감시 시작 리비전
int64 start_revision = 3;
...
}
message WatchResponse {
ResponseHeader header = 1;
...
// 효율성을 위해 여러 이벤트를 반환할 수 있음
repeated mvccpb.Event events = 11;
}
PostgreSQL
PostgreSQL은 내장된 watch 기능을 제공하지 않으며, 대신 트리거와 채널을 조합하여 유사한 기능을 구현해야 합니다. pg_notify
를 사용하여 특정 채널을 감시하는 모든 애플리케이션에 데이터를 전송할 수 있습니다.
-- put/delete 이벤트를 배포하기 위한 트리거 함수
CREATE FUNCTION notify_config_change() RETURNS TRIGGER AS $$
DECLARE
data json;
channel text;
is_channel_exist boolean;
BEGIN
IF (TG_OP = 'INSERT') THEN
-- JSON으로 인코딩
data = row_to_json(NEW);
-- 키에서 배포할 채널 이름 추출
channel = (SELECT SUBSTRING(NEW.key, '/(.*)/'));
-- 애플리케이션이 채널을 감시 중이면 이벤트를 통해 전송
is_channel_exist = NOT pg_try_advisory_lock(9080);
IF is_channel_exist THEN
PERFORM pg_notify(channel, data::text);
ELSE
PERFORM pg_advisory_unlock(9080);
END IF;
END IF;
RETURN NULL; -- AFTER 트리거이므로 결과는 무시됨
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER notify_config_change
AFTER INSERT ON config
FOR EACH ROW EXECUTE FUNCTION notify_config_change();
watch 기능이 캡슐화되었기 때문에 클라이언트 애플리케이션도 해당 로직을 구현해야 합니다. Golang을 예로 들면 다음 단계를 수행해야 합니다:
- 감시 시작: 감시가 시작되면 모든 notify 데이터는 PostgreSQL 및 Golang 채널 수준에서 캐시됩니다.
- get_all(key_prefix, revision)을 사용하여 모든 데이터 검색: 이 함수는 지정된 리비전부터 시작하여 모든 기존 데이터를 읽습니다. 각 키에 대해 최신 리비전 데이터만 반환되며, 삭제된 데이터는 자동으로 제거됩니다. 리비전이 지정되지 않으면 주어진
key_prefix
를 가진 모든 키의 최신 데이터를 반환합니다. - 새로운 데이터 감시, 첫 번째와 두 번째 단계 사이에 캐시된 알림을 포함하여 이 시간 창 동안 발생할 수 있는 새로운 데이터를 놓치지 않도록 합니다. 두 번째 단계에서 이미 읽은 리비전은 무시합니다.
func watch(l *pq.Listener) {
for {
select {
case n := <-l.Notify:
if n == nil {
log.Println("listener reconnected")
log.Printf("get all routes from rev %d including tombstones...\n", latestRev)
// 재연결 시, 연결이 끊기기 전의 리비전을 기반으로 전송을 재개합니다.
str := fmt.Sprintf(`select * from get_all_from_rev_with_stale('/routes/', %d)`, latestRev)
rows, err := db.Query(str)
...
continue
}
...
// 받은 최신 리비전을 기록하는 상태 유지
updateRoute(cfg)
case <-time.After(15 * time.Second):
log.Println("Received no events for 15 seconds, checking connection")
go func() {
// 오랜 시간 동안 이벤트를 받지 못하면 연결 상태를 확인합니다.
if err := l.Ping(); err != nil {
log.Println("listener ping error: ", err)
}
}()
}
}
}
log.Println("get all routes...")
// 초기화 시 애플리케이션은 현재 모든 키-값 쌍을 가져온 후 watch를 통해 증분 업데이트를 모니터링해야 합니다.
rows, err := db.Query(`select * from get_all('/routes/')`)
...
go watch(listener)
transaction
etcd
etcd의 트랜잭션은 조건 검사와 함께 여러 작업의 집합이며, 트랜잭션에 의한 변경 사항은 원자적으로 커밋됩니다.
message TxnRequest {
// 트랜잭션 실행 조건 지정
repeated Compare compare = 1;
// 조건이 충족되면 실행할 작업
repeated RequestOp success = 2;
// 조건이 충족되지 않으면 실행할 작업
repeated RequestOp failure = 3;
}
PostgreSQL
PostgreSQL의 DO
명령은 저장 프로시저를 포함한 모든 명령을 실행할 수 있습니다. PL/pgSQL 및 Python과 같은 내장 언어를 포함하여 여러 언어를 지원합니다. 이러한 언어를 사용하면 모든 조건부 판단, 루프 및 기타 제어 로직을 구현할 수 있어 etcd보다 더 다재다능합니다.
DO LANGUAGE plpgsql $$
DECLARE
n_plugins int;
BEGIN
SELECT COUNT(1) INTO n_plugins FROM get_all('/plugins/');
IF n_plugins = 0 THEN
perform set('/routes/1', 'foobar');
perform set('/upstream/1', 'foobar');
...
ELSE
...
END IF;
END;
$$;
lease
etcd
etcd에서는 애플리케이션이 주기적으로 갱신해야 하는 리스 객체를 생성할 수 있습니다. 각 키-값 쌍은 리스 객체에 연결될 수 있으며, 리스 객체가 만료되면 연결된 모든 키-값 쌍도 만료되어 자동으로 삭제됩니다.
message LeaseGrantRequest {
// 리스의 TTL
int64 TTL = 1;
int64 ID = 2;
}
// 리스 갱신
message LeaseKeepAliveRequest {
int64 ID = 1;
}
message PutRequest {
bytes key = 1;
bytes value = 2;
// TTL을 구현하기 위한 리스 ID
int64 lease = 3;
...
}
PostgreSQL
- PostgreSQL에서는 외래 키를 통해 리스를 유지할 수 있습니다. 쿼리 시 연결된 리스 객체가 만료된 경우 툼스톤으로 간주됩니다.
- Keepalive 요청은 리스 테이블의
last_keepalive
타임스탬프를 업데이트합니다.
CREATE TABLE IF NOT EXISTS config (
key text,
value text,
...
-- 연결된 리스 객체를 지정하기 위해 외래 키 사용.
lease int64 references lease(id),
);
CREATE TABLE IF NOT EXISTS lease (
id text,
ttl int,
last_keepalive timestamp;
);
성능 비교
PostgreSQL은 etcd의 다양한 API를 캡슐화를 통해 시뮬레이트해야 합니다. 그렇다면 성능은 어떨까요? 간단한 테스트 결과는 다음과 같습니다: https://github.com/kingluo/pg_watch_demo#benchmark.
결과를 보면 읽기 및 쓰기 성능이 거의 동일하며, PostgreSQL이 etcd보다 더 나은 성능을 보이는 경우도 있습니다. 또한 업데이트가 발생한 시점부터 애플리케이션이 이벤트를 받는 데 걸리는 지연 시간은 업데이트 배포의 효율성을 결정하며, PostgreSQL과 etcd 모두 비슷한 성능을 보입니다. 클라이언트와 서버가 동일한 머신에서 테스트되었을 때 watch 지연 시간은 1밀리초 미만이었습니다.
그러나 PostgreSQL에는 몇 가지 단점이 있습니다:
- 각 업데이트에 대한 WAL 로그가 더 크기 때문에 etcd에 비해 디스크 I/O가 두 배 더 많이 발생합니다.
- etcd에 비해 더 많은 CPU를 소비합니다.
- 채널 기반의 Notify는 트랜잭션 수준 개념입니다. 동일한 유형의 리소스를 업데이트할 때 업데이트는 동일한 채널로 전송되며, 업데이트 요청은 상호 배제 잠금을 경쟁하므로 요청이 직렬화됩니다. 즉, 채널을 사용하여 watch를 구현하면 put 작업의 병렬성이 영향을 받습니다.
이는 동일한 요구 사항을 충족하기 위해 PostgreSQL을 더 많이 학습하고 최적화해야 함을 보여줍니다.
저장소
성능은 기본 저장소에 의해 결정되며, 데이터가 어떻게 저장되는지에 따라 데이터베이스가 메모리, 디스크 및 기타 리소스에 대한 요구 사항이 결정됩니다.
etcd
etcd 저장소의 아키텍처 다이어그램:
etcd는 먼저 업데이트를 WAL(Write-Ahead Log)에 기록하고 디스크로 플러시하여 업데이트가 손실되지 않도록 합니다. 로그가 성공적으로 기록되고 다수의 노드에 의해 확인되면 결과를 클라이언트에 반환할 수 있습니다. etcd는 또한 TreeIndex와 BoltDB를 비동기적으로 업데이트합니다.
로그가 무한히 증가하는 것을 방지하기 위해 etcd는 주기적으로 저장소의 스냅샷을 찍고, 스냅샷 이전의 로그는 삭제할 수 있습니다.
etcd는 모든 키를 메모리(TreeIndex)에 인덱싱하여 각 키의 버전 정보를 기록하지만, 값에 대해서는 BoltDB(revision)에 대한 포인터만 유지합니다.
키에 해당하는 값은 디스크에 저장되며 BoltDB를 사용하여 유지됩니다.
TreeIndex와 BoltDB 모두 btree 데이터 구조를 사용하며, 이는 조회 및 범위 조회에 효율적입니다.
TreeIndex 구조 다이어그램:
(이미지 출처: https://blog.csdn.net/H_L_S/article/details/112691481, CC 4.0 BY-SA 라이선스 준수)
각 키는 다른 세대로 나뉘며, 각 삭제는 세대의 끝을 표시합니다.
값에 대한 포인터는 두 개의 정수로 구성됩니다. 첫 번째 정수 main
은 etcd의 트랜잭션 ID이며, 두 번째 정수 sub
는 해당 트랜잭션 내에서 이 키의 업데이트 ID를 나타냅니다.
Boltdb는 트랜잭션과 스냅샷을 지원하며, 리비전에 해당하는 값을 저장합니다.
(이미지 출처: https://blog.csdn.net/H_L_S/article/details/112691481, CC 4.0 BY-SA 라이선스 준수)
데이터 쓰기 예제:
key="key1", revision=(12,1), value="keyvalue5"
를 작성합니다. treeIndex와 BoltDB의 빨간색 부분의 변경 사항을 주목하세요:
(이미지 출처: https://blog.csdn.net/H_L_S/article/details/112691481, CC 4.0 BY-SA 라이선스 준수)
key="key", revision=(13,1)
를 삭제하면 treeIndex에 새로운 빈 세대가 생성되고 BoltDB에 key="13_1t"
인 빈 값이 생성됩니다.
여기서 t
는 "tombstone"을 의미합니다. 이는 treeIndex의 포인터가 (13,1)
이지만 BoltDB에서는 13_1t
이므로 툼스톤을 읽을 수 없음을 의미합니다.
(이미지 출처: https://blog.csdn.net/H_L_S/article/details/112691481, CC 4.0 BY-SA 라이선스 준수)
etcd는 읽기와 쓰기를 모두 BoltDB에 단일 고루틴으로 스케줄링하여 무작위 디스크 I/O를 줄이고 I/O 성능을 향상시킵니다.
PostgreSQL
PostgreSQL 저장소의 아키텍처 다이어그램:
etcd와 마찬가지로 PostgreSQL은 먼저 업데이트를 로그 파일에 추가하고, 로그가 디스크에 성공적으로 플러시될 때까지 기다린 후 트랜잭션을 완료합니다. 동시에 업데이트는 shared_buffer 메모리에 기록됩니다.
shared_buffer는 PostgreSQL의 모든 테이블과 인덱스가 공유하는 메모리 영역이며, 이러한 객체의 매핑 역할