最近处理一个问题,我们nginx服务器作为透明代理,将核心网过来的用户上网请求代理到我们的cache服务器,如果cache服务器没有命中内容,则需要我们

作为客户端往源站请求内容,但用户对此一无所知,也就是我们使用透明代理的模式来给用户提供上网服务。

问题出在:我们作为客户端,往服务器端请求数据。服务器端主动断链之后,我们使用相同的ip和端口去连接服务器端,发现syn 没有得到响应。

从图中TCP Port numbers reused 开始这行可以看出:

106.332208  我们服务器在收到源站的主动断链请求

106.371754  我们服务器发送了针对源站主动fin的ack。

107.597531 我们服务器收到用户的一个GET 请求,

107.598388 我们服务器调用close(socket),触发内核发送了fin请求给源站。

107.605880 我们服务器收到源站返回的针对我们fin的ack,在此,四次挥手结束。那么主动断链的源站,肯定处于time_wait状态。

107.636754 我们服务器收到用户的一个ack,这个因为我们服务器使用用户的ip和端口跟源站交互,所以ip和端口是一样的,所以只能从Seq,Ack,或者mac地址来区分链路。

倒数的四个报文:

109.597985 我们服务器使用新的socket,但是ip和端口跟之前的链路一样,往源站进行connect,触发内核发送syn请求,

110.600579 我们服务器的第一个syn未收到回复,重发该请求。1s超时

112.604765 我们服务器退避发送syn请求。2s超时

116.613191 我们服务器在退避之后,4s超时,达到tcp_syn_retries 设置的2次上限,无奈给用户回复502.

报文分析完毕,我们在排除丢包的情况下,想想源站为什么会对我们的syn无动于衷。

下面都是假设源站是linux 3.10下的实现。

由于源站是主动断链,在回复给我们服务器的fin的ack之后,进入time_wait状态。

int tcp_v4_rcv(struct sk_buff *skb)
{
。。。
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)----这里搜出来的sk,其实是inet_timewait_sock
goto no_tcp_socket; process:
if (sk->sk_state == TCP_TIME_WAIT)----------大状态是time_wait,大状态下又分为两个子状态,如fin_wait2,time_wait
goto do_time_wait;
。。。
do_time_wait:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
inet_twsk_put(inet_twsk(sk));
goto discard_it;
} if (skb->len < (th->doff << 2)) {
inet_twsk_put(inet_twsk(sk));
goto bad_packet;
}
if (tcp_checksum_complete(skb)) {
inet_twsk_put(inet_twsk(sk));
goto csum_error;
}
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {----返回四种结果
case TCP_TW_SYN: {----------合理的syn,处理建联请求
struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),---------查找监听socket
&tcp_hashinfo,
iph->saddr, th->source,
iph->daddr, th->dest,
inet_iif(skb));
if (sk2) {---找到对应listen的socket,则继续处理,注意这个sk已经是listen的sk了。
inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
inet_twsk_put(inet_twsk(sk));
sk = sk2;
goto process;
}
/* Fall through to ACK */---------没找到listen的socket的话,则没有break,会进入下面的TCP_TW_ACK,回复ack并丢弃skb
}
case TCP_TW_ACK:---------回ack
tcp_v4_timewait_ack(sk, skb);
break;
case TCP_TW_RST:---------关闭链路
tcp_v4_send_reset(sk, skb);---发送rst包给对端,
inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
inet_twsk_put(inet_twsk(sk));
goto discard_it;
case TCP_TW_SUCCESS:;----虽然叫success,但是什么都不做,空语句,最终会走到discrad_it
}
goto discard_it;
}
}

为了减少一点内存占用,在tcp_time_wait 函数中,将处于timewait状态的sock 替换为了 inet_timewait_sock 。

crash> p sizeof(struct tcp_sock)
$5 = 1968
crash> p sizeof(struct inet_timewait_sock)
$6 = 152

也就是处于time_wait状态的socket比处于正常状态的socket少占用了1.8k内存,对于很多服务器来说,timewait状态下的socket比较多,算起来也很可观了,所以,linux又设计了一个

tcp_max_tw_buckets 来限制处于time_wait的数量。

这个也是 tcp_timewait_state_process(inet_twsk(sk), skb, th) 中能够将sock直接转换为 inet_timewait_sock 的原因。

