En realidad, no tenía la intención de ver de qué color eran las tripas de Rust. Recogí un proyecto de pasatiempo en Go, fui a GitHub para ver el estado de fasthttp: ¿se está desarrollando? Bueno, al menos compatible? Creció Fui, miré dónde se encuentra fasthttp en los puntos de referencia TechEmpower . Miro: y allí el fasthttp apenas muestra la mitad de lo que el líder tiene éxito, a algunos actix en algunos Rust. Que dolor
Aquí doblaba los brazos, me golpeaba la cabeza contra el suelo (tres veces) y gritaba: "¡Aleluya, en verdad Rust es un dios verdadero, qué ciego estaba antes!". Pero o las asas no funcionaron, o la frente se arrepintió ... En cambio, me metí en el código de las pruebas escritas en Go y en las pruebas de web actix en Rust. Para solucionarlo.
Después de un par de horas descubrí:
- por qué el marco actix-web Rust ocupa el primer lugar en todas las pruebas de TechEmpower,
- Cómo Java inicia Script.
Ahora te diré todo en orden.
¿Qué es el marco de referencia de TechEmpower Framework?
Si un marco web demuestra si va a funcionar o, por ejemplo, a veces piensa en susurrarle a sus amigos "Soy rápido", seguramente entrará en el marco de referencia del marco TechEmpower. Un lugar popular para medir el rendimiento.
El sitio tiene un diseño peculiar: las pestañas de filtros, rondas, condiciones y resultados para diferentes tipos de pruebas están dispersas en la página con una mano generosa. Tan generoso y amplio que simplemente no los notas. Pero vale la pena hacer clic en las pestañas, la información detrás de ellas es útil.
La forma más fácil es obtener los resultados de la prueba de texto sin formato, "¡Hola, mundo!" para servidores web. Los autores del marco generalmente le dan un enlace: supuestamente nos estamos quedando en los primeros cien. El caso es correcto y útil. En general, regalar texto sin formato es bueno para muchos, y los líderes van en un grupo apretado.
Cerca, en esas pestañas, se encuentran los resultados de pruebas de otros tipos (escenarios). Hay siete de ellos, más detalles se pueden encontrar aquí . Estos scripts prueban no solo cómo el marco / plataforma maneja el procesamiento de una solicitud http simple, sino también una combinación con un cliente de base de datos, un motor de plantillas o un serializador JSON.
Hay datos de prueba en un entorno virtual, en un hardware físico. Además de los gráficos, hay datos tabulares. En general, vale la pena investigar muchas cosas interesantes, no solo mirar la posición de "su" plataforma.
Lo primero que me vino a la mente después de revisar los resultados de la prueba: "¿Por qué todo es TAN TAN diferente del texto sin formato?". En texto plano, los líderes van en un grupo ajustado, pero cuando se trata de trabajar con la base de datos, actix-web lidera por un margen significativo. Al mismo tiempo, muestra un tiempo de procesamiento de solicitud estable. Shaitan
Otra anomalía: una solución JavaScript increíblemente poderosa. Se llama ex4x. Resultó que su código estaba ligeramente menos que completamente escrito en Java. Utilizado por Java Runtime, JDBC. El código JavaScript se traduce en bytecode y pega las bibliotecas Java. Literalmente lo tomaron y adjuntaron Script a Java. Los trucos de las caras pálidas no tienen límites.
Cómo mirar el código y lo que hay dentro
El código para todas las pruebas está en GitHub. Todo está en un único repositorio, lo cual es muy conveniente. Puedes clonar y mirar, puedes mirar directamente en GitHub. La prueba involucra más de 300 combinaciones diferentes del marco con serializadores, motores de plantillas y el cliente de la base de datos. En diferentes lenguajes de programación, con un enfoque diferente para el desarrollo. Las implementaciones en un idioma están cerca, se puede comparar con la implementación en otros idiomas. El código es mantenido por la comunidad, no es el trabajo de una persona o equipo.
El código de referencia es un gran lugar para ampliar sus horizontes. Es interesante analizar cómo diferentes personas resuelven los mismos problemas. No hay mucho código, las bibliotecas y las soluciones utilizadas son fáciles de distinguir. No me arrepiento de todo lo que llegué allí. Aprendí mucho Primero que nada sobre Rust.
Antes de Rust, tuve una idea muy vaga. Cualquier artículo sobre C, C ++, D y especialmente Go seguramente tendrá un par de comentaristas que explicarán en detalle y con angustia que la vanidad, el sinsentido y la estupidez están escritos en otra cosa, siempre que haya Gascuña Óxido. A veces se dejan llevar tanto que dan ejemplos de código que una persona no preparada o pocos aceptan llevado a un estupor: "¿Por qué, por qué, por qué todos estos símbolos?"
Por lo tanto, abrir el código daba miedo.
Yo miré Resultó que los programas en Rust se pueden leer. Además, el código se lee tan bien que incluso instalé Rust, intenté compilar la prueba y jugar un poco con ella.
Aquí casi abandoné este negocio, porque la compilación dura mucho tiempo. Mucho tiempo Si yo fuera D'Artagnan, o incluso un colérico, habría corrido a Gascuña, y mil demonios se arrastrarían abatidos. Pero lo hice Bebí té de nuevo. Parece que ni siquiera una taza: en mi computadora portátil, la primera compilación tardó unos 20 minutos. Luego, sin embargo, todo se vuelve más divertido. Tal vez hasta la próxima gran actualización de cajas.
¿Pero no es el óxido mismo?
No No es un lenguaje de programación.
Por supuesto, Rust es un lenguaje maravilloso. Potente, flexible, aunque fuera de hábito y detallado. Pero el lenguaje en sí no escribirá código rápido. El lenguaje es una de las herramientas, una de las decisiones tomadas por el programador.
Como dije, muchos usuarios obtienen rápidamente el texto sin formato. El rendimiento de los marcos actix-web, fasthttp y una docena de otros cuando se procesa una solicitud simple es bastante comparable, es decir, otros lenguajes tienen la capacidad técnica de competir con Rust.
Actix-web en sí, por supuesto, es “culpable”: un producto rápido, pragmático y excelente. La serialización es conveniente, el motor de plantillas es bueno, también ayuda mucho.
En particular, los resultados de las pruebas que trabajan con la base de datos difieren.
Después de profundizar un poco en el código, destaqué tres diferencias principales que (me parece) ayudaron a las pruebas de Actix a separarse de los competidores en las pruebas sintéticas:
- Modo de operación de canalizado tokio-postgres canalizado;
- Usar una sola conexión con una prueba de Rust en lugar de un grupo de conexiones con una prueba escrita en Go;
- Actualización de los puntos de referencia de Actix con un solo comando enviado a través de una consulta simple en lugar de enviar múltiples comandos de ACTUALIZACIÓN.
¿Qué tipo de modo transportador?
Aquí hay un fragmento de la documentación de tokio-postgres (utilizado en el punto de referencia de la biblioteca del cliente PostgreSQL) que explica lo que quieren decir sus desarrolladores:
Sequential Pipelined | Client | PostgreSQL | | Client | PostgreSQL | |----------------|-----------------| |----------------|-----------------| | send query 1 | | | send query 1 | | | | process query 1 | | send query 2 | process query 1 | | receive rows 1 | | | send query 3 | process query 2 | | send query 2 | | | receive rows 1 | process query 3 | | | process query 2 | | receive rows 2 | | | receive rows 2 | | | receive rows 3 | | | send query 3 | | | | process query 3 | | receive rows 3 | |
El cliente en modo canalizado (canalizado) no espera una respuesta de PostgreSQL, sino que envía la siguiente consulta mientras PostgreSQL procesa la anterior. Se puede ver que de esta manera puede procesar la misma secuencia de consultas a la base de datos significativamente más rápido.
Si la conexión en modo canalizado es dúplex (brindando la posibilidad de obtener resultados en paralelo con el envío), este tiempo puede reducirse ligeramente. Parece que ya hay una versión experimental de tokio-postgres donde se abre una conexión dúplex.
Dado que el cliente PostgreSQL envía varios mensajes (Parse, Bind, Execute y Sync) a cada consulta SQL enviada para su ejecución, y recibe una respuesta a ellos, el modo canalizado será más efectivo incluso al procesar consultas individuales.
¿Y por qué no está en Go?
Debido a que Go generalmente usa grupos de conexión de base de datos. Las conexiones no están diseñadas para usarse en paralelo.
Si ejecuta las mismas consultas SQL a través de un grupo, en lugar de una conexión, teóricamente puede obtener un tiempo de ejecución aún más corto con un cliente serie ordinario que cuando trabaja a través de una sola conexión, ya sea tres veces canalizado:
| Connection | Connection 2 | Connection 3 | PostgreSQL | |----------------|----------------|----------------|-----------------| | send query 1 | | | | | | send query 2 | | process query 1 | | receive rows 1 | | send query 3 | process query 2 | | | receive rows 2 | | process query 3 | | | receive rows 3 | |
Parece que la piel de oveja (modo transportador) no vale la pena.
Solo bajo una carga alta, el número de conexiones al servidor PostgreSQL puede ser un problema.
¿Y qué tiene que ver el número de conexiones con él?
El punto aquí es cómo el servidor PostgreSQL responde a un aumento en el número de conexiones.
El grupo izquierdo de columnas muestra el aumento y la caída del rendimiento de PostgreSQL en función del número de conexiones abiertas:

( Adaptado de la publicación de Percona )
Se puede ver que con un aumento en el número de conexiones abiertas, el rendimiento del servidor PostgreSQL está disminuyendo rápidamente.
Además, abrir una conexión directa no es "gratis". Inmediatamente después de abrir, el cliente envía información de servicio, "está de acuerdo" con el servidor PostgreSQL sobre cómo se procesarán las solicitudes.
Por lo tanto, en la práctica, debe limitar el número de conexiones activas con PostgreSQL, a menudo pasándolas adicionalmente a través de pgbouncer u otra odisea.
Entonces, ¿por qué actix-web fue más rápido?
En primer lugar, actix-web en sí es bastante rápido. Es él quien establece el "techo", y es un poco más alto que el de los demás. Otras bibliotecas utilizadas (serde, yarde) también son muy, muy productivas. Pero me parece que en las pruebas de trabajo con PostgreSQL fue posible salir porque el servidor web actix inicia un hilo en el núcleo del procesador. Cada hilo abre solo una conexión a PostgreSQL.
Cuantas menos conexiones activas, más rápido funciona PostgreSQL (consulte los gráficos anteriores).
El cliente que opera en modo canalizado (tokio-postgres) le permite utilizar efectivamente una conexión con PostgreSQL para el procesamiento paralelo de consultas de usuarios. Los manejadores de solicitudes HTTP vuelcan sus comandos SQL en una cola y se alinean en otra para recibir resultados. Los resultados son divertidos, los retrasos son mínimos, todos están contentos. El rendimiento general es más alto que un sistema con un grupo de conexiones.
¿Entonces necesita abandonar el grupo, escribir un cliente de canalización PostgreSQL y la felicidad y la velocidad increíble vendrán de inmediato?
Posiblemente Pero no todo a la vez.
Cuando es poco probable que se guarde el modo de transportador y ciertamente no se guardará
El esquema utilizado en el código de referencia no funcionará con las transacciones de PostgreSQL.
En el punto de referencia, no se necesitan transacciones y el código está escrito teniendo en cuenta que no habrá transacciones. En la práctica, suceden.
Si el código de back-end abre una transacción PostgreSQL (por ejemplo, para realizar un cambio en dos tablas atómicas diferentes), todos los comandos enviados a través de esta conexión se ejecutarán dentro de esta transacción.
Dado que la conexión con PostgreSQL se usa en paralelo, todo se mezcla. Los comandos que deben ejecutarse en una transacción según lo diseñado por el desarrollador se mezclan con los comandos sql iniciados por los controladores de solicitud http paralelos. Recibiremos pérdida de datos aleatoria y problemas con su integridad.
Así que hola transacción: adiós uso paralelo de una conexión. Deberá asegurarse de que la conexión no sea utilizada por otros manejadores de solicitudes http. Deberá detener el procesamiento de las solicitudes HTTP entrantes antes de cerrar la transacción, o utilizar un grupo para las transacciones, abriendo varias conexiones al servidor de la base de datos. Hay varias implementaciones de grupo para Rust, y ninguna. Además, existen en Rust por separado de la implementación del cliente de la base de datos. Puede elegir según el gusto, el color, el olor o al azar. Ir no funciona de esa manera. El poder de los genéricos, sí.
Un punto importante: en la prueba, cuyo código busqué, las transacciones no se abren. Esta pregunta simplemente no vale la pena. El código de referencia está optimizado para una tarea específica y condiciones de operación de aplicaciones muy específicas. La decisión de usar una conexión por flujo de servidor probablemente se hizo conscientemente y resultó ser muy efectiva.
¿Hay algo más interesante en el código de referencia?
Si
El escenario para medir el rendimiento se explica con gran detalle. Además de los criterios que debe cumplir el código que participa en las pruebas. Una de ellas es que todas las consultas al servidor de la base de datos deben ejecutarse secuencialmente.
Parece que el siguiente fragmento de código (ligeramente abreviado) no cumple con los criterios:
let mut worlds = Vec::with_capacity(num);
Todo parece un lanzamiento típico de procesos paralelos. Pero, dado que se usa una conexión a PostgreSQL, las consultas al servidor de la base de datos se envían secuencialmente. Uno por uno. Según sea necesario. Sin crimen
Por qué Bueno, en primer lugar, en el código (se dio en la oficina editorial, que funcionó en la ronda 18) async / wait todavía no se usa, apareció en Rust más tarde. Y a través de futuros num
es más fácil enviar consultas SQL "en paralelo", como en el código anterior. Esto le permite obtener un aumento de rendimiento adicional: mientras PostgreSQL acepta y procesa la primera consulta SQL, el resto se le envía. El servidor web no espera el resultado de cada uno, sino que cambia a otras tareas y vuelve a procesar la solicitud http solo cuando se completan todas las consultas SQL.
Para PostgreSQL, la ventaja es que el mismo tipo de consulta en el mismo contexto (conexión) va en una fila. La probabilidad de que el plan de consulta no se reconstruya aumenta.
Resulta que las ventajas del modo de canalización (ver el diagrama de la documentación de tokio-postgres) se explotan por completo incluso cuando se procesa una única solicitud http.
Que mas
Usando el protocolo de consulta simple para actualizaciones por lotes
El protocolo de comunicación entre el cliente y el servidor PostgreSQL permite métodos alternativos para ejecutar comandos SQL. El protocolo habitual (consulta extendida) implica enviar al cliente varios mensajes: analizar, vincular, ejecutar y sincronizar. Una alternativa es el protocolo Simple Query, según el cual un solo mensaje es suficiente para ejecutar un comando y obtener resultados: Consulta.
La diferencia clave entre el protocolo habitual es la transferencia de parámetros de solicitud: se transmiten por separado del comando en sí. Es más seguro El protocolo simplificado supone que todos los parámetros de la consulta SQL se convertirán en una cadena y se incluirán en el cuerpo de la consulta.
Una solución interesante utilizada en los puntos de referencia de actix-web fue actualizar varias entradas de la tabla con un solo comando enviado a través del protocolo Simple Query.
Según el punto de referencia, al procesar una solicitud de usuario, el servidor web debe actualizar varios registros en la tabla, escribir números aleatorios. Obviamente, la actualización de registros en sucesión con consultas secuenciales lleva más tiempo que una sola consulta que actualiza todos los registros a la vez.
La solicitud generada en el código de prueba se ve así:
UPDATE world SET randomnumber = temp.randomnumber FROM (VALUES (1, 2), (2, 3) ORDER BY 1) AS temp(id, randomnumber) WHERE temp.id = world.id
Donde (1, 2), (2, 3)
son los pares de identificador de línea / nuevo valor del campo de número aleatorio.
El número de registros actualizados es variable, preparar la solicitud (PREPARAR) por adelantado no tiene sentido. Dado que los datos para la actualización son numéricos y se puede confiar en la fuente (el código de prueba en sí), no hay riesgo de inyección de SQL, los datos simplemente se incluyen en el cuerpo de SQL y todo se envía utilizando el protocolo Simple Query.
Se rumorea simple consulta. Cumplí con una recomendación: "Trabaje solo en el protocolo de consulta simple, y todo será rápido y bueno". La percibo con mucho escepticismo. Simple Query le permite reducir la cantidad de mensajes enviados al servidor PostgreSQL moviendo el procesamiento de los parámetros de consulta al lado del cliente. Puede ver la ganancia de las consultas generadas dinámicamente con un número variable de parámetros. Para el mismo tipo de consultas SQL (que son más comunes), la ganancia no es obvia. Bueno, y qué tan seguro resultará el procesamiento de los parámetros de consulta, en el caso de Simple Query determina la implementación de la biblioteca del cliente.
Como escribí anteriormente, en este caso, el cuerpo de la consulta SQL se genera dinámicamente, los datos son numéricos y los genera el propio servidor. La combinación perfecta para Simple Query. Pero incluso en este caso, vale la pena probar otras opciones. Las alternativas dependen de la plataforma y el cliente de PostgreSQL: pgx (el cliente para Go) permite enviar un paquete de comandos, JDBC, para ejecutar un comando varias veces seguidas con diferentes parámetros. Ambas soluciones pueden funcionar a la misma velocidad o incluso ser más rápidas.
Entonces, ¿por qué lidera Rust?
El líder, por supuesto, no es Rust. Las pruebas basadas en actix-web son líderes: es él quien establece el "techo" del rendimiento. Hay, por ejemplo, cohetes y hierro, que ocupan posiciones modestas. Pero por el momento, es actix-web que determina el potencial para usar Rust en el desarrollo web. En cuanto a mí, el potencial es muy alto.
Otro servidor "secreto" no obvio, pero importante basado en actix-web, que permitió ocupar el primer lugar en todos los puntos de referencia de TechEmpower, en cómo funciona con PostgreSQL:
- Solo se abre una conexión con PostgreSQL por flujo de servidor web. Esta conexión utiliza el modo canalizado, lo que le permite ser utilizado efectivamente para el procesamiento paralelo de las solicitudes de los usuarios.
- Cuantas menos conexiones activas, más rápido responde PostgreSQL. La velocidad de procesamiento de las solicitudes de los usuarios aumenta. Al mismo tiempo, bajo carga, todo el sistema funciona más estable (los retrasos en el procesamiento de las solicitudes entrantes son menores, crecen más lentamente).
Donde la velocidad es importante, esta opción probablemente será más rápida que usar multiplexores (como pgbouncer y odyssey). Y ciertamente fue más rápido en los puntos de referencia.
Es muy interesante cómo async / wait, que apareció en Rust, y el drama reciente con actix-web afectará la popularidad de Rust en el desarrollo web. También es interesante cómo cambiarán los resultados de la prueba después de procesarlos en asíncrono / espera.