Box zum Speichern von Daten in Go-Anwendungen

Bild

Eine kurze Notiz über eine eingebettete Schlüsselwert-Datenbank namens Coffer geschrieben in Golang. Kurz gesagt: Wenn die Datenbank angehalten ist, befinden sich die Daten auf dem Datenträger. Wenn sie gestartet werden, werden die Daten in den Speicher kopiert. Lesen kommt aus dem Gedächtnis. Während der Aufzeichnung werden die Speicherdaten geändert und die Änderungen in das Festplattenprotokoll geschrieben. Die maximale Größe der gespeicherten Daten ist durch die Größe des RAM begrenzt. Mit der API können Sie Header für Datenbankdatensätze erstellen und in Transaktionen verwenden, wobei die Datenkonsistenz erhalten bleibt.

Aber zuerst eine kleine lyrische Einführung. Es war einmal, als das Gras grüner war, und ich musste eine Schlüsselwert-Datenbank für die Go-Anwendung einbetten. Als ich mich umsah und auf verschiedene Pakete stieß, fand ich irgendwie nicht das, was ich (subjektiv) haben wollte, und habe die Lösung einfach mit einer externen relationalen Datenbank angewendet. Tolle funktionierende Lösung. Aber wie sie sagen, wurde ein Löffel gefunden, aber das Sediment blieb. Zunächst wollte ich genau die native, auf Go geschriebene Datenbank, direkt native-native. Und es gibt solche, die einfach toll aussehen. Es gibt jedoch nicht eine Million von ihnen. Dies ist sogar überraschend, wenn man bedenkt, dass ein Programmierer auf der Welt selten ist, der in seinem Leben keine Datenbank, kein Framework oder kein Casual Game geschrieben hat.


Nun, Sie können versuchen, Ihr Fahrrad mit Blackjack und anderen Extras auf Ihr Knie zu stapeln. Gleichzeitig weiß oder ahnt jeder, dass das Schreiben einer einfachen Schlüsselwertdatenbank nur auf den ersten Blick einfach erscheint. Tatsächlich macht aber alles viel mehr Spaß (und es ist passiert). Und ich war neugierig auf ACID und machte mir Sorgen um Transaktionen. Echte Transaktionen sind eher im finanziellen Sinne, weil Ich war damals bei Fintech beschäftigt.


Datensicherheit


Betrachten Sie den Fall, dass während des Betriebs einer Anwendung mit aktiver Aufzeichnung die Stromversorgung im Computer mit einem Kupferbecken abgedeckt war und die Festplatte nicht brach. Wenn zu diesem Zeitpunkt die Anwendung von der Datenbank ok empfangen wurde, gehen die Daten aus dieser Operation nicht verloren. Wenn der Antrag eine negative Antwort erhielt, wurde der Vorgang natürlich nicht abgeschlossen. In dem Fall, in dem die Anwendung eine Anforderung gesendet hat, aber keine Antwort erhalten hat: Dieser Vorgang wurde höchstwahrscheinlich nicht abgeschlossen, es besteht jedoch eine geringe Wahrscheinlichkeit, dass der Vorgang in das Protokoll aufgenommen wurde. Genau zum Zeitpunkt des Versendens der Antwort ist jedoch ein Stromausfall aufgetreten.


Wie kann man im letzten Fall herausfinden, was mit den letzten Operationen passiert ist? Das ist eine interessante Frage. Indirekt können Sie es erraten (Schlussfolgerungen ziehen), indem Sie den Wert des interessierenden Datensatzes nach dem Start einer neuen Anwendung aus der Datenbank betrachten. Wenn die Operationen jedoch häufig genug sind, wird das leider nicht helfen. Sie können die Datei des letzten Protokolls sehen (es wird die höchste Nummer haben), aber manuell ist dies unpraktisch. Ich denke, in Zukunft können Sie die Möglichkeit hinzufügen, Protokolle in der API anzuzeigen (natürlich sollten die Protokolle in diesem Fall nicht gelöscht werden).


Ehrlich gesagt, ich selbst habe das Kabel nicht aus der Steckdose gezogen, weil Ich möchte kein Eisen riskieren, um die Datenbank zu überprüfen. In Tests verderbe ich nur normale Protokolldateien, und in diesem Fall geschieht alles so, wie ich es erwartet hatte. Es liegen jedoch keine Erfahrungen mit der praktischen Verwendung der Datenbank vor, sie hat am Produkt nicht funktioniert, und es bestehen Risiken. Ich denke jedoch, dass die Datenbank für Haustierprojekte ziemlich furchtlos verwendet werden kann. Generell gelten die üblichen Haftungsausschlüsse, keine Garantien.


Die Datenbank ist derzeit nicht vor der Verwendung in zwei verschiedenen Anwendungen geschützt (oder dasselbe, spielt hier keine Rolle), die für die Arbeit mit demselben Verzeichnis konfiguriert sind. Bitte berücksichtigen Sie diesen Moment! Da die Datenbank bereits integriert ist und sie dann in den Argumenten als Referenztyp übergeben wird, lohnt es sich auf keinen Fall, sie irgendwo in der Parallel-Goroutine zu ändern.



Konfiguration


Die Datenbank verfügt über eine Reihe konfigurierbarer Parameter, von denen jedoch fast alle Standardwerte haben. cof, err, wrn := Db(dirPath).Create() kann alles in eine kurze Zeile passen cof, err, wrn := Db(dirPath).Create() wird cof, err, wrn := Db(dirPath).Create() Fehler zurückgegeben (wenn ein Fehler auftritt, arbeiten Sie mit der Datenbank weiter verboten) und eine Warnung, über die Sie Bescheid wissen können, die jedoch den Betrieb der Datenbank nicht beeinträchtigt.


Ich werde den Text nicht mit umfangreichen Beschreibungen überfrachten, wenn nötig, sieh sie dir bitte in der Readme-Datei des Projektarchivs an - github.com/claygod/coffer/blob/master/README_RU.md#config Achten Sie auf die Handler-Methode, die den Handler für die Transaktion verbindet. Ich werde ein paar Zeilen darüber schreiben Unten liste ich sie nur auf:


  • Db (dirPath)
  • BatchSize (batchSize)
  • LimitRecordsPerLogfile (limitRecordsPerLogfile)
  • FollowPause (100 * time.Second)
  • LogsByCheckpoint (1000)
  • AllowStartupErrLoadLogs (true)
  • MaxKeyLength (maxKeyLength)
  • MaxValueLength (maxValueLength)
  • MaxRecsPerOperation (1.000.000)
  • RemoveUnlessLogs (true)
  • LimitMemory (100 * 1.000.000)
  • LimitDisk (1000 * 1.000.000)
  • Handler ("handler1", & handler1)
  • Handler ("handler2", & handler2)
  • Handler (map [string] * handler)
  • Erstellen ()

API


Ich habe die API so weit wie möglich vereinfacht und für eine Schlüsselwertbasis sollten Sie nicht zu schlau sein:


  • Start - Starten Sie die Datenbank
  • Stop - Stoppt die Datenbank
  • StopHard - ein Stopp, unabhängig davon, welche Vorgänge gerade ausgeführt werden (möglicherweise werde ich ihn entfernen)
  • Speichern - Speichert eine Momentaufnahme des aktuellen Status der Datenbank
  • Schreiben - einen Datensatz zur Datenbank hinzufügen
  • WriteList - Mehrere Datensätze zur Datenbank hinzufügen (strikter und optionaler Modus)
  • WriteListUnsafe - Hinzufügen mehrerer Datensätze zur Datenbank, ohne Rücksicht auf die Datensicherheit
  • Lesen - Holen Sie sich einen Datensatz per Schlüssel
  • ReadList - Liste der Datensätze abrufen
  • ReadListUnsafe - Liste der Datensätze ohne Berücksichtigung der Datensicherheit abrufen
  • Löschen - Löscht einen Datensatz
  • DeleteList - Löscht mehrere Datensätze im strengen / optionalen Modus
  • Transaktion - Führen Sie eine Transaktion aus
  • Anzahl - wie viele Datensätze in der Datenbank
  • CountUnsafe - wie viele Datensätze in der Datenbank (etwas schneller, aber unsicher)
  • RecordsList - eine Liste aller Datenbankschlüssel
  • RecordsListUnsafe - eine Liste aller Datenbankschlüssel (etwas schneller, aber unsicher)
  • RecordsListWithPrefix - eine Liste von Schlüsseln mit dem angegebenen Präfix
  • RecordsListWithSuffix - eine Liste von Schlüsseln mit dem angegebenen Ende

Kurze Erläuterungen zur API:


  • Strenger Modus - alles oder nichts.
  • Optionaler Modus - alles tun, was funktioniert.
  • StopHard - Vielleicht sollte diese Methode aus der API entfernt werden, bis entschieden wird.
  • Alle RecordsList-Methoden sind nicht schnell, weil Derzeit befinden sich keine Indizes im Speicher, während dies ein vollständiger Scan ist.
  • Alle unsicheren Methoden sind schneller, aber die Konsistenz wird bei ihrer Verwendung nicht vorausgesetzt. Es ist logisch, sie für eine gestoppte Datenbank zu verwenden, um sie schnell zu füllen, oder für etwas Ähnliches.
  • Der Follower überwacht die regelmäßige Aktualisierung des Datenbank-Snapshots. Daher ist die Save-Methode für einige Sonderfälle am wahrscheinlichsten, in denen Sie definitiv einen neuen Snapshot erstellen möchten (bis mir ein solcher Fall in den Sinn kommt, aber vielleicht ist dies der Fall).

