2013-01-24 5 views
7

Obecnie pracuję na serwerze sieciowym w tornado, ale mam problemy z różnymi bitami kodu próbującymi uzyskać dostęp do bazy danych na raz.Efektywność ponownego otwierania bazy danych SQLite po każdym zapytaniu

mam uproszczona to po prostu posiadające funkcję zapytań, które w zasadzie robi to (ale nieco bardziej zaawansowany):

def query(command, arguments = []): 
    db = sqlite3.open("models/data.db") 
    cursor = db.cursor() 
    cursor.execute(command, arguments) 
    result = cursor.findall() 
    db.close() 
    return result 

Zastanawiam się, jak skuteczne jest to, aby ponownie otworzyć bazy danych po każdym zapytaniu (I zgaduje, że jest to bardzo duża stała operacja czasowa, czy może cache coś lub coś?), i czy jest lepszy sposób to zrobić.

+1

Proszę zaksięgować rzeczywisty kod, z właściwymi funkcjami, takimi jak 'connect' i' fetchall' zamiast 'open' i' findall'. – abarnert

+0

Jakie są "problemy z różnymi bitami kodu"? Jeśli próbujesz wielu jednoczesnych zapisów, użyj prawdziwego RDBMS http://www.sqlite.org/faq.html#q5 – msw

Odpowiedz

12

Dodaję własną odpowiedź, ponieważ nie zgadzam się z aktualnie przyjętą. Stwierdza, że ​​operacja nie jest bezpieczna dla wątków, ale jest to niewłaściwe - SQLite uses file locking odpowiednie dla jej obecnej platformy, aby zapewnić, że wszystkie dostępy są zgodne z ACID.

W systemach Unix będzie to blokada fcntl() lub flock(), która jest blokadą na jeden plik. W wyniku tego, za każdym razem, kod, który tworzy nowe połączenie, zawsze przydziela nowy uchwyt pliku, a zatem samo blokowanie SQLite zapobiegnie uszkodzeniu bazy danych. Konsekwencją tego jest to, że zazwyczaj nie jest dobrym pomysłem korzystanie z SQLite na udziale NFS lub podobnym, ponieważ często nie zapewniają one szczególnie niezawodnego blokowania (zależy to jednak od implementacji NFS).

Jak już zauważył @abernert w komentarzach, SQLite has had issues with threads, ale było to związane z dzieleniem pojedynczego połączenia między wątkami. Jak również wspomina, oznacza to, że jeśli używasz całej puli aplikacji, otrzymasz błędy środowiska wykonawczego, jeśli drugi wątek wyciągnie z puli przetworzone połączenie. Są to również irytujące błędy, których możesz nie zauważyć podczas testowania (lekkie ładowanie, może tylko jeden wątek w użyciu), ale które mogą łatwo powodować bóle głowy później. Późniejsza sugestia Martijna Pietersa dotycząca puli wątków lokalnych powinna działać dobrze.

Jak podkreślono w SQLite FAQ od wersji 3.3.1 to rzeczywiście bezpieczne przekazać połączenia pomiędzy wątkami, o ile nie posiadają zamki - było to ustępstwo, że autor SQLite dodany pomimo krytyczny ogólne wykorzystanie wątków.Każda sensowna implementacja łączenia puli zawsze zapewni, że wszystko zostało zatwierdzone lub wycofane przed zamianą połączenia w puli, więc faktycznie globalna pula aplikacji będzie prawdopodobnie prawdopodobnie bezpieczna, jeśli nie będzie to sprawdzanie Pythona przed udostępnieniem , który, jak sądzę, pozostaje na miejscu, nawet jeśli używana jest nowsza wersja SQLite. Z pewnością mój system Python 2.7.3 ma moduł sqlite3 z sqlite_version_info raportowaniem 3.7.9, ale nadal wysyła RuntimeError, jeśli uzyskujesz dostęp do niego z wielu wątków.

W każdym razie, dopóki istnieje kontrola, połączenia nie mogą być skutecznie udostępniane, nawet jeśli biblioteka bazowa obsługuje je.

Co do oryginalnego pytania, z pewnością tworzenie nowego połączenia za każdym razem jest mniej efektywne niż utrzymywanie puli połączeń, ale już zostało wspomniane, że musi to być lokalny wątek, co jest niewielkim problemem . Narzut tworzenia nowego połączenia z bazą danych zasadniczo otwiera plik i czyta nagłówek, aby upewnić się, że jest to poprawny plik SQLite. Narzut faktycznego wykonywania instrukcji jest wyższy, ponieważ wymaga wyodrębnienia wyglądu i wykonania dość wielu operacji wejścia/wyjścia pliku, więc większość pracy jest faktycznie odroczona do wykonania instrukcji i/lub zatwierdzenia.

Co ciekawe, przynajmniej w systemach Linux przyjrzałem się kodowi do wykonania instrukcji powtarza kroki czytania nagłówka pliku - w rezultacie otwarcie nowego połączenia nie będzie wcale takie złe ponieważ początkowy odczyt podczas otwierania połączenia spowoduje wyciągnięcie nagłówka do pamięci podręcznej systemu plików systemu. Sprowadza się to do otwarcia jednego uchwytu pliku.

Należy również dodać, że jeśli spodziewasz się skalowania kodu do wysokiej współbieżności, to SQLite może być złym wyborem. Jako their own website points out nie jest ona odpowiednia dla wysokiej współbieżności, ponieważ uderzenie wydajnościowe polegające na konieczności wyciśnięcia całego dostępu przez pojedynczą globalną blokadę zaczyna bić, gdy rośnie liczba współbieżnych wątków. W porządku, jeśli używasz wątków dla wygody, ale jeśli naprawdę oczekujesz wysokiego stopnia współbieżności, to uniknęłbym SQLite.

Podsumowując, nie uważam, że twoje podejście do otwierania za każdym razem jest tak naprawdę złe. Czy pula lokalna wątku może poprawić wydajność? Prawdopodobnie tak. Czy ten wzrost wydajności byłby zauważalny? Moim zdaniem, chyba że widzisz dość wysokie współczynniki połączeń, i na tym etapie będziesz mieć wiele wątków, więc prawdopodobnie i tak będziesz chciał odejść od SQLite, ponieważ nie radzi sobie z tym strasznie dobrze. Jeśli zdecydujesz się użyć jednego z nich, upewnij się, że czyści ono połączenie przed zwróceniem go do puli - SQLAlchemy ma pewną funkcjonalność connection pooling, która może Ci się przydać, nawet jeśli nie chcesz, aby wszystkie warstwy ORM były na wierzchu.

EDIT

Jako całkiem rozsądnie wskazał, należy załączyć rzeczywiste czasy. Te są z dość niskiego zasilania VPS:

>>> timeit.timeit("cur = conn.cursor(); cur.execute('UPDATE foo SET name=\"x\" 
    WHERE id=3'); conn.commit()", setup="import sqlite3; 
    conn = sqlite3.connect('./testdb')", number=100000) 
5.733098030090332 
>>> timeit.timeit("conn = sqlite3.connect('./testdb'); cur = conn.cursor(); 
    cur.execute('UPDATE foo SET name=\"x\" WHERE id=3'); conn.commit()", 
    setup="import sqlite3", number=100000) 
16.518677949905396 

Możesz zobaczyć współczynnik około 3x różnicy, która nie jest bez znaczenia. Jednak czas bezwzględny to nadal mniej niż jedna milisekunda, więc jeśli nie wykonasz wielu zapytań na żądanie, prawdopodobnie najpierw inne miejsca do optymalizacji. Jeśli wykonujesz wiele zapytań, rozsądnym kompromisem może być nowe połączenie na żądanie (ale bez złożoności puli, po prostu podłączaj się ponownie za każdym razem).

Dla odczytu (tj. SELECT), względny narzut łączenia za każdym razem będzie wyższy, ale absolutny narzut w czasie zegara ściennego powinien być stały.

Jak już omówiono w innym miejscu tego pytania, powinieneś testować z prawdziwymi pytaniami, po prostu chciałem udokumentować, co zrobiłem, aby dojść do moich wniosków.

+0

Zgadzam się z większością tego, ale ... zamiast zgadywać o wydajności, można ją przetestować. I, jak widać z mojej odpowiedzi, otwarcie połączenia zajmuje około 4-8 razy tyle, co proste zapytanie dla mnie, i więcej niż 10x dla OP, co sugeruje, że twoje przypuszczenie, że "otwarcie nowego połączenia nie idzie być tak złym "jest złe. Może się zdarzyć, że rzeczywiste zapytania OP będą znacznie wolniejsze niż moje proste zapytania, które okazują się nie mieć znaczenia, ale nie można zakładać, że a priori. – abarnert

+0

Myślę, że jeśli przejdziesz 'check_same_thread = False', możesz udostępnić wątek połączenia.Moje pytanie brzmi: jeśli to zrobisz, czy nadal musisz zapewnić własną synchronizację lub sqllite obsługiwać za pomocą jednego wątku połączenia dla Ciebie? –

+0

Najnowsze wersje (3.3.1 i późniejsze) SQLite3 zezwalają na używanie połączeń w wielu wątkach, a parametr 'check_same_thread' istnieje w celu obsługi tego w sposób, który nie złamie żadnego starszego kodu, który był oparty na starym zachowaniu (lub jest na platformie, do której jest dołączona starsza wersja SQLite). Należy jednak upewnić się, że nie ma żadnych wybitnych blokad plików SQLite - tj. Wszystkie instrukcje zostały sfinalizowane. Aby uzyskać szczegółowe informacje, patrz [tutaj] (http://www.sqlite.org/faq.html#q6). Jest to ograniczenie SQLite, a nie moduł Python. – Cartroo

0

Jest bardzo nieefektywny, a nie wątkowy - do rozruchu.

Zamiast tego należy użyć przyzwoitej biblioteki puli połączeń. sqlalchemy oferuje łączenie i wiele więcej, lub znaleźć lżejszą pulę dla sqlite.

+0

Co dokładnie masz na myśli, mówiąc, że nie jest bezpieczny dla wątków? Czy to nie oznacza, że ​​tylko jedno ma dostęp do bazy danych w tym samym czasie? – matts

+0

@matts: Ach, jak się okazuje, tornado nie używa nici (dochodzi do jednego założenia). Jeśli korzystasz z aplikacji wielowątkowej, otwarcie bazy danych sqlite z wielu takich wątków nie będzie działać. –

+0

Dobrze. Więc przez wątki bezpieczne masz na myśli błędy takie jak "baza danych jest zablokowana"? – matts

1

Jeśli chcesz wiedzieć, jak nieskuteczne jest coś, napisz test i przekonaj się sam.

Po naprawieniu błędów, aby Twój przykład zadziałał w pierwszej kolejności, i napisałem kod, aby utworzyć test, aby go uruchomić, ustalenie, jak to zrobić z timeit było tak banalne jak zwykle.

Zobacz http://pastebin.com/rd39vkVa

Więc, co się dzieje, kiedy go uruchomić?

$ python2.7 sqlite_test.py 10000 
reopen: 2.02089715004 
reuse: 0.278793811798 
$ python3.3 sqlite_test.py 10000 
reopen: 1.8329595914110541 
reuse: 0.2124928394332528 
$ pypy sqlite_test.py 10000 
reopen: 3.87628388405 
reuse: 0.760829925537 

Więc otwierania bazy danych trwa około 4 do 8 razy tak długo, jak działa martwego-proste zapytanie przeciwko niemal pusta tabela, która nic nie zwraca. Oto twój najgorszy przypadek.

+0

'$ python3.2 test.py 10000' ' ponowne otwarcie: 0.8462200164794922' 'ponowne użycie: 0.07594895362854004' Wygląda na to, że biorąc pod uwagę, że większość moich zapytań będzie o wiele większa, nie spowoduje to zauważalnej różnicy , i biorąc pod uwagę, że sam wszystko wdrażam, byłby to ból z niewielkim zyskiem. Przykro mi, ale nie wiem, jak wstawić znaki nowej linii w komentarzach: – matts

+0

@matts: Niestety, z tego, co wiem, nie można wstawiać nowych wierszy w komentarzach. (Możesz je wkleić, ale i tak zmienią się one w spacje). W każdym razie proponuję wypróbować test z próbkami realistycznych zapytań dla twojego przypadku użycia przed podjęciem decyzji. Jeśli jest to 11x dla minimalnego zapytania, może nadal wynosić 2x dla ważnego zapytania, które jest zdecydowanie nadal zauważalne ... lub może wynosić 1.001x, w takim przypadku możesz je zignorować. Ale bardzo trudno jest odgadnąć z góry. – abarnert

0

Dlaczego nie po prostu ponownie połączyć co N sek. W moich usług AJAX uprzedzona/baz danych, których są 30-40 wierszy butelki ja podłączyć każdą godzinę, aby pobrać aktualizacje, istnieją lepsze bazy dostosowane jeśli trzeba pracować na danych na żywo:

t0 = time.time() 
con = None 
connect_interval_in_sec = 3600 

def myconnect(dbfile=<path to dbfile>): 
    try: 
     mycon = sqlite3.connect(dbfile) 
     cur = mycon.cursor() 
     cur.execute('SELECT SQLITE_VERSION()') 
     data = cur.fetchone() 
    except sqlite3.Error as e: 
     print("Error:{}".format(e.args[0])) 
     sys.exit(1) 
    return mycon 

I w pętli głównej :

if con is None or time.time()-t0 > connect_interval_in_sec: 
    con = myconnect() 
    t0 = time.time() 
<do your query stuff on con> 
Powiązane problemy