前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微服务架构之网关层Zuul剖析

微服务架构之网关层Zuul剖析

作者头像
公众号_松华说
发布2019-07-16 11:36:56
4270
发布2019-07-16 11:36:56
举报
文章被收录于专栏:松华说松华说

单体架构时代,应用可以自己做过滤器、限流等非业务逻辑,但是随着微服务的推广盛行,如果每个微服务重复造轮子甚至需要对多终端兼容,效率低下,此时迫切需要一种通用的解决方案,从而演化出API网关,单点入口、路由转发、限流熔断、监控、安全认证等通用的功能由网关来承担

一、Zuul简介

Zuul相当于是第三方调用和服务提供方之间的防护门,其中最大的亮点就是可动态发布过滤器

二、Zuul可以为我们提供什么

1、 权限控制

2、预警和监控

3、红绿部署、(粘性)金丝雀部署,流量调度支持

4、流量复制转发,方便分支测试、埋点测试、压力测试

5、跨区域高可用,异常感知

6、防爬防攻击

7、负载均衡、健康检查和屏蔽坏节点

8、静态资源处理

9、重试容错服务

三、Zuul网关架构

可以看到其架构主要分为发布模块、控制管理加载模块、运行时模块、线程安全的请求上下文模块。在Spring Cloud中,Zuul每个后端都称为一个Route,为了避免资源抢占,整合了Hystrix进行隔离和限流,基于线程的隔离机制,另外一种机制是信号量,后面文章会提到。Zuul默认使用ThreadLocal

代码语言:javascript
复制
protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
     @Override
     protected RequestContext initialValue() {
        try {
           return contextClass.newInstance();
        } catch (Throwable e) {
           e.printStackTrace();
           throw new RuntimeException(e);
        }
     }
  };

请求处理生命周期,"pre" filters(认证、路由、请求日记)->"routing filters"(将请求发送到后端)->"post" filters(增加HTTP头、收集统计和度量、客户端响应)

四、过滤器

一些概念

1、类型Type,定义被运行的阶段,也就是pre\routing\post\error阶段

2、顺序Execution Order,定义同类型链执行中顺序

3、条件Criteria,定义过滤器执行的前提条件

4、动作Action,定义过滤器执行的业务

下面是一个DEMO

代码语言:javascript
复制
class DebugFilter extends ZuulFilter {

    static final DynamicBooleanProperty routingDebug = DynamicPropertyFactory.getInstance().getBooleanProperty(ZuulConstants.ZUUL_DEBUG_REQUEST, true)
    static final DynamicStringProperty debugParameter = DynamicPropertyFactory.getInstance().getStringProperty(ZuulConstants.ZUUL_DEBUG_PARAMETER, "d")

    @Override
    String filterType() {
        return 'pre'
    }

    @Override
    int filterOrder() {
        return 1
    }

    boolean shouldFilter() {
        if ("true".equals(RequestContext.getCurrentContext().getRequest().getParameter(debugParameter.get()))) {
            return true
        }

        return routingDebug.get();
    }

    Object run() {
        RequestContext ctx = RequestContext.getCurrentContext()
        ctx.setDebugRouting(true)
        ctx.setDebugRequest(true)
        ctx.setChunkedRequestBody()
        return null;
    }
代码语言:javascript
复制

五、代码剖析

在Servlet API 中有一个ServletContextListener接口,它能够监听 ServletContext 对象的生命周期,实际上就是监听 Web 应用的生命周期。接口中定义了两个方法

代码语言:javascript
复制
/**
 * 当Servlet 容器启动Web 应用时调用该方法。在调用完该方法之后,容器再对Filter 初始化,
 * 并且对那些在Web 应用启动时就需要被初始化的Servlet 进行初始化。
 */
contextInitialized(ServletContextEvent sce)


/**
 * 当Servlet 容器终止Web 应用时调用该方法。在调用该方法之前,容器会先销毁所有的Servlet 和Filter 过滤器。
 */
contextDestroyed(ServletContextEvent sce)
代码语言:javascript
复制

在Zuul网关中

代码语言:javascript
复制
代码语言:javascript
复制
public class InitializeServletListener implements ServletContextListener {

@Override
public void contextInitialized(ServletContextEvent arg0) {
    try {
        //实例化
        initZuul();
       
    } catch (Exception e) {
        LOGGER.error("Error while initializing zuul gateway.", e);
        throw new RuntimeException(e);
    }
}

