Comments 48
Где-то может понадобиться держать информацию о блокировках […]
Нет. В акторной модели процесс обрабатывает входящие сообщения последовательно; он сам себе блокировка.
И речь, как я понимаю, идёт о том, чтобы расшить ничем логически не обусловленные взаимные завязки.
Ровно наоборот. Процессы при помощи синхронных (блокирующих) сообщений могут общаться между собой точно так, как будто они просто вызывают функции друг друга. Адресация процесса прозрачна для всего кластера. Я уже приводил пример с курсами валют, кажется. Ну вот эти курсы упакованы в процессы и размазаны по кластеру с помощью hashring. Чтобы проверить, годный ли ко мне пришел из стороннего источника курс для покупки оманских реалов при продаже мексиканских песо, я сделаю следующее:
Rates.valid?("OMRMXN", 1.5)
По имени "OMRMXN"
адресуется процесс, отвечающий за этот курс. НИкто, кроме виртуальной машины, не знает, на какой он ноде — это и не нужно. Я отправлю этому процессу сообщение: «проверь валидность». Процесс заглянет в свой стейт и увидит, что три предыдущих значения ± такие же и ответит «всё ок». Если же скачок довольно заметный, этот процесс отправит в свою очередь сообщения процессам "OMRUSD"
и "USDMXN"
, получит от них кросс-курсы, перемножит и сравнит с 1.5
.
Все три вышеупомянутых процесса живут «где-то в кластере» и их идентификаторов достаточно, чтобы к ним обратиться. Ноду знать не нужно.
да даже web-сокеты выросли не из целесообразности сохранения состояния соединения (что хорошо, правильно и часто нужно), а из приклеивания к инстансу сервера
Хм, а как сделать сохранение состояния соединения без приклеивания к инстансу? Если мы пересоединяемся к другому инстансу, то это значит рвем предыдущее соединение и создаем новое, то есть нет никакого сохранения.
что никогда не нужно
Ну вот простой пример: гуглодоки, или там, фигма, где несколько человек, в пределах ста, одновременно работают с одним и тем-же документом и одновременно меняют его. То есть один что-то поменял, нам надо разослать изменения всем остальным. Если все, кто работают с одним документом, подключены через вебсокет к одному и тому-же инстансу, то все происходит в пределах памяти этого инстанса: получили сообщение из одного сокета - разослали его во все остальные сокеты. Если у нас все подключены как попало, к разным инстансам, то нам надо делать pub/sub за пределами этих инстансов, соотвественно все идет через сеть и сетевой трафик увеличивается на порядки. Вопрос: а зачем нам это?
как сделать сохранение состояния соединения без приклеивания к инстансу?
Ну представьте себе, что по идентификатору сессии каждый чих записывается в базу, и источником правды является база. Только тут это процесс с идентификатором.
нам надо делать pub/sub
Какой еще pub/sub? Есть процесс «документ», у него есть список процессов каждого пользователя, там одно сообщение на каждого пользователя, это внутренний трафик в пределах локальной подсети (в случае гуглодоков — возможно, правда, и межконтинетальный иногда, это уже надо по месту смотреть). Это исчезающе малый трафик, зато ремарка «несколько человек, в пределах ста» — перестаёт быть актуальной. Хоть миллион, никакой разницы для приложения. Именно это и называется «горизонтальное масштабирование», но я понимаю, конечно, что оно не всем требуется.
Вышеупомянутым гуглодокам, как раз, требуется, иначе у вас чувак из Вайоминга, подключившийся в документу, созданному барышней из Парижа — будет за каждой нажатой клавишей в европейский датацентр ходить.
Ок, пойдем другим путем, вот у вас процесс «документ», это один инстанс, или документ размазан по куче инстансов? Нужен нам внешний кэш, Redis, или там Memcached, не важно, положить/изменить/забрать данные по одному и тому-же ключу все лезут в один и тот-же инстанс, или разные? В реляционной базе данных вам надо поменять запись, вы пойдете в один и тот-же инстанс базы данных, или разные? (есть, конечно, мультимастер базы данные, но те, что я знаю, они noSQL и даже там наверняка для определенной записи нужен определенный инстанс).
В общем, полно сценариев, когда нам всегда нужен определенный инстанс.
Один документ — один процесс, просто не имеет значения, на какой ноде он запущен.
А редис и мемкэшд — это и вовсе костыли, которые в распределенной версии эрланга просто не нужны.
Оок, допустим у вас банковское приложение, пользователь логинится под своим логином/паролем и получает от сервиса аутентификации токен, который либо будет удален принудительно при закрытии приложения, либо если в течении 10 минут не было запросов в систему по этому токену. Далее, к вам в систему на любую ноду пришел запрос с каким-то токеном, например, узнать текущий баланс пользователя. Если Редис это костыль и sticky sessions вы не пользуетесь, как вы узнаете, что токен валидный и запрос надо обрабатывать?
Ну, этот частный случай - несложный: токен при выдаче вполне можно криптографически подписать, а узел просто проверит эту подпись - всякие Single Sign-On примерно так и работают. И даже домен Windows так работает: клиент (пользователь на ПК) получает у DC билет Kerberos для некоторого сервера, и серверу после этого не требуется ради проверки подлинности пользователя обращаться к DC. Технологии прошлого века, так сказать...
Оно работает только если токен либо бессрочный, либо жестко задана дата/время когда он инвалидируется. Я же изначально писал, что токен должен быть отозван при закрытии приложения, или если в течении 10 минут никаких запросов ни к одной ноде по нему не приходило. Как вы подобное организуете с подписанным токеном?
Я отвечал исключительно на вопрос "как узнать, что токен валидный?".
А начет всего остального - автор статьи, как я понял, рассматривает тут случай, когда все запросы из одного сеанса обрабатываются на назначенном ему узле. И в таком случае любой принявший запрос узел, во-первых, может проверить, что маркер доступа - не поддельный, а во-вторых, если принявший узел сумеет найти этот назначенный узел (что не тривиально, см. мой комментарий к самой статье), то он спроксирует, его на назначенный узел, и тот уже займется вопросом времени жизни маркера доступа на основе своих локальных данных. Осталась мелочь ;-) - найти назначенный узел. Ну, и ещё парочка мелочей, типа, куда распределить запрос из сеанса, который ещё никакому узлу не назначен.
PS В принципе, архитектура, как это сделать в своей библиотеке, у меня в голове вырисовывается (но я над этим немало думал), но автору статьи я подсказки дарить не буду - больно уж у него всё просто, пусть сам осознает, что на самом деле все не настолько просто.
Всё тривиально, да и я в нескольких предыдущих текстах всё это уже показывал. Просто нужно выбирать правильный инструмент. У меня нет привычки скрывать от коллег решения, поэтому можете просто попытаться понять мой код.
Идентификатор процесса (или имя) — прозрачны внутри кластера. Я даже в этом тексте рассказал, как запустить процесс «где-то в кластере», получить обратно его идентификатор.
принявший узел сумеет найти этот назначенный узел
Вот моя библиотека для распределенных конечных автоматов. Чтобы послать сообщение определенному автомату — вообще не нужно знать, на какой именно ноде он запущен, адресация — повторюсь — прозрачна. Идентификатора процесса, или его имени, полученных при старте — достаточно.
куда распределить запрос из сеанса, который ещё никакому узлу не назначен
pid = with nil <- Process.whereis(token),
do: token |> HashRing.node_for() |> Process.launch(token)
Вот вам функция (это псевдокод, идентичный натуральному), которая всегда возвращает корректный идентификатор процесса (и запускает его, если надо). Отсылайте ему сообщение, а всё готово. Как эмулировать «вызов функции» (отослать синхронное сообщение и ждать ответа) — я рассказал вот тут.
я над этим немало думал
Эта проблема была решена Аланом Каем и Джо Армстронгом 40 лет назад, над ней не имеет смысла думать сейчас, разве что серое вещество потренировать если.
Идентификатор процесса (или имя) — прозрачны внутри кластера.
"Но как, Холмс?"(c). Не, технические варианты решения всегда есть, но это - именно варианты решения, с достоинствами и недостатками, а не нечто внутренне присущее реальной вычислительной среде.
Вот вам функция (это псевдокод, идентичный натуральному), которая всегда возвращает корректный идентификатор процесса
С коллизиями что делать будем, а? А с изменением числа узлов?
Эта проблема была решена Аланом Каем и Джо Армстронгом 40 лет назад, над ней не имеет смысла думать сейчас, разве что серое вещество потренировать если.
Нет, над ней есть смысл думать - но не с точки зрения теории ("Где мы?" - "Вы на воздушном шаре"), а с точки зрения, как вписать это в существующий ландшафт проекта, с Redis, RabbitMQ или, не дай бог, Kafka (страшный был писатель, пострашнее Кинга ;-)) - чтобы библиотека практическую ценность имела хотя бы в теории ;-)
Идентификатор процесса — прозрачен внутри всего кластера. Это похоже на единое адресное пространство. Я вызову (в псевдокоде) функцию whereis(token)
и мне вернется назад либо nil
(нет такой сессии), либо pid
(process identifier). Если второе — я просто отправлю ему сообщение «обрабатывай». Если надо — синхронное (то есть вызов будет дожидаться ответа).
Вот, например, мой текст про распределённый брокер сообщений, там это всё реализовано. Или моя библиотека конечных автоматов — каждая стейтмашина живет «где-то в кластере» и адресуется по имени. Вызывающий процесс внутри кластера даже не знает, с какой вообще нодой он разговаривает.
Похоже на лёгкий встроенный прокси.
С малым оверхедом. С back pressure.
Но, видимо, хорошо работает, только пока все ноды видят друг друга (LAN или один регион в облаке). Если соединение между нодами рвётся, все «прокси-вызовы» к этой ноде одновременно упадут с таймаутом
Спасибо за цикл. Такое ощущение, что у вас какой-то внезапный прилив вдохновения.
Кстати, были ли попытки скрестить Erlang с VM/370?
Я просто решил запротоколировать некоторые вещи на русском языке: мне показалось не совсем правильным, что мировое коммьюнити по большей части в курсе, а русскоязычное — нет.
были ли попытки скрестить Erlang с VM/370?
Я о таковых не слышал, но эрланг написан на си, в принципе, наличие компилятора может быть достаточно; но в мейнфреймах все иначе, я просто не уверен, что большинство плюсов останутся осмысленными.
Я о таковых не слышал
Я просто под впечатлением от говнодокера, который не позволяет так просто запустить контейнер внутри контейнера. В отличие от нормальной реализации в VM/370.
Я просто решил запротоколировать некоторые вещи на русском языке
Это очень правильно. Вы применяли Эрланг для обычных десктопных программ? Несетевых, но чтобы вполне себе использовали многоядерность?
Оооо, не будем с утра пораньше про докер, пожалуйста.
Вы применяли Эрланг для обычных десктопных программ?
Я лично нет, и, насколько мне известно, примитивы GUI в этом стеке жиденькие, все-таки оно создавалось мягко говоря совсем не для десктопа. Но с появлением tauri, есть надежда, что кто-то напишет эрланговский бэкенд для него.
В отличие от нормальной реализации в VM/370.
Вы пробовали запускать VM под VM без аппартаного ускорения (например, на ЕС ЭВМ Ряд 2)& Мне - доводилось, для отладки. Ну, для отладки это было замечательно, но вот гонять под этим рабочую нагрузку... В конце концов, параметр V=R в описании VM, со всеми его ограничениями - он не от хорошей жизни появился.
В настоящей распределенной системе между нодами нет разницы: не имеет никакого значения, какая именно нода приняла тот, или иной запрос из внешнего мира.
Это всё хорошо, когда полезный пользователю результат можно вернуть сразу после обработки одного запроса. Но часто веб-приложению этого мало, и для получения полезного для пользователя результатата требуется сеанс, состоящего из передачи несколько запросов и ответов на них плюс сохранение состояния и даже выполнение кода обработки в промежутках между ними - как в приложениях, основанных на ASP.NET Core SignalR, или (понескромничаю) моей библиотеке ActiveSession. И это сохранение состояния сеанса(не всегда но часто), и код, выполняющийся в сеансе(практически всегда), привязывает обработку к конкретному узлу. А ещё бывает, что обработка для определенного пользователя заранее оказывается привязанной к конкретному узлу, где находится БД с его данными: именно так, например работает MS Exchange - мне близкий и хорошо знакомый.
сервер всегда обладает всей необходимой информацией для того, чтобы принять запрос на ноде A, выполнить его на ноде B
Нет. Сервер на узле A, вообще говоря, не знает, что сеанс, в который входит обрабатываемый им запрос, привязан именно к узлу B и передать запрос на разработку надо именно туда. Не кажется ли вам, что нельзя просто взять и вызвать неведомый узел? И что тут должен быть тот самый "ненужный" компонент, который укажет, куда нужно передать этот конкретный запрос, если, как вы предлагаете использовать проксирование. Ну, или сам передаст, как, к примеру, брокер сообщений.
PS Ну, а микросервисы и саги - они вообще про другое.
Проблема синхронизации, конечно, в общем случае существует. Где-то может понадобиться держать информацию о блокировках, и это потенциально может стать узким местом (как в случае DB2 на мейнфреймах, где часто наиболее нагружен именно блокировочный узел). Но это актуально только для нагруженного OLTP, вроде очереди в винном магазине в годы перестройки, которую более романтичные авторы представляют оторванной от жизни метафорой философов с двумя вилками за столом со спагетти. А многие процессы реально асинхронны друг от друга. В том же воцапе нам с вами логически нечего делить, если мы не общаемся друг с другом. И речь, как я понимаю, идёт о том, чтобы расшить ничем логически не обусловленные взаимные завязки.
Где-то может понадобиться держать информацию о блокировках […]
Нет. В акторной модели процесс обрабатывает входящие сообщения последовательно; он сам себе блокировка.
И речь, как я понимаю, идёт о том, чтобы расшить ничем логически не обусловленные взаимные завязки.
Ровно наоборот. Процессы при помощи синхронных (блокирующих) сообщений могут общаться между собой точно так, как будто они просто вызывают функции друг друга. Адресация процесса прозрачна для всего кластера. Я уже приводил пример с курсами валют, кажется. Ну вот эти курсы упакованы в процессы и размазаны по кластеру с помощью hashring. Чтобы проверить, годный ли ко мне пришел из стороннего источника курс для покупки оманских реалов при продаже мексиканских песо, я сделаю следующее:
Rates.valid?("OMRMXN", 1.5)
По имени "OMRMXN"
адресуется процесс, отвечающий за этот курс. НИкто, кроме виртуальной машины, не знает, на какой он ноде — это и не нужно. Я отправлю этому процессу сообщение: «проверь валидность». Процесс заглянет в свой стейт и увидит, что три предыдущих значения ± такие же и ответит «всё ок». Если же скачок довольно заметный, этот процесс отправит в свою очередь сообщения процессам "OMRUSD"
и "USDMXN"
, получит от них кросс-курсы, перемножит и сравнит с 1.5
.
Все три вышеупомянутых процесса живут «где-то в кластере» и их идентификаторов достаточно, чтобы к ним обратиться. Ноду знать не нужно.
На двусторонних взаимодействиях эффективно во всех случаях всё не сделать. В DB2, помнится, было 28 типов блокировок, в зависимости от уровня изоляции (то есть допустимых феноменов) для каждой транзакции и их взаимного многостороннего взаимодействия (приводящего к эскалации блокировок).
С курсами валют в вашем примере всё просто, они распространяются в одну сторону, от поставщика к потребителю, и ничем не обусловлены. А вот когда возникнет логическая драка за ресурсы, то при недостаточно гранулированных блокировках это всё быстро приведёт к дедлоку, как у тех типов с вилками. Скажем, если у вас биржа с миллионом продавцов и покупателей валют, и у каждого своя цена и количество в заявке, то так разрулить, как вы описываете, не получится. При этом биржа - довольно нетребовательный на самом деле вариант, потому что заявки можно упорядочить и удовлетворять в порядке поступления, не парясь над максимизацией суммарного оборота. Но уже там возникнет вопрос, кому продать, когда пришли две заявки на покупку, а валюты мало. Поэтому на бирже есть стакан, который и является общим хранилищем блокировок. Если заявка Педро на покупку песо первой попала в стакан биржи, то она должна быть первой удовлетворена, хотя заявка Хуана могла бы прийти к продавцу раньше.
это всё быстро приведёт к дедлоку, как у тех типов с вилками
Нет. Я не смогу, наверное, показать это на пальцах, но я решал ровно такую задачу с вилками. Да, есть очередь заявок, это тоже процессы (на каждую пару валют в тривиальном случае, гораздо сложнее, если мы хотим вовлекать несколько трейдеров в транзакцию, но это тоже решаемо). Последовательность обеспечивается из коробки устройством mailbox’а процессов эрланга: пока процесс не обработает предыдущее сообщение, ничего не происходит.
В асинхронной модели акторов дедлоков не бывает, кроме очень простых ошибок начинающих программистов (типа, синхронно вызвать из обработчика сообщения самого себя).
Тут даже база не нужна, как источник данных, только как резервный бэкап после сбоя.
Да, есть очередь заявок, это тоже процессы
Так вот эти процессы могут быть самыми сложными и критичными в определённых случаях.
Критичными — да, сложными — нет.
Но проблема критичных процессов давно решена; пришло сообщение «куплю» — нашел в соственном стейте подходящее «продаю», заблокировал оба и запустил сторонний процесс. Если он выполнился с успехом — вычеркнул заблокированное, если нет — разблокировал.
Предвосхищаю вопрос о «заявка Педро на покупку песо первой попала в стакан биржи, то она должна быть первой удовлетворена»: что делать, если контрагент Педро оказался мошенником, и надо откатывать, но так, чтобы Хуану не досталась следующая заявка, а пошла снова Педро. Зависит от бизнеса уже. Наша очередь обслуживает только пары с покупкой песо, поэтому если нужно удовлетворить Педро любой ценой — ну блокируем всю очередь, тут нет же вариантов, неважно, на чем это реализовывать.
Гранулярность в данном случае достигается тем, какие именно валюты/суммы обслуживает процесс.
И это сохранение состояния сеанса(не всегда но часто), и код, выполняющийся в сеансе(практически всегда), привязывает обработку к конкретному узлу.
Нет. Если бы вы попробовали понять, о чем я тут последние две недели пишу, а не бросались сразу спорить, — вы бы это уже знали.
Нет.
Ну, тогда вы решаете более простую задачу, чем приходится решать многим разработчикам. Например - тем, кто в своем решении задействует чат на SignalR. И, подозреваю, компоненты Blazor - тоже (Blazor - он на SignalR базируется).
Если бы вы попробовали понять, о чем я тут последние две недели пишу
Если бы вы попробовали писать для людей более простых и привяханных мыслью к решению практических задач...
Ну, для начала, если бы вы выбрали для своих примеров менее экзотический язык...
А в имеющемся раскладе я ограничиваюсь всего лишь указанием на те слабые места в ваших рассуждениях которые вижу - без понимания всех глубин вашей концепции.
В предыдущем комментарии - как раз про конкретное слабое место, подробности - в комментарии ранее.
А нет, кроме эрланга, другого языка, в котором эти задачи решены из коробки.
В джаве есть суррогат — Akka, её потом даже в дотнет втащили.
А нет, кроме эрланга, другого языка, в котором эти задачи решены из коробки.
Решены-то они как, магически?
Или за этим, как обычно, стоит какая-то инфраструктура.
Бывают в IT такие вабстракции, которые выглядят красиво, но реализуются сложно, прямо по формуле Боромира: "нельзя просто так взять и ...". Классический пример - ассоциативная память: она - либо небольшой объем специальной дорогой памяти, в которой это сделано схемотехнически - типа как TCAM в Catalyst'ах, либо эмулируется на обычной дешевой памяти чем-нибудь, типа хэш-таблиц, т.е. некой нетривиальной инфраструктурой.
Так вот, про эту вашу инфраструктуру большинство ваших читателей не знают, а в магию они не верят, потому что уже большие. И если вы им не раскроете секрет этой магии, то ваши статьи будут восприниматься так, как они воспринимаются.
статьи будут восприниматься так, как они воспринимаются
А как именно они воспринимаются? Основной цели я, например, добился: мотивировал некоторых людей поиграться, иных целей я перед собой и не ставил.
Вы меня путаете, мне кажется, с евангелистами и инфоцыганами. Erlang Solutions мне не платит, документации по азам — вагон и маленькая тележка. Я рассказываю только про то, что сделал сам, своими руками (и иногда — даже головой). Тексты «пережеванная манная каша из документации» — не моя ипостась, простите уж. Мне фиолетово, как мои тексты принимают en masse, я не стодолларовая купюра, а уж на этом форуме — минусы давно говорят о человеке в среднем лучше, чем плюсы.
Хотя, я может как-нибудь и напишу «Как вкатиться в эрланг за 7 дней», если будет не лень.
Не понятно. Например есть 1 прокси, который перестраделяет запросы по одинаковым серверам, для которых нет разницы, какой запрос выполнять (сессия в БД) - это распереленная система, по вашему? (Аналогично в websocket, но надо чтобы прокси связывал сокет прокси с ответом от сервера за прокси).
Но такой подход, когда есть независимые и одинаковые ноды, которые могут выполнить любой запрос, и могут быть остановленны в момент, когда нет в ним запросов (или их прокси не отпрааляет), не дают возможность, быть в процессе (сохранять в памяти информацию процесса), предполагает, что есть внешниее хранилище данных (которое есть узкое место системы?).
Короче не понятно. Вроде одно и тоже, но в реалях конкретной технологии.
Один прокси и множество серверов за ним, готовых выполнить запрос - это не распределённая система?
Привязка сессий необходима для предотвращения временных аномалий. От того, что вы вкорячили самописный прокси в каждый узел, привязка сессии к одному единственному узлу, на котором запущен процесс, никуда не делась. А только добавилась дополнительная нагрузка по проксированию запроса, которой можно было бы избежать через клиентскую балансировку. Или хотя бы через вынос проксирования на отдельные узлы, расширив тем самым бутылочное горлышко вашего выделенного процессингового узла.
Касательно же горизонтального масштабирования, под этим термином обычно понимают линейную зависимость продуктивности системы от числа обработчиков. От того, что перед единственным узлом с процессом отвечающим за документ вы влепите 100500 "масштабируемых" прокси - горизонтальное масштабирование у вас не появится.
От того, что перед единственным узлом с процессом отвечающим за документ вы влепите 100500 «масштабируемых» прокси — горизонтальное масштабирование у вас не появится.
Угу, я об этом в первой строке текста написал.
вы вкорячили самописный прокси в каждый узел
Поскольку я ничего такого не делал, дальнейшие выводы бессмысленны.
Конечно же это никакой не прокси:
def request(params) do
GenServer.call({Handler, :"n2@am-victus"}, {:handle_request, params})
end
Вы серьёзно думаете, что сможете кого-то так обмануть? Хоть бы замаскировали это за очередной библиотекой.
Я серьёзно думаю, что не имеет смысла дискутировать с человеком, который пример вызова процесса на другой ноде, который можно воспроизвести копипастой у себя дома — не в состоянии отличить от рабочего кода.
Идентификатор процесса в OTP — прозрачен для всех нод кластера. Я показал, как вызвать код на определенной ноде. В общем случае, знать, на какой ноде находится вызываемый процесс не требуется.
Я настоятельно рекомендую вам перестать открывать рот там, где вы ни слова не понимаете. Идите вон на D подрочите лучше лишний раз.
Я не спец по эрлангу но гдето потерялся код для регистрации обработчиков которые живут на других нодах а без этого в любом языке можно написать call("функция", "параметры") - ой это же rpc)
Распределенные системы и горизонтальное масштабирование