网关介绍以及搭建
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
网关
学习目标
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.<route>.path=/xxx/** 来指定映射路径。<route>是自定义的路由名
zuul.routes.<route>.serviceId=spring-provider来指定服务名。
而大多数情况下我们的<route>路由名称往往和服务名会写成一样的。因此Zuul就提供了一种简化的配置语法zuul.routes.<serviceId>=<path>
比方说上面我们关于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-prefixstrip-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、我们发起请求:localhost:8380/c-service/test2时由于c-service没配置则直接会使用默认的路径规则转发到c-service的路径test2的请求。
3、但是配置了ignored-services: c-service 表示不允许c-service走默认的路径规则即我们输入的localhost:8380/c-service/test2zuul不会认为c-service是服务名。
如果所有的请求都不允许使用默认的路由规则就是什么都不配地址栏直接写服务名就可以在配置文件中加入下列内容即可关闭所有默认的路由规则那么需要在配置文件中逐个为需要路由的服务添加映射规则。
zuul:
ignored-services: '*' ##忽略掉所有直接在地址栏写服务名的请求
通配符 | 说明 | 举例 |
? | 匹配任意单个字符 | /xxx/? |
* | 匹配任意数量的字符 | /xxx/* |
** | 匹配任意数量的字符 包括多级目录 | /xxx/** |
ignored-patternszuul 还提供了一个忽略表达式参数 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";
}
/**
* 过滤器的执行顺序这个顺序只是控制同一种类型过滤器的先后顺序如果是不同类型的则还是按照先执行preroutepost的顺序执行
* @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过滤器收到的响应状态值是500post过滤器可以对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.<SimpleClassName>.<filterType>.disabled=true就能禁用名为 <SimpleClassName> 的过滤器。例如:
zuul:
JwtFilter:
pre:
disable: true
上述配置就禁用掉了我们自定义的 JwtFilter 。