微服务网关需求分析与设计

一、概念

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

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

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

微服务网关需求分析与设计插图

二、网关功能性设计

1、路由

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

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

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

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

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

  • 基于数据库:即将路由规则配置到数据库中,当网关收到请求后,从数据库中查询规则进行规则匹配。根据匹配到的规则进行路由。考虑到性能,可以缓存规则,例如缓存到redis中。当修改配置后,需要将修改的数据刷到缓存中。此方式需要实现数据库与缓存的同步逻辑,提供操作界面,需要一定的开发量。
  • 基于配置文件:即将路由规则配置到配置文件中,网关启动时直接加载即可。普通的配置文件方式无法动态处理配置,每次修改后都需要启动网关,比较麻烦。对于微服务架构来说,一般会有配置服务器,可以基于配置服务器来实现配置的实时生效。相对于前一种方法,可以基于微服务基础设施来实现,降低了一定的开发量。

2、负载均衡

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

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

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

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

一般负载均衡算法有:

  • 随机算法:从多个服务中,随机选择一个服务来处理请求。此算法的问题是,实际无法做到负载均衡,极端情况下可能会导致所有请求都由同一个服务进行处理。且对于有状态的服务,对状态的管理会比较麻烦。
  • 加权随机:同随机算法,不同之处是每个服务的权重不同。比如有的服务器性能较好,则可以提高权重,能够处理较多的请求;有的服务器性能较差,则可以降低权重,处理较少的请求。
  • 轮询算法:对服务进行排序,将请求按顺序发送给对应的服务来处理。假设有两个服务A,B,第一个请求由A处理,第二个请求则由B处理,第三个请求还由A处理,以次类推。对于有状态的服务,轮询算法对状态处理也比较麻烦。
  • 加权轮询:同加权轮询,不同之处是每个服务的权重不同。比如还以上面的例子,A,B权重2:1,则第一个请求A处理,第二个请求还是A处理,第三个请求B处理,第四个请求A处理,以次类推。
  • 最小连接算法:根据服务的连接数来判断请求由哪个服务来处理,选择当前连接数最少的服务来处理请求。此算法需要维护每个服务的连接数,比较复杂,不推荐使用。
  • 源地址hash:根据请求地址取hash,然后对服务数量取模,由对应的服务来处理对应的请求。此算法可以保证相同用户的请求由同一个服务来处理,可以保障服务端状态。

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

  1. 首先,不需要处理随机、轮询这种算法需要处理的服务端Session共享的问题
  2. 其次,实现简单
  3. 最后,考虑服务的变动不会太频繁,前期用户量也不会很大,使用源地址hash的性价比最高

3、聚合服务

「路由」处理的是一个请求直接由对应服务来处理的场景。微服务中每个服务提供的是系统的一部分功能,所以可能一个完整的功能需要由多个微服务来提供服务,这可能就需要客户端为了完成某个功能发送多次请求。这会导致几个问题:

  • 多次的网络请求会影响系统性能
  • 客户端需要调用多个服务,对于前端开发人员来说,开发体验不够友好

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

服务聚合有如下优缺点:

1)优点:

  • 解耦:服务聚合封装了应用的内部结构,客户端直接和网关通信,而不必关注特定的服务。当对应某个服务的接口发生了改变,客户端不需要调整,在网关进行调整即可
  • 针对性优化:可以对每种客户端提供特定的优化API,例如:某个系统有PC和手机端,都有订单详情页面,但是手机端需要的信息要比手机端少很多,此时可以通过网关针对PC和手机端提供不同的API。对于手机端可以提供一个简化的API接口,提高访问性能。
  • 简化客户端逻辑:原来需要客户端发送多个请求才能完成的功能,可以通过一个请求来完成,降低了客户端的开发复杂度。
  • 性能优化:客户端发送的请求是公共网络请求,聚合服务后,公共网络请求变成了内部网络请求,性能相对提高了一些。

