1 系统监控系统监控需要涵盖系统的整体资源使用情况，比如 CPU、内存、磁盘、文件系统和网络等各种系统资源。1.1 USE 法USE 法把系统资源的性能指标简化为了三个类别，使用率、饱和度和错误数。USE 方法的指标直接表明了系统的资源瓶颈。1.2 监控系统需要建立监控系统，将监控的性能指标存下来；然后，根据监控到的状态自动分析和定位大概的瓶颈来源；最后，再通过告警系统把问题及时汇报给相关的团队处理。一个完整的监控系统包括数据采集、数据存储、数据查询、数据处理、告警、可视化等多个模块。开源的监控工具包括 Zabbix、Nagios、Prometheus 等。Prometheus 架构图：Retrivel 负责采集数据，包括 Pull 模式和 Push 模式。TSDB 负责将采集到的数据持久化到 SSD 等磁盘中，还提供了数据查询和基本的数据处理功能，也就是 PromQL 语言。Alertmanager 提供了告警功能，包括基于 PromQL 语言的触发条件、告警规则的配置管理以及告警的发送等。还支持通过分组、抑制或静默等多种方式来聚合同类告警，并减少告警数量。web UI 提供了简单的可视化界面，用于执行 PromQL 查询语句，配合 Grafana 可以构建强大的图形界面。2 应用监控应用程序的核心指标，不再是资源的使用情况，而是请求数、错误率和响应时间。这些指标不仅直接关系到用户的使用体验，还反映应用整体的可用性和可靠性。另外，如下三个指标也很重要：进程的资源使用情况。应用程序之间的调用情况。应用程序内部核心逻辑执行情况，需要应用程序提供指标。为了迅速定位跨应用的性能瓶颈，可以使用 Zipkin、Jaeger、Pinpoint 等各类开源工具，来构建全链路跟踪系统。全链路跟踪除了可以快速定位跨应用的性能问题外，还可以生成线上系统的调用拓扑图。2.1 日志监控日志是指标上下文信息的来源。对日志监控来说，最经典的方法，就是使用 ELK 技术栈，即使用 Elasticsearch、Logstash 和 Kibana 这三个组件的组合。Logstash 负责从各个日志源采集日志，然后进行预处理。Elasticsearch 负责对日志索引，提供搜索引擎。Kibana 负责对日志进行可视化分析，包括日志搜索、处理、仪表板展示等。在资源紧张的环境中，往往使用资源消耗更低的 Fluentd 替代 Logstash。参考倪朋飞. Linux 性能优化实战.
使用动态追踪，可以在不修改代码、不重启服务的情况下，动态了解应用程序或者内核的行为，这对排查线上问题、特别是不容易重现的问题尤其有效。Linux 提供了一系列的动态追踪机制，比如 ftrace、perf、eBPF 等1 ftrace当想要了解某个内核函数的调用过程时， 可以使用 ftrace 跟踪到它的执行过程。ftrace 的使用接口与 procfs 类似，通过 debugfs 以普通文件的形式向用户空间提供访问接口。切到 debugfs 的挂载点：$ cd /sys/kernel/debug/tracing $ ls README instances set_ftrace_notrace trace_marker_raw available_events kprobe_events set_ftrace_pid trace_options ...查询所有支持的跟踪器：$ cat available_tracers hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nopfunction 表示跟踪函数执行。funcion_graph 表示跟踪函数的调用关系。查询支持的函数和事件：$ cat available_filter_functions $ cat available_events接下来以 ls 命令为例，看下 ftrace 的使用方法。一：ls 调用 open 系统调用打开目录文件，而 open 在内核中对应的函数名称为 do_sys_open。所以，需要将跟踪函数设为 open：$ echo do_sys_open > set_graph_function二：配置跟踪项，开启函数调用跟踪，并跟踪调用进程：$ echo function_graph > current_tracer $ echo funcgraph-proc > trace_options三：开启跟踪：$ echo 1 > tracing_on四：执行一个 ls 命令后，再关闭跟踪：$ ls $ echo 0 > tracing_on五：查看跟踪结果：$ cat trace # tracer: function_graph # # CPU TASK/PID DURATION FUNCTION CALLS # | | | | | | | | | 0) ls-12276 | | do_sys_open() { 0) ls-12276 | | getname() { 0) ls-12276 | | getname_flags() { 0) ls-12276 | | kmem_cache_alloc() { 0) ls-12276 | | _cond_resched() { 0) ls-12276 | 0.049 us | rcu_all_qs(); 0) ls-12276 | 0.791 us | } 0) ls-12276 | 0.041 us | should_failslab(); 0) ls-12276 | 0.040 us | prefetch_freepointer(); 0) ls-12276 | 0.039 us | memcg_kmem_put_cache(); 0) ls-12276 | 2.895 us | } 0) ls-12276 | | __check_object_size() { 0) ls-12276 | 0.067 us | __virt_addr_valid(); 0) ls-12276 | 0.044 us | __check_heap_object(); 0) ls-12276 | 0.039 us | check_stack_object(); 0) ls-12276 | 1.570 us | } 0) ls-12276 | 5.790 us | } 0) ls-12276 | 6.325 us | } ...第一列是运行的 CPU第二列是任务名称和进程 ID第三列是函数执行延迟最后一列是函数调用关系图也可以用 trace-cmd 代替 ftrace。安装 trace-cmd：# Ubuntu $ apt-get install trace-cmd # CentOS $ yum install trace-cmdftrace 的五步可简化为两步：$ trace-cmd record -p function_graph -g do_sys_open -O funcgraph-proc ls $ trace-cmd report ... ls-12418 [000] 85558.075341: funcgraph_entry: | do_sys_open() { ls-12418 [000] 85558.075363: funcgraph_entry: | getname() { ls-12418 [000] 85558.075364: funcgraph_entry: | getname_flags() { ls-12418 [000] 85558.075364: funcgraph_entry: | kmem_cache_alloc() { ls-12418 [000] 85558.075365: funcgraph_entry: | _cond_resched() { ls-12418 [000] 85558.075365: funcgraph_entry: 0.074 us | rcu_all_qs(); ls-12418 [000] 85558.075366: funcgraph_exit: 1.143 us | } ls-12418 [000] 85558.075366: funcgraph_entry: 0.064 us | should_failslab(); ls-12418 [000] 85558.075367: funcgraph_entry: 0.075 us | prefetch_freepointer(); ls-12418 [000] 85558.075368: funcgraph_entry: 0.085 us | memcg_kmem_put_cache(); ls-12418 [000] 85558.075369: funcgraph_exit: 4.447 us | } ls-12418 [000] 85558.075369: funcgraph_entry: | __check_object_size() { ls-12418 [000] 85558.075370: funcgraph_entry: 0.132 us | __virt_addr_valid(); ls-12418 [000] 85558.075370: funcgraph_entry: 0.093 us | __check_heap_object(); ls-12418 [000] 85558.075371: funcgraph_entry: 0.059 us | check_stack_object(); ls-12418 [000] 85558.075372: funcgraph_exit: 2.323 us | } ls-12418 [000] 85558.075372: funcgraph_exit: 8.411 us | } ls-12418 [000] 85558.075373: funcgraph_exit: 9.195 us | } ...2 perfperf 可以用来分析 CPU cache、CPU 迁移、分支预测、指令周期等各种硬件事件，也可以只对感兴趣的事件进行动态追踪。perf 既可以跟踪内核事件，也可以跟踪用户空间的函数。查看 perf 支持的所有事件：$ perf list2.1 跟踪内核函数 do_sys_open添加 do_sys_open 探针：$ perf probe --add do_sys_open Added new event: probe:do_sys_open (on do_sys_open) You can now use it in all perf tools, such as: perf record -e probe:do_sys_open -aR sleep 1对 10s 内 do_sys_open 采样：$ perf record -e probe:do_sys_open -aR sleep 10 [ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 0.148 MB perf.data (19 samples) ]查看采样结果：$ perf script perf 12886 [000] 89565.879875: probe:do_sys_open: (ffffffffa807b290) sleep 12889 [000] 89565.880362: probe:do_sys_open: (ffffffffa807b290) sleep 12889 [000] 89565.880382: probe:do_sys_open: (ffffffffa807b290) sleep 12889 [000] 89565.880635: probe:do_sys_open: (ffffffffa807b290) sleep 12889 [000] 89565.880669: probe:do_sys_open: (ffffffffa807b290)但是我们还希望追踪它打开了哪些文件，通过追踪它的参数即可得知。根据该文档 https://wiki.ubuntu.com/Debug%20Symbol%20Packages 安装调试信息后，查看 do_sys_open 的所有参数：$ perf probe -V do_sys_open Available variables at do_sys_open @<do_sys_open+0> char* filename int dfd int flags struct open_flags op umode_t mode重新添加探针：# 先删除旧的探针 perf probe --del probe:do_sys_open # 添加带参数的探针 $ perf probe --add 'do_sys_open filename:string' Added new event: probe:do_sys_open (on do_sys_open with filename:string) You can now use it in all perf tools, such as: perf record -e probe:do_sys_open -aR sleep 1重新采样并查看记录：# 重新采样记录 $ perf record -e probe:do_sys_open -aR ls # 查看结果 $ perf script perf 13593 [000] 91846.053622: probe:do_sys_open: (ffffffffa807b290) filename_string="/proc/13596/status" ls 13596 [000] 91846.053995: probe:do_sys_open: (ffffffffa807b290) filename_string="/etc/ld.so.cache" ls 13596 [000] 91846.054011: probe:do_sys_open: (ffffffffa807b290) filename_string="/lib/x86_64-linux-gnu/libselinux.so.1" ls 13596 [000] 91846.054066: probe:do_sys_open: (ffffffffa807b290) filename_string="/lib/x86_64-linux-gnu/libc.so.6” ... # 使用完成后不要忘记删除探针 $ perf probe --del probe:do_sys_open便可以看懂 open 打开的文件了。执行 strace 时也和看到以上打开的动态库：$ strace ls ... access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 ... access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3 ...但是 perf 比 strace 对进程的性能影响更小。perf 跟踪内核事件，而 strace 跟踪进程，需要在内核态和用户态切换，还需要借助 SIGSTOP 信号挂起目标进程。perf 也提供了 trace 子命令，与 strace 很像：$ perf trace ls ? ( ): ls/14234 ... [continued]: execve()) = 0 0.177 ( 0.013 ms): ls/14234 brk( ) = 0x555d96be7000 0.224 ( 0.014 ms): ls/14234 access(filename: 0xad98082 ) = -1 ENOENT No such file or directory 0.248 ( 0.009 ms): ls/14234 access(filename: 0xad9add0, mode: R ) = -1 ENOENT No such file or directory 0.267 ( 0.012 ms): ls/14234 openat(dfd: CWD, filename: 0xad98428, flags: CLOEXEC ) = 3 0.288 ( 0.009 ms): ls/14234 fstat(fd: 3</usr/lib/locale/C.UTF-8/LC_NAME>, statbuf: 0x7ffd2015f230 ) = 0 0.305 ( 0.011 ms): ls/14234 mmap(len: 45560, prot: READ, flags: PRIVATE, fd: 3 ) = 0x7efe0af92000 0.324 Dockerfile test.sh ( 0.008 ms): ls/14234 close(fd: 3</usr/lib/locale/C.UTF-8/LC_NAME> ) = 0 ...但 perf 可跟踪所有进程，而 strace 只能跟踪特定的进程。2.2 用户空间的库函数readline 是从终端读取用户输入，并将这些数据返回调用方。所以，相对于 open 系统调用，我们更关注 readline 调用的结果。通过 -x 指定 bash 二进制文件的路径，就可以动态跟踪库函数。这其实就是跟踪了所有用户在 bash 中执行的命令：# 为 /bin/bash 添加 readline 探针 $ perf probe -x /bin/bash 'readline%return +0($retval):string' # 采样记录 $ perf record -e probe_bash:readline__return -aR sleep 5 # 查看结果 $ perf script bash 13348 [000] 93939.142576: probe_bash:readline__return: (5626ffac1610 <- 5626ffa46739) arg1="ls" # 跟踪完成后删除探针 $ perf probe --del probe_bash:readline__return如果不确定探针格式，也可以通过下面的命令，查询所有支持的函数和函数参数：# 查询所有的函数 $ perf probe -x /bin/bash —funcs # 查询函数的参数 $ perf probe -x /bin/bash -V readline Available variables at readline @<readline+0> char* prompt3 eBPF 和 BCCftrace 和 perf 有一个共同的缺陷，那就是不够灵活，没法像 DTrace 那样通过脚本自由扩展。而 eBPF 就是 Linux 版的 DTrace，可以通过 C 语言自由扩展。BCC 把 eBPF 中的各种事件源（比如 kprobe、uprobe、tracepoint 等）和数据操作（称为 Maps），都转换成了 Python 接口（也支持 lua）。这样，使用 BCC 进行动态追踪时，编写简单的脚本就可以了。参考倪鹏飞. Linux 性能优化实战.
案例篇：服务吞吐量下降很厉害，怎么分析？
1 现象查看服务性能：# 默认测试时间为 10s，请求超时 2s $ wrk --latency -c 1000 http://192.168.0.30 Running 10s test @ http://192.168.0.30 2 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 14.82ms 42.47ms 874.96ms 98.43% Req/Sec 550.55 1.36k 5.70k 93.10% Latency Distribution 50% 11.03ms 75% 15.90ms 90% 23.65ms 99% 215.03ms 1910 requests in 10.10s, 573.56KB read Non-2xx or 3xx responses: 1910 Requests/sec: 189.10 Transfer/sec: 56.78KB吞吐量只有 189，并且所有 1910 个请求收到的都是异常响应（非 2xx 或 3xx）。吞吐量太低了，并且请求处理都失败了。根据 wrk 输出的统计结果，可以看到总共传输的数据量只有 573 KB，肯定不是带宽受限导致的。所以，应该从请求数的角度来分析。2 排查2.1 连接数优化分析请求数，特别是 HTTP 的请求数，可以从 TCP 连接数入手。将测试延长到 30min：$ wrk --latency -c 1000 -d 1800 http://192.168.0.30在另一个终端中观察 TCP 连接数：$ ss -s Total: 177 (kernel 1565) TCP: 1193 (estab 5, closed 1178, orphaned 0, synrecv 0, timewait 1178/0), ports 0 Transport Total IP IPv6 * 1565 - - RAW 1 0 1 UDP 2 2 0 TCP 15 12 3 INET 18 14 4 FRAG 0 0 0wrk 并发 1000 请求时，建立连接数只有 5，而 closed 和 timewait 状态的连接则有 1100 多。内核中的连接跟踪模块，有可能会导致 timewait 问题。服务运行在 Docker 内，而 Docker 使用的 iptables ，就会使用连接跟踪模块来管理 NAT。通过 dmesg 查看系统日志，如果有连接跟踪出了问题，应该会看到 nf_conntrack 相关的日志：$ dmesg | tail [88356.354329] nf_conntrack: nf_conntrack: table full, dropping packet [88356.354374] nf_conntrack: nf_conntrack: table full, dropping packet 查看当前连接跟踪数和最大限制：$ sysctl net.netfilter.nf_conntrack_max net.netfilter.nf_conntrack_max = 200 $ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_count = 200连接跟踪数最大限制只有 200，且全部被占了。将 nf_conntrack_max 增大：# 将连接跟踪限制增大到 1048576 $ sysctl -w net.netfilter.nf_conntrack_max=1048576重新测试 nginx 性能：# 默认测试时间为 10s，请求超时 2s $ wrk --latency -c 1000 http://192.168.0.30 ... 54221 requests in 10.07s, 15.16MB read Socket errors: connect 0, read 7, write 0, timeout 110 Non-2xx or 3xx responses: 45577 Requests/sec: 5382.21 Transfer/sec: 1.50MB吞吐量已经从刚才的 189 增大到了 5382。看起来性能提升了将近 30 倍，但是有 4 万多响应异常，即真正成功的只有 8000 多个。2.2 工作进程优化由于这些响应并非 Socket error，说明 Nginx 已经收到了请求，只不过，响应的状态码并不是我们期望的 2xx （表示成功）或 3xx（表示重定向）。所以，需要搞清楚 Nginx 真正的响应。查看 nginx 容器日志：$ docker logs nginx --tail 3 192.168.0.2 - - [15/Mar/2019:2243:27 +0000] "GET / HTTP/1.1" 499 0 "-" "-" "-" 192.168.0.2 - - [15/Mar/2019:22:43:27 +0000] "GET / HTTP/1.1" 499 0 "-" "-" "-" 192.168.0.2 - - [15/Mar/2019:22:43:27 +0000] "GET / HTTP/1.1" 499 0 "-" "-" "-"响应状态码为 499，并非标准的 HTTP 状态码，而是由 Nginx 扩展而来，表示服务器端还没来得及响应时，客户端就已经关闭连接了。即服务器端处理太慢，客户端因为超时（wrk 超时时间为 2s），主动断开了连接。服务是 nginx + php，会不会是 php 比较慢，查看 php 容器日志：$ docker logs phpfpm --tail 5 WARNING: [pool www] server reached max_children setting (5), consider raising it WARNING: [pool www] server reached max_children setting (5), consider raising it建议增大 max_children，max_children 表示 php-fpm 子进程的最大数量。一般来说，每个 php-fpm 子进程可能会占用 20 MB 左右的内存。所以，可以根据内存和 CPU 个数，估算一个合理的值，本文设置成了 20。切换终端，再次测试 nginx 性能：# 默认测试时间为 10s，请求超时 2s $ wrk --latency -c 1000 http://192.168.0.30 ... 47210 requests in 10.08s, 12.51MB read Socket errors: connect 0, read 4, write 0, timeout 91 Non-2xx or 3xx responses: 31692 Requests/sec: 4683.82 Transfer/sec: 1.24MB成功的请求数多了不少，从原来的 8000，增长到了 15000（47210-31692=15518）。但是 4000 多的吞吐量显然还是比较差。如何进一步提升吞吐量呢？2.3 套接字优化还是用 -d 参数延长测试时间，以便模拟性能瓶颈的现场：# 测试时间 30 分钟 $ wrk --latency -c 1000 -d 1800 http://192.168.0.30观察有没有丢包：# 只关注套接字统计 $ netstat -s | grep socket 73 resets received for embryonic SYN_RECV sockets 308582 TCP sockets finished time wait in fast timer 8 delayed acks further delayed because of locked socket 290566 times the listen queue of a socket overflowed 290566 SYNs to LISTEN sockets dropped # 稍等一会，再次运行 $ netstat -s | grep socket 73 resets received for embryonic SYN_RECV sockets 314722 TCP sockets finished time wait in fast timer 8 delayed acks further delayed because of locked socket 344440 times the listen queue of a socket overflowed 344440 SYNs to LISTEN sockets dropped根据两次统计结果中 socket overflowed 和 sockets dropped 的变化，可以看到，有大量的套接字丢包，并且丢包都是套接字队列溢出导致的。所以，接下来，我们应该分析连接队列的大小是不是有异常。查看套接字队列大小：$ ss -ltnp State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 10 10 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=10491,fd=6),("nginx",pid=10490,fd=6),("nginx",pid=10487,fd=6)) LISTEN 7 10 *:9000 *:* users:(("php-fpm",pid=11084,fd=9),...,("php-fpm",pid=10529,fd=7))Nginx 和 php-fpm 的监听队列 （Send-Q）只有 10，而 nginx 的当前监听队列长度 （Recv-Q）已经达到了最大值，php-fpm 也已经接近了最大值。套接字监听队列长度既可以在应用程序中，通过套接字接口调整，也支持通过内核选项来配置。分别查询 Nginx 和内核选项对监听队列长度的配置：# 查询 nginx 监听队列长度配置 $ docker exec nginx cat /etc/nginx/nginx.conf | grep backlog listen 80 backlog=10; # 查询 php-fpm 监听队列长度 $ docker exec phpfpm cat /opt/bitnami/php/etc/php-fpm.d/www.conf | grep backlog ; Set listen(2) backlog. ;listen.backlog = 511 # somaxconn 是系统级套接字监听队列上限 $ sysctl net.core.somaxconn net.core.somaxconn = 10需增大以上三个值，比如，可以把 Nginx 和 php-fpm 的队列长度增大到 8192，而把 somaxconn 增大到 65536。重新测试 nginx 性能：$ wrk --latency -c 1000 http://192.168.0.30 ... 62247 requests in 10.06s, 18.25MB read Non-2xx or 3xx responses: 62247 Requests/sec: 6185.65 Transfer/sec: 1.81MB吞吐量已经增大到了 6185，并且执行 netstat 时没有丢包问题了，但是请求全部失败了，查看 nginx 日志：$ docker logs nginx --tail 10 [crit] 15#15: *999779 connect() to 127.0.0.1:9000 failed (99: Cannot assign requested address) while connecting to upstream, client: 192.168.0.2, server: localhost, request: "GET / HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000", host: "192.168.0.30"Nginx 报出了无法连接 fastcgi 的错误，错误消息是 Connect 时， Cannot assign requested address。这个错误消息对应的错误代码为 EADDRNOTAVAIL，表示 IP 地址或者端口号不可用。接下来分析端口号的问题。2.4 端口号优化当客户端连接服务器端时，需要分配一个临时端口号。查询系统配置的临时端口号范围：$ sysctl net.ipv4.ip_local_port_range net.ipv4.ip_local_port_range=20000 20050临时端口的范围只有 50 个，增大端口号范围：$ sysctl -w net.ipv4.ip_local_port_range="10000 65535"再次测试性能：$ wrk --latency -c 1000 http://192.168.0.30/ ... 32308 requests in 10.07s, 6.71MB read Socket errors: connect 0, read 2027, write 0, timeout 433 Non-2xx or 3xx responses: 30 Requests/sec: 3208.58 Transfer/sec: 682.15KB异常的响应少了 ，但吞吐量下降到了 3208。并且，这次还出现了很多 Socket read errors。2.5 火焰图是不是其他系统资源遇到了瓶颈？启动长时间测试：# 测试时间 30 分钟 $ wrk --latency -c 1000 -d 1800 http://192.168.0.30观察 CPU 和内存使用：$ top ... %Cpu0 : 30.7 us, 48.7 sy, 0.0 ni, 2.3 id, 0.0 wa, 0.0 hi, 18.3 si, 0.0 st %Cpu1 : 28.2 us, 46.5 sy, 0.0 ni, 2.0 id, 0.0 wa, 0.0 hi, 23.3 si, 0.0 st KiB Mem : 8167020 total, 5867788 free, 490400 used, 1808832 buff/cache KiB Swap: 0 total, 0 free, 0 used. 7361172 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 20379 systemd+ 20 0 38068 8692 2392 R 36.1 0.1 0:28.86 nginx 20381 systemd+ 20 0 38024 8700 2392 S 33.8 0.1 0:29.29 nginx 1558 root 20 0 1118172 85868 39044 S 32.8 1.1 22:55.79 dockerd 20313 root 20 0 11024 5968 3956 S 27.2 0.1 0:22.78 docker-containe 13730 root 20 0 0 0 0 I 4.0 0.0 0:10.07 kworker/u4:0-ev内存还是很充足的，系统 CPU 使用率（sy）比较高，两个 CPU 的系统 CPU 使用率都接近 50%，且空闲 CPU 使用率只有 2%。再看进程部分，CPU 主要被两个 Nginx 进程和两个 docker 相关的进程占用，使用率都是 30% 左右。可以使用 perf 寻找系统中的热点函数：# 执行perf记录事件 $ perf record -g # 切换到 FlameGraph 安装路径执行下面的命令生成火焰图 $ perf script -i ~/perf.data | ./stackcollapse-perf.pl --all | ./flamegraph.pl > nginx.svg用浏览器打开生成的 nginx.svg：__init_check_established 函数是检查端口号是否可用。如果有大量连接占用着端口，那么检查端口号可用的函数，不就会消耗更多的 CPU 吗？查看连接状态统计：$ ss -s TCP: 32775 (estab 1, closed 32768, orphaned 0, synrecv 0, timewait 32768/0), ports 0 ...有大量连接（32768）处于 timewait 状态，而 timewait 状态的连接会继续占用端口号。如果这些端口号可以重用，就可以缩短 __init_check_established 的过程。查看配置：$ sysctl net.ipv4.tcp_tw_reuse net.ipv4.tcp_tw_reuse = 0开启端口复用：$ sysctl net.ipv4.tcp_tw_reuse=1重新测试 nginx 性能：$ wrk --latency -c 1000 http://192.168.0.30/ ... 52119 requests in 10.06s, 10.81MB read Socket errors: connect 0, read 850, write 0, timeout 0 Requests/sec: 5180.48 Transfer/sec: 1.07MB吞吐量已经达到了 5000 多，并且只有少量的 Socket errors，也不再有 Non-2xx or 3xx 的响应了。参考倪鹏飞. Linux 性能优化实战.
案例篇：内核线程 CPU 利用率太高，我该怎么办？
在排查网络问题时，经常会碰到的一个问题是，内核的 CPU 利用率较高。比如高并发场景下，内核线程 ksoftrqd 的 CPU 使用率就会比较高，是网络收发的软中断导致的。接下来看下如何分析内核线程的性能问题。1 内核线程Linux 启动中有 3 个特殊的进程：0 号进程为 idle 进程，在初始化完 1 号和 2 号进程后演变为空闲任务。1 号进程为 init 进程，通常是 systemd 进程，在用户态运行，管理其他的用户态进程。2 号进程 kthreadd 进程，在内核态运行，管理内核线程。所以要查找内核线程，从 2 和进程开始，找它的子孙即可：$ ps -f --ppid 2 -p 2 UID PID PPID C STIME TTY TIME CMD root 2 0 0 12:02 ? 00:00:01 [kthreadd] root 9 2 0 12:02 ? 00:00:21 [ksoftirqd/0] root 10 2 0 12:02 ? 00:11:47 [rcu_sched] root 11 2 0 12:02 ? 00:00:18 [migration/0] ... root 11094 2 0 14:20 ? 00:00:00 [kworker/1:0-eve] root 11647 2 0 14:27 ? 00:00:00 [kworker/0:2-cgr]容易发送性能问题的内核线程：kswapd0：用于内存回收。kworker：执行内核工作队列，分为绑定 CPU 和未绑定 CPU 两类。migration：负载均衡过程中，将进程迁移到 CPU 上。每个 CPU 都有一个 migration 内核线程。jbd2/sda1-8：journaling blocking device，为文件系统提供日志功能。sda1-8 表示磁盘分区和设备号。每个使用 ext4 文件系统的磁盘分区都有一个 jbd2 内核线程。pdflush：将内存中的脏页写入磁盘。已在 3.10 中和入到 kworker 中。2 现象运行 hping3 命令，模拟客户端请求：# -S 参数表示设置 TCP 协议的 SYN（同步序列号），-p 表示目的端口为 80 # -i u10 表示每隔 10 微秒发送一个网络帧 # 注：如果你在实践过程中现象不明显，可以尝试把 10 调小，比如调成 5 甚至 1 $ hping3 -S -p 80 -i u10 192.168.0.30观察系统和进程 CPU 使用情况：$ top top - 08:31:43 up 17 min, 1 user, load average: 0.00, 0.00, 0.02 Tasks: 128 total, 1 running, 69 sleeping, 0 stopped, 0 zombie %Cpu0 : 0.3 us, 0.3 sy, 0.0 ni, 66.8 id, 0.3 wa, 0.0 hi, 32.4 si, 0.0 st %Cpu1 : 0.0 us, 0.3 sy, 0.0 ni, 65.2 id, 0.0 wa, 0.0 hi, 34.5 si, 0.0 st KiB Mem : 8167040 total, 7234236 free, 358976 used, 573828 buff/cache KiB Swap: 0 total, 0 free, 0 used. 7560460 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 9 root 20 0 0 0 0 S 7.0 0.0 0:00.48 ksoftirqd/0 18 root 20 0 0 0 0 S 6.9 0.0 0:00.56 ksoftirqd/1 2489 root 20 0 876896 38408 21520 S 0.3 0.5 0:01.50 docker-containe 3008 root 20 0 44536 3936 3304 R 0.3 0.0 0:00.09 top 1 root 20 0 78116 9000 6432 S 0.0 0.1 0:11.77 systemd ...两个 CPU 的软中断都超过了 30%，而 CPU 使用率最高的正好是两个软中断内核线程 ksoftrqd/0、ksoftrqd/1。我们知道大概是因为大量的网络收发导致的，但是它到底在执行什么逻辑，我们却不知道，而 strace、pstack、lsof 等工具不适合内核线程。3 排查3.1 perf 分析可以用 perf 分析一下进程号为 9 的 ksoftrqd：# -a 表示追踪所有的 CPU # -g 表示额外记录函数调用关系 # -- sleep 30 表示采样 30s 后退出 $ perf record -a -g -p 9 -- sleep 30继续执行 perf report，按上下方向键以及回车键，展开比例最高的 ksoftirqd，就可以得到如调用关系：net_rx_action 和 netif_receive_skb，表明这是接收网络包。br_handle_frame，表明网络包经过了网桥。br_nf_pre_routing，表明在网桥上执行了 netfilter 的 PREROUTING，可能有 DNAT 发生。br_pass_frame_up，表明网桥处理后，再交给其桥接的其他桥接网卡进一步处理。这时因为服务跑在 docker 容器中，而 docker 容器会自动为容器创建虚拟网卡、桥接到 docker 的网桥并配置 NAT 规则。3.2 火焰图分析可以根据 perf record 生成的 perf.data 来生成火焰图。下载火焰图工具：$ git clone https://github.com/brendangregg/FlameGraph $ cd FlameGraph生成火焰图：$ perf script -i perf.data | ./stackcollapse-perf.pl --all | ./flamegraph.pl > ksoftirqd.svg使用浏览器打开 ksoftrqd.svg：火焰图介绍：横轴表示采样数和采样比例。一个函数占用的横轴越宽，就代表它的执行时间越长。同一层的多个函数，则是按照字母来排序。纵轴表示调用栈，由下往上根据调用关系逐个展开。换句话说，上下相邻的两个函数中，下面的函数，是上面函数的父函数。这样，调用栈越深，纵轴就越高。火焰图不包含任何时间因素，所以无法看出各函数的执行次序。火焰图分析：最开始，还是 net_rx_action 到 netif_receive_skb 处理网络收包；然后， br_handle_frame 到 br_nf_pre_routing ，在网桥中接收并执行 netfilter 钩子函数；再向上， br_pass_frame_up 到 netif_receive_skb ，从网桥转到其他网络设备又一次接收。如果要进一步查看 TCP 相关的调用，可以点击上图左上角的“Reset Zoom”，回到完整火焰图中，再去查看其他小火焰的堆栈。到这里，我们就找出了内核线程 ksoftirqd 执行最频繁的函数调用堆栈，而这个堆栈中的各层级函数，就是潜在的性能瓶颈来源。这样，后面想要进一步分析、优化时，也就有了根据。参考倪鹏飞. Linux 性能优化实战.
案例篇：为什么应用容器化后，启动慢了很多？
1 现象一启动容器：# -m 表示设置内存为512MB $ docker run --name tomcat --cpus 0.1 -m 512M -p 8080:8080 -itd feisky/tomcat:8确认容器启动：$ curl localhost:8080 curl: (56) Recv failure: Connection reset by peer2 排查查看容器日志：$ docker logs -f tomcat Using CATALINA_BASE: /usr/local/tomcat Using CATALINA_HOME: /usr/local/tomcat Using CATALINA_TMPDIR: /usr/local/tomcat/temp Using JRE_HOME: /docker-java-home/jre Using CLASSPATH: /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar只打印了环境变量，说明还在启动中，所以没有响应。多次尝试访问 tomcat：$ for ((i=0;i<30;i++)); do curl localhost:8080; sleep 1; done curl: (56) Recv failure: Connection reset by peer curl: (56) Recv failure: Connection reset by peer # 这儿会阻塞一会 Hello, wolrd! curl: (52) Empty reply from server curl: (7) Failed to connect to localhost port 8080: Connection refused curl: (7) Failed to connect to localhost port 8080: Connection refused接续查看容器日志：18-Feb-2019 12:43:32.719 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/docs] 18-Feb-2019 12:43:33.725 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/docs] has finished in [1,006] ms 18-Feb-2019 12:43:33.726 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/manager] 18-Feb-2019 12:43:34.521 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/manager] has finished in [795] ms 18-Feb-2019 12:43:34.722 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"] 18-Feb-2019 12:43:35.319 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-nio-8009"] 18-Feb-2019 12:43:35.821 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 24096 ms root@ubuntu:~#最后一行已经回到了终端，说明容器已经退出，或者 dockerd 进程退出了。查看容器状态：$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0f2b3fcdd257 feisky/tomcat:8 "catalina.sh run" 2 minutes ago Exited (137) About a minute ago tomcat状态是 Exited，说明是容器退出了。查看容器状态：# 显示容器状态，jq用来格式化json输出 $ docker inspect tomcat -f '{{json .State}}' | jq { "Status": "exited", "Running": false, "Paused": false, "Restarting": false, "OOMKilled": true, "Dead": false, "Pid": 0, "ExitCode": 137, "Error": "", ... }表面容器已被 OOM 杀死。查看系统日志：$ dmesg [193038.106393] java invoked oom-killer: gfp_mask=0x14000c0(GFP_KERNEL), nodemask=(null), order=0, oom_score_adj=0 [193038.106396] java cpuset=0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53 mems_allowed=0 [193038.106402] CPU: 0 PID: 27424 Comm: java Tainted: G OE 4.15.0-1037 #39-Ubuntu [193038.106404] Hardware name: Microsoft Corporation Virtual Machine/Virtual Machine, BIOS 090007 06/02/2017 [193038.106405] Call Trace: [193038.106414] dump_stack+0x63/0x89 [193038.106419] dump_header+0x71/0x285 [193038.106422] oom_kill_process+0x220/0x440 [193038.106424] out_of_memory+0x2d1/0x4f0 [193038.106429] mem_cgroup_out_of_memory+0x4b/0x80 [193038.106432] mem_cgroup_oom_synchronize+0x2e8/0x320 [193038.106435] ? mem_cgroup_css_online+0x40/0x40 [193038.106437] pagefault_out_of_memory+0x36/0x7b [193038.106443] mm_fault_error+0x90/0x180 [193038.106445] __do_page_fault+0x4a5/0x4d0 [193038.106448] do_page_fault+0x2e/0xe0 [193038.106454] ? page_fault+0x2f/0x50 [193038.106456] page_fault+0x45/0x50 [193038.106459] RIP: 0033:0x7fa053e5a20d [193038.106460] RSP: 002b:00007fa0060159e8 EFLAGS: 00010206 [193038.106462] RAX: 0000000000000000 RBX: 00007fa04c4b3000 RCX: 0000000009187440 [193038.106463] RDX: 00000000943aa440 RSI: 0000000000000000 RDI: 000000009b223000 [193038.106464] RBP: 00007fa006015a60 R08: 0000000002000002 R09: 00007fa053d0a8a1 [193038.106465] R10: 00007fa04c018b80 R11: 0000000000000206 R12: 0000000100000768 [193038.106466] R13: 00007fa04c4b3000 R14: 0000000100000768 R15: 0000000010000000 [193038.106468] Task in /docker/0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53 killed as a result of limit of /docker/0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53 [193038.106478] memory: usage 524288kB, limit 524288kB, failcnt 77 [193038.106480] memory+swap: usage 0kB, limit 9007199254740988kB, failcnt 0 [193038.106481] kmem: usage 3708kB, limit 9007199254740988kB, failcnt 0 [193038.106481] Memory cgroup stats for /docker/0f2b3fcdd2578165ea77266cdc7b1ad43e75877b0ac1889ecda30a78cb78bd53: cache:0KB rss:520580KB rss_huge:450560KB shmem:0KB mapped_file:0KB dirty:0KB writeback:0KB inactive_anon:0KB active_anon:520580KB inactive_file:0KB active_file:0KB unevictable:0KB [193038.106494] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name [193038.106571] [27281] 0 27281 1153302 134371 1466368 0 0 java [193038.106574] Memory cgroup out of memory: Kill process 27281 (java) score 1027 or sacrifice child [193038.148334] Killed process 27281 (java) total-vm:4613208kB, anon-rss:517316kB, file-rss:20168kB, shmem-rss:0kB [193039.607503] oom_reaper: reaped process 27281 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB第 34 行：被杀死的是一个 java 进程，因 OOM 被杀死。第 27、28 行：java 进程是在容器内运行的，而容器的内存和使用都是 512M（524288kB）。第 35 行：被杀死的进程虚拟内存为 4.3G（total-vm:4613208kB），匿名内存为 505M（anon-rss:517316kB），页内存为 19M（20168kB）。匿名内存和页内存加起来为 524M，超过了 512M 的限制。tomcat 的内存占用主要是匿名内存，而匿名内存就是主动分配的堆内存。为什么会申请这么多的堆内存呢？可能是 JVM 堆内存配置的问题。JVM 堆内存的默认限制是物理内存的四分之一。重启 tomcat 并查看堆内存：# 重新启动容器 $ docker rm -f tomcat $ docker run --name tomcat --cpus 0.1 -m 512M -p 8080:8080 -itd feisky/tomcat:8 # 查看堆内存，注意单位是字节 $ docker exec tomcat java -XX:+PrintFlagsFinal -version | grep HeapSize uintx ErgoHeapSizeLimit = 0 {product} uintx HeapSizePerGCThread = 87241520 {product} uintx InitialHeapSize := 132120576 {product} uintx LargePageHeapSizeThreshold = 134217728 {product} uintx MaxHeapSize := 2092957696 {product}初始堆内存为（InitialHeapSize）是 126MB，最大堆内存则是 1.95GB，这可比容器限制的 512 MB 大多了。通过环境变量 JAVA_OPTS=’-Xmx512m -Xms512m’ ，把 JVM 的初始内存和最大内存都设为 512MB：# 删除问题容器 $ docker rm -f tomcat # 运行新的容器 $ docker run --name tomcat --cpus 0.1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8查看 tomcat 响应：$ for ((i=0;i<30;i++)); do curl localhost:8080; sleep 1; done curl: (56) Recv failure: Connection reset by peer curl: (56) Recv failure: Connection reset by peer Hello, wolrd! Hello, wolrd! Hello, wolrd!过了一会后，正常输出，说明 tomcat 已正常启动。3 现象二查看 tomcat 容器日志：$ docker logs -f tomcat ... 18-Feb-2019 12:52:00.823 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/usr/local/tomcat/webapps/manager] 18-Feb-2019 12:52:01.422 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/usr/local/tomcat/webapps/manager] has finished in [598] ms 18-Feb-2019 12:52:01.920 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"] 18-Feb-2019 12:52:02.323 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-nio-8009"] 18-Feb-2019 12:52:02.523 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 22798 ms最后一行显示启动耗时 22s，太慢了。重启容器后查看 top 输出：$ top top - 12:57:18 up 2 days, 5:50, 2 users, load average: 0.00, 0.02, 0.00 Tasks: 131 total, 1 running, 74 sleeping, 0 stopped, 0 zombie %Cpu0 : 3.0 us, 0.3 sy, 0.0 ni, 96.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st %Cpu1 : 5.7 us, 0.3 sy, 0.0 ni, 94.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 8169304 total, 2465984 free, 500812 used, 5202508 buff/cache KiB Swap: 0 total, 0 free, 0 used. 7353652 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 29457 root 20 0 2791736 73704 19164 S 10.0 0.9 0:01.61 java 27349 root 20 0 1121372 96760 39340 S 0.3 1.2 4:20.82 dockerd 27376 root 20 0 1031760 43768 21680 S 0.3 0.5 2:44.47 docker-containe 29430 root 20 0 7376 3604 3128 S 0.3 0.0 0:00.01 docker-containe 1 root 20 0 78132 9332 6744 S 0.0 0.1 0:16.12 systemd内存和 CPU 利用率都不高，没啥问题。不过，既然 java 进程的 CPU 利用率最高，接下来重点看它，使用 pidstat 命令：# 删除旧容器 $ docker rm -f tomcat # 运行新容器 $ docker run --name tomcat --cpus 0.1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8 # 查询新容器中进程的Pid $ PID=$(docker inspect tomcat -f '{{.State.Pid}}') # 执行 pidstat $ pidstat -t -p $PID 1 12:59:28 UID TGID TID %usr %system %guest %wait %CPU CPU Command 12:59:29 0 29850 - 10.00 0.00 0.00 0.00 10.00 0 java 12:59:29 0 - 29850 0.00 0.00 0.00 0.00 0.00 0 |__java 12:59:29 0 - 29897 5.00 1.00 0.00 86.00 6.00 1 |__java ... 12:59:29 0 - 29905 3.00 0.00 0.00 97.00 3.00 0 |__java 12:59:29 0 - 29906 2.00 0.00 0.00 49.00 2.00 1 |__java 12:59:29 0 - 29908 0.00 0.00 0.00 45.00 0.00 0 |__java虽然 CPU 使用率（%CPU）很低，但等待运行的使用率（%wait）却非常高，最高甚至已经达到了 97%。这说明，这些线程大部分时间都在等待调度，而不是真正的运行。是因为容器启动时设置了 --cpus 0.1 ，只有 0.1 个 CPU，也就是 10% 的 CPU。将 CPU 限制增到 1，然后重启容器，并查看日志：# 删除旧容器 $ docker rm -f tomcat # 运行新容器 $ docker run --name tomcat --cpus 1 -m 512M -e JAVA_OPTS='-Xmx512m -Xms512m' -p 8080:8080 -itd feisky/tomcat:8 # 查看容器日志 $ docker logs -f tomcat ... 18-Feb-2019 12:54:02.139 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 2001 ms现在可以看到，Tomcat 的启动过程，只需要 2 秒就完成了。参考倪朋飞. Linux 性能优化实战.
服务器总是时不时丢包，我该怎么办？
1 现象# -c 表示发送 10 个请求，-S 表示使用 TCP SYN，-p 指定端口为 80 $ hping3 -c 10 -S -p 80 192.168.0.30 HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=3 win=5120 rtt=7.5 ms len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=4 win=5120 rtt=7.4 ms len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=5120 rtt=3.3 ms len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=7 win=5120 rtt=3.0 ms len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=6 win=5120 rtt=3027.2 ms --- 192.168.0.30 hping statistic --- 10 packets transmitted, 5 packets received, 50% packet loss round-trip min/avg/max = 3.0/609.7/3027.2 ms发送了 10 个请求包，却只收到了 5 个回复，50% 的包都丢了。2 排查可能丢包的原因：两台机器连接之间，可能会发生传输失败的错误，比如网络拥塞、线路错误等；网卡收包后，环形缓冲区可能会因为溢出而丢包；链路层，可能会因为网络帧校验失败、QoS 等而丢包；IP 层，可能会因为路由失败、组包大小超过 MTU 等而丢包；传输层，可能会因为端口未监听、资源占用超过内核限制等而丢包；套接字层，可能会因为套接字缓冲区溢出而丢包；应用层，可能会因为应用程序异常而丢包；如果配置了 iptables 规则，可能因为 iptables 过滤规则而丢包。2.1 链路层当缓冲区溢出等原因导致网卡丢包时，Linux 会在网卡收发数据的统计信息中，记录下收发错误的次数。查看网卡的丢包记录：root@nginx:/# netstat -i Kernel Interface table Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg eth0 100 31 0 0 0 8 0 0 0 BMRU lo 65536 0 0 0 0 0 0 0 0 LRU没有发现任何错误，说明容器的虚拟网卡没有丢包。不过要注意，如果用 tc 等工具配置了 QoS，那么 tc 规则导致的丢包，就不会包含在网卡的统计信息中。检查一下 eth0 上是否配置了 tc 规则，是否丢包：root@nginx:/# tc -s qdisc show dev eth0 qdisc netem 800d: root refcnt 2 limit 1000 loss 30% Sent 432 bytes 8 pkt (dropped 4, overlimits 0 requeues 0) backlog 0b 0p requeues 0eth0 上面配置了一个网络模拟排队规则（qdisc netem），并且配置了丢包率为 30%（loss 30%）。再看后面的统计信息，发送了 8 个包，但是丢了 4 个。应该是 netem 模块丢弃了 Nginx 的响应包。删除 tc 中的 netem 模块：root@nginx:/# tc qdisc del dev eth0 root netem loss 30%重新执行 hping3 命令后，发现问题没有解决。2.2 网络层和传输层查看协议收发汇总：root@nginx:/# netstat -s Ip: Forwarding: 1 //开启转发 31 total packets received //总收包数 0 forwarded //转发包数 0 incoming packets discarded //接收丢包数 25 incoming packets delivered //接收的数据包数 15 requests sent out //发出的数据包数 Icmp: 0 ICMP messages received //收到的ICMP包数 0 input ICMP message failed //收到ICMP失败数 ICMP input histogram: 0 ICMP messages sent //ICMP发送数 0 ICMP messages failed //ICMP失败数 ICMP output histogram: Tcp: 0 active connection openings //主动连接数 0 passive connection openings //被动连接数 11 failed connection attempts //失败连接尝试数 0 connection resets received //接收的连接重置数 0 connections established //建立连接数 25 segments received //已接收报文数 21 segments sent out //已发送报文数 4 segments retransmitted //重传报文数 0 bad segments received //错误报文数 0 resets sent //发出的连接重置数 Udp: 0 packets received ... TcpExt: 11 resets received for embryonic SYN_RECV sockets //半连接重置数 0 packet headers predicted TCPTimeouts: 7 //超时数 TCPSynRetrans: 4 //SYN重传数 ...只有 TCP 协议发生了丢包和重传，且主要错误是半连接重置。换句话说，主要的失败，都是三次握手失败。但是失败的根源还未找到。2.3 iptablesiptables 和内核的连接跟踪机制也可能会导致丢包。要确认是不是连接跟踪导致的问题，其实只需要对比当前的连接跟踪数和最大连接跟踪数即可。连接跟踪在 Linux 内核中是全局的（不属于网络命名空间）。查看连接跟踪数：$ sysctl net.netfilter.nf_conntrack_max net.netfilter.nf_conntrack_max = 262144 $ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_count = 182连接数小于最大连接数，所以不是连接跟踪导致的。执行 iptables 命令，查看 filter 表的统计数据：root@nginx:/# iptables -t filter -nvL Chain INPUT (policy ACCEPT 25 packets, 1000 bytes) pkts bytes target prot opt in out source destination 6 240 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.29999999981 Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 15 packets, 660 bytes) pkts bytes target prot opt in out source destination 6 264 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.29999999981两条 DROP 规则的统计数值不是 0，它们分别在 INPUT 和 OUTPUT 链中。这两条规则实际上是一样的，指的是使用 statistic 模块，进行随机 30% 的丢包。删除这两条 DROP 规则：root@nginx:/# iptables -t filter -D INPUT -m statistic --mode random --probability 0.30 -j DROP root@nginx:/# iptables -t filter -D OUTPUT -m statistic --mode random --probability 0.30 -j DROP重新执行 hping3，发现丢包问题解决了。检查 Nginx 对 HTTP 请求的响应：$ curl --max-time 3 http://192.168.0.30 curl: (28) Operation timed out after 3000 milliseconds with 0 bytes received发现连接超时了。继续执行 hping3 命令，发现还是正常的，这时候需要抓包了。2.4 tcpdump在终端一中执行 tcpdump 命令：root@nginx:/# tcpdump -i eth0 -nn port 80 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes在终端二再次执行前面的 curl 命令。然后在终端一会看到如下输出：14:40:00.589235 IP 10.255.255.5.39058 > 172.17.0.2.80: Flags [S], seq 332257715, win 29200, options [mss 1418,sackOK,TS val 486800541 ecr 0,nop,wscale 7], length 0 14:40:00.589277 IP 172.17.0.2.80 > 10.255.255.5.39058: Flags [S.], seq 1630206251, ack 332257716, win 4880, options [mss 256,sackOK,TS val 2509376001 ecr 486800541,nop,wscale 7], length 0 14:40:00.589894 IP 10.255.255.5.39058 > 172.17.0.2.80: Flags [.], ack 1, win 229, options [nop,nop,TS val 486800541 ecr 2509376001], length 0 14:40:03.589352 IP 10.255.255.5.39058 > 172.17.0.2.80: Flags [F.], seq 76, ack 1, win 229, options [nop,nop,TS val 486803541 ecr 2509376001], length 0 14:40:03.589417 IP 172.17.0.2.80 > 10.255.255.5.39058: Flags [.], ack 1, win 40, options [nop,nop,TS val 2509379001 ecr 486800541,nop,nop,sack 1 {76:77}], length 0前三个包是正常的三次握手，第四个包确在 3s 后才到，还是客户端发来的 FIN 包，说明是客户端的连接关闭了。我们并没有抓到 curl 发来的 HTTP Get 请求，究竟是网卡丢包了，还是客户端根本没有发来呢？查看网卡有没有丢包：root@nginx:/# netstat -i Kernel Interface table Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg eth0 100 157 0 344 0 94 0 0 0 BMRU lo 65536 0 0 0 0 0 0 0 0 LRU接收包丢包数是 344，看来是在网卡接收时丢包了。但为什么 hping3 时不丢包，但是 GET 会丢包呢？对比下 hping3 和 curl 两个工具：hping3 只发送了 SYN 包。curl 在发送了 SYN 包后，还会发送 HTTP GET 请求包。HTTP GET 请求本质上也是 TCP 包，但是还携带了 HTTP GET 数据。可能是 MTU 配置错误导致的。继续查看网卡的输出，eth0 的 MTU 只有 100，太小了，修改成 1500：root@nginx:/# ifconfig eth0 mtu 1500再次执行 curl 命令，发现收到了正常的响应。参考倪鹏飞. Linux 性能优化实战.
1 确定优化目标具体到不同的应用中，每个指标的优化标准可能不同。对于 NAT 网关，直接影响到整个数据中心的网络出入性能，需要达到或接近线性转发，PPS 是主要的性能指标。对于数据库、缓存等，需要快速完成网络收发，低延迟是主要的性能指标。对于 Web 服务，需同时兼顾吞吐量和延迟。可按照协议栈的每一层进行基准测试，底层性能决定高层性能。网络接口层和网络层，每秒可处理的网络包数 PPS 是重要的性能指标，可用内核自带的工具 pktgen 测试。传输层，吞吐量（BPS）、连接数和延迟是重要的性能指标，可用 perf 或 iperf 测试，不过要注意测试不同网络包下的性能。应用层，关注的是吞吐量（BPS）、延迟和每秒请求数等指标，可用 wrk、ab 等测试。2 网络性能工具2.1 根据指标查找工具2.1.1 吞吐量（BPS）sar 可查看网络接口的吞吐量。rxkB/s 和 txkB/s 分别是接收和发送的吞吐量，单位是 KB/ 秒。# 数字1表示每隔1秒输出一组数据 $ sar -n DEV 1 Linux 4.15.0-1035-azure (ubuntu) 01/06/19 _x86_64_ (2 CPU) 13:21:40 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil 13:21:41 eth0 18.00 20.00 5.79 4.25 0.00 0.00 0.00 0.00 13:21:41 docker0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 13:21:41 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.002.1.2 PPSsar 可查看网络接口的 PPS。rxpck/s 和 txpck/s 分别是接收和发送的 PPS，单位为包 / 秒。2.1.3 连接数# head -n 3 表示只显示前面3行 # -l 表示只显示监听套接字 # -n 表示显示数字地址和端口(而不是名字) # -p 表示显示进程信息 $ netstat -nlp | head -n 3 Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 840/systemd-resolve # -l 表示只显示监听套接字 # -t 表示只显示 TCP 套接字 # -n 表示显示数字地址和端口(而不是名字) # -p 表示显示进程信息 $ ss -ltnp | head -n 3 State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=840,fd=13)) LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1459,fd=3))当套接字处于 Established 时：Recv-Q 表示套接字缓冲还没有被应用程序取走的字节数（即接收队列长度）。而 Send-Q 表示还没有被远端主机确认的字节数（即发送队列长度）。当套接字处于 Listening 时：Recv-Q 表示全连接队列的长度。而 Send-Q 表示全连接队列的最大长度。2.1.4 延迟ping 通过 ICMP 测试网络延迟。# -c3表示发送三次ICMP包后停止 $ ping -c3 114.114.114.114 PING 114.114.114.114 (114.114.114.114) 56(84) bytes of data. 64 bytes from 114.114.114.114: icmp_seq=1 ttl=54 time=244 ms 64 bytes from 114.114.114.114: icmp_seq=2 ttl=47 time=244 ms 64 bytes from 114.114.114.114: icmp_seq=3 ttl=67 time=244 ms --- 114.114.114.114 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2001ms rtt min/avg/max/mdev = 244.023/244.070/244.105/0.034 mshping3 通过 TCP 测试网络延迟。# -c表示发送3次请求，-S表示设置TCP SYN，-p表示端口号为80 $ hping3 -c 3 -S -p 80 baidu.com HPING baidu.com (eth0 123.125.115.110): S set, 40 headers + 0 data bytes len=46 ip=123.125.115.110 ttl=51 id=47908 sport=80 flags=SA seq=0 win=8192 rtt=20.9 ms len=46 ip=123.125.115.110 ttl=51 id=6788 sport=80 flags=SA seq=1 win=8192 rtt=20.9 ms len=46 ip=123.125.115.110 ttl=51 id=37699 sport=80 flags=SA seq=2 win=8192 rtt=20.9 ms --- baidu.com hping statistic --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 20.9/20.9/20.9 ms2.1.5 连接跟踪数$ sysctl -a | grep conntrack net.netfilter.nf_conntrack_count = 180 net.netfilter.nf_conntrack_max = 1000 net.netfilter.nf_conntrack_buckets = 65536 net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60 net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 120 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120 ...net.netfilter.nf_conntrack_count，表示当前连接跟踪数；2net.netfilter.nf_conntrack_max，表示最大连接跟踪数；net.netfilter.nf_conntrack_buckets，表示连接跟踪表的大小。2.1.6 路由traceroute 会在路由的每一跳发送三个包，并在收到响应后，输出往返延时。如果无响应或者响应超时（默认 5s），就会输出一个星号。# --tcp表示使用TCP协议，-p表示端口号，-n表示不对结果中的IP地址执行反向域名解析 $ traceroute --tcp -p 80 -n baidu.com traceroute to baidu.com (123.125.115.110), 30 hops max, 60 byte packets 1 * * * 2 * * * 3 * * * 4 * * * 5 * * * 6 * * * 7 * * * 8 * * * 9 * * * 10 * * * 11 * * * 12 * * * 13 * * * 14 123.125.115.110 20.684 ms * 20.798 ms2.1.7 DNS排查 DNS 解析问题。$ nslookup time.geekbang.org # 域名服务器及端口信息 Server: 114.114.114.114 Address: 114.114.114.114#53 # 非权威查询结果 Non-authoritative answer: Name: time.geekbang.org Address: 39.106.233.17dig 可展示递归查询的整个过程。# +trace表示开启跟踪查询 # +nodnssec表示禁止DNS安全扩展 $ dig +trace +nodnssec time.geekbang.org ; <<>> DiG 9.11.3-1ubuntu1.3-Ubuntu <<>> +trace +nodnssec time.geekbang.org ;; global options: +cmd . 322086 IN NS m.root-servers.net. . 322086 IN NS a.root-servers.net. . 322086 IN NS i.root-servers.net. . 322086 IN NS d.root-servers.net. . 322086 IN NS g.root-servers.net. . 322086 IN NS l.root-servers.net. . 322086 IN NS c.root-servers.net. . 322086 IN NS b.root-servers.net. . 322086 IN NS h.root-servers.net. . 322086 IN NS e.root-servers.net. . 322086 IN NS k.root-servers.net. . 322086 IN NS j.root-servers.net. . 322086 IN NS f.root-servers.net. ;; Received 239 bytes from 114.114.114.114#53(114.114.114.114) in 1340 ms org. 172800 IN NS a0.org.afilias-nst.info. org. 172800 IN NS a2.org.afilias-nst.info. org. 172800 IN NS b0.org.afilias-nst.org. org. 172800 IN NS b2.org.afilias-nst.org. org. 172800 IN NS c0.org.afilias-nst.info. org. 172800 IN NS d0.org.afilias-nst.org. ;; Received 448 bytes from 198.97.190.53#53(h.root-servers.net) in 708 ms geekbang.org. 86400 IN NS dns9.hichina.com. geekbang.org. 86400 IN NS dns10.hichina.com. ;; Received 96 bytes from 199.19.54.1#53(b0.org.afilias-nst.org) in 1833 ms time.geekbang.org. 600 IN A 39.106.233.176 ;; Received 62 bytes from 140.205.41.16#53(dns10.hichina.com) in 4 ms2.1.8 防火墙和 NAT配置 SNAT：$ iptables -t nat -A POSTROUTING -s 192.168.0.0/16 -j MASQUERADE $ iptables -t nat -A POSTROUTING -s 192.168.0.2 -j SNAT --to-source 100.100.100.100配置 DNAT：$ iptables -t nat -A PREROUTING -d 100.100.100.100 -j DNAT --to-destination 192.168.0.2查看 NAT：$ iptables -nL -t nat Chain PREROUTING (policy ACCEPT) target prot opt source destination DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL ... Chain DOCKER (2 references) target prot opt source destination RETURN all -- 0.0.0.0/0 0.0.0.0/0 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80802.1.9 抓包$ tcpdump -nn udp port 53 or host 35.190.27.188 -w ping.pcap-nn 表示不解析 IP 地址和端口号名称。然后用 Wireshark 打开 ping.pcap，能以更规整的格式看到抓包内容。2.1.10 内核协议栈跟踪$ stap --all-modules dropwatch.stp Monitoring for dropped packets其中，dropwatch.stp 为用户自己写的脚本。2.2 根据工具查指标ifconfig、ip：配置和查看网络接口。ss：查看网络连接数。sar：查看网络接口和网络收发情况。nethogs：查看进程的网络收发情况。iftop：查看 IP 的网络收发情况。ethool：查看和配置网络接口。conntrack：查看和管理连接跟踪情况。nslookup、dig：排查 DNS 解析问题。traceroute：查看路由并测试链路信息。ping、hping3：测试网络延迟。tcpdump：网络抓包工具。Wireshark：网络抓包和图形界面分析工具。iptables：配置管理防火墙及 NAT 规则。perf：剖析内核协议栈性能。systemtap：动态追踪内核协议栈行为。3 网络性能优化3.1 应用程序从网络 I/O 角度：I/O 多路复用技术 epoll。异步 I/O。从进程的工作模型：主进程 + 多个 work 子进程。监听相同端口的多进程模型。应用层的网络协议优化：长连接取代短连接。使用内存等方式缓存不常变化的数据。使用 Protocol Buffer 序列化等方式压缩网络 I/O 数据量。使用 DNS 缓存、预取、HTTPDNS 等方式减少 DNS 解析延迟。3.2 套接字调整缓冲区大小，比如：增大每个套接字的缓冲区大小 net.core.optmem_tax增大套接字接收缓冲区大小 net.core.rmem_max 和发送缓冲区大小 net.core.wmem_max增大 TCP 接收缓冲区大小 net.ipv4.tcp_rmem 和发送缓冲区大小 net.ipv4.tcp_wmem比如发送缓冲区大小，理想值是吞吐量 * 延迟。套接字接口也提供了一些配置项，用来改变网络连接行为：为 TCP 连接设置 TCP_NODELAY 后，可禁用 Nagle 算法。为 TCP 连接开启 TCP_CORK 后，可以让小包聚合成大包后再发送。使用 SO_SNDBUF 和 SO_RCVBUF，可分别调整套接字的发送和接收缓冲区大小。3.3 传输层请求量比较大时，可能会有大量连接处于 TIME_WAIT 状态，可采用如下措施：增大处于 TIME_WAIT 状态的连接数量 net.ipv4.tcp_max_tw_buckets，并增大连接跟踪表大小 net.netfilter.nf_conntrack_max减小 net.ipv4.tcp_fin_timeout 和 net.netfilter.nf_conntrack_tcp_timeout_time_wait，让系统尽快释放它们所占用的资源。开启端口复用 net.ipv4.tcp_tw_reuse，这样，被 TIME_WAIT 连接占用的端口还可以用到新的连接中。增大本地端口范围 net.ipv4.ip_local_port_range，可支持更多的连接，提供并发能力。增大最大文件描述符数量。可用 fs.nr_open 和 fs.file_max 分别增大进程和系统的最大文件描述符数量；或在应用程序的 systemd 配置文件中配置 LimitNOFILE，设置应用程序的最大文件描述符数量。为缓解 SYN Flood，可：增大 TCP 半连接的最大数量 net.ipv4.tcp_max_syn_backlog，或者开启 TCP SYN Cookie net.ipv4.tcp_syncookies （这两个选项不可同时使用）。减少 SYN_RECV 状态的连接重传 SYN + ACK 包的次数 net.ipv4.tcp_synack_tries在长连接场景中，系统默认的 Keepalive 探测间隔和重试次数一般无法满足应用程序的性能要求，需要进行优化：缩短最后一次数据包到 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_time缩短发送 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_intvl减少 Keepalive 探测包失败后，一直到通知应用程序前的重试次数 net.ipv4.keepalive_probes3.4 网络层从路由和转发角度，可调整如下内核选项：在需要转发的服务器中，比如 NAT 网关或使用 Docker 时，开启 IP 转发，设置 net.ipv4.ip_forward = 1调整数据包的生存周期 TTL，比如设置 net.ipv4.ip_default_ttl = 64。注意，增大该值会降低系统性能。开启数据包的反向地址校验，比如设置 net.ipv4.conf.eth0.rp_filter = 1。可防止 IP 欺骗，并减少伪造 IP 带来的 DDos。从分片的角度，主要是调整 MTU 大小。比如在使用 VXLAN、GRE 等叠加网络技术时，需调大 MTU。从 ICMP 出发，为避免 ICMP 主机探测、ICMP Flood 等问题，可限制 ICMP 行为：禁止 ICMP 协议，设置 net.ipv4.icmp_echo_ignore_all = 1禁止广播 ICMP，设置 net.ipv4.icmp_echo_ignore_broadcasts = 13.5 链路层网卡收到包后调用中断处理程序需要消耗大量 CPU，可将这些中断调度到不同的 CPU 上：为网卡硬中断配置 CPU 亲和性（smp_affinity），或者开启 irqbalance 服务。开启 RPS（Receive Packet Steering）和 RFS（Receive Flow Steering），将应用程序和软中断的处理调度到相同 CPU 上，这样就可以增加 CPU 缓存命中率，减少网络延迟。将内核中通过软中断处理的功能卸载到网卡中，通过硬件来执行；TSO（TCP Segmentation Offload）和 UFO（UDP Fragmentation Offload）：在 TCP/UDP 协议中直接发送大包；而 TCP 包的分段（按照 MSS 分段）和 UDP 的分片（按照 MTU 分片）功能由网卡完成。GSO（Generic Segmentation Offload）：在网卡不支持 TSO/UFO 时，将 TCP/UDP 包的分段延迟到进入网卡前再执行。不仅可以减少 CPU 的消耗，还可在发生丢包时只重传分段后的包。LRO（Large Receive Offload）：在接收 TCP 分段包时，由网卡将其组装合并后，再交给上层网络处理。但在需要 IP 转发的情况下，不能开启 LRO，因为多个包的头部信息不一致，LRO 合并会导致网络包的校验错误。GRO（Genetric Receive Offload）：GRO 修复了 LRO 的缺陷，并且更为通用，同时支持 TCP 和 UDP。RSS（Receive Side Scaling）：多队列接收，基于多个接收队列来分配网络接收进程，这样可以让多个 CPU 来处理收到的网络包。VXLAN 卸载：让网卡来完成 VXLAN 的组包功能。优化网络的吞吐量：开启网络接口的多队列功能。增大网络接口的缓冲区大小，以及队列长度等，提高网络吞吐量（可能导致延迟增大）。使用 Traffic Control 工具，为不同网络流量配置 QoS。参考倪鹏飞. Linux 性能优化实战.
案例篇：网络请求延迟变大了，我该怎么办？
1 现象$ wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/ Running 10s test @ http://192.168.0.30:8080/ 2 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 43.60ms 6.41ms 56.58ms 97.06% Req/Sec 1.15k 120.29 1.92k 88.50% Latency Distribution 50% 44.02ms 75% 44.33ms 90% 47.62ms 99% 48.88ms 22853 requests in 10.01s, 18.55MB read Requests/sec: 2283.31 Transfer/sec: 1.85MB在并发请求下，平均延迟为 43ms，延迟较大。2 排查使用 tcpdump 抓包：$ tcpdump -nn tcp port 8080 -w nginx.pcap用 Wireshark 打开 nginx.pcap。由于网络包数量较多，可以过滤一下，单击右键并选择 “Follow” -> “TCP Stream”。为了更加直观，继续点击菜单栏里的 Statics -> Flow Graph，选中 “Limit to display filter” 并设置 Flow type 为 “TCP Flows”：第二次 HTTP 请求较慢，特别是客户端在收到服务器第一个分组后，40ms 后才发出了 ACK 响应（图中蓝色行）。40ms 是 TCP 延迟确认（Delayed ACK）的最小超时时间。这是 TCP 针对 ACK 的优化机制，不用每次请求都发送一个 ACK，而是等一会（比如 40ms），看有没有“顺风车”，如果恰好有其它包需要发送，则捎带着 ACK 一起发送过去，否则就超时后发送。因为 40ms 延迟发生在了客户端 wrk，所以有可能是客户端开启了延迟确认。执行 man tcp，会发现只有 TCP 套接字专门设置了 TCP_QUICK 才会开启快速确认模式：TCP_QUICKACK (since Linux 2.4.4) Enable quickack mode if set or disable quickack mode if cleared. In quickack mode, acks are sent immediately, rather than delayed if needed in accordance to normal TCP operation. This flag is not permanent, it only enables a switch to or from quickack mode. Subsequent operation of the TCP protocol will once again enter/leave quickack mode depending on internal protocol processing and factors such as delayed ack timeouts occurring and data transfer. This option should not be used in code intended to be portable.利用 strace 查看 wrk 为套接字设置了哪些选项：$ strace -f wrk --latency -c 100 -t 2 --timeout 2 http://192.168.0.30:8080/ ... setsockopt(52, SOL_TCP, TCP_NODELAY, [1], 4) = 0 ...可以看到并没有设置 TCP_QUICKACK 选项。但是服务端不应该受客户端的影响，回到 Wireshark 重新观察：1173 是之前看到的 ACK 延迟包，697 包和 1175 包共同组成一个 HTTP 响应（ACK 都是 85）。第二个分组 1175 没有和前一个分组 697 一起发送，而是等 697 的 ACK 后才发送了 1175，这跟延迟确认比较像，只是不是 ACK，而是数据包。这里可以想到 Nagle 算法，通过合并小包来提高带宽利用率。Nagle 算法规定，一个 TCP 连接上最多只有一个未被确认的未完成分组；在收到这个分组的 ACK 前不发送别的分组。这些小分组会被组合起来，在收到 ACK 后一起发送出去。Nagle 算法跟客户端的默认延迟确认机制结合起来，就会导致延迟较大。man tcp，会看到只有设置了 TCP_NODELAY 后才会禁用 Nagle 算法：TCP_NODELAY If set, disable the Nagle algorithm. This means that segments are always sent as soon as possible, even if there is only a small amount of data. When not set, data is buffered until there is a sufficient amount to send out, thereby avoiding the frequent sending of small packets, which results in poor uti‐ lization of the network. This option is overridden by TCP_CORK; however, setting this option forces an explicit flush of pending output, even if TCP_CORK is currently set.查看 nginx 配置：$ cat /etc/nginx/nginx.conf | grep tcp_nodelay tcp_nodelay off;将其设置为 on 应该就可以解决了。重新测试后发现延迟已缩短到了 9ms。参考倪朋飞. Linux 性能优化实战.
Content: # 1 NAT 原理NAT 可以重新 IP 数据包的源 IP 或目的 IP，用来解决公网中 IP 地址短缺问题。原理是内网中的多个机器共用一个公网 IP 来访问外网。SNAT 和 DNAT 示例：2 iptables 与 NATLinux 支持 4 种表，包括 filter（用于过滤）、nat（用于 NAT）、mangle（用于修改分组数据） 和 raw（用于原始数据包）等。nat 表中内置了三个链：PREROUTING，用于路由判断前所执行的规则，比如，对接收到的包进行 DNAT。POSTROUTIG，用于路由判断后执行的规则，比如，对发送或转发的数据包进行 SNAT 或 MASQUERADE。OUTPUT，类似 PREROUTING，但只处理从本机发送出去的包。3 管理 NAT 规则在使用 iptables 配置 NAT 规则时，Linux 需要转发来自其它 IP 的网络包，所以需要开启 Linux 的 IP 转发功能，查看是否开启：$ sysctl net.ipv4.ip_forward net.ipv4.ip_forward = 1输出结果是 1，表示已经开启。如果没有开启，可以手动开启：$ sysctl -w net.ipv4.ip_forward=1 net.ipv4.ip_forward = 1写到 /etc/sysctl.conf 文件中后，重启仍然会生效。3.1 SNAT为一个子网统一配置 SNAT，并由 Linux 选择默认的出口 IP：$ iptables -t nat -A POSTROUTING -s 192.168.0.0/16 -j MASQUERADE为具体的 IP 地址配置 SNAT，并指定转换后的源地址：$ iptables -t nat -A POSTROUTING -s 192.168.0.2 -j SNAT --to-source 100.100.100.1003.2 DNAT$ iptables -t nat -A PREROUTING -d 100.100.100.100 -j DNAT --to-destination 192.168.0.24 现象压测场景下，并发请求数大大降低：# -c 表示并发请求数为5000，-n 表示总的请求数为 10 万 # -r 表示套接字接收错误时仍然继续执行，-s 表示设置每个请求的超时时间为 30s $ ab -c 5000 -n 10000 -r -s 30 http://192.168.0.30:8080/ ... Requests per second: 76.47 [#/sec] (mean) Time per request: 65380.868 [ms] (mean) Time per request: 13.076 [ms] (mean, across all concurrent requests) Transfer rate: 44.79 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 1300 5578.0 1 65184 Processing: 0 37916 59283.2 1 130682 Waiting: 0 2 8.7 1 414 Total: 1 39216 58711.6 1021 130682 ...每秒 74 个请求，每个请求延迟为 65s5 排查本文从 NAT 的角度排查。NAT 正常工作至少需要两个步骤：利用 Netfilter 中的钩子函数（hook）修改源地址后目标地址。利用连接跟踪模块 conntrack 关联同一个连接的请求和响应。SystemTap 是 Linux 的一种动态追踪框架，它把用户提供的脚本转换为内核模块来执行，用来检测和跟踪内核的行为。创建一个 dropwatch.stp 脚本：#! /usr/bin/env stap ############################################################ # Dropwatch.stp # Author: Neil Horman <nhorman@redhat.com> # An example script to mimic the behavior of the dropwatch utility # http://fedorahosted.org/dropwatch ############################################################ # Array to hold the list of drop points we find global locations # Note when we turn the monitor on and off probe begin { printf("Monitoring for dropped packets\n") } probe end { printf("Stopping dropped packet monitor\n") } # increment a drop counter for every location we drop at probe kernel.trace("kfree_skb") { locations[$location] <<< 1 } # Every 5 seconds report our drop locations probe timer.sec(5) { printf("\n") foreach (l in locations-) { printf("%d packets dropped at %s\n", @count(locations[l]), symname(l)) } delete locations }执行 stap 命令，运行丢包跟踪脚本：$ stap --all-modules dropwatch.stp Monitoring for dropped packets切换终端再次执行 ab 压测命令后，观察 stap 的输出：10031 packets dropped at nf_hook_slow 676 packets dropped at tcp_v4_rcv 7284 packets dropped at nf_hook_slow 268 packets dropped at tcp_v4_rcv大量的丢包发生在 nf_hook_slow 上，这是在 Netfilter Hook 的钩子函数中出现丢包问题了，但还不能是否是 NAT。接下来使用 perf 跟踪 nf_hook_slow 的执行过程。再次执行 ab 命令后，切换终端执行 perf：# 记录一会（比如30s）后按Ctrl+C结束 $ perf record -a -g -- sleep 30 # 输出报告 $ perf report -g graph,0在 perf report 界面中查找 hf_hook_slow，再展开调用堆栈，可以看到如下调用图：可以看到 nf_hook_slow 调用最多的有 3 个地方，分别是 ipv4_conntrack_in、br_nf_pre_routing 以及 iptable_nat_ipv4_in。说明 nf_hook_slow 主要执行 3 个动作：接收网络包时，在连接跟踪表中查找连接，并为新的连接分配跟踪对象。在网桥中转发包。这是因为本文中的 Nginx 运行在容器中，而容器的网络通过网桥实现。接收网络包时，执行 DNAT，即把 8080 端口收到的包转发给容器。以上是性能下降的 3 个来源，而这 3 个来源都是 Linux 内核机制，所以接下来的优化从内核入手。DNAT 的基础是 conntrack，查看内核提供了哪些 conntrack 配置项：$ sysctl -a | grep conntrack net.netfilter.nf_conntrack_count = 180 net.netfilter.nf_conntrack_max = 1000 net.netfilter.nf_conntrack_buckets = 65536 net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60 net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 120 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120 ...最重要的 3 个指标：net.netfilter.nf_conntrack_count，表示当前连接跟踪数，为 180；net.netfilter.nf_conntrack_max，表示最大连接跟踪数，为 1000；net.netfilter.nf_conntrack_buckets，表示连接跟踪表的大小，为 65535。ab 命令中，并发请求数是 5000，请求数是 100000，而跟踪表中只记录 1000 个连接，是远远不够的。实际上，内核异常时，会将异常信息记录到日志中，执行 dmesg 即可看到：$ dmesg | tail [104235.156774] nf_conntrack: nf_conntrack: table full, dropping packet [104243.800401] net_ratelimit: 3939 callbacks suppressed [104243.800401] nf_conntrack: nf_conntrack: table full, dropping packet [104262.962157] nf_conntrack: nf_conntrack: table full, dropping packet可以适当调大连接跟踪数（连接跟踪数过大会耗费内存）：$ sysctl -w net.netfilter.nf_conntrack_max=131072 $ sysctl -w net.netfilter.nf_conntrack_buckets=65536重新执行 ab 命令后，结果比之前好了很多。也可以查看连接跟踪表内容：# -L 表示列表，-o 表示以扩展格式显示 $ conntrack -L -o extended | head ipv4 2 tcp 6 7 TIME_WAIT src=192.168.0.2 dst=192.168.0.96 sport=51744 dport=8080 src=172.17.0.2 dst=192.168.0.2 sport=8080 dport=51744 [ASSURED] mark=0 use=1 ipv4 2 tcp 6 6 TIME_WAIT src=192.168.0.2 dst=192.168.0.96 sport=51524 dport=8080 src=172.17.0.2 dst=192.168.0.2 sport=8080 dport=51524 [ASSURED] mark=0 use=1统计连接：# 统计总的连接跟踪数 $ conntrack -L -o extended | wc -l 14289 # 统计TCP协议各个状态的连接跟踪数 $ conntrack -L -o extended | awk '/^.*tcp.*$/ {sum[$6]++} END {for(i in sum) print i, sum[i]}' SYN_RECV 4 CLOSE_WAIT 9 ESTABLISHED 2877 FIN_WAIT 3 SYN_SENT 2113 TIME_WAIT 9283 # 统计各个源IP的连接跟踪数 $ conntrack -L -o extended | awk '{print $7}' | cut -d "=" -f 2 | sort | uniq -c | sort -nr | head -n 10 14116 192.168.0.2 172 192.168.0.96大部分的 TCP 连接都处于 TIME_WAIT 状态，它们大部分来源于 192.168.0.2 这个 IPTIME_WAIT 的连接跟踪记录，会在超时后清理，而默认的超时时间是 120s：$ sysctl net.netfilter.nf_conntrack_tcp_timeout_time_wait net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120参考倪朋飞. Linux 性能优化实战.
案例篇：怎么缓解 DDoS 攻击带来的性能下降问题？
1 现象# -w 表示只输出 HTTP 状态码及总时间， # -o表 示将响应重定向到 /dev/null # --connect-timeout 表示连接超时时间 $ curl -w 'Http code: %{http_code}\nTotal time:%{time_total}s\n' -o /dev/null --connect-timeout 10 http://192.168.0.30 ... Http code: 000Total time:10.001scurl: (28) Connection timed out after 10000 milliseconds客户端连接超时，且没有收到服务端的响应。2 排查查看网络：$ sar -n DEV 1 08:55:49 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil 08:55:50 docker0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 08:55:50 eth0 22274.00 629.00 1174.64 37.78 0.00 0.00 0.00 0.02 08:55:50 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00网络接收的 PPS 已经达到了 20000 多，但是 BPS 却只有 1174 kB，这样每个包的大小就只有 54（1174*1024/22274=54）。用 tcpdump 查看小包是什么：# -i eth0 只抓取eth0网卡， # -n 不解析协议名和主机名 # tcp port 80 表示只抓取 tcp 协议并且端口号为 80 的网络帧 $ tcpdump -i eth0 -n tcp port 80 09:15:48.287047 IP 192.168.0.2.27095 > 192.168.0.30: Flags [S], seq 1288268370, win 512, length 009:15:48.287050 IP 192.168.0.2.27131 > 192.168.0.30: Flags [S], seq 2084255254, win 512, length 009:15:48.287052 IP 192.168.0.2.27116 > 192.168.0.30: Flags [S], seq 677393791, win 512, length 009:15:48.287055 IP 192.168.0.2.27141 > 192.168.0.30: Flags [S], seq 1276451587, win 512, length 009:15:48.287068 IP 192.168.0.2.27154 > 192.168.0.30: Flags [S], seq 1851495339, win 512, length 0 ...都是 SYN 包，可见是受到了 SYN Flood 攻击。查看 SYN 包的数量：$ netstat -n -p | grep SYN_REC | wc -l 1933 缓解 DDos 攻击限制单个攻击源：# 限制syn并发数为每秒1次 $ iptables -A INPUT -p tcp --syn -m limit --limit 1/s -j ACCEPT # 限制单个IP在60秒新建立的连接数为10 $ iptables -I INPUT -p tcp --dport 80 --syn -m recent --name SYN_FLOOD --update --seconds 60 --hitcount 10 -j REJECT对 TCP 优化：$ cat /etc/sysctl.conf # 降低连接每个 SYN_RECV 失败时的默认的重试次数 sysctl -w net.ipv4.tcp_synack_retries=1 # 开启 SYN Cookies，不需要维护半开连接状态 sysctl -w net.ipv4.tcp_syncookies=1要使 /etc/sysctl.conf 生效，需执行 sysctl -p 命令。写到 /etc/sysctl.conf 中后，重启仍然会生效。参考倪朋飞. Linux 性能优化实战.