你的点赞就是对我最大的支持。java
原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。
本文将从知识拓扑讲起,谈一下api网关的功能,以及spring cloud gateway的使用方法。文章很长,能够先过一下目录。react
1、知识拓扑 (使用和原理) 2、网关的做用 3、Predicate,路由匹配 4、Filter,过滤器编写 5、自定义过滤器 6、常见问题
为何不少人以为spring cloud gateway难用?由于它的背后用的是webflux
,涉及到响应式编程,而不是传统的过程式编程。nginx
咱们把背后的技术梳理一下,不难发现,这个晦涩的根源,就来自于project reactor,与spring项目并驾齐驱的,”面向将来”的响应式编程框架。程序员
结果最后的代码,都长的和lambda同样。其背后的思想,是观察者模式和非阻塞杂交的产物,学习曲线相对陡峭。web
spring cloud gateway涉及到许多比较新的知识和理念,但仅仅对于使用来讲,坡度并非很大。正则表达式
咱们能够想象一下一个路由的必要元素:web请求,经过一些匹配条件,定位到真正的服务节点。并在这个转发过程的先后,进行一些精细化控制。redis
其中,predicate就是咱们的匹配条件;而filter,就能够理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就能够实现一个具体的路由了。spring
因为spring cloud gateway是基于springboot的,因此使用yml进行路由的配置。yml的层次一般比较深,这就形成了配置文件看起来很是的乱。它也可使用java代码(或者kotlin)进行路由的编写,风格偏向函数编程,因此须要首先了解lambda表达式的写法。编程
spring cloud gateway大多数时候是做为http服务的网关,能够针对http的报文进行一些细粒度的控制,因此还须要对http协议有较多的理解,才能在使用时游刃有余。后端
而在原理方面,却复杂的多。因为实践方面的滞后性,现有的组件大多数尚未追上“响应式”这个“超前”的理念,催生了一堆晦涩的组件(主要是专用函数太多)。好在,使用spring cloud gateway并不须要直接接触这些api。
最重要的,就是对webflux框架的封装。webflux是能够替代spring mvc的一套解决方案,能够编写响应式的应用,二者之间的关系能够看下图。它的底层使用的是netty,因此操做是异步非阻塞的。
再往下走,webflux是运行在project reactor之上的一个封装,其根本特性是由后者提供的。这个东西和vert.x同样,初次接触使用起来会感受特别怪异。
reactor是观察者模式的发扬,因此里面有Publisher的概念,其中最主要的实现,就是Flux和Mono。所谓的webflux,取名就在于此。
reactor参考: https://url.cn/5B7f5iY
从传统的开发模式过渡到reactor的开发模式,是有必定成本的。若是有时间能够了解一下背后的原理,对spring cloud gateway的使用,仍是有好处的。
从名字就能够看到,它是一个网络的关卡,不管后端多么的复杂,这个对外的关卡表现是一致的。
更加剧要的是,隐藏在关卡后面的一些通用的事务,均可以抽象出来进行处理。能够把网关,想像成一个相似于海关的东西,你的签证资料准备、安检、调度等,均可以统一进行处理。
api网关就是伴随着微服务概念兴起的一种架构模式,固然也不只限于微服务。从图中咱们能够看到网关的位置。
且看下面网关的具体做用。
这个是全部网关,包括nginx的基本功能。除了可以对服务进行整形,网关一个很是重要的附加收益,就是对后端的服务细节进行了屏蔽。
反向代理同时会带有负载均衡的功能,包括带权重的流量分配。
就是权限认证,也就是常说的权限系统。因为鉴权服务有很是高的类似性,就能够进行抽象处理,放在网关层。
好比https协议的统一接入,分布式session的处理问题,新的登陆鉴权通道的接入等。
流量控制若是分散到每一个服务里去作,就是一种灾难,网关是最适合的地方。
流量控制一般有多种策略,对后端服务进行屏蔽。非正常请求和超出负载能力的请求,都会被快速拦截在外,为系统的稳定性提供了必不可少的支持。
流量控制有单机限流和分布式限流两种方式,后者控制更加精细一些,spring cloud gateway都有提供。
熔断与流控的主要区别,在于前者在一段时间内,服务“不可用”,然后者仅几率性失败。
除了服务之间的调用涉及到熔断,在网关层的熔断,做用范围会更大,须要对服务进行准确的分级。
网关的一个终极功能,就是实现服务的灰度发布。好比常说的AB test,就是一种灰度发布方式。
灰度会进行精细化控制,好比针对一类用户,某个物理区域,特定请求路径,特定模块,随机百分比等方面的一些灰度控制等。
灰度是一个总体架构配合的结果,但协调的入口就是网关,经过对请求头或者参数加入一些特定的标志,就能够对每一个请求进行划分,决定是否落入灰度。
网关是最适合进行日志监控的地方。经过对访问日志的精细分析,可以获得不少有价值的数据,进而对后端服务的优化提供决策依据。
好比,某个“业务”的访问趋势,运营数据,QPS峰值,同比、环比等。
spring cloud gateway的配置方式有Fluent API和yml两种方式,都操蛋的很。
Predicate在英文中是断言的意思。这里咱们能够看做是条件匹配,可以根据http头或者http参数进行匹配。
在某个时间点以前,或者以后的匹配。好比让路由在某个时间段内生效。
配置文件相似于:
spring: cloud: gateway: routes: - id: after_route uri: https://example.org predicates: - After=2020-10-20T17:42:47.789-07:00[America/Denver]
其中。id
是本路由的惟一不可重复名称,uri指定匹配后的路由地址,而predicates的After,就是咱们的时间匹配器。
1.以后
或者翻译成代码方式。
builder.routes().route( r -> r.after(LocalDateTime.of(2020, 10, 17, 42, 47).atZone(ZoneId.of("America/Denver"))) .uri("https://example.org") );
因为代码大部分相似,下面的篇幅,咱们只截取最主要的片断。
2.以前
上面是某个时间点以后,以前的写法,以下:
Before=2017-01-20T17:42:47.789-07:00[America/Denver] r.before(LocalDateTime.of(2020, 10, 17, 42, 47).atZone(ZoneId.of("America/Denver")))
3.之间
还有在某个时间段以内的
Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] r.between( LocalDateTime.of(2020, 10, 17, 42, 47).atZone(ZoneId.of("America/Denver")), LocalDateTime.of(2027, 10, 17, 42, 47).atZone(ZoneId.of("America/Denver")) )
咱们简单看一下一个http请求的信息,其中,General和Request Headers中的信息,均可以进行匹配控制。对于Cookie、Host等经常使用的信息,还进行了专门的优化。这其中,最经常使用的,就是path、cookie、host、query等。
Path
path是最重要的匹配方式,多个path可使用,
分隔。
Path=/foo/{segment},/bar/{segment} r.path("/foo/{segment}","/bar/{segment}")
注意,咱们将{segment}使用大括号围了起来,这个值,能够经过代码取出来。
Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange); String segment = uriVariables.get("segment");
Header头信息
Header=X-Request-Id, \d+ r.header("Header=X-Request-Id", "\\d+")
与cookie相似,这里指的是http头方面的匹配,不少灰度信息,或者trace信息,就喜欢放在这里。
Cookie [header]
Cookie=chocolate, ch.p r.cookie("chocolate","ch.p")
http信息中,是否有一个名字叫作chocolate
的Cookie,是否与正则ch.p
匹配。
Host信息 [header]
虽然host信息也在header信息里,可是因为它太经常使用了,因此有专门的匹配器。
Host=**.somehost.org,**.anotherhost.org r.host("Host=**.somehost.org","**.anotherhost.org")
注意,这里的匹配字符串,是Ant风格的,更简洁一些,并非java中的正则表达式。多个host使用,
进行分隔。
Request Method
Method=GET r.method("GET")
注意,我在源代码里没有找到大小写转换的代码,因此路由中切记保持大写方式。除了CONNECT,都支持。
Query
这里指的就是url问号后面的一串参数。
Query=baz r.query("baz") Query=foo, ba. r.query("foo","ba.")
太简单,都不须要我作过多介绍了。
RemoteAddr
RemoteAddr=192.168.1.1/24 r->r.remoteAddr("192.168.1.1/24")
权重信息的配置,有点2b。好比,咱们后面有2台服务器,spring cloud gateway对其作了两个路由,其中连接的枢纽就是一个叫作Weight
的group。
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
一样的代码以下。
builder.routes() .route("weight_high",r -> r.weight("group1", 8).uri("https://weighthigh.org")) .route("weight_low",r -> r.weight("group1", 2).uri("https://weightlow.org"));
假如服务有100个节点,还有一堆filter,要重复配置100次?不得不说很是的fuck。
匹配,可以定位到要进行代理的路由。如今,已经进入到了咱们的路由内部。上面提到的路由的做用,大部分功能就是在这里进行配置的。
用过zuul网关的可能都知道,在自定义路由时,会有pre和post两个注解控制在代理先后的路由行为。spring cloud gatewa有着一样的功效。
crud不只仅存在SSM中,路由的配置也是如此。你可能在路由到真正的后端服务以前,对http头或者其余信息修改;或者在代理到相应的连接以后,再进行一些修改。
按照咱们的理解,所谓request对应的是pre,而response对应的是post。
AddRequestHeader=X-Request-Foo, Bar AddRequestParameter=foo, bar AddResponseHeader=X-Response-Foo, Bar RemoveRequestHeader=X-Request-Foo RemoveResponseHeader=X-Response-Foo RemoveRequestParameter=foo SetRequestHeader=X-Request-Foo, Bar SetResponseHeader=X-Response-Foo, Bar SetStatus=401
这个就蛋疼了一些,缘由仍是由webflux引发的,在写法上比较个性一些。
.filters(f -> f.modifyRequestBody(String.class, String.class, MediaType.APPLICATION_JSON_VALUE, (exchange, s) -> { return Mono.just(s.toUpperCase()); })
上面的代码,将requestBody中的内容,所有转成了大写方式。
类似的,response对应的是modifyResponseBody,写法是相似的。具体的能够参见
ModifyRequestBodyGatewayFilterFactory
的代码。若是没有接触过上面说到的理论部分,读起来仍是比较吃力的。
RedirectTo=302, https://acme.org .filters(f -> f.redirect(302,"https://acme.org"))
直接重定向。这个比较简单,不作过多介绍。
重点。
StripPrefix=2 .filters(f->f.stripPrefix(2))
StripPrefix能够接受一个非负整数,用于去掉对应的前缀。好比,外部访问的path是/a/b/c/d
那么,转向后端服务的path,就是/c/d
,去掉了/a/b
前缀。
这属于路径重写的一种特殊方式,经常使用在对uri为lb://
协议的微服务路径重写。
RewritePath是和nginx的路径重写很是相近的一个东西。
RewritePath=/foo(?<segment>/?.*), $\{segment} f.rewritePath("/foo(?<segment>/?.*)", "${segment}")
官方说说明,因为yml配置文件的缘故。要把$
写成$\
的方式,可是java代码中并不须要这么作。因为内部使用的仍是java的正则,同时用上了group的概念,代码真是脏的能够。
默认集成的断路器,依然是hystrix。
Hystrix=myCommandName .filters(f -> f.hystrix(c->c.setName("myCommandName")))
另外,熔断还有一个参数叫作fallbackUri
,但惋惜的是,只支持forward方式。好比:
fallbackUri: forward:/myfallback
对于一些对稳定性要求很是高的服务,一个没法回避的问题,就是重试。重试的参数比较多,一个典型的配置以下:
- name: Retry args: retries: 3 statuses: BAD_GATEWAY backoff: firstBackoff: 10ms maxBackoff: 50ms factor: 2 basedOnPreviousValue: false
其中,backoff指定了重试的策略和间隔,会按照公式firstBackoff * (factor ^ n)
进行增加。
熔断保证了服务的安全性,重试保证了服务的健壮性,要注意甄别使用场景。
内置的限流器,若是被触发,将返回"HTTP 429 - Too Many Requests"错误。
限流器的参数是一个叫作KeyResolver实现,其中,就有咱们上面提到的概念Mono
。因此若是你想要扩展这个限流器的话,就须要了解webflux那一套东西。
public interface KeyResolver { Mono<String> resolve(ServerWebExchange exchange); }
同时,基于redis的令牌桶原理的分布式限流。因为底层使用的是"spring-boot-starter-data-redis-reactive",因此就拥有了“响应式”的应用特色,支持 WebFlux (Reactor) 的背压(Backpressure)。对于其中的配置,是有些绕的,好比官方的这段配置。
- name: RequestRateLimiter args: key-resolver: '#{@ipKeyResolver}' redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20
咱们就须要定一个名字叫作ipKeyResolver
的bean。
限流的维度不少,须要自行开发管理后台。因为篇幅缘由,咱们不作展开讨论。
spring cloud gateway的过滤器,有全局过滤器和局部过滤器之分,对应的接口为GatewayFilter
和GlobalFilter
。
若是内置的过滤器不能知足需求,则可经过自定义过滤器解决。经过实现GatewayFilter
和Ordered
接口,能够进行更加灵活的控制。
能够参考内置过滤器的实现方式。后面的文章,咱们将详细介绍这方面的具体代码实现。
lb://表示什么?
lb://serviceName是spring cloud gateway在微服务中自动为咱们建立的负载均衡uri,在某些特殊状况下,能够直接书写。好比,在eureka中的注册名称为pay-rpc,则此时的写法为:
lb://pay-rpc
如何修改http内容?好比method?
注意ServerWebExchange这个东西。使用它的
exchange.mutate()函数,能够进入修改模式。好比,把GET转成POST方式:
ServerHttpRequest request = exchange.getRequest(); if (request.getMethod() == HttpMethod.GET) { exchange = exchange.mutate().request(request.mutate().method(HttpMethod.POST).build()).build(); }
如何动态更新路由?
主要是经过actuator管理接口,确保这些内容放在了内网中。
GET /actuator/gateway/routes 路由列表 GET /actuator/gateway/routes/{id} 获取某个路由信息 GET /actuator/gateway/globalfilters 全局过滤器 GET /actuator/gateway/routefilters filter列表 POST /actuator/gateway/refresh 刷新路由 POST /gateway/routes/{id_route_to_create} 建立路由 DELETE /gateway/routes/{id_route_to_delete} 删除某个路由
如何作一些数据统计
这个功能简单的很,咱们只须要实现一个全局的过滤器,就能够加入任何统计功能。经常使用的方式有两种:经过日志进行分析;经过应用内聚合进行分析。
这二者都不是很难,主要在于对功能的规划而不是代码。
我有更高级的功能,好比解密数据的需求,该如何作?
这个就要本身实现过滤器了。
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
经过ServerWebExchange,能够控制整个请求过程当中的任何一个参数的添加,修改,删除,重写等。在代理方法先后,能够经过
exchange.getAttributes().put(); exchange.getAttribute()
这两个函数,进行参数传递
因此,即便官方不编写任何上面提到的filter,咱们依然能够用这个基本接口玩的转。
微信公众号真的是不太适合写一些教程类的文章,因此本文依然是一个总结性的经验之谈。
随着zuul1的退出和zuul2的难产,亲生的SCG成为了最优的选择。Spring团队颇有意思,直接采用了webflux做为后端的技术(改怕了?),这会让不少人痛痛痛:又要学习新技术了。
本文并无测试SCG的性能,这个已经有不少团队进行验证了,效果都不错。
但如今的spring cloud gateway,问题还不少。好在这个问题是使用问题,并非功能问题。它已经内置了很是多的Predicate和Filter,但不少时候并不能解决问题,须要使用者自行建立本身的过滤器。好吧,个人大多数过滤器全是自行建立的。
另外吐槽一下Fluent API和yml的配置方式,真是丑的一b,须要开发一个管理后台。还有复杂的java正则的那些东西,都让人抓狂--请看墙上那些深深的爪痕,就是个人杰做。
做者简介: 小姐姐味道 (xjjdog),一个不容许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不同的味道。个人我的微信xjjdog0,欢迎添加好友,进一步交流。