springCloud学*4(Zuul服务路由)
本文内容纲要:
-一、背景
-二、服务网关
-1、什么是服务网关
-2、功能
-三、实战
-1、创建zuulsvr项目
-2、配置zuul
-3、路由配置
-1)、服务发现自动映射
-2)、服务发现手动手动
-3)、静态URL手动映射
-4、动态重载路由
-5、服务超时
-6、重点:过滤器
-a、前置过滤器
-b、后置过滤器
-c、路由过滤器
-结束
镇博图
springcloud总集:https://www.tapme.top/blog/detail/2019-02-28-11-33
本篇中Zuul版本为1.x,目前最新的是2.x,二者在过滤器的使用上有较大区别
超长警告
项目代码见文章结尾
一、背景
微服务架构将一个应用拆分为很多个微小应用,这样会导致之前不是问题的问题出现,比如:
- 安全问题如何实现?
- 日志记录如何实现?
- 用户跟踪如何实现?
上面的问题在传统的单机应用很容易解决,只需要当作一个功能实现即可。但是在微服务中就行不通了,让每个服务都实现一份上述功能,那是相当不现实的,费时,费力还容易出问题。
为了解决这个问题,需要将这些横切关注点(分布式系统级别的横切关注点和spring中的基本一个意思)抽象成一个独立的且作为应用程序中所有微服务调用的过滤器和路由器的服务。这样的服务被称为——服务网管(servicegateway),服务客户端不再直接调用服务。取而代之的是,服务网关作为单个策略执行点(PolicyEnforcementPoint,PEP),所有调用都通过服务网管进行路由,然后送到目的地。
二、服务网关
1、什么是服务网关
之前的几节中我们是通过http请求直接调用各个服务,通常在实际系统中不会直接调用。而是通过服务网关来进行服务调用。服务网关充当了服务客户端和被调用服务间的中介。服务客户端仅与服务网关管理的单个url进行对话。下图说了服务网关在一个系统中的作用:
服务网关位于服务客户端和相应的服务实例之间。所有的服务调用(内部和外部)都应流经服务网关。
2、功能
由于服务网关代理了所有的服务调用,因此它还能充当服务调用的中央策略执行点(PEP),通俗的说就能能够在此实现横切关注点,不用在各个微服务中实现。主要有以下几个:
- 静态路由——服务网关将所有的服务调用放置在单个URL和API路由后,每个服务对应一个固定的服务端点,方便开发人员的服务调用。
- 动态路由——服务网关可以检测传入的请求,根据请求数据和请求者执行职能路由。比如将一部分的调用路由到特定的服务实例上,比如测试版本。
- 验证和授权——所有服务调用都经过服务网关,显然可以在此进行权限验证,确保系统安全。
- 日志记录——当服务调用经过服务网关时,可以使用服务网关来收集数据和日志信息(比如服务调用次数,服务响应时间等)。还能确保在用户请求上提供关键信息以确保日志统计(比如给每个用户请求加一个url参数,每个服务中可通过该参数将关键信息对应到某个用户请求)。
看到这儿可能会有这样的疑问:所有调用都通过服务网关,难道服务网关不是单点故障和潜在瓶颈吗?
1.在单独的服务器前,负载均衡器是很有用的。将负载均衡器放到多个服务网关前面是比较好的设计,确保服务网关可以实现伸缩。但是如果将负载均衡器置于所有服务前便不是一个好主意,会造成瓶颈。
2.服务网关的代码应该是无状态的。有状态的应用实现伸缩性较为麻烦
3.服务网关的代码应该轻量的。服务网关是服务调用的“阻塞点”,不易在服务网关处耽误较长的时间,比如进行同步数据库操作
三、实战
使用NetflixZuul来构建服务网关,配合之前的代码,让服务网关来管理服务调用。
在生产环境中不建议使用zuul,该组件性能较弱,且已经停止更新
1、创建zuulsvr项目
详细过程不赘述,和之前一样(注意springcloud版本要和之前一致),主要pom依赖如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
2、配置zuul
首先在启动加入注解开启zuul并注册到eureka中
然后编写配置文件:
spring:
application:
name:zuulservice
#服务发现配置
eureka:
instance:
prefer-ip-address:true
client:
register-with-eureka:true
fetch-registry:true
service-url:
defaultZone:http://localhost:8761/eureka/
server:
port:5555
这样便以默认配置启动了zuul服务网关。
3、路由配置
Zuul核心就是一个反向代理。在微服务架构下,Zuul从客户端接受微服务调用并将其转发给下游服务。要和下游服务进行沟通,Zuul必须知道如何将进来的调用映射到下游路由中。Zuul有一以下几种路由机制:
- 通过服务发现自动映射路由
- 通过服务发现手动映射路由
- 使用静态URL手动映射
1)、服务发现自动映射
默认情况下,Zuul根据服务ID来进行自动路由。先将组织服务中的延时去掉
启动之前的所有服务实例,然后通过postman访问localhost:5555/organizationservice/organization/12,得到结果如下:
说明服务网关自动路由成功。
如果要查看Zuul服务器管理的路由,可以通过访问Zuul服务器上的/routes,返回结果如下:
{
"/confsvr/**":"confsvr",
"/licensingservice/**":"licensingservice",
"/organizationservice/**":"organizationservice"
}
左边的路由由基于Eureka的服务ID自动创建的,右边为路由所有映射的Eureka服务ID。
2)、服务发现手动手动
如果觉得自动路由不好用,我们还可以更细粒度地明确定义路由映射。例如想要缩短组织服务名称来简化路由,可在application.yml
配置中定义路由映射,在配置文件中加入如下配置:
zuul:
routes:
organizationservice:/org/**
上面的配置将org开头的路径映射到组织服务上了。重启服务器,访问localhost:5555/org/organization/12,仍然能够获取到数据。
现在访问/routes
端点可以看到如下结果:
{
"/org/**":"organizationservice",
"/confsvr/**":"confsvr",
"/licensingservice/**":"licensingservice",
"/organizationservice/**":"organizationservice"
}
可以看到不光有自定义的组织路由,自动映射的组织路由也存在,如果想要排除自动映射的路由可配置ignored-services
属性,用法如下:
zuul:
routes:
organizationservice:/org/**
#使用","分隔,“*”表示全部忽略
ignored-services:'organizationservice'
服务网关有一种常见模式是通过使用/api
之类的标记来为所有服务调用添加前缀,可通过配置prefix
属性来支持。用法如下:
zuul:
routes:
organizationservice:/org/**
#使用","分隔,“*”表示全部忽略
ignored-services:'organizationservice'
prefix:/api
配置后再次访问/routes
端点可以看到路径前都加上了/api
3)、静态URL手动映射
如果系统系统中还存在一些不受Eureka管理的服务,可以建立Zuul直接路由到一个静态定义的URL。假设许可证服务是其他语言编写的web项目,并且希望通过Zuul来代理,可这样配置:
zuul:
routes:
#用于内部识别关键字
licensestatic:
path:/licensestatic/**
url:http://localhost:8091
配置完成后重启zuul访问/routes
端点如下所示,静态路由已经加入:
{
"/api/licensestatic/**":"http://localhost:8091",
"/api/org/**":"organizationservice",
"/api/confsvr/**":"confsvr",
"/api/licensingservice/**":"licensingservice",
"/api/zuulservice/**":"zuulservice"
}
licensestatic端点不再使用Eureka,直接将请求路由到localhost:8091
。但是这里存在一个问题,如果许可证服务有多个实例,该如何用到负载均衡?这里只能配置一条路径指向请求。这里又有一个配置项来禁用Ribbon与Eureka集成,然后列出许可证服务的所有实例,配置如下:
#zuul配置
zuul:
routes:
#用于内部识别关键字
licensestatic:
path:/licensestatic/**
serviceId:licensestatic
organizationservice:/org/**
#使用","分隔,“*”表示全部忽略
ignored-services:'organizationservice'
prefix:/api
ribbon:
eureka:
#禁用Eureka支持
enabled:false
licensestatic:
ribbon:
#licensestatic服务将会路由到下列地址
listOfServers:http://localhost:10011,http://localhost:10012
配置完毕后,访问/routes
端点发现licensestatic/**
映射到了licensestatic服务上,相当于Zuul模拟了一个服务出来。但是Eureka上是没有这个服务的,所以需要禁用掉Ribbon的Eureka支持,不然是无法访问成功的(Ribbon向Eureka查询该服务不存在,报错)。现在x=连续访问localhost:5555//api/licensestatic/licensing/12,可以发现正常响应和404交替出现(10011上能否访问成功,10012报错404),说明配置的多个地址生效了。
问题又来了
禁用eureka支持会导致所有服务的地址都需要手动指定,ribbon不会再从eureka中获取服务实例信息。所以没办法混合使用
目前有两种办法来规避这个问题:
- 对于不能用Eureka管理的应用,可以建立一个单独的Zuul服务器来处理这些路由。
- 建立一个SpringCloudSidecar实例。SpringCloudSidecar允许开发使用Eureka实例注册非JVM服务,然后再通过Zuul代理,相当于曲线救国。
4、动态重载路由
zuul还有一个动态加载路由的功能,也就是在不重启zuul服务的情况下刷新路由。
直接修改application.yml
将prefix从/api
改为/apis
。注意这里修改后要让修改生效需编译一次application.yml让修改替换到target文件中(idea如此,eclipse应该类似),或者直接到编译文件夹下修改application.yml
然后访问/refresh
路径,可以看到如下返回值:
响应表明更新prefix。然后访问/routes
路径会发现前缀变成了apis
这个功能与springcloudconfig配合,用起来就是爽。
5、服务超时
Zuul使用Netflix的Hystrix和Ribbon库来进行http请求。so也是有超时机制存在的。配置方法和前面的一篇类似。但是只能通过配置文件来进行,无法通过注解(这是Zuul管理的没有地方给你写注解)。通过配置hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
属性来实现。如果要为特定的服务配置只需将default替换为服务名就行了。
注意还要只有有另一个超时机制。虽然覆盖了hystrix的超时,但是Ribbon也会超时任何超过5s的调用。so如果超时时间大于5s还要配置Ribbon的超时,配置方式如下:
#对所有服务生效
ribbon.readTimeout:7000
#对组织服务生效
licensingservice.ribbon.readTimeout:7000
6、重点:过滤器
这才是服务网关真正重要的东西。有了过滤器才能实现自定义的通用处理逻辑。可在此进行通用的安全验证、日志、服务跟踪等操作。和springboot中的过滤器概念类似,这里就不做说明了。
Zuul支持以下四种过滤器:
- 前置过滤器——在将请求发送到目的地之前被调用。通常进行请求格式检查、身份验证等操作。
- 后置过滤器——在目标服务被调用被将响应发回调用者后被调用。通常用于记录从目标服务返回的响应、处理错误或审核敏感信息。
- 路由过滤器——在目标服务被调用之前拦截调用。通常用来做动态路由。
- 错误过滤器——在产生错误是调用,用于对错误进行统一处理。
下图展示了在处理客户端请求时,各种过滤器时如何工作的:
下面说说如何来使用这些过滤器:
a、前置过滤器
这里我们来实现一个过滤器-IdFilter,对每个请求检查请求头中是否有一个关联id,无id生成一个id加入到header中。代码如下:
@Component
publicclassIdFilterextendsZuulFilter{
privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(IdFilter.class);
/**
*返回过滤器类型;pre:前置过滤器。post:后置过滤器。routing:路由过滤器。error:错误过滤器
*/
@Override
publicStringfilterType(){
return"pre";
}
/**
*过滤器执行顺序
*/
@Override
publicintfilterOrder(){
return1;
}
/**
*是否启动此过滤器
*/
@Override
publicbooleanshouldFilter(){
returntrue;
}
@Override
publicObjectrun(){
RequestContextctx=RequestContext.getCurrentContext();
Stringid=ctx.getRequest().getHeader("id");
//如果request找不到,再到zuul的方法中找id.request不允许直接修改response中的header,
//所以为了让后续的过滤器能够获取到id才有下面的语法
if(id==null){
id=ctx.getZuulRequestHeaders().get("id");
}
if(id==null){
id=UUID.randomUUID().toString();
LOGGER.info("{}无id,生成id:{}",ctx.getRequest().getRequestURI(),id);
ctx.addZuulRequestHeader("id",id);
}else{
LOGGER.info("{}存在id:{}",ctx.getRequest().getRequestURI(),id);
}
returnnull;
}
}
要在Zuul中实现过滤器,必须拓展ZuulFilter类(2.x版本中不是这样的),然后覆盖上述4个方法。
要给请求头加入一个header需要在ctx.addZuulRequestHreader("","")
(上面代码中的RequestContext是zuul重写的,在其中加入了一些方法)方法中操作,zuul会在发出请求是把header加到请求头中。(因为Zuul本质是一个代理,它截取请求,然后自己再发送这个请求,所有不能也没有必要在原来的request上加header。
重启项目Zuul,访问localhost:5555/apis/licensestatic/licensing/12
,可以看到控制台有如下打印:
说明前置过滤器生效。
现在从zuul服务网关发往许可证服务的http请求已经携带了id。
b、后置过滤器
后置过滤器通常用于进行敏感信息过滤和响应记录。这里我们实现一个后置过滤器,将许可证服务请求的响应内容打印到控制台上同时把id
header插入到服务客户端请求的response中。
@Component
publicclassResponseFilterextendsZuulFilter{
privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(ResponseFilter.class);
/**
*返回过滤器类型;pre:前置过滤器。post:后置过滤器。routing:路由过滤器。error:错误过滤器
*/
@Override
publicStringfilterType(){
return"post";
}
/**
*过滤器执行顺序
*/
@Override
publicintfilterOrder(){
return1;
}
/**
*是否启动此过滤器
*/
@Override
publicbooleanshouldFilter(){
returntrue;
}
@Override
publicObjectrun(){
RequestContextctx=RequestContext.getCurrentContext();
Stringid=ctx.getZuulRequestHeaders().get("id");
ctx.getResponse().addHeader("id",id);
try{
BufferedReaderreader=newBufferedReader(newInputStreamReader(ctx.getResponseDataStream()));
Stringresponse=reader.readLine();
LOGGER.info("响应为:{}",response);
//写到输出流中,本来可以由zuul框架来操作,但是我们已经读取了输入流,zuul读不到数据了,所以要手动写响应到response
ctx.getResponse().setHeader("Content-Type","application/json;charset=utf-8");
ctx.getResponse().getWriter().write(response);
}catch(Exceptione){
}
returnnull;
}
}
经过这样一波操作,就能达到目的了。访问:localhost:5555/apis/licensestatic/licensing/12。控制台打印如下:
请求响应如下:
c、路由过滤器
路由过滤器用起来有点复杂,这里不写具体的实际代码,只是写一个思路。具体代码可以参考spring微服务
- 获取当前请求路径
- 判断是否需要进行特殊路由
- 如需要进行特殊路由,在此进行http调用
- 将http调用的response写入到当前请求的response中
结束
终于写完了,微服务的基础学*又*了一步,加油!
本篇代码存放于:github
本篇原创发布于:FleyX的个人博客
本文内容总结:一、背景,二、服务网关,1、什么是服务网关,2、功能,三、实战,1、创建zuulsvr项目,2、配置zuul,3、路由配置,1)、服务发现自动映射,2)、服务发现手动手动,3)、静态URL手动映射,4、动态重载路由,5、服务超时,6、重点:过滤器,a、前置过滤器,b、后置过滤器,c、路由过滤器,结束,
原文链接:https://www.cnblogs.com/wuyoucao/p/11043440.html