基于NIO实现公司API远程调用技术探讨

      前 言

      目前公司一些工具会远程调用一些API,这些API调用有两个比较显著特点。     

                  1、消耗时间比较长,无论是报表调用的API,还是 backend ws API 单次调用平均达到20 s 左右。 

                  2、返回来的数据有时也会比较大,我见过单次调用返回的数据有可能有3MB 左右。

   基于上述特点,很显然有优化的空间,当然这个优化的方法很多,比如目前公司在服务器端采用诸如 k8s 这种按需扩容方案,是能够大大提升整个远程API的调用效率,又比如对服务器端代码进行优化等等。

   由于本人日常工作中只接触API 在客户端的调用,所以本文探讨会在以下前提下进行探讨:

     1、基于不改变API服务器代码。

     2、只讨论客户端发出请求,服务器端返回数据这一段,对于客户端后续获取数据的处理不在本文探讨范围。

     3、客户端远程调用 100次 API。

     4、服务器端对于每次调用响应时间为 10s

     5、目前API 服务器,我是使用自己搭建的http 服务器来测试。

   

 

      传统调用模式

           单线程调用模式:

                  基于NIO实现公司API远程调用技术探讨

    public static void singleThread() {
        try {
            Date startDate = new Date();
            
            for(int i=0;i<100;i++) { 
                String apiUrl = "http://127.0.0.1:6668/";
                HttpClient1.sendPost(apiUrl, "");
            }
            
            Date endDate = new Date();
            long interval = (endDate.getTime() - startDate.getTime())/1000;
            SimpleDateFormat timeFomat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:ms"); 
            System.out.println("singleThread startDate="+timeFomat.format(startDate)+"  endDate="+timeFomat.format(endDate)+" diff="+interval+"秒");
            
        }catch(Exception ex) {
            ex.printStackTrace(System.out);
        }
    }

 

       单线程模式总耗时 = 100次 *10s = 1000s  约等于 17分钟。

          多线程调用模式(Future模式+线程池【3个线程】):

     基于NIO实现公司API远程调用技术探讨

 1 public static void multiThread() throws InterruptedException, ExecutionException {
 2         Date startDate = new Date();
 3         int count = 100;
 4         
 5         List<FutureTask<String>> lstFuture = new ArrayList<FutureTask<String>>();
 6         ExecutorService es = Executors.newFixedThreadPool(3);
 7         
 8         for (int i = 0; i < count; i++) {
 9             FutureTask<String> future = new FutureTask<String>(new Task());
10             es.submit(future);// 异步阻塞,调用后会立即返回【非阻塞】在这里体现
11             lstFuture.add(future);
12         }
13         
14         for(int i=0;i<count;i++) {
15             lstFuture.get(i).get();// 等到所有的结果返回,【同步】在这里体现
16         }
17         
18         Date endDate = new Date();
19          long interval = (endDate.getTime() - startDate.getTime())/1000;
20          SimpleDateFormat timeFomat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:ms"); 
21          System.out.println("multiThread Task total count="+count+ " startDate="+timeFomat.format(startDate)+"  endDate="+timeFomat.format(endDate)+" diff="+interval+"秒");
22          System.exit(-1);
23     }
24     
25   

               该模式由于启用了3个线程同时进行,所以是单线程总花费时间的三分之一,单线程总耗时1000s,  则多线程模式总耗时 = 1000s /3 = 333s 约等于 6分钟。

     

       NIO 调用模式

           以上是基于公司目前所广泛使用的远程API方式,接下来会基于NIO 方式来探讨客户端对远程API调用。为了方便较系统来探讨NIO 技术,会按照以下提纲进行编排。

                                   1、网络基础知识概览

                                   2、NIO 核心讲述

                                   3、基于HttpAsyncClients 实现API远程调用

                                   4、基于Netty 4 实现API远程调用

 

      网络基础知识概览

                      在常见的web 应用程序里面,都是基于不同的协议来实现,例如我们常用的邮件发送,是基于SMTP,FTP工具上传当然就是基于FTP协议,浏览器是基于HTTP协议等等。

               无论是哪种协议,最终都会被抽象成Socket 来进行网络操作,如下图所示。为什么要在这里强调Socket 是因为NIO 是基于Socket 一些列操作的抽象。

        基于NIO实现公司API远程调用技术探讨

                  Socket 常处理事件:

        OP_CONNECT 事件:主要是客户端连接服务端时候触发。

        OP_ACCEPT 事件:主要客户端发出连接请求后,与服务器端3次握手成功后,触发事件。这个时候往往意味着客户与服务器链路已经完成连接成功,可以进行下一步通信。

        OP_READ 事件:无论是客户端还是服务器端,只要Socket 检测到缓冲区有可以读写的数据就会触发。

        OP_WRITE 事件:主要是检测内核缓冲区是否已经满,如果没有满则代表缓冲区还可以继续写入数据,会触发该事件,否则不会触发。

                      基于NIO实现公司API远程调用技术探讨

      

       NIO核心讲述

                          NIO 有说是non-blocking IO 缩写,也有说是New IO 缩写,在这里不纠结这个问题,按照我的理解NIO 核心思想就是 "多路复用"。为了说清楚 多路复用 我们先看一个传统的HTTP 服务器架构实现。 

              基于NIO实现公司API远程调用技术探讨

      从图中看出传统的HTTP服务器模型,最大的特点就是每一个客户端(Socket)连接 在服务器端都开启一个新的线程(ps:在实际中用线程池实现),这种感觉就好像餐厅为每个新客户都配置一个服务员,

  实际是没有必要。因为服务员(类似线程)一般只会在客户 点菜、传菜、结账(类似Socket 事件)需要提供服务。其他大部分事件是不需要服务员。所以NIO 就是一个线程(服务员)可以同时负责成千上百个

  Socket(客户)。 所谓的“多路复用”就体现在这里   多路其实指多个客户端(Socket),复用 指的是重复使用相同的线程来负责多个客户端(Socket)事件监听。如下图所示,使用NIO模型,一个线程就可以

       负责成千上百个客户,相比较传统的HTTP模型一个线程负责一个Socket相比,是能极大提升服务器响应效率这也是各大电商所广泛采用的模型。

                                                                             基于NIO实现公司API远程调用技术探讨

 

       以下是非常经典简单的NIO模型核心代码实现

 1  SelectionKey acceptKey = ssc.register(selector, SelectionKey.OP_ACCEPT);// 注册OP_ACCEPT事件
 2         for (;;) { // 循环遍历各个Socket事件
 3             selector.select();
 4             Set readyKeys = selector.selectedKeys();
 5             Iterator i = readyKeys.iterator();
 6             long e=0;
 7             while (i.hasNext()) { // 捕获到某些Socket事件
 8                 SelectionKey sk = (SelectionKey) i.next();
 9                 i.remove();
10                 
11                 if (sk.isAcceptable()) { // 处理 OP_ACCEPT事件
12                     doAccept(sk);
13                 }
14                 else if (sk.isValid() && sk.isReadable()) {//处理 OP_READ事件
15                     if(!time_stat.containsKey(((SocketChannel)sk.channel()).socket()))
16                         time_stat.put(((SocketChannel)sk.channel()).socket(), 
17                             System.currentTimeMillis());
18                     doRead(sk);
19                 }
20                 else if (sk.isValid() && sk.isWritable()) {//处理 OP_WRITE事件
21                     doWrite(sk);
22                     e=System.currentTimeMillis();
23                     long b=time_stat.remove(((SocketChannel)sk.channel()).socket());
24                     System.out.println("spend:"+(e-b)+"ms");
25                 }
26             }

       以上为了讲述方便,只是列举了最简单的NIO实现模型,实际上诸如比较成熟的NIO实现框架,例如后面说的HttpAsyncClients 和 Netty 4.0 模型如下图所示,由于篇幅关系在这里就不对这个模型展开讲,

     后续如果有需要会补充上来。初步计划放在 《基于NIO实现公司API Http服务器》 这篇文章来说。

                基于NIO实现公司API远程调用技术探讨

 

       基于HttpAsyncClients 实现API远程调用

          异步阻塞模型,这个实现原理实际上是  Future模式+NIO+线程(ps:实际上是包含了两种线程,一种是专门用来处理OP_ACCEPT 事件,一种是专门用来处理OP_READ 和 OP_WRITE 事件)具体调用代码如下:

                      另外这种方式耗时为341秒左右。要比传统多出10秒左右,为什么用这种方式反而更慢? 这个统一在后面回答。

 1 public static void main(String[] args){
 2         int count = 100;
 3         Date startDate = new Date();
 4         try {
 5         RequestConfig requestConfig = RequestConfig.custom()
 6                 .build();
 7    
 8         IOReactorConfig ioReactorConfig = IOReactorConfig.custom().
 9                 setIoThreadCount(1)// 设定Woker 线程池数量,系统默认是CPU核数
10                 .setSoKeepAlive(true)
11                 .build();
12         
13         //设置连接池大小
14         ConnectingIOReactor ioReactor=null;
15         try {
16             ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);
17         } catch (IOReactorException e) {
18             e.printStackTrace();
19         }
20         PoolingNHttpClientConnectionManager connManager = new PoolingNHttpClientConnectionManager(ioReactor);
21         connManager.setMaxTotal(150);//最大连接数设置15
22         connManager.setDefaultMaxPerRoute(3);//每次默认发送3个连接,也就是一次性发送3个socket连接。
23 
24         final CloseableHttpAsyncClient client = HttpAsyncClients.custom().
25                 setConnectionManager(connManager)
26                 .setDefaultRequestConfig(requestConfig)
27                 .build();
28         client.start();
29         
30         List<Future<HttpResponse>> respList = new LinkedList<Future<HttpResponse>>();
31         final HttpGet request = new HttpGet("http://127.0.0.1:6668/");
32         request.setHeader("Connection", "close");
33         for (int i = 0; i < count; i++) {
34             respList.add(client.execute(request, null)); // 使用Future模式提交请求
35         }
36         
37         int index = 0;
38         BufferedInputStream bufferedInput = null;
39         byte[] buffer = new byte[1024];
40  
41         for (Future<HttpResponse> response : respList) {
42             index = index+1;
43             HttpResponse resp = response.get();// 等待任务完成
44             InputStream input = resp.getEntity().getContent();
45             bufferedInput = new BufferedInputStream(input);
46             int bytesRead = 0;
47             while ((bytesRead = bufferedInput.read(buffer)) != -1) {
48                 String chunk = new String(buffer, 0, bytesRead);
49                 System.out.println("index="+index+" "+chunk);
50             }
51         }
52  
53         client.close();
54         }catch(Exception ex) {
55             ex.printStackTrace(System.err);
56         }finally{
57             Date endDate = new Date();
58             long interval = (endDate.getTime() - startDate.getTime())/1000;
59             SimpleDateFormat timeFomat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:ms"); 
60             System.out.println("TestHttpClient Total count="+count+"  "+"startDate="+timeFomat.format(startDate)+"  endDate="+timeFomat.format(endDate)+" diff="+interval+"秒");
61         }
62     }

       基于Netty 4 实现API远程调用  

                            其实这种方式实现原理底层也是基于NIO,与HttpAsyncClients 相比,Netty 客户端这里允许只用1个线程,而HttpAsyncClients 至少需要2个线程,因为系统后台必须默认一个专门用来处理OP_ACCEPT线程。

               如果只是单纯实现客户端调用,然后远程服务器返回数据。我认为只需要1个线程就可以,所以才会提这种方式。这种方式耗时为331秒与传统的线程池模式差不多,具体实现核心代码如下:

基于NIO实现公司API远程调用技术探讨
 1 public class HttpClient {
 2     public void run() throws Exception {
 3         Date startDate = new Date();
 4         int count = 33;// 发送33次
 5         int perOfPath = 3;// 每次模拟发送3个连接
 6         EventLoopGroup group = new NioEventLoopGroup(1); // 设定一个线程作为轮询Socket事件
 7         try {
 8             Bootstrap client = new Bootstrap(); 
 9             client.group(group).channel(NioSocketChannel.class)
10                     .handler(new ChannelInitializer<SocketChannel>() {
11                         @Override
12                         protected void initChannel(SocketChannel socketChannel) throws Exception {
13                             socketChannel.pipeline().addLast(new HttpResponseDecoder()); 
14                             socketChannel.pipeline().addLast(new HttpRequestEncoder()); 
15                             socketChannel.pipeline().addLast(new HttpClientHandler()); 
16                         }
17                     });
18               
19             for (int i = 0; i < count; i++) {
20                 List<ChannelFuture> channelFutures = new ArrayList<ChannelFuture>();
21                 for(int j=0;j<perOfPath;j++) {
22                     channelFutures.add(client.connect(HostInfo.HOST_NAME, HostInfo.PORT));
23                 }
24                 
25                 for(int j=0;j<perOfPath;j++) {
26                     channelFutures.get(j).channel().closeFuture().sync();
27                 }
28             }
29         } finally {
30             Date endDate = new Date();
31             long interval = (endDate.getTime() - startDate.getTime())/1000;
32              SimpleDateFormat timeFomat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:ms"); 
33             System.out.println("Task total count="+count*perOfPath+ " startDate="+timeFomat.format(startDate)+"  endDate="+timeFomat.format(endDate)+" diff="+interval+"秒");
34             System.out.println("--group.shutdownGracefully");
35             group.shutdownGracefully();
36             System.exit(1);
37         }
38     }
View Code

             上面就是几种远程调用API方式,从目前来看用NIO 实现远程调用 与 传统的多线程调用耗时差不多,甚至基于HttpAsyncClients 比多线程模式还要慢10秒,这是因为我为了与多线程方式调用统一,目前多线程是用

了3个线程,也就是一次可以模拟发送3个请求。为了统一NIO 这种调用方式每次也只是模拟发送3个请求。实际上我们可以根据API 服务器处理能力来设定请求数,比如API服务器每次能同事处理100个连接,NIO 就可以

把每次请求变成100,或许有人就说了,使用多线程方式调用,也可以开100个线程来达到同样的效果。基于这个就得出今天的结论,使用NIO 方式调用的优点就是 用最小的线程数实现对API服务器最大程度调用。

         以上就是使用NIO对客户端调用的优化,相比较客户端优化,也许对API服务器端进行优化意义更大一些,毕竟对于整个系统而言服务器的QPS 或者说TPS 才是最重要的效率考量,对于服务器端也同样可以基于NIO

这种方式来优化,对于服务器返回数据量比较大问题,也有一系列的数据传输压缩方案,比如Google Protocol Buffer 。

 

 

                

      

                

 

      

 

 

 

 

    

 

 

 

   

           

 

 

 

       

基于NIO实现公司API远程调用技术探讨

上一篇:C#工作流WorkflowCore学习:Hello World


下一篇:01-window下ng安装使用