По следам USENIX CMS 2010

Dec 30, 2010 10:42

Удалось тут прочитать http://www.verticalsysadmin.com/config2010/ про Bcfg2, Cfengine, Chef, Puppet.

Вспоминается собственный опыт. В Netli (2002+) мы сделали свою систему менеджмента конфигурации. Под конец она управляла парой десятков кластеров с сотнями машин и десятком типов кастомного софта на каждой. Это немного не то, что сисадминские системы управления конфигурацией, рассмотренные в USENIX CMS. Ведь мы в основном занимались управлением конфигурации нашего собственного софта, а не механизмом администрирования хостов с опенсорснутым софтом. Но сравнивать на высоком уровне их можно.

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


Сначала, в прототипе CDN/ADN сервиса, конфигурация софта выражалась четырьмя текстовыми tab-separated-values файлами, которые указывали на то, что с чем связано (какие ноды с какими), и с какой целью. Так как компания на тот момент работала на $500k посевных инвестиций, конфиг был последним делом, на которое обращали внимание - надо было запустить прототип софта. До восьми боксов этого было достаточно, затем, конечно, начали путаться в назначениях полей, а файлы начали рассинхронизироваться.

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

box "p1" {
process "proxy1" {
service {
port 8080;
ip-address 1.2.3.4;
}
service {
port 443;
ip-address 1.2.3.4;
use-ssl "yes";
}
}
}

И уже из конфигурации, написанной на этом языке генерировались tab-separated-values файлы, которые читались софтом.

Быстро стало понятно, что в этой конфигурации огорчает отсутствие средств для шаблонизации: абстракции, переиспользования, подстановки переменных, etc. Поэтому сверху неё был накручен Perl-шаблонизатор, который компенсировал недостатки языка. Главной особенностью этого шаблонизатора было то, что все конструкции, которые нужно было порождать по многу раз с небольшими вариациями, делались во вложенных циклах `for` прямо в шаблонах.

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

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

Поэтому следующим этапом была разработка чисто декларативного языка, но который можно было легко шаблонизировать в нём же самом. Особенности языка:
1. Отделение языка от семантики сервисов. Например, "box" или "service" в упомянутом выше примере - это были конструкции языка, записанные в качестве таковых прямо в yacc'овском BNF. А в новом языке - это просто наборы букв, интерпретирующиеся только внутри самого конфигурируемого процесса, который умеет сам понимать, что значит "service" для него. В этом плане мы могли бы использовать и разметку XML, и JSON с похожим успехом.
2. Добавление в язык конструкции "inherit", которая служила главным механизмом обеспечения reuse.
3. Добавление в язык value assignments.
4. Раз язык становился декларативным и независимым от семантики, появилась лёгкая возможность обеспечения смены конфигурации на лету. Библиотека поддержки языка предоставляла возможность регистрировать коллбеки и навешивать их на произвольные узлы синтаксического дерева. Затем, при перезагрузке конфигурации, те синтаксические конструкции, внутри которых были детектированы какие-то изменения, вызывали ассоциированные с ними коллбеки.
5. Раз язык становился декларативным и независимым от семантики, появилась возможность сделать такой же независимый от семантики валидатор этой самой семантики. Например, можно было выразить такое правило, как "каждый бокс должен иметь как минимум один процесс, а каждый процесс - как минимум два сервиса".
6. Раз язык становился декларативным и независимым от семантики, появилась возможность делать отдельные тулзы типа pssh, которые управлялись конфигурацией, даже не полностью понимая её.

Конфигурация с "inherit" могла выглядеть, например, так:

process-defaults "proxy-std" {
_validator-entity "process-defaults";
_validator-entity "process-defaults:proxy";
process-type "proxy";
pidfile "/var/run/proxy.pid";
logging-data = standard-disk-logging; // Value assignment
max-shutdown-time "12";

/*
* Settings for the ladmin.
*/
ladmin-settings "ladmin-settings" {
cmdline "/bin/cpproxy -C /etc/ncnf.conf -S $sysid -H $heartbeat_pipe_fd";
inherit ladmin-settings "hb-ladmin-settings"; // inheritance
heartbeat-timeout "25";
sleep-before-kill "16";
max-ru-idrss "255000";
}
}

Конфигурация, описывающая все сервисы в системе, занимала несколько мегабайт на диске, и около 65 мегабайт в памяти. Для того, чтобы её генерировать, редактировать руками единый конфигурационный файл было бы убийством. Поэтому файл распилили на несколько частей и завернули сборку этих частей в единый файл через M4. M4 - это тьюринг-полный шаблонизатор, использующийся, в частности, для конфигурирования sendmail'а. В этом историческом качестве он обладал удивительнейшим свойством, исключительно полезным для наших целей: его никто не хотел трогать, менять его макры. То есть, декларативная система, обёрнутая в сборку на M4, оставалась в целом декларативной. В отличие от шаблонизатора на перле, никто в M4 шаловливыми ручками не лез и лапшу из него не делал.

Возвращаясь к нашим баранам (Cfengine, Chef, Puppet, Bcfg2), хочется предложить несколько критериев, при которых конфигурация живой системы, подверженной изменениям, будет оставаться стабильной дольшее время:
1. Язык описания конфигурации должен быть декларативным. Конфигурация на Руби, например, этому условию может не удовлетворять - она требует слишком большой дисциплины от исполнителей.
2. Язык должен быть быть как можно более независим от семантики контролируемых сущностей. Это разделение на уровни (синтаксис-семантика) позволит добавлять в экосистему новые тулзы, работающие на соответствующем уровне. Тут, пожалуй, можно вспомнить XML. И закопать подальше.
3. Язык должен содержать как можно меньше конструкций. Это полезно для человека (меньше возможностей менять систему = большее единообразие = легче передавать опыт), это полезно для машины - парсинга, преобразований, etc.
4. К такому декларативному способу задания конфигурации, который на нижнем своём уровне (синтаксис языка) не соответствует какой-то конкретной семантике тех сущностей, которые он конфигурирует, должен прилагаться валидатор. Валидатор должен проверять семантическую валидность того, что получилось. Он всё, конечно, не проверит, но наиболее злостные ошибки должен иметь возможность отловить. Без автоматического валидатора рулить чем-то, кроме несколько боксов, может потребовать нечеловеческой дисциплины. Автоматический валидатор должен тоже конфигурироваться: правила валидации должны задаваться человеком.

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

netli, fp

Previous post Next post
Up