1. 程式人生 > >Netty之ChannelOption的各種引數之EpollChannelOption.SO_REUSEPORT

Netty之ChannelOption的各種引數之EpollChannelOption.SO_REUSEPORT

socket選項 SO_REUSEPORT 

miffa   miffa 釋出於 2015/03/24 17:21   字數 3383   閱讀 6076   收藏  6      評論 0

開發十年,就只剩下這套Java開發體系了 >>>   

前言

本篇用於記錄學習SO_REUSEPORT的筆記和心得,末尾還會提供一個bindp小工具也能為已有的程式享受這個新的特性。

當前Linux網路應用程式問題

執行在Linux系統上網路應用程式,為了利用多核的優勢,一般使用以下比較典型的多程序/多執行緒伺服器模型:

  1. 單執行緒listen/accept,多個工作執行緒接收任務分發,雖CPU的工作負載不再是問題,但會存在:

    • 單執行緒listener,在處理高速率海量連線時,一樣會成為瓶頸

    • CPU快取行丟失套接字結構(socket structure)現象嚴重

  2. 所有工作執行緒都accept()在同一個伺服器套接字上呢,一樣存在問題:

    • 多執行緒訪問server socket鎖競爭嚴重

    • 高負載下,執行緒之間處理不均衡,有時高達3:1不均衡比例

    • 導致CPU快取行跳躍(cache line bouncing)

    • 在繁忙CPU上存在較大延遲

上面模型雖然可以做到執行緒和CPU核繫結,但都會存在:

  • 單一listener工作執行緒在高速的連線接入處理時會成為瓶頸

  • 快取行跳躍

  • 很難做到CPU之間的負載均衡

  • 隨著核數的擴充套件,效能並沒有隨著提升

比如HTTP CPS(Connection Per Second)吞吐量並沒有隨著CPU核數增加呈現線性增長: 

Linux kernel 3.9帶來了SO_REUSEPORT特性,可以解決以上大部分問題。

SO_REUSEPORT解決了什麼問題

linux man文件中一段文字描述其作用:

The new socket option allows multiple sockets on the same host to bind to the same port, and is intended to improve the performance of multithreaded network server applications running on top of multicore systems.

SO_REUSEPORT支援多個程序或者執行緒繫結到同一埠,提高伺服器程式的效能,解決的問題:

  • 允許多個套接字 bind()/listen() 同一個TCP/UDP埠

    • 每一個執行緒擁有自己的伺服器套接字

    • 在伺服器套接字上沒有了鎖的競爭

  • 核心層面實現負載均衡

  • 安全層面,監聽同一個埠的套接字只能位於同一個使用者下面

其核心的實現主要有三點:

  • 擴充套件 socket option,增加 SO_REUSEPORT 選項,用來設定 reuseport。

  • 修改 bind 系統呼叫實現,以便支援可以繫結到相同的 IP 和埠

  • 修改處理新建連線的實現,查詢 listener 的時候,能夠支援在監聽相同 IP 和埠的多個 sock 之間均衡選擇。

程式碼分析,可以參考引用資料 [多個程序繫結相同埠的實現分析[Google Patch]]。

CPU之間平衡處理,水平擴充套件

以前通過fork形式建立多個子程序,現在有了SO_REUSEPORT,可以不用通過fork的形式,讓多程序監聽同一個埠,各個程序中accept socket fd不一樣,有新連線建立時,核心只會喚醒一個程序來accept,並且保證喚醒的均衡性。

模型簡單,維護方便了,程序的管理和應用邏輯解耦,程序的管理水平擴充套件許可權下放給程式設計師/管理員,可以根據實際進行控制程序啟動/關閉,增加了靈活性。

這帶來了一個較為微觀的水平擴充套件思路,執行緒多少是否合適,狀態是否存在共享,降低單個程序的資源依賴,針對無狀態的伺服器架構最為適合了。

新特性測試或多個版本共存

可以很方便的測試新特性,同一個程式,不同版本同時執行中,根據執行結果決定新老版本更迭與否。

針對對客戶端而言,表面上感受不到其變動,因為這些工作完全在伺服器端進行。

伺服器無縫重啟/切換

想法是,我們迭代了一版本,需要部署到線上,為之啟動一個新的程序後,稍後關閉舊版本程序程式,服務一直在執行中不間斷,需要平衡過度。這就像Erlang語言層面所提供的熱更新一樣。

