當(dāng)UDP丟包的時候劫笙,我們正常情況下是增加各種緩沖區(qū)的大小溉躲,有調(diào)整內(nèi)核緩沖區(qū)的,也有調(diào)整應(yīng)用緩沖區(qū)的牍白。但是還有另外一種方式脊凰,就是加速UDP數(shù)據(jù)包的處理速度。
1.當(dāng)前Linux網(wǎng)絡(luò)應(yīng)用程序問題
運行在Linux系統(tǒng)上網(wǎng)絡(luò)應(yīng)用程序淹朋,為了利用多核的優(yōu)勢笙各,一般使用以下比較典型的多進程/多線程服務(wù)器模型:
首先需要單線程listen一個端口上,然后由多個工作進程/線程去accept()在同一個服務(wù)器套接字上础芍。 但有以下兩個瓶頸:
- 單線程listener杈抢,在處理高速率海量連接時,一樣會成為瓶頸
- 多線程訪問server socket鎖競爭嚴重仑性。
那么怎么解決惶楼? 這里先別扯什么分布式調(diào)度,集群xxx的 , 就拿單機來說問題。在Linux kernel 3.9帶來了SO_REUSEPORT特性诊杆,她可以解決上面(單進程listen歼捐,多工作進程accept() )的問題.
如上,SO_REUSEPORT是支持多個進程或者線程綁定到同一端口晨汹,提高服務(wù)器程序的吞吐性能豹储,具體來說解決了下面的幾個問題:
- 允許多個套接字 bind()/listen() 同一個TCP/UDP端口
- 每一個線程擁有自己的服務(wù)器套接字
- 在服務(wù)器套接字上沒有了鎖的競爭,因為每個進程一個服務(wù)器套接字
- 內(nèi)核層面實現(xiàn)負載均衡
- 安全層面淘这,監(jiān)聽同一個端口的套接字只能位于同一個用戶下面
關(guān)于SO_REUSEPORT可以參考這篇文章SO_REUSEPORT學(xué)習(xí)筆記
2.Netty使用SO_REUSEPORT
要想在Netty中使用SO_REUSEPORT特性剥扣,需要滿足以下兩個前提條件
- linux內(nèi)核版本 >= 3.9
- Netty版本 >= 4.0.16
然后只需要兩步就可以使用SO_REUSEPORT特性了。第一步:添加Netty本地庫依賴铝穷。第二步:替換Netty中的Nio組件為原生組件钠怯。第三步:多線程綁定同一個端口
2.1.添加Netty本地庫依賴
Netty官方提供了使用本地庫的說明 Native transports
Netty是通過JNI本地庫的方式來提供的。而且這種本地庫的方式不是Netty核心的一部分曙聂,所以需要有額外依賴
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
...
</build>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<version>${project.version}</version>
<classifier>${os.detected.name}-${os.detected.arch}</classifier>
</dependency>
...
</dependencies>
其中# os-maven-plugin 插件是為了自檢檢測當(dāng)前系統(tǒng)的名稱以及架構(gòu)晦炊。然后自動填充到classifier中的兩個變量 ${os.detected.name} 以及 ${os.detected.arch}。如果是在Linux 64系統(tǒng)宁脊,那么可能的結(jié)果就是os.detected.name=linux,os.detected.arch=x86_64 断国。
由于官網(wǎng)中沒有提供gradle的配置,所以這邊總結(jié)一下gradle的配置
// gradle構(gòu)建配置
buildscript {
// buildscript 加上osdetector的依賴
dependencies {
classpath 'com.google.gradle:osdetector-gradle-plugin:1.6.0'
}
}
// 添加原生依賴
dependencies{
compile group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.22.Final', classifier: osdetector.classifier
}
以上的gradle配置雖然沒什么問題朦佩,但是實際上大多數(shù)開發(fā)者實在Windows上開發(fā)的并思,所以osdetector.classifier=windows.x86_64,而實際上Netty并沒有這樣的組件语稠,所以會編譯報錯宋彼。
所以我的建議是直接寫死osdetector.classifier=linux-x86_64
2.2.替換Netty中的Nio組件為原生組件
直接在Netty啟動類中替換為在Linux系統(tǒng)下的epoll組件
- NioEventLoopGroup → EpollEventLoopGroup
- NioEventLoop → EpollEventLoop
- NioServerSocketChannel → EpollServerSocketChannel
- NioSocketChannel → EpollSocketChannel
如下所示
group = new EpollEventLoopGroup();//NioEventLoopGroup ->EpollEventLoopGroup
bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(EpollDatagramChannel.class) // NioServerSocketChannel -> EpollDatagramChannel
.option(ChannelOption.SO_BROADCAST, true)
.option(EpollChannelOption.SO_REUSEPORT, true) // 配置EpollChannelOption.SO_REUSEPORT
.option(ChannelOption.SO_RCVBUF, 1024 * 1024 * bufferSize)
.handler( new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel)
throws Exception {
ChannelPipeline pipeline = channel.pipeline();
// ....
}
});
不過要注意這些代碼只能在Linux上運行,如果實在windows或者mac上開發(fā)仙畦,那最好還是要換成普通Nio方式的输涕,Netty提供了方法Epoll.isAvailable()來判斷是否可用epoll
所以實際上優(yōu)化的時候需要加上是否支持epoll特性的判斷
group = Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(Epoll.isAvailable() ? EpollDatagramChannel.class : NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.option(ChannelOption.SO_RCVBUF, 1024 * 1024)
.handler( new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel)
throws Exception {
ChannelPipeline pipeline = channel.pipeline();
}
});
// linux平臺下支持SO_REUSEPORT特性以提高性能
if (Epoll.isAvailable()) {
bootstrap.option(EpollChannelOption.SO_REUSEPORT, true);
}
2.3. 多線程綁定同一個端口
使用原生epoll組件替換nio原來的組件后,需要多次綁定同一個端口慨畸。
if (Epoll.isAvailable()) {
// linux系統(tǒng)下使用SO_REUSEPORT特性莱坎,使得多個線程綁定同一個端口
int cpuNum = Runtime.getRuntime().availableProcessors();
log.info("using epoll reuseport and cpu:" + cpuNum);
for (int i = 0; i < cpuNum; i++) {
ChannelFuture future = bootstrap.bind(UDP_PORT).await();
if (!future.isSuccess()) {
throw new Exception("bootstrap bind fail port is " + UDP_PORT);
}
}
}
3.測試
3.1優(yōu)化前
我們使用大概17萬的QPS來壓測我們的UDP服務(wù)
可以發(fā)現(xiàn)最終丟棄了一部分UDP。
下面再來看一下運行期間的CPU分布寸士¢苁玻可以看到其中一個線程占用99%的CPU碴卧。
我們來看一下是哪一個線程。
[root@localhost ~]# printf "%x\n" 1983
7bf
然后使用jstack命令dump出線程乃正∽〔幔可以看到是處理UDP的連接的線程比較繁忙,導(dǎo)致在高QPS的情況下處理不過來瓮具,從而丟包荧飞。
3.2優(yōu)化后
使用epoll優(yōu)化后,在啟動的時候有一些錯誤信息值得關(guān)注名党。
03:23:06.155 [main] DEBUG io.netty.util.internal.NativeLibraryLoader - Unable to load the library 'netty_transport_native_epoll_x86_64', trying other loading mechanism.
java.lang.UnsatisfiedLinkError: no netty_transport_native_epoll_x86_64 in java.library.path
...
03:23:06.155 [main] DEBUG io.netty.util.internal.NativeLibraryLoader - netty_transport_native_epoll_x86_64 cannot be loaded from java.libary.path, now trying export to -Dio.netty.native.workdir: /tmp
java.lang.UnsatisfiedLinkError: no netty_transport_native_epoll_x86_64 in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
... 18 common frames omitted
03:23:06.174 [main] DEBUG io.netty.util.internal.NativeLibraryLoader - Successfully loaded the library /tmp/libnetty_transport_native_epoll_x86_647320427488873314678.so
初看上去好像是啟動出錯了叹阔,但是再細看實際上沒什么問題。因為其實上面的日志只是在說netty在加載本地庫的時候有優(yōu)先級传睹。前兩次加載失敗了耳幢,最后一次加載成功了。所以這段時間可以忽略蒋歌。關(guān)于這個問題github上也有人提出了issue帅掘。可以關(guān)注一下When netty_transport_native_epoll_x86_64 cannot be found, stacktrace is logged
我們同樣適用大概17萬的QPS來壓測我們的UDP服務(wù)
可以看到?jīng)]有丟包
我們再來看一下接受連接的線程所占的CPU
可以看到同時有4個線程負責(zé)處理UDP連接堂油。其中3個線程比較繁忙修档。
可能是因為QPS還不夠高,所以4個線程中只有3個比較繁忙府框,剩余一個幾乎不占用CPU吱窝。但是由于單機Jmeter能轟出的UDP QPS有限(我本機大概在17萬左右),所以暫時無法測試迫靖。后續(xù)我們可以使用分布式j(luò)meter來測試院峡,敬請期待。
3.3測試結(jié)論
使用SO_REUSEPORT優(yōu)化后系宜,不但性能提升了照激,而且CPU占用更加均衡,在一定程度上性能和CPU個數(shù)成正相關(guān)