Grandes apetitos para pequeños buffers en Node.js

Ya hablé sobre el servicio para monitorear consultas a PostgreSQL , para lo cual implementamos un recopilador de registros del servidor en línea, cuya tarea principal es recibir simultáneamente flujos de registros de una gran cantidad de hosts a la vez, analizarlos rápidamente en líneas , agruparlos en paquetes de acuerdo con ciertas reglas, procesar y escribir dar como resultado el almacenamiento de PostgreSQL .



En nuestro caso, estamos hablando de varios cientos de servidores y millones de solicitudes y planes que generan más de 100 GB de registros por día . Por lo tanto, no fue nada sorprendente cuando descubrimos que la mayor parte de los recursos se gasta precisamente en estas dos operaciones: analizar en líneas y escribir en la base de datos.

Nos sumergimos en las entrañas del generador de perfiles y encontramos algunas características de trabajar con BufferNode.js, cuyo conocimiento puede ahorrarle mucho tiempo y recursos de servidor.

Carga de la CPU




La mayor parte del tiempo del procesador se dedicó a procesar la secuencia de registro entrante, lo cual es comprensible. Pero lo que no estaba claro era el recurso intensidad de uso de la primitiva “rebanar” de la corriente entrante de bloques binarios en líneas por \r\n: El



desarrollador atento será inmediatamente notar aquí una no tan eficiente byte de ciclo a través de la memoria intermedia de entrada. Bueno, dado que la línea puede ser "desgarrada" entre bloques vecinos, también queda un "accesorio de cola" funcional del bloque procesado anterior.

Intentando readline


Una revisión rápida de las soluciones disponibles nos trajo al módulo de línea de lectura regular exactamente con la funcionalidad necesaria para cortar en líneas:



después de que se implementó, la "división" desde la parte superior del generador de perfiles se hizo más profunda:



Pero, como resultó, readline conduce a la fuerza la cadena a UTF-8 , lo cual es imposible hacer si la entrada de registro (solicitud, plan, texto de error) tiene una codificación de origen diferente.

De hecho, incluso en un servidor PostgreSQL, varias bases de datos pueden estar activas simultáneamente, cada una de las cuales genera salida a un registro de servidor común exactamente en su codificación original. Como resultado, los propietarios de bases en win-1251 (a veces es conveniente usarlo para ahorrar espacio en disco si no se necesita un UNICODE multibyte "honesto") pudieron observar sus planes con aproximadamente tales nombres "rusos" de tablas e índices:



Modificando la bicicleta


Es un problema ... Todavía es necesario hacer el corte usted mismo, pero con optimizaciones del tipo en Buffer.indexOf()lugar de "byte-scan":



Parece que todo está bien, la carga en el circuito de prueba no aumentó, win1251-nombres fueron reparados, salimos a la batalla ... ¡Ta-dam! El uso de la CPU rompe periódicamente el techo al 100% :



¿Cómo es? ... Resulta que es nuestra culpa Buffer.concatcon la que "pegamos la cola" sobrante del bloque anterior:



Pero solo tenemos un pegado cuando pasamos una línea a través del bloque , pero no deberían ser muchos - realmente, realmente? .. Bueno, casi. Solo que ahora a veces vienen "cadenas" de varios cientos de segmentos de 16 KB :



Gracias a otros desarrolladores que se encargaron de generar esto. Ocurre "raramente, pero con precisión", por lo que no fue posible ver de antemano en el circuito de prueba.

Está claro que pegar varios cientos de veces al búfer en varios megabytes de piezas pequeñas es un camino directo al abismo de reasignaciones de memoria con el consumo de recursos de la CPU, lo que observamos. Entonces, no lo peguemos hasta que la línea termine por completo. Simplemente colocaremos las "piezas" en una matriz hasta que sea el momento de dar "salida" a toda la línea:



ahora la carga ha vuelto a los indicadores de línea de lectura.

Consumo de memoria


Muchas personas que escribieron en idiomas con asignación dinámica de memoria son conscientes de que uno de los "asesinos de rendimiento" más desagradables es la actividad en segundo plano del recolector de basura (GC), que escanea los objetos creados en la memoria y elimina los que son más grandes No se requiere nadie. Este problema también nos sobrecogió: en algún momento comenzamos a notar que la actividad de GC era demasiado y fuera de lugar.



Los "giros" tradicionales realmente no ayudaron ... "¡Si todo lo demás falla, volcado!" Y la sabiduría popular no decepcionó: vimos una nube de Buffer de 8360 bytes con un tamaño total de 520 MB ...



Y se generaron dentro de CopyBinaryStream, la situación comenzó a aclararse ...

COPIAR ... DE STDIN CON BINARIO


Para reducir la cantidad de tráfico transmitido a la base de datos, utilizamos el formato binario COPY . De hecho, para cada registro, debe enviar un búfer a la secuencia, que consta de "piezas": el número de campos en el registro (2 bytes) y luego la representación binaria de los valores de cada columna (4 bytes por tipo ID + datos).

Dado que dicha fila de la tabla casi siempre tiene una longitud variable "resumida", la asignación inmediata de un búfer de una longitud fija no es una opción ; la reasignación si hay una falta de tamaño "devorará" fácilmente el rendimiento; ya ha aumentado. Por lo tanto, también vale la pena "pegar con piezas" usando Buffer.concat().

memorándum


Bueno, dado que tenemos muchas piezas repetidas una y otra vez (por ejemplo, la cantidad de campos en los registros de la misma tabla), recordemos simplemente y luego tomemos las listas , generadas una vez en la primera llamada. Según el formato de COPIA, hay pocas opciones: las piezas típicas tienen una longitud de 1, 2 o 4 bytes:



Y ... ¡bam, ha llegado un rastrillo!



Es decir, sí, cada vez que crea un búfer, se asigna un trozo de memoria de 8 KB de forma predeterminada, de modo que los pequeños búferes que se crean en una fila se pueden apilar "uno al lado del otro" en la memoria ya asignada. Y nuestra asignación funcionó "a pedido", y resultó que no estaba en absoluto "cerca", es por eso que cada uno de nuestros búfer de 1-2-4 bytes ocupaba físicamente un encabezado de 8 KB + , ¡aquí están, nuestros 520 MB!

memo inteligente


Hmm ... ¿Por qué tenemos que esperar hasta que se necesite este o aquel búfer de 1/2 byte? Con 4 bytes es un problema por separado, pero algunas de estas diferentes opciones para un total de 256 + 65536. ¡ Deje que nagenerim su fila de una vez ! Al mismo tiempo, reducimos la condición para la existencia de cada verificación; también funcionará más rápido, ya que la inicialización se lleva a cabo solo al comienzo del proceso.



Es decir, además de las memorias intermedias de 1/2 byte, inmediatamente inicializamos la mayoría de los valores en ejecución (2 bytes inferiores y -1) para los de 4 bytes. Y, ¡ayudó, solo 10 MB en lugar de 520 MB!


All Articles