Как стать автором
Обновить

Комментарии 45

Текстом можно напоить несколько африканских деревень. Обычные зеленые потоки.

Не понимаю экзальтации по поводу этого лума. Будет еще одно средство реализации асинхронного/реактивного подхода. Все приложения резко станут модными реактивными? Ну да, так же как они "стали" модными реактивными с выходом реактора или корутин колина

Отрасли по прежнему критически не хватает гуру реактивной парадигмы, а также практики миграции с классики на неблокирующий подход. Писать хеллоуворлды на реакторе это одно, а мигрировать работающий сервис хотя бы на 20к строк - совсем другое.

Все приложения резко станут модными реактивными?

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

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

Неблокируемость обеспечивается и обычными потоками

И да и нет. Внутри традиционного потока код блокирующий (кроме того, локи, synchronized и прочее). Кол-во физических потоков ограничено + переключение между ними небыстрое. Если код неблокирующий, то и большое кол-во потоков не требуется. В эту сторону пошли реактивщики. А loom, да, позволяет исполнять код на виртуальных потоках так, как будто это реальные потоки. При этом в теории никаких телодвижений не требуется и код, написанный под обычные потоки, начнет работать на виртуальных, что сильно повысит масштабируемость.

Здесь нас (разработчиков) ждёт несколько сюрпризов.

Это понятно, поэтому и написал "в теории". Но, возможно со временем с развитием этой темы, этих сюрпризов станет поменьше.

уже существующий код может стать неблокирующим

Если почитать описание изменений API, то видно, что это не совсем так - там куча мелочей, которые изменены в реализации, от тихого игнора вызова до падения. Учитывая, что игры с потоками присущи как раз application-серверам, рискну предположить, что их как прослойку между VM и бизнес-кодом придется очень плотно переписывать.

Что такое "реактивная парадигма"?

Об этом -- в следующей части заметки.

Что ж, надеюсь вы там подробно раскроете разницу между dataflow, controlflow, continuation и eventstream.

Про это -- только в 73-й части заметки. Только, боюсь, здесь её опубликовать не получится. Меня и так на Хабре уже почти забанили.

Loom устарел, он был более актуален 15 лет назад, чем сейчас. Вон в соседней новости пишут про 128 ядерный серверный процессор. На современном железе, которое в настоящее время только 64-битное, на современной JVM легко создается несколько сотен тысяч обычных системных потоков.

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

Системные потоки кушают много памяти на стек, медленно переключаются и делают это в недетерминированные моменты времени. Поэтому куда эффективнее запускать по воркеру на ядро, каждый из которых пережёвывает легковесные потоки.

Всё так. Сергей Куксенко измерял выигрыш в производительности и получал 2-3 раза, если не ошибаюсь. Иван Углянский тоже что-то намерил и расскажет на JPoint (рекомендую). Кто-то говорит о порядках в отдельных сценариях. Всё может быть.

Вместе с этим кричащая разница -- это максимально возможное количество тредов, системных и виртуальных. Она оценивается в два с половиной порядка. Триста раз. То есть виртуальных потоков можно создать даже не сотни тысяч, как предполагал уважаемый pin2t, а миллионы. И JVM потянет.

Нуркевич в своём докладе (ссылка в заметке) упоминает, что "решил проблему 10k соединений". Про что это? Блокирующий (классический) подход предполагает, что каждое новое установленное соединение обрабатывается одним тредом. А в JDK жёсткое соответствие: один JDK-тред привязан одному системному треду, и наоборот. Вот 10k системных тредов уже слишком много для ОС. Приложение падает. Скорее всего, оно упадёт ещё до 10k. Поэтому несколько сотен тысяч -- вряд ли.

Проблема 10k решается неблокирующим подходом. Сейчас таких в мейнстриме два: реактивный и легковесные треды (фибры, горутины, корутины, виртуальные треды и так далее).


