目前的架构(使用Spring Cloud Netflix
中的Eureka
实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon
或Feign
实现服务的消费以及均衡负载;通过Spring Cloud Config
实现了应用多环境的外部化配置以及版本管理。为了使得服务集群更为健壮,使用Hystrix
的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。)还存在一些不足:
REST API
无状态的特点。从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外可续对接口访问的控制处理。为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器,它就是服务网关。
API
网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的Facade
模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由、负载均衡、校验过滤等功能之外,还需要更多能力,比如与服务治理框架的结合、请求转发时的熔断机制、服务的聚合等一系列高级功能。
API
网关是一套主要用于统一API
入口的应用组件,可以管理所有的API
,形成一个API
的入口。
API
API
入口在Spring Cloud
中提供了基于Netflix Zuul
实现的API
网关组件——Spring Cloud Zuul
。
Spring Cloud Zuul
提供了认证、鉴权、限流、动态路由、监控、弹性、安全、负载均衡、协助单点压测、静态响应等边缘服务的框架。
首先,对于路由规则与服务实例的维护问题。Spring Cloud Zuul
通过与Spring Cloud Eureka
进行整合,将自身注册为Eureka
服务治理下的应用,同时从Eureka
中获得了所有其他微服务的实例信息。这样的设计非常巧妙地将服务治理体系中维护的实例信息利用起来,使得维护服务实例的工作交给了服务治理框架自动完成,不再需要人工介入。而对于路由规则的维护,Zuul
默认会将通过以服务名作为ContextPath
的方式来创建路由映射,大部分情况下,这样的默认设置已经可以实现我们大部分的路由需求,除了一些特殊情况(比如兼容一些老的URL
)还需要做一些特别的配置。但是相比于之前架构下的运维工作量,通过引入Spring Cloud Zuul
实现API
网关后,已经能够大大减少了。
其次,对于类似签名校验、登录校验在微服务架构中的冗余问题。理论上来说,这些校验逻辑在本质上与微服务应用自身的业务并没有多大的关系,所以它们完全可以独立成一个单独的服务存在,只是它们被剥离和独立出来之后,并不是给各个微服务调用,而是在API
网关服务上进行统一调用来对微服务接口做前置过滤,以实现对微服务接口的拦截和校验。Spring Cloud Zuul
提供了一套过滤器机制,它可以很好地支持这样的任务。开发者可以通过使用Zuul
来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然就返回错误提示。通过这样的改造,各个业务层的微服务应用就不再需要非业务性质的校验逻辑了,这使得我们的微服务应用可以更专注于业务逻辑的开发,同时微服务的自动化测试也变得更容易实现。
微服务架构虽然可以将我们的开发单元拆分得更为细致,有效降低了开发难度,但是它所引出的各种问题如果处理不当会成为实施过程中的不稳定因素,甚至掩盖掉原本实施微服务带来的优势。所以,在微服务架构的实施方案中,API
网关服务的使用几乎成为了必然的选择。
所谓的传统路由配置方式就是在不依赖于服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现API
网关对外部请求的路由。
没有Eureka
等服务治理框架的帮助,我们需要根据服务实例的数量采用不同方式的配置来实现路由规则。
zuul.routes..path
与zuul.routes..url
参数对的方式进行配置,比如:zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.url=http://localhost:8080/
该配置实现了对符合/user-service/**
规则的请求路径转发到http://localhost:8080/
地址的路由规则。比如,当有一个请求http://localhost:5555/user-service/hello
被发送到API
网关上,由于/user-service/hello
能够被上述配置的path
规则匹配,所以API
网关转发请求到http://localhost:8080/hello
地址。
zuul.routes..path
与zuul.routes..serviceId
参数对的方式进行配置,比如:zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
# 由于zuul.routes..serviceId指定的是服务名称,默认情况下Ribbon会根据服务发现机制来获取配置服务名对应的实例清单。
# 但是,该示例并没有整合类似Eureka之类的服务治理框架,所以需要将该参数设置为false,否则配置的serviceId获取不到对应实例的清单
ribbon.eureka.enabled=false
# 该参数内容与zuul.routes..serviceId的配置相对应,开头的user-service对应了serviceId的值,
# 这两个参数的配置相当于在该应用内部手工维护了服务与实例的对应关系
user-service.ribbon.listOfServers=http://localhost:8080/,http://localhost:8081/
该配置实现了对符合/user-service/**
规则的请求路径转发到http://localhost:8080/
和http://localhost:8081/
两个实例地址的路由规则。它的配置方式与服务路由的配置方式一样,都采用了zuul.routes.
与zuul.routes.
参数对的映射方式,只是这里的serviceId
是由用户手工命名的服务名称,配合service.ribbon.listOfServers
参数实现服务与实例的维护。由于存在多个实例,API
网关在进行路由转发时需要实现负载均衡策略,于是这里还需要Spring Cloud Ribbon
的配合。由于在Spring Cloud Zuul
中自带了对Ribbon
的依赖,所以我们只需做一些配置即可,比如上面示例中关于Ribbon
的各个配置。
不论是单实例还是多实例的配置方式,我们都需要为每一对映射关系指定一个名称,也就是上面配置中的
,每一个
对应了一条路由规则。每条路由规则都需要通过path
属性来定义一个用来匹配客户端请求的路径表达式,并通过url
或serviceId
属性来指定请求表达式映射具体实例地址或服务名。
Spring Cloud Zuul
通过与Spring Cloud Eureka
的整合,实现了对服务实例的自动化维护,所以在使用服务路由配置的时候,我们不需要像传统路由配置方式那样为serviceId
指定具体的服务实例地址,只需要通过zuul.routes.
与zuul.routes.
参数对的方式进行配置即可。
比如下面的示例,它实现了对符合/user-service/**
规则的请求路径转发到名为user-service
的服务实例上去的路由规则。其中
可以指定为任意的路由名称。
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
对于面向服务的路由配置,除了使用path
与serviceId
映射的配置方式之外,还有一种更简洁的配置方式:zuul.routes.
,其中
用来指定路由的具体服务名,
用来配置匹配的请求表达式。比如下面的例子,它的路由规则等价于上面通过path
与serviceId
组合使用的配置方式。
zuul.routes.user-service=/user-service/**
传统路由的映射方式比较直观且容易理解,API
网关直接根据请求的URL
路径找到最匹配的path
表达式,直接转发给该表达式对应的url
或对应serviceId
下配置的实例地址,以实现外部请求的路由。
Zuul
巧妙地整合了Eureka
来实现面向服务的路由。实际上,我们可以直接将API
网关也看作Eureka
服务治理下的一个普通微服务应用。它除了会将自己注册到Eureka
服务注册中心上之外,也会从注册中心获取所有服务以及它们的实例清单。所以,在Eureka
的帮助下,API
网关服务本身就已经维护了系统中所有serviceId
与实例地址的映射关系。当有外部请求到达API
网关的时候,根据请求的URL
路径找到最佳匹配的path
规则,API
网关就可以知道要将该请求路由到哪个具体的serviceId
上去。由于在API
网关中已经知道serviceId
对应服务实例的地址清单,那么只需要通过Ribbon
的负载均衡策略,直接在这些清单中选择一个具体的实例进行转发就能完成路由工作了。
spring-cloud-starter-zuul
依赖自身就包含了对spring-cloud-starter-hystrix
和spring-cloud-starter-ribbon
模块的依赖,所以Zuul
天生就拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡功能。但是需要注意,当使用path
与url
的映射关系来配置路由规则的时候,对于路由转发的请求不会采用HystrixCommand
来包装,所以这类路由请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。因此,我们在使用Zuul
的时候尽量使用path
和serviceId
的组合来进行配置,这样不仅可以保证API
网关的健壮和稳定,也能用到Ribbon
的客户端负载均衡功能。
当我们为Spring Cloud Zuul
构建的API
网关服务引入Spring Cloud Eureka
之后,它为Eureka
中的每个服务都自动创建一个默认路由规则,这些默认规则的path
会使用serviceId
配置的服务名作为请求前缀。
由于默认情况下所有Eureka
上的服务都会被Zuul
自动地创建映射关系来进行路由,这会使得一些我们不希望对外开放的服务也可能被外部访问到。这个时候,我们可以使用zuul.ignored-services
参数来设置一个服务名匹配表达式来定义不自动创建路由的规则。Zuul
在自动创建服务路由的时候会根据该表达式来进行判断,如果服务名匹配表达式,那么Zuul
将跳过该服务,不为其创建路由规则。比如,设置为zuul.ignored-services=*
的时候,Zuul
将对所有的服务都不自动创建路由规则。在这种情况下,我们就要在配置文件中逐个为需要路由的服务添加映射规则(可以使用path
与serviceId
组合的配置方式,也可使用更简洁的zuul.routes.
配置方式),只有在配置文件中出现的映射规则会被创建路由,而从Eureka
中获取的其他服务,Zuul
将不会再为它们创建路由规则。
不论是使用传统路由的配置方式还是服务路由的配置方式,我们都需要为每个路由规则定义匹配表达式,也就是path
参数。在Zuul
中,路由匹配的路径表达式采用了Ant
风格定义。
Ant
风格的路径表达式使用起来非常简单,它一共有下面这三种通配符:
通配符 | 说明 |
---|---|
? | 匹配任意单个字符 |
* | 匹配任意数量的字符 |
** | 匹配任意数量的字符,支持多级目录 |
当存在多个匹配的路由规则时,匹配结果完全取决于路由规则的保存顺序。为了保证路由的优先顺序,我们需要使用YAML
文件来配置,以实现有序的路由规则。
为了更细粒度和更为灵活地配置路由规则,Zuul
还提供了一个忽略表达式参数zuul.ignored-patterns
。该参数可以用来设置不希望被API
网关进行路由的URL
表达式。该参数在使用时需要注意它的范围并不是对某个路由,而是对所有路由。所以在设置的时候需要全面考虑URL
规则,防止忽略了不该被忽略的URL
路径。
为了方便全局地为路由规则增加前缀信息,Zuul
提供了zuul.prefix
参数来进行设置。比如,希望为网关上的路由规则都增加/api
前缀,那么我们可以在配置文件中增加配置:zuul.prefix=/api
。另外,对于代理前缀会默认从路径中移除,我们可以通过设置zuul.stripPrefix=false
来关闭该移除代理前缀的动作,也可以通过zuul.routes.
来对指定路由关闭移除代理前缀的动作。
在Zuul
实现的API
网关路由功能中,还支持forward
形式的服务端跳转配置。实现方式非常简单,只需通过使用path
与url
的配置方式就能完成,通过url
中使用forward
来指定需要跳转的的服务器资源路径。
如下api-a
路由使用了本地跳转,它实现了将符合/api-a/**
规则的请求转发到API
网关中以/local
为前缀的请求上,由API
网关进行本地处理。比如,当API
网关接收到请求/api-a/hello
,它符合/api-a/**
的路由规则,所以该请求会被API
网关转发到网关的/local/hello
请求上进行本地处理。
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.url=forward:/local
要注意的是由于需要在API
网关上实现本地跳转,所以相应的我们也需要为本地跳转实现对应的请求接口。按照上面的例子,在API
网关上还需要增加一个/local/hello
的接口实现才能让api-a
路由规则生效,比如下面的实现。否则Zuul
在进行forward
转发的时候会因为找不到该请求而返回404
错误。
@RestController
public class HelloController{
@RequestMapping("/local/hello")
public String hello(){
return "Hello World Local";
}
}
我们在使用Zuul
搭建API
网关的时候,可以通过Hystrix
和Ribbon
的参数来调整路由请求的各种超时时间等配置,比如下面这些参数的设置。
参数 | 说明 |
---|---|
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds | 可以用来设置API网关中路由转发请求的HystrixCommand执行超时时间,单位为毫秒。当路由转发请求的命令执行时间超过该配置值之后,Hystrix会将该执行命令标记为TIMEOUT并抛出异常,Zuul会对该异常进行处理并返回JSON信息给外部调用方。 |
ribbon.ConnectTimeout | 用来设置路由转发请求的时候,创建请求连接的超时时间。当ribbon.ConnectTimeout的配置值小于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds配置值的时候,若出现路由请求出现连接超时,会自动进行重试路由请求,如果重试依然失败,会返回JSON信息给外部调用方。如果ribbon.ConnectTimeout的配置值大于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds配置值的时候,当出现路由请求连接超时时,由于此时对于路由转发的请求命令已经超时,所以不会进行重试路由请求,而是直接按请求命令超时处理,返回TIMEOUT的错误信息。 |
ribbon.ReadTimeout | 用来设置路由转发请求的超时时间。它的处理与ribbon.ConnectTimeout类似,只是它的超时是对请求连接建立之后的处理时间。当ribbon.ReadTimeout的配置值小于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds配置值的时候,若路由请求的处理时间超过该配置值且依赖服务的请求还未响应的时候,会自动进行重试路由请求。如果重试后依然没有获得请求响应,Zuul会返回NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED错误。如果ribbon.ReadTimeout的配置值大于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds的配置值,若路由请求的处理时间超过该配置值且依赖服务的请求还未响应时,不会进行重试路由请求,而是直接按请求命令超时处理,返回TIMEOUT的错误信息。 |
在使用Zuul
的服务路由时,如果路由转发请求发生超时(连接超时或处理超时),只要超时时间的设置小于Hystrix
的命令超时时间,那么他就会自动发起重试。但是在有些情况下,我们可能需要关闭该重试机制,那么可以通过下面的两个参数来进行设置:
# 全局关闭重试机制
zuul.retryable=false
# 指定路由关闭重试机制
zuul,routes.>.retryable=false
路由功能在真正运行时,它的路由映射和请求转发都是由几个不同的过滤器完成的。其中,路由映射主要通过pre
类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址;而请求转发的部分则是由route
类型的过滤器来完成,对pre
类型过滤器获得的路由地址进行转发。所以,过滤器可以说是Zuul
实现API
网关功能最为核心的部件,每一个进入Zuul
的HTTP
请求都会进过一系列的过滤器处理链得到请求响应并返回给客户端。
在Spring Cloud Zuul
中实现的过滤器必须包含4
个基本特征:过滤类型、执行顺序、执行条件、具体操作,实际上就是ZuulFilter
接口中定义的4
个抽象方法:
//需要返回一个字符串来代表过滤器的类型,而这个类型就是在HTTP请求过程中定义的各个阶段
//Zuul中默认定义了4种不同生命周期的过滤器类型,如下所示:
//1. pre:可以在请求被路由之前调用
//2. routing:在路由请求时被调用
//3. post:在routing和error过滤器之后被调用
//4. error:处理请求时发生错误时被调用
String filterType();
//通过int值来定义过滤器的执行顺序,数值越小优先级越高
int filterOrder();
//返回一个boolean值来判断该过滤器是否要执行
//我们可以通过此方法来指定过滤器的有效范围
boolean shouldFilter();
//过滤器的具体逻辑
//在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,
//或是在请求路由返回结果之后,对处理结果做一些加工等
Object run();
不论是核心过滤器还是自定义过滤器,只要在API
网关应用中为它们创建了实例,那么默认情况下,它们都是启用状态的。如果有些过滤器我们不想使用了,可以禁用过滤器。
在Zuul
中特别提供了一个参数来禁用指定的过滤器,该参数的配置格式如下:
# 代表过滤器的类名
# 代表过滤器类型
zuul.>.>.disable=true
该参数除了可以对自定义的过滤器进行禁用配置之外,很多时候可以用它来禁用Spring Cloud Zuul
中默认定义的核心过滤器。这样我们就可以抛开Spring Cloud Zuul
自带的那套核心过滤器,实现一套更符合我们实际需求的处理机制。
专注于提供微服务API
网关的管理平台,底层实现技术基于NGINX
,在NGINX
基础之上提供了一些更加简单的配置方式,也提供了很多插件,如:验证、日志、调用频率限制。