Хранение параметров программ в текстовых конфигах — задача довольно частая и на первый взгляд тривиальная. Многие тут же хмыкнут: а в чем проблема-то? Есть куча форматов (и библиотек для работы с ними): properties, XML, JSON, YAML. В чем хочешь — в том и храни. Делов-то.

Однако масштабы вынуждают посмотреть на это иначе. В частности, после многолетней разработки игровых серверов на Java я постепенно пришел к выводу, что управление конфигами не настолько уж банально. В этой статье речь пойдет о формате HOCON — какие возможности он предоставляет и почему в последнем проекте мы стали пользоваться именно им. Если конкретнее, то мы используем Typesafe Config — opensource-библиотеку написанную на Java.

HOCON — это формат конфиг-файлов, основанный на JSON. По сравнению с JSON этот формат менее строгий и обладает дополнительными возможностями:

{
    // Можно писать комментарии
    "a" : 42 // Можно пропускать запятые в конце строки
    b: hello // Можно пропускать кавычки
}

Однако основная ценность HOCON — это копирование значений переменных и даже целых JSON-объектов.

{
    a: 42
    b: ${a} // Присваиваем переменной b значение переменной a
    c : {
        m1 : 1
        m2 : 2
    }
    d : ${c} { // Копируем в d значение из c
        m3 : 3 // Добавляем в d переменную m3 = 3
    }
}

Это, конечно, прикольно, скажет тут читатель, но зачем мне это надо? К чему городить весь этот огород вместо того, чтобы хранить свои конфиги в обычном JSON или XML? Чтобы ответить на этот резонный вопрос, поделюсь двумя примерами из нашей рабочей практики.

Пример 1. Из жизни админов


Мы разрабатываем игровые серверы. А игровые серверы — это целый зоопарк сервисов, которые в зависимости от требований могут работать на разном наборе железа и в различных раскладках. В качестве примера приведу схему раскладки сервисов по хостам с одного из моих прошлых проектов:



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

Пусть для примера у нас есть три сервиса s1, s2, s3, у которых надо настроить IP-адрес:

{
    s1 :
    {
        ip: “192.168.10.1”
    }
    s2 :
    {
        ip: “192.168.10.1”
    }
    s3 :
    {
        ip: “192.168.10.1”
    }
}

Очень часто эти сервисы запускаются на одном и том же хосте и имеют один и тот же IP-адрес. И мы не хотим при смене IP-адреса лазить по всему конфигу и везде их менять (помним о том, что в жизни их не три, а 100500). Что же делать? Если хранить конфиг в обычном JSON, то можно было бы завести общий параметр host_ip и написать примерно такой код:

ip = config.getValue(“s1.ip”);
if ( ip == null ) {
    ip = config.getValue(“host_ip”);
}

Однако такое решение имеет существенные недостатки:

  1. Разработчик сервиса может его не предусмотреть для данного конкретного случая.
  2. Эта логика скрыта от администратора, который настраивает конфиги. Откуда ему знать, что если параметр s1.ip не указан, то он будет взят из параметра host_ip? Если же параметров много и они станут часто выделывать подобные фокусы, то с администратором может случиться сердечный приступ (и его тень будет по ночам являться разработчику).

На HOCON же решение полностью прозрачно:

{
    host_ip: “192.168.10.1”
    s1 :
    {
        ip: ${host_ip}
    }
    s2 :
    {
        ip: ${host_ip}
    }
    s3 :
    {
        ip: ${host_ip}
    }
}

Пример 2. Из жизни разработчиков


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

{
    host_ip: “192.168.10.1”
    s1 :
    {
        ip: ${host_ip}
    }
    s2 :
    {
        ip: ${host_ip}
    }
    s3 :
    {
        ip: ${host_ip}
    }
}

Но тут возникает загвоздка: общий конфиг-то лежит под системой контроля версий! Если я поменяю в нем host_ip, то возникнет куча неудобств: постоянно висит дифф, надо мержить изменения, внесенные другими. Еще не дай бог случайно закоммитишь свой host_ip в общую версию. Можно также хранить общий конфиг где-то в другом месте и подкладывать в нужное. Но как тогда туда будут попадать изменения, сделанные в версии другими разработчиками?

И тут нам на помощь приходит директива include:

{
    host_ip: “127.0.0.1” // Пусть по умолчанию открываются локально
    s1 :
    {
        ip: ${host_ip}
    }
    s2 :
    {
        ip: ${host_ip}
    }
    s3 :
    {
        ip: ${host_ip}
    }
    include “local_config.conf” // Подключаем параметры из файла local_config.conf
}

А дальше рядом с основным конфиг-файлом мы подкладываем файл local_config.conf с таким содержимым:

{
    host_ip: “192.168.10.1” // Переопределяем IP на наше значение
}

Файл local_config.conf игнорируется системой контроля версий, никаких конфликтов не происходит.

Заключение


Рассмотренные примеры использования взяты из реальной разработки. И конечно же, возможности HOCON ими не ограничиваются. Фактически HOCON — это не просто текстовый формат, а, скорее, узкоспециализированный скрипт для конфигов, который может существенно облегчить жизнь администратору и разработчикам.

Приводить более подробное описание я тут специально не стал: все прекрасно изложено в официальном руководстве. Если у вас есть свои достойные внимания случаи использования этой библиотеки — делитесь ими в комментариях!
Поделиться с друзьями
-->

Комментарии (14)


  1. nskazki
    01.08.2016 16:36
    +2

    Наследование в конфигах, также, хорошо для настройки групп сервисов.
    Например, для воркера и сервера заводится базовый конфиг в с адресом: портом сервера и наследуется в конфиг каждого зависимого.
    Учитывая возможность интерполировать ключи в значения такие решения отлично масштабируется на группы микросервисов с конфигами под разные окружения.
    Пользуясь случаем представляю релеватный по возможностям загрузчик под ноду: https://github.com/nskazki/natan


    1. strangeraven
      01.08.2016 17:29
      +1

      Да, так и есть, у нас тоже есть подобные использования.


  1. Googolplex
    01.08.2016 16:57
    +2

    Библиотека кстати называется Typesafe Config; это язык называется HOCON. И кстати, Typesafe Config — это go-to библиотека для конфигурации проектов на Scala, потому что многие популярные фреймворки и библиотеки, в частности, Play Framework и Akka, используют именно её.


    1. strangeraven
      01.08.2016 17:49
      +1

      Спасибо за указанную неточность в терминах, принято.


  1. vayho
    01.08.2016 17:08
    +1

    ну примерно из за того же что описано в статье и использую HOCON


  1. fr33zy
    01.08.2016 22:22

    Примеры несколько странные, можно же написать проще, вместо:


    {
        s1 :
        {
            ip: “192.168.10.1”
        }
        s2 :
        {
            ip: “192.168.10.1”
        }
        s3 :
        {
            ip: “192.168.10.1”
        }
    }

    писать:


    s1.ip = “192.168.10.1”
    s2.ip = “192.168.10.1”
    s3.ip = “192.168.10.1”

    и это будет тоже HOCON.


    Добавлю, что важный недостаток (а иногда и нет) HOCON — это наличие include, но отсутствие require. Если конфиг, указанный по пути include не найден, он будет проигнорирован без каких-либо предупреждений. Так что будьте осторожнее.


    1. strangeraven
      01.08.2016 22:38

      Можно и так. Просто примеры купированные, в них опущено что в s1 помимо ip есть и другие параметры. В исходном варианте было как-то так

      s1: 
      {
          ip = “192.168.10.1”
          port = 123
          database = "goodwin"
          username = "root666"
          password = "nobodyknows"
          threadpool_size = 10
          // и так далее
      }
      


      Кстати да, вместо двоеточия можно писать =, это как кому нравится.

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

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


  1. arvitaly
    02.08.2016 00:16

    > К чему городить весь этот огород вместо того, чтобы хранить свои конфиги в обычном JSON или XML?
    Почему не хранить конфиги в скриптовом языке, например, javascript?


    1. strangeraven
      02.08.2016 01:12

      Парсер HOCON — это маленькая библиотечка на Java, которая к тому же не тянет никаких зависимостей.
      А если JavaScript — то в нашем случае это надо внутрь JVM, на которой наш сервер работает, целый JavaScript интерпретатор затаскивать.

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

      Ну то есть при желании так сделать конечно можно, но я бы так делать не стал :)


      1. arvitaly
        02.08.2016 01:44

        Есть и другие скриптовые языки, предустановленные на множестве систем (perl, bash, python), но тут скорее соглашусь.
        А вот как это связано с увеличением сложности, я не понял. Ведь и на HOCON можно понаписать что-угодно, а любой интерпретируемый язык можно проверить статическим анализатором или еще кучей способов. Ведь, по-хорошему. конфиги тоже нужно тестировать.
        Суть в чем, вот есть XML/JSON, при небольшой сложности всем устраивает, но вот понадобились переменные и уже нужно учить/выбирать новый язык, при следующем витке увеличения сложности — опять понадобится новый язык еще с чуть более расширенными возможностями. А это обучение, поддержка и т.д. Не проще ли сразу заложить один формат конфигурации и для простых схем, и для очень сложных, а контролировать формат отдельно для каждого проекта.
        Интересует ваше мнение именно в контексте крупной компании, с множеством проектов, где важнее стандарты и единообразие?


        1. strangeraven
          02.08.2016 10:24

          Ну сначала о чисто бытовых неудобствах, которые мне видятся на пути подключения полноценных скриптов:
          1. Они предустановленны, но не везде. Например мы в процессе разработки запускаем свои сервера на чем попало, в том числе и windows. Придется бегать и ставить скрипты там.
          2. Предустановленная версия скриптов может не совпасть с требуемой. Или нестыковки в каких-то требуемых библиотеках, в путях, в переменных окружения.
          3. Надо еще как то передать данные из скрипта в свою программу, а как? Первое что приходит в голову, скрипт печатает их в stdout, а программа парсит. Но тогда нужны какие-то соглашения о формате этих данных, чем-то печатать, чем-то парсить. То есть скрипт ещё не является готовым решением, надо еще строить мост между скриптом и родительской программой.

          Если же сравнить эти телодвидения с HOCON, то нам надо всего лишь
          1. Поключить библиотеку (с помощью maven или gradle это делается одной строчкой)
          2. Вызвать функцию parseFile.
          3. Profit. После этого мы получаем готовый объект из которого можно читать значения.

          Теперь насчет сложности как языка. HOCON — это все-таки небольшая надстройка над JSON, с весьма ограниченными возможностями, которые решают вполне конкретную задачу. По сути это всё те же текстовые данные, но с возможностью прототипирования.

          Когда делаешь конфиги на обычном JSON или XML, то основная проблема с которой сталкиваешься: дублирование данных. И хочется эти дубликаты как-то выносить в общее место и потом на них ссылаться. Собственно, HOCON с помощью прототипирования как раз и помогает это сделать.

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

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

          Конечно, возможно в будущем опять окажется что чего-то не хватает, но пока что хватает. Одно из сильных достоинств HOCON — это его легковесность.


          1. yarulan
            02.08.2016 11:29

            Вы все усложняете со скриптами. Не нужно ничего предустанавливать, согласовывать версии и парсить stdout. В джаве есть интерпретатор джаваскрипта.

            val engine = new ScriptEngineManager().getEngineByName("nashorn")
            val config = engine.eval("""var config = {host: "localhost", port: 8080}; config""").asInstanceOf[ScriptObjectMirror]
            println(config.get("host"))
            

            Вот и все. Даже не нужно подключать внешних библиотек.


            1. strangeraven
              02.08.2016 11:38

              Принято.
              Да, встроенный JavaScript убирает все инфраструктурные издержки.

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

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


  1. terziele
    02.08.2016 09:37

    Довольно интересно, спасибо. Надо будет посмотреть повнимательней на этот HOCON.