Saltar al contenido principal

Cleanup old data from datastore

· 5 min de lectura
Franco R.
Software Person @ sils.tech

Impulsado por la necesidad de reducir costos en Google Cloud Platform me encontré un alto costo en un proyecto relacionado a Cloud Firestore Storage. Analizando el Datastore vi que tenía el siguiente tamaño

  • 834.079.717 eventos (entidades de Datastore)
  • 1,73 TB built-in index size
  • 222,85 GB composite index size
  • 2,17 TB total size

En esta base de datos guardamos los datos crudos, tal cual salen de los servidores de decodificación, tanto los enviados vía protocolo N9M de Streamax, como los eventos enviados por dispositivos con protocolo GPS vía Traccar.

Como nunca habíamos hecho una limpieza había eventos desde el inicio de los tiempos, esto es desde el año 2021.

Debido a la cantidad de datos y que Datastore no brinda muchas herramientas para hacer borrado masivo es que fracasé en varios intentos anteriores.

Vamos a ver qué estrategias seguí para intentar borrar datos y al final les cuento qué herramienta sí nos brinda Datastore para establecer TTL.

Intento 1 - borrado con JS CLI

Este intento a priori funciona, pero como hay que cargar los datos en memoria local para después borrar (hacer un SELECT para luego un DELETE) no pude hacerlo andar con más de 50000 eventos a la vez. Lo cual para borrar > 800M registros puede tardar varios días.

De todos modos menciono los pasos:

Abrimos una consola de node pasando la variable GOOGLE_APPLICATION_CREDENTIALS apuntando a un service account con permisos en el proyecto que querramos borrar:

(en mi caso la versión de node que usé es la 23.8.0)

GOOGLE_APPLICATION_CREDENTIALS=/path/sils-prod-service-account.json node

Luego creamos el cliente y vamos haciendo las obtenciones y los borrados

const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();

q = datastore.createQuery('Eventos').filter('fechaGPS' , '<', '2022-01-01 03:00:00').select('__key__').limit(50000)
[tasks,] = await datastore.runQuery(q)

await datastore.delete(tasks.map(t => t[datastore.KEY]))

Así pude borrar de forma satisfactoria bloques de 50000 entidades. Obviamente es muy difícil borrar >800M con este enfoque.

Intento 2 - Dataflow

Luego de dimensionar que la solución manual no escalaba para semejante volumen de información opté por hacer un intento con Dataflow. Para hacerlo usé el template Firestore to Firestore Delete con la siguiente configuración

Dataflow config

De hecho, en la primera ejecución en la GQL Query apliqué el filtro por tiempo:

SELECT * FROM Eventos WHERE fechaGPS < '2025-01-01 03:00:00'

Al lanzar el job nunca terminaba y quedaba en estado "atascado". Esto se debe a que el primer paso, que es obtener los eventos, nunca terminaba.

Intento 3 - Dataflow con UDF

En el segundo intento con Dataflow probé sacando el filtro por fecha de la GQL query quedando

SELECT * FROM Eventos

y haciendo el filtro en un UDF.

Dataflow UDF 1

Usando el siguiente archivo

/**
* User-defined function (UDF) to transform elements as part of a Dataflow
* template job.
*
* @param {string} inJson input JSON message (stringified)
* @return {?string} outJson output JSON message (stringified)
*/
function process(inJson) {
const obj = JSON.parse(inJson);

// Add a field: obj.newField = 1;
// Modify a field: obj.existingField = '';
// Filter a record: return null;
if (obj.hasOwnProperty('properties') && obj.properties.hasOwnProperty('fechaGPS')) {
if (typeof obj.properties.fechaGPS !== 'string') return JSON.stringify(obj);
const oldDate = obj.properties.fechaGPS.endsWith('Z') && obj.properties.fechaGPS || obj.properties.fechaGPS + 'Z'
if (new Date(oldDate) < new Date('2025-01-01 03:00:00Z')) {
return JSON.stringify(obj);
}
}
return null
}

El mismo sirve para hacer el filtrado de las entidades por fecha.

El resultado no fue el esperado ya que otra vez se generó un atasco por throttling y nunca llegó a eliminar.

Intento 4 - Dataflow con UDF sin throttling

Lo único que cambié acá es poner en 0 el número de workers en la configuración "Expected number of workers".

Hint for the expected number of workers in the Firestore ramp-up throttling step. Defaults to: 500.

Dataflow UDF sin throttling

En este caso, luego de "7 hr 47 min" borró 755.069.116 entidades de Datastore.

IMPORTANTE

El borrado masivo con Dataflow puede sonar bien pero hay que tener en cuenta el excesivo costo en las consultas a entidades de datastore. En un solo día, el gasto puede superar los 1000USD.

Implementación de TTL

Hay una funcionalidad en Datastore para que las entidades se borren automáticamente pasado cierto tiempo. Para ello hay que crear una regla de TTL sobre una propiedad de tipo datetime.

Agregué la propiedad expireAt de tipo datetime que se puede ver en el colector

function createEntity(obj, key) {
let expirationDate = new Date();
if (obj.expireAt) {
expirationDate = new Date(obj.expireAt)
delete obj.expireAt
} else {
// Set expiration for 2 years from now.
expirationDate.setDate(expirationDate.getDate() + 2 * 365);
}

const data = Object.assign(obj, {
id: key.name,
expireAt: expirationDate
});
return {
data,
key,
};
}

Luego para crear la regla de TTL ir a la consola de Google Cloud, en Datastore y crearla:

Datastore TTL

Consideraciones finales

Si luego del borrado, el index no se refresca, conviene borrarlo y crearlo de nuevo

gcloud alpha datastore indexes cleanup index.yaml --project sils-producto

y luego

gcloud alpha datastore indexes create index.yaml --project sils-producto