高并发系统(C10K, C100K)

本篇只讨论OS级别的并发优化,不包括语言层面对并发的支持特性

有关Linux系统的限制

系统最大文件数

使用ulimit设置最多能打开文件数:

  • 查看所有的限制: ulimit -a
  • 设置最大打开文件句柄数: ulimit -n 65535, 65535 = 2^16 = 0xFFFF
  • 设置 每个用户的 最大进程数: ulimit -u 32768
  • 设置线程栈的大小: ulimit -s 10240
  • 设置最大线程数数: ulimit -T (在Unix上可能不同)
  • 设置产生core文件大小: ulimit -c xxx
  • 不限制core的大小: ulimit -c unlimited

ulimit起作用的范围是”当前Shell”, 并不是作用于”当前用户”, 如要对”用户”级别做限制, 则需要修改系统文件 /etc/security/limits.conf:

# * 表示所有用户, nofile表示限制文件打开数, 限制在100
* hard nofile 100

如果是针对整个系统, 则需要使用sysctl修改, 命令格式为: sysctl -w fs.nr_open=10000000, 每个系统参数对应一个/proc下的文件, fs.nr_open对应的文件路径是/proc/sys/fs/nr_open
系统最大打开文件数相关的参数有两个:

  • fs.nr_open,进程级别
  • fs.file-max,系统级别

至此总结一下, “Linux系统最多能打开文件数”有当前shell, 用户, 系统三个级别,
shell级别的更改限制命令是ulimit,
更改系统级别限制的命令是sysctl, 限制优先级最大的当然是fs.file-max, 假如fs.file-max设置为100万, ulimit是不能超过100万的.

cat /proc/sys/fs/file-nr, 输出 9344 0 592026,分别为:1.已经分配的文件句柄数,2.已经分配但没有使用的文件句柄数,3.最大文件句柄数
file-nr不是单个进程的限制, 是系统级的, 最后一个数字与file-max相同

此外, sysctl能修改的其他系统参数包括:

  • net.ipv4.ip_local_port_range = 1024 65000 //允许系统打开的端口范围, 操作系统上端口号1024以下是系统保留的,从1024-65535是用户使用的
  • net.core.netdev_max_backlog = 262144 // 每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目
  • net.ipv4.tcp_max_tw_buckets = 6000 //timewait 的数量,默认是180000。
  • net.ipv4.tcp_tw_recycle = 1 // 启用timewait 快速回收
  • net.ipv4.tcp_tw_reuse = 1 // 开启重用。允许将TIME-WAIT sockets 重新用于新的TCP 连接。
  • net.ipv4.tcp_syncookies = 1 // 开启SYN Cookies,当出现SYN 等待队列溢出时,启用cookies 来处理。
  • net.ipv4.tcp_max_orphans = 262144 //系统所能处理不属于任何进程的 socket数量,当我们需要快速建立大量连接时,就需要关注下这个值了. 这个限制仅仅是为了防止简单的DoS 攻击,不能过分依靠它或者人为地减小这个值,更应该增加这个值(如果增加了内存之后)。
  • net.ipv4.tcp_max_syn_backlog = 262144 // 记录的那些尚未收到客户端确认信息的连接请求的最大值。对于有128M 内存的系统而言,缺省值是1024,小内存的系统则是128。
  • net.ipv4.tcp_timestamps = 0 //时间戳可以避免序列号的卷绕。一个1Gbps 的链路肯定会遇到以前用过的序列号。时间戳能够让内核接受这种“异常”的数据包。这里需要将其关掉。
  • net.ipv4.tcp_synack_retries = 1 //为了打开对端的连接,内核需要发送一个SYN 并附带一个回应前面一个SYN 的ACK。也就是所谓三次握手中的第二次握手。这个设置决定了内核放弃连接之前发送SYN+ACK 包的数量。
  • net.ipv4.tcp_syn_retries = 1 // 在内核放弃建立连接之前发送SYN 包的数量。
  • net.ipv4.tcp_fin_timeout = 1 //如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2 状态的时间。对端可以出错并永远不关闭连接,甚至意外当机。缺省值是60 秒。2.2 内核的通常值是180 秒,3你可以按这个设置,但要记住的是,即使你的机器是一个轻载的WEB 服务器,也有因为大量的死套接字而内存溢出的风险,FIN- WAIT-2 的危险性比FIN-WAIT-1 要小,因为它最多只能吃掉1.5K 内存,但是它们的生存期长些。
  • net.ipv4.tcp_keepalive_time = 30 //当keepalive 起用的时候,TCP 发送keepalive 消息的频度。缺省是2 小时。
  • net.ipv4.tcp_mem = 786432 2097152 3145728 // tcp的内存大小,其单位是页, 当超过第二个值时,TCP进入 pressure模式,此时TCP尝试稳定其内存的使用,当小于第一个值时,就退出pressure模式。当内存占用超过第三个值时,TCP就拒绝分配 socket了
  • net.ipv4.tcp_rmem = 4096 4096 16777216 # 读缓冲的大小, 三个分别是最小/默认/最大值
  • net.ipv4.tcp_wmem = 4096 4096 16777216

永久生效方法:在/etc/sysctl.conf中添加

