# 前言

上一篇使用 Consul 完成了服务的注册与发现,实际中光有服务注册与发现往往是不够的,需要一个统一的入口来连接客户端与服务。

# Ocelot

官网:https://ocelot.readthedocs.io (opens new window) ,Ocelot 正是为.Net微服务体系提供一个统一的入口点,称为:Gateway(网关)。

首先创建一个空的 asp.net core web 项目:

img

注意:ocelot.json 是Ocelot的配置文件,设置生成时需要复制到输出目录。ocelot.json 文件名不是固定的可以自己定义

使用 NuGet 安装 Ocelot,简单修改几处默认代码:

Program.cs:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hostingContext, config) =>
             {
                 config.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
             })
            .ConfigureWebHostDefaults(webBuilder =>
             {
                 webBuilder.UseStartup<Startup>();
             });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // 添加ocelot服务
    services.AddOcelot();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // 启用Ocelot中间件
    app.UseOcelot().Wait();
}
1
2
3
4
5
6
7
8
9
10
11
12

ocelot.json:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "192.168.31.191",
          "Port": 80
        },
        {
          "Host": "192.168.31.191",
          "Port": 81
        },
        {
          "Host": "192.168.31.191",
          "Port": 82
        }
      ],
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //负载均衡,轮询机制 LeastConnection/RoundRobin/NoLoadBalancer/CookieStickySessions
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5000"
  }
}
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

这里将服务实例的地址写在配置文件中。

Routes 节点用来配置路由:

  • Downstream 代表下游,也就是服务实例
  • Upstream 代表上游,也就是客户端。这里路径比较简单,只有 /orders 路径中如果有不固定参数则使用 {} 匹配。

这里配置的意思是:客户端访问网关的 /orders,网关会转发给服务实例的 /orders 。注意:上游的路径不一定要和下游一致,比如上游路径可以配置成 /api/orders

LoadBalancerOptions 节点用来配置负载均衡,Ocelot 内置了 LeastConnectionRoundRobinNoLoadBalancerCookieStickySessions 4种负载均衡策略:

  • LeastConnection 最少连接,跟踪哪些服务正在处理请求,并把新请求发送到现有请求最少的服务上。该算法状态不在整个Ocelot集群中分布
  • RoundRobin 轮询可用的服务并发送请求。 该算法状态不在整个Ocelot集群中分布
  • NoLoadBalancer 不负载均衡,从配置或服务发现提供程序中取第一个可用的下游服务
  • CookieStickySessions 使用cookie关联所有相关的请求到制定的服务

BaseUrl 节点用来配置 Ocelot 网关将要运行的地址。

浏览器访问:

img

# 客户端

上面实现通过 Ocelot 网关访问服务实例,调整客户端代码:这里选择直接新建 GatewayServiceHelper

using RestSharp;

using System;
using System.Threading.Tasks;

namespace Web.Client
{
    /// <summary>
    /// 通过OcelotGateway调用服务
    /// </summary>
    public class GatewayServiceHelper : IServiceHelper
    {
        public async Task<string> GetOrder()
        {
            var client = new RestClient("http://localhost:5000");
            var request = new RestRequest("/orders", Method.GET);

            var response = await client.ExecuteAsync(request);
            return response.Content;
        }

        public void GetServices()
        {
            throw new NotImplementedException();
        }
    }
}
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

Startup.cs:修改注入类型

services.AddSingleton<IServiceHelper, GatewayServiceHelper>();
1

下面获取服务地址的代码也不需要了

// 程序启动时获取服务列表
serviceHelper.GetServices();
1
2

经过以上调整现在客户端对服务的调用都通过网关进行中转,客户端不再关心服务实例的地址,只需要知道网关地址就可以。另外服务端也避免了服务地址直接暴露给客户端。这样做对客户端,服务都非常友好。但是又出现了一个新的问题:目前服务地址写在 ocelot.json 配置文件中,一旦服务变化,需要人为的修改配置文件,这又显得不太合理。这里比较常用的方案是:结合Consul来实现服务发现。

# 服务发现

NuGet 安装Ocelot.Provider.Consul后,修改Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // 添加Ocelot服务并添加Consul支持
    services.AddOcelot().AddConsul();
}
1
2
3
4
5

修改ocelot.json配置:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "order.service",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5000",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "192.168.31.191",
      "Port": 8500,
      "Type": "Consul"
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这个配置很好理解,就是把 DownstreamHostAndPorts 节点去掉然后增加了 ServiceDiscoveryProvider 服务发现相关配置。

注意,Ocelot 除了支持 Consul 服务发现以外,还有 Eureka 也可以,Eureka 也是一个类似的注册中心

浏览器测试:

img

至此就实现了服务注册与发现和api网关的基本功能。接下来就要提到:服务治理。

# 服务治理

服务治理没有非常明确的定义。它的作用简单来说,就是帮我们更好的管理服务,提升服务的可用性。缓存、限流、熔断、链路追踪等等都属于常用的服务治理手段。之前讲的负载均衡,服务发现也可以算是服务治理。

# 缓存

在 Ocelot 中启用缓存,需要NuGet 安装Ocelot.Cache.CacheManager,修改Startup.cs 中的 ConfigureServices() 方法:

public void ConfigureServices(IServiceCollection services)
{
    services.AddOcelot()
            .AddConsul()
            .AddCacheManager(p =>
            {
                p.WithDictionaryHandle();
            });
}
1
2
3
4
5
6
7
8
9

修改 ocelot.json 配置文件:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "order.service",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      // 缓存
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5000",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "192.168.31.191",
      "Port": 8500,
      "Type": "Consul"
    }
  }
}
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

在 Routes 路由配置中增加 FileCacheOptions

  • TtlSeconds 缓存的过期时间
  • Region 缓冲区名称,目前用不到

代码修改完编译重启一下网关项目,然后打开浏览器测试会发现5秒之内的请求都是同样的缓存数据。Ocelot也支持自定义缓存。

# 限流

限流就是限制客户端一定时间内的请求次数。

修改 ocelot.json 配置文件:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "order.service",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      // 缓存
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      },
      // 限流
      "RateLimitOptions": {
        "ClientWhitelist": [ "SuperClient" ],
        "EnableRateLimiting": true,
        "Period": "2s",
        "PeriodTimespan": 2,
        "Limit": 1
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5000",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "192.168.31.191",
      "Port": 8500,
      "Type": "Consul"
    },
    "RateLimitOptions": {
      "DisableRateLimitHeaders": false,
      "QuotaExceededMessage": "too many requests...",
      "HttpStatusCode": 999,
      "ClientIdHeader": "Test"
    }
  }
}
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

在 Routes 路由配置中增加 RateLimitOptions

  • ClientWhitelist 客户端白名单(白名单中的客户端不受限流影响)
  • EnableRateLimiting 是否限流
  • Period 限流的单位时间,例如1s、5m、1h、1d等
  • PeriodTimespan 客户端达到请求上限多少秒后可以重试
  • Limit 客户端在定义的时间内可以发出的最大请求数

在 GlobalConfiguration 配置中也增加 RateLimitOptions

  • DisableRateLimitHeaders 是否禁用 X-Rate-LimitRetry-After 标头(请求达到上限时response header中的限制数和多少秒后能重试)
  • QuotaExceededMessage :请求达到上限时返回给客户端的消息
  • HttpStatusCode :请求达到上限时返回给客户端的 HTTP状态码
  • ClientIdHeader 可以允许自定义用于标识客户端的标头。默认情况下为 ClientId

代码修改完编译重启一下网关项目,然后打开浏览器测试会发现限制已经生效。

# 超时/熔断

  • 超时:网关请求服务时可容忍的最长响应时间
  • 熔断:当请求某个服务的异常次数达到一定量时,网关在一定时间内就不再对这个服务发起请求直接熔断

在 Ocelot 中启用超时/熔断,需要 NuGet 安装Ocelot.Provider.Polly,修改Startup.cs 中的 ConfigureServices() 方法:

public void ConfigureServices(IServiceCollection services)
{
    services.AddOcelot()
            .AddConsul()
            .AddCacheManager(p =>
            {
                p.WithDictionaryHandle();
            }).AddPolly();
}
1
2
3
4
5
6
7
8
9

修改 ocelot.json 配置文件:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "Get" ],
      "ServiceName": "order.service",
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      },
      // 缓存
      "FileCacheOptions": {
        "TtlSeconds": 5,
        "Region": "regionname"
      },
      // 限流
      "RateLimitOptions": {
        "ClientWhitelist": [ "SuperClient" ],
        "EnableRateLimiting": true,
        "Period": "2s",
        "PeriodTimespan": 2,
        "Limit": 1
      },
      // 超时熔断
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 3,
        "DurationOfBreak": 10000,
        "TimeoutValue": 5000
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5000",
    "ServiceDiscoveryProvider": {
      "Scheme": "http",
      "Host": "192.168.201.191",
      "Port": 8500,
      "Type": "Consul"
    },
    "RateLimitOptions": {
      "DisableRateLimitHeaders": false,
      "QuotaExceededMessage": "too many requests...",
      "HttpStatusCode": 999,
      "ClientIdHeader": "Test"
    }
  }
}
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
  • ExceptionsAllowedBeforeBreaking 发生错误的次数
  • DurationOfBreak 熔断时间
  • TimeoutValue 超时时间

以上配置意思是当请求服务发生3次错误时,就熔断10秒,期间客户端的请求直接返回错误,10秒后恢复。