在设计基于微服务架构的解决方案时,我们经常遇到这样的需求:快速轻松地管理整个系统,尽可能高的自动化,而无需对各个组件进行必要的调整。
这是一个真正的挑战,这就是为什么我决定准备一个教程,演示如何以最简单的方式建立微服务架构,该架构可以快速、轻松地扩展并适应客户的需求。
我不想干扰各个服务的代码和设置,而是仅通过编排 Docker 中的容器来控制系统。
结果是一个简单的微服务架构,只需对容器设置进行一些更改即可轻松扩展,其他一切由 Ocelot 作为网关/负载均衡器和 Consul 作为服务发现代理提供。
这样的架构允许我们重新部署单个服务,而无需协调其他服务内的部署。重新部署的服务会在服务发现时自动注册,并立即通过网关可用。可以想象这对每个开发团队来说是多大的推动!
当然,使用单个网关服务会成为我们架构的单点故障,因此我们需要部署至少两个实例才能获得高可用性。但我会把这个问题留给你去解决。
在之前的演示中,我展示了如何将 Ocelot 实现为服务网关和负载均衡器,并与 Eureka 一起实现服务发现。相反,Eureka 这个演示使用 Consul 进行服务发现。
Consul 是一个服务网格解决方案,提供具有服务发现、配置和分段功能的全功能控制平面。这些功能中的每一个都可以根据需要单独使用,也可以一起使用来构建完整的服务网格。 Consul 需要数据平面并支持代理和本机集成模型。 Consul 附带一个简单的内置代理,以便一切开箱即用,但也支持第 3 方代理集成,例如 Envoy。
Consul 的主要特点是:
服务发现:Consul的客户端可以注册一个服务,例如api或mysql,其他客户端可以使用Consul来发现给定服务的提供者。使用 DNS 或 HTTP,应用程序可以轻松找到它们所依赖的服务。
健康检查:Consul 客户端可以提供任意数量的健康检查,要么与给定服务相关(“Web 服务器是否返回 200 OK”),要么与本地节点相关(“内存利用率是否低于 90%”)。操作员可以使用此信息来监视集群的运行状况,服务发现组件可以使用它来将流量路由到不健康的主机。
KV 存储:应用程序可以将 Consul 的分层键/值存储用于多种目的,包括动态配置、功能标记、协调、领导者选举等。简单的 HTTP API 使其易于使用。
安全服务通信:Consul 可以为服务生成和分发 TLS 证书,以建立相互的 TLS 连接。意图可用于定义允许哪些服务进行通信。服务分段可以通过可以实时更改的意图轻松管理,而不是使用复杂的网络拓扑和静态防火墙规则。
多数据中心:Consul 支持开箱即用的多个数据中心。这意味着 Consul 的用户不必担心构建额外的抽象层以扩展到多个区域。
Consul 的设计宗旨是对 DevOps 社区和应用程序开发人员都友好,使其非常适合现代、弹性的基础设施。
来源:领事介绍
本教程的一个关键部分是使用 Consul 动态发现服务端点。一旦服务在 Consul 中注册,就可以使用典型的 DNS 或自定义 API 来发现它。
Consul 对这些服务实例提供健康检查。如果服务实例或服务本身不健康或未通过健康检查,则注册表将了解这一情况并避免返回服务的地址。在这种情况下,负载平衡器要做的工作由注册表处理。
因为我们使用同一服务的多个实例,Consul 会随机将流量发送到不同的实例。因此,它平衡了服务实例之间的负载。
Consul 可以处理跨多个服务实例的故障检测和负载分配的挑战,而无需部署集中式负载均衡器。
它自动管理注册表,当服务的任何新实例注册并可用于接收流量时,注册表就会更新。这有助于我们轻松扩展服务。
在详细了解如何向 Consul 实现自注册之前,让我们先了解一下自注册的服务发现是如何真正工作的。
第一步,服务实例通过提供其名称、ID 和地址将自身注册到服务发现服务。此网关能够通过其名称/ID 查询 Consul 服务发现来获取该服务的地址。
这里需要注意的关键是,服务实例使用唯一的服务 ID进行注册,以便消除在同一 Consul 服务代理上运行的服务实例之间的歧义。要求所有服务的每个节点都有唯一的 ID ,因此如果名称可能发生冲突(我们的情况),则必须提供唯一的 ID。
让我们看看如何在 .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要求您提供一个配置文件,其中包含路由列表(用于映射上游请求的配置)和全局配置(其他配置,如 QoS、速率限制等)。在下面的 ocelot.json 文件中,您可以看到我们如何转发 HTTP 请求。我们必须指定要使用哪种类型的负载均衡器,在我们的例子中,这是一个“RoundRobin” ,它循环可用服务并向可用服务发送请求。在ServiceDiscoveryProvider的GlobalConfiguration中将 Consul 设置为服务发现服务非常重要。
{
"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
}
}
}
以下是GlobalConfiguration部分中ServiceDiscoveryProvider设置的一些必要说明:
定义好配置后,我们就可以开始实现 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 提供了一个开箱即用的漂亮的 Web 用户界面。您可以通过端口8500访问它:http://localhost:8500。让我们看一些屏幕。
Consul UI 服务的主页,包含与 Consul 代理和 Web 服务检查相关的所有相关信息。
让我们通过 API Gateway 进行几次调用:http://localhost:9500/api/values。负载均衡器将循环访问可用服务并发送请求并返回响应:
微服务系统不容易构建和维护。但本教程展示了使用微服务架构开发和部署应用程序是多么容易。 HashiCorp Consul 为服务发现、健康检查、键值存储和多数据中心提供一流的支持。 Ocelot 作为网关成功与 Consul 服务注册中心通信并检索服务注册,负载均衡器循环访问可用服务并发送请求。使用两者可以让面临此类挑战的开发人员的生活变得更加轻松。你同意?
享受!
获得麻省理工学院许可。在 LinkedIn 上联系我。