提高服务端性能的几个socket选项
提高服务端性能的几个socket选项
在之前的一篇文章中,作者在配置了SO_REUSEPORT
选项之后,使得应用的性能提高了数十倍。现在介绍socket选项中如下几个可以提升服务端性能的选项:
SO_REUSEADDR
SO_REUSEPORT
SO_ATTACH_REUSEPORT_CBPF/EBPF
验证环境:OS:centos 7.8;内核:5.9.0-1.el7.elrepo.x86_64
默认行为
TCP/UDP连接主要靠五元组来区分一条链接。只要五元组不同,则视为不同的连接。
{protocol, src addr, src port, dest addr, dest port}
默认情况下,两个sockets不能绑定相同的源地址和源端口。运行如下服务端代码,然后使用nc 127.0.0.1 9999
连接服务端,通过crtl+c
中断服务之后,此时可以在系统上看到到9999端口有一条连接处于TIME-WAIT
状态,再启动服务端就可以看到Address already in use
错误。
# ss -nta|grep TIME-WAIT
TIME-WAIT 0 0 127.0.0.1:9999 127.0.0.1:49040
//例1
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[]) {
int lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (lfd == -1) {
perror("socket: ");
return -1;
}
struct sockaddr_in sockaddr;
memset(&sockaddr, 0, sizeof(struct sockaddr_in));
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(9999);
inet_pton(AF_INET, "127.0.0.1", &sockaddr.sin_addr);
if (bind(lfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == -1) {
perror("bind: ");
return -1;
}
if (listen(lfd, 128) == -1) {
perror("listen: ");
return -1;
}
struct sockaddr_storage claddr;
socklen_t addrlen = sizeof(struct sockaddr_storage);
int cfd = accept(lfd, (struct sockaddr*)&claddr, &addrlen);
if (cfd == -1) {
perror("accept: ");
return -1;
}
printf("client connected: %d\n", cfd);
char buff[100];
for (;;) {
ssize_t num = read(cfd, buff, 100);
if (num == 0) {
printf("client close: %d\n", cfd);
close(cfd);
break;
} else if (num == -1) {
int no = errno;
if (no != EINTR && no != EAGAIN && no != EWOULDBLOCK) {
printf("client error: %d\n", cfd);
close(cfd);
}
} else {
if (write(cfd, buff, num) != num) {
printf("client error: %d\n", cfd);
close(cfd);
}
}
}
return 0;
}
只要源端口不同,源地址实际上就无关紧要。假设将socketA绑定到A:X,socketB绑定到B:Y,其中A和B为地址,X和Y为端口。只要X!=Y(端口不同),这两个socket都能绑定成功。如果X==Y,只要A!=B(地址不同),这两个socket也能绑定成功。如果一个socket绑定到了0.0.0.0:21
,则表示该socket绑定了所有现有的本地地址,此时,其他socket不能绑定到任何本地地址的21
端口上。否则同样会出现Address already in use
错误。
测试场景为:创建两个绑定地址分别为0.0.0.0
和127.0.0.1
的服务app1和app2。启动app1-->nc连接app1-->ctrl+c断开app1-->启动app2,此时就会出现Address already in use
错误。
TCP客户端通常不会绑定IP地址,内核会根据路由表选择连接需要的源地址;而服务端通常会绑定一个地址,如果绑定了INADDR_ANY,则内核会使用接收到的报文的目的地址作为服务端的源地址。IPv4地址绑定的规则如下:
IP Address IP Port Result INADDR_ANY 0 Kernel chooses IP address and port INADDR_ANY non zero Kernel chooses IP address, process specifies port Local IP address 0 Process specifies IP address, kernel chooses port Local IP address non zero Process specifies IP address and port
SO_REUSEADDR
在启用SO_REUSEADDR
选项之后,就可以在非TCP_LISTEN
状态复用本地地址,当然,主要是为了在TIME_WAIT
状态复用本地地址(如支持服务端快速重启)。需要注意的是Linux中对该选项的实现与BSD不同:前者要求复用者和被复用者都必须设置SO_REUSEADDR
选项,而后者仅要求复用者设置SO_REUSEADDR
选项即可。参见Linux socket帮助文档。
启用SO_REUSEADDR
选项后,在例1中的bind
前添加如下代码,然后运行,此时不会再报错:
int optval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
SO_REUSEPORT
使用SO_REUSEPORT
选项之后,就可以完全复用端口(无论被复用者处理任何状态)。SO_REUSEPORT的目的主要是为多核多线程环境提供并行处理能力。如可以启用多个worker线程,这些worker线程绑定相同的地址和端口。当新接入一条流时,内核会使用流哈希算法选择使用哪个socket。
与SO_REUSEADDR
选项类似,使用SO_REUSEPORT
选项时,同样要求复用者和被复用者同时设置该选项,如果被复用者没有设置,即使复用者设置了该选项,最终绑定还是失败的。
使用SO_REUSEPORT
选项时可以不使用SO_REUSEADDR
选项。设置方式为:
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
SO_ATTACH_REUSEPORT_CBPF/EBPF
BPF相关的socket选项介绍
socket选项中,与bpf相关的有的有如下四个选项:
SO_ATTACH_FILTER(since Linux 2.2):给socket附加一个cBPF,用于过滤接收到的报文。
SO_ATTACH_BPF(since Linux 3.19) :给socket附加一个eBPF,用于过滤接收到的报文。其参数为bpf(2)返回的指向类型为
BPF_PROG_TYPE_SOCKET_FILTER
的程序的文件描述符SO_ATTACH_REUSEPORT_CBPF:与
SO_REUSEPORT
配合使用,用于将报文分给reuseport组(即配置了SO_REUSEPORT
选项,且使用相同的本地地址接收报文 )中的socket。如果BPF程序返回了无效的值,则回退为SO_REUSEPORT
机制,与SO_ATTACH_FILTER使用相同的参数。socket按添加到组的顺序进行编号(即UDP socket使用bind(2)的顺序,或TCP socket使用listen(2)的顺序),当一个reuseport组新增一个socket后,该socket会集成该组中的BPF程序。当一个reuseport组(通过close(2))移除一个socket时,组中的最后一个socket会转移到closed位置。
SO_ATTACH_REUSEPORT_EBPF:与SO_ATTACH_BPF使用相同的参数。
SO_DETACH_FILTER(since Linux 2.2)/SO_DETACH_BPF(since Linux 3.19) :用于移除使用SO_ATTACH_FILTER/SO_ATTACH_BPF附加到socket的cBPF/eBPF。
SO_LOCK_FILTER :用于防止附加的过滤器被意外detach掉。
Linux 4.5添加了对UDP的支持,Linux 4.6添加了对TCP的支持。
如何使用BPF socket选项
如何编写BPF程序
使用libpcap:如果是使用BPF对报文进行处理,官方推荐使用libpcap,即tcpdump使用的库,该库提供了使用BPF对报文进行处理的函数,可以方便地对报文进行操作。缺点是该库完全屏蔽了BPF的实现,仅能使用它提供的库函数。例如不能使用socket选项SO_ATTACH_FILTER将一个socket和BPF程序进行关联。
使用BPF指令集编写BPF程序,可以参见内核官方给出的例子
/tools/testing/selftests/net/reuseport_bpf.c
。这种方式下编写的BPF代码很简洁,但由于BPF指令集其实就是一个特殊的汇编语言,理解上比较晦涩,且维护起来比较困难。官方对为这类汇编也封装了一些简单的宏,可以参见/include/linux/filter.h
。下面以bpf(2)帮助手册中的例子,看下如何使用BPF指令集编写BPF程序:
/* bpf+sockets example:
* 1. create array map of 256 elements
* 2. load program that counts number of packets received
* r0 = skb->data[ETH_HLEN + offsetof(struct iphdr, protocol)]
* map[r0]++
* 3. attach prog_fd to raw socket via setsockopt()
* 4. print number of received TCP/UDP packets every second
*/
int main(int argc, char **argv)
{
int sock, map_fd, prog_fd, key;
long long value = 0, tcp_cnt, udp_cnt; map_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 256);
if (map_fd < 0) {
printf("failed to create map '%s'\n", strerror(errno));
/* likely not run as root */
return 1;
} struct bpf_insn prog[] = {
/* 该部分内容参见下表解析 */
}; prog_fd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, prog, sizeof(prog) / sizeof(prog[0]), "GPL");
sock = open_raw_sock("lo");
assert(setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) == 0); for (;;) {
key = IPPROTO_TCP;
assert(bpf_lookup_elem(map_fd, &key, &tcp_cnt) == 0);
key = IPPROTO_UDP;
assert(bpf_lookup_elem(map_fd, &key, &udp_cnt) == 0);
printf("TCP %lld UDP %lld packets\n", tcp_cnt, udp_cnt);
sleep(1);
} return 0;
}
bpf_insn prog[]
中的内容如下:指令 解释 BPF_MOV64_REG(BPF_REG_6, BPF_REG_1), dst_reg = src_reg
,即:r6 = r1BPF_LD_ABS(BPF_B, ETH_HLEN + offsetof(struct iphdr, protocol)), R0 = *(uint *) (skb->data + imm32)
,即:r0 = ip->proto,此时r0保存了IP报文中的协议号,可以为TCP/UDP等BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), *(uint *) (dst_reg + off16) = src_reg
,即:*(u32 *)(fp - 4) = r0,将r0保存的协议号入栈,地址为fp - 4(4个字节是因为BPF_LD_ABS中用于保存协议号的imm32的大小为4个字节)BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), r2 = fp,将帧指针地址传给r2,下一步用于获取栈中保存的协议号 BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), dst_reg += imm32
,即:r2 = r2 - 4,此时r2指向栈中保存的IP协议号BPF_LD_MAP_FD(BPF_REG_1, map_fd), 将本地创建的map_fd保存到寄存器中,即:r1 = map_fd BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem), 调用 map_lookup
函数在map_fd中查找r2指针指向的key(索引)对应的value,即:r0 = map_lookup(r1, r2),r0为map中key对应的地址,map_fd[*r2]。BPF_FUNC_map_lookup_elem
对应bpf_map_lookup_elem
函数。BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2), if (dst_reg 'op' imm32) goto pc + off16
,即:if (r0 == 0) goto pc+2(2个字节,16bits),如果没有在map_fd中找到对应的值(r0,即对应协议号的报文),则跳转到BPF_MOV64_IMM(BPF_REG_0, 0),返回0BPF_MOV64_IMM(BPF_REG_1, 1), 如果在map_fd中找到了r1 = 1,作为累加单位,1 BPF_XADD(BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0), *(u64 *) r0 += r1,即如果在map_fd中找到的期望的报文,则r0指向的值加1,相当于map_fd[key]++ BPF_MOV64_IMM(BPF_REG_0, 0), r0 = 0,此时表示没有找到任何期望的报文,数目为0 BPF_EXIT_INSN(), return r0 通过BPF辅助函数:可以比较方便地编写BPF用户态和内核态代码。典型的例子可以参考内核源码树中提供的例子:
/tools/testing/selftests/bpf/progs_test/select_reuseport.c
和/tools/testing/selftests/bpf/progs/test_select_reuseport_kern.c
。具体用法可以学习XDP教程。这种方式相比使用BPF指令集在开发和维护上成本要低一些,但也有一些不便利的地方,即用户态需要依赖内核态编译出来的.o文件,因此在编译上需要分成两步。
Note:建议借用xdp-tutorial中的Makefile编译bpf内核态和用户态的程序。
将socket与BPF程序关联
有了上述知识,其实将socket与BPF程序关联其实就是将BPF过滤出来的报文传递给这个关联的socket。
在提高UDP交互性能一文中,提高流量的一个方式就是使用BPF程序将socket与CPU核关联起来,实际就是将一个socket与这个核上的流进行了关联,防止因为哈希算法导致多条流争用同一个socket导致性能下降,也提升了CPU缓存的命中率。
还有一点需要注意的是,使用BPF将socket与CPU核进行关联之前,需要确保该socket所在的流不会漂移到其他核上,在提高UDP交互性能中使用了irqbalance的-h exact
选项,防止冲突核漂移。
拓展
系统参数
net.ipv4.tcp_tw_reuse
可以用于快速回收TIME_WAIT
状态的端口,但只适用于客户端,且只在客户端执行connect
时才会生效。调用链如下:tcp_v4_pre_connect->inet_hash_connect->__inet_check_established->tcp_twsk_unique->sysctl_tcp_tw_reuse(内核参数值)
在How do SO_REUSEADDR and SO_REUSEPORT differ? 这篇文章中对
SO_REUSEADDR
有如下描述,原意是说启用SO_REUSEADDR
之后,系统会将泛地址和非泛地址分开,如当一个socket绑定0.0.0.0:port
时,另外一个socket可以成功绑定本地地址192.168.0.1:port
,但在本次测试中发现这种情况下也会失败。With
SO_REUSEADDR
it will succeed, since0.0.0.0
and192.168.0.1
are not exactly the same address, one is a wildcard for all local addresses and the other one is a very specific local address当前cBPF格式用于在32位架构上执行JIT编译;而eBPF指令集用于在x86-64, aarch64, s390x, powerpc64, sparc64, arm32, riscv64, riscv32 架构上执行JIT编译。
参考
- How do SO_REUSEADDR and SO_REUSEPORT differ?
- Socket Programming
- setsockopt
- Linux Socket Filtering aka Berkeley Packet Filter (BPF)
提高服务端性能的几个socket选项的更多相关文章
- Web服务端性能提升实践
随着互联网的不断发展,日常生活中越来越多的需求通过网络来实现,从衣食住行到金融教育,从口袋到身份,人们无时无刻不依赖着网络,而且越来越多的人通过网络来完成自己的需求. 作为直接面对来自客户请求的Web ...
- Java服务端性能优化
<Java程序性能优化>说性能优化包含五个层次:设计调优.代码调优.JVM调优.数据库调优.操作系统调优. 常用的几个代码优化方案: 使用单例 对于IO处理.数据库连接.配置文件解析加载等 ...
- chrome debug 服务端性能
设置 http header 在 chrome 查看服务端性能 \Yii::$app->getResponse()->headers->set('Server-Timing', 'c ...
- 成功使Linux服务端和Windows客户端建立socket通信
一.准备工作 1.一台装有虚拟机的Windows7操作系统,虚拟机中装的是CentOS6.5版本的Linux 2.Windows7已经装有java环境 二.编码 使用java编写socket通信的服务 ...
- Delphi服务端和PHP客户端通过Socket通信
在开始之前看下效果 PHP页面作为客户端发送请求给作为服务端的Delphi应用程序 PHP客户端页面打开如下 Delphi服务端应用程序打开如下 每次PHP页面刷新一下,Delphi的文本框都显示&q ...
- [经验] Java 服务端 和 C# 客户端 实现 Socket 通信
由于项目需要, 我需要通过 Java 开发的服务端对 C# 作为脚本语言开发的 unity 项目实现控制 话不多说, 直接上代码 首先, 我们先来构建服务端的代码, 服务端我们使用 Java 语言 i ...
- NFS介绍、服务端安装配置、NFS配置选项
6月21日任务 14.1 NFS介绍14.2 NFS服务端安装配置14.3 NFS配置选项 14.1 NFS介绍 14.2 NFS服务端安装配置 1.首先需要2台机器,一台是服务端,一台是客户端,分别 ...
- Linux centosVMware NFS介绍、NFS服务端安装配置、NFS配置选项
一.NFS介绍 NFS是Network File System的缩写 NFS最早由Sun公司开发,分2,3,4三个版本,2和3由Sun起草开发,4.0开始Netapp公司参与并主导开发,最新为4.1版 ...
- [转]C服务端与java客户端的socket通信注意事项
http://blog.csdn.net/gaoxin1076/article/details/7671752 Socket网络通讯开发总结之:Java 与 C进行Socket通讯 注意以下问题: 1 ...
随机推荐
- charles抓包使用
Proxy ---> Proxy Setting ---> HTTP Proxy (设置代理的端口) 设备和代理处于同一局域网,并在设备端配置IP,端口,然后监听请求. 抓取本机的请求
- Java数据结构(十四)—— 平衡二叉树(AVL树)
平衡二叉树(AVL树) 二叉排序树问题分析 左子树全部为空,从形式上看更像一个单链表 插入速度没有影响 查询速度明显降低 解决方案:平衡二叉树 基本介绍 平衡二叉树也叫二叉搜索树,保证查询效率较高 它 ...
- 第十三章、Designer中的按钮Buttons组件详解
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 一.引言 Qt Designer中的Buttons部件包括Push Button(常规按钮.一般称按 ...
- PyQt(Python+Qt)学习随笔:部件的inputMethodHints属性
inputMethodHints属性只对输入部件有效,输入法使用它来检索有关输入法应如何操作的提示,例如,如果设置了只允许输入数字的标志,则输入法可能会更改其可视组件,以反映只能输入数字.相关取值及含 ...
- 手写Json解析器学习心得
一. 介绍 一周前,老同学阿立给我转了一篇知乎回答,答主说检验一门语言是否掌握的标准是实现一个Json解析器,网易游戏过去的Python入门培训作业之一就是五天时间实现一个Json解析器. 知乎回答- ...
- 深入分析 Java Lock 同步锁
前言 Java 的锁实现,有 Synchronized 和 Lock.上一篇文章深入分析了 Synchronized 的实现原理:由Java 15废弃偏向锁,谈谈Java Synchronized 的 ...
- centos安装scrapy
安装scrapy centos 7 安装scrapy报错说找不到scrapy需要的Twisted13.0以上版本? Collecting Twisted>=13.1.0 (from Scrapy ...
- 【opencv】学习笔记
安装 此笔记仅对python36实用 OpenCV装3.4.1.15 指令:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv ...
- dbeaver 驱动安装
一.背景: 在Windows 10 安装dbeaver数据库连接工具,点"测试连接"的时候出现报错如下: Error resolving dependencies Maven ...
- 蒲公英 · JELLY技术周刊 Vol.34: 芜湖~ Flutter
蒲公英 · JELLY技术周刊 Vol.34 提及跨端,你能想到那些技术?PWA.小程序.Ionic.React Native.Weex--当然也少不了 Flutter,历时 3 年,Flutter ...