Objekte in Django "löschen"



Früher oder später stehen die Entwickler vor der Aufgabe, unnötige Daten zu entfernen. Und je komplizierter der Service, desto mehr Nuancen müssen Sie berücksichtigen. In diesem Artikel werde ich beschreiben, wie wir das "Löschen" in einer Datenbank mit Hunderten von Links implementiert haben.

Hintergrund


Um die Funktionsfähigkeit der meisten Projekte zu überwachen, verwenden Mail.ru Group und VKontakte einen eigenen Entwicklungsdienst - Monitoring . Das Projekt begann seine Geschichte ab Ende 2012 und hat sich über 6 Jahre zu einem riesigen System entwickelt, das viele Funktionen erhalten hat. Die Überwachung überprüft regelmäßig die Verfügbarkeit von Servern und die Richtigkeit der Antworten auf Anforderungen, sammelt Statistiken über den verwendeten Speicher, die CPU-Auslastung usw. Wenn die Parameter des überwachten Servers die zulässigen Werte überschreiten, erhalten die für den Server Verantwortlichen Benachrichtigungen im System und per SMS.

Alle Überprüfungen und Vorfälle werden protokolliert, um die Dynamik der Serverleistung zu verfolgen, sodass die Datenbank die Größenordnung von Hunderten von Millionen Datensätzen erreicht hat. In regelmäßigen Abständen werden neue Server angezeigt, und alte werden nicht mehr verwendet. Informationen über nicht verwendete Server müssen aus dem Überwachungssystem gelöscht werden, um: a) die Schnittstelle nicht mit unnötigen Informationen zu überlasten und b) eindeutige Kennungen freizugeben .

Löschen


Ich habe wissentlich in der Überschrift des Artikels das Wort "Löschen" in Anführungszeichen geschrieben. Es gibt verschiedene Möglichkeiten, ein Objekt aus dem System zu entfernen:

  • vollständig aus der Datenbank gelöscht;
  • Objekte als gelöscht markieren und vor der Benutzeroberfläche verstecken. Als Markierung können Sie Boolean oder DateTime für eine genauere Protokollierung verwenden.

Iteration # 1


Anfangs wurde der erste Ansatz verwendet, als wir einfach object.delete() ausführten und das Objekt mit allen Abhängigkeiten gelöscht wurde. Im Laufe der Zeit mussten wir diesen Ansatz jedoch aufgeben, da ein Objekt Abhängigkeiten von Millionen anderer Objekte aufweisen und das Löschen von Tabellen durch kaskadierende Löschvorgänge starr blockiert werden konnte. Und da der Dienst Tausende von Überprüfungen pro Sekunde durchführt und protokolliert, führte das Blockieren der Tabellen zu einer ernsthaften Verlangsamung des Dienstes, die für uns nicht akzeptabel war.

Iteration # 2


Um lange Sperren zu vermeiden, haben wir uns entschlossen, Daten stapelweise zu löschen. Dies würde es ermöglichen, tatsächliche Überwachungsdaten in den Intervallen zwischen dem Löschen von Objekten aufzuzeichnen. Eine Liste aller Objekte, die in der Kaskade gelöscht werden, kann mit der Methode abgerufen werden, die im Admin-Bereich beim Löschen eines Objekts (beim Bestätigen des Löschvorgangs) verwendet wird:

 from django.contrib.admin.util import NestedObjects from django.db import DEFAULT_DB_ALIAS collector = NestedObjects(using=DEFAULT_DB_ALIAS) collector.collect([obj]) objects_to_delete = collector.nested() # Recursive delete objects 

Die Situation verbesserte sich: Die Last wurde über die Zeit verteilt, neue Daten wurden schneller aufgezeichnet. Aber wir sind sofort in die nächste Falle geraten. Tatsache ist, dass die Liste der zu löschenden Objekte ganz am Anfang des Löschvorgangs erstellt wird. Wenn beim abhängigen Löschen neue abhängige Objekte hinzugefügt werden, kann das übergeordnete Element nicht gelöscht werden.

Wir haben die Idee eines Fehlers beim rekursiven Löschen sofort aufgegeben, um erneut Daten über neue Abhängigkeiten zu sammeln oder das Hinzufügen abhängiger Datensätze beim Löschen zu verbieten, da a) Sie in eine Endlosschleife gehen können oder b) Sie alle abhängigen Objekte im gesamten Code finden müssen .

Iteration # 3


Wir haben über die zweite Art des Löschens nachgedacht, wenn Daten markiert und vor der Schnittstelle verborgen werden. Anfänglich wurde dieser Ansatz abgelehnt, da es mindestens eine Woche lang als Aufgabe erschien, alle Abfragen zu finden und einen Filter für das Fehlen eines gelöschten übergeordneten Elements hinzuzufügen. Darüber hinaus bestand eine hohe Wahrscheinlichkeit, dass der erforderliche Code fehlte, was zu unvorhersehbaren Konsequenzen führen würde.

Dann haben wir uns entschieden, Dekoratoren zu verwenden, um den Abfrage-Manager zu überschreiben. Außerdem ist es besser, den Code zu sehen, als hundert Wörter zu schreiben.

 def exclude_objects_for_deleted_hosts(*fields): """ Decorator that adds .exclude({field__}is_deleted=True) for model_class.objects.get_queryset :param fields: fields for exclude condition """ def wrapper(model_class): def apply_filters(qs): for field in filter_fields: qs = qs.exclude(**{ '{}is_deleted'.format('{}__'.format(field) if field else ''): True, }) return qs model_class.all_objects = copy.deepcopy(model_class.objects) filter_fields = set(fields) get_queryset = model_class.objects.get_queryset model_class.objects.get_queryset = lambda: apply_filters(get_queryset()) # save info about model decorator setattr(model_class, DECORATOR_DEL_HOST_ATTRIBUTE, filter_fields) return model_class return wrapper 

Der exclude_objects_for_deleted_hosts(fields) für die angegebenen Felder des exclude_objects_for_deleted_hosts(fields) fügt automatisch für jede Anforderung einen exclude hinzu, der nur Einträge entfernt, die nicht in der Benutzeroberfläche angezeigt werden sollen.

Jetzt reicht es für alle Modelle, die in irgendeiner Weise von der Löschung betroffen sind, aus, einen Dekorateur hinzuzufügen:

 @exclude_objects_for_deleted_hosts('host') class Alias(models.Model): host = models.ForeignKey(to=Host, verbose_name='Host', related_name='alias') 

Um das Host Objekt zu entfernen, ändern Sie is_deleted Attribut is_deleted :

 host.is_deleted = True # after this save the host and all related objects will be inaccessible host.save() 

Bei allen Abfragen werden automatisch Datensätze ausgeschlossen, die auf entfernte Objekte verweisen:

 # model decorator @exclude_objects_for_deleted_hosts('checker__monhost', 'alias__host') CheckerToAlias.objects.filter( alias__hostname__in=['cloud.spb.s', 'cloud.msk.s'] ).values('id') 

Es stellt sich die folgende SQL-Abfrage heraus:

 SELECT monitoring_checkertoalias.id FROM monitoring_checkertoalias INNER JOIN monitoring_checker ON (`monitoring_checkertoalias`.`checker_id` = monitoring_checker.`id`) INNER JOIN Hosts ON (`monitoring_checker`.`monhost_id` = Hosts.`id`) INNER JOIN dcmap_alias ON (`monitoring_checkertoalias`.`alias_id` = dcmap_alias.`id`) INNER JOIN Hosts T5 ON (`dcmap_alias`.`host_id` = T5.`id`) WHERE ( NOT (`Hosts`.`is_deleted` = TRUE) -- ,   monitoring_checker AND NOT (T5.`is_deleted` = TRUE) -- ,   dcmap_alias AND dcmap_alias.name IN ('dir1.server.p', 'dir2.server.p') ); 

Wie Sie sehen können, `is_deleted` = TRUE der Anforderung zusätzliche `is_deleted` = TRUE für die im Dekorator angegebenen Felder und Überprüfungen auf `is_deleted` = TRUE hinzugefügt.

Ein bisschen über Zahlen


Es ist logisch, dass zusätzliche Verknüpfungen und Bedingungen die Ausführungszeit der Abfrage verlängern. Die Studie zu diesem Thema hat gezeigt, dass der Grad der „Komplikation“ von der Struktur der Datenbank, der Anzahl der Datensätze und dem Vorhandensein von Indizes abhängt.

In unserem Fall wird für jede Abhängigkeitsstufe eine Geldstrafe von etwa 30% verhängt. Dies ist die maximale Strafe, die wir am größten Tisch mit Millionen von Datensätzen erhalten, bei kleineren Tischen wird die Strafe auf einige Prozent reduziert. Glücklicherweise haben wir die erforderlichen Indizes konfiguriert, und für die meisten kritischen Abfragen waren die erforderlichen Verknüpfungen bereits vorhanden, sodass wir keinen großen Unterschied in der Leistung verspürten.

Eindeutige Kennungen


Vor dem Löschen von Daten müssen möglicherweise Bezeichner freigegeben werden, deren Verwendung in Zukunft geplant ist, da dies beim Erstellen eines neuen Objekts zu einem nicht eindeutigen Fehler führen kann. Trotz der Tatsache, dass in der Django-Anwendung keine Objekte sichtbar sind, befinden sie sich weiterhin in der Datenbank. Daher hängen wir für gelöschte Objekte die UUID an den Bezeichner an.

 host.hostname = '{}_{}'.format(host.hostname, uuid.uuid4()) host.is_deleted = True host.save() 

Bedienung


Für jedes neue Modell oder jede neue Abhängigkeit muss der Dekorateur bei Bedarf aktualisiert werden. Um die Suche nach abhängigen Modellen zu vereinfachen, haben wir einen „intelligenten“ Test geschrieben:

 def test_deleted_host_decorator_for_models(self): def recursive_host_finder(model, cache, path, filters): # cache for skipping looked models cache.add(model) # process all related models for field in (f for f in model._meta.fields if isinstance(f, ForeignKey)): if field.related_model == Host: filters.add(path + field.name) elif field.related_model not in cache: recursive_host_finder(field.related_model, cache.copy(), path + field.name + '__', filters) # check all models for current_model in apps.get_models(): model_filters = getattr(current_model, DECORATOR_DEL_HOST_ATTRIBUTE, set()) found_filters = set() if current_model == Host: found_filters.add('') else: recursive_host_finder(current_model, set(), '', found_filters) if found_filters or model_filters: try: self.assertSetEqual(model_filters, found_filters) except AssertionError as err: err.args = ( '{}\n !!! Fix decorator "exclude_objects_for_deleted_hosts" ' 'for model {}'.format(err.args[0], current_model), ) raise err 

Der Test überprüft rekursiv alle Modelle auf das Vorhandensein einer Abhängigkeit von dem zu löschenden Modell. Anschließend wird geprüft, ob der Dekorator für die erforderlichen Felder für dieses Modell festgelegt wurde. Wenn etwas fehlt, sagt Ihnen der Test genau, wo Sie den Dekorateur hinzufügen müssen.

Nachwort


So war es mit Hilfe eines Dekorateurs möglich, ein „kleines Löschen“ von Daten mit einer großen Anzahl von Abhängigkeiten zu implementieren. Alle Anfragen erhalten automatisch den erforderlichen exclude . Das Auferlegen zusätzlicher Bedingungen verlangsamt den Prozess der Datenerfassung. Der Grad der „Komplikation“ hängt von der Struktur der Datenbank, der Anzahl der Datensätze und der Verfügbarkeit von Indizes ab. Der vorgeschlagene Test zeigt Ihnen, für welche Modelle Sie Dekorateure hinzufügen müssen, und überwacht in Zukunft deren Konsistenz.

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


All Articles