想法不錯,但是實際操作起來,就不是那麼平滑了,還好有一個hubtime開源工具,原理為SIGHUP訊號處理器+SO_REUSEPORT+LD_RELOAD,可以幫助我們輕鬆做到,有需要的同學可以檢出試用一下。

SO_REUSEPORT已知問題

SO_REUSEPORT根據資料包的四元組{src ip, src port, dst ip, dst port}和當前繫結同一個埠的伺服器套接字數量進行資料包分發。若伺服器套接字數量產生變化,核心會把本該上一個伺服器套接字所處理的客戶端連線所傳送的資料包(比如三次握手期間的半連線,以及已經完成握手但在佇列中排隊的連線)分發到其它的伺服器套接字上面,可能會導致客戶端請求失敗,一般可以使用:

  • 使用固定的伺服器套接字數量,不要在負載繁忙期間輕易變化

  • 允許多個伺服器套接字共享TCP請求表(Tcp request table)

  • 不使用四元組作為Hash值進行選擇本地套接字處理,挑選隸屬於同一個CPU的套接字

與RFS/RPS/XPS-mq協作,可以獲得進一步的效能:

  • 伺服器執行緒繫結到CPUs

  • RPS分發TCP SYN包到對應CPU核上

  • TCP連線被已繫結到CPU上的執行緒accept()

  • XPS-mq(Transmit Packet Steering for multiqueue),傳輸佇列和CPU繫結,傳送資料

  • RFS/RPS保證同一個連線後續資料包都會被分發到同一個CPU上

  • 網絡卡接收佇列已經繫結到CPU,則RFS/RPS則無須設定

  • 需要注意硬體支援與否

目的嘛,資料包的軟硬中斷、接收、處理等在一個CPU核上,並行化處理,儘可能做到資源利用最大化。

SO_REUSEPORT不是一貼萬能膏藥

雖然SO_REUSEPORT解決了多個程序共同繫結/監聽同一埠的問題,但根據新浪林曉峰同學測試結果來看,在多核擴充套件層面也未能夠做到理想的線性擴充套件:

可以參考Fastsocket在其基礎之上的改進,連結地址

支援SO_REUSEPORT的Tengine

淘寶的Tengine已經支援了SO_REUSEPORT特性,在其測試報告中,有一個簡單測試,可以看出來相對比SO_REUSEPORT所帶來的效能提升:

使用SO_REUSEPORT以後,最明顯的效果是在壓力下不容易出現丟請求的情況,CPU均衡性平穩。

Java支援否?

JDK 1.6語言層面不支援,至於以後的版本,由於暫時沒有使用到,不多說。

Netty 3/4版本預設都不支援SO_REUSEPORT特性,但Netty 4.0.19以及之後版本才真正提供了JNI方式單獨包裝的epoll native transport版本(在Linux系統下執行),可以配置類似於SO_REUSEPORT等(JAVA NIIO沒有提供)選項,這部分是在io.netty.channel.epoll.EpollChannelOption中定義(線上程式碼部分)。

在linux環境下使用epoll native transport,可以獲得核心層面網路堆疊增強的紅利,如何使用可參考Native transports文件。

使用epoll native transport倒也簡單,類名稍作替換:

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

比如寫一個PING-PONG應用伺服器程式,類似程式碼:

public void run() throws Exception {     EventLoopGroup bossGroup = new EpollEventLoopGroup();     EventLoopGroup workerGroup = new EpollEventLoopGroup();     try {         ServerBootstrap b = new ServerBootstrap();         ChannelFuture f = b                 .group(bossGroup, workerGroup)                 .channel(EpollServerSocketChannel.class)                 .childHandler(new ChannelInitializer<SocketChannel>() {                     @Override                     public void initChannel(SocketChannel ch)                             throws Exception {                         ch.pipeline().addLast(                                 new StringDecoder(CharsetUtil.UTF_8),                                 new StringEncoder(CharsetUtil.UTF_8),                                 new PingPongServerHandler());                     }                 }).option(ChannelOption.SO_REUSEADDR, true)                 .option(EpollChannelOption.SO_REUSEPORT, true)                 .childOption(ChannelOption.SO_KEEPALIVE, true).bind(port)                 .sync();         f.channel().closeFuture().sync();     } finally {         workerGroup.shutdownGracefully();         bossGroup.shutdownGracefully();     } }

若不要這麼折騰,還想讓以往Java/Netty應用程式在不做任何改動的前提下順利在Linux kernel >= 3.9下同樣享受到SO_REUSEPORT帶來的好處,不妨嘗試一下bindp,更為經濟,這一部分下面會講到。

