摘要: 本文主要从网关的需求,以及Spring Cloud Zuul的线程模型和源码瓶颈分析结合,目前最近一段时间自研网关中间件纳管Spring Cloud的经验汇总整理。

一.自研网关纳管Spring Cloud的原因

1.1 为什么要自研网关

1.网关配置实时生效,配置灰度,回滚等 2.网关的性能,特别是防刷,限流,WAF等 3.动态Filter ,目前Zuul可以做到动态Filter,Filter配置下发,实时动态Filter 4.对网关的监控,告警,流量调拨,网关集群。 5.流程审计,增加Dsboard便捷的操作。

1.2 回顾Web容器线程模型

Servlet只是基于Java技术的web组件,该组件由容器托管,用于生成动态内容。Servlet容器是web Server或application server 的一部分,供基于Request/Response发送模型的网络服务,解码基于MIME的请求,并格式化基于MIME的响应。Servlet容器包含并管理Servlet生命周期。典型的Servlet容器有Tomcat、Jetty。

如上图所示,Tomcat基于NIO的多线程模型,如下图所示,其基于典型的Acceptor/Reactor线程模型,在Tomcat的线程模型中,Worker线程用来处理Request。当容器收到一个Request后,调度线程从Worker线程池中选出一个Worker线程,将请求传递给该线程,然后由该线程来执行Servlet的service()方法。且该worker线程只能同时处理一个Request请求,如果过程中发生了阻塞,那么该线程就会被阻塞,而不能去处理其他任务。 Servlet默认情况下一个单例多线程。

回到zuul,zuul逻辑的入口是 ZuulServlet.service(ServletRequest servletRequest, ServletResponse servletResponse),ZuulServlet本质就是一个Servlet。

RequestContext提供了执行filter Pipeline所需要的Context,因为Servlet是 单例多线程,这就要求RequestContext即要线程安全又要Request安全。context使用ThreadLocal保存,这样每个worker线程都有一个与其绑定的RequestContext,因为worker仅能同时处理一个Request, 这就保证了RequestContext即是线程安全的,又是Request安全的。所谓Request 安全,即该Request的Context不会与其他同时处理Request冲突。 RequestContext继承了ConcurrentHashMap。

三个核心的方法preRoute(),route(), postRoute(),zuul对request处理逻辑都在这三个方法里, ZuulServlet交给ZuulRunner去执行。由于 ZuulServlet是单例,因此 ZuulRunner也仅有一个实例

因此综上所述,Spring Cloud Zuul的Qps在 1000-2000Qps之间是有原因的,网关作为如此重要的组件,基于如上所述的需求,觉得自研网关中间件纳管Spring Cloud很有必要。

二.自研网关纳管Spring Cloud

2.1 网关整合Spring Cloud服务治理体系

2.1.1 整合服务治理体系思路

  • 如果服务注册中心使用的是Eureka,可以不引入Spring Cloud Eureka相关的依赖,直接通过定时任务发起Eureka REST请求,网关自身维护一个缓存列表,自己写LB,找到服务列表转发。

优点:不需要引入Spring Cloud,对网关Server进行瘦身,洁癖讨厌各种引入无用的jar; 缺点: 注册中心使用Eureka,可以通过Eureka REST接口获取服务注册列表,但是换成ZK,Consul,或者Etcd,直接歇菜。


  • 通过集成Spring Cloud Common中高度抽象的DiscoveryClient。

优点: 通过高度抽象的DiscoveryClient,无需关心实现细节和定时任务去刷新注册列表。 缺点:换注册中心,需要相应的更换对应配置和依赖,一堆有些无关紧要的jar,需要自己对其瘦身。

2.1.2 网关整合Spring Cloud Eureka

1.引入Spring Cloud Eureka Starter,排除不用的依赖,还需要努力瘦身ing。

  1.  <dependency>

  2.       <groupId>org.springframework.cloud</groupId>

  3.      <artifactId>spring-cloud-starter-eureka</artifactId>

  4.       <version>1.3.1.RELEASE</version>

  5.         <exclusions>

  6.                <exclusion>

  7.                    <groupId>org.springframework.cloud</groupId>

  8.                    <artifactId>spring-cloud-netflix-core</artifactId>

  9.                </exclusion>

  10.                <exclusion>

  11.                    <groupId>com.netflix.ribbon</groupId>

  12.                    <artifactId>ribbon-eureka</artifactId>

  13.                </exclusion>

  14.                <exclusion>

  15.                    <groupId>org.springframework.cloud</groupId>

  16.                    <artifactId>spring-cloud-starter-ribbon</artifactId>

  17.                </exclusion>

  18.                <exclusion>

  19.                    <groupId>org.springframework.cloud</groupId>

  20.                    <artifactId>

  21.                        spring-cloud-starter-archaius

  22.                    </artifactId>

  23.                </exclusion>

  24.                <exclusion>

  25.                    <artifactId>hibernate-validator</artifactId>

  26.                      <groupId>org.hibernate</groupId>

  27.                </exclusion>

  28.            </exclusions>

  29.  </dependency>

