[TOC]
1. API网关简介
API 网关可以看做系统与外界联通的入口,我们可以在网关处理一些非业务逻辑的逻辑,比如权限验证,监控,缓存,请求路由等等。因此API网关可以承接两个方向的入口。
- 移动APP/WEB的统一入口网关。
- 业务方快速提开放能力。
2. 如何实现一个网关
2.1 网关核心
API网关一般按照职责链的模式实现,核心链路一般分为三个部分: 预处理、请求转发和处理结果。
职责链可以通过过滤器的方式去实现,过滤器中定义是否需要执行和执行的顺序,通过上下文变量透传给每个过滤器。
2.1.1 预处理
这一环节,可以可插拔式的,扩展很多过滤器,例如:
2.1.1.1 初始化API
将API信息、服务提供方信息查出来,并验证API的合法性。
2.1.1.2 API鉴权
对API进行鉴权认证,可自定义鉴权方式,例如OAuth2、签名认证。
2.1.1.3 访问控制
对API的访问进行控制,调用者是否进入黑名单,调用方是否已授权调用该API。
2.1.1.4 限流控制
对API进行流量控制,可以根据调用者、API两个维度进行流量控制,流量控制相对比较灵活,可以按照组合方式进行流控。
2.1.1.5 参数转换
根据API路由到后端地址的规则,进行参数转换,构建出需要请求的参数。
2.1.2 请求转发
这一环节,可以根据协议的不通选择不同的转发方式,rpc、http协议转发的方式不同,这一环节可以借助一些框架来实现,rpc可选择dubbo、http可选择ribbon,这样方便解决负载均衡的调用。同时可以为调用做资源隔离、保证路由转发时具备容错机制,市面上较为主流的为hystrix。
2.1.3 处理结果
这一环节,对于调用需要处理的报文进行处理,记录下来,用于对调用情况做分析统计。同时也对一些异常情况处理,加上默认的响应报文。
2.2 网关设计图
3. netlifx-zuul 1.x
zuul是由netflix开源的一个网关,可以提供动态的路由、监控和安全性保证。 zuul 1.x是基于servlet构建的一个框架,通过一系列filter,完成职责链的设计模式。而zuul1.x主要包含了四类过滤器:
- pre: 请求路由被调用前的前置过滤器。
- route:真正实现路由转发的过滤器,这种过滤器讲请求转发至真实的后端微服务,返回相关请求结果。
- post: 收到请求后进行调用。用于收集统计信息,处理响应报文。
- error: 在任意阶段发生错误后会执行,进行统一的异常处理。
3.1 Zuul Request Lifecycle
3.2 zuul实现
3.2.1 ZuulServlet
ZuulServlet是Zuul的转发引擎,所有的请求都由该servlet统一处理,调用servlet的service函数对请求进行过滤。
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
// 初始化当前的zuul request context,将request和response放入上下文中
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();
zuul对请求的处理流程 start
// zuul对一个请求的处理流程:pre -> route -> post
// 1. post是必然执行的(可以类比finally块),但如果在post中抛出了异常,交由error处理完后就结束,避免无限循环
// 2. 任何阶段抛出了ZuulException,都会交由error处理
// 3. 非ZuulException会被封装后交给error处理
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
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();
}
}
复制代码
3.2.2 RequestContext
保存请求、响应、状态信息和数据,以便zuulfilters访问和共享,可以通过设置ContextClass来替换RequestContext的扩展。
3.2.3 ZuulRunner
该类将servlet请求和响应初始化为RequestContext并包装FilterProcessor(filter的处理器)调用,用于处理 reRoute(), route(), postRoute(), and error()。
3.2.4 FilterProcessor
过滤器的处理器,核心函数是runFilters():
/**
* runs all filters of the filterType sType/ Use this method within filters to run custom filters by type
*
* @param sType the filterType.
* @return
* @throws Throwable throws up an arbitrary exception
*/
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
// 通过FilterLoader获取指定类型的所有filter
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
// 这里没有进行try...catch... 意味着只要任何一个filter执行失败了整个过程就会中断掉
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;
}
复制代码
3.2.5 ZuulFilter
/**
* runFilter checks !isFilterDisabled() and shouldFilter(). The run() method is invoked if both are true.
*
* @return the return from ZuulFilterResult
*/
public ZuulFilterResult runFilter() {
ZuulFilterResult zr = new ZuulFilterResult();
// 当前filter是否被禁用
if (!isFilterDisabled()) {
if (shouldFilter()) {
Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
try {
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;
}
复制代码
3.2.6 FilterRegistry
Filter 注册类,包含一个ConcurrentHashMap, 按照类型保存filter。
3.2.7 FilterLoader
用来通过加载groovy的过滤器文件,注册到FilterRegistry。
/**
* From a file this will read the ZuulFilter source code, compile it, and add it to the list of current filters
* a true response means that it was successful.
* 从一个文件中,read出filter的源代码,编译它,并将其添加到当前过滤器列表中。
*
* @param file
* @return true if the filter in file successfully read, compiled, verified and added to Zuul
* @throws IllegalAccessException
* @throws InstantiationException
* @throws IOException
*/
public boolean putFilter(File file) throws Exception {
String sName = file.getAbsolutePath() + file.getName();
if (filterClassLastModified.get(sName) != null && (file.lastModified() != filterClassLastModified.get(sName))) {
LOG.debug("reloading filter " + sName);
filterRegistry.remove(sName);
}
ZuulFilter filter = filterRegistry.get(sName);
if (filter == null) {
Class clazz = COMPILER.compile(file);
if (!Modifier.isAbstract(clazz.getModifiers())) {
filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
List<ZuulFilter> list = hashFiltersByType.get(filter.filterType());
if (list != null) {
hashFiltersByType.remove(filter.filterType()); //rebuild this list
}
filterRegistry.put(file.getAbsolutePath() + file.getName(), filter);
filterClassLastModified.put(sName, file.lastModified());
return true;
}
}
return false;
}
复制代码
8.FileManager
/**
* Initialized the GroovyFileManager.
*
* @param pollingIntervalSeconds the polling interval in Seconds 多少秒进行轮训
* @param directories Any number of paths to directories to be polled may be specified
* @throws IOException
* @throws IllegalAccessException
* @throws InstantiationException
*/
public static void init(int pollingIntervalSeconds, String... directories) throws Exception, IllegalAccessException, InstantiationException {
if (INSTANCE == null) INSTANCE = new FilterFileManager();
//文件夹路径 ["src/main/groovy/filters/pre", "src/main/groovy/filters/route", "src/main/groovy/filters/post"]
INSTANCE.aDirectories = directories;
//轮训时间
INSTANCE.pollingIntervalSeconds = pollingIntervalSeconds;
//按照文件夹路径扫出以.groovy文件结尾的文件数组,然后通过FilterLoader读取filter,并放入filter到内存中。
INSTANCE.manageFiles();
//一直轮训的线程
INSTANCE.startPoller();
}
复制代码
3.2.8 StartServer
StartServer是一个ServletContextListener,负责在web应用启动后执行一些初始化操作
4 spring-cloud-netflix-zuul
4.1 spring-cloud做了什么?
4.1.1.ZuulHandlerMapping
ZuulHandlerMapping在注册发生在第一次请求发生的时候,在ZuulHandlerMapping.lookupHandler方法中执行。在ZuulHandlerMapping.registerHandlers方法中首先获取所有的路由,然后调用AbstractUrlHandlerMapping.registerHandler将路由中的路径和ZuulHandlerMapping相关联。
@Override
protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
if (this.errorController != null && urlPath.equals(this.errorController.getErrorPath())) {
return null;
}
if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) return null;
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.containsKey("forward.to")) {
return null;
}
//默认dirty为true,第一次请求进入。
if (this.dirty) {
synchronized (this) {
if (this.dirty) {
//注册handler,将自定义的路由映射到springmvc的map中。
registerHandlers();
this.dirty = false;
}
}
}
//调用抽象类的lookupHandler,匹配不到的话,直接抛出404。ZuulHandlerMapping借助springmvc特性,做路由匹配。
return super.lookupHandler(urlPath, request);
}
private boolean isIgnoredPath(String urlPath, Collection<String> ignored) {
if (ignored != null) {
for (String ignoredPath : ignored) {
if (this.pathMatcher.match(ignoredPath, urlPath)) {
return true;
}
}
}
return false;
}
private void registerHandlers() {
//通过路由定位器扫出路由信息,遍历路由,调用springmvc的路由。转发的handler是自定义的ZuulController,用于包装ZuulServlet。
Collection<Route> routes = this.routeLocator.getRoutes();
if (routes.isEmpty()) {
this.logger.warn("No routes found from RouteLocator");
}
else {
for (Route route : routes) {
registerHandler(route.getFullPath(), this.zuul);
}
}
}
复制代码
4.1.2 ZuulController
ZuulController是ZuulServlet的一个包装类,ServletWrappingController是将当前应用中的某个Servlet直接包装为一个Controller,所有到ServletWrappingController的请求实际上是由它内部所包装的这个Servlet来处理。
public class ZuulController extends ServletWrappingController {
public ZuulController() {
setServletClass(ZuulServlet.class);
setServletName("zuul");
setSupportedMethods((String[]) null); // Allow all
}
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
// We don't care about the other features of the base class, just want to
// handle the request
return super.handleRequestInternal(request, response);
}
finally {
// @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter
RequestContext.getCurrentContext().unset();
}
}
}
复制代码
4.1.3 RouteLocator
RouteLocator有三个实现类:SimpleRouteLocator、DiscoveryClientRouteLocator、CompositeRouteLocator。CompositeRouteLocator是一个综合的路由定位器,会包含当前定义的所有路由定位器。
4.1.4 springcloud提供的filter
pre filter | 位置 | 是否执行 | 作用 |
---|---|---|---|
ServletDetectionFilter | -3 | 一直执行 | 判断该请求是否过dispatcherServlet,是否从spring mvc转发过来 |
Servlet30WrapperFilter | -2 | 一直执行 | 包装HttpServletRequest |
FormBodyWrapperFilter | -1 | Content-Type为application/x-www-form-urlencoded或multipart/form-data | request包装成FormBodyRequestWrapper |
DebugFilter | 1 | 配置了zuul.debug.parameter或者请求中包含zuul.debug.parameter | 设置debugRouting和debugRequest参数设置为true,可以通过开启此参数,激活debug信息。 |
PreDecorationFilter | 5 | 上下文不存在forward.to和serviceId两个参数 | 从上下文解析出地址,然后取出路由信息,将路由信息放入上下文中。 |
4.1.4.1 ServletDetectionFilter
判断该请求是否过dispatcherServlet,是否从spring mvc转发过来。
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (!(request instanceof HttpServletRequestWrapper)
&& isDispatcherServletRequest(request)) {
ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
} else {
ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
}
return null;
}
复制代码
@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
public ServletRegistrationBean zuulServlet() {
//servlet
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;
}
复制代码
4.1.4.2 Servlet30WrapperFilter
关于Servlet30WrapperFilter的存在,存在意义不是很大,主要是为了给zuul1.2.2版本容错,最新版的zuul1.x已经修改,bug原因是,从zuul获取的request包装类,拿到的是HttpServletRequestWrapper,老版本的zuul,是这么做的:
public class HttpServletRequestWrapper implements HttpServletRequest
复制代码
而在tomcat容器中的ApplicationDispatcher类中对request包装类判断,会导致直接break。
while (!same) {
if (originalRequest.equals(dispatchedRequest)) {
same = true;
}
if (!same && dispatchedRequest instanceof ServletRequestWrapper) {
dispatchedRequest =
((ServletRequestWrapper) dispatchedRequest).getRequest();
} else {
break;
}
}
复制代码
route filter | 位置 | 是否执行 | 作用 |
---|---|---|---|
RibbonRoutingFilter | 10 | 一直执行 | 判断该请求是否过dispatcherServlet,是否从spring mvc转发过来 |
SimpleHostRoutingFilter | 100 | 上下文包含routeHost | 包装HttpServletRequest |
SendForwardFilter | 500 | 上下文中包含forward.to | 获取转发的地址,做跳转。 |
4.1.4.3 RibbonRoutingFilter
// 根据上下文创建command,command是hystrix包裹后的实例。
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
RibbonCommand command = this.ribbonCommandFactory.create(context);
try {
ClientHttpResponse response = command.execute();
return response;
}catch (HystrixRuntimeException ex) {
return handleException(info, ex);
}
}
复制代码
RibbonCommand根据RibbonCommandFactory来创建,工厂类一共有三个实现类,分别对应三种http调用框架:httpClient、okHttp、restClient。默认选择HttpClient:
@Configuration
@ConditionalOnRibbonHttpClient
protected static class HttpClientRibbonConfiguration {
@Autowired(required = false)
private Set<FallbackProvider> zuulFallbackProviders = Collections.emptySet();
@Bean
@ConditionalOnMissingBean
public RibbonCommandFactory<?> ribbonCommandFactory(
SpringClientFactory clientFactory, ZuulProperties zuulProperties) {
return new HttpClientRibbonCommandFactory(clientFactory, zuulProperties, zuulFallbackProviders);
}
}
复制代码
4.1.4.4 RibbonCommand
以默认HttpClientRibbonCommand为例:
public HttpClientRibbonCommand create(final RibbonCommandContext context) {
//获取所有ZuulFallbackProvider,即当Zuul调用失败后的降级方法
FallbackProvider zuulFallbackProvider = getFallbackProvider(context.getServiceId());
//创建转发的client类,是RibbonLoadBalancingHttpClient类型的。
final String serviceId = context.getServiceId();
final RibbonLoadBalancingHttpClient client = this.clientFactory.getClient(serviceId, RibbonLoadBalancingHttpClient.class);
//设置LoadBalancer
client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));
// 创建Command,设置hystrix配置的众多参数。
return new HttpClientRibbonCommand(serviceId, client, context, zuulProperties, zuulFallbackProvider, clientFactory.getClientConfig(serviceId));
}
复制代码
RibbonCommand根据模板的设计模式,抽象类中有默认的实现方式:
@Override
protected ClientHttpResponse run() throws Exception {
final RequestContext context = RequestContext.getCurrentContext();
RQ request = createRequest();
RS response;
boolean retryableClient = this.client instanceof AbstractLoadBalancingClient
&& ((AbstractLoadBalancingClient)this.client).isClientRetryable((ContextAwareRequest)request);
if (retryableClient) {
response = this.client.execute(request, config);
} else {
response = this.client.executeWithLoadBalancer(request, config);
}
context.set("ribbonResponse", response);
// Explicitly close the HttpResponse if the Hystrix command timed out to
// release the underlying HTTP connection held by the response.
//
if (this.isResponseTimedOut()) {
if (response != null) {
response.close();
}
}
return new RibbonHttpResponse(response);
}
复制代码
4.1.4.5 executeWithLoadBalancer
当调用者希望将请求分派给负载均衡器选择的服务器时,应该使用此方法,而不是在请求的URI中指定服务器。
/**
* This method should be used when the caller wants to dispatch the request to a server chosen by
* the load balancer, instead of specifying the server in the request's URI.
* It calculates the final URI by calling {@link #reconstructURIWithServer(com.netflix.loadbalancer.Server, java.net.URI)}
* and then calls {@link #executeWithLoadBalancer(ClientRequest, com.netflix.client.config.IClientConfig)}.
*
* @param request request to be dispatched to a server chosen by the load balancer. The URI can be a partial
* URI which does not contain the host name or the protocol.
*/
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
// 专门用于失败切换其他服务端进行重试的 Command
LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
try {
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}
复制代码
public Observable<T> submit(final ServerOperation<T> operation) {
// ...
// 外层的 observable 为了不同目标的重试
// selectServer() 是进行负载均衡,返回的是一个 observable,可以重试,重试时再重新挑选一个目标server
Observable<T> o = selectServer().concatMap(server -> {
// 这里又开启一个 observable 主要是为了同机重试
Observable<T> o = Observable
.just(server)
.concatMap(server -> {
return operation.call(server).doOnEach(new Observer<T>() {
@Override
public void onCompleted() {
// server 状态的统计,譬如消除联系异常,抵消activeRequest等
}
@Override
public void onError() {
// server 状态的统计,错误统计等
}
@Override
public void onNext() {
// 获取 entity, 返回内容
}
});
})
// 如果设置了同机重试,进行重试
if (maxRetrysSame > 0)
// retryPolicy 判断是否重试,具体分析看下面
o = o.retry(retryPolicy(maxRetrysSame, true));
return o;
})
// 设置了异机重试,进行重试
if (maxRetrysNext > 0)
o = o.retry(retryPolicy(maxRetrysNext, false));
return o.onErrorResumeNext(exp -> {
return Observable.error(e);
});
}
复制代码
关于默认情形下为什么不会重试?参考: blog.didispace.com/spring-clou…
4.1.4.6 ribbon的IRule负载均衡策略
默认选择ZoneAvoidanceRule策略,该策略剔除不可用区域,判断出最差的区域,在剩下的区域中,将按照服务器实例数的概率抽样法选择,从而判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。
具体的策略参考该博文:ju.outofmemory.cn/entry/25384…
post filter | 位置 | 是否执行 | 作用 |
---|---|---|---|
LocationRewriteFilter | 900 | http响应码是3xx | 对 状态是 301 ,相应头中有 Location 的相应进行处理 |
SendResponseFilter | 1000 | 没有抛出异常,RequestContext中的throwable属性为null(如果不为null说明已经被error过滤器处理过了,这里的post过滤器就不需要处理了),并且RequestContext中zuulResponseHeaders、responseDataStream、responseBody三者有一样不为null(说明实际请求的响应不为空)。 | 将服务的响应数据写入当前响应 |
error filter | 位置 | 是否执行 | 作用 |
---|---|---|---|
SendErrorFilter | 0 | 上下文throable不为null | 处理上下文有错误的filter |
4.2 借助spring-cloud如何扩展?
- 如果服务发现不是用eureka,要自己重写服务发现逻辑,也就是ribbon获取ServerList,前提是用ribbon。
- 如果用ribbon,并且用RibbonCommand,那么会捆绑Hystrix组件,容错不可自选。
- 如果走rpc协议,需要自己重写route的所有逻辑。
- springcloud的autoconfig,默认开启了很多配置,需要禁用filter、以及重写一些bean的创建。
4.3 总结
spring cloud对于zuul的封装比较完善,同时也表现出较难扩展,尤其对ribbon、hystrix等组件不够熟悉的前提下,使用它无非是给自己未来制造难题,相比之下原生的zuul-core相对比较简单和灵活,但是开发成本较高。