Linux作为一个强大的操作系统,提供了一系列内核参数供我们进行调优。光TCP的调优参数就有50多个。在和线上问题斗智斗勇的过程中,笔者积累了一些在内网环境应该进行调优的参数。在此分享出来,希望对大家有所帮助。
调优清单
好了,在这里先列出调优清单。请记住,这里只是笔者在内网进行TCP内核参数调优的经验,仅供参考。同时,笔者还会在余下的博客里面详细解释了为什么要进行这些调优!
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之间的超时时间。
下图给出了,重传和超时情况的对应图:
当然了,不同内核版本的超时时间可能不一样,因为初始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下的表现为:
如果我们在应用层设置的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 window */
#define TCP_INIT_CWND 10
总结
Linux提供了一大堆内参参数供我们进行调优,其默认设置的参数在很多情况下并不是最佳实践,所以我们需要潜心研究,找到最适合当前环境的组合。
Linux多线程应用性能分析
如今CPU的核心数越来越多, 在2019年你可以轻易买到超过50个核心的x86服务器CPU,一个中端台式机拥有8个执行线程也没什么好奇怪的。问题是我们怎样找到工作负载来“喂饱”那些相对饥饿的核。
到目前为止,我的博客中的大多数文章都还是聚焦在单核应用的性能上,完全忽略了多线程应用这一方面。所以,我决定写一篇面向初学者的文章,来叙述一下如何快速分析多线程应用的性能。当然了,一篇文章中不可能包含所有的细节,这里我只想简单介绍一下对多线程应用的性能分析应该如何落地,并提供测试清单和可以使用的工具集。我的博客上肯定还会有更多相关内容,敬请期待。
本文是一个系列文章的一部分,全系列包括:
多线程应用程序的性能分析 (本文).
如何在多线程应用程序中找到开销过大的锁.
使用Data Address Profiling检测伪共享.
我的文章通常喜欢提供一些案例,本文也不例外。首先介绍一下我们将要使用的benchmark。
Benchmark测试
为了更好地说明问题,我觉得在选择benchmark时应该添加一些约束:1
应该显式地使用 pthread/std::thread库来实现并行(而不是使用OpenMP隐式多线程库)。
性能不能随着线程数量的增加而同等地提高。任务不会被均匀地拆分到每个线程上执行。
自由,开源。
我选择了 h264dec benchmark (可以在Starbench parallel benchmark suite中获取),它可以解码原生H.264视频并且使用pthread库管理线程。
可以像这样运行 h264dec benchmark:
$ ./h264dec -i park_joy_2160p.h264 -t
该benchmark有一个主线程(大部分情况处于空闲状态),一个用于读取输入的线程,可配置数量的工作线程(用于解码)以及一个用于写入输出的线程。
性能伸缩
在多线程应用程序中,首先需要评估的是,当我们添加核心/线程时,性能有着怎样的伸缩,这实际上是一个应用未来成功与否的重要指标。下图显示了 h264dec benchmark性能与线程数量之间的伸缩关系。我的CPU(Intel Core i5-8259U)是4核/8线程的,因此我将上限设为8线程。值得注意的是,大于 4个线程后,性能不再按线程增加的速度继续快速提高。
当你想要对工作负载的性能进行建模时,这就对你非常有用。例如,当你估算完成一个特定任务所需的硬件时,看看上面这张图片,我宁愿选择更少数量的核心,但每颗核心主频更高的配置方案。2
有时我们可能会被问到这样的问题:怎样的硬件配置刚好能够处理一个特定任务。当应用程序能够线性伸缩时(换句话说,每个线程处理独立的数据单元并且不需要同步),此时你可以大概估计一下:只需在单个线程中运行工作负载并测量其执行周期。然后,你可以选择特定核心数和频率的CPU来达到你的延迟/吞吐量的需求。但是,当应用程序的性能不能很好地伸缩时,事情就变得很麻烦。此时,你不能再依赖于IPC了(详见后文),因为一个线程可能表现得很忙,显得有很高的利用率,但实际上它仅仅是在锁上自旋。在Brendan Gregg 撰写的《性能之巅》中有更多关于容量规划的相关信息。
CPU 利用率
下一个重要指标是CPU利用率3,它可以告诉你在执行某个任务时平均有多少线程处于繁忙状态。注意,线程不一定是在做有用的工作,可能只是在自旋。下图可以看到 h264dec benchmark中线程数和CPU利用率之间的关系。
例如,当工作负载使用了5个线程时,平均只有4个线程处于繁忙状态。也就是说,通常有一个线程一直处于睡眠状态,这阻碍了性能的线性伸缩(性能没有随着线程数量增大而线性提高),表明线程之间存在一些同步。
同步开销
为了估量线程间通信的开销,我们可以通过统计时钟周期总数以及其期间执行的指令总数来计算:
# 1 个工作线程
$ perf stat ./h264dec -i park_joy_2160p.h264 -t 1 -o output.file -v
261,963,433,544 cycles # 3.766 GHz
485,932,999,948 instructions # 1.85 insn per cycle
66.236645032 seconds time elapsed
66.290561000 seconds user
3.324930000 seconds sys
# 4 个工作线程
$ perf stat ./h264dec -i park_joy_2160p.h264 -t 4 -o output.file -v
272,518,956,365 cycles # 3.479 GHz
523,079,251,733 instructions # 1.92 insn per cycle
23.643595782 seconds time elapsed
73.979057000 seconds user
4.402318000 seconds sys
# 8 个工作线程
$ perf stat ./h264dec -i park_joy_2160p.h264 -t 8 -o output.file -v
453,581,394,912 cycles # 3.410 GHz
661,715,307,682 instructions # 1.46 insn per cycle
22.700304401 seconds time elapsed
128.122821000 seconds user
4.883838000 seconds sys
这里有一些非常有趣的现象,可以看到8个线程时的计数结果可能与你的想象不同,先不要觉得奇怪,因为CPU时间是墙上时间的5倍以上,出现这种现象完全与CPU利用率有关。
再来查看8个线程运行结束时所执行的指令数量,8个工作线程执行的指令比单个线程多36%(661,715,307,682 / 485,932,999,948 ≈ 1.36),这些多出来的指令都可以视为开销,这可能是线程同步(自旋)引起的。
分析多线程应用
要想在源代码级别上进行分析,先看看在它上面运行profiler的记录:4
# 1 个工作线程
$ perf record -o perf1.data -- ./h264dec -i park_joy_2160p.h264 -t 1 -o output.file -v
[ perf record: Captured and wrote 10.790 MB perf1.data (282808 samples) ]
# 4 个工作线程
$ perf record -o perf4.data -- ./h264dec -i park_joy_2160p.h264 -t 4 -o output.file -v
[ perf record: Captured and wrote 12.592 MB perf4.data (330029 samples) ]
# 8 个工作线程
$ perf record -o perf8.data -- ./h264dec -i park_joy_2160p.h264 -t 8 -o output.file -v
[ perf record: Captured and wrote 21.239 MB perf8.data (556685 samples) ]
相关样本的数量与我们之前看到的user时间相关。也就是说,工作线程越多,profiler需要执行的中断就越多。如果我们仔细查看profiles,除了函数 ed_rec_thread以外,其他内容基本一致:
# 1 个工作线程
$ perf report -n -i perf1.data --stdio
# Overhead Samples Command Shared Object Symbol
# ........ ............ ....... ................ .....................................
0.18% 524 h264dec h264dec [.] ed_rec_thread
# 4 个工作线程
$ perf report -n -i perf4.data --stdio
# Overhead Samples Command Shared Object Symbol
# ........ ............ ....... ................ .....................................
3.62% 11417 h264dec h264dec [.] ed_rec_thread
# 8 个工作线程
$ perf report -n -i perf8.data --stdio
# Overhead Samples Command Shared Object Symbol
# ........ ............ ....... ................ .....................................
17.53% 95619 h264dec h264dec [.] ed_rec_thread
需要注意的问题是随着线程数量的增加, ed_rec_thread函数如何获取更多样本。这一问题很可能是阻碍我们通过添加更多线程来进一步扩展性能的瓶颈。
简单地比较和分析不同数量工作线程得到的profiles,也是我们在多线程应用中找到同步瓶颈所在的一个方法。
实际上, decode_slice_mb函数内联到 ed_rec_thread函数,而下面一行代码是 decode_slice_mb函数中的一段:
while (rle->mb_cnt >= rle->prev_line->mb_cnt -1);
这里的mb_cnt用关键字volatile修饰,所以自旋实际上发生在这里。
查看"每线程"
Linux的perf非常强大,perf可以收集到一个进程中主线程派生的每一个线程进行分析。如果你想查看单个线程执行的情况,可以使用perf的-s选项:
$ perf record -s ./h264dec -i park_joy_2160p.h264 -t 8 -o output.file -v
然后可以列出所有线程的ID以及每个线程收集的样本数量:
$ perf report -n -T
...
# PID TID cycles:ppp
6602 6607 52758679502
6602 6603 487183790
6602 6613 49670283608
6602 6608 51173619921
6602 6604 165362635
6602 6609 38662454026
6602 6610 31375722931
6602 6606 48270267494
6602 6611 53793234480
6602 6612 25640899076
6602 6605 14481176486
现在如果你只想分析程序某个特定线程所收集的样本,则可以使用perf的–tid选项:
$ perf report -T --tid 6607 -n
8.28% 25657 h264dec h264dec [.] decode_cabac_residual_nondc
7.12% 35880 h264dec h264dec [.] put_h264_qpel8_hv_lowpass
6.19% 31430 h264dec h264dec [.] put_h264_qpel8_v_lowpass
5.87% 28874 h264dec h264dec [.] h264_v_loop_filter_luma_c
2.82% 10105 h264dec h264dec [.] ff_h264_decode_mb_cabac
1.19% 4525 h264dec h264dec [.] get_cabac_noinline
如果你刚好有Intel Vtune Amplifier工具,可以用它的GUI对特定线程和热点进行及时的过滤和放大缩小。
当你已经确定了某个引起了问题的线程,并且不想再关注其他线程时,可以这样用perf分析一个线程:
perf record -t
使用strace工具
我经常使用的另外一个工具是 strace:
$ strace -tt -ff -T -o strace-dump -- ./h264dec -i park_joy_2160p.h264 -t 8 -o output.file
上述命令将为每个运行的线程都生成一个记录系统调用的转储文件,转储文件中每个系统调用都附带精确的时间戳和持续时间。我们可以进一步处理单个转储文件,并从中提取很多有趣的信息。例如:
# 线程等待mutex来解锁的总用时
$ grep futex strace-dump.3740 > futex.dump
$ sed -i 's/.* $ sed -i 's/>//g' futex.dump
$ paste -s -d+ futex.dump | bc
16.407458
# 线程从文件中读取输入的总用时
$ grep read strace-dump.3740 > read.dump
$ sed -i 's/.* $ sed -i 's/>//g' read.dump
$ paste -s -d+ read.dump | bc
2.179850
这个线程总运行时间为21秒,而几乎80%的时间都被阻塞。然而此线程为所有工作线程提供输入,所以很可能是其他事情限制了benchmark的性能伸缩。
优化多线程应用
在处理单线程应用程序时,优化程序的一部分通常会提升整体性能5。但是,对于多线程应用程序就不一定了。实际上,这很难预测, 这种情况coz profiler能帮助分析。但由于我第一次使用,可能哪里出了问题,我没有获取到有效的信息。
如果你的应用程序能够很好地伸缩,或者线程在工作时不参与输入工作,那么进行优化后该程序性能可以成比例提高。在这种情况下,更容易对运行在单个线程上的应用程序进行性能分析,对于单线程起作用的优化很有可能扩展得很好。6
自顶向下法可以很好的帮助我们发现微体系结构问题,并对Intel下一代CPU有更好的解释。
本文由西邮陈莉君教授研一学生梁金荣、戴君毅、马明慧等翻译,陈莉君、宋宝华老师指导和审核。译者梁金荣、戴君毅、马明慧等同学热爱开源,践行开放、自由和分享。
总结了很多有关于java面试的资料,希望能够帮助正在学习java的小伙伴。由于资料过多不便发表文章,创作不易,望小伙伴们能够给我一些动力继续创建更好的java类学习资料文章,
请多多支持和关注小作,别忘了点赞+评论+转发。右上角私信我回复【666】即可领取免费学习资料谢谢啦!