2 puntos por GN⁺ 2025-03-01 | 1 comentarios | Compartir por WhatsApp
  • En el pasado, el uso de CPU de mi sistema llegó al 3,200%, saturando por completo los 32 núcleos
  • Estaba usando el runtime de Java 17 y, al revisar el tiempo de CPU en un volcado de hilos y ordenar por tiempo de CPU, encontré muchos hilos similares
  • Análisis del código problemático
    • A través del stack trace, confirmé la línea 29 de la clase BusinessLogic
    • Ese código recorría la lista unrelatedObjects mientras insertaba el valor de relatedObject en treeMap
    • Era un código ineficiente porque no usaba unrelatedObject dentro del bucle

Corrección del código y pruebas

  • Se eliminó el bucle innecesario y se cambió por una sola línea: treeMap.put(relatedObject.a(), relatedObject.b());
  • Se ejecutaron pruebas unitarias antes y después del cambio, pero no fue posible reproducir el problema
  • Incluso cuando treeMap y unrelatedObjects tenían más de 1,000,000 de elementos cada uno, el problema no aparecía

Descubrimiento de la causa del problema

  • treeMap estaba siendo accedido de forma concurrente por varios hilos y no tenía sincronización
  • El problema ocurría porque varios hilos modificaban TreeMap al mismo tiempo

Reproducción del problema mediante un experimento

  • Se realizó un experimento en el que varios hilos actualizaban aleatoriamente un TreeMap compartido
  • Se configuró un bloque try-catch para ignorar NullPointerException
  • Como resultado del experimento, se confirmó un fenómeno en el que el uso de CPU subía hasta 500%

Conclusión

  • La modificación concurrente de un TreeMap sin sincronización puede causar problemas graves de rendimiento
  • Para evitar este tipo de problemas, se recomienda sincronizar TreeMap o usar una colección thread-safe como ConcurrentMap

1 comentarios

 
GN⁺ 2025-03-01
Comentarios de Hacker News
  • Pensaba que una condición de carrera causaba corrupción de datos o deadlocks, pero no había considerado que también pudiera provocar problemas de rendimiento. Los datos pueden corromperse de una forma que genere un bucle infinito

    • Creo que los errores, comportamientos anómalos y advertencias en un proyecto, por principio, deben corregirse. Esto se debe a que pueden provocar problemas no relacionados
    • Es bien sabido que las colecciones principales de Java no son thread-safe por diseño. El OP también debería revisar si varios hilos están manipulando colecciones en otras partes del código
    • La solución más sencilla es envolver TreeMap con Collections.synchronizedMap o cambiar a ConcurrentHashMap y ordenar cuando sea necesario
    • Se puede hacer que las operaciones individuales del mapa sean thread-safe, pero no se puede asegurar que una secuencia de operaciones lo sea. Tampoco se puede asegurar que el objeto que posee el TreeMap sea thread-safe
    • Como solución discutible, rastrear los nodos visitados no es una buena idea. La colección sigue sin ser thread-safe y puede fallar de otras formas más sutiles
    • Un desarrollador atento a los detalles podría notar la combinación de hilos y TreeMap, o sugerir no usar TreeMap si no se necesitan elementos ordenados. Pero en este caso no ocurrió
    • El problema es que se violó el contrato de la colección, y aunque se cambie TreeMap por HashMap, seguiría estando mal
  • En código donde funcionan varios hilos, la única estrategia realmente segura es hacer que todos los objetos sean inmutables, y limitar los objetos que no puedan ser inmutables a secciones pequeñas, autocontenidas y estrictamente controladas

    • Reescribieron los módulos centrales siguiendo estos principios, y se transformaron de una fuente constante de problemas en una de las secciones más resilientes de la base de código
    • Tener estas pautas hizo que las revisiones de código fueran mucho más fáciles
  • La mención de que "casi no se podía entrar por ssh" me recordó una situación de la época del posgrado, cuando usaban un Sun UltraSparc 170

    • Un usuario nuevo o un estudiante intentó ejecutar trabajo en paralelo, dividió un archivo de texto grande en varias secciones según los números de línea y procesó cada sección en paralelo
    • Se usó mucha RAM, y los intentos de swap hacían una búsqueda frenética para leer otras secciones del mismo archivo
    • No se podía obtener el prompt de login en la consola, pero ya había una sesión iniciada y se pudo obtener una sesión de root para resolver el problema
    • El problema fue no entender los límites del sistema
  • El código podría reducirse simplemente a lo siguiente

    • El código original solo hace treeMap.put cuando <i>unrelatedObjects</i> no está vacío. Eso también podría ser un bug
    • Hay que verificar si <i>a</i> y <i>b</i> devuelven siempre el mismo valor, y si treeMap realmente se comporta como un mapa
  • Otra forma de obtener un bucle infinito es usar una implementación de <i>Comparator</i> o <i>Comparable</i> que no implemente un orden total consistente

    • Esto no tiene relación con la concurrencia, y puede ocurrir según ciertos datos y el orden de procesamiento
  • Se podría considerar detectar ciclos usando un contador incremental y lanzar una excepción si supera la profundidad del árbol o el tamaño de la colección

    • Esto casi no requiere sobrecarga de memoria ni de CPU, y es más probable que sea aceptado
  • En Java, realizar operaciones concurrentes sobre objetos que no son thread-safe produce los bugs más interesantes

  • Existe la pregunta de si un TreeMap sin protección puede causar una utilización del 3,200%

    • Vi un problema similar alrededor de 2009, y todavía puede pasar
    • Resulta decepcionante para quienes creen que una carrera de datos es solo algo ligeramente malo
  • El autor encontró una especie de Poison Pill. Esto es más común en sistemas de event sourcing: un mensaje que mata todo lo que toca

    • Cuando una estructura de datos llega a un estado ilegal, todos los hilos posteriores quedan atrapados en la misma bomba lógica
  • Las excepciones en hilos son un problema absoluto

    • Hay historias de caza de bugs de terror con C++, select() y hilos lanzando excepciones