背景

最近小伙伴讲到 TCP_USER_TIMEOUT 这个tcp 参数, 今天特意学习下

深入

  1. TCP_USER_TIMEOUT 用来做什么?和其他参数会不会相互作用?

TCP_USER_TIMEOUT 是用来超时检测. 当上个健康的package 发出去一段时间后依旧没有消息返回, 会触发 TCP_USER_TIMEOUT 机制. 当时这个不是100%触发, 比如 下游 recv buffer 满了, 导致 client send buffer 也堵塞重传 (swnd=0), 这个时候 TCP_USER_TIMEOUT 是不会计数的

  1. 什么场景用?

client 最好都添加上, 但是如果有 keepalive 的设计, 需要保证:

TCP_USER_TIMEOUT >= TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT. 

文章学习

文章: https://blog.cloudflare.com/when-tcp-sockets-refuse-to-die/

TCP_USER_TIMEOUT: 文章里讲到 linux kernel 5.2 并没有效果, 对 client connect sync retry 没有限制

TCP_DEFER_ACCEPT:

	final-ack 没有传递的时候, server 还是会将 fd 将 sync-recv 移动到 estab
	因此, 设计了 TCP_DEFER_ACCEPT 来避免这个情况

java tcp_so_timeout

tcp keepalive 的学习

SO_KEEPALIVE = 1 - Let's enable keepalives.
TCP_KEEPIDLE = 5 - Send first keepalive probe after 5 seconds of idleness.
TCP_KEEPINTVL = 3 - Send subsequent keepalive probes after 3 seconds.
TCP_KEEPCNT = 3 - Time out after three failed probes.

不同场景的讨论:

  • 空闲连接下, TCP_USER_TIMEOUT 会影响到 KEEPALIVE, 导致 keepalive 的连接被关闭. 也就是说 心跳消息不会重置 TCP_USER_TIMEOUT 的超时 计算; 如果 TCP_USER_TIMEOUT 设置的特别大, 还会使 TCP_KEEPCNT 无效.

  • send unack 消息的连接, TCP_USER_TIMEOUT 会影响到 KEEPALIVE, 导致连接因为长期没有接收到 相应而关闭

  • server 处理慢, client sender 被流控. 这个时候 server read buffer 被填满然后流控了, 返回滑动窗口 0, sender send buffer 也是满的, 这个时候 TCP_USER_TIMEOUT 会暂停直到 win不为0 才重新计算.

所以总结下来, TCP_USER_TIMEOUT 是不受重传影响的, 重传不会导致 TCP_USER_TIMEOUT 更新. 文章中还提供, 对于慢连接(server处理慢), 不在使用应用的超时, 而是使用 TCP_USER_TIMEOUT, 这样更加有效

推荐配置:

  1. Enable TCP keepalives
  2. Set TCP_USER_TIMEOUT to TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT.
  3. 一般使用 TCP keepalives and user-timeout 足够了, 如果希望降低资源及时清理超时的连接, 可以 ioctl (统计unsent+unacked) 和 TCP_INFO (tcp unsent)

学习

对于我们而言, 这个有什么意义? 主要是 TCP_USER_TIMEOUT, 没有设置的情况下, 如果下游主机直接挂了, client 没有感知, 那么 client 还在不断的发送数据 直到 sendbuffer 满了, 这个时候 write timeout 超时 才会导致连接关闭, 但是这个时间 已经很长了, 对业务是有损的. 因此 通过设置 TCP_USER_TIMEOUT 能够解决问题

补充

在 grpc 的实现中, 当开启keepalive的时候, 会将超时设置到 TPCUserTimeout 中

	if kp.Time != infinity {
		if err = syscall.SetTCPUserTimeout(conn, kp.Timeout); err != nil {
			return nil, connectionErrorf(false, err, "transport: failed to set TCP_USER_TIMEOUT: %v", err)
		}
		keepaliveEnabled = true
	}