前言
本篇用于記錄學(xué)習(xí)SO_REUSEPORT的筆記和心得,末尾還會(huì)提供一個(gè)bindp小工具也能為已有的程序享受這個(gè)新的特性。
當(dāng)前Linux網(wǎng)絡(luò)應(yīng)用程序問(wèn)題
運(yùn)行在Linux系統(tǒng)上網(wǎng)絡(luò)應(yīng)用程序,為了利用多核的優(yōu)勢(shì),一般使用以下比較典型的多進(jìn)程/多線程服務(wù)器模型:
- 單線程listen/accept,多個(gè)工作線程接收任務(wù)分發(fā),雖CPU的工作負(fù)載不再是問(wèn)題,但會(huì)存在:
- 單線程listener,在處理高速率海量連接時(shí),一樣會(huì)成為瓶頸
- CPU緩存行丟失套接字結(jié)構(gòu)(socket structure)現(xiàn)象嚴(yán)重
- 所有工作線程都accept()在同一個(gè)服務(wù)器套接字上呢,一樣存在問(wèn)題:
- 多線程訪問(wèn)server socket鎖競(jìng)爭(zhēng)嚴(yán)重
- 高負(fù)載下,線程之間處理不均衡,有時(shí)高達(dá)3:1不均衡比例
- 導(dǎo)致CPU緩存行跳躍(cache line bouncing)
- 在繁忙CPU上存在較大延遲
上面模型雖然可以做到線程和CPU核綁定,但都會(huì)存在:
- 單一listener工作線程在高速的連接接入處理時(shí)會(huì)成為瓶頸
- 緩存行跳躍
- 很難做到CPU之間的負(fù)載均衡
- 隨著核數(shù)的擴(kuò)展,性能并沒(méi)有隨著提升
比如HTTP CPS(Connection Per Second)吞吐量并沒(méi)有隨著CPU核數(shù)增加呈現(xiàn)線性增長(zhǎng):

Linux kernel 3.9帶來(lái)了SO_REUSEPORT特性,可以解決以上大部分問(wèn)題。
SO_REUSEPORT解決了什么問(wèn)題
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支持多個(gè)進(jìn)程或者線程綁定到同一端口,提高服務(wù)器程序的性能,解決的問(wèn)題:
- 允許多個(gè)套接字 bind()/listen() 同一個(gè)TCP/UDP端口
- 每一個(gè)線程擁有自己的服務(wù)器套接字
- 在服務(wù)器套接字上沒(méi)有了鎖的競(jìng)爭(zhēng)
- 內(nèi)核層面實(shí)現(xiàn)負(fù)載均衡
- 安全層面,監(jiān)聽(tīng)同一個(gè)端口的套接字只能位于同一個(gè)用戶(hù)下面
其核心的實(shí)現(xiàn)主要有三點(diǎn):
- 擴(kuò)展 socket option,增加 SO_REUSEPORT 選項(xiàng),用來(lái)設(shè)置 reuseport。
- 修改 bind 系統(tǒng)調(diào)用實(shí)現(xiàn),以便支持可以綁定到相同的 IP 和端口
- 修改處理新建連接的實(shí)現(xiàn),查找 listener 的時(shí)候,能夠支持在監(jiān)聽(tīng)相同 IP 和端口的多個(gè) sock 之間均衡選擇。
代碼分析,可以參考引用資料 [多個(gè)進(jìn)程綁定相同端口的實(shí)現(xiàn)分析[Google Patch]]。
CPU之間平衡處理,水平擴(kuò)展
以前通過(guò)fork
形式創(chuàng)建多個(gè)子進(jìn)程,現(xiàn)在有了SO_REUSEPORT,可以不用通過(guò)fork
的形式,讓多進(jìn)程監(jiān)聽(tīng)同一個(gè)端口,各個(gè)進(jìn)程中accept socket fd
不一樣,有新連接建立時(shí),內(nèi)核只會(huì)喚醒一個(gè)進(jìn)程來(lái)accept
,并且保證喚醒的均衡性。
模型簡(jiǎn)單,維護(hù)方便了,進(jìn)程的管理和應(yīng)用邏輯解耦,進(jìn)程的管理水平擴(kuò)展權(quán)限下放給程序員/管理員,可以根據(jù)實(shí)際進(jìn)行控制進(jìn)程啟動(dòng)/關(guān)閉,增加了靈活性。
這帶來(lái)了一個(gè)較為微觀的水平擴(kuò)展思路,線程多少是否合適,狀態(tài)是否存在共享,降低單個(gè)進(jìn)程的資源依賴(lài),針對(duì)無(wú)狀態(tài)的服務(wù)器架構(gòu)最為適合了。
新特性測(cè)試或多個(gè)版本共存
可以很方便的測(cè)試新特性,同一個(gè)程序,不同版本同時(shí)運(yùn)行中,根據(jù)運(yùn)行結(jié)果決定新老版本更迭與否。
針對(duì)對(duì)客戶(hù)端而言,表面上感受不到其變動(dòng),因?yàn)檫@些工作完全在服務(wù)器端進(jìn)行。
服務(wù)器無(wú)縫重啟/切換
想法是,我們迭代了一版本,需要部署到線上,為之啟動(dòng)一個(gè)新的進(jìn)程后,稍后關(guān)閉舊版本進(jìn)程程序,服務(wù)一直在運(yùn)行中不間斷,需要平衡過(guò)度。這就像Erlang語(yǔ)言層面所提供的熱更新一樣。
想法不錯(cuò),但是實(shí)際操作起來(lái),就不是那么平滑了,還好有一個(gè)hubtime開(kāi)源工具,原理為SIGHUP信號(hào)處理器+SO_REUSEPORT+LD_RELOAD
,可以幫助我們輕松做到,有需要的同學(xué)可以檢出試用一下。
SO_REUSEPORT已知問(wèn)題
SO_REUSEPORT根據(jù)數(shù)據(jù)包的四元組{src ip, src port, dst ip, dst port}和當(dāng)前綁定同一個(gè)端口的服務(wù)器套接字?jǐn)?shù)量進(jìn)行數(shù)據(jù)包分發(fā)。若服務(wù)器套接字?jǐn)?shù)量產(chǎn)生變化,內(nèi)核會(huì)把本該上一個(gè)服務(wù)器套接字所處理的客戶(hù)端連接所發(fā)送的數(shù)據(jù)包(比如三次握手期間的半連接,以及已經(jīng)完成握手但在隊(duì)列中排隊(duì)的連接)分發(fā)到其它的服務(wù)器套接字上面,可能會(huì)導(dǎo)致客戶(hù)端請(qǐng)求失敗,一般可以使用:
- 使用固定的服務(wù)器套接字?jǐn)?shù)量,不要在負(fù)載繁忙期間輕易變化
- 允許多個(gè)服務(wù)器套接字共享TCP請(qǐng)求表(Tcp request table)
- 不使用四元組作為Hash值進(jìn)行選擇本地套接字處理,挑選隸屬于同一個(gè)CPU的套接字
與RFS/RPS/XPS-mq協(xié)作,可以獲得進(jìn)一步的性能:
- 服務(wù)器線程綁定到CPUs
- RPS分發(fā)TCP SYN包到對(duì)應(yīng)CPU核上
- TCP連接被已綁定到CPU上的線程accept()
- XPS-mq(Transmit Packet Steering for multiqueue),傳輸隊(duì)列和CPU綁定,發(fā)送數(shù)據(jù)
- RFS/RPS保證同一個(gè)連接后續(xù)數(shù)據(jù)包都會(huì)被分發(fā)到同一個(gè)CPU上
- 網(wǎng)卡接收隊(duì)列已經(jīng)綁定到CPU,則RFS/RPS則無(wú)須設(shè)置
- 需要注意硬件支持與否
目的嘛,數(shù)據(jù)包的軟硬中斷、接收、處理等在一個(gè)CPU核上,并行化處理,盡可能做到資源利用最大化。
SO_REUSEPORT不是一貼萬(wàn)能膏藥
雖然SO_REUSEPORT解決了多個(gè)進(jìn)程共同綁定/監(jiān)聽(tīng)同一端口的問(wèn)題,但根據(jù)新浪林曉峰同學(xué)測(cè)試結(jié)果來(lái)看,在多核擴(kuò)展層面也未能夠做到理想的線性擴(kuò)展:

可以參考Fastsocket在其基礎(chǔ)之上的改進(jìn),鏈接地址。
支持SO_REUSEPORT的Tengine
淘寶的Tengine已經(jīng)支持了SO_REUSEPORT特性,在其測(cè)試報(bào)告中,有一個(gè)簡(jiǎn)單測(cè)試,可以看出來(lái)相對(duì)比SO_REUSEPORT所帶來(lái)的性能提升:

使用SO_REUSEPORT以后,最明顯的效果是在壓力下不容易出現(xiàn)丟請(qǐng)求的情況,CPU均衡性平穩(wěn)。
Java支持否?
JDK 1.6語(yǔ)言層面不支持,至于以后的版本,由于暫時(shí)沒(méi)有使用到,不多說(shuō)。
Netty 3/4版本默認(rèn)都不支持SO_REUSEPORT特性,但Netty 4.0.19以及之后版本才真正提供了JNI方式單獨(dú)包裝的epoll native transport版本(在Linux系統(tǒng)下運(yùn)行),可以配置類(lèi)似于SO_REUSEPORT等(JAVA NIIO沒(méi)有提供)選項(xiàng),這部分是在io.netty.channel.epoll.EpollChannelOption
中定義(在線代碼部分)。
在linux環(huán)境下使用epoll native transport,可以獲得內(nèi)核層面網(wǎng)絡(luò)堆棧增強(qiáng)的紅利,如何使用可參考Native transports文檔。
使用epoll native transport倒也簡(jiǎn)單,類(lèi)名稍作替換:
NioEventLoopGroup → EpollEventLoopGroup
NioEventLoop → EpollEventLoop
NioServerSocketChannel → EpollServerSocketChannel
NioSocketChannel → EpollSocketChannel
比如寫(xiě)一個(gè)PING-PONG應(yīng)用服務(wù)器程序,類(lèi)似代碼:
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應(yīng)用程序在不做任何改動(dòng)的前提下順利在Linux kernel >= 3.9下同樣享受到SO_REUSEPORT帶來(lái)的好處,不妨嘗試一下bindp,更為經(jīng)濟(jì),這一部分下面會(huì)講到。
bindp,為已有應(yīng)用添加SO_REUSEPORT特性
以前所寫(xiě)bindp小程序,可以為已有程序綁定指定的IP地址和端口,一方面可以省去硬編碼,另一方面也為測(cè)試提供了一些方便。
另外,為了讓以前沒(méi)有硬編碼SO_REUSEPORT
的應(yīng)用程序可以在Linux內(nèi)核3.9以及之后Linux系統(tǒng)上也能夠得到內(nèi)核增強(qiáng)支持,稍做修改,添加支持。
但要求如下:
- Linux內(nèi)核(>= 3.9)支持SO_REUSEPORT特性
- 需要配置
REUSE_PORT=1
不滿(mǎn)足以上條件,此特性將無(wú)法生效。
使用示范:
REUSE_PORT=1 BIND_PORT=9999 LD_PRELOAD=./libbindp.so java -server -jar pingpongserver.jar &
當(dāng)然,你可以根據(jù)需要運(yùn)行命令多次,多個(gè)進(jìn)程監(jiān)聽(tīng)同一個(gè)端口,單機(jī)進(jìn)程水平擴(kuò)展。
使用示范
使用python腳本快速構(gòu)建一個(gè)小的示范原型,兩個(gè)進(jìn)程,都監(jiān)聽(tīng)同一個(gè)端口10000,客戶(hù)端請(qǐng)求返回不同內(nèi)容,僅供娛樂(lè)。
server_v1.py,簡(jiǎn)單PING-PONG:
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,輸出當(dāng)前時(shí)間:
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運(yùn)行兩個(gè)版本的程序:
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 &
模擬客戶(hù)端請(qǐng)求10次:
for i in {1..10};do echo "hello" | nc 127.0.0.1 10000;done
看看結(jié)果吧:
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)]
可以看出來(lái),CPU分配很均衡,各自分配50%的請(qǐng)求量。
嗯,雖是小玩具,有些意思 :))
bindp的使用方法
更多使用說(shuō)明,請(qǐng)參考README。
參考資料