一、C10k的由来
互联网的基础就是网络通信,早期的互联网可以说是一个小群体的集合。互联网还不够普及,用户也不多,一台服务器同时在线100个用户估计在当时已经算是大型应用了,所以并不存在什么 C10K 的难题。互联网的爆发期应该是在www网站,浏览器,雅虎出现后。最早的互联网称之为Web1.0,互联网大部分的使用场景是下载一个HTML页面,用户在浏览器中查看网页上的信息,这个时期也不存在C10K问题。
Web2.0时代到来后就不同了,一方面是普及率大大提高了,用户群体几何倍增长。另一方面是互联网不再是单纯的浏览万维网网页,逐渐开始进行交互,而且应用程序的逻辑也变的更复杂,从简单的表单提交,到即时通信和在线实时互动,C10K的问题才体现出来了。因为每一个用户都必须与服务器保持TCP连接才能进行实时的数据交互。
然而最初的服务器都是基于进程/线程模型的,对于每一个TCP连接都分配1个进程(或者线程)去处理。而进程又是操作系统最昂贵的资源,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么单机而言操作系统是无法承受的(往往出现效率低下甚至完全瘫痪)。如果是采用分布式系统,维持1亿用户在线需要10万台服务器,成本巨大。
基于上述考虑,如何突破单机性能局限,是高性能网络编程所必须要直面的问题。这些局限和问题最早被Dan Kegel 进行了归纳和总结,并首次成系统地分析和提出解决方案,后来这种普遍的网络现象和技术局限都被大家称为 C10K 问题。
二、传统的BIO
BIO模型图
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行处理,处理完成之后,通过输出流返回应答给客户端,最后再销毁线程。这就是典型的一请求一应答通信模型。正如我们前面所说的,这种模型局限性非常大,我们需要寻求更好的模型来解决高并发的问题。
三、伪异步IO的实现
(一)、定义
1、同步与异步
同步和异步通常用来形容一次方法调用
同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
异步方法调用更想一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而,异步方法通常在另外一个线程中执行着,整个过程不会妨碍调用者的工作。
2、伪异步IO
Java中提供了线程池和任务队列的机制来帮助我们实现伪异步IO模型。大致的处理过程为:每当服务器接收到一个客户端的连接请求,我们就把socket包装成一个task,并把这个task丢进线程池中,由线程池来处理该任务。线程池中的最大线程数是一定的,JVM会调度其中的空闲线程来处理我们的任务,当没有可用线程时,任务将会被加入任务列队中进行等待。这样子我们就不用为每个客户端请求都分配一个线程了。
而为何我们说这种处理方法是伪异步IO。主要是因为虽然它已经可以实现一个或多个线程处理N个客户端请求,但是它的底层仍然用的是同步阻塞IO机制,所以我们说这种处理方式是伪异步IO。
(二)、线程池简单说明
1、参数说明
2、处理机制
如果想更仔细地了解线程池的知识,可以读下这篇博客:https://www.cnblogs.com/dolphin0520/p/3932921.html
(三)、代码实现
之前写过BIO模型的聊天室(《JAVA简单聊天室的实现》),所能处理的最大并发数大概在2000到2300左右。今天我们就利用线程池来对这个聊天室进行简单的改造,并测试下它的性能。
1、线程池类
package server;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ChatServerHandlerExcutePool {
private ExecutorService executor;
public ChatServerHandlerExcutePool(int maxPoolSize, int queueSize) {
/*
* Runtime.getRuntime().availableProcessors():JVM运行所能创建的最大线程数
* 空闲线程存活时间为120s
*/
executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),maxPoolSize,120L,TimeUnit.SECONDS,new ArrayBlockingQueue<java.lang.Runnable>(queueSize));
}
public void execute(java.lang.Runnable task) {
executor.execute(task);
}
}
2、ChatServer类的改造
package server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ChatServer {
//主函数入口
public static void main(String[] args) throws IOException {
//实例化一个服务器类的对象
ChatServer cs=new ChatServer();
//调用方法,为指定端口创建服务器
cs.setUpServer(9003);
}
private void setUpServer(int port) throws IOException {
// TODO Auto-generated method stub
ServerSocket server=new ServerSocket(port);
//打印出当期创建的服务器端口号
System.out.println("服务器创建成功!端口号:"+port);
ChatServerHandlerExcutePool singleExecutor = new ChatServerHandlerExcutePool(50,10000);
Socket socket=null;
while(true) {
//等待连接进入
socket=server.accept();
System.out.println("进入了一个客户机连接:"+socket.getRemoteSocketAddress().toString());
//启动一个线程去处理这个对象
// ServerThread st=new ServerThread(socket);
// st.start();
//创建一个线程,并加入线程池
singleExecutor.execute(new ServerThread(socket));
}
}
}
(四)、性能测试
接着我们运行服务器,并且利用Jmeter来测试一下服务器的性能
设定启动1w个线程
查看结果
没有异常,说明1w个线程都已成功发送消息并且接受到返回的信息了。
(五)、局限性
虽然相较于传统的BIO,伪异步IO的性能有了一定的提升,但是仍然存在不少的问题
(六)、故障
后续我们可以尝试用NIO编程来进一步提升服务器的性能。
说明:本文主体内容来自《Netty权威指南》一书
本文所用的聊天室代码地址:https://github.com/Alexlingl/Chatroom