
Tarde o temprano, los desarrolladores se enfrentan a la tarea de eliminar datos innecesarios. Y cuanto más complicado sea el servicio, más matices debe tener en cuenta. En este artículo, describiré cómo implementamos la "eliminación" en una base de datos con cientos de enlaces.
Antecedentes
Para monitorear la operabilidad de la mayoría de los proyectos,
Mail.ru Group y
VKontakte usan un servicio de su propio desarrollo:
Monitoreo . Comenzando su historia desde finales de 2012, durante más de 6 años, el proyecto se ha convertido en un gran sistema, que ha ganado mucha funcionalidad. El monitoreo verifica regularmente la disponibilidad de servidores y la exactitud de las respuestas a las solicitudes, recopila estadísticas sobre la memoria utilizada, la utilización de la CPU, etc. Cuando los parámetros del servidor monitoreado exceden los valores permitidos, los responsables del servidor reciben notificaciones en el sistema y por SMS.
Todas las verificaciones e incidentes se registran para rastrear la dinámica del rendimiento del servidor, por lo que la base de datos ha alcanzado el orden de cientos de millones de registros. Periódicamente aparecen nuevos servidores, y los viejos dejan de usarse. La información sobre los servidores no utilizados debe eliminarse del sistema de Monitoreo para:
a) no sobrecargar la interfaz con información innecesaria, yb) liberar identificadores únicos .
Eliminar
A sabiendas, en el encabezado del artículo, la palabra "eliminar" escribió entre comillas. Hay varias formas de eliminar un objeto del sistema:
- completamente eliminado de la base de datos;
- marcar objetos como eliminados y esconderse de la interfaz. Como marcador, puede usar Boolean o DateTime para un registro más preciso.
Iteración # 1
Inicialmente, se utilizó el primer enfoque, cuando simplemente
object.delete()
y el objeto se eliminó con todas las dependencias. Pero con el tiempo, tuvimos que abandonar este enfoque, ya que un objeto podría tener dependencias con millones de otros objetos, y la eliminación en cascada de tablas rígidamente bloqueadas. Y dado que el servicio realiza miles de comprobaciones cada segundo y las registra, el bloqueo de las mesas condujo a una seria desaceleración en el servicio, lo que era inaceptable para nosotros.
Iteración # 2
Para evitar bloqueos largos, decidimos eliminar datos en lotes. Esto permitiría registrar datos de monitoreo reales en los intervalos entre eliminaciones de objetos. Se puede obtener una lista de todos los objetos que se eliminarán en cascada mediante el método que se utiliza en el panel de administración al eliminar un objeto (al confirmar la eliminación):
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 situación mejoró: la carga se distribuyó con el tiempo, los nuevos datos comenzaron a registrarse más rápido. Pero inmediatamente nos encontramos con el siguiente escollo. El hecho es que la lista de objetos a eliminar se forma al comienzo de la eliminación, y si se agregan nuevos objetos dependientes en el proceso de eliminación "en porciones", el elemento padre no se puede eliminar.
Inmediatamente abandonamos la idea de un error en la eliminación recursiva para recopilar nuevamente datos sobre nuevas dependencias o prohibimos agregar registros dependientes al eliminar, porque
a) puede ir a un bucle infinito ob) debe encontrar todos los objetos dependientes en todo el código .
Iteración # 3
Pensamos en el segundo tipo de eliminación, cuando los datos se marcan y se ocultan de la interfaz. Inicialmente, este enfoque fue rechazado, porque parecía una tarea durante al menos una semana encontrar todas las consultas y agregar un filtro por la ausencia de un padre eliminado. Además, había una alta probabilidad de perder el código necesario, lo que llevaría a consecuencias impredecibles.
Luego decidimos usar decoradores para anular el administrador de consultas. Además, es mejor ver el código que escribir cien palabras.
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())
El
exclude_objects_for_deleted_hosts(fields)
para los campos especificados del modelo de campos agrega automáticamente un filtro de
exclude
para cada solicitud, que simplemente elimina las entradas que no deberían mostrarse en la interfaz.
Ahora es suficiente para todos los modelos que se verán afectados de alguna manera por la eliminación para agregar un decorador:
@exclude_objects_for_deleted_hosts('host') class Alias(models.Model): host = models.ForeignKey(to=Host, verbose_name='Host', related_name='alias')
Ahora, para eliminar el objeto
Host
, simplemente cambie el atributo
is_deleted
:
host.is_deleted = True
Todas las consultas excluirán automáticamente los registros que hacen referencia a objetos remotos:
Resulta la siguiente consulta SQL:
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)
Como puede ver,
`is_deleted` = TRUE
agregado a la solicitud combinaciones adicionales para los campos especificados en el decorador y comprobaciones de
`is_deleted` = TRUE
.
Un poco sobre números
Es lógico que las uniones y condiciones adicionales aumenten el tiempo de ejecución de la consulta. El estudio de este tema mostró que el grado de "complicación" depende de la estructura de la base de datos, el número de registros y la presencia de índices.
Específicamente, en nuestro caso, para cada nivel de dependencia, la solicitud es multada alrededor del 30%. Esta es la penalización máxima que obtenemos en la tabla más grande con millones de registros; en tablas más pequeñas, la penalización se reduce a un pequeño porcentaje. Afortunadamente, tenemos los índices necesarios configurados, y para la mayoría de las consultas críticas, las uniones necesarias ya estaban allí, por lo que no sentimos una gran diferencia en el rendimiento.
Identificadores únicos
Antes de eliminar datos, puede ser necesario liberar los identificadores que se planean usar en el futuro, ya que esto puede dar lugar a un error no único al crear un nuevo objeto. A pesar de que no habrá objetos visibles en la aplicación Django, todavía estarán en la base de datos. Por lo tanto, para los objetos eliminados, agregamos uuid al identificador.
host.hostname = '{}_{}'.format(host.hostname, uuid.uuid4()) host.is_deleted = True host.save()
Operación
Para cada nuevo modelo o dependencia, el decorador debe actualizarse si es necesario. Para simplificar la búsqueda de modelos dependientes, escribimos una prueba "inteligente":
def test_deleted_host_decorator_for_models(self): def recursive_host_finder(model, cache, path, filters):
La prueba verifica recursivamente todos los modelos para detectar la presencia de una dependencia en el modelo que se va a eliminar, luego busca ver si el decorador para los campos requeridos se ha configurado para este modelo. Si falta algo, la prueba le indicará delicadamente dónde agregar el decorador.
Epílogo
Por lo tanto, con la ayuda de un decorador, fue posible implementar una "pequeña eliminación" de datos que tiene una gran cantidad de dependencias. Todas las solicitudes reciben automáticamente el filtro de
exclude
requerido. La imposición de condiciones adicionales ralentiza el proceso de obtención de datos, el grado de "complicación" depende de la estructura de la base de datos, el número de registros y la disponibilidad de índices. La prueba propuesta le dirá para qué modelos necesita agregar decoradores, y en el futuro controlará su consistencia.