ru_java


ru.java

все о языке программирования java


Previous Entry Share Next Entry
Как сделать неблокирующую загрузку из базы?
raven
strangeraven wrote in ru_java
На всякий случай опишу текущую ситуацию:
Есть web сервер, сделанный из Tomcat+Tapestry+Hibernate+Postgresql+еще всякое до кучи.

И вот приходит пользователь и говорит: отдай мне страничку.

А чтобы страничку нарисовать, надо слазить за данными в postgres и еще другие удаленные места.

Допустим, для страницы нужны данные d1, d2, d3 из внешних источников s1, s2, s3 (один из которых postgres)
Как это выглядит с точки зрения потоков:
Для обработки пользовательского запроса tomcat достает из пула рабочий поток и говорит ему: нарисуй html. Рабочий поток последовательными блокирующими запросами лезет за данными в s1, s2, s3 и рисует html.

Итого получается, что общая задержка на извлечение данных суммируется: t1+t2+t3.

Хочется запросить данные параллельно неблокирующими запросами. То есть сказать что то типа: источник, вот тебе запрос на эти данные. Но ты пожалуйста мой поток не блокируй и верни сразу же управление. А когда данные приедут, кинь мне какой-нибудь event или дерни мой callback.

Тогда бы я одновременно сделал запросы в s1, s2, s3 и спокойно ждал, пока они все не приедут. Задержка бы получилась не сумма, а max( t1, t2, t3)

С прочими внешними источниками так договориться можно, а вот можно ли так договориться с Hibernate?
Ну или если Hibernate так не умеет, может есть что-то аналогичное, что умеет?

  • 1
А почему не сделать три треда, лазающие за данными?

Ну проблема в том, что их тогда не 3 будет, а целая куча.

Ведь пользовательских запросов одновременно идет много, каждый томкатовский поток захочет породить по 3 новых потока под блокирующие ожидания. Или не 3, а сколько там ему этих данных надо.

Придется для этих потоков отдельный пул заводить, как то их менеджить, перекладывать результаты из них обратно в томкатовский... Можно конечно, но возня еще та.

Я и подумал, может есть уже что-то готовое на эту тему.

Зачем его заводить? Куча DB connection pool-ов есть.

Ну а какие еще варианты, если ты хочешь распараллелить выполнение каждого запроса?

В идеале - неблокирующие асинхронные запросы.

Чтобы сказать что-то типа:
Future data1 = session.get(id1);
Future data2 = session.get(id2);
Future data3 = session.get(id3);

Ну и дальше в цикле ждать пока все data.isDone() не вернут true.

Не вопрос, можно действительно сделать ExecutorService, скармливать ему таски на каждый запрос. Но меня тут смущает - а не породится ли при этом over 9000 потоков, которые будут висеть на блокировках и ждать.

Ок. я могу что-то до конца недопонимать.
Если у тебя есть ExecutorService, то там есть пул тредов.
Если к тебе в худшем варианте пришли 9000 пользователей, каждому из которых нужно три параллельных задачи - тебе понадобится по три дополнительных треда на брата. То есть, огромное количество тредов в сумме. А Экзекутор имеет пул тредов, и одновременно может выдать все равно какой-то разумный лимит. Но в этом случае остальные будут ждать свободных тредов в пуле экзекуторсервиса.

В приципе так и есть. Но ситуация,когда пул кончился не очень хорошая - это означает что будут сильно лагать запросы, которым пула не хватило.

Еще будет висеть столько же db connections - по 1 на каждый ожидающий запрос.

Скорее всего в итоге придется выкрутить максимум, как пула потоков, так и пула db connections в те самые 3x9000

У томкета тоже не бесконечный пул, если что. Можно ориентироваться на его максимум.

с ява-фучами это неизбежно, они всегда блокирующие, используйте Акку или rxJava.

В Java 8 есть CompletableFuture. Если есть callback на асинхронные вызовы, то получить CompletableFuture легко: http://stackoverflow.com/questions/23301598/transform-java-future-into-a-completablefuture

(второй ответ)

Edited at 2015-10-08 02:34 pm (UTC)

Просто надо сделать асинхронный сборщик данных на основе ExecutorService на уровне выше абстракций источников данных, и пофигу, Hibernate там, голый JDBC или что вообще.

Подозреваю, что в таком случае может поломаться OpenSessionInView, если он используется, конечно. Тут же потоки из пула не будут дочерними к тому, что обрабатывает запрос.
Если данные выкачиваются из БД полностью, и ленивая догрузка при прорисовке страницы не происходит, то да, проще.

Ну вот, это даже считается некоторыми антипаттерном: https://blog.frankel.ch/the-opensessioninview-antipattern

Доводы справедливые в статье, но ведь нет однозначного решения, которое решило бы все проблемы, ничего альтернативного не предложено.

Другими словами, изначальный мой посыл остается актуальным: все контексты, управляемые через ThreadLocal, в случае использования ExecutorService придется протягивать явно в вызываемый поток. Это может многое сломать.

В тэйпстри можно не на активации начитывать данные а on demand в геттере.
пример - проверка на нулл и инициализация с кэшированием на весь request processing cycle.
еще как вариант пользовать progressive display

Оба варианта применимы в t5. Если более ранняя версия - хз.

Да, конечно, есть и другие методы оптимизации.
И кеши используются.

Но я сейчас раздумываю еще вот над таким.

не айс это потоки на вебе порождать. да и с транзакциями неясно как разруливать если понадобятся, потоки то уже не мэнеджед. Я бы не смотрел в данном случае в сторону этой оптимизации, а если есть необходимость - то лучше пересмотреть дизайн приложения и гуй

Я начал смотреть, потому что для других источников данных можно сделать честные асинхронные запросы без порождения новых потоков.

То есть там можно отправить запрос в сеть без блокировки и выставить callback на ответ. Когда обработчик запросов выгребет ответ из сети, он этот callback дернет, я через callback разбужу ожидающий рабочий поток Томката и начну рисовать.

Ну и дальше возник вопрос, а можно ли как то таким же способом и с базой данных поступить.

> я через callback разбужу ожидающий рабочий поток Томката и начну рисовать

не очень понятно как такое возможно. разве что через серверный пуш?

Если уж для 2-х ресурсов вы можете организовать асинк, то почему в главном потоке не дергать постгресс, а в асинке остальные 2 ресурса. 3-й немэнеджед поток тогда не нужен

Edited at 2015-10-07 12:36 pm (UTC)

> не очень понятно как такое возможно. разве что через серверный пуш?

Дело в том, что другие источники данных - это не бд. Это сервисы, которые выставляют некое сетевое API. И у меня есть возможность запрашивать данные у этого API асинхронно, с получением ответа через callback.

Почему хочется для бд тоже асинк:
1. Ну во первых чтобы все было единообразно.
2. Во вторых, потому что может потребоваться несколько запросов к бд, которые друг с другом никак не связаны. Ну например, на web странице несколько независимых компонентов, и каждому надо какие-то свои данные. И тут было бы конечно хорошо эти данные запросить не по очереди, а параллельно. Да, я помню про progressive display, но все равно пытаюсь рассмотреть и вот такой вариант, с распараллеливанием на серверной стороне.

Edited at 2015-10-07 03:10 pm (UTC)

ajax в наши дни уже устарел?
как насчет кеширования?

Видимо придётся как-то без Hibernate. А Postgres работает асинхронно замечательно, правда я тестировал в C#.

Добро пожаловать в мир async :)

Правильный путь: всё асинхронно, даже веб-сервер – всё на NIO (ищите серверы/контейнеры с async servlets в 3.0 и кто их имплементирует, или Jetty).
Первый NIO реактор - веб-сервера, второй реактор - внешние ресурсы (API & БД). Так сделан, например, Akka Http module / Akka Http Async client.

Дальше, если со сторонними async API понятно, то с классическими БД и асинхронными чтениями всё в очень ранней стадии:
https://github.com/mauricio/postgresql-async для PostgreSQL
https://code.google.com/p/async-mysql-connector/ для MySQL

Hibernate здесь не поможет: все вызовы синхронные и блокирующие.

В терминах Java 8 ваши действия таковы (псевдо-код для Jetty: https://wiki.eclipse.org/Jetty/Feature/Continuations):

Continuation continuation = ContinuationSupport.getContinuation(request);

CompletableFuture<Response1> apiResponse1Future = restService1.callSomething(params);
CompletableFuture<Response2> apiResponse2Future = restService2.callSomething2(params2);
CompletableFuture<DBResultSet> dbResponseFuture = asyncDBDriver.sqlQuery("SELECT ... ");

// дальше вы делаете общий CompletableFuture, и когда всё готово – строите http response для отдачи веб-серверу:

CompletableFuture.allOf(apiResponse1Future, apiResponse2Future, dbResponseFuture).
  thenRunAsync(() -> {
      Response1 r1 = apiResponse1Future.get();
      Response2 r2 = apiResponse2Future.get();
      DBResultSet r3 = dbResponse3Future.get();

      // ...build response page

      continuation.resume();
      writeResults(httpContinuation.getServletResponse(),results);
      continuation.complete();
  })

continuation.suspend();

///

С таким подходом у вас не будет ни одного заблокированного на IO потока. Что в целом даёт очень хорошую производительность/scalability.

надеюсь мой сумбурный ответ помог :)

P.S. извините за разметку


Edited at 2015-10-07 07:44 pm (UTC)

Это все хорошо, но у меня уже старый работающий проект, который "слегка тормозит".

То есть взять там и все поменять уже не получится. Только какие-то небольшие отдельные части. Даже соскочить с Hibernate довольно трудозатратно. Самостоятельно поисследовав документацию я никаких упоминаний об асинках там не нашел. Но как говорится, надежда умирает последней: а вдруг я просто что-то пропустил, или там есть какие-то скрытые возможности, или какая-то сторонняя модификация, которая позволяет использовать его в асинхронном режиме.

Можете попробовать вынести какую-то критическую/высоконагруженную часть вашего проекта в микросервис. Таковы мировые тенденции - уходить от монолитной архитектуры.
Конечно, вам принимать решение - всё зависит от ситуации,

  • 1
?

Log in