bindp,為已有應用新增SO_REUSEPORT特性

以前所寫bindp小程式,可以為已有程式繫結指定的IP地址和埠,一方面可以省去硬編碼,另一方面也為測試提供了一些方便。

另外,為了讓以前沒有硬編碼SO_REUSEPORT的應用程式可以在Linux核心3.9以及之後Linux系統上也能夠得到核心增強支援,稍做修改,新增支援。

但要求如下:

  1. Linux核心(>= 3.9)支援SO_REUSEPORT特性

  2. 需要配置REUSE_PORT=1

不滿足以上條件,此特性將無法生效。

使用示範:

REUSE_PORT=1 BIND_PORT=9999 LD_PRELOAD=./libbindp.so java -server -jar pingpongserver.jar &

當然,你可以根據需要執行命令多次,多個程序監聽同一個埠,單機程序水平擴充套件。

使用示範

使用python指令碼快速構建一個小的示範原型,兩個程序,都監聽同一個埠10000,客戶端請求返回不同內容,僅供娛樂。

server_v1.py,簡單PING-PONG:

# -*- coding:UTF-8 -*-

import socket
import os

PORT = 10000
BUFSIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', PORT)) s.listen(1) while True:     conn, addr = s.accept()     data = conn.recv(PORT)     conn.send('Connected to server[%s] from client[%s]\n' % (os.getpid(), addr))     conn.close() s.close()

server_v2.py,輸出當前時間:

# -*- coding:UTF-8 -*-

import socket
import time
import os

PORT = 10000 BUFSIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', PORT)) s.listen(1) while True:     conn, addr = s.accept()     data = conn.recv(PORT)     conn.send('server[%s] time %s\n' % (os.getpid(), time.ctime()))     conn.close() s.close()

藉助於bindp執行兩個版本的程式:

REUSE_PORT=1 LD_PRELOAD=/opt/bindp/libindp.so python server_v1.py &
REUSE_PORT=1 LD_PRELOAD=/opt/bindp/libindp.so python server_v2.py &

模擬客戶端請求10次:

for i in {1..10};do echo "hello" | nc 127.0.0.1 10000;done

看看結果吧:

Connected to server[3139] from client[('127.0.0.1', 48858)] server[3140] time Thu Feb 12 16:39:12 2015 server[3140] time Thu Feb 12 16:39:12 2015 server[3140] time Thu Feb 12 16:39:12 2015 Connected to server[3139] from client[('127.0.0.1', 48862)] server[3140] time Thu Feb 12 16:39:12 2015 Connected to server[3139] from client[('127.0.0.1', 48864)] server[3140] time Thu Feb 12 16:39:12 2015 Connected to server[3139] from client[('127.0.0.1', 48866)] Connected to server[3139] from client[('127.0.0.1', 48867)]

可以看出來,CPU分配很均衡,各自分配50%的請求量。

嗯,雖是小玩具,有些意思 :))

 

 

SO_REUSADDR VS SO_REUSEPORT

因為能力有限,還是有很多東西(SO_REUSEADDR和SO_REUSEPORT的區別等)沒有能夠在一篇文字中表達清楚,作為補遺,也方便以後自己回過頭來複習。

兩者不是一碼事,沒有可比性。有時也會被其搞暈,自己總結的不好,推薦StackOverflow的Socket options SO_REUSEADDR and SO_REUSEPORT, how do they differ?資料,總結的很全面。

簡單來說:

  • 設定了SO_REUSADDR的應用可以避免TCP 的 TIME_WAIT 狀態 時間過長無法複用埠,尤其表現在應用程式關閉-重啟交替的瞬間

  • SO_REUSEPORT更強大,隸屬於同一個使用者(防止埠劫持)的多個程序/執行緒共享一個埠,同時在核心層面替上層應用做資料包程序/執行緒的處理均衡

若有困惑,推薦兩者都設定,不會有衝突。

Netty多執行緒使用SO_REUSEPORT

上一篇講到SO_REUSEPORT,多個程繫結同一個埠,可以根據需要控制程序的數量。這裡講講基於Netty 4.0.25+Epoll navtie transport在單個程序內多個執行緒繫結同一個埠的情況,也是比較實用的。

TCP伺服器,同一個程序多執行緒繫結同一個埠

