Saltar al contenido principal

Anunciando entcache: un controlador de caché para Ent

· 7 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Mientras trabajábamos en el motor de consultas de grafos de datos operativos de Ariga, identificamos una oportunidad para mejorar drásticamente el rendimiento en numerosos casos de uso mediante una biblioteca de caché robusta. Como usuarios intensivos de Ent, era natural implementar esta capa como extensión de Ent. En esta publicación, explicaré brevemente qué son las cachés, cómo encajan en arquitecturas de software y presentaré entcache, un controlador de caché para Ent.

El almacenamiento en caché es una estrategia popular para mejorar el rendimiento de aplicaciones. Se basa en la observación de que la velocidad para recuperar datos usando distintos tipos de medios puede variar en órdenes de magnitud. Jeff Dean presentó famosamente estas cifras en una conferencia sobre "Consejos de Ingeniería de Software para construir sistemas distribuidos a gran escala":

cifras de caché

Estas cifras reflejan lo que ingenieros de software experimentados conocen intuitivamente: leer de memoria es más rápido que de disco, y obtener datos del mismo centro de datos es más rápido que desde internet. Añadamos que algunos cálculos son costosos y lentos, por lo que recuperar resultados precomputados puede ser mucho más rápido (y económico) que recalcularlos cada vez.

La inteligencia colectiva de Wikipedia define una caché como "un componente hardware o software que almacena datos para servir futuras solicitudes más rápido". En otras palabras: si almacenamos resultados de consultas en RAM, podemos atender solicitudes dependientes mucho más rápido que si debemos consultar la base de datos a través de red, esperar que lea datos de disco, ejecute cómputos y finalmente nos envíe la respuesta (por red).

Sin embargo, como ingenieros debemos recordar que las cachés son notoriamente complejas. Como dijo Phil Karlton, ingeniero de Netscape: "Solo hay dos problemas difíciles en Informática: la invalidación de caché y nombrar cosas". Por ejemplo, en sistemas que requieren consistencia fuerte, una entrada de caché obsoleta puede causar comportamientos incorrectos. Por ello, diseñar cachés en arquitecturas requiere extremo cuidado y atención al detalle.

Presentando entcache

El paquete entcache proporciona un nuevo controlador para Ent que envuelve drivers SQL existentes. A alto nivel, decora el método Query del driver subyacente realizando en cada llamada:

  1. Genera una clave de caché (ej. hash) con sus argumentos (sentencia y parámetros).

  2. Consulta la caché para ver si los resultados están disponibles. Si existen (lo que llamamos cache-hit), omite la base de datos y devuelve los resultados desde memoria.

  3. Si la caché no contiene la entrada, pasa la consulta a la base de datos.

  4. Tras ejecutar la consulta, registra los valores brutos de las filas devueltas (sql.Rows) y las almacena en caché con la clave generada.

El paquete ofrece varias opciones para configurar el TTL de entradas, controlar la función hash, implementar almacenes personalizados o multinivel, invalidar y omitir entradas. Consulta la documentación completa en https://pkg.go.dev/ariga.io/entcache.

Como mencionamos anteriormente, configurar correctamente el almacenamiento en caché para una aplicación es una tarea delicada. Por ello, entcache ofrece a los desarrolladores diferentes niveles de caché que pueden utilizarse:

  1. Una caché basada en context.Context. Suele vincularse a una solicitud y no funciona con otros niveles de caché. Se utiliza para eliminar consultas duplicadas que ejecuta la misma solicitud.

  2. Una caché a nivel de driver utilizada por ent.Client. Una aplicación normalmente crea un driver por base de datos, por lo que lo tratamos como una caché a nivel de proceso.

  3. Una caché remota. Por ejemplo, una base de datos Redis que proporciona una capa de persistencia para almacenar y compartir entradas de caché entre múltiples procesos. Esta capa es resistente a cambios o fallos en los despliegues de aplicaciones, y permite reducir el número de consultas idénticas ejecutadas en la base de datos por diferentes procesos.

  4. Una jerarquía de caché, o caché multinivel, permite estructurar la caché de forma jerárquica. La jerarquía de almacenes de caché se basa principalmente en velocidades de acceso y tamaños. Por ejemplo, una caché de 2 niveles compuesta por una caché LRU en la memoria de la aplicación y una caché remota respaldada por Redis.

Ilustremos esto explicando la caché basada en context.Context.

Caché a Nivel de Contexto

La opción ContextLevel configura el driver para trabajar con una caché a nivel de context.Context. El contexto suele vincularse a una solicitud (ej. *http.Request) y no está disponible en modo multinivel. Cuando se usa esta opción como almacén de caché, el context.Context adjunto lleva una caché LRU (configurable de otra manera), y el driver almacena y busca entradas en esta caché al ejecutar consultas.

Esta opción es ideal para aplicaciones que requieren consistencia fuerte pero quieren evitar ejecutar consultas duplicadas en la misma solicitud. Por ejemplo, dada la siguiente consulta GraphQL:

query($ids: [ID!]!) {
nodes(ids: $ids) {
... on User {
id
name
todos {
id
owner {
id
name
}
}
}
}
}

Una solución ingenua para resolver esta consulta ejecutaría: 1 consulta para obtener N usuarios, otras N consultas para obtener los todos de cada usuario, y una consulta por cada todo para obtener su propietario (más sobre el Problema N+1).

Sin embargo, Ent ofrece un enfoque único para resolver estas consultas (detalles en el sitio de Ent), por lo que solo se ejecutan 3 consultas: 1 para obtener N usuarios, 1 para obtener los todos de todos los usuarios, y 1 para obtener los propietarios de todos los todos.

Con entcache, el número de consultas puede reducirse a 2, ya que la primera y última consulta son idénticas (ver ejemplo de código).

context-level-cache

Los diferentes niveles se explican en profundidad en el README del repositorio.

Empezando

Si no estás familiarizado con la creación de proyectos en Ent, completa primero el tutorial de configuración.

Primero, go get el paquete usando el siguiente comando:

go get ariga.io/entcache

Tras instalar entcache, puedes añadirlo fácilmente a tu proyecto con este fragmento:

// Open the database connection.
db, err := sql.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal("opening database", err)
}
// Decorates the sql.Driver with entcache.Driver.
drv := entcache.NewDriver(db)
// Create an ent.Client.
client := ent.NewClient(ent.Driver(drv))

// Tell the entcache.Driver to skip the caching layer
// when running the schema migration.
if client.Schema.Create(entcache.Skip(ctx)); err != nil {
log.Fatal("running schema migration", err)
}

// Run queries.
if u, err := client.User.Get(ctx, id); err != nil {
log.Fatal("querying user", err)
}
// The query below is cached.
if u, err := client.User.Get(ctx, id); err != nil {
log.Fatal("querying user", err)
}

Para ver ejemplos más avanzados, visita el directorio de ejemplos del repositorio.

Conclusión

En esta entrada, he presentado "entcache", un nuevo controlador de caché para Ent que desarrollé mientras trabajaba en el motor de consultas del Grafo de Datos Operacionales de Ariga. Comenzamos la discusión mencionando brevemente la motivación para incluir cachés en sistemas de software. A continuación, describimos las características y capacidades de entcache y concluimos con un breve ejemplo de cómo puedes configurarlo en tu aplicación.

Hay varias características en las que estamos trabajando y que nos gustaría implementar, pero necesitamos ayuda de la comunidad para diseñarlas correctamente (¿alguien para resolver la invalidación de caché? ;)). Si estás interesado en contribuir, contáctame en el canal de Slack de Ent.

[Para más noticias y actualizaciones de Ent:]