asp.net core microservices 架構之eureka服務發現
一 簡介
微服務將需多的功能拆分為許多的輕量級的子應用,這些子應用相互排程。好處就是輕量級,完全符合了敏捷開發的精神。我們知道ut(單元測試),不僅僅提高我們的程式的健壯性,而且可以強制將類和方法的設計儘量的單一化。那麼微服務也是這樣,敏捷對於軟體工程的意義就是快速開發,驗證市場需求,然後快速改進,從而適應市場節奏。什麼東西要快速,就必須輕量級。大家知道一個應用的複雜程度,完全是和這個專案的功能和程式碼數量掛鉤的,這是軟體自誕生就存在的問題,一個設計不好的軟體,最後會讓這個軟體更新和改進變的非常複雜,直至沒有人敢去碰這個應用,我一直認為這個程式碼量和複雜程度掛鉤,困難程度是以指數的量增加的,而不僅僅是線性增加。這就需要一個好的設計去解決問題,一個微服務儘量的單一,與其他子應用幾乎沒有程式碼上的關聯,所以可以快速出原型,快速開發,驗證市場,也可以快速砍掉一個專案,而不影響其他的應用。
當然,因為微服務有他的侷限性,所以也有它的壞處,比如一致性會打折扣,對於一致性的問題,我前幾篇文章已經做過探討。還有就是雖然軟體開發部署工作解脫了,但是您想,原來一個應用,現在三四個應用進行協調和通訊,對於這個分散式架構的要求就會高,如何把他們打散又要弱關聯在一起,那麼eureka誕生了。asp.net core支援一個底層的庫,Microsoft.Extensions.Http.Polly,這個庫是表達策略,例如以流暢且執行緒安全的方式處理重試、斷路器、超時、Bulkhead 隔離和回退,就是防止雪崩,熔斷降級等特性。感興趣的可以看asp.net core 官方文件: https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-2.2#use-polly-based-handlers 。多說一句,asp.net core也有後臺服務job的底層api,有興趣的可以試著做出一套asp.net core的job框架。甚至服務發現,配合zookeeper,也可以開發出一套屬於asp.net core的框架。
言歸正傳,這裡還需要注意的是一點是,一個微服務是可以和以前的比如面向傳統服務型別的應用共存的,如果公司內部可以提出一個應用作為微服務來設計,那麼我恭喜你,從泥潭中跨出了一步,如果你們公司全部用微服務設計了,那麼真的恭喜,完全跳出了泥潭。當然一些重量級的應用,因為必須使用分散式事務等等的特殊要求,還是可以存在,微服務還是需要根據實際情況來實施的。
二 eureka整合
說到eureka前,我們需要明白一個術語:backing service。我把他翻譯為協助服務,或者幫助服務。主要意思就是對資源具有管理,丟失處理,連線和配置等功能的一個程式。那微服務資源自然就是我們各種各樣的服務,如圖所示:
當然除過eureka,還有etcd,Consul,Marathon,ZooKeeper可以供大家挑選。
使用docker執行eureka伺服器端:
docker run -p 8081:8080 -d --name eureka \ -d netflixoss/eureka:1.3.1
執行成功後,如下圖所示:
讓我們用我們的Walt.Framework.OrderService專案定義一個api介面和將配置eureka客戶端,然後至今執行,就會直接將例項新增進eureka服務端:
設計api,我們直接看生成的api描述,使用swagger生成的api描述:
再安裝eureka客戶端,首先查詢都有那些包:
nuget.exe list eureka
顯示結果,紅框中的包就是文件中要求針對netcore的包:
客戶端需要引入包:
dotnet add Walt.TestMcroServices.Webapi.csproj package Steeltoe.Discovery.ClientCore
然後再註冊服務,這個大家都應該熟悉的不能再熟悉了,因為我們開發自定義的服務已經有三個了:
public class Startup { ... public IConfiguration Configuration { get; private set; } public Startup(...) { ... } public void ConfigureServices(IServiceCollection services) { // Add Steeltoe Discovery Client service services.AddDiscoveryClient(Configuration); //新增服務 // Add framework services. services.AddMvc(); ... } public void Configure(IApplicationBuilder app, ...) { ... app.UseStaticFiles(); app.UseMvc(); // Use the Steeltoe Discovery Client service app.UseDiscoveryClient(); //這裡是將服務應用到http通道,我們知道這裡處理的都是asp.net core 相關請求的http通道的一些事情,類似與asp.net的那幾個application事件。 }
這裡多說一句關於owin的事情,上面程式碼的 中介軟體技術和 OWIN完全不同,OWIN:開放 Web 介面,它定義了在管道中使用中介軟體來處理請求和相關響應的標準方法。我們看看asp.net core 的管道都幹些什麼? 如圖所示:
官方說辭:
所以無論各個環節都是處理http上下文中的內容,web驅動監聽到http請求和傳送請求的內容,就是request,和response,在這兩個之間就是我們的通道,不管是處理生成http內容時,還是身份驗證,session處理,都是這個通道之間的某個環節。
那owin作用是什麼尼?就是定義這些中間環節的一些標準,所以任何web框架,只要實現了owin介面標準,兩個不同web框架,就都可以相互相容。
言歸正準,繼續eureka的客戶配置, 配置檔案:
{ "Logging": { "LogLevel": { "Default": "trace", "System": "trace", "Microsoft": "trace", "Steeltoe": "Debug" }, "KafkaLog":{ "Prix":"這是我的自定義日誌提供程式", "LogStoreTopic":"mylog-orderservice" } }, "KafkaService":{ "Properties":{ "bootstrap.servers":"192.168.249.106:9092", "group.id":"group1" } }, "zookeeperService":{ "Connectstring":"192.168.249.106:2181", "SessionTimeout":12000000 }, "spring": { "application": { "name": "orderservice" } }, "eureka": { "client": { "serviceUrl": "http://192.168.249.105:8080/eureka/v2/", "shouldFetchRegistry": false }, "instance": { "ipAddress":"192.168.249.102", "preferIpAddress":true, "port": 802, "instanceId":"orderserviceinstance" } } }
紅顏色的就是eureka需要使用的配直節。
所以在startup中的配置就需要把整個configuration傳進去,讓他自己查詢,而不用精確找到configuration配直節後,再傳入服務構建,這個非常簡單,總共就兩行程式碼。
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Steeltoe.Discovery.Client; using Swashbuckle.AspNetCore.Swagger; namespace Walt.TestMicroservices.OrderService { public class Startup { public Startup(IConfiguration configuration ,IHostingEnvironment hostingEn , ILoggerFactory loggerFac ) { Configuration = configuration; HostingEn = hostingEn; LoggerFac = loggerFac; } public IConfiguration Configuration { get; } public IHostingEnvironment HostingEn { get; } public ILoggerFactory LoggerFac { get; set; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc(mvcOptions=>{ }).SetCompatibilityVersion(CompatibilityVersion.Version_2_1); //services.AddAuthorization(); //services.AddAuthentication("Bearer") //.AddIdentityServerAuthentication(options => //{ //options.Authority = "http://localhost:64433"; //options.RequireHttpsMetadata = false; //options.ApiName = "api1"; //}); // Add Steeltoe Discovery Client service services.AddDiscoveryClient(Configuration); // Register the Swagger generator, defining 1 or more Swagger documents services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" }); }); var log = LoggerFac.CreateLogger<Startup>(); log.LogDebug("服務配置完成"); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { var log = LoggerFac.CreateLogger<Startup>(); log.LogInformation("infomation"); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseDiscoveryClient(); app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); c.RoutePrefix = string.Empty; }); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "api/{controller=Home}/{action=Index}/{id?}"); }); app.UseAuthentication(); log.LogDebug("通道配置完畢"); } } }
執行:
我們看到註冊成功了,那看呼叫方:Walt.TestMicroServices.Webapi
startup類中註冊:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Walt.Framework.Service; using Walt.Framework.Configuration; using Walt.Framework.Service.Kafka; using Steeltoe.Discovery.Client; using System; using Steeltoe.Common.Http.Discovery; namespace Walt.TestMicroServoces.Webapi { public class Startup { public Startup(IConfiguration configuration ,IHostingEnvironment hostingEn , ILoggerFactory loggerFac ) { Configuration = configuration; HostingEn = hostingEn; LoggerFac = loggerFac; } public IConfiguration Configuration { get; } public IHostingEnvironment HostingEn { get; } public ILoggerFactory LoggerFac { get; set; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IKafkaService, KafkaService>(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddAuthorization(); services.AddAuthentication("Bearer") .AddIdentityServerAuthentication(options => { options.Authority = "http://localhost:64433"; options.RequireHttpsMetadata = false; options.ApiName = "api1"; }); services.AddKafka(KafkaBuilder => { var kafkaConfig = Configuration.GetSection("KafkaService"); KafkaBuilder.AddConfiguration(kafkaConfig); }); //services.AddSingleton<IOrderService, OrderService>(); // Add Steeltoe Discovery Client service services.AddDiscoveryClient(Configuration); // Add Steeltoe handler to container services.AddTransient<DiscoveryHttpMessageHandler>(); // Configure a HttpClient services.AddHttpClient<OrderService>(c => { c.BaseAddress = new Uri("http://orderservice"); }) .AddHttpMessageHandler<DiscoveryHttpMessageHandler>() //這個就是攔截http請求,然後讓發現服務處理這個http請求,而不適用預設的http請求處理程式 .AddTypedClient<IOrderService, OrderService>(); //將上面DiscoveryHttpMessageHandler這個處理程式生成httpclient然後注入給這個服務類,然後再將這個服務類加入DI var log = LoggerFac.CreateLogger<Startup>(); log.LogDebug("服務配置完成"); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { var log= LoggerFac.CreateLogger<Startup>(); log.LogInformation("infomation"); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseMvc(); app.UseDiscoveryClient(); app.UseAuthentication(); log.LogDebug("通道配置完畢"); } } }
這裡看配置檔案:
{ "Logging": { "LogLevel": { "Default": "Debug", "System": "Debug", "Microsoft": "Debug" }, "KafkaLog":{ "Prix":"這是我的自定義日誌提供程式", "LogStoreTopic":"mylog-webapi" } }, "KafkaService":{ "Properties":{ "bootstrap.servers":"192.168.249.106:9092", "group.id":"group1" } }, "zookeeperService":{ "Connectstring":"192.168.249.106:2181", "SessionTimeout":12000000 }, "spring": { "application": { "name": "webapi" } }, "eureka": { "client": { "serviceUrl": "http://192.168.249.105:8080/eureka/v2/", "shouldRegisterWithEureka": true }, "instance": { "ipAddress":"192.168.249.102", "preferIpAddress":true, "port": 801, "instanceId":"webapiinstance" } } }
看呼叫類,很簡單,因為asp.net core和eueka的整合已經在上一步驟中的startup中處理,這裡直接使用:
using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Steeltoe.Common.Discovery; namespace Walt.TestMicroServoces.Webapi { public class OrderService:IOrderService { private IConfiguration _config; private HttpClient _httpClient; public OrderService(IConfiguration config ,HttpClient httpClient) { _config=config; _httpClient = httpClient; } public Task<string> GetOrder() { var task=_httpClient.GetStringAsync("Order"); return task; } } }
呼叫:
首先看日誌:
只要獲取到這個例項,就沒什麼問題了。
頁面結果:
注意:我在用nuget包的時候,總是獲取不到例項,不知道什麼,後來沒辦法就用原始碼:
直接就可以了,就算不可以,調式原始碼,很快也就解決了,看來坑都怕原始碼,尤其是這種還沒大規模使用和討論的小專案,所以我這次的程式碼就把這個引用保留,大家下載後需要自己下載 Descover原始碼,然後引用這個工程就ok了。
Discovery github地址:https://github.com/SteeltoeOSS/Discovery
三 整合polly
polly 提供了微服務之間呼叫的容錯功能,無論是慢超市,還是失敗,對於微服務之間提供了很多策略,如下的策略:
Policy | Premise | Aka | How does the policy mitigate? |
---|---|---|---|
Retry (policy family) ( quickstart ; deep ) |
Many faults are transient and may self-correct after a short delay. 重試 |
"Maybe it's just a blip" | Allows configuring automatic retries. |
Circuit-breaker (policy family) ( quickstart ; deep ) |
When a system is seriously struggling, failing fast is better than making users/callers wait. 失敗而不是呼叫等待 Protecting a faulting system from overload can help it recover. |
"Stop doing it if it hurts" "Give that system a break" |
Breaks the circuit (blocks executions) for a period, when faults exceed some pre-configured threshold. |
Timeout ( quickstart ; deep ) |
Beyond a certain wait, a success result is unlikely. 超時 |
"Don't wait forever" | Guarantees the caller won't have to wait beyond the timeout. |
Bulkhead Isolation ( quickstart ; deep ) |
When a process faults, multiple failing calls backing up can easily swamp resource (eg threads/CPU) in a host. A faulting downstream system can also cause 'backed-up' failing calls upstream. Both risk a faulting process bringing down a wider system. 一個程序失敗或多個失敗的呼叫可以很容易的淹沒主機的資源。 所以需要隔離牆。 |
"One fault shouldn't sink the whole ship" | Constrains the governed actions to a fixed-size resource pool, isolating their potential to affect others. |
Cache ( quickstart ; deep ) |
Some proportion of requests may be similar. | "You've asked that one before" | Provides a response from cache if known. Stores responses automatically in cache, when first retrieved. |
Fallback ( quickstart ; deep ) |
Things will still fail - plan what you will do when that happens. 某些將仍然失敗,在它發生的時候,你要有計劃準備做一些事。 |
"Degrade gracefully" | Defines an alternative value to be returned (or action to be executed) on failure. |
PolicyWrap ( quickstart ; deep ) |
Different faults require different strategies; resilience means using a combination. 不同的失敗請求有不同的情況,所以可以對上面的策略有不同的結合處理。 |
"Defence in depth" | Allows any of the above policies to be combined flexibly. |
每個策略大家有興趣試著做例子。
程式碼中開發非常簡單,提供了三種配置策略的方法,下面是其中之一:
var timeout = Policy.TimeoutAsync<HttpResponseMessage>( TimeSpan.FromSeconds(10)); var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>( TimeSpan.FromSeconds(30)); services.AddHttpClient("conditionalpolicy") // Run some code to select a policy based on the request .AddPolicyHandler(request => request.Method == HttpMethod.Get ? timeout : longTimeout);
這個在官方文件:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-2.2#use-polly-based-handlers
講解的非常詳細。
還有就是polly的github地址:https://github.com/App-vNext/Polly
都有非常詳細的介紹。