Ну не совсем. Порядка 10к java threads я создавал на достаточно обычной серверной машине с 64 гигабайтами и 48 ядрами. Вовсе не обязательно приложение будет падать, но с настройкой наверняка придется повозиться. Скорее всего, вылезет какой-нибудь лимит ОС типа числа открытых файлов, придется потюнить GC, ну и т.п. Насколько я помню, 100к threads просто на стек сожрет дохрена памяти (порядка 10 гигабайт, если мне память не изменяет). Так что утверждение про «легко 100к» больше похоже на байки.

И в любом случае, планировщик на 10к процессов, и планировщик на 500к процессов — это две большие разницы, независимо от того, легковесные они сами, или нет.

Нет никакой проблемы создать более 10К тредов на достаточно "средней" современной серверной машинке.
Немного возни с конфигами операционки, конфигами JVM, и все создастся.

Причем, вам надо 10К тредов в классическом подходе поток-на-запрос только для случая, когда у вас на одном хосте нужно обслужить 10К запросов одновременно. Либо, у вас есть 10К постоянно открытых соединений.

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

Реактивное программирование - это вообще не про решение проблемы 10К.
"проблема" 10К - жалкий частный случай, который просто удобно показывать в презенташках.

И green thread тоже не про решение проблемы 10К.
В конце концов, даже с green threads вы упретесь в ограничение количества локальных портов в системе (если ваш хост не торчит напрямую в интернет), а там порядок даже тот же.

В целом согласен, прокомментирую.

нет никакой проблемы создать более 10К тредов

ОК. Интересно, между прочим -- зачем?

на одном хосте нужно обслужить 10К запросов одновременно.

Да. В этом тонкость сообщения Нуркевича. Когда проект разрастается так, что нагрузка превышает некоторый порог (и число 10k тут условно, согласен), тогда блокирующий подход перестаёт быть адекватным. А до того момента блокирующий подход ого-го какой адекватный.

решать с помощью дополнительных хостов

Так о том и речь. Смотри "стоимость железа".

вы упретесь в ограничение количества локальных портов

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

В конце концов, даже с green threads вы упретесь в ограничение количества локальных портов в системе (если ваш хост не торчит напрямую в интернет), а там порядок даже тот же.

Это решается дополнительными адресами. Или, да — "торчанием" напрямую в интернет.

Синхронный вызов загоняет в парадигму «запрос-ответ». Проблема в том, что понятие функции, с которой мы все начинали учить программирование, совершенно неадекватно реальным процессам обработки в сети. Там по факту два встречных асинхронных потока (как минимум) и число событий в обеих направлениях неодинаково и заранее не всегда известно.

Конкретный пример — передача большого документа на примере таможенной декларации. В реальных системах (не буду называть конкретную, но это из реальной жизни) встречается такая дичь, что диву даешься. Пользователь нажимает кнопку приёма или отправки и весь интерфейс виснет. Что происходит, вообще непонятно. Или скорость низкая, но передача идет и надо ждать, или процесс завис и надо перегружаться. Диагностики ноль. Никакой loom тут не поможет.

По сути, надо иметь гранулярность уровня sub-method, когда внутри одного метода (прием или передача документа) асинхронно происходит куча другой обработки, причем, в обе стороны.

Записали очередной массив байтов в сокет. С какой скоростью он будет физически передаваться (или не передаваться) в данный момент, зависит от массы внешних факторов, так что надежный вариант ждать события успешного окончания записи в сокет или сбоя. Каждое надо обработать. При успехе обновить интерфейс пользователя, чтобы человек видел, как идет процесс. Даже это в своём потоке. Также нужно обеспечить дросселирование, чтобы не писать в сокет быстрее, чем физическая линия способна передать данные. Недопустимо ждать подтверждения приема на каждый фрагмент, пока первый фрагмент в пути, могут быть отправлены несколько следующих. Буфер свободен — пишем следующий фрагмент.

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

В общем случае может возникнуть набор разных асинхронных событий. Не вижу способа избавиться от асинхронности и сохранить эффективность.