如何查看已创建文件描述符数?

  • 某进程打开文件数 ll /proc/1599/fd | wc -l
  • 某进程打开socket的数量: ll /proc/1599/fd | grep socket | wc -l # nginx一个worker打开了200-300个socket
  • 系统全部打开的文件数 lsof | wc -l
  • 系统全部打开的TCP连接数 lsof | grep TCP | wc -l
  • 查看tcp不同状态连接数: netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
    • 注意处于TIME_WAIT的链接, 如果这个数过高会占用大量连接, 应该调整参数尽快的释放time_wait连接

在Nginx机器上测试:

[@zw_85_63 ~]# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
TIME_WAIT 37968
SYN_SENT 1
FIN_WAIT1 5
FIN_WAIT2 4
ESTABLISHED 2725
SYN_RECV 18
LAST_ACK 4

系统最大进线程数

某些服务程序(Apache, Tomcat) 采用 “Thread Per Request”, 系统的进线程最大数也会影响并发性能.

Linux 没有 直接限制 每个进程能够创建线程数, 仅限制了系统最大进线程数, 相关的配置有 :

  • 仅对当前 shell有效: ulimit -u 102400, -u表示 “max user processes”;
  • 系统级别有效:
    1. 临时生效: echo 102400 > /proc/sys/kernel/threads-maxsysctl -w sys.kernel.threads-max=10240 ;
    2. 永久生效: 修改 /etc/sysctl.conf 文件;

这里的 threads-max 不是指 进程, 是 “maximum number of threads that can be created using fork()”,
参考 @Ref https://www.kernel.org/doc/Documentation/sysctl/kernel.txt

每个进程能创建的最大线程数, 是由 total virtual memory 和stack size 共同决定的, number of threads = total virtual memory / stack size
这两个参数分别用 ulimit -v xxxulimit -s xxx设置

此外系统能创建最大进程数还受 kernel.pid_max影响:

  • 方式1 运行时限制,临时生效 echo 999999 > /proc/sys/kernel/pid_max
  • 方式2 修改/etc/sysctl.conf,永久生效 sys.kernel.pid_max = 999999

应用程序的设置

根据不同程序不同IO模型, 设置的思路也不一样

BIO:Apache/Tomcat, 主要是并发量要求不高的场景
NIO:Nginx/Redis, 主要是高并发量要求的场景
Redis使用单线程的I/O复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll、kqueue和select。对于单纯只有I/O操作来说,单线程可以将速度优势发挥到最大。
但是Redis也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型实际会严重影响整体吞吐量,CPU计算过程中,整个I/O调度都是被阻塞住的,

nginx

  • worker_connections= #每个worker线程能创建的连接数
  • upstream可以使用http 1.1的keepalive #与后端服务器创建的连接池大小
  • worker_processes 8; # nginx进程数,一般等于cpu core数量
  • worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000; # 每个进程分配到cpu的core上
  • worker_rlimit_nofile 65535; # 一个nginx进程打开的最多文件描述符数目

redis

@TODO

apache

默认是多进程同步处理request, 所以思路和Nginx每个Core一个进程epoll轮询的方式不同, apache应该增加”系统创建进程数上限”, 并且减小进程栈内存

tomcat

  • maxThreads=500 最大线程数,此值限制了bio的最大连接数
    • 一般的当一个进程有500个线程在跑的话,那性能已经是很低很低了。Tomcat默认配置的最大请求数是150。当某个应用拥有250个以上并发的时候,应考虑应用服务器的集群。
  • maxConnection=8192: 使用nio或者apr时,最大连接数受此值影响。

参考 聊下并发和Tomcat线程数(Updated) - zhanjindong - 博客园 @Ref

FastCGI

  • fastcgi_connect_timeout 300;

高并发配置-无废话总结

  1. 应用程序的并发设置: 主要是timeout, 进/线程数这几类参数
  2. 操作系统打开文件数量限制: ulimit -n单个Shell环境的限制, sysctl -w fs.file-max修改系统打开文件限制
  3. 操作系统打开端口数量限制: 最大端口数65535(2^16), 但1024以后的端口是给系统用的
  4. sysctl修改的TCP协议栈参数

并发性能测试工具

ab(Apache Bench)

1000并发, 总共20000次请求: ab -n 20000 -c 1000 <url>

http_load

30个并发线程, 共60秒测试: http_load -p 30 -s 60 Url.txt

JMeter

配置

@TODO

测试

@TODO

报告

在聚合报告中,会显示一行数据,共有10个字段,含义分别如下。

  • Label:每个 JMeter 的 element(例如 HTTP Request)都有一个 Name 属性,这里显示的就是 Name 属性的值
  • #Samples:表示你这次测试中一共发出了多少个请求,如果模拟10个用户,每个用户迭代10次,那么这里显示100
  • Average:平均响应时间——默认情况下是单个 Request 的平均响应时间,当使用了 Transaction Controller 时,也可以以Transaction 为单位显示平均* 响应时间
  • Median:中位数,也就是 50% 用户的响应时间
  • 90% Line:90% 用户的响应时间
  • Min:最小响应时间
  • Max:最大响应时间
  • Error%:本次测试中出现错误的请求的数量/请求的总数
  • Throughput:吞吐量——默认情况下表示每秒完成的请求数(Request per Second)
  • KB/Sec:每秒从服务器端接收到的数据量,相当于LoadRunner中的Throughput/Sec

参考: 使用JMeter进行负载测试——终极指南 - ImportNew @Ref

wrk

wg/wrk: Modern HTTP benchmarking tool

参考 @Ref