Cleanup old data from datastore
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

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.

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.
En este caso, luego de "7 hr 47 min" borró 755.069.116 entidades de Datastore.
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:

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