API网关可以提供一个单独且统一的API入口用于访问内部一个或多个API。简单来说嘛就是一个统一入口,比如现在的支付宝或者微信的相关api服务一样,都有一个统一的api地址,统一的请求参数,统一的鉴权。
API 网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。在未加入网关时,一般上会在服务外网架设一个负载均衡,如nginx等。此时,微服务的组成为:
此时,对于Open Service而言可能需要提供权限控制等和业务无关的能力,这样本身就破坏了微服务服务单一的原则。所以,一般上在Open Service之上,还有一层服务提供诸如通用的权限校验、参数校验等功能,此服务就是网关了。之后,对于内部微服务而言,只需要关心各自微服务提供的业务功能即可,无需去关心其他非业务相关的功能。
Spring Cloud Zuul:本身基于Netflix开源的微服务网关,可以和Eureka,Ribbon,Hystrix等组件配合使用。
Kong : 基于OpenResty的 API 网关服务和网关服务管理层。
Spring Cloud Gateway:是由spring官方基于Spring5.0,Spring Boot2.0,Project Reactor等技术开发的网关,提供了一个构建在Spring Ecosystem之上的API网关,旨在提供一种简单而有效的途径来发送API,并向他们提供交叉关注点,例如:安全性,监控/指标和弹性。目的是为了替换Spring Cloud Netfilx Zuul的。
在Spring cloud体系中,一般上选择zuul或者Gateway。当然,也可以综合自己的业务复杂性,自研一套或者改造一套符合自身业务发展的api网关的,最简单做法是做个聚合api服务,通过SpringBoot构建对外的api接口,实现统一鉴权、参数校验、权限控制等功能,说白了就是一个rest服务。
优点
减少api请求次数
限流
缓存
统一认证
灰度发布
路由转发
降低微服务的复杂度
支持混合通信协议(前端只和api通信,其他的由网关调用)
缺点
网关需高可用,可能产生单点故障
管理复杂
对验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求。
审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论。
动态路由: 以动态方式根据需要将请求路由至不同后端集群处。
压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平。
负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。
静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。
多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-demo</artifactId>
<groupId>com.qf</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api-gateway</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
</dependency>
</dependencies>
</project>
package com.qf.spring.cloud.api.gatewat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy //启用网关
public class ApiGateway {
public static void main(String[] args) {
SpringApplication.run(ApiGateway.class, args);
}
}
server:
port: 1700
spring:
application:
name: api-gateway
eureka:
client:
service-url:
defaultZone: http://localhost:11000/eureka/
启动eureka
启动 config-server
启动 任一 微服务
访问测试
注意:这里是通过网关进行的访问
路径中的微服务名主要作用就是指向访问的微服务,如果能够将url访问地址跟微服务名匹配起来,那么访问的时候就可以将微服务名去掉了。
server:
port: 14000
spring:
application:
name: api-gateway
eureka:
client:
service-url:
defaultZone: http://localhost:11000/eureka/
zuul: # 网关
routes: # 路由
goods: # 这个名字可以随便取
serviceId: goods-service-prod #使用的微服务名
path: /goods/** # 访问的url地址
strip-prefix: false # 是否去掉前缀,默认值就是true
再次测试
启动eureka
启动 config-server
启动微服务多台(同一个微服务名的服务),如果不启动多台,那么路由(不带微服务名)将失效
访问测试
http://localhost:14000/goods/content
http://localhost:14000/goods-service-prod/goods/content
测试结果:使用微服务名和不使用微服务名均可访问
server:
port: 14000
spring:
application:
name: api-gateway
eureka:
client:
service-url:
defaultZone: http://localhost:11000/eureka/
zuul:
ignored-services: '*' # 忽略的微服务名,多个使用逗号分隔开,全部忽略使用*,但需要在*号两端加上单引号或者双引号
routes:
goods:
serviceId: goods-service-prod
path: /goods/**
strip-prefix: false
启动服务,再次测试:http://localhost:14000/goods-service-prod/goods/content
测试发现不能访问
输入 http://localhost:14000/goods/content 访问成功
server:
port: 14000
spring:
application:
name: api-gateway
eureka:
client:
service-url:
defaultZone: http://localhost:11000/eureka/
zuul:
ignored-services: '*' # 忽略的微服务名,多个使用逗号分隔开,全部忽略使用*,但需要在*号两端加上单引号或者双引号
prefix: /qf #访问的url地址中必须有这个前缀,否则不能访问
routes:
goods:
serviceId: goods-service-prod
path: /goods/**
strip-prefix: false
<!--rate limit 限流包-->
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>1.3.4.RELEASE</version>
</dependency>
server:
port: 14000
spring:
application:
name: api-gateway
eureka:
client:
service-url:
defaultZone: http://localhost:11000/eureka/
zuul:
ignored-services: '*' # 忽略的微服务名,多个使用逗号分隔开,全部忽略使用*,但需要在*号两端加上单引号或者双引号
prefix: /qf #访问的url地址中必须有这个前缀,否则不能访问
routes:
goods:
serviceId: goods-service-prod
path: /goods/**
strip-prefix: false
ratelimit:
enabled: true
#IN_MEMORY 内存存储 使用的ConcurrentHashMap存储,默认值
#REDIS redis存储
#CONSUL 使用consul存储
#JPA 使用数据库存储
# repository: IN_MEMORY #对应存储类型(用来存储统计信息)
default-policy: #针对所有的路由配置的策略,除非特别配置了policies
# 60 秒内只允许访问100次,这100次的请求总时间不能超过200秒
limit: 100 #每个刷新时间窗口对应的请求数量限制
quota: 200 #每个刷新时间窗口对应的请求时间限制(秒)
refresh-interval: 60 # 刷新时间窗口的时间,默认值 (秒)
type: #限流方式
- user #针对特定用户群
- origin #针对IP地址
- url #针对URL请求地址
policies:
goods: #特定的路由,与路由中的配置有关
limit: 100 #可选- 每个刷新时间窗口对应的请求数量限制
quota: 200 #可选- 每个刷新时间窗口对应的请求时间限制(秒)
refresh-interval: 60 # 刷新时间窗口的时间,默认值 (秒)
type: #可选 限流方式
- user
- origin
- url
启动服务进行测试:http://localhost:14000/qf/goods/content
反复刷新,测试限流效果
<!-- 修改Zuul的负载均衡策略,也就是进行灰度发布使用的 -->
<dependency>
<groupId>io.jmnarloch</groupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
<version>2.1.0</version>
</dependency>
package com.qf.spring.cloud.gateway.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.HttpServletRequest;
public class GrayReleaseFilter extends ZuulFilter {
public String filterType() {
return "pre";
}
public int filterOrder() {
return 0;
}
public boolean shouldFilter() {
// 进行跨域请求的时候,并且请求头中有额外参数,比如token,客户端会先发送一个OPTIONS请求来探测后续需要发起的跨域POST请求是否安全可接受
// 所以这个请求就不需要拦截,下面是处理方式
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
//OPTIONS请求不做拦截操作
return false;
}
return true;
}
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String requestURI = request.getRequestURI();
AntPathMatcher matcher = new AntPathMatcher();
if (matcher.match("/goods/**", requestURI)){
double rate = Math.random();
if (rate <= 0.1) {
//也就是百分之10的请求转发到forward=1的服务上去
RibbonFilterContextHolder.getCurrentContext().add("grayRelease", "1");//这句话就代表将请求路由到metadata-map里grayRelease为1的那个服务
} else {
//百分之90的请求转发到forward=2的服务上去
RibbonFilterContextHolder.getCurrentContext().add("grayRelease", "2");//这句话就代表将请求路由到metadata-map里grayRelease为2的那个服务
}
}
return null;
}
}
goods-service01 工程的 yml 配置
eureka:
client: #eureka客户端
service-url:
#对外暴露的查询服务的地址,供服务订阅者获取服务使用
defaultZone: http://localhost:11000/eureka/
instance:
instance-id: goods-service01
goods-service02 工程的 yml 配置
eureka:
client: #eureka客户端
service-url:
#对外暴露的查询服务的地址,供服务订阅者获取服务使用
defaultZone: http://localhost:11000/eureka/
instance:
instance-id: goods-service02
package com.qf.spring.cloud.gateway.gray;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.loadbalancer.AbstractServerPredicate;
import com.netflix.loadbalancer.PredicateKey;
import com.netflix.loadbalancer.Server;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import java.util.Arrays;
import java.util.List;
public class GrayReleasePredicate extends AbstractServerPredicate{
//这里就是要进行灰度发布的实例ID,生产环境可以使用定时器任务将灰度发布信息缓存自redis,然后从redis中获取
private List<String> grayReleaseServiceInstanceIds = Arrays.asList("goods-service02");
@Override
public boolean apply(PredicateKey predicateKey) { //包含负载均衡使用的key和一个Server实例的实体
Server server = predicateKey.getServer();
String instanceId;
//DiscoveryEnabledServer表示能够被发现且具有元数据信息的Server
if(server instanceof DiscoveryEnabledServer){
InstanceInfo instanceInfo = ((DiscoveryEnabledServer) server).getInstanceInfo();
System.out.println(instanceInfo);
instanceId = instanceInfo.getInstanceId().toUpperCase();
} else {
System.out.println(server);
instanceId = server.getId().toUpperCase();
}
if(grayReleaseServiceInstanceIds.stream().anyMatch(id->id.toUpperCase().equals(instanceId))){
double rate = Math.random();
System.out.println(rate + instanceId);
return rate < 0.4;
} else {
return true;
}
}
}
package com.qf.spring.cloud.gateway.gray;
import com.netflix.loadbalancer.AbstractServerPredicate;
import com.netflix.loadbalancer.PredicateBasedRule;
public class GrayReleaseRule extends PredicateBasedRule {
private GrayReleasePredicate grayReleasePredicate;
//这个构造方法必须有,由系统来调用
public GrayReleaseRule(){}
//这个是我们编写的规则
public GrayReleaseRule(GrayReleasePredicate grayReleasePredicate){
this.grayReleasePredicate = grayReleasePredicate;
}
@Override
public AbstractServerPredicate getPredicate() {
return grayReleasePredicate;
}
}