Два встречных асинхронных потока событий это не набор пар запрос/ответ.

В серьезных системах при приеме последнего фрагмента нужно сделать кучу работы — собрать объект/документ до кучи, проверить его ЭЦП, сформировать заверенное ЭЦП системы уведомление в приёме документа, отправить его по ответному каналу отправителю, записать в реестры входящих/исходящих, при этом никакой обработки даже не начиналось. Потом найти обработчик и передать ему документ на обработку. Это тоже асинхронный процесс, на некоторые документы ещё должен человек посмотреть. Для многих сценариев синхронная парадигма неприменима даже при нулевой стоимости потока.

Нужен контекст более высокого уровня, с помощью которого можно всё собрать вместе.


Я вам всё же рекомендую поработать с файберами, чтобы не распространять глупости. На каждую задачу создаётся файбер, который может приостанавливаться, когда хочет, не блокируя ни UI, ни другие файберы. Код получается простой, понятный, без возьни с событиями, с дросселированием, отображением статуса в реальном времени и прочими куртизанками.

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

Можно и с событиями повозиться. Это же задача синхронизации. Для её решения понадобятся дополнительные примитивы. Например, Future / Deferred.

  1. Отправитель формирует Future-контейнер для ожидания результата и кладёт контейнер в словарь по идентификатору запроса.

  2. Отправитель отправляет запрос.

  3. Отправитель засыпает на Future.

  4. Поток-получатель находит по идентификатору запроса в словаре контейнер и помещает туда ответ.

  5. Отправитель просыпается и продолжает обработку результата.

Код отправителя последовательный, в этом он не отличается от синхронного.

Так просто не получается.

Отправитель производит отправку записью множества фрагментов по одному за один вызов. С некоторой задержкой начинает поступать обратный поток посредством множества асинхронных вызовов. Так что отправитель не может заснуть ни на отправке, ни на приёме.

Запрос не посылается за один раз, при такой организации работать невозможно при больших объемах. И результатов множество - подтверждение приема каждого фрагмента, уведомление в приеме всего документа с ЭЦП и только потом приходят результаты запроса. Логика обработки каждого типа результата разная. И не факт, что пользователь не выключит компьютер до того, как придет ответ.

В функции отправки данные засовываются и выгребаются в цикле (есть место для отправки - держи, есть что принять - давай). Для вызывающей же стороны это обычный синхронный вызов функции.

Цикл запрос-ответ не катит, добавлять задержку к каждому запросу недопустимо.

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

Примитивные способы грамотного Заказчика не устраивают.

Цикл "запрос или ответ".

При двух встречных асинхронных потоках в каждом направлении запись следующего фрагмента начинается по локальному событию успешного окончания записи в сокет предыдущего. Когда предыдущий запрос даже до сервера провайдера не добрался.

Такое поведение нельзя получить, вызывая в цикле вызов "запрос-ответ" с ожиданием результата каждого вызова.

Попробуйте получить такие тайминги при вызове в цикле синхронного вызова функции

09:34:38.343 - Send MD successful
09:34:38.343 - Send MD successful
09:34:38.344 - Send MD successful
09:34:38.344 - Send MD successful
09:34:38.345 - Send MD successful
09:34:38.345 - Send MD successful
09:34:38.345 - Send MD successful
09:34:38.346 - Send MD successful
09:34:38.346 - Send MD successful
09:34:38.346 - Send MD successful
09:34:38.346 - Send MD successful
...
09:34:38.379 - Send MD successful
09:34:38.379 - Send MD successful
09:34:38.379 - Send MD successful
09:34:38.379 - Send MD successful
09:34:38.380 - Send MD successful
09:34:38.380 - Send MD successful
09:34:38.380 - Send MD successful
09:34:38.381 - Send MD successful
09:34:38.381 - Send MD successful
09:34:38.400 - ## handle response ACK from Server to Msg 7 from GUI
09:34:38.401 - ## handle response ACK from Server to Msg 8 from GUI
09:34:38.402 - ## handle response ACK from Server to Msg 9 from GUI
09:34:38.402 - ## handle response ACK from Server to Msg 10 from GUI
09:34:38.403 - ## handle response ACK from Server to Msg 11 from GUI
09:34:38.403 - ## handle response ACK from Server to Msg 12 from GUI
...
09:34:38.436 - ## handle response ACK from Server to Msg 86 from GUI
09:34:38.436 - ## handle response ACK from Server to Msg 87 from GUI
09:34:38.436 - ## handle response ACK from Server to Msg 88 from GUI
09:34:38.437 - ## handle response ACK from Server to Msg 89 from GUI
09:34:38.437 - ## handle response ACK from Server to Msg 90 from GUI
09:34:38.437 - ## handle response ACK from Server to Msg 91 from GUI
09:34:38.437 - ## handle response ACK from Server to Msg 92 from GUI
09:34:38.438 - ## handle response ACK from Server to Msg 93 from GUI
09:34:38.438 - ## handle response ACK from Server to Msg 94 from GUI
09:34:38.438 - ## handle response ACK from Server to Msg 95 from GUI
09:34:38.439 - ## handle response ACK from Server to Msg 96 from GUI
09:34:38.439 - ## handle response ACK from Server to Msg 97 from GUI
09:34:38.440 - ## handle response ACK from Server to Msg 98 from GUI
09:34:38.440 - ## handle response ACK from Server to Msg 99 from GUI
09:34:38.441 - ## handle response ACK from Server to Msg 100 from GUI

Это по обычному интернету на ширпотебовских раутерах, компьютеры в разных квартирах на разных улицах. Ещё и древний комп на i7-3770 с древним сетевым оборудованием.

Вы думаете пинг в 50мс тут кого-то впечатлит?
Меня бы больше впечатлило умение читать, что вам пишет собеседник, вместо героической борьбы с соломенными чучелами. Ну или на ходой конец понимание отличия оператора "И" от оператора "ИЛИ".

При пинге 50 мс 100 вызовов в цикле синхронного вызова функции дадут 5000 мс, а не 98 мс

Дадут. А ведь можно запустить 100 новых тредов и в каждом их них сделать вызов. И дождаться завершения 100 тредов в треде-родителе. Понятно, эту схему придётся усложнить с учётом дросселирования, переотправки кусочков в случае сбоя и отмены пересылки всего документа в каждом из дочерних тредов.

Между прочим, интересно, поддерживаете ли вы в своём приложении отмену отправки документа и остановку пересылки его кусочков? Это одна из центральных тем второй части заметки.

Давайте пойдём сверху вниз? Допустим, у вас имеется чёрный ящик, внутри которого реализована функциональность:

  1. отсылка документа по кусочкам с подтверждением приёма каждого кусочка;

  2. получение уведомления о приёме документа с ЭЦП;

  3. получение результатов запроса.

Ну так схема 1-2-3-4-5 из комментария выше подходит. На шаге 2 (отправитель отправляет запрос) посылаем событие в чёрный ящик "начать отсылку докумена по кусочкам". На шаге 4 из комментария выше чёрный ящик, когда получил результат запроса, положит его в контейнер. В результате на самом верхнем уровне чёрный ящик вместе со всей своей головоломной машинерией скрыт за фасадом "школьной" функции "удалённо обработать документ".

Не так всё просто.

Каждое уведомление надо обработать и в результате обработки обновить интерфейс пользователя и данные на стороне клиента. Запросов параллельно несколько, сокеты ценные ресурсы и на клиента сокет один. По которому надо передать туда обратно кучу всего. И данные и команды. И то и то имеет приоритеты, команды обрабатываются в первую очередь. Более приоритетный документ ставит на паузу отправку менее приоритетного. Двустороння асинхронная шина (набор шин) краеугольный элемент распределенной системы и попытка спрятать её под одеяло приносит больше проблем, чем решает. Удаленный метод не должен выглядеть как локальный. Удаленный метод требует реализации формализованного двустороннего протокола обмена.

Это 100% корректный подход, которым вы достигаете нужного вам результата. Прекрасно. Рискну предположить, что поддержка кода вашего приложения стоит так много, как нам об этом и рассказывает Нуркевич.

Жизнь асинхронна и события по факту происходят асинхронно.

Установилось сетевое соединение - событие.

Разорвалось сетевое соединение - событие. Надо всё корректно закрыть и почистить.

Успешно закончился SSL handshake - событие, из параметров которого ещё нужно извлечь сертификат другой стороны и произвести аутентификацию, авторизацию и создать ентри в таблице сессий.

И всё это надо обработать как можно быстрее.

Что касается стоимости - часто более общее решение гораздо дешевле кучи частных. У нас всё на метаданных, весь обмен идет по набору унифицированных шин. Переход на OSGi позволил избавиться от необходимости писать кучу самопала. Один перенос парсинга XML с сервера на клиентов позволил сэкономить тучу денег на серверном оборудовании. Сейчас ещё вычистим старые реализации обработки на сервере списочных данных, которые сделаны в виде старых добрых списков в оперативной памяти, и будет совсем хорошо.

Если в процессе передачи нужно обработать кучу промежуточных событий разного типа, это уже не черный ящик с синхронной функцией "удалённо обработать документ". Не говоря о том, что нужно корректно обработать случай обрыва связи и начать с нужного места дальше после восстановления связи/перезагрузки на одной или обеих сторонах.

Во-первых, подчеркну ещё раз, не с "синхронной функцией", а с асинхронной. И, понятно, эта функция должна быть идемпотентна. И её нужно вызывать при старте приложения для недообработанных документов по-новой.

Во-вторых, здесь не проповедуют какой-то религии, здесь не секта. Мы обмениваемся опытом, точками зрения. С моей точки зрения, асинхронное программирование на лёгких тредах способствует декомпозиции кода, облегчает написание сложных систем. Эта точка зрения никого ни к чему не обязывает. Считаете свой способ построения высокопроизводительного асинхронного кода единственно возможным -- пожалуйста.

По описанному процессу спорить не о чем. Всё так и есть. Два встречных асинхронных потока, и так далее.

Не вижу способа избавиться от асинхронности и сохранить эффективность.

Задачи избавиться от асинхронности не стоит. И обработка сообщений, и треды (в том числе легковесные треды) -- это разные способы организации асинхронной работы.

Два это минимум. Только ответных минимум три.

Один - отправка подтверждения приема фрагмента.

Другой - отправка уведомления в приеме документа, на которое наложена ЭЦП системы.

Третий - отправка результатов обработки.

Все формируются разными сервисами. Второй и третий запускаются на обработку параллельно.

А где вы храните высокоуровневый контекст: в БД (на диске) или в оперативной памяти?

И там, и там. На БД или ФС долго, в ОЗУ ненадежно.

Хотим попробовать Optane Persistent Memory с возможностью побайтовой адресации.

Пока сбоев нет, можно пользоваться данными в ОЗУ, в случае сбоя надо синхронизировать данные на обеих сторонах. Так как всё асинхронно, а по сети ненадежно, надо определить, с какого места продолжать после сбоя. Отдельным запросом по UID документа запрашивается сколько байт записано на сервере, отправитель продолжает отправку с нужного места. Самое сложное в том, что все ответные данные на сетевом уровне приходят на один и тот же метод обработчика, только на разные экземпляры, и надо понять, какой обработчик дальше вызвать.

Давайте пока упростим: отложим в сторону проблему персистентности и будем считать, что экземпляр один. Данные в памяти умещаются, и хорошо. Обычно для хранения контекста в памяти применяют Акторы. Вы пользуетесь Акторами?

Контекст размазан по ФС и памяти, Акторами не пользуемся.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий