Русский
Русский
English
Статистика
Реклама

Распределенные системы

Как ускорить аутентификацию и снизить потребление памяти в 5 раз? Наймите дворецкого

07.05.2021 12:20:51 | Автор: admin

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

Меня зовут Роман Литвинов, я разработчик в команде Учи.ру. Хочу вам рассказать именно о такой истории из практики и о нашем сервисе под названием Butler, о дворецком, через которого проходит каждый пользователь, прежде чем зайти на платформу.

Итак, овсянка, сэр.

По мере роста популярности платформы система аутентификации перестала соответствовать нашим требованиям, как и некоторые другие части нашей архитектуры. Когда весной 2020 года в базе Учи.ру стало больше 11 млн активных пользователей (8 млн учеников, примерно 350 тыс. учителей и около 3,5 млн родителей), существующая реализация стала медленной, непрозрачной и потребляла слишком много памяти. Мы решили ее обновить.

Поставили перед собой следующие цели:

  • повысить производительность;

  • внедрить новые технологии защиты учетных данных;

  • выделить аутентификацию из монолита в микросервис;

  • подключить мобильное приложение.

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

В список новых функций попали:

  • принудительное отключение пользователя;

  • блокировка аккаунта;

  • верификация;

  • шифрование паролей учеников;

  • поддержка jwt-токенов и двухфакторной аутентификации;

  • интеграция с социальными сетями;

  • обеспечение единой точки входа для множества сервисов с защитой от брутинга.

Сложности работы с таблицами. Роли пользователей

Долгая аутентификация

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

Проблема уникальности почты и логина

Если родитель одновременно является учителем и хочет зарегистрироваться на Учи.ру, он должен завести две учетные записи. Это значит, что его e-mail будет числиться в двух разных таблицах. Та же история может происходить, если один человек является и сотрудником, и родителем или и сотрудником, и учителем и т. д. Если e-mail один, то сложно определить, какого именно пользователя нужно аутентифицировать.

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

Теперь мы используем Butler

Чтобы исправить ситуацию, мы решили вынести аутентификацию в отдельный сервис. Для этого был создан Butler. Саму аутентификацию было решено проводить на базе открытой библиотеки Ruby под названием rodauth. Ее, конечно, пришлось немного доработать, но в целом решение подходило. Параллельно с улучшением своего продукта мы внесли небольшой вклад в развитие open source-сообщества.

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

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

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

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

Небольшой тюнинг решения

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

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

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

Обновление камень преткновения

Обновлять исходный код open-source компонента нужно аккуратно, потому что в открытых проектах вопросы обратной совместимости не всегда оказываются решены полностью. Так, нам нужно было установить обновление, которое требовалось для внедрения очередной фичи. Но с этим обновлением возникал обязательный параметр связи между access-токеном и refresh-токеном. Раньше он был необязательным и от нашего внимания ускользнул. Это значит, что все выданные токены перестали бы работать разом, если бы мы накатили это обновление. То есть, если бы мы сразу обновили всю систему, это привело бы к сбросу всех активных сессий.

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

Криптография и защита от брутинга

Хранить логины и пароли в новой базе данных было решено в зашифрованном виде. Тогда дополнительным тормозом стал один из наиболее используемых алгоритмов в экосистеме Ruby BCrypt.

Его реализация на Ruby отнимала слишком много процессорного времени, затрачивая 250 мс на создание хеша со стандартным костом, равным 12. Зачастую подобную операцию необходимо проводить дважды в течение цикла вопрос-ответ, поскольку требуется проверка на использование этого же пароля пользователем прежде. Вкупе со множественным использованием колбеков в монолите ситуация стала приводить к большому времени ожидания внутри транзакций БД (idle in transaction), что начало сказываться на производительности системы. На этих самых колбеках висело слишком много бизнес-логики, что не позволило свести проблему к простому рефакторингу.

Выходом из положения стала смена алгоритма шифрования на более гибкий, который поддерживает конфигурацию как по затрачиваемой памяти, так и временному ресурсу. Мы взяли открытый алгоритм Argon2 и постепенно мигрировали пользователей с одного на второй, благо в rodauth имелись зачатки функционала для осуществления плавного перехода. Для этого потребовалось внести изменения в саму библиотеку rodauth. К счастью, Jeremy Evans ее автор оказался очень открытым человеком, пошел нам навстречу.

В сервисе rodauth также нашлось встроенное решение по защите от брутинга паролей. После определенного количества попыток аккаунт блокируется на некоторое время. Разблокировку можно провести за счет подтверждения через e-mail или вручную.

Перенос аккаунтов

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

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

Важным пунктом миграции стала необходимость не завершать активные сессии пользователей и не менять данные их учетных записей, то есть пароли нельзя было сбрасывать. К счастью, зная salt и cost-factor, можно просто перенести хеш без потери функциональности.

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

Результаты

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

Главный плюс же в том, что мы получили внимательного дворецкого: полноценный быстрый и безопасный сервис аутентификации с единой точкой входа. При этом он так же работает на базе Ruby, как и основная часть системы Учи.ру. Теперь процессы аутентификации происходят быстрее в десятки раз, а памяти нужно на это примерно в 5 раз меньше, чем раньше.

Подробнее..

Честное онлайн-голосование миф или реальность?

27.05.2021 22:07:59 | Автор: admin

Привет, Хабр! Меня зовут Иван, я разрабатываю сервис онлайн-голосований WE.Vote на основе блокчейн-платформы Waves Enterprise. Сама идея голосований в онлайне уже давным-давно реализована разными компаниями, но в любых кейсах повышенной ответственности все равно прибегают к старой доброй бумаге. Давайте посмотрим, как электронное голосование сможет посостязаться с ней в максимально строгих условиях.

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

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

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

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

Но что если вам нужно больше?Что если одновременно нужна экономическая эффективность, конфиденциальность принимаемых решений и максимальная теоретическая невозможность для оператора сервиса онлайн-голосований повлиять на их результат? Представьте гипотетическую ситуацию, в которой одна глобальная корпорация Кэппл принимает решения о своей глобальной стратегии развития на инфраструктуре другой глобальной корпорации Тубель.Сколько бы соглашений и договоров нибыло подписано удержится ли Тубель от возможности подсмотреть, о чем там договаривается его прямой или косвенный конкурент? Будет ли менеджмент Кэппла спать спокойно, будучи уверенным, что никто не вмешается в их прорывные планы развития?

Стоит ли, положившись на идеальность нашего мира, отвечать на оба вопроса да? Или мы можем создать техническое решение, отвечающее всем требованиям?Давайте посмотрим.

Чего мы хотим добиться

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

  1. Организатор голосования создает повестку и хочет управлять правилами доступа к этой повестке: сделать ли ее публичной или раскрыть только участникам голосования.

  2. Организатор голосования создает список его участников и хочет быть уверенным, что в его голосовании примут участие именно они. Сами участники также заинтересованы в этом, так как подразумевается, что они принимают решение по важному для них вопросу и мнение/намерения/выгода третьих лиц не должны на это решение повлиять.

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

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

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

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

При чем тут блокчейн

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

Распределенное хранение

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

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

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

  • повестка и материалы голосования;

  • контактные данные пользователей идентификатор пользователей в реальном мире (e-mail или номер телефона);

  • персональные данные пользователей в целом, в системе не требуются, но делают работу удобнее как для организатора голосования, так и для самих участников;

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

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

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

Пара ключей создается пользователем локально, на его персональном устройстве. Приватный ключ этого устройства не покидает, а публичный сохраняется набэкендекак параметр учетной записи. Организатор голосования работает со списком участников в виде Ф.И.О. и e-mail. При сохранении данных голосования в блокчейне туда же уходит список публичных ключей. Голос подписан ключом пользователя, и если публичный ключ отправителя есть в списке участников, мы принимаем бюллетень. Такая схема позволяет,с одной стороны,не светить персональными данными пользователей, а с другой сделать более прозрачной работу системы.

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

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

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

Подводя промежуточный итог, можно сказать,что:

  • Мы решили проблему прозрачности и immutable-хранения исторических данных, используя блокчейн.

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

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

Но, несмотря на то что мы обещали обойтись без магии, она все-таки произошла. Для шифрования бюллетеней нам понадобился открытый ключ голосования, но никто не сказал, откуда он взялся! Очевидно, что это предельно важная часть всего процесса голосования и к нему нельзя отнестись легкомысленно. Еще более интересным кусочком пазла является приватный ключ, соответствующий открытому ключу голосования, так как именно с его помощью мы сможем получить итоги голосования.Настал момент шагнуть в область криптографии (которая для 99.9% людей не сильно отличается от магии).

Криптография

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

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

Несколько болеенадежным вариантом будет техника разделения приватного ключа после генерации хорошо известная схема разделения секрета Шамира. Ключевая пара создается, публичный ключ сохраняется в блокчейне как открытый ключ голосования, а приватный ключ разделяется на несколько частей, которые независимо хранятся доверенными участниками. Чтобы подвести итоги голосования, приватный ключ необходимо собрать и после этого расшифровать бюллетени. Если кто-то из доверенных участников заболел, схема Шамира предполагает возможность сбора приватного ключа меньшим количеством участников. То есть если ключ был разбит на N частей, собрать обратно его можно, используя K частей, где K < N.

Такой вариант выглядит гораздо более надежным и продвинутым, и это действительно так. Но есть нюансы. Во-первых, в промежуток времени между генерацией ключа и его разделением он является очевидной мишенью для злоумышленников и единой точкой отказа системы. Во-вторых, после сбора ключа мы получаем возможность расшифровать каждый индивидуальный бюллетень. Это не позволит нам их отфильтровать, так как они уже находятся в блокчейне и никуда оттуда не денутся. Но это позволит нарушить конфиденциальность голосования. На бэкенде есть связка Ф.И.О. и публичного ключа участника голосования, а в блокчейне связка публичного ключа и теперь уже расшифрованного бюллетеня. Мы знаем, как и кто проголосовал можем взять и, например, лишить премии неугодных.

Конечно, существуют механизмы разрыва первой связки персональных данных и открытого ключа через технику слепой подписи, но это очень своеобразный механизм, который необходимо правильно внедрить. При этом всё равно может сохраниться возможность вычислить по IP голосующего. Он приходит на авторизованный метод получать слепую подпись, а потом стучится на неавторизованный метод отправить голос. Формально во втором случае мы не знаем, кто именно к нам пришел, и опираемся только на проверку слепой подписи. Но у нас есть возможность сопоставить параметры устройства/браузера/соединения и понять, что это тот самый Иванов, который 5 минут назад получал у нас слепую подпись. Или представим похожую атаку на сопоставление по времени получения подписи и отправки голоса. Когда голосующие идут толпой по 500 человек в секунду, такая атака теряет свою эффективность, но при меньшей нагрузке вполне себе работает.

Попробуем сделать лучше?

Распределенная генерация ключа

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

Для формирования общегооткрытогоключа голосования (MainPubliсKey) используется алгоритм DKG (distributed key generation) из статьи TorbenPrydsPedersenA threshold cryptosystem without a trusted party,перенесенный на эллиптические кривые (в оригинальной статье используется мультипликативная группа конечного поля (поля Галуа)GF(p)). При этом есть ограничение:при любой жалобе (не сходится контрольная сумма) одного из участников на другого необходимо перезапустить процесс генерации с самого начала.

В нашей текущей реализации DKG используются стандартные эллиптические кривые seсp256k1 (Bitcoin, Ethereum) и функция хеширования SHA-256. Можно легко добавить, например, Ed25519 или даже российские кривые ТК-26 и хеш Стрибог, если потребуется. Также можно не завязываться на 256-битных кривых, а использовать 512-битные.

Участниками DKG у нас будут несколько экземпляров криптографических сервисов, выполняющих всю криптографическую магию. Чтобы не возникало вопросов о возможности непрозрачного общения между сервисами, единственным их интерфейсом будет взаимодействие с блокчейн-нодой. Один декрипт одна нода. Принципиально такое соотношение 1 к 1 не требуется, декриптов может быть больше или меньше чем нод, но выглядит логичным иметь одинаковый уровень децентрализации как на слое хранения/транспорта данных (см. раздел выше), так и на слое криптографии.

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

Протокол DKG Pedersen 91 на эллиптических кривых

Параметры протокола:

  1. Эллиптическая кривая E и генератор (Base) подгруппы этой кривой большого простого порядка q.

  2. Другой генератор (BaseCommit) той же подгруппы, для которого число x из соотношения BaseCommit = x * Base неизвестно никому.

  3. (k, n), гдеnобщее число развернутых криптографических сервисов (DecryptService), сгенерировавших пары ключей, аkминимальное число сервисов, которое необходимо для восстановления общего секрета. k <= (n+1)/2, то есть еслиk - 1участниковнечестные или у них украли ключи, то это никак не повлияет на безопасность общего секрета (MainPubliсKey).

Шаг 0. Индексы DecryptService

Каждому изnDecryptServiceприсваивается уникальный порядковый номер от 1 доn. Это нужно, потому что от порядкового номераDecryptServiceзависит коэффициент Лагранжа, который потребуется для реализации схемы K из N.

Шаг 1. Создание открытого ключа голосования

Каждый изnDecryptServiceгенерирует пару публичного (Pub_n)и приватного (priv_n) ключей для эллиптической кривой: j-йсервер генерирует пару ключей:priv_j,Pub_j,гдеPub_j = priv_j * Base(точка Base генератор простой подгруппы). И делает Pedersen commitment для публичного ключа:

  1. Генерируется случайное число, скалярr_j.

  2. Вычисляется точка, коммитС_j = r * BaseCommit + Pub_j.

  3. С_jпубликуется в блокчейн.

После того как каждый изnDecryptServiceопубликовал свой коммит ПедерсенаС_j, каждый DecryptService публикует свой скалярr_j. На основе опубликованных в блокчейне скаляров любой сторонний наблюдатель может восстановить публичные ключи каждого DecryptService Pub_j =С_j -r * BaseCommit а затем вычислить общий публичный ключ Pub (MainPublicKey) как сумму отдельныхPub_j.

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

Шаг 2. Генерация полиномов и раздача теней

Каждыйj-йDecryptService случайным образом:

  • Генерирует полином степениk - 1:f_j(x) = f_j_0 + f_j_1*x + ... + f_j_k-1* x^(k-1), где коэффициентf_j0 = priv_j, а остальные случайные элементы поляGF(q), гдеq порядок подгруппы точек.

  • Считает значения полинома для каждогоi-гоизnзначений:f_j(i) = f_j_0+ f_j_1*i + ... + f_j_k-1* i^(k-1). Значениеf_j(i)называется тенью (shadow).

  • Шифруетf_j(i)при помощиPub_iдля всех других серверов и публикует результаты шифрования. Таким образом,значениеf_j(i)может узнать только владелецpriv_i, т.е. DecryptService номерi.

Шаг 3. Проверка коэффициентов полиномов

Чтобы убедиться, что каждый из DecryptService следует протоколу DKG, они проверяют значения теней, полученных друг от друга. Каждый DecryptServiceпубликует каждый коэффициент своего полинома, умноженного на генератор Base: j-й сервер:fj,0* Base, fj,1* Base, ... , fj,k-1* Base, где fj,k-1 это коэффициент при степениk - 1.

После этого каждыйi-йDecryptServiceпроверяет все расшифрованные тениf_j(i)(гдеjиз множества от 1 доn, исключаяi), которые для него зашифровали другиеn - 1участников DKG. i-йDecryptServiceдля тени от сервераj:

  1. Вычисляетf_j(i) * Base

  2. Берет экспоненты его коэффициентов:fj,0* Base, fj.1* Base, ... , fj,k-1* Base

  3. Домножает каждый на соответствующую степеньi:fj,0* Base, i * ( fj,1* Base), ... , i^(k-1) * ( fj,k-1* Base)

  4. Складывает их.

Если результат сложения равенf_j(i) * Base(тень отjдляi, умноженная на генератор), то результат принимается. В противном случае публикуется жалоба на серверj: значение тениf_j(i), и протокол запускается с самого начала шага 0.

Если ни у кого нет жалоб, то каждый сервер вычисляет свой секретный ключs_iкак сумму значенийf_j(i)от всехjсерверов, включая себя.

Если взять любые изkучастников, то сложив ихs_i * Lagrange(k, i), где Lagrange(k, i) коэффициент Лагранжа, который зависит от номеров из выбранной группы (k) и номераi, мы получим приватный ключ, соответствующий общему ключу Pub (MainPublicKey), то есть по сути сумму всехpriv_i.

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

Мы решили проблему угона приватного ключа до начала голосования. Он не был создан централизовано, и чтобы получить доступ к данным голосования, злоумышленнику необходимо взломать K серверов, а не один. Но остается вопрос: как мы будем проводить расшифровывание. Будем ли мы собирать приватный ключ в этот момент и дадим ли шанс злоумышленнику? Пожалуй, не дадим.

Шаг 4. Распределенное дешифрование

Допустим, мы зашифровываем сообщение M на открытом ключе голосования (MainPublicKey):

  1. Генерируем число r и считаем R = r * Base.

  2. ВычисляемС = M + r *MainPublicKey.

  3. Получившийся шифротекст пара точек (R, C) мы публикуем в блокчейне.

  4. Владелец приватного ключаprivвычисляет значение:priv * R.

  5. И расшифровываетM:M = С -priv * R.

Таким образом, для расшифровывания (R, C) нужно вычислитьpriv * R.

Если наш приватный ключ распределен (допустим, что (k, n) = (3,6)), каждый криптографический сервис независимо считает значениеs_i * R, используя свою часть приватного ключа, и публикует результат в блокчейне. Назовем это значение частичной расшифровкой. Дальше остается домножить любые 3 из 6 результатовs_i * Rна соответствующий коэффициент Лагранжа, сложить три точки и получить priv * R. А используя это значение, мы расшифровываем сообщение М.

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

Гомоморфное шифрование

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

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

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

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

Бюллетень в виде матрицы вопросов и вариантов ответовБюллетень в виде матрицы вопросов и вариантов ответов

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

Подсчет результатов в зашифрованном видеПодсчет результатов в зашифрованном виде

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

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

Зашифрованное сообщение 1: ( R1, С1 ) =( r1 * Base,M1 + r1 *MainPublicKey)

Зашифрованное сообщение 2: ( R2, С2 ) =( r2 * Base,M2 + r2 *MainPublicKey)

Их сумма: ( R1 + R2, C1 + C2 ) = ( ( r1+r2 ) * Base, M1 + M2 + ( r1 + r2 ) *MainPublicKey)

Сумму расшифровываем так же, как отдельные сообщения (помним чтоMainPublicKey= priv * Base):

( M1 + M2 ) = ( C1 + C2 ) priv * ( R1 + R2 ) = M1 + M2 + ( r1 + r2 ) *MainPublicKey priv * ( r1 + r2 ) * Base = M1 + M2

Кто-то скажет магия, кто-то возразит математика.

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

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

Доказательства с нулевым разглашением (ZKP Zero Knowledge Proofs)

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

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

Одна из самых наглядных демонстраций работы ZKP (интерактивной разновидности) это Пещера Али-Бабы или Лабиринт:

Участник A обладает ключом, открывающим дверь в лабиринте, и хочет доказать это участнику B, но не хочет показывать ключ. Чтобы В мог убедиться в верности утверждения А, они организуют серию испытаний:

  1. А заходит в лабиринт пока В отвернулся. В не знает, в какую сторону пошел А.

  2. В дает А указание выйти с какой-либо стороны, например, слева.

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

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

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

ZKP на бюллетене

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

При желании (а оно у нас есть) мы можем на базе этой схемы ZKP реализовать более сложные схемы голосований. Например, взвешенное голосование, где каждый участник отдает не один голос, а количество голосов, пропорциональное своей доле акций компании. Для этого мы должны вместо 1 создать ZKP для значения веса голоса участника. Или вариант голосования с множественным выбором, где каждый голосующий может выбрать не один вариант из N, а несколько. Для этого мы по каждой ячейке добавляем ZKP для ряда значений [0, 1, 2, 3]. Суммарный ZKP может быть на значение [3] тогда голосующий должен распределить все свои голоса. Или на ряд значений[1, 2, 3] то есть он может выбрать от 1 до 3 вариантов, но не может не ответить на вопрос.

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

Структура зашифрованного бюллетеня выглядит следующим образом:

(R_1, C_1), Proof_1,

.........................

(R_M, C_M), Proof_M,

Sum_Proof

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

ZKP на частичных расшифровках

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

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

Второе условие: если расшифровывание нескладывается,и мы подозреваем, что некоторые криптографические сервисы решили саботировать голосование, неплохо бы иметь возможность проверить, какой именно из сервисов сбоит. Для этого при публикации частичных расшифровок каждый криптосервис создает и прикладывает ZK-доказательство расшифровки, используя алгоритмZKP Chaum-Pedersen, который доказывает знание числа x для двух соотношений A = x * B и C = x * D (где A B, C, D точки на одной кривой).

Теперь у любого квалифицированного стороннего наблюдателя есть возможность:

  • самостоятельно провести гомоморфное сложение валидных бюллетеней и получить итоговый бюллетень;

  • проверить доказательства расшифровки этого суммарного бюллетеня от каждого криптографического сервиса;

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

Для удобства последний шаг мы проведем сами и зафиксируем итоги голосования в блокчейне как массива массивов вида [ [ 2,5,6 ], [ 3,5,5 ], [ 7,6 ], [ 10,3 ] ].

Смарт-контракты

Фух, кажется, мы описали протокол голосования, который, используя распределенные технологии и криптографию, позволяет дать честным участникам и организаторам голосований все необходимые гарантии и защищает процесс коллективного принятия решений от внутреннего и внешнего нарушителя. Очевидно, при условии правильной децентрализации распределении узлов системы между независимыми участниками; без этого мы получаем замороченные Google Forms :)

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

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

А что дальше?

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

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

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

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

Подробнее..

Musiphone децентрализованный музыкальный плеер

22.04.2021 16:18:43 | Автор: admin


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


Свой рассказ я бы хотел поделить на две части:


1. Плеер изнутри (musiphone, museria-player)


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


const Node = require('musiphone').Node;(async () => {try {const node = new Node({port: 4000,hostname: 'localhost',musicStorageAddress: 'storage.museria.com:80'});await node.init();}catch(err) {console.error(err.stack);process.exit(1);}})();

const Client = require('musiphone').Client;(async () => {try {const client = new Client({address: 'localhost:4000'});await client.init();const title = 'Playlist title';const songs = ['Onycs - Eden','Onycs - Shine','Onycs - Timeless'];// Add the playlistconst response = await client.addPlaylist(title, songs);// Get the playlistconst playlist = await client.getPlaylist(response.hash);}catch(err) {console.error(err.stack);process.exit(1);}})();

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


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


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


2. Плеер извне (сайт, android приложение)


Снаружи плеер это сайт и приложение для андроид (честно скажу, мало на каких версиях и устройствах тестировал). Несмотря на не очень большую ныне популярность и очевидные проблемы, выбор пал на cordova, поскольку она удовлетворяет основным требованиям к проекту и сильно упрощает и ускоряет разработку.


Интерфейс везде примерно одинаковый, поэтому разберем все на примере сайта.


Создание и сохранение плейлиста в сеть.



Вначале вы попадаете на интерфейс с бобром и неактивной кнопкой "NEW PLAYLIST". Это означает, что сейчас идет создание нового плейлиста. Чтобы добавить песню, нужно найти ее в музыкальном хранилище, используя поле ввода слева. Если нужной песни там нет, то вы можете сами добавить ее, перейдя по ссылке "MUSIC STORAGE" сверху, тем самым вы поможете и себе и другим людям, которые в дальнейшем будут ее искать.


Для примеров будут использоваться рандомные песни, разрешенные для свободного распространения и прослушивания. Давайте поищем "Onycs Eden"



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



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


Попробуем сначала сохранить все в сеть. Для этого нажимаем "SAVE TO WEB".



Ввели название и сохраняем.



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


Также мы видим, что исчезло коричневое предупреждение и появилось синее табло, в котором у нас ссылка на плейлист. Вы можете дать эту ссылку любому человеку, перейдя по ней, он увидит тоже самое. Создадим еще один плейлист. Для этого нажимаем "NEW PLAYLIST". Повторяем те же шаги и получаем:



Видим уже два плейлиста, а также сам плеер, который появился после нажатия на блок с песней.


Сохранение плейлистов в файл
Для большей надежности вы можете сохранять плейлисты в файлы. Для этого нажимаем "SAVE TO FILE". Файл будет сохранен в общепринятом формате m3u и может быть загружен и прослушан в любом другом плеере.


Загрузка плейлистов
Чтобы загрузить плейлист, нажимаем "LOAD PlAYLIST".



В модальном окне у нас есть два варианта. Мы либо загружаем по ссылке, либо из файла. В первом случае существует два типа ссылок:


  • Статическая ссылка. Это обычная ссылка с хэшем на плейлист в хранилище: http://player.museria.com:80/musiphone/3deeb6052c5a46c05d6bec2cab5bade9 Напрашивается вопрос, зачем ее грузить через форму, если можно просто по ней перейти. Дело в том, что во-первых это нужно для мобильной версии, а во вторых узлы относительно которых создается ссылка рандомные. Это может быть неудобно когда вы уже настроили свое окружение на каком-то хосте, ведь вся временная информация хранится в localStorage. Поэтому переходы по таким ссылкам удобны, чтобы ознакомится с плейлистами, но чтобы формировать свое пространство нужно работать с интерфейсом какого-то одного узла, например, дефолтного: player.museria.com


  • Динамическая ссылка. Такая ссылка не связана с хранилищем, она должна содержать путь к любому валидному m3u файлу/ответу сервера в интернете. Содержимое будет автоматически трансформировано в плейлист приложения. Каждые 10 секунд будет происходить новый фоновый запрос по этой ссылке, на случай, если вдруг данные изменились и нужно обновить список. Динамическая ссылка проебразуется в следующий вид, для того, чтобы ею можно было также делиться: http://player.museria.com:80/musiphone/external:someUrlHash



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


Конфиги
Все, что вы настраиваете в плеере хранится в localStorage. Чтобы сохранить эту информацию в файл(json), используйте кнопку "SAVE CONFIG", для загрузки "LOAD CONFIG". Вы можете настраивать различные группы плейлистов, уровень громкости плеера и прочее, создавая разные конфиги. Вот, например, вам конфиг из примеров в этой статье.


Вы можете помочь проекту, запустив хотя бы один узел для музыкального хранилища у себя на сервере пространством 50-1000гб, от 2гб оперативки, 2 ядра. Чем больше узлов, тем больше песен будет доступно.


Либо узел для сети плеера: от 300 мб свободного пространства, 1гб оперативки, 1 ядро. Больше узлов дольше живут ссылки.


Группа в телеграм на английском, либо сразу пишите мне в личку ortex

Подробнее..

Использование Spring Cloud Stream Binding с брокером сообщений Kafka

15.04.2021 16:11:54 | Автор: admin

Всем привет! Меня зовут Виталий, я разработчик в компании Web3Tech. В этом посте я представлю основные концепции и конструкции платформы Spring Cloud Stream для поддержки и работы с брокерами сообщений Kafka, с полным циклом их контекстного unit-тестирования. Мы используем такую схему в своем проекте всероссийского электронного голосования на блокчейн-платформе Waves Enterprise.

Являясь частью группы проектов Spring Cloud, Spring Cloud Stream основан на Spring Boot и использует Spring Integration для обеспечения связи с брокерами сообщений. При этом он легко интегрируется с различными брокерами сообщений и требует минимальной конфигурации для создания event-driven или message-driven микросервисов.

Конфигурация и зависимости

Для начала нам нужно добавить зависимость spring-cloud-starter-stream-kafka в build.gradle:

dependencies {   implementation(kotlin("stdlib"))   implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion")   implementation("com.fasterxml.jackson.module:jackson-module-kotlin")   implementation("org.springframework.boot:spring-boot-starter-web")   implementation("org.springframework.cloud:spring-cloud-starter-stream-kafka")   testImplementation("org.springframework.boot:spring-boot-starter-test")   testImplementation("org.springframework.cloud:spring-cloud-stream-test-support")   testImplementation("org.springframework.kafka:spring-kafka-test:springKafkaTestVersion")}

В конфигурацию проекта Spring Cloud Stream необходимо включить URL Kafka-брокера, имя очереди (топик) и другие параметры биндинга. Вот пример YAML-конфигурации для сервиса application.yaml:

spring: application:   name: cloud-stream-binding-kafka-app cloud:   stream:     kafka:       binder:         brokers: 0.0.0.0:8080         configuration:           auto-offset-reset: latest     bindings:       customChannel:                   #Channel name         destination: 0.0.0.0:8080      #Destination to which the message is sent (topic)         group: input-group-N         contentType: application/json         consumer:           max-attempts: 1           autoCommitOffset: true           autoCommitOnError: false

Концепция и классы

По сути, мы имеем дело с сервисом, построенным на Spring Cloud Stream, который прослушивает входящую очередь, используя биндинги (SpringCloudStreamBindingKafkaApp.kt):

@EnableBinding(ProducerBinding::class)@SpringBootApplication  class SpringCloudStreamBindingKafkaApp fun main(args: Array<String>) { SpringApplication.run(SpringCloudStreamBindingKafkaApp::class.java, *args) }

Аннотация @EnableBinding указывает сервису на биндинг как входящего, так и исходящего канала.

Здесь необходимо уточнить ряд концепций.

Binding интерфейс, в котором описаны входящие и исходящие каналы.
Binder имплементация middleware для сообщений.
Channel представляет канал для передачи сообщений между middleware и приложением.
StreamListeners методы обработки сообщений в виде бинов (beans), которые будут автоматически вызваны после того, как MessageConverter осуществит сериализацию или десериализацию между событиями в middleware и типами объектов в домене DTO.
Message Schema схемы, используемые для сериализации и десериализации сообщений. Могут быть прочитаны из источника или динамически загружены.

Тестирование

Чтобы протестировать сообщение и операции send/receive, нам нужно создать как минимум одного producer и одного consumer. Вот простейший пример того, как это можно сделать в Spring Cloud Stream.

Инстанс бина Producer будет отправлять сообщение в топик Kafka, используя биндер (ProducerBinding.kt):

interface ProducerBinding {   @Output(BINDING_TARGET_NAME)   fun messageChannel(): MessageChannel}

Инстанс бина Сonsumer будет слушать топик Kafka и получать сообщения.

ConsumerBinding.kt:

interface ConsumerBinding {   companion object {       const val BINDING_TARGET_NAME = "customChannel"   }   @Input(BINDING_TARGET_NAME)   fun messageChannel(): MessageChannel}

Consumer.kt:

@EnableBinding(ConsumerBinding::class)class Consumer(val messageService: MessageService) {   @StreamListener(target = ConsumerBinding.BINDING_TARGET_NAME)   fun process(       @Payload message: Map<String, Any?>,       @Header(value = KafkaHeaders.OFFSET, required = false) offset: Int?   ) {       messageService.consume(message)   }}

Мы создали брокер Kafka с топиком. Для тестирования будем использовать встроенную Kafka, доступную нам с зависимостью spring-kafka-test.

Функциональное тестирование с MessageCollector

Мы имеем дело с имплементацией биндера, позволяющей взаимодействовать с каналами и получать сообщения. Отправим сообщение в канал ProducerBinding и затем получим его в виде payloadProducerTest.kt:

@SpringBootTestclass ProducerTest {   @Autowired   lateinit var producerBinding: ProducerBinding   @Autowired   lateinit var messageCollector: MessageCollector   @Test   fun `should produce somePayload to channel`() {       // ARRANGE       val request = mapOf(1 to "foo", 2 to "bar", "three" to 10101)       // ACTproducerBinding.messageChannel().send(MessageBuilder.withPayload(request).build())       val payload = messageCollector.forChannel(producerBinding.messageChannel())           .poll()           .payload       // ASSERT       val payloadAsMap = jacksonObjectMapper().readValue(payload.toString(), Map::class.java)       assertTrue(request.entries.stream().allMatch { re ->           re.value == payloadAsMap[re.key.toString()]       })       messageCollector.forChannel(producerBinding.messageChannel()).clear()   }}

Тестирование с брокером Embedded Kafka

Используем аннотацию @ClassRule для создания брокера. Так мы сможем поднять сервера Kafka и Zookeeper на случайном порте перед началом теста и выключить их, когда тест завершится. Это избавляет нас от необходимости в рабочем инстансе Kafka и Zookeper на всё время проведения теста (ConsumerTest.kt):

@SpringBootTest@ActiveProfiles("test")@EnableAutoConfiguration(exclude = [TestSupportBinderAutoConfiguration::class])@EnableBinding(ProducerBinding::class)class ConsumerTest {   @Autowired   lateinit var producerBinding: ProducerBinding   @Autowired   lateinit var objectMapper: ObjectMapper   @MockBean   lateinit var messageService: MessageService   companion object {       @ClassRule @JvmField       var embeddedKafka = EmbeddedKafkaRule(1, true, "any-name-of-topic")   }   @Test   fun `should consume via txConsumer process`() {       // ACT       val request = mapOf(1 to "foo", 2 to "bar")       producerBinding.messageChannel().send(MessageBuilder.withPayload(request)           .setHeader("someHeaderName", "someHeaderValue")           .build())       // ASSERT       val requestAsMap = objectMapper.readValue<Map<String, Any?>>(objectMapper.writeValueAsString(request))       runBlocking {           delay(20)           verify(messageService).consume(requestAsMap)       }   }}

Заключение

В этом посте я продемонстрировал возможности Spring Cloud Stream и использования его с Kafka. Spring Cloud Stream предлагает удобный интерфейс с упрощенными нюансами настройки брокера, быстро внедряется, стабильно работает и поддерживает современные популярные брокеры, такие как Kafka. По итогам я привел ряд примеров с unit-тестированием на основе EmbeddedKafkaRule с использованием MessageCollector.

Все исходники можно найти на Github. Спасибо за прочтение!

Подробнее..

Стабильная нестабильность оксиморон или необходимость?

19.04.2021 12:12:03 | Автор: admin

Вы скажете - оксиморон! Позволю себе привести некоторые аргументы в защиту данного выражения.

Краткое вступление

Я часто занимаюсь поиском неисправностей в IT системах сложности от средней и выше. Ещё это иногда называют troubleshooting. Хотя иногда переходят на личности и даже обзывают бездельником. Перегрузи хост и дело сделано, говорят мне некоторые коллеги. Я поначалу удивлялся, как можно такое предложить, если на хосте крутится куча сервисов, иногда критических, если куча разработчиков родила массу процессов и хост является частью большой системы!? Потом перестал. И вот почему...

No RebootNo Reboot

В IT сфере бушует индустриализация. Ничего плохого в этом нет и даже иногда это оправдано. Разделение труда, планирование и т.п. - отлично. Проблема в том, что индустриальная эпоха спровоцировала появление в IT большого количества узких специалистов, которые не имеют широгоко взгляда на задачу/систему, которую они разрабатывают или обслуживают. Они в этом не виноваты и обличать никого не собираюсь, все усилия направлены на поиск причины и решения проблемы. Я убеждён, что специалис должен иметь "широкий взгляд на мир" и на ряду с основной специализацией хотябы поверхносто понимать соседние области. Я называю таких allrounder или "мультинструменталистами" (муз. - играющими на разных инструменах)

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

  • стабильное (всё работает как надо)

  • метастабильное (работает, но по принципу "never touch a running system")

  • нестабильное (всё плохо)

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

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

Здесь я введу ещё два важных термина. Состояния системы, как подмножество нестабильного:

  • стабильно нестабильное (или устойчиво нестабильное)

  • нестабильно нестабильное

Стабильно нестабильное или устойчиво нестабильное состояние системы

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

Простой пример из жизни. Система работала со сбоями. После многочисленных ребутов проблема пропадала и мне ставили на вид, мол помогает, а ты нам тут сказки рассказываешь. Правда заодно появлялись странные артефакты в виде подвисания клиентских сессий и пропадания данных (иногда). Проблема спорадически появлялась снова с снова в самый неприятный момент, обычно на выходных. Оказалось, что накосячили с ротацией логов и диск переполнялся. При ребуте временный раздел освобождался и всё какое-то время как-то работало. Пока он снова не переполнялся. Мониторить этот раздел конечно забыли. Элементарно подправили алгоритм ротации логов. За 5 минут где-то. Есть масса более сложных случаев в больших системах с кучей серверов и сервисов, но не буду засорять эфир, смысл думаю понятен.

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

Подведём итоги. Для успехов в поиске неисправностей надо:

  • быть смелым и признаваться в своих ошибках. Это 50% успеха на пути к цели

  • удерживать нестабильную систему в стабильно нестабильном состоянии

  • искать неисправность

Всё, как в жизни :-)

Я "по верхам" (high level) пробежал по теории поиска неисправностей и высветил только её один аспект. Если будет интерес, могу углубиться в другие. Всем спасибо.

Подробнее..

Идеальная избирательная система

02.05.2021 16:16:26 | Автор: admin

На днях мне пришло сообщение от портала Госуслуги с предложением поучаствовать в тестировании дистанционного электронного голосования (ДЭГ). Стало интересно, начал гуглить и поисковик сразу же выдал ссылку на хабровскую статью Обзор системы дистанционного электронного голосования ЦИК РФ. Ознакомилсяипосле прочтения, испытал противоречивые чувства, которые вылились в эту статью, созданную на базе идеи, описанной мной еще в 2018 году на сайте change.org.

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

Ведь, очевидно, что в процессе проведения выборов возникает конфликт интересов между

  • власть имущими, не желающими уступать дорогу другим

  • теми, кто власти пока не имеет, но желает её получить

  • теми, кто хорошо устроился при действующей власти и ничего менять не хочет.

  • простыми гражданами, радеющих за принципы сменяемости власти и соблюдения своих избирательных прав.

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

Каким образом система, предложенная ЦИК-ом защищена от организатора выборов? Да, никак. И дочитав эту статью до конца, вы поймете почему.

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

Причем сделать это можно тайно, без возможности дальнейшего расследования и проверки

И лично у меня создается ощущение, что циковская система специально для этого и создана :)

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

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

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

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

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

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

  1. Внесение фиктивных имен в списки избирателей или Мертвые души (F)

  2. Фальсификация итоговых цифр голосования. Это когда глава избирательной комиссии УИК No 666, приезжает с итоговым протоколом в ТИК No 777, там его встречают, отводят в сторонку, и перед внесением протокола в систему ГАС-выборы немного правят протокол (T)

  3. Вбросы (в урну закидывают пачку бюллетеней) (F)

  4. Хитрый палец - порча бюллетеней, заполненных в пользу нежелательного кандидата (M)

  5. Перестановка результатов голосования, - это когда кандидата, занявшего последнее место, меняют с первым (T)

  6. Неправильный подсчет бюллетеней сотрудниками избирательной комиссии (M-T)

  7. Подмена сейф пакетов и избирательных урн темной безлунной ночью (M)

  8. Приезд пожарных, с последующим досрочным закрытием участка (T)

  9. Забор сейф пакетов сотрудниками МВД на ночное хранение. С целью препятствования подмене бюллетеней и фальсификации выборного процесса. Наша доблестная всегда на страже наших интересов (M-T)

  10. Голосование по паспортным данным. Если избирательным комиссиям будут известны не только ФИО и адреса избирателей, но и их паспортные данные, то не исключено, что недобросовестные члены комиссий смогут внести в журнал за непрошедших на участок избирателей их данные, взять бюллетень и проголосовать (F)

  11. Изъятие или признание недействительными бюллетеней, заполненных в пользу нежелательного кандидата (M)

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

  13. Волшебная урна или выездное голосование на дому (F)

  14. Пустышка - члены избирательных комиссий могут специально выдавать недействительные бюллетени, не имеющие подписей двух членов избирательной комиссии или печати (M)

Все эти проблемы можно разбить по трем классам:

  1. Изменение существующего бюллетеня (M)

  2. Вброс фантомных бюллетеней (F)

  3. Фальсификация итоговых цифр (T)

И подобно требованиям ACID к базам данных, давайте озвучим минимальные требования к избирательной системе:

  • Immutability. Неизменяемость голоса сразу после того как человек проголосовал. Защита от проблемы (1)(M)

  • Accuracy. Точное соответствие проголосовавших людей количеству бюллетеней в базе данных. Защита от проблемы (2)(F)

  • Countability. Возможность избирателя, самостоятельно подсчитать итоговые цифры. Защита от проблемы (3)(T)

Итого, если избирательная система удовлетворяет требованиям IAC то это значит, что можно её обсуждать в сообществе. Ну а так как система предложенная ЦИК, удовлетворяет только первому требованию IAC (Immutability)*, то и тратить время на её дальнейшее полоскание смысла не вижу.

Теперь настало время привести пример избирательной системы, удовлетворяющей требованиям IAC.

Сценарии взаимодействия с iac-системой

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

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

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

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

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

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

И публичная и приватная базы данных находится на носителях, которые на хардварном уровне позволяют только писать информацию, без возможности перезаписи. За счет невозможности перезаписи носителей, мы достигаем выполнения принципа Immutability. Далее наблюдатели могут проверить подключение провода терминала к серверу, также убедится, что к серверу не идут левые провода неизвестно откуда. Как уже сказал, локальная сеть на участке является автономной, без подключения к интернету. Immutability в этот момент достигает значения 100%.

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

Когда голосование по всей стране завершилось, на центральном сервере, единый агрегирующий механизм, собирает всю информацию вместе и после проводит ряд операций:

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

  2. Подсчитывает и публикует итоговые результаты выборов

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

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

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

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

Немного о защите программного кода

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

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

Также, поправьте меня если я ошибаюсь, можно взять хэш от секции кода собранных библиотек. А также секций импорта/экспорта. Тут идея в том, чтобы любой программист мог скачать исходники, собрать бинарные файлы, и проверить полученный хэш на соответствие. Этот шаг необходим для того, чтобы можно было провести соответствие между версией исходного кода, скачанного программистом, и хэшами эталонных бинарных файлов, опубликованными ЦИК-ом. Если в изначальных исходниках был вредоносные код, рано или поздно это вскроется. Соответственно атака на избирательную систему с этой стороны становится бессмысленной.

В итоге мы имеем систему, удовлетворяющую требования IAC, для которой характерны:

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

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

  3. Количество людей, необходимых для сопровождения системы, сведено к минимуму. Следовательно, получаем удешевление обслуживания системы. А также имеем повышенную надежность системы, ввиду уменьшения роли человека.

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

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

На этом у меня всё, друзья. Надеюсь не сильно загрузил деталями и вы всё-таки читаете эти строчки.

Примечания:

* - непосредственное доказательство этого утверждения выходит за рамки данной статьи. Если в комментариях 200 раз попросят разобраться этот момент подробней, то ждите вторую статью)

Материалы использованные для написания статьи:

1) Статья Способы фальсификаций на выборах

Подробнее..

Первый митап Почтатеха DevOps на набережной

23.04.2021 10:10:48 | Автор: admin

29 апреля в Санкт-Петербурге состоится первый открытый митап Почтовых технологий цифровой дочки Почты России.

Почему на набережной? Дело в том, что мероприятие пройдёт в гибридном формате первые 50 зарегистрировавшихся смогут бесплатно посетить его вживую в нашем питерском офисе на Аптекарской набережной. Для остальных будет доступна онлайн-трансляция.

29 апреля мы поговорим о DevOps. Как создавать отказоустойчивые приложения на базе Kubernetes? Своим опытом поделятся три эксперта из Почтатеха, Yandex.Cloud и IT-one.

  • Иван Гаас, лидер DevOps-сообщества Почтатеха, экс-разработчик Parallels и Docker Это сложно? Kubernetes для чайников.

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

  • Алексей Баранов, руководитель разработки сервиса Yandex Managed Service for Kubernetes и DevTools (public API, SDK, CLI, Terraform) Лучшие практики создания отказоустойчивых микросервисных приложений на базе Kubernetes.

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

  • Сергей Иванцов, главный DevOps Expert в Luxoft и DevOps Architect в IT-One Как сократить время поиска отказа распределённой системы с 5 часов до 5 минут.

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

Дата и время: 29 апреля в 18:00.

Место: Санкт-Петербург, Аптекарская набережная, 18, стр. 1.

Стоимость: Бесплатно.

Регистрация: https://pochtameetups.ru.

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

Подробнее..

Неочевидные сложности CRDT

12.05.2021 16:20:09 | Автор: admin


Мы все так привыкли к облачной синхронизации Dropbox и совместному редактированию в Google Docs, что объединение результатов действий разных пользователей может казаться давно решённой проблемой. Но на самом деле в этом вопросе остаётся множество подводных камней, а работа над алгоритмами CRDT вовсю продолжается.


Один из людей, ведущих эту работу Мартин Клеппманн (Martin Kleppmann): исследователь в Кембриджском университете и создатель популярной библиотеки Automerge. И на конференции Hydra он рассказывал о нескольких вещах, которые исследовал буквально в последнюю пару лет. Какие действия пользователя могут заставить Google Drive выдать unknown error? Почему в CRDT метаданные о работе над документов могут занимать в сто раз больше места, чем сам документ, и как с этим бороться? А у какой проблемы сейчас даже не существует известного решения?


Обо всём этом он поведал в докладе, а теперь мы сделали для Хабра текстовый перевод. Видео и перевод под катом, далее повествование будет от лица спикера.



Содержание доклада



Сегодня я хотел бы рассказать вам о CRDT (Conflict-free Replicated Data Types, бесконфликтные реплицированные типы данных). Вначале мы вкратце выясним, что же это такое и зачем они нам нужны, но в основном будет новый материал на основе исследований, которые мы проводили последний год или два. Возможно, вы читали мою книгу. Но сегодня речь пойдёт о ПО для совместной работы (collaborative software).


ПО для совместной работы


С помощью такого ПО несколько пользователей могут участвовать в редактировании и обновлении общего документа, файла, базы данных или какого-либо другого состояния. Примеров такого ПО много. С помощью Google Docs или Office 365 можно совместно редактировать текстовые документы. Среди графических приложений ту же функциональность даёт Figma. Trello позволяет пользователям давать друг другу задания, комментировать их и тому подобное. Общим у всех этих приложений является то, что в них пользователи могут параллельно обновлять данные.


В качестве примера рассмотрю редактирование текста (но те же механизмы работают для довольно широкого спектра приложений).


В редакторе текста для совместной работы два пользователя могут почти одновременно внести изменения в документ, при этом не видя изменений друг друга. Предположим, есть документ, состоящий из слова Hello!. Красный пользователь добавляет перед восклицательным знаком слово World, а параллельно с ним синий пользователь добавляет после восклицательного знака смайлик:



Необходимо обеспечить, чтобы при подключении к сети документ обоих пользователей пришёл к одинаковому состоянию, отражающему все внесённые изменения. В нашем случае документ будет выглядеть так: Hello World! :-).


Алгоритмы для параллельного редактирования документов


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


На самом общем уровне можно различить два вида алгоритмов, обеспечивающих параллельное редактирование документов:


  • Давно существует семейство алгоритмов операционального преобразования (operational transformation, OT), используемых в Google Docs и многих других современных приложениях.
  • А примерно 15 лет назад появился более новый вид: CRDT.

Оба вида алгоритмов решают примерно одну и ту же проблему, но существенно разными способами.


Рассмотрим вкратце механизм работы операционального преобразования. Предположим, существует документ, состоящий из букв Helo, к которому есть доступ у двух пользователей. Один пользователь добавляет в документ вторую букву l, а другой восклицательный знак. Теперь необходимо объединить эти версии документа:



При операциональном преобразовании каждое изменение записывается как операция. Операция это добавление или удаление символа в документе. Поэтому необходим способ указать, где именно произошло это изменение. В операциональном преобразовании для этого используется индекс символа. В случае нашего документа индекс новой l 3, а восклицательного знака 4.


В какой-то момент пользователи обмениваются изменениями через сервер. Второй пользователь узнаёт, что по индексу 3 необходимо вставить букву l, и получает необходимый результат. Но в случае с первым пользователем всё несколько менее очевидно. Если просто добавить восклицательный знак по индексу 4, то получится Hell!o, поскольку первый пользователь до этого добавил новый символ по индексу 3, и из-за этого индексы всех последующих символов увеличились на единицу. Поэтому индекс 4 необходимо преобразовать в индекс 5. Отсюда название алгоритма операциональное преобразование. И отсюда же главная сложность с ним.


Большинство алгоритмов операционального преобразования исходят из предположения о том, что все взаимодействия выполняются через единый сервер. Даже если пользователи сидят в одной комнате и могли бы передать друг другу свои версии по Bluetooth, им всё равно нужно связаться с сервером (который в случае Google Docs предоставляет им Google). Этот алгоритм правилен только в том случае, если все изменения упорядочены одним сервером. А следовательно, любые другие каналы обмена информацией не допускаются, даже если технически доступны будь то Bluetooth, P2P или банальная флешка.



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


Сходимость


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


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


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


Хочу привести четыре примера конкретных проблем. Первый:


Аномалии чередования в текстовых редакторах для совместной работы


Речь идёт о ситуации, когда несколько пользователей пытаются одновременно вставить текст на одной и той же позиции документа.


Я уже говорил, что алгоритмы операционального преобразования используют индекс символа. Алгоритмы CRDT же индексами не пользуются; вместо этого они присваивают каждому символу документа уникальный идентификатор. Механизмы генерации таких идентификаторов используются разные. Часто это делают с помощью дробей. Предположим, каждый символ у нас представлен числом от 0 до 1, где 0 начало документа, а 1 окончание. В примере с документом Helo позицию первого символа будет представлять число 0.2, второго 0.4, третьего 0.6, четвёртого 0.8.


Тогда при добавлении второй буквы l между первой l и o этому новому символу будет присвоено значение от 0.6 до 0.8 например, 0.7. Подобным же образом восклицательному знаку присваивается значение 0.9.



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


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


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



У нас есть документ Hello!. Один пользователь добавил Alice перед восклицательным знаком, второй пользователь добавил Charlie. Но в результате слияния этих изменений мы получили нечто совершенно непонятное. Почему это произошло? Чтобы разобраться, взглянем на схему, представленную на следующем слайде.



В первоначальном документе символу o соответствует цифра 0.64, а восклицательному знаку 0.95. Первый пользователь добавил несколько символов между этими двумя, и новым символам были присвоены значения от 0.64 и до 0.95. Параллельно этому второй пользователь добавил свои символы на той же позиции, и им были присвоены цифры в том же диапазоне.


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


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


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



Проблему чередования мы нашли как минимум в двух алгоритмах Logoot и LSEQ. Из-за особенностей этих алгоритмов простого способа решить эту проблему для них нет. Нам не удалось улучшить их в этом отношении, не изменив при этом фундаментально самих алгоритмов. В Treedoc и WOOT мы этой проблемы не нашли, но, к сожалению, эти алгоритмы наименее эффективные. Собственно говоря, главной причиной, по которой был создан LSEQ, было достижение большей производительности по сравнению с Treedoc и WOOT. Возвращаться к этим неэффективным алгоритмам не хотелось бы. Далее, Astrong не алгоритм, а спецификация, формализующая требования к алгоритмам редактирования текста. К сожалению, она также допускает чередование.


RGA


Отдельный случай алгоритм RGA. Непосредственно той проблемы, о которой я сейчас говорил, там нет, зато есть другая, правда, менее существенная. Попробуем вкратце её рассмотреть.



На слайде представлен пример, в котором один пользователь добавляет слово reader между Hello и !, затем возвращает курсор к позиции непосредственно перед reader и добавляет dear. Здесь очень важно именно перемещение курсора. Второй пользователь только добавляет Alice между Hello и !. При действии алгоритма RGA возможным результатом этих операций будет чередование слов: Hello dear Alice reader!. Это тоже нежелательно, хоть и лучше, чем чередование символов.


Давайте разберёмся, почему это происходит. RGA использует структуру, показанную на слайде.



В ней текст представлен в виде очень глубокого дерева, в котором родителем каждого символа является предшествующий на момент добавления символ. Счёт идёт от положения курсора на момент ввода символа. Исходный текст Hello!. Затем у нас есть три случая перемещения курсора перед написанием reader, Alice и dear. В такой ситуации алгоритм RDA генерирует один из трёх возможных результатов, перечисленных на слайде. Из них второй, на мой взгляд, является нежелательным, поскольку в нём происходит чередование слов разных пользователей. Другие два результата вполне допустимы.


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


В Logoot и LSEQ проблема чередования, как мне кажется, нерешаема, и, к сожалению, использовать их из-за этого нельзя. Мы разработали алгоритм для RGA, который позволяет избавиться даже от этого варианта проблемы чередования, но сейчас у меня нет времени на нём останавливаться. При желании с ним можно ознакомиться в нашей статье об аномалиях чередования, представленной на семинаре PaPoC в 2019 году. Там подробно описан этот алгоритм.


Перемещение элементов в списке


Мы опубликовали статью на эту тему всего несколько месяцев назад (PaPoC 2020). На слайде проблема представлена на примере списка задач.



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


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


Давайте посмотрим, что произойдёт при таком подходе в ситуации, когда два пользователя осуществляют перемещение параллельно как это происходит на слайде.



Оба пользователя одновременно выполняют одну и ту же операцию: перемещают операцию phone joe с последней позиции в списке на первую. Операция перемещения реализована как удаление с последующим добавлением, поэтому вначале необходимый элемент дважды удаляется на старой позиции. Здесь разницы с тем, чтобы удалить его один раз, нет. А вот если добавить элемент дважды, то на новой позиции появится два элемента phone joe вместо одного. Ясно, что такой вариант нам не подходит.


Давайте спросим себя: что именно должно было произойти в этой ситуации?



На слайде представлен другой пример, в котором всё тот же элемент пользователи переместили на разные итоговые позиции в списке. Первый пользователь переместил элемент phone joe в начало списка, второй на позицию после buy milk. К какому именно результату должен прийти алгоритм? Он может, как и в прошлом примере, дублировать элемент phone joe, чтобы в итоге этот элемент оказался и в начале списка, и после buy milk. Но это нежелательный вариант. Перемещаемый элемент должен оказаться лишь на одной из этих двух позиций. Чтобы этого достичь, необходимо выбрать одну из двух конфликтующих версий, причём выбрать произвольно, но детерминистично. В нашем примере выбрана версия, в которой phone joe оказывается в начале списка.


Как реализовать такой вариант? Если вы знакомы с существующими работами в области CRDT, это может выглядеть знакомо. Мы тут смотрим на ситуацию, в которой произвольно, но детерминистически выбираем одно из нескольких параллельно записываемых значений. Такое происходит к last writer wins register. Там, если значение могут обновлять несколько пользователей, при параллельной записи нескольких значений выбирается одно на основе произвольного критерия вроде метки времени.


Дальше это значение становится общим, а остальные значения удаляются. В нашем примере со списком необходимо именно такое поведение. Мы можем представить позицию каждого элемента в списке как last writer wins register, а значение, находящееся в регистре как описание позиции данного элемента. Когда первый пользователь указывает для позиции phone joe начало списка, а второй перемещает этот элемент на позицию после buy milk, нам необходимо, чтобы одно из этих значений стало общим для обоих пользователей. Таким итоговым значением может быть начало списка.


То есть для операции перемещения мы можем использовать существующий CRDT (last writer wins register). Чтобы реализовать такую операцию, необходимы две вещи. Во-первых, сам регистр, и, во-вторых, способ описать положение некоторого элемента в списке (это и будет значением, содержащимся в регистре). Но если подумать, то такой способ уже есть.


Вспомните, что я уже говорил выше. Все CRDT присваивают каждому элементу списка или символу в тексте уникальный идентификатор. Каким именно будет этот идентификатор, зависит от того, какой используется алгоритм CRDT. Например, в Treedoc применяется путь через бинарное дерево, в Logoot путь через более сложное дерево с большим фактором ветвления, в RGA метка времени и так далее. Во всех этих CRDT есть работающий механизм обращения к определённой позиции в списке. Так что позиции можно сгенерировать с помощью существующих алгоритмов. А при перемещении элемента на новую позицию можно создать новый идентификатор для позиции, на которую мы хотим переместить элемент, после чего записать этот идентификатор в наш last writer wins register.


Помимо этого, нам необходим отдельный регистр last writer wins для каждого элемента в списке. Но здесь тоже есть простое решение: это набор CRDT add-wins set (AWSet на слайде):



Можно создать add-wins set, в котором каждый элемент набора является пунктом нашего списка и состоит из пары, а именно, значения элемента (например, описание пункта списка) и регистра last writer wins с положением этого пункта. Регистр last writer wins можно поместить в набор AWSet, а идентификаторы позиции из CRDT списка можно поместить в регистр last writer wins register. Итак, мы объединили три CRDT и создали новый CRDT, а именно CRDT списка с операцией перемещения. Он позволяет атомарно переместить элемент с одной позиции в списке на другую что, согласитесь, весьма неплохо. Заметьте, что этот подход работает с любым из существующих алгоритмов CRDT списка. То есть любой существующий CRDT можно дополнить операцией перемещения.


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



Предположим, второй пользователь хочет переместить элемент Milk со второго места в списке на первое, то есть одновременно переместить последовательность символов. Параллельно этому первый пользователь изменяет Milk на Soy milk. Для этого он вначале удаляет первый символ слова Milk, а затем добавляет все остальные символы. В итоге возникло два отличающихся документа, которые теперь необходимо привести к одному состоянию. Каким должно быть это состояние? Интуитивно ожидаемый вариант переместить Milk в начало списка и изменить текст на Soy milk. К сожалению, если мы применим описанную мной выше операцию перемещения, то получим результат, показанный на следующем слайде.



Не знаю как вам, а мне на него смотреть грустно. Элемент Milk был перенесён на первую позицию в списке, но все изменения в этом элементе остались на старой позиции этого элемента. В результате документ заканчивается символами Soy m. Это произошло потому, что позиция, на которой было выполнено добавление этих символов, не изменилась. Я не думаю, что эта проблема нерешаемая, но пока что нам не удалось найти адекватного способа её преодолеть. Я буду очень рад, если кто-то из вас захочет над ней подумать.


Перемещение деревьев


Вообще деревья крайне полезная структура данных. Файловая система вашего компьютера, документ JSON или XML всё это примеры деревьев. Предположим, у вас есть приложение, которое использует то или иное дерево например, распределённую файловую систему или документ JSON с возможностью перемещать поддерева документа. Такому приложению необходима операция перемещения. И здесь сразу возникает проблема, целиком аналогичной той, с которой мы уже встречались в контексте списков: что происходит, когда несколько пользователей параллельно перемещают один и тот же узел на разные позиции?



В примере на слайде первый пользователь перемещает узел А (со всеми дочерними узлами) так, что тот становится дочерним узлом B. Одновременно второй пользователь делает узел А дочерним узлом C. При таком конфликте возможны четыре итоговых дерева, которые представлены в правой части слайда. Во-первых, узел А может быть дублирован это не лучшее решение проблемы. В новом дереве один узел А окажется дочерним для B, а другой для C. Но помимо самого узла A нужно также скопировать все его дочерние узлы. В большинстве приложений такой итог нежелателен.


Второй вариант сделать узел A одновременно дочерним и для B, и для C. Такая структура данных уже не дерево, а ориентированный ациклический граф (DAG), или граф более общего типа. На мой взгляд, этот вариант тоже неудачный. Следует выбирать между третьим и четвёртым вариантами, где A является либо дочерним для B, либо дочерним для C. Здесь, как и в ситуации с перемещением в списке, одна версия становится общей, а вторая удаляется.


Но сейчас перед нами встаёт дополнительное затруднение. Вы можете поставить эксперимент у себя на компьютере. Создайте каталог a, затем в нём создайте другой каталог b, а потом попробуйте переместить a в b. То есть попробуйте сделать a подкаталогом самого себя. Как вы думаете, что произойдёт? По идее, в результате должен возникнуть цикл. Я попробовал в macOS и получил сообщение об ошибке Invalid argument. Подозреваю, что большинство операционных систем, в которых файловая система является деревом, реагируют подобным образом, потому что если разрешить такую операцию, то система уже не будет деревом.


Но обнаружить ситуацию, когда пользователь пытается сделать директорию дочерней для себя самой, довольно просто. А вот в случае с CRDT всё сложнее. Взглянем на пример на слайде.



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


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


А как ведут себя существующие программы в таком случае? Я поставил эксперимент с Google Drive, результат которого виден на слайде.



На одном компьютере я переместил A в B, на другом B в A, затем дал им синхронизироваться. Google Drive вернул сообщение о неизвестной ошибке. Так что как видим, даже Google не удалось найти удовлетворительное решение этой проблемы. Но для CRDT правильное решение всё-таки необходимо. С этой целью мы разработали алгоритм, который позволяет безопасно выполнять такого рода операции для деревьев, не приводя к возникновению циклических связей. О нём я сейчас и расскажу.


Схематично этот алгоритм представлен на следующем слайде:



Первый пользователь здесь выполнил операции op1, op3, mv A B и op5. Каждой операции присвоена метка времени t. Для соответствующих операций значения этих меток 1, 3, 4 и 5. У второго пользователя есть операция mv B A с меткой времени 6. Как уже говорилось, сами по себе эти операции перемещения безопасны. Сложности начинаются, когда мы пытаемся синхронизировать эти операции, как это показано на следующем слайде.



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


Давайте взглянем теперь на ситуацию с точки зрения первого пользователя. В его версии уже выполнены операции с метками времени от 1 до 5. С ними теперь нужно синхронизировать операцию op2, выполненную вторым пользователем. Поскольку значение её метки времени 2, она должна оказаться между двумя операциями первого пользователя, как это показано на следующем слайде.



Мы не можем просто добавить op2 в конец списка, потому что тогда они не будут упорядочены по метке времени. Чтобы определить, какие операции безопасные, а какие нет, важно знать порядок, в котором операции выполняются.


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



Для реализации этого алгоритма необходима возможность отмены операций. Как видим, первому пользователю нужно добавить новую операцию с меткой времени 2, в то время как у него уже есть операция с меткой времени 5. Поэтому ему необходимо отменить все операции с меткой времени больше 2, то есть операции op5, mv A B и op3. Теперь можно добавить операцию op2, а затем повторно применить все отменённые операции. При условии, что все операции полностью отменяемые, этот алгоритм генерирует список операций, применённых в порядке возрастания метки времени.


Все эти преобразования могут показаться ресурсозатратными. Какие-то издержки здесь, конечно, неизбежны. Чтобы определить их размер, мы поставили эксперимент с тремя репликами, которые мы развернули на трёх разных континентах, чтобы коммуникация происходила с большой задержкой. Затем мы запустили генерацию операций перемещения на этих репликах. Чем быстрее эти операции генерируются, тем больше отмен и повторов необходимо выполнять. Мы обнаружили, что время выполнения отдельной операции растёт линейно в зависимости от скорости применения операций, поскольку количество отмен и повторов растёт линейно в зависимости от числа операций. Тем не менее, в нашей довольно простой программе обрабатывалось около 600 операций в секунду. Это существенно меньше показателей, скажем, системы работы с большими данными, но значительно больше того, что необходимо приложению, запущенному одним пользователем в своей локальной системе для отдельного дерева. Так что для большого диапазона приложений такая производительность вполне допустима.


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



В сокращённом виде операция перемещения описывается структурой MoveOp. В каждой операции есть метка времени Timestamp. Такая метка должна быть уникальной для всего приложения; этого легко добиться с помощью, например, меток времени Лэмпорта. Далее, для операции необходим идентификатор, по которому можно обращаться к узлам дерева. Здесь child это идентификатор перемещаемого узла, а parent идентификатор узла назначения. Metadata meta это метаданные отношения родительского и дочернего узла. Например, если мы работаем с файловой системой, в качестве meta может выступать имя файла. Обратите внимание, что здесь нигде не записано старое местоположение дочернего узла. Операция просто переносит узел со старого местоположения на новое.


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


Теперь мы можем описать алгоритм перемещения. Вначале нужно определить, что значит, что a является родительским узлом b. Формально это сделано на слайде.



a является родителем b в том случае, если (a, m, b) дереву, то есть если a является прямым родителем b в дереве, или если есть узел c, такой, что a является родителем c, а c является родителем b. При таком определении операцию перемещения можно определить следующим образом. Вначале нужно проверить, не является ли перемещаемый узел родителем по отношению к своему родительскому узлу. Если да, то операция не выполняется, потому что она привела бы к возникновению цикла. То же самое в ситуации, когда дочерний узел идентичен родительскому. Если эти проверки успешно пройдены, то ситуация безопасна. Старое отношение можно удалить, а затем вставить новое отношение из дерева с новыми метаданными.


Мы доказали правильность этого алгоритма. А именно, мы показали, что для каждого дерева, с которым выполняется операция, сохраняется свойство дерева. Структура данных является деревом в случае, если у каждого узла есть уникальный родитель и если в дереве нет циклов. Выше мы показали, что операция перемещения не нарушает этих свойств. Кроме того, мы доказали, что эта операция является CRDT. То есть не важно, в каком порядке применяются операции если два пользователя применяют один и тот же набор операций, то итоговое состояние одинаковое. Иначе говоря, мы доказали, что операции коммутативны.


Сокращение издержек метаданных


CRDT с самого их появления преследовали проблемы с издержками метаданных. Для свойств CRDT всегда требовалось много места и сетевых ресурсов. Как вы помните, при редактировании текста мы присваивали уникальный идентификатор каждому символу в текстовом документе.



На следующем слайде показан текст, в котором использован такой алгоритм. Если работа идёт с английским текстом, то в кодировке UTF-8 один символ занимает один байт; для текста на русском языке каждый символ, скорее всего, будет занимать два байта. Это не так много; но в дополнение к этому необходим идентификатор позиции. Чаще всего в качестве него используется какой-нибудь путь в дереве, размер которого зависит от длины пути. Он с лёгкостью может занимать несколько десятков байт, если не сотню. Помимо него необходим идентификатор узла, то есть пользователя, который добавил данный символ. В итоге для символа, занимающего один байт, необходимо 100 байт метаданных, или даже больше. Согласитесь, ситуация крайне печальная.


Хочу поговорить об экспериментах, которые мы проводили в проекте Automerge, специальной реализации CRDT. За последние несколько месяцев я посвятил довольно много усилий тому, чтобы сократить издержки CRDT. Результаты вкратце представлены на следующем слайде.



Давайте обсудим их по порядку. Набор данных, на который мы тут смотрим история редактирования одной статьи в академическом издании. Мы написали исследовательскую статью (в формате plain text-файла в формате LaTeX размером около 100 килобайт) с помощью текстового редактора собственной разработки, записав каждое нажатие клавиши во время этой работы. В общей сложности зафиксировано около 300 000 изменений: это включает все добавления и удаления символов, а также перемещения курсора. Мы хотели сохранить всю эту историю изменений, чтобы иметь возможность наблюдать за эволюцией документа (так же, как в системе контроля версий сохраняют прошлые версии проекта).


Проще всего этого добиться, если хранить журнал операций CRDT. Если записать такой журнал в формате JSON, он займёт около 150 мегабайт. С помощью gzip его можно сжать до примерно 6 мегабайт, но по сравнению со 100 килобайтами исходного текста без метаданных это всё равно огромный размер. Нам удалось, более эффективно закодировать данные и сократить размер файла метаданных до 700 килобайт (то есть где-то в 200 раз), при этом сохранив полную историю изменений. Если же отказаться от хранения перемещений курсора, размер можно сократить ещё на 22%. Далее, если убрать историю редактирования CRDT и оставить только метаданнные, необходимые для слияния текущей версии документа, размер сокращается до 228 килобайт. Это всего лишь в два раза больше размера исходного текста без метаданных. Если эти данные ещё и сжать в gzip, то мы получим чуть больше 100 килобайт. Далее, если избавиться от отметок полного удаления (tombstones), то мы получим 50 килобайт. Это то, что нужно для хранения уникальных идентификаторов для каждого символа. Но без отметок полного удаления мы уже не можем обеспечить слияния параллельно добавленных изменений.


Даже сохраняя целиком историю редактирования и отметки полного удаления, мы всё равно можем добиться весьма компактного размера итогового файла. Сейчас я постараюсь кратко объяснить, как это сделать. Мы храним информацию о всех выполненных операциях, и каждой операции присваивается уникальный идентификатор. В нашем случае таким идентификатором является метка времени Лэмпорта, это пара из счётчика и идентификатора узла. Как и в RGA, в нашем алгоритме для определения местоположения символа используется ссылка на идентификатор предшествующего символа. Далее все операции сохраняются в порядке, в котором они появляются в тексте. В итоге получается таблица вроде той, которая показана на следующем слайде.



В первых двух столбцах слайда хранятся метки времени Лэмпорта для каждой операции. Каждая строка здесь операция вставки. Как видим, в третьем и четвёртом столбце указаны идентификаторы предшествующих символов. Здесь второй символ добавляется после первого, третий после второго и так далее. В следующих двух столбцах указаны вставляемые символы в кодировке UTF-8, и количество байт, которое занимает каждый символ. Наконец, в последних двух столбцах записано, был ли символ удалён.


Эти данные можно сжать с помощью нескольких довольно простых приёмов. Каждый столбец этой таблицы можно закодировать отдельно. Первый столбец содержит числа 1, 2, 3, 4, 5, 6. К ним можно применить дельта-кодирование, то есть вычислить для каждого числа разницу между ним и предшествующим числом. Тогда мы получим 1, 1, 1, 1, 1, 1. К этому ряду можно применить кодирование длин серий, то есть сказать, что мы храним 6 повторений числа 1. Если к этому применить кодировку переменной длины для целых чисел, результат займёт всего два байта. Эта кодировка записывает самые короткие числа одним байтом, которые подлиннее двумя, трёмя и тому подобное. Итак, для записи первого столбца нам потребовалось всего два байта, то есть байта на операцию. Для второго столбца нужно создать таблицу подстановки, в которой идентификаторам пользователей присваиваются короткие числа. Тогда второй столбец можно представить как 0, 0, 0, 0, 0, 0. Дальше опять-таки можно применить кодирование длин серий, а затем кодировку переменной длины для целых чисел, и, как и в первом случае, результат будет занимать два байта.


Взглянем на столбец с символами UTF-8. Обратите внимание, что у нас есть отдельный столбец, в котором указана длина символа в байтах. Сам этот столбец легко закодировать только что описанным способом. А символы UTF-8 можно просто объединить в строку UTF-8 размером в 6 байт. Наконец, в последних двух столбцах мы записываем только тот факт, что мы удалили один символ. В дополнение ко всему этому нам необходимо совсем немного метаданных с информацией о том, когда произошло каждое изменение, какой диапазон идентификаторов операций, какие значения счетчиков для каждого изменения. В результате можно реконструировать состояние документа в любой момент времени в прошлом. Итак, мы сохранили целиком историю документа, но сжали её так, что она занимает совсем немного места.


На этом я хотел бы перейти к заключению. Как я уже сказал вначале, в работе с CRDT много трудностей. Сделать хорошую реализацию непросто, а плохую легче лёгкого. Мы рассмотрели четыре проблемы, которые давно преследуют CRDT: чередование, операции перемещения в списках и деревьях, а также проблему издержек. Если вы хотите ознакомиться с дополнительной литературой по теме, вот список статей, в которых описан целый ряд CRDT для редактирования текста.


Научные статьи по теме

Logoot: Stephane Weiss, Pascal Urso, and Pascal Molli: Logoot: A Scalable Optimistic Replication Algorithm for Collaborative Editing on P2P Networks, ICDCS 2009.


LSEQ: Brice Nedelec, Pascal Molli, Achour Mostefaoui, and Emmanuel Desmontils: LSEQ: an Adaptive Structure for Sequences in Distributed Collaborative Editing, DocEng 2013.


RGA: Hyun-Gul Roh, Myeongjae Jeon, Jin-Soo Kim, and Joonwon Lee: Replicated abstract data types: Building blocks for collaborative applications, Journal of Parallel and Distributed Computing, 71(3):354368, 2011.


Treedoc: Nuno Preguica, Joan Manuel Marques, Marc Shapiro, and Mihai Letia: A Commutative Replicated Data Type for Cooperative Editing, ICDCS 2009.


WOOT: Gerald Oster, Pascal Urso, Pascal Molli, and Abdessamad Imine: Data consistency for P2P collaborative editing, CSCW 2006.


Astrong: Hagit Attiya, Sebastian Burckhardt, Alexey Gotsman, Adam Morrison, Hongseok Yang, and Marek Zawirski: Specification and Complexity of Collaborative Text Editing, PODC 2016.


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


Interleaving anomaly: Martin Kleppmann, Victor B. F. Gomes, Dominic P. Mulligan, and Alastair R. Beresford: Interleaving anomalies in collaborative text editors. PaPoC 2019.


Proof of no interleaving in RGA: Martin Kleppmann, Victor B F Gomes, Dominic P Mulligan, and Alastair R Beresford: OpSets: Sequential Specifications for Replicated Datatypes, May 2018.


Moving list items: Martin Kleppmann: Moving Elements in List CRDTs. PaPoC 2020.


Move operation in CRDT trees: Martin Kleppmann, Dominic P. Mulligan, Victor B. F. Gomes, and Alastair R. Beresford: A highly-available move operation for replicated trees and distributed filesystems. Preprint


Reducing metadata overhead: Martin Kleppmann: Experiment: columnar data encoding for Automerge, 2019.


Local-first software: Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark McGranaghan: Local-first software: You own your data, in spite of the cloud. Onward! 2019.


Все слайды доступны в электронном виде. Спасибо большое за внимание!


Если вы дочитали этот доклад с Hydra 2020 до конца, похоже, что вы интересуетесь распределёнными вычислениями. В таком случае вам будет интересно и на Hydra 2021, которая пройдёт с 15 по 18 июня. Там будут как доклады от академиков, так и выступления людей из индустрии, которые имеют дело с параллельными и распределёнными системами в продакшне на сайте уже анонсирован ряд докладов.
Подробнее..

Часть 3. MPI Как процессы обшаются? Сообщения типа точка-точка

27.03.2021 22:10:41 | Автор: admin

В этом цикле статей речь идет о параллельном программировании с использованием MPI.

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


Предисловие

Практически все программы с использованием технологии MPI используют не только средства для порождения процессов и их завершения, но и одну из самых важных частей, заложенных в названии самой технологии (Message Passing Interface), конечно же явная посылка сообщений между процессами. Описание этих процедур начнем, пожалуй с операций типа точка-точка.

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

Работает это так: один из процессов, назовем его P1, какого то коммуникатора C должен указать явный номер процесса P2, который также должен быть под коммуникатором С, и с помощью одной из процедур передать ему данные D, но на самом деле не обязательно нужно знать номер процесса, но это мы обсудим далее.

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

Оба типа процедур мы затронем подробно, а пока начнем с процедур с блокировкой.

Передаем и принимаем сообщения

Наконец приступим к практической части. Для передачи сообщений используется процедура MPI_Send. Эта процедура осуществляет передачу сообщения с блокировкой. Синтаксис у нее следующий:

int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest,  int msgtag, MPI_Comm comm);

Что тут есть что:
buf - ссылка на адрес по которому лежат данные, которые мы пересылаем. В случае массивов ссылка на первый элемент.
count - количество элементов в этом массиве, если отправляем просто переменную, то пишем 1.
datatype - тут уже чутка посложнее, у MPI есть свои переопределенные типы данных которые существуют в С++. Их таблицу я приведу чуть дальше.
dest - номер процесса кому отправляем сообщения.
msgtag - ID сообщения (любое целое число)
comm - Коммуникатор в котором находится процесс которому мы отправляем сообщение.

А вот как называются основные стандартные типы данных С++ определенные в MPI_Datatype:

Название в MPI

Тип даных в С++

MPI_CHAR

char

MPI_SHORT

signed short

MPI_INT

signed int

MPI_LONG

signed long int

MPI_LONG_LONG

signed long long int

MPI_UNSIGNED_*** (Вместо *** int и т.п.)

unsigned ...

MPI_FLOAT

float

MPI_DOUBLE

double

MPI_LONG_DOUBLE

long double

MPI_INT8_T

int8_t

MPI_INT16_T

int16_t

MPI_C_COMPLEX

float _Complex

Аналогичным способом указываются и другие типы данных определенные в стандартной библиотеке С/С++ - MPI_[Через _ в верхнем регистре пишем тип так как он назван в С]. Еще один пример для закрепления понимания, есть тип беззнаковых 32 битных целых чисел, назван он uint32_t, чтобы получить этот тип данных переопределенным в MPI необходимо написать следующую конструкцию: MPI_UINT32_T. То есть все вполне логично и легко, верхний регистр, вместо пробелов знаки андерскора и в начале пишем MPI.

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

Теперь поговорим о приеме этих сообщений. Для этого в MPI определена процедура MPI_Recv. Она осуществляет, соответственно, блокирующий прием данных. Синтаксис выглядит вот так:

int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status* status);

Что тут есть что:
buf - ссылка на адрес по которому будут сохранены передаваемые данные.
count - максимальное количество принимаемых элементов.
datatype - тип данных переопределенный в MPI(по аналогии с Send).
source - номер процесса который отправил сообщение.
tag - ID сообщения которое мы принимаем (любое целое число)
comm - Коммуникатор в котором находится процесс от которого получаем сообщение.
status - структура, определенная в MPI которая хранит информацию о пересылке и статус ее завершения.

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

MPI_Status status;MPI_Recv(&buffer, 1, MPI_Float, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status)tag = status.MPI_SOURCEsource = status.MPI_SOURCE

Здесь представлен кусочек возможного кода в процессе который принимает данные не более одного float элемента от любого процесса с любым тегом сообщения. Чтобы узнать какой процесс прислал это сообщение и с каким тэгом нужно собственно воспользоваться структурой MPI_Status.

Заметим появление констант MPI_ANY_SOURCE и MPI_ANY_TAG, они явно указывают, что можно принимать сообщения от любого процесса с любым тэгом.

Дабы закрепить знания об этих двух процедурах приведу пример как это выглядит на практике, данная программа определяет простые числа на заданном интервале:

#include <stdio.h>#include <time.h>#include "mpi.h"#define RETURN return 0#define FIRST_THREAD 0int* get_interval(int, int, int*);inline void print_simple_range(int, int);void wait(int);int main(int argc, char **argv){// инициализируем необходимые переменныеint thread, thread_size, processor_name_length;int* thread_range, interval;double cpu_time_start, cpu_time_fini;char* processor_name = new char[MPI_MAX_PROCESSOR_NAME * sizeof(char)];MPI_Status status;interval = new int[2];// Инициализируем работу MPIMPI_Init(&argc, &argv);// Получаем имя физического процессораMPI_Get_processor_name(processor_name, &processor_name_length);// Получаем номер конкретного процесса на котором запущена программаMPI_Comm_rank(MPI_COMM_WORLD, &thread);// Получаем количество запущенных процессовMPI_Comm_size(MPI_COMM_WORLD, &thread_size);// Если это первый процесс, то выполняем следующий участок кодаif(thread == FIRST_THREAD){// Выводим информацию о запускеprintf("----- Programm information -----\n");printf(">>> Processor: %s\n", processor_name);printf(">>> Num threads: %d\n", thread_size);printf(">>> Input the interval: ");// Просим пользователья ввести интервал на котором будут вычисленияscanf("%d %d", &interval[0], &interval[1]);// Каждому процессу отправляем полученный интервал с тегом сообщения 0. for (int to_thread = 1; to_thread < thread_size; to_thread++)       MPI_Send(&interval, 2, MPI_INT, to_thread, 0, MPI_COMM_WORLD);// Начинаем считать время выполненияcpu_time_start = MPI_Wtime();}// Если процесс не первый, тогда ожидаем получения данныхelse     MPI_Recv(&interval, 2, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);// Все процессы запрашивают свой интервалrange = get_interval(thread, thread_size, interval);// После чего отправляют полученный интервал в функцию которая производит вычисленияprint_simple_range(range[0], range[1]);// Последний процесс фиксирует время завершения, ожидает 1 секунду и выводит результатif(thread == thread_size - 1){cpu_time_fini = MPI_Wtime();wait(1);printf("CPU Time: %lf ms\n", (cpu_time_fini - cpu_time_start) * 1000);}MPI_Finalize();RETURN;}int* get_interval(int proc, int size, int interval){// Функция для рассчета интервала каждого процессаint* range = new int[2];int interval_size = (interval[1] - interval[0]) / size;range[0] = interval[0] + interval_size * proc;range[1] = interval[0] + interval_size * (proc + 1);range[1] = range[1] == interval[1] - 1 ? interval[1] : range[1];return range;}inline void print_simple_range(int ibeg, int iend){// Прострейшая реализация определения простого числаbool res;for(int i = ibeg; i <= iend; i++){res = true;while(res){res = false;for(int j = 2; j < i; j++) if(i % j == 0) res = true;if(res) break;}res = not res;if(res) printf("Simple value ---> %d\n", i);}}void wait(int seconds) { // Функция ожидающая в течение seconds секундclock_t endwait;endwait = clock () + seconds * CLOCKS_PER_SEC ;while (clock() < endwait) {};}

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

Делаем прием данных более гибким

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

Первая процедура которую мы обсудим следующая:

int MPI_Get_count(MPI_Status* status, MPI_Datatype datatype, int* count);

По структуре status процедура определяет сколько данных типа datatype передано соответствующим сообщением и записывает результат по адресу count.

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

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

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

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

int MPI_Probe(int source, int tag, MPI_Comm comm, MPI_Status* status);

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

Теперь на очень простом примере соединим эти процедуры вместе:

#include <iostream>#include "mpi.h"using namespace std;void show_arr(int* arr, int size){for(int i=0; i < size; i++) cout << arr[i] << " ";cout << endl;}int main(int argc, char **argv){int size, rank;MPI_Init(&argc, &argv);MPI_Comm_size(MPI_COMM_WORLD, &size);MPI_Comm_rank(MPI_COMM_WORLD, &rank);if(rank == 0){int* arr = new int[size];for(int i=0; i < size; i++) arr[i] = i;for(int i=1; i < size; i++) MPI_Send(arr, i, MPI_INT, i, 5, MPI_COMM_WORLD);}else{int count;MPI_Status status;MPI_Probe(MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);MPI_Get_count(&status, MPI_INT, &count);int* buf = new int[count];MPI_Recv(buf, count, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);cout << "Process:" << rank << " || Count: " << count << " || Array: ";show_arr(buf, count);}MPI_Finalize();return 0;}

Что тут происходит?
В данной программе первый процесс создает массив размером равным количеству процессов и заполняет его номерами процессов по очереди. Потом соответствующему процессу он отправляет такое число элементов этого массива, какой номер у этого процесса. Напрмер: процесс 1 получит 1 элемент, процесс 2 получит 2 элемента этого массива и так далее.

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

Process:1 || Count: 1 || Array: 0 Process:2 || Count: 2 || Array: 0 1 Process:4 || Count: 4 || Array: 0 1 2 3 Process:3 || Count: 3 || Array: 0 1 2 

Собственно 4 результата потому что нулевой процесс занимается отправкой этих сообщений.

И еще несколько типов процедур посылки

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

Для того чтобы повысить степень определенности существуют еще несколько процедур которые никак не отличаются по синтаксису от MPI_Send, но отличаются по типу взаимодействия процессов и в способе отправки.

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

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

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


Резюме

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

Процедура/Константа/Структура

Назначение

MPI_Send

Отправка сообщения

MPI_Recv

Прием сообщения

MPI_Status

Структура статуса сообщения

MPI_ANY_SOURCE

Константа "Любому процессу"

MPI_ANY_TAG

Константа "Любой тег сообщения"

MPI_Get_count

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

MPI_Get_elements

Получить количество базовых элементов по статусу

MPI_Probe

Получить данные о сообщении без его приема

MPI_PROC_NULL

Константа-идентификатор не существующего процесса

MPI_Ssend

Отправка сообщения которая осуществляет синхронизацию процессов

MPI_Bsend

Отправка сообщения которая осуществляет буферизацию

MPI_Rsend

Отправка сообщения по готовности. Требует инициализации приема у процесса-получателя.

На этом пока все, приятного отдыха хабравчане :)

Подробнее..

Часть 3. MPI Как процессы общаются? Сообщения типа точка-точка

28.03.2021 00:13:26 | Автор: admin

В этом цикле статей речь идет о параллельном программировании с использованием MPI.

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


Предисловие

Практически все программы с применением технологии MPI используют не только средства для порождения процессов и их завершения, но и одну из самых важных частей, заложенных в названии самой технологии (Message Passing Interface), конечно же явная посылка сообщений между процессами. Описание этих процедур начнем, пожалуй, с операций типа точка-точка.

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

Работает это так: один из процессов, назовем его P1, какого-то коммуникатора C должен указать явный номер процесса P2, который также должен быть под коммуникатором С, и с помощью одной из процедур передать ему данные D, но на самом деле не обязательно нужно знать номер процесса, но это мы обсудим далее.

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

Оба типа процедур мы затронем подробно, а пока начнем с процедур с блокировкой.

Передаем и принимаем сообщения

Наконец приступим к практической части. Для передачи сообщений используется процедура MPI_Send. Эта процедура осуществляет передачу сообщения с блокировкой. Синтаксис у нее следующий:

int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest,  int msgtag, MPI_Comm comm);

Что тут есть что:
buf - ссылка на адрес по которому лежат данные, которые мы пересылаем. В случае массивов ссылка на первый элемент.
count - количество элементов в этом массиве, если отправляем просто переменную, то пишем 1.
datatype - тут уже чутка посложнее, у MPI есть свои переопределенные типы данных которые существуют в С++. Их таблицу я приведу чуть дальше.
dest - номер процесса кому отправляем сообщения.
msgtag - ID сообщения (любое целое число)
comm - Коммуникатор в котором находится процесс которому мы отправляем сообщение.

