微服务网关!从需求,设计到实现,微服务网关选型

4747 375 2022-08-30


上文讲述了微服务网关!从需求,设计到实现,微服务网关选型。

微服务网关——需求篇

概念

API Gateway(API GW / API 网关),顾名思义,是企业 软件系统在系统边界上提供给外部访问内部接口服务的统一入口。网关并不是微服务所特有的,实际上网关在微服务之前就已经存在很久了,例如银行、证券等领域常见的前置机系统,它实际就是一个网关。

API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理、流量控制、日志、重试、熔断等。

在微服务场景下,网关的功能基本没有变化,即网关是微服务的入口,处理所有的非业务功能。

image.png

网关功能性需求

路由

对于微服务架构来说,一般都是由多个微服务共同对外提供服务,每个服务对外提供部分接口,这些接口提供的功能,共同组成完整的可用的系统功能。

当用户发送一个请求后,对于传统的单体架构来说,因为只有一个服务来对外提供服务(负载均衡见下文),所以根据请求的地址即可定位到对应的服务。但是对于微服务来说,因为有多个服务对外提供服务,所以系统需要有能力分辨出对应的请求应该委托给哪个具体的服务来处理,这就是路由的功能。

网关作为系统的入口,需要提供路由功能。同时考虑到,网关是整个系统的入口,一旦网关停止服务,则整个系统都无法对外提供服务。所以网关不能够频繁的启停,故路由模块需要能够动态的配置规则。

负载均衡

一般情况下,为了可用性,每个服务都需要做集群部署,即每个服务至少需要部署两个实例对外提供服务,避免单个实例时,由于服务本身的问题,导致该实例无法对外提供服务。

当一个服务进行了集群部署后,请求来访问时,需要确定由哪个服务来处理该请求,即负载均衡。

注意,此处「确定处理请求的服务」的逻辑和上面的路由逻辑不是一个概念。路由是根据请求来匹配到目标服务,此处是从多个不同功能的服务中确定服务;而负载均衡是在其之后,当匹配了目标服务后,由于有多个目标实例,需要从中选择一个来处理,此处是从多个相同功能的服务中选择一个来处理该请求。

对于网关来说,需要提供负载均衡的功能,至于是否需要支持动态负载均衡算法的调整,考虑到负载均衡算法确定后,一般不会变化,所以不需要动态调整负载均衡算法的功能。至于是否需要支持多种负载均衡算法,可以视具体情况而定。

聚合服务

上面的「路由」处理的是一个请求直接由对应服务来处理的场景。

前面说了,微服务中每个服务提供的是系统的一部分功能,所以可能一个完整的功能需要由多个微服务来提供服务,这可能就需要客户端为了完成某个功能发送多次请求。这会导致几个问题:

  • 多次的网络请求会影响系统性能

  • 客户端需要调用多个服务,对于前端开发人员来说,开发体验不够友好

所以,网关需要提供「聚合服务」的功能,即客户端只需要发送一个请求到网关,网关针对该请求,向多个目标微服务发送请求,将请求结果整合后返回给客户端。

服务聚合有如下优缺点:

优点:

  • 解耦:服务聚合封装了应用的内部结构,客户端直接和网关通信,而不必关注特定的服务。当对应某个服务的接口发生了改变,客户端不需要调整,在网关进行调整即可

  • 针对性优化:可以对每种客户端提供特定的优化API,例如:某个系统有PC和手机端,都有订单详情页面,但是手机端需要的信息要比手机端少很多,此时可以通过网关针对PC和手机端提供不同的API。对于手机端可以提供一个简化的API接口,提高访问性能。

  • 简化客户端逻辑:原来需要客户端发送多个请求才能完成的功能,可以通过一个请求来完成,降低了客户端的开发复杂度。

  • 性能优化:客户端发送的请求是公共网络请求,聚合服务后,公共网络请求变成了内部网络请求,性能相对提高了一些。

缺点:

  • 需要开发:针对每个聚合服务API都需要进行开发,增加了额外的开发、测试、部署、管理成本。

  • 影响高可用:聚合服务会从两个方面影响网关的可用性

    • 由于需要发布新接口,需要频繁发布网关。上面说了,网关是系统的入口,频繁的发布可能会导致系统的可用性下降。需要考虑平滑发布或接口的热发布,这又增加了网关本身的开发难度。

    • 同时,由于网关中包含了请求逻辑,代码逻辑错误可能也会降低网关的可用性。对于网关代码需要做较多的防御性编程保障。

具体哪些接口需要进行聚合,哪些直接进行委托,需要视具体接口而定。故网关,需要支持聚合服务的可编码功能。同时为了网关的稳定性,最好能支持聚合服务的动态发布。

认证授权

网关作为系统的入口,需要做好安全防护,否则后端的所有服务都会存在安全隐患。除了基本的网络安全防护外(算法签名,SSL 加密等),网关需要处理整个系统的认证(Authentication)和授权(Authorization)。

权限是资源的集合,在微服务里资源可以认为就是对外提供的接口服务。具体的权限配置上,可以将权限分为:操作权限和数据权限。

操作权限:用户在系统中的任何动作、交互都是操作权限,如增删改查等。

数据权限:一般系统,都有数据私密性的要求,即哪些人可以看到哪些数据,不可以看到哪些数据。例如:不同租户下的用户只能看到对应租户下的数据;相同租户下的不同角色看到的数据也有差异,比如:普通角色只能看自己的薪资信息,会计角色可以看到所有人的薪资信息。

如果对数据权限细化的话,还可以细分出一个「页面权限」:所有系统都是由一个个的页面组成,页面再组成模块,用户是否能看到这个页面的菜单、是否能进入这个页面就称为页面权限。

另外,操作权限还可以细化一个「菜单权限」:即哪些用户可以看到/操作哪些菜单。

对于网关来说,需要提供较完善的「认证」和「授权」功能,以保障整个系统的安全。

过载保护

系统在设计之初就会有一个预估容量,超过系统所能承受的容量阈值称为过载。长时间超过系统能承受的容量阈值,系统可能会被压垮,最终导致整个服务不可用。

为了避免这类情况的发生,需要对系统进行过载保护。一般方式有:流量控制、熔断和服务升降级。

流量控制

流量控制的目的是通过对「并发访问请求数量进行控制」或者「一个时间窗口内的的请求数量进行控制」来保护系统,一旦达到控制速率则可以拒绝服务、排队或等待。

流量控制可以针对整个系统,也可以针对单个接口来进行控制。

网关需要控制单位时间内接口允许被调用次数,以保护后端服务,实现用户分级。 可以根据接口的重要程度来配置不同流控,从而保障重要业务的稳定运行;支持用户、应用和例外流控,可以根据用户的重要性来配置不同流控,从而可以保证大用户的权益; 流控粒度:分钟、小时、天。

熔断

当一个服务对外无响应或者响应时间很长时,此种情况下可能会导致请求的大量积压,继而影响整个系统对外提供服务。熔断可以避免此类问题的发生。

当一个服务对外无响应或者响应时间过长时,对该服务进行熔断操作。即对该服务的请求立即返回特定的结果,避免请求积压。等一段时间后,恢复服务对外提供服务。如果服务还是无法对外服务,则再次触发熔断。

服务升降级

上面的流量控制和熔断都是相对比较「公平」的方法,主要是为了保证系统的可用性。在系统过载的情况下,无差别的对待所有的服务/接口。

不过对于一个系统来说,有些服务是核心服务而有些服务是非核心服务,对于核心服务来说,即使在系统过载的情况下也不能拒绝对外服务,否则这个系统实际就失去了它原有的价值。这个时候就需要非核心业务为核心业务让路,即在系统过载的时候,非核心服务让出系统资源,即服务降级,保障核心服务能稳定的对外提供服务。待系统负载正常后,再恢复非核心服务。

缓存

对于经常调用的接口,且结果基本不会出现变化的接口,可以对这些接口进行缓存。缓存后的接口,由于请求不会到达目标服务端,可以给系统带来如下好处:

  • 减少了请求链路

  • 降低了系统的响应时间

  • 降低了微服务的负载

同时也带来了如下劣势:

  • 需要同步缓存与接口结果,增加了开发难度

  • 需要管理缓存,增加运维成本

在后端并发和处理能力不够的情况下,将缓存前置来提供更好的服务,而且是从网关层统一处理,可简化后端服务处理的复杂度。

服务重试

对于某些服务,可能会由于某些原因导致服务短时间内没有响应,例如网路波动。当出现这些情况的时候,默认情况下客户端会直接收到错误消息。对于某些服务,可以通过重试的方式来降低/避免此类问题。即如果某次请求,对应的服务在规定时间内,没有得到响应,则自动再尝试一次/n次,如果成功则返回结果。如果多次尝试后,依然失败,则再返回错误消息。

服务重试在某些情况下能提高系统的可用性。

日志

日志记录是对整个微服务的要求,需要记录请求访问日志,便于后期对请求访问的统计分析、问题定位。

管理

对于上面的功能,为了方便操作,最好能提高管理界面。例如:

  • 路由配置界面

  • 服务升降级界面

  • 服务调用统计界面

  • 服务发布管理界面

  • ......

网关的非功能性需求

安全性

系统把服务暴露给外部使用时,首先要确保服务使用的安全,防止外部的恶意访问对业务的影响,特别是涉及交易方面的服务,更是要全面考虑安全性。为确保安全,需要考虑在通讯链路的建立、通讯数据的加密、数据的完整性、不可抵赖性等方面的安全。

性能

网关作为整个系统的入口,所有的请求都会先经过网关,或由网关直接处理、或从缓存获取数据、或转发给后面的微服务进行具体的业务处理、或转换为多个请求由服务处理后再整合结果。

可以看出,网关是整个系统中访问压力最大的组件,如果设计不当,无法保证高性能,则很容易成为整个系统的瓶颈。同时,为了保障系统容量,则可能需要投入大量的硬件设备,通过横向扩容的方式来提高网关层的容量。这无疑增加了系统的投入成本。

所以保障网关的高性能,是保证整个系统高性能的前提条件。

高可用

网关作为整个系统的入口,一旦发生问题,将造成这个系统的不可用。所以必须保障网关的高可用,可用性需要达到4个9以上,即99.99%以上。尽量达到5个9,即99.999%。

也就是说需要保障网关全年故障时间在50分钟以内,最好能达到5分钟以内,保证网关可用性不会影响整个系统的可用性。

扩展性

前面说到,网关需要提供例如日志、安全、负载均衡策略、鉴权等功能。对于这些功能,需要能方便的扩展,以适应业务的不断发展。同时由于这些功能会随着业务逻辑或规模的变化,不断进行强化与调整。以及系统的可用性考虑,建议网关层能提供热部署机制,使得可以灵活地进行这些调整和变化,而不用频繁对网关进行改动,确保网关的稳定性。

伸缩性

