Postgres nie nadąża z zapisem danych

Przy zapisie danych do bazy postgres w bardzo krótkim czasie następuje zakleszczenie i otzrymuje komunikat:

PG::UnableToSend: another command is already in progress

Problem pojawił się w następującej sytuacji:

require 'typhoeus' hydra = Typhoeus::Hydra.hydra request = Typhoeus::Request.new("http://0.0.0.0:9000", params: { ... }) hydra.queue(request) hydra.queue(request) hydra.queue(request) hydra.queue(request) hydra.queue(request) hydra.queue(request) hydra.queue(request) hydra.queue(request) hydra.queue(request) hydra.queue(request) hydra.run
Na porcie 9000 mam uruchomiony serwer Goliath, który wykorzystuje Sequel i asynchroniczny strownik do postgresa (em-postgresql-sequel). W momencie przyjścia requsta do Goliatha, w uproszczeniu, następuje wykonanie 2 selectów i 1 insert do bazy. Wiem, że w Typhoeus requesty przychodzą bardzo szybko, ale zależy mi na obsłudze dużej liczby requestów jednocześnie. Zastanawiam się czy ten problem można jakoś rozwiązać (funkcja sleep jest tutaj bardzo złym pomysłem), ale nic jakoś nie przychodzi mi do głowy. Przełączenie bazy na Mongo pewnie by pomogło, bo tam nie ma transakcji, ale nie wiem czy to w tym jest problem.

Nie wiem czy to kwestia używania event machine (naprawdę Ci to potrzebne?) czy jakiegoś błędu w aplikacji, ale mi to wygląda na złe zarządzanie połączeniem do pg - tak jakbyś np. rozpoczął jakieś zapytanie, które pobiera dane z STDIN (COPY?) i w tym samym czasie na tym samym połączeniu chciał wykonać inne zapytanie.

Na moje oko wygląda to na problem używania tego samego połączenia przez wiele wątków (na poziomie goliatha). Jest wysyłane kolejne zapytanie SQL, chociaż poprzednie nie zostało obsłużone.

Tutaj coś gość pisze, że miał podobny problem: https://github.com/sonicbee9/em-postgresql-adapter/pull/8

Niestety nie pomogę Ci bardziej bo nie używałem tego zestawu.

@drogus: mój cel jest taki, żeby w ciągu np. 1-2 sekund obsłużyć jak na razie 200 requestów. Wybrałem goliatha bo jest on szybki w obsłużeniu operacji IO, te dane które dostanie ma zapisać do bazy, ale widaćtu jest problem.

Może dokładniej opiszę mój problem:
Mam np. 20 źródeł na których dane zmieniają się co kilka-kilkanaście sekund. Chcę te dane pobierać i składować w bazie, żeby móc je później odpowiedni obrobić i wyświetlać statystyki. (Kwestia parsowania jest już rozwiązania i zajmuje ona pomijalnie mało czasu). Liczba źródeł, może ulec zmianie co ok. 1 godzine.

Opisany problem chciałem rozwiązać następująco:
Co godzinę uruchamiany byłby skrypt, który pobierałby listę źródeł następnie dla każdego źródła tworzyłby osobny proces (forkiem), który co kilka-kilkanaście skund, pobierałby wyniki z danego żródła i zapisywał w bazie, z racji tego, że zapis w bazie może być kosztowny, dlatego użyłem typhoeus, żeby zebrał te kilka requestów w calość i wysłał je równolegle. Requesty odebrałby goliath, który zapisałby je w bazie.

Jeśli jest jakieś lepsze rozwiązanie tego problemu, to jestem otwarty na Wasze sugestie.

Pokaż może ten kod, który wkłada dane, to łatwiej będzie coś o tym powiedzieć.

Strukturalnie kond wyglądałby tak:

[code=ruby]response = HTTParty.get("…")
output = parse(response.body)
output.each do |line|
pid = fork do
begin
response = HTTParty.get(source_url)
output = parse(response.body)

  hydra = Typhoeus::Hydra.hydra
  output.each do |element|
    request = Typhoeus::Request.new("http://0.0.0.0:9000", params: { .. })
    hydra.queue request
  end

  hydra.run
  sleep 10
end while condition

end

remember pid

end[/code]
kod jest uproszczony, nie zawiera np. zapamietania pida żeby dla tego samego źródła nie tworzyć osobnego procesu w trakcie ponownego wywołania tego kodu przez CRON

Pokaż też kod który używa postgresa.
Możesz też podać szerszy obraz sytuacji? Schemat komunikacji - twoje apki, zewnętrzne serwisy? I mam takie pytania:

Czy na Goliathcie stoi prosta appka która wysyła tylko requesty do bazy?
Czy program wykorzystujący Typhoeus jest jakimś demonem? Czemu nie używasz tam wątków tylko procesów?

Tak jak Sławosz napisał, bardziej chodziło mi właśnie o postgresa, samo wysyłanie requestów nie ma tu większego znaczenia imho - przy dobrze zrobionej appce nie powinno być takich problemów z postgresem, nawet jak ją zasypiesz requestami, to raczej serwer powinien nie wyrobić niż rzucać takie błędy.

Chyba opisałem w moim drugim poście schemat komunikacji, w skrócie co kilka, kilkanaście sekund dane się zmieniają i trzeba pobrać nowe. Mam dostep do czegos w rodzaju API, ale to nie jest ani API jsonowe, xmlowe ani inne popularne znane. Jest to plik tekstowy, który parsuje i w tym momencei wysyłam każdy wpis/rekord w osobnym requescie do Goliatha.

Tak, kod zamieszczony poniżej

Typhoeus to gem do hurtowej wysyłki wielu requestów, patrz: https://github.com/typhoeus/typhoeus

Procesy łatwiej (szybciej) zakodowałem.

Jak wcześniej miałem na serwerze Goliath parsowanie wyników to problemów takich nie było, pojawiły się dopiero jak okroiłem go do roli odbierania danych i zapisywania ich do bazy

[code=ruby]require ‘goliath’

class Application < Goliath::API
use Goliath::Rack::Params

def response(env)
puts params.inspect

save_data unless find_data

[200, {}, '']

end

def find_source
db[:sources].where(name: params[‘name’]).select(‘id’).map(:id).first
end

def create_source
db[:sources].insert(name: params[‘name’], …)
end

def source_id
unless @source_id
@source_id = find_source
@source_id ||= create_source
end
@source_id
end

def find_data
db[:datas].where(source_id: source_id, …).select(‘id’).map(:id).first
end

def create_data
db[:datas].insert(source_id: source_id, …)
end
end[/code]
i config

[code=ruby]require ‘yaml’
require ‘pg’
require ‘sequel’
require ‘em-postgresql-sequel’

conf = YAML.load_file(‘config/database.yml’)[Goliath.env.to_s]
config[‘db’] = Sequel.connect “#{conf[‘adapter’]}://#{conf[‘username’]}:#{conf[‘password’]}@#{conf[‘host’]}/#{conf[‘database’]}”[/code]
Kod chyba aż tak skomplikowany nie jest.

Nie mam teraz tego jak za bardzo sprawdzić, ale czy nie robisz jakiegoś głupiego błędu typu używania jednego połączenia? Albo jesteś pewien, że nie blokujesz gdzieś reaktora?

Co do procesów w demonie - myślę że możesz tutaj wykorzystać bardzo fajnie Celluloid - mniejsze zużycie pamięci, choć wprowadzenie wątków, jeśli kod jest rzeczywiście taki prosty, też będzie banalne.

PS. Masz świetną okazję do wypróbowania Go :wink:

Patrząc na README z pewnością brakuje Ci parametru :pool_class => EM::Sequel::FiberedConnectionPool przy tworzeniu połączenia.

Rzadko używałem eventmachine, więc mogę pisać jakieś głupoty, ale ja bym zaczął od ustawienia jakiejś wysokiej wartości dla :max_connections w sequelu, np:

Sequel.connect "#{conf['adapter']}://#{conf['username']}:#{conf['password']}@#{conf['host']}/#{conf['database']}", :max_connections => 100
Nie wiem czy to cokolwiek da, bo nie jestem pewien jaka jest implementacja connection pool w sequelu - jeżeli podobnie jak w AR, to w przypadku eventmachine to nie zadziała, bo implementacja jest zrobiona dla wątków.

Jeżeli właśnie tak by było, to spróbował bym użyć connection pool przygotowanego dla event machine, tak jak jest tutaj: https://github.com/postrank-labs/goliath/wiki/Configuration

config['db'] = EM::Synchrony::ConnectionPool.new(:size => 20) do Sequel.connect "#{conf['adapter']}://#{conf['username']}:#{conf['password']}@#{conf['host']}/#{conf['database']}" end
Możesz też przy tym drugim sposobie spróbować dać większy rozmiar.

Jakby Ci się udało to uruchomić w ten sposób, to ja byłbym bardzo zainteresowany porównaniem tego z rozwiązaniem opartym na wątkach. Jakbyś miał chwilę czasu, to mogę pomóc to przygotować - można stworzyć jakąś testową tabelę i odpalić skrypt na obu rozwiązaniach. Ciekawi mnie czy rzeczywiście EM miałby jakąś zauważalną przewagę.

UPDATE:

Wydaje mi się, że rozwiązanie radarka będzie najlepsze - nie zauważyłem tego fibered connection pool, a prawdopodobnie o to właśnie chodzi. EM::Synchrony::ConnectionPool też pewnie rozwiązałoby problem, ale lepiej używać czegoś co jest wbudowane w sequela niż zewnętrznego.

W sumie robie to na własne potrzeby, nie dla klienta zewnętrznego, więc mógłbym zrobić też wersje na wątkach dla porównania wydajności, tylko pytanie jak dokonać takiego porównania wydajności.

JMeter i sprawdzasz ilość zużytej pamięci + czas. Myślę, że tak od 200 połączeń jednocześnie EM może być szybsze :wink: Czy jest to tajny projekt, czy mógłbym się dołączyć z implementacją w Go?

@slawosz: jak zobaczyłem nawiasy klamrowe przy funkcjach w Go to mnie odrzuciło. Między innymi ze względu na brak nawiasów, które wg mnie są zbędne i upierdliwe przełączyłem się z czystego JavaScript na CoffeScript.

Napisz Prywatną Wiadomość, jeśli chciałbyś dokładniejszy opis i cel tego projektu, ale nie wiem czy byłbyś tym zainteresowany, gdybyś poznał jego cel.

To nie musi być to co Ty robisz dokładnie, wystarczy porównywalny scenariusz - jakaś tabela “things”, jakiś select, jakiś insert, test na wątkach i na EM. Nie wiem czy będę miał czas się tym zająć, ale pomyślę.

Spróbuj zrobić tak jak napisał radarek, to znaczy oprócz max_connections dodaj jeszcze pool_class. Możesz też spróbować użyć connection pool z em synchrony, tak jak pokazałem w swoim poście. Chociaż deadlock wskazuje na trochę inny problem, więc nie wiem czy to coś zmieni.

Ja bym zrobił jakiś prosty własny kod - nie musi być bezpośrednio oparty o to co wafcio, interesuje mnie podobny use case.

działa, tylko czasy mnie trochę martwią:

[3909:INFO] 2013-09-23 17:28:50 :: Status: 200, Content-Length: 0, Response Time: 147.40ms [3909:INFO] 2013-09-23 17:28:50 :: Status: 200, Content-Length: 0, Response Time: 148.27ms [3909:INFO] 2013-09-23 17:28:50 :: Status: 200, Content-Length: 0, Response Time: 200.26ms [3909:INFO] 2013-09-23 17:28:50 :: Status: 200, Content-Length: 0, Response Time: 152.16ms [3909:INFO] 2013-09-23 17:28:50 :: Status: 200, Content-Length: 0, Response Time: 143.27ms [3909:INFO] 2013-09-23 17:28:50 :: Status: 200, Content-Length: 0, Response Time: 158.60ms [3909:INFO] 2013-09-23 17:28:50 :: Status: 200, Content-Length: 0, Response Time: 149.95ms [3909:INFO] 2013-09-23 17:28:50 :: Status: 200, Content-Length: 0, Response Time: 93.38ms [3909:INFO] 2013-09-23 17:28:50 :: Status: 200, Content-Length: 0, Response Time: 98.96ms [3909:INFO] 2013-09-23 17:28:50 :: Status: 200, Content-Length: 0, Response Time: 90.42ms
przy większej liczbie requestów czas spada

[3909:INFO] 2013-09-23 17:30:39 :: Status: 200, Content-Length: 0, Response Time: 123.76ms [3909:INFO] 2013-09-23 17:30:39 :: Status: 200, Content-Length: 0, Response Time: 123.20ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 116.86ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 110.27ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 102.37ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 107.66ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 90.53ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 100.45ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 82.62ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 97.68ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 89.80ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 75.64ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 81.68ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 73.02ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 59.94ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 65.54ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 71.35ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 53.53ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 59.33ms [3909:INFO] 2013-09-23 17:30:40 :: Status: 200, Content-Length: 0, Response Time: 50.97ms
Trochę to dziwne, zwłaszcza że w testowych requestach jest te same params czyli wykonuje dokladnie te same informacje.

UPDATE:
dziwne wyniki są naprawdę różne w kolejnych wywołaniach, pewnie to zależy od obciążenia systemu w danej chwili.

Próbowałeś odpalić https://github.com/levicook/goliath-postgres-spike i polookac co się dzieje ?

Masz indeksy założone na te pola, których używasz w selectach?