Ein einfacher Anwendungsfall:

 package main import ( "fmt" "github.com/claygod/coffer" ) const curDir = "./" func main() { // STEP init db, err, wrn := coffer.Db(curDir).Create() switch { case err != nil: fmt.Println("Error:", err) return case wrn != nil: fmt.Println("Warning:", err) return } if !db.Start() { fmt.Println("Error: not start") return } defer db.Stop() // STEP write if rep := db.Write("foo", []byte("bar")); rep.IsCodeError() { fmt.Sprintf("Write error: code `%d` msg `%s`", rep.Code, rep.Error) return } // STEP read rep := db.Read("foo") rep.IsCodeError() if rep.IsCodeError() { fmt.Sprintf("Read error: code `%v` msg `%v`", rep.Code, rep.Error) return } fmt.Println(string(rep.Data)) } 

Transaktionen


Wie oben erwähnt, stimmt meine Definition von Transaktionen möglicherweise nicht mit der im DB-Bau allgemein akzeptierten überein, vielleicht werden sie nur durch eine Idee vereint. In einer bestimmten Implementierung ist eine Transaktion ein bestimmter Header, der in der Datenbankkonfigurationsphase festgelegt wurde ( Handler Methode). Wenn wir eine Transaktion mit diesem Header aufrufen, blockiert die Datenbank die Datensätze, mit denen der Header arbeitet, und überträgt ihre aktuellen Werte in den Header. Der Header bearbeitet diese Daten nach Bedarf und gibt die neuen Werte der Datenbank zurück. Dadurch werden sie in hundert gespeichert. Danach werden die Datensätze entsperrt und stehen für andere Vorgänge zur Verfügung.


Es gibt Beispiele im Repo, die zeigen, wie wichtig es ist, Transaktionen sehr gut zu nutzen. Aus Neugier machte ich ein kleines finanzielles Beispiel, in dem es um Debit- und Kreditgeschäfte, Überweisung, Kauf und Verkauf geht. Dieses Beispiel war sehr einfach zu schreiben, und gleichzeitig ist diese kniehohe Implementierung ziemlich konsistent und für den Einsatz in verschiedenen Finanzlösungen oder zum Beispiel in der Logistik geeignet.


Ein wichtiger Punkt: Der Handler-Code wird nicht in der Datenbank gespeichert. Ich hatte die Idee, es in einem Protokoll zu speichern, aber es schien mir zu verschwenderisch, sodass ich es nicht komplizierte, und dementsprechend liegt die Verantwortung für die Konsistenz der Handler zwischen verschiedenen Datenbankstarts beim Entwickler des Codes, der die Datenbank verwendet. Handler können definitiv nicht geändert werden, wenn die Anwendung und die Datenbank nicht mehr abstürzen. In diesem Fall müssen Sie die Datenbank zuerst starten und dann regelmäßig stoppen - ein neuer Datenschnappschuss wird erstellt. Um nicht durcheinander zu kommen, empfehle ich Ihnen, die Versionsnummer im Namen der Handler zu verwenden.



Antworten empfangen und verarbeiten


Die Datenbank gibt Berichte mit dem Status der Antwort und den Daten zurück. Da es viele Codes gibt und das Schreiben eines Schalters mit der Verarbeitung jedes Codes mühsam ist, sollten Sie nach ca. Dies sollte nicht getan werden. Tatsache ist, dass der Code den Status Ok, Error, Panic haben kann. Mit Ok ist alles klar, aber was ist mit den anderen beiden? Wenn der Status Fehler lautet, ist ein bestimmter Vorgang abgeschlossen oder nicht abgeschlossen. Dieser Fehler muss in der Anwendung entsprechend behandelt werden. Weiteres Arbeiten mit der Datenbank ist jedoch möglich (und notwendig). Eine andere Sache Panik - die Arbeit mit der Datenbank sollte eingestellt werden.


Durch IsCodeError die Arbeit mit allen Fehlern vereinfacht. Wenn Sie also nicht an den Details interessiert sind, fahren Sie mit der Arbeit fort.
Die IsCodePanic Prüfung deckt alle Fälle ab, in denen die Arbeit mit der Datenbank gestoppt werden muss.


Im einfachen Fall reicht ein Triple-Switch aus, um die Antwort zu verarbeiten:


  • IsCodeOk - weiter wie IsCodeOk
  • IsCodeError - protokolliert den Fehler aus dem Bericht und arbeitet weiter
  • IsCodePanic - Protokolliert den Fehler aus dem Bericht und beendet die Arbeit mit der Datenbank

Offtop


Für den Namen wurde eine der Optionen zum Übersetzen der ins Englische gewählt, ich würde natürlich die box bevorzugen, aber dies ist ein zu beliebtes Wort, ich hoffe, dass die coffer auch coffer .
Das Thema mit ACID scheint mir eher ganzheitlich zu sein, daher würde ich sagen, dass Coffer dem verpflichtet ist, aber keine Tatsache, und ich behaupte nicht, dass es ihm gelungen ist.



Leistung


Ich habe sofort eine Datenbank geschrieben, die Parallelität und Konkurrenz berücksichtigt. In diesem Modus zeigt es seine Wirksamkeit (obwohl dies wahrscheinlich zu laut gesagt wird). In den folgenden Ergebnissen zeigt der Benchmark eine Bandbreite von 200.000 U / min. Dies ist natürlich eine künstliche Bank, und die Realität wird völlig anders sein, weil Vieles hängt von der Größe der aufgezeichneten Daten, der Menge der bereits aufgezeichneten Daten, der Leistung von Eisen und der Mondphase ab. Aber der Trend ist zumindest verständlich. Wenn die Datenbank im Singlethread-Modus verwendet wird, wird jede Anforderung erst ausgeführt, nachdem die Antwort auf die vorherige empfangen wurde. Die Geschwindigkeit ist gering. Ich rate Ihnen, sich andere Datenbanken anzusehen, Coffer jedoch nicht.


  • BenchmarkCofferTransactionSequence-4 2000 227928 ns / op
  • BenchmarkCofferTransactionPar32HalfConcurent-4 100000 4199 ns / op

Übrigens, wenn jemand Zeit mit Coffer verbringt und zu sich selbst neigt, lassen Sie die Bank, die darin liegt, wenn möglich laufen. Mich interessiert sehr, auf welchen Rechnern die Performance die Datenbank anzeigt. Zuallererst hängt natürlich alles von der Festplatte ab. Dies wurde mir besonders deutlich, nachdem ich kürzlich ein neues Samsung EVO gekauft hatte. Aber keine Sorge, dies ist kein Ersatz für eine tote Festplatte. Das alte Toshiba wird weiterhin ordnungsgemäß bedient und speichert jetzt mein Videoarchiv.


Die eingebaute In-Memory-Uhr ist immer noch eine einfache Karte, die nicht einmal in Abschnitte unterteilt ist. Natürlich kann es großartig sein, es zu verbessern, um beispielsweise die Auswahl von Schlüsseln durch Präfixe und Suffixe zu beschleunigen. Während ich das nicht tat, tk. Die Hauptfunktionalität, wie ich den DB-Chip sage, den ich bei Transaktionen sehe, und der Leistungsengpass bei Transaktionen werden mit der Festplatte und nur dann mit dem Speicher funktionieren.



Lizenz


Jetzt können Sie mit der Lizenz bis zu zehn Millionen Datensätze in der Datenbank speichern. Dies scheint mir eine ausreichende Anzahl zu sein. Weitere Pläne für den Aufbau der Datenbank sind in Vorbereitung.
Im Allgemeinen ist es für mich interessant, die Datenbank als Paket zu verwenden und mich in erster Linie auf ihre API zu konzentrieren.



Schlussfolgerungen


In letzter Zeit stoße ich häufig auf die Aufgabe, Dienste mit der Eigenschaft hoher Verfügbarkeit zu schreiben. Leider lohnt es sich in einem solchen Fall nicht, die integrierte Datenbank zu verwenden, da dies fast immer das Vorhandensein mehrerer Instanzen impliziert. Es bleibt die Option einer regulären Anwendung oder eines regulären Dienstes, der in einer Instanz vorhanden ist. Es scheint mir ein seltener Fall zu sein, aber es ist dennoch so, und in einem solchen Fall ist es schön, eine Datenbank zu haben, die so weit wie möglich versucht, die darin gespeicherten Daten zu speichern. Der von mir erstellte Koffer versucht, ein solches Problem zu lösen. Mal sehen, wie er es macht.



Danksagung


  • An alle, die den Artikel bis zum Ende gelesen haben
  • Kommentatoren, die ihre Meinung mitteilen möchten
  • Senden Sie eine persönliche Information über Tippfehler und Fehler im Text
  • Nachbar, der nachts Musik einschaltet

Referenzen


DB-Repository
Beschreibung in russischer Sprache

Source: https://habr.com/ru/post/de477154/


All Articles