1. 执行 wrk 命令,来测试 Nginx 的性能
可以看到吞吐量(也就是每秒请求数)只有 189,并且所有 1910 个请求收到的都是异常响应(非 2xx 或 3xx)。
数据显然表明,吞吐量太低了,并且请求处理都失败。
以下开始查找原因。
2. 使用ss查看 TCP 连接数的汇总情况
成功建立连接数太少了,是 timewait 状态太多了。
3. 查看系统系统日志
4. 查看连接跟踪数
发现最大值太小了,改大:
wrk 命令,重新测试 Nginx 的性能:
真正成功的只有 8000 多个(54221-45577=8644)。大部分请求的响应都是异常的
5. 查询 Nginx 容器日志
499表示服务器端还没来得及响应时,客户端就已经关闭连接了。
问题在于服务器端处理太慢,客户端因为超时(wrk 超时时间为 2s),主动断开了连接。
6. 查询 PHP 容器日志
两条警告信息,server reached max_children setting (5),并建议增大 max_children。
max_children 表示 php-fpm 子进程的最大数量,每个 php-fpm 子进程可能会占用 20 MB 左右的内存。
把它设置成了 20。重新测试 Nginx 的性能:
4000 多的吞吐量显然还是比较差的,并且大部分请求的响应依然还是异常。
7. 观察有没有发生套接字的丢包现象
有大量的套接字丢包,并且丢包都是套接字队列溢出导致的。
8. 查看套接字的队列大小 ss -ltnp
php-fpm 也已经接近了最大值。很明显,套接字监听队列的长度太小了,需要增大。
把 Nginx 和 php-fpm 的队列长度增大到8192,而把 somaxconn 增大到 65536。
重新测试 Nginx 的性能:
同时,在另一终端重新执行 netstat -s | grep socket
已经没有套接字丢包问题了。但是,Nginx 的响应,再一次全部失败了
9. 再次查看 Nginx 日志
这个错误消息对应的错误代码为 EADDRNOTAVAIL,表示 IP 地址或者端口号不可用。
10. 端口号优化
查询系统配置的临时端口号范围:
端口的范围只有 50 个,显然太小了 。把端口号范围扩展为 “10000 65535”:
再次测试性能:
异常的响应少了,但 出现了很多 Socket read errors。
11. 执行top,观察CPU和内存的变化
两个 CPU 的系统 CPU 使用率都接近 50%,Nginx 进程和两个 docker 相关的进程占用,使用率都是 30% 左右。
瓶颈在CPU。
12. 火焰图,寻找热点函数
do_syscall_64、tcp_v4_connect、inet_hash_connect 这个堆栈,很明显就是最需要关注的地方。
inet_hash_connect() 是Linux 内核中负责分配临时端口号的函数。
瓶颈应该还在临时端口的分配上。
再顺着 inet_hash_connect 往堆栈上面查看,下一个热点是 __init_check_establishe函数。
这个函数的目的,是检查端口号是否可用。
说明有大量连接占用着端口,查端口号可用的函数,消耗更多的 CPU。
13. ss 命令, 查看连接状态统计
有大量连接(这儿是 32768)处于 timewait 状态,而 timewait 状态的连接,本身会继续占用端口号。
如果这些端口号可以重用,那么自然就可以缩短 __init_check_established 的过程。
14. 设置端口重用
查询状态:
把它设置成 1 就可以开启。
输入: net.ipv4.tcp_tw_reuse = 1
再次测试优化后的效果:
吞吐量已经达到了 5000 多,并且只有少量的 Socket errors,也不再有 Non-2xx or 3xx 的响应了。
说明一切终于正常了。