 private void initZuul() throws Exception {
     //文件管理
    FilterFileManager.init(5, preFiltersPath, postFiltersPath, routeFiltersPath, errorFiltersPath);
    //从DB中加载Filter
    startZuulFilterPoller();
  }
}

在initZuul中,FilterFileManager主要是做文件管理,起一个poll Thread,定期把FilterDirectory中file放到FilterLoader中,在FilterLoad中会进行编译再放到filterRegistry中。而startZuulFilterPoller主要是判断DB中有是否变化或者新增的Filer,然后写到FilterDirectory中

代码语言:javascript
复制
public boolean putFilter(File file) throws Exception {
   Class clazz = COMPILER.compile(file);
    if (!Modifier.isAbstract(clazz.getModifiers())) {
        filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
        filterRegistry.put(file.getAbsolutePath() + file.getName(), filter);
        filterClassLastModified.put(sName, file.lastModified());
        //二次hash检查
        List<ZuulFilter> list = hashFiltersByType.get(filter.filterType());
        if (list != null) {
            hashFiltersByType.remove(filter.filterType()); //rebuild this list
      }
     }
  }
代码语言:javascript
复制

过滤器对应的DB字段有

filter_id,revision,create_time,is_active,is_canary,filter_code,filter_type,filter_name,disable_property_name,filter_order,application_name

我们再回到主流程看ZuulServlet,每当一个客户请求一个HttpServlet对象,该对象的service()方法就要被调用,而且传递给这个方法一个"请求"(ServletRequest)对象和一个"响应"(ServletResponse)对象作为参数

代码语言:javascript
复制
public class SynZuulServlet extends HttpServlet {

private ZuulRunner zuulRunner = new ZuulRunner();

@Override
public void service(javax.servlet.ServletRequest req, javax.servlet.ServletResponse res) throws javax.servlet.ServletException, java.io.IOException {
    try {
        init((HttpServletRequest) req, (HttpServletResponse) res);
RequestContext.getCurrentContext().setZuulEngineRan();
        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) {
    } finally {
        RequestContext.getCurrentContext().unset();
    }
}
代码语言:javascript
复制

运行时主要从filterRegistry根据type取出过滤器依次执行

六、Zuul2.x版本解读

Zuul2.x的核心功能特性

服务器协议

HTTP/2——完整的入站(inbound)HTTP/2连接服务器支持 双向TLS(Mutual TLS)——支持在更安全的场景下运行

弹性特性

自适应重试——Netflix用于增强弹性和可用性的核心重试逻辑 源并发保护——可配置的并发限制,避免源过载,隔离Zuul背后的各个源

运营特性

请求Passport——跟踪每个请求的所有生命周期事件,这对调试异步请求非常有用 状态分类——请求成功和失败的可能状态枚举,比HTTP状态码更精细 请求尝试——跟踪每个代理的尝试和状态,对调试重试和路由特别有用

实际上Zuul2.x是将ZuulFilter变换成Netty Handler,在Netty中,一系列的Handler会聚合在一起并使用Pipline执行,拿Netty的Sample来说明下

代码语言:javascript
复制
代码语言:javascript
复制
//EventLoopGroup线程组,包含一组NIO线程
 //bossGroup\workerGroup,一个用于连接管理,另外一个进行SocketChannel的网络读写
 EventLoopGroup bossGroup = new NioEventLoopGroup();
 EventLoopGroup workerGroup = new NioEventLoopGroup();
 ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(10240, 0, 2, 0, 2))
                                    .addLast(new StringDecoder(UTF_8))
                                    .addLast(new LengthFieldPrepender(2))
                                    .addLast(new StringEncoder(UTF_8))
                                    .addLast(new ServerHandler());
                        }
                    }).childOption(ChannelOption.TCP_NODELAY, true);
  ChannelFuture future = bootstrap.bind(18080).sync();
代码语言:javascript
复制

在Zuul2.x中默认注册了这些Handler

代码语言:javascript
复制
代码语言:javascript
复制
@Override
protected void initChannel(Channel ch) throws Exception
{
    // Configure our pipeline of ChannelHandlerS.
    ChannelPipeline pipeline = ch.pipeline();

    storeChannel(ch);
    addTimeoutHandlers(pipeline);
    addPassportHandler(pipeline);
    addTcpRelatedHandlers(pipeline);
    addHttp1Handlers(pipeline);
    addHttpRelatedHandlers(pipeline);
    addZuulHandlers(pipeline);
}
代码语言:javascript
复制

我们在上面的pipeline中注册了一个ServerHandler,这个handler就是用来处理Client端实际发送的数据的

代码语言:javascript
复制
代码语言:javascript
复制
public class ServerHandler extends  SimpleChannelInboundHandler<String> {

@Override
public void channelRead0(ChannelHandlerContext ctx, String message) throws Exception {
    System.out.println("from client:" + message);
    JSONObject json = JSONObject.fromObject(message);
    String source = json.getString("source");
    String md5 = DigestUtils.md5Hex(source);
    json.put("md5Hex",md5);
    ctx.writeAndFlush(json.toString());//write bytes to socket,and flush(clear) the buffer cache.
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
    ctx.flush();
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    cause.printStackTrace();
    ctx.close();
}
}
代码语言:javascript
复制

Zuul2.x相比1.x最大的变化就是异步化,最大的功臣莫过于Netty,上面涉及到的很重要的就是ChannelPipleline和ChannelFuture

ChannelPipleline实际上是一个双向链表,提供了addBefore\addAfter\addFirst\addLast\remove等方法,链表操作会影响Handler的调用关系。ChannelFuture是为了解决如何获取异步结果的问题而声音设计的接口,有未完成和完成这两种状态,不过通过CannelFuture的get()方法获取结果可能导致线程长时间被阻塞,一般使用非阻塞的GenericFutureListener

代码语言:javascript
复制
@Override
 public void channelRead(ChannelHandlerContext ctx, Object msg) {
     ChannelFuture future = ctx.channel().close();
     future.addListener(new ChannelFutureListener() {
         public void operationComplete(ChannelFuture future) {

         }
     });
 }
代码语言:javascript
复制

点击原文深度了解NIO,Netty相关资料感兴趣的朋友可以网上了解

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 松华说 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Zuul简介
  • 二、Zuul可以为我们提供什么
  • 三、Zuul网关架构
  • 四、过滤器
  • 五、代码剖析
  • 六、Zuul2.x版本解读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档