~本人的案例是承接上面的需要朋友可以 访问这里~
在学习完前面的知识后,微服务架构已经初具雏形。但还有一些问题:
具有独立的 ip 端口...
微服务网关
所有的外部请求都会先经过微服务网关。
这样简化了开发还有以下优点:
服务器
, 是系统对外的 唯一入口
客户端不需要在记录 大量的微服接口只需要记住一个 网关服务就行了, 通过它就可以找到需要的服务接口;在网关层处理所有的非业务功能。
REST/HTTP的访问API
。服务端通过API-GW注册和管理服务。网关具有的职责如:
身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。 当然,最主要的职责还是与“外界联系”。
Nginx
+ Lua
开发 `稍后整理Nginx笔记!`优点:
性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。问题:
只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。👍
JAVA开发
,易于二次开发;需要运行在web容器中
如Tomcat。
性能不如 Nginx
;👍
Netflflix开源
的微服务网关 它可以和Eureka、Ribbon、Hystrix等组件配合使用
(都是一家的怎么见外😥?)动态将请求路由到不同后端集群
逐渐增加指向集群的流量,以了解性能
为每一种负载类型分配对应容量,并弃用超出限定值的请求
对于不符要求请求直接丢弃为每一种负载类型分配对应容量,并弃用超出限定值的请求
页面的响应在 网关进行展示不会影响到 服务模块~
识别每一个资源的验证要求,并拒绝那些不符的请求。
三板斧: 依赖-注解-配置
pom.xml
<dependencies>
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- starter-netflix-zuul:所有zuul的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<!-- 网关的限流配置依赖: -->
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>1.3.4.RELEASE</version>
</dependency>
</dependencies>
SpringBoot 微服务项目
Myzuul
Myzuul.Java
@SpringBootApplication
@EnableZuulProxy //通过 @EnableZuulProxy 注解开启Zuul网管功能
public class Myzuul {
public static void main(String[] args) {
SpringApplication.run(Myzuul.class, args);
}
}
注解开启Zuul网管功能
个人猜测, 因为并不需要声明 Eureka的客户端注解:
@EnableEurekaClient
application.yml
并添加相应配置application.yml
server:
port: 7004 #zuul网关端口
spring:
application:
name: zuul-server #zuul网关微服名~
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka/ #指定注册的注册中心~
instance:
prefer-ip-address: true #显示浏览器中的状态栏显示ip
#zuul网关配置
zuul:
routes:
#定义路由,因为是自定义的所有下面并没有提示~
myuser-consumer: #这里是路由id, myuser-consumer是随意写,后面根据 路由做:限流操作!
path: /userserver/** #这里是映射路径,指在网关请求时候,需要在原请求下加上前缀进行访问!
serviceId: user-server #指定当前路由的——微服务名(到注册中心中找对应的名获取对应的 ip 端口~)
加上前缀 userserver
进行访问!
当然也可以不设置前缀:直接 /**
接口
ok,这样就大致完成了!
可以看到, 通过网关完成了 服务的调用! 这样只需要给前端网关的 ip 端口即可~
负责管理分配:
通过算法机制 对多个提供者的均衡调用...减轻服务器压力..
是统一多个微服务 IP 端口 方便前端的访问调用...减轻前端的压力
@EnableZuulProxy
-------.yml的网关配置:请求服务
(当然, 事先这些服务都注册到注册中心去...)
通过之前的学习,我们得知Zuul它包含了两个核心功能:对请求的 路由 和 过滤
动态路由:负责将外部请求转发到具体的微服务实例上
是实现外部访问统一入口的基础;
过滤器:
例如:验证用户是否登录校验 未登录直接过滤!
过滤器可以说是Zuul实现API网关功能最为核心的部件
,
Zuul 中的过滤器总共有 4 种类型,且每种类型都有对应的使用场景。 (类似于AOP的环绕增强!)
路由之前调用。
我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。(就是在PRE过滤器之后执行)
使用Apache HttpClient或Netfifilx Ribbon请求微服务。
路由到微服务以后执行。
这种过滤器可用来为响应添加标准的HTTP
Header、收集统计信息和指标、将响应从微服务发送给客户端等。发生错误时执行该过滤器。
分布获取当前时间 相差即可!
正常流程:
异常流程:
最终也会进入POST过滤器,而后返回。
会跳转到error过滤器,但是与pre和routing不同的时, 请求不会再到达POST过滤器了。
验证当前是否Token登录!
实现用户登录: 是否存在Token 如果没有则过滤掉请求!
PowerFilter.Java
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
//使当前类被Spring容器管理!
@Component
public class PowerFilter extends ZuulFilter { //继承ZuulFilter抽象类,并实现拦截器方法;
@Override
public String filterType() { //当前拦截器类型;
return FilterConstants.PRE_TYPE; //根据常量来确定当前过滤器类型 pre 路由前执行~
} // 常量值就是pre直接写字符pre也可以
@Override
public int filterOrder() {
return 0; //设置过滤器级别 值越大执行顺序越慢
}
@Override
public boolean shouldFilter() {
return true; //有可能存在多个拦截器,设置false 则忽略之后的过滤器!直接执行代码!
}
@Override
public Object run() throws ZuulException { //主要过滤的操作run()
System.out.println("zuul 前过滤!");
//Java原生的Servlet类: RequestContext 获得请求上下文对象
RequestContext currentContext = RequestContext.getCurrentContext();
//获取去一个request 对象,根据对象获取页面参数是否存在 Tonken如果没有Tonken则直接报错不给登录!
HttpServletRequest request = currentContext.getRequest();
String token = request.getParameter("token");
//判断
if (token == null || token.equals("")) {
//发送zuul 响应终止. 过滤器~
currentContext.setSendZuulResponse(false);
//给浏览器发送当前状态码!
currentContext.setResponseStatusCode(401);
//给浏览器返回响应数据!
currentContext.setResponseBody("{'code:':401,'data':'token not found!'}");
}
return "next"; //通过当前 过滤器
}
}
ZuulFilter
抽象类重写方法!
根据需求编写对应的方法:
通俗易懂: 为了防止请求流量过大, 程序服务器扛不住压力! 对请求流量做出限制的操作!
可以直接通过配置来设置的 ip 限流
限制一定时间内访问的数量...
.yml
#zuul网关配置
zuul:
routes:
#定义路由,因为是自定义的所有下面并没有提示~
myuser-consumer: #这里是路由id, myuser-consumer是随意写,后面根据 路由做:限流操作!
path: /userserver/** #这里是映射路径
serviceId: user-server #指定当前路由的——微服务名(到注册中心中找对应的名获取对应的 ip 端口~)
ratelimit:
enabled: true #开启限流
policies:
#指定 路由 进行的限流设置!
myuser-consumer:
limit: 10 #60s 内请求超过10次,服务端就抛出异常,60s后可以恢复正常请求,抛出异常就会被全局的异常处理接受导!
refresh-interval: 60
type: origin #针对IP进行限流,不影响其他IP
#还可以通过如下配置来开启全局的服务限流! default-policy:
#但不建议! 正常情况下只是对某一个高访问的模块进行限流..没必要所有模块都做限流!
# ratelimit:
# enabled: true #开启限流
# default-policy: #所有的路由都限流...
# limit: 3
# refresh-interval: 60
# type: origin
MyErrorController.Java
@RestController
public class MyErrorController implements ErrorController {
@Override
public String getErrorPath() {
return "error";
}
@RequestMapping("/error")
public String error(HttpServletResponse response)
{
int code = response.getStatus();
if (404 == code) {
System.out.println(404+"未找到资源");
} else if (403 == code) {
System.out.println(403+"没有访问权限");
} else if (401 == code) {
System.out.println(401+"登录过期");
} else {
System.out.println(500+"服务器错误");
}
//可以根据返回的 状态码来返回指定的页面!
return "{\"result\":\"访问太多频繁,请稍后再访问!!!\"}";
}
}
全局异常处理
如果避免出现404,405,500这种报错信息时, 可以通过
全局异常处理实现对应用的限流
在漏桶算法基础上的更改:
无论请求如何都不会超出某一个请求值
在令牌桶算法中,存在一个桶,用来存放固定数量的令牌
(一次创建多少令牌 允许多少请求的匀速请求)
首先注释 .yml中的 计数限流
避免冲突~
RouteFilter.Java
import com.google.common.util.concurrent.RateLimiter; //谷歌的组件..
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
//令牌限流:
@Component
public class RouteFilter extends ZuulFilter {
//设置常量令牌每秒产生的次数;
//定义一个令牌桶,每秒产生2个令牌,即每秒最多处理2个请求
private static final RateLimiter rateLimiter = RateLimiter.create(2);
//过滤器类型 pre 路由前执行
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
//级别 越小越先执行
@Override
public int filterOrder() {
return -10;
}
//方法返回 ture/false ture继续执行,false跳出不在执行后面~
@Override
public boolean shouldFilter() {
System.out.println("令牌生效");
RequestContext currentContext = RequestContext.getCurrentContext();
//判断当前是否存在令牌:没有令牌则不同行!
if (!rateLimiter.tryAcquire()) { //RateLimiter的方法底层会自动获取当前是否存在令牌...
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(401);
currentContext.setResponseBody("{'code:':401,'data':'limit'}");
return false;
}
//如果成立则同行~
return true;
}
//run().....拦截器的代码...
@Override
public Object run() throws ZuulException {
return null;
}
}
令牌桶对象: RateLimiter
GateWay
:本质上就是一个同步Servlet
,采用多线程阻塞模型进行请求转发。
Servelt 会问每一个请求,专门分配一个线程来执行… 直到客户端响应线程才返回 线程池
而 Servelt 是Java编写的… 对于多线程的操作并不完善…线程池大小存在限制.
如果, 后台频繁调用 比较耗时的业务
那么 , 执行的线程就会堵塞来完成该功能~ 线程资源会被占用
(基于Netty,也是非阻塞的,支持长连接)
但Spring Cloud 暂时还没有整合计划。
Spring Cloud Gateway 比 Zuul 1.x 系列的性能和功能整体要好。
GateWay
Zuul 2.x整合
有亲儿子了…
每秒能处理的请求数目。
Spring Cloud Gateway 的RPS 是Zuul的1.6倍
开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改
pom.xml
<!-- SpringCloud提供的 gateway依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
注意
建议Maven开发的话: 父工程不要在放 web依赖.. 不然会发生冲突... 移动到需要的子模块配置 web依赖...
Mygateway.Java
@SpringBootApplication //SpringBoot主程序
@EnableEurekaClient //开启注册中心客户端注解
public class Mygateway {
public static void main(String[] args) {
SpringApplication.run(Mygateway.class, args);
}
}
application.yml
server:
port: 7005
spring:
application:
name: gateway-server
#gateway 的配置...
cloud:
gateway:
routes:
- id: user-server #自定义的路由 ID,保持唯一
uri: http://127.0.0.1:6002 #通过uri 指定目标服务地址,可以通过 lb://指定注册中心的服务,实现动态调用。。。请求
predicates: #路由规则,返回一个布尔值结果。符合条件才能请求~
- Path=/user/** #请求前缀需要 /user 才能请求! (调用的服务也要有 /user前缀!)
#Eureka 注册服务;
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka/
instance:
prefer-ip-address: true #显示浏览器中的状态栏显示ip
网关 和 微服的请求前缀必须加 /user
才能进行请求!
微服 controller层加上:@RequestMapping("/user")
, 请求前缀加上 user 便于区分模块!
url
上面的url 写四了请求, 当然也可以同注册中心
来完成动态调用
注册中心!
application.yml
lb://指定注册中心的服务,实现动态调用。。。请求
cloud:
gateway:
routes:
- id: user-server
# uri: http://127.0.0.1:6002
uri: lb://user-server #动态请求 user-server的 ip 端口...
predicates:
- Path=/user/**
首先把 user-server模块的@RequestMapping("/user")
移除掉~
ok, 发现网关请求 需要前缀, 而服务单独请求不在需要了…! 完成!
上面 .yml配置可以查看, 创建一个路由需要:id
url
规则
建议浏览一遍, 需要时候copy即可
#路由断言之后匹配
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://xxxx.com
#路由断言之前匹配
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
#路由断言之前匹配
- id: before_route
uri: https://xxxxxx.com
predicates:
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]
#路由断言之间
- id: between_route
uri: https://xxxx.com
predicates:
- Between=xxxx,xxxx
#路由断言Cookie匹配,此predicate匹配给定名称(chocolate)和正则表达式(ch.p)
curl -H 'Cookie:name=forezp' localhost:8081
- id: cookie_route
uri: https://xxxx.com
predicates:
- Cookie=name, forezp
#路由断言Header匹配,header名称匹配X-Request-Id,且正则表达式匹配\d+
- id: header_route
uri: https://xxxx.com
predicates:
- Header=X-Request-Id, \d+
#路由断言匹配Host匹配,匹配下面Host主机列表,**代表可变参数
- id: host_route
uri: https://xxxx.com
predicates:
- Host=**.somehost.org,**.anotherhost.org
#路由断言Method匹配,匹配的是请求的HTTP方法
- id: method_route
uri: https://xxxx.com
predicates:
- Method=GET
#路由断言匹配,{segment}为可变参数
- id: host_route
uri: https://xxxx.com
predicates:
- Path=/foo/{segment},/bar/{segment}
#路由断言Query匹配,将请求的参数param(baz)进行匹配,也可以进行regexp正则表达式匹配 (参数包含 foo,并且foo的值匹配ba.)
- id: query_route
uri: https://xxxx.com
predicates:
- Query=baz 或 Query=foo,ba.
#路由断言RemoteAddr匹配,将匹配192.168.1.1~192.168.1.254之间的ip地址,其中24为子网掩码位数即255.255.255.0
- id: remoteaddr_route
uri: https://example.org
predicates:
- RemoteAddr=192.168.1.1/24
Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那么丰富,它只有两个:pre
和 post
。
这种过滤器在请求被路由之前调用。
这种过滤器在路由到微服务以后执行。
Spring Cloud Gateway 分为另外两种 GatewayFilter
(内置过滤器) 与 GlobalFilter
(自定义过滤器)。
局部过滤器
是针对单个路由的过滤器。
声明在单个, 路由id下,只有改路由存在改过滤器!
局部过滤器。
1. Spring Cloud Gateway的一个约定
Ctrl+N 输入:RewritePath
可以在类中方一个断点,运行类。发现程序暂停!!
全局过滤器(GlobalFilter)和 局部相反就是作用于所有路由
GlobalFilter
(全局过滤器)接口Spring Cloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发
进行处理如下:
TokenFitter.Java
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//Gate way实现过滤器:
@Component
@Slf4j
public class TokenFitter implements GlobalFilter, Ordered { //自定义过滤器类 实现GlobalFilter Ordered接口并实现两个方法;
//执行过滤
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//根据参数 exchange 获得request对象 并从url中获取参数...
String token = exchange.getRequest().getQueryParams().getFirst("token");
//判断是否存在Token
if (token == null || "".equals(token)) {
log.info("用户没有登录, 权限被拦截!");
//返回页面状态码!
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
//终止请求!
return exchange.getResponse().setComplete(); //退出..,拦截拦截退出...
}
//存在Token 则同行;
return chain.filter(exchange);
}
//过滤顺序级别
@Override
public int getOrder() {
return 0;
}
}
(放行)
exchange.getResponse().setComplete(); 终止请求;
这时候,你所有的 网关请求
就必须要带着 Token才可以执行请求。 不然都会被 全局拦截!
这里这是一个展示,更多功能请自己学习… 学会了教我~
默认就提供了 令牌桶的限流支持!
RequestRateLimiterGatewayFilterFactory 实现
Redis
和lua
脚本结合的方式进行流量控制。使用Redis 进行监听并对 相同数量进行限制!
redis 目录下:redis-server.exe
首先启动 Redis 服务
pom.xml
<!--监控依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
application:
name: gateway-server
#gateway 的配置...
cloud:
gateway:
routes:
- id: user-server #自定义的路由 ID,保持唯一
# uri: http://127.0.0.1:6002 #通过uri 指定目标服务地址,可以通过 lb://指定注册中心的服务,实现动态调用。。。请求
uri: lb://user-server
predicates: #路由条件,返回一个布尔值结果。符合条件才能请求~
- Path=/user/** #请求前缀需要 /user 才能请求! (调用的服务也要有 /user前缀!)
filters:
- RewritePath=/user/(?<segment>.*), /$\{segment} #网关请求的 /user/** 请求会重写为 /** 请求~
#Gateway 限流过滤操作! 默认令牌桶操作!
- name: RequestRateLimiter
args:
key-resolver: '#{@tokenKeyResolver}' # 使用SpEL从容器中获取对象,用户自定义的类!
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 3 # 令牌桶的总容量
#Spring 节点下配置redis依赖!(本人没有密码!)
redis:
host: 127.0.0.1
port: 6379
redis的信息
,并配置了RequestRateLimiter
的限流过滤器
- name: RequestRateLimiter
: 设置Gate way的限流配置:
key-resolver 使用SpEL从容器中获取对象 {@beanName} 从Spring 容器中获取 Bean 对象。开发者自定义的类!
burstCapacity 令牌桶总容量。
replenishRate 令牌桶每秒填充平均速率。
LimitConfig
本次简单实现两种: 根据ip 限流 根据token 登录用户限流
LimitConfig.Java
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//gatewat 完成限流: 采用令牌桶限流法....
@Configuration
public class LimitConfig {
//根据ip地址进行 限流,
/* @Bean
public KeyResolver pathKeyResolver() {
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
System.out.println("ip 限流");
String addr = exchange.getRequest().getPath().toString();
System.out.println("===================:" + addr);
return Mono.just(addr);
}
};
}*/
//根据token 登录用户限流...
@Bean
public KeyResolver tokenKeyResolver() {
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
System.out.println("token 限流");
String addr = exchange.getRequest().getQueryParams().getFirst("token"); //获取请求中的Token 打印;
System.out.println("===================:" + addr);
//返回,Redis会记录每次的返回结果. 对一样的解决进行计数.令牌桶限流!
//同一个Token 一端时间请求多次超过令牌桶则不在进行响应直接限制访问!!等待新的令牌!
return Mono.just(addr);
}
};
}
}
tokenKeyResolver
pathKeyResolver
分别是两种不同的限流方式;key-resolver: '#{@方法名}'
配置限流的操作!跨域请求
.yml
配置:使网关统一完成 跨域的操作!
#Spring 配置下
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
网关管理者很多的微服务暴漏的 接口
为了方便区别通常会设置前缀… 而一个微服务可能会有很多的 controller 又作不同的事情(进行区分)…
- Path=/api/...
用户模块下的多个功能 Contrller的区分可以, 使用 , 逗号
进行分隔区分…- StripPrefix=1
移除 微服模块 /api/user...
请求前缀的前一个… 这里就是移除 /api/
Gateway 的请求前缀是要在 微服模块和网关都要有前缀才可以的请求. 通常都会在微服务模块加入 @RequestMapping("/api/user")
而 StripPrefix=1 就可以省去 微服务模块 @RequestMapping("/user")
的 /api/ 前缀! 很大程度上简化了开发!优化格式