Si está realizando solicitudes internet con WebClient de Spring Boot, tal vez, al igual que nosotros, lea que la definición de la URL de su solicitud debe realizarse mediante un generador de URI (por ejemplo, Cliente internet Spring 5):
Si ese es el caso, le recomendamos que ignore lo que lee (a menos que su pasatiempo sea buscar fugas de memoria difíciles de encontrar) y use lo siguiente para construir un URI en su lugar:
En esta publicación de weblog, explicaremos cómo evitar pérdidas de memoria con Spring Boot WebClient y por qué es mejor evitar el patrón anterior, usando nuestra experiencia private como motivación.
¿Cómo descubrimos esta fuga de memoria?
Hace un tiempo, actualizamos nuestra aplicación para usar la última versión del marco Axle. Axle es el marco de bol.com para crear aplicaciones Java, como servicios (REST) y aplicaciones frontend. Se basa en gran medida en Spring Boot y esta actualización también implicó la actualización de Spring Boot versión 2.3.12 a la versión 2.4.11.
Al ejecutar nuestras pruebas de rendimiento programadas, todo se veía bien. La mayoría de los puntos finales de nuestra aplicación aún brindaban tiempos de respuesta de menos de 5 milisegundos. Sin embargo, a medida que avanzaba la prueba de rendimiento, notamos que los tiempos de respuesta de nuestra aplicación aumentaban hasta 20 milisegundos y, después de una larga prueba de carga durante el fin de semana, las cosas empeoraron mucho. Los tiempos de respuesta se dispararon a segundos, no es bueno.
(caption id=”attachment_7280″ align=”aligncenter” width=”680″)Antes de la actualización del eje: tiempos de respuesta del percentil 90 superior de uno de nuestros puntos finales(/caption)(caption id=”attachment_7281″ align=”aligncenter” width= “680”)Después de la actualización del eje: tiempos de respuesta del percentil 90 superior del mismo punto closing(/título)
Después de un largo concurso de miradas con nuestros tableros de Grafana, que brindan información sobre el uso de CPU, subprocesos y memoria de nuestra aplicación, este patrón de uso de memoria nos llamó la atención:
Este gráfico muestra el tamaño del montón de JVM antes, durante y después de una prueba de rendimiento que se ejecutó de 21:00 a 0:00. Durante la prueba de rendimiento, la aplicación creó subprocesos y objetos para manejar todas las solicitudes entrantes. Entonces, la línea caprichosa que muestra el uso de la memoria durante este período es exactamente lo que esperaríamos. Sin embargo, cuando se asiente el polvo de la prueba de rendimiento, esperaríamos que la memoria también se asiente al mismo nivel que antes, pero en realidad es más alto. ¿Alguien más huele una pérdida de memoria?
Es hora de llamar a MAT (Eclipse Reminiscence Analyzer Instrument) para averiguar qué causa esta pérdida de memoria.
¿Qué causó esta pérdida de memoria?
Para solucionar este problema de pérdida de memoria:
- Reinicie la aplicación.
- Realizó un volcado de pila (una instantánea de todos los objetos que están en la memoria en la JVM en un momento determinado).
- Desencadenó una prueba de rendimiento.
- Realizó otro volcado de pila una vez que finaliza la prueba.
Esto nos permitió usar la característica avanzada de MAT para detectar los sospechosos de fugas al comparar dos volcados de almacenamiento dinámico tomados con cierto tiempo de diferencia. Pero no tuvimos que ir tan lejos, ya que el volcado de almacenamiento dinámico después de la prueba fue suficiente para que MAT encontrara algo sospechoso:
Aquí MAT nos cube que una instancia de AutoConfiguredCompositeMeterRegistry de Spring Boot ocupa casi 500 MB, que es el 74 % del tamaño complete del almacenamiento dinámico utilizado. También nos cube que tiene un hashmap (concurrente) que se encarga de esto. ¡Casi estámos allí!
con MAT árbol dominador característica, podemos enumerar los objetos más grandes y ver qué mantuvieron con vida. Eso suena útil, así que usémoslo para echar un vistazo a lo que hay dentro de este enorme hashmap:
Usando el árbol de dominadores, pudimos navegar fácilmente a través del contenido del hashmap. En la imagen de arriba abrimos dos nodos hashmap. Aquí vemos muchos cronómetros micrométricos etiquetados con “v2/merchandise/…” y una identificación de producto. Hmm, ¿dónde hemos visto eso antes?
¿Qué tiene que ver WebClient con esto?
Entonces, son las métricas de Spring Boot las responsables de esta pérdida de memoria, pero ¿qué tiene que ver WebClient con esto? Para averiguarlo, realmente debe comprender qué hace que las métricas de Spring almacenen todos estos temporizadores.
Al inspeccionar la implementación de AutoConfiguredCompositeMeterRegistry, vemos que almacena las métricas en un hashmap llamado meterMap. Por lo tanto, coloquemos un punto de interrupción bien ubicado en el lugar donde se agregan nuevas entradas y activamos nuestra llamada sospechosa que realiza nuestro WebClient al punto closing “v2/product/{productId}”.
Ejecutamos de nuevo la aplicación y… Entendido! Para cada llamada que hace WebClient al punto closing “v2/product/{productId}”, vimos que Spring creaba un nuevo temporizador para cada instancia única de identificador de producto. A continuación, cada uno de esos temporizadores se almacena en el bean AutoConfiguredCompositeMeterRegistrybean. Eso explica por qué vemos tantos temporizadores con etiquetas como estas:
/v2/productos/9200000109074941 /v2/productos/9200000099621587
¿Cómo puedes arreglar esta pérdida de memoria?
Antes de identificar cuándo podría afectarle esta fuga de memoria, primero expliquemos cómo se solucionaría. Hemos mencionado en la introducción, que simplemente al no usar un generador de URI para construir las URL de WebClient, puede evitar esta pérdida de memoria. ahora vamos a explicar por qué funciona.
Después de investigar un poco en línea, encontramos esta publicación (https://rieckpil.de/expose-metrics-of-spring-webclient-using-spring-boot-actuator/) de Philip Riecks, en la que explica:
“Como normalmente queremos la cadena URI con plantilla como “/todos/{id}” para informes y no múltiples métricas, por ejemplo, “/todos/1337” o “/todos/42″. WebClient ofrece varias formas de construir el URI (. ..), que todos pueden usar, excepto uno”.
Y ese método está usando el generador de URI, coincidentemente el que estamos usando:
De hecho, cuando construimos la URI de esa manera, la fuga de memoria desaparece. Además, los tiempos de respuesta han vuelto a la normalidad.
¿Cuándo podría afectarte la pérdida de memoria? – una respuesta sencilla
¿Necesita preocuparse por esta pérdida de memoria? Bueno, empecemos por el caso más obvio. Si su aplicación expone sus métricas de cliente HTTP y utiliza un método que utiliza un generador de URI para configurar un URI con plantilla en un WebClient, definitivamente debería preocuparse.
Puede verificar fácilmente si su aplicación expone las métricas del cliente http de dos maneras diferentes:
- Inspeccionar el “/actuador/métricas/http.shopper.requests” punto closing de su aplicación Spring Boot después de que su aplicación haya realizado al menos una llamada externa. Un 404 significa que su aplicación no los expone.
- Verificar si el valor de la propiedad de la aplicación administration.metrics.allow.http.shopper.metrics está establecido en verdadero, en cuyo caso su aplicación los expone.
Sin embargo, esto no significa que esté seguro si no expone las métricas del cliente HTTP. Hemos pasado URI con plantilla al WebClient usando un constructor durante mucho tiempo y nunca hemos expuesto nuestras métricas de cliente HTTP. Sin embargo, de repente, esta fuga de memoria apareció después de una actualización de la aplicación.
Entonces, ¿podría afectarte esta pérdida de memoria? Simplemente no use constructores de URI con su WebClient y debería estar protegido contra esta posible pérdida de memoria. Esa sería la respuesta sencilla. ¿No aceptas respuestas simples? Bastante justo, sigue leyendo para descubrir qué fue lo que realmente nos causó esto.
¿Cuándo podría afectarte la pérdida de memoria? – una respuesta más completa
Entonces, ¿cómo una easy actualización de la aplicación hizo que esta fuga de memoria apareciera? Evidentemente, la adición de una dependencia transitiva de Prometheus (https://prometheus.io/), un marco de monitoreo y alertas de código abierto, provocó la fuga de memoria en nuestro caso explicit. Para entender por qué, volvamos a la situación anterior a la adición de Prometheus.
Antes de arrastrar la biblioteca de Prometheus, enviamos nuestras métricas a statsd (https://github.com/statsd/statsd), un demonio de purple que escucha y agrega métricas de aplicaciones enviadas a través de UDP o TCP. El StatsdMeterRegistry que forma parte del marco Spring es responsable de enviar las métricas a statsd. El StatsdMeterRegistry solo envía métricas que no están filtradas por un MeterFilter. La propiedad administration.metrics.allow.http.shopper.metrics es un ejemplo de este tipo de MeterFilter. En otras palabras, si administration.metrics.allow.http.shopper.metrics = false, StatsdMeterRegistry no enviará ninguna métrica de cliente HTTP a statsd y tampoco almacenará estas métricas en la memoria. Hasta ahora tan bueno.
Al agregar la dependencia transitiva de Prometheus, agregamos otro registro de medidor a nuestra aplicación, PrometheusMeterRegistry. Cuando hay más de un registro de medidor para exponer las métricas, Spring instancia un bean CompositeMeterRegistry. Este bean realiza un seguimiento de todos los registros de medidores individuales, recopila todas las métricas y las reenvía a todos los delegados que posee. Es la adición de este frijol lo que causó el problema.
El problema es que las instancias de MeterFilter no se aplican a CompositeMeterRegistry, sino solo a las instancias de MeterRegistry en CompositeMeterRegistry (consulte este comprometerse para obtener más información). Eso explica por qué el registro de medidor compuesto autoconfigurado acumula todas las métricas del cliente HTTP en la memoria, incluso cuando establecemos explícitamente administration.metrics.allow.http.shopper.metrics en falso.
¿Sigo confundido? No se preocupe, simplemente no use constructores de URI con su WebClient y debería estar protegido contra esta pérdida de memoria.
Conclusión
En esta publicación de weblog, explicamos que es mejor evitar este enfoque de definir las URL de su solicitud con WebClient de Spring Boot: