
Tôt ou tard, les développeurs sont confrontés à la tâche de supprimer les données inutiles. Et plus le service est compliqué, plus vous devez prendre en compte les nuances. Dans cet article, je vais décrire comment nous avons implémenté la «suppression» dans une base de données avec des centaines de liens.
Contexte
Pour surveiller l'opérabilité de la plupart des projets,
Mail.ru Group et
VKontakte utilisent un service de leur propre développement -
Monitoring . Commençant son histoire à partir de la fin de 2012, le projet est devenu en 6 ans un énorme système, qui a gagné en fonctionnalités. La surveillance vérifie régulièrement la disponibilité des serveurs et l'exactitude des réponses aux demandes, collecte des statistiques sur la mémoire utilisée, l'utilisation du processeur, etc. Lorsque les paramètres du serveur surveillé dépassent les valeurs autorisées, les responsables du serveur reçoivent des notifications dans le système et par SMS.
Toutes les vérifications et les incidents sont enregistrés pour suivre la dynamique des performances du serveur, de sorte que la base de données a atteint l'ordre de centaines de millions d'enregistrements. De nouveaux serveurs apparaissent périodiquement et les anciens cessent d'être utilisés. Les informations sur les serveurs inutilisés doivent être supprimées du système de surveillance afin de:
a) ne pas surcharger l'interface avec des informations inutiles, et
b) libérer des identifiants uniques .
Effacer
J'ai sciemment dans le titre de l'article le mot «supprimer» écrit entre guillemets. Il existe plusieurs façons de supprimer un objet du système:
- complètement supprimé de la base de données;
- marquer les objets comme supprimés et se cacher de l'interface. En tant que marqueur, vous pouvez utiliser Boolean ou DateTime pour une journalisation plus précise.
Itération # 1
Initialement, la première approche a été utilisée, lorsque nous avons simplement exécuté
object.delete()
et que l'objet a été supprimé avec toutes les dépendances. Mais au fil du temps, nous avons dû abandonner cette approche, car un objet pouvait avoir des dépendances avec des millions d'autres objets, et la suppression en cascade des tables bloquées de manière rigide. Et comme le service effectue des milliers de contrôles chaque seconde et les enregistre, le blocage des tables a entraîné un sérieux ralentissement du service, ce qui était inacceptable pour nous.
Itération # 2
Pour éviter de longs verrous, nous avons décidé de supprimer les données par lots. Cela permettrait d'enregistrer des données de surveillance réelles dans les intervalles entre les suppressions d'objets. Une liste de tous les objets qui seront supprimés en cascade peut être obtenue par la méthode utilisée dans le panneau d'administration lors de la suppression d'un objet (lors de la confirmation de la suppression):
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()
La situation s'est améliorée: la charge a été répartie dans le temps, de nouvelles données ont commencé à être enregistrées plus rapidement. Mais nous avons immédiatement rencontré le prochain écueil. Le fait est que la liste des objets à supprimer est formée au tout début de la suppression, et si de nouveaux objets dépendants sont ajoutés pendant la suppression "portionnée", l'élément parent ne peut pas être supprimé.
Nous avons immédiatement abandonné l'idée d'une erreur de suppression récursive pour collecter à nouveau des données sur les nouvelles dépendances ou interdire l'ajout d'enregistrements dépendants lors de la suppression, car
a) vous pouvez entrer dans une boucle infinie ou
b) vous devez trouver tous les objets dépendants dans le code entier .
Itération # 3
Nous avons pensé au deuxième type de suppression, lorsque les données sont marquées et cachées de l'interface. Initialement, cette approche a été rejetée, car il semblait être une tâche pendant au moins une semaine pour trouver toutes les requêtes et ajouter un filtre pour l'absence d'un parent supprimé. En outre, il y avait une forte probabilité de manquer le code nécessaire, ce qui entraînerait des conséquences imprévisibles.
Ensuite, nous avons décidé d'utiliser des décorateurs pour remplacer le gestionnaire de requêtes. De plus, il vaut mieux voir le code que d'écrire cent mots.
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())
Le
exclude_objects_for_deleted_hosts(fields)
pour les champs spécifiés du modèle de champs ajoute automatiquement un filtre d'
exclude
pour chaque demande, qui supprime simplement les entrées qui ne doivent pas être affichées dans l'interface.
Maintenant, il suffit que tous les modèles qui seront affectés d'une manière ou d'une autre par la suppression ajoutent un décorateur:
@exclude_objects_for_deleted_hosts('host') class Alias(models.Model): host = models.ForeignKey(to=Host, verbose_name='Host', related_name='alias')
Maintenant, afin de supprimer l'objet
Host
, modifiez simplement l'attribut
is_deleted
:
host.is_deleted = True
Toutes les requêtes excluront automatiquement les enregistrements qui référencent des objets distants:
Il s'avère que la requête SQL suivante:
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)
Comme vous pouvez le voir, des jointures supplémentaires pour les champs spécifiés dans le décorateur et vérifie
`is_deleted` = TRUE
ajoutées à la demande.
Un peu de chiffres
Il est logique que des jointures et des conditions supplémentaires augmentent le temps d'exécution des requêtes. L'étude de cette question a montré que le degré de «complication» dépend de la structure de la base de données, du nombre d'enregistrements et de la présence d'indices.
Plus précisément, dans notre cas, pour chaque niveau de dépendance, la demande est condamnée à une amende d'environ 30%. Il s'agit de la pénalité maximale que nous obtenons sur la plus grande table avec des millions d'enregistrements; sur les petites tables, la pénalité est réduite à quelques pour cent. Heureusement, nous avons configuré les index nécessaires, et pour la majorité des requêtes critiques, les jointures nécessaires étaient déjà là, nous n'avons donc pas ressenti une grande différence de performances.
Identifiants uniques
Avant de supprimer des données, il peut être nécessaire de libérer les identifiants dont l'utilisation est prévue à l'avenir, car cela peut entraîner une erreur non unique lors de la création d'un nouvel objet. Malgré le fait qu'aucun objet ne sera visible dans l'application Django, ils seront toujours dans la base de données. Par conséquent, pour les objets supprimés, nous ajoutons uuid à l'identifiant.
host.hostname = '{}_{}'.format(host.hostname, uuid.uuid4()) host.is_deleted = True host.save()
Fonctionnement
Pour chaque nouveau modèle ou dépendance, le décorateur doit être mis à jour si nécessaire. Pour simplifier la recherche de modèles dépendants, nous avons écrit un test «intelligent»:
def test_deleted_host_decorator_for_models(self): def recursive_host_finder(model, cache, path, filters):
Le test vérifie récursivement tous les modèles pour la présence d'une dépendance sur le modèle à supprimer, puis il cherche à voir si le décorateur pour les champs requis a été défini pour ce modèle. Si quelque chose manque, le test vous indiquera délicatement où ajouter le décorateur.
Épilogue
Ainsi, avec l'aide d'un décorateur, il a été possible d'implémenter une «petite suppression» de données comportant un grand nombre de dépendances. Toutes les demandes reçoivent automatiquement le filtre d'
exclude
requis. L'imposition de conditions supplémentaires ralentit le processus d'obtention des données, le degré de «complication» dépend de la structure de la base de données, du nombre d'enregistrements et de la disponibilité des indices. Le test proposé vous indiquera pour quels modèles vous devez ajouter des décorateurs, et à l'avenir contrôlera leur cohérence.