¿Cómo mejorar significativamente el rendimiento de Java?

23

El equipo de LMAX hizo una presentación sobre cómo pudieron hacer 100k TPS a menos de 1 ms de latencia . Han respaldado esa presentación con un blog , un documento técnico (PDF) y el código fuente en sí.

Recientemente, Martin Fowler publicó un excelente artículo sobre la arquitectura LMAX y menciona que ahora pueden manejar seis millones de pedidos por segundo y destaca algunos de los pasos que el equipo tomó para subir otro orden de magnitud en el rendimiento.

Hasta ahora he explicado que la clave de la velocidad del procesador de lógica de negocios es hacer todo de forma secuencial, en memoria. Simplemente hacer esto (y nada realmente estúpido) permite a los desarrolladores escribir código que pueda procesar 10K TPS.

Luego descubrieron que concentrarse en los elementos simples de un buen código podría llevar esto al rango de 100K TPS. Esto solo necesita código bien factorizado y métodos pequeños, esencialmente esto permite que Hotspot haga un mejor trabajo de optimización y que las CPU sean más eficientes en el almacenamiento en caché del código mientras se ejecuta.

Se necesitó un poco más de inteligencia para subir otro orden de magnitud. Hay varias cosas que el equipo de LMAX encontró útiles para llegar allí. Una fue escribir implementaciones personalizadas de las colecciones de Java que fueron diseñadas para ser amigables con la caché y cuidadosas con la basura.

Otra técnica para alcanzar ese nivel superior de rendimiento es poner atención en las pruebas de rendimiento. Hace mucho que noté que la gente habla mucho sobre técnicas para mejorar el rendimiento, pero lo único que realmente marca la diferencia es probarlo.

Fowler mencionó que se encontraron varias cosas, pero solo mencionó un par.

¿Hay otras arquitecturas, bibliotecas, técnicas o "cosas" que sean útiles para alcanzar esos niveles de rendimiento?

Dakotah North
fuente
11
"¿Qué otras arquitecturas, bibliotecas, técnicas o" cosas "son útiles para alcanzar esos niveles de rendimiento?" ¿Por qué preguntar? Esa cita es la lista definitiva. Hay muchísimas otras cosas, ninguna de las cuales tiene el impacto amable de los elementos de esa lista. Cualquier otra cosa que alguien pueda nombrar no será tan útil como esa lista. ¿Por qué pedir malas ideas cuando ha citado una de las mejores listas de optimización jamás producidas?
S.Lott
Sería bueno saber qué herramientas utilizaron para ver cómo se ejecuta el código generado en el sistema.
1
He oído hablar de personas que juran por todo tipo de técnicas. Lo que he encontrado más efectivo es el perfil de nivel de sistema. Puede mostrarle cuellos de botella en la forma en que su programa y carga de trabajo ejercitan el sistema. Sugeriría adherirse a pautas bien conocidas con respecto al rendimiento y escribir código modular para que pueda ajustarlo fácilmente más tarde ... No creo que pueda salir mal con la creación de perfiles del sistema.
ritesh

Respuestas:

21

Hay todo tipo de técnicas para el procesamiento de transacciones de alto rendimiento y la que se encuentra en el artículo de Fowler es solo una de las más avanzadas. En lugar de enumerar un montón de técnicas que pueden o no ser aplicables a la situación de cualquier persona, creo que es mejor discutir los principios básicos y cómo LMAX aborda una gran cantidad de ellos.

Para un sistema de procesamiento de transacciones a gran escala, desea hacer todo lo siguiente tanto como sea posible:

  1. Minimice el tiempo que pasa en los niveles de almacenamiento más lentos. De más rápido a más lento en un servidor moderno que tiene: CPU / L1 -> L2 -> L3 -> RAM -> Disco / LAN -> WAN. El salto desde incluso el disco magnético moderno más rápido a la RAM más lenta es más de 1000x para acceso secuencial ; El acceso aleatorio es aún peor.

  2. Minimice o elimine el tiempo de espera . Esto significa compartir el menor estado posible y, si el estado debe compartirse, evitar bloqueos explícitos siempre que sea posible.

  3. Difundir la carga de trabajo. Las CPU no se han vuelto mucho más rápidas en los últimos años, pero se han vuelto más pequeñas y 8 núcleos son bastante comunes en un servidor. Más allá de eso, incluso puede distribuir el trabajo en varias máquinas, que es el enfoque de Google; Lo mejor de esto es que escala todo, incluidas las E / S.

