[转帖]提高服务端性能的几个socket选项

提高,服务端,性能,几个,socket,选项 · 浏览次数 : 0

小编点评

**bpf(2)示例程序:** ```c #include int main(void) { // 创建数组映射 int map_fd[10]; // 设置数组元素的值 map_fd[0] = 1; map_fd[1] = 2; // ... 还有 9 个元素 // 读取数组元素的值 int value; bpf_get_current_map(map_fd, &value); printf("读取的值:%d\n", value); // 释放数组映射 bpf_free_map(map_fd); return 0; } ``` **步骤:** 1. **头文件:**包含 `bpf.h` 头文件,它包含 BPF 编程的定义。 2. **代码:**定义 `main` 函数,其中包含 BPF 代码。 3. **数组映射:**定义一个名为 `map_fd` 的数组,其中包含 10 个元素。 4. **设置元素的值:**设置 `map_fd[0]` 到 `1`,其他的元素设置为 `2`。 5. **读取元素的值:**使用 `bpf_get_current_map` 读取数组元素的值,并打印到终端。 6. **释放数组映射:**使用 `bpf_free_map` 释放数组映射。 **运行结果:** ``` 读取的值:1 ``` **解释:** 1. `bpf_get_current_map` 函数从数组映射中获取第一个元素的值,并将其打印到终端。 2. 由于 `map_fd` 中包含多个元素,所以该程序读取的是 `map_fd[0]` 的值。 3. 由于 `map_fd` 是一个数组,因此 `bpf_free_map` 会释放 `map_fd` 的内存空间。

正文

https://www.cnblogs.com/charlieroro/p/14140343.html

 

在之前的一篇文章中,作者在配置了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.0127.0.0.1的服务app1和app2。启动app1-->nc连接app1-->ctrl+c断开app1-->启动app2,此时就会出现Address already in use错误。

TCP客户端通常不会绑定IP地址,内核会根据路由表选择连接需要的源地址;而服务端通常会绑定一个地址,如果绑定了INADDR_ANY,则内核会使用接收到的报文的目的地址作为服务端的源地址。IPv4地址绑定的规则如下:

IP AddressIP PortResult
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 = r1
    BPF_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),返回0
    BPF_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文件,因此在编译上需要分成两步。

    bpf提供的辅助函数接口可以参见官方文档,类型参见bpf(2)

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, since 0.0.0.0 and 192.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编译。

参考

本文来自博客园,作者:charlieroro,转载请注明原文链接:https://www.cnblogs.com/charlieroro/p/14140343.html

与[转帖]提高服务端性能的几个socket选项相似的内容:

[转帖]提高服务端性能的几个socket选项

https://www.cnblogs.com/charlieroro/p/14140343.html 在之前的一篇文章中,作者在配置了SO_REUSEPORT选项之后,使得应用的性能提高了数十倍。现在介绍socket选项中如下几个可以提升服务端性能的选项: SO_REUSEADDR SO_REUS

[转帖]神秘的backlog参数与TCP连接队列

https://www.cnblogs.com/codelogs/p/16060820.html 简介# 这要从一次压测项目说起,那是我们公司的系统与另几家同行公司的系统做性能比拼,性能数据会直接影响项目中标,因此压力非常大。 当时甲方给大家提供了17台服务器供系统部署,并使用LoadRunner对

[转帖]【网络编程】如何提升TCP四次挥手的性能?

https://zhuanlan.zhihu.com/p/602231255 面试官:请描述一下三次握手的过程吧求职者:第一次客户端给服务端发送一个报文,第二次是服务器收到包之后,也给客户端应答一个报文,第三次是客户端再给服务器发送一个回复报文,TCP 三次握手成功。面试官:还有吗?求职者:说完了哈

【转帖】linux 调优篇 :硬件调优(BIOS配置)* 壹

一. 设置内存刷新频率为Auto二. 开启NUMA三. 设置Stream Write Mode四. 开启CPU预取配置五. 开启SRIOV六. 开启SMMU 通过在BIOS中设置一些高级选项,可以有效提升虚拟化平台性能。表1列出了TaiShan服务器和性能相关的BIOS推荐配置项。 表1 BIOS性

[转帖]国产服务器CPU架构与行业研究报告(节选四)

https://zhuanlan.zhihu.com/p/527034350 ​ 目录 收起 4 服务器CPU演进趋势 4.1 CPU优化的传统方式 4.1.1 工艺制程提升 4.1.2 并行度(核数)提升 4.1.3 缓存提升 4.1.4 专用指令集 4.2 CPU提升性能的新趋势 4.2.1 H

[转帖]docker多主机网络方案

http://t.zoukankan.com/bethal-p-6046816.html 本文探讨Docker多主机网络的性能。 在过去的博文里,我测试过 Docker的网络 。 MySQL服务器团队 提供了他们自己的结果,和我的观察是一致的。 本文里一系列的测试,想更多关注使用多主机的Docker

[转帖]微服务的简介和技术栈

https://www.cnblogs.com/PatrickLiu/p/13925259.html 一、简介 这些年软件的设计规模越来越庞大,业务需求也越来越复杂,针对系统的性能、高吞吐率、高稳定性、高扩展等特性提出了更高的要求。可以说业务需求是软件架构能力的第一推动力,由于这些因素导致了软件架构

[转帖]《Linux性能优化实战》笔记(20)—— 使用 tcpdump 和 Wireshark 分析网络流量

tcpdump 和 Wireshark 是最常用的网络抓包和分析工具,更是分析网络性能必不可少的利器。 tcpdump 仅支持命令行格式使用,常用在服务器中抓取和分析网络包。Wireshark 除了可以抓包,还提供了强大的图形界面和汇总分析工具,在分析复杂的网络情景时,尤为简单和实用。因而,在实际分

[转帖]Nginx10万+并发 内核优化

由于默认的linux内核参数考虑的是最通用场景,这明显不符合用于支持高并发访问的Web服务器的定义,所以需要修改Linux内核参数,是的Nginx可以拥有更高的性能; 在优化内核时,可以做的事情很多,不过,我们通常会根据业务特点来进行调整,当Nginx作为静态web内容服务器、反向代理或者提供压缩服

[转帖]Oracle 通过 Exadata 云基础设施 X9M 提供卓越的数据库性能和规模

https://www.modb.pro/db/397202 32个节点的RAC 服务器 每个服务器 两个 64核心的AMD CPU 四个线程干管理 252个线程进行数据库处理 252*32=8064 Exadata Cloud Infrastructure X9M 以相同的价格比上一代产品多 2.