Asynchroniczne zapytania do bazy danych w kontrolerze

Odpowiedz Nowy wątek
2018-11-13 10:34
eL
0

Mam endpointy w kontrolerze napisane w Springu które zwracają pewne dane z bazy danych (wcześniej trzeba je trochę obrobić).
Niektóre selecty po stronie bazy wykonują się za długo (~2 sekundy) a w tym czasie wątek jest zablokowany. Kontroler co prawda musi czekać na dane z bazy więc i tak zwróci je po ~2 sekundach, ale można by wykonywać inne operacje zanim baza nie zwróci wyników (np. obrabianie danych które przyszły z kolejnego requesta do wykonania następnego selecta).
Jest to dla mnie ważne bo zapytań jest na tyle dużo że muszą być kolejkowane przez ograniczoną pulę wątków.
Zacząłem eksperymentować z ComplatableFuture ale koniec końców nie działa to tak jak bym oczekiwał więc może moje rozumowanie jest złe albo wykonana implementacja. Zacznijmy od tego pierwszego.
Oczekuję:

  1. Kontroler łapie zapytanie
  2. Obrabia dane
  3. Robi select'a do bazy (~2-3 sekundy)
  4. W czasie trwania zapytania do bazy kontroler odbiera kolejny request, obrabia dane i robi zapytanie do bazy.
  5. Jeśli poprzednie zapytanie do db już się zakończyło to zwróć wyniki, jeśli nie to przyjmij kolejny request.

Natomiast u mnie tak nie działa. Kontroler może wykonać kilka operacji asynchronicznie w ramach tego samego requesta w czasie kiedy baza pracuje nad zwróceniem jakichś wyników, natomiast nie odbierze kolejnego zapytania dopóki pierwsze w całości się nie zakończyło.
Pytanie więc czy jest to w ogóle możliwe? Aktualnie zrobiłem to na Springu ale mogę to zastąpić czymkolwiek, byle podstawą była Java.

edytowany 2x, ostatnio: eL, 2018-11-13 10:37

Pozostało 580 znaków

2018-11-13 11:55
Śliczny Siusiak
0

Może przyda się Spring WebFlux (ReactiveApi)?

Pozostało 580 znaków

2018-11-13 12:06
0

O ile kojarzę są 2 modele:

  • polling (klient co jakiś czas, być może krótki, odpytuje, czy jest już odpowiedź na mój request appX_UID_XYZ i dostaje responsa z payloadem)
  • notification / callback - przy requstowaniu dodajesz parametr, gdzie ma być wysłana notyfikacja o zakończeniu przetwarzania requestu "call me back at http://foo/bar/notify/request/appX_UID_XYZ") i tam możesz dorzucać jakiś payload

A komu niby ma być zwrócona odpowiedź? I w jaki sposób? Dla GUI to może wyglądać inaczej (websockets?) i inaczej dla aplikacji backendowych.

edytowany 1x, ostatnio: yarel, 2018-11-13 12:06
Z tego co rozumiem, @eL pyta o asynchroniczność wewnątrz serwera (na zasadzie async + await, Futureów, Promiseów czy Tasków znanych z innych języków), nie na zewnątrz (w odniesieniu do klienta). - Patryk27 2018-11-13 12:11

Pozostało 580 znaków

2018-11-13 12:15
eL
0

Trochę się nie zrozumieliśmy.
Klienci pukają do moich endpointów o konkretne dane. Kontroler łapie request, przygotowuje zapytanie do bazy i pyta bazę o dane X. Operacja selecta trwa czasami 2-3 sekundy. Jak się wykona ta operacja to dane z bazy muszą być jeszcze przygotowane, obrobione itp po czym można je zwrócić.
Z punktu widzenia klienta całość będzie synchroniczna bo wszystkie te operacje muszą się wykonać aby otrzymał wyniki natomiast wątki po stronie serwera kiedy robione jest zapytanie do bazy po dane stoją i nic nie robią a mogłyby w tym czasie na warsztat brać kolejny request który trzeba zwalidować lub też przetworzyć wyniki z bazy.

@Patryk27 dodał komentarz który krótko to podsumowuje.

Pozostało 580 znaków

2018-11-13 12:23
0

Zacznijmy od tego że żeby osiągnąć to co napisałeś w Springu nie musisz wykorzystać CompletableFuture.

Dla każdego requesta domyślnie Spring sam zrobi nowy wątek i zapytania będzie wykonywał równolegle. Jeżeli u ciebie tak nie jest to musiałeś coś pokombinować, np. zrobić customowy config, transakcje na poziomie SERIALIZABLE, manualne lockowanie wątków czy jeszcze coś innego.

Żeby ci dalej pomóc musiałbyś dać minimalny weryfikowalny przykład. (https://stackoverflow.com/help/mcve) i dał więcej informacji m.in. z jakiej bazy korzystasz.

edytowany 1x, ostatnio: Noozen, 2018-11-13 12:24

Pozostało 580 znaków

2018-11-13 12:28
0

Hmm, wewnątrz serwera to nie wystarczy zwykłe ExecutorService z dedykowaną pulą wątków i parę kolejek (w zależności od tego jak bardzo złożony jest flow przetwarzający)?

Jak dla mnie requesty nie biorą się znikąd i trzeba w końcu odpowiedź wysłać do klienta i albo synchronicznie (tak jak masz teraz, że wątek odbierający request jest przyblokowany, aż obliczenia będą gotowe) albo asynchronicznie (callback do klienta). 2-3 sek dla CPU to dużo, więc może być tak, że nie będziesz miał co przetwarzać, bo wąskie gardło będzie na bazie :-)

Pozostało 580 znaków

2018-11-13 12:33
eL
0
Noozen napisał(a):

Zacznijmy od tego że żeby osiągnąć to co napisałeś w Springu nie musisz wykorzystać CompletableFuture.

Dla każdego requesta domyślnie Spring sam zrobi nowy wątek i zapytania będzie wykonywał równolegle.

Jak najbardziej się z tym zgadzam. Domyślna pula wątków dla Tomcata uruchomionego jako internal w Spring Boot wynosi 200. Mogę to edytować ale problem jest w tym że mi to nie wystarcza. Zdarza się że requestów jest kilka tysięcy.
Kod produkcyjny póki co nie jest istotny bo tam wszystko jest synchronicznie zrobione przez co serwer nie wyrabia i chciałbym to zrobić od nowa. Odpowiedzi do klientów nie mogę zwracać asynchronicznie, natomiast w momencie kiedy wątek nic nie robi bo zrobił synchroniczne zapytanie do DB, chciałbym je zamienić na async i w tym czasie przetwarzać kolejny request z kolejki.

No ale ten wątek co teraz przetwarza jest zablokowany, bo jest synchroniczna komunikacja między klientem a Tomcatem, czyż nie? Zostanie zwolniony jak odeślesz response. Baza odpowiada 2-3 sekundy, jak wpuścisz więcej wątków (nie 200, a np. 400), to będzie odpowiadała szybciej przy większym obciążeniu? ;) - yarel 2018-11-13 12:42
Ja bym spróbował zmierzyć się z tematem od strony tuningu bazy danych, bo 2-3 sekundy to nie brzmi dobrze. - yarel 2018-11-13 12:51
W sumie racja... jakoś automatycznie założyłem, że ta operacja na bazie musi być skomplikowana i autor wie, że nie da się jej zoptymalizować - qbns 2018-11-13 12:59

Pozostało 580 znaków

2018-11-13 12:35
1

W takim wypadku zainteresuj się WebFlux. Albo podnieś pulę do paru tys (nie rób tego :D)

EDIT: WebFlux oznaczałby również zmianę w kodzie klientów, bo odpowiedzi byłyby już event streamem

Jeśli nie chcesz zmieniać API clientów i zwiększać puli połączeń na tomcacie, pomyślałbym o postawieniu jakiegoś API gatewaya, który robiłby delay jeśli byłoby dużo przychodzących requestów.
Np: Nginx https://www.nginx.com/blog/rate-limiting-nginx/, lub Zuul (nie znam)

