При проектировании решений на базе микросервисной архитектуры мы часто сталкиваемся с требованием быстрого и простого управления всей системой, максимально возможной автоматизации, без необходимой настройки отдельных компонентов.
Это настоящая задача, и поэтому я решил подготовить руководство, демонстрирующее, как максимально простым способом создать микросервисную архитектуру, которую можно быстро и очень легко масштабировать и адаптировать к требованиям клиента.
Я не хотел вмешиваться в код и настройки отдельных сервисов, а управлять системой только оркестровкой контейнеров в Docker.
Результатом является простая микросервисная архитектура, которую можно легко масштабировать с помощью всего лишь нескольких изменений в настройках контейнера, все остальное обеспечивается Ocelot в качестве шлюза/балансировщика нагрузки и Consul в качестве агента обнаружения сервисов.
Такая архитектура позволяет нам повторно развернуть один сервис без координации развертывания внутри других сервисов. Повторно развернутая служба автоматически регистрируется при обнаружении службы и сразу же становится доступной через шлюз. Вы можете себе представить, какой это большой стимул для каждой команды разработчиков!
Конечно, использование одного шлюза становится единственной точкой отказа в нашей архитектуре, поэтому нам необходимо развернуть как минимум два его экземпляра, чтобы обеспечить высокую доступность. Но я оставлю эту проблему на ваше усмотрение.
В своей предыдущей демонстрации я показал, как реализовать Ocelot в качестве шлюза служб и балансировщика нагрузки вместе с Eureka для обнаружения служб. Вместо Eureka в этой демонстрации используется Consul для обнаружения сервисов.
Consul — это решение Service Mesh, предоставляющее полнофункциональную плоскость управления с функциями обнаружения, настройки и сегментации сервисов. Каждую из этих функций можно использовать индивидуально по мере необходимости или вместе для создания полноценной сети сервисов. Consul требует плоскости данных и поддерживает как прокси-сервер, так и собственную модель интеграции. Consul поставляется с простым встроенным прокси-сервером, так что все работает «из коробки», но также поддерживает интеграцию сторонних прокси, таких как Envoy.
Ключевые особенности Консула:
Обнаружение служб . Клиенты Consul могут зарегистрировать службу, например API или MySQL, а другие клиенты могут использовать Consul для обнаружения поставщиков данной услуги. Используя DNS или HTTP, приложения могут легко найти службы, от которых они зависят.
Проверка работоспособности . Клиенты Consul могут выполнять любое количество проверок работоспособности, связанных либо с конкретной службой («возвращает ли веб-сервер 200 ОК»), либо с локальным узлом («использование памяти ниже 90%)». Эта информация может использоваться оператором для мониторинга работоспособности кластера, а также компонентами обнаружения служб для маршрутизации трафика от неработоспособных узлов.
KV Store : приложения могут использовать иерархическое хранилище ключей/значений Consul для любого количества целей, включая динамическую настройку, пометку функций, координацию, выборы лидеров и многое другое. Простой HTTP API упрощает использование.
Безопасная связь между службами : Consul может генерировать и распространять сертификаты TLS для служб для установления взаимных соединений TLS. Намерения можно использовать для определения того, каким сервисам разрешено взаимодействовать. Сегментацией услуг можно легко управлять с помощью намерений, которые можно изменить в реальном времени, вместо использования сложных сетевых топологий и статических правил брандмауэра.
Мультицентр обработки данных : Consul поддерживает несколько центров обработки данных «из коробки». Это означает, что пользователям Consul не нужно беспокоиться о создании дополнительных уровней абстракции для расширения до нескольких регионов.
Consul спроектирован так, чтобы быть дружелюбным как к сообществу DevOps, так и к разработчикам приложений, что делает его идеальным для современных гибких инфраструктур.
Источник: введение консула.
Ключевой частью руководства является использование Consul для динамического обнаружения конечных точек службы. После регистрации службы в Consul ее можно обнаружить с помощью обычного DNS или специального API.
Consul обеспечивает проверку работоспособности этих экземпляров службы. Если один из экземпляров службы или сама служба неработоспособна или не проходит проверку работоспособности, реестр узнает об этом и не будет возвращать адрес службы. В этом случае работа, которую будет выполнять балансировщик нагрузки, выполняется реестром.
Поскольку мы используем несколько экземпляров одного и того же сервиса , Consul будет случайным образом отправлять трафик в разные экземпляры. Таким образом, он балансирует нагрузку между экземплярами служб.
Consul решает проблемы обнаружения сбоев и распределения нагрузки между несколькими экземплярами сервисов без необходимости развертывания централизованного балансировщика нагрузки.
Он автоматически управляет реестром, который обновляется при регистрации любого нового экземпляра службы и становится доступным для приема трафика. Это помогает нам легко масштабировать услуги.
Прежде чем углубляться в детали реализации самостоятельной регистрации в Consul, давайте посмотрим, как на самом деле работает обнаружение сервисов с помощью самостоятельной регистрации.
На первом этапе экземпляр службы регистрируется в службе обнаружения служб, предоставляя свое имя, идентификатор и адрес. После этого шлюз сможет получить адрес этой службы, запросив обнаружение службы Consul по ее имени/идентификатору.
Здесь важно отметить, что экземпляры службы регистрируются с уникальным идентификатором службы , чтобы устранить неоднозначность между экземплярами службы, которые работают на одном и том же агенте службы Consul. Требуется, чтобы все службы имели уникальный идентификатор для каждого узла , поэтому, если имена могут конфликтовать (в нашем случае), необходимо предоставить уникальные идентификаторы.
Давайте посмотрим, как можно реализовать саморегистрацию в .NET-приложении. Во-первых, нам нужно прочитать конфигурацию, необходимую для обнаружения службы, из переменных среды, которые были переданы через файл docker-compose.override.yml .
public static class ServiceConfigExtensions
{
public static ServiceConfig GetServiceConfig ( this IConfiguration configuration )
{
ArgumentNullException . ThrowIfNull ( configuration ) ;
ServiceConfig serviceConfig = new ( )
{
Id = configuration . GetValue < string > ( "ServiceConfig:Id" ) ,
Name = configuration . GetValue < string > ( "ServiceConfig:Name" ) ,
ApiUrl = configuration . GetValue < string > ( "ServiceConfig:ApiUrl" ) ,
Port = configuration . GetValue < int > ( "ServiceConfig:Port" ) ,
ConsulUrl = configuration . GetValue < Uri > ( "ServiceConfig:ConsulUrl" ) ,
HealthCheckEndPoint = configuration . GetValue < string > ( "ServiceConfig:HealthCheckEndPoint" ) ,
} ;
return serviceConfig ;
}
}
После прочтения конфигурации, необходимой для доступа к службе обнаружения служб, мы можем использовать ее для регистрации нашей службы. Код ниже реализован как фоновая задача (размещенная служба), которая регистрирует службу в Consul, переопределяя предыдущую информацию о службе, если таковая существовала. Если служба закрывается, она автоматически удаляется из реестра Consul.
public class ServiceDiscoveryHostedService (
ILogger < ServiceDiscoveryHostedService > logger ,
IConsulClient client ,
ServiceConfig config )
: IHostedService
{
private AgentServiceRegistration _serviceRegistration ;
/// <summary>
/// Registers service to Consul registry
/// </summary>
public async Task StartAsync ( CancellationToken cancellationToken )
{
_serviceRegistration = new AgentServiceRegistration
{
ID = config . Id ,
Name = config . Name ,
Address = config . ApiUrl ,
Port = config . Port ,
Check = new AgentServiceCheck ( )
{
DeregisterCriticalServiceAfter = TimeSpan . FromSeconds ( 5 ) ,
Interval = TimeSpan . FromSeconds ( 15 ) ,
HTTP = $ "http:// { config . ApiUrl } : { config . Port } /api/values/ { config . HealthCheckEndPoint } " ,
Timeout = TimeSpan . FromSeconds ( 5 )
}
} ;
try
{
await client . Agent . ServiceDeregister ( _serviceRegistration . ID , cancellationToken ) . ConfigureAwait ( false ) ;
await client . Agent . ServiceRegister ( _serviceRegistration , cancellationToken ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
logger . LogError ( ex , $ "Error while trying to deregister in { nameof ( StartAsync ) } " ) ;
}
}
/// <summary>
/// If the service is shutting down it deregisters service from Consul registry
/// </summary>
public async Task StopAsync ( CancellationToken cancellationToken )
{
try
{
await client . Agent . ServiceDeregister ( _serviceRegistration . ID , cancellationToken ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
logger . LogError ( ex , $ "Error while trying to deregister in { nameof ( StopAsync ) } " ) ;
}
}
}
После того как мы зарегистрировали наши службы в службе обнаружения служб, мы можем приступить к реализации API шлюза.
Ocelot требует, чтобы вы предоставили файл конфигурации, содержащий список маршрутов (конфигурация, используемая для сопоставления восходящего запроса) и глобальную конфигурацию (другая конфигурация, такая как качество обслуживания, ограничение скорости и т. д.). В файле ocelot.json ниже вы можете увидеть, как мы пересылаем HTTP-запросы. Нам нужно указать, какой тип балансировщика нагрузки мы будем использовать. В нашем случае это «RoundRobin» , который циклически перебирает доступные сервисы и отправляет запросы к доступным сервисам. Важно настроить Consul как службу обнаружения служб в GlobalConfiguration для ServiceDiscoveryProvider .
{
"Routes" : [
{
"Servicename" : " ValueService " ,
"DownstreamPathTemplate" : " /{everything} " ,
"DownstreamScheme" : " http " ,
"UpstreamPathTemplate" : " /{everything} " ,
"UpstreamHttpMethod" : [ " GET " ],
"UseServiceDiscovery" : true ,
"RouteIsCaseSensitive" : false ,
"LoadBalancerOptions" : {
"Type" : " RoundRobin "
},
"QoSOptions" : {
"ExceptionsAllowedBeforeBreaking" : 3 ,
"DurationOfBreak" : 5000 ,
"TimeoutValue" : 2000
}
}
],
"GlobalConfiguration" : {
"RequestIdKey" : " OcelotRequestId " ,
"UseServiceDiscovery" : true ,
"ServiceDiscoveryProvider" : {
"Host" : " consul " ,
"Port" : 8500 ,
"Type" : " PollConsul " ,
"PollingInterval" : 100
}
}
}
Ниже приведены некоторые необходимые пояснения к настройкам ServiceDiscoveryProvider в разделе GlobalConfiguration :
После того, как мы определили нашу конфигурацию, мы можем приступить к реализации API-шлюза. Ниже мы можем увидеть реализацию службы Ocelot API Gateway, которая использует наш файл конфигурации ocelot.json и Consul в качестве реестра службы.
IHostBuilder hostBuilder = Host . CreateDefaultBuilder ( args )
. UseContentRoot ( Directory . GetCurrentDirectory ( ) )
. ConfigureWebHostDefaults ( webBuilder =>
{
webBuilder . ConfigureServices ( services =>
services
. AddOcelot ( )
. AddConsul < MyConsulServiceBuilder > ( )
. AddCacheManager ( x =>
{
x . WithDictionaryHandle ( ) ;
} )
. AddPolly ( ) ) ;
webBuilder . Configure ( app =>
app . UseOcelot ( ) . Wait ( ) )
. ConfigureAppConfiguration ( ( hostingContext , config ) =>
{
config
. SetBasePath ( hostingContext . HostingEnvironment . ContentRootPath )
. AddJsonFile ( "appsettings.json" , false , true )
. AddJsonFile ( $ "appsettings. { hostingContext . HostingEnvironment . EnvironmentName } .json" , true , true )
. AddJsonFile ( "ocelot.json" , false , true )
. AddEnvironmentVariables ( ) ;
} )
. ConfigureLogging ( ( builderContext , logging ) =>
{
logging . ClearProviders ( ) ;
logging . AddConsole ( ) ;
logging . AddDebug ( ) ;
} ) ;
} ) ;
IHost host = hostBuilder . Build ( ) ;
await host . RunAsync ( ) ;
Как упоминалось ранее, мы будем контейнеризировать все сервисы с помощью Docker, включая Consul , используя для контейнеров облегченные дистрибутивы GNU/Linux .
Файл docker-compose.yml с настройкой для всех контейнеров выглядит так:
services :
services :
consul :
image : hashicorp/consul
container_name : consul
command : consul agent -dev -log-level=warn -ui -client=0.0.0.0
hostname : consul
networks :
- common_network
valueservice1.openapi :
image : valueservice.openapi:latest
container_name : valueservice1.openapi
restart : on-failure
hostname : valueservice1.openapi
build :
context : .
dockerfile : src/ValueService.OpenApi/Dockerfile
networks :
- common_network
valueservice2.openapi :
image : valueservice.openapi:latest
container_name : valueservice2.openapi
restart : on-failure
hostname : valueservice2.openapi
build :
context : .
dockerfile : src/ValueService.OpenApi/Dockerfile
networks :
- common_network
valueservice3.openapi :
image : valueservice.openapi:latest
container_name : valueservice3.openapi
restart : on-failure
hostname : valueservice3.openapi
build :
context : .
dockerfile : src/ValueService.OpenApi/Dockerfile
networks :
- common_network
services.gateway :
image : services.gateway:latest
container_name : services.gateway
restart : on-failure
hostname : services.gateway
build :
context : .
dockerfile : src/Services.Gateway/Dockerfile
networks :
- common_network
networks :
common_network :
driver : bridge
Обратите внимание, что наши сервисы не содержат никаких файлов конфигурации, для этой цели мы будем использовать файл Docker-compose.override.yml :
services :
consul :
ports :
- " 8500:8500 "
valueservice1.openapi :
# Swagger UI: http://localhost:9100/index.html
# http://localhost:9100/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
- ServiceConfig__ApiUrl=valueservice1.openapi
- ServiceConfig__ConsulUrl=http://consul:8500
- ServiceConfig__HealthCheckEndPoint=healthcheck
- ServiceConfig__Id=ValueService.OpenApi-9100
- ServiceConfig__Name=ValueService
- ServiceConfig__Port=8080
ports :
- 9100:8080
depends_on :
- consul
valueservice2.openapi :
# Swagger UI: http://localhost:9200/index.html
# http://localhost:9200/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
- ServiceConfig__ApiUrl=valueservice2.openapi
- ServiceConfig__ConsulUrl=http://consul:8500
- ServiceConfig__HealthCheckEndPoint=healthcheck
- ServiceConfig__Id=ValueService.OpenApi-9200
- ServiceConfig__Name=ValueService
- ServiceConfig__Port=8080
ports :
- 9200:8080
depends_on :
- consul
valueservice3.openapi :
# Swagger UI: http://localhost:9300/index.html
# http://localhost:9300/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
- ServiceConfig__ApiUrl=valueservice3.openapi
- ServiceConfig__ConsulUrl=http://consul:8500
- ServiceConfig__HealthCheckEndPoint=healthcheck
- ServiceConfig__Id=ValueService.OpenApi-9300
- ServiceConfig__Name=ValueService
- ServiceConfig__Port=8080
ports :
- 9300:8080
depends_on :
- consul
services.gateway :
# Call first available service: http://localhost:9500/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
ports :
- 9500:8080
depends_on :
- consul
- valueservice1.openapi
- valueservice2.openapi
- valueservice3.openapi
Чтобы выполнить файл создания, откройте Powershell и перейдите к файлу создания в корневой папке. Затем выполните следующую команду: docker-compose up -d --build --remove-orphans, которая запустит и запустит все службы. Параметр -d выполняет команду отдельно. Это означает, что контейнеры работают в фоновом режиме и не блокируют окно Powershell. Чтобы проверить все запущенные контейнеры, используйте команду docker ps .
Consul предлагает приятный веб-интерфейс прямо из коробки. Вы можете получить к нему доступ через порт 8500 : http://localhost:8500. Давайте посмотрим на некоторые экраны.
Домашняя страница служб пользовательского интерфейса Consul со всей соответствующей информацией, касающейся агента Consul и проверки веб-сервиса.
Сделаем несколько вызовов через API Gateway: http://localhost:9500/api/values. Балансировщик нагрузки будет перебирать доступные сервисы, отправлять запросы и возвращать ответы:
Микросервисные системы непросто создавать и обслуживать. Но это руководство показало, насколько легко разработать и развернуть приложение с микросервисной архитектурой. HashiCorp Consul предлагает первоклассную поддержку для обнаружения сервисов, проверки работоспособности, хранения ключей и нескольких центров обработки данных. Ocelot в качестве шлюза успешно взаимодействует с реестром служб Consul и получает регистрации служб, балансировщик нагрузки просматривает доступные службы и отправляет запросы. Использование обоих значительно облегчает жизнь разработчикам, сталкивающимся с такими проблемами. Вы согласны?
Наслаждаться!
Лицензия MIT. Свяжитесь со мной в LinkedIn.