2015-03-17 8 views
9

Właśnie zacząłem grać z node.js z postgres, używając node-postgres. Jedną z rzeczy, które próbowałem zrobić, było napisanie krótkiego pliku js do zapełnienia mojej bazy danych przy użyciu pliku zawierającego około 200 000 wpisów.node-postgres z ogromną ilością zapytań

Zauważyłem, że po jakimś czasie (mniej niż 10 sekund), zaczynam otrzymywać komunikat "Błąd: połączenie zostało zakończone". Nie jestem pewien, czy jest to problem z korzystaniem z node-postgres, czy też z powodu spamowania postgresów.

W każdym razie, o to prosty kod, który pokazuje to zachowanie:

var pg = require('pg'); 
var connectionString = "postgres://xxxx:[email protected]/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(); 
    }); 
    } 
}); 

To nie po około 18,000-20,000 zapytaniami. Czy to jest zły sposób na użycie client.query? Próbowałem zmienić domyślny numer klienta, ale nie pomogło.

client.connect() również nie pomaga, ale to dlatego, że miałem zbyt wielu klientów, więc zdecydowanie uważam, że łączenie klientów jest drogą do zrobienia.

Dzięki za pomoc!

Odpowiedz

13

UPDATE

Ta odpowiedzi został od wyparte z tego artykułu: Data Imports, który prezentuje najbardziej podejście up-to-date.


W celu powtórzenia scenariusza użyłem pg-promise bibliotekę, i mogę potwierdzić, że próbuje on czołowo nigdy nie będzie działać, bez względu na to, która biblioteka użyć, jest to podejście, które liczy.

Poniżej jest zmodyfikowaną podejście gdzie partycje wkładki na kawałki, a następnie wykonać każdy klocek w ramach transakcji, która jest równoważenie obciążenia (aka dławienia):

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(); 
    }); 

Ten wyprodukowany w około 1,000,000 rekordy 4 minuty, dramatycznie spowalniające po pierwszych 3 transakcjach. Używałem Node JS 0.10.38 (64-bit), który zużył około 340 MB pamięci. W ten sposób wprowadziliśmy 100 000 rekordów 10 razy z rzędu.

Jeśli zrobimy to samo, tylko tym razem wstawimy 10 000 rekordów w ramach 100 transakcji, te same 1,000,000 rekordów zostanie dodanych w zaledwie 1m25s, bez spowolnienia, z Node JS zużywającym około 100 MB pamięci, co mówi nam, że partycjonowanie danych takich jak To bardzo dobry pomysł.

To nie ma znaczenia, który biblioteki użyć podejście powinno być takie same:

  1. partycji/przepustnica twoje wstawki na wiele transakcji;
  2. Lista wkładek powinna znajdować się w pojedynczej transakcji z około 10 000 rekordów;
  3. Wykonaj wszystkie transakcje w łańcuchu synchronicznym.
  4. Zwolnij połączenie z powrotem do puli po COMMIT każdej transakcji.

Jeśli złamiesz którąkolwiek z tych zasad, masz problem. Na przykład, jeśli złamiesz regułę 3, proces Node JS prawdopodobnie szybko wyczerpie pamięć i wygeneruje błąd. Zasada 4 w moim przykładzie została dostarczona przez bibliotekę.

Jeśli podążasz za tym schematem, nie musisz się kłopotać ustawieniami puli połączeń.

UPDATE 1

Późniejsze wersje pg-promise wsparcia takich sytuacjach doskonale, jak pokazano poniżej:

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); 
    }); 

i jeśli nie chcą zawierać żadnych dodatków, takich jak tworzenie tabeli, to wygląda jeszcze prostsze:

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; 
    }); 

Aby uzyskać szczegółowe informacje, patrz: Synchronous Transactions.

Używanie jako biblioteki obietnic, na przykład, zajmuje 1m43 na maszynie produkcyjnej do wstawienia 1 000 000 rekordów (bez włączonych długich wątków stosu).