這是一個PING-PONG示範應用:

     public void run() throws Exception {             final EventLoopGroup bossGroup = new EpollEventLoopGroup();             final EventLoopGroup workerGroup = new EpollEventLoopGroup();             ServerBootstrap b = new ServerBootstrap();            b.group(bossGroup, workerGroup)                      .channel(EpollServerSocketChannel. class)                      .childHandler( new ChannelInitializer<SocketChannel>() {                             @Override                             public void initChannel(SocketChannel ch) throws Exception {                                 ch.pipeline().addLast(                                             new StringDecoder(CharsetUtil.UTF_8 ),                                             new StringEncoder(CharsetUtil.UTF_8 ),                                             new PingPongServerHandler());                            }                      }).option(ChannelOption. SO_REUSEADDR, true)                      .option(EpollChannelOption. SO_REUSEPORT, true)                      .childOption(ChannelOption. SO_KEEPALIVE, true);             int workerThreads = Runtime.getRuntime().availableProcessors();            ChannelFuture future;

 

            //new  thread            for ( int i = 0; i < workerThreads; ++i) {
                future = b.bind( port).await();
                 if (!future.isSuccess())
                      throw new Exception(String. format("fail to bind on port = %d.",                                  port), future.cause());            }            Runtime. getRuntime().addShutdownHook (new Thread(){                  @Override                  public void run(){                      workerGroup.shutdownGracefully();                      bossGroup.shutdownGracefully();                 }            });      }

 

打成jar包,在CentOS 7下面執行,檢查同一個埠所開啟的檔案控制代碼。

# lsof -i:8000
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    3515 root   42u  IPv6  29040      0t0  TCP *:irdmi (LISTEN) java    3515 root   43u  IPv6  29087      0t0  TCP *:irdmi (LISTEN) java    3515 root   44u  IPv6  29088      0t0  TCP *:irdmi (LISTEN) java    3515 root   45u  IPv6  29089      0t0  TCP *:irdmi (LISTEN)

 

同一程序,但開啟的檔案控制代碼是不一樣的。

UDP伺服器,多個執行緒綁同一個埠

/**
 * UDP諺語伺服器,單程序多執行緒繫結同一埠示範
 */
public final class QuoteOfTheMomentServer {        private static final int PORT = Integer.parseInt(System. getProperty("port" ,                    "9000" ));        public static void main(String[] args) throws Exception {              final EventLoopGroup group = new EpollEventLoopGroup();             Bootstrap b = new Bootstrap();             b.group(group).channel(EpollDatagramChannel. class)                         .option(EpollChannelOption. SO_REUSEPORT, true )                         .handler( new QuoteOfTheMomentServerHandler());             int workerThreads = Runtime.getRuntime().availableProcessors();              for (int i = 0; i < workerThreads; ++i) {                   ChannelFuture future = b.bind( PORT).await();                    if (!future.isSuccess())                          throw new Exception(String.format ("Fail to bind on port = %d.",                                      PORT), future.cause());             }             Runtime. getRuntime().addShutdownHook(new Thread() {                    @Override                    public void run() {                         group.shutdownGracefully();                   }             });       } } } @Sharable class QuoteOfTheMomentServerHandler extends             SimpleChannelInboundHandler<DatagramPacket> {        private static final String[] quotes = {                    "Where there is love there is life." ,                    "First they ignore you, then they laugh at you, then they fight you, then you win.",                    "Be the change you want to see in the world." ,                    "The weak can never forgive. Forgiveness is the attribute of the strong.", };        private static String nextQuote() {              int quoteId = ThreadLocalRandom.current().nextInt( quotes .length );              return quotes [quoteId];       }        @Override        public void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet)                    throws Exception {              if ("QOTM?" .equals(packet.content().toString(CharsetUtil. UTF_8))) {                   ctx.write( new DatagramPacket(Unpooled.copiedBuffer( "QOTM: "                               + nextQuote(), CharsetUtil. UTF_8), packet.sender()));             }       }        @Override        public void channelReadComplete(ChannelHandlerContext ctx) {             ctx.flush();       }        @Override        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {             cause.printStackTrace();       } }

 

同樣也要檢測一下埠檔案控制代碼開啟情況:

# lsof -i:9000
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    3181 root   26u  IPv6  27188      0t0  UDP *:cslistener java    3181 root   27u  IPv6  27217      0t0  UDP *:cslistener java    3181 root   28u  IPv6  27218      0t0  UDP *:cslistener java    3181 root   29u  IPv6  27219      0t0  UDP *:cslistener

 

小結

以上為Netty+SO_REUSEPORT多執行緒繫結同一埠的一些情況,是為記載。