предисловие
Я знаком с Elixir некоторое время (почти месяц), это язык, в отношении которого я очень оптимистичен, у него удобный синтаксис и дружелюбный стиль программирования, а также мощный и элегантный дизайн параллелизма Erlang.
На самом деле, изначально я думал связаться с Erlang, я выбрал Elixir вместо Erlang еще и из-за того, что «Дорога слушать, как книга» прикручивает себя к Erlang виртуальному черному Mo какой-то «странный синтаксис», шляпа. За странный синтаксис придется больше побеспокоиться: не писать ли код, чтобы играть очень сильно, не сжать ли его?
И после того, как я примерно за два дня изучил базовое содержание Эликсира, и полностью осознал всю элегантность Эликсира, я еще больше забеспокоился о том, не устарел ли Эрланг в дизайне, поэтому я взял Эликсир?
Только в конце концов, потому что я не мог понять программирование OTP в официальном руководстве Elixir, я понял, что попытка полностью понять дизайн OTP без изучения Erlang была «заблуждением». Именно по этой причине я официально соприкоснулся с Erlang и родным ТОРом, именно это решение позволяет мне понять, что такое OTP-программирование, и в то же время не упустить из-за "непонимания" такой прекрасный язык, как Erlang. ".
Итак, мое первоначальное решение перейти на Erlang было предметом этой статьи: Что такое программирование OTP в Elixir? Я попытаюсь проанализировать его с точки зрения Эликсира и представить принципы проектирования в Эрланге. В конце концов, не каждый разработчик Elixir должен быть пользователем Erlang, что является плюсом, но не требованием.
Концепция одноразового пароля
OTP — это аббревиатура от Open Telecom Platform. Происхождение этого названия может быть связано с бизнесом, который первоначально обслуживал Erlang, ведь когда-то Erlang был проприетарным программным обеспечением, принадлежащим гиганту коммуникационной индустрии Ericsson. На самом деле дизайн и функции OTP были отделены от первоначального значения его названия, поэтому OTP следует рассматривать как понятие, не имеющее ничего общего с его названием.
В Erlang/Elixir вы, возможно, уже сможете использовать встроенные функции языка для реализации некоторых распространенных сценариев параллелизма, но было бы излишним предполагать, что каждый должен делать это один или несколько раз в проекте. Как человек с достаточным опытом кодирования, вы должны быть в состоянии думать, что можете комбинировать их и абстрагировать в «фреймворк», применимый к определенным сценариям или как можно более общий Это OTP. Использование OTP требует только реализации шаблона поведения OTP и разработки API на основе шаблона поведения в качестве деталей связи, которые могут охватывать различные сценарии, позволяя разработчикам больше сосредоточиться на реализации бизнеса и не беспокоиться о параллелизме и отказоустойчивости.
OTP-приложение
В отличие от большинства программ и языков программирования, сами приложения OTP не имеют основного потока выполнения (параллельных единиц, таких как потоки/процессы), который блокирует выполнение программы. Чтобы быть точным, процесс приложения OTP сам по себе не блокирует приложение, и предпосылкой этого является процессно-ориентированное программирование Erlang.
Для OTP-приложений само приложение состоит из нескольких процессов, вообще говоря, древовидной структуры управления Эти процессы будут иметь различное разделение труда, но не будут иметь никаких привилегий. Напротив, например, обычная программа продолжает работать, блокируя поток, который запустил приложение, и если этот поток завершается, программа завершается (обычно все фоновые потоки освобождаются). Однако приложения OTP загружаются и запускаются с помощью ERTS (система выполнения Erlang), и все процессы одинаковы. Вы обнаружите, что каждое приложение OTP похоже на систему, состоящую из нескольких микросервисов (процессов), ориентированных на процессы. микросервисы» один за другим в этой системе. Программы, разработанные по этому принципу, являются приложениями OTP.
Давайте возьмем в качестве примера реальный код, сначала мы создадим проект hello_main:
mix new hello_main
Измените файл lib/hello_main.ex и добавьте функцию входа для запуска (main/0), логика заключается в вызове бесконечной рекурсивной функции, которая выводит строку Hello! (loop_echo/1):
defmodule HelloMain do
def main do
loop_echo("Hello!")
end
def loop_echo(text) do
IO.puts(text)
:timer.sleep(1000)
loop_echo(text)
end
end
Выполнить (запустить) эту программу:
iex -S mix run -e HelloMain.main
Мы увидим такой вывод:
Erlang/OTP 21 [erts-10.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Compiling 1 file (.ex)
Generated hello_main app
Hello!
Hello!
Hello!
# ……
Обратите внимание, что в это время наш терминал iex заблокирован процессом, выполняемым в main, и этот процесс полностью неуправляем.
(Обратите внимание, что вы также можете смоделировать эту программу с помощью esscript, который будет более интуитивным. Однако escrit Эликсира требует поддержки микса, что не является интуитивно понятным)
Затем реализуем программу с той же функцией, но организованную по принципам OTP. Создайте проект hello_otp:
mix new hello_otp
Измените mix.exs, чтобы добавить модуль обратного вызова:
def application do
[
mod: {HelloOtp, []},
# ……
]
end
Добавьте ту же функцию loop_echo в lib/hello_otp.ex и реализуйте шаблон поведения приложения:
defmodule HelloOtp do
use Application
def start(_type, _args) do
children = [{Task, fn -> loop_echo("Hello!") end}]
Supervisor.start_link(children, strategy: :one_for_one)
end
# loop_echo/1 defined here ……
end
Запустите приложение (Примечание. Поскольку мы реализуем приложение и определите модуль обратного вызова, вам не нужно вручную указывать функцию записи):
iex -S mix
Здесь мы используем процесс супервизора для запуска и управления процессом, вызывающим функцию loop_echo. А поскольку контролирующий процесс не блокирует ввод терминального процесса iex, функция iex может нормально использоваться при выводе Hello!
Разница между двумя программами в том, что весь цикл выполнения hello_main не завершит выполнение входной функции main, потому что это логика функции, которую нельзя вернуть, даже если программа принудительно завершится. Функция start/2 функции hello_opt завершается сразу после запуска процесса контроля и возвращает соответствующий результат. Таким образом, в это время как контролируемый процесс, так и контролируемый процесс работают в фоновом режиме, и процессы должным образом организованы и управляются.
PS: На самом деле супервизорный процесс hello_otp и терминальный процесс iex находятся в отношениях уровня.
Отличительной чертой hello_otp является то, что он правильно реализует режим поведения приложения (результаты возвращаются), все приложение состоит из одного или нескольких процессов, каждый процесс работает в фоновом режиме, и процесс правильно организован как встроенный процесс приложения. в ЕРТС.
А hello_main ближе к обычной программе, которую мы видели, первый запущенный процесс блокирует выполнение, он завершает приложение и завершает работу. Я считаю, что когда вы видите это, вы, вероятно, должны понять определение приложений OTP.
Суть применения OTP
Если вы столкнулись с Erlang и организовали OTP-приложения, то вам следует знать, что у каждого приложения есть «спецификация», которая будет загружаться модулем Application (для вышеприведенного приложения hello_otp оно также будет загружено после загрузки. указанную функцию).
Давайте воспроизведем это, вызвав модуль вручную без mix , но убедитесь, что ваша программа скомпилирована:
mix compile
Запустите iex (или erl) напрямую:
iex -pa _build/dev/lib/hello_otp/ebin/
(Параметр -pa добавляет указанный вручную путь в список путей поиска модулей, чтобы наши собственные модули можно было найти во время загрузки)
Отличие от предыдущего заключается в том, что в это время консоль iex не выводит Hello!, потому что приложение не загружено и не будет запущено, этот шаг мы должны проделать вручную:
Application.start :hello_otp # 如果是 erl 则使用 application:start(hello_otp)
Консоль напечатает :ok (возвращаемое значение функции Application.start/1), а затем непрерывно выведет Hello!, что аналогично использованию mix для запуска, но эти шаги выполняются с помощью mix run.
别忘了上面提过,每一个 OTP 应用都有一份“规范”文件,Application.start/1 函数首先做的就是寻找这份规范文件,然后根据解析结果载入模块。我们可以从 _build/dev/lib/hello_otp/ebin 目录中看到一个名为 hello_otp.app 的文件,这便是所谓的“规范”文件。 Its format is an Erlang tuple, where mod defines the entry module (that is, added in mix.exs before), and the reason why executing Application.start(:hello_otp) will call back the HelloOtp.start/2 function is also for эта причина.
Это позволяет нам понимать, что приложение OTP на самом деле представляет собой серию модулей, которые будут загружены erts, начать процесс приложения, генерируемый модулем ввода, реализует поведение приложения во время выполнения функции обратного вызова.
Затем, после загрузки модуля, который не создает процесс, но соответствует структуре приложения OTP, считается ли он приложением OTP? Ответ: считать. Приложения OTP, которые не порождают процессы, распространены, и это «библиотечные» приложения. На самом деле мы также можем загрузить hello_otp как библиотечное приложение (повторно введите iex):
Application.load :hello_otp
Вызов функции Application.load/1 обнаружил, что она также возвращает :ok, но Hello! не было сгенерировано, потому что не было обратного вызова для функции HelloOtp.start/2. На этом этапе вы можете вручную вызвать функцию HelloOtp.start/2 или HelloOtp.loop_echo/1.Вы должны быть достаточно умны, чтобы понять, что hello_otp становится «библиотечным приложением» в это время. Если вы хотите, чтобы эта библиотека порождала процесс, то есть запускала программу hello_otp, просто:
HelloOtp.start(:normal, [])
Просто вызовите функцию HelloOtp.start/2 вручную. То есть для Erlang/Elixir, особенно для модулей, организованных по принципу OTP, между библиотекой и программой с записью мало различий, и они оба называются приложениями.
Следовательно, необходимо сбросить вышеупомянутое приложение OTP, запуск которого будет генерировать один или несколько фоновых процессов, что не обязательно. Если вы хотите четко определить, относится ли программа к OTP-приложению, вам нужно только посмотреть на ее модульную организацию, а процесс ее выполнения не важен. Но даже если организация модулей соответствует спецификации, все равно могут быть проблемные OTP-приложения: например, неправильные паттерны поведения реализации. Если я не запускаю процесс супервизора в функции start/2, а напрямую вызываю loop_echo/1, такой подход приведет к блокировке процесса переднего плана, и функция обратного вызова start/2 никогда не вернется, что мало чем отличается от hello_main .
Принципы проектирования OTP
Наконец, как устроен OTP? Он предназначен для реализации этих концепций сущностей соответственно? Чтобы подробно объяснить OTP, на самом деле нужно описать много деталей, и этот раздел представляет собой лишь общее описание структуры OTP. Будет открыта новая статья для объяснения конкретной практики OTP.
1. Дерево наблюдения
Дерево наблюдения — очень важная концепция для OTP, а также краеугольный камень OTP для достижения гарантии «высокой отказоустойчивости». Проще говоря, дерево наблюдения — это способ организации процессов, потому что процесс в целом представляет собой древовидную структуру, а корень — это другой процесс наблюдения верхнего уровня, поэтому его называют «деревом наблюдения».
PS: Для построения наблюдательного дерева требуется модуль Supervisor.
Заимствуйте картинку с официального сайта:
Прямоугольники представляют процессы контроля, а кружки — рабочие процессы. Процесс контроля может контролировать процесс контроля следующего уровня, и каждый рабочий процесс контролируется своим собственным процессом контроля, что очень похоже на отношения между начальником, руководством и рядовыми работниками на предприятии. Каждый супервизор может определить стратегию перезапуска контролируемого процесса, а каждый рабочий может определить собственное время перезапуска. Комплекс может быть сконфигурирован с набором узкоспециализированных механизмов отказоустойчивости, а простой может быть гарантирован для «постоянной работы».
Кстати, приложение hello_otp, реализованное выше, представляет собой простейший процесс супервизора root + структуру рабочего процесса, но если мы убьем рабочий процесс, Hello! Хм... Кажется, что-то не так (⊙?⊙) Не следует ли перезапустить его сразу и продолжать выводить? Разве это не то, что делает процесс мониторинга? Причину, по которой рабочий процесс приложения hello_otp убивается, но не перезапускается, здесь упоминаться не будет, и вы поймете, прочитав следующую статью :)
Во-вторых, общий сервер
В повседневной разработке, если вы хотите реализовать базовый сервер, вам необходимо задействовать такие функции, как поддержание состояния, создание процессов, непрерывный прием и ответные сообщения и выход из процесса. Общий сервер (модуль GenServer) OTP является инкапсуляцией модели клиент-сервер.Общий сервер может использоваться не только как простая и надежная зависимость модели C/S, но и как основа для реализации других частей поведения OTP. модель.
Принципиальная схема простейшей модели C/S (из документа официального сайта):
Универсальный сервер также является наиболее типичным примером основной цели OTP, которая заключается в абстрагировании общего кода/компонентов и их повторном использовании в максимально возможной степени.
3. Государственный автомат
Государственный автомат (Gen_Statem) с сервером общего назначения (GEN_SERVER) в качестве одного из стандартных моделей ОТП по поведению типичного примера моделирования бизнес-процессов состояния - «Open / Close»:
Изображение выше взято из статьи, знакомящей с рабочими процессами Drupal (не потому, что мне лень рисовать схему, а потому, что существует достаточно концептуальных описаний конечных автоматов, чтобы мне не приходилось делать это несколько раз). В этом примере дверь перейдет в три состояния (открыто, закрыто и заперто) в зависимости от ввода, но дверь должна перейти из запертого состояния (закрыта) в закрытое (закрыто) состояние, прежде чем ее можно будет открыть ( Opened) Дверь открывается напрямую без отпирания, то есть напрямую преобразуется из Locked в Opened.
Примечание. Elixir не является оболочкой для модуля Erlang gen_statem, а соответствующий режим строки, предоставляемый до Erlang 19.0, — это gen_fsm.
Дизайн модуля gen_statem очень похож на модуль GenServer, и GenServer также может в определенной степени решать аналогичные службы. В официальной рекомендации используйте GenServer только в том случае, если бизнес-процесс достаточно прост и в будущем не возникнет ситуаций, когда вам потребуется реализовать шаблон поведения gen_statem для полной адаптации к вашей проблеме.
В-четвертых, организатор мероприятий
Менеджер событий (gen_event) также является стандартным шаблоном поведения OTP, и его API-интерфейс похож на общий сервер (gen_server), но принцип его работы отличается.
Диспетчер событий называется «менеджером», потому что он не обрабатывает события напрямую, а управляет «обработчиками» событий, которые представляют собой специальные модули, реализующие шаблон поведения gen_event. Менеджер событий — это, по сути, список, который поддерживает пары {Module, State}, где Module — это обработчик событий, а State — внутреннее состояние обработчика.
В дереве супервизии он часто используется для запуска только одного диспетчера событий gen_event, а затем "горячей замены" нескольких обработчиков событий. Добавляйте, когда нужно, и удаляйте, когда не нужно. Именно это отношение «один ко многим» определяет, как он работает иначе, чем обычный сервер.
Наконец
Erlang и Elixir — очень хорошие языки, а набор одноразовых паролей — сильная поддержка Erlang/Elixir. Разработка приложения с использованием принципов OTP достаточна, чтобы гарантировать надежность программы, поскольку OTP тщательно и полностью протестирован. А применение паттернов поведения OTP может сильно улучшить читабельность программы, ведь это известные (единственный способ войти в Erlang) паттерны проектирования. Что касается объяснения конкретного случая режима поведения OTP, я опубликую его в следующей статье, а эта статья на данный момент неизвестна :)
Наконец, приглашаем друзей присоединиться к группе Telegram для обучения и общения (Erlang/Elixir): https://t.me/elixir_cn