socket選項 SO_REUSEPORT

前言

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

當前Linux網絡應用程序問題

運行在Linux系統上網絡應用程序,爲了利用多核的優點,通常使用如下比較典型的多進程/多線程服務器模型:java

  1. 單線程listen/accept,多個工做線程接收任務分發,雖CPU的工做負載再也不是問題,但會存在:python

    • 單線程listener,在處理高速率海量鏈接時,同樣會成爲瓶頸linux

    • CPU緩存行丟失套接字結構(socket structure)現象嚴重git

  2. 全部工做線程都accept()在同一個服務器套接字上呢,同樣存在問題:程序員

    • 多線程訪問server socket鎖競爭嚴重github

    • 高負載下,線程之間處理不均衡,有時高達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多線程綁定同一端口的一些狀況,是爲記載。

相關文章
相關標籤/搜索