2)缺点:

  • 需要开发:针对每个聚合服务API都需要进行开发,增加了额外的开发、测试、部署、管理成本。
  • 影响高可用:聚合服务会从两个方面影响网关的可用性
    • 由于需要发布新接口,需要频繁发布网关。上面说了,网关是系统的入口,频繁的发布可能会导致系统的可用性下降。需要考虑平滑发布或接口的热发布,这又增加了网关本身的开发难度。
    • 同时,由于网关中包含了请求逻辑,代码逻辑错误可能也会降低网关的可用性。对于网关代码需要做较多的防御性编程保障。

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

聚合服务有两种方案:

  • GraphQL:一种用于 API 的查询语言。使用GraphQL有三种可选方案
    • 在网关前增加一个聚合服务Server,基于GraphQL来实现服务聚合(也可以使用编码的形式来处理,此服务主要是IO密集型操作,故可以使用擅长IO密集型操作的技术,比如nodeJs,golang)
    • 直接在网关中使用GraphQL来进行服务聚合,此方式需要重启网关
    • 网关后增加聚合服务层,用于组装聚合请求
  • 编码:在网关层进行服务请求的处理,针对需要聚合的服务构建微服务请求,将获得的结果构建为最终结果返回。此方案需要编码,发布。对于需要频繁发布的聚合服务,也可以考虑独立「聚合服务」,避免频繁的发布网关,影响系统稳定性。

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

4、认证授权

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

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

  • 操作权限:用户在系统中的任何动作、交互都是操作权限,如增删改查等。
  • 数据权限:一般系统,都有数据私密性的要求,即哪些人可以看到哪些数据,不可以看到哪些数据。例如:不同租户下的用户只能看到对应租户下的数据;相同租户下的不同角色看到的数据也有差异,比如:普通角色只能看自己的薪资信息,会计角色可以看到所有人的薪资信息。

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

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

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

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

微服务网关需求分析与设计插图2

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

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

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

三、过载保护

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

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

1、流量控制

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

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

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

一般的流量控制模式有:

  • 控制并发,即限制并发的总数量(比如数据库连接池、线程池)
  • 控制速率,即限制并发访问的速率(如nginx的limitconn模块,用来限制瞬时并发连接数)
  • 控制单位时间窗口内的请求数量(如Guava的RateLimiter、nginx的limitreq模块,限制每秒的平均速率)
  • 控制远程接口调用速率
  • 控制MQ的消费速率
  • 根据网络连接数、网络流量、CPU或内存负载等来限流。

对于微服务场景来说,控制速率是比较合适的流量控制方案。

通常情况下,使用令牌桶算法来实现访问速率的控制,常用的令牌桶算法有两种:

  • 漏桶算法:水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出。可以看出漏桶算法能强行限制数据的传输速率,但是某些情况下,系统可能需要允许某种程度的突发访问量,此时可以使用令牌桶算法。
  • 令牌桶算法:系统会以一个恒定的速度向桶里放入令牌。如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。

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

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

2、熔断

  • 当一个服务对外无响应或者响应时间很长时,此种情况下可能会导致请求的大量积压,继而影响整个系统对外提供服务。熔断可以避免此类问题的发生。
  • 当一个服务对外无响应或者响应时间过长时,对该服务进行熔断操作。即对该服务的请求立即返回特定的结果,避免请求积压。等一段时间后,恢复服务对外提供服务。如果服务还是无法对外服务,则再次触发熔断。

服务熔断的实现思路:

  • 调用失败次数累积达到了阈值(或一定比例)则启动熔断机制
  • 此时对调用直接返回错误。待达到设置的时间后(这个时间一般设置成平均故障处理时间,也就是MTTR),进入半熔断状态
  • 此时允许定量的服务请求,如果调用都成功(或一定比例)则认为恢复了,关闭熔断;否则认为还没好,继续熔断

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

3、服务升降级

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

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

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

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

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

