美文网首页性能调优系列
从NIO原理到性能优化

从NIO原理到性能优化

作者: 默写流年 | 来源:发表于2020-05-27 12:47 被阅读0次

在这个互联网的世界里,我们每天都会跟io打交道,linux系统中将io分成三类:网络IO、磁盘IO、内存IO。那么在java中io 模型到底有哪些?每种io模型的应用场景是怎么样的?这些IO模型和性能优化又有什么关系呢?这些将会是我们这篇文章要探讨的内容,也是性能调优系列的开篇。
ps:本篇文章只是简单由io模型引出性能优化的话题,以及在性能优化中常用的套路。但是本文不涉及对性能优化中引入的新组件或者新架构的所带来的问题分析。本文只是起到抛砖引玉的功能,具体细节我们会在后续章节中陆续解读。
本文大致分为以下几个板块

  1. 前言
  2. java io 模型
    2.1 BIO
    2.2 NIO
    2.3 AIO
  3. tomcat 的三种模式
    3.1 不同模式的性能差异
  4. 互联网公司的架构演变
    4.1 LAMP
    4.2 应用服务与数据服务分离
    4.3 使用缓存改善性能
    4.4 应用服务器集群
    4.5 数据库读写分离
    4.6 反向代理和CDN加速
    4.7 使用分布式文件系统与分布式数据库系统
    4.8 使用nosql与搜索引擎
    4.9 业务拆分
    4.10 服务化
    4.11 大数据技术、监控、日志分析系统
  5. 总结

1. 前言

性能优化是一个庞大的话题,从前端到后端,小到tcp数据包到一行代码,再到业务中间件,再到工程架构,每个部分都有不同到优化手段。而这些优化手段及其实现原理,每个都可以单独展作为一个专题来讲。同样要理解为什么这样优化,首先要了解现有架构的成因,即要理解互联网架构到演变史。这也是本文将互联网架构演变史作为本文的核心来将的原因。本文先是讲解了java io 模型,再由其引出了tomat的三种模式,再到互联网架构到演变史。为后续的性能调优专题做一个铺垫。


2. java io 模型

说起io,每天我们都在面对各种io,概览如下:

IO种类 场景 java中到应用
内存IO 从内存中读取数据,将数据写入内存 线程从内存中将数据读取到工作空间,将值在工作空间完成更改后,将值由工作空间刷新到内存中(jmm)
磁盘IO 读取磁盘文件 ,写文件到磁盘 java-io,write,read(java.io)
网络IO 网络数据的读写和传输 tcp/udp的抽象api即socket 通信 (java.net)

2.1 基本概念

以上是linux中的io划分,那么在java 中io 模型是怎么样的呢,在聊java的io模型前,我们先要了解以下概念:

阻塞、非阻塞、同步、异步

  • 阻塞与非阻塞
    阻塞与非阻塞是针对进程在访问数据的时候,根据io操作的就绪状态来采取不同的方式(等待or 做其他事情)
    阻塞:以read为例,当调用read方法的时候,会发起系统调用,由内核将数据从内核空间拷贝到系统空间。此时用户线程挂起,直到数据复制完成或者出现异常,这种就是阻塞。
    非阻塞:如果没有东西可读,或者不可写,读写函数马上返回,而不会等待。这样线程可以继续处理其他事情。
  • 同步与异步
    同步和异步是针对应用程序和内核的交互而言
    同步:A调用B,B处理直到获得结果才返回给A,此时A需要一直等待或者确认结果是否返回,然后再继续往下执行。
    异步:A调用B,B处理直到获得结果才返回给A,A无需等待B返回,而是可以继续做其他事情。B会以消息或者回调的方式通知A。
  • 区别与联系
    同步异步指的是被调用者结果返回时通知线程的一种机制。
    阻塞非阻塞指的是结果返回前,当前线程的状态。

    老王去小卖部买瓶汽水
  • 同步阻塞:店主说等下,然后去仓库里找汽水,期间老王一直等待,直到店主回来。

同步体现在:老王主动等待。
阻塞体现在:等待过程中,不能做其它事情

老王觉得这样很傻

  • 同步非阻塞:老王一边玩手机一边等待,并时不时的问一句好了没

同步体现在:老王主动等待
非阻塞体现在:老王等待的过程中又做了其它事情,玩手机

老王要时不时的吼一句好了没,有点傻

  • 异步非阻塞:老板说找到了打电话通知老王,于是老王愉快的回家了

异步:老板主动通知老王
非阻塞:通知到达的时候,老王并不是在傻等,而是做另一件事,回家

在这个例子中, 同步和异步指的是收到老板找到汽水通知的一种机制(等待老板找到/老板找到打电话)而阻塞和非阻塞指的是等待找到的过程中老王的状态(等待什么都不做/边等边玩手机)。可以看到,同步和异步关注的是调用者获取结果的机制,而阻塞和非阻塞指的是在获取结果的过程中调用者所处的状态

2.2 BIO

BIO(Blocking IO),同步阻塞IO。全部java.io及部分java.net包是阻塞的,下面以socket编程来说明下BIO,及其带来的问题。

  • BIOServer
public class BIOServer {
    public static void main(String[] args) {
        ServerSocket socket=null;
        byte[] bytes = new byte[1024];
        try {
            //创建server
            socket = new ServerSocket();
            //绑定端口
            socket.bind(new InetSocketAddress("127.0.0.1", 9900));
            while (true) {
                System.out.println("等待连接");
                //侦听并接受到此套接字的连接。
                //阻塞
                Socket client = socket.accept();
                System.out.println("已连接");
                System.out.println("data start");
                //读取数据
                //阻塞
                client.getInputStream().read(bytes);
                System.out.println("data end");
                System.out.println(" receive data:" + new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • Client
public class Client {
    public static void main(String[] args) {
        Socket socket = null;
        try {
            socket = new Socket();
            socket.connect(new InetSocketAddress("127.0.0.1", 9900));
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入>>>>>>>");
            while (true) {
                String msg = scanner.next();
                socket.getOutputStream().write(msg.getBytes());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 运行server,输出结果如下:
    无连接时server输出
    当没有客户端来连接的时候,server阻塞在Socket client = socket.accept(); 这行代码中
  2. 连接一个client,且不在控制台做任何输入,结果如下:
    客户端状态
    服务端首次连接状态
    当只有一个连接,且无任何输入时,server 阻塞在client.getInputStream().read(bytes);这行代码中
  3. 保持2的client不关闭,再开一个新的client连接,结果如下:
    client
    server
    我们发现server 并没有打印出新的连接信息,也就是说server阻塞在 client.getInputStream().read(bytes);
    我们查看accept的方法签名,可以看到,其中很明确的指出来,会阻塞到直到有一个连接建立
   /**
     * Listens for a connection to be made to this socket and accepts
     * it. The method blocks until a connection is made.
     *
     * <p>A new Socket {@code s} is created and, if there
     * is a security manager,
     * the security manager's {@code checkAccept} method is called
     * with {@code s.getInetAddress().getHostAddress()} and
     * {@code s.getPort()}
     * as its arguments to ensure the operation is allowed.
     * This could result in a SecurityException.
     *

ques1: 那么这种同步阻塞IO会带来哪些问题呢?
ans1:显而易见,只能接收一个连接,无法处理大量连接
ques2: 那么有什么解决方案呢?
ans: 既然单线程会阻塞,那么用多线程不是就可以解决这个问题吗,每次只阻塞子线程

  • 多线程版本
  • BIOServerByThread
public class BIOServerByThread {
    public static void main(String[] args) {
        ServerSocket socket=null;
        try {
            //创建server
            socket = new ServerSocket();
            //绑定端口
            socket.bind(new InetSocketAddress("127.0.0.1", 9900));
            while (true) {
                System.out.println("等待连接");
                //侦听并接受到此套接字的连接。
                //阻塞
                Socket client = socket.accept();
                System.out.println("已连接");
                //当有新的连接进来的时候,为其分配一个线程
                new Thread(new InnerThread(client)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static class InnerThread implements Runnable{
        byte[] bytes = new byte[1024];
        Socket client;
        InnerThread(Socket client){
            this.client=client;
        }

        @Override
        public void run()  {
            System.out.println("data start");
            try {
                //读取数据
                //阻塞
                client.getInputStream().read(bytes);
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("data end");
            System.out.println(" receive data:" + new String(bytes));
        }
    }
}
  • client 保持不变即可
    开启server后,同时开启两个client,运行结果如下:


    多线程server

我们发现,这样的却可以实现并发连接,会不会有其它问题呢?

  1. 我们知道线程占用内存的大小一般在64KB-1M之间,jvm内存是有限的,这就决定了线程的创建数量是有限的,这种方式一旦线程创建过多就会OOM。有些可能会想到线程池,即便用了线程池,如果请求量过大的时候,还是会在线程池中排队,对client端来说依然是无法连接。
  2. 多线程必然带来了很多的线程上下文切换,这个操作是比耗资源的

那么难道就没办法解决了吗?


2.3 NIO

java 1.4 版本推出了NIO ,即同步非阻塞IO,利用多路复用IO模型,将多个IO阻塞到一个select上,实现了单个线程处理多个连接的情况。话不多说,先上代码:

  • NIOServer
public class NIOServer {
    public static void main(String[] args) throws Exception {
        Selector selector = Selector.open();
        ServerSocketChannel channel = ServerSocketChannel.open().bind(new InetSocketAddress("127.0.0.1", 9900));
        channel.configureBlocking(false);
        channel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            System.out.println("等待连接");
            //阻塞到事件到达(即阻塞到当前selector阻塞的事件到达)
            selector.select();
            System.out.println("已连接");
            Set keys = selector.selectedKeys();
            Iterator iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = (SelectionKey) iterator.next();
                if(key.isAcceptable()) {
                    //接受连接事件
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = serverSocketChannel.accept();
                    if (clientChannel==null){
                        continue;
                    }
                    clientChannel.configureBlocking(false);
                    clientChannel.register(key.selector(),SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    //可读事件
                    SocketChannel clientChannel=(SocketChannel) key.channel();
                    ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
                   int byteReads= clientChannel.read(byteBuffer);
                   while (byteReads>0){
                       byteBuffer.flip();
                       System.out.println("client data:"+new String(byteBuffer.array()));
                       byteBuffer.clear();
                       byteReads=clientChannel.read(byteBuffer);
                   }
                    //写数据
                   byteBuffer.clear();
                   byteBuffer.put("客户端你好,我是服务端".getBytes());
                   byteBuffer.flip();
                   clientChannel.write(byteBuffer);
                }
            }
        }
    }
}
  • client 不变
    分别运行server,打开两个客户端之后输出结果如下:
    NIOServer
    说明我们利用nio实现了单线程处理多个连接,要理解上面的逻辑,首先我们要理解下什么是IO 多路复用
  • IO多路复用也被叫做事件驱动,即Reactor模式,即利于linux提供的select/poll函数,将多个文件句柄传递给select/poll系统调用,select/poll会轮询这些文件句柄,当有文件可读写的时候,会解除阻塞,并返回给应用程序就绪事件。这样就实现了一个线程处理多个socket连接
  • java 1.5 之前 Selector是基于linux select/poll 实现。java 1.5 之后,使用epoll替代了poll。
  • epoll 是一种信号驱动机制,通过信号回调来通知应用程序读写数据。
  • BIO只能阻塞一个IO操作,而IO复用模型能阻塞多个IO操作,所以才叫多路复用

下面简单介绍下查看Selector 源码的过程,至于select/poll/epoll函数不在深入

  • NIOServer
 //阻塞到事件到达(即阻塞到当前selector阻塞的事件到达)
            selector.select();
  • java.nio.channels.Selector
/**
     * Selects a set of keys whose corresponding channels are ready for I/O
     * operations.
     *
     * <p> This method performs a blocking <a href="#selop">selection
     * operation</a>.  It returns only after at least one channel is selected,
     * this selector's {@link #wakeup wakeup} method is invoked, or the current
     * thread is interrupted, whichever comes first.  </p>
     *
     * @return  The number of keys, possibly zero,
     *          whose ready-operation sets were updated
     *
     * @throws  IOException
     *          If an I/O error occurs
     *
     * @throws  ClosedSelectorException
     *          If this selector is closed
     */
    public abstract int select() throws IOException;
  • sun.nio.ch.SelectorImpl.select()
  public int select() throws IOException {
        return this.select(0L);
    }
  • sun.nio.ch.WindowsSelectorImpl.doSelect
protected int doSelect(long var1) throws IOException {
      try {
                        this.subSelector.poll();
                    } catch (IOException var7) {
                        this.finishLock.setException(var7);
                    }
}
  • sun.nio.ch.WindowsSelectorImpl.poll0
private native int poll0(long var1, int var3, int[] var4, int[] var5, int[] var6, long var7);

那么问题来了,这个poll0的native方法究竟在哪里呢?此时需要我们下载openjdk的源码
在src\windows\native\sun\nio\ch 目录下有个WindowsSelectorImpl.c,其中有个poll0方法
签名如下

Java_sun_nio_ch_WindowsSelectorImpl_00024SubSelector_poll0(JNIEnv *env, jobject this,
                                   jlong pollAddress, jint numfds,
                                   jintArray returnReadFds, jintArray returnWriteFds,
                                   jintArray returnExceptFds, jlong timeout)

关键部分如下

    /* Call select */
    if ((result = select(0 , &readfds, &writefds, &exceptfds, tv))
                                                             == SOCKET_ERROR) {
        /* Bad error - this should not happen frequently */
        /* Iterate over sockets and call select() on each separately */
        FD_SET errreadfds, errwritefds, errexceptfds;

注意的是这个只是windows版本jdk的源码

说完了NIO的部分,我们可以发现NIO的api特别复杂,要对这套api特别熟悉才能开发出高质量的网络程序,所以一般推荐使用第三方类库如netty。

2.4 AIO

终于到AIO了,jdk 1.4 带来了NIO,1.7 带来了AIO,也叫NIO2。AIO是真正的异步非阻塞IO,无需通过多路复用轮询select的方式来实现,通过异步回调来实现。话不多说,上代码

public class AIOServer {
    private AsynchronousServerSocketChannel channel;
    private void listen() throws IOException{
        this.channel=AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("127.0.0.1",9900), 1024);
        channel.accept(this,new ConnectHandler());
        new Thread(()->{
           while (true){
               try {
                   TimeUnit.SECONDS.sleep(2);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        }).start();
    }

    private class ConnectHandler implements CompletionHandler<AsynchronousSocketChannel, AIOServer>{
        @Override
        public void completed(AsynchronousSocketChannel result, AIOServer attachment) {
            try{
                if (result.isOpen()){
                    System.out.println("connected");
                    final ByteBuffer buffer = ByteBuffer.allocate(1024);
                    buffer.clear();
                    result.read(buffer, result, new ReadHandler(buffer));
                }
            }finally {
                channel.accept(attachment,this);
            }
        }

        @Override
        public void failed(Throwable exc, AIOServer attachment) {
            try {
                exc.printStackTrace();
            }finally {
                channel.accept(attachment,this);
            }
        }
    }


    private class ReadHandler implements CompletionHandler<Integer,AsynchronousSocketChannel>{
        private ByteBuffer buffer;

        public ReadHandler(ByteBuffer buffer){
            this.buffer=buffer;
        }

        @Override
        public void completed(Integer result, AsynchronousSocketChannel attachment) {
            buffer.flip();
            CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();

            CharBuffer charBuffer = null;
            try {
                charBuffer = decoder.decode(buffer);
            } catch (CharacterCodingException e) {
                e.printStackTrace();
            }
            System.out.println(charBuffer.toString());
            buffer.clear();
            buffer.put("hello".getBytes());
            attachment.write(buffer,attachment,new WriteHandler(buffer));

        }

        @Override
        public void failed(Throwable exc, AsynchronousSocketChannel attachment) {

        }
    }

    private class WriteHandler implements CompletionHandler<Integer,AsynchronousSocketChannel>{
        private ByteBuffer buffer;

        public WriteHandler(ByteBuffer buffer) {
            this.buffer = buffer;
        }

        @Override
        public void completed(Integer result, AsynchronousSocketChannel attachment) {
            buffer.clear();

            try {
                attachment.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
            exc.printStackTrace();
            try {
                attachment.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception{
        new AIOServer().listen();
    }

}

同样,先运行server,再分别打开两个client,输出如下:


AIOServer.png

可以看到实现了处理多个客户端同时连接。
值得注意的是,既然AIO是真正的异步非阻塞IO,那么netty为什么不选择AIO作为默认实现呢?
这是因为aio在windows层面支持的比较完善,但是在linux系统中还是基于epoll来实现,netty曾经有4.X小 版本中支持AIO,后来发现在linux中性能并无太大提升,反而增加代码维护的复杂性,最后在新版本中去掉了。

3. tomcat的三种模式

tomcat 支持三种启动模式,bio、nio、apr。下面我们来介绍这三种模式

  • bio即以阻塞io的模式启动
  • nio以同步非阻塞的模式启动
  • apr即以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高Tomcat对静态文件的处理性能,是在操作系统层面的直接调用。

3.1 不同模式的性能差距

tomcat 版本 7.0,压测工具ab

  • BIO模式(tomcat8及以上舍弃了BIO,默认NIO)
  <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
  • NIO 模式
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
               connectionTimeout="20000"
               redirectPort="8443" />
  • APR模式
<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol"
              connectionTimeout="20000"
              redirectPort="8443" />

具体的性能差异如下:

20151208224230425.jpg
(ps:图摘自网络,感兴趣的童鞋可以自己下去用jmeter压)
我们可以看到bio模式下,随着并发数的提高,吞吐量明显下降,而切换为nio模式后,性能有了较大的提升

4. 互联网公司的架构演变

了解架构演变之前,我们先思考以下两个问题。
1. 互联网系统是不是一开始就这么大?
2. 有没有必要一开始就做个大型系统?

4.1 LAMP

  • 公司发展的初期,作为一个无名的小网站,一般都是从LAMP开始的。所谓LAMP架构即 Linux服务器+Apache应用服务器+Mysql数据存储+Php
  • 同时把这些应用部署到一台服务器上,在初期,访问量很低的时候能满足业务需求。

    此时网站的架构图如下: LAMP.png

架构优点:开销比较低,可以快速推向市场。

随着网站业务的发展,越来越多的用户访问,会带来以下问题

  1. 性能越来越差
  2. 越来越多的数据导致存储空间不足

4.2 应用服务与数据服务分离

  • 所谓应用服务于数据服务分离,即是将原来部署到一台机器上的 应用程序、数据库、文件服务独立开来。
  • 原来一个人做三件事,现在三个人做三件事。提升性能简单粗暴。
    此时的网站结构图如下:


    应用服务与数据服务分离.png

采用这种架构的网站,有以下优点

  1. 便于我们为不同的服务器定制不同的硬件,因为应用服务器、数据服务器、文件服务器所需的硬件是不同的。
  2. 不同服务器承担不同的服务角色
  3. 快速提升系统性能

不同服务器种类的特点如下:

优服务器种类 特点
应用服务器 需要更快更强大的CPU(处理大量的业务逻辑)
数据库服务器 需要更快的硬盘和更大的缓存(快速磁盘检索和数据缓存)
文件服务器 需要更大的硬盘(存储大量用户上传的文件)

随着用户数逐渐增多,会出现哪些问题呢?
数据库压力太大导致访问延迟,进而影响整个网站的性能,用户体验受到影响!

4.3 使用缓存改善性能

既然数据压力大,那么我们能不能减少数据库的访问次数呢,最简单的做法就是使用缓存(本地缓存和远程分布式缓存)。此时的架构图如下:


使用缓存来改善性能.png
  • 本地缓存,特点是速度快,但是能缓存的数据量有限,且会和我们的应用程序争用CPU资源
  • 分布式缓存,特点就是没有本地缓存快,但是可以根据需要缓存的数据量线性扩容
  • 缓存的常用组件有memcache,redis。
    随着用户数逐渐增多,会出现哪些问题呢?
    单一服务器能够处理的请求连接有限,网站访问高峰期,应用服务器成为整个网站的瓶颈!

4.4 应用服务器集群

既然单一服务器成为瓶颈,除了提升单一服务器的性能之外(单机性能是有上限的),最简单的做法就是应用服务器的集群了,此时架构图如下:


应用服务器集群.png

常见的负载均衡实现方式包含几种

  1. 软件层面:Apache、Nginx、LVS
  2. 硬件层面:F5
  3. DNS负载均衡

使用了缓存之后,虽然大大减轻了数据库的读压力,但是又面临了新的问题

  • 缓存击穿、缓存雪崩!
  • 缓存不一致性问题!
  • 所有的写操作都是要访问数据库,数据库因为负载过高,成为整个性能的瓶颈!

4.5 数据库的读写分离

数据库的读写分离,即数据库使用主从复制,从库读,主库写。此时架构图如下:


数据库读写分离.png
  • 为什么要有数据访问模块?因为要实现架构变更不影响上层应用。
  • 数据访问模块:mycat,sharding-jdbc
    随着用户规模越来越大,发布地域原来越广,地域网络环境差别很大,又带来哪些问题
  • 网络差异导致访问缓慢

4.6 使用CDN 加速

所谓CDN即将源站内容分发至最接近用户的节点,使用户可就近取得所需内容,提高用户访问的响应速度和成功率。解决因分布、带宽、服务器性能带来的访问延迟问题,适用于站点加速、点播、直播等场景。用了CDN后的架构图如下:


CDN加速.png

用了CDN后可以
1. 加快用户访问响应速度
2. 减轻后端服务器的负载压力

随着用户的增多,访问量的增大,当前架构又会面临哪些问题
单文件服务器、单数据库服务器,存不下日益增长的数据

4.7 使用分布式文件系统与分布式数据库系统

既然单文件服务器,单数控服务器存不在数据,那么我们就使用分布式文件集群,分布式数据库集群来解决瓶颈,架构图如下:


分布式文件系统与分布式数据库系统.png

分布式文件系统:FastDFS
分布式数据库即分库分表:Mycat,sharing-JDBC

那么存储不了数据的问题解决了,随着用户的发展,还会有哪些问题呢
存储的字段差异较大,骷髅表(即一个大表中,所有的属性只使用了部分,无法同时满足)
复杂的文本检索(like)

4.8 使用nosql和搜索引擎

既然字段差异较大,那么我们就考虑将数据存储到非关系型数据库,即nosql如MongoDB中,要做复杂文本检索的时候使用搜索引擎搜索如Elasticsearch中,此时架构如下:


使用NoSql和搜索引擎.png

常用的搜索引擎:lucene、solr、elasticsearch
常用的nosql:mongodb、elasticsearch

现在检索和存储的问题解决了,那么还会有问题吗
网站越做越好,业务不断扩大,越来越复杂。应用程序也变得越来越大
牵一发而动全身,如何应对快速的业务发展需要(发版周期从月到周再到天)

4.9 业务拆分

应用越来越大,内部耦合越来越高,那只能做业务拆分了。按照不同的业务将原来的单个应用,拆分为多个应用,应用于应用之间通过MQ、数据存储、服务调用建立关联。此时的架构图如下:


业务拆分.png

消息队列常用的有:RabbitMQ,ActiveMQ,Kafka

业务规模不断增大,应用拆分越来越小,越来越多,会面临哪些问题呢?
应用间的关系越来越复杂,应用中存在大量相同的业务操作!
后端的数据库要被成千上万台服务器连接,数据库连接资源不足!

4.10 服务化

所谓服务化,即把业务操作中公共的部分,独立开来做成单独的服务。需要使用该业务的时候调用相应的服务即可。此时架构图如下:


服务化.png

服务化的框架:springcloud,dubbo

到这里,我们的服务从各个角度都是处于高可用的状态了,那么再往后,需要做什么呢?
数据挖掘、分析、推荐等业务需求,庞大系统的监控、问题分析等需求

4.11 大数据技术、监控、日志分析系统

当我们的系统进化到这种程度后,我们发现,我们要完善的不是架构,而是架构相关的基础设施,从而让其为我们带来更大的业务价值,即日志分析、监控、及大数据。此时架构图如下:


大数据技术、监控、日志分析系统.png

集中日志分析:elk,Logstash 做数据采集和过滤,Elasticsearch做数据存储及搜索引擎,Kibana 做数据展示及分析
监控服务:Zabbix
大数据:Hadoop、Spark

经过了上面的过程,我们完成了互联网公司的架构演变。
同样,我们可以轻松的得到了章节初提出的两个问题的答案。结果显然是否定的

5. 总结

这边文章中,我们先是了解了几种IO模型,然后简单了解了NIO的原理,接着了解了NIO实际的应用即tomcat的三种模式,引出了性能优化的话题。为了更好的了解性能优化的套路,我们主要讲解了互联网公司的架构演变之路,为以后的各个击破做一个铺垫。所以本文虽然说是叫性能优化,实际上只是性能优化的开篇。限于篇幅,并未提及性能优化。

由于技术水平所限,文章难免有不足之处,欢迎大家指出。希望每位读者都能有新的收获,我们下一篇文章见.....

参考文章

  • 网易公开课-互联网架构演变之路
  • 大型网站技术架构 核心原理与案例分析-李智慧

相关文章

网友评论

    本文标题:从NIO原理到性能优化

    本文链接:https://www.haomeiwen.com/subject/vkkqahtx.html