16 网关
16.1 概念
服务治理,服务注册发现,服务调用,熔断。已经学完。
微服务基本模块已经有了,也可以做微服务了。但完成一个复杂的业务,可能需要多个微服务合作来完成,比如下单,需要用户服务,支付服务,地图服务,订单服务。一般是我们对外服务的窗口,进行服务内外隔离。一般微服务都在内网,不做安全验证,
就好像:很多明星,可以独立开演唱会(独立提供服务)。也可以去春晚(微服务群提供服务)。但一台春晚就不能让 观众一个一个调用了。观众要调用,需要检票啥的,检票就类似于网关,进来之后,界面随便看,不会说你 看个小品,还需要再检票。
微服务没有网关,会有下面的问题:
-
客户端请求多个微服务,增加了客户端复杂性,每个微服务都要做用户认证,限流等,避免和多个微服务打交道的复杂性。
-
有跨域问题,不在同一个域。
-
认证复杂,每个服务都要独立认证,服务要求的权限不一致。
-
难以重构。因为微服务被客户端调用着,重构难以实施。
网关是介于客户端(外部调用方比如app,h5)和微服务的中间层。
Zuul是Netflix开源的微服务网关,核心是一系列过滤器。这些过滤器可以完成以下功能。
-
是所有微服务入口,进行分发。
-
身份认证与安全。识别合法的请求,拦截不合法的请求。
-
监控。在入口处监控,更全面。
-
动态路由。动态将请求分发到不同的后端集群。
-
压力测试。可以逐渐增加对后端服务的流量,进行测试。
-
负载均衡。也是用ribbon。
-
限流(望京超市)。比如我每秒只要1000次,10001次就不让访问了。
《网关使用架构图》
网关和服务的关系:演员和剧场检票人员的关系。
zuul默认集成了:ribbon和hystrix。
16.2 使用
初步使用
提醒自己:9100
启动 eureka 7900,api-driver 9002(服务提供者), api-passenger 9001。
项目:online-taxi-zuul
pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
启动类
@EnableZuulProxy
该注解声明这是一个zuul代理,该代理使用Ribbon来定位注册到eureka server上的微服务,同时,整合了hystrix,实现了容错。
yml
普通配置,端口,应用名,eureka地址。即可
server:
port: 9000
spring:
application:
name: online-taxi-zuul
#注册中心
eureka:
client:
#设置服务注册中心的URL
service-url:
defaultZone: http://root:root@eureka-7901:7901/eureka/
instance:
hostname: localhost
instance-id: online-taxi-zuul
测试点:
启动eureka,api-driver, zuul
-
访问:online-taxi-zuul中,测试网关api-driver。
http://网关ip:网关端口/服务名/微服务路径 浏览器访问即可: http://localhost:9100/api-driver/test/hello 相当于访问: http://localhost:9002/test/hello
结论:网关会将服务名转换成具体服务的ip和端口,实际进行访问。
注意:此处的ip和端口是 网关的ip和端口。
ps:网关一般命名
https://域名/v1/sms/路径
看高德开放平台:https://lbs.amap.com/api/webservice/guide/api/geofence_service#t6
负载均衡
启动两个api-driver-9002, api-driver-9003。
测试点:
轮询访问上面地址:http://localhost:9100/api-driver/test/hello,会看到返回结果中,端口一直轮询在变。说明负载均衡生效了,默认是轮询。
改负载均衡
api-driver:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
测试点:
轮询访问上面地址:http://localhost:9100/api-driver/test/hello,会看到返回结果中,端口一直随机在变。说明负载均衡生效了。
路由端点
zuul yml中
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
##默认是never
show-details: ALWAYS
enabled: true
routes:
enabled: true
访问:http://localhost:9100/actuator/routes
结果:
{
/gate-way/sms/**: "service-sms",
/api-driver/**: "api-driver"
}
结果中是,eureka中有的实例的网关,和自己配置的映射。如果eureka中没有实例,则只有自己配置的映射。
作用:调试的时候,看网关请求的地址,以及 映射是否正确。网关请求有误时,可以通过此处排查错误。
过滤器端点
访问:http://localhost:9100/actuator/filters
可以看到 如下4中过滤器,后面讲,网关就是一系列过滤器。每个类型的过滤器都罗列出来了。
有我们自己定义的,也有默认的。
error: [],
post: [],
pre: [],
route: []
配置指定微服务的访问路径
- 通过服务名配置(虚拟主机名)
zuul:
routes:
api-driver: /zuul-api-driver/**
配置前先访问,然后做对比。
这样访问
http://localhost:9100/zuul-api-driver/test/hello
- 自定义命名配置
zuul:
routes:
custom-zuul-name: #此处名字随便取
path: /zuul-custom-name/**
url: http://localhost:9002/
访问前 看结果,做对比。
访问:http://localhost:9100/zuul-custom-name/test/hello
这样 ribbon和hystrix 就都失效了。
- 基于2,恢复ribbon+hystrix
zuul:
routes:
#此处名字随便取
custom-zuul-name:
path: /zuul-custom-name/**
service-id: no-eureka-api-driver
no-eureka-api-driver:
ribbon:
listOfServers: localhost:9003,localhost:9002
ribbon:
eureka:
enabled: false
访问:http://localhost:9100/zuul-custom-name/test/hello
ribbon之前讲过类似这种配置。
- 指定serviceId
zuul:
routes:
#此处名字随便取
custom-zuul-name:
path: /zuul-custom-name/**
service-id: api-driver
访问:http://localhost:9100/zuul-custom-name/test/hello
忽略微服务
原来访问:
http://localhost:9100/api-driver/test/hello
http://localhost:9100/zuul-api-driver/test/hello(基于基础的例子的)
好使。
- 忽略微服务数组
增加如下配置
zuul:
routes:
api-driver: /zuul-api-driver/**
ignored-services:
- api-driver
再访问:
http://localhost:9100/api-driver/test/hello , 不好使
http://localhost:9100/zuul-api-driver/test/hello, 好使
不好使。只能访问:
现在只有api-driver不好使。api-passenger还是好使的。
http://localhost:9100/api-passenger/test/hello , 好使
http://localhost:9100/zuul-api-passenger/test/hello , 好使
- 忽略正则
# 忽略正则,不能通过 zuul-api-driver 和 api-driver访问。
# ignored-patterns:
# - /*-driver/**
可以测试一下。
测试点:
http://localhost:9100/zuul-api-driver/test/hello,不好使,
http://localhost:9100/api-driver/test/hello ,不好使。
- 忽略全部,下去实验。
访问:http://localhost:9100/api-passenger/test/hello
发现api-passenger也不好使。
只能走routes的配置。
前缀
接口一般命名:/api/v1/xxxx
zuul:
prefix: /api
# 是否移除前缀
strip-prefix: true
访问时带上前缀,实际 请求会将前缀去掉。
比如访问:http://localhost:9100/api/zuul-api-driver/test/hello
实际:http://localhost:9002/test/hello
注意全局的移除,和自定义名字下面的移除。
即zuul下的移除,和custom-zuul-name2: 下面的移除。
查看路由日志
关键点找 200,最后几行。看到路由成功,找到了目的地。
2020-02-19 15:36:41.269 DEBUG [online-taxi-zuul,,,] 21456 --- [imer-api-driver] c.netflix.loadbalancer.BaseLoadBalancer : LoadBalancer: PingTask executing [1] servers configured
2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.web.servlet.DispatcherServlet : GET "/zuul-api-driver/test/hello", parameters={}
2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='gate-way/sms', path='/gate-way/sms/**', serviceId='service-sms', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, }
2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='zuul-api-driver', path='/zuul-api-driver/**', serviceId='api-driver', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, }
2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='api-passenger', path='/api-passenger/**', serviceId='api-passenger', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, }
2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='api-driver', path='/api-driver/**', serviceId='api-driver', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, }
2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.web.ZuulHandlerMapping : Mapped to org.springframework.cloud.netflix.zuul.web.ZuulController@4dcf34e0
2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Path = null
2020-02-19 15:36:41.590 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Transfer-Encoding = null
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Content-Encoding = null
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.zuul.http.HttpServletRequestWrapper : Content-Length header = -1
来源uri:/zuul-api-driver/test/hello
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : Finding route for path: /zuul-api-driver/test/hello
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : servletPath=/
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : zuulServletPath=/zuul
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : RequestUtils.isDispatcherServletRequest()=true
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : RequestUtils.isZuulServletRequest()=false
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : adjustedPath=/zuul-api-driver/test/hello
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : Matching pattern:/gate-way/sms/**
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : Matching pattern:/zuul-api-driver/**
2020-02-19 15:36:41.591 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.c.n.zuul.filters.SimpleRouteLocator : route matched=ZuulRoute{id='zuul-api-driver', path='/zuul-api-driver/**', serviceId='api-driver', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, }
2020-02-19 15:36:41.592 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.loadbalancer.ZoneAwareLoadBalancer : Zone aware logic disabled or there is only one zone
2020-02-19 15:36:41.592 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] c.n.loadbalancer.LoadBalancerContext : api-driver using LB returned Server: localhost:9002 for request /test/hello
2020-02-19 15:36:41.602 DEBUG [online-taxi-zuul,c121eba852592312,c121eba852592312,true] 21456 --- [nio-9100-exec-9] o.s.web.servlet.DispatcherServlet : Completed 200 OK
敏感Header
测试点:
停止一个api-driver。访问:yapi:网关token,看返回。
初始请求。返回值中token为msb cookie
加上下面配置
敏感的header不会传播到下游去,也就是说此处的token不会传播的其它的微服务中去。
zuul:
#一下配置,表示忽略下面的值向微服务传播,以下配置为空表示:所有请求头都透传到后面微服务。
sensitive-headers: token
访问。网关token为null。
上面是网关的路由。
过滤器
Zuul的大部分功能都是有过滤器实现的。
4种过滤器
PRE: 在请求被路由之前调用,可利用这种过滤器实现身份验证。选择微服务,记录日志。
ROUTING:在将请求路由到微服务调用,用于构建发送给微服务的请求,并用http clinet(或者ribbon)请求微服务。
POST:在调用微服务执行后。可用于添加header,记录日志,将响应发给客户端。
ERROR:在其他阶段发生错误是,走此过滤器。
自定义过滤器
PreFilter看代码,注意下面4点。
filterType:pre,routing,post,error
filterOrder:执行顺序,在谁前,在谁后,可以+1,-1
shouldFilter:此过滤器是否执行,true false,可以写过滤器是否执行的判断条件。
run:具体执行逻辑。
访问:yapi中 网关token
pre来源uri:/api-driver/test/token
pre拦截
pre 业务逻辑 token:msb coolie
说一下AuthFilter。利用filter实现了 鉴权。看代码。(实际用jwt)
测试一下,
// 测试路径
// if(uri.contains("api-driver")) {
// return true;
// }
接口容错
@Component
public class MsbFallback implements FallbackProvider{
/**
* 表明为哪个微服务提供回退
* 服务Id ,若需要所有服务调用都支持回退,返回null 或者 * 即可
*/
@Override
public String getRoute() {
// TODO Auto-generated method stub
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
//return status;
return HttpStatus.BAD_REQUEST;
}
@Override
public int getRawStatusCode() throws IOException {
//return status.value();
return HttpStatus.BAD_REQUEST.value();
}
@Override
public String getStatusText() throws IOException {
//return status.getReasonPhrase();
//return HttpStatus.BAD_REQUEST.name();
return HttpStatus.BAD_REQUEST.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
String msg = "{\"msg\":\"服务故障\"}";
return new ByteArrayInputStream(msg.getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
选用基础yml
测试点:启动eureka,api-driver, online-taxi-zuul
正常启动,正常访问yapi 网关token。正常
停止api-driver。
则走了容错 方法。
将fallback的改成:
@Override
public String getRoute() {
// TODO Auto-generated method stub
// return "*";
return "api-passenger";
}
在访问上面 yapi 中 zuul,中网关token。则报500。
再改成:
@Override
public String getRoute() {
// TODO Auto-generated method stub
// return "*";
// return "api-passenger";
return "api-driver";
}
重写访问,熔断生效。
最后改回去*。
限流
保护自己,用ratelimit。
令牌桶
假设进入高速公路的车辆需要在入口处领取到通行卡才能进入高速公路。为了节约人力成本,入口处放置自动出卡机。按照国家高速公路交通安全法的规定,在高速公路上行驶的车辆,车速超过100km/h时,应与同车道前车保持100米以上距离。为了保持最小安全行车距离100米,按车速100km/h计算,需要间隔至少3.6秒才能放行一辆车,因此出卡机每隔3.6秒出一张通行卡。在自动出卡机下放置一个盒子,自动出卡机按照3.6秒的间隔向盒子中投放通行卡。每辆进入高速公路的车辆,从盒子中领取通行卡之后才可以进入高速公路。
令牌桶可以看作是一个存放一定数量令牌的容器。系统按设定的速度向桶中放置令牌。当桶中令牌满时,多出的令牌溢出,桶中令牌不再增加。在使用令牌桶对流量规格进行评估时,是以令牌桶中的令牌数量是否足够满足报文的转发为依据的。每个需要被转发的报文,都要从令牌桶中领取一定数量的令牌(具体数量视报文大小而定),才可以被正常转发。如果桶中存在足够的令牌可以用来转发报文,称流量遵守或符合约定值,否则称为不符合或超标。
-
启动jmeter,双击:jmeter.bat
-
右击TestPlan,add ,Threads,Thread Group
-
右击测试令牌桶线程组,add,sampler, http request。
-
在线程组:
1、Number of Threads(users):用户个数
2、Ramp-up Period(in seconds):在多长时间内,加载指定的用户个数,单位为s。
假如需加载100个用户,在5s中之内加载完成,那么平均每秒钟加载20个用户。
3、Loop Count(循环次数):用户执行操作的循环次数,如果选择forever,则永远循环下去。
测试点:启动eureka,api-driver,online-taxi-zuul。
令牌桶设置成2,jemter 用10个并发。可以看到控制台输出结果。
单独限流。
高可用
一般做法
前面架上nginx。
zuul作为普通的服务。对外访问。前面加一层(nginx+keepalived)
第8节课完。2020.3.8
maven,
剔除。
原理
《Zuul原理流程图》
让我们做,如何实现?
方案:请求过来->pre(一组,鉴权,限流之类的。)->route(一组,路由到别的服务,具体微服务。)->post(一组,处理响应)。
zuul本质就是filter。
通过filter解析url来决定我们去访问哪个微服务。
发请求访问微服务,也是通过filter实现。
响应数据,也是通过filter实现。
源码
所有断点入口打在:
ZuulServlet中service方法第一行。
从ZuulFilter类的Object res = run();进入每个过滤器。包括路由转发规则(此时debug主要 走route方法。不是preRoute)。
我只debug了。RibbonRoutingFilter(debug时。list有三个RibbonRoutingFilter,SimpleHostRoutingFilter,SendForwardFilter)。ServletDetectionFilter。通过网关token地址去debug。
选择路由用哪个过滤器,注意每个路由规则过滤器的:
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
}
入口开关(所有启动类上的开关,套路都一样。)
spring-cloud-netflix-zuul-2.1.3.RELEASE.jar中spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration,\
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration
点ZuulProxyAutoConfiguration进去
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
知道了@EnableZuulProxy的作用,开关。
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}
接着:看ZuulProxyAutoConfiguration中
服务发现
@Autowired
private DiscoveryClient discovery;
ribbon相关
在 import注解中,有RibbonCommandFactoryConfiguration
还注入了
pre filter(PreDecorationFilter点进去看 filterType)
routing filter:(RibbonRoutingFilter,SimpleHostRoutingFilter,进去查看filterType)
post filter:在ZuulServerAutoConfiguration中注入的SendResponseFilter。
error filter:和post都在ZuulServerAutoConfiguration。SendErrorFilte
各种过滤器等,搜索注释可以看到。
PreDecorationFilter:解析决定使用哪种url。
RibbonRoutingFilter:向微服务发请求
SendResponseFilter:接受微服务响应,并向用户响应。
主要filter执行流程
debug 上面3个类的中的 run方法。
PreDecorationFilter
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper
.getPathWithinApplication(ctx.getRequest());
// 根据请求路径获取route
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
String location = route.getLocation();
if (location != null) {
ctx.put(REQUEST_URI_KEY, route.getPath());
ctx.put(PROXY_KEY, route.getId());
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper.addIgnoredHeaders(
this.properties.getSensitiveHeaders().toArray(new String[0]));
}
else {
this.proxyRequestHelper.addIgnoredHeaders(
route.getSensitiveHeaders().toArray(new String[0]));
}
if (route.getRetryable() != null) {
ctx.put(RETRYABLE_KEY, route.getRetryable());
}
if (location.startsWith(HTTP_SCHEME + ":")
|| location.startsWith(HTTPS_SCHEME + ":")) {
ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader(SERVICE_HEADER, location);
}
else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
ctx.set(FORWARD_TO_KEY,
StringUtils.cleanPath(
location.substring(FORWARD_LOCATION_PREFIX.length())
+ route.getPath()));
ctx.setRouteHost(null);
return null;
}
else {
// set serviceId for use in filters.route.RibbonRequest
ctx.set(SERVICE_ID_KEY, location);
ctx.setRouteHost(null);
ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
}
if (this.properties.isAddProxyHeaders()) {
addProxyHeaders(ctx, route);
String xforwardedfor = ctx.getRequest()
.getHeader(X_FORWARDED_FOR_HEADER);
serviceId 虚拟主机名。(spring.application.name,vhost)
String remoteAddr = ctx.getRequest().getRemoteAddr();
if (xforwardedfor == null) {
xforwardedfor = remoteAddr;
}
else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
xforwardedfor += ", " + remoteAddr;
}
ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);
}
if (this.properties.isAddHostHeader()) {
ctx.addZuulRequestHeader(HttpHeaders.HOST,
toHostHeader(ctx.getRequest()));
}
}
}
else {
log.warn("No route found for uri: " + requestURI);
String forwardURI = getForwardUri(requestURI);
ctx.set(FORWARD_TO_KEY, forwardURI);
}
return null;
}
解析url地址,获取到当前要使用的是哪个 route。没有具体业务。
RibbonRoutingFilter
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
RibbonCommandContext commandContext = buildCommandContext(context);
// 得到请求微服务的结果。进入forward,在下面。
ClientHttpResponse response = forward(commandContext);
setResponse(response);
return response;
}
catch (ZuulException ex) {
throw new ZuulRuntimeException(ex);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
}
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
Map<String, Object> info = this.helper.debug(context.getMethod(),
context.getUri(), context.getHeaders(), context.getParams(),
context.getRequestEntity());
// 用ribbon来访问
RibbonCommand command = this.ribbonCommandFactory.create(context);
try {
// 向微服务发起请求,也就是执行具体请求,此处的command,有熔断功能,回想熔断 也是用command
ClientHttpResponse response = command.execute();往里走:会走到熔断的地方(execute)。
this.helper.appendDebug(info, response.getRawStatusCode(),
response.getHeaders());
return response;
}
catch (HystrixRuntimeException ex) {
return handleException(info, ex);
}
}
底层的请求在:AbstractRibbonCommand类中的protected ClientHttpResponse run() throws Exception {中if (retryableClient) {
response = this.client.execute(request, config);
}
else {
response = this.client.executeWithLoadBalancer(request, config);
}发起执行。底层走到了ribbon的源码。回忆ribbon源码。回忆httpclient okclient等的配置。 还有用hystrix包裹请求。
实际请求走的ribbon。
com.netflix.loadbalancer.LoadBalancerContext
通过reconstructURIWithServer替换成 微服务实际的ip+port
SendResponseFilter
public Object run() {
try {
添加响应头
addResponseHeaders();
向客户端写数据
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
过滤器存储
其父类ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration
在缺失zuulServlet bean的情况下注入了ZuulServlet
@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
@ConditionalOnProperty(name = "zuul.use-filter", havingValue = "false", matchIfMissing = true)
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>(
new ZuulServlet(), this.zuulProperties.getServletPattern());
// The whole point of exposing this servlet is to provide a route that doesn't
// buffer requests.
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}
另外也注册了一大堆过滤器。pre route post, error
还有初始化了
@Bean
public ZuulFilterInitializer zuulFilterInitializer(CounterFactory counterFactory,
TracerFactory tracerFactory) {
FilterLoader filterLoader = FilterLoader.getInstance();
FilterRegistry filterRegistry = FilterRegistry.instance();
return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory,
filterLoader, filterRegistry);
}
点击FilterRegistry进去:
private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();
FilterRegistry管理了一个ConcurrentHashMap,用作存储过滤器的,并有一些基本的CURD过滤器的方法,
过滤器执行流程
关键:(请求转发器)ZuulServlet,所有请求先到ZuulServlet
上面类注入了servlet,打开servlet,过滤器执行的关键 为什么 先pre,routing,post
public class ZuulServlet extends HttpServlet
// zuul执行器,ZuulServlet直接访问这个类的方法
private ZuulRunner zuulRunner;
可以断点到service
看业务逻辑:
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
//包装http请求和响应
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
获取当前的请求上下文
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
执行前置过滤器,主要做权限严重,限流。debug一个一个进入。zuulRunner->FilterProcessor(由它来执行具体过滤器,)
preRoute();
} catch (ZuulException e) {
// 如果执行出错,先执行错误处理,再执行后置过滤器,此处注意一下
error(e);
// 为什么要走post,因为要响应用户,
postRoute();
return;
}
try {
路由过滤器,有zuul构造请求,访问实际微服务。
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
后置过滤器,将微服务的响应数据,响应给用户。
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
这个方法为每个请求生成了RequestContext,RequestContext继承了ConcurrentHashMap<String, Object>,在请求结束时销毁掉该RequestContext,RequestContext的生命周期为请求到zuulServlet开始处理,直到请求结束返回结果。
RequestContext类在存储了很多重要的信息,包括HttpServletRequest、HttpServletRespons、ResponseDataStream、ResponseStatusCode等。 RequestContext对象在处理请求的过程中,一直存在,所以这个对象为所有Filter共享。
从ZuulServlet的service()方法可知,它是先处理pre()类型的处理器,然后在处理route()类型的处理器,最后再处理post类型的处理器。
通过上面方法,可以得出如下结论:
RequestContext贯穿整个请求filter线程。
通过service方法,可以看出整个servlet的处理流程:
pre异常: pre -> error -> post
route异常: pre -> route -> error -> post
post异常: pre -> route -> post -> error
正常: pre -> route -> post
为什么最后都要走post,因为post最后,才能直接给用户响应数据。
pre:表示路由的前置过滤器链,route:表示路由的过滤器链,post:表示路由的后置过滤器链,error:表示路由错误过滤器链。
由此可见,责任链模式是zuul的核心。
处理,增加下一个处理的节点。
Zuul责任链模式的执行顺序由filterType和filterOrder共同决定。不同的类型执行顺序为:pre过滤器 -> route过滤器 -> post过滤器。同一类型的执行顺序为:按filterOrder值大小排序,filterOrder值越小,越先执行。
通过上面,就知道我们的自定义过滤器,应该如何写了。(回忆我们前面自定义过滤器),
//获取当前上下文
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
过滤器排序
自定义过滤器的排序源码
在com.netflix.zuul.http.ZuulServlet中,service方法中,有一行:preRoute();点进去
void preRoute() throws ZuulException {
zuulRunner.preRoute();
}
点preRoute进去
public void preRoute() throws ZuulException {
FilterProcessor.getInstance().preRoute();
}
点preRoute进去
public void preRoute() throws ZuulException {
try {
runFilters("pre");
} catch (ZuulException e) {
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
}
}
点runFilters进去
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
for (int i = 0; i < list.size(); i++) {
ZuulFilter zuulFilter = list.get(i);
Object result = processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
bResult |= ((Boolean) result);
}
}
}
return bResult;
}
点getFiltersByType进去
public List<ZuulFilter> getFiltersByType(String filterType) {
List<ZuulFilter> list = hashFiltersByType.get(filterType);
if (list != null) return list;
list = new ArrayList<ZuulFilter>();
通过注册器找到所有的过滤器
Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
查找指定类型的过滤器
for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
ZuulFilter filter = iterator.next();
if (filter.filterType().equals(filterType)) {
list.add(filter);
}
}
根据filterOrder排序
Collections.sort(list); // sort by priority
hashFiltersByType.putIfAbsent(filterType, list);
return list;
}
看到了,排序方法。
过滤器顺序
过滤器定义order
FilterConstants
看里面的顺序,我们可以定义我们的过滤器 何时执行。
FormBodyWrapperFilter -1 pre 解析表单数据
SendErrorFilter 0 error 如果中途出现错误
DEBUG_FILTER_ORDER 1:pre 设置请求过程是否开启debug
PreDecorationFilter 5 pre 根据uri决定调用哪一个route过滤器
RibbonRoutingFilter 10 route 如果写配置的时候用ServiceId,则用这个route过滤器,该过滤器可以用Ribbon 做负载均衡,用hystrix做熔断
SimpleHostRoutingFilter 100 route 如果写配置的时候用url则用这个route过滤
SendResponseFilter 1000 post 用RequestDispatcher请求转发
过滤器的order值越小,就越先执行,并且在执行过滤器的过程中,它们共享了一个RequestContext对象,该对象的生命周期贯穿于请求,可以看出优先执行了pre类型的过滤器,并将执行后的结果放在RequestContext中,供后续的filter使用,
而error类型的过滤器,是在程序发生异常的时候执行的。
post类型的过滤,在默认的情况下,只注入了SendResponseFilter,该类型的过滤器是将最终的请求结果以流的形式输出给客户端。
打开:SendResponseFilter
@Override
public Object run() {
try {
addResponseHeaders();
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
private void writeResponse() throws Exception {
RequestContext context = RequestContext.getCurrentContext();
// there is no body to send
if (context.getResponseBody() == null
&& context.getResponseDataStream() == null) {
return;
}
HttpServletResponse servletResponse = context.getResponse();
if (servletResponse.getCharacterEncoding() == null) { // only set if not set
servletResponse.setCharacterEncoding("UTF-8");
}
String servletResponseContentEncoding = getResponseContentEncoding(context);
OutputStream outStream = servletResponse.getOutputStream();
InputStream is = null;
try {
if (context.getResponseBody() != null) {
String body = context.getResponseBody();
is = new ByteArrayInputStream(
body.getBytes(servletResponse.getCharacterEncoding()));
}
else {
is = context.getResponseDataStream();
if (is != null && context.getResponseGZipped()) {
// if origin response is gzipped, and client has not requested gzip,
// decompress stream before sending to client
// else, stream gzip directly to client
if (isGzipRequested(context)) {
servletResponseContentEncoding = "gzip";
}
else {
servletResponseContentEncoding = null;
is = handleGzipStream(is);
}
}
}
if (servletResponseContentEncoding != null) {
servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING,
servletResponseContentEncoding);
}
if (is != null) {
writeResponse(is, outStream);
}
}
finally {
/**
* We must ensure that the InputStream provided by our upstream pooling
* mechanism is ALWAYS closed even in the case of wrapped streams, which are
* supplied by pooled sources such as Apache's
* PoolingHttpClientConnectionManager. In that particular case, the underlying
* HTTP connection will be returned back to the connection pool iif either
* close() is explicitly called, a read error occurs, or the end of the
* underlying stream is reached. If, however a write error occurs, we will end
* up leaking a connection from the pool without an explicit close()
*
* @author Johannes Edmeier
*/
if (is != null) {
try {
is.close();
}
catch (Exception ex) {
log.warn("Error while closing upstream input stream", ex);
}
}
// cleanup ThreadLocal when we are all done
if (buffers != null) {
buffers.remove();
}
try {
Object zuulResponse = context.get("zuulResponse");
if (zuulResponse instanceof Closeable) {
((Closeable) zuulResponse).close();
}
outStream.flush();
// The container will close the stream for us
}
catch (IOException ex) {
log.warn("Error while sending response to client: " + ex.getMessage());
}
}
}
重点 writeResponse方法。
从RequestContext中获取ResponseBody获或者ResponseDataStream来写入到HttpServletResponse中的。
RequestContext 贯穿整个请求。
过滤器执行
FilterProcessor
/**
*
* 运行某种类型的所有过滤器
*
* @param sType 过滤器类型:pre,route,post,error
* @return
* @throws Throwable throws up an arbitrary exception
*/
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
// 如果开启了路由的请求日志 ,将日志添加到RequestContext对象中
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
//
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
for (int i = 0; i < list.size(); i++) {
ZuulFilter zuulFilter = list.get(i);
// 【2】
Object result = processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
// 如果结果是布尔类型
bResult |= ((Boolean) result);
}
}
}
return bResult;
}
(1) 添加路由日志
(2) 根据过滤器的优先级排序整个过滤器链
(3) 依次执行过滤器,如果是布尔类型汇总结果
看这行:
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
boolean bDebug = ctx.debugRouting();
final String metricPrefix = "zuul.filter-";
long execTime = 0;
String filterName = "";
try {
long ltime = System.currentTimeMillis();
filterName = filter.getClass().getSimpleName();
RequestContext copy = null;
Object o = null;
Throwable t = null;
if (bDebug) {
Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
copy = ctx.copy();
}
// 执行,进去 ,在下面
ZuulFilterResult result = filter.runFilter();
ExecutionStatus s = result.getStatus();
execTime = System.currentTimeMillis() - ltime;
switch (s) {
case FAILED:
t = result.getException();
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
break;
case SUCCESS:
o = result.getResult();
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
if (bDebug) {
Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
Debug.compareContextState(filterName, copy);
}
break;
default:
break;
}
if (t != null) throw t;
usageNotifier.notify(filter, s);
return o;
} catch (Throwable e) {
if (bDebug) {
Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
}
usageNotifier.notify(filter, ExecutionStatus.FAILED);
if (e instanceof ZuulException) {
throw (ZuulException) e;
} else {
ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
throw ex;
}
}
}
每个filter都要run方法。
public ZuulFilterResult runFilter() {
ZuulFilterResult zr = new ZuulFilterResult();
if (!isFilterDisabled()) {
if (shouldFilter()) {
Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
try {
// 每个filter都要run方法。
Object res = run();
zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
} catch (Throwable e) {
t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
zr = new ZuulFilterResult(ExecutionStatus.FAILED);
zr.setException(e);
} finally {
t.stopAndLog();
}
} else {
zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
}
}
return zr;
}
debug时,注意 post,router。的顺序。演示一下。
流程总结:zuulServlet->ZuulerRunner->FilterProcessor
主要执行在FilterProcessor(获取过滤器列表,然后执行),
总结:
网关的使用。
原理。
源码。