Макс Лапшин (levgem) wrote,
Макс Лапшин
levgem

Category:

Конфигурация приложений

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

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



Когда конфигурироваться?



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


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

В книжках по OTP этот момент описывается очень невнятно и может возникнуть ощущение, что предлагается писать опции различных приложений в .app файлы. Т.е. ставим мы приложение, потом лезем в /usr/lib/myserver/apps/myserver/ebin/myserver.app и редактируем там env секцию. После этого делаем apt-get install следующую версию и получаем черти что, потому что .app файл поменялся (в нём повысилась версия), а автоматическим мерджем никто заниматься не будет. Этот способ отбрасываем сразу.

Другой способ — указать файл, которым сверху будет перекрываться конфиг приложения. Важно понимать, что в эрланге есть свои приложения. Это, фактически, библиотеки, которые запускаются синглтонами. Т.е. библиотека — это целая запущенная подсистема внутри эрланга в одном экземпляре. Вот её конфиг и предлагается настраивать, что бы потом можно было в коде добраться до него через application:get_env(Application, Key).

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

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

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

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

Такой подход позволил во-первых сделать приложение, которое никогда не требует рестарта при изменении конфига, хотя и требует кропотливой работы по обработке сообщений {update_options, Options}. Во-вторых, конфиг можно провалидировать прежде чем его слепо применять. «Релизный» подход не дает провалидировать конфиг заранее.

Например, мы добавили валидацию на уникальность имени потока в конфиге. Целая масса клиентов засыпали нас письмами «что значит “you have duplicated name of stream: 'chan15'”?» При этом почти всем им хватило ответа, что у них есть дублирующиеся имена потоков.


Итак, в эрливидео мы остановились на том, что релизы не используются, потому что сначала всё стартуем, а потом из функции, которая была запущена через -s: erl -pa apps/*/ebin -pa deps/*/ebin -s flussonic start мы накатываем конфиг. Причем конфиг валидируем до старта и если он плохой, то пишем сообщения во все логи и останавливаемся.

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

Синтаксис



Теперь про синтаксис. Самым простым и очевидным решением является использование эрланговского синтаксиса:
{http, 80}.


Такой файл читается с помощью file:consult(Path) и возвращает список готовых эрланговских термов. Теоретически они готовы, а практически, это всё надо потом выпрямлять и становится очень грустно:

{key1, <<"value1">>}.
{key2, "value2"}.


И пойди, объясни сисадмину разницу между list и binary. Он не будет слушать и правильно сделает. Но это не всё, чем сложнее конфиг, тем сложнее его держать целостным. Особенно грустно начинается, когда надо выровнять баланс скобок в конце: }}]}]}]]]}

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

Мы решили сделать по-другому и выбрать синтаксис а-ля nginx:
http 80;
stream ort {
  url udp://239.0.0.1:1234 multicast_loop multicast_if=eth0;
  url tshttp://pirate.backup.tv/ort;
  dvr /storage 7d;
  cache /ssd 1d 90G;
}


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

На эрланге это выглядело бы ужасно:

{stream, <<"ort">>, [
  {urls, [
     {<<"udp://239.0.0.1:1234">>, [{multicast_loop,true},{multicast_if,<<"eth0">>}]},
     {<<"tshttp://pirate.backup.tv/ort">>, []}
  ]},
  {dvr, [
     {path, <<"/storage">>},
     {time_limit, 168}
  ]}
  {cache, [
     {path, <<"/ssd">>},
     {time_limit, 24},
     {disk_limit, 92160}
  ]}
]}.


Это я ещё аккуратно расставил скобки. Люди таким заниматься не любят, так что умение расставить скобки сравнимо с умением написания программ.

Такой конфиг парсится с помощью библиотеки neotoma. Не ожидайте от неё высокоскоростного парсера, http ей парсить не стоит, но вот конфиг очень даже стоит.

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

Работы с таким конфигом много, но оно того стоит.

Альтернативным вариантом может быть ini parser. Но умоляю, не надо считать json или yaml подходящим синтаксисом для более-менее сложного конфига.

Итак, аргументы за собственный синтаксис и собственный парсер:

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


Представления и трансформации



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

Для того, что бы его реализовать, потребовалось сделать механизм трансляции конфига не только из спец-синтаксиса (S) в эрланговские термы (E), но и в JSON (J) и обратно в спец-синтаксис.

Т.е. функция S -> E есть, функцию E -> E_j -> J написали и на яваскрипте написали J -> S. E_j — это промежуточное представление в эрланговских термах, которое можно засунуть в библиотеку json-генерации.

Например, терм {stream, <<"ort">>, [{urls, []}]} надо превратить в [{entry,<<"stream">>},{name,<<"ort">>},{options,[{urls, []}]}], это как раз форма E_j

Теперь возникает проблема, о которой стоит подумать заранее. Вопрос в том, как именно попадает конфиг в JSON.

Тут есть два варианта:
1) распарсить конфиг с диска и отдать его в json: S -> E -> E_j -> J
2) забрать конфиг из внутреннего состояния системы.

Дело в том, что, как правило, внутреннее состояние системы не равно конфигу. Описывается им, но не равно. Ведь есть ещё дефолтные настройки и есть ещё изменения, сделанные через API.

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

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

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

После этого конфиг при сохранении обратно парсится и ещё раз генерируется: S -> E -> S. Это нужно для полной валидации конфига.

Важно, что это всё тестируется, т.е. в тестах берется кусочек конфига, нормализуется (S1 -> E -> S2), потом прогоняется через яваскрипт: S2 -> J -> S3. S3 обязан быть равен S2.


Итого, при проектировании системы конфигурирования очень рекомендую заранее продумать трансформации между разными представлениями.
Tags: erlang, erlyvideo, flussonic, fp, конфигурация
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 18 comments