tcp传输性能下降?也许是内核搞的鬼!

2022-09-27 17:42:20 浏览数 (2)

标题党勿喷,内核可以搞的鬼很多,本文只分析其中一种。 现网问题中,我们经常会遇到一种场景,带宽明明没超限,但是tcp传输性能却不符合预期,而且时快时慢?本文展开分析其中一种常见原因——tcp内存使用太高搞的鬼。

注:本文中涉及代码均为TencentOS内核 5.4.119-19-0007,不过针对本文中的场景,各版本内核代码几乎无差异,因此问题基本在各种内核上通用。

原理说明

查看当前tcp内存使用情况可通过cat /proc/net/sockstat中的mem部分,而调整tcp使用内存的行为可以通过sysctl中的tcp_mem参数。

上一段网上博客的摘抄,取其中精华:

tcp_mem,TCP的内存大小,其单位是页,1页等于4096字节。系统默认值查看方式 cat /proc/sys/net/ipv4/tcp_mem

tcp_mem(3个INTEGER变量):low, pressure, high

  • low:当TCP使用了低于该值的内存页面数时,TCP不会考虑释放内存。
  • pressure:当TCP使用了超过该值的内存页面数量时,TCP试图稳定其内存使用,进入pressure模式,当内存消耗低于low值时则退出pressure状态。
  • high:允许所有tcp sockets用于排队缓冲数据报的页面量,当内存占用超过此值,系统拒绝分配socket,后台日志输出“TCP: too many of orphaned sockets”。

简单来说,当tcp使用内存<=low,一切正常;当>low && <= high,tcp会尝试回收内存,因此tcp传输的各方面会收到一定影响,一个典型的影响就是窗口不会增长 (这里理论层面上,是要第一次>pressure后才进入pressure状态回收内存,然后掉回到low后才恢复正常,不过实际场景下,大部分情况只要处在这个区间内,表现就会比较接近);当>high,tcp out of memory,一切都不工作了,dmesg会打印"too many orphaned sockets"或者"out of memory -- consider tuning tcp_mem"。

代码分析

先从mem部分读取的值来自何方入手:

代码语言:txt复制
static int sockstat_seq_show(struct seq_file *seq, void *v)
{
/*省略*/

        seq_printf(seq, "TCP: inuse %d orphan %d tw %d alloc %d mem %ldn",
                   sock_prot_inuse_get(net, &tcp_prot), orphans,
                   atomic_read(&net->ipv4.tcp_death_row.tw_count), sockets,
                   proto_memory_allocated(&tcp_prot));

/*省略*/

        return 0;

可以看到,来自tcp_prot,其中有关的成员:

代码语言:txt复制
struct proto tcp_prot = {
…
.memory_allocated       = &tcp_memory_allocated,    //mem的值
.memory_pressure        = &tcp_memory_pressure,     //判断是否在pressure模式
.sysctl_mem             = sysctl_tcp_mem,           //sysctl中tcp_mem设置的值
…
};

三者如何关联的?这部分代码可以很直接的说明:

代码语言:txt复制
int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
        struct proto *prot = sk->sk_prot;
        long allocated = sk_memory_allocated_add(sk, amt);    //这里获取tcp_memory_allocated

/*省略*/

        // sk_prot_mem_limits获取sysctl_tcp_mem设置,0、1、2分别代表low、pressure、high
        /* Under limit. */
        if (allocated <= sk_prot_mem_limits(sk, 0)) {
                sk_leave_memory_pressure(sk);
                return 1;
        }
 
        /* Under pressure. */
        if (allocated > sk_prot_mem_limits(sk, 1))
                sk_enter_memory_pressure(sk);    //>pressure 进入pressure模式,见下

        /* Over hard limit. */
        if (allocated > sk_prot_mem_limits(sk, 2))
                goto suppress_allocation;

/*省略*/
}

static void sk_enter_memory_pressure(struct sock *sk)
{
        if (!sk->sk_prot->enter_memory_pressure)
                return;

        sk->sk_prot->enter_memory_pressure(sk);    //tcp_enter_memory_pressure,见下
}

void tcp_enter_memory_pressure(struct sock *s) 
{
        unsigned long val;

        if (READ_ONCE(tcp_memory_pressure))
                return;
        val = jiffies;

        if (!val)
                val--;
        if (!cmpxchg(&tcp_memory_pressure, 0, val))    //修改tcp_memory_pressure值为jiffies
                NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPMEMORYPRESSURES);
}
EXPORT_SYMBOL_GPL(tcp_enter_memory_pressure);

进入pressure模式后有什么影响?这里以最大的影响——窗口无法增长来举例:

代码语言:txt复制
static void tcp_grow_window(struct sock *sk, const struct sk_buff *skb)
{
        struct tcp_sock *tp = tcp_sk(sk);
        int room;

        room = min_t(int, tp->window_clamp, tcp_space(sk)) - tp->rcv_ssthresh;

        /* Check #1 */
        // 这里会判断,如果在pressure状态下,则不会增加窗口,tcp_under_memory_pressure就是读取tcp_memory_pressure的值
        if (room > 0 && !tcp_under_memory_pressure(sk)) {
                int incr;
        
                /* Check #2. Increase window, if skb with such overhead
                 * will fit to rcvbuf in future.
                 */
                if (tcp_win_from_space(sk, skb->truesize) <= skb->len)
                        incr = 2 * tp->advmss;
                else
                        incr = __tcp_grow_window(sk, skb);

                if (incr) {
                        incr = max_t(int, incr, 2 * skb->len);
                        tp->rcv_ssthresh  = min(room, incr);
                        inet_csk(sk)->icsk_ack.quick |= 1;
                }
        }
}

