自研网关纳管Spring Cloud(一)

简介: 摘要: 本文主要从网关的需求,以及Spring Cloud Zuul的线程模型和源码瓶颈分析结合,目前最近一段时间自研网关中间件纳管Spring Cloud的经验汇总整理。 一.自研网关纳管Spring Cloud的原因 1.1 为什么要自研网关 1.网关配置实时生效,配置灰度,回滚等 2.网关的性能,特别是防刷,限流,WAF等 3.动态Filter ,目前Zuul可以做到动态Filter,Filter配置下发,实时动态Filter 4.对网关的监控,告警,流量调拨,网关集群。

摘要: 本文主要从网关的需求,以及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.  

  3. spring.application.name=janus-server

  4.  

  5.  

  6. 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.  

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

  6.  

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

  8.    public static int httpPort = 8081;

  9.  

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

  11.  

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

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

  14.  

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

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

  17.  

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

  19.        // 解析启动参数

  20.        parseArgs(args);

  21.  

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

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

  24.        JanusBootStrap.initGateway();

  25.  

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

  27.        final JanusNettyServer gatewayServer = new JanusNettyServer();

  28.        // ②启动HTTP容器

  29.        gatewayServer.startServer(httpPort);

  30.  

  31.    }

  32. }

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.  

  3.        // http请求ChannelInbound

  4.        final HttpInboundHandler httpInboundHandler = new HttpInboundHandler();

  5.  

  6.        ServerBootstrap insecure = new ServerBootstrap();

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

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

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

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

  11.                /**

  12.                 * SO_KEEPALIVE

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

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

  15.                 */

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

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

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

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

  20.                    @Override

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

  22.                        ChannelPipeline pipeline = ch.pipeline();

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

  24.                        // keepalive_timeout 的支持

  25.                        pipeline.addLast(

  26.                                new IdleStateHandler(ProperityConfig.keepAliveTimeout, 0,

  27.                                        0, TimeUnit.MILLISECONDS));

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

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

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

  31.                        // HttpChunk,....HttpChunkTrailer

  32.                        pipeline.addLast(new HttpRequestDecoder(

  33.                                ProperityConfig.maxInitialLineLength,

  34.                                ProperityConfig.maxHeaderSize, 8192,

  35.                                ProperityConfig.validateHeaders));

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

  37.                        pipeline.addLast(new HttpObjectAggregator(

  38.                                ProperityConfig.httpAggregatorMaxLength));

  39.  

  40.                        // gzip的支持

  41.                        if (ProperityConfig.gzip) {

  42.                            pipeline.addLast(new JanusHttpContentCompressor(

  43.                                    ProperityConfig.gzipLevel,

  44.                                    ProperityConfig.gzipMinLength));

  45.                        }

  46.  

  47.                        pipeline.addLast(httpInboundHandler);

  48.                    }

  49.                });

  50.  

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

  52.  

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

  54.  

  55.        /**

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

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

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

  59.         **/

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

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

  62.  

  63. }

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

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

  1. public class RestInvokerFilter extends AbstractFilter {

  2.  

  3.  

  4.    @Override

  5.    public void run(final AbstractFilterContext filterContext,

  6.            final JanusHandleContext janusHandleContext) throws Exception {

  7.  

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

  9.        ServiceInstance serviceInstance = SpringCloudHelper.getServiceInstanceByLB(

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

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

  12.        FullHttpRequest outBoundRequest = getOutBoundHttpRequest(janusHandleContext);

  13.  

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

  15.        AsyncHttpRequest.builder()

  16.                .remoteAddress(

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

  18.                .sessionContext(janusHandleContext)

  19.                /**

  20.                 * connection holding 500ms

  21.                 */

  22.                .holdingTimeout(ProperityConfig.janusHttpPoolOauthMaxHolding).build()

  23.                .execute(new SimpleHttpCallback(janusHandleContext) {

  24.                    @Override

  25.                    public void onSuccess(FullHttpResponse result) {

  26.                        // testResult(result);

  27.                        janusHandleContext.setRestFullHttpResponse(result);

  28.                        // 跳转到下一个Filter

  29.                        filterContext.skipNextFilter(janusHandleContext);

  30.                    }

  31.  

  32.                    @Override

  33.                    public void onError(Throwable e) {

  34.                        //省略

  35.                    }

  36.  

  37.                    @Override

  38.                    public void onTimeout() {

  39.                        //省略

  40.                    }

  41.                }, outBoundRequest);

  42.  

  43.    }

  44.  

  45.    //其余省略

  46. }

三.自研网关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

 

相关实践学习
部署高可用架构
本场景主要介绍如何使用云服务器ECS、负载均衡SLB、云数据库RDS和数据传输服务产品来部署多可用区高可用架构。
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
30天前
|
SpringCloudAlibaba Java 网络架构
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(二)Rest微服务工程搭建
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(二)Rest微服务工程搭建
46 0
|
29天前
|
负载均衡 Java API
Spring Cloud 面试题及答案整理,最新面试题
Spring Cloud 面试题及答案整理,最新面试题
130 1
|
29天前
|
Java Nacos Sentinel
Spring Cloud Alibaba 面试题及答案整理,最新面试题
Spring Cloud Alibaba 面试题及答案整理,最新面试题
136 0
|
30天前
|
SpringCloudAlibaba Java 持续交付
【构建一套Spring Cloud项目的大概步骤】&【Springcloud Alibaba微服务分布式架构学习资料】
【构建一套Spring Cloud项目的大概步骤】&【Springcloud Alibaba微服务分布式架构学习资料】
130 0
|
30天前
|
SpringCloudAlibaba Java 网络架构
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(七)Spring Cloud Gateway服务网关
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(七)Spring Cloud Gateway服务网关
80 0
|
1月前
|
消息中间件 JSON Java
Spring Boot、Spring Cloud与Spring Cloud Alibaba版本对应关系
Spring Boot、Spring Cloud与Spring Cloud Alibaba版本对应关系
394 0
|
2天前
|
负载均衡 Java 开发者
细解微服务架构实践:如何使用Spring Cloud进行Java微服务治理
【4月更文挑战第17天】Spring Cloud是Java微服务治理的首选框架,整合了Eureka(服务发现)、Ribbon(客户端负载均衡)、Hystrix(熔断器)、Zuul(API网关)和Config Server(配置中心)。通过Eureka实现服务注册与发现,Ribbon提供负载均衡,Hystrix实现熔断保护,Zuul作为API网关,Config Server集中管理配置。理解并运用Spring Cloud进行微服务治理是现代Java开发者的关键技能。
|
2天前
|
Java API 对象存储
对象存储OSS产品常见问题之使用Spring Cloud Alibaba情况下文档添加水印如何解决
对象存储OSS是基于互联网的数据存储服务模式,让用户可以安全、可靠地存储大量非结构化数据,如图片、音频、视频、文档等任意类型文件,并通过简单的基于HTTP/HTTPS协议的RESTful API接口进行访问和管理。本帖梳理了用户在实际使用中可能遇到的各种常见问题,涵盖了基础操作、性能优化、安全设置、费用管理、数据备份与恢复、跨区域同步、API接口调用等多个方面。
19 2
|
17天前
|
负载均衡 网络协议 Java
构建高效可扩展的微服务架构:利用Spring Cloud实现服务发现与负载均衡
本文将探讨如何利用Spring Cloud技术实现微服务架构中的服务发现与负载均衡,通过注册中心来管理服务的注册与发现,并通过负载均衡策略实现请求的分发,从而构建高效可扩展的微服务系统。
|
30天前
|
SpringCloudAlibaba 负载均衡 Java
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(目录大纲)
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(目录大纲)
61 1

热门文章

最新文章