从流程看,需要分析 tcp_timewait_state_process 的处理:

enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
const struct tcphdr *th)
{
struct tcp_options_received tmp_opt;
struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
bool paws_reject = false; tmp_opt.saw_tstamp = 0;
if (th->doff > (sizeof(*th) >> 2) && tcptw->tw_ts_recent_stamp) {
tcp_parse_options(skb, &tmp_opt, 0, NULL); if (tmp_opt.saw_tstamp) {
tmp_opt.rcv_tsecr -= tcptw->tw_ts_offset;
tmp_opt.ts_recent = tcptw->tw_ts_recent;
tmp_opt.ts_recent_stamp = tcptw->tw_ts_recent_stamp;
paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
}
}--------------这个是时间戳的检查,我们自己作为请求方但是没有开启时间戳,所以paws_reject为0,saw_tstamp为0. if (tw->tw_substate == TCP_FIN_WAIT2) {-----根据挥手流程,处于fin_wait2状态的socket会在收到fin之后迁入time_wait状态,这个是指tw_substate也是time_wait状态
/* Just repeat all the checks of tcp_rcv_state_process() */ /* Out of window, send ACK */
if (paws_reject ||--------如注释,超过接收包的tcp窗口。则走oow流程
!tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
tcptw->tw_rcv_nxt,
tcptw->tw_rcv_nxt + tcptw->tw_rcv_wnd))
return tcp_timewait_check_oow_rate_limit(
tw, skb, LINUX_MIB_TCPACKSKIPPEDFINWAIT2); if (th->rst)---收到rst包,直接kill,但是要注意的是,kill返回的其实是 TCP_TW_SUCCESS,也就是啥都不干。
goto kill; if (th->syn && !before(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt))---在fin_wait2状态,收到syn,并且seq小于我们需要接收的nxt,则rst掉,认为是过期的syn
return TCP_TW_RST; /* Dup ACK? */
if (!th->ack ||---没有ack标志,则丢弃,说明走到这的肯定都带ack标志,因为就算是fin,ack标志也是设置的。
!after(TCP_SKB_CB(skb)->end_seq, tcptw->tw_rcv_nxt) ||-----有ack标志,但是end_seq在窗口左边,也就是oow,有可能是重复ack,丢弃
TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq) {---是纯ack,我们是因为收到fin-ack才进入的fin-wait2,现在又来个纯ack,不是fin,也不是syn,丢弃
inet_twsk_put(tw);
return TCP_TW_SUCCESS;
} /* New data or FIN. If new data arrive after half-duplex close,
* reset.
*/
if (!th->fin ||---不带fin标志,直接rst掉
TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1)---是fin包,收到的seq有数据,rst掉,看这意思,不能fin带数据。
return TCP_TW_RST; /* FIN arrived, enter true time-wait state. */
tw->tw_substate = TCP_TIME_WAIT;----------到这的,肯定是有fin标志的,否则前面就返回了,fin-wait2收到fin,迁入time_wait状态,此时子状态也是time_wait了
tcptw->tw_rcv_nxt = TCP_SKB_CB(skb)->end_seq;
if (tmp_opt.saw_tstamp) {
tcptw->tw_ts_recent_stamp = get_seconds();
tcptw->tw_ts_recent = tmp_opt.rcv_tsval;
} if (tcp_death_row.sysctl_tw_recycle &&-----开启了tw_recyle的情况下,
tcptw->tw_ts_recent_stamp &&----------开启了时间戳的情况下下
tcp_tw_remember_stamp(tw))
inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,---设置超时为tw_timeout,这个跟链路相关,在tcp_time_wait 中设置为3.5*RTO。
TCP_TIMEWAIT_LEN);
else
inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,----没有设置时间戳和tw_recyle,则默认的60s,这个值是写死的,尼玛也不让改,只能编译内核
TCP_TIMEWAIT_LEN);
return TCP_TW_ACK;
}-------------如果子状态是fin-wait2,则在这个里面处理 /*
* Now real TIME-WAIT state.---------------------本文syn发送的时候,服务器应该处于这个状态,下面就是服务器收到本syn该执行的代码
*
* RFC 1122:
* "When a connection is [...] on TIME-WAIT state [...]
* [a TCP] MAY accept a new SYN from the remote TCP to
* reopen the connection directly, if it:-----------------在timewait状态下重新open的条件:
*
* (1) assigns its initial sequence number for the new----初始seq比之前老链路ack的序号大
* connection to be larger than the largest sequence
* number it used on the previous connection incarnation,
* and
*
* (2) returns to TIME-WAIT state if the SYN turns out
* to be an old duplicate".
*/ if (!paws_reject &&------------防回绕校验失败
(TCP_SKB_CB(skb)->seq == tcptw->tw_rcv_nxt &&-------当前需要和预期的序号相同且纯fin或者纯rst,
(TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq || th->rst))) {----rst标志被置位
/* In window segment, it may be only reset or bare ack. */ if (th->rst) {------我们已经处于timewait状态,收到rst,
/* This is TIME_WAIT assassination, in two flavors.
* Oh well... nobody has a sufficient solution to this
* protocol bug yet.
*/
if (sysctl_tcp_rfc1337 == 0) {
kill:
inet_twsk_deschedule(tw, &tcp_death_row);
inet_twsk_put(tw);
return TCP_TW_SUCCESS;--------丢弃这个包
}
}
inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
TCP_TIMEWAIT_LEN); if (tmp_opt.saw_tstamp) {----有时间戳选项的话,更新时间戳
tcptw->tw_ts_recent = tmp_opt.rcv_tsval;
tcptw->tw_ts_recent_stamp = get_seconds();
} inet_twsk_put(tw);
return TCP_TW_SUCCESS;--------丢弃这个包
}--------------显然,我们的syn不满足这个if /* Out of window segment. All the segments are ACKed immediately. The only exception is new SYN. We accept it, if it is
not old duplicate and we are not in danger to be killed
by delayed old duplicates. RFC check is that it has
newer sequence number works at rates <40Mbit/sec.
However, if paws works, it is reliable AND even more,
we even may relax silly seq space cutoff. RED-PEN: we violate main RFC requirement, if this SYN will appear
old duplicate (i.e. we receive RST in reply to SYN-ACK),
we must return socket to time-wait state. It is not good,
but not fatal yet.
*/ if (th->syn && !th->rst && !th->ack && !paws_reject &&-------我们的syn包不含rst标志,也没有ack标志,但没有开启时间戳选项,所以paws_reject为0.满足条件
(after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) ||-------我们的syn的序号是2972897916,而老的链路的tw_rcv_nxt为2674663925,满足条件,按道理就&&条件满足
(tmp_opt.saw_tstamp &&----------------------------------有时间戳选项的话
(s32)(tcptw->tw_ts_recent - tmp_opt.rcv_tsval) < 0))) {---且时间戳条件满足
u32 isn = tcptw->tw_snd_nxt + 65535 + 2;
if (isn == 0)
isn++;
TCP_SKB_CB(skb)->tcp_tw_isn = isn;
return TCP_TW_SYN;
} if (paws_reject)
NET_INC_STATS_BH(twsk_net(tw), LINUX_MIB_PAWSESTABREJECTED); if (!th->rst) {-----其他情况处理,如不是有效的syn,比如序列号在window之前,ack包,但oow,
/* In this case we must reset the TIMEWAIT timer.
*
* If it is ACKless SYN it may be both old duplicate
* and new good SYN with random sequence number <rcv_nxt.
* Do not reschedule in the last case.
*/
if (paws_reject || th->ack)
inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
TCP_TIMEWAIT_LEN); return tcp_timewait_check_oow_rate_limit(
tw, skb, LINUX_MIB_TCPACKSKIPPEDTIMEWAIT);
}
inet_twsk_put(tw);
return TCP_TW_SUCCESS;
}