窗口都不涨了,那么传输性能无疑会受影响了。

下面,我们来验证下这个事情。

场景复现

其实我们从大于high时的报错,能够看出如何复现这个场景。

  1. 太多socket,这种情况能在/proc/net/sockstat中的sockets中看出
  2. 单纯的内存使用太多

所以,直接的复现思路是写个多线程server/client发包收包demo,复现1可以搞很多socket连接,复现2则是让互相收发文件,观察内存使用的增长。

但是无论哪个方法,都很麻烦,要先写demo,然后不断调整demo,观察具体的内存增长情况,而显然不同的机型在这上面的处理能力也是不同的,因此观察这个过程意义也不大,只要达到mem使用高的结果,来验证问题可复现即可!

因此我这里的复现方法是曲线救国,hack一把来直接修改内核的tcp_memory_allocated,骗内核tcp内存使用很高。

具体的实现比较暴力,因为tcp协议栈收包会过tcp_recvmsg,那么就在进tcp_recvmsg之前把tcp_memory_allocated改大就好了,大概率不会被改回来,实验了一把果然如此。(小范围的改动方式肯定会更好,不过我们这里只做测试验证的话,理论上不影响)

附上stap代码:

代码语言:txt复制
%{
#include <linux/kernel.h>
#include <linux/net.h>
#include <net/sock.h>
#include <linux/atomic.h>
#include <uapi/linux/tcp.h>
%}


function get_memory_allocated:long(sk:long)
%{
        struct sock *sk = (struct sock*)STAP_ARG_sk;
        STAP_RETVALUE = atomic_long_read(sk->sk_prot->memory_allocated);
%}

function set_memory_allocated(sk:long)
%{
        struct sock *sk = (struct sock*)STAP_ARG_sk;
        atomic_long_set(sk->sk_prot->memory_allocated, 30000);  //这里调整成想调整的mem值
%}

probe kernel.function("tcp_recvmsg")
{
        set_memory_allocated($sk)
}

probe kernel.function("tcp_recvmsg").return
{
        ret = get_memory_allocated(@entry($sk))     //这里验证下是不是真的在tcp协议栈里全程生效
        printf("%lun", ret)
}

测试方式:

搞两台机器,一台server一台client,client发文件server收,一次正常收发,一次server通过上述脚本调整mem到pressure以上后收发,对比收发的时间。

机器2C4G,mem相关设定如下:

代码语言:txt复制
[root@VM-128-19-centos tcp_mem_hook]# sysctl -a | grep tcp_mem
net.ipv4.tcp_mem = 42492        56658   84984

修改前的mem:
[root@VM-128-19-centos ~]# cat /proc/net/sockstat
sockets: used 155
TCP: inuse 6 orphan 0 tw 0 alloc 7 mem 1
UDP: inuse 2 mem 0
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

修改后的mem > pressure:
[root@VM-128-19-centos tcp_mem_hook]# cat /proc/net/sockstat
sockets: used 169
TCP: inuse 7 orphan 0 tw 1 alloc 8 mem 59932
UDP: inuse 2 mem 0
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

修改后的mem > high:
[root@VM-128-19-centos tcp_mem_hook]# cat /proc/net/sockstat
sockets: used 169
TCP: inuse 7 orphan 0 tw 1 alloc 8 mem 199982
UDP: inuse 2 mem 0
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

测试程序可参考我之前文章中的server/client测试程序,程序原始版本出自校长zorro之手。

测试结果(收发4G大小文件):

代码语言:txt复制
[root@VM-128-19-centos ~]# ./server 
avg:25 us, count: 1033343, total: 26090636 us

avg:23 us, count: 1100260, total: 26251547 us

avg:23 us, count: 1101735, total: 26134933 us

avg:54 us, count: 1169491, total: 64043303 us

avg:58 us, count: 1119055, total: 65372606 us

avg:65 us, count: 1114867, total: 72688344 us

avg:433 us, count: 1517596, total: 658690821 us

前三次,是<low的结果;中间三次,是>pressure的结果,可以看到,延迟大了一倍以上;最后一次,是>high的结果(为什么发出去了?是因为socket没有被关闭,此时如果关掉,是无法新建socket的)。

抓包观察窗口变化(这里实验改成4M大小的文件,方便传抓包文件,平均传输时间跟上面表现基本一致):

<low时,窗口增长(绿线),传输很快:

image.pngimage.png

>pressure时,窗口不增长,传输较慢:

image.pngimage.png

至此,理论得到了实践证明。

总结

在现网遇到传输性能不如预期,尤其是不稳定的情况,可以通过查看/proc/net/sockstat中mem的情况,如果很高就符合本文描述的场景。进一步看看,如果socket很多可以lsof查看程序是不是有socket fd泄漏;如果单纯的mem很高,在机器free内存充足的情况下,可以调大tcp_mem观察效果(内存充足的话,一般double一下即可)。

0 人点赞