分布式实战(干货)
spring cloud 实战(干货)
mybatis 实战(干货)
spring boot 实战(干货)
React 入门实战(干货)
构建中小型互联网企业架构(干货)
python 学习持续更新
ElasticSearch 笔记
kafka storm 实战 (干货)
scala 学习持续更新
RPC
深度学习
GO 语言 持续更新
Android 学习
nginx 相关文章
本篇将介绍API网关的基本概念、Zuul网关的功能和工作机制。结合代码介绍如何使用Zuul构建一个简单的网关、介绍Zuul的路由配置方式、了解Filter工作原理并实现一些扩展功能。
在微服务架构中,通常会有多个服务提供者。设想一个电商系统,可能会有商品、订单、支付、用户等多个类型的服务,而每个类型的服务数量也会随着整个系统体量的增大也会随之增长和变更。作为UI端,在展示页面时可能需要从多个微服务中聚合数据,而且服务的划分位置结构可能会有所改变。网关就可以对外暴露聚合API,屏蔽内部微服务的微小变动,保持整个系统的稳定性。
当然这只是网关众多功能中的一部分,它还可以做负载均衡,统一鉴权,协议转换,监控监测等一系列功能。
下图是一张典型的Backend for Front-End网关:
Zuul是Spring Cloud全家桶中的微服务API网关。
所有从设备或网站来的请求都会经过Zuul到达后端的Netflix应用程序。作为一个边界性质的应用程序,Zuul提供了动态路由、监控、弹性负载和安全功能。Zuul底层利用各种filter实现如下功能:
1、新建一个gateway-zuul-demo模块,在依赖项处添加【Cloud Discovery->Eureka Discovery和Cloud Rouing->Zuul】。
2、修改入口类,增加EnableZuulProxy
注解
@SpringBootApplication
@EnableZuulProxy
public class GatewayZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayZuulDemoApplication.class, args);
}
}
3、修改appliation.yml
server:
port: 9006
spring:
application:
name: gateway-zuul-demo
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
instance:
prefer-ip-address: true
4、启动Eureka Server、Rest-Demo和Gateway-Zuul-Demo,在浏览器中输入http://localhost:9006/rest-demo/user/xdlysk获取返回结果。
从上面的例子中的地址可以看出来默认Zuul的路由方式是:http://ZUULHOST:ZUULPORT/serviceId/**。
如果启动多个Rest-Demo可以发现Zuul里面还内置了Ribbon的负载均衡功能。
Zuul提供了一套简单且强大路由配置策略,利用路由配置我们可以完成对微服务和URL更精确的控制。
1、重写指定微服务的访问路径:
zuul:
routes:
rest-demo: /rest/**
这表示将rest-demo微服务的地址映射到/rest/**路径。
2、忽略指定微服务:
zuul:
ignored-services: rest-demo,xxx-service
使用“*”可忽略所有微服务,多个指定微服务以半角逗号分隔。
3、忽略所有微服务,只路由指定微服务:
zuul:
ignored-services: *
routes:
rest-demo: /rest/**
4、路由别名:
zuul:
routes:
route-name: #路由别名,无其他意义,与例1效果一致
service-id: rest-demo
path: /rest/**
5、指定path和URL
zuul:
routes:
route-name:
url: http://localhost:8000/
path: /rest/**
此例将http://ZUULHOST:ZUULPORT/rest/映射到http://localhost:8000/。同时由于并非用service-id定位服务,所以也无法使用负载均衡功能。
6、即指定path和URL,又保留Zuul的Hystrix、Ribbon特性
zuul:
routes:
route-name: #路由别名,无其他意义,与例1效果一致
service-id: rest-demo
path: /rest/**
ribbon:
eureka:
enable: false #为Ribbon禁用Eureka
rest-demo:
ribbon:
listOfServers: localhost:9000,localhost:9001
7、借助PatternServiceRouteMapper
实现路由的正则匹配
@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
/**
* A RegExp Pattern that extract needed information from a service ID. Ex :
* "(?.*)-(?v.*$)"
*/
//private Pattern servicePattern;
/**
* A RegExp that refer to named groups define in servicePattern. Ex :
* "${version}/${name}"
*/
//private String routePattern;
return new PatternServiceRouteMapper("(?^.+)-(?v.+$)", "${version}/${name}");
}
此例可以将rest-demo-v1映射为/v1/rest-demo/**。
8、路由前缀
zuul:
prefix: /api
strip-prefix: true
routes:
rest-demo: /rest/**
此时访问Zuul的/api/rest/user/xdlysk会被转发到/rest-demo/user/xdlysk。
9、忽略某些微服务中的某些路径
zuul:
ignoredPatterns: /**/user/* #忽略所有包含/user/的地址请求
routes:
route-demo:
service-Id: rest-demo
path: /rest/**
更多的配置项和配置方法可以参考
spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java并结合上述例子扩展。在ZuulProperties.java中每个字段都会有注释解释其作用。
6.1 Zuul中的Filter
Zuul是围绕一系列Filter展开的,这些Filter在整个HTTP请求过程中执行一连串的操作。
Zuul Filter有以下几个特征:
Zuul提供了动态读取、编译和执行Filter的框架。各个Filter间没有直接联系,但是都通过RequestContext
共享一些状态数据。
尽管Zuul支持任何基于JVM的语言,但是过滤器目前是用Groovy编写的。 每个过滤器的源代码被写入到Zuul服务器上的一组指定的目录中,这些目录将被定期轮询检查是否更新。Zuul会读取已更新的过滤器,动态编译到正在运行的服务器中,并后续请求中调用。
6.2 Filter Types
以下提供四种标准的Filter类型及其在请求生命周期中所处的位置:
除了上述默认的四种Filter类型外,Zuul还允许自定义Filter类型并显示执行。例如,我们定义一个STATIC类型的Filter,它直接在Zuul中生成一个响应,而非将请求在转发到目标。
6.3 Zuul请求生命周期
一图胜千言,下面通过官方的一张图来了解Zuul请求的生命周期。
6.4 自定义一个Filter
1、添加一个PreRequestLogFilter
:
public class PreRequestLogFilter extends ZuulFilter{
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
System.out.print(String.format("send %s request to %s",request.getMethod(),request.getRequestURL()));
return null;
}
}
Java
Copy
2、修改启动类GatewayZuulDemoApplication
,添加Filter注入:
@Bean
public PreRequestLogFilter preRequestLogFilter(){
return new PreRequestLogFilter();
}
3、启动服务并请求服务,观察控制台输出日志信息。
6.4 Filter的启用与禁用
我们自己写的Filter可以通过修改shouldFilter()
启用或禁用。如果第三方的Filter怎样控制其启用及禁用呢?
很简单,通过配置文件就可以做到:
zuul:
PreRequestLogFilter: #自定义Filter类名
pre: #Type
disable: true
在前面提到在ROUTING过滤器中会选择使用Apache HttpClient或Netflix Ribbon请求目标服务,那么什么时候会使用Ribbon是么时候用Apache HttpClient呢?
这里涉及到两个关键Filter:RibbonRoutingFilter
和SimpleHostRoutingFilter
。从名字就可以看出来RibbonRoutingFilter
是使用Ribbon请求目标服务,而SimpleHostRoutingFilter
则是另一个。
通过filterOrder()
可以看到RibbonRoutingFilter
会在SimpleHostRoutingFilter
之前执行。
//spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null /*获取serviceId*/
&& ctx.sendZuulResponse());
}
//spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/SimpleHostRoutingFilter.java
@Override
public boolean shouldFilter() {
return RequestContext.getCurrentContext().getRouteHost() != null
&& RequestContext.getCurrentContext().sendZuulResponse();
}
对比两个Filter的生效条件可以看出当配置serviceId时RibbonRoutingFilter
生效。
下面看看RibbonRoutingFilter
是如何将Ribbon和Hystrix集成进来的:
//spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java
public RibbonRoutingFilter(ProxyRequestHelper helper,
RibbonCommandFactory> ribbonCommandFactory,
List requestCustomizers) {
this.helper = helper;
this.ribbonCommandFactory = ribbonCommandFactory;
this.requestCustomizers = requestCustomizers;
// To support Servlet API 3.1 we need to check if getContentLengthLong exists
// Spring 5 minimum support is 3.0, so this stays
try {
HttpServletRequest.class.getMethod("getContentLengthLong");
} catch(NoSuchMethodException e) {
useServlet31 = false;
}
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
RibbonCommandContext commandContext = buildCommandContext(context);
ClientHttpResponse response = forward(commandContext);
setResponse(response);
return response;
}
catch (ZuulException ex) {
throw new ZuulRuntimeException(ex);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
}
protected RibbonCommandContext buildCommandContext(RequestContext context) {
HttpServletRequest request = context.getRequest();
MultiValueMap headers = this.helper
.buildZuulRequestHeaders(request);
MultiValueMap params = this.helper
.buildZuulRequestQueryParams(request);
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request);
if (request.getContentLength() < 0 && !verb.equalsIgnoreCase("GET")) {
context.setChunkedRequestBody();
}
String serviceId = (String) context.get(SERVICE_ID_KEY);
Boolean retryable = (Boolean) context.get(RETRYABLE_KEY);
Object loadBalancerKey = context.get(LOAD_BALANCER_KEY);
String uri = this.helper.buildZuulRequestURI(request);
// remove double slashes
uri = uri.replace("//", "/");
long contentLength = useServlet31 ? request.getContentLengthLong(): request.getContentLength();
return new RibbonCommandContext(serviceId, verb, uri, retryable, headers, params,
requestEntity, this.requestCustomizers, contentLength, loadBalancerKey);
}
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
Map info = this.helper.debug(context.getMethod(),
context.getUri(), context.getHeaders(), context.getParams(),
context.getRequestEntity());
RibbonCommand command = this.ribbonCommandFactory.create(context);
try {
ClientHttpResponse response = command.execute();
this.helper.appendDebug(info, response.getRawStatusCode(), response.getHeaders());
return response;
}
catch (HystrixRuntimeException ex) {
return handleException(info, ex);
}
}
上述代码在构造函数中注入了ribbonCommandFactory
(实际类型为RestClientRibbonCommandFactory
)。接着在run()
方法中构造RibbonCommandContext
,并通过RestClientRibbonCommandFactory
创建一个RibbonCommand
(实际类型为RestClientRibbonCommand
)。RestClientRibbonCommand
继承AbstractRibbonCommand
时所带的泛型参数RestClient
具备负载均衡能力。又由于在RestClientRibbonCommand
的继承链上出现了HystrixCommand
,所以通过该Filter发出的请求实际上就同时集成了Ribbon和Hystrix。
通过实现接口可以实现Zuul的容错与回退功能,下面这个例子来自Zuul的源码DefaultFallbackProvider
,这里我稍微修改了下:
@Component
public class DefaultFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
//匹配微服务名字,*表示匹配所有
return "*";
}
@Override
public ClientHttpResponse fallbackResponse() {
return null;
}
@Override
public ClientHttpResponse fallbackResponse(Throwable cause) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
//当fallback时返回给调用者的状态码
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return this.getStatusCode().value();
}
@Override
public String getStatusText() throws IOException {
//状态码的文本形式
return null;
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
//响应体
return new ByteArrayInputStream("default fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
//设定headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return headers;
}
};
}
}
停掉Rest-Demo后再请求,会发现返回了DefaultFallbackProvider
中定义fallback的内容。
作为网关这么重要的角色,高可用是非常有必要的。但是通常来说网关所面对的请求应该的是来于外部,所以虽然说网关可以注册到Eureka Server上,但是外部的客户端数量众多,是不可能向Eureka Server注册的。那么要实现高可用的,要么在网关前面再架一个前置代理(如Nginx),要么让客户端从Eureka Server处获取Zuul网关地址实现客户端负载均衡。