Almacenamiento en caché. Parte 2: 60 días antes del lanzamiento

¡Hola! Ya te escribí sobre cómo promover iniciativas en una corporación. Más precisamente, cómo (a veces) esto tiene éxito y qué dificultades pueden surgir: una retrospectiva de rastrillo. Cómo una solución hecha por uno mismo resultó ser más fría que una paga y Cómo elegimos un sistema de almacenamiento en caché. Parte 1 .

Hoy quiero continuar y hablar sobre el momento psicológicamente más estresante de ese proyecto, sobre el cual aparecen los primeros dos artículos, cuando el resultado del proyecto fue determinado no tanto por las habilidades técnicas del equipo como por la confianza en sus cálculos y la voluntad de llegar al final.

Tengo que decir, creo que para llevar el proyecto a un momento tan intenso, es un error muy usado sobre lshaya que cualquier heroísmo al sacar el proyecto de este problema ...
Pero no oculto esta experiencia y la comparto voluntariamente, porque considero:

  • precisamente las áreas problemáticas son puntos de crecimiento
  • los mayores problemas "llegan" precisamente de donde no se espera

La combinación de estos puntos, simplemente te obliga a compartir la maravillosa experiencia de "cómo ganar una cuneta de la nada". Pero, debe notarse, una situación similar es excepcional en la compañía Sportmaster. Es decir, es posible que esta situación vuelva a ocurrir, planificación y definición de responsabilidad ahora, en un nivel completamente diferente.

Entonces, parece que la introducción es suficiente, si estás listo, bienvenido a cat.



Junio ​​2017 Estamos modificando el panel de administración. El panel de administración no es solo un conjunto de formularios y tablas en la interfaz web: los valores ingresados ​​deben estar pegados con docenas de otros datos que obtenemos de sistemas de terceros. Además, de alguna manera, transfórmelo y, en última instancia, envíelo a los consumidores (el principal es el sitio ElasticSearch de Sportmaster).

La principal dificultad es simplemente convertir y enviar. A saber:

  1. debe proporcionar datos en forma de json, que pesa 100 Kb cada uno, y algunos aparecen por 10 MB (busque la disponibilidad y los criterios de entrega de productos a las tiendas)
  2. hay json con una estructura que tiene archivos adjuntos recursivos de cualquier nivel de anidamiento (por ejemplo, un menú dentro de un elemento de menú, en el que hay elementos de menú nuevamente, etc.)
  3. la declaración final no se aprueba y cambia constantemente (por ejemplo, el trabajo con productos por Modelos se reemplaza por un enfoque cuando trabajamos por Modelos en color). Constantemente: esto es varias veces a la semana, con una tasa máxima de 2 veces al día durante una semana.

Si los primeros 2 puntos son puramente técnicos y están dictados por la tarea misma, entonces, con el tercer punto, por supuesto, debe tratarlo organizativamente. Pero, el mundo real está lejos de ser ideal, por lo que trabajamos con lo que tenemos.

Es decir, descubrieron cómo remachar rápidamente los formularios web y sus objetos en el lado del servidor.

Se designó a una persona del equipo para que desempeñara el papel de "bofetada de formularios" profesional y, utilizando componentes web preparados, lanzó una demostración para la interfaz de usuario más rápido de lo que los analistas corrigieron los dibujos de esta interfaz de usuario.

Pero para cambiar el esquema de transformaciones, aquí surgió la complejidad.

Primero, seguimos el camino habitual: llevar a cabo la transformación en la consulta SQL a Oracle. Había un especialista en DB en el equipo. Duró hasta el momento en que la solicitud fue de 2 páginas de texto sql continuo. Podría seguir y seguir, pero cuando los cambios vinieron de los analistas, objetivamente, lo más difícil fue encontrar el lugar donde hacer los cambios.

Los analistas expresaron regla en los esquemas, los cuales, a pesar de que se pintaron en algo separado del código (una especie de Visio / draw.io / Gliffy), pero no eran tansimilar a los cuadrados y flechas en los sistemas ETL (por ejemplo, Pentaho Kettle, que en ese momento se usaba para suministrar datos al sitio web de Sportmaster). Ahora, si no tuviéramos una consulta SQL, ¡sino un esquema ETL! ¡Entonces la declaración y la solución se expresarían de manera idéntica topológicamente, lo que significa que editar el código podría tomar tanto tiempo como editar la declaración!

