La nueva estructura de datos de Redis 5, llamada streams, ha despertado un gran interés en la comunidad. De alguna manera hablaré con aquellos que usan streams en producción y escribiré sobre eso. Pero ahora quiero considerar un tema ligeramente diferente. Me parece que muchas personas piensan que las transmisiones son una herramienta surrealista para resolver tareas terriblemente difíciles. De hecho, esta estructura de datos * también * proporciona mensajes, pero será una simplificación increíble suponer que la funcionalidad de Redis Streams está limitada solo por esto.
Los flujos son una plantilla excelente y un "modelo mental" que se pueden usar con gran éxito en el diseño del sistema, pero en realidad los flujos, como la mayoría de las estructuras de datos de Redis, son una estructura más general y se pueden usar para muchas otras tareas. En este artículo, presentaremos las transmisiones como una estructura de datos pura, ignorando completamente las operaciones de bloqueo, los grupos de destinatarios y todas las demás funciones de mensajería.
Streams: esto es CSV con esteroides
Si desea registrar una serie de elementos de datos estructurados y piensa que la base de datos será un exceso aquí, simplemente puede abrir el archivo en modo de
append only
y escribir cada línea como un CSV (Valor separado por comas):
(open data.csv in append only) time=1553096724033,cpu_temp=23.4,load=2.3 time=1553096725029,cpu_temp=23.2,load=2.1
Se ve simple. La gente ha hecho esto hace mucho tiempo y todavía lo hace: es una plantilla confiable, si sabes qué es qué. Pero, ¿cuál será el equivalente en la memoria? En la memoria, es posible un procesamiento de datos mucho más avanzado, y muchas restricciones de los archivos CSV se eliminan automáticamente, como:
- Es difícil (ineficiente) cumplir con las solicitudes de rango.
- Demasiada información redundante: cada registro tiene casi el mismo tiempo y los campos están duplicados. Al mismo tiempo, la eliminación de datos hará que el formato sea menos flexible si quiero cambiar a un conjunto diferente de campos.
- Los desplazamientos de elementos son simplemente desplazamientos de bytes en el archivo: si cambiamos la estructura del archivo, los desplazamientos se volverán incorrectos, por lo que no existe un concepto real de un identificador primario. Las entradas en esencia no pueden presentarse sin ambigüedades.
- Sin la capacidad de recolectar basura y sin reescribir el registro, no puede eliminar entradas, solo marcarlas como no válidas. Reescribir registros generalmente es una mierda por varias razones, es recomendable evitarlo.
Al mismo tiempo, dicho registro CSV es bueno a su manera: no hay una estructura fija, los campos pueden cambiar, es trivial generarlo y es bastante compacto. La idea con las transmisiones de Redis era preservar las virtudes, pero superar las limitaciones. El resultado es una estructura de datos híbrida que es muy similar a los conjuntos ordenados de Redis: * se parecen * a la estructura de datos fundamental, pero usan varias representaciones internas para obtener este efecto.
Introducción a los hilos (puede omitir si ya está familiarizado con los conceptos básicos)
Las secuencias de Redis se representan como nodos macro comprimidos en delta conectados por un árbol base. Como resultado, puede buscar rápidamente registros aleatorios, obtener rangos, eliminar elementos antiguos, etc. Al mismo tiempo, la interfaz para un programador es muy similar a un archivo CSV:
> XADD mystream * cpu-temp 23.4 load 2.3 "1553097561402-0" > XADD mystream * cpu-temp 23.2 load 2.1 "1553097568315-0"
Como puede ver en el ejemplo, el comando XADD genera y devuelve automáticamente el identificador del registro, que aumenta monotónicamente y consta de dos partes: <hora> - <contador>. Tiempo en milisegundos, y el contador se incrementa para registros con el mismo tiempo.
Entonces, la primera abstracción nueva para la idea de un archivo CSV en modo de
append only
es usar el asterisco como argumento de ID para XADD: así es como obtenemos el identificador de registro del servidor de forma gratuita. Este identificador es útil no solo para indicar un elemento específico en la secuencia, sino que también está asociado con el momento en que el registro se agregó a la secuencia. De hecho, con XRANGE, puede ejecutar consultas de rango o recuperar elementos individuales:
> XRANGE mystream 1553097561402-0 1553097561402-0 1) 1) "1553097561402-0" 2) 1) "cpu-temp" 2) "23.4" 3) "load" 4) "2.3"
En este caso, utilicé la misma ID para iniciar y finalizar el rango para identificar un elemento. Sin embargo, puedo usar cualquier rango y argumento COUNT para limitar el número de resultados. Del mismo modo, no es necesario especificar identificadores completos para un rango, simplemente puedo usar solo el tiempo de Unix para obtener elementos en un rango de tiempo dado:
> XRANGE mystream 1553097560000 1553097570000 1) 1) "1553097561402-0" 2) 1) "cpu-temp" 2) "23.4" 3) "load" 4) "2.3" 2) 1) "1553097568315-0" 2) 1) "cpu-temp" 2) "23.2" 3) "load" 4) "2.1"
Por el momento, no es necesario mostrarle otras características de la API, hay documentación para esto. Por ahora, concentrémonos en este patrón de uso: XADD para agregar, XRANGE (y también XREAD) para extraer rangos (dependiendo de lo que quieras hacer), y veamos por qué los flujos son tan poderosos como para llamarlos estructuras de datos.
Si desea obtener más información sobre las transmisiones y las API, asegúrese de leer el
tutorial .
Tenistas
Hace unos días, un amigo mío que comenzó a estudiar Redis y simulé una aplicación para rastrear canchas de tenis locales, jugadores y partidos. La forma de modelar jugadores es obvia, el jugador es un objeto pequeño, por lo que solo necesitamos un hash con teclas como
player:<id>
. Luego, inmediatamente se dará cuenta de que necesita una forma de rastrear juegos en clubes de tenis específicos. Si el
player:1
y el
player:2
jugaron entre ellos y el
player:1
ganó, podemos enviar el siguiente registro a la transmisión:
> XADD club:1234.matches * player-a 1 player-b 2 winner 1 "1553254144387-0"
Una operación tan simple nos da:
- Identificador único de coincidencia: ID en la secuencia.
- No es necesario crear un objeto para la identificación de coincidencias.
- Solicitudes de rango libre para paginar partidos o ver partidos para una fecha y hora específicas.
Antes de que aparecieran las secuencias, tendríamos que crear un conjunto ordenado por tiempo: los elementos del conjunto ordenado serían identificadores de coincidencia, que se almacenan en una clave diferente como valor hash. No es solo más trabajo, sino también más memoria. Mucha, mucha más memoria (ver más abajo).
Ahora nuestro objetivo es mostrar que las transmisiones de Redis son una especie de conjunto ordenado en modo de
append only
, con claves por tiempo, donde cada elemento es un pequeño hash. Y en su simplicidad, esta es una verdadera revolución en el contexto del modelado.
El recuerdo
El caso de uso anterior no es solo un patrón de programación más coherente. El consumo de memoria en los subprocesos es tan diferente del enfoque anterior con un conjunto ordenado + hash para cada objeto que algunas cosas ahora comienzan a funcionar que antes eran imposibles de implementar.
Aquí hay estadísticas sobre la cantidad de memoria para almacenar un millón de coincidencias en la configuración presentada anteriormente:
+ = 220 (242 RSS) = 16,8 (18.11 RSS)
La diferencia es más que un orden de magnitud (es decir, 13 veces). Esto significa poder trabajar con tareas que antes eran demasiado caras para realizar en la memoria. Ahora son bastante viables. La magia es introducir flujos de Redis: los nodos macro pueden contener varios elementos que están codificados de manera muy compacta en una estructura de datos llamada listpack. Esta estructura se ocupará, por ejemplo, de codificar enteros en forma binaria, incluso si son cadenas semánticamente. Además, aplicamos compresión delta y comprimimos los mismos campos. Sin embargo, sigue siendo posible buscar por ID o tiempo, ya que dichos nodos macro están vinculados en un árbol base, que también está diseñado con optimización de memoria. Juntos, esto explica el uso económico de la memoria, pero la parte interesante es que semánticamente el usuario no ve ningún detalle de implementación que haga que los hilos sean tan eficientes.
Ahora cuentemos. Si puedo almacenar 1 millón de registros en aproximadamente 18 MB de memoria, entonces puedo almacenar 10 millones en 180 MB y 100 millones en 1.8 GB. Con solo 18 GB de memoria, puedo tener mil millones de artículos.
Series de tiempo
Es importante tener en cuenta que el ejemplo anterior con los partidos de tenis es semánticamente * muy diferente * al uso de transmisiones de Redis para series de tiempo. Sí, lógicamente todavía estamos registrando algún tipo de evento, pero hay una diferencia fundamental. En el primer caso, registramos y creamos registros para representar objetos. Y en la serie de tiempo, simplemente medimos algo que sucede afuera que en realidad no representa el objeto. Puede decir que esta distinción es trivial, pero no lo es. Es importante comprender la idea de que los hilos Redis se pueden usar para crear objetos pequeños con un orden común y para asignar identificadores a dichos objetos.
Pero incluso la opción más simple para usar series temporales es obviamente un gran avance, porque antes de la llegada de los hilos, Redis era prácticamente impotente para hacer algo aquí. Las características de la memoria y la flexibilidad de las transmisiones, así como la capacidad de limitar las transmisiones limitadas (consulte los parámetros XADD) son herramientas muy importantes en manos del desarrollador.
Conclusiones
Las transmisiones son flexibles y ofrecen muchos casos de uso, pero quería escribir un artículo muy breve para mostrar claramente los ejemplos y el consumo de memoria. Quizás para muchos lectores este uso de las transmisiones fue obvio. Sin embargo, las conversaciones con los desarrolladores en los últimos meses me han dejado la impresión de que muchos tienen una fuerte asociación entre las transmisiones y la transmisión de datos, como si la estructura de datos solo fuera buena allí. Esto no es asi. :-)