• Netty 原理与 API 网关 lcl


    一、再谈谈什么是高性能

    (一)性能指标与演示

      首先明确一下,高性能涵盖:高并发用户、高吞吐量、低延迟、容量四个方面,高并发用户是指可以承载海量的并发用户,例如十万个、百万个用户同时连接和并发访问,不会造成系统崩溃;吞吐量就是我们常说的TPS、QPS,即每秒的查询数和事务数;低延时指的是请求的响应速度;容量是指能容纳的量,例如并发量、用户量、磁盘容量、带宽、内存容量、CPU性能等等

      那如何来确定是否是高性能呢,就需要进行压力测试了,例如下面三段代码:

      1、BIO单线程模式

    public class HttpServer01 {
        public static void main(String[] args) throws IOException {
            ServerSocket serverSocket = new ServerSocket(8080);
            while (true){
                try{
                    Socket socket = serverSocket.accept();
                    service(socket);
                }catch (Exception e){
                    e.printStackTrace();
                }
    
            }
        }
    
        private static void service(Socket socket) {
            String body = "hello lcl-nio-001";
            try{
                PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
                printWriter.println("HTTP/1.1 200 OK");
                printWriter.println("Content-Type:text/html;charset=utf-8");
                printWriter.println("Content-Length:" + body.getBytes().length);
                printWriter.println();
                printWriter.write(body);
                printWriter.flush();
                printWriter.close();
                socket.close();
            }catch (Exception e){
    
            }
        }

      2、Netty的NIO模式

    public class MyHttpHandler extends ChannelInboundHandlerAdapter {
    
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) {
            ctx.flush();
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            FullHttpRequest fullHttpRequest = (FullHttpRequest) msg;
            String uri = fullHttpRequest.getUri();
            if("/test".equals(uri)){
                handlerTest(ctx, fullHttpRequest, "hello lcl");
            }else {
                handlerTest(ctx, fullHttpRequest, "hello other");
            }
            ctx.fireChannelRead(msg);
        }
    
        private void handlerTest(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, String body) {
            FullHttpResponse response = null;
            String value = body;
    
            try {
                response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(value.getBytes("UTF-8")));
                response.headers().set("Content-Type", "application/json");
                response.headers().set("Content-Length", response.content().readableBytes());
            } catch (UnsupportedEncodingException e) {
                System.out.printf("处理异常" + e.getMessage());
                response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
            }finally {
                if(fullHttpRequest != null){
                    if(!HttpUtil.isKeepAlive(fullHttpRequest)){
                        ctx.write(response).addListener(ChannelFutureListener.CLOSE);
                    }else {
                        response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderNames.KEEP_ALIVE);
                        ctx.write(response);
                    }
                }
            }
        }
    }
    
    
    ============== 类分割线
    
    public class MyHttpInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            ChannelPipeline pipeline = socketChannel.pipeline();
            pipeline.addLast(new HttpServerCodec());
            pipeline.addLast(new HttpObjectAggregator(1024*1024));
            pipeline.addLast(new MyHttpHandler());
        }
    }
    
    
    ============== 类分割线
    
    public class MyNettyHttpServer {
        public static void main(String[] args) {
            int port = 8888;
    
            EventLoopGroup bossGroup = new NioEventLoopGroup(2);
            EventLoopGroup workerGroup = new NioEventLoopGroup(16);
    
    
            try {
                ServerBootstrap serverBootstrap = new ServerBootstrap();
    
                serverBootstrap.option(ChannelOption.SO_BACKLOG, 128)
                        .childOption(ChannelOption.TCP_NODELAY, true)
                        .childOption(ChannelOption.SO_KEEPALIVE, true)
                        .childOption(ChannelOption.SO_REUSEADDR, true)
                        .childOption(ChannelOption.SO_RCVBUF, 21*1024)
                        .childOption(ChannelOption.SO_SNDBUF, 32*1024)
                        .childOption(EpollChannelOption.SO_REUSEPORT, true)
                        .childOption(ChannelOption.SO_KEEPALIVE, true)
                        .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
                serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                        .handler(new LoggingHandler(LogLevel.INFO))
                        .childHandler(new MyHttpInitializer());
    
                Channel channel = serverBootstrap.bind(port).channel();
                System.out.println("开启netty http服务器,监听地址和端口为 http://127.0.0.1:" + port + '/');
                channel.closeFuture().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
        }
    }

      然后使用压测工具进行压测:

    conglongli@localhost ~ % wrk -c40 -d30s --latency http://localhost:8080
    Running 30s test @ http://localhost:8080
      2 threads and 40 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     0.93ms    2.66ms 198.89ms   99.59%
        Req/Sec     0.91k     1.12k    3.84k    76.29%
      Latency Distribution
         50%  844.00us
         75%    1.05ms
         90%    1.30ms
         99%    2.32ms
      21989 requests in 30.10s, 8.35MB read
      Socket errors: connect 39, read 176095, write 10, timeout 13
    Requests/sec:    730.62
    Transfer/sec:    284.21KB
    conglongli@localhost ~ % wrk -c40 -d30s --latency http://localhost:8888
    Running 30s test @ http://localhost:8888
      2 threads and 40 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     5.16ms   32.86ms 454.95ms   97.13%
        Req/Sec    50.05k    10.28k   58.73k    90.64%
      Latency Distribution
         50%  347.00us
         75%  367.00us
         90%  640.00us
         99%  148.79ms
      2732276 requests in 30.10s, 276.20MB read
      Socket errors: connect 0, read 0, write 0, timeout 40
    Requests/sec:  90777.41
    Transfer/sec:      9.18MB

      首先说一下压测输出的内容:

        -c:实际上就是在压测时使用的连接数,对应的是高并发用户指标;

        Requests/sec:吞吐量,例如上面使用Netty的QPS为 90777.41,对应的是吞吐量指标;

        Latency 延迟时间,例如上面netty的案例压测中,50%的请求在347us中返回等,用来衡量具体的请求中位数,这种衡量一般使用TP来表示,例如这里的 TP99,即为148.79ms,其对应的是延迟指标;

        Thread Stats:是对吞吐量和延迟的平均值(Avg)、标准偏差(Stdev)、最大值(Max)、正负一个标准差占比(+/-) Stdev的统计,一般主要关注Avg和Max。Stdev如果太大说明样本本身离散程度比较高,有可能系统性能波动很大。

      另外,从上面对于BIO和NIO的压测结果看,NIO的吞吐量和延迟要比BIO好的多。

    (二)高性能系统优缺点和应对策略

      高并发的优势:系统处理的更快、吞吐量更大、能承受更大的压力、能够容纳更大的压力和更大的请求量、客户体验更好,线上部署的机器也可以更少。

      高性能缺点:为了实现高性能,系统的复杂度会更高,甚至要高十倍以上;建设和维护成本也非常高;如果线上出现问题带来的破坏性也是成倍增加。

      应对策略:对于系统稳定性有一个专门的学科,就是混沌工程,基于混沌工程的思想,就可以从容量、爆炸半径、工程积累和改进这三个方面进行应对。

        容量,要考虑目前系统的容量和目标容量,例如如果提前预知后面某一个时间段的TPS会增大,那么就需要提前加机器,然后进行压测。

        爆炸半径:需要提前了解系统的关系,知道出了问题影响范围,那么就要求我们尽量少的重启或者上线,另外要假设这次上线会出现问题,影响范围是什么。

        工程积累和改进:即问题复盘,可以从问题描述、问题现象、造成原因、解决过程、后续优化等几个温度进行

    二、Netty 如何实现高性能

      关于Netty高性能的体现,就是Reactor模式,分为单Reactor单线程、单Reactor多线程、主从Reactor多线程三种,具体的原理可以参考之前的文章 Netty高性能架构设计

      这里主要说几个之前没有理清的概念。

      1、NioEventLoopGroup

        NioEventLoopGroup可以看做是一组EventLoop,一个EventLoop其实是一个线程数为一的线程池,同时EventLoop中还有一个selector,一个selector可以绑定一个或多个Channel,EventLoop线程循环selector中的Channel,如果有就绪的,就交给业务handler处理。

      2、BossEventLoopGroup和WorkerEventLoopGroup的关系

        单Reactor单线程模型:Reactor维护的职责是负责客户端连接事件、事件状态的维护,如果IO完成,则分发请求给Handler处理,这样实际上作为处理网络事件、事件分发、业务处理用的是同一个线程,之间会有相互影响

        单Reactor多线程模型:Reactor维护的职责是负责客户端连接事件、事件状态的维护,如果IO完成,则分发请求给Handler线程池处理,这样将IO操作和业务操作做了隔离,如果业务线程不够,可以单独调整handler线程池参数即可。

        主从Reactor多线程模型:单Reactor单线程的Reactor模型,Reactor维护的职责是负责客户端连接事件、事件状态的维护,就绪事件的分发,其实这是两部分内容,一部分是连接事件和事件状态维护,另一部分是当IO处理完成后,需要进行业务处理时的分发操作,这样对于Reactor来说仍然较重,因此主从Reactor模型将连接事件&状态维护和网络分发做了分离,主Reactor负责连接事件和状态维护,子Reactor负责就绪事件分发给Handler线程池。

      3、Netty对于三种模式的具体实现

        如果是单Reactor单线程,直接定义NioEventLoopGroup时,传入参数 1,则直接创建一个handler线程。

        如果是单Reactor多线程,则可以将参数设置为大于 1,则会创建多个handler,如果不传参数,则会取系统配置,如果系统没有配置,则默认取可运行CPU数量的2倍。

        如果是主从Reactor多线程,需要同时设置BossGroup和WorkerGroup

            

      4、Netty运行原理

        Netty的启动和处理流程如下图所示,在启动时,首先创建bossgroup和workergroup并将其绑定到ServerBootStrap上,同时设置了监听端口,当客户端的请求时,bossgroup接收请求并将请求分配给workergroup。

             

        上面是一个大致的流程,实际上在启动时,设置了workergroup中Eventloop的数量,每一个EventLoop中都有一个TaskQueue和DelayTaskqueue,同时还有一个或多个Channel,每一Channel上都帮定了一个ChannelPipeine,当有事件从BossGroup分发到WorkerGroup中时,wokergroup会将事件放入Channel中,同时Eventloop会轮询所有的Channel,当事件IO处理完毕后,则将请求通过ChannelPipeline传递给业务处理线程。

            

    三、Netty 网络程序优化

      1、粘包拆包

        这个在我之前的文章中有过说明:Netty编解码器&粘包拆包

      2、Nagle与TCP_NODELAY

        在发送请求时,如果发送的数据量很小,但是发送了很多,那么在网络上就会有很多小网络包数据在传输,就有可能会引起网络拥堵;但是如果等网络缓冲区满了再传输请求,就可能会导致请求的延迟变高。

        为了平衡网络拥堵和延迟,TCP协议使用了Nagle算法,即在网络通信时,等待网络缓冲区满了再进行传输,但是如果在一定时间范围内落网缓冲区还没有满,例如200ms,同样也将数据进行传输。这这样的处理中,既保证了网络传输中不会有大量的小数据包导致网络拥堵,又不会一直等待导致很大的延迟。

        如果我们的系统对延迟也别敏感,同时并发又不是很大,那么就可以将TCP_NODELAY打开,那么Nagle算法就会被禁用,这样只要网络缓冲区有数据就会发送,可以提高响应时间。

        上面说的是操作系统的网络缓冲区,也就是网卡的缓冲区,实际上在发送数据时,TCP底层仍然会将网卡缓冲区中的数据进行分块发送,每一块数据的大小使用MTU表示,Maxitum Transmission Unit 最大传输单元,其值为 1500 Byte,但是在TCP协议中,由于还需要传输TCP协议头和IP协议头的相关信息,因此最大可传输的数据MSS大小为 1460 Byte,MSS:Maxitum Segment Size,最大分段大小,为 MTU-20(IP)-20(TCP),如果发送数据的字节数大于1460Byte,那么TCP仍然会将其拆分发送,在接收端会将数据做合并。

        基于MTU和MSS,如果发送的数据大于1460Byte时,在压测时性能会比数据小于1460Byte的性能要差。

      3、连接优化

        首先看下TCP的三次握手和四次挥手,流程如下图所示,如果要看详细的解读,可以看下这篇文章 《两张动图-彻底明白TCP的三次握手与四次挥手》

            

         在TCP的四次挥手中,如果服务端断开连接,客户端响应后,会等待两个时钟周期,然后才会将客户端的状态改为关闭,在Linux操作系统中,一个时钟周期为一分钟,在Windows中为两分钟,这个时间还是比较长的,那么客户端的相关资源就会被这些半死不活的连接占用,例如在进行压测时,如果第一次压测完后直接进行第二次压测,有可能会失败,例如出现连接不够用的情况。

        基于上述分析,在做高性能的压测分析时,做完一次压测后,可以等待几分钟后再做第二次压测,同时也可以调整服务器的配置,将时间窗口降低;甚至在使用NIO时,可以直接复用这些连接。

      4、Netty优化

      (1)不要阻塞 EventLoop

        EventLoop是单线程的,如果阻塞EventLoop就会导致其他请求不能处理,如果有比较耗时间的处理,可以新创建一个线程或者使用线程池进行处理,不要在EventLoop中处理。

      (2)系统参数优化

        文件描述符:在Linux操作系统中,一切皆是文件,如果开了大量的连接,就会占用大量的文件描述符,可以使用 ulimit -a 来查看资源描述符的限制,可以将其调大

        时钟周期:另外上面连接优化中说到可以更改时钟周期的大小,在Linux中可以调整 /proc/sys/net/ipv4/tcp_fin_timeout,,在Windows中可以调整路由注册表中的 TcpTimedWaitDelay

      (3)缓冲区优化

        网络缓冲区大小:即上面描述的Nagle算法中使用的缓冲区,包括接收缓冲区SO_RCVBUF和发送缓冲区SO_SNDBUF,这两个可以在启动Netty时直接设置

        半连接数量(SO_BACKLOG):这个指的是在TCP三次握手时,还在有建立连接的情况下,可以创建多少个连接,也就是方服务处理不过来时,可以有多少个连接进行等待,如果等待的连接超过了该数值,则会报错。

        复用TIME-WAIT连接:即上面提到处于TIME-WAIT状态的连接虽然不可用,但是仍然会占用很多的资源,如果使用REUSEXXX配置,则可以复用这些连接,这种处理比重新创建连接更经济实惠。其也可以通过在Netty启动时进行配置。

        下面是一个Netty启动时的样例代码,同时设置了接收缓冲区、发送缓冲区、backlog缓冲区和复用连接的配置

                serverBootstrap.option(ChannelOption.SO_BACKLOG, 128)
                        .childOption(ChannelOption.TCP_NODELAY, true)
                        .childOption(ChannelOption.SO_KEEPALIVE, true)
                        .childOption(ChannelOption.SO_REUSEADDR, true)
                        .childOption(ChannelOption.SO_RCVBUF, 21*1024)
                        .childOption(ChannelOption.SO_SNDBUF, 32*1024)
                        .childOption(EpollChannelOption.SO_REUSEPORT, true)
                        .childOption(ChannelOption.SO_KEEPALIVE, true)
                        .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

      (4)心跳周期优化

        这个没什么可说的,就是使用心跳机制与断线重连来保证网络通信是好的,提高可用性。可以在Netty启动时设置参数SO_KEEPALIVE,具体示例如上面代码所示。

      (5)内存与 ByteBuffer 优化

        Netty内存可以分为直接内存DirectBuffer 与 堆内存HeapBuffer,其中直接内存不会受JVM GC影响,效率会更高,因此使用Netty时,一般会使用堆外内存,可以在Netty启动时设置重用缓冲池参数 ALLOCATOR具体示例如上面代码所示。

      (6)其他优化

        ioRatio: IO 消耗的CPU资源和业务处理消耗的CPU资源比例,默认为50:50,这一块也是可以调整的。

        Watermark:高低水位,用于配置是否可写。业务数据不可能无限制向Netty缓冲区写入数据,TCP缓冲区也不可能无限制写入数据.Netty通过高低水位控制向Netty缓冲区写入数据的多少,它的大体流程就是向Netty缓冲区写入数据的时候,会判断写入的数据总量是否超过了设置的高水位值,如果超过了就设置通道(Channel)不可写状态。当Netty缓冲区中的数据写入到TCP缓冲区之后,Netty缓冲区的数据量变少,当低于低水位值的时候,就设置通过(Channel)可写状态。

          netty默认设置的高水位为64KB,低水位为32KB,设置好了高低水位参数,需要自己在写代码的时候,判断 channel.isWritable() ,否则仍然会继续写入。

    bootstrap.childOption(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 32 * 1024);
    bootstrap.childOption(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 8 * 1024);

        TrafficShaping:Netty自带了 流量整形 的流控,当网络请求特别大时,可以使用内存将请求缓存,Netty使用一定的速率处理请求,这样可以保证系统的稳定,不会被突发的大量请求冲垮。Netty提供了GlobalTrafficShapingHandler、ChannelTrafficShapingHandler、GlobalChannelTrafficShapingHandler三个类来实现流量整形,他们都是AbstractTrafficShapingHandler抽象类的实现类,可以在组装ChannelPipeline时,将该Handler组装到ChannelPipeline中。

    四、典型应用:API 网关

      API网关的四大职能:

        请求接入:作为所有API接口服务请求的接入点,例如有成千上万的连接,或者是同一时间有十万的连接,API网关需要有这样的能力,将所有的请求都Hold住,因此这一块一般都需要类似Netty的NIO能力。

        业务聚合:作为所有后端业务服务的聚合点

        中介策略:实现安全、验证、路由、过滤、流控等策略

        统一管理:对所有API服务和策略进行统一管理

      从网关的职能上来看,可以分为流量网关和业务网关:

        流量网关:流量网关是在整个集群的最前端,做一些通用的处理,和具体的业务无关,主要关注稳定与安全,例如全局性流控、日志统计、防止SQL注入、防止WEB攻击、屏蔽工具扫描、黑白IP名单、证书或加解密处理等。因此流量网关有部分LoadBalance的能力、有部分安全领域WAF网络应用防火墙的能力,流量网关关注的是整个微服务集群,因此流量会非常大,对其性能也有很高的要求。

        业务网关:主要是提供更好的服务,例如服务级别流控、服务降级与熔断、路由与负载以及灰度策略、服务过滤聚合发现、权限验证与用户等级策略、业务规则与权限校验、多级缓存策略等

      网关的主要流程都是先对请求进行拦截过滤,然后对请求进行路由转发,获取到响应结果后,对结果进行拦截过滤,最终返回结果。

      在Java体系中,常见的网关有 Zuul、Zuul2、Spring Cloud Gateway、OpenRestry、Kong等,其中OpenRestry和Kong都是基于Nginx + lua脚本实现的,性能非常好,适合做流量网关,而Zuul2和SpringCloud Gateway是基于Netty实现的,扩展性好,可以做二次开发,适合做业务网关。

      如果想要自己动手实现 API 网关,那就是上面提到的,先对请求进行拦截过滤,然后对请求进行路由转发,获取到响应结果后,对结果进行拦截过滤,最终返回结果。

      手写的网关可以参考我的另外一篇文章 使用Netty手撸一个简单网关

      

  • 相关阅读:
    吹气球
    Leetcode 235
    什么是BPMN网关?
    BPMN中的任务和活动之间有什么区别?
    两款流程图设计推荐
    Activiti7.1, jBPM7.25, Camunda, Flowable6.3技术组成对比
    Flowable与activiti对比
    机器学习中的数学
    WopiServerTutorial
    如何整合Office Web Apps至自己开发的系统(二)
  • 原文地址:https://www.cnblogs.com/liconglong/p/16299686.html
Copyright © 2020-2023  润新知