Pull to refresh

Comments 37

А если прочитать как «Стивьер» — в воздухе отчётливо зазвенят пиастры ^______^

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

Кстати, это было бы очень интересно почитать. Как оплачивал домены, сервера, как админил их, какие трюки использовал. Может быть - пытались ли его вычислить, и на каком уровне это пытались сделать, а на каком уже не стали искать (очевидно, что при бесконечных ресурсах - вычислили бы).

Кстати, это было бы очень интересно почитать...

Это пока что секретно. Флибуста работает.

Если вам интересно, могу написать статью о работе библиотечного сообщества Флибусты, которую имел (не) счастье наблюдать со стороны.

Раз заговорили о кишках жавы, повторю свой старый вопрос…

Допустим, у нас есть жаба. Которая «нажралась памяти и спит» © Башорг.

Мы подключили вторую машину по гигабитке. Запустили там вторую жабу.

Пока ничего сложного.

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

Первая жаба более-менее помониторила статистику обращений к памяти, помониторила лаги и поняла, какие данные ей отправить в своп, а какие — держать под рукой. Все так делают, с большей или меньшей эффективностью и с более или менее «умными» профилировщиками. Даже Wolf3D так делал. Прямо на 80286-й. Там всё было предельно просто — страницы фиксированного размера, ридонли с дискетки, история последнего обращения к странице… тут, конечно, всё сложнее. При желании можно даже по коду предсказывать заранее, когда потребуется страница, и запрашивать из свопа чуть загодя. Виртуалка же, всё перед глазами.

Но я отвлёкся/размечтался. Пока всё было просто. Речь шла только о данных. Теперь мы делаем финальный шаг: смотрим, какие треды к этим данным обращаются. И видим, что у нас, допустим, данные о партиклях живут «вещью в себе», ими занимается отдельный тред, а тред, допустим, геймплея знай себе спаунит иногда десяток-другой партиклей и запрашивает у этого отдельного треда, сколько и каких партиклей в поле зрения сейчас.

И тут мы принимаем эпохальное решение: отправить этот тред в ссылку на вторую машину вместе с его страницами памяти. Получается, что для нас эти страницы — своп, а тред поимел дикие лаги в плане общения с остальными тредами. Но для него, наоборот, свопом выглядит вся остальная память, а лаги, таки да, обоюдоострые. Синхронизация? Так у нас же виртуалка! Мы можем напихать в ходе JiT любые дополнительные операции, которые нужны для прозрачной синхры тредов, крутящихся на разных машинах, чтобы мерзавцы не заподозрили подвоха. А гигабитка — штука очень быстрая и лаги зачастую у неё на уровне самой PCIe, в которую она воткнута.

Получается такой «своп тредов вместе с данными», и если приложение написано так, что какие-то треды крутятся «вещью в себе», минимально общаясь с соседями (а зачастую так и бывает) — после пары секунд профилирования они улетают в этот «метафорический своп».

Кто что по этому поводу может сказать?

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

Согласен, поэтому про нативное ПО не заикался… но разве мы не вольны в JRE перехватить что угодно и отфорвардить куда угодно, в рамках такой реализации?

Вот нативные библиотеки той же графики или звука, они да, могут работать только на «главной» машине… звук и графику, например, разнести по кластеру эта штука не сможет, или будет постоянная пересылка данных.

Не могу придумать пример ПО, где это было бы востребовано и уместно.

Ну, это самое лёгкое — игры с ожиревшей физикой типа Мимокрафта, где уже более-менее изолированные клиент и сервер под одним капотом (там даже вручную можно распараллелить вроде бы), синтезаторы-монстры типа XILINX (часть тредов проверяет одни варианты трассировки, часть — другие, обмен между ними минимален), а уж обработка видео-то…

Но могут быть проблемы с обращением к OpenGL, конечно. Софт должен в принципе уметь работать в режиме поддержки нескольких видеокарт, причём каждая — из отдельного треда. Или придётся выбирать из всех видеокарт какую-то одну, как обычно и бывает. Но если это просто потому, что нет поддержки — это нормально. А если потому, что работу со всеми видеокартами сделали в общем треде — вот тут именно моя архитектура даёт спотык.

