Zuul是在云平台上提供动态路由,监控、弹性、安全等边缘服务的框架。Zuul相当于是设备和Netflix流应用的Web网站后端所有请求的前门。
架构概述
从高层来看,Zuul 2.0是一个Netty服务器,它运行pre-filters(入站过滤器),然后使用Netty客户端代理请求,然后在运行post-filter(出站过滤器)后返回响应。
Filters
过滤器是Zuul业务逻辑的核心所在。它们具有执行大量操作的能力,可以在请求-响应生命周期的不同部分运行,如上图所示。
- Inbound Filters 入站过滤器,在路由到Origin之前执行,可用于身份验证、路由和装饰请求等。
- Endpoint Filters 端点筛选器,可用于返回静态响应,否则内置的ProxyEndpoint筛选器将把请求路由到origin。
- Outbound Filters 出站过滤器,在从origin获取响应后执行,可用于度量、装饰用户响应或添加自定义头。
还有两种类型的过滤器:同步(sync)和异步(async)。因为我们在一个事件循环上运行,所以绝对不要在过滤器中阻塞。如果要阻止,请在单独的线程池上的异步筛选器中执行,否则可以使用同步筛选器。
服务器模式
当前支持的服务器模式有:
- HTTP(超文本传输协议)
- HTTP/2(需要TLS)
- HTTP- Mutual TLS
我们可以通过更改服务器类型变量的值来运行不同的模式。
HTTP
这是为了在ELB HTTP侦听器后面运行时使用,该侦听器为我们终止TLS并向我们传递XFF头。使用此模式的属性设置示例。
如果我们想在没有任何ELB的情况下以明文模式运行,出于安全原因,我们可能需要删除代理头。我们将要使用此设置:
channelConfig.set(CommonChannelConfigKeys.allowProxyHeadersWhen, StripUntrustedProxyHeadersHandler.AllowWhen.NEVER);
HTTP/2
ELBs不支持HTTP/2,因此,如果要使用HTTP/2,则可能会使用ELB TCP侦听器并在Zuul上终止协议。因此,HTTP/2配置需要启用SSL证书和代理协议来代替XFF头。
如果使用ALB终止HTTP/2,则可以使用上面的HTTP配置。
Mutual TLS
ELB也不支持 mutual TLS,所以同样,我们必须使用ELB TCP侦听器并在Zuul上终止TLS。在此模式下,我们需要TLS证书和传入客户端证书的信任存储。我们可能还希望启用代理协议来代替XFF头。
Filters
过滤器是Zuul功能的核心。它们负责应用程序的业务逻辑,并可以执行各种任务。
- Type:通常在路由流期间定义将应用筛选器的阶段(尽管它可以是任何自定义字符串)
- Async:定义过滤器是同步还是异步,通常意味着我们需要进行外部调用还是只在box上工作
- Execution Order:应用于类型中,定义跨多个筛选器的执行顺序
-
Criteria:执行筛选器所需的条件
-
Action:满足条件时要执行的操作
Zuul提供了一个框架来动态读取、编译和运行这些过滤器。过滤器之间不直接通信,而是通过每个请求唯一的RequestContext共享状态。
虽然Zuul支持任何基于JVM的语言,但过滤器目前是用Groovy编写的。每个过滤器的源代码都会写入Zuul服务器上的一组指定目录,这些目录会定期轮询更改。更新的过滤器从磁盘读取,动态编译到正在运行的服务器中,并由Zuul为每个后续请求调用。
Incoming
Incoming filters在请求代理到origin之前执行。这通常是执行大部分业务逻辑的地方。例如:身份验证、动态路由、速率限制、DDoS保护、度量。
Endpoint
Endpoint filters 负责根据incoming filters的执行来处理请求。Zuul附带了一个内置过滤器(ProxyEndpoint),用于将请求代理到后端服务器,因此这些过滤器的典型用途是用于静态端点。例如:健康检查响应、静态错误响应、404响应。
Outgoing
Outgoing filters在接收到来自后端的响应后处理操作。通常,它们比任何繁重的工作更多地用于形成响应和添加指标。例如:存储统计信息、添加/剥离标准头、将事件发送到实时流、gziping响应。
Async
过滤器可以同步或异步执行。如果我们的过滤器没有做很多工作,并且没有阻塞或关闭,我们可以通过扩展HttpInboundSyncFilter或HttpOutboundSyncFilter安全地使用同步过滤器。
然而,在某些情况下,我们需要从另一个服务或缓存获取一些数据,或者可能需要进行一些繁重的计算。在这些情况下,我们不应该阻塞Netty线程,而应该使用异步过滤器为响应返回一个可观察的包装器。对于这些过滤器,我们可以扩展HttpInboundFilter或HttpOutboundFilter。
sync filter 的例子:
/**
* Author: js
* Date: Created in 2021/10/25 19:02
*/
public class SyncRoutes extends HttpInboundSyncFilter {
@Override
public HttpRequestMessage apply(HttpRequestMessage httpRequestMessage) {
SessionContext context = httpRequestMessage.getContext();
String path = httpRequestMessage.getPath();
String originalHost = httpRequestMessage.getOriginalHost();
if ("/Healthcheck".equalsIgnoreCase(path)){
context.setEndpoint(Healthcheck.class.getCanonicalName());
}else {
context.setEndpoint(ProxyEndpoint.class.getCanonicalName());
context.setRouteVIP("api");
}
return httpRequestMessage;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter(HttpRequestMessage httpRequestMessage) {
return true;
}
}
提取body内容
默认情况下,Zuul不会缓冲正文内容,这意味着它会在收到正文之前将收到的标题流式传输到origin。只要过滤逻辑依赖于头数据,这种流行为是非常有效和可取的。但是,如果要在入站或出站筛选器中提取请求或响应正文,则需要显式地告诉Zuul缓冲正文。我们可以通过覆盖过滤器中的needsBodyBuffered()方法来实现这一点:
@Override
public boolean needsBodyBuffered(HttpRequestMessage input) {
return true;
}
有用的过滤器
Zuul在示例应用程序中附带了一些有用的过滤器和其他过滤器的示例。
Sample Filters
- DebugRequest - 查找查询参数以为请求添加额外的调试日志记录
public class DebugRequest extends HttpInboundSyncFilter {
@Override
public HttpRequestMessage apply(HttpRequestMessage httpRequestMessage) {
SessionContext context = httpRequestMessage.getContext();
Debug.addRequestDebug(context,"request::"+httpRequestMessage.getOriginalScheme()+" "+httpRequestMessage.getOriginalHost()+" "+httpRequestMessage.getOriginalPort());
Debug.addRequestDebug(context,"request::= "+httpRequestMessage.getMethod()+" "+httpRequestMessage.reconstructURI()+" "+httpRequestMessage.getProtocol());
Headers headers = httpRequestMessage.getHeaders();
Iterator<Header> iterator = headers.entries().iterator();
while (iterator.hasNext()){
String name = iterator.next().getName().getName();
String value = httpRequestMessage.getHeaders().getFirst(name);
Debug.addRequestDebug(context,"request::="+name+":"+value);
}
if (httpRequestMessage.hasBody()){
Debug.addRequestDebug(context,"request::="+httpRequestMessage.getBodyAsText());
}
return httpRequestMessage;
}
@Override
public int filterOrder() {
return 21;
}
@Override
public boolean shouldFilter(HttpRequestMessage httpRequestMessage) {
return httpRequestMessage.getContext().debugRequest();
}
@Override
public boolean needsBodyBuffered(HttpRequestMessage input) {
return shouldFilter(input);
}
}
- Healthcheck - 简单的静态端点过滤器,如果一切都正确引导,则返回200
public class Healthcheck extends HttpSyncEndpoint {
@Override
public HttpResponseMessage apply(HttpRequestMessage httpRequestMessage) {
HttpResponseMessageImpl httpResponseMessage = new HttpResponseMessageImpl(httpRequestMessage.getContext(), httpRequestMessage, 200);
httpResponseMessage.setBodyAsText("healthy");
//需要手动设置,因为我们没有经过ProxyEndpoint
StatusCategoryUtils.setStatusCategory(httpRequestMessage.getContext(), ZuulStatusCategory.SUCCESS);
return httpResponseMessage;
}
}
- ZuulResponseFilter - 添加信息头以提供有关路由、请求执行、状态和错误原因的额外详细信息
public class ZuulResponseFilter extends HttpOutboundSyncFilter {
private static final Logger log = LoggerFactory.getLogger(ZuulResponseFilter.class);
private static final DynamicBooleanProperty SEND_RESPONSE_HEADERS = new DynamicBooleanProperty("zuul.responseFilter.send.headers",true);
@Override
public HttpResponseMessage apply(HttpResponseMessage httpResponseMessage) {
SessionContext context = httpResponseMessage.getContext();
if (SEND_RESPONSE_HEADERS.get()){
Headers headers = httpResponseMessage.getHeaders();
StatusCategory statusCategory = StatusCategoryUtils.getStatusCategory(context);
if (statusCategory != null){
headers.set(X_ZUUL_STATUS,statusCategory.name());
}
RequestAttempts attempts = RequestAttempts.getFromSessionContext(context);
String headerStr="";
if (attempts != null){
headerStr = attempts.toString();
}
headers.set(X_ZUUL_PROXY_ATTEMPTS,headerStr);
headers.set(X_ZUUL,"zuul");
headers.set(X_ZUUL_INSTANCE,System.getenv("EC2_INSTANCE_ID") == null ? "unknown": System.getenv("EC2_INSTANCE_ID"));
headers.set(CONNECTION,KEEP_ALIVE);
headers.set(X_ORIGINATING_URL,httpResponseMessage.getInboundRequest().reconstructURI());
if (httpResponseMessage.getStatus()>=400 && context.getError() != null){
Throwable error = context.getError();
headers.set(X_ZUUL_ERROR_CAUSE,error instanceof ZuulException ? ((ZuulException) error).getErrorCause() : "UNKNOWN_CAUSE");
}
if (httpResponseMessage.getStatus()>=500){
log.info("Passport:{}", CurrentPassport.fromSessionContext(context));
}
if (log.isDebugEnabled()){
log.debug("Filter execution summary :: {}",context.getFilterExecutionSummary());
}
if (context.debugRequest()){
Debug.getRequestDebug(context).forEach(s ->{log.info("req_debug:"+s);});
Debug.getRoutingDebug(context).forEach(s ->{log.info("zuul_debug:"+s);});
}
}
return httpResponseMessage;
}
@Override
public int filterOrder() {
return 999;
}
@Override
public boolean shouldFilter(HttpResponseMessage httpResponseMessage) {
return true;
}
}
Core Filters
- GZipResponseFilter - 可以启用gzip出站响应
- SurgicalDebugFilter - 可以将特定请求路由到不同的主机进行调试