Все, что вы хотели знать о Rebar, но ленились прочитать

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

Выступил с докладом про Rebar. Видео снимали, еще пока не готова. Презентация тут. Ну и текст выступления ниже )

Все, что вы хотели знать о Rebar, но ленились прочитать

Если верить википедии, то Rebar -- это арматура для железобетонных конструкций :)

Но нас интересует не арматура, а известный инструмент для сборки Erlang-проектов.

Занимается он не только сборкой Erlang-проектов, но и многими другими полезными вещами. А именно:

  • сборка драйверов из сорцов С/C++;
  • сборка прочих сопутствующих штук (ErlyDTL шаблоны, Protocol Buffers)
  • создание приложений, модулей, тестов из шаблонов
  • подготовка релизов
  • управление зависимостями
  • запуск тестов (EUnit и Common Test)
  • сборка escript-приложений
  • генерация документации

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

Так же Rebar, это популярный проект на github.com, имеющий 117 контрибуторов, 100 форков и 12600 строк кода.

Документация

Rebar легко использовать. Достаточно знать 4 команды и уметь описывать зависимости в rebar.config.

А что бы разобраться чуть глубже, нужно заглянуть в документацию. Она есть на github.com в виде wiki-страниц.

И есть встроенная в сам Rebar и доступная из консоли:

rebar help

rebar --commands

rebar help compile

rebar help get-deps

rebar help clean

Эти две документации, конечно, пересекаются, но в общем не одинаковые.

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

Еще есть доклад Dizzy Smith, Erlang-разработчика из компании Basho на Erlang User Conference 2012 Applied Rebar. Но, чесно говоря, этот доклад ничего не добавляет к документации, и не избавляет от необходимости ее читать.

Основные команды

Начнем с 4х самых важных команд.

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

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

rebar get-deps

После чего проект можно собирать вместе с зависимостями.

rebar compile

Ну вот и все, хватило даже 2-х команд :)

Кроме этого нужно уметь запускать юнит-тесты (у вас ведь есть юнит-тесты, правда? :)

rebar eunit

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

rebar clean

К этим командам добавим одну важную опцию: skip_deps=true

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

rebar compile skip_deps=true

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

rebar eunit skip_deps=true

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

rebar clean skip_deps=true

Ну вот, 4 команды, одна опция, и теперь вы все знатоки Rebar :)

Компиляция

Теперь поговорим о главном -- о компиляции.

Rebar предполагает, что ваш проект организован согласно OTP Design Principles и, в частности, имеет типовую структуру:

  • ebin -- сюда складываются скомпилированные beam-файлы
  • include -- здесь находятся заголовочные hrl-файлы
  • src -- здесь исходники, erl-файлы
  • priv -- здесь всякие сопутствующие файлы, шаблоны, статика, скрипты и т.д.

К этой структуре Rebar добавляет свои соглашения:

  • deps -- сюда выкачиваются зависимости
  • test -- здесь находятся юнит-тесты
  • c_src -- здесь находятся C/C++ сорцы

Проект с такой структурой, если он не имеет зависимостей, собирается Rebar даже при отсутствии rebar.config.

erlc

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

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

Если бы мы собирали наш типовой проект с помощью erlc, то это выглядело бы, например, так:

erlc -I include -o ebin src/*.erl

Опция -I указывает папку с хедер-файлами, опция -o указывает папку, куда складывать результат компиляции, ну и src/*.erl задает список файлов, которые нужно скомпилировать.

Еще есть опции для определения макросов -Dname=value, для отключения предупреждений -W0 (никогда так не делайте :), или, наоборот, для трактовки предупреждений как ошибок -Werror (а так можете делать :). Ну и несколько других, не очень нужных.

Кроме компилятора erl-файлов, erlc еще включает, например, Yecc -- парсер-генератор, умеющий создавать erl-сорцы из описаний грамматик в форме Бэкуса — Наура. И другие, не менее экзотические штуки.

rebar

Ну вот, сорцы Erlang были скомпилированы с помощью ercl. А что же к этому добавляет Rebar? Да многое.

Важная вещь в OTP-приложении, это файл ebin/myapp.app описывающий метаинформацию о приложении: имя, номер версии, главный модуль, зависимости от системных приложений. И, помимо прочего, там перечисляются все модули, входящие в состав приложения. Без Rebar этот файл пришлось бы поддерживать вручную -- не забывать добавлять туда все новые модули.

Вместо этого Rebar предлагает использовать файл src/myapp.app.src, где указано все тоже самое, кроме списка модулей. Из него Rebar автоматически генерирует ebin/myapp.app, но уже сам добавляет туда все модули, которые есть в src. Ну а если вы, все-таки, создали ebin/myapp.app сами, то Rebar проверит, чтобы там все модули были перечислены, и чтобы не было указано лишних.

Далее, Rebar умеет компилировать C/C++ сорцы драйверов, если находит их в папке c_src. Делает он это опять не сам, а поручает компиляторам cc и c++. Но контролирует изменения в файлах сам.

Еще Rebar умеет компилировать шаблоны ErlyDTL. Это html-шаблоны, такие же, как в Django, популярном веб фреймворке для Python. Rebar компилирует каждый шаблон в отдельный Erlang-модуль, сразу в beam-файл. Вернее, он опять не сам это делает, а поручает компилятору, входящему в состав библиотеки erlydtl.

И это еще не все :)

Есть такая популярная библиотека сериализации данных Google Protocol Buffers, она же protobuf. Фишка этой библиотеки в том, что данные описываются в текстовых proto файлах, из которых автоматически генерируется клиентский и серверный код, описывающий соответствующие объекты на нужном языке программирования. Сам гугл поддерживает генерацию кода для Java, Python и C++. Но есть сторонние библиотеки для других языков, в т.ч. и для Erlang -- erlang_protobuffs.

Rebar умеет генерировать из proto файла Erlang-модуль (erl) и хедер файл (hrl). Как вы уже догадались, он делает это не сам, а поручает компилятору, входящему в состав библиотеки erlang_protobuffs :)

Ну, теперь все.

Управление зависимостями

Rebar умеет клонировать и собирать зависимые библиотеки из репозиториев git, mercurial и bazaar.

Для этого зависимости нужно описать в rebar.config

{deps, [Dependency1, Dependency2, Dependency3]}.

где Dependency это

{App, VsnRegex, Source}

App -- имя OTP-приложения библиотеки, VsnRegex -- регулярное выражение, с которым должна совпадать версия библиотеки Source -- источник, откуда брать исходные коды.

Источник описывается так:

{git, Url, Rev}

{hg, Url, Rev}

{bzr, Url, Rev}

Url -- путь к репозиторию Rev -- ветка, тэг или коммит

{branch, "master"}

{tag, "v1.0"}

"62b7c9b12daacfcbcf274bc0925a7f8d10e3a1e0"

"v1.0"

"HEAD"

""

Пример:

{deps, [

{emysql, ".*", {git, "https://github.com/Eonblast/Emysql.git", "62b7c9b12daacfcbcf274bc0925a7f8d10e3a1e0"}},

{mcd, ".*", {git, "https://github.com/EchoTeam/mcd.git", "f72ebf5006e1b1234e16f86514e4291c57506024"}},

{cowboy, ".*", {git, "https://github.com/extend/cowboy", "0.8.6"}},

{mimetypes, ".*", {git, "git://github.com/spawngrid/mimetypes.git", {branch, "master"}}},

{lager, ".*", {git, "https://github.com/basho/lager.git", "2.0.1"}},

{ux, ".*", {git, "https://github.com/erlang-unicode/ux.git", "v3.4.1"}}

]}.

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

Подразумевается, что все эти зависимости тоже собираются Rebar. И они тоже могут иметь свой rebar.config и свои зависимости (транзитивные). Например, cowboy зависит от ranch. Если это так, то Rebar клонирует и соберет транзивные зависимости тоже.

Однако может быть так, что вам нужна какая-то библиотека, которая не собирается Rebar. Тогда зависимость указывается так:

{somelib, ".*", {git, "https://somewhere.com/somelib.git", "v1.0"}, [raw]}

Тогда Rebar скачает ее сорцы, но не будет компилировать. Вам придется собрать ее отдельно.

Кроме уже известной нам команды get-deps есть несколько других

check-deps проверяет, все ли зависимости клонированы. Не проверяет транзитивные зависимости.

list-deps проверяет, все ли зависимости клонированы в т.ч. транзитивные. Выводит информацию о каждой зависимости: имя приложения, номер версии, источник.

update-deps обновляет зависимости, клонирует свежие версии. Тут Rebar проверяет конфликты версий библиотек. И выдает ошибку, если одна и та же библиотека, но разных версий, является зависимостью. Интересно, что Rebar этого не делает в get-deps и compile :)

delete-deps удаляет зависимости, оставляет пустую папку deps.

Шаблоны

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

rebar create template= [var=foo,...]

Например, вот так можно создать новое приложение:

rebar create template=simpleapp appid=myapp

А вот так можно создать модуль gen_server:

rebar create template=simplesrv srvid=my_server

Для самых важных шаблонов есть сокращенный вариант:

rebar create-app appid=myapp

rebar create-node nodeid=mynode

Список всех шаблонов можно посмотреть командой list-templates

rebar list-templates

У этой команды есть странность. Она зачем-то рекурсивно обходит все каталоги внутри текущего каталога, и для всех найденных erlang-проектов показывает один и тот же список. Зачем нужно лазить по каталогам, а не понял. Если запустить в своем домашнем каталоге, то она будет работать долго. И у меня падает с ошибкой на каком-то проекте :)

Если запустить в пустом каталоге, то вывод будет таким:

yura ~/tmp $ rebar list-templates ==> tmp (list-templates) simplesrv: priv/templates/simplesrv.template (escript) (variables: "srvid") simplenode: priv/templates/simplenode.template (escript) (variables: "nodeid") simplemod: priv/templates/simplemod.template (escript) (variables: "modid") simplefsm: priv/templates/simplefsm.template (escript) (variables: "fsmid") simpleapp: priv/templates/simpleapp.template (escript) (variables: "appid") ctsuite: priv/templates/ctsuite.template (escript) (variables: "testmod") * basicnif: priv/templates/basicnif.template (escript) (variables: "module")

Rebar показывает имя шаблона, где он хранится в проекте rebar, и какие переменные можно подставить.

К сожалению, документации по шаблонам нет в вики. И rebar help create тоже не показывает ничего интересного. Так что нужно просто пробовать и смотреть, что получается.

Создадим приложение:

yura ~/p $ mkdir coolstuff; cd coolstuff yura ~/p/coolstuff $ rebar create template=simpleapp appid=coolstuff ==> coolstuff (create) Writing src/coolstuff.app.src Writing src/coolstuff_app.erl Writing src/coolstuff_sup.erl yura ~/p/coolstuff $ tree . └── src ├── coolstuff_app.erl ├── coolstuff.app.src └── coolstuff_sup.erl

1 directory, 3 files

Как видно, Rebar сгенерировал модуль приложения, модуль корневого супервизора и .app.src файл. Дал соответствующие имена файлам, и подставил соответствующие -module(name) конструкции в них.

Добавим в него модуль ген-сервер:

yura ~/p/coolstuff $ rebar create template=simplesrv srvid=my_server ==> coolstuff (create) Writing src/my_server.erl yura ~/p/coolstuff $ cat src/my_server.erl -module(my_server). -behaviour(gen_server). -define(SERVER, ?MODULE). ...

Добавим еще один модуль:

yura ~/p/coolstuff $ rebar create template=simplemod modid=my_cool_module ==> coolstuff (create) Writing src/my_cool_module.erl Writing test/my_cool_module_tests.erl yura ~/p/coolstuff $ cat src/my_cool_module.erl -module(my_cool_module).

-export([my_func/0]).

my_func() -> ok. yura ~/p/coolstuff $ cat test/my_cool_module_tests.erl -module(my_cool_module_tests). -include_lib("eunit/include/eunit.hrl").

Rebar создал не только модуль, но и тесты для него.

Все шаблоны можно найти на github в проекте rebar. в папке priv/templates.

yura ~/p/rebar/priv/templates $ ls -1 *.template basicnif.template ctsuite.template simpleapp.template simplefsm.template simplemod.template simplenode.template simplesrv.template

Итого их 7 штук.

Как они устроены? Довольно просто:

yura ~/p/rebar/priv/templates $ ls -1 simpleapp* simpleapp_app.erl simpleapp.app.src simpleapp_sup.erl simpleapp.template yura ~/p/rebar/priv/templates $ cat simpleapp.template {variables, [{appid, "myapp"}]}. {template, "simpleapp.app.src", "src/{{appid}}.app.src"}. {template, "simpleapp_app.erl", "src/{{appid}}_app.erl"}. {template, "simpleapp_sup.erl", "src/{{appid}}_sup.erl"}.

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

Эти три шаблона мы уже попробовали:

  • simpleapp -- создает приложение;
  • simplesrv -- создает gen_server модуль;
  • simplemod -- создает пустой модуль;

Еще есть:

  • simplefsm -- создает gen_fsm модуль;
  • basicnif -- заготовка для порта, создает erlang-модуль и с-файл;
  • ctsuite -- создает common test suite модуль в папке test;
  • simplenode -- самый сложный шаблон, создает файлы для релиза.

В свежей версии Rebar появился еще simplelib, но в установленном у меня Rebar такого шаблона нет.

Все эти шаблоны находятся внутри файла Rebar в архивированом виде. Если хочется что-то в них поменять, то есть два пути. Либо клонировать проект Rebar, изменить в нем шаблоны, и собрать свою версию Rebar. Либо положить шаблоны в папку ~/.rebar/templates.

yura ~ $ mkdir -p .rebar/templates yura ~ $ cp p/rebar/priv/templates/simplemod* .rebar/templates

И изменить их там.

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

В ~/.rebar/templates можно добавлять свои собственные шаблоны. Их довольно легко сделать, взяв за основу стандартные.

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

Rebar умеет запускать тесты. С этим довольно просто:

rebar eunit

При этом Rebar отдельно собирает проект с включенным макросом -DDEBUG=true в папку .eunit, так что это не влияет на собранные в обычном режиме beam-файлы.

Как уже сказано выше, обычно мы хотим запускать тесты только нашего проекта, а не тесты зависимых библиотек:

rebar eunit skip_deps=true

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

rebar eunit skip_deps=true suites=module1_test

Можно запустить тесты для двух-трех модулей:

rebar eunit skip_deps=true suites="module1_test,module2_test"

Можно запустить конкретные тесты в модуле:

rebar eunit skip_deps=true suites=module1_test tests=some rebar eunit skip_deps=true suites=module1_test tests="some,another"

Опция tests пока нестабильная, у меня, бывает, выдает ошибки.

Отчеты о тестах тоже сохраняются в папке .eunit в файлах TEST-module1_test.xml. И если в rebar.config включена опция cover_enabled, то сохраняются также отчеты о покрытии кода тестами в файлах module1_test.COVER.html.

Rebar также умеет запускать common test:

rebar ct [suites=] [case=]

Но я их не использую, так что не буду углубляться.

rebar.config

Еще одна важная тема -- конфигурирование Rebar.

Если ваш проект следует структуре OTP-приложения, не имеет зависимостей и не требует нестандартных опций при сборке, то rebar.config не нужен. Впрочем, его все равно лучше иметь, это сразу скажет другим разработчиком, что проект собирается Rebar.

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

Пример конфига есть в проекте ребар: rebar.config.sample там указано много разных опций с комментариями, но не все :)

Посмотрим некоторые из них.

erl_opts задают настройки компиляции:

{erl_opts, [debug_info, warn_export_all, warn_missing_spec, warning_as_errors, {parse_transform, lager_transform} ]}.

Поддерживаются все опции, которые есть у функции compile:file/2. Примеры:

  • debug_info -- включить отладочную информацию, нужную отладчику и xref тулу;
  • warnings_as_errors -- считать предупреждения ошибками, и не компилировать код;
  • {d, Macro} и {d, Macro, Value} -- определить макрос;
  • warn_export_all -- предупреждать об использовании export_all;
  • bin_opt_info -- предупреждать, если матчинг на binary может быть оптимизирован;

Можно переопределить стандартные папки:

  • src_dir -- папка с исходниками;
  • deps_dir -- папка с зависимостями;
  • target_dir -- папка для скомпилированных beam-файлов;
  • {erl_opts, [{i, "my_include"}]} -- папка с заголовочными файлами

Если в вашем проекте есть вложенные OTP-приложения, то нужны опции lib_dirs и sub_dirs.

{lib_dirs, ["deps", "apps"]}. {sub_dirs, ["apps/app1", "apps/app2"]}.

lib_dirs указывает папки, где нужно искать хедер-файлы, подключаемые через include_lib. А sub_dirs указывает папки, где находятся вложенные приложения.

Сохранять отчеты о покрытии тестами:

{cover_enabled, true}.

Удалять файлы при очиске проекта (rebar clean):

{clean_files, ["erl_crash.dump"]}.

Настройки для утилиты xref:

{xref_checks, [ undefined_function_calls, undefined_functions, locals_not_used, exports_not_used, deprecated_function_calls, deprecated_functions ]}.

Что касается rebar.config, надо сказать, что здесь документация слабая. Я пересмотрел много таких конфигов из разных проектов, и часто видел опции, нигде не документированные. Например:

{erl_opts, [ warn_missing_spec, warn_untyped_recod, fail_on_warning ]}.

Как они действуют, и действуют ли вообще как-нибудь, неизвестно :)

На самом деле все хорошо работает по-умолчанию, и настройки нужны по-минимуму. Самое главное, это {deps, []}, конечно. Все предупреждения включены по умолчанию, специально включать их не нужно. Полезная вещь warning_as_errors, хотя эта опция часто докучает. Тем, кто использует lager не обойтись без {parse_transform, lager_transform}. Вот и все, этого достаточно для большинства проектов.

Прочие возможности

rebar escriptize создание escript-приложения. Об этом чуть подробнее, потому что ребар сам является таким приложением.

escript -- это консольное приложение, которое должно работать как все консольные утилиты unix-подобных ОС: получать аргументы на входе, отрабатывать, выводить инфу на стандартный вывод, возвращать код возврата и т.д. Не типичное применение Erlang, но иногда полезное.

escript файл, как и все скриптовые файлы, начинается с заголовка

#!/usr/bin/env escript

Потом идут настройки для erlang vm. У самого rebar такие:

%%! -pa rebar/rebar/ebin

а потом возможны варианты:

  • исходный код Erlang;
  • бинарные данные скомпилированного beam-файла;
  • бинарные данные zip-архива, содержащего beam-файлы.

Rebar представляет собой 3-й вариант. Его даже можно распаковать, только сперва нужно переименовать файл, иначе он будет конфликтовать с именем папки внутри архива.

yura ~/tmp/look_inside_rebar $ mv rebar rebar_file yura ~/tmp/look_inside_rebar $ unzip rebar_file Archive: rebar_file warning [rebar_file]: 51 extra bytes at beginning or within zipfile (attempting to process anyway) creating: rebar/ creating: rebar/ebin/ inflating: rebar/ebin/getopt.beam inflating: rebar/ebin/mustache.beam inflating: rebar/ebin/rebar.app inflating: rebar/ebin/rebar.beam ... creating: priv/ creating: priv/templates/ inflating: priv/templates/basicnif.c inflating: priv/templates/basicnif.erl inflating: priv/templates/basicnif.template ...

Как видим, внутри beam-файлы и шаблоны. Ну вот, Rebar умеет создавать такие приложения, в т.ч. самого себя :)

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

rebar doc генерация документации утилитой EDoc. Ну тут рассказывать особо нечего, аналогичные тулы есть во многих других языка.

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

Comments (8)

Чем хорош Erlang вообще и для веб-разработки в частности?

Доклад для митапа белорусского сообщества Ruby-разработчиков. Митап был 25 января 2014 года, и был посвящен языку Erlang и его применению в вебе.

Есть видео моего доклада и презентация.

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

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

Чем хорош Erlang вообще и для веб-разработки в частности?

Сегодня я расскажу: - Что такое Erlang - Архитектура веб-серверов - Преимущества Erlang для веб - Примеры использования: компании и продукты - Место Erlang в вебе сейчас и в будущем

Что такое Эрланг

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

Представьте себе: середина 80-х годов. Java еще нет. C++ еще нет. Есть C, Fortran, Cobol, Prolog, LISP. ООП только зарождается -- Modula, Smalltalk. Мощности железа так себе, не сравнить с нынешними. А компании Ericsson уже прямо сейчас нужно обслуживать много клиентов и много запросов на своем телекоммуникационном оборудовании. Erlang изначально создавался для этого, полировался в этой сфере больше 20 лет. И сейчас мало какая технология может с ним потягаться. Только немногие, написанные на С, как nginx.

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

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

Процессы бывают разные в разных ОС. Более того, внутри одной ОС они тоже бывают разные. Как образец для рассматривания и сравнения мы возьмем такой процесс, который создается в unix-подобных ОС системным вызовом fork().

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

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

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

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

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

Поскольку такие процессы относительно медленные и относительно неэффективные, возникло желание создавать паралельные потоки выполнения опираясь на более простые и быстрые сущности. И были придуманы нити (thread). Это легковестные процессы, которые запускаются внутри процесса ОС и разделяют его ресурсы между собой.

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

Важность всего этого будет ясна дальше :)

Архитектура веб-серверов

Давайте выделим следующие уровни в работе веб-приложения:

  • принятие запросов и роутинг
  • бизнес-логика
  • хранение данных

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

Разработчику привычно работать на уровне бизнес-логики, и часто он неплохо ориентируется в уровне хранения данных. Ну там, может с умным видом порассуждать о преимуществах MongoDB (а некоторые даже умеют ей пользоваться :) Между тем мало кто понимает, что происходит на 1-м уровне. И действительно, а зачем там чего-то понимать? Этот уровень как-то там работает сам по себе, и слава богу :) Необходимость туда лезть обычно не возникает.

Обычно, но не всегда. Бывает, что возникает необходимость обслуживать 10К, 100К, 1M запросов в секунду. И тогда 1й уровень становится очень важным.

Фишка Erlang в том, что он хорош на всех 3х уровнях.

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

Зато если нам нужно выйти за пределы второго уровня, то тут преимущества становятся очевидны. На 3-м уровне Erlang вполне справляется с кешированием данных в оперативной памяти и заменяет специализированные тулы, типа memcached или Redis.

А уж на первом уровне Erlang просто прекрасен. Это его родная стихия. Что бы понять, почему, нужно вернуться к нашим процессам ОС.

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

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

Отдельным, 4-м уровнем можно выделить отложенное выполнение длительных задач, типа: сбор большого количества данных по базе и их анализ, конвертация видео и графики, анализ логов и т.д. Для этого используют разные инструменты, от простого cron до мощных AMQP (Advanced Message Queuing Protocol) тулов, типа RabbitMQ.

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

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

Преимущества Erlang для веб

Из-за эффективного 1-го уровня Erlang позволяет на том же железе обслуживать больше запросов. В некоторых случаях, например, в сравнении с проектами, построенными на Ruby On Rails, на порядки больше.

Там, где сферический в вакууме Ruby-код обслужит 500-800 запросов в секунду, Erlang-код, написанный программистом средней квалификации, обслужит 5-8К запросов в секунду. А код Erlang-гуру выдаст и 50-100K запросов в секунду.

Но это мы говорим об одном сервере. А Erlang предназначен для разработки распределенных систем. Так что если вы изначально планируете кластер из нескольких (или многих десятков) машин, то Erlang в таком проекте самое место :) Ибо на нем разрабатывать такие системы настолько просто, насколько это вообще может быть простым.

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

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

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

Маргинальные технологии и языки -- это те, которые не имеют большого количества разработчиков и проектов: - Функциональные языки: Haskell, OCaml, Erlang; - Диалекты LISP (Сейчас набирает популярность и становится модной Clojure); - Языки молодые: Go, Rust - и т.д.

Лет 5 назад и Ruby можно было причислить к маргинальным языкам, но сейчас это скорее мейнстрим. С точки зрения Erlang разработчика, так уж точно мейнстрим :)

Недостатки всех маргинальных технологий:

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

Недостатки собственно Erlang:

  • плохая работа со строками, поддержка Unicode появилась поздно и не очень качественная;
  • динамическая типизация (частично компенсируется использованием dialyzer)

Пару слов о фреймворках:

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

Вы все хорошо знаете, что такое современные веб фреймворки. Я же не очень хорошо. Но я проникся их мощью, когда полтора месяца назад пришел на работу в новую компанию и начал работать над новым проектом. Мы используем Erlang и Python/Django. Пользовательская часть сайта делается на Erlang, а зона администрирования на Django. Мне нужно было делать и то, и другое. Django я не знал совсем, а с Python был немного знаком.

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

Полагаю, Ruby on Rail работает аналогично. Так вот, такого на Erlang нету. Есть фреймворк Chicago Boss, сделанный по идеям Ruby on Rail, но вряд ли он дотягивает до оригинала.

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

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

Однако я в теме фреймворков ориентируюсь плохо. Больше об этом расскажет Максим Сохацкий :)

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

Учитывая аудиторию, начнем с компаний, хорошо знакомых Ruby-разработчикам :) И первой компанией в этом списке будет...

37signals

Один из продуктов этой компании -- Campfire -- групповой веб-чат для общения команды.

Mark Imbriaco на Erlang Factory London 2009 рассказал об этом проекте: Campfire Loves Erlang

Первая версия была сделана на RubyOnRails/MySQL. Но она не справлялась с планируемой нагрузкой в 1500 запросов в секунду и не пошла в production. Затем часть кода была переписана на C. Эта версия была очень эффективна по CPU и памяти и уверенно справлялась с нагрузкой, но была не масштабируема, требовала по 1 процессу ОС на каждое клиентское соединение. Поэтому код опять был переписан, уже на Erlang. Эта версия оказалась такой же эффективной, как вариант на С, но при этом легко масштабировалась.

В презентации Марка есть такая таблица:

Ruby C Erlang
LOC 127 397 273
Req/sec 250-350 1800 1800
Responce 20ms 2-3ms 2-3ms
OS Processes n/a 80 1
Extensible Yes No Yes

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

Я только уточню еще, что Erlang использует не 1 процесс ОС, а по 1 процессу на каждое ядро CPU.

Heroku

Облачная PaaS-платформа. Сначала поддерживала хостинг только Ruby проектов, сейчас поддерживает проекты на нескольких языках (в т.ч. и на Erlang). Heroku хостит десятки тысяч проектов в многопользовательском окружении

Heroku активно использует Erlang. На нем реализованы ключевые части сервиса: - Балансировщик нагрузки - Проксирование и роутинг запросов (HTTP Routing Mesh) - Клей, для склеивания разных компонентов в общую инфраструктуру - Централизованный сбор и обработков логов сервиса и хостящихся проектов (Logplex)

Про Erlang, в сравнении с другими языками, они говорят следующее: Perl: Making easy things easy and hard things possible! Ruby: Making easy things trivial and hard things fun! Erlang: Making easy things possible and impossible things trivial!

Разработчики Heroku не редко выступают на конференциях Erlang Factory.

Blake Mizerany и Orion Henry на Erlang Factory London 2009: How the Cloud Got Its Groove Back

Jacob Vorreuter на Erlang Factory London 2011: Utilizing Redis in distributed Erlang systems

Geoff Cant на Erlang Factory SF Bay Area 2013 про Logplex: High Throughput Erlang

Fred Hebert, хорошо известный среди Erlang-разработчиков, автор отличной книги "Learn You Some Erlang for Great Good!" на NYC Erlang Factory Lite 2013: Why Heroku (still) uses Erlang

Есть еще длинный и эпичный пост Fred Hebert Troubleshooting Down the Logplex Rabbit Hole о борьбе с утечками памяти в Logplex, но для его чтения требуется глубокое знание Erlang платформы :)

Github

Вряд ли нужно объяснять, что это такое. Зато нужно сказать, что исходные коды Erlang/OTP хранятся именно там :)

Github также использует Erlang как часть своей инфраструктуры.

Сервис Erlectricity, позволяет Ruby-узлам обмениваться сообщениями друг с другом, аналогично, как это делают Erlang-процессы. Erlang-узел создает слой коммуникации, и к нему подключаются Ruby-узлы.

Сервис GitHub Pages, позволяет создавать статические веб сайты. На самом деле, html-код, который этот сервис отдает клиентам, не хранится в виде html-файлов на диске. Сервис является распределенным, все данные хранятся в Riak, а бизнес-логика реализована на Erlang.

Tom Preston-Werner, со-основатель github, любит и Ruby, и Erlang, и ищет способы совместить преимущества этих двух языков. В его блоге можно найти несколько постов на эту тему.

Ну и разработчики github тоже отметились на Erlang Factory:

Tom Preston-Werner на Erlang Factory SF Bay Area 2009 Mixing Erlang and Ruby with Erlectricity

Tom Preston-Werner на Erlang Factory London 2010: Contributing to Erlang: Making the most of Git and GitHub

Jesse Newland на Erlang Factory SF Bay Area 2012: Rewriting GitHub Pages with Riak Core, Riak KV, and Webmachine

Facebook Chat backend in Erlang

Данные 2011 года: 1 миллиард сообщений в сутки 10 миллионов активных пользователей в пике 100 серверов

Eugene Letuchy на Erlang Factory SF Bay Area 2009 Erlang at Facebook

Amazone SimpleDB распределенная база данных, часть Amazon Web Services http://en.wikipedia.org/wiki/SimpleDB

Yahoo! Использует Erlang в своих проектах. Подробностей не нашел.

Yogish Baliga на Erlang Factory SF Bay Area 2010 Deploying Erlang into a Large Organization - A Case Study

Yandex Серверная часть Яндекс.Диск.

Как мы делали Яндекс.Диск

DemonWare Mассовоая многопользовательская игра Call of Duty Black Ops 2.5 млн пользователей онлайн. Логика игры на питон, инфраструктура на эрланг.

Malcolm Dowse на Erlang Factory London 2011 Erlang and First-Person Shooters in online games

Wooga Серверная часть для социальных игр.

Knut Nesheim на Erlang Factory London 2011 Designing online games for scale with Erlang

WhatsApp Instant messaging для мобильных устройств.

Rick Reed на Erlang Factory SF Bay Area 2012 Scaling to Millions of Simultaneous Connections

Известные продукты на Erlang

Riak Распределенная Key-Value база данных

CouchDB Распределенная документ-ориентированная база данных

Rabbit MQ Брокер сообщений, одна из реализаций AMPQ (Advanced Message Queuing Protocol)

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

Место Эрланг в вебе сейчас и в будущем

Итак, из примеров выше ясны области применения Erlang: - Cloud Platforms - Messaging - Games - Databases

А что в будущем?

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

Ждем SPDY и HTTP 2.0. Они тоже подразумевают, что соединение клиента с сервером удерживается довольно долго.

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

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

Comments

Курс по Erlang в Днепропетровске

Давеча ездил в Днепропетровск по приглашению ПриватБанка, чтобы провести курс по Erlang для их программистов.

У ПриватБанка есть проекты на Erlang, причем уже в продакшене. Свой опыт они оценили позитивно. По объёму кода, скорости разработки и простоте поддержки Erlang оказался лучше, чем Java. И теперь у ПриватБанка большие планы на него. Вплоть до переписывания основного проекта Приват24, изначально написанного на Java, и нынче уже плохо справляющегося с нагрузками. Веру ПриватБанка в Erlang символизирует флаг с логотипом Erlang, висящий у главного офиса, вместе с флагами стран, где есть филиалы и партнеры банка.

Флаг с логотипом  Erlang

Тут можно задаться вопросом: почему пригласили именно меня, никому неизвестного программиста из Минска? При том, что, скажем в Киеве есть более опытные Erlang программисты. И они тоже проводят обучающие курсы.

А все потому, что 2 года назад я выступал с рассказом про Erlang на встрече scala.by. После той встречи осталось видео:

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

И какой из этого вывод? Будьте активны, выступайте на конференциях, делитесь опытом :)

Подробности о курсе

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

Слушателями были программисты из разных отделов ПриватБанка, как оказалось, с самым разным уровнем владения Erlang, и они по-разному воспринимали материал. Мне пришлось выбирать, на какой уровень ориентироваться. Решил ориентироваться на средний и выше среднего, а слабо знакомым с языком было трудно.

Было 3 дня занятий по 6 часов. Около 30 слушателей в аудитории + онлайн трансляция куда-то, которую смотрели еще кто-то :)

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

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

Третий день делали с нуля полноценный проект по всем правилам -- key value storage. Процесс проходил примерно так же, как и во второй день. Но код уже был посложнее, нюансов больше. Все фичи сделать не успели, но получили storage с сохранением на диск, с HTTP API, и покрытый тестами. Треть аудитории, человек 10, справились с этим. Этот день тоже был для меня изматывающим. Особенно трудно было вылавливать ошибки на самым разных компах, с разными ОС, разными редакторами и т.д. Но совместными усилиями мы справлялись.

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

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

Впечатление от команды ПриватБанка

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

В основном я общался с людьми из Центра Электронного Бизнеса. Это подразделение хоть и является частью ПриватБанка, но имеет некоторую свободу и больше похоже на продуктовую IT компанию. Возглавляет лабораторию Витязь Александр Павлович. Собственно, вся эта движуха с Erlang: проекты, обучающий курс, флаг возле офиса -- его инициатива. Он не похож на топ-менеджера банка. Нет скучной серьезности и пафоса, а есть энтузиазм и желание захватить мир своими проектами. Энтузиазм заразительный, каким он и должен быть :)

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

Меня встретили гостеприимно, поселили в пафосном отеле, опекали и не давали скучать -- ужинал все время с разными интересными людьми. Спасибо Ольге Худик за заботу :)

А в Днепропетровск я еще раз съезжу в феврале, на конференцию ErlangDnipro.

Comments

Опыт использования Erlang в разработке многопользовательской игры

Выступил с докладом на "IT_Share. GameDev Web".

Видео тут.

Презентация тут.

Полный текст ниже.

Кратенько про Erlang

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

Разработан в компании Ericsson в середине 80х годов. Создавался он не из научного интереса, а для решения конкретных практических задач. А именно, для коммутаторов широкополосных телефонных линий. То есть, еще до появления этих ваших интернетов Erlang уже был предназначен для высоких нагрузок и обслуживания большого числа пользователей :)

В любой книге про Erlang вы прочитаете, что упор в дизайне языка сделан на 4 фичи:

  • Модель многопоточности, основанная на легких процессах, обменивающихся сообщениями;
  • Распределенность, сетевая прозрачность;
  • Устойчивость к ошибкам;
  • Горячее обновление кода.

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

Кратенько про проект

Мы, компания Diesel Puppet, использовали Erlang в 3х проектах, один из которых дожил до релиза, и уже 5 месяцев находится в production. Проект этот с одной стороны не очень сложный. Это игра "Русское лото", с мобильным (iPhone/iPad) клиентом и серверной частью на Erlang. (На подходе flash-клиент для вконтакте). С другой стороны, многопользовательские real time игры, даже такие простые, таят в себе свои сложности.

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

Фичи Erlang в теории и на практике

Многопоточность. Теория.

Разработчики, использующие мейнстримовые языки, такие как Java или .NET, знают, что многопоточность -- это очень сложно. Нужно выучить назубок Java Concurrency in Practice. Мьютексы и семафоры должны сниться по ночам. И все равно неуловимые и невоспроизводимые баги будут блуждать по коду. Лучше вообще с этим не связываться. Но в Erlang все не так.

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

Каждый поток имеет свою собственную область памяти и не делит ее с другими. Нет общей области памяти, разделяемой между процессами, и нет типичных проблем многопоточных систем: race condition, dead lock и т.д. (На самом деле кое где есть и разделяемая память, и оные проблемы, но эти нюансы пока опустим. На начальном уровне можно считать, что их нет :)

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

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

Многопоточность. Практика.

На практике процессы являются основными кирпичиками, из которых строится архитектура проекта.

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

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

Есть пул из 50 соединений с базой данных. Каждое соединение -- отдельный процесс (там даже больше, вроде бы по 3 процесса на одно соединение).

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

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

Вообще есть сходство с ООП. Процессы являются аналогами объектов. Они инкапсулируют данные в своем состоянии, предлагают публичное АПИ для работы с ними, имеют приватное АПИ для собственных нужд. У них даже конструкторы и деструкторы есть :) И взаимодействие через отправку сообщений, а не через вызов методов, это вполне в духе ООП. Хотя Erlang относится к функциональной парадигме, но есть нюансы. Жаль, нет времени развить эту тему :)

Когда я в первый раз получил в своем коде dead lock, то даже удивился, что такое бывает. Бывает, бывает, если не знать некоторых нюансов работы gen_server. К счастью, это легко диагностируется и легко исправляется. Позже и race condition получал, но и с ними то же не трудно. То есть, такие проблемы бывают и в Erlang, но их фиксить не сложнее, чем "обычные" баги.

Кто делал похожие (многопользовательские) сервера на python, или node.js, или еще на чем-то, тот знает, что нужно запустить несколько нод на одной машине, чтобы утилизировать все ядра процессора. По одной ноде на каждое ядро. Нужно наладить коммуникацию между нодами. А для этого подключить что-нибудь типа Rabbit MQ. С Erlang ничего этого не нужно. Одна нода эффективно использует все имеющиеся в наличии ядра. И все процессы в этой ноде общаются без лишних посредников. Все это проще и в разработке, и в развертывании, и в диагностике, и в поддержке.

Многопроцессорная модель Erlang -- это самое ценное, что в нем есть. Это самая суть языка, из которой вытекает все остальное. Не зря она заимствуется при разработке других языков, например Scala и Go.

Распределенность и сетевая прозрачность

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

Итак, вам не нужно возиться с сокетами и сериализацией данных, чтобы наладить общение нескольких нод друг с другом. Нодам нужно только знать имена друг друга (node-name@server-name), чтобы собраться в кластер. И дальше работает сетевая прозрачность -- любой процесс может оправить сообщение другому процессу в другой ноде точно так же, как и процессу в своей ноде.

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

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

Устойчивость к ошибкам. Теория.

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

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

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

Третий уровень защиты -- распределенность. Erlang-узлы объединяются в кластер, и это позволяет сохранять работоспособность при падении одной ноды.

Устойчивость к ошибкам. Практика.

На практике, конечно, все это не спасает от проблем. Волшебной таблетки нет :) Если у вас баг в логике, то сервер не может нормально обслуживать клиентов, пока не найдешь и не исправишь баг.

Бывали у меня косяки, и серьезные косяки. Нет, нода целиком никогда не падала, но были отказы в обслуживании. А для клиента нет разницы, работает ли сервер или не работает совсем, если он не обслуживает запросы.

Так что по части обработки ошибок большой разницы в сравнении с той же Java нет. Разве что не нужно везде и всюду пихать try/catch. В Erlang реакция по умолчанию на ошибку -- записать в лог и перезапустить процесс. В большинстве случаев это подходит, и явную обработку ошибок писать не нужно. Подход Let it crash. Ну а где не подходит, там можно и try/catch поставить.

Горячее обновление кода. Теория.

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

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

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

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

В простых случаях это хорошо работает. Но бывают случаи сложные.

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

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

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

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

Горячее обновление кода. Практика.

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

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

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

Мы подняли отдельный тестовый сервер, и выкатывали изменения сперва на него. Production сервер обновляли теперь уже редко, раз 1-2 недели (после каждого спринта). Изменений накапливалось много, обновлять их по-горячему уже было проблематично из-за вышеописанных сложных случаев. Так что перешли на обновление с рестартом сервера.

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

В итоге роль горячего обновления уменьшилась, но я все равно его использую. Есть две ситуации, где оно полезно даже на production сервере.

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

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

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

Борьба за качество проекта

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

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

Не зря в функциональном программировании (и не только в нем), так ценятся функции без побочных эффектов. Их любо-дорого тестировать. Написал юнит-тест, с разными аргументами, с правильными, с неправильными, с граничными случаями. Запустил -- и уверен на 100%, что функция правильная.

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

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

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

Вот есть такой язык -- Haskell. Якобы язык без побочных эффектов. Чтобы заставить его сделать что-то годное, нужны монады. Без монад он ну вообще бесполезен. А монады, это замаскированный способ таки добавить побочные эффекты, просто на другом уровне абстракции :)

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

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

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

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

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

Далее этих клиентов можно запускать в двух режимах. Можно запустить небольшое количество и не на долго, на 3-5 минут. Потом вручную просмотреть сообщения в консоли на клиентах и на сервере, нет ли в них ошибок или необычных данных. А можно запустить несколько сотен клиентов на 10-15 минут. Получится режим стресс-теста с нагрузкой на сервер порядка 5-8 тысяч запросов в секунду. Тут уж все логи вручную просмотреть не получится, но ошибки, если они будут, незамеченными не останутся.

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

Недостатки Erlang

Было бы неправильным умолчать об этом :)

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

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

Динамическая типизация -- для меня это недостаток. Я бы предпочел статическую, но без фанатизма :) В дело обеспечения качества продукта статическая типизация вносит свой весомый вклад.

Есть некоторый хаос с библиотеками третьих сторон, не входящих в стандартную поставку Erlang. Устоявшихся библиотек, которым можно доверять, не так много. Большинство не стабильны, слабо документированы, часто не имеют нормальной версионности, зато имеют много форков :) Мне самому пришлось форкать драйвер для работы с PostgreSQL, чтобы пофиксить в нем какую-то мелочь.

Выводы

Самый главный -- будем ли мы применять Erlang в следующих проектах? Однозначно да :)

Comments (3)

Год с Erlang, продолжение

Прошло 2 месяца после первой статьи, где я рассказывал про опыт использования Erlang в игровом проекте. И за эти 2 месяца много воды утекло, и кое-что изменилось. И поэтому нужно продолжение :)

Еще раз про тестирование

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

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

Нет, я не стал выдумывать хитрые моки, чтобы покрыть все юнит-тестами. И не стал сочинять сложный тестовый стенд, прогоняющий внутри себя игры и эмулирующий реальных пользователей. Сделал проще: Консольное Mac OS приложение запускает N десятков (сотен) тестовых клиентов, каждый из которых дергает все возможные методы серверного АПИ. Клиенты сгруппированы по функциям. Часть из них дергает регистрацию и авторизацию, часть АПИ игры, часть АПИ покупок и т.д. Ответы сервера собираются, но не анализируются автоматически. Еще ведется учет дисконнектов (они, обычно, вызваны ошибками на сервере).

Далее этих клиентов можно запускать в двух режимах. Можно запустить небольшое количество и не на долго, на 3-5 минут. Потом вручную просмотреть логи на клиентах и на сервере, нет ли в них ошибок или необычных данных. А можно запустить несколько сотен клиентов на 10-15 минут. Получится режим стресс-теста с нагрузкой на сервер порядка 5-8 тысяч запросов в секунду. Тут уж все логи вручную просмотреть не получится, но ошибки на сервере, если они будут, незамеченными не останутся.

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

Обновление сервера по-взрослому

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

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

Впрочем, решилось это относительно просто. Каждая игра -- это gen_server процесс, и его состояние укладывается в некий довольно сложный record. Каким бы сложным ни был этот рекорд, он легко сериализуется в бинарник с помощью term_to_binary и десериализуется обратной функцией. Бинарник же можно положить в БД перед остановкой сервера. Так что процесс получился такой:

  • запускаем из удаленной консоли команду adm:stop_server();
  • сервер перестает принимать запросы клиентов и останавливает таймеры -- состояния замораживаются;
  • сервер обходит существующие комнаты, сериализует состояния, складывает в БД;
  • закрывает соединения и останавливается;
  • клиенты, потеряв соединение, делают попытки реконнекта каждые 5 секунд;
  • архивируем старую версию кода и выкладываем на ее место новую;
  • модифицируем структуру БД, если нужно;
  • запускаем новый сервер;
  • он достает из БД состояния игр, десериализует их, и запускает процессы игр;
  • начинает принимать соединения от клиентов;
  • клиенты реконнектятся, подключаются к своим играм, и продолжают играть.

Со стороны клиента это выглядит так, что игра замирает на 5-10-15 секунд, потом продолжается дальше.

Еще раз про горячее обновление кода

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

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

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

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

Comments

Год с Erlang

Прошло чуть больше года с тех пор, как я стал Erlang-разработчиком. Конечно, весь этот год я занимался не только Erlang. Попутно я освоил iOS-разработку и делал клиентские приложения для iPhone/iPad. Но при этом считался разработчиком серверной части, и делал оную для 3-х проектов. Так что спустя год могу поделиться своими впечатлениями об Erlang.

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

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

Устойчивость к ошибкам

И начнем мы с отказоустойчивости, этой знаменитой фичи Erlang :)

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

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

Так что ошибки бывают двух видов -- те, при которых сервер продолжает обслуживать клиентов, и те, при которых не обслуживает.

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

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

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

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

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

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

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

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

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

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

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

Динамическая типизация

Ох да, знаю, холиварная тема :) Вы хотите поговорить об этом? Я хочу :)

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

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

Да, я читал Мартина Фаулера, знаю, как теоретически нужно делать рефакторинг: покрыть все юнит-тестами и двигаться маленькими шагами, после каждого шага запуская тесты.

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

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

Крайне желательно продумать систему типов в масштабах всего проекта. Erlang поддерживает описания типов, наподобие алгебраических типов данных в Haskell. Компилятор не сильно обращает внимания на эту систему, а вот Dialyzer -- да. Затем нужно написать -spec для каждой функции, где аккуратно указать типы аргументов и возвращаемого значения. И вот, ура-ура, Dialyzer находит нам косяки в самых неожиданных местах, где мы думали, что у нас все ок.

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

Dialyzer можно запустить вручную, когда надо. Обычно лень. А раз он не запускается, но и spec писать лень. И так оно живет, до поры до времени, до следующего серьезного рефакторинга. Перед которым все приводится в порядок -- spec пишутся, код исправляется до тех пор, пока не понравится dialyzer, и тогда можно рефакторить. Результат рефакторинга тестируется вручную. И это вторая причина, почему на сервере в продакшене могут быть серьезные ошибки.

Так проект и живет: фаза клепания фич, фаза приведения в порядок, и по кругу.

Обратная совместимость

Головная боль и главный источник проблем :) Как я уже говорил, клиент у нас -- iOS приложение. Пользователь устанавливает приложение на свой iPhone, и потом отнюдь не всегда хочет его обновлять. Но при этом хочет, чтобы оно работало :) Но и те пользователи, которые установят обновление, получат его не быстро. Нужна неделя, чтобы обновление получило апрув у Эппл. Поэтому мы имеет примерно такую картину:

  • часть пользователей имеют на своем iPhone версию 1.0 приложения;
  • другая часть, которые не поленились обновиться, или скачали приложение позже, имеют версию 1.1;
  • уже выпущена, но ожидает апрува версия 1.2;
  • разработчик работает с версией 1.3;

И со всеми этими клиентами сервер должен работать. Я могу добавить в АПИ что-то, но не могу ничего убрать.

Помните, я говорил про ограниченность автоматического тестирования, и приоритет ручного? Так вот, после каждого изменения на сервере, нужно вручную тестировать клиентов всех версий :trollface

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

Обратная совместимость -- третья причина, почему на сервере в продакшене могут быть серьезные ошибки.

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

Горячее обновление кода

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

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

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

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

Я обновляю сервер практически ежедневно. А перегружаю довольно редко. Может, раз в 2 недели, где-то так. Не любое обновление можно загрузить по-горячему. Если были изменения в структуре супервизоров, или в рекордах, то проще не маяться, а перегрузить сервер. При этом у пользователей прервутся и не восстановятся текущие игры. Увы. Пока что мы просто возвращаем им ставки. Позже сделаем восстановление игр.

Простой проект

Я думал, что это будет простой проект. Ну что может быть сложного в русском лото? Собрал 6 юзеров в одной комнате, дал им карточки, генерируй бочонки, да проверяй, как они закрывают клетки. Фигня же. Да тут работы на месяц, не больше :)

Да, прототип я сделал за месяц, и клиент, и сервер. До релиза понадобилась еще 2 месяца. Эх, сколько времени нужно на создание дизайна и реализации его в клиенте! А, казалось бы, 3 экрана, 2 попапа.

И вот 3 месяца после релиза. Куча изменений и на клиенте, и на сервере. Версия 1.5, ждущая сейчас апрува от Эппл, далеко ушла от версии 1.0. Один я уже не справляюсь, по клиенту работает второй разработчик.

Не бывает простых проектов :)

Comments

Distell -- друг Erlang разработчика

Жил я долго и счастливо без Distell, используя Emacs и erlang-mode, и чувствовал себя неплохо. Помимо всего прочего, использовал я sync. Sync представляет собой otp-приложение, которое нужно запустить на ноде, после чего он собирает инфу обо всех сорцах и следит за изменениями в них. Как только какие-то сорцы меняются, sync перекомпилирует их и перезагружает измененные модули на ноде. Довольно удобно -- редактируешь код, сохраняешь файл, и изменения тут же подхватываются.

Однако через какое-то время sync стал утомлять своими побочными эффектами. К сожалению, нельзя указать, за какими именно сорцами нужно следить. Sync сам берет инфу из рантайма и следит не только за моим кодом, но и за всеми зависимостями в папочке deps. Вроде бы это не должно мешать, ведь исходники зависимостей не меняются. Но нет, почему-то sync перегружает модули epgsql (драйвер для работы с PostgreSQL) и yaws (веб-сервер). Случается это много раз за день, в непредсказуемые моменты времени, и сопровождается обильными сообщениями на много экранов в консоли. Этот лишний информационный шум изрядно затруднял чтение интересующих меня сообщений. В итоге это мне надоело, и я стал присматриваться к альтернативам.

Это была одна причина попробовать Distell. Есть и другая.

Примерно месяц я работал в Intellij IDEA с Erlang-плагином. По причинам, о которых будет сказано позже, в другом посте, я решил вернуться к Emacs. Но теперь уже возможностей erlang-mode мне было мало. А Distell, как оказалось, дает практически все, что есть в Intellij IDEA.

Я готовлю подробное сравнение Intellij IDEA + Erlang-плагин с одной стороны, и Emacs + ido + erlang-mode + Distell с другой стороны. Об этом будет отдельная статья. Пока скажу только, что IDEA будет очень хорошим вариантом для тех, кто хотел бы разрабатывать на Erlang, но привык пользоваться мощной IDE и неуютно чувствует себя в Emacs.

Итак, в чем фишка Distell? Он представляет собой расширение Emacs, которое умеет общаться с работающей Erlang-нодой. Таким образом он имеет всю актуальную информацию из рантайма и может выполнять свои запросы на ноде. Поэтому ему не нужен синтаксический анализ исходного кода, Distell и так знает, какие модули и функции там есть. Отсюда умный автокомплит, навигация по коду, заменяющая ненужный TAGS, find usages и т.д.

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

Итак, нам нужна замена sync, чтобы с удобством обновлять измененные модули на ноде. Distel предлагает две функции:

  • erl-reload-module C-c C-d L
  • erl-reload-modules C-c C-d r

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

В erlang-mode есть функция erlang-compile, но она практически бесполезна. Потому как наш проект, конечно, является otp-приложением правильной структуры (ebin, include, src, priv, test), и собирать его нужно через rebar :)

Можно воспользоваться более общей функцией compile, если сказать ей, какой командой собирать проект. Например:

  (compile "cd ..; rebar compile")

или

(compile "cd ..; make")

если у вас есть свой Makefile с более сложной сборкой.

Итак, сперва собираем

  M-x compile "cd ..; make"

Потом перегружаем

  C-c C-d L

Гм, неудобно. Много букв писать надо :) Ну для начала зададим дефолтную команду компиляции:

(setq-default compile-command "cd ..; make")

Затем повесим шоткат на compile, и шоткат попроще на reload:

  (global-set-key [f6] 'compile)
  (global-set-key [f7] 'erl-reload-module)
  (global-set-key [f8] 'erl-reload-modules)

Уже лучше. F6, F7 и готово. Но все-таки хотелось бы сделать все в одно действие. Не вопрос, пишем свою функцию и вешаем шоткат на нее:

(defun my-sync()
  (interactive)
  (compile "cd ..; make")
  (erl-reload-modules))
(global-set-key [f8] 'my-sync)

И вот тут начинаются проблемы. Оказывается erl-reload-modules без аргументов вызвать нельзя, она требует ссылку на ноду. Ссылку эту взять негде. Пришлось лезть в сорцы Distell и глядеть, как там все реализовано. Оказалось, там есть функция erl-target-node. Но мой скрипт ее не видит и не может вызывать. Посему нужно залезть в path/to/distell/elisp/erl-service.el, найти 26 строку, где определена erl-reload-modules, и добавить (interactive). После этого ее можно вызывать :)

Учтем еще пару нюансов:

  • перед сборкой нужно сохранить буфер
  • compile работает асинхронно, поэтому вызов erl-reload-modules произойдет раньше, чем компиляция закончится.

Посему нужно делать так:

(defun my-sync()
  (interactive)
  (save-buffer)
  (compile "cd ..; make") ;; asyn call, emacs doesn't wait for compilation end
  (sleep-for 2)
  (erl-reload-modules (erl-target-node)))
(global-set-key [f8] 'my-sync)

Теперь все работает как надо.

Comments (16)

dp_push -- Erlang библиотека для работы с Apple Push Notification Service

Библиотека для работы с APNs от dieselpuppet.com.

APNs -- что это и зачем

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

Но вот пользователь запустил другое приложение, или положил девайс в карман, или оказался где-то, где нет WiFi. И увы, связи между сервером и вашим приложением больше нет. А серверу нужно доставить какие-то данные, или уведомить о каком-то событии.

Для такого случая существует Apple Push Notification Service.

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

Как это работает

Ваш сервер соединяется через SSL сокет с APNs и передает сообщение в определенном формате. Сообщение содержит payload -- собственно данные, которые нужно передать, и device token -- идентификатор iOS устройства, которому адресовано сообщение.

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

APNs имеет хорошую документацию, где описаны все подробности.

Так же рекомендую замечательный тутор, который дает хороший старт. И, в частности, раскрывает непростую тему сертификатов :)

dp_push -- что это и зачем

Поскольку работать с APNs нужно практически в каждом нашем проекте, решено это унифицировать и вынести в отдельную библиотеку Diesel Puppet push. Никаких тайных секретов в этом функционале нет, нужно просто аккуратно его реализовать. Посему мы смело выкладываем модуль в open source, под MIT лицензией.

Реализация довольно простая, и представляет собой OTP-приложение из 4х модулей.

dp_push.erl Главный модуль, запускает приложение и является фасадом к функциям библиотеки.

dp_push_sup.erl Супервизор, запускает dp_push_sender и следит, что бы он не падал :)

dp_push_sender.erl gen_server, выполняет send запросы и работает с feedback сервисом (о котором ниже).

dp_push_apns.erl Содержит набор низкоуровневых функций для формирования пакетов, установки SSL соединения, отправки данных.

Приложение требует кое-каких настроек. Они, впрочем, довольно очевидные -- куда конектится, какой сертификат использовать, и настройки для feedback сервиса (о котором ниже).

device token

Немного подробнее о токене. iOS устройство получает токен у APNs сервера (подробности в документации). Потом ваше клиентское приложение должно передать его вашему серверу. А сервер будет использовать для отправки сообщений.

Токен имеет размер 32 байта, и выглядит примерно так:

  8253de13 f71d310d 05a13135 e09e09b6 32c478d5 32313723 1f04a7c7 b5de947d

Работая с ним в Erlang, нужно иметь в виду, что это не строка и не binary. Вот так не правильно:

  DeviceToken = "8253de13 f71d310d 05a13135 e09e09b6 32c478d5 32313723 1f04a7c7 b5de947d"
  DeviceToken = "8253de13f71d310d05a13135e09e09b632c478d5323137231f04a7c7b5de947d"
  DeviceToken = <<"8253de13f71d310d05a13135e09e09b632c478d5323137231f04a7c7b5de947d">>

Это 16-ти разрядное число. Вот так правильно:

 DeviceToken = 16#8253de13f71d310d05a13135e09e09b632c478d5323137231f04a7c7b5de947d

feedback сервис

Все чутка сложнее, чем сперва казалось :) Бывает такое, что ваше приложение было удалено с девайса, а вы продолжаете присылать ему сообщения. Apple не очень хотят, чтобы APNs нагружали ненужными сообщениями, поэтому они настоятельно просят периодически обращаться к их feedback сервису, получать оттуда список таких девайсов, хранить список у себя и не посылать им сообщения.

Эта функциональность тоже реализована в dp_push_sender.erl. Модуль опрашивает feedback сервис с заданной частотой и хранит токены в DETS.

Если немного подумать, то все еще немножко сложнее, чем сперва казалось :) Бывает такое, что после того, как ваше приложение было удалено с девайса, оно было опять на него установлено :) Поэтому должен быть способ удалить токен из DETS. И соответствующий метод предусмотрен в API библиотеки.

У меня еще есть mock_feedback_service.erl. Это заглушка, которая использовалась для отладки работы с feedback service. Ибо делать такую отладку, постоянно устанавливая и удаляя приложение с девайса очень уж неудобно.

Как подключить dp_push к проекту и как использовать

Подключаем как зависимость в rebar.config:

{deps, [
    {dp_push, ".*", {git, "https://github.com/yzh44yzh/dp-push.git", "v1.0"}}
   ]}.

Запускаем приложение:

main() ->
    ssl:start(),
    application:start(dp_push),

И вызываем API методы:

dp_push:send(Msg, DeviceToken),

Как можно dp_push развивать дальше

Запросы к APNs могут быть двух видов: simple и enchanced. На simple запросы сервис ничего не отвечает, а на enchanced отвечает. Можно принимать эти ответы и, например, как-то обрабатывать ошибки.

Можно сделать dp_push не подключаемой библиотекой, а отдельным сервером (TCP и/или Web). В этом случае он может обслуживать сразу несколько приложений. Правда ему нужно будет иметь сертификаты для каждого приложения и выбирать нужный сертификат при отправке сообщения.

Можно добавить сбор какой-нибудь статистики и средства мониторинга.

Comments (17)

О TCP сокете для чайников

Очень упрощенный рассказ про TCP сокет для тех, кто не в теме :)

Сидел я, писал внутреннюю документацию по своему проекту. И нужно было, помимо прочего, описать реализацию сокета со стороны клиента. Я сам делал эту реализацию на Java для специфического клиента, выполняющего функциональное и стресс тестирование сервера. А нужна будет еще одна реализация на .NET для Unity приложения, которое и будет настоящим клиентом моего сервера. И эту реализацию будет писать другой разработчик.

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

Как работает сокет на низком уровне? Речь идет о TCP Full Duplex сокете, без всяких надстроек типа HTTP протокола. Full Duplex -- это две трубы. По одной трубе данные текут с клиента на сервер. По другой трубе текут с сервера на клиент. Текут они маленькими пакетами, например, по 1500 байт (в зависимости от настроек сети).

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

И чтобы с этим работать, нужно решить две проблемы.

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

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

Давайте условно представим, что клиент посылает такой запрос:

{
action:"login",
name:"Bob",
password:"123"
}

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

{action:"login",name:"Bob",password:"123"}

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

{action:"login",n
ame:"Bob",passwor
d:"123"}

Стало быть, сервер читает из трубы первый кусок {action:"login",n. И что ему с этим делать? Можно попробовать десериализовать. Если получим ошибку, то будет ясно, что данные не полные, и нужно получить больше. И так каждый раз, когда что-то приходит из трубы, мы будем пытаться это десериализовать. Получилось -- хорошо, интерпретируем и отправляем дальше на обработку. Не получилось -- ждем больше данных.

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

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

То есть, целый запрос выглядит так:

42{action:"login",name:"Bob",password:"123"}

А разбитый на пакеты так:

42{action:"login"
,name:"Bob",passw
ord:"123"}

И когда на сервер приходит 42{action:"login", то сервер читает заголовок, видит в нем длину запроса -- 42 байта, и понимает, что нужно дождаться, пока придут эти 42 байта. И после этого данные можно десериализовать и интерпретировать. Например, интерпретация может заключаться в том, что сервер вызовет у себя метод login с аргументами "Bob" и "123". Точно также будет извлекать данные и клиент, когда он будет получать их с сервера.

Размер заголовка может быть 1 или 2 или 4 байта. Такие варианты предлагает gen_tcp, когда используется в активном режиме. (А в пассивном режиме мы сами извлекаем и интерпретируем этот заголовок, так что вольны делать как угодно).

Какой размер заголовка лучше? В 1 байт влезет число 2 ^ 8 = 256. Значит запрос не может быть больше 256 байт. Это слишком мало. В 2 байта влезет число 2 ^ 16 = 65536. Значит запрос может быть до 65536 байт. Этого вполне достаточно для большинства случаев.

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

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

Конечно, такая мелочная оптимизация только рассмешит тех, кто использует HTTP :) Ибо HTTP не мелочится, и в каждом запросе посылает нехилую пачку метаданных, совершенно не нужных серверу, и потому транжирит трафик в масштабах не сравнимых с моим аккуратным TCP сокетом :)

Проблема сопоставления запросов и ответов

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

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

Вообще нам нужны три варианта взаимодействия клиента и сервера:

  • Клиент посылает запрос на сервер и хочет получить ответ
  • Клиент посылает запрос на сервер и ему не нужен никакой ответ
  • Сервер активно пушит данные клиенту

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

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

{qid:15,action:"login",name:"Bob",password:"123"}

И получаем ответ:

{qid:15,success:true}

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

{action:"logout"}

И тогда сервер знает, что ответ не требуется, и не отвечает.

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

{event:"invitation",fromUser:"Bill",msg:"Hello, wanna chat?"}

И тогда клиент знает, что это не ответ на какой-то запрос, а активный пуш с сервера.

Некоторые детали реализации

Отправляем данные с клиента с 4х байтным заголовком:

protected void send(byte[] data) {
    try {
        byte[] header = new byte[]{
            (byte) (data.length >>> 24),
            (byte) (data.length >>> 16),
            (byte) (data.length >>> 8),
            (byte) (data.length)
        };
        out.write(header);
        out.write(data);
        out.flush();
    } catch (IOException ioException) {
        ioException.printStackTrace();
    }
}

Читаем данные на сервере

handle_info(read_data, #state{socket = Socket, transport = Transport} = State) ->
    case Transport:recv(Socket, 4, 500) of 
        {ok,  <<Size:32/integer>>} -> 
            {ok, RawData} = Transport:recv(Socket, Size, infinity),
            do_something(RawData),
            {noreply, State};
        {error, timeout} -> self() ! read_data,
            {noreply, State};
        _ -> ok = Transport:close(Socket),
            {stop, normal, State}
    end;

Отправляем данные с сервера

Reply = some_data(),
RSize = byte_size(Reply),
Transport:send(Socket, <<RSize:32, Reply/binary>>).

Читаем данные на клиенте

protected byte[] receive() throws IOException {
    int b1 = in.read();
    int b2 = in.read();
    int b3 = in.read();
    int b4 = in.read();
    int len = ((b1 << 24) + (b2 << 16) + (b3 << 8) + b4);
    byte[] data = new byte[len];
    in.read(data);
    return data;
}

И последнее: запись и чтение данных на клиенте должны работать в разных потоках, ибо чтение -- блокирующая операция. Пока процесс висит на in.read(data), он больше ничего не может делать. А процесс висит там большую часть времени :)

Comments (7)

Moscow Erlang Factory Lite, краткие мысли по поводу

Побывал на Erlang Factory, было клево. Интересно посмотреть и послушать лучших людей в этой сфере.

Краткие мысли по докладам

В Яндексе 3 Erlang-программиста, они занимаются поддержкой кастомной ветки ejabberd. Форк был сделал довольно давно и далеко ушел от главной ветки. Они выкинули Mnesia (очевидный шаг), и заменили на MongoDB (сами не помнят, почему именно MongoDB). У них несколько десятков узлов, про нагрузку не помню.

Peter Lemenkov -- клевый чувак. Выступать в красных шортах с цветочками -- мегафишка :) Основная мысль его доклада: rebar хорош, но этого мало. Нужно, чтобы установка erlang-продуктов поддерживалась стандартным для данного дистрибутива пакетным менеджером. И этим должен заняться лично ТЫ, ибо кто, если не ты?

Bob Ippolito показал, как пользоваться рандомным генератором. Содержание не впечатлило (тема не сложная), но форма, как сделана презентация, удачная. Надо брать на вооружение.

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

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

Лев Валкин дал своему докладу весьма холиварное название, но содержание было мудрое. Толково рассказал, что нужно учитывать, выбирая технологию для проекта. Я вот не считал Erlang и Node.js конкурентами, но после доклада Валкина понял, что их области применения частично пересекаются.

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

Michal Niec показал интересный проект. Технически никакой rocket science из себя не представляет, но практический потенциал весьма и весьма имеет. Хороший пример, как можно использовать Erlang с пользой для людей.

Anton Lebedevich помог мне выбрать драйвер для PostgresSQL :) Вернее подтвердил, что я выбрал правильный драйвер. А вот в Echo используют что-то свое, в open source не выкладывают. Но, может быть, выложат.

Краткие мысли по афтепати

Конфа без афтепати не полноценна, даже если там были мегадоклады :)

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

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

Краткие мысли вообще

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

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

В книжных магазинах отделы IT-литературы -- полный отстой. Прям даже нечего взять в руки, не то, что купить. Я там надеялся найти книгу по Haskell, а там даже и по Java достойной книги нет.

Сделать хороший доклад и хорошую презентацию -- большое искусство. И я потихоньку начинаю постигать его.

Что мне не удалось

Пообщаться с крутыми Erlang-программистами из Echo. Юра Бушмелев меня с ними познакомил, но беседа сразу как-то не сложилась. А потом они как-то не попадались мне на глаза. Жаль.

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

Впрочем, по части Ульяновска и компании Echo я многое узнал от Юры еще на Гроненском LVEE.

Итоги и выводы

В русском (ну или СНГшном) IT есть немало достойных людей и высокого уровня программистов, у которых есть чему учиться. И люди эти вполне досягаемы :)

Я укрепился в мысли, что я сам и мой текущий проект на правильном пути. И тактически, и стратегически.

Нужно мыслить позитивно, получать удовольствие от работы и от жизни.Be happy :)

P.S. Спасибо Алексею Фомкину aka Yelbota за беседы и за приют.

Comments (1)

Эффективный TCP сервер с помощью Ranch Acceptor Pool

Интро

Допустим, у нас есть задача написать эффективный сервер, который будет работать с мобильными клиентами (iPhone, Android и т.д.) по TCP соединению. Допустим, это будет не веб-сервер (зачем бы писать веб-сервер, если Cowboy уже есть :).

Ну что ж, читаем у Армстронга 14 главу и/или у Чезарини 15 главу, берем на вооружение модуль gen_tcp и уже через несколько минут (часов) весело общаемся с нашим новым-клевым сервером через telnet клиент :)

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

А потом, благодаря природному любопытсву и любви совать нос во всякие блоги-книги-доки-мануалы, мы узнаем, что такая наша реализация слишком наивна, а серьезные чуваки используют Acceptor Pool. Пользуясь случаем, выражаю глубокий респект чуваку по имени Frederic Trottier-Hebert за его монументальный труд Learn you some Erlang for great good!, где, помимо всего прочего, можно неплохо прошариться по части работы с сокетами в Erlang и узнать про Acceptor Pool.

После этого мы уже готовы реализовать свой Acceptor Pool, но все то же природное любопытство находит для нас уже готовую реализацию, да не какую-нибудь, а выделенную из самого веб-сервера Cowboy в отдельный проект Ranch.

Вот о нем и пойдет речь.

Как оно устроено

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

Идея состоит в том, что есть:

  1. protocol handler, который будет получать бинарные данные и чего-нибудь с ними делать, на ваше усмотрение;
  2. transport handler, который инкапсулирует в себе gen_tcp или ssl или еще чего-нибудь и предоставляет к ним абстрактный интерфейс;
  3. ranch_listener, который раскладывает соединения по пулам, перекидывает их туда-сюда, следит за лимитами и за изменением параметров;
  4. супервизоры, которые рулят всеми процессами сверху.

protocol handler вы пишете свой, остальное предоставляет ranch. Из transport handlerов есть в наличии ranch_tcp и ranch_ssl. А если вам нужен, к примеру UDP, то и transport handler придется написать свой.

Работает оно так:

Стартует ranch, которое суть OTP приложение. И ничего не делает, пока вы его не попросите что-нибудь сделать :) Оно такое хорошее, ленивое приложение, предпочитает ничего не делать. А попросить вы можете start_listener/6.

Туда нужно передать:

  1. идентификатор вашего пула, который нужен на случай, если у вас несколько разных пулов для разных транспортов;
  2. сколько вы хотите acceptors в пуле;
  3. transport handler -- какой модуль будет выполнять роль транспорта (это может быть ranch_tcp, или ranch_ssl, или ваш модуль);
  4. аргументы для запуска transport handler;
  5. protocol handler -- какой-нибудь ваш модуль, который будет обрабатывать данные;
  6. аргументы для запуска protocol handler;

Вы еще можете попросить ranch остановить пул, показать или поменять настройки для acceptors, ну и еще кой-чего по мелочи.

start_listener запускает ranch_sup супервизор. Это будет корневой супервизор для вашего пула. Если вы запустите несколько разных пулов, то у каждого будет свой такой супервизор. ranch_sup тоже ленивый, сам нифига делать не хочет, а запускает ranch_listener_sup и скидывает всю работу на него :) (Вот не знаю, зачем нужен лишний супервизор).

ranch_listener_sup, понимая, что нельзя бесконечно спихивать работу на кого-то другого, берет на себя кое-какие заботы. Он запускает вокера ranch_listener и еще парочку супервизоров: ranch_conns_sup и ranch_acceptors_sup.

ranch_listener суть gen_server. Он хранит текущие соединения в ets; считает, сколько их; следит за лимитом соединений, за обрывом соединений и т.д. Если лимит превышен, он не отвечает на вызов add_connection, тем самым заставляя вызывающий процесс зависнуть и ждать. Здесь еще предусмотренны группы пулов, с отдельным лимитом в каждой группе и возможностью перекладывать соединения из одной группы в другую. Но это, насколько я вижу, не используется. Единственный вызов add_connection из модуля ranch_acceptor жестко задает группу default.

ranch_conns_sup для каждого нового соединения запускает новый поток в вашем модуле protocol handler, для чего модуль должен иметь функцию start_link/4. Пример такого модуля будет ниже.

ranch_acceptors_sup запускает нужное количество потоков ranch_acceptor. И все эти acceptor занимаются тем, чем должны -- принимают соединения. Они просят тот transport handler, который им дали, принять соединение. Затем просят ranch_conns_sup создать новый поток protocol handler. Затем опять просят transport handler задать поток protocol handler как controlling process для сокета. После чего protocol handler сможет читать из него данные.

Наконец, ranch_tcp собственно и работает с gen_tcp.

Оставим в стороне вопрос, как можно динамически менять настройки для acceptors и пойдем дальше :)

Как им пользоваться

Создаем свой protocol handler модуль. Вот, например, в тестах есть модуль, который делает эхо: echo_protocol.

Или вот кусок моего модуля:

Реализуем start_link/4

start_link(ListenerPid, Socket, Transport, Opts) ->
    Pid = spawn_link(?MODULE, init, [ListenerPid, Socket, Transport, Opts]),
    {ok, Pid}.

init(ListenerPid, Socket, Transport, _Opts = []) ->
    ok = ranch:accept_ack(ListenerPid),
    loop(Socket, Transport).

В цикле читаем из сокета данные

loop(Socket, Transport) ->
    case Transport:recv(Socket, 2, infinity) of
        {ok, Data} -> <<Size:16/integer>> = Data,
            process_query(Transport, Socket, Size),
            loop(Socket, Transport);
        _ -> ok = Transport:close(Socket)
    end.

И обрабатываем их

process_query(Transport, Socket, Size) ->
    {ok, RawData} = Transport:recv(Socket, Size, infinity),
    #rpc{action = Action, payload = Payload} = queries_pb:decode_rpc(RawData),
    Reply = process_query(Action, Payload),
    RSize = byte_size(Reply),
    Transport:send(Socket, <<RSize:16, Reply/binary>>).

process_query("auth", RawData) ->
    Data = queries_pb:decode_authquery(RawData),
    Reply = #authresult{success = true, uid = 23},
    queries_pb:encode_authresult(Reply);

protocol handler есть, теперь запускаем ranch:

application:start(ranch)

и вызываем мега-функцию ranch:start_listen/4

Port = 8080,
NumAcceptors = 200,

ranch:start_listener(my_pool, NumAcceptors,
    ranch_tcp, [{port, Port}],
    my_protocol_handler, []).

И все, enjoy :)

Ах да, в вашем rebar.config должна быть зависимость от ranch, конечно

{deps, [
    {ranch, ".*", {git, "https://github.com/extend/ranch.git", "HEAD"}}
]}.

Теперь enjoy :)

Comments (8)

Opscode: история успеха

В Opscode вычистили весь хипстерский хлам, типа рубей, единорогов и каучдб, и заменили это на Erlang+MySQL, после чего у них наступила благодать.

(Read more)

Comments (2)

Отладка в Erlang

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

Трассировщик

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

Трассировщик запускаеся простой командой:

dbg:tracer().

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

А если недостаточно?

Этот параграф можно не читать.

Если стандартное поведение трассировщика нас не устраивает, то его можно с лёгкостью определить самому, используя функцию dbg:tracer/2. В этой функции первым аргументом описывается тип трассировщика: process или port.

В случае типа process, трассировщик будет работать как обычный erlang-процесс, принимая сообщения и применяя к ним функцию, определённую вторым аргументом.

В случае типа port, вторым аргументом должна идти функция, определённая с помощью dbg:trace_port/2. Использование порта позволяет снизить издержки на добавление трассировочных сообщений в очередь erlang-процесса, посылая их сразу напрямую в драйвер. Определены два трассировочных драйвера: ip и file, из названия которых сразу понятно, что они делают.

Драйвер ip открывает TCP/IP-порт, указанный вторым аргументом, и начинает его слушать. Как только на этот порт подцепляется трассировочный клиент с другой erlang-ноды, используя команду dbg:trace_client/2, он начинает получать сообщения и печатать их на консоль, как обычный dbg:tracer/0. Чтобы переопределить поведение клиента, используем dbg:trace_client/3.

Драйвер file работает похожим образом, но использует запись в файл, для последующего чтения с помощью dbg:trace_client/2.

События

Теперь определяемся, что хотим видеть, и задаём это с помощью команды:

dbg:p(Item, Flags).

Здесь Item задаёт необходимое нам множество процессов, действия которых надо отловить:

  • Переменная типа pid(): будем получать события от этого конкретного процесса
  • Атом all: получаем события от всех процессов в системе
  • Атом new: только от новых процессов, уже запущенные будут игнорироваться
  • Атом existing: только от уже запущенных процессов, новые будут игнорироваться (противоположность new)
  • Какой-то другой атом: будем получать события от процесса, зарегистрированного под этим атомом
  • Целое число: преобразуется в идентификатор процесса <0.Item.0>
  • Кортеж {X,Y,Z}: преобразуется в идентификатор процесса <X.Y.Z>
  • Строка "<X.Y.Z>": преобразуется в идентификатор процесса <X.Y.Z>

Значение Flags может быть атомом или списком следующих атомов:

  • s (send): отправляемые сообщения
  • r (receive): получаемые сообщения
  • m (messages): и то, и другое
  • c (call): вызываемые функции (это самый используемый флаг и подробнее о нём в параграфе про шаблоны функций)
  • p (procs): прочие события процесса
  • sos (set on spawn): флаги трассировки будут наследоваться всеми процессами, порождёнными множеством процессов, заданным Item
  • sol (set on link): флаги трассировки будут наследоваться всеми процессами, с которыми линкуется множество процессов, заданное Item
  • sofs (set on first spawn): то же самое, что и sos, но только для первого порождённого процесса
  • sofl (set on first link): то же самое, что и sol, но только для первого залинкованного процесса
  • all: устанавливает все вышеперечисленные флаги
  • clear: сбрасывает все вышеперечисленные флаги
  • timestamp: добавляет время события

Описание dbg:p/2

Шаблоны функций

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

  • dbg:tp/2,3,4: добавляет трассировку только глобальных функций
  • dbg:tpl/2,3,4: добавляет трассировку локальных функций
  • dbg:ctp/0,1,2,3: удаляет трассировку, заданную dbg:tp/2,3,4 или dbg:tpl/2,3,4
  • dbg:ctpl/0,1,2,3: удаляет трассировку, заданную только dbg:tpl/2,3,4 (локальные функции)
  • dbg:ctpg/0,1,2,3: удаляет трассировку, заданную только dbg:tp/2,3,4 (глобальные функции)

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

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

Разберём пример:

dbg:tracer().
dbg:p(all, [c, timestamp]).
dbg:tp(random, uniform, cx).

Здесь используется функция dbg:tp/3, гре первый аргумент — имя модуля random, второй — имя функции uniform, аргумента арности здесь нет, поэтому будет браться любая из существующих. Последний аргумент — MatchSpec имеет значение cx. Это то, что нам обычно и нужно. Параметр представляет собой суперпозицию двух параметров: c и x. Параметр c показывает, какая функция вызывает трассируемую функцию, а параметр x показывает возвращаемое значение трассируемой функции или исключение, если оно происходит. Вот так будет выглядеть вызов функции random:uniform/1 после включенной трассировки:

random:uniform(14).
(<0.32.0>) call random:uniform(14) ({erl_eval,do_apply,6}) (Timestamp: {1335,366136,885591})
(<0.32.0>) returned from random:uniform/1 -> 7 (Timestamp: {1335,366136,885625})
7

И в случае исключения (передаём неверный аргумент — атом вместо целого числа):

random:uniform(asd).
(<0.32.0>) call random:uniform(asd) ({erl_eval,do_apply,6}) (Timestamp: {1335,366468,181974})
(<0.32.0>) exception_from {random,uniform,1} {error,function_clause} (Timestamp: {1335,366468,181997})
** exception error: no function clause matching random:uniform(asd) (random.erl, line 111)

Это простой пример. При трассировке в больших проектах, наглядность сообщений очень повышается, по ним можно проследить любой произвольный отрезок жизни системы, включая внутренние состояния процессов (помним, что состояния процессов передаются из одной функции в другую, в качестве аргумента, например, handle_cast(Message, State) в процессе gen_server). Если же хочется просто посмотреть состояние нужного процесса gen_server в произвольный момент времени, то для этого существует функция sys:get_status(Pid).

Oстановка трассировки

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

Распределённая отладка

Не забываем, что Erlang/OTP имеет хорошие средства для построения кластеров. Отладка тоже не отстаёт. Для этого всего лишь надо добавить удалённые ноды с помощью вызова dbg:n(remotenode1@somehostname). Теперь трассировщик будет принимать сообщения от удалённых нод тоже. Удалить удалённую ноду из списка трассируемых можно вызовом dbg:cn(remotenode1@somehostname).

Заключение

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

Comments

Почему я больше не буду троллить Node.js

В среду ездил в 2gis на мероприятие #DevDay. Там проводился квартирник, посвящённый Node.js и меня пригласили в качестве тролля-эксперта, противника Node.js. Я раньше был замечен в нелестных отзывах о ноде, всяческих сравнениях её с Erlang (не в пользу ноды, разумеется), поэтому должен был троллить и унижать её на квартирнике. Сразу скажу, что после мероприятия у меня абсолютно пропало желание троллить ноду и в ближайшее время я этого делать не буду, делал я это только по незнанию, так как не совсем чётко представлял себе, что такое нода.

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

Затем после перерыва начался сам квартирник, вели который Влад Семёнов (@Semenov) и Степан Столяров (@stevebest). Ребята сразу же рассказали столько страшных вещей про Node.js, что абсолютно перехотелось троллить. Оказалось, что приложение на ноде надо перезапускать каждые два дня, ибо оно течёт, и утечки очень сложно поймать. Говорили, что в Яндекс сделали шаблоны, способные работать на стороне клиента и сервера сначала на JS (сервер на Node.js), так как хотелось использовать один и тот же код, но потом серверную часть переписали на C, получив выигрыш в производительности всего 20%. Видать, стало тяжело поддерживать, и хотя бы на сервере решили упростить задачу. (Яндексоиды, если кто в теме, можете прояснить?)

Говорили про библиотеку socket.io, которая реализует WebSockets и позволяет прозрачно делать даунгрейд в браузерах вплоть до IE5.5, и в качестве примера сервиса, использующую эту библиотеку с Node.js, привели trello.com, где из socket.io удалили всю возможность даунгрейда и поддерживают только новые браузеры. Это расово верно, так как чем меньше сайтов будет поддерживать старое барахло, тем лучше для мировой революции.

Тут я понял, что люди, выбирающие Node.js в здравом уме и твёрдой памяти, не нуждаются в Erlang. Нет смысла сравнивать ноду и ерланг, у них разные непересекающиеся ниши. Сторонников ноды не страшат пилообразные графики CPU и памяти в munin. Ну, упал один инстанс ноды, ну увидели несколько пользователей ошибку на nginx, это ж не катастрофа. Нажмёт он F5 и запрос обработает другой инстанс ноды, а первый пока поднимется. Это не смертельно, это не критично. Зато прикольно, круто и модно. Starbucks одобряє. Когда я раньше троллил ноду, считал, что она наступает ерлангу на пятки. Увы, это не так. Пропасть между нодой и ерлангом просто огроменная.

Общеизвестно, что 95% софта может быть написано абсолютно на любом языке. Не всем нужны кластеры, не всем нужна отказоустойчивость, не всем нужна возможность держать миллион соединений, не у всех есть высокие нагрузки. Это нормально. Оставшиеся 5% в принципе могут быть написаны только на чём-то узкоспециализированном и не имеющем альтернатив: C, ASM, Verilog, Erlang, JS (client-side!). Выбирать технологию для 95% можно руководствуясь лишь личными предпочтениями или модой. Node.js для этого хорошо подходит.

UPD: @illbullet меня поправил. Про шаблонизатор на C и JS — это было в Mail.ru, подробнее тут. Не знаю, почему у меня про Яндекс отложилось.

Comments (9)

Массовый gen_tcp:send из одного процесса

В книжках и мануалах по эрлангу есть несколько тонких мест, в которых нельзя слушать мануалы, а надо читать исходники. Одно из этих мест — gen_tcp:send. Если вы хотите раздать много данных, то надо пользоваться недокументированными возможностями.

gen_tcp:send устроен следующим образом: сначала вызывается port_command, это от 1 до 2 микросекунд; потом ожидается сообщение {inet_reply,Socket,Reply}

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

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

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

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

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

Поэтому надо вызывать port_command(Socket, Data, [nosuspend]). Если он возвращает false, значит писать в этот сокет больше нельзя. Тут надо решить что делать: либо выкидывать этот сокет на какое-то время из обслуживания, либо вообще отключать.

Так же немного неясно, что делать, если сокет закрылся.

Рецепт простой: (catch port_command(Socket, Data, [nosuspend])) либо возвращает true, либо с клиентом что-то плохо. Можно вдаваться в детали, а можно тупо отключить его, потому что он никак не вычитывает данные.

Comments (9)

Отложенная инициализация процесса

Инициализация процесса gen_server происходит в коллбеке init/1. Этот коллбэк вызывается уже запущенным новым процессом с поведением gen_server после того, как внешний процесс захотел его запустить с помощью функции start_link/1. Пусть модуль с описанием этого нового процесса имеет имя zz_worker. Тогда функция, запускающая этот процесс будет выглядеть так:

start_link(Args) ->
    gen_server:start_link(zz_worker, Args, []).

Разберём функцию gen_server:start_link/3. Первый её аргумент — имя модуля, где находятся коллбэки gen_server, описывающие действия процесса в ответ на определённые сообщения. Второй аргумент Args — то, что передаётся в функцию init/1. Третий аргумент — системные параметры, мы их тут не рассматриваем, поэтому просто передаём пустой список.

Процесс, вызывающий zz_worker:start_link/1 получит ответ {ok, Pid :: pid()} только тогда, когда функция init/1 закончит свою работу, а до тех пор вызывающий процесс будет находиться в блокировке. Если инициализация процесса проходит быстро, то и вызывающий процесс быстро получит ответ и продолжит работу. Если для инициализации требуется совершить какое-то длительное действие, то вызывающий процесс получит ответ и продолжит своё выполнение не скоро. Очень часто нас такое поведение устраивает и ничего больше не надо делать. Часто, но не всегда. Иногда возникает необходимость быстро запускать много процессов, не дожидаясь завершения инициализации каждого. Для этого надо уменьшить время выполнения функции init/1, а всю тяжёлую инициализацию оставить на потом.

Рассмотрим функцию init/1:

init(Args) ->
    State = do_long_initialization(Args),
    {ok, State}.

Здесь некая функция do_long_initialization/1 выполняется очень долго, своим выполнением оттягивая момент получения идентификатора этого процесса внешним процессом. Подумаем, как её выполнение оставить на потом. Вспоминаем, что в Erlang у нас все процессы обмениваются сообщениями, и сообщение можно послать любому процессу, в том числе и самому себе. Если мы пошлём сообщение самому себе в функции init/1, то оно гарантированно появится в мейлбоксе самым первым, так как больше никто про этот процесс ещё не узнал и не может ничего ему послать.

init(Args) ->
    self() ! {init, Args},
    {ok, #state{}}.

init/1 возвращает кортеж {ok, State :: term()} если инициализация прошла успешно, причём значение State будет использоваться в дальнейшем в качестве состояния процесса. Сейчас мы просто вернём сконструированный рекорд #state{} со значениями полей по умолчанию. Всё, в init/1 процесс посылает сообщение самому себе, функция на этом завершается и внешний процесс получает идентификатор нового процесса, после чего выходит из состояния блокировки и может работать дальше.

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

handle_info({init, Args}, OldState) ->
    NewState = do_long_initialization(Args),
    {noreply, NewState}.

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

Вместо посылки сообщения с помощью ! можно так же использовать gen_server:cast(self(), {init, Args}), тогда обрабатывать сообщение надо в обработчике handle_cast/2.

Comments (1)

Делаем свои шаблоны Erlang модулей для Emacs

erlang-mode для Emacs включает довольно приличный набор шаблонов. Попробуйте выполнить Meta-X tempo-temp Tab Tab, и увидите что их там 23 штуки.

Но с ними связаны некоторые неудобства. Во-первых, довольно громоздкий способ вызова (впрочем, это можно обернуть в свою функцию, с более лаконичным именем). Во-вторых, сами шаблоны трудно модифицировать и добавлять (они определены здесь: /usr/lib/erlang/lib/tools-2.6.6.3/emacs/erlang_appwiz.el). А хотелось бы, чтобы каждый шаблон был определен в отдельном файле и был удобен для модификаций.

К счастью, при некотором знании elisp Emacs позволяет сделать все так, как нам хочется :) Для начала определимся, где мы будем их хранить. Например тут: ~/emacs.d/tpl. И положим туда, например, шаблон Erlang-модуля:

-module(module_tpl).
-author('Vasja Pupkin <pupkin@somewhere.com>').

-export([]).

%%% module API

Или вот такой шаблон для gen_server. Или вот такой шаблон для supervisor.

Затем определим где-нибудь (например в ~/.emacs.d/init.el) функцию на elisp, которая будет копировать нужный файл в проект и модифицировать его соотвественно указанному имени. Много кода не понадобится :)

(defun erl-new-file (module-name tpl-file)
  (setq new-file (format "%s.erl" module-name))
  (copy-file tpl-file new-file)
  (switch-to-buffer (find-file new-file))
  (search-forward "(")
  (setq begin (point))
  (search-forward ")")
  (backward-char)
  (kill-region begin (point))
  (insert module-name))

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

(defun erl-new-module (module-name)
  (interactive "MModule name:")
  (erl-new-file module-name "~/.emacs.d/tpl/module_tpl.erl"))

(defun erl-new-supervisor (module-name)
  (interactive "MModule name:")
  (erl-new-file module-name "~/.emacs.d/tpl/supervisor_tpl.erl"))

(defun erl-new-gen-server (module-name)
  (interactive "MModule name:")
  (erl-new-file module-name "~/.emacs.d/tpl/gen_server_tpl.erl"))

Ну вот, теперь, если нам нужно создать в проекте новый модуль gen_server, то мы вызываем Meta-X erl-new-gen Tab, указываем имя модуля, и он создается из шаблона и открывается у нас в редакторе.

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

Comments (1)

Erlang. Прагматичный рассказ про прагматичный язык.

15 октября 2011 выступал на 5-й встрече сообщества scala.by. Было клева, аудитория оказалась весьма заинтересованная, засыпали вопросами. Хотя, казалось бы, Erlang для сообщества Scala программистов немного оффтопик. Но нет. Некоторые даже приехали из других городов, чтобы послушать. Я был весьма польщен этим :)

Встреча была довольно долгая, затянулась часов на 5. Сперва я рассказывал про историю Erlang и давал общий обзор языка (многопоточность, устойчивость к ошибкам, распределенность, горячее обновление). Затем была довольно длинная live coding сессия, где я делал сервер сокращения ссылок :) Сперва сделал его без OTP, потом переделал в нормальный gen_server (чтобы наглядно показать, почему с gen_server лучше, чем без него). Потом была беседа про OTP, и вопросы по другим темам.

Здесь отчет о встрече на сайте сообщества. Фотки доступны здесь и здесь.

Видеозапись встречи:

Первая часть моего выступления также доступна в формате plain txt :) И вот она ниже:

(Read more)
Comments (3)

Несбывшиеся мечты

На заре программирования, в 80-х и 90-х годах люди верили, что если сделать код, имеющий многие уровни абстракций, то наступит счастье. Для добавления новой функциональности надо будет всего лишь унаследовать пару классов, переопределить три метода и всё само заработает. Кроме того, написанный один раз код можно будет использовать повторно. Именно тогда появились такие вещи, как C++, Java, SOAP, CORBA, ASN.1, где, казалось бы, заложено всё, что угодно, расширяй — не хочу.

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

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

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

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

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

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

Слабая связность vs. сильная. Сообщения vs. методы. Объектно ориентированное программирование vs. процессы.

P.S. И да, говорить, что вызов метода в C++/Java/ObjC, это тоже посылка сообщения — глупость. Посылка сообщений подразумевает их потенциальную асинхронность и наличие очереди сообщений. Если сообщения только синхронные и не складываются в очередь, то это не сообщения, а миф.

Comments

Python после Erlang

После ерланга наиболее естественным стал считать следующее поведение функции: при её завершении возвращается последнее вычисленное значение. Например:

retfun() ->
  io:format("~p~n", [abc]),
  5+5.

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

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

И когда я пишу на питоне что-то вроде:

def retfun(self, arg):
    self.some_other_fun(arg)

а оно мне возвращает None вместо результата функции self.some_other_fun(arg), я очень негодую. Зачем делать функции, которые ничего не возвращают? Ведь если сделать так, что каждая функция должна что-то возвращать, тогда можно будет выкинуть ключевое слово return.

Comments (9)

Релиз CEAN 2.0

После долгих лет забвения вышел релиз CEAN 2.0. Теперь это не только репозиторий с пакетами, но ещё и фреймворк для разработки на Erlang/OTP на основе git и zsh.

  • Добавляет новые команды в Unix shell и Erlang.
  • Фреймворк теперь Open Source (GPL).
  • Возможность генерировать пакеты и инсталляторы.
  • Работает в кластерном окружении. Возможна синхронизация Erlang/CEAN на нескольких хостах, используя всего одну команду.
  • Возможность иметь столько версий инсталляций Erlang/OTP, сколько хочется.
  • Надёжный генератор зависимостей пакетов.

Ссылки:

Comments

HTTP Streaming

«HTTP Streaming» by Макс Лапшин:

Comments

«Зачем рельсовику Ерланг» by Макс Лапшин:

Comments

Вставка кода

Код можно записывать так:

handle_info({Port, {data, Data}}, #state{port = Port} = State) ->
  ?DBG("Data from port: ~p", [Data]),
  port_close(Port),
  {noreply, State#state{port = undefined}};
handle_info(_Info, State) ->
  ?DBG("Unhandled info: ~p", [_Info]),
  {noreply, State}.

или так:

Comments (21)

Erlang Russian теперь работает на платформе Metalkia
Comments