Distributed Erlang

30 марта 2014

Вчера выступил для Minsk F# User Group, с рассказом про распределенность в Erlang. Народу собралось не много, но зато те, кто собрались, почти все выдержали до конца выступления. Значит было интересно :) А получилось довольно длинно – где-то 2 часа рассказывал теорию, и 1 час live coding.

Помещение любезно предоставлено лучшей в Беларуси IT-компанией Taucraft. Мне было интересно посмотреть их офис, и я просил организаторов, чтобы встреча была именно там. Посмотрел. Офис очень клевый. Я бы там с удовольствием работал. Жаль, по стеку технологий я совсем не подхожу :)

Презентация тут. Текст выступления ниже. Код live coding тут.

1 Распределенность в Erlang

1.1 Что такое распределенная система?

Лесли Лэмпорт (ученый, исследователь теории распределенных систем):

A distributed system is one in which the failure of a computer you didn’t even know existed can render your own computer unusable.

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

Систем таких много, самых разных. И примеры долго искать не нужно:

Хранилища данных: Google BigTable, Amazon SimpleDB, Hadoop, Cassandra, множество их, в т.ч. и написаные на Erlang: Riak, CouchDB.

Сервисы: Google Search, поиск Яндекса, WhatsApp, множество их, в т.ч. и написаные частично на Erlang: Heroku, Github.

Социальные сети: Facebook, Twitter, Вконтакте и др.

Многопользовательские игры: Word of Warcraft, Call of Duty Black Ops и др.

1.2 Зачем их делают?

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

  • Хранить данных больше, чем может поместиться на одной машине;
  • Обрабатывать данных больше, чем можно обработать на одной машине;
  • Повысить надежность системы;
  • Построить масштабируемую систему, способную адаптироваться к росту нагрузки;
  • Сервисы, распределенные по своей сути (например, CDN – content delivery network);

1.3 Какова надежность Erlang?

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

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

Availability % Downtime per year
90% (one nine) < month
99% < 4 days
99,9% < 9 hours
99,99% < 1 hour
99,999% ~ 5 min
99,9999% ~ 31 sec
99,99999% ~ 3 sec
99,999999% ~ 300 msec
99,9999999% ~ 30 msec

С помощью Erlang была построена система с надежностью Nine Nines (девять девяток)

Вот тут один из создателей языка Джо Армстронг хвастается:

The AXD301 has achieved a NINE nines reliability (yes, you read that right, 99.9999999%). Let’s put this in context: 5 nines is reckoned to be good (5.2 minutes of downtime/year). 7 nines almost unachievable … but we did 9.

А компания 99s, давшая нам такие прекрасные тулы, как Cowboy, Ranch и Bullet, названа именно в честь такой отказоустойчивости Erlang.

1.4 Терминология

Введем немного терминологии.

Эрланг приложение (Application) – система, сервис или библиотека с определенной функцией. Например: веб-сервер Cowboy, сервис логирования Lager, драйвер к базе данных и т.д.

Узел (Node) – экземпляр виртуальной машины Erlang. Обычно выполняет несколько Эрланг приложений. На одной машине может быть запущено несколько узлов.

Кластер – несколько узлов, взаимодействующих между собой. Узлов в кластере может быть от 2х до сотен и тысяч :)

1.5 Что такого особенного в Erlang?

Эффективную распределенную систему можно построить на самых разных технологиях и ЯП. А в чем особенность именно Erlang? Он предлагает единый подход и к локальному и к распределенному программированию.

Архитектура Erlang-приложения строится из независимых процессов, общающихся между собой отправкой сообщений.

Как элемент архитектуры, процесс:

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

На что это похоже? На объект в ООП.

Идем уровнем выше. Из процессов строятся приложения.

Приложение:

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

Идем уровнем выше. Для выполнения приложений запускается узел.

Узел:

  • имеет публичный АПИ;
  • хранит внутри себя данные;
  • общается с клиентами и другими узлами;
  • имеет закрытую бизнес-логику;

Идем уровне выше. Из узлов строится кластер.

Кластер:

  • имеет публичный АПИ;
  • хранит внутри себя данные;
  • общается с клиентами;
  • имеет закрытую бизнес-логику;

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

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

Пробуем:

erl -name node1@127.0.0.1

erl -name node2@127.0.0.1

(node1@127.0.0.1)1> net_adm:ping('node2@127.0.0.1').
pong
(node1@127.0.0.1)2> register(my_shell, self()).
true

(node2@127.0.0.1)1> {my_shell, 'node1@127.0.0.1'} ! "hello there".
"hello there"

(node1@127.0.0.1)3> flush().
Shell got "hello there"
ok
(node1@127.0.0.1)4> node().
'node1@127.0.0.1'
(node1@127.0.0.1)5> nodes().
['node2@127.0.0.1']

Это база всех взаимодействий. Если я знаю Pid процесса, или знаю, под каким именем он зарегистрирован, то я могу с ним общаться. Pid уникален в пределах кластера. Имя уникально в пределах узла.

На самом деле вот такой код, с отправкой сообщений, пишут редко. На базе сообщений построены более высокие и удобные уровни: gen_server, rpc:call, распределенное OTP приложение и т.д. И разработчики обычно пользуются ими.

Но сетевая прозрачность касается не только отправки сообщений, но и мониторинга процессов. Мониторинг, это когда один процесс (supervisor) наблюдает за состоянием другого процесса (worker), и получает сообщение если с worker случается какая-то проблема. Supervisor может предпринять в этом случае какие-то действия. Например, перезапустить worker.

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

1.6 Ссылки

Весьма рекомендую книгу Фреда Хеберта Learn you some Erlang for greate good!

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

2 главы там посвящены распределенным приложениям: Distribunomicon Distributed OTP Applications

Соответствующие главы по теме есть и в классических книгах: Programming Erlang: Software for a Concurrent World. Joe Armstrong Erlang Programming. Francesco Cesarini, Simon Thompson

И в менее известной, но тоже очень хорошей Erlang and OTP in Action. Martin Logan, Eric Merritt

2 Erlang кластер

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

Если мы хотим наладить взаимодействие между узлами в разных сетях, то лучше делать независимые веб-сервисы, предоставляющие защищенное АПИ внешнему миру (например, REST HTTP API), как вы бы это делали на других языках. То есть, отказаться от эрланговской сетевой прозрачности.

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

2.1 Имена узлов

При запуске узла ему дается имя

erl -name node@host

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

В длинных именах имя хоста должно определяться DNS или это может быть IP адрес. Я предпочитаю IP адрес, чтобы не возиться с настройкой и поддержкой DNS :)

2.2 Соединение

Соединение устанавливается автоматически если один узел обращается к другому: отправляет сообщение зарегистрированному процессу, net_adm:ping и др.

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

  • 4 узла – 6 соединений;
  • 10 узлов – 45 соединений;
  • и дальше рост в арифметической прогрессии :)

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

Но есть альтернатива – скрытые узлы.

erl -name foo@host -hidden

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

2.3 Куки

Некоторый уровень безопасности в кластере все-таки есть. Все Erlang узлы на старте получают куку – специальную строку символов, типа как пароль, только без логина (или, наоборот, логин без пароля).

Когда узел пытается соединится с другим, он посылает свою куку (в зашифрованном виде). Другой узел сравнивает со своей кукой, и отклоняет соединение, если куки не совпали.

Задать куку можно 3-мя способами:

положить в файл

~/.erlang.cookie

(и поставить на него права 400, иначе узел не запустится)

задать в аргументах при запуске узла

erl -setcookie abc

выполнить

erlang:set_cookie(Node, Cookie)

в уже запущеном узле.

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

rpc:multicall(nodes(), os, cmd, ["rm -rf /"]).

2.4 epmd

Erlang Port Mapper Daemon

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

