nodo-postgres con gran cantidad de consultas

Acabo de empezar a jugar con node.js con postgres, usando node-postgres. Una de las cosas que intenté hacer es escribir un js corto para poblar mi base de datos, usando un archivo con aproximadamente 200,000 entradas.

Noté que después de algún tiempo (menos de 10 segundos), comienzo a obtener “Error: Conexión terminada”. No estoy seguro de si esto es un problema con la forma en que uso node-postgres, o si es porque fui postgres spamming.

De todos modos, aquí hay un código simple que muestra este comportamiento:

var pg = require('pg'); var connectionString = "postgres://xxxx:xxxx@localhost/xxxx"; pg.connect(connectionString, function(err,client,done){ if(err) { return console.error('could not connect to postgres', err); } client.query("DROP TABLE IF EXISTS testDB"); client.query("CREATE TABLE IF NOT EXISTS testDB (id int, first int, second int)"); done(); for (i = 0; i < 1000000; i++){ client.query("INSERT INTO testDB VALUES (" + i.toString() + "," + (1000000-i).toString() + "," + (-i).toString() + ")", function(err,result){ if (err) { return console.error('Error inserting query', err); } done(); }); } }); 

Falla después de unas 18,000-20,000 consultas. ¿Es esta la manera incorrecta de usar client.query? Intenté cambiar el número de cliente predeterminado, pero no pareció ayudar.

client.connect () tampoco parece ayudar, pero fue porque tenía demasiados clientes, por lo que definitivamente creo que la agrupación de clientes es el camino a seguir.

¡Gracias por cualquier ayuda!

ACTUALIZAR

Esta respuesta se ha sustituido desde entonces con este artículo: Importaciones de datos , que representa el enfoque más actualizado.


Para replicar su escenario, utilicé la biblioteca pg-promise , y puedo confirmar que intentarlo de frente nunca funcionará, sin importar qué biblioteca use, lo que importa es el enfoque.

A continuación se muestra un enfoque modificado donde particionamos inserciones en fragmentos y luego ejecutamos cada fragmento dentro de una transacción, que es el equilibrio de carga (también conocido como regulación).

 function insertRecords(N) { return db.tx(function (ctx) { var queries = []; for (var i = 1; i <= N; i++) { queries.push(ctx.none('insert into test(name) values($1)', 'name-' + i)); } return promise.all(queries); }); } function insertAll(idx) { if (!idx) { idx = 0; } return insertRecords(100000) .then(function () { if (idx >= 9) { return promise.resolve('SUCCESS'); } else { return insertAll(++idx); } }, function (reason) { return promise.reject(reason); }); } insertAll() .then(function (data) { console.log(data); }, function (reason) { console.log(reason); }) .done(function () { pgp.end(); }); 

Esto produjo 1000,000 registros en aproximadamente 4 minutos, disminuyendo dramáticamente después de las 3 primeras transacciones. Estaba usando el nodo JS 0.10.38 (64 bits), que consumía aproximadamente 340 MB de memoria. De esta manera insertamos 100,000 registros, 10 veces seguidas.

Si hacemos lo mismo, solo que esta vez insertamos 10,000 registros dentro de 100 transacciones, los mismos 1,000,000 de registros se agregan en solo 1m25s, sin ralentización, con el Nodo JS consumiendo alrededor de 100MB de memoria, lo que nos dice que particionar datos como este es muy buena idea.

No importa qué biblioteca utilice, el enfoque debe ser el mismo:

  1. Partición / aceleración sus inserciones en múltiples transacciones;
  2. Mantenga la lista de inserciones en una sola transacción en alrededor de 10,000 registros;
  3. Ejecuta todas tus transacciones en una cadena sincrónica.
  4. Libere la conexión de nuevo al grupo después de COMPROMETAR cada transacción

Si rompes alguna de esas reglas, tienes problemas garantizados. Por ejemplo, si infringe la regla 3, es probable que el proceso de Node JS se quede sin memoria rápidamente y se produzca un error. La regla 4 en mi ejemplo fue proporcionada por la biblioteca.

Y si sigue este patrón, no necesita preocuparse por la configuración del grupo de conexiones.

ACTUALIZACIÓN 1

Las versiones posteriores de pg-promise soportan tales escenarios perfectamente, como se muestra a continuación:

 function factory(index) { if (index < 1000000) { return this.query('insert into test(name) values($1)', 'name-' + index); } } db.tx(function () { return this.batch([ this.none('drop table if exists test'), this.none('create table test(id serial, name text)'), this.sequence(factory), // key method this.one('select count(*) from test') ]); }) .then(function (data) { console.log("COUNT:", data[3].count); }) .catch(function (error) { console.log("ERROR:", error); }); 

y si no desea incluir nada adicional, como la creación de tablas, entonces se verá aún más simple:

 function factory(index) { if (index < 1000000) { return this.query('insert into test(name) values($1)', 'name-' + index); } } db.tx(function () { return this.sequence(factory); }) .then(function (data) { // success; }) .catch(function (error) { // error; }); 

Vea Transacciones síncronas para más detalles.

Al usar Bluebird como la biblioteca de promesas, por ejemplo, se necesitan 1m43 en mi máquina de producción para insertar 1,000,000 de registros (sin tener habilitados rastreos de stack largos).

Solo tendría las solicitudes de devolución del método de factory acuerdo con el index , hasta que no quede ninguna, así de simple.

Y la mejor parte, esto no solo es rápido, sino que también crea poca carga en su proceso NodeJS. El proceso de prueba de memoria permanece por debajo de 60 MB durante toda la prueba, y consume solo el 7-8% del tiempo de CPU.

ACTUALIZACIÓN 2

A partir de la versión 1.7.2, pg-promise admite transacciones súper masivas con facilidad. Ver capítulo Transacciones sincrónicas .

Por ejemplo, podría insertar 10,000,000 registros en una sola transacción en solo 15 minutos en la PC de mi hogar, con Windows 8.1 de 64 bits.

Para la prueba, puse mi PC en modo de producción y usé Bluebird como la biblioteca de promesa. Durante la prueba, el consumo de memoria no superó los 75 MB para todo el proceso NodeJS 0.12.5 (64 bits), mientras que mi CPU i7-4770 mostró una carga constante del 15%.

Insertar registros de 100 m de la misma manera requeriría solo más paciencia, pero no más recursos de computadora.

Mientras tanto, la prueba anterior para inserciones de 1m cayó de 1m43s a 1m31s.

ACTUALIZACIÓN 3

Las siguientes consideraciones pueden hacer una gran diferencia: Mejora del rendimiento .

ACTUALIZACIÓN 4

Pregunta relacionada, con un mejor ejemplo de implementación: inserciones masivas con pg-promise .

Actualización 5

Un ejemplo mejor y más nuevo se puede encontrar aquí: nodeJS insertando datos en el error PostgreSQL

Supongo que estás alcanzando el tamaño máximo de la piscina. Dado que client.query es asíncrono, muchas de las conexiones disponibles se utilizan antes de ser devueltas.

El tamaño predeterminado de la agrupación es 10. Consulte aquí: https://github.com/brianc/node-postgres/blob/master/lib/defaults.js#L27

Puede boost el tamaño predeterminado de la agrupación configurando pg.defaults.poolSize :

 pg.defaults.poolSize = 20; 

Actualizar: Ejecutar otra consulta después de liberar una conexión.

 var pg = require('pg'); var connectionString = "postgres://xxxx:xxxx@localhost/xxxx"; var MAX_POOL_SIZE = 25; pg.defaults.poolSize = MAX_POOL_SIZE; pg.connect(connectionString, function(err,client,done){ if(err) { return console.error('could not connect to postgres', err); } var release = function() { done(); i++; if(i < 1000000) insertQ(); }; var insertQ = function() { client.query("INSERT INTO testDB VALUES (" + i.toString() + "," + (1000000-i).toString() + "," + (-i).toString() + ")", function(err,result){ if (err) { return console.error('Error inserting query', err); } release(); }); }; client.query("DROP TABLE IF EXISTS testDB"); client.query("CREATE TABLE IF NOT EXISTS testDB (id int, first int, second int)"); done(); for (i = 0; i < MAX_POOL_SIZE; i++){ insertQ(); } }); 

La idea básica es que, dado que está realizando una gran cantidad de consultas con un tamaño de agrupación de conexiones relativamente pequeño, está alcanzando el tamaño máximo de agrupación. Aquí hacemos una nueva consulta solo después de que se haya liberado una conexión existente.