因为能力有限,还是有很多东西(so_reuseaddr和so_reuseport的区别等)没有能够在一篇文字中表达清楚,作为补遗,也方便以后自己回过头来复习。
两者不是一码事,没有可比性。有时也会被其搞晕,自己总结的不好,推荐stackoverflow的资料,总结的很全面。
简单来说:
若有困惑,推荐两者都设置,不会有冲突。
上一篇讲到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;
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多线程绑定同一端口的一些情况,是为记载。
本篇用于记录学习so_reuseport的笔记和心得,末尾还会提供一个bindp小工具也能为已有的程序享受这个新的特性。
运行在linux系统上网络应用程序,为了利用多核的优势,一般使用以下比较典型的多进程/多线程服务器模型:
上面模型虽然可以做到线程和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支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:
其核心的实现主要有三点:
代码分析,可以参考引用资料 [多个进程绑定相同端口的实现分析[google patch]]。
以前通过fork
形式创建多个子进程,现在有了so_reuseport,可以不用通过fork
的形式,让多进程监听同一个端口,各个进程中accept socket fd
不一样,有新连接建立时,内核只会唤醒一个进程来accept
,并且保证唤醒的均衡性。
模型简单,维护方便了,进程的管理和应用逻辑解耦,进程的管理水平扩展权限下放给程序员/管理员,可以根据实际进行控制进程启动/关闭,增加了灵活性。
这带来了一个较为微观的水平扩展思路,线程多少是否合适,状态是否存在共享,降低单个进程的资源依赖,针对无状态的服务器架构最为适合了。
可以很方便的测试新特性,同一个程序,不同版本同时运行中,根据运行结果决定新老版本更迭与否。
针对对客户端而言,表面上感受不到其变动,因为这些工作完全在服务器端进行。
想法是,我们迭代了一版本,需要部署到线上,为之启动一个新的进程后,稍后关闭旧版本进程程序,服务一直在运行中不间断,需要平衡过度。这就像erlang语言层面所提供的热更新一样。
想法不错,但是实际操作起来,就不是那么平滑了,还好有一个开源工具,原理为sighup信号处理器 so_reuseport ld_reload
,可以帮助我们轻松做到,有需要的同学可以检出试用一下。
so_reuseport根据数据包的四元组{src ip, src port, dst ip, dst port}和当前绑定同一个端口的服务器套接字数量进行数据包分发。若服务器套接字数量产生变化,内核会把本该上一个服务器套接字所处理的客户端连接所发送的数据包(比如三次握手期间的半连接,以及已经完成握手但在队列中排队的连接)分发到其它的服务器套接字上面,可能会导致客户端请求失败,一般可以使用:
与rfs/rps/xps-mq协作,可以获得进一步的性能:
目的嘛,数据包的软硬中断、接收、处理等在一个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,可以获得内核层面网络堆栈增强的红利,如何使用可参考文档。
使用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() {
@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带来的好处,不妨尝试一下,更为经济,这一部分下面会讲到。
以前所写小程序,可以为已有程序绑定指定的ip地址和端口,一方面可以省去硬编码,另一方面也为测试提供了一些方便。
另外,为了让以前没有硬编码so_reuseport
的应用程序可以在linux内核3.9以及之后linux系统上也能够得到内核增强支持,稍做修改,添加支持。
但要求如下:
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%的请求量。
嗯,虽是小玩具,有些意思 :))
更多使用说明,请参考。
前面啰啰嗦嗦的几篇文字,各个方面介绍了fastsocket,盲人摸象一般,能力有限,还得继续深入学习不是。这不,到了该小结收尾的时候了。
使用linux作为服务器,在请求量很小的时候,是不用担心其性能。但在海量的数据请求下,linux内核在tcp/ip网络处理方面,已经成为瓶颈。比如新浪在某台haproxy服务器上取样,90%的cpu时间被内核占用,应用程序只能够分配到较少的cpu时钟周期的资源。
经过haproxy系统详尽分析后,发现大部分cpu资源消耗在kernel里,并且在多核平台下,kernel在网络协议栈处理过程中存在着大量同步开销。
同时在多核上进行测试,http cps(connection per second)吞吐量并没有随着cpu核数增加呈现线性增长:
tcp处理&多核
linux vfs的同步损耗严重
cpu之间不共享数据,并行化各自独立处理tcp连接,也是其高效的主要原因。其架构图可以看出其改进:
fastsocket架构图可以很清晰说明其大致结构,内核态和用户态通过ioctl
函数传输。记得netmap在重写网卡驱动里面通过ioctl
函数直接透传到用户态中,其更为高效,但没有完整的tcp/ip网络堆栈支持嘛。
在新浪测试中,在24核的安装有centos 6.5的服务器上,借助于fastsocket,nginx和haproxy每秒处理连接数指标(connection/second)性能很惊人,分别增加290%和620%。这也证明了,fastsocket带来了tcp连接快速处理的能力。 除此之外,借助于硬件特性:
fastsocket v1.0正式版从2014年3月份开始已经在新浪生产环境中使用,用作代理服务器,因此大家可以考虑是否可以采用。针对1.0版本,以下环境较为收益:
多线程嘛,就得需要参考示范应用所提供实践建议了。
从下表测试图片中,可以看到:
测试结果中:
8核服务器线上环境运行了24小时的成绩,图a展示了部署fastsocket之前cpu利用率,图b为部署了fastsocekt之后的cpu利用率。 fastsocket带来的收益:
其实吧,这一块期待新浪公布更多的数据。
长连接支持,还是需要等一等的。但是要支持什么类型长连接?百万级别应用服务器类型,还是redis,可能是后者。虽然目前正做,但目前没有时间表,但目前所做特性总结如下:
测试环境:
redis配置选项:
测试结果:
但需要注意:
v1.1版本要增加长连接的支持,那么类似于redis的服务器应用程序就很受益了,因为没有具体的时间表,只能够慢慢等待了。
说是对比,其实是我从mtcp论文中摘取出来,增加了fastsocket一栏,可以看出人们一直努力的脚步。
types | accept queue | conn. locality | socket api | event handling | packet i/o | application mod- ification | kernel modification |
psio , dpdk , pf ring , netmap | no tcp stack | batched | no interface for transport layer | no (nic driver) | |||
linux-2.6 | shared | none | bsd socket | syscalls | per packet | transparent | no |
linux-3.9 | per-core | none | bsd socket | syscalls | per packet | add option so reuseport | no |
affinity-accept | per-core | yes | bsd socket | syscalls | per packet | transparent | yes |
megapipe | per-core | yes | lwsocket | batched syscalls | per packet | event model to completion i/o | yes |
flexsc,vos | shared | none | bsd socket | batched syscalls | per packet | change to use new api | yes |
mtcp | per-core | yes | user-level socket | batched function calls | batched | socket api to mtcp api | no (nic driver) |
fastsocket | per-core | yes | bsd socket | ioctl kernel calls | per packet | transparent | no |
有一个大致的印象,也方便对比,但这只能是一个暂时的摘要而已,人类对性能的渴求总是朝着更好的方向发展着。
怎么说呢,fastsocket是为大家耳熟能详服务器程序nginx,haproxy等而开发的。但若应用环境为大量的短连接,并且是小文件类型请求,不需要强制支持keep-alive特性(短连接要的是快速请求-相应,然后关闭),那么管理员可以尝试一下fastsocket,至于部署策略,选择性部署几台作为实验看看结果。
本系列到此算是告一段落啦。以后呢,自然是希望fastsocket尽快发布对长连接的支持,还有更高性能的提升咯 :))
前面分析fastsocket慢慢凑成了几篇烂文字,要把一件事情坚持做下来,有时味同爵蜡,但既然选择了,也得硬着头皮做下去。闲话少说,文归正文。本文接自上篇内核模块篇,继续记录学习fastsocket内核的笔记内容。
linux kernel 3.9包含tcp/udp支持多进程、多线程绑定同一个ip和端口的特性,即so_reuseport
;在内核层面同时也让线程/进程之间各自独享socket,避免cpu核之间以锁资源争夺accept queue
的调用。在fastsocket/kernel/net/sock.h定义sock_common
结构时,可以看到其身影:
unsigned char skc_reuse:4;
unsigned char skc_reuseport:4;
在多个socket.h文件中(比如fastsocket/kernel/include/asm/socket.h),定义了so_reusesort的变量值:
#define so_reuseport 15
在fastsocket/kernel/net/core/sock.c的sock_setsockopt和sock_getsockopt函数中,都有so_reuseport
的身影:
sock_setsockopt函数中:
case so_reuseaddr:
sk->sk_reuse = valbool;
break;
case so_reuseport:
sk->sk_reuseport = valbool;
break;
sock_getsockopt函数体中:
case so_reuseaddr:
v.val = sk->sk_reuse;
break;
case so_reuseport:
v.val = sk->sk_reuseport;
break;
在so_reuseport
特性支持之前的事件驱动驱动服务器资源竞争:
之后呢,可以看做是并行的了:
fastsocket没有重复发明轮子,在so_reuseport
基础上进行进一步的优化等。
嗯,后面准备写一个动态链接库小程序,打算让以前的没有硬编码so_reuseport
的程序也能够在linux kernel >= 3.9系统上享受真正的端口重用的新特性的支持。
下面按照其架构图所示内核层面从上到下一一列出。
因为linux kernel vfs的同步损耗严重
提交记录:
a209dfc vfs: dont chain pipe/anon/socket on superblock s_inodes list
4b93688 fs: improve scalability of pseudo filesystems
对vfs的改进,在所提升的性能中占有超过60%的比例,效果非常明显:
对于多核多接收队列来说,linux原生的协议栈只能listen在一个socket上面,并且所有完成三次握手还没来得及被应用accept的套接字都会放入其附带的accept队列中,accept系统调用必须串行的从队列取出,当并发量较大时多核竞争,这将成为性能瓶颈,影响建立连接处理速度。
local listen table,fastsocket为每一个cpu核克隆监听套接字,并保存到其本地表中,cpu核之间不会存在accept的竞争关系。下面为引用描述内容:
使用流程图概括上面所述:
linux内核使用一个全局的hash表以及锁操作来维护establised sockets(被用来跟踪连接的sockets)。fastsocket 想法是把全局table分散到per-core table,当一个core需要访问socket的时候,只在隶属于自己的table中搜索,因此不需要锁操纵,也不存在资源竞争。由fastsocket建立的socket本地local established table中,其他的regular sockets保存在global的table中。core首先去自己的local table中查找(不需要锁),然后去global中查找。
默认情况下,应用程序主动发包的时候,发出去的包是通过正在执行本进程的那个cpu 核(系统分配的)来完成的;而接收数据包的时cpu 核是由前面提到的rss或rps来传递。这样一来,连接可能由不同的两个cpu核来完成。连接应该在本地化处理。rfs和intel网卡的flowdirector可以从软件和硬件上缓解这种情况,但是不完备。
rfd(receive flow deliver)主要的思想是cpu核数主动发起连接的时候可以把cpu core的标识和连接的source port编码到一起。cpu cores和ports的关系由一个关系集合来决定【cores,ports】, 对于一个port,有唯一的一个core与之对应。当一个core来建立connection的时候,rfd随机选择一个跟当前core匹配的port。接收包的时候,rfd负责决定这个包应该让哪一个core来处理,如果当前core不是被选中的cpu core,那么就deliver到选中的cpu core。
一般来说,rfd对代理程序收益比较大,单纯的web服务器可以选择禁用。
以上参考了大量的外部资料进行整理而成,进而可以获得一个较为整体的fastsocket内核架构印象。
fastsocket的努力,在单个tcp连接的管理从网卡触发的硬中断、软中断、三次握手、数据传输、四次挥手等完整的过程在完整在一个cpu核上进行处理,从而实现了每一个cpu核心tcp资源本地化,这样为多核水平扩展打好了基础,减少全局资源竞争,平行化处理连接,同时降低文件锁的副作用,做到了极为高效的短连接处理方案,不得不赞啊。
本篇学习fastsocket内核模块fastsocket.so
,作为用户态libfsocket.so
的内核态的支持,处理ioctl
传递到/dev/fastsocket
的数据,非常核心和基础。嗯,还是先翻译,随后挟带些点评进来。
fastsocket内核模块 (fastsocket.ko
) 提供若干特性,并各自具有开启和关闭等丰富选项可配置。
centos 6.5带来的内核锁竞争处处可见,导致无论如何优化tcp/ip网络堆栈都不能够带来很好的性能扩展。比较严重锁竞争例子,inode_lock
和dcache_lock
,针对套接字文件系统sockfs而言,并不是必须。fastsocket通过在vfs初始化结构时提供fastpath快速路径用以解决此项问题,已经向代号为香草(vanilla)的内核提交了两处修改:
a209dfc vfs: dont chain pipe/anon/socket on superblock s_inodes list
4b93688 fs: improve scalability of pseudo filesystems
此项修改没有提供选项可供配置,因此所有fastsocket创建的套接字sockets都会强制经由fastpath传输。
fastsocket为每个cpu创建了一个本地socket监听表(local listen table),应用程序可以决定在一个特定cpu内核上处理某个新的连接,具体就是通过拷贝原始监听套接字socket,然后插入到本地套接字socket监听表中。当新建连接在某cpu处理时,系统内核尝试匹配本地socket监听表,匹配成功会插入到本地accept队列中。稍后,cpu会从本地accept队列中获取进行处理。
这种方式每一个网络软中断都会有隶属于自己本地套接字队列当新的连接进来时可以压入,每一个进程从本地队列中弹出连接进行处理。当进程和cpu进行绑定,一旦有网卡接口决定投递到某个cpu内核上,那么包括硬中断、软中断、系统调用以及用户进程,都会有这个cpu全程负责。好处就是客户端请求连接在没有锁的竞争环境下分散到各个cpu上被动处理本地连接。
本特性更适合以下情况:
第一种情况下,rps可以在网卡接收队列小于cpu核数时被使用。第二种方案可以满足两个方面:
因此,enable_listen_spawn
具有三个值可供配置:
一旦开启,需要为文件结构额外添加一字段用以保存文件与epitem的映射关系,这样可省去在epoll_ctl
方法被调用时从epoll红黑树查找epitem的开销。
虽然此项优化有所修改epoll语义,但带来了套接字性能提升。开启的前提是一个套接字只允许添加到一个epoll实例中,但不包括监听套接字。默认值为true可以适用于绝大多数应用程序,若你的程序不满足条件就得需要禁用了。
enable_fast_epoll 为布尔型boolean选项:
rfd(receive flow deliver)会把为新建连接分配的cpu id封装到其连接的端口号中,而不是随机选择新创建的主动连接的源端口进行分配到cpu上。
当应用从活动连接收到数据包rfd解码时,会从目的地端口上解析出对应的cpu内核id,继而转发给对应的cpu内核。再加上listen_spawn,保证了一个连接cpu处理的完全本地化。
enable_receive_flow是一个布尔型选项:
注意事项:
以上,翻译完毕。
fastsocket的内核模块相对路径为fastsocket/module/,除了readme.md外,就是两个软连接文件了:
换种说法,fastsocket内核模块真正路径为fastsocket/kernel/net/fastsocket
,具体文件列表为:
fastsocket_api.c实现内核模块接口,在源码里面注册了好多文档暂时没有公开的可配置项目:
int enable_fastsocket_debug = 3;
/* fastsocket feature switches */
int enable_listen_spawn = 2;
int enable_receive_flow_deliver;
int enable_fast_epoll = 1;
int enable_skb_pool;
int enable_rps_framework;
int enable_receive_cpu_selection = 0;
int enable_direct_tcp = 0;
int enable_socket_pool_size = 0;
module_param(enable_fastsocket_debug,int, 0);
module_param(enable_listen_spawn, int, 0);
module_param(enable_receive_flow_deliver, int, 0);
module_param(enable_fast_epoll, int, 0);
module_param(enable_direct_tcp, int, 0);
module_param(enable_skb_pool, int, 0);
module_param(enable_receive_cpu_selection, int, 0);
module_param(enable_socket_pool_size, int, 0);
module_parm_desc(enable_fastsocket_debug, " debug level [default: 3]" );
module_parm_desc(enable_listen_spawn, " control listen-spawn: 0 = disabled, 1 = process affinity required, 2 = autoset process affinity[default]");
module_parm_desc(enable_receive_flow_deliver, " control receive-flow-deliver: 0 = disabled[default], 1 = enabled");
module_parm_desc(enable_fast_epoll, " control fast-epoll: 0 = disabled, 1 = enabled[default]");
module_parm_desc(enable_direct_tcp, " control direct-tcp: 0 = disbale[default], 1 = enabled");
module_parm_desc(enable_skb_pool, " control skb-pool: 0 = disbale[default], 1 = receive skb pool, 2 = send skb pool, 3 = both skb pool");
module_parm_desc(enable_receive_cpu_selection, " control rcs: 0 = disabled[default], 1 = enabled");
module_parm_desc(enable_socket_pool_size, "control socket pool size: 0 = disabled[default], other are the pool size");
接收用户态的libfsocket.so通过ioctl传递过来的数据,根据命令进行数据分发:
static long fastsocket_ioctl(struct file *filp, unsigned int cmd, unsigned long __user u_arg)
{
struct fsocket_ioctl_arg k_arg;
if (copy_from_user(&k_arg, (struct fsocket_ioctl_arg *)u_arg, sizeof(k_arg))) {
eprintk_limit(err, "copy ioctl parameter from user space to kernel failed\n");
return -efault;
}
switch (cmd) {
case fsocket_ioc_socket:
return fastsocket_socket(&k_arg);
case fsocket_ioc_listen:
return fastsocket_listen(&k_arg);
case fsocket_ioc_spawn_listen:
return fastsocket_spawn_listen(&k_arg);
case fsocket_ioc_accept:
return fastsocket_accept(&k_arg);
case fsocket_ioc_close:
return fastsocket_close(&k_arg);
case fsocket_ioc_shutdown_listen:
return fastsocket_shutdown_listen(&k_arg);
//case fsocket_ioc_epoll_ctl:
// return fastsocket_epoll_ctl((struct fsocket_ioctl_arg *)arg);
default:
eprintk_limit(err, "ioctl [%d] operation not support\n", cmd);
break;
}
return -einval;
}
fastsocket/library/libsocket.h
头文件定义的fsocket_ioc_*
操作状态码就能够一一对应的上。 ioctl
传输数据从用户态->内核态,需要经过一次拷贝过程(copy_from_user
),然后根据cmd命令进行功能路由。
通过指定的设备通道/dev/fastsocket进行交互:
/dev/fastsocket
/dev/fastsocket
设备获得文件句柄,开始ioctl
数据传递 简单梳理了fastsocket内核模块,但一样有很多的点没有涉及,后面可能会在fastsocket内核篇中再次梳理一下。
本篇为fastsocket的动态链接库学习笔记,对应源码目录为 fastsocket/library,先翻译readme.md文件内容,后面添加上个人学习心得。
动态链接库libfsocket.so
,为已有应用程序提供加速服务,具有可维护性和兼容性。
很简单,进入目录之后,执行make
命令编译即可:
cd fastsocket/library
make
最后在当前目录下生成libfsocket.so
文件。
很简单的说,借助于ld_preload
加载libfsocket.so
,启动应用程序,以nginx为例:
ld_preload=/your_path/fastsocket/library/libfsocket.so nginx
若回滚,就简单了,直接启动nginx就行:
nginx
注意事项:
fastsocket.ko
内核模块已经加载成功
libfsocket.so
的上层应用程序有效果 fastsocket拦截网络套接字的常规系统调用,并使用ioctl接口取代之。
若不依赖于libfsocket.so
,上层应用程序要想使用fastsocket percore-listen-table的特点,应用程序需要在父流程forking之后,以及提前做事件循环(event loop)处理,应用工作进程需要手动调用listen_spawn
函数,复制全局的监听套接字并插入到本地监听表中。
libfsocket.so
为上层应用程序做了listien_spawn
的工作,用以保持应用程序的代码不变,方法如下:
libfsocket.so
跟踪所有需要监听的套接字文件句柄
libfsocket.so
拦截了epoll_ctl
系统调用
epoll_ctl
添加监听套接字文件句柄到epoll时,libfsocket.so
会调用listen_spawn
方法 不是所有应用程序都适合本方案,但nginx、haproxy、lighttpd与之配合就工作得相当不错。因此当你在其他应用程序中想使用percore-listen-table特性时,请务必小心测试了,确保是否合适。
ok,翻译完毕。
fastsocket/library用于构建libfsocket.so
动态链接库,主要组成:
定义了ioctl
(为input/output control缩写)函数和伪设备(/dev/fastsocket
)交换数据所使用到的几个命令:
#define ioc_id 0xf5
#define fsocket_ioc_socket _io(ioc_id, 0x01)
#define fsocket_ioc_listen _io(ioc_id, 0x02)
#define fsocket_ioc_accept _io(ioc_id, 0x03)
#define fsocket_ioc_close _io(ioc_id, 0x04)
//#define fsocket_ioc_epoll_ctl _io(ioc_id, 0x05)
#define fsocket_ioc_spawn_listen _io(ioc_id, 0x06)
#define fsocket_ioc_shutdown_listen _io(ioc_id, 0x07)
紧接着定义了需要在用户态和内核态通过ioctl
进行交互的结构:
struct fsocket_ioctl_arg {
u32 fd;
u32 backlog;
union ops_arg {
struct socket_accept_op_t {
void *sockaddr;
int *sockaddr_len;
int flags;
}accept_op;
struct spawn_op_t {
int cpu;
}spawn_op;
struct io_op_t {
char *buf;
u32 buf_len;
}io_op;
struct socket_op_t {
u32 family;
u32 type;
u32 protocol;
}socket_op;
struct shutdown_op_t {
int how;
}shutdown_op;
struct epoll_op_t {
u32 epoll_fd;
u32 size;
u32 ep_ctl_cmd;
u32 time_out;
struct epoll_event *ev;
}epoll_op;
}op;
};
这样看来,ioctl
函数原型调用为:
ioctl(/dev/fastsocket设备文件句柄, fsocket_ioc_具体宏命令, fsocket_ioctl_arg结构指针)
现在大致能够弄清楚了内核态和用户态之间通过ioctl
传递结构化的数据的方式了。
连接内核模块已经注册好的设备管道/dev/fastsocket
,获取到文件描述符,同时做些cpu进程绑定的工作
#define init_fdset_num 65536
......
__attribute__((constructor))
void fastsocket_init(void)
{
int ret = 0;
int i;
cpu_set_t cmask;
ret = open("/dev/fastsocket", o_rdonly); // 建立fastsocket通道
if (ret < 0) {
fsocket_err("open fastsocket channel failed, please check\n");
/* just exit for safty*/
exit(-1);
}
fsocket_channel_fd = ret;
fsocket_fd_set = calloc(init_fdset_num, sizeof(int));
if (!fsocket_fd_set) {
fsocket_err("allocate memory for listen fd set failed\n");
exit(-1);
}
fsocket_fd_num = init_fdset_num; // 值为65535
cpu_zero(&cmask);
for (i = 0; i < get_cpus(); i )
cpu_set(i, &cmask);
ret = sched_setaffinity(0, get_cpus(), &cmask);
if (ret < 0) {
fsocket_err("clear process cpu affinity failed\n");
exit(-1);
}
return;
}
主观上,仅仅是为了短连接而设置的,定义的fastsocket文件句柄数组大小为65535,针对类似于web server、http api等环境足够了,针对百万级别的长连接服务器环境就不适合了。
socket/listen/accept/close/shutdown/epoll_ctl等函数,通过dlsym
方式替换已有套接字系统函数等,具体的交互过程使用ioctl
替代一些系统调用。
除了重写socket/listen/accept/close/shutdown等套接字接口,同时也对epoll_ctl
方法动了手术(江湖传言cpu多核多进程的epoll服务器存在惊群现象),更好利用多核:
int epoll_ctl(int efd, int cmd, int fd, struct epoll_event *ev)
{
static int (*real_epoll_ctl)(int, int, int, struct epoll_event *) = null;
int ret;
struct fsocket_ioctl_arg arg;
if (fsocket_channel_fd >= 0) {
arg.fd = fd;
arg.op.spawn_op.cpu = -1;
/* "automatically" do the spawn */
if (fsocket_fd_set[fd] && cmd == epoll_ctl_add) {
ret = ioctl(fsocket_channel_fd, fsocket_ioc_spawn_listen, &arg);
if (ret < 0) {
fsocket_err("fsocket: spawn failed!\n");
}
}
}
if (!real_epoll_ctl)
real_epoll_ctl = dlsym(rtld_next, "epoll_ctl");
ret = real_epoll_ctl(efd, cmd, fd, ev);
return ret;
}
因为定义了作用于内部的静态变量real_epoll_ctl
,只有在第一次加载的时候才会被赋值,real_epoll_ctl = dlsym(rtld_next, "epoll_ctl")
,后面调用时通过ioctl
把fsocket_ioctl_arg传递到内核模块中去。
其它socket/listen/accept/close/shutdown等套接字接口,流程类似。
以上简单翻译、粗略分析用户态fastsocket动态链接库大致情况,若要起作用,需要和内核态fastsocket进行交互、传递数据才能够作用的很好。
前面编译安装好了包含有fastsocket的内核模块,以及fastsocket的动态链接库libfsocket.so,下面其实就可以设置网卡了。
下面为一些名词解释,上下文中需要使用到:
本文网卡设置笔记内容,大部分来自于fastsocket源码相对路径fastsocket/scripts/
;老规矩,先翻译。
nic.sh
脚本负责网卡配置以尽可能的最大化受益于fastsocket带来的问题。给定一个网卡接口, 它调整接口的各种特性以及一些系统配置。
每个网卡硬件队列及其关联中断绑定到不同的cpu核心。若硬件队列数大于cpu核数,队列需要配置成循环round-robin方式, irqbalance服务需要被禁用以防其更改配置。
nic.sh
脚本通过ethtool
命令设置每秒中断数上限,防止中断风暴。两个rx中断间隔设置成至少333us,约3000个中断每秒。
为每个cpu核心与不同的网卡硬件队列之间建立一一映射对应关系,这样cpu核心就可以很均匀地处理网络数据包。当网卡硬件队列小于cpu内核数,nic.sh
脚本利用rps (receive packet steering)软件方式平衡进入流量负载,这样cpu和硬件队列不存在对应关系。rps机制可以让进入的数据包自由分发到任一cpu核上。
网卡接收产生的中断可以均衡分配到对应cpu上。
xps (transmit packet steering) 建立cpu内核和tx发送队列映射对应关系,掌控出站数据包。系统有n个cpu核心,脚本会设置xps至少存在n个tx队列在网卡接口上,这样就可以建立cpu内核和tx队列1对1的映射关系。
网卡传送数据产生的中断一样可以均很分配到cpu上,避免单个cpu核心过于繁忙。
压测时,防火墙iptables的规则会占用更多的cpu周期,有所降低网络堆栈性能。因此nic.sh
脚本若检测到iptables后台运行中会直接输出报警信息,提示关闭之。
nic.sh
脚本脚本分析经过验证好用的intel和博通系列千兆和万兆网卡列表:
# igb
"intel corporation 82576 gigabit network connection (rev 01)"
"intel corporation i350 gigabit network connection (rev 01)"
# ixgbe
"intel corporation 82599eb 10-gigabit sfi/sfp network connection (rev 01)"
"intel corporation 82599es 10-gigabit sfi/sfp network connection (rev 01)"
# tg3
"broadcom corporation netxtreme bcm5720 gigabit ethernet pcie"
"broadcom corporation netxtreme bcm5761 gigabit ethernet pcie (rev 10)"
# bnx2
"broadcom corporation netxtreme ii bcm5708 gigabit ethernet (rev 12)"
"broadcom corporation netxtreme ii bcm5709 gigabit ethernet (rev 20)"
若当前服务器没有以上网卡,会警告一下,无碍。
这里把一些常规性的cpu、网卡驱动、网络队列情况检查单独抽取出来,重温好多已经遗忘的命令,有改变,这样写较简单嘛,便于以后使用:
grep -c processor /proc/cpuinfo
ls /sys/class/net/eth0/queues | grep -c rx
ls /sys/class/net/eth0/queues | grep -c tx
egrep -c eth0 /proc/interrupts
lspci | grep ethernet | sed "s/ethernet controller: //g"
ethtool -i eth0 | grep driver
脚本先是获取cpu、网卡等信息,接着设置中断单位秒内吞吐量: ethtool -c eth0 rx-usecs 333 > /dev/null 2>&1
启用xps,充分借助网卡发送队列,提升网卡发送吞吐量,是有条件限制的,发送队列数要大于cpu核数:
if [[ $tx_queues -ge $cores ]]; then
for i in $(seq 0 $((cores-1))); do
cpuid_to_mask $((i%cores)) | xargs -i echo {} > /sys/class/net/$iface/queues/tx-$i/xps_cpus
done
info_msg " xps enabled"
fi
接着判断是否可以启用prs,省去手动设置的麻烦,但启用rps前提是cpu核数与网卡硬件队列不相等:
if [[ ! $hw_queues == $cores ]]; then
for i in /sys/class/net/$iface/queues/rx-*; do
printf "%x\n" $((2**cores-1)) | xargs -i echo {} > $i/rps_cpus;
done
info_msg " rps enabled"
else
for i in /sys/class/net/$iface/queues/rx-*; do
echo 0 > $i/rps_cpus;
done
info_msg " rps disabled"
fi
若没有使用fastsocket,单纯借助于rps,会带来处理中断的cpu和处理当前数据包的cpu不是同一个,自然会造成cpu cache miss(cpu缓存丢失),造成少许的性能影响,为了避免这种情况,人们会依赖于rfs(receive flow steering)。
使用了fastsocket后,就不用这么麻烦了。
irqbalance和fastsocket有冲突,会强制禁用:
if ps aux | grep irqbalance | grep -v grep; then
info_msg "disable irqbalance..."
# xxx do we have a more moderate way to do this?
killall irqbalance > /dev/null 2>&1
fi
脚本也包含了设置中断和cpu的亲和性:
i=0
intr_list $iface $driver | while read irq; do
cpuid_to_mask $((i%cores)) | xargs -i echo {} > /proc/irq/$irq/smp_affinity
i=$((i 1))
done
若iptables服务存在,会友善建议禁用会好一些,毕竟会带来性能损耗。文件打开句柄不大于1024,脚本同样会提醒,怎么设置文件打开句柄,可以参考以前博文。
针对不使用fastsocket的服务器,当前比较流行的针对网卡的网络堆栈性能扩展、优化措施,一般会使用到rss、rps、rfs、xfs等方式,以便充分利用cpu多核和硬件网卡等自身性能,达到并行/并发处理的目的。下面总结一个表格,可以凑合看一下。
rss (receive side scaling) |
rps (receive packet steering) |
rfs (receive flow steering) |
accelerated rfs (accelerated receive flow steering) |
xps (transmit packet steering) | |
---|---|---|---|---|---|
解决问题 | 网卡和驱动支持 | 软件方式实现rss | 数据包产生的中断和应用处理在同一个cpu上 | 基于rfs硬件加速的负载平衡机制 | 智能选择网卡多队列的队列快速发包 |
内核支持 | 2.6.36开始引入,需要硬件支持 | 2.6.35 | 2.6.35 | 2.6.35 | 2.6.38 |
建议 | 网卡队列数和物理核数一直 | 至此多队列的网卡若rss已经配置了,则不需要rps了 | 需要rps_sock_flow_entries和rps_flow_cnt属性 | 需要网卡设备和驱动都支持加速。并且要求ntuple过滤已经通过ethtool启用 | 单传输队列的网卡无效,若队列比cpu少,共享指定队列的cpu最好是与处理传输硬中断的cpu共享缓存的cpu |
fastsocket | 网卡特性 | 改进版rps,性能提升 | 源码包含,文档没有涉及 | 文档没有涉及 | 要求发送队列数要大于cpu核数 |
传送方向 | 网卡接收 | 内核接收 | cpu接收处理 | 加速并接收 | 网卡发送数据 |
更具体优化措施,可以参考文档:。
另,若网卡支持flow director filters
特性(这里有一个非常有趣的动画介绍,,值得一看),那么可以结合fastsocket一起加速。比如,在其所作redis长连接测试中,启用flow-director特性要比禁用可以带来25%的性能提升。
自然软硬结合,可以做的更好一些嘛。
延伸阅读:
以上记录了学习fastsocket的网卡设置脚本方面笔记。
不过呢,nic.sh
脚本,值得收藏,无论使不使用fastsocket,对线上服务器网卡调优都是不错选择哦。
运行环境为centos 6.5系统,默认内核为2.6.32-431.el6.x86_64,下面所有编译安装操作是以root
用户权限进行操作。
第一步需要下载代码,当然这是废话了,下载到/opt目录下:
git clone https://github.com/fastos/fastsocket.git
下载之后,需要进入其目录中:
cd fastsocket/kernel
因为是涉及到内核嘛,编译之前需要做一些参数选项配置,使用make config
会累死人的,好几千个选项参数需要你一一配置,大部分时间,默认配置就挺好的:
make defconfig
然后嘛,编译内核的节奏:
make
内核编译相当耗费时间,至少20分钟时间。之后紧接着是编译所需的内核模块,fastsocket模块:
make modules_install
编译完成之后,最后一条输出,会看到:
depmod 2.6.32-431.17.1.el6.fastsocket
fastsocket内核模块编译好之后,需要安装内核:
make install
上面命令其实执行shell脚本进行安装:
sh /opt/fastsocket/kernel/arch/x86/boot/install.sh 2.6.32-431.17.1.el6.fastsocket arch/x86/boot/bzimage \ system.map "/boot"
基本上,fastsocket内核模块已经构建安装完毕了,但需要告知linux系统在下次启动的时候切换到新编译的、包含有fastsocket模块的内核。
这部分需要在/etc/grup.conf中配置,现在看一下其文件内容:
default=1
timeout=5
splashimage=(hd0,0)/grub/splash.xpm.gz
hiddenmenu
title centos (2.6.32-431.17.1.el6.fastsocket)
root (hd0,0)
kernel /vmlinuz-2.6.32-431.17.1.el6.fastsocket ro root=/dev/mapper/vg_centos6-lv_root rd_no_luks rd_no_md rd_lvm_lv=vg_centos6/lv_swap crashkernel=auto lang=zh_cn.utf-8 rd_lvm_lv=vg_centos6/lv_root keyboardtype=pc keytable=us rd_no_dm rhgb quiet
initrd /initramfs-2.6.32-431.17.1.el6.fastsocket.img
title centos (2.6.32-431.el6.x86_64)
root (hd0,0)
kernel /vmlinuz-2.6.32-431.el6.x86_64 ro root=/dev/mapper/vg_centos6-lv_root rd_no_luks rd_no_md rd_lvm_lv=vg_centos6/lv_swap crashkernel=auto lang=zh_cn.utf-8 rd_lvm_lv=vg_centos6/lv_root keyboardtype=pc keytable=us rd_no_dm rhgb quiet
initrd /initramfs-2.6.32-431.el6.x86_64.img
defautl=1
,表示目前系统选择的以原先内核作作为启动项,原先位于第二个root (hd0,0)
后面,需要切换到新的内核下面,需要修改default=0
,保存后,reboot重启系统,使之生效。
系统重启后,需要加载fastsocket模块到系统运行中去,下面以默认选项参数方式加载:
modprobe fastsocket
加载之后,列出当前系统所加载模块列表,检查是否成功
lsmod | grep fastsocket
若能看到类似输出信息,表示ok:
fastsocket 39766 0
上面内核模块安装好之后,可以构建fastsocket的动态链接库文件了:
cd /opt/fastsocket/library/
make
可能会收到一些警告信息,无碍:
gcc -g -shared -ldl -fpic libsocket.c -o libfsocket.so -wall
libsocket.c: 在函数‘fastsocket_init’中:
libsocket.c:59: 警告:隐式声明函数‘open’
libsocket.c: 在函数‘fastsocket_expand_fdset’中:
libsocket.c:109: 警告:隐式声明函数‘ioctl’
libsocket.c: 在函数‘accept’中:
libsocket.c:186: 警告:对指针赋值时目标与指针符号不一致
libsocket.c: 在函数‘accept4’中:
libsocket.c:214: 警告:对指针赋值时目标与指针符号不一致
最后,可以看到gcc编译之后生成的libfsocket.so
库文件,说明编译成功。
ok,编译安装到此结束,后面就是如何使用fastsocket的示范程序进行测试了。
上篇介绍了如何构建安装fastsocket内核模块,下面将基于fastsocket/demo/readme.md
文件翻译整理而成。
嗯,下面进入翻译篇。
示范为一个简单tcp server服务器程序,用于基准测试和剖析liunx内核网络堆栈性能表现,当然也是为了演示fastsocket可扩展和其性能改进。
示范应用基于epoll模型和非阻塞性io,处理网络连接,但只有在多核的模式下才能够工作得很好:程序的每一个进程被绑定到cpu的不同核,起始于cpu core 0,各自独立处理客户端连接请求。
示范程序具有两种工作模式:
这是一个简单傻瓜形式的tcp server,仅仅用于测试使用,使用时要求客户端和服务器端只能够携带一个packet包大小的数据,否则程序会处理不了。
以下面方式进行构建:
cd demo && make
最简单方式以默认配置无参数形式运行:
./server
参数如下:
在运行之前,需要注意两点:
script
目录 服务器模式至少需要两台主机:
设定每台主机cpu 12核,网络大概设置如下:
-------------------- --------------------
| host a | | host b |
| | | |
| 10.0.0.1/24 |-----| 10.0.0.2/24 |
| | | |
-------------------- --------------------
下面是运行两台主机的步骤:
主机b:
web服务器模式单独运行,开启12个工作进程,和cpu核心数一致:
./server -w 12 -a 10.0.0.2:80
或者测试借助于fastsocket所带来的性能
ld_preload=../library/libfsocket.so ./server -w 12 -a 10.0.0.2:80
主机a:
ab -n 1000000 -c 100 http://10.0.0.2:80/
n=12; for i in $(seq 1 $n); do ab -n 1000000 -c 100 http://10.0.0.2:80/ > /dev/null 2>&1; done
代理模式下,需要三台机器:
设定每台机器cpu内核数12,网络结构如下:
-------------------- -------------------- --------------------
| host a | | host b | | host c |
| | | | | |
| 10.0.0.1/24 | | 10.0.0.2/24 | | 10.0.0.3/24 |
------------------- ------------------- -------------------
| | |
-----------------------------------------------------------------------
| switch |
--------------------------------------------------------------------------
下面为具体的运行步骤:
主机b:
./server -w 12 -a 10.0.0.2:80 -x 10.0.0.3:80
ld_preload=../library/libsocket.so ./server -w 12 -a 10.0.0.2:80 -x 10.0.0.3:80
主机c:
./server -w 12 -a 10.0.0.3:80
主机a:
n=12; for i in $(seq 1 $n); do ab -n 1000000 -c 100 http://10.0.0.2:80/ > /dev/null 2>&1; done
以上翻译完毕,下面将是根据上面内容进行动手测试描述吧。
检查一下包含apache ab命令的软件包:
yum provides /usr/bin/ab
可以看到类似于如下字样:
httpd-tools-2.2.15-39.el6.centos.x86_64 : tools for use with the apache http server
安装它就可以了
yum install httpd-tools
windows 7专业版跑vmware workstation 10.04虚拟机,两个centos 6.5系统,配置一致,2g内存,8个cpu逻辑处理器核心。
客户端安装apache ab命令测试,跑8个实例: for i in $(seq 1 8); do ab -n 10000 -c 100 http://192.168.192.16:80/ > /dev/null 2>&1; done
服务器端,分别记录:/opt/fast/server -w 8
ld_preload=../library/libfsocket.so ./server -w 8
两组数据对比:
运行方式 | 处理消耗时间(秒) | 处理总数 | 平均每秒处理数 | 最大值 |
---|---|---|---|---|
单独运行 | 34s | 80270 | 2361 | 2674 |
加载fasocket | 28s | 80399 | 2871 | 2964 |
测试方式如上,三台服务器(测试端 代理端 服务器端)配置一样。第一次代理单独启动,第二次代理预加载fastsocket方式。
运行方式 | 处理消耗时间(秒) | 处理总数 | 平均每秒处理数 | 最大值 |
---|---|---|---|---|
第一次测试后端 | 44s | 80189 | 1822 | 2150 |
第一次测试代理 | 44s | 80189 | 1822 | 2152 |
第二次测试后端 | 42s | 80051 | 1906 | 2188 |
第二次测试代理 | 42s | 80051 | 1906 | 2167 |
备注:虚拟机上数据,不代表真实服务器上数据,仅供参考。
虽然基于虚拟机,测试环境受限,但一样可以看到基于fastsocket服务器模型,处理性能有所提升:总体处理时间,每秒平均处理数,以及处理上限等。
动态链接预先加载ld_preload虽是利器,但不是万能药,ld_preload遇到下面情况会失效:
情况很复杂,小心为上。
学习并测试了fastsocket的源码示范部分,前后对比可以看到fastsocket带来了处理性能的提升。
以前在infoq上看到fastsocket的宣传,明白了fastsocket是什么:
开源协议为gplv2
总之很吸引人,从内核层面进行优化tcp/ip网络堆栈,上层网络应用程序不用做修改,就可以得到处理性能的提升,很赞!
近期有点小空闲,开始对fastsocket进行关注,虽然资料不多,但也记录了几篇连续的学习笔记。大部分笔记,思路主要是优先翻译官方文档,紧接着会夹带些个人一些学习笔记。
fastsocket项目地址是:,其wiki和代码是本系列笔记主要来源。一开始想进一步全面认知fastsocket,发现无从下手,只能从侧面开始一一旁敲侧击,逐渐加深。本系列笔记根据其源码目录结构划分特性,分开记录学习:
怎么说呢,能力有限,若发现问题/纰漏,请帮忙及时指正,不胜感激。
代码贡献者,除了之外,目前提交最为频繁的是同学,其博客地址为,也是一位牛人。
优秀的开源项目,总是可以吸引到最优秀的开发者。
网络延迟是客观存在的,但网络游戏行业已经积累了大量优质经验,使用一些策略、技术手段在客户端消除/隐藏掉延迟带来的不便,以尽可能的掩盖实际存在的延时,同时实现实时渲染,将用户带入快速的交互式实时游戏中,体验完美的互动娱乐中。
这样处理结果,稍高延迟的玩家也不会因为网络不是那么好,也能够很和谐的与其它网络参差不及玩家一起游戏中。
虽然延时决定了实时游戏的最低反应时间,但最重要的是客户端看起来要流畅。第一人称设计游戏(fps)可巧妙的化解与规避,最终在适合普遍用户网络环境中(200ms),实现实时快速互动游戏。
嗯,下面就是近期脑补结果。
早先网游使用p2p网络拓扑在玩家之间进行交换数据通信。但p2p模型引起的高延迟在fps游戏中无法被很好掩盖,所有玩家的延迟取决于当前玩家中延迟最烂的那个。好比木桶理论,低延迟网络好的玩家会被高延迟坏网络的玩家拖累。最终结果导致,所有玩家都不太开心了。但在局域网环境下,不会感觉到延迟带来的问题。另,游戏逻辑大部分都集中在客户端了,很难避免作弊行为。
c/s结构网游:
服务器可以允许某些情况下客户端本地即时执行移动操作,这种方法可以称为客户端预测。
比如游戏中键盘控制角色行走,这个时候可以在很小的时间段(时间很短,比如1-3秒)内预测用户行动轨迹(方向 加速度,角色行走结果),这部分的命令客户端会全部发送到服务器端校验正确与否(避免瞬间转移等外挂)。但客户端预测有时也不是百分百准确,需要服务器进行纠正(所谓服务器就是上帝,the sever is the man!)。纠正结果可能就是游戏角色行走轨迹和客户端预测轨迹有所偏差,客户端可以使用插值方式(粗略来讲,就是角色在两点之间移动渲染的方式)渲染游戏角色在游戏世界中的位置转移平滑一些,避免游戏角色从一个位置瞬间拉回到另一个位置,让人有些莫名其妙。
插值,有人也称之为路径补偿,都是一回事。插值的方法会涉及到很多数学公式,线性插值、三次线性插值等,比如这篇文章所讲到的。
小结:客户端预测,服务器端纠正,客户端采用插值方式微调。
针对交互的一群玩家,网络好坏层次不齐,游戏的一些操作效果可能需要”延迟补偿“策略进行
延迟补偿是游戏服务器端执行的一种策略,处理用户命令回退到客户端发送命令的准确时间(延迟导致),根据客户端的具体情况进行修正,以牺牲游戏在伤害判定方面的真实感来弥补攻击行为等方面真实感,本质上是一种折中选择。
主要注意,延迟补偿不是发生在客户端。
关于延迟补偿的一个例子:
若游戏延迟补偿被禁用,那么就会有许多玩家抱怨自己明明打中了对方却没有造成任何伤害。。
有所得,有所失:但这对低延时玩家貌似有些不公平,移动速度快,可能已经跑到角落里并且已蹲在一个箱子后面隐藏起来时被对手击中的错觉(子弹无视掩体,玩家隔着墙被射击),确实有些不乐意。
延迟补偿,网络高延迟的玩家有利,低延迟的玩家优势可能会被降低(低延迟玩家利益受损),但对维护游戏世界的平衡还是有利的。
客户端和服务器需要对时,互相知道彼此延迟情况,比如云风定义的某个步骤:
客户端发送一个本地时间量给服务器,服务收到包后,夹带一个服务器时间返回给客户端。当客户端收到这个包后,可以估算出包在路程上经过的时间。同时把本地新时间夹带进去,再次发送给服务器。服务器也可以进一步的了解响应时间。
c/s两端通过类似步骤进行计算彼此延时/时差,同时会对实时同步设置一个阀值,比如对延迟低于10ms(0.01秒)的交互认为是即时同步发生,不会认为是延迟。
不同类型的游戏会钟爱不同的协议呢,不一而足:
tcp会认定丢包是因为本地带宽不足导致(本地带宽不足是丢包的一部分原因),但国内isp可能会在自身机房网络拥挤时丢弃数据包,这时候可能需要快速发包争抢通道,而非tcp窗口收缩,udp没有tcp窗口收缩的负担,可以很容易做到这一点。
要求实时性放在第一位的fps游戏(eg:quake,cs),广域网一般采用udp,因可容许有丢失数据包存在(另客户端若等待一段时间中间丢包,可以通过插值等手段忽略掉),一旦检测到可以快速发送,另不涉及到重发的时候udp比tcp要快一点嘛。但会在udp应用层面有所增加协议控制,比如ack等。
很多时候协议混用,比如mmo客户端也许首先使用http去获取上一次的更新内容, 重要信息如角色获得的物品和经验需要通过tcp传输,而周围人物的动向、npc移动、技能动画指令等则可以使用udp传输,虽然可能丢包,但影响不大。
网游通过客户端预测、插值和服务器端延迟补贴等,化解/消除用户端网络延迟造成的停顿。我们虽然可能没有机会接触游戏开发,学习跨界的优良经验和实践,说不准会对当前工作某些业务点的处理有所启发呢。
本集由韩国宇航局赞助播出:我们要去远方看看,还有什么是我们的思密达。 ------ 《万万没想到》王大锤
说是android网络调试,其实也不过是在被root后android系统操作,使用adb shell执行一些常规的终端命令,检测2g/3g/4g/wifi网络等,进而确定一些因网络等导致的问题而已。但adb shell默认没有几个支持的命令,比如 cat
, tcpdump
,这些都是最基本的必备命令,也不支持。对于想要查看网络请求有几次跳转,不借助些外力,确实是件很不可能的事情。
基本将会包含如下内容:
- 如何安装需要的linux终端命令tcpdump,mtr
- 调试2g/3g等网络连通,域名请求跳转
- 请求丢包情况
说它是神器,一点都不夸张。homepage((http://dan.drown.org/android/))上开篇明义:
unix command-line programs ported to run on android. this project uses opkg, which handles downloading and installing packages and their dependencies (like yum or apt). source for all packages are available.
作者dan (http://blog.dan.drown.org/)为我们移植到android平台,并且还为我们编译好相当多的常用程序,具体支持列表,可从changelog
(http://dan.drown.org/android/)中找到,这里不再累述。
十分难得,由衷感谢。
预先把依赖下载到本地:
http://dan.drown.org/android/system/xbin/busybox
http://dan.drown.org/android/opkg.tar.gz
设安装到android手机的 /data/local 目录,那么首先需要确保这个目录具有可读写权限。
记得要使用su命令切换到root管理员账户,操作、权限才不会受阻。
adb shell chmod 777 /data/local
拷贝opkg到/data/local目录
adb push busybox /data/local
adb push opkg.tar.gz /data/local
adb shell进去之后,开始编译安装:
cd /data/local
chmod 777 busybox
./busybox tar zxvf opkg.tar.gz
设置环境变量:
export path=$path:/data/local/bin
执行更新、安装准备
opkg update
opkg install opkg
opkg list # 可以查看可以支持安装的终端应用程序(命令)
话说,opkg可以应用于各种嵌入式环境中,超强的说。
可以一口气安装几个试试:
opkg install mtr curl tcpdump cat
当然,你也可以一个一个安装。
安装好之后呢,就是直接运行应用/命令了,测试baidu.com域名解析、丢包情况。
mtr -r baidu.com host: localhost loss% snt last
avg best wrst stdev
1.|-- ??? 100.0 10 0.0 0.0 0.0 0.0 0.0
2.|-- 192.168.61.1 0.0% 10 504.3 635.0 339.3 1024. 238.7
3.|-- 192.168.63.138 0.0% 10 392.9 588.7 298.5 847.7 220.3
4.|-- 221.130.39.106 0.0% 10 340.9 557.3 257.4 823.5 211.7
5.|-- 221.179.159.45 10.0% 10 649.6 631.4 332.6 821.4 165.0
6.|-- 111.13.14.6 10.0% 10 561.9 551.3 268.2 777.0 170.0
7.|-- 111.13.0.162 10.0% 10 510.6 570.6 385.5 767.6 116.6
8.|-- 111.13.1.14 10.0% 10 775.4 565.2 377.7 775.4 130.9
9.|-- 111.13.2.130 10.0% 10 707.2 564.6 381.1 887.3 173.4
嗯,通过mtr确实很容易就看出,网络跳数,每一个节点丢包率。这样就能很容易找到在移动2g/3g网络连接超时比较严重的问题所在。下面就是希望运维的同学尽快处理好,避免再次出现由联通机房再次跳转到移动机房问题。
非常感谢陈杰同学推荐的比ping traceroute还要好用命令mtr。一旦拥有,不会放手!
要想抓取2g/3g网络下数据包,必须安装一个tcpdump命令:
opkg install tcpdump
opkg很贴心的会把所依赖的libpcap也都一并安装上,完全不用担心版本问题!
tcpdump -i any -p -vv -s 0 -w /sdcard/capture.pcap
下面就是一气呵成的导出,使用wireshark进行分析了。
adb pull /sdcard/tmp1.pcap c:/tmp
不习惯使用终端诊断网络,可以直接使用现成的app。
有更好的app推荐,欢迎推荐一二。
线上情况:
改进工作:
实际效果:
一般命令前缀若添加上m字符串,表示支持多个、批量命令提交了。
显式的...
mset key value [key value ...]
msetnx key value [key value ...]
hmget key field [field ...]
hmset key field value [field value ...]
一般方式的...
hdel key field [field ...]
srem key member [member ...]
rpush key value [value ...]
......
更多,请参考:
官方文档:
一般业务、接入前端请求量过大,生产者速度过快,这时候使用队列暂时缓存会比较好一些,消费者直接直接从队列获取任务,通过队列让生产者和消费者进行分离这也是业界普通采用的方式。
有的时候,若可以监控一下队列消费情况,可以监控一下,就很直观。同事为队列添加了一个监控线程,清晰明了了解队列消费情况。
示范使用了redis pipeline,线程池,准备数据,生产者-消费者队列,队列监控等,消费完毕,程序关闭。
/**
* 以下测试在jedis 2.6下测试通过
*
* @author nieyong
*
*/
public class testjedispipeline {
private static final int num = 512;
private static final int max = 1000000; // 100w
private static jedispool redispool;
private static final executorservice pool = executors.newcachedthreadpool();
protected static final blockingqueue queue = new arrayblockingqueue(
max); // 100w
private static boolean finished = false;
static {
jedispoolconfig config = new jedispoolconfig();
config.setmaxactive(64);
config.setmaxidle(64);
try {
redispool = new jedispool(config, "192.168.192.8", 6379, 10000,
null, 0);
} catch (exception e) {
system.err.println("init msg redis factory error! " e.tostring());
}
}
public static void main(string[] args) throws interruptedexception {
system.out.println("prepare test data 100w");
preparetestdata();
system.out.println("prepare test data done!");
// 生产者,模拟请求100w次
pool.execute(new runnable() {
@override
public void run() {
for (int i = 0; i < max; i ) {
if (i % 3 == 0) {
queue.offer("del_key key_" i);
} else {
queue.offer("get_key key_" i);
}
}
}
});
// cpu核数*2 个工作者线程
int threadnum = 2 * runtime.getruntime().availableprocessors();
for (int i = 0; i < threadnum; i )
pool.execute(new consumertask());
pool.execute(new monitortask());
thread.sleep(10 * 1000);// 10sec
system.out.println("going to shutdown server ...");
setfinished(true);
pool.shutdown();
pool.awaittermination(1, timeunit.milliseconds);
system.out.println("colse!");
}
private static void preparetestdata() {
jedis redis = redispool.getresource();
pipeline pipeline = redis.pipelined();
for (int i = 0; i < max; i ) {
pipeline.set("key_" i, (i * 2 1) "");
if (i % (num * 2) == 0) {
pipeline.sync();
}
}
pipeline.sync();
redispool.returnresource(redis);
}
// queue monitor,生产者-消费队列监控
private static class monitortask implements runnable {
@override
public void run() {
while (!thread.interrupted() && !isfinished()) {
system.out.println("queue.size = " queue.size());
try {
thread.sleep(500); // 0.5 second
} catch (interruptedexception e) {
break;
}
}
}
}
// consumer,消费者
private static class consumertask implements runnable {
@override
public void run() {
while (!thread.interrupted() && !isfinished()) {
if (queue.isempty()) {
try {
thread.sleep(100);
} catch (interruptedexception e) {
}
continue;
}
list tasks = new arraylist(num);
queue.drainto(tasks, num);
if (tasks.isempty()) {
continue;
}
jedis jedis = redispool.getresource();
pipeline pipeline = jedis.pipelined();
try {
list> resultlist = new arraylist>(
tasks.size());
list waitdeletelist = new arraylist(
tasks.size());
for (string task : tasks) {
string key = task.split(" ")[1];
if (task.startswith("get_key")) {
resultlist.add(pipeline.get(key));
waitdeletelist.add(key);
} else if (task.startswith("del_key")) {
pipeline.del(key);
}
}
pipeline.sync();
// 处理返回列表
for (int i = 0; i < resultlist.size(); i ) {
resultlist.get(i).get();
// handle value here ...
// system.out.println("get value " value);
}
// 读取完毕,直接删除之
for (string key : waitdeletelist) {
pipeline.del(key);
}
pipeline.sync();
} catch (exception e) {
redispool.returnbrokenresource(jedis);
} finally {
redispool.returnresource(jedis);
}
}
}
}
private static boolean isfinished(){
return finished;
}
private static void setfinished(boolean bool){
finished = bool;
}
}
代码作为示范。若线上则需要处理一些异常等。
若能够批量请求进行合并操作,自然可以节省很多的网络带宽、cpu等资源。有类似问题的同学,不妨考虑一下。
新申请的服务器内核为2.6.32,原先的tcp server直接在新内核的linxu服务器上运行,运行dmesg命令,可以看到大量的syn flooding警告:
possible syn flooding on port 8080. sending cookies.
原先的2.6.18内核的参数在2.6.32内核版本情况下,简单调整"net.ipv4.tcp_max_syn_backlog"已经没有作用。
怎么办,只能再次阅读2.6.32源码,以下即是。
最后小结处有直接结论,心急的你可以直接阅读总结好了。
net/socket.c:
syscall_define2(listen, int, fd, int, backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned)backlog > somaxconn)
backlog = somaxconn;
err = security_socket_listen(sock, backlog);
if (!err)
err = sock->ops->listen(sock, backlog);
fput_light(sock->file, fput_needed);
}
return err;
}
net/ipv4/af_inet.c:
/*
* move a socket into listening state.
*/
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
int err;
lock_sock(sk);
err = -einval;
if (sock->state != ss_unconnected || sock->type != sock_stream)
goto out;
old_state = sk->sk_state;
if (!((1 << old_state) & (tcpf_close | tcpf_listen)))
goto out;
/* really, if the socket is already in listen state
* we can only allow the backlog to be adjusted.
*/
if (old_state != tcp_listen) {
err = inet_csk_listen_start(sk, backlog);
if (err)
goto out;
}
sk->sk_max_ack_backlog = backlog;
err = 0;
out:
release_sock(sk);
return err;
}
inet_listen调用inet_csk_listen_start函数,所传入的backlog参数改头换面,变成了不可修改的常量nr_table_entries了。
net/ipv4/inet_connection_sock.c:
int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
struct inet_sock *inet = inet_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);
if (rc != 0)
return rc;
sk->sk_max_ack_backlog = 0;
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
/* there is race window here: we announce ourselves listening,
* but this transition is still not validated by get_port().
* it is ok, because this socket enters to hash table only
* after validation is complete.
*/
sk->sk_state = tcp_listen;
if (!sk->sk_prot->get_port(sk, inet->num)) {
inet->sport = htons(inet->num);
sk_dst_reset(sk);
sk->sk_prot->hash(sk);
return 0;
}
sk->sk_state = tcp_close;
__reqsk_queue_destroy(&icsk->icsk_accept_queue);
return -eaddrinuse;
}
下面处理的是tcp syn_recv状态的连接,处于握手阶段,也可以说是半连接时,等待着连接方第三次握手。
/*
* maximum number of syn_recv sockets in queue per listen socket.
* one syn_recv socket costs about 80bytes on a 32bit machine.
* it would be better to replace it with a global counter for all sockets
* but then some measure against one socket starving all other sockets
* would be needed.
*
* it was 128 by default. experiments with real servers show, that
* it is absolutely not enough even at 100conn/sec. 256 cures most
* of problems. this value is adjusted to 128 for very small machines
* (<=32mb of memory) and to 1024 on normal or better ones (>=256mb).
* note : dont forget somaxconn that may limit backlog too.
*/
int reqsk_queue_alloc(struct request_sock_queue *queue,
unsigned int nr_table_entries)
{
size_t lopt_size = sizeof(struct listen_sock);
struct listen_sock *lopt;
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
nr_table_entries = max_t(u32, nr_table_entries, 8);
nr_table_entries = roundup_pow_of_two(nr_table_entries 1);
lopt_size = nr_table_entries * sizeof(struct request_sock *);
if (lopt_size > page_size)
lopt = __vmalloc(lopt_size,
gfp_kernel | __gfp_highmem | __gfp_zero,
page_kernel);
else
lopt = kzalloc(lopt_size, gfp_kernel);
if (lopt == null)
return -enomem;
for (lopt->max_qlen_log = 3;
(1 << lopt->max_qlen_log) < nr_table_entries;
lopt->max_qlen_log );
get_random_bytes(&lopt->hash_rnd, sizeof(lopt->hash_rnd));
rwlock_init(&queue->syn_wait_lock);
queue->rskq_accept_head = null;
lopt->nr_table_entries = nr_table_entries;
write_lock_bh(&queue->syn_wait_lock);
queue->listen_opt = lopt;
write_unlock_bh(&queue->syn_wait_lock);
return 0;
}
关键要看nr_table_entries变量,在reqsk_queue_alloc函数中nr_table_entries变成了无符号变量,可修改的,变化受限。
比如实际内核参数值为:
net.ipv4.tcp_max_syn_backlog = 65535
所传入的backlog(不大于net.core.somaxconn = 65535)为8102,那么
// 取listen函数的backlog和sysctl_max_syn_backlog最小值,结果为8102
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
// 取nr_table_entries和8进行比较的最大值,结果为8102
nr_table_entries = max_t(u32, nr_table_entries, 8);
// 可看做 nr_table_entries*2,结果为8102*2=16204
nr_table_entries = roundup_pow_of_two(nr_table_entries 1);
计算结果,max_qlen_log = 14
for (lopt->max_qlen_log = 6;
(1 << lopt->max_qlen_log) < sysctl_max_syn_backlog;
lopt->max_qlen_log );
作为listen_sock结构定义了需要处理的处理半连接的队列元素个数为nr_table_entries,此例中为16204长度。
/** struct listen_sock - listen state
*
* @max_qlen_log - log_2 of maximal queued syns/requests
*/
struct listen_sock {
u8 max_qlen_log;
/* 3 bytes hole, try to use */
int qlen;
int qlen_young;
int clock_hand;
u32 hash_rnd;
u32 nr_table_entries;
struct request_sock *syn_table[0];
};
经描述而知,2^max_qlen_log = 半连接队列长度qlen值。
再回头看看报告syn flooding的函数:
net/ipv4/tcp_ipv4.c
#ifdef config_syn_cookies
static void syn_flood_warning(struct sk_buff *skb)
{
static unsigned long warntime;
if (time_after(jiffies, (warntime hz * 60))) {
warntime = jiffies;
printk(kern_info
"possible syn flooding on port %d. sending cookies.\n",
ntohs(tcp_hdr(skb)->dest));
}
}
#endif
被调用的处,已精简若干代码:
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
......
#ifdef config_syn_cookies
int want_cookie = 0;
#else
#define want_cookie 0 /* argh, why doesn't gcc optimize this :( */
#endif
......
/* tw buckets are converted to open requests without
* limitations, they conserve resources and peer is
* evidently real one.
*/
// 判断半连接队列是否已满 && !0
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef config_syn_cookies
if (sysctl_tcp_syncookies) {
want_cookie = 1;
} else
#endif
goto drop;
}
/* accept backlog is full. if we have already queued enough
* of warm entries in syn queue, drop request. it is better than
* clogging syn queue with openreqs with exponentially increasing
* timeout.
*/
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;
req = inet_reqsk_alloc(&tcp_request_sock_ops);
if (!req)
goto drop;
......
if (!want_cookie)
tcp_ecn_create_request(req, tcp_hdr(skb));
if (want_cookie) {
#ifdef config_syn_cookies
syn_flood_warning(skb);
req->cookie_ts = tmp_opt.tstamp_ok;
#endif
isn = cookie_v4_init_sequence(sk, skb, &req->mss);
} else if (!isn) {
......
}
......
}
判断半连接队列已满的函数很关键,可以看看运算法则:
include/net/inet_connection_sock.h:
static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{
return reqsk_queue_is_full(&inet_csk(sk)->icsk_accept_queue);
}
include/net/rquest_sock.h:
static inline int reqsk_queue_is_full(const struct request_sock_queue *queue)
{
// 向右移位max_qlen_log个单位
return queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log;
}
返回1,自然表示半连接队列已满。
以上仅仅是分析了半连接队列已满的判断条件,总之应用程序所传入的backlog很关键,如值太小,很容易得到1.
若 somaxconn = 128,sysctl_max_syn_backlog = 4096,backlog = 511 则最终 nr_table_entries = 256,max_qlen_log = 8。那么超过256个半连接的队列,257 >> 8 = 1,队列已满。
如何设置backlog,还得需要结合具体应用程序,需要为其调用listen方法赋值。
tcp server使用netty 3.7 版本,版本较低,在处理backlog,若我们不手动指定backlog值,jdk 1.6默认为50。
有证如下: java.net.serversocket:
public void bind(socketaddress endpoint, int backlog) throws ioexception {
if (isclosed())
throw new socketexception("socket is closed");
if (!oldimpl && isbound())
throw new socketexception("already bound");
if (endpoint == null)
endpoint = new inetsocketaddress(0);
if (!(endpoint instanceof inetsocketaddress))
throw new illegalargumentexception("unsupported address type");
inetsocketaddress epoint = (inetsocketaddress) endpoint;
if (epoint.isunresolved())
throw new socketexception("unresolved address");
if (backlog < 1)
backlog = 50;
try {
securitymanager security = system.getsecuritymanager();
if (security != null)
security.checklisten(epoint.getport());
getimpl().bind(epoint.getaddress(), epoint.getport());
getimpl().listen(backlog);
bound = true;
} catch(securityexception e) {
bound = false;
throw e;
} catch(ioexception e) {
bound = false;
throw e;
}
}
netty中,处理backlog的地方:
org/jboss/netty/channel/socket/defaultserversocketchannelconfig.java:
@override
public boolean setoption(string key, object value) {
if (super.setoption(key, value)) {
return true;
}
if ("receivebuffersize".equals(key)) {
setreceivebuffersize(conversionutil.toint(value));
} else if ("reuseaddress".equals(key)) {
setreuseaddress(conversionutil.toboolean(value));
} else if ("backlog".equals(key)) {
setbacklog(conversionutil.toint(value));
} else {
return false;
}
return true;
}
既然需要我们手动指定backlog值,那么可以这样做:
bootstrap.setoption("backlog", 8102); // 设置大一些没有关系,系统内核会自动与net.core.somaxconn相比较,取最低值
相对比netty 4.0,有些不智能,可参考:
在linux内核2.6.32,若在没有遭受到syn flooding攻击的情况下,可以适当调整:
sysctl -w net.core.somaxconn=32768
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -p
另千万别忘记修改tcp server的listen接口所传入的backlog值,若不设置或者过小,都会有可能造成syn flooding的警告信息。开始不妨设置成1024,然后观察一段时间根据实际情况需要再慢慢往上调。
无论你如何设置,最终backlog值范围为:
backlog <= net.core.somaxconn
半连接队列长度约为:
半连接队列长度 ≈ 2 * min(backlog, net.ipv4.tcpmax_syn_backlog)
另,若出现syn flooding时,此时tcp syn_recv数量表示半连接队列已经满,可以查看一下:
ss -ant | awk 'nr>1 { s[$1]} end {for(k in s) print k,s[k]}'
感谢运维书坤小伙提供的比较好用查看命令。
最近线上服务器,dmesg会给出一些警告信息:
possible syn flooding on port 8080. sending cookies.
初看以为是受到dos拒绝性攻击,但仔细一分析,一天量也就是在1000多条左右,感觉上属于正常可接受范围。
下面需要找出来源,以及原因,以下内容基于linux 2.6.18内核。
net/ipv4/tcp_ipv4.c:
#ifdef config_syn_cookies
static void syn_flood_warning(struct sk_buff *skb)
{
static unsigned long warntime; // 第一次加载初始化为零,后续warntime = jiffies
if (time_after(jiffies, (warntime hz * 60))) {
warntime = jiffies;
printk(kern_info
"possible syn flooding on port %d. sending cookies.\n",
ntohs(skb->h.th->dest));
}
}
#endif
很显然,config_syn_cookies在linux系统编译时,已被设置true。
time_after宏定义:
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(b) - (long)(a) < 0))
两个无符号的时间比较,确定先后顺序。
jiffies真身:
# define jiffies raid6_jiffies()
#define hz 1000
......
static inline uint32_t raid6_jiffies(void)
{
struct timeval tv;
gettimeofday(&tv, null);
return tv.tv_sec*1000 tv.tv_usec/1000; // 秒*1000 微秒/1000
}
回过头来,再看看syn_flood_warning函数:
static void syn_flood_warning(struct sk_buff *skb)
{
static unsigned long warntime; // 第一次加载初始化为零,后续warntime = jiffies
if (time_after(jiffies, (warntime hz * 60))) {
warntime = jiffies;
printk(kern_info
"possible syn flooding on port %d. sending cookies.\n",
ntohs(skb->h.th->dest));
}
}
warntime为static类型,第一次调用时被初始化为零,下次调用就是上次的jiffies值了,前后间隔值超过hz*60就不会输出警告信息了。
有关time_after和jiffies,分享几篇文章:
http://wenku.baidu.com/view/c75658d480eb6294dd886c4e.html
注意观察want_cookie=1时的条件。
net/ipv4/tcp_ipv4.c:
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
struct inet_request_sock *ireq;
struct tcp_options_received tmp_opt;
struct request_sock *req;
__u32 saddr = skb->nh.iph->saddr;
__u32 daddr = skb->nh.iph->daddr;
__u32 isn = tcp_skb_cb(skb)->when; // when在tcp_v4_rcv()中会被置为0
struct dst_entry *dst = null;
#ifdef config_syn_cookies
int want_cookie = 0;
#else
#define want_cookie 0 /* argh, why doesn't gcc optimize this :( */
#endif
/* never answer to syns send to broadcast or multicast */
if (((struct rtable *)skb->dst)->rt_flags &
(rtcf_broadcast | rtcf_multicast))
goto drop;
/* tw buckets are converted to open requests without
* limitations, they conserve resources and peer is
* evidently real one.
*/
// if(判断半连接队列已满 && !0)
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef config_syn_cookies
if (sysctl_tcp_syncookies) { // net.ipv4.tcp_syncookies = 1
want_cookie = 1;
} else
#endif
goto drop;
}
/* accept backlog is full. if we have already queued enough
* of warm entries in syn queue, drop request. it is better than
* clogging syn queue with openreqs with exponentially increasing
* timeout.
*/
// if(连接队列是否已满 && 半连接队列中还有未重传ack半连接数字 > 1)
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;
......
tcp_openreq_init(req, &tmp_opt, skb);
ireq = inet_rsk(req);
ireq->loc_addr = daddr;
ireq->rmt_addr = saddr;
ireq->opt = tcp_v4_save_options(sk, skb);
if (!want_cookie)
tcp_ecn_create_request(req, skb->h.th);
if (want_cookie) { // 半连接队列已满会触发
#ifdef config_syn_cookies
syn_flood_warning(skb);
#endif
isn = cookie_v4_init_sequence(sk, skb, &req->mss);
} else if (!isn) {
......
}
/* kill the following clause, if you dislike this way. */
// net.ipv4.tcp_syncookies未设置情况下,sysctl_max_syn_backlog发生的作用
else if (!sysctl_tcp_syncookies &&
(sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(sysctl_max_syn_backlog >> 2)) &&
(!peer || !peer->tcp_ts_stamp) &&
(!dst || !dst_metric(dst, rtax_rtt))) {
/* without syncookies last quarter of
* backlog is filled with destinations,
* proven to be alive.
* it means that we continue to communicate
* to destinations, already remembered
* to the moment of synflood.
*/
limit_netdebug(kern_debug "tcp: drop open "
"request from %u.%u.%u.%u/%u\n",
nipquad(saddr),
ntohs(skb->h.th->source));
dst_release(dst);
goto drop_and_free;
}
isn = tcp_v4_init_sequence(sk, skb);
}
tcp_rsk(req)->snt_isn = isn;
if (tcp_v4_send_synack(sk, req, dst))
goto drop_and_free;
if (want_cookie) {
reqsk_free(req);
} else {
inet_csk_reqsk_queue_hash_add(sk, req, tcp_timeout_init);
}
return 0;
drop_and_free:
reqsk_free(req);
drop:
return 0;
}
总之,如系统出现:
possible syn flooding on port 8080. sending cookies.
若量不大,是在提醒你需要关心一下sysctl_max_syn_backlog其值是否过低:
sysctl -a | grep 'max_syn_backlog'
不妨成倍增加一下
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
sysctl -p
若进程无法做到重新加载,那就需要重启应用,以适应新的内核参数。进而持续观察一段时间。
貌似tcp_max_syn_backlog参数其完整作用域还没有理解完整,下次有时间再写吧。
有些东西总是很容易遗忘,一时记得了,过两天就真正还给周公了。零零碎碎的不如一并记下来,以后可以直接拿过来查询即可。
以下内容基于linux 2.6.18内核。
这个参数具体意义,先看看linux socket的listen解释
man listen
#include
int listen(int sockfd, int backlog);
int类型的backlog参数,listen方法的backlog意义为,已经完成三次握手、已经成功建立连接的套接字将要进入队列的长度。
一般我们自己定义设定backlog值,若我们设置的backlog值大于net.core.somaxconn值,将被置为net.core.somaxconn值大小。若不想直接硬性指定,跟随系统设定,则需要读取/proc/sys/net/core/somaxconn。
net\socket.c :
/*
* perform a listen. basically, we allow the protocol to do anything
* necessary for a listen, and if that works, we mark the socket as
* ready for listening.
*/
int sysctl_somaxconn = somaxconn;
asmlinkage long sys_listen(int fd, int backlog)
{
struct socket *sock;
int err, fput_needed;
if ((sock = sockfd_lookup_light(fd, &err, &fput_needed)) != null) {
if ((unsigned) backlog > sysctl_somaxconn)
backlog = sysctl_somaxconn;
err = security_socket_listen(sock, backlog);
if (!err)
err = sock->ops->listen(sock, backlog);
fput_light(sock->file, fput_needed);
}
return err;
}
比如经常使用的netty(4.0)框架,在linux下启动时,会直接读取/proc/sys/net/core/somaxconn值然后作为listen的backlog参数进行调用linux系统的listen进行初始化等。
int somaxconn = 3072;
bufferedreader in = null;
try {
in = new bufferedreader(new filereader("/proc/sys/net/core/somaxconn"));
somaxconn = integer.parseint(in.readline());
logger.debug("/proc/sys/net/core/somaxconn: {}", somaxconn);
} catch (exception e) {
// failed to get somaxconn
} finally {
if (in != null) {
try {
in.close();
} catch (exception e) {
// ignored.
}
}
}
somaxconn = somaxconn;
......
private volatile int backlog = netutil.somaxconn;
一般稍微增大net.core.somaxconn值就显得很有必要。
设置其值方法:
sysctl -w net.core.somaxconn=65535
较大内存的linux,65535数值一般就可以了。
若让其生效,sysctl -p 即可,然后重启你的server应用即可。
内核代码中sysctl.c文件解释:
number of unprocessed input packets before kernel starts dropping them, default 300
我所理解的含义,每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的最大数目,一旦超过将被丢弃。
所起作用处,net/core/dev.c:
int netif_rx(struct sk_buff *skb)
{
struct softnet_data *queue;
unsigned long flags;
/* if netpoll wants it, pretend we never saw it */
if (netpoll_rx(skb))
return net_rx_drop;
if (!skb->tstamp.off_sec)
net_timestamp(skb);
/*
* the code is rearranged so that the path is the most
* short when cpu is congested, but is still operating.
*/
local_irq_save(flags);
queue = &__get_cpu_var(softnet_data);
__get_cpu_var(netdev_rx_stat).total ;
if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {
if (queue->input_pkt_queue.qlen) {
enqueue:
dev_hold(skb->dev);
__skb_queue_tail(&queue->input_pkt_queue, skb);
local_irq_restore(flags);
return net_rx_success;
}
netif_rx_schedule(&queue->backlog_dev);
goto enqueue;
}
__get_cpu_var(netdev_rx_stat).dropped ;
local_irq_restore(flags);
kfree_skb(skb);
return net_rx_drop;
}
以上代码看一下,大概会明白netdev_max_backlog会在什么时候起作用。
公司内技术分享文档,不涉及公司内部技术等,可以拿出来分享一下。
访问地址:
有些粗糙,有些点可能未表达清楚,您若发现谬误之处,欢迎及时指出。
主动模式(选项{active, true})一般让人很喜欢,非阻塞消息接收,但在系统无法应对超大流量请求时,客户端发送的数据快过服务器可以处理的速度,那么系统就可能会造成消息缓冲区被塞满,可能出现持续繁忙的流量的极端情况下,系统因请求而溢出,虚拟机造成内存不足的风险而崩溃。
使用被动模式(选项{active, false})的套接字,底层的tcp缓冲区可用于抑制请求,并拒绝客户端的消息,在接收数据的地方都会调用gen_tcp:recv,造成阻塞(单进程模式下就只能消极等待某一个具体的客户端套接字,很危险)。需要注意的是,操作系统可能还会做一些缓存允许客户端机器继续发送少量数据,然后才会将其阻塞,此时erlang尚未调用recv函数。
混合型模式(半阻塞),使用选项{active, once}打开,主动仅针对一个消息,在控制进程发送完一个数据消息后,必须显示调用inet:setopts(socket, [{active, once}])重新激活以便接受下一个消息(在此之前,系统处于阻塞状态)。可见,混合型模式综合了主动模式和被动模式的两者优势,可实现流量控制,防止服务器被过多消息淹没。
以下tcp server代码,都是建立在混合型模式(半阻塞)基础上。
prim_inet没有官方文档,可以认为是对底层socket的直接包装。淘宝说,这是otp内部实现的细节 是针对erlang库开发者的private module,底层模块,不推荐使用。但在示范中演示了prim_inet操作socket异步特性。
一般来说,需要一个单独进程进行客户端套接字监听,每一个子进程进行处理来自具体客户端的socket请求。
在示范中,子进程使用gen_fsm处理,很巧妙的结合状态机和消息事件,值得学习。
在文章中,作者也是使用此模式,但子进程不符合otp规范,因此个人认为不是一个很好的实践模式。
简易的一对一监督进程,用来创建一组动态子进程。对于需要并发处理多个请求的服务器较为合适。比如socket 服务端接受新的客户端连接请求以后,需要动态创建一个新的socket连接处理子进程。若遵守otp原则,那就是子监督进程。
也是基于{active, once}模式,但阻塞的等待下一个客户端连接的任务被抛给了子监督进程。
看一下入口tcp_server_app吧
读取端口,然后启动主监督进程(此时还不会监听处理客户端socket请求),紧接着启动子监督进程,开始处理来自客户端的socket的连接。
监督进程tcp_server_sup也很简单:
需要注意的是,只有调用start_child函数时,才真正调用tcp_server_handler:start_link([lsock])函数。
tcp_server_handler的代码也不复杂:
代码很精巧,有些小技巧在里面。子监督进程调用start_link函数,init会返回{ok, #state{lsock = socket}, 0}. 数字0代表了timeout数值,意味着gen_server马上调用handle_info(timeout, #state{lsock = lsock} = state)函数,执行客户端socket监听,阻塞于此,但不会影响在此模式下其它函数的调用。直到有客户端进来,然后启动一个新的子监督进程tcp_server_handler,当前子监督进程解除阻塞。
这个实现师从于non-blocking tcp server using otp principles一文,但子进程改为了gen_server实现。
看一看入口,很简单的:
监督进程代码:
策略不一样,one_for_one包括了一个监听进程tcp_listener,还包含了一个tcp_client_sup进程树(simple_one_for_one策略)
tcp_listener单独一个进程用于监听来自客户端socket的连接:
很显然,接收客户端的连接之后,转交给tcp_client_handler模块进行处理:
和标准api对比一下,可以感受到异步io的好处。
通过不同的模式,简单实现一个基于erlang otp的tcp服务器,也是学习总结,不至于忘记。
您若有更好的建议,欢迎告知,谢谢。