⼤家都知道的是在微服务架构中,⼀个系统会被拆分为很多个微服务。那么作为客户端要如何去调⽤这么多的微服务呢?如果没有⽹关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调⽤。
这样的架构,存在的问题
① 每个业务都会需要鉴权、限流、权限校验、跨域等逻辑,如果每个业务都各自为战,自己造轮子实现一遍,会很蛋疼,完全可以抽出来,放到一个统一的地方去做。
② 如果业务量比较简单的话,这种方式前期不会有什么问题,但随着业务越来越复杂,比如淘宝、亚马逊打开一个页面可能会涉及到数百个微服务协同工作,如果每一个微服务都分配一个域名的话,一方面客户端代码会很难维护,涉及到数百个域名,另一方面是连接数的瓶颈,想象一下你打开一个APP,通过抓包发现涉及到了数百个远程调用,这在移动端下会显得非常低效.
③ 后期如果需要对微服务进行重构的话,也会变的非常麻烦,需要客户端配合你一起进行改造,比如商品服务,随着业务变的越来越复杂,后期需要进行拆分成多个微服务,这个时候对外提供的服务也需要拆分成多个,同时需要客户端配合你进行改造,非常蛋疼。
网关就是用来解决上述问题的
关注稳定与安全
提供更好的服务
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控路由转发等等。
添加上API网关之后,系统的架构图变成了如下所示:
Spring Cloud Gateway是由WebFlux + Netty + Reactor实现的响应式的API网关。它不能在传统的servlet容器中工作,也不能构建成war包
Spring Cloud Gateway旨在为微服务架构提供一种简单且有效的API路由的管理方式,并基与Filter提供网关的基本功能,例如说安全认证、监控、限流等等。
用来代提Zuul的,性能是zuul的1.6倍
Spring Cloud Gateway 功能特征
路由(route)
- 路由是网关中最基础的部分,路由信息包括一个ID、一个目的URI、一组断言工厂、一组Filter组成。如果断言为真,则说明请求的URL和配置的路由匹配.
断言(predicates)
- Java8中的断言函数,SpringCloud Gateway中的断言函数美型是Spring5.0框架中的SeverWebExchange。断言函数允许开发者去定义匹配Http request中的任何信息,比如请求头和参数等
过滤器(Filter)
- SpringCloud Gateway中的filter分为Gateway Filler和Global Filter。Filter可以对请求和响应进行处理。
Gateway核⼼架构
路由( Route ) 是 gateway 中最基本的组件之⼀,表示⼀个具体的路由信息载体。主要定义了下⾯的⼏个信息:
id:路由标识符,区别于其他 Route 。
uri:路由指向的⽬的地 uri ,即客户端请求最终被转发到的微服务。
order:⽤于多个 Route 之间的排序,数值越⼩排序越靠前,匹配优先级越⾼。
predicate:断⾔的作⽤是进⾏条件判断,只有断⾔都返回真,才会真正的执⾏路由。
filter:过滤器⽤于修改请求和响应信息。
① 引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
② 配置GataWay
spring:
application:
name: huozhexiao-gateway
cloud:
gateway:
routes: #路由规则列表,指定当请求满足条件转到对应服务
- id: order-router # 路由唯一标识
uri: http://localhost:7001 #需要转发的地址
# 配置断言,用于路由规则匹配
predicates:
- Path=/order-server/**
# http://localhost:7010/order-server/order/add 路由到↓
# http://localhost:7001/order-server/order/add
filters:
- StripPrefix=1 #转发前去掉第一层路径
# - id: stock-router
访问:http://localhost:7010/order-server/order/add
① 引入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-loadbalancerartifactId>
dependency>
② 配置Nacos和GataWay整合
spring:
application:
name: huozhexiao-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.198.129:4399
password: nacos
username: nacos
gateway:
routes: #路由规则列表,指定当请求满足条件转到对应服务
- id: order-router # 路由唯一标识
# - id: stock-router
uri: lb://order-service #需要转发的地址 lb:负载均衡
# 配置断言,用于路由规则匹配
predicates:
- Path=/order-server/**
# http://localhost:7010/order-server/order/add 路由到↓
# http://localhost:7001/order-server/order/add
filters:
- StripPrefix=1 #转发前去掉第一层路径
访问:http://localhost:7010/order-server/order/add
spring:
application:
name: huozhexiao-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.198.129:4399
password: nacos
username: nacos
gateway:
discovery:
locator:
enabled: true # 让gateway可以发现nacos中的微服务
访问:http://localhost:7010/order-server/order/add
作用:当请求 gateway 时, 使用断言进行匹配,如果匹配成功正常路由转发,反之返回404
类型:
内置断言路由工厂
自定义路由断言工厂
SpringCloud Gateway
包括许多内置的断⾔⼯⼚,所有这些断⾔都与 HTTP 请求的不同属性匹配。具体如下:
此类型的断⾔根据时间做判断,主要有三个:
AfterRoutePredicateFactory: 接收⼀个⽇期参数,判断请求⽇期是否晚于指定⽇期。 BeforeRoutePredicateFactory: 接收⼀个⽇期参数,判断请求⽇期是否早于指定⽇期。 BetweenRoutePredicateFactory: 接收两个⽇期参数,判断请求⽇期是否在指定时间段内。
predicates:
- After=2023-05-27T23:16:55.889-07:00[Asia/Shanghai] # 匹配上海时间2023年5月27日23:16之后提出的请求
- Before=2023-05-27T23:16:55.889-07:00[Asia/Shanghai] # 匹配上海时间2023年5月27日23:16之前提出的请求
- Between=2023-05-27T23:16:55.889-07:00[Asia/Shanghai], 2023-05-30T23:16:55.889-07:00[Asia/Shanghai] #匹配上海时间2023年5月27日23:16-上海时间2023年5月30日23:16之间提出的请求
CookieRoutePredicateFactory:接收两个参数, cookie 名字和⼀个正则表达式。
predicates:
- Cookie=huozhexiao, hu. #匹配具有名为huozhexiao其值与hu.正则表达式
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求 Header 是否具有给定名称且值与正则表达式匹配。
predicates:
- Header=X-Request-Id, \d+ #匹配有一个名为X-Request-Id其值与\d+正则表达式的请求
HostRoutePredicateFactory:接收⼀个参数,主机名模式。判断请求的 Host 是否满⾜匹配规则。
predicates:
- Host=**.baidu.com #匹配是否是来源于baidu.com的请求。
MethodRoutePredicateFactory:接收⼀个参数,判断请求类型是否跟指定的类型匹配。
predicates:
- Method=GET #只接收get请求
PathRoutePredicateFactory:接收⼀个参数,判断请求的 URI 部分是否满⾜路径规则。
predicates:
- Path=/huozhexiao/* # 只有请求路径中带有huozhexiao的请求被接受
QueryRoutePredicateFactory :接收两个参数,请求 param 和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。
predicates:
- Query=name, huozhe.+ # 请求参数中必须有name参数,并且名称中要以huozhe开头
RemoteAddrRoutePredicateFactory:接收⼀个 IP 地址段,判断请求主机地址是否在地址段中。根据请求 IP 进⾏路由判断,接收 CIDR 表示法( IPv4 或 IPv6 )的字符串列表(列表最⼩⻓度为1)作为参数, 例如 192.168.1.6/ 24 , 其中 192.168.1.6 是 IP 地址, 24 是⼦⽹掩码。
predicates:
- RemoteAddr=192.168.1.1/24 # 匹配请求的远程地址是,192.168.1.(1-25)
WeightRoutePredicateFactory:接收⼀个[组名,权重], 然后对于同⼀个组内的路由按照权重转发。
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: https://weighthigh.org
predicates:
- Weight=group1, 8
- id: weight_low
uri: https://weightlow.org
predicates:
- Weight=group1, 2
这条路线将把大约80%的流量转发到weighthigh.org约20%的流量流向weighlow.org
这
XForwarded Remote Addr
路由谓词工厂采用以下列表(最小大小为1)sources
,这是CIDR符号(IPv4或IPv6)字符串,例如192.168.0.1/16
(哪里192.168.0.1
是IP地址,并且16
是子网掩码)。这个路由谓词允许根据
X-Forwarded-For
HTTP头。这可以用于反向代理,如负载平衡器或web应用程序防火墙,其中只有当请求来自这些反向代理使用的IP地址的可信列表时,才应该允许该请求。
spring:
cloud:
gateway:
routes:
- id: xforwarded_remoteaddr_route
uri: https://example.org
predicates:
- XForwardedRemoteAddr=192.168.1.1/24
此路由匹配,如果X-Forwarded-For
标题包含,例如,192.168.1.10
.
自定义路由断言工厂需要继承
AbstractRoutePredicateFactory
类,重写appy方法的逻辑。在appy方法中可以通过
exchange.getReuest()
拿到ServerHttpRequest()
对象。从而可以求取到清求的参数、请求方式、请求头等信息。
注意命名要以RoutePredicateFactory结尾
@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config>{
public MyRoutePredicateFactory() {
super(MyRoutePredicateFactory.Config.class);
}
public List<String> shortcutFieldOrder() {
return Arrays.asList("name");
}
@Override
public Predicate<ServerWebExchange> apply(final MyRoutePredicateFactory.Config config) {
return new GatewayPredicate() {
public boolean test(ServerWebExchange exchange) {
if (config.getName().equals("huozhexiao")){
return true;
}else {
return false;
}
}
};
}
//接受配置文件断言信息
public static class Config {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
配置
predicates:
- My=huozhexiao
在 Gateway 中, Filter 的⽣命周期只有两个: pre
和 post
。
PRE: 这种过滤器在请求被路由调⽤之前调⽤。可利⽤这种过滤器实现身份验证、在集群中选择请求的微服 务、记录调试信息等。
POST:这种过滤器在路由到微服务以后执⾏。可⽤来为响应添加标准的 HTTP Header 、收集统计信息和指 标、将响应从微服务发送给客户端等。
Gateway 的过滤器从作⽤范围可分为两种: GatewayFilter 与 GlobalFilter 。
GatewayFilter:应⽤到单个路由或者⼀个分组的路由上。
GlobalFilter:应⽤到所有的路由上。
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 |
Hystrix | 为路由引入Hystrix的断路器保护 | HystrixCommand 的名称 |
FallbackHeaders | 为fallbackUri的请求头中添加具体的异常信息 | Header的名称 |
PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
PreserveHostHeader | 为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 |
RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的url |
RemoveHopByHopHeadersFilter | 为原始请求删除IETF组织规定的一系列Header | 默认就会启用,可以通过配置指定仅删除哪些Header |
RemoveRequestHeader | 为原始请求删除某个Header | Header名称 |
RemoveResponseHeader | 为原始响应删除某个Header | Header名称 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则表达 |
RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正则表达式,重写后的值 |
SaveSession | 在转发请求之前,强制执行WebSession::save 操作 |
无 |
secureHeaders | 为原始响应添加一系列起安全作用的响应头 | 无,支持修改这些安全响应头的值 |
SetPath | 修改原始的请求路径 | 修改后的路径 |
SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后的值 |
SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是数字,也可以是字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的路径的数量 |
Retry | 针对不同的响应进行重试 | retries、statuses、methods、series |
RequestSize | 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large |
请求包大小,单位为字节,默认值为5M |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
Default | 为所有路由添加过滤器 | 过滤器工厂名称及值 |
每个过滤器工厂都对应一个实现类,并且这些类的名称必须以GatewayFilterFactory
结尾
继承
AbstractGatewayFilterFactory
@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config> {
public List<String> shortcutFieldOrder() {
return Arrays.asList("value");
}
public MyGatewayFilterFactory() {
super(MyGatewayFilterFactory.Config.class);
}
@Override
public GatewayFilter apply(MyGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
/*
* 1.获取name参数
* 2. 如果!=value不放行,反正放行
* */
String name = exchange.getRequest().getQueryParams().getFirst("name");
if (StringUtils.isNotBlank(name)){
if (config.getValue().equals(name)){
chain.filter(exchange);
}else {
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
return exchange.getResponse().setComplete();
}
}
//正常请求
return chain.filter(exchange);
}
};
}
public static class Config {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
配置
filters:
- My=huozhexiao
无需配置、可直接使用
具体使用方法看官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#global-filters
继承
GlobalFilter
实现filter()
方法
@Component
public class LogFilter implements GlobalFilter {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info(exchange.getRequest().getPath().value());
return chain.filter(exchange);
}
}
Reactor Netty 访问日志
要启用Reactor Netty 访问日志,请设置
-Dreactor.netty.http.server.accessLogEnabled=true
.
- 它必须是Java系统属性,而不是spring Boot属性。
@Component
@Slf4j
public class TokenGlobalFilter implements GlobalFilter, Ordered {
//鉴权
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("开始鉴权...");
List<String> tokens = exchange.getRequest().getHeaders().get("token");
if (tokens != null || tokens.size() == 0) {
return error(exchange);
}
//获取token
String token = tokens.get(0);
log.info("token:{}",token);
//token判断
return chain.filter(exchange);
}
private Mono<Void> error(ServerWebExchange exchange) {
// 浏览器没有携带token
// 获取响应对象
ServerHttpResponse response = exchange.getResponse();
// 设置响应状态码
response.setStatusCode(HttpStatus.resolve(403));
// 设置返回格式
response.getHeaders().set("ContentType", "application/json;charset=utf-8");
// 设置返回的内容
JsonResult result = new JsonResult(true,"200",null,"用户未登陆...");
String str = JSONArray.toJSONString(result);
DataBuffer data = response.bufferFactory().wrap(str.getBytes(StandardCharsets.UTF_8));
log.info("⽤户未携带token,{}", str);
return response.writeWith(Mono.just(data));
}
//优先级
@Override
public int getOrder() {
return 1;
}
}
通过yml配置
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]‘: //允许跨域访问访问的资源
allowedOrigins: "*" //允许跨域访问的来源
allowedMethods: //允许跨域请求的访问
- GET
- POST
- DELETE
- PUT
-OPTION
通过配置类配置
@configuration
public class corsconfig {
@Bean
public corswebFilter corsFilter() {
corsconfiguration config = new corsconfiguration();
config.addAllowedMethod("*");
config.addAllowedorigin("*");
config.addAllowedHeader("*");
//允许访问的资源
UrlBasedCorsConfigurationSource source = new UrlBasedcorsconfigurationsource(new PathPatternParser());
source.registerCorsconfiguration("/**", config);
return new corsWebFilter(source);
}
}
网关层限流可针对不同路由,也可针对业务接口进行限流,根据接口特征进行限流
官方文档:https://sentinelguard.io/zh-cn/docs/api-gateway-flow-control.html
① 引入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
②添加sentinel控制台配置
spring:
application:
name: huozhexiao-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.198.129:4399
password: nacos
username: nacos
gateway:
routes: #路由规则列表,指定当请求满足条件转到对应服务
- id: order-router # 路由唯一标识
uri: lb://order-service #需要转发的地址 lb:负载均衡
# 配置断言,用于路由规则匹配
predicates:
- Path=/order/**
sentinel:
transport:
dashboard: localhost:1234
整合流控降级详细配置
API分组:可定义一组api进行流控
降级规则
使用自定义异常进行降级处理
@PostConstruct
public void init(){
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
//自定义异常处理
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(new JsonResult(true,HttpStatus.TOO_MANY_REQUESTS.toString(),null,"限流了")));
}
};
//设置自定义异常
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
在庞大的微服务体系中,某个服务出现错误,会很难解决,SkyWalking链路追踪可以帮助我们理清整个服务调用链路,解决问题
skywalking是一个国产开源框架,2015年由吴晟开源,2017年加入Apache孵化器。skywalking是分布式系统的应用程序性能监视工具,专为微服务、云原生架构和基于容器(Docker、K8s、Mesos)架构而设计。它是一款优秀的APM(Application Perfomance Management)工具,包括了分布式追踪、性能指标分析、应用和服务依赖分析等。
市场上的链路追踪框架对比
链路追踪框架对比
zipkin:zipkin是Twitter开源的调用链分析工具,目前基于springcloud sleuth得到了广泛的使用,特点是轻量,使用部署简单。
Pinpoint:Pinpoint是韩国人开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件,U功能强大,接入端无代码侵入
SkyWlaking:SkyWlaking是本土开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件,UI功能较强,接入端无代码侵入。目前已加入Apache孵化器。
CAT是大众点评开源的基于编码和配置的调用链分析,应用监控分析,日志采集,监控报警等一系列的监控平台工具。
SkyWalking主要功能特性
1.多种监控手段,可以通过语言探针和service mesh获得监控的数据
2.支持多种语言自动探针,包括Java、.Net Core和Node JS等
3.轻量高效,无需大数据平台和大量的服务器资源
4.模块化、UI、存储、集群管理都有多种机制可选
5.支持告警
6.优秀的可视化解决方案
- 下载地址:https://skywalking.apache.org/downloads/
选择版本下载
新版本agent目录独立出去了,需要自己下载
启动bin/startup.sh会同时启动(oapservice和webapp)
在webapp/webapp.yml修改webapp端口号,默认为8080
在conf/application.yml修改oapservice端口, 服务启动后会暴露11800和12800端口,分别为收集监控数据的端口11800和接收前端请求的端口12800
启动微服务时添加配置
更多配置见:https://skywalking.apache.org/docs/skywalking-java/next/en/setup/service-agent/java-agent/configurations/
-javaagent:E:\skywalking\apache-skywalking-apm-bin\skywalking-agent\skywalking-agent.jar -DSW_AGENT_NAME=stock-service #服务名
-DSW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800 #aollector连接地址
SkyWalking默认使用h2进行持久化
修改conf/application.yml文件
storage:
selector: ${SW_STORAGE:mysql}
mysql:
properties:
jdbcUrl: ${SW_JDBC_URL:"jdbc:mysql://192.168.198.129:3306/swtest?rewriteBatchedStatements=true"}
dataSource.user: ${SW_DATA_SOURCE_USER:root}
dataSource.password: ${SW_DATA_SOURCE_PASSWORD:root}
dataSource.cachePrepStmts: ${SW_DATA_SOURCE_CACHE_PREP_STMTS:true}
dataSource.prepStmtCacheSize: ${SW_DATA_SOURCE_PREP_STMT_CACHE_SQL_SIZE:250}
dataSource.prepStmtCacheSqlLimit: ${SW_DATA_SOURCE_PREP_STMT_CACHE_SQL_LIMIT:2048}
dataSource.useServerPrepStmts: ${SW_DATA_SOURCE_USE_SERVER_PREP_STMTS:true}
metadataQueryMaxSize: ${SW_STORAGE_MYSQL_QUERY_MAX_SIZE:5000}
maxSizeOfBatchSql: ${SW_STORAGE_MAX_SIZE_OF_BATCH_SQL:2000}
asyncBatchPersistentPoolSize: ${SW_STORAGE_ASYNC_BATCH_PERSISTENT_POOL_SIZE:4}
启动skywalking会自动建表
报错:Failed to get driver instance for jdbcUrl=jdbc:mysql://192.168.198.129:3306/swtest?rewriteBatchedStatements=true
缺少mysql驱动,oap-libs中导入mysql驱动包即可
如果我们希望对项目中的业务方法,实现链路追踪,方便我们排查问题,可以使用如下的代码
<dependency>
<groupId>org.apache.skywalkinggroupId>
<artifactId>apm-toolkit-traceartifactId>
<version>8.12.0version>
dependency>
@Trace
注解 @Trace
public Order create(Order order) {
orderMapper.add(order);
//扣减库存能否成功
stockFegin.reduct(order.getProduct_id());
int a = 1 / 0;
return order;
}
@Tags
或@Tag
增加其他额外信息例如参数和返回信息
@Trace
@Tags({@Tag(key = "param",value = "arg[0]"),
@Tag(key = "order",value = "returnObj")})
public Order create(Order order) {
orderMapper.add(order);
//扣减库存能否成功
stockFegin.reduct(order.getProduct_id());
int a = 1 / 0;
return order;
}
疑似出现异常或者出现异常进行告警
告警规则
SkyWalking 的发行版都会默认提供config/alarm-settings.yml
文件,里面预先定义了一些常用的告警规则。如下;
告警规则配置项的说明: