背景:
随着微服务的流行,服务数量也在急剧增加,为了保障服务的安全性(鉴权,限流,过滤掉非法的路由请求,减少后面微服务的压力),各个服务之间都进行处理的话又会显得代码臃肿不易维护,并且破坏了微服务本身的逻辑;提供一个公共包或许也可以解决这个问题,但这需要你每个业务服务都去添加公共服务的依赖,无端增大了jar包的大小,同时去修改公共包的时候其他所有的业务包需要重新依赖新版本的公共包,重新构建,灵活度大大降低;而网关的引入将很好的解决这个问题;
网关解决的问题:
传统的不用网关:
使用网关:
没网关时存在的问题:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性
- 存在跨域请求,在一定场景下相对复杂
- 身份认值问题,每个微服务需要独立的身份认值
- 某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难
引入网关所带来的优势:
- 易于监控,可在微服务网关收集监控数据并将其推送外部系统进行分析
- 易于认证,可在微服务网关中进行认证,然后再将其转发到后端的微服务
- 减少客户端与各个微服务的交互次数 网关解决的问题:
- 性能:API高可用,负债均衡,容错机制
- 安全:权限身份认证,脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)
- 日志:日志记录,一旦设计分布式,全链路跟踪必不可少
- 缓存:数据缓存
- 监控:记录请求响应数据,API耗时分析,性能监控
- 限流:流量控制,错峰流控,可定义多种限流规则
- 灰度:线上灰度部署,减少风险
- 路由:动态路由规则
技术的选型:
市面上常用的网关类型层出不穷主要,主要分为以下四类:
1、Nginx(Kong):
Kong是一款基于OpenResty(Nginx + Lua模块)编写的高可用、易扩展的,由Mashape公司开源的API Gateway项目;
Nginx + Lua对开发人员的技术要求性较高,有兴趣的大神可以尝试
2、Zuul
Zuul 是 Netflix 开源的微服务网关组件,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用。
java实现,zuul1.x是基于Servlet,不支持长连接(如socket)是阻塞的io性能最差,zuul2.x引入了netty,虽然是非阻塞的,但是目前还没整合到最新的Spring Cloud Starter Netflix Zuul依赖中(貌似两个主开发人员跑路了,并且spring团队重新研发了Spring Cloud Gateway网关,估计整合的事情遥不可及)
3、Spring Cloud Gateway
Spring Cloud Gateway 是Spring Cloud的一个全新的API网关项目,目的是为了替换掉Zuul1。Gateway可以与Spring Cloud Discovery Client(如Eureka)、Ribbon、Hystrix等组件配合使用,实现路由转发、负载均衡、熔断等功能,并且Gateway还内置了限流过滤器,实现了限流的功能。
java实现基于webflux非阻塞io,性能不错是zuul1.x的1.6倍,上手简单,也是我的选择
4、Soul
官方介绍:这是一个异步的,高性能的,跨语言的,响应式的API网关。参考了Kong,Spring-Cloud-Gateway等优秀的网关后,站在巨人的肩膀上,Soul由此诞生!
性能优于Spring Cloud Gateway,功能更加全面,无缝整合dubbo和spring cloud,此外Soul2.0以后版本可以摆脱ZK,丰富的功能;
其实从性能上看的话,Nginx(Kong) > Soul > Zuul和Spring Cloud Gateway
Spring Cloud Gateway的原理
三大术语:
(1)Filter(过滤器):
和Zuul的过滤器在概念上类似,可以使用它拦截和修改请求,并且对上游的响应,进行二次处理。过滤器为org.springframework.cloud.gateway.filter.GatewayFilter类的实例。
分为网关过滤器(GatewayFilter)和全局过滤器(GlobalFilter),
网关过滤器需要手动配置作用的路由链接,否则无效
全局过滤器无需配置,默认作用于所有路由链接
(2)Route(路由):
网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。
(3)Predicate(断言):
Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
gateway内置了多种断言规则例:
1、通过时间匹配(datetime)
2、通过Cookie匹配(Cookie)
3、通过 Header 属性匹配(Header)
4、通过Host匹配(Host)
5、通过请求方式匹配(Method)
6、通过请求路径匹配(Path) 较为常用
7、通过请求参数匹配(QueryParam)
8、通过请求 ip 地址进行匹配(RemoteAddr)
8、组合Predicate(可结合上诉多个断言条件)
部分代码:
这边我采用的是动态路由并数据库持久化的方式写的一个Demo
sql:
CREATE TABLE “APP_CENTER”.“GATEWAY_API_DEFINE” (
“ID” NUMBER(21,0) NOT NULL, – 主键
“PATH” VARCHAR2(256 BYTE), – 断言路径
“SERVICE_ID” VARCHAR2(256 BYTE), – 服务ID(应用代码)
“URL” VARCHAR2(256 BYTE), – 请求地址
“RETRYABLE” NUMBER(1,0) NOT NULL, – 重试机制
“ENABLED” NUMBER(1,0) NOT NULL, – 启用状态
“STRIP_PREFIX” NUMBER(11,0) NOT NULL, – 代理前缀
“TYPE” NUMBER(11,0) NOT NULL, – 类型
“API_NAME” VARCHAR2(256 BYTE), – API名称
“PRIORITY” NUMBER(11,0) NOT NULL – 优先级
)
pom.xml部分代码
路由初始化代码
package com.firesoon.gateway.service;
import com.firesoon.gateway.entity.pojo.GatewayRoute;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author: liang
* @Date: 2021/1/11 14:44
* @Description
*/
@Slf4j
@Service
public class GatewayServiceHandler implements ApplicationEventPublisherAware, CommandLineRunner {
private ApplicationEventPublisher applicationEventPublisher;
@Autowired
private RouteDefinitionRepository routeDefinitionRepository;
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
public void run(String... args) throws Exception {
this.refresh();
}
public void refresh(){
List<GatewayRoute> gatewayRouteList = jdbcTemplate.query(
"select ID as id, PATH as path, SERVICE_ID as serviceId, " +
"URL as url, RETRYABLE as retryable, ENABLED as enabled, " +
"STRIP_PREFIX as stripPrefix, TYPE as type, API_NAME as apiName, " +
"PRIORITY as priority from GATEWAY_API_DEFINE " +
"where enabled = 1 order by priority desc",
new BeanPropertyRowMapper<>(GatewayRoute.class));
if(!CollectionUtils.isEmpty(gatewayRouteList)){
gatewayRouteList.forEach(gatewayRoute -> {
RouteDefinition routeDefinition = convertRouteDefinition(gatewayRoute);
if(routeDefinition != null){
routeDefinitionRepository.save(Mono.just(routeDefinition)).subscribe();
}
});
}
this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
private RouteDefinition convertRouteDefinition(GatewayRoute gatewayRoute) {
RouteDefinition definition = new RouteDefinition();
String url = gatewayRoute.getUrl();
if(StringUtils.isBlank(url)) {
return null;
}
URI uri = null;
if(url.startsWith("http")){
// http地址
uri = UriComponentsBuilder.fromHttpUrl(url).build().toUri();
}else{
// 其他类型的请求或注册中心
uri = UriComponentsBuilder.fromUriString(url).build().toUri();
}
definition.setId(gatewayRoute.getServiceId());
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName("Path");
Map<String, String> predicateParams = new HashMap<>(8);
predicateParams.put("pattern", gatewayRoute.getPath());
predicate.setArgs(predicateParams);
definition.setPredicates(Arrays.asList(predicate));
// FilterDefinition filterDefinition = new FilterDefinition();
// // 名称是固定的, 路径去前缀
// filterDefinition.setName("AddRequestHeader");
// Map<String, String> filterParams = new HashMap<>(8);
// // 该_genkey_前缀是固定的,见org.springframework.cloud.gateway.support.NameUtils类
// filterParams.put(NameUtils.GENERATED_NAME_PREFIX + 0, "header");
// filterParams.put(NameUtils.GENERATED_NAME_PREFIX + 1, "addHeader");
// filterDefinition.setArgs(filterParams);
// definition.setFilters(Arrays.asList(filterDefinition));
definition.setUri(uri);
definition.setOrder(gatewayRoute.getPriority());
return definition;
}
public static void main(String[] args) {
String text = "StripPrefix=1";
int eqIdx = text.indexOf(61);
if (eqIdx <= 0) {
System.out.println(text);
} else {
System.out.println(text.substring(0, eqIdx));
String[] array = org.springframework.util.StringUtils.tokenizeToStringArray(text.substring(eqIdx + 1), ",");
for(int i = 0; i < array.length; ++i) {
System.out.println(NameUtils.generateName(i));
System.out.println(array[i]);
}
}
}
}
全局过滤器的定时(可以实现自身过滤逻辑,getOrder方法控制优先级int类型,数字越小越先执行,默认为0,可以为负数)
@Slf4j
@Component
public class PathSourceFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}