Según Fowler, LMAX adopta el siguiente enfoque para cada uno de estos:

  1. Mantenga todo el estado en la memoria en todo momento. La mayoría de los motores de bases de datos lo harán de todos modos, si toda la base de datos puede caber en la memoria, pero no quieren dejar nada al azar, lo cual es comprensible en una plataforma de negociación en tiempo real. Para lograr esto sin agregar un montón de riesgos, tuvieron que construir un montón de infraestructura de respaldo y conmutación por error liviana.

  2. Use una cola sin bloqueo ("disruptor") para la secuencia de eventos de entrada. Contraste con las colas de mensajes duraderas tradicionales que definitivamente no están libres de bloqueo y, de hecho, generalmente implican transacciones distribuidas dolorosamente lentas .

  3. No mucho. LMAX lanza este debajo del bus sobre la base de que las cargas de trabajo son interdependientes; el resultado de uno cambia los parámetros para los otros. Esta es una advertencia crítica , y que Fowler llama explícitamente. Hacen alguna uso de concurrencia con el fin de proporcionar capacidades de conmutación por error, pero toda la lógica de negocio se procesa en un solo hilo .

LMAX no es el único enfoque para OLTP a gran escala. Y aunque es bastante brillante por derecho propio, no es necesario utilizar técnicas de vanguardia para lograr ese nivel de rendimiento.

De todos los principios anteriores, el # 3 es probablemente el más importante y el más efectivo, porque, francamente, el hardware es barato. Si puede dividir adecuadamente la carga de trabajo en media docena de núcleos y varias docenas de máquinas, entonces el cielo es el límite para las técnicas convencionales de computación paralela . Te sorprendería la cantidad de rendimiento que puedes lograr con nada más que un montón de colas de mensajes y un distribuidor de turnos. Obviamente, no es tan eficiente como LMAX, en realidad ni siquiera cercano, pero el rendimiento, la latencia y la rentabilidad son preocupaciones separadas, y aquí estamos hablando específicamente sobre el rendimiento.

Si tiene el mismo tipo de necesidades especiales que LMAX tiene, en particular, un estado compartido que corresponde a una realidad empresarial en lugar de una elección de diseño apresurada, entonces sugeriría probar su componente, porque no he visto mucho De lo contrario, es adecuado para esos requisitos. Pero si simplemente estamos hablando de alta escalabilidad, entonces le insto a que investigue más sobre los sistemas distribuidos, porque son el enfoque canónico utilizado por la mayoría de las organizaciones hoy en día (Hadoop y proyectos relacionados, ESB y arquitecturas relacionadas, CQRS que Fowler también menciona, y así sucesivamente).

Los SSD también se convertirán en un cambio de juego; posiblemente, ya lo son. Ahora puede tener almacenamiento permanente con tiempos de acceso similares a la RAM, y aunque los SSD de nivel de servidor siguen siendo terriblemente caros, eventualmente bajarán de precio una vez que aumenten las tasas de adopción. Se ha investigado ampliamente y los resultados son bastante alucinantes y solo mejorarán con el tiempo, por lo que todo el concepto de "mantener todo en la memoria" es mucho menos importante de lo que solía ser. Entonces, una vez más, trataría de centrarme en la concurrencia siempre que sea posible.

