Al diseñar soluciones basadas en la arquitectura de microservicios, a menudo nos encontramos con el requisito de una gestión rápida y sencilla de todo el sistema, la mayor automatización posible, sin el ajuste necesario de los componentes individuales.
Este es un verdadero desafío y es por eso que decidí preparar un tutorial que demuestra cómo establecer una arquitectura de microservicio de la manera más sencilla posible, que se puede escalar y adaptar rápida y fácilmente a los requisitos del cliente.
No quería interferir con el código y la configuración de servicios individuales, sino controlar el sistema solo organizando contenedores en Docker.
El resultado es una arquitectura de microservicio simple que se puede escalar fácilmente con solo unos pocos cambios en la configuración del contenedor; todo lo demás lo proporciona Ocelot como puerta de enlace/equilibrador de carga y Consul como agente de descubrimiento de servicios.
Esta arquitectura nos permite volver a implementar un único servicio sin coordinar la implementación dentro de otros servicios. El servicio redistribuido se registra automáticamente en el momento del descubrimiento del servicio y está disponible inmediatamente a través de la puerta de enlace. ¡Puedes imaginar el gran impulso que supone esto para cada equipo de desarrollo!
Claro, el uso de un servicio de puerta de enlace única se convierte en un punto único de falla para nuestra arquitectura, por lo que debemos implementar al menos dos instancias del mismo para tener alta disponibilidad. Pero te dejaré ese problema para que juegues con él.
En mi demostración anterior mostré cómo implementar Ocelot como puerta de enlace de servicios y equilibrador de carga junto con Eureka para el descubrimiento de servicios. En lugar de Eureka, esta demostración utiliza Consul para el descubrimiento de servicios.
Consul es una solución de malla de servicios que proporciona un plano de control completo con funcionalidad de segmentación, configuración y descubrimiento de servicios. Cada una de estas funciones se puede utilizar individualmente según sea necesario o se pueden utilizar juntas para crear una red de servicios completa. Consul requiere un plano de datos y admite un modelo de integración nativo y de proxy. Consul viene con un proxy incorporado simple para que todo funcione de inmediato, pero también admite integraciones de proxy de terceros, como Envoy.
Las características clave de Consul son:
Descubrimiento de servicios : los clientes de Consul pueden registrar un servicio, como api o mysql, y otros clientes pueden usar Consul para descubrir proveedores de un servicio determinado. Al utilizar DNS o HTTP, las aplicaciones pueden encontrar fácilmente los servicios de los que dependen.
Comprobación de estado : los clientes de Consul pueden proporcionar cualquier número de comprobaciones de estado, ya sea asociadas con un servicio determinado ("el servidor web devuelve 200 OK") o con el nodo local ("la utilización de la memoria es inferior al 90 %"). Un operador puede utilizar esta información para supervisar el estado del clúster y los componentes de descubrimiento de servicios la utilizan para enrutar el tráfico lejos de los hosts en mal estado.
KV Store : las aplicaciones pueden hacer uso del almacén jerárquico de clave/valor de Consul para cualquier número de propósitos, incluida la configuración dinámica, el marcado de funciones, la coordinación, la elección de líder y más. La sencilla API HTTP facilita su uso.
Comunicación de servicio segura : Consul puede generar y distribuir certificados TLS para que los servicios establezcan conexiones TLS mutuas. Se pueden utilizar intenciones para definir qué servicios pueden comunicarse. La segmentación de servicios se puede gestionar fácilmente con intenciones que se pueden cambiar en tiempo real en lugar de utilizar topologías de red complejas y reglas de firewall estáticas.
Centro de datos múltiple : Consul admite múltiples centros de datos desde el primer momento. Esto significa que los usuarios de Consul no tienen que preocuparse por crear capas adicionales de abstracción para crecer en múltiples regiones.
Consul está diseñado para ser amigable tanto para la comunidad DevOps como para los desarrolladores de aplicaciones, lo que lo hace perfecto para infraestructuras modernas y elásticas.
Fuente: Introducción del cónsul
Una parte clave del tutorial es el uso de Consul para descubrir dinámicamente puntos finales de servicio. Una vez que un servicio se registra en Consul, se puede descubrir utilizando DNS típico o API personalizada.
Consul proporciona controles de estado en estas instancias de servicio. Si una de las instancias de servicio o los servicios en sí no están en buen estado o no superan su verificación de estado, el registro lo sabrá y evitará devolver la dirección del servicio. En este caso, el trabajo que haría el equilibrador de carga lo maneja el registro.
Debido a que utilizamos varias instancias del mismo servicio , Consul enviaría tráfico aleatoriamente a diferentes instancias. De este modo, equilibra la carga entre instancias de servicios.
Consul maneja los desafíos de detección de fallas y distribución de carga en múltiples instancias de servicios sin la necesidad de implementar un equilibrador de carga centralizado.
Gestiona automáticamente el registro, que se actualiza cuando se registra una nueva instancia del servicio y queda disponible para recibir tráfico. Esto nos ayuda a escalar fácilmente los servicios.
Antes de entrar en detalles de implementación sobre cómo implementar el autorregistro en Consul, veamos cómo funciona realmente el descubrimiento de servicios con autorregistro.
En un primer paso, una instancia de servicio se registra en el servicio de descubrimiento de servicios proporcionando su nombre, ID y dirección. Después de que esta puerta de enlace pueda obtener la dirección de este servicio consultando el descubrimiento del servicio Consul por su nombre/ID.
Lo clave a tener en cuenta aquí es que las instancias de servicio se registran con un ID de servicio único para eliminar la ambigüedad entre instancias de servicio que se ejecutan en el mismo agente de servicio Consul. Se requiere que todos los servicios tengan una ID única por nodo , por lo que si los nombres pueden entrar en conflicto (nuestro caso), se deben proporcionar ID únicas.
Veamos cómo podemos implementar el autoregistro en la aplicación .NET. Primero, debemos leer la configuración requerida para el descubrimiento de servicios a partir de las variables de entorno que se pasaron a través del archivo 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 ;
}
}
Después de leer la configuración requerida para llegar al servicio de descubrimiento de servicios, podemos usarlo para registrar nuestro servicio. El siguiente código se implementa como una tarea en segundo plano (servicio alojado), que registra el servicio en Consul anulando la información anterior sobre el servicio, si existiera. Si el servicio se cierra, se cancela automáticamente su registro en el registro de 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 ) } " ) ;
}
}
}
Una vez que hayamos registrado nuestros servicios en el servicio de descubrimiento de servicios, podemos comenzar a implementar la API Gateway.
Ocelot requiere que proporcione un archivo de configuración que contenga una lista de Rutas (configuración utilizada para mapear la solicitud ascendente) y una Configuración Global (otra configuración como QoS, limitación de velocidad, etc.). En el archivo ocelot.json a continuación, puede ver cómo reenviamos las solicitudes HTTP. Tenemos que especificar qué tipo de balanceador de carga usaremos; en nuestro caso, este es un "RoundRobin" que recorre los servicios disponibles y envía solicitudes a los servicios disponibles. Es importante configurar Consul como un servicio de descubrimiento de servicios en GlobalConfiguration para 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
}
}
}
Aquí hay algunas explicaciones necesarias para la configuración de ServiceDiscoveryProvider en la sección GlobalConfiguration :
Una vez que hayamos definido nuestra configuración, podemos comenzar a implementar API Gateway. A continuación podemos ver la implementación del servicio Ocelot API Gateway, que utiliza nuestro archivo de configuración ocelot.json y Consul como registro de servicio.
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 ( ) ;
Como se mencionó anteriormente, incluiremos en contenedores todos los servicios con Docker, incluido Consul , utilizando distribuciones ligeras de GNU/Linux para contenedores.
El archivo docker-compose.yml con la configuración para todos los contenedores se ve así:
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
Tenga en cuenta que nuestros servicios no contienen ningún archivo de configuración, para ello usaremos el archivo 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
Para ejecutar el archivo de redacción, abra Powershell y navegue hasta el archivo de redacción en la carpeta raíz. Luego ejecute el siguiente comando: docker-compose up -d --build --remove-orphans que inicia y ejecuta todos los servicios. El parámetro -d ejecuta el comando separado. Esto significa que los contenedores se ejecutan en segundo plano y no bloquean la ventana de Powershell. Para verificar todos los contenedores en ejecución, use el comando docker ps .
The Consul ofrece una agradable interfaz de usuario web lista para usar. Puede acceder a él en el puerto 8500 : http://localhost:8500. Veamos algunas de las pantallas.
La página de inicio de los servicios de Consul UI con toda la información relevante relacionada con un agente de Consul y la verificación del servicio web.
Hagamos varias llamadas a través de API Gateway: http://localhost:9500/api/values. El equilibrador de carga recorrerá los servicios disponibles y enviará solicitudes y devolverá respuestas:
Los sistemas de microservicios no son fáciles de construir y mantener. Pero este tutorial mostró lo fácil que es desarrollar e implementar una aplicación con arquitectura de microservicio. HashiCorp Consul cuenta con soporte de primera clase para descubrimiento de servicios, verificación de estado, almacenamiento de valores clave y centros de datos múltiples. Ocelot como puerta de enlace se comunica exitosamente con el registro de servicios de Consul y recupera registros de servicios, el balanceador de carga recorre los servicios disponibles y envía solicitudes. El uso de ambos hace la vida mucho más fácil para los desarrolladores que enfrentan tales desafíos. ¿Estás de acuerdo?
¡Disfrutar!
Con licencia del MIT. Contáctame en LinkedIn.