Spring Cloud Zuul(1)

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 - 可以将特定请求路由到不同的主机进行调试

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值