Linux 作为一个强大的操作系统, 提供了一系列内核参数供我们进行调优. 光 TCP 的调优参数就有 50 多个. 在和线上问题斗智斗勇的过程中, 笔者积累了一些在内网环境应该进行调优的参数. 在此分享出来, 希望对大家有所帮助.
调优清单
好了, 在这里先列出调优清单. 请记住, 这里只是笔者在内网进行 TCP 内核参数调优的经验, 仅供参考. 同时, 笔者还会在余下的博客里面详细解释了为什么要进行这些调优!
序号 | 内核参数 | 值 | 备注 |
---|---|---|---|
1.1 | /proc/sys/net/ipv4/tcp_max_syn_backlog | 2048 | |
1.2 | /proc/sys/net/core/somaxconn | 2048 | |
1.3 | /proc/sys/net/ipv4/tcp_abort_on_overflow | 1 | |
2.1 | /proc/sys/net/ipv4/tcp_tw_recycle | 0 | NAT 环境必须为 0 |
2.2 | /proc/sys/net/ipv4/tcp_tw_reuse | 1 | |
3.1 | /proc/sys/net/ipv4/tcp_syn_retries | 3 | |
3.2 | /proc/sys/net/ipv4/tcp_retries2 | 5 | |
3.3 | /proc/sys/net/ipv4/tcp_slow_start_after_idle | 0 |
tcp_max_syn_backlog,somaxconn,tcp_abort_on_overflow
tcp_max_syn_backlog,somaxconn,tcp_abort_on_overflow 这三个参数是关于
内核 TCP 连接缓冲队列的设置. 如果应用层来不及将已经三次握手建立成功的 TCP 连接从队列中取出, 溢出了这个缓冲队列 (全连接队列) 之后就会丢弃这个连接. 如下图所示:
从而产生一些诡异的现象, 这个现象诡异之处就在于, 是在 TCP 第三次握手的时候丢弃连接
就如图中所示, 第二次握手的 SYNACK 发送给 client 端了. 所以就会出现 client 端认为连接成功, 而 Server 端确已经丢弃了这个连接的现象! 由于无法感知到 Server 已经丢弃了连接.
所以如果没有心跳的话, 只有在发出第一个请求后, Server 才会发送一个 reset 端通知这个连接已经被丢弃了, 建立连接后第二天再用, 也会报错! 所以我们要调大 Backlog 队列!
- echo 2048> /proc/sys.NET/ipv4/tcp_max_syn_backlog
- echo 2048> /proc/sys.NET/core/somaxconn
当然了, 为了尽量避免第一笔调用失败问题, 我们也同时要设置
echo 1> /proc/sys.NET/ipv4/tcp_abort_on_overflow
设置这个值以后, Server 端内核就会在这个连接被溢出之后发送一个 reset 包给 client 端.
如果我们的 client 端是 NIO 的话, 就可以收到一个 socket close 的事件以感知到连接被关闭!
注意 Java 默认的 Backlog 是 50
这个 TCP Backlog 的队列大小值是 min(tcp_max_syn_backlog,somaxconn, 应用层设置的 backlog), 而 Java 如果不做额外设置, Backlog 默认值仅仅只有 50.C 语言在使用 listen 调用的时候需要传进 Backlog 参数.
tcp_tw_recycle
tcp_tw_recycle 这个参数一般是用来抑制 TIME_WAIT 数量的, 但是它有一个副作用. 即在 tcp_timestamps 开启(Linux 默认开启),tcp_tw_recycle 会经常导致下面这种现象.
也即, 如果你的 Server 开启了 tcp_tw_recycle, 那么别人如果通过 NAT 之类的调用你的 Server 的话, NAT 后面的机器只有一台机器能正常工作, 其它情况大概率失败. 具体原因呢由下图所示:
在 tcp_tw_recycle=1 同时 tcp_timestamps(默认开启的情况下), 对同一个 IP 的连接会做这样的限制, 也即之前后建立的连接的时间戳必须要大于之前建立连接的最后时间戳, 但是经过 NAT 的一个 IP 后面是不同的机器, 时间戳相差极大, 就会导致内核直接丢弃时间戳较低的连接的现象. 由于这个参数导致的问题, 高版本内核已经去掉了这个参数. 如果考虑 TIME_WAIT 问题, 可以考虑设置一下
- echo 1> /proc/sys.NET/ipv4/tcp_tw_reuse
- tcp_syn_retries
这个参数值得是 client 发送 SYN 如果 server 端不回复的话, 重传 SYN 的次数. 对我们的直接影响呢就是 connet 建立连接时的超时时间. 当然 Java 通过一些 C 原生系统调用的组合使得我们可以进行超时时间的设置. 在 Linux 里面默认设置是 5, 下面给出建议值 3 和默认值 5 之间的超时时间.
tcp_syn_retries | timeout |
---|---|
1 | min(so_sndtimeo,3s) |
2 | min(so_sndtimeo,7s) |
3 | min(so_sndtimeo,15s) |
4 | min(so_sndtimeo,31s) |
5 | min(so_sndtimeo,63s) |
下图给出了, 重传和超时情况的对应图:
当然了, 不同内核版本的超时时间可能不一样, 因为初始 RTO 在内核小版本间都会有细微的变化. 所以, 有时候在抓包时候可能会出现 (3,6,12......) 这样的序列. 当然 Java 的 API 有超时时间:
java:
- // 函数调用中携带有超时时间
- public void connect(SocketAddress endpoint, int timeout) ;
所以, 对于 Java 而言, 这个内核参数的设置没有那么重要. 但是, 有些代码可能会有忘了设置 timeout 的情况, 例如某个版本的 Kafka 就是, 所以它在我们一些混沌测试的情况下, 容灾恢复的时间会达到一分多钟, 主要时间就是卡在 connect 上面 -_-!, 而这时我们的 tcp_syn_retries 设置的是 5, 也即超时时间 63s. 减少这个恢复时间的手段就是:
- echo 3> /proc/sys.NET/ipv4/tcp_syn_retries
- tcp_retries2
tcp_retries2 这个参数表面意思是在传输过程中 tcp 的重传次数. 但在某个版本之后 Linux 内核仅仅用这个 tcp_retries2 来计算超时时间, 在这段时间的重传次数纯粹由 RTO 等环境因素决定, 重传超时时间在 5/15 下的表现为:
tcp_retries2 | 对端无响应 |
---|---|
5 | 25.6s-51.2s 根据动态 rto 定 |
15 | 924.6s-1044.6s 根据动态 rto 定 |
如果我们在应用层设置的 Socket 所有 ReadTimeout 都很小的话(例如 3s), 这个内核参数调整是没有必要的. 但是, 笔者经常发现有的系统, 因为一两个慢的接口或者 SQL, 所以将 ReadTimeout 设的很大的情况.
平常这种情况是没有问题的, 因为慢请求频率很低, 不会对系统造成什么风险. 但是, 物理机突然宕机时候的情况就不一样了, 由于 ReadTimeOut 设置的过大, 导致所有落到这台宕机的机器都会在 min(ReadTimeOut,(924.6s-1044.6s)(Linux 默认 tcp_retries2 是 15))后才能从 read 系统调用返回. 假设 ReadTimeout 设置了个 5min, 系统总线程数是 200, 那么只要 5min 内有 200 个请求落到宕机的 server 就会使 A 系统失去响应!
但如果将 tcp_retries2 设置为 5, 那么超时返回时间即为 min(ReadTimeOut 5min,25.6-51.2s), 也就是 30s 左右, 极大的缓解了这一情况.
echo 5> /proc/sys.NET/ipv4/tcp_retries2
但是针对这种现象, 最好要做资源上的隔离, 例如线程上的隔离或者机器级的隔离.
golang 的 goroutine 调度模型就可以很好的解决线程资源不够的问题, 但缺点是 goroutine 里面不能有阻塞的系统调用, 不然也会和上面一样, 但仅仅对于系统之间互相调用而言, 都是非阻塞 IO, 所以 golang 做微服务还是非常 Nice 的. 当然了我大 Java 用纯 IO 事件触发编写代码也不会有问题, 就是对心智负担太高 -_-!
物理机突然宕机和进程宕不一样
值得注意的是, 物理机宕机和进程宕但内核还存在表现完全不一样.
仅仅进程宕而内核存活, 那么内核会立马发送 reset 给对端, 从而不会卡住 A 系统的线程资源.
tcp_slow_start_after_idle
还有一个可能需要调整的参数是 tcp_slow_start_after_idle,Linux 默认是 1, 即开启状态. 开启这个参数后, 我们的 TCP 拥塞窗口会在一个 RTO 时间空闲之后重置为初始拥塞窗口 (CWND) 大小, 这无疑大幅的减少了长连接的优势. 对应 Linux 源码为:
- static void tcp_event_data_sent(struct tcp_sock *tp,
- struct sk_buff *skb, struct sock *sk){
- // 如果开启了 start_after_idle, 而且这次发送的时间 - 上次发送的时间>一个 rto, 就重置 tcp 拥塞窗口
- if (sysctl_tcp_slow_start_after_idle &&
- (!tp->packets_out && (s32)(now - tp->lsndtime)> icsk->icsk_rto))
- tcp_cwnd_restart(sk, __sk_dst_get(sk));
- }
关闭这个参数后, 无疑会提高某些请求的传输速度(在带宽够的情况下).
echo 0> /proc/sys.NET/ipv4/tcp_slow_start_after_idle
当然了, Linux 启用这个参数也是有理由的, 如果我们的网络情况是时刻在变化的, 例如拿个手机到处移动, 那么将拥塞窗口重置确实是个不错的选项. 但是就我们内网系统间调用而言, 是不太必要的了.
初始 CWND 大小
毫无疑问, 新建连接之后的初始 TCP 拥塞窗口大小也直接影响到我们的请求速率. 在 Linux2.6.32 源码中, 其初始拥塞窗口是(2-4 个)mss 大小, 对应于内网估计也就是(2.8-5.6K)(MTU 1500), 这个大小对于某些大请求可能有点捉襟见肘.
在 Linux 2.6.39 以上或者某些 RedHat 维护的小版本中已经把 CWND
增大到 RFC 6928 所规定的的 10 段, 也就是在内网里面估计 14K 左右(MTU 1500).
Linux 新版本
- /* TCP initial congestion Windows */
- #define TCP_INIT_CWND 10
公众号
关注笔者公众号, 获取更多干货文章
总结
Linux 提供了一大堆内参参数供我们进行调优, 其默认设置的参数在很多情况下并不是最佳实践, 所以我们需要潜心研究, 找到最适合当前环境的组合.
来源: https://www.cnblogs.com/alchemystar/p/13175276.html