网关介绍以及搭建
网关
学习目标:
1、网关的作用
2、独立搭建Zuul网关
3、会用zuul网关路由功能
4、了解Zuul的过滤器
此山是我开,此树是我栽,要想此路过,留下买路财。
奈何桥是中国民间神话观念中是送人转世投胎必经的地点,在奈何桥边会有一名称作[孟婆]的年长女性神祇,给予每个鬼魂一碗[孟婆汤]以遗忘前世记忆,好投胎到下一世。
引言
大家都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?
这样的架构,会存在着诸多的问题:
每个业务都会需要鉴权、限流、权限校验、跨域等逻辑,如果每个业务都各自为战,自己造轮子实现一遍,会很蛋疼,完全可以抽出来,放到一个统一的地方去做。
如果业务量比较简单的话,这种方式前期不会有什么问题,但随着业务越来越复杂,比如淘宝、亚马逊打开一个页面可能会涉及到数百个微服务协同工作,如果每一个微服务都分配一个域名的话,一方面客户端代码会很难维护,涉及到数百个域名,另一方面是连接数的瓶颈,想象一下你打开一个APP,通过抓包发现涉及到了数百个远程调用,这在移动端下会显得非常低效。
后期如果需要对微服务进行重构的话,也会变的非常麻烦,需要客户端配合你一起进行改造,比如商品服务,随着业务变的越来越复杂,后期需要进行拆分成多个微服务,这个时候对外提供的服务也需要拆分成多个,同时需要客户端配合你进行改造,非常蛋疼。
上面的这些问题可以借助API网关来解决
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。
它是一个路由网关组件,通过前面的学习,使用Spring Cloud实现微服务的架构基本成型,大致是这样的:
简介
官网:https://github.com/Netflix/zuul
Zuul加入后的架构
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
快速入门
新建工程
pom文件导入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
编写配置
server:
port: 8380 #服务端口
spring:
application:
name: zuul-server #指定服务名
编写引导类
通过@EnableZuulProxy注解开启Zuul的功能:
@SpringBootApplication
@EnableZuulProxy // 开启网关功能
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
编写路由规则
我们需要用Zuul来代理spring-provider服务
ip为:127.0.0.1
端口为:8180
映射规则:
zuul:
routes:
spring-provider: # 这里是路由id,随意写
path: / spring-provider/** # 这里是映射路径
url: http://127.0.0.1:8180 # 映射路径对应的实际url地址
我们将符合path 规则的一切请求,都代理到 url参数指定的地址
本例中,我们将 /provider/**开头的请求,代理到http://127.0.0.1:8180
启动测试
访问的路径中需要加上配置规则的映射路径,我们访问:http://127.0.0.1:8380/spring-provider/provider/1
面向服务的路由
在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行动态路由才对!
对spring-zuul工程修改优化:
添加Eureka客户端依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加Eureka配置,获取服务信息
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8081/eureka
开启Eureka客户端发现功能
@SpringBootApplication
@EnableZuulProxy // 开启Zuul的网关功能
@EnableDiscoveryClient
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
修改映射配置,通过服务名称获取
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
zuul:
routes:
spring-provider: # 这里是路由id,路由名,随意写,不能写中文
path: /spring-provider/** # 这里是映射路径
serviceId: spring-provider # 指定服务名称
启动测试
再次启动,这次Zuul进行代理时,会利用Ribbon进行负载均衡访问:
再次访问
简化的路由配置
在刚才的配置中,我们的规则是这样的:
zuul.routes.
.path=/xxx/**: 来指定映射路径。 是自定义的路由名 zuul.routes.
.serviceId=spring-provider:来指定服务名。
而大多数情况下,我们的
比方说上面我们关于spring-provider的配置可以简化为一条:
zuul:
routes:
spring-provider: /spring-provider/** # 这里是映射路径 左边是一个服务名
http://localhost:8380/spring-provider/provider/2
注意:下面配置方式所有的请求都会访问到spring-provider服务
zuul:
routes:
spring-provider: /** # 这里是映射路径 左边是一个服务名
http://localhost:8380/spring-provider/provider/2
http://localhost:8380/provider/2
默认的路由规则
在使用Zuul的过程中,上面讲述的规则已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul就指定了默认的路由规则:
- 默认情况下,一切服务的映射路径就是服务名本身。例如服务名为:spring-provider,则默认的映射路径就 是:/spring-provider/**
也就是说,刚才的映射规则我们完全可以不用配置。
#zuul:
# routes:
# spring-provider: /spring-provider/** # 这里是映射路径 左边是一个服务名
路由参数配置
- 路由前缀prefix
配置示例:
zuul:
routes:
spring-provider: /spring-provider/**
spring-consumer: /spring-consumer/**
prefix: /api # 添加路由前缀,当然最终默认/api/spring-provider或者/api/spring-consumer帮我们截掉
我们通过zuul.prefix=/api来指定了路由的前缀,这样在发起请求时,路径就要以/api开头。
- 不去除前缀 strip-prefix:strip-prefix 默认值为true,表示除去前缀,strip-prefix = false 表示不除去前缀
配置示例:
zuul:
routes:
spring-provider:
path: /spring-provider/**
#url: http://localhost:8081 或者用下面的serviceId
serviceId: user-service ##服务名
strip-prefix: true
说明:如果采用第三种(服务名:路径)配置方式,则 strip-prefix不会生效,例如:
zuul:
routes:
spring-provider: /spring-provider/**
spring-consumer: /spring-consumer/**
strip-prefix: false #采用这个方式配置路由strip-prefix不会生效
- ignored-services忽略使用默认的路由规则
配置示例:
zuul:
routes:
b-service: /xx/** ##以/xx开头的,转发给b-service微服务
ignored-services: c-service ##如果有多个,用逗号隔开
1、我们发起请求 localhost:8380/xx/test1,则会执行b-service的路径test1映射请求
2、我们发起请求8380/c-service/test2时,由于c-service没配置,则直接会使用默认的路径规则,转发到c-service的路径test2的请求。
3、但是配置了ignored-services: c-service ,表示不允许c-service走默认的路径规则,即我们输入的localhost:8380/c-service/test2,zuul不会认为c-service是服务名。
如果所有的请求都不允许使用默认的路由规则,就是什么都不配,地址栏直接写服务名,就可以在配置文件中加入下列内容,即可关闭所有默认的路由规则,那么需要在配置文件中逐个为需要路由的服务添加映射规则。
zuul:
ignored-services: '*' ##忽略掉所有直接在地址栏写服务名的请求
通配符 | 说明 | 举例 |
? | 匹配任意单个字符 | /xxx/? |
匹配任意数量的字符 | /xxx/ | |
匹配任意数量的字符, 包括多级目录 | /xxx/ |
ignored-patterns:zuul 还提供了一个忽略表达式参数 zuul.ignored-patterns,该参数用来设置不被网关进行路由的 Url 表达式
zuul:
routes:b-service: /xx/**
c-service: /yy/**
ignored-patterns: /xx
则我们在浏览器中输入:http://localhost:8380/xx/test1,不会帮我们转发
过滤器
Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。
ZuulFilter
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 来自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true执行,返回false不执行。
run:过滤器的具体业务逻辑。
filterType:返回字符串,代表过滤器的类型。包含以下4种:
pre:请求在被路由之前执行
route:在路由请求时调用
post:在route和errror过滤器之后调用
error:处理请求时发生错误调用
filterOrder:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
过滤器执行生命周期
这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。
正常流程:
- 请求到达首先会经过pre类型过滤器,而后到达route类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
异常流程:4种情况
整个过程中,如果pre或者route过滤器出现异常,都会直接进入error过滤器,在error处理完毕后,会将请求交给POST过滤器,最后返回给用户。(2种)
如果是error过滤器自己出现异常,最终也会进入POST过滤器,将最终结果返回给请求客户端。
如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和route不同的是,请求不会再到达POST过滤器了,而是直接响应用户
所有内置过滤器列表:
使用场景
场景非常多
请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
异常处理:一般会在error类型和post类型过滤器中结合来处理。
服务调用时长统计:pre和post结合使用。
自定义过滤器
接下来我们来自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求中有access-token参数,则认为请求有效,放行。
定义过滤器类
内容:
@Component
public class LoginFilter extends ZuulFilter {
/**
* 过滤器类型,前置过滤器
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤器的执行顺序,这个顺序只是控制同一种类型过滤器的先后顺序,如果是不同类型的,则还是按照先执行pre,route,post的顺序执行
* @return
*/
@Override
public int filterOrder() {
return 10;
}
/**
* 该过滤器是否生效
* @return
*/
@Override //返回true,表示执行下面方法的run,返回false表示不执行run,但是依然会执行后面的所有过滤器.
//另外是否路由到目标微服务,主要看 “route”类型过滤器,如果route返回false,则不会执行run()方法,会路由到目标微服务, //如果”route“的该方法返回true,则执行run()方法,如果run()方法执行context.setSendZuulResponse(false),则不会路由
public boolean shouldFilter() {
return true;
}
/**
* 登陆校验逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
// 获取zuul提供的上下文对象
RequestContext context = RequestContext.getCurrentContext();
// 从上下文对象中获取请求对象
HttpServletRequest request = context.getRequest();
// 获取token信息
String token = request.getParameter("access-token");
HttpServletResponse response = context.getResponse();
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-type","text/html;charset=UTF-8");
// 判断
if (StringUtils.isBlank(token)) {
// 过滤该请求,不对其进行路由,注意这个设置只是zuul不对其路由到目标微服务,但是后面的过滤器还是会正常执行
context.setSendZuulResponse(false);
// 设置响应状态码,401
context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
// 设置响应信息
response.getWriter().write("{\"status\":\"401\", \"text\":\"token无效!\"}");
//context.setResponseBody("{\"status\":\"401\", \"text\":\"request error!\"}");
}
// 校验通过,把登陆信息放入上下文信息,继续向后执行
context.set("token", token);
return null;
}
}
测试
没有token参数时,访问失败:
添加token参数后:
Zuul 与 Hystrix 结合实现熔断
Zuul 和 Hystrix 结合使用实现熔断功能时,需要完成 FallbackProvider 接口。该接口提供了 2 个方法:
.getRoute 方法:用于指定为哪个服务提供 fallback 功能。
.fallbackResponse 方法:用于执行回退操作的具体逻辑。
例如,我们为 service-id 为 eureka-client-department 的微服务(在 zuul 网关处)提供熔断 fallback 功能。
- 实现 FallbackProvider 接口,并托管给 Spring IoC 容器
1、Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:
ribbon:
ReadTimeout: 12000
ConnectTimeout: 10000
spring-provider: #在网关上配置 请求某个服务名的负载均衡测试 服务名小写
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
1.网关转发给A服务,A服务通过openfeign请求B服务,并且在A服务配置了openfeign的重试功能,假如A服务第一次请求B服务以及重试总时长是12s((1+5)*2)),那么网关的 ribbon.ReadTimeout应该等于12s,12s之后立刻降级.
2.网关降级之后,如果定义了post过滤器,则走post过滤器,此时post过滤器收到的状态值就是zuul网关降级的状态值,
@Override
publicHttpStatusgetStatusCode() throwsIOException {
returnHttpStatus.OK; //或者其他的状态值
}
然后post把处理结果响应给浏览器,如果post过滤器没有对status进行处理,最终响应结果还是网关降级的结果,这并不意味着post过滤器不执行,也就是说执行降级后,依然会执行post过滤器
3.如果ReadTimeout超时时间小于微服务请求的总时长,如ReadTimeout=3000,那么3s网关就降级了,然后执行post过滤器,此时post过滤器收到的响应状态值依然是200.只不过不要这样配置,否则网关都给客户响应了,而微服务通过feign还在不断的重试
4.如果ReadTimeout超时时间大于微服务请求的总时长,如ReadTimeout=20000,也就是20s后才降级,那么在12s后,A服务不再重试B服务了,请求失败,直接抛出状态码为500的异常,而此时zuul还没有降级,所以post过滤器收到的响应状态值是500,post过滤器可以对500的状态码进行处理,然后响应
5.如果A服务通过feign调用了B服务失败,A服务自己实现了feign的降级功能,那么站在zuul的角度来看,A服务是正常响应,此时zuul不会执行降级,最终只执行post类型过滤器,过滤器收到A服务的响应状态码就是200,你可以注掉feign的降级实现类。
2、zuul网关的降级,实现ClientHttpResponse接口
package com.woniu.fallback;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
public class ClientHttpResponseImpl implements ClientHttpResponse {
/**
* 设置一个状态码,如果有post过滤器,则post过滤器得到的状态码就是该值
*/
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
/**
* 状态值
*/
@Override
public int getRawStatusCode() throws IOException {
return this.getStatusCode().value();
}
@Override
public String getStatusText() throws IOException {
return this.getStatusCode().getReasonPhrase();
}
@Override
public void close() {
}
/**
* 最终要响应给浏览器的内容
* @return
* @throws IOException
*/
@Override
public InputStream getBody() throws IOException {
String str = "{xxx:请注意,msg:服务器正忙,请稍后再试}";
return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
}
/**
* 响应头 MediaType
* @return
*/
@Override
public HttpHeaders getHeaders() {
HttpHeaders header = new HttpHeaders();
MediaType type = new MediaType("application","json", StandardCharsets.UTF_8);
header.setContentType(type);
return header;
}
}
3、实现FallbackProvider接口
@Component
public class UserProviderFallBack implements FallbackProvider {
/**
* 要降级的 服务名
* @return
*/
@Override
public String getRoute() {
return "*"; // * 所有服务的降级
//return “服务名 (服务名小写)”
}
/**
* 网关降级响应方法
* @param route
* @param cause
* @return
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
ClientHttpResponse response = new ClientHttpResponseImpl();
return response;
}
}
在启动类添加@EnableCircuitBreaker注解
Zuul 中的 Eager Load 配置
zuul 的路由转发也是由通过 Ribbon 实现负载均衡的。默认情况下,客户端相关的 Bean 会延迟加载,在第一次调用微服务时,才会初始化这些对象。所以 zuul 无法在第一时间加载到 Ribbon 的负载均衡。
如果想提前加载 Ribbon 客户端,就可以在配置文件中开启饥饿加载(即,立即加载):
zuul:
ribbon:
eager-load:
enabled: true
注意 eager-load 配置对于默认路由不起作用。因此,通常它都是结合 zuul.ignored-services=* (即忽略所有的默认路由) 一起使用的,以达到 zuul 启动时就默认已经初始化各个路由所要转发的负载均衡对象。
禁用 zuul 过滤器
Spring Cloud 默认为 zuul 编写并启动了一些过滤器,这些过滤器都放在 org.springframework.cloud.netflix.zuul.filters 包下。
如果需要禁用某个过滤器,只需要设置 zuul.
zuul:
JwtFilter:
pre:
disable: true
上述配置就禁用掉了我们自定义的 JwtFilter 。
还没有评论,来说两句吧...