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/
相關文章
相關標籤/搜索