Его можно запускать вручную с разными настройками:

  • задать не стандартный порт (стандартный для него 4369);
  • время heart beat сообщений;
  • настройки, позволяющие эмулировать загруженность сети.

3 Модули и функции

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

Global A Global Name Registration Facility

Регистрация имен процессов в глобальной (для всего кластера) области видимости.

global:register_name(Name, Pid)
global:re_register_name(Name, Pid)
global:whereis_name(Name)

При такой регистрации возможен конфликт имен. Модуль также предлагает средства для разрешения этого конфликта.

net_adm Various Erlang Net Administration Routines

Тут есть популярная функция:

net_adm:ping(node)

erlang The Erlang BIFs

Среди можножества Erlang BIFs есть:

  • node() – узнать имя своего узла;
  • nodes() – получить список узлов, с которым соединен данный узел;
  • monitor_node(Node, Flag) – включить/выключить мониторинг состояния узла;
  • disconnect_node(Node) – отсоединиться от узла;
  • и другие.

Все функции для запуска процесса имеют варианты запуска на другом узле

  • spawn(Fun);
  • spawn(Node, Fun);
  • spawn(Module, Function, Args);
  • spawn(Node, Module, Function, Args);
  • spawn_link(Fun);
  • spawn_link(Node, Fun);
  • spawn_link(Module, Function, Args);
  • spawn_link(Node, Module, Function, Args).

net_kernel Erlang Networking Kernel

Соединиться с другим узлом:

net_kernel:connect_node/1

Дать имя своему узлу, настроить heart beat:

net_kernel:start([Name, shortnames])
net_kernel:start([Name, longnames, HeartBeat])
net_kernel:set_net_ticktime(5)

rpc Remote Procedure Call Services

Понятно, что этот модуль очень важный и полезный :)

Синхронный вызов функции на другом узле:

rpc:call(Node, Module, Function, Args)
rpc:call(Node, Module, Function, Args, Timeout)

Асинхронный вызов функции:

Res = rpc:asyn_call(node, module, function, arguments)

и отложенное получение результата, блокирующее:

rpc:yield(Res)

и не блокирующее:

rpc:nb_yield(Res, Timeout) (non blocking)

Синхронный вызов функции на всех, или на указанных узлах в кластере:

rpc:multicall(Module, Function, Args)
rpc:multicall(Nodes, Module, Function, Args)
rpc:multicall(Module, Function, Args, Timeout)
rpc:multicall(Nodes, Module, Function, Args, Timeout)

Асинхронный вызов функции, когда результат вообще не нужен:

rpc:cast(Node, Module, Function, Args)

Бродкаст сообщения зарегистрированному процессу на указанных узлах. С подтверждением получения:

rpc:sbcast(Nodes, Name, Msg)

Без подтверждения получения:

rpc:abcast(Nodes, Name, Msg)

4 Распределенное OTP приложение

Одно из удобных высокоуровневых средств, это распределенное OTP приложение.

Тема хорошо раскрыта у Фреда Хеберта в LYSE, в главе Distributed OTP Applications.

Обычное OTP приложение имеет состояния: loaded, started, stopped, uploaded. У распределенного еще добавляется состояние running.

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

Затем, когда упавший узел восстанавливается, приложение на нем снова запускается, а резервное переходит в режим ожидания. Это механизм Takeover.

Мониторинг состояния узлов, Failover и Takeover OTP берет на себя. Но это не значит, что программисту ничего делать не нужно. Когда приложение падает, оно теряет свое состояние. Как сохранить это состояние, как восстановить после падения, как передать из одного приложения другому – это все на совести программиста.

5 Net Split

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

Если они хранят и модифицируют какие-то состояния внутри себя, то эти состояния рассинхронизируются. И после восстановления связи возникает проблема – состояния нужно опять синхронизировать.

Ситуация похожа на конфликт в системе контроля версий, когда один и тот же исходник одновременно модифицировали два программиста. Но такой конфликт программисты исправят вручную, а с Net Split система должна как-то справится автоматически.

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

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

comments powered by Disqus