Привет, Хабр! Если вы работаете с микросервисами, то знаете, что они имеют свойство образовывать некоторую связанность. Хорошо, когда связи между микросервисами однонаправленные, но всё становится сложнее, если возникают циклические зависимости.
Такие зависимости приводят к сложностям развертывания, которые можно преодолеть по-разному — например, используя docker compose. Но на локальном компьютере обычно не возникает необходимости поднятия всей инфраструктуры, потому что разработчика для выполнения задачи обычно интересует какая-нибудь конкретная её часть. В этом случае пригодятся средства изоляции микросервисов.
Меня зовут Сергей Прохоров, я техлид бэкенд-разработки в Ak Bars Digital, и давайте вместе рассмотрим, как реализовать такую изоляцию на примере микросервиса веб-API ASP.NET Core. Метод изоляции основан на использовании feature toggles, или переключателей функциональности, о которых и пойдёт речь в двух частях статьи.
Зачем нужна изоляция микросервисов
Потребности в изоляции могут быть разными, вот лишь некоторые из них, которые встречаются в разработке и тестировании.
Для разработки
При онбординге нового сотрудника. В финтехе получение доступа к внешним зависимостям требуют написания служебных записок в службу безопасности (а это может быть не быстро), а в ряде случаев, к некоторым сервисам доступ вообще не может быть предоставлен — шифрование или каналы ГОСТ являются ярким тому примером.
При аудите или анализе микросервиса с целью выявления технического или архитектурного долга. Иногда логика не столь тривиальна и пройтись отладчиком сильно проще трассировки «в уме».
При необходимости оперативно вносить правки (bug fix) с возможностью отладки. Ускоряем отладку, не тратя время на подъем зависимых контуров и настройку взаимодействия с ними.
Чтобы постоянно не поддерживать актуальность долгоживущих веток предотвращать конфликты слияния. Здесь лучше посмотреть статью по ссылке — там эта информация хорошо объясняется.
Для тестирования
Интеграционное и регрессионное тестирование при разработке. Тестирование изолированного микросервиса позволяет разработчику, при внесении правок, самостоятельно и быстро обнаруживать возникающие дефекты в существующем коде.
Тестирование в рамках непрерывной интеграции (CI), которую выполняет агент сборки (build agent). Зачастую в его окружении нет, или даже не может быть, доступа ко всем зависимостям микросервиса, например из-за все тех же требований безопасности или инфраструктурных ограничений.
Тестирование сценариев взаимодействия с зависимостями. Так можно воспроизводить различные варианты без явных вызовов внешней зависимости, получая соответствующий результат используя внутреннюю логику заглушек.
При тестировании микросервиса, когда внешняя зависимость не имеет тестового контура, а использование промышленной версии в таких объемах неприемлемо. Примером могут служить внешние сервисы оплаты, в которые отправлять реальные деньги на нереальные суммы было бы крайне нежелательно.
При эксплуатации staging-контура заглушки нужны для целей тестирования. Сопряжение стейджинга с промышленными контурами чревато для целей тестирования, а сопряжение с тестовыми контурами зачастую неактуально. При этом исходная кодовая база должна быть идентичной production-контуру.
При нагрузочном тестировании, когда число запросов к внешним зависимостям лимитировано по частоте запросов или по их количеству.
Вместо изоляции в этих сценариях применяют различные инструменты, но в тех случаях, когда реализация посредством изоляции будет дешевле, нужно выбирать именно её.
Что такое feature toggles
Переключатели функциональности (feature toggles) или флаги функциональности (feature flags) — это переменные, которые используются внутри условных операторов.
Блоки внутри этих условных операторов могут быть включены или выключены в зависимости от значения переключателя функции. Это позволяет разработчикам контролировать поток выполнения своего программного обеспечения и обходить функциональность, которая не должна выполняться.
Переключатели можно использовать в следующих сценариях:
добавление новой функции;
улучшение существующей функции;
сокрытие или отключение функции;
расширение интерфейса.
Переключатели можно сохранить как свойство в файле конфигурации, в виде записи в базе данных или как запись во внешней службе функциональных флагов.
Канареечные релизы
Еще одно преимущество флагов функциональности — канареечные релизы.
Канареечный релиз позволяет разработчикам постепенно тестировать функции на небольшой группе пользователей. Если производительность функции по каким-то причинам не устраивает разработку или бизнес, ее можно откатить во избежание побочных эффектов.
Microsoft.FeatureManagement
Библиотеки управления функциями добавляют реализацию флагов функций в приложении и реализованы в NuGet-пакетах Microsoft.FeatureManagement
и Microsoft.FeatureManagement.AspNetCore
. Оба пакета имеют поддержку .Net Standard 2.0 и выше.
Команды для подключения NuGet-пакетов в проекты с использованием dotnet CLI:
dotnet add package Microsoft.FeatureManagement
dotnet add package Microsoft.FeatureManagement.AspNetCore
или используя диспетчер пакетов Visual Studio:
Install-Package Microsoft.FeatureManagement
Install-Package Microsoft.FeatureManagement.AspNetCore
Поскольку Microsoft.FeatureManagement построен поверх системы конфигурации, он может управляться любым из самых разных поставщиков конфигурации. Это означает, что вы можете управлять функциями из файла appsettings.json, из переменных среды, из базы данных, или выбрать какой-то из большого количества готовых поставщиков или даже реализовать поставщика самостоятельно. Включение и отключение функции просто требует изменения значения в выбранном поставщике конфигурации.
Библиотека Microsoft.FeatureManagement.AspNetCore добавляет для ASP.NET функциональность, которая будет зависеть от заданного режима (включено/выключено):
фильтры действий для удаления действий контроллеров;
методы расширения для условной регистрации маршрутов, глобальные фильтры и промежуточное ПО;
вспомогательные функции тегов для условного скрытия разделов пользовательского интерфейса MVC.
Примеры настройки appsetings.json
Вот несколько примеров простой, но очень гибкой настройки appsettings.json
, которая обретет функциональность с некоторыми дополнениями в коде приложения:
{
"FeatureManagement": {
// Включение функциональности булевым значением.
"Beta": true,
// Включение новогоднего баннера.
"NYBanner": {
"EnabledFor": [
{
"Name": "Microsoft.TimeWindow",
"Parameters": {
"Start": "01.01.2022 00:00:00 +03:00",
"End": "01.01.2022 23:59:59 +03:00"
}
}
]
},
// Канареечное применение моноширинных шрифтов для 10% пользователей.
"MonospaceFonts": {
"EnabledFor": [
{
"Name": "Microsoft.Percentage",
"Parameters": {
"Value": 10
}
}
]
},
// Канареечное испытание Alpha-версии определенными пользователями.
"Alpha": {
"EnabledFor": [
{
"Name": "Claims",
"Parameters": {
"RequiredClaims": [ "Internal" ]
}
}
]
}
}
}
Далее в этой и следующей части статьи я на примере разберу все аспекты работы с флагами функциональности.
Создание веб API ASP.NET Core приложения
Создадим приложение, на котором отработаем изоляцию микросервиса с использованием заглушки.
Создаём шаблонное приложение в Visual Studio
Шаг 1. Создаём пустое приложение Веб-API ASP.NET Core
На этом этапе вводим имя проекта и выбираем целевую платформу .NET 5.0 (актуальная версия на момент написания примера — прим. ред.)
Теперь можно запустить приложение и проверить его функциональность по умолчанию.
Мастер генерации приложения сформировал код контроллера, который генерирует случайный прогноз погоды.
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
Давайте сделаем так, чтобы приложение получало реальный прогноз с погодного сервиса, а сгенерированный код позже используем для изоляции — сможем переключаться на него с помощью флага функциональности.
Реализуем прогноз погоды
Для получения реальных данных прогноза погоды я выбрал сервис OpenWeather — он бесплатный, у него низкий порог входа и хорошая документация.
Класс конфигурации OpenWeather
/// <summary>
/// Конфигурация взаимодействия с сервисом прогноза погоды OpenWeather.
/// </summary>
public class OpenWeatherConfiguration
{
/// <summary>
/// Базовый адрес сервиса.
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// Идентификатор города, для которого запрашивается прогноз.
/// </summary>
public string CityId { get; set; }
/// <summary>
/// API-ключ, доступный после регистрации по адресу https://home.openweathermap.org/api_keys.
/// </summary>
public string ApiKey { get; set; }
}
Поиск идентификатора города
Для поиска идентификатора нужного города нужно перейти на http://bulk.openweathermap.org/sample/ и скачать архив city.list.json.gz
. После распаковки файла city.list.json
находим в нём город по наименованию, стране и географическим координатам и сохраняем его идентификатор.
Например, у Казани в этом файле идентификатор 551487
.
{
"id": 551487,
"name": "Kazan",
"state": "",
"country": "RU",
"coord": {
"lon": 49.122139,
"lat": 55.788738
}
},
Чтобы легко жилось не только разработчикам, но и DevOps и QA-инженерам, вместе с ключами конфигурации стоит копировать в appsettings.json и комментарии. Например, так
{
// ...
// Раздел настроек приложения.
"App": {
// Конфигурация взаимодействия с сервисом прогноза погоды OpenWeather.
"OpenWeather": {
// Базовый адрес сервиса.
"BaseAddress": "https://api.openweathermap.org/data/2.5/ ",
// Идентификатор города, для которого запрашивается прогноз.
"CityId": "551487", // Казань, РТ.
// API-ключ, доступный после регистрации по адресу https://home.openweathermap.org/api_keys.
"ApiKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
},
// ...
}
Каждая библиотека, подключенная к проекту с помощью NuGet, может добавлять новые разделы в
appsettings.json
. Чтобы исключить случайные пересечения наименований разделов, мы размещаем разделы в качестве подразделов в App.
Для сопоставления значений из appsettings.json
в экземпляры классов конфигурации OpenWeatherConfiguration
осуществляется настройка в Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<OpenWeatherConfiguration>(Configuration.GetSection("App:OpenWeather"));
// ...
}
Реализация сервиса запросов
Далее объявляется интерфейс и класс реализации сервиса:
Интерфейс и класс реализации сервиса
public interface IWeatherForecastService
{
public Task<IEnumerable<WeatherForecastResponse>> GetByIdAsync(string cityId);
}
public class OpenWeatherService : IWeatherForecastService
{
private readonly HttpClient _httpClient;
private readonly IMapper _mapper;
private readonly string _apiKey;
public OpenWeatherService(
HttpClient httpClient, // Настроенный HTTP-клиент предоставляется фабрикой. Настройку см. далее в статье.
IOptions<OpenWeatherConfiguration> options,
IMapper mapper)
{
_httpClient = httpClient;
_mapper = mapper;
_apiKey = options.Value.ApiKey; // Получение API-ключа из конфигурации.
}
public Task<IEnumerable<WeatherForecastResponse>> GetByIdAsync(string cityId)
{
string path = $"forecast?id={cityId}&appid={_apiKey}&units=metric&cnt=5&lang=ru";
return GetByPathAsync(path);
}
private async Task<IEnumerable<WeatherForecastResponse>> GetByPathAsync(string path)
{
using HttpResponseMessage httpResponseMessage = await _httpClient.GetAsync(path);
httpResponseMessage.EnsureSuccessStatusCode();
using Stream stream = await httpResponseMessage.Content.ReadAsStreamAsync();
var weatherResponse = await JsonSerializer.DeserializeAsync<Response>(stream);
var result = weatherResponse.list.Select(x => _mapper.Map<WeatherForecastResponse>(x));
return result;
}
}
Регистрация созданного сервиса и настройка фабрики HTTP-клиентов в Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
// ...
// Регистрация OpenWeatherService и конфигурирование HTTP-клиента для него.
services.AddHttpClient<IWeatherForecastService, OpenWeatherService>(
"OpenWeather",
(serviceProvider, client) =>
{
// Настройка базового адреса согласно конфигурации.
var config = serviceProvider.GetRequiredService<IOptions<OpenWeatherConfiguration>>().Value;
client.BaseAddress = new Uri(config.BaseAddress);
});
// ...
}
Генерация и сопоставление моделей ответов
Генерация моделей ответа OpenWeather
Можно было бы воспользоваться документацией на сайте и написать модели ответа вручную, но можно пойти простым путём. Немного модифицировав исходный код текста получаем ответ от сервера в виде строки. После выполнения команды контекстного меню «Быстрая проверка» для переменной response, отображается её текущее значение:
Копируем значение в буфер обмена, а затем, остановив отладку и переставив курсор в конец файла, выполняем следующую команду:
Теперь класс Rootobject
переименован в Response
. Все классы выделены в отдельные файлы, которые помещены в папку Models/OpenWeather
, ну и пространства имен приведены в порядок. Исходный класс ответа контроллера WeatherForecast
переименован в WeatherForecastResponse
и перемещён в папку Models
.
Получилась такая структура проекта:
Такой подход к генерации моделей имеет недостатки: в зависимости от текущего значения в тексте могут быть применены целочисленные типы вместо типов с плавающей запятой, или вовсе не будут сгенерированы отдельные классы, если поля имеют значение null. Так что сверка с документацией всё же не будет лишней.
Подключение AutoMapper
Для сопоставления модели ответа OpenWeather и модели ответа контроллера используем NuGet-пакет AutoMapper.
Для такого простого примера это слишком, но в реальных проектах такой способ существенно упрощает разработку, поэтому используем именно его.
Команда подключения NuGet-пакета в проект с использованием dotnet CLI:
dotnet add package AutoMapper
или используя диспетчер пакетов Visual Studio:
Install-Package AutoMapper
Класс настройки сопоставления моделей ответов:
public class MapperProfile : Profile
{
public MapperProfile()
{
CreateMap<List, WeatherForecastResponse>()
.ForMember(dest => dest.Date, opt => opt.MapFrom(src => DateTime.Parse(src.dt_txt)))
.ForMember(dest => dest.TemperatureC, opt => opt.MapFrom(src => src.main.temp))
.ForMember(dest => dest.Summary, opt => opt.MapFrom(src => src.weather[0].description));
}
}
Настройка AutoMapper
в Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAutoMapper(typeof(Startup));
// ...
}
Изменения в контроллере
Как я уже упоминал, позже переиспользуем код, полученный при генерации решения по шаблону.
Чтобы не занимать буфер обмена, перетянем выделенный код контроллера на панель элементов, чтобы затем перетянуть его обратно в редактор при написании заглушки.
Теперь, когда прежний код отложен на панель элементов, код контроллера приводим к следующему виду:
[Route("weather-forecast")]
public class WeatherForecastController
{
[HttpGet]
public async Task<IEnumerable<WeatherForecastResponse>> Get(
[FromServices] IWeatherForecastService weatherForecastService,
[FromServices] IOptions<OpenWeatherConfiguration> options)
{
var result = await weatherForecastService.GetByIdAsync(options.Value.CityId);
return result;
}
}
Проверка функциональности приложения
На этом этапе у нас есть готовое приложение. Теперь его можно запустить и проверить боевую функциональность.
Что дальше?
Как видим, приложение работает, но это далеко не всё, что нужно сделать. Теперь нам нужна реализация заглушки, которую мы сделали во второй части статьи. Кроме этого сделали экспериментальную конечную точку, функциональность которой можно включать или выключать, не останавливая работу приложения. А ещё разобрались с экстренными ситуациями, которые могут случиться в реальной разработке, и поняли, как этого избежать с помощью флагов функциональности.
А пока спасибо за внимание, мы с коллегами из Ak Bars Digital готовы ответить на все вопросы в комментариях.