edytowany 7x, ostatnio: qbns, 2018-11-13 12:46

Pozostało 580 znaków

2018-11-13 13:00
eL
0

@yarel: odpowiem tutaj w nawiązaniu do komentarza bardziej szczegółówo.

Komunikacja między klientem a serwerem jest synchroniczna i tego póki co nie ruszam bo nie mam możliwości modyfikować klienta. Ruch jest już rozłożony na kilka maszyn ale to nie wystarcza. Zależy mi tylko żeby szybciej rozładować kolejkę requestów. Dla uproszczenia weźmy taki przykład.
Pula wątków dla tomcata wynosi 10. W jednym czasie dostaję 100 requestów o jakieś dane z bazy. 10 requestów będzie obsługiwanych natomiast 90 pozostałych będzie czekać. Aktualnie wygląda to tak że 10 requestów będzie walidowanych, sklejane będą jakieś tam dane i robiony są zapytania do bazy które czasami trwają np. 2 sekundy a wątek w tym czasie czeka na odpowiedź z DB. Odebrał dane to je zwraca do klienta i kolejny request jest przetwarzany.
Koniec końców obsłużenie wszystkiego zajmuje jakieś 25 sekund z czego większość czasu te wątki czekają na odpowiedź z bazy przez to że są to zapytania synchroniczne.
Chciałbym więc to poprawić. Jeśli dla analogicznej sytuacji wpadnie taka sama liczba zapytań to 90 z nich i tak będzie w kolejce natomiast jeśli 10 aktualnie pracujących wątków wyśle asynchroniczne zapytanie do bazy to mogłoby brać z kolejki kolejny request i już przygotowywać zapytanie do bazy danych. Nie przyspieszy to jakoś diametralnie bo nadal zapytania do bazy będą trwały 2-3 sekundy, natomiast wątki które teraz po wykonaniu takiego synchronicznego zapytania nie robią nic, mogłyby w tym czasie obsługiwać kolejne zapytania. Tym bardziej że wiele zapytań zajmuje znacznie mniej czasu (np. 20-30ms) a i tak są zblokowane przez to że w kolejce wcześniej trafiły sie zapytania trwające znacznie dłużej.

Ps. Podany przeze mnie czas 2-3 sekund na zapytanie bazodanowe to bardziej takie zwrócenie uwagi na to że część zapytań wykonuje się dużo dłużej niż pozostałe. Mam konkretne metryki ale też nie chcę tutaj o nich pisac więc przyjąłem że jest to po prostu długo trwające zapytanie.

edytowany 1x, ostatnio: eL, 2018-11-13 13:00

Pozostało 580 znaków

2018-11-13 13:08
0

Hmm, jeśli umiesz rozróżnić na podstawie requestu, które zapytania będą ciężkie, a które nie, mógłbyś utworzyć osobne Tomcat connector pools. Jedna byłaby do obsługiwania szybkich zapytań, druga - do wolnych. Wiąże się to z tym, że serwis wystawiony byłby na dwóch portach, ale jeśli pomiędzy klientem a serwerem masz jakieś proxy, to możesz wdrożyć taką zmianę bez zauważalnej różnicy dla klienta.

edytowany 1x, ostatnio: qbns, 2018-11-13 13:10

Pozostało 580 znaków

2018-11-13 13:10
1

Rozumiem że chciałbyś zrobić nowy wątek przed zapytaniem SQL i robić coś sobie w tle. Niestety w praktyce nic tym nie osiągniesz. Dopóki SQL się nie skonczy to trzymasz połączenie na Tomcacie, innymi słowy nigdy nie będziesz ptrzetwarzał więcej niz te 200 połączeń w puli naraz.

Jeżeli chcesz zwolnić wątek zanim dasz odpowiedź do klienta, to tak jak napisał qbns, musisz wykorzystać np. WebFluxa który nie bazuje na serwletach.

Oprócz tego pozostaje ci: optymalizacja SQL, zwiększenie puli wątków, skalowanie wszerz i ew. żonglowanie połaczeniami co też już opisał qbns.

edytowany 1x, ostatnio: Noozen, 2018-11-13 13:11

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0