2、同Zuul一样,把网关自身注册到Eureka Server上,目的是为了获取服务注册列表。

  1. server.port=8082

  2. spring.application.name=janus-server

  3. eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

PS:鄙视的一点就是,Spring Cloud应该提供一个轻量级的java client,配置注册中心的地址,还不需要把网关自身注册到注册中心上。原因是:网关中间件,不需要和服务治理框架耦合的很深。

2.1.3 Netty Server与Spring Cloud内置的Server的整合

Netty Http Servert提供端口用于接收网关对外的请求,Spring Boot内置的server提供端口用于和Gateway-console交互,目前没找到Spring Boot内置Server和Netty Server合二为一的方法,但是一个服务暴露两个端口,很有必要。

  1. @SpringBootApplication

  2. @EnableDiscoveryClient

  3. public class JanusServerAppliaction {

  4.    private static Logger logger = LoggerFactory.getLogger(JanusServerAppliaction.class);

  5.    // 非SSL的监听HTTP端口

  6.    public static int httpPort = 8081;

  7.    public static void main(String[] args) throws Exception {

  8.        //①先启动Spring Boot内置Server

  9.        SpringApplication.run(JanusServerAppliaction.class, args);

  10.        // logger.info("services: {}", context.getBean("discoveryClient",

  11.        // DiscoveryClient.class).getServices());

  12.        logger.info("Gateway Server Application Start...");

  13.        // 解析启动参数

  14.        parseArgs(args);

  15.        // 初始化网关Filter和配置

  16.        logger.info("init Gateway Server ...");

  17.        JanusBootStrap.initGateway();

  18.        logger.info("start netty  Server...");

  19.        final JanusNettyServer gatewayServer = new JanusNettyServer();

  20.        // ②启动HTTP容器

  21.        gatewayServer.startServer(httpPort);

  22.    }

  23. }

NettyServer服务启动后,阻塞监听端口,会导致集成spring boot内置Server启动无日志打印,spring Boot容器也没启动。因此注意启动顺序。

2.2 提高自研网关的QPS必杀技

2.2.1 NettyServer初始化及启动代码

自研网关使用netty自带的线程池,共有三组线程池,分别为bossGroup、workerGroup和executorGroup,bossGroup用于接收客户端的TCP连接,workerGroup用于处理I/O等,executorGroup用于处理网关作业(执行Filter链)。

  1. public void startServer(int noSSLPort) throws InterruptedException {

  2.        // http请求ChannelInbound

  3.        final HttpInboundHandler httpInboundHandler = new HttpInboundHandler();

  4.        ServerBootstrap insecure = new ServerBootstrap();

  5.        insecure.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)

  6.                // SO_REUSEADDR,表示允许重复使用本地地址和端口

  7.                .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)

  8.                .option(ChannelOption.ALLOCATOR, ByteBufManager.byteBufAllocator)

  9.                /**

  10.                 * SO_KEEPALIVE

  11.                 * 该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,

  12.                 * 如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。

  13.                 */

  14.                .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)

  15.                .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)

  16.                .childOption(ChannelOption.ALLOCATOR, ByteBufManager.byteBufAllocator)

  17.                .childHandler(new ChannelInitializer<SocketChannel>() {

  18.                    @Override

  19.                    public void initChannel(SocketChannel ch) throws Exception {

  20.                        ChannelPipeline pipeline = ch.pipeline();

  21.                        // 对channel监控的支持 暂不支持

  22.                        // keepalive_timeout 的支持

  23.                        pipeline.addLast(

  24.                                new IdleStateHandler(ProperityConfig.keepAliveTimeout, 0,

  25.                                        0, TimeUnit.MILLISECONDS));

  26.                        // pipeline.addLast(new JanusHermesHandler());

  27.                        pipeline.addLast(new HttpResponseEncoder());

  28.                        // 经过HttpRequestDecoder会得到N个对象HttpRequest,first HttpChunk,second

  29.                        // HttpChunk,....HttpChunkTrailer

  30.                        pipeline.addLast(new HttpRequestDecoder(

  31.                                ProperityConfig.maxInitialLineLength,

  32.                                ProperityConfig.maxHeaderSize, 8192,

  33.                                ProperityConfig.validateHeaders));

  34.                        // 把HttpRequestDecoder得到的N个对象合并为一个完整的http请求对象

  35.                        pipeline.addLast(new HttpObjectAggregator(

  36.                                ProperityConfig.httpAggregatorMaxLength));

  37.                        // gzip的支持

  38.                        if (ProperityConfig.gzip) {

  39.                            pipeline.addLast(new JanusHttpContentCompressor(

  40.                                    ProperityConfig.gzipLevel,

  41.                                    ProperityConfig.gzipMinLength));

  42.                        }

  43.                        pipeline.addLast(httpInboundHandler);

  44.                    }

  45.                });

  46.        ChannelFuture insecureFuture = insecure.bind(noSSLPort).sync();

  47.        logger.info("[listen HTTP NoSSL][" + noSSLPort + "]");

  48.        /**

  49.         * Wait until the server socket is closed.</br>

  50.         * 找到之前的无日志打印spring 容器也没启动的原因了,集成spring boot

  51.         * 和eureka放上放下并不是问题,是因为JanusNettyServer服务启动后,阻塞监听端口导致的

  52.         **/

  53.        insecureFuture.channel().closeFuture().sync();

  54.        logger.info("[stop HTTP NoSSL success]");

  55. }

