不要盲目增加ip_conntrack_max-理解Linux内核内存


1.由ip_conntrack引出的Linux内存映射

有很多文章在讨论关于ip_conntrack表爆满之后丢弃数据包的问题,对此研究深入一些的知道Linux有个内核参数ip_conntrack_max,在拥有较大内存的机器中默认65536,于是疯狂的增加这个参数,比如设置成10000…00,只要不报设置方面的错误,就一定要设置成最大值。这种方式实在是将软件看成大神了,殊不知软件的技术含量还不如锅炉呢!
       如果考虑的再全面一些,比如经验丰富的程序员或者网管,可能会想到内存的问题,他们知道所有的连接跟踪信息都是保存于内存中的,因此会考虑单纯放大这个ip_conntrack_max参数会占据多少内存,会权衡内存的占用,如果系统没有太大的内存,就不会将此值设置的太高。
       但是如果你的系统有很大的内存呢?比如有8G的内存,分个1G给连接跟踪也不算什么啊,这是合理的,然而在传统的32位架构Linux中是做不到,为什么?因为你可能根本不懂Linux内核的内存管理方式。
       内存越来越便宜的今天,linux的内存映射方式确实有点过时了。然而事实就摆在那里,ip_conntrack处于内核空间,它所需的内存必须映射到内核空间,而传统的32位Linux内存映射方式只有1G属于内核,这1G的地址空间中,前896M是和物理内存一一线性映射的,后面的若干空洞之后,有若干vmalloc的空间,这些vmalloc空间和一一映射空间相比,很小很小,算上4G封顶下面的很小的映射空间,一共可以让内核使用的地址空间不超过1G。对于ip_conntrack来讲,由于其使用slab分配器,因此它还必须使用一一映射的地址空间,这就是说,它最多只能使用不到896M的内存!

       为何Linux使用如此“落后”的内存映射机制这么多年还不改进?其实这种对内核空间内存十分苛刻的设计在64位架构下有了很大的改观,然而问题依然存在,即使64位架构,内核也无法做到透明访问所有的物理内存,它同样需要把物理内存映射到内核地址空间后才能访问,对于一一映射,这种映射是事先确定的,对于大小有限(实际上很小)非一一映射空间,需要动态创建页表,页目录等。另外还有一个解释,那就是“内核本来就不该做ip_conntrack这种事”,那是协议栈的事,而不巧的是,Liunx的协议栈完全在内核中实现,可能在skb接收软中断中处理的ip_conntrack不能睡眠,因此也就不能将此任务交给进程,也就不能利用进程地址空间(进程地址空间[用户态+内核态]可以访问所有的物理内存)。

       Linux之所以对内核内存要求如此苛刻,目的就是不想让你随意使用,因为它宝贵,你才更要珍惜它们。

2.在32位架构Linux系统上的实验

以下是为了证明以上的事实所作的实验,可能实验中使用的一些手段仍然不符合常识,然而我觉得成一家之言即可,毕竟这种方案永远不会也不可能出现在公司的标准文档上,那样会让人学会投机取巧或者称偷懒,但是为了备忘,还得有个地方留着,那就写成博客吧。
       还有一个参数会影响查找连接跟踪的时间复杂度和空间复杂度,那就是ip_conntrack_buckets。该值描述了哈希桶的数量,理论上,这个值越大,哈希碰撞就会越小,查找时间就会越快,但是需要为每一个桶预分配一块不是很大的内存,如果桶数量很大,就会占用很大的内存,并且这些内存还都是宝贵的“仅有1G空间内的内核内存”,和ip_conntrack结构体的分配策略不同,这个哈希桶可以分配在vmalloc空间而不一定非要在一一线性映射空间。

2.1.快速压满ip_conntrack的方法

使用loadrunner绝对是一种方式,然而术业有专攻,工作之余我又很讨厌windows上的一切,因此需要采用其它方式,下班在家,只身一人,也不想使用netcat之类的“瑞士军刀”,我怕端口占满,又怕我的macbook狂热,因此需要再想办法。由于目的只是想测试ip_conntrack最多能占用多少内存,其实这个我早就知道了,只是想证实一下子,那么办法也就有了,那就是增加ip_conntrack结构体的大小,而这很容易,只需要在结构体后面增加一个很大的字段即可。下面的修改基于Red Hat Enterprise 5的2.6.18内核

2.2.测试前对ip_conntrack内核模块的修改

编辑$build/include/linux/netfilter_ipv4/ip_conntrack.h文件,在结构体ip_conntrack的最后加上下面一句:
  1. char aaa[102400]; //这个102400是通过二分法得到的,如果设置成2xxxxx则在加载的时候就会使内核crash,因为这个数组是直接分配(类似栈上分配)的而不是动态分配的,它载入的时候很可能会冲掉内核的关键数据,因此还是选取一个可行的数值,然后慢慢加连接吧,毕竟扩大了起码100000倍呢~~  
进入$src/net/ipv4/netfilter,执行:
  1. make –C /lib/modules/2.6.18-92.e15/build SUBDIRS=`pwd` modules  
如此一来加载ip_conntrack.ko之后,内核日志将打印出:
ip_conntrack version 2.4 (8192 buckets, 65536 max) - 102628 bytes per conntrack
由此看出ip_conntrack结构体已经增大了,这样撑满整个可用内存所需的网络连接压力就大大减小了,也就不用什么loadrunner之类的东西了。为了尽快撑满可以使用的内存,还要将关于ip_conntrack的所有timeout设置的比较长,相当长:
  1. sysctl -w net.ipv4.netfilter.ip_conntrack_generic_timeout=600000  
  2. sysctl -w net.ipv4.netfilter.ip_conntrack_icmp_timeout=300000  
  3. sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_close=1000000  
  4. sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_time_wait=120000  
  5. sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_last_ack=300000  
  6. sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_close_wait=60000  
  7. sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_fin_wait=120000  
  8. sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_established=432000  
  9. sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_syn_recv=600000  
  10. sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_syn_sent=120000  
这样既有的一个流就会“永久保持”了,一直占着ip_conntrack结构体不放,直到可用的内存溢出。
在加载了ip_conntrack模块之后,所有过往的数据包就会自动被追踪,下面编写以下脚本:
  1. for (( i=1; i<255; i++));  
  2. do  
  3.     for (( j=1; j<255; j++));  
  4.     do  
  5.         ping 192.168.$i.$j -c 1 -W 1  
  6.         curl --connect-timeout 1 http://138.$i.$j.80/   
  7.         curl --connect-timeout 1 http://38.$i.$j.80/   
  8.         curl --connect-timeout 1 http://$i.1.$j.80/   
  9.         curl --connect-timeout 1 http://$j.$i.9.8/   
  10.     done  
  11. done  
  • 1
  • 2
  • 下一页

相关内容