Socket分片:基于Netty的Java实现

Socket分片:基于Netty的Java实现

尽管Java很早就有一个特性请求: JDK-6432031 ,但是时至今日,Oracle JDK依然不支持这个选项,因此我们只能通过 hack 的方式在Java中使用此特性。

Google已经在内部服务器中开启了这个特性:

SO_REUSEPORT

Scaling Techniques for Servers with High Connection Rates

SO_REUSEPORT

就像聂永的博客中所说:

运行在Linux系统上网络应用程序,为了利用多核的优势,一般使用以下比较典型的多进程/多线程服务器模型:

  • 单线程listen/accept,多个工作线程接收任务分发,虽CPU的工作负载不再是问题,但会存在:
    • 单线程listener,在处理高速率海量连接时,一样会成为瓶颈
    • CPU缓存行丢失套接字结构(socket structure)现象严重
  • 所有工作线程都accept()在同一个服务器套接字上呢,一样存在问题:
    • 多线程访问server socket锁竞争严重
    • 高负载下,线程之间处理不均衡,有时高达3:1不均衡比例
    • 导致CPU缓存行跳跃(cache line bouncing)
    • 在繁忙CPU上存在较大延迟

上面模型虽然可以做到线程和CPU核绑定,但都会存在:

  • 单一listener工作线程在高速的连接接入处理时会成为瓶颈
  • 缓存行跳跃
  • 很难做到CPU之间的负载均衡
  • 随着核数的扩展,性能并没有随着提升

SO_REUSEPORT 在*BSD平台早已经实现,而Linux平台则由谷歌工程师实现并于2013年正式纳入Linux的trunk (kernel 3.9)。

当前的操作系统支持情况:

  • Linux: 内核自3.9开始支持此特性。因此Redhat 7.0中支持。
  • BSD: 支持 (FreeBSD, DragonFly BSD. OpenBSD, NetBSD等)
  • Mac OS:支持
  • Windows: windows上只有SO_REUSEADDR选项,没有SO_REUSEPORT。windows上设置了SO_REUSEADDR的socket其行为与BSD上设定了SO_REUSEPORT和SO_REUSEADDRd的行为大致一样。
  • Solaris:Solaris 11中支持 (patch已打)

和SO_REUSEADDR的区别

SO_REUSEADDR提供如下四个功能

  1. SO_REUSEADDR允许启动一个监听服务器并捆绑一个端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在(TIME_WAIT)。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。
  2. SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。
  3. SO_REUSEADDR允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。
  4. SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。

当使用通配符的时候更为复杂,这里有一张表格列出了各种情况(服务器的IP地址是192.168.0.1):

SO_REUSEADDRsocketA socketB Result
---------------------------------------------------------------------
 ON/OFF 192.168.0.1:21192.168.0.1:21Error (EADDRINUSE)
 ON/OFF 192.168.0.1:2110.0.0.1:21OK
 ON/OFF 10.0.0.1:21192.168.0.1:21OK
 OFF 0.0.0.0:21192.168.1.0:21Error (EADDRINUSE)
 OFF 192.168.1.0:210.0.0.0:21Error (EADDRINUSE)
 ON 0.0.0.0:21192.168.1.0:21OK
 ON 192.168.1.0:210.0.0.0:21OK
 ON/OFF 0.0.0.0:210.0.0.0:21Error (EADDRINUSE)

SO_REUSEPORT选项有如下语义

  1. 此选项允许完全重复捆绑,但仅在想捆绑相同IP地址和端口的套接口都指定了此套接口选项才行。
  2. 如果被捆绑的IP地址是一个多播地址,则SO_REUSEADDR和SO_REUSEPORT等效。

Netty的实现

Netty不是唯一通过 hack 方式实现 SO_REUSEPORT 特性的Java网络框架。比如下面的方式,使用 sun.nio.ch.Net 进行设置:

importsun.nio.ch.Net;

 ......

publicvoidsetReusePort(ServerSocketChannel serverChannel) {
try{
 Field fieldFd = serverChannel.getClass().getDeclaredField("fd");
//NoSuchFieldException
 fieldFd.setAccessible(true);
 FileDescriptor fd = (FileDescriptor)fieldFd.get(serverChannel);
//IllegalAccessException

 Method methodSetIntOption0 =
 Net.class.getDeclaredMethod("setIntOption0", FileDescriptor.class,
 Boolean.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE);
 methodSetIntOption0.setAccessible(true);
 methodSetIntOption0.invoke(null, fd,false,'\uffff',
 SO_REUSEPORT, 1);
 } catch(Exception e) {
 System.out.println(e.toString());
 }
}

但是本文将关注Netty,因为Netty提供了一个通过JNI封装的库,可以更方便的进行 SO_REUSEPORT 设置。

自4.0.16版本,Netty为Linux提供了 native socket transport,通过JNI的方式实现,它可以提供更高的性能和极少的垃圾回收。

它兼容NIO的方式,只需改成:

NioEventLoopGroup → EpollEventLoopGroup
NioEventLoop → EpollEventLoop
NioServerSocketChannel → EpollServerSocketChannel
NioSocketChannel → EpollSocketChannel

Maven pom.xml加入:

<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.2.3.Final</version>
</extension>
</extensions>
 ...
</build>

<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<version>${project.version}</version>
<classifier>${os.detected.classifier}</classifier>
</dependency>
 ...
</dependencies>

SBT配置文件的话则加入:

"io.netty"%"netty-transport-native-epoll"%"${project.version}"classifier"linux-x86_64"

代码中设置 SO_REUSEPORT :

bootstrap.option(EpollChannelOption.SO_REUSEPORT, true)

很简单,完整的代码可以参照: WebServer.scala

你可能感兴趣的:(Socket分片:基于Netty的Java实现)