Escalado a 5M RPM

En Trendyol, tenemos la responsabilidad de servir páginas de contenido y datos. También servimos datos de contenido de forma masiva para favoritos, colecciones o incluso páginas de pago. En breve, nos involucraremos cada vez que vea las pantallas a continuación.


¿Cuáles son nuestras necesidades o cargas esperadas?
Como era de esperar, normalmente recibimos más tráfico en las temporadas de compras o en las épocas de eventos/descuentos. Si bien recibimos alrededor de 1 millón de rpm (solicitudes por minuto) en un día típico, la carga sube a aproximadamente 3 millones de rpm en las horas punta. En aquellos tiempos, no podíamos cumplir con precisión los requisitos. Nuestro tiempo de respuesta se estaba duplicando de lo habitual, e incluso tuvimos más tiempos de espera de los que podíamos ignorar.
Nuestro próximo objetivo y expectativa era alcanzar al menos 8 millones de rpm con, como máximo, los mismos recursos. Entonces, el problema era que no podíamos escalar tanto como necesitábamos, y mucho menos lo que esperábamos a continuación.
¿A quién le gustan los desafíos? Sí, ciertamente nosotros :)

¿Cómo encontramos la causa del problema?
Entonces hicimos un perfil para entender qué parte del código toma más tiempo y usa más recursos. Primero, usamos un perfilador de Java para reducir qué partes rastrear. Después de eso, agregamos algunos rastros personalizados de New Relic en el código. Cuando miramos los resultados, vimos que una parte específica del código era la que más tiempo y recursos consumía.
Esa parte era una lógica de conversión para convertir el documento de Couchbase a los datos que necesitamos para realizar la lógica comercial y servir como volumen. Se estaba ejecutando para cada solicitud y cada contenido. Empezamos a pensar si podríamos hacer esta lógica de conversión de forma asíncrona. Podríamos ejecutar esta lógica una vez para cada contenido. Para la mayoría de las partes, la respuesta fue: ¡SÍ!
No es suficiente hacer lo mejor que puedas; debes saber qué hacer y luego hacerlo lo mejor que puedas. —W Edwards Deming

¿Podemos manejar los datos de forma asíncrona?
Estamos utilizando Couchbase como fuente de datos. Cuando observamos cómo podemos convertir nuestros datos de Couchbase de forma asincrónica, vimos que Couchbase tiene un protocolo de cambio de base de datos (DCP). DCP puede darnos un flujo para las mutaciones. Entonces podemos escuchar desde nuestra antigua fuente de datos, hacer la conversión asíncrona y escribir en nuestra nueva fuente de datos. Puede consultar el artículo de Ahmet Hatipoglu si tiene dudas sobre los detalles.
Ahora es el momento de determinar si podemos optimizar los datos.
¿Necesitamos todos los datos?
Utilizamos el mismo modelo de datos para la página de contenido y las solicitudes masivas. Para solicitudes masivas, varios clientes necesitan datos diferentes. Así que necesitábamos un análisis para comprender quién necesita qué parte de los datos. Hicimos un análisis exhaustivo con todos nuestros clientes juntos.
Después del análisis, nos dimos cuenta de que solo necesitábamos algunos de los datos, y casi todos nuestros clientes requerían casi las mismas partes. Pero no pudimos tocar el esquema porque algunos de nuestros clientes usan la misma fuente de datos directamente. Estamos estrechamente unidos. En conclusión, separar las fuentes de datos podría ser un paso hacia una solución.
¿Qué tenemos ahora hasta ahora?
Un servicio que escucha los cambios de una fuente de datos de contenido realiza la conversión pesada y escribe en una nueva fuente de datos. Y sabemos por el análisis que solo necesitamos algunos de los datos. Entonces podemos omitir todas las partes innecesarias mientras hacemos la conversión.
¿Estás listo? Guardamos el 77% de los datos (1,28 TB). ¡Qué alivio!

¿Ahora que?
Necesitamos algo que use los datos que preparamos. Queríamos separar las necesidades de contenido masivo y las necesidades de la página de detalles del producto solo porque sus clientes, necesidades de escala y conjuntos de reglas comerciales difieren. Entonces, es hora de escribir un nuevo servicio.
¿Qué se puede hacer diferente?
Tal vez las tecnologías que usamos. El servicio anterior usaba Java 8 y Spring boot. Tuvimos una experiencia estable con Quarkus . Quizás podríamos reducir el uso de recursos y el tiempo de arranque para ser más escalables. O estábamos usando Flux para adquirir los documentos de Couchbase, y tal vez podríamos usar CompletableFuture para variar. Después de enumerar nuestras opciones, desarrollamos muchas aplicaciones POC y ejecutamos muchas pruebas de carga. Pero los resultados podrían haber sido mejores. Al menos no por el esfuerzo que vamos a necesitar. La investigación debe continuar.
Por cierto, he sido un topo durante cuatro años. Creo que podemos obtener los documentos de manera efectiva con goroutines en paralelo. Así que quería probar eso. Luego, miré el SDK oficial de Couchbase Go, que tiene una función de "obtención masiva de documentos". ¡Eso fue genial! Las necesidades eran adecuadas para usar Go. Y quería probar ambas opciones.
Realicé pruebas de carga tanto en rutinas paralelas como en la función masiva. Los tiempos de respuesta y el uso de recursos mejoraron significativamente para ambos. La característica masiva fue mejor que nuestra implementación simple. Pero algo estaba mal. El uso de la red fue cuatro veces mayor. Podríamos dar una configuración a la conexión de Couchbase en Java SDK que permita la compresión. Pero Go SDK no tiene una configuración de compresión en sus opciones de configuración de clúster. Así que me perdí configurar en consecuencia. Lamentablemente, la función de compresión no se pudo controlar a través de Couchbase Go SDK, a diferencia de Couchbase Java SDK. Así que investigué su código, hice una depuración dolorosa y descubrí que la compresión se podía controlar a través de una variable de consulta al final de la cadena de conexión.
couchbase://{HOST_HERE}?compression=true
- Tenemos una ganancia masiva en el uso de RAM y el uso de memoria disminuyó de 800 MB a 60 MB por módulo .
- El tiempo de respuesta se ha ido a la mitad. 10 ms a 5 ms . Y es más estable frente a un alto rendimiento.
- El uso de la CPU se reduce drásticamente. Para alcanzar los 5 millones con el servicio anterior, necesitábamos más módulos. Así que esta será una comparación del uso total de la CPU. Ahora son 130 núcleos en lugar de 300 .
- El tamaño total del documento ahora es menos de una cuarta parte del anterior, de 1,67 TB a solo 386 GB .
- Naturalmente, la carga de la red es mucho menor.
- ¡El tiempo de arranque ahora es de 85 milisegundos en lugar de 12 segundos !

Gracias a Emre Odabas por su apoyo y aliento al escribir este artículo.