前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >tcp传输性能下降?也许是内核搞的鬼!

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

原创
作者头像
johnazhang
发布2022-09-27 17:42:20
1.5K0
发布2022-09-27 17:42:20
举报
文章被收录于专栏:Linux问题笔记Linux问题笔记

标题党勿喷,内核可以搞的鬼很多,本文只分析其中一种。 现网问题中,我们经常会遇到一种场景,带宽明明没超限,但是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 %ld\n",
                   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("%lu\n", 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.png
image.png

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

image.png
image.png

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

总结

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 原理说明
  • 代码分析
  • 场景复现
  • 总结
相关产品与服务
云服务器
云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档