上面说了,网关是整个系统的入口,为了保证系统能快速的伸缩,网关必须要提供方便的伸缩能力,这就需要保证网关是无状态的。

服务监控

能够对服务接口进行监控,包括:调用量、调用方式、响应时间、错误率等。能够清楚的了解服务接口的运行状况和用户的行为习惯。

支持自定义报警规则,来针对异常情况进行报警,降低故障处理时间。提供可订阅的数据分析报表和智能分析。

需求优先级

网关作为整个系统的入口,核心功能包括:

  • 路由

  • 负载均衡

  • 认证授权

  • 过载保护

同时需要保障:

  • 高性能

  • 高可用

  • 伸缩性

对于聚合服务,缓存,服务重试,扩展性(热加载)功能,管理等需求,是优化性需求,提高网关的易用性,可以延后实现。

对于日志、服务监控功能作为网关的基础支撑类需求,可以通过开源项目快速实现,后续迭代的方式进行改进。

微服务网关——设计篇

前面讨论了微服务网关的需求,本文将对微服务网关进行设计。考虑到实际情况的差异,这里实际给出的是设计选项,最终设计基于实际场景来确定。

网关功能性设计

路由

一般情况下,服务对外提供的是RESTful接口,所以一般路由模块根据请求的host, url等规则转发到指定的服务。

考虑到路由规则需要频繁的修改发布,为了发布的便利性,考虑针对规则实现热发布。有几种实现方式:

基于数据库

即将路由规则配置到数据库中,当网关收到请求后,从数据库中查询规则进行规则匹配。根据匹配到的规则进行路由。

考虑到性能,可以缓存规则,例如缓存到redis中。当修改配置后,需要将修改的数据刷到缓存中。

此方式需要实现数据库与缓存的同步逻辑,提供操作界面,需要一定的开发量。

基于配置文件

即将路由规则配置到配置文件中,网关启动时直接加载即可。普通的配置文件方式无法动态处理配置,每次修改后都需要启动网关,比较麻烦。对于微服务架构来说,一般会有配置服务器,可以基于配置服务器来实现配置的实时生效。

相对于前一种方法,可以基于微服务基础设施来实现,降低了一定的开发量。

负载均衡

一般负载均衡算法有:

  • 随机算法:从多个服务中,随机选择一个服务来处理请求。此算法的问题是,实际无法做到负载均衡,极端情况下可能会导致所有请求都由同一个服务进行处理。且对于有状态的服务,对状态的管理会比较麻烦。

  • 加权随机:同随机算法,不同之处是每个服务的权重不同。比如有的服务器性能较好,则可以提高权重,能够处理较多的请求;有的服务器性能较差,则可以降低权重,处理较少的请求。

  • 轮询算法:对服务进行排序,将请求按顺序发送给对应的服务来处理。假设有两个服务A,B,第一个请求由A处理,第二个请求则由B处理,第三个请求还由A处理,以次类推。对于有状态的服务,轮询算法对状态处理也比较麻烦。

  • 加权轮询:同加权轮询,不同之处是每个服务的权重不同。比如还以上面的例子,A,B权重2:1,则第一个请求A处理,第二个请求还是A处理,第三个请求B处理,第四个请求A处理,以次类推。

  • 最小连接算法:根据服务的连接数来判断请求由哪个服务来处理,选择当前连接数最少的服务来处理请求。此算法需要维护每个服务的连接数,比较复杂,不推荐使用。

  • 源地址hash:根据请求地址取hash,然后对服务数量取模,由对应的服务来处理对应的请求。此算法可以保证相同用户的请求由同一个服务来处理,可以保障服务端状态。

对于微服务场景来说,优先选择源地址hash:

  • 首先,不需要处理随机、轮询这种算法需要处理的服务端Session共享的问题

  • 其次,实现简单

  • 最后,考虑服务的变动不会太频繁,前期用户量也不会很大,使用源地址hash的性价比最高

聚合服务

聚合服务有两种方案:

  • GraphQL:一种用于 API 的查询语言。使用GraphQL有三种可选方案

    • 在网关前增加一个聚合服务Server,基于GraphQL来实现服务聚合(也可以使用编码的形式来处理,此服务主要是IO密集型操作,故可以使用擅长IO密集型操作的技术,比如nodeJs,golang)

    • 直接在网关中使用GraphQL来进行服务聚合,此方式需要重启网关

    • 网关后增加聚合服务层,用于组装聚合请求

  • 编码:在网关层进行服务请求的处理,针对需要聚合的服务构建微服务请求,将获得的结果构建为最终结果返回。此方案需要编码,发布。对于需要频繁发布的聚合服务,也可以考虑独立「聚合服务」,避免频繁的发布网关,影响系统稳定性。

考虑到GraphQL的学习成本,以及聚合服务的量不是很多,优先考虑在网关中直接进行编码的方式。

认证授权

目前大部分系统采用的都是基于RBAC的认证授权。RBAC模型是目前主流权限控制的理论基础。

RBAC(Role-Based Access Control)即:基于角色的权限控制。通过角色关联用户,用户关联权限的方式间接赋予用户权限。如下图:

image.png

RBAC模型可分为:RBAC0、RBAC1、RBAC2、RBAC3四种。其中RBAC0是基础,也是最简单的,相当于底层逻辑,RBAC1、RBAC2、RBAC3都是以RBAC0为基础的升级。具体内容请自行Google。

考虑互联网项目对用户角色的区分没有特别的严格(相对后台管理系统),RBAC0模型就可以满足常规的权限管理系统的需求,所以选择基于RBAC0来实现认证与鉴权。

对于Java来说,主流的认证与鉴权框架是SpringSecurity和Shiro,考虑集成的便利性,选择SpringSecurity作为认证鉴权框架。

过载保护

流量控制

一般的流量控制模式有:

  • 控制并发,即限制并发的总数量(比如数据库连接池、线程池)

  • 控制速率,即限制并发访问的速率(如nginx的limitconn模块,用来限制瞬时并发连接数)

  • 控制单位时间窗口内的请求数量(如Guava的RateLimiter、nginx的limitreq模块,限制每秒的平均速率)

  • 控制远程接口调用速率

  • 控制MQ的消费速率

  • 根据网络连接数、网络流量、CPU或内存负载等来限流。

对于微服务场景来说,控制速率是比较合适的流量控制方案。通常情况下,使用令牌桶算法来实现访问速率的控制,常用的令牌桶算法有两种:

  • 漏桶算法:水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出。可以看出漏桶算法能强行限制数据的传输速率,但是某些情况下,系统可能需要允许某种程度的突发访问量,此时可以使用令牌桶算法。

  • 令牌桶算法:系统会以一个恒定的速度向桶里放入令牌。如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。

流量控制算法在确定后也是基本不需要变化的,所以对于热部署的需求不是必要的。

另外流量控制可以前置,放到接入层来处理,一般的网络接入服务,如nginx是支持流量控制的。如果前期对流量控制没有太多的定制化需求,可以考虑基于nginx来进行处理。

熔断

服务熔断的实现思路:

  • 调用失败次数累积达到了阈值(或一定比例)则启动熔断机制

  • 此时对调用直接返回错误。待达到设置的时间后(这个时间一般设置成平均故障处理时间,也就是MTTR),进入半熔断状态

  • 此时允许定量的服务请求,如果调用都成功(或一定比例)则认为恢复了,关闭熔断;否则认为还没好,继续熔断

考虑到有较成熟的开源项目,推荐直接使用开源项目来处理。

服务升降级

一种服务升降级的方案可以基于阻塞队列来实现:

  • 网关接收到请求后,进入定长的阻塞队列

  • 消费线程从消费队列中获取请求来进行处理

  • 当生产速率大于消费速率,会导致队列中请求不断增加,当请求数量超过设定的阈值时,根据配置的服务升降级规则判定当前请求是否属于可降级的服务(或者基于队列来判定),如果属于可降级的服务,则根据配置的降级逻辑对该请求进行处理(比如直接拒绝);如果请求属于不可降级的服务,则依然添加到请求队列中

考虑到有较成熟的开源项目,推荐直接使用开源项目来处理。

缓存

考虑到网关是集群化部署,所以优先使用集中式缓存方式,即网关中所有需要缓存的数据都集中进行缓存。使用常用的分布式缓存中间件即可,例如redis。

基于缓存的网关工作步骤:

  • 网关通过加载缓存模块,根据请求URL和参数解析,从缓存中查询数据

  • 如果缓存命中(缓存有效期内),那么直接返回结果

  • 如果缓存未命中(缓存失效或者未缓存),那么请求目标服务

  • 请求结果返回网关

  • 网关缓存请求结果

此处需要注意缓存常见问题:缓存雪崩、缓存击穿、缓存穿透,需要针对性的做好处理。

服务重试

对于服务重试至少需要提供两个功能:

  • 配置:即需要配置哪些接口需要进行重试,重试几次

  • 执行:针对配置进行重试

对于配置来说,需要配置请求的超时时间、单次请求的超时时间、重试次数,注意单次请求的超时时间*重试次数要小于请求的超时时间,否则会影响服务重试逻辑。同时,也需要考虑配置的动态生效,以保障网关的稳定性。

对于执行来说,根据配置的次数来进行处理即可。

逻辑实现并不复杂,不过考虑到有较成熟的开源项目,推荐直接使用开源项目来处理。

日志

应用日志记录遵循项目日志规范。对于访问日志来说,前期可以考虑在接入层实现,例如通过nginx的访问日志来实现对访问请求的记录。待后期有特定需求后再进行定制化。

管理

对于管理功能,由于是非核心需求,前期可以暂不考虑。

网关非功能性设计

高性能

传统的基于线程的并发模型(Thread-based concurrency),为每一个请求分配一个线程或进程。这种模型编程简单,可以将处理一个完整请求的代码编写在一个代码路径中。这种模型的弊端是,随着线程(进程)数的上升,操作系统在这些线程(进程)之间的频繁切换,将急剧降低系统的性能。

网关作为整个系统的入口,需要处理大量的请求,故基于线程的并发模型并不适用。需要使用Reactor模型来进行处理。

目前常用的IO框架Netty可通过配置实现上述Reactor模型,如自行开发网关,可基于Netty进行开发。

高可用设计

高可用包含了前面所说的流量控制、熔断和服务升降级。除了这些功能外,还需要提供服务的优雅上下线功能以及自身的优雅下线功能。

对于使用Java开发的项目来说,由于JVM的特性,一般需要一个预热的过程,即服务启动后,需要访问一段时间后,服务才会达到最佳状态。如果服务刚启动就接收高强度的请求,可能会导致响应时间过长、服务负载过高的问题,严重时可能导致服务被瞬间压垮。为了避免这种情况,网关可以考虑支持Slow Start特性。即经过一段时间,逐渐把请求压力增加到预设的值。

