Расскажу про нашу библиотеку django-liveconfigs, которая, как и множество других решений, позволяет администратору настраивать сервис, но при этом, как мне кажется, делает это чуть красивей и более по-питоновски.
Про какие настройки речь?
-
Говорим тут только о бизнес-настройках приложения и немного о технических
-
Не говорим о большой массе технических настроек, которые должны лежать в переменных окружения
-
Не говорим о настройках пользователя
История и предпосылки
Когда-то давно, еще в 2019 году, мы писали для заказчика бота-ассистента-секретаря с обработкой естественного языка вот с такими вводными:
-
нас 3 бэкендера
-
фронта у проекта нет, фронтендера тем более
-
весь бэк состоит из нескольких контейнеров — django, celery, celery-beat, redis, postgres, nginx. При этом django, celery и celery-beat раскатываются из одного образа, кодовая база у них одна
-
языковые модели большие и работают из оперативной памяти
-
сервис рестартует около минуты, за это время пользователи начинают переживать
-
возможности сначала поднять копию сервиса рядом, а потом переключить на нее трафик нет — ограничения архитектуры и ограничения ресурсов.
Нам понадобился способ добавить себе быстрое включение-выключение фич и какие-нибудь числовые и строковые настройки, которые можно менять условно мгновенно, не перезапуская сервис.
Дополнительные соображения
-
Отдельный (микро)сервис не стоит делать, иначе за ним тоже нужно будет следить.
-
Пользователей относительно немного.
-
Эксперименты с выкаткой на часть пользователей нам не нужны
-
Управлять настройками должен администратор через обычную админку django. Это значит, что перед глазами у него должна быть документация, которая тоже как-то должна попадать в админку.
-
У нас несколько стендов и нет никакого желания добавлять настройки на них руками, поэтому настройки в админке должны появляться “сами”, вместе с документацией и значением по-умолчанию. Значит, они должны быть в самом сервисе.
-
Добавлять и использовать настройки должен разработчик, причем для него это должно быть максимально легко.
-
Хорошо бы еще при попытке изменения значения настройки добавить проверку типа и самого значения, и также удобно описывать валидаторы.
Пример
Так мы пришли к тому, что решили описывать настройки в самом сервисе в виде атрибутов класса (с небольшой метаклассово-дескрипторной магией для работы с бд). На тот момент не было функционала Annotated, а теперь мы внимательно на него смотрим.
class Config(BaseConfig):
USE_NEW_FEATURE: bool = FalseUSE_NEW_FEATURE_DESCRIPTION = “Включена ли новая фича, которую мы разрабатывали два года”
SOME_VALUE: float = 42.1
SOME_VALUE_DESCRIPTION = “Новая настройка для старой фичи, по-умолчанию 42.1, значение должно быть больше 10”
SOME_VALUE_VALIDATORS = [greater_than(10)]
При обращении к Config.USE_NEW_FEATURE
наша магия по необходимости обновляет метаданные читаемой настройки на стенде — приводит описание и прочие атрибуты, кроме собственно значения, в соответствие с тем, что сейчас есть в исходниках.
Как это устроено внутри
Настройки описываются в классах, наследующихся от BaseConfig
. Для него же есть метакласс, который подменяет атрибуты класса на дескрипторы, которые, в свою очередь, и выполняют всю работу.
При чтении из дескриптора:
-
если в кеше есть значение, то возвращаем его и ничего больше не делаем
-
иначе,
-
обновляем метаданные о настройке в БД по необходимости. Поэтому можно и не добавлять отдельный шаг при выкатке «добавить все настройки на стенд»
-
получаем значение из БД, сохраняем его в кеш, отдаем пользователю
-
Подробнее можно посмотреть вот тут
А почему именно так?
Почему бы не value = get_value(‘some-value’)
Потому что при этом остаются следующие проблемы и вопросы:
-
можно ошибиться при наборе строки, как бы это смешно ни звучало
-
где хранится “источник” документации и кто за него отвечает?
-
какой тип у полученного значения? как об этом быстро узнать?
Почему не что-нибудь вроде USE_FEATURE = BooleanFlag(False, “Description”)
Теряем информацию о настоящем типе самого значения при разработке. В рантайме там будет boolean. Например, для поддержки подобного поведения полей в django у pycharm вообще должна быть платная версия.
Почему бы не хранить описание настроек в yml/json/где-то еще и не читать их в рантайме?
Теряем информацию о настоящих типах для линтера, есть возможность огрести в рантайме.
Почему бы не хранить описание настройки в docstring класса?
class UseNewFeature(BooleanConfig):
“”””Включена ли новая фича, которую мы делали-делали и, наконец, доделали“”””
default = False
class SomeValueConfig(FloatConfig):
“”””Очень важное значение, которое должно быть больше 10“”””
default = 42.1
validators = [greater_than(10)]
Непонятно, как такое использовать. Инстанцировать класс? Обращаться к атрибуту класса, например UseNewFeature.value? Выглядит не очень красиво.
Сейчас можно спокойно двигаться в сторону Annotated
Почему не unleash, flagsmith, growthbook или flipt
Это рабочие, хорошо зарекомендовавшие себя решения, но:
-
это отдельные сервисы, за которыми нужно следить
-
чтобы автоматически до них докатывать новые настройки, нужно дописывать скрипты выкатки. Многие просто заносят новые настройки руками через админку и это становится ручной частью каждой выкатки. Очень не хочется занимать этим команду.
-
они заточены на эксперименты и частичную раскатку нового функционала, а у нас такой потребности не было
-
кто отвечает за документацию значений и где источник правды?
-
как настраивать сложную валидацию?
-
у них местами очень странные клиенты. Вот, например, как из flipt нужно получать значение простого флага, если следовать документации:
boolean_flag = flipt_client.evaluation.boolean(
EvaluationRequest(
namespace_key="default",
flag_key="flag_boolean",
entity_id="entity",
context={"fizz": "buzz"},
)
)
Почему не django-waffle
На это есть множество причин, ниже приведу некоторые из них:
-
Это feature-flipper.
-
Он поддерживает только on/off для флагов.
-
В нем есть только boolean-значения.
-
Он, может ответить лишь “да” или “нет”.
-
Он не может хранить int, float, string или произвольный json
-
Позволяет только включать и выключать фичи.
Почему не django-constance
Очень близкая к нам библиотека, но заточена она под “оживление” settings. Кроме того, синтаксис добавления настроек у нее менее удобный:
CONSTANCE_CONFIG = OrderedDict([
('SITE_NAME', ('My Title', 'Website title')),
('SITE_DESCRIPTION', ('', 'Website description')),
('THEME', ('light-blue', 'Website theme')),
('THE_ANSWER', (42, 'The answer')),
])
Итоги и выводы
-
Если настройки добавлять легко, их будут добавлять. Сервисом становится сильно легче управлять , многие решения можно перенести ближе к заказчику
-
Документировать настройки нужно сразу
-
Хранить описание настроек в самом сервисе — хорошо
-
Хранить описание настроек в виде кода — хорошо
-
Для настроек нужен нормальный поиск
-
Настройки нужно уметь выводить из эксплуатации
-
Писать велосипеды иногда полезно
Что еще хотим сделать:
-
«Заморозка» значений
-
Асинхронная работа
-
Перейти на Annotated — кажется, это то, что нам нужно, чтобы меньше плодить атрибутов в классе.
ссылка на оригинал статьи https://habr.com/ru/articles/869432/
Добавить комментарий