總體介紹
隨着業務需求的快速發展變化,需求不斷增長,迫切需要一種更加快速高效的軟件交付方式。微服務可以彌補單體應用不足,是一種更加快速高效軟件架構風格。單體應用被分解成多個更小的服務,每個服務有自己的獨立模塊,單獨部署,然後共同組成一個應用程序。把範圍限定到單個獨立業務模塊功能。分佈式部署在各臺服務器上。本篇我們將介紹如何使用.NET Core打造自己的微服務架構。
注意:微服務架構不是萬能藥,本篇僅供參考和探討。對於大部分小項目來說,請不要爲了微服務而微服務。畢竟技術不是萬能的,技術是爲業務服務的。
微服務架構的好處
-
單個服務很容易開發、理解和維護。
-
每個服務都可以有專門開發團隊來開發。
-
每個微服務獨立的部署。
-
每個服務獨立擴展。
微服務架構的不足(這個時候就需要用到服務發現)
-
微服務應用是分佈式系統,由此會帶來固有的複雜性。
-
服務地址目錄,服務健康度,部署困難,服務依賴問題,數據庫分區問題。
傳統模式
Ocelot(網關)模式
集成IdentityService(認證)
集成consul(服務發現)
搭建一個簡單的微服務架構
Ocelot
Ocelot 是一個僅適用於 .Net Core 的網關組件。Ocelot
中間件使用非常簡單,難的點在於如何去配置。它的功能包括了:路由、請求聚合、服務發現、認證、鑑權、限流熔斷、並內置了負載均衡器等的集成,而這些功能都是通過配置實現。
Ocelot的開源地址:https://github.com/ThreeMammals/Ocelot
Ocelot官網地址:https://ocelot.readthedocs.io/en/latest/index.html
基本集成
添加Ocelot
新建一個 .Net core 2.2 web 項目(ApiGateway),添加以下Nuget包:
-
Ocelot
-
Ocelot.Administration Ocelot支持在運行時通過經過身份驗證的HTTP
API更改配置。這可以通過兩種方式進行身份驗證:使用Ocelot的內部IdentityServer(僅用於驗證對管理API的請求)或將管理API身份驗證掛鉤到您自己的IdentityServer中。 -
Ocelot.Cache.CacheManager CacheManager.Net擴展包
-
Ocelot.Provider.Polly Polly.NET擴展包
在項目根目錄添加ocelot.json,名字可以自取。
前面說了,所有功能都是通過配置實現的,所以配置也相對複雜。配置有兩個部分。一組ReRoutes和一個GlobalConfiguration。ReRoutes是告訴Ocelot如何處理上游請求的對象。GlobalConfiguration顧名思義是全局配置,具體配置請查看官網。下面列舉簡單配置
{
"GlobalConfiguration": {
//外部訪問路徑
"BaseUrl": "http://localhost:13000",
//限速配置
"RateLimitOptions": {
//白名單
"ClientWhitelist": [],
"EnableRateLimiting": true,
//限制時間段,例如1s,5m,1h,1d
"Period": "1s",
//重試等待的時間間隔(秒)
"PeriodTimespan": 1,
//限制
"Limit": 1,
//自定義消息
"QuotaExceededMessage": "單位時間內請求次數超過限制!",
"HttpStatusCode": 999
},
//熔斷配置
"QoSOptions": {
"ExceptionsAllowedBeforeBreaking": 3,
"DurationOfBreak": 5,
//超時值(毫秒)
"TimeoutValue": 5000
}
},
"ReRoutes": []
}
配置文件初始化好之後,需要在Program.cs
文件中加載JSON配置,Ocelot支持根據環境變量使用配置文件。
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => new WebHostBuilder()
.UseKestrel((context, opt) =>
{
opt.AddServerHeader = false;
////從配置文件讀取配置
//opt.Configure(context.Configuration.GetSection("Kestrel"));
})
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
//根據環境變量加載不同的JSON配置
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
.AddJsonFile("ocelot.json")
.AddEnvironmentVariables(); //從環境變量添加配置
})
.UseIISIntegration()
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
//添加控制檯日誌,Docker環境下請務必啓用
logging.AddConsole();
//添加調試日誌
logging.AddDebug();
})
.UseStartup<Startup>();
}
然後在Startup.cs
文件ConfigureServices方法中註冊服務時使用AddOcelot(),Configure
方法中使用app.UseOcelot().Wait(); 這樣網關的配置就完成了。
services.AddOcelot(Configuration)
app.UseOcelot().Wait();
添加測試API項目
新建兩個 .Net core 2.2 web項目(vs 自建的那種就OK),並使用Swagger來做接口說明。
Nuget 添加 Swashbuckle.AspNetCore 和
Microsoft.Extensions.PlatformAbstractions 實現Swagger ui,代碼如下
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("SwaggerAPI1", new Info { Title = "API1", Version = "v1" });
var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var xmlPath = Path.Combine(basePath, "Services.Test1.xml");
options.IncludeXmlComments(xmlPath);
});
//服務註冊
//services.Configure<ServiceRegistrationOptions>
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger(c => { c.RouteTemplate = "{documentName}/swagger.json"; });
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/SwaggerAPI1/swagger.json", "API1");
});
app.UseMvc();
}
項目.csproj文件中設置XML文檔輸出路徑
Services.Test1 和 Services.Test2
一樣的配置,略過。編譯啓動,頁面如下,接口配置完成。
配置項目的上游請求對象(ocelot.json)
"ReRoutes": [
//API1項目配置
{
"UpstreamPathTemplate": "/gateway/1/{url}",
"UpstreamHttpMethod": [ "Get", "Post", "Delete", "Put" ],
"DownstreamPathTemplate": "/api1/{url}",
"DownstreamScheme": "http",
"ServiceName": "API1",
"UseServiceDiscovery": true,
"LoadBalancer": "RoundRobin",
"DownstreamHostAndPorts": [
{
"Host": "119.29.50.115",
"Port": 80
},
{
"Host": "localhost",
"Port": 13001
}
],
"QoSOptions": {
"ExceptionsAllowedBeforeBreaking": 3,
"DurationOfBreak": 10,
"TimeoutValue": 5000
}
//"AuthenticationOptions": {
// "AuthenticationProviderKey": "Bearer",
// "AllowedScopes": [
// ]
//}
},
//API2項目配置
{
"UpstreamPathTemplate": "/gateway/2/{url}",
"UpstreamHttpMethod": [ "Get", "Post", "Delete", "Put" ],
"DownstreamPathTemplate": "/api2/{url}",
"DownstreamScheme": "http",
"ServiceName": "API2",
"UseServiceDiscovery": true,
"LoadBalancer": "RoundRobin",
"DownstreamHostAndPorts": [
{
"Host": "111.230.160.62",
"Port": 80
},
{
"Host": "localhost",
"Port": 13002
}
],
"QoSOptions": {
"ExceptionsAllowedBeforeBreaking": 3,
"DurationOfBreak": 10,
"TimeoutValue": 5000
}
//"AuthenticationOptions": {
// "AuthenticationProviderKey": "Bearer",
// "AllowedScopes": [
// ]
//}
},
]
ReRoutes API對象模板配置節點解釋如下:
UpstreamPathTemplate | 上游路徑模板 |
---|---|
UpstreamHttpMethod | 上游HTTP請求方法 |
DownstreamPathTemplate | 下游路徑模板 |
DownstreamScheme | 下游協議Https/Http |
DownstreamHostAndPorts | 下游主機和端口號,允許配置多個 |
UseServiceDiscovery | 是否使用服務發現(True/False) |
ServiceName | 服務名稱(結合服務發現使用) |
LoadBalancer | 指定一個負載均衡算法: RoundRobin:輪詢 LeastConnection:最少連接數 NoLoadBalancer:不適用負載均衡 |
LoadBalancerOptions | 負載均衡器配置 |
QoSOptions | 熔斷配置,在請求向下遊服務時使用斷路 |
AuthenticationOptions | 權限配置 |
啓動結果
啓動web
項目,web頁面報錯,但無妨,使用PostMan請求網關接口訪問api1/TestOnes成功。
聚合API文檔(SwaggerUI)
前面配置了網關接口上游,但是頁面Swagger沒有顯示,這節主要是整合SwaggerUI。
首先需要配置ApiGateway項目的Swagger,在配置文件配置上面兩個接口的SwaggerNames,代碼中遍歷添加到網關項目的SwaggerUI中,代碼如下
ConfigureServices
services.AddSwaggerGen(options =>
{
options.SwaggerDoc(Configuration["Swagger:Name"], new Info { Title = Configuration["Swagger:Title"], Version = Configuration["Swagger:Version"] });
});
Configure
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
var apis = Configuration["Apis:SwaggerNames"].Split(";").ToList();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc()
.UseSwagger()
.UseSwaggerUI(options =>
{
apis.ToList().ForEach(key =>
{
options.SwaggerEndpoint($"/{key}/swagger.json", key);
});
options.DocumentTitle = "網關";
});
app.UseOcelot().Wait();
}
appsettings.json
"Swagger": {
"Name": "ApiGateway",
"Title": "網關服務",
"Version": "v1"
},
"Apis": {
"SwaggerNames": "SwaggerAPI1;SwaggerAPI2"
}
PS:SwaggerAPI1、SwaggerAPI2是前面兩個接口的SwaggerName,這裏需要對應上。
配置Swagger的上游請求對象(ocelot.json)
//swagger API1配置
{
"DownstreamPathTemplate": "/SwaggerAPI1/swagger.json",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/SwaggerAPI1/swagger.json",
"UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ],
"DownstreamHostAndPorts": [
{
"Host": "119.29.50.115",
"Port": 80
},
{
"Host": "localhost",
"Port": 13001
}
]
},
//swagger API2配置
{
"DownstreamPathTemplate": "/SwaggerAPI2/swagger.json",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/SwaggerAPI2/swagger.json",
"UpstreamHttpMethod": [ "GET", "POST", "DELETE", "PUT" ],
"DownstreamHostAndPorts": [
{
"Host": "111.230.160.62",
"Port": 80
},
{
"Host": "localhost",
"Port": 13002
}
]
}
啓動結果
使用SwaggerUI整合了API1和API2的接口文檔。
IdentityServer 集成
官網文檔地址:http://docs.identityserver.io/en/latest/index.html
IdentityServer4是一個基於OpenID Connect和 OAuth 2.0的針對 ASP .NET Core 2.0的框架。
IdentityServer是將規範兼容的OpenID Connect和OAuth 2.0終結點添加到任意ASP .NET
Core應用程序的中間件。你構建包含登錄和註銷頁面的應用程序,IdentityServer中間件會向其添加必要的協議頭,以便客戶端應用程序可以使用這些標準協議與其對話。
添加授權服務項目
新建 .Net core 2.2 web項目,添加以下Nuget包:
-
IdentityServer4.AspNetIdentity
-
IdentityServer4.EntityFramework 使用數據存儲機制
配置appsetting.json
配置測試環境下的客服端信息和Identity API
資源配置,具體配置需要按照自己的邏輯定義,這裏只是爲了結合我下面的IdentityServerConfig文件所定義,代碼如下,
爲了結合我下面的IdentityServerConfig文件所定義,代碼如下,
"IdentityServer": {
"ApiName": "default-api",
"ApiSecret": "secret",
"Clients": [
{
"ClientId": "client",
"AllowedGrantTypes": [
"password"
],
"ClientSecrets": [
{
"Value": "def2edf7-5d42-4edc-a84a-30136c340e13"
}
],
"AllowedScopes": [
"default-api"
]
},
{
"ClientId": "demo",
"ClientName": "MVC Client Demo",
"AllowedGrantTypes": [
"hybrid",
"client_credentials"
],
"RequireConsent": "true",
"ClientSecrets": [
{
"Value": "def2edf7-5d42-4edc-a84a-30136c340e13"
}
],
"RedirectUris": [
"http://openidclientdemo.com:8001/signin-oidc"
],
"PostLogoutRedirectUris": [
"http://openidclientdemo.com:8001/signout-callback-oidc"
],
"AllowedScopes": [
"openid",
"profile",
"default-api"
],
"AllowOfflineAccess": "true"
}
]
}
添加IdentityServerConfig類
IdentityServerConfig 類分爲三個方法:
定義API資源:
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("default-api", "Default (all) API")
{
Description = "AllFunctionalityYouHaveInTheApplication",
ApiSecrets= {new Secret("secret") }
}
};
}
定義身份資源:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResources.Phone(),
new IdentityResources.Address()
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
var customProfile = new IdentityResource(
name: "custom.profile",
displayName: "Custom profile",
claimTypes: new[] { "name", "email", "status" });
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
customProfile
};
}
IdentityResource 具體屬性
Enabled
指示此資源是否已啓用且可以請求。默認爲true。
Name
標識資源的唯一名稱。這是客戶端將用於授權請求中的scope參數的值。
DisplayName
顯示名稱。
Description
描述。
Required
默認爲false。(暫未深究理解)
Emphasize
默認爲false。(暫未深究理解)
ShowInDiscoveryDocument
指定此範圍是否顯示在發現文檔中。默認爲true。
UserClaims
應包含在身份令牌中的關聯用戶聲明類型的列表。
定義測試客服端
此處則是通過appsetting.json 文件獲取配置
public static IEnumerable<Client> GetClients(IConfiguration configuration)
{
var clients = new List<Client>();
foreach (var child in configuration.GetSection("IdentityServer:Clients").GetChildren())
{
clients.Add(new Client
{
ClientId = child["ClientId"],
ClientName = child["ClientName"],
AllowedGrantTypes = child.GetSection("AllowedGrantTypes").GetChildren().Select(c => c.Value).ToArray(),
RequireConsent = bool.Parse(child["RequireConsent"] ?? "false"),
AllowOfflineAccess = bool.Parse(child["AllowOfflineAccess"] ?? "false"),
ClientSecrets = child.GetSection("ClientSecrets").GetChildren().Select(secret => new Secret(secret["Value"].Sha256())).ToArray(),
AllowedScopes = child.GetSection("AllowedScopes").GetChildren().Select(c => c.Value).ToArray(),
RedirectUris = child.GetSection("RedirectUris").GetChildren().Select(c => c.Value).ToArray(),
PostLogoutRedirectUris = child.GetSection("PostLogoutRedirectUris").GetChildren().Select(c => c.Value).ToArray(),
});
}
return clients;
}
配置Startup
ConfigureServices
這裏只是用作測試,所以沒有在數據庫中讀取配置,而是在內存中獲取。相應的數據庫讀取方法也有說明。
public void ConfigureServices(IServiceCollection services)
{
//var connectionString = Configuration.GetConnectionString("Default");
//services.AddDbContext<MagicodesAdminContext>(options => options.UseSqlServer(connectionString));
//services.AddIdentity<AbpUsers, AbpRoles>()
// .AddEntityFrameworkStores<MagicodesAdminContext>()
// .AddDefaultTokenProviders();
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
.AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
.AddInMemoryClients(IdentityServerConfig.GetClients(Configuration))
//.AddAspNetIdentity<AbpUsers>()
//從數據庫讀取配置等內容(clients, resources)
//.AddConfigurationStore(options =>
//{
// options.ConfigureDbContext = b =>
// b.UseSqlServer(connectionString);
//})
// this adds the operational data from DB (codes, tokens, consents)
//.AddOperationalStore(options =>
//{
// options.ConfigureDbContext = b =>
// b.UseSqlServer(connectionString);
// options.PersistedGrants.Name = "AbpPersistedGrants";
// //options.DeviceFlowCodes.Name =
// // this enables automatic token cleanup. this is optional.
// options.EnableTokenCleanup = true;
//});
//.AddAspNetIdentity()
//.AddAbpPersistedGrants<AdminDbContext>()
//.AddAbpIdentityServer<User>();
;
}
Configure
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
}
啓動結果
就這樣可以啓動服務了,瀏覽器啓動會顯示如下頁面,因爲沒有任何頁面啓動,所爲顯示爲404。
但無妨,我們可以使用PostMan 訪問:
http://localhost:13004/.well-known/openid-configuration
你會看到官方所謂的發現文檔。客戶端和API將使用它來下載必要的配置數據。到此爲止IdentityServer服務已經搭建成功!
首次啓動時,IdentityServer將爲您創建一個開發人員簽名密鑰,它是一個名爲的文件。您不必將該文件檢入源代碼管理中,如果該文件不存在,將重新創建該文件。tempkey.rsa
配置ApiGateway網關項目
在前面Ocelot章節中,配置了ocelot.json,這裏繼續修改ocelot.json文件,啓用權限認證
{
"UpstreamPathTemplate": "/gateway/1/{url}",
"UpstreamHttpMethod": [ "Get", "Post", "Delete", "Put" ],
"DownstreamPathTemplate": "/api1/{url}",
"DownstreamScheme": "http",
"ServiceName": "API1",
"UseServiceDiscovery": true,
"LoadBalancer": "RoundRobin",
"DownstreamHostAndPorts": [
{
"Host": "119.29.50.115",
"Port": 80
},
{
"Host": "localhost",
"Port": 13001
}
],
"QoSOptions": {
"ExceptionsAllowedBeforeBreaking": 3,
"DurationOfBreak": 10,
"TimeoutValue": 5000
}
//啓用權限認證
"AuthenticationOptions": {
"AuthenticationProviderKey": "IdentityBearer",
"AllowedScopes": [
]
}
}
然後還需要在ApiGateway項目中修改appsetting.json文件,添加IdentityService服務配置。
"IdentityService": {
"Uri": "http://localhost:13004",//認證服務IP
"DefaultScheme": "IdentityBearer",
"UseHttps": false,
"ApiName": "default-api",
"ApiSecret": "def2edf7-5d42-4edc-a84a-30136c340e13"
}
接下來就是配置 ApiGateway項目 Startup文件了。
需要引入Nuget包:IdentityServer4.AccessTokenValidation
public void ConfigureServices(IServiceCollection services)
{
//Identity Server Bearer Tokens
Action<IdentityServerAuthenticationOptions> isaOpt = option =>
{
option.Authority = Configuration["IdentityService:Uri"];
option.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]);
option.ApiName = Configuration["IdentityService:ApiName"];
option.ApiSecret = Configuration["IdentityService:ApiSecret"];
option.SupportedTokens = SupportedTokens.Both;
};
services.AddAuthentication().AddIdentityServerAuthentication(Configuration["IdentityService:DefaultScheme"], isaOpt);
services
.AddOcelot(Configuration)
//啓用緩存
.AddCacheManager(x => { x.WithDictionaryHandle(); })
.AddPolly()
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); ;
services.AddSwaggerGen(options =>
{
options.SwaggerDoc(Configuration["Swagger:Name"], new Info { Title = Configuration["Swagger:Title"], Version = Configuration["Swagger:Version"] });
});
}
配置完成後啓用Service.Test1、Service.Test2、ApiGateway、IdentityService項目。使用SwaggerUI請求會提示401
Unauthorized,這個時候IdentityService就起到作用了。
使用PostMan去請求IdentityService獲取token
使用token訪問接口,數據返回正常
調用Ocelot管理API
通過IdentityServer 身份驗證來調用Ocelot 管理接口。
首先需要做的是引入相關的NuGet包:Install-Package Ocelot.Administration
修改 ApiGateway項目 Startup文件
添加代碼**.AddAdministration("/administration", isaOpt);**路徑名稱可自取。
public void ConfigureServices(IServiceCollection services)
{
//Identity Server Bearer Tokens
Action<IdentityServerAuthenticationOptions> isaOpt = option =>
{
option.Authority = Configuration["IdentityService:Uri"];
option.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]);
option.ApiName = Configuration["IdentityService:ApiName"];
option.ApiSecret = Configuration["IdentityService:ApiSecret"];
option.SupportedTokens = SupportedTokens.Both;
};
services.AddAuthentication().AddIdentityServerAuthentication(Configuration["IdentityService:DefaultScheme"], isaOpt);
services
.AddOcelot(Configuration)
//啓用緩存
.AddCacheManager(x => { x.WithDictionaryHandle(); })
.AddPolly()
.AddAdministration("/administration", isaOpt);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); ;
services.AddSwaggerGen(options =>
{
options.SwaggerDoc(Configuration["Swagger:Name"], new Info { Title = Configuration["Swagger:Title"], Version = Configuration["Swagger:Version"] });
});
}
API方法
POST {adminPath} / connect / token
獲取token
請求的主體是表單數據,如下所示
client_id 設爲管理員
*client_secret *設置爲設置管理服務時使用的任何內容。
scope 設爲管理員
grant_type 設置爲client_credentials
獲取{adminPath} /configuration
獲得當前的Ocelot配置。
POST {adminPath} / configuration
這會覆蓋現有配置。
請求的主體是JSON,它與我們用於在文件系統上設置Ocelot.json格式相同。
如果要使用此API,則運行Ocelot的進程必須具有寫入ocelot.json或ocelot.{environment}
.json所在磁盤的權限。這是因爲Ocelot會在保存時覆蓋它們。
刪除{adminPath} / outputcache / {region}
清除所有緩存區域
Consul(服務發現)
Consul包含多個組件,但是作爲一個整體,提供服務發現和服務配置的工具。
主要特性:
-
服務發現
組件記錄了分佈式系統中所有服務的信息,其它服務可以據此找到這些服務。 -
健康檢查 Consul 客戶端可用提供任意數量的健康檢查。
-
Key/Value存儲 應用程序可用根據自己的需要使用 Consul 的層級的 Key/Value
存儲。 -
多數據中心
Consul支持開箱即用的多數據中心。這意味着用戶不需要擔心需要建立額外的抽象層讓業務擴展到多個區域。
這裏框架主要介紹服務發現和健康檢查。
本地部署
下載相應版本consul
軟件包,下載地址:https://www.consul.io/downloads.html,以下內容爲windows講解。承接上面的網關項目,整合Consul。
安裝
解壓完成,只有一個consul.exe,別慌,確實就只有一個文件。
管理員運行CMD ,CD 到consul 文件夾,直接運行 consul
命令,出現如下頁面,則配置成功
添加服務配置
添加服務註冊配置文件,在consul.exe同級目錄下添加config
(名字可自取)文件夾,在config
文件夾中創建**service.json(名字可自取)**文件,用來註冊服務和服務檢查配置。如圖所示:
配置service.json,代碼如下:
{
"services": [
{
"id": "API1",//唯一標識
"name": "API1",//服務名稱
"tags": [ "API1" ],//服務標籤
"address": "172.0.0.1",//我隨便配的IP,注意配置服務的真實IP和port
"port": 80
},
{
"id": "API2",
"name": "API2",
"tags": [ "API2" ],
"address": "172.0.0.1",//我隨便配的IP,注意配置服務的真實IP和port
"port": 81
}
]
}
這樣服務註冊配置就OK了,接下來使用配置啓動Consul,下面是幾種形式啓動consul,詳細的命令參數可以移步到官方文檔查看。
-
以開發模式啓動 consul agent -dev -config-dir=./config
-
以服務方式啓動 consul agent -server -bootstrap-expect 2 -data-dir
./tmp/consul -node=n1 -bind=192.168.109.241 -ui-dir ./dist -dc=dc1 -
以客戶端方式啓動 consul agent -data-dir ./tmp/consul -ui-dir ./dist
-bind=192.168.109.204 -dc=dc1
開發模式啓動如下,在輸出窗口中可以看到consul ui HTTP 啓動路徑爲
127.0.0.1:8500 ,註冊了API 和 API2 兩個服務。
瀏覽器訪問 127.0.0.1:8500 ,可以看到Consul UI頁面
添加檢查配置
需要查看服務的運行狀態是否健康,就需要配置檢查。具體檢查配置移步官方文檔。
檢查定義有一下幾種:
腳本檢查:
{
"check": {
"id": "mem-util",
"name": "Memory utilization",
"args": ["/usr/local/bin/check_mem.py", "-limit", "256MB"],
"interval": "10s",
"timeout": "1s"
}
}
HTTP檢查:
{
"check": {
"id": "api",
"name": "HTTP API on port 5000",
"http": "https://localhost:5000/health",
"tls_skip_verify": false,
"method": "POST",
"header": {"x-foo":["bar", "baz"]},
"interval": "10s",
"timeout": "1s"
}
}
TCP檢查:
{
"check": {
"id": "ssh",
"name": "SSH TCP on port 22",
"tcp": "localhost:22",
"interval": "10s",
"timeout": "1s"
}
}
TTL檢查:
{
"check": {
"id": "web-app",
"name": "Web App Status",
"notes": "Web app does a curl internally every 10 seconds",
"ttl": "30s"
}
}
Docker檢查:
{
"check": {
"id": "mem-util",
"name": "Memory utilization",
"docker_container_id": "f972c95ebf0e",
"shell": "/bin/bash",
"args": ["/usr/local/bin/check_mem.py"],
"interval": "10s"
}
}
gRPC檢查:
{
"check": {
"id": "mem-util",
"name": "Service health status",
"grpc": "127.0.0.1:12345",
"grpc_use_tls": true,
"interval": "10s"
}
}
本地服務的別名檢查:
{
"check": {
"id": "web-alias",
"alias_service": "web"
}
}
我這邊簡單使用了TCP檢查, 繼續修改service.json文件,檢測 tcp爲
"172.0.0.1:80"的服務,修改爲如下代碼:
{
"services": [
{
"id": "API1",//唯一標識
"name": "API1",//服務名稱
"tags": [ "API1" ],//服務標籤
"address": "172.0.0.1",//我隨便配的IP,注意配置服務的真實IP和port
"port": 80
},
{
"id": "API2",
"name": "API2",
"tags": [ "API2" ],
"address": "172.0.0.1",//我隨便配的IP,注意配置服務的真實IP和port
"port": 81
}
],
"check": [
{
"id": "APICheck",
"name": "APICheck",
"tcp": "119.29.50.115:80",
"interval": "10s",
"timeout": "1s"
}
]
}
check
定義爲service同級節點則是爲所有服務使用同一個檢查規則,定義在services節點內則是具體爲某一個服務定義檢查規則
啓動如下圖,很明顯多了一個名叫APICheck 的代理。
啓動頁面也有不同,checks 爲2了,說明check
配置成功了。點擊某個服務進去可以查看詳細信息
docker部署(騰訊雲)
前面說的是本地部署,現在說一下基於騰訊雲docker
部署。首先拉去docker鏡像創建服務。
Docker Hub(鏡像文件庫) 裏包含Consul
的鏡像文件,只需要在Docker創建服務使用鏡像就可以了。
設置容器端口爲8500,服務端口爲80,通過Ingress進行路由轉發。
訪問服務外網,結果如下,配置成功
配置Ocelot 網關
首先修改前面的網關項目ApiGateway Startup.cs 文件裏的 ConfigureServices方法,添加
**.AddConsul()**方法代碼如下:
public void ConfigureServices(IServiceCollection services)
{
//Identity Server Bearer Tokens
Action<IdentityServerAuthenticationOptions> isaOpt = option =>
{
option.Authority = Configuration["IdentityService:Uri"];
option.RequireHttpsMetadata = Convert.ToBoolean(Configuration["IdentityService:UseHttps"]);
option.ApiName = Configuration["IdentityService:ApiName"];
option.ApiSecret = Configuration["IdentityService:ApiSecret"];
option.SupportedTokens = SupportedTokens.Both;
};
services.AddAuthentication().AddIdentityServerAuthentication(Configuration["IdentityService:DefaultScheme"], isaOpt);
services
.AddOcelot(Configuration)
.AddConsul()
//啓用緩存
.AddCacheManager(x => { x.WithDictionaryHandle(); })
.AddPolly()
.AddAdministration("/administration", isaOpt);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); ;
services.AddSwaggerGen(options =>
{
options.SwaggerDoc(Configuration["Swagger:Name"], new Info { Title = Configuration["Swagger:Title"], Version = Configuration["Swagger:Version"] });
});
}
接下來配置ocelot.json 文件,在GlobalConfiguration
節點下添加服務發現提供程序配置
//服務發現提供程序
"ServiceDiscoveryProvider": {
"Host": "111.230.118.59",
"Port": 80,
"Type": "PollConsul",
"PollingInterval": 1000
}
項目上游配置添加ServiceName 和 UseServiceDiscovery屬性,代碼如下:
{
"UpstreamPathTemplate": "/gateway/2/{url}",
"UpstreamHttpMethod": [ "Get", "Post", "Delete", "Put" ],
"DownstreamPathTemplate": "/api2/{url}",
"DownstreamScheme": "http",
"ServiceName": "API2",
"UseServiceDiscovery": true,
"LoadBalancer": "RoundRobin",
"DownstreamHostAndPorts": [
{
"Host": "111.230.160.62",
"Port": 80
},
{
"Host": "localhost",
"Port": 13002
}
],
"QoSOptions": {
"ExceptionsAllowedBeforeBreaking": 3,
"DurationOfBreak": 10,
"TimeoutValue": 5000
}
"AuthenticationOptions": {
"AuthenticationProviderKey": "IdentityBearer",
"AllowedScopes": [
]
}
}
啓動OcelotGateway,API001,API002項目,通過http://localhost:13000/gateway/1/values,和http://localhost:13000/gateway/2/values訪問;因爲Ocelot配置了Consul的服務治理,所以可以通過配置的服務名稱和GlobalConfiguratin的Consul
http
api接口查找到對應服務的地址,進行訪問,這些都是Ocelot幫我們做,這點很容易證明,可以修改Consul配置文件中服務的address爲錯誤IP,就會發現通過13000端口訪問不成功。
集成消息隊列——CAP
簡介
CAP 是一個基於 .NET Standard 的 C#
庫,它是一種處理分佈式事務的解決方案,同樣具有 EventBus
的功能,它具有輕量級、易使用、高性能等特點。
微服務系統的過程中,通常需要使用事件來對各個服務進行集成,在這過程中簡單的使用消息隊列並不能保證數據的最終一致性,
CAP
採用的是和當前數據庫集成的本地消息表的方案來解決在分佈式系統互相調用的各個環節可能出現的異常,它能夠保證任何情況下事件消息都是不會丟失的。
Github 地址:https://github.com/dotnetcore/CAP
支持消息隊列:
-
Kafka
-
RabbitMQ
-
AzureServiceBus
數據庫存儲:
-
Sql Server
-
MySql
-
PostgreSQL
-
MongoDB
環境準備
我們以RabbitMQ 與Sql Server來講解。
首先我們需要安裝RabbitMQ 服務,很簡單,官方下載最新的安裝包。
但是在安裝RabbitMQ
時會提示安裝Erlang,Erlang是一種通用的面向併發的編程語言,Erlang來編寫分佈式應用要簡單的多。RabbitMQ是用Erlang實現的一個高併發高可靠AMQP消息隊列服務器。
官方下載對應的Erlang 安裝程序,建議RabbitMQ和Erlang都安裝最新版本
安裝完成之後,會多了以下幾個程序,安裝包幫我生成了start、remove、stop等命令程序。我們拿來直接用就可以了,當然你也可以配置環境變量,使用命令啓動。先運行start
程序運行起來。
.Net Core 集成 CAP
Nuget 包下載:
-
DotNetCore.CAP 核心包
-
DotNetCore.CAP.RabbitMQ CAP RabbitMQ 包
-
DotNetCore.CAP.SqlServer CAP Sql Server 擴展包
繼續修改測試項目Service.Test1項目,使用CodeFirst生成數據庫:
新建測試類Test:
public class Test
{
public int Id { get; set; }
public string Name { get; set; }
public string Title { get; set; }
}
添加AppDbContext 數據庫上下文 文件,代碼如下:
public class AppDbContext:DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public virtual DbSet<Test> Tests { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
配置數據庫連接字符串:
"ConnectionStrings": {
"Default": "Server=(localdb)\\MSSQLLocalDB; Database=Service_test1; Trusted_Connection=True;"
}
Program.cs 文件配置讀取appsettings.json文件。
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
})
.UseStartup<Startup>();
Startup.cs 文件ConfigureServices添加數據訪問配置
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Default")));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
到這裏正常的CodeFirst
項目配置已經完成了,直接運行數據遷移命令就可以創建數據庫了。
但是我這裏需要集成CAP,肯定這樣是不行的。需要進行CAP的配置,繼續在ConfigureServices
添加如下代碼:
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("Default")));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Action<CapOptions> capOptions = option =>
{
option.UseEntityFramework<AppDbContext>();
option.UseSqlServer(Configuration.GetConnectionString("Default"));
option.UseRabbitMQ("localhost");//UseRabbitMQ 服務器地址配置,支持配置IP地址和密碼
option.UseDashboard();//CAP2.X版本以後官方提供了Dashboard頁面訪問。
if (Convert.ToBoolean(Configuration["Cap:UseConsul"]))
{
option.UseDiscovery(discovery =>
{
discovery.DiscoveryServerHostName = Configuration["Cap:DiscoveryServerHostName"];
discovery.DiscoveryServerPort = Convert.ToInt32(Configuration["Cap:DiscoveryServerPort"]);
discovery.CurrentNodeHostName = Configuration["Cap:CurrentNodeHostName"];
discovery.CurrentNodePort = Convert.ToInt32(Configuration["Cap:CurrentNodePort"]);
discovery.NodeId = Convert.ToInt32(Configuration["Cap:NodeId"]);
discovery.NodeName = Configuration["Cap:NodeName"];
discovery.MatchPath = Configuration["Cap:MatchPath"];
});
}
};
services.AddCap(capOptions);
RabbitMQ 也是支持配置options
option.UseRabbitMQ(cfg =>
{
cfg.HostName = Configuration["MQ:Host"];
cfg.VirtualHost = Configuration["MQ:VirtualHost"];
cfg.Port = Convert.ToInt32(Configuration["MQ:Port"]);
cfg.UserName = Configuration["MQ:UserName"];
cfg.Password = Configuration["MQ:Password"];
});
CAP 內置集成了Consul
服務註冊,註冊的同時默認攜帶了簡況檢查,但是隻支持HTTP檢查,所以我們需要在接口中定義health
路徑提供給檢查訪問。
在appsetting.json 文件中添加相應的配置節點:
"Cap": {
"UseConsul": true,//是否開啓
"CurrentNodeHostName": "localhost",//當前節點IP
"CurrentNodePort": 13001,//當前節點Port
"DiscoveryServerHostName": "127.0.0.1",//發現服務主機IP
"DiscoveryServerPort": 8500,//發現服務主機Port
"NodeId": 1,//節點標識
"NodeName": "CAP_API1",//節點名稱
"MatchPath": "/api1/TestOnes"//健康檢查根路勁 最終的路徑爲api1/TestOnes/health
}
進行數據遷移創建數據庫,表結構如下:
Cap 發佈
接下來就是去使用Cap 發佈了,修改Controller代碼
public class TestOnesController : ControllerBase
{
private readonly ICapPublisher _capBus;
public TestOnesController(ICapPublisher capPublisher)
{
_capBus = capPublisher;
}
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
_capBus.Publish("services.test1.show.time", DateTime.Now);
return new string[] { "TestOnes_value1", "TestOnes_value2" };
}
//定義路由爲health提供給服務檢查使用
[HttpGet]
[Route("health")]
public ActionResult<string> Health()
{
return "Health!!!!!";
}
}
因爲啓用的Consul ,所以要按照前面說過的consul 教程來啓動consul
訪問http://127.0.0.1:8500,頁面如下
接下來啓動項目,還是老樣子直接看到如下頁面。
但是我們集成了CAP,所以可以訪問呢http://localhost:13001/cap 訪問cap
Dashboard頁面查看詳細
這裏一般啓動的話發出的時不存在,也是因爲前面有測試過,數據庫裏存在了。我們調用api1/TestOnes方法
發出消息。
請求成功,在來看看數據庫。數據庫多了兩張表,以張是接收數據表,一張是發佈數據表。
再來看看裏面的數據,也是就是發佈的消息,因爲之前請求過四次,我這邊就多了四條數據。
cap Dashboard也能看到一些統計和數據列表
再來看看consul 頁面,一個CAP_API1 的服務已經被註冊進來了
如果前面 MatchPath
路徑沒有配置對的話,就會出現下面的情況,導致無法通過健康檢查。
Cap 訂閱(接收)
使用API訂閱消息,爲了方便,使用同一個項目的另一個接口實現訂閱
[Route("api1/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[HttpGet("Received")]
[CapSubscribe("services.test1.show.time")]//配置發佈時填寫的Name
public ActionResult<string> GetReceivedMessage(DateTime datetime)
{
Console.WriteLine("訂閱:"+datetime);
return "訂閱:" + datetime;
}
}
這樣就OK了,但是如果你時在不同的項目,還是需要像前面一樣配置CAP。
啓動項目請求一次CAP發佈接口,查看http://localhost:13001/cap
可以看到接收的裏面有1條數據
訂閱列表中也有了一條數據
在來看數據庫也添加一條數據
最後——附上總體代碼
整個實踐代碼已託管到Github,具體如下所示:https://github.com/magicodes/Magicodes.Simple.Services