Pero con los sistemas ETL hay otra dificultad. El mismo Pentaho Kettle: es excelente cuando necesita crear un nuevo índice en ElasticSearch, en el que escribir todos los datos pegados de varias fuentes (observación: de hecho, es Pentaho Kettle el que no funciona muy bien, porque no usa JavaScript en las transformaciones asociado con las clases de Java a través de las cuales el consumidor accede a los datos; debido a esto, puede escribir algo que no puede convertir en los objetos pojo necesarios más adelante, pero este es un tema separado, alejado del curso principal del artículo).

Pero, ¿qué hacer cuando en el panel de administración el usuario corrigió un campo en un documento? Para enviar este cambio al sitio ElasticSearch de Sportmaster, ¡no cree un nuevo índice en el que completar todos los documentos de este tipo, incluido uno actualizado!

Quería que cuando un objeto en los datos de entrada cambiara, luego enviar una actualización a ElasticSearch del sitio solo para el documento de salida correspondiente.

De acuerdo, el documento de entrada en sí, pero después de todo, de acuerdo con el esquema de transformación, ¡podría adjuntarse a documentos de un tipo diferente a través de unirse! Por lo tanto, debe analizar el esquema de transformación y calcular qué documentos de salida se verán afectados por el cambio en los datos en las fuentes.

La búsqueda de productos en caja para resolver este problema no condujo a nada. Extraviado.
Y cuando se desesperaron por encontrarlo, lo descubrieron, pero ¿cómo debería funcionar dentro y cómo se puede hacer esto?

La idea surgió de inmediato.

Si el ETL final se puede dividir en sus partes constituyentes, cada una de las cuales tiene un cierto tipo de un conjunto finito (por ejemplo, filtro, unión, etc.), entonces, tal vez, será suficiente para crear el mismo conjunto final de nodos especiales que corresponden a los originales, pero con la diferencia de que trabajan no con los datos en sí, sino con su cambio?

En gran detalle, con ejemplos y puntos clave en la implementación, nuestra solución: quiero cubrirla en un artículo separado. Para lidiar con posiciones de apoyo, esto requerirá una inmersión seria, la capacidad de pensar de manera abstracta y confiar en lo que aún no se ha manifestado. De hecho, será interesante precisamente desde un punto de vista matemático y es interesante solo para aquellos habrovitas que estén interesados ​​en detalles técnicos .
Aquí solo puedo decir que creamos un modelo matemático en el que describimos 7 tipos de nodos y mostramos que este sistema está completo, es decir, usando estos 7 tipos de nodos y las conexiones entre ellos, se puede expresar cualquier esquema de transformación de datos. La implementación se basa en el uso activo de obtener y registrar datos por clave (es decir, por clave, sin condiciones adicionales).

Por lo tanto, nuestra solución tenía un punto fuerte con respecto a todas las dificultades introductorias:

  1. los datos deben proporcionarse en forma de json -> trabajamos con objetos pojo (simple objeto java antiguo, si alguien no encontró los tiempos en que se usaba dicha designación), que son fáciles de superar en json
  2. hay json con una estructura que tiene incrustaciones recursivas de cualquier nivel de anidación -> nuevamente, pojo (lo principal es que no hay bucles, pero cuántos niveles de anidación no es importante, porque podemos procesarlo fácilmente en java a través de la recursión)
  3. la declaración final cambia constantemente -> excelente, ya que estamos cambiando el esquema de transformación más rápido de lo que los analistas elaboran (en los diagramas) deseos de experimentos

De los momentos de riesgo, solo uno: escribimos la solución desde cero, por nuestra cuenta.

En realidad, las trampas no tardaron en llegar.

Momento especial N1. Trampa. “Bien extrapolado”


Otra sorpresa de naturaleza organizativa fue que, al mismo tiempo que nuestro desarrollo, el repositorio principal principal se estaba moviendo a una nueva versión, y el formato en el que este repositorio proporciona datos cambió. Y sería bueno si nuestro sistema funcionara inmediatamente con el nuevo almacenamiento, y no con el anterior. Pero el nuevo almacenamiento aún no está listo. Pero entonces, las estructuras de datos son conocidas y pueden darnos una base de demostración en la que se vertirá una pequeña cantidad de datos relacionados. ¿Va?

Aquí, en el enfoque del producto, cuando se trabaja con el flujo de suministro de valor, todos los optimistas reciben una advertencia inequívocamente: hay un bloqueador -> la tarea no funciona, punto.

Pero entonces, tal dependencia ni siquiera despertó sospechas. De hecho, estábamos eufóricos por el éxito con el prototipo de procesador Delta: un sistema para procesar datos en deltas (implementación de un modelo matemático cuando los cambios en los datos de salida se calculan utilizando el esquema de transformación como respuesta a un cambio en los datos de entrada).

Entre todos los esquemas de transformación, uno fue el más importante. Además del hecho de que el circuito en sí mismo era el más grande y complejo, también existía un requisito estricto para que la transformación se realizara de acuerdo con este circuito: un límite de tiempo para la ejecución de la cantidad total de datos.

Entonces, la transformación debe llevarse a cabo 15 minutos y no un segundo más. La entrada principal es una tabla con 5.5 millones de registros. En la etapa de desarrollo, la tabla aún no está poblada. Más precisamente, se llena con un pequeño conjunto de datos de prueba en la cantidad de 10 mil filas.

Bueno, empecemos. En la primera implementación, el procesador Delta trabajó en el HashMap como el almacenamiento de valor clave (permítame recordarle que necesitamos leer y escribir mucho los objetos por clave). Por supuesto, que en los volúmenes de producción, todos los objetos intermedios no caben en la memoria, por lo tanto, en lugar de HashMap, cambiamos a Hazelcast.

¿Por qué exactamente Hazelcast? Entonces, debido a que este producto era familiar, se utilizó en el backend del sitio del Sportmaster. Además, este es un sistema distribuido y, como nos pareció, si un amigo hace algo mal con el rendimiento, agregamos más instancias a un par de máquinas y el problema se resuelve. En casos extremos: una docena de autos. Escalado horizontal y todas las cosas.

Y así, estamos lanzando nuestro procesador Delta para una transformación dirigida. Funciona casi al instante. Esto es comprensible: los datos son solo 10 mil en lugar de 5.5 millones. Por lo tanto, multiplicamos el tiempo medido por 550 y obtenemos el resultado: algo así como 2 minutos. ¡Multa! De hecho, ¡una victoria!

Esto fue al comienzo del trabajo del proyecto: justo cuando necesita decidir sobre la arquitectura, confirmar las hipótesis (realizar pruebas que las confirmen), integrar la solución piloto verticalmente.

Como las pruebas mostraron un excelente resultado, es decir, confirmamos todas las hipótesis, rápidamente cambiamos el piloto, armamos un "esqueleto" integrado verticalmente para una pequeña pieza de funcionalidad. Y comenzaron la codificación principal: llenar el "esqueleto con carne".

Lo que con éxito y vigorosamente comprometido. Hasta ese hermoso día, cuando se cargó un conjunto completo de datos en la tienda maestra .

Ejecute la prueba en este conjunto.

Después de 2 minutos no funcionó. Tampoco trabajé después de 5, 10, 15 minutos. Es decir, no encajaban en el marco necesario. Pero, con quién no sucede, será necesario ajustar algo en detalle y en forma.

Pero la prueba no funcionó una hora después. E incluso después de 2 horas había esperanzas de que trabajara, y buscaremos qué ajustar. Restos de esperanza fueron incluso después de 5 horas. Pero, después de 10 horas, cuando se fueron a casa, pero la prueba aún no funcionó, ya no había esperanza.

El problema fue que al día siguiente, cuando llegaron a la oficina, la prueba seguía trabajando diligentemente. Como resultado, se desplazó durante 30 horas, no esperó, se apagó.
¡Catástrofe!

El problema se localizó lo suficientemente rápido.

Hazelcast, cuando se trabaja en una pequeña cantidad de datos, en realidad desplaza todo en la memoria. Pero cuando fue necesario volcar datos en un disco, el rendimiento bajó miles de veces.

La programación sería una ocupación aburrida e insípida, de no ser por las autoridades y la obligación de entregar el producto terminado. Entonces, literalmente, un día después, después de recibir un conjunto completo de datos, tenemos que ir a las autoridades con un informe sobre cómo pasó la prueba de los volúmenes de producción.

Esta es una elección muy seria y difícil:

  1. diga "como está" = abandone el proyecto
  2. diga "como me gustaría" = arriesgarse, tal vez, no se sabe si podemos solucionar el problema

Para comprender qué sentimientos surgen en este caso, solo es posible invertir completamente en la idea, realizar el plan durante medio año, crear un producto que ayude a los colegas a resolver una gran capa de problemas.

Y así, renunciar a su amada creación es muy difícil.
Esto es característico de todas las personas: amamos aquello en lo que hemos puesto mucho esfuerzo. Por lo tanto, es difícil escuchar críticas: debe hacer esfuerzos conscientes para percibir adecuadamente los comentarios.

En general, decidimos que todavía hay muchos, muchos sistemas diferentes que se pueden usar como almacenamiento de valor clave, y si Hazelcast no encaja, entonces algo definitivamente funcionará. Es decir, decidieron arriesgarse. Para nuestra justificación, podemos decir que todavía no era un "plazo sangriento", en general, todavía había un margen de tiempo para "pasar" a una solución de respaldo.

En esa reunión con los jefes, nuestro gerente indicó que "la prueba mostró que el sistema funciona de manera estable en los volúmenes de producción, no se bloquea". De hecho, el sistema funcionó de manera estable. 60 días

para liberar .

Momento especial N2. No es una trampa, pero no un descubrimiento. "Menos es más"


Para encontrar un reemplazo para Hazelcast con el rol de almacén de datos Key-Value, compilamos una lista de todos los candidatos: obtuvimos una lista de 31 productos. Esto es todo lo que logré buscar en Google y descubrir por mis amigos. Además, Google ofreció algunas opciones absolutamente obscenas, como el trabajo de un estudiante.

Para evaluar a los candidatos más rápido, preparamos una pequeña prueba que, en unos minutos de lanzamiento, mostró el rendimiento en los volúmenes correctos. Y paralelaron el trabajo: todos tomaron el siguiente sistema de la lista, configuraron, ejecutaron la prueba, tomaron el siguiente.
Trabajaron rápidamente, rompieron varios sistemas al día.

En el sistema 18, quedó claro que esto no tenía sentido. Bajo nuestro perfil de carga, ninguno de estos sistemas está afilado. Tienen muchos volantes y reverencias para que sea conveniente usarlos, muchos enfoques hermosos para el escalado horizontal, pero esto no nos da ningún beneficio.

Necesitamos un sistema que _fast_ guarde la clave en un objeto en el disco y lea rápidamente la clave.

Si es así, describimos el algoritmo de cómo se puede implementar esto. En general, parece bastante factible, si al mismo tiempo: a) sacrifica la cantidad de datos que ocupará el disco, b) tiene aproximadamente estimaciones de la cantidad y el tamaño característico de los datos en cada tabla.
Algo con estilo, asignar memoria (en disco) para objetos con un margen, piezas de un volumen máximo fijo. Luego, usando las tablas de índice ... y así sucesivamente ...
Fue una suerte que no llegara a esto.

La salvación llegó en forma de RocksDB.
Este es un producto de Facebook que está diseñado para una lectura rápida y guardar una matriz de bytes en el disco. Al mismo tiempo, el acceso a los archivos se proporciona a través de una interfaz que es similar al almacenamiento Key-Value. De hecho, la clave es una matriz de bytes, el valor es una matriz de bytes. Optimizado para hacer este trabajo de forma rápida y confiable. Todas. Si necesita algo más hermoso y de alto nivel, atorníllelo usted mismo.
¡Exactamente lo que necesitamos!

RocksDB, atornillado en el rol de almacenamiento de valor clave, llevó el indicador de prueba objetivo al nivel de 5 horas. Estaba lejos de los 15 minutos, pero lo principal estaba hecho. Lo principal era comprender lo que estaba sucediendo, comprender que escribir en el disco era lo más rápido posible, más rápido que imposible. En SSD, en pruebas refinadas, RocksDB exprimió 400Mb / s, y eso fue suficiente para nuestra tarea. Retrasos: en algún lugar del nuestro, en un código vinculante.

En nuestro código, lo que significa que podemos manejarlo. Vamos a desarmarlo, pero podemos manejarlo.

Momento especial N3. Apoyo. "Cálculo teórico"


Tenemos un algoritmo y entrada. Tomamos el rango de datos de entrada, calculamos cuántas acciones debe realizar el sistema, cómo se expresan estas acciones en los costos de tiempo de ejecución de JVM (asigne un valor a una variable, ingrese un método, cree un objeto, copie una matriz de bytes, etc.), además de cuántas llamadas a RocksDB debe celebrarse.

Según los cálculos, resulta que deberían cumplir 2 minutos (aproximadamente, como mostró la prueba para HashMap al principio, pero esto es solo una coincidencia: el algoritmo ha cambiado desde entonces).

Y, sin embargo, la prueba dura 5 horas.

Y ahora, antes del lanzamiento de 30 días.

Esta es una fecha especial, ahora será imposible colapsar, no tendremos tiempo para cambiar a la opción de copia de seguridad.
Por supuesto, en este día el gerente del proyecto es convocado a las autoridades. La pregunta es la misma: ten tiempo, ¿está todo bien?