игры с ожиревшей физикой типа Мимокрафта, где уже более-менее изолированные клиент и сервер под одним капотом

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

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

синтезаторы-монстры типа XILINX (часть тредов проверяет одни варианты трассировки, часть — другие

Тут будет конфликт на общих данных. Нужно, чтобы при обращении к данным они погдружались в память одной машины, но тогда должны быть "вытеснены в своп" для другой машины, т.е. при обращении к данным происходил Page Fault и перенос на ноду, которая обращается. Тогда какие-то общие данные, сам проект, который трассируем, какие-то правила трассировки, настройки, к которым идёт постоянное обращение, будут безостановочно кочевать с одной ноды на другую, если заранее не озаботиться копией для каждого потока. Но мы же хотим, чтобы всё работало автоматом. Ручное распараллеливание и так понятно, что можно сделать.

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

Немного сложнее, но в целом плюс-минус понятно.

Как потом изменения мержить, если оба конца захотят выполнить запись.

Видимо, «потом» тут не прокатит, а придётся при записи в дублированную область приостановить тред, записать в обе копии (локальную и удалённую), а затем возобновить.

То есть если какие-то треды пытаются выдать гигабайт данных по очереди, один чётные байты, другой нечётные — профилировщик после десятка записей это заметит и загонит оба на одну из машин. Но в целом это для общих данных, да ещё и используемых кучей тредов, не очень типично КМК :) Особенно если они действительно входные.

Вот что может быть — так это маленький локальный кусочек данных в этой общей области, в который преимущественно пишет один тред. Но тогда его страницу профилировщик быстро выловит и дедуплицирует обратно. Никаких особых действий тут не нужно — он именно по этому критерию и принимает решение в общем случае.

Это я всё, конечно, про асинхронную запись. Если два треда собираются по одному адресу писать — обычно (если это не синхра на атомарках) используются средства синхронизации, а с ними мы вроде разобрались — предполагаем, что обе жабы нормально и вовремя друг с другом в этом плане делятся информацией. А если синхра на атомарках (не знаю, насколько это нормально в жабе, кстати) — то она сама по себе асинхронна, и на неё распространяется вся первая часть.

UPD: кармаплюс за хорошие, конструктивные, предметные вопросы.

Видимо, «потом» тут не прокатит, а придётся при записи в дублированную область приостановить тред, записать в обе копии (локальную и удалённую), а затем возобновить

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

Если там не использовалась синхронизация — значит, разработчик (предполагаем его в здравом уме и трезвой памяти) знает, что произвольные лаги записи не имеют значения.

Соответственно, мерж получается ну ооочень примитивный :) Скорее даже синхронизация собственной организации памяти, чем мерж. Чтобы просто одно и то же было в обеих копиях.

Будь это данные из треда А или из треда Б — не суть, раз разработчику не важно, то и нам — тем более.

UPD: или речь о записи в одну область, разные адреса? Но тогда это просто одновременный взаимный обмен пакетами, в которых указано, что на что изменить, а после получения подтверждения поток размораживаем.

Отдельного разруливания потребует только ситуация, когда мы вместо подтверждения получаем встречный пакет с иными хотелками. Ну просто назначаем одну жабу главной и весь мерж :)

Вот ещё сам придумал ситуацию.

Тред А пишет в ячейку в дуп-области. Ждёт подтверждения от второй машины (ну, или опровержения, потому что там тред Бэ записал туда же другое значение, тогда вместо подтверждающего пакета прилетит пакет на свою запись, не суть; в обоих случаях мы знаем, что «там» ячейка уже записалась, нашим значением или конкурентским, и знаем это значение).

Тем временем тред Цэ прочитал локально наше значение и увидел, что оно новое. Он радостно разрешил треду Дэ на второй машине что-то делать (у них могут быть разные варианты общения помимо жабы) — а он взял и прочитал старое значение, потому что туда пакет ещё не дошёл и она ни сном, ни духом.

Проще говоря, пока мы не синхронизировали перезаписываемую область — мы саспендим не только пишущий поток, но и все прочие попытки обратиться к этому адресу (уточняю: локальные. На том конце — не можем и не нужно, там обычная гонка состояний типа «тред ещё туда не успел записать», если это нормально приложению — нормально и нам).

Ещё проще говоря — дупнутая область у нас получается практически ридонли, потому что допускает только крайне эпизодические моменты записи, сопряжённые каждый раз с целой перепиской сюцая Ху с цензором управления (оба ерундуки). Впрочем, это уже давно стало заметно :)

Расшаренные области будут проблемой, потому что грануляция - 1 страница. А на странице могут быть все глобальные переменные в одной куче (все поля StartUp-класса, что будет аналогом глобалов в Java). А это значит, каждая запись - Page Fault и сетевая синхронизация. Учитывайте ещё работу сборщика мусора, который ходит от глобальных рутов )))

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

Там, где алгоритм расчитан на частые наносекундные задержки (например, управление жизненным циклом через подсчёт ссылок), время будет на 4-6 порядков больше.

Как вы уже отметили, это всё предположении, что в java "не принято" использовать атомики самому, а всё делать через примитивы библиотеки классов. Но на деле может быть что угодно. Программа глючит, падает, мы не знаем почему - ну, наверное, самописная синхронизация, которую наша платформа не поддерживает.

Кроме того, есть куча технических сложностей. Вы считаете, что тривиально определить какая ячейка, какого размера записана/прочитана. На самом же деле, у нас Excepion на конструкции типа

      add [rsi+rbx*8+1000056], rax

а страница, к которой идёт доступ, не замаплена.
Надо дизассемблировать, понять тип доступа (чтение/запись), адрес доступа и размер области. Возможно, операция проходит на границе страниц. Если страницы на этой стороне нет, а данные модифицируются, как в этом примере, скачать страницу по сети, выполнить сложение, и только тогда поймём что писать. Изменённые данные отправить по сети, страницу снова размапить, чтобы следующий доступ к ней снова вызвал Page Fault.

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

синхрить по сети все, абсолютно все мьютексы/семафоры/спинлоки, потому что кто знает, какую область память защищает конкретный мьютекс

А мы же вроде можем посмотреть по коду, где ещё к ним идут обращения, проверки состояний и так далее, а где — не идут? И даже, скорее всего, это уже делаем в ходе JiT-компиляции. Сейчас любая оптимизирующая компиляция старается выбросить всё, что с одной стороны кто-то пишет/заполняет/взводит, а с другой — никто не прочитал/не проверил.

Я в эту сторону немного подумал и, в принципе, решил, что достаточно будет сгруппировать их в две пачки, чтобы они взводились-проверялись в основном только по одну сторону боттлнека, ну а если существуют в коде ситуации, когда есть модификация и/или проверка с обеих сторон — вот тут да, эти надо в особую группу, которая требует каждый чих синхрить через сеть. И желательно её иметь поменьше :)

Надо дизассемблировать, понять тип доступа (чтение/запись), адрес доступа и размер области. Возможно, операция проходит на границе страниц. Если страницы на этой стороне нет, а данные модифицируются, как в этом примере, скачать страницу по сети, выполнить сложение, и только тогда поймём что писать. Изменённые данные отправить по сети, страницу снова размапить, чтобы следующий доступ к ней снова вызвал Page Fault.

Но по сути при оптимизации JiT мы разве не делаем уже что-то подобное? Просто больше нюансов появилось. «Да, ужас. Но не ужас-ужас-ужас!» (© старый анекдот про оооочень уродливого клиента борделя).

Стоп. Получается, что я придумал не расширение JRE, а расширение JiT, по сути.

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

Чёрт, вот это бы на реальном графе чего-нибудь реального проверить, конечно. Насовать в JiT логирования и попробовать разбить на две кучки, посчитать число нужной синхры того, что упорно не разбивается… и учесть при этом, что это только с JiT будет работать, а всё, что не влезло — остаётся только на одной из машин (и его как-то надо уравновесить с).

Труд явно не на мою серую голову, а как минимум на молодого и энергичного Стивера…

Но по сути при оптимизации JiT мы разве не делаем уже что-то подобное?