Po prostu otrzymasz żądanie zwrotu według metody factory zgodnie z index, dopóki nie pozostanie Ci nic prostszego.

A najlepsze jest to, że nie jest to tylko szybki proces, ale także powoduje niewielkie obciążenie procesu NodeJS. Proces testowania pamięci pozostaje poniżej 60 MB podczas całego testu, zużywając tylko 7-8% czasu procesora.

UPDATE 2

Począwszy od wersji 1.7.2, pg-promise obsługuje super-masywne transakcji z łatwością. Zobacz rozdział Synchronous Transactions.

Na przykład mogę wrzucić 10 000 000 rekordów w pojedynczej transakcji w ciągu zaledwie 15 minut na moim komputerze domowym, z systemem Windows 8.1 w wersji 64-bitowej.

Dla testu ustawiłem komputer na tryb produkcji i użyłem Bluebird jako biblioteki obietnicy. Podczas testu zużycie pamięci nie przekroczyło 75 MB dla całego procesu NodeJS 0.12.5 (64-bit), podczas gdy mój procesor i7-4770 wykazywał stałe 15% obciążenia.

Wstawienie 100-metrowych rekordów w ten sam sposób wymagałoby więcej cierpliwości, ale nie więcej zasobów komputera.

W międzyczasie poprzedni test na wkładki 1m spadł z 1m43s do 1m31s.

UPDATE 3

Poniższe rozważania mogą stanowić ogromną różnicę: Performance Boost.

UPDATE 4

Powiązane pytanie, z lepszym przykład realizacji: Massive inserts with pg-promise.

UPDATE 5

lepsze i nowsze przykład można znaleźć tutaj: nodeJS inserting Data into PostgreSQL error

+2

Dzięki. Zgadzam się, że morał tej historii brzmi "nie rób tego w ten sposób". FWIW skończyłem używając postgres COPY FROM. –

+0

Mam rację, zakładając, że 'ten.sekwencja (fabryka), this.one ('wybierz liczbę (*) z testu')' nie da mi prawidłowej liczby, ponieważ nie mogę się upewnić, że sekwencja jest w całości napisana podczas uruchamiania kwerendy liczenia? – stephanlindauer

+0

@stephanlindauer, kiedy sekwencja się rozstrzyga, rozwiązuje się z obiektem '{total, duration}', więc total jest twoją liczbą elementów przetwarzanych przez sekwencję. –

2

Zgaduję, że osiągasz maksymalny rozmiar basenu. Ponieważ client.query jest asynchroniczny, wszystkie dostępne połączenia są używane przed ich zwróceniem.

Domyślny rozmiar Basen jest 10. Sprawdź tutaj: https://github.com/brianc/node-postgres/blob/master/lib/defaults.js#L27

Można zwiększyć domyślny rozmiar puli poprzez ustawienie pg.defaults.poolSize:

pg.defaults.poolSize = 20; 

Aktualizacja: Wykonaj kolejne zapytanie po uwolnieniu połączenia.

var pg = require('pg'); 
var connectionString = "postgres://xxxx:[email protected]/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(); 
    } 
}); 

Podstawową ideą jest to, że w momencie, gdy publikujesz dużą liczbę zapytań o stosunkowo małym rozmiarze puli połączeń, osiągasz maksymalny rozmiar puli. Tutaj tworzymy nową kwerendę dopiero po uwolnieniu istniejącego połączenia.

+0

Dzięki. Zmieniłem go na 25, ale to nie pomogło w moim oryginalnym kodzie. Jednak tego nie próbowałem. Odwołałem się do "domyślnego numeru klienta" ... oops ... czy to było coś innego? –

+0

@DanielSutantyo: jak zmienia się "domyślny numer klienta"? –

+0

Próbowałem więc obu pg.defaults.poolsize = 25; (może nawet więcej) i pg.defaults.poolIdleTimeout = 60000; Pierwsza z nich zdecydowanie zwiększa liczbę wpisów, które mogę umieścić, ale nie za dużo. –