2.2.2 基于Netty Channel Pool实现REST的异步转发

RestInvokerFilter异步转发Filter,基于Netty Channel Pool实现REST的异步转发,提高自网关的性能的必杀技。

  1. public class RestInvokerFilter extends AbstractFilter {

  2.    @Override

  3.    public void run(final AbstractFilterContext filterContext,

  4.            final JanusHandleContext janusHandleContext) throws Exception {

  5.        // 自定义LB从Spring Cloud中服务注册缓存列表中获取服务实例

  6.        ServiceInstance serviceInstance = SpringCloudHelper.getServiceInstanceByLB(

  7.                janusHandleContext, janusHandleContext.getAPIInfo().getRouteServiceId());

  8.        // 生成发送的Request对象

  9.        FullHttpRequest outBoundRequest = getOutBoundHttpRequest(janusHandleContext);

  10.        // 转发的时候设置LB获取到的主机IP和端口即可

  11.        AsyncHttpRequest.builder()

  12.                .remoteAddress(

  13.                        serviceInstance.getHost() + ":" + serviceInstance.getPort())

  14.                .sessionContext(janusHandleContext)

  15.                /**

  16.                 * connection holding 500ms

  17.                 */

  18.                .holdingTimeout(ProperityConfig.janusHttpPoolOauthMaxHolding).build()

  19.                .execute(new SimpleHttpCallback(janusHandleContext) {

  20.                    @Override

  21.                    public void onSuccess(FullHttpResponse result) {

  22.                        // testResult(result);

  23.                        janusHandleContext.setRestFullHttpResponse(result);

  24.                        // 跳转到下一个Filter

  25.                        filterContext.skipNextFilter(janusHandleContext);

  26.                    }

  27.                    @Override

  28.                    public void onError(Throwable e) {

  29.                        //省略

  30.                    }

  31.                    @Override

  32.                    public void onTimeout() {

  33.                        //省略

  34.                    }

  35.                }, outBoundRequest);

  36.    }

  37.    //其余省略

  38. }

三.自研网关Filter链的设计

一层接口,一层 abstract类, 一层基于Event观察者模式的抽象类,一个基于观察者模式的接口, 自定义Filter根据需要继承处理,在这里不做过多介绍。

四.自研网关纳管Spring Cloud的结果

4.1 自研网关注册到Eureka Server上

把自研网关注册到Eureka Server上,用于获取服务列表,如下图所示。

上图中有两个服务提供者1,2,以及一个网关Server。

4.2 无缝支持REST转REST的GET和POST的转发

自定义LB,基于Netty Channel Pool实现了GET,POST的协议适配和异步转发,如下所示。

http://localhost:8081/,是本地网关Server的主机和端口。

五.参考文章

Netty系列之Netty线程模型

http://mp.weixin.qq.com/s?__biz=MzI2ODYxMjU4MQ==&mid=2247483787&idx=1&sn=2f5a4fba83efece7f07c7c5e4643a30e&chksm=eaeda701dd9a2e179a5c699fb0fc71376fb3f1d3cf26f56872c14533672e1485a75a171839b1&mpshare=1&scene=1&srcid=0820vd0sb6kp3oncNeLZQz74#rd

自研网关纳管Spring Cloud(一)的更多相关文章

  1. API网关服务:Spring Cloud Zuul

    最近在学习Spring Cloud的知识,现将API网关服务:Spring Cloud Zuul 的相关知识笔记整理如下.[采用 oneNote格式排版]

  2. 第七章 API网关服务:Spring Cloud Zuul

    API网关是一个更为智能的应用服务器, 它的定义类似于面向对象设计模式中的Facade模式, 它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤.它除了要实现 ...

  3. 网关我选 Spring Cloud Gateway

    网关可提供请求路由与组合.协议转换.安全认证.服务鉴权.流量控制与日志监控等服务.可选的网关有不少,比如 Nginx.高性能网关 OpenResty.Linkerd 以及 Spring Cloud G ...

  4. Dubbo想要个网关怎么办?试试整合Spring Cloud Gateway

    一.背景 在微服务架构中 API网关 非常重要,网关作为全局流量入口并不单单是一个反向路由,更多的是把各个边缘服务(Web层)的各种共性需求抽取出来放在一个公共的"服务"(网关)中 ...

  5. springcloud(十五):Spring Cloud 终于按捺不住推出了自己的服务网关 Gateway

    Spring 官方最终还是按捺不住推出了自己的网关组件:Spring Cloud Gateway ,相比之前我们使用的 Zuul(1.x) 它有哪些优势呢?Zuul(1.x) 基于 Servlet,使 ...

  6. Spring Cloud Gateway服务网关

    原文:https://www.cnblogs.com/ityouknow/p/10141740.html Spring 官方最终还是按捺不住推出了自己的网关组件:Spring Cloud Gatewa ...

  7. 网关服务Spring Cloud Gateway(一)

    Spring 官方最终还是按捺不住推出了自己的网关组件:Spring Cloud Gateway ,相比之前我们使用的 Zuul(1.x) 它有哪些优势呢?Zuul(1.x) 基于 Servlet,使 ...

  8. 微服务网关哪家强?一文看懂Zuul, Nginx, Spring Cloud, Linkerd性能差异

      导语:API Gateway是实现微服务重要的组件之一.面对诸多的开源API Gateway,如何进行选择也是架构师需要关注的焦点.本文作者对几个较大的开源API Gateway进行了压力测试,对 ...

  9. 微服务网关实战——Spring Cloud Gateway

    导读 作为Netflix Zuul的替代者,Spring Cloud Gateway是一款非常实用的微服务网关,在Spring Cloud微服务架构体系中发挥非常大的作用.本文对Spring Clou ...

随机推荐

  1. Netflix Recommendations

    by Xavier Amatriain and Justin Basilico (Personalization Science and Engineering) In part one of thi ...

  2. Git与远程reposiory的相关命令

    问题1:Git如何同步远程repository的分支(branch) 某天,小C同学问我,为啥VV.git仓库里面本来已经删除了branchA这个分支,但是我的mirror中还是有这个分支呢? 分析: ...

  3. 服务端技术进阶(二)JBoss和tomcat的区别

    JBoss和tomcat的区别 注意JBoss和tomcat是不一样,JBoss是一个可伸缩的服务器平台,当你的EJB程序编制完成后,如果访问量增加,只要通过增加服务器硬件就可以实现多台服务器同时运算 ...

  4. LeetCode之“数学”:Happy Number

    题目链接 题目要求: Write an algorithm to determine if a number is "happy". A happy number is a num ...

  5. Android 高逼格纯代码实现类似微信钱包带分割线的GridView

    前言    原文地址:http://blog.csdn.net/sk719887916/article/details/40348837: Tamic 通过上两篇关于自定view的文章,在自定义vie ...

  6. BP 神经网络

    BP(Back Propagation)网络是1986年由Rumelhart和McCelland为首的科学家小组提出,是一种按误差逆传播算法训练的多层前馈网络,是目前应用最广泛的神经网络模型之一.BP ...

  7. Nginx使用图片处理模块

    Nginx可以编写很多额外的模块,这里我们需要按照能够通过URL响应返回缩放且含图片水印功能的模块. 1.安装一些使用过程中会用到的工具 yum install libgd2-devel yum in ...

  8. SQLSERVER 执行过的语句查询

    total_worker_time AS [总消耗CPU 时间(ms)], execution_count [运行次数], qs.total_worker_time AS [平均消耗CPU 时间(ms ...

  9. ftp实现普通账号和vip账号限速

    ftp工作流程: ftp回话包含了两个通道,控制通道和数据通道,ftp的工作有两种模式,一种是主动模式,一种是被动模式,以ftpserver为参照物,主动模式,服务器主动连接客户端传输,被动模式,等待 ...

  10. django-debug-toolbar的配置以及使用

    django-debug-toolbar django,web开中,用django-debug-toolbar来调试请求的接口,无疑是完美至极.   可能本人,见识博浅,才说完美至极, 大神,表喷,抱 ...