CouchDB Datensätze zu Postgres migrieren (NoSQL zu SQL)

CouchDB Datensätze zu Postgres migrieren (NoSQL zu SQL)

Folgender Artikel stellt ein kleines Skript vor, welches kleine bis mittelgroße CouchDB NoSQL-Datenbanken zu einer PostgreSQL-Datenbank migriert.

Motivation: Kompatibilität und Ressourcenverbrauch

Ich entschied mich für ein kleines Projekt zur Speicherung von Sensordaten für CouchDB. Primär ging es darum, CouchDB kennen zu lernen. Darüber hinaus erschien mir die Datenbank schlang genug für einen Raspberry Pi. Wobei das auf andere Datenbanken auch zutrifft, im Nachhinein betrachtet sogar noch besser. Die Entscheidung beruhte aber nicht primär auf technischen Grundlagen. Vielmehr dachte ich mir: Für ein kleines Bastelprojekt bietet es sich doch gerade zu an, mal etwas neues auszuprobieren. Schauen wir mal, wie das damit funktioniert. Schließlich geht es nicht nur ums Ergebnis, sondern auch um den Weg.

Im Zuge des Projektes habe ich die Grundlagen von CouchDB kennen gelernt. Musste aber auch feststellen, dass diese Datenbank für mein Vorhaben aus zwei Gründen nicht optimal geeignet ist.

Kompatibilität mit Grafana

Anfänglich habe ich die Auswertungen der Daten noch komplett händisch durchgeführt. Wichtig war somit nur, serverseitig z.B. mit Python und einer Bibliothek auf die Daten zugreifen zu können. Das ist mit so ziemlich jeder gängigen Datenbank und Programmiersprache kein Problem. Das habe ich mit einem anderen RPI-Projekt ähnlich gehandhabt. Der Vorteil ist maximale Flexibilität, da alles angepasst werden kann. Bei anderen Projekten kam zudem noch eine Verwaltungsoberfläche dazu. Somit integrierte sich die Auswertung in zusätzliche Funktionalitäten.

Als ich mir Grafana etwas näher anschaute, stellte ich die Entscheidung für dieses Projekt infrage. Schließlich bot Grafana sämtliche Möglichkeiten der Visualisierung, sogar noch weitere darüber hinaus. Da keine zusätzlichen Funktionen geplant sind, schien es mir eine gute Idee, hierfür Grafana auszuprobieren. Schließlich gilt das Tool in Kombination mit Prometheus als Standard-OS Werkzeug fürs Monitorring. Es steht daher auch schon länger auf meiner Liste an auszuprobierenden interessanten Projekten. Mit meinen Sensoren hatte ich einen idealen Anwendungsfall, der mit ein paar einfachen Diagrammen überschaubar zum Einstieg geeignet scheint.

Allerdings gab es einen Haken: Grafana unterstützt viele Datenquellen, jedoch bisher nicht MongoDB. SQL-Datenbanken wie MySQL oder Postgres können dagegen direkt angesteuert werden. Der einfachste Weg schien mir daher, auf Postgres zu wechseln. So hatte ich zwar wieder eine SQL-Datenbank. Aber eine aufstrebende, auf die ich unabhängig davon auch gerne mal einen Blick werfen wollte.

Performance

Hinsichtlich der Datenbankabfragen würde sich grundsätzlich kein nennenswerter Vorteil bei NoSQL-Datenbanken geben. Dazu ist die Datenbankstruktur nicht komplex genug. Für ein produktives System wäre das im Rahmen der technisch begründeten Entscheidung ein Hauptargument, grundsätzlich auf NoSQL statt SQL zu setzen.

Da ich sowohl mit CouchDB als auch Postgre gearbeitet habe, konnte ich mir ein erstes Bild vom Ressourcenverbrauch der Datenbanken selbst machen. Auf einem Raspberry Pi mit begrenzten Ressourcen ist das ein durchaus wichtiges Kriterium. Zumal es sich um ein älteres Modell 3 handelte. Bei den neueren 4ern mit 2 GB, 4 GB oder sogar 8 GB Arbeitsspeicher kann das wieder etwas in den Hintergrund rücken.

Im Dauerbetrieb ist mir aufgefallen, dass CouchDB einen deutlich höheren Arbeitsspeicherverbrauch aufweist als Postgres. Obwohl Postgres zu diesem Zeitpunkt etwas mehr unter Last stand. Beide Datenbanken wurden per Cronjob stündlich mit einem Datensatz befüllt. Postgres war zusätzlich lesenden Anfragen durch Grafana ausgesetzt – mangels direkter Einbindungsmöglichkeit fanden diese bei CouchDB nicht statt.

Der Verbrauch von CouchDB war hierbei nahezu um den Faktor 8 höher, wie man an folgendem Beispiel sieht:

postgres_1      0.04%               7.34MiB / 926.1MiB    0.79%               1.97MB / 1.82MB     0B / 0B             11
couchdb_1       3.03%               55.04MiB / 256MiB     21.50%              4.45MB / 3.65MB     0B / 0B             62

