Используйте переключатель функций для безопасного выпуска обновлений на вашем сервере
25 января 2023 г.Часто разработчики создают технические решения, используя все, что доступно, чтобы запустить функциональность. Особенно, когда нужно проверить какую-то продуктовую гипотезу и нет особого смысла тратить на это много времени и ресурсов. После подтверждения гипотезы начинается еще более интересное время — поиск компромиссов: нужно быстро выпускать фичи, подстраиваясь под рост числа пользователей.
Очень удобно иметь возможность в любой момент изменить поведение сервиса без перезагрузки серверов, отключить неработающий функционал или провести AB-тестирование. Для этого разработчики обычно используют переключатели функций. Если в вашем стартапе не гигант, а небольшая группа энтузиастов — вам придется решать эту проблему доступными инструментами. В этой статье я покажу вам простой способ реализации переключения функций с помощью общей базы данных SQL.
Описание типичной системы
Сколько бы усилий ни вкладывалось в разработку отказоустойчивых самодостаточных систем, ручное вмешательство неизбежно:
* исправление несоответствий данных, * запуск задач, * восстановление данных после сбоя, * заполнение или миграция, * отображение бизнес-операций и связанных данных.
Если проблему можно легко исправить, разработчик может настроить данные непосредственно в базе данных или написать bash/python/любой скрипт. Обычно мы избегаем прямого доступа к данным, но обстоятельства могут заставить вас это сделать. Но есть более надежный способ - приложение внутреннего пользования - Панель администрирования.
Преимущества панели администратора:
* авторизованный доступ; * протоколирование и аудит всех действий пользователя; * его код обычно пересматривается и тестируется; * доступен не только пользователям, но и техподдержке.
Часто админ-панель предназначена для доступа к базе данных серверов бизнес-логики. Этот подход имеет некоторые недостатки, но является хорошим компромиссом: обеспечение приемлемого уровня безопасности за счет простой реализации. Таким образом, скорее всего, вы можете использовать основную базу данных для хранения настроек, в том числе переключателя функций и доступа к ним со своих серверов.
Запачкайте руки кодом
Простой переключатель
Самый простой вариант использования — это двухпозиционный переключатель: либо включить для всех, либо отключить для всех.
private final FeatureToggleService featureToggleService;
public void businessLogic() {
if (featureToggleService.isFeatureEnabled(IMPORTANT_FEATURE)) {
doImportantFeatureStuff();
} else {
log.info("Feature disabled {}", IMPORTANT_FEATURE);
}
}
Если бы я реализовал это в памяти, я бы проверил, есть ли ключ в наборе хэшей. Если есть - функция включена, иначе - отключена. В базе данных SQL может быть таблица с именами признаков — это все, что нам здесь нужно. Мы можем запросить ключи, и вот как это может выглядеть:
public class FeatureToggleService {
private static final String STATE_QUERY = "SELECT id FROM features WHERE name = (?)";
private final JdbcOperations jdbcOperations;
private final LoadingCache<String, Boolean> toggleStateCache = CacheBuilder.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.build(new CacheLoader<>() {
public Boolean load(String featureName) {
return jdbcOperations.queryForList(STATE_QUERY, String.class, featureName)
.size() > 0;
}
});
public boolean isFeatureEnabled(String featureName) {
boolean state = toggleStateCache.getUnchecked(featureName);
return Boolean.TRUE.equals(state);
}
}
В примере выше я использовал кеш от Google Guava. Это простой локальный кеш, но в 99% случаев он наверняка удовлетворит ваши требования. Функции переключаются нечасто, нет смысла делать запросы часто. В качестве примера я выбрал 60 секунд, но это хороший компромисс, не слишком много запросов и не слишком долгое переключение.
Переключатель пользователя
Чуть более сложный случай. Вы хотите протестировать функциональность в рабочей среде, но не хотите, чтобы она была доступна всем пользователям. Вместо этого вы хотите включить эту функцию только для группы пользователей,
которые считаются тестировщиками.
private final FeatureToggleService featureToggleService;
public void businessLogic(LoggedUser loggedUser) {
if (featureToggleService.isFeatureEnabled(IMPORTANT_FEATURE, loggedUser.id())) {
doImportantFeatureStuff();
} else {
log.info("Feature disabled {}", IMPORTANT_FEATURE);
}
}
Появилось третье состояние фичи — тестирование. Итак, состояния:
- Включено для всех
- Отключено для всех
- Включено для тестировщиков
Если функция включена для тестирования - вы должны проверить, является ли текущий пользователь тестировщиком. Другими словами, находится ли пользователь в списке лиц, которые должны иметь доступ к этой функции.
Базе данных потребуется второй столбец, в котором будет храниться состояние функции (это может быть число или значимое имя). А еще вам нужно будет где-то хранить список тестировщиков, например, это может быть другая таблица в базе данных.
И реализация может быть:
public class FeatureToggleService {
private static final String STATE_QUERY =
"SELECT enabled_for FROM features WHERE name = (?)";
private static final String TESTERS_QUERY = "SELECT id FROM test_users";
private final JdbcOperations jdbcOperations;
private final Supplier<Set<Long>> testUsersSupplier =
memoizeWithExpiration(this::extractTesters, 60, TimeUnit.SECONDS);
private final LoadingCache<String, String> toggleStateCache = CacheBuilder.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.build(new CacheLoader<>() {
public String load(String featureName) {
return jdbcOperations.queryForList(STATE_QUERY, String.class, featureName)
.stream().findFirst().orElse("NONE");
}
});
public boolean isFeatureEnabled(String featureName, long userId) {
String state = toggleStateCache.getUnchecked(featureName);
return "ALL".equals(state) ||
("TEST".equals(state) && testUsersSupplier.get().contains(userId));
}
private Set<Long> extractTesters() {
return Set.copyOf(jdbcOperations.queryForList(TESTERS_QUERY, Long.class));
}
}
Сначала мы получаем состояние фичи по ее имени (либо из базы данных, либо из кеша), затем, если она ALL
, считаем ее включенной для всех, или если она TEST
— рассматриваем только ее включен для тестировщиков. Если это так, мы получаем список тестировщиков (либо из базы данных, либо из кеша) и проверяем, есть ли текущий пользователь в списке. Если ничего из вышеперечисленного — считайте, что функция отключена для всех.
Что может пойти не так в приведенных выше реализациях
Есть одна неочевидная проблема с локальным кешем, о которой вам следует знать:
- 09:45:27 функция включена для всех
- 09:45:47 запрос 1 поступил на сервер 1, и функция работала как включенная
- 09:45:51 запрос 2 поступил на сервер 2, и функция не работала, так как отключена
Почему это так? Локальные кэши ничего не знают о других серверах, в большинстве случаев это вообще не может быть проблемой. В противном случае вам следует выполнить одно из следующих действий:
- продолжить использовать локальный кеш, но удалить его с помощью уведомления (используя подсистему публикации или широковещательные сообщения);
- использовать распределенный кеш (eCache или аналогичный) и удалять его при каждом изменении;
- использовать базы данных в оперативной памяти (Redis, Zookeeper или аналогичные) для хранения функций и предоставления недорогого доступа к ним;
- воспользоваться платным сервисом, предоставляющим этот функционал (их несколько, здесь нет дополнений и настроек);
- придумайте свою гениальную идею, их может быть много :)
Заключение
Многие разработчики считают, что использовать причудливую структуру — это здорово, но вам нужно оценить, действительно ли это оправдано. Часто бывает проще написать простой модуль, удовлетворяющий специфические потребности вашего приложения, и разработать его по-своему. Но тут нужно чувствовать грань — писать свой HashMap с нуля нет никакого смысла.
Оригинал