Aaronaught
fuente
Discutir los principios es principios subyacentes es excelente y su comentario es excelente y ... a menos que el documento de Fowler no haya tenido una referencia en una nota al pie para almacenar algoritmos ajenos en.wikipedia.org/wiki/Cache-oblivious_algorithm (que encaja muy bien en categoría número 1 que tienes arriba) Nunca me habría topado con ellos. Entonces ... con respecto a cada categoría que tienes arriba, ¿conoces las 3 cosas principales que una persona debería saber?
Dakotah North
@Dakotah: Ni siquiera comenzaría a preocuparme por la ubicación de la memoria caché a menos y hasta que haya eliminado por completo la E / S del disco, que es donde se pasa la gran mayoría del tiempo esperando en la gran mayoría de las aplicaciones. Aparte de eso, ¿qué quieres decir con "3 cosas principales que una persona debe saber"? Top 3 qué, saber sobre qué?
Aaronaught
El salto desde la latencia de acceso RAM (~ 10 ^ -9s) a la latencia del disco magnético (~ 10 ^ -3s en el caso promedio) es otro orden de magnitud superior a 1000x. Incluso los SSD todavía tienen tiempos de acceso medidos en cientos de microsegundos.
Sedate Alien
@Sedate: Latencia, sí, pero esto es más una cuestión de rendimiento que de latencia sin procesar, y una vez que pasa los tiempos de acceso y alcanza la velocidad de transferencia total, los discos no son tan malos. Es por eso que hice la distinción entre acceso aleatorio y secuencial; para los escenarios de acceso aleatorio que no se convierta principalmente un problema de latencia.
Aaronaught
@Aaronaught: Al volver a leer, supongo que tienes razón. Tal vez debería señalarse que todo el acceso a datos debe ser lo más secuencial posible; También se pueden obtener beneficios significativos al acceder a los datos en orden desde la RAM.
Sedate Alien
10

Creo que la mayor lección para aprender de esto es que debes comenzar con lo básico:

  • Buenos algoritmos, estructuras de datos apropiadas y no hacer nada "realmente estúpido"
  • Código bien factorizado
  • Pruebas de rendimiento

Durante las pruebas de rendimiento, puede perfilar su código, encontrar los cuellos de botella y corregirlos uno por uno.

Demasiadas personas saltan directamente a la parte "arreglarlas una por una". Pasan mucho tiempo escribiendo "implementaciones personalizadas de las colecciones de Java", porque saben que la razón por la cual su sistema es lento es debido a errores de caché. Eso puede ser un factor contribuyente, pero si saltas directamente a modificar código de bajo nivel como ese, es probable que te pierdas el problema más grande de usar una ArrayList cuando deberías usar una LinkedList, o que la verdadera razón es que tu sistema es lento es porque su ORM está cargando de manera lenta a los hijos de una entidad y, por lo tanto, realiza 400 viajes separados a la base de datos para cada solicitud.

Adam Jaskiewicz
fuente
7

No comentaré particularmente sobre el código LMAX porque creo que está ampliamente descrito, pero aquí hay algunos ejemplos de cosas que he hecho que han resultado en mejoras significativas de rendimiento medibles.