Hier darf man sich nicht von den Limits irritieren lassen. Für Postgres ist aktuell noch keines gesetzt, CouchDB besitzt dagegen eine Grenze von maximal 256 MB. Relevant ist daher der effektive Verbrauch von 7,3 MB bei Postgres und satten 55 MB durch CouchDB.

Fairer weise muss man dazu sagen, dass selbst CouchDB nur knapp 6% des zur Verfügung stehenden Arbeitsspeichers nutzt. In diesem Falle bleibt somit genug Luft nach oben. Für mich war das nicht der entscheidende Grund, da es primär um die Anbindung an Grafana ging. Dennoch fand ich den Unterschied interessant. Auf einem Desktop-PC oder Server ist das zwar irrelevant, dort wird man es nicht mal merken. Für einen älteren RPI mit weniger RAM sieht es dagegen schon anders aus. Hier möchte man die Differenz vielleicht lieber für andere Anwendungen nutzen, oder zumindest als Puffer frei halten.

CouchDB zu Postgres mit Python3 migrieren

Da ich an dem Projekt nebenbei arbeite und mit dem Sammeln der Sensordaten begonnen hatte, befanden sich in der CouchDB bereits einige Messwerte. Darunter auch von kühleren Tagen, die sich bei den aktuellen Temperaturen nicht so schnell wieder sammeln lassen. Zwar keine essenziellen Daten, da ich diese auch später mit etwas Zeit erneut sammeln könnte. Aber da ich schon beide Datenbanken aus Python heraus ansteuerbar sind und es nur um wenige hundert Datensätze geht, kann ein kleines Migrationsskript nicht viel Aufwand sein.

Erstellen einer CouchDB-View

Um die Daten abzufragen, wird in CouchDB eine View benötigt. Sämtliche Details der Abfrage sind darin festgelegt. Sie ist am ehesten mit einer SQL-Prozedur vergleichbar. Allerdings mit einem Unterschied: Bei SQL hat man die Wahl, ob man die vollständige Abfrage clientseitig erstellen und übermitteln möchte – oder auf eine Prozedur zurückgreifen. Letzteres macht vor allem bei komplexen oder häufig genutzten Abfragen Sinn. Bei komplexen Abfragen entsteht weniger Datenverkehr zum SQL-Server. Häufig verwendete können auf Seiten des Servers zentral bereitgestellt werden.

Jedenfalls gibt es bei CouchDB keine Wahl. Auch für eine einfache SELECT * FROM xyz Abfrage muss daher eine extra View erstellt werden. Hier reicht eine einfache Map-Funktion, die das gesamte Dokument zurückliefert:

function(doc) { emit(doc.id, doc); }

Wer nicht extra zur Datenbank möchte, kann die View auch in Python anlegen:

_doc = {
   "_id": "_design/testing",
   "views": {
       "names": {
           "map": "function(doc) { emit(doc.id, doc); }"
       }
   }
}

doc = db.save(_doc)

Datenbanktreiber für CouchDB und Postgres installieren

Für CouchDB nutzen wir pycouchdb:

pip3 install pycouchdb

Im Falle von Postgres stellt psycopg2 eine aktiv weiterentwickelte Bibliothek bereit. Sie benötigt zwei Pakete als Abhängigkeiten. Unter Debian/Ubuntu können diese wie folgt installiert werden:

sudo apt install python3-dev libpq-dev

Anschließend folgt auch hier das dazugehörige Pip-Paket:

pip3 install psycopg2

Python-Skript für die Migration

Sind alle Abhängigkeiten installiert, können wir mit dem Kern der Migration beginnen. Der Aufbau des Skriptes ist einfach: Es lädt alle Datensätze aus der alten CouchDB. Anschließend wird für jeden Datensatz eine SQL-Abfrage erzeugt, womit wir das JSON-Dokument auf die neue Tabelle mappen.

import pycouchdb
import psycopg2
from datetime import datetime
from typing import NamedTuple
from collections import OrderedDict
from chirp_modbus import SoilMoistureSensor

server = pycouchdb.Server("http://127.0.0.1:5984/")

dbName = "sensors"
db = server.database(dbName)

# Zuvor erstellte Ansicht
res = list(db.query("testing/names"))

conn = psycopg2.connect(host="127.0.0.1",database="pms", user="postgres", password="xxx")
cur = conn.cursor()

for doc in res:
  row = doc['value']
  print(row)

  sql = """
    insert into sensors(soilMoisture, temp, dateTime)
    values(%s, %s, %s)
  """
  cur.execute(sql, (row['soilMoisture'], row['temp'], row['dateTime']))
  conn.commit()

cur.close()
conn.close()

Die Tabellen/Spalten sind natürlich auf den jeweiligen Einzelfall anzupassen. Darüber hinaus ist diese recht einfache Vorgehensweise nicht für große Datensätze geeignet: Sie lädt alle Datensätze in den RAM. Wer viele bzw. sehr große Dokumente migrieren möchte, sollte je nach zur Verfügung stehender Hardware ggf. lieber mit einem Puffer arbeiten. Statt der gesamten Datenbank wird so Schritt-für-Schritt immer nur eine bestimmte Anzahl an Daten geladen und migriert, bis alles übertragen wurde.

Leave a Reply