深入理解TCP链接建立过程

参考资料:深入理解Linux网络

  1. 为什么服务端程序都需要先listen一下?
  2. 半链接队列和全连接队列长度如何确定?
  3. “Cannot assign requested address”这个报错你知道是怎么回事吗?
  4. 一个客户端接口可以同时用在两条链接上吗?
  5. 服务端半/全链接队列满了会怎么样?
  6. 新连接的socket内核对象是什么时候建立的?
  7. 建立一条TCP连接需要消耗多长时间?
  8. 把服务器部署在北京,给纽约的用户访问可行吗?
  9. 服务器负载很正常,但是CPU被打倒底了是怎么回事?

服务端

int fd = socket(AF_INET, SOCK_STREAM, 0);
bind(fd, ...);
listen(fd, 128);
accept(fd, ...);

客户端

int fd = socket(AF_INET, SOCK_STREAM, 0);
connect(fd, ...);

深入理解listen

int fd = socket(AF_INET, SOCK_STREAM, 0);
bind(fd, ...);
listen(fd, 128);
accept(fd, ...);

listen系统调用

  • 根据fd寻找socket内核对象
  • backlog = min(backlog, net.core.somaxconn) (backlog与半连接队列、全连接队列有关)
    • 半连接队列:SYN
    • 全连接队列:ACK

协议栈listen(inet_listen(struct socket *sock,int backlog))

  • 设置全连接队列长度 min(backlog、net.core.somaxconn)
    • 如果遇到全连接队列溢出的问题,需要同时考虑backlog、net.core.somaxconn两个参数
  • 接收队列内核对象的申请和初始化

sudo sysctl net.core.somaxconn

接收队列定义

  • request_sock_queue

  • 服务端需要在第三次握手时快速找出第一次握手时留存的request_sock_queue对象,所以定义了一个哈希表

接收队列的申请和初始化

  • 计算半连接队列长度

    • min(backlog,somaxconn,tcp_max_syn_backlog) + 1再向上取整到2的N次幂,最小不能小于16
  • 内存申请

    • 申请的只是一个指针(哈希表),真正的半连接用的request_sock对象是在握手过程中分配的,计算完哈希值后挂到这个哈希表上
  • 全连接队列头初始化

sudo sysctl net.ipv4.tcp_max_syn_backlog

深入理解Connect

int fd = socket(AF_INET, SOCK_STREAM, 0);
connect(fd, ...);

connect调用链展开

  • 根据用户fd查找内核中的socket对象
  • 设置socket状态 SS_UNCONNECTED ->TCP_SYN_SENT
  • 动态选择端口
  • 根据sk中的信息,构建一个syn报文并发送出去

选择可用端口

  • 根据目的IP和端口等信息动态生成一个随机数
  • 判断是否绑定过端口(客户端指定端口)
  • 获取本地端口配置(net.ipv4.ip_local_port_rang),遍历查找;默认32678~61000
    • 查看是否保留端口(ip_local_reserved_ports)(不希望内核调用的端口,可以写在里面)
    • 查找和遍历已经使用的端口的哈希表
    • 已经被使用的,通过check_established继续检查是否可用
    • 未使用直接用,并添加到哈希表
  • 遍历完所有端口没有可用的就会报错“Cannot assign requested address”

端口被使用怎么办

  • 四元组唯一则可用
  • 客户端单机最大连接数

image-20220914204541095

发起SYN请求

  • 申请并设置skb
  • 添加到发送队列sk_write_queue
  • tcp_transmit_skb实际发出syn
  • 启动重传定时器;重传时间3.10版本是1秒

如果可用端口比较少,大概率需要循环很多轮选择端口,这回导致connect系统调用的CPU开销上涨

完整TCP链接建立过程

服务端响应SYN(tcp_rcv_state_process)

  • 根据网络包(skb)TCP头信息中的目的IP信息查到当前处于listen状态的socket,进入tcp_v4_do_rcv处理握手过程
  • 查找半连接队列,判断半连接队列是否满了
    • 满了&未开启tcp_syncookies,则直接丢弃包
    • 满了&开启tcp_syncookies,继续往下走
  • 在全连接队列满的情况下,如果有young_ack,那么直接丢弃
  • 分配request_sock内核对象
  • 构造syn+ack包
  • 发送syn+ack相应
  • 添加到半连接队列,并开启计时器

客户端响应SYNACK(tcp_rcv_state_process)

  • 删除发送队列

  • 删除定时器

  • 修改socket状态。状态变更为ESTABLISHED

  • 初始化拥塞控制

  • 打开TCP的保活计时器

  • 申请和构造ack包并发送

服务端响应ACK

  • 在半连接队列中查找request_socket对象
  • 创建子socket对象
  • 清理半连接队列
  • 判断全连接队列是否满了,新的socket添加全连接队列
  • 设置连接状态为ESTABLISHED

服务端accept

  • 从全连接队列中获取一个头元素(request_socket)返回给用户进程

一条TCP连接建立要消耗多长时间

  • 第一类:内核消耗CPU接收、发送或者处理,包括系统调用、软中断和上下文切换,耗时基本都是几微妙左右
  • 第二类:网络传输几毫秒到几百毫秒不等

因此一般TCP建立连接的耗时取决于网络延时。

从客户端角度来看,只要ACK包发出了,内核就认为连接成功了,可以开始发送数据了。

异常TCP连接建立情况

connect系统调用耗时失控

  • 平时服务器每秒2000QPS,CPU的idle(空闲占比)70%以上。

  • 服务器4核,负载3(Load Average),cpu被打到底。

排查发现connect系统调用的CPU大幅上涨,又追查发现根本原因是实发当时可用端口不是特别充足。

为啥端口不充足会导致CPU消耗大幅上涨?
  • 大量循环
  • 循环内部需要等待锁以及在哈希表中执行多次的搜索,锁是CAS自旋锁,非阻塞,不断的尝试。
  • 假设接口范围10000~30000,而且已经用尽了,那么每次发起连接的时候都需要循环2万次才退出
  • 正常的connect系统调用耗时22微秒,端口不足耗时2581微秒;100多倍;虽然是微秒,但时间全是CPU时间

image-20220913221011875

第一次握手丢包

image-20220913094406312

  • 查看半连接队列是否满了&tcp_syncookies是否开启
  • 查看全连接队列是否满了&有young_ack存在(未处理完的半连接请求)
  • 客户端发起重试,等不到预期的synack,触发超时重传逻辑。但是重传时间都是以秒计算的,那么意味着即使一次重传成功,也是1秒后的事情了
  • 重试次数:net.ipv4.tcp_syn_retries,不是简单的次数比对,转化为时间进行对比。下一次重传时间是前一次的两倍

第三次握手丢包

image-20220913094340958

  • 全连接队列满了,直接丢弃
  • 第三次握手失败,并不是客户端重试,而是由服务端重发synack
  • 客户端发送完ack之后就会认为连接成功了,紧接着推送PSH,其实这个时候连接还没有真的建立起来。

image-20220913094306193

握手异常小结

  • 打开syncookies——防止SYN Flood攻击。
  • 加大连接队列长度——ss -nlt
  • 尽快调用accept——尽早从全连接队列里面取socket
  • 尽早拒绝——Redis、MySQL等服务器的内核参数tcp_abort_on_overflow=1.队列满了直接发送reset命令给客户端,告诉客户端不要等待重试了,直接报错“connect reset by peer”。牺牲一个用户,比网站崩了要好。
  • 尽量减少tcp连接次数——长连接

sudo sysctl net.ipv4.tcp_syncookies

如何查看是否有连接队列溢出

全连接队列

  • watch 'netstat -s | grep overflowed' 输出xxx times the listen queue of a socket overflowed;查看xxx次数有没有发生变化

半连接队列溢出判断

  • tcp_syncookies打开就不会半连接队列溢出,建议打开
  • 因为各种原因不想打开。netstat -antp|grep SYN_RECV查看SYN_RECV的数量与计算的队列长度对比。

总结

半/全连接的创建与长度限制、客户端端口的选择、半/全连接队列的添加删除以及重传定时器的启动。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