Spring Boot y cientos de peticiones por segundo

soulsville

Buenas a todos,
llevo un tiempo intentando mejorar el rendimiento de una pequeña aplicación de pruebas que consiste en atender y despachar compras de X en orden de llegada (entradas, tickets o lo que sea, da igual) de tal forma que al menos consiga alcanzar algo aceptable como cientos de peticiones por segundo consistentemente. Hasta el momento no he podido avanzar sin perjuicio de un funcionamiento poco fiable: errores de transacciones en db, lentitud, etc
El escenario es relativamente sencillo: N clientes realizan 1 o más compras de estos elementos, se entregan y se descuenta el importe asociado a la compra d un monedero.

¿Cuál es la mejor forma de abordar este tipo de escenarios donde se requiere atender un alto número de peticiones por segundo pero manteniendo y despachando consistemente y efizcamente los elementos?

He probado con DeferredResults y separando los 3 procesos en diferentes transacciones (@Transactional/@Async...) pero no consigo mejorar gran cosa.
Gracias.

eXtreM3

A ver si alguien se pasa, me interesa el tema también. "Cientos" de peticiones por segundo cuántas son, 100, 200, 500, 1000? Suponiendo 200, son 17 millones al día, es una barbaridad para que lo controles todo en un mismo servidor o base de datos.

soulsville

Ya estaría satisfecho con >100/200, pero desconozco cómo abordar este tipo de problemas, especialmente cuando se tiene que seguir un orden estricto en cuanto al procesamiento de la petición (al 1ª en llegar - 1 X libre etc) y se dan escenarios de concurrencia elevada. No tiene por qué ser cada segundo, pero sí ser capaz de atender estas situaciones.

He probado varias soluciones y la que más garantías me da (pero con un rendimiento pobre) es buscar y servir los elementos disponibles en un método con la anotación Transactional e isolation=Serializable (utilizando un Lock Pessimistic Write en la consulta), pero a la mínima se degrada mucho el rendimiento (>8 peticiones) y se produce un cuello de botella.

En fin, habrá que seguir probando.

Wei-Yu

¿Meterlo en una cola, message bus o algo parecido para procesarlo de fondo es viable?

crb2222

Vas a tener que tener procesos distribuidos, una maquina dudo te aguante eso

Y meterte en concurrencia distribuida es un dolor de cabeza, al menos las pase putas en la uni con esto.
En la vida real imagino que habrán soluciones más transparentes que usar tu mismo los algoritmos de concurrencia, como lamport

Lecherito

Pero que estas haciendo para que 8 requests/s el rendimiento se vaya a la mierda? La verdad es que tampoco das demasiada informacion como para que se te ayude mucho.

He visto una base de datos tipica postgre con 1000s de transacciones por segundo sin despeinarse

1 1 respuesta
MTX_Anubis

madre mía, procesos distribuidos dice uno xDDDDD

Estoy con #6, no entiendo cómo te puede ir tan lento a no ser que hagas 200 consultas dentro de la transacción.

Con un threadpool executor de un solo hilo y ejecutando ahí las peticiones que te vayan entrando también debería valerte aunque el rendimiento debería ser parecido al de un Pessimistic Write. Es esperar a obtener el resultado con un CompletableFuture o algo así y ya está. En tu caso es lo que probaría para hacer una prueba rápida.

O puedes usar akka o puedes usar un sistema de colas que garanticen el orden.

También puedes decirlos que hace el código y sobre que máquina se está ejecutando pero vamos, que tienes que tener algún problema porque esas 8 por segundo son insultantes

1 1 respuesta
Lecherito

#7 De todas maneras tienes benchmarks para probar que es tu codigo y no spring xD

https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=db

Que spring para mi sigue siendo una mierda pero bueno, 8 request/s como dice Anubis, es algo insultante xD

soulsville

Os doy la razón es que es una mierda (no cuestiono la tecnología, al menos no con estas penosas pruebas). Acabo de probar ahora y lanzando 10 peticiones desde el JMeter comprando 1 elemento tarda sobre 10 segundos en despachar todo. Es lamentable.

Controller:

@RequestMapping("/putavida")
public DeferredResult<List<Elemento>> cola(@RequestParam int comprados, @RequestParam String id_cliente, @RequestParam String tiempo_compra) 
{
 // ...
		DeferredResult<List<Elemento>> result= new DeferredResult<List<Elemento>>();
		elementoService.testBusquedaDev(comprados)

	return result;
}
}

Búsqueda y devolución:

@Async
@Transactional(isolation=Isolation.SERIALIZABLE)
public void testBusquedaDev()
{
		Instant start = Instant.now();
		List<Elemento> elementos = em.createQuery("SELECT e FROM Elemento e WHERE disponible=true AND precio=:precio ORDER BY c.id_elemento).setParameter("precio", precio).setLockMode(LockModeType.PESSIMISTIC_WRITE).setMaxResults(comprados).getResultList();

	elementos.forEach(action -> action.setDisponible(false));
// Devolvemos resultado 
		req.setResult(elementos);
		Instant finish = Instant.now();
		long timeElapsed = Duration.between(start, finish).toMillis();

}

El hardware de pruebas donde corre la aplicación no es gran cosa, pero no puedo creer que sea tan lamentable.

  • Aplicación corriendo en W10 + 8GB RAM + i3 7100 CPU 3,9Ghz
  • DB MariaDB en una VM Debian9 con 4GB y 2sock/2core. Tabla 500k registros (distinta máquina a la mía)
Lecherito

Me da sida hacer asi las queries xDD

  1. Ah que el loadtest lo estas haciendo desde tu maquina a tu maquina?...
  2. Tienes metricas de cuanto se tarda en hacer cada cosa?
  3. Le has pasado un profiler?
  4. Necesitas toda esa mierda de Async y Transactional? xD
  5. Cuantas conexiones simultaneas a tu base de datos?
  6. Latencia en ir a la base de datos?

Si de verdad tienes un i3 con 8 gigas de ram y una vm con 0.5mill de registros no se que haces intentando rular un load test en esa mierda xD

1 respuesta
soulsville

#10 La VM está en otra máquina, pero el test lo lanzo desde la mía, efectivamente. Sé que las condiciones no son muy realistas, pero creo que no explica el rendimiento pésimo.

¿Cómo recomiendas hacer la consulta? Por cierto, soy retrasado. Acabo de fijarme que tenía mal un índice de la tabla (sólo el campo disponible en lugar de tener un índex compuesto del campo disponible/precio y el tiempo se ha reducido x10 xDDD Ahora mismo la media está en 10-12req/sec.

El tema del Transactional lo necesito para poder utilizar el aislamiento Serializable y el Pessimistic Write. De esta forma consigo que no se produzcan deadlocks o conflictos (al menos hasta ahora), pero entiendo que penaliza considerablemente al bloquear la/s fila/s y esperar a que el hilo la libere, no he visto otra forma mejor para ello. Esta es una de las preguntas principales del hilo: cómo diseñar una solución óptima para este tipo de escenarios

Tengo configurado el fichero de propiedades con un pool de 25 para Hikari después de leer algunos artículos sobre ello.
Editaré cuando tenga más info,

1 respuesta
Lecherito

#11 Pues yo te recomendaria ir poco a poco, haz una query de mierda o a la mierda los deadlocks o conflictos y ver cuando se produce la bajada enorme de eficiencia. Ahora mismo estas en un barco que se esta hundiendo y tienes habitaciones y no sabes cual se esta inundando xD

JuAn4k4

PESSIMISTIC_WRITE en un Select ? why ?

No entiendo muy bien que quieres conseguir, 100-200 request/s en qué llamada ?

En comprar entrada ?
En ver disponibles ?

Depende mucho del negocio/dominio vaya...

Si lo que quieres es "ver disponibles", puedes tenerlo cacheado si es una sala, de forma que actualizas la cache por cada compra (redis o similar), si solo es 1 server, incluso en memoria.

Tu mayor problema aquí, es que quieres procesar los requests en orden de llegada por tiesmtamp y no hay nada que te solucione esto, porque es muy complicado garantizar esto, y que vaya en real time.

Si tienes 1 solo server, sincronizas en memoria, si quieres tener 2 o más servers, tendrías que meter todo en un sitio, e ir ordenandolo, por ejemplo:

A las 10:37 procesas todo lo que falte por procesar hasta las 10:36 (hace un minuto), si algo llega con más de un minuto de retraso respecto a su timestamp, que se jodan. (Pero ahi ya no es un 100% guarantee).

Y tienes X servers que reciben las peticiones y las guarda, y un worker que las procesa (solo puedes tener 1 si quieres orden estricto) (cada minuto ordena lo que halla llegado con un ts < hace un minuto, lo ordena y lo procesa)

Si no permites paralelizar 2 llamadas, no tiene sentido tener más de 1 máquina procesando los comandos, pero puedes tener más de 1 recibiendo peticiones y guardandolas.

Pros:

  • Aguantas los picos de peticiones sin problemas
  • Order guaranteed
    Cons:
  • Throughput limitado a 1 maquina, la cola puede crecer mucho (como las del super)
  • La compra es asíncrona

Usuarios habituales

  • JuAn4k4
  • Lecherito
  • soulsville
  • MTX_Anubis
  • crb2222
  • Wei-Yu
  • eXtreM3