在設計基於微服務架構的解決方案時,我們經常遇到這樣的需求:快速輕鬆地管理整個系統,盡可能高的自動化,而無需對各個元件進行必要的調整。
這是一個真正的挑戰,這就是為什麼我決定準備一個教程,演示如何以最簡單的方式建立微服務架構,該架構可以快速、輕鬆地擴展並適應客戶的需求。
我不想幹擾各個服務的程式碼和設置,而是僅透過編排 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 提供了一個很好的開箱即用的網路使用者介面。您可以透過連接埠8500存取它:http://localhost:8500。讓我們來看看一些螢幕。
Consul UI 服務的主頁,包含與 Consul 代理程式和 Web 服務檢查相關的所有相關資訊。
讓我們透過 API Gateway 進行幾次呼叫:http://localhost:9500/api/values。負載平衡器將循環存取可用服務並發送請求並回傳回應:
微服務系統不容易建置和維護。但本教學展示了使用微服務架構開發和部署應用程式是多麼容易。 HashiCorp Consul 為服務發現、健康檢查、鍵值儲存和多重資料中心提供一流的支援。 Ocelot 作為網關成功與 Consul 服務註冊中心通訊並檢索服務註冊,負載平衡器循環存取可用服務並發送請求。使用兩者可以讓面臨此類挑戰的開發人員的生活變得更加輕鬆。你同意?
享受!
獲得麻省理工學院許可。在 LinkedIn 上聯絡我。