Spring Cloud Eureka:实现高可用的服务注册中心以及实现微服务的注册与发现。
Spring Cloud Config:配置中心
(Nacos:注册中心&配置管理)
Spring Cloud Ribbon或者Feign:实现服务间负载均衡的接口调用。
Spring Cloud Hystrix:实现线程隔离并加入熔断机制,避免在微服务架构中因个别服务出现异常而引起级联故障蔓延。
Spring Cloud Zuul:API网关,所有的外部客户端访问都需要经过API网关进行调度和过滤。
Spring Cloud Zuul主要解决了两个问题:路由规则与服务实例的维护问题;签名检验、登陆校验在微服务中的冗余问题。
- 对于路由规则与服务实例的维护问题:Spring Cloud Zuul通过与Spring Cloud Eureka进行整合,将自身注册为Eureka服务治理下的应用,同时从Eureka中获得了所有其他微服务的实例信息。将维护服务实例的工作交给了服务治理框架自动完成,不再需要人工介入;对于路由规则的维护,Zuul默认会将通过以服务名作为ContextPath的方式来创建路由映射。
- 对于签名检验、登陆校验在微服务中的冗余问题:Spring Cloud Zuul提供了一套过滤机制,开发者可以使用Zuul来创建各种检验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然就返回错误提示。
------------------------------------------------快速入门----------------------------------------------
这部分的代码请参考:Spring Cloud (十四)、API网关服务(入门)
一、构建网关
这里我们需要引入spring-cloud-starter-netflix-zuul依赖,可以通过查看它的依赖内容了解到:该模块不仅包含了Zuul的核心依赖zuul-core,还包含了下面这些网关服务重要依赖:
- spring-cloud-starter-netflix-hystrix:实现对微服务转发时候的保护机制,通过线程隔离和断路器,防止微服务的故障引发API网关资源无法释放,从而影响了其他应用的对外服务。
- spring-cloud-starter-netflix-ribbon:实现在网关服务进行路由转发时候的客户端负载均衡以及请求重试。
- spring-boot-starter-actuator:提供常规的微服务管理端点。另外,在Spring Cloud Zuul中太特别提供了/routes端点来返回当前的所有路由规则。
用到的注解:@EnableZuulProxy开启Zuul的API网关服务功能。
二、请求路由
1、传统路由方式
#传统路由方式
zuul.routes.api-a-url.path=/api-a/**
zuul.routes.api-a-url.url=http://localhost:8080/
解释:所有符合/api-a/**规则的访问都将被转发到http://localhost:8080/地址上。
2、面向服务的路由
可以让路由的path不是映射到具体的url,而是让它映射到某个具体的服务,而具体的url则交给Eureka的服务发现机制去自动维护,这就是面向服务额路由。
- 面向服务的请求需要与Eureka整合,就需要引入spring-cloud-starter-netflix-eureka-client的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 指定注册中心的位置,并配置服务路由。
#面向服务的路由
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=hello-servicezuul.routes.api-b.path=/api-c/**
zuul.routes.api-b.serviceId=feign-consumereureka.client.service-url.defaultZone=http://localhost:1111/eureka/
解释:符合/api-a/**规则的访问,由api-a路由负责转发,该路由映射的serviceId为hello-service,所以最终请求会被转发到hello-service服务的某个实例上;同理,符合/api-c/**规则的访问,由api-b路由负责转发,该路由映射的serviceId为feign-consumer,所以最终请求会被转发到feign-consumer服务的某个实例上。
请求路由小结:使用传统路由方式需要为各个路由维护微服务应用的具体实例的位置;而使用面向服务的方式只需要通过简单的path和serviceId的映射组合,是的维护工作变得非常简单。这完全归功于Spring Cloud Eureka的服务发现机制,它使得API网关服务可以自动化完成服务实例清单的维护,完美地解决了对路由映射实例地维护问题。
三、请求过滤:实现对客户端请求的校验
实现方法:定义过滤器,来继承ZuulFilter抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。
public class AccessFilter extends ZuulFilter {
private Logger logger = Logger.getLogger(String.valueOf(AccessFilter.class));
/*过滤器的类型:决定过滤器在请求的哪个生命周期中执行。定义为pre,带包在请求被路由之前执行*/
@Override
public String filterType() {
return "pre";
}
/*过滤器的执行顺序:当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来一次执行*/
@Override
public int filterOrder() {
return 0;
}
/*判断过滤器是否需要执行:为true表示该过滤器对所有的请求都会有效。实际运用中可以利用该函数来指定过滤器的有效范围*/
@Override
public boolean shouldFilter() {
return true;
}
/*过滤器的具体逻辑*/
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
logger.info("send "+request.getMethod()+" request to "+request.getRequestURI().toString());
Object accessToken = request.getParameter("accessToken");
if(accessToken == null){
logger.warning("access token is empty");
//令zuul过滤该请求,不对其进行路由
ctx.setSendZuulResponse(false);
//设置返回的错误码
ctx.setResponseStatusCode(401);
return null;
}
logger.info("access tiken ok");
return null;
}
}
以上代码定义了一个简单的Zuul过滤器,它实现了在请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 unauthorized错误。
在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的Bean才能启动该过滤器:
@EnableZuulProxy
@SpringCloudApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
@Bean
public AccessFilter accessFilter(){
return new AccessFilter();
}
}
微服务网关额重要性,如下:
- 作为系统的统一入口,屏蔽了系统内部各个微服务的细节。
- 可以与服务治理框架结合,实现自动化的服务实例维护和负载均衡的路由转发。
- 实现接口权限校验与微服务业务逻辑解耦。
- 通过服务网关中的过滤器,在各个生命周期中去校验请求的内容,将原本在对外服务层做的校验前移,保证了微服务的无状态性,同时降低了微服务的测试难度,让服务本身更集中关注业务逻辑的处理。
------------------------------------------------路由详解----------------------------------------------
路由功能:负责将外部请求转发到具体的为服务实例上,是实现外部访问统一入口的功能。
四、传统路由配置
传统路由配置就是在不依赖于服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现API网关对外部请求的路由。
没有Eureka等服务治理框架的帮助,我们需要根据服务实例的数量采用不同方式的配置来实现路由规则。
- 单实例配置:通过zuul.routes.<route>.path与zuul.routes.<route>.url参数对的方式进行配置。
zuul.routes.hello-service.path=/hello-service/**
zuul.routes.hello-service.url=http://localhost:8080/
- 多实例配置:通过zuul.routes.<route>.path与zuul.routes.<route>.serviceId参数对的方式进行配置。
zuul.routes.hello-service.path=/hello-service/**
#用户手工名命名的服务名称
zuul.routes.hello-service.serviceId=hello-service
#默认情况下Ribbon会根据服务发现机制来获取配置服务名对应的实例清单。但是,该实例并没有整合类似Eureka之类的服务治理框架,所以需要设置为false,否则配置的serviceId获取不到对应实例的清单
ribbon.eureka.enabled=false
#用户维护的服务实例(配合serviceId)
hello-service.ribbon.listOfServers=http://localhost:8080/,http://localhost:8081/
通过 zuul.routes.hello-service.serviceId=hello-service和hello-service.ribbon.listOfServers=http://localhost:8080/,http://localhost:8081/这两个参数的配置相当于在该应用内部手工服务与实例的对应关系。
五、服务路由配置
要在application.properties配置文件中指定注册中心。
只需要通过zuul.routes.<route>.path与zuul.routes.<route>.serviceId参数对的方式配置即可。
zuul.routes.hello-service.path=/hello-service/**
zuul.routes.hello-service.serviceId=hello-service
另外一种更简洁的配置方式:zuul.routes.<serviceId>=<path>。
zuul.routes.hello-service=/hello-service/**
在Eureka的帮助下,API网关服务本身就已经维护了系统中所有的serviceId与实例地址的映射关系。当有外部请求到达API网关的时候,根据请求的URL路径找到最佳匹配的path规则,API网关就可以知道要将该请求路由到哪个具体的serviceId上去。由于在API网关中已经知道serviceId对应服务实例的地址清单,那么只需要通过Ribbon的负载均衡策略,直接在这些清单中选择一个具体的实例进行转发就能完成路由工作。
六、服务路由的默认规则
1、当我们为Spring Cloud Zuul构建的API引入Spring Cloud Eureka之后,它为Eureka中的每个服务都自动创建一个默认路由规则,这些默认规则的path会使用serviceId配置的服务名作为请求前缀,如下面的例子(就不需要我们手动再去维护路由规则了):
zuul.routes.hello-service.path=/hello-service/**
zuul.routes.hello-service.serviceId=hello-service
2、默认情况下所有的Eureka上的服务都会被Zuul自动创建映射关系来进行路由,不希望对外开发的服务我们可以使用zuul.ignored-services参数来设置一个服务名匹配表达式来定义不自动创建路由匹配规则。不让hello-service和feign-consumer这两个服务进行自动创建路由匹配规则,如下:
zuul.ignored-services=hello-service,feign-consumer
可以使用*来表示Zuul将对所有的服务都不创建路由规则。
七 、自定义路由映射规则
在系统的迭代过程中,有时候需要我们为一组互相配合的微服务定义一个版本标识来方便管理它们的版本关系,根据这个标识我们可以很容易地知道这些服务需要一起启动并配合使用。
我们在各个为服务应用中将服务名命名为:hello-service-v1、feign-consumer-v1。
默认情况下,Zuul自动为服务创建的路由表达式会采用服务名作为前缀:/hello-service-v1/**、/feign-consumer-v1/**,这样生成的表达式规则较为单一,不利于通过路径规则来进行管理。
通常的作法是为这些不同版本的微服务应用生成以版本号作为路由前缀定义的路由规则,比如:/v1/feign-consumer/**、/v1/hello-service/**。这时候,通过这样具有版本号前缀的URL路径,我们就可以很容易地通过路径表达式来归类和管理这些具有版本信息地微服务了。
具体实现就是在API网关应用程序中,增加如下Bean的创建(PatternServiceRouteMapper):
@EnableZuulProxy
@SpringCloudApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
@Bean
public AccessFilter accessFilter(){
return new AccessFilter();
}
@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)","${version}/${name}");
}
}
八、路径匹配
在Zuul中,路由匹配的路径表达式采用了Ant风格的定义,它一共有以下三种通配符:
通配符 | 说明 | URL路径 | 举例 |
---|---|---|---|
? | 匹配单个字符 | /hello-service/? | /hello-service/a,/hello-service/b |
* | 匹配任意数量的字符 | /hello-service/* | /hello-service/a,/hello-service/aa |
** | 匹配任意数量的字符,支持多及目录 | /hello-service/** | /hello-service/a,/hello-service/aa,/hello-service/a/b |
当存在多个匹配的路由规则时,匹配结果完全取决于路由规则的保存顺序。但需要注意的是,properties的配置内容无法保证有序,所以当出现这种情况的时候,为了保证路由的优先顺序,我们需要使用YAML文件来配置,以实现有序的路由规则。
九、忽略表达式
Zuul可以通过zuul.ignored-patterns参数来忽略表达式。该参数可以用来设置不希望被API网关进行路由的URL表达式。
当我们不希望/hello接口被路由,可以做以下这样的设置:
zuul.ignored-patterns=/**/hello/**
该参数在使用的时候需要注意它的范围并不是对某个路由,而是对所有的路由。
十、路由前缀
为了方便全局地为路由规则增加前缀信息,Zuul提供了zuul.prefix参数来进行设置。
zuul.prefix=/api:为网关上的路由规则都增加/api前缀。
zuul.stripPrefix=false:关闭该移除代理前缀地操作。(代理前缀默认从路径中移除)
zuul.routes.<route>.strip-prefix=false:对指定路由关闭移除代理前缀的动作。
十一、本地跳转
在Zuul实现的API网关路由功能中,还支持forward形式服务端跳转的功能。
zuul.routes.api-local.path=/api-local/**
zuul.routes.api-local.url=forward:/local
以上配置中将符合/api-local/**规则的请求转发到API网关的中以/local为前缀的请求,由API网关进行本地处理。
在API网关上增加一个/local/hello的接口实现让/api-local/**的路由规则生效:
@RestController
public class HelloController {
@RequestMapping("/local/hello")
public String hello(){
return "Hello World Local!!";
}
}
当API接收到请求/api-hello,它符合api-local的路由规则,所以该请求会被API网关转发到网关的/local/hello请求上进行本地处理。
十二、Cookie与头信息
默认情况下,Spring Cloud Zuul在请求路由的时候,会过滤掉HTTP请求头部信息中的一些敏感信息,防止它们被传递到下游的外部服务器。所以我们在开发Web项目时常用的Cookie在Spring Cloud Zuul网关中默认是不会传递的,这就会引发一个常见的问题:如果我们要将使用Spring Security、Shiro等安全框架构建的Web应用通过Spring Cloud Zuul构建的网关来进行路由时,由于Cookie信息无法传递,我们的Web应用将无法实现登陆和鉴权。
针对以上问题,我们可以通过以下方法来解决:
- 通过设置全局参数为空来覆盖默认值:zuul.sensitive-headers=
- 通过指定参数的路由来配置:
#方法1:对指定路由开启敏感信息
zuul.routes.<router>.customSensitiveHeaders==true
#方法2:将指定路由的敏感头设置为空
zuul.routes.<router>.sensitiveHeaders=
十三、Hystrix和Ribbon支持
我们已经知道了zuul依赖自身就包含了hystrix依赖和Ribbon依赖,所以Zuul天生就拥有线程隔离和断路由的自我保护功能,以及对服务调用的客户端负载均衡功能。
但是,在使用path与url的映射关系来配置路由规则的时候,对于路由转发的请求不会采用HystrixCommand来包装,所以这类路由请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。因此我们在使用Zuul的时候尽量使用path和serviceId的组合来进行配置,这样不仅可以保证网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。
- hystrix.command.default.execution.isolation.thread.timeoutInMillisecond:该参数可以用来设置API网关中路由转发请求的HystrixCommand执行超时时间,单位为毫秒。当路由转发请求的命令执行时间超过该配置值后,Hystrix会将该执行命令标记为TIMEOUT并抛出异常,Zuul会对该异常进行处理并返回JSON信息给外部调用方。
{
"timestamp": "2019-06-28T01:53:46.470+0000",
"status": 500,
"error": "Internal Server Error",
"message": "TIMEOUT"
}
- ribbon.ConnectTimeout:该参数用来设置路由转发请求的时候,创建请求连接的超时时间。当ribbon.ConnectTimeout的配置值<hystrix.command.default.execution.isolation.thread.timeoutInMillisecond配置值的时候,若出现路由请求连接超时,会自动进行重试路由请求,Zuul会返回如下JSON信息给外部调用方。
{
"timestamp": "2019-06-28T02:57:05.501+0000",
"status": 500,
"error": "Internal Server Error",
"message": "NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED"
}
以上两个配置,谁配置的值小,优先执行谁。
- ribbon.ReadTimeout:该参数用来设置路由转发请求的超时时间。它的处理与ribbon.ConnectTimeout类似,只是它的超时是对请求连接建立之后的处理时间。返回信息和ribbon.ConnectTimeout也类似。
从以上我们可以得到,在使用Zuul的服务路由时,如果路由转发请求发生超时(连接超时或者处理超时),只要超时时间的设置小于Hystrix的命令超时时间,那么它就会自动发起重试。那么如何关闭重试机制,可以通过以下两个参数进行设置:
#关闭去全局重试机制
zuul.retryable=false
#指定路由关闭重试机制
zuul.routes.<route>.retryable=false
------------------------------------------------过滤器详解----------------------------------------------
过滤器功能:负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。
十四、过滤器
实际上,在路由功能运行的时候,路由映射和请求转发都是由几个不同的过滤器完成的。其中,路由映射主要时pre类型过滤器完成,将请求路径与配置的路由规则进行匹配,已找到需要转发额目标地址;请求转发的部分则是由route类型的过滤器来完成,对pre类型过滤器获得的路由地址进行转发。
在Spring Cloud Zuul中实现的过滤器必须包含4个基本特性:过滤类型、执行顺序、执行条件、具体操作:
String filterType();
int filterOrder();
boolean shouldFilter();
Object run();
- filterType:该函数需要返回一个字符串来代表过滤器的类型,而这个类型就是在HTTP请求过程中定义的各个阶段。在Zuul中默认定义了4种不同生命周期的过滤器类型:
(1)、pre:可以在请求被路由之前调用。
(2)、route:在路由请求时被调用。
(3)、post:在route和error过滤器之后被调用。
(4)、error:处理请求时发生错误时被调用。
- filterOrder:通过int值来定义过滤器的执行顺序,数值越小优先级越高。
- shouldFilter:返回一个boolean值来判断该过滤器是否要执行。我们可以通过此方法来指定过滤器的有效范围。
- run:过滤器的具体逻辑。在这里来确定时候要拦截当前的请求。
十五、请求的声明周期
当HTTP请求到达API网关的时候,先进入pre类型的过滤器,进行进行路由匹配找到转发的目标地址;再进入到route类型的过滤器,将外部请求转发到具体的微服务实例上去,获取相应结果,由post类型的过滤器将相应结果进行加工或转换。而error过滤器则是只有在发生异常的时候才会触发,最后又post将异常结果进行处理和转换并进行返回。
十六、核心过滤器
默认启动了pre、route、post三种类型的过滤器。
十七、禁用过滤器
不论是核心过滤器还是自定义过滤器,只要在API网关应用中为它们创建了实例,那么默认情况下,它们都是启用状态。
实际上,在Zuul中特别提供了一个参数来禁用指定的过滤器,该参刷的配置格式如下:
zuul.<simpleClassName>.<filterType>.disable=true
其中<simpleClassName>代表过滤器的类名,<filterType>代表过滤器的类型。
------------------------------------------------动态加载----------------------------------------------
该部分的代码可参考:Spring Cloud (十五)、动态路由和动态过滤器的动态加载
在微服务架构中,由于API网关服务担负着外部访问同意入口的重任,它同其它应用应用不同,任何关闭应用和重启应用的操作都会使系统对外服务停止,对于很多7×24小时服务系统来说,这样的情况绝对不被允许。所以,作为最外部的网关,它必须具备动态更新内部逻辑的能力,比如动态修改路由规则、动态添加/删除过滤器等。
通过Zuul实现的API网关服务当然也具备了动态路由和动态过滤器的能力。我们可以在不重启API网关服务的前提,为其动态修改路由规则和添加或删除过滤器。