4、缓存

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

  • 减少了请求链路
  • 降低了系统的响应时间
  • 降低了微服务的负载

同时也带来了如下劣势:

  • 需要同步缓存与接口结果,增加了开发难度
  • 需要管理缓存,增加运维成本

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

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

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

  • 网关通过加载缓存模块,根据请求URL和参数解析,从缓存中查询数据
  • 如果缓存命中(缓存有效期内),那么直接返回结果
  • 如果缓存未命中(缓存失效或者未缓存),那么请求目标服务
  • 请求结果返回网关
  • 网关缓存请求结果

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

5、服务重试

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

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

  • 配置:即需要配置哪些接口需要进行重试,重试几次
  • 执行:针对配置进行重试
  1. 对于配置来说,需要配置请求的超时时间、单次请求的超时时间、重试次数,注意单次请求的超时时间*重试次数要小于请求的超时时间,否则会影响服务重试逻辑。同时,也需要考虑配置的动态生效,以保障网关的稳定性。
  2. 对于执行来说,根据配置的次数来进行处理即可。

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

6、日志

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

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

7、管理

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

  • 路由配置界面
  • 服务升降级界面
  • 服务调用统计界面
  • 服务发布管理界面
  • ……

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

四、网关非功能性设计

1、安全性

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

2、高性能

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

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

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

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

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

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

3、高可用

网关作为整个系统的入口,一旦发生问题,将造成这个系统的不可用。所以必须保障网关的高可用,可用性需要达到4个9以上,即99.99%以上。尽量达到5个9,即99.999%。也就是说需要保障网关全年故障时间在50分钟以内,最好能达到5分钟以内,保证网关可用性不会影响整个系统的可用性。

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

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

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

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

4、扩展性

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

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

  • 接受请求
  • 路由并转发请求
    • 如果是直接路由转发,则将请求直接转发给目标服务
    • 如果是聚合服务,则可能分发多个请求到各个目标服务
  • 接受服务的返回数据并返回给请求者
    • 如果是直接路由转发,则直接将结果进行返回
    • 如果是聚合服务,则等待所有服务返回结果后,组装结果数据后再返回
  • 错误处理
    • 统一的错误处理,例如服务请求错误返回统一的错误
    • 对于聚合服务,如果部分请求错误,根据业务需求决定是返回统一请求错误还是组合部分结果返回

对于此类请求的扩展,主要是基于过滤器/拦截器来实现。一般拦截器可以分为两大类:

  • 全局拦截器,即对所有请求都进行拦截处理,例如安全校验、日志记录等
  • 业务拦截器,即为了某些业务逻辑,针对符合特定规则的请求进行拦截处理。

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

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

  • 通过接口调用的方式来处理
  • 基于配置服务器的方式来处理

5、伸缩性

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

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

  • 用户在登录页完成登录操作后,服务端会生成一个登录用户信息,缓存起来,同时设置失效时间。返回给前端对应的key作为登录token凭证。
  • 用户后续的每次请求里会带着这个token信息,服务端根据token从缓存中获取登录用户信息,进行校验,校验通过就认为是合法用户,执行请求操作。否则就拒绝操作。
  • 对于访问鉴权流程类似,服务端根据token从缓存中获取登录用户信息,根据用户角色、当前访问的接口,判定当前用户是否有权限访问该接口,如果有权限则执行请求操作,否则就拒绝操作。

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

6、服务监控

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

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

对于微服务监控目前市面上有较完善的项目,例如:SkyWalking,Pinpoint。可以基于这些项目快速搭建一个服务监控系统。对于定制化需求,可以进行二次开发。同时可以基于ELK对日志进行收集分析,方便快速的定位问题。

五、需求优先级

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

  • 路由
  • 负载均衡
  • 认证授权
  • 过载保护

同时需要保障:

  • 高性能
  • 高可用
  • 伸缩性

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

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

发表评论