为了防止回绕,一般我们通过开启 /proc/sys/net/ipv4/tcp_timestamps 来防止回绕,也就是PAWS(Protect Against Wrapped Sequence numbers) 。

在本案例中,我们发送的syn,按道理是符合条件的,对方为啥一点反应都没有呢?为了弄清楚这个问题,我们发了一堆命令给源站,源站表示看不懂,

后来才知道,因为他们是windows系统来提供网站服务的,因此不能继续分析了。当然也不是没有任何收获,毕竟对于大多数linux服务器的实现流程更

清楚了,从代码看,如果是linux服务器,就算没有建联成功,好歹会回复一个ack,而不是像目前这样啥都不回,导致请求端重传并超时。

状态问题:

tcp        0      0 10.47.242.207:8000      10.47.242.118:7000      FIN_WAIT2   7344/tcp_server.o

12: CFF22F0A:1F40 76F22F0A:1B58 05 00000000:00000000 00:00000000 00000000     0        0 5271486 1 ffff940408230f80 20 4 30 10 -1 

根据/proc/net/tcp中的显示,当状态5,也就是 TCP_FIN_WAIT2,因为:

static int tcp4_seq_show(struct seq_file *seq, void *v)
{
...
switch (st->state) {
case TCP_SEQ_STATE_LISTENING:
case TCP_SEQ_STATE_ESTABLISHED:
if (sk->sk_state == TCP_TIME_WAIT)------------当状态为time-wait的时候,会显示子状态
get_timewait4_sock(v, seq, st->num, &len);
else
get_tcp4_sock(v, seq, st->num, &len);
break;
case TCP_SEQ_STATE_OPENREQ:
get_openreq4(st->syn_wait_sk, v, seq, st->num, st->uid, &len);
break;
}
...}

对参数理解的收获:

net.ipv4.tcp_tw_recycle = 0 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭,开启时,回收的时间为3.5*RTO。

net.ipv4.tcp_fin_timeout = 60 表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间,如果发送fin的一端是使用shutdown方式来关闭写的一端,

则这个状态可能会维持很长很长,而不是这个60s。

我通过写的简单的tcp的一个简单例子来模拟源站,发现了只要没有将服务器端缓冲区的数据recv干净,调用close的话,会发rst,recv干净之后再调用close的话,会发fin。

void tcp_close(struct sock *sk, long timeout)
{
。。。
else if (data_was_unread) {
/* Unread data was tossed, zap the connection. */
NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, sk->sk_allocation);
}
。。。。
else if (tcp_close_state(sk)) {
tcp_send_fin(sk);
}
}

Q:开启了tw_recycle,也就是快速回收,那么回收的速度是多快呢?

tcp_time_wait函数中,该值为3.5倍的RTO。
void tcp_time_wait(struct sock *sk, int state, int timeo)
{
。。。
if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
recycle_ok = tcp_remember_stamp(sk); if (tcp_death_row.tw_count < tcp_death_row.sysctl_max_tw_buckets)
tw = inet_twsk_alloc(sk, state); if (tw != NULL) {
struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);//3.5*rto
。。。
if (recycle_ok) {//开启tw 快速回收,则超时时间很短
tw->tw_timeout = rto;//这个rto其实是3.5倍的rtt
} else {
tw->tw_timeout = TCP_TIMEWAIT_LEN;
if (state == TCP_TIME_WAIT)
timeo = TCP_TIMEWAIT_LEN;
}
。。。
}

也就是说,开启recycle,则回收tw的socket时间为3.5倍的rto。

Q.timewait定时器到期后,怎么释放这些tw的资源

inet_twdr_hangman 函数负责干这事。具体可以在设置timer的时候看到,tcp_death_row 是处理所有tw状态的一个结构,包括设置定时器,锁,清理tw等。它分为快慢的两种timer,一种是正常处理2MSL的timer,一种是快速回收的tw的timer。具体可以查看 inet_twsk_schedule ,两种timer分别调用inet_twdr_hangman,inet_twdr_twcal_tick最终调用的都是 __inet_twsk_kill来回收资源。

 

linux tcp 在timewait 状态下的报文处理的更多相关文章

  1. TCP/IP协议栈(三)——linux 向下的报文处理

    应用程序连接服务器时,目的地套接字地址(端口号和IP地址)以参数形式传递给系统调用connect(tcp_v4_connect()).下面逐步介绍初始化该连接 检查内核路由表,查找给定目的地IP地址路 ...

  2. Linux:TCP状态/半关闭/2MSL/端口复用

    TCP状态 CLOSED:表示初始状态. LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接. SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行 ...

  3. TCP三次握手和Time-Wait状态

    第一次握手:建立连接时.client发送syn包和一个随机序列号seq=x到server,并进入SYN_SEND状态,等待server进行确认. (syn,同 步序列编号). 第二次握手,server ...

  4. TCP协议端口状态说明:CLOSE-WAIT、TIME-WAIT 、LISTENING、SYN_SENT、ESTABLISHED、LAST-ACK ...

    了解TCP协议端口的连接状态,对排除和定位网络或系统故障会有很大帮助,因此了解一下是有必要的: 一.LISTENING  提供某种服务,侦听远方TCP端口的连接请求,当提供的服务没有被连接时,处于LI ...

  5. Linux TCP不同状态的连接数统计

    方法一:利用netstat命令 统计 TIME_WAIT/CLOSE_WAIT/ESTABLISHED/LISTEN 等TCP状态的连接数 netstat -tan |grep ^tcp |awk ' ...

  6. linux 免交互状态下修改用户密码

    当利用某些工具对linux用户进行远程密码更改时,输入[ passwd 用户名 ] 后需要输入两次密码, 但是如果你利用的某些工具无法与linux进行交互的情况下,就没办法变更用户密码了,这个时候可以 ...

  7. Linux TCP/IP调优-Linux内核参数注释

    固定文件的内核参数 下列文件所在目录: /proc/sys/net/ipv4/ 名称 默认值 建议值 描述 tcpsyn_retries 5 1 对于一个新建连接,内核要发送多少个SYN连接请求才决定 ...

  8. tcp连接的状态变迁以及如何调整tcp连接中处于time_wait的时间

    一.状态变迁图 二.time_wait状态 针对time_wait和close_wait有个简单的描述帮助理解: Due to the way TCP/IP works, connections ca ...

  9. 转载:TCP连接的状态详解以及故障排查

    FROM:http://blog.csdn.net/hguisu/article/details/38700899 该博文的条理清晰,步骤明确,故复制到这个博文中收藏,若文章作者看到且觉得不能装载,麻 ...

随机推荐

  1. 基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录

    在我们对数据进行重要修改调整的时候,往往需要跟踪记录好用户操作日志.一般来说,如对重要表记录的插入.修改.删除都需要记录下来,由于用户操作日志会带来一定的额外消耗,因此我们通过配置的方式来决定记录那些 ...

  2. 深度学习与计算机视觉教程(15) | 视觉模型可视化与可解释性(CV通关指南·完结)

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  3. docker和docker compose安装使用、入门进阶案例

    一.前言 现在可谓是容器化的时代,云原生的袭来,导致go的崛起,作为一名java开发,现在慌得一批.作为知识储备,小编也是一直学关于docker的东西,还有一些持续继承jenkins. 提到docke ...

  4. 【翻译】 For OData For C# play on RESTier

    要获得统一的体验,请转到GitHub Issues询问问题,报告错误并要求功能.本文档适用于当前版本 1.0(第一个 GA).0.6.0版本文档参考0.6.0版本文档. 入门 1.1引言 OData ...

  5. Linux开放指定端口命令(CentOS)

    1.开启防火墙 systemctl start firewalld 2.开放指定端口 ##linux打开防火墙3389端口 firewall-cmd --zone=public --add-port= ...

  6. 简单到爆——用Python在MP4和GIF间互转,我会了

    写在前面的一些P话: 昨天用公众号写文章的时候,遇到个问题.我发现公众号插入视频文件太繁琐,一个很小的视频,作为视频传上去平台还要审核,播放的时候也没gif来的直接.于是想着找个工具将mp4转换成gi ...

  7. Spring Boot 整合 minio(一步到位)

    按照这个步骤来,宝贝保你一步到位 一.minio版本安装:这里我安装的新版本 新版本安装 # docker 下载镜像 docker pull minio/minio # 安装镜像 docker run ...

  8. 【Azure Developer】记录一次使用Java Azure Key Vault Secret示例代码生成的Jar包,单独运行出现 no main manifest attribute, in target/demo-1.0-SNAPSHOT.jar 错误消息

    问题描述 创建一个Java Console程序,用于使用Azure Key Vault Secret.在VS Code中能正常Debug,但是通过mvn clean package打包为jar文件后, ...

  9. Tomcat深入浅出——Servlet(二)

    一.Servlet简介 Servlet类最终开发步骤: 第一步:编写一个Servlet类,直接继承HttpServlet 第二步:重写doGet方法或者doPost方法,重写哪个我说的算! 第三步:将 ...

  10. labview从入门到出家7(进阶篇)--队列的使用

    本节简单讲解队列在Labview中的使用,队列你可以认为就是一组先进先出的数据列表,在Labview中常用来缓存和传递数据.用了这么久的队列,个人认为有个方便的地方在于数据传递的把控,不管是局部变量还 ...