etcd vs PostgreSQL
Jinhua Luo
March 17, 2023
Historischer Hintergrund
PostgreSQL
PostgreSQL wurde ursprünglich 1986 unter der Leitung von Professor Michael Stonebraker an der University of California, Berkeley, entwickelt. Im Laufe mehrerer Jahrzehnte der Entwicklung hat sich PostgreSQL als das führende Open-Source-Relational-Datenbankmanagementsystem etabliert, das heute verfügbar ist. Seine freizügige Lizenz ermöglicht es jedem, PostgreSQL frei zu verwenden, zu modifizieren und zu verteilen, unabhängig davon, ob es für private, kommerzielle oder akademische Forschungszwecke genutzt wird.
PostgreSQL bietet eine robuste Unterstützung für sowohl Online Analytical Processing (OLAP) als auch Online Transaction Processing (OLTP) und verfügt über leistungsstarke SQL-Abfragefunktionen sowie eine breite Palette von Erweiterungen, die es ermöglichen, nahezu alle kommerziellen Anforderungen zu erfüllen. Infolgedessen hat es in den letzten Jahren zunehmend an Aufmerksamkeit gewonnen. Tatsächlich ermöglichen die Skalierbarkeit und hohe Leistungsfähigkeit von PostgreSQL, die Funktionalität nahezu jeder anderen Art von Datenbank zu replizieren.
Bildquelle (gemäß CC 3.0 BY-SA Lizenzvereinbarung): https://en.wikibooks.org/wiki/PostgreSQL/Architecture
etcd
Wie ist etcd entstanden und welches Problem löst es?
Im Jahr 2013 entwickelte das Startup-Team CoreOS ein Produkt namens Container Linux. Es handelt sich um ein Open-Source, leichtgewichtiges Betriebssystem, das die Automatisierung und schnelle Bereitstellung von Anwendungsdiensten priorisiert. Container Linux erfordert, dass Anwendungen in Containern ausgeführt werden, und bietet eine Cluster-Management-Lösung, die es Benutzern ermöglicht, Dienste so zu verwalten, als ob sie auf einer einzigen Maschine laufen würden.
Um sicherzustellen, dass Benutzerdienste nicht aufgrund eines Neustarts eines Knotens ausfallen, musste CoreOS mehrere Replikate ausführen. Aber wie würden sie die Koordination zwischen mehreren Replikaten sicherstellen und vermeiden, dass alle Replikate während Änderungen nicht verfügbar werden?
Um dieses Problem zu lösen, benötigte das CoreOS-Team einen Koordinationsdienst, der Dienstkonfigurationsinformationen speichern und Funktionen wie verteilte Sperren bieten konnte. Was war also ihr Ansatz? Sie analysierten zunächst das Geschäftsszenario, die Schmerzpunkte und die Kernziele. Dann wählten sie eine Lösung aus, die ihren Zielen entsprach, und bewerteten, ob sie eine Open-Source-Community-Lösung wählen oder ihr eigenes benutzerdefiniertes Tool entwickeln sollten. Dieser Ansatz ist eine universelle Problemlösungsmethode, die oft angewendet wird, wenn man mit schwierigen Problemen konfrontiert ist, und das CoreOS-Team folgte dem gleichen Prinzip.
Ein Koordinationsdienst sollte idealerweise die folgenden fünf Ziele erfüllen:
- Hohe Verfügbarkeit mit mehreren Datenreplikaten
- Datenkonsistenz mit Versionsprüfung zwischen Replikaten
- Minimale Speicherkapazität: Der Koordinationsdienst sollte nur kritische Metadatenkonfigurationsinformationen für Dienste und Knoten speichern, die zur Steuerungsebene gehören, anstatt benutzerbezogene Daten. Dieser Ansatz minimiert den Bedarf an Datensharding für die Speicherung und vermeidet übermäßiges Design.
- Funktionalität für CRUD (create, read, update, und delete) sowie ein Mechanismus zum Überwachen von Datenänderungen. Es sollte den Statusinformationen von Diensten speichern, und wenn es Änderungen oder Anomalien in den Diensten gibt, sollte es das Änderungsereignis schnell an die Steuerungsebene weiterleiten. Dies hilft, die Dienstverfügbarkeit zu verbessern und unnötige Leistungsüberlastungen für den Koordinationsdienst zu reduzieren.
- Einfache Bedienbarkeit: Der Koordinationsdienst sollte einfach zu bedienen, zu warten und zu beheben sein. Eine benutzerfreundliche Schnittstelle kann das Risiko von Fehlern verringern, die Wartungskosten senken und Ausfallzeiten minimieren.
Aus der Perspektive des CAP-Theorems gehört etcd zum CP-System (Consistency & Partition Tolerance).
Als zentrales Element eines Kubernetes-Clusters verwendet kube-apiserver etcd als zugrunde liegenden Speicher.
Einerseits wird etcd für die Persistenz bei der Erstellung von Ressourcenobjekten in einem k8s-Cluster verwendet. Andererseits ist es der Daten-Watch-Mechanismus von etcd, der die gesamte Informer-Arbeit des Clusters antreibt und eine kontinuierliche Container-Orchestrierung ermöglicht.
Daher sind die technischen Kernpunkte, warum Kubernetes etcd verwendet:
- etcd ist in der Programmiersprache Go geschrieben, was mit dem k8s-Technologie-Stack übereinstimmt, einen geringen Ressourcenverbrauch hat und extrem einfach zu deployen ist.
- Die starke Konsistenz, Watch-, Lease- und andere Funktionen von etcd sind Kernabhängigkeiten von k8s.
Zusammenfassend ist etcd eine verteilte Schlüssel-Wert-Datenbank, die speziell für die Konfigurationsverwaltung und -verteilung entwickelt wurde. Als Cloud-nativer Software bietet es eine sofortige Nutzbarkeit und hohe Leistung, was es in diesem speziellen Bedarfsbereich traditionellen Datenbanken überlegen macht.
Um einen objektiven Vergleich zwischen etcd und PostgreSQL, zwei verschiedenen Arten von Datenbanken, zu ziehen, ist es wichtig, sie im Kontext der gleichen Anforderung zu bewerten. Daher wird dieser Artikel nur die Unterschiede zwischen den beiden in Bezug auf ihre Fähigkeit, die Anforderungen der Konfigurationsverwaltung zu erfüllen, diskutieren.
Datenmodell
Verschiedene Datenbanken haben unterschiedliche Datenmodelle, die sie den Benutzern präsentieren, und dieser Faktor bestimmt die Eignung der Datenbank für verschiedene Szenarien.
Schlüssel-Wert vs SQL
Das Schlüssel-Wert-Datenmodell ist ein beliebtes Modell in NoSQL, das auch von etcd übernommen wurde. Wie verhält sich dieses Modell im Vergleich zu SQL und was sind seine Vorteile?
Zunächst werfen wir einen Blick auf SQL.
Relationale Datenbanken verwalten Daten in Tabellen und bieten eine effiziente, intuitive und flexible Möglichkeit, strukturierte Informationen zu speichern und darauf zuzugreifen.
Eine Tabelle, auch als Relation bekannt, besteht aus Spalten, die eine oder mehrere Kategorien von Daten enthalten, und Zeilen, auch als Tabellendatensätze bekannt, die eine Reihe von Daten enthalten, die die Kategorien definieren. Anwendungen rufen Daten durch Abfragen ab, die Operationen wie "Projektion" zur Identifizierung von Attributen, "Selektion" zur Identifizierung von Tupeln und "Join" zur Kombination von Relationen verwenden. Das relationale Modell für die Verwaltung von Datenbanken wurde 1970 von Edgar Codd, einem Informatiker bei IBM, entwickelt.
Bildquelle (gemäß CC 3.0 BY-SA Lizenzvereinbarung): https://en.wikipedia.org/wiki/Associative_entity
Datensätze in einer Tabelle haben keine eindeutigen Identifikatoren, da Tabellen dafür ausgelegt sind, mehrere doppelte Zeilen aufzunehmen. Um Schlüssel-Wert-Abfragen zu ermöglichen, muss der Tabelle ein eindeutiger Index für das Feld hinzugefügt werden, das als Schlüssel dient. Der Standardindex von PostgreSQL ist btree, der, ähnlich wie etcd, Bereichsabfragen für Schlüssel durchführen kann.
Structured Query Language (SQL) ist eine Programmiersprache zum Speichern und Verarbeiten von Informationen in einer relationalen Datenbank. Eine relationale Datenbank speichert Informationen in tabellarischer Form, wobei Zeilen und Spalten verschiedene Datenattribute und die verschiedenen Beziehungen zwischen den Datenwerten darstellen. Sie können SQL-Anweisungen verwenden, um Informationen in der Datenbank zu speichern, zu aktualisieren, zu entfernen, zu suchen und abzurufen. Sie können SQL auch verwenden, um die Leistung der Datenbank zu warten und zu optimieren.
PostgreSQL hat SQL mit zahlreichen Erweiterungen erweitert, wodurch es zu einer Turing-vollständigen Sprache geworden ist. Dies bedeutet, dass SQL jede komplexe Operation ausführen kann, was die Ausführung von Datenverarbeitungslogik vollständig auf der Serverseite erleichtert.
Im Vergleich dazu ist etcd als Konfigurationsverwaltungstool konzipiert, wobei Konfigurationsdaten typischerweise als Hash-Tabelle dargestellt werden. Aus diesem Grund ist sein Datenmodell als Schlüssel-Wert-Format strukturiert, was effektiv eine einzige große globale Tabelle erzeugt. CRUD-Operationen können auf dieser Tabelle durchgeführt werden, die nur zwei Felder hat: einen eindeutigen Schlüssel mit Versionsinformationen und einen untypisierten Wert. Daher müssen Clients den vollständigen Wert für die weitere Verarbeitung abrufen.
Insgesamt vereinfacht die Schlüssel-Wert-Struktur von etcd SQL und ist für die spezifische Aufgabe der Konfigurationsverwaltung bequemer und intuitiver.
MVCC (Multi-Version Concurrency Control)
MVCC ist eine wesentliche Funktion für die Versionsverwaltung von Daten in der Konfigurationsverwaltung. Es ermöglicht:
- Abfragen historischer Daten
- Bestimmung des Alters von Daten durch Vergleich von Versionen
- Überwachen von Daten, was eine Versionsverwaltung erfordert, um inkrementelle Benachrichtigungen zu ermöglichen
Sowohl etcd als auch PostgreSQL haben MVCC, aber was sind die Unterschiede zwischen ihnen?
etcd verwendet einen global inkrementierenden 64-Bit-Versionszähler, um sein MVCC-System zu verwalten. Es besteht keine Sorge vor Überlauf. Der Zähler ist dafür ausgelegt, eine große Anzahl von Aktualisierungen zu verarbeiten, selbst wenn sie mit einer Rate von Millionen pro Sekunde auftreten. Jedes Mal, wenn ein Schlüssel-Wert-Paar erstellt oder aktualisiert wird, wird ihm eine Versionsnummer zugewiesen. Wenn ein Schlüssel-Wert-Paar gelöscht wird, wird ein Grabstein mit einer Versionsnummer erstellt, die auf 0 zurückgesetzt wird. Dies bedeutet, dass jede Änderung eine neue Version erzeugt, anstatt die vorherige zu überschreiben.
Darüber hinaus behält etcd alle Versionen eines Schlüssel-Wert-Paares bei und macht sie für Benutzer sichtbar. Die Schlüssel-Wert-Daten werden niemals überschrieben, und neue Versionen werden neben den bestehenden gespeichert. Die MVCC-Implementierung in etcd bietet auch eine Lese-Schreib-Trennung, die es Benutzern ermöglicht, Daten ohne Sperren zu lesen, was es für leseintensive Anwendungsfälle geeignet macht.
Die MVCC-Implementierung von PostgreSQL unterscheidet sich von der von etcd dadurch, dass sie nicht darauf abzielt, inkrementierende Versionsnummern bereitzustellen, sondern Transaktionen und verschiedene Isolationsstufen transparent für den Benutzer zu implementieren. MVCC ist ein optimistisches Sperrmechanismus, der gleichzeitige Aktualisierungen ermöglicht. Jede Zeile in einer Tabelle hat einen Transaktions-ID-Datensatz, wobei xmin
die Transaktions-ID der Zeilenerstellung und xmax
die Transaktions-ID der Zeilenaktualisierung darstellt.
- Transaktionen können nur Daten lesen, die bereits vor ihnen committet wurden.
- Beim Aktualisieren von Daten wird, wenn ein Versionskonflikt auftritt, PostgreSQL mit einem Matching-Mechanismus erneut versuchen, zu bestimmen, ob die Aktualisierung fortgesetzt werden soll.
Ein Beispiel finden Sie unter folgendem Link: https://devcenter.heroku.com/articles/postgresql-concurrency
Leider ist die Verwendung von Transaktions-IDs für die Versionskontrolle von Konfigurationsdaten in PostgreSQL aus mehreren Gründen nicht möglich:
- Transaktions-IDs werden allen Zeilen zugewiesen, die an derselben Transaktion beteiligt sind, was bedeutet, dass die Versionskontrolle nicht auf Zeilenebene angewendet werden kann.
- Historische Abfragen können nicht durchgeführt werden, und nur die neueste Version einer Zeile kann abgerufen werden.
- Aufgrund ihrer 32-Bit-Zähler-Natur sind Transaktions-IDs anfällig für Überlauf und werden während des Vakuumierens zurückgesetzt.
- Es ist nicht möglich, eine Watch-Funktionalität basierend auf Transaktions-IDs zu implementieren.
Daher benötigt PostgreSQL alternative Methoden für die Versionskontrolle von Konfigurationsdaten, da eine eingebaute Unterstützung nicht verfügbar ist.
Client-Schnittstelle
Das Design einer Client-Schnittstelle ist ein kritischer Aspekt, wenn es darum geht, die Kosten und den Ressourcenverbrauch im Zusammenhang mit ihrer Nutzung zu bestimmen. Durch die Analyse der Unterschiede zwischen Schnittstellen kann man fundierte Entscheidungen treffen, wenn es darum geht, die am besten geeignete Option auszuwählen.
Die kv/watch/lease-APIs von etcd haben sich als besonders geeignet für die Verwaltung von Konfigurationen erwiesen. Wie kann man diese APIs jedoch in PostgreSQL implementieren?
Leider bietet PostgreSQL keine eingebaute Unterstützung für diese APIs, und es ist eine Kapselung erforderlich, um sie zu implementieren. Um ihre Implementierung zu analysieren, werden wir das von mir entwickelte pg_watch_demo-Projekt untersuchen: pg_watch_demo.
gRPC/HTTP vs TCP
PostgreSQL folgt einer Multi-Prozess-Architektur, bei der jeder Prozess nur eine TCP-Verbindung gleichzeitig verarbeitet. Es verwendet ein benutzerdefiniertes Protokoll, um Funktionalität über SQL-Abfragen bereitzustellen, und folgt einem Anfrage-Antwort-Interaktionsmodell (ähnlich wie HTTP/1.1, das nur eine Anfrage gleichzeitig verarbeitet und Pipelining für die gleichzeitige Verarbeitung mehrerer Anfragen erfordert). Angesichts des hohen Ressourcenverbrauchs und der relativ geringen Effizienz ist ein Verbindungspool-Proxy (wie pgbouncer) entscheidend für die Verbesserung der Leistung, insbesondere in Szenarien mit hohem QPS.
Andererseits ist etcd auf einer Multi-Coroutine-Architektur in Golang entwickelt und bietet zwei benutzerfreundliche Schnittstellen: gRPC und RESTful. Diese Schnittstellen sind einfach zu integrieren und effizient in Bezug auf den Ressourcenverbrauch. Darüber hinaus kann jede gRPC-Verbindung mehrere gleichzeitige Abfragen verarbeiten, was eine optimale Leistung gewährleistet.
Daten definieren
etcd
message KeyValue {
bytes key = 1;
// Revisionsnummer, wenn der Schlüssel erstellt wurde
int64 create_revision = 2;
// Revisionsnummer, wenn der Schlüssel zuletzt geändert wurde
int64 mod_revision = 3;
// Inkrementierender Zähler, der sich jedes Mal erhöht, wenn der Schlüssel aktualisiert wird.
// Dieser Zähler wird auf Null zurückgesetzt, wenn der Schlüssel gelöscht wird, und wird als Grabstein verwendet.
int64 version = 4;
bytes value = 5;
// Das Lease-Objekt, das vom Schlüssel für TTL verwendet wird. Wenn der Wert 0 ist, gibt es keine TTL.
int64 lease = 6;
}
PostgreSQL
PostgreSQL muss eine Tabelle verwenden, um den globalen Datenraum von etcd zu simulieren:
CREATE TABLE IF NOT EXISTS config (
key text,
value text,
-- Entspricht `create_revision` und `mod_revision`
-- Hier wird ein großer Integer-Inkrementierungssequenztyp verwendet, um die Revision zu simulieren
revision bigserial,
-- Grabstein
tombstone boolean NOT NULL DEFAULT false,
-- Zusammengesetzter Index, Suche zuerst nach Schlüssel, dann nach Revision
primary key(key, revision)
);
get
etcd
Die get
-API von etcd hat eine breite Palette von Parametern:
- Bereichsabfragen, zum Beispiel wird durch Setzen von
key
auf/abc
undrange_end
auf/abd
alle Schlüssel-Wert-Paare mit/abc
als Präfix abgerufen. - Historische Abfragen, die
revision
oder einen Bereich vonmod_revision
angeben. - Sortierung und Begrenzung der Anzahl der zurückgegebenen Ergebnisse.
message RangeRequest {
...
bytes key = 1;
// Bereichsabfragen
bytes range_end = 2;
int64 limit = 3;
// Historische Abfragen
int64 revision = 4;
// Sortierung
SortOrder sort_order = 5;
SortTarget sort_target = 6;
bool serializable = 7;
bool keys_only = 8;
bool count_only = 9;
// Historische Abfragen
int64 min_mod_revision = 10;
int64 max_mod_revision = 11;
int64 min_create_revision = 12;
int64 max_create_revision = 13;
}
PostgreSQL
PostgreSQL kann die get-Funktion von etcd durch SQL ausführen und sogar komplexere Funktionalitäten bereitstellen. Da SQL selbst eine Sprache und keine festparametrische Schnittstelle ist, ist es äußerst vielseitig. Hier zeigen wir ein einfaches Beispiel für das Abrufen des neuesten Schlüssel-Wert-Paares. Da der Primärschlüssel ein zusammengesetzter Index ist, kann er schnell durch Bereichssuche durchsucht werden, was zu einer hohen Abrufgeschwindigkeit führt.
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;
// ob mit den Schlüssel-Wert-Paar-Daten vor der Aktualisierung durch diese `Put`-Anfrage geantwortet werden soll.
bool prev_kv = 4;
bool ignore_value = 5;
bool ignore_lease = 6;
}
PostgreSQL
Genau wie in etcd führt PostgreSQL Änderungen nicht direkt aus. Stattdessen wird eine neue Zeile eingefügt, und eine neue Revision wird ihr zugewiesen.
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
Ähnlich wie etcd ändert PostgreSQL Daten beim Löschen nicht direkt. Stattdessen wird eine neue Zeile eingefügt, wobei das Feld tombstone
auf true
gesetzt wird, um anzuzeigen, dass es sich um einen Grabstein handelt.
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;
// Gibt den Bereich der zu überwachenden Schlüssel an
bytes range_end = 2;
// Startrevision für die Überwachung
int64 start_revision = 3;
...
}
message WatchResponse {
ResponseHeader header = 1;
...
// Aus Effizienzgründen können mehrere Ereignisse zurückgegeben werden
repeated mvccpb.Event events = 11;
}
PostgreSQL
PostgreSQL verfügt nicht über eine eingebaute Watch-Funktion, und stattdessen ist eine Kombination aus Triggern und Kanälen erforderlich, um eine ähnliche Funktionalität zu erreichen. Durch die Verwendung von pg_notify
können Daten an alle Anwendungen gesendet werden, die auf einen bestimmten Kanal hören.
-- Triggerfunktion für die Verteilung von put/delete-Ereignissen
CREATE FUNCTION notify_config_change() RETURNS TRIGGER AS $$
DECLARE
data json;
channel text;
is_channel_exist boolean;
BEGIN
IF (TG_OP = 'INSERT') THEN
-- Verwenden Sie JSON zur Kodierung
data = row_to_json(NEW);
-- Extrahieren Sie den Kanalnamen für die Verteilung aus dem Schlüssel
channel = (SELECT SUBSTRING(NEW.key, '/(.*)/'));
-- Wenn eine Anwendung den Kanal überwacht, senden Sie ein Ereignis darüber
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; -- Das Ergebnis wird ignoriert, da dies ein AFTER-Trigger ist
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER notify_config_change
AFTER INSERT ON config
FOR EACH ROW EXECUTE FUNCTION notify_config_change();
Da die Watch-Funktion gekapselt ist, müssen Client-Anwendungen auch entsprechende Logik implementieren. Am Beispiel von Golang müssen die folgenden Schritte durchgeführt werden:
- Starten Sie das Zuhören: Wenn das Zuhören beginnt, werden alle Benachrichtigungsdaten sowohl auf PostgreSQL- als auch auf Golang-Kanal-Ebene zwischengespeichert.
- Rufen Sie alle Daten mit get_all(key_prefix, revision) ab: Diese Funktion liest alle vorhandenen Daten ab der angegebenen Revision. Für jeden Schlüssel wird nur die neueste Revisionsdaten zurückgegeben, wobei gelöschte Daten automatisch entfernt werden. Wenn keine Revision angegeben ist, werden die neuesten Daten für alle Schlüssel mit dem angegebenen
key_prefix
zurückgegeben. - Überwachen Sie neue Daten, einschließlich aller Benachrichtigungen, die zwischen dem ersten und zweiten Schritt zwischengespeichert wurden, um zu vermeiden, dass neue Daten, die in diesem Zeitfenster auftreten, übersehen werden. Ignorieren Sie alle Revisionen, die bereits im zweiten Schritt gelesen wurden.
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)
// Bei der erneuten Verbindung wird die Übertragung basierend auf der Revision vor der Trennung fortgesetzt.
str := fmt.Sprintf(`select * from get_all_from_rev_with_stale('/routes/', %d)`, latestRev)
rows, err := db.Query(str)
...
continue
}
...
// Pflegen Sie einen Zustand, der die neueste Revision aufzeichnet, die empfangen wurde
updateRoute(cfg)
case <-time.After(15 * time.Second):
log.Println("Received no events for 15 seconds, checking connection")
go func() {
// Wenn über einen längeren Zeitraum keine Ereignisse empfangen werden, überprüfen Sie die Gesundheit der Verbindung
if err := l.Ping(); err != nil {
log.Println("listener ping error: ", err)
}
}()
}
}
}
log.Println("get all routes...")
// Bei der Initialisierung sollte die Anwendung alle aktuellen Schlüssel-Wert-Paare abrufen und dann inkrementelle Updates durch watch überwachen
rows, err := db.Query(`select * from get_all('/routes/')`)
...
go watch(listener)
Transaktion
etcd
Transaktionen in etcd sind eine Sammlung von mehreren Operationen mit bedingten Überprüfungen, und die durch die Transaktion vorgenommenen Änderungen werden atomar committet.
message TxnRequest {
// Geben Sie die Ausführungsbedingung der Transaktion an
repeated Compare compare = 1;
// Operationen, die ausgeführt werden sollen, wenn die Bedingung erfüllt ist
repeated RequestOp success = 2;
// Operationen, die ausgeführt werden sollen, wenn die Bedingung nicht erfüllt ist
repeated RequestOp failure = 3;
}
PostgreSQL
Der DO
-Befehl in PostgreSQL ermöglicht die Ausführung beliebiger Befehle, einschließlich gespeicherter Prozeduren. Er unterstützt mehrere Sprachen, einschließlich eingebauter Sprachen wie PL/pgSQL und Python. Mit diesen Sprachen können beliebige bedingte Überprüfungen, Schleifen und andere Kontrolllogiken implementiert werden, was ihn vielseitiger macht als 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
In etcd ist es möglich, ein Lease-Objekt zu erstellen, das Anwendungen regelmäßig erneuern müssen, um ein Ablaufen zu verhindern. Jedes Schlüssel-Wert-Paar kann mit einem Lease-Objekt verknüpft werden, und wenn das Lease-Objekt abläuft, werden alle zugehörigen Schlüssel-Wert-Paare ebenfalls ablaufen und automatisch gelöscht.
message LeaseGrantRequest {
// TTL des Leases
int64 TTL = 1;
int64 ID = 2;
}
// Lease-Erneuerung
message LeaseKeepAliveRequest {
int64 ID = 1;
}
message PutRequest {
bytes key = 1;
bytes value = 2;
// Lease-ID, verwendet zur Implementierung von TTL
int64 lease = 3;
...
}
PostgreSQL
- In PostgreSQL kann ein Lease durch einen Fremdschlüssel verwaltet werden. Bei Abfragen wird, wenn ein zugehöriges Lease-Objekt abgelaufen ist, dies als Grabstein betrachtet.
- Keepalive-Anfragen aktualisieren den
last_keepalive
-Zeitstempel in der Lease-Tabelle.
CREATE TABLE IF NOT EXISTS config (
key text,
value text,
...
-- Verwenden Sie einen Fremdschlüssel, um das zugehörige Lease-Objekt anzugeben.
lease int64 references lease(id),
);
CREATE TABLE IF NOT EXISTS lease (
id text,
ttl int,
last_keepalive timestamp;
);
Leistungsvergleich
PostgreSQL muss verschiedene APIs von etcd durch Kapselung simulieren. Wie ist also seine Leistung? Hier sind die Ergebnisse eines einfachen Tests: https://github.com/kingluo/pg_watch_demo#benchmark.
Die Ergebnisse zeigen, dass die Lese- und Schreibleistung nahezu identisch sind, wobei PostgreSQL sogar etcd übertrifft. Darüber hinaus bestimmt die Latenz von einem Update bis zum Empfang des Ereignisses durch die Anwendung die Effizienz der Update-Verteilung, und sowohl PostgreSQL als auch etcd performen ähnlich. Bei Tests auf derselben Maschine für sowohl Client als auch Server betrug die Watch-Latenz weniger als 1 Millisekunde.
PostgreSQL hat jedoch einige Nachteile, die erwähnenswert sind:
- Das WAL-Protokoll für jedes Update ist größer, was zu doppelt so viel Festplatten-I/O führt wie bei etcd.
- Es verbraucht mehr CPU im Vergleich zu etcd.
- Notify basierend auf Kanälen ist ein Transaktionskonzept. Wenn derselbe Ressourcentyp aktualisiert wird, wird das Update an denselben Kanal gesendet, und die Update-Anfragen konkurrieren um gegenseitige Ausschlusssperren, was zu serialisierten Anfragen führt. Mit anderen Worten, die Verwendung von Kanälen zur Implementierung von watch beeinflusst die Parallelität von put-Operationen.
Dies zeigt, dass wir, um die gleichen Anforderungen zu erfüllen, mehr in das Lernen und Optimieren von PostgreSQL investieren müssen.
Speicher
Die Leistung wird durch den zugrunde liegenden Speicher bestimmt, und wie Daten gespeichert werden, bestimmt die Ressourcenanforderungen der Datenbank für Speicher, Festplatte und andere Ressourcen.
etcd
Architekturdiagramm des etcd-Speichers:
etcd schreibt Updates zunächst in das Write-Ahead-Log (WAL) und speichert sie auf der Festplatte, um sicherzustellen, dass die Updates nicht verloren gehen. Sobald das Protokoll erfolgreich geschrieben und von einer Mehrheit der Knoten bestätigt wurde, können die Ergebnisse an den Client zurückgegeben werden. etcd aktualisiert auch asynchron TreeIndex und BoltDB.
Um zu verhindern, dass das Protokoll unendlich wächst, erstellt etcd regelmäßig eine Momentaufnahme des Speichers, und Protokolle vor der Momentaufnahme können gelöscht werden.
etcd indiziert alle Schlüssel im Speicher (TreeIndex), zeichnet die Versionsinformationen jedes Schlüssels auf, behält jedoch nur einen Zeiger auf BoltDB (Revision) für den Wert.
Der Wert, der dem Schlüssel entspricht, wird auf der Festplatte gespeichert und mit BoltDB verwaltet.
Sowohl TreeIndex als auch BoltDB verwenden die btree-Datenstruktur, die für ihre Effizienz bei Such- und Bereichssuchen bekannt ist.
TreeIndex-Strukturdiagramm:
(Bildquelle: https://blog.csdn.net/H_L_S/article/details/112691481, lizenziert unter CC 4.0 BY-SA)
Jeder Schlüssel wird in verschiedene Generationen unterteilt, wobei jede Löschung das Ende einer Generation markiert.
Der Zeiger auf den Wert besteht aus zwei ganzen Zahlen. Die erste ganze Zahl main
ist die Transaktions-ID von etcd, während die zweite ganze Zahl sub
die Aktualisierungs-ID dieses Schlüssels innerhalb dieser Transaktion darstellt.
Boltdb unterstützt Transaktionen und Momentaufnahmen und speichert den Wert, der der Revision entspricht.
(Bildquelle: https://blog.csdn.net/H_L_S/article/details/112691481, lizenziert unter CC 4.0 BY-SA)
Beispiel für das Schreiben von Daten:
Schreiben von key="key1", revision=(12,1), value="keyvalue5"
. Beachten Sie die Änderungen in den roten Teilen von treeIndex und BoltDB:
(Bildquelle: https://blog.csdn.net/H_L_S/article/details/112691481, lizenziert unter CC 4.0 BY-SA)
Das Löschen von key="key", revision=(13,1)
erzeugt eine neue leere Generation in treeIndex und erzeugt einen leeren Wert in BoltDB mit key="13_1t"
.
Hier steht das t
für "tombstone". Dies bedeutet, dass Sie den Grabstein nicht lesen können, da der Zeiger in treeIndex (13,1)
ist, aber in BoltDB ist es 13_1t
, was nicht übereinstimmen kann.
(Bildquelle: https://blog.csdn.net/H_L_S/article/details/112691481, lizenziert unter CC 4.0 BY-SA)
Es ist erwähnenswert, dass etcd sowohl Lese- als auch Schreibvorgänge für BoltDB mit einer einzigen Goroutine plant, um zufällige Festplatten-I/O zu reduzieren und die I/O-Leistung zu verbessern.
PostgreSQL
Architekturdiagramm des PostgreSQL