- 论坛徽章:
- 3
|
本帖最后由 gaojl0728 于 2014-04-29 21:06 编辑
一个Linux Kernel Memory Leak的定位过程。
最近客户那里报了个的OOM问题, 一开始以为是HTTP并发连接数太多,导致TCP在协议栈这一层分配的内存太多,
在这种情况下基本没有很好地解决方案,无非就是调整下TCP的一些参数如tcp_window_scaling, tcp_rmem, tcp_wmem等让单TCP连接占用的低端内存少一些, 这样服务器可以承受更多连接。
后来远程登录到客户的机器研究了下,发现OOM的时候连接数并不多,才300多TCP连接, 所以排除了并发连接数太多的问题,在分析OOM log的时候发现有时OOM发生kernel干掉的进程才释放了200多K的物理内存,
说明当时系统的内存已经极其紧缺,连200K物理内存都不放过,但是干掉进程后系统并没有释放多少内存,紧接着又发生了多次OOM, 于是怀疑耗尽内存的源头不在应用层,而在内核。
用slabtop发现了OOM的根源, 我们的服务器物理内存一共才9G, 内核的ip_dst_cache占去了7G多, 加上其他一些关键业务进程占用的内存, 导致系统内存耗尽。
而且ip_dst_cache的内存一直在增长。
查了下内核源码, ip_dst_cache的内存主要分配给了内核路由表rt_table, 这个问题应该跟内核协议栈相关。
后来公司的TS提供了一些关键的线索, 他们做了一些测试发现在干掉某个进程的时候, ip_dst_cache停止增长,这个进程主要业务就是每隔一秒钟向网络里的其他服务器发送4个多播UDP包用于同步系统状态信息。
现在范围已经很小了, 肯定跟内核的多播IP报文的处理有关系。
于是痛苦的读kernel代码开始了, 分析了从ip_rcv_finish内核收到IP报文开始, 在IP协议栈报文向上传递到ip_route_input_noref->ip_route_input_mc(就是在这里内核从ip_dst_cache分配了rt_table)->ip_local_deliver, 然后一直到UDP多播报文的处理udp_rcv->__udp4_lib_rcv->__udp4_lib_mcast_deliver, 一切流程貌似正常,所有IP/UDP报文分配的rt_table在应用层通过recv系统调用跑到udp_recvmsg的时候会正常释放。
到这里一切都卡住了没有进展,后来分析了一下从客户那里抓的pcap, 发现了一点线索, 就是在这4个多播报文当中,有一个多播报文因为太大(大于1500), 被分成了两个分片。
难道是因为IP分片导致了内存泄露?
读了下内核IP分片重组的那块代码,终于找到了问题所在:)
流程是这样的,ip_local_deliver收到新的IP报文的时候会检查是不是分片, 如果是分片,会调用ip_defrag-->ip_frag_queue->ip_frag_reasm重组分片,
问题就是出在重组分片的时候,上代码:
static int ip_frag_reasm(struct ipq *qp, struct sk_buff *prev,
struct net_device *dev)
{
for (fp = head->next; fp {
if (skb_try_coalesce(head, fp, &headstolen, &delta)) {
kfree_skb_partial(fp, headstolen);
} else {
if (!skb_shinfo(head)->frag_list)
skb_shinfo(head)->frag_list = fp;
head->data_len += fp->len;
head->len += fp->len;
head->truesize += fp->truesize;
}
fp = next;
}
}
在分片重组的时候, 内核合并所有分片到一个skb_buff, 这样之前分片的skb就没用了,内核会调用kfree_skb_partial释放分片,
内存泄露就发生在kfree_skb_partial
这是出问题的版本,内核只是放了skb本身, 并没有有释放skb引用的dst_entry/rt_table
void kfree_skb_partial(struct sk_buff *skb, bool head_stolen)
{
if (head_stolen)
kmem_cache_free(skbuff_head_cache, skb);
else
__kfree_skb(skb);
}
对比了下3.12.6的内核代码,这个问题已经修正了, 就是这行skb_release_head_state(skb):
void kfree_skb_partial(struct sk_buff *skb, bool head_stolen)
{
if (head_stolen) {
skb_release_head_state(skb);
kmem_cache_free(skbuff_head_cache, skb);
} else {
__kfree_skb(skb);
}
}
改完之后重新上线测试, 一切OK, ip_dst_cache一直维持在200K以内不怎么增长。
终于可以松一口气了
需要说明的是我们服务器的内核版本是3.6.3 |
评分
-
查看全部评分
|