C10K 问题
C10K 问题翻看高新能网络文章,里面做了详细叙述。
我们使用BIO的时候,来一个连接就抛出一个线程。被抛出的独立的线程进行阻塞,等待接收已连接的client发来的数据,这样不会影响其他client继续连接。每个线程自己忙自己的。
但是随着连接数的变大,抛出的线程越多,由于线程之间的切换,系统的性能会越来越低。
举一个例子
一个客户端可以通过2个不同的ip,与服务端创建2*65000个连接。
C10Kclient.java
package com.bjmashibing.system.io;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
public class C10Kclient {
public static void main(String[] args) {
LinkedList<SocketChannel> clients = new LinkedList<>();
InetSocketAddress serverAddr = new InetSocketAddress("192.168.150.11", 9090);
for (int i = 10000; i < 65000; i++) { // 一个客户端可以通过2个不同的ip,与服务端创建2*65000个连接
try {
SocketChannel client1 = SocketChannel.open();
SocketChannel client2 = SocketChannel.open();
/*
linux中你看到的连接就是:
client...port: 10508
client...port: 10508
*/
client1.bind(new InetSocketAddress("192.168.150.1", i)); // 这台机器上的第一个ip
// 192.168.150.1:10000 192.168.150.11:9090
client1.connect(serverAddr);
boolean c1 = client1.isOpen();
clients.add(client1);
client2.bind(new InetSocketAddress("192.168.110.100", i)); // 这台机器上的第二个ip
// 192.168.110.100:10000 192.168.150.11:9090
client2.connect(serverAddr);
boolean c2 = client2.isOpen();
clients.add(client2);
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("clients " + clients.size());
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
关于为什么需要在Linux上单独配一个路由条目
在Linux上单独配一个路由条目
因为物理机的192.168.110.100和虚拟机的192.168.150.0不是直连关系
192.168.150.2是虚拟机所在网络的网关,用于完成网络地址转换。
来自192.168.110.100的网络包在返回给客户端的时候,经过NAT地址转换,目标ip被改成了192.168.150.2,Windows收到之后不知道这个ip应该发给谁了,导致三次握手的第二次回的包被windows丢弃了,没有连接上。
关于为什么连接的速度并不快?(一秒钟仅能够建立4个连接左右)
为什么BIO慢?
创建一个连接的过程如下图所示。
- accept系统调用,是一个阻塞的循环过程,这个过程耗费时间。
- 抛出一个线程的速度比较慢。
这就是整个BIO的弊端。想要调优,就要解决阻塞的问题,但阻塞是由内核提供给我们的API决accept receive定的。
NIO 的引入
NIO的N是啥意思呢?有两个角度可以理解
- Non-Blocking IO (操作系统中)
- New IO (JDK中)
Linux中的文件描述符是输入输出双向的。
Java中ServerSocketChannel也是淡化了输入、输出的概念,把输入、输出合在一起了。
一段NIO的代码
SocketNIO.java
package com.bjmashibing.system.io;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
public class SocketNIO {
public static void main(String[] args) throws Exception {
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(9090));
ss.configureBlocking(false); //设置false时,是非阻塞。OS层面的NONBLOCKING,不会阻塞地去等待连接。
// ss.setOption(StandardSocketOptions.TCP_NODELAY, false);
// StandardSocketOptions.TCP_NODELAY
// StandardSocketOptions.SO_KEEPALIVE
// StandardSocketOptions.SO_LINGER
// StandardSocketOptions.SO_RCVBUF
// StandardSocketOptions.SO_SNDBUF
// StandardSocketOptions.SO_REUSEADDR
while (true) {
//接受客户端的连接,不会阻塞
Thread.sleep(1000);
SocketChannel client = ss.accept(); //开始接收客户端。不会阻塞等待连接,如果得不到连接,则返回 -1,NULL
// ss.accept();方法调用内核了,有下面这些情况:
// 1,没有客户端连接进来,返回值是啥呢?在 BIO 的时候一直卡着,但是在NIO的时候不会卡着,返回的是-1,NULL
// 2、有客户端的连接,ss.accept();返回的是这个客户端的fd 5(OS层面),Client对象(java层面)
if (client == null) {
System.out.println("null.....");// 可以不处理它,不要把这段当做性能消耗去思考。
} else {
client.configureBlocking(false); //重点
// socket有两个角度:
// 1、服务端的listen socket<连接请求三次握手后,往我这里扔,我去通过accept,得到后面的连接的socket>
// 2、服务端接受客户端连接进来之后,形成的连接socket<连接后的数据读写使用的>
int port = client.socket().getPort();
System.out.println("client...port: " + port);
clients.add(client);
}
//执行读取行为:遍历已连接的客户端去读写数据,这个过程不会阻塞
ByteBuffer buffer = ByteBuffer.allocateDirect(4096); //直接内存分配,可以在堆里分配,也可以在堆外分配
for (SocketChannel c : clients) { //串行化!!!! 多线程!!
int num = c.read(buffer); // 返回值 >0 -1 0 不会阻塞
if (num > 0) {
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);
String b = new String(aaa);
System.out.println(c.socket().getPort() + " : " + b);
buffer.clear();
}
}
}
}
}
运行起来
ss.configureBlocking(true);阻塞状态下
ss.configureBlocking(false);非阻塞状态下
非阻塞了,这有什么意义?
假设这时候有两个客户端连接进来了,其中一个的示例是下面这个样子的:
曾经我们是需要抛出一个线程,把这个线程扔出去,让它自己去读取数据。
现在不需要抛线程了,完全在一个线程中执行,只不过是无数个循环。而在没有连接的时候,循环也不会被阻塞,依然继续循环,从而让循环后半部分的读取数据的逻辑就有机会在当前循环当中被执行到。
再用C10K压测一下
用C10K压测一下NIO的性能(单客户端10W连接):大约每秒钟能建立50个左右的连接
现在使用NIO的瓶颈是:当连接进来很多客户端时,for (SocketChannel c : clients)遍历每一个客户端去读取数据的过程耗费了性能。
另外,报错超出文件描述符的数量,这个是可以设置的:ulimit -SHn 500000(软硬openfile,改成50万)
注:为啥ulimit -n 1024,但是连接数超过了1024呢?
这个理论是对的,只不过要看用户。权限对root来说等于虚设,很多资源的约束在root用户也是放开的。而且在公司里,生产环境肯定是非root用户启动程序。