本篇用於記錄學習SO_REUSEPORT的筆記和心得,末尾還會提供一個bindp小工具也能爲已有的程序享受這個新的特性。html
運行在Linux系統上網絡應用程序,爲了利用多核的優點,通常使用如下比較典型的多進程/多線程服務器模型:java
單線程listen/accept,多個工做線程接收任務分發,雖CPU的工做負載再也不是問題,但會存在:python
單線程listener,在處理高速率海量鏈接時,同樣會成爲瓶頸linux
CPU緩存行丟失套接字結構(socket structure)現象嚴重git
全部工做線程都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特性,能夠解決以上大部分問題。
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]]。
之前經過fork
形式建立多個子進程,如今有了SO_REUSEPORT,能夠不用經過fork
的形式,讓多進程監聽同一個端口,各個進程中accept socket fd
不同,有新鏈接創建時,內核只會喚醒一個進程來accept
,而且保證喚醒的均衡性。
模型簡單,維護方便了,進程的管理和應用邏輯解耦,進程的管理水平擴展權限下放給程序員/管理員,能夠根據實際進行控制進程啓動/關閉,增長了靈活性。
這帶來了一個較爲微觀的水平擴展思路,線程多少是否合適,狀態是否存在共享,下降單個進程的資源依賴,針對無狀態的服務器架構最爲適合了。
能夠很方便的測試新特性,同一個程序,不一樣版本同時運行中,根據運行結果決定新老版本更迭與否。
針對對客戶端而言,表面上感覺不到其變更,由於這些工做徹底在服務器端進行。
想法是,咱們迭代了一版本,須要部署到線上,爲之啓動一個新的進程後,稍後關閉舊版本進程程序,服務一直在運行中不間斷,須要平衡過分。這就像Erlang語言層面所提供的熱更新同樣。
想法不錯,可是實際操做起來,就不是那麼平滑了,還好有一個hubtime開源工具,原理爲SIGHUP信號處理器+SO_REUSEPORT+LD_RELOAD
,能夠幫助咱們輕鬆作到,有須要的同窗能夠檢出試用一下。
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解決了多個進程共同綁定/監聽同一端口的問題,但根據新浪林曉峯同窗測試結果來看,在多核擴展層面也未可以作到理想的線性擴展:
能夠參考Fastsocket在其基礎之上的改進,連接地址。
淘寶的Tengine已經支持了SO_REUSEPORT特性,在其測試報告中,有一個簡單測試,能夠看出來相對比SO_REUSEPORT所帶來的性能提高:
使用SO_REUSEPORT之後,最明顯的效果是在壓力下不容易出現丟請求的狀況,CPU均衡性平穩。
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小程序,能夠爲已有程序綁定指定的IP地址和端口,一方面能夠省去硬編碼,另外一方面也爲測試提供了一些方便。
另外,爲了讓之前沒有硬編碼SO_REUSEPORT
的應用程序能夠在Linux內核3.9以及以後Linux系統上也可以獲得內核加強支持,稍作修改,添加支持。
但要求以下:
Linux內核(>= 3.9)支持SO_REUSEPORT特性
須要配置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_REUSEADDR和SO_REUSEPORT的區別等)沒有可以在一篇文字中表達清楚,做爲補遺,也方便之後本身回過頭來複習。
二者不是一碼事,沒有可比性。有時也會被其搞暈,本身總結的很差,推薦StackOverflow的Socket options SO_REUSEADDR and SO_REUSEPORT, how do they differ?資料,總結的很全面。
簡單來講:
設置了SO_REUSADDR的應用能夠避免TCP 的 TIME_WAIT 狀態 時間過長沒法複用端口,尤爲表如今應用程序關閉-重啓交替的瞬間
SO_REUSEPORT更強大,隸屬於同一個用戶(防止端口劫持)的多個進程/線程共享一個端口,同時在內核層面替上層應用作數據包進程/線程的處理均衡
如有困惑,推薦二者都設置,不會有衝突。
上一篇講到SO_REUSEPORT,多個程綁定同一個端口,能夠根據須要控制進程的數量。這裏講講基於Netty 4.0.25+Epoll navtie transport
在單個進程內多個線程綁定同一個端口的狀況,也是比較實用的。
這是一個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諺語服務器,單進程多線程綁定同一端口示範 */ 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多線程綁定同一端口的一些狀況,是爲記載。