标题党勿喷,内核可以搞的鬼很多,本文只分析其中一种。 现网问题中,我们经常会遇到一种场景,带宽明明没超限,但是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
简单来说,当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部分读取的值来自何方入手:
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,其中有关的成员:
struct proto tcp_prot = {
…
.memory_allocated = &tcp_memory_allocated, //mem的值
.memory_pressure = &tcp_memory_pressure, //判断是否在pressure模式
.sysctl_mem = sysctl_tcp_mem, //sysctl中tcp_mem设置的值
…
};
三者如何关联的?这部分代码可以很直接的说明:
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模式后有什么影响?这里以最大的影响——窗口无法增长来举例:
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时的报错,能够看出如何复现这个场景。
所以,直接的复现思路是写个多线程server/client发包收包demo,复现1可以搞很多socket连接,复现2则是让互相收发文件,观察内存使用的增长。
但是无论哪个方法,都很麻烦,要先写demo,然后不断调整demo,观察具体的内存增长情况,而显然不同的机型在这上面的处理能力也是不同的,因此观察这个过程意义也不大,只要达到mem使用高的结果,来验证问题可复现即可!
因此我这里的复现方法是曲线救国,hack一把来直接修改内核的tcp_memory_allocated,骗内核tcp内存使用很高。
具体的实现比较暴力,因为tcp协议栈收包会过tcp_recvmsg,那么就在进tcp_recvmsg之前把tcp_memory_allocated改大就好了,大概率不会被改回来,实验了一把果然如此。(小范围的改动方式肯定会更好,不过我们这里只做测试验证的话,理论上不影响)
附上stap代码:
%{
#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相关设定如下:
[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大小文件):
[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时,窗口增长(绿线),传输很快:
>pressure时,窗口不增长,传输较慢:
至此,理论得到了实践证明。
在现网遇到传输性能不如预期,尤其是不稳定的情况,可以通过查看/proc/net/sockstat中mem的情况,如果很高就符合本文描述的场景。进一步看看,如果socket很多可以lsof查看程序是不是有socket fd泄漏;如果单纯的mem很高,在机器free内存充足的情况下,可以调大tcp_mem观察效果(内存充足的话,一般double一下即可)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。