Aquí está la mejor manera de describir esta situación: una imagen de portada ampliada para este artículo. Es decir, a los jefes se les muestra esa parte de la imagen que se muestra en el título. Pero en realidad, así.

Aunque, en realidad, por supuesto, no fuimos para nada graciosos. Y decir que "¡Todo está bien!" - esto es posible solo para una persona con una habilidad muy fuerte en el dominio propio.
Gran respeto por el gerente, por creer y confiar en los desarrolladores.

Código realmente disponible: muestra 5 horas. Un cálculo teórico: muestra 2 minutos. ¿Cómo se puede creer esto?

Pero es posible si: el modelo está formulado claramente, cómo contar es comprensible y qué valores sustituir también son comprensibles. Es decir, el hecho de que en realidad la ejecución lleva más tiempo significa que en realidad no se está ejecutando exactamente el código que esperamos ejecutar allí.

La tarea central es encontrar "lastre" en el código. Es decir, se realizan algunas acciones además de la secuencia principal de creación de los datos finales.

Apresuramos. Pruebas unitarias, composiciones funcionales, fragmentación de funciones y localización de lugares con una cantidad desproporcionada de tiempo dedicado a la ejecución. Se han hecho muchas cosas.
En el camino, formulamos esos lugares en los que puede ajustarse seriamente.

Por ejemplo, serialización. Primero usé el estándar java.io. Pero si sujetamos Cryo, en nuestro caso obtendremos un aumento de 2.5 veces en la velocidad de serialización y una reducción de 3 veces en el volumen de datos serializados (lo que significa que IO es 3 veces más pequeño, lo que solo consume los recursos principales). Pero, con más detalle, este es un tema para un artículo técnico separado.

Pero el punto clave, o "dónde se escondió el elefante", trataré de describirlo en un párrafo.

Punto especial 4. Recepción para encontrar una solución. "Problema = Solución"


Cuando obtenemos / establecemos por clave, en los cálculos se realizó como 1 operación, afecta a IO en el volumen igual a clave + valor de objeto (en forma serializada, por supuesto).
Pero, ¿qué pasa si el objeto en sí mismo en el que llamamos get / set es un Mapa, que también obtenemos mediante get / set desde el disco. ¿Cuánto se hará el IO en este caso?

En nuestros cálculos, esta característica no se tuvo en cuenta. Es decir, se consideró como 1 IO para clave + valor-objeto. ¿Pero, de hecho?

Por ejemplo, en el almacenamiento Key-Value, por key-1 hay un objeto obj-1 con el tipo Map, en el que cierto objeto obj-2 debe almacenarse bajo la clave key-2. Aquí pensamos que la operación requeriría un IO para key-2 + obj-2. Pero en realidad, debe considerar obj-1, manipularlo y enviarlo a IO: key-1 + obj-1. Y si se trata de un mapa en el que hay 1000 objetos, el consumo de E / S será aproximadamente 1000 veces más. Y si 10,000 objetos, entonces ... Así es como obtuvieron el "lastre".

Cuando se identifica un problema, la solución suele ser obvia.

En nuestro caso, esto se ha convertido en una estructura especial para las manipulaciones dentro del Mapa anidado. Es decir, dicho valor-clave, que para obtener / establecer acepta dos claves a la vez, que deben aplicarse secuencialmente: clave-1, clave-2, es decir, para el primer nivel y para el anidado. Cómo implementar una estructura de este tipo: te lo contaré detalladamente con gusto, pero nuevamente, en un artículo técnico separado.
Aquí, de este episodio, enfatizo y promuevo tal característica: un problema extremadamente detallado es una buena solución.

Terminación


En este artículo, traté de mostrar los puntos organizativos y las trampas que pueden surgir. Estas trampas son claramente visibles "desde el costado" o con el tiempo, pero es muy fácil entrar en ellas cuando te encuentras por primera vez junto a ellas. Espero que alguien recuerde tal descripción, y en el momento adecuado el recordatorio funcionará "He escuchado algo así antes".

Y, lo más importante, ahora que todo se cuenta sobre el proceso, los momentos psicológicos, los de organización. Ahora que tenemos una idea de qué tareas y bajo qué condiciones se creó el sistema. Ahora, puede y debe contar sobre el sistema desde un punto de vista técnico: qué tipo de modelo matemático es, qué trucos en el código utilizamos y qué soluciones innovadoras pensamos.

Sobre esto en el próximo artículo.

Mientras tanto, Happy New Code!

All Articles