Socket分片:基于Netty的Java实现

最近Nginx发布了1.9.1版,其中一个新的特性就是支持socket的SO_REUSEPORT选项。这个socket的SO_REUSEPORT选项已经有许多现实世界的应用。对NGINX而言,它经过将链接均衡的分给多个进程以提高性能。SO_REUSEPORT已经在一些操做系统上实现了支持。这个选项容许多个socket监听同一个IP地址+端口。内核负载均衡这些进来的sockets链接,将这些socket有效的分片。 尽管Java很早就有一个特性请求:JDK-6432031,可是时至今日,Oracle JDK依然不支持这个选项,所以咱们只能经过hack的方式在Java中使用此特性。html

Google已经在内部服务器中开启了SO_REUSEPORT这个特性: Scaling Techniques for Servers with High Connection Rateslinux

####SO_REUSEPORTgit

就像聂永的博客中所说:github

运行在Linux系统上网络应用程序,为了利用多核的优点,通常使用如下比较典型的多进程/多线程服务器模型:web

单线程listen/accept,多个工做线程接收任务分发,虽CPU的工做负载再也不是问题,但会存在:bootstrap

  • 单线程listener,在处理高速率海量链接时,同样会成为瓶颈;
  • CPU缓存行丢失套接字结构(socket structure)现象严重。

全部工做线程都accept()在同一个服务器套接字上呢,同样存在问题:windows

  • 多线程访问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_REUSEPORTSO_REUSEADDR的行为大体同样;
  • Solaris:Solaris 11中支持 (patch已打)。

####和SO_REUSEADDR的区别 #####SO_REUSEADDR提供以下四个功能:

  • SO_REUSEADDR容许启动一个监听服务器并捆绑一个端口,即便之前创建的将此端口用作他们的本地端口的链接仍存在(TIME_WAIT)。这一般是重启监听服务器时出现,若不设置此选项,则bind时将出错。
  • SO_REUSEADDR容许在同一端口上启动同一服务器的多个实例,只要每一个实例捆绑一个不一样的本地IP地址便可。
  • SO_REUSEADDR容许单个进程捆绑同一端口到多个套接口上,只要每一个捆绑指定不一样的本地IP地址便可。这通常不用于TCP服务器。
  • SO_REUSEADDR容许彻底重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还容许此IP地址和端口捆绑到另外一个套接口上。通常来讲,这个特性仅在支持多播的系统上才有,并且只对UDP套接口而言(TCP不支持多播)。

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

SO_REUSEADDR socketA socketB Result
ON/OFF 192.168.0.1:21 192.168.0.1:21 Error (EADDRINUSE)
ON/OFF 192.168.0.1:21 10.0.0.1:21 OK
ON/OFF 10.0.0.1:21 192.168.0.1:21 OK
OFF 0.0.0.0:21 192.168.1.0:21 Error (EADDRINUSE)
OFF 192.168.1.0:21 0.0.0.0:21 Error (EADDRINUSE)
ON 0.0.0.0:21 192.168.1.0:21 OK
ON 192.168.1.0:21 0.0.0.0:21 OK
ON/OFF 0.0.0.0:21 0.0.0.0:21 Error (EADDRINUSE)

####SO_REUSEPORT选项有以下语义:

  • 此选项容许彻底重复捆绑,但仅在想捆绑相同IP地址和端口的套接口都指定了此套接口选项才行。
  • 若是被捆绑的IP地址是一个多播地址,则SO_REUSEADDR和SO_REUSEPORT等效。

####Netty的实现

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

import sun.nio.ch.Net;
   ......
public void setReusePort(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的方式,只需改为:
```Java
NioEventLoopGroup → EpollEventLoopGroup
NioEventLoop → EpollEventLoop
NioServerSocketChannel → EpollServerSocketChannel
NioSocketChannel → EpollSocketChannel
```
* Maven pom.xml加入:
```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配置文件的话则加入:
```Sbt
"io.netty" % "netty-transport-native-epoll" % "${project.version}" classifier "linux-x86_64"
```
* 代码中设置`SO_REUSEPORT`:
```Java
bootstrap.option(EpollChannelOption.SO_REUSEPORT, true)
```
* 很简单,完整的代码能够参照:[WebServer.scala](https://github.com/smallnest/C1000K-Servers/blob/master/netty/src/main/scala/com/colobu/webtest/netty/WebServer.scala)

####参考文档

* Socket Sharding in NGINX Release 1.9.1  
https://github.com/tokuhirom/jetty-so_reuseport-sample
* SO_REUSEPORT学习笔记  
http://www.cnblogs.com/mydomain/archive/2011/08/23/2150567.html  
http://stackoverflow.com/questions/14388706/socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean-t  
http://blog.chinaunix.net/uid-26133817-id-4814341.html  
https://lwn.net/Articles/542629/
相关文章
相关标签/搜索