Como siempre, estas son técnicas que deben aplicarse una vez que sepa que tiene un problema y necesita mejorar el rendimiento ; de lo contrario, es probable que solo esté haciendo una optimización prematura.

  • Utilice la estructura de datos correcta y cree una personalizada si es necesario : el diseño correcto de la estructura de datos eclipsa la mejora que obtendrá de las micro optimizaciones, así que hágalo primero. Si su algoritmo depende del rendimiento en muchas lecturas rápidas de acceso aleatorio O (1), ¡asegúrese de tener una estructura de datos que lo admita! Vale la pena saltar a través de algunos aros para hacer esto bien, por ejemplo, encontrar una manera de representar sus datos en una matriz para explotar lecturas indexadas O (1) muy rápidas.
  • La CPU es más rápida que el acceso a la memoria : puede hacer muchos cálculos en el tiempo que lleva hacer que una memoria aleatoria se lea si la memoria no está en el caché L1 / L2. Por lo general, vale la pena hacer un cálculo si le ahorra una lectura de memoria.
  • Ayudar al compilador JIT con la finalización de campos, métodos y clases finales permite optimizaciones específicas que realmente ayudan al compilador JIT. Ejemplos específicos:

    • El compilador puede suponer que una clase final no tiene subclases, por lo que puede convertir las llamadas a métodos virtuales en llamadas a métodos estáticos
    • El compilador puede tratar los campos finales estáticos como una constante para una mejora agradable del rendimiento, especialmente si la constante se usa en cálculos que se pueden calcular en tiempo de compilación.
    • Si un campo que contiene un objeto Java se inicializa como final, entonces el optimizador puede eliminar tanto la verificación nula como el envío del método virtual. Agradable.
  • Reemplace las clases de colección con matrices : esto da como resultado un código menos legible y es más difícil de mantener, pero casi siempre es más rápido, ya que elimina una capa de indirección y se beneficia de muchas optimizaciones agradables de acceso a la matriz. Por lo general, una buena idea en los bucles internos / código sensible al rendimiento después de haberlo identificado como un cuello de botella, ¡pero evite lo contrario por razones de legibilidad!

  • Utilice primitivas siempre que sea posible : las primitivas son fundamentalmente más rápidas que sus equivalentes basados ​​en objetos. En particular, el boxeo agrega una gran cantidad de sobrecarga y puede causar pausas desagradables en el GC. No permita que se empaqueten primitivas si le preocupa el rendimiento / la latencia.

  • Minimice el bloqueo de bajo nivel : los bloqueos son muy caros a un nivel bajo. Encuentre formas de evitar el bloqueo por completo, o bloquee en un nivel de grano grueso para que solo necesite bloquear con poca frecuencia en grandes bloques de datos y el código de bajo nivel pueda continuar sin tener que preocuparse por los problemas de bloqueo o concurrencia.

  • Evite asignar memoria : esto podría retrasarlo en general, ya que la recolección de basura de JVM es increíblemente eficiente, pero es muy útil si está tratando de alcanzar una latencia extremadamente baja y necesita minimizar las pausas de GC. Existen estructuras de datos especiales que puede usar para evitar asignaciones: la biblioteca http://javolution.org/ en particular es excelente y destaca por ellas.
mikera
fuente
No estoy de acuerdo con hacer que los métodos sean finales . El JIT es capaz de descubrir que un método nunca se anula. Además, en caso de que una subclase se cargue más tarde, puede deshacer la optimización. También tenga en cuenta que "evitar la asignación de memoria" también puede dificultar el trabajo del GC y, por lo tanto, retrasarlo, así que úselo con precaución.
maaartinus
@maaartinus: con respecto a finalalgunos JIT podrían resolverlo, otros podrían no. Depende de la implementación (al igual que muchos consejos de ajuste de rendimiento). Acuerde las asignaciones: debe comparar esto. Por lo general, he descubierto que es mejor eliminar las asignaciones, pero YMMV.
mikera
4

Aparte de lo que ya se indicó en una excelente respuesta de Aaronaught, me gustaría señalar que un código como ese podría ser bastante difícil de desarrollar, comprender y depurar. "Si bien es muy eficiente ... es muy fácil fastidiar ..." como uno de sus muchachos mencionó en el blog de LMAX .

  • Para un desarrollador acostumbrado a las consultas y bloqueos tradicionales , la codificación de un nuevo enfoque puede ser como montar el caballo salvaje. Al menos esa fue mi propia experiencia al experimentar con Phaser, concepto que se menciona en el documento técnico de LMAX. En ese sentido, diría que este enfoque intercambia la contención de bloqueo por la contención cerebral del desarrollador .

Dado lo anterior, creo que aquellos que eligen Disruptor y enfoques similares se aseguran mejor de que tengan recursos de desarrollo suficientes para mantener su solución.

En general, el enfoque disruptivo me parece bastante prometedor. Incluso si su empresa no puede permitirse el lujo de utilizarlo, por ejemplo, por los motivos mencionados anteriormente, considere convencer a su gerencia para que "invierta" algo de esfuerzo en estudiarlo (y SEDA en general), porque si no lo hacen, existe la posibilidad de que algún día sus clientes los dejarán a favor de alguna solución más competitiva que requiera 4x, 8x, etc. menos servidores.

mosquito
fuente