jit генерит код и забывает границы инструкций. А так придётся для каждой инструкции хранить x10 мета-информации, что она делает, чтобы при Page Fault на ней понять, как это выполнить в распределённой парадигме.

А мы же вроде можем посмотреть по коду, где ещё к ним идут обращения, проверки состояний и так далее, а где — не идут?

Не можем. Например, код

mutex.lock();
DoSomething();
mutex.unlock();

Где функция DoSomething() через 10 уровней вызова трогает 100500 страниц памяти, но автоматически невозможно понять намерение программиста, какие из этих операций чтения/записи он защищает мьютексом, а какие

На том конце — не можем и не нужно, там обычная гонка состояний типа «тред ещё туда не успел записать», если это нормально приложению — нормально и нам)

Похоже на то… ну тогда тем более ещё раз спасибо за анализ!

Посмотрите на модель акторов в Scala и библиотеке Akka. Там в основе всего лежит идея, что есть отдельные акторы, которые обмениваются сообщениями и в идеале больше никак не связаны с остальным кодом.

При этом акторы могут быть (но не обязательно) раскиданы по разным серверам, несколько акторов могут выполнять одну и ту же роль (например, родитель-актор будет раскидывать сообщения детям, а те будут что-то тяжёлое считать (возможно, на разных серверах)

Подход интересный, но очень сильно влияет на архитектуру.

P.S. В чистом виде эта идея реализована в Эрланге: https://ru.wikipedia.org/wiki/Erlang

Ну чисто теоретически можно наверное нагнать несколько сотен суперкрутых инженеров и заделать за пару лет распределенную JDK работающую поверх RDMA но вот только зачем? Кто за это заплатит?

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

Речь не о языках, а о умеренной модификации JRE, не трогая само прикладное ПО (допустим, его некому и не на какие шиши переписывать на других языках/в других парадигмах/с другой структурой).

Если не повезло — остаётся при своих.

Если повезло и нашли что вытащить на соседнюю машину — получили свои 20-30-40% производительности.

Если сказочно повезло — получили почти +100%, то есть почти в два раза. Но «таких слоников не бывает», конечно. Даже хиний-хиний ХИЛИНХ вряд ли прямо вот так уж и состоит из однородных тредов.

Выглядит как изобретение велосипеда.

Если нужно выполнить код на другой машине, есть RPC.

Если нужна внешняя память или кеш, есть миллион специализированных сервисов, типа redis.

Если это попытка сэкономить ресурсы, то лучше просто обновить железо: на современном железе можно не думать о памяти и потоках. Бытовое железо позволяет крутить 32 потока, серверное - больше 256 потоков. Память измеряется десятками или тысячами гигов соответственно. И все это локально, без всяких игр с сетью, с минимальными задержками. Основная сложность на таком железе это максимально его нагрузить, что сделать совсем не просто.

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

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

А вообще на практике успешно работают следующие способы оптимизации:

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

  • размен циклов на память. Например замена вычислений на выборку по смещению в предварительно посчитанной табличке или карте - этот лайфхах еще из докомпьютерной эпохи

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

  • размен циклов на время. Например сжатие больших кешей или отправляемых данных, если они хорошо жмутся: это дает дополнительную нагрузку на cpu, но ускоряет ввод/вывод в 5-10 раз. Жмут современные cpu быстро, хватает на большинство каналов ввода/вывода. Если cpu систематически недогружен и много текстового ввода/вывода, появляется возможность использовать такой размен

Просто комбинируя эти принципы можно ускорить код на 2-3 порядка практически на ровном месте

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

А так, конечно, всё да — загнал тот же XILINX на корпоративный сервак и пусть там себе собирает.

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

И да, априорный скепсис — это не ответ «нет», это сомнения в ответе «да» :) пока чётко гробящей всю концепцию ситуации не нашли (вроде), так что я пришёл с сомнениями и остаюсь с сомнениями :-D

Я был в курсе о кончине основателя Флибусты, об этом много писали.
Но то, что это тот же самый Stiver, с которым я вел переписку насчет Fernflower в 2014 году - это неожиданный поворот. Многогранная была личность. Очень жаль.

Sign up to leave a comment.

Articles