一、网关基本概念
1.解耦
解耦的好处,客户端和微服务独立扩展,服务端可以随意增加,客户端没有感知,当然因为在程序架构中加入了一层产生的好处,我们也可以在网关本身做一些扩展,例如缓存,抗并发等。
2.分发
分发的好处就是网关可以将请求根据不同的算法负载均衡到不同的实例上,提高系统的伸缩性,在某种意义上同nginx的某些理念有点相同,但是定位确实不一样的
二、为什么要使用网关?
从上面的基本概念我们大概的可以了解到网关的作用, 其实在我学习时我更加在意使用实际的概念引入,这样可以加深理解和思维发散。目前我们针对高可用,和高并发的最常见策略就是集群部署,水平铺开承载压力,为了说明为什么要是用网关,简单整理了下。
1.问题背景
问题1
稍微简单的系统抛开暂且来不说,如果服务集群实例数量庞大,如何去有效的管理众多的集群实例,并且保证集群实例的运行以及调用和提供服务的可用性呢?例如A服务调用B服务 B调用C服务,A1调用B1,内部如同蜘蛛网一样错综复杂,如下图,毋庸置疑配置、代码肯定写的到处都是,估计很痛苦吧
这时候使用网关,就可以解决此类问题。使用网关来做统一的调度中心,我们服务只需要调用网关一个服务地址就行了,至于具体调用什么服务,根据网关的配置规则以及路由将请求送达。
问题2
当我的A服务,调用微服务B的2个实例B1和B2时,传统的方式,A直接通过HTTP或者gRPC调用就行了,但是会存在问题,当某一天在新增一个B3实例,A服务中还需要去修改负载均衡的代码, 如果B服务的地址发生修改,那么A服务就也要跟着修改 ,这2个问题直接违反了开闭原则,降低系统的可用性。
2.网关模型
基于上面问题,对网关层的抽象,我们可以得到简单结构图,同时我们可以将 例如缓存,负载均衡,鉴权认证,限速,节流等机制,从原有的各自服务中剥离到放在网关这一层,使微服务只专注本身的业务处理剥离。
其实剥离后我们有2种模型
1.进程外
如上图一样网关被独立为一个服务,就是进程外,比较推荐
这种方式
2.进程内
将网关放到用户调用的聚合服务里面,就是进程内,但是对业务有侵入,所以不建议
三、网关实现
我们实现进程外网关主要使用Net Core开发,所以选择NET相关的,目前相对于大众的就是Ocelot
1.实现网关环境
主要步骤如下
1.创建一个独立的API服务作为我们的网关
2.改造项目为Abp vNEXT项目
3.集成Ocelot,并修改配置文件
4.创建HttpApi,网关项目引入
5.修改网关模块类配置
基本概念
:
上游Upstream:请求地址
下游Downstream:需要被路由的服务地址
1.通过Nuget包安装
2.添加中间件并添加到依赖注入容器
context.Services.AddOcelot(context.Services.GetConfiguration());
app.UseOcelot().Wait();
3.网关项目添加不同微服务的xxx.HttpApi项目的引用,然后在模块类中引入依赖
[DependsOn(
typeof(AbpAutofacModule),
typeof(OrderHttpApiModule),
typeof(ProductHttpApiModule)
)]
public class InternalGatewayModule : AbpModule
{ }
4.在不同服务的xxx.HttpApi项目中添加控制器,然后禁用
AppService中的接口远程功能,AppService做服务层被控制器调用
5.配置网关项目中的appsettings
配置文件
"Routes": [
{
//下游微服务的信息
"DownstreamPathTemplate": "/api/OrderService/{everything}",
//微服务启动用的http这里就用http
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 44361
}
],
//调用网关的格式以及支持的方法
"UpstreamPathTemplate": "/api/OrderService/{everything}",
"UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ]
},
{
"DownstreamPathTemplate": "/api/ProductService/{everything}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 44372
}
],
"UpstreamPathTemplate": "/api/ProductService/{everything}",
"UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ]
}
],
"GlobalConfiguration": {
//网关的地址
"BaseUrl": "https://localhost:44329"
},
6.为防止网关Swagger映射不到微服务中的控制器,所以需要在网关模块类依赖注入中中修改swagger的配置,然后取消终结点中间件配置
,然后运行
context.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "InternalGateway.Services", Version = "v1" });
c.DocInclusionPredicate((docName,description)=>true);
c.CustomSchemaIds(t=>t.FullName);
});
//注释
//app.UseEndpoints(endpoints =>
//{
// endpoints.MapControllers();
//});
2.负载均衡
上面搭建基本框架的例子是单个实例通过网关来路由。在现实中其实肯定不是单个实例承载请求,那么Ocelot网关是如何应对多实例的请求转发呢?
我们知道多实例肯定涉及负载均衡,Ocelot中间件目前为我们提供了2种
负载均衡的算法
,第一种是轮询(RoundRobin),第二种是最小连接数(LeastConnection)
1.在这里我们再开启一个实例,这时同时运行2个实例端口分别为44361和44362
2.修改网关的配置文件appsettings.json,添加一个新的实例
"DownstreamHostAndPorts": [
{
//实例1
"Host": "localhost",
"Port": 44361
},
{
//实例2
"Host": "localhost",
"Port": 44362
}
],
3.添加负载均衡算法节点,选择轮询算法
//负载均衡,轮询算法
"LoadBalancerOptions": {
"Type": "RoundRobin"
}
//负载均衡,最小活跃数
//"LoadBalancerOptions": {
//"Type": "LeastConnection"
//}
3.自定义随机负载均衡算法
由于Ocelot中间件只提供了负载均衡的2种算法,如果需要其他的算法支持,就需要我们自己实现了,假设我们需要扩展一个简单的随机负载均衡的算法,需要怎么做呢?
1.实现Ocelot中负载均衡算法的接口
ILoadBalancer
2.写一个随机算法,然后通过依赖注入加入到Ocelot中
3.修改配置文件,重启网关
1.创建一个继承自ILoadBalancer
类名为RandomLoadBalancer
的随机负载均衡类
2.在Lease方法中实现随机算法,
public class RandomLoadBalancer : ILoadBalancer
{
private readonly Func<Task<List<Service>>> _services;
private readonly object _lock = new object();
private int _last;
public RandomLoadBalancer(Func<Task<List<Service>>> services)
{
_services = services;
}
//随机算法
public async Task<Response<ServiceHostAndPort>> Lease(HttpContext httpContext)
{
var service = await _services();
lock (_lock)
{
Random random = new Random();
int randomNum = random.Next(service.Count);
var next = service[randomNum];
return new OkResponse<ServiceHostAndPort>(next.HostAndPort);
}
}
public void Release(ServiceHostAndPort hostAndPort)
{
throw new System.NotImplementedException();
}
}
3.首先添加到容器,然后修改appsettings.json中负载均衡节点
添加到容器
var func = new Func<IServiceProvider, DownstreamRoute, IServiceDiscoveryProvider, RandomLoadBalancer>((serviceProvider, Router, serviceDiscover) =>
{
return new RandomLoadBalancer(serviceDiscover.Get);
});
// 添加到IOC容器
context.Services.AddOcelot(context.Services.GetConfiguration())
.AddCustomLoadBalancer<RandomLoadBalancer>(func);
修改配置
//负载均衡,随机算法
"LoadBalancerOptions": {
"Type": "RandomLoadBalancer"
}
4.限流
有时候请求过大,我们的服务器承载能力有限,我们需要适当控制客户端的请求,在Ocelot中可以采用限流的策略
来控制请求的流入
1.配置appsettings.json文件,添加限流节点
"RateLimitOptions": {
//白名单
"ClientWhitelist": [],
//启用限流
"EnableRateLimiting": true,
//10秒最多1次请求
"Period": "10s",
"PeriodTimespan": 1,
"Limit": 1
}
5.熔断
熔断的意思是停止将请求转发到下游服务。当下游服务已经出现故障的时候再请求也是无功而返,并且增加下游服务器和API网关的负担。当有大量请求进入,导致服务宕机,此时后续大量请求依然流入,导致堆积产生问题,这个时候我们就应该使用Ocelot的熔断机制来避免服务宕机导致的后续问题。在Ocelot网关中主要使用Pollly
来实现熔断机制的.
1.通过Nuget引入Ocelot.Provider.Pollly
2.添加Polly到容器
context.Services.AddOcelot(context.Services.GetConfiguration())
.AddCustomLoadBalancer<RandomLoadBalancer>(func)
.AddPolly();
3.在appsettings.json中配置熔断机制
//熔断
"QoSOptions": {
//如果系统出现3个异常
"ExceptionsAllowedBeforeBreaking": 3,
//熔断时间500毫秒
"DurationOfBreak": 500,
//5秒钟超时熔断
"TimeoutValue": 5000
}
6.路由缓存
Ocelot可以对下游请求结果进行缓存 ,目前缓存的功能还不是很强大。它主要是依赖于CacheManager 来实现的,通过Nuget下载依赖,然后只需要在appsettings.json路由下添加以下配置就可以完成。
"FileCacheOptions":
{
"TtlSeconds": 15,
"Region": "somename"
}