另外,当一个服务下线时,不能直接关闭服务,需要先关闭该服务的对外接口,当该服务处理完所有正在处理的请求并返回后,方可关闭服务。

对于网关自身也类似,当网关需要关闭时,不是直接结束网关进程,而是先关闭监听套接字,但是继续为当前连接的客户提供服务,当所有客户端的服务都完成后,再把进程关闭。

扩展性

网关对请求的处理,可以分为:

  • 接受请求

  • 路由并转发请求

    • 如果是直接路由转发,则将请求直接转发给目标服务

    • 如果是聚合服务,则可能分发多个请求到各个目标服务

  • 接受服务的返回数据并返回给请求者

    • 如果是直接路由转发,则直接将结果进行返回

    • 如果是聚合服务,则等待所有服务返回结果后,组装结果数据后再返回

  • 错误处理

    • 统一的错误处理,例如服务请求错误返回统一的错误

    • 对于聚合服务,如果部分请求错误,根据业务需求决定是返回统一请求错误还是组合部分结果返回

对于此类请求的扩展,主要是基于过滤器/拦截器来实现。

一般拦截器可以分为两大类:

  • 全局拦截器,即对所有请求都进行拦截处理,例如安全校验、日志记录等

  • 业务拦截器,即为了某些业务逻辑,针对符合特定规则的请求进行拦截处理。

一般来说,先执行全局拦截器,再执行为了业务逻辑编写的拦截器。不过,为了灵活性,网关最好能提供一种机制,可以较容易地调整拦截器的执行顺序。最简单的一种方法,就是给每个拦截器定义一个优先级,网关按优先级顺序依次调用各拦截器。

同时,网关也需要能方便的动态配置拦截器,即动态配置拦截器的开启与关闭、以及配置哪些拦截器针对哪些请求生效。可以通过两种方式来处理:

  • 通过接口调用的方式来处理

  • 基于配置服务器的方式来处理

伸缩性

网关层为保证高可用,易于伸缩,快速启动,需要设计成无状态的(微服务里的绝大部分服务都需要设计为无状态的)。但是,由于网关需要处理用户的认证与鉴权,势必与用户状态有关系,此处需要解耦用户状态关系。目前一般做法是基于token来进行处理:

  • 用户在登录页完成登录操作后,服务端会生成一个登录用户信息,缓存起来,同时设置失效时间。返回给前端对应的key作为登录token凭证。

  • 用户后续的每次请求里会带着这个token信息,服务端根据token从缓存中获取登录用户信息,进行校验,校验通过就认为是合法用户,执行请求操作。否则就拒绝操作。

  • 对于访问鉴权流程类似,服务端根据token从缓存中获取登录用户信息,根据用户角色、当前访问的接口,判定当前用户是否有权限访问该接口,如果有权限则执行请求操作,否则就拒绝操作。

通过此方式,保证了网关的无状态,继而保证网关的快速扩容。

服务监控

对于微服务监控目前市面上有较完善的项目,例如SkyWalking,Pinpoint。可以基于这些项目快速搭建一个服务监控系统。对于定制化需求,可以进行二次开发。

同时可以基于ELK对日志进行收集分析,方便快速的定位问题。

微服务网关——实现篇

对于网关的开发,完全自研的难度比较大,特别是IO的处理。考虑到目前市面上有比较多的成熟框架,可以基于成熟的开源框架,进行二次开发。

对于Java来说,目前Spring提供了SpringCloud Gateway(下面简称SG),SG基于SpringWebFlux,底层默认使用的是Netty框架,支持高并发请求,同时提供了一些默认的组件,支持路由、限流等功能。可以基于SG进行二次开发。

如果对网关有特别多的自定义需求,可以直接基于Netty来进行更彻底的二次开发,适配自身系统的个性化需求。

本文将基于SG来讨论网关的实现。

基于SpringCloud Gateway的网关设计

这里针对前面的需求,梳理如何基于SpringCloud Gateway来实现。关于SpringCloud Gateway本身的设计和实现,会新开一篇专门讨论。

SpringCloud Gateway整体架构

SpringCloud Gateway基于SpringWebFlux,整体架构如下图所示:

image.png

SG定义了几个概念:

  • 路由(Route):路由是网关的基本构成单元。它由一个ID、一个目标URL、一组谓词以及一组过滤器组成。当谓词判定为true时,表示请求与对应路由匹配

  • 谓词(Predicate):Java8函数式谓词。输入参数是Spring框架封装的ServerWebExchange对象。开发人员可以基于此对象来匹配HTTP请求的任意内容,比如请求头或请求参数

  • 过滤器(Filter):由特定工厂类构造的一组Spring框架提供的GatewayFilter对象。过滤器可以在请求或响应被处理前/后对其进行修改。

SG处理请求的大致流程如下:

GatewayHandlerMapping判定对应的请求是否匹配某个路由。如果匹配到某个路由,则将请求交给GatewayWebHandler处理。Handler调用一个Filter链来处理这个请求,具体执行流程如下:

  • 首先,会执行「pre」过滤器的逻辑

  • 然后执行请求处理逻辑

  • 最后再执行「post」过滤器的逻辑

SG提供了GatewayFilter和GlobalFilter两种类型的过滤器,从名字可以看出GlobalFilter是对全局生效的,而GatewayFilter是对特定请求生效的。

注意:这里的GlobalFilter是针对所有匹配了GatewayHandlerMapping的请求生效,而不是对所有进入网关的请求生效。
假设,你在网关中编写了一个Controller,但是路由配置中并没有匹配该Controller的路径,那么针对该Controller的请求并不会触发任何GlobalFilter。

SG针对一个请求的完整流程如下图所示:

image.png

可以看到,SG的扩展是基于一个个的Filter来实现的。前面提到的大部分需求也完全可以基于Filter去实现,包括但不限于路由、负载均衡、认证授权、过载保护、缓存、服务重试、日志记录等。

这里仅以限流为例,来说明SG的扩展逻辑。其它实现请自行阅读源码,或关注后续内容。

限流实现

SG提供了
RequestRateLimiterGatewayFilterFactory过滤器支持限流,同时也支持基于histrix的过载保护,直接集成使用即可,具体请见histrix文档。

这里以
RequestRateLimiterGatewayFilterFactory为例来分析SG如何基于拦截器来实现j限流的。SG中的限流是针对每个路由来单独定义的,配置内容如下:

image.png

上面的配置,配置了一个路由:

  • id为test_route

  • 路由地址到lb://test。前面的lb://表示支持负载均衡,后面的test是服务名称

  • predicates是谓词,表示当请求以/test开头时,此路由生效

  • 同时配置了一个限流拦截器,包括名称和参数。基于此配置通过RequestRateLimiterGatewayFilterFactory来构建对应的Filter。其中的参数是用来配置请求速率的,即每秒允许10个请求,允许短时间(这里是1秒)涌入20个请求。

  • 上面的配置会被构建为一个RouteLocator实例,该类根据配置构建Route、Predicates、Filter等实例。对于Filter来说,

在创建RouteLocator实例时,已创建的GatewayFilterFactory实例列表会被作为参数传入

list被转换为map,key为去除了GatewayFilterFactory后缀名的Filter类名(与配置文件中的Filter的配置匹配),value是对应的GatewayFilterFactory实例

image.png

  • 在构建Route实例时,会根据配置文件中的Filter的配置获取Filter实例设置进Route实例中,提供给后续流程使用

  • RequestRateLimiterGatewayFilterFactory核心代码如下所示:

image.png

  • 1处,获取配置参数。这里的KeyResolver是对获取请求的key逻辑的抽象。比如其中的一个实现是PrincipalNameKeyResolver,它从ServerWebExchange中获取Principal对象,并将Principal.getName()的返回值作为当前请求的key,如果值相同,则认定为同一个请求。

  • 2处,构建匿名GatewayFilter

  • 3处,基于限流器来处理限流逻辑

  • 官方提供的分布式限流方案是基于redis实现。

image.png

核心逻辑在RedisRateLimiter的isAllowed()方法中:

image.png

  • 1处,获取参数(上面已经解释过了)

  • 2处,构建lua参数,执行lua脚本

  • 3处,根据返回值判定是否允许访问

lua脚本就不贴出来了,使用的是令牌桶算法。

自定义拦截器

从上面的梳理,可以看出,要编写一个自定义的GatewayFilter的流程如下:

  • 编写一个FilterFactory类继承AbstractGatewayFilterFactory,这个类实现了GatewayFilterFactory接口,该接口提供了一个名叫name的默认方法,用于提供Factory的name,作为获取对应实例的名称。逻辑就是将对应FilterFactory类名后的「GatewayFilterFactory」去掉,例如上面的RequestRateLimiterGatewayFilterFactory,对应的配置名称就是RequestRateLimiter。

  • 在覆写的apply方法中构建对应的Filter,可以是直接返回匿名GatewayFilter,或返回对应Filter的实例

  • 如果返回的是Filter的实例,则需要编写一个实现了GatewayFilter的Filter类,实现其filter方法

  • 然后将FilterFactory、实现的Filter(如果有的话)配置给Spring管理

  • 最后,就可以在配置文件中针对需要的路由配置对应的Filter了

聚合服务

SG基于SpringWebFlux,支持编写异步RESTful接口,同时提供WebClient异步REST客户端来实现聚合服务的编写。不过由于是编码的形式,所以需要发布网关。对于集群部署的情况下,在可用性要求没有特别严格的情况下,此方式可以接受。

如果聚合服务较多且发布频繁,可以独立出聚合服务层。即基于SpringWebFlux构建微服务,使用WebClient来整合独立微服务的逻辑,网关路由至对应的服务即可。

关于WebClient的使用方法请自行搜索,这里就不赘述了。

非功能性需求

SG本身就基本符合前面提到的非功能性需求

基于SpringWebFlux的非阻塞IO模型,支持高并发。

基于分布式配置服务进行动态配置

  • 基于GatewayFilter和GlobalFilter进行扩展

  • 可以接入PinPoint的链路监控以及基于ELK的日志监控平台

    主要注意保证无状态设计!

  • 在Kubernetes中,service IP和Pod IP主要供集群内部访问使用,对于集群外部是不可见的。

  • 流量入口代理作为互联网系统的门户组件,具备众多选型:从老牌代理 HAProxy、Nginx,到微服务 API 网关 Kong、Zuul,再到容器化 Ingress 规范与实现,不同选型间功能、性能、可扩展性、适用场景参差不齐。当云原生时代大浪袭来,Envoy 这一 CNCF 毕业数据面组件为更多人所知。那么,优秀“毕业生”Envoy 能否成为云原生时代下流量入口标准组件?

  • 背景 —— 流量入口的众多选型与场景

  • 在互联网体系下,凡是需要对外暴露的系统几乎都需要网络代理:较早出现的 HAProxy、Nginx 至今仍在流行;进入微服务时代后,功能更丰富、管控能力更强的 API 网关又成为流量入口必备组件;在进入容器时代后,Kubernetes Ingress 作为容器集群的入口,是容器时代微服务的流量入口代理标准。关于这三类典型的七层代理,核心能力对比如下:

  • 从上述核心能力对比来看:

  • HAProxy&Nginx 在具备基础路由功能基础上,性能、稳定性经历多年考验。Nginx 的下游社区 OpenResty 提供了完善的 Lua 扩展能力,使得 Nginx 可以更广泛的应用与扩展,如 API 网关 Kong 即是基于 Nginx+OpenResty 实现。API 网关作为微服务对外 API 流量暴露的基础组件,提供比较丰富的功能和动态管控能力。Ingress 作为 Kubernetes 入口流量的标准规范,具体能力视实现方式而定。如基于 Nginx 的 Ingress 实现能力更接近于 Nginx,Istio Ingress Gateway 基于 Envoy+Istio 控制面实现,功能上更加丰富(本质上 Istio Ingress Gateway 能力上强于通常的 Ingress 实现,但未按照 Ingress 规范实现)。那么问题来了:同样是流量入口,在云原生技术趋势下,能否找到一个能力全面的技术方案,让流量入口标准化?

  • 一、API网关概述

  • 1、什么是API网关

  • API网关

  • (1)为啥需要API网关

  • 计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。— David Wheeler

  • API网关的观念其实和当前流行的SOA架构和微服务架构模式有关。在传统大型企业比较流行的SOA架构中,有一个企业服务总线(ESB)的概念,再ESB中融合了管理、注册、中介、编排、治理等功能,是一个访问高度频繁、功能高度集中的地方,因此常常也是性能瓶颈所在。而在微服务架构中,伴随着去中心化的理念,几乎没有EBS的的概念,分布式服务架构技术不再依赖于具体的服务中心容器技术( ESB),而是将服务寻址和调用完全分开,这样就不需要通过容器作为服务代理,在运行期实现最高效的直连调用。

  • 在微服务架构中,服务的粒度被进一步细分,各个业务服务可以被独立的设计、开发、测试、部署和管理。各个独立部署单元可以用不同的开发测试团队维护,可以使用不同的编程语言和技术平台进行设计,这就要求必须使用一种语言和平台无关的服务协议作为各个单元间的通讯方式。而REST API 由于其简单、高效、跨平台、易开发、易测试、易集成,成为了不二选择。此时如果都是采用客户端和服务器直连的话,那么此时系统就会出现大量的冗余代码和功能,维护起来工作量巨大,而且随着服务增多,出错性也大大的增加。因此一个类似综合前置的系统就产生了,这就是 API 网关(API Gateway)。API 网关作为分散在各个业务系统微服务的 API 聚合点和统一接入点,外部请求通过访问这个接入点,即可访问内部所有的 REST API 服务。

  • (2)API网关定义

  • 网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问。

  • API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。

  • 通俗的说API网关中就是做一些通用的基础设施功能。类似AOP中的横切关注点概念,把业务系统中涉及的一些通用功能(日志分析、鉴权、路由等)抽取到API网关中统一管理。API 网关不是一个典型的业务系统, 而是一个为了让业务系统更专注与业务服务本身,给API服务提供更多附加能力的一个中间层。

  • (3)API网关的四大职能

  • 请求接入:作为所有 API 接口服务请求的接入点,管理所有的接入请求;业务聚合:作为所有后端业务服务的聚合点,所有的业务服务都可以在这里被调用;中介策略:实现安全、验证、路由、过滤、流控,缓存等策略,进行一些必要的中介处理;统一管理:提供配置管理工具,对所有 API 服务的调用生命周期和相应的中介策略进行统一管理。

  • (4)API网关关注点

  • (1)开发维护简单,节约人力成本和维护成本。(2)高性能,节约设备成本,提高系统吞吐能力。(3)高可用(非常重要)(4)方便灵活地实现安全、验证、过滤、聚合、限流、监控等各种策略。

  • 二、API网关分类

  • api网关分类

  • 如上图所示,面对互联网复杂的业务系统,基本可以将API网关分成两类:流量网关和业务网关。

  • 流量网关:跟具体的后端业务系统和服务完全无关的部分,比如安全策略、全局性流控策略、流量分发策略等。流量网关的功能跟 Web 应用防火墙(WAF)非常类似。WAF一般是基于 Nginx/OpenResty 的 ngx_lua 模块开发的 Web 应用防火墙。业务网关:针对具体的后端业务系统,或者是服务和业务有一定关联性的部分,并且一般被直接部署在业务服务的前面。业务网关一般部署在流量网关之后,业务系统之前,比流量网关更靠近系统。我们大部分情况下说的 API 网关,狭义上指的是业务网关。并且如果系统的规模不大,我们也会将两者合二为一,使用一个网关来处理所有的工作

  • 三、开源API网关介绍

  • 开源api网关

  • 目前常见的开源网关大致上按照语言分类有如下几类:

  • Nginx+lua:Open Resty、Kong、Orange、Abtesting gateway 等Java:Zuul/Zuul2、Spring Cloud Gateway、Kaazing KWG、gravitee、Dromara soul 等Go:Janus、fagongzi、Grpc-gatewayDotnet:OcelotNodeJS:Express Gateway、Micro Gateway

  • 按照使用数量、成熟度等来划分,主流的有 4 个:

  • OpenRestyKongZuul/Zuul2Spring Cloud Gateway

  • 1、Nginx+Lua(Open Resty)

  • OpenResty 基于 Nginx,集成了 Lua 语言和 Lua 的各种工具库,可用的第三方模块,这样我们就在 Nginx 既有的高效 HTTP 处理的基础上,同时获得了 Lua 提供的动态扩展能力。官网是这么介绍的:

  • 项目地址:openresty.org/

  • OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

  • OpenResty 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

  • OpenResty 的目标是让你的 Web 服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

  • 2、kong

  • 项目地址:

  • konghq.com/github.com/kong/kong

  • Kong 基于 OpenResty,是一个云原生、快速、可扩展、分布式的微服务抽象层(Microservice Abstraction Layer),也叫 API 网关(API Gateway),在 Service Mesh 里也叫 API 中间件(API Middleware)。

  • Kong 开源于 2015 年,核心价值在于高性能和扩展性。从全球 5000 强的组织统计数据来看,Kong 是现在依然在维护的,在生产环境使用最广泛的 API 网关。

  • Kong 宣称自己是世界上最流行的开源微服务 API 网关(The World’s Most Popular Open Source Microservice API Gateway)。

  • 3、Zuul/Zuul2

  • 项目地址:github.com/Netflix/zuul

  • Zuul 是 Netflix 开源的 API 网关系统,它的主要设计目标是动态路由、监控、弹性和安全。

  • Zuul 的内部原理可以简单看做是很多不同功能 filter 的集合

  • Zuul 1.x 基于同步 IO,也是 Spring Cloud 全家桶的一部分,可以方便的配合 Spring Boot/Spring Cloud 配置和使用。在 Zuul 1.x 里,filter 的种类和处理流程可以参见下图,最主要的就是 pre、routing、post 这三种过滤器,分别作用于调用业务服务 API 之前的请求处理、直接响应、调用业务服务 API 之后的响应处理。

  • zuul1.x架构图

  • Zuul 2.x 最大的改进就是基于 Netty Server 实现了异步 IO 来接入请求,同时基于 Netty Client 实现了到后端业务服务 API 的请求。这样就可以实现更高的性能、更低的延迟。此外也调整了 filter 类型,将原来的三个核心 filter 显式命名为:Inbound Filter、Endpoint Filter 和 Outbound Filter。

  • zuul2.x架构图

  • 4、Spring Cloud Gateway

  • 项目地址:

  • github.com/spring-cloud/spring-cloud-gateway/

  • Spring Cloud Gateway 基于 Java 8、Spring 5.0、Spring Boot 2.0、Project Reactor,发展的比 Zuul 2 要早,目前也是 Spring Cloud 全家桶的一部分。

  • Spring Cloud Gateway 可以看做是一个 Zuul 1.x 的升级版和代替品,比 Zuul 2 更早的使用 Netty 实现异步 IO,从而实现了一个简单、比 Zuul 1.x 更高效的、与 Spring Cloud 紧密配合的 API 网关。

  • Spring Cloud Gateway 里明确的区分了 Router 和 Filter,并且一个很大的特点是内置了非常多的开箱即用功能,并且都可以通过 SpringBoot 配置或者手工编码链式调用来使用。

  • 5、四大开源网关框架对比

  • 对 Zuul/Zuul2/Spring Cloud Gateway 的一些功能点分析可以参考 Spring Cloud Gateway 作者 Spencer Gibb 的文章:

  • spencergibb.netlify.com/preso/detroit-cf-api-gateway-2017-03/

  • 6、四大开源网关性能对比

  • (1)实测情况是性能 SCG~Zuul2 << OpenResty ~< Kong << Direct(直连);(2)Spring Cloud Gateway、Zuul2 的性能差不多,大概是直连的40%;(3)OpenResty、Kong 差不多,大概是直连的 60-70%;(4)大并发下,例如模拟 200 并发用户、1000 并发用户时,Zuul2 会有很大概率返回出错。zuul2.x的可用性不好。

  • 以上测试用到的模拟服务和网关 demo 代码,大部分可以在这里找到:

  • github.com/kimmking/spring-cloud-gateway-demo

  • 7、开源网关的选择

  • 脱离场景谈性能,脱离业务谈架构,都是耍流氓。

  • (1)Kong 的性能非常不错,非常适合做流量网关,并且对于 service、route、upstream、consumer、plugins 的抽象,也是自研网关值得借鉴的。对于复杂系统,不建议业务网关用 Kong,或者更明确的说是不建议在 Java 技术栈的系统深度定制 Kong 或 OpenResty,主要是工程性方面的考虑。毕竟维护lua脚本的工作量和成本不低。

  • (2)pring Cloud Gateway/Zuul2 对于 Java 技术栈来说比较方便,可以依赖业务系统的一些 common jar。Lua 不方便,不光是语言的问题,更是复用基础设施的问题。另外,对于网关系统来说,性能不是差一个数量级,问题不大,多加 2 台机器就可以搞定。

  • (3)目前来看 Zuul2 的坑还是比较多的,因此作为java技术栈,比较建议使用 Spring Cloud Gateway 作为基础骨架。

  • 上文是小编为大家整理的微服务网关!从需求,设计到实现,微服务网关选型。

  • 国内(北京、上海、广州、深圳、成都、重庆、杭州、西安、武汉、苏州、郑州、南京、天津、长沙、东莞、宁波、佛山、合肥、青岛)推荐eolink。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:什么是API测试,api检测公司
下一篇:java并发编程synchronized底层实现原理
相关文章

 发表评论

暂时没有评论,来抢沙发吧~