Net Core API网关Ocelot
Ocelot在github的地址 https://github.com/TomPallister/Ocelot , 非常给力的是在课程当天完成了.NET Core 2.0的升级,升级过程请看https://github.com/TomPallister/Ocelot/issues/114 。昨天我花了半小时就把我的另外一个POC项目Nanofabric https://github.com/geffzhang/NanoFabric 升级到了.NET Core 2.0, 这个POC项目也是我的分享的项目的原型,可以这么说.NET Core 2.0 8月份正式发布,经过3 个月时间的发展,社区生态已经都已经做好了准备,开发新项目可以采用.NET Core 2,Ocelot 是一个集成社区中众多优秀开源项目的代表。
业务的飞速发展,产生的非常多的对外的服务接口,分散在组织的各个地方需要进行统一的管理,而且我们的环境是linux和windows的混合环境,我们的目标是统一在公司的Linux环境,.NET Core对于.NET 技术团队来说是一个非常棒的技术,而且.NET Core本身的架构非常好,性能就更好了。
这里列出了Ocelot目前支持的特性:
- Routing
- 用户可以指定上游请求之间的映射,并将其转发到下游服务上的不同URL。
- Service Discovery
-
- Ocelot可以查看你的服务发现,并找到它应该转发下游请求的服务。它可以在这些服务之间进行负载平衡。.
- Authentication using IdentityServer
- 您可以将端点标记为已认证,并使用IdentityServer承载标记对您的用户进行身份验证.
- Authorisation using Claims
- 如果使用 bearer tokens, 可以使用 claims 标记特定 endpoints是授权的
- Claims Transformation
- Ocelot提供了一种语法来转换给下游请求,并将声明数据添加到标题,URL参数,其他声明等等
- Quality of service
- Retries, circuit breaker, timeouts etc.
- Request / Correlation Ids
- Caching
- Logging
- Custom Middleware
更详细的内容参看文档 https://github.com/TomPallister/Ocelot/wiki
上面介绍了Ocelot的功能特性,接下来我们进入介绍Ocelot 的实现原理剖析,核心是是ASP.NET Core Middleware 以及 ASP.NET Core DependencyInjection:
ASP.NET Core 传统的ASP.NET 在架构上有很大的改进,更加的模块化,下图形象的说明了他们之间区别,Application 和 Middleware 是平等的,比如ASP.NET Core MVC也是一个Middleware,通过Middleware这样的结构我们非常容易的扩展我们的应用程序。
Ocelot就是使用Middleware来完成网关的所有功能,每个小功能就是一个Middleware,具体可以看代码 https://github.com/TomPallister/Ocelot/blob/develop/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs ,Ocelot 是如何把各个Middleware串起来协同完成一个API网关的功能。 asp.net core 非常巧妙的设计,把Middleware抽象成了一个委托RequestDelegate, ASP.NET Core 的每个 Request 都会经过每个所注册的 Middleware,Response 也是逐一回传,以先进后出的方式处理每一个封包:
具体内容参考: ASP.NET Core HTTP 管道中的那些事儿 和 如何一秒钟从头构建一个 ASP.NET Core 中间件, 我们在Middleware的编程过程中需要关注HttpContext 以及管道的注册者和构建者 ApplicationBuilder。
ASP.NET Core 使用了大量的 DI (Dependency Injection) 设计,同样我们在Ocelot的设计中也使用了大量的DI设计,具体参看源码https://github.com/TomPallister/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs
注册 Service 有分三种方式:
- Transient 每次注入时,都重新 new 一个新的实体。
- Scoped 每个 Request 都重新 new 一个新的实体。
- Singleton 程序启动后会 new 一个实体。也就是运行期间只会有一个实体。
下面这张图来自https://blog.johnwu.cc/article/asp-net-core-dependency-injection.html ,形象的演示了对象生命周期。
- A 为 Singleton
- B 为 Scoped
- C 为 Transient
上面介绍完了Ocelot开发的基本原理,目前Ocelot 由17 个Middleware 来完成,在每个Middleware的内部实现上还有涉及到很多业务的知识,本篇文章先不做展开,后续写具体的文章详细解析。接下来我们来说说如何自定义扩展,在我们的项目中主要在三个方面进行了扩展:
1、自定义扩展API 接口验证
Ocelot 默认支持基于IdentityServer4的认证,需要自定义认证,可以参考 https://github.com/TomPallister/Ocelot/pull/110,添加自定义的验证,但是.net core 2.0 认证部分基本上重写了。
2、自定义扩展下游通讯协议
Ocelot 默认支持Http的通讯,在我们的实际项目中有很多老的服务是RPC调用,使用的是私有的Relay通讯框架,在API网关上需要做协议转换,自动将Http的请求转换成Relay的tcp通讯。
3、自定义管理控制台
ocelot 有管理API,可以基于管理API 做自定义的管理控制台,github 有个 https://github.com/dbarkwell/Ocelot.ConfigEditor,这个项目实现了asp.net core mvc 的在线编辑路由。
首先,让我们简单了解下什么是API网关?
API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。
API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务。
其次,我们了解下Ocelot框架
Ocelot的目标是使用.NET运行微服务/面向服务架构,我们需要一个统一的入口进入我们的服务,提供监控、鉴权、负载均衡等机制,也可以通过编写中间件的形式,来扩展Ocelot的功能。 Ocelot是一堆特定顺序的中间件。
Ocelot框架内部集成了IdentityServer和Consul(服务注册发现),还引入了Polly来处理进行故障处理,关于Polly,可以在这篇博客中了解更多《已被.NET基金会认可的弹性和瞬态故障处理库Polly介绍》
Ocelot开源地址:https://github.com/TomPallister/Ocelot
接下来,我们就针对Ocelot的具体用法展开介绍。
这里使用的Ocelot版本为2.0,.Net Core版本2.0
1、搭建Asp.net Core WebApi项目,引用Ocelot.dll。
Nuget控制台,执行Ocelot安装。
1
|
PM>Install-Package Ocelot |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
GET https: //api .nuget.org /v3/registration3-gz-semver2/ocelot/index .json OK https: //api .nuget.org /v3/registration3-gz-semver2/ocelot/index .json 104 毫秒 GET https: //api .nuget.org /v3/registration3-gz-semver2/ocelot/page/1 .5.0-unstable0134 /2 .0.0.json OK https: //api .nuget.org /v3/registration3-gz-semver2/ocelot/page/1 .5.0-unstable0134 /2 .0.0.json 108 毫秒 正在还原 J:\Demo\RichCodeBox\APIGatewayApp\APIGatewayApp.csproj 的包... GET https: //api .nuget.org /v3-flatcontainer/ocelot/index .json OK https: //api .nuget.org /v3-flatcontainer/ocelot/index .json 476 毫秒 GET https: //api .nuget.org /v3-flatcontainer/ocelot/2 .0.0 /ocelot .2.0.0.nupkg OK https: //api .nuget.org /v3-flatcontainer/ocelot/2 .0.0 /ocelot .2.0.0.nupkg 125 毫秒 GET https: //api .nuget.org /v3-flatcontainer/identityserver4 .accesstokenvalidation /index .json GET https: //api .nuget.org /v3-flatcontainer/cachemanager .core /index .json GET https: //api .nuget.org /v3-flatcontainer/cachemanager .microsoft.extensions.configuration /index .json GET https: //api .nuget.org /v3-flatcontainer/cachemanager .microsoft.extensions.logging /index .json GET https: //api .nuget.org /v3-flatcontainer/consul/index .json GET https: //api .nuget.org /v3-flatcontainer/polly/index .json GET https: //api .nuget.org /v3-flatcontainer/identityserver4/index .json OK https: //api .nuget.org /v3-flatcontainer/identityserver4 .accesstokenvalidation /index .json 133 毫秒 GET https: //api .nuget.org /v3-flatcontainer/identityserver4 .accesstokenvalidation /2 .1.0 /identityserver4 .accesstokenvalidation.2.1.0.nupkg OK https: //api .nuget.org /v3-flatcontainer/cachemanager .microsoft.extensions.logging /index .json 286 毫秒 OK https: //api .nuget.org /v3-flatcontainer/polly/index .json 287 毫秒 OK https: //api .nuget.org /v3-flatcontainer/identityserver4 .accesstokenvalidation /2 .1.0 /identityserver4 .accesstokenvalidation.2.1.0.nupkg 160 毫秒 GET https: //api .nuget.org /v3-flatcontainer/cachemanager .microsoft.extensions.logging /1 .1.1 /cachemanager .microsoft.extensions.logging.1.1.1.nupkg |
2、修改Startup程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/// <summary> /// /// </summary> /// <param name="environment"></param> public Startup(IHostingEnvironment environment) { var builder = new Microsoft.Extensions.Configuration.ConfigurationBuilder(); builder.SetBasePath(environment.ContentRootPath) .AddJsonFile( "appsettings.json" , false , reloadOnChange: true ) .AddJsonFile($ "appsettings.{environment.EnvironmentName}.json" , optional: false , reloadOnChange: true ) .AddJsonFile( "configuration.json" , optional: false , reloadOnChange: true ) .AddEnvironmentVariables(); Configuration = builder.Build(); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
/// <summary> ///modified:配置 /// </summary> public IConfigurationRoot Configuration { get ; } /// <summary> /// 配置服务 /// </summary> /// <param name="services"></param> public void ConfigureServices(IServiceCollection services) { Action<ConfigurationBuilderCachePart> settings = (x) => { x.WithMicrosoftLogging(log => { log.AddConsole(LogLevel.Debug); }).WithDictionaryHandle(); }; services.AddOcelot(Configuration, settings); //services.AddMvc(); } /// <summary> /// 配置Ocelot /// </summary> /// <param name="app"></param> /// <param name="env"></param> public async void Configure(IApplicationBuilder app, IHostingEnvironment env) { //if (env.IsDevelopment()) //{ // app.UseDeveloperExceptionPage(); //} await app.UseOcelot(); //app.UseMvc(); } /// <summary> /// 入口程序 /// </summary> /// <param name="args"></param> public static void Main( string [] args) { IWebHostBuilder builder = new WebHostBuilder(); builder.ConfigureServices(s => { s.AddSingleton(builder); }); builder.UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<Startup>() .UseApplicationInsights(); var host = builder.Build(); host.Run(); } |
3、配置Ocelot。
我们新建一个名为configuration的json文件,配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
{ "ReRoutes" : [ { "DownstreamPathTemplate" : "/api/values" , "DownstreamScheme" : "http" , "DownstreamHost" : "localhost" , "DownstreamPort" : 8801, "UpstreamPathTemplate" : "/api/values" , "UpstreamHttpMethod" : [ "Get" ], "QoSOptions" : { "ExceptionsAllowedBeforeBreaking" : 3, "DurationOfBreak" : 10, "TimeoutValue" : 5000 }, "HttpHandlerOptions" : { "AllowAutoRedirect" : false , "UseCookieContainer" : false }, "AuthenticationOptions" : { } }, { "DownstreamPathTemplate" : "/api/product" , "DownstreamScheme" : "http" , "DownstreamPort" : 8802, "DownstreamHost" : "localhost" , "UpstreamPathTemplate" : "/api/product" , "UpstreamHttpMethod" : [ "Get" ], "QoSOptions" : { "ExceptionsAllowedBeforeBreaking" : 3, "DurationOfBreak" : 10, "TimeoutValue" : 5000 }, "AuthenticationOptions" : { } } ], "GlobalConfiguration" : { "RequestIdKey" : "OcRequestId" , "AdministrationPath" : "/admin" } } |
在这里,我们配置了两个服务,端口分别为8801和8802的。
Ocelot支持负载均衡(提供轮询、最少访问)。Ocelot大部分功能,都可以通过中间件来完成,也可以实现和重写中间件。
Ocelot原理非常简单,这里的配置文件,体现了上游请求和下游服务间的映射关系,你可以理解为,上游是客户端直接调用的URL ,下游,则是对应我们开发的服务。
4、新增两个WebApi项目,分别为APIUserServiec和APIProductService。
API服务 | 端口(Port) |
APIUserServiec | 8801 |
APIProductService | 8802 |
解决方案如下:
5、配置VS启动端口:
依次类推,分别设置端口。
6、启动项目。
配置多个项目启动。
F5启动项目。
再通过API网关,访问商品服务:http://localhost:5000/api/product。
常见问题:
首次在启动API网关时,触发以下错误。
Sequence contains no matching element
根据错误详细信息,可知原因是由于系统调用AddIdentityServer方法时,触发异常。
刚开始,怀疑是配置文件configuration.json文件配置导致的,Ocelot2.0版,采用官方配置仍然触发该异常,由此排除这种可能。接下来,直接从github上克隆源代码,查看。
找到触发错误的地方,
private static void AddIdentityServer(this IServiceCollection services, IIdentityServerConfiguration identityServerConfiguration, IConfigurationRoot configurationRoot) { services.TryAddSingleton<IIdentityServerConfiguration>(identityServerConfiguration); services.TryAddSingleton<IHashMatcher, HashMatcher>(); var identityServerBuilder = services .AddIdentityServer(o => { o.IssuerUri = "Ocelot"; }) .AddInMemoryApiResources(Resources(identityServerConfiguration)) .AddInMemoryClients(Client(identityServerConfiguration)) .AddResourceOwnerValidator<OcelotResourceOwnerPasswordValidator>(); //todo - refactor a method so we know why this is happening var whb = services.First(x => x.ServiceType == typeof(IWebHostBuilder));//这个地方触发了错误 var urlFinder = new BaseUrlFinder((IWebHostBuilder)whb.ImplementationInstance); var baseSchemeUrlAndPort = urlFinder.Find(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddIdentityServerAuthentication(o => { var adminPath = configurationRoot.GetValue("GlobalConfiguration:AdministrationPath", string.Empty); o.Authority = baseSchemeUrlAndPort + adminPath; o.ApiName = identityServerConfiguration.ApiName; o.RequireHttpsMetadata = identityServerConfiguration.RequireHttps; o.SupportedTokens = SupportedTokens.Both; o.ApiSecret = identityServerConfiguration.ApiSecret; }); //todo - refactor naming.. if (string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificateLocation) || string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificatePassword)) { identityServerBuilder.AddDeveloperSigningCredential(); } else { //todo - refactor so calls method? var cert = new X509Certificate2(identityServerConfiguration.CredentialsSigningCertificateLocation, identityServerConfiguration.CredentialsSigningCertificatePassword); identityServerBuilder.AddSigningCredential(cert); }
var whb = services.First(x => x.ServiceType == typeof(IWebHostBuilder));
这就代码触发了错误,是由于表达式条件不成立,导致First发生异常,这就简单了,我们修改Main函数,
把这句代码:
var builder = new WebHostBuilder();
改成:
IWebHostBuilder builder = new WebHostBuilder();
这样,就解决了问题,API网关启动成功。另外,官方例子https://github.com/TomPallister/Ocelot/blob/develop/test/Ocelot.ManualTest/Program.cs中,写法是正确的,大家自己写的时候,注意把IWebHostBuilder换成var会引发错误。
这样,使用Ocelot框架搭建API网关的工作已经完成,尝试通过访问API网关,来访问下游的服务。
此实例源码:https://gitee.com/lichaoqiang/RichCodeBox.git
欢迎大家一起研究探讨,开启你的微服务之路。