最近处理一个问题,我们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. 基于RabbltMQ延迟插件实现延迟队列代码示例

    上一篇文章写了docker安装RabbitMQ及延迟插件的安装,这篇的话是基于RabbitMQ延迟插件实现延迟队列的示例 那么废话不多说 直接上代码!! 首先创建延迟队列配置类 DelayedQueu ...

  2. Amazon 消息订阅对接

    亚马逊的api 谁用谁知道...... 除了坑还是坑 头疼一周整出来,分享给铁汁们 amazon 的订阅思维,我只能说外国人脑回路有点长 下面就讲讲具体流程步骤: 第一步: 参照官方教程:设置通知(A ...

  3. 使用SSH连接Windows Server 2019 Core

    更新记录 本文迁移自Panda666原博客,原发布时间:2021年7月7日. 一.说明 Windows Server 2019 Core,是纯命令行的Windows Server版本,没有办法使用GU ...

  4. 自定义监控lvs

    1. 修改zabbix_agent配置文件添加以下内容,重启agent Include=/etc/zabbix/zabbix_agentd.d/ 2. 在zabbix安装目录下的scripts目录下添 ...

  5. 前端下载图片的N种方法

    前几天一个简单的下载图片的需求折腾了我后端大佬好几天,最终还是需要前端来搞,开始说不行的笔者最后又行了,所以趁着这个机会来总结一下下载图片到底有多少种方法. 先起个服务 使用expressjs起个简单 ...

  6. 编译调试Net6源码

    前言 编辑调试DotNet源码可按照官网教程操作,但因为网络问题中间会出现各种下载失败的问题,这里出个简单的教程(以6为版本) 下载源码 下载源码 GitHub下载源码速度极慢,可替换为国内仓库htt ...

  7. SQL Server数据库 备份A库,然后删除A库,再还原A库,此时数据库一直显示“正在还原”的解决方法

    SQL Server数据库 备份A库,然后删除A库,再还原A库,此时数据库一直显示"正在还原"的解决方法: A库一直显示"正在还原". 在这种状态下,由于未提交 ...

  8. 实现一个Prometheus exporter

    Prometheus 官方和社区提供了非常多的exporter,涵盖数据库.中间件.OS.存储.硬件设备等,具体可查看exporters.exporterhub.io,通过这些 exporter 基本 ...

  9. Nacos 的安装与服务的注册

    Nacos 的安装与服务的注册 我们都知道naocs是一个注册中心,那么注册中心是什么呢? 什么是注册中心? 它类似与一个中介角色(不收费的良心中介), 在微服务中起纽带的作用,它提供了服务和服务地址 ...

  10. string的底层实现

    String底层实现 string在C++也是一个重要的知识,但是想要用好它,就要知道它的底层是如何写的,才能更好的用好这个string,那么这次就来实现string的底层,但是string的接口功能 ...