А вот как называются основные стандартные типы данных С++ определенные в MPI_Datatype:

Название в MPI

Тип даных в С++

MPI_CHAR

char

MPI_SHORT

signed short

MPI_INT

signed int

MPI_LONG

signed long int

MPI_LONG_LONG

signed long long int

MPI_UNSIGNED_*** (Вместо *** int и т.п.)

unsigned ...

MPI_FLOAT

float

MPI_DOUBLE

double

MPI_LONG_DOUBLE

long double

MPI_INT8_T

int8_t

MPI_INT16_T

int16_t

MPI_C_COMPLEX

float _Complex

Аналогичным способом указываются и другие типы данных определенные в стандартной библиотеке С/С++ - MPI_[Через _ в верхнем регистре пишем тип так как он назван в С]. Еще один пример для закрепления понимания, есть тип беззнаковых 32 битных целых чисел, назван он uint32_t, чтобы получить этот тип данных переопределенным в MPI необходимо написать следующую конструкцию: MPI_UINT32_T. То есть все вполне логично и легко, верхний регистр, вместо пробелов знаки андерскора и в начале пишем MPI.

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

Теперь поговорим о приеме этих сообщений. Для этого в MPI определена процедура MPI_Recv. Она осуществляет, соответственно, блокирующий прием данных. Синтаксис выглядит вот так:

int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status* status);

Что тут есть что:
buf - ссылка на адрес по которому будут сохранены передаваемые данные.
count - максимальное количество принимаемых элементов.
datatype - тип данных переопределенный в MPI(по аналогии с Send).
source - номер процесса который отправил сообщение.
tag - ID сообщения которое мы принимаем (любое целое число)
comm - Коммуникатор в котором находится процесс от которого получаем сообщение.
status - структура, определенная в MPI которая хранит информацию о пересылке и статус ее завершения.

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

MPI_Status status;MPI_Recv(&buffer, 1, MPI_Float, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status)tag = status.MPI_SOURCEsource = status.MPI_SOURCE

Здесь представлен кусочек возможного кода в процессе который принимает данные не более одного float элемента от любого процесса с любым тегом сообщения. Чтобы узнать какой процесс прислал это сообщение и с каким тэгом нужно собственно воспользоваться структурой MPI_Status.

Заметим появление констант MPI_ANY_SOURCE и MPI_ANY_TAG, они явно указывают, что можно принимать сообщения от любого процесса с любым тэгом.

Дабы закрепить знания об этих двух процедурах приведу пример как это выглядит на практике, данная программа определяет простые числа на заданном интервале:

#include <stdio.h>#include <time.h>#include "mpi.h"#define RETURN return 0#define FIRST_THREAD 0int* get_interval(int, int, int*);inline void print_simple_range(int, int);void wait(int);int main(int argc, char **argv){// инициализируем необходимые переменныеint thread, thread_size, processor_name_length;int* thread_range, interval;double cpu_time_start, cpu_time_fini;char* processor_name = new char[MPI_MAX_PROCESSOR_NAME * sizeof(char)];MPI_Status status;interval = new int[2];// Инициализируем работу MPIMPI_Init(&argc, &argv);// Получаем имя физического процессораMPI_Get_processor_name(processor_name, &processor_name_length);// Получаем номер конкретного процесса на котором запущена программаMPI_Comm_rank(MPI_COMM_WORLD, &thread);// Получаем количество запущенных процессовMPI_Comm_size(MPI_COMM_WORLD, &thread_size);// Если это первый процесс, то выполняем следующий участок кодаif(thread == FIRST_THREAD){// Выводим информацию о запускеprintf("----- Programm information -----\n");printf(">>> Processor: %s\n", processor_name);printf(">>> Num threads: %d\n", thread_size);printf(">>> Input the interval: ");// Просим пользователья ввести интервал на котором будут вычисленияscanf("%d %d", &interval[0], &interval[1]);// Каждому процессу отправляем полученный интервал с тегом сообщения 0. for (int to_thread = 1; to_thread < thread_size; to_thread++)       MPI_Send(&interval, 2, MPI_INT, to_thread, 0, MPI_COMM_WORLD);// Начинаем считать время выполненияcpu_time_start = MPI_Wtime();}// Если процесс не первый, тогда ожидаем получения данныхelse     MPI_Recv(&interval, 2, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);// Все процессы запрашивают свой интервалrange = get_interval(thread, thread_size, interval);// После чего отправляют полученный интервал в функцию которая производит вычисленияprint_simple_range(range[0], range[1]);// Последний процесс фиксирует время завершения, ожидает 1 секунду и выводит результатif(thread == thread_size - 1){cpu_time_fini = MPI_Wtime();wait(1);printf("CPU Time: %lf ms\n", (cpu_time_fini - cpu_time_start) * 1000);}MPI_Finalize();RETURN;}int* get_interval(int proc, int size, int interval){// Функция для рассчета интервала каждого процессаint* range = new int[2];int interval_size = (interval[1] - interval[0]) / size;range[0] = interval[0] + interval_size * proc;range[1] = interval[0] + interval_size * (proc + 1);range[1] = range[1] == interval[1] - 1 ? interval[1] : range[1];return range;}inline void print_simple_range(int ibeg, int iend){// Прострейшая реализация определения простого числаbool res;for(int i = ibeg; i <= iend; i++){res = true;while(res){res = false;for(int j = 2; j < i; j++) if(i % j == 0) res = true;if(res) break;}res = not res;if(res) printf("Simple value ---> %d\n", i);}}void wait(int seconds) { // Функция ожидающая в течение seconds секундclock_t endwait;endwait = clock () + seconds * CLOCKS_PER_SEC ;while (clock() < endwait) {};}

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

Делаем прием данных более гибким

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

Первая процедура которую мы обсудим следующая:

int MPI_Get_count(MPI_Status* status, MPI_Datatype datatype, int* count);

По структуре status процедура определяет сколько данных типа datatype передано соответствующим сообщением и записывает результат по адресу count.

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

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

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

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

int MPI_Probe(int source, int tag, MPI_Comm comm, MPI_Status* status);

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

Теперь на очень простом примере соединим эти процедуры вместе:

#include <iostream>#include "mpi.h"using namespace std;void show_arr(int* arr, int size){for(int i=0; i < size; i++) cout << arr[i] << " ";cout << endl;}int main(int argc, char **argv){int size, rank;MPI_Init(&argc, &argv);MPI_Comm_size(MPI_COMM_WORLD, &size);MPI_Comm_rank(MPI_COMM_WORLD, &rank);if(rank == 0){int* arr = new int[size];for(int i=0; i < size; i++) arr[i] = i;for(int i=1; i < size; i++) MPI_Send(arr, i, MPI_INT, i, 5, MPI_COMM_WORLD);}else{int count;MPI_Status status;MPI_Probe(MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);MPI_Get_count(&status, MPI_INT, &count);int* buf = new int[count];MPI_Recv(buf, count, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);cout << "Process:" << rank << " || Count: " << count << " || Array: ";show_arr(buf, count);}MPI_Finalize();return 0;}

Что тут происходит?
В данной программе первый процесс создает массив размером равным количеству процессов и заполняет его номерами процессов по очереди. Потом соответствующему процессу он отправляет такое число элементов этого массива, какой номер у этого процесса. Напрмер: процесс 1 получит 1 элемент, процесс 2 получит 2 элемента этого массива и так далее.

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

Process:1 || Count: 1 || Array: 0 Process:2 || Count: 2 || Array: 0 1 Process:4 || Count: 4 || Array: 0 1 2 3 Process:3 || Count: 3 || Array: 0 1 2 

Собственно 4 результата потому что нулевой процесс занимается отправкой этих сообщений.

И еще несколько типов процедур посылки

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

Для того чтобы повысить степень определенности существуют еще несколько процедур которые никак не отличаются по синтаксису от MPI_Send, но отличаются по типу взаимодействия процессов и в способе отправки.

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

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

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


Резюме

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

Процедура/Константа/Структура

Назначение

MPI_Send

Отправка сообщения

MPI_Recv

Прием сообщения

MPI_Status

Структура статуса сообщения

MPI_ANY_SOURCE

Константа "Любому процессу"

MPI_ANY_TAG

Константа "Любой тег сообщения"

MPI_Get_count

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

MPI_Get_elements

Получить количество базовых элементов по статусу

MPI_Probe

Получить данные о сообщении без его приема

MPI_PROC_NULL

Константа-идентификатор не существующего процесса

MPI_Ssend

Отправка сообщения которая осуществляет синхронизацию процессов

MPI_Bsend

Отправка сообщения которая осуществляет буферизацию

MPI_Rsend

Отправка сообщения по готовности. Требует инициализации приема у процесса-получателя.

На этом пока все, приятного отдыха, хабравчане.

Подробнее..

Разработчики встраиваемых систем не умеют программировать

02.05.2021 18:15:06 | Автор: admin

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

Редко когда речь заходит об обратной проблеме, имеющей место в куда более узких кругах разработчиков встраиваемых систем, включая системы повышенной отказоустойчивости. Есть основания полагать, что ранний опыт использования MCS51/AVR/PIC оказывается настолько психически травмирующим, что многие страдальцы затем продолжают считать байты на протяжении всей карьеры, даже когда объективных причин для этого не осталось. Это, конечно, не относится к случаям, где жёсткие ценовые ограничения задают потолок ресурсов вычислительной платформы (микроконтроллера). Но это справедливо в случаях, где цена вычислительной платформы в серии незначительна по сравнению со стоимостью изделия в целом и стоимостью разработки и верификации его нетривиального ПО, как это бывает на транспорте и сложной промышленной автоматизации. Именно о последней категории систем этот пост.

Обычно здесь можно встретить упрёк: "Ты чё пёс А MISRA? А стандарты AUTOSAR? Ты, может, и руководства HIC++ не читал? У нас тут серьёзный бизнес, а не эти ваши побрякушки. Кран на голову упадёт, совсем мёртвый будешь." Тут нужно аккуратно осознать, что адекватное проектирование ПО и практики обеспечения функциональной корректности в ответственных системах не взаимоисключающи. Если весь ваш софт проектируется по V-модели, то вы, наверное, в этой заметке узнаете мало нового хотя бы уже потому, что ваша методология содержит пункт под многозначительным названием проектирование архитектуры. Остальных эмбедеров я призываю сесть и подумать над своим поведением.

Не укради

Что, в конечном итоге, говорят нам вышеупомянутые стандарты в кратком изложении? Примерно вот что:

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

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

  • Делай свои намерения явными и избегай неявных предположений. Это касается проверки инвариантов, исключения платформно-зависимых конструкций, исключения UB, unsafe и схожих граблей, заботливо разложенных языком программирования и средой исполнения.

  • Не забывай об асимптотической сложности. Ответственные системы обычно являются системами реального времени. Адептов C++ призывают воздержаться от злоупотреблений RTTI и использования динамической памяти (хотя последнее к реальному времени относят ошибочно, потому что подобающим образом реализованные malloc() и free() выполняются за постоянное время и даже с предсказуемой фрагментацией кучи).

  • Не игнорируй ошибки. Если что-то идёт не так, обрабатывай как следует, а не надейся на лучшее.

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

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

Я имел несчастье ознакомиться с некоторым количеством встраиваемого ПО реального времени, к надёжности которого предъявляются повышенные требования, и в пугающем числе случаев я ощущал, как у меня шевелятся на голове волосы. Меня, например, сегодня уже не удивляет старая байка об ошибках в системе управления Тойоты Приус, или байка чуть поновее про Boeing 737MAX (тот самый самолёт, который проектировали клоуны под руководством обезьян). В нашем новом дивном мире скоро каждая первая система станет программно-определяемой, что (безо всякой иронии) здорово, потому что это открывает путь к решению сложных проблем затратой меньших ресурсов. Но с повальной проблемой качества системоопределяющего ПО нужно что-то делать.

Если взять типичную встраиваемую систему достаточно высокой сложности, можно выделить примерно следующие сгустки логики:

  • Класс-бог, отвечающий за всё сущее.

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

  • Utils или helpers, без них никуда.

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

Инфоцыгане

Косвенным образом масла в огонь подливают некоторые поставщики программных инструментов для разработчиков встраиваемого ПО: Mbed, Arduino, и т.п. Их маркетинговые материалы вполне могут заставить начинающего специалиста поверить, что суть этой работы заключается в низкоуровневом управлении железом, потому что именно на этом аспекте диспропорционально фокусируются упомянутые поставщики ПО. Вот у меня на соседнем рабочем столе открыт в CLion проект ПО для одной встраиваемой системы; проект собирается из чуть более чем ста тысяч строк кода. Из этой сотни примерно три тысячи приходятся на драйверы периферии, остальное приходится на бизнес-логику и всякий матан. Моя скромная практика показывает, что за исключением простых устройств сложность целевой бизнес-логики приложения несопоставима с той его частью, что непосредственно работает с железом.

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

Смотри, что я нашёл! Есть крутая новая система, Mbed называется, значит, для эмбедеров. Гляди, как можно быстро прототипы лепить! Клац, клац, и мигалка готова! Вот же, на видео. А ты, Илья, свой алгоритм оптимизации CAN фильтров пилишь уже неделю, не дело это, давай переходить на Mbed.

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

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

Когда один бэкэндер лучше двух эмбедеров

Ранее я публиковал большую обзорную статью о нашем открытом проекте UAVCAN (Uncomplicated Application-level Vehicular Computing And Networking), который позволяет строить распределённые вычислительные системы (жёсткого) реального времени в бортовых сетях поверх Ethernet, CAN FD или RS-4xx. Это фреймворк издатель-подписчик примерно как DDS или ROS, но с упором на предсказуемость, реальное время, верификацию, и с поддержкой baremetal сред.

Для организации распределённого процесса UAVCAN предлагает предметно-ориентированный язык DSDL с помощью которого разработчик может указать типы данных в системе и базовые контракты, и вокруг этого затем соорудить бизнес-логику. Это работает примерно как REST эндпоинты в вебе, XMLRPC, вот это вот всё. Если взять одного обычного бэкендера человека, измученного сервис-ориентированным проектированием и поддержкой сложных распределённых комплексов и объяснить ему суть реального времени, то он в короткие сроки начнёт выдавать хорошие, годные интерфейсы на UAVCAN.

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

Допустим, ответ подопытного является вариацией на тему "измерение воздушной скорости, барометрической высоты и статического давления". Тогда на свет появляются примерно следующие строки DSDL:

# Calibrated airspeeduavcan.time.SynchronizedTimestamp.1.0 timestampuavcan.si.unit.velocity.Scalar.1.0    calibrated_airspeedfloat16                               error_variance
# Pressure altitudeuavcan.time.SynchronizedTimestamp.1.0 timestampuavcan.si.unit.length.Scalar.1.0      pressure_altitudefloat16                               error_variance
# Static pressure & temperatureuavcan.time.SynchronizedTimestamp.1.0 timestampuavcan.si.unit.pressure.Scalar.1.0    static_pressureuavcan.si.unit.temperature.Scalar.1.0 outside_air_temperaturefloat16[3] covariance_urt# The upper-right triangle of the covariance matrix:#   0 -- pascal^2#   1 -- pascal*kelvin#   2 -- kelvin^2

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

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

В БПЛА некоторых типов практикуется автокалибровка, где выборка измерений с приёмника воздушного давления за значительный промежуток времени сопоставляется с наземной скоростью, чтобы таким образом определить калибровочные параметры эмпирически. В рамках сервис-ориентированного подхода это решается превращением системы воздушных сигналов в потребителя данных о наземной скорости. Проще говоря, наш сетевой узел, что измеряет воздушную скорость, подписывается на топик, в котором есть данные о наземной скорости летательного аппарата, получая таким образом доступ к необходимому контексту для выполнения автокалибровки.

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

uint16 differential_pressure_readinguint16 static_pressure_readinguint16 outside_air_temperature_reading

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

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

Художника каждый может обидеть

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

Коллеги, одумайтесь.

Я вижу, как нашим микроскопом заколачивают ржавые гвозди, и представляю, сколько ещё подобного происходит за пределами моего поля зрения. В прошлом году уровень отчаяния в нашей скромной команде был столь высок, что мы опубликовали наноучебник, где объясняется, как выглядит сетевой сервис здорового человека: UAVCAN Interface Design Guidelines. Это, конечно, капля в море, но в один прекрасный день я всё-таки переведу его на русский язык ради подъёма уровня профессиональной грамотности.

Непонимание основ организации распределённых вычислений затрудняет внедрение новых стандартов на замену устаревших подходов. Наши наработки в рамках стандарта DS-015 (созданного в коллаборации с небезызвестными NXP Semiconductors и Auterion AG) встречают определённое сопротивление ввиду своей непривычности для целевой аудитории, в то время как ключевые принципы, на которых они основаны, известны индустрии информационных технологий уже не одно десятилетие. Этот разрыв должен быть устранён.

Желающие принять участие в движении за архитектурную чистоту и здравый смысл могут причаститься в телеграм-канале uavcan_ru или на форуме forum.uavcan.org.

Подробнее..

Что нужно для самовосстановления удаленных рабочих мест?

25.03.2021 14:07:14 | Автор: admin
How close is the reality of self-repairing endpoints?How close is the reality of self-repairing endpoints?

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

Хотим мы этого или нет, но мир постепенно двигается к действительно распределенным инфраструктурам. Если до 2020 года речь шла о мобильных устройствах и BYOD, то с началом пандемии лидерами по подключению к корпоративным сетям стали домашние компьютеры.

В одном из прошлых постов (ссылка) мы говорили о результатах исследования Acronis Cyber Readiness Report. Отчет показал, что более трети всех компаний в мире столкнулись с проблемой подключения и управления новыми устройствами. И проблема обеспечения надежной защиты данных на этих часто вообще никак не сконфигурированных для рабочих процессов компьютеров, является не единственной. Оказалось, что обеспечить постоянную доступность удаленного рабочего места тоже непросто особенно с учетом явного смещения фокуса внимания киберпреступности на удаленных работников.

По данным исследования Acronis Cyber Threats 2020 (ссылка), За прошлый год 31% компаний были атакованы каждый день, причем версии вредоносного ПО постоянно обновлялись среднее время жизни одного экземпляра не превышало 4 дня. Подобное положение дел создает реальный риск взлома хотя бы одной системы из корпоративного периметра (который давно уже не периметр, а решето). При этом аналитики из Aberdeen сообщают, что стоимость простоя рабочих мест на протяжении часа даже для СМБ может составлять до $8,600 в час. В случае с крупным бизнесом все еще хуже при неудачном стечении обстоятельств, простой может обойтись в сотни тысяч долларов в час по мнению аналитиков Gartner.

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

Интеграция компонентов играет ключевую роль

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

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

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

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

Патчи и самовосстановление

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

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

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

Как это будет работать на практике?

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

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

Подробнее..

Азбука libp2p от Textile (или за что мы её любим)

05.04.2021 14:09:20 | Автор: admin

Перевод статьи начального уровня в блоге проекта Textile от 19 ноября 2019 г.

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

Стремительно нарастающая сложность

Внедрить распределённое (p2p) взаимодействие в какое бы то ни было приложение - задача не из простых. Попредставляйте лишь пару минут, как должно работать ваше творение - и всё начинает усложняться прямо на глазах. Рассмотрим две основные проблемы для p2p-приложений: состояние приложения и инфраструктура взаимодействия. Управление текущим состоянием системы не тривиально. Нет центрального органа, который бы его определял. Состояние системы - производное от состояний множества других узлов, которое имеет взрывную сложность в ненадежных сетях и сложных протоколах. Что касается инфраструктуры связи, ваше приложение должно взаимодействовать со многими равноправными узлами, поэтому вам придется столкнуться с изрядным количеством проблем. Поразмыслите на следующие темы касательно удалённых узлов:

  • У них ненадежное оборудование и сеть.

  • У них неопределённые технические параметры, такие, как вычислительная мощность и доступный объём долговременной памяти.

  • Брандмауэры могут блокировать их либо они могут находиться в сетях с NAT.

  • Они могут использовать старые версии приложений.

Прежде, чем вы начнёте писать свою первую строку кода, вы можете просто замёрзнуть из-за огромного объёма работы, которая вам предстоит. Даже если вам удастся выпустить первую версию приложения, - насколько легко затем будет что-то изменить без особых разрушений вокруг? Не правда ли, было бы неплохо иметь некую библиотеку, которая облегчила бы нашу задачу?!

Libp2p приходит на помощь!

Libp2p - библиотека, появившаяся в результате работы Protocol Labs над IPFS. Если вы хоть немного следите за нашим блогом, то знаете, что мы их большие поклонники! Когда вы берётесь за написание p2p-приложения производственного уровня с нуля, то понимаете, что создаёте не только приложение, но и его инфраструктуру - и до вас очень быстро доходит, что требуется дополнительно изобрести ещё кучу колёс. Не весело, однако. С другой стороны, libp2p позволяет вам встать на плечи неких не хилых гигантов, дабы уменьшить сложность инфраструктуры - и чтобы вы могли сосредоточиться на бизнес-логике. А вот это уже веселее! Конечно, libp2p - не панацея в борьбе со всеми нашими p2p-чудищами, но она определённо облегчает бремя реализации инфраструктуры взаимодействия.

Основные концепции Libp2p

Сердцевиной libp2p является объект Хост (Host), который представляет наш локальный узел в сети p2p. Общее описание его компонентов:

  • Идентификатор, по которому наш узел опознаётся другими узлами.

  • Набор локальных адресов, по которым к нам можно обращаться.

  • Журналы сведений о других узлах: об их идентификаторах, ключах, адресах и т. д.

  • Сетевой интерфейс для управления соединениями с другими узлами.

  • Muxer, который позволяет единичному соединению работать с несколькими протоколами (подробнее об этом позже).

Следующая базовая абстракция в libp2p - это Потоки. Поток (Stream) - это канал прямой связи с другим узлом. Важно понимать разницу между Stream и "грубым", "сырым" ("raw") сетевым протоколом, таким как TCP или UDP - ибо здесь открывается вся мощь libp2p: сами по себе сетевые протоколы - лишь средства отправки байтов по сети. Если вам нужна высокая надежность доставки пакетов, вы можете использовать TCP, в других случаях UDP может оказаться предпочтительнее. Короче, сами сетевые протоколы не заботятся о том, какие данные передают; для них это просто байты.

Stream же - это поточный канал связи между двумя одноранговыми узлами, имеющий определенную семантику. Тут уже не просто байты, но байты, соответствующие протоколу (более высокого уровня), определённому разработчиком. Протокол этот помечается идентификатором, например /sumtwointegers/v1.0.0. А сам Stream - диалог в рамках данного протокола. К примеру, одна сторона отправляет два целых числа, а другая отвечает их суммой. Вот что мы подразумеваем под потоком байтов с семантикой.

Потоки работают поверх сетевых протоколов, таких как TCP или UDP - по сути, идея состоит в том, чтобы отделить p2p-коммуникацию от сетевых протоколов (более низкого уровня). Нам нужно думать лишь о прикладном использовании поточного канала - для отправки значимой информации, а гибкость работы на любом сетевом протоколе, доступном между узлами, уже заложена в нём (в Stream). Это действительно здорово и мощно. И это также означает, что мы можем оптимизировать функционал раздельно на каждом уровне стека. Нужна лучшая реализация сетевого протокола? Отлично, оставьте это libp2p. Более выразительный семантически дизайн протокола p2p? Круто, это тоже!

Более того, отделение семантики p2p от базовых сетевых протоколов позволяет libp2p пойти дальше и мультиплексировать Потоки в одном и том же сетевом протоколе. Мы можем использовать одно и то же TCP-соединение для многих Потоков. Мы можем запустить протокол умножения двух целых чисел в том же соединении, в котором работали наши протоколы суммы двух целых чисел. Но Streams не обязаны справляться с этим самостоятельно. В действительности Libp2p полагается на мультиплексоры ("Muxers") для исполнения сей магии. Задача мультиплексора - разделить данные (последовательности байтов) на соответствующие потоки внутри одного потока сетевого протокола более низкого уровня. Вот краткая диаграмма, которая немного поясняет сказанное.

Как мы видели выше, у нашего Хоста есть Muxer, который мультиплексирует (объединяет) множество потоков одного узла в одном и том же соединении. Можно сказать, что Максер предваряет сообщения разных потоков некоторыми идентификаторами, чтобы принимающая сторона могла идентифицировать байты разных потоков. Конечно, используемый "нижележащий" сетевой протокол может ограничивать некоторые аспекты мультиплексирования, например, блокировку заголовка. Или же, напротив, сам транспорт (нижележащий сетевой протокол) может включать в себя полностью реализованный механизм мультиплексирования (как, например, QUIC). Но это уже другой разговор...

Возвращаясь на шаг назад

Давайте попробуем отделить то, о чем мы должны думать при написании приложения, от того, что libp2p делает для нас. Со своей стороны (разработчиков) мы хотим иметь ясное представление обо всех вещах, стараясь как можно больше отделить логику нашего приложения от концепций инфраструктуры. Нам следует думать о взаимодействии узлов, а не об адресах. И затем просто реализовать задуманное взаимодействие на уровне абстракции Stream.

На заднем плане libp2p выполняет тяжелую работу по соединению с удалённым узлом, используя информацию, хранящуюся в адресной книге. Она старается определить, какой сетевой протокол оба узла понимают, чтобы установить сетевое соединение. Ловко, однако!

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

Зачем так стремиться к множественному использованию одного соединения? Может статься, что наши p2p-приложения должны будут работать в очень ограниченных сетевых средах, где существуют брандмауэры, NAT и ограничения на количество подключений. Поэтому, после того, как мы установим соединение, мы должны выжать из него максимальную пользу! При переходе на образ мышления p2p, вы можете встретить много неизвестного, поэтому использование libp2p поможет вам сосредоточиться на хорошем дизайне, даже если вы не являетесь экспертом.

Но это ещё не всё!

Libp2p разрабатывалась как модульная библиотека. Такой дизайн упрощает изменения в реализации отдельных компонентов без влияния на остальные. Я думаю, что рациональность такого подхода - помимо хорошей инженерной практики - даёт подтверждение разнообразия сред, в которых будет работать наше приложение. И вы можете держать пари, что так оно и есть в приложениях p2p. Наша программа будет способна работать в самых разных средах выполнения, с разными возможностями или ограничениями сети. Кроме того, развитие вашего приложения при сохранении обратной совместимости со старыми версиями программы не является тривиальным делом.

По этой причине, когда два приложения libp2p впервые общаются друг с другом, большая часть их работы заключается в определении того, какой совместимостью они обладают, дабы достичь успеха в коммуникации. Надо выяснить, каким сетевым транспортом оба собеседника пользуются (TCP, UDP, QUIC, Websockets), какие версии протокола мы можем обрабатывать (/myprotocol/v1.0.0, /myprotocol/v1.1.0), какие реализации мультиплексора можем использовать, надо согласовать параметры безопасности, и т. д. И если этого было недостаточно, libp2p имеет ещё целый ряд встроенных протоколов для решения самых разных повседневных задач p2p-приложений, таких как:

  • Обход NAT: это больное место у p2p-приложений

  • Обнаружение узлов в сети: действительно, как вы обнаруживаете новые узлы вне централизованной модели?

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

  • И многое, многое другое!

Небольшой бонус на заметку: Libp2p очень быстро растёт, поэтому вы можете ожидать появления новых мощных инструментов, которые можно будет использовать с небольшой добавкой собственного кода. Помните, что приложения p2p предполагают разнообразие, поэтому libp2p будет иметь в разных языковых реализациях свои особенности. Скажем, некоторые реализации мультиплексора могут быть недоступны в JS, но вполне себе - в Go или Rust.

Подведём итоги

Отныне есть надежда, что мы не будем обескуражены необходимостью вновь изобретать колеса в нашем приложении. Мы можем сосредоточиться на создании прикладного функционала, а libp2p доверить тянуть инфраструктурную рутину. Более того, дальнейшие усовершенствования в libp2p будут улучшать и наше приложение, не затрагивая его бизнес-логику.

Ну, пока что всё. Следите за обновлениями в нашем следующем эпизоде, где мы переведем некоторые из этих концепций в код на практическом примере. Кроме того, если вам интересны такие вещи или вы хотите интегрировать p2p-коммуникацию в свое приложение или проект, свяжитесь с нами, и давайте поэкспериментируем вместе! Вы также можете попробовать одну из облегченных библиотек, если вам нужен простой способ использования однорангового узла libp2p в браузере, iOS, Android и/или на рабочем столе. Наконец, если вам нравятся такого типа обзорные статьи, сообщите нам об этом или запросите дополнительные темы ... а тем временем, удачного кодирования!

Автор оригинального текста: Ignacio Hagopian

Перевод: Алексей Силин (StarVer)

Подробнее..

Перевод Азбука libp2p от Textile, часть 2

25.04.2021 14:23:00 | Автор: admin

Переводстатьиначального уровня в блоге проектаTextileот 12 декабря 2019 г.

Впредыдущей статьемы начали с вопроса: Как подойти к своему первому p2p-приложению? После недолгих размышлений мы быстро пришли к выводу, что решениене полагаться на централизованный сервери сосредоточиться на том, чтобы сделать приложение дляравноправных узлов, сопряжено с множеством дополнительных сложностей. Две основные группы проблем - этосостояние приложенияиинфраструктурное разнообразие протоколов. К счастью, мы обнаружили, что нам не нужно изобретать велосипед, заново решая груду инфраструктурных задач - вместо того мы можем использовать великолепный сетевой p2p-стек: библиотеку libp2p.

В сегодняшнем посте мы пойдем немного дальше и представим игрушечное приложение, чтобы почувствовать, как на самом деле можно что-то разрабатывать с помощью libp2p, и, надеюсь, мотивировать вас создать собственное p2p-приложение. Серьезно, вы удивитесь, насколько это просто!

Приложение

Сразу оговоримся, наша программа нынче будет написана на языкеGo, с использованием библиотекиgo-libp2p. Если вы ещё не знакомы с этим языком, настоятельно рекомендуем ознакомиться. Он действительно хорош для приложений, имеющих дело с параллелизмом и сетевыми взаимодействиями (такими, как например, обработка множества p2p-соединений). Большинство библиотек IPFS/libp2p имеют свои базовые реализации, написанные на Go. Прекрасным введением в Go являетсятур на golang.org.

Итак, наша программка будет простым приложением для пинг-понга с некоторыми добавочными настройками, чтобы сделать её более интересной, в отличие от обычных безыскусных примеров. Вот некоторые особенности нашего приложения (не волнуйтесь, мы расскажем подробней об этих пунктах позже):

  • По умолчанию приложение цепляется к свободному TCP-порту.

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

  • Узел будет использовать службу mDNS для обнаружения новых узлов в локальной сети.

  • На каждом вновь обнаруженном узле (скажем, узле A) наше приложение будет запускать собственный протокол sayMyAddr (мы его реализуем), который будет узнавать для нас предпочтительный адрес для игры в пинг-понг этого узла.

  • Мы подключаемся к узлу А, используя предпочитаемый им адрес - и запускаем танец Пинг-Понг. Другими словами, мы запустим ещё один наш самопальный протокол, посредством которого отправим сообщение Ping, а узел A ответит нам сообщением Pong. Круть!

Даже для такой простой системы (если мы хотим сделать p2p-приложение) потребуется принять ряд отдельных решений. Для начала, надо будет ответить на следующие вопросы:

  • Какой транспортный протокол (TCP, QUIC и т.п.) использовать?

  • Какой механизм обнаружения других узлов в сети (например, mDNS) применить - то есть, как мы узнаем о других узлах, использующих наше приложение?

  • Как наши собственные протоколы (Streams) будут работать? - то есть, как мы будем поддерживать двунаправленную связь с другими узлами?

Решения этих вопросов независимы друг от друга, и, к счастью, модульность libp2p прямо-такизаставляетнас избегать их объединения. Что ж, плюс один за хороший дизайн библиотеки!

Ныряем в код!

Рекомендуем вам сразу же начать играться с кодом приложения. Он содержит массу комментариев, чтобы помочь читателю впутешествии понимания!Кроме того, здесь мы набросаем общий обзор, который может стать хорошей преамбулой для подробного прочтения кода. Не стесняйтесь клонировать репо себе на компьютер, дабы и вы могли запачкать руки:

КОД:ВДЕЛИТЬ ВСЁ

git clone git@github.com:textileio/go-libp2p-primer-article.gitcd go-libp2p-primer-articlecode . // Нам нравится VSCode, ну а вы - сами с усами ;)

Далее: начнём сmain.go, где вы можете лицезреть, как создаётся и запускается хост libp2p. Дополнительно здесь мы указываем, какие сетевые транспортные протоколы будет поддерживать наш хост. Заметьте, что если для флага -quic установлено значение true, мы добавляем новую привязку для транспорта QUIC. Добавление в работу транспортных протоколов сводится к простому добавлению параметров в конструктор хоста! Также обратите внимание, что мы регистрируем здесь все обработчики наших собственных протоколов: RegisterSayPreferAddr и RegisterPingPong. Наконец, мы регистрируем встроенную службуmDNS.

Теперь заглянем вdiscovery.go, где у нас находится настройка mDNS. Здесь, по сути, надо определить частоту широковещательной рассылки mDNS и строковый идентификатор, который в нашем случае не требуется и потому пустой. Последний шаг здесь - регистрация обработки уведомленияdiscovery.Notifee, которая будет вызываться всякий раз, когда mDNS запускает обнаружение пиров, предоставляя нам их информацию. Логика у нас тут будеттакая:

  1. Если мы уже знаем об этом узле - ничего не делаем; мы уже играли в пинг-понг. Иначе же

  2. открываем поток нашего протокола SayPreferAddr, чтобы узнать у обнаруженного узла, по какому адресу (addr) он предпочитает играть в пинг-понг. Ну, и наконец

  3. добавляем адрес узла в нашу адресную книгу, закрываем текущее соединение с ним и запускаем наш второй протокол PingPong, который повторно подключится к узлу, используя его предпочитаемый адрес (который мы добавили на предыдущем шаге).

Наконец, вpingpong.goмы можем увидеть упомянутый ранее метод RegisterPingPong, вызываемый из main.go, и еще два метода:

  • Handler: этот метод будет вызываться, когда сторонний узел зовёт нас играть в PingPong. Вы можете думать о Handler как об обработчике HTTP REST. В этом обработчике мы получаемStream, реализующий io.ReadWriteCloser, из которого мы можем запускать наш протокол для отправки и получения информации, чтобы делать что-то полезное.

  • playPingPong: Это другая сторона медали; клиент запускает новыйStreamдля внешнего узла для запуска протокола PingPong.

Как видите, реализация своих протоколов довольно-таки проста и полностью абстрагирована от других, инфраструктурных задач. Единственное, о чем нам нужно позаботиться, так это о написании полезного для нашего проекта прикладного кода. Заметьте также, что добавление нового протокола, например, вsaymyaddr.go, очень похоже на pingpong.go.

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

Чтобы протестировать нашу демо-программу, можно открыть два терминала и просто запустить: go run * .go , go run * .go -quic или их комбинации. Ниже вы можете видеть иллюстрацию с двумя терминалами, работающими с флагом -quic:

Обратите внимание, как, сразу после запуска, узел в нижнем терминале обнаруживает узел в верхнем, ибо mDNS немедленно находит существующие узлы. Затем "нижний" сразу переходит к игре в пинг-понг. "Верхний" узел тоже, но с определённой задержкой (из-за 5-секундного интервала, который мы установили для нашей службы mDNS) обнаружит "нижний" собственными средствами, что, в свою очередь, вызоветновуюигру в пинг-понг.

Заметим также, что когда каждая из сторон отправляет сообщение PingPong или отвечает на него, она выдает полную информацию о мульти-адресе (multiaddr), на который обращается, где можно увидеть, что используется протокол QUIC. Попробуйте запустить этот примербезфлага -quic для обоих партнеров и посмотрите, как это повлияет на результат!

И ещё отметим для себя, что если запустить один терминал с флагом -quic, а другой - без него, последний партнер не сможет играть в PingPong с первым, потому что у него не включена поддержка QUIC. В более реалистичном сценарии вы могли бы использоватьвседоступные вам адреса узла-собеседника, чтобы иметь больше возможностей общаться в рамках базового транспортного протокола, которыйобапонимают. Изящно, не правда ли?

Что дальше?

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

КОД:ВДЕЛИТЬ ВСЁ

const (    protoPingPong = "/pingpong/1.0.0")...func RegisterPingPong(h host.Host) {    pp := &pingPong{host: h}    // Здесь мы регистрируем наш _pingpong_ протокол.    // В будущем, если решите достраивать/исправлять ваш протокол,    // вы можете либо делать текущую версию обратно совместимой,    // либо зарегистрировать новый обработчик,     // с указанием новой главной версии протокола.    // Если хотите, можете также использовать логику semver,    // см. здесь: http://bit.ly/2YaJsJr    h.SetStreamHandler(protoPingPong, pp.Handler)}

Комментарии прекрасно всё объясняют.

Другой важный вопрос связан с механизмом обнаружения других узлов, в нашем случае это mDNS. Этот протокол делает свою работу в локальных сетях, но как насчет обнаружения пиров в Интернете? Позднее вы можете добавить в своё приложениеKademlia DHTили использовать один из механизмовpubsub- также, чтобы находить новые узлы.

Что здесь особо ценно, так это то, что будущие доработки не заставят вас переписывать существующую реализацию.Лёгкость изменения приложения - признак хорошего дизайна, а это значит, что libp2p также помогает выстраивать наш код в верном ключе.Спасибо за это разработчикам libp2p!

Заключительные слова

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

Важно: имейте также в виду, если вы используете libp2p с включенными Go-модулями, вам нужно явно указывать тег версии в go get, поскольку иначе вы можете получить не то, что ожидали по умолчанию. Больше информации об этом вы можете найти в секцииUsagereadme-файла go-libp2p.

Надеемся, что вам понравился наш игрушечный проект, и надеемся, что он вселит в вас уверенность в том, что писать p2p-приложения не так сложно, как могло бы показаться! На самом-то деле, это может быть довольно кайфово и вдохновляюще! Если вам по нраву сей материал, присоединяйтесь к нам нанашем канале в Slack, чтобы пообщаться на тему p2p-протоколов, или подписывайтесь на нас вTwitter, чтобы узнавать о самых свежих и славных разработкахTextile. Ну, или хотя бы гляньте некоторыедругие наши статьи и демонстрации, чтобы полнее прочувствовать, что возможно в этом захватывающем мире приложений P2P!

Автор оригинального текста:Ignacio Hagopian

Перевод: Алексей Силин (StarVer)

Подробнее..

Перевод Инженерная надежность и отказоустойчивость распределенной системы

08.06.2021 16:18:29 | Автор: admin

Это гостевая публикация отПэдди Байерса (Paddy Byers), сооснователя и технического директораAbly платформы для стриминга данных в реальном времени. Оригинал статьи опубликован вблоге Ably.

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

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

Для начала дадим несколько определений:

Надежность cтепень того, насколько пользователи могут положиться на продукт или сервис для решения своих задач.Доступностьиустойчивостьявляются видами надежности.
Доступность степеньготовностипродукта или сервиса к эксплуатации по требованию. Это понятие часто сcодят к обеспечению необходимого излишка ресурсов с учетомстатистически независимых отказов.
Устойчивость cпособность продукта или сервисасоответствовать заявленным характеристикамв процессе использования. Это значит, что система не просто готова к эксплуатации: благодаря дополнительным мощностям, предусмотренным в ходе проектирования,она может продолжать работать под нагрузкой, как и ожидают пользователи.
Отказоустойчивость способность системы сохранятьнадежность(доступность и устойчивость) при отказе отдельных компонентов или сбоях в подсистемах.

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

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

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

Доступность, устойчивость и состояние компонентов системы

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

В физическом мире традиционно различают:

  • ситуации, угрожающие доступности, когда можно остановить и затем возобновить работу сервиса например, остановить автомобиль, чтобы сменить покрышку;

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

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

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

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

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

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

Отказы неизбежны и естественны

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

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

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

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

Сервисы без сохранения состояния

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

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

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

Таким образом, надо задать себе следующие вопросы:

  • Как сохранить работоспособность системы после разных типов отказов?

  • Какой уровень избыточности возможно обеспечить?

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

  • Каких эксплуатационных расходов требует управление этим уровнем избыточности?

Выбранное решение должно удовлетворять следующим критериям:

  • Требования клиентов к высокой доступности сервиса

  • Уровень эксплуатационных расходов для бизнеса

  • Инженерная целесообразность

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

Чтобы увеличить степень статистической независимости отказов в Ably,мы распределили мощности по множественным зонам доступности в разных регионах. Оказывать услуги из нескольких зон доступности в одном регионе проще AWS делает это практически без усилий. Зоны доступности в целом хорошо позволяют поддерживать независимость отказов, но их создание само по себе предполагает обеспечение необходимой избыточности для поддержания высокого уровня доступности.

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

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

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

Сервисы с сохранением состояния

В Ablyустойчивость определяется бесперебойной работойсервисов с сохранением состояния, и добиться этого намного сложнее, чем просто обеспечить доступность.

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

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

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

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

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

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

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

Когда клиент отправляет сообщение в Ably, сервис принимает его и уведомляет, была попытка публикации успешной или нет. В этом случае главный вопрос в контексте обеспечения доступности будет таким:

В течение какой доли времени сервис может принимать (и обрабатывать) сообщения, а не отклонять их?

Минимальный целевой показатель доступности для нас 99,99; 99,999 или даже 99,9999%.

Если вы попытались опубликовать сообщение и мы уведомили вас, что попытка не удалась, это говорит онехватке доступности. Это плохо, но, по крайней мере, вы в курсе ситуации.

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

Архитектурный подход к обеспечению устойчивости

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

Размещение роли с сохранением состояния

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

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

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

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

Это становится возможным благодаря последовательному алгоритму размещения в хеш-кольце

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

Выявить, хешировать, продолжить

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

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

Слой обеспечения постоянства канала

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

Во-первых, данные о приеме сообщения записываютсякак минимум в двух разных зонах доступности. Во-вторых, к этим зонам доступности предъявляются те же требования избыточности при последующем процессе обработки данных. Это главный принцип работы слоя обеспечения постоянства в Ably: мы записываем сообщение в нескольких местах и следим за тем, чтобы процесс записи был транзакционным. Мы прекращаем его, только убедившись, что попытка записи быланеудачнойилиоднозначно успешной. Зная это, мы можем быть уверены, что дальнейшая обработка будет произведена даже при отказе ролей, ответственных за этот процесс.

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

Благодаря нашей математической модели при отказе узла мы точно знаем, сколько времени уйдет на выявление причины сбоя, достижение консенсуса и последующего перемещения роли. Эти данные вкупе с частотой отказов для каждой зоны доступности позволяют создать вероятностную модель возникновения комплексного сбоя, который может привести к потере текущего состояния сервиса. Описанные базовые принципы позволяют нам гарантироватьустойчивость работы сервиса с вероятностью 99,999999%.

Вопросы внедрения отказоустойчивости

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

Достижение консенсуса в глобально распределенных системах

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

Это классическая проблема достижения консенсуса: члены кластера, каждый из которых сам может пережить отказ, должны договориться о статусе одного из них. Протоколы достижения консенсуса, такие какRaft или Paxos, широко известны и имеют прочную теоретическую базу. Однако на практике в них существует ряд ограничений, касающихся масштабирования и ширины канала связи. В частности, они не эффективны в сетях, которые охватывают множество регионов, потому что их производительность падает, как только задержка при передаче данных между одноранговыми узлами становится слишком большой.

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

Работоспособность не определяется двумя состояниями

Классическая теория, которая легла в основу разработки протоколов Paxos и Raft, базируется на том, что при отказе или проблемах с работоспособностью сущности не просто сбоят или перестают отвечать на запросы. Вместо этого они могут выдавать отдельные симптомы неполадок, такие как отклонение запросов, высокий процент ошибок или в целом нестабильное и некорректное поведение (см.Задача византийских генералов). Эта проблема стоит очень остро и касается даже взаимодействия клиентов с сервисом.

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

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

Проблема доступности ресурсов

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

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

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

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

Проблема масштабирования ресурсов

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

Тривиальные механизмы обеспечения отказоустойчивости могут демонстрировать сложность O(N) или даже хуже, и это необходимо учитывать при поиске решения проблемы. Это еще одно напоминание о том, что отказ может произойти не только в результате поломки его причиной может стать слишком большой масштаб, сложность инфраструктуры и другие факторы нерационального использования ресурсов.

Заключение

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

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

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

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

Сервис Ably включает множество слоев, на каждом из которых применяется целый ряд механизмов обеспечения отказоустойчивости. Перечень инженерных проблем, с которыми мы столкнулись, включал в числе прочего размещение ролей с сохранением состояния, выявление отказов, хеширование и возможность постепенного восстановления сервиса. Мы также гарантируем непрерывность работы сервиса на слое обеспечения постоянства: например, то, что дальнейшая обработка сообщения будет продолжена, после того как мы подтвердили его получение.

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

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


Перевод материала подготовлен в рамках курса "Highload Architect". Всех желающих приглашаем на день открытых дверей онлайн, где вы сможете узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса. Регистрация здесь

Подробнее..

Сравнение криптографической производительности популярных ARM-процессоров для DYI и Edge-устройств, плюс Xeon E-2224

13.04.2021 10:07:06 | Автор: admin

В одном из наших проектов используется Edge-модуль, работающий на широком наборе оборудования c процессором ARM, типа Raspberry Pi. Данное устройство используется для того, чтобы пересылать медиа-данные посредством зашифрованного канала на сервер.

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

На тесте были:

  • Nvidia Jetson Nano - 4 core ARM A57 @ 1.43 GHz

  • Raspberry Pi 4, Model B - Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz

  • Raspberry Pi 3, Model B+ - Broadcom BCM2837B0, Cortex-A53 (ARMv8) 64-bit SoC @ 1.4GHz

  • Orange Pi Zero LTS - AllWinner H2 Quad-coreCortex-A7

и, чтобы им не скучно было, к тестированию подключил Intel Xeon E-2224, дабы возникло понимание в сравнительных возможностях ARM vs Intel.

На Jetson Nano установлено активное охлаждение с помощью вентилятора, на других платформах просто радиатор.

Все процессоры 4х-ядерные, SMT нигде нет. Сравнение производилось в рамках однопоточного теста с помощью стандартных возможностей OpenSSL (OpenSSL 1.1.1 11 Sep 2018).

Тест алгоритмов семейства SHA

openssl speed sha

Победителем теста среди ARM оказался Nvidia Jetson Nano, причем для sha-256/16KB он обогнал даже Xeon E-2224 - я повторно провел данный бенчмарк для Xeon E-2224, результат остался тем же.

Сравнительные данные в таблицах ниже ранжированы по месту в рейтинге производительности.

Nvidia Jetson Nano

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytessha1             73350.09k   200777.04k   416181.08k   573274.72k   645373.43k   653034.35ksha256           68908.76k   188685.90k   412290.48k   568202.87k   644962.46k   651681.85ksha512           19732.45k    78505.91k   122326.65k   175421.47k   201366.51k   202794.47k

Raspberry Pi 4

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytessha1             40358.89k   103684.42k   199123.37k   258472.96k   283866.45k   285665.96ksha256           27360.34k    65673.69k   120294.66k   151455.74k   164413.44k   165281.79ksha512           10255.33k    40882.35k    60587.95k    83416.41k    94066.01k    94874.28k

Raspberry Pi 3

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytessha1             19338.74k    52534.42k   105558.02k   140777.13k   156311.55k   157537.62ksha256           12821.65k    31949.78k    59951.62k    77581.99k    84858.20k    85415.25ksha512            7444.83k    29450.71k    47035.65k    66549.76k    75893.42k    76660.74k

Orange Pi Zero

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytessha1              9313.16k    23691.09k    45304.83k    58655.40k    64249.86k    64684.03ksha256            6051.17k    14204.69k    25856.60k    32542.38k    35198.29k    35400.36ksha512            3319.25k    13320.17k    19863.55k    27670.87k    31290.71k    31582.89k

Вне конкурса: Intel Xeon E-2224

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytessha1            171568.52k   420538.79k   843694.68k  1124105.90k  1259615.57k  1257401.00ksha256          101953.18k   231621.03k   427492.44k   534554.28k   575944.02k   582303.74ksha512           69861.21k   279030.78k   493514.41k   732609.88k   855792.76k   864578.22k

Тест алгоритмов семейства AES

openssl speed aes

Победителем теста среди ARM оказался Raspberry Pi 4, отставание от Xeon E-2224 более чем в 2 раза, следом расположился Nvidia Jetson Nano.

Raspberry Pi 4

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytesaes-128 cbc      74232.10k    80724.61k    84387.02k    85057.54k    85314.22k    85196.80kaes-192 cbc      66069.32k    70589.59k    72967.00k    73584.64k    73766.23k    73667.93kaes-256 cbc      58926.68k    62458.30k    64351.40k    64619.16k    64976.21k    64913.41k

Nvidia Jetson Nano

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytesaes-128 cbc      64590.62k    68711.06k    71231.36k    71509.33k    71963.57k    71401.47kaes-192 cbc      55971.60k    59210.12k    60951.72k    61140.65k    61300.74k    61289.31kaes-256 cbc      49413.88k    51999.08k    53115.39k    53581.57k    53518.34k    53513.76k

Raspberry Pi 3

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytesaes-128 cbc      37401.48k    45102.98k    47455.83k    48064.51k    48130.73k    48119.81kaes-192 cbc      33444.87k    38794.88k    40544.51k    40930.30k    41047.38k    41036.46kaes-256 cbc      30299.70k    34635.65k    36142.58k    36331.52k    36424.36k    36427.09k

Orange Pi Zero

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytesaes-128 cbc      22108.15k    24956.95k    25791.06k    26007.89k    26069.67k    26072.41kaes-192 cbc      19264.22k    21327.66k    21971.29k    22138.88k    22186.67k    22189.40kaes-256 cbc      17211.09k    18887.40k    19399.34k    19532.12k    19570.69k    19573.42k

Вне конкурса: Intel Xeon E-2224

type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytesaes-128 cbc     173352.23k   196197.33k   201970.86k   203862.70k   204595.20k   203975.34kaes-192 cbc     149237.38k   164843.65k   167562.58k   168944.98k   169667.24k   169056.58kaes-256 cbc     130430.20k   141325.91k   143808.17k   144901.46k   145601.88k   145424.38k

Выводы

Делать выводы о том, насколько хорошо, согласно результатам бенчмарка OpenSSL, будет работать тот или иной CPU на реальной невычислительной задаче, конечно, нельзя. Однако, с точки зрения производительности в рамках задач, завязанных на TLS, можно сказать, что чипы, использованные в Raspberry Pi 4 и Jetson Nano, обладая низкой стоимостью, позволяют обеспечить достойную производительность: в расчете на 1 рубль, вероятно, непринужденно побеждают Xeon E-2224.

Надеюсь, что было полезно.

Подробнее..

Правильная архитектура MMO эмулятора

04.04.2021 14:14:58 | Автор: admin

Предыстория/Мотивация


Все началось с хобби в начале 2020 года с очередной попытки написания эмулятора игрового сервера Lineage 2 "по новому". Перед этим шагом было несколько попыток распиливания монолита существующих решений на рынке по новым практикам разработки, но затея оказалась тщетной, ибо те монолиты, которые и по сей день существуют и участвуют в так называемом "продакшен-пиратстве", имеют сильную связанность компонентов и решения поставленных задач, сопоставимые с началом 2000х годов, когда сфера только начинала развиваться. А самое главное, что монолит не заточен на построение распределенной архитектуры и, как следствие, обладает низкой эффективностью.


image


Было принято решение взять часть бизнес-логики (основной составляющей обработки действий игрока) из допотопных проектов эмуляторов и создать современный/масштабируемый эмулятор игрового сервера Lineage 2 Prelude Of War.


Исследование


Что на рынке


Возьмем, к примеру, EVE Online проект который смог. Архитектура серверной части позволяет при относительно средних вычислительных мощностях, обеспечивать:


  • низкую задержку игрок-сервер;
  • стабильный онлайн до 20тыс. игроков.

Что там внутри:


  • Stackless Python & Infiniband;
  • публичные узлы подключения игроков (Proxy);
  • внутренние узлы-контроллеры солнечных систем (SOL);
  • контроллеры твердотельных накопителей для операций ввода/вывода (Blade);
  • SQL Server Cluster.

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


Чтож, отлично, некий план стека технологий вырисовывался, но, нет Не все так просто. Поддержка физических машин для такой архитектуры слишком дорогое удовольствие, и на рынке Lineage 2 оно никому не нужно.


Главное, что можно выделить из архитектуры EVE Online:


  • распределенное состояние игровых объектов;
  • распределенный контроль игровых регионов (солнечных систем).

Продолжим.
Дальнейшим примером выступает официальный сервер PTS Lineage 2, написанный на C++.


Что внутри:


  • сервисная архитектура;
  • сервис взаимодействия с базой данных;
  • сервис аутентификации;
  • 2 сервиса-контроллера игровой логики игроков и NPC;
  • SQL Server.

Минусы:


  • не поддерживает масштабируемость сервисов;
  • ограничение игроков в 5 тыс. на одной инсталляции;
  • full stateful.

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


Из этого решения можно выделить также некоторые важные моменты на перспективу:


  • сервисная архитектура;
  • разделение ответственности.

Spring cloud стек


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


image


Стандартная архитектура на этом стеке представляет собой набор инфраструктурных сервисов, взаимодействующих между собой с помощью REST API или Event Queue (брокеры).


Главными компонентами выступают:


  • discovery-service сервис-регистратор, обнаружения и хранения мета-информации каждого инфраструктурного микросервиса;
  • config-server сервис централизованной конфигурации инфраструктурных микросервисов;
  • api-gateway маршрутизация запросов от клиента во внутреннюю кухню по метаинформации из discovery-service.

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


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


  • api-шлюз для взаимодействия между микросервисами и клиентами;
  • discovery-service, как неотъемлемая часть архитектуры.

Базовое представление архитектуры


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


Какой стек будет использоваться:


  • spring boot удобный микрофреймворк построения приложения с инъекцией зависимостей/из коробки поддержкой конфигурации приложения;
  • project reactor проект, посвященных реактивным потокам данных;
  • reactor netty реактивная обертка над сетевым фреймворком netty;
  • r2dbc реактивная реализация драйвера взаимодействия с базой данных.

Почерпнуть полезной информации по netty можно тут.


Служебные подсистемы:


  • PostgreSQL (базовое состояние);
  • KeyDB (промежуточное состояние системных компонентов).

Перед составом сервисов небольшая вводная в игровой клиент Lineage2.


Сетевая логика клиента


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


Клиент работает сразу с двумя серверами:


  1. сервер авторизационных данных игрока через него происходит первый этап подключения и проверка логина: пароля игрока, а также дальнейший вход в лобби;
  2. сервер игровой бизнес логики второй этап подключения клиента, после пройденных проверок на сервере авторизации (предигровое лобби выбора персонажей, вход в игровой мир)

Протокол, по которому клиент взаимодействует с серверами TCP.
Каждый пакет состоит из размера пакета (2 байта), типа пакета (1 байт) и блока параметров(переменная длина). В дополнение к этому, в пакетах сервера авторизации, в конце добавляется контрольная сумма и дополняется нулями так, чтобы размер пакета был кратен 8-ми байтам. Контрольная сумма может быть рассчитана следующей функцией:


public int checksum(ByteBuf buf) {    int checksum = 0;    while (buf.isReadable(8)) {        checksum ^= buf.readInt();    }    return checksum;}

Протокол lineage использует 6 разных типов данных:


  • char может принимать значение от -128 до 127. Имеет длину 1 байт
  • short может принимать значение от -32768 до 32767. Имеет длину 2 байта
  • int может принимать значение от -2147483648 до 2147483647. Имеет длину 4 байта
  • int64 (long) может принимать значение от -9223372036854775808 до 9223372036854775807. Имеет длину 8 байт.
  • float может принимать значение от 2.22507e-308 до 1.79769e+308. Имеет длину 8 байт
  • string текстовая строка в юникоде(UTF8). Каждая буква представлена двумя байтами, первый байтом код буквы, а второй номер кодовой таблицы. Индикатором конца строки служит символ с кодом 0.

Пакеты сервера авторизации шифруются по алгоритму Blowfish. Пакет Init содержит динамический Blowfish ключ случайно генерируемый для каждого клиента. Этот пакет сначала шифруется по алгоритму XOR (ключ генерируется случайным образом и помещается в конце пакета), а потом шифруется по алгоритму Blowfish, статическим ключом. По умолчанию статический ключ 6B 60 CB 5B 82 CE 90 B1 CC 2B 6C 55 6C 6C 6C 6C. Все последующие пакеты будут шифроваться динамическим Blowfish ключом. Пакет LoginRequest дополнительно шифруется по алгоритму RSA. Ключ состоит из следующих частей: B = 1024, E = 65537, N = передается в пакете Init. Вместе эти 3 части составляют целый RSA ключ.
Байты N в пакете зашифрованы функцией:


RSA crypt
public byte[] scrambledRSA(KeyPair keyPair) {    byte[] scrambledModulus = ((RSAPublicKey) keyPair.getPublic()).getModulus().toByteArray();    if (scrambledModulus.length == 0x81 && scrambledModulus[0] == 0) {        scrambledModulus = Arrays.copyOfRange(scrambledModulus, 1, 0x81);    }    // step 1 : 0x4d-0x50 <-> 0x00-0x04    for (int i = 0; i < 4; i++) {        byte temp = scrambledModulus[i];        scrambledModulus[i] = scrambledModulus[0x4d + i];        scrambledModulus[0x4d + i] = temp;    }    // step 2 : xor first 0x40 bytes with last 0x40 bytes    for (int i = 0; i < 0x40; i++) {        scrambledModulus[i] = (byte) (scrambledModulus[i] ^ scrambledModulus[0x40 + i]);    }    // step 3 : xor bytes 0x0d-0x10 with bytes 0x34-0x38    for (int i = 0; i < 4; i++) {        scrambledModulus[0x0d + i] = (byte) (scrambledModulus[0x0d + i] ^ scrambledModulus[0x34 + i]);    }    // step 4 : xor last 0x40 bytes with first 0x40 bytes    for (int i = 0; i < 0x40; i++) {        scrambledModulus[0x40 + i] = (byte) (scrambledModulus[0x40 + i] ^ scrambledModulus[i]);    }    return scrambledModulus;}

Для расшифровки можно воспользоваться следующей функцией:


Decrypt key
private byte[] decryptXorModulus(byte[] xor) {    // step 1 xor last 0x40 bytes with first 0x40 bytes    for (int i = 0; i < 0x40; i++) {        xor[0x40 + i] = (byte) (xor[0x40 + i] ^ xor[i]);    }    // step 2 xor bytes 0x0d-0x10 with bytes 0x34-0x38    for (int i = 0; i < 4; i++) {        xor[0x0d + i] = (byte) (xor[0x0d + i] ^ xor[0x34 + i]);    }    // step 3 xor first 0x40 bytes with last 0x40 bytes    for (int i = 0; i < 0x40; i++) {        xor[i] = (byte) (xor[i] ^ xor[0x40 + i]);    }    // step 4 : 0x00-0x04 <-> 0x4d-0x50    for (int i = 0; i < 4; i++) {        final byte temp = xor[i];        xor[i] = xor[0x4d + i];        xor[0x4d + i] = temp;    }    if (xor.length == 0x81 && xor[0] == 0) {        xor = Arrays.copyOfRange(xor, 1, 0x81);    }    return xor;}public PublicKey parseScrambledModulus(byte[] scrambledModulus) {    scrambledModulus = decryptXorModulus(scrambledModulus);    final KeyFactory keyFactory = KeyFactory.getInstance("RSA");    final BigInteger modulus = new BigInteger(1, scrambledModulus);    final RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec(modulus, RSAKeyGenParameterSpec.F4);    try {        return keyFactory.generatePublic(rsaPublicKeySpec);    } catch (InvalidKeySpecException e) {        throw new RuntimeException(e);    }}

Функции шифрования и дешифрации игрового трафика:


Enc/Dec
public void decryptData(ByteBuf byteBuf) {       int read = 0;       while (byteBuf.isReadable()) {           final int sub = byteBuf.readByte() & 0xFF;           byteBuf.setByte(byteBuf.readerIndex() - 1, sub ^ inKey[(byteBuf.readerIndex() - 1) & 15] ^ read);           read = sub;       }       shiftKey(inKey, byteBuf.writerIndex());   }   public void encryptData(ByteBuf byteBuf) {       if (!cryptEnable) {           cryptEnable = true;           return;       }       byteBuf.resetReaderIndex();       int read = 0;       while (byteBuf.isReadable()) {           final int sub = byteBuf.readByte() & 0xFF;           read = sub ^ outKey[(byteBuf.readerIndex() - 1) & 15] ^ read;           byteBuf.setByte(byteBuf.readerIndex() - 1, read);       }       shiftKey(outKey, byteBuf.writerIndex());       byteBuf.resetReaderIndex();   }   public void shiftKey(byte[] key, int size) {       int old = key[8] & 0xff;       old |= (key[9] << 8) & 0xff00;       old |= (key[10] << 0x10) & 0xff0000;       old |= (key[11] << 0x18) & 0xff000000;       old += size;       key[8] = (byte) (old & 0xff);       key[9] = (byte) ((old >> 0x08) & 0xff);       key[10] = (byte) ((old >> 0x10) & 0xff);       key[11] = (byte) ((old >> 0x18) & 0xff);   }

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


Из выше перечисленного выделяются три уровня ответственности решения:


  1. сервер работы в базой данных;
  2. сервер авторизации игроков;
  3. сервер бизнес-логики игроков (окрестратор).

Игровой мир/карта/система регионов


Если покопаться внутри клиента, то можно обнаружить файлы карты игрового мира с кодами регионов {x}_{y}.unr (расширение unreal level map) и как следствие должна быть региональная сетка игрового мира. В версии клиента, который я использую, мир состоит из 28 регионов в ширину (x) и 26 регионов в высоту (y). Каждый регион имеет блоки размером 256х256 координат. Каждый блок разделен на 8х8 ячеек.
Общая высота мира (y) 851968 координат.
Общая ширина мира (х) 917504 координат.
Из полученных данных нехитрым математическими преобразованиями можем нарисовать региональную сетку на карте игрового клиента.


image


Расчеты
 int REGION_WH = 222; // предрасчитаная ширина региона для изображения (легитимно только для моего варианта собранной карты из клиента) private void renderGrid(BufferedImage img, Graphics2D g, FontMetrics fontMetrics) {        //          draw grid        for (int x = 19, y; x-- > 0; ) {            for (y = 17; y-- > 0; ) {                final String name = (x + 10) + "_" + (y + 10);                final int strx = ((x * REGION_WH) + (REGION_WH / 2)) - (fontMetrics.stringWidth(name) / 2);                final int stry = (y * REGION_WH) + (REGION_WH / 2) + (fontMetrics.getHeight() / 2);                g.drawString(name, strx, stry);                g.drawLine(0, (y * REGION_WH) + REGION_WH, img.getWidth(), (y * REGION_WH) + REGION_WH);                g.drawLine((x * REGION_WH) + REGION_WH, 0, (x * REGION_WH) + REGION_WH, img.getHeight());            }        }    }

Центральные регионы игрового мира X=19 Y=18, с которых начинается отсчет координат, а также стоит учитывать что ось Y инвертированная по отношению к изображению, поэтому координаты отрицательными значениями идут выше отметки (0, 0), а положительные соответственно ниже.


Существует также внутренняя кухня для регионов, где рассчитываются ячейки.


Итак, из грид сетки можно увидеть, что мир построен из каждого отдельного куска карты. Отсюда рождается идея контроллеров части регионов карты (региональных ячеек) по оси Х и, собственно, находящихся в них объектов. Т.е. фактически это будет кластер, с центральным оркестратором распределения игровых объектов по его же шардам.


То самое


Из чего должно состоять решение:


  1. discovery-gateway-service сервис, который содержит информацию обо всех инфраструктурных сервисах приложения. Каждый микросервис регистрируется на сервере discovery, и discovery знает все инфраструктурные сервисы, работающие на каждом порту и IP-адресе, и, выполняет функции балансировки/маршрутизации пакетов между ними;
  2. data-service сервис, который отвечает за работу с базой PG и предоставляет свое АПИ инфраструктурным сервисам. Идея сервиса такова, что каждый другой сервис не должен взаимодействовать с базой напрямую, т.е. разделение ответственности, но может обратиться или записать некоторые свои данные используя Net обертку;
  3. auth-service сервис, обеспечивающий механизм входной точки доступа игрового клиента (центрального шлюза) аутентификации игровых аккаунтов и распределения игроков по выбранным серверам в списке серверов (зоны);
  4. game-gateway-orcehstrator сервис, обеспечивающий механизм входной точки доступа игрового клиента (центрального шлюза) авторизированных игровых аккаунтов после Auth Service и распределения игроков по Game Shard. Дополнительно хранит механизм кодеков/декодеров шифрования клиента с привязкой сессионных ключей каждого живого аккаунта, т.е. основная функция прием шифрованных запросов от игрового клиента, дешифровка и ретрансляция запросов на дочернюю игровую ноду привязанную к игроку по региональной сетке (Game Shard);
  5. game-shard сервис, обеспечивающий основную бизнес логику обработки входящих пакетов от Game Orchestrator, взаимодействие с остальными инфраструктурными сервисами, обработкой действий игрока и отправки результата по сессионным идентификаторам обратно в Game Orchestrator;
  6. npc-shard аналогично Game Shard, только в случаи для NPC (Non Player Character).

Любой сервис может масштабироваться.
Протокол общения сервисов TCP.
Стоит добавить, что общение сервисов между собой не блокирующее, т.е. сервис на определенное действие не ждет ответа. Можно разделить такую логику на два обработчика producer и consumer данных. В момент получения результата с течением времени, механика продолжит выполнение того или иного действия.


Пример:


  1. Shard запрашивает данные игрока у data-service, отправляя пакет с информацией ReqCharInfo (produce). При этом работа потока не блокируется для ожидания результата.
  2. Data-service принимает пакет ReqCharInfo (consume), выполняет некоторую логику забора данных из базы данных или кешей, отправляет обработанную информацию обратно в Shard пакетом RespCharInfo (produce).
  3. Shard принимает некоторым обработчиком данные игрока (consume) и выполняет следующий кусок логики манипуляции с данными.

Распределённое состояние


Фактически, что нам известно из многопользовательской игры существует два состояния игрового объекта:


  1. базовое
  2. промежуточное

Базовое состояние первичные данные, такие как уровень/наименование/тип/идентификатор etc., из которых составляется базовое представление объекта при инициализации в игровом мире.


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


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


Промежуточное состояние должно хранится в памяти приложения для быстрых расчетов и доступа, но в распределенной архитектуре это состояние должно хранится в централизованном хранилище, достаточно быстрым для темпа выбранной игры. Т.е. на каждого игрока в среднем рейт нормальной игры (атака, передвижение, манипуляции с предметами, системные задачи обработки) начинается с отметки в 100 rps. И по логике, распределение игроков в мире осуществляется так:


  • 15% торговля
  • 85% внутриигровая активность (осады, события, пвп контент и пр.)

Возьмем к примеру средне статистический онлайн с официального сервера в воскресенье
image


4122 игрока подключены к серверу. Рассчитаем нагрузку 4122 х 0.85 х (50 150 rps) = от 175 krps до 525 krps read/write. Не хило.


Какую систему использовать для хранения?
Требования:


  • отказоустойчивость
  • масштабирование
  • хранение и обработка большого количества данных игровых объектов
  • низкий latency и потребление ресурсов

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


  1. Redis/KeyDB
  2. Hazelcast
  3. Apache Cassandra (как отдельно так и в режиме embed шарда)
  4. Apache Ignite (как отдельно так и в режиме embed шарда)

Они все прекрасно работают с небольшими пакетами данных, и заявленные графики производительности на сайтах этих систем исключительная правда для небольших моделей.
А модели игровых объектов промежуточного состояния достаточно жирные в среднем 100 200 полей различных типов данных.


У всех наблюдаеются одни и те же проблемы, точнее это даже не проблемы самих систем, а проблемы построения функционала получения/записи данных:


  • блокирующие вызовы операций чтения/записи;
  • сериализация/десериализация данных.

И, как следствие потребление большого количества ресурсов.


P.s. исследование продолжалось в течении почти года.


Уже на данном этапе понятно, что из проекта ничего не выйдет. Погодите ка, что это? Off-Heap память? Серьезно?


В связи с неудовлетворительными показателями испробованных систем, было принято решение взять за основу off-heap таблицы хранения данных с прямым доступом в память процесса. Но это дает дополнительную реализацию функционала репликации. За основу off-heap системы хранения структуры <K,V> был взят алгоритм LSM-дерева поскольку обеспечивает быструю доступность данных по индексам при большой частоте записи и использует механизм слияния одного слоя памяти в другой (heap -> off-heap) с кастомной сериализацией объектов, основанной на ручной записи в буфер данных (Externalizable). Так же стоит дополнить, что в некоторых кейсах десериализации, объекты в их полном представлении не нужны, а нужна лишь часть данных. Поэтому была написана обертка, которая умеет работать с конфигурируемым View уровнем с перечислением необходимых полей. По факту это прямое чтение данных по смещению в байтовом массиве.


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


Немного статистики тестов

Xeon E5-2689 8 x 2.6, DDR 3 ECC 1866
Смешанный режим, 10 миллионов объектов, длинна ключа 8 байт, длинна значения 3567 байт


sync mode


avg write: 3128 ns
avg read: 2741 ns


write all: 25617 ms 400k rps
read all: 21430 ms 476k rps


async mode


avg write: 2689 ns
avg read: 2145 ns


write all: 16573 ms 603k rps
read all: 11620 ms 860k rps


CPU factor: 30% wo socket io


При желании можно разогнать показатели еще больше.


Представление кластера (оркестратор шард)


Как описывалось выше, оркестратор, фактически master уровень кластера, распределяет регионы для шарда (slave). Мы знаем, что игровой мир можно разделить на n-частей по оси X и эти самые части игрового мира отдаются под контроль шарда.


Как это выглядит визуально.
image


А что там по отказоустойчивости?


  1. Оркестратор может быть масштабируемым с хранением состояния кластера в KeyDB.
  2. По отключению шарда от кластера и его восстановлению, осуществляется балансировка регионов, как по живым шардам, так и по вновь подключенным.

Алгоритм балансировки кластера основан на Consistent Hashing, но со своими нюансами:


  1. балансировка затрагивает абсолютно все состояние кластера, а не его отдельный элемент
  2. балансировка осуществляться влево, вправо и в две стороны по горизонтальному представлению кластера

image


Конечная архитектура


Базовое представление
image


Показана схема взаимодействия инфраструктурных сервисов внутри кластера.
Дополнительно был реализован функционал discovery zones где шлюзом между ними выступает auth-service (2 зоны 2 разных сервера) и игровой клиент дает возможность выбирать на этапе авторизации, на какой сервер игрок хочет зайти.


Представление зон обнаружения
image


Представление взаимодействия игрового клиента с инфраструктурой
image


Представление репликации данных
image


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


Заключение


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


В рамках анализа было представлено обобщение архитектур, используемых в играх MMO, и рассмотрены методы для распределения игрового мира на несколько сервисов/машин.


Основными аспектами решения являются:


  • масштабируемость любого сервиса;
  • балансировка нагрузки алгоритмом RR, который обеспечивает discovery-gateway-service при общении сервис-сервис;
  • для orchestrator действуют правила RR распределения соединений как и для всех остальных сервисов;
  • availability & partition tolerance (consistency и нет и да);
  • разделение ответственности компонентов системы;
  • региональное распределение/контроль игрового мира, как распределение нагрузки игрового мира на решение.

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


image


Надеюсь кому-нибудь это будет полезно.

Подробнее..

Перевод - recovery mode Приватность в сетиБиткоин Лучшие практики

24.03.2021 14:22:50 | Автор: admin

ПереводстатьиGigiподготовлен биткоинеромTony

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

Джеффри Фишер, архиепископ Кентерберийский (1959)

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

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

Значимость приватности

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

Эти слова послужили вступлением к Манифесту шифропанка Эрика Хьюза в 1993 году. Разница между приватностью и секретностью тонка, но значима. Решение поддерживать приватность не говорит о том, что человеку есть что скрывать. В качестве примера, задумайтесь о том, что вы делаете в туалете или в спальне; это не является ни противозаконным, ни секретным (в большинстве случаев), но при этом вы закрываете за собой дверь и задергиваете шторы.

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

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

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

Статья 12, Декларация прав человека Организации Объединенных Наций

Биткоин и приватность

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

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

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

Передовые методы обеспечения приватности

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

Ниже перечислены практические шаги, которые вы можете предпринять для повышения уровня приватности:

  • Самостоятельно храните собственные монеты

  • Не используйте адреса повторно

  • Минимизируйте прохождение процедур KYC (знай своего клиента)

  • Минимизируйте взаимодействие с третьими лицами

  • Запустите свою Биткоин-ноду

  • Используйте сеть лайтнинг для небольших транзакций

  • Не используйте публичные обозреватели блоков

  • Пользуйтесь CoinJoin как можно чаще

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

Не используйте адреса повторно:Повторное использование адресовсводит приватность как отправителя, так и получателя на нет. Этого следует избегать любой ценой.

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

Минимизируйте взаимодействие с третьими лицами:Доверенные третьи стороны это дыры в безопасности. Если у вас есть возможность самостоятельно справиться с задачей, не полагаясь на доверенные третьи стороны, сделайте это.

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

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

Не используйте публичные обозреватели блоков:Поиск адресов в общественном обозревателе подразумевает возможность третьих лиц связывать эти адреса с вашим IP, который, в свою очередь, может быть связан с вашей персоной. Такие приложения какUmbrelиmyNodeпомогут вам запустить собственный обозреватель блоков. Если вам необходимо использовать обозреватель, убедитесь, что вы замаскировали свой IP, подключившись черезTor, или, по крайней мере, используетеVPN.

Пользуйтесь CoinJoin как можно чаще:Так как Биткоин это необратимый реестр, использование лучших практик проведения транзакций, таких каксовместные транзакции CoinJoin, гарантирует, что ваша приватность будет защищена в дальнейшем. В то время как транзакции CoinJoin обладают рядом нюансов, существует удобное для пользователя программное обеспечение, которое поможет вам создать и автоматизировать такие транзакции. Например,Whirlpoolот Samourai отличное решение для пользователей Android. Также можно обратиться к JoinMarket, который, благодаря таким проектам, какJoininBox, может быть легко настроен на вашей собственной ноде.

Заключение

Каждый должен стремиться использовать Биткоин наиболее приватным образом. Приватность секретность. Приватность это право каждого, и мы все должны защищать и поддерживать это право. Удалить информацию из интернета сложно; удалить информацию из публичного реестра Биткоин невозможно.

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

Функционирование Биткоина не соответствует традиционным концепциям. Ответить на такие вопросы, как "кто владеет этими средствами" или "откуда пришли эти деньги", в большинстве случаев нелегко.

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

И до тех пор, пока у нас есть свобода речи, подписание сообщения (будь то в частном порядке или нет) не должно считаться преступлением.

Больше информации о приватности в сети Биткоин можно найти в